# Step 4 — Compose: one mind, whole canvas

Build the app — all surfaces in one continuous context, the chosen package's design.md in view throughout. **Never** decompose this step into per-surface handoffs with frozen intermediate artifacts; the composing mind holds the whole canvas.

## Chrome

Per the primitives switch recorded in the project's `design.md`:

- **`nurix`** — compose chrome from `@nurix/components`. The chrome recipes (`data/chrome/` — read only the recipes for surfaces in the structure doc) carry composition, states, and when-to-use; the component dictionary (`components/`) carries props, imports, and usage across both provenances — consult the index for what exists, and open the touched category's API reference on need.
- **`native`** — the recipes are *specifications*: anatomy, the eight states, keyboard behavior, token reach — implement them with the incumbent stack's own components. The index's shadcn/fallback columns carry the composition; category API references never load.

Recipes, generated files, and library compositions are starting points, not jails: restyle or extend them in place when the package demands it.

## Composition rules

- **Every surface starts from a chosen layout blueprint** (`data/blueprints/<archetype>.md`) — pick the shape reading the brief, record it in one line. Structural variety, not visual variety, is what reads as designed.
- **Values come from the token artifact** — components consume the emitted custom properties and never invent raw values ([emission.md](emission.md) has the pipe rules). Switching packages must re-skin the whole app.
- **Positive obligations apply** — load the surface's archetype checklist (`data/checklists/<archetype>.md`); the checklists are ceilings: a sidebar ships with brand block, icons, and user context; a dashboard tile ships with number + comparison + trend + state + door; every surface ships its state matrix.
- **The register scope holds**: the display face never touches cells, buttons, labels, or form chrome; data numerals run the mono face with `tabular-nums`.
- **Motion follows the package's §7** and the frequency law: actions performed 100+ times a day, and any keyboard-initiated action, get no animation, ever; panel/drawer motion is interruptible; entrances never `ease-in`.
- **A finishing pass with authority over everything is part of this step** — walk the whole canvas once at the end: rhythm, alignment, density, identity-consistency across surfaces, the one-accent discipline.

## Library reach — under `primitives: nurix`

The library carries behavior stock shadcn does not, and each item below is reachable only when the composition routes to it; a hand-rolled equivalent is a defect, not a style choice.

