diff --git a/src/app/(admin)/theme-toggle.tsx b/src/app/(admin)/theme-toggle.tsx index 3cc7845..b2cd155 100644 --- a/src/app/(admin)/theme-toggle.tsx +++ b/src/app/(admin)/theme-toggle.tsx @@ -1,5 +1,6 @@ "use client"; +import { useLayoutEffect } from "react"; import { Sun, Moon } from "lucide-react"; function getEffectiveTheme(): "light" | "dark" { @@ -12,6 +13,22 @@ function getEffectiveTheme(): "light" | "dark" { // 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 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 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;