54 lines
2 KiB
TypeScript
54 lines
2 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { Moon, Sun } from "lucide-react";
|
||
|
|
import { useSyncExternalStore } from "react";
|
||
|
|
|
||
|
|
const STORAGE_KEY = "plan-my-meals:theme";
|
||
|
|
const EVENT_NAME = "plan-my-meals:theme-change";
|
||
|
|
|
||
|
|
type ResolvedTheme = "light" | "dark";
|
||
|
|
|
||
|
|
function getSnapshot(): ResolvedTheme {
|
||
|
|
const explicit = document.documentElement.dataset.theme;
|
||
|
|
if (explicit === "light" || explicit === "dark") return explicit;
|
||
|
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||
|
|
}
|
||
|
|
|
||
|
|
function subscribe(callback: () => void): () => void {
|
||
|
|
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||
|
|
mq.addEventListener("change", callback);
|
||
|
|
window.addEventListener(EVENT_NAME, callback);
|
||
|
|
return () => {
|
||
|
|
mq.removeEventListener("change", callback);
|
||
|
|
window.removeEventListener(EVENT_NAME, callback);
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export function ThemeToggle() {
|
||
|
|
// Server snapshot returns null so hydration never mismatches; button renders a stable shell first.
|
||
|
|
const theme = useSyncExternalStore<ResolvedTheme | null>(subscribe, getSnapshot, () => null);
|
||
|
|
|
||
|
|
function toggleTheme() {
|
||
|
|
const next: ResolvedTheme = (theme ?? getSnapshot()) === "dark" ? "light" : "dark";
|
||
|
|
document.documentElement.dataset.theme = next;
|
||
|
|
try {
|
||
|
|
window.localStorage.setItem(STORAGE_KEY, next);
|
||
|
|
} catch {
|
||
|
|
// Ignore blocked storage; dataset.theme still updates the active session.
|
||
|
|
}
|
||
|
|
window.dispatchEvent(new Event(EVENT_NAME));
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={toggleTheme}
|
||
|
|
aria-label={theme === "dark" ? "Switch to light paper theme" : "Switch to dark slate theme"}
|
||
|
|
title={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
|
||
|
|
className="grid size-10 shrink-0 place-items-center rounded-lg border border-line bg-surface text-ink-muted transition-colors hover:border-line-strong hover:bg-raised hover:text-ink"
|
||
|
|
>
|
||
|
|
{theme === "dark" ? <Sun className="size-4" strokeWidth={1.75} aria-hidden /> : <Moon className="size-4" strokeWidth={1.75} aria-hidden />}
|
||
|
|
</button>
|
||
|
|
);
|
||
|
|
}
|