style(web): unify design system across default frontend

🔤 Typography
- Fix --font-sans stack: 'Public Sans Variable' was never applied (wrong family name), add CJK fallbacks (PingFang SC / Microsoft YaHei UI / Noto Sans SC)
- Sync <html lang> with i18n language for correct CJK glyph selection
- Remove dead fake font system (font-provider.tsx, config/fonts.ts, --font-inter/--font-manrope)
- Typography contract: 3-tier headings, arbitrary text-[10px]/[11px] → text-xs (46 files), no uppercase table headers, font-bold → font-semibold
- Numbers use tabular-nums only; font-mono reserved for keys/IDs/code (29 files)

🎨 Color discipline
- Retire StatusBadge string-hash rainbow (autoColor → neutral); all variants resolve to 5 semantic voices
- Add tintedBadgeClassMap as the single tinted-pill recipe
- Replace hardcoded emerald/amber/rose/sky classes with semantic tokens across ~30 files (timing badges, progress bars, system panels, pricing, rankings, log row tints)
- Drop all !important text overrides and [font-family:var(--font-body)] hacks

🧘 Motion restraint
- Remove admin entrance animations: table row stagger, card hover lift/shadow, button press scale, dashboard shimmer line
- Neuter page-transition stagger components into plain wrappers; page transitions reduced to pure opacity fade
- Decorative motion stays landing-only

📐 Shape
- Single radius source: --radius 1rem → 0.625rem
- Sparklines use solid chart tokens instead of decorative gradients
- PanelWrapper: border-only (no shadow), align radius with Card

📏 Guardrails
- Add "Design System Discipline" section (3.10.1) to web/default/AGENTS.md

 Verified: tsgo typecheck clean, rsbuild production build passes
