-
Notifications
You must be signed in to change notification settings - Fork 19
/
Theme.tsx
89 lines (68 loc) · 2.26 KB
/
Theme.tsx
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
84
85
86
87
88
89
import { useEffect, type ReactNode, useMemo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { atom, useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
const STORAGE_KEY = 'theme';
enum Theme {
LIGHT = 'light',
DARK = 'dark',
}
const SYSTEM_THEME = 'system';
const darkThemeMq = window.matchMedia('(prefers-color-scheme: dark)');
const getSystemTheme = () => (darkThemeMq.matches ? Theme.DARK : Theme.LIGHT);
const isTheme = (v: unknown): v is Theme | typeof SYSTEM_THEME =>
typeof v === 'string' && [SYSTEM_THEME, ...Object.values(Theme)].includes(v);
const getValidTheme = (theme?: string) => (theme && isTheme(theme) ? theme : getSystemTheme());
const setThemeAttr = (theme: string) => {
const rootEl = window.document.documentElement;
rootEl.classList.remove(...Object.values(Theme));
rootEl.classList.add(theme);
};
const persistedThemeAtom = atomWithStorage<Theme | typeof SYSTEM_THEME>(
STORAGE_KEY,
getValidTheme(localStorage.getItem(STORAGE_KEY) ?? ''),
);
const themeAtom = atom(
(get) => getValidTheme(get(persistedThemeAtom)),
(_, set, theme: string) => set(persistedThemeAtom, getValidTheme(theme)),
);
export const useTheme = () => {
const { t } = useTranslation('global');
const [theme, setTheme] = useAtom(themeAtom);
const themes = useMemo(
() => [
{ value: Theme.LIGHT, label: t('theme.light') },
{ value: Theme.DARK, label: t('theme.dark') },
{ value: SYSTEM_THEME, label: t('theme.system') },
],
[t],
);
return useMemo(
() =>
({
theme: theme === SYSTEM_THEME ? getSystemTheme() : theme,
rawTheme: theme,
setTheme,
themes,
}) as const,
[theme, setTheme, themes],
);
};
type Props = {
children: ReactNode;
};
const ThemeProvider = ({ children }: Props) => {
const { theme, rawTheme } = useTheme();
useEffect(() => setThemeAttr(theme), [theme]);
const mqListener = useCallback(() => {
if (rawTheme === SYSTEM_THEME) {
setThemeAttr(getSystemTheme());
}
}, [rawTheme]);
useEffect(() => {
darkThemeMq.addEventListener('change', mqListener);
return () => darkThemeMq.removeEventListener('change', mqListener);
}, [mqListener]);
return children;
};
export default ThemeProvider;