SPB Git

spb/rareindex

Public
4.5 KB · 92 lines tsx
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
Files · d93392862678cbf30e175697fcb812bc4ff0b5a7
Menu

Theme