✨ 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:
@@ -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}
|
||||
|
||||
+4
-4
@@ -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)
|
||||
)}
|
||||
>
|
||||
|
||||
+4
-21
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -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'>
|
||||
|
||||
Reference in New Issue
Block a user