mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-04 07:19:35 +00:00
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`
129 lines
3.2 KiB
TypeScript
Vendored
129 lines
3.2 KiB
TypeScript
Vendored
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
} from 'react'
|
|
import { getCookie, setCookie, removeCookie } from '@/lib/cookies'
|
|
|
|
type Theme = 'dark' | 'light' | 'system'
|
|
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
|
|
defaultTheme?: Theme
|
|
storageKey?: string
|
|
}
|
|
|
|
type ThemeProviderState = {
|
|
defaultTheme: Theme
|
|
resolvedTheme: ResolvedTheme
|
|
theme: Theme
|
|
setTheme: (theme: Theme) => void
|
|
resetTheme: () => void
|
|
}
|
|
|
|
const initialState: ThemeProviderState = {
|
|
defaultTheme: DEFAULT_THEME,
|
|
resolvedTheme: 'light',
|
|
theme: DEFAULT_THEME,
|
|
setTheme: () => null,
|
|
resetTheme: () => null,
|
|
}
|
|
|
|
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>(() =>
|
|
getStoredTheme(storageKey, defaultTheme)
|
|
)
|
|
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() =>
|
|
resolveTheme(getStoredTheme(storageKey, defaultTheme))
|
|
)
|
|
|
|
useEffect(() => {
|
|
const root = window.document.documentElement
|
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
|
|
|
const applyTheme = () => {
|
|
const nextResolvedTheme = theme === 'system' ? getSystemTheme() : theme
|
|
root.classList.remove('light', 'dark')
|
|
root.classList.add(nextResolvedTheme)
|
|
setResolvedTheme(nextResolvedTheme)
|
|
}
|
|
|
|
applyTheme()
|
|
|
|
mediaQuery.addEventListener('change', applyTheme)
|
|
|
|
return () => mediaQuery.removeEventListener('change', applyTheme)
|
|
}, [theme])
|
|
|
|
const setTheme = useCallback(
|
|
(theme: Theme) => {
|
|
setCookie(storageKey, theme, THEME_COOKIE_MAX_AGE)
|
|
_setTheme(theme)
|
|
},
|
|
[storageKey]
|
|
)
|
|
|
|
const resetTheme = useCallback(() => {
|
|
removeCookie(storageKey)
|
|
_setTheme(defaultTheme)
|
|
}, [defaultTheme, storageKey])
|
|
|
|
const contextValue = useMemo(
|
|
() => ({
|
|
defaultTheme,
|
|
resolvedTheme,
|
|
resetTheme,
|
|
theme,
|
|
setTheme,
|
|
}),
|
|
[defaultTheme, resolvedTheme, resetTheme, theme, setTheme]
|
|
)
|
|
|
|
return (
|
|
<ThemeContext value={contextValue} {...props}>
|
|
{children}
|
|
</ThemeContext>
|
|
)
|
|
}
|
|
|
|
// eslint-disable-next-line react-refresh/only-export-components
|
|
export const useTheme = () => {
|
|
const context = useContext(ThemeContext)
|
|
|
|
if (!context) throw new Error('useTheme must be used within a ThemeProvider')
|
|
|
|
return context
|
|
}
|