From 73a57a1407f0b7037b82c26255665c54b9615870 Mon Sep 17 00:00:00 2001 From: "Anaz S. Aji" Date: Fri, 10 Jul 2026 14:45:23 +0700 Subject: [PATCH] refactor(web): conventional class-based dark mode + decoupled theme module - Bind dark: variant to .dark class via @custom-variant dark (&:is(.dark *)) in app.css. Previously dark: followed the OS media query while CSS vars followed a .light opt-in class, so manual light/dark toggling desynced the 21 dark: usages in badge/button/input/textarea from the variable swap. - Light is now the default; dark is opt-in via .dark on . Add color-scheme: light/dark so native UI (scrollbars, select, autofill) follows the theme. - Extract theme logic into src/lib/theme.ts (pure TS, SSR-safe, framework-agnostic): getStoredTheme, resolveTheme, applyTheme, setTheme, cycleTheme, watchSystemTheme (live OS change listener). ThemeToggle.svelte is now a thin UI shell over it. - Fix FOUC script in app.html: handle stored 'system' correctly (was treated as concrete dark -> dark->light flash for system + light-OS users). - Add 16 unit tests in theme.test.ts (vitest). localStorage key unchanged (backward compatible). --- web/src/app.css | 84 ++++++---- web/src/app.html | 18 +- web/src/lib/components/ThemeToggle.svelte | 59 +++---- web/src/lib/theme.test.ts | 192 ++++++++++++++++++++++ web/src/lib/theme.ts | 103 ++++++++++++ 5 files changed, 378 insertions(+), 78 deletions(-) create mode 100644 web/src/lib/theme.test.ts create mode 100644 web/src/lib/theme.ts diff --git a/web/src/app.css b/web/src/app.css index 51b5202..1e68269 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -1,26 +1,34 @@ @import 'tailwindcss'; +/* + * Bind the `dark:` utility variant to a `.dark` class on instead of the + * OS media query. This is the universal Tailwind v4 convention and keeps the + * variant in lockstep with the CSS-variable swap below (both react to the same + * class), so manual light/dark toggling never desyncs from `dark:` utilities. + */ +@custom-variant dark (&:is(.dark *)); + @theme inline { - /* Dark mode (default) */ - --color-background: oklch(0.145 0.015 265); - --color-foreground: oklch(0.985 0 0); - --color-card: oklch(0.18 0.015 265); - --color-card-foreground: oklch(0.985 0 0); - --color-popover: oklch(0.18 0.015 265); - --color-popover-foreground: oklch(0.985 0 0); - --color-primary: oklch(0.55 0.2 250); + /* Light mode = default token values */ + --color-background: oklch(0.985 0 0); + --color-foreground: oklch(0.145 0.015 265); + --color-card: oklch(0.985 0 0); + --color-card-foreground: oklch(0.145 0.015 265); + --color-popover: oklch(0.985 0 0); + --color-popover-foreground: oklch(0.145 0.015 265); + --color-primary: oklch(0.5 0.2 250); --color-primary-foreground: oklch(0.985 0 0); - --color-secondary: oklch(0.27 0.015 265); - --color-secondary-foreground: oklch(0.985 0 0); - --color-muted: oklch(0.27 0.015 265); - --color-muted-foreground: oklch(0.7 0.015 265); - --color-accent: oklch(0.27 0.015 265); - --color-accent-foreground: oklch(0.985 0 0); + --color-secondary: oklch(0.96 0.005 265); + --color-secondary-foreground: oklch(0.25 0.015 265); + --color-muted: oklch(0.96 0.005 265); + --color-muted-foreground: oklch(0.55 0.015 265); + --color-accent: oklch(0.96 0.005 265); + --color-accent-foreground: oklch(0.25 0.015 265); --color-destructive: oklch(0.55 0.22 25); --color-destructive-foreground: oklch(0.985 0 0); - --color-border: oklch(0.35 0.015 265); - --color-input: oklch(0.35 0.015 265); - --color-ring: oklch(0.6 0.15 250); + --color-border: oklch(0.87 0.005 265); + --color-input: oklch(0.87 0.005 265); + --color-ring: oklch(0.45 0.15 250); --radius-sm: 0.25rem; --radius-md: 0.375rem; --radius-lg: 0.5rem; @@ -38,26 +46,32 @@ font-family: system-ui, -apple-system, sans-serif; } - /* Light mode β€” applied via .light class on */ - :root.light { - --color-background: oklch(0.985 0 0); - --color-foreground: oklch(0.145 0.015 265); - --color-card: oklch(0.985 0 0); - --color-card-foreground: oklch(0.145 0.015 265); - --color-popover: oklch(0.985 0 0); - --color-popover-foreground: oklch(0.145 0.015 265); - --color-primary: oklch(0.5 0.2 250); + /* Native UI (scrollbars,