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
+20 -20
View File
@@ -156,12 +156,12 @@ function OverviewMetric(props: {
<div className='flex min-w-0 items-center gap-2 px-3 py-2'>
<Icon className='text-muted-foreground/70 size-3.5 shrink-0' />
<div className='min-w-0 flex-1'>
<div className='text-muted-foreground truncate text-[10px] font-medium tracking-wider uppercase'>
<div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
{props.label}
</div>
<div
className={cn(
'text-foreground truncate font-mono text-sm font-semibold tabular-nums',
'text-foreground truncate text-sm font-semibold tabular-nums',
props.valueClassName
)}
>
@@ -254,7 +254,7 @@ function CatalogTextValue(props: { children: React.ReactNode }) {
function CatalogInfoCell(props: { label: string; children: React.ReactNode }) {
return (
<div className='bg-card flex min-w-0 flex-col gap-1 px-3 py-2.5'>
<span className='text-muted-foreground text-[10px] font-medium tracking-wider uppercase'>
<span className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
{props.label}
</span>
{props.children}
@@ -361,7 +361,7 @@ function ModelBackendQuickStats(props: { model: PricingModel }) {
key={stat.key}
className='bg-background flex min-w-0 flex-col gap-0.5 px-3 py-2.5'
>
<span className='text-muted-foreground inline-flex min-w-0 items-center gap-1 text-[10px] font-medium tracking-wider uppercase'>
<span className='text-muted-foreground inline-flex min-w-0 items-center gap-1 text-xs font-medium tracking-wider uppercase'>
<Icon className='size-3 shrink-0' />
<span className='truncate'>{stat.label}</span>
</span>
@@ -369,7 +369,7 @@ function ModelBackendQuickStats(props: { model: PricingModel }) {
{stat.value}
</span>
{stat.hint && (
<span className='text-muted-foreground/60 truncate text-[10px]'>
<span className='text-muted-foreground/60 truncate text-xs'>
{stat.hint}
</span>
)}
@@ -565,7 +565,7 @@ function ModelHeader(props: { model: PricingModel }) {
{model.billing_mode === 'tiered_expr' && model.billing_expr && (
<>
<span className='text-muted-foreground/30'>·</span>
<span className='rounded bg-amber-100 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 dark:bg-amber-500/20 dark:text-amber-300'>
<span className='bg-warning/15 text-warning rounded px-1.5 py-0.5 text-xs font-medium'>
{isSpecialExpression
? t('Special billing expression')
: t('Dynamic Pricing')}
@@ -649,15 +649,15 @@ function PriceSection(props: {
return (
<section>
<SectionTitle>{t('Base Price')}</SectionTitle>
<div className='rounded-lg border border-amber-200/70 bg-amber-50/70 p-3 dark:border-amber-500/20 dark:bg-amber-500/10'>
<div className='text-sm font-medium text-amber-800 dark:text-amber-200'>
<div className='border-warning/30 bg-warning/10 rounded-lg border p-3'>
<div className='text-warning text-sm font-medium'>
{t('Special billing expression')}
</div>
<p className='text-muted-foreground mt-1 text-xs'>
{t('Unable to parse structured pricing')}
</p>
<div className='mt-3'>
<div className='text-muted-foreground mb-1 text-[10px] font-medium tracking-wider uppercase'>
<div className='text-muted-foreground mb-1 text-xs font-medium tracking-wider uppercase'>
{t('Raw expression')}
</div>
<code className='text-muted-foreground bg-background/80 block max-h-28 overflow-auto rounded-md border px-2 py-1.5 font-mono text-xs break-all'>
@@ -682,7 +682,7 @@ function PriceSection(props: {
<div className='text-muted-foreground text-xs'>
{t(entry.shortLabel)}
</div>
<div className='text-foreground mt-1 font-mono text-base font-semibold tabular-nums'>
<div className='text-foreground mt-1 text-base font-semibold tabular-nums'>
{entry.formatted}
<span className='text-muted-foreground/40 ml-1 text-xs font-normal'>
/ {tokenUnitLabel}
@@ -707,7 +707,7 @@ function PriceSection(props: {
<span className='text-muted-foreground/70 text-sm'>
{t(entry.shortLabel)}
</span>
<span className='text-muted-foreground font-mono text-sm tabular-nums'>
<span className='text-muted-foreground text-sm tabular-nums'>
{entry.formatted}
<span className='text-muted-foreground/40 ml-1 text-xs font-normal'>
/ {tokenUnitLabel}
@@ -730,7 +730,7 @@ function PriceSection(props: {
<span className='text-muted-foreground text-sm'>
{t('Per request')}
</span>
<span className='text-foreground font-mono text-sm font-semibold tabular-nums'>
<span className='text-foreground text-sm font-semibold tabular-nums'>
{formatFixedPrice(
props.model,
baseGroupKey,
@@ -771,7 +771,7 @@ function PriceSection(props: {
{primaryPriceTypes.map((item) => (
<div key={item.type} className='bg-muted/20 rounded-lg border p-3'>
<div className='text-muted-foreground text-xs'>{item.label}</div>
<div className='text-foreground mt-1 font-mono text-base font-semibold tabular-nums'>
<div className='text-foreground mt-1 text-base font-semibold tabular-nums'>
{renderPrice(item.type)}
</div>
</div>
@@ -788,7 +788,7 @@ function PriceSection(props: {
<span className='text-muted-foreground/70 text-sm'>
{item.label}
</span>
<span className='text-muted-foreground font-mono text-sm tabular-nums'>
<span className='text-muted-foreground text-sm tabular-nums'>
{renderPrice(item.type)}
</span>
</div>
@@ -923,7 +923,7 @@ function GroupPricingSection(props: {
}
const thClass =
'text-muted-foreground py-2 text-[10px] font-medium tracking-wider uppercase'
'text-muted-foreground py-2 text-xs font-medium tracking-wider uppercase'
if (isDynamicPricingModel(props.model)) {
const dynamicTiers = getDynamicPricingTiers(props.model)
@@ -933,8 +933,8 @@ function GroupPricingSection(props: {
<section>
<SectionTitle>{t('Pricing by Group')}</SectionTitle>
<AutoGroupChain model={props.model} autoGroups={props.autoGroups} />
<div className='rounded-lg border border-amber-200/70 bg-amber-50/70 p-3 dark:border-amber-500/20 dark:bg-amber-500/10'>
<div className='text-sm font-medium text-amber-800 dark:text-amber-200'>
<div className='border-warning/30 bg-warning/10 rounded-lg border p-3'>
<div className='text-warning text-sm font-medium'>
{t('Special billing expression')}
</div>
<p className='text-muted-foreground mt-1 text-xs'>
@@ -943,7 +943,7 @@ function GroupPricingSection(props: {
)}
</p>
<div className='mt-3'>
<div className='text-muted-foreground mb-1 text-[10px] font-medium tracking-wider uppercase'>
<div className='text-muted-foreground mb-1 text-xs font-medium tracking-wider uppercase'>
{t('Raw expression')}
</div>
<code className='text-muted-foreground bg-background/80 block max-h-28 overflow-auto rounded-md border px-2 py-1.5 font-mono text-xs break-all'>
@@ -1028,7 +1028,7 @@ function GroupPricingSection(props: {
</div>
)
})}
<p className='text-muted-foreground/40 mt-1.5 text-[10px]'>
<p className='text-muted-foreground/40 mt-1.5 text-xs'>
{t('Prices shown per')} {tokenUnitLabel} tokens
</p>
</div>
@@ -1119,7 +1119,7 @@ function GroupPricingSection(props: {
/>
<div className='-mx-4 sm:mx-0'>
{isTokenBased && (
<p className='text-muted-foreground/40 mt-1.5 px-4 text-[10px] sm:px-0'>
<p className='text-muted-foreground/40 mt-1.5 px-4 text-xs sm:px-0'>
{t('Prices shown per')} {tokenUnitLabel} tokens
</p>
)}