- **Density is the trait.** Control height and padding come from `personality.density` through `--density-control-*`; container padding and gaps on scanned surfaces (cards in a grid, list rows, panel bodies) read `--density-pad-xs|sm|md|lg` and `--density-gap-sm|md|lg` — `p-(--density-pad-md)`, `gap-(--density-gap-md)` — so one trait re-spaces the product. A `p-3` on a data surface is the per-surface mood foundations forbid; structural spacing (a divider, an icon's optical nudge) stays literal.
- **Motion rides the seam.** A `transition-*` utility with no `duration-*` class inherits `--motion-duration-fast`, which `pace`, the reduced-motion block, and the inline clamp all reach; a `duration-200` opts that element out of all three. App-owned animation reads `var(--motion-duration-base)` and `var(--motion-ease-standard)`, never a literal; the library's `effect/*` primitives already read them, so an identity moment composes from those before a bespoke variant.
- **Collections are inline.** Any container of repeated rows the operator scans — a table body, a list, a feed, a board column — carries `data-mode="inline"`: every DOM-descendant transition collapses to instant (portaled tooltips escape) while loaders keep breathing on the ambient duration. `<Cell mode="inline">` stamps it on the cell and `block/list` on each of its rows; `block/table` stamps nothing on its container, so the app stamps it on the collection — a table, and every collection it builds itself.
- **Every time on screen is `<Timestamp>`** (`element/timestamp`): it reads relative inside `relativeThreshold` and absolute beyond it (`format` takes the absolute label's Intl options), the full absolute stamp sits on the native `title` by default, and `isLive` (on by default) ticks on one shared interval. A custom live label subscribes through `useNow` (`lib/time`) — one ticker per interval, never a timer per row.
- **Typing is not saving.** Input (Enter, blur), Textarea (blur, Cmd/Ctrl+Enter), NumberInput (blur, Enter, the steppers), Slider (release), Combobox (the pick; close for a multi-select), `DatePicker` and `DatePicker.Range` (the pick) carry `onValueChange(value, details)` for the draft and `onValueCommitted` for the commit, once per settled change; `InlineEdit` carries `onSave` alone. Mutations wire to `onValueCommitted`; `onValueChange` never reaches the server. A form with a Save button is the one exception — a transaction the button commits whole.
- **Remote and long option lists are the Combobox's** (`compound/combobox`): hold the query with `searchValue` / `onSearchValueChange` (holding it turns the built-in filter off), pass the fetched rows as `options` with `loading`, page with `hasMore`, `loadingMore` and `onLoadMore()`, and cap a `multiple` select with `max`. Fetching and debouncing are the app's — `useSearchSource` (`hooks/use-search-source`) drops stale responses and debounces. A fetch-in-effect feeding a filtered `<Select>` is a defect.
- **Time-range filters are `DatePicker.Range` with `presets`** (its preset column shows by default, `DatePicker`'s needs `hasPresets`), `weekStartsOn` from the locale decision, `min` / `max` where the domain bounds them; a single date in a table cell is a `DatePicker` with `variant="bare"` (a host such as `Cell` passes it). A non-required picker carries its own Clear; `required` removes it. A bespoke window picker is a defect.
- **Uploads reject before they upload.** `FilePicker` (`compound/file-picker`) takes `accept`, `maxSize`, `maxFiles`, and `validate` (return the rejection message, or nothing to accept); a rejected file never enters the list and `onFileReject(file, message)` receives it. The dropzone only reads invalid for the dwell hold — the composition surfaces the message (a field error or a toast from `compound/toast`).
- **Shell affordances are library compositions**: "as of" is `useFreshness({ fetchedAt, onRefresh, staleAfterMs })` (`hooks/use-freshness`) on the app's own `Button` beside a `Timestamp`; an action a gate blocks is an `Empty` (`compound/empty-state`) carrying the icon, the reason and the way out; a system-wide notice is `SystemBanner` (`element/system-banner`). A bespoke freshness timer or banner is a defect.
- **Status colour is the role.** The package's `success` / `warning` / `info` reach the library as `--intent-success|warning|info`, and `destructive` as `--intent-danger` (library fallbacks when undeclared): status meaning is the `intent` prop (`neutral`, `info`, `success`, `warning`, `danger` — a status dot is `<Chip prefix="dot" intent="success">`, a destructive button `<Button intent="danger">`), never a raw hue and never `color`, which carries a stored data colour, and app utilities map onto the same roles (`--color-success: var(--success)` in the app's `@theme inline`), so a chart legend and a chip dot never disagree.
- **Library controls carry the 24px pointer floor built in; an app-drawn target under 24px grows through `hit-area-*`** (`hit-area-2` adds 0.5rem of pointer box on every side; `hit-area-x-*` / `-y-*` / `-t-*` / `-b-*` / `-l-*` / `-r-*` where a neighbour would overlap) — the pointer box grows, the paint box does not. `hit-area-debug` shows the box while composing.
- **Clipping is `clip`.** `clip` for one line, `clip-<n>` for n lines, `clip-fade` where prose is expected to overflow — `clip` and `clip-<n>` carry `min-width: 0`, and `clip-<n>` `overflow-wrap: anywhere`, which Tailwind's `truncate` and `line-clamp-*` lack (the flex-child overflow bug those leave open).
- **Type roles are utilities** — `text-display`, `text-title`, `text-h1`, `text-body`, `text-dense`, `text-meta`, … from the library's `@theme` — and `cn()` (the `cn` package, on tailwind-merge's tables) classifies `text-<role>` as a colour: a role and a colour passed through `cn`, together or apart, keep only the later one. Keep both in a `className` string that never goes through `cn`, or put the colour on the parent.
