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>
101 lines
4.4 KiB
Markdown
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)
|