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.
9.5 KiB
CSS custom properties (theme variables)
Colour variables live in two standalone stylesheets, split so a theme swap touches only the first:
colors.css— the 16--base00…--base0Fbase16 slots. This is the entire theme swap contract. A generator (e.g. one fed a stylix base16 scheme, which is natively base00–base0F) replaces only this file.theme.css— the semantic layer:--bg,--fg,--purple, … derived from the base16 slots (--bg: var(--base00)etc.). This is what the app references and it never changes on a swap.
Both files live in frontend/packages/shared/src/. Per-page stylesheets
(common.css, dashboard.css, flow.css, logs.css, agent.css)
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 @imported into the page bundles:
each package re-exports them (src/{colors,theme}.css →
@import "@hive/shared/…") so esbuild emits its own
dist/static/colors.css + dist/static/theme.css, and every page links
them first (colors.css then theme.css) ahead of the page CSS. Note
theme.css does not @import colors.css — that would bake the
slots back into it; they're two separate output files so a swap replaces
just colors.css. (Load order doesn't actually affect resolution —
custom properties resolve at computed-value time — but colors.css is
linked first for clarity.)
Semantic palette (theme.css)
Each semantic var derives from a base16 slot (or, for the three that have
no clean slot, a color-mix() over base16 — pixel-identical under the
default palette). Default hexes shown are Catppuccin Mocha.
| Variable | Derives from | Default hex | Use |
|---|---|---|---|
--bg |
base00 |
#1e1e2e |
page background |
--bg-elev |
base01 |
#181825 |
elevated surfaces: floating dropdowns, popovers |
--border |
base02 |
#313244 |
general borders, hover/active backgrounds |
--purple-dim |
base03 |
#45475a |
subtle borders, terminal chrome, badge backgrounds |
--fg |
base05 |
#cdd6f4 |
primary text colour |
--red |
base08 |
#f38ba8 |
errors, fail state |
--amber |
base09 |
#fab387 |
warnings, pending / running state |
--yellow |
base0A |
#f9e2af |
flash messages, mild warnings |
--green |
base0B |
#a6e3a1 |
success, ok state |
--cyan |
base0C |
#89dceb |
tool-use events, info accents |
--blue |
base0D |
#89b4fa |
links, interactive accent (distinct from cyan) |
--purple |
base0E |
#cba6f7 |
accent — active tabs, links, highlights |
--pink |
base0F |
#f5c2e7 |
thinking events |
--crust |
mix(base00 58%, #000) |
#11111b |
terminal / code block background (below --bg) |
--muted |
mix(base05 55.5%, base00) |
#7f849c |
secondary / dimmed text |
--subtext0 |
mix(base05 77.7%, base00) |
#a6adc8 |
toolbar/status text; dimmer than --fg, lighter than --muted |
Common mistakes
The Catppuccin colour names do not map 1:1 to the variable names. Variables to avoid (undefined — they will silently resolve to transparent / inherited):
| Wrong | Correct |
|---|---|
--text |
--fg |
--mauve |
--purple |
--surface0 |
--bg-elev (float bg) or --border (border/hover) |
--surface1 |
--border |
--surface2 |
--purple-dim |
--overlay0, --overlay1 |
--muted |
--base, --mantle |
--bg, --bg-elev |
Usage guide
Floating menus and dropdowns (e.g. agent context menu, tabbar overflow):
background: var(--bg-elev);
border: 1px solid var(--purple-dim);
Hover / active state backgrounds:
background: var(--border);
Active tab text / accent elements:
color: var(--purple);
Muted / meta text:
color: var(--muted);
Error / warning / success badges:
color: var(--red); /* error */
color: var(--amber); /* warning / running */
color: var(--green); /* ok */
Theme swapping — the base16 contract
The swap interface is colors.css — the 16 base16 slots, not our
semantic names. A theme generator (e.g. one reading a stylix base16
scheme) overrides only colors.css; the semantic layer in theme.css
derives everything else, so the whole UI re-themes with nothing else to
template or regenerate. The base16 slot → semantic mapping is internal
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
(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 |
|---|---|---|---|
base00 |
#1e1e2e |
default bg | --bg, (darkened) --crust |
base01 |
#181825 |
lighter bg | --bg-elev |
base02 |
#313244 |
selection/surface | --border |
base03 |
#45475a |
comments/dim surface | --purple-dim |
base04 |
#585b70 |
dark foreground | (unused; kept for completeness) |
base05 |
#cdd6f4 |
default foreground | --fg, (blended) --muted/--subtext0 |
base06 |
#f5e0dc |
light foreground | (unused) |
base07 |
#b4befe |
lightest | (unused) |
base08 |
#f38ba8 |
red | --red |
base09 |
#fab387 |
orange | --amber |
base0A |
#f9e2af |
yellow | --yellow |
base0B |
#a6e3a1 |
green | --green |
base0C |
#89dceb |
cyan | --cyan (our sky; Catppuccin's base0C is teal) |
base0D |
#89b4fa |
blue | --blue |
base0E |
#cba6f7 |
magenta | --purple |
base0F |
#f5c2e7 |
extra accent | --pink |
Notes for theme authors:
--crust,--muted,--subtext0have no own slot — they're derived viacolor-mix()over base16 (--crust= a darkenedbase00;--muted/--subtext0=base05↔base00blends). They still track a swap automatically; no generator action needed.- A standard Catppuccin base16 scheme uses teal for
base0C; we default it to sky (#89dceb) to preserve the historical accent. A stylix Catppuccin scheme will shift--cyanto teal — that's the operator's chosen scheme, working as intended. base04/base06/base07aren't consumed by a semantic var today; they're kept at their standard values so the 16-slot contract is complete (a partial scheme override still works).