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

101 lines
4.4 KiB
Markdown

# 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 `borderHi` — **MUST 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](../../../governance/decisions/accessibility.md)),
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)