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`
137 lines
4.5 KiB
TypeScript
Vendored
137 lines
4.5 KiB
TypeScript
Vendored
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
import { VChart } from '@visactor/react-vchart'
|
|
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,
|
|
DEFAULT_TIME_GRANULARITY,
|
|
} from '@/features/dashboard/constants'
|
|
import { processChartData } from '@/features/dashboard/lib'
|
|
import type {
|
|
ConsumptionDistributionChartType,
|
|
QuotaDataItem,
|
|
} from '@/features/dashboard/types'
|
|
|
|
let themeManagerPromise: Promise<
|
|
(typeof import('@visactor/vchart'))['ThemeManager']
|
|
> | null = null
|
|
|
|
interface ConsumptionDistributionChartProps {
|
|
data: QuotaDataItem[]
|
|
loading?: boolean
|
|
timeGranularity?: TimeGranularity
|
|
defaultChartType?: ConsumptionDistributionChartType
|
|
}
|
|
|
|
const CHART_TYPE_ICONS: Record<
|
|
ConsumptionDistributionChartType,
|
|
typeof BarChart3
|
|
> = {
|
|
bar: BarChart3,
|
|
area: AreaChart,
|
|
}
|
|
|
|
export function ConsumptionDistributionChart(
|
|
props: ConsumptionDistributionChartProps
|
|
) {
|
|
const { t } = useTranslation()
|
|
const { resolvedTheme } = useTheme()
|
|
const { customization } = useThemeCustomization()
|
|
const [chartType, setChartType] = useState<ConsumptionDistributionChartType>(
|
|
props.defaultChartType ?? 'bar'
|
|
)
|
|
const [themeReady, setThemeReady] = useState(false)
|
|
const themeManagerRef = useRef<
|
|
(typeof import('@visactor/vchart'))['ThemeManager'] | null
|
|
>(null)
|
|
const timeGranularity = props.timeGranularity ?? DEFAULT_TIME_GRANULARITY
|
|
|
|
useEffect(() => {
|
|
if (props.defaultChartType) setChartType(props.defaultChartType)
|
|
}, [props.defaultChartType])
|
|
|
|
useEffect(() => {
|
|
const updateTheme = async () => {
|
|
setThemeReady(false)
|
|
|
|
if (!themeManagerPromise) {
|
|
themeManagerPromise = import('@visactor/vchart').then(
|
|
(m) => m.ThemeManager
|
|
)
|
|
}
|
|
|
|
const ThemeManager = await themeManagerPromise
|
|
themeManagerRef.current = ThemeManager
|
|
ThemeManager.setCurrentTheme(resolvedTheme === 'dark' ? 'dark' : 'light')
|
|
setThemeReady(true)
|
|
}
|
|
|
|
updateTheme()
|
|
}, [resolvedTheme])
|
|
|
|
const chartData = useMemo(
|
|
() =>
|
|
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
|
|
|
|
return (
|
|
<div className='overflow-hidden rounded-lg border'>
|
|
<div className='flex w-full flex-col gap-1.5 border-b px-3 py-2 sm:gap-3 sm:px-5 sm:py-3 lg:flex-row lg:items-center lg:justify-between'>
|
|
<div className='flex items-center gap-2'>
|
|
<WalletCards className='text-muted-foreground/60 size-4' />
|
|
<div className='text-sm font-semibold'>{t('Quota Distribution')}</div>
|
|
<span className='text-muted-foreground text-xs'>
|
|
{t('Total:')} {chartData.totalQuotaDisplay}
|
|
</span>
|
|
</div>
|
|
|
|
<div className='bg-muted/60 inline-flex h-7 w-full overflow-x-auto rounded-lg border p-0.5 sm:h-8 sm:w-auto'>
|
|
{CONSUMPTION_DISTRIBUTION_CHART_OPTIONS.map((item) => {
|
|
const Icon = CHART_TYPE_ICONS[item.value]
|
|
return (
|
|
<button
|
|
key={item.value}
|
|
type='button'
|
|
onClick={() => setChartType(item.value)}
|
|
className={`inline-flex shrink-0 items-center gap-1.5 rounded-md px-3 text-xs font-medium transition-colors ${
|
|
chartType === item.value
|
|
? 'bg-background text-foreground shadow-sm'
|
|
: 'text-muted-foreground hover:text-foreground'
|
|
}`}
|
|
>
|
|
<Icon className='size-3.5' />
|
|
{t(item.labelKey)}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div className='h-[300px] p-1.5 sm:h-96 sm:p-2'>
|
|
{themeReady && spec && (
|
|
<VChart
|
|
key={`${chartType}-${resolvedTheme}-${customization.preset}`}
|
|
spec={{
|
|
...spec,
|
|
theme: resolvedTheme === 'dark' ? 'dark' : 'light',
|
|
background: 'transparent',
|
|
}}
|
|
option={VCHART_OPTION}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|