// Per-device theme preference. Mirrors the data-mode store pattern. // // Three options: // - 'system' follows `prefers-color-scheme` (default for new visitors) // - 'light' force light // - 'dark' force dark // // `appliedTheme` is the *resolved* light/dark for the current moment — derived from // `themePreference` + the OS preference when 'system' is selected. Consumers that // just want "is it dark right now?" should read `$appliedTheme`. // // The `applyTheme` side-effect toggles a `dark` class on the element so the // Tailwind v4 dark variant (configured in `tailwind-theme.css`) kicks in for every // `dark:` utility across the app. import { writable, derived, get } from 'svelte/store'; import { browser } from '$app/environment'; export type ThemePreference = 'system' | 'light' | 'dark'; export type AppliedTheme = 'light' | 'dark'; const KEY = 'eventsnap_theme'; const DEFAULT: ThemePreference = 'system'; function readInitial(): ThemePreference { if (!browser) return DEFAULT; const raw = localStorage.getItem(KEY); return raw === 'light' || raw === 'dark' || raw === 'system' ? raw : DEFAULT; } function systemPrefersDark(): boolean { if (!browser) return false; return window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false; } export const themePreference = writable(readInitial()); /** Resolved light/dark — recomputed when the preference or OS theme changes. */ export const appliedTheme = derived(themePreference, ($pref, set) => { const compute = () => set($pref === 'system' ? (systemPrefersDark() ? 'dark' : 'light') : $pref); compute(); if (!browser || $pref !== 'system') return; // no OS listener needed when forced const mq = window.matchMedia('(prefers-color-scheme: dark)'); const listener = () => compute(); mq.addEventListener('change', listener); return () => mq.removeEventListener('change', listener); }); /** Side-effect: keep the class + localStorage + meta-color in sync. */ export function initTheme(): void { if (!browser) return; themePreference.subscribe((pref) => { try { localStorage.setItem(KEY, pref); } catch { // localStorage may be unavailable (Safari private mode); ignore. } }); appliedTheme.subscribe((mode) => { document.documentElement.classList.toggle('dark', mode === 'dark'); // Update the browser chrome / status bar color so iOS Safari + Android stop // painting it white on a dark page. const meta = document.querySelector('meta[name="theme-color"]'); if (meta) meta.setAttribute('content', mode === 'dark' ? '#111827' : '#ffffff'); }); } /** Convenience for one-off reads outside reactive contexts. */ export function isDark(): boolean { return get(appliedTheme) === 'dark'; }