SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
2.6 KB · 60 lines tsx
Raw Blame History
1'use client';2import { Monitor, Moon, Sun } from 'lucide-react';3import { useEffect, useState } from 'react';4import { cn } from '@/lib/cn';5import { THEME_DARK, THEME_LIGHT } from '@/lib/site';67import { THEME_KEY } from '@/lib/prepaint';89export type ThemePref = 'light' | 'dark' | 'system';10const KEY = THEME_KEY;1112/** Inline in <head> before paint — the string itself lives in `lib/prepaint.ts` (server-safe); import it from there in server code. */13export { THEME_SCRIPT } from '@/lib/prepaint';1415function apply(pref: ThemePref) {16  const dark = window.matchMedia('(prefers-color-scheme: dark)').matches;17  const t = pref === 'system' ? (dark ? 'dark' : 'light') : pref;18  document.documentElement.setAttribute('data-theme', t);19  document.documentElement.style.colorScheme = t;20  const meta = document.querySelector('meta[name="theme-color"]');21  if (meta) meta.setAttribute('content', t === 'dark' ? THEME_DARK : THEME_LIGHT);22}2324export function useTheme(): [ThemePref, (p: ThemePref) => void] {25  const [pref, setPref] = useState<ThemePref>('system');26  useEffect(() => {27    const p = localStorage.getItem(KEY);28    if (p === 'light' || p === 'dark') setPref(p);29    // Error/not-found shells are client-rendered from scratch and lose the attribute set by THEME_SCRIPT: re-apply.30    if (!document.documentElement.getAttribute('data-theme')) apply(p === 'light' || p === 'dark' ? p : 'system');31    const m = window.matchMedia('(prefers-color-scheme: dark)');32    const onChange = () => {33      const cur = localStorage.getItem(KEY);34      if (cur !== 'light' && cur !== 'dark') apply('system');35    };36    m.addEventListener('change', onChange);37    return () => m.removeEventListener('change', onChange);38  }, []);39  const set = (p: ThemePref) => {40    setPref(p);41    if (p === 'system') localStorage.removeItem(KEY);42    else localStorage.setItem(KEY, p);43    apply(p);44  };45  return [pref, set];46}4748/** Three-state toggle (system → light → dark). Touch target 44 px. */49export function ThemeToggle({ className }: { className?: string }) {50  const [pref, set] = useTheme();51  const next: ThemePref = pref === 'system' ? 'light' : pref === 'light' ? 'dark' : 'system';52  const Icon = pref === 'system' ? Monitor : pref === 'light' ? Sun : Moon;53  const label = pref === 'system' ? 'Theme: system' : pref === 'light' ? 'Theme: light' : 'Theme: dark';54  return (55    <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}>56      <Icon className="size-[18px]" aria-hidden />57    </button>58  );59}60