From 5ecfe13234b6353365a08a28ac7cd4d83e655625 Mon Sep 17 00:00:00 2001 From: ecwu Date: Fri, 20 Dec 2024 18:28:33 +0800 Subject: [PATCH] Add theme toggle functionality with ThemeProvider and ModeToggle component --- src/components/mode-toggle.tsx | 37 ++++++++++++++++ src/components/theme-provider.tsx | 74 +++++++++++++++++++++++++++++++ src/main.tsx | 12 +++-- src/pages/App.tsx | 3 +- 4 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 src/components/mode-toggle.tsx create mode 100644 src/components/theme-provider.tsx diff --git a/src/components/mode-toggle.tsx b/src/components/mode-toggle.tsx new file mode 100644 index 0000000..6490f27 --- /dev/null +++ b/src/components/mode-toggle.tsx @@ -0,0 +1,37 @@ +import { Moon, Sun } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { useTheme } from "@/components/theme-provider"; + +export function ModeToggle() { + const { setTheme } = useTheme(); + + return ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ); +} diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx new file mode 100644 index 0000000..75eeffc --- /dev/null +++ b/src/components/theme-provider.tsx @@ -0,0 +1,74 @@ +import { createContext } from "preact"; +import { useContext, useEffect, useState } from "preact/hooks"; + +type Theme = "dark" | "light" | "system"; + +type ThemeProviderProps = { + children: preact.VNode; + defaultTheme?: Theme; + storageKey?: string; +}; + +type ThemeProviderState = { + theme: Theme; + setTheme: (theme: Theme) => void; +}; + +const initialState: ThemeProviderState = { + theme: "system", + setTheme: () => null, +}; + +const ThemeProviderContext = createContext(initialState); + +export function ThemeProvider({ + children, + defaultTheme = "system", + storageKey = "vite-ui-theme", + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState( + () => (localStorage.getItem(storageKey) as Theme) || defaultTheme + ); + + 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; + } + + root.classList.add(theme); + }, [theme]); + + const value = { + theme, + setTheme: (theme: Theme) => { + localStorage.setItem(storageKey, theme); + setTheme(theme); + }, + }; + + return ( + + {children} + + ); +} + +export const useTheme = () => { + const context = useContext(ThemeProviderContext); + + if (context === undefined) + throw new Error("useTheme must be used within a ThemeProvider"); + + return context; +}; diff --git a/src/main.tsx b/src/main.tsx index 1cd9d59..ba96249 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,6 +5,7 @@ import { App } from "@/pages/App"; import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate"; import { SidebarProvider } from "@/components/ui/sidebar"; import { Toaster } from "@/components/ui/toaster"; +import { ThemeProvider } from "@/components/theme-provider"; function Base() { const [langCode, _setLangCode] = useState("en-US"); @@ -47,11 +48,14 @@ function Base() { return ( /* @ts-ignore */ + - - - - + + + + + + ); } diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 00fb6da..9f9f061 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -71,6 +71,7 @@ import { CogIcon, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; +import { ModeToggle } from "@/components/mode-toggle"; export function App() { // init selected index @@ -396,7 +397,7 @@ export function App() { - +