Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | 7x 7x 66x 29x 29x 29x 29x | "use client";
import { useState } from "react";
import { useDeviceValue } from "@/lib/useDeviceValue";
type Theme = "light" | "dark";
// ThemeScript writes data-theme on <html> before paint, so the attribute — not this
// component — is the source of truth on arrival. It is device-local (invisible to the
// server render), hence the useDeviceValue read.
const readTheme = (): Theme =>
document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light";
// Flips data-theme on <html> and persists the choice. Reads the current value
// set by ThemeScript so it stays in sync on first paint.
export function ThemeToggle() {
const painted = useDeviceValue<Theme>(readTheme, "light");
// The flip is remembered here rather than re-read from the DOM: a failed
// localStorage write (private mode) must still leave the toggle in its new state.
const [flipped, setFlipped] = useState<Theme | null>(null);
const theme = flipped ?? painted;
function flip() {
const next: Theme = theme === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", next);
try {
localStorage.setItem("pi-theme", next);
} catch {
// private mode / storage disabled — the in-memory toggle still works
}
setFlipped(next);
}
return (
<button
type="button"
onClick={flip}
aria-label={`Switch to ${theme === "dark" ? "light" : "dark"} theme`}
className="inline-flex h-11 min-w-11 items-center justify-center rounded-control px-3 text-ink transition-colors hover:bg-content-soft"
>
<span aria-hidden="true">{theme === "dark" ? "☾" : "☀"}</span>
</button>
);
}
|