Files
MikrotikManager/components/theme-provider.tsx
T
Denozordec 40e2040fb0
Docker images / backend-image (push) Successful in 2m35s
Docker images / frontend-image (push) Successful in 2m9s
Docker images / updater-image (push) Successful in 38s
Docker images / notify-webhook (push) Has been skipped
feat: refactor theme management and remove next-themes dependency
- 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.
2026-05-12 16:07:40 +07:00

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
}