@maxhealth.tech/prefab 0.3.13 → 0.3.15

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 (89) hide show
  1. package/CHANGELOG.md +762 -739
  2. package/dist/a2ui.min.js +1 -1
  3. package/dist/auto/chart.d.ts +1 -1
  4. package/dist/auto/chart.js +1 -1
  5. package/dist/auto/comparison.d.ts +1 -1
  6. package/dist/auto/comparison.js +1 -1
  7. package/dist/auto/form.d.ts +1 -1
  8. package/dist/auto/form.js +1 -1
  9. package/dist/auto/index.d.ts +1 -1
  10. package/dist/auto/index.js +1 -1
  11. package/dist/auto/metrics.d.ts +1 -1
  12. package/dist/auto/metrics.js +1 -1
  13. package/dist/auto/progress.d.ts +1 -1
  14. package/dist/auto/progress.js +1 -1
  15. package/dist/auto/timeline.d.ts +1 -1
  16. package/dist/auto/timeline.js +1 -1
  17. package/dist/core/guards.d.ts +8 -0
  18. package/dist/core/guards.d.ts.map +1 -0
  19. package/dist/core/guards.js +29 -0
  20. package/dist/core/guards.js.map +1 -0
  21. package/dist/core/version.d.ts +1 -1
  22. package/dist/core/version.js +1 -1
  23. package/dist/renderer/actions.d.ts +14 -2
  24. package/dist/renderer/actions.d.ts.map +1 -1
  25. package/dist/renderer/actions.js +134 -164
  26. package/dist/renderer/actions.js.map +1 -1
  27. package/dist/renderer/bridge.d.ts.map +1 -1
  28. package/dist/renderer/bridge.js +9 -2
  29. package/dist/renderer/bridge.js.map +1 -1
  30. package/dist/renderer/components/charts/cartesian.d.ts +4 -0
  31. package/dist/renderer/components/charts/cartesian.d.ts.map +1 -0
  32. package/dist/renderer/components/charts/cartesian.js +189 -0
  33. package/dist/renderer/components/charts/cartesian.js.map +1 -0
  34. package/dist/renderer/components/{chart-helpers.d.ts → charts/helpers.d.ts} +5 -6
  35. package/dist/renderer/components/charts/helpers.d.ts.map +1 -0
  36. package/dist/renderer/components/{chart-helpers.js → charts/helpers.js} +8 -8
  37. package/dist/renderer/components/charts/helpers.js.map +1 -0
  38. package/dist/renderer/components/charts/index.d.ts +2 -0
  39. package/dist/renderer/components/charts/index.d.ts.map +1 -0
  40. package/dist/renderer/components/charts/index.js +15 -0
  41. package/dist/renderer/components/charts/index.js.map +1 -0
  42. package/dist/renderer/components/charts/polar.d.ts +5 -0
  43. package/dist/renderer/components/charts/polar.d.ts.map +1 -0
  44. package/dist/renderer/components/charts/polar.js +189 -0
  45. package/dist/renderer/components/charts/polar.js.map +1 -0
  46. package/dist/renderer/components/charts/props.d.ts +30 -0
  47. package/dist/renderer/components/charts/props.d.ts.map +1 -0
  48. package/dist/renderer/components/charts/props.js +67 -0
  49. package/dist/renderer/components/charts/props.js.map +1 -0
  50. package/dist/renderer/components/charts/scatter.d.ts +4 -0
  51. package/dist/renderer/components/charts/scatter.d.ts.map +1 -0
  52. package/dist/renderer/components/charts/scatter.js +107 -0
  53. package/dist/renderer/components/charts/scatter.js.map +1 -0
  54. package/dist/renderer/components/{chart-tooltip.d.ts → charts/tooltip.d.ts} +3 -4
  55. package/dist/renderer/components/charts/tooltip.d.ts.map +1 -0
  56. package/dist/renderer/components/{chart-tooltip.js → charts/tooltip.js} +11 -5
  57. package/dist/renderer/components/charts/tooltip.js.map +1 -0
  58. package/dist/renderer/components/choice.d.ts.map +1 -1
  59. package/dist/renderer/components/choice.js.map +1 -1
  60. package/dist/renderer/components/data.d.ts.map +1 -1
  61. package/dist/renderer/components/data.js.map +1 -1
  62. package/dist/renderer/components/form.d.ts.map +1 -1
  63. package/dist/renderer/components/form.js.map +1 -1
  64. package/dist/renderer/components/index.js +1 -1
  65. package/dist/renderer/components/index.js.map +1 -1
  66. package/dist/renderer/components/layout.d.ts.map +1 -1
  67. package/dist/renderer/components/layout.js +39 -24
  68. package/dist/renderer/components/layout.js.map +1 -1
  69. package/dist/renderer/components/media.d.ts.map +1 -1
  70. package/dist/renderer/components/media.js.map +1 -1
  71. package/dist/renderer/engine.js.map +1 -1
  72. package/dist/renderer/index.d.ts +1 -0
  73. package/dist/renderer/index.d.ts.map +1 -1
  74. package/dist/renderer/index.js.map +1 -1
  75. package/dist/renderer/node-guard.d.ts +3 -0
  76. package/dist/renderer/node-guard.d.ts.map +1 -0
  77. package/dist/renderer/node-guard.js +16 -0
  78. package/dist/renderer/node-guard.js.map +1 -0
  79. package/dist/renderer.auto.min.js +10 -10
  80. package/dist/renderer.min.js +10 -10
  81. package/package.json +2 -2
  82. package/dist/renderer/components/chart-helpers.d.ts.map +0 -1
  83. package/dist/renderer/components/chart-helpers.js.map +0 -1
  84. package/dist/renderer/components/chart-tooltip.d.ts.map +0 -1
  85. package/dist/renderer/components/chart-tooltip.js.map +0 -1
  86. package/dist/renderer/components/charts.d.ts +0 -9
  87. package/dist/renderer/components/charts.d.ts.map +0 -1
  88. package/dist/renderer/components/charts.js +0 -593
  89. package/dist/renderer/components/charts.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,747 +1,770 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
5
  ## [Unreleased]
6
6
 
7
7
  <!-- Add new entries directly below. Keep this line: it makes a release merge conflict rather than file them under a published version. -->
8
8
 
9
+ ## [0.3.15] — 2026-10-23
10
+
11
+ ### Fixed
12
+
13
+ - **A Grid with responsive `columns` renders.** `Grid({ columns: { sm: 1, md: 2, lg: 3 } })` produced `repeat([object Object], 1fr)`, which the browser drops. A breakpoint map now renders at its widest breakpoint, since an inline style has no media query to hold the rest.
14
+ - **`setInterval` without an `intervalMs` no longer spins.** The missing value became `NaN`, which bypassed the 100 ms floor and fired the tick as fast as the event loop allowed. It now falls back to the floor, and an interval with no `onTick` is not started.
15
+ - **Charts survive malformed `data`.** A non-array value or a `null` row threw out of the render; non-array data now shows the empty state and non-object rows are skipped.
16
+ - **`toolCall` / `callHandler` without a tool name runs `onError`.** It used to call the transport with `undefined` as the tool name.
17
+
18
+ ### Internal
19
+
20
+ - **The renderer's chart, layout and action modules read wire values through guards instead of `as` casts** (154 assertions removed). `src/core/guards.ts` holds the narrowing readers and `src/renderer/node-guard.ts` the `ComponentNode` guard. `dispatchActions` takes the raw wire value and parses it, so the `as ActionJSON | ActionJSON[]` at every call site is gone too. The chart renderers moved into `src/renderer/components/charts/` (cartesian, polar, scatter, typed props), and ESLint's `consistent-type-assertions: never` now guards those files.
21
+
22
+ ## [0.3.14] — 2026-10-06
23
+
24
+ ### Added
25
+
26
+ - **A view can tell the model what happened after the tool returned.** The model only reads the tool result, so anything a view learned later, such as an image request that failed with a 404, never reached it, and it went on describing a success. `McpTransport.updateModelContext({ content, structuredContent })` sends MCP Apps `ui/update-model-context` with the `content` text the model reads, and custom components reach it as `ctx.transport.updateModelContext`. Over the `prefab:*` protocol the same update travels as `prefab:update-context`. `Bridge.updateContext` still sends only `structuredContent`, which a host keeps but the model does not read as text.
27
+
28
+ ### Internal
29
+
30
+ - **The JSON-RPC `sendMessage` no longer casts its result.** It awaited nothing and asserted `Promise<void>` over the request's `Promise<unknown>`; it now awaits the request.
31
+
9
32
  ## [0.3.13] — 2026-08-29
10
33
 
11
- ### Added
12
-
13
- - **`fieldsFromJsonSchema()` — a form derived from the schema that validates it.** `autoForm` took a field list, and writing that list restated something the server had already declared: an MCP tool's `inputSchema`, a REST route's request body, an OpenAPI operation. All three are JSON Schema, and this is the inverse of `formSchema()`, so one declaration now serves both delivery paths — the schema draws the prefab form, and the same fields derive the elicitation schema for a host with no UI. Formats map onto controls a browser actually has (`date-time` renders as `datetime-local`, `integer` as `number`, since neither is an input type), an enum becomes a Select and an array of enums a multi-select, and bounds carry across. It emits only what a flat form can honestly ask for: a nested object, an array of objects, or a `readOnly` property is skipped rather than rendered as a control that cannot round-trip. Unions are resolved rather than dropped, which is what makes it usable against a real server — a nullable field is one optional control, and Elysia's coercion union (`anyOf: [{ type: 'string', format: 'integer' }, { type: 'integer' }]`, so a query string can carry a number) is one number field written twice.
14
-
15
- ### Internal
16
-
34
+ ### Added
35
+
36
+ - **`fieldsFromJsonSchema()` — a form derived from the schema that validates it.** `autoForm` took a field list, and writing that list restated something the server had already declared: an MCP tool's `inputSchema`, a REST route's request body, an OpenAPI operation. All three are JSON Schema, and this is the inverse of `formSchema()`, so one declaration now serves both delivery paths — the schema draws the prefab form, and the same fields derive the elicitation schema for a host with no UI. Formats map onto controls a browser actually has (`date-time` renders as `datetime-local`, `integer` as `number`, since neither is an input type), an enum becomes a Select and an array of enums a multi-select, and bounds carry across. It emits only what a flat form can honestly ask for: a nested object, an array of objects, or a `readOnly` property is skipped rather than rendered as a control that cannot round-trip. Unions are resolved rather than dropped, which is what makes it usable against a real server — a nullable field is one optional control, and Elysia's coercion union (`anyOf: [{ type: 'string', format: 'integer' }, { type: 'integer' }]`, so a query string can carry a number) is one number field written twice.
37
+
38
+ ### Internal
39
+
17
40
  - **Key humanization had four copies, three of them byte-identical.** `autoDetail`/`autoTable`, `autoForm` and `autoComparison` each carried their own, and the schema converter would have made a fifth. They are one function plus one optional rule — the `id` → `ID` fix that only the table and detail paths applied — and now live in `src/core/humanize.ts` as `humanizeKey` and `humanizeColumnKey`. No label changes: each caller keeps the exact variant it had.
18
41
 
