planmymeal/app/components/ThemeToggle.tsx

54 lines
2 KiB
TypeScript
Raw Normal View History

2026-10-31 20:44:29 -05:00
"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>
);
}