feat(desktop): add resolvedTheme to theme provider

- Add resolvedTheme state to expose actual applied theme
- Listen for system theme changes when in "system" mode
- Fix mode toggle to show correct icon based on selected theme

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-02-11 17:43:18 +08:00
parent 54bc00ce3f
commit 07b8a014aa
2 changed files with 36 additions and 22 deletions

View file

@ -1,6 +1,5 @@
import { HugeiconsIcon } from "@hugeicons/react"
import { Sun03Icon, Moon02Icon, ComputerIcon } from "@hugeicons/core-free-icons"
import { Button } from "@multica/ui/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
@ -10,22 +9,15 @@ import {
import { useTheme } from "./theme-provider"
export function ModeToggle() {
const { setTheme } = useTheme()
const { theme, setTheme } = useTheme()
const icon = theme === "light" ? Sun03Icon : theme === "dark" ? Moon02Icon : ComputerIcon
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="size-8">
<HugeiconsIcon
icon={Sun03Icon}
className="size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
/>
<HugeiconsIcon
icon={Moon02Icon}
className="absolute size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
/>
<span className="sr-only">Toggle theme</span>
</Button>
<DropdownMenuTrigger className="inline-flex items-center justify-center size-8 rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
<HugeiconsIcon icon={icon} className="size-4" />
<span className="sr-only">Toggle theme</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>

View file

@ -10,16 +10,24 @@ type ThemeProviderProps = {
type ThemeProviderState = {
theme: Theme
resolvedTheme: "light" | "dark"
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: "system",
resolvedTheme: "light",
setTheme: () => null,
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
function getSystemTheme(): "light" | "dark" {
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light"
}
export function ThemeProvider({
children,
defaultTheme = "system",
@ -28,25 +36,39 @@ export function ThemeProvider({
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
)
const [resolvedTheme, setResolvedTheme] = useState<"light" | "dark">(
() => (theme === "system" ? getSystemTheme() : theme)
)
useEffect(() => {
const root = window.document.documentElement
root.classList.remove("light", "dark")
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light"
root.classList.add(systemTheme)
return
const resolved = theme === "system" ? getSystemTheme() : theme
root.classList.add(resolved)
setResolvedTheme(resolved)
}, [theme])
// Listen for system theme changes
useEffect(() => {
if (theme !== "system") return
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
const handleChange = () => {
const resolved = getSystemTheme()
const root = window.document.documentElement
root.classList.remove("light", "dark")
root.classList.add(resolved)
setResolvedTheme(resolved)
}
root.classList.add(theme)
mediaQuery.addEventListener("change", handleChange)
return () => mediaQuery.removeEventListener("change", handleChange)
}, [theme])
const value = {
theme,
resolvedTheme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme)
setTheme(theme)