feat: add dark mode support with theme toggle

This commit is contained in:
张翔
2026-02-13 15:17:42 +08:00
parent 34c96c119d
commit 8175c2f035
8 changed files with 1761 additions and 241 deletions
+74
View File
@@ -0,0 +1,74 @@
'use client';
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
type Theme = 'light' | 'dark' | 'system';
interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
resolvedTheme: 'light' | 'dark';
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
const THEME_STORAGE_KEY = 'ruixin-theme';
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>('system');
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
const stored = localStorage.getItem(THEME_STORAGE_KEY) as Theme | null;
if (stored && ['light', 'dark', 'system'].includes(stored)) {
setThemeState(stored);
}
}, []);
useEffect(() => {
const root = document.documentElement;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const updateTheme = () => {
let resolved: 'light' | 'dark';
if (theme === 'system') {
resolved = mediaQuery.matches ? 'dark' : 'light';
} else {
resolved = theme;
}
setResolvedTheme(resolved);
if (resolved === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
};
updateTheme();
mediaQuery.addEventListener('change', updateTheme);
return () => mediaQuery.removeEventListener('change', updateTheme);
}, [theme]);
const setTheme = (newTheme: Theme) => {
setThemeState(newTheme);
localStorage.setItem(THEME_STORAGE_KEY, newTheme);
};
return (
<ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}