/** * Valeurs persistées pour User.theme / localStorage `theme-preference`. * Une seule source de vérité pour le DOM : évite les écarts entre header, settings et hydratation. */ const THEME_IDS = [ 'light', 'dark', 'auto', 'sepia', 'midnight', 'rose', 'green', 'lavender', 'sand', 'ocean', 'sunset', 'blue', ] as const export type ThemeId = (typeof THEME_IDS)[number] const NAMED_PALETTE_IDS: ThemeId[] = [ 'sepia', 'midnight', 'rose', 'green', 'lavender', 'sand', 'ocean', 'sunset', 'blue', ] export function isThemeId(value: string | null | undefined): value is ThemeId { return value !== undefined && value !== null && (THEME_IDS as readonly string[]).includes(value) } /** Corrige les anciennes valeurs (ex. formulaire « slate ») vers un thème supporté. */ export function normalizeThemeId(raw: string | null | undefined): ThemeId { if (!raw) return 'light' if (raw === 'slate') return 'light' if (isThemeId(raw)) return raw return 'light' } /** Cookie lu par le layout serveur pour éviter le flash clair au changement de route. */ export const THEME_COOKIE_NAME = 'memento-theme' /** * Applique le thème sur `` : `class="dark"` et/ou `data-theme`. * - `light` → papier par défaut (`:root`), pas de `data-theme` * - `dark` → sombre global (`.dark`) * - `auto` → `.dark` si prefers-color-scheme: dark * - palettes nommées → `data-theme=""` ; `midnight` force aussi `.dark` (variante sombre du thème) * * Important : on n’enlève JAMAIS `dark` avant de le remettre (sinon flash clair * d’une frame à chaque navigation / re-render de ThemeInitializer). */ export function applyDocumentTheme(theme: string): void { if (typeof document === 'undefined') return const t = normalizeThemeId(theme) const root = document.documentElement const wantsDark = t === 'dark' || t === 'midnight' || (t === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) // Classe dark : bascule atomique (pas de remove puis add) if (wantsDark) { root.classList.add('dark') } else { root.classList.remove('dark') } try { root.style.colorScheme = wantsDark ? 'dark' : 'light' } catch { /* ignore */ } // data-theme pour les palettes nommées uniquement if (t === 'auto' || t === 'dark' || t === 'light') { root.removeAttribute('data-theme') } else if ((NAMED_PALETTE_IDS as readonly string[]).includes(t)) { root.setAttribute('data-theme', t) } else { root.removeAttribute('data-theme') } root.dataset.appliedTheme = t } /** Persiste le thème (localStorage + cookie) pour que le serveur connaisse le dark. */ export function persistThemePreference(theme: string): void { const t = normalizeThemeId(theme) try { localStorage.setItem('theme-preference', t) } catch { /* ignore */ } try { // 1 an — lu par le layout racine via cookies() document.cookie = `${THEME_COOKIE_NAME}=${encodeURIComponent(t)}; path=/; max-age=31536000; SameSite=Lax` } catch { /* ignore */ } applyDocumentTheme(t) }