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,
|
title: it.title,
|
||||||
url: it.url,
|
url: it.url,
|
||||||
icon: it.icon,
|
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"
|
const isDark = (resolvedTheme ?? "dark") === "dark"
|
||||||
|
|
||||||
|
|||||||
+155
-20
@@ -1,25 +1,160 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
import {
|
||||||
import type { ComponentProps } from "react"
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
type ReactNode,
|
||||||
|
} from "react"
|
||||||
|
import { useServerInsertedHTML } from "next/navigation"
|
||||||
|
|
||||||
/** Обёртка над next-themes: тот же storageKey `rl-theme`, класс на `<html>`. */
|
const STORAGE_KEY = "rl-theme"
|
||||||
export function ThemeProvider({
|
const MEDIA_QUERY = "(prefers-color-scheme: dark)"
|
||||||
children,
|
const THEMES = ["light", "dark", "system"] as const
|
||||||
...props
|
|
||||||
}: ComponentProps<typeof NextThemesProvider>) {
|
type Theme = (typeof THEMES)[number]
|
||||||
return (
|
type ResolvedTheme = "light" | "dark"
|
||||||
<NextThemesProvider
|
|
||||||
attribute="class"
|
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){}})()`
|
||||||
defaultTheme="dark"
|
|
||||||
enableSystem
|
type ThemeContextValue = {
|
||||||
storageKey="rl-theme"
|
theme: Theme | undefined
|
||||||
disableTransitionOnChange
|
setTheme: (theme: Theme | ((prev: Theme) => Theme)) => void
|
||||||
{...props}
|
resolvedTheme: ResolvedTheme | undefined
|
||||||
>
|
systemTheme: ResolvedTheme | undefined
|
||||||
{children}
|
themes: readonly Theme[]
|
||||||
</NextThemesProvider>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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"
|
"use client"
|
||||||
|
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "@/components/theme-provider"
|
||||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||||
|
|
||||||
const Toaster = ({ ...props }: ToasterProps) => {
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
|
|||||||
Generated
+1
-26
@@ -7,6 +7,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "mmapp",
|
"name": "mmapp",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"hasInstallScript": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
"backend"
|
"backend"
|
||||||
@@ -18,7 +19,6 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^1.11.0",
|
"lucide-react": "^1.11.0",
|
||||||
"next": "16.2.4",
|
"next": "16.2.4",
|
||||||
"next-themes": "^0.4.6",
|
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"shadcn": "^4.5.0",
|
"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": {
|
"node_modules/next/node_modules/@next/swc-win32-x64-msvc": {
|
||||||
"version": "16.2.4",
|
"version": "16.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz",
|
||||||
@@ -12684,21 +12674,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "^4.4.1"
|
"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",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^1.11.0",
|
"lucide-react": "^1.11.0",
|
||||||
"next": "16.2.4",
|
"next": "16.2.4",
|
||||||
"next-themes": "^0.4.6",
|
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"shadcn": "^4.5.0",
|
"shadcn": "^4.5.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user