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>
65 lines
2.5 KiB
CSS
65 lines
2.5 KiB
CSS
@import 'tailwindcss/base';
|
|
@import 'tailwindcss/components';
|
|
@import 'tailwindcss/utilities';
|
|
|
|
/* Inter font */
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
|
|
|
/* Design tokens — light mode (default).
|
|
*
|
|
* Surface hierarchy (stacked sheets of paper):
|
|
* background → surface-container-low → surface → surface-raised (cards pop up)
|
|
*
|
|
* IMPORTANT (repo-wide gotcha): values are RGB triplets so they can be
|
|
* consumed via rgb(var(--token) / <alpha>). Do NOT switch to hsl() — the
|
|
* first triplet number would be interpreted as a hue degree and break the
|
|
* palette (51 65 85 → light yellow instead of slate).
|
|
*
|
|
* Fase 9.1 splits :root into :root and :root[data-theme=light] so the
|
|
* inline anti-FOUC script in app.html can flip the attribute synchronously
|
|
* (light is the fallback when no attribute is set, dark is opt-in).
|
|
*/
|
|
:root,
|
|
:root[data-theme='light'] {
|
|
--background: 247 249 251; /* #f7f9fb — spec exact value */
|
|
--surface-container-low: 241 245 249; /* slate-100 — subtle grouping bg */
|
|
--surface: 255 255 255; /* white — base card surface */
|
|
--surface-raised: 248 250 252; /* slate-50 — popped card (sidebar) */
|
|
--text-primary: 15 23 42; /* slate-900 — titles only */
|
|
--text-secondary: 100 116 139; /* slate-500 — meta / labels */
|
|
--text-muted: 148 163 184; /* slate-400 */
|
|
--destructive: 185 28 28; /* red-700 ≈ #ba1a1a spec */
|
|
}
|
|
|
|
/* Design tokens — dark mode.
|
|
* Contrast on text-primary against background = 14.7:1 (slate-50 on
|
|
* slate-950) — well above WCAG AAA 7:1. text-secondary on background =
|
|
* 5.8:1, above AA 4.5:1. */
|
|
:root[data-theme='dark'] {
|
|
--background: 2 6 23; /* #020617 slate-950 — spec exact value */
|
|
--surface-container-low: 15 23 42; /* slate-900 — subtle grouping */
|
|
--surface: 30 41 59; /* slate-800 — base card surface */
|
|
--surface-raised: 51 65 85; /* slate-700 — popped card (sidebar) */
|
|
--text-primary: 248 250 252; /* slate-50 */
|
|
--text-secondary: 148 163 184; /* slate-400 */
|
|
--text-muted: 100 116 139; /* slate-500 */
|
|
--destructive: 239 68 68; /* red-500 — more visible on dark bg */
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html,
|
|
body {
|
|
height: 100%;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Inter', system-ui, sans-serif;
|
|
background-color: rgb(var(--background));
|
|
color: rgb(var(--text-primary));
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|