--- url: /prefab/guide/getting-started.md description: >- Install @maxhealth.tech/prefab and build your first MCP App UI in minutes. Covers npm setup, display() helpers, and browser rendering. --- # Getting Started ## Installation ```bash npm install @maxhealth.tech/prefab # or bun add @maxhealth.tech/prefab ``` ## Base Theme CSS Prefab ships a base CSS theme (`prefab.css`) that provides design tokens and structural styles for all components. **Bundler (Vite / webpack):** ```ts import '@maxhealth.tech/prefab/prefab.css' ``` **CDN:** ```html ``` When using `toHTML()`, the base CSS is injected automatically. Pass `{ includeStyles: false }` to opt out. The layering order is: `prefab.css` (base) → `stylesheets[]` (your overrides) → `theme` (runtime CSS variables). ### Token values are host-adaptive Each token in `prefab.css` is a fallback chain rather than a literal: ```css --background: var(--color-background-primary, var(--vscode-editor-background, #ffffff)); ``` MCP Apps host variables win, then VS Code webview variables, then the static default. That is what makes a viewer inherit the surrounding editor or client theme without any configuration, so avoid replacing these with flat values unless you genuinely want to override the host. ### Driving prefab from a shared brand The token *names* are the [brandc](https://www.npmjs.com/package/brandc) contract, the same vocabulary the other Max Network kits read. If you author a brand as data, `toPrefabTheme()` emits exactly prefab's wire `theme` shape, so no adapter is needed: ```ts import { toPrefabTheme, dashboard } from 'brandc' import { display, autoTable } from '@maxhealth.tech/prefab' return display(autoTable(rows), { theme: toPrefabTheme(dashboard) }) ``` prefab takes no runtime dependency on brandc (its `dependencies` are empty by design, and the CDN renderer bundle must stay that way). brandc is a devDependency used by `test/brand-contract.test.ts`, which fails if prefab ever introduces a token outside the shared contract or if a brand stops covering a token prefab reads. ## Usage Modes prefab has four usage modes: | Mode | Where | Import | |------|-------|--------| | **Server-side** | MCP tool handlers (Python/TS) | `@maxhealth.tech/prefab` | | **Client-side** | Browser (MCP Apps iframe) | `dist/renderer.min.js` script tag | | **Hybrid** | Node/Bun backend → HTML response | `PrefabApp.toHTML()` | | **Remote** | Any MCP client (VS Code, Claude, etc.) | See [Remote section](#remote-use-the-hosted-mcp-server) | ::: tip See it in action The [interactive demo](/demo/) shows how an LLM prompt becomes a fully rendered UI — dashboards, forms, charts, and more — powered by the client-side renderer. ::: *** ## Server-Side: Build UIs in MCP Tool Handlers Build a component tree, wrap it with `display()`, and return it as an MCP tool result. ```ts import { display, Column, H1, Text, DataTable, col, Badge, autoTable, } from '@maxhealth.tech/prefab' // Simple: auto-generate a table from data async function listUsers() { const users = await db.query('SELECT * FROM users') return display(autoTable(users), { title: 'Users' }) } // Advanced: hand-craft the layout async function userDashboard() { const users = await db.query('SELECT * FROM users') return display( Column({ gap: 8, children: [ H1('User Dashboard'), Text('Manage your organization members.'), DataTable({ rows: users, columns: [ col('name', 'Name'), col('email', 'Email'), col('role', 'Role'), col('status', 'Status'), ], search: true, }), ], }), { title: 'User Dashboard' }, ) } ``` The `display()` function serializes the tree to `$prefab` wire JSON and wraps it in an MCP tool result content array. Any MCP client that understands prefab can render it. ## Client-Side: Browser ext-app Load the renderer bundle and use the `app()` factory. See the [live demo](/demo/) for a complete working example. ```html My App
``` ::: tip Use versioned CDN URLs Always pin a version (e.g. `@0.3`) in production to prevent breaking changes. ::: The `app()` factory: 1. Detects whether the page is in an iframe (bridge mode) or standalone 2. Performs the PostMessage handshake with the host (if bridged) 3. Applies the host theme 4. Returns an API object with `callTool`, `render`, `onToolInput`, etc. ## Hybrid: Self-Contained HTML Use `PrefabApp.toHTML()` to generate a complete HTML page from a server: ```ts import { PrefabApp, Column, H1, Text } from '@maxhealth.tech/prefab' const app = new PrefabApp({ title: 'My Dashboard', view: Column({ gap: 4, children: [H1('Hello'), Text('World')] }), }) const html = app.toHTML() // Returns a self-contained HTML page with embedded JSON + renderer script ``` Options: | Option | Default | Description | |--------|---------|-------------| | `cdnVersion` | Current package version | CDN version for script/CSS tags | | `pretty` | `false` | Pretty-print the embedded JSON | | `includeStyles` | `true` | Inject the `prefab.css` base theme | ## Remote: Use the Hosted MCP Server The fastest way to use prefab — no installation needed. Point any MCP client at the hosted renderer server: > **Important:** Claude Code / Claude Desktop require `/mcp` as the > first path segment. VS Code accepts any path. Use the appropriate > URL for your client. **VS Code (`settings.json` or `.vscode/mcp.json`):** ```json { "servers": { "prefab-renderer": { "type": "http", "url": "https://maxhealth.tech/prefab/mcp" } } } ``` **Claude Desktop / Claude Code (`claude_desktop_config.json`):** ```json { "mcpServers": { "prefab-renderer": { "type": "http", "url": "https://maxhealth.tech/mcp/prefab" } } } ``` Once connected, the server exposes a `render_prefab_ui` tool that accepts `$prefab` wire-format JSON and returns rendered HTML. Your LLM can call it directly to produce rich UI from structured data. ::: tip See it in action The [interactive demo](/demo/) shows exactly what the remote renderer produces — dashboards, forms, charts, and more. ::: ## Subpath Imports ```ts import { ... } from '@maxhealth.tech/prefab' // Everything import { ... } from '@maxhealth.tech/prefab/actions' // Actions only import { ... } from '@maxhealth.tech/prefab/rx' // Rx expressions import { ... } from '@maxhealth.tech/prefab/charts' // Chart components import { ... } from '@maxhealth.tech/prefab/mcp' // MCP display helpers import { ... } from '@maxhealth.tech/prefab/renderer' // Browser renderer import '@maxhealth.tech/prefab/prefab.css' // Base theme CSS ``` ## Next Steps * [Live Demo](/demo/) — see LLM prompts rendered as live UIs * [Components](./components) — the component model, with the [full catalog](/reference/components) of all 115+ components * [Actions](./actions) — client-side and MCP actions ([API](/reference/api/actions/)) * [Reactive Expressions](./rx) — dynamic values and [Signals & Collections](./rx#signals--collections) ([API](/reference/api/rx/)) * [Auto-Renderers](./auto-renderers) — generate UIs from raw data ([API](/reference/api/auto/)) * [Wire Format](/reference/wire-format) — the `$prefab` JSON spec --- --- url: /prefab/guide/components.md description: >- Guide to prefab's 115+ declarative UI components — layout, typography, forms, data tables, charts, media, and interactive elements. --- # Components A prefab UI is just data. You describe what you want as a tree of typed nodes, and the renderer turns that tree into a live interface. There is no JSX, no virtual DOM to reason about, and no framework to learn — only a vocabulary of component functions that you nest inside one another. ## The mental model Each component is a function that returns a `Component` instance. Calling it builds one node in the tree; passing other components as its children grows the tree downward. When you are ready to ship, the whole structure serializes to plain `$prefab` wire JSON, which any prefab-aware client can render. Most components share a friendly shape: an optional **props** object first, then an array of **children**. ```ts Column({ gap: 6, children: [ H1('Patients'), Text('Everyone under your care, at a glance.'), ], }) ``` Containers like `Column` and `Row` hold other nodes; leaf nodes like `Text` and `Badge` carry content. Containers take a single props object, with the child nodes under `children`. Leaf nodes take their content positionally, which is why `H1('Patients')` and `Text('…')` read the way they do. ## How components compose Composition is the whole game. You build small, meaningful pieces and slot them into larger layouts, the same way you would nest boxes inside boxes. A card lives inside a column, a table lives inside a card, a badge lives inside a table cell. Because every node is data, you can also build trees programmatically — map over rows, conditionally include a section, or factor a repeated fragment into a helper function. Props that drive behavior (an `onClick` action, a reactive `cssClass`, a `from` collection) make these static trees come alive without changing the composition model. ## The categories prefab groups its 115+ components into a handful of families. Reach for the family that matches your intent: * **Layout** — structural containers that control direction, spacing, and grids (`Column`, `Row`, `Grid`, `MasterDetail`). * **Typography** — headings, paragraphs, labels, and inline text styles. * **Card & Alert** — grouped content surfaces and status callouts. * **Forms** — inputs, selects, checkboxes, date pickers, and field wrappers. * **Data display** — tables, badges, metrics, and progress indicators. * **Charts** — bar, line, area, pie, radar, scatter, and more, rendered natively as SVG. * **Media** — images, audio, video, embeds, and file drop zones. * **Interactive** — tabs, accordions, dialogs, popovers, and carousels. * **Control flow** — `If`/`Else`, `ForEach`, and reusable `Define`/`Use` templates that shape the tree at render time. Pick a container, fill it with the leaves and nested containers you need, and let the renderer handle the rest. → See the [Components reference](/reference/components) for the full catalog: every component, its props, and examples. --- --- url: /prefab/guide/actions.md description: >- How to attach actions to components — CallTool, SetState, ShowToast, Navigate, and other serializable event handlers for MCP Apps. --- # Actions Actions are how a prefab UI does something. They are small, serializable commands you attach to a component event — `onClick`, `onChange`, `onSubmit`, or the app-level `onMount`. Because they are plain data (not closures), they travel across the wire and run wherever the renderer lives, which is what makes interactivity possible in an MCP App. ## Two execution models Every action falls into one of two camps: * **Client-side** actions resolve entirely in the renderer with no server roundtrip. Setting state, toggling a flag, showing a toast, opening a link, or picking a file all happen instantly in the browser. * **MCP transport** actions take a roundtrip through the host — calling a tool, sending a chat message, or updating shared context. These are how your UI reaches back into the server that produced it. Reaching for the right camp is usually obvious: if the work is purely visual or local, keep it client-side; if it needs the model or your backend, call a tool. ## Chaining with onSuccess and onError Most actions accept `onSuccess` and `onError` options, each taking a single action or an array of them. This lets you compose flows declaratively — save a record, then flip a flag and show a success toast, and surface an error toast if it fails — without writing any imperative glue. ```ts Button('Save & Notify', { onClick: new CallTool('save_item', { arguments: { name: STATE.name }, onSuccess: [new SetState('saved', true), new ShowToast('Saved!', { variant: 'success' })], onError: new ShowToast('Save failed', { variant: 'error' }), }), }) ``` ## Lifecycle: running on mount Actions are not limited to user gestures. Pass `onMount` to `display()` to run an action the moment the UI first renders — perfect for loading initial data or opening a real-time subscription so the view arrives already populated. ## When to use the builder sugar For the state-mutating actions (`set`, `toggle`, `append`, `pop`), prefab ships ergonomic wrappers that accept a `Signal` or `Collection` instead of a raw string key. Prefer them whenever you already model your state with signals: you keep type safety, avoid stringly-typed keys, and produce the exact same wire-format actions. Drop down to the raw action classes when you need a one-off or are working without a signal. → See the [Actions API reference](/reference/api/actions/) for every action class, its constructor options, and serialized shape. --- --- url: /prefab/guide/rx.md description: >- Reactive template expressions, pipes, signals, and collections — prefab's client-side state system with auto-updating UI bindings. --- # Reactive Expressions (`rx`) A reactive expression is just a string wrapped in `{{ }}` that the renderer keeps alive. Instead of baking a value into your UI once, you hand the renderer a tiny formula. Whenever the underlying state changes, the renderer re-runs the formula and patches only the affected parts of the screen. ``` Hello, {{ user.name }}! → Hello, Ada! ``` Change `user.name` in state and the greeting updates on its own. No re-render call, no event wiring — that is the whole idea. ## The mental model Think of four moving pieces that flow in one direction: 1. **State store** — a plain key/value bag that lives in the browser (`count`, `user`, `patients`, …). 2. **Expressions** — `{{ }}` formulas that read from that store and from loop or event scope. 3. **Pipes** — `| filters` that format a value for display (`{{ price | currency:'EUR' }}`). 4. **Auto-update** — when state changes, every expression that touched the changed key re-evaluates and the DOM follows. You rarely write the raw strings by hand. The `rx()` builder gives you a typed, chainable way to compose them, so a typo becomes a compile error instead of a blank screen: ```ts import { rx, STATE } from '@maxhealth.tech/prefab' rx('count').add(1) // → "{{ count + 1 }}" rx('name').upper() // → "{{ name | upper }}" rx('active').then('On', 'Off') ``` Expressions can also reference scope that only exists in certain places — the current `item` and `index` inside a `ForEach` loop, the `event` payload on a form, or the `result` and `error` of a tool call. Each has a matching builder (`ITEM`, `INDEX`, `EVENT`, `RESULT`, `ERROR`). ## Signals & Collections Raw state keys work, but they are untyped and easy to misspell. **Signals** and **collections** are a thin typed layer on top of the same state store, giving your reactive data a name, a shape, and a home. * A **signal** is a single reactive scalar — a string, number, boolean, or null. Think "the currently selected id" or "is the panel open". * A **collection** is a keyed array of rows, with helpers to look a row up by a signal's value. The nice part: declaring them is enough. Their initial values are auto-collected into `PrefabApp` state, so you never hand-assemble a `state: { ... }` object. ```ts import { signal, collection } from '@maxhealth.tech/prefab' const patients = collection('patients', data, { key: 'id' }) const selectedId = signal('selectedPatientId', patients.firstKey()) // patients.by(selectedId) → "{{ patients | find:'id',selectedPatientId }}" // Reads the selected row, reactively. Bind it straight into a component. ``` Because everything compiles down to `{{ }}` expressions, a signal change ripples through every binding that depends on it — exactly like a hand-written expression, just type-safe. → See the [Rx API reference](/reference/api/rx/) for the full API: `rx`, `signal`, `collection`, every scope variable, and the pipe registry. --- --- url: /prefab/guide/auto-renderers.md description: >- Auto-generate tables, charts, forms, metrics, and timelines from raw data with autoTable, autoChart, autoForm, and autoMetrics. --- # Auto-Renderers Most of the time you already have the data — a list of users from a query, a few KPIs, a row of monthly sales. What you *don't* want to do is hand-wire a table, pick column definitions, choose a chart type, and style badges every single time. Auto-renderers close that gap: hand them raw data, get a finished UI back in one call. ## The Mental Model Think of an auto-renderer as a single function with a simple shape: **data in → component tree out.** ```ts const users = await db.query('SELECT * FROM users') return display(autoTable(users), { title: 'Users' }) ``` That `autoTable(users)` call inspects your data, infers the columns from the object keys, detects which fields are statuses (and renders them as badges), and returns a normal `Component`. Because the output is just a regular component, it slots into any layout — wrap it in a `Column`, drop it next to a chart, or hand it to `display()`. There is no special runtime and no escape hatch to learn. The family follows one consistent convention: the **first argument is your data**, and an **optional second argument is an options object** (title, layout hints, behaviour flags). `autoForm` is the small exception — it takes a submit-tool name in the middle so a generated form knows which MCP tool to call. A useful way to picture the whole family is by the shape of input each one expects. Roughly: * a **single object** describes one thing → a detail view, * a **list of records** describes many of the same thing → a table, chart, or comparison, * a **sequence in time** describes a journey → a timeline or progress tracker, * a **set of fields** describes an intent to capture → a form. Once you internalise that mapping, choosing the right helper rarely needs the reference at all — you reach for the one whose input shape matches the data already in your hand. ## When to Reach for Them Auto-renderers shine when the *structure* of your UI mirrors the *structure* of your data: * An array of records → a searchable, sortable table. * A handful of numbers → a row of metric cards. * Date-stamped events → a vertical timeline. * A list of fields → a working form wired to a tool. They are perfect for MCP tool handlers, where you frequently turn an API response straight into something renderable with minimal ceremony. ## From a Schema, Not From a Field List `autoForm` takes a list of fields, and writing that list is usually restating something the server already knows. An MCP tool declares an `inputSchema`. A REST route declares a request body. Both are JSON Schema, and `fieldsFromJsonSchema()` turns one into the field list: ```ts const fields = fieldsFromJsonSchema({ type: 'object', required: ['email'], properties: { email: { type: 'string', format: 'email', title: 'Email' }, plan: { type: 'string', enum: ['pro', 'team'] }, seats: { type: 'integer', minimum: 1, maximum: 50 }, }, }) return display(autoForm(fields, 'create_account', { title: 'New account' })) ``` The form now asks for exactly what the tool accepts, because it is derived from the same declaration the tool validates against. Formats become the right control — `email`, `uri`, `date-time`, `password` — an enum becomes a Select, an array of enums becomes a multi-select, and `minimum` / `maxLength` carry over as bounds. It only emits what a flat form can honestly ask for. A nested object, an array of objects, or a property marked `readOnly` is skipped rather than rendered as a control that cannot round-trip. Pass `include` to fix the order, or `exclude` to drop keys the caller already knows. This is the inverse of `formSchema()`, which derives an elicitation schema from a field list for hosts with no UI. Together they mean one declaration serves both paths: the schema draws the form, and the form's fields describe the schema. ## When to Hand-Craft Instead Reach for the underlying components directly when you need precise control — bespoke column renderers, custom interactions, conditional layouts, or a design that doesn't map cleanly onto your raw data shape. Auto-renderers are a fast on-ramp, not a ceiling: you can start with `autoTable`, then graduate to a hand-built `DataTable` the moment you need something the auto path can't express. The two styles compose freely in the same view. → See the [Auto-Renderers API reference](/reference/api/auto/) for the full API: every helper, its options, and examples. --- --- url: /prefab/guide/mcp-display.md description: >- display(), displayForm(), rendererHtml(), and registerViewerResource() — server-side helpers to return prefab UIs from MCP tools. --- # MCP Display Helpers When an MCP tool runs, it hands back a result. The display helpers let that result be a living UI instead of a wall of text. You build a component tree on the server, hand it to a helper, and the helper packages it into the exact shape an MCP host expects to render. ## The mental model Think of every tool handler as three small steps: 1. **Build** — compose your screen from prefab components (a `Column`, a table, a form, a chart). 2. **Wrap** — pass that tree to a display helper. The helper serializes it to the `$prefab` wire format and folds it into an MCP tool-result envelope. 3. **Return** — hand the envelope straight back from your handler. Any host that speaks prefab paints it. ```ts import { display, Column, H1, autoTable } from '@maxhealth.tech/prefab' async function listPatients() { const patients = await db.query('SELECT * FROM patients') return display(Column({ children: [H1('Patients'), autoTable(patients)] }), { title: 'Patients' }) } ``` You never touch the envelope by hand. The helper knows where the JSON goes, which fields the host reads, and how to keep older and newer hosts happy. ## Full display vs. incremental update There are two ways to send something back, and picking the right one keeps your UIs snappy. Reach for a **full display** when the screen changes shape — a new list, a detail page, a form, an error or success card. The host swaps in the whole tree and renders it fresh. Reach for an **incremental update** when the screen is already on the user's screen and you only need to nudge some values inside it — a counter ticking up, a status flipping to "done", a freshly fetched total. Instead of re-sending the entire view, you send just the changed state, and the renderer merges it into the live store without rebuilding anything. That split is the whole game: send a full view when the layout changes, send a patch when only the data does. ## Chaining tools Because each handler returns a self-contained UI, tools compose naturally. A list view can carry a button whose click calls a detail tool; the detail view can carry one that opens an edit form; submitting the form calls a save tool. Each step is just another handler returning its own `display(...)`, so rich, multi-screen flows fall out of simple pieces. → See the [MCP API reference](/reference/api/mcp/) for the full API: every helper, its options, and the tool-result shape. --- --- url: /prefab/guide/input-required.md description: >- Ask the user for input under MCP 2026-07-28 — display_form with elicit, formInputRequest, and reading the answer with acceptedFormInput. --- # Asking for input Protocol revision 2026-07-28 made the MCP core stateless and removed server-initiated `elicitation/create` pushes. A handler now asks for input by **returning** an `input_required` result: the client collects the answers and retries the original call, and the handler runs again with the answers in hand. That matters to `display_form()`. A prefab form is a UI, and a UI only exists on a host that renders one. On a host with no MCP Apps surface the form was unreachable. The same `AutoFormField[]` now also derives the restricted elicitation schema, so one field list serves both paths. ## The two paths ```ts const fields = [ { name: 'email', label: 'Email', type: 'email', required: true }, { name: 'plan', label: 'Plan', options: [{ value: 'pro', label: 'Pro' }, { value: 'team' }] }, ] // A host that renders UI: a prefab form calling the `signup` tool on submit. display_form(fields, 'signup', { title: 'Create your account' }) // Any host: an input_required result the client fills in natively. display_form(fields, 'signup', { title: 'Create your account', elicit: true }) ``` Both ask for exactly the same thing, because both are derived from `fields`. ## The write-once handler Write one handler that runs on every round: read the answers first, request only what is still missing, and return the real result once everything has arrived. ```ts const FIELDS = [ { name: 'email', label: 'Email', type: 'email', required: true }, { name: 'plan', label: 'Plan', options: [{ value: 'pro' }, { value: 'team' }] }, ] async function signup(_args: unknown, ctx: { inputResponses?: McpInputResponses }) { const answers = acceptedFormInput(ctx.inputResponses, 'signup', FIELDS) if (answers == null) { return formInputRequest(FIELDS, { key: 'signup', message: 'Create your account' }) } return display(autoDetail(await createAccount(answers))) } ``` Round one finds no answers and returns the request. The client shows its form and retries. Round two finds the answers and the handler finishes. ## Reading the answer `inputResponses` comes from the client, so treat it as untrusted. `acceptedFormInput` checks it against the same field list that produced the schema: unknown keys are dropped, values of the wrong type or outside the advertised bounds are dropped, an enum value that was never offered is dropped, and a missing required field fails the whole answer. ```ts const answers = acceptedFormInput(responses, 'signup', FIELDS) ``` It returns `undefined` for a missing, declined, or cancelled answer alike. Re-requesting is the right move for all three only when the request is idempotent. When a refusal has to be told apart from a first pass, read the response directly: ```ts const view = inputResponse(responses, 'signup') if (view?.action === 'decline') { // The user said no. Asking again is not the answer. } ``` ## The schema `formSchema(fields)` produces the restricted JSON Schema on its own, which is useful when the elicitation is issued through the SDK rather than through prefab: ```ts const schema = formSchema([ { name: 'email', type: 'email', required: true }, { name: 'age', type: 'number', min: 18, max: 120 }, { name: 'tags', options: [{ value: 'a' }, { value: 'b' }], multiple: true }, ]) ``` The wire accepts a flat object of primitives and nothing else, so the mapping is deliberately narrow: | Field | Schema | |---|---| | `type: 'email' \| 'url' \| 'date' \| 'datetime'` | `string` with the matching `format` | | `type: 'number' \| 'integer' \| 'range'` | `number` / `integer`, `min`/`max` as bounds | | `type: 'checkbox' \| 'boolean'` | `boolean` | | `options` | `string` with `enum` | | `options` + `multiple` | `array` of `string` with `enum` items | | anything else | `string`, `min`/`max` as length bounds | `required: true` puts the field in the schema's `required` list. `label` becomes the `title`, `description` becomes the `description`, and `default` is carried across when its type matches. A `password` field produces a plain string. The restricted schema has no secret-input format, and claiming one would misrepresent how the client is going to render it. ## Nesting and richer shapes The restricted subset has no nested objects and no arrays beyond multi-select enums. A flow that genuinely needs a richer shape belongs on the UI path, where prefab renders the whole form and submits it through `CallTool`, or in several rounds carrying `requestState` between them: ```ts formInputRequest(FIELDS, { key: 'signup', requestState: signedToken }) ``` `requestState` round-trips through the client and comes back as attacker-controlled input. Sign it — the MCP TypeScript SDK's `createRequestStateCodec` gives you an HMAC `{ mint, verify }` pair — and mint only what earlier rounds already proved. --- --- url: /prefab/guide/a2ui.md description: >- Emit A2UI from a prefab component tree — toA2UI(), the a2ui:// resource helper, the Basic-catalog mapping table, and what degrades on the way across. --- # A2UI [A2UI](https://a2ui.org) is the declarative agent-to-UI protocol: an agent sends JSON describing a component tree plus a data model, and the renderer maps the abstract component names onto its own widgets — React, Angular, Lit, Flutter, Swift, Jetpack Compose. prefab speaks it as a second output target. The same server-side component tree that produces a `$prefab` payload also produces A2UI messages, so one authoring API reaches both: | | `$prefab` | A2UI | |---|---|---| | Rendered by | prefab's own renderer | the host's A2UI renderer | | Delivery | MCP Apps `ui://` HTML resource, or any web page | `a2ui://` resource or an embedded resource in a tool result | | Surface | sandboxed iframe | native widgets, no iframe | | Catalog | 115+ components | the 18-component Basic catalog | ## Emitting ```ts const app = new PrefabApp({ view: Column({ children: [H1('Users'), autoTable(rows)] }), }) const { messages, diagnostics } = app.toA2UI() ``` `messages` is a list of A2UI protocol messages. By default everything is inlined into a single `createSurface`, which is what a stored payload wants. Pass `{ stream: true }` to split it into `createSurface` + `updateComponents` + `updateDataModel` so a streaming transport can paint early. `diagnostics` is the part worth reading. prefab has 115+ components and the Basic catalog has 18, so some of the tree changes shape on the way across, and every change is reported: ```ts for (const d of app.toA2UI().diagnostics) { console.warn(`${d.kind}: ${d.subject} — ${d.detail}`) } ``` | Kind | Meaning | |---|---| | `degraded` | Rendered as something simpler. An `Alert` became a `Card`, a `Metric` became a `Column` of `Text`. | | `unsupported` | Dropped. Charts, diagrams and file uploads have no Basic-catalog reading. | | `expression` | A `{{ }}` template was richer than a JSON Pointer, so the binding could not be made. | | `action` | An action had no direct equivalent and was reported to the agent as an event instead. | ## Serving it over MCP Return a surface from a tool with `display_a2ui`. The payload travels as an embedded resource under the `application/a2ui+json` MIME type, which is how a host knows to route it to its A2UI renderer: ```ts server.registerTool('list-users', schema, async () => display_a2ui(autoTable(await db.users()))) ``` For a surface that does not depend on the conversation, register it as a resource instead — the host reads it once and caches it: ```ts registerA2uiResource(server, () => Column({ children: [ H1('Settings'), Input({ name: 'apiKey', label: 'API key' }), ] }), { uri: 'a2ui://myserver/settings' }) ``` The builder runs on every read, so a surface closing over live data refreshes without re-registering. Caching is off by default for that reason; pass `cache: { ttlMs, cacheScope }` when the surface really is static. Both live alongside the MCP Apps path in [MCP Apps](/reference/mcp-apps) — a server can offer `ui://` and `a2ui://` from the same tool and let the host pick. ## In the browser The emitter also ships as a standalone IIFE bundle, apart from `renderer.min.js`. Almost no page that renders `$prefab` also emits A2UI, so folding the two together would tax every consumer for a feature they do not use: ```html ``` `emit` takes parsed `$prefab` JSON rather than a component tree, so nothing in the bundle needs the authoring API. That is what keeps it around a sixth the size of the renderer. The [playground](https://maxhealth.tech/prefab/playground/) runs this bundle: switch the preview pane to **A2UI** to see any payload translated live, with the diagnostics listed underneath. ## How the tree crosses over Two structural differences drive everything: **Flat, not nested.** A2UI components live in an adjacency list. Every component carries an `id` and parents reference children by id. prefab's nested `children` are flattened, ids are allocated deterministically in traversal order, and the entry component is named `root` as the protocol requires. An `id` you set yourself is honoured. **Bound, not interpolated.** A2UI reads dynamic values through JSON Pointer bindings. `{{ user.name }}` becomes `{ "path": "/user/name" }`, and prefab's `state` becomes the surface data model. Text that mixes literals with values goes through the `formatString` catalog function, so `Score: {{ score }}` becomes `{ call: 'formatString', args: { value: 'Score: ${/score}' } }` rather than being lost. What has no equivalent is arithmetic, pipes and conditionals — `{{ count + 1 }}`, `{{ price | currency:'USD' }}` — and those raise an `expression` diagnostic. A string is interpolated only if *every* value in it binds; one unbindable expression makes the whole string unbindable, because interpolating half of it would change what the text says without saying so. ### Pipes A2UI has no expression language, but its catalog has the formatting functions that prefab's common pipes correspond to, so seven of the twenty-two map: | prefab | A2UI | |---|---| | `currency` | `formatCurrency(value, currency)` | | `number`, `round` | `formatNumber(value, decimals)` | | `date`, `time`, `datetime` | `formatDate(value, format)` | | `pluralize` | `pluralize(value, one, other)` | `{{ price | currency:'EUR' }}` becomes `{ call: 'formatCurrency', args: { value: { path: '/price' }, currency: 'EUR' } }`. The date pipes are the one inexact mapping. prefab renders through the reader's locale; A2UI's `formatDate` requires an explicit Unicode TR35 pattern, so one is chosen and a `degraded` diagnostic says which. Losing the value entirely would be the worse trade. The other fifteen — `truncate`, `join`, `selectattr`, `percent`, `compact` and friends — transform data rather than format it, and stay reported. So does a chained pipe: no single catalog function is two pipes, and translating half of one would change the value without saying so. ### Validation Every A2UI input is `Checkable`, which is the same job prefab's `required` and `inputType` do, so a form keeps its validation on the way across: ```json { "component": "TextField", "label": "Email", "checks": [ { "condition": { "call": "required", "args": { "value": { "path": "/email" } } } }, { "condition": { "call": "email", "args": { "value": { "path": "/email" } } } } ] } ``` No `message` is emitted. The rule already says which check failed, and the renderer is better placed to word and localize that than prefab is. `numeric` is deliberately not emitted for a number field. The catalog requires it to carry a `min` or a `max` — it is a range check rather than a type check — and prefab's number inputs carry no range. `variant: 'number'` on the field already says the value is numeric. ### Control flow | prefab | A2UI | |---|---| | `ForEach` | the child template — one instance per item, `$item` resolving to a path relative to the current item and `$index` to the `@index` function | | `Define` / `Use` / `Slot` | resolved at emit time by inlining the definition; a `Use`'s `overrides` are seeded into the data model and brought into scope by name | | `If` / `Elif` / `Else` / `Condition` | **no equivalent** | Conditionals are the one real capability gap between the two protocols. A2UI has no declarative `if`: the renderer draws what the adjacency list says, and the agent sends a fresh `updateComponents` when the shape should change. prefab runs a reactive client that re-shapes itself without a round trip. A one-item list template would *look* like a conditional and behave like one only by accident, so the emitter reports the loss instead of faking it. A UI leaning on `If` does not cross over intact, and no amount of emitter work changes that. ### Tables `DataTable` and `autoTable` map onto A2UI's child template rather than being flattened row by row: one `Row` template, one `Text` per column bound to the column key, and a `Column` whose `children` is `{ path, componentId }`. The renderer instantiates one copy per item in the data-model list, so the emitted surface stays as small and as reactive as the prefab original. A literal row array is seeded into the data model so the template has something to iterate; a `{{ rows }}` expression binds straight to where the rows already live. ### Mapping table | prefab | A2UI Basic | Note | |---|---|---| | `Column`, `Row` | `Column`, `Row` | `align` and `justify` carried across | | `Div`, `Container`, `Grid`, `Form`, `Page`, … | `Column` | containers with no A2UI meaning flatten | | `Card`, `CardContent` | `Card` | several children get a `Column` wrapper | | `H1`–`H6`, `Heading` | `Text` | Markdown `#` prefix | | `Text`, `P`, `Lead`, `Large`, `Markdown` | `Text` | | | `Muted`, `Small`, `Label`, `Badge` | `Text` | `variant: caption` | | `Code`, `Kbd` | `Text` | backtick-wrapped | | `BlockQuote` | `Text` | `>` prefix | | `Input`, `Textarea` | `TextField` | `inputType` picks the variant; `required` becomes a check | | `Checkbox`, `Switch` | `CheckBox` | | | `Select`, `RadioGroup`, `Combobox` | `ChoicePicker` | options read from the children | | `Slider` | `Slider` | `step` converted to division count | | `DatePicker`, `TimePicker` | `DateTimeInput` | | | `Button` | `Button` | label becomes a child `Text` | | `Link` | `Button` | borderless, running the `openUrl` function | | `Image`, `Video`, `Audio`, `Icon` | `Image`, `Video`, `AudioPlayer`, `Icon` | | | `Tabs` / `Tab` | `Tabs` | | | `Dialog` | `Modal` | | | `Separator` | `Divider` | | | `Alert` | `Card` | variant styling dropped | | `Metric` | `Column` of `Text` | trend and delta dropped | | `Table`, `DataTable` | `Column` of `Row`s | see above | | `CardTitle`, `CardDescription`, `Tooltip` | `Text` | | | `ForEach` | templated `Column` | see Control flow | | `Define`, `Use`, `Slot` | inlined | see Control flow | | `If`, `Elif`, `Else`, `Condition` | — | `unsupported` | | charts, `Mermaid`, `Svg`, `DropZone`, `Progress` | — | `unsupported` | A component the table does not name still emits: one with children flattens to a `Column`, one with text renders as `Text`, and each raises a `degraded` diagnostic. Nothing is dropped silently. ### Actions | prefab action | A2UI | |---|---| | `CallTool` (either `toolCall` or `callTool` on the wire) | `{ event: { name: tool, context: arguments } }` | | `SendMessage` | `{ event: { name: 'sendMessage', context: { message } } }` | | `OpenLink` | `{ functionCall: { call: 'openUrl', args: { url } } }` | | `SetState`, `ToggleState`, everything else | an agent event named after the action | Argument values go through the same binding conversion as component props, so `CallTool('search', { arguments: { q: '{{ query }}' } })` sends the bound value rather than the raw template. A2UI carries one action per control. Where prefab binds several, the first is used and the rest raise an `action` diagnostic. ## Conformance Emitted payloads are validated against the official A2UI v1.0 JSON Schemas, vendored under `test/fixtures/a2ui/v1_0/`, plus the two structural rules the schemas cannot express: every referenced child id must exist, and every component must be reachable from `root`. `test/a2ui.test.ts` runs one view per mapper family through that gate. Refresh the vendored schemas when A2UI publishes a revision: ```bash bun scripts/sync-a2ui-schemas.ts ``` --- --- url: /prefab/guide/renderer.md description: >- Mount $prefab wire JSON into the browser with the vanilla DOM renderer. Zero dependencies, works in any iframe or web page. --- # Browser Renderer The browser renderer is how your `$prefab` UIs come alive in a real page. It is a single, self-contained script (`dist/renderer.min.js`) with **zero framework dependencies** — no React, no Vue, no build step. Drop it in with a ` ``` **Manual mount** is for when the data arrives later — from a fetch, a tool call, or user interaction. Call `PrefabRenderer.mount(element, data, options)` yourself and you get back a **render handle** you can hold onto: feed it new data with `update()`, force a redraw with `rerender()`, reach into the reactive `store`, or tear everything down with `destroy()`. ::: tip Always ship prefab.css The renderer paints structure, but `prefab.css` carries the design tokens and base styles. Load it alongside the script or your components come out unstyled. ::: ## Going further The renderer is extensible without forking it. You can teach it new node types with `registerComponent`, add new `{{ }}` filters with `registerPipe`, and inject scoped stylesheets straight from the wire format — all covered in the reference. → See the [Renderer API reference](/reference/api/renderer/) for the full API: every method, option, and the render handle. --- --- url: /prefab/guide/bridge.md description: >- PostMessage bridge connecting prefab iframes to MCP Apps hosts (VS Code, Claude, ChatGPT) via prefab:* and ui/* JSON-RPC protocols. --- # PostMessage Bridge When a prefab app renders inside an MCP Apps host like VS Code, Claude, or ChatGPT, it lives in a sandboxed iframe. The bridge is what lets that iframe and its host hold a conversation — passing tool input in, sending tool calls and chat messages out, and reacting to theme or context changes — all over the browser's `postMessage` channel. ## Two protocols, one bridge The bridge speaks two message dialects at once, so the same app works across hosts that disagree on conventions: * **`prefab:*`** — the custom, prefab-native protocol (for example `prefab:tool-input`, `prefab:tool-call`). * **`ui/*`** — the JSON-RPC dialect from the [MCP Apps spec](https://modelcontextprotocol.io), which standards-based hosts prefer. You never choose between them. The bridge negotiates the right one during connection and translates underneath, so your code stays the same regardless of which host loaded it. ## Connection lifecycle Every session opens with a short handshake. The app announces the capabilities it supports, the host replies with its own capabilities plus the active theme, and from there messages flow both ways for the life of the iframe. Tool input arrives, your app renders, tool calls go back out, and either side can tear the connection down cleanly when it's done. The host can also push updates mid-session — a theme switch or a fresh context payload (locale, access tokens, and the like) — which your handlers receive as they happen. ## High-level vs. low-level Most apps only ever touch the high-level **`prefab.app()`** factory. It auto-detects whether you're in an iframe or running standalone, runs the handshake for you, applies the host theme, and hands back a ready-to-use `PrefabApp` object with friendly methods like `onToolInput`, `render`, and `callTool`. ```html ``` If you need finer control — a custom transport, your own handshake timing, or raw message handling — reach for the low-level **`Bridge`** class instead. It exposes the wire directly: connect, initialize, subscribe to raw message types, and disconnect yourself. The factory is built on top of it, so you lose convenience but gain control. ## A note on origin security By default the bridge accepts messages from any origin (`'*'`), which is fine for local development but unsafe in production. Always pass an explicit `hostOrigin` so the bridge only trusts messages from your real host. It validates incoming `event.origin`, correlates tool responses by id to prevent spoofing, and times out stalled tool calls — but the origin you set is the first line of defense. → See the [Renderer API reference](/reference/api/renderer/) for the full API: the `Bridge` class, the `PrefabApp` methods, and the `app()` factory. --- --- url: /prefab/reference/components.md description: >- Complete API reference for all 115+ prefab components — props, signatures, and wire format examples for layout, forms, data, media, and charts. --- # Components Reference All components are functions that return a `Component` instance. They serialize to JSON via `.toJSON()` and compose as children of container components. *** ## Layout Structural containers that control spacing, direction, and grid placement. ### `Column(props?)` Vertical flex container. The most common layout primitive. ```ts Column({ gap: 6, children: [H1('Title'), Text('Body')] }) Column({ children: [Text('No gap')] }) // shorthand ``` | Prop | Type | Description | |------|------|-------------| | `gap` | `number \| GapToken` | Spacing between children. Accepts a number or semantic token: `'none'`, `'xs'`, `'sm'`, `'md'`, `'lg'`, `'xl'`, `'2xl'` | | `align` | `string` | Cross-axis alignment (`start`, `center`, `end`, `stretch`) | | `justify` | `string` | Main-axis alignment | | `cssClass` | `RxStr` | Extra CSS class — supports reactive expressions (e.g. `rx('active').then('bg-green', 'bg-red')`) | | `onClick` | `Action \| Action[]` | Action(s) dispatched on click. Non-button elements get `role="button"` and keyboard support automatically | **Gap tokens** map to numbers: `none`=0, `xs`=1, `sm`=2, `md`=3, `lg`=4, `xl`=6, `2xl`=8. ```ts Column({ gap: 'md', children: [Text('Hello')] }) // same as gap: 3 Row({ gap: 'xl', children: [Button('A'), Button('B')] }) ``` ### `Row(props?)` Horizontal flex container. ```ts Row({ gap: 4, children: [Button('Cancel'), Button('Save')] }) ``` Same props as `Column`. ### `Grid(props?)` CSS Grid container. ```ts Grid({ columns: 3, gap: 4, children: [ GridItem({ colSpan: 2, children: [Card({ children: [Text('Wide')] })] }), GridItem({ children: [Card({ children: [Text('Narrow')] })] }), ] }) ``` | Prop | Type | Description | |------|------|-------------| | `columns` | `number` | Number of columns | | `gap` | `number \| GapToken` | Grid gap (number or semantic token) | ### `GridItem(props?)` Child of `Grid`. | Prop | Type | Description | |------|------|-------------| | `colSpan` | `number` | Column span | | `rowSpan` | `number` | Row span | ### `Container(props?)` Generic wrapper with max-width and padding. ### `Div(props?)` / `Span(props?)` Generic block/inline wrappers. ### `Dashboard(props?)` / `DashboardItem(props?)` Dashboard grid layout with named items. ### `Pages(props?)` / `Page(props?)` Paginated view container. ### `Detail(props)` Conditional detail pane. Shows `children` when `of` resolves, shows `empty` otherwise. ```ts import { collection, signal, Detail, Heading, Text } from '@maxhealth.tech/prefab' const patients = collection('patients', data, { key: 'id' }) const selectedId = signal('selectedPatientId', patients.firstKey()) const selected = patients.by(selectedId) Detail({ of: selected, empty: Text('Select a patient'), children: [ Heading(selected.dot('name')), Text(selected.dot('dob')), ] }) ``` | Prop | Type | Description | |------|------|-------------| | `of` | `Ref \| RxStr` | Reactive reference expression | | `empty` | `Component` | Shown when ref is null/undefined | ### `MasterDetail(props?)` Two-pane layout (master list + detail). Expects two children. ```ts MasterDetail({ masterWidth: '350px', gap: 4, children: [ table, // master panel detail, // detail panel ] }) ``` | Prop | Type | Description | |------|------|-------------| | `masterWidth` | `string` | Master pane width (default: `'33%'`) | | `gap` | `number` | Gap between panes | *** ## Typography Text rendering components. ### `Heading(content, props?)` ```ts Heading('Welcome', { level: 2 }) ``` | Prop | Type | Description | |------|------|-------------| | `content` | `string \| Rx` | Text content (supports reactive expressions) | | `level` | `1-4` | Heading level | ### `H1(content)` / `H2(content)` / `H3(content)` / `H4(content)` Shorthand heading constructors. ```ts H1('Dashboard') // same as Heading('Dashboard', { level: 1 }) ``` ### `Text(content, props?)` ```ts Text('Hello world') Text('Welcome, {{ userName }}!') ``` ### `P(content)` / `Lead(content)` / `Large(content)` / `Small(content)` / `Muted(content)` Semantic text variants. ### `BlockQuote(content)` Block quotation. ### `Label(content, props?)` Form label text. ### `Link(content, props?)` ```ts Link('Visit site', { href: 'https://example.com', target: '_blank' }) ``` | Prop | Type | Description | |------|------|-------------| | `href` | `string` | URL | | `target` | `string` | Link target (`_blank`, `_self`, etc.) | ### `Code(content)` / `Kbd(content)` Inline code / keyboard shortcut styling. ### `Markdown(content)` Rendered Markdown content. ```ts Markdown('## Hello\n\nThis is **bold** text.') ``` *** ## Card Card containers for grouped content. ### `Card(props?)` ```ts Card({ children: [ CardHeader({ children: [CardTitle('User'), CardDescription('Profile info')] }), CardContent({ children: [Text('Name: Alice')] }), CardFooter({ children: [Button('Edit')] }), ] }) // With variant: Card({ variant: 'elevated', children: [CardContent({ children: [Text('Raised card')] })] }) ``` | Prop | Type | Description | |------|------|-------------| | `variant` | `CardVariant` | `'default'` | `'outline'` | `'ghost'` | `'elevated'` | `'destructive'` | ### `CardHeader` / `CardTitle` / `CardDescription` / `CardContent` / `CardFooter` Card sub-components. All accept children. *** ## Data Display ### `DataTable(props)` Rich data table with search, column definitions, and optional row selection. ```ts DataTable({ rows: users, columns: [ col('name', 'Name'), col('email', 'Email'), col('status', 'Status'), ], search: true, }) ``` | Prop | Type | Description | |------|------|-------------| | `rows` | `unknown[] \| RxStr` | Array of row objects (or reactive expression) | | `columns` | `DataTableColumnDef[]` | Column definitions (use `col()`) | | `search` | `boolean` | Enable search | | `from` | `Collection` | Derive rows from a Collection (mutually exclusive with `rows`) | | `selected` | `Signal` | Signal tracking selected row key (requires `from`) | #### Row Selection with Signal/Collection When `from` and `selected` are provided, DataTable auto-generates row click handling: ```ts const patients = collection('patients', data, { key: 'id' }) const selectedId = signal('selectedPatientId', patients.firstKey()) DataTable({ columns: [col('name'), col('dob')], from: patients, selected: selectedId, }) // Wire: rows="{{ patients }}", rowKey="id", selected="{{ selectedPatientId }}", // onRowClick=[{ action: "setState", key: "selectedPatientId", value: "{{ $item.id }}" }] ``` ### `col(key, header?, opts?)` Column definition helper — short form or descriptor form. ```ts // Short form col('name', 'Full Name') col('email', 'Email', { sortable: true }) // Descriptor form (object) col({ key: 'amount', header: 'Amount', format: 'currency' }) col({ key: 'name', accessor: 'name | humanName', header: 'Patient' }) ``` | Field | Type | Description | |-------|------|-------------| | `key` | `string` | Row object field name | | `header` | `string` | Column header (defaults to `key`) | | `sortable` | `boolean` | Enable column sorting | | `format` | `string` | Pipe name for cell display (e.g. `'currency'`) | | `accessor` | `string` | Pipe expression for complex access | ### `Badge(content, props?)` ```ts Badge('Active', { variant: 'success' }) ``` | Variant | Color | |---------|-------| | `default` | Neutral | | `secondary` | Muted | | `outline` | Border only | | `success` | Green | | `warning` | Yellow | | `destructive` | Red | | `info` | Blue | ### `Metric(props)` ```ts Metric({ label: 'Revenue', value: '$125K', delta: '+12.5%' }) ``` | Prop | Type | Description | |------|------|-------------| | `label` | `string` | Metric name | | `value` | `string \| number` | Display value | | `change` | `number` | Percentage change (positive = green, negative = red) | | `prefix` | `string` | Value prefix (e.g. `$`) | | `suffix` | `string` | Value suffix (e.g. `%`) | ### Other Data Components | Component | Description | |-----------|-------------| | `Dot(props)` | Colored status dot | | `Ring(props)` | Circular progress ring | | `Progress(props)` | Linear progress bar | | `Separator()` | Horizontal divider | | `Loader()` | Loading spinner | | `Icon(name, props?)` | Named icon | *** ## Table Low-level HTML table primitives (for custom table layouts beyond `DataTable`). ```ts Table({ striped: true, children: [ TableHead({ children: [ TableRow({ children: [TableHeader('Name'), TableHeader('Age')] }), ] }), TableBody({ children: [ TableRow({ children: [TableCell({ children: [Text('Alice')] }), TableCell({ children: [Text('30')] })] }), ] }), TableCaption('User list'), ] }) ``` ### Components `Table`, `TableHead`, `TableBody`, `TableFooter`, `TableRow`, `TableHeader`, `TableCell`, `TableCaption`, `ExpandableRow` `TableCell` supports `colSpan` and `rowSpan` props. *** ## Form ### `Form(props?)` ```ts Form({ onSubmit: new CallTool('create_user'), children: [ Input({ name: 'email', inputType: 'email', required: true }), Button('Create', { submit: true }), ] }) ``` | Prop | Type | Description | |------|------|-------------| | `onSubmit` | `Action` | Action to run when the form is submitted | ### `Input(props)` ```ts Input({ name: 'email', inputType: 'email', label: 'Email', placeholder: 'you@example.com', required: true }) ``` | Prop | Type | Description | |------|------|-------------| | `name` | `string` | State key (also used as form field name) | | `type` | `string` | `text`, `email`, `number`, `password`, `url`, `tel`, `date`, `search`, `hidden` | | `label` | `string` | Label text | | `placeholder` | `string` | Placeholder text | | `required` | `boolean` | Validation | | `defaultValue` | `string` | Initial value | | `onChange` | `Action` | Action on value change | ### `Textarea(props)` Multi-line text input. Same props as `Input` plus `rows`. ### `Button(content, props?)` ```ts Button('Save', { variant: 'default', onClick: new ShowToast('Saved!') }) ``` | Prop | Type | Description | |------|------|-------------| | `variant` | `ButtonVariant` | `default`, `secondary`, `outline`, `ghost`, `destructive`, `link` | | `size` | `ButtonSize` | `default`, `sm`, `lg`, `icon` | | `onClick` | `Action` | Click action | | `type` | `string` | `button` (default), `submit` | | `disabled` | `boolean` | Disabled state | ### `ButtonGroup(children)` Horizontal button row. ### `Select(props?)` ```ts Select({ name: 'role', label: 'Role', required: true, children: [ SelectOption('admin', 'Admin'), SelectOption('user', 'User'), ] }) ``` | Prop | Type | Description | |------|------|-------------| | `name` | `string` | State key the choice is stored under | | `label` | `string` | Visible field label | | `value` | `string \| string[]` | Pre-selected choice, or choices when `multiple` | | `placeholder` | `string` | Shown until a choice is made | | `required` | `boolean` | Must be filled before submit | | `multiple` | `boolean` | Accept several choices; submits an array under `name` | | `onChange` | `Action` | Fired with the chosen value | Sub-components: `SelectOption`, `SelectGroup`, `SelectLabel`, `SelectSeparator` `required` is shared by every stateful control, so `RadioGroup`, `Combobox`, `Textarea`, `Calendar` and `DatePicker` take it the same way. ### `Checkbox(props)` / `Switch(props)` / `Slider(props)` Boolean and range inputs. ```ts Checkbox({ name: 'agree', label: 'I agree to the terms' }) Switch({ name: 'notifications', label: 'Enable notifications' }) Slider({ name: 'volume', min: 0, max: 100, step: 1 }) ``` ### `Radio(props)` / `RadioGroup(props?)` ```ts RadioGroup({ name: 'color', label: 'Favorite Color', children: [ Radio({ value: 'red', label: 'Red' }), Radio({ value: 'blue', label: 'Blue' }), Radio({ value: 'green', label: 'Green' }), ] }) ``` ### `Combobox(props?)` / `ComboboxOption(props)` Autocomplete select with search. ```ts Combobox({ name: 'country', placeholder: 'Search countries...', searchable: true, children: [ ComboboxOption('us', 'United States'), ComboboxOption('de', 'Germany'), ] }) ``` Sub-components: `ComboboxGroup`, `ComboboxLabel`, `ComboboxSeparator` ### `Calendar(props)` / `DatePicker(props)` Date selection. ```ts Calendar({ name: 'date', minDate: '2024-01-01', maxDate: '2024-12-31' }) DatePicker({ name: 'dob', label: 'Date of Birth', placeholder: 'Pick a date' }) ``` ### `Field(props?)` Structured form field wrapper. ```ts Field({ children: [ FieldTitle('Email'), FieldDescription('Your work email address'), FieldContent({ children: [Input({ name: 'email', inputType: 'email' })] }), FieldError('Invalid email format'), ] }) ``` Sub-components: `FieldTitle`, `FieldDescription`, `FieldContent`, `FieldError` ### `ChoiceCard(props)` Selectable card for option picking. ```ts ChoiceCard({ value: 'pro', label: 'Pro Plan', description: '$29/mo', selected: true }) ``` *** ## Interactive ### `Tabs(props?)` Tabbed interface with keyboard navigation (Arrow keys, Home/End). ```ts Tabs({ defaultTab: 'overview', children: [ Tab({ id: 'overview', title: 'Overview', children: [Text('Overview content')] }), Tab({ id: 'details', title: 'Details', children: [Text('Details content')] }), ] }) ``` ### `Accordion(props?)` Collapsible sections. ```ts Accordion({ children: [ AccordionItem({ title: 'FAQ 1', children: [Text('Answer 1')] }), AccordionItem({ title: 'FAQ 2', children: [Text('Answer 2')] }), ] }) ``` ### `Dialog(props?)` Modal dialog (ARIA `role="dialog"`). ```ts Dialog({ title: 'Confirm', trigger: Button('Delete'), children: [ Text('Are you sure?'), Button('Delete', { variant: 'destructive', onClick: new CallTool('delete_item') }), ] }) ``` ### `Popover(props?)` / `Tooltip(props?)` / `HoverCard(props?)` Overlay components. ### `Carousel(props?)` Image/content carousel with prev/next buttons. *** ## Charts All charts accept a `data` prop (array of objects) and a `series` array. ### `BarChart(props)` / `LineChart(props)` / `AreaChart(props)` / `PieChart(props)` ```ts BarChart({ data: [{ month: 'Jan', revenue: 100 }, { month: 'Feb', revenue: 150 }], series: [{ dataKey: 'revenue', label: 'Revenue', color: '#3b82f6' }], xAxis: 'month', valueFormat: 'currency', height: 300, }) ``` | Prop | Type | Description | |------|------|-------------| | `data` | `object[]` | Data array | | `series` | `ChartSeries[]` | Series definitions (see below) | | `xAxis` | `string` | Data key for X axis labels | | `xAxisFormat` | `string` | Pipe applied to X axis tick labels (e.g. `'date'`, `'truncate:10'`) | | `tooltipXKey` | `string` | Data key for tooltip category label (defaults to `xAxis`) | | `tooltipXFormat` | `string` | Pipe applied to tooltip category label (e.g. `'datetime'`, `'upper'`) | | `height` | `number` | Chart height in px | | `showTooltip` | `boolean` | Show tooltip on hover (default `true`) | | `showGrid` | `boolean` | Show horizontal grid lines | | `showYAxis` | `boolean` | Show Y axis labels (default `true`) | | `valueFormat` | `string` | **Canonical** value-axis tick + tooltip format: `'currency'`, `'percent:1'`, `'compact'`, … (`'auto'` = none). Matches upstream prefab (PR #454). | | `yAxisFormat` | `string` | Left-axis override for dual-axis charts (overrides `valueFormat`) | | `showYAxisRight` | `boolean` | Show secondary Y axis on the right | | `yAxisRightFormat` | `string` | Format for the right Y axis | | `showLegend` | `boolean` | Show legend below chart | `PieChart` takes `dataKey` (numeric value) + `nameKey` (slice label) instead of `series`/`xAxis` (the legacy series form is still accepted), plus `innerRadius`, `showLabel`, `paddingAngle`, and `valueFormat`. #### `ChartSeries` | Field | Type | Description | |-------|------|-------------| | `dataKey` | `string` | **Required.** Key in data objects for this series | | `label` | `string` | Display label (defaults to `dataKey`) | | `color` | `string` | Series color (auto-assigned if omitted) | | `yAxisId` | `'left' \| 'right'` | Which Y axis this series binds to | | `tooltipFormat` | `string` | Pipe for this series' value in the tooltip (overrides `yAxisFormat`) | #### Formatting Example Same timestamp field, different presentation on axis vs tooltip: ```ts LineChart({ data: timeseries, xAxis: 'timestamp', xAxisFormat: 'date', // axis: "4/25/2026" tooltipXFormat: 'datetime', // tooltip: "4/25/2026, 2:30:00 PM" series: [ { dataKey: 'revenue', label: 'Revenue', tooltipFormat: 'currency' }, { dataKey: 'growth', label: 'Growth', tooltipFormat: 'percent' }, ], }) ``` All built-in pipes work: `upper`, `lower`, `truncate`, `currency`, `percent`, `compact`, `date`, `time`, `datetime`, `number`, `round`, plus custom wire pipes. ### `RadarChart(props)` Spider/radar chart — one polygon per series across angular axes. ```ts RadarChart({ data: [{ subject: 'Math', alice: 120 }, { subject: 'English', alice: 98 }], series: [{ dataKey: 'alice', label: 'Alice' }], axisKey: 'subject', // spoke labels (falls back to xAxis) filled: true, // fill polygons (default true) showDots: false, // vertices }) ``` ### `ScatterChart(props)` Scatter / bubble chart. ```ts ScatterChart({ data: [{ h: 170, w: 65, age: 25 }, { h: 180, w: 80, age: 30 }], series: [{ dataKey: 'people', label: 'People' }], xAxis: 'h', yAxis: 'w', zAxis: 'age', // optional — sizes each point (bubble chart) }) ``` ### `Sparkline(props)` Inline mini chart. ```ts Sparkline({ data: [10, 20, 15, 30, 25], variant: '#22c55e', height: 32 }) ``` ### `RadialChart(props)` / `Histogram(props)` Radial bar chart (concentric value arcs) and histogram distribution chart. ```ts RadialChart({ data: [{ browser: 'Chrome', visitors: 275 }, { browser: 'Safari', visitors: 200 }], dataKey: 'visitors', // value (legacy series form still accepted) nameKey: 'browser', // label innerRadius: 30, startAngle: 180, endAngle: 0, }) ``` > All chart types now render natively as SVG in the built-in renderer. *** ## Media ### `Image(src, opts?)` / `Image(props)` Positional form (consistent with Audio/Video/Embed) or props form: ```ts Image('/photo.jpg', { alt: 'Profile photo' }) // positional (recommended) Image({ src: '/photo.jpg', alt: 'Profile photo' }) // props form (also works) ``` ### `Audio(props)` / `Video(props)` / `Embed(props)` Media embeds. ### `Svg(props)` Inline SVG content. ### `DropZone(props)` File upload drop area. Accepts files by drag-and-drop or by click-to-browse, and is keyboard reachable. ```ts DropZone({ accept: 'image/*', multiple: true, resultKey: 'uploads', onDrop: new CallTool('upload_file'), }) ``` | Prop | Type | Description | |------|------|-------------| | `label` | `RxStr` | Prompt shown inside the area. Defaults to `Drop files here` | | `accept` | `string` | Accepted types, as the HTML `accept` attribute (`image/*`, `.pdf`, `text/csv`). Enforced for dropped files too, which the browser does not do | | `multiple` | `boolean` | Allow more than one file. When false, only the first accepted file is taken | | `resultKey` | `string` | State key the chosen files are written to | | `onDrop` | `Action \| Action[]` | Fired once files are chosen, with `$result` bound to the file list | `resultKey` and `$result` match the `OpenFilePicker` action, so both paths to files behave the same. ### `Mermaid(content)` Mermaid diagram (rendered by the browser if `mermaid` library is available). ```ts Mermaid('graph TD; A-->B; B-->C;') ``` *** ## Alert ### `Alert(props?)` ```ts Alert({ variant: 'warning', children: [ AlertTitle('Warning'), AlertDescription('This action cannot be undone.'), ] }) Alert({ variant: 'success', icon: 'CheckCircle', children: [ AlertTitle('Saved'), AlertDescription('Changes applied successfully.'), ] }) ``` | Variant | Color | |---------|-------| | `default` | Neutral | | `success` | Green | | `warning` | Yellow | | `destructive` | Red | | Prop | Type | Description | |------|------|-------------| | `variant` | `AlertVariant` | `'default'` | `'destructive'` | `'success'` | `'warning'` | | `icon` | `string` | Icon name (e.g. `'CheckCircle'`, `'AlertTriangle'`) | *** ## Control Flow ### `ForEach(props?)` Iterate over a reactive array. ```ts ForEach({ expression: rx('items'), children: [ Text(ITEM.dot('name')), ] }) ``` | Prop | Type | Description | |------|------|-------------| | `each` | `Rx \| string` | Expression for the array to iterate | | `as` | `string` | Variable name for each item (default: `item`) | ### `If(condition, children)` / `If(props)` / `Elif` / `Else` Conditional rendering. Supports both shorthand and props form: ```ts // Shorthand (recommended): If('$loading', [Loader()]) Elif('$error', [Alert({ variant: 'destructive', children: [Text('$error')] })]) Else({ children: [Text('Content loaded!')] }) // Props form (also works): If({ condition: '$loading', children: [Loader()] }) ``` ### `Define(props?)` / `Use(props)` / `Slot(props?)` Component templates for reuse. ```ts Define({ name: 'userCard', children: [ Card({ children: [CardContent({ children: [ Slot({ name: 'name' }), Slot({ name: 'role' }), ] })] }), ] }) // Later: Use({ def: 'userCard', overrides: { name: 'Alice', role: 'Admin' } }) ``` --- --- url: /prefab/reference/wire-format.md description: >- $prefab v0.3 wire format specification — JSON structure, component nodes, state, actions, pipes, defs, and template slots. --- # Wire Format Specification The `$prefab` wire format is the JSON protocol that connects server-side component builders to client-side renderers. Both the TypeScript and Python libraries produce this format. > **Note:** This TypeScript library is a superset of the Python `prefab-ui` (v0.20.x). Core components and the v0.3 protocol are identical. Chart formatting props (`xAxisFormat`, `tooltipXFormat`, `tooltipXKey`, per-series `tooltipFormat`, dual Y-axis) are TS-only extensions. The renderer handles both payloads seamlessly, and still accepts legacy `0.2` payloads. ## Envelope Every prefab UI is wrapped in a top-level envelope: ```json { "$prefab": { "version": "0.3" }, "view": { ... }, "state": { ... }, "css": [ ... ], "stylesheets": [ ... ], "mode": "dark", "defs": { ... }, "keyBindings": { ... }, "onMount": { ... } } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `$prefab` | `{ version: string }` | **Yes** | Format identifier. Current version: `"0.3"` | | `view` | `ComponentJSON` | **Yes** | Root component tree | | `state` | `Record` | No | Initial reactive state | | `css` | `string[]` | No | Inline CSS blocks injected as `