🎨 fix(web): align UI and charts with theme tokens and presets
Improve theme switching fidelity (including system preference), extend design tokens so color presets tint real surfaces—not only primary/chrome—and refactor shared badges, tables, and dashboard visuals to semantic colors. Wire VChart series colors to `--chart-*` with safe fallbacks. **Changes** - **Theme runtime** (`theme-provider.tsx`): Validate stored theme cookie; keep `resolvedTheme` in sync with DOM + `(prefers-color-scheme)`; `resetTheme` respects `defaultTheme`; memoized context value. - **Tokens** (`theme.css`): Add `--success|warning|info|neutral` (+ foregrounds) and map them under `@theme inline` for Tailwind utilities. - **Presets** (`theme-presets.css`): For non-`default` presets, derive `card`, `popover`, `muted`, `accent`, `border`, `input`, and sidebar tokens from `--primary`/`--background`; map semantic status colors to preset chart variables. - **Components**: `status-badge`, `colors` (avatars, announcements), `copy-button`, `group-badge`, `data-table` row styles, `sidebar` outline shadow (fix `var(--sidebar-border)` usage), ai-elements tool/web-preview status colors. - **Dashboard**: Latency/API helpers and overview fragments use semantic tokens; `charts.ts` reads `--chart-1`…`--chart-5` from computed styles with fallbacks; `processChartData` / `processUserChartData` accept optional `themeKey` for preset churn; chart components pass `customization.preset` and bump `VChart` keys. **Verification** - `bun run typecheck`
This commit is contained in:
+59
-41
@@ -1,4 +1,11 @@
|
||||
import { createContext, useContext, useEffect, useState, useMemo } from 'react'
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { getCookie, setCookie, removeCookie } from '@/lib/cookies'
|
||||
|
||||
type Theme = 'dark' | 'light' | 'system'
|
||||
@@ -7,6 +14,7 @@ type ResolvedTheme = Exclude<Theme, 'system'>
|
||||
const DEFAULT_THEME = 'system'
|
||||
const THEME_COOKIE_NAME = 'vite-ui-theme'
|
||||
const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 // 1 year
|
||||
const THEMES = new Set<Theme>(['dark', 'light', 'system'])
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode
|
||||
@@ -32,66 +40,76 @@ const initialState: ThemeProviderState = {
|
||||
|
||||
const ThemeContext = createContext<ThemeProviderState>(initialState)
|
||||
|
||||
function getSystemTheme(): ResolvedTheme {
|
||||
if (typeof window === 'undefined') return 'light'
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light'
|
||||
}
|
||||
|
||||
function resolveTheme(theme: Theme): ResolvedTheme {
|
||||
return theme === 'system' ? getSystemTheme() : theme
|
||||
}
|
||||
|
||||
function getStoredTheme(storageKey: string, fallback: Theme): Theme {
|
||||
const storedTheme = getCookie(storageKey) as Theme | undefined
|
||||
return storedTheme && THEMES.has(storedTheme) ? storedTheme : fallback
|
||||
}
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = DEFAULT_THEME,
|
||||
storageKey = THEME_COOKIE_NAME,
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, _setTheme] = useState<Theme>(
|
||||
() => (getCookie(storageKey) as Theme) || defaultTheme
|
||||
const [theme, _setTheme] = useState<Theme>(() =>
|
||||
getStoredTheme(storageKey, defaultTheme)
|
||||
)
|
||||
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() =>
|
||||
resolveTheme(getStoredTheme(storageKey, defaultTheme))
|
||||
)
|
||||
|
||||
// Optimized: Memoize the resolved theme calculation to prevent unnecessary re-computations
|
||||
const resolvedTheme = useMemo((): ResolvedTheme => {
|
||||
if (theme === 'system') {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light'
|
||||
}
|
||||
return theme as ResolvedTheme
|
||||
}, [theme])
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
|
||||
const applyTheme = (currentResolvedTheme: ResolvedTheme) => {
|
||||
root.classList.remove('light', 'dark') // Remove existing theme classes
|
||||
root.classList.add(currentResolvedTheme) // Add the new theme class
|
||||
const applyTheme = () => {
|
||||
const nextResolvedTheme = theme === 'system' ? getSystemTheme() : theme
|
||||
root.classList.remove('light', 'dark')
|
||||
root.classList.add(nextResolvedTheme)
|
||||
setResolvedTheme(nextResolvedTheme)
|
||||
}
|
||||
|
||||
const handleChange = () => {
|
||||
if (theme === 'system') {
|
||||
const systemTheme = mediaQuery.matches ? 'dark' : 'light'
|
||||
applyTheme(systemTheme)
|
||||
}
|
||||
}
|
||||
applyTheme()
|
||||
|
||||
applyTheme(resolvedTheme)
|
||||
mediaQuery.addEventListener('change', applyTheme)
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange)
|
||||
return () => mediaQuery.removeEventListener('change', applyTheme)
|
||||
}, [theme])
|
||||
|
||||
return () => mediaQuery.removeEventListener('change', handleChange)
|
||||
}, [theme, resolvedTheme])
|
||||
const setTheme = useCallback(
|
||||
(theme: Theme) => {
|
||||
setCookie(storageKey, theme, THEME_COOKIE_MAX_AGE)
|
||||
_setTheme(theme)
|
||||
},
|
||||
[storageKey]
|
||||
)
|
||||
|
||||
const setTheme = (theme: Theme) => {
|
||||
setCookie(storageKey, theme, THEME_COOKIE_MAX_AGE)
|
||||
_setTheme(theme)
|
||||
}
|
||||
|
||||
const resetTheme = () => {
|
||||
const resetTheme = useCallback(() => {
|
||||
removeCookie(storageKey)
|
||||
_setTheme(DEFAULT_THEME)
|
||||
}
|
||||
_setTheme(defaultTheme)
|
||||
}, [defaultTheme, storageKey])
|
||||
|
||||
const contextValue = {
|
||||
defaultTheme,
|
||||
resolvedTheme,
|
||||
resetTheme,
|
||||
theme,
|
||||
setTheme,
|
||||
}
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
defaultTheme,
|
||||
resolvedTheme,
|
||||
resetTheme,
|
||||
theme,
|
||||
setTheme,
|
||||
}),
|
||||
[defaultTheme, resolvedTheme, resetTheme, theme, setTheme]
|
||||
)
|
||||
|
||||
return (
|
||||
<ThemeContext value={contextValue} {...props}>
|
||||
|
||||
Reference in New Issue
Block a user