feat(fase-9): ThemeToggle component + anti-FOUC bootstrap

Wires the dark-mode plumbing:

- New $lib/theme module: themePreference + resolvedTheme stores plus
  initTheme() / setThemePreference(). Owns localStorage persistence,
  <html data-theme=...> mirroring, and the matchMedia listener that
  re-resolves "system" when the OS preference flips. Storage-agnostic
  (no Supabase coupling) so the caller persists to public.users.theme
  separately.
- Inline anti-FOUC script in app.html reads localStorage + matchMedia
  synchronously and writes data-theme before the first paint — refresh
  in dark mode no longer flashes white.
- Tailwind switched to darkMode: ['selector', '[data-theme="dark"]']
  so all existing `dark:` utilities resolve from the same attribute.
- :root and :root[data-theme=light] share the light token set; dark
  tokens move into :root[data-theme=dark]. Tokens stay as RGB triplets
  (rgb(var(--token) / <alpha>)) per the repo gotcha — no hsl().
- ThemeToggle.svelte: three-way radiogroup (Light / Dark / System) with
  paraglide messages in EN + ES.

Tests (TDD): 8 new unit specs in src/lib/theme.test.ts cover defaults,
localStorage round-trip, data-theme reflection, system resolution
against matchMedia, invalid-value fallback, and OS preference change
propagation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 01:09:19 +02:00
parent 3efe0b9be0
commit 5873c2fa9f
8 changed files with 358 additions and 7 deletions

View File

@@ -183,5 +183,9 @@
"list_bulk_mark_checked": "Mark checked",
"list_undo_bulk_delete": "Deleted {n} items",
"list_move_title": "Move to list",
"list_move_create_new": "Create new list"
"list_move_create_new": "Create new list",
"settings_appearance": "Appearance",
"settings_theme_light": "Light",
"settings_theme_dark": "Dark",
"settings_theme_system": "System"
}