Repository navigation
Expand file tree
/
Copy pathThemeContext.tsx
More file actions
122 lines (101 loc) · 3.01 KB
/
Copy pathThemeContext.tsx
File metadata and controls
122 lines (101 loc) · 3.01 KB
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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import React, {useEffect, useState} from 'react'
import {insertPrefetches, updateTheme} from './updateTheme'
import {light as lightTheme, dark as darkTheme} from './theme'
import {
DefaultTheme,
ThemeProvider as StyledThemeProvider
} from 'styled-components'
export type ThemeKey = 'light' | 'dark'
type UpdateTheme = React.Dispatch<React.SetStateAction<ThemeKey>>
export interface ThemeContextProperties {
theme: ThemeKey
update: UpdateTheme
}
export const ThemeContext = React.createContext<ThemeContextProperties>(
{
theme: 'light',
update: () => 'light'
}
)
export function useTheme(): [ThemeKey, UpdateTheme] {
const {theme, update} = React.useContext(ThemeContext)
return [theme, update]
}
type ToggleTheme = () => void
export function useThemeToggle(): ToggleTheme {
const [, update] = useTheme()
return () => update((current: ThemeKey) => toggle(current))
}
function toggle(current: ThemeKey): ThemeKey {
return current === 'light' ? 'dark' : 'light'
}
export interface ThemeProviderProperties {
initialTheme?: ThemeKey
children: React.ReactNode
}
export type ThemeTable<T> = Record<ThemeKey, T>
const themeSources: ThemeTable<string> = {
light: '/theme/light.css',
dark: '/theme/dark.css'
}
const themeVariables: ThemeTable<DefaultTheme> = {
light: lightTheme,
dark: darkTheme
}
const themeStorageKey = 'jsheets.theme'
export function detectTheme(): ThemeKey {
try {
const stored = localStorage.getItem(themeStorageKey)
if (stored) {
return stored === 'dark' ? 'dark' : 'light'
}
} catch (error) {
console.error({message: 'failed to detect theme', error})
}
return selectUserPreference()
}
function storeTheme(theme: ThemeKey) {
try {
localStorage.setItem(themeStorageKey, theme)
} catch (error) {
console.error({message: 'failed to store theme', error})
}
}
function selectUserPreference(): ThemeKey {
const prefersDark = window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches
return prefersDark ? 'dark' : 'light'
}
export async function installThemes(): Promise<string> {
return new Promise(resolve => {
const initial = detectTheme()
insertPrefetches(themeSources)
updateTheme(initial, themeSources[initial], () => resolve(initial))
})
}
function useUpdate(call: () => void, dependencies: any[]) {
const firstRender = React.useRef(true)
useEffect(() => {
if (firstRender.current) {
firstRender.current = false
} else {
call()
}
}, [...dependencies, firstRender])
}
export default function ThemeProvider(properties: ThemeProviderProperties) {
const [theme, update] = useState<ThemeKey>(properties.initialTheme || 'light')
const variables = themeVariables[theme]
useUpdate(() => {
const link = themeSources[theme]
updateTheme(theme, link)
storeTheme(theme)
}, [theme])
return (
<ThemeContext.Provider value={{theme, update}}>
<StyledThemeProvider theme={variables}>
{properties.children}
</StyledThemeProvider>
</ThemeContext.Provider>
)
}