# conventions

> Part of the documentation that comes with Beamline's complete system (docs/CONVENTIONS.md in the kit). Your coding agent can do all of this for you through Beamline's MCP server: [connect it](https://beamline.io/connect).

One API shape for every component, so an agent (or a person) that has used one can use any other.

## Files and imports

```
ui/components/<id>/
  <id>.tsx          component(s): named exports, plus a default export of the main one
  <id>.module.css   optional; styles read tokens only
  demo.tsx          default export Demo[]: every variant and state, live
  meta.json         catalog entry + spec (see below)
ui/blocks/<id>/     same shape, for composed screens
```

- **One import path per component:** `import { Button } from "@/components/button/button"`. Nothing re-exports
  another component's parts. Internal helpers live in folders starting with `_` and are never imported by products.
- Ids are kebab-case nouns (`order-book`, `date-range-picker`). Exports are PascalCase (`OrderBook`). Parts are
  prefixed with the component (`DialogContent`, `SidebarMenuItem`).
- `@/lib/utils` → `cn()`; `@/hooks/use-motion-tokens` → host-local durations, eases, springs; `@/hooks/*` for shared hooks.

## Props — one vocabulary

| concern | name | values |
|---|---|---|
| visual weight | `variant` | `primary` · `secondary` · `ghost` · `outline` · `danger` (use the subset that makes sense) |
| semantic colour | `tone` | `neutral` · `accent` · `success` · `warning` · `danger` · `info` |
| size | `size` | `sm` · `md` · `lg` (default `md`); icon-only buttons need `aria-label` |
| row density | `density` | `compact` · `default` · `comfortable` |
| value | `value` / `defaultValue` / `onValueChange(value)` | controlled + uncontrolled, always both |
| open state | `open` / `defaultOpen` / `onOpenChange(open)` | same |
| selection | `selected` / `defaultSelected` / `onSelectedChange` | same |
| busy | `loading` | keeps size and focus, sets `aria-busy` |
| disabled | `disabled` | native semantics where possible |
| empty data | `emptyLabel` | text shown instead of an empty frame |
| formatting | `format(value) => string` | numbers are passed raw, formatting is a function |
| missing data | `null` | a `null` value renders as unmeasured (hatched), never 0 |

Callbacks are `on<Thing>Change` for state and `on<Verb>` for events (`onSubmit`, `onRowClick`). Booleans are
positive (`interactive`, not `disableInteraction`).

## Rendering contract

- **One root, nothing outside it.** Every token and base style exists only inside a `.pui` root (`<html class="pui">`
  or `UIRoot`), and every name the system defines starts with `pui` (`--pui-surface`, `@keyframes pui-shimmer`). A
  component never writes to `:root`, `html`, `body` or a global class, and never adds a keyframe or custom property
  without the prefix. The package build confines every remaining global rule to roots (`scripts/lib/scope-css.mjs`).
- **Overlays open in the root's layer.** Anything portalled (dialog, sheet, popover, menu, select, tooltip, hover card,
  toast, a floating toolbar) reads `usePortalContainer()` from `@/runtime/appearance`. `undefined` means no provider;
  `null` means a managed scope whose host is pending. Keep hooks and triggers mounted, but render no portal content
  while the value is `null`. Only `undefined` may fall back to `document.body`. An initially open overlay must mount
  directly in its already-styled scope host; appearance updates reuse that destination. The old UIRoot component
  path re-exports this same runtime, never another context.

- **`className`** and `style` address the documented visual root; `classNames` addresses documented composite parts.
  Component CSS modules sit in `@layer components`, below Tailwind utilities and normal unlayered customer rules.
  Overrides follow the real cascade: layers, importance and specificity matter; placing a class last is not a
  universal precedence rule. Read public override inputs through fallbacks instead of assigning them per instance.
- **`data-slot="<part>"`** on every meaningful part (`data-slot="order-book-row"`) so products can target parts
  without classes. State goes in data attributes: `data-state="open"`, `data-side="bid"`, `data-trend="up"`.
- **Refs:** React 19 `ref` as a prop on the documented native target. Button/Table share their visual root with it;
  Input's ref/native attributes/value target its input and Select's ref targets its trigger. Their visual root is the
  field wrapper. In 0.2 Input `style` moves to that wrapper; use `classNames.input` for native input styling. Older
  components that use `forwardRef` stay valid. Slot styling never replaces measurement refs, handlers or ARIA links.
