[FE-COMP-020] fe-comp: Add dark mode support

This commit is contained in:
senke 2025-12-25 12:13:29 +01:00
parent 928927adaf
commit 9a55ea5360
4 changed files with 75 additions and 17 deletions

View file

@ -7821,8 +7821,12 @@
"description": "Add theme switching and dark mode styles",
"owner": "frontend",
"estimated_hours": 6,
"status": "todo",
"files_involved": [],
"status": "completed",
"files_involved": [
"apps/web/src/stores/ui.ts",
"apps/web/src/app/App.tsx",
"apps/web/src/index.css"
],
"implementation_steps": [
{
"step": 1,
@ -7842,7 +7846,9 @@
"Unit tests",
"Integration tests"
],
"notes": ""
"notes": "",
"completed_at": "2025-12-25T15:15:00.000Z",
"implementation_notes": "Enhanced dark mode support across the application. Improvements: Improved theme application logic in UI store with proper class toggle (add/remove instead of toggle), added system preference change listener in App.tsx to automatically update theme when system preference changes in 'system' mode, enhanced CSS with explicit .dark class support alongside media query fallback, proper theme initialization on app load, and theme persistence via Zustand persist middleware. The dark mode now supports three modes: light, dark, and system (auto-detect from OS preference). The Header component already had a theme toggle button, and the settings page has theme selection. All components use Tailwind's dark: variant for dark mode styles, ensuring consistent theming across the application."
},
{
"id": "FE-COMP-021",

View file

@ -10,7 +10,7 @@ import { csrfService } from '@/services/csrf';
export function App() {
const { refreshUser } = useAuthStore();
const { setTheme } = useUIStore();
const { theme, setTheme } = useUIStore();
// Initialiser l'application
useEffect(() => {
@ -31,19 +31,40 @@ export function App() {
};
checkAndFetchCSRF();
// Appliquer le thème au chargement
const savedTheme = localStorage.getItem('ui-storage');
if (savedTheme) {
try {
const parsed = JSON.parse(savedTheme);
if (parsed.state?.theme) {
setTheme(parsed.state.theme);
}
} catch (error) {
console.error('Error parsing theme from localStorage:', error);
// Appliquer le thème au chargement (le store persist le fait déjà, mais on s'assure qu'il est appliqué)
setTheme(theme);
}, [refreshUser, setTheme, theme]);
// Écouter les changements de préférence système pour le mode 'system'
useEffect(() => {
if (theme !== 'system') return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
const root = document.documentElement;
if (e.matches) {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
};
// Écouter les changements
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handleChange);
} else {
// Fallback pour les navigateurs plus anciens
mediaQuery.addListener(handleChange);
}
}, [refreshUser, setTheme]);
return () => {
if (mediaQuery.removeEventListener) {
mediaQuery.removeEventListener('change', handleChange);
} else {
mediaQuery.removeListener(handleChange);
}
};
}, [theme]);
return (
<ErrorBoundary>

View file

@ -50,6 +50,29 @@
);
}
/* Dark mode theme variables */
.dark {
--color-background: #0a0a0a;
--color-foreground: #fafafa;
--color-card: #121212;
--color-card-foreground: #fafafa;
--color-popover: #121212;
--color-popover-foreground: #fafafa;
--color-primary: #8b9aff;
--color-primary-foreground: #0a0a0a;
--color-secondary: #1e293b;
--color-secondary-foreground: #fafafa;
--color-muted: #1e293b;
--color-muted-foreground: #94a3b8;
--color-accent: #1e293b;
--color-accent-foreground: #fafafa;
--color-destructive: #dc2626;
--color-destructive-foreground: #fafafa;
--color-border: #334155;
--color-input: #334155;
--color-ring: #8b9aff;
}
@media (prefers-color-scheme: dark) {
@theme {
--color-background: #0a0a0a;

View file

@ -28,13 +28,21 @@ export const useUIStore = create<UIState & UIActions>()(
set({ theme });
// Appliquer le thème au DOM
const root = document.documentElement;
const applyTheme = (isDark: boolean) => {
if (isDark) {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
};
if (theme === 'system') {
const prefersDark = window.matchMedia(
'(prefers-color-scheme: dark)',
).matches;
root.classList.toggle('dark', prefersDark);
applyTheme(prefersDark);
} else {
root.classList.toggle('dark', theme === 'dark');
applyTheme(theme === 'dark');
}
},