19
- ## [0.3.12] — 2026-08-26
20
-
21
- ### Added
22
-
23
- - **`required` is a property of every stateful control, not of the text input.** It lived on `InputProps`, so only an `Input` could state it. Select, RadioGroup and Combobox are containers that call `statefulProps` rather than inheriting it, which is the same seam `label` went missing through in 0.3.9 — and `autoForm` turns every field carrying `options` into a Select, so the fields with a fixed choice set were exactly the ones whose constraint was dropped. It moves to `StatefulProps`, is marked natively where the element supports it and with `aria-required` everywhere, and the A2UI emitter's `ChoicePicker` checks — added in 0.3.11 against a `node.required` no source component could carry — now actually fire.
24
- - **`Select({ multiple })`, so a multi-value choice field exists on the UI path too.** `AutoFormField.multiple` was honoured only when deriving the elicitation schema: the same field definition asked a schema host for an array and a rendering host for one value. A multi-select submits an array under its name even when one option is chosen, since the field asked for a list either way, and it maps onto A2UI's `multipleSelection` variant — which the emitter defined and nothing could reach.
25
- - **`Select({ placeholder })` reaches the wire.** The renderer has always drawn a placeholder option; `Select` serialized `statefulProps` alone, so the prop was accepted and discarded.
26
-
27
- ### Internal
28
-
29
- - **Choice control renderers move to `renderer/components/choice.ts`.** `form.ts` had grown past the point where Select, Radio, Combobox and ChoiceCard could be read as a group, and the required marking above touches all four. The shared label wrapper, required marker and container helpers stay in `form.ts` and are imported.
30
-
31
- ## [0.3.11] — 2026-08-25
32
-
33
- ### Added
34
-
35
- - **Formatting pipes map onto A2UI catalog functions.** A2UI has no expression language, which is why pipes were reported as unbindable, but its catalog has the functions the common ones correspond to: `currency` to `formatCurrency`, `number` and `round` to `formatNumber`, `date`/`time`/`datetime` to `formatDate`, and `pluralize` to `pluralize`. Seven of prefab's twenty-two built-ins now cross over; the rest transform data rather than format it and stay reported, as does a chained pipe, since no single catalog function is two pipes. The date pipes are the one inexact mapping: prefab renders in the reader's locale and `formatDate` requires an explicit TR35 pattern, so one is chosen and a diagnostic says which.
36
- - **Form validation survives the crossing.** Every A2UI input is `Checkable`, which is the same job prefab's `required` and `inputType` do, and neither was emitted — so a form crossed over with a required field that was no longer required. `TextField`, `CheckBox` and `ChoicePicker` now carry `checks`. No `message` is emitted with them: the rule already says which check failed, and the renderer is better placed to word and localize that. `numeric` is deliberately omitted for number fields, because the catalog requires it to carry a `min` or a `max` and prefab's number inputs carry no range; emitting a bare one fails validation, which is how the omission was found.
37
-
38
- - **`ForEach` maps onto A2UI's child template.** A2UI has iteration, the same construct `DataTable` already used: one template component instantiated per item, with paths inside it resolving relative to the current row. The emitter simply never wired it, so every loop was dropped. `$item` now resolves to a relative path and `$index` to the `@index` system function.
39
- - **Mixed literal and template text is interpolated through `formatString`.** A2UI's `formatString` takes a single template string with JSON Pointers embedded as `${/path}`, so `Score: {{ score }}` crosses over intact. The previous code claimed the function "would need an argument list this function has no way to name", which was simply wrong about its signature. A string is interpolated only if every value in it binds; one unbindable expression makes the whole string unbindable, since interpolating half of it would change what the text says.
40
- - **`Define` / `Use` / `Slot` are resolved at emit time by inlining the definition.** A2UI needs no named-template concept when the agent can expand the tree before sending it. A `Use`'s `overrides` are seeded into the data model and brought into scope by name, so a `{{ title }}` inside the body reads that call's value rather than the model root. Definitions are collected in a pre-pass, since prefab imposes no ordering between a `Use` and its `Define`, and a definition that uses itself reports a cycle rather than recursing until the stack gives out.
41
- - `CardTitle`, `CardDescription` and `Tooltip` have first-class text mappings. They rendered correctly through the generic fallback but reported a degradation for what is an exact mapping, which accounted for 36 of the 160 diagnostics across the shipped examples.
42
-
43
- ### Fixed
44
-
45
- - **The release merge could file unreleased entries under the version that just shipped.** Promoting `[Unreleased]` on `main` and adding new entries on `dev` are insertions at the same point, so git's line-based merge orders them heading-first and attributes unshipped work to a published version. It does so without conflicting, which is why the release workflow's best-effort sync never had anything to report, and it has now happened three times. `scripts/changelog-release.ts` keeps an anchor line under `[Unreleased]` so the two insertions land at the same offset and collide, and `scripts/check-changelog.ts` verifies the outcome independently in CI: every version section must stay byte-identical to `main`'s. The anchor is a guard, the check is the guarantee.
46
- - **A dropped node left its already-emitted children orphaned, producing a payload the renderer rejects.** A mapper reads its children before deciding whether it can map at all: `Dialog` emits its body, then finds it cannot build a trigger, returns nothing, and the body stays in the adjacency list with no component pointing at it. `interactive-showcase.json` — one of the shipped examples — emitted an unreachable `Row` on v0.3.10 for exactly this reason. Emission is now transactional: a node that drops rolls back every component and data-model key added while mapping it. The per-type registry tests could not catch this, because each mapper is correct in isolation and the fault is in the interaction; `test/a2ui-control.test.ts` now runs every shipped example through the emitter, which does catch it.
47
- - **`Define` emitted its template body as visible content.** It fell to the generic fallback and was flattened to a `Column`, so a definition rendered where it was declared. The renderer draws nothing at the point of definition and now neither does the emitter.
48
- - **The emitter knew only one of the two spellings for a tool-call action.** The renderer accepts `toolCall` and `callTool`, and the shipped examples use the latter, so every hand-written tool call was silently downgraded to a generic agent event and had its arguments dropped for not being scalar.
49
- - **A string containing two templates was mis-read as one.** The sole-template pattern is lazy but still spanned `}} … {{`, so `{{ a }} of {{ b }}` matched as a single template whose body was `a }} of {{ b`, failed to parse as a path, and was reported unbindable instead of being interpolated.
50
- - **A `Modal` whose trigger could not map lost the whole dialog.** prefab's dialog trigger is usually a `Button` with no `onClick`, because opening the dialog is implicit, and the `Button` mapper rightly refuses one with no action. A2UI's `Modal` accepts any component as its trigger, so it now falls back to the trigger's label as `Text`.
51
-
52
- Across the twenty shipped examples this takes the emitter from 160 diagnostics and one invalid payload to 95 diagnostics and none. What remains is largely honest: charts, `Metric` trend styling, table controls, and the conditionals.
53
-
54
- ## [0.3.10] — 2026-08-24
55
-
56
- ### Added
57
-
58
- - **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.
59
- - **`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.
60
- - **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.
61
- - **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.
62
- - **`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.
63
- - `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.
64
- - `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.
65
-
66
- ### Changed
67
-
68
- - **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.
69
-
70
- ### Fixed
71
-
72
- - **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.
73
- - **`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.
74
-
75
- ### Internal
76
-
77
- - **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.
78
- - Removed `hasTemplate` and `isTemplatedList`, both exported with no caller anywhere.
79
- - 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`.
80
-
81
- ## [0.3.9] — 2026-08-07
82
-
83
- ### Added
84
-
85
- - **`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.
86
- - **`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.
87
- - **`Table({ striped })`** — shades alternate body rows via a `pf-table-striped` modifier and a `:nth-child(even)` rule, rather than per-table styling.
88
- - **`Sparkline({ width, height })`** — the renderer hardcoded 120×32; those are now the defaults.
89
-
90
- - **`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.
91
-
92
- All five were documented in `docs/reference/components.md` with props-table entries before they existed, and were found by `bun run check:docs`.
93
-
94
- ### Docs
95
-
96
- - **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.
97
-
98
- ## [0.3.8] — 2026-08-05
99
-
100
- ### Fixed
101
-
102
- - **Strict wire-format validation rejected four renderable component types.** `validateWireFormat(data, { strict: true })` checked `type` against a hand-written list in `src/core/validate.ts` that had fallen behind the renderer's registry: `Condition` (emitted by `If` / `Elif` / `Else`), `Detail`, `MasterDetail` and `PdfViewer` were all registered and renderable, but reported as `Unknown component type`. Any UI using a conditional therefore failed strict validation, as did two of the shipped examples. The list is no longer written by hand: `src/core/component-types.ts` is generated from the registry (`bun run gen:types`, also run by the build), and the validator consumes it. The generated module is import-free, so the validator still works server-side where the renderer is never loaded. Non-strict validation was unaffected, since unknown types are only an error under `strict`.
103
- - **The shipped example UIs were never validated against the wire format.** `docs/public/examples/*.json` is the single source for the demo, the playground and the generated skill assets, and nothing pointed the package's own `validateWireFormat` at it, so an example could ship a key the renderer ignores and simply render nothing. That had already happened once (0.3.5 repaired examples declaring conditional branches under a non-existent `then` key). `test/doc-examples.test.ts` now validates all 20 in strict mode, which is what surfaced the validator drift above.
104
-
105
- - **The display helpers could not be returned to an SDK tool handler without a cast.** `PrefabWireFormat` and `PrefabUpdateWire` were interfaces, so the `structuredContent` on every result from `display()`, `display_form()`, `display_update()`, `display_error()` and `display_success()` failed against the SDK's `{ [x: string]: unknown }` with "Index signature for type 'string' is missing". This is the same by-design TypeScript behaviour fixed for `src/mcp/types.ts` in 0.3.7 ([microsoft/TypeScript#15300](https://github.com/microsoft/TypeScript/issues/15300)); these two types were missed because they live outside that file, in `src/app.ts` and `src/mcp/display.ts`. Both are aliases now. Consumers had no way to fix this on their side, since the type came from here, and the only escape was an assertion. prefab carried the same workaround internally: `PrefabApp.toMcpResult()` used `wire as unknown as Record<string, unknown>`, a double assertion that only existed because of this, and is gone.
106
- - **The 0.3.7 interop guard had a blind spot that let the above through.** `test/mcp-types.test.ts` modelled an SDK result as a flat `Record<string, unknown>`, which `McpToolResult` satisfies through its own explicit index signature, so the *value* of `structuredContent` was never checked against the narrower field type the SDK declares. The guard stayed green while every display helper was unassignable. It now also asserts against a stand-in that keeps the field shape, covering all five helpers and the typed-handler return position.
107
-
108
- ### Internal
109
-
110
- - **`registeredComponentTypes()` on the renderer** returns every type the registry can render. The registry is the authority on that set, and nothing could read it, which is why a second copy existed to drift from. `test/component-types.test.ts` uses it to fail the build when the generated list and the registry disagree, in either direction, so a stale committed generated file cannot be merged.
111
- - **One `escapeHtml` in `src/core/escape.ts` replaces three private copies** in `src/app.ts`, `src/mcp/resource.ts` and `src/renderer/components/typography.ts`, plus an `escapeAttr` in the first of those. The four escaped three different character sets (`& < > "`, `& < >`, and `& "`). Each was correct for its own context, and `resource.ts` was deliberately splitting text-content from attribute escaping, so this was not a live vulnerability, but nothing enforced any of it. The shared function covers `& < > " '`, which is safe in element text and in attribute values of either quote style, so callers no longer reason about context. Note for anyone narrowing it later: `renderMarkdownToHtml` escapes each line *before* parsing link and image URLs, so the up-front escape is what neutralizes `"` in `href`/`src`; a text-content-only escape there would open an attribute break-out. `test/escape.test.ts` pins the character set.
112
- - Scoped the `consistent-type-definitions` exception per-declaration for the two wire types rather than per-file as `src/mcp/types.ts` does. `src/app.ts` and `src/mcp/display.ts` are mostly domain types that should stay interfaces, so flipping either file would invert the default for the wrong declarations. Moving the two types into `src/mcp/types.ts` was considered and rejected: that file has no imports by design, and `PrefabWireFormat` needs `ComponentJSON`, `ActionJSON`, `ColorMode` and `LayoutHints`, which would also make it cyclic with `src/app.ts`.
113
-
114
- - **Staging CI is now the org pattern: push to `dev` keeps a promote PR open, merging it releases.** `auto-pr.yml` creates or refreshes a standing `dev → main` PR on every push to `dev`, and `release.yml` gained an `on: push: branches: [main]` trigger, so shipping is a one-click merge instead of a hand-written PR plus a manual `workflow_dispatch`. Copied from the sibling implementation in `Max-Health-Inc/mcp-http` rather than calling `Max-Health-Inc/.github`'s reusable `create-pr.yml`: that repo is private and this one is public, and GitHub does not let a public repo consume a reusable workflow from a private one. Tests are not duplicated into the PR workflow, since `ci.yml` already runs on the same push and reports onto the PR.
115
- - **A merge that documents nothing now skips the release instead of failing it.** `release.yml` splits into a `gate` job and a `release` job; the gate uses the new `--check` flag on `scripts/changelog-release.ts`, which reports `promoted` / `present` / `missing` on stdout, writes nothing and always exits 0. A README or CI-only merge to `main` therefore leaves `main` green rather than red, while an explicit `workflow_dispatch` with an empty `[Unreleased]` still fails loudly, because asking for a release and having nothing to ship is an error. `--check` was added to `mcp-http`'s port of this script and is ported back here, where the script originated. Note the plain invocation was never a dry run: it rewrites `CHANGELOG.md` in place.
116
- - **The release now merges itself back into `dev`.** The release commit promotes `[Unreleased]` to a version heading on `main`; leaving `dev` behind means its still-open `[Unreleased]` and `main`'s new heading differ only in where the heading sits, so git's line-based merge files `dev`'s newer entries under the *older* release heading. That silently attributes unshipped work to a published version and empties `[Unreleased]`, so the next merge skips its release. It happened between v0.3.6 and v0.3.7 and had to be repaired by hand. The sync step is best-effort and never fails a completed release; a conflict is reported in the run summary instead.
117
- - Bumped `softprops/action-gh-release` to v3, which clears the Node 20 deprecation warning the v0.3.7 release run emitted.
118
- - Bumped `actions/checkout` to v7 (six call sites across `auto-pr`, `ci`, `docs`, `publish` and `release`) and `actions/setup-node` to v7. Both were two majors behind. `setup-node` v7 drops its dummy `NODE_AUTH_TOKEN` export, which `publish.yml` never relied on: it publishes through OIDC trusted publishing (`id-token: write` plus `npx npm@latest publish --provenance`), the pattern that release documents. `oven-sh/setup-bun@v2` and `peaceiris/actions-gh-pages@v4` are on their current majors and unchanged.
119
- - **Added `.github/dependabot.yml`, targeting `dev`.** Now that `release.yml` runs on `push: branches: [main]`, a Dependabot PR merged to `main` would cut a release for a routine bump, so the updates land on `dev` and ship through the standing promotion PR. Grouped per ecosystem, one PR a week each. Adapted from the sibling config in `mcp-http`, with a correction: TypeScript major bumps are ignored because `typescript-eslint@8.65.0` declares `peer typescript: >=4.8.4 <6.1.0` (verified against the installed tree, on `typescript` 6.0.3), and a grouped PR that fails every week would block the other dev-dependency updates. Worth knowing that the cap is `<6.1.0`, so even a minor bump to 6.1 falls outside the supported range.
120
-
121
- ### Docs
122
-
123
- - **The npm package page now links to `https://maxhealth.tech/prefab` as its homepage.** `package.json` carried no `homepage`, so npm fell back to `repository` + `#readme` and sent every visitor to the raw README on GitHub instead of the documentation site. `bugs.url` is set alongside it, which npm was likewise deriving from `repository`. Both fields are read from the published tarball, so the listing only changes on the next release.
124
- - Removed a stale README blockquote that restated the superset relationship six lines below the tagline already stating it, and pinned it to upstream `v0.19.1`. That pin predated 0.3.0's catch-up to upstream 0.20.x and contradicted the wire-format reference. The chart-formatting caveat it carried still lives in the Chart Formatting section, next to the props it describes.
125
-
126
- ## [0.3.7] — 2026-08-01
127
-
128
- ### Fixed
129
-
130
- - **The MCP protocol types could not be returned to an SDK handler without a cast.** Every shape in `src/mcp/types.ts` was an `interface`, and the SDK's result types are passthrough (`{ [x: string]: unknown }`). TypeScript grants an implicit index signature only to aliases of object types, never to interfaces, whose key set is not final because declaration merging can reopen them ([microsoft/TypeScript#15300](https://github.com/microsoft/TypeScript/issues/15300), closed as by-design), so returning a `McpResourceReadResult` from a `resources/read` handler failed with "Index signature for type 'string' is missing" — the one thing the type exists for. They are all type aliases now. `McpToolResult` was unaffected because it carries an explicit index signature. Verified against `@modelcontextprotocol/server` 2.0.0 rather than a stand-in, and guarded by `test/mcp-types.test.ts`, which fails `typecheck` (not at runtime) if any of them reverts to an interface.
131
- - `McpTextResourceContents`, `McpBlobResourceContents`, `McpImageContent` and `McpResourceContent` are now exported from the package root, not just from `/mcp`. `McpResourceReadResult`'s default type parameter references the contents types, so naming it from a root import was impossible.
132
-
133
- ### Internal
134
-
135
- - Scoped `consistent-type-definitions` to `'type'` for `src/mcp/types.ts` only. The repo default stays `'interface'`, which matches the prevailing TypeScript style and typescript-eslint's own recommendation; flipping all 151 interfaces in `src/` would be churn for a problem that only bites where a type must satisfy a foreign passthrough shape. Scoping it means the linter enforces the correct form for anything added to that file rather than pushing the next author into the bug and then into a per-line disable.
136
-
137
- ## [0.3.6] — 2026-07-31
138
-
139
- ### Fixed
140
-
141
- - **A scheme set on the prefab container, rather than on `<html>`, now flips prefab's own tokens too.** `setThemeAttrs` themes the mount container (`applyMode` sets both it and `documentElement`, and `createThemeToggle({ syncDocument: false })` sets *only* the container), and the compiled wire theme has always matched at any depth via `.dark, [data-theme="dark"]`. But every scheme block in `prefab.css` was gated on `:root[…]`, so the two sheets disagreed about depth: a container-scoped toggle applied the theme's dark values over prefab's *light* `--background` and `--foreground`, a half-flipped panel. The base sheet's explicit dark and light blocks now carry the same three selectors the compiled theme emits (`:root[data-theme=…]`, the bare attribute, and the `dark`/`light` class), so both flip on the same element. The `:root[…]` branch is retained, so root-level behaviour and its specificity tie against the `prefers-color-scheme` block are unchanged; only elements below the root gain the tokens they were already asking for. Two schemes on one page work as a consequence.
142
- - **Inline `Svg` authored without `xmlns` now renders.** `renderSvg` parses with `DOMParser` `image/svg+xml` (strict XML), where a missing `xmlns` produced namespace-less elements that rendered at 0×0. The renderer now injects `xmlns="http://www.w3.org/2000/svg"` when it's absent (`xmlns` is optional in HTML, so authors routinely omit it).
143
- - The MCP Apps `ui/initialize` handshake reported a stale hard-coded app version (`0.2`) in `appInfo`. It now uses the real package `VERSION`, so a host sees the actual prefab version. (This is the *app/package* version, distinct from the `0.3` wire-format version in `$prefab.version` and the `2026-01-26` MCP Apps protocol version.)
144
-
145
- - **Object values no longer render as `[object Object]`.** Every value that reaches a table cell, chart label, axis tick, tooltip title, heading, `{{ }}` interpolation or state-bound form input is typed `unknown` (it comes from JSON), and each site coerced it with a bare `String()`. Objects and arrays of objects therefore rendered as `[object Object]`. All of those sites now go through the new `stringifyValue` helper, which emits compact JSON for objects (honouring `toJSON()`), ISO strings for dates, and comma-joined members for arrays.
146
- - **Identity comparisons on object values no longer collide.** `DataTable` row-selection highlighting, the `find:` pipe's key map and `Collection.firstKey()`/`lastKey()` used the same `String()` coercion to build lookup keys, so *every* object value produced the identical key `[object Object]` and matched every other object. They use `stringifyValue` now, which is injective for distinct JSON values.
147
-
148
- - **Custom pipes now work in hosts whose CSP forbids eval.** Wire pipes arrive as function source and were always hydrated with `new Function()`, which throws in a sandboxed iframe or VS Code webview without `'unsafe-eval'`, so every custom pipe silently degraded to the raw value. A pipe that is already registered locally (by a companion script loaded through `rendererHtml({ scripts })`) is now kept instead of being re-evaluated, so pre-registering is all it takes. The eval failure is also reported once per mount with an actionable message, rather than once per pipe as an opaque `EvalError`.
149
- - **`registerViewerResource` emits the cache fields required by protocol revision 2026-07-28.** `resources/read` results extend `CacheableResult` (SEP-2549), and a result without `ttlMs`/`cacheScope` falls back to `{ ttlMs: 0, cacheScope: 'private' }`, meaning the viewer was re-fetched on every render. The viewer HTML is a pure function of the package version (the CDN base pins it), so it now ships `{ ttlMs: 86_400_000, cacheScope: 'public' }`, overridable via the new `cache` option and validated so an out-of-range value throws instead of being silently discarded.
150
- - **`registerViewerResource` prefers `registerResource()` over the retired `resource()` overload.** `resource()` is deprecated in SDK v1 and gone in `@modelcontextprotocol/server` v2; `resource()` is now only a fallback, and a server exposing neither raises a clear `TypeError`.
151
-
152
- ### Added
153
-
154
- - **`rendererHtml({ themeBridge: 'vscode' })` and the same option on `registerViewerResource`.** `prefab.css` resolves each token as MCP Apps host variable, then VS Code webview variable, then static default, so a host that defines the MCP Apps `--color-*` variables shadows the VS Code ones entirely and the viewer stops following the user's editor theme. The bridge emits a `<style>` block after `prefab.css` re-declaring the affected tokens with `--vscode-*` first and the MCP Apps layer dropped, keeping prefab's own fallbacks so nothing changes outside VS Code. Head order is the contract: base theme, bridge, then the caller's `stylesheets`. It covers `:root` and the `prefers-color-scheme: dark` block but deliberately not `:root[data-theme="dark"]`, which is the standalone dark palette (host theming cascades, the manual toggle does not). `VSCODE_BRIDGE` is the exported mapping table, and `test/theme-bridge.test.ts` cross-checks every entry against `prefab.css` so the bridge cannot drift from the theme it overrides.
155
- - `registerViewerResource` declares the `io.modelcontextprotocol/ui` extension capability (SEP-2133) on the server, since MCP Apps is an extension to the core protocol rather than part of it. Opt out with `declareCapability: false`. Registering on an already-connected server logs a warning instead of throwing, and still registers the resource. (This entry said `…/apps` while the constant held that value; both are corrected. `…/ui` is what the MCP Apps spec defines and what `ext-apps` exports as `EXTENSION_ID`, so the old value declared the capability under a key no host looks up. Never published, so there is nothing to migrate.)
156
- - `toolResult(payload)` builds the MCP tool-result envelope (`content[0].text` plus `structuredContent`) that the `display_*` helpers return, exported for handlers that already have wire JSON and were hand-rolling the same shape.
157
- - New MCP protocol types: `McpCacheScope`, `McpCacheHint`, `McpResourceReadResult`, `ResourceConfig`, `ResourceReadHandler`, and `MCP_APP_MIME` / `APPS_EXTENSION` / `DEFAULT_VIEWER_CACHE` constants.
158
-
159
- ### Internal
160
-
161
- - Tracked `brandc` 0.6.0, which stops `@property`-registering the scheme-dependent colour tokens so their `light-dark()` resolves against the element that reads it rather than once at `:root` (max-network/brandc#14). That only changes `toCss` output, which prefab does not consume — it compiles its own CSS from the wire `theme` — so the drift guard passes unchanged. Worth knowing anyway, because `setThemeAttrs` accepts any element: a host that themes a container rather than `documentElement` now gets the subtree themed correctly if it loads brandc's stylesheet.
162
- - Added `stringifyValue` (`src/core/stringify.ts`, exported from the package root) as the single value-to-string coercion for display and identity, replacing 34 ad-hoc `String(unknown)` call sites across 11 modules.
163
- - Bumped dev tooling: `eslint` 10.8.0, `typescript-eslint` 8.65.0, `happy-dom` 20.11.1, `@types/bun` 1.3.14, `jiti` 2.7.0, `typedoc` 0.28.20, `vitepress-plugin-llms` 1.13.4. The `typescript-eslint` bump is what surfaced the `String(unknown)` and `cacheScope` defects above, via `no-base-to-string` and `no-unnecessary-condition`.
164
- - Centralized `VERSION` and `PROTOCOL_VERSION` into `src/core/version.ts` (a zero-import single source of truth) so the lean renderer bundle can read the version without pulling in the builder. `release.yml` now bumps that file.
165
- - `structuredContent` on `McpToolResult` is now a type parameter carrying the payload type (SEP-2106 loosened the field to any JSON value), which removed the five `as unknown as Record<string, unknown>` casts in the display helpers and lets callers read the wire back without casting.
166
- - Split `src/mcp/display.ts` (606 lines) into `display.ts` (the builders), `resource.ts` (`ui://` registration, CSP meta, viewer HTML) and `result.ts` (the shared envelope). All five `display_*` builders constructed the same envelope inline; they now share one, so protocol-level result fields have a single home.
167
- - Confirmed `resultType` (SEP-2322) is not ours to set: `@modelcontextprotocol/server` treats it as a wire-only key, stamps it at its 2026-era encode seam and strips it before results reach consumers, which is why its public result types omit it. The one handler-authored case is `resultType: 'input_required'` for multi-round-trip results, which no display helper produces.
168
- - Tracked `brandc` 0.5.0, which declares the token contract explicitly instead of deriving it from one brand (max-network/brandc#13, filed from prefab's side while writing the guard below). The guard now iterates the exported `BRANDS` rather than naming brands, splits scalars from colours using `CONTRACT_SCALARS`, and adds two checks the new API makes possible: prefab reads no token that left the contract (`DEPRECATED_TOKENS`) and none of any brand's private extras (`brandExtras`), either of which would tie prefab to a single brand.
169
- - Pinned prefab's design tokens to the shared `brandc` contract with a drift guard (`test/brand-contract.test.ts`). All 30 token names `prefab.css` defines are already contract names, so the test asserts that and the reverse direction: every token prefab reads is covered by each shipped brand, in `:root` for scalars and in both schemes for colours. brandc is a devDependency only. prefab keeps zero runtime dependencies, and the token *values* stay as host-adaptive `var()` fallback chains (MCP Apps var, then VS Code var, then static default) rather than adopting brandc's literal `oklch()` values, which would break host theming and change the default look. `toPrefabTheme()` already emits prefab's exact wire `theme` shape, so a brand drives prefab with no adapter.
170
- - `test/fixtures/run_fixtures.py` skips regeneration with a notice when the `prefab_ui` Python package is absent, instead of failing and blocking `bun run test`. Its status output is also written as UTF-8, since a cp1252 Windows console raised `UnicodeEncodeError` from inside the error handler and masked the real failure.
171
-
172
- ### Docs
173
-
174
- - Rewrote the playground example descriptions in plain language for first-time viewers, dropping internal jargon (`Define`/`Use`/`Condition`, `display_update`, "mock MCP tool", `onChange`/`setState`, "wire format") in favor of what the user sees and does. The Composition example is restructured into two labeled sections with natural labels ("Grade:", "Note:") instead of "Condition:" / "If / Elif / Else:".
175
-
176
- ## [0.3.5] — 2026-07-22
177
-
178
- ### Fixed
179
-
180
- - `validateWireFormat` no longer false-flags legitimate component slots. `trigger` (Dialog), `empty` (Detail/MasterDetail), `else` (Condition), and `summary` (ExpandableRow) are read by the renderer as component nodes, so they are no longer reported as misplaced children.
181
- - **`Popover` and `HoverCard` now accept a `trigger` component** (like `Dialog`). Previously they rendered all children into hidden content with no visible trigger, so there was nothing to click or hover. The `trigger` is rendered as the always-visible target; `children` are the content.
182
- - **`Slot` now receives injected content from `Use`.** A `Use` node's own `children` fill the default slot, and a `slots` map fills named slots, so a `Slot` inside a `Define` template renders the caller's content instead of only its fallback.
183
-
184
- ### Docs
185
-
186
- - Added playground showcase examples: **Chart Gallery** (all six chart types as native SVG), **Analytics Dashboard** (KPI metrics, charts, tabs, and range controls driving reactive state), and **Interactive Components** (Dialog, Accordion, and Switch/Slider bound to a live readout).
187
- - Added **MCP app examples** with an in-page mock MCP server, so the playground demonstrates the round-trip (not just on-demand rendering): **MCP: Tool Call** (`callTool` → `display_update`, server-authoritative counter), **MCP: Form → Tool** (form submit → tool → updated list), and **MCP: Live Data** (`Subscribe` with polling fallback → live metrics and chart). Modeled on the `_action` + `chess://` subscription pattern from real prefab MCP apps.
188
- - Added coverage-filling examples that raise demonstrated component coverage from ~37% to ~60%: **Master / Detail** (`MasterDetail` + `Detail` + selection), **Form Controls** (`RadioGroup`, `Combobox`, `DatePicker`, `Switch`, `Slider` with a live readout), **Composition & Control Flow** (`Define`/`Use` template reuse + `Condition` + `If`/`Elif`/`Else`), and **Content & Media** (`Markdown`, `Code`, `Kbd`, inline `Svg`, `Ring`, `Sparkline`, `RadialChart`, `Histogram`, `Tooltip`, `Carousel`).
189
- - Corrected the playground's AI system prompt: it now teaches the `0.3` wire format, the real component and action set, control flow, and pipes, replacing the stale `0.2` guidance (`showToast` `title`, `appendState` `item`, and non-existent components).
190
-
191
- ### Internal
192
-
193
- - Expanded regression coverage with TDD probes across formatting pipes, expressions, state ops, `ForEach`, the semantic `Table`, `DataTable` interactions (accessor, row click, search), and chart edge cases (empty data, single point), alongside tests for the `Popover`/`HoverCard`/`Slot` fixes above.
194
-
195
- ## [0.3.4] — 2026-07-22
196
-
197
- ### Fixed
198
-
199
- - Action dispatch failures are no longer silent. Every fire-and-forget handler (`onClick`, `onMount`, `onChange`, `onSubmit`, `onRowClick`, keyboard bindings, interval ticks, and `display_update` actions) now routes rejections through the logger via a new `fireAndForget` helper, so a throwing action surfaces instead of vanishing. The async actions (`toolCall`, `fetch`, `callHandler`, `subscribe`) report through a shared `reportActionError`: at `debug` when an `onError` handler is wired, at `warn` when none is, so an unhandled failure is never fully quiet.
200
-
201
- ### Internal
202
-
203
- - The release workflow now requires a changelog entry: it fails if `## [Unreleased]` has no entries for the version about to ship, and otherwise promotes that section to the new version heading (`scripts/changelog-release.ts`). This keeps the changelog from drifting behind published releases.
204
-
205
- ## [0.3.3] — 2026-07-22
206
-
207
- ### Added
208
-
209
- - **Centralized logger** (`src/core/logger.ts`), zero runtime dependencies. Mirrors the `@maxhealth.tech/utils` `createLogger` contract (scoped `error`/`warn`/`info`/`debug`, `[prefab]` / `[prefab:scope]` prefix) and adds a runtime `setLogLevel()` so an embedding host can mute or raise prefab's console output. All internal `console.*` calls now route through it. New exports: `createLogger`, `log`, `setLogLevel`, `getLogLevel`, `LogLevel`, `Logger` (and `window.prefab.setLogLevel`).
210
- - **Wire validation now catches silent authoring mistakes.** `validateWireFormat` flags child components placed under a non-`children` key (e.g. `then`/`else`/`body`), which the renderer never reads, and requires a non-empty `message` on `showToast`. `PrefabRenderer.mount()` runs validation and reports any problems as non-fatal console warnings (opt out with `{ validate: false }`), so a payload that renders nothing now tells you why. `validateWireFormat` / `isValidWireFormat` are also exported from the renderer entry.
211
-
212
- ### Fixed
213
-
214
- - **DataTable renders primitive rows.** A table over an array of strings or numbers rendered blank cells, because `row[column.key]` is undefined on a primitive. A primitive row is now used directly as the cell value, so a plain string list displays.
215
-
216
- ### Docs
217
-
218
- - Repaired the playground examples, each verified against the live renderer: Conditional UI declared its branches under a non-existent `then` key (renamed to `children`), Todo List could not display its string tasks through a DataTable (now a `ForEach` list), and the Contact form toast passed `title` instead of `message`. The playground's own toast handler read `t.title` (never present on `ToastEvent`) and now reads `t.message`. All example payloads bumped to wire version `0.3`.
219
- - Reframed the project tagline across the README and the site description: from a declarative UI component library "wire-compatible with" PrefectHQ's Python `prefab-ui` to a full-stack framework and a superset of it.
220
-
221
- ## [0.3.2] — 2026-07-19
222
-
223
- ### Fixed
224
-
225
- - **Badge no longer stretches to full width.** `.pf-badge` is `inline-flex`, but as a flex child it was blockified and stretched to the container's cross size (full width inside a column, full height inside a row). It now uses `align-self: flex-start` to stay intrinsically sized; an explicit `w-full` or width utility still overrides it.
226
-
227
- ### Docs
228
-
229
- - **Playground preview text contrast.** The mounted preview inherited the playground chrome's light-gray text color, leaving content near-invisible on a light preview. The preview now applies the theme's `--foreground`.
230
- - **Single-sourced the playground theme state.** The toolbar "Dark" checkbox and the renderer's floating theme toggle were independent (one toggled a class, the other set `data-theme` plus localStorage and document sync) and drifted apart. They now share one source of truth. The demo and playground CDN example pins were bumped from `@0.2` to `@0.3`.
231
-
232
- ## [0.3.1] — 2026-06-27
233
-
234
- Documentation and tooling release. No runtime or API changes: `src/` is unchanged aside from the version bump, so the published bundle is functionally identical to 0.3.0.
235
-
236
- ### Fixed
237
-
238
- - Corrected the wire-protocol version label across the user-facing docs. The README badge and tagline, the skill header, the homepage feature text, and a version-pin example still advertised `0.2`, but the library has shipped protocol `0.3` since 0.3.0 (the renderer still accepts legacy `0.2` payloads).
239
-
240
- ### Docs
241
-
242
- - Deduplicated the Guide and Reference sections. Each reference topic now lives in one place, and the Guide pages are concise conceptual pages that link into the reference, removing a large amount of duplicated content.
243
- - Single-sourced the example UIs. The demo and playground now load one canonical set of `$prefab` examples from `docs/public/examples/` at runtime instead of embedding their own copies, and the skill bundle's example assets are generated from that same set.
244
- - The programmatic API reference (actions, rx, mcp, auto, renderer) is now generated from the source TSDoc with TypeDoc, so it stays in sync with the code. The component catalog, wire format, and MCP Apps pages remain hand-written.
245
-
246
- ## [0.3.0] — 2026-06-16
247
-
248
- ### Wire Format (Breaking) — protocol `0.3`
249
-
250
- Catches up to upstream PrefectHQ/prefab v0.20.x ("Wire Transfer", PR #431). The `$prefab.version` envelope is now **`0.3`**. The renderer still accepts `0.2` payloads, so existing stored wire data keeps rendering.
251
-
252
- - **New top-level `css` field** — an array of inline CSS blocks, injected as `<style>` tags. The **theme is now compiled into this array** (`:root` for light, `.dark, [data-theme="dark"]` for dark) instead of shipping a structured `theme` object; `PrefabApp.toJSON()` no longer emits a `theme` key. New `compileThemeCss()` / `ThemeVars` exports expose the compiler.
253
- - **`stylesheets` redefined as external URLs** — rendered as `<link rel="stylesheet" href="…">`. The renderer keeps a heuristic that still injects inline CSS found in `stylesheets` as `<style>` for backward compatibility with `0.2` payloads.
254
- - **New top-level `mode` field** (`'light' | 'dark'`) — forces a color scheme. The renderer applies it via both the `data-theme` attribute and the `dark`/`light` class (so upstream-compiled `.dark {}` CSS resolves), and `toHTML()` stamps it on `<html>`.
255
- - **`PrefabApp` / `DisplayOptions`** gain `css` and `mode` options; `display()` merges `css` (concatenated) and `mode` (override) like the other options.
256
- - `toHTML()` now emits `css`/`stylesheets`/`mode` into `<head>` and strips them from the embedded JSON to avoid double-injection (mirrors upstream `html()`).
257
-
258
- ### Charts
259
-
260
- - **`valueFormat` parity (PR #454)** — added the canonical `valueFormat` prop to cartesian/pie/radial charts (value-axis ticks + tooltip values), matching upstream's field name so chart formats are wire-compatible in both directions. `yAxisFormat` / `yAxisRightFormat` remain TS-only overrides for dual-axis charts. The renderer reads `valueFormat` as the base format and treats `"auto"` as "no explicit format".
261
- - **Pie/Radial/Scatter data-binding parity** — `PieChart` and `RadialChart` now use upstream's `dataKey` (value) + `nameKey` (label) model, and `ScatterChart` gains `xAxis`/`yAxis`/`zAxis` (z = bubble size). The legacy `series`/`xAxis` inputs are still accepted and mapped onto the canonical fields (**dual-accept**, non-breaking), and the Pie renderer reads both shapes — so these charts now round-trip with the Python renderer. New exports: `CategoricalChartProps`, `PieChartProps`, `ScatterChartProps`.
262
- - **Native Radial, Scatter & Radar renderers** — replaced the "not yet supported" placeholders with real SVG renderers, so **every chart type now draws natively** (the fallback is gone):
263
- - `RadialChart` — concentric value arcs (muted track + coloured value), configurable `innerRadius`/`startAngle`/`endAngle`.
264
- - `ScatterChart` — points over min/max axes with optional grid, tooltips, and **bubble sizing** from `zAxis`.
265
- - `RadarChart` — one polygon per series over angular axes, with `axisKey` spoke labels, `filled`, and `showDots`; legacy `xAxis` maps to `axisKey`.
266
- - All honour `valueFormat`.
267
- - **Refactor** — the chart renderers' shared toolkit (colours, axes, SVG primitives, value formatting, legend) was extracted into `chart-helpers.ts` to keep each renderer file under the 700-LOC limit.
268
-
269
- ### Internal
270
-
271
- - **DRY theme compilation** — `applyTheme()` (the legacy `theme`-object path) now routes its dark block through the same `compileThemeCss()` the wire path uses, so both paths emit byte-identical dark CSS. The CSS sanitizers are shared too.
272
-
273
- > **Fixtures verified against upstream.** Golden fixtures were regenerated from upstream `prefab-ui` **0.20.2**: 9/10 are byte-identical to the previous `0.2` fixtures apart from the version bump (confirming parity, and that upstream emits `0.3`). The 10th (chart) now carries upstream's `valueFormat`, which the builder also emits.
274
-
275
- ## [0.2.40] — 2026-05-15
276
-
277
- ### New Features
278
- - **`onHostContextChanged` callback** — `PrefabApp` now exposes `onHostContextChanged((ctx) => {...})` so components can react to arbitrary fields in `ui/notifications/host-context-changed` (e.g. refreshed access tokens, locale, custom app config). The existing `prefab:theme-update` event continues to fire for backward-compat theme handling (closes #14)
279
- - **`prefab:host-context-changed` bridge event** — the bridge now dispatches the full params object from `host-context-changed` as a separate `prefab:host-context-changed` event, in addition to the existing `prefab:theme-update`
280
-
281
- ## [0.2.39] — 2026-05-10
282
-
283
- ### New Features
284
- - **`display_update()` actions** — `display_update()` now accepts an optional `actions` parameter to fire actions alongside state deltas. Actions execute after the state is merged into the store (closes #13)
285
- ```ts
286
- display_update(
287
- { 'game.turn': 'b', 'game.fen': '...' },
288
- { actions: [new SendMessage("Your turn. Pick a move.")] }
289
- )
290
- ```
291
-
292
- ## [0.2.38] — 2026-05-09
293
-
294
- ### Bug Fixes
295
- - **Fixed**: `display()` now merges options into an existing `PrefabApp` instance — previously `state`, `stylesheets`, `theme`, `layout`, `pipes`, and all other options were silently discarded when a `PrefabApp` was passed. State and defs are shallow-merged (options win on conflict), stylesheets are concatenated, and scalar options (theme, layout, cssClass, onMount) fall back to the app's value (closes #12)
296
-
297
- ## [0.2.37] — 2026-05-09
298
-
299
- ### New Features
300
- - **Reactive `cssClass`** — `cssClass` now accepts `RxStr` (reactive expressions) on all components. Use `rx()` to dynamically set CSS classes based on state (closes #11 part 1)
301
- - **`onClick` on all components** — `Div`, `Span`, `Column`, `Row`, `Grid`, `GridItem`, `Container`, and all other components now support `onClick` actions. Non-button elements automatically get `role="button"`, `tabindex="0"`, and keyboard (Enter/Space) support for accessibility (closes #11 part 2)
302
-
303
- ## [0.2.36] — 2026-05-09
304
-
305
- ### Bug Fixes
306
- - **Fixed**: Subscribe `onDataCallback` no longer clobbers merged state with raw response data — when `display_update()` merges a state delta via `applyPrefabUpdate()`, `store.set(stateKey, data)` is now skipped, preventing `stateKey` collision with delta keys (closes #10)
307
- - **Fixed**: ESLint config now ignores `docs/` (VitePress cache/dist) and `eslint.config.ts` — previously caused 60 spurious parsing errors
308
-
309
- ## [0.2.35] — 2026-05-09
310
-
311
- ### Bug Fixes
312
- - **Fixed**: `display_form()` now forwards all `DisplayOptions` — `layout`, `cssClass`, `stylesheets`, `pipes`, `onMount`, `keyBindings`, and `defs` were silently dropped (only `state` and `theme` worked). `DisplayFormOptions` now extends `DisplayOptions`
313
- - **Fixed**: `display_success` / `displaySuccess` and `DisplaySuccessOptions` are now exported from the package entry point
314
-
315
- ## [0.2.34] — 2026-05-09
316
-
317
- ### Bug Fixes
318
- - **Fixed**: Subscribe fallback poll handler now detects `$prefab` responses — poll results containing full views trigger `remount()`, and `display_update` payloads merge state into the store. Previously poll results were stored as raw data, leaving the DOM frozen (closes #9)
319
-
320
- ## [0.2.33] — 2026-05-09
321
-
322
- ### Bug Fixes
323
- - **Fixed**: `display()` now forwards `stylesheets` and `pipes` options to `PrefabApp` — previously these were silently dropped since `DisplayOptions` didn't include them (closes #8)
324
-
325
- ## [0.2.32] — 2026-05-09
326
-
327
- ### Bug Fixes
328
- - **Fixed**: `If()` / `Elif()` shorthand now correctly detects `Rx` expression objects — the previous inline type check only matched strings and `Ref` (subscribe), silently treating `Rx({ expression })` as a props object
329
- - **Fixed**: Action handlers (`toolCall`, `callHandler`, `fetch`) now handle `display_update()` state delta payloads (`{ $prefab, update: { state } }`) by merging into the store — previously these were silently dropped while `onToolResult` in auto-mount already handled them correctly (closes #7)
330
-
331
- ## [0.2.31] — 2026-05-09
332
-
333
- ### New Features
334
- - **CallTool structuredContent remount** — when a `toolCall`, `callHandler`, or `fetch` action returns a full prefab view (`{ $prefab, view }`), the renderer remounts with the new view instead of just storing the raw result (#7)
335
- - Supports direct payloads, MCP `structuredContent` wrappers, and `content[].text` JSON blocks
336
- - Preserves existing store state across remounts
337
- - Handles pipes, stylesheets, layout hints, key bindings, defs, and theme on remount
338
-
339
- ## [0.2.30] — 2026-05-09
340
-
341
- ### Bug Fixes
342
- - **Fixed**: Reverted serializer output back to **camelCase** (`cssClass`, `onMount`, `onClick`, `resultKey`, etc.), matching the upstream PrefectHQ/prefab wire format (`by_alias=True`). The snake_case serializer in v0.2.28 was a breaking change based on an incorrect assumption about the wire spec.
343
- - **Kept**: Renderer normalization layer — `renderNode()` and `dispatchOne()` accept **both** snake_case and camelCase input, so Python-SDK-generated wire data still renders correctly.
344
- - **Fixed**: `autoTable` column keys now match raw data keys (no `toCamelCase` conversion) — snake_case row keys like `proposed_start` render correctly without column/key mismatch.
345
- - **Fixed**: `cdnBase()` now uses the **exact** version (`@0.2.30`) instead of a semver range (`@0.2`) — eliminates stale jsDelivr cache serving old renderer bundles after a new patch publish (closes #6)
346
-
347
- ## [0.2.29] — 2026-05-09 *(unpublished — included the faulty snake_case serializer from v0.2.28)*
348
-
349
- ### Bug Fixes
350
- - **Fixed**: `rendererHtml()` CDN URL now derives major.minor from `VERSION` automatically — no more hardcoded `@0.2` constant that would go stale on a minor/major bump
351
-
352
- ## [0.2.28] — 2026-05-09 *(unpublished — snake_case serializer was a breaking change, reverted in v0.2.30)*
353
-
354
- ### New Features
355
- - **`Subscribe` action** — real-time resource updates via `subscribe(uri, stateKey, opts)` with automatic fallback polling when the host doesn't support MCP `notifications/resources/updated` (#3)
356
- - Supports `fallbackInterval`, `fallbackTool`, `fallbackArgs` for polling-based hosts
357
- - `onData` / `onError` callbacks for reactive state binding
358
- - **`PdfViewer` component** — embed PDF documents with `pdfViewer(src, opts)` (#2)
359
-
360
- ### ~~Wire Format (Breaking)~~
361
- - ~~**snake_case wire format** — all structural keys in the `$prefab` JSON output are now snake_case~~ *(reverted in v0.2.30)*
362
- - Renderer normalizes incoming JSON at entry points (`renderNode`, `dispatchOne`), accepting both snake_case and camelCase input for backwards compatibility
363
- - User-data containers (`state`, `arguments`, `context`, `overrides`) are **not** converted — keys inside them are preserved as-is
364
-
365
- ### Bug Fixes
366
- - **Fixed**: `fallbackArgs` reactive expressions now resolve correctly in Subscribe action
367
- - **Fixed**: resilient cleanup for Subscribe timers and listeners
368
- - **Fixed**: chart series structural keys (`dataKey` → `data_key`) now correctly serialized
369
- - **Fixed**: `PrefabApp.toJSON()` root view outputs `css_class` (was bypassing `Component.toJSON()`)
370
- - **Fixed**: wire format validator accepts both camelCase and snake_case action prop names
371
-
372
- ### Documentation
373
- - SEO & LLM visibility improvements for documentation site
374
-
375
- ### Tests
376
- - 22 new TDD tests for snake_case serialization and renderer normalization
377
- - All 10 golden fixtures updated to snake_case wire format
378
- - **1237 tests** passing across 36 files
379
-
380
- ## [0.2.27] — 2026-05-02
381
-
382
- ### Bug Fixes
383
- - **Fixed**: `resourceMeta()` now serializes permissions as `{}` (empty objects) per the MCP Apps spec, not `true`. The DX-facing input type stays `boolean` for ergonomics; the wire output is now spec-compliant with `McpUiResourcePermissions`
384
-
385
- ## [0.2.26] — 2026-05-02
386
-
387
- ### New Features
388
- - **`rendererHtml(opts?)`** — generates the viewer HTML page string for MCP Apps resources (loads `prefab.css` + `renderer.auto.min.js` from CDN, with optional extra scripts/stylesheets)
389
- - **`registerViewerResource(server, opts?)`** — one-liner to register a `ui://` viewer resource with correct MIME type, CSP (auto-merged with jsdelivr default + script origins), Permission Policy, and `_meta` on both listing and content item
390
- - **`PREFAB_RESOURCE_URI`** — default URI constant (`'ui://prefab/viewer'`)
391
- - All three exported from both `@maxhealth.tech/prefab/mcp` and the main barrel
392
-
393
- ### Documentation
394
- - Corrected CSP/permissions claims about Claude Desktop (it DOES enforce CSP via HTTP headers on `{hash}.claudemcpcontent.com`)
395
- - Corrected cross-server tool call claim (spec says `"app": Tool callable by the app from this server only`)
396
- - Updated host compatibility table (all hosts enforce CSP)
397
- - Added `registerViewerResource()` shortcut section to `docs/reference/mcp-apps.md`
398
- - Added `rendererHtml()`, `registerViewerResource()`, `PREFAB_RESOURCE_URI` reference docs to `docs/reference/mcp-display.md`
399
-
400
- ### Tests
401
- - 15 new tests for `rendererHtml()`, `registerViewerResource()`, `PREFAB_RESOURCE_URI` (security escaping, CSP merging, handler invocation)
402
- - Comprehensive layout hints test coverage (bridge, renderer combos, passthrough, HTML/MCP round-trip)
403
- - **1174 tests** passing across 32 files
404
-
405
- ## [0.2.25] — 2026-04-30
406
-
407
- ### Layout Hints
408
- - **New**: `LayoutHints` type — `preferredHeight`, `minHeight`, `maxHeight` (px) for declarative host container sizing
409
- - `PrefabWireFormat.layout` — new optional field in the `$prefab` wire format
410
- - `PrefabAppOptions.layout` — set directly on the app constructor
411
- - `DisplayOptions.layout` — set via `display(view, { layout: { preferredHeight: 600 } })`
412
- - Renderer applies `height` / `min-height` / `max-height` + `overflow: auto` as inline styles on the mount root
413
- - `Bridge.notifyPreferredSize()` — emits `ui/notifications/preferred-size` (JSON-RPC) or `prefab:preferred-size` (prefab protocol) to the host on mount
414
- - Auto-mount (`renderer.auto.min.js`) forwards layout hints automatically when wire data contains a `layout` field
415
-
416
- ### CSS Utilities
417
- - **Added**: `max-h-*` utility classes (`max-h-48`, `max-h-64`, `max-h-80`, `max-h-96`, `max-h-full`, `max-h-screen`, `max-h-none`)
418
-
419
- ### Bug Fixes
420
- - **Fixed**: `DOM.Iterable` added to tsconfig `lib` — resolves `NodeListOf` iteration errors in strict mode
421
- - **Fixed**: null-safe `.textContent` access in `data.ts` (DataTable search) and `form.ts` (Combobox filter)
422
- - **0 tsc errors**, **1149 tests** passing across 32 files
423
-
424
- ## [0.2.24] — 2026-04-30
425
-
426
- ### Utility Classes
427
- - **Added**: ~200 Tailwind-compatible utility classes in `prefab.css` — auto-renderers no longer require Tailwind CSS
428
- - Categories: display, flexbox, gap, padding (px/py/pt/pb/pl/pr scales), margin (mx/my/mt/mb/ml/mr scales), width, height, max-width (xs–7xl), typography (text-xs–4xl, font weights, alignment, line-height, tracking), theme-aware text/bg colors, borders, rounded, shadows, overflow, position, z-index, opacity, cursor, transitions, sr-only
429
-
430
- ### Theme Fix
431
- - **Fixed**: reverted to v0.2.20 theme behaviour after regressions in v0.2.21/v0.2.22
432
- - `applyHostTheme()` sets `data-theme` from host `colorScheme` again — toggle works correctly in hosted mode
433
- - `syncVsCodeTheme()` restored for standalone VS Code webviews (reads `data-vscode-theme-kind`, MutationObserver)
434
- - `[data-theme]` blocks use static values only (no host var references) — prevents both blocks resolving identically
435
-
436
- ## [0.2.22] — 2026-04-29
437
-
438
- ### Host Theme Adaptation
439
- - Separated host theming from toggle: `applyHostTheme()` no longer sets `data-theme`
440
- - Removed `syncVsCodeTheme()` (later restored in v0.2.23)
441
-
442
- ## [0.2.21] — 2026-04-29
443
-
444
- ### Host Theme Adaptation
445
- - **Fixed**: hosted mode (Claude Desktop / VS Code) now adapts to host theme colors correctly
446
- - `applyHostTheme()` no longer sets `data-theme` — host's inline CSS variables resolve through the `var()` fallback chain in `:root` / `@media dark` blocks automatically
447
- - Removed `syncVsCodeTheme()` — VS Code's `--vscode-*` vars resolve through the same chain without needing `data-theme`
448
- - `[data-theme]` blocks use static values only and are reserved for the standalone manual toggle
449
- - Architecture: hosts control theme via inline vars (cascade), standalone controls via `data-theme` (static) — no conflict
450
-
451
- ## [0.2.20] — 2026-04-29
452
-
453
- ### Theme Toggle Fix
454
- - **Fixed**: dark/light toggle icon flipped but colours didn't change — `@media (prefers-color-scheme: dark) :root:not(...)` at specificity (0,2,0) beat `[data-theme]` at (0,1,0). Bumped to `:root[data-theme="dark/light"]` (0,2,0) so toggle wins by source order
455
-
456
- ### VS Code Theme Sync
457
- - `syncVsCodeTheme()` — reads `data-vscode-theme-kind` from `document.body`, maps to `data-theme` on `:root` (`vscode-dark` / `vscode-high-contrast` → `dark`, else `light`)
458
- - `MutationObserver` watches for VS Code theme switches and keeps `data-theme` in sync automatically
459
- - Only active in standalone / VS Code context (skipped when MCP Apps bridge is present)
460
-
461
- ### Tests
462
- - Added `renderer-destroy.test.ts` — 12 tests for component destroy hook lifecycle
463
- - **1142 tests** passing across 32 files
464
-
465
- ## [0.2.19] — 2026-04-29
466
-
467
- ### SDK Type Compatibility
468
- - `McpToolResult` now structurally assignable to `@modelcontextprotocol/sdk` `CallToolResult` — no cast needed when returning `display()` from SDK tool handlers
469
- - Added `[key: string]: unknown` index signature (satisfies SDK's `Result` base)
470
- - Split `McpResourceContent.resource` into discriminated union (`McpTextResourceContents | McpBlobResourceContents`) matching SDK's `EmbeddedResource`
471
- - Added optional `annotations?` and `_meta?` on all content types
472
-
473
- ### Host Theme Token Mapping
474
- - **CSS fallback chain**: all design tokens in `prefab.css` resolve through 3 tiers — MCP Apps spec vars (`--color-background-primary`) → VS Code vars (`--vscode-editor-background`) → static defaults
475
- - Applies to all theme blocks: `:root`, `@media (prefers-color-scheme: dark)`, `[data-theme="dark"]`, `[data-theme="light"]`
476
- - Downstream MCP servers no longer need custom CSS for Claude Desktop or VS Code webview theming
477
- - Added `--shadow-sm/md/lg` and `--border-radius-*` → `--radius` mapping from MCP Apps spec
478
- - `applyHostTheme()` now injects host-provided `@font-face` / `@import` CSS from `styles.css.fonts` (idempotent `<style>` tag)
479
- - `HostTheme.fontCss` field added; extracted from both `ui/initialize` and `ui/notifications/host-context-changed`
480
-
481
- ## [0.2.18] — 2026-04-29
482
-
483
- ### Builder API Improvements
484
- - `display_success(title, body?)` — success-variant alert helper
485
- - `resourceMeta(opts)` — build `_meta` for `resources/read` (CSP, permissions, domain, border)
486
- - `PREFAB_CDN_META` — pre-built meta with jsDelivr CDN CSP for common deployments
487
- - `structuredContent` on all display helpers (`display`, `display_form`, `display_update`, `display_error`, `display_success`)
488
- - `PrefabApp.toMcpResult()` — returns `{ content, structuredContent }` for direct SDK tool handler return
489
- - MCP actions: `RequestDisplayMode` action + `displayMode` option on `CallTool`
490
- - New component support: `Embed`, `Markdown`, `Mermaid`, `CodeBlock` builder classes
491
-
492
- ### Tests
493
- - **1130 tests** passing across 31 files
494
-
495
- ## [0.2.17] — 2026-04-28
496
-
497
- ### Auto-Resize
498
- - `Bridge.setupAutoResize(el)` — `ResizeObserver` on the target element, notifies the host whenever the content dimensions change via `ui/notifications/size-changed` (JSON-RPC) or `prefab:size-changed` (prefab protocol). Mirrors the ext-apps SDK `autoResize: true` behaviour without the SDK dependency.
499
- - `sendRpcNotification()` — fire-and-forget JSON-RPC notification (no `id`, no response expected)
500
- - `PrefabApp.setupAutoResize(target)` — public API accepting selector or element
501
- - `renderer.auto.min.js` now auto-observes `#root` after boot — hosts get size updates out of the box
502
- - Deduplicates identical dimensions, fires initial notification immediately
503
- - 4 new tests — **1130 total tests**
504
-
505
- ## [0.2.16] — 2026-04-28
506
-
507
- ### Docs
508
- - Added `appInfo` vs `clientInfo` Common Pitfall section to `mcp-apps.md`
509
- - Added ext-apps SDK vs native `ui/*` JSON-RPC comparison table
510
- - Added ext-apps SDK source references to Reference section
511
-
512
- ## [0.2.15] — 2026-04-28
513
-
514
- ### Bug Fix: Claude Desktop / ChatGPT Breakage
515
- - **Fixed**: `ui/initialize` handshake sent `clientInfo` instead of `appInfo` — hosts validate with Zod schema that requires `appInfo`, causing silent handshake failure (blank iframe, no error). The ext-apps SDK fallback in v0.2.11 masked this; removing the SDK in v0.2.12 exposed it.
516
- - `Bridge.initialize()` error handling: `Promise.any` wrapped in try/catch, rethrows as descriptive `Error('Bridge init failed — no host responded')` with `{ cause }` preserving the `AggregateError`
517
- - 2 new tests: `appInfo` field validation, clear error on dual-protocol failure — **1126 total tests**
518
-
519
- ## [0.2.14] — 2026-04-28
520
-
521
- ### Built-in Theme Toggle
522
- - `createThemeToggle(root, options?)` — renders a floating sun/moon toggle button with two-way sync to `data-theme` attribute via `MutationObserver`
523
- - `PrefabRenderer.mount()` auto-attaches toggle by default (opt out with `themeToggle: false`)
524
- - Toggle preserved across re-renders
525
-
526
- ### Bug Fixes
527
- - `appendState` action: support `item` alias (Python SDK compat)
528
- - Charts: restore CSS custom properties in SVG presentation attributes for dark mode
529
-
530
- ## [0.2.13] — 2026-04-27
531
-
532
- ### Bug Fixes
533
- - Renderer: remove all inline `theme-variable` styles, rely on CSS classes (CSP compliance)
534
- - Badge: use `Partial<Record>` to satisfy `strict-boolean-expressions` lint rule
535
- - Demo: fix theme toggle, favicon 404, copy-MCP-button styling
536
- - Docs: inject CDN version from `package.json` at build time
537
-
538
- ## [0.2.12] — 2026-04-27
539
-
540
- ### Bundle Size Reduction
541
- - **Removed** `@modelcontextprotocol/ext-apps` SDK dependency — 405 KB → 80 KB bundle
542
- - Bridge now speaks native `ui/*` JSON-RPC without the SDK wrapper
543
- - Docs updated to remove ext-apps references
544
- - ⚠️ **Regression**: `clientInfo` field name broke Claude Desktop / ChatGPT (fixed in v0.2.15)
545
-
546
- ## [0.2.11] — 2026-04-27
547
-
548
- ### Theming
549
- - `data-theme` attribute support in `prefab.css` — light/dark mode via attribute selector
550
- - MCP agent skill: attach `prefab-skill.zip` to GitHub releases
551
-
552
- ## [0.2.10] — 2026-04-27
553
-
554
- ### Docs & Polish
555
- - Select options shorthand syntax
556
- - Remote usage mode documentation
557
- - Brand assets, favicon, logo paths
558
- - Playground: dark preview background
559
- - Lint fixes: `Array<T>` → `T[]`, control char regex, `RegExp.exec`
560
- - Markdown renderer: protect inline code from formatting, fix CRLF infinite loop
561
- - DRY: `serializeCallbacks`, camelCase display aliases
562
-
563
- ## [0.2.9] — 2026-04-26
564
-
565
- ### Bug Fix
566
- - `autoTable` column keys now match serialized row keys
567
-
568
- ## [0.2.8] — 2026-04-26
569
-
570
- ### Universal MCP Apps Bridge
571
- - **Fixed**: `renderer.auto.min.js` now works in VS Code, Claude Desktop, ChatGPT, and all MCP Apps hosts without any inline adapter code
572
- - `Bridge.initialize()` races `prefab:init` and `ui/initialize` JSON-RPC **in parallel** — whichever host protocol responds first wins. Eliminates the 1.5s dead time on JSON-RPC hosts
573
- - `app()` now buffers `tool-result` events — host can send results before `onToolResult` is registered without data loss
574
- - `auto.ts` defers `boot()` to `DOMContentLoaded` (or microtask if already loaded)
575
-
576
- ## [0.2.7] — 2026-04-25
577
-
578
- ### Chart Formatting
579
- - Generic pipe formatting for chart axes and tooltips
580
- - `tooltipXKey` — separate data key for tooltip vs x-axis labels
581
- - Fix: remove non-null assertion in PieChart tooltip key lookup (lint)
582
-
583
- ## [0.2.6] — 2026-04-25
584
-
585
- ### Chart Tooltips
586
- - Production-quality tooltips with crosshair, data dots, null gaps, a11y, touch support
587
-
588
- ## [0.2.5] — 2026-04-25
589
-
590
- ### Custom Renderers
591
- - `registerComponent(type, renderFn)` exposed on `window.prefab` for custom component renderers
592
-
593
- ## [0.2.4] — 2026-04-25
594
-
595
- ### Chart Axes
596
- - Y-axis, X-axis labels, grid lines, dual Y-axis support
597
-
598
- ## [0.2.3] — 2026-04-25
599
-
600
- ### JSON-RPC Protocol
601
- - Native `ui/*` JSON-RPC protocol — zero-adapter VS Code support
602
-
603
- ## [0.2.2] — 2026-04-25
604
-
605
- ### Auto-Mount Bundle
606
- - `renderer.auto.min.js` — CSP-safe self-executing bundle
607
- - CI: attach `renderer.auto.min.js` + `prefab.css` to GitHub releases
608
- - Wire compat: `callTool` action alias + `Condition` component
609
-
610
- ### Docs
611
- - Signal, Collection, Ref, sugar actions, find/dot pipes, Detail/MasterDetail, CSS theme, versioned CDN
612
- - Live playground (Monaco editor + shareable URLs + AI prompt)
613
-
614
- ## [0.2.1] — 2026-04-25
615
-
616
- ### Bug Fix: If/Elif/Else Conditional Chains
617
- - **Fixed**: `Elif` and `Else` nodes rendered independently instead of being consumed by the preceding `If` chain
618
- - New `renderChildArray()` detects `If/Elif/Else` sibling sequences as a single conditional chain
619
- - 55 new tests — **913 total tests**
620
-
621
- ### Bug Fix: Browser Pipe Registration
622
- - **Fixed**: Custom pipes registered in Node were not available in the browser renderer bundle
623
- - `PrefabApp({ pipes })` accepts pipe functions, serializes source into wire format
624
- - Renderer `mount()` hydrates wire pipes via `new Function()` before first render
625
- - Built-in pipes cannot be shadowed by wire pipes (security)
626
- - 12 new tests — **858 total tests**
627
-
628
- ## [0.2.0] — 2026-04-25
629
-
630
- ### Breaking: Wire Format v0.2
631
- - `$prefab.version` bumped to `0.2`
632
- - Initial release of the `0.2.x` series with all v0.1.x features plus the universal MCP Apps bridge
633
-
634
- ## [0.1.10] — 2026-04-24
635
-
636
- ### Action-Builder Sugar
637
- - `set(signal, value)` — ergonomic wrapper for `new SetState(signal.key, value)`
638
- - `toggle(signal)` — wrapper for `new ToggleState(signal.key)`
639
- - `append(collection, item, index?)` — wrapper for `new AppendState(collection.stateKey, item)`
640
- - `pop(collection, indexOrValue?)` — wrapper for `new PopState(collection.stateKey, indexOrValue)`, defaults to last element
641
- - All helpers accept `Signal`, `Collection`, or raw `string` key via `StateTarget` type
642
- - `set()` passes through `SetStateOpts` (onSuccess/onError callbacks)
643
- - 17 new tests in `test/sugar.test.ts` — **846 total tests**
644
-
645
- ## [0.1.8] — 2026-04-24
646
-
647
- ### Reactive Primitives
648
- - `signal(key, initial)` — named reactive scalar for wire format, auto-registers state
649
- - `collection(key, rows, { key })` — named keyed array, auto-registers state
650
- - `Ref<T>` — lazy pipe expression referencing a row in a collection via `collection.by(signal)`
651
- - Typed `Ref.dot(field)` — returns `Ref<T[K]>` with autocomplete on `keyof T`
652
- - `Ref.formatted(field, pipe, ...args)` — sugar for `.dot(field).pipe(pipe)`
653
- - `Rx.pipe(name, ...args)` — public variadic pipe builder (was private single-arg)
654
- - `Ref.pipe(name, ...args)` — delegates to `Rx.pipe()`
655
-
656
- ### Pipe Extension Point
657
- - `registerPipe(name, fn)` — global custom pipe registry for companion packages
658
- - `unregisterPipe(name)` — remove a pipe (tests)
659
- - `listPipes()` — list registered names (debugging)
660
- - Built-in pipes always shadow custom pipes (safety)
661
- - Re-registration warns and overwrites (HMR-friendly)
662
- - Custom pipes receive variadic parsed args (`| date:'long'`, `| between:1,10`)
663
-
664
- ### Selection & Master-Detail
665
- - `DataTable({ from, selected })` — auto-wires `rowKey`, `onRowClick → SetState`, highlight
666
- - `Detail({ of, empty, children })` — conditional pane, shows children when ref resolves
667
- - `MasterDetail({ masterWidth, gap, children })` — two-pane flex layout
668
- - `col()` descriptor overload: `col({ key, header, format, accessor, sortable })`
669
- - `format` on columns applies pipe (built-in or custom) to cell values
670
- - `accessor` on columns resolves pipe expressions per cell (`name | humanName`)
671
-
672
- ### Auto State Collection
673
- - `signal()` and `collection()` factories auto-register into a global collector
674
- - `PrefabApp` constructor drains collector — no more `state: { ...c.toState(), ...s.toState() }`
675
- - Explicit `state` overrides auto-collected on key conflicts
676
- - Duplicate state keys warn (`[prefab] state key "X" registered multiple times`)
677
- - `resetAutoState()` exported for test cleanup
678
-
679
- ### Renderer
680
- - `find` pipe filter — O(1) keyed lookup with generation-aware cache
681
- - `dot` pipe filter — extract property from object
682
- - `Store.generation` counter — monotonically increasing, invalidates find cache on mutation
683
- - `applyFilter` falls through to custom pipe registry after built-ins
684
- - `RxStr` widened to `string | Rx | Ref` — Ref works in all component props
685
-
686
- ### Bug Fixes (TDD)
687
- - `find` pipe: numeric key coercion (`'2' !== 2`) — fixed with `String()` on both sides
688
- - `find` pipe: scope dot-path resolution (`$item.managerId`) — walk scope object
689
- - `find` cache: stale after in-place mutation — fixed with generation counter
690
- - `Detail`: `0` treated as truthy — added explicit `!== 0` check
691
- - `col({ format })`: built-in pipes silently ignored — route through `applyFilter`
692
- - `col({ accessor, format })`: double-applied formatting — skip format when accessor present
693
- - Duplicate auto-state keys: silent overwrite — now warns
694
-
695
- ### Tests
696
- - 829 tests passing across 24 files
697
- - New: `test/signal-collection.test.ts` (67 tests)
698
- - New: `test/pipes.test.ts` (17 tests)
699
- - New: `test/tdd-bugs.test.ts` (6 tests)
700
-
701
- ## [0.1.0] — 2026-04-20
702
-
703
- Initial release.
704
-
705
- ### Component Library
706
- - Core classes: `Component`, `ContainerComponent`, `StatefulComponent`
707
- - `PrefabApp` wrapper with `$prefab` v0.2 wire format and `toHTML()` self-contained page export
708
- - `rx()` reactive expression builder (pipes, comparisons, ternary, built-in vars)
709
- - 80+ components across layout, typography, card, data, form, chart, control, interactive, media, alert
710
- - Table components: `Table`, `TableHead`, `TableBody`, `TableFooter`, `TableRow`, `TableHeader`, `TableCell`, `TableCaption`, `ExpandableRow`
711
- - Form extensions: `Radio`, `RadioGroup`, `Combobox`, `ComboboxOption`, `Calendar`, `DatePicker`, `Field`, `ChoiceCard`, and more
712
- - Chart extensions: `RadialChart`, `Histogram` (in addition to Bar, Line, Area, Pie, Radar, Scatter, Sparkline)
713
- - Composition: `Define`, `Use`, `Slot` for template reuse
714
- - Client actions: `SetState`, `ToggleState`, `AppendState`, `PopState`, `ShowToast`, `CloseOverlay`, `OpenLink`, `SetInterval`, `Fetch`, `OpenFilePicker`, `CallHandler`
715
- - MCP actions: `CallTool`, `SendMessage`, `UpdateContext`, `RequestDisplayMode`
716
-
717
- ### MCP Display Helpers
718
- - `display()`, `display_form()`, `display_update()`, `display_error()`
719
-
720
- ### Auto-Renderers
721
- - `autoDetail`, `autoTable`, `autoChart`, `autoForm`, `autoComparison`, `autoMetrics`, `autoTimeline`, `autoProgress`
722
-
723
- ### Browser Renderer
724
- - Vanilla DOM renderer — 55+ components, zero framework dependencies
725
- - Reactive `Store` with get/set/merge/toggle/append
726
- - Rx expression engine: ternary, logical, arithmetic, 15+ pipes, dot access, scoped variables
727
- - Action dispatcher — 15 action types
728
- - MCP transport — HTTP POST to `/mcp/tools/call`
729
- - Theme engine — CSS custom properties from `theme` field (light/dark)
730
- - Chart renderer — built-in SVG for Bar, Line, Area, Pie
731
- - Mermaid integration — delegates to global `mermaid` if available
732
- - IIFE bundle: `renderer.min.js` (54KB) for `<script>` tag usage, `window.prefab` global
733
-
734
- ### ext-apps Bridge
735
- - `app()` one-call factory with PostMessage transport, host theme mapping, lifecycle hooks
736
- - Capability negotiation, display mode requests, tool input/result/cancelled/partial events
737
- - Auto-detect environment: iframe → PostMessage, standalone → HTTP transport
738
-
739
- ### Validation & Accessibility
740
- - `validateWireFormat()` + `isValidWireFormat()` with detailed error reporting
741
- - Stylesheet injection — renderer applies `stylesheets` field as `<style>` tags
742
- - ARIA roles/attributes and keyboard navigation on all interactive components
743
-
744
- ### Infra
745
- - GitHub Actions CI (test + build on push) and publish (npm on tag)
746
- - 253 tests passing across 11 files (570 assertions)
747
- - MIT license
42
+ ## [0.3.12] — 2026-08-26
43
+
44
+ ### Added
45
+
46
+ - **`required` is a property of every stateful control, not of the text input.** It lived on `InputProps`, so only an `Input` could state it. Select, RadioGroup and Combobox are containers that call `statefulProps` rather than inheriting it, which is the same seam `label` went missing through in 0.3.9 — and `autoForm` turns every field carrying `options` into a Select, so the fields with a fixed choice set were exactly the ones whose constraint was dropped. It moves to `StatefulProps`, is marked natively where the element supports it and with `aria-required` everywhere, and the A2UI emitter's `ChoicePicker` checks — added in 0.3.11 against a `node.required` no source component could carry — now actually fire.
47
+ - **`Select({ multiple })`, so a multi-value choice field exists on the UI path too.** `AutoFormField.multiple` was honoured only when deriving the elicitation schema: the same field definition asked a schema host for an array and a rendering host for one value. A multi-select submits an array under its name even when one option is chosen, since the field asked for a list either way, and it maps onto A2UI's `multipleSelection` variant — which the emitter defined and nothing could reach.
48
+ - **`Select({ placeholder })` reaches the wire.** The renderer has always drawn a placeholder option; `Select` serialized `statefulProps` alone, so the prop was accepted and discarded.
49
+
50
+ ### Internal
51
+
52
+ - **Choice control renderers move to `renderer/components/choice.ts`.** `form.ts` had grown past the point where Select, Radio, Combobox and ChoiceCard could be read as a group, and the required marking above touches all four. The shared label wrapper, required marker and container helpers stay in `form.ts` and are imported.
53
+
54
+ ## [0.3.11] — 2026-08-25
55
+
56
+ ### Added
57
+
58
+ - **Formatting pipes map onto A2UI catalog functions.** A2UI has no expression language, which is why pipes were reported as unbindable, but its catalog has the functions the common ones correspond to: `currency` to `formatCurrency`, `number` and `round` to `formatNumber`, `date`/`time`/`datetime` to `formatDate`, and `pluralize` to `pluralize`. Seven of prefab's twenty-two built-ins now cross over; the rest transform data rather than format it and stay reported, as does a chained pipe, since no single catalog function is two pipes. The date pipes are the one inexact mapping: prefab renders in the reader's locale and `formatDate` requires an explicit TR35 pattern, so one is chosen and a diagnostic says which.
59
+ - **Form validation survives the crossing.** Every A2UI input is `Checkable`, which is the same job prefab's `required` and `inputType` do, and neither was emitted — so a form crossed over with a required field that was no longer required. `TextField`, `CheckBox` and `ChoicePicker` now carry `checks`. No `message` is emitted with them: the rule already says which check failed, and the renderer is better placed to word and localize that. `numeric` is deliberately omitted for number fields, because the catalog requires it to carry a `min` or a `max` and prefab's number inputs carry no range; emitting a bare one fails validation, which is how the omission was found.
60
+
61
+ - **`ForEach` maps onto A2UI's child template.** A2UI has iteration, the same construct `DataTable` already used: one template component instantiated per item, with paths inside it resolving relative to the current row. The emitter simply never wired it, so every loop was dropped. `$item` now resolves to a relative path and `$index` to the `@index` system function.
62
+ - **Mixed literal and template text is interpolated through `formatString`.** A2UI's `formatString` takes a single template string with JSON Pointers embedded as `${/path}`, so `Score: {{ score }}` crosses over intact. The previous code claimed the function "would need an argument list this function has no way to name", which was simply wrong about its signature. A string is interpolated only if every value in it binds; one unbindable expression makes the whole string unbindable, since interpolating half of it would change what the text says.
63
+ - **`Define` / `Use` / `Slot` are resolved at emit time by inlining the definition.** A2UI needs no named-template concept when the agent can expand the tree before sending it. A `Use`'s `overrides` are seeded into the data model and brought into scope by name, so a `{{ title }}` inside the body reads that call's value rather than the model root. Definitions are collected in a pre-pass, since prefab imposes no ordering between a `Use` and its `Define`, and a definition that uses itself reports a cycle rather than recursing until the stack gives out.
64
+ - `CardTitle`, `CardDescription` and `Tooltip` have first-class text mappings. They rendered correctly through the generic fallback but reported a degradation for what is an exact mapping, which accounted for 36 of the 160 diagnostics across the shipped examples.
65
+
66
+ ### Fixed
67
+
68
+ - **The release merge could file unreleased entries under the version that just shipped.** Promoting `[Unreleased]` on `main` and adding new entries on `dev` are insertions at the same point, so git's line-based merge orders them heading-first and attributes unshipped work to a published version. It does so without conflicting, which is why the release workflow's best-effort sync never had anything to report, and it has now happened three times. `scripts/changelog-release.ts` keeps an anchor line under `[Unreleased]` so the two insertions land at the same offset and collide, and `scripts/check-changelog.ts` verifies the outcome independently in CI: every version section must stay byte-identical to `main`'s. The anchor is a guard, the check is the guarantee.
69
+ - **A dropped node left its already-emitted children orphaned, producing a payload the renderer rejects.** A mapper reads its children before deciding whether it can map at all: `Dialog` emits its body, then finds it cannot build a trigger, returns nothing, and the body stays in the adjacency list with no component pointing at it. `interactive-showcase.json` — one of the shipped examples — emitted an unreachable `Row` on v0.3.10 for exactly this reason. Emission is now transactional: a node that drops rolls back every component and data-model key added while mapping it. The per-type registry tests could not catch this, because each mapper is correct in isolation and the fault is in the interaction; `test/a2ui-control.test.ts` now runs every shipped example through the emitter, which does catch it.
70
+ - **`Define` emitted its template body as visible content.** It fell to the generic fallback and was flattened to a `Column`, so a definition rendered where it was declared. The renderer draws nothing at the point of definition and now neither does the emitter.
71
+ - **The emitter knew only one of the two spellings for a tool-call action.** The renderer accepts `toolCall` and `callTool`, and the shipped examples use the latter, so every hand-written tool call was silently downgraded to a generic agent event and had its arguments dropped for not being scalar.
72
+ - **A string containing two templates was mis-read as one.** The sole-template pattern is lazy but still spanned `}} … {{`, so `{{ a }} of {{ b }}` matched as a single template whose body was `a }} of {{ b`, failed to parse as a path, and was reported unbindable instead of being interpolated.
73
+ - **A `Modal` whose trigger could not map lost the whole dialog.** prefab's dialog trigger is usually a `Button` with no `onClick`, because opening the dialog is implicit, and the `Button` mapper rightly refuses one with no action. A2UI's `Modal` accepts any component as its trigger, so it now falls back to the trigger's label as `Text`.
74
+
75
+ Across the twenty shipped examples this takes the emitter from 160 diagnostics and one invalid payload to 95 diagnostics and none. What remains is largely honest: charts, `Metric` trend styling, table controls, and the conditionals.
76
+
77
+ ## [0.3.10] — 2026-08-24
78
+
79
+ ### Added
80
+
81
+ - **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.
82
+ - **`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.
83
+ - **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.
84
+ - **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.
85
+ - **`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.
86
+ - `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.
87
+ - `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.
88
+
89
+ ### Changed
90
+
91
+ - **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.
92
+
93
+ ### Fixed
94
+
95
+ - **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.
96
+ - **`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.
97
+
98
+ ### Internal
99
+
100
+ - **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.
101
+ - Removed `hasTemplate` and `isTemplatedList`, both exported with no caller anywhere.
102
+ - 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`.
103
+
104
+ ## [0.3.9] — 2026-08-07
105
+
106
+ ### Added
107
+
108
+ - **`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.
109
+ - **`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.
110
+ - **`Table({ striped })`** — shades alternate body rows via a `pf-table-striped` modifier and a `:nth-child(even)` rule, rather than per-table styling.
111
+ - **`Sparkline({ width, height })`** — the renderer hardcoded 120×32; those are now the defaults.
112
+
113
+ - **`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.
114
+
115
+ All five were documented in `docs/reference/components.md` with props-table entries before they existed, and were found by `bun run check:docs`.
116
+
117
+ ### Docs
118
+
119
+ - **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.
120
+
121
+ ## [0.3.8] — 2026-08-05
122
+
123
+ ### Fixed
124
+
125
+ - **Strict wire-format validation rejected four renderable component types.** `validateWireFormat(data, { strict: true })` checked `type` against a hand-written list in `src/core/validate.ts` that had fallen behind the renderer's registry: `Condition` (emitted by `If` / `Elif` / `Else`), `Detail`, `MasterDetail` and `PdfViewer` were all registered and renderable, but reported as `Unknown component type`. Any UI using a conditional therefore failed strict validation, as did two of the shipped examples. The list is no longer written by hand: `src/core/component-types.ts` is generated from the registry (`bun run gen:types`, also run by the build), and the validator consumes it. The generated module is import-free, so the validator still works server-side where the renderer is never loaded. Non-strict validation was unaffected, since unknown types are only an error under `strict`.
126
+ - **The shipped example UIs were never validated against the wire format.** `docs/public/examples/*.json` is the single source for the demo, the playground and the generated skill assets, and nothing pointed the package's own `validateWireFormat` at it, so an example could ship a key the renderer ignores and simply render nothing. That had already happened once (0.3.5 repaired examples declaring conditional branches under a non-existent `then` key). `test/doc-examples.test.ts` now validates all 20 in strict mode, which is what surfaced the validator drift above.
127
+
128
+ - **The display helpers could not be returned to an SDK tool handler without a cast.** `PrefabWireFormat` and `PrefabUpdateWire` were interfaces, so the `structuredContent` on every result from `display()`, `display_form()`, `display_update()`, `display_error()` and `display_success()` failed against the SDK's `{ [x: string]: unknown }` with "Index signature for type 'string' is missing". This is the same by-design TypeScript behaviour fixed for `src/mcp/types.ts` in 0.3.7 ([microsoft/TypeScript#15300](https://github.com/microsoft/TypeScript/issues/15300)); these two types were missed because they live outside that file, in `src/app.ts` and `src/mcp/display.ts`. Both are aliases now. Consumers had no way to fix this on their side, since the type came from here, and the only escape was an assertion. prefab carried the same workaround internally: `PrefabApp.toMcpResult()` used `wire as unknown as Record<string, unknown>`, a double assertion that only existed because of this, and is gone.
129
+ - **The 0.3.7 interop guard had a blind spot that let the above through.** `test/mcp-types.test.ts` modelled an SDK result as a flat `Record<string, unknown>`, which `McpToolResult` satisfies through its own explicit index signature, so the *value* of `structuredContent` was never checked against the narrower field type the SDK declares. The guard stayed green while every display helper was unassignable. It now also asserts against a stand-in that keeps the field shape, covering all five helpers and the typed-handler return position.
130
+
131
+ ### Internal
132
+
133
+ - **`registeredComponentTypes()` on the renderer** returns every type the registry can render. The registry is the authority on that set, and nothing could read it, which is why a second copy existed to drift from. `test/component-types.test.ts` uses it to fail the build when the generated list and the registry disagree, in either direction, so a stale committed generated file cannot be merged.
134
+ - **One `escapeHtml` in `src/core/escape.ts` replaces three private copies** in `src/app.ts`, `src/mcp/resource.ts` and `src/renderer/components/typography.ts`, plus an `escapeAttr` in the first of those. The four escaped three different character sets (`& < > "`, `& < >`, and `& "`). Each was correct for its own context, and `resource.ts` was deliberately splitting text-content from attribute escaping, so this was not a live vulnerability, but nothing enforced any of it. The shared function covers `& < > " '`, which is safe in element text and in attribute values of either quote style, so callers no longer reason about context. Note for anyone narrowing it later: `renderMarkdownToHtml` escapes each line *before* parsing link and image URLs, so the up-front escape is what neutralizes `"` in `href`/`src`; a text-content-only escape there would open an attribute break-out. `test/escape.test.ts` pins the character set.
135
+ - Scoped the `consistent-type-definitions` exception per-declaration for the two wire types rather than per-file as `src/mcp/types.ts` does. `src/app.ts` and `src/mcp/display.ts` are mostly domain types that should stay interfaces, so flipping either file would invert the default for the wrong declarations. Moving the two types into `src/mcp/types.ts` was considered and rejected: that file has no imports by design, and `PrefabWireFormat` needs `ComponentJSON`, `ActionJSON`, `ColorMode` and `LayoutHints`, which would also make it cyclic with `src/app.ts`.
136
+
137
+ - **Staging CI is now the org pattern: push to `dev` keeps a promote PR open, merging it releases.** `auto-pr.yml` creates or refreshes a standing `dev → main` PR on every push to `dev`, and `release.yml` gained an `on: push: branches: [main]` trigger, so shipping is a one-click merge instead of a hand-written PR plus a manual `workflow_dispatch`. Copied from the sibling implementation in `Max-Health-Inc/mcp-http` rather than calling `Max-Health-Inc/.github`'s reusable `create-pr.yml`: that repo is private and this one is public, and GitHub does not let a public repo consume a reusable workflow from a private one. Tests are not duplicated into the PR workflow, since `ci.yml` already runs on the same push and reports onto the PR.
138
+ - **A merge that documents nothing now skips the release instead of failing it.** `release.yml` splits into a `gate` job and a `release` job; the gate uses the new `--check` flag on `scripts/changelog-release.ts`, which reports `promoted` / `present` / `missing` on stdout, writes nothing and always exits 0. A README or CI-only merge to `main` therefore leaves `main` green rather than red, while an explicit `workflow_dispatch` with an empty `[Unreleased]` still fails loudly, because asking for a release and having nothing to ship is an error. `--check` was added to `mcp-http`'s port of this script and is ported back here, where the script originated. Note the plain invocation was never a dry run: it rewrites `CHANGELOG.md` in place.
139
+ - **The release now merges itself back into `dev`.** The release commit promotes `[Unreleased]` to a version heading on `main`; leaving `dev` behind means its still-open `[Unreleased]` and `main`'s new heading differ only in where the heading sits, so git's line-based merge files `dev`'s newer entries under the *older* release heading. That silently attributes unshipped work to a published version and empties `[Unreleased]`, so the next merge skips its release. It happened between v0.3.6 and v0.3.7 and had to be repaired by hand. The sync step is best-effort and never fails a completed release; a conflict is reported in the run summary instead.
140
+ - Bumped `softprops/action-gh-release` to v3, which clears the Node 20 deprecation warning the v0.3.7 release run emitted.
141
+ - Bumped `actions/checkout` to v7 (six call sites across `auto-pr`, `ci`, `docs`, `publish` and `release`) and `actions/setup-node` to v7. Both were two majors behind. `setup-node` v7 drops its dummy `NODE_AUTH_TOKEN` export, which `publish.yml` never relied on: it publishes through OIDC trusted publishing (`id-token: write` plus `npx npm@latest publish --provenance`), the pattern that release documents. `oven-sh/setup-bun@v2` and `peaceiris/actions-gh-pages@v4` are on their current majors and unchanged.
142
+ - **Added `.github/dependabot.yml`, targeting `dev`.** Now that `release.yml` runs on `push: branches: [main]`, a Dependabot PR merged to `main` would cut a release for a routine bump, so the updates land on `dev` and ship through the standing promotion PR. Grouped per ecosystem, one PR a week each. Adapted from the sibling config in `mcp-http`, with a correction: TypeScript major bumps are ignored because `typescript-eslint@8.65.0` declares `peer typescript: >=4.8.4 <6.1.0` (verified against the installed tree, on `typescript` 6.0.3), and a grouped PR that fails every week would block the other dev-dependency updates. Worth knowing that the cap is `<6.1.0`, so even a minor bump to 6.1 falls outside the supported range.
143
+
144
+ ### Docs
145
+
146
+ - **The npm package page now links to `https://maxhealth.tech/prefab` as its homepage.** `package.json` carried no `homepage`, so npm fell back to `repository` + `#readme` and sent every visitor to the raw README on GitHub instead of the documentation site. `bugs.url` is set alongside it, which npm was likewise deriving from `repository`. Both fields are read from the published tarball, so the listing only changes on the next release.
147
+ - Removed a stale README blockquote that restated the superset relationship six lines below the tagline already stating it, and pinned it to upstream `v0.19.1`. That pin predated 0.3.0's catch-up to upstream 0.20.x and contradicted the wire-format reference. The chart-formatting caveat it carried still lives in the Chart Formatting section, next to the props it describes.
148
+
149
+ ## [0.3.7] — 2026-08-01
150
+
151
+ ### Fixed
152
+
153
+ - **The MCP protocol types could not be returned to an SDK handler without a cast.** Every shape in `src/mcp/types.ts` was an `interface`, and the SDK's result types are passthrough (`{ [x: string]: unknown }`). TypeScript grants an implicit index signature only to aliases of object types, never to interfaces, whose key set is not final because declaration merging can reopen them ([microsoft/TypeScript#15300](https://github.com/microsoft/TypeScript/issues/15300), closed as by-design), so returning a `McpResourceReadResult` from a `resources/read` handler failed with "Index signature for type 'string' is missing" — the one thing the type exists for. They are all type aliases now. `McpToolResult` was unaffected because it carries an explicit index signature. Verified against `@modelcontextprotocol/server` 2.0.0 rather than a stand-in, and guarded by `test/mcp-types.test.ts`, which fails `typecheck` (not at runtime) if any of them reverts to an interface.
154
+ - `McpTextResourceContents`, `McpBlobResourceContents`, `McpImageContent` and `McpResourceContent` are now exported from the package root, not just from `/mcp`. `McpResourceReadResult`'s default type parameter references the contents types, so naming it from a root import was impossible.
155
+
156
+ ### Internal
157
+
158
+ - Scoped `consistent-type-definitions` to `'type'` for `src/mcp/types.ts` only. The repo default stays `'interface'`, which matches the prevailing TypeScript style and typescript-eslint's own recommendation; flipping all 151 interfaces in `src/` would be churn for a problem that only bites where a type must satisfy a foreign passthrough shape. Scoping it means the linter enforces the correct form for anything added to that file rather than pushing the next author into the bug and then into a per-line disable.
159
+
160
+ ## [0.3.6] — 2026-07-31
161
+
162
+ ### Fixed
163
+
164
+ - **A scheme set on the prefab container, rather than on `<html>`, now flips prefab's own tokens too.** `setThemeAttrs` themes the mount container (`applyMode` sets both it and `documentElement`, and `createThemeToggle({ syncDocument: false })` sets *only* the container), and the compiled wire theme has always matched at any depth via `.dark, [data-theme="dark"]`. But every scheme block in `prefab.css` was gated on `:root[…]`, so the two sheets disagreed about depth: a container-scoped toggle applied the theme's dark values over prefab's *light* `--background` and `--foreground`, a half-flipped panel. The base sheet's explicit dark and light blocks now carry the same three selectors the compiled theme emits (`:root[data-theme=…]`, the bare attribute, and the `dark`/`light` class), so both flip on the same element. The `:root[…]` branch is retained, so root-level behaviour and its specificity tie against the `prefers-color-scheme` block are unchanged; only elements below the root gain the tokens they were already asking for. Two schemes on one page work as a consequence.
165
+ - **Inline `Svg` authored without `xmlns` now renders.** `renderSvg` parses with `DOMParser` `image/svg+xml` (strict XML), where a missing `xmlns` produced namespace-less elements that rendered at 0×0. The renderer now injects `xmlns="http://www.w3.org/2000/svg"` when it's absent (`xmlns` is optional in HTML, so authors routinely omit it).
166
+ - The MCP Apps `ui/initialize` handshake reported a stale hard-coded app version (`0.2`) in `appInfo`. It now uses the real package `VERSION`, so a host sees the actual prefab version. (This is the *app/package* version, distinct from the `0.3` wire-format version in `$prefab.version` and the `2026-01-26` MCP Apps protocol version.)
167
+
168
+ - **Object values no longer render as `[object Object]`.** Every value that reaches a table cell, chart label, axis tick, tooltip title, heading, `{{ }}` interpolation or state-bound form input is typed `unknown` (it comes from JSON), and each site coerced it with a bare `String()`. Objects and arrays of objects therefore rendered as `[object Object]`. All of those sites now go through the new `stringifyValue` helper, which emits compact JSON for objects (honouring `toJSON()`), ISO strings for dates, and comma-joined members for arrays.
169
+ - **Identity comparisons on object values no longer collide.** `DataTable` row-selection highlighting, the `find:` pipe's key map and `Collection.firstKey()`/`lastKey()` used the same `String()` coercion to build lookup keys, so *every* object value produced the identical key `[object Object]` and matched every other object. They use `stringifyValue` now, which is injective for distinct JSON values.
170
+
171
+ - **Custom pipes now work in hosts whose CSP forbids eval.** Wire pipes arrive as function source and were always hydrated with `new Function()`, which throws in a sandboxed iframe or VS Code webview without `'unsafe-eval'`, so every custom pipe silently degraded to the raw value. A pipe that is already registered locally (by a companion script loaded through `rendererHtml({ scripts })`) is now kept instead of being re-evaluated, so pre-registering is all it takes. The eval failure is also reported once per mount with an actionable message, rather than once per pipe as an opaque `EvalError`.
172
+ - **`registerViewerResource` emits the cache fields required by protocol revision 2026-07-28.** `resources/read` results extend `CacheableResult` (SEP-2549), and a result without `ttlMs`/`cacheScope` falls back to `{ ttlMs: 0, cacheScope: 'private' }`, meaning the viewer was re-fetched on every render. The viewer HTML is a pure function of the package version (the CDN base pins it), so it now ships `{ ttlMs: 86_400_000, cacheScope: 'public' }`, overridable via the new `cache` option and validated so an out-of-range value throws instead of being silently discarded.
173
+ - **`registerViewerResource` prefers `registerResource()` over the retired `resource()` overload.** `resource()` is deprecated in SDK v1 and gone in `@modelcontextprotocol/server` v2; `resource()` is now only a fallback, and a server exposing neither raises a clear `TypeError`.
174
+
175
+ ### Added
176
+
177
+ - **`rendererHtml({ themeBridge: 'vscode' })` and the same option on `registerViewerResource`.** `prefab.css` resolves each token as MCP Apps host variable, then VS Code webview variable, then static default, so a host that defines the MCP Apps `--color-*` variables shadows the VS Code ones entirely and the viewer stops following the user's editor theme. The bridge emits a `<style>` block after `prefab.css` re-declaring the affected tokens with `--vscode-*` first and the MCP Apps layer dropped, keeping prefab's own fallbacks so nothing changes outside VS Code. Head order is the contract: base theme, bridge, then the caller's `stylesheets`. It covers `:root` and the `prefers-color-scheme: dark` block but deliberately not `:root[data-theme="dark"]`, which is the standalone dark palette (host theming cascades, the manual toggle does not). `VSCODE_BRIDGE` is the exported mapping table, and `test/theme-bridge.test.ts` cross-checks every entry against `prefab.css` so the bridge cannot drift from the theme it overrides.
178
+ - `registerViewerResource` declares the `io.modelcontextprotocol/ui` extension capability (SEP-2133) on the server, since MCP Apps is an extension to the core protocol rather than part of it. Opt out with `declareCapability: false`. Registering on an already-connected server logs a warning instead of throwing, and still registers the resource. (This entry said `…/apps` while the constant held that value; both are corrected. `…/ui` is what the MCP Apps spec defines and what `ext-apps` exports as `EXTENSION_ID`, so the old value declared the capability under a key no host looks up. Never published, so there is nothing to migrate.)
179
+ - `toolResult(payload)` builds the MCP tool-result envelope (`content[0].text` plus `structuredContent`) that the `display_*` helpers return, exported for handlers that already have wire JSON and were hand-rolling the same shape.
180
+ - New MCP protocol types: `McpCacheScope`, `McpCacheHint`, `McpResourceReadResult`, `ResourceConfig`, `ResourceReadHandler`, and `MCP_APP_MIME` / `APPS_EXTENSION` / `DEFAULT_VIEWER_CACHE` constants.
181
+
182
+ ### Internal
183
+
184
+ - Tracked `brandc` 0.6.0, which stops `@property`-registering the scheme-dependent colour tokens so their `light-dark()` resolves against the element that reads it rather than once at `:root` (max-network/brandc#14). That only changes `toCss` output, which prefab does not consume — it compiles its own CSS from the wire `theme` — so the drift guard passes unchanged. Worth knowing anyway, because `setThemeAttrs` accepts any element: a host that themes a container rather than `documentElement` now gets the subtree themed correctly if it loads brandc's stylesheet.
185
+ - Added `stringifyValue` (`src/core/stringify.ts`, exported from the package root) as the single value-to-string coercion for display and identity, replacing 34 ad-hoc `String(unknown)` call sites across 11 modules.
186
+ - Bumped dev tooling: `eslint` 10.8.0, `typescript-eslint` 8.65.0, `happy-dom` 20.11.1, `@types/bun` 1.3.14, `jiti` 2.7.0, `typedoc` 0.28.20, `vitepress-plugin-llms` 1.13.4. The `typescript-eslint` bump is what surfaced the `String(unknown)` and `cacheScope` defects above, via `no-base-to-string` and `no-unnecessary-condition`.
187
+ - Centralized `VERSION` and `PROTOCOL_VERSION` into `src/core/version.ts` (a zero-import single source of truth) so the lean renderer bundle can read the version without pulling in the builder. `release.yml` now bumps that file.
188
+ - `structuredContent` on `McpToolResult` is now a type parameter carrying the payload type (SEP-2106 loosened the field to any JSON value), which removed the five `as unknown as Record<string, unknown>` casts in the display helpers and lets callers read the wire back without casting.
189
+ - Split `src/mcp/display.ts` (606 lines) into `display.ts` (the builders), `resource.ts` (`ui://` registration, CSP meta, viewer HTML) and `result.ts` (the shared envelope). All five `display_*` builders constructed the same envelope inline; they now share one, so protocol-level result fields have a single home.
190
+ - Confirmed `resultType` (SEP-2322) is not ours to set: `@modelcontextprotocol/server` treats it as a wire-only key, stamps it at its 2026-era encode seam and strips it before results reach consumers, which is why its public result types omit it. The one handler-authored case is `resultType: 'input_required'` for multi-round-trip results, which no display helper produces.
191
+ - Tracked `brandc` 0.5.0, which declares the token contract explicitly instead of deriving it from one brand (max-network/brandc#13, filed from prefab's side while writing the guard below). The guard now iterates the exported `BRANDS` rather than naming brands, splits scalars from colours using `CONTRACT_SCALARS`, and adds two checks the new API makes possible: prefab reads no token that left the contract (`DEPRECATED_TOKENS`) and none of any brand's private extras (`brandExtras`), either of which would tie prefab to a single brand.
192
+ - Pinned prefab's design tokens to the shared `brandc` contract with a drift guard (`test/brand-contract.test.ts`). All 30 token names `prefab.css` defines are already contract names, so the test asserts that and the reverse direction: every token prefab reads is covered by each shipped brand, in `:root` for scalars and in both schemes for colours. brandc is a devDependency only. prefab keeps zero runtime dependencies, and the token *values* stay as host-adaptive `var()` fallback chains (MCP Apps var, then VS Code var, then static default) rather than adopting brandc's literal `oklch()` values, which would break host theming and change the default look. `toPrefabTheme()` already emits prefab's exact wire `theme` shape, so a brand drives prefab with no adapter.
193
+ - `test/fixtures/run_fixtures.py` skips regeneration with a notice when the `prefab_ui` Python package is absent, instead of failing and blocking `bun run test`. Its status output is also written as UTF-8, since a cp1252 Windows console raised `UnicodeEncodeError` from inside the error handler and masked the real failure.
194
+
195
+ ### Docs
196
+
197
+ - Rewrote the playground example descriptions in plain language for first-time viewers, dropping internal jargon (`Define`/`Use`/`Condition`, `display_update`, "mock MCP tool", `onChange`/`setState`, "wire format") in favor of what the user sees and does. The Composition example is restructured into two labeled sections with natural labels ("Grade:", "Note:") instead of "Condition:" / "If / Elif / Else:".
198
+
199
+ ## [0.3.5] — 2026-07-22
200
+
201
+ ### Fixed
202
+
203
+ - `validateWireFormat` no longer false-flags legitimate component slots. `trigger` (Dialog), `empty` (Detail/MasterDetail), `else` (Condition), and `summary` (ExpandableRow) are read by the renderer as component nodes, so they are no longer reported as misplaced children.
204
+ - **`Popover` and `HoverCard` now accept a `trigger` component** (like `Dialog`). Previously they rendered all children into hidden content with no visible trigger, so there was nothing to click or hover. The `trigger` is rendered as the always-visible target; `children` are the content.
205
+ - **`Slot` now receives injected content from `Use`.** A `Use` node's own `children` fill the default slot, and a `slots` map fills named slots, so a `Slot` inside a `Define` template renders the caller's content instead of only its fallback.
206
+
207
+ ### Docs
208
+
209
+ - Added playground showcase examples: **Chart Gallery** (all six chart types as native SVG), **Analytics Dashboard** (KPI metrics, charts, tabs, and range controls driving reactive state), and **Interactive Components** (Dialog, Accordion, and Switch/Slider bound to a live readout).
210
+ - Added **MCP app examples** with an in-page mock MCP server, so the playground demonstrates the round-trip (not just on-demand rendering): **MCP: Tool Call** (`callTool` → `display_update`, server-authoritative counter), **MCP: Form → Tool** (form submit → tool → updated list), and **MCP: Live Data** (`Subscribe` with polling fallback → live metrics and chart). Modeled on the `_action` + `chess://` subscription pattern from real prefab MCP apps.
211
+ - Added coverage-filling examples that raise demonstrated component coverage from ~37% to ~60%: **Master / Detail** (`MasterDetail` + `Detail` + selection), **Form Controls** (`RadioGroup`, `Combobox`, `DatePicker`, `Switch`, `Slider` with a live readout), **Composition & Control Flow** (`Define`/`Use` template reuse + `Condition` + `If`/`Elif`/`Else`), and **Content & Media** (`Markdown`, `Code`, `Kbd`, inline `Svg`, `Ring`, `Sparkline`, `RadialChart`, `Histogram`, `Tooltip`, `Carousel`).
212
+ - Corrected the playground's AI system prompt: it now teaches the `0.3` wire format, the real component and action set, control flow, and pipes, replacing the stale `0.2` guidance (`showToast` `title`, `appendState` `item`, and non-existent components).
213
+
214
+ ### Internal
215
+
216
+ - Expanded regression coverage with TDD probes across formatting pipes, expressions, state ops, `ForEach`, the semantic `Table`, `DataTable` interactions (accessor, row click, search), and chart edge cases (empty data, single point), alongside tests for the `Popover`/`HoverCard`/`Slot` fixes above.
217
+
218
+ ## [0.3.4] — 2026-07-22
219
+
220
+ ### Fixed
221
+
222
+ - Action dispatch failures are no longer silent. Every fire-and-forget handler (`onClick`, `onMount`, `onChange`, `onSubmit`, `onRowClick`, keyboard bindings, interval ticks, and `display_update` actions) now routes rejections through the logger via a new `fireAndForget` helper, so a throwing action surfaces instead of vanishing. The async actions (`toolCall`, `fetch`, `callHandler`, `subscribe`) report through a shared `reportActionError`: at `debug` when an `onError` handler is wired, at `warn` when none is, so an unhandled failure is never fully quiet.
223
+
224
+ ### Internal
225
+
226
+ - The release workflow now requires a changelog entry: it fails if `## [Unreleased]` has no entries for the version about to ship, and otherwise promotes that section to the new version heading (`scripts/changelog-release.ts`). This keeps the changelog from drifting behind published releases.
227
+
228
+ ## [0.3.3] — 2026-07-22
229
+
230
+ ### Added
231
+
232
+ - **Centralized logger** (`src/core/logger.ts`), zero runtime dependencies. Mirrors the `@maxhealth.tech/utils` `createLogger` contract (scoped `error`/`warn`/`info`/`debug`, `[prefab]` / `[prefab:scope]` prefix) and adds a runtime `setLogLevel()` so an embedding host can mute or raise prefab's console output. All internal `console.*` calls now route through it. New exports: `createLogger`, `log`, `setLogLevel`, `getLogLevel`, `LogLevel`, `Logger` (and `window.prefab.setLogLevel`).
233
+ - **Wire validation now catches silent authoring mistakes.** `validateWireFormat` flags child components placed under a non-`children` key (e.g. `then`/`else`/`body`), which the renderer never reads, and requires a non-empty `message` on `showToast`. `PrefabRenderer.mount()` runs validation and reports any problems as non-fatal console warnings (opt out with `{ validate: false }`), so a payload that renders nothing now tells you why. `validateWireFormat` / `isValidWireFormat` are also exported from the renderer entry.
234
+
235
+ ### Fixed
236
+
237
+ - **DataTable renders primitive rows.** A table over an array of strings or numbers rendered blank cells, because `row[column.key]` is undefined on a primitive. A primitive row is now used directly as the cell value, so a plain string list displays.
238
+
239
+ ### Docs
240
+
241
+ - Repaired the playground examples, each verified against the live renderer: Conditional UI declared its branches under a non-existent `then` key (renamed to `children`), Todo List could not display its string tasks through a DataTable (now a `ForEach` list), and the Contact form toast passed `title` instead of `message`. The playground's own toast handler read `t.title` (never present on `ToastEvent`) and now reads `t.message`. All example payloads bumped to wire version `0.3`.
242
+ - Reframed the project tagline across the README and the site description: from a declarative UI component library "wire-compatible with" PrefectHQ's Python `prefab-ui` to a full-stack framework and a superset of it.
243
+
244
+ ## [0.3.2] — 2026-07-19
245
+
246
+ ### Fixed
247
+
248
+ - **Badge no longer stretches to full width.** `.pf-badge` is `inline-flex`, but as a flex child it was blockified and stretched to the container's cross size (full width inside a column, full height inside a row). It now uses `align-self: flex-start` to stay intrinsically sized; an explicit `w-full` or width utility still overrides it.
249
+
250
+ ### Docs
251
+
252
+ - **Playground preview text contrast.** The mounted preview inherited the playground chrome's light-gray text color, leaving content near-invisible on a light preview. The preview now applies the theme's `--foreground`.
253
+ - **Single-sourced the playground theme state.** The toolbar "Dark" checkbox and the renderer's floating theme toggle were independent (one toggled a class, the other set `data-theme` plus localStorage and document sync) and drifted apart. They now share one source of truth. The demo and playground CDN example pins were bumped from `@0.2` to `@0.3`.
254
+
255
+ ## [0.3.1] — 2026-06-27
256
+
257
+ Documentation and tooling release. No runtime or API changes: `src/` is unchanged aside from the version bump, so the published bundle is functionally identical to 0.3.0.
258
+
259
+ ### Fixed
260
+
261
+ - Corrected the wire-protocol version label across the user-facing docs. The README badge and tagline, the skill header, the homepage feature text, and a version-pin example still advertised `0.2`, but the library has shipped protocol `0.3` since 0.3.0 (the renderer still accepts legacy `0.2` payloads).
262
+
263
+ ### Docs
264
+
265
+ - Deduplicated the Guide and Reference sections. Each reference topic now lives in one place, and the Guide pages are concise conceptual pages that link into the reference, removing a large amount of duplicated content.
266
+ - Single-sourced the example UIs. The demo and playground now load one canonical set of `$prefab` examples from `docs/public/examples/` at runtime instead of embedding their own copies, and the skill bundle's example assets are generated from that same set.
267
+ - The programmatic API reference (actions, rx, mcp, auto, renderer) is now generated from the source TSDoc with TypeDoc, so it stays in sync with the code. The component catalog, wire format, and MCP Apps pages remain hand-written.
268
+
269
+ ## [0.3.0] — 2026-06-16
270
+
271
+ ### Wire Format (Breaking) — protocol `0.3`
272
+
273
+ Catches up to upstream PrefectHQ/prefab v0.20.x ("Wire Transfer", PR #431). The `$prefab.version` envelope is now **`0.3`**. The renderer still accepts `0.2` payloads, so existing stored wire data keeps rendering.
274
+
275
+ - **New top-level `css` field** — an array of inline CSS blocks, injected as `<style>` tags. The **theme is now compiled into this array** (`:root` for light, `.dark, [data-theme="dark"]` for dark) instead of shipping a structured `theme` object; `PrefabApp.toJSON()` no longer emits a `theme` key. New `compileThemeCss()` / `ThemeVars` exports expose the compiler.
276
+ - **`stylesheets` redefined as external URLs** — rendered as `<link rel="stylesheet" href="…">`. The renderer keeps a heuristic that still injects inline CSS found in `stylesheets` as `<style>` for backward compatibility with `0.2` payloads.
277
+ - **New top-level `mode` field** (`'light' | 'dark'`) — forces a color scheme. The renderer applies it via both the `data-theme` attribute and the `dark`/`light` class (so upstream-compiled `.dark {}` CSS resolves), and `toHTML()` stamps it on `<html>`.
278
+ - **`PrefabApp` / `DisplayOptions`** gain `css` and `mode` options; `display()` merges `css` (concatenated) and `mode` (override) like the other options.
279
+ - `toHTML()` now emits `css`/`stylesheets`/`mode` into `<head>` and strips them from the embedded JSON to avoid double-injection (mirrors upstream `html()`).
280
+
281
+ ### Charts
282
+
283
+ - **`valueFormat` parity (PR #454)** — added the canonical `valueFormat` prop to cartesian/pie/radial charts (value-axis ticks + tooltip values), matching upstream's field name so chart formats are wire-compatible in both directions. `yAxisFormat` / `yAxisRightFormat` remain TS-only overrides for dual-axis charts. The renderer reads `valueFormat` as the base format and treats `"auto"` as "no explicit format".
284
+ - **Pie/Radial/Scatter data-binding parity** — `PieChart` and `RadialChart` now use upstream's `dataKey` (value) + `nameKey` (label) model, and `ScatterChart` gains `xAxis`/`yAxis`/`zAxis` (z = bubble size). The legacy `series`/`xAxis` inputs are still accepted and mapped onto the canonical fields (**dual-accept**, non-breaking), and the Pie renderer reads both shapes — so these charts now round-trip with the Python renderer. New exports: `CategoricalChartProps`, `PieChartProps`, `ScatterChartProps`.
285
+ - **Native Radial, Scatter & Radar renderers** — replaced the "not yet supported" placeholders with real SVG renderers, so **every chart type now draws natively** (the fallback is gone):
286
+ - `RadialChart` — concentric value arcs (muted track + coloured value), configurable `innerRadius`/`startAngle`/`endAngle`.
287
+ - `ScatterChart` — points over min/max axes with optional grid, tooltips, and **bubble sizing** from `zAxis`.
288
+ - `RadarChart` — one polygon per series over angular axes, with `axisKey` spoke labels, `filled`, and `showDots`; legacy `xAxis` maps to `axisKey`.
289
+ - All honour `valueFormat`.
290
+ - **Refactor** — the chart renderers' shared toolkit (colours, axes, SVG primitives, value formatting, legend) was extracted into `chart-helpers.ts` to keep each renderer file under the 700-LOC limit.
291
+
292
+ ### Internal
293
+
294
+ - **DRY theme compilation** — `applyTheme()` (the legacy `theme`-object path) now routes its dark block through the same `compileThemeCss()` the wire path uses, so both paths emit byte-identical dark CSS. The CSS sanitizers are shared too.
295
+
296
+ > **Fixtures verified against upstream.** Golden fixtures were regenerated from upstream `prefab-ui` **0.20.2**: 9/10 are byte-identical to the previous `0.2` fixtures apart from the version bump (confirming parity, and that upstream emits `0.3`). The 10th (chart) now carries upstream's `valueFormat`, which the builder also emits.
297
+
298
+ ## [0.2.40] — 2026-05-15
299
+
300
+ ### New Features
301
+ - **`onHostContextChanged` callback** — `PrefabApp` now exposes `onHostContextChanged((ctx) => {...})` so components can react to arbitrary fields in `ui/notifications/host-context-changed` (e.g. refreshed access tokens, locale, custom app config). The existing `prefab:theme-update` event continues to fire for backward-compat theme handling (closes #14)
302
+ - **`prefab:host-context-changed` bridge event** — the bridge now dispatches the full params object from `host-context-changed` as a separate `prefab:host-context-changed` event, in addition to the existing `prefab:theme-update`
303
+
304
+ ## [0.2.39] — 2026-05-10
305
+
306
+ ### New Features
307
+ - **`display_update()` actions** — `display_update()` now accepts an optional `actions` parameter to fire actions alongside state deltas. Actions execute after the state is merged into the store (closes #13)
308
+ ```ts
309
+ display_update(
310
+ { 'game.turn': 'b', 'game.fen': '...' },
311
+ { actions: [new SendMessage("Your turn. Pick a move.")] }
312
+ )
313
+ ```
314
+
315
+ ## [0.2.38] — 2026-05-09
316
+
317
+ ### Bug Fixes
318
+ - **Fixed**: `display()` now merges options into an existing `PrefabApp` instance — previously `state`, `stylesheets`, `theme`, `layout`, `pipes`, and all other options were silently discarded when a `PrefabApp` was passed. State and defs are shallow-merged (options win on conflict), stylesheets are concatenated, and scalar options (theme, layout, cssClass, onMount) fall back to the app's value (closes #12)
319
+
320
+ ## [0.2.37] — 2026-05-09
321
+
322
+ ### New Features
323
+ - **Reactive `cssClass`** — `cssClass` now accepts `RxStr` (reactive expressions) on all components. Use `rx()` to dynamically set CSS classes based on state (closes #11 part 1)
324
+ - **`onClick` on all components** — `Div`, `Span`, `Column`, `Row`, `Grid`, `GridItem`, `Container`, and all other components now support `onClick` actions. Non-button elements automatically get `role="button"`, `tabindex="0"`, and keyboard (Enter/Space) support for accessibility (closes #11 part 2)
325
+
326
+ ## [0.2.36] — 2026-05-09
327
+
328
+ ### Bug Fixes
329
+ - **Fixed**: Subscribe `onDataCallback` no longer clobbers merged state with raw response data — when `display_update()` merges a state delta via `applyPrefabUpdate()`, `store.set(stateKey, data)` is now skipped, preventing `stateKey` collision with delta keys (closes #10)
330
+ - **Fixed**: ESLint config now ignores `docs/` (VitePress cache/dist) and `eslint.config.ts` — previously caused 60 spurious parsing errors
331
+
332
+ ## [0.2.35] — 2026-05-09
333
+
334
+ ### Bug Fixes
335
+ - **Fixed**: `display_form()` now forwards all `DisplayOptions` — `layout`, `cssClass`, `stylesheets`, `pipes`, `onMount`, `keyBindings`, and `defs` were silently dropped (only `state` and `theme` worked). `DisplayFormOptions` now extends `DisplayOptions`
336
+ - **Fixed**: `display_success` / `displaySuccess` and `DisplaySuccessOptions` are now exported from the package entry point
337
+
338
+ ## [0.2.34] — 2026-05-09
339
+
340
+ ### Bug Fixes
341
+ - **Fixed**: Subscribe fallback poll handler now detects `$prefab` responses — poll results containing full views trigger `remount()`, and `display_update` payloads merge state into the store. Previously poll results were stored as raw data, leaving the DOM frozen (closes #9)
342
+
343
+ ## [0.2.33] — 2026-05-09
344
+
345
+ ### Bug Fixes
346
+ - **Fixed**: `display()` now forwards `stylesheets` and `pipes` options to `PrefabApp` — previously these were silently dropped since `DisplayOptions` didn't include them (closes #8)
347
+
348
+ ## [0.2.32] — 2026-05-09
349
+
350
+ ### Bug Fixes
351
+ - **Fixed**: `If()` / `Elif()` shorthand now correctly detects `Rx` expression objects — the previous inline type check only matched strings and `Ref` (subscribe), silently treating `Rx({ expression })` as a props object
352
+ - **Fixed**: Action handlers (`toolCall`, `callHandler`, `fetch`) now handle `display_update()` state delta payloads (`{ $prefab, update: { state } }`) by merging into the store — previously these were silently dropped while `onToolResult` in auto-mount already handled them correctly (closes #7)
353
+
354
+ ## [0.2.31] — 2026-05-09
355
+
356
+ ### New Features
357
+ - **CallTool structuredContent remount** — when a `toolCall`, `callHandler`, or `fetch` action returns a full prefab view (`{ $prefab, view }`), the renderer remounts with the new view instead of just storing the raw result (#7)
358
+ - Supports direct payloads, MCP `structuredContent` wrappers, and `content[].text` JSON blocks
359
+ - Preserves existing store state across remounts
360
+ - Handles pipes, stylesheets, layout hints, key bindings, defs, and theme on remount
361
+
362
+ ## [0.2.30] — 2026-05-09
363
+
364
+ ### Bug Fixes
365
+ - **Fixed**: Reverted serializer output back to **camelCase** (`cssClass`, `onMount`, `onClick`, `resultKey`, etc.), matching the upstream PrefectHQ/prefab wire format (`by_alias=True`). The snake_case serializer in v0.2.28 was a breaking change based on an incorrect assumption about the wire spec.
366
+ - **Kept**: Renderer normalization layer — `renderNode()` and `dispatchOne()` accept **both** snake_case and camelCase input, so Python-SDK-generated wire data still renders correctly.
367
+ - **Fixed**: `autoTable` column keys now match raw data keys (no `toCamelCase` conversion) — snake_case row keys like `proposed_start` render correctly without column/key mismatch.
368
+ - **Fixed**: `cdnBase()` now uses the **exact** version (`@0.2.30`) instead of a semver range (`@0.2`) — eliminates stale jsDelivr cache serving old renderer bundles after a new patch publish (closes #6)
369
+
370
+ ## [0.2.29] — 2026-05-09 *(unpublished — included the faulty snake_case serializer from v0.2.28)*
371
+
372
+ ### Bug Fixes
373
+ - **Fixed**: `rendererHtml()` CDN URL now derives major.minor from `VERSION` automatically — no more hardcoded `@0.2` constant that would go stale on a minor/major bump
374
+
375
+ ## [0.2.28] — 2026-05-09 *(unpublished — snake_case serializer was a breaking change, reverted in v0.2.30)*
376
+
377
+ ### New Features
378
+ - **`Subscribe` action** — real-time resource updates via `subscribe(uri, stateKey, opts)` with automatic fallback polling when the host doesn't support MCP `notifications/resources/updated` (#3)
379
+ - Supports `fallbackInterval`, `fallbackTool`, `fallbackArgs` for polling-based hosts
380
+ - `onData` / `onError` callbacks for reactive state binding
381
+ - **`PdfViewer` component** — embed PDF documents with `pdfViewer(src, opts)` (#2)
382
+
383
+ ### ~~Wire Format (Breaking)~~
384
+ - ~~**snake_case wire format** — all structural keys in the `$prefab` JSON output are now snake_case~~ *(reverted in v0.2.30)*
385
+ - Renderer normalizes incoming JSON at entry points (`renderNode`, `dispatchOne`), accepting both snake_case and camelCase input for backwards compatibility
386
+ - User-data containers (`state`, `arguments`, `context`, `overrides`) are **not** converted — keys inside them are preserved as-is
387
+
388
+ ### Bug Fixes
389
+ - **Fixed**: `fallbackArgs` reactive expressions now resolve correctly in Subscribe action
390
+ - **Fixed**: resilient cleanup for Subscribe timers and listeners
391
+ - **Fixed**: chart series structural keys (`dataKey` → `data_key`) now correctly serialized
392
+ - **Fixed**: `PrefabApp.toJSON()` root view outputs `css_class` (was bypassing `Component.toJSON()`)
393
+ - **Fixed**: wire format validator accepts both camelCase and snake_case action prop names
394
+
395
+ ### Documentation
396
+ - SEO & LLM visibility improvements for documentation site
397
+
398
+ ### Tests
399
+ - 22 new TDD tests for snake_case serialization and renderer normalization
400
+ - All 10 golden fixtures updated to snake_case wire format
401
+ - **1237 tests** passing across 36 files
402
+
403
+ ## [0.2.27] — 2026-05-02
404
+
405
+ ### Bug Fixes
406
+ - **Fixed**: `resourceMeta()` now serializes permissions as `{}` (empty objects) per the MCP Apps spec, not `true`. The DX-facing input type stays `boolean` for ergonomics; the wire output is now spec-compliant with `McpUiResourcePermissions`
407
+
408
+ ## [0.2.26] — 2026-05-02
409
+
410
+ ### New Features
411
+ - **`rendererHtml(opts?)`** — generates the viewer HTML page string for MCP Apps resources (loads `prefab.css` + `renderer.auto.min.js` from CDN, with optional extra scripts/stylesheets)
412
+ - **`registerViewerResource(server, opts?)`** — one-liner to register a `ui://` viewer resource with correct MIME type, CSP (auto-merged with jsdelivr default + script origins), Permission Policy, and `_meta` on both listing and content item
413
+ - **`PREFAB_RESOURCE_URI`** — default URI constant (`'ui://prefab/viewer'`)
414
+ - All three exported from both `@maxhealth.tech/prefab/mcp` and the main barrel
415
+
416
+ ### Documentation
417
+ - Corrected CSP/permissions claims about Claude Desktop (it DOES enforce CSP via HTTP headers on `{hash}.claudemcpcontent.com`)
418
+ - Corrected cross-server tool call claim (spec says `"app": Tool callable by the app from this server only`)
419
+ - Updated host compatibility table (all hosts enforce CSP)
420
+ - Added `registerViewerResource()` shortcut section to `docs/reference/mcp-apps.md`
421
+ - Added `rendererHtml()`, `registerViewerResource()`, `PREFAB_RESOURCE_URI` reference docs to `docs/reference/mcp-display.md`
422
+
423
+ ### Tests
424
+ - 15 new tests for `rendererHtml()`, `registerViewerResource()`, `PREFAB_RESOURCE_URI` (security escaping, CSP merging, handler invocation)
425
+ - Comprehensive layout hints test coverage (bridge, renderer combos, passthrough, HTML/MCP round-trip)
426
+ - **1174 tests** passing across 32 files
427
+
428
+ ## [0.2.25] — 2026-04-30
429
+
430
+ ### Layout Hints
431
+ - **New**: `LayoutHints` type — `preferredHeight`, `minHeight`, `maxHeight` (px) for declarative host container sizing
432
+ - `PrefabWireFormat.layout` — new optional field in the `$prefab` wire format
433
+ - `PrefabAppOptions.layout` — set directly on the app constructor
434
+ - `DisplayOptions.layout` — set via `display(view, { layout: { preferredHeight: 600 } })`
435
+ - Renderer applies `height` / `min-height` / `max-height` + `overflow: auto` as inline styles on the mount root
436
+ - `Bridge.notifyPreferredSize()` — emits `ui/notifications/preferred-size` (JSON-RPC) or `prefab:preferred-size` (prefab protocol) to the host on mount
437
+ - Auto-mount (`renderer.auto.min.js`) forwards layout hints automatically when wire data contains a `layout` field
438
+
439
+ ### CSS Utilities
440
+ - **Added**: `max-h-*` utility classes (`max-h-48`, `max-h-64`, `max-h-80`, `max-h-96`, `max-h-full`, `max-h-screen`, `max-h-none`)
441
+
442
+ ### Bug Fixes
443
+ - **Fixed**: `DOM.Iterable` added to tsconfig `lib` — resolves `NodeListOf` iteration errors in strict mode
444
+ - **Fixed**: null-safe `.textContent` access in `data.ts` (DataTable search) and `form.ts` (Combobox filter)
445
+ - **0 tsc errors**, **1149 tests** passing across 32 files
446
+
447
+ ## [0.2.24] — 2026-04-30
448
+
449
+ ### Utility Classes
450
+ - **Added**: ~200 Tailwind-compatible utility classes in `prefab.css` — auto-renderers no longer require Tailwind CSS
451
+ - Categories: display, flexbox, gap, padding (px/py/pt/pb/pl/pr scales), margin (mx/my/mt/mb/ml/mr scales), width, height, max-width (xs–7xl), typography (text-xs–4xl, font weights, alignment, line-height, tracking), theme-aware text/bg colors, borders, rounded, shadows, overflow, position, z-index, opacity, cursor, transitions, sr-only
452
+
453
+ ### Theme Fix
454
+ - **Fixed**: reverted to v0.2.20 theme behaviour after regressions in v0.2.21/v0.2.22
455
+ - `applyHostTheme()` sets `data-theme` from host `colorScheme` again — toggle works correctly in hosted mode
456
+ - `syncVsCodeTheme()` restored for standalone VS Code webviews (reads `data-vscode-theme-kind`, MutationObserver)
457
+ - `[data-theme]` blocks use static values only (no host var references) — prevents both blocks resolving identically
458
+
459
+ ## [0.2.22] — 2026-04-29
460
+
461
+ ### Host Theme Adaptation
462
+ - Separated host theming from toggle: `applyHostTheme()` no longer sets `data-theme`
463
+ - Removed `syncVsCodeTheme()` (later restored in v0.2.23)
464
+
465
+ ## [0.2.21] — 2026-04-29
466
+
467
+ ### Host Theme Adaptation
468
+ - **Fixed**: hosted mode (Claude Desktop / VS Code) now adapts to host theme colors correctly
469
+ - `applyHostTheme()` no longer sets `data-theme` — host's inline CSS variables resolve through the `var()` fallback chain in `:root` / `@media dark` blocks automatically
470
+ - Removed `syncVsCodeTheme()` — VS Code's `--vscode-*` vars resolve through the same chain without needing `data-theme`
471
+ - `[data-theme]` blocks use static values only and are reserved for the standalone manual toggle
472
+ - Architecture: hosts control theme via inline vars (cascade), standalone controls via `data-theme` (static) — no conflict
473
+
474
+ ## [0.2.20] — 2026-04-29
475
+
476
+ ### Theme Toggle Fix
477
+ - **Fixed**: dark/light toggle icon flipped but colours didn't change — `@media (prefers-color-scheme: dark) :root:not(...)` at specificity (0,2,0) beat `[data-theme]` at (0,1,0). Bumped to `:root[data-theme="dark/light"]` (0,2,0) so toggle wins by source order
478
+
479
+ ### VS Code Theme Sync
480
+ - `syncVsCodeTheme()` — reads `data-vscode-theme-kind` from `document.body`, maps to `data-theme` on `:root` (`vscode-dark` / `vscode-high-contrast` → `dark`, else `light`)
481
+ - `MutationObserver` watches for VS Code theme switches and keeps `data-theme` in sync automatically
482
+ - Only active in standalone / VS Code context (skipped when MCP Apps bridge is present)
483
+
484
+ ### Tests
485
+ - Added `renderer-destroy.test.ts` — 12 tests for component destroy hook lifecycle
486
+ - **1142 tests** passing across 32 files
487
+
488
+ ## [0.2.19] — 2026-04-29
489
+
490
+ ### SDK Type Compatibility
491
+ - `McpToolResult` now structurally assignable to `@modelcontextprotocol/sdk` `CallToolResult` — no cast needed when returning `display()` from SDK tool handlers
492
+ - Added `[key: string]: unknown` index signature (satisfies SDK's `Result` base)
493
+ - Split `McpResourceContent.resource` into discriminated union (`McpTextResourceContents | McpBlobResourceContents`) matching SDK's `EmbeddedResource`
494
+ - Added optional `annotations?` and `_meta?` on all content types
495
+
496
+ ### Host Theme Token Mapping
497
+ - **CSS fallback chain**: all design tokens in `prefab.css` resolve through 3 tiers — MCP Apps spec vars (`--color-background-primary`) → VS Code vars (`--vscode-editor-background`) → static defaults
498
+ - Applies to all theme blocks: `:root`, `@media (prefers-color-scheme: dark)`, `[data-theme="dark"]`, `[data-theme="light"]`
499
+ - Downstream MCP servers no longer need custom CSS for Claude Desktop or VS Code webview theming
500
+ - Added `--shadow-sm/md/lg` and `--border-radius-*` → `--radius` mapping from MCP Apps spec
501
+ - `applyHostTheme()` now injects host-provided `@font-face` / `@import` CSS from `styles.css.fonts` (idempotent `<style>` tag)
502
+ - `HostTheme.fontCss` field added; extracted from both `ui/initialize` and `ui/notifications/host-context-changed`
503
+
504
+ ## [0.2.18] — 2026-04-29
505
+
506
+ ### Builder API Improvements
507
+ - `display_success(title, body?)` — success-variant alert helper
508
+ - `resourceMeta(opts)` — build `_meta` for `resources/read` (CSP, permissions, domain, border)
509
+ - `PREFAB_CDN_META` — pre-built meta with jsDelivr CDN CSP for common deployments
510
+ - `structuredContent` on all display helpers (`display`, `display_form`, `display_update`, `display_error`, `display_success`)
511
+ - `PrefabApp.toMcpResult()` — returns `{ content, structuredContent }` for direct SDK tool handler return
512
+ - MCP actions: `RequestDisplayMode` action + `displayMode` option on `CallTool`
513
+ - New component support: `Embed`, `Markdown`, `Mermaid`, `CodeBlock` builder classes
514
+
515
+ ### Tests
516
+ - **1130 tests** passing across 31 files
517
+
518
+ ## [0.2.17] — 2026-04-28
519
+
520
+ ### Auto-Resize
521
+ - `Bridge.setupAutoResize(el)` — `ResizeObserver` on the target element, notifies the host whenever the content dimensions change via `ui/notifications/size-changed` (JSON-RPC) or `prefab:size-changed` (prefab protocol). Mirrors the ext-apps SDK `autoResize: true` behaviour without the SDK dependency.
522
+ - `sendRpcNotification()` — fire-and-forget JSON-RPC notification (no `id`, no response expected)
523
+ - `PrefabApp.setupAutoResize(target)` — public API accepting selector or element
524
+ - `renderer.auto.min.js` now auto-observes `#root` after boot — hosts get size updates out of the box
525
+ - Deduplicates identical dimensions, fires initial notification immediately
526
+ - 4 new tests — **1130 total tests**
527
+
528
+ ## [0.2.16] — 2026-04-28
529
+
530
+ ### Docs
531
+ - Added `appInfo` vs `clientInfo` Common Pitfall section to `mcp-apps.md`
532
+ - Added ext-apps SDK vs native `ui/*` JSON-RPC comparison table
533
+ - Added ext-apps SDK source references to Reference section
534
+
535
+ ## [0.2.15] — 2026-04-28
536
+
537
+ ### Bug Fix: Claude Desktop / ChatGPT Breakage
538
+ - **Fixed**: `ui/initialize` handshake sent `clientInfo` instead of `appInfo` — hosts validate with Zod schema that requires `appInfo`, causing silent handshake failure (blank iframe, no error). The ext-apps SDK fallback in v0.2.11 masked this; removing the SDK in v0.2.12 exposed it.
539
+ - `Bridge.initialize()` error handling: `Promise.any` wrapped in try/catch, rethrows as descriptive `Error('Bridge init failed — no host responded')` with `{ cause }` preserving the `AggregateError`
540
+ - 2 new tests: `appInfo` field validation, clear error on dual-protocol failure — **1126 total tests**
541
+
542
+ ## [0.2.14] — 2026-04-28
543
+
544
+ ### Built-in Theme Toggle
545
+ - `createThemeToggle(root, options?)` — renders a floating sun/moon toggle button with two-way sync to `data-theme` attribute via `MutationObserver`
546
+ - `PrefabRenderer.mount()` auto-attaches toggle by default (opt out with `themeToggle: false`)
547
+ - Toggle preserved across re-renders
548
+
549
+ ### Bug Fixes
550
+ - `appendState` action: support `item` alias (Python SDK compat)
551
+ - Charts: restore CSS custom properties in SVG presentation attributes for dark mode
552
+
553
+ ## [0.2.13] — 2026-04-27
554
+
555
+ ### Bug Fixes
556
+ - Renderer: remove all inline `theme-variable` styles, rely on CSS classes (CSP compliance)
557
+ - Badge: use `Partial<Record>` to satisfy `strict-boolean-expressions` lint rule
558
+ - Demo: fix theme toggle, favicon 404, copy-MCP-button styling
559
+ - Docs: inject CDN version from `package.json` at build time
560
+
561
+ ## [0.2.12] — 2026-04-27
562
+
563
+ ### Bundle Size Reduction
564
+ - **Removed** `@modelcontextprotocol/ext-apps` SDK dependency — 405 KB → 80 KB bundle
565
+ - Bridge now speaks native `ui/*` JSON-RPC without the SDK wrapper
566
+ - Docs updated to remove ext-apps references
567
+ - ⚠️ **Regression**: `clientInfo` field name broke Claude Desktop / ChatGPT (fixed in v0.2.15)
568
+
569
+ ## [0.2.11] — 2026-04-27
570
+
571
+ ### Theming
572
+ - `data-theme` attribute support in `prefab.css` — light/dark mode via attribute selector
573
+ - MCP agent skill: attach `prefab-skill.zip` to GitHub releases
574
+
575
+ ## [0.2.10] — 2026-04-27
576
+
577
+ ### Docs & Polish
578
+ - Select options shorthand syntax
579
+ - Remote usage mode documentation
580
+ - Brand assets, favicon, logo paths
581
+ - Playground: dark preview background
582
+ - Lint fixes: `Array<T>` → `T[]`, control char regex, `RegExp.exec`
583
+ - Markdown renderer: protect inline code from formatting, fix CRLF infinite loop
584
+ - DRY: `serializeCallbacks`, camelCase display aliases
585
+
586
+ ## [0.2.9] — 2026-04-26
587
+
588
+ ### Bug Fix
589
+ - `autoTable` column keys now match serialized row keys
590
+
591
+ ## [0.2.8] — 2026-04-26
592
+
593
+ ### Universal MCP Apps Bridge
594
+ - **Fixed**: `renderer.auto.min.js` now works in VS Code, Claude Desktop, ChatGPT, and all MCP Apps hosts without any inline adapter code
595
+ - `Bridge.initialize()` races `prefab:init` and `ui/initialize` JSON-RPC **in parallel** — whichever host protocol responds first wins. Eliminates the 1.5s dead time on JSON-RPC hosts
596
+ - `app()` now buffers `tool-result` events — host can send results before `onToolResult` is registered without data loss
597
+ - `auto.ts` defers `boot()` to `DOMContentLoaded` (or microtask if already loaded)
598
+
599
+ ## [0.2.7] — 2026-04-25
600
+
601
+ ### Chart Formatting
602
+ - Generic pipe formatting for chart axes and tooltips
603
+ - `tooltipXKey` — separate data key for tooltip vs x-axis labels
604
+ - Fix: remove non-null assertion in PieChart tooltip key lookup (lint)
605
+
606
+ ## [0.2.6] — 2026-04-25
607
+
608
+ ### Chart Tooltips
609
+ - Production-quality tooltips with crosshair, data dots, null gaps, a11y, touch support
610
+
611
+ ## [0.2.5] — 2026-04-25
612
+
613
+ ### Custom Renderers
614
+ - `registerComponent(type, renderFn)` exposed on `window.prefab` for custom component renderers
615
+
616
+ ## [0.2.4] — 2026-04-25
617
+
618
+ ### Chart Axes
619
+ - Y-axis, X-axis labels, grid lines, dual Y-axis support
620
+
621
+ ## [0.2.3] — 2026-04-25
622
+
623
+ ### JSON-RPC Protocol
624
+ - Native `ui/*` JSON-RPC protocol — zero-adapter VS Code support
625
+
626
+ ## [0.2.2] — 2026-04-25
627
+
628
+ ### Auto-Mount Bundle
629
+ - `renderer.auto.min.js` — CSP-safe self-executing bundle
630
+ - CI: attach `renderer.auto.min.js` + `prefab.css` to GitHub releases
631
+ - Wire compat: `callTool` action alias + `Condition` component
632
+
633
+ ### Docs
634
+ - Signal, Collection, Ref, sugar actions, find/dot pipes, Detail/MasterDetail, CSS theme, versioned CDN
635
+ - Live playground (Monaco editor + shareable URLs + AI prompt)
636
+
637
+ ## [0.2.1] — 2026-04-25
638
+
639
+ ### Bug Fix: If/Elif/Else Conditional Chains
640
+ - **Fixed**: `Elif` and `Else` nodes rendered independently instead of being consumed by the preceding `If` chain
641
+ - New `renderChildArray()` detects `If/Elif/Else` sibling sequences as a single conditional chain
642
+ - 55 new tests — **913 total tests**
643
+
644
+ ### Bug Fix: Browser Pipe Registration
645
+ - **Fixed**: Custom pipes registered in Node were not available in the browser renderer bundle
646
+ - `PrefabApp({ pipes })` accepts pipe functions, serializes source into wire format
647
+ - Renderer `mount()` hydrates wire pipes via `new Function()` before first render
648
+ - Built-in pipes cannot be shadowed by wire pipes (security)
649
+ - 12 new tests — **858 total tests**
650
+
651
+ ## [0.2.0] — 2026-04-25
652
+
653
+ ### Breaking: Wire Format v0.2
654
+ - `$prefab.version` bumped to `0.2`
655
+ - Initial release of the `0.2.x` series with all v0.1.x features plus the universal MCP Apps bridge
656
+
657
+ ## [0.1.10] — 2026-04-24
658
+
659
+ ### Action-Builder Sugar
660
+ - `set(signal, value)` — ergonomic wrapper for `new SetState(signal.key, value)`
661
+ - `toggle(signal)` — wrapper for `new ToggleState(signal.key)`
662
+ - `append(collection, item, index?)` — wrapper for `new AppendState(collection.stateKey, item)`
663
+ - `pop(collection, indexOrValue?)` — wrapper for `new PopState(collection.stateKey, indexOrValue)`, defaults to last element
664
+ - All helpers accept `Signal`, `Collection`, or raw `string` key via `StateTarget` type
665
+ - `set()` passes through `SetStateOpts` (onSuccess/onError callbacks)
666
+ - 17 new tests in `test/sugar.test.ts` — **846 total tests**
667
+
668
+ ## [0.1.8] — 2026-04-24
669
+
670
+ ### Reactive Primitives
671
+ - `signal(key, initial)` — named reactive scalar for wire format, auto-registers state
672
+ - `collection(key, rows, { key })` — named keyed array, auto-registers state
673
+ - `Ref<T>` — lazy pipe expression referencing a row in a collection via `collection.by(signal)`
674
+ - Typed `Ref.dot(field)` — returns `Ref<T[K]>` with autocomplete on `keyof T`
675
+ - `Ref.formatted(field, pipe, ...args)` — sugar for `.dot(field).pipe(pipe)`
676
+ - `Rx.pipe(name, ...args)` — public variadic pipe builder (was private single-arg)
677
+ - `Ref.pipe(name, ...args)` — delegates to `Rx.pipe()`
678
+
679
+ ### Pipe Extension Point
680
+ - `registerPipe(name, fn)` — global custom pipe registry for companion packages
681
+ - `unregisterPipe(name)` — remove a pipe (tests)
682
+ - `listPipes()` — list registered names (debugging)
683
+ - Built-in pipes always shadow custom pipes (safety)
684
+ - Re-registration warns and overwrites (HMR-friendly)
685
+ - Custom pipes receive variadic parsed args (`| date:'long'`, `| between:1,10`)
686
+
687
+ ### Selection & Master-Detail
688
+ - `DataTable({ from, selected })` — auto-wires `rowKey`, `onRowClick → SetState`, highlight
689
+ - `Detail({ of, empty, children })` — conditional pane, shows children when ref resolves
690
+ - `MasterDetail({ masterWidth, gap, children })` — two-pane flex layout
691
+ - `col()` descriptor overload: `col({ key, header, format, accessor, sortable })`
692
+ - `format` on columns applies pipe (built-in or custom) to cell values
693
+ - `accessor` on columns resolves pipe expressions per cell (`name | humanName`)
694
+
695
+ ### Auto State Collection
696
+ - `signal()` and `collection()` factories auto-register into a global collector
697
+ - `PrefabApp` constructor drains collector — no more `state: { ...c.toState(), ...s.toState() }`
698
+ - Explicit `state` overrides auto-collected on key conflicts
699
+ - Duplicate state keys warn (`[prefab] state key "X" registered multiple times`)
700
+ - `resetAutoState()` exported for test cleanup
701
+
702
+ ### Renderer
703
+ - `find` pipe filter — O(1) keyed lookup with generation-aware cache
704
+ - `dot` pipe filter — extract property from object
705
+ - `Store.generation` counter — monotonically increasing, invalidates find cache on mutation
706
+ - `applyFilter` falls through to custom pipe registry after built-ins
707
+ - `RxStr` widened to `string | Rx | Ref` — Ref works in all component props
708
+
709
+ ### Bug Fixes (TDD)
710
+ - `find` pipe: numeric key coercion (`'2' !== 2`) — fixed with `String()` on both sides
711
+ - `find` pipe: scope dot-path resolution (`$item.managerId`) — walk scope object
712
+ - `find` cache: stale after in-place mutation — fixed with generation counter
713
+ - `Detail`: `0` treated as truthy — added explicit `!== 0` check
714
+ - `col({ format })`: built-in pipes silently ignored — route through `applyFilter`
715
+ - `col({ accessor, format })`: double-applied formatting — skip format when accessor present
716
+ - Duplicate auto-state keys: silent overwrite — now warns
717
+
718
+ ### Tests
719
+ - 829 tests passing across 24 files
720
+ - New: `test/signal-collection.test.ts` (67 tests)
721
+ - New: `test/pipes.test.ts` (17 tests)
722
+ - New: `test/tdd-bugs.test.ts` (6 tests)
723
+
724
+ ## [0.1.0] — 2026-04-20
725
+
726
+ Initial release.
727
+
728
+ ### Component Library
729
+ - Core classes: `Component`, `ContainerComponent`, `StatefulComponent`
730
+ - `PrefabApp` wrapper with `$prefab` v0.2 wire format and `toHTML()` self-contained page export
731
+ - `rx()` reactive expression builder (pipes, comparisons, ternary, built-in vars)
732
+ - 80+ components across layout, typography, card, data, form, chart, control, interactive, media, alert
733
+ - Table components: `Table`, `TableHead`, `TableBody`, `TableFooter`, `TableRow`, `TableHeader`, `TableCell`, `TableCaption`, `ExpandableRow`
734
+ - Form extensions: `Radio`, `RadioGroup`, `Combobox`, `ComboboxOption`, `Calendar`, `DatePicker`, `Field`, `ChoiceCard`, and more
735
+ - Chart extensions: `RadialChart`, `Histogram` (in addition to Bar, Line, Area, Pie, Radar, Scatter, Sparkline)
736
+ - Composition: `Define`, `Use`, `Slot` for template reuse
737
+ - Client actions: `SetState`, `ToggleState`, `AppendState`, `PopState`, `ShowToast`, `CloseOverlay`, `OpenLink`, `SetInterval`, `Fetch`, `OpenFilePicker`, `CallHandler`
738
+ - MCP actions: `CallTool`, `SendMessage`, `UpdateContext`, `RequestDisplayMode`
739
+
740
+ ### MCP Display Helpers
741
+ - `display()`, `display_form()`, `display_update()`, `display_error()`
742
+
743
+ ### Auto-Renderers
744
+ - `autoDetail`, `autoTable`, `autoChart`, `autoForm`, `autoComparison`, `autoMetrics`, `autoTimeline`, `autoProgress`
745
+
746
+ ### Browser Renderer
747
+ - Vanilla DOM renderer — 55+ components, zero framework dependencies
748
+ - Reactive `Store` with get/set/merge/toggle/append
749
+ - Rx expression engine: ternary, logical, arithmetic, 15+ pipes, dot access, scoped variables
750
+ - Action dispatcher — 15 action types
751
+ - MCP transport — HTTP POST to `/mcp/tools/call`
752
+ - Theme engine — CSS custom properties from `theme` field (light/dark)
753
+ - Chart renderer — built-in SVG for Bar, Line, Area, Pie
754
+ - Mermaid integration — delegates to global `mermaid` if available
755
+ - IIFE bundle: `renderer.min.js` (54KB) for `<script>` tag usage, `window.prefab` global
756
+
757
+ ### ext-apps Bridge
758
+ - `app()` one-call factory with PostMessage transport, host theme mapping, lifecycle hooks
759
+ - Capability negotiation, display mode requests, tool input/result/cancelled/partial events
760
+ - Auto-detect environment: iframe → PostMessage, standalone → HTTP transport
761
+
762
+ ### Validation & Accessibility
763
+ - `validateWireFormat()` + `isValidWireFormat()` with detailed error reporting
764
+ - Stylesheet injection — renderer applies `stylesheets` field as `<style>` tags
765
+ - ARIA roles/attributes and keyboard navigation on all interactive components
766
+
767
+ ### Infra
768
+ - GitHub Actions CI (test + build on push) and publish (npm on tag)
769
+ - 253 tests passing across 11 files (570 assertions)
770
+ - MIT license