- **Polymorphism:** triggers and link-able parts accept `asChild` (Radix `Slot`). Nothing else is polymorphic;
  text components that need it take `as`.
- **Primitives:** overlays, menus, and form controls are built on Radix. Do not add another headless library.
- **Icons:** lucide-react only, 16px in controls (`size-4`), 14px in dense rows. Pass icons as elements
  (`icon={<Plus />}`), not component types.
- **Styling:** tokens only (`var(--pui-surface)`, `bg-surface`, `text-ink-2`). A hex, rgb or ms literal in a
  component is a bug, except inside `tokens.css`, `looks.css` and `motion-tokens.ts`.
- **Paint comes from a look's role, never from the element.** A painted part names what it is with one role
  class (`m-control`, `m-solid`, `m-field`, `m-inset`, `m-toggle`, `m-knob`, `m-tag`, `m-surface`, `m-overlay`;
  DESIGN-LANGUAGE.md "Looks") and keeps only layout and its own marks. `bg-surface shadow-card` or `bg-surface-2 shadow-btn` in a
  className is the old way and a bug now. In a module: `.track { composes: m-toggle from global; }`. A mark on top of
  a role composes the role's own value: `shadow-[var(--m-surface-edge),inset_3px_0_0_var(--pui-red)]`. An element
  with a role lists `var(--m-transition)` in its transition. A part that follows a chosen item (a thumb, a ring, an
  underline) uses `useIndicator` and `glideProps` from `@/hooks/use-indicator` and wears the `glide` class (`glide-2d`
  on both axes), which places it and stretches it toward each new choice; its module adds only paint and shape.
  Type, radii and icon stroke come from tokens too (`font-title`, `rounded-control`, the heading rule in base.css), so
  a look restyles them; a component never names a font family or tests which look is on.
- **Motion:** import from `motion/react` and call `useMotionTokens(hostRef)` at the actual control. Its `reduced` flag follows managed/static policy and OS preference; before `ready` it exposes the pure foundation snapshot. CSS
  transitions use `--pui-motion-in` / `--pui-motion-out`.
- **Client-only APIs** (ResizeObserver, canvas, WebGL, audio) are touched in effects, so components render on the
  server without crashing.
- **Numbers:** tabular figures; prices keep their precision (`0.00482`), never rounded by a component.
- **Accessibility:** keyboard reachable, visible focus, roles from Radix or native elements, `aria-live="polite"`
  for values that change because of a user action (a total after a filter), never for high-frequency feeds (a
  price that ticks twice a second is readable on demand, not announced); labels on every icon-only control.

## Managed appearance runtime

`@/runtime/appearance` is the single client context entry. `@/runtime/preset` contains pure data/types only;
`PresetRef = { id, visualContract: 1 }` loads no React, CSS, fonts or effects. Import the selected preset CSS
explicitly. `UIRoot preset` wins over the deprecated built-in `look` shorthand. Outermost defaults are declared by
the runtime; nested roots inherit omitted inputs. Sparse tokens merge by key and design defaults by component/key.
`inherit={false}` starts from library defaults. A local accent replaces an inherited explicit brand; local brand wins
within the same scope. Use the dedicated brand prop, never `tokens["--pui-brand"]`.

`appearanceClassName` is mirrored on the root and its independent portal host; selectors must work on each boundary.
Omitting it inherits, a supplied value replaces, and `""` clears. Ordinary `className`/`style` remain local. A static
`.pui`/`data-pui-preset` boundary supplies DOM styling only; arbitrary descendant CSS writes neither mirror to portals
nor publish renderer invalidations.

Scoped defaults use lowercase catalog IDs, for example `defaults={{ button: { size: "sm" }, table: { density:
"compact" } }}`. Each supporting client component reads only its allowed design keys through
`useComponentDefaults<ButtonDefaults>("button", ["size", "variant"])`; explicit instance props win. Export lightweight
`defaults.ts` types without importing component implementations. Never put handlers, domain data, value/open/loading,
selection or adapters into defaults. Pure/static entries can keep explicit props and inherited CSS without acquiring
a client context dependency.

Canvas/renderer code uses `useResolvedAppearance(actualHostRef, { colors, metrics, numbers })`. The server and first
client render are unresolved; apply values only when `ready`. Colors are normalized to rgba, metrics to px, and
unitless inputs are returned separately. Typography includes the host, body and numeric font families. Full CSS
variable names are accepted for private resolved geometry. Subscribe through the nearest descriptor and font
readiness, and call `refresh()` for explicit external stylesheet changes. Never resolve from the first document root
or observe all document style mutations. Apply engine options in place; appearance must not reset feed markers,
editor history, resource controllers, selection or an open overlay's lifetime.

