SPB Git forge
3commits 1branches 0releases
417.0 KBsize
maindefault branch
10 days agolast push
TypeScript 66.5% Python 30.9% JavaScript 1.4% CSS 0.7%
4.8 KB · 102 lines tsx
Raw Blame History
1import Link from 'next/link';2import type { ReactNode } from 'react';3import { cn } from '@/lib/cn';45/** Long-form typography primitives for the meta pages (methodology, about, privacy, terms, developers). Server-safe. */67export function Prose({ children, className }: { children: ReactNode; className?: string }) {8  return <div className={cn('max-w-3xl text-[15px] leading-relaxed text-ink-2 [&_p+p]:mt-4 [&_strong]:font-semibold [&_strong]:text-ink [&_ul]:mt-3 [&_ul]:space-y-1.5 [&_ul]:pl-5 [&_ul]:list-disc [&_ol]:mt-3 [&_ol]:space-y-1.5 [&_ol]:pl-5 [&_ol]:list-decimal [&_code]:mono [&_code]:rounded [&_code]:bg-plane-2 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:text-[13px] [&_code]:text-ink', className)}>{children}</div>;9}1011/** Anchored section with a hairline on top; `id` is used by the TOC. */12export function DocSection({ id, eyebrow, title, children, className }: { id: string; eyebrow?: string; title: ReactNode; children: ReactNode; className?: string }) {13  return (14    <section id={id} className={cn('scroll-mt-[calc(var(--header-h)+1rem)] border-t border-rule py-8 md:py-10', className)}>15      {eyebrow && <p className="eyebrow">{eyebrow}</p>}16      <h2 className="mt-1 text-xl font-semibold tracking-tight md:text-2xl">17        <a href={`#${id}`} className="hover:text-accent">18          {title}19        </a>20      </h2>21      <div className="mt-4">{children}</div>22    </section>23  );24}2526export function SubHeading({ children, id }: { children: ReactNode; id?: string }) {27  return (28    <h3 id={id} className="mt-6 scroll-mt-[calc(var(--header-h)+1rem)] text-base font-semibold text-ink">29      {children}30    </h3>31  );32}3334/** Sticky mini table of contents (desktop); a compact inline list on mobile. DOM order = visual order (TOC first on all breakpoints). */35export function Toc({ items, className }: { items: { id: string; label: string }[]; className?: string }) {36  return (37    <nav aria-label="Contents" className={cn('min-w-0 max-w-full lg:sticky lg:top-[calc(var(--header-h)+1.5rem)] lg:self-start', className)}>38      <p className="eyebrow mb-2">Contents</p>39      <ol className="no-scrollbar flex max-w-full gap-1 overflow-x-auto lg:block lg:space-y-0.5 lg:overflow-visible">40        {items.map((it, i) => (41          <li key={it.id} className="shrink-0">42            <a href={`#${it.id}`} className="inline-flex min-h-9 items-center gap-2 rounded-md border border-rule px-2.5 text-xs text-ink-2 hover:bg-plane-2 hover:text-ink lg:min-h-8 lg:border-0 lg:px-0 lg:text-[13px]">43              <span className="mono text-[10px] text-ink-3">{String(i + 1).padStart(2, '0')}</span>44              {it.label}45            </a>46          </li>47        ))}48      </ol>49    </nav>50  );51}5253/** Two-column doc layout: TOC (left, sticky on desktop) + content. */54export function DocLayout({ toc, children }: { toc: { id: string; label: string }[]; children: ReactNode }) {55  return (56    <div className="grid gap-8 lg:grid-cols-[200px_minmax(0,1fr)] lg:gap-14">57      <Toc items={toc} />58      <div className="min-w-0">{children}</div>59    </div>60  );61}6263/** Code block in mono on the secondary plane; horizontal scroll instead of overflow. */64export function Code({ children, className, label }: { children: string; className?: string; label?: string }) {65  return (66    <figure className={cn('mt-3 min-w-0 overflow-hidden rounded-md border border-rule bg-plane-2', className)}>67      {label && <figcaption className="eyebrow border-b border-rule px-3 py-1.5">{label}</figcaption>}68      <pre className="scrollbar-thin overflow-x-auto p-3 text-[12.5px] leading-relaxed text-ink">69        <code className="mono">{children}</code>70      </pre>71    </figure>72  );73}7475/** Definition list rendered as hairline rows (label / value). */76export function KeyValues({ rows, className }: { rows: { k: ReactNode; v: ReactNode }[]; className?: string }) {77  return (78    <dl className={cn('divide-y divide-rule border-y border-rule', className)}>79      {rows.map((r, i) => (80        <div key={i} className="grid gap-1 py-2.5 sm:grid-cols-[200px_minmax(0,1fr)] sm:gap-4">81          <dt className="text-xs uppercase tracking-wider text-ink-3 sm:pt-0.5">{r.k}</dt>82          <dd className="min-w-0 break-words text-sm text-ink">{r.v}</dd>83        </div>84      ))}85    </dl>86  );87}8889export function Callout({ children, tone = 'info', className }: { children: ReactNode; tone?: 'info' | 'warn'; className?: string }) {90  return (91    <div className={cn('mt-4 rounded-md border-l-2 px-4 py-3 text-sm leading-relaxed', tone === 'warn' ? 'border-warn bg-warn-soft text-ink' : 'border-accent bg-accent-soft text-ink', className)}>{children}</div>92  );93}9495export function MethodologyLink({ anchor, children }: { anchor?: string; children?: ReactNode }) {96  return (97    <Link href={anchor ? `/methodology#${anchor}` : '/methodology'} className="link text-sm">98      {children ?? 'Methodology'}99    </Link>100  );101}102