# @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 instead of
   overriding 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 that gets a background
   needs `INK_ON_FILL` (a tone fill) or `style={inkOnSurfaceStyle("--card-foreground")}`
   (any other surface: `--card`, `--popover`, `--sidebar` or your own), or the text
   inside it keeps the page's ink and fails contrast silently. `Card` and
   `Bulletin` already do it for the card.
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`, `variant="prose"` beside reading copy | root |
| a timestamp, count or value | `TypographyCaption` | root |
| an icon, checkbox or dot beside words | the words' `mark` or `markEnd`, `Marked` for lines under them, `Mark` inside a sentence | root |
| a form label or column header | `TypographyLabel` | root |
| small print | `TypographyCaption as="small"`, with `block` on its own line | 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, accordion or collapsible | `Disclosure`, `DisclosureGroup` | `/blocks` |
| tabbed content | `TabGroup` (client) | `/blocks` |
| a segmented picker driving state | `TabGroup` with no `content`, `size="sm"` in a toolbar | `/blocks` |
| a long-form article page | `EssayHeader` + `EssayLayout` + `EssaySection` | `/essay` |
| an article whose body is prose or MDX | `EssayHeader` + `ReadingLayout`, the body in `ProseFlow` | `/essay`, root |
| 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`, `TypographyCaption`, `TypographyLabel`, `TypographyStat`, `TypographyInlineCode`, `TypographyLink`, `TypographyHighlight`, `ProseFlow`, `headingClass`, `headingFace`, `eyebrowClass`, `toneClass`, `impliedTone`, `resolveLink`, `isExternalHref`, `CAP_TRIM`, `Mark`, `Marked`. 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`, `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, to spread into a `no-restricted-syntax` entry. The set is assembled inside, so it cannot drift. Types: `DesignRuleOptions`, `RestrictedSyntax` |
| `@supertype.ai/foundations/rehype` | `rehypeProseCode`, `PROSE_THEMES`. Build-time only, must not resolve React |
| `@supertype.ai/foundations/contrast` | `checkLegibility` (inks at 4.5:1), `checkSignals` (fills at 3:1, tinted inks at 4.5:1, `--subtle-foreground` at the 3:1 it is documented for, labels against their own fill), `checkHairlines` (`--border` and `--input` at 1.4:1 on `--background` and `--card`, `--sidebar-border` on `--sidebar`), `resolveTokens`, `formatFailures`, `specificity`, `parseColor`, `luminance`, `contrast` (WCAG ratio), `lc` (APCA lightness contrast), `tokenCuts`. 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` and `TypographyProse` are presets of `TypographyP`. The prop
  each one pins is dropped from its type, so passing it fails to compile.
- `TypographyList`: `variant?: "ui" | "prose"` (default `ui`), `ordered?`.
- `TypographyCaption`, `TypographyLabel`: `size?: "sm" | "xs" | "2xs" | "inherit"`, `as?`. The component is the look and `as` the element: `span` beside a subject, `p` for a note on its own line, `small` for small print.
- **Caption or muted copy.** `TypographyMuted` is the content said quietly (a sentence explaining a setting). `TypographyCaption` is words about something else (a timestamp, count, byline, the value beside a key, small print). Same rung and ink at the caption's default size; pick by role.
- `TypographyEyebrow`: `tone?: "heading" | "brand" | "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. Use `brand` for a kicker in the identity hue, `muted` for the uppercase micro-label over a group of controls, and `subtle` for a column head or rail marker read on the way past.
- `TypographyStat`: `size?: "inherit" | "3xs" | "2xs" | "xs" | "sm" | "base" | "lg" | "xl" | "2xl" | "3xl" | "card" | "panel" | "section" | "page" | "display"`, `figures?: "tabular" | "proportional"`, `tone?: "default" | "muted"`.
- `TypographyLink`: `href` (required), `tone?: Tone` (none takes the ink around it), `addArrow?`, `newTab?`. The `href` decides whether the link is internal or external.
- `TypographyHighlight`: `tone?: HighlightTone`, one of `"primary" | "success" | "ochre" | "terracotta" | "sage" | "fig"`, plus `seed?: number`.
- `Card`: `href`, `title`, `description`, `icon`, `external`. An `href` makes the whole card a link.
- `Anchor`: `href` (required), plus `external`/`newTab`.
- `Tone` is the semantic colour vocabulary shared by `Button`, `Badge`, `Callout`, `TypographyLink`, `TabGroup` and `DisclosureGroup`: `"muted" | "primary" | "secondary" | "brand" | "success" | "warn" | "destructive"`. Unstated, a solid `Button` or `Badge` is `primary` and a quieter one `muted`, and a `TypographyLink` takes the ink around it.
- **A mark goes in the words' `mark` prop.** An icon, checkbox, dot or tick beside words: `<TypographyLabel mark={<Icons.Clock />}>…</TypographyLabel>`, or `markEnd` after them. Every text role takes both, headings included. A bare glyph, a direct child of the slot, is sized to one em of the words, and every mark is seated on the middle of the first line's capitals, at any rung, leading or zoom; a checkbox or dot keeps its own size. Give the glyph no size class and the row no alignment class. A mark is no taller than the line: an avatar or a progress ring is an object, and the words centre on it in an `items-center` row with `CAP_TRIM` on them. A leading mark starts the line and the words fill the rest; an end mark follows the words, and `justify-between` sends it to the edge. The row is a grid unless you name another display; `gap-x-` changes the space; the `truncate` prop cuts the words, never the mark.
- **`Marked` puts the lines under a title in its words' column.** `<Marked mark={<Icon />} title={<TypographyLabel>Title</TypographyLabel>}><TypographyCaption>Body</TypographyCaption></Marked>`. The title takes the mark; without one, `Marked` is a plain stack.
- **`<Mark>` is the slot on its own**, for a glyph inside a sentence or in words no role renders (a code line, a menu item): `Open the <Mark><Icons.Settings /></Mark> menu`. It reads the size of the words around it.
- **Controls own their glyphs.** `Button`, `Badge` and a `TabGroup` tab size an icon to one em of their label and centre it on the letters. Pass it as a direct child, with no class.
- **`CAP_TRIM` centres a line of text on something**: in a box of its own, such as an avatar's initials or a date in a cell, or beside an object taller than the line, such as an avatar. It is not for glyphs.
- **Ink is handed down by whatever paints.** `toneClass(tone)` is a palette and sets no ink. A filled surface adds `INK_ON_FILL`; any other spreads `inkOnSurfaceStyle(token)` into `style`, the one way for every surface.
- `Button`: `variant?: "solid" | "soft" | "outline" | "ghost" | "link"` (default `solid`), `tone?: Tone`, `size?: "xs" | "sm" | "md" | "lg" | "xl"` (default `md`), `icon?: boolean`, `pill?: boolean`, `href` to make it a link, `render` for an element that is neither a button nor a link.
- `Badge`: `variant?: "solid" | "soft" | "outline" | "ghost"` (default `solid`), `tone?: Tone`, `size?: "xs" | "sm"` (default `sm`), `pill?: boolean`, `href` for a badge that leads somewhere.
- `Callout`: `tone?: Tone`, `density?: "compact" | "editorial"`, `title`, `icon`, `action`.
- `Bulletin`: `variant?: "card" | "line"` (default `card`), `accent`, `eyebrow`, `headline`, `lede`, `points`, `action`, `footnote`, `children`, `className`.
- `DisclosureGroup`: `type?: "multiple" | "single"`, `defaultValue` (matches the title string).
- `TabGroup<V extends string>`: `tabs: readonly TabItem<V>[]` (`{ value, label, icon?, content? }`), `defaultValue?: V`, `value?: V`, `onValueChange?: (value: V) => void` (a union of values flows through, so `useState<Range>` needs no cast), `variant?: "default" | "line"`, `size?: "md" | "sm"` (default `md`), `tone?: Tone` (default `primary`), `iconPosition?: "inline-start" | "inline-end"`. With no `content` on any tab it is a picker: the strip alone. Build `tabs` with `.map` when they are data; put a frame around the whole `TabGroup`.
- `Disclosure`: `title`, plus every `<details>` prop. A row the app keeps open or closed takes `open` and `onToggle`, natively.

