swarm-ui: derive theme override from colors.css instead of duplicating hex values

mara's review: the theme override read as a hand-copied duplicate of
colors.css's hex values instead of deriving from it. Restructured
colors.css to declare each palette's 16 hex values exactly once
(--mocha-baseNN, --latte-baseNN) and have every activation block
(the default, the prefers-color-scheme media query, and two new
:root[data-theme='light'|'dark'] blocks) just re-point the active
--baseNN slot at one of those two raw palettes via var() - never a
second copy of a hex value.

theme-apply.ts simplifies to match: it now only toggles a data-theme
attribute on <html>, same shape motion-apply.ts already had. No
palette values live in JS at all anymore.

Re-verified the override still genuinely outranks the media query
with the new mechanism: same seed-localStorage-while-forcing-the-
opposite-OS-preference test as before, both directions still render
the stored override correctly.
This commit is contained in:
iris 2026-08-18 23:53:42 +02:00 committed by mara
commit afe627e0a9
3 changed files with 200 additions and 163 deletions

View file

@ -17,18 +17,23 @@ reference the **semantic** names and must **not** redeclare them or reach
for the raw `--baseNN` slots directly. (`base.css` holds only the shared
`body` typography — it references the palette but no longer defines it.)
`colors.css` ships two rows of the same 16 slots: an unconditional
Catppuccin Mocha (dark) default, and a Catppuccin-Latte-derived (light)
row gated behind `@media (prefers-color-scheme: light)` — both use the
identical slot mapping, so which row is active never changes what a slot
means. The light row's `base00`-`base07` (surfaces/foreground) are stock
Latte hexes; `base08`-`base0F` (the eight chromatic slots) are darkened
from stock Latte to actually clear WCAG AA against how this app uses
them (real contrast failures found in review — see the comment above
those declarations in `colors.css` for the numbers and why). This is the
OS/browser-level default only; it's what a client with no more specific
theme decision gets (see "Theme swapping" below for how a stylix
deployment or a future per-user override supersede it).
`colors.css` declares each palette's 16 hex values exactly once —
`--mocha-baseNN` (dark, Catppuccin Mocha) and `--latte-baseNN` (light,
Catppuccin-Latte-derived; `base08`-`base0F` are darkened from stock
Latte to actually clear WCAG AA against how this app uses them — real
contrast failures found in review, see the comment above those
declarations in `colors.css` for the numbers and why). Everything else
in the file just points the active `--baseNN` slot at one of those two
raw palettes via `var()`, never a second copy of a hex value: an
unconditional default (Mocha), a row gated behind `@media
(prefers-color-scheme: light)` (Latte, the OS/browser-level default),
and two `:root[data-theme='light'|'dark']` blocks a per-user override
sets (swarm-ui's `SettingsMenu`) — an attribute selector always
outranks the plain `:root` inside the media query, so the override wins
regardless of the actual OS preference. Absent an override, the media
query decides; that's what a client with no more specific theme
decision gets (see "Theme swapping" below for how a stylix deployment
supersedes all of this at once).
Both are deliberately standalone, not `@import`ed into the page bundles:
each package re-exports them (`src/{colors,theme}.css`
@ -127,12 +132,13 @@ to `theme.css` (the "Derives from" column above) — a generator never
needs to know our var names, and `theme.css` + the page bundles stay
untouched.
**Two rows, one contract.** `colors.css` ships both a dark default
**Two palettes, one contract.** `colors.css` ships both a dark default
(Catppuccin Mocha, unconditional) and a light default (Catppuccin Latte,
behind `@media (prefers-color-scheme: light)`) — a theme generator that
overrides the file wholesale (e.g. the stylix path, which writes a single
unconditional `:root` block with no media query) supersedes both rows at
once, same as today.
behind `@media (prefers-color-scheme: light)`), plus a per-user override
path (`:root[data-theme]`) — a theme generator that overrides the file
wholesale (e.g. the stylix path, which writes a single unconditional
`:root` block with no media query, no `--mocha-*`/`--latte-*` vars, no
`[data-theme]` blocks) supersedes all of it at once, same as today.
`colors.css` base16 slot defaults (Catppuccin Mocha, dark default):