From 56e7a309e8731433eb004b439c652cdcd6293621 Mon Sep 17 00:00:00 2001 From: iris Date: Tue, 18 Aug 2026 22:42:36 +0200 Subject: [PATCH 1/2] swarm-ui: respect prefers-color-scheme for a light theme default colors.css shipped one fixed dark (Catppuccin Mocha) palette with no light variant and no OS/browser-preference detection, so anyone without a stylix-managed session (a phone browser, a future PWA install) got the dark palette regardless of their light-mode preference. This is an accessibility gap, not a cosmetic one - some people need light for low-vision/contrast reasons, others need dark for photosensitivity, the same reasoning that already gates prefers-reduced-motion elsewhere. Add a Catppuccin Latte row behind `@media (prefers-color-scheme: light)`, ported through the identical base16 slot mapping (including the existing sky-over-teal base0C substitution) so a swap between the two rows never changes what a slot means. A stylix-generated colors.css replaces the file wholesale with a single unconditional :root block and has no media query in it, so a themed deployment is unaffected either way. Verified with headless chromium screenshots forcing both prefers-color-scheme values against the built swarm-ui bundle - both render with correct contrast, and the default (no forced preference) render matches the light row. --- docs/web-ui/css-vars.md | 17 ++++++++++- frontend/packages/shared/src/colors.css | 40 +++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/docs/web-ui/css-vars.md b/docs/web-ui/css-vars.md index fa17eaaa..5b2a7281 100644 --- a/docs/web-ui/css-vars.md +++ b/docs/web-ui/css-vars.md @@ -17,6 +17,14 @@ 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 (light) row gated +behind `@media (prefers-color-scheme: light)` — both ported through the +identical slot mapping, so which row is active never changes what a slot +means. 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). + Both are deliberately standalone, not `@import`ed into the page bundles: each package re-exports them (`src/{colors,theme}.css` → `@import "@hive/shared/…"`) so esbuild emits its own @@ -114,7 +122,14 @@ 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. -`colors.css` base16 slot defaults (Catppuccin Mocha): +**Two rows, 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. + +`colors.css` base16 slot defaults (Catppuccin Mocha, dark default): | Slot | Default | Standard base16 role | Mapped to | | -------- | --------- | -------------------- | --------------------------------------------------- | diff --git a/frontend/packages/shared/src/colors.css b/frontend/packages/shared/src/colors.css index 3fa8ca51..7c5943e8 100644 --- a/frontend/packages/shared/src/colors.css +++ b/frontend/packages/shared/src/colors.css @@ -29,3 +29,43 @@ --base0E: #cba6f7; /* magenta / mauve */ --base0F: #f5c2e7; /* extra accent — pink */ } + +/* Light-mode default — Catppuccin Latte, the standard light complement + to the Mocha defaults above, ported through the identical base16 + slot mapping (down to the same base0C sky-over-teal substitution) so + a swap between the two rows never changes which semantic role a slot + plays. This is a *default*, not a user preference store: it only + applies when nothing more specific has already decided the palette. + A stylix-generated colors.css (nix/host-modules/hive-c0re/theme.nix) + replaces this entire file wholesale — a fixed, unconditional `:root` + block with no media query in it at all — so a stylix-themed + deployment is naturally unaffected by, and never fights, this block. + Some people need light for low-vision/contrast reasons, others need + dark for photosensitivity, so respect the OS/browser signal by + default the same way `prefers-reduced-motion` already is elsewhere — + rather than always forcing the bundled dark palette on a client with + no stylix session of its own (e.g. a phone browser hitting swarm-ui + directly). A future per-user override takes precedence over this + media query by simply setting the vars later in the cascade (e.g. + via an inline `style` on `:root`, which always outranks a + stylesheet rule). */ +@media (prefers-color-scheme: light) { + :root { + --base00: #eff1f5; /* default bg */ + --base01: #e6e9ef; /* lighter bg (elevated surfaces) */ + --base02: #ccd0da; /* selection / surface — borders */ + --base03: #bcc0cc; /* comments / dim surface */ + --base04: #acb0be; /* dark foreground */ + --base05: #4c4f69; /* default foreground */ + --base06: #dc8a78; /* light foreground */ + --base07: #7287fd; /* lightest */ + --base08: #d20f39; /* red */ + --base09: #fe640b; /* orange / peach */ + --base0A: #df8e1d; /* yellow */ + --base0B: #40a02b; /* green */ + --base0C: #04a5e5; /* cyan (our sky — note: Catppuccin's base0C is teal) */ + --base0D: #1e66f5; /* blue */ + --base0E: #8839ef; /* magenta / mauve */ + --base0F: #ea76cb; /* extra accent — pink */ + } +} From 72e087b086f8d36715f3bcd70ad3c888266ee986 Mon Sep 17 00:00:00 2001 From: iris Date: Tue, 18 Aug 2026 22:57:47 +0200 Subject: [PATCH 2/2] swarm-ui: fix WCAG contrast failures in the light-theme accent slots argus review: the literal Catppuccin Latte accent hexes (base08-0F) fail badly as StatusChip fill-text (green/amber/red/yellow on --purple-dim/base03: 1.4:1-3:1, need 4.5:1) and even as plain text on --bg/base00 in the agent/dashboard packages that also import this file (2.3:1-4.8:1). Root cause: Latte's own accents are calibrated against Lattes near-white base/crust, not a mid-gray surface1; the Mocha row does not hit this because Mochas pastel accents are already light, so they contrast fine against a dark surface1 - Lattes saturated-but-mid accents do not have the same headroom against Lattes own lighter surface1. Fix: darken each accent slot from stock Latte (same hue/saturation, lower HSL lightness via binary search) until real WCAG contrast against base03 clears 4.5:1 with margin - verified this also clears >7:1 against base00, so both the swarm-ui chip case and the plain-text case elsewhere are covered by one set of values. base00-07 stay stock Latte (no contrast role, just surfaces/foreground). Yellow and peach read closer to olive/brown than a bright yellow/orange once darkened this far - an inherent property of darkening a warm hue in sRGB, not a mapping mistake; boosting saturation further did not rescue it (checked). Verified with headless-chromium screenshots against the built swarm-ui bundle: StatusChip text (the exact case argus flagged) and every chip tone on /components now read clearly. --- docs/web-ui/css-vars.md | 15 ++++--- frontend/packages/shared/src/colors.css | 58 ++++++++++++++++++------- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/docs/web-ui/css-vars.md b/docs/web-ui/css-vars.md index 5b2a7281..a7d42f22 100644 --- a/docs/web-ui/css-vars.md +++ b/docs/web-ui/css-vars.md @@ -18,12 +18,17 @@ 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 (light) row gated -behind `@media (prefers-color-scheme: light)` — both ported through the +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. 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). +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). Both are deliberately standalone, not `@import`ed into the page bundles: each package re-exports them (`src/{colors,theme}.css` → diff --git a/frontend/packages/shared/src/colors.css b/frontend/packages/shared/src/colors.css index 7c5943e8..25181d13 100644 --- a/frontend/packages/shared/src/colors.css +++ b/frontend/packages/shared/src/colors.css @@ -30,13 +30,16 @@ --base0F: #f5c2e7; /* extra accent — pink */ } -/* Light-mode default — Catppuccin Latte, the standard light complement - to the Mocha defaults above, ported through the identical base16 - slot mapping (down to the same base0C sky-over-teal substitution) so - a swap between the two rows never changes which semantic role a slot - plays. This is a *default*, not a user preference store: it only - applies when nothing more specific has already decided the palette. - A stylix-generated colors.css (nix/host-modules/hive-c0re/theme.nix) +/* Light-mode default. base00-07 (surfaces/foreground) are stock + Catppuccin Latte — no contrast issue there, they're the ends of the + bg/fg scale. base08-0F (the eight chromatic "tone" slots: red through + pink) are deliberately NOT stock Latte hexes — see the comment right + above those declarations below for why; don't reuse stock Catppuccin + Latte's own accent hexes there if this ever gets regenerated. + + This is a *default*, not a user preference store: it only applies + when nothing more specific has already decided the palette. A + stylix-generated colors.css (nix/host-modules/hive-c0re/theme.nix) replaces this entire file wholesale — a fixed, unconditional `:root` block with no media query in it at all — so a stylix-themed deployment is naturally unaffected by, and never fights, this block. @@ -59,13 +62,38 @@ --base05: #4c4f69; /* default foreground */ --base06: #dc8a78; /* light foreground */ --base07: #7287fd; /* lightest */ - --base08: #d20f39; /* red */ - --base09: #fe640b; /* orange / peach */ - --base0A: #df8e1d; /* yellow */ - --base0B: #40a02b; /* green */ - --base0C: #04a5e5; /* cyan (our sky — note: Catppuccin's base0C is teal) */ - --base0D: #1e66f5; /* blue */ - --base0E: #8839ef; /* magenta / mauve */ - --base0F: #ea76cb; /* extra accent — pink */ + /* base08-0F: darkened from stock Catppuccin Latte's own accent + hexes (same hue/saturation, lower HSL lightness), not a literal + upstream port like base00-07 above. argus's review on this PR + computed real WCAG contrast ratios and found the literal Latte + accents fail badly as `StatusChip` fill-text (green/amber/red/ + yellow on `--purple-dim`/base03: 1.4:1-3:1, need 4.5:1) and even + as plain text on `--bg`/base00 in the agent/dashboard packages + that also consume this same file (2.3:1-4.8:1). Root cause: + Latte's own accent colors are calibrated against Latte's + near-white `base`/`crust`, not against a mid-gray `surface1` — + the Mocha row doesn't have this problem because Mocha's pastel + accents are already *light*, so they contrast fine against a + *dark* surface1; Latte's saturated-but-mid-brightness accents + don't have the equivalent headroom against Latte's own + (much lighter) surface1. Each value below was picked by holding + the stock Latte hue+saturation fixed and binary-searching HSL + lightness down to the point real contrast against base03 clears + 4.5:1 with a small margin (also verified against base00: all + land above 7:1 there) — same hue identity, same base16 role, + just dark/saturated enough to actually be legible as filled-chip + or plain text either way. Yellow and peach read closer to + olive/brown than a bright yellow/orange once darkened this far — + an inherent property of darkening a warm hue in sRGB (Bezold- + Brücke shift), not a mapping mistake; boosting saturation + further didn't rescue it (verified). */ + --base08: #9c0b2a; /* red */ + --base09: #883201; /* orange / peach */ + --base0A: #6d450e; /* yellow */ + --base0B: #235818; /* green */ + --base0C: #025374; /* cyan (our sky — note: Catppuccin's base0C is teal) */ + --base0D: #0843b8; /* blue */ + --base0E: #6311ce; /* magenta / mauve */ + --base0F: #8f166e; /* extra accent — pink */ } }