@maxhealth.tech/prefab 0.3.8 → 0.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +79 -11
  3. package/dist/a2ui/browser.d.ts +46 -0
  4. package/dist/a2ui/browser.d.ts.map +1 -0
  5. package/dist/a2ui/browser.js +58 -0
  6. package/dist/a2ui/browser.js.map +1 -0
  7. package/dist/a2ui/catalog.d.ts +51 -0
  8. package/dist/a2ui/catalog.d.ts.map +1 -0
  9. package/dist/a2ui/catalog.js +465 -0
  10. package/dist/a2ui/catalog.js.map +1 -0
  11. package/dist/a2ui/emit.d.ts +59 -0
  12. package/dist/a2ui/emit.d.ts.map +1 -0
  13. package/dist/a2ui/emit.js +269 -0
  14. package/dist/a2ui/emit.js.map +1 -0
  15. package/dist/a2ui/expr.d.ts +58 -0
  16. package/dist/a2ui/expr.d.ts.map +1 -0
  17. package/dist/a2ui/expr.js +81 -0
  18. package/dist/a2ui/expr.js.map +1 -0
  19. package/dist/a2ui/icons.d.ts +30 -0
  20. package/dist/a2ui/icons.d.ts.map +1 -0
  21. package/dist/a2ui/icons.js +87 -0
  22. package/dist/a2ui/icons.js.map +1 -0
  23. package/dist/a2ui/index.d.ts +27 -0
  24. package/dist/a2ui/index.d.ts.map +1 -0
  25. package/dist/a2ui/index.js +23 -0
  26. package/dist/a2ui/index.js.map +1 -0
  27. package/dist/a2ui/table.d.ts +31 -0
  28. package/dist/a2ui/table.d.ts.map +1 -0
  29. package/dist/a2ui/table.js +155 -0
  30. package/dist/a2ui/table.js.map +1 -0
  31. package/dist/a2ui/types.d.ts +174 -0
  32. package/dist/a2ui/types.d.ts.map +1 -0
  33. package/dist/a2ui/types.js +35 -0
  34. package/dist/a2ui/types.js.map +1 -0
  35. package/dist/a2ui.min.js +1 -0
  36. package/dist/app.d.ts +17 -0
  37. package/dist/app.d.ts.map +1 -1
  38. package/dist/app.js +19 -0
  39. package/dist/app.js.map +1 -1
  40. package/dist/auto/form.d.ts +25 -0
  41. package/dist/auto/form.d.ts.map +1 -1
  42. package/dist/auto/form.js +21 -8
  43. package/dist/auto/form.js.map +1 -1
  44. package/dist/components/charts/index.d.ts +4 -0
  45. package/dist/components/charts/index.d.ts.map +1 -1
  46. package/dist/components/charts/index.js +2 -0
  47. package/dist/components/charts/index.js.map +1 -1
  48. package/dist/components/form/index.d.ts +1 -6
  49. package/dist/components/form/index.d.ts.map +1 -1
  50. package/dist/components/form/index.js +7 -37
  51. package/dist/components/form/index.js.map +1 -1
  52. package/dist/components/interactive/index.d.ts +5 -1
  53. package/dist/components/interactive/index.d.ts.map +1 -1
  54. package/dist/components/interactive/index.js +5 -2
  55. package/dist/components/interactive/index.js.map +1 -1
  56. package/dist/components/media/index.d.ts +20 -2
  57. package/dist/components/media/index.d.ts.map +1 -1
  58. package/dist/components/media/index.js +13 -1
  59. package/dist/components/media/index.js.map +1 -1
  60. package/dist/components/table/index.d.ts +5 -1
  61. package/dist/components/table/index.d.ts.map +1 -1
  62. package/dist/components/table/index.js +5 -2
  63. package/dist/components/table/index.js.map +1 -1
  64. package/dist/core/component.d.ts +11 -0
  65. package/dist/core/component.d.ts.map +1 -1
  66. package/dist/core/component.js +22 -9
  67. package/dist/core/component.js.map +1 -1
  68. package/dist/core/version.d.ts +1 -1
  69. package/dist/core/version.d.ts.map +1 -1
  70. package/dist/core/version.js +1 -1
  71. package/dist/core/version.js.map +1 -1
  72. package/dist/index.d.ts +9 -2
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +4 -0
  75. package/dist/index.js.map +1 -1
  76. package/dist/mcp/a2ui.d.ts +91 -0
  77. package/dist/mcp/a2ui.d.ts.map +1 -0
  78. package/dist/mcp/a2ui.js +134 -0
  79. package/dist/mcp/a2ui.js.map +1 -0
  80. package/dist/mcp/display.d.ts +28 -6
  81. package/dist/mcp/display.d.ts.map +1 -1
  82. package/dist/mcp/display.js +10 -9
  83. package/dist/mcp/display.js.map +1 -1
  84. package/dist/mcp/index.d.ts +7 -2
  85. package/dist/mcp/index.d.ts.map +1 -1
  86. package/dist/mcp/index.js +3 -1
  87. package/dist/mcp/index.js.map +1 -1
  88. package/dist/mcp/input-required.d.ts +79 -0
  89. package/dist/mcp/input-required.d.ts.map +1 -0
  90. package/dist/mcp/input-required.js +231 -0
  91. package/dist/mcp/input-required.js.map +1 -0
  92. package/dist/mcp/resource.d.ts +10 -0
  93. package/dist/mcp/resource.d.ts.map +1 -1
  94. package/dist/mcp/resource.js +12 -4
  95. package/dist/mcp/resource.js.map +1 -1
  96. package/dist/mcp/result.d.ts +7 -5
  97. package/dist/mcp/result.d.ts.map +1 -1
  98. package/dist/mcp/result.js +5 -3
  99. package/dist/mcp/result.js.map +1 -1
  100. package/dist/mcp/types.d.ts +117 -0
  101. package/dist/mcp/types.d.ts.map +1 -1
  102. package/dist/prefab.css +4 -0
  103. package/dist/renderer/components/data.js +2 -2
  104. package/dist/renderer/components/data.js.map +1 -1
  105. package/dist/renderer/components/form.js +41 -15
  106. package/dist/renderer/components/form.js.map +1 -1
  107. package/dist/renderer/components/index.d.ts.map +1 -1
  108. package/dist/renderer/components/index.js +6 -0
  109. package/dist/renderer/components/index.js.map +1 -1
  110. package/dist/renderer/components/interactive.js +17 -4
  111. package/dist/renderer/components/interactive.js.map +1 -1
  112. package/dist/renderer/components/media.d.ts +7 -0
  113. package/dist/renderer/components/media.d.ts.map +1 -1
  114. package/dist/renderer/components/media.js +63 -2
  115. package/dist/renderer/components/media.js.map +1 -1
  116. package/dist/renderer/components/table.js +1 -1
  117. package/dist/renderer/components/table.js.map +1 -1
  118. package/dist/renderer/engine.d.ts +4 -0
  119. package/dist/renderer/engine.d.ts.map +1 -1
  120. package/dist/renderer/engine.js +24 -0
  121. package/dist/renderer/engine.js.map +1 -1
  122. package/dist/renderer.auto.min.js +13 -13
  123. package/dist/renderer.min.js +13 -13
  124. package/package.json +13 -3
