🎨 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:
+11
-3
@@ -4,6 +4,7 @@ import { AreaChart, BarChart3, WalletCards } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { TimeGranularity } from '@/lib/time'
|
||||
import { VCHART_OPTION } from '@/lib/vchart'
|
||||
import { useThemeCustomization } from '@/context/theme-customization-provider'
|
||||
import { useTheme } from '@/context/theme-provider'
|
||||
import {
|
||||
CONSUMPTION_DISTRIBUTION_CHART_OPTIONS,
|
||||
@@ -39,6 +40,7 @@ export function ConsumptionDistributionChart(
|
||||
) {
|
||||
const { t } = useTranslation()
|
||||
const { resolvedTheme } = useTheme()
|
||||
const { customization } = useThemeCustomization()
|
||||
const [chartType, setChartType] = useState<ConsumptionDistributionChartType>(
|
||||
props.defaultChartType ?? 'bar'
|
||||
)
|
||||
@@ -72,8 +74,14 @@ export function ConsumptionDistributionChart(
|
||||
}, [resolvedTheme])
|
||||
|
||||
const chartData = useMemo(
|
||||
() => processChartData(props.loading ? [] : props.data, timeGranularity, t),
|
||||
[props.data, props.loading, timeGranularity, t]
|
||||
() =>
|
||||
processChartData(
|
||||
props.loading ? [] : props.data,
|
||||
timeGranularity,
|
||||
t,
|
||||
customization.preset
|
||||
),
|
||||
[props.data, props.loading, timeGranularity, t, customization.preset]
|
||||
)
|
||||
const spec = chartType === 'bar' ? chartData.spec_line : chartData.spec_area
|
||||
|
||||
@@ -113,7 +121,7 @@ export function ConsumptionDistributionChart(
|
||||
<div className='h-[300px] p-1.5 sm:h-96 sm:p-2'>
|
||||
{themeReady && spec && (
|
||||
<VChart
|
||||
key={`${chartType}-${resolvedTheme}`}
|
||||
key={`${chartType}-${resolvedTheme}-${customization.preset}`}
|
||||
spec={{
|
||||
...spec,
|
||||
theme: resolvedTheme === 'dark' ? 'dark' : 'light',
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PieChart as PieChartIcon } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { TimeGranularity } from '@/lib/time'
|
||||
import { VCHART_OPTION } from '@/lib/vchart'
|
||||
import { useThemeCustomization } from '@/context/theme-customization-provider'
|
||||
import { useTheme } from '@/context/theme-provider'
|
||||
import {
|
||||
DEFAULT_TIME_GRANULARITY,
|
||||
@@ -37,6 +38,7 @@ interface ModelChartsProps {
|
||||
export function ModelCharts(props: ModelChartsProps) {
|
||||
const { t } = useTranslation()
|
||||
const { resolvedTheme } = useTheme()
|
||||
const { customization } = useThemeCustomization()
|
||||
const [activeTab, setActiveTab] = useState<ModelAnalyticsChartTab>(
|
||||
props.defaultChartTab ?? 'trend'
|
||||
)
|
||||
@@ -70,8 +72,14 @@ export function ModelCharts(props: ModelChartsProps) {
|
||||
}, [resolvedTheme])
|
||||
|
||||
const chartData = useMemo(
|
||||
() => processChartData(props.loading ? [] : props.data, timeGranularity, t),
|
||||
[props.data, props.loading, timeGranularity, t]
|
||||
() =>
|
||||
processChartData(
|
||||
props.loading ? [] : props.data,
|
||||
timeGranularity,
|
||||
t,
|
||||
customization.preset
|
||||
),
|
||||
[props.data, props.loading, timeGranularity, t, customization.preset]
|
||||
)
|
||||
|
||||
const spec = chartData[CHART_SPEC_KEYS[activeTab]]
|
||||
@@ -110,7 +118,7 @@ export function ModelCharts(props: ModelChartsProps) {
|
||||
<div className='h-[300px] p-1.5 sm:h-96 sm:p-2'>
|
||||
{themeReady && spec && (
|
||||
<VChart
|
||||
key={`${activeTab}-${resolvedTheme}`}
|
||||
key={`${activeTab}-${resolvedTheme}-${customization.preset}`}
|
||||
spec={{
|
||||
...spec,
|
||||
theme: resolvedTheme === 'dark' ? 'dark' : 'light',
|
||||
|
||||
+6
-11
@@ -210,13 +210,11 @@ function StartStepItem(props: {
|
||||
<span
|
||||
className={cn(
|
||||
'bg-background relative z-10 flex size-8 shrink-0 items-center justify-center rounded-full border shadow-xs',
|
||||
props.step.completed && 'border-emerald-500/30 bg-emerald-500/10'
|
||||
props.step.completed && 'border-success/30 bg-success/10'
|
||||
)}
|
||||
>
|
||||
<StatusIcon
|
||||
className={
|
||||
props.step.completed ? 'size-4 text-emerald-600' : 'size-4'
|
||||
}
|
||||
className={props.step.completed ? 'text-success size-4' : 'size-4'}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</span>
|
||||
@@ -316,9 +314,9 @@ function RequestPreview(props: {
|
||||
|
||||
<div className='bg-foreground/[0.035] my-3 rounded-xl p-3 font-mono text-xs'>
|
||||
<div className='mb-2 flex items-center gap-1.5'>
|
||||
<span className='size-2 rounded-full bg-red-400' />
|
||||
<span className='size-2 rounded-full bg-amber-400' />
|
||||
<span className='size-2 rounded-full bg-emerald-400' />
|
||||
<span className='bg-destructive size-2 rounded-full' />
|
||||
<span className='bg-warning size-2 rounded-full' />
|
||||
<span className='bg-success size-2 rounded-full' />
|
||||
</div>
|
||||
<div className='flex flex-col gap-1 overflow-hidden'>
|
||||
{previewLines.map((line, index) => (
|
||||
@@ -650,10 +648,7 @@ export function OverviewDashboard() {
|
||||
<div className='relative flex flex-wrap items-center justify-between gap-3'>
|
||||
<div className='flex min-w-0 items-center gap-3'>
|
||||
<span className='bg-background/70 flex size-9 shrink-0 items-center justify-center rounded-xl border shadow-xs'>
|
||||
<Check
|
||||
className='size-4 text-emerald-600'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
<Check className='text-success size-4' aria-hidden='true' />
|
||||
</span>
|
||||
<div className='min-w-0'>
|
||||
<div className='flex items-center gap-2'>
|
||||
|
||||
@@ -188,7 +188,7 @@ export function SummaryCards() {
|
||||
</StaggerContainer>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col justify-between gap-5 border-t bg-amber-50/80 p-4 sm:p-5 xl:border-t-0 xl:border-l dark:bg-amber-950/20'>
|
||||
<div className='bg-warning/10 flex flex-col justify-between gap-5 border-t p-4 sm:p-5 xl:border-t-0 xl:border-l'>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='text-muted-foreground text-sm'>
|
||||
{t('Credit remaining')}
|
||||
|
||||
@@ -12,10 +12,10 @@ import type {
|
||||
import { PanelWrapper } from '../ui/panel-wrapper'
|
||||
|
||||
const STATUS_COLOR_MAP: Record<number, string> = {
|
||||
1: 'bg-emerald-500',
|
||||
0: 'bg-red-500',
|
||||
2: 'bg-amber-500',
|
||||
3: 'bg-blue-500',
|
||||
1: 'bg-success',
|
||||
0: 'bg-destructive',
|
||||
2: 'bg-warning',
|
||||
3: 'bg-info',
|
||||
}
|
||||
const DEFAULT_STATUS_COLOR = 'bg-muted-foreground/40'
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Users, Loader2 } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getRollingDateRange, type TimeGranularity } from '@/lib/time'
|
||||
import { VCHART_OPTION } from '@/lib/vchart'
|
||||
import { useThemeCustomization } from '@/context/theme-customization-provider'
|
||||
import { useTheme } from '@/context/theme-provider'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { getUserQuotaDataByUsers } from '@/features/dashboard/api'
|
||||
@@ -46,6 +47,7 @@ const TOP_USER_LIMIT_OPTIONS = [5, 10, 20, 50]
|
||||
export function UserCharts() {
|
||||
const { t } = useTranslation()
|
||||
const { resolvedTheme } = useTheme()
|
||||
const { customization } = useThemeCustomization()
|
||||
const [themeReady, setThemeReady] = useState(false)
|
||||
const themeManagerRef = useRef<
|
||||
(typeof import('@visactor/vchart'))['ThemeManager'] | null
|
||||
@@ -117,9 +119,17 @@ export function UserCharts() {
|
||||
isLoading ? [] : (userData ?? []),
|
||||
timeGranularity,
|
||||
t,
|
||||
topUserLimit
|
||||
topUserLimit,
|
||||
customization.preset
|
||||
),
|
||||
[userData, isLoading, timeGranularity, t, topUserLimit]
|
||||
[
|
||||
userData,
|
||||
isLoading,
|
||||
timeGranularity,
|
||||
t,
|
||||
topUserLimit,
|
||||
customization.preset,
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -207,7 +217,7 @@ export function UserCharts() {
|
||||
themeReady &&
|
||||
spec && (
|
||||
<VChart
|
||||
key={`user-${chart.value}-${topUserLimit}-${resolvedTheme}`}
|
||||
key={`user-${chart.value}-${topUserLimit}-${resolvedTheme}-${customization.preset}`}
|
||||
spec={{
|
||||
...spec,
|
||||
theme: resolvedTheme === 'dark' ? 'dark' : 'light',
|
||||
|
||||
Reference in New Issue
Block a user