Theme-Toggle: Dark/Light-Mode pro Nutzer (localStorage, System-Default, FOUC-Schutz)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { AuthProvider, useAuth } from './context/AuthContext';
|
||||
import { ThemeProvider } from './context/ThemeContext';
|
||||
import { ToastProvider, useToast } from './components/Toast';
|
||||
import { setGlobalErrorHandler } from './utils/api';
|
||||
import AuthModal from './components/AuthModal';
|
||||
@@ -139,11 +140,13 @@ function AppContent() {
|
||||
export default function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<ToastProvider>
|
||||
<AppContent />
|
||||
</ToastProvider>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { useTheme } from '../context/ThemeContext';
|
||||
|
||||
// Punkt 5: SVG Icons für jeden Tab
|
||||
const tabIcons = {
|
||||
@@ -22,6 +23,7 @@ const tabIcons = {
|
||||
|
||||
export default function Sidebar({ activeTab, onTabChange }) {
|
||||
const { user, logout } = useAuth();
|
||||
const { isDark, toggleTheme } = useTheme();
|
||||
// Punkt 19: Collapsible Sidebar State
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
@@ -101,6 +103,26 @@ export default function Sidebar({ activeTab, onTabChange }) {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /></svg>
|
||||
</button>
|
||||
)}
|
||||
{/* Theme-Toggle: Dark/Light-Mode, individuelle Einstellung pro Nutzer (localStorage) */}
|
||||
{!collapsed && (
|
||||
<button className="btn btn-ghost btn-sm w-full justify-start gap-2 mt-2" onClick={toggleTheme} title={isDark ? 'Zu hellem Design wechseln' : 'Zu dunklem Design wechseln'}>
|
||||
{isDark ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /></svg>
|
||||
)}
|
||||
<span>{isDark ? 'Helles Design' : 'Dunkles Design'}</span>
|
||||
</button>
|
||||
)}
|
||||
{collapsed && (
|
||||
<button className="btn btn-ghost btn-xs btn-square" onClick={toggleTheme} title={isDark ? 'Zu hellem Design wechseln' : 'Zu dunklem Design wechseln'}>
|
||||
{isDark ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /></svg>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
53
frontend/src/context/ThemeContext.jsx
Normal file
53
frontend/src/context/ThemeContext.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Theme-Context: Dark/Light-Mode-Umschaltung für daisyUI (data-theme auf <html>).
|
||||
* - Persistenz in localStorage ('workflow_theme': 'dark' | 'light')
|
||||
* - Ohne gespeicherte Wahl folgt das Theme der Systemeinstellung (prefers-color-scheme)
|
||||
* - FOUC-Schutz: initiales data-theme wird synchron vor dem ersten Render gesetzt
|
||||
*/
|
||||
|
||||
const STORAGE_KEY = 'workflow_theme';
|
||||
|
||||
// Synchron beim Modul-Load ausführen — verhindert einen Theme-Flash beim Start
|
||||
function resolveInitialTheme() {
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved === 'dark' || saved === 'light') return saved;
|
||||
} catch (e) { /* localStorage nicht verfügbar — Fallback unten */ }
|
||||
if (typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches) {
|
||||
return 'dark';
|
||||
}
|
||||
return 'light';
|
||||
}
|
||||
|
||||
// Direkt beim Laden das Theme setzen (vor React-Render)
|
||||
const initialTheme = resolveInitialTheme();
|
||||
document.documentElement.setAttribute('data-theme', initialTheme);
|
||||
|
||||
const ThemeContext = createContext(null);
|
||||
|
||||
export function ThemeProvider({ children }) {
|
||||
const [theme, setTheme] = useState(initialTheme);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, theme);
|
||||
} catch (e) { /* Speichern fehlgeschlagen — Theme bleibt für diese Session aktiv */ }
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => setTheme((t) => (t === 'dark' ? 'light' : 'dark'));
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme, isDark: theme === 'dark' }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const ctx = useContext(ThemeContext);
|
||||
if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
|
||||
return ctx;
|
||||
}
|
||||
Reference in New Issue
Block a user