diff --git a/apps/desktop/renderer/src/components/theme-provider.tsx b/apps/desktop/renderer/src/components/theme-provider.tsx index 76c19b4..e926fea 100644 --- a/apps/desktop/renderer/src/components/theme-provider.tsx +++ b/apps/desktop/renderer/src/components/theme-provider.tsx @@ -1,19 +1,34 @@ import { createContext, useContext, useEffect, useState } from "react"; -const ThemeContext = createContext({ theme: "light", setTheme: () => {} }); +const ThemeContext = createContext({ theme: "light", setTheme: () => {}, resolvedTheme: "light" }); + +function getSystemTheme() { + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; +} export function ThemeProvider({ children, defaultTheme = "light", storageKey = "ui-theme" }) { const [theme, setTheme] = useState(() => localStorage.getItem(storageKey) || defaultTheme); + const [systemTheme, setSystemTheme] = useState(getSystemTheme); + const resolvedTheme = theme === "system" ? systemTheme : theme; useEffect(() => { const root = document.documentElement; root.classList.remove("light", "dark"); - root.classList.add(theme); + root.classList.add(resolvedTheme); localStorage.setItem(storageKey, theme); - }, [theme, storageKey]); + }, [theme, resolvedTheme, storageKey]); + + useEffect(() => { + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const handleChange = () => setSystemTheme(getSystemTheme()); + + handleChange(); + mediaQuery.addEventListener("change", handleChange); + return () => mediaQuery.removeEventListener("change", handleChange); + }, []); return ( - + {children} ); diff --git a/apps/desktop/renderer/src/components/theme-toggle.tsx b/apps/desktop/renderer/src/components/theme-toggle.tsx index 55c881c..2bed7bf 100644 --- a/apps/desktop/renderer/src/components/theme-toggle.tsx +++ b/apps/desktop/renderer/src/components/theme-toggle.tsx @@ -1,20 +1,80 @@ -import { Moon, Sun } from "lucide-react"; +import { Check, Monitor, Moon, Sun } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { cn } from "../lib/utils"; import { useI18n } from "./i18n-provider"; import { useTheme } from "./theme-provider"; +const themeOptions = [ + { value: "light", labelKey: "theme.light", icon: Sun }, + { value: "dark", labelKey: "theme.dark", icon: Moon }, + { value: "system", labelKey: "theme.system", icon: Monitor } +]; + export function ThemeToggle() { - const { theme, setTheme } = useTheme(); + const { theme, setTheme, resolvedTheme } = useTheme(); const { t } = useI18n(); + const [open, setOpen] = useState(false); + const menuRef = useRef(null); + const TriggerIcon = theme === "system" ? Monitor : resolvedTheme === "dark" ? Sun : Moon; + + useEffect(() => { + if (!open) return; + + const handlePointerDown = (event) => { + if (menuRef.current?.contains(event.target)) return; + setOpen(false); + }; + + document.addEventListener("pointerdown", handlePointerDown); + return () => document.removeEventListener("pointerdown", handlePointerDown); + }, [open]); return ( - +
+ + + {open && ( +
+ {themeOptions.map((option) => { + const Icon = option.icon; + const selected = theme === option.value; + + return ( + + ); + })} +
+ )} +
); } diff --git a/apps/desktop/renderer/src/lib/i18n.ts b/apps/desktop/renderer/src/lib/i18n.ts index 67bdcea..00fa177 100644 --- a/apps/desktop/renderer/src/lib/i18n.ts +++ b/apps/desktop/renderer/src/lib/i18n.ts @@ -2,6 +2,9 @@ export const messages = { en: { "nav.settings": "Settings", "theme.toggle": "Toggle theme", + "theme.light": "Light", + "theme.dark": "Dark", + "theme.system": "System", "language.switch": "Switch language", "status.completed": "Completed", "status.paused": "Paused", @@ -359,6 +362,9 @@ export const messages = { "zh-CN": { "nav.settings": "设置", "theme.toggle": "切换主题", + "theme.light": "浅色", + "theme.dark": "深色", + "theme.system": "跟随系统", "language.switch": "切换语言", "status.completed": "已完成", "status.paused": "已暂停",