# Strata generative UI — how Elle builds with Strata

> Elle composes runtime UI ("generated sheets") from a **closed,
> Zod-validated catalog** that maps 1:1 onto real Strata components. The
> catalog is the single source of truth on both sides of the wire; there is
> no second list to drift. Full interaction doctrine:
> `apps/web-v2/docs/AGENT_APP_UI_CONVENTIONS.md` § "Generated sheets compose
> from the closed catalog".

## The pipeline

```
packages/ui-schema (@layers/ui-schema)          ← the vocabulary
  src/catalog.ts    ELLE_UI_CATALOG — per-component Zod prop schemas,
                    whenToUse guidance, uiComponent binding, acceptsChildren
  src/tree.ts       ElleUITreeSchema + normalize/validate + node/depth caps
  src/action.ts     ElleUIActionSchema (navigate | message Elle)
  src/prompt.ts     renderCatalogIndex() — the prompt-embedded index

apps/elle                                        ← the producer
  src/tools/render-sheet.ts   the renderSheet tool; inputSchema IS
                              ElleUITreeSchema (validation at the boundary)
  src/prompts/system-prompt.ts embeds renderCatalogIndex() — prompt and
                              schema cannot drift

apps/web-v2                                      ← the renderer
  client/components/elle-ui/registry.tsx  catalog name → @layers/ui adapter
  client/components/elle-ui/renderer.tsx  re-validates EVERY node, fails
                                          closed per node
  client/components/elle-ui/sheet-host.tsx mounts on the canvas behind a
                                          view-link
```

## Doctrine

- **One vocabulary.** Every catalog entry maps onto a real Strata component.
  Never an Elle-only component; never bespoke generated DOM.
- **Fail closed per node.** Unknown component or invalid props renders an
  inline placeholder in that slot; one bad node never takes down the sheet.
- **`Grid` is the only container** (side-by-side intent). The shell owns
  vertical rhythm — trees never author it.
- **Data binds by `dataPath` JSON Pointers** (`/toolResults/<toolName>/…`)
  into the turn's tool results; prefer pointers over inlining large arrays.
  An unresolvable pointer renders an honest "data unavailable" placeholder.
- **Actions are the only commitment.** A component may carry an
  `ElleUIAction` (navigate — validated against the route catalog — or
  message Elle). Everything else is display.
- Generated views render on the canvas behind a view-link, **never inline in
  the chat stream**.

## Adding a component to Elle's vocabulary

1. The component exists in `packages/ui` (or add it there first — see
   `components.md`). It must be composable from JSON-serializable props;
   function/ReactNode props get a data projection (icon names, declarative
   columns).
2. Add the entry to `ELLE_UI_CATALOG` in `packages/ui-schema/src/catalog.ts`:
   `uiComponent`, a sharp `whenToUse` (this is prompt text — it steers Elle),
   the Zod `props` projection, `acceptsChildren`.
3. **Rebuild the package** (`pnpm build` in `packages/ui-schema` — it ships
   `dist/`). Elle's system prompt picks the entry up automatically via
   `renderCatalogIndex()`.
4. Add the adapter in `apps/web-v2/client/components/elle-ui/registry.tsx`
   mapping the catalog props onto the real component (resolve `dataPath`,
   map icon names, wire actions).
5. Verify in the workbench's catalog browser, then live: Elle runs locally
   via `apps/elle` (`pnpm dev`, Studio at :4111) — a local restart picks up
   prompt/catalog changes.

Current binding exception (to fix, not to copy): `MediaCard`/`MediaGrid`
bind to web-v2-local `live-post-card` — new entries must bind to
`@layers/ui` components so the catalog stays app-agnostic.