From 9cd6c894f86d4cd5f4cfaf46ce689bc5953a4bf8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 03:30:57 +0000 Subject: [PATCH] docs(guide): author the button keys the schema actually declares (objectui#7898) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nine JSON fences under content/docs/guide/ authored `button` keys that do not exist on `ButtonSchema`, and no instrument saw them because they all hold LITERALS: the expression-carriage census only judges sites that author `${...}`, and check:doc-types only judges the `type` literal. Six sites spelled the button's text as `text`. Measured from the built `@object-ui/types` dist: `ButtonSchema.shape` has `label` and has no `text`, and `button.tsx` renders `schema.label || renderChildren(...)`. Nothing refuses the misspelling either — `BaseSchema` is `.passthrough()`, so the validator KEEPS the unknown key. Rendered through the real SchemaRenderer, the node with `text` produces a button whose textContent is the empty string. Three sites authored a top-level `onClick`. `ButtonSchema.onClick` is `handlerKeyRefusal('onClick', 'runtime-slot', ...)`, a `z.custom(() => false)` that refuses every authored value by name — and because `onClick` is on SDUI_DOM_PASS_THROUGH_KEYS, an authored string or object is forwarded to the real DOM listener slot, where React throws on the first click. Repaired onto the channel the refusal message itself names and the channel measured to dispatch: an `action:button` node carrying `actionType`. Judged per passage, never one blanket rule: five sites are teaching how a button shows text and take `label`; four are teaching how a button binds an action and become `action:button`. `quick-start.md`'s `events`-bag site is deliberately untouched. The census figure is unchanged by this PR, as predicted — 65 expression sites, 61 carried, 4 uncarried in 3 pages, byte-identical before and after. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3 --- content/docs/guide/architecture.md | 8 +++++- content/docs/guide/layout.md | 37 +++++++++++++++++++------- content/docs/guide/schema-rendering.md | 21 +++++++++++---- 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/content/docs/guide/architecture.md b/content/docs/guide/architecture.md index 35c560cf50..0d1192fbba 100644 --- a/content/docs/guide/architecture.md +++ b/content/docs/guide/architecture.md @@ -243,12 +243,18 @@ ObjectUI includes a powerful expression engine for dynamic UIs: ```json { "type": "button", - "text": "Submit", + "label": "Submit", "visible": "${form.isValid && !form.isSubmitting}", "disabled": "${form.isSubmitting}" } ``` +A button's text key is `label`, and `text` is not a `ButtonSchema` key at all. Nothing +refuses the misspelling either: `BaseSchema` is `.passthrough()`, so the validator KEEPS +the unknown key, and the renderer — which reads `schema.label` — never looks at it. +Measured on the node above with `text`: the button renders with an empty `textContent`, +so it appears on screen as a blank rectangle with no text. + ### Data Transformations ```json diff --git a/content/docs/guide/layout.md b/content/docs/guide/layout.md index f54c7cd887..12b73b1300 100644 --- a/content/docs/guide/layout.md +++ b/content/docs/guide/layout.md @@ -152,13 +152,13 @@ The `Page` component provides a consistent wrapper for individual pages with opt "actions": [ { "type": "button", - "text": "Add Product", + "label": "Add Product", "variant": "default", "icon": "plus" }, { "type": "button", - "text": "Export", + "label": "Export", "variant": "outline", "icon": "download" } @@ -170,6 +170,10 @@ The `Page` component provides a consistent wrapper for individual pages with opt } ``` +`label` is the button's text key — `text` is not a `ButtonSchema` key, and because +`BaseSchema` is `.passthrough()` nothing refuses it: the validator keeps the unknown key +and `button.tsx`, which reads `schema.label`, renders a button with no text. + ### Schema API @@ -516,18 +520,20 @@ Omit `sidebar` and the content fills the width under the top bar. ], "actions": [ { - "type": "button", - "text": "Edit", + "type": "action:button", + "name": "edit_record", + "label": "Edit", "variant": "default", "icon": "pencil", - "onClick": "editRecord" + "actionType": "editRecord" }, { - "type": "button", - "text": "Delete", + "type": "action:button", + "name": "delete_record", + "label": "Delete", "variant": "destructive", "icon": "trash", - "onClick": "deleteRecord" + "actionType": "deleteRecord" } ], "body": { @@ -539,6 +545,19 @@ Omit `sidebar` and the content fills the width under the top bar. } ``` +A button that RUNS something is an `action:button` node, not a `button` carrying an +`onClick`. `ButtonSchema.onClick` is declared as a runtime slot for a host-supplied +function and the zod mirror refuses it BY NAME — JSON has no function value, and no +handler key consumes a declarative action object. The refusal is not the whole cost: +`onClick` is on `SDUI_DOM_PASS_THROUGH_KEYS`, so an authored string or object is +forwarded to the real DOM listener slot, and React throws the moment anyone clicks. +Measured, React's own error: "Expected `onClick` listener to be a function, instead got +a value of `object` type." + +The handler name goes in `actionType`, which `action:button` forwards to the action +runner as the action's type; the runner dispatches to the handler registered under it. +Same spelling as [Record Edit Modes](./record-edit-modes.md). + ## Responsive Behavior The shell has exactly **one** layout breakpoint, at **768px** — Tailwind's `md`, and @@ -650,7 +669,7 @@ Place primary actions in page headers: "type": "page", "title": "Orders", "actions": [ - { "type": "button", "text": "New Order", "variant": "default" } + { "type": "button", "label": "New Order", "variant": "default" } ] } ``` diff --git a/content/docs/guide/schema-rendering.md b/content/docs/guide/schema-rendering.md index 61257c6cb6..3d51712e88 100644 --- a/content/docs/guide/schema-rendering.md +++ b/content/docs/guide/schema-rendering.md @@ -252,15 +252,26 @@ Reference actions in schemas: ```json { - "type": "button", + "type": "action:button", + "name": "call_api", "label": "Click Me", - "onClick": { - "actionType": "ajax", - "api": "/api/action" - } + "actionType": "api", + "endpoint": "/api/action", + "method": "POST" } ``` +Three things about the shape this replaces. A declarative action is its own NODE TYPE, +`action:button` — a plain `button` has no authorable handler: `ButtonSchema.onClick` is a +runtime slot for a host-supplied function, refused by name by the zod mirror, and (being +on `SDUI_DOM_PASS_THROUGH_KEYS`) forwarded straight to the DOM listener slot, where React +throws on the first click: "Expected `onClick` listener to be a function, instead got a +value of `object` type." The execution type is `actionType`, and the built-in vocabulary +is `script` | `url` | `modal` | `flow` | `api` | `form` (plus objectui's `navigation` +alias) — anything else must be a handler your host registered on `ActionProvider`. `ajax` +is neither. And the endpoint key is `endpoint`, with `method`; `api` is not a key any +action renderer forwards. + ## Performance Optimization ### Lazy Loading