Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f4df139
feat(types,react,sdui-parser)!: retire the node-level visibleOn / hid…
claude Oct 26, 2026
3e3159f
chore(sdui): re-record the registry source digests after the node-fac…
claude Oct 26, 2026
22a9050
test(react): describe the ablation site of the retired-dialect pin by…
claude Oct 26, 2026
6f044d4
test(types): the static table pin lists the three node-face tombstone…
claude Oct 26, 2026
578dc82
Merge branch 'main' into claude/project-thread-flw1js
claude Oct 26, 2026
9951000
chore(sdui): re-record the registry source digests on the merged tree
claude Oct 26, 2026
64e57a2
fix(types): the object-view table slot's refusal of the retired spell…
claude Oct 26, 2026
82adea8
chore(sdui): re-record the types registry source digest after the tab…
claude Oct 26, 2026
33a4c4d
Merge branch 'main' into claude/project-thread-flw1js
claude Oct 26, 2026
1ef5c48
chore(sdui): re-record the registry source digests on the merged tree
claude Oct 26, 2026
a3f79a5
fix(ui): teach the gates the retired node-visibility spellings
claude Oct 26, 2026
586ed40
docs(skills): a retired spelling under props is not a node-gate predi…
claude Oct 26, 2026
cfcf6b1
Merge branch 'main' into claude/project-thread-flw1js
claude Oct 26, 2026
ee8049a
chore(sdui): re-record the registry source digests on the merged tree
claude Oct 26, 2026
c74ae05
Merge branch 'main' into claude/project-thread-flw1js
claude Oct 26, 2026
195b303
chore(sdui): re-record the registry source digests on the merged tree
claude Oct 26, 2026
2f3f538
Merge branch 'main' into claude/project-thread-flw1js
claude Oct 26, 2026
4be7b84
chore(sdui): re-record the registry source digests on the merged tree
claude Oct 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .changeset/25926-retire-node-visibility-dialects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
'@object-ui/types': minor
'@object-ui/react': minor
'@object-ui/sdui-parser': minor
'@object-ui/app-shell': patch
---

feat(types,react,sdui-parser,app-shell)!: the node-level `visibleOn` / `hiddenOn` / `disabledOn` spellings are retired; `visibleWhen` / `hidden` / `disabled` are the one predicate each

Clause-②: no (narrowing)
Metadata-protocol: narrowed

<!-- adr-0087: not-required (no-migration-prescription) The three keys are `@object-ui/types` schema members on objectui's own node face, not an `@objectstack/spec` key, object definition, stored row or stored shape: the spec declares `visibleWhen` as the one conditional-visibility predicate at every layer (ADR-0089) and never declared a `*On` suffix on a page component, so the ADR-0087 registries in `packages/spec` have no entry to write and `objectstack migrate meta` has nothing to rewrite. Each old spelling is refused by name on both faces (the TypeScript compiler, the Zod mirror behind `objectui validate`), and the refusal names the surviving key; that is the same disposition the objectui retirements took when the package merged into this repository (maintainer ruling 28 = B on objectstack#25831). The guidance below is for the author of a page, not for a metadata upgrader. The other categories are closed on facts: the four packages publish (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and what narrows is a declared schema key on a published metadata face, not a runtime-only interface, a type annotation or a runtime export (not runtime-interface-only, type-surface-only or runtime-export-retired). -->

`@objectstack/spec` declares exactly one conditional-visibility predicate at every layer, `visibleWhen`, and objectui carried three spellings of its own beside it: `visibleOn` beside `visibleWhen`, `hiddenOn` beside `hidden`, `disabledOn` beside `disabled`. Each pair was evaluated by the same code with the same polarity, so the second spelling added a dialect and nothing else. The three are retired, with no alias window.

