Files
explorer/src/components/ThemeDropdown.tsx

99 lines
3.9 KiB
TypeScript

"use client";
import { useEffect, useState, useCallback } from "react";
import * as Icon from "react-bootstrap-icons";
import { Nav, Dropdown } from "react-bootstrap";
type Theme = "light" | "dark" | "auto";
const COOKIE = "NBN-theme";
const getCookie = (name: string) => {
const m = document.cookie.match(new RegExp("(^|; )" + name + "=([^;]+)"));
return m ? decodeURIComponent(m[2]) : null;
};
const setCookie = (name: string, value: string) => {
document.cookie = `${name}=${encodeURIComponent(value)}; Path=/; Max-Age=31536000; SameSite=Lax; Domain=specnext.dev`;
};
const prefersDark = () =>
window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
export default function ThemeDropdown() {
const [mounted, setMounted] = useState(false);
const [theme, setTheme] = useState<Theme>("auto");
const apply = useCallback((t: Theme) => {
const effective = t === "auto" ? (prefersDark() ? "dark" : "light") : t;
document.documentElement.setAttribute("data-bs-theme", effective);
}, []);
useEffect(() => {
setMounted(true);
const v = getCookie(COOKIE);
console.log("Cookie:", v);
const initial: Theme = v === "light" || v === "dark" || v === "auto" ? (v as Theme) : "auto";
setTheme(initial);
// Important: apply immediately so “auto” reflects system after hydration
apply(initial);
}, [apply]);
useEffect(() => {
if (!mounted || theme !== "auto") return;
const mql = window.matchMedia("(prefers-color-scheme: dark)");
const onChange = () => apply("auto");
mql.addEventListener("change", onChange);
const onPageShow = () => apply("auto");
window.addEventListener("pageshow", onPageShow);
return () => {
mql.removeEventListener("change", onChange);
window.removeEventListener("pageshow", onPageShow);
};
}, [mounted, theme, apply]);
const choose = (t: Theme) => {
setCookie(COOKIE, t);
setTheme(t);
apply(t);
};
const isActive = (t: Theme) => theme === t;
const ToggleIcon = !mounted
? Icon.CircleHalf
: theme === "dark"
? Icon.MoonStarsFill
: theme === "light"
? Icon.SunFill
: Icon.CircleHalf;
return (
<Nav className="ms-md-auto">
<Dropdown as={Nav.Item} align="end">
<Dropdown.Toggle as={Nav.Link} id="bd-theme" className="px-0 px-lg-2 py-2 d-flex align-items-center">
<ToggleIcon />
<span className="d-lg-none ms-2" id="bd-theme-text">Toggle theme</span>
</Dropdown.Toggle>
<Dropdown.Menu aria-labelledby="bd-theme-text">
<Dropdown.Item as="button" className="d-flex align-items-center" active={isActive("light")} onClick={() => choose("light")}>
<Icon.SunFill />
<span className="ms-2">Light</span>
{isActive("light") && <Icon.Check2 className="ms-auto" aria-hidden="true" />}
</Dropdown.Item>
<Dropdown.Item as="button" className="d-flex align-items-center" active={isActive("dark")} onClick={() => choose("dark")}>
<Icon.MoonStarsFill />
<span className="ms-2">Dark</span>
{isActive("dark") && <Icon.Check2 className="ms-auto" aria-hidden="true" />}
</Dropdown.Item>
<Dropdown.Item as="button" className="d-flex align-items-center" active={isActive("auto")} onClick={() => choose("auto")}>
<Icon.CircleHalf />
<span className="ms-2">Auto</span>
{isActive("auto") && <Icon.Check2 className="ms-auto" aria-hidden="true" />}
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</Nav>
);
}