integratedhub/islands/ThemeHydration.tsx
2026-02-15 03:00:24 +00:00

72 lines
1.9 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 readTheme(): string {
try {
const stored = localStorage.getItem(THEME_KEY);
if (stored === "light" || stored === "dark") return stored;
} catch {
// ignore
}
return "dark";
}
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);
});
}
function persistTheme(mode: string): void {
try {
localStorage.setItem(THEME_KEY, 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;
applyTheme(root, readTheme());
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;
}