SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
8.0 KB · 192 lines tsx
Raw Blame History
1import * as React from "react";2import Link from "next/link";3import { cn } from "@/lib/utils";4import { InlineCode } from "@/components/ui/code-block";56/**7 * Prose primitives for the documentation. Implemented with plain Tailwind classes8 * (no typography plugin) so the docs share the exact tokens used by the product.9 */1011function textOf(node: React.ReactNode): string {12  if (node == null || typeof node === "boolean") return "";13  if (typeof node === "string" || typeof node === "number") return String(node);14  if (Array.isArray(node)) return node.map(textOf).join("");15  if (React.isValidElement<{ children?: React.ReactNode }>(node)) return textOf(node.props.children);16  return "";17}1819export function headingId(children: React.ReactNode, explicit?: string): string {20  if (explicit) return explicit;21  return (22    textOf(children)23      .normalize("NFD")24      .replace(/[̀-ͯ]/g, "")25      .toLowerCase()26      .trim()27      .replace(/[^a-z0-9]+/g, "-")28      .replace(/^-+|-+$/g, "")29      .slice(0, 80) || "section"30  );31}3233function AnchorLink({ id }: { id: string }) {34  return (35    <a36      href={`#${id}`}37      aria-label="Link to this section"38      className="ml-2 inline-block select-none text-fg-subtle/0 transition-colors group-hover:text-fg-subtle focus-visible:text-fg-subtle"39    >40      #41    </a>42  );43}4445export function H2({ id, children, className }: { id?: string; children: React.ReactNode; className?: string }) {46  const hid = headingId(children, id);47  return (48    <h2 id={hid} data-toc data-toc-level="2" className={cn("group mt-12 mb-4 scroll-mt-24 text-[20px] font-semibold tracking-tight text-fg first:mt-0", className)}>49      {children}50      <AnchorLink id={hid} />51    </h2>52  );53}5455export function H3({ id, children, className }: { id?: string; children: React.ReactNode; className?: string }) {56  const hid = headingId(children, id);57  return (58    <h3 id={hid} data-toc data-toc-level="3" className={cn("group mt-8 mb-3 scroll-mt-24 text-[15.5px] font-semibold tracking-tight text-fg", className)}>59      {children}60      <AnchorLink id={hid} />61    </h3>62  );63}6465export function H4({ children, className }: { children: React.ReactNode; className?: string }) {66  return <h4 className={cn("mt-6 mb-2 text-[13.5px] font-semibold text-fg", className)}>{children}</h4>;67}6869export function P({ children, className }: { children: React.ReactNode; className?: string }) {70  return <p className={cn("my-4 text-[14.5px] leading-[1.7] text-fg-muted", className)}>{children}</p>;71}7273export function Lead({ children, className }: { children: React.ReactNode; className?: string }) {74  return <p className={cn("my-4 text-[16px] leading-[1.65] text-fg-muted", className)}>{children}</p>;75}7677export function Strong({ children }: { children: React.ReactNode }) {78  return <strong className="font-medium text-fg">{children}</strong>;79}8081export function Ul({ children, className }: { children: React.ReactNode; className?: string }) {82  return <ul className={cn("my-4 list-disc space-y-1.5 pl-5 text-[14.5px] leading-[1.7] text-fg-muted marker:text-fg-subtle", className)}>{children}</ul>;83}8485export function Ol({ children, className }: { children: React.ReactNode; className?: string }) {86  return <ol className={cn("my-4 list-decimal space-y-1.5 pl-5 text-[14.5px] leading-[1.7] text-fg-muted marker:text-fg-subtle marker:tabular", className)}>{children}</ol>;87}8889export function Li({ children, className }: { children: React.ReactNode; className?: string }) {90  return <li className={cn("pl-0.5", className)}>{children}</li>;91}9293export function Code({ children }: { children: React.ReactNode }) {94  return <InlineCode>{children}</InlineCode>;95}9697export function A({ href, children, className }: { href: string; children: React.ReactNode; className?: string }) {98  const external = /^https?:\/\//.test(href) || href.startsWith("mailto:");99  const cls = cn("font-medium text-accent underline decoration-accent/30 underline-offset-[3px] transition-colors hover:decoration-accent", className);100  if (external) {101    return (102      <a href={href} className={cls} target={href.startsWith("mailto:") ? undefined : "_blank"} rel="noreferrer">103        {children}104      </a>105    );106  }107  return (108    <Link href={href} className={cls}>109      {children}110    </Link>111  );112}113114export function Kbd({ children }: { children: React.ReactNode }) {115  return <kbd className="rounded-[4px] border border-border bg-bg-muted px-1.5 py-0.5 font-mono text-[0.8em] text-fg">{children}</kbd>;116}117118export function Hr() {119  return <hr className="my-10 border-border" />;120}121122/* ---------------------------------------------------------------------------------------------- */123/* Tables                                                                                          */124/* ---------------------------------------------------------------------------------------------- */125126export function Table({ children, className, dense }: { children: React.ReactNode; className?: string; dense?: boolean }) {127  return (128    <div className={cn("my-5 overflow-x-auto rounded-lg border border-border scrollbar-thin", className)}>129      <table className={cn("w-full border-collapse text-left text-[13.5px] leading-relaxed", dense && "text-[13px]")}>{children}</table>130    </div>131  );132}133134export function THead({ children }: { children: React.ReactNode }) {135  return <thead className="bg-bg-subtle text-[12px] font-medium uppercase tracking-wide text-fg-subtle">{children}</thead>;136}137138export function TBody({ children }: { children: React.ReactNode }) {139  return <tbody className="divide-y divide-border">{children}</tbody>;140}141142export function Tr({ children, className }: { children: React.ReactNode; className?: string }) {143  return <tr className={cn("align-top", className)}>{children}</tr>;144}145146export function Th({ children, className }: { children?: React.ReactNode; className?: string }) {147  return <th className={cn("px-3.5 py-2 font-medium", className)}>{children}</th>;148}149150export function Td({ children, className, mono }: { children?: React.ReactNode; className?: string; mono?: boolean }) {151  return <td className={cn("px-3.5 py-2.5 text-fg-muted", mono && "font-mono text-[12.5px] text-fg", className)}>{children}</td>;152}153154/* ---------------------------------------------------------------------------------------------- */155/* Small layout helpers                                                                            */156/* ---------------------------------------------------------------------------------------------- */157158export function Steps({ children }: { children: React.ReactNode }) {159  return <ol className="my-6 space-y-0 border-l border-border pl-0 [counter-reset:step]">{children}</ol>;160}161162export function Step({ title, children }: { title: React.ReactNode; children?: React.ReactNode }) {163  return (164    <li className="relative pb-8 pl-8 last:pb-0 [counter-increment:step]">165      <span166        aria-hidden167        className="absolute -left-[13px] top-0 flex size-[26px] items-center justify-center rounded-full border border-border bg-bg font-mono text-[11.5px] font-medium text-fg-muted tabular before:content-[counter(step)]"168      />169      <div className="text-[15px] font-semibold text-fg">{title}</div>170      {children ? <div className="mt-1 [&>p]:my-2 [&>ul]:my-2">{children}</div> : null}171    </li>172  );173}174175export function CardGrid({ children, className }: { children: React.ReactNode; className?: string }) {176  return <div className={cn("my-6 grid gap-3 sm:grid-cols-2", className)}>{children}</div>;177}178179export function LinkCard({ href, title, description }: { href: string; title: string; description: string }) {180  return (181    <Link href={href} className="group rounded-lg border border-border bg-bg p-4 transition-colors hover:border-border-strong hover:bg-bg-subtle">182      <div className="flex items-center justify-between gap-2 text-[14px] font-medium text-fg">183        {title}184        <span aria-hidden className="text-fg-subtle transition-transform group-hover:translate-x-0.5">185          &rarr;186        </span>187      </div>188      <p className="mt-1 text-[13px] leading-relaxed text-fg-muted">{description}</p>189    </Link>190  );191}192