package/CHANGELOG.md CHANGED
@@ -4,6 +4,50 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.3.10] — 2026-08-24
8
+
9
+ ### Added
10
+
11
+ - **A2UI is now a first-class output target: `PrefabApp.toA2UI()` emits the same component tree as [A2UI](https://a2ui.org) v1.0.** A2UI is the Google-backed declarative agent-to-UI protocol, and it overlaps prefab's category exactly: the agent sends a component tree plus a data model, and the host's own renderer draws native widgets from it. It reaches renderers prefab has none of (React, Angular, Lit, Flutter, Swift, Jetpack Compose) with no iframe involved, and its agent-side SDK is Python-only, so a TypeScript authoring layer had no equivalent. Two structural gaps had to be bridged: A2UI keeps components in a flat adjacency list rather than a nested tree, and it binds dynamic values through JSON Pointers rather than interpolating `{{ }}` templates. `src/a2ui/` handles both, allocating ids deterministically so the same tree always emits byte-identical output.
12
+ - **`display_a2ui()` and `registerA2uiResource()` serve A2UI over MCP**, under the `application/a2ui+json` MIME type as an embedded resource in a tool result or as a standalone `a2ui://` resource, per the [A2UI over MCP](https://a2ui.org/guides/a2ui_over_mcp/) guide. They sit alongside the existing `ui://` MCP Apps helpers rather than replacing them, so one server can offer both and let the host choose. A `ui://` viewer is a pure function of the package version and stays shared-cacheable; an `a2ui://` surface is rebuilt on every read and defaults to no caching, so `resolveCache` is now exported from `resource.ts` and takes the defaults as a parameter instead of hard-coding the viewer's.
13
+ - **Multi Round-Trip input requests (protocol revision 2026-07-28).** The revision made the protocol core stateless and removed server-initiated `elicitation/create`; a handler now returns an `input_required` result and the client retries the call with the answers. That left `display_form()` reachable only on hosts that render UI. The same `AutoFormField[]` now also derives the restricted elicitation schema, so `display_form(fields, tool, { elicit: true })` asks for exactly what the rendered form asks for. `formSchema`, `formInputRequest`, `acceptedFormInput` and `inputResponse` are exported for handlers that compose the rounds themselves. `acceptedFormInput` validates the untrusted client response against the same field list: unknown keys, wrong types, out-of-bound numbers and unoffered enum values are dropped, and a missing required field fails the answer.
14
+ - **The playground has an A2UI tab.** The preview pane now switches between the rendered UI and the same payload translated to A2UI, with the message and component counts, a pill per diagnostic kind, and every diagnostic listed underneath. A `Stream` toggle splits the output into the three-message form. Being able to see what a payload costs in translation, on the twenty shipped examples, says more than the mapping table does.
15
+ - **`dist/a2ui.min.js`, the emitter as a standalone browser bundle.** The playground needs the emitter and does not need it inside `renderer.min.js`: almost no page that renders `$prefab` also emits A2UI, so folding it in would tax every consumer for a feature they do not use. It exposes `PrefabA2UI.emit()` and `PrefabA2UI.envelope()`, takes wire JSON rather than a component tree, and comes to 17 KB against the renderer's 108 KB. Attached to GitHub releases alongside the renderer bundles.
16
+ - `AutoFormField` gained `options`, `multiple`, `description`, `min`, `max` and `default`. A field with `options` renders as a `Select` instead of a bare `Input` on the UI path, and as an enum on the elicitation path.
17
+ - `McpDisplayResult<S>` pins `structuredContent` as present. Every display helper populates it, and the optional field on `McpToolResult` was forcing callers into a null check on something that is never absent.
18
+
19
+ ### Changed
20
+
21
+ - **README, docs landing page and package description now lead with "TypeScript authoring for A2UI and MCP Apps".** The previous framing put "a superset of PrefectHQ's Python prefab-ui" in the second line, which tied the package's story to a project moving slowly and described an implementation detail rather than what the package is for. The superset relationship is still stated, one bullet down, where it belongs.
22
+
23
+ ### Fixed
24
+
25
+ - **The A2UI emitter produced an invalid surface for most icons.** A2UI's `Icon.name` is a closed enum of 59 Material names, so an unrecognised value does not degrade to a fallback glyph — it fails schema validation and takes the component with it. prefab's `Icon(name)` accepts any string and follows Lucide, so every icon outside that enum emitted an invalid payload. The hand-written test view happened to use `Icon('mail')`, which is in the enum, so nothing caught it. `src/a2ui/icons.ts` now normalises the name, matches it against the enum case- and separator-insensitively, translates the Lucide names prefab itself ships (`AlertCircle` → `error`, `CheckCircle` → `check`), and drops the icon with a diagnostic when nothing means the same thing. The enum is duplicated as a literal because the package has no runtime dependencies and cannot read the catalog at emit time; a test asserts it matches the vendored catalog exactly, so a spec revision cannot let it drift.
26
+ - **`test/component-types.test.ts` depended on which test file the runner reached first.** It compared the generated `src/core/component-types.ts` against the whole renderer registry, which is a process-wide singleton any module can add to. `test/renderer-destroy.test.ts` and `test/pipe-wire.test.ts` register a dozen widgets inside their test bodies, so once the runner reached either of them first, all twelve became "renderable but absent from the generated list" and thirteen assertions failed. It passed locally and failed on CI purely on file-discovery order, and adding the A2UI test files was enough to flip it. `registerAllComponents()` now records the delta it contributes as the built-in set, and both the generator and the sync check read that instead of the full registry. The generated list is unchanged at 119 types.
27
+
28
+ ### Internal
29
+
30
+ - **Registry-wide A2UI invariants, rather than one hand-written view per mapper family.** The per-family views check that the mappings are right; they cannot check that they are safe, because a mapper is only covered if someone remembered to write a view for it, and the branches that matter most are the guards deciding "this cannot be expressed, drop it and say why". Coverage of `catalog.ts` sat at 72% of functions with nearly every uncovered line being one of those guards. `test/a2ui-registry.test.ts` now asserts two properties across all 81 registry entries, so a mapper added later is covered the moment it is registered: nothing a mapper emits is ever invalid, given generous props or none at all; and nothing is dropped without a diagnostic. The furnished pass additionally requires an actual component, since "emits nothing" would otherwise satisfy the first property trivially. This is what surfaced the `Icon` enum bug. `catalog.ts` is now at 100% of functions and 97% of lines.
31
+ - Removed `hasTemplate` and `isTemplatedList`, both exported with no caller anywhere.
32
+ - Emitted A2UI payloads are validated in CI against the official v1.0 JSON Schemas, vendored under `test/fixtures/a2ui/v1_0/` (Apache-2.0, with a `NOTICE.md` recording the upstream commit) and refreshable with `bun scripts/sync-a2ui-schemas.ts`. They are checked in rather than fetched so the suite runs offline. The upstream YAML conformance suites were considered and rejected: they exercise SDK internals (streaming parser, catalog pruning, validator behaviour) that a producer does not implement, whereas the schemas define exactly what a producer must emit. Two structural rules the schemas cannot express — every child reference resolves, and every component is reachable from `root` — are asserted separately, taken from `conformance/core/validator.yaml`.
33
+
34
+ ## [0.3.9] — 2026-08-07
35
+
36
+ ### Added
37
+
38
+ - **`label` on every stateful control.** `InputProps` declared it privately and only `renderInput` drew it, so `Select({ label })` and `DatePicker({ label })` were documented but silently dropped. `label` moves to `StatefulProps`, and the wire shape shared by all of them is now one exported `statefulProps()` helper. `Select`, `RadioGroup` and `Combobox` are containers rather than `StatefulComponent`s and were each restating that block by hand, which is exactly how `label` went missing on them; they call the helper now. On the renderer side a single `withLabel()` decorator is applied at registration to the five controls that lay out vertically (`Select`, `DatePicker`, `Combobox`, `Textarea`, `Slider`). Checkbox, Switch, Radio, RadioGroup and ChoiceCard place their own label beside the control and are deliberately not wrapped.
39
+ - **`Tabs({ defaultTab })`** — a `Tab` title or a 0-based index, replacing a hardcoded `i === 0` in the renderer. An unmatched title or out-of-range index falls back to the first tab rather than leaving no tab selected.
40
+ - **`Table({ striped })`** — shades alternate body rows via a `pf-table-striped` modifier and a `:nth-child(even)` rule, rather than per-table styling.
41
+ - **`Sparkline({ width, height })`** — the renderer hardcoded 120×32; those are now the defaults.
42
+
43
+ - **`DropZone` actually handles files.** The renderer was a stub (`// File handling would go here`) with drag listeners and nothing behind them, so the documented `accept` applied to nothing. It now takes files by drag-and-drop or click-to-browse, is keyboard reachable (`role="button"`, Enter/Space), and accepts `label`, `accept`, `multiple`, `resultKey` and `onDrop`. Those names and the `$result` callback binding are the ones the `OpenFilePicker` action already uses, so both routes to a file list behave identically. `accept` is enforced for **dropped** files as well: the browser applies it to the file picker only, so drag-and-drop would otherwise bypass it entirely. Exported `matchesAccept()` handles the three `accept` forms (`image/*`, `.pdf`, `text/csv`); no equivalent existed to reuse, and the org's other drop zones are all React components with no overlap between them.
44
+
45
+ All five were documented in `docs/reference/components.md` with props-table entries before they existed, and were found by `bun run check:docs`.
46
+
47
+ ### Docs
48
+
49
+ - **Every fenced `ts` example now typechecks, and CI enforces it.** `bun run check:docs` runs in `ci.yml` between the test and build steps. 71 blocks across 17 files, from 86 errors down to zero. The remaining fixes after the container-shape pass were all stale API forms rather than missing features: `rx` used as a tagged template (it is `rx(key)`, and interpolated text is a plain `'{{ key }}'` string, while `STATE.name` and `ITEM.dot('name')` cover the built-ins), `SelectOption`/`ComboboxOption` documented with an object when they take `(value, label)` positionally, `GridItem`'s `span` which is `colSpan`, `Button`'s `type: 'submit'` which is `submit: true`, `Metric`'s numeric `delta` where the type is `RxStr`, `Dialog`'s `trigger` documented as an element id when it takes a `Component`, `ForEach`'s `as` which does not exist (the loop variable is the exported `ITEM`), and `Slot('name')` which takes props. Four blocks carry `<!-- doccheck: skip -->` with a stated reason: two entry-point listings using `import { ... }`, a CSS side-effect import the bundler resolves rather than tsc, and companion-script code written against the renderer's browser global.
50
+
7
51
  ## [0.3.8] — 2026-08-05
8
52
 
9
53
  ### Fixed
package/README.md CHANGED
@@ -1,30 +1,39 @@
1
1
  # prefab
2
2
 
3
3
  [![CI](https://github.com/Max-Health-Inc/prefab/actions/workflows/ci.yml/badge.svg)](https://github.com/Max-Health-Inc/prefab/actions/workflows/ci.yml)
4
- [![tests](https://img.shields.io/badge/tests-1077%20passing%20(100%25)-brightgreen)](https://github.com/Max-Health-Inc/prefab/actions/workflows/ci.yml)
4
+ [![tests](https://img.shields.io/badge/tests-2009%20passing%20(100%25)-brightgreen)](https://github.com/Max-Health-Inc/prefab/actions/workflows/ci.yml)
5
5
  [![@maxhealth.tech/prefab](https://img.shields.io/npm/v/@maxhealth.tech/prefab?label=%40maxhealth.tech%2Fprefab)](https://www.npmjs.com/package/@maxhealth.tech/prefab)
6
6
  [![prefab-protocol](https://img.shields.io/badge/prefab--protocol-v0.3-brightgreen)](https://maxhealth.tech/prefab/reference/wire-format.html)
7
+ [![A2UI](https://img.shields.io/badge/A2UI-v1.0-4285F4)](https://maxhealth.tech/prefab/guide/a2ui.html)
7
8
  [![TypeScript](https://img.shields.io/badge/TypeScript-6.0-blue?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
8
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
10
 
10
- The TypeScript full-stack framework for MCP app UIs: author server-side, ship JSON, render anywhere.
11
+ TypeScript authoring for **A2UI** and **MCP Apps**: build the UI server-side, emit either wire format, render anywhere.
11
12
 
12
- A superset of PrefectHQ's Python [prefab-ui](https://github.com/PrefectHQ/prefab), on the **`$prefab` v0.3 wire protocol** (still renders legacy `0.2` payloads).
13
+ **[Live Demo](https://maxhealth.tech/prefab/demo/)** · **[Playground](https://maxhealth.tech/prefab/playground/)** · **[Docs](https://maxhealth.tech/prefab/)**
13
14
 
14
- **[Live Demo](https://maxhealth.tech/prefab/demo/)** · **[Playground](https://maxhealth.tech/prefab/playground/)**
15
+ Agent UIs have settled on two shapes. [MCP Apps](https://modelcontextprotocol.io/seps/1865-mcp-apps-interactive-user-interfaces-for-mcp) ships HTML that the host renders in a sandboxed iframe. [A2UI](https://a2ui.org) ships a declarative component tree that the host's own renderer draws as native widgets. Writing a UI twice to reach both is the problem this package removes.
15
16
 
16
- Write MCP servers in **TypeScript/Bun** and generate the same wire format that Python servers produce. Render the output in **any web app** with the included vanilla DOM renderer. Full circle: server-side DSL → JSON → browser UI.
17
+ You describe the interface once with a typed component API, and prefab emits whichever wire format the host wants:
18
+
19
+ ```ts
20
+ const app = new PrefabApp({ view: Column({ children: [H1('Users'), autoTable(rows)] }) })
21
+
22
+ app.toJSON() // $prefab — rendered by prefab's renderer, in an MCP Apps iframe or any web app
23
+ app.toA2UI() // A2UI v1.0 — rendered natively by React, Angular, Lit, Flutter, Swift, Compose
24
+ ```
17
25
 
18
26
  - **115+ components** — layout, form, data, charts, media, interactive, control flow
27
+ - **Auto-renderers** — `autoTable()`, `autoChart()`, `autoForm()`, `autoMetrics()` and more
28
+ - **Two wire formats** — `$prefab` v0.3 (superset of PrefectHQ's Python [prefab-ui](https://github.com/PrefectHQ/prefab), still renders legacy `0.2`) and [A2UI v1.0](https://a2ui.org), schema-validated against the official specification
29
+ - **MCP-native** — `display()`, `display_a2ui()`, `ui://` and `a2ui://` resource helpers, `input_required` for the 2026-07-28 revision
19
30
  - **Reactive state** — `rx()` expressions, `SetState`/`ToggleState`/`AppendState` actions
20
- - **MCP-native** — `display()`, `display_form()`, `CallTool`, `SendMessage` built in
21
31
  - **Browser renderer** — zero dependencies, vanilla DOM (optional separate import)
22
32
  - **PostMessage bridge** — `app()` factory with dual-protocol handshake, host theme, lifecycle hooks
23
- - **Auto-renderers** — `autoTable()`, `autoChart()`, `autoForm()`, `autoMetrics()` and more
24
33
 
25
34
  ## Works Everywhere
26
35
 
27
- The renderer is **vanilla DOM** — no framework dependency. Drop it into any web app:
36
+ On the `$prefab` path the renderer is **vanilla DOM** — no framework dependency. Drop it into any web app:
28
37
 
29
38
  - **React** — mount into a `ref` div
30
39
  - **Vue / Svelte / Angular** — same, it's just DOM
@@ -32,7 +41,7 @@ The renderer is **vanilla DOM** — no framework dependency. Drop it into any we
32
41
  - **Electron / Tauri** — desktop apps with web views
33
42
  - **Any iframe** — MCP Apps, embedded widgets, sandboxed UIs
34
43
 
35
- Any app that connects to MCP servers can render `$prefab` tool output as rich interactive UI tables, charts, forms, badges with zero custom code.
44
+ On the A2UI path there is no prefab renderer at all: the host draws the components itself, so the same tree reaches the A2UI renderers for React, Angular, Lit, Flutter, Swift and Jetpack Compose. See the [A2UI guide](https://maxhealth.tech/prefab/guide/a2ui) for the mapping table and what degrades.
36
45
 
37
46
  ## Install
38
47
 
@@ -361,6 +370,62 @@ return display_error('User not found', `No user with id ${id}.`, {
361
370
  })
362
371
  ```
363
372
 
373
+ ### Asking for input (MCP 2026-07-28)
374
+
375
+ The revision removed server-initiated elicitation: a handler asks for input by
376
+ returning an `input_required` result, and the client retries the call with the
377
+ answers. The same field list drives both paths, so a host with no UI surface
378
+ still gets the form:
379
+
380
+ ```ts
381
+ const FIELDS = [
382
+ { name: 'email', label: 'Email', type: 'email', required: true },
383
+ { name: 'plan', label: 'Plan', options: [{ value: 'pro' }, { value: 'team' }] },
384
+ ]
385
+
386
+ // Rendered as prefab UI, submitting to the `signup` tool:
387
+ display_form(FIELDS, 'signup', { title: 'Create your account' })
388
+
389
+ // Or asked natively by the client, which then retries the call:
390
+ display_form(FIELDS, 'signup', { title: 'Create your account', elicit: true })
391
+ ```
392
+
393
+ Read the answer back with `acceptedFormInput`, which checks the untrusted client
394
+ response against the same fields. Full walkthrough in
395
+ [Asking for Input](https://maxhealth.tech/prefab/guide/input-required).
396
+
397
+ ## A2UI
398
+
399
+ Emit the same tree as [A2UI](https://a2ui.org) and let the host render it natively:
400
+
401
+ ```ts
402
+ const { messages, diagnostics } = app.toA2UI()
403
+ ```
404
+
405
+ Serve it over MCP under the `application/a2ui+json` MIME type:
406
+
407
+ ```ts
408
+ // Per-call, as an embedded resource in a tool result
409
+ server.registerTool('list-users', schema, async () => display_a2ui(autoTable(await db.users())))
410
+
411
+ // Or as a static a2ui:// resource the host can cache
412
+ registerA2uiResource(server, () => Column({ children: [H1('Settings')] }))
413
+ ```
414
+
415
+ Or translate a payload in the browser, from a bundle separate to the renderer:
416
+
417
+ ```html
418
+ <script src="https://cdn.jsdelivr.net/npm/@maxhealth.tech/prefab/dist/a2ui.min.js"></script>
419
+ <script>const { messages, diagnostics } = PrefabA2UI.emit(wireJson)</script>
420
+ ```
421
+
422
+ prefab has 115+ components and the A2UI Basic catalog has 18, so parts of the
423
+ tree change shape on the way across. `diagnostics` reports every one — nothing
424
+ degrades silently. Payloads are validated against the official A2UI v1.0 JSON
425
+ Schemas in CI. The [playground](https://maxhealth.tech/prefab/playground/) has an
426
+ A2UI tab that shows the translation and its diagnostics live. See the
427
+ [A2UI guide](https://maxhealth.tech/prefab/guide/a2ui).
428
+
364
429
  ### `rendererHtml()` — Viewer HTML Shell
365
430
 
366
431
  Generate the complete HTML page for an MCP Apps viewer resource. Loads `prefab.css` + `renderer.auto.min.js` from the CDN automatically — no manual script wiring needed:
@@ -372,7 +437,7 @@ import { rendererHtml, registerViewerResource } from '@maxhealth.tech/prefab/mcp
372
437
  const html = rendererHtml()
373
438
 
374
439
  // With extras
375
- const html = rendererHtml({
440
+ const htmlWithExtras = rendererHtml({
376
441
  title: 'My App',
377
442
  stylesheets: ['https://cdn.example.com/theme.css'],
378
443
  scripts: ['https://cdn.example.com/plugin.js'],
@@ -489,6 +554,8 @@ All UIs serialize to the `$prefab` wire format (JSON):
489
554
 
490
555
  ## Subpath Exports
491
556
 
557
+ <!-- doccheck: skip — an entry-point listing, not compilable code -->
558
+
492
559
  ```ts
493
560
  import { ... } from '@maxhealth.tech/prefab' // Everything
494
561
  import { ... } from '@maxhealth.tech/prefab/actions' // Actions only
@@ -496,6 +563,7 @@ import { ... } from '@maxhealth.tech/prefab/rx' // Rx expressions only
496
563
  import { ... } from '@maxhealth.tech/prefab/charts' // Chart components only
497
564
  import { ... } from '@maxhealth.tech/prefab/auto' // Auto-renderers
498
565
  import { ... } from '@maxhealth.tech/prefab/mcp' // MCP display helpers
566
+ import { ... } from '@maxhealth.tech/prefab/a2ui' // A2UI emitter
499
567
  import { ... } from '@maxhealth.tech/prefab/renderer' // Browser renderer
500
568
  import '@maxhealth.tech/prefab/prefab.css' // Default stylesheet
501
569
  ```
@@ -504,7 +572,7 @@ import '@maxhealth.tech/prefab/prefab.css' // Default stylesheet
504
572
 
505
573
  ```bash
506
574
  bun install # Install dependencies
507
- bun test # Run tests (996 passing)
575
+ bun test # Run tests
508
576
  bun run build # TypeScript compile + IIFE bundle
509
577
  bun run lint # ESLint
510
578
  bun run typecheck # Type check without emitting
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Browser entry for the A2UI emitter — `dist/a2ui.min.js`.
3
+ *
4
+ * Bundled separately from `renderer.min.js` on purpose. The renderer is what
5
+ * every `$prefab` page loads, and emitting A2UI is something almost none of
6
+ * them do, so folding the emitter in would tax every consumer for a feature
7
+ * they do not use. Keeping it apart also means the two can be loaded
8
+ * independently: a tool that only translates payloads needs no renderer at all.
9
+ *
10
+ * The emitter takes wire JSON and returns wire JSON, so nothing here needs the
11
+ * component API. That is what keeps the bundle small.
12
+ *
13
+ * ```html
14
+ * <script src="https://cdn.jsdelivr.net/npm/@maxhealth.tech/prefab/dist/a2ui.min.js"></script>
15
+ * <script>
16
+ * const { messages, diagnostics } = PrefabA2UI.emit(wireJson)
17
+ * </script>
18
+ * ```
19
+ */
20
+ import { type A2uiEmitOptions, type A2uiEmitResult } from './emit.js';
21
+ import { mappedTypes } from './catalog.js';
22
+ import { type A2uiMessage, type A2uiMessageList } from './types.js';
23
+ /**
24
+ * Emit A2UI from a `$prefab` payload.
25
+ *
26
+ * Accepts `unknown` because the caller is usually handing over parsed editor
27
+ * text or a tool result, neither of which is typed. A payload without a `view`
28
+ * is rejected here rather than producing an empty surface further downstream.
29
+ */
30
+ declare function emit(wire: unknown, options?: A2uiEmitOptions): A2uiEmitResult;
31
+ /** Wrap messages in the list envelope, for transports needing a JSON object. */
32
+ declare function envelope(messages: A2uiMessage[]): A2uiMessageList;
33
+ declare const PrefabA2UI: {
34
+ emit: typeof emit;
35
+ envelope: typeof envelope;
36
+ /** Every prefab component type with a first-class A2UI mapping. */
37
+ mappedTypes: typeof mappedTypes;
38
+ VERSION: string;
39
+ A2UI_VERSION: string;
40
+ A2UI_BASIC_CATALOG: string;
41
+ A2UI_MIME: string;
42
+ A2UI_SCHEME: string;
43
+ };
44
+ export default PrefabA2UI;
45
+ export { emit, envelope };
46
+ //# sourceMappingURL=browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/a2ui/browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAY,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAG1C,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,eAAe,EACrB,MAAM,YAAY,CAAA;AAEnB;;;;;;GAMG;AACH,iBAAS,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,cAAc,CAKtE;AAED,gFAAgF;AAChF,iBAAS,QAAQ,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,eAAe,CAE1D;AAED,QAAA,MAAM,UAAU;;;IAGd,mEAAmE;;;;;;;CAOpE,CAAA;AAED,eAAe,UAAU,CAAA;AACzB,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAA"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Browser entry for the A2UI emitter — `dist/a2ui.min.js`.
3
+ *
4
+ * Bundled separately from `renderer.min.js` on purpose. The renderer is what
5
+ * every `$prefab` page loads, and emitting A2UI is something almost none of
6
+ * them do, so folding the emitter in would tax every consumer for a feature
7
+ * they do not use. Keeping it apart also means the two can be loaded
8
+ * independently: a tool that only translates payloads needs no renderer at all.
9
+ *
10
+ * The emitter takes wire JSON and returns wire JSON, so nothing here needs the
11
+ * component API. That is what keeps the bundle small.
12
+ *
13
+ * ```html
14
+ * <script src="https://cdn.jsdelivr.net/npm/@maxhealth.tech/prefab/dist/a2ui.min.js"></script>
15
+ * <script>
16
+ * const { messages, diagnostics } = PrefabA2UI.emit(wireJson)
17
+ * </script>
18
+ * ```
19
+ */
20
+ import { emitA2UI } from './emit.js';
21
+ import { mappedTypes } from './catalog.js';
22
+ import { VERSION } from '../core/version.js';
23
+ import { A2UI_BASIC_CATALOG, A2UI_MIME, A2UI_SCHEME, A2UI_VERSION, } from './types.js';
24
+ /**
25
+ * Emit A2UI from a `$prefab` payload.
26
+ *
27
+ * Accepts `unknown` because the caller is usually handing over parsed editor
28
+ * text or a tool result, neither of which is typed. A payload without a `view`
29
+ * is rejected here rather than producing an empty surface further downstream.
30
+ */
31
+ function emit(wire, options) {
32
+ if (wire == null || typeof wire !== 'object' || !('view' in wire)) {
33
+ throw new TypeError('PrefabA2UI.emit: expected a $prefab payload with a "view"');
34
+ }
35
+ return emitA2UI(wire, options);
36
+ }
37
+ /** Wrap messages in the list envelope, for transports needing a JSON object. */
38
+ function envelope(messages) {
39
+ return { messages };
40
+ }
41
+ const PrefabA2UI = {
42
+ emit,
43
+ envelope,
44
+ /** Every prefab component type with a first-class A2UI mapping. */
45
+ mappedTypes,
46
+ VERSION,
47
+ A2UI_VERSION,
48
+ A2UI_BASIC_CATALOG,
49
+ A2UI_MIME,
50
+ A2UI_SCHEME,
51
+ };
52
+ export default PrefabA2UI;
53
+ export { emit, envelope };
54
+ if (typeof window !== 'undefined') {
55
+ ;
56
+ window.PrefabA2UI = PrefabA2UI;
57
+ }
58
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/a2ui/browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,QAAQ,EAA6C,MAAM,WAAW,CAAA;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AAE5C,OAAO,EACL,kBAAkB,EAClB,SAAS,EACT,WAAW,EACX,YAAY,GAGb,MAAM,YAAY,CAAA;AAEnB;;;;;;GAMG;AACH,SAAS,IAAI,CAAC,IAAa,EAAE,OAAyB;IACpD,IAAI,IAAI,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,IAAI,IAAI,CAAC,EAAE,CAAC;QAClE,MAAM,IAAI,SAAS,CAAC,2DAA2D,CAAC,CAAA;IAClF,CAAC;IACD,OAAO,QAAQ,CAAC,IAAwB,EAAE,OAAO,CAAC,CAAA;AACpD,CAAC;AAED,gFAAgF;AAChF,SAAS,QAAQ,CAAC,QAAuB;IACvC,OAAO,EAAE,QAAQ,EAAE,CAAA;AACrB,CAAC;AAED,MAAM,UAAU,GAAG;IACjB,IAAI;IACJ,QAAQ;IACR,mEAAmE;IACnE,WAAW;IACX,OAAO;IACP,YAAY;IACZ,kBAAkB;IAClB,SAAS;IACT,WAAW;CACZ,CAAA;AAED,eAAe,UAAU,CAAA;AACzB,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAA;AAEzB,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;IAClC,CAAC;IAAC,MAA6C,CAAC,UAAU,GAAG,UAAU,CAAA;AACzE,CAAC"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * prefab component type → A2UI Basic catalog mapper registry.
3
+ *
4
+ * prefab ships 115+ components; the A2UI Basic catalog defines 18. The gap is
5
+ * closed by degradation rather than by refusing to emit: a `Badge` becomes
6
+ * `Text`, an `Alert` becomes a `Card` wrapping its body, an `H2` becomes `Text`
7
+ * carrying a Markdown `##` prefix (which the Basic catalog's `Text` explicitly
8
+ * supports). Every degradation is recorded as a diagnostic, so what changed is
9
+ * visible at emit time instead of in someone else's renderer.
10
+ *
11
+ * A mapper returns the A2UI properties for one node without its `id`; the
12
+ * emitter allocates ids and owns the adjacency list. Mappers that expand one
13
+ * prefab node into several A2UI components (`Metric`, the table family in
14
+ * `./table.ts`) push the extras through `ctx.push` and return the root of the
15
+ * expansion.
16
+ */
17
+ import type { ComponentJSON } from '../core/component.js';
18
+ import type { A2uiComponentProps, A2uiAction, A2uiDiagnosticKind } from './types.js';
19
+ /** A2UI properties for one component, before the emitter assigns its id. */
20
+ export type A2uiProps = A2uiComponentProps;
21
+ /** Services the emitter lends to a mapper. */
22
+ export interface EmitContext {
23
+ /** Emit one child subtree, returning its id, or `undefined` if it was dropped. */
24
+ child(node: ComponentJSON): string | undefined;
25
+ /** Emit several child subtrees, returning the ids that survived. */
26
+ children(nodes: unknown): string[];
27
+ /** Emit `nodes` as a single child, wrapping them in a Column when there are several. */
28
+ single(nodes: unknown): string | undefined;
29
+ /** Add a component the mapper synthesized, returning its id. */
30
+ push(props: A2uiProps): string;
31
+ /** Record a translation loss. */
32
+ note(kind: A2uiDiagnosticKind, subject: string, detail: string): void;
33
+ /** Convert a serialized prefab action into an A2UI action. */
34
+ action(value: unknown, subject: string): A2uiAction | undefined;
35
+ /** Seed a literal value into the surface data model, returning its JSON Pointer. */
36
+ bindData(key: string, value: unknown): string;
37
+ }
38
+ export type Mapper = (node: ComponentJSON, ctx: EmitContext) => A2uiProps | undefined;
39
+ export declare function mapperFor(type: string): Mapper | undefined;
40
+ export declare function isConsumedByParent(type: string): boolean;
41
+ /** Every prefab type with a first-class mapping, for docs and tests. */
42
+ export declare function mappedTypes(): string[];
43
+ /**
44
+ * Last-resort mapping for a type the registry does not name.
45
+ *
46
+ * A node with children becomes a Column, a node with text becomes a Text, and
47
+ * anything else is dropped. This is what keeps a tree built from unmapped
48
+ * components emitting something coherent instead of failing outright.
49
+ */
50
+ export declare function fallbackMapper(node: ComponentJSON, ctx: EmitContext): A2uiProps | undefined;
51
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/a2ui/catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAKpF,4EAA4E;AAC5E,MAAM,MAAM,SAAS,GAAG,kBAAkB,CAAA;AAE1C,8CAA8C;AAC9C,MAAM,WAAW,WAAW;IAC1B,kFAAkF;IAClF,KAAK,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,CAAA;IAC9C,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAAA;IAClC,wFAAwF;IACxF,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;IAC1C,gEAAgE;IAChE,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAAA;IAC9B,iCAAiC;IACjC,IAAI,CAAC,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrE,8DAA8D;IAC9D,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAA;IAC/D,oFAAoF;IACpF,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAAA;CAC9C;AAED,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,WAAW,KAAK,SAAS,GAAG,SAAS,CAAA;AAybrF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE1D;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD;AAED,wEAAwE;AACxE,wBAAgB,WAAW,IAAI,MAAM,EAAE,CAEtC;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,WAAW,GAAG,SAAS,GAAG,SAAS,CAsB3F"}