## Writing copy

Every string a reader sees goes through this: page copy, section notes, button
labels, callout titles, error messages, and comments in the code. Write as a
person would: direct, clear, and respectful of the reader's time.

- **Say the thing.** State what is true and move on. Skip the setup sentence and
  the closing summary of what you just said.
- **Define forwards.** Prefer a direct description over a negative form unless the
  absence is the point.
- **One em dash per page, at most.** Prefer a full stop, a comma, or a colon.
- **No arrow glyphs in prose.** Write “3.44:1 in light, 6.75:1 in dark” instead of
  joining the two with an arrow.
- **Lead with the point.** If a clause explains the reason, put it up front or make
  it its own sentence.
- **Cut the AI scaffolding.** Avoid “it is not…”, “there is no…”, “this is not…”,
  “what this does is…”, “the reason is…”, and “because a…” patterns when a
  direct statement does the work.
- **Write the actual rule.** Prefer “Badges are labels, not links” to “This is not
  a link, because a badge is not a link.” Prefer “One function builds the set” to
  “There is one function because the package builds the whole set in one place.”
- **Break the triples.** Long “X, Y, and Z” rhythms read like machine prose.
- **Cut the throat-clearing.** Avoid “it is worth noting”, “it is important to”,
  “simply”, “just”, “of course”, and “in other words”.
