🎨 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:
t0ng7u
2026-05-07 11:20:43 +08:00
parent 415d21d071
commit a7475a1e67
19 changed files with 315 additions and 172 deletions
+3 -3
View File
@@ -5,12 +5,12 @@ import type { PingStatus } from '@/features/dashboard/types'
*/
export function getLatencyColorClass(latency: number): string {
if (latency < 200) {
return 'text-green-600 dark:text-green-400'
return 'text-success'
}
if (latency < 500) {
return 'text-yellow-600 dark:text-yellow-400'
return 'text-warning'
}
return 'text-red-600 dark:text-red-400'
return 'text-destructive'
}
/**
+51 -8
View File
@@ -20,7 +20,37 @@ type TooltipLineItem = {
shapeSize?: number
}
function getVChartDefaultColors(domainLength: number) {
const THEME_CHART_COLOR_VARIABLES = [
'--chart-1',
'--chart-2',
'--chart-3',
'--chart-4',
'--chart-5',
] as const
function getThemeChartColors(themeKey?: string): string[] {
if (typeof document === 'undefined') return []
void themeKey
const bodyStyle = window.getComputedStyle(document.body)
const rootStyle = window.getComputedStyle(document.documentElement)
return THEME_CHART_COLOR_VARIABLES.map((name) => {
return (
bodyStyle.getPropertyValue(name) || rootStyle.getPropertyValue(name)
).trim()
}).filter(Boolean)
}
function getVChartDefaultColors(domainLength: number, themeKey?: string) {
const themeColors = getThemeChartColors(themeKey)
if (themeColors.length > 0) {
return Array.from(
{ length: Math.max(domainLength, themeColors.length) },
(_, index) => themeColors[index % themeColors.length]
)
}
const scheme =
vchartDefaultDataScheme.find(
(item) => !item.maxDomainLength || domainLength <= item.maxDomainLength
@@ -49,7 +79,8 @@ function renderQuotaCompat(rawQuota: number, digits = 4): string {
export function processChartData(
data: QuotaDataItem[],
timeGranularity: TimeGranularity = 'day',
t?: TFunction
t?: TFunction,
themeKey?: string
): ProcessedChartData {
const tt: TFunction = t ?? ((x) => x)
const otherLabel = tt('Other')
@@ -240,7 +271,10 @@ export function processChartData(
const sortedTimes = Array.from(timeModelMap.keys()).sort()
const sortedModels = [...allModels].sort()
const modelColorDomain = Array.from(new Set([...sortedModels, otherLabel]))
const modelColorRange = getVChartDefaultColors(modelColorDomain.length)
const modelColorRange = getVChartDefaultColors(
modelColorDomain.length,
themeKey
)
const otherColor = modelColorRange[modelColorDomain.indexOf(otherLabel)]
const otherTooltipColor =
typeof otherColor === 'string' ? otherColor : '#FF8A00'
@@ -665,7 +699,7 @@ export function processChartData(
}
}
const USER_COLORS = [
const USER_COLOR_FALLBACKS = [
'#5B8FF9',
'#5AD8A6',
'#F6BD16',
@@ -682,11 +716,20 @@ export function processUserChartData(
data: QuotaDataItem[],
timeGranularity: TimeGranularity = 'day',
t?: TFunction,
limit = 10
limit = 10,
themeKey?: string
): ProcessedUserChartData {
const tt: TFunction = t ?? ((x) => x)
const { config } = getCurrencyDisplay()
const quotaPerUnit = config.quotaPerUnit
const themeUserColors = getThemeChartColors(themeKey)
const userColorRange =
themeUserColors.length > 0
? Array.from(
{ length: Math.max(limit, themeUserColors.length) },
(_, index) => themeUserColors[index % themeUserColors.length]
)
: USER_COLOR_FALLBACKS
const formatVal = (raw: number) => renderQuotaCompat(raw, 2)
@@ -704,7 +747,7 @@ export function processUserChartData(
subtext: tt('No data available'),
},
legends: { visible: false },
color: { type: 'ordinal', range: USER_COLORS },
color: { type: 'ordinal', range: userColorRange },
background: { fill: 'transparent' },
},
spec_user_trend: {
@@ -719,7 +762,7 @@ export function processUserChartData(
subtext: tt('No data available'),
},
legends: { visible: true, selectMode: 'single' },
color: { type: 'ordinal', range: USER_COLORS },
color: { type: 'ordinal', range: userColorRange },
point: { visible: false },
background: { fill: 'transparent' },
},
@@ -749,7 +792,7 @@ export function processUserChartData(
const userColorMap = topUsers.reduce<Record<string, string>>(
(acc, user, i) => {
acc[user] = USER_COLORS[i % USER_COLORS.length]
acc[user] = userColorRange[i % userColorRange.length]
return acc
},
{}