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 React, { useState, useEffect } from 'react';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { AuthProvider, useAuth } from './context/AuthContext';
|
import { AuthProvider, useAuth } from './context/AuthContext';
|
||||||
|
import { ThemeProvider } from './context/ThemeContext';
|
||||||
import { ToastProvider, useToast } from './components/Toast';
|
import { ToastProvider, useToast } from './components/Toast';
|
||||||
import { setGlobalErrorHandler } from './utils/api';
|
import { setGlobalErrorHandler } from './utils/api';
|
||||||
import AuthModal from './components/AuthModal';
|
import AuthModal from './components/AuthModal';
|
||||||
@@ -139,11 +140,13 @@ function AppContent() {
|
|||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<ThemeProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<AppContent />
|
<AppContent />
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
</ThemeProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
import { useTheme } from '../context/ThemeContext';
|
||||||
|
|
||||||
// Punkt 5: SVG Icons für jeden Tab
|
// Punkt 5: SVG Icons für jeden Tab
|
||||||
const tabIcons = {
|
const tabIcons = {
|
||||||
@@ -22,6 +23,7 @@ const tabIcons = {
|
|||||||
|
|
||||||
export default function Sidebar({ activeTab, onTabChange }) {
|
export default function Sidebar({ activeTab, onTabChange }) {
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
|
const { isDark, toggleTheme } = useTheme();
|
||||||
// Punkt 19: Collapsible Sidebar State
|
// Punkt 19: Collapsible Sidebar State
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
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>
|
<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>
|
</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>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</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