Refactor theme and language providers to use global stores (#728)

This commit is contained in:
shenlan 2025-12-02 15:52:54 +08:00 committed by GitHub
parent 8c687ef53a
commit 6b7bdc7dcd
9 changed files with 74 additions and 92 deletions

View File

@ -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 (
<LanguageProvider>
<UserProvider>
<ThemeProvider>{children}</ThemeProvider>
</UserProvider>
</LanguageProvider>
<ThemeProvider>
<LanguageProvider>
<UserProvider>{children}</UserProvider>
</LanguageProvider>
</ThemeProvider>
)
}

View File

@ -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() {
<p className="text-xs font-medium uppercase tracking-wide text-[var(--color-primary)]">Theme system</p>
<h1 className="text-3xl font-bold text-[var(--color-heading)]">主题演示与调试</h1>
<p className="max-w-3xl text-sm text-[var(--color-text-subtle)]">
当前主题:<span className="font-semibold text-[var(--color-text)]">{theme}</span>。使用下方卡片可以即时切换主题,并查看核心设计 token 的取值情况,帮助校验多主题下的界面一致性。
当前主题:<span className="font-semibold text-[var(--color-text)]">{resolvedTheme}</span>。使用下方卡片可以即时切换主题,并查看核心设计 token 的取值情况,帮助校验多主题下的界面一致性。
</p>
</header>

View File

@ -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: '#' },

View File

@ -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<ThemeContextValue | undefined>(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<ThemeContextValue>(
() => ({
theme,
preference,
tokens,
colorScheme,
availableThemes,
setPreference,
toggleTheme,
}),
[availableThemes, colorScheme, preference, setPreference, theme, tokens, toggleTheme],
)
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
}
export function useThemeContext() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useThemeContext must be used within a ThemeProvider')
}
return context
return <>{children}</>
}

View File

@ -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'

View File

@ -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<ThemeState & ThemeActions>((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)
})
},
}))

View File

@ -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,
}
}

View File

@ -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<ThemePreference>(() => preference ?? 'system', [preference])
@ -133,7 +135,7 @@ export default function ThemePreferenceCard() {
<button
key={option.value}
type="button"
onClick={() => setPreference(option.value)}
onClick={() => setTheme(option.value)}
className={`flex flex-1 flex-col items-start gap-2 rounded-[var(--radius-xl)] border px-4 py-4 text-left transition-all md:min-h-[140px] ${
isSelected
? 'border-[color:var(--color-primary)] bg-white text-[var(--color-primary)] shadow-[var(--shadow-md)] dark:bg-[color:var(--color-surface-elevated)]'

View File

@ -1,6 +1,6 @@
'use client'
import { useTheme } from '@components/theme'
import { useThemeStore } from '@components/theme'
import { useLanguage } from '../../i18n/LanguageProvider'
@ -33,7 +33,10 @@ const sectionCardClass =
export default function Homepage() {
const { language, setLanguage } = useLanguage()
const { theme, preference, setPreference, toggleTheme } = useTheme()
const theme = useThemeStore((state) => state.theme)
const resolvedTheme = useThemeStore((state) => state.resolvedTheme)
const setTheme = useThemeStore((state) => state.setTheme)
const toggleTheme = useThemeStore((state) => state.toggleTheme)
const quickActions: QuickAction[] = [
{
@ -257,16 +260,16 @@ export default function Homepage() {
] as const
).map((option) => {
const active =
preference === option.value ||
(option.value !== 'system' && preference === 'system' && theme === option.value)
theme === option.value ||
(option.value !== 'system' && theme === 'system' && resolvedTheme === option.value)
return (
<button
key={option.value}
type="button"
onClick={() =>
option.value === 'light' || option.value === 'dark'
? setPreference(option.value)
: setPreference('system')
? setTheme(option.value)
: setTheme('system')
}
className={`px-3 py-1 text-xs font-semibold transition-colors ${
active
@ -285,10 +288,10 @@ export default function Homepage() {
className="rounded-full border border-[color:var(--color-surface-border)] px-3 py-1 text-[11px] font-semibold text-[color:var(--color-text-subtle)] transition-colors hover:border-[color:var(--color-primary-border)] hover:text-[color:var(--color-primary)]"
>
{language === 'zh'
? theme === 'dark'
? resolvedTheme === 'dark'
? '切换为浅色'
: '切换为深色'
: theme === 'dark'
: resolvedTheme === 'dark'
? 'Switch to light'
: 'Switch to dark'}
</button>