Add dark mode toggle in the sidebar
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>
This commit is contained in:
parent
a99980c47b
commit
64ccd1f829
4 changed files with 395 additions and 127 deletions
63
src/frontend/components/ThemeToggle.tsx
Normal file
63
src/frontend/components/ThemeToggle.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue