refactor(provider-settings): connection slots + page shell

Signed-off-by: Siin Xu <31815270+SiinXu@users.noreply.github.com>
This commit is contained in:
Siin Xu
2026-07-02 20:39:27 -07:00
parent b24998f699
commit ab7263cd32
15 changed files with 634 additions and 216 deletions

View File

@@ -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}

View File

@@ -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 (
<ProviderField
title={t('settings.provider.api_host')}
titleClassName="text-foreground"
titleClassName="text-foreground font-normal"
help={
<div className="space-y-1 pt-1">
{isVertexAI && (
@@ -86,20 +84,21 @@ export function ApiHostField({
</div> */}
</div>
}>
{isCherryIN && isChineseUser ? (
{isCherryIN ? (
<div className={cn(fieldClasses.inputRow, 'group')}>
<div className="flex min-w-0 flex-1">
<CherryInSettings providerId={providerIdForSettings} />
</div>
<Tooltip content={t('settings.provider.request_configuration_tooltip')}>
<span className="inline-flex shrink-0">
<button
<Button
type="button"
variant="outline"
className={fieldClasses.inputActionButton}
aria-label={t('settings.provider.request_configuration_tooltip')}
onClick={onOpenRequestConfig}>
<Settings size={14} aria-hidden />
</button>
</Button>
</span>
</Tooltip>
</div>
@@ -135,27 +134,29 @@ export function ApiHostField({
{isApiHostResettable ? (
<Tooltip content={t('settings.provider.api.url.reset')}>
<span className="inline-flex shrink-0">
<button
<Button
type="button"
variant="outline"
className={fieldClasses.inputActionButton}
aria-label={t('settings.provider.api.url.reset')}
onClick={() => {
onResetApiHost()
}}>
<RotateCcw size={14} />
</button>
</Button>
</span>
</Tooltip>
) : null}
<Tooltip content={t('settings.provider.request_configuration_tooltip')}>
<span className="inline-flex shrink-0">
<button
<Button
type="button"
variant="outline"
className={fieldClasses.inputActionButton}
aria-label={t('settings.provider.request_configuration_tooltip')}
onClick={onOpenRequestConfig}>
<Settings size={14} aria-hidden />
</button>
</Button>
</span>
</Tooltip>
</div>
@@ -216,13 +217,14 @@ export function AnthropicApiHostField({
</InputGroup>
<Tooltip content={t('settings.provider.request_configuration_tooltip')}>
<span className="inline-flex shrink-0">
<button
<Button
type="button"
variant="outline"
className={fieldClasses.inputActionButton}
aria-label={t('settings.provider.request_configuration_tooltip')}
onClick={onOpenRequestConfig}>
<Settings size={14} aria-hidden />
</button>
</Button>
</span>
</Tooltip>
</div>

View File

@@ -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 (
<>
<ProviderSection id={provider.id === 'cherryin' ? 'cherryin-api-key-section' : undefined}>
@@ -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' && (
<InputGroupAddon align="inline-end" className="-mr-0.5 pr-0">
@@ -96,19 +101,21 @@ export default function ApiKey({
</InputGroup>
<Tooltip content={t('settings.provider.api.key.list.title')}>
<span className="inline-flex shrink-0">
<button
<Button
variant="outline"
type="button"
disabled={provider.id === 'copilot'}
disabled={provider.id === 'copilot' || isOAuthLoggedIn}
className={fieldClasses.inputActionButton}
aria-label={t('settings.provider.api.key.list.title')}
onClick={() => setKeyListOpen(true)}>
<KeyRound size={14} />
</button>
</Button>
</span>
</Tooltip>
<Tooltip content={t('settings.provider.check')}>
<span className="inline-flex shrink-0">
<button
<Button
variant="outline"
type="button"
disabled={provider.id === 'copilot' || !inputApiKey || apiKeyConnectivity.checking}
className={fieldClasses.inputActionButton}
@@ -119,7 +126,7 @@ export default function ApiKey({
) : (
<Activity size={14} />
)}
</button>
</Button>
</span>
</Tooltip>
</div>

View File

@@ -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 (
<section className="shrink-0 space-y-4">
<ProviderSpecificSettings providerId={providerId} placement="beforeAuth" />
<div className="flex flex-col gap-3">
<div className={authConnectionClasses.shell}>
<div className={authConnectionClasses.body}>
{children}
<ProviderSpecificSettings providerId={providerId} placement="afterAuth" />
</div>
<section className="shrink-0">
<h2 className={modelListClasses.sectionTitle}>{t('settings.provider.connection_title')}</h2>
<div className="mt-2 space-y-3">
<ProviderSpecificSettings providerId={providerId} placement="beforeAuth" />
<div className="flex flex-col gap-2">
{children}
<ProviderSpecificSettings providerId={providerId} placement="afterAuth" />
</div>
</div>
</section>

View File

@@ -185,7 +185,9 @@ export default function ProviderApiKeyListDrawer({ providerId, open, onClose }:
<div className={apiKeyListClasses.listWrap}>
<Scrollbar className={apiKeyListClasses.listScroller}>
{apiKeys.length === 0 && !draft ? (
<div className="px-4 py-6 text-center text-muted-foreground text-sm">{t('error.no_api_key')}</div>
<div className="text-(length:--font-size-body-md) px-4 py-6 text-center text-muted-foreground">
{t('error.no_api_key')}
</div>
) : null}
{apiKeys.map((entry) => (
<div key={entry.id} className={apiKeyListClasses.keyRow}>

View File

@@ -45,7 +45,10 @@ export default function ProviderSetting({ providerId, isOnboarding = false }: Pr
return (
<ProviderSettingsContainer theme={theme}>
<div className="flex h-full min-h-0 w-full flex-col">
<div data-testid="provider-detail-shell" className="flex min-h-0 flex-1 flex-col overflow-hidden">
{/* Scoped mock alignment: tokens in `provider-settings-scoped-theme.css`, compositions in ProviderSettingsPrimitives. */}
<div
data-testid="provider-detail-shell"
className="provider-settings-default-scope flex min-h-0 flex-1 flex-col overflow-hidden">
<div className={providerDetailColumnClasses.headerPad}>
<div className={providerDetailColumnClasses.headerContentMaxWidth}>
<ProviderHeader providerId={providerId} />

View File

@@ -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 (
<div className="relative flex h-full min-h-0 w-full min-w-0 overflow-hidden">
<div className="provider-settings-default-scope provider-settings-layout-cq relative flex h-full min-h-0 w-full min-w-0 overflow-hidden">
<ProviderList
selectedProviderId={selectedProviderId}
filterModeHint={filterModeHint}

View File

@@ -131,30 +131,40 @@ const PopupContainer = ({ id, apiKey: newApiKey, baseUrl, type, name, resolve }:
handleCancel()
}
}}>
<DialogContent className="gap-5 rounded-2xl border-border-muted bg-popover p-5 sm:max-w-md">
<DialogContent className="provider-settings-default-scope gap-5 rounded-2xl border-(--color-border-fg-muted) bg-popover p-5 sm:max-w-md">
<DialogHeader className="gap-1.5 pr-6">
<DialogTitle className="text-foreground/90 text-sm leading-5">
<DialogTitle className="text-(length:--font-size-body-md) text-foreground/90 leading-(--line-height-body-md)">
{t('settings.models.provider_key_confirm_title', { provider: displayName })}
</DialogTitle>
<DialogDescription className="text-muted-foreground/80 text-sm leading-5">{confirmMessage}</DialogDescription>
<DialogDescription className="text-(length:--font-size-body-sm) text-muted-foreground/80 leading-(--line-height-body-sm)">
{confirmMessage}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="overflow-hidden rounded-xl border border-border-muted bg-transparent">
<div className="overflow-hidden rounded-xl border border-(--color-border-fg-muted) bg-transparent">
{rows.map((row) => (
<div
key={row.label}
className="grid grid-cols-[7.5rem_minmax(0,1fr)] gap-3 border-border-subtle border-b px-3 py-2.5 last:border-b-0">
<div className="text-foreground-muted text-xs">{row.label}</div>
<div className="min-w-0 truncate text-foreground/85 text-sm">{row.value}</div>
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">
<div className="text-(length:--font-size-body-xs) text-foreground-muted">{row.label}</div>
<div className="text-(length:--font-size-body-sm) min-w-0 truncate text-foreground/85">{row.value}</div>
</div>
))}
<div className="grid grid-cols-[7.5rem_minmax(0,1fr)] gap-3 px-3 py-2.5">
<div className="text-foreground-muted text-xs">{t('settings.models.api_key')}</div>
<div className="text-(length:--font-size-body-xs) text-foreground-muted">
{t('settings.models.api_key')}
</div>
<div className="flex min-w-0 items-center justify-between gap-2">
<span className="min-w-0 truncate font-mono text-foreground/85 text-sm">
<span className="text-(length:--font-size-body-sm) min-w-0 truncate font-mono text-foreground/85">
{showFullKey ? newApiKey : maskApiKey(newApiKey)}
</span>
<Button variant="ghost" size="icon-sm" onClick={() => setShowFullKey((prev) => !prev)}>
<Button
variant="ghost"
size="icon-sm"
aria-label={t(
showFullKey ? 'settings.provider.api_key.hide_key' : 'settings.provider.api_key.show_key'
)}
onClick={() => setShowFullKey((prev) => !prev)}>
{showFullKey ? <Eye size={16} /> : <EyeOff size={16} />}
</Button>
</div>

View File

@@ -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;
}

View File

@@ -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
}

View File

@@ -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 (
<div className="flex flex-col gap-1">
<div className={cn('flex min-h-8 items-center justify-between gap-3', className)}>
<div
className={cn(
'text-(length:--font-size-body-xs) shrink-0 font-medium text-foreground-secondary leading-(--line-height-body-xs)',
titleClassName
)}>
{title}
</div>
<div className={cn('flex w-44 shrink-0 items-center justify-end gap-2', controlClassName)}>
{children}
{action}
</div>
</div>
{help}
</div>
)
}
return (
<div className={cn('space-y-2', className)}>
<div className="flex items-center justify-between gap-3">
<div className={cn('font-medium text-foreground-secondary text-sm leading-5', titleClassName)}>{title}</div>
<div
className={cn(
'text-(length:--font-size-body-sm) font-medium text-foreground-secondary leading-(--line-height-body-sm)',
titleClassName
)}>
{title}
</div>
{action}
</div>
{children}

View File

@@ -19,7 +19,11 @@ export default function ProviderSection({ id, title, description, action, childr
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
{title && <div className={sectionHeadingClasses}>{title}</div>}
{description && <div className="mt-1 text-foreground-muted text-xs leading-tight">{description}</div>}
{description && (
<div className="text-(length:--font-size-body-xs) mt-1 text-foreground-muted leading-(--line-height-body-xs)">
{description}
</div>
)}
</div>
{action}
</div>

View File

@@ -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 ? (
<div className="flex min-w-0 flex-1 flex-col gap-1">
<span className="font-semibold text-base text-foreground">{title}</span>
<span className="text-foreground-muted text-xs leading-tight">{description}</span>
<span className="text-foreground-muted text-xs leading-(--line-height-body-xs)">{description}</span>
</div>
) : 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}>

View File

@@ -43,11 +43,7 @@ export function ProviderHelpTextRow({ children, className }: { children: ReactNo
export function ProviderHelpLink({ children, className, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
return (
<a
className={cn(
'mx-[5px] cursor-pointer text-(--color-primary) hover:underline',
providerSettingsTypography.label,
className
)}
className={cn('!text-info mx-[5px] cursor-pointer hover:underline', providerSettingsTypography.label, className)}
{...props}>
{children}
</a>

View File

@@ -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 `<Button variant="outline">` so visual parity with the input neighbor stays exact
* (same `h-8`, `rounded-lg`, `border-fg-muted`, hover-solid, icon-stroke).
*/
inputActionButton:
'size-8 min-h-0 p-0 rounded-lg border-[color:var(--color-border-fg-muted)] bg-transparent text-foreground shadow-none hover:bg-(--color-surface-fg-subtle-solid) hover:text-foreground [&_svg]:size-3.5! [&_svg]:[stroke-width:var(--icon-stroke)]',
/** Inline show/hide control kept inside the field without adding another border. */
apiKeyVisibilityToggle:
'flex size-5 shrink-0 items-center justify-center text-muted-foreground/70 transition-colors hover:text-foreground disabled:pointer-events-none disabled:opacity-40',
'flex size-5 shrink-0 items-center justify-center text-foreground transition-colors hover:text-foreground disabled:pointer-events-none disabled:opacity-40 [&_svg]:[stroke-width:var(--icon-stroke)]',
titleWithHelp: 'flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1',
titleHelpLink: 'mx-0 inline-flex shrink-0 items-center leading-5 text-primary hover:underline'
titleHelpLink:
'mx-0 inline-flex shrink-0 items-center leading-[var(--line-height-body-sm)] !text-info hover:underline'
} as const