diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 3ebc102..4d872b6 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -6,5 +6,5 @@ "stylesheet": "src/styles.css", "functions": ["cn", "cva"] }, - "ignorePatterns": ["**/routeTree.gen.ts"] + "ignorePatterns": ["**/routeTree.gen.ts", "convex/_generated/**"] } diff --git a/bun.lock b/bun.lock index b4a885d..fa6d127 100644 --- a/bun.lock +++ b/bun.lock @@ -25,7 +25,6 @@ "clsx": "^2.1.1", "convex": "^1.34.1", "convex-helpers": "^0.1.115", - "next-themes": "^0.4.6", "react": "^19.2.5", "react-dom": "^19.2.5", "sonner": "^2.0.7", @@ -1088,8 +1087,6 @@ "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], - "nf3": ["nf3@0.3.16", "", {}, "sha512-Gs0xRPpUm2nDkqbi40NJ9g7qDIcjcJzgExiydnq6LAyqhI2jfno8wG3NKTL+IiJsx799UHOb1CnSd4Wg4SG4Pw=="], "nitro": ["nitro@3.0.260311-beta", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.4", "db0": "^0.3.4", "env-runner": "^0.1.6", "h3": "^2.0.1-rc.16", "hookable": "^6.0.1", "nf3": "^0.3.11", "ocache": "^0.1.2", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "rolldown": "^1.0.0-rc.8", "srvx": "^0.11.9", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.6" }, "peerDependencies": { "dotenv": "*", "giget": "*", "jiti": "^2.6.1", "rollup": "^4.59.0", "vite": "^7 || ^8 || >=8.0.0-0", "xml2js": "^0.6.2", "zephyr-agent": "^0.1.15" }, "optionalPeers": ["dotenv", "giget", "jiti", "rollup", "vite", "xml2js", "zephyr-agent"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-0o0fJ9LUh4WKUqJNX012jyieUOtMCnadkNDWr0mHzdraoHpJP/1CGNefjRyZyMXSpoJfwoWdNEZu2iGf35TUvQ=="], diff --git a/package.json b/package.json index a23bb1e..7af6e4a 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,6 @@ "clsx": "^2.1.1", "convex": "^1.34.1", "convex-helpers": "^0.1.115", - "next-themes": "^0.4.6", "react": "^19.2.5", "react-dom": "^19.2.5", "sonner": "^2.0.7", diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx index 3e6aaec..7b76b63 100644 --- a/src/components/theme-provider.tsx +++ b/src/components/theme-provider.tsx @@ -1,5 +1,5 @@ import { ScriptOnce } from "@tanstack/react-router" -import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react" +import { createContext, useContext, useEffect, useState } from "react" type Theme = "dark" | "light" | "system" @@ -14,8 +14,11 @@ type ThemeProviderState = { setTheme: (theme: Theme) => void } -function buildThemeScript(storageKey: string, defaultTheme: Theme) { - return `(function(){try{var k=${JSON.stringify(storageKey)};var d=${JSON.stringify(defaultTheme)};var t=localStorage.getItem(k);if(t!=='light'&&t!=='dark'&&t!=='system'){t=d}var m=matchMedia('(prefers-color-scheme: dark)').matches;var r=t==='system'?(m?'dark':'light'):t;var e=document.documentElement;e.classList.add(r);e.style.colorScheme=r}catch(e){}})();` +function getThemeScript(storageKey: string, defaultTheme: Theme) { + const key = JSON.stringify(storageKey) + const fallback = JSON.stringify(defaultTheme) + + return `(function(){try{var t=localStorage.getItem(${key});if(t!=='light'&&t!=='dark'&&t!=='system'){t=${fallback}}var d=matchMedia('(prefers-color-scheme: dark)').matches;var r=t==='system'?(d?'dark':'light'):t;var e=document.documentElement;e.classList.add(r);e.style.colorScheme=r}catch(e){}})();` } const ThemeProviderContext = createContext({ @@ -23,68 +26,66 @@ const ThemeProviderContext = createContext({ setTheme: () => {}, }) -function resolveTheme(theme: Theme): "dark" | "light" { - if (theme !== "system") return theme - return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" -} - -function applyTheme(resolved: "dark" | "light") { +function applyTheme(theme: Theme) { const root = document.documentElement root.classList.remove("light", "dark") + + const resolved = + theme === "system" + ? window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light" + : theme + root.classList.add(resolved) root.style.colorScheme = resolved } -function isTheme(value: unknown): value is Theme { - return value === "light" || value === "dark" || value === "system" -} - export function ThemeProvider({ children, defaultTheme = "system", storageKey = "theme", }: ThemeProviderProps) { - const [theme, setThemeState] = useState(() => { - if (typeof window === "undefined") return defaultTheme + const [theme, setThemeState] = useState(defaultTheme) + const [mounted, setMounted] = useState(false) + + useEffect(() => { const stored = localStorage.getItem(storageKey) - return isTheme(stored) ? stored : defaultTheme - }) + setThemeState( + stored === "light" || stored === "dark" || stored === "system" ? stored : defaultTheme, + ) + setMounted(true) + }, [defaultTheme, storageKey]) - const mounted = useRef(false) useEffect(() => { - if (!mounted.current) { - mounted.current = true - return - } - applyTheme(resolveTheme(theme)) - }, [theme]) + if (!mounted) return + applyTheme(theme) + }, [theme, mounted]) useEffect(() => { - if (theme !== "system") return undefined + if (!mounted || theme !== "system") return undefined + const media = window.matchMedia("(prefers-color-scheme: dark)") - const onChange = () => applyTheme(media.matches ? "dark" : "light") + const onChange = () => applyTheme("system") media.addEventListener("change", onChange) return () => media.removeEventListener("change", onChange) - }, [theme]) - - const setTheme = useCallback( - (next: Theme) => { - localStorage.setItem(storageKey, next) - setThemeState(next) - }, - [storageKey], - ) + }, [theme, mounted]) - const value = useMemo(() => ({ theme, setTheme }), [theme, setTheme]) + const setTheme = (next: Theme) => { + localStorage.setItem(storageKey, next) + setThemeState(next) + } return ( - - {buildThemeScript(storageKey, defaultTheme)} + + {getThemeScript(storageKey, defaultTheme)} {children} ) } export function useTheme() { - return useContext(ThemeProviderContext) + const context = useContext(ThemeProviderContext) + if (context === undefined) throw new Error("useTheme must be used within a ThemeProvider") + return context } diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx index 673dae3..fd3c055 100644 --- a/src/components/ui/sonner.tsx +++ b/src/components/ui/sonner.tsx @@ -1,31 +1,24 @@ -"use client" - -import { useTheme } from "next-themes" -import { Toaster as Sonner, type ToasterProps } from "sonner" -import { HugeiconsIcon } from "@hugeicons/react" import Alert02Icon from "@hugeicons/core-free-icons/Alert02Icon" import CheckmarkCircle02Icon from "@hugeicons/core-free-icons/CheckmarkCircle02Icon" import InformationCircleIcon from "@hugeicons/core-free-icons/InformationCircleIcon" import Loading03Icon from "@hugeicons/core-free-icons/Loading03Icon" import MultiplicationSignCircleIcon from "@hugeicons/core-free-icons/MultiplicationSignCircleIcon" +import { HugeiconsIcon } from "@hugeicons/react" +import { Toaster as Sonner, type ToasterProps } from "sonner" + +import { useTheme } from "@/components/theme-provider" const Toaster = ({ ...props }: ToasterProps) => { - const { theme = "system" } = useTheme() + const { theme } = useTheme() return ( - ), - info: ( - - ), - warning: ( - - ), + success: , + info: , + warning: , error: ( ),