feat: theme toggle

This commit is contained in:
2026-09-23 21:57:24 +02:00
parent 9db3602dea
commit 2663d7eac6
9 changed files with 222 additions and 9 deletions
+12
View File
@@ -144,6 +144,18 @@ Because both surface blocks define the *same* token names, a component written
against `--text-primary` is automatically correct on either one - which is why
components must never reach past a role to a primitive.
**The one global exception, accepted deliberately - the theme toggle.** The
nav's sun/moon control (Navigation/ThemeToggle.tsx) lets a reader re-root the
surface the whole document stands on: the root action persists the
`sharper-theme` cookie, the root loader returns it and `<html
data-surface={theme}>` follows, so a light-theme visit stands on
`[data-surface="light"]` and every token re-resolves. The mechanism is still
surfaces-only - the toggle never repaints anything itself, it just chooses
which pole `:root` behaviour applies to - and the header/footer follow the
theme unless a route's `handle` forces a surface. `--surface-nav` (the
translucent bar) must stay defined in BOTH surface blocks, or a light-theme
nav resolves against the dark block.
Utility variants `surface-dark:` and `surface-light:` exist for the rare case a
utility (not a token) has to differ, and `rtl:` / `ltr:` for direction. Prefer
CSS logical properties (`ps-`, `pe-`, `ms-`, `me-`, `start-`, `end-`) so RTL
+10
View File
@@ -17,6 +17,13 @@
:root and [data-surface="dark"] share one block; [data-surface="light"]
comes afterwards so it wins on the same element, and proximity makes the
nearest data-surface ancestor win for any subtree.
THEME TOGGLE (the declared exception to "no global theme"). The nav's
theme control flips WHICH surface the DOCUMENT stands on: the root action
persists `sharper-theme`, the root loader returns it, and <html
data-surface> follows - so light mode means the whole site stands on
[data-surface="light"] and every token re-resolves, including the nav and
footer, which follow the theme unless a route's handle forces a surface.
========================================================================= */
/* ---------------------------------------------------------------------------
@@ -193,6 +200,9 @@
--surface-danger-subtle: var(--color-red-100);
--surface-inverse: var(--color-neutral-900); /* figma background/inverse #0b0b0a */
--surface-footer: var(--color-white); /* figma 140:7812, the light footer */
/* The light bar floats the same way the dark one does - the ground colour at
90% with a blur behind it, so content shows through as it scrolls under. */
--surface-nav: color-mix(in srgb, var(--color-white) 90%, transparent);
--surface-selection: var(--color-green-200);
/* --- text ------------------------------------------------------------- */