From 1f0ca29fdfe7ddea09d91540da8fd4f7b293deb4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 06:56:57 +0000 Subject: [PATCH 01/13] feat(spec): ComponentPropsMap rows for action:button/group/menu/icon and element:definition-list/repeater Six curated objectui public blocks had no row: the props gate skipped the four action:* types and component-type-unknown refused the two element:* lists. Each row is strict from birth, with its key set measured from the renderer read points at the objectui pin. Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW Co-authored-by: Claude --- packages/spec/src/ui/component.zod.ts | 543 ++++++++++++++++++++++++++ 1 file changed, 543 insertions(+) diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index b6d256b45a8..0ab4444d3a9 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -2649,6 +2649,516 @@ export const ElementTextInputPropsSchema = lazySchema(() => strictObject({ aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes'), })); +/** + * ---------------------------------------------------------------------- + * 4b. Curated public blocks that had no row: the `action:*` quartet and the + * two `element:*` lists (#20371) + * ---------------------------------------------------------------------- + * + * objectui's ADR-0080 curated public vocabulary carries six blocks this map + * had no row for — `action:button`, `action:group`, `action:menu`, + * `action:icon`, `element:definition-list`, `element:repeater` + * (`core/src/registry/public-blocks.ts:109-114` at the pin this repo builds + * against, `.objectui-sha` = `f8a9d0fb0`). The missing row failed in two + * different ways: + * + * - The four `action:*` types sit outside every namespace the + * `PageComponentType` enum populates, so the #5068 props gate skipped them + * as unregistered custom strings: any key inside `properties` parsed, + * stored and rode through to the renderer, read or not. + * - The two `element:*` types sit INSIDE a reserved namespace with neither an + * enum member nor a row, so `component-type-unknown` refused the whole node + * (severity error) although objectui registers, publishes and offers both. + * + * A row closes both. `component-type-vocabulary.ts` derives the known set from + * `Object.keys(ComponentPropsMap)`, so the two `element:*` types join the + * `element:` vocabulary through their rows — the `element:metadata_viewer` + * shape, no enum member — and the props gate now dispatches on all six. Their + * three-part evidence (registration, publication, authorship) is written on + * the map rows below, as the vocabulary's string-arm ledger asks of every + * type that enters it without an enum member. + * + * KEY SETS ARE MEASURED FROM THE RENDERERS' READ POINTS at that pin — the + * #7751 / #8691 / #8744 method — never transcribed from objectui's + * `UIActionSchema`, from the registrations' `inputs`, or from this package's + * object-metadata `ActionSchema` (a different declaration: a registered action + * keyed by a required `name`, whose `type` is the executor). Per-key citations + * are in each schema's docblock; where a declaration disagrees with the read, + * the read wins and the disagreement is written beside the key. + * + * VALUE posture is #7751's. A key the renderer interprets itself gets a value + * schema from that read (the Button primitive's own vocabulary where it is + * handed straight to `Button`). A key the renderer only FORWARDS to the action + * runner (`execute({ ...forwarded, ...localContext })`) gets the scalar the + * runner's `ActionDef` types it as, and `z.unknown()` where `ActionDef` types + * it as a spec-derived block (`bodyShape`, `onSuccess`, `resultDialog`, …) — + * tightening those is a later ratchet with its own inventory. + * + * Deliberately NOT declared on the action rows, each with its reason: + * + * - `className` / `style` / `id` — read, but node keys (`COMPONENT_NODE_KEYS`). + * - `enabled` — the renderers' legacy fallback beside `disabled`: a + * back-compat read for stored documents, not a second authorable spelling + * (the #5775 `body` precedent). Refused with a prescription. + * - `autoTrigger` — a host transport flag; `auto-trigger.ts:18-19` says it is + * "NOT persisted metadata" and "hosts its only producers". Refused with a + * prescription. + * - `data` / `context` — the host's row and execute-context channels (React + * props the renderers take by name), not authorable surface. + * - `onClick` — a function; only a code-composed schema can carry one. + * - `objectName` — not read at the pin. objectui's `origin/main` at the time + * of this measurement (`9f0c84a`) forwards it on all four renderers, so the + * row gains it with the pin bump that carries that read, not before. + */ + +/** What an undeclared key on one of the four `action:*` blocks met before its row. */ +const actionBlockHistory = (type: string) => + `Until \`${type}\` had a ComponentPropsMap row, the authoring gate skipped it as an unregistered ` + + "type: every key inside `properties` parsed clean, was stored, and reached objectui's renderer, " + + 'which ignored any key it does not read.'; + +/** What happened to one of the two `element:*` lists before its row. */ +const elementListHistory = (type: string) => + `Until \`${type}\` had a ComponentPropsMap row, the whole node was refused as an unknown ` + + '`element:` type although objectui renders it, so no key inside `properties` was ever judged.'; + +/** + * The action-level condition shape the renderers evaluate — `visible` on all + * four, `disabled` on `action:button` / `action:icon`: a boolean literal, a + * CEL string (normalized to the `{ dialect, source }` envelope on parse), or + * the envelope itself. Every read goes through `toPredicateInput`, which takes + * exactly those three arms; it is the same union `ActionSchema`'s own + * condition keys and `record:alert.visible` declare. + */ +const actionCondition = () => + z.union([z.boolean(), EvaluatedExpressionInputSchema], { + error: (issue) => evaluatedExpressionUnionRefusal(issue.input), + }); + +/** objectui's `Button` primitive vocabulary (`ui/button.tsx:19-35` at the pin). */ +const BUTTON_PRIMITIVE_VARIANTS = ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link'] as const; +const BUTTON_PRIMITIVE_SIZES = ['default', 'sm', 'lg', 'icon'] as const; + +/** + * The action-row aliases shared by `action:button` and `action:icon`. + * + * - `type` → `actionType`: `type` is the SDUI envelope's component + * discriminator, and the hoist refuses to copy a `properties.type` onto the + * node, so an executor written there is read by nothing. objectui renamed + * the input to `actionType` with no alias and no transition window + * (objectui#7415, objectstack#14490 ruling A); an author copying an + * `ActionSchema` entry, whose executor IS `type`, brings that spelling along. + * - `visibleWhen` / `visibility` → `visible`: the `record:alert` pair. The + * renderer evaluates `visible` itself, so an author bringing the node + * spelling down into this bag is reaching for exactly that key. + */ +const ACTION_NODE_ALIASES = { type: 'actionType', visibleWhen: 'visible', visibility: 'visible' } as const; + +/** + * Read-but-not-authorable keys shared by `action:button` and `action:icon`, + * each read by the renderer and each refused here with what to write instead. + */ +const ACTION_NODE_GUIDANCE = { + enabled: '`enabled` is the renderer\'s legacy fallback for stored documents, not an authorable ' + + 'key: write `disabled` instead, with the condition inverted — `disabled` is the predicate ' + + 'that greys the action out when it evaluates TRUE.', + autoTrigger: '`autoTrigger` is a host transport flag, not metadata: a host sets it on a schema it ' + + 'composes at runtime to run the action once on mount (a deep link that asks for it). ' + + 'Authored into a page, it would run the action on every page load. Remove it.', +} as const; + +/** + * `action:button` — a button that runs one action through objectui's action + * runner (`components/src/renderers/action/action-button.tsx` at the pin). + * Read points, per key: + * + * - `name` — `:118` (the visibility-diagnostic label, `schema.name ?? + * schema.label`) and `:188` (forwarded). OPTIONAL, as it is read: an inline + * page button is not a registered object action, and the runner takes a + * nameless action on its `type` leg. `UIActionSchema` declares it required; + * the read does not. + * - `label` — `:346`, placed as a React child (so a literal string: an inline + * locale map is not resolved on this path; the translation bundle's + * `components..label` is the localization channel); also `:118`, `:192`. + * - `icon` — `:133`, through the shared `resolveIcon`. + * - `actionType` — `:187`, forwarded as the runner's `type`. + * - `variant` / `size` — `:136` / `:137`: `primary` → the primitive's + * `default` and `md` → `default`, everything else handed to `Button` as-is. + * - `visible` / `disabled` — `:116` + `:298` / `:129` + `:334`, evaluated + * against the row the host binds (`usePredicateRecordContext`). + * - Forwarded to the runner (`:153-155`, `:187-270`): `params`, `description`, + * `target`, `openIn`, `endpoint`, `method`, `bodyExtra`, `bodyShape`, + * `operation`, `patch`, `confirmText`, `successMessage`, `errorMessage`, + * `refreshAfter`, `undoable`, `recordIdField`, `locations`, `toast`, + * `resultDialog`, `onSuccess`. + * + * The registration's `inputs` (`:358-379`) publish seven of these twenty-eight + * keys; the other twenty-one are read and unpublished. It also publishes + * `className`, which is read but is a node key. + */ +export const ActionButtonPropsSchema = lazySchema(() => strictObject({ + surface: 'this `action:button`', + history: actionBlockHistory('action:button'), + guidanceSets: [COMPONENT_NODE_KEYS_GUIDANCE], + aliases: ACTION_NODE_ALIASES, + guidance: ACTION_NODE_GUIDANCE, +}, { + name: z.string().optional() + .describe('Action name, forwarded to the action runner. Optional: an inline page button is not a registered object action, and without a name the runner dispatches on `actionType` alone'), + label: z.string().optional() + .describe('Button text. A literal string, placed as-is — localize through the translation bundle entry for this component id, not an inline locale map'), + icon: z.string().optional() + .describe('Lucide icon name drawn left of the label, resolved through the shared action-icon resolver (an unknown name draws no icon)'), + actionType: z.string().optional() + .describe('Executor the action runner dispatches to — built in: `script`, `url`, `modal`, `flow`, `api`, `form`; a handler registered under another name is dispatched too. Write it here, not as `type`: on a page component `type` is the component itself'), + variant: z.enum([...BUTTON_PRIMITIVE_VARIANTS, 'primary']).optional() + .describe('Button variant — the Button primitive\'s vocabulary, plus `primary`, which renders as `default` (renderer default: `default`)'), + size: z.enum([...BUTTON_PRIMITIVE_SIZES, 'md']).optional() + .describe('Button size — the Button primitive\'s vocabulary, plus `md`, which renders as `default` (renderer default: `default`)'), + visible: actionCondition().optional() + .describe('Visibility predicate — a boolean, a CEL string, or a `{ dialect, source }` envelope, evaluated against the row the host binds; the button is not rendered when it is FALSE, and a predicate that fails to evaluate hides it. Omit for always-visible'), + disabled: actionCondition().optional() + .describe('Disabled predicate — a boolean, a CEL string, or a `{ dialect, source }` envelope; the button is shown but cannot be pressed while it is TRUE. Omit for never-disabled'), + params: z.unknown().optional() + .describe('Action parameters, forwarded to the runner: an array is the list of inputs to collect from the user before the action runs; an object is forwarded as the static parameter values'), + description: z.string().optional() + .describe('Action description, forwarded to the runner — the parameter dialog shows it under its title'), + target: z.string().optional() + .describe('Executor target, forwarded to the runner: the URL, script name, flow name or API endpoint, per `actionType`'), + openIn: z.enum(['self', 'new-tab']).optional() + .describe('For a `url` action: `self` navigates in place, `new-tab` opens a new browser tab'), + endpoint: z.string().optional().describe('API endpoint for an `api` action, forwarded to the runner'), + method: z.string().optional().describe('HTTP method for an `api` action, forwarded to the runner'), + bodyExtra: z.unknown().optional().describe('Static request-body fields for an `api` action, forwarded to the runner'), + bodyShape: z.unknown().optional().describe('How an `api` action shapes its request body, forwarded to the runner'), + operation: z.unknown().optional().describe('Declarative single-record write, forwarded to the runner together with `patch`'), + patch: z.unknown().optional().describe('Field values the declarative `operation` writes, forwarded to the runner'), + confirmText: z.string().optional().describe('Confirmation question asked before the action runs'), + successMessage: z.string().optional().describe('Toast shown when the action succeeds'), + errorMessage: z.string().optional().describe('Toast shown when the action fails, in place of the raw error'), + refreshAfter: z.boolean().optional().describe('Refresh the surrounding data after the action runs'), + undoable: z.boolean().optional().describe('Offer an Undo affordance after an update action'), + recordIdField: z.string().optional().describe('Row field whose value identifies the record the action acts on'), + locations: z.array(ActionLocationSchema).optional() + .describe('Action locations, forwarded to the runner — the console uses them to tell a record-scoped action from an object-level one'), + toast: z.unknown().optional().describe('Toast behaviour, forwarded to the runner'), + resultDialog: z.unknown().optional().describe('One-shot result dialog for a value the response shows exactly once, forwarded to the runner'), + onSuccess: z.unknown().optional().describe('Declared post-success navigation, forwarded to the runner'), +})); + +/** + * `action:icon` — an icon-only action button with a tooltip + * (`components/src/renderers/action/action-icon.tsx` at the pin). The same + * runner path as `action:button`, measured separately because the two differ: + * + * - `size` is NOT read — `:106` pins the primitive's `icon` size — so it is + * not declared (the registration does not publish it either). + * - `undoable` and `recordIdField` are NOT forwarded (`:119-174` lists the + * rest of `action:button`'s forward and not these two), so not declared. + * - `label` is read four times: `:137` (forwarded), `:243` (`aria-label`, + * falling back to `name`), `:252` (its first letter when no icon resolves) + * and `:258` / `:264` (the tooltip). `description` is the tooltip's + * fallback (`:264`) as well as forwarded (`:138`). + * - `visible` / `disabled` — `:96` + `:207` / `:101` + `:236`. `variant` — + * `:105`, `primary` mapped to `default`, renderer default `ghost`. + * - Forwarded (`:132-173`): `actionType`, `name`, `target`, `openIn`, + * `endpoint`, `method`, `params`, `bodyExtra`, `bodyShape`, `operation`, + * `patch`, `confirmText`, `successMessage`, `errorMessage`, `refreshAfter`, + * `locations`, `toast`, `resultDialog`, `onSuccess`. + */ +export const ActionIconPropsSchema = lazySchema(() => strictObject({ + surface: 'this `action:icon`', + history: actionBlockHistory('action:icon'), + guidanceSets: [COMPONENT_NODE_KEYS_GUIDANCE], + aliases: ACTION_NODE_ALIASES, + guidance: { + ...ACTION_NODE_GUIDANCE, + size: '`action:icon` is always icon-sized — the renderer fixes the Button primitive\'s `icon` ' + + 'size and reads no `size`. Remove the key, or use `action:button` for a sized button.', + }, +}, { + name: z.string().optional() + .describe('Action name, forwarded to the action runner; also the `aria-label` when there is no `label`. Optional, as for `action:button`'), + label: z.string().optional() + .describe('Accessible label and tooltip text (and its first letter stands in when no icon resolves). A literal string — localize through the translation bundle entry for this component id'), + icon: z.string().optional() + .describe('Lucide icon name, resolved through the shared action-icon resolver (renderer registration default `play`)'), + actionType: z.string().optional() + .describe('Executor the action runner dispatches to — built in: `script`, `url`, `modal`, `flow`, `api`, `form`; a handler registered under another name is dispatched too. Write it here, not as `type`'), + variant: z.enum([...BUTTON_PRIMITIVE_VARIANTS, 'primary']).optional() + .describe('Button variant — the Button primitive\'s vocabulary, plus `primary`, which renders as `default` (renderer default: `ghost`)'), + visible: actionCondition().optional() + .describe('Visibility predicate — a boolean, a CEL string, or a `{ dialect, source }` envelope, evaluated against the row the host binds; the icon is not rendered when it is FALSE. Omit for always-visible'), + disabled: actionCondition().optional() + .describe('Disabled predicate — a boolean, a CEL string, or a `{ dialect, source }` envelope; the icon is shown but cannot be pressed while it is TRUE. Omit for never-disabled'), + description: z.string().optional() + .describe('Action description — the tooltip text when there is no `label`, and forwarded to the runner for the parameter dialog'), + params: z.unknown().optional() + .describe('Action parameters, forwarded to the runner: an array is the list of inputs to collect from the user; an object is forwarded as the static parameter values'), + target: z.string().optional() + .describe('Executor target, forwarded to the runner: the URL, script name, flow name or API endpoint, per `actionType`'), + openIn: z.enum(['self', 'new-tab']).optional() + .describe('For a `url` action: `self` navigates in place, `new-tab` opens a new browser tab'), + endpoint: z.string().optional().describe('API endpoint for an `api` action, forwarded to the runner'), + method: z.string().optional().describe('HTTP method for an `api` action, forwarded to the runner'), + bodyExtra: z.unknown().optional().describe('Static request-body fields for an `api` action, forwarded to the runner'), + bodyShape: z.unknown().optional().describe('How an `api` action shapes its request body, forwarded to the runner'), + operation: z.unknown().optional().describe('Declarative single-record write, forwarded to the runner together with `patch`'), + patch: z.unknown().optional().describe('Field values the declarative `operation` writes, forwarded to the runner'), + confirmText: z.string().optional().describe('Confirmation question asked before the action runs'), + successMessage: z.string().optional().describe('Toast shown when the action succeeds'), + errorMessage: z.string().optional().describe('Toast shown when the action fails, in place of the raw error'), + refreshAfter: z.boolean().optional().describe('Refresh the surrounding data after the action runs'), + locations: z.array(ActionLocationSchema).optional() + .describe('Action locations, forwarded to the runner — the console uses them to tell a record-scoped action from an object-level one'), + toast: z.unknown().optional().describe('Toast behaviour, forwarded to the runner'), + resultDialog: z.unknown().optional().describe('One-shot result dialog for a value the response shows exactly once, forwarded to the runner'), + onSuccess: z.unknown().optional().describe('Declared post-success navigation, forwarded to the runner'), +})); + +/** + * The member list `action:group` and `action:menu` both read — a LIST, as the + * renderers read it (`schema.actions || []`, then `.filter` / `.map`), where + * both registrations publish the input as `type: 'object'`. + * + * Each member is an action object the container draws and runs ITSELF, never + * through `SchemaRenderer`, so a member is not a page component and this row + * does not judge its keys: the members' value contract is the runner's. What + * the containers read off a member, at the pin: `visible` / `disabled` / + * `enabled`, `icon`, `variant`, `className`, `label` (falling back to `name`), + * `tags` (a `separator-before` tag draws a divider), `name` (the React key) and + * the runner forward — which hands the runner the member's own `type`, not + * `actionType`: a member is an action entry, and an action entry's executor is + * `type`. A bare string is refused here: an action NAME list is + * `record:quick_actions`' `actionNames`, and a string member would render as an + * unlabeled button that runs nothing. + */ +const actionMemberList = () => z.array(z.record(z.string(), z.unknown())); + +/** + * `action:group` — a row or dropdown of actions + * (`components/src/renderers/action/action-group.tsx` at the pin). Read + * points, per key: + * + * - `actions` — `:243`, then filtered by `location` through `actionRendersAt` + * (`:244`); members are read at `:79-132` (inline), `:164-202` (dropdown) + * and forwarded at `:248-287`. + * - `display` — `:309`, `inline` unless it is `dropdown`. + * - `label` / `icon` — `:328` / `:313`: the DROPDOWN trigger's text (default + * `Actions`) and icon. Inline mode renders neither. + * - `variant` — `:319` (dropdown trigger) and `:360` (each inline member's + * fallback); no `primary` mapping at group level, so the primitive's six. + * - `size` — `:320` maps `md` → `default` for the dropdown trigger, but `:361` + * hands the group size to each inline member raw, and `:89` maps only a + * member's OWN `md`. So `md` renders only in dropdown mode, and in the + * default inline mode reaches the Button primitive, which has no `md`. + * Declared: the primitive's four sizes. (The registration publishes + * `sm` / `md` / `lg`; `md` is objectui's to map on both paths or drop.) + * - `visible` — `:233` + `:306`. `:306` tests the raw value's truthiness, so a + * literal `false` is honoured by `SchemaRenderer`'s node gate, which also + * evaluates the hoisted value, rather than by this check. + * + * NOT read: the group's own `name`. The registration publishes it (`:378`), + * the renderer never reads `schema.name`, and inline mode only spreads it + * onto the wrapping `
` as a DOM attribute. Refused with a prescription, + * the `page:accordion` item `value` precedent: an author copying the + * designer's output is told what happened rather than merely refused. + */ +export const ActionGroupPropsSchema = lazySchema(() => strictObject({ + surface: 'this `action:group`', + history: actionBlockHistory('action:group'), + guidanceSets: [COMPONENT_NODE_KEYS_GUIDANCE], + aliases: { visibleWhen: 'visible', visibility: 'visible' }, + guidance: { + name: '`action:group` reads no group-level `name` — nothing renders, forwards or keys on it. ' + + 'Each member action\'s own `name` is what identifies it. Remove the key.', + }, +}, { + actions: actionMemberList().optional() + .describe('The actions in this group, in order — each an action object the group draws and runs itself (`name`, `label`, `icon`, `type`, `target`, `visible`, `disabled`, …); a member\'s executor is its `type`'), + display: z.enum(['inline', 'dropdown']).optional() + .describe('Display mode: `inline` renders every action as a button row; `dropdown` renders one trigger button and lists the actions in its menu (renderer default: `inline`)'), + location: ActionLocationSchema.optional() + .describe('Render only the members whose `locations` include this location. Omit to render every member'), + label: z.string().optional() + .describe('Dropdown trigger text (renderer default: `Actions`). Inline mode renders no group label. A literal string — localize through the translation bundle entry for this component id'), + icon: z.string().optional() + .describe('Lucide icon name on the dropdown trigger. Inline mode renders no group icon'), + variant: z.enum(BUTTON_PRIMITIVE_VARIANTS).optional() + .describe('Button variant for the dropdown trigger and for every inline member that sets none — the Button primitive\'s vocabulary (renderer default: `outline`)'), + size: z.enum(BUTTON_PRIMITIVE_SIZES).optional() + .describe('Button size for the dropdown trigger and for every inline member that sets none — the Button primitive\'s vocabulary'), + visible: actionCondition().optional() + .describe('Visibility predicate for the whole group — a boolean, a CEL string, or a `{ dialect, source }` envelope, evaluated against the row the host binds. Omit for always-visible'), +})); + +/** + * `action:menu` — a dropdown ("more") menu of actions + * (`components/src/renderers/action/action-menu.tsx` at the pin). Read + * points, per key: + * + * - `actions` — `:299`; members are read at `:78`, `:106-145` and `:360`, run + * through `ActionAutoTrigger` (`:320-327`), and forwarded at `:237-285`. + * - `label` — `:341` (`aria-label`, default: the translated "More actions") + * and `:350-351` (trigger text; icon-only when omitted). + * - `icon` — `:224`, default the `MoreHorizontal` glyph. + * - `variant` / `size` — `:225` / `:226`, handed to the Button primitive + * unmapped (defaults `ghost` / `icon`): no `primary`, no `md` here. + * - `visible` — `:219` + `:293`, fail-closed; the same truthiness note as + * `action:group`'s applies to a literal `false`. + * + * The registration's `inputs` (`:381-390`) publish `label`, `icon`, + * `actions`, `variant` and `className`; `size` and `visible` are read and + * unpublished. + */ +export const ActionMenuPropsSchema = lazySchema(() => strictObject({ + surface: 'this `action:menu`', + history: actionBlockHistory('action:menu'), + guidanceSets: [COMPONENT_NODE_KEYS_GUIDANCE], + aliases: { visibleWhen: 'visible', visibility: 'visible' }, +}, { + actions: actionMemberList().optional() + .describe('The menu\'s actions, in order — each an action object the menu draws and runs itself (`name`, `label`, `icon`, `type`, `target`, `visible`, `disabled`, `tags`, …); a member\'s executor is its `type`'), + label: z.string().optional() + .describe('Trigger text and accessible label; omit for an icon-only trigger labelled "More actions". A literal string — localize through the translation bundle entry for this component id'), + icon: z.string().optional() + .describe('Lucide icon name on the trigger (renderer default: the horizontal ellipsis)'), + variant: z.enum(BUTTON_PRIMITIVE_VARIANTS).optional() + .describe('Trigger button variant — the Button primitive\'s vocabulary (renderer default: `ghost`)'), + size: z.enum(BUTTON_PRIMITIVE_SIZES).optional() + .describe('Trigger button size — the Button primitive\'s vocabulary (renderer default: `icon`)'), + visible: actionCondition().optional() + .describe('Visibility predicate for the whole menu — a boolean, a CEL string, or a `{ dialect, source }` envelope, evaluated against the row the host binds; a predicate that fails to evaluate hides it. Omit for always-visible'), +})); + +/** + * `element:definition-list` — a compact key/value `
` + * (`components/src/renderers/basic/data-list.tsx` at the pin). Props are read + * through `readProps` (`:42-47`, `properties` first). Read points: `items` + * (`:48`), `columns` (`:49`), `inline` (`:63`), and per item `term` (`:66`) and + * `description` (`:68`). + * + * - `columns` is compared as the NUMBER `2` (`props.columns === 2`); any other + * value renders one column. Declared as the literal pair `1 | 2`, which is + * what the Studio designer writes (a `number` control, + * `previews/block-config.ts:306`). The registration's enum publishes the + * STRINGS `'1'` / `'2'` (`:82`), and the string `'2'` renders one column — + * the read wins. + * - `items` is optional, as it is read: absent and empty both render the + * renderer's own "No details" state (`:51-53`). The registration marks it + * required. + * - An item is strict. `term` is required — it is the row's only label, placed + * as a React child, so a literal string — and `description` takes any value + * (`toText` prints objects as JSON and an absent one as an em dash). The + * designer wrote `label` / `value` items until objectui#8279 and every row + * rendered blank; that is the mistake an item that refuses unknown keys + * stops at authoring time. + */ +export const ElementDefinitionListPropsSchema = lazySchema(() => strictObject({ + surface: 'this `element:definition-list`', + history: elementListHistory('element:definition-list'), + guidanceSets: COMPONENT_LEVEL_GUIDANCE, +}, { + items: z.array(strictObject({ + surface: 'this `element:definition-list` item', + history: elementListHistory('element:definition-list'), + // The objectui#8279 pair: the designer wrote `label` / `value` items, and + // every row rendered a blank term and an em dash. + aliases: { label: 'term', value: 'description' }, + }, { + term: z.string().describe('The term — rendered as the row\'s label, as-is'), + description: z.unknown().optional() + .describe('The value shown under (or beside) the term — a string or number as-is, an object as JSON; omitted renders an em dash'), + })).optional() + .describe('Term/description pairs, in order. Omitted or empty renders the "No details" empty state'), + columns: z.literal([1, 2], { + // The registration's own enum spells these as strings, so the string is + // the likeliest wrong value — and the one the renderer silently collapses + // to a single column. + error: (issue) => (issue.input === '1' || issue.input === '2' + ? `\`columns\` on this \`element:definition-list\` takes the NUMBER \`${String(issue.input)}\`, not ` + + `the string '${String(issue.input)}' — the renderer compares it to the number 2, so the string ` + + `renders a single column. Write \`columns: ${String(issue.input)}\`.` + : undefined), + }).optional() + .describe('Grid columns from the small breakpoint up — the NUMBER `1` or `2` (renderer default: 1)'), + inline: z.boolean().optional() + .describe('Put each term and its description on one baseline-aligned row instead of stacking them'), +})); + +/** + * `element:repeater` — a data-bound, chrome-free list: one line per record + * (`components/src/renderers/basic/data-list.tsx` at the pin, props through + * `readProps`, `:97-107`). Read points: `object` (`:122`, `:134`), `filter` + * (`:113`, `:131` → `$filter`), `sort` (`:132` → `$orderby`), `limit` (`:133` → + * `$top`), `emptyText` (`:160`), `divided` (`:165`), `titleField` + * (`:170-171`) and `fields` (`:116`, `:173-175`). + * + * - `object` is REQUIRED, as `element:number`'s is: without it the renderer + * never queries and shows its "No records" state (`:122-125`, `:160`) — + * indistinguishable from an object that really has no rows. The + * registration marks it required too. + * - `filter` / `sort` are the family's one orthography from birth — + * `ViewFilterRule[]` and `SortItem[]` — and both are delivered: + * `ObjectStackAdapter.find` lowers a `{ field, operator, value }` array + * through `translateFilterArray` and serializes `{ field, order }` items + * through `serializeOrderBy` (`data-objectstack/src/index.ts:4793-4804`, + * `:759-785`). The rule-array door here is the bare one `record:related_list` + * declares, not a `ruleArrayFilterError` door: that prescription speaks to a + * door that used to take the record form, and a door wired to it joins the + * stored-row conversion's reach (`conversions/registry.ts`), which is not + * this row's to change. + * - `fields` takes a bare field name or `{ field }`. The renderer reads only + * `field` off the object form (`:175`) — the `label` its TS type and its + * registration's description both advertise is never rendered (the list has + * no header row), so it is refused with that reason. + * + * NOT read at the pin, whatever a sibling might suggest: the node-level + * `dataSource` binding. This renderer is not wrapped in objectui's + * element-data-source gate, so the query keys above are the only way to aim it. + */ +export const ElementRepeaterPropsSchema = lazySchema(() => strictObject({ + surface: 'this `element:repeater`', + history: elementListHistory('element:repeater'), + guidanceSets: COMPONENT_LEVEL_GUIDANCE, + // The same four query keys the element data-source binding declares, with + // its spellings for them (`ElementDataSourceSchema`, page.zod.ts), plus the + // `object-*` family's `objectName`. + aliases: { + objectName: 'object', filters: 'filter', where: 'filter', + orderBy: 'sort', sortBy: 'sort', top: 'limit', pageSize: 'limit', + }, +}, { + object: z.string() + .describe('Object whose records the list repeats over — required: without it the list never queries'), + titleField: z.string().optional() + .describe('Field shown first on each line, emphasized'), + fields: z.array(z.union([ + z.string(), + strictObject({ + surface: 'this `element:repeater` field', + history: elementListHistory('element:repeater'), + guidance: { + label: '`label` is not rendered — the repeater has no header row and prints only each ' + + 'field\'s value. Remove it, or write the bare field name.', + }, + }, { + field: z.string().describe('Field name'), + }), + ])).optional() + .describe('Fields shown after the title on each line, in order — a bare field name, or `{ field }`'), + filter: z.array(ViewFilterRuleSchema).optional() + .describe('Filter rules narrowing the records — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` in this map shares'), + sort: z.array(SortItemSchema).optional() + .describe('Sort order — `[{ field, order }]`'), + limit: z.number().int().positive().optional() + .describe('Maximum records fetched and shown'), + emptyText: z.string().optional() + .describe('Copy shown when the query returns no records (renderer default: "No records"). A literal string — localize through the translation bundle entry for this component id'), + divided: z.boolean().optional() + .describe('Draw a separator between lines (renderer default: true)'), +})); + /** * ---------------------------------------------------------------------- * 5. Object-bound SDUI blocks (#7751, maintainer ruling 2026-08-12: direction A) @@ -4626,6 +5136,39 @@ export const ComponentPropsMap = { 'element:form': ElementFormPropsSchema, 'element:record_picker': ElementRecordPickerPropsSchema, 'element:text_input': ElementTextInputPropsSchema, + // #20371 — the two `element:*` lists in objectui's curated public + // vocabulary. Until these rows the pair sat inside the reserved `element:` + // namespace with no enum member and no row, so `component-type-unknown` + // refused both nodes outright. The rows are what admit them to the + // `element:` vocabulary (`component-type-vocabulary.ts` derives the known + // set from this map's keys), on the three-part evidence that vocabulary's + // string-arm ledger asks of a type admitted without an enum member — all + // measured at the pin this repo builds against (`.objectui-sha` = + // `f8a9d0fb0`): + // - registration: `@object-ui/components` registers both in the `element` + // namespace (`components/src/renderers/basic/data-list.tsx:75`, `:184`); + // - publication: both are `PUBLIC_BLOCKS` members + // (`core/src/registry/public-blocks.ts:109-110`), which is how the + // tracked `sdui.manifest.json` carries both; + // - authorship: the Studio page designer's palette offers both + // (`app-shell/src/views/metadata-admin/previews/block-types.ts:129-130`), + // each with its own inspector (`previews/block-config.ts:282-316`), so + // stored pages hold them. + 'element:definition-list': ElementDefinitionListPropsSchema, + 'element:repeater': ElementRepeaterPropsSchema, + + // Actions — #20371. The same curated vocabulary's four `action:*` blocks + // (`core/src/registry/public-blocks.ts:111-114` at the pin), registered by + // `@object-ui/components` in the `action` namespace + // (`components/src/renderers/action/`) and taught by objectui's own + // AGENTS.md as the node that runs an action. `action:` is not a namespace + // the enum populates, so these rows add no vocabulary claim; what they add + // is the props gate's dispatch, which skipped all four until now. Key sets + // measured from the renderers' read points — see section 4b above. + 'action:button': ActionButtonPropsSchema, + 'action:group': ActionGroupPropsSchema, + 'action:menu': ActionMenuPropsSchema, + 'action:icon': ActionIconPropsSchema, // Object-bound SDUI blocks (#7751, maintainer ruling 2026-08-12 direction A). // Key sets derived from the objectui renderers' own read points — see the From 640e3d263d0f965e896c9026a07f605ec1e39211 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 07:13:39 +0000 Subject: [PATCH 02/13] feat(spec): type aliases for the six new rows; declare the repeater's dropped filter refinement Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW Co-authored-by: Claude --- content/docs/references/index.mdx | 10 +- content/docs/references/ui/component.mdx | 174 +++++++++++++++++- ...07-unknown-key-strictness-ledger.counts.md | 14 +- packages/spec/api-surface/ui.json | 6 + packages/spec/authorable-surface/ui.json | 78 ++++++++ packages/spec/declaration-map/ui.json | 6 + .../spec/dropped-refinements.baseline.json | 5 + packages/spec/export-origins/ui.json | 6 + packages/spec/json-schema.manifest/ui.json | 6 + .../src/type-alias-convention.pin.test.ts | 23 ++- packages/spec/src/ui/component.zod.ts | 47 +++++ 11 files changed, 358 insertions(+), 17 deletions(-) diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index fbd5465c59f..a12510933a2 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,6 +1,6 @@ --- title: Protocol Reference -description: Every schema published by @objectstack/spec — 1523 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1529 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -32,8 +32,8 @@ counts are sums of the rows they head. Regenerate with | [Shared Protocol](/docs/references/shared) | 10 | 31 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. | | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 34 | 275 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | -| [UI Protocol](/docs/references/ui) | 16 | 159 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **196** | **1523** | 14 protocol modules | +| [UI Protocol](/docs/references/ui) | 16 | 165 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | +| **Total** | **196** | **1529** | 14 protocol modules | --- @@ -362,7 +362,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a ## UI Protocol -**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 159 schemas** +**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 165 schemas** Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. @@ -373,7 +373,7 @@ Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI lay | [`app.zod.ts`](/docs/references/ui/app) | `ActionNavItem`, `App`, `AppBranding`, `AppContextSelector`, `ComponentNavItem`, `DashboardNavItem`, `DocNavItem`, `GroupNavItem`, `NavigationArea`, `NavigationContribution`, `NavigationItem`, `ObjectNavItem`, `PageNavItem`, `ReportNavItem`, `UrlNavItem` | | [`bulk-action.zod.ts`](/docs/references/ui/bulk-action) | `BulkActionDef`, `BulkActionExecution`, `BulkActionOperation`, `BulkActionParam` | | [`chart.zod.ts`](/docs/references/ui/chart) | `ChartAggregate`, `ChartAggregateFunction`, `ChartAnnotation`, `ChartAxis`, `ChartConfig`, `ChartDrillDown`, `ChartGroupBy`, `ChartInteraction`, `ChartSeries`, `ChartType` | -| [`component.zod.ts`](/docs/references/ui/component) | `AIChatWindowProps`, `ElementButtonProps`, `ElementFilterProps`, `ElementFormProps`, `ElementImageProps`, `ElementMetadataViewerProps`, `ElementNumberProps`, `ElementRecordPickerProps`, `ElementTextInputProps`, `ElementTextProps`, `ObjectCalendarProps`, `ObjectFormProps`, `ObjectGanttProps`, `ObjectGridProps`, `ObjectKanbanProps`, `ObjectMapProps`, `ObjectMasterDetailFormProps`, `ObjectMetricProps`, `ObjectTimelineProps`, `ObjectTreeProps`, `PageAccordionProps`, `PageCardProps`, `PageContainerProps`, `PageHeaderProps`, `PageTabsProps`, `RecordActivityProps`, `RecordAlertAction`, `RecordAlertProps`, `RecordChatterProps`, `RecordDetailsProps`, `RecordHighlightsField`, `RecordHighlightsProps`, `RecordHistoryProps`, `RecordPathProps`, `RecordQuickActionsProps`, `RecordReferenceRailProps`, `RecordRelatedListProps`, `ReferenceRailEntry` | +| [`component.zod.ts`](/docs/references/ui/component) | `AIChatWindowProps`, `ActionButtonProps`, `ActionGroupProps`, `ActionIconProps`, `ActionMenuProps`, `ElementButtonProps`, `ElementDefinitionListProps`, `ElementFilterProps`, `ElementFormProps`, `ElementImageProps`, `ElementMetadataViewerProps`, `ElementNumberProps`, `ElementRecordPickerProps`, `ElementRepeaterProps`, `ElementTextInputProps`, `ElementTextProps`, `ObjectCalendarProps`, `ObjectFormProps`, `ObjectGanttProps`, `ObjectGridProps`, `ObjectKanbanProps`, `ObjectMapProps`, `ObjectMasterDetailFormProps`, `ObjectMetricProps`, `ObjectTimelineProps`, `ObjectTreeProps`, `PageAccordionProps`, `PageCardProps`, `PageContainerProps`, `PageHeaderProps`, `PageTabsProps`, `RecordActivityProps`, `RecordAlertAction`, `RecordAlertProps`, `RecordChatterProps`, `RecordDetailsProps`, `RecordHighlightsField`, `RecordHighlightsProps`, `RecordHistoryProps`, `RecordPathProps`, `RecordQuickActionsProps`, `RecordReferenceRailProps`, `RecordRelatedListProps`, `ReferenceRailEntry` | | [`dashboard.zod.ts`](/docs/references/ui/dashboard) | `Dashboard`, `DashboardHeader`, `DashboardHeaderAction`, `DashboardWidget`, `DashboardWidgetChartConfig`, `DashboardWidgetOptions`, `GlobalFilter`, `GlobalFilterOptionsFrom`, `WidgetActionType`, `WidgetColorVariant` | | [`dataset.zod.ts`](/docs/references/ui/dataset) | `Dataset`, `DatasetDimension`, `DatasetMeasure`, `DerivedMeasureOp` | | [`expression-bindable-text-keys.zod.ts`](/docs/references/ui/expression-bindable-text-keys) | `ExpressionBindableTextKey` | diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index 3a4672382c6..08c4340e48b 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -1,6 +1,6 @@ --- title: Component -description: "Component schemas of the ObjectStack UI Protocol: AIChatWindowProps and 37 more — each property with its type, default and a TypeScript example." +description: "Component schemas of the ObjectStack UI Protocol: AIChatWindowProps and 43 more — each property with its type, default and a TypeScript example." --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -12,7 +12,7 @@ description: "Component schemas of the ObjectStack UI Protocol: AIChatWindowProp ## TypeScript Usage ```typescript -import { AIChatWindowProps, ElementButtonPropsSchema, ElementFilterPropsSchema, ElementFormPropsSchema, ElementImagePropsSchema, ElementMetadataViewerPropsSchema, ElementNumberPropsSchema, ElementRecordPickerPropsSchema, ElementTextInputPropsSchema, ElementTextPropsSchema, ObjectCalendarPropsSchema, ObjectFormPropsSchema, ObjectGanttPropsSchema, ObjectGridPropsSchema, ObjectKanbanPropsSchema, ObjectMapPropsSchema, ObjectMasterDetailFormPropsSchema, ObjectMetricPropsSchema, ObjectTimelinePropsSchema, ObjectTreePropsSchema, PageAccordionProps, PageCardProps, PageContainerProps, PageHeaderProps, PageTabsProps, RecordActivityProps, RecordAlertActionSchema, RecordAlertProps, RecordChatterProps, RecordDetailsProps, RecordHighlightsField, RecordHighlightsProps, RecordHistoryProps, RecordPathProps, RecordQuickActionsProps, RecordReferenceRailProps, RecordRelatedListProps, ReferenceRailEntrySchema } from '@objectstack/spec/ui'; +import { AIChatWindowProps, ActionButtonPropsSchema, ActionGroupPropsSchema, ActionIconPropsSchema, ActionMenuPropsSchema, ElementButtonPropsSchema, ElementDefinitionListPropsSchema, ElementFilterPropsSchema, ElementFormPropsSchema, ElementImagePropsSchema, ElementMetadataViewerPropsSchema, ElementNumberPropsSchema, ElementRecordPickerPropsSchema, ElementRepeaterPropsSchema, ElementTextInputPropsSchema, ElementTextPropsSchema, ObjectCalendarPropsSchema, ObjectFormPropsSchema, ObjectGanttPropsSchema, ObjectGridPropsSchema, ObjectKanbanPropsSchema, ObjectMapPropsSchema, ObjectMasterDetailFormPropsSchema, ObjectMetricPropsSchema, ObjectTimelinePropsSchema, ObjectTreePropsSchema, PageAccordionProps, PageCardProps, PageContainerProps, PageHeaderProps, PageTabsProps, RecordActivityProps, RecordAlertActionSchema, RecordAlertProps, RecordChatterProps, RecordDetailsProps, RecordHighlightsField, RecordHighlightsProps, RecordHistoryProps, RecordPathProps, RecordQuickActionsProps, RecordReferenceRailProps, RecordRelatedListProps, ReferenceRailEntrySchema } from '@objectstack/spec/ui'; import type { ElementNumberProps, ElementRecordPickerProps, ObjectCalendarProps, ObjectFormProps, ObjectGanttProps, ObjectGridProps, ObjectKanbanProps, ObjectMapProps, ObjectMasterDetailFormProps, ObjectMetricProps, ObjectTimelineProps, ObjectTreeProps, PageContainerProps, RecordAlertAction, RecordAlertProps, RecordHighlightsField, RecordHistoryProps, RecordPathProps, RecordQuickActionsProps, RecordReferenceRailProps, ReferenceRailEntry } from '@objectstack/spec/ui'; // Validate data @@ -41,6 +41,113 @@ const result = AIChatWindowProps.parse(data); | **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | +--- + +## ActionButtonProps + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Action name, forwarded to the action runner. Optional: an inline page button is not a registered object action, and without a name the runner dispatches on `actionType` alone | +| **label** | `string` | optional | Button text. A literal string, placed as-is — localize through the translation bundle entry for this component id, not an inline locale map | +| **icon** | `string` | optional | Lucide icon name drawn left of the label, resolved through the shared action-icon resolver (an unknown name draws no icon) | +| **actionType** | `string` | optional | Executor the action runner dispatches to — built in: `script`, `url`, `modal`, `flow`, `api`, `form`; a handler registered under another name is dispatched too. Write it here, not as `type`: on a page component `type` is the component itself | +| **variant** | `Enum<'default' \| 'destructive' \| 'outline' \| 'secondary' \| 'ghost' \| 'link' \| 'primary'>` | optional | Button variant — the Button primitive's vocabulary, plus `primary`, which renders as `default` (renderer default: `default`) | +| **size** | `Enum<'default' \| 'sm' \| 'lg' \| 'icon' \| 'md'>` | optional | Button size — the Button primitive's vocabulary, plus `md`, which renders as `default` (renderer default: `default`) | +| **visible** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate — a boolean, a CEL string, or a `{ dialect, source }` envelope, evaluated against the row the host binds; the button is not rendered when it is FALSE, and a predicate that fails to evaluate hides it. Omit for always-visible | +| **disabled** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Disabled predicate — a boolean, a CEL string, or a `{ dialect, source }` envelope; the button is shown but cannot be pressed while it is TRUE. Omit for never-disabled | +| **params** | `any` | optional | Action parameters, forwarded to the runner: an array is the list of inputs to collect from the user before the action runs; an object is forwarded as the static parameter values | +| **description** | `string` | optional | Action description, forwarded to the runner — the parameter dialog shows it under its title | +| **target** | `string` | optional | Executor target, forwarded to the runner: the URL, script name, flow name or API endpoint, per `actionType` | +| **openIn** | `Enum<'self' \| 'new-tab'>` | optional | For a `url` action: `self` navigates in place, `new-tab` opens a new browser tab | +| **endpoint** | `string` | optional | API endpoint for an `api` action, forwarded to the runner | +| **method** | `string` | optional | HTTP method for an `api` action, forwarded to the runner | +| **bodyExtra** | `any` | optional | Static request-body fields for an `api` action, forwarded to the runner | +| **bodyShape** | `any` | optional | How an `api` action shapes its request body, forwarded to the runner | +| **operation** | `any` | optional | Declarative single-record write, forwarded to the runner together with `patch` | +| **patch** | `any` | optional | Field values the declarative `operation` writes, forwarded to the runner | +| **confirmText** | `string` | optional | Confirmation question asked before the action runs | +| **successMessage** | `string` | optional | Toast shown when the action succeeds | +| **errorMessage** | `string` | optional | Toast shown when the action fails, in place of the raw error | +| **refreshAfter** | `boolean` | optional | Refresh the surrounding data after the action runs | +| **undoable** | `boolean` | optional | Offer an Undo affordance after an update action | +| **recordIdField** | `string` | optional | Row field whose value identifies the record the action acts on | +| **locations** | `Enum<'list_toolbar' \| 'list_item' \| 'record_header' \| 'record_more' \| 'record_related' \| 'record_section'>[]` | optional | Action locations, forwarded to the runner — the console uses them to tell a record-scoped action from an object-level one | +| **toast** | `any` | optional | Toast behaviour, forwarded to the runner | +| **resultDialog** | `any` | optional | One-shot result dialog for a value the response shows exactly once, forwarded to the runner | +| **onSuccess** | `any` | optional | Declared post-success navigation, forwarded to the runner | + + +--- + +## ActionGroupProps + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **actions** | `Record[]` | optional | The actions in this group, in order — each an action object the group draws and runs itself (`name`, `label`, `icon`, `type`, `target`, `visible`, `disabled`, …); a member's executor is its `type` | +| **display** | `Enum<'inline' \| 'dropdown'>` | optional | Display mode: `inline` renders every action as a button row; `dropdown` renders one trigger button and lists the actions in its menu (renderer default: `inline`) | +| **location** | `Enum<'list_toolbar' \| 'list_item' \| 'record_header' \| 'record_more' \| 'record_related' \| 'record_section'>` | optional | Render only the members whose `locations` include this location. Omit to render every member | +| **label** | `string` | optional | Dropdown trigger text (renderer default: `Actions`). Inline mode renders no group label. A literal string — localize through the translation bundle entry for this component id | +| **icon** | `string` | optional | Lucide icon name on the dropdown trigger. Inline mode renders no group icon | +| **variant** | `Enum<'default' \| 'destructive' \| 'outline' \| 'secondary' \| 'ghost' \| 'link'>` | optional | Button variant for the dropdown trigger and for every inline member that sets none — the Button primitive's vocabulary (renderer default: `outline`) | +| **size** | `Enum<'default' \| 'sm' \| 'lg' \| 'icon'>` | optional | Button size for the dropdown trigger and for every inline member that sets none — the Button primitive's vocabulary | +| **visible** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate for the whole group — a boolean, a CEL string, or a `{ dialect, source }` envelope, evaluated against the row the host binds. Omit for always-visible | + + +--- + +## ActionIconProps + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | optional | Action name, forwarded to the action runner; also the `aria-label` when there is no `label`. Optional, as for `action:button` | +| **label** | `string` | optional | Accessible label and tooltip text (and its first letter stands in when no icon resolves). A literal string — localize through the translation bundle entry for this component id | +| **icon** | `string` | optional | Lucide icon name, resolved through the shared action-icon resolver (renderer registration default `play`) | +| **actionType** | `string` | optional | Executor the action runner dispatches to — built in: `script`, `url`, `modal`, `flow`, `api`, `form`; a handler registered under another name is dispatched too. Write it here, not as `type` | +| **variant** | `Enum<'default' \| 'destructive' \| 'outline' \| 'secondary' \| 'ghost' \| 'link' \| 'primary'>` | optional | Button variant — the Button primitive's vocabulary, plus `primary`, which renders as `default` (renderer default: `ghost`) | +| **visible** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate — a boolean, a CEL string, or a `{ dialect, source }` envelope, evaluated against the row the host binds; the icon is not rendered when it is FALSE. Omit for always-visible | +| **disabled** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Disabled predicate — a boolean, a CEL string, or a `{ dialect, source }` envelope; the icon is shown but cannot be pressed while it is TRUE. Omit for never-disabled | +| **description** | `string` | optional | Action description — the tooltip text when there is no `label`, and forwarded to the runner for the parameter dialog | +| **params** | `any` | optional | Action parameters, forwarded to the runner: an array is the list of inputs to collect from the user; an object is forwarded as the static parameter values | +| **target** | `string` | optional | Executor target, forwarded to the runner: the URL, script name, flow name or API endpoint, per `actionType` | +| **openIn** | `Enum<'self' \| 'new-tab'>` | optional | For a `url` action: `self` navigates in place, `new-tab` opens a new browser tab | +| **endpoint** | `string` | optional | API endpoint for an `api` action, forwarded to the runner | +| **method** | `string` | optional | HTTP method for an `api` action, forwarded to the runner | +| **bodyExtra** | `any` | optional | Static request-body fields for an `api` action, forwarded to the runner | +| **bodyShape** | `any` | optional | How an `api` action shapes its request body, forwarded to the runner | +| **operation** | `any` | optional | Declarative single-record write, forwarded to the runner together with `patch` | +| **patch** | `any` | optional | Field values the declarative `operation` writes, forwarded to the runner | +| **confirmText** | `string` | optional | Confirmation question asked before the action runs | +| **successMessage** | `string` | optional | Toast shown when the action succeeds | +| **errorMessage** | `string` | optional | Toast shown when the action fails, in place of the raw error | +| **refreshAfter** | `boolean` | optional | Refresh the surrounding data after the action runs | +| **locations** | `Enum<'list_toolbar' \| 'list_item' \| 'record_header' \| 'record_more' \| 'record_related' \| 'record_section'>[]` | optional | Action locations, forwarded to the runner — the console uses them to tell a record-scoped action from an object-level one | +| **toast** | `any` | optional | Toast behaviour, forwarded to the runner | +| **resultDialog** | `any` | optional | One-shot result dialog for a value the response shows exactly once, forwarded to the runner | +| **onSuccess** | `any` | optional | Declared post-success navigation, forwarded to the runner | + + +--- + +## ActionMenuProps + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **actions** | `Record[]` | optional | The menu's actions, in order — each an action object the menu draws and runs itself (`name`, `label`, `icon`, `type`, `target`, `visible`, `disabled`, `tags`, …); a member's executor is its `type` | +| **label** | `string` | optional | Trigger text and accessible label; omit for an icon-only trigger labelled "More actions". A literal string — localize through the translation bundle entry for this component id | +| **icon** | `string` | optional | Lucide icon name on the trigger (renderer default: the horizontal ellipsis) | +| **variant** | `Enum<'default' \| 'destructive' \| 'outline' \| 'secondary' \| 'ghost' \| 'link'>` | optional | Trigger button variant — the Button primitive's vocabulary (renderer default: `ghost`) | +| **size** | `Enum<'default' \| 'sm' \| 'lg' \| 'icon'>` | optional | Trigger button size — the Button primitive's vocabulary (renderer default: `icon`) | +| **visible** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate for the whole menu — a boolean, a CEL string, or a `{ dialect, source }` envelope, evaluated against the row the host binds; a predicate that fails to evaluate hides it. Omit for always-visible | + + --- ## ElementButtonProps @@ -85,6 +192,26 @@ const result = AIChatWindowProps.parse(data); | **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | +--- + +## ElementDefinitionListProps + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **items** | `{ term: string; description?: any }[]` | optional | Term/description pairs, in order. Omitted or empty renders the "No details" empty state | +| **columns** | `Enum<1 \| 2>` | optional | Grid columns from the small breakpoint up — the NUMBER `1` or `2` (renderer default: 1) | +| **inline** | `boolean` | optional | Put each term and its description on one baseline-aligned row instead of stacking them | + +### Nested Shape: `ElementDefinitionListProps.items[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **term** | `string` | ✅ | The term — rendered as the row's label, as-is | +| **description** | `any` | optional | The value shown under (or beside) the term — a string or number as-is, an object as JSON; omitted renders an em dash | + + --- ## ElementFilterProps @@ -251,6 +378,49 @@ Sort field and direction pair | **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | +--- + +## ElementRepeaterProps + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Object whose records the list repeats over — required: without it the list never queries | +| **titleField** | `string` | optional | Field shown first on each line, emphasized | +| **fields** | `(string \| { field: string })[]` | optional | Fields shown after the title on each line, in order — a bare field name, or `{ field }` | +| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter rules narrowing the records — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` in this map shares | +| **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Sort order — `[{ field, order }]` | +| **limit** | `integer` | optional | Maximum records fetched and shown | +| **emptyText** | `string` | optional | Copy shown when the query returns no records (renderer default: "No records"). A literal string — localize through the translation bundle entry for this component id | +| **divided** | `boolean` | optional | Draw a separator between lines (renderer default: true) | + +### Nested Shape: `ElementRepeaterProps.fields[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name | + +### Nested Shape: `ElementRepeaterProps.filter[number]` + +View filter rule + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name to filter on | +| **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | +| **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: `icontains` takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. | + +### Nested Shape: `ElementRepeaterProps.sort[number]` + +Sort field and direction pair + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name to sort by | +| **order** | `Enum<'asc' \| 'desc'>` | ✅ | Sort direction | + + --- ## ElementTextInputProps diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md index e4d205a396d..434e0069c35 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md @@ -21,7 +21,7 @@ regenerate. | Measure | Value | |---|---| | Triaged directories | 5 | -| Object sites in them | 453 | +| Object sites in them | 461 | | Still-open (strip) sites | 125 | | Files carrying at least one | 22 | @@ -44,12 +44,12 @@ The `strict` column is the one the campaign schedules against; it counts both th | Dir | Sites | strict | passthrough | catchall | strip | |---|---|---|---|---|---| -| `ui/` | 180 | 170 | 3 | 0 | 7 | +| `ui/` | 188 | 178 | 3 | 0 | 7 | | `data/` | 159 | 76 | 1 | 0 | 82 | | `automation/` | 67 | 43 | 0 | 1 | 23 | | `security/` | 20 | 7 | 0 | 0 | 13 | | `studio/` | 27 | 27 | 0 | 0 | 0 | -| **total** | **453** | **323** | **4** | **1** | **125** | +| **total** | **461** | **331** | **4** | **1** | **125** | ## File-level triage — site counts @@ -66,7 +66,7 @@ classify and is not listed (it becomes reportable the day it grows its first sit | `app.zod.ts` | 19 | | `bulk-action.zod.ts` | 4 | | `chart.zod.ts` | 8 | -| `component.zod.ts` | 48 | +| `component.zod.ts` | 56 | | `dashboard.zod.ts` | 11 | | `dataset.zod.ts` | 4 | | `i18n.zod.ts` | 1 | @@ -76,7 +76,7 @@ classify and is not listed (it becomes reportable the day it grows its first sit | `sharing.zod.ts` | 1 | | `view.zod.ts` | 62 | | `widget.zod.ts` | 1 | -| **total** | **180** | +| **total** | **188** | ### `data/` — sites @@ -155,7 +155,7 @@ over it is here. ### `ui/` — open -**7 strip of 180**, in 4 file(s). +**7 strip of 188**, in 4 file(s). | File | Strip | Sites | |---|---|---| @@ -163,7 +163,7 @@ over it is here. | `app.zod.ts` | 1 | 19 | | `view.zod.ts` | 4 | 62 | | `widget.zod.ts` | 1 | 1 | -| **total** | **7** | **180** | +| **total** | **7** | **188** | | Bucket | Sites | |---|---| diff --git a/packages/spec/api-surface/ui.json b/packages/spec/api-surface/ui.json index d90dcff4eee..9922f0389de 100644 --- a/packages/spec/api-surface/ui.json +++ b/packages/spec/api-surface/ui.json @@ -11,11 +11,15 @@ "ActionAi (type)", "ActionAiParsed (type)", "ActionAiSchema (const)", + "ActionButtonPropsSchema (const)", "ActionEngineFacade (interface)", + "ActionGroupPropsSchema (const)", "ActionHandler (type)", "ActionHandlerContext (interface)", + "ActionIconPropsSchema (const)", "ActionLocation (type)", "ActionLocationSchema (const)", + "ActionMenuPropsSchema (const)", "ActionNavItem (type)", "ActionNavItemParsed (type)", "ActionNavItemSchema (const)", @@ -146,6 +150,7 @@ "ElementDataSource (type)", "ElementDataSourceParsed (type)", "ElementDataSourceSchema (const)", + "ElementDefinitionListPropsSchema (const)", "ElementFilterPropsSchema (const)", "ElementFormPropsSchema (const)", "ElementImagePropsSchema (const)", @@ -156,6 +161,7 @@ "ElementRecordPickerProps (type)", "ElementRecordPickerPropsParsed (type)", "ElementRecordPickerPropsSchema (const)", + "ElementRepeaterPropsSchema (const)", "ElementTextInputPropsSchema (const)", "ElementTextPropsSchema (const)", "ExpandViewResult (interface)", diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index 5228c0ba78d..f944f66ae42 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -60,6 +60,73 @@ "ui/ActionAi:outputSchema", "ui/ActionAi:paramHints", "ui/ActionAi:requiresConfirmation", + "ui/ActionButtonProps:actionType", + "ui/ActionButtonProps:bodyExtra", + "ui/ActionButtonProps:bodyShape", + "ui/ActionButtonProps:confirmText", + "ui/ActionButtonProps:description", + "ui/ActionButtonProps:disabled", + "ui/ActionButtonProps:endpoint", + "ui/ActionButtonProps:errorMessage", + "ui/ActionButtonProps:icon", + "ui/ActionButtonProps:label", + "ui/ActionButtonProps:locations", + "ui/ActionButtonProps:method", + "ui/ActionButtonProps:name", + "ui/ActionButtonProps:onSuccess", + "ui/ActionButtonProps:openIn", + "ui/ActionButtonProps:operation", + "ui/ActionButtonProps:params", + "ui/ActionButtonProps:patch", + "ui/ActionButtonProps:recordIdField", + "ui/ActionButtonProps:refreshAfter", + "ui/ActionButtonProps:resultDialog", + "ui/ActionButtonProps:size", + "ui/ActionButtonProps:successMessage", + "ui/ActionButtonProps:target", + "ui/ActionButtonProps:toast", + "ui/ActionButtonProps:undoable", + "ui/ActionButtonProps:variant", + "ui/ActionButtonProps:visible", + "ui/ActionGroupProps:actions", + "ui/ActionGroupProps:display", + "ui/ActionGroupProps:icon", + "ui/ActionGroupProps:label", + "ui/ActionGroupProps:location", + "ui/ActionGroupProps:size", + "ui/ActionGroupProps:variant", + "ui/ActionGroupProps:visible", + "ui/ActionIconProps:actionType", + "ui/ActionIconProps:bodyExtra", + "ui/ActionIconProps:bodyShape", + "ui/ActionIconProps:confirmText", + "ui/ActionIconProps:description", + "ui/ActionIconProps:disabled", + "ui/ActionIconProps:endpoint", + "ui/ActionIconProps:errorMessage", + "ui/ActionIconProps:icon", + "ui/ActionIconProps:label", + "ui/ActionIconProps:locations", + "ui/ActionIconProps:method", + "ui/ActionIconProps:name", + "ui/ActionIconProps:onSuccess", + "ui/ActionIconProps:openIn", + "ui/ActionIconProps:operation", + "ui/ActionIconProps:params", + "ui/ActionIconProps:patch", + "ui/ActionIconProps:refreshAfter", + "ui/ActionIconProps:resultDialog", + "ui/ActionIconProps:successMessage", + "ui/ActionIconProps:target", + "ui/ActionIconProps:toast", + "ui/ActionIconProps:variant", + "ui/ActionIconProps:visible", + "ui/ActionMenuProps:actions", + "ui/ActionMenuProps:icon", + "ui/ActionMenuProps:label", + "ui/ActionMenuProps:size", + "ui/ActionMenuProps:variant", + "ui/ActionMenuProps:visible", "ui/ActionNavItem:actionDef", "ui/ActionNavItem:badge", "ui/ActionNavItem:badgeVariant", @@ -377,6 +444,9 @@ "ui/ElementDataSource:object", "ui/ElementDataSource:sort", "ui/ElementDataSource:view", + "ui/ElementDefinitionListProps:columns", + "ui/ElementDefinitionListProps:inline", + "ui/ElementDefinitionListProps:items", "ui/ElementFilterProps:aria [RETIRED]", "ui/ElementFilterProps:fields [RETIRED]", "ui/ElementFilterProps:layout [RETIRED]", @@ -422,6 +492,14 @@ "ui/ElementRecordPickerProps:sort", "ui/ElementRecordPickerProps:targetVariable [RETIRED]", "ui/ElementRecordPickerProps:valueField", + "ui/ElementRepeaterProps:divided", + "ui/ElementRepeaterProps:emptyText", + "ui/ElementRepeaterProps:fields", + "ui/ElementRepeaterProps:filter", + "ui/ElementRepeaterProps:limit", + "ui/ElementRepeaterProps:object", + "ui/ElementRepeaterProps:sort", + "ui/ElementRepeaterProps:titleField", "ui/ElementTextInputProps:aria", "ui/ElementTextInputProps:defaultValue", "ui/ElementTextInputProps:description", diff --git a/packages/spec/declaration-map/ui.json b/packages/spec/declaration-map/ui.json index 84bd3941c77..e3696b7ad74 100644 --- a/packages/spec/declaration-map/ui.json +++ b/packages/spec/declaration-map/ui.json @@ -6,8 +6,12 @@ "Action": "ui/Action", "ActionAi": "ui/ActionAi", "ActionAiSchema": "ui/ActionAi", + "ActionButtonPropsSchema": "ui/ActionButtonProps", + "ActionGroupPropsSchema": "ui/ActionGroupProps", + "ActionIconPropsSchema": "ui/ActionIconProps", "ActionLocation": "ui/ActionLocation", "ActionLocationSchema": "ui/ActionLocation", + "ActionMenuPropsSchema": "ui/ActionMenuProps", "ActionNavItem": "ui/ActionNavItem", "ActionNavItemSchema": "ui/ActionNavItem", "ActionParam": "ui/ActionParam", @@ -92,6 +96,7 @@ "ElementButtonPropsSchema": "ui/ElementButtonProps", "ElementDataSource": "ui/ElementDataSource", "ElementDataSourceSchema": "ui/ElementDataSource", + "ElementDefinitionListPropsSchema": "ui/ElementDefinitionListProps", "ElementFilterPropsSchema": "ui/ElementFilterProps", "ElementFormPropsSchema": "ui/ElementFormProps", "ElementImagePropsSchema": "ui/ElementImageProps", @@ -100,6 +105,7 @@ "ElementNumberPropsSchema": "ui/ElementNumberProps", "ElementRecordPickerProps": "ui/ElementRecordPickerProps", "ElementRecordPickerPropsSchema": "ui/ElementRecordPickerProps", + "ElementRepeaterPropsSchema": "ui/ElementRepeaterProps", "ElementTextInputPropsSchema": "ui/ElementTextInputProps", "ElementTextPropsSchema": "ui/ElementTextProps", "ExpressionBindableTextKey": "ui/ExpressionBindableTextKey", diff --git a/packages/spec/dropped-refinements.baseline.json b/packages/spec/dropped-refinements.baseline.json index 65d1e049a8f..7cf2bf47919 100644 --- a/packages/spec/dropped-refinements.baseline.json +++ b/packages/spec/dropped-refinements.baseline.json @@ -1209,6 +1209,11 @@ "filter.element" ] }, + "ui/ElementRepeaterProps": { + "sites": [ + "filter.element" + ] + }, "ui/FormField": { "sites": [ "in.publicPicker.filter.element" diff --git a/packages/spec/export-origins/ui.json b/packages/spec/export-origins/ui.json index b9e4ff35012..6a6208d1e4b 100644 --- a/packages/spec/export-origins/ui.json +++ b/packages/spec/export-origins/ui.json @@ -10,11 +10,15 @@ "ActionAi": "src/ui/action.zod.ts#ActionAi (type)", "ActionAiParsed": "src/ui/action.zod.ts#ActionAiParsed (type)", "ActionAiSchema": "src/ui/action.zod.ts#ActionAiSchema (const)", + "ActionButtonPropsSchema": "src/ui/component.zod.ts#ActionButtonPropsSchema (const)", "ActionEngineFacade": "src/ui/action-params.zod.ts#ActionEngineFacade (interface)", + "ActionGroupPropsSchema": "src/ui/component.zod.ts#ActionGroupPropsSchema (const)", "ActionHandler": "src/ui/action-params.zod.ts#ActionHandler (type)", "ActionHandlerContext": "src/ui/action-params.zod.ts#ActionHandlerContext (interface)", + "ActionIconPropsSchema": "src/ui/component.zod.ts#ActionIconPropsSchema (const)", "ActionLocation": "src/ui/action.zod.ts#ActionLocation (type)", "ActionLocationSchema": "src/ui/action.zod.ts#ActionLocationSchema (const)", + "ActionMenuPropsSchema": "src/ui/component.zod.ts#ActionMenuPropsSchema (const)", "ActionNavItem": "src/ui/app.zod.ts#ActionNavItem (type)", "ActionNavItemParsed": "src/ui/app.zod.ts#ActionNavItemParsed (type)", "ActionNavItemSchema": "src/ui/app.zod.ts#ActionNavItemSchema (const)", @@ -142,6 +146,7 @@ "ElementDataSource": "src/ui/page.zod.ts#ElementDataSource (type)", "ElementDataSourceParsed": "src/ui/page.zod.ts#ElementDataSourceParsed (type)", "ElementDataSourceSchema": "src/ui/page.zod.ts#ElementDataSourceSchema (const)", + "ElementDefinitionListPropsSchema": "src/ui/component.zod.ts#ElementDefinitionListPropsSchema (const)", "ElementFilterPropsSchema": "src/ui/component.zod.ts#ElementFilterPropsSchema (const)", "ElementFormPropsSchema": "src/ui/component.zod.ts#ElementFormPropsSchema (const)", "ElementImagePropsSchema": "src/ui/component.zod.ts#ElementImagePropsSchema (const)", @@ -152,6 +157,7 @@ "ElementRecordPickerProps": "src/ui/component.zod.ts#ElementRecordPickerProps (type)", "ElementRecordPickerPropsParsed": "src/ui/component.zod.ts#ElementRecordPickerPropsParsed (type)", "ElementRecordPickerPropsSchema": "src/ui/component.zod.ts#ElementRecordPickerPropsSchema (const)", + "ElementRepeaterPropsSchema": "src/ui/component.zod.ts#ElementRepeaterPropsSchema (const)", "ElementTextInputPropsSchema": "src/ui/component.zod.ts#ElementTextInputPropsSchema (const)", "ElementTextPropsSchema": "src/ui/component.zod.ts#ElementTextPropsSchema (const)", "ExpandViewResult": "src/ui/view.zod.ts#ExpandViewResult (interface)", diff --git a/packages/spec/json-schema.manifest/ui.json b/packages/spec/json-schema.manifest/ui.json index f3f25a5dbe8..d3c58865bf9 100644 --- a/packages/spec/json-schema.manifest/ui.json +++ b/packages/spec/json-schema.manifest/ui.json @@ -5,7 +5,11 @@ "ui/AIChatWindowProps", "ui/Action", "ui/ActionAi", + "ui/ActionButtonProps", + "ui/ActionGroupProps", + "ui/ActionIconProps", "ui/ActionLocation", + "ui/ActionMenuProps", "ui/ActionNavItem", "ui/ActionParam", "ui/ActionSession", @@ -49,12 +53,14 @@ "ui/DocNavItem", "ui/ElementButtonProps", "ui/ElementDataSource", + "ui/ElementDefinitionListProps", "ui/ElementFilterProps", "ui/ElementFormProps", "ui/ElementImageProps", "ui/ElementMetadataViewerProps", "ui/ElementNumberProps", "ui/ElementRecordPickerProps", + "ui/ElementRepeaterProps", "ui/ElementTextInputProps", "ui/ElementTextProps", "ui/ExpressionBindableTextKey", diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts index f51e1475899..e029ea73fb9 100644 --- a/packages/spec/src/type-alias-convention.pin.test.ts +++ b/packages/spec/src/type-alias-convention.pin.test.ts @@ -275,7 +275,7 @@ import type * as M187 from './shared/duration.zod.js'; import type * as M188 from './ai/build-progress.zod.js'; // --------------------------------------------------------------------------- -// 786 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. +// 787 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. // // That number is machine-checked, not hand-kept. The runtime companion at the // bottom of this file recomputes the pin count from the source and asserts that @@ -1498,6 +1498,11 @@ export type Iso_ui_chart__ChartTypeSchema = Assert, z.infer< typeof M170.ElementDefinitionListPropsSchema > >>; export type Iso_ui_component__ObjectFormPropsSchema = Assert, z.infer< typeof M170.ObjectFormPropsSchema > >>; export type Iso_ui_component__ObjectMasterDetailFormPropsSchema = Assert, z.infer< typeof M170.ObjectMasterDetailFormPropsSchema > >>; export type Iso_ui_component__PageContainerProps = Assert, z.infer< typeof M170.PageContainerProps > >>; @@ -1658,7 +1663,7 @@ describe('ADR-0122 type-alias convention', () => { // this title and the section header above the pin list — are now asserted // against the recomputed count below, so neither can go stale without a red // test naming it. - it('still declares all 786 isomorphic pins', () => { + it('still declares all 787 isomorphic pins', () => { // The truth of each pin is proved by tsc, not here — an `Assert>` // that stops holding is a compile error with the alias named. What tsc // cannot notice is a pin that was DELETED: removing the assertion removes @@ -2305,7 +2310,19 @@ describe('ADR-0122 type-alias convention', () => { // touch disjoint pins (M22's three, M14's one); #17158 landed first, so // this entry's arrow starts from its 787. The count below was re-derived // from the merged file, not added up. -1 removed. - expect(pins).toHaveLength(786); + // + // 786 -> 787 is #20371's `ElementDefinitionListPropsSchema` — the + // `element:definition-list` row of `ComponentPropsMap` (ui/component.zod.ts, + // module slot M170), the (RISE) case once. Isomorphism MEASURED, not + // assumed: a strict item of one `z.string()` and one `z.unknown()`, a + // `z.literal([1, 2])` and a `z.boolean()`, every member optional but the + // item's `term`, with no `.default()`, `.transform()`, `.catch()` or + // `.pipe()` anywhere. Its five siblings in that card are NOT here: the four + // `action:*` rows carry `EvaluatedExpressionInputSchema` on `visible` (and + // `disabled`), whose bare-string arm transforms to the canonical envelope, + // and `element:repeater` carries `ViewFilterRuleSchema` on `filter` — so + // each declares an `XParsed` alias instead. +1 added. + expect(pins).toHaveLength(787); // The count is stated in PROSE twice as well — this case's title and the // section header above the pin list — and until #6605 nothing read either diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index 0ab4444d3a9..21c7e0490b9 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -2845,6 +2845,14 @@ export const ActionButtonPropsSchema = lazySchema(() => strictObject({ resultDialog: z.unknown().optional().describe('One-shot result dialog for a value the response shows exactly once, forwarded to the runner'), onSuccess: z.unknown().optional().describe('Declared post-success navigation, forwarded to the runner'), })); +/** Author state (ADR-0122: the bare name is the author state). */ +export type ActionButtonProps = z.input; +/** + * ADR-0122: the parsed state differs from the authored state on `visible` and `disabled` — the + * bare-string arm of the condition union normalizes to the canonical + * `{ dialect, source }` envelope (`EvaluatedExpressionInputSchema`'s transform). + */ +export type ActionButtonPropsParsed = z.infer; /** * `action:icon` — an icon-only action button with a tooltip @@ -2915,6 +2923,14 @@ export const ActionIconPropsSchema = lazySchema(() => strictObject({ resultDialog: z.unknown().optional().describe('One-shot result dialog for a value the response shows exactly once, forwarded to the runner'), onSuccess: z.unknown().optional().describe('Declared post-success navigation, forwarded to the runner'), })); +/** Author state (ADR-0122: the bare name is the author state). */ +export type ActionIconProps = z.input; +/** + * ADR-0122: the parsed state differs from the authored state on `visible` and `disabled` — the + * bare-string arm of the condition union normalizes to the canonical + * `{ dialect, source }` envelope (`EvaluatedExpressionInputSchema`'s transform). + */ +export type ActionIconPropsParsed = z.infer; /** * The member list `action:group` and `action:menu` both read — a LIST, as the @@ -2991,6 +3007,14 @@ export const ActionGroupPropsSchema = lazySchema(() => strictObject({ visible: actionCondition().optional() .describe('Visibility predicate for the whole group — a boolean, a CEL string, or a `{ dialect, source }` envelope, evaluated against the row the host binds. Omit for always-visible'), })); +/** Author state (ADR-0122: the bare name is the author state). */ +export type ActionGroupProps = z.input; +/** + * ADR-0122: the parsed state differs from the authored state on `visible` — the + * bare-string arm of the condition union normalizes to the canonical + * `{ dialect, source }` envelope (`EvaluatedExpressionInputSchema`'s transform). + */ +export type ActionGroupPropsParsed = z.infer; /** * `action:menu` — a dropdown ("more") menu of actions @@ -3030,6 +3054,14 @@ export const ActionMenuPropsSchema = lazySchema(() => strictObject({ visible: actionCondition().optional() .describe('Visibility predicate for the whole menu — a boolean, a CEL string, or a `{ dialect, source }` envelope, evaluated against the row the host binds; a predicate that fails to evaluate hides it. Omit for always-visible'), })); +/** Author state (ADR-0122: the bare name is the author state). */ +export type ActionMenuProps = z.input; +/** + * ADR-0122: the parsed state differs from the authored state on `visible` — the + * bare-string arm of the condition union normalizes to the canonical + * `{ dialect, source }` envelope (`EvaluatedExpressionInputSchema`'s transform). + */ +export type ActionMenuPropsParsed = z.infer; /** * `element:definition-list` — a compact key/value `
` @@ -3085,6 +3117,12 @@ export const ElementDefinitionListPropsSchema = lazySchema(() => strictObject({ inline: z.boolean().optional() .describe('Put each term and its description on one baseline-aligned row instead of stacking them'), })); +/** + * Author state (ADR-0122). No `XParsed`: the tree carries no default, transform, + * catch or pipe, so the two shapes coincide and the schema is pinned isomorphic in + * `type-alias-convention.pin.test.ts` instead. + */ +export type ElementDefinitionListProps = z.input; /** * `element:repeater` — a data-bound, chrome-free list: one line per record @@ -3158,6 +3196,15 @@ export const ElementRepeaterPropsSchema = lazySchema(() => strictObject({ divided: z.boolean().optional() .describe('Draw a separator between lines (renderer default: true)'), })); +/** Author state (ADR-0122: the bare name is the author state). */ +export type ElementRepeaterProps = z.input; +/** + * ADR-0122: the parsed state differs from the authored state on exactly one + * key — `filter` carries `ViewFilterRuleSchema`, whose `operator` is + * normalized on parse (why `ViewFilterRuleParsed` exists), the route + * `element:number` and `element:record_picker` took. + */ +export type ElementRepeaterPropsParsed = z.infer; /** * ---------------------------------------------------------------------- From cd4df9a08e5638c7b8fb1cb82f1c96e5ca39fc7f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 07:24:42 +0000 Subject: [PATCH 03/13] chore(spec): regenerate api-surface, export-origins and reference docs for the six rows Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW Co-authored-by: Claude --- content/docs/references/ui/component.mdx | 2 +- packages/spec/api-surface/ui.json | 11 +++++++++++ packages/spec/export-origins/ui.json | 11 +++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index 08c4340e48b..c9f509f2123 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -13,7 +13,7 @@ description: "Component schemas of the ObjectStack UI Protocol: AIChatWindowProp ```typescript import { AIChatWindowProps, ActionButtonPropsSchema, ActionGroupPropsSchema, ActionIconPropsSchema, ActionMenuPropsSchema, ElementButtonPropsSchema, ElementDefinitionListPropsSchema, ElementFilterPropsSchema, ElementFormPropsSchema, ElementImagePropsSchema, ElementMetadataViewerPropsSchema, ElementNumberPropsSchema, ElementRecordPickerPropsSchema, ElementRepeaterPropsSchema, ElementTextInputPropsSchema, ElementTextPropsSchema, ObjectCalendarPropsSchema, ObjectFormPropsSchema, ObjectGanttPropsSchema, ObjectGridPropsSchema, ObjectKanbanPropsSchema, ObjectMapPropsSchema, ObjectMasterDetailFormPropsSchema, ObjectMetricPropsSchema, ObjectTimelinePropsSchema, ObjectTreePropsSchema, PageAccordionProps, PageCardProps, PageContainerProps, PageHeaderProps, PageTabsProps, RecordActivityProps, RecordAlertActionSchema, RecordAlertProps, RecordChatterProps, RecordDetailsProps, RecordHighlightsField, RecordHighlightsProps, RecordHistoryProps, RecordPathProps, RecordQuickActionsProps, RecordReferenceRailProps, RecordRelatedListProps, ReferenceRailEntrySchema } from '@objectstack/spec/ui'; -import type { ElementNumberProps, ElementRecordPickerProps, ObjectCalendarProps, ObjectFormProps, ObjectGanttProps, ObjectGridProps, ObjectKanbanProps, ObjectMapProps, ObjectMasterDetailFormProps, ObjectMetricProps, ObjectTimelineProps, ObjectTreeProps, PageContainerProps, RecordAlertAction, RecordAlertProps, RecordHighlightsField, RecordHistoryProps, RecordPathProps, RecordQuickActionsProps, RecordReferenceRailProps, ReferenceRailEntry } from '@objectstack/spec/ui'; +import type { ActionButtonProps, ActionGroupProps, ActionIconProps, ActionMenuProps, ElementDefinitionListProps, ElementNumberProps, ElementRecordPickerProps, ElementRepeaterProps, ObjectCalendarProps, ObjectFormProps, ObjectGanttProps, ObjectGridProps, ObjectKanbanProps, ObjectMapProps, ObjectMasterDetailFormProps, ObjectMetricProps, ObjectTimelineProps, ObjectTreeProps, PageContainerProps, RecordAlertAction, RecordAlertProps, RecordHighlightsField, RecordHistoryProps, RecordPathProps, RecordQuickActionsProps, RecordReferenceRailProps, ReferenceRailEntry } from '@objectstack/spec/ui'; // Validate data const result = AIChatWindowProps.parse(data); diff --git a/packages/spec/api-surface/ui.json b/packages/spec/api-surface/ui.json index 9922f0389de..b8af2cf6a02 100644 --- a/packages/spec/api-surface/ui.json +++ b/packages/spec/api-surface/ui.json @@ -11,14 +11,22 @@ "ActionAi (type)", "ActionAiParsed (type)", "ActionAiSchema (const)", + "ActionButtonProps (type)", + "ActionButtonPropsParsed (type)", "ActionButtonPropsSchema (const)", "ActionEngineFacade (interface)", + "ActionGroupProps (type)", + "ActionGroupPropsParsed (type)", "ActionGroupPropsSchema (const)", "ActionHandler (type)", "ActionHandlerContext (interface)", + "ActionIconProps (type)", + "ActionIconPropsParsed (type)", "ActionIconPropsSchema (const)", "ActionLocation (type)", "ActionLocationSchema (const)", + "ActionMenuProps (type)", + "ActionMenuPropsParsed (type)", "ActionMenuPropsSchema (const)", "ActionNavItem (type)", "ActionNavItemParsed (type)", @@ -150,6 +158,7 @@ "ElementDataSource (type)", "ElementDataSourceParsed (type)", "ElementDataSourceSchema (const)", + "ElementDefinitionListProps (type)", "ElementDefinitionListPropsSchema (const)", "ElementFilterPropsSchema (const)", "ElementFormPropsSchema (const)", @@ -161,6 +170,8 @@ "ElementRecordPickerProps (type)", "ElementRecordPickerPropsParsed (type)", "ElementRecordPickerPropsSchema (const)", + "ElementRepeaterProps (type)", + "ElementRepeaterPropsParsed (type)", "ElementRepeaterPropsSchema (const)", "ElementTextInputPropsSchema (const)", "ElementTextPropsSchema (const)", diff --git a/packages/spec/export-origins/ui.json b/packages/spec/export-origins/ui.json index 6a6208d1e4b..b66bfd5ccfa 100644 --- a/packages/spec/export-origins/ui.json +++ b/packages/spec/export-origins/ui.json @@ -10,14 +10,22 @@ "ActionAi": "src/ui/action.zod.ts#ActionAi (type)", "ActionAiParsed": "src/ui/action.zod.ts#ActionAiParsed (type)", "ActionAiSchema": "src/ui/action.zod.ts#ActionAiSchema (const)", + "ActionButtonProps": "src/ui/component.zod.ts#ActionButtonProps (type)", + "ActionButtonPropsParsed": "src/ui/component.zod.ts#ActionButtonPropsParsed (type)", "ActionButtonPropsSchema": "src/ui/component.zod.ts#ActionButtonPropsSchema (const)", "ActionEngineFacade": "src/ui/action-params.zod.ts#ActionEngineFacade (interface)", + "ActionGroupProps": "src/ui/component.zod.ts#ActionGroupProps (type)", + "ActionGroupPropsParsed": "src/ui/component.zod.ts#ActionGroupPropsParsed (type)", "ActionGroupPropsSchema": "src/ui/component.zod.ts#ActionGroupPropsSchema (const)", "ActionHandler": "src/ui/action-params.zod.ts#ActionHandler (type)", "ActionHandlerContext": "src/ui/action-params.zod.ts#ActionHandlerContext (interface)", + "ActionIconProps": "src/ui/component.zod.ts#ActionIconProps (type)", + "ActionIconPropsParsed": "src/ui/component.zod.ts#ActionIconPropsParsed (type)", "ActionIconPropsSchema": "src/ui/component.zod.ts#ActionIconPropsSchema (const)", "ActionLocation": "src/ui/action.zod.ts#ActionLocation (type)", "ActionLocationSchema": "src/ui/action.zod.ts#ActionLocationSchema (const)", + "ActionMenuProps": "src/ui/component.zod.ts#ActionMenuProps (type)", + "ActionMenuPropsParsed": "src/ui/component.zod.ts#ActionMenuPropsParsed (type)", "ActionMenuPropsSchema": "src/ui/component.zod.ts#ActionMenuPropsSchema (const)", "ActionNavItem": "src/ui/app.zod.ts#ActionNavItem (type)", "ActionNavItemParsed": "src/ui/app.zod.ts#ActionNavItemParsed (type)", @@ -146,6 +154,7 @@ "ElementDataSource": "src/ui/page.zod.ts#ElementDataSource (type)", "ElementDataSourceParsed": "src/ui/page.zod.ts#ElementDataSourceParsed (type)", "ElementDataSourceSchema": "src/ui/page.zod.ts#ElementDataSourceSchema (const)", + "ElementDefinitionListProps": "src/ui/component.zod.ts#ElementDefinitionListProps (type)", "ElementDefinitionListPropsSchema": "src/ui/component.zod.ts#ElementDefinitionListPropsSchema (const)", "ElementFilterPropsSchema": "src/ui/component.zod.ts#ElementFilterPropsSchema (const)", "ElementFormPropsSchema": "src/ui/component.zod.ts#ElementFormPropsSchema (const)", @@ -157,6 +166,8 @@ "ElementRecordPickerProps": "src/ui/component.zod.ts#ElementRecordPickerProps (type)", "ElementRecordPickerPropsParsed": "src/ui/component.zod.ts#ElementRecordPickerPropsParsed (type)", "ElementRecordPickerPropsSchema": "src/ui/component.zod.ts#ElementRecordPickerPropsSchema (const)", + "ElementRepeaterProps": "src/ui/component.zod.ts#ElementRepeaterProps (type)", + "ElementRepeaterPropsParsed": "src/ui/component.zod.ts#ElementRepeaterPropsParsed (type)", "ElementRepeaterPropsSchema": "src/ui/component.zod.ts#ElementRepeaterPropsSchema (const)", "ElementTextInputPropsSchema": "src/ui/component.zod.ts#ElementTextInputPropsSchema (const)", "ElementTextPropsSchema": "src/ui/component.zod.ts#ElementTextPropsSchema (const)", From 836b19aef1d987a1cd64ab9a7ce9072b1433a0df Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 07:26:20 +0000 Subject: [PATCH 04/13] test(spec): pin the six rows' key sets, refusals and vocabulary admission Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW Co-authored-by: Claude --- ...omponent-action-element-rows-20371.test.ts | 340 ++++++++++++++++++ 1 file changed, 340 insertions(+) create mode 100644 packages/spec/src/ui/component-action-element-rows-20371.test.ts diff --git a/packages/spec/src/ui/component-action-element-rows-20371.test.ts b/packages/spec/src/ui/component-action-element-rows-20371.test.ts new file mode 100644 index 00000000000..89225a08c12 --- /dev/null +++ b/packages/spec/src/ui/component-action-element-rows-20371.test.ts @@ -0,0 +1,340 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// #20371 — six `ComponentPropsMap` rows for the curated objectui public blocks +// that had none: `action:button`, `action:group`, `action:menu`, `action:icon`, +// `element:definition-list`, `element:repeater`. +// +// Before these rows the four `action:*` types were skipped by the props gate +// (outside every namespace the enum populates) and the two `element:*` lists +// were refused as `component-type-unknown` (inside the reserved `element:` +// namespace with neither an enum member nor a row). Each row is strict from +// birth, with its key set measured from the renderer's read points at the +// `.objectui-sha` pin — the per-key citations live in `component.zod.ts`, +// section 4b. The key sets are asserted WHOLE, as #18305's were: a row derived +// from read points is a claim about a complete set, and only an equality holds +// a later addition to having been measured too. + +import { describe, expect, it } from 'vitest'; +import { + ActionButtonPropsSchema, + ActionGroupPropsSchema, + ActionIconPropsSchema, + ActionMenuPropsSchema, + ComponentPropsMap, + ElementDefinitionListPropsSchema, + ElementRepeaterPropsSchema, +} from './component.zod'; +import { + KNOWN_COMPONENT_TYPE_CANDIDATES, + STRING_ARM_REGISTERED_TYPES, + hasReservedComponentNamespace, + isKnownComponentType, +} from './component-type-vocabulary'; +import { PageComponentSchema, PageComponentType } from './page.zod'; + +type Issue = { code: string; path: PropertyKey[]; message: string; keys?: string[]; errors?: Issue[][] }; + +/** The issues of a failed safeParse — fails the test when the parse succeeded. */ +const issuesOf = (result: { success: boolean; error?: { issues: unknown[] } }): Issue[] => { + expect(result.success).toBe(false); + return result.error!.issues as Issue[]; +}; + +/** The one `unrecognized_keys` issue at the bag's root. */ +const unknownKeyIssue = (result: { success: boolean; error?: { issues: unknown[] } }): Issue => { + const at = issuesOf(result).filter((i) => i.code === 'unrecognized_keys' && i.path.length === 0); + expect(at).toHaveLength(1); + return at[0]!; +}; + +/** Top-level declared keys of a (lazy) strict object schema. */ +const keysOf = (schema: unknown): string[] => + Object.keys((schema as { shape: Record }).shape).sort(); + +const ROWS = { + 'action:button': ActionButtonPropsSchema, + 'action:group': ActionGroupPropsSchema, + 'action:menu': ActionMenuPropsSchema, + 'action:icon': ActionIconPropsSchema, + 'element:definition-list': ElementDefinitionListPropsSchema, + 'element:repeater': ElementRepeaterPropsSchema, +} as const; + +describe('the six rows exist and are the exported schemas (#20371)', () => { + it.each(Object.entries(ROWS))('`%s` dispatches to its exported schema', (type, schema) => { + expect(Object.keys(ComponentPropsMap)).toContain(type); + expect((ComponentPropsMap as Record)[type]).toBe(schema); + }); + + it('the page node still parses all six through the open `type` arm — the rows add no parse change there', () => { + for (const type of Object.keys(ROWS)) { + const node = PageComponentSchema.safeParse({ type, properties: { anything: 1 } }); + // The carrier stays an open bag: the rows are judged at the authoring + // door (the props gate), not by `PageComponentSchema.parse`. + expect(node.success, type).toBe(true); + } + }); +}); + +describe('key sets, asserted whole — measured from the renderers\' read points', () => { + // Forwarded to the action runner by `action:button` / `action:icon` + // (`execute({ ...forwarded })`). `undoable` / `recordIdField` are forwarded + // by the button only. + const FORWARDED = [ + 'params', 'description', 'target', 'openIn', 'endpoint', 'method', 'bodyExtra', 'bodyShape', + 'operation', 'patch', 'confirmText', 'successMessage', 'errorMessage', 'refreshAfter', + 'locations', 'toast', 'resultDialog', 'onSuccess', + ]; + + it('action:button', () => { + expect(keysOf(ActionButtonPropsSchema)).toEqual([ + 'name', 'label', 'icon', 'actionType', 'variant', 'size', 'visible', 'disabled', + ...FORWARDED, 'undoable', 'recordIdField', + ].sort()); + }); + + it('action:icon — no `size` (fixed icon size), no `undoable` / `recordIdField` (not forwarded)', () => { + expect(keysOf(ActionIconPropsSchema)).toEqual([ + 'name', 'label', 'icon', 'actionType', 'variant', 'visible', 'disabled', ...FORWARDED, + ].sort()); + }); + + it('action:group — no group-level `name` (never read)', () => { + expect(keysOf(ActionGroupPropsSchema)).toEqual( + ['actions', 'display', 'location', 'label', 'icon', 'variant', 'size', 'visible'].sort(), + ); + }); + + it('action:menu', () => { + expect(keysOf(ActionMenuPropsSchema)).toEqual( + ['actions', 'label', 'icon', 'variant', 'size', 'visible'].sort(), + ); + }); + + it('element:definition-list', () => { + expect(keysOf(ElementDefinitionListPropsSchema)).toEqual(['items', 'columns', 'inline'].sort()); + }); + + it('element:repeater', () => { + expect(keysOf(ElementRepeaterPropsSchema)).toEqual( + ['object', 'titleField', 'fields', 'filter', 'sort', 'limit', 'emptyText', 'divided'].sort(), + ); + }); + + it('no row declares a node key (`className` / `style` / `id` stay on the node)', () => { + for (const [type, schema] of Object.entries(ROWS)) { + for (const nodeKey of ['className', 'style', 'id', 'dataSource', 'responsiveStyles']) { + expect(keysOf(schema), `${type}.${nodeKey}`).not.toContain(nodeKey); + } + } + }); +}); + +describe('one accepted authored example per type, taken from objectui', () => { + it('action:button — the node objectui\'s AGENTS.md teaches', () => { + const authored = { label: 'Open details', actionType: 'url', target: '/users/ada' }; + expect(ActionButtonPropsSchema.parse(authored)).toEqual(authored); + }); + + it('action:icon — the registration\'s own defaults', () => { + const authored = { icon: 'play', actionType: 'script', variant: 'ghost' as const }; + expect(ActionIconPropsSchema.parse(authored)).toEqual(authored); + }); + + it('action:group — the registration\'s defaults with one member action', () => { + const authored = { + display: 'inline' as const, + variant: 'outline' as const, + size: 'sm' as const, + actions: [{ name: 'approve', label: 'Approve', type: 'api', target: '/api/approve' }], + }; + expect(ActionGroupPropsSchema.parse(authored)).toEqual(authored); + }); + + it('action:menu — the registration\'s defaults with one member action', () => { + const authored = { + variant: 'ghost' as const, + actions: [{ name: 'archive', label: 'Archive', type: 'script', tags: ['separator-before'] }], + }; + expect(ActionMenuPropsSchema.parse(authored)).toEqual(authored); + }); + + it('element:definition-list — objectui\'s own renderer specimen', () => { + const authored = { + items: [ + { term: 'Status', description: 'Active' }, + { term: 'Owner', description: 'Ada' }, + ], + }; + expect(ElementDefinitionListPropsSchema.parse(authored)).toEqual(authored); + }); + + it('element:repeater — objectui\'s own renderer specimen', () => { + const authored = { object: 'showcase_category', fields: ['name'], emptyText: 'Nothing here' }; + expect(ElementRepeaterPropsSchema.parse(authored)).toEqual(authored); + }); +}); + +describe('strict from birth — an unknown key is refused on every row', () => { + it.each(Object.entries(ROWS))('`%s` refuses an undeclared key, naming its surface', (type, schema) => { + const base = type === 'element:repeater' ? { object: 'task' } : {}; + const issue = unknownKeyIssue(schema.safeParse({ ...base, notARealProp: 1 })); + expect(issue.keys).toEqual(['notARealProp']); + expect(issue.message).toContain(`\`${type}\``); + }); + + it('a node key written into the bag gets the wrong-layer prescription, not a bare refusal', () => { + const issue = unknownKeyIssue(ActionButtonPropsSchema.safeParse({ label: 'X', className: 'mt-2' })); + expect(issue.keys).toEqual(['className']); + expect(issue.message).toContain('NODE'); + }); +}); + +describe('what the measurement decided, pinned', () => { + it('`name` is OPTIONAL on action:button — the renderer reads `schema.name ?? schema.label`', () => { + expect(ActionButtonPropsSchema.safeParse({ label: 'Save', actionType: 'script' }).success).toBe(true); + }); + + it('`type` is refused on action:button / action:icon and renamed to `actionType`', () => { + for (const schema of [ActionButtonPropsSchema, ActionIconPropsSchema]) { + const issue = unknownKeyIssue(schema.safeParse({ label: 'X', type: 'url' })); + expect(issue.keys).toEqual(['type']); + expect(issue.message).toContain('`actionType`'); + } + }); + + it('`enabled` and `autoTrigger` are read-but-not-authorable on action:button / action:icon', () => { + for (const schema of [ActionButtonPropsSchema, ActionIconPropsSchema]) { + for (const key of ['enabled', 'autoTrigger']) { + const issue = unknownKeyIssue(schema.safeParse({ label: 'X', [key]: true })); + expect(issue.keys).toEqual([key]); + } + } + }); + + it('action:icon refuses `size` — the renderer pins the icon size', () => { + expect(unknownKeyIssue(ActionIconPropsSchema.safeParse({ size: 'sm' })).keys).toEqual(['size']); + // Firing control: the button sibling reads `size`. + expect(ActionButtonPropsSchema.safeParse({ size: 'sm' }).success).toBe(true); + }); + + it('action:group refuses a group-level `name`, which the registration publishes and nothing reads', () => { + expect(unknownKeyIssue(ActionGroupPropsSchema.safeParse({ name: 'toolbar' })).keys).toEqual(['name']); + }); + + it('`variant` / `size`: `primary` and `md` only where the renderer maps them', () => { + // action:button maps both; action:icon maps `primary` (no `size` at all). + expect(ActionButtonPropsSchema.safeParse({ variant: 'primary', size: 'md' }).success).toBe(true); + expect(ActionIconPropsSchema.safeParse({ variant: 'primary' }).success).toBe(true); + // action:menu hands both to the Button primitive unmapped; action:group + // maps `md` on its dropdown trigger only, not in its default inline mode. + for (const schema of [ActionMenuPropsSchema, ActionGroupPropsSchema]) { + const variant = issuesOf(schema.safeParse({ variant: 'primary' })); + expect(variant.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_value', 'variant']]); + const size = issuesOf(schema.safeParse({ size: 'md' })); + expect(size.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_value', 'size']]); + } + }); + + it('`actions` is a LIST of action objects — a bare action-name list is refused', () => { + for (const schema of [ActionGroupPropsSchema, ActionMenuPropsSchema]) { + const issues = issuesOf(schema.safeParse({ actions: ['approve'] })); + expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'actions.0']]); + // The object form, and the record shape the registration publishes, is + // not a list either. + expect(schema.safeParse({ actions: { approve: {} } }).success).toBe(false); + } + }); + + it('a bare CEL `visible` normalizes to the canonical envelope; a boolean stays a literal', () => { + const parsed = ActionMenuPropsSchema.parse({ visible: "record.status == 'open'" }); + expect(parsed.visible).toEqual({ dialect: 'cel', source: "record.status == 'open'" }); + expect(ActionButtonPropsSchema.parse({ visible: false, disabled: true })).toEqual({ visible: false, disabled: true }); + }); + + it('definition-list `columns` is the NUMBER 1 or 2 — the registration\'s string spelling is refused', () => { + expect(ElementDefinitionListPropsSchema.safeParse({ columns: 2 }).success).toBe(true); + const issues = issuesOf(ElementDefinitionListPropsSchema.safeParse({ columns: '2' })); + expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_value', 'columns']]); + expect(issues[0]!.message).toContain('`columns: 2`'); + expect(ElementDefinitionListPropsSchema.safeParse({ columns: 3 }).success).toBe(false); + }); + + it('definition-list items are strict — the designer\'s old `label` / `value` pair is refused and renamed', () => { + const issues = issuesOf(ElementDefinitionListPropsSchema.safeParse({ items: [{ label: 'A', value: 1 }] })); + const keyIssue = issues.find((i) => i.code === 'unrecognized_keys'); + expect(keyIssue?.path).toEqual(['items', 0]); + expect(keyIssue?.keys).toEqual(['label', 'value']); + expect(keyIssue?.message).toContain('`term`'); + expect(keyIssue?.message).toContain('`description`'); + // `term` is required on an item; `items` itself is optional (the renderer + // shows its "No details" state for absent and empty alike). + expect(issues.some((i) => i.code === 'invalid_type' && i.path.join('.') === 'items.0.term')).toBe(true); + expect(ElementDefinitionListPropsSchema.safeParse({}).success).toBe(true); + }); + + it('repeater `object` is required — without it the list never queries', () => { + const issues = issuesOf(ElementRepeaterPropsSchema.safeParse({ fields: ['name'] })); + expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'object']]); + }); + + it('repeater `fields` takes a name or `{ field }`; the unrendered `label` is refused inside the union', () => { + expect(ElementRepeaterPropsSchema.safeParse({ object: 't', fields: ['a', { field: 'b' }] }).success).toBe(true); + const [union] = issuesOf(ElementRepeaterPropsSchema.safeParse({ object: 't', fields: [{ field: 'b', label: 'B' }] })); + expect(union!.code).toBe('invalid_union'); + expect(union!.path).toEqual(['fields', 0]); + // Exactly one arm judged keys, and only keys — the shape the props gate + // unpacks back onto its unknown-key rule. + const keyArms = union!.errors!.filter((arm) => arm.some((i) => i.code === 'unrecognized_keys')); + expect(keyArms).toHaveLength(1); + expect(keyArms[0]!.map((i) => i.keys)).toEqual([['label']]); + }); + + it('repeater `filter` / `sort` are the family\'s one orthography — the record form is refused', () => { + const ok = ElementRepeaterPropsSchema.safeParse({ + object: 'task', + filter: [{ field: 'status', operator: 'equals', value: 'open' }], + sort: [{ field: 'due_date', order: 'asc' }], + limit: 10, + }); + expect(ok.success).toBe(true); + const record = issuesOf(ElementRepeaterPropsSchema.safeParse({ object: 'task', filter: { status: 'open' } })); + expect(record.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'filter']]); + const limit = issuesOf(ElementRepeaterPropsSchema.safeParse({ object: 'task', limit: 0 })); + expect(limit.map((i) => [i.code, i.path.join('.')])).toEqual([['too_small', 'limit']]); + }); + + it('repeater: the `object-*` family\'s `objectName` is refused and renamed to `object`', () => { + const issue = unknownKeyIssue(ElementRepeaterPropsSchema.safeParse({ object: 'task', objectName: 'task' })); + expect(issue.keys).toEqual(['objectName']); + expect(issue.message).toContain('`object`'); + }); +}); + +describe('the `element:` vocabulary admits the two lists through their rows', () => { + it('both are reserved-namespace AND known — `component-type-unknown` no longer refuses them', () => { + for (const type of ['element:definition-list', 'element:repeater']) { + expect(hasReservedComponentNamespace(type), type).toBe(true); + expect(isKnownComponentType(type), type).toBe(true); + expect(KNOWN_COMPONENT_TYPE_CANDIDATES, type).toContain(type); + } + // Firing control: a typo inside the same namespace is still unknown, so + // the claim did not open the namespace — it named two members of it. + expect(isKnownComponentType('element:repeatr')).toBe(false); + expect(hasReservedComponentNamespace('element:repeatr')).toBe(true); + }); + + it('they join by ROW, not by enum member or string-arm ledger entry (the `element:metadata_viewer` shape)', () => { + for (const type of ['element:definition-list', 'element:repeater']) { + expect(PageComponentType.options as readonly string[]).not.toContain(type); + expect(STRING_ARM_REGISTERED_TYPES).not.toContain(type); + } + }); + + it('the four `action:*` types stay outside every reserved namespace — the rows add no vocabulary claim', () => { + for (const type of ['action:button', 'action:group', 'action:menu', 'action:icon']) { + expect(hasReservedComponentNamespace(type), type).toBe(false); + expect(isKnownComponentType(type), type).toBe(true); + } + }); +}); From 158daae2603c4aea030b79192ed568b5299f8c74 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 07:37:06 +0000 Subject: [PATCH 05/13] chore(changeset): spec minor for the six ComponentPropsMap rows Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW Co-authored-by: Claude --- ...371-component-props-action-element-rows.md | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .changeset/20371-component-props-action-element-rows.md diff --git a/.changeset/20371-component-props-action-element-rows.md b/.changeset/20371-component-props-action-element-rows.md new file mode 100644 index 00000000000..227bb809163 --- /dev/null +++ b/.changeset/20371-component-props-action-element-rows.md @@ -0,0 +1,21 @@ +--- +"@objectstack/spec": minor +--- + +`ComponentPropsMap` declares `action:button`, `action:group`, `action:menu`, `action:icon`, `element:definition-list` and `element:repeater` — six blocks in objectui's curated public vocabulary that had no row (#20371). Each row is strict from birth, with its key set measured from the objectui renderer's own read points at the `.objectui-sha` pin, not transcribed from objectui's `UIActionSchema`, the registrations' `inputs`, or this package's object-metadata `ActionSchema`. + +Clause-②: yes + +Six new declared rows on a published surface, and two types the `element:` vocabulary now answers for, so the accept set a consumer writes against grows. Nothing previously accepted by a declared row is refused and nothing is retired. + +What changes at the authoring doors (`os validate` / `os build` / `os lint`): + +- **`element:definition-list` and `element:repeater` are no longer refused as `component-type-unknown`.** Both sit inside the reserved `element:` namespace; with no enum member and no row, the vocabulary refused them although objectui registers, publishes and offers both in the Studio page designer. They join the `element:` vocabulary through their rows (no enum member), and a typo inside the namespace (`element:repeatr`) is still refused. +- **The props gate now judges all six.** The four `action:*` types sat outside every reserved namespace, so an authored `properties` bag on them was skipped — a misspelled key parsed, stored and did nothing. Findings stay at the gate's existing warning tier. + +Measured decisions worth knowing when you author these blocks: + +- **`action:button` / `action:icon`** — `name` is optional (the renderer reads `name ?? label`). The executor is `actionType`; `type` inside `properties` is refused with a rename to `actionType` (on a page component `type` is the component itself). `visible` / `disabled` take a boolean, a CEL string or a `{ dialect, source }` envelope. The legacy `enabled` fallback and the host-only `autoTrigger` flag are refused with a prescription. `action:icon` reads no `size`. +- **`action:group` / `action:menu`** — `actions` is a LIST of action objects (a member's executor is its own `type`); a bare list of action names is refused. `action:group` reads no group-level `name`, so it is refused with a prescription. `variant` / `size` take the Button primitive's vocabulary; `primary` and `md` are accepted only on `action:button` (and `primary` on `action:icon`), where the renderer maps them. +- **`element:definition-list`** — `items` of strict `{ term, description? }`; `columns` is the NUMBER `1` or `2` (the string `'2'` renders one column and is refused). +- **`element:repeater`** — `object` is required; `filter` / `sort` take the family's one orthography (`ViewFilterRule[]`, `SortItem[]`); `fields` takes a field name or `{ field }` (an unrendered `label` is refused). From 7bd546c137758de8266ec732c02e1cd7020e7cce Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 07:52:44 +0000 Subject: [PATCH 06/13] fix(spec): baseline header totals for the repeater entry; drop an unresolved citation Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW Co-authored-by: Claude --- packages/spec/dropped-refinements.baseline.json | 4 ++-- packages/spec/src/ui/component.zod.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/spec/dropped-refinements.baseline.json b/packages/spec/dropped-refinements.baseline.json index 7cf2bf47919..b818696662f 100644 --- a/packages/spec/dropped-refinements.baseline.json +++ b/packages/spec/dropped-refinements.baseline.json @@ -2,8 +2,8 @@ "description": "Shrink-only ledger of every PUBLISHED JSON Schema that is STILL WIDER than the Zod type it was generated from, because a rule written as `.refine()` reaches the runtime and not the file (#18670). `z.toJSONSchema()` has no arm for a `custom` check: a plain record, the same record with a `.refine()`, and the same record with an ABORTING `.refine()` all project byte-identically (measured on zod 4.4.3, the version packages/spec resolves). So a document one of these files ACCEPTS can still be refused at parse time, and an author -- or an AI -- validating against packages/spec/json-schema/** finds out a release later. Each `sites` path is a position under that schema at which a refinement is dropped; the same paths are written onto the artifact itself as `x-dropped-refinements`. Item 2 closed the first patterns: a refinement DECLARED through the closed list in src/shared/refinement-projection.ts is emitted into the published file, reads `projected` rather than `dropped`, and its row LEAVES this ledger in the same PR -- which is why the ledger shrinks and never grows on a repair. Every refinement outside that closed list stays here, and adding an arm to the list is a public-contract decision, not a refactor. Hand-edited on purpose and with no `gen:` script: a generator would let a new gap be admitted by running a command instead of by a decision, which is the silence this ledger exists to end. Adding, removing or moving a site fails packages/spec/scripts/build-schemas.ts until the line moves with it, and the failure prints the corrected entry in full. ⛔ Do not delete or weaken a refinement to shorten this file -- the runtime rule is correct; it is the projection that is silent, and the remedy is to teach the closed list a NAMED pattern, never to drop the rule.", "measured": { "zod": "4.4.3", - "publishedSchemasWithDroppedRefinements": 211, - "droppedRefinementSites": 609, + "publishedSchemasWithDroppedRefinements": 212, + "droppedRefinementSites": 610, "refinementSitesThatDidProject": 369, "refinementSitesWithNoJsonFormToCompare": 0 }, diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index 21c7e0490b9..2ff92383022 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -2746,7 +2746,7 @@ const BUTTON_PRIMITIVE_SIZES = ['default', 'sm', 'lg', 'icon'] as const; * discriminator, and the hoist refuses to copy a `properties.type` onto the * node, so an executor written there is read by nothing. objectui renamed * the input to `actionType` with no alias and no transition window - * (objectui#7415, objectstack#14490 ruling A); an author copying an + * (objectui#7415); an author copying an * `ActionSchema` entry, whose executor IS `type`, brings that spelling along. * - `visibleWhen` / `visibility` → `visible`: the `record:alert` pair. The * renderer evaluates `visible` itself, so an author bringing the node From d43666c19aaca9800cd68c58cbc0b4b360ba858e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 08:27:49 +0000 Subject: [PATCH 07/13] chore(spec): regenerate the merged generated artifacts (api-surface, authorable-surface, export-origins, declaration-map) Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW Co-authored-by: Claude --- packages/spec/api-surface/ui.json | 3 +++ packages/spec/authorable-surface/ui.json | 2 +- packages/spec/declaration-map/ui.json | 6 ++++++ packages/spec/export-origins/ui.json | 3 +++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/spec/api-surface/ui.json b/packages/spec/api-surface/ui.json index b8af2cf6a02..789c7f53cbc 100644 --- a/packages/spec/api-surface/ui.json +++ b/packages/spec/api-surface/ui.json @@ -233,6 +233,7 @@ "InterfacePageConfigParsed (type)", "InterfacePageConfigSchema (const)", "JoinedReportBlock (type)", + "JoinedReportBlockParsed (type)", "JoinedReportBlockSchema (const)", "KNOWN_COMPONENT_TYPES (const)", "KNOWN_COMPONENT_TYPE_CANDIDATES (const)", @@ -437,8 +438,10 @@ "ViewItem (type)", "ViewItemName (type)", "ViewItemNameSchema (const)", + "ViewItemParsed (type)", "ViewItemSchema (const)", "ViewItemWire (type)", + "ViewItemWireParsed (type)", "ViewItemWireSchema (const)", "ViewKeyCollision (interface)", "ViewKind (type)", diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index f944f66ae42..a6e0dd2f679 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -14,7 +14,7 @@ "ui/Action:_packageVersion", "ui/Action:_provenance", "ui/Action:ai", - "ui/Action:aria", + "ui/Action:aria [RETIRED]", "ui/Action:body", "ui/Action:bodyExtra", "ui/Action:bodyShape", diff --git a/packages/spec/declaration-map/ui.json b/packages/spec/declaration-map/ui.json index e3696b7ad74..31ffbe0df1b 100644 --- a/packages/spec/declaration-map/ui.json +++ b/packages/spec/declaration-map/ui.json @@ -6,11 +6,15 @@ "Action": "ui/Action", "ActionAi": "ui/ActionAi", "ActionAiSchema": "ui/ActionAi", + "ActionButtonProps": "ui/ActionButtonProps", "ActionButtonPropsSchema": "ui/ActionButtonProps", + "ActionGroupProps": "ui/ActionGroupProps", "ActionGroupPropsSchema": "ui/ActionGroupProps", + "ActionIconProps": "ui/ActionIconProps", "ActionIconPropsSchema": "ui/ActionIconProps", "ActionLocation": "ui/ActionLocation", "ActionLocationSchema": "ui/ActionLocation", + "ActionMenuProps": "ui/ActionMenuProps", "ActionMenuPropsSchema": "ui/ActionMenuProps", "ActionNavItem": "ui/ActionNavItem", "ActionNavItemSchema": "ui/ActionNavItem", @@ -96,6 +100,7 @@ "ElementButtonPropsSchema": "ui/ElementButtonProps", "ElementDataSource": "ui/ElementDataSource", "ElementDataSourceSchema": "ui/ElementDataSource", + "ElementDefinitionListProps": "ui/ElementDefinitionListProps", "ElementDefinitionListPropsSchema": "ui/ElementDefinitionListProps", "ElementFilterPropsSchema": "ui/ElementFilterProps", "ElementFormPropsSchema": "ui/ElementFormProps", @@ -105,6 +110,7 @@ "ElementNumberPropsSchema": "ui/ElementNumberProps", "ElementRecordPickerProps": "ui/ElementRecordPickerProps", "ElementRecordPickerPropsSchema": "ui/ElementRecordPickerProps", + "ElementRepeaterProps": "ui/ElementRepeaterProps", "ElementRepeaterPropsSchema": "ui/ElementRepeaterProps", "ElementTextInputPropsSchema": "ui/ElementTextInputProps", "ElementTextPropsSchema": "ui/ElementTextProps", diff --git a/packages/spec/export-origins/ui.json b/packages/spec/export-origins/ui.json index b66bfd5ccfa..a46afed5f30 100644 --- a/packages/spec/export-origins/ui.json +++ b/packages/spec/export-origins/ui.json @@ -229,6 +229,7 @@ "InterfacePageConfigParsed": "src/ui/page.zod.ts#InterfacePageConfigParsed (type)", "InterfacePageConfigSchema": "src/ui/page.zod.ts#InterfacePageConfigSchema (const)", "JoinedReportBlock": "src/ui/report.zod.ts#JoinedReportBlock (type)", + "JoinedReportBlockParsed": "src/ui/report.zod.ts#JoinedReportBlockParsed (type)", "JoinedReportBlockSchema": "src/ui/report.zod.ts#JoinedReportBlockSchema (const)", "KNOWN_COMPONENT_TYPES": "src/ui/component-type-vocabulary.ts#KNOWN_COMPONENT_TYPES (const)", "KNOWN_COMPONENT_TYPE_CANDIDATES": "src/ui/component-type-vocabulary.ts#KNOWN_COMPONENT_TYPE_CANDIDATES (const)", @@ -423,8 +424,10 @@ "ViewItem": "src/ui/view.zod.ts#ViewItem (type)", "ViewItemName": "src/ui/view.zod.ts#ViewItemName (type)", "ViewItemNameSchema": "src/ui/view.zod.ts#ViewItemNameSchema (const)", + "ViewItemParsed": "src/ui/view.zod.ts#ViewItemParsed (type)", "ViewItemSchema": "src/ui/view.zod.ts#ViewItemSchema (const)", "ViewItemWire": "src/ui/view.zod.ts#ViewItemWire (type)", + "ViewItemWireParsed": "src/ui/view.zod.ts#ViewItemWireParsed (type)", "ViewItemWireSchema": "src/ui/view.zod.ts#ViewItemWireSchema (const)", "ViewKeyCollision": "src/ui/view.zod.ts#ViewKeyCollision (interface)", "ViewKind": "src/ui/view.zod.ts#ViewKind (type)", From fa5631fb2f626ffdd0a659a00acfc5c663e54b6d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 10:25:23 +0000 Subject: [PATCH 08/13] chore(spec): regenerate the reference index and strictness-ledger counts on the merged tree Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW Co-authored-by: Claude --- content/docs/references/index.mdx | 10 +++++----- .../2026-07-unknown-key-strictness-ledger.counts.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index a12510933a2..950a42eba1b 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,6 +1,6 @@ --- title: Protocol Reference -description: Every schema published by @objectstack/spec — 1529 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1522 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -24,7 +24,7 @@ counts are sums of the rows they head. Regenerate with | [Automation Protocol](/docs/references/automation) | 14 | 75 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. | | [Data Protocol](/docs/references/data) | 29 | 175 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | | [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. | -| [Integration Protocol](/docs/references/integration) | 1 | 24 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. | +| [Integration Protocol](/docs/references/integration) | 1 | 17 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. | | [Kernel Protocol](/docs/references/kernel) | 30 | 157 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. | | [Marketplace Protocol](/docs/references/marketplace) | 4 | 30 | The package & marketplace format — package identity and versions, listing, publish, review, search, install, template manifests. | | [QA Protocol](/docs/references/qa) | 1 | 8 | Declarative test suites — scenarios, steps, actions and assertions. | @@ -33,7 +33,7 @@ counts are sums of the rows they head. Regenerate with | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 34 | 275 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | | [UI Protocol](/docs/references/ui) | 16 | 165 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **196** | **1529** | 14 protocol modules | +| **Total** | **196** | **1522** | 14 protocol modules | --- @@ -186,13 +186,13 @@ Users and accounts, organizations, positions, SCIM provisioning. ## Integration Protocol -**Source:** `packages/spec/src/integration/` · **Import:** `@objectstack/spec/integration` · **1 page, 24 schemas** +**Source:** `packages/spec/src/integration/` · **Import:** `@objectstack/spec/integration` · **1 page, 17 schemas** The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. | File | Schemas | | :--- | :--- | -| [`connector.zod.ts`](/docs/references/integration/connector) | `CircuitBreakerConfig`, `Connector`, `ConnectorAction`, `ConnectorActionEffect`, `ConnectorConflictResolution`, `ConnectorFieldMapping`, `ConnectorHealth`, `ConnectorInstanceAPIKeyAuth`, `ConnectorInstanceAuth`, `ConnectorInstanceBasicAuth`, `ConnectorInstanceBearerAuth`, `ConnectorInstanceNoAuth`, `ConnectorRetryStrategy`, `ConnectorStatus`, `ConnectorTrigger`, `ConnectorType`, `DataSyncConfig`, `DeclarativeConnectorEntry`, `HealthCheckConfig`, `RetryConfig`, `SyncStrategy`, `WebhookConfig`, `WebhookEvent`, `WebhookSignatureAlgorithm` | +| [`connector.zod.ts`](/docs/references/integration/connector) | `Connector`, `ConnectorAction`, `ConnectorActionEffect`, `ConnectorConflictResolution`, `ConnectorFieldMapping`, `ConnectorInstanceAPIKeyAuth`, `ConnectorInstanceAuth`, `ConnectorInstanceBasicAuth`, `ConnectorInstanceBearerAuth`, `ConnectorInstanceNoAuth`, `ConnectorRetryStrategy`, `ConnectorTrigger`, `ConnectorType`, `DataSyncConfig`, `DeclarativeConnectorEntry`, `RetryConfig`, `SyncStrategy` | --- diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md index 434e0069c35..0133c3b34d0 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md @@ -259,7 +259,7 @@ directory rather than per file. | `ai/` | 78 | | `api/` | 431 | | `identity/` | 32 | -| `integration/` | 8 | +| `integration/` | 5 | | `kernel/` | 247 | | `marketplace/` | 29 | | `qa/` | 6 | From 29fd4591e4c58735a265b1a19df759acd05471d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 10:57:21 +0000 Subject: [PATCH 09/13] test(dogfood): classify the action:* page-block visible/disabled expression surfaces Three ADR-0060 conformance rows for the six predicate positions the new ComponentPropsMap rows declare, split by the fault face the objectui renderers and SchemaRenderer's node gate compose to at the pin. Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW Co-authored-by: Claude --- .../test/expression-conformance.ledger.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/qa/dogfood/test/expression-conformance.ledger.ts b/packages/qa/dogfood/test/expression-conformance.ledger.ts index c7a4e63478a..b224920fd8b 100644 --- a/packages/qa/dogfood/test/expression-conformance.ledger.ts +++ b/packages/qa/dogfood/test/expression-conformance.ledger.ts @@ -392,6 +392,56 @@ export const EXPRESSION_SURFACE: ExprSurface[] = [ covers: ['ui/component.zod.ts:RecordAlertProps.visible'], note: 'A SEPARATE row from `cel-ui` on purpose, on both axes that row fixes at once: `cel-ui` is `fail-soft-log` (its form-view section/field predicates fault OPEN) and its evaluator is the SchemaRenderer, while this one faults closed through the record:alert renderer. ⚠️ And the honest limit, measured here: because `PageComponentSchema.properties` is an opaque bag served verbatim, a predicate authored in `properties` on a raw `Page` object literal never reaches `ExpressionInputSchema`\'s transform at all — a bare string there stays on the console\'s LEGACY JS evaluator, which has no `has()`, and only an explicit `{dialect:"cel"}` envelope routes to CEL. So the SCHEMA declares the CEL contract this row records while the page path can still deliver the legacy one; the platform\'s sys_user page carries that reading at its own declaration site and gates on the component-NODE `visibleWhen` instead. ⛔ NOT MEASURED HERE: the renderer, which is in objectui and not in this checkout.', }, + // #20371 — the `action:*` page-component rows. Each renderer evaluates its own + // `visible` / `disabled` off the hoisted `properties` bag, AND `SchemaRenderer`'s + // node gate evaluates the same hoisted keys first, so every surface below has + // two evaluation legs; the row states what the COMPOSITION does to a predicate + // that faults. Split three ways because the faces differ, per renderer — the + // collision rule `cel-action-visible` / `cel-action-disabled` were split on. + // Read (not run) at the `.objectui-sha` pin `f8a9d0fb0596`: the + // renderers are `components/src/renderers/action/action-{button,menu,icon,group}.tsx`, + // `useCondition` is `react/src/hooks/useExpression.ts:200-244`, the node gate's + // two legs are `react/src/SchemaRenderer.tsx` `evaluateVisibilityPredicate` + // (:1048) and `evaluateEnablementPredicate` (:1138, :1772-1784). Tracker anchors + // kept out of the strings below (`check:doc-authoring`): the node gate's fault + // report is objectui#6038 (visibility legs) and objectui#6445 (enablement legs); + // the empty-`disabled` fix `cel-action-disabled` cites is objectui#3842. + { + id: 'cel-action-block-visible-closed', + summary: '`action:button` / `action:menu` page-block visibility (props `visible`) — the block is not rendered when the predicate is FALSE', + dialect: 'cel', mode: 'interpret', state: 'enforced', failPolicy: 'fail-closed', + enforcement: + 'BUILD-TIME GATE, measured here: lint/page-envelope-audit.ts door 3 parses `ComponentPropsMap["action:button" | "action:menu"]`, the one door that reaches `properties.visible`. EVALUATOR, two legs: (1) SchemaRenderer node gate `evaluateVisibilityPredicate` on the hoisted value, page scope — a fault there answers `evaluateCondition`\'s fail-soft `true` (shown) and is REPORTED in every build; (2) the renderer\'s own `useCondition(toPredicateInput(schema.visible), recordData, { throwOnError: true })` (action-button.tsx:116-119, action-menu.tsx:219-222) against the host-bound row — a fault returns `false` and warns once per predicate (useExpression.ts:215-238), and the block returns null (action-button.tsx:298, action-menu.tsx:293). The legs AND, so a faulting predicate HIDES the block: fail-CLOSED, the face `cel-action-visible` records for a registered action', + covers: [ + 'ui/component.zod.ts:ActionButtonPropsSchema.visible', + 'ui/component.zod.ts:ActionMenuPropsSchema.visible', + ], + note: 'Separate from `cel-action-visible` (a registered object action, keyed by `name` in `actions[]`, drawn by `ActionEngine`) because this slot is a PAGE component\'s props bag drawn through `SchemaRenderer`, and separate from `cel-action-block-visible-soft` because `action:icon` / `action:group` evaluate the same key WITHOUT `throwOnError` and fault the other way. ⚠️ The same honest limit `cel-record-alert-visible` records, and for the same reason: `PageComponentSchema.properties` is an opaque bag served verbatim, so a bare string never passes `EvaluatedExpressionInputSchema`\'s transform on the page path and reaches the console\'s LEGACY JS evaluator (`evaluateCondition` routes only an explicit `{dialect:"cel"}` envelope to CEL); the pin carries that envelope through the bag loops intact (`preservePredicateEnvelope`, SchemaRenderer.tsx:469). ⛔ NOT MEASURED HERE: the renderers were read at the pin, not run.', + }, + { + id: 'cel-action-block-visible-soft', + summary: '`action:icon` / `action:group` page-block visibility (props `visible`) — the block is not rendered when the predicate is FALSE', + dialect: 'cel', mode: 'interpret', state: 'enforced', failPolicy: 'fail-soft-log', + enforcement: + 'BUILD-TIME GATE, measured here: lint/page-envelope-audit.ts door 3 parses `ComponentPropsMap["action:icon" | "action:group"]`. EVALUATOR, two legs: (1) SchemaRenderer node gate `evaluateVisibilityPredicate` on the hoisted value — a fault answers the fail-soft `true` (shown) and is REPORTED in every build; (2) the renderer\'s own `useCondition(toPredicateInput(schema.visible), recordData)` WITHOUT `throwOnError` (action-icon.tsx:96, action-group.tsx:233) — a fault answers `evaluateCondition`\'s fail-soft `true` as well (ExpressionEvaluator.ts:387-408), and action-icon.tsx:197-199 names that policy "fail-soft, unlike `action:button`\'s fail-closed one". Both legs show, so a faulting predicate leaves the block RENDERED, and the node gate is the leg that logs it: fail-SOFT-LOG', + covers: [ + 'ui/component.zod.ts:ActionIconPropsSchema.visible', + 'ui/component.zod.ts:ActionGroupPropsSchema.visible', + ], + note: 'The opposite fault face to `cel-action-block-visible-closed` on the same key, which is why the two do not share a row. The `-log` half rests on the node gate\'s report; the renderer leg itself is silent for a bare string and warns only on the `{dialect:"cel"}` route. A literal `false` on `action:group` is honoured by the node gate, not by the renderer, whose `:306` check is a truthiness test. Same bare-string limit as `cel-action-block-visible-closed`. ⛔ NOT MEASURED HERE: the renderers were read at the pin, not run.', + }, + { + id: 'cel-action-block-disabled', + summary: '`action:button` / `action:icon` page-block disabling (props `disabled`) — the button stays on screen and cannot be pressed while the predicate is TRUE', + dialect: 'cel', mode: 'interpret', state: 'enforced', failPolicy: 'fail-closed', + enforcement: + 'BUILD-TIME GATE, measured here: lint/page-envelope-audit.ts door 3 parses `ComponentPropsMap["action:button" | "action:icon"]`. EVALUATOR, two legs, both un-negated: (1) SchemaRenderer node gate `evaluateEnablementPredicate` on the hoisted `disabled` (SchemaRenderer.tsx:1772-1784), forwarded as the `disabled` prop — a fault answers `evaluateCondition`\'s fail-soft `true`, which on this leg means GREYED OUT, and is REPORTED in every build (SchemaRenderer.tsx:1138-1165); (2) the renderer\'s own `useCondition(toPredicateInput(schema.disabled), recordData)` (action-button.tsx:129 + :333-339, action-icon.tsx:101 + :235-241), the same `true` on a fault. The legs OR, so a faulting predicate leaves the button DISABLED: the action is refused, fail-CLOSED', + covers: [ + 'ui/component.zod.ts:ActionButtonPropsSchema.disabled', + 'ui/component.zod.ts:ActionIconPropsSchema.disabled', + ], + note: 'Deliberately NOT `fail-soft-log` like `cel-action-disabled`: the renderers measured here answer a faulting `disabled` with `true`, and on an un-negated enablement leg that `true` greys the control out — SchemaRenderer.tsx:1149-1156 states exactly that asymmetry ("on the negated visibility legs that means SHOWN, here it means GREYED OUT"). The objectui fix `cel-action-disabled` cites is about an EMPTY `disabled: \'\'`, which `hasDeclaredVisibilityGate` / `hasDeclaredPredicate` now treat as no gate, not about a faulting one. ⚠️ Scope consequence worth knowing: the node-gate leg evaluates at PAGE scope, so a row-scoped `record.*` predicate that does not resolve there faults and greys the button out whatever the row says. Same bare-string limit as `cel-action-block-visible-closed`. ⛔ NOT MEASURED HERE: the renderers were read at the pin, not run.', + }, { id: 'cel-flow', summary: 'flow / loader branching + filter predicates', From 28681fc5baabf169602c2de284b3ab99ebeeba1d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 12:05:11 +0000 Subject: [PATCH 10/13] chore(spec): regenerate the reference index and component page on the merged tree (keeps the docs title rule) Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW Co-authored-by: Claude --- content/docs/references/index.mdx | 3 ++- content/docs/references/ui/component.mdx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 950a42eba1b..6c2aea6ead3 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,5 +1,6 @@ --- -title: Protocol Reference +title: Protocol reference — every schema by module +navTitle: Protocol Reference description: Every schema published by @objectstack/spec — 1522 schemas across 14 protocol modules --- diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index c9f509f2123..ea64057809e 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -1,5 +1,6 @@ --- -title: Component +title: Component schema — UI Protocol reference +navTitle: Component description: "Component schemas of the ObjectStack UI Protocol: AIChatWindowProps and 43 more — each property with its type, default and a TypeScript example." --- From c628e1d0202c30b7321e3ee6a2488c535a39f1f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 14:21:10 +0000 Subject: [PATCH 11/13] chore(spec): regenerate authorable-surface on the merged tree Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW Co-authored-by: Claude --- packages/spec/authorable-surface/ui.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index a6e0dd2f679..2e99ccfa169 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -771,7 +771,7 @@ "ui/ListView:showRecordCount", "ui/ListView:sort", "ui/ListView:striped [RETIRED]", - "ui/ListView:tabs", + "ui/ListView:tabs [RETIRED]", "ui/ListView:timeline", "ui/ListView:tree", "ui/ListView:type", @@ -954,7 +954,7 @@ "ui/ObjectListView:showRecordCount", "ui/ObjectListView:sort", "ui/ObjectListView:striped [RETIRED]", - "ui/ObjectListView:tabs", + "ui/ObjectListView:tabs [RETIRED]", "ui/ObjectListView:timeline", "ui/ObjectListView:tree", "ui/ObjectListView:type", From 3ac9125cf8d335c405f39c789018a08aa09d9428 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 14:27:27 +0000 Subject: [PATCH 12/13] fix(spec): re-measure the six #20371 rows at objectui dd3f7e1be and declare objectName The objectui pin moved from f8a9d0fb0596 to dd3f7e1be356. Every read point the action:* and element:* rows cite was re-derived at the new pin and re-anchored; the asserting citations now name dd3f7e1be. The new pin forwards `objectName` to the action runner from action:button and action:icon, so both rows declare it (the ActionDef string scalar, per the #7751 value posture). action:group and action:menu forward it per member, so the container rows gain no key. `params` keeps its meaning on the page path (array = input list, object = static values via properties.params). The dogfood expression-conformance rows for these keys are re-anchored at the same pin; their fault policies are unchanged across the hop. Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW Co-authored-by: Claude --- ...371-component-props-action-element-rows.md | 4 +- .../test/expression-conformance.ledger.ts | 18 +- ...omponent-action-element-rows-20371.test.ts | 19 +- packages/spec/src/ui/component.zod.ts | 174 ++++++++++-------- 4 files changed, 129 insertions(+), 86 deletions(-) diff --git a/.changeset/20371-component-props-action-element-rows.md b/.changeset/20371-component-props-action-element-rows.md index 227bb809163..444f82e773a 100644 --- a/.changeset/20371-component-props-action-element-rows.md +++ b/.changeset/20371-component-props-action-element-rows.md @@ -15,7 +15,7 @@ What changes at the authoring doors (`os validate` / `os build` / `os lint`): Measured decisions worth knowing when you author these blocks: -- **`action:button` / `action:icon`** — `name` is optional (the renderer reads `name ?? label`). The executor is `actionType`; `type` inside `properties` is refused with a rename to `actionType` (on a page component `type` is the component itself). `visible` / `disabled` take a boolean, a CEL string or a `{ dialect, source }` envelope. The legacy `enabled` fallback and the host-only `autoTrigger` flag are refused with a prescription. `action:icon` reads no `size`. -- **`action:group` / `action:menu`** — `actions` is a LIST of action objects (a member's executor is its own `type`); a bare list of action names is refused. `action:group` reads no group-level `name`, so it is refused with a prescription. `variant` / `size` take the Button primitive's vocabulary; `primary` and `md` are accepted only on `action:button` (and `primary` on `action:icon`), where the renderer maps them. +- **`action:button` / `action:icon`** — `name` is optional (the renderer reads `name ?? label`). The executor is `actionType`; `type` inside `properties` is refused with a rename to `actionType` (on a page component `type` is the component itself). `visible` / `disabled` take a boolean, a CEL string or a `{ dialect, source }` envelope. The legacy `enabled` fallback and the host-only `autoTrigger` flag are refused with a prescription. `action:icon` reads no `size`. `objectName` names the object the action acts on (forwarded to the runner; omitted, the action acts on the page's object). +- **`action:group` / `action:menu`** — `actions` is a LIST of action objects (a member's executor is its own `type`); a bare list of action names is refused. A member's `objectName` rides the member object; the containers themselves read no `objectName`. `action:group` reads no group-level `name`, so it is refused with a prescription. `variant` / `size` take the Button primitive's vocabulary; `primary` and `md` are accepted only on `action:button` (and `primary` on `action:icon`), where the renderer maps them. - **`element:definition-list`** — `items` of strict `{ term, description? }`; `columns` is the NUMBER `1` or `2` (the string `'2'` renders one column and is refused). - **`element:repeater`** — `object` is required; `filter` / `sort` take the family's one orthography (`ViewFilterRule[]`, `SortItem[]`); `fields` takes a field name or `{ field }` (an unrendered `label` is refused). diff --git a/packages/qa/dogfood/test/expression-conformance.ledger.ts b/packages/qa/dogfood/test/expression-conformance.ledger.ts index b224920fd8b..f21b363262e 100644 --- a/packages/qa/dogfood/test/expression-conformance.ledger.ts +++ b/packages/qa/dogfood/test/expression-conformance.ledger.ts @@ -398,11 +398,13 @@ export const EXPRESSION_SURFACE: ExprSurface[] = [ // two evaluation legs; the row states what the COMPOSITION does to a predicate // that faults. Split three ways because the faces differ, per renderer — the // collision rule `cel-action-visible` / `cel-action-disabled` were split on. - // Read (not run) at the `.objectui-sha` pin `f8a9d0fb0596`: the - // renderers are `components/src/renderers/action/action-{button,menu,icon,group}.tsx`, + // Read (not run) at the `.objectui-sha` pin `dd3f7e1be356` (re-read there + // 2026-09-28; first read at `f8a9d0fb0596`, and across that hop the node-gate + // evaluators and `useCondition` are code-identical, so every fault face below + // stands and only line numbers moved): the renderers are `components/src/renderers/action/action-{button,menu,icon,group}.tsx`, // `useCondition` is `react/src/hooks/useExpression.ts:200-244`, the node gate's // two legs are `react/src/SchemaRenderer.tsx` `evaluateVisibilityPredicate` - // (:1048) and `evaluateEnablementPredicate` (:1138, :1772-1784). Tracker anchors + // (:1157) and `evaluateEnablementPredicate` (:1247, :1879-1891). Tracker anchors // kept out of the strings below (`check:doc-authoring`): the node gate's fault // report is objectui#6038 (visibility legs) and objectui#6445 (enablement legs); // the empty-`disabled` fix `cel-action-disabled` cites is objectui#3842. @@ -411,7 +413,7 @@ export const EXPRESSION_SURFACE: ExprSurface[] = [ summary: '`action:button` / `action:menu` page-block visibility (props `visible`) — the block is not rendered when the predicate is FALSE', dialect: 'cel', mode: 'interpret', state: 'enforced', failPolicy: 'fail-closed', enforcement: - 'BUILD-TIME GATE, measured here: lint/page-envelope-audit.ts door 3 parses `ComponentPropsMap["action:button" | "action:menu"]`, the one door that reaches `properties.visible`. EVALUATOR, two legs: (1) SchemaRenderer node gate `evaluateVisibilityPredicate` on the hoisted value, page scope — a fault there answers `evaluateCondition`\'s fail-soft `true` (shown) and is REPORTED in every build; (2) the renderer\'s own `useCondition(toPredicateInput(schema.visible), recordData, { throwOnError: true })` (action-button.tsx:116-119, action-menu.tsx:219-222) against the host-bound row — a fault returns `false` and warns once per predicate (useExpression.ts:215-238), and the block returns null (action-button.tsx:298, action-menu.tsx:293). The legs AND, so a faulting predicate HIDES the block: fail-CLOSED, the face `cel-action-visible` records for a registered action', + 'BUILD-TIME GATE, measured here: lint/page-envelope-audit.ts door 3 parses `ComponentPropsMap["action:button" | "action:menu"]`, the one door that reaches `properties.visible`. EVALUATOR, two legs: (1) SchemaRenderer node gate `evaluateVisibilityPredicate` on the hoisted value, page scope — a fault there answers `evaluateCondition`\'s fail-soft `true` (shown) and is REPORTED in every build; (2) the renderer\'s own `useCondition(toPredicateInput(schema.visible), recordData, { throwOnError: true })` (action-button.tsx:117-120, action-menu.tsx:224-227) against the host-bound row — a fault returns `false` and warns once per predicate (useExpression.ts:215-238), and the block returns null (action-button.tsx:335, action-menu.tsx:322). The legs AND, so a faulting predicate HIDES the block: fail-CLOSED, the face `cel-action-visible` records for a registered action', covers: [ 'ui/component.zod.ts:ActionButtonPropsSchema.visible', 'ui/component.zod.ts:ActionMenuPropsSchema.visible', @@ -423,24 +425,24 @@ export const EXPRESSION_SURFACE: ExprSurface[] = [ summary: '`action:icon` / `action:group` page-block visibility (props `visible`) — the block is not rendered when the predicate is FALSE', dialect: 'cel', mode: 'interpret', state: 'enforced', failPolicy: 'fail-soft-log', enforcement: - 'BUILD-TIME GATE, measured here: lint/page-envelope-audit.ts door 3 parses `ComponentPropsMap["action:icon" | "action:group"]`. EVALUATOR, two legs: (1) SchemaRenderer node gate `evaluateVisibilityPredicate` on the hoisted value — a fault answers the fail-soft `true` (shown) and is REPORTED in every build; (2) the renderer\'s own `useCondition(toPredicateInput(schema.visible), recordData)` WITHOUT `throwOnError` (action-icon.tsx:96, action-group.tsx:233) — a fault answers `evaluateCondition`\'s fail-soft `true` as well (ExpressionEvaluator.ts:387-408), and action-icon.tsx:197-199 names that policy "fail-soft, unlike `action:button`\'s fail-closed one". Both legs show, so a faulting predicate leaves the block RENDERED, and the node gate is the leg that logs it: fail-SOFT-LOG', + 'BUILD-TIME GATE, measured here: lint/page-envelope-audit.ts door 3 parses `ComponentPropsMap["action:icon" | "action:group"]`. EVALUATOR, two legs: (1) SchemaRenderer node gate `evaluateVisibilityPredicate` on the hoisted value — a fault answers the fail-soft `true` (shown) and is REPORTED in every build; (2) the renderer\'s own `useCondition(toPredicateInput(schema.visible), recordData)` WITHOUT `throwOnError` (action-icon.tsx:97, action-group.tsx:238) — a fault answers `evaluateCondition`\'s fail-soft `true` as well (ExpressionEvaluator.ts:387-408), and action-icon.tsx:219-221 names that policy "fail-soft, unlike `action:button`\'s fail-closed one". Both legs show, so a faulting predicate leaves the block RENDERED, and the node gate is the leg that logs it: fail-SOFT-LOG', covers: [ 'ui/component.zod.ts:ActionIconPropsSchema.visible', 'ui/component.zod.ts:ActionGroupPropsSchema.visible', ], - note: 'The opposite fault face to `cel-action-block-visible-closed` on the same key, which is why the two do not share a row. The `-log` half rests on the node gate\'s report; the renderer leg itself is silent for a bare string and warns only on the `{dialect:"cel"}` route. A literal `false` on `action:group` is honoured by the node gate, not by the renderer, whose `:306` check is a truthiness test. Same bare-string limit as `cel-action-block-visible-closed`. ⛔ NOT MEASURED HERE: the renderers were read at the pin, not run.', + note: 'The opposite fault face to `cel-action-block-visible-closed` on the same key, which is why the two do not share a row. The `-log` half rests on the node gate\'s report; the renderer leg itself is silent for a bare string and warns only on the `{dialect:"cel"}` route. A literal `false` on `action:group` is honoured by the node gate, not by the renderer, whose `:343` check is a truthiness test. Same bare-string limit as `cel-action-block-visible-closed`. ⛔ NOT MEASURED HERE: the renderers were read at the pin, not run.', }, { id: 'cel-action-block-disabled', summary: '`action:button` / `action:icon` page-block disabling (props `disabled`) — the button stays on screen and cannot be pressed while the predicate is TRUE', dialect: 'cel', mode: 'interpret', state: 'enforced', failPolicy: 'fail-closed', enforcement: - 'BUILD-TIME GATE, measured here: lint/page-envelope-audit.ts door 3 parses `ComponentPropsMap["action:button" | "action:icon"]`. EVALUATOR, two legs, both un-negated: (1) SchemaRenderer node gate `evaluateEnablementPredicate` on the hoisted `disabled` (SchemaRenderer.tsx:1772-1784), forwarded as the `disabled` prop — a fault answers `evaluateCondition`\'s fail-soft `true`, which on this leg means GREYED OUT, and is REPORTED in every build (SchemaRenderer.tsx:1138-1165); (2) the renderer\'s own `useCondition(toPredicateInput(schema.disabled), recordData)` (action-button.tsx:129 + :333-339, action-icon.tsx:101 + :235-241), the same `true` on a fault. The legs OR, so a faulting predicate leaves the button DISABLED: the action is refused, fail-CLOSED', + 'BUILD-TIME GATE, measured here: lint/page-envelope-audit.ts door 3 parses `ComponentPropsMap["action:button" | "action:icon"]`. EVALUATOR, two legs, both un-negated: (1) SchemaRenderer node gate `evaluateEnablementPredicate` on the hoisted `disabled` (SchemaRenderer.tsx:1879-1891), forwarded as the `disabled` prop — a fault answers `evaluateCondition`\'s fail-soft `true`, which on this leg means GREYED OUT, and is REPORTED in every build (SchemaRenderer.tsx:1247-1274); (2) the renderer\'s own `useCondition(toPredicateInput(schema.disabled), recordData)` (action-button.tsx:130 + :370-376, action-icon.tsx:102 + :257-263), the same `true` on a fault. The legs OR, so a faulting predicate leaves the button DISABLED: the action is refused, fail-CLOSED', covers: [ 'ui/component.zod.ts:ActionButtonPropsSchema.disabled', 'ui/component.zod.ts:ActionIconPropsSchema.disabled', ], - note: 'Deliberately NOT `fail-soft-log` like `cel-action-disabled`: the renderers measured here answer a faulting `disabled` with `true`, and on an un-negated enablement leg that `true` greys the control out — SchemaRenderer.tsx:1149-1156 states exactly that asymmetry ("on the negated visibility legs that means SHOWN, here it means GREYED OUT"). The objectui fix `cel-action-disabled` cites is about an EMPTY `disabled: \'\'`, which `hasDeclaredVisibilityGate` / `hasDeclaredPredicate` now treat as no gate, not about a faulting one. ⚠️ Scope consequence worth knowing: the node-gate leg evaluates at PAGE scope, so a row-scoped `record.*` predicate that does not resolve there faults and greys the button out whatever the row says. Same bare-string limit as `cel-action-block-visible-closed`. ⛔ NOT MEASURED HERE: the renderers were read at the pin, not run.', + note: 'Deliberately NOT `fail-soft-log` like `cel-action-disabled`: the renderers measured here answer a faulting `disabled` with `true`, and on an un-negated enablement leg that `true` greys the control out — SchemaRenderer.tsx:1258-1265 states exactly that asymmetry ("on the negated visibility legs that means SHOWN, here it means GREYED OUT"). The objectui fix `cel-action-disabled` cites is about an EMPTY `disabled: \'\'`, which `hasDeclaredVisibilityGate` / `hasDeclaredPredicate` now treat as no gate, not about a faulting one. ⚠️ Scope consequence worth knowing: the node-gate leg evaluates at PAGE scope, so a row-scoped `record.*` predicate that does not resolve there faults and greys the button out whatever the row says. Same bare-string limit as `cel-action-block-visible-closed`. ⛔ NOT MEASURED HERE: the renderers were read at the pin, not run.', }, { id: 'cel-flow', diff --git a/packages/spec/src/ui/component-action-element-rows-20371.test.ts b/packages/spec/src/ui/component-action-element-rows-20371.test.ts index 89225a08c12..be7eec60ad7 100644 --- a/packages/spec/src/ui/component-action-element-rows-20371.test.ts +++ b/packages/spec/src/ui/component-action-element-rows-20371.test.ts @@ -79,11 +79,12 @@ describe('the six rows exist and are the exported schemas (#20371)', () => { describe('key sets, asserted whole — measured from the renderers\' read points', () => { // Forwarded to the action runner by `action:button` / `action:icon` // (`execute({ ...forwarded })`). `undoable` / `recordIdField` are forwarded - // by the button only. + // by the button only. `objectName` joined the forward with the pin that + // carries it; the four renderers forward it, the two containers per member. const FORWARDED = [ 'params', 'description', 'target', 'openIn', 'endpoint', 'method', 'bodyExtra', 'bodyShape', 'operation', 'patch', 'confirmText', 'successMessage', 'errorMessage', 'refreshAfter', - 'locations', 'toast', 'resultDialog', 'onSuccess', + 'locations', 'toast', 'resultDialog', 'onSuccess', 'objectName', ]; it('action:button', () => { @@ -304,6 +305,20 @@ describe('what the measurement decided, pinned', () => { expect(limit.map((i) => [i.code, i.path.join('.')])).toEqual([['too_small', 'limit']]); }); + it('`objectName` is declared where the renderer forwards it — on the action, never on a container', () => { + for (const schema of [ActionButtonPropsSchema, ActionIconPropsSchema]) { + expect(schema.parse({ label: 'Close child', objectName: 'task' })).toEqual({ label: 'Close child', objectName: 'task' }); + } + // `action:group` / `action:menu` forward each MEMBER's `objectName`: it + // rides the member object, which this row does not judge ... + for (const schema of [ActionGroupPropsSchema, ActionMenuPropsSchema]) { + const member = { name: 'close', label: 'Close', type: 'script', objectName: 'task' }; + expect(schema.safeParse({ actions: [member] }).success).toBe(true); + // ... and a container-level one is read by nothing. + expect(unknownKeyIssue(schema.safeParse({ objectName: 'task' })).keys).toEqual(['objectName']); + } + }); + it('repeater: the `object-*` family\'s `objectName` is refused and renamed to `object`', () => { const issue = unknownKeyIssue(ElementRepeaterPropsSchema.safeParse({ object: 'task', objectName: 'task' })); expect(issue.keys).toEqual(['objectName']); diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index 60b4897c27b..d133710476d 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -2682,9 +2682,10 @@ export const ElementTextInputPropsSchema = lazySchema(() => strictObject({ * objectui's ADR-0080 curated public vocabulary carries six blocks this map * had no row for — `action:button`, `action:group`, `action:menu`, * `action:icon`, `element:definition-list`, `element:repeater` - * (`core/src/registry/public-blocks.ts:109-114` at the pin this repo builds - * against, `.objectui-sha` = `f8a9d0fb0`). The missing row failed in two - * different ways: + * (`core/src/registry/public-blocks.ts:117-122` at the pin this repo builds + * against, `.objectui-sha` = `dd3f7e1be`; first measured at `.objectui-sha` + * pin `f8a9d0fb0`, every read point below re-derived at the current pin + * 2026-09-28). The missing row failed in two different ways: * * - The four `action:*` types sit outside every namespace the * `PageComponentType` enum populates, so the #5068 props gate skipped them @@ -2730,9 +2731,14 @@ export const ElementTextInputPropsSchema = lazySchema(() => strictObject({ * - `data` / `context` — the host's row and execute-context channels (React * props the renderers take by name), not authorable surface. * - `onClick` — a function; only a code-composed schema can carry one. - * - `objectName` — not read at the pin. objectui's `origin/main` at the time - * of this measurement (`9f0c84a`) forwards it on all four renderers, so the - * row gains it with the pin bump that carries that read, not before. + * + * `objectName` IS declared on `action:button` / `action:icon`: absent at the + * first measurement, it is forwarded to the runner at the current pin + * (`action-button.tsx:307`, `action-icon.tsx:195`), and the console resolves + * its dispatch target as `action.objectName || `. On + * `action:group` / `action:menu` the forward is the MEMBER's + * (`action-group.tsx:323`, `action-menu.tsx:313`), so it rides each member + * object and the container rows gain no key. */ /** What an undeclared key on one of the four `action:*` blocks met before its row. */ @@ -2796,28 +2802,35 @@ const ACTION_NODE_GUIDANCE = { * runner (`components/src/renderers/action/action-button.tsx` at the pin). * Read points, per key: * - * - `name` — `:118` (the visibility-diagnostic label, `schema.name ?? - * schema.label`) and `:188` (forwarded). OPTIONAL, as it is read: an inline + * - `name` — `:119` (the visibility-diagnostic label, `schema.name ?? + * schema.label`) and `:212` (forwarded). OPTIONAL, as it is read: an inline * page button is not a registered object action, and the runner takes a * nameless action on its `type` leg. `UIActionSchema` declares it required; * the read does not. - * - `label` — `:346`, placed as a React child (so a literal string: an inline + * - `label` — `:383`, placed as a React child (so a literal string: an inline * locale map is not resolved on this path; the translation bundle's - * `components..label` is the localization channel); also `:118`, `:192`. - * - `icon` — `:133`, through the shared `resolveIcon`. - * - `actionType` — `:187`, forwarded as the runner's `type`. - * - `variant` / `size` — `:136` / `:137`: `primary` → the primitive's + * `components..label` is the localization channel); also `:119`, `:216`. + * - `icon` — `:134`, through the shared `resolveIcon`. + * - `actionType` — `:211`, forwarded as the runner's `type`. + * - `variant` / `size` — `:137` / `:138`: `primary` → the primitive's * `default` and `md` → `default`, everything else handed to `Button` as-is. - * - `visible` / `disabled` — `:116` + `:298` / `:129` + `:334`, evaluated + * - `visible` / `disabled` — `:117` + `:335` / `:130` + `:371`, evaluated * against the row the host binds (`usePredicateRecordContext`). - * - Forwarded to the runner (`:153-155`, `:187-270`): `params`, `description`, - * `target`, `openIn`, `endpoint`, `method`, `bodyExtra`, `bodyShape`, - * `operation`, `patch`, `confirmText`, `successMessage`, `errorMessage`, - * `refreshAfter`, `undoable`, `recordIdField`, `locations`, `toast`, - * `resultDialog`, `onSuccess`. + * - `params` — `:176-179`. An array is the input list, forwarded as + * `actionParams`; the static values are read off `properties.params` + * itself (`readStaticParamValues`, `static-params.ts:91-101`). On a page + * node the two are one key: this row IS `properties`, and `SchemaRenderer`'s + * hoist makes `schema.params` the same object, so an array here is the input + * list and an object is the static values. (A node-level object `params` + * outside `properties` is ignored with a development warning.) + * - Forwarded to the runner (`:211-307`): `description`, `target`, `openIn`, + * `endpoint`, `method`, `bodyExtra`, `bodyShape`, `operation`, `patch`, + * `confirmText`, `successMessage`, `errorMessage`, `refreshAfter`, + * `undoable`, `recordIdField`, `locations`, `toast`, `resultDialog`, + * `onSuccess`, `objectName`. * - * The registration's `inputs` (`:358-379`) publish seven of these twenty-eight - * keys; the other twenty-one are read and unpublished. It also publishes + * The registration's `inputs` (`:395-416`) publish seven of these twenty-nine + * keys; the other twenty-two are read and unpublished. It also publishes * `className`, which is read but is a node key. */ export const ActionButtonPropsSchema = lazySchema(() => strictObject({ @@ -2868,6 +2881,8 @@ export const ActionButtonPropsSchema = lazySchema(() => strictObject({ toast: z.unknown().optional().describe('Toast behaviour, forwarded to the runner'), resultDialog: z.unknown().optional().describe('One-shot result dialog for a value the response shows exactly once, forwarded to the runner'), onSuccess: z.unknown().optional().describe('Declared post-success navigation, forwarded to the runner'), + objectName: z.string().optional() + .describe('Object the action acts on, forwarded to the runner — the console dispatches to it instead of the page\'s object. Omit to act on the page\'s object'), })); /** Author state (ADR-0122: the bare name is the author state). */ export type ActionButtonProps = z.input; @@ -2883,20 +2898,21 @@ export type ActionButtonPropsParsed = z.infer; * (`components/src/renderers/action/action-icon.tsx` at the pin). The same * runner path as `action:button`, measured separately because the two differ: * - * - `size` is NOT read — `:106` pins the primitive's `icon` size — so it is + * - `size` is NOT read — `:107` pins the primitive's `icon` size — so it is * not declared (the registration does not publish it either). - * - `undoable` and `recordIdField` are NOT forwarded (`:119-174` lists the + * - `undoable` and `recordIdField` are NOT forwarded (`:134-196` lists the * rest of `action:button`'s forward and not these two), so not declared. - * - `label` is read four times: `:137` (forwarded), `:243` (`aria-label`, - * falling back to `name`), `:252` (its first letter when no icon resolves) - * and `:258` / `:264` (the tooltip). `description` is the tooltip's - * fallback (`:264`) as well as forwarded (`:138`). - * - `visible` / `disabled` — `:96` + `:207` / `:101` + `:236`. `variant` — - * `:105`, `primary` mapped to `default`, renderer default `ghost`. - * - Forwarded (`:132-173`): `actionType`, `name`, `target`, `openIn`, - * `endpoint`, `method`, `params`, `bodyExtra`, `bodyShape`, `operation`, - * `patch`, `confirmText`, `successMessage`, `errorMessage`, `refreshAfter`, - * `locations`, `toast`, `resultDialog`, `onSuccess`. + * - `label` is read four times: `:152` (forwarded), `:265` (`aria-label`, + * falling back to `name`), `:274` (its first letter when no icon resolves) + * and `:280` / `:286` (the tooltip). `description` is the tooltip's + * fallback (`:286`) as well as forwarded (`:153`). + * - `visible` / `disabled` — `:97` + `:229` / `:102` + `:258`. `variant` — + * `:106`, `primary` mapped to `default`, renderer default `ghost`. + * - `params` — `:130-133`, routed exactly as on `action:button`. + * - Forwarded (`:147-195`): `actionType`, `name`, `target`, `openIn`, + * `endpoint`, `method`, `bodyExtra`, `bodyShape`, `operation`, `patch`, + * `confirmText`, `successMessage`, `errorMessage`, `refreshAfter`, + * `locations`, `toast`, `resultDialog`, `onSuccess`, `objectName`. */ export const ActionIconPropsSchema = lazySchema(() => strictObject({ surface: 'this `action:icon`', @@ -2946,6 +2962,8 @@ export const ActionIconPropsSchema = lazySchema(() => strictObject({ toast: z.unknown().optional().describe('Toast behaviour, forwarded to the runner'), resultDialog: z.unknown().optional().describe('One-shot result dialog for a value the response shows exactly once, forwarded to the runner'), onSuccess: z.unknown().optional().describe('Declared post-success navigation, forwarded to the runner'), + objectName: z.string().optional() + .describe('Object the action acts on, forwarded to the runner — the console dispatches to it instead of the page\'s object. Omit to act on the page\'s object'), })); /** Author state (ADR-0122: the bare name is the author state). */ export type ActionIconProps = z.input; @@ -2968,10 +2986,13 @@ export type ActionIconPropsParsed = z.infer; * `enabled`, `icon`, `variant`, `className`, `label` (falling back to `name`), * `tags` (a `separator-before` tag draws a divider), `name` (the React key) and * the runner forward — which hands the runner the member's own `type`, not - * `actionType`: a member is an action entry, and an action entry's executor is - * `type`. A bare string is refused here: an action NAME list is - * `record:quick_actions`' `actionNames`, and a string member would render as an - * unlabeled button that runs nothing. + * `actionType` (a member is an action entry, and an action entry's executor is + * `type`), its `objectName`, and its static values off the member's OWN + * `properties.params`, evaluated by the container + * (`readMemberStaticParamValues`, `static-params.ts:142-148`). A bare string + * is refused here: an action NAME list is `record:quick_actions`' + * `actionNames`, and a string member would render as an unlabeled button that + * runs nothing. */ const actionMemberList = () => z.array(z.record(z.string(), z.unknown())); @@ -2980,25 +3001,26 @@ const actionMemberList = () => z.array(z.record(z.string(), z.unknown())); * (`components/src/renderers/action/action-group.tsx` at the pin). Read * points, per key: * - * - `actions` — `:243`, then filtered by `location` through `actionRendersAt` - * (`:244`); members are read at `:79-132` (inline), `:164-202` (dropdown) - * and forwarded at `:248-287`. - * - `display` — `:309`, `inline` unless it is `dropdown`. - * - `label` / `icon` — `:328` / `:313`: the DROPDOWN trigger's text (default + * - `actions` — `:248`, then filtered by `location` through `actionRendersAt` + * (`:249`); members are read at `:81-134` (inline), `:166-204` (dropdown) + * and forwarded at `:274-324` (static values `:274-280`, `objectName` + * `:323`). + * - `display` — `:346`, `inline` unless it is `dropdown`. + * - `label` / `icon` — `:365` / `:350`: the DROPDOWN trigger's text (default * `Actions`) and icon. Inline mode renders neither. - * - `variant` — `:319` (dropdown trigger) and `:360` (each inline member's + * - `variant` — `:356` (dropdown trigger) and `:397` (each inline member's * fallback); no `primary` mapping at group level, so the primitive's six. - * - `size` — `:320` maps `md` → `default` for the dropdown trigger, but `:361` - * hands the group size to each inline member raw, and `:89` maps only a + * - `size` — `:357` maps `md` → `default` for the dropdown trigger, but `:398` + * hands the group size to each inline member raw, and `:91` maps only a * member's OWN `md`. So `md` renders only in dropdown mode, and in the * default inline mode reaches the Button primitive, which has no `md`. * Declared: the primitive's four sizes. (The registration publishes * `sm` / `md` / `lg`; `md` is objectui's to map on both paths or drop.) - * - `visible` — `:233` + `:306`. `:306` tests the raw value's truthiness, so a + * - `visible` — `:238` + `:343`. `:343` tests the raw value's truthiness, so a * literal `false` is honoured by `SchemaRenderer`'s node gate, which also * evaluates the hoisted value, rather than by this check. * - * NOT read: the group's own `name`. The registration publishes it (`:378`), + * NOT read: the group's own `name`. The registration publishes it (`:415`), * the renderer never reads `schema.name`, and inline mode only spreads it * onto the wrapping `
` as a DOM attribute. Refused with a prescription, * the `page:accordion` item `value` precedent: an author copying the @@ -3045,17 +3067,18 @@ export type ActionGroupPropsParsed = z.infer; * (`components/src/renderers/action/action-menu.tsx` at the pin). Read * points, per key: * - * - `actions` — `:299`; members are read at `:78`, `:106-145` and `:360`, run - * through `ActionAutoTrigger` (`:320-327`), and forwarded at `:237-285`. - * - `label` — `:341` (`aria-label`, default: the translated "More actions") - * and `:350-351` (trigger text; icon-only when omitted). - * - `icon` — `:224`, default the `MoreHorizontal` glyph. - * - `variant` / `size` — `:225` / `:226`, handed to the Button primitive + * - `actions` — `:328`; members are read at `:80`, `:108-147` and `:389`, run + * through `ActionAutoTrigger` (`:349-356`), and forwarded at `:253-314` + * (static values `:253-259`, `objectName` `:313`). + * - `label` — `:370` (`aria-label`, default: the translated "More actions") + * and `:379-380` (trigger text; icon-only when omitted). + * - `icon` — `:229`, default the `MoreHorizontal` glyph. + * - `variant` / `size` — `:230` / `:231`, handed to the Button primitive * unmapped (defaults `ghost` / `icon`): no `primary`, no `md` here. - * - `visible` — `:219` + `:293`, fail-closed; the same truthiness note as + * - `visible` — `:224` + `:322`, fail-closed; the same truthiness note as * `action:group`'s applies to a literal `false`. * - * The registration's `inputs` (`:381-390`) publish `label`, `icon`, + * The registration's `inputs` (`:410-419`) publish `label`, `icon`, * `actions`, `variant` and `className`; `size` and `visible` are read and * unpublished. */ @@ -3097,7 +3120,7 @@ export type ActionMenuPropsParsed = z.infer; * - `columns` is compared as the NUMBER `2` (`props.columns === 2`); any other * value renders one column. Declared as the literal pair `1 | 2`, which is * what the Studio designer writes (a `number` control, - * `previews/block-config.ts:306`). The registration's enum publishes the + * `previews/block-config.ts:307`). The registration's enum publishes the * STRINGS `'1'` / `'2'` (`:82`), and the string `'2'` renders one column — * the read wins. * - `items` is optional, as it is read: absent and empty both render the @@ -3151,27 +3174,30 @@ export type ElementDefinitionListProps = z.input Date: Mon, 28 Sep 2026 14:33:53 +0000 Subject: [PATCH 13/13] chore(spec): regenerate the authorable surface and reference docs for objectName Output of `pnpm --filter @objectstack/spec check:generated --fix` after action:button and action:icon gained `objectName`. Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW Co-authored-by: Claude --- content/docs/references/ui/component.mdx | 2 ++ packages/spec/authorable-surface/ui.json | 2 ++ 2 files changed, 4 insertions(+) diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index ea64057809e..930d64f9d7a 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -78,6 +78,7 @@ const result = AIChatWindowProps.parse(data); | **toast** | `any` | optional | Toast behaviour, forwarded to the runner | | **resultDialog** | `any` | optional | One-shot result dialog for a value the response shows exactly once, forwarded to the runner | | **onSuccess** | `any` | optional | Declared post-success navigation, forwarded to the runner | +| **objectName** | `string` | optional | Object the action acts on, forwarded to the runner — the console dispatches to it instead of the page's object. Omit to act on the page's object | --- @@ -131,6 +132,7 @@ const result = AIChatWindowProps.parse(data); | **toast** | `any` | optional | Toast behaviour, forwarded to the runner | | **resultDialog** | `any` | optional | One-shot result dialog for a value the response shows exactly once, forwarded to the runner | | **onSuccess** | `any` | optional | Declared post-success navigation, forwarded to the runner | +| **objectName** | `string` | optional | Object the action acts on, forwarded to the runner — the console dispatches to it instead of the page's object. Omit to act on the page's object | --- diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index 2e99ccfa169..f034e256898 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -73,6 +73,7 @@ "ui/ActionButtonProps:locations", "ui/ActionButtonProps:method", "ui/ActionButtonProps:name", + "ui/ActionButtonProps:objectName", "ui/ActionButtonProps:onSuccess", "ui/ActionButtonProps:openIn", "ui/ActionButtonProps:operation", @@ -109,6 +110,7 @@ "ui/ActionIconProps:locations", "ui/ActionIconProps:method", "ui/ActionIconProps:name", + "ui/ActionIconProps:objectName", "ui/ActionIconProps:onSuccess", "ui/ActionIconProps:openIn", "ui/ActionIconProps:operation",