import Link from 'next/link'; import type { ReactNode } from 'react'; import { cn, deltaClass, fmtPct } from '@/lib/format'; /** Minimal, dense UI primitives. Keep them boring; the data is the design. */ export function Card({ children, className, as: Tag = 'section', id }: { children: ReactNode; className?: string; as?: 'section' | 'div' | 'article'; id?: string }) { return ( {children} ); } export function CardHeader({ title, subtitle, action, className, icon }: { title: ReactNode; subtitle?: ReactNode; action?: ReactNode; className?: string; icon?: ReactNode }) { return ( {icon ? {icon} : null} {title} {subtitle ? {subtitle} : null} {action ? {action} : null} ); } export function Delta({ value, digits = 2, className, arrow = false }: { value: number | null | undefined; digits?: number; className?: string; arrow?: boolean }) { const has = value !== null && value !== undefined && Number.isFinite(value); const glyph = !has || Math.abs(value) < 0.00005 ? '' : value > 0 ? '▲ ' : '▼ '; return ( {arrow && glyph ? {glyph} : null} {fmtPct(value, digits)} ); } /** * Ask vs RareIndex Valuation (§83–§84, §196). `discount` = (ask − RIV) / RIV as stored: negative = the ask * is below the valuation. Anything implausible (ask < 10 % or > 10× RIV) is a data/identity anomaly and is * labelled as such instead of being shown as a percentage — never as a "deal". */ export function VsRiv({ discount, className, digits = 1, showLabel = true }: { discount: number | null | undefined; className?: string; digits?: number; showLabel?: boolean }) { if (discount === null || discount === undefined || !Number.isFinite(discount)) return —; const ratio = 1 + discount; if (ratio < 0.1 || ratio > 10) { return ( Data/identity anomaly ); } if (discount < -0.5) { return ( Needs review · {fmtPct(discount, 0)} ); } const tone = discount <= -0.1 ? 'text-gain' : discount >= 0.1 ? 'text-loss' : 'text-flat'; const word = discount <= -0.1 ? 'below RIV' : discount >= 0.1 ? 'above RIV' : 'near RIV'; return ( {fmtPct(discount, digits)} {showLabel ? vs RIV : null} ); } export function Badge({ children, tone = 'neutral', className, dot = false }: { children: ReactNode; tone?: 'neutral' | 'gain' | 'loss' | 'index' | 'rarity' | 'alert' | 'gold'; className?: string; dot?: boolean }) { const tones: Record = { neutral: 'bg-inset text-muted', gain: 'bg-gain-bg text-gain', loss: 'bg-loss-bg text-loss', index: 'bg-index-bg text-index', rarity: 'bg-rarity-bg text-rarity', alert: 'bg-alert-bg text-alert', gold: 'bg-alert-bg text-gold', }; return ( {dot ? : null} {children} ); } export function Stat({ label, value, sub, className, size = 'md' }: { label: ReactNode; value: ReactNode; sub?: ReactNode; className?: string; size?: 'sm' | 'md' | 'lg' }) { return ( {label} {value} {sub ? {sub} : null} ); } /** Dense KPI strip used under page headers and in the hero. */ export function StatStrip({ items, className, cols }: { items: Array<{ label: ReactNode; value: ReactNode; sub?: ReactNode; href?: string }>; className?: string; cols?: string }) { const grid = cols ?? (items.length >= 8 ? 'grid-cols-2 sm:grid-cols-4 lg:grid-cols-8' : items.length >= 6 ? 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-6' : items.length === 5 ? 'grid-cols-2 sm:grid-cols-5' : items.length === 4 ? 'grid-cols-2 sm:grid-cols-4' : 'grid-cols-2 sm:grid-cols-3'); return ( {items.map((it, i) => { const inner = ( <> {it.label} {it.value} {it.sub ? {it.sub} : null} > ); return it.href ? ( {inner} ) : ( {inner} ); })} ); } export function EmptyState({ title, description, action, className, compact }: { title: ReactNode; description?: ReactNode; action?: ReactNode; className?: string; compact?: boolean }) { return ( {title} {description ? {description} : null} {action ? {action} : null} ); } /** Honest placeholder for metrics without enough evidence (§191–§192). */ export function Unavailable({ reason = 'Data unavailable', className }: { reason?: string; className?: string }) { return ( — ); } export function SectionTitle({ children, href, hrefLabel = 'View all', className, subtitle }: { children: ReactNode; href?: string; hrefLabel?: string; className?: string; subtitle?: ReactNode }) { return ( {children} {subtitle ? {subtitle} : null} {href ? ( {hrefLabel} → ) : null} ); } export function Skeleton({ className }: { className?: string }) { return ; } /** Skeleton matching a dense table (header + n rows) to avoid layout shift. */ export function TableSkeleton({ rows = 8, className }: { rows?: number; className?: string }) { return ( {Array.from({ length: rows }).map((_, i) => ( ))} ); } export function Table({ children, className, dense = true, sticky = true }: { children: ReactNode; className?: string; dense?: boolean; sticky?: boolean }) { return ( {children} ); } export const th = 'sticky top-0 z-[1] whitespace-nowrap border-b border-border bg-elevated px-3 py-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-subtle'; export const td = 'whitespace-nowrap border-b border-border px-3 py-2 align-middle'; export const tdNum = `${td} num text-right`; /** Sequential page "kicker" — small uppercase label above a title. */ export function Kicker({ children, className }: { children: ReactNode; className?: string }) { return {children}; } export function Divider({ className }: { className?: string }) { return ; }
{subtitle}
{title}
{description}
{children}