82 lines
2.6 KiB
TypeScript
82 lines
2.6 KiB
TypeScript
"use client";
|
|
|
|
// Zentrales Theming (Hell/Dunkel) — SHL-01 Akzeptanzkriterium 6.
|
|
// Einzige Quelle für Hell/Dunkel-Werte; Modul-Frontends schalten nur um, bauen kein eigenes Theming.
|
|
|
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
import type { ReactNode } from "react";
|
|
import { colorTokens, cssVariables, type ColorScheme } from "../tokens/tokens";
|
|
|
|
const STORAGE_KEY = "nexarch-shl-theme";
|
|
|
|
interface ThemeContextValue {
|
|
scheme: ColorScheme;
|
|
setScheme: (scheme: ColorScheme) => void;
|
|
toggle: () => void;
|
|
}
|
|
|
|
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
|
|
function readStoredScheme(): ColorScheme | null {
|
|
if (typeof window === "undefined") return null;
|
|
try {
|
|
const stored = window.localStorage.getItem(STORAGE_KEY);
|
|
return stored === "light" || stored === "dark" ? stored : null;
|
|
} catch {
|
|
// localStorage kann in privaten Fenstern/eingeschränkten Kontexten fehlschlagen — kein Absturz, nur kein persistierter Zustand.
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function systemPrefersDark(): boolean {
|
|
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
}
|
|
|
|
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
const [scheme, setSchemeState] = useState<ColorScheme>("light");
|
|
|
|
useEffect(() => {
|
|
const stored = readStoredScheme();
|
|
setSchemeState(stored ?? (systemPrefersDark() ? "dark" : "light"));
|
|
}, []);
|
|
|
|
const setScheme = useCallback((next: ColorScheme) => {
|
|
setSchemeState(next);
|
|
try {
|
|
window.localStorage.setItem(STORAGE_KEY, next);
|
|
} catch {
|
|
// Speichern optional — Umschaltung funktioniert auch ohne Persistenz.
|
|
}
|
|
}, []);
|
|
|
|
const toggle = useCallback(() => {
|
|
setScheme(scheme === "light" ? "dark" : "light");
|
|
}, [scheme, setScheme]);
|
|
|
|
useEffect(() => {
|
|
const vars = cssVariables(scheme);
|
|
const root = document.documentElement;
|
|
for (const [key, value] of Object.entries(vars)) {
|
|
root.style.setProperty(key, value);
|
|
}
|
|
root.dataset.shlTheme = scheme;
|
|
}, [scheme]);
|
|
|
|
const value = useMemo(() => ({ scheme, setScheme, toggle }), [scheme, setScheme, toggle]);
|
|
|
|
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
|
}
|
|
|
|
export function useTheme(): ThemeContextValue {
|
|
const ctx = useContext(ThemeContext);
|
|
if (!ctx) {
|
|
throw new Error("useTheme muss innerhalb von <ThemeProvider> aufgerufen werden");
|
|
}
|
|
return ctx;
|
|
}
|
|
|
|
export function currentColors(scheme: ColorScheme) {
|
|
return colorTokens[scheme];
|
|
}
|