# @supertype.ai/foundations

The shared design layer for Supertype's Next apps: typography primitives,
content blocks, a long-form essay shell, token and theme CSS, and build-time
tooling for SEO, OG cards, lint rules and contrast checks.

This file is for coding agents working in an app that consumes the package. It
covers what to import, how the copy should read, and the mistakes that pass
silently. Full reference: https://github.com/supertypeai/foundations

## Rules

1. **Never write type styles by hand.** If you are about to write `text-sm`,
   `text-xs`, `font-medium`, `leading-tight`, `text-muted-foreground` or similar
   on a text element, there is a primitive for it. Use that instead.
2. **Retune with CSS variables, not classes.** The package owns its classnames.
   Change a `--text-*` rung, `--heading-weight` or a colour token. Do not
   override the package's utilities.
3. **Import from the right entry point.** `blocks`, `essay`, `mdx`, `seo`, `og`,
   `eslint`, `rehype` and `contrast` are subpaths, not part of the root barrel.
4. **Use colour tokens, never literal colours.** `bg-card`, `text-foreground`,
   `border-border`. No hex values, no `bg-zinc-800`, no `dark:` overrides that
   swap one token for another.
5. **Paint a surface, hand down its ink.** Any element you give a background
   needs `INK_ON_FILL` (a tone fill) or one of `INK_ON_CARD`, `INK_ON_POPOVER`,
   `INK_ON_SIDEBAR` (a tinted one), or the type inside it keeps the page's ink
   and fails contrast silently. Never build one of these class strings
   yourself: Tailwind only generates a class it can read as text, so an
   assembled one styles nothing. For a surface with no constant, spread
   `inkOnSurfaceStyle(token)` into `style`.
6. **Write the words like a person would.** See Writing copy below. It applies
   to every string a reader sees and to the comments you leave behind.

## Which component

| you want | use | from |
|---|---|---|
| a page title | `TypographyH1` (add `variant="display"` on marketing pages) | root |
| a section heading | `TypographyH2` (add `divider` for a rule under it) | root |
| a subheading | `TypographyH3`, `TypographyH4` | root |
| a kicker above a heading | `TypographyEyebrow` | root |
| interface copy, 13px | `TypographyP` | root |
| the same, secondary ink | `TypographyMuted` | root |
| reading copy, 18px | `TypographyProse` | root |
| a bulleted or numbered list | `TypographyList`, `TypographyProseList` | root |
| a timestamp, count or value | `TypographyCaption` | root |
| a form label or column header | `TypographyLabel` | root |
| small print as a block | `TypographySmall` | root |
| a number or metric | `TypographyStat` | root |
| inline code | `TypographyInlineCode` | root |
| a link | `TypographyLink` | root |
| an anchor around something already drawn | `Anchor` | `/blocks` |
| where an href goes, without a component | `resolveLink` | root |
| a highlighted phrase | `TypographyHighlight` | root |
| a button, or a link that looks like one | `Button` | `/blocks` |
| a status label, count or chip | `Badge` | `/blocks` |
| a link card, or a grid of them | `Card`, `Cards` | `/blocks` |
| an aside, warning or note | `Callout` | `/blocks` |
| numbered instructions | `Steps`, `Step` | `/blocks` |
| an announcement, release note or credits panel | `Bulletin` | `/blocks` |
| a compact statement block for a page, section, or footer | `Colophon` | `/blocks` |
| the same statement as one item in a footer row | `BuiltWithFoundations` | `/blocks` |
| an FAQ or collapsible, no JS | `Disclosure`, `DisclosureGroup` | `/blocks` |
| an animated accordion | `Accordion` (client) | `/blocks` |
| tabbed content | `TabGroup` when the tabs are data, `Tabs` to compose by hand (client) | `/blocks` |
| a long-form article page | `EssayHeader` + `EssayLayout` + `EssaySection` | `/essay` |
| an article whose body is prose or MDX | `EssayHeader` + `ReadingLayout` | `/essay` |
| a post meta row (date, read time, tags) | `PostMetaRow` and friends | `/essay` |
| a table of contents | `TableOfContents`, `ReadingRail` | `/essay` |
| page metadata and JSON-LD | `createSeo` | `/seo` |
| an OG image | `ogCard`, `OG_SIZE` | `/og` |
| to merge classnames | `cn` | root |

## Entry points

