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.
This commit is contained in:
@@ -209,10 +209,10 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
title: it.title,
|
||||
url: it.url,
|
||||
icon: it.icon,
|
||||
badge: badgeFor(it.url),
|
||||
badge: mounted ? badgeFor(it.url) : undefined,
|
||||
})),
|
||||
}))
|
||||
}, [mode, backendStatus, liveCounts, mockBadges, evo.enabled, evo.snapshot])
|
||||
}, [mounted, mode, backendStatus, liveCounts, mockBadges, evo.enabled, evo.snapshot])
|
||||
|
||||
const isDark = (resolvedTheme ?? "dark") === "dark"
|
||||
|
||||
|
||||
+155
-20
@@ -1,25 +1,160 @@
|
||||
"use client"
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
import type { ComponentProps } from "react"
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react"
|
||||
import { useServerInsertedHTML } from "next/navigation"
|
||||
|
||||
/** Обёртка над next-themes: тот же storageKey `rl-theme`, класс на `<html>`. */
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<typeof NextThemesProvider>) {
|
||||
return (
|
||||
<NextThemesProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
enableSystem
|
||||
storageKey="rl-theme"
|
||||
disableTransitionOnChange
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</NextThemesProvider>
|
||||
)
|
||||
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[]
|
||||
}
|
||||
|
||||
export { useTheme } from "next-themes"
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { useTheme } from "@/components/theme-provider"
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
|
||||
Generated
+1
-26
@@ -7,6 +7,7 @@
|
||||
"": {
|
||||
"name": "mmapp",
|
||||
"version": "0.1.0",
|
||||
"hasInstallScript": true,
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"backend"
|
||||
@@ -18,7 +19,6 @@
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.11.0",
|
||||
"next": "16.2.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"shadcn": "^4.5.0",
|
||||
@@ -9017,16 +9017,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-themes": {
|
||||
"version": "0.4.6",
|
||||
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
|
||||
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/next/node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz",
|
||||
@@ -12684,21 +12674,6 @@
|
||||
"dependencies": {
|
||||
"zod": "^4.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz",
|
||||
"integrity": "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.11.0",
|
||||
"next": "16.2.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"shadcn": "^4.5.0",
|
||||
|
||||
Reference in New Issue
Block a user