Source of truth: token sources live in
packages/ui/src/styles/tokens/and Tailwind-facing aliases are generated inpackages/ui/src/styles/theme.css. Renderer-only bridge aliases live insrc/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) tovar(--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-2xsthrough--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)
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.
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 layersoklch(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 solidoklchcolor steps — never alpha — because their identity must stay constant on any background.
When you reach for a value:
- 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 inventoklch(0 0 0 / 0.x)literals — the token already encodes the intent. - 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:
var(--color-primary)— exported primary accent for true page actions, selected states, links, and component accents. Shared Buttondefault/emphasiscurrently define their own neutral strong fills. - Primary Foreground:
var(--color-primary-foreground)— contrast text onbg-primarysurfaces - Primary Hover:
var(--color-primary-hover)
- 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
- Background:
var(--color-background)— primary page background (#FFFFFFlight /#0A0A0Adark) - 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:
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
- 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
- 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, andborder-lare 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/10throughborder-border/80, plus hover/focus/active variants) are compatibility-mapped in@cherrystudio/ui/styles/theme.cssso old surfaces do not fall back tocurrentColor. - Do not introduce new opacity-modified semantic border classes such as
border-border/60,border-border/40,border-border/30, orborder-border/15. Use the semantic border utilities above so the visual role is explicit.
- 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
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-*
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 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.
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
Dialogoverlay (bg-black/50) and customize only throughoverlayClassNamewhen needed. - Floating panels: use
bg-popover,border-border, and the appropriate shadow utility (shadow-mdtoshadow-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.
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.
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.
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.
- 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.
| 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.
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 |
| 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-2xltoken. For display contexts usingvar(--font-size-heading-2xl), set a one-off Tailwind line-height utility (e.g.leading-[72px]) until a canonical token is added.
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.
- 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×).
Padding values use
var(--cs-size-*)directly because--spacing-*is currently kept opt-in intheme.cssto 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.
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-900light /bg-neutral-100dark) - Text: white in light mode, neutral dark in dark mode
- Shadow:
shadow-xs - Hover: neutral hover fill (
hover:bg-neutral-800light /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-900light /bg-neutral-100dark) - Text: white in light mode, neutral dark in dark mode
- Radius:
var(--radius-lg) - Shadow: none
- Hover: neutral hover fill (
hover:bg-neutral-800light /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), textvar(--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 renderstext-neutral-900 dark:text-neutral-100.) - No, it's a utility shortcut → mute it with
text-foreground-muted hover:text-foregroundso 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-foregroundoverride to every icon button by reflex — the ghost default is for one action per cluster, not all of them. - Use
text-primaryas a "more emphasis" replacement for the ghost default;text-primaryis 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
ConfirmDialogplus 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
IconButtonwrapper 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 sharedIconButtonif another page needs the same behavior; do not create more page-local copies.
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:
- Default and destructive buttons keep the base
shadow-xs. - Outline, secondary, emphasis, and ghost buttons are flat (
shadow-none) at rest and on hover. - Link hover adds underline and a text color change only — no background, no shadow.
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], defaultbg-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 thesizeprop onDialogContent: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 asizeinstead; if no size fits, propose a new size in@cherrystudio/uirather than patching at the call site.classNameonDialogContentis reserved for non-width layout concerns (e.g.max-h-[70vh],flex flex-col overflow-hiddenfor 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 fromsm - Title:
text-lg leading-none font-semibold - Description:
text-muted-foreground text-sm - Footer: mobile
flex-col-reverse, desktop row withsm:justify-end - Close button: shown by default, absolute
top-4 right-4, low opacity, higher opacity on hover; hide withshowCloseButton={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 usingdefaultare acceptable during migration, but new work should not introduce a page-local primary style. - Use
Button variant="destructive"for dangerous confirmation actions. ConfirmDialogcurrently usesdefaultfor non-destructive confirms anddestructivefor 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.
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 over0.15s - Panel: fixed
top-3 bottom-3,right-3orleft-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:
PageSidePanelowns only the floating drawer shell: placement, backdrop, title/close chrome, body scroll, and footer.PageSidePanelSectionowns a settings group: section title, optional right-aligned low-emphasis actions, and group spacing.PageSidePanelItemowns 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— thisgap-3is 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 optionalchildrenslot 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-lgorrounded-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 frommd - 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.
Standard Card
- Background:
var(--color-card) - Text:
var(--color-card-foreground) - Border: 1px solid
var(--color-border) - Radius:
var(--radius-lg)tovar(--radius-xl) - Padding:
var(--cs-size-2xs)tovar(--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
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
PopoverContentfrom@cherrystudio/ui; override layout density withw-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 matchesContextMenu'smin-w-[8rem]baseline inpackages/ui/src/components/primitives/context-menu.tsx. Do not hard-code widths likew-40/w-44— they trap trailing whitespace when labels are shorter than the slot. - Compose menu bodies with
MenuListandMenuItemfrom@cherrystudio/ui. - Menu rows should use 32px height (
h-8),rounded-lg,px-2.5, andtext-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-popoverunless 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)tovar(--radius-xl) - Use: Floating toolbars, header bars over scrollable content, tooltips on imagery
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-backgroundwith a constrained width (max-w-5xlstyle scale) and responsive card grid. - Prominent tool-entry cards may use
bg-card,border-border,p-4, andvar(--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 useshadow-lgonly 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
PageSidePanelwith grouped sections and dense list rows. Use the sharedDrawerprimitive 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-backgroundpanes 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 usebg-primary/10ortext-primary. - File upload/drop states should use dashed semantic borders (
border-border-muted/ hoverborder-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.
- 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 examplefocus-visible:ring-2 focus-visible:ring-ring/50) - Font:
var(--font-family-body)betweenvar(--font-size-body-sm)andvar(--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 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 staysvar(--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 / SidebarMenuItemfamily lands in@cherrystudio/ui, hand-rolled sidebar menus will not be allowed. Until that family ships, compose withMenuList+MenuItem+ project-level className tokens (seesrc/renderer/pages/settings/index.tsxfor the canonical token pattern:settingsSubmenuItemClassName,settingsSubmenuItemLabelClassName,settingsSubmenuSectionTitleClassName,settingsSubmenuDividerClassName).
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>withtruncatefor overflow safety.action(optional) — right-aligned slot for icon-buttons (filter, add, etc.).bordered(optional) — adds aborder-b border-borderdivider underneath the header row. Defaultfalse. Use on right-pane detail headers to visually separate the title from the body; omit on left sidebar headers (which sit above aMenuListand 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 — wrapperpx-2.5(10px) + itempx-2.5(10px) = 20px. Do not change to symmetric padding. - Two adjacent
PageHeaderinstances (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-PageHeadercomponent (e.g.ProviderHeader, which carries a<Switch>plus multiple icons) must wrap itself in a container that draws an equivalentborder-b border-borderdivider — seeproviderDetailColumnClasses.headerContentMaxWidthinProviderSettings/primitives/classNames.ts. - Provider settings section headings use full
text-foregroundrather 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.
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.
loadingstate switches root/thumb coloring tobg-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 toswitchRootVariants/switchThumbVariantsand document it here. - Use
<DescriptionSwitch label="..." description="...">for reusable standalone preference rows. In densePageSidePanellayouts, composing a row label plus a bare<Switch>is acceptable when the surrounding row owns spacing and helper text.
- 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 tovar(--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 ash-11orh-[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:
SettingsWindowServiceinsrc/main/services/SettingsWindowService.ts.
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
PageHeaderfrom@cherrystudio/uiat 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
PageHeaderand passtitleClassName="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'smt-3 + h-8 + mb-2outer geometry is preserved, so the label baseline still aligns with the right column's PageHeader heading. Seepage-header.stories.tsx›SectionTitleStylefor the canonical example. - Wrap menu rows in
MenuListwithgap-1; group withMenuDivider+ a section title<div>carryingsettingsSubmenuSectionTitleClassName. - Each row is a
MenuItemstyled by the canonical settings token pair:settingsSubmenuItemClassNameonclassName(height / hover / active surface) andsettingsSubmenuItemLabelClassNameonlabelClassName(group-data-[active=true]:font-mediumfor the bold-on-active label). Both tokens live insrc/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 inProviderSettings/primitives/classNames.tsbut 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 legacySettingContainerfor "simple right-content" pages).SettingsContentBody— the same two-layer wrap, but for pages that mount their ownScrollbarexternally (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-4orpx-5 py-4on a settings page's outermost content container — they were the old, divergent paddings and are banned for new pages. - Apply
max-w-3xldirectly on a child component to "fix" centering on one page — fix the page container so every page is consistent. - Modify
SettingContainerto 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.
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 |
| 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 |
- Max content widths: Tailwind utilities (
max-w-smthroughmax-w-7xl) - Screen breakpoints: Tailwind defaults (
sm640px,md768px,lg1024px,xl1280px,2xl1536px)
⚠️ Cherry remaps the Tailwind default radius scale.rounded-mdresolves to 8px (Tailwind default: 6px),rounded-lgto 10px (default: 8px),rounded-xlto 14px (default: 12px), androunded-3xlto 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 |
Cherry Studio uses a dual depth system: surface color layering for structural hierarchy and box-shadows for interactive feedback (hover states, floating elements).
| 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.
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 |
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.
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.
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.
Use icon-library defaults unless a component has a documented reason to override SVG stroke-width.
- 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, andvar(--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#0A0A0Awith 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
*-hovertokens 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. fromtokens/colors/status.css) for richer status surfaces - Use
var(--color-border),var(--color-border-muted), andvar(--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)vsvar(--color-background)vsvar(--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)tovar(--shadow-lg)for floating elements (popovers, dropdowns, large panels), andvar(--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 use shadows for static elevation — reserve shadows for hover feedback and floating elements
- Don't use
var(--radius-xs)orvar(--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, orborder-border/15— choose a semantic border token instead - Don't apply
var(--shadow-xl)orvar(--shadow-2xl)to standard UI elements — reservevar(--shadow-xl)for Dialogs, PageSidePanel, and full-screen overlays, andvar(--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-2unless they are exported by the theme in the same change
| 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) |
- 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
| 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 |
- "Create a chat interface on
var(--color-background). Messages usevar(--font-size-body-md)var(--font-weight-regular),var(--line-height-body-md),var(--color-foreground)text. User messages in cards withvar(--color-secondary)background andvar(--radius-lg)border-radius. Primary send button uses the Buttondefaultvariant." - "Design a sidebar navigation:
var(--color-sidebar)background, 1px right bordervar(--color-sidebar-border). Nav items usevar(--font-size-body-sm)var(--font-weight-medium),var(--color-sidebar-foreground)text. Active and hover items usevar(--color-sidebar-accent)withvar(--color-sidebar-accent-foreground)text." - "Build a settings card:
var(--color-card)background, 1pxvar(--color-border),var(--radius-lg). Title invar(--font-size-heading-sm)with the matching heading line-height. Description invar(--font-size-body-sm)var(--font-weight-regular),var(--color-foreground-secondary). Toggles and inputs atvar(--radius-md)." - "Create a dark-mode conversation view:
var(--color-background)page. Message cards onvar(--color-card). Assistant code blocks use the code-rendering component's mono font stack atvar(--font-size-body-sm)onvar(--color-popover)withvar(--radius-md). Borders atvar(--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-viewportbg-black/50backdrop, fixedtop-3 bottom-3 right-3,w-100,bg-card,rounded-3xl,shadow-xl,titlefor the sharedtext-baseheading, bodypx-6 py-4,PageSidePanelSectiongroups separated bygap-8, andPageSidePanelItemrows separated bygap-5inside each group. Use onlyPageSidePanelfor non-settings history/list/detail drawers, with a task-specific body layout." - "Build a modal bottom drawer with the shared
Drawerprimitive:bg-background, edge-attached bottom content,max-h-[80vh],rounded-t-lg,border-t, built-in drag handle, header/footerp-4. Do not use the floatingPageSidePanelshell for this." - "Floating toolbar:
bg-popover, 1pxvar(--color-border),var(--radius-xl),var(--shadow-md). Icon buttons inside use the sharedButtonwithvariant=\"ghost\"andsize=\"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 sharedIconButtonbefore reusing it across pages."
- Start from semantic tokens — never hard-code hex / oklch / rgba values.
- Elevation at rest through surface color layering (
var(--color-background)→var(--color-card)→var(--color-popover)); usevar(--shadow-xs)on hover andvar(--shadow-md)+for floating elements. - Button hover: follow the shared Button variant definitions; only
defaultanddestructivekeep the baseshadow-xs, while outline/secondary/emphasis/ghost remain flat. - Public icon-only actions use shared
Buttonghost icon sizes first. For dense row-level low-emphasis actions with tone/active/tooltip behavior, promote a sharedIconButtonbefore duplicating page-local wrappers. - Body / heading font aliases handle UI typography; code-rendering components own mono font stacks.
- Keep weights at
var(--font-weight-regular)/var(--font-weight-medium)for UI andvar(--font-weight-bold)for page-level emphasis. 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.- Semantic accents:
var(--color-destructive)for danger,var(--color-success)for positive,var(--color-warning)for caution,var(--color-info)for informational. - 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)). - Charts: use primitive
var(--color-blue-*)/var(--color-green-*)/var(--color-amber-*)scales — no dedicated chart palette. - 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. - New headings: use the
var(--font-size-heading-*)size tokens with the matchingvar(--line-height-heading-*).