| import | exports |
|---|---|
| `@supertype.ai/foundations` | `cn`, `TypographyH1`, `TypographyH2`, `TypographyH3`, `TypographyH4`, `TypographyEyebrow`, `TypographyP`, `TypographyMuted`, `TypographyProse`, `TypographyList`, `TypographyProseList`, `TypographyCaption`, `TypographySmall`, `TypographyLabel`, `TypographyStat`, `TypographyInlineCode`, `TypographyLink`, `TypographyHighlight`, `headingClass`, `headingFace`, `eyebrowClass`, `toneClass`, `impliedTone`, `resolveLink`, `isExternalHref`. Types: `TypographyTag`, `ParagraphVariants`, `ListProps`, `CaptionVariants`, `LabelVariants`, `StatVariants`, `Tone`, `HighlightTone`, `LinkBehavior`, `ResolvedLink` |
| `@supertype.ai/foundations/blocks` | `Anchor`, `Cards`, `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `Callout`, `Button`, `buttonVariants`, `Badge`, `badgeVariants`, `Steps`, `Step`, `Disclosure`, `DisclosureGroup`, `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent`, `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`, `TabGroup`, `SEGMENT`, `Bulletin`, `Ribbon`, `EDITORIAL_INKS`, `Colophon`, `BuiltWithFoundations`, `FoundationsMark`, `FOUNDATIONS_URL`. Types: `ButtonLook`, `BadgeLook`, `TabItem`, `BulletinProps`, `BulletinPoint`, `RibbonHue`, `ColophonProps` |
| `@supertype.ai/foundations/mdx` | `proseMdxComponents` |
| `@supertype.ai/foundations/essay` | `createEssay`, `EssayHeader`, `EssayLayout`, `EssaySection`, `EssayPullQuote`, `EssayFigure`, `EssayMovements`, `EssayDocument`, `EssayColumns`, `EssayAside`, `EssayBody`, `ReadingLayout`, `TableOfContents`, `ReadingRail`, `ReadingProgressBar`, `Rail`, `RailLink`, `PostMetaRow`, `PostDate`, `ReadTime`, `TagPills`, `MetaDot`, `formatPostDate`, `extractHeadings`, `readingTime`, `createSlugger`, `useReadingProgress`, `useScrollSpy`. Types: `TocHeading`, `EssayDecorations`, `EssayIndexEntry`, `EssayDocSection`, `EssayMovement`, `PostDateFormat` |
| `@supertype.ai/foundations/seo` | `createSeo`. Types: `SeoConfig`, `ArticleAuthor`, `ArticleOptions`, `PageMetadata` |
| `@supertype.ai/foundations/og` | `ogCard`, `OG_SIZE`. Types: `OgCardOptions` |
| `@supertype.ai/foundations/eslint` | `designRules` (every rule as one array, the one to spread), `designConfig` (the same set wrapped as a flat-config entry). The builders `colourRules`, `typographyRules`, `linkRules`, `themeOverrideRules`, `surfaceAsInkRules`, `renamedTokenRules` are exported too, though spreading them by hand is how a consumer ends up missing one. Types: `FlatConfigEntry`, `DesignRuleOptions`, `DesignConfigOptions`, `RestrictedSyntax`, `ColourOptions`, `TypographyOptions` |
| `@supertype.ai/foundations/rehype` | `rehypeProseCode`, `proseCodeOptions`, `PROSE_LANGS`, `PROSE_THEMES`. Build-time only, must not resolve React |
| `@supertype.ai/foundations/contrast` | `checkLegibility` (inks at 4.5:1), `checkSignals` (fills at 3:1 — status hues, the categorical earth hues and the six chart series alike — tinted inks at 4.5:1, `--subtle-foreground` at the 3:1 it is documented for, labels against their own fill: the tone rows are read off `TONE` and each cut is resolved along its `var()` fallback chain, so an app that declares `--brand` without `--brand-foreground` is measured on the label the cascade really reaches for rather than skipped), `checkHairlines` (`--border` and `--input` at 1.4:1 on `--background` and `--card`, `--sidebar-border` on `--sidebar`: a rule is exempt from the ink and mark bars, but it still has to read as the same weight in both themes), `resolveTokens`, `formatFailures`, `specificity`, `parseColor`, `luminance`, `contrast` (WCAG ratio), `lc` (APCA lightness contrast: polarity-aware, for checking that an ink ramp is perceptually ordered rather than merely ordered by ratio), `tokenCuts` (which cuts a token ships: fill, the label printed on it, the hue as words, the taxonomy `checkSignals` measures against). Types: `Rgb`, `Theme`, `LegibilityFailure`, `TokenCuts`. Build-time only |

## Props worth knowing

- `TypographyH1`, `TypographyH3`: `variant?: "default" | "display"`.
- `TypographyH2`: `variant?`, plus `divider?: boolean` for a rule underneath.
- `TypographyP`: `variant?: "ui" | "prose"` (default `ui`), `tone?: "default" | "muted"`.
- `TypographyMuted`, `TypographyProse`, `TypographyProseList` are presets. The
  prop each one pins is dropped from its type, so passing it fails to compile.
- `TypographyCaption`, `TypographyLabel`, `TypographySmall`: `size?: "sm" | "xs" | "2xs" | "inherit"`, `as?`.
- `TypographyEyebrow`: `tone?: "heading" | "label" | "muted" | "subtle"`, `size?: "sm" | "xs" | "2xs" | "3xs"`, `as?`. Each tone carries the rung it is usually set at and `size` overrides it, so omitting it changes nothing. Reach for `muted` for the uppercase micro-label a dense product sets over a group of controls, and `subtle` for a column head or rail marker read on the way past — that shape hand-rolled is the most common way an app ends up spelling type classes.
- `TypographyStat`: `size?: "inherit" | "3xs" | "2xs" | "xs" | "sm" | "base" | "lg" | "xl" | "2xl" | "3xl" | "card" | "panel" | "section" | "page" | "display"`, `figures?: "tabular" | "proportional"`, `tone?: "default" | "muted"`. Two ladders in one axis: the rung names are the body ramp, for a figure beside interface copy it should step with, and the surface names ride the heading ladder, for a figure that is the headline. `tone="muted"` is the qualifier after a figure, "of 2,000" beside "1,284": still tight, no longer competing. Keep tabular where a value updates in place.
- `TypographyLink`: `href` (required), `tone?: Tone` (default `muted`), `addArrow?`, `newTab?`. The href decides internal versus external.
- `TypographyHighlight`: `tone?: HighlightTone`, one of `"primary" | "success" | "ochre" | "terracotta" | "sage" | "fig"`, plus `seed?: number`. A separate type from `Tone` on purpose: this axis is categorical (which one it is) where `Tone` is semantic (what it means), the same split theme.css draws between the earth swatches and the status tokens.
- `Card`: `href`, `title`, `description`, `icon`, `external`. An href makes the whole card a link.
- `Anchor`: `href` (required), plus `external`/`newTab`. An unstyled anchor for a
  link that is not typography and not a control — a thumbnail, a chip, a tooltip
  trigger. It exists so `target`/`rel` are never written at a call site; `external`
  is for a same-origin path that is not a route, which the router would prefetch.
- `Tone` is the one semantic colour vocabulary, shared by `Button`, `Badge`, `Callout`, `TypographyLink` and `TabsList`: `"muted" | "primary" | "secondary" | "brand" | "success" | "warn" | "destructive"`, defaulting to `muted` everywhere except a solid `Button`. Seven tones, seven tokens, one to one, which is the bar for adding one. Four names map onto others: `neutral` and `foreground` are `muted`, the word the rest of the package uses; `accent` is `--primary`'s hover tint, so a washed `primary` renders the same thing; `info` is covered by the `success | warn | destructive` triad. `brand` falls back to `--primary` in an app that defines no `--brand`.
- **Ink is handed down by whatever paints.** `toneClass(tone)` is a palette and sets no ink. A surface that fills adds `INK_ON_FILL`; a tinted one adds `INK_ON_CARD`, `INK_ON_POPOVER` or `INK_ON_SIDEBAR`, and one the package does not name spreads `inkOnSurfaceStyle(token)` into `style` rather than building a class. Both declare `--ink` and `--ink-muted`, which every type primitive reads, falling back to the page. Paint a background without them and a nested `TypographyLabel` prints `--foreground` on your fill, which measures 2.34:1 on `--primary`. On a hue fill `--ink-muted` equals `--ink`: a filled control has one ink, and wanting a second rung means wanting a tinted surface.
- `Button`: `variant?: "solid" | "soft" | "outline" | "ghost" | "link"` (default `solid`), `tone?: Tone` (defaults to `primary` on a solid button and `muted` on every other variant — filling a button in is how a page says this is the action), `size?: "xs" | "sm" | "md" | "lg" | "xl"` (default `md`), `icon?: boolean` for a square glyph box, `pill?: boolean` for full-round corners, `href` to make it a link, `render` for an element that is neither a button nor a link. Variant is how much ink it spends and tone is what the ink means, on separate axes, so a quiet delete is `variant="ghost" tone="destructive"`.
- `Badge`: `variant?: "solid" | "soft" | "outline" | "ghost"` (default `solid`), `tone?: Tone`, `size?: "xs" | "sm"` (default `sm`), `pill?: boolean`, `href` for a badge that leads somewhere. Same axes and same spellings as `Button`, minus `link`, which belongs to things you click. `warning` and `supertype` were `tone="warn"` and `tone="brand"` under invented names.
- `Callout`: `tone?: Tone`, `density?: "compact" | "editorial"`, `title`, `icon`, `action`.
- `Bulletin`: the panel shape with no copy in it — `variant?: "card" | "line"` (default `card`), `accent` (drawn along the top edge, full width, before the padding; `<Ribbon />` is the one that ships), `eyebrow`, `headline`, `lede`, `points` (a `BulletinPoint[]` of `{ title, body, mark?, ink? }`, where `mark` and `ink` are class names like `bg-fern`/`text-fern-ink`; one point takes the width, two or more go two-up from `sm`), `action` (the control on the footer rule), `footnote` (the line opposite it), `children` (under the rule) and `className`. Every slot is optional and an omitted one renders nothing, so the same component covers a credits panel and a headline with one button under it. Reach for it for an announcement, a release note, a status bulletin. `Ribbon` takes `hues?: RibbonHue[]` (default `EDITORIAL_INKS`, the eight categorical hues as data) and states no size of its own — the caller passes `h-1.5 w-full` on a panel and `h-1 min-w-20 flex-1 rounded-full` in a row.
- `Colophon`: `Bulletin` with this package's words in it, and the words are not props. `variant?: "card" | "line"` (default `card`), plus `label` (the link's words, for translation) and `children` (the consumer's own credit, rendered under the rule on `card`). A compact statement block for a page (`card`) or a footer row (`line`). There is nothing to configure, so `<Colophon />` is the whole call. Its copy is deliberately not props: this is the package's only content component — every other block is an empty container whose words come from the call site — which makes it a mark rather than a block, and a mark every site rewrites is not a mark. An app wanting to say something in its own words wants a `Card`. It draws the eight categorical hues and inks two of them, all declared in theme.css: an app that imported tokens.css alone renders a panel with an invisible ribbon and no error.
- **`Colophon` or `BuiltWithFoundations`.** A colophon is a block with something to say and needs a place to say it — a credits page, an about page, the foot of a long article. `BuiltWithFoundations` is one item in a row somebody else laid out, beside a privacy link and a copyright line, and says only where the site came from. The test is what the surface is for: somewhere with its own columns already takes the link, and somewhere the block itself is being placed takes the colophon. There is no `Colophon` variant rendering the link alone, since that would be a second name for a component that already has one. `BuiltWithFoundations` is a `Button variant="outline" size="sm"` with an `href`, so it sits on the same hairline and the same radius rung as every other control on the page. `FoundationsMark` is the eight-hue chip alone and `FOUNDATIONS_URL` the bare href.
- `DisclosureGroup`: `type?: "multiple" | "single"`, `defaultValue` (matches the title string).
- `TabsList`: `variant?: "default" | "line"`, `tone?: Tone` (default `primary`). The tone
  inks the marker alone, which on `line` means the underline and the active icon. The
  boxed track keeps its marker flat, so a tone there paints nothing.
- `TabGroup`: `tabs: readonly TabItem[]` (`{ value, label, icon?, content }`), `defaultValue?`,
  `value?`, `onValueChange?`, `variant?`, `tone?`, `iconPosition?: "inline-start" | "inline-end"`.
  `icon` is an element (`<Icons.Mic />`) that the trigger sizes and inks. Reach for it
  whenever the tabs are data, and compose `Tabs`/`TabsTrigger` by hand only for a shape
  it cannot express.

## Writing copy

Every string a reader sees goes through this: page copy, section notes, button
labels, callout titles, error messages, and the comments you leave in the file.
Write the way a person writes when they know the subject and respect the reader's
time.

- **Say the thing.** State what is true and move on. Skip the setup sentence, the
  restatement, and the closing summary of what you just said.
- **Define forwards.** "A tone sets three variables and the component paints from
  them" over "the component does not branch on the tone". Reach for a negative
  only when the absence is the point.
- **One em dash per page, at most.** Prefer a full stop, a comma, or a colon. The
  same goes for a parenthetical aside mid-sentence.
- **No arrow glyphs in prose.** Write "3.44:1 in light, 6.75:1 in dark" rather
  than joining the two with an arrow. Where a UI genuinely points at something,
  render a real icon.
- **Watch the tacked-on clause.** "…, and it is the shape to reach for", "…, and
  it exists so that…". The second half is usually the point: lead with it, or
  make it its own sentence.
- **Break the triples.** "X, Y, and Z" as a rhetorical flourish, three clauses
  joined by "and", and the "not just A, but B" shape all read as machine prose.
  Two items, or a list.
- **Cut the throat-clearing.** No "it is worth noting", "it is important to",
  "simply", "just", "of course", "in other words". No "this is not X, it is Y".
- **Skip the rationale.** A note explains what the reader needs to use the thing.
  Design history, near-misses, and the case for a decision go in the commit
  message or the docs, not in the copy.
- **Concrete over abstract.** Name the class, the prop, the file. "Five
  components take the prop" beats "several components".
- **Vary the sentences.** A paragraph of same-length clauses reads as generated
  even when every sentence is true.
- **Headings are labels.** "How it stays current", not "Why it cannot go stale".

Code samples and commit messages stay conventional. This is about prose.

## Common mistakes

- `<p className="text-sm text-muted-foreground">`: use `<TypographyMuted>`.
- `<h2 className="text-lg font-semibold">`: use `<TypographyH2>`, or
  `headingClass()` to pass classes into someone else's component.
- Importing `Card`, `Callout` or `Button` from `@supertype.ai/foundations`. They
  live in `@supertype.ai/foundations/blocks`.
- `<Button variant="destructive">` or `variant="secondary"`: those are tones.
  Write `variant="soft" tone="destructive"`, `variant="solid" tone="secondary"`.
- `<Button size="icon-sm">`: one size ladder. Write `size="sm" icon`.
- `<Badge variant="warning">` or `variant="supertype"`: tones, and misspelled ones.
  Write `variant="outline" tone="warn"` and `variant="solid" tone="brand"`.
- `tone="neutral"`, `tone="foreground"` or `tone="accent"`: all three went when the
  tone lists merged. Write `tone="muted"` for the first two, `tone="primary"` for
  the third.
- An `<a>` or `<Link>` styled by hand to look like a button, and equally
  `render={<a href="/x" />}` or `render={<Link href="/x" />}` on anything that
  takes an `href`. Write `<Button href="/x">`. `Button`, `Badge`, `Card` and
  `TypographyLink` all take `href` and route it through one rule
  (`resolveLink`): a scheme leaves the app and gets `target`/`rel`, a `#hash`
  stays a plain anchor, everything else goes through the router's `Link` and
  keeps the view transition. A hand-passed anchor skips all of that. `render` is
  for an element that is genuinely not a link — and for `RailLink`, which takes a
  router `Link` that way on purpose, since `essay/rail.tsx` has to import without
  Next.
