Add theme toggle with light, dark, and system support

- **Header:** Integrate `ThemeToggle` component into the user menu area.
- **Theme Provider:** Introduce `ThemeProvider` context for managing and persisting theme preferences.
- **New Components:** Add `ThemeToggle` for switching themes and `ThemeProvider` to handle state and system preferences.
- Ensure responsive updates and localStorage persistence for user-selected themes.
This commit is contained in:
2025-11-02 06:53:46 +01:00
parent 30f0ec5a64
commit af260e4748
4 changed files with 144 additions and 2 deletions

View File

@@ -22,6 +22,7 @@ import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Settings, LogOut, User, Shield } from 'lucide-react';
import { cn } from '@/lib/utils';
import { ThemeToggle } from '@/components/theme';
/**
* Get user initials for avatar
@@ -97,8 +98,9 @@ export function Header() {
</nav>
</div>
{/* Right side - User menu */}
<div className="ml-auto flex items-center space-x-4">
{/* Right side - Theme toggle and user menu */}
<div className="ml-auto flex items-center space-x-2">
<ThemeToggle />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-10 w-10 rounded-full">

View File

@@ -0,0 +1,83 @@
/**
* Theme Provider
* Manages light/dark mode with persistence
*/
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
type Theme = 'light' | 'dark' | 'system';
interface ThemeContextValue {
theme: Theme;
setTheme: (theme: Theme) => void;
resolvedTheme: 'light' | 'dark';
}
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setThemeState] = useState<Theme>('system');
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light');
// Initialize theme from localStorage on mount
useEffect(() => {
const stored = localStorage.getItem('theme') as Theme | null;
if (stored && ['light', 'dark', 'system'].includes(stored)) {
setThemeState(stored);
}
}, []);
// Apply theme to document and resolve system preference
useEffect(() => {
const root = window.document.documentElement;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const applyTheme = () => {
let effectiveTheme: 'light' | 'dark';
if (theme === 'system') {
effectiveTheme = mediaQuery.matches ? 'dark' : 'light';
} else {
effectiveTheme = theme;
}
setResolvedTheme(effectiveTheme);
root.classList.remove('light', 'dark');
root.classList.add(effectiveTheme);
};
applyTheme();
// Listen for system theme changes
const handleChange = () => {
if (theme === 'system') {
applyTheme();
}
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, [theme]);
const setTheme = (newTheme: Theme) => {
setThemeState(newTheme);
localStorage.setItem('theme', newTheme);
};
return (
<ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}

View File

@@ -0,0 +1,51 @@
/**
* Theme Toggle Button
* Switches between light, dark, and system themes
*/
'use client';
import { Moon, Sun, Monitor } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useTheme } from './ThemeProvider';
export function ThemeToggle() {
const { theme, setTheme, resolvedTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" aria-label="Toggle theme">
{resolvedTheme === 'dark' ? (
<Moon className="h-5 w-5" />
) : (
<Sun className="h-5 w-5" />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme('light')}>
<Sun className="mr-2 h-4 w-4" />
Light
{theme === 'light' && <span className="ml-auto"></span>}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
<Moon className="mr-2 h-4 w-4" />
Dark
{theme === 'dark' && <span className="ml-auto"></span>}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
<Monitor className="mr-2 h-4 w-4" />
System
{theme === 'system' && <span className="ml-auto"></span>}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,6 @@
/**
* Theme components
*/
export { ThemeProvider, useTheme } from './ThemeProvider';
export { ThemeToggle } from './ThemeToggle';