main
tsx 96 lines 2.06 KB
Raw
1 import {
2 createContext,
3 startTransition,
4 useContext,
5 useLayoutEffect,
6 useState,
7 type PropsWithChildren,
8 } from "react";
9
10 export type Theme = "light" | "dark";
11
12 const DEFAULT_THEME: Theme = "light";
13 const THEME_STORAGE_KEY = "theme";
14
15 interface ThemeContextValue {
16 theme: Theme;
17 toggleTheme: () => void;
18 }
19
20 const ThemeContext = createContext<ThemeContextValue | null>(null);
21
22 function isTheme(value: string | null): value is Theme {
23 return value === "light" || value === "dark";
24 }
25
26 function readStoredTheme(): Theme | null {
27 if (typeof window === "undefined") {
28 return null;
29 }
30
31 try {
32 const storedTheme = window.localStorage.getItem(THEME_STORAGE_KEY);
33 return isTheme(storedTheme) ? storedTheme : null;
34 } catch {
35 return null;
36 }
37 }
38
39 function getInitialTheme(): Theme {
40 if (typeof document === "undefined") {
41 return DEFAULT_THEME;
42 }
43
44 const storedTheme = readStoredTheme();
45 if (storedTheme) {
46 return storedTheme;
47 }
48
49 return document.documentElement.classList.contains("dark")
50 ? "dark"
51 : DEFAULT_THEME;
52 }
53
54 function applyTheme(theme: Theme) {
55 const root = document.documentElement;
56 root.classList.toggle("dark", theme === "dark");
57 root.style.colorScheme = theme;
58 }
59
60 export function ThemeProvider({ children }: PropsWithChildren) {
61 const [theme, setThemeState] = useState<Theme>(getInitialTheme);
62
63 useLayoutEffect(() => {
64 applyTheme(theme);
65 }, [theme]);
66
67 const updateTheme = (nextTheme: Theme) => {
68 try {
69 window.localStorage.setItem(THEME_STORAGE_KEY, nextTheme);
70 } catch {
71 // Ignore storage failures and still apply the theme locally.
72 }
73
74 startTransition(() => {
75 setThemeState(nextTheme);
76 });
77 };
78
79 const toggleTheme = () => {
80 updateTheme(theme === "dark" ? "light" : "dark");
81 };
82
83 return (
84 <ThemeContext.Provider value={{ theme, toggleTheme }}>
85 {children}
86 </ThemeContext.Provider>
87 );
88 }
89
90 export function useTheme() {
91 const value = useContext(ThemeContext);
92 if (!value) {
93 throw new Error("useTheme must be used within ThemeProvider");
94 }
95 return value;
96 }