- **`@object-ui/types`**: `BaseSchema.visibleOn`, `hiddenOn` and `disabledOn` are `never` on the TypeScript face and alias refusals on the Zod mirror. A document that writes one is refused at that key, and the message names the key that survives (`visibleWhen`, `hidden`, `disabled`), says the polarity is the same, and says the value is unchanged. `objectui validate` reads the same mirror, so it prints the same refusal. The `object-view` table slot's refusal of the same three keys now names the surviving node key too (write `visibleWhen` / `hidden` / `disabled` on the object-view node itself) instead of sending the author to a retired spelling. Boolean `visible` stays: the spec declares it on actions, navigation entries, tabs, alerts and settings, and the renderer keeps evaluating it.
- **`@object-ui/react`**: `SchemaRenderer`'s visibility chain has three legs, `visibleWhen` and `visible` (shown when truthy) and `hidden` (hidden when truthy), and the enablement gate has one, `disabled`. A stale retired key that reaches the renderer unvalidated is not consulted and is held out of the component's props, so it neither gates the node nor lands in the DOM.
- **`@object-ui/sdui-parser`**: the three keys are no longer base props. `validateTree` reports each as an `unknown-prop` diagnostic that names the surviving key, so a TSX tree is told the same thing a JSON document is. `checkRetiredVisibilityDialect` and `RETIRED_VISIBILITY_DIALECTS` are exported for that reading.
- **`@object-ui/app-shell`**: the command palette reads a navigation entry's `visible` only; the `visibleOn` fallback it carried read a key `NavigationItem` never declared.

**BREAKING** (an accept-set narrowing) for any page, view or component document that writes `visibleOn`, `hiddenOn` or `disabledOn` on a node. No shipped example, fixture or skill example in this repository wrote one; the ones in the documentation and the `objectui` skill are rewritten in this change.

