Compare commits

...
Author SHA1 Message Date
iris
72e087b086 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.
2026-08-18 23:29:20 +02:00
iris
56e7a309e8 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.
2026-08-18 23:29:20 +02:00
2 changed files with 89 additions and 1 deletions

View file

@ -17,6 +17,19 @@ 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).
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 +127,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 |
| -------- | --------- | -------------------- | --------------------------------------------------- |

View file

@ -29,3 +29,71 @@
--base0E: #cba6f7; /* magenta / mauve */
--base0F: #f5c2e7; /* extra accent — pink */
}
/* 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.
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-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 */
}
}