From 417771b93eb308f4b0850ee19cf02751dab4ce7d Mon Sep 17 00:00:00 2001 From: v0 Date: Sat, 21 Mar 2026 12:01:30 +0000 Subject: [PATCH] feat: implement custom ThemeProvider without next-themes Create React Context-based theme provider with localStorage support. Co-authored-by: Simon <85533298+handsomezhuzhu@users.noreply.github.com> --- components/theme-provider.tsx | 103 +++++++++++++++++++++++++++++++--- 1 file changed, 96 insertions(+), 7 deletions(-) diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx index 20feb83..df0d5bf 100644 --- a/components/theme-provider.tsx +++ b/components/theme-provider.tsx @@ -1,12 +1,101 @@ "use client" -import type * as React from "react" -import { ThemeProvider as NextThemesProvider } from "next-themes" +import * as React from "react" +import { createContext, useContext, useEffect, useState } from "react" -export { useTheme } from "next-themes" +type Theme = "dark" | "light" | "system" -type ThemeProviderProps = React.ComponentProps - -export function ThemeProvider({ children, ...props }: ThemeProviderProps) { - return {children} +type ThemeProviderProps = { + children: React.ReactNode + defaultTheme?: Theme + storageKey?: string + attribute?: string + enableSystem?: boolean + disableTransitionOnChange?: boolean +} + +type ThemeProviderState = { + theme: Theme + setTheme: (theme: Theme) => void + resolvedTheme?: string +} + +const ThemeProviderContext = createContext(undefined) + +export function ThemeProvider({ + children, + defaultTheme = "system", + storageKey = "theme", + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState(defaultTheme) + const [resolvedTheme, setResolvedTheme] = useState(undefined) + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + const stored = localStorage.getItem(storageKey) as Theme | null + if (stored) { + setTheme(stored) + } + }, [storageKey]) + + useEffect(() => { + if (!mounted) return + + const root = window.document.documentElement + + root.classList.remove("light", "dark") + + let resolved: string + if (theme === "system") { + resolved = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" + } else { + resolved = theme + } + + root.classList.add(resolved) + setResolvedTheme(resolved) + }, [theme, mounted]) + + useEffect(() => { + if (!mounted || theme !== "system") return + + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") + const handleChange = () => { + const resolved = mediaQuery.matches ? "dark" : "light" + const root = window.document.documentElement + root.classList.remove("light", "dark") + root.classList.add(resolved) + setResolvedTheme(resolved) + } + + mediaQuery.addEventListener("change", handleChange) + return () => mediaQuery.removeEventListener("change", handleChange) + }, [theme, mounted]) + + const value = { + theme, + setTheme: (newTheme: Theme) => { + localStorage.setItem(storageKey, newTheme) + setTheme(newTheme) + }, + resolvedTheme, + } + + return ( + + {children} + + ) +} + +export function useTheme() { + const context = useContext(ThemeProviderContext) + + if (context === undefined) { + throw new Error("useTheme must be used within a ThemeProvider") + } + + return context }