SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
4.4 KB · 100 lines tsx
Raw Blame History
1import Link from 'next/link';2import type { ReactNode } from 'react';3import { cn } from '@/lib/cn';45/** Page-width wrapper. Pages render inside <main> without padding: wrap content in Container (1280) or Container wide (1600). */6export function Container({ children, className, wide = false }: { children: ReactNode; className?: string; wide?: boolean }) {7  return <div className={cn('container-x mx-auto w-full', wide ? 'max-w-[1600px]' : 'max-w-[1280px]', className)}>{children}</div>;8}910/** Section with eyebrow + title + optional action link. Spatial composition with a hairline — not a card. */11export function Section({12  eyebrow,13  title,14  lede,15  action,16  children,17  className,18  id,19  hairline = true,20}: {21  eyebrow?: string;22  title?: ReactNode;23  lede?: ReactNode;24  action?: { href: string; label: string };25  children: ReactNode;26  className?: string;27  id?: string;28  hairline?: boolean;29}) {30  return (31    <section id={id} className={cn('section-y scroll-mt-20', hairline && 'hairline', className)}>32      {(eyebrow || title) && (33        <div className="mb-4 flex items-end justify-between gap-4 md:mb-5">34          <div className="min-w-0">35            {eyebrow && <p className="eyebrow">{eyebrow}</p>}36            {title && <h2 className="mt-1 text-lg font-semibold tracking-tight md:text-xl">{title}</h2>}37            {lede && <p className="mt-1 max-w-2xl text-sm text-ink-2">{lede}</p>}38          </div>39          {action && (40            <Link href={action.href} className="link shrink-0 py-1 text-sm">41              {action.label} →42            </Link>43          )}44        </div>45      )}46      {children}47    </section>48  );49}5051export function PageHeader({ eyebrow, title, lede, children, className, aside }: { eyebrow?: ReactNode; title: ReactNode; lede?: ReactNode; children?: ReactNode; className?: string; aside?: ReactNode }) {52  return (53    <div className={cn('pb-5 pt-7 md:pb-7 md:pt-10', className)}>54      <div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">55        <div className="min-w-0">56          {eyebrow && <div className="eyebrow flex flex-wrap items-center gap-2">{eyebrow}</div>}57          <h1 className="display mt-2 text-[28px] md:text-[40px]">{title}</h1>58          {lede && <p className="mt-3 max-w-2xl text-[15px] leading-relaxed text-ink-2 md:text-base">{lede}</p>}59        </div>60        {aside && <div className="shrink-0">{aside}</div>}61      </div>62      {children}63    </div>64  );65}6667/** Stat tile: big tabular number + label + optional delta/hint. Composes in a hairline grid, never in a card. */68export function Stat({ label, value, hint, delta, className, accent = false, href }: { label: string; value: ReactNode; hint?: ReactNode; delta?: { value: string; tone?: 'positive' | 'negative' | 'neutral' }; className?: string; accent?: boolean; href?: string }) {69  const body = (70    <>71      <p className="eyebrow">{label}</p>72      <p className={cn('tnum mt-1 text-[26px] font-semibold leading-none tracking-tight md:text-[32px]', accent && 'text-accent')}>{value}</p>73      {(hint || delta) && (74        <p className="mt-1.5 flex items-center gap-2 text-xs text-ink-3">75          {delta && <span className={cn('tnum font-medium', delta.tone === 'positive' && 'text-positive', delta.tone === 'negative' && 'text-danger', (!delta.tone || delta.tone === 'neutral') && 'text-accent-2')}>{delta.value}</span>}76          {hint}77        </p>78      )}79    </>80  );81  if (href)82    return (83      <Link href={href} className={cn('block min-w-0 py-3 hover:text-accent md:py-4', className)}>84        {body}85      </Link>86    );87  return <div className={cn('min-w-0 py-3 md:py-4', className)}>{body}</div>;88}8990/** Grid of Stat tiles separated by hairlines (2 columns on mobile, n on desktop). */91export function StatGrid({ children, cols = 4, className }: { children: ReactNode; cols?: 3 | 4 | 5 | 6 | 8; className?: string }) {92  const desktop = { 3: 'md:grid-cols-3', 4: 'md:grid-cols-4', 5: 'md:grid-cols-5', 6: 'md:grid-cols-6', 8: 'md:grid-cols-4 lg:grid-cols-8' }[cols];93  return <div className={cn('grid grid-cols-2 gap-x-6 border-y border-rule [&>*]:border-b [&>*]:border-rule md:[&>*]:border-b-0', desktop, className)}>{children}</div>;94}9596/** Small honest note (derived / estimated / methodology pointer). */97export function Note({ children, className }: { children: ReactNode; className?: string }) {98  return <p className={cn('text-xs leading-relaxed text-ink-3', className)}>{children}</p>;99}100