Adds a persistent dark/light theme toggle (localStorage + data-bs-theme on <html>) so it applies globally, including the circuit editor route which has no visible sidebar. An inline beforeInteractive script sets the theme before hydration to avoid a flash of the wrong theme. Bootstrap components pick up data-bs-theme automatically; the custom circuit-grid CSS previously used ~150 hardcoded hex colors, all converted to semantic tokens in globals.css with light/dark values. Verified with a headless-browser pass across the project list, project detail page, circuit editor, project-device drawer and a settings modal in both themes; no console errors. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
63 lines
1.8 KiB
TypeScript
63 lines
1.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
export const THEME_STORAGE_KEY = "leistungsbilanz:theme";
|
|
|
|
type Theme = "light" | "dark";
|
|
|
|
function applyTheme(theme: Theme) {
|
|
document.documentElement.setAttribute("data-bs-theme", theme);
|
|
}
|
|
|
|
export function ThemeToggle() {
|
|
const [theme, setTheme] = useState<Theme>("light");
|
|
|
|
// The inline script in layout.tsx already applied the persisted/system
|
|
// theme before hydration; read it back so the toggle starts in sync
|
|
// instead of flashing to "light" first.
|
|
useEffect(() => {
|
|
const current = document.documentElement.getAttribute("data-bs-theme");
|
|
setTheme(current === "dark" ? "dark" : "light");
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
function handleStorage(event: StorageEvent) {
|
|
if (
|
|
event.key === THEME_STORAGE_KEY &&
|
|
(event.newValue === "dark" || event.newValue === "light")
|
|
) {
|
|
setTheme(event.newValue);
|
|
applyTheme(event.newValue);
|
|
}
|
|
}
|
|
window.addEventListener("storage", handleStorage);
|
|
return () => window.removeEventListener("storage", handleStorage);
|
|
}, []);
|
|
|
|
function toggleTheme() {
|
|
const next: Theme = theme === "dark" ? "light" : "dark";
|
|
setTheme(next);
|
|
applyTheme(next);
|
|
try {
|
|
localStorage.setItem(THEME_STORAGE_KEY, next);
|
|
} catch {
|
|
// Private browsing or disabled storage: theme still applies for
|
|
// this page load, just without persistence across reloads.
|
|
}
|
|
}
|
|
|
|
return (
|
|
<button
|
|
aria-pressed={theme === "dark"}
|
|
className="sidebar-theme-toggle"
|
|
onClick={toggleTheme}
|
|
type="button"
|
|
>
|
|
<span className="sidebar-icon" aria-hidden="true">
|
|
{theme === "dark" ? "☀" : "☾"}
|
|
</span>
|
|
{theme === "dark" ? "Hellmodus" : "Dunkelmodus"}
|
|
</button>
|
|
);
|
|
}
|