feat(ui): default theme to system (follow OS light/dark)
Tests · UI / test (push) Has been cancelled
Tests · UI / test (pull_request) Failing after 2m7s

Light has been signed off, so the theme store's default choice is now
`system` (it was `dark` during the incremental migration). This matches
the app.html pre-paint guard, which already resolved an unset choice via
prefers-color-scheme — removing the brief boot-time mismatch where the
store re-pinned dark. Users still pin light/dark via the account-menu
picker. Updates the store default + its test and the design-system /
finalize-plan docs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-22 08:36:17 +02:00
parent 642c5b7322
commit e193f3ca88
4 changed files with 14 additions and 13 deletions
+3 -3
View File
@@ -25,9 +25,9 @@ Merged to `development` via PR #26 (2026-05-22): a shared design-token
system (`ui/frontend/src/lib/theme/`), light/dark theming with a picker system (`ui/frontend/src/lib/theme/`), light/dark theming with a picker
and pre-paint guard, and the whole UI migrated onto the tokens. and pre-paint guard, and the whole UI migrated onto the tokens.
Documented literal exceptions: the battle-scene data-viz palette, overlay Documented literal exceptions: the battle-scene data-viz palette, overlay
scrims, and directional/deliberate drop shadows. The default theme is scrims, and directional/deliberate drop shadows. The default theme
dark; flipping it to `system` is an available follow-up. Tokens and follows the OS (`system`); the account-menu picker pins light or dark.
conventions live in `ui/docs/design-system.md`. Tokens and conventions live in `ui/docs/design-system.md`.
Goal: replace the ad-hoc per-component styling (inline hex colors like Goal: replace the ad-hoc per-component styling (inline hex colors like
`#0a0e1a`, one-off spacing) with a shared design language so every view `#0a0e1a`, one-off spacing) with a shared design language so every view
+4 -4
View File
@@ -72,7 +72,8 @@ the colour block in `tokens.css`.
persists the choice, applies `data-theme`, and — while the choice is persists the choice, applies `data-theme`, and — while the choice is
`system` — follows OS theme changes via `matchMedia`. `system` — follows OS theme changes via `matchMedia`.
- The account menu (`account-menu.svelte`) exposes the picker. The - The account menu (`account-menu.svelte`) exposes the picker. The
default is `dark`; `system` follows the OS. default is `system` (it follows the OS preference); `light` / `dark`
pin a theme.
The `app.html` guard and the store deliberately duplicate the The `app.html` guard and the store deliberately duplicate the
resolution logic (one runs before modules load, the other after) — keep resolution logic (one runs before modules load, the other after) — keep
@@ -115,6 +116,5 @@ The only remaining literal colours are the documented exceptions above:
the battle-scene data-viz palette, the overlay scrims, and the the battle-scene data-viz palette, the overlay scrims, and the
directional / deliberate drop shadows. directional / deliberate drop shadows.
The default theme is **dark** while light coherence is being verified The default theme is **`system`** — it follows the OS light/dark
across the migrated views; once the owner signs off on light, the preference; users can pin light or dark via the account-menu picker.
default can flip to `system`.
+2 -2
View File
@@ -24,11 +24,11 @@ export const THEME_STORAGE_KEY = "galaxy-theme";
const SYSTEM_LIGHT_QUERY = "(prefers-color-scheme: light)"; const SYSTEM_LIGHT_QUERY = "(prefers-color-scheme: light)";
function readStoredChoice(): ThemeChoice { function readStoredChoice(): ThemeChoice {
if (typeof localStorage === "undefined") return "dark"; if (typeof localStorage === "undefined") return "system";
const value = localStorage.getItem(THEME_STORAGE_KEY); const value = localStorage.getItem(THEME_STORAGE_KEY);
return value === "light" || value === "dark" || value === "system" return value === "light" || value === "dark" || value === "system"
? value ? value
: "dark"; : "system";
} }
function systemTheme(): ResolvedTheme { function systemTheme(): ResolvedTheme {
+5 -4
View File
@@ -42,9 +42,10 @@ describe("theme store", () => {
vi.restoreAllMocks(); vi.restoreAllMocks();
}); });
it("defaults to dark and applies it to the document", async () => { it("defaults to system and applies the resolved theme", async () => {
const { theme } = await freshStore(); const { theme } = await freshStore();
expect(theme.choice).toBe("dark"); expect(theme.choice).toBe("system");
// freshStore() leaves the OS at dark (prefersLight = false).
expect(theme.resolved).toBe("dark"); expect(theme.resolved).toBe("dark");
expect(document.documentElement.dataset.theme).toBe("dark"); expect(document.documentElement.dataset.theme).toBe("dark");
}); });
@@ -80,9 +81,9 @@ describe("theme store", () => {
expect(document.documentElement.dataset.theme).toBe("light"); expect(document.documentElement.dataset.theme).toBe("light");
}); });
it("falls back to dark for an unrecognised stored value", async () => { it("falls back to system for an unrecognised stored value", async () => {
localStorage.setItem(STORAGE_KEY, "neon"); localStorage.setItem(STORAGE_KEY, "neon");
const { theme } = await freshStore(); const { theme } = await freshStore();
expect(theme.choice).toBe("dark"); expect(theme.choice).toBe("system");
}); });
}); });