Music collection management tool
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: theme provider

+37 -191
+29 -186
web/src/components/theme-provider.tsx
··· 1 - /* eslint-disable react-refresh/only-export-components */ 2 - import * as React from "react" 1 + import { createContext, useContext, useEffect, useState } from "react" 3 2 4 3 type Theme = "dark" | "light" | "system" 5 - type ResolvedTheme = "dark" | "light" 6 4 7 5 type ThemeProviderProps = { 8 6 children: React.ReactNode 9 7 defaultTheme?: Theme 10 8 storageKey?: string 11 - disableTransitionOnChange?: boolean 12 9 } 13 10 14 11 type ThemeProviderState = { ··· 16 13 setTheme: (theme: Theme) => void 17 14 } 18 15 19 - const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)" 20 - const THEME_VALUES: Theme[] = ["dark", "light", "system"] 21 - 22 - const ThemeProviderContext = React.createContext< 23 - ThemeProviderState | undefined 24 - >(undefined) 25 - 26 - function isTheme(value: string | null): value is Theme { 27 - if (value === null) { 28 - return false 29 - } 30 - 31 - return THEME_VALUES.includes(value as Theme) 32 - } 33 - 34 - function getSystemTheme(): ResolvedTheme { 35 - if (window.matchMedia(COLOR_SCHEME_QUERY).matches) { 36 - return "dark" 37 - } 38 - 39 - return "light" 40 - } 41 - 42 - function disableTransitionsTemporarily() { 43 - const style = document.createElement("style") 44 - style.appendChild( 45 - document.createTextNode( 46 - "*,*::before,*::after{-webkit-transition:none!important;transition:none!important}" 47 - ) 48 - ) 49 - document.head.appendChild(style) 50 - 51 - return () => { 52 - window.getComputedStyle(document.body) 53 - requestAnimationFrame(() => { 54 - requestAnimationFrame(() => { 55 - style.remove() 56 - }) 57 - }) 58 - } 16 + const initialState: ThemeProviderState = { 17 + theme: "system", 18 + setTheme: () => null, 59 19 } 60 20 61 - function isEditableTarget(target: EventTarget | null) { 62 - if (!(target instanceof HTMLElement)) { 63 - return false 64 - } 65 - 66 - if (target.isContentEditable) { 67 - return true 68 - } 69 - 70 - const editableParent = target.closest( 71 - "input, textarea, select, [contenteditable='true']" 72 - ) 73 - if (editableParent) { 74 - return true 75 - } 76 - 77 - return false 78 - } 21 + const ThemeProviderContext = createContext<ThemeProviderState>(initialState) 79 22 80 23 export function ThemeProvider({ 81 24 children, 82 25 defaultTheme = "system", 83 - storageKey = "theme", 84 - disableTransitionOnChange = true, 26 + storageKey = "vite-ui-theme", 85 27 ...props 86 28 }: ThemeProviderProps) { 87 - const [theme, setThemeState] = React.useState<Theme>(() => { 88 - const storedTheme = localStorage.getItem(storageKey) 89 - if (isTheme(storedTheme)) { 90 - return storedTheme 91 - } 92 - 93 - return defaultTheme 94 - }) 95 - 96 - const setTheme = React.useCallback( 97 - (nextTheme: Theme) => { 98 - localStorage.setItem(storageKey, nextTheme) 99 - setThemeState(nextTheme) 100 - }, 101 - [storageKey] 29 + const [theme, setTheme] = useState<Theme>( 30 + () => (localStorage.getItem(storageKey) as Theme) || defaultTheme 102 31 ) 103 32 104 - const applyTheme = React.useCallback( 105 - (nextTheme: Theme) => { 106 - const root = document.documentElement 107 - const resolvedTheme = 108 - nextTheme === "system" ? getSystemTheme() : nextTheme 109 - const restoreTransitions = disableTransitionOnChange 110 - ? disableTransitionsTemporarily() 111 - : null 33 + useEffect(() => { 34 + const root = window.document.documentElement 112 35 113 - root.classList.remove("light", "dark") 114 - root.classList.add(resolvedTheme) 36 + root.classList.remove("light", "dark") 115 37 116 - if (restoreTransitions) { 117 - restoreTransitions() 118 - } 119 - }, 120 - [disableTransitionOnChange] 121 - ) 38 + if (theme === "system") { 39 + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") 40 + .matches 41 + ? "dark" 42 + : "light" 122 43 123 - React.useEffect(() => { 124 - applyTheme(theme) 125 - 126 - if (theme !== "system") { 127 - return undefined 44 + root.classList.add(systemTheme) 45 + return 128 46 } 129 47 130 - const mediaQuery = window.matchMedia(COLOR_SCHEME_QUERY) 131 - const handleChange = () => { 132 - applyTheme("system") 133 - } 134 - 135 - mediaQuery.addEventListener("change", handleChange) 136 - 137 - return () => { 138 - mediaQuery.removeEventListener("change", handleChange) 139 - } 140 - }, [theme, applyTheme]) 141 - 142 - React.useEffect(() => { 143 - const handleKeyDown = (event: KeyboardEvent) => { 144 - if (event.repeat) { 145 - return 146 - } 48 + root.classList.add(theme) 49 + }, [theme]) 147 50 148 - if (event.metaKey || event.ctrlKey || event.altKey) { 149 - return 150 - } 151 - 152 - if (isEditableTarget(event.target)) { 153 - return 154 - } 155 - 156 - if (event.key.toLowerCase() !== "d") { 157 - return 158 - } 159 - 160 - setThemeState((currentTheme) => { 161 - const nextTheme = 162 - currentTheme === "dark" 163 - ? "light" 164 - : currentTheme === "light" 165 - ? "dark" 166 - : getSystemTheme() === "dark" 167 - ? "light" 168 - : "dark" 169 - 170 - localStorage.setItem(storageKey, nextTheme) 171 - return nextTheme 172 - }) 173 - } 174 - 175 - window.addEventListener("keydown", handleKeyDown) 176 - 177 - return () => { 178 - window.removeEventListener("keydown", handleKeyDown) 179 - } 180 - }, [storageKey]) 181 - 182 - React.useEffect(() => { 183 - const handleStorageChange = (event: StorageEvent) => { 184 - if (event.storageArea !== localStorage) { 185 - return 186 - } 187 - 188 - if (event.key !== storageKey) { 189 - return 190 - } 191 - 192 - if (isTheme(event.newValue)) { 193 - setThemeState(event.newValue) 194 - return 195 - } 196 - 197 - setThemeState(defaultTheme) 198 - } 199 - 200 - window.addEventListener("storage", handleStorageChange) 201 - 202 - return () => { 203 - window.removeEventListener("storage", handleStorageChange) 204 - } 205 - }, [defaultTheme, storageKey]) 206 - 207 - const value = React.useMemo( 208 - () => ({ 209 - theme, 210 - setTheme, 211 - }), 212 - [theme, setTheme] 213 - ) 51 + const value = { 52 + theme, 53 + setTheme: (theme: Theme) => { 54 + localStorage.setItem(storageKey, theme) 55 + setTheme(theme) 56 + }, 57 + } 214 58 215 59 return ( 216 60 <ThemeProviderContext.Provider {...props} value={value}> ··· 220 64 } 221 65 222 66 export const useTheme = () => { 223 - const context = React.useContext(ThemeProviderContext) 67 + const context = useContext(ThemeProviderContext) 224 68 225 - if (context === undefined) { 69 + if (context === undefined) 226 70 throw new Error("useTheme must be used within a ThemeProvider") 227 - } 228 71 229 72 return context 230 73 }
+4 -4
web/src/index.css
··· 120 120 @layer base { 121 121 * { 122 122 @apply border-border outline-ring/50; 123 - } 123 + } 124 124 body { 125 125 @apply bg-background text-foreground; 126 - } 126 + } 127 127 html { 128 128 @apply font-sans; 129 - } 130 - } 129 + } 130 + }
+4 -1
web/src/main.tsx
··· 3 3 import { RouterProvider } from '@tanstack/react-router' 4 4 import { router } from './router' 5 5 import './index.css' 6 + import { ThemeProvider } from './components/theme-provider' 6 7 7 8 createRoot(document.getElementById('root')!).render( 8 9 <StrictMode> 9 - <RouterProvider router={router} /> 10 + <ThemeProvider defaultTheme="system" storageKey="musica-ui-theme"> 11 + <RouterProvider router={router} /> 12 + </ThemeProvider> 10 13 </StrictMode>, 11 14 )