Refactor theme and language providers to use global stores (#728)
This commit is contained in:
parent
8c687ef53a
commit
6b7bdc7dcd
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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: '#' },
|
||||
|
||||
@ -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}</>
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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)
|
||||
})
|
||||
},
|
||||
}))
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)]'
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user