- Exporting a constant from a `"use client"` module and importing it into a server
  component. It crosses the boundary as a stub, not a string, so an `href` built that
  way arrives as a function. Keep shared constants in a plain module.
- `<TypographyMuted tone="default">`: a type error. Use `TypographyP`.
- Binding fonts with `font.className` instead of `font.variable`. The className
  form sets `font-family` on the element and leaves the roles unresolved.
- Omitting `@source '../node_modules/@supertype.ai/foundations/dist/**/*.js'` from
  the CSS entry. Tailwind then purges every class the package ships.
- Omitting `@import "@supertype.ai/foundations/theme.css"`. `tokens.css` names the
  colour roles but holds no values, so the whole palette resolves to nothing.
- Using `Accordion` for a static FAQ. `Disclosure` runs on the browser alone.
- Adding a second `@custom-variant dark`. `tokens.css` already binds it.

Run `npx foundations doctor` in the app to check the last four.

## Surfaces

Two classes change how everything renders, and both go on `<html>` or a subtree:

- `.dark` re-points every colour token. `tokens.css` binds it to the class, so
  the OS setting leaves it alone.
- `.editorial` gives the heading role to the serif at weight 400 and retunes the
  whole heading ladder against an 18px body, instead of the product's 13px.

## Docs

Read from `node_modules`, so these are absolute:

- README: https://github.com/supertypeai/foundations#readme
- Typography: https://github.com/supertypeai/foundations/blob/main/docs/typography.md
- Blocks and MDX: https://github.com/supertypeai/foundations/blob/main/docs/blocks.md
- The essay shell: https://github.com/supertypeai/foundations/blob/main/docs/essay.md
- Build-time tooling: https://github.com/supertypeai/foundations/blob/main/docs/tooling.md
- The CLI: https://github.com/supertypeai/foundations/blob/main/docs/cli.md
