80 lines
2.3 KiB
TypeScript
80 lines
2.3 KiB
TypeScript
import { useEffect } from "preact/hooks";
|
|
|
|
const THEME_KEY = "cyberhybrid-theme-preference";
|
|
|
|
function applyTheme(root: Element, mode: string): void {
|
|
root.setAttribute("data-theme-mode", mode);
|
|
}
|
|
|
|
function syncToggleLabels(root: Element): void {
|
|
const isDark = root.getAttribute("data-theme-mode") !== "light";
|
|
const label = isDark ? "Switch to light mode" : "Switch to dark mode";
|
|
const toggles = root.querySelectorAll("[data-theme-toggle]");
|
|
toggles.forEach((el) => {
|
|
el.setAttribute("aria-label", label);
|
|
el.setAttribute("title", label);
|
|
});
|
|
}
|
|
|
|
const THEME_COOKIE = "cyberhybrid-theme";
|
|
const COOKIE_MAX_AGE = 365 * 24 * 60 * 60; // 1 year
|
|
|
|
function setThemeCookie(mode: string): void {
|
|
try {
|
|
document.cookie =
|
|
`${THEME_COOKIE}=${mode}; path=/; max-age=${COOKIE_MAX_AGE}; SameSite=Lax`;
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
function persistTheme(mode: string): void {
|
|
try {
|
|
localStorage.setItem(THEME_KEY, mode);
|
|
setThemeCookie(mode);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Client-only island that applies saved theme and wires up theme toggle clicks.
|
|
* Renders nothing; runs theme logic in useEffect to avoid dangerouslySetInnerHTML.
|
|
*/
|
|
export default function ThemeHydration() {
|
|
useEffect(() => {
|
|
const root = document.documentElement;
|
|
|
|
// Trust server-rendered theme (from cookie) on first paint; do not overwrite
|
|
// with localStorage to avoid flicker. Sync storage to match so next load is correct.
|
|
const serverTheme = root.getAttribute("data-theme-mode") === "light"
|
|
? "light"
|
|
: "dark";
|
|
persistTheme(serverTheme);
|
|
syncToggleLabels(root);
|
|
|
|
const handleClick = (event: Event) => {
|
|
const target = event.target as Node;
|
|
if (!target || !(target instanceof Element)) return;
|
|
const toggle = target.closest("[data-theme-toggle]");
|
|
if (!toggle) return;
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
const current = root.getAttribute("data-theme-mode") === "light"
|
|
? "light"
|
|
: "dark";
|
|
const next = current === "dark" ? "light" : "dark";
|
|
applyTheme(root, next);
|
|
persistTheme(next);
|
|
syncToggleLabels(root);
|
|
};
|
|
|
|
document.addEventListener("click", handleClick, true);
|
|
return () => document.removeEventListener("click", handleClick, true);
|
|
}, []);
|
|
|
|
return null;
|
|
}
|