// CRITICAL'use client'; import { useEffect } from 'react';import { Moon, Sun, Monitor } from 'lucide-react';import { motion, AnimatePresence } from 'framer-motion';import { useAppStore } from '@/store'; type Theme = 'light' | 'dark' | 'system'; export function ThemeToggle() { const theme = useAppStore((state) => state.themeMode) as Theme; const setThemeMode = useAppStore((state) => state.setThemeMode); const resolvedTheme = useAppStore((state) => state.resolvedTheme); const setResolvedTheme = useAppStore((state) => state.setResolvedTheme); const isOpen = useAppStore((state) => state.themeMenuOpen); const setThemeMenuOpen = useAppStore((state) => state.setThemeMenuOpen); useEffect(() => { // Load saved theme preference const saved = localStorage.getItem('theme') as Theme | null; if (saved) { setThemeMode(saved); } // Determine initial resolved theme const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; const initialTheme = saved === 'system' ? systemTheme : (saved || systemTheme); setResolvedTheme(initialTheme); applyTheme(initialTheme); }, [setResolvedTheme, setThemeMode]); useEffect(() => { // Listen for system theme changes const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const handleChange = (e: MediaQueryListEvent) => { if (theme === 'system') { const newTheme = e.matches ? 'dark' : 'light'; setResolvedTheme(newTheme); applyTheme(newTheme); } }; mediaQuery.addEventListener('change', handleChange); return () => mediaQuery.removeEventListener('change', handleChange); }, [setResolvedTheme, theme]); const applyTheme = (newTheme: 'light' | 'dark') => { const root = document.documentElement; if (newTheme === 'dark') { root.style.setProperty('--background', '#1a1a1a'); root.style.setProperty('--foreground', '#ededed'); root.style.setProperty('--card', '#242424'); root.style.setProperty('--card-foreground', '#ededed'); root.style.setProperty('--popover', '#242424'); root.style.setProperty('--popover-foreground', '#ededed'); root.style.setProperty('--primary', '#f3f4f6'); root.style.setProperty('--primary-foreground', '#1a1a1a'); root.style.setProperty('--secondary', '#2a2a2a'); root.style.setProperty('--secondary-foreground', '#ededed'); root.style.setProperty('--muted', '#2a2a2a'); root.style.setProperty('--muted-foreground', '#d4d4d4'); root.style.setProperty('--accent', '#2a2a2a'); root.style.setProperty('--accent-foreground', '#f3f4f6'); root.style.setProperty('--destructive', '#7f1d1d'); root.style.setProperty('--destructive-foreground', '#f3f4f6'); root.style.setProperty('--border', '#2a2a2a'); root.style.setProperty('--input', '#2a2a2a'); root.style.setProperty('--ring', '#52525b'); root.style.setProperty('--success', '#22c55e'); root.style.setProperty('--warning', '#eab308'); } else { root.style.setProperty('--background', '#ffffff'); root.style.setProperty('--foreground', '#171717'); root.style.setProperty('--card', '#f5f5f5'); root.style.setProperty('--card-foreground', '#171717'); root.style.setProperty('--popover', '#ffffff'); root.style.setProperty('--popover-foreground', '#171717'); root.style.setProperty('--primary', '#171717'); root.style.setProperty('--primary-foreground', '#fafafa'); root.style.setProperty('--secondary', '#e5e5e5'); root.style.setProperty('--secondary-foreground', '#171717'); root.style.setProperty('--muted', '#f5f5f5'); root.style.setProperty('--muted-foreground', '#737373'); root.style.setProperty('--accent', '#f5f5f5'); root.style.setProperty('--accent-foreground', '#171717'); root.style.setProperty('--destructive', '#dc2626'); root.style.setProperty('--destructive-foreground', '#fafafa'); root.style.setProperty('--border', '#e5e5e5'); root.style.setProperty('--input', '#e5e5e5'); root.style.setProperty('--ring', '#d4d4d8'); root.style.setProperty('--success', '#16a34a'); root.style.setProperty('--warning', '#ca8a04'); } }; const changeTheme = (newTheme: Theme) => { setThemeMode(newTheme); localStorage.setItem('theme', newTheme); setThemeMenuOpen(false); const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; const newResolvedTheme = newTheme === 'system' ? systemTheme : newTheme; setResolvedTheme(newResolvedTheme); applyTheme(newResolvedTheme); }; const themes: { value: Theme; icon: typeof Sun; label: string }[] = [ { value: 'light', icon: Sun, label: 'Light' }, { value: 'dark', icon: Moon, label: 'Dark' }, { value: 'system', icon: Monitor, label: 'System' }, ]; const CurrentIcon = theme === 'light' ? Sun : theme === 'dark' ? Moon : Monitor; return ( <div className="relative"> <motion.button whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} onClick={() => setThemeMenuOpen(!isOpen)} className="p-2 rounded-lg hover:bg-[var(--accent)] transition-colors text-[#b0a8a0] hover:text-[var(--foreground)]" title="Toggle theme" > <motion.div key={resolvedTheme} initial={{ rotate: -180, opacity: 0 }} animate={{ rotate: 0, opacity: 1 }} exit={{ rotate: 180, opacity: 0 }} transition={{ duration: 0.3, type: 'spring', stiffness: 200, damping: 20 }} > <CurrentIcon className="h-5 w-5" /> </motion.div> </motion.button> <AnimatePresence> {isOpen && ( <> {/* Backdrop */} <div className="fixed inset-0 z-40" onClick={() => setThemeMenuOpen(false)} /> {/* Menu */} <motion.div initial={{ opacity: 0, scale: 0.95, y: -10 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.95, y: -10 }} transition={{ duration: 0.15 }} className="absolute right-0 top-full mt-2 w-40 bg-[var(--card)] border border-[var(--border)] rounded-xl shadow-xl z-50 overflow-hidden" > <div className="p-1.5"> {themes.map(({ value, icon: Icon, label }) => ( <motion.button key={value} whileHover={{ backgroundColor: 'var(--accent)' }} whileTap={{ scale: 0.98 }} onClick={() => changeTheme(value)} className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors ${ theme === value ? 'bg-[var(--accent)] text-[var(--primary)]' : 'text-[#b0a8a0] hover:text-[var(--foreground)]' }`} > <Icon className="h-4 w-4" /> <span>{label}</span> {theme === value && ( <motion.div layoutId="activeTheme" className="ml-auto w-1.5 h-1.5 rounded-full bg-[var(--primary)]" transition={{ type: 'spring', stiffness: 500, damping: 30 }} /> )} </motion.button> ))} </div> </motion.div> </> )} </AnimatePresence> </div> );}
// CRITICAL
'use client';
import { useEffect } from 'react';
import { Moon, Sun, Monitor } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { useAppStore } from '@/store';
type Theme = 'light' | 'dark' | 'system';
export function ThemeToggle() {
const theme = useAppStore((state) => state.themeMode) as Theme;
const setThemeMode = useAppStore((state) => state.setThemeMode);
const resolvedTheme = useAppStore((state) => state.resolvedTheme);
const setResolvedTheme = useAppStore((state) => state.setResolvedTheme);
const isOpen = useAppStore((state) => state.themeMenuOpen);
const setThemeMenuOpen = useAppStore((state) => state.setThemeMenuOpen);
useEffect(() => {
// Load saved theme preference
const saved = localStorage.getItem('theme') as Theme | null;
if (saved) {
setThemeMode(saved);
}
// Determine initial resolved theme
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
const initialTheme = saved === 'system' ? systemTheme : (saved || systemTheme);
setResolvedTheme(initialTheme);
applyTheme(initialTheme);
}, [setResolvedTheme, setThemeMode]);
useEffect(() => {
// Listen for system theme changes
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
if (theme === 'system') {
const newTheme = e.matches ? 'dark' : 'light';
setResolvedTheme(newTheme);
applyTheme(newTheme);
}
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, [setResolvedTheme, theme]);
const applyTheme = (newTheme: 'light' | 'dark') => {
const root = document.documentElement;
if (newTheme === 'dark') {
root.style.setProperty('--background', '#1a1a1a');
root.style.setProperty('--foreground', '#ededed');
root.style.setProperty('--card', '#242424');
root.style.setProperty('--card-foreground', '#ededed');
root.style.setProperty('--popover', '#242424');
root.style.setProperty('--popover-foreground', '#ededed');
root.style.setProperty('--primary', '#f3f4f6');
root.style.setProperty('--primary-foreground', '#1a1a1a');
root.style.setProperty('--secondary', '#2a2a2a');
root.style.setProperty('--secondary-foreground', '#ededed');
root.style.setProperty('--muted', '#2a2a2a');
root.style.setProperty('--muted-foreground', '#d4d4d4');
root.style.setProperty('--accent', '#2a2a2a');
root.style.setProperty('--accent-foreground', '#f3f4f6');
root.style.setProperty('--destructive', '#7f1d1d');
root.style.setProperty('--destructive-foreground', '#f3f4f6');
root.style.setProperty('--border', '#2a2a2a');
root.style.setProperty('--input', '#2a2a2a');
root.style.setProperty('--ring', '#52525b');
root.style.setProperty('--success', '#22c55e');
root.style.setProperty('--warning', '#eab308');
} else {
root.style.setProperty('--background', '#ffffff');
root.style.setProperty('--foreground', '#171717');
root.style.setProperty('--card', '#f5f5f5');
root.style.setProperty('--card-foreground', '#171717');
root.style.setProperty('--popover', '#ffffff');
root.style.setProperty('--popover-foreground', '#171717');
root.style.setProperty('--primary', '#171717');
root.style.setProperty('--primary-foreground', '#fafafa');
root.style.setProperty('--secondary', '#e5e5e5');
root.style.setProperty('--secondary-foreground', '#171717');
root.style.setProperty('--muted', '#f5f5f5');
root.style.setProperty('--muted-foreground', '#737373');
root.style.setProperty('--accent', '#f5f5f5');
root.style.setProperty('--accent-foreground', '#171717');
root.style.setProperty('--destructive', '#dc2626');
root.style.setProperty('--destructive-foreground', '#fafafa');
root.style.setProperty('--border', '#e5e5e5');
root.style.setProperty('--input', '#e5e5e5');
root.style.setProperty('--ring', '#d4d4d8');
root.style.setProperty('--success', '#16a34a');
root.style.setProperty('--warning', '#ca8a04');
}
};
const changeTheme = (newTheme: Theme) => {
setThemeMode(newTheme);
localStorage.setItem('theme', newTheme);
setThemeMenuOpen(false);
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
const newResolvedTheme = newTheme === 'system' ? systemTheme : newTheme;
setResolvedTheme(newResolvedTheme);
applyTheme(newResolvedTheme);
};
const themes: { value: Theme; icon: typeof Sun; label: string }[] = [
{ value: 'light', icon: Sun, label: 'Light' },
{ value: 'dark', icon: Moon, label: 'Dark' },
{ value: 'system', icon: Monitor, label: 'System' },
];
const CurrentIcon = theme === 'light' ? Sun : theme === 'dark' ? Moon : Monitor;
return (
<div className="relative">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setThemeMenuOpen(!isOpen)}
className="p-2 rounded-lg hover:bg-[var(--accent)] transition-colors text-[#b0a8a0] hover:text-[var(--foreground)]"
title="Toggle theme"
>
<motion.div
key={resolvedTheme}
initial={{ rotate: -180, opacity: 0 }}
animate={{ rotate: 0, opacity: 1 }}
exit={{ rotate: 180, opacity: 0 }}
transition={{ duration: 0.3, type: 'spring', stiffness: 200, damping: 20 }}
>
<CurrentIcon className="h-5 w-5" />
</motion.div>
</motion.button>
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-40"
onClick={() => setThemeMenuOpen(false)}
/>
{/* Menu */}
<motion.div
initial={{ opacity: 0, scale: 0.95, y: -10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: -10 }}
transition={{ duration: 0.15 }}
className="absolute right-0 top-full mt-2 w-40 bg-[var(--card)] border border-[var(--border)] rounded-xl shadow-xl z-50 overflow-hidden"
>
<div className="p-1.5">
{themes.map(({ value, icon: Icon, label }) => (
<motion.button
key={value}
whileHover={{ backgroundColor: 'var(--accent)' }}
whileTap={{ scale: 0.98 }}
onClick={() => changeTheme(value)}
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors ${
theme === value
? 'bg-[var(--accent)] text-[var(--primary)]'
: 'text-[#b0a8a0] hover:text-[var(--foreground)]'
}`}
>
<Icon className="h-4 w-4" />
<span>{label}</span>
{theme === value && (
<motion.div
layoutId="activeTheme"
className="ml-auto w-1.5 h-1.5 rounded-full bg-[var(--primary)]"
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
/>
)}
</motion.button>
))}
</div>
</motion.div>
</>
)}
</AnimatePresence>
</div>
);
}