1import Link from 'next/link';2import type { ReactNode } from 'react';3import { cn, deltaClass, fmtPct } from '@/lib/format';45/** Minimal, dense UI primitives. Keep them boring; the data is the design. */67export function Card({ children, className, as: Tag = 'section', id }: { children: ReactNode; className?: string; as?: 'section' | 'div' | 'article'; id?: string }) {8 return <Tag className={cn('card', className)} id={id}>{children}</Tag>;9}1011export function CardHeader({ title, subtitle, action, className }: { title: ReactNode; subtitle?: ReactNode; action?: ReactNode; className?: string }) {12 return (13 <header className={cn('flex items-start justify-between gap-3 border-b border-border px-4 py-3', className)}>14 <div className="min-w-0">15 <h2 className="text-sm font-semibold tracking-tight text-fg">{title}</h2>16 {subtitle ? <p className="mt-0.5 text-xs text-muted">{subtitle}</p> : null}17 </div>18 {action ? <div className="shrink-0 text-xs">{action}</div> : null}19 </header>20 );21}2223export function Delta({ value, digits = 2, className }: { value: number | null | undefined; digits?: number; className?: string }) {24 return <span className={cn('num font-medium', deltaClass(value), className)}>{fmtPct(value, digits)}</span>;25}2627export function Badge({ children, tone = 'neutral', className }: { children: ReactNode; tone?: 'neutral' | 'gain' | 'loss' | 'index' | 'rarity' | 'alert' | 'gold'; className?: string }) {28 const tones: Record<string, string> = {29 neutral: 'bg-inset text-muted',30 gain: 'bg-gain-bg text-gain',31 loss: 'bg-loss-bg text-loss',32 index: 'bg-index-bg text-index',33 rarity: 'bg-rarity-bg text-rarity',34 alert: 'bg-alert-bg text-alert',35 gold: 'bg-alert-bg text-gold',36 };37 return <span className={cn('inline-flex items-center rounded-sm px-1.5 py-0.5 text-[11px] font-medium leading-4 tracking-wide', tones[tone], className)}>{children}</span>;38}3940export function Stat({ label, value, sub, className }: { label: ReactNode; value: ReactNode; sub?: ReactNode; className?: string }) {41 return (42 <div className={cn('flex flex-col gap-0.5', className)}>43 <span className="text-[11px] font-medium uppercase tracking-wider text-subtle">{label}</span>44 <span className="num text-lg font-semibold leading-tight text-fg">{value}</span>45 {sub ? <span className="text-xs text-muted">{sub}</span> : null}46 </div>47 );48}4950export function EmptyState({ title, description, action, className }: { title: ReactNode; description?: ReactNode; action?: ReactNode; className?: string }) {51 return (52 <div className={cn('flex flex-col items-center justify-center gap-2 px-6 py-12 text-center', className)}>53 <p className="text-sm font-medium text-fg">{title}</p>54 {description ? <p className="max-w-md text-xs leading-relaxed text-muted">{description}</p> : null}55 {action ? <div className="mt-2">{action}</div> : null}56 </div>57 );58}5960/** Honest placeholder for metrics without enough evidence (§191–§192). */61export function Unavailable({ reason = 'Data unavailable', className }: { reason?: string; className?: string }) {62 return <span className={cn('text-subtle', className)} title={reason}>—</span>;63}6465export function SectionTitle({ children, href, hrefLabel = 'View all', className }: { children: ReactNode; href?: string; hrefLabel?: string; className?: string }) {66 return (67 <div className={cn('mb-3 flex items-baseline justify-between gap-3', className)}>68 <h2 className="text-base font-semibold tracking-tight text-fg">{children}</h2>69 {href ? (70 <Link href={href} className="text-xs font-medium text-muted hover:text-fg">71 {hrefLabel} →72 </Link>73 ) : null}74 </div>75 );76}7778export function Skeleton({ className }: { className?: string }) {79 return <div className={cn('animate-pulse rounded-sm bg-inset', className)} aria-hidden />;80}8182export function Table({ children, className, dense = true }: { children: ReactNode; className?: string; dense?: boolean }) {83 return (84 <div className={cn('w-full overflow-x-auto scrollbar-none', className)}>85 <table className={cn('w-full border-collapse text-left', dense ? 'text-[13px]' : 'text-sm')}>{children}</table>86 </div>87 );88}89export const th = 'sticky top-0 z-[1] whitespace-nowrap border-b border-border bg-elevated px-3 py-2 text-[11px] font-medium uppercase tracking-wider text-subtle';90export const td = 'whitespace-nowrap border-b border-border px-3 py-2 align-middle';91export const tdNum = `${td} num text-right`;92