Files
clide/.claude/skills/ui-design/references/theme.md
T
jpmschweitzerandClaude Opus 4.8 3ff09dfb6e docs: note the don't-retune-published-themes rule in ui-design
Records the D-69 lesson surfaced live: a named theme's palette is a user
contract — ship an -hc sibling for a11y rather than retuning the artist's
colours. Includes the baseline-vs-extended split and the Catppuccin Latte
case where even the baseline chrome pairs are too soft.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:54:13 +02:00

4.4 KiB

Theme — token system, palette, typography

Token identity rule

Every visual surface gets its own named token. Never borrow a token from another surface just because they happen to resolve to the same color.

Wrong: sidebarBackground for the hat bar (the hat isn't a sidebar). Right: Create chromeBackground that resolves to the same palette key.

When two surfaces share a color:

  1. Same conceptual surface (sidebar + context panel are both "side panels") → one shared token set is fine.
  2. Different surfaces that happen to match (hat bar + sidebar + status bar are all "chrome frame") → create a shared primitive in the palette/semantic layer (e.g. bgChrome) and give each surface its own token that maps to that primitive. This lets themes diverge them later without breaking widgets.

Palette depth primitives

The palette layer has these depth primitives (defined in each theme YAML):

  • bg (#20202C) — outermost root, behind everything
  • bgSunken (#1A1A24) — chrome frame: sidebar, hat, statusbar
  • surface (#242838) — elevated: pane headers, active tabs
  • surfaceHi (#2C3046) — interactive: hover states, selections

Chrome tokens (chromeBackground / chromeForeground / chromeBorder) are the shared root for all frame surfaces. They resolve to bgSunken / textDim / border in the palette. Themes can override them to diverge hat from sidebar from status bar if desired.

Typography

Three constants — never hardcode sizes or families:

family UI    → inherited from DefaultTextStyle (JosefinSans Light 300)
family mono  → clideMonoFamily (JetBrainsMono)
body size    → clideFontBody (15)
caption size → clideFontCaption (14) — status bar, section headers, git info
mono size    → clideFontMono (14) — terminal, code, paths, IDs

Use ClideText for themed text. Set muted: true for secondary text (resolves to globalTextMuted). Set fontFamily: clideMonoFamily for code/paths/IDs. Don't set fontFamily for UI text — it inherits.

Extension-owned domain colors

Extensions that need domain-specific color coding (ticket types, decision types, priority levels) should NOT add tokens to SurfaceTokens. Instead:

  1. Create a color map class in the extension (e.g. TicketTypeColors).
  2. Ship dark and light presets, auto-selected via ClideTheme.of(context).dark.
  3. Store user overrides under ext.<id>.colors in settings.
  4. Reference: lib/builtin/tickets/src/ticket_colors.dart.

This keeps the core token surface lean and lets each extension own its palette. The pattern scales to any extension needing domain colors.

Named themes are user contracts — never retune their palette (D-69)

A bundled theme that ships under a recognisable name (clide, midnight, paper, terminal, catppuccin-*, …) is a user contract. Its palette — including syntax tokens, status colours, and borderHiMUST NOT be retuned to pass a contrast gate. Users pick Catppuccin because it looks like Catppuccin; quietly darkening a swatch to clear WCAG changes what they signed up for. Don't "fix" the artist's vision.

When a theme fails the contrast gate (D-22), the fix is a sibling -hc (high-contrast) theme, not an edit to the faithful one (D-69):

  • Keep foo.yaml pixel-faithful to the source palette.
  • Add foo-hc.yaml — same silhouette, but muted text / status chips / syntax tokens / focus border bumped to clear the strict (extendedPairs) gate. Only -hc/-cb variants are held to the extended set; base themes pass the baseline (canonicalPairs) only.
  • Register both in main.dart _loadBundledThemes and the bundled list in test/a11y/contrast_test.dart.

Mechanics: canonicalPairs (baseline, every theme) vs extendedPairs (strict, -hc/-cb only) live in lib/kernel/src/theme/contrast.dart. If a faithful palette is so soft it can't clear even the baseline chrome pairs, that's a design call — surface it, don't silently retune. (Catppuccin Latte hit exactly this on two chrome pairs.)

This is the same split VS Code ships: Default Dark+ plus a separate Default High Contrast.

Where to look in the codebase

  • Full token list: lib/kernel/src/theme/tokens.dart
  • Resolver fallbacks: lib/kernel/src/theme/resolver.dart
  • Theme YAML example: lib/kernel/src/theme/themes/clide.yaml
  • Decision: D-43 (handoff), D-44 (four bundled themes), D-45 (syntax tokens)