diff --git a/src/renderer/pages/settings/ProviderSettings/ConnectionSettings/ApiHost.tsx b/src/renderer/pages/settings/ProviderSettings/ConnectionSettings/ApiHost.tsx index ae79cfb6d1..4288881e03 100644 --- a/src/renderer/pages/settings/ProviderSettings/ConnectionSettings/ApiHost.tsx +++ b/src/renderer/pages/settings/ProviderSettings/ConnectionSettings/ApiHost.tsx @@ -67,7 +67,6 @@ export default function ApiHost({ providerId }: ApiHostProps) { providerIdForSettings={provider.id} apiHost={apiHost} isCherryIN={meta.isCherryIN} - isChineseUser={meta.isChineseUser} isVertexAI={provider.id === 'vertexai'} isApiHostResettable={hostPreview.isApiHostResettable} onResetApiHost={endpointActions.resetApiHost} diff --git a/src/renderer/pages/settings/ProviderSettings/ConnectionSettings/ApiHostFields.tsx b/src/renderer/pages/settings/ProviderSettings/ConnectionSettings/ApiHostFields.tsx index 6a0f2b16c9..4aa1f04fda 100644 --- a/src/renderer/pages/settings/ProviderSettings/ConnectionSettings/ApiHostFields.tsx +++ b/src/renderer/pages/settings/ProviderSettings/ConnectionSettings/ApiHostFields.tsx @@ -50,7 +50,6 @@ interface ApiHostFieldProps { providerIdForSettings: string apiHost: string isCherryIN: boolean - isChineseUser: boolean isVertexAI: boolean isApiHostResettable: boolean onResetApiHost: () => void @@ -61,7 +60,6 @@ export function ApiHostField({ providerIdForSettings, apiHost, isCherryIN, - isChineseUser, isVertexAI, isApiHostResettable, onResetApiHost, @@ -73,7 +71,7 @@ export function ApiHostField({ return ( {isVertexAI && ( @@ -86,20 +84,21 @@ export function ApiHostField({ */} }> - {isCherryIN && isChineseUser ? ( + {isCherryIN ? (
- +
@@ -135,27 +134,29 @@ export function ApiHostField({ {isApiHostResettable ? ( - + ) : null} - + @@ -216,13 +217,14 @@ export function AnthropicApiHostField({ - + diff --git a/src/renderer/pages/settings/ProviderSettings/ConnectionSettings/ApiKey.tsx b/src/renderer/pages/settings/ProviderSettings/ConnectionSettings/ApiKey.tsx index d79b6a2029..b682a02185 100644 --- a/src/renderer/pages/settings/ProviderSettings/ConnectionSettings/ApiKey.tsx +++ b/src/renderer/pages/settings/ProviderSettings/ConnectionSettings/ApiKey.tsx @@ -1,6 +1,7 @@ -import { InputGroup, InputGroupAddon, InputGroupInput, Tooltip, WarnTooltip } from '@cherrystudio/ui' -import { useProvider } from '@renderer/hooks/useProvider' +import { Button, InputGroup, InputGroupAddon, InputGroupInput, Tooltip, WarnTooltip } from '@cherrystudio/ui' +import { useProvider, useProviderAuthConfig } from '@renderer/hooks/useProvider' import type { ApiKeyConnectivity } from '@renderer/pages/settings/ProviderSettings/types/healthCheck' +import { hasApiKeys } from '@shared/utils/provider' import { Activity, Eye, EyeOff, KeyRound, Loader2 } from 'lucide-react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -27,6 +28,7 @@ export default function ApiKey({ }: ApiKeyProps) { const { t } = useTranslation() const { provider } = useProvider(providerId) + const { data: authConfig } = useProviderAuthConfig(providerId) const meta = useProviderMeta(providerId) const { inputApiKey, setInputApiKey } = useAuthenticationApiKey() const [showApiKey, setShowApiKey] = useState(false) @@ -40,6 +42,9 @@ export default function ApiKey({ return null } + // After OAuth login the key is provisioned and managed by the OAuth flow, so manual editing is locked. + const isOAuthLoggedIn = hasApiKeys(provider) && authConfig?.type === 'oauth' && Boolean(authConfig.accessToken) + return ( <> @@ -68,7 +73,7 @@ export default function ApiKey({ value={inputApiKey} placeholder={t('settings.provider.api_key.placeholder')} onChange={(event) => setInputApiKey(event.target.value)} - disabled={provider.id === 'copilot'} + disabled={provider.id === 'copilot' || isOAuthLoggedIn} /> {provider.id !== 'copilot' && ( @@ -96,19 +101,21 @@ export default function ApiKey({ - + - + diff --git a/src/renderer/pages/settings/ProviderSettings/ConnectionSettings/AuthConnectionSlotsLayout.tsx b/src/renderer/pages/settings/ProviderSettings/ConnectionSettings/AuthConnectionSlotsLayout.tsx index 0550a68204..5e05a76e98 100644 --- a/src/renderer/pages/settings/ProviderSettings/ConnectionSettings/AuthConnectionSlotsLayout.tsx +++ b/src/renderer/pages/settings/ProviderSettings/ConnectionSettings/AuthConnectionSlotsLayout.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from 'react' +import { useTranslation } from 'react-i18next' -import { authConnectionClasses } from '../primitives/ProviderSettingsPrimitives' +import { modelListClasses } from '../primitives/ProviderSettingsPrimitives' import ProviderSpecificSettings from '../ProviderSpecific/ProviderSpecificSettings' interface AuthConnectionSlotsLayoutProps { @@ -9,15 +10,15 @@ interface AuthConnectionSlotsLayoutProps { } export default function AuthConnectionSlotsLayout({ providerId, children }: AuthConnectionSlotsLayoutProps) { + const { t } = useTranslation() return ( -
- -
-
-
- {children} - -
+
+

{t('settings.provider.connection_title')}

+
+ +
+ {children} +
diff --git a/src/renderer/pages/settings/ProviderSettings/ConnectionSettings/ProviderApiKeyListDrawer.tsx b/src/renderer/pages/settings/ProviderSettings/ConnectionSettings/ProviderApiKeyListDrawer.tsx index fa88330dc4..1e0290ff07 100644 --- a/src/renderer/pages/settings/ProviderSettings/ConnectionSettings/ProviderApiKeyListDrawer.tsx +++ b/src/renderer/pages/settings/ProviderSettings/ConnectionSettings/ProviderApiKeyListDrawer.tsx @@ -185,7 +185,9 @@ export default function ProviderApiKeyListDrawer({ providerId, open, onClose }:
{apiKeys.length === 0 && !draft ? ( -
{t('error.no_api_key')}
+
+ {t('error.no_api_key')} +
) : null} {apiKeys.map((entry) => (
diff --git a/src/renderer/pages/settings/ProviderSettings/ProviderSetting.tsx b/src/renderer/pages/settings/ProviderSettings/ProviderSetting.tsx index 38d16bd7f4..37d0b9eefa 100644 --- a/src/renderer/pages/settings/ProviderSettings/ProviderSetting.tsx +++ b/src/renderer/pages/settings/ProviderSettings/ProviderSetting.tsx @@ -45,7 +45,10 @@ export default function ProviderSetting({ providerId, isOnboarding = false }: Pr return (
-
+ {/* Scoped mock alignment: tokens in `provider-settings-scoped-theme.css`, compositions in ProviderSettingsPrimitives. */} +
diff --git a/src/renderer/pages/settings/ProviderSettings/ProviderSettingsPage.tsx b/src/renderer/pages/settings/ProviderSettings/ProviderSettingsPage.tsx index d9ace8c3f7..e81df8b36e 100644 --- a/src/renderer/pages/settings/ProviderSettings/ProviderSettingsPage.tsx +++ b/src/renderer/pages/settings/ProviderSettings/ProviderSettingsPage.tsx @@ -1,3 +1,5 @@ +import './assets/styles/provider-settings-scoped-theme.css' + import { usePersistCache } from '@data/hooks/useCache' import { useProviders } from '@renderer/hooks/useProvider' import { useNavigate, useSearch } from '@tanstack/react-router' @@ -89,7 +91,7 @@ export default function ProviderSettingsPage({ isOnboarding = false }: ProviderS ) return ( -
+
- + - + {t('settings.models.provider_key_confirm_title', { provider: displayName })} - {confirmMessage} + + {confirmMessage} +
-
+
{rows.map((row) => (
-
{row.label}
-
{row.value}
+ className="grid grid-cols-[7.5rem_minmax(0,1fr)] gap-3 border-(--color-border-fg-hairline) border-b px-3 py-2.5 last:border-b-0"> +
{row.label}
+
{row.value}
))}
-
{t('settings.models.api_key')}
+
+ {t('settings.models.api_key')} +
- + {showFullKey ? newApiKey : maskApiKey(newApiKey)} -
diff --git a/src/renderer/pages/settings/ProviderSettings/assets/styles/provider-settings-scoped-theme.css b/src/renderer/pages/settings/ProviderSettings/assets/styles/provider-settings-scoped-theme.css new file mode 100644 index 0000000000..a0e4189510 --- /dev/null +++ b/src/renderer/pages/settings/ProviderSettings/assets/styles/provider-settings-scoped-theme.css @@ -0,0 +1,277 @@ +/* + * `provider-settings-scoped-theme.css` — scoped theme (`.provider-settings-default-scope`) + * + * Not a second Tailwind build: hand-authored CSS variables + `@container` queries only (no `@tailwind` preflight here). + * + * Local semantic palette + atomic layout/type tokens for the **provider detail column** only, so we can tune + * this surface without changing app-wide `:root` / global Tailwind theme. + * + * - **Atoms here** — spacing, radii, font steps, soft borders/surfaces, scroll max-height, chip refs, etc. + * - **Compose in TS** — `ProviderSettingsPrimitives.tsx` (`actionClasses`, `fieldClasses`, `modelListClasses`, …). + * - **Bridge `--color-*`** — utilities from `@cherrystudio/ui` often resolve `var(--color-foreground)` etc.; those + * are normally `:root`-bound, so this block shadows `--color-*` to the same values as `--foreground`, … inside + * the shell only. + * + * **Workflow** — add or adjust variables (and bridge) first; then extend `*Classes`; then TSX. Cross-links: + * `ProviderSetting.tsx` (shell node), `ProviderSettingsPage.tsx` (`provider-settings-layout-cq` + `ModelList` `ps-model-list-cq` + `@container` rules below). + */ +.provider-settings-default-scope { + --font-size: 16px; + --background: var(--cs-background); + --drawer-background: oklch(1 0 0); + --foreground: var(--cs-foreground); + --card: var(--cs-card); + --card-foreground: var(--cs-foreground); + --popover: var(--cs-popover); + --popover-foreground: var(--cs-foreground); + --primary: var(--cs-primary); + --primary-foreground: var(--cs-primary-foreground); + --secondary: oklch(0.95 0.0058 264.53); + --secondary-foreground: #030213; + --muted-foreground: #717182; + --accent: #e9ebef; + --accent-foreground: #030213; + --destructive: #d4183d; + --destructive-foreground: #ffffff; + --border: rgba(0, 0, 0, 0.1); + --input: transparent; + --input-background: #f3f3f5; + --switch-background: #cbced4; + --font-weight-medium: 500; + --font-weight-normal: 400; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --radius-4xs: 0.25rem; + --radius-3xs: 0.5rem; + --radius-2xs: 0.625rem; + --radius-xs: 0.75rem; + --radius-sm: 0.875rem; + /* --radius-md intentionally NOT overridden: it must match the design system default (8px) + so `rounded-md` from shared components like Button, Input, Avatar renders consistently. */ + --radius-lg: 0.625rem; + --radius-xl: 0.75rem; + --radius-2xl: 1rem; + --radius-3xl: 1.5rem; + --radius-round: 9999px; + /* provider column — `border-section-border` */ + --section-border: #f0f0ef; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: #030213; + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); + --cherry-active-bg: rgba(107, 114, 128, 0.08); + --cherry-active-border: rgba(107, 114, 128, 0.15); + --cherry-primary: #6b7280; + --cherry-primary-hover: #9ca3af; + --cherry-primary-dark: #4b5563; + --cherry-text-muted: rgba(107, 114, 128, 0.7); + /* Provider list — Sortable list uses inline `gap: var(--provider-list-row-gap)` */ + --provider-list-row-gap: 0.5rem; + /* + * Atomic tokens for this scope (spacing / type / soft surfaces). Compose in components; do not name by screen. + * Caption scale 0.8125rem aligns with endpoint tabs here; section titles use larger type for hierarchy. + */ + --space-inline-xs: 0.375rem; + --space-inline-sm: 0.5rem; + --space-inline-md: 0.75rem; + --space-stack-2xs: 0.25rem; + --space-stack-xs: 0.5rem; + --space-stack-sm: 0.625rem; + --space-stack-md: 0.75rem; + --space-stack-lg: 1.5rem; + --radius-control: var(--radius-lg); + --font-size-caption: 0.8125rem; + --line-height-caption: 1.25; + --padding-x-control: 0.75rem; + --padding-y-control: 0.375rem; + --padding-x-control-compact: 0.5rem; + --padding-y-control-compact: 0.1875rem; + --icon-size-caption: 0.8125rem; + --icon-size-body-xs: 0.75rem; + /* Model list capability glyphs — match CustomTag `size` (e.g. 10px) for lucide SVG + iconfont */ + --icon-size-model-list-cap: 0.625rem; + --color-border-default-soft: color-mix(in srgb, var(--border) 25%, transparent); + --color-fg-subtle: color-mix(in srgb, var(--foreground) 70%, transparent); + /* 1rem (16px): block title (e.g. Models); stays above caption tier; not shrinking to smaller design-only specs */ + --font-size-heading-sm: 1rem; + --line-height-heading-sm: 1.3; + /* 0.875rem (14px): search field + empty-state body */ + --font-size-body-md: 0.875rem; + --line-height-body-md: 1.4; + /* Connection / Models section titles: same type tier as `ProviderSection`; slightly tighter than body line-height 1.4. */ + --line-height-section-label: 1.3; + /* 0.9375rem (15px): list row primary name; half step above body-md for scan readability vs matching smaller fixed pixels */ + --font-size-row-title: 0.9375rem; + --line-height-row-title: 1.2; + /* 0.75rem (12px): id badge / secondary identifiers only; smallest tier in this shell */ + --font-size-body-xs: 0.75rem; + --line-height-body-xs: 1.2; + /* + * Model category pills: `rounded-full` in UI for Figma “infinite” radius. + * Label/count align to shell caption + body-xs for readability (strict 9px/8px mock was too small). + */ + --font-size-chip-label: var(--font-size-caption); + --font-size-chip-count: var(--font-size-body-xs); + --color-border-fg-hairline: color-mix(in srgb, var(--foreground) 6%, transparent); + --color-surface-fg-sunken: color-mix(in srgb, var(--foreground) 3%, transparent); + --color-surface-fg-subtle: color-mix(in srgb, var(--foreground) 4%, transparent); + --color-border-fg-muted: color-mix(in srgb, var(--foreground) 12%, transparent); + --color-surface-fg-muted: color-mix(in srgb, var(--foreground) 10%, transparent); + /* Matches prior hover:bg-accent/40 intent; follows --accent in light/dark */ + --color-surface-hover-soft: color-mix(in srgb, var(--accent) 40%, transparent); + --color-surface-warning-soft: color-mix(in srgb, var(--destructive) 8%, var(--background)); + --color-border-warning-soft: color-mix(in srgb, var(--destructive) 22%, transparent); + --color-surface-info-soft: color-mix(in srgb, var(--foreground) 4%, var(--background)); + --color-border-info-soft: color-mix(in srgb, var(--foreground) 12%, transparent); + --padding-x-list-group: 0.875rem; + /* Unify outer list scroller with virtual list max (was 380 vs 390) */ + --max-height-scroll-sm: 390px; + /* + * Bridge shadcn-style tokens above to Tailwind `@theme` / `text-foreground` etc. + * Those utilities resolve `var(--color-foreground)` from @cherrystudio/ui — that + * variable is normally only set on `:root`, so it would NOT follow this scope + * unless we shadow `--color-*` here to point at the same values as `--foreground`, … + */ + --color-foreground: var(--foreground); + --color-background: var(--background); + --color-drawer-background: var(--drawer-background); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-section-border: var(--section-border); + --color-input: var(--input); + --color-ring: var(--ring); + background: var(--background); + color: var(--foreground); +} + +/* Dark: same scope node; only semantic palette overrides (atoms above inherit unless reset here). */ +.dark .provider-settings-default-scope { + --background: var(--cs-background); + --drawer-background: oklch(0 0 0); + --foreground: var(--cs-foreground); + --card: var(--cs-card); + --card-foreground: var(--cs-foreground); + --popover: var(--cs-popover); + --popover-foreground: var(--cs-foreground); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --radius-4xs: 0.25rem; + --radius-3xs: 0.5rem; + --radius-2xs: 0.625rem; + --radius-xs: 0.75rem; + --radius-sm: 0.875rem; + /* --radius-md intentionally NOT overridden — see comment in light-mode block. */ + --radius-lg: 0.625rem; + --radius-xl: 0.75rem; + --radius-2xl: 1rem; + --radius-3xl: 1.5rem; + --radius-round: 9999px; + --section-border: #1f201d; + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); + --cherry-active-bg: rgba(156, 163, 175, 0.06); + --cherry-active-border: rgba(156, 163, 175, 0.1); + --cherry-primary: #9ca3af; + --cherry-primary-hover: #d1d5db; + --cherry-primary-dark: #6b7280; + --cherry-text-muted: rgba(156, 163, 175, 0.7); +} + +/* + * PageSidePanel defaults to `bg-card`, while Provider Settings page background can be translucent in dark mode + * through `--cs-background`. Drawer surfaces must stay opaque because they sit above live settings content. + */ +.provider-settings-default-scope[data-slot='page-side-panel'] { + background-color: var(--drawer-background) !important; +} + +/* + * Provider settings page — size context for in-page drawer widths (`cqw` in `ProviderSettingsDrawer`). + * Nested `ps-model-list-cq` is nearer for model-list drawers; auth-only drawers use this ancestor. + */ +.provider-settings-default-scope.provider-settings-layout-cq { + container-type: inline-size; + container-name: provider-settings; +} + +/* Model list — inline-size container for compact row rules. */ +.provider-settings-default-scope .ps-model-list-cq { + container-type: inline-size; + container-name: model-list; +} + +@container model-list (max-width: 759px) { + .provider-settings-default-scope .ps-model-list-cq .ps-model-list-id { + display: none; + } + + .provider-settings-default-scope .ps-model-list-cq .ps-model-list-meta { + display: none; + } +} + +@container model-list (max-width: 919px) { + .provider-settings-default-scope .ps-model-list-cq .ps-model-list-health .ant-typography { + display: none !important; + } +} + +/* + * Inline model row — one footprint for capability glyphs. + * ReasoningTag uses `i.iconfont`, not SVG; CustomTag wraps icons with inline `font-size`, so iconfont needs explicit override. + */ +.provider-settings-default-scope .ps-compact-cap-strip svg, +.provider-settings-default-scope .ps-model-list-cap-strip svg { + flex-shrink: 0; + width: var(--icon-size-model-list-cap); + height: var(--icon-size-model-list-cap); +} + +.provider-settings-default-scope .ps-compact-cap-strip i.iconfont, +.provider-settings-default-scope .ps-model-list-cap-strip i.iconfont { + flex-shrink: 0; + font-size: var(--icon-size-model-list-cap) !important; + line-height: 1 !important; +} diff --git a/src/renderer/pages/settings/ProviderSettings/hooks/providerSetting/useProviderMeta.ts b/src/renderer/pages/settings/ProviderSettings/hooks/providerSetting/useProviderMeta.ts index b7a4b3f3c6..faeb5826cf 100644 --- a/src/renderer/pages/settings/ProviderSettings/hooks/providerSetting/useProviderMeta.ts +++ b/src/renderer/pages/settings/ProviderSettings/hooks/providerSetting/useProviderMeta.ts @@ -1,7 +1,13 @@ import { useProvider } from '@renderer/hooks/useProvider' -import { hasVisibleProviderApiOptions } from '@renderer/pages/settings/ProviderSettings/utils/providerApiOptions' import { getFancyProviderName } from '@renderer/pages/settings/ProviderSettings/utils/providerDisplay' -import { isAwsBedrockProvider, isAzureOpenAIProvider, isVertexProvider, matchesPreset } from '@shared/utils/provider' +import { + isAnthropicSupportedProvider, + isAwsBedrockProvider, + isAzureOpenAIProvider, + isSystemProvider, + isVertexProvider, + matchesPreset +} from '@shared/utils/provider' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -25,7 +31,7 @@ export function useProviderMeta(providerId: string) { isCherryIN: provider ? matchesPreset(provider, 'cherryin') : false, isDmxapi, isChineseUser: i18n.language.startsWith('zh'), - showApiOptionsButton: provider ? hasVisibleProviderApiOptions(provider) : false, + showApiOptionsButton: provider ? !isSystemProvider(provider) || isAnthropicSupportedProvider(provider) : false, isApiKeyFieldVisible: !hideApiInput && !hideApiKeyInput, isConnectionFieldVisible: !hideApiInput && !isDmxapi } diff --git a/src/renderer/pages/settings/ProviderSettings/primitives/ProviderField.tsx b/src/renderer/pages/settings/ProviderSettings/primitives/ProviderField.tsx index 67aff595d0..c40c38c829 100644 --- a/src/renderer/pages/settings/ProviderSettings/primitives/ProviderField.tsx +++ b/src/renderer/pages/settings/ProviderSettings/primitives/ProviderField.tsx @@ -9,6 +9,10 @@ interface ProviderFieldProps { help?: ReactNode children: ReactNode className?: string + /** When true, render label on the left and control on the right in a single row (default: false = stacked). */ + horizontal?: boolean + /** Merged onto the right-side control wrapper in horizontal mode. */ + controlClassName?: string } export default function ProviderField({ @@ -17,12 +21,41 @@ export default function ProviderField({ action, help, children, - className + className, + horizontal = false, + controlClassName }: ProviderFieldProps) { + if (horizontal) { + return ( +
+
+
+ {title} +
+
+ {children} + {action} +
+
+ {help} +
+ ) + } + return (
-
{title}
+
+ {title} +
{action}
{children} diff --git a/src/renderer/pages/settings/ProviderSettings/primitives/ProviderSection.tsx b/src/renderer/pages/settings/ProviderSettings/primitives/ProviderSection.tsx index 6503861666..8187abb69f 100644 --- a/src/renderer/pages/settings/ProviderSettings/primitives/ProviderSection.tsx +++ b/src/renderer/pages/settings/ProviderSettings/primitives/ProviderSection.tsx @@ -19,7 +19,11 @@ export default function ProviderSection({ id, title, description, action, childr
{title &&
{title}
} - {description &&
{description}
} + {description && ( +
+ {description} +
+ )}
{action}
diff --git a/src/renderer/pages/settings/ProviderSettings/primitives/ProviderSettingsDrawer.tsx b/src/renderer/pages/settings/ProviderSettings/primitives/ProviderSettingsDrawer.tsx index 50f8a23f15..88f5c111a7 100644 --- a/src/renderer/pages/settings/ProviderSettings/primitives/ProviderSettingsDrawer.tsx +++ b/src/renderer/pages/settings/ProviderSettings/primitives/ProviderSettingsDrawer.tsx @@ -1,4 +1,5 @@ import { PageSidePanel } from '@cherrystudio/ui' +import { cn } from '@renderer/utils/style' import type { ReactNode } from 'react' import { useTranslation } from 'react-i18next' @@ -17,7 +18,7 @@ interface ProviderSettingsDrawerProps { } // All callers follow PageSidePanel defaults from DESIGN.md §4 "Drawers & Page Side Panels": -// w-100, rounded-3xl, shadow-xl, bg-card, +// w-100, rounded-3xl, shadow-xl, bg-card (opaque via provider-settings-scoped-theme.css), // backdrop bg-black/50, header px-6 pt-6 pb-3, body space-y-4 px-6 py-4, footer px-6 pt-3 pb-6. export default function ProviderSettingsDrawer({ open, @@ -37,7 +38,7 @@ export default function ProviderSettingsDrawer({ const header = description ? (
{title} - {description} + {description}
) : undefined @@ -50,7 +51,7 @@ export default function ProviderSettingsDrawer({ footer={footer} closeLabel={t('common.close')} showCloseButton={showHeaderCloseButton} - contentClassName={contentClassName} + contentClassName={cn('provider-settings-default-scope', contentClassName)} headerClassName={headerClassName} bodyClassName={bodyClassName} footerClassName={footerClassName}> diff --git a/src/renderer/pages/settings/ProviderSettings/primitives/ProviderSettingsPrimitives.tsx b/src/renderer/pages/settings/ProviderSettings/primitives/ProviderSettingsPrimitives.tsx index 79e310eeaa..868c0eabb6 100644 --- a/src/renderer/pages/settings/ProviderSettings/primitives/ProviderSettingsPrimitives.tsx +++ b/src/renderer/pages/settings/ProviderSettings/primitives/ProviderSettingsPrimitives.tsx @@ -43,11 +43,7 @@ export function ProviderHelpTextRow({ children, className }: { children: ReactNo export function ProviderHelpLink({ children, className, ...props }: React.AnchorHTMLAttributes) { return ( {children} diff --git a/src/renderer/pages/settings/ProviderSettings/primitives/classNames.ts b/src/renderer/pages/settings/ProviderSettings/primitives/classNames.ts index 90b1f1c00f..9090aecd3c 100644 --- a/src/renderer/pages/settings/ProviderSettings/primitives/classNames.ts +++ b/src/renderer/pages/settings/ProviderSettings/primitives/classNames.ts @@ -1,34 +1,52 @@ import { cn } from '@renderer/utils/style' -/** Provider settings class compositions. Prefer Tailwind utilities and exported design tokens. */ +/** + * Provider settings — design alignment (scoped theme + composition) + * + * **Shell** — `ProviderSetting.tsx` wraps the detail column in `.provider-settings-default-scope`. Everything + * that must follow the provider-settings surface must stay in that subtree so tokens and `--color-*` bridge apply. + * + * **Two layers** + * - **CSS** — `assets/styles/provider-settings-scoped-theme.css`: atomic vars only (`--font-size-*`, `--space-*`, soft + * surfaces, `--color-*` → shadcn/Tailwind). No screen- or feature-prefixed names. When fixed pixels hurt a11y / + * readability, prefer named steps and note the tradeoff in a CSS comment. + * - **TS (this file)** — merge atoms into `actionClasses`, `fieldClasses`, `modelListClasses`, `providerDetailColumnClasses`, + * `apiKeyListClasses`, … + * Use `var(--*)` in class strings; avoid scattered `text-[Npx]` and inline `fontWeight` styles. + * + * **Rules (short)** — Do not satisfy this page by editing global `:root` unless product wants a global change. + * Figma “infinite” radius exports → `rounded-full` in UI. Secondary actions: `btnNeutral`, not brand primary fill, + * unless the spec demands emphasis. Execution order: scope vars + bridge in CSS → extend `*Classes` → touch TSX. + */ export const providerSettingsTypography = { - menu: 'text-sm leading-5', - body: 'text-sm leading-5', - label: 'text-xs leading-tight', - micro: 'text-xs leading-tight', - caption: 'text-xs leading-tight', - subtitle: 'text-sm leading-5' + menu: 'text-[length:var(--font-size-body-sm)] leading-[length:var(--line-height-body-sm)]', + body: 'text-[length:var(--font-size-body-sm)] leading-[length:var(--line-height-body-sm)]', + label: 'text-[length:var(--font-size-body-xs)] leading-[length:var(--line-height-body-xs)]', + micro: 'text-[length:var(--font-size-body-xs)] leading-[length:var(--line-height-body-xs)]', + caption: 'text-[length:var(--font-size-body-xs)] leading-[length:var(--line-height-body-xs)]', + subtitle: 'text-[length:var(--font-size-body-md)] leading-[length:var(--line-height-body-md)]' } as const +/** + * Input row + icon slots for provider settings, using tokens from `provider-settings-scoped-theme.css` + * (`.provider-settings-default-scope` — `--border`, `--foreground`, `--cherry-*`). + * The provider detail shell should include `provider-settings-default-scope` so these inherit correctly. + */ /** Connection — transparent input body + same muted border as model search. * Fixed `h-8` (32px) so all input groups in this page line up regardless of trailing-control height. */ -const providerSettingsInputGroupBase = 'h-8 rounded-lg border border-border-muted bg-background px-2.5 shadow-none' +const providerSettingsInputGroupBase = + 'h-8 rounded-lg border border-[color:var(--color-border-fg-muted)] bg-transparent px-2.5 shadow-none' /** Softer focus ring than `@cherrystudio/ui` InputGroup default (`ring-[3px]`) — business-layer override only. */ const providerSettingsInputGroupFocusOverride = 'has-[[data-slot=input-group-control]:focus-visible]:ring-[1px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/35' /** Connection and `ProviderSection`: 14px, deepest foreground, section-label line-height. */ -const sectionHeadingBase = 'm-0 text-sm text-foreground leading-[1.3]' +const sectionHeadingBase = + 'm-0 text-[length:var(--font-size-body-md)] text-foreground leading-[var(--line-height-section-label)]' export const sectionHeadingClasses = cn(sectionHeadingBase, 'font-medium') -/** Authentication section layout: slot stack only; fields provide their own surfaces. */ -export const authConnectionClasses = { - shell: '', - body: 'flex flex-col gap-2' -} as const - /** * Provider detail column (`ProviderSetting.tsx`) — padding + gap between Authentication + ModelList. */ @@ -36,18 +54,20 @@ export const providerDetailColumnClasses = { headerPad: 'shrink-0 px-6 pt-2', scrollStrip: 'min-h-0 flex-1 overflow-x-hidden px-6 pt-6 pb-4', contentMaxWidth: 'mx-auto w-full max-w-3xl', - /** Header inner wrapper: same max-width as body content + bottom divider aligned to content edges. */ - headerContentMaxWidth: 'mx-auto w-full max-w-3xl border-b border-border pb-2', + /** Header inner wrapper: same max-width as body content; no divider — sections below carry their own borders. */ + headerContentMaxWidth: 'mx-auto w-full max-w-3xl', sectionStack: 'mx-auto flex min-h-full w-full min-w-0 max-w-3xl flex-col gap-5' } as const -/** Connection-field actions. */ +/** Connection-field actions; composes atomic `--space-*`, `--font-size-caption`, `--color-*-soft` from scope CSS. */ export const actionClasses = { - row: 'flex flex-wrap items-center gap-3', - icon: 'size-3 shrink-0', - btnBase: 'h-auto min-h-0 gap-2 rounded-lg px-3 py-1.5 text-[13px] leading-tight shadow-none', + row: 'flex flex-wrap items-center gap-[length:var(--space-inline-md)]', + icon: 'size-[length:var(--icon-size-caption)] shrink-0', + btnBase: + 'h-auto min-h-0 gap-2 rounded-[length:var(--radius-control)] px-[length:var(--padding-x-control)] py-[length:var(--padding-y-control)] text-[length:var(--font-size-caption)] leading-[var(--line-height-caption)] shadow-none', /** Neutral outline (design: action row — no brand fill on check / API-key-list actions). */ - btnNeutral: 'border-border-muted bg-transparent text-foreground/70 hover:bg-accent hover:text-foreground' + btnNeutral: + 'border-[color:var(--color-border-default-soft)] bg-transparent text-[color:var(--color-fg-subtle)] hover:bg-[var(--accent)] hover:text-[color:var(--foreground)]' } as const const providerListItemFrame = @@ -55,15 +75,16 @@ const providerListItemFrame = /** Provider list rows + detached menus. */ export const providerListClasses = { - shell: 'flex h-full w-[232px] shrink-0 basis-[232px] flex-col border-border border-r-[0.5px]', + shell: 'flex h-full w-[232px] shrink-0 basis-[232px] flex-col border-r border-[color:var(--section-border)]', headerIconButton: - 'flex size-6 shrink-0 items-center justify-center rounded-md text-foreground/45 transition-colors hover:bg-accent/40 hover:text-foreground/75 disabled:pointer-events-none disabled:opacity-30', + 'flex size-6 shrink-0 items-center justify-center rounded-md text-foreground/45 transition-colors hover:bg-[var(--color-surface-hover-soft)] hover:text-foreground/75 disabled:pointer-events-none disabled:opacity-30', headerAddButton: - 'flex size-7 shrink-0 items-center justify-center rounded-md text-primary transition-colors hover:bg-accent/40 hover:text-primary disabled:pointer-events-none disabled:opacity-30', + 'flex size-7 shrink-0 items-center justify-center rounded-md text-primary transition-colors hover:bg-[var(--color-surface-hover-soft)] hover:text-primary disabled:pointer-events-none disabled:opacity-30', searchInlineAddButton: - 'flex size-[22px] shrink-0 items-center justify-center rounded-md transition-colors hover:bg-accent/40 disabled:pointer-events-none disabled:opacity-30', + 'flex size-6 shrink-0 items-center justify-center rounded-[8px] bg-(--color-surface-fg-subtle-solid) text-foreground transition-colors hover:bg-(--color-frame-border) disabled:pointer-events-none disabled:opacity-30', searchRow: 'flex items-center gap-1.5 px-2.5 pb-2.5', - searchWrap: 'flex h-8 items-center gap-1 rounded-[10px] border border-border-muted bg-background py-1 pl-2.5 pr-1', + searchWrap: + 'flex h-8 items-center gap-1 rounded-[10px] border border-[color:var(--section-border)] bg-background py-1 pl-2.5 pr-1', searchIcon: 'size-4 shrink-0 text-muted-foreground/60', searchInput: 'min-w-0 flex-1 bg-transparent text-sm leading-none text-foreground/80 outline-none placeholder:text-muted-foreground/60', @@ -73,10 +94,11 @@ export const providerListClasses = { sectionHeader: 'pb-0.5 pl-2 pr-2 pt-1.5', sectionHeaderAfterEnabled: 'pt-2', sectionLabel: 'mb-0.5 text-xs leading-[1.2] text-foreground-muted', - emptyState: 'flex h-full min-h-40 items-center justify-center px-3 text-center text-foreground-muted text-[14px]', - addWrap: 'shrink-0 border-t border-border-muted px-2.5 py-2', + emptyState: + 'flex h-full min-h-40 items-center justify-center px-3 text-center text-foreground-muted text-(length:--font-size-body-sm)', + addWrap: 'shrink-0 border-t border-[color:var(--section-border)] px-2.5 py-2', addButton: - 'flex w-full items-center justify-center gap-1.5 rounded-lg border border-border-muted border-dashed bg-transparent py-[5px] text-xs text-foreground-muted shadow-none transition-colors hover:border-border hover:bg-accent/50 hover:text-foreground disabled:pointer-events-none disabled:opacity-40', + 'flex w-full items-center justify-center gap-1.5 rounded-lg border border-[color:var(--section-border)] border-dashed bg-transparent py-[5px] text-xs text-foreground-muted shadow-none transition-colors hover:border-[color:var(--color-border)] hover:bg-accent/50 hover:text-foreground disabled:pointer-events-none disabled:opacity-40', item: providerListItemFrame, itemSelected: 'bg-muted', itemIdle: 'hover:bg-muted', @@ -87,7 +109,7 @@ export const providerListClasses = { itemDragHandleSpacer: 'flex w-2.5 shrink-0', itemAvatar: 'shrink-0 rounded-md border border-border/30 [&_[data-slot=avatar-fallback]]:rounded-[inherit] [&_[data-slot=avatar-image]]:rounded-[inherit]', - itemLabel: 'min-w-0 truncate text-sm leading-[1.35] text-foreground font-[weight:500]', + itemLabel: 'min-w-0 truncate text-sm leading-[1.35] text-foreground font-medium', itemTrailingSlot: 'relative -mr-1 ml-1 flex shrink-0 items-center justify-center', itemTrailingSlotAction: 'size-5', itemTrailingSlotIndicatorOnly: 'size-2', @@ -97,17 +119,17 @@ export const providerListClasses = { groupHeaderHasSelected: 'bg-muted', groupChevron: 'shrink-0 text-muted-foreground/60 transition-transform duration-150', groupChevronOpen: 'rotate-90', - groupCount: 'shrink-0 text-xs leading-none text-muted-foreground/60 tabular-nums', - groupBody: 'mt-1 flex flex-col gap-2 pl-3.5', + groupCount: 'shrink-0 text-[length:var(--font-size-body-xs)] leading-none text-muted-foreground/60 tabular-nums', + groupBody: 'mt-1 flex flex-col gap-[var(--provider-list-row-gap)] pl-3.5', itemMoreActions: - 'absolute top-1/2 right-0 flex size-5 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground/50 opacity-0 transition-[color,opacity,background-color] hover:bg-accent/40 hover:text-foreground group-hover/row:opacity-100 group-focus-within/row:opacity-100 focus-visible:opacity-100', + 'absolute top-1/2 right-0 flex size-5 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground/50 opacity-0 transition-[color,opacity,background-color] hover:bg-[var(--color-surface-fg-subtle)] hover:text-foreground group-hover/row:opacity-100 group-focus-within/row:opacity-100 focus-visible:opacity-100', /** Enabled-state dot — shown when `provider.isEnabled` is true; hidden on row hover or focus so the kebab takes the slot. */ itemEnabledDot: - 'pointer-events-none absolute top-1/2 right-0.5 size-1.5 -translate-y-1/2 rounded-full bg-green-500 transition-opacity group-hover/row:opacity-0 group-focus-within/row:opacity-0', + 'pointer-events-none absolute top-1/2 right-0.5 size-1.5 -translate-y-1/2 rounded-full bg-success transition-opacity group-hover/row:opacity-0 group-focus-within/row:opacity-0', groupAddRow: - 'flex w-full items-center gap-2 rounded-[10px] border border-dashed border-border-muted bg-transparent px-2 py-[6px] text-xs leading-[1.35] text-muted-foreground/70 shadow-none transition-colors hover:border-border hover:bg-accent/40 hover:text-foreground', + 'flex w-full items-center gap-2 rounded-[10px] border border-dashed border-[color:var(--section-border)] bg-transparent px-2 py-[6px] text-[length:var(--font-size-body-xs)] leading-[1.35] text-muted-foreground/70 shadow-none transition-colors hover:border-[color:var(--color-border)] hover:bg-accent/40 hover:text-foreground', disclosureToggle: - 'flex w-full items-center gap-1.5 rounded-md bg-transparent px-1 py-1 text-left text-xs leading-none text-muted-foreground/80 shadow-none outline-none transition-colors hover:text-foreground focus-visible:ring-0', + 'flex w-full items-center gap-1.5 rounded-md bg-transparent px-1 py-1 text-left text-[length:var(--font-size-body-xs)] leading-none text-muted-foreground/80 shadow-none outline-none transition-colors hover:text-foreground focus-visible:ring-0', disclosureChevron: 'size-3 shrink-0 text-muted-foreground/60 transition-transform duration-150', disclosureChevronOpen: 'rotate-90', disclosureBody: 'mt-2 flex flex-col gap-3 pl-1' @@ -120,140 +142,155 @@ export const customHeaderDrawerClasses = { bodyScroll: 'flex flex-col gap-4', /** JSON mode — matches structured monospace block for custom headers. */ headersJsonEditor: - 'min-h-[120px] w-full resize-y rounded-xl border border-border-muted bg-muted/50 px-3 py-2.5 font-mono text-xs leading-relaxed text-foreground shadow-none outline-none focus-visible:ring-[1px] focus-visible:ring-ring/35 placeholder:text-muted-foreground/45', + 'min-h-[120px] w-full resize-y rounded-xl border border-[color:var(--section-border)] bg-muted/50 px-3 py-2.5 font-mono text-xs leading-relaxed text-foreground shadow-none outline-none focus-visible:ring-[1px] focus-visible:ring-ring/35 placeholder:text-muted-foreground/45', /** Header rows stack; each row is `[name] [value] [delete]` on a single line. */ headerList: 'flex flex-col gap-2', headerRow: 'grid grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)_auto] items-center gap-2', /** Quiet trailing delete: neutral until hover, then destructive. */ removeIconButton: - 'size-7 shrink-0 rounded-lg text-muted-foreground/45 shadow-none transition-colors hover:bg-accent hover:text-destructive [&_svg]:size-3.5', + 'size-7 shrink-0 rounded-lg text-muted-foreground/45 shadow-none transition-colors hover:bg-accent hover:text-destructive [&_svg]:size-3.5 [&_svg]:[stroke-width:var(--icon-stroke)]', addRowButton: 'flex h-auto w-full items-center justify-center gap-1.5 rounded-xl border border-dashed border-border-muted py-2 text-xs text-muted-foreground shadow-none transition-colors hover:border-border-hover hover:bg-accent/40 hover:text-foreground' } as const export const drawerClasses = { - form: 'flex min-h-0 flex-col gap-4 py-0', + form: 'provider-settings-default-scope flex min-h-0 flex-col gap-4 py-0', section: 'space-y-3', - sectionCard: 'space-y-3.5 rounded-lg border border-border bg-background px-3 py-3 text-foreground shadow-none', - sectionDescription: 'text-xs leading-tight text-foreground-muted', + sectionCard: + 'space-y-3.5 rounded-[length:var(--radius-lg)] border border-border bg-background px-3 py-3 text-foreground shadow-none', + sectionDescription: + 'text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-foreground-muted', fieldList: 'space-y-3.5', field: 'space-y-1.5', - fieldTitle: 'font-medium text-sm leading-5 text-foreground-secondary', + fieldTitle: 'font-normal text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-foreground', input: - 'h-8 min-h-8 w-full rounded-md border border-input bg-background px-3 py-1 text-sm leading-5 text-foreground shadow-none outline-none transition-[border-color,box-shadow] placeholder:text-foreground-muted disabled:cursor-not-allowed disabled:opacity-60 focus-visible:border-ring focus-visible:ring-[2px] focus-visible:ring-ring/35', + 'h-8 min-h-8 w-full rounded-[length:var(--radius-md)] border border-input bg-background px-3 py-1 text-[length:var(--font-size-body-sm)] leading-[var(--line-height-body-sm)] text-foreground shadow-none outline-none transition-[border-color,box-shadow] placeholder:text-foreground-muted disabled:cursor-not-allowed disabled:opacity-60 focus-visible:border-ring focus-visible:ring-[2px] focus-visible:ring-ring/35', inputDisabled: 'bg-muted text-foreground-muted', selectTrigger: - 'h-auto w-full rounded-md border-input bg-background px-3 py-2 text-sm leading-5 text-foreground shadow-none data-[placeholder]:text-foreground-muted aria-expanded:border-ring aria-expanded:ring-[2px] aria-expanded:ring-ring/35', - selectContent: 'rounded-lg border-[0.5px] border-border bg-popover text-popover-foreground shadow-lg', - helpText: 'text-xs leading-tight text-foreground-muted', - errorText: 'text-xs leading-tight text-destructive', + 'h-auto w-full rounded-[length:var(--radius-md)] border-input bg-background px-3 py-2 text-[length:var(--font-size-body-sm)] leading-[var(--line-height-body-sm)] text-foreground shadow-none data-[placeholder]:text-foreground-muted aria-expanded:border-ring aria-expanded:ring-[2px] aria-expanded:ring-ring/35', + selectContent: + 'provider-settings-default-scope rounded-[length:var(--radius-lg)] border-[0.5px] border-border bg-popover text-popover-foreground shadow-lg', + helpText: 'text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-foreground-muted', + errorText: 'text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-destructive', emptyInline: - 'rounded-md border border-dashed border-border-muted px-3 py-2 text-[13px] leading-tight text-muted-foreground/70', + 'rounded-[length:var(--radius-md)] border border-dashed border-[color:var(--color-border-fg-muted)] px-3 py-2 text-[length:var(--font-size-caption)] leading-[var(--line-height-caption)] text-muted-foreground/70', toggleButton: - 'h-auto justify-start gap-1.5 px-0 py-0 text-sm leading-5 text-foreground-muted shadow-none hover:bg-transparent hover:text-foreground', + 'h-auto justify-start gap-1.5 px-0 py-0 text-[length:var(--font-size-body-sm)] leading-[var(--line-height-body-sm)] text-foreground-muted shadow-none hover:bg-transparent hover:text-foreground', inlineRow: 'flex flex-wrap items-center gap-2', valueRow: 'flex min-w-0 items-center gap-2', responsiveValueRow: 'flex min-w-0 flex-col items-stretch gap-2 sm:flex-row sm:items-center', - valueSuffix: 'shrink-0 text-xs leading-tight text-foreground-muted', + valueSuffix: + 'shrink-0 text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-foreground-muted', divider: 'h-px bg-border-muted', - switchCard: 'rounded-md border border-border bg-background px-3 py-3 [&_[data-slot=switch]]:mt-0.5', + switchCard: + 'rounded-[length:var(--radius-md)] border border-border bg-background px-3 py-3 [&_[data-slot=switch]]:mt-0.5', endpointChipRow: 'flex min-w-0 flex-wrap items-center gap-2', splitFooter: 'flex w-full items-center justify-between gap-3', footer: 'flex items-center justify-end gap-2', footerTextButton: - 'h-auto min-h-0 rounded-md px-0 py-0 text-xs leading-tight text-foreground-muted/60 shadow-none hover:bg-transparent hover:text-foreground-muted', + 'h-auto min-h-0 rounded-md px-0 py-0 text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-foreground-muted/60 shadow-none hover:bg-transparent hover:text-foreground-muted', healthCostWarning: - 'shrink-0 rounded-lg border-warning bg-warning-bg px-3 py-2.5 text-xs leading-tight text-warning shadow-none [&_[data-slot=alert-icon]]:mt-0 [&_[data-slot=alert-icon]_svg]:size-4 [&_[data-slot=alert-message]]:font-medium', + 'shrink-0 rounded-[length:var(--radius-lg)] border-[color:var(--color-warning-base)] bg-[color:var(--color-warning-bg)] px-3 py-2.5 text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-[color:var(--color-warning-base)] shadow-none [&_[data-slot=alert-icon]]:mt-0 [&_[data-slot=alert-icon]_svg]:size-4 [&_[data-slot=alert-message]]:font-medium', /** Model health-check drawer: determinate progress (scoped neutral track + primary fill). */ healthProgressTrack: 'h-1.5 w-full overflow-hidden rounded-full bg-[color:color-mix(in_srgb,var(--muted-foreground)_12%,transparent)]', healthProgressFill: 'h-full rounded-full bg-primary transition-[width] duration-300 ease-out', - healthProgressMeta: 'text-[13px] tabular-nums text-muted-foreground/85', - healthProgressCurrent: 'truncate text-[13px] text-foreground/80' + healthProgressMeta: 'text-[length:var(--font-size-caption)] tabular-nums text-muted-foreground/85', + healthProgressCurrent: 'truncate text-[length:var(--font-size-caption)] text-foreground/80' } as const +/** Model list block; composes atomic tokens from `provider-settings-scoped-theme.css` under `.provider-settings-default-scope`. */ export const modelListClasses = { - cqRoot: 'flex h-full min-h-0 min-w-0 w-full flex-1 flex-col gap-2.5', - section: 'flex h-full min-h-0 min-w-0 w-full flex-1 flex-col gap-2.5', + /** Inline-size container for `@container model-list` rules in `provider-settings-scoped-theme.css` (replaces JS width measurement). */ + cqRoot: 'ps-model-list-cq flex h-full min-h-0 min-w-0 w-full flex-1 flex-col gap-[length:var(--space-stack-sm)]', + section: 'flex h-full min-h-0 min-w-0 w-full flex-1 flex-col gap-[length:var(--space-stack-sm)]', headerBlock: 'flex min-h-0 min-w-0 w-full flex-1 flex-col gap-3', titleRow: 'flex min-w-0 w-full flex-wrap items-center justify-between gap-2.5', /** Model list header stack — matches model list block. */ headerToolStack: 'flex min-w-0 w-full flex-col gap-2', - titleWrap: 'flex w-full min-w-0 items-center gap-3', + titleWrap: 'flex w-full min-w-0 items-center gap-[length:var(--space-inline-md)]', titleActions: 'flex max-w-full shrink-0 flex-wrap items-center justify-end gap-2', toolbarDesignIcon: 'size-4 shrink-0', /** Connected top-row model list actions; uses shared ButtonGroup + Button outline primitives. */ toolbarButtonGroup: 'max-w-full shrink-0', - /** Model-list section title: same size, line-height, and color; semibold emphasis. */ + /** Model-list section title: same size, line-height, and color; scoped weight `--font-weight-semibold` (600). */ sectionTitleLine: 'flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1', sectionTitle: cn(sectionHeadingBase, 'shrink-0 whitespace-nowrap font-semibold'), titleHelpRow: 'flex min-w-0 flex-wrap items-center gap-x-1 self-center text-foreground-muted', titleHelpText: 'shrink-0 opacity-60', - titleHelpLink: 'mx-0 inline-flex shrink-0 items-center leading-[1.3] text-primary hover:underline', - titleHelpSeparator: 'inline-flex shrink-0 items-center leading-[1.3] text-foreground-muted/50', - countMeta: 'text-xs leading-tight text-foreground-muted tabular-nums', + titleHelpLink: + 'mx-0 inline-flex shrink-0 items-center leading-[var(--line-height-section-label)] !text-info hover:underline', + titleHelpSeparator: + 'inline-flex shrink-0 items-center leading-[var(--line-height-section-label)] text-foreground-muted/50', + countMeta: + 'text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-foreground-muted tabular-nums', toolbarGhost: - 'h-auto rounded-3xs px-2.5 py-[5px] text-[13px] leading-tight text-muted-foreground/70 shadow-none hover:bg-accent/40 hover:text-foreground', + 'h-auto rounded-3xs px-2.5 py-[5px] text-[length:var(--font-size-caption)] leading-[length:var(--line-height-caption)] text-muted-foreground/70 shadow-none hover:bg-[var(--color-surface-hover-soft)] hover:text-foreground', /** Model-list title-row ghost: one step tighter than `toolbarGhost` (padding + body-xs + small icon). */ toolbarHeaderGhost: - 'h-auto min-h-0 rounded px-2 py-0.5 text-xs leading-tight text-muted-foreground/70 shadow-none hover:bg-accent/40 hover:text-foreground', + 'h-auto min-h-0 rounded-[length:var(--radius-4xs)] px-[length:var(--padding-x-control-compact)] py-[length:var(--padding-y-control-compact)] text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-muted-foreground/70 shadow-none hover:bg-[var(--color-surface-fg-subtle)] hover:text-foreground', toolbarHeaderIconButton: - 'size-8 rounded p-0 text-muted-foreground/70 shadow-none hover:bg-accent/40 hover:text-foreground', - toolbarIcon: 'size-3 shrink-0', - toolbarHeaderIcon: 'size-3 shrink-0', + 'size-8 rounded-[length:var(--radius-4xs)] p-0 text-muted-foreground/70 shadow-none hover:bg-[var(--color-surface-fg-subtle)] hover:text-foreground', + toolbarIcon: 'size-[length:var(--icon-size-caption)] shrink-0', + toolbarHeaderIcon: 'size-[length:var(--icon-size-body-xs)] shrink-0', searchExpandRow: 'flex min-w-0 w-full flex-wrap items-center gap-2', searchRow: 'flex min-w-0 w-full flex-wrap items-center gap-2', searchActions: 'flex max-w-full shrink-0 flex-wrap items-center gap-2', searchWrap: - 'flex h-8 min-w-0 flex-1 items-center gap-1 rounded-[10px] border border-border-muted bg-background px-2.5 py-1', + 'flex h-8 min-w-0 flex-1 items-center gap-1 rounded-[10px] border border-[color:var(--color-border-fg-muted)] bg-background px-2.5 py-1', searchIcon: 'size-3 shrink-0 text-muted-foreground/65', searchInput: 'min-w-0 flex-1 border-none bg-transparent text-sm leading-5 text-foreground/80 outline-none placeholder:text-muted-foreground/75 disabled:cursor-not-allowed disabled:opacity-60', searchClear: - 'flex h-[18px] w-[18px] items-center justify-center rounded-full text-foreground/45 transition-colors hover:bg-accent/40 hover:text-foreground/65', + 'flex h-[18px] w-[18px] items-center justify-center rounded-full text-foreground/45 transition-colors hover:bg-[var(--color-surface-fg-subtle)] hover:text-foreground/65', fetchActionButton: - 'h-8 min-h-0 gap-1.5 rounded-[length:var(--cs-radius-md)] border-border-muted bg-background px-2.5 py-0 text-sm leading-5 text-foreground shadow-none hover:bg-accent/40 hover:text-foreground disabled:opacity-40 [&_svg]:size-3.5', + 'h-8 min-h-0 gap-1.5 rounded-[length:var(--cs-radius-md)] border-[color:var(--color-border-fg-muted)] bg-background px-2.5 py-0 text-sm leading-5 text-foreground shadow-none hover:bg-(--color-surface-fg-subtle-solid) hover:text-foreground disabled:opacity-40 [&_svg]:size-3.5 [&_svg]:[stroke-width:var(--icon-stroke)]', addModelIconButton: - 'size-8 min-h-0 rounded-[length:var(--cs-radius-md)] border-border-muted bg-background p-0 text-foreground shadow-none hover:bg-accent/40 hover:text-foreground disabled:opacity-40 [&_svg]:size-3.5', + 'size-8 min-h-0 rounded-[length:var(--cs-radius-md)] border-[color:var(--color-border-fg-muted)] bg-background p-0 text-foreground shadow-none hover:bg-(--color-surface-fg-subtle-solid) hover:text-foreground disabled:opacity-40 [&_svg]:size-3.5 [&_svg]:[stroke-width:var(--icon-stroke)]', addIconButton: - 'size-8 rounded-lg border-border-muted bg-transparent text-muted-foreground/70 shadow-none hover:bg-accent/40 hover:text-foreground', + 'size-8 rounded-lg border-[color:var(--color-border-fg-muted)] bg-transparent text-muted-foreground/70 shadow-none hover:bg-[var(--color-surface-fg-subtle)] hover:text-foreground', capabilityFilterRoot: 'flex min-w-0 shrink-0 items-center gap-1', capabilityFilterButton: - 'h-7 min-h-0 max-w-[170px] gap-1.5 rounded-[length:var(--cs-radius-md)] border-border-muted bg-background px-2 py-0 text-xs leading-tight text-foreground shadow-none hover:bg-accent/40 hover:text-foreground disabled:opacity-40', - capabilityFilterButtonIconOnly: 'size-7 px-0', - capabilityFilterButtonActive: 'border-border-active bg-accent/40', + 'h-8 min-h-0 max-w-[170px] gap-1.5 rounded-[length:var(--cs-radius-md)] border-[color:var(--color-border-fg-muted)] bg-background px-2 py-0 text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-foreground shadow-none hover:bg-(--color-surface-fg-subtle-solid) hover:text-foreground disabled:opacity-40 [&_svg]:[stroke-width:var(--icon-stroke)]', + capabilityFilterButtonIconOnly: 'size-8 px-0', + capabilityFilterButtonActive: 'border-[color:var(--color-border-active)] bg-[var(--color-surface-fg-subtle)]', capabilityFilterLabel: 'min-w-0 truncate', capabilityFilterClear: - 'inline-flex size-5 min-h-0 shrink-0 items-center justify-center rounded-md p-0 text-muted-foreground/45 transition-colors hover:bg-accent/40 hover:text-muted-foreground/80', + 'inline-flex size-5 min-h-0 shrink-0 items-center justify-center rounded-md p-0 text-muted-foreground/45 transition-colors hover:bg-[var(--color-surface-fg-subtle)] hover:text-muted-foreground/80', capabilityFilterMenu: 'w-fit min-w-40 rounded-xl p-1.5', capabilityFilterMenuItem: 'h-8 rounded-lg px-2.5 text-sm', - capabilityTabIcon: 'size-3 shrink-0', + capabilityTabIcon: 'size-3.5 shrink-0', subsectionRow: 'flex min-w-0 items-center gap-2 px-1', subsectionTitleWrap: 'flex min-w-0 items-center gap-2', subsectionActions: 'ml-1 flex shrink-0 items-center gap-2', subsectionIconButton: - 'inline-flex size-5 min-h-0 shrink-0 items-center justify-center rounded-md p-0 text-muted-foreground/80 shadow-none hover:bg-accent/40 hover:text-foreground disabled:opacity-40', + 'inline-flex size-5 min-h-0 shrink-0 items-center justify-center rounded-md p-0 text-muted-foreground/80 shadow-none hover:bg-(--color-surface-fg-subtle-solid) hover:text-foreground disabled:opacity-40', subsectionIcon: 'size-4 shrink-0', listActionTriggerButton: - 'inline-flex size-6 min-h-0 shrink-0 items-center justify-center rounded-md p-0 text-muted-foreground/55 shadow-none hover:bg-accent/40 hover:text-foreground/80 disabled:opacity-40', + 'inline-flex size-6 min-h-0 shrink-0 items-center justify-center rounded-md p-0 text-muted-foreground/55 shadow-none hover:bg-[var(--color-surface-fg-subtle)] hover:text-foreground/80 disabled:opacity-40', listActionTriggerIcon: 'size-4 shrink-0', listActionMenu: 'w-fit min-w-40 rounded-xl p-1.5', listActionMenuItem: 'h-9 rounded-lg px-3 text-sm', listActionMenuIcon: 'size-3.5 text-muted-foreground/70', subsectionTooltipTrigger: 'inline-flex size-5 min-h-0 shrink-0 items-center justify-center leading-none', - subsectionTitleEnabled: 'text-sm leading-5 text-foreground font-semibold', - subsectionCountEnabled: 'text-sm leading-5 text-foreground-muted tabular-nums font-medium', - subsectionTitleDisabled: 'text-sm leading-5 text-foreground font-semibold', - subsectionCountDisabled: 'text-sm leading-5 text-foreground-muted tabular-nums font-medium', + subsectionTitleEnabled: + 'text-[length:var(--font-size-body-sm)] leading-[var(--line-height-body-sm)] text-foreground font-medium', + subsectionCountEnabled: + 'text-[length:var(--font-size-body-sm)] leading-[var(--line-height-body-sm)] text-foreground-muted tabular-nums font-medium', + subsectionTitleDisabled: + 'text-[length:var(--font-size-body-sm)] leading-[var(--line-height-body-sm)] text-foreground font-medium', + subsectionCountDisabled: + 'text-[length:var(--font-size-body-sm)] leading-[var(--line-height-body-sm)] text-foreground-muted tabular-nums font-medium', emptyState: - 'flex min-h-40 items-center justify-center rounded-2xl border border-border border-dashed bg-muted/30 px-4 text-center text-sm leading-5 text-foreground-muted', + 'flex min-h-40 items-center justify-center rounded-2xl border border-(--color-border) border-dashed bg-[var(--color-surface-fg-sunken)] px-4 text-center text-[length:var(--font-size-body-md)] leading-[var(--line-height-body-md)] text-foreground-muted', listScroller: 'min-h-0 min-w-0 w-full flex-1 overflow-x-hidden pr-1', /** * — grouped catalog inside manage drawer (flat headers, no collapse). */ manageListGroupShell: 'mb-1', manageListGroupHeader: 'flex items-center gap-1.5 px-1 py-[3px]', - manageListGroupTitle: 'font-medium text-xs leading-tight text-foreground-muted', + manageListGroupTitle: + 'font-medium text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-foreground-muted', manageListGroupRule: 'h-px min-w-0 flex-1 bg-muted/50', manageListRow: 'group flex items-center gap-2 rounded-lg px-1.5 py-[5px] transition-colors hover:bg-accent/50', manageListRowLast: 'mb-0.5', @@ -265,18 +302,19 @@ export const modelListClasses = { manageDrawerCapChipActive: 'bg-accent/50 !text-foreground', manageDrawerCapChipIdle: 'text-foreground-muted hover:bg-accent/50 hover:text-foreground', manageDrawerCountBadge: - 'shrink-0 rounded-full bg-muted/50 px-1.5 py-[1px] text-xs text-muted-foreground/60 tabular-nums', + 'shrink-0 rounded-full bg-muted/50 px-1.5 py-[1px] text-[length:var(--font-size-body-xs)] text-muted-foreground/60 tabular-nums', /** Trailing close in manage drawer title row (paired with bulk actions); matches `hover:bg-accent`. */ manageDrawerCloseInTitle: - "ml-1 !size-6 !min-h-6 shrink-0 gap-0 rounded-lg p-0 text-muted-foreground/60 shadow-none hover:bg-accent hover:text-foreground [&_svg:not([class*='size-'])]:size-[11px]", + "ml-1 !size-6 !min-h-6 shrink-0 gap-0 rounded-[length:var(--radius-control)] p-0 text-muted-foreground/60 shadow-none hover:bg-accent hover:text-foreground [&_svg:not([class*='size-'])]:size-[11px]", manageDrawerBulkGhost: - 'inline-flex !h-auto !min-h-0 items-center justify-center gap-1 rounded-lg px-1.5 py-[2px] text-xs font-medium tracking-[-0.14px] text-muted-foreground/60 shadow-none transition-colors hover:bg-accent has-[>svg]:px-1.5', - /** Enable-all hover — primary action color. */ + 'inline-flex !h-auto !min-h-0 items-center justify-center gap-1 rounded-[length:var(--radius-control)] px-1.5 py-[2px] text-[length:var(--font-size-body-xs)] font-medium tracking-[-0.14px] text-muted-foreground/60 shadow-none transition-colors hover:bg-accent has-[>svg]:px-1.5', + /** Enable-all hover — brand `--primary` in this shell (design `hover:text-cherry-primary`). */ manageDrawerBulkGhostEnableHover: 'hover:!text-primary', /** Disable-all hover — destructive (design draft). */ manageDrawerBulkGhostDisableHover: 'hover:!text-destructive', /** Provider-grouped card: bordered shell with leading chevron; rows render inside the same card on expand. */ - groupCard: 'group/modelGroup min-w-0 w-full rounded-md border border-border-subtle bg-transparent px-2 py-1', + groupCard: + 'group/modelGroup min-w-0 w-full rounded-[length:var(--radius-xl)] border border-[color:var(--color-border-fg-hairline)] bg-transparent px-2 py-1', groupHeader: 'group/groupRow flex min-h-7 w-full items-center justify-between gap-2 bg-transparent text-left outline-none focus-visible:outline-none', groupToggleButton: @@ -284,120 +322,146 @@ export const modelListClasses = { groupHeaderActions: 'flex h-6 shrink-0 items-center gap-1', groupHeaderIconTooltipTrigger: 'inline-flex h-6 shrink-0 items-center justify-center leading-none', groupSwitchTooltipTrigger: 'inline-flex h-6 shrink-0 items-center justify-center leading-none', - groupTitle: 'min-w-0 truncate text-sm leading-5 text-foreground font-medium', + groupTitle: + 'min-w-0 flex-1 truncate text-[length:var(--font-size-body-sm)] leading-[var(--line-height-body-sm)] text-foreground font-medium', groupChevron: - 'size-4 shrink-0 text-muted-foreground/65 transition-[transform,color] duration-150 group-hover/groupRow:text-foreground', + 'size-3 shrink-0 text-muted-foreground/30 transition-[transform,color] duration-150 group-hover/groupRow:text-foreground', groupChevronOpen: 'rotate-90', groupBody: 'mt-0.5 flex flex-col gap-0.5', groupOverflowHint: - 'mt-1 rounded-lg px-3 py-2 text-left text-[13px] leading-tight text-muted-foreground/70 transition-colors hover:bg-accent/40 hover:text-foreground', + 'mt-1 rounded-lg px-3 py-2 text-left text-[length:var(--font-size-caption)] leading-[var(--line-height-caption)] text-muted-foreground/70 transition-colors hover:bg-[var(--color-surface-fg-subtle)] hover:text-foreground', row: 'group flex min-h-11 items-center gap-3 py-2 text-foreground leading-none', rowMain: 'min-w-0 flex-1 items-center gap-3 self-center', rowAvatar: 'h-[26px] w-[26px] shrink-0 rounded-lg', rowBody: 'flex min-w-0 max-w-full flex-1 items-center overflow-hidden', /** Model name opens the edit drawer; the settings icon is the explicit secondary action. */ rowNameCopyable: 'cursor-pointer', + /** Shown when model id !== name; hidden in narrow container via `.ps-model-list-id` rule. */ modelIdBadge: - 'min-w-0 max-w-[50%] shrink truncate rounded-md bg-foreground/[0.05] px-1.5 py-[1px] font-mono text-xs text-foreground-muted leading-tight', + 'ps-model-list-id min-w-0 max-w-[50%] shrink truncate rounded-md bg-foreground/[0.05] px-1.5 py-[1px] font-mono text-[length:var(--font-size-body-xs)] text-foreground-muted leading-[var(--line-height-body-xs)]', rowBadges: 'mt-1 flex min-h-[18px] min-w-0 max-w-full flex-wrap items-center gap-1.5', /** Capability / trial tags to the left of the enable switch; design: single line with the toggle. */ rowCapabilityStrip: 'flex h-7 min-w-0 max-w-[min(100%,20rem)] shrink items-center gap-1.5 overflow-x-auto overflow-y-hidden [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden', - rowCapabilityTagCluster: 'flex min-w-0 shrink items-center', - rowMeta: 'mt-[3px] block min-w-0 max-w-full truncate text-xs leading-tight text-foreground/65', - healthStatusSlot: 'shrink-0', + /** Wraps `ModelTagsWithLabel` only; pairs with `.ps-model-list-cap-strip` rules in `provider-settings-scoped-theme.css`. */ + rowCapabilityTagCluster: 'ps-compact-cap-strip flex min-w-0 shrink items-center', + rowMeta: + 'ps-model-list-meta mt-[3px] block min-w-0 max-w-full truncate text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-foreground/65', + /** Wraps `HealthStatusIndicator` so latency (antd Typography) can be hidden via container query. */ + healthStatusSlot: 'ps-model-list-health shrink-0', /** Trailing column: health + (capability strip + enable) on one row. */ rowActionsCluster: 'flex min-h-7 min-w-0 items-center gap-2', rowActions: 'min-w-0 shrink-0 items-center gap-1.5 self-center', rowIconButton: - 'size-7 rounded-lg border border-border-muted bg-transparent text-muted-foreground/70 shadow-none hover:bg-accent/40 hover:text-foreground' + 'size-7 rounded-lg border border-[color:var(--color-border-fg-muted)] bg-transparent text-muted-foreground/70 shadow-none hover:bg-[var(--color-surface-fg-subtle)] hover:text-foreground' } as const export const modelSyncClasses = { - panel: 'flex min-h-0 flex-1 flex-col gap-4', - summaryCard: 'rounded-2xl border border-border-muted bg-muted/30 px-4 py-3', - summaryTitle: 'text-sm leading-5 text-foreground/85 font-medium', - summaryMeta: 'text-[13px] leading-tight text-muted-foreground/75', + panel: 'provider-settings-default-scope flex min-h-0 flex-1 flex-col gap-4', + summaryCard: + 'rounded-2xl border border-[color:var(--color-border-fg-muted)] bg-[var(--color-surface-fg-sunken)] px-4 py-3', + summaryTitle: + 'text-[length:var(--font-size-body-md)] leading-[var(--line-height-body-md)] text-foreground/85 font-medium', + summaryMeta: 'text-[length:var(--font-size-caption)] leading-[var(--line-height-caption)] text-muted-foreground/75', summaryGrid: 'mt-3 grid gap-2 sm:grid-cols-3', summaryMetric: - 'rounded-xl border border-border-subtle bg-background/75 px-3 py-2 text-[13px] leading-tight text-foreground/75', - section: 'rounded-2xl border border-border-muted bg-background px-4 py-4 shadow-none', + 'rounded-xl border border-[color:var(--color-border-fg-hairline)] bg-background/75 px-3 py-2 text-[length:var(--font-size-caption)] leading-[var(--line-height-caption)] text-foreground/75', + warningBlock: + 'rounded-2xl border border-[color:var(--color-border-warning-soft)] bg-[var(--color-surface-warning-soft)] px-4 py-3 text-[length:var(--font-size-caption)] leading-[var(--line-height-body-md)] text-foreground/80', + section: 'rounded-2xl border border-[color:var(--color-border-fg-muted)] bg-background px-4 py-4 shadow-none', sectionHeader: 'flex flex-wrap items-center justify-between gap-3', sectionTitleWrap: 'min-w-0', - sectionTitle: 'text-sm leading-5 text-foreground/85 font-medium', - sectionMeta: 'text-[13px] leading-tight text-muted-foreground/75', + sectionTitle: + 'text-[length:var(--font-size-body-md)] leading-[var(--line-height-body-md)] text-foreground/85 font-medium', + sectionMeta: 'text-[length:var(--font-size-caption)] leading-[var(--line-height-caption)] text-muted-foreground/75', sectionActions: 'flex flex-wrap items-center gap-2', toggleButton: cn( actionClasses.btnBase, actionClasses.btnNeutral, - 'rounded-lg border-border-muted px-3 py-1.25 text-foreground/70 hover:bg-accent/40 hover:text-foreground' + 'rounded-lg border-(--color-border-fg-muted) px-3 py-1.25 text-foreground/70 hover:bg-(--color-surface-fg-subtle) hover:text-foreground' ), list: 'mt-4 space-y-2', - row: 'flex items-start gap-3 rounded-xl border border-border-subtle bg-muted/30 px-3 py-3', + row: 'flex items-start gap-3 rounded-xl border border-[color:var(--color-border-fg-hairline)] bg-[var(--color-surface-fg-sunken)] px-3 py-3', rowBody: 'min-w-0 flex-1', - rowTitle: 'truncate text-sm leading-5 text-foreground/85', - rowMeta: 'mt-1 text-[13px] leading-tight text-muted-foreground/75', + rowTitle: 'truncate text-[length:var(--font-size-body-md)] leading-[var(--line-height-body-md)] text-foreground/85', + rowMeta: 'mt-1 text-[length:var(--font-size-caption)] leading-[var(--line-height-caption)] text-muted-foreground/75', rowBadgeRow: 'mt-2 flex flex-wrap items-center gap-1.5', rowBadge: - 'rounded-full border border-border-muted bg-background px-2 py-0.5 text-xs leading-tight text-foreground/65', + 'rounded-full border border-[color:var(--color-border-fg-muted)] bg-background px-2 py-0.5 text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-foreground/65', + rowDangerBadge: + 'rounded-full border border-[color:var(--color-border-warning-soft)] bg-[var(--color-surface-warning-soft)] px-2 py-0.5 text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-foreground/75', + impactCard: + 'rounded-2xl border border-[color:var(--color-border-info-soft)] bg-[var(--color-surface-info-soft)] px-4 py-4', impactList: 'mt-3 space-y-2', impactItem: - 'rounded-xl border border-border-subtle bg-background/80 px-3 py-2 text-[13px] leading-5 text-foreground/78', + 'rounded-xl border border-[color:var(--color-border-fg-hairline)] bg-background/80 px-3 py-2 text-[length:var(--font-size-caption)] leading-[var(--line-height-body-md)] text-foreground/78', emptyState: - 'rounded-2xl border border-dashed border-border-muted bg-muted/30 px-4 py-8 text-center text-sm leading-5 text-muted-foreground/75', + 'rounded-2xl border border-dashed border-[color:var(--color-border-fg-muted)] bg-[var(--color-surface-fg-sunken)] px-4 py-8 text-center text-[length:var(--font-size-body-md)] leading-[var(--line-height-body-md)] text-muted-foreground/75', footer: 'flex items-center justify-end gap-2', /** pull preview panel — pull result side panel */ fetchEmpty: 'flex flex-col items-center justify-center px-4 py-12 text-center', fetchEmptyIconWrap: 'mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-muted', fetchEmptyIcon: 'size-4 text-foreground-muted', - fetchEmptyTitle: 'font-medium text-xs leading-tight text-foreground-secondary', - fetchEmptyDescription: 'mt-1 text-xs leading-tight text-foreground-muted', + fetchEmptyTitle: + 'font-medium text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-foreground', + fetchEmptyDescription: + 'mt-1 text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-foreground-muted', fetchSection: 'min-w-0', fetchSectionHeader: 'mb-2.5 flex items-center justify-between gap-3', fetchSectionTitleRow: 'flex items-center gap-1.5', fetchDotNew: 'h-[6px] w-[6px] shrink-0 rounded-full bg-primary', fetchDotRemoved: 'h-[6px] w-[6px] shrink-0 rounded-full bg-destructive', - fetchSectionTitle: 'text-sm font-medium text-foreground leading-5', - fetchSectionCount: 'text-xs leading-tight text-foreground-muted tabular-nums', + fetchSectionTitle: + 'text-[length:var(--font-size-body-sm)] font-medium text-foreground leading-[var(--line-height-body-sm)]', + fetchSectionCount: + 'text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-foreground-muted tabular-nums', fetchGhostAll: - 'inline-flex !h-auto !min-h-0 items-center justify-center rounded-lg px-2 py-[3px] !text-xs !leading-none text-foreground-muted shadow-none hover:bg-accent hover:text-foreground', + 'inline-flex !h-auto !min-h-0 items-center justify-center rounded-[length:var(--radius-control)] px-2 py-[3px] !text-[length:var(--font-size-body-xs)] !leading-none text-foreground-muted shadow-none hover:bg-accent hover:text-foreground', fetchGhostAllRemoved: - 'inline-flex !h-auto !min-h-0 items-center justify-center rounded-lg px-2 py-[3px] !text-xs !leading-none text-foreground-muted shadow-none hover:bg-destructive/10 hover:text-destructive', + 'inline-flex !h-auto !min-h-0 items-center justify-center rounded-[length:var(--radius-control)] px-2 py-[3px] !text-[length:var(--font-size-body-xs)] !leading-none text-foreground-muted shadow-none hover:bg-destructive/10 hover:text-destructive', fetchList: 'space-y-1', fetchWarning: - 'my-2 gap-2 rounded-lg border-[color:color-mix(in_srgb,var(--color-warning-base)_35%,transparent)] bg-[color:color-mix(in_srgb,var(--color-warning-bg)_52%,transparent)] px-2.5 py-2 text-xs leading-tight shadow-none [&_[data-slot=alert-icon]]:mt-0 [&_[data-slot=alert-icon]_svg]:size-3.5 [&_[data-slot=alert-message]]:font-normal', + 'my-2 gap-2 rounded-[length:var(--radius-lg)] border-[color:color-mix(in_srgb,var(--color-warning-base)_35%,transparent)] bg-[color:color-mix(in_srgb,var(--color-warning-bg)_52%,transparent)] px-2.5 py-2 text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] shadow-none [&_[data-slot=alert-icon]]:mt-0 [&_[data-slot=alert-icon]_svg]:size-3.5 [&_[data-slot=alert-message]]:font-normal', fetchRowNew: - 'flex min-h-11 cursor-pointer items-center gap-2 rounded-lg border border-transparent px-2.5 py-2 transition-colors hover:border-border/60 hover:bg-accent/30 focus-visible:outline-none focus-visible:ring-[2px] focus-visible:ring-ring/30 data-[checked=true]:border-border/40 data-[checked=true]:bg-background', + 'flex min-h-9 cursor-pointer items-center gap-2 rounded-[length:var(--radius-lg)] border border-transparent px-2.5 py-2 transition-colors hover:border-border/60 hover:bg-accent/30 focus-visible:outline-none focus-visible:ring-[2px] focus-visible:ring-ring/30 data-[checked=true]:border-border/40 data-[checked=true]:bg-background', fetchRowRemoved: - 'flex min-h-11 cursor-pointer items-center gap-2 rounded-lg border border-transparent px-2.5 py-2 transition-colors hover:border-destructive/15 hover:bg-destructive/[0.03] focus-visible:outline-none focus-visible:ring-[2px] focus-visible:ring-ring/30 data-[checked=true]:border-destructive/15 data-[checked=true]:bg-background', + 'flex min-h-9 cursor-pointer items-center gap-2 rounded-[length:var(--radius-lg)] border border-transparent px-2.5 py-2 transition-colors hover:border-destructive/15 hover:bg-destructive/[0.03] focus-visible:outline-none focus-visible:ring-[2px] focus-visible:ring-ring/30 data-[checked=true]:border-destructive/15 data-[checked=true]:bg-background', fetchAvatar: - 'flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-muted font-medium text-xs leading-none text-foreground-muted', - fetchRowTitle: 'truncate text-sm font-medium leading-tight text-foreground', + 'flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-muted font-medium text-[length:var(--font-size-body-xs)] leading-none text-foreground-muted', + fetchRowTitle: + 'truncate text-[length:var(--font-size-body-sm)] font-medium leading-[var(--line-height-body-xs)] text-foreground', fetchRowTitleStrike: - 'truncate text-sm font-medium leading-tight text-foreground-muted line-through decoration-foreground-muted', - fetchRowId: 'mt-0.5 truncate font-mono text-xs leading-tight text-foreground-muted', - fetchRowIdStrike: 'mt-0.5 truncate font-mono text-xs leading-tight text-foreground-muted/70', - fetchContextValue: 'shrink-0 text-xs leading-tight text-foreground-muted tabular-nums', + 'truncate text-[length:var(--font-size-body-sm)] font-medium leading-[var(--line-height-body-xs)] text-foreground-muted line-through decoration-foreground-muted', + fetchRowId: + 'mt-0.5 truncate font-mono text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-foreground', + fetchRowIdStrike: + 'mt-0.5 truncate font-mono text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-foreground', + fetchContextValue: + 'shrink-0 text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-foreground-muted tabular-nums', /** Trailing capability icons — pull preview panel strip */ - fetchCapabilityStrip: 'flex shrink-0 items-center justify-end gap-[3px]' + fetchCapabilityStrip: 'ps-compact-cap-strip flex shrink-0 items-center justify-end gap-[3px]' } as const export const apiKeyListClasses = { - summaryMeta: 'text-xs leading-tight text-foreground-muted tabular-nums', - helperText: 'text-[13px] leading-tight text-foreground-muted', - listWrap: 'overflow-hidden rounded-lg border border-border-muted', + summaryMeta: + 'text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-foreground-muted tabular-nums', + helperText: 'text-[length:var(--font-size-caption)] leading-[var(--line-height-caption)] text-foreground-muted', + listWrap: 'overflow-hidden rounded-lg border border-[color:var(--color-border-fg-muted)]', listScroller: 'max-h-[60vh] overflow-x-hidden', - keyRow: 'flex flex-col gap-2 border-b border-border-subtle px-4 py-3 last:border-b-0', + keyRow: 'flex flex-col gap-2 border-b border-[color:var(--color-border-fg-hairline)] px-4 py-3 last:border-b-0', keyDisplayRow: 'flex min-w-0 items-center gap-3', keyTextBlock: 'min-w-0 flex-1', keyRowActions: 'flex shrink-0 items-center gap-1.5', - keyLabel: 'min-w-0 truncate text-[13px] leading-tight text-foreground font-medium', - keyValue: 'min-w-0 flex-1 truncate font-mono text-xs leading-tight text-foreground-muted', + keyLabel: + 'min-w-0 truncate text-[length:var(--font-size-caption)] leading-[var(--line-height-caption)] text-foreground font-medium', + keyValue: + 'min-w-0 flex-1 truncate font-mono text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] text-foreground-muted', keyDraftRow: 'flex min-w-0 items-center gap-2', keyDraftInputs: 'grid min-w-0 flex-1 gap-2 sm:grid-cols-[minmax(4.5rem,6rem)_minmax(0,1fr)]', - keyDraftInput: 'h-8 rounded-md bg-background px-2.5 text-sm leading-5', + keyDraftInput: + 'h-8 rounded-[length:var(--radius-md)] bg-background px-2.5 text-[length:var(--font-size-body-sm)] leading-[var(--line-height-body-sm)]', keyIconButton: - 'inline-flex size-5 shrink-0 items-center justify-center rounded-md text-muted-foreground/40 transition-colors hover:bg-accent/40 hover:text-muted-foreground/70 disabled:pointer-events-none disabled:opacity-30 [&_svg]:size-3', + 'inline-flex size-5 shrink-0 items-center justify-center rounded-md text-muted-foreground/40 transition-colors hover:bg-[var(--color-surface-fg-subtle)] hover:text-muted-foreground/70 disabled:pointer-events-none disabled:opacity-30 [&_svg]:size-3', keySaveIconButton: 'inline-flex size-5 shrink-0 items-center justify-center rounded-md text-muted-foreground/40 transition-colors hover:bg-success/10 hover:text-success disabled:pointer-events-none disabled:opacity-30 [&_svg]:size-3', keyDestructiveIconButton: @@ -409,46 +473,51 @@ export const oauthCardClasses = { /** Fills the auth column; no max-width so the card tracks the detail pane (fluid layout). */ container: 'w-full min-w-0', /** Large bordered auth card, no shadow or filled background. */ - shell: 'w-full min-w-0 overflow-hidden rounded-xl border border-border-subtle px-3 py-2.5', + shell: + 'w-full min-w-0 overflow-hidden rounded-[length:var(--radius-xl)] border border-[color:var(--color-border-fg-hairline)] px-4 py-3', loginFooterRow: 'mt-2.5 flex items-center justify-center gap-4', loginFooterLink: - 'h-auto min-h-0 p-0 text-xs text-muted-foreground/60 shadow-none hover:bg-transparent hover:text-foreground', - loginFooterDivider: 'text-xs text-muted-foreground/50', + 'h-auto min-h-0 p-0 text-[length:var(--font-size-body-xs)] text-muted-foreground/60 shadow-none hover:bg-transparent hover:text-foreground', + loginFooterDivider: 'text-[length:var(--font-size-body-xs)] text-muted-foreground/50', /** CherryIN portal link — matches scoped caption + primary link treatment. */ - externalLink: 'mt-1 inline-block text-xs leading-tight text-primary hover:underline', + externalLink: + 'mt-1 inline-block text-[length:var(--font-size-body-xs)] leading-[var(--line-height-body-xs)] !text-info hover:underline', /** Logged-in CherryIN: mock CherryIN account section — one row, no stat grid. */ - shellLoggedIn: 'w-full min-w-0 overflow-hidden rounded-xl border border-border-subtle px-3 py-2.5', - loggedInRow: 'flex w-full min-w-0 flex-wrap items-center justify-between gap-3', + shellLoggedIn: + 'w-full min-w-0 overflow-hidden rounded-[length:var(--radius-xl)] border border-[color:var(--color-border-fg-hairline)] px-4 py-3', + loggedInRow: + 'flex w-full min-w-0 flex-col items-stretch gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between', profileMeta: 'flex min-w-0 flex-1 items-center gap-3', /** Avatar: 32px round avatar, primary fill, initials (/ CherryIN row). */ avatarSm: 'flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary text-xs font-semibold text-white', nameBlock: 'min-w-0', nameRow: 'flex flex-wrap items-center gap-1.5', - name: 'truncate text-[15px] leading-[1.2] font-semibold tracking-tight text-foreground', + name: 'truncate text-(length:--font-size-body-md) leading-[1.2] font-semibold tracking-tight text-foreground', /** Logged-in title line — `text-xs` in structured. */ - loggedInName: 'truncate text-xs font-medium leading-tight text-foreground', - loggedInEmail: 'mt-0.5 truncate text-xs leading-[1.35] text-muted-foreground/40', + loggedInName: 'truncate text-[length:var(--font-size-body-xs)] font-medium leading-tight text-foreground', + loggedInEmail: 'mt-0.5 truncate text-[length:var(--font-size-body-xs)] leading-[1.35] text-muted-foreground/40', badge: - 'inline-flex items-center rounded bg-[color:color-mix(in_srgb,var(--warning)_10%,transparent)] px-1 py-[0.5px] text-[10px] font-medium leading-tight text-[color:var(--warning)]', + 'inline-flex items-center rounded bg-[color:color-mix(in_srgb,var(--warning)_10%,transparent)] px-1 py-[0.5px] text-(length:--font-size-body-2xs) font-medium leading-tight text-[color:var(--warning)]', loggedInActions: 'flex shrink-0 flex-wrap items-center justify-end gap-2', inlineBalanceBlock: 'text-right', - inlineBalanceLabel: 'text-xs text-muted-foreground/40', + inlineBalanceLabel: 'text-[length:var(--font-size-body-xs)] text-muted-foreground/40', inlineBalanceValue: 'text-sm font-semibold leading-tight text-foreground tabular-nums', balanceValueSkeleton: 'inline-block w-20', /** CherryIN top-up CTA — solid primary background, white label (compact inline size). */ topupPrimaryButton: 'h-auto min-h-0 px-2.5 py-[3px] text-xs shadow-none', logoutCompact: 'h-auto min-h-0 rounded-md px-1.5 py-[3px] text-xs text-muted-foreground/30 shadow-none hover:bg-transparent hover:text-foreground', - serviceAttribution: 'mt-2.5 border-t border-border-subtle pt-2.5 text-xs text-muted-foreground/25', + serviceAttribution: + 'mt-2.5 border-t border-[color:var(--color-border-fg-hairline)] pt-2.5 text-[length:var(--font-size-body-xs)] text-muted-foreground/25', serviceLink: 'text-muted-foreground/40 transition-colors hover:text-foreground', actionsRow: 'flex flex-wrap items-center gap-2', - footer: 'mt-4 text-[12px] leading-[1.35] text-foreground-muted' + footer: 'mt-4 text-(length:--font-size-body-xs) leading-[1.35] text-foreground-muted' } as const -/** Shared visual for provider-settings icon buttons; size is composed per usage. */ +/** Shared visual for provider-settings icon buttons (bordered, cherry-* hover); size is composed per usage. */ const fieldIconButtonBase = - 'flex shrink-0 items-center justify-center rounded-lg border border-border-muted bg-background text-muted-foreground/70 transition-colors hover:bg-accent/40 hover:text-foreground disabled:pointer-events-none disabled:opacity-40' + 'flex shrink-0 items-center justify-center rounded-lg border border-[color:var(--color-border-fg-muted)] text-foreground transition-colors hover:bg-(--color-surface-fg-subtle-solid) hover:text-foreground disabled:pointer-events-none disabled:opacity-40 [&_svg]:[stroke-width:var(--icon-stroke)]' export const fieldClasses = { inputRow: 'flex min-w-0 items-center gap-1.5', @@ -472,15 +541,21 @@ export const fieldClasses = { */ input: 'min-h-0 h-auto min-w-0 flex-1 border-0 bg-transparent p-0 shadow-none outline-none focus-visible:ring-0 ' + - 'text-sm leading-5 text-foreground ' + - 'placeholder:text-muted-foreground/60 md:text-sm', + 'text-[length:var(--font-size-body-md)] leading-[var(--line-height-body-md)] text-foreground ' + + 'placeholder:text-muted-foreground/60 md:text-[length:var(--font-size-body-md)]', /** Small 24px icon control (e.g. copy / inline settings) — for compact rows, not next to a full input. */ iconButton: cn(fieldIconButtonBase, 'size-6'), - /** 32px icon control that matches the connection input-group height (`h-8`) when placed beside it in an `inputRow`. */ - inputActionButton: cn(fieldIconButtonBase, 'size-8'), + /** + * 32px icon control that matches the connection input-group height (`h-8`) when placed beside it in an `inputRow`. + * Apply to `