SPB Git

Repository Public

theme.ts 1.7 KB 30 lines typescript

1// Follows the system by default; the user may choose light or dark. Applied before first paint by main.tsx.2import { useSyncExternalStore } from 'react';34export type ThemeChoice = 'light' | 'dark' | 'system';5const KEY = 'spd.theme';6const listeners = new Set<() => void>();7const media = window.matchMedia('(prefers-color-scheme: dark)');8export const getTheme = (): ThemeChoice => { const v = localStorage.getItem(KEY); return v === 'dark' || v === 'light' ? v : 'system'; };9export const isDark = (): boolean => { const c = getTheme(); return c === 'dark' || (c === 'system' && media.matches); };10/** The browser chrome (status bar, address bar) takes the colour of the page. */11export function paintChrome(dark: boolean): void {12  document.querySelectorAll<HTMLMetaElement>('meta[name=theme-color]').forEach((m) => { m.removeAttribute('media'); m.content = dark ? '#050508' : '#f4f4f9'; });13}14export function applyTheme(): void {15  const dark = isDark();16  document.documentElement.classList.toggle('dark', dark);17  paintChrome(dark);18  listeners.forEach((f) => f());19}20export function setTheme(c: ThemeChoice): void {21  localStorage.setItem(KEY, c);22  const doc = document as Document & { startViewTransition?: (cb: () => void) => unknown };23  if (doc.startViewTransition && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) doc.startViewTransition(applyTheme); else applyTheme();24}25media.addEventListener('change', applyTheme);26export function useTheme(): { choice: ThemeChoice; dark: boolean } {27  const snap = useSyncExternalStore((cb) => { listeners.add(cb); return () => listeners.delete(cb); }, () => `${getTheme()}:${isDark()}`);28  return { choice: snap.split(':')[0] as ThemeChoice, dark: snap.endsWith('true') };29}30
Files main
Menu

Theme