import { writable, get } from "svelte/store"; import { browser } from "$app/environment"; import { callXrpc } from "$hatk/client"; export type ThemePreference = "system" | "light" | "dark"; type ResolvedTheme = "light" | "dark"; // Also read by the inline script in app.html, which applies the theme before // first paint. Keep the two in sync. const STORAGE_KEY = "grain:theme"; const THEME_COLOR: Record = { dark: "#080b12", light: "#ffffff", }; /** What the user picked — "system" follows the OS. */ export const themePreference = writable(readStored()); function isThemePreference(value: unknown): value is ThemePreference { return value === "system" || value === "light" || value === "dark"; } function systemTheme(): ResolvedTheme { if (!browser) return "dark"; return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark"; } /** * Everything theme-dependent keys off `data-theme` on , in CSS — that way * server-rendered markup is already correct and nothing has to swap on hydration. */ function apply(preference: ThemePreference): void { if (!browser) return; const resolved = preference === "system" ? systemTheme() : preference; document.documentElement.dataset.theme = resolved; document .querySelector('meta[name="theme-color"]') ?.setAttribute("content", THEME_COLOR[resolved]); } function readStored(): ThemePreference { if (!browser) return "system"; try { const stored = localStorage.getItem(STORAGE_KEY); if (isThemePreference(stored)) return stored; } catch {} return "system"; } function store(preference: ThemePreference): void { if (!browser) return; try { localStorage.setItem(STORAGE_KEY, preference); } catch {} } /** * Sync the store with what the inline app.html script already applied, and keep * following the OS while the preference is "system". Call once from the root layout. */ export function initTheme(): () => void { if (!browser) return () => {}; const preference = readStored(); themePreference.set(preference); apply(preference); const media = window.matchMedia("(prefers-color-scheme: light)"); const onChange = () => { if (get(themePreference) === "system") apply("system"); }; media.addEventListener("change", onChange); return () => media.removeEventListener("change", onChange); } /** User picked a theme: apply it, remember it locally, and sync it to the account. */ export async function setTheme(preference: ThemePreference): Promise { if (get(themePreference) === preference) return; themePreference.set(preference); store(preference); apply(preference); try { await callXrpc("dev.hatk.putPreference", { key: "theme", value: preference }); } catch { // Logged-out or offline — localStorage still holds the choice. } } /** Apply a theme that came from the account's saved preferences (no write-back). */ export function applyStoredTheme(value: unknown): void { if (!isThemePreference(value) || !browser) return; if (get(themePreference) === value) return; themePreference.set(value); store(value); apply(value); }