Files
CherryHQ-cherry-studio/DESIGN.md
2026-06-30 20:06:57 +08:00

867 lines
64 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.41.5×, headings tighter (~1.01.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 `<div className="flex flex-col gap-5">` 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)` (1624px)
- 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 `<h2>` with manual padding.
**Anatomy:**
- `title` (required) — heading text, rendered inside an `<h2>` 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 `<Switch>` 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 `<DescriptionSwitch label="..." description="...">` for reusable standalone preference rows. In dense `PageSidePanel` layouts, composing a row label plus a bare `<Switch>` 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 `<div>` 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 0384px 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 24px 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 | 6401024px | Collapsible sidebar overlay, condensed spacing |
| Desktop | 10241280px | 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 4896px to 2448px 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-*)`.