- **Before.** The three keys parsed, were evaluated beside their canonical twin, and the docs taught both spellings.
- **Now.** Each is refused by name on `objectui validate`, on the Zod mirror, by the TypeScript compiler and by the SDUI parser, and the renderer does not read it.
- **Fix.** Rename the key and keep the value: `visibleOn` becomes `visibleWhen`, `hiddenOn` becomes `hidden`, `disabledOn` becomes `disabled`. A form field's `fields[].visibleOn` is a different, view-level slot and is unchanged.
2 changes: 1 addition & 1 deletion scripts/sdui-manifest.sources/components.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"package":"ui/packages/components","sources":226,"sha256":"75ed8c66878430e296c285bb0885304449f091b00da2209889cb99f634ef1690","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
{"package":"ui/packages/components","sources":226,"sha256":"2321bcd998e35f7367cbab8745cb1837768ff5dec8a5684709b8e06e13be6c69","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
2 changes: 1 addition & 1 deletion scripts/sdui-manifest.sources/core.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"package":"ui/packages/core","sources":112,"sha256":"e586f7573193068eefdbf8c22651072941bbd3843d60612cc2bc5d7e16003fca","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
{"package":"ui/packages/core","sources":112,"sha256":"810fe0e8f6db868fd443540cb2e30a85f691a48a08d58ba7ba8db0c577d579e7","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
2 changes: 1 addition & 1 deletion scripts/sdui-manifest.sources/react.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"package":"ui/packages/react","sources":70,"sha256":"0a4e839e9459da5ce7a6eb9b112d527bf8afecd3f7e8c6f63b936e5eb9593c68","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
{"package":"ui/packages/react","sources":70,"sha256":"d7a6fb1f906784fd1c2d04592bf54d095db68a222bc5dce8b4747f944288a944","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
2 changes: 1 addition & 1 deletion scripts/sdui-manifest.sources/sdui-parser.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"package":"ui/packages/sdui-parser","sources":9,"sha256":"08704fc188cb4749560f2a3faedd77b60820c1990f03311bcad21f283b2d7be4","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
{"package":"ui/packages/sdui-parser","sources":10,"sha256":"57c2952e2ffa03ee0144f1b26fb0af73272c684004f8e8dd689b2b061243509e","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
2 changes: 1 addition & 1 deletion scripts/sdui-manifest.sources/types.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"package":"ui/packages/types","sources":81,"sha256":"0373b9f45d8be39821641918e2e8058b8016ad24232b39111193116732fdf00f","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
{"package":"ui/packages/types","sources":81,"sha256":"f8450c19832a17c2f45e0164ec9e6e68cb247efdc00c81998c77e8c42c11da65","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
20 changes: 8 additions & 12 deletions skills/objectui/guides/schema-expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,8 @@ name, so calls are case-insensitive.

### Syntax options

Each condition field has two forms — a shorthand and an `On` suffix:
The node-level condition keys are `visibleWhen` (show), `hidden` and
`disabled`. Each takes a boolean, a `${}` template, or a raw expression:

<!-- os:check -->
```jsonc
Expand All @@ -250,10 +251,11 @@ Each condition field has two forms — a shorthand and an `On` suffix:

<!-- os:check -->
```jsonc
{ "hiddenOn": "data.status !== 'active'" } // raw expression (no ${} needed)
{ "hidden": "data.status !== 'active'" } // raw expression (no ${} needed)
```

The `On` variants accept raw expressions without `${}` wrapping — the entire string is the expression.
`visibleOn` / `hiddenOn` / `disabledOn` are retired and refused by name —
rename to `visibleWhen` / `hidden` / `disabled`; the value is unchanged.

### Visibility patterns

Expand All @@ -269,12 +271,6 @@ The `On` variants accept raw expressions without `${}` wrapping — the entire s
{ "visible": "${data.record.status === 'active'}" }
```

**Data-dependent:**
<!-- os:check -->
```json
{ "hidden": "${!data.items || data.items.length === 0}" }
```

**Combined conditions:**
<!-- os:check -->
```json
Expand All @@ -290,13 +286,13 @@ The `On` variants accept raw expressions without `${}` wrapping — the entire s

<!-- os:check -->
```json
{ "disabledOn": "!data.canPerformAction || data.isLocked" }
{ "disabled": "!data.canPerformAction || data.isLocked" }
```

## Field-level conditional rules (`visibleWhen` / `readonlyWhen` / `requiredWhen`)

These live on the **object's field metadata**, are written in CEL, and are
evaluated by `@objectstack/formula` -- a different tier from the `${}` / `On`
evaluated by `@objectstack/formula` -- a different tier from the node-level
conditions above, which are schema/widget-level and run on the recursive-descent
`SafeExpressionParser`. **How to author them is the `objectstack-data` skill's
job**, under "Conditional Field Rules": the predicate scope, the client-only vs
Expand Down Expand Up @@ -641,6 +637,6 @@ When an expression isn't working:
1. **Which key is it on, and does that type declare the key?** `content` and the predicate keys are evaluated and read on every type. `title` / `label` / `value` / `description` are evaluated **only on the types that declare them** — `statistic` (`label` / `value` / `description`), `card` (`title` / `description`), `button` (`label`) — and read raw everywhere else, including on a namespaced spelling such as `ui:statistic`. A `${...}` inside a `props` envelope is evaluated and then discarded. (A `properties` envelope is the one that is evaluated *and* hoisted onto the node — see [`rules/protocol.md`](../rules/protocol.md) for why that is recorded, not recommended.)
2. Is the `${}` syntax correct? Check for unmatched braces.
3. Is the data actually available in scope? Check what the host published on `PredicateScopeProvider` — ⛔ not `SchemaRendererProvider`'s `dataSource`, which publishes no expression root.
4. For conditions: are you using `On` suffix correctly? (`hiddenOn` takes raw expression, `hidden` needs `${}` if it's a string).
4. For conditions: `visibleWhen` / `hidden` / `disabled` take a raw expression or a `${}` template; the `On` spellings are refused.
5. Does the expression use a blocked pattern? Check for constructors, `eval`, `window`, etc.
6. Is type coercion causing issues? `${0 && "yes"}` returns `0`, not `false`.
15 changes: 6 additions & 9 deletions skills/objectui/rules/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,15 @@ value must clear before it reaches the screen.
| Field | Evaluation Type | Return Type | Example |
|---|---|---|---|
| `content` | Template (`${}`) | string | `"content": "Total: ${data.total}"` |
| `hidden` | Condition | boolean | `"hidden": "${data.status !== 'active'}"` |
| `hiddenOn` | Condition | boolean | `"hiddenOn": "data.status === 'draft'"` |
| `visibleWhen` | Condition | boolean | `"visibleWhen": "data.permissions.canView"` (raw expression, no `${}`) |
| `hidden` | Condition | boolean | `"hidden": "data.status === 'draft'"` |
| `visible` | Condition | boolean | `"visible": "${data.isActive}"` |
| `visibleOn` | Condition | boolean | `"visibleOn": "data.permissions.canView"` |
| `disabled` | Condition | boolean | `"disabled": "${form.isSubmitting}"` |
| `disabledOn` | Condition | boolean | `"disabledOn": "!data.hasPermission"` |
| `disabled` | Condition | boolean | `"disabled": "!data.hasPermission"` |
| `title` / `label` / `value` / `description` | Template (`${}`) | Preserves original type | **Only on the component types the spec declares** — see "Rule: Bindable Text Keys" below. `"value": "${data.total}"` on a `statistic` renders the number. |
| `properties.*` | Template (`${}`) | Preserves original type | Evaluated, then **hoisted onto the node** (`type` / `id` excepted), so the result lands where every renderer reads. See "Rule: Keys Live on the Node" below. |
| `props.*` | Template (`${}`) | Preserves original type | Evaluated, then spread as **React props** — a `ui:*` / `page:*` renderer reads `schema.*` and never sees the result. Consumed only by `element:*` components. |

**Precedence rule:** `visible` takes priority over `hidden`.
**Precedence rule:** `visible` takes priority over `hidden`. `visibleOn` / `hiddenOn` / `disabledOn` are retired: `objectui validate` refuses them by name, pointing at `visibleWhen` / `hidden` / `disabled`.

### Fields That are NOT Evaluated

Expand Down Expand Up @@ -111,9 +109,8 @@ as React props (`packages/react/src/SchemaRenderer.tsx`), which those renderers
ignore. A key parked under `props` is therefore dropped: the component renders
an empty frame, and the envelope itself lands in the DOM as the invalid
attribute `props="[object Object]"`. Silent for every key except the node-gate
predicates (`visibleWhen` / `visible` / `visibleOn` / `visibility` / `hidden` /
`hiddenOn` / `disabled` / `disabledOn`), which are refused by name on the
console, with the migration, instead of gating nothing.
predicates (`visibleWhen` / `visible` / `hidden` / `disabled`), which are
refused by name on the console, with the migration, instead of gating nothing.

**❌ WRONG — renders an empty card:**
<!-- doc-json: fragment — deliberate counter-example: a title parked under props never reaches the card renderer, and props is no declared key -->
Expand Down
17 changes: 8 additions & 9 deletions ui/content/docs/api/schema-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,8 @@ All schema types extend `BaseSchema`. These shared properties are available on e
"description": "A container element",
"className": "p-4 bg-white rounded-lg",
"visible": true,
"visibleOn": "${data.showWrapper}",
"disabled": false,
"disabledOn": "${data.isLocked}",
"visibleWhen": "${data.showWrapper}",
"disabled": "${data.isLocked}",
"testId": "wrapper-element",
"ariaLabel": "Content wrapper",
"children": []
Expand All @@ -96,12 +95,12 @@ One row per declared member, in declaration order, so the list can be checked ag
| `body` | *retired* | ⛔ Refused by name (objectui#6771). `body` was a second child-list spelling `BaseSchema` declared beside `children`; it is now `never` on the TypeScript face and an alias refusal on the Zod mirror, and the refusal names `children`. |
| `children` | `SchemaNode \| SchemaNode[]` | Child components rendered inside this component — the child-list key, and since objectui#6771 the only one. Whether a given node type renders a child list at all is still per component; see the note below. The keys OTHER than this one through which a renderer hands nodes to `SchemaRenderer` (a dialog's `trigger`, a tab item's `content`, a page's `regions[].components`) are per type and declared once as `NODE_SLOT_DECLARATIONS` / `nodeSlotsFor` (objectui#11170); `objectui check`, the core schema validator and the SDUI parser walk those positions as they walk `children`. |
| `visible` | `boolean \| string \| { dialect?: string; source: string }` | Visibility control. Accepts a boolean, a predicate expression string, **or** the CEL envelope object (`{ dialect: 'cel', source }` — what `objectstack build` emits for every authored predicate) — the renderer evaluates this key rather than reading it as a boolean. The string-or-envelope half is `ExpressionWire`, the one wire type `visibleWhen` on form fields already carries. |
| `visibleWhen` | `string \| { dialect, source }` | Canonical conditional-visibility predicate (ADR-0089); the element is shown when it evaluates truthy. Typed as `@objectstack/spec`'s `EvaluatedExpressionInput`: a predicate string, or the envelope the spec's parse writes (`dialect` is `cel`, `cron` or `template`, and `source` is not blank). Evaluated **before** `visible` and `visibleOn`, and outranks both. |
| `visibleOn` | `string` | Expression for conditional visibility. **Deprecated** (ADR-0089) — use `visibleWhen`. |
| `hidden` | `boolean \| string \| { dialect?: string; source: string }` | Inverse of `visible` — the node is not rendered. Accepts a boolean, a predicate expression string **or** the CEL envelope object (`ExpressionWire`), which the renderer evaluates rather than reading as a boolean; `hiddenOn` remains the sibling spelling. |
| `hiddenOn` | `string` | Expression for conditional hiding. |
| `visibleWhen` | `string \| { dialect, source }` | Canonical conditional-visibility predicate (ADR-0089); the element is shown when it evaluates truthy. Typed as `@objectstack/spec`'s `EvaluatedExpressionInput`: a predicate string, or the envelope the spec's parse writes (`dialect` is `cel`, `cron` or `template`, and `source` is not blank). Evaluated **before** `visible`, and outranks it. |
| `visibleOn` | *retired* | ⛔ Refused by name. `visibleOn` was objectui's own spelling of `visibleWhen`; it is `never` on the TypeScript face and an alias refusal on the Zod mirror, and the refusal names `visibleWhen` (same predicate, same polarity). |
| `hidden` | `boolean \| string \| { dialect?: string; source: string }` | Inverse of `visible` — the node is not rendered. Accepts a boolean, a predicate expression string **or** the CEL envelope object (`ExpressionWire`), which the renderer evaluates rather than reading as a boolean. |
| `hiddenOn` | *retired* | ⛔ Refused by name; the refusal names `hidden` (same predicate, same polarity). |
| `disabled` | `boolean \| string \| { dialect?: string; source: string }` | Disabled state. Accepts a boolean, a predicate expression string **or** the CEL envelope object (`ExpressionWire`), on the same evaluated path as `visible`. |
| `disabledOn` | `string` | Expression for conditional disabling. |
| `disabledOn` | *retired* | ⛔ Refused by name; the refusal names `disabled` (same predicate, same polarity). |
| `testId` | `string` | Test identifier, rendered as `data-testid`. |
| `ariaLabel` | `string \| KeyedI18nLabel` | Accessibility label, rendered as `aria-label`. `KeyedI18nLabel` is the **keyed** form (`{ key, defaultValue?, params? }`), resolved by `resolveKeyedI18nLabel` — **not** the `I18nLabel` that `label` and `description` carry. The two are structurally confusable and each returns nothing useful for the other's input. |

Expand Down Expand Up @@ -1439,6 +1438,6 @@ import type { BaseSchema, SchemaNode } from '@object-ui/types';

- **[Schema Overview](/docs/guide/schema-overview)** — High-level guide to ObjectUI schemas
- **[Quick Start](/docs/guide/quick-start)** — Build your first ObjectUI application
- **[Expressions](/docs/guide/expressions)** — Dynamic expressions with `visibleOn`, `disabledOn`
- **[Expressions](/docs/guide/expressions)** — Dynamic expressions with `visibleWhen`, `hidden`, `disabled`
- **[Fields Guide](/docs/guide/fields)** — Deep dive into form fields
- **[Plugin Development](/docs/guide/plugin-development)** — Build custom schema renderers
Loading
Loading