Files
top-reservation/src/app/(admin)/theme-toggle.tsx
T

45 lines
1.9 KiB
TypeScript

"use client";
import { useLayoutEffect } from "react";
import { Sun, Moon } from "lucide-react";
function getEffectiveTheme(): "light" | "dark" {
const stored = document.documentElement.dataset.theme;
if (stored === "light" || stored === "dark") return stored;
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
// Both icons always render; CSS (mirroring the same data-theme cascade the
// palette itself uses) decides which one shows — avoids a useState/useEffect
// pair just to reflect the DOM attribute the no-flash script already set.
export function ThemeToggle() {
// The <head> no-flash script sets data-theme before hydration, but
// something in this app's streaming SSR/hydration commits again shortly
// after first paint and strips it back off (confirmed: present at ~0ms,
// gone by ~50ms, and it doesn't come back on its own) — suppressHydration
// Warning on <html> only silences the mismatch warning for the *first*
// hydration diff, it doesn't stop a later commit from resetting an
// attribute React doesn't know about. Re-applying here, once this client
// component has mounted (i.e. after that reset has already happened),
// wins the race and makes it stick.
useLayoutEffect(() => {
const stored = localStorage.getItem("theme");
if (stored === "light" || stored === "dark") {
document.documentElement.dataset.theme = stored;
}
}, []);
function toggle() {
const next = getEffectiveTheme() === "dark" ? "light" : "dark";
document.documentElement.dataset.theme = next;
localStorage.setItem("theme", next);
}
return (
<button onClick={toggle} className="btn btn-ghost theme-toggle-btn" title="Переключить тему" aria-label="Переключить тему">
<Sun size={15} className="theme-icon-sun" />
<Moon size={15} className="theme-icon-moon" />
</button>
);
}