From 30a0939c4ca0e6f9e4cf480a0e444011a6118d5c Mon Sep 17 00:00:00 2001 From: typist Date: Tue, 28 Oct 2025 22:28:20 +0800 Subject: [PATCH] feat: implement theme provider and toggle functionality - Added a `ThemeProvider` component to manage theme state and persist it in local storage. - Introduced a `ModeToggle` component for switching between light, dark, and system themes. - Updated the `Layout` component to include the `ThemeProvider`. - Enhanced the `AppSidebar` to include the `ModeToggle` for user accessibility. --- src/components/sidebar/index.tsx | 9 +++- src/components/theme/provider.tsx | 73 +++++++++++++++++++++++++++++++ src/components/theme/toggle.tsx | 38 ++++++++++++++++ src/layout.tsx | 27 +++++++----- src/main.tsx | 1 + 5 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 src/components/theme/provider.tsx create mode 100644 src/components/theme/toggle.tsx diff --git a/src/components/sidebar/index.tsx b/src/components/sidebar/index.tsx index 85fbc54..a6dde77 100644 --- a/src/components/sidebar/index.tsx +++ b/src/components/sidebar/index.tsx @@ -1,6 +1,8 @@ import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar"; import { tools } from "@/components/tool"; import { Link } from "react-router-dom"; +import { ModeToggle } from "@/components/theme/toggle"; +import { Button } from "../ui/button"; export const AppSidebar = () => ( @@ -28,8 +30,11 @@ export const AppSidebar = () => ( - - contact us + + + ) \ No newline at end of file diff --git a/src/components/theme/provider.tsx b/src/components/theme/provider.tsx new file mode 100644 index 0000000..ee5f2db --- /dev/null +++ b/src/components/theme/provider.tsx @@ -0,0 +1,73 @@ +import { createContext, useContext, useEffect, useState } from "react" + +type Theme = "dark" | "light" | "system" + +type ThemeProviderProps = { + children: React.ReactNode + 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 +} \ No newline at end of file diff --git a/src/components/theme/toggle.tsx b/src/components/theme/toggle.tsx new file mode 100644 index 0000000..3ed6435 --- /dev/null +++ b/src/components/theme/toggle.tsx @@ -0,0 +1,38 @@ +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 "./provider" + +export function ModeToggle() { + const { setTheme } = useTheme() + + return ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ) +} \ No newline at end of file diff --git a/src/layout.tsx b/src/layout.tsx index 868feeb..726a539 100644 --- a/src/layout.tsx +++ b/src/layout.tsx @@ -1,20 +1,23 @@ import type { FC } from "react" import { Outlet } from "react-router-dom"; +import { ThemeProvider } from "@/components/theme/provider" import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar" import { AppSidebar } from "@/components/sidebar"; export const Layout: FC = () => ( - - -
- -
- -
-
-
+ + + +
+ +
+ +
+
+
+
); \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index b5a9d99..2f36f66 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client' import { Toaster } from '@/components/ui/sonner' + import './index.css' import { AppRouter } from './router'