From 6b7bdc7dcdaffa0410be3d327cf05b81abd5d208 Mon Sep 17 00:00:00 2001 From: shenlan Date: Tue, 2 Dec 2025 15:52:54 +0800 Subject: [PATCH] Refactor theme and language providers to use global stores (#728) --- dashboard/src/app/AppProviders.tsx | 12 ++--- dashboard/src/app/demo/theme/page.tsx | 4 +- dashboard/src/components/Footer.tsx | 5 +- .../src/components/theme/ThemeProvider.tsx | 52 +++---------------- dashboard/src/components/theme/index.ts | 3 +- dashboard/src/components/theme/store.ts | 29 ++++++----- dashboard/src/components/theme/useTheme.ts | 34 +++++++----- .../account/ThemePreferenceCard.tsx | 8 +-- dashboard/src/modules/homepage/page.tsx | 19 ++++--- 9 files changed, 74 insertions(+), 92 deletions(-) diff --git a/dashboard/src/app/AppProviders.tsx b/dashboard/src/app/AppProviders.tsx index ccd872e..8e1fc9f 100644 --- a/dashboard/src/app/AppProviders.tsx +++ b/dashboard/src/app/AppProviders.tsx @@ -2,16 +2,16 @@ import type { ReactNode } from 'react' +import { ThemeProvider } from '@components/theme' import { LanguageProvider } from '@i18n/LanguageProvider' import { UserProvider } from '@lib/userStore' -import { ThemeProvider } from '@components/theme' export function AppProviders({ children }: { children: ReactNode }) { return ( - - - {children} - - + + + {children} + + ) } diff --git a/dashboard/src/app/demo/theme/page.tsx b/dashboard/src/app/demo/theme/page.tsx index a67dc81..035bf6f 100644 --- a/dashboard/src/app/demo/theme/page.tsx +++ b/dashboard/src/app/demo/theme/page.tsx @@ -17,7 +17,7 @@ const COLOR_TOKENS: Array<{ key: keyof ThemeTokens['colors']; label: string }> = ] export default function ThemeShowcasePage() { - const { theme, tokens } = useTheme() + const { resolvedTheme, tokens } = useTheme() const { colors } = tokens return ( @@ -26,7 +26,7 @@ export default function ThemeShowcasePage() {

Theme system

主题演示与调试

- 当前主题:{theme}。使用下方卡片可以即时切换主题,并查看核心设计 token 的取值情况,帮助校验多主题下的界面一致性。 + 当前主题:{resolvedTheme}。使用下方卡片可以即时切换主题,并查看核心设计 token 的取值情况,帮助校验多主题下的界面一致性。

diff --git a/dashboard/src/components/Footer.tsx b/dashboard/src/components/Footer.tsx index 6e0d62e..672eafb 100644 --- a/dashboard/src/components/Footer.tsx +++ b/dashboard/src/components/Footer.tsx @@ -1,10 +1,11 @@ 'use client' import { BookOpen, Github, Globe, Link, MessageCircle, Sparkles, Moon, Sun } from 'lucide-react' -import { useTheme } from '@theme/useTheme' +import { useThemeStore } from '@components/theme' export default function Footer() { - const { isDark, toggleTheme } = useTheme() + const isDark = useThemeStore((state) => state.isDark) + const toggleTheme = useThemeStore((state) => state.toggleTheme) const socials = [ { label: 'GitHub', icon: Github, href: 'https://github.com/CloudNativeSuite/' }, { label: 'Docs', icon: BookOpen, href: '#' }, diff --git a/dashboard/src/components/theme/ThemeProvider.tsx b/dashboard/src/components/theme/ThemeProvider.tsx index f2e1beb..2c2a856 100644 --- a/dashboard/src/components/theme/ThemeProvider.tsx +++ b/dashboard/src/components/theme/ThemeProvider.tsx @@ -1,21 +1,9 @@ 'use client' -import { createContext, useContext, useEffect, useMemo } from 'react' +import { useEffect } from 'react' import { getThemeDefinition, useThemeStore } from './store' -import type { ThemeName, ThemePreference, ThemeTokens } from './types' - -interface ThemeContextValue { - theme: ThemeName - preference: ThemePreference - tokens: ThemeTokens - colorScheme: 'light' | 'dark' - availableThemes: ThemeName[] - setPreference: (preference: ThemePreference) => void - toggleTheme: () => void -} - -const ThemeContext = createContext(undefined) +import type { ThemeName, ThemePreference } from './types' function applyTheme(theme: ThemeName) { if (typeof document === 'undefined') { @@ -26,6 +14,7 @@ function applyTheme(theme: ThemeName) { const root = document.documentElement root.dataset.theme = definition.name root.style.setProperty('color-scheme', definition.colorScheme) + root.classList.toggle('dark', definition.colorScheme === 'dark') const body = document.body if (body) { @@ -54,13 +43,7 @@ export interface ThemeProviderProps { } export function ThemeProvider({ children, initialPreference = 'system' }: ThemeProviderProps) { - const theme = useThemeStore((state) => state.theme) - const preference = useThemeStore((state) => state.preference) - const tokens = useThemeStore((state) => state.tokens) - const colorScheme = useThemeStore((state) => state.colorScheme) - const availableThemes = useThemeStore((state) => state.availableThemes) - const setPreference = useThemeStore((state) => state.setPreference) - const toggleTheme = useThemeStore((state) => state.toggleTheme) + const resolvedTheme = useThemeStore((state) => state.resolvedTheme) const hydrate = useThemeStore((state) => state.hydrate) const setSystemTheme = useThemeStore((state) => state.setSystemTheme) @@ -69,8 +52,8 @@ export function ThemeProvider({ children, initialPreference = 'system' }: ThemeP }, [hydrate, initialPreference]) useEffect(() => { - applyTheme(theme) - }, [theme]) + applyTheme(resolvedTheme) + }, [resolvedTheme]) useEffect(() => { if (typeof window === 'undefined') { @@ -88,26 +71,5 @@ export function ThemeProvider({ children, initialPreference = 'system' }: ThemeP } }, [setSystemTheme]) - const value = useMemo( - () => ({ - theme, - preference, - tokens, - colorScheme, - availableThemes, - setPreference, - toggleTheme, - }), - [availableThemes, colorScheme, preference, setPreference, theme, tokens, toggleTheme], - ) - - return {children} -} - -export function useThemeContext() { - const context = useContext(ThemeContext) - if (!context) { - throw new Error('useThemeContext must be used within a ThemeProvider') - } - return context + return <>{children} } diff --git a/dashboard/src/components/theme/index.ts b/dashboard/src/components/theme/index.ts index b24772e..1bee39e 100644 --- a/dashboard/src/components/theme/index.ts +++ b/dashboard/src/components/theme/index.ts @@ -1,5 +1,6 @@ -export { ThemeProvider, useThemeContext } from './ThemeProvider' +export { ThemeProvider } from './ThemeProvider' export { useTheme } from './useTheme' +export { useThemeStore } from './store' export { lightTheme } from './light' export { darkTheme } from './dark' export type { ThemeDefinition, ThemeName, ThemePreference, ThemeTokens } from './types' diff --git a/dashboard/src/components/theme/store.ts b/dashboard/src/components/theme/store.ts index 14d344c..03249a8 100644 --- a/dashboard/src/components/theme/store.ts +++ b/dashboard/src/components/theme/store.ts @@ -47,28 +47,30 @@ function persistPreference(preference: ThemePreference) { window.localStorage.setItem(STORAGE_KEY, preference) } -function buildThemeState(preference: ThemePreference, theme: ThemeName): ThemeState { - const definition = themeRegistry[theme] +function buildThemeState(themePreference: ThemePreference, resolvedTheme: ThemeName): ThemeState { + const definition = themeRegistry[resolvedTheme] return { - preference, - theme, + theme: themePreference, + resolvedTheme, tokens: definition.tokens, colorScheme: definition.colorScheme, availableThemes, + isDark: resolvedTheme === 'dark', } } export type ThemeState = { - preference: ThemePreference - theme: ThemeName + theme: ThemePreference + resolvedTheme: ThemeName tokens: ThemeTokens colorScheme: 'light' | 'dark' availableThemes: ThemeName[] + isDark: boolean } export type ThemeActions = { hydrate: (initialPreference: ThemePreference) => void - setPreference: (preference: ThemePreference) => void + setTheme: (theme: ThemePreference) => void toggleTheme: () => void setSystemTheme: (theme: ThemeName) => void } @@ -80,21 +82,22 @@ export const useThemeStore = create((set, get) => ({ const resolvedTheme = preference === 'system' ? resolveSystemTheme() : preference set(buildThemeState(preference, resolvedTheme)) }, - setPreference: (preference) => { + setTheme: (preference) => { const resolvedTheme = preference === 'system' ? resolveSystemTheme() : preference persistPreference(preference) set(buildThemeState(preference, resolvedTheme)) }, toggleTheme: () => { - const nextTheme = get().theme === 'light' ? 'dark' : 'light' - get().setPreference(nextTheme) + const { resolvedTheme, setTheme, theme } = get() + const nextTheme = resolvedTheme === 'dark' ? 'light' : 'dark' + setTheme(theme === 'system' ? nextTheme : (theme === 'dark' ? 'light' : 'dark')) }, - setSystemTheme: (theme) => { + setSystemTheme: (resolvedTheme) => { set((state) => { - if (state.preference !== 'system') { + if (state.theme !== 'system') { return state } - return buildThemeState('system', theme) + return buildThemeState('system', resolvedTheme) }) }, })) diff --git a/dashboard/src/components/theme/useTheme.ts b/dashboard/src/components/theme/useTheme.ts index b82d98d..a4fd3d3 100644 --- a/dashboard/src/components/theme/useTheme.ts +++ b/dashboard/src/components/theme/useTheme.ts @@ -1,6 +1,4 @@ -import { useMemo } from 'react' - -import { useThemeContext } from './ThemeProvider' +import { useThemeStore } from './store' import type { ThemeName, ThemePreference, ThemeTokens } from './types' export interface UseThemeResult { @@ -9,18 +7,30 @@ export interface UseThemeResult { tokens: ThemeTokens colorScheme: 'light' | 'dark' availableThemes: ThemeName[] - setPreference: (preference: ThemePreference) => void + setTheme: (preference: ThemePreference) => void toggleTheme: () => void isDark: boolean + resolvedTheme: ThemeName } export function useTheme(): UseThemeResult { - const context = useThemeContext() - return useMemo( - () => ({ - ...context, - isDark: context.theme === 'dark', - }), - [context], - ) + const theme = useThemeStore((state) => state.theme) + const resolvedTheme = useThemeStore((state) => state.resolvedTheme) + const tokens = useThemeStore((state) => state.tokens) + const colorScheme = useThemeStore((state) => state.colorScheme) + const availableThemes = useThemeStore((state) => state.availableThemes) + const setTheme = useThemeStore((state) => state.setTheme) + const toggleTheme = useThemeStore((state) => state.toggleTheme) + const isDark = useThemeStore((state) => state.isDark) + + return { + theme, + resolvedTheme, + tokens, + colorScheme, + availableThemes, + setTheme, + toggleTheme, + isDark, + } } diff --git a/dashboard/src/modules/extensions/builtin/user-center/account/ThemePreferenceCard.tsx b/dashboard/src/modules/extensions/builtin/user-center/account/ThemePreferenceCard.tsx index a02348c..ff11bad 100644 --- a/dashboard/src/modules/extensions/builtin/user-center/account/ThemePreferenceCard.tsx +++ b/dashboard/src/modules/extensions/builtin/user-center/account/ThemePreferenceCard.tsx @@ -3,7 +3,7 @@ import { useMemo } from 'react' import Card from '../components/Card' -import { darkTheme, lightTheme, useTheme } from '@components/theme' +import { darkTheme, lightTheme, useThemeStore } from '@components/theme' import type { ThemeDefinition, ThemeName, ThemePreference } from '@components/theme' const THEME_OPTIONS: Array<{ @@ -111,7 +111,9 @@ function ThemePreview({ definition, active }: { definition: ThemeDefinition; act } export default function ThemePreferenceCard() { - const { preference, theme, setPreference } = useTheme() + const preference = useThemeStore((state) => state.theme) + const theme = useThemeStore((state) => state.resolvedTheme) + const setTheme = useThemeStore((state) => state.setTheme) const normalizedPreference = useMemo(() => preference ?? 'system', [preference]) @@ -133,7 +135,7 @@ export default function ThemePreferenceCard() {