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
@@ -169,7 +169,7 @@ export function LogStatCards(props: LogStatCardsProps) {
</div>
) : error ? (
<>
<div className='text-muted-foreground mt-1.5 font-mono text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
<div className='text-muted-foreground mt-1.5 text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
--
</div>
<div className='text-muted-foreground/40 mt-1 hidden text-xs md:block'>
@@ -179,7 +179,7 @@ export function LogStatCards(props: LogStatCardsProps) {
) : (
<>
<div
className='text-foreground mt-1.5 max-w-full truncate font-mono text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'
className='text-foreground mt-1.5 max-w-full truncate text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'
title={it.fullValue}
>
{it.value}
@@ -186,10 +186,10 @@ function InlineMetric(props: {
className='text-muted-foreground/50 size-3 shrink-0'
aria-hidden='true'
/>
<span className='text-muted-foreground text-[11px]'>{props.label}</span>
<span className='text-muted-foreground text-xs'>{props.label}</span>
<span
className={cn(
'font-mono text-xs font-semibold tabular-nums',
'text-xs font-semibold tabular-nums',
props.valueClassName
)}
>
@@ -204,7 +204,7 @@ function ModelBadge(props: { model: PerfModelSummary }) {
return (
<span className='bg-muted/50 inline-flex items-center gap-1.5 rounded-full px-2.5 py-1'>
<span className='max-w-[10rem] truncate font-mono text-[11px]'>
<span className='max-w-[10rem] truncate font-mono text-xs'>
{model.model_name}
</span>
<span
@@ -216,7 +216,7 @@ function ModelBadge(props: { model: PerfModelSummary }) {
/>
<span
className={cn(
'font-mono text-[11px] font-semibold tabular-nums',
'text-xs font-semibold tabular-nums',
getSuccessRateTextClass(model.success_rate)
)}
>
@@ -36,7 +36,6 @@ import {
Timer,
type LucideIcon,
} from 'lucide-react'
import { motion, useReducedMotion } from 'motion/react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
@@ -50,7 +49,6 @@ import { fetchTokenKey, getApiKeys } from '@/features/keys/api'
import type { ApiKey } from '@/features/keys/types'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { getUserModels } from '@/lib/api'
import { MOTION_TRANSITION } from '@/lib/motion'
import { ROLE } from '@/lib/roles'
import { cn } from '@/lib/utils'
import { useAuthStore } from '@/stores/auth-store'
@@ -202,7 +200,7 @@ function SetupGuideBackdrop(props: { compact?: boolean }) {
'absolute right-3 [mask-image:linear-gradient(90deg,transparent_0%,black_30%,black_82%,transparent_100%)] text-right tracking-[0.38em] whitespace-pre',
props.compact
? '-top-6 text-[9px] leading-4'
: 'top-1 text-[11px] leading-5'
: 'top-1 text-xs leading-5'
)}
>
{SETUP_GUIDE_CODE_PATTERN}
@@ -254,7 +252,7 @@ function StartStepItem(props: {
</span>
<span className='flex min-w-0 flex-col gap-0.5'>
<span className='flex items-center gap-2 text-sm font-medium'>
<span className='text-muted-foreground font-mono text-xs tabular-nums'>
<span className='text-muted-foreground text-xs tabular-nums'>
{props.index + 1}.
</span>
<span className='truncate'>{props.step.title}</span>
@@ -278,7 +276,6 @@ function RequestPreview(props: {
signals: HeroSignal[]
}) {
const { t } = useTranslation()
const shouldReduceMotion = useReducedMotion()
const [isCopying, setIsCopying] = useState(false)
const { copyToClipboard } = useCopyToClipboard({ notify: false })
const previewCurl = buildCurlCommand({
@@ -316,21 +313,7 @@ function RequestPreview(props: {
}
return (
<motion.div
initial={shouldReduceMotion ? false : { opacity: 0, y: 10, scale: 0.98 }}
animate={shouldReduceMotion ? undefined : { opacity: 1, y: 0, scale: 1 }}
transition={MOTION_TRANSITION.slow}
className='bg-background/75 relative overflow-hidden rounded-2xl border p-3 shadow-sm backdrop-blur'
>
{!shouldReduceMotion && (
<motion.div
className='via-foreground/30 pointer-events-none absolute inset-x-0 top-0 h-px bg-linear-to-r from-transparent to-transparent'
animate={{ x: ['-100%', '100%'] }}
transition={{ duration: 3.2, repeat: Infinity, ease: 'easeInOut' }}
aria-hidden='true'
/>
)}
<div className='bg-background relative overflow-hidden rounded-2xl border p-3'>
<div className='flex items-center justify-between gap-3 border-b pb-3'>
<div className='flex min-w-0 items-center gap-2'>
<span className='bg-muted flex size-8 shrink-0 items-center justify-center rounded-lg'>
@@ -410,7 +393,7 @@ function RequestPreview(props: {
)
})}
</div>
</motion.div>
</div>
)
}
@@ -135,7 +135,7 @@ export function PerformanceHealthPanel() {
) : (
hasData && (
<div>
<span className='text-muted-foreground mb-1 block text-[11px] font-medium'>
<span className='text-muted-foreground mb-1 block text-xs font-medium'>
{t('Top models by traffic')}
</span>
<div className='grid grid-cols-1 gap-x-4 sm:grid-cols-2'>
@@ -144,7 +144,7 @@ export function PerformanceHealthPanel() {
key={model.model_name}
className='flex items-center justify-between gap-2 rounded px-1.5 py-1'
>
<span className='min-w-0 flex-1 truncate font-mono text-[11px]'>
<span className='min-w-0 flex-1 truncate font-mono text-xs'>
{model.model_name}
</span>
<span className='inline-flex shrink-0 items-center gap-1'>
@@ -157,7 +157,7 @@ export function PerformanceHealthPanel() {
/>
<span
className={cn(
'font-mono text-[11px] font-semibold tabular-nums',
'text-xs font-semibold tabular-nums',
getSuccessRateTextClass(model.success_rate)
)}
>
@@ -185,7 +185,7 @@ function MetricCell(props: {
const Icon = props.icon
return (
<div className='bg-muted/40 rounded-xl px-3 py-2.5'>
<div className='text-muted-foreground flex items-center gap-1.5 text-[11px] font-medium'>
<div className='text-muted-foreground flex items-center gap-1.5 text-xs font-medium'>
<Icon className='size-3 shrink-0' aria-hidden='true' />
<span className='truncate'>{props.label}</span>
</div>
@@ -194,7 +194,7 @@ function MetricCell(props: {
) : (
<div
className={cn(
'mt-1.5 font-mono text-sm font-semibold tabular-nums',
'mt-1.5 text-sm font-semibold tabular-nums',
props.valueClassName
)}
>
@@ -281,7 +281,7 @@ export function SummaryCards() {
className={cn('size-1.5 rounded-full', healthCfg.dotClass)}
aria-hidden='true'
/>
<span className='text-muted-foreground text-[11px] font-medium'>
<span className='text-muted-foreground text-xs font-medium'>
{t(healthCfg.labelKey)}
</span>
</span>
@@ -293,7 +293,7 @@ export function SummaryCards() {
<div className='grid grid-cols-2 gap-2'>
<div className='bg-background/60 rounded-lg px-2.5 py-2'>
<div className='text-muted-foreground flex items-center gap-1 text-[11px] leading-none font-medium'>
<div className='text-muted-foreground flex items-center gap-1 text-xs leading-none font-medium'>
<Flame className='size-3 shrink-0' aria-hidden='true' />
<span className='truncate'>{t('Last 24h usage')}</span>
</div>
@@ -302,7 +302,7 @@ export function SummaryCards() {
</div>
</div>
<div className='bg-background/60 rounded-lg px-2.5 py-2'>
<div className='text-muted-foreground flex items-center gap-1 text-[11px] leading-none font-medium'>
<div className='text-muted-foreground flex items-center gap-1 text-xs leading-none font-medium'>
{runwayDays !== null && runwayDays < 3 ? (
<TrendingDown
className='size-3 shrink-0'
@@ -32,10 +32,10 @@ import { cn } from '@/lib/utils'
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'
@@ -131,7 +131,7 @@ export function UptimePanel() {
<h4 className='text-muted-foreground text-xs font-semibold tracking-wider uppercase'>
{group.categoryName}
</h4>
<span className='text-muted-foreground/40 font-mono text-xs tabular-nums'>
<span className='text-muted-foreground/40 text-xs tabular-nums'>
{group.monitors?.length || 0}
</span>
</div>
@@ -159,7 +159,7 @@ export function UptimePanel() {
</span>
)}
</div>
<span className='text-foreground shrink-0 font-mono text-sm font-semibold tabular-nums'>
<span className='text-foreground shrink-0 text-sm font-semibold tabular-nums'>
{((monitor.uptime ?? 0) * 100).toFixed(2)}%
</span>
</div>
@@ -42,7 +42,9 @@ function PanelHeader(props: {
}) {
const heading = (
<div className='flex flex-col gap-1'>
<div className='text-sm font-semibold'>{props.title}</div>
<div className='text-base font-semibold tracking-tight'>
{props.title}
</div>
{props.description != null && (
<div className='text-muted-foreground text-xs'>{props.description}</div>
)}
@@ -68,7 +70,7 @@ export function PanelWrapper(props: PanelWrapperProps) {
const resolvedEmptyMessage = props.emptyMessage ?? t('No data available')
const height = props.height ?? 'h-64'
const frameClassName = cn(
'overflow-hidden rounded-2xl border bg-card shadow-xs',
'overflow-hidden rounded-xl border bg-card',
props.className
)
@@ -51,15 +51,17 @@ interface StatCardProps {
action?: ReactNode
}
/* Sparklines are data visualization, so they draw from chart tokens — solid
* fills, no decorative gradients, consistent with the dashboard charts. */
const TONE_CLASSES: Record<StatCardTone, string> = {
rose: 'from-rose-500/80 via-rose-300/70 to-rose-200/20 dark:from-rose-400/70 dark:via-rose-500/30 dark:to-rose-500/5',
teal: 'from-teal-500/80 via-teal-300/70 to-teal-200/20 dark:from-teal-400/70 dark:via-teal-500/30 dark:to-teal-500/5',
gray: 'from-muted-foreground/50 via-muted-foreground/20 to-transparent dark:from-muted-foreground/40 dark:via-muted-foreground/20',
rose: 'bg-chart-5/60',
teal: 'bg-chart-2/60',
gray: 'bg-muted-foreground/30',
}
const LINE_TONE_CLASSES: Record<StatCardTone, string> = {
rose: 'text-warning',
teal: 'text-primary',
rose: 'text-chart-5',
teal: 'text-chart-2',
gray: 'text-muted-foreground',
}
@@ -166,7 +168,7 @@ function BarSparkline(props: { values?: number[]; tone: StatCardTone }) {
<span
key={`spark-${index}`}
className={cn(
'flex-1 rounded-t-sm bg-linear-to-t',
'flex-1 rounded-t-sm',
height <= 0 && 'opacity-20',
TONE_CLASSES[props.tone]
)}
@@ -185,7 +187,7 @@ function StatCardDetails(props: { details: StatCardDetail[] }) {
key={detail.label}
className='bg-muted/40 rounded-lg border border-transparent px-2.5 py-2'
>
<div className='text-muted-foreground truncate text-[11px] leading-none font-medium'>
<div className='text-muted-foreground truncate text-xs leading-none font-medium'>
{detail.label}
</div>
<div
@@ -228,7 +230,7 @@ export function StatCard(props: StatCardProps) {
</div>
) : props.error ? (
<div className='flex flex-col gap-1'>
<div className='text-muted-foreground mt-0.5 font-mono text-base font-bold tracking-tight break-all tabular-nums sm:text-2xl'>
<div className='text-muted-foreground mt-0.5 text-2xl font-semibold tracking-tight break-all tabular-nums'>
--
</div>
<p className='text-muted-foreground/60 text-xs'>
@@ -237,7 +239,7 @@ export function StatCard(props: StatCardProps) {
</div>
) : (
<div className='flex flex-col gap-1'>
<div className='text-foreground font-mono text-2xl font-semibold tracking-tight break-all tabular-nums'>
<div className='text-foreground text-2xl font-semibold tracking-tight break-all tabular-nums'>
{props.value}
</div>
<p className='text-muted-foreground/60 text-xs leading-relaxed'>