// 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;
}