This commit is contained in:
t0ng7u
2026-07-11 02:02:11 +08:00
parent 0cb741d8da
commit 262ab93123
97 changed files with 561 additions and 863 deletions
@@ -22,7 +22,7 @@ import { Zap } from 'lucide-react'
import { useState } from 'react'
import { DataTableColumnHeader } from '@/components/data-table'
import { StatusBadge } from '@/components/status-badge'
import { StatusBadge, tintedBadgeClassMap } from '@/components/status-badge'
import {
Tooltip,
TooltipContent,
@@ -90,7 +90,7 @@ export function createTimestampColumn<T>(config: {
return <span className='text-muted-foreground/60 text-xs'>-</span>
}
return (
<span className='font-mono text-xs tabular-nums'>
<span className='text-xs tabular-nums'>
{formatTimestampToDate(timestamp, unit)}
</span>
)
@@ -137,22 +137,16 @@ export function createDurationColumn<T>(config: {
const variant =
duration.durationSec > warningThresholdSec ? 'danger' : 'success'
const durationBgMap: Record<string, string> = {
success:
'border border-emerald-200/40 bg-emerald-50/35 !text-emerald-600 dark:border-emerald-900/40 dark:bg-emerald-950/15 dark:!text-emerald-400',
warning:
'border border-amber-200/45 bg-amber-50/35 !text-amber-600 dark:border-amber-900/40 dark:bg-amber-950/15 dark:!text-amber-400',
danger:
'border border-rose-200/50 bg-rose-50/35 !text-red-600 dark:border-rose-900/40 dark:bg-rose-950/15 dark:!text-red-400',
}
return (
<StatusBadge
label={`${duration.durationSec.toFixed(1)}s`}
variant={variant}
size='sm'
copyable={false}
className={cn('rounded-md font-mono', durationBgMap[variant])}
className={cn(
'rounded-md tabular-nums',
tintedBadgeClassMap[variant]
)}
/>
)
},
@@ -225,7 +219,7 @@ export function createFailReasonColumn<T>(config: {
onClick={() => setDialogOpen(true)}
title={cellTitle}
>
<span className='truncate leading-snug text-red-600 group-hover:underline dark:text-red-400'>
<span className='text-destructive truncate leading-snug group-hover:underline'>
{failReason}
</span>
</button>
@@ -21,7 +21,11 @@ import { CircleAlert, GitBranch, Sparkles, KeyRound } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge'
import {
StatusBadge,
tintedBadgeClassMap,
type StatusBadgeProps,
} from '@/components/status-badge'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import {
Popover,
@@ -304,7 +308,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
return (
<div className='flex min-w-0 flex-col gap-0.5'>
<span className='truncate font-mono text-xs tabular-nums'>
<span className='truncate text-xs tabular-nums'>
{formatTimestampToDate(timestamp)}
</span>
<StatusBadge
@@ -312,7 +316,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
variant={config.color as StatusBadgeProps['variant']}
size='sm'
copyable={false}
className='!text-xs [&_span]:!text-xs'
className='text-xs [&_span]:text-xs'
/>
</div>
)
@@ -402,7 +406,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
}
>
<GitBranch
className='size-3.5 text-amber-500'
className='text-warning size-3.5'
aria-hidden='true'
/>
</PopoverTrigger>
@@ -423,7 +427,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
{affinity && (
<button
type='button'
className='absolute -top-1 -right-1 leading-none text-amber-500'
className='text-warning absolute -top-1 -right-1 leading-none'
onClick={(e) => {
e.stopPropagation()
setAffinityTarget({
@@ -443,7 +447,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
)}
</div>
{log.channel_name && (
<span className='text-muted-foreground/70 truncate [font-family:var(--font-body)] !text-xs'>
<span className='text-muted-foreground/70 truncate text-xs'>
{channelName}
</span>
)}
@@ -510,7 +514,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
<Avatar className='ring-border/60 size-6 ring-1 max-sm:hidden'>
<AvatarFallback
className={cn(
'text-[11px] font-semibold',
'text-xs font-semibold',
!sensitiveVisible && 'bg-muted text-muted-foreground'
)}
style={
@@ -577,7 +581,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
copyText={sensitiveVisible ? tokenName : undefined}
size='sm'
showDot={false}
className='border-border/60 bg-muted/30 text-foreground h-6 max-w-full gap-1.5 overflow-hidden rounded-md border px-2 py-0.5 [font-family:var(--font-body)]'
className='border-border/60 bg-muted/30 text-foreground h-6 max-w-full gap-1.5 overflow-hidden rounded-md border px-2 py-0.5'
/>
</TooltipTrigger>
{sensitiveVisible && tokenName.length > 16 && (
@@ -588,7 +592,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
</Tooltip>
</TooltipProvider>
{metaParts.length > 0 && (
<span className='text-muted-foreground/60 truncate [font-family:var(--font-body)] !text-xs'>
<span className='text-muted-foreground/60 truncate text-xs'>
{metaParts.join(' · ')}
</span>
)}
@@ -637,16 +641,12 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
? getFirstResponseTimeColor(frt / 1000)
: 'neutral'
const timingBgMap: Record<string, string> = {
success:
'border border-emerald-200/40 bg-emerald-50/35 !text-emerald-600 dark:border-emerald-900/40 dark:bg-emerald-950/15 dark:!text-emerald-400',
warning:
'border border-amber-200/45 bg-amber-50/35 !text-amber-600 dark:border-amber-900/40 dark:bg-amber-950/15 dark:!text-amber-400',
danger:
'border border-rose-200/50 bg-rose-50/35 !text-red-600 dark:border-rose-900/40 dark:bg-rose-950/15 dark:!text-red-400',
neutral:
'border border-border/60 bg-muted/30 dark:border-border/40 dark:bg-muted/20',
}
const timingClass = (variant: string) =>
tintedBadgeClassMap[
(variant in tintedBadgeClassMap
? variant
: 'neutral') as keyof typeof tintedBadgeClassMap
]
return (
<div className='flex flex-col gap-1'>
@@ -656,7 +656,10 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
variant={timeVariant as StatusBadgeProps['variant']}
size='sm'
copyable={false}
className={cn('rounded-md font-mono', timingBgMap[timeVariant])}
className={cn(
'rounded-md tabular-nums',
timingClass(timeVariant)
)}
/>
{log.is_stream &&
(frt != null && frt > 0 ? (
@@ -667,8 +670,8 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
showDot={false}
copyable={false}
className={cn(
'rounded-md font-mono',
timingBgMap[frtVariant]
'rounded-md tabular-nums',
timingClass(frtVariant)
)}
/>
) : (
@@ -678,12 +681,15 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
size='sm'
showDot={false}
copyable={false}
className={cn('rounded-md font-mono', timingBgMap.neutral)}
className={cn(
'rounded-md tabular-nums',
tintedBadgeClassMap.neutral
)}
/>
))}
</div>
<div className='flex items-center gap-1 [font-family:var(--font-body)] !text-xs leading-none'>
<span className='text-muted-foreground/60 [font-family:var(--font-body)] !text-xs leading-none'>
<div className='flex items-center gap-1 text-xs leading-none'>
<span className='text-muted-foreground/60 text-xs leading-none'>
{log.is_stream ? t('Stream') : t('Non-stream')}
{tokensPerSecond != null && (
<>
@@ -701,7 +707,9 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={<CircleAlert className='size-3 text-red-500' />}
render={
<CircleAlert className='text-destructive size-3' />
}
></TooltipTrigger>
<TooltipContent>
<div className='space-y-0.5 text-xs'>
@@ -751,12 +759,12 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
return (
<div className='flex flex-col gap-0.5'>
<span className='font-mono text-xs font-medium tabular-nums'>
<span className='text-xs font-medium tabular-nums'>
{promptTokens.toLocaleString()} /{' '}
{completionTokens.toLocaleString()}
</span>
{(cacheReadTokens > 0 || cacheWriteTokens > 0) && (
<div className='flex items-center gap-1 text-[11px]'>
<div className='flex items-center gap-1 text-xs'>
{cacheReadTokens > 0 && (
<span className='text-muted-foreground/60'>
{t('Cache')} {cacheReadTokens.toLocaleString()}
@@ -815,7 +823,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
return (
<div className='flex flex-col gap-0.5'>
<span className='border-border/80 bg-muted/60 inline-flex h-6 w-fit items-center rounded-md border px-2 [font-family:var(--font-body)] text-sm leading-none font-semibold tabular-nums'>
<span className='border-border/80 bg-muted/60 inline-flex h-6 w-fit items-center rounded-md border px-2 text-sm leading-none font-semibold tabular-nums'>
{quotaDisplay.prefix && (
<span className='mr-1'>{quotaDisplay.prefix}</span>
)}
@@ -853,7 +861,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
primary.muted
? 'text-muted-foreground/60'
: primary.danger
? 'text-red-600 dark:text-red-400'
? 'text-destructive'
: 'text-foreground'
)}
>
@@ -95,7 +95,7 @@ export function useDrawingLogsColumns(
return (
<div className='flex min-w-0 flex-col gap-0.5'>
<span className='truncate font-mono text-xs tabular-nums'>
<span className='truncate text-xs tabular-nums'>
{formatTimestampToDate(submitTime, 'milliseconds')}
</span>
<StatusBadge
@@ -151,7 +151,7 @@ export function useDrawingLogsColumns(
copyText={mjId}
variant='neutral'
size='sm'
className='border-border/60 bg-muted/30 !text-foreground max-w-full truncate rounded-md border px-1.5 py-0.5 font-mono'
className='border-border/60 bg-muted/30 text-foreground max-w-full truncate rounded-md border px-1.5 py-0.5 font-mono'
/>
</div>
)
@@ -102,15 +102,15 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
return (
<div className='flex min-w-0 flex-col gap-0.5'>
<span className='truncate font-mono text-xs tabular-nums'>
<span className='truncate text-xs tabular-nums'>
{formatTimestampToDate(submitTime, 'seconds')}
</span>
{log.finish_time ? (
<span className='text-muted-foreground/60 truncate font-mono text-[11px] tabular-nums'>
<span className='text-muted-foreground/60 truncate text-xs tabular-nums'>
{formatTimestampToDate(log.finish_time, 'seconds')}
</span>
) : (
<span className='text-muted-foreground/50 text-[11px]'>-</span>
<span className='text-muted-foreground/50 text-xs'>-</span>
)}
</div>
)
@@ -143,7 +143,7 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
<Avatar className='ring-border/60 size-6 ring-1 max-sm:hidden'>
<AvatarFallback
className={cn(
'text-[11px] font-semibold',
'text-xs font-semibold',
!sensitiveVisible && 'bg-muted text-muted-foreground'
)}
style={
@@ -179,9 +179,9 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
copyText={taskId}
variant='neutral'
size='sm'
className='border-border/60 bg-muted/30 !text-foreground max-w-full truncate rounded-md border px-1.5 py-0.5 font-mono'
className='border-border/60 bg-muted/30 text-foreground max-w-full truncate rounded-md border px-1.5 py-0.5 font-mono'
/>
<span className='text-muted-foreground/60 truncate text-[11px]'>
<span className='text-muted-foreground/60 truncate text-xs'>
{t(log.platform)} · {t(taskActionMapper.getLabel(log.action))}
</span>
</div>
@@ -41,7 +41,7 @@ function StatBadge(props: {
<span className='border-border/60 bg-muted/25 inline-flex h-7 items-center gap-2 rounded-md border px-2.5 text-xs shadow-xs'>
<span className={cn('h-3.5 w-0.5 rounded-full', props.accent)} />
<span className='text-muted-foreground'>{props.label}</span>
<span className='text-foreground/85 font-mono font-semibold tabular-nums'>
<span className='text-foreground/85 font-semibold tabular-nums'>
{props.value}
</span>
</span>
@@ -91,17 +91,17 @@ export function CommonLogsStats() {
<StatBadge
label={t('Usage')}
value={sensitiveVisible ? formatLogQuota(stats?.quota || 0) : '••••'}
accent='bg-sky-500/70'
accent='bg-chart-1/70'
/>
<StatBadge
label={t('RPM')}
value={stats?.rpm || 0}
accent='bg-rose-500/65'
accent='bg-chart-5/70'
/>
<StatBadge
label={t('TPM')}
value={stats?.tpm || 0}
accent='bg-slate-400/70'
accent='bg-muted-foreground/50'
/>
</div>
)
@@ -77,9 +77,9 @@ const CHANNEL_FIELD_LABELS: Record<string, string> = {
function timingTextColorClass(
variant: 'success' | 'warning' | 'danger'
): string {
if (variant === 'success') return 'text-emerald-600'
if (variant === 'warning') return 'text-amber-600'
return 'text-rose-600'
if (variant === 'success') return 'text-success'
if (variant === 'warning') return 'text-warning'
return 'text-destructive'
}
function DetailRow(props: {
@@ -628,7 +628,10 @@ export function DetailsDialog(props: DetailsDialogProps) {
label={t('IP Address')}
value={
<span className='flex items-center gap-1'>
<Globe className='size-3 text-amber-500' aria-hidden='true' />
<Globe
className='text-muted-foreground size-3'
aria-hidden='true'
/>
{props.log.ip}
</span>
}
@@ -814,7 +817,7 @@ export function DetailsDialog(props: DetailsDialogProps) {
/>
))}
{showLegacyTopupWarning && (
<div className='flex items-start gap-1.5 text-xs text-amber-600 dark:text-amber-400'>
<div className='text-warning flex items-start gap-1.5 text-xs'>
<Info className='mt-0.5 size-3.5 shrink-0' aria-hidden='true' />
<span>
{t(
@@ -1030,9 +1033,9 @@ export function DetailsDialog(props: DetailsDialogProps) {
value={
<span className='flex items-center gap-1'>
{other.admin_info.local_count_tokens ? (
<Monitor className='size-3 text-blue-500' />
<Monitor className='text-muted-foreground size-3' />
) : (
<Cloud className='size-3 text-emerald-500' />
<Cloud className='text-muted-foreground size-3' />
)}
<span className='text-xs'>
{other.admin_info.local_count_tokens
@@ -1080,7 +1083,7 @@ export function DetailsDialog(props: DetailsDialogProps) {
)}
{Array.isArray(other.stream_status.errors) &&
other.stream_status.errors.length > 0 && (
<pre className='bg-background/60 mt-1 max-h-32 overflow-y-auto rounded border p-2 font-mono text-[11px] leading-relaxed wrap-break-word whitespace-pre-wrap'>
<pre className='bg-background/60 mt-1 max-h-32 overflow-y-auto rounded border p-2 font-mono text-xs leading-relaxed wrap-break-word whitespace-pre-wrap'>
{other.stream_status.errors.join('\n')}
</pre>
)}
@@ -1155,7 +1158,7 @@ export function DetailsDialog(props: DetailsDialogProps) {
className='shrink-0 font-medium'
copyable={false}
/>
<span className='min-w-0 font-mono text-[11px] leading-relaxed break-all sm:wrap-break-word'>
<span className='min-w-0 font-mono text-xs leading-relaxed break-all sm:wrap-break-word'>
{parsed.content}
</span>
</div>
@@ -120,7 +120,7 @@ export function LogsFilterToolbar<TData>(props: LogsFilterToolbarProps<TData>) {
>
{advancedOpen ? t('Collapse') : t('Expand')}
{activeAdvancedCount > 0 && (
<Badge className='ml-0.5 size-5 justify-center p-0 text-[10px]'>
<Badge className='ml-0.5 size-5 justify-center p-0 text-xs'>
{activeAdvancedCount}
</Badge>
)}
@@ -157,7 +157,7 @@ export function LogsFilterToolbar<TData>(props: LogsFilterToolbarProps<TData>) {
>
{t('Filter')}
{activeMobileFilterCount > 0 && (
<Badge className='ml-0.5 size-5 justify-center p-0 text-[10px]'>
<Badge className='ml-0.5 size-5 justify-center p-0 text-xs'>
{activeMobileFilterCount}
</Badge>
)}
@@ -132,7 +132,7 @@ function ModelBadgeContent(props: ModelBadgeProps) {
showDot={!provider}
autoColor={provider ? undefined : props.modelName}
className={cn(
'border-border/60 bg-muted/30 h-6 max-w-none gap-1.5 rounded-md border px-2 [font-family:var(--font-body)]',
'border-border/60 bg-muted/30 h-6 max-w-none gap-1.5 rounded-md border px-2',
provider && 'text-foreground',
props.className
)}
@@ -41,10 +41,8 @@ import { getLogTypeConfig } from '../lib/utils'
import type { LogCategory } from '../types'
const logTypeRowTint: Record<number, string> = {
[LOG_TYPE_ENUM.ERROR]:
'bg-rose-50/40 dark:bg-rose-950/20 border-rose-200/50 dark:border-rose-900/30',
[LOG_TYPE_ENUM.REFUND]:
'bg-blue-50/30 dark:bg-blue-950/15 border-blue-200/50 dark:border-blue-900/30',
[LOG_TYPE_ENUM.ERROR]: 'bg-destructive/5 border-destructive/20',
[LOG_TYPE_ENUM.REFUND]: 'bg-info/5 border-info/20',
}
interface UsageLogsMobileListProps<TData> {
@@ -129,7 +127,7 @@ function SummaryField<TData>({
<div
className={cn('bg-muted/20 min-w-0 rounded-md px-2 py-1.5', className)}
>
<div className='text-muted-foreground mb-1 text-[11px] leading-none font-medium select-none'>
<div className='text-muted-foreground mb-1 text-xs leading-none font-medium select-none'>
{label}
</div>
<CompactCell
@@ -156,7 +154,7 @@ function MobileLogTimeStatus({
return (
<div className='space-y-1'>
<div className='font-mono text-xs leading-tight tabular-nums'>
<div className='text-xs leading-tight tabular-nums'>
{formatTimestampToDate(timestamp)}
</div>
<div
@@ -200,7 +198,7 @@ function CommonLogsCard<TData>({
<div className='grid grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)] gap-1.5'>
<div className='bg-muted/20 min-w-0 rounded-md px-2 py-1.5'>
<div className='text-muted-foreground mb-1 text-[11px] leading-none font-medium select-none'>
<div className='text-muted-foreground mb-1 text-xs leading-none font-medium select-none'>
{t('Time')}
</div>
<MobileLogTimeStatus
@@ -217,7 +215,7 @@ function CommonLogsCard<TData>({
<SummaryField
label={t('Token')}
cell={cells.get('token_name')}
valueClassName='[&_.flex-col]:max-w-none [&_.flex-col>*:not(:first-child)]:text-[11px] [&_.flex-col>*:not(:first-child)]:leading-none'
valueClassName='[&_.flex-col]:max-w-none [&_.flex-col>*:not(:first-child)]:text-xs [&_.flex-col>*:not(:first-child)]:leading-none'
/>
<SummaryField
label={t('Timing')}
@@ -48,13 +48,13 @@ import { UsageLogsMobileList } from './usage-logs-mobile-card'
const route = getRouteApi('/_authenticated/usage-logs/$section')
const logTypeRowTint: Record<number, string> = {
[LOG_TYPE_ENUM.ERROR]: 'bg-rose-50/40 dark:bg-rose-950/20',
[LOG_TYPE_ENUM.REFUND]: 'bg-blue-50/30 dark:bg-blue-950/15',
[LOG_TYPE_ENUM.ERROR]: 'bg-destructive/5',
[LOG_TYPE_ENUM.REFUND]: 'bg-info/5',
}
// Warning tint for logs where a quota conversion saturated (admin-only marker).
// Takes precedence over the per-type tint since it flags a billing anomaly.
const quotaSaturationRowTint = 'bg-amber-50/60 dark:bg-amber-950/25'
const quotaSaturationRowTint = 'bg-warning/10'
function getColumnVisibilityStorageKey(
logCategory: LogCategory,