SPB Git

spb/ai-atlas

Public
2.8 KB · 57 lines tsx
1'use client';2import { Monitor, Moon, Sun } from 'lucide-react';3import { useEffect, useState } from 'react';4import { cn } from '@/lib/cn';56export type ThemePref = 'light' | 'dark' | 'system';7const KEY = 'aia-theme';89/** Inline in <head> before paint: applies the persisted preference (or the system scheme) to <html data-theme>. */10export const THEME_SCRIPT = `(function(){try{var k='${KEY}',p=localStorage.getItem(k),m=window.matchMedia('(prefers-color-scheme: dark)'),t=(p==='light'||p==='dark')?p:(m.matches?'dark':'light');document.documentElement.setAttribute('data-theme',t);document.documentElement.style.colorScheme=t;}catch(e){}})();`;1112function apply(pref: ThemePref) {13  const dark = window.matchMedia('(prefers-color-scheme: dark)').matches;14  const t = pref === 'system' ? (dark ? 'dark' : 'light') : pref;15  document.documentElement.setAttribute('data-theme', t);16  document.documentElement.style.colorScheme = t;17  const meta = document.querySelector('meta[name="theme-color"]');18  if (meta) meta.setAttribute('content', t === 'dark' ? '#0b0d11' : '#f6f6f3');19}2021export function useTheme(): [ThemePref, (p: ThemePref) => void] {22  const [pref, setPref] = useState<ThemePref>('system');23  useEffect(() => {24    const p = localStorage.getItem(KEY);25    if (p === 'light' || p === 'dark') setPref(p);26    // Error/not-found shells are client-rendered from scratch and lose the attribute set by THEME_SCRIPT: re-apply.27    if (!document.documentElement.getAttribute('data-theme')) apply(p === 'light' || p === 'dark' ? p : 'system');28    const m = window.matchMedia('(prefers-color-scheme: dark)');29    const onChange = () => {30      const cur = localStorage.getItem(KEY);31      if (cur !== 'light' && cur !== 'dark') apply('system');32    };33    m.addEventListener('change', onChange);34    return () => m.removeEventListener('change', onChange);35  }, []);36  const set = (p: ThemePref) => {37    setPref(p);38    if (p === 'system') localStorage.removeItem(KEY);39    else localStorage.setItem(KEY, p);40    apply(p);41  };42  return [pref, set];43}4445/** Three-state toggle (system → light → dark). Touch target 44 px. */46export function ThemeToggle({ className }: { className?: string }) {47  const [pref, set] = useTheme();48  const next: ThemePref = pref === 'system' ? 'light' : pref === 'light' ? 'dark' : 'system';49  const Icon = pref === 'system' ? Monitor : pref === 'light' ? Sun : Moon;50  const label = pref === 'system' ? 'Theme: system' : pref === 'light' ? 'Theme: light' : 'Theme: dark';51  return (52    <button type="button" onClick={() => set(next)} className={cn('flex size-11 items-center justify-center rounded-sm text-ink-2 hover:bg-surface-2 hover:text-ink md:size-10', className)} aria-label={`${label} — switch`} title={label}>53      <Icon className="size-[18px]" aria-hidden />54    </button>55  );56}57
Files · 9f1db171c786cf4a3b99436329f29b145c02aeec
Menu

Theme