Repository Public
1'use client';2import { useQueryClient } from '@tanstack/react-query';3import { Monitor, Moon, Sun } from 'lucide-react';4import { saveSettings } from '@/lib/queries';5import { useTheme, type ThemePref } from '@/stores/theme';6import { IconButton } from '@/components/ui/button';7import { cn } from '@/lib/cn';89/** Applies the theme locally at once and stores the choice on the server (when signed in). */10export function useSetTheme(): (pref: ThemePref) => void {11 const qc = useQueryClient();12 return (pref) => {13 useTheme.getState().setPref(pref);14 void saveSettings(qc, { theme: pref }).catch(() => undefined);15 };16}1718/** Sun/moon button: flips between light and dark. */19export function ThemeToggle({ className }: { className?: string }) {20 const theme = useTheme((s) => s.theme);21 const setTheme = useSetTheme();22 return (23 <IconButton label={theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'} onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} className={className} data-testid="theme-toggle">24 <span className="relative block size-4">25 <Sun className={cn('absolute inset-0 size-4 transition-all duration-300', theme === 'dark' ? 'rotate-90 scale-0 opacity-0' : 'rotate-0 scale-100 opacity-100')} />26 <Moon className={cn('absolute inset-0 size-4 transition-all duration-300', theme === 'dark' ? 'rotate-0 scale-100 opacity-100' : '-rotate-90 scale-0 opacity-0')} />27 </span>28 </IconButton>29 );30}3132const OPTIONS: { id: ThemePref; label: string; icon: typeof Sun }[] = [33 { id: 'light', label: 'Light', icon: Sun },34 { id: 'dark', label: 'Dark', icon: Moon },35 { id: 'system', label: 'System', icon: Monitor },36];37/** Three-way segmented control (Settings, phone "More" sheet). */38export function ThemeSegmented({ className }: { className?: string }) {39 const pref = useTheme((s) => s.pref);40 const setTheme = useSetTheme();41 return (42 <div role="radiogroup" aria-label="Theme" className={cn('grid grid-cols-3 gap-1 rounded-full bg-surface-2 p-1', className)} data-testid="theme-segmented">43 {OPTIONS.map((o) => (44 <button45 key={o.id}46 role="radio"47 aria-checked={pref === o.id}48 onClick={() => setTheme(o.id)}49 className={cn('flex h-9 items-center justify-center gap-1.5 rounded-full text-xs font-medium transition-colors pointer-coarse:h-11 pointer-coarse:text-sm', pref === o.id ? 'bg-surface text-text shadow-card' : 'text-muted hover:text-text-2')}50 >51 <o.icon className="size-3.5" aria-hidden /> {o.label}52 </button>53 ))}54 </div>55 );56}57