# Cherry Studio Design System
## 1. Visual Theme & Atmosphere
> **Source of truth:** token sources live in `packages/ui/src/styles/tokens/` and Tailwind-facing aliases are generated in `packages/ui/src/styles/theme.css`. Renderer-only bridge aliases live in `src/renderer/assets/styles/tailwind.css`. This document references public aliases only when they are actually exported; for actual values open the relevant token source or generated theme alias.
Cherry Studio is a shadcn/ui-based design system built for an AI conversation application. The design language follows a neutral-first approach — a restrained, systematic palette rooted in pure neutral grays where the interface itself recedes to let content take center stage. The aesthetic is utilitarian-modern: clean surfaces, subtle borders, and restrained use of the exported primary color for true primary actions, creating a tool that feels professional, focused, and endlessly customizable through its robust light/dark mode support.
The typography system is single-track: `var(--font-family-body)` and `var(--font-family-heading)` currently resolve to the same primary UI font token. Code-rendering components own their mono font stack locally. This single-family approach reflects a product with a unified voice — coherent in conversation, precise in code.
What makes Cherry Studio distinctive is its commitment to a calm UI foundation. Primary actions use `var(--color-primary)` as the strongest action color in the chrome, while neutral strong fills are used by shared buttons where that component defines the action hierarchy. New UI should avoid introducing a page-local chromatic brand hue. Other chromatic departures are reserved for semantic feedback: `var(--color-destructive)` for dangerous actions, `var(--color-success)` for positive states, `var(--color-warning)` for caution, `var(--color-info)` for informational surfaces. This creates an interface that feels like a high-quality writing tool — think iA Writer meets VS Code — where the user's content is usually the most colorful thing on screen.
**Key Characteristics:**
- Calm UI foundation: chrome stays mostly neutral; `var(--color-primary)` is reserved for true primary actions and selected states, while semantic accents carry feedback
- Dual-mode system: fully specified light and dark tokens with true inversion (not just darkening)
- Primary action color resolves through `var(--color-primary)`; do not introduce a separate page-local brand hue
- Full semantic color set: `var(--color-destructive)` (red), `var(--color-success)` (green), `var(--color-warning)` (amber), `var(--color-info)` (blue)
- Status palette pairs (base / text / bg / border, with hover + active variants) defined in `tokens/colors/status.css`
- Border-radius scale from `var(--radius-none)` (0) to `var(--radius-round)` (9999px), 10 steps
- Subtle borders via `var(--color-border)` (semi-transparent neutral) for structure, not decoration
- Surfaces stack via color, not shadow: `var(--color-background)` → `var(--color-card)` → `var(--color-popover)`
- 7-level shadow utility system (`--shadow-2xs` through `--shadow-2xl`)
- Floating overlays use concrete Tailwind utilities from the shared primitive unless a token-backed alias exists; do not invent `--color-glass`, `--color-overlay`, or `--blur-*` variables in product code
- Sidebar as a distinct spatial zone with its own complete token set: `var(--color-sidebar)`, `var(--color-sidebar-primary)`, `var(--color-sidebar-accent)`, `var(--color-sidebar-border)`
## 2. Color Palette & Roles
> Token values are defined in `packages/ui/src/styles/tokens/colors/{primitive,semantic,status}.css`. This section names what each token is for; refer to the source files for resolved values.
### Palette Philosophy — Neutrals via Alpha, Colors via Steps
The color system follows one consistent rule:
- **Neutral tokens** (text, borders, secondary fills, hover backgrounds, ghost states) are composed as **black/white + an alpha channel**. Light mode layers `oklch(0 0 0 / x)` on top of the surface; dark mode layers `oklch(1 0 0 / x)` instead. This makes neutrals automatically harmonise with whatever surface they sit on (cards, glass, sidebars) and means light/dark inversion only flips the base ink, not every step of a gray scale.
- **Chromatic tokens** (`--color-primary`, `--color-destructive`, status colors, brand/lime, primitive scales) use **solid `oklch` color steps** — never alpha — because their identity must stay constant on any background.
When you reach for a value:
1. If the role is "tint of the surface" (text, divider, soft fill, hover), use the existing semantic neutral token (`--color-foreground*`, `--color-border*`, `--color-secondary`, `--color-accent`, `--color-ghost-*`). Do not invent `oklch(0 0 0 / 0.x)` literals — the token already encodes the intent.
2. If the role is "this exact color regardless of surface" (brand, error, success), use the corresponding solid token from the `--color-{primary,destructive,success,warning,info,*-base,*-text,*-bg}` set or a primitive scale.
### Primary
- **Primary**: `var(--color-primary)` — exported primary accent for true page actions, selected states, links, and component accents. Shared Button `default` / `emphasis` currently define their own neutral strong fills.
- **Primary Foreground**: `var(--color-primary-foreground)` — contrast text on `bg-primary` surfaces
- **Primary Hover**: `var(--color-primary-hover)`
### Text Colors
- **Foreground**: `var(--color-foreground)` — primary body text
- **Foreground Secondary**: `var(--color-foreground-secondary)` — secondary text, helper labels
- **Foreground Muted**: `var(--color-foreground-muted)` — placeholder, disabled, low-emphasis text
- **Card / Popover / Accent / Secondary Foreground**: `var(--color-card-foreground)` / `var(--color-popover-foreground)` / `var(--color-accent-foreground)` / `var(--color-secondary-foreground)` — contrast text on each surface
### Surface & Background
- **Background**: `var(--color-background)` — primary page background (`#FFFFFF` light / `#0A0A0A` dark)
- **Background Subtle**: `var(--color-background-subtle)` — slightly tinted background variant
- **Card**: `var(--color-card)` — elevated card surfaces
- **Popover**: `var(--color-popover)` — floating panel surfaces (dropdowns, menus, tooltips)
- **Muted**: `var(--color-muted)` — subdued backgrounds, disabled states
- **Accent**: `var(--color-accent)` — hover/active backgrounds for transparent buttons
- **Secondary**: `var(--color-secondary)` — secondary action backgrounds
- **Secondary Hover / Active**: `var(--color-secondary-hover)` / `var(--color-secondary-active)`
- **Ghost Hover / Active**: `var(--color-ghost-hover)` / `var(--color-ghost-active)` — fill on hover for ghost buttons
### Sidebar (Distinct Spatial Zone)
- **Sidebar**: `var(--color-sidebar)` — sidebar surface
- **Sidebar Foreground**: `var(--color-sidebar-foreground)` — text on sidebar
- **Sidebar Accent / Sidebar Accent Foreground**: `var(--color-sidebar-accent)` / `var(--color-sidebar-accent-foreground)` — hover/active state in sidebar (same neutral tint as `--color-secondary`; either token works, but stay consistent within a page)
- **Sidebar Border**: `var(--color-sidebar-border)` — sidebar dividers
- **Sidebar Ring**: `var(--color-sidebar-ring)` — focus ring inside sidebar
### Borders & Rings
- **Border**: `var(--color-border)` — component borders, dividers
- **Border Muted**: `var(--color-border-muted)` — low-emphasis dividers inside dense lists, tables, and grouped settings
- **Border Subtle**: `var(--color-border-subtle)` — very quiet outlines on cards, nested panels, and non-interactive containers
- **Border Hover / Active**: `var(--color-border-hover)` / `var(--color-border-active)`
- **Frame Border**: `var(--color-frame-border)` — page-level wrapping frames and stronger outer chrome
- **Input**: `var(--color-input)` — input field borders
- **Ring**: `var(--color-ring)` — focus ring
### Border Token Rules
- Use semantic border utilities (`border-border`, `border-border-muted`, `border-border-subtle`, `border-frame-border`, `border-input`, `border-sidebar-border`) instead of hard-coded colors.
- Plain `border`, `border-t`, `border-r`, `border-b`, and `border-l` are acceptable only when the global theme base provides the color fallback; reusable components should still name a semantic border color when the role is known.
- For 0.5px hairline dividers, use an explicit token-backed property such as `[border-bottom:0.5px_solid_var(--color-border)]` or `[border-right:0.5px_solid_var(--color-border-muted)]`.
- Legacy opacity-modified border classes (`border-border/10` through `border-border/80`, plus hover/focus/active variants) are compatibility-mapped in `@cherrystudio/ui/styles/theme.css` so old surfaces do not fall back to `currentColor`.
- Do not introduce new opacity-modified semantic border classes such as `border-border/60`, `border-border/40`, `border-border/30`, or `border-border/15`. Use the semantic border utilities above so the visual role is explicit.
### Semantic Status — Single-token aliases
- **Destructive**: `var(--color-destructive)` — error states, dangerous actions
- **Destructive Hover**: `var(--color-destructive-hover)`
- **Destructive Foreground**: `var(--color-destructive-foreground)`
- **Success**: `var(--color-success)` — positive states, confirmations
- **Warning**: `var(--color-warning)` — caution states, pending actions
- **Info**: `var(--color-info)` — informational states, neutral highlights
### Semantic Status — Full palettes (base / text / bg / border / hover / active)
Defined in `tokens/colors/status.css`. Use these when a status surface needs more than a single accent color (e.g. alert banners, toast bodies, tag pills). All four families share the same shape.
- **Error**: `var(--color-error-base)` · `var(--color-error-text)` · `var(--color-error-text-hover)` · `var(--color-error-bg)` · `var(--color-error-bg-hover)` · `var(--color-error-border)` · `var(--color-error-border-hover)` · `var(--color-error-active)`
- **Success**: same shape as error, prefix `--color-success-*`
- **Warning**: same shape as error, prefix `--color-warning-*`
- **Info**: same shape as error, prefix `--color-info-*`
### Brand
Do not use a page-local chromatic brand color for new UI chrome. `var(--color-brand-*)` exists as a primitive compatibility scale, but new component styling should express action hierarchy through semantic aliases such as `var(--color-primary)` and status through the semantic status tokens.
### Links
Links inherit `var(--color-primary)` for color and add an underline on hover. There is no separate `--color-link` token by design — primary is the link color.
### Floating Scrims
No dedicated public `--color-glass`, `--color-glass-border`, `--color-glass-blur`, or `--color-overlay` aliases are exported today. Use the shared primitive defaults first:
- Dialog overlay: use the shared `Dialog` overlay (`bg-black/50`) and customize only through `overlayClassName` when needed.
- Floating panels: use `bg-popover`, `border-border`, and the appropriate shadow utility (`shadow-md` to `shadow-xl`) rather than a page-local glass token.
- If a reusable translucent surface is needed, add/export a real token first and document it here in the same change.
### Chart Colors
Not yet defined as a dedicated palette. For data visualization, use the primitive color scales (`--color-blue-*`, `--color-green-*`, `--color-amber-*`, etc.) from `tokens/colors/primitive.css`.
### Primitive Color Families
Available primitive scales in `tokens/colors/primitive.css` (each has 11 shades, `*-50` through `*-950`): neutral / stone / zinc / slate / gray / red / orange / amber / yellow / lime / green / emerald / teal / cyan / sky / blue / indigo / violet / purple / fuchsia / pink / rose. Use these as raw building blocks; prefer semantic tokens for UI surfaces.
## 3. Typography Rules
> Token values defined in `packages/ui/src/styles/tokens/typography.css`. The technical contract is the CSS variable; family-name strings appear here for human readability.
### Font Families
- **Body / Heading**: `var(--font-family-body)` / `var(--font-family-heading)` → primary UI font with system-ui fallbacks. Handles functional UI text.
- **Mono**: use the app mono font stack where code-rendering components define one. Code blocks, terminals, technical content.
### Size Scale
| Role | Token | Approx. value |
|------|-------|--------------|
| Body XS | `var(--font-size-body-xs)` | 12px — tags, badges, timestamps, metadata |
| Body SM | `var(--font-size-body-sm)` | 14px — navigation, secondary labels, captions |
| Body MD | `var(--font-size-body-md)` | 16px — standard body text, form inputs, descriptions |
| Body LG | `var(--font-size-body-lg)` | 18px — emphasized body, sub-headings |
| Heading XS | `var(--font-size-heading-xs)` | 20px — minor section titles |
| Heading SM | `var(--font-size-heading-sm)` | 24px — sub-section headings |
| Heading MD | `var(--font-size-heading-md)` | 32px — section headings |
| Heading LG | `var(--font-size-heading-lg)` | 40px — page titles |
| Heading XL | `var(--font-size-heading-xl)` | 48px — hero headlines |
| Heading 2XL | `var(--font-size-heading-2xl)` | 60px — display / landing |
The full Tailwind text scale is also exposed: `--text-xs` through `--text-9xl` (12px → 128px) for large display contexts.
### Weight System
Three weights are exposed as semantic tokens; the rest of the Tailwind weight utility scale (`font-thin` → `font-black`, including `font-semibold`) is available but not part of the token contract.
| Weight | Token | Usage |
|--------|-------|-------|
| Regular | `var(--font-weight-regular)` (400) | Body text, descriptions, secondary labels |
| Medium | `var(--font-weight-medium)` (500) | Navigation, emphasized body, form labels |
| Bold | `var(--font-weight-bold)` (700) | Page titles, strong emphasis, hero headlines |
### Line Heights
| Token | Approx. value | Usage |
|-------|---------------|-------|
| `var(--line-height-body-xs)` | 20px | Body XS / tight labels |
| `var(--line-height-body-sm)` | 24px | Body SM (14px) |
| `var(--line-height-body-md)` | 24px | Body MD (16px) |
| `var(--line-height-body-lg)` | 28px | Body LG (18px) |
| `var(--line-height-heading-xs)` | 32px | Heading XS (20px) |
| `var(--line-height-heading-sm)` | 40px | Heading SM (24px) |
| `var(--line-height-heading-md)` | 48px | Heading MD (32px) |
| `var(--line-height-heading-lg)` | 60px | Heading LG (40px) |
| `var(--line-height-heading-xl)` | 80px | Heading XL (48px) |
> Heading 2XL (60px) currently has no matching `--line-height-heading-2xl` token. For display contexts using `var(--font-size-heading-2xl)`, set a one-off Tailwind line-height utility (e.g. `leading-[72px]`) until a canonical token is added.
### Paragraph Spacing
`var(--paragraph-spacing-body-{xs|sm|md|lg})` and `var(--paragraph-spacing-heading-{xs|sm|md|lg|xl|2xl})` set vertical rhythm between paragraphs and headings.
### Principles
- **One font handles the entire UI**: lean on the body / heading font aliases everywhere unless rendering code, where the code-rendering component's mono font stack takes over.
- **Medium (500) is the pivot point**: regular for content, medium for structural labels, bold for page-level emphasis.
- **Consistent line-height rhythm**: body at ~1.4–1.5×, headings tighter (~1.0–1.3×).
## 4. Component Stylings
> Padding values use `var(--cs-size-*)` directly because `--spacing-*` is currently kept opt-in in `theme.css` to avoid clobbering Tailwind container utilities. Prefer Tailwind utility classes (`px-4 py-2`) in component code; the `--cs-size-*` references below are the underlying contract.
### Buttons
Source: `Button` from `@cherrystudio/ui` (`packages/ui/src/components/primitives/button.tsx`).
**Base**
- Layout: inline flex, centered, `gap-2`, no wrapping
- Radius / font / motion: `rounded-md`, `font-normal`, `transition-all`
- Disabled: pointer events disabled, `opacity-40`
- Loading: `data-loading=true`, `cursor-progress`, `opacity-40`, spinner before content
- Focus: ring color from `var(--color-ring)` via the shared button primitive
**Default**
- Background: neutral strong action fill as defined in the shared Button primitive (`bg-neutral-900` light / `bg-neutral-100` dark)
- Text: white in light mode, neutral dark in dark mode
- Shadow: `shadow-xs`
- Hover: neutral hover fill (`hover:bg-neutral-800` light / `dark:hover:bg-neutral-200`)
- Use: Main CTAs outside dialogs ("Send", "Save", "Create")
**Outline**
- Background: transparent
- Text: `var(--color-foreground)`
- Border: 1px solid `var(--color-border)`
- Shadow: none
- Hover: fill `var(--color-accent)`
- Use: Secondary or cancel actions that need a visible boundary
**Secondary**
- Background: `var(--color-secondary)`
- Text: `var(--color-secondary-foreground)`
- Radius: `var(--radius-lg)`
- Shadow: none
- Hover: `var(--color-secondary-hover)`
- Use: Secondary actions ("Cancel", "Back", "Export")
**Emphasis**
- Background: neutral strong action fill as defined in the shared Button primitive (`bg-neutral-900` light / `bg-neutral-100` dark)
- Text: white in light mode, neutral dark in dark mode
- Radius: `var(--radius-lg)`
- Shadow: none
- Hover: neutral hover fill (`hover:bg-neutral-800` light / `dark:hover:bg-neutral-200`)
- Use: Primary action inside Dialog footers; visually strong, flatter than default
**Ghost**
- Background: transparent
- Text: neutral foreground
- Shadow: none
- Hover: fill `var(--color-accent)`, text `var(--color-accent-foreground)`
- Active: `var(--color-ghost-active)`
- Use: Toolbar actions, inline actions, icon buttons
**Destructive**
- Background: `var(--color-destructive)`
- Text: white
- Shadow: `shadow-xs`
- Hover: `var(--color-destructive-hover)`
- Use: Dangerous actions ("Delete", "Remove", "Reset")
**Link**
- Background: none
- Text: neutral foreground
- Hover: neutral muted text + underline
- Use: Inline text links, navigation shortcuts
**Sizes**
| Size | Classes | Use |
|------|---------|-----|
| `default` | `min-h-7.5 gap-1.5 px-2.5 text-[13px]` | Standard buttons |
| `sm` | `min-h-7 gap-1.5 px-2.5 text-xs` | Dense controls |
| `lg` | `min-h-9 px-4 text-sm` | Higher-emphasis actions |
| `icon` | `size-9` | Standard icon button |
| `icon-sm` | `size-7` | Dense icon button |
| `icon-lg` | `size-10` | Large icon button |
**Pill** — shape modifier, not a color variant
- Radius: `var(--radius-round)`
- Use: Tags, filters, toggles, tab indicators
**Icon-only buttons and low-emphasis actions**
Public icon-only buttons should use the shared `Button` primitive first: `variant="ghost"` with `size="icon"` or `size="icon-sm"`. They must provide an `aria-label`; add `Tooltip` / `NormalTooltip` when the icon meaning is not obvious.
**Color hierarchy — ask one question first: is this icon the user's primary reason to be on this page?**
- **Yes** → use the Button ghost variant's default text color (no `text-*` override). The icon *is* the action. (The ghost variant currently renders `text-neutral-900 dark:text-neutral-100`.)
- **No, it's a utility shortcut** → mute it with `text-foreground-muted hover:text-foreground` so it recedes at rest and surfaces on hover.
| Case | Color | Example |
|---|---|---|
| Page-primary action in chrome | (Button ghost variant default, no override) | Mini-apps page top-right `+` and menu — the page exists to launch apps; these icons *are* the action. |
| Secondary utility entry | `text-foreground-muted hover:text-foreground` | Translate page top-right history / settings — user came to translate, not to manage history. |
| Toggle while active | `text-foreground` when active; muted otherwise | Panel-toggle icon while its panel is open. |
| Destructive row action | `text-foreground-muted hover:text-destructive` | Delete X next to a custom language row. |
**Rule of thumb:** if an area shows 3+ icon buttons, at most one should sit at the ghost default. The rest are utilities — mute them. Otherwise the eye has no anchor.
**Do not:**
- Apply a heavy `text-foreground` override to every icon button by reflex — the ghost default is for one action per cluster, not all of them.
- Use `text-primary` as a "more emphasis" replacement for the ghost default; `text-primary` is reserved for selected / branded states, not for raising icon weight.
**Row-level patterns**
- Row-level low-emphasis actions are a distinct pattern: copy, edit, delete, favorite, history, and other secondary actions inside dense rows or work surfaces should stay visually quiet by default (`text-foreground-muted`, no static fill or shadow) and only gain emphasis on hover, focus, active, or pressed state.
- Dangerous row actions should not be permanently red. Keep the trigger low-emphasis, then use `ConfirmDialog` plus a destructive confirm button for the actual destructive decision.
- Favorite / starred actions may use an amber active tint only for favorite semantics. Do not reuse that tint for generic active states.
- The translate page currently has a page-local `IconButton` wrapper for this row-level low-emphasis behavior (`xs` / `sm` / `md`, `ghost` / `destructive` / `star`, `active`, built-in tooltip). Treat that as a pattern to promote into a shared `IconButton` if another page needs the same behavior; do not create more page-local copies.
### Button Hover Interaction Summary
Button hover behavior is variant-specific:
| Variant | Hover Fill | Hover Border | Hover Shadow | Text Change |
|---------|-----------|-------------|-------------|-------------|
| Default | neutral hover fill | — | keeps `shadow-xs` | — |
| Outline | `var(--color-accent)` | existing border | none | — |
| Secondary | `var(--color-secondary-hover)` | — | none | — |
| Emphasis | neutral hover fill | — | none | — |
| Ghost | `var(--color-accent)` | — | none | `var(--color-accent-foreground)` |
| Destructive | `var(--color-destructive-hover)` | — | keeps `shadow-xs` | — |
| Link | — | — | none | muted text + underline |
**Hover rules:**
1. Default and destructive buttons keep the base `shadow-xs`.
2. Outline, secondary, emphasis, and ghost buttons are flat (`shadow-none`) at rest and on hover.
3. Link hover adds underline and a text color change only — no background, no shadow.
### Dialogs
Source: `DialogContent` and related primitives from `@cherrystudio/ui` (`packages/ui/src/components/primitives/dialog.tsx`).
**Shell**
- Surface: `bg-card`
- Text: `text-card-foreground`
- Radius: `rounded-3xl`
- Border: none (`border-0`)
- Padding / gap: `p-6`, `gap-4`
- Shadow: `shadow-xl`
- Motion: fade + zoom transitions, `duration-200`
**Layout**
- Overlay: fixed full-window scrim, `z-[80]`, default `bg-black/50`
- Content: fixed centered, `top-[50%] left-[50%]`, translated by `-50%`
- Width: full width with `max-w-[calc(100%-2rem)]` (narrow-window fallback, all sizes). Desktop width is set by the `size` prop on `DialogContent`:
- `size="sm"` → `sm:max-w-sm` (24rem ≈ 384px) — single-field inputs, rename, short confirmations. Use this whenever the body is one label + one input or a one-line confirmation; the default size feels empty for that amount of content.
- `size="default"` (current default) → `sm:max-w-lg` (32rem ≈ 512px) — standard forms with a few fields.
- `size="lg"` → `sm:max-w-xl` (36rem ≈ 576px) — multi-field forms, scrollable bodies, rich configuration panels.
- Do not override the dialog width with `className="sm:max-w-*"` or similar. Pick a `size` instead; if no size fits, propose a new size in `@cherrystudio/ui` rather than patching at the call site. `className` on `DialogContent` is reserved for non-width layout concerns (e.g. `max-h-[70vh]`, `flex flex-col overflow-hidden` for scrollable bodies).
- Consumers should use the default overlay first. If the scrim needs local tuning, pass `overlayClassName`; do not rewrite a page-local Dialog shell.
**Structure**
- Header: flex column, `gap-2`, centered on mobile and left-aligned from `sm`
- Title: `text-lg leading-none font-semibold`
- Description: `text-muted-foreground text-sm`
- Footer: mobile `flex-col-reverse`, desktop row with `sm:justify-end`
- Close button: shown by default, absolute `top-4 right-4`, low opacity, higher opacity on hover; hide with `showCloseButton={false}` when the surrounding UI supplies its own close affordance
**Actions**
- Use `Button variant="outline"` for cancel/secondary actions.
- Prefer `Button variant="emphasis"` for new neutral Dialog primary actions; existing dialogs using `default` are acceptable during migration, but new work should not introduce a page-local primary style.
- Use `Button variant="destructive"` for dangerous confirmation actions.
- `ConfirmDialog` currently uses `default` for non-destructive confirms and `destructive` for dangerous confirms. Treat that as a migration-compatible composite, not as a reason to invent page-local Dialog button styles.
**Use Dialog for**
- Centered confirmations, focused form flows, command palettes, and blocking decisions.
- Short-to-medium content that should not feel attached to a page edge.
- Cases where the user must either complete or dismiss the interaction before returning to the page.
### Drawers & Page Side Panels
There are two different drawer patterns. Do not collapse them into one generic "side drawer" rule.
**PageSidePanel** — in-page side panel
Source: `PageSidePanel` from `@cherrystudio/ui` (`packages/ui/src/components/composites/page-side-panel/index.tsx`).
Use `PageSidePanel` for page-owned management surfaces such as mini-app display settings, translate settings, and translate history. By default, the panel and backdrop portal to `document.body` so page scroll containers, transformed ancestors, and nested layout shells cannot clip or re-base the drawer. In app shell route tabs, the tab content root is provided via `PortalContainerProvider` on every platform; `PageSidePanel` reads it with `usePortalContainer()`, portals into its owning tab root, and switches to absolute positioning, so a still-open panel stays hidden with its owning tab instead of surfacing over the active tab. The same provider scopes tab-owned Radix floating overlays (popovers, dropdown menus, selects, tooltips, hover cards, context menus) to that root, so the panel and those overlays share one portal container per tab.
- Backdrop: default fixed `inset-0`, scoped absolute `inset-0`, `z-60`, `bg-black/50`, fades over `0.15s`
- Panel: default fixed `top-3 bottom-3`, scoped absolute `top-3 bottom-3`, `right-3` or `left-3`, `z-70`
- Size / shell: `w-100`, `rounded-3xl`, `bg-card`, `text-card-foreground`, `shadow-xl`, `overflow-hidden`
- Motion: horizontal slide from the chosen side with spring transition (`damping: 30`, `stiffness: 350`)
- Header: `px-6 pt-6 pb-3`, optional header content plus ghost close button
- Body: shared `Scrollbar`, `space-y-4 px-6 py-4`
- Footer: optional, `px-6 pt-3 pb-6`, for sticky action groups
- Accessibility: role `dialog`, `aria-modal=true`, focus moves into the panel on open and returns to the trigger on close
For standard settings panels, pass `title` instead of custom `header`. This renders the shared title style (`font-semibold text-base text-foreground`). Use custom `header` only when the title area needs richer layout.
Use `PageSidePanelSection` and `PageSidePanelItem` as optional content primitives for settings-style panels. The structure is intentionally three-layered:
1. `PageSidePanel` owns only the floating drawer shell: placement, backdrop, title/close chrome, body scroll, and footer.
2. `PageSidePanelSection` owns a settings group: section title, optional right-aligned low-emphasis actions, and group spacing.
3. `PageSidePanelItem` owns a single setting row: title/description stack, trailing control, and optional expanded content below the row.
Use this full shell → section → item stack for settings drawers such as mini-app display settings and translate settings:
- Section: `flex flex-col gap-3` — this `gap-3` is the rhythm between the section title row, its actions, and the preference row group below; it is **not** the spacing between preference rows themselves.
- Preference row group: wrap the row stack inside the section with an extra `
` so individual preference rows breathe more than the title-to-rows gap. Existing callers (`TranslateSettings`, `MiniAppDisplaySettings`) follow this convention.
- Item: title/description stack with a trailing `action`; the trailing control may also expand into an optional `children` slot below the row.
- Related sections should be separated by `gap-8`.
- Do not place repeated cards inside the panel unless each card is a genuine repeated entity.
Do not force `PageSidePanelSection` / `PageSidePanelItem` onto non-settings content. List, history, detail, or picker drawers should still use the shared `PageSidePanel` shell, but their body layout should match the task. For example, translate history uses `PageSidePanel` for the drawer chrome and a custom list/detail/empty-state layout inside the body.
**Drawer primitive** — modal edge drawer
Source: `Drawer` primitives from `@cherrystudio/ui` (`packages/ui/src/components/primitives/drawer.tsx`, Vaul-based).
Use `Drawer` for modal edge/bottom sheets, especially mobile-oriented or full-viewport overlays that are not visually nested inside a page workspace.
- Overlay: fixed `inset-0`, `z-50`, `bg-black/50`
- Content: fixed `z-50`, flex column, `bg-background`
- Top / bottom: full width, `max-h-[80vh]`, border on the attached edge, `rounded-b-lg` or `rounded-t-lg`
- Bottom drawer: includes the built-in centered drag handle (`h-2 w-25 rounded-full bg-muted`)
- Left / right: `inset-y-0`, `w-3/4`, `sm:max-w-sm`, border on the attached edge
- Header: `p-4`, `gap-0.5`, centered for top/bottom and left-aligned from `md`
- Footer: `mt-auto flex flex-col gap-2 p-4`
- Title / description: `font-semibold text-foreground`; `text-sm text-muted-foreground`
`Drawer` uses `bg-background` and edge attachment, not the floating `bg-card rounded-3xl shadow-xl` shell of `PageSidePanel`. New drawer work should use `PageSidePanel` or this shared `Drawer` primitive.
### Cards
**Standard Card**
- Background: `var(--color-card)`
- Text: `var(--color-card-foreground)`
- Border: 1px solid `var(--color-border)`
- Radius: `var(--radius-lg)` to `var(--radius-xl)`
- Padding: `var(--cs-size-2xs)` to `var(--cs-size-xs)` (16–24px)
- Use: Content containers, conversation panels, settings sections
**Popover / Floating**
- Background: `var(--color-popover)`
- Text: `var(--color-popover-foreground)`
- Border: 0.5px hairline `var(--color-border)`
- Radius: `var(--radius-lg)`
- Shadow: `var(--shadow-lg)`
- Use: Dropdowns, menus, tooltips, command palettes
### Popover
Source: `Popover`, `PopoverTrigger`, `PopoverAnchor`, and `PopoverContent` from `@cherrystudio/ui` (`packages/ui/src/components/primitives/popover.tsx`). Use this as the default floating container for dropdowns, compact action menus, filters, and other trigger-bound transient panels.
**Default `PopoverContent`:**
- Background: `var(--color-popover)`
- Text: `var(--color-popover-foreground)`
- Border: 0.5px hairline `var(--color-border)` (`border-[0.5px]`)
- Radius: `var(--radius-lg)`
- Padding: 16px (`p-4`)
- Width: 288px (`w-72`)
- Shadow: `var(--shadow-lg)` (`shadow-lg`)
- Offset from trigger: 4px
- Z-index: 80
**Compact menu popovers:**
- Keep `PopoverContent` from `@cherrystudio/ui`; override layout density with `w-fit min-w-32 rounded-xl p-1.5`.
- Width is content-driven (`w-fit`), floored at 128px (`min-w-32`) so short menus stay legible. This matches `ContextMenu`'s `min-w-[8rem]` baseline in `packages/ui/src/components/primitives/context-menu.tsx`. Do not hard-code widths like `w-40` / `w-44` — they trap trailing whitespace when labels are shorter than the slot.
- Compose menu bodies with `MenuList` and `MenuItem` from `@cherrystudio/ui`.
- Menu rows should use 32px height (`h-8`), `rounded-lg`, `px-2.5`, and `text-sm`.
- Close the popover after a menu action is selected unless the action intentionally opens an inline sub-flow.
- Do not add page-specific theme scopes to portal popovers unless the whole floating surface is intentionally part of that page-local theme.
**Glass Panel** (floating chrome with backdrop blur)
- Background: use `bg-popover` unless a real translucent token is introduced
- Border: 1px solid `var(--color-border)`
- Backdrop filter: use Tailwind blur utilities directly only when the component is intentionally translucent
- Radius: `var(--radius-lg)` to `var(--radius-xl)`
- Use: Floating toolbars, header bars over scrollable content, tooltips on imagery
### Page-Level Patterns
These patterns reflect the current v2 pages and should be treated as valid design-system usage, not exceptions.
**Tool Gallery / Code Tools**
- Use a focused, centered gallery on `bg-background` with a constrained width (`max-w-5xl` style scale) and responsive card grid.
- Prominent tool-entry cards may use `bg-card`, `border-border`, `p-4`, and `var(--radius-2xl)` to create a launchpad feel without adding shadows.
- Selection should use border/ring feedback (`border-border-active`, `ring-ring`) rather than a new chromatic accent.
- Hero or product icons may be circular (`radius-round`) and use `shadow-lg` only when they behave as a visual anchor, not as repeated card elevation.
**Mini App Launchpad / Settings Drawer**
- The launchpad should stay sparse: small icon buttons in the top action area, centered search, then an app grid with compact launchpad tiles.
- Settings and visibility management belong in `PageSidePanel` with grouped sections and dense list rows. Use the shared `Drawer` primitive only for modal edge/bottom sheets.
- Dense mini-app rows should use `rounded-md`, subtle hover fills, and compact icons; avoid converting every row into a card.
**Translation Workspace**
- Translation input/output panes are work surfaces, not cards. Use full-height `bg-background` panes separated by structure and controls.
- Keep the two-pane workspace flat at rest: no card nesting, no static shadows, no decorative color.
- The main translate/confirm action may use `bg-primary text-primary-foreground`; target-language chips and selected language states may use `bg-primary/10` or `text-primary`.
- File upload/drop states should use dashed semantic borders (`border-border-muted` / hover `border-border-hover`) and muted foreground text.
- Toolbar and copy/clear controls should use ghost/icon-button behavior so text content remains the primary visual focus.
### Inputs
- Background: `var(--color-background)`
- Border: 1px solid `var(--color-input)`
- Radius: `var(--radius-md)` (8px)
- Shadow: none — inputs stay flat at rest; per the depth philosophy, shadows are reserved for hover feedback and floating elements
- Focus ring: use Tailwind ring utilities with `var(--color-ring)` (for example `focus-visible:ring-2 focus-visible:ring-ring/50`)
- Font: `var(--font-family-body)` between `var(--font-size-body-sm)` and `var(--font-size-body-md)`, `var(--font-weight-regular)`
- Placeholder: `var(--color-foreground-muted)`
**Search field with trailing action:**
When a search field needs an inline trailing button (e.g. add provider in `ProviderList`), embed a 24×24 icon button inside the search wrap, after the input:
- Size: 24×24 (`size-6`)
- Radius: 8px (`rounded-[8px]`)
- Idle background: `var(--color-muted)` (`bg-muted`)
- Hover background: `var(--color-surface-hover-soft)`
- Foreground: `var(--color-foreground)` at full opacity
- Disabled: `pointer-events-none opacity-30`
Canonical implementation: `providerListClasses.searchInlineAddButton` in `src/renderer/pages/settings/ProviderSettings/primitives/classNames.ts`. The search wrap itself stays the standard input surface (`bg-background`, hairline border, `rounded-xl`).
### Sidebar
Sidebar primitives currently live in `src/renderer/components/Sidebar`, not in `@cherrystudio/ui`. Treat this section as renderer sidebar guidance until a shared `@cherrystudio/ui` sidebar API exists.
The page owns the outer wrapper (width / Scrollbar / padding). Reusable sidebar internals should own spacing, sizing, and active state so individual pages do not hand-roll divergent menus.
**Colors:**
- Background: `var(--color-sidebar)`
- Text: `var(--color-sidebar-foreground)` for body; `var(--color-foreground-muted)` for SectionTitle
- Border-right (when divider needed): `0.5px solid var(--color-border)`
- Active item: `var(--color-sidebar-accent)` background, `var(--color-sidebar-accent-foreground)` text — **icon color stays `var(--color-sidebar-accent-foreground)` on active (no color change)**
- Hover item: `var(--color-sidebar-accent)` background
- Focus ring: `var(--color-sidebar-ring)`
**Type:**
- Header/title rows: `var(--font-size-body-sm)` / `var(--font-weight-medium)`
- Section labels: `var(--font-size-body-xs)` / `var(--font-weight-regular)`
- Menu item labels: `var(--font-size-body-sm)` / `var(--font-weight-regular)`
**Spacing & sizing (canonical, baked into the components):**
| Relationship | Value | Token |
|---|---|---|
| Header / section label / menu item own height | 32px | `var(--spacing-8)` |
| Horizontal inset on all rows (left/right padding) | 12px | `var(--spacing-3)` |
| Gap between section blocks (Header → first Section, Section → next Section) | 12px | `var(--spacing-3)` |
| Gap **inside** a section (section label → item, item → item) | 4px | `var(--spacing-1)` |
| MenuItem corner radius | 10px | `rounded-[10px]` |
| MenuItem icon size | 16px | `[&_svg]:size-4` |
| MenuItem icon ↔ label gap | 12px | `gap-3` |
**Page-level wrapper guidance (set on the container, NOT on the components):**
- Recommended sidebar column width: 220px
- Recommended container padding: 8px horizontal, 12px vertical (`px-2 py-3`)
> If a sidebar elsewhere needs different spacing, propose a shared renderer variant before hard-coding page-local overrides.
>
> **Target rule:** once the `SidebarHeader / SidebarSection / SidebarSectionTitle / SidebarMenuItem` family lands in `@cherrystudio/ui`, hand-rolled sidebar menus will not be allowed. Until that family ships, compose with `MenuList` + `MenuItem` + project-level className tokens (see `src/renderer/pages/settings/index.tsx` for the canonical token pattern: `settingsSubmenuItemClassName`, `settingsSubmenuItemLabelClassName`, `settingsSubmenuSectionTitleClassName`, `settingsSubmenuDividerClassName`).
### Page Header
Source: `PageHeader` from `@cherrystudio/ui`. The single component for any page or side-panel top title. All settings pages, sidebars, drawers, and content panels that need a heading row **must** use this — never hand-roll `
` with manual padding.
**Anatomy:**
- `title` (required) — heading text, rendered inside an `` with `truncate` for overflow safety.
- `action` (optional) — right-aligned slot for icon-buttons (filter, add, etc.).
- `bordered` (optional) — adds a `border-b border-border` divider underneath the header row. Default `false`. Use on right-pane detail headers to visually separate the title from the body; omit on left sidebar headers (which sit above a `MenuList` and don't need a divider).
**Type:**
- Title: `var(--font-size-body-sm)` (14px) · `var(--font-weight-medium)` · `leading-4` · `text-foreground`
**Spacing & sizing (baked in — must not be overridden per-page):**
| Relationship | Value | Token |
|---|---|---|
| Bar height | 32px | `h-8` |
| Margin top (gap above) | 12px | `mt-3` |
| Margin bottom (gap below) | 8px | `mb-2` |
| Left padding (title aligns with menu item icon column) | 20px | `pl-5` |
| Right padding (action sits 12px from the column edge) | 12px | `pr-3` |
| Title ↔ action gap | 8px | `gap-2` |
| Bottom border (when `bordered`) | 1px | `border-b border-border` |
**Rules:**
- Action buttons should be 24×24 (`size-6`); they sit centered inside the 32px bar.
- Title text comes from i18next; do not hard-code strings.
- The asymmetric padding is intentional: `pl-5` (20px) aligns the title's left edge with the icon column of menu items below — wrapper `px-2.5` (10px) + item `px-2.5` (10px) = 20px. Do not change to symmetric padding.
- Two adjacent `PageHeader` instances (left nav + right panel) are guaranteed to be vertically aligned because spacing tokens are identical; the title line box starts 20px from the column top.
- Right-pane detail headers in two-column settings layouts **must** pass `bordered`; left sidebar headers **must not** (the menu list below them already provides visual structure). A right-pane header rendered by a non-`PageHeader` component (e.g. `ProviderHeader`, which carries a `` plus multiple icons) must wrap itself in a container that draws an equivalent `border-b border-border` divider — see `providerDetailColumnClasses.headerContentMaxWidth` in `ProviderSettings/primitives/classNames.ts`.
- Provider settings section headings use full `text-foreground` rather than reduced opacity. The right pane already has dense secondary helper text, badges, and inline controls; fully opaque section labels preserve scan hierarchy without introducing another local color rule.
### Switch
Source: `Switch` and `DescriptionSwitch` from `@cherrystudio/ui` (`packages/ui/src/components/primitives/switch.tsx`). Current implementation uses a quiet gray off state and a brand/primary on state, matching the settings screenshots.
**Anatomy & sizing:**
| Size | Track | Thumb | Travel | Use |
|------|-------|-------|--------|-----|
| `xs` | 32 × 18 | 16 × 16 | 14px | Dense inline controls |
| `sm` | 36 × 20 | 18 × 18 | 16px | Slightly larger settings rows |
| `md` (default) | 44 × 22 | 19 × 19 | 21px | Standard switch |
| `lg` | 44 × 24 | 20 × 20 | 18px | Hero / marketing surfaces |
**Colors:**
| State | Light | Dark |
|---|---|---|
| Track — off | `bg-gray-500/20` | `bg-gray-500/20` |
| Track — on | `bg-brand-600` | `bg-brand-600` |
| Loading | `bg-brand-300!` | `bg-brand-300!` |
| Thumb glyph | white internal SVG | white internal SVG |
**Other rules:**
- Track carries `shadow-xs`; do not add extra page-local shadow.
- The thumb is rendered by the component's internal white SVG glyph. Do not add custom thumb icons from the call site.
- `loading` state switches root/thumb coloring to `bg-brand-300!` and animates the thumb SVG.
- Focus ring: `focus-visible:ring-[3px] focus-visible:ring-ring/50` (no track border change).
**Don't:**
- Don't pass page-local status colors (`bg-success`, `bg-warning`, etc.) to the track. The component owns its brand on state.
- Don't add inline `style={{ ... }}` overrides for switch dimensions. If a new size is needed, add a variant to `switchRootVariants`/`switchThumbVariants` and document it here.
- Use `` for reusable standalone preference rows. In dense `PageSidePanel` layouts, composing a row label plus a bare `` is acceptable when the surrounding row owns spacing and helper text.
## 5. Layout Principles
### Window Chrome
- **Top chrome height**: `var(--app-top-chrome-height)` = 44px. Use this for the main window tab bar and any standalone macOS window top drag area that should visually align with the main app chrome.
- **Navbar content height**: `var(--navbar-height)` defaults to `var(--app-top-chrome-height)` for the fixed top-menu layout. Only override it for inner content calculations that intentionally do not include a top navbar.
- Settings-style floating windows with a transparent macOS shell must keep the outer top inset tied to `var(--app-top-chrome-height)` instead of hard-coded pixel classes such as `h-11` or `h-[50px]`.
- **Settings window sizing** (standalone settings window only): sized to 80% of the main window with a hard floor of 760×560 and a 1280px max width, centered on the main window. The 760×560 floor keeps the ~200px sidebar plus the detail column usable when the user shrinks the main window; the 1280px ceiling prevents 2K/4K displays from stretching settings into empty space. Canonical implementation: `SettingsWindowService` in `src/main/services/SettingsWindowService.ts`.
### Settings Panel Layout
Settings pages (both the in-app `/settings` route and the standalone settings window) share the same two-column shape:
| Column | Width | Composition |
|---|---|---|
| Left submenu | `var(--settings-width)` (200px in the standalone window, 250px default in `responsive.css`) | `PageHeader` (title) → `Scrollbar` → `MenuList` of grouped `MenuItem` rows |
| Right detail | `flex-1` | Page-owned content |
Submenu composition rules:
- Use `PageHeader` from `@cherrystudio/ui` at the top — do not hand-roll a header.
- **Section-title-as-page-title exception**: when a page-level label is itself a *group name* that should match in-list group labels, keep using `PageHeader` and pass `titleClassName="font-normal text-foreground-muted text-xs leading-4"` so the heading swaps to section-title typography while preserving the same 16px line box. The PageHeader's `mt-3 + h-8 + mb-2` outer geometry is preserved, so the label baseline still aligns with the right column's PageHeader heading. See `page-header.stories.tsx` › `SectionTitleStyle` for the canonical example.
- Wrap menu rows in `MenuList` with `gap-1`; group with `MenuDivider` + a section title `` carrying `settingsSubmenuSectionTitleClassName`.
- Each row is a `MenuItem` styled by the canonical settings token pair: `settingsSubmenuItemClassName` on `className` (height / hover / active surface) and `settingsSubmenuItemLabelClassName` on `labelClassName` (`group-data-[active=true]:font-medium` for the bold-on-active label). Both tokens live in `src/renderer/pages/settings/index.tsx`.
- Provider-style nested lists (`ProviderList`) follow the same shape: `PageHeader` + search field with trailing action + scroll body. They use their own scoped tokens in `ProviderSettings/primitives/classNames.ts` but keep the 200px column convention.
**Right-detail content container (mandatory):**
The right pane of every "simple right-content" settings page (i.e. pages whose right column is one big content area, not its own further-split layout) must use a two-layer wrapper:
| Layer | Class | Purpose |
|---|---|---|
| Outer (full-width, scrolling) | `px-6 py-4` | Page edge padding — keeps `24px` between the content card and the column edge |
| Inner (constrained, centered) | `mx-auto w-full max-w-3xl` | Caps content at `768px` and centers it on wide screens |
Use the canonical components in `src/renderer/pages/settings/index.tsx`:
- `SettingsContentColumn` — full-page container that owns its own native scroll (replaces the legacy `SettingContainer` for "simple right-content" pages).
- `SettingsContentBody` — the same two-layer wrap, but for pages that mount their own `Scrollbar` externally (e.g. `CommonSettings`, `ShortcutSettings`).
This mirrors the model service (Provider Settings) detail column (`providerDetailColumnClasses` in `ProviderSettings/primitives/classNames.ts`), which is the reference implementation.
Do **not**:
- Use `p-4` or `px-5 py-4` on a settings page's outermost content container — they were the old, divergent paddings and are banned for new pages.
- Apply `max-w-3xl` directly on a child component to "fix" centering on one page — fix the page container so every page is consistent.
- Modify `SettingContainer` to add max-width: it intentionally stays a plain padded scroller for nested-split pages (Data, Integration, MCP, WebSearch, FileProcessing, Channels, Skills) whose right pane is further subdivided.
When embedded in a `PageSidePanel` drawer or onboarding context (e.g. `ModelSettings compact`), the page must NOT add `max-w-3xl` — the drawer width is already constrained and the centered cap would visually mis-align. Branch on the embedding flag and fall back to a plain padded container.
### Spacing System
> Defined in `tokens/spacing.css`. The full Tailwind numeric scale (`--spacing-*`) is exposed plus semantic legacy aliases (`--cs-size-*`). In component code prefer Tailwind utilities (`p-4`, `gap-6`); in raw CSS use the tokens below.
**Numeric scale (Tailwind-aligned, 4px base unit):**
- 0, px (1px), 0.5 (2px), 1 (4px), 1.5 (6px), 2 (8px), 2.5 (10px), 3 (12px), 3.5 (14px), 4 (16px), 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 72, 80, 96 — exposed as `--spacing-N`.
- Total: 35 numeric tokens covering 0–384px including .5 micro-steps.
**Semantic aliases** (shorthand for component code):
| Token | Approx. value |
|-------|---------------|
| `var(--cs-size-5xs)` | 4px |
| `var(--cs-size-4xs)` | 8px |
| `var(--cs-size-3xs)` | 12px |
| `var(--cs-size-2xs)` | 16px |
| `var(--cs-size-xs)` | 24px |
| `var(--cs-size-sm)` | 32px |
| `var(--cs-size-md)` | 40px |
| `var(--cs-size-lg)` | 48px |
| `var(--cs-size-xl)` | 56px |
| `var(--cs-size-2xl)` | 64px |
| `var(--cs-size-3xl)` | 72px |
| `var(--cs-size-4xl)` | 80px |
| `var(--cs-size-5xl)` | 88px |
| `var(--cs-size-6xl)` | 96px |
| `var(--cs-size-7xl)` | 104px |
| `var(--cs-size-8xl)` | 112px |
### Common Spacing Patterns
| Context | Token range | Tailwind |
|---------|-------------|----------|
| Inline spacing (icon to text) | `var(--cs-size-5xs)` – `var(--cs-size-4xs)` | `gap-1` to `gap-2` |
| Component internal padding | `var(--cs-size-4xs)` – `var(--cs-size-2xs)` | `p-2` to `p-4` |
| Card padding | `var(--cs-size-2xs)` – `var(--cs-size-xs)` | `p-4` to `p-6` |
| Section gaps | `var(--cs-size-xs)` – `var(--cs-size-lg)` | `gap-6` to `gap-12` |
| Page section spacing | `var(--cs-size-lg)` – `var(--cs-size-6xl)` | `py-12` to `py-24` |
### Grid & Container
- Max content widths: Tailwind utilities (`max-w-sm` through `max-w-7xl`)
- Screen breakpoints: Tailwind defaults (`sm` 640px, `md` 768px, `lg` 1024px, `xl` 1280px, `2xl` 1536px)
### Border Radius Scale
> ⚠️ **Cherry remaps the Tailwind default radius scale.** `rounded-md` resolves to 8px (Tailwind default: 6px), `rounded-lg` to 10px (default: 8px), `rounded-xl` to 14px (default: 12px), and `rounded-3xl` to 22px (default: 24px). When copying components from shadcn examples, Tailwind tutorials, or any third-party Tailwind library, expect a 2–4px visual difference until the radius is consciously chosen against the table below.
> Defined in `tokens/radius.css`. 10 levels exposed via `--radius-*`.
| Token | Approx. value | Usage |
|-------|---------------|-------|
| `var(--radius-none)` | 0 | Square corners |
| `var(--radius-xs)` | 2px | Badges, tags |
| `var(--radius-sm)` | 6px | Chips, small buttons |
| `var(--radius-md)` | 8px | **Default** — buttons, inputs, dropdowns |
| `var(--radius-lg)` | 10px | Cards, panels, secondary/emphasis buttons |
| `var(--radius-xl)` | 14px | Large cards, hero sections |
| `var(--radius-2xl)` | 18px | Feature cards, prominent containers |
| `var(--radius-3xl)` | 22px | Dialogs, PageSidePanel, marketing cards, large modals |
| `var(--radius-round)` | 9999px | Pills, avatars, circular buttons |
## 6. Depth & Elevation
Cherry Studio uses a dual depth system: **surface color layering** for structural hierarchy and **box-shadows** for interactive feedback (hover states, floating elements).
### Surface Color Layers
| Level | Token | Use |
|-------|-------|-----|
| Ground (Level 0) | `var(--color-background)` | Page background |
| Surface (Level 1) | `var(--color-card)` | Cards, main panels |
| Raised (Level 2) | `var(--color-popover)` | Popovers, menus, dropdowns |
| Accent (Level 3) | `var(--color-accent)` | Accent/hover backgrounds, tooltips |
| Sidebar (Ambient) | `var(--color-sidebar)` | Sidebar — distinct from main surface |
| Floating panel | `var(--color-popover)` + border/shadow utilities | Dropdowns, popovers, transient chrome |
| Modal scrim | shared Dialog / Drawer / PageSidePanel overlay (`bg-black/50`) | Behind modals, dimmed backdrops |
**Depth Philosophy**: Surface color layering is the primary depth mechanism — `var(--color-border)` separates same-tone surfaces, and in dark mode progressively lighter neutrals create natural stacking. Shadows are reserved for **interactive feedback** (hover states add a small lift) and **floating elements** (popovers, centered Dialogs, and PageSidePanel use medium-to-heavy lift). The Vaul `Drawer` primitive relies on edge attachment and borders rather than the floating card shell. This keeps the interface feeling flat at rest and responsive on interaction.
## 7. Shadow / Blur / Opacity / Border / Stroke
### Shadow
> Shadow utilities are exposed through the Tailwind theme. Treat them as utility-level design tokens.
**Box shadows (7 levels):**
| Token | Use |
|-------|-----|
| `var(--shadow-2xs)` | Subtle dividers, pressed states |
| `var(--shadow-xs)` | **Button hover** — primary interactive feedback |
| `var(--shadow-sm)` | Cards, small floating elements |
| `var(--shadow-md)` | Dropdowns, tooltips |
| `var(--shadow-lg)` | Large floating panels |
| `var(--shadow-xl)` | Dialogs, PageSidePanel, full-screen overlays |
| `var(--shadow-2xl)` | Hero cards, peak emphasis |
### Blur
Use Tailwind blur/backdrop-blur utilities directly when a component intentionally needs blur. There are currently no public `--blur-*` design-token aliases in `@cherrystudio/ui`.
### Opacity
> Use Tailwind opacity utilities or component-level state classes.
Use Tailwind opacity utilities (`opacity-40`, `opacity-70`, etc.) or component-level state classes. There are currently no public `--opacity-*` design-token aliases in `@cherrystudio/ui`.
### Border Width
> Use Tailwind border-width utilities and semantic border color tokens.
Use Tailwind border-width utilities (`border`, `border-0`, `border-2`, etc.) with semantic border colors. There are currently no public `--border-width-*` design-token aliases in `@cherrystudio/ui`.
### Stroke Width
Use icon-library defaults unless a component has a documented reason to override SVG `stroke-width`.
## 8. Do's and Don'ts
### Do
- Use calm, low-saturation chrome — reserve `var(--color-primary)` for true primary actions/selected states and semantic colors for feedback
- Apply `var(--radius-md)` as the base button radius, `var(--radius-lg)` where the Button variant explicitly rounds itself, and `var(--radius-md)` for inputs
- Use `var(--color-primary)` / neutral strong fills for main CTAs; do not introduce page-local brand hues
- Let dark mode feel genuinely dark: `var(--color-background)` resolves to `#0A0A0A` with layered surfaces stacking lighter
- Use `var(--color-foreground-secondary)` / `var(--color-foreground-muted)` for secondary text
- Keep `var(--shadow-xs)` only on button variants that already carry the base shadow (`default`, `destructive`)
- Use `*-hover` tokens or neutral hover classes according to the Button variant definition
- Use `var(--color-accent)` fill for outline and ghost button hover states
- Use semantic color tokens (`var(--color-success)`, `var(--color-warning)`, `var(--color-info)`, `var(--color-destructive)`) for status feedback, toasts, and badges
- Use the full status palettes (`--color-error-bg`, `--color-error-text`, etc. from `tokens/colors/status.css`) for richer status surfaces
- Use `var(--color-border)`, `var(--color-border-muted)`, and `var(--color-border-subtle)` for neutral structure instead of opacity-modified border utilities
- Use the body / heading font aliases at `var(--font-weight-regular)`/`var(--font-weight-medium)` for body and labels, `var(--font-weight-bold)` for page-level emphasis
- Separate spatial zones (sidebar, main, popover) through surface color layering: `var(--color-sidebar)` vs `var(--color-background)` vs `var(--color-popover)`
- Use heading size and line-height tokens directly for new headings
- Use primitive color scales (`--color-blue-*`, `--color-green-*`, etc.) for charts and data visualization
- Apply `var(--radius-round)` specifically for pills, avatars, and circular buttons
- Use `var(--shadow-md)` to `var(--shadow-lg)` for floating elements (popovers, dropdowns, large panels), and `var(--shadow-xl)` for Dialogs or PageSidePanel surfaces that need stronger separation from the dimmed page
- Use shared overlay/floating primitives first; add real exported tokens before documenting new glass or scrim aliases
### Don't
- Don't use shadows for static elevation — reserve shadows for hover feedback and floating elements
- Don't use `var(--radius-xs)` or `var(--radius-sm)` for buttons or cards — `var(--radius-md)`/`var(--radius-lg)` are the button radii in the shared primitive
- Don't use font weights below `var(--font-weight-regular)` for functional UI text — thin/light/extralight weights are display-only
- Don't apply `var(--color-destructive)` to non-dangerous actions — it's reserved for delete/error/warning only
- Don't use `var(--color-success)` / `var(--color-warning)` / `var(--color-info)` for decorative purposes — they carry semantic meaning
- Don't introduce a page-local chromatic brand color — use semantic tokens or primitive chart colors by role
- Don't darken the sidebar to match the main background — its distinct surface via `var(--color-sidebar)` and dedicated palette creates spatial separation
- Don't use `var(--color-popover)` background for cards or vice versa — each elevation level has its specific token
- **Don't hard-code hex / rgba / oklch values** — always reference semantic tokens so light/dark mode works automatically
- Don't use `border-border/60`, `border-border/40`, `border-border/30`, or `border-border/15` — choose a semantic border token instead
- Don't apply `var(--shadow-xl)` or `var(--shadow-2xl)` to standard UI elements — reserve `var(--shadow-xl)` for Dialogs, PageSidePanel, and full-screen overlays, and `var(--shadow-2xl)` for peak display emphasis
- Don't invent token-looking variables such as `--color-glass`, `--color-overlay`, `--blur-md`, `--opacity-50`, or `--border-width-2` unless they are exported by the theme in the same change
## 9. Responsive Behavior
### Breakpoints
| Name | Width | Key Changes |
|------|-------|-------------|
| Mobile | <640px | Sidebar hidden, single-column chat, bottom action bar |
| Tablet | 640–1024px | Collapsible sidebar overlay, condensed spacing |
| Desktop | 1024–1280px | Persistent sidebar + main content area |
| Wide | >1280px | Sidebar + main + optional right panel (settings/info) |
### Collapsing Strategy
- Sidebar: persistent → overlay → hidden (with hamburger toggle)
- Chat layout: full-width with max-width constraint → stacked mobile view
- Card grids: multi-column → 2-column → single-column stacked
- Typography: display sizes scale down ~40% on mobile (48px → 30px)
- Spacing: section gaps compress from 48–96px to 24–48px on mobile
- Navigation: horizontal tabs → bottom bar or hamburger menu
## 10. Agent Prompt Guide
### Quick Token Reference
| Role | Token | Notes |
|------|-------|-------|
| Page background | `var(--color-background)` | `#FFFFFF` light / `#0A0A0A` dark |
| Primary text | `var(--color-foreground)` | Primary body text |
| Secondary / muted text | `var(--color-foreground-secondary)` / `var(--color-foreground-muted)` | Helper, placeholder |
| Primary accent | `var(--color-primary)` | Page-level primary actions, selected states, links, component accents |
| Destructive action | `var(--color-destructive)` | Hover: `var(--color-destructive-hover)`; Text: `var(--color-destructive-foreground)` |
| Success / Warning / Info | `var(--color-success)` / `var(--color-warning)` / `var(--color-info)` | Single-token semantic accents |
| Borders | `var(--color-border)` (hover/active variants available) | Neutral hairline |
| Quiet borders | `var(--color-border-muted)` / `var(--color-border-subtle)` | Dense dividers, nested cards, non-interactive panels |
| Card surface | `var(--color-card)` (text: `--color-card-foreground`) | Layer above background |
| Popover / floating | `var(--color-popover)` (text: `--color-popover-foreground`) | Layer above card |
| Overlay / floating chrome | shared Dialog overlay, `bg-popover`, `border-border`, shadow utilities | Modal scrims, popovers, transient panels |
| Sidebar surface | `var(--color-sidebar)` | Distinct spatial zone with full sub-palette |
| Hover backgrounds | `var(--color-accent)` (outline/default), `var(--color-ghost-hover)` (ghost), `var(--color-secondary-hover)` (secondary) | Choose by variant |
| Status palettes | `var(--color-{error,success,warning,info}-{base,text,bg,border,…})` | See `tokens/colors/status.css` |
| Charts | Primitive scales: `var(--color-blue-500)`, `var(--color-green-500)`, etc. | No dedicated chart palette |
| Shadow | `var(--shadow-xs)` for hover, `var(--shadow-md)` for floating | 7-level scale |
### Example Component Prompts
- "Create a chat interface on `var(--color-background)`. Messages use `var(--font-size-body-md)` `var(--font-weight-regular)`, `var(--line-height-body-md)`, `var(--color-foreground)` text. User messages in cards with `var(--color-secondary)` background and `var(--radius-lg)` border-radius. Primary send button uses the Button `default` variant."
- "Design a sidebar navigation: `var(--color-sidebar)` background, 1px right border `var(--color-sidebar-border)`. Nav items use `var(--font-size-body-sm)` `var(--font-weight-medium)`, `var(--color-sidebar-foreground)` text. Active and hover items use `var(--color-sidebar-accent)` with `var(--color-sidebar-accent-foreground)` text."
- "Build a settings card: `var(--color-card)` background, 1px `var(--color-border)`, `var(--radius-lg)`. Title in `var(--font-size-heading-sm)` with the matching heading line-height. Description in `var(--font-size-body-sm)` `var(--font-weight-regular)`, `var(--color-foreground-secondary)`. Toggles and inputs at `var(--radius-md)`."
- "Create a dark-mode conversation view: `var(--color-background)` page. Message cards on `var(--color-card)`. Assistant code blocks use the code-rendering component's mono font stack at `var(--font-size-body-sm)` on `var(--color-popover)` with `var(--radius-md)`. Borders at `var(--color-border)`."
- "Design a destructive confirmation dialog with the shared Dialog shell: `bg-card`, `text-card-foreground`, `rounded-3xl`, `border-0`, `p-6`, `gap-4`, `shadow-xl`, default overlay. Footer uses outline cancel + destructive delete."
- "Build a page-owned settings side panel with `PageSidePanel`: it reads `usePortalContainer()` to scope into the owning route tab/page root when a `PortalContainerProvider` is present, otherwise falls back to the body portal; default fixed and scoped absolute `bg-black/50` backdrop, `top-3 bottom-3 right-3`, `w-100`, `bg-card`, `rounded-3xl`, `shadow-xl`, `title` for the shared `text-base` heading, body `px-6 py-4`, `PageSidePanelSection` groups separated by `gap-8`, and `PageSidePanelItem` rows separated by `gap-5` inside each group. Use only `PageSidePanel` for non-settings history/list/detail drawers, with a task-specific body layout."
- "Build a modal bottom drawer with the shared `Drawer` primitive: `bg-background`, edge-attached bottom content, `max-h-[80vh]`, `rounded-t-lg`, `border-t`, built-in drag handle, header/footer `p-4`. Do not use the floating `PageSidePanel` shell for this."
- "Floating toolbar: `bg-popover`, 1px `var(--color-border)`, `var(--radius-xl)`, `var(--shadow-md)`. Icon buttons inside use the shared `Button` with `variant=\"ghost\"` and `size=\"icon-sm\"`."
- "Dense row actions: use low-emphasis icon-only controls with muted default text, no static fill, tooltip/`aria-label`, hover-only emphasis, and active tint only when the action has persistent state. Promote this pattern into a shared `IconButton` before reusing it across pages."
### Iteration Guide
1. Start from semantic tokens — never hard-code hex / oklch / rgba values.
2. Elevation at rest through surface color layering (`var(--color-background)` → `var(--color-card)` → `var(--color-popover)`); use `var(--shadow-xs)` on hover and `var(--shadow-md)+` for floating elements.
3. Button hover: follow the shared Button variant definitions; only `default` and `destructive` keep the base `shadow-xs`, while outline/secondary/emphasis/ghost remain flat.
4. Public icon-only actions use shared `Button` ghost icon sizes first. For dense row-level low-emphasis actions with tone/active/tooltip behavior, promote a shared `IconButton` before duplicating page-local wrappers.
5. Body / heading font aliases handle UI typography; code-rendering components own mono font stacks.
6. Keep weights at `var(--font-weight-regular)` / `var(--font-weight-medium)` for UI and `var(--font-weight-bold)` for page-level emphasis.
7. `var(--radius-md)` for the base Button and inputs, `var(--radius-lg)` where a Button variant explicitly rounds itself, larger (14px+) for cards, `var(--radius-round)` for pills.
8. Semantic accents: `var(--color-destructive)` for danger, `var(--color-success)` for positive, `var(--color-warning)` for caution, `var(--color-info)` for informational.
9. For richer status surfaces use the full palettes in `tokens/colors/status.css` (e.g. `var(--color-error-bg)` + `var(--color-error-text)` + `var(--color-error-border)`).
10. Charts: use primitive `var(--color-blue-*)` / `var(--color-green-*)` / `var(--color-amber-*)` scales — no dedicated chart palette.
11. Overlay/floating surfaces: use the shared Dialog overlay or `bg-popover` + semantic border + shadow utilities. Add real exported tokens before introducing reusable glass/scrim aliases.
12. New headings: use the `var(--font-size-heading-*)` size tokens with the matching `var(--line-height-heading-*)`.