spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1'use client';23import { useEffect, useState } from 'react';4import { Moon, Sun, Monitor } from 'lucide-react';56type Theme = 'light' | 'dark' | 'system';7const KEY = 'ci-theme';89/**10 * Light / dark / system toggle (SPEC §86). The choice is stored in localStorage and applied to11 * <html data-theme> before first paint by the inline script in layout.tsx, so there is no flash.12 * "system" removes the attribute and lets the prefers-color-scheme media query decide.13 */14export function ThemeToggle() {15 const [theme, setTheme] = useState<Theme>('system');16 useEffect(() => {17 const stored = (typeof window !== 'undefined' && window.localStorage.getItem(KEY)) as Theme | null;18 if (stored === 'light' || stored === 'dark') setTheme(stored);19 }, []);20 const apply = (t: Theme) => {21 setTheme(t);22 const root = document.documentElement;23 if (t === 'system') {24 window.localStorage.removeItem(KEY);25 root.removeAttribute('data-theme');26 } else {27 window.localStorage.setItem(KEY, t);28 root.setAttribute('data-theme', t);29 }30 };31 const next: Theme = theme === 'system' ? 'dark' : theme === 'dark' ? 'light' : 'system';32 const label = theme === 'system' ? 'Theme: system' : theme === 'dark' ? 'Theme: dark' : 'Theme: light';33 const Icon = theme === 'system' ? Monitor : theme === 'dark' ? Moon : Sun;34 return (35 <button type="button" className="ci-theme-toggle" onClick={() => apply(next)} aria-label={`${label}. Switch to ${next}.`} title={`${label} — click for ${next}`}>36 <Icon className="h-3.5 w-3.5" aria-hidden />37 <span className="hidden text-[11px] sm:inline">{theme === 'system' ? 'Auto' : theme === 'dark' ? 'Dark' : 'Light'}</span>38 </button>39 );40}41