- Replaced the next-themes package with a custom ThemeProvider component for enhanced theme management. - Implemented local storage handling for theme persistence and system theme detection. - Updated the Toaster component to utilize the new theme context. - Removed unused next-themes references from package.json and package-lock.json.
161 lines
4.9 KiB
TypeScript
161 lines
4.9 KiB
TypeScript
"use client"
|
|
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
type ReactNode,
|
|
} from "react"
|
|
import { useServerInsertedHTML } from "next/navigation"
|
|
|
|
const STORAGE_KEY = "rl-theme"
|
|
const MEDIA_QUERY = "(prefers-color-scheme: dark)"
|
|
const THEMES = ["light", "dark", "system"] as const
|
|
|
|
type Theme = (typeof THEMES)[number]
|
|
type ResolvedTheme = "light" | "dark"
|
|
|
|
const THEME_INIT_SCRIPT = `(function(){try{var k="${STORAGE_KEY}",d="dark",e=document.documentElement,t=localStorage.getItem(k)||d,s=function(){return window.matchMedia("${MEDIA_QUERY}").matches?"dark":"light"}();if(t==="system")t=s;e.classList.remove("light","dark");e.classList.add(t);e.style.colorScheme=t}catch(n){}})()`
|
|
|
|
type ThemeContextValue = {
|
|
theme: Theme | undefined
|
|
setTheme: (theme: Theme | ((prev: Theme) => Theme)) => void
|
|
resolvedTheme: ResolvedTheme | undefined
|
|
systemTheme: ResolvedTheme | undefined
|
|
themes: readonly Theme[]
|
|
}
|
|
|
|
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined)
|
|
|
|
type ThemeProviderProps = {
|
|
children: ReactNode
|
|
defaultTheme?: Theme
|
|
enableSystem?: boolean
|
|
disableTransitionOnChange?: boolean
|
|
}
|
|
|
|
function readStoredTheme(defaultTheme: Theme, enableSystem: boolean): Theme {
|
|
try {
|
|
const stored = localStorage.getItem(STORAGE_KEY)
|
|
if (stored === "light" || stored === "dark") return stored
|
|
if (enableSystem && stored === "system") return stored
|
|
} catch {
|
|
// ignore
|
|
}
|
|
return defaultTheme
|
|
}
|
|
|
|
function resolveTheme(theme: Theme, systemTheme: ResolvedTheme): ResolvedTheme {
|
|
return theme === "system" ? systemTheme : theme
|
|
}
|
|
|
|
function applyThemeClass(resolved: ResolvedTheme, disableTransitionOnChange: boolean) {
|
|
const root = document.documentElement
|
|
if (disableTransitionOnChange) {
|
|
const style = document.createElement("style")
|
|
style.appendChild(
|
|
document.createTextNode(
|
|
"*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}",
|
|
),
|
|
)
|
|
document.head.appendChild(style)
|
|
window.getComputedStyle(document.body)
|
|
setTimeout(() => {
|
|
document.head.removeChild(style)
|
|
}, 1)
|
|
}
|
|
root.classList.remove("light", "dark")
|
|
root.classList.add(resolved)
|
|
root.style.colorScheme = resolved
|
|
}
|
|
|
|
export function ThemeProvider({
|
|
children,
|
|
defaultTheme = "dark",
|
|
enableSystem = true,
|
|
disableTransitionOnChange = false,
|
|
}: ThemeProviderProps) {
|
|
useServerInsertedHTML(() => (
|
|
<script suppressHydrationWarning dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }} />
|
|
))
|
|
|
|
const [theme, setThemeState] = useState<Theme>(defaultTheme)
|
|
const [systemTheme, setSystemTheme] = useState<ResolvedTheme>("dark")
|
|
const [ready, setReady] = useState(false)
|
|
|
|
useEffect(() => {
|
|
const media = window.matchMedia(MEDIA_QUERY)
|
|
const onSystemChange = (event: MediaQueryListEvent) => {
|
|
setSystemTheme(event.matches ? "dark" : "light")
|
|
}
|
|
|
|
setSystemTheme(media.matches ? "dark" : "light")
|
|
media.addEventListener("change", onSystemChange)
|
|
|
|
const stored = readStoredTheme(defaultTheme, enableSystem)
|
|
setThemeState(stored)
|
|
applyThemeClass(resolveTheme(stored, media.matches ? "dark" : "light"), false)
|
|
setReady(true)
|
|
|
|
const onStorage = (event: StorageEvent) => {
|
|
if (event.key !== STORAGE_KEY || !event.newValue) return
|
|
const next = event.newValue
|
|
if (next !== "light" && next !== "dark" && next !== "system") return
|
|
setThemeState(next)
|
|
}
|
|
window.addEventListener("storage", onStorage)
|
|
|
|
return () => {
|
|
media.removeEventListener("change", onSystemChange)
|
|
window.removeEventListener("storage", onStorage)
|
|
}
|
|
}, [defaultTheme, enableSystem])
|
|
|
|
useEffect(() => {
|
|
if (!ready) return
|
|
applyThemeClass(resolveTheme(theme, systemTheme), disableTransitionOnChange)
|
|
}, [theme, systemTheme, ready, disableTransitionOnChange])
|
|
|
|
const setTheme = useCallback((value: Theme | ((prev: Theme) => Theme)) => {
|
|
setThemeState((prev) => {
|
|
const next = typeof value === "function" ? value(prev) : value
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, next)
|
|
} catch {
|
|
// ignore
|
|
}
|
|
return next
|
|
})
|
|
}, [])
|
|
|
|
const value = useMemo<ThemeContextValue>(
|
|
() => ({
|
|
theme: ready ? theme : undefined,
|
|
setTheme,
|
|
resolvedTheme: ready ? resolveTheme(theme, systemTheme) : undefined,
|
|
systemTheme: ready ? systemTheme : undefined,
|
|
themes: enableSystem ? THEMES : (["light", "dark"] as const),
|
|
}),
|
|
[ready, theme, setTheme, systemTheme, enableSystem],
|
|
)
|
|
|
|
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
|
}
|
|
|
|
export function useTheme() {
|
|
const value = useContext(ThemeContext)
|
|
if (!value) {
|
|
return {
|
|
theme: undefined,
|
|
setTheme: () => {},
|
|
resolvedTheme: undefined,
|
|
systemTheme: undefined,
|
|
themes: [] as const,
|
|
}
|
|
}
|
|
return value
|
|
}
|