feat: update styling and layout for SponsorQuickStartPanel and shared forms; enhance responsiveness and accessibility

This commit is contained in:
Supra4E8C
2026-06-24 23:41:17 +08:00
parent 88d4d2b639
commit fdd2f991ab
4 changed files with 430 additions and 271 deletions
@@ -15,37 +15,35 @@
.header {
display: flex;
flex-direction: column;
flex-direction: row;
align-items: flex-start;
gap: 10px;
min-width: 0;
}
.topLink {
display: inline-flex;
align-items: center;
gap: 7px;
gap: 6px;
min-width: 0;
max-width: 100%;
padding: 7px 10px;
border: 1px solid var(--border-color);
height: 30px;
padding: 0 8px;
border: 1px solid transparent;
border-radius: var(--radius-md);
background: var(--bg-primary);
color: var(--text-primary);
background: transparent;
color: var(--muted-foreground);
font-size: 13px;
font-weight: 600;
line-height: 1.45;
line-height: 1;
text-decoration: none;
overflow-wrap: anywhere;
transition:
background-color $transition-fast,
border-color $transition-fast,
transform $transition-fast;
color $transition-fast;
&:hover {
border-color: var(--border-hover);
background: var(--bg-hover);
transform: translateY(-1px);
color: var(--text-primary);
}
&:focus-visible {
@@ -77,7 +75,8 @@
.titleRow {
display: flex;
align-items: flex-start;
align-items: center;
flex-wrap: wrap;
gap: 12px;
min-width: 0;
}
@@ -145,6 +144,10 @@
line-height: 1.25;
}
.titleRow {
gap: 10px;
}
.footer {
justify-content: stretch;
@@ -90,17 +90,16 @@ export function SponsorQuickStartPanel({
<div className={styles.titleText}>
<h2 className={styles.title}>{t('providersPage.providerNames.apikeyFun')}</h2>
</div>
<a
className={`${styles.topLink} ${resource ? '' : styles.topLinkEmphasis}`.trim()}
href={actionHref}
target="_blank"
rel="noreferrer"
>
<IconExternalLink size={14} />
<span>{actionLabel}</span>
</a>
</div>
<a
className={`${styles.topLink} ${resource ? '' : styles.topLinkEmphasis}`.trim()}
href={actionHref}
target="_blank"
rel="noreferrer"
>
<IconExternalLink size={14} />
<span>{actionLabel}</span>
</a>
</div>
<SponsorProviderForm
@@ -4,6 +4,7 @@ import { Collapsible } from '@/components/ui/Collapsible';
import { Select } from '@/components/ui/Select';
import {
IconAlertTriangle,
IconChevronDown,
IconCheckCircle2,
IconDollarSign,
IconDownload,
@@ -14,6 +15,7 @@ import {
IconX,
} from '@/components/ui/icons';
import { hasDisableAllModelsRule } from '@/components/providers/utils';
import { maskApiKey } from '@/utils/format';
import type { ModelInfo } from '@/utils/models';
import {
APIKEY_FUN_BASE_URL_OPTIONS,
@@ -352,8 +354,16 @@ function SponsorKeyEntryCard({
}: SponsorKeyEntryCardProps) {
const { t, i18n } = useTranslation();
const [showApiKey, setShowApiKey] = useState(false);
const [expanded, setExpanded] = useState(
() => mode === 'create' || !entry.existingApiKey?.trim()
);
const endpointUrl = protocolUrlForEntry(entry);
const protocolLabel = t(`providersPage.sponsor.protocols.${protocolI18nKey(entry.protocol)}`);
const titleLabel = t('providersPage.sponsor.groupedKey', { index: index + 1 });
const summaryKey = entry.apiKey.trim() || entry.existingApiKey?.trim() || '';
const summaryKeyLabel = summaryKey
? maskApiKey(summaryKey)
: t('providersPage.status.notConfigured');
const modelKey = protocolModelI18nKey(entry.protocol);
const usageMessages = useMemo<SponsorUsageMessages>(
() => ({
@@ -412,268 +422,306 @@ function SponsorKeyEntryCard({
return (
<div className={styles.entryCard}>
<div className={styles.entryCardHeader}>
<div className={styles.sponsorGroupTitle}>
<span>{t('providersPage.sponsor.groupedKey', { index: index + 1 })}</span>
<strong>{protocolLabel}</strong>
</div>
<button
type="button"
className={styles.removeBtn}
onClick={onRemove}
disabled={mutating || !canRemove}
title={t('providersPage.sponsor.removeGroupedKey')}
aria-label={t('providersPage.sponsor.removeGroupedKey')}
className={styles.entryCardToggle}
aria-expanded={expanded}
onClick={() => setExpanded((value) => !value)}
>
<IconX size={12} />
</button>
</div>
<div className={styles.field}>
<label className={styles.label} htmlFor={`${formId}-group-${index}-protocol`}>
{t('providersPage.sponsor.protocol')}
</label>
<Select
id={`${formId}-group-${index}-protocol`}
value={entry.protocol}
options={protocolOptions}
onChange={(value) =>
updateEntry({ protocol: value as SponsorProtocol, models: [emptyModel()] })
}
disabled={mutating}
ariaLabel={t('providersPage.sponsor.protocol')}
/>
<span className={styles.labelHint}>{t('providersPage.sponsor.protocolHint')}</span>
</div>
<div className={styles.field}>
<span className={styles.label}>{t('providersPage.sponsor.urlMode')}</span>
<div className={styles.sponsorUrlOptions} role="radiogroup">
{APIKEY_FUN_BASE_URL_OPTIONS.map((option) => {
const checked = resolveApiKeyFunBaseUrl(entry.baseUrl) === option.baseUrl;
const className = [
styles.sponsorUrlOption,
checked ? styles.sponsorUrlOptionActive : '',
]
.filter(Boolean)
.join(' ');
return (
<label key={option.id} className={className}>
<input
type="radio"
name={`${formId}-group-${index}-base-url`}
value={option.baseUrl}
checked={checked}
onChange={() => updateEntry({ baseUrl: option.baseUrl })}
disabled={mutating}
/>
<span className={styles.sponsorUrlOptionText}>
<span>{t(`providersPage.sponsor.urlOptions.${option.id}`)}</span>
<small>{option.baseUrl}</small>
</span>
</label>
);
})}
</div>
<span className={styles.labelHint}>{t('providersPage.sponsor.urlHint')}</span>
</div>
<div className={styles.sponsorProtocolCard}>
<span className={styles.sponsorProtocolName}>
{t('providersPage.sponsor.protocolEndpoint')}
</span>
<span className={styles.sponsorProtocolUrl}>{endpointUrl}</span>
</div>
<div className={styles.field}>
<label className={styles.label} htmlFor={`${formId}-group-${index}-api-key`}>
{t('providersPage.form.apiKey')}
</label>
<div className={styles.passwordField}>
<input
id={`${formId}-group-${index}-api-key`}
className={styles.passwordInput}
type={showApiKey ? 'text' : 'password'}
value={entry.apiKey}
onChange={(event) => updateEntry({ apiKey: event.target.value })}
autoComplete="new-password"
data-1p-ignore="true"
data-lpignore="true"
data-bwignore="true"
placeholder={
mode === 'edit'
? t('providersPage.form.apiKeyEditPlaceholder')
: t('providersPage.form.apiKeyCreatePlaceholder')
}
disabled={mutating}
/>
<button
type="button"
className={styles.passwordToggle}
onClick={() => setShowApiKey((value) => !value)}
disabled={mutating}
aria-label={
showApiKey ? t('providersPage.form.hideApiKey') : t('providersPage.form.showApiKey')
}
title={
showApiKey ? t('providersPage.form.hideApiKey') : t('providersPage.form.showApiKey')
}
>
{showApiKey ? <IconEyeOff size={16} /> : <IconEye size={16} />}
</button>
</div>
<span className={styles.labelHint}>{t('providersPage.sponsor.apiKeyHint')}</span>
</div>
<div className={styles.sponsorUsageSection}>
<button
type="button"
className={styles.connectivityBtn}
onClick={() => void usageCheck.run()}
disabled={mutating || usageCheck.isLoading}
>
{usageCheck.isLoading ? (
<IconLoader2 className={styles.statusIconLoading} size={14} />
) : (
<IconDollarSign size={14} />
)}
<span>
{usageCheck.isLoading
? t('providersPage.sponsor.usageChecking')
: t('providersPage.sponsor.usageCheck')}
<span className={styles.sponsorGroupTitle}>
<span>{titleLabel}</span>
<strong>{protocolLabel}</strong>
</span>
<span className={styles.sponsorGroupSummary}>
<span className={styles.sponsorSummaryKey}>{summaryKeyLabel}</span>
<span className={styles.sponsorSummaryUrl}>{endpointUrl}</span>
</span>
</button>
{usageCheck.status.state === 'success' && usageSummary ? (
<div
className={[
styles.sponsorUsageResult,
usageHealthy ? '' : styles.sponsorUsageResultWarning,
]
.filter(Boolean)
.join(' ')}
<div className={styles.entryCardHeaderRight}>
<button
type="button"
className={styles.entryCardIconBtn}
onClick={() => setExpanded((value) => !value)}
title={expanded ? t('common.collapse') : t('common.expand')}
aria-label={expanded ? t('common.collapse') : t('common.expand')}
>
<div className={styles.sponsorUsageMain}>
{usageHealthy ? (
<IconCheckCircle2
className={`${styles.statusIcon} ${styles.statusIconSuccess}`}
size={14}
/>
<IconChevronDown
className={[
styles.entryCardChevron,
expanded ? styles.entryCardChevronOpen : '',
]
.filter(Boolean)
.join(' ')}
size={14}
/>
</button>
<button
type="button"
className={styles.removeBtn}
onClick={onRemove}
disabled={mutating || !canRemove}
title={t('providersPage.sponsor.removeGroupedKey')}
aria-label={t('providersPage.sponsor.removeGroupedKey')}
>
<IconX size={12} />
</button>
</div>
</div>
{expanded ? (
<div className={styles.entryCardBody}>
<div className={styles.field}>
<label className={styles.label} htmlFor={`${formId}-group-${index}-protocol`}>
{t('providersPage.sponsor.protocol')}
</label>
<Select
id={`${formId}-group-${index}-protocol`}
value={entry.protocol}
options={protocolOptions}
onChange={(value) =>
updateEntry({ protocol: value as SponsorProtocol, models: [emptyModel()] })
}
disabled={mutating}
ariaLabel={t('providersPage.sponsor.protocol')}
/>
<span className={styles.labelHint}>{t('providersPage.sponsor.protocolHint')}</span>
</div>
<div className={styles.field}>
<span className={styles.label}>{t('providersPage.sponsor.urlMode')}</span>
<div className={styles.sponsorUrlOptions} role="radiogroup">
{APIKEY_FUN_BASE_URL_OPTIONS.map((option) => {
const checked = resolveApiKeyFunBaseUrl(entry.baseUrl) === option.baseUrl;
const className = [
styles.sponsorUrlOption,
checked ? styles.sponsorUrlOptionActive : '',
]
.filter(Boolean)
.join(' ');
return (
<label key={option.id} className={className}>
<input
type="radio"
name={`${formId}-group-${index}-base-url`}
value={option.baseUrl}
checked={checked}
onChange={() => updateEntry({ baseUrl: option.baseUrl })}
disabled={mutating}
/>
<span className={styles.sponsorUrlOptionText}>
<span>{t(`providersPage.sponsor.urlOptions.${option.id}`)}</span>
<small>{option.baseUrl}</small>
</span>
</label>
);
})}
</div>
<span className={styles.labelHint}>{t('providersPage.sponsor.urlHint')}</span>
</div>
<div className={styles.sponsorProtocolCard}>
<span className={styles.sponsorProtocolName}>
{t('providersPage.sponsor.protocolEndpoint')}
</span>
<span className={styles.sponsorProtocolUrl}>{endpointUrl}</span>
</div>
<div className={styles.field}>
<label className={styles.label} htmlFor={`${formId}-group-${index}-api-key`}>
{t('providersPage.form.apiKey')}
</label>
<div className={styles.passwordField}>
<input
id={`${formId}-group-${index}-api-key`}
className={styles.passwordInput}
type={showApiKey ? 'text' : 'password'}
value={entry.apiKey}
onChange={(event) => updateEntry({ apiKey: event.target.value })}
autoComplete="new-password"
data-1p-ignore="true"
data-lpignore="true"
data-bwignore="true"
placeholder={
mode === 'edit'
? t('providersPage.form.apiKeyEditPlaceholder')
: t('providersPage.form.apiKeyCreatePlaceholder')
}
disabled={mutating}
/>
<button
type="button"
className={styles.passwordToggle}
onClick={() => setShowApiKey((value) => !value)}
disabled={mutating}
aria-label={
showApiKey
? t('providersPage.form.hideApiKey')
: t('providersPage.form.showApiKey')
}
title={
showApiKey
? t('providersPage.form.hideApiKey')
: t('providersPage.form.showApiKey')
}
>
{showApiKey ? <IconEyeOff size={16} /> : <IconEye size={16} />}
</button>
</div>
<span className={styles.labelHint}>{t('providersPage.sponsor.apiKeyHint')}</span>
</div>
<div className={styles.sponsorUsageSection}>
<button
type="button"
className={styles.connectivityBtn}
onClick={() => void usageCheck.run()}
disabled={mutating || usageCheck.isLoading}
>
{usageCheck.isLoading ? (
<IconLoader2 className={styles.statusIconLoading} size={14} />
) : (
<IconAlertTriangle
className={`${styles.statusIcon} ${styles.statusIconError}`}
size={14}
/>
<IconDollarSign size={14} />
)}
<span>
{t('providersPage.sponsor.usageRemaining', {
amount: usageRemaining,
unit: usageSummary.unit,
})}
</span>
</div>
{usageSummary.used !== null || usageSummary.limit !== null ? (
<span className={styles.sponsorUsageMeta}>
{t('providersPage.sponsor.usageBreakdown', {
used: usageUsed,
limit: usageLimit,
})}
{usageCheck.isLoading
? t('providersPage.sponsor.usageChecking')
: t('providersPage.sponsor.usageCheck')}
</span>
</button>
{usageCheck.status.state === 'success' && usageSummary ? (
<div
className={[
styles.sponsorUsageResult,
usageHealthy ? '' : styles.sponsorUsageResultWarning,
]
.filter(Boolean)
.join(' ')}
>
<div className={styles.sponsorUsageMain}>
{usageHealthy ? (
<IconCheckCircle2
className={`${styles.statusIcon} ${styles.statusIconSuccess}`}
size={14}
/>
) : (
<IconAlertTriangle
className={`${styles.statusIcon} ${styles.statusIconError}`}
size={14}
/>
)}
<span>
{t('providersPage.sponsor.usageRemaining', {
amount: usageRemaining,
unit: usageSummary.unit,
})}
</span>
</div>
{usageSummary.used !== null || usageSummary.limit !== null ? (
<span className={styles.sponsorUsageMeta}>
{t('providersPage.sponsor.usageBreakdown', {
used: usageUsed,
limit: usageLimit,
})}
</span>
) : null}
{!usageHealthy ? (
<span className={styles.sponsorUsageMeta}>
{t('providersPage.sponsor.usageStatus', {
status: usageSummary.status || t('providersPage.sponsor.usageInvalid'),
})}
</span>
) : null}
</div>
) : null}
{!usageHealthy ? (
<span className={styles.sponsorUsageMeta}>
{t('providersPage.sponsor.usageStatus', {
status: usageSummary.status || t('providersPage.sponsor.usageInvalid'),
})}
</span>
{usageCheck.status.state === 'error' ? (
<div className={styles.connectivityError}>{usageCheck.status.message}</div>
) : null}
</div>
) : null}
{usageCheck.status.state === 'error' ? (
<div className={styles.connectivityError}>{usageCheck.status.message}</div>
) : null}
</div>
<div className={styles.field}>
<label className={styles.label} htmlFor={`${formId}-group-${index}-proxy`}>
{t('providersPage.form.proxyUrl')}
</label>
<input
id={`${formId}-group-${index}-proxy`}
className={styles.input}
value={entry.proxyUrl}
onChange={(event) => updateEntry({ proxyUrl: event.target.value })}
placeholder="http://127.0.0.1:7890"
disabled={mutating}
/>
</div>
<div className={styles.field}>
<label className={styles.label} htmlFor={`${formId}-group-${index}-proxy`}>
{t('providersPage.form.proxyUrl')}
</label>
<input
id={`${formId}-group-${index}-proxy`}
className={styles.input}
value={entry.proxyUrl}
onChange={(event) => updateEntry({ proxyUrl: event.target.value })}
placeholder="http://127.0.0.1:7890"
disabled={mutating}
/>
</div>
<div className={styles.fieldRow}>
<div className={styles.field}>
<label className={styles.label} htmlFor={`${formId}-group-${index}-prefix`}>
{t('providersPage.form.prefix')}
<div className={styles.fieldRow}>
<div className={styles.field}>
<label className={styles.label} htmlFor={`${formId}-group-${index}-prefix`}>
{t('providersPage.form.prefix')}
</label>
<input
id={`${formId}-group-${index}-prefix`}
className={styles.input}
value={entry.prefix}
onChange={(event) => updateEntry({ prefix: event.target.value })}
disabled={mutating}
/>
</div>
<div className={styles.field}>
<label className={styles.label} htmlFor={`${formId}-group-${index}-priority`}>
{t('providersPage.form.priority')}
</label>
<input
id={`${formId}-group-${index}-priority`}
type="number"
className={styles.input}
value={entry.priority ?? ''}
onChange={(event) =>
updateEntry({
priority: event.target.value === '' ? undefined : Number(event.target.value),
})
}
disabled={mutating}
/>
</div>
</div>
<label className={styles.checkboxRow}>
<input
type="checkbox"
className={styles.checkboxBox}
checked={entry.disabled}
disabled={mutating}
onChange={(event) => updateEntry({ disabled: event.target.checked })}
/>
<span className={styles.checkboxText}>
<span>{t('providersPage.form.disabled')}</span>
<small>{t('providersPage.form.disabledHint')}</small>
</span>
</label>
<input
id={`${formId}-group-${index}-prefix`}
className={styles.input}
value={entry.prefix}
onChange={(event) => updateEntry({ prefix: event.target.value })}
disabled={mutating}
<label className={styles.checkboxRow}>
<input
type="checkbox"
className={styles.checkboxBox}
checked={entry.disableCooling ?? false}
disabled={mutating}
onChange={(event) => updateEntry({ disableCooling: event.target.checked })}
/>
<span className={styles.checkboxText}>
<span>{t('providersPage.form.disableCooling')}</span>
<small>{t('providersPage.form.disableCoolingHint')}</small>
</span>
</label>
<SponsorModelSection
label={t(`providersPage.sponsor.protocolModels.${modelKey}`)}
description={t(`providersPage.sponsor.protocolModelHints.${modelKey}`)}
models={entry.models}
discovery={discovery}
mutating={mutating}
onChange={(models) => updateEntry({ models })}
/>
</div>
<div className={styles.field}>
<label className={styles.label} htmlFor={`${formId}-group-${index}-priority`}>
{t('providersPage.form.priority')}
</label>
<input
id={`${formId}-group-${index}-priority`}
type="number"
className={styles.input}
value={entry.priority ?? ''}
onChange={(event) =>
updateEntry({
priority: event.target.value === '' ? undefined : Number(event.target.value),
})
}
disabled={mutating}
/>
</div>
</div>
<label className={styles.checkboxRow}>
<input
type="checkbox"
className={styles.checkboxBox}
checked={entry.disabled}
disabled={mutating}
onChange={(event) => updateEntry({ disabled: event.target.checked })}
/>
<span className={styles.checkboxText}>
<span>{t('providersPage.form.disabled')}</span>
<small>{t('providersPage.form.disabledHint')}</small>
</span>
</label>
<label className={styles.checkboxRow}>
<input
type="checkbox"
className={styles.checkboxBox}
checked={entry.disableCooling ?? false}
disabled={mutating}
onChange={(event) => updateEntry({ disableCooling: event.target.checked })}
/>
<span className={styles.checkboxText}>
<span>{t('providersPage.form.disableCooling')}</span>
<small>{t('providersPage.form.disableCoolingHint')}</small>
</span>
</label>
<SponsorModelSection
label={t(`providersPage.sponsor.protocolModels.${modelKey}`)}
description={t(`providersPage.sponsor.protocolModelHints.${modelKey}`)}
models={entry.models}
discovery={discovery}
mutating={mutating}
onChange={(models) => updateEntry({ models })}
/>
) : null}
</div>
);
}
@@ -706,7 +754,7 @@ export function SponsorProviderForm({
);
const [error, setError] = useState<string | null>(null);
const entries = useMemo(
() => (form.sponsorKeyEntries?.length ? form.sponsorKeyEntries : [emptySponsorKeyEntry()]),
() => form.sponsorKeyEntries ?? [emptySponsorKeyEntry()],
[form.sponsorKeyEntries]
);
const usedProtocols = useMemo(() => new Set(entries.map((entry) => entry.protocol)), [entries]);
@@ -734,7 +782,7 @@ export function SponsorProviderForm({
const removeEntry = (entryIndex: number) => {
const nextEntries = entries.filter((_, index) => index !== entryIndex);
updateEntries(nextEntries.length ? nextEntries : [emptySponsorKeyEntry()]);
updateEntries(nextEntries.length || mode === 'edit' ? nextEntries : [emptySponsorKeyEntry()]);
};
const addEntry = () => {
@@ -744,7 +792,9 @@ export function SponsorProviderForm({
};
const validateEntries = (): string | null => {
if (!entries.length) return t('providersPage.sponsor.validation.keyRequired');
if (!entries.length) {
return mode === 'edit' ? null : t('providersPage.sponsor.validation.keyRequired');
}
const missingKey = entries.some(
(entry) => !entry.apiKey.trim() && !entry.existingApiKey?.trim()
);
@@ -787,7 +837,7 @@ export function SponsorProviderForm({
formId={formId}
mode={mode}
usedProtocols={usedProtocols}
canRemove={entries.length > 1}
canRemove={mode === 'edit' || entries.length > 1}
mutating={mutating}
onChange={(nextEntry) => updateEntry(index, nextEntry)}
onRemove={() => removeEntry(index)}
@@ -5,6 +5,8 @@
display: flex;
flex-direction: column;
gap: 16px;
min-width: 0;
max-width: 100%;
}
.quickStartForm {
@@ -133,6 +135,7 @@
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.sectionTitle {
@@ -155,6 +158,7 @@
.field {
display: grid;
gap: 6px;
min-width: 0;
}
.label {
@@ -266,6 +270,7 @@
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
min-width: 0;
}
.divider {
@@ -317,6 +322,9 @@
display: flex;
flex-direction: column;
gap: 10px;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
background: var(--bg-secondary);
}
@@ -325,11 +333,33 @@
align-items: center;
justify-content: space-between;
gap: 12px;
min-width: 0;
font-size: 12px;
font-weight: 500;
color: var(--muted-foreground);
}
.entryCardToggle {
display: flex;
min-width: 0;
flex: 1;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 0;
border: 0;
background: transparent;
color: inherit;
text-align: left;
cursor: pointer;
&:focus-visible {
outline: 2px solid var(--primary-color);
outline-offset: 3px;
border-radius: var(--radius-sm);
}
}
.sponsorGroupTitle {
display: flex;
min-width: 0;
@@ -339,13 +369,89 @@
strong {
color: var(--text-primary);
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.sponsorGroupSummary {
display: flex;
min-width: 0;
flex: 1;
justify-content: flex-end;
align-items: center;
gap: 8px;
}
.sponsorSummaryKey,
.sponsorSummaryUrl {
display: inline-block;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sponsorSummaryKey {
max-width: 140px;
font-family: $font-mono;
color: var(--text-primary);
}
.sponsorSummaryUrl {
max-width: min(360px, 45vw);
font-family: $font-mono;
color: var(--muted-foreground);
}
.entryCardHeaderRight {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.entryCardBody {
display: flex;
min-width: 0;
flex-direction: column;
gap: 10px;
}
.entryCardIconBtn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
border: 1px solid transparent;
border-radius: var(--radius-md);
background: transparent;
color: var(--text-tertiary);
cursor: pointer;
transition:
background-color $transition-fast,
color $transition-fast;
&:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
&:focus-visible {
outline: 2px solid var(--primary-color);
outline-offset: -1px;
}
}
.entryCardChevron {
transition: transform $transition-fast;
}
.entryCardChevronOpen {
transform: rotate(180deg);
}
.entriesToolbar {
@@ -785,6 +891,7 @@
position: relative;
display: flex;
align-items: center;
min-width: 0;
}
.passwordInput {