SPB Git forge

spb/spinza

Public
8commits 1branches 0releases
1.6 MBsize
maindefault branch
16 days agolast push
TypeScript 97.6% SQL 1.4% JavaScript 0.5%
2.8 KB · 65 lines tsx
Raw Blame History
1import Link from "next/link";2import { AppShell } from "@/components/shell/app-shell";3import { SiteFooter } from "./site-footer";4import { cn } from "@/lib/utils";56/** Long-form page shell: eyebrow, title, lead, sectioned prose, footer. */7export function ProsePage({ eyebrow, title, lead, updated, children, toc }: { eyebrow: string; title: string; lead: string; updated?: string; children: React.ReactNode; toc?: { id: string; label: string }[] }) {8  return (9    <AppShell>10      <article className="mx-auto max-w-3xl">11        <header className="mb-10">12          <div className="eyebrow mb-2">{eyebrow}</div>13          <h1 className="text-4xl font-semibold tracking-[-0.03em] sm:text-5xl">{title}</h1>14          <p className="mt-4 text-lg leading-relaxed text-fg-2">{lead}</p>15          {updated ? <p className="mt-3 text-[12px] text-fg-4">Last updated {updated}</p> : null}16        </header>17        {toc?.length ? (18          <nav aria-label="On this page" className="mb-10 rounded-lg border border-line p-4">19            <div className="eyebrow mb-2">On this page</div>20            <ol className="grid gap-1.5 text-sm sm:grid-cols-2">21              {toc.map((t, i) => (22                <li key={t.id}>23                  <Link href={`#${t.id}`} className="inline-flex items-baseline gap-2 text-fg-2 hover:text-fg">24                    <span className="font-mono text-[11px] text-fg-4">{String(i + 1).padStart(2, "0")}</span> {t.label}25                  </Link>26                </li>27              ))}28            </ol>29          </nav>30        ) : null}31        <div className="prose-spinza space-y-10">{children}</div>32        <SiteFooter className="mt-20" />33      </article>34    </AppShell>35  );36}3738export function Section({ id, title, children, className }: { id: string; title: string; children: React.ReactNode; className?: string }) {39  return (40    <section id={id} aria-labelledby={`${id}-h`} className={cn("scroll-mt-24", className)}>41      <h2 id={`${id}-h`} className="text-2xl font-semibold tracking-tight">42        {title}43      </h2>44      <div className="mt-3 space-y-3 text-[15px] leading-relaxed text-fg-2">{children}</div>45    </section>46  );47}4849export function Callout({ children, tone = "accent" }: { children: React.ReactNode; tone?: "accent" | "neutral" }) {50  return <div className={cn("rounded-md border p-4 text-[15px] leading-relaxed", tone === "accent" ? "border-accent/30 bg-accent-soft text-fg" : "border-line bg-surface text-fg-2")}>{children}</div>;51}5253export function Bullets({ items }: { items: React.ReactNode[] }) {54  return (55    <ul className="space-y-2">56      {items.map((it, i) => (57        <li key={i} className="flex gap-3">58          <span className="mt-[9px] h-1.5 w-1.5 shrink-0 rounded-full bg-accent" aria-hidden />59          <span>{it}</span>60        </li>61      ))}62    </ul>63  );64}65