// Theme: dark (default) + light, persisted. When the user hasn't pinned a // theme we follow the OS via CSS `prefers-color-scheme`; pinning sets an // explicit `data-theme` on that overrides the media query. type Theme = "light" | "dark"; const KEY = "maestro-theme"; function stored(): Theme | null { const v = localStorage.getItem(KEY); return v === "light" || v === "dark" ? v : null; } function systemTheme(): Theme { return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark"; } /** The theme actually showing right now (pinned or system). */ export function resolvedTheme(): Theme { return stored() ?? systemTheme(); } /** Apply a pinned theme at boot (no-op when following the system). */ export function initTheme(): void { // Dev-only `?theme=light|dark` override for visual review; stripped in prod. if (import.meta.env.DEV) { const q = new URLSearchParams(location.search).get("theme"); if (q === "light" || q === "dark") { document.documentElement.dataset.theme = q; return; } } const pinned = stored(); if (pinned) document.documentElement.dataset.theme = pinned; } function setTheme(t: Theme): void { localStorage.setItem(KEY, t); document.documentElement.dataset.theme = t; } /** A header button that flips between light and dark. */ export function createThemeToggle(): HTMLButtonElement { const btn = document.createElement("button"); btn.type = "button"; btn.className = "btn btn-ghost btn-icon theme-toggle"; btn.innerHTML = ``; const sync = () => { const next = resolvedTheme() === "dark" ? "light" : "dark"; btn.setAttribute("aria-label", `Switch to ${next} theme`); btn.title = `Switch to ${next} theme`; }; sync(); btn.addEventListener("click", () => { setTheme(resolvedTheme() === "dark" ? "light" : "dark"); sync(); }); return btn; }