- **Skip the rationale.** Explain what the reader needs to use, not the design
  history behind it.
- **Concrete over abstract.** Name the class, the prop, or the file.
- **Vary the sentences.** A paragraph of same-length clauses reads as generated.
- **Headings are labels.** “How it stays current” is clearer than “Why it cannot go stale”.

Code samples and commit messages should 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()`.
  Lint enforces this on `<p>` and `<h1>`-`<h6>`, where the tag says the content is text.
- 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 className="bg-primary text-background">` or `<Button variant="ghost" className="text-muted-foreground">`: a fill or ink written past the tone axis, which is the one pair nothing measures. Write `variant="solid" tone="brand"`, `variant="ghost" tone="muted"`. Lint enforces this on `Button` and `Badge` once a consumer passes `designRules({ tone: true })`.
- `<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 were replaced when the tone lists merged. Write `tone="muted"` for the first two, `tone="primary"` for the third.
- Hand-styling an `<a>` or `<Link>` to look like a button. Write `<Button href="/x">` instead.
- Exporting a constant from a `"use client"` module and importing it into a server component.
- `<TypographyMuted tone="default">`: a type error. Use `TypographyP`.
- Binding fonts with `font.className` instead of `font.variable`.
- Writing the style layer out by hand. `@import "@supertype.ai/foundations";` after `@import "tailwindcss";` is the whole of it.
- Adding a second `@custom-variant dark`. `tokens.css` already binds it.
- `<TypographyCaption className="flex items-center gap-1"><Icons.Clock className="size-3" /> 44d ago</TypographyCaption>`, or an icon in a wrapper beside the words: a glyph centred on a line box, or pushed down a pixel, is right at one rung and wrong at the rest. Write `<TypographyCaption mark={<Icons.Clock />}>44d ago</TypographyCaption>`.
- `<Icons.Check className="inline h-4 w-4 align-middle" />` or `-mt-0.5` on an inline icon. Pass it as the words' `mark`, or wrap it in `<Mark>` inside a sentence.
- `<Badge><Icons.Mail className="size-3" />CC</Badge>`: the badge sizes its glyph. Drop the class.
- `<div className="flex items-center gap-2"><span>WhatsApp</span><TypographyStat size="2xs">+62…</TypographyStat></div>`: words at two sizes line up on their baseline. Write `items-baseline`.
- `text-[10rem]` for a display ornament. The ramp goes to `text-9xl`; past that, name the size in CSS so something else can share it.

Run `npx foundations doctor` in the app to check the font binding, the style layer and the dark variant.

## 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.

## Upgrading from 0.3

`ON_FIRST_LINE`, `ON_BASELINE`, `icon-inline` and the caption's sizing of a loose
`svg` are gone; a glyph beside words goes in their `mark`. Each second way to do
one thing is gone too: `TypographySmall` is `TypographyCaption` (`as="small"` for
small print, `as="p"` for a note), `TypographyProseList` is
`TypographyList variant="prose"`, `Accordion` is `Disclosure`, the `Tabs` parts
are `TabGroup`, and `INK_ON_CARD`, `INK_ON_POPOVER` and `INK_ON_SIDEBAR` are
`inkOnSurfaceStyle`. The README section "Upgrading to
0.4" maps each removal to its replacement.
`npx foundations upgrade --dry-run` lists what moves and `npx foundations upgrade`
moves it (on a clean git tree, in `.tsx` and `.jsx`); lint names the rest.

## 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
