SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
1.7 KB · 41 lines tsx
Raw Blame History
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