File size: 2,449 Bytes
7c5ed63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import { useCallback, useEffect, useState } from 'react';

export type ThemeChoice = 'light' | 'dark' | 'auto';
export type ResolvedTheme = 'light' | 'dark';

const STORAGE_KEY = 'vis.theme';

function systemTheme(): ResolvedTheme {
  if (typeof window === 'undefined') return 'dark';
  return window.matchMedia('(prefers-color-scheme: light)').matches
    ? 'light'
    : 'dark';
}

function readStored(): ThemeChoice {
  try {
    const v = localStorage.getItem(STORAGE_KEY);
    if (v === 'light' || v === 'dark' || v === 'auto') return v;
  } catch {
    /* ignore quota/permission errors */
  }
  return 'auto';
}

function apply(resolved: ResolvedTheme): void {
  const root = document.documentElement;
  root.dataset.theme = resolved;
  // Sync the address-bar theme-color so native chrome (URL bar, title bar)
  // transitions too.
  const meta = document.querySelector<HTMLMetaElement>('meta[name="theme-color"]');
  if (meta) {
    meta.content = resolved === 'light' ? '#fafbfc' : '#0b0d12';
  }
}

/**
 * Three-state theme: auto (follow system), light, dark. The resolved concrete
 * theme is reflected on `<html data-theme="...">`. User choice is persisted
 * in localStorage; absence ⇒ auto.
 */
export function useTheme(): {
  choice: ThemeChoice;
  resolved: ResolvedTheme;
  cycle: () => void;
  set: (c: ThemeChoice) => void;
} {
  const [choice, setChoice] = useState<ThemeChoice>(() => readStored());
  const [sys, setSys] = useState<ResolvedTheme>(() => systemTheme());

  // Listen to system changes when in auto mode.
  useEffect(() => {
    const m = window.matchMedia('(prefers-color-scheme: light)');
    const handler = () =>{  setSys(m.matches ? 'light' : 'dark'); };
    m.addEventListener('change', handler);
    return () =>{  m.removeEventListener('change', handler); };
  }, []);

  const resolved: ResolvedTheme = choice === 'auto' ? sys : choice;

  useEffect(() => {
    apply(resolved);
  }, [resolved]);

  const set = useCallback((c: ThemeChoice) => {
    setChoice(c);
    try {
      if (c === 'auto') localStorage.removeItem(STORAGE_KEY);
      else localStorage.setItem(STORAGE_KEY, c);
    } catch {
      /* ignore */
    }
  }, []);

  // Cycle: auto → light → dark → auto
  const cycle = useCallback(() => {
    const next: ThemeChoice =
      choice === 'auto' ? 'light' : choice === 'light' ? 'dark' : 'auto';
    set(next);
  }, [choice, set]);

  return { choice, resolved, cycle, set };
}