feat: add dark/light theme toggle

- Add light theme CSS variables in globals.css
- Create ThemeProvider with localStorage persistence (lib/theme.tsx)
- Add ThemeSwitcher button (☀️/🌙) to sidebar
- Wrap app with ThemeProvider in providers.tsx
This commit is contained in:
xmanrui
2026-02-23 07:09:45 +08:00
parent fcfdf6b587
commit 7915a913c9
4 changed files with 82 additions and 1 deletions
+12
View File
@@ -12,6 +12,18 @@
--orange: #fb923c;
}
[data-theme="light"] {
--bg: #f8fafc;
--card: #ffffff;
--border: #e2e8f0;
--text: #0f172a;
--text-muted: #64748b;
--accent: #0284c7;
--accent2: #7c3aed;
--green: #16a34a;
--orange: #ea580c;
}
body {
background: var(--bg);
color: var(--text);
+6 -1
View File
@@ -1,8 +1,13 @@
"use client";
import { I18nProvider } from "@/lib/i18n";
import { ThemeProvider } from "@/lib/theme";
import { ReactNode } from "react";
export function Providers({ children }: { children: ReactNode }) {
return <I18nProvider>{children}</I18nProvider>;
return (
<ThemeProvider>
<I18nProvider>{children}</I18nProvider>
</ThemeProvider>
);
}
+2
View File
@@ -4,6 +4,7 @@ import { useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useI18n, LanguageSwitcher } from "@/lib/i18n";
import { ThemeSwitcher } from "@/lib/theme";
const NAV_ITEMS = [
{
@@ -65,6 +66,7 @@ export function Sidebar() {
</Link>
<div className="flex items-center gap-2">
<LanguageSwitcher />
<ThemeSwitcher />
<button
onClick={() => setCollapsed(true)}
className="text-[var(--text-muted)] hover:text-[var(--text)] transition-colors text-lg"
+62
View File
@@ -0,0 +1,62 @@
"use client";
import { createContext, useContext, useState, useCallback, useEffect, ReactNode } from "react";
export type Theme = "dark" | "light";
interface ThemeContextType {
theme: Theme;
setTheme: (t: Theme) => void;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType>({
theme: "dark",
setTheme: () => {},
toggleTheme: () => {},
});
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>("dark");
useEffect(() => {
const saved = localStorage.getItem("theme") as Theme;
if (saved === "light" || saved === "dark") {
setThemeState(saved);
document.documentElement.setAttribute("data-theme", saved);
}
}, []);
const setTheme = useCallback((t: Theme) => {
setThemeState(t);
document.documentElement.setAttribute("data-theme", t);
localStorage.setItem("theme", t);
}, []);
const toggleTheme = useCallback(() => {
setTheme(theme === "dark" ? "light" : "dark");
}, [theme, setTheme]);
return (
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
export function ThemeSwitcher() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
className="px-2 py-1.5 rounded-lg bg-[var(--bg)] border border-[var(--border)] text-sm hover:border-[var(--accent)] transition cursor-pointer"
title={theme === "dark" ? "切换到亮色模式" : "切换到暗色模式"}
>
{theme === "dark" ? "☀️" : "🌙"}
</button>
);
}