e8839b15a0
- Theme is now class-based (.dark on <html>) with a System/Light/Dark toggle in the sidebar, persisted to localStorage and applied pre-paint by an inline script (no flash). Replaces the prefers-color-scheme-only behavior, so a phone on a light OS theme can still choose dark and vice versa. - New brand-derived --line token (Ink at 55%): a dark line on the light paper, light on dark. The family-chart tree connectors had the library's default white stroke and were invisible in light mode — now they use --line, as do the pedigree brackets and the fan-chart sectors. - Light/dark tokens use the exact brand palette (Ink/Muted flip; Bronze/Paper constant). Frontend only — no migration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
55 lines
1.7 KiB
TypeScript
55 lines
1.7 KiB
TypeScript
"use client";
|
|
|
|
import { Monitor, Moon, Sun } from "lucide-react";
|
|
import { useEffect, useState } from "react";
|
|
|
|
type Theme = "light" | "dark" | "system";
|
|
const ORDER: Theme[] = ["system", "light", "dark"];
|
|
const ICON = { system: Monitor, light: Sun, dark: Moon };
|
|
const LABEL = { system: "System", light: "Light", dark: "Dark" };
|
|
|
|
function apply(theme: Theme) {
|
|
const dark =
|
|
theme === "dark" ||
|
|
(theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
|
document.documentElement.classList.toggle("dark", dark);
|
|
}
|
|
|
|
/** Cycles theme System → Light → Dark, persisting the choice in localStorage. */
|
|
export function ThemeToggle() {
|
|
const [theme, setTheme] = useState<Theme>("system");
|
|
|
|
useEffect(() => {
|
|
const saved = (localStorage.getItem("theme") as Theme) || "system";
|
|
setTheme(saved);
|
|
}, []);
|
|
|
|
// Keep "system" in sync if the OS preference changes while selected.
|
|
useEffect(() => {
|
|
if (theme !== "system") return;
|
|
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
const onChange = () => apply("system");
|
|
mq.addEventListener("change", onChange);
|
|
return () => mq.removeEventListener("change", onChange);
|
|
}, [theme]);
|
|
|
|
function cycle() {
|
|
const next = ORDER[(ORDER.indexOf(theme) + 1) % ORDER.length];
|
|
localStorage.setItem("theme", next);
|
|
setTheme(next);
|
|
apply(next);
|
|
}
|
|
|
|
const Icon = ICON[theme];
|
|
return (
|
|
<button
|
|
onClick={cycle}
|
|
className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm text-[var(--muted)] transition-colors hover:bg-bronze/[0.07] hover:text-[var(--foreground)]"
|
|
title={`Theme: ${LABEL[theme]} (click to change)`}
|
|
>
|
|
<Icon className="h-4 w-4 shrink-0" />
|
|
Theme: {LABEL[theme]}
|
|
</button>
|
|
);
|
|
}
|