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 { useUi } from '@/stores/ui';7import { IconButton } from '@/components/ui/button';8import { cn } from '@/lib/cn';910/** Applies the theme locally at once and stores the choice on the server (when signed in). */11export function useSetTheme(): (pref: ThemePref) => void {12 const qc = useQueryClient();13 return (pref) => {14 useTheme.getState().setPref(pref);15 useUi.getState().set({ themeSet: true });16 void saveSettings(qc, { theme: pref }).catch(() => undefined);17 };18}1920/** Sun/moon button: flips between light and dark. */21export function ThemeToggle({ className }: { className?: string }) {22 const theme = useTheme((s) => s.theme);23 const setTheme = useSetTheme();24 return (25 <IconButton label={theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'} onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} className={className} data-testid="theme-toggle">26 <span className="relative block size-4">27 <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')} />28 <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')} />29 </span>30 </IconButton>31 );32}3334const OPTIONS: { id: ThemePref; label: string; icon: typeof Sun }[] = [35 { id: 'light', label: 'Light', icon: Sun },36 { id: 'dark', label: 'Dark', icon: Moon },37 { id: 'system', label: 'System', icon: Monitor },38];39/** Three-way segmented control (Settings, phone "More" sheet). */40export function ThemeSegmented({ className }: { className?: string }) {41 const pref = useTheme((s) => s.pref);42 const setTheme = useSetTheme();43 return (44 <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">45 {OPTIONS.map((o) => (46 <button47 key={o.id}48 role="radio"49 aria-checked={pref === o.id}50 onClick={() => setTheme(o.id)}51 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')}52 >53 <o.icon className="size-3.5" aria-hidden /> {o.label}54 </button>55 ))}56 </div>57 );58}59