Skip to content

Latest commit

 

History

History
866 lines (673 loc) · 63.8 KB

File metadata and controls

866 lines (673 loc) · 63.8 KB

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-thinfont-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. 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.

  • Backdrop: fixed inset-0, z-[60], bg-black/50, fades over 0.15s
  • Panel: fixed 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; legacy antd drawers are migration targets, not the design contract.

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 <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). Only override it for legacy navbar-position modes or 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) → ScrollbarMenuList 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.tsxSectionTitleStyle 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 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: body-portaled full-viewport bg-black/50 backdrop, fixed 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-*).