---
name: formic-design-system
description: Use the Formic AI Design System (tokens + React components) for ALL UI work in a project that contains src/formic (or any folder holding Formic's tokens.css and components). Trigger on "use Formic", "the design system", or any React/Tailwind UI request — pages, dashboards, chat surfaces, forms, components — in such a project.
---

# Formic AI Design System

A token-driven React + Tailwind v4 design system for AI product interfaces, by Eyosiyas Ketema.

**Source of truth:** https://github.com/eyosiyasketema1/formic-design-system
**Live gallery:** https://formicai.dev/preview.html

## Two modes: decide first

**A. Consuming Formic in an app** (most common): the system is vendored at `src/formic/` (`styles/` + `components/`). Import its components and compose. Never fork component internals.

**B. Extending Formic itself** (the repo has `scripts/qa_check.py` at its root): read and obey `CLAUDE.md`. It carries the non-negotiable design rules, the extraction rule, and a mandatory QA workflow (`python3 scripts/qa_check.py` must pass, mirror every change into `preview.html`, branch + PR per unit of work).

## Procedure (consuming) — follow in order, every time

Generic Tailwind is the failure mode: it happens when the agent invents styles instead of importing the system, or hand-rolls a piece because no component fit. Both are caught by `formic_check.py`; neither is acceptable.

1. **Find Formic.** `ls src/formic/styles/tokens.css src/formic/components/primitives.tsx`. If missing, install before writing any UI. In an existing project (from its root):
   ```bash
   npx formicai init
   ```
   Run in an empty folder and the same command scaffolds a Vite + React + Tailwind v4 app (a welcome page with three test prompts, dependencies installed, nothing to edit); `--new my-app` creates the folder for you. In that app `src/main.tsx` and `src/CustomizeNudge.tsx` stay as they are (the nudge shows the customizer on every page you build); `src/App.tsx` and `src/pages/Welcome.tsx` are yours. Both put the base into `src/formic/` (`styles/`, the shared modules in `components/`, the scripts) and write `AGENTS.md`, a Cursor rule, Copilot instructions, the MCP server entry and this skill into the project. Components are not all there on day one: `init` installs the base and you add each component as you need it (next section); `--all` installs every one.
2. **Wire the CSS once** (Tailwind v4 global stylesheet, in this order; fix the relative path to `src/formic`):
   ```css
   @import "./formic/styles/fonts.css";    /* first: the Urbanist font */
   @import "tailwindcss";
   @import "./formic/styles/formic.css";   /* tokens, palettes, Tailwind bridge, component sheets */
   ```
   Peer deps: `react >=18`, `react-dom >=18`, `@phosphor-icons/react >=2.1`; `@dicebear/core` + `@dicebear/notionists` ^9 only if doodle avatars are used (loaded on demand). `formic.css` already includes the `sidebar.css` / `records.css` component sheets. Wrap the app in `<ToastProvider>` if toasts are used.
3. **Read before writing.** Open `src/formic/styles/tokens.css` and list `src/formic/components/`. Never write a component that already exists. Then write the screen's brief (next section) before any markup.
4. **Map the screen to the inventory.** Name the Formic component for every piece the screen needs (rail, header, figures, chart, table, form, dialog) from the inventory below. A piece that resolves to "a div with classes" is where generic UI starts; a raw `<button>`, `<input>`, `<table>` or `<svg>` in a page is a defect. **When a component you need is not in `src/formic/components`, run `npx formicai add <name>` (names: `npx formicai add --list`); never write a stand-in.** `npx formicai docs <name>` prints its props and an example before you import it.
5. **When the registry has nothing that fits, build the component; never fake it inline.** Say so ("Formic has no X; building `src/formic/components/X.tsx`"), then build it the way the system builds its own: composed from `primitives.tsx` and token utilities, control metrics (24/32/36/40, `.corner-smooth`, `.optical-text`), the ramp, typed variant union, `DEFAULT_*` demo props, fluid root, ARIA, the shared focus rule, `useReducedMotion()`, tokens only so both themes come free. Take the time it needs and tell the user what you built.
6. **Readability is not optional.** Body copy `text-body text-ink`; secondary `text-caption`/`text-small` in `text-ink-2`; `text-ink-3` never under 12px; no `opacity-*` on text; `text-canvas` (never `text-white`) on fills; numbers `tabular-nums`, right-aligned. Hard to read on the screenshot means defective.
7. **Run both gates, fix every line, report.** `python3 src/formic/scripts/formic_check.py src` (how it was built: tokens, ramp, radii, no raw elements, no second kit, `.page-content` under a rail) and `python3 src/formic/scripts/compose_check.py src` (what is on it). `npm run formic` runs both. Both print clean before the work is done; a true exception carries a `formic-ok` comment with its reason. End with a short report: components used, components built, the gates' output.

**Existing app: migrate the whole thing.** A page on Formic beside a page on the old UI is worse than either. `formicai init` marked the existing source folder `legacy` in `package.json`, so the gates and the hook leave it alone until a folder is in scope. Start with `npx formicai inventory` (every UI file, worst first, legacy ones marked), show the plan and get a yes; put the app in one `AppShell` and delete the old layout first; then folder by folder: `npx formicai scope add <folder>`, `npx formicai migrate <file> --write` on each file (the codemods do the mechanical part and leave `formic-todo` comments), finish each file by hand (routes, state and data calls unchanged; no Formic table next to an old `<button>`) until `formic_check` prints clean; `npx formicai doctor` names the old kit still installed, delete it and its CSS, icons and chart library once nothing imports them; finish when `npx formicai gates` prints clean with nothing legacy, not for the files you touched. Asked for one page in an unmigrated app: build it on Formic, then say how many files the inventory still lists and offer to do them; never build the new page on the old kit to match.

**Prompt length.** The user's request can be one line; this skill is the specification. Do not ask about spacing, radius, weights, colours, rail or chart type, the system decides those. Ask only about the brief: who reads it, what question it answers, what they do next.

## The command line

`npx formicai` is how Formic gets into a project and how it grows; run it from the project root. `init` installs the base only, so the components a screen needs are added as the screen is built.

| Command | When you run it |
| --- | --- |
| `npx formicai init [--new <dir>] [--all] [--preset <code>]` | Once, when `src/formic` is missing: adds Formic to the project you are in (or scaffolds a new Vite app with `--new`). Base only by default; `--all` installs every component; `--preset <code>` applies a design preset (the code under the copy block at formicai.dev/customize, or from `formicai preset` in another project). It also writes the MCP server entry (`--no-mcp` skips it). |
| `npx formicai add <name…>` | Every time a component you need is not in `src/formic/components`: it writes the component and what it imports, installs its packages, records it in `registry.lock`. `--list` prints every name; `--dry-run` shows the files first. |
| `npx formicai docs [<name>]` | Before importing a component you have not used: its props with types and defaults, its dependencies, an example. Without a name, the whole list with one line each. |
| `npx formicai update` | When the registry has a newer release than `src/formic/VERSION`: refreshes the installed files, shows a diff per file, keeps your edits unless you say otherwise. |
| `npx formicai doctor` | When something does not build or look right: every check (Node, Tailwind v4, the CSS import order, the alias, peer packages, a second kit, the config, the hook) with its fix. |
| `npx formicai gates` | At the end of every piece of UI work: both gates on the source folder; both must print clean. |
| `npx formicai inventory` | In an existing app, before migrating: every UI file still off Formic, worst first. |
| `npx formicai scope add <folder>` | When a folder of an existing app is ready to be held to the gates (drops it from `legacy`). |
| `npx formicai migrate <file…> [--write]` | On each legacy page: the codemods do the mechanical part and leave `formic-todo` comments for the rest. |
| `npx formicai preset` | To hand this project's design choices to another project as one code. |
| `npx formicai key [<key>]` | Once, when the user has a Formic Pro key: activates it and writes it to `.env.local` (git-ignored); without a key, the status; `--remove` deletes it. `init --key <key>` does the same after an install. |
| `npx formicai mcp install` | If `.mcp.json` / `.cursor/mcp.json` lack the Formic server: registers it. The server offers `list_components`, `component_docs`, `add_component`, `inventory`, `doctor`, `gates`, the same commands as tools. |

**The rule: when a component you need is not in `src/formic/components`, run `npx formicai add <name>` (names: `npx formicai add --list`); never write a stand-in.** A page that hand-rolls a table because `DataTable` was not installed yet is the defect this section exists to prevent; adding it takes one command.

**Formic Pro.** `npx formicai add --list` shows the Pro components after the free ones under a "Formic Pro" heading; they install with the same `add` and live in `src/formic/pro/`, but they need a key. If `add` answers that the component is in Formic Pro and no key is set, do not copy, rebuild or fake it: tell the user the exact command, `npx formicai key <key>` (keys come from https://formicai.dev/pro), and build the rest of the screen on the free components until they have one.

## Composition intelligence (before any markup)

Formic's value is in what it leaves out. Nothing goes on a screen by habit: no row of KPI cards because dashboards have them, no chart for a single number, no icon per card, no invented deltas. Do this, in order, for every screen:

1. **Write the brief** as a comment at the top of the file and keep it: `Reader` (who, when, on what), `Question` (the one thing the screen answers), `Action` (what they do next), `Register` (Text | Balanced | Analytical | Visual). No honest brief, no screen.
2. **Stay in the register.** Text (settings, documents, answers): prose, fields, steps; no charts or figures. Balanced (one thing's page): header with status and the one action, facts as `MetricRow`s, at most one chart, a `DataTable` for its children. Analytical (overviews, reports, finance): 3 to 5 figures that answer the question, the one chart that carries the trend, then the table to act on; a second chart only for a different question. Visual (browse and pick): `CardGroup`s, no figures. Mixed pages mix by section, each section declaring its register.
3. **Admit each element** only if it answers the question or enables the action, the data has the shape it needs (a figure has a period; a sparkline has 6+ points of one measure; a line has 5+ points; a donut has 3 to 6 parts; a gauge is a rate toward a target; a table has 4+ rows), it is not already said elsewhere, and removing it would cost the reader something. "It would look empty" is never a reason; a smaller page is.
4. **Budgets:** figures 0 / 1 / 3–5 / 0 by register; charts 0 / 1 / 1–3 / 0; one accent button and at most one accent tile; icons only where they say what the label says; real empty and loading states (`EmptyState` with the right `kind` and one action), never fake numbers; labels in the reader's words, captions with the period, chart titles that are the question they answer.
5. **Lint, then review against the brief:** `python3 src/formic/scripts/compose_check.py src/` flags the mechanical tells (no brief, register broken, too many figures, deltas without a period, short sparklines, two-part donuts, several accent tiles, placeholder copy). Then read the brief and the screen together and remove whatever does not serve it.

## Non-negotiable conventions

- **Tokens only.** Never hardcode colors, font sizes, radii, shadows, or easings. Utilities come from the bridge: `text-ink`, `text-ink-2/3`, `bg-canvas/surface/field/inset/hover/hover-2/sidebar`, `border-line/line-strong`, `text-accent/green/red/orange`, tints `bg-accent-tint/green-tint/red-tint/orange-tint`.
- **Type scale** (14px base, integer ramp): `text-nano` 8 · `micro` 10 · `tiny` 11 · `small` 12 · `caption` 13 · `body` 14 · `lead` 16 · `title` 18 · `heading` 20 · `display` 24 · `display-lg` 32 · `display-xl` 48. Never `text-[Npx]`.
- **Weights:** `font-medium` is the working default, `font-semibold` the maximum, `font-bold` never. **Tracking:** only `tracking-wide` / `tracking-tight`, never an arbitrary value.
- **Radii:** `rounded-sm` 6 · `rounded-chip` 7 · `rounded-control` 8 · `rounded-md` 10 · `rounded-card` 14 · `rounded-capsule` 22.
- **Motion:** one easing, `var(--ease-out-quint)`. Keyword easings only for simple fades and spins. Hover color transitions are `duration-150`. Never write a `cubic-bezier()` literal.
- **No drop shadows.** Elevation is hairline borders; the `--shadow-*` tokens are 1px rings or `none`.
- **Control metrics:** heights 24/32/36/40 (xs/sm/md/lg) shared by buttons and fields; padding is about height/3; labels use `leading-none` plus `.optical-text`; corners use `.corner-smooth`. Flat fills, no inner sheens.
- **Accent carries the primary action.** One accent CTA per view, one destructive action per view.
- **Global scales:** `data-radius` (sharp / rounded / full, or `custom` from a pixel `radius` for controls and an optional `cardRadius` for cards in the config; avatars follow `--radius-avatar`), `data-corners` (round instead of the default squircle), `data-controls` (pill: capsule controls, cards on the scale) and `data-size` (comfortable / spacious) are token overrides like palettes. Components never read them.
- **Light by default.** Dark is opt in via `<html data-theme="dark">`; there is no OS auto-detect. Palettes are `data-palette` overrides. Components never branch on theme, they only read tokens.
- **Contrast:** WCAG AA everywhere (text >= 4.5:1, non-text >= 3:1) across both modes and all 10 palettes (plus a custom one derived from a colour).
- **Icons:** the label is the verb (`Export`, `Add`); the icon goes in the `icon` prop, as a name or an element: `<Button icon="plus">New report</Button>` or `icon={<Icon name="plus" />}`. Never put the icon's name in the label (`Plus New report`, `Download Export` are defects); `iconFor(label)` picks the name from the verb. Phosphor only, through the shared `Icon` wrapper: `<Icon name="check" size={14} strokeWidth={2} />` (`strokeWidth` 2 and up is Phosphor's bold weight, under 2 regular; `weight="fill"` for a solid glyph). To add an icon, map a Phosphor component into `ICONS` in `primitives.tsx`. Never inline SVG icon paths, never add a second icon package.
- **Charts** use the categorical ramp `--chart-1..5` (chart-1 is the accent, so the primary series follows the brand). Never colour a series with `--green`/`--red`: those mean success and danger. No chart library — bars are HTML, lines are SVG with `vector-effect="non-scaling-stroke"`. Multi-series charts always ship a legend, because hue alone fails for colour-blind readers.
- **App rails** sit on `--sidebar` with a `border-line` edge. Nav active state is a flat `bg-hover-2` fill with ink copy and ink icon: no border, no accent recolor.
- **Responsive:** component roots are fluid (`w-full` plus a `max-w-*` cap); wide content scrolls in its own `overflow-x-auto`; text truncates with `min-w-0 truncate`; touch targets >= 24px.

## Layout and composition (what agents get wrong)

- **Checkbox vs Switch.** `Switch` only for a setting that applies the moment it flips. Completion, selection, "done today", "include row" = `Checkbox`. Never switches down a list.
- **Icons are furniture.** `StatCard` icons are ink on inset by default; at most one tile per view is `iconTone="accent"`. A different colour per card is the machine-made tell. Colour lives in three places only: accent (one primary action, lead series), green/red (success/danger), chart ramp (series).
- **Every titled section is a `Panel`** (`title`, `caption`, `actions`, body). Never hand-build a card header. `Card` is for untitled things only.
- **Panels in a row are equal height and their bodies fill:** grid stretches (never `items-start`), charts and lists take `fill` (`<LineChart fill />`, `<BarChart fill />`, `<BarList fill />`), tables are `w-full`. Charts have no width cap. An empty lower half, or a chart stopping at 60% of the panel's width, is a bug.
- **Buttons are one line.** Icon and label on one row, via `icon=` or as a child; labels never wrap. Icon above label = broken.
- **Tables fill their container** unless the user asks for a width; wide ones scroll in their own `overflow-x-auto`.
- **Rails:** `AppShell` always; the page's kind picks the rail. A page inside the app takes the rail from `formic.config.json` (`AppSidebar` full, `ProjectSidebar` inset / edge, or full plus a `TopBar`); a chat or agent page (anything with a `PromptBar`) takes `rail="chat"` (`SidebarNav`: new chat, recents, search, with the conversation's title strip; `chat={{ recents, onNewChat, onPick }}`); a page that stands alone takes `rail="none"`. The dashboard rail on a chat page is a defect whatever the config says. Never build a sidebar from divs, never mount a rail outside `AppShell`.
- **Nothing moves on its own.** A component shows the state its props give it and changes when the data or the person changes it. `PromptBar`, `ThinkingState`, `TaskRows` and `ToolChips` have a `demo` prop for the gallery's self-running walkthroughs; never pass it in an app, and never write a timer that opens a menu, flips a status or types into a field to make a page look alive. `ThinkingState` takes `working` while the agent runs and shows a finished, collapsed trace otherwise.
- **An agent screen is a conversation, not a pile.** The thread is the spine: `ThinkingState`, `TaskRows` or `ToolChips`, `ApprovalCard` or `AskUserQuestions`, then the answer (`Markdown` / `StreamingText`) are assistant turns in one `max-w-2xl` column, `gap-3` apart, in the order they happened, never in a side column of their own. The `PromptBar` is pinned under the thread at the same width. A second pane (`SplitPane`) exists only for a result to keep in view (the file being edited, the page built) and is one titled thing (`Panel`, `FileTree` + `CodeBlock`). `ChatApp` is the reference shape.
- **Proximity and alignment.** Filters in one row directly above the list, sized to content, search at the right end; comparison pickers together with "vs" in the panel's `actions`. One grid: `gap-3.5` in sections, `gap-6` between; numbers right with `tabular-nums`; nothing centred unless alone.
- **Brand colour:** when the user gives a hex, run `python3 src/formic/scripts/set_accent.py "#hex"` (or `setAccent(hex)` from `theme.ts` at runtime). It derives a light-mode and a dark-mode variant that both pass AA and rewrites both `--accent` tokens. Never write one hex into `tokens.css` by hand.
- **App configuration:** `src/formic/formic.config.json` (accent, palette from ten built-ins or custom + paletteColor, radius as a preset or a pixel number 0–32, corners smooth/round, controls scale/pill (capsule buttons and inputs, cards unchanged), size, starting theme, avatar kind initials/doodle/photo, sidebar full/inset/edge/topbar (the rails on the Sidebar page; topbar = full rail + TopBar) + sidebarState expanded/rail, font from the approved Google list, type base/lg/xl, layout full (the default) / medium / compact applied through `.page-content`, motion) is the source of truth. When the user pastes a block from formicai.dev/customize, save the JSON there verbatim and run `python3 src/formic/scripts/apply_config.py`; it fits the accent, swaps the font, sets the `<html data-*>` attributes and writes `components/config.ts` (the prop defaults). Build on the rail the config names; wrap page content in `.page-content`. "Rounder corners", "start dark", "use doodle avatars" are config edits, not inline props. Never override a config value inline.
- **Filters are a `FilterBar`** (`leading` label, children = the selects, `trailing` for a compare toggle, `search` pinned right, `active` + `onClear`; one wrapping row, never clipped): one row, wrap only on overflow, each control sized to content. Never one filter per row.
- **Every chart animates in by default** (bars grow, lines reveal, donuts sweep, sparklines draw); reduced motion is honoured automatically. `animate={false}` only for live-updating charts. No custom keyframes, no motion libraries.
- **Icons match labels:** `iconFor(label)` from `primitives.tsx` (`"Refresh"` → `retry`, `"Export CSV"` → `download`, `"Add user"` → `user-add`); `undefined` means leave the icon off.
- **Brand marks:** `<BrandIcon name="github" />` from `brand.tsx` (80+ companies and social networks), monochrome by default; `color="brand"` for a one-colour mark. `<BrandLogo name="google" />` from `brand-logos.tsx` is the real full-colour logo (101 official marks, dark variants automatic) for sign-in buttons and integration directories. Never recolour or stretch a logo. No logo images, no second icon package.
- **Page density.** `p-6 sm:p-8`, sections `gap-6`.

## Component inventory

**Primitives** (`primitives.tsx`): `Icon`, `Spinner`, `ShimmerLabel`, `StreamText`, `StreamCaret`, `Skeleton`, `Avatar` (`src` photo | `doodle` face from the name | initials), `AvatarGroup` (`people`, `max`, `ring`), `Tooltip`, `Progress`, `Separator`, `Chip`, `DiffStat`, `IconButton`, `SendButton`, `Switch`, `Checkbox`, `Disclosure`, `GlideMenu`, `Card`, `Badge`, `RadioCheck`, `AvatarStack`, `Popover`.
**Brand** (`brand.tsx`, `brand-logos.tsx`): `FormicMark`, `BrandLogo` (real logos, `BRAND_LOGO_NAMES`, `brandTitle`), `BrandIcon` (`name`: github, google, apple, slack, notion, figma, linkedin, x, instagram, youtube, stripe, openai, …). **Helper:** `iconFor(label)`.
**Hooks** (`hooks.ts`): `useSequence`, `useElapsed`, `useStream`, `useAnchoredLayer`, `useModalLayer`, `useReducedMotion` (the one way to read `prefers-reduced-motion`; never call `matchMedia` in a component).

**Controls:** `Button` (10 variants x 4 sizes x square/pill).
**Forms:** `Field`, `Input`, `Textarea`, `Select` (`options` [{value, label, swatch?}], a colour dot before a label), `Combobox` (`options` [{value, label, hint?, swatch?, icon?, disabled?}], `value`/`onChange(value, option)`, `allowCustom`, `loading` + `onQueryChange`, `filter`), `TagInput` (`value`/`onChange(tags)`, `suggestions`, `allowCustom`, `max`, `validate`), `Switch`, `Checkbox`, `FilterBar`, `Slider` (`steps` + `showSteps`, `formatValue`, `editable` readout, `valuePosition="thumb"`, `thickness` thin | default | thick | px), `RangeSlider` (two thumbs, `value: [lo, hi]`), `OTPInput`, `FileDropzone`, `DatePicker`, `DateRangePicker`, `Calendar`, `ColorPicker`, `InputCopy` (read-only value + copy, `variant` icon | button), `InputGroup` + `InputField` (several fields as one block, label inside the row, `icon`, `error`, `trailing`).
**Overlays:** `Modal`, `Drawer`, `Toast`, `DropdownMenu`, `Popover`, `Tooltip`, `NotificationList` (`notifications` [{id, title, body?, time, section?, read?, kind?, actor?, actions?}] + `onRead`/`onDismiss`, or `defaultNotifications`; `onOpen`, `frame` panel | plain).
**Feedback:** `EmptyState` (`kind` first | search | filter | error, `query`, `action`, `secondary`, `size` sm | md; DataTable's default empty body), `Alert`, `Progress`, `Skeleton`, `LoadingState`, `ThinkingIndicator` (one line, `words`, `glyph`), `ThinkingState`, `TaskRows`, `ToolChips` (a run's summary), `ToolCall` + `ToolCalls` (one call in full: `call` {name, label, state pending | running | done | error, input?, output?, error?, duration?, icon?}, `defaultOpen`, `onRetry`; errors open by default).
**Conversation:** `ImageResult` (`src`, `prompt`, `state` generating | done | error, `progress`, `aspect`, `meta`, `onDownload`, `onRegenerate`, `onOpen`, `onUse`, `onRetry`) + `ImageResults` (`images`, `onPick`, `onUse`), `Terminal` (`lines` [{text, kind cmd | out | err | info}], `title`, `status`, `exitCode`, `duration`, `stream`, `onStop`), `ContextMeter` (`used`, `max`, `parts` [{label, tokens}], `cost`, `model`, `variant` ring | bar), `ModelSelector` (`models` {key, name, provider, brand?, description?, context?, price?, tags?, disabled?}, `value` / `defaultValue` / `onChange`, `size` sm | md, `align`), `Sources` + `InlineCitation` (`Source` {id, title, url, domain?, favicon?, brand?, quote?, meta?}; the citation is a superscript-height number opening a card, Sources one row under the reply opening into rows), `ChatThread`, `StreamingText`, `Markdown`, `CodeBlock`, `SelectionActions`, `PromptBar`, `ChatComposer`, `ApprovalCard`, `ApprovalFlow`, `InsightCards` (`pages[]` {key, prose, Card, prompt}, `onPrompt`; cards: `CompareCard`, `AnomalyCard`, `AllocationCard`; helpers `Entity`, `Delta`), `AskUserQuestions` (`questions[]` with `options`, `multiSelect`, `allowOther`, `freeText`, `skippable`; `onComplete(answers)`), `RecommendationCard`, `ContextCards`.
**Dashboard:** `Panel` (titled card: `title`, `caption`, `actions`, `padding`), `StatCard` (`iconTone` "neutral" | "accent"; `layout` label-first | value-first | chart-middle; `trendKind` line | bars | stacked with `trendSplit`; `ring` {value, max, label} for a progress ring beside the copy; `align="center"` for a report panel's hero halves; `chart` slots any small chart; `layout="inline"` puts the icon and number on one row with the delta as text; `layout="tile"` puts the icon tile and delta on the top row with a `period` chip at the foot; `layout="key"` is a cardless figure with a coloured rule (`keyTone`) that doubles as a chart legend; `size="sm"` is a compact tile on inset for a grid inside a panel), `MetricRow` (`leading` tile or a small `DonutChart`, `detail`, `progress` 0..1 bar beside the value or `progressLayout="under"` for a full-width bar beneath, `meta` muted share after the value, `big` label-over-display-number, `trend` bar sparkline or `trendKind="line"`), `StatStrip` (three or four headline figures on one card with icon tiles and hairline dividers; `tone` up | down colours a result that is itself a gain or loss), `Progress` (`segments` for a pill-segment plan, `vertical` for a ladder lit from the bottom, `tone` accent | green | ink), `Rating` (stars out of `max`, half stars), `Shortcut` (`keys` array as keycaps, `quiet` for hover-reveal), `ShareBar` (one bar split into parts, ticks above with name and share, tooltip per part), `Delta`, `BarChart` (`highlight`, `showValues`, `valuePosition` top | bar, `axis` + `format` for a value axis with dashed gridlines at round values, `thin` bars, negatives hang below the baseline, `horizontal` rows) / `ScatterChart` (`points` [{name, x, y, size}], `xLabel`, `yLabel`, `sizeLabel`, `formatX`, `formatY`) / `ActivityCalendar` (`data` [{date, value}], `weeks`, `end`, `label`, `format`, `weekStart` 0 | 1; contributions-style year of squares) / `LineChart` (`fill` to take the panel's height, no width cap, `animate` default true, `curve` linear | step | smooth, a series with `style: "dashed"` for a comparison line, `endMarker`, `axis`, `floor={false}` to start at the lowest value, `points` true | "hover", `tooltip` point | shared, `backdrop` columns behind the line, `guides`, `legend`, negative values allowed; labels thin out to what fits) / `RadarChart` (`axes`, `series`, `max`, `format`, `detail` for a second tooltip line), `MiniBars` (capsule columns, `track`, `split`, `names`, `legend`), `DonutChart` (`value`/`max` ring, or `segments` [{name, value, detail}] with a legend that brings each part to the centre, `legend="list"` for rows beside the ring, `center` for a total), `Sparkline` (`smooth`, `animate`), `ChartLegend`, `CountUp`, `Gauge`, `BarList` (`rank` numbers, `axis` percent ticks; always one hue) (`charts.tsx`); `PrivacyScope`, `PrivacyToggle`, `Masked` (`Privacy.tsx`) mask figures until the eye is opened. `StatCard`'s `display` and `MetricRow`'s `value` accept a node, so `<CountUp>` and `<Masked>` slot straight in.
**Data:** `SplitPane` (`children` [first, second], `direction`, `defaultSize`, `size`/`onResize`, `min`, `max`, `minSecond`, `collapsible`, `storageKey`, `stackBelow`), `FileTree` (`nodes` [{name, children?, status?, meta?, open?}], `selected` / `defaultSelected` / `onSelect`), `DataTable` (`columns` [{key, header, render, align, width, muted}], `rows` with `id`, `selectable` + `selected`/`onSelectedChange`, `toolbar`, `actions` (row) => `<RowActions onView onDelete onMore />`, `pageSize` + `total` + `page`/`onPageChange` for the footer, `empty`; cells `PersonCell`, `IconCell` (icon or a `tile` node), `ProgressCell`, `CountCell`, `StatusCell` tone green | red | orange | neutral), `RecordsTable` (AI spreadsheet), `FilterTable`, `DiffTable`.
**Structure:** `Accordion`, `Steps`, `Timeline`, card family (`cards.tsx`): `CardGroup` (`columns` 1..4 | `orientation="inline"`), `Card` + `CardHeader`, `CardMedia` (`icon` + `tone` neutral | accent, or `src` with `aspect` video | banner), `CardTitle`, `CardDescription`, `CardFooter`, `CardButton` (ghost in a row, secondary on a surface, `variant="accent"` for the one lead). Same markup in a grid and in an inline list.
**Navigation:** `CommandPalette` (`open`, `onClose`, `groups` of {key, label, hint?, icon?, keys?, data?}, `onSelect(item, group)`, `search=` for a custom index; `useCommandPalette()` wires ⌘K), `AppShell` (`rail` defaults to the config, `"chat"` and `"none"` are the page's choice; `chat` {activeTitle, recents, navItems, onNewChat, onPick}, `sections`, `active`, `onSelect`, `title`, `caption`, `actions`, `user`, `theme`/`onTheme`, `notifications`, `onSearch`, `padding`, `fill`), `TopBar` (`title`, `leading`, `search`/`onSearch`, `theme`/`onTheme`, `notifications`, `user`/`userMenu`/`onUserAction`, `actions`; use with `<AppSidebar user={null} />`), `Tabs` (`variant` underline | segmented | subtle), `Pagination`, `Breadcrumbs`, `Menubar`, `AppSidebar` (dashboard rail: `expanded` | `rail`, `sections` with optional `children` sub-menus or `submenus="none"` for flat, `active`, `onSelect`, `user`, `workspace.logo` defaults to the Formic mark in accent), `ProjectSidebar` (workspace rail: `workspaces`, `groups` with `items[].status` unread | live | error, `items[].children` for a second level, and `onAdd`, `itemActions` + `onItemAction`, `user` + `userMenu`, `onSettings`, `onTheme`, `layout`, `open` from `useProjectSidebar()`, `ProjectSidebarTrigger`, `ProjectInset`), `SidebarNav` (chat rail, mounted by `AppShell rail="chat"`; `theme`/`onTheme` above the account row), `SearchList`.

All components ship demo content as prop defaults (`DEFAULT_*`); always pass real data via props in apps. Variant props are typed unions.

## Composition guidance

Human-in-the-loop: `ApprovalCard` for a short, directly-controlled approval; `ApprovalFlow` when the run needs several questions in sequence (sliding stack, rolling counter, auto-advance on single choice); `AskUserQuestions` when the agent must ask before it can continue (numbered options with 1 to 9 shortcuts, Other row, free text, Skip, answers returned in `onComplete`).

Every page is an `AppShell` (`sections`, `active`, `onSelect`, `title`, `caption`, `actions`, children): it mounts the rail (full / inset / edge / topbar from the config, `rail="chat"` for a chat or agent page, `rail="none"` for one that stands alone), renders the page header where it belongs and wraps the content in `.page-content`; the page writes no `<h1>` and no rail. The theme switch lives in the shell on every page (beside the profile, in the TopBar, above the account row in the chat rail, or at the right of the strip); never add one of your own. Rails fill their parent, never give them a fixed height.

A chat app is `AppShell rail="chat"` + `ChatThread` for the conversation (embed `Markdown`, `CodeBlock`, `ToolChips`, `ApprovalCard` as assistant message children via `MessageBubble`) + `PromptBar` pinned at the bottom + `ToastProvider` at the root. Streaming replies use the `StreamText` primitive or `StreamingText`; before one starts, `ThinkingIndicator`. Agent progress uses `ThinkingState` or `TaskRows`. `PromptBar` takes `status="streaming"` + `onStop` (Stop button, queued drafts via `queue`/`onQueueChange`), `history`, `suggestion` (Tab fills), `suggestions`. `Accordion` has `variant="grouped"` (rows on the page, `highlight` item | trigger). File input uses `FileDropzone` or PromptBar attachments.

## Live gallery

`preview.html` opens standalone in any browser (CDN React and Tailwind, no build) with the full component gallery and theme, palette, and accent switching. Use it to show options before writing code.