Selected look SVG resources and optional edge-light are explicit application setup. The runtime does not import all
look initializers. Setup returns a cleanup function and shares document resources by reference count. Semantic motion
CSS and JavaScript derive from `ui/lib/motion-source.ts`; timing values remain governed by the visual contract.

## Code

- **Formatted and linted, always.** `pnpm format` (Prettier, 120 columns, Tailwind classes in the official order) and
  `pnpm lint` (hook rules and TypeScript's recommended checks) run clean on `ui/` and `gallery/`; `pnpm format:check`
  and `pnpm lint` are part of every change. A lint suppression names its rule and says why after `--`.
- **One statement per line**, and a component reads top to bottom: props and their defaults, state, derived values,
  effects, handlers, then the markup.
- **State lives in the stylesheet.** Utilities are for layout and spacing at the call site. A part whose className
  carries four or more state variants (`hover:`, `focus-visible:`, `data-[…]:`, `aria-…:`, `group-…:`, `after:`)
  moves to the component's `.module.css`, where states are selectors and every value is a token; so does any part
  that only exists to be styled (a resize grip, a prose body). `className` passed to another system component (a
  Button) stays utilities, since that is the documented way to override it.
- **No inline stylesheets.** Never `<style>{CSS}</style>` or a CSS string in a component; keyframes and rules live
  in the module. Inline `style` is only for values computed at run time, passed as custom properties
  (`style={{ "--pui-indicator-x": `${x}px` }}`) that the module reads.
- **Keyframes belong to the module that plays them.** CSS modules rename every animation name, so a module that
  names a global `@keyframes` (`pui-…` in `ui/styles`) never plays; `node scripts/layer-modules.mjs` refuses it.
- **Variant tables are typed records** (`Record<ButtonVariant, string>`), never chains of ternaries in a className.

## meta.json — catalog entry and spec

```jsonc
{
  "name": "order-book", "title": "Order book", "category": "trading", "kind": "component",
  "description": "One sentence: what it is for.",
  "source": { "from": "Beamline", "license": "MIT", "mode": "original" },   // original | wrapper | vendored
  "dependencies": ["motion"], "tags": ["depth", "bids", "asks"],
  "whenToUse": ["…"], "notFor": ["… → use X"],
  "spec": {
    "anatomy": ["root", "header", "row (price, size, total, depth bar)", "spread"],
    "variants": { "layout": ["stacked", "side-by-side"] },
    "states": ["loading", "empty", "live", "stale"],
    "keyboard": ["↑/↓ move row focus", "Enter sends price to onPriceSelect"],
    "motion": "Changed sizes flash 90ms in / 600ms out; rows never slide.",
    "props": [{ "name": "bids", "type": "Level[]", "required": true, "description": "…" }]
  }
}
```

`spec.variants` takes either shape: the object above (`{ name: [values] }`) or a list of descriptive lines when a
variant needs words, as the charts use (`["stack: none · stacked · expanded", "brush"]`). Tools print both.

`scripts/gen-catalog.mjs` turns every meta.json into `catalog.json` and `CATALOG.md`. Vendored and wrapped code keeps
`source.url` and `license`; third-party code inside an original element is listed in `source.credits` (name, licence,
url); both end up in `NOTICE`.

meta.json ships to customers, so `source` says only what a customer may read: Beamline, or the third-party package and
its licence. Where an element came from — references observed, clean-room recreations of paid kits, merged sources,
lineage notes — goes in `research/provenance.json` (internal, never shipped; `scripts/acceptance.mjs` reads it).

## Categories

actions · forms · overlays · navigation · layout · feedback · display · data · charts · trading · dashboard · ai ·
voice · effects · blocks.

Rendered motion consumers resolve each CSS duration/easing/spring/stagger separately with `useMotionTokens(hostRef)`. The optional no-ref form resolves the nearest managed root; static scopes require a real host ref. `motionTokens` is the pure, server-safe foundation table for non-DOM use. `useResolvedAppearance` accepts `times` (seconds) and `easings` (computed CSS timing functions), resolves conditional hosts when attached, and clears readiness after removal. No module observes the first page root. Reduced timing declarations take precedence over inline timing overrides. Reading intervals, deadlines and data pacing remain caller behavior.
