From e193f3ca888143c8e09a2b2531796a82f694d7ef Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Fri, 22 May 2026 08:36:17 +0200 Subject: [PATCH] feat(ui): default theme to system (follow OS light/dark) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- ui/PLAN-finalize.md | 6 +++--- ui/docs/design-system.md | 8 ++++---- ui/frontend/src/lib/theme/theme.svelte.ts | 4 ++-- ui/frontend/tests/theme.test.ts | 9 +++++---- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/ui/PLAN-finalize.md b/ui/PLAN-finalize.md index 251d640..2e1dfed 100644 --- a/ui/PLAN-finalize.md +++ b/ui/PLAN-finalize.md @@ -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 and pre-paint guard, and the whole UI migrated onto the tokens. Documented literal exceptions: the battle-scene data-viz palette, overlay -scrims, and directional/deliberate drop shadows. The default theme is -dark; flipping it to `system` is an available follow-up. Tokens and -conventions live in `ui/docs/design-system.md`. +scrims, and directional/deliberate drop shadows. The default theme +follows the OS (`system`); the account-menu picker pins light or dark. +Tokens and conventions live in `ui/docs/design-system.md`. Goal: replace the ad-hoc per-component styling (inline hex colors like `#0a0e1a`, one-off spacing) with a shared design language so every view diff --git a/ui/docs/design-system.md b/ui/docs/design-system.md index 81b090a..736c743 100644 --- a/ui/docs/design-system.md +++ b/ui/docs/design-system.md @@ -72,7 +72,8 @@ the colour block in `tokens.css`. persists the choice, applies `data-theme`, and — while the choice is `system` — follows OS theme changes via `matchMedia`. - 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 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 directional / deliberate drop shadows. -The default theme is **dark** while light coherence is being verified -across the migrated views; once the owner signs off on light, the -default can flip to `system`. +The default theme is **`system`** — it follows the OS light/dark +preference; users can pin light or dark via the account-menu picker. diff --git a/ui/frontend/src/lib/theme/theme.svelte.ts b/ui/frontend/src/lib/theme/theme.svelte.ts index 7ce5a26..f9598ef 100644 --- a/ui/frontend/src/lib/theme/theme.svelte.ts +++ b/ui/frontend/src/lib/theme/theme.svelte.ts @@ -24,11 +24,11 @@ export const THEME_STORAGE_KEY = "galaxy-theme"; const SYSTEM_LIGHT_QUERY = "(prefers-color-scheme: light)"; function readStoredChoice(): ThemeChoice { - if (typeof localStorage === "undefined") return "dark"; + if (typeof localStorage === "undefined") return "system"; const value = localStorage.getItem(THEME_STORAGE_KEY); return value === "light" || value === "dark" || value === "system" ? value - : "dark"; + : "system"; } function systemTheme(): ResolvedTheme { diff --git a/ui/frontend/tests/theme.test.ts b/ui/frontend/tests/theme.test.ts index d963fe0..4eafc21 100644 --- a/ui/frontend/tests/theme.test.ts +++ b/ui/frontend/tests/theme.test.ts @@ -42,9 +42,10 @@ describe("theme store", () => { 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(); - expect(theme.choice).toBe("dark"); + expect(theme.choice).toBe("system"); + // freshStore() leaves the OS at dark (prefersLight = false). expect(theme.resolved).toBe("dark"); expect(document.documentElement.dataset.theme).toBe("dark"); }); @@ -80,9 +81,9 @@ describe("theme store", () => { 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"); const { theme } = await freshStore(); - expect(theme.choice).toBe("dark"); + expect(theme.choice).toBe("system"); }); });