Skip to content

Commit e843b5b

Browse files
feat(types,react,sdui-parser)!: retire the node-level visibleOn / hiddenOn / disabledOn spellings (#26274)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbASAH4WuLBFpTWVCeQ9PHQu)_ Fixes #25926 Clause-②: no (narrowing) Metadata-protocol: narrowed ## What changed `@objectstack/spec` declares one conditional-visibility predicate at every layer, `visibleWhen` (ADR-0089). objectui carried three spellings of its own beside the spec's keys: `visibleOn` beside `visibleWhen`, `hiddenOn` beside `hidden`, `disabledOn` beside `disabled`, each pair evaluated by the same code with the same polarity. The three are retired, immediately, with no alias window. - **`@object-ui/types`** — `BaseSchema.visibleOn` / `hiddenOn` / `disabledOn` are `?: never` tombstones on the TypeScript face and `aliasKeyRefusal` arms on the Zod mirror (the `chartType` → `type` shape). Each refusal names the key, the surviving key, the unchanged polarity, and the remedy ("rename the key; the value is unchanged"). Boolean `visible` stays on `BaseSchema` (see Acceptance note 1). - **`@object-ui/react`** — `SchemaRenderer`'s visibility chain is `visibleWhen` / `visible` (SHOW legs, negated) and `hidden` (HIDE leg, not negated); the enablement gate is `disabled` alone. A stale retired key that reaches the renderer unvalidated is not consulted and is held out of the component's props (the `body` treatment). The diagnostics' key lists, the props-bag refusal and the fault copy follow. - **`@object-ui/sdui-parser`** — the three leave `SDUI_BASE_PROPS`; `validateTree` reports each as an `unknown-prop` diagnostic that names the surviving key and says the spelling is retired (`checkRetiredVisibilityDialect`, `RETIRED_VISIBILITY_DIALECTS`, exported). - **`@object-ui/cli`** — `objectui validate` reads the Zod mirror and prints the tombstone (measured below). `objectui check` keeps the three in `OBJECTUI_STRUCTURAL_KEYS` as identification markers, exactly as it keeps `body` (objectui#6771): a file authored under a retired spelling is still recognised and sent to `validate`; the sweep itself does not parse recognised files (triage ruling objectui#11007). - **`@object-ui/app-shell`** — the command palette reads a navigation entry's `visible` only; the `?? item.visibleOn` fallback read a key `NavigationItem` never declared. - **Readers** (components form/action/disclosure renderers, core `declaredPredicate` / `dom-props`, react diagnostics, plugin-chatbot, `complex.ts`): comment-only, the retired spellings dropped from the prose. - **Tests** — the `hiddenOn` / `disabledOn` / `visibleOn` cases are retired from 13 react/components/sdui-parser pins and the surviving cases re-pointed; two new pins: `node-visibility-dialects-retired.test.ts` (one `invalid_type` issue at the key, message carries `Did you mean X → Y?`, `.describe()` equals the message, `z.input` is `undefined`, both document faces refuse, canonical spellings parse) and `SchemaRenderer.visibilityDialectsRetired.test.tsx` (a stale key is not a leg and does not leak, each with a control on the surviving key). - **Skill + docs** — `skills/objectui/rules/protocol.md` and `guides/schema-expressions.md` teach `visibleWhen` / `hidden` / `disabled` (raw expression or `${}` template) and name the retired spellings as refused; `ui/content/docs` `guide/expressions.md`, `api/schema-reference.md`, `guide/schema-playground.md`, plus `guide/schema-rendering.md`, `guide/metadata-diagnostics.md` and `utilities/runner.mdx` (see deviation 1). The form field's view-level `fields[].visibleOn` is a different slot and is left as it was, with a sentence saying so. - **Changeset** `.changeset/25926-retire-node-visibility-dialects.md` (types / react / sdui-parser minor, app-shell patch; BREAKING; ADR-0087 `not-required (no-migration-prescription)` on the objectui-surface precedent of #25831). ## Skills diff readings (lines; tokens = ceil(bytes/4) per `check-skills-token-ratchet`) | file | lines before → after | tokens before → after | |---|---|---| | `skills/objectui/rules/protocol.md` | 377 → 375 | 4368 → 4365 | | `skills/objectui/guides/schema-expressions.md` | 646 → 642 | 6571 → 6571 | | whole package, sum of all `SKILL.md` | 4525 → 4525 | — | The guide's net content is paid by deleting one redundant `hidden` template example (the "Data-dependent" pattern duplicated the syntax block above it); the token ratchet is green at the ceiling. ## 维护者速读(草稿) ### 改了什么 objectui 节点上的 `visibleOn` / `hiddenOn` / `disabledOn` 三个拼法退役:类型面变为 `never`,Zod 镜像按名拒绝并指向 `visibleWhen` / `hidden` / `disabled`(同一谓词、同一极性,只改键名,值不变);渲染器只读存活的三个键;SDUI 解析器对 TSX 树同样按名报告;技能包与文档改教存活拼法;`objectui validate` 直接打印拒绝原因。 ### 为什么改 规范(ADR-0089)在每一层只声明一个条件可见谓词 `visibleWhen`,从未声明 `*On` 后缀;objectui 自造的三个别名与规范键被同一段代码、同一极性求值,只增加了方言数,没有增加能力。保留别名意味着同一件事有两种写法、两份文档和两组测试要维护。 ### 风险与代价(含回滚) - 风险:任何仍写这三个键的页面/视图/组件文档会在校验时被按名拒绝(有修法提示),渲染时该键被忽略。仓内示例、fixture、技能示例均无命中;文档与技能中的示例已同 PR 改写。 - 代价:四个 `@object-ui/*` 包发版(三个 minor、一个 patch),changeset 已标 BREAKING;无迁移脚本(objectui 面不进 `packages/spec` 的 ADR-0087 账本,沿 #25831 的处置)。 - 回滚:整体 revert 本 PR 即可,无数据迁移、无生成物需要回退(`sdui.manifest.json` 字节不变,仅源码摘要记录重录)。 ### 席位意见 (留空) ### 你要做的 1. 确认接受「立即退役、无别名窗口」(派发令如此要求)与 `Metadata-protocol: narrowed` 的判定(认领写的是「按 diff 判定,预期 none」,实测为 narrowed:可写面上的键被 tombstone)。 2. 确认 Acceptance note 1:布尔 `visible` 按规范继续保留在 `BaseSchema` 上(ADR-0089 D4 明确不在范围),派发令中「收敛为 visibleWhen + hidden」的字面读法未照办。 3. 按席位复核后合入;本 PR 为 Tier H,等人批。 ## Acceptance notes 1. **`visible` stays.** The order reads "collapse to `visibleWhen` + `hidden`". `visible` is spec-declared on action nodes, app navigation entries, tab rows, `record:alert`, bulk actions, action params and settings, and ADR-0089 D4 keeps boolean `visible` out of scope; the premise comment says "keep boolean `visible` only where the spec declares it". Removing the renderer leg would leave a declared key unevaluated (声明即强制) and would breach the 38-file reader surface. So the chain collapses from five legs to three: `visibleWhen` / `visible` / `hidden`. Flagged for the seat to confirm. 2. **`Metadata-protocol: narrowed`, not `none`.** The claim expected `none` ("ui types only"); the diff tombstones three keys on objectui's authorable node face, which is "a key removed or tombstoned" by ruling A's definition, and the level gate pairs it with the claim's `no (narrowing)` arm. Both lines are on the changeset; the ADR-0087 gate accepts the disposition. The seat may amend the claim. 3. **`objectui check` names nothing by itself.** The order says `validate` / `check` refuse the three by name. `validate` does (measured below). `check` is an advisory sweep that, by the objectui#11007 triage ruling, does not parse a file its structural arm recognised; a stale file is reported "recognised but not validated" and sent to `validate`. Dropping the three markers instead would let a leaf file carrying only a retired key and an unregistered `type` fall out of the sweep unmentioned, which is the silence the `body` precedent refused. Left as the ruling has it; the seat may rule otherwise. 4. **Out of scope, found while reading:** the table-slot refusals in `zod/objectql.zod.ts` (`tableKeyRefusal('hiddenOn')` and siblings at lines 1171–1199) still prescribe "belongs on the object-view node itself", which now points at a tombstoned spelling. Not this card's keys; for the seat to file. `responsive.hiddenOn`, `FormField.visibleOn` (view-level) and the `suggestions.zod.ts` examples are untouched as the order states. ## Deviations 1. **File surface, docs:** the claim lists `guide/expressions.md`, `api/schema-reference.md`, `guide/schema-playground.md`. Three more files under `ui/content/docs` taught the retired spellings as live node keys (`guide/schema-rendering.md` ×11, `guide/metadata-diagnostics.md` ×3 node-level mentions, `utilities/runner.mdx` ×1) and are fixed in place under the bounded in-place-fix exemption (same defect class, mechanical rename, same gate family, no new verification surface). The seat is asked to amend the claim's file surface. 2. **Ablation docblock corrected once:** the new react pin's reverse-verification paragraph first named the `VISIBILITY_*_KEYS` lists as the ablation site; the first ablation stayed green because the legs are hand-written `if` blocks and the lists only drive diagnostics. The paragraph now names the leg; the second ablation (one restored leg) went red on exactly the predicted case. Both runs are in the report. ## Verification - `objectui validate` on `{ "type": "text", "content": "Open", "visibleOn": "data.ok" }` → exit 1, one error at `visibleOn`, code `invalid_type`, message "Unrecognized key(s) on this node: `visibleOn`. Did you mean `visibleOn` → `visibleWhen`? …Rename the key; the value is unchanged."; the same node spelled `visibleWhen` → exit 0. - Reverse verification (TypeScript face): `visibleOn: 'data.ok'` pasted into the typed node literal of the new types test → `tsc -p tsconfig.test.json` red, TS2322 "Type 'string' is not assignable to type 'undefined'" at line 78; restored via `git checkout HEAD -- FILE` → green. - Ablation (`scripts/ablation-replace.mjs`, mutation landed and restored, blob == HEAD): restoring a `visibleOn` leg after the `visible` leg in `SchemaRenderer.tsx` → `SchemaRenderer.visibilityDialectsRetired.test.tsx` 1 failed | 6 passed, the failing case being "`visibleOn: "false"` is not a SHOW leg"; the earlier list-only mutation stayed 7 passed (recorded, see deviation 2). - Type-check: `@object-ui/types`, `core`, `sdui-parser`, `components`, `react`, `app-shell`, `cli`, `plugin-chatbot` — all exit 0 after `pnpm --filter '@object-ui/cli...' --filter '@object-ui/app-shell...' build`. - Tests: `cd ui && pnpm exec vitest run packages/types/ packages/sdui-parser/ packages/react/ packages/components/ packages/plugin-chatbot/ --maxWorkers=2` (under the verify lock, 19m58s) → first run 1 failed | 1050 passed (`static-table-narrow-surface.test.ts` pinned the table mirror's exact tombstone set, which now inherits the three node-face tombstones; re-pointed in 6f044d4, that file alone re-run: 39 passed); `… packages/app-shell/ --maxWorkers=2` → Test Files 1735 passed | 1 skipped (1736), Tests 15960 passed | 9 skipped; `pnpm --filter @object-ui/core test` → 211 files passed; `pnpm --filter @object-ui/cli test` → 30 files passed; the 13 directly touched/new pins run alone → 13 files, 267 tests passed. (Per-package `pnpm --filter @object-ui/types test` fails with `spawn ENOENT` from the repo root: vitest lives in `ui/node_modules/.bin`, so the ui suites are run from `ui/`.) - Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 68 families at the final head; every command run from the worktree with its exit captured before any pipe; `--ran` reconciliation: 68 derived, 68 run, 0 UNRUN. 66 exit 0. `pnpm check:type-check-debt` → re-run under the lock after the suites (exit 0, held the lock 131s). `pnpm check:dual-build-cjs-loads` → exit 99 (never acquired the lock behind the 74-minute app-shell run; a full-build gate CI runs on the PR, not re-run here). - Dogfood battery (final head, `(narrowing)` arm): head 6f044d4, after `pnpm --filter '@objectstack/dogfood^...' build` (exit 0); `pnpm --filter @objectstack/dogfood test` under the lock → VERDICT command-exit 0 · held the lock 973s (16m13s) · Test Files 297 passed | 3 skipped (300) · Tests 2527 passed | 18 skipped (2545); no failure. - `check-platform-checklist --changed-from origin/main`: 0 anchored files, nothing judged. `check-skills-token-ratchet`, `check-adr-0087-registration`, `check-changeset-no-major`, `check-empty-changeset`, `check:doc-authoring`, `check:pm-skill-id-lint`, `check-sdui-manifest` all green on the committed tree. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01E5XcbmSeZTDGm5zCYkC9XV --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 77a52cf commit e843b5b

61 files changed

Lines changed: 870 additions & 443 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@object-ui/types': minor
3+
'@object-ui/react': minor
4+
'@object-ui/sdui-parser': minor
5+
'@object-ui/app-shell': patch
6+
---
7+
8+
feat(types,react,sdui-parser,app-shell)!: the node-level `visibleOn` / `hiddenOn` / `disabledOn` spellings are retired; `visibleWhen` / `hidden` / `disabled` are the one predicate each
9+
10+
Clause-②: no (narrowing)
11+
Metadata-protocol: narrowed
12+
13+
<!-- 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). -->
14+
15+
`@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.
16+
17+
- **`@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.
18+
- **`@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.
19+
- **`@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.
20+
- **`@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.
21+
22+
**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.
23+
24+
- **Before.** The three keys parsed, were evaluated beside their canonical twin, and the docs taught both spellings.
25+
- **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.
26+
- **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.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"package":"ui/packages/components","sources":226,"sha256":"75ed8c66878430e296c285bb0885304449f091b00da2209889cb99f634ef1690","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
1+
{"package":"ui/packages/components","sources":226,"sha256":"2321bcd998e35f7367cbab8745cb1837768ff5dec8a5684709b8e06e13be6c69","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"package":"ui/packages/core","sources":112,"sha256":"e586f7573193068eefdbf8c22651072941bbd3843d60612cc2bc5d7e16003fca","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
1+
{"package":"ui/packages/core","sources":112,"sha256":"810fe0e8f6db868fd443540cb2e30a85f691a48a08d58ba7ba8db0c577d579e7","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"package":"ui/packages/react","sources":70,"sha256":"0a4e839e9459da5ce7a6eb9b112d527bf8afecd3f7e8c6f63b936e5eb9593c68","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
1+
{"package":"ui/packages/react","sources":70,"sha256":"d7a6fb1f906784fd1c2d04592bf54d095db68a222bc5dce8b4747f944288a944","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"package":"ui/packages/sdui-parser","sources":9,"sha256":"08704fc188cb4749560f2a3faedd77b60820c1990f03311bcad21f283b2d7be4","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
1+
{"package":"ui/packages/sdui-parser","sources":10,"sha256":"57c2952e2ffa03ee0144f1b26fb0af73272c684004f8e8dd689b2b061243509e","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"package":"ui/packages/types","sources":81,"sha256":"0373b9f45d8be39821641918e2e8058b8016ad24232b39111193116732fdf00f","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
1+
{"package":"ui/packages/types","sources":81,"sha256":"f8450c19832a17c2f45e0164ec9e6e68cb247efdc00c81998c77e8c42c11da65","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}

‎skills/objectui/guides/schema-expressions.md‎

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,8 @@ name, so calls are case-insensitive.
236236

237237
### Syntax options
238238

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

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

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

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

258260
### Visibility patterns
259261

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

272-
**Data-dependent:**
273-
<!-- os:check -->
274-
```json
275-
{ "hidden": "${!data.items || data.items.length === 0}" }
276-
```
277-
278274
**Combined conditions:**
279275
<!-- os:check -->
280276
```json
@@ -290,13 +286,13 @@ The `On` variants accept raw expressions without `${}` wrapping — the entire s
290286

291287
<!-- os:check -->
292288
```json
293-
{ "disabledOn": "!data.canPerformAction || data.isLocked" }
289+
{ "disabled": "!data.canPerformAction || data.isLocked" }
294290
```
295291

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

298294
These live on the **object's field metadata**, are written in CEL, and are
299-
evaluated by `@objectstack/formula` -- a different tier from the `${}` / `On`
295+
evaluated by `@objectstack/formula` -- a different tier from the node-level
300296
conditions above, which are schema/widget-level and run on the recursive-descent
301297
`SafeExpressionParser`. **How to author them is the `objectstack-data` skill's
302298
job**, under "Conditional Field Rules": the predicate scope, the client-only vs
@@ -641,6 +637,6 @@ When an expression isn't working:
641637
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.)
642638
2. Is the `${}` syntax correct? Check for unmatched braces.
643639
3. Is the data actually available in scope? Check what the host published on `PredicateScopeProvider` — ⛔ not `SchemaRendererProvider`'s `dataSource`, which publishes no expression root.
644-
4. For conditions: are you using `On` suffix correctly? (`hiddenOn` takes raw expression, `hidden` needs `${}` if it's a string).
640+
4. For conditions: `visibleWhen` / `hidden` / `disabled` take a raw expression or a `${}` template; the `On` spellings are refused.
645641
5. Does the expression use a blocked pattern? Check for constructors, `eval`, `window`, etc.
646642
6. Is type coercion causing issues? `${0 && "yes"}` returns `0`, not `false`.

‎skills/objectui/rules/protocol.md‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,15 @@ value must clear before it reaches the screen.
1313
| Field | Evaluation Type | Return Type | Example |
1414
|---|---|---|---|
1515
| `content` | Template (`${}`) | string | `"content": "Total: ${data.total}"` |
16-
| `hidden` | Condition | boolean | `"hidden": "${data.status !== 'active'}"` |
17-
| `hiddenOn` | Condition | boolean | `"hiddenOn": "data.status === 'draft'"` |
16+
| `visibleWhen` | Condition | boolean | `"visibleWhen": "data.permissions.canView"` (raw expression, no `${}`) |
17+
| `hidden` | Condition | boolean | `"hidden": "data.status === 'draft'"` |
1818
| `visible` | Condition | boolean | `"visible": "${data.isActive}"` |
19-
| `visibleOn` | Condition | boolean | `"visibleOn": "data.permissions.canView"` |
20-
| `disabled` | Condition | boolean | `"disabled": "${form.isSubmitting}"` |
21-
| `disabledOn` | Condition | boolean | `"disabledOn": "!data.hasPermission"` |
19+
| `disabled` | Condition | boolean | `"disabled": "!data.hasPermission"` |
2220
| `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. |
2321
| `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. |
2422
| `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. |
2523

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

2826
### Fields That are NOT Evaluated
2927

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

118115
**❌ WRONG — renders an empty card:**
119116
<!-- doc-json: fragment — deliberate counter-example: a title parked under props never reaches the card renderer, and props is no declared key -->

‎ui/content/docs/api/schema-reference.md‎

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,8 @@ All schema types extend `BaseSchema`. These shared properties are available on e
7070
"description": "A container element",
7171
"className": "p-4 bg-white rounded-lg",
7272
"visible": true,
73-
"visibleOn": "${data.showWrapper}",
74-
"disabled": false,
75-
"disabledOn": "${data.isLocked}",
73+
"visibleWhen": "${data.showWrapper}",
74+
"disabled": "${data.isLocked}",
7675
"testId": "wrapper-element",
7776
"ariaLabel": "Content wrapper",
7877
"children": []
@@ -96,12 +95,12 @@ One row per declared member, in declaration order, so the list can be checked ag
9695
| `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`. |
9796
| `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`. |
9897
| `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. |
99-
| `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. |
100-
| `visibleOn` | `string` | Expression for conditional visibility. **Deprecated** (ADR-0089) — use `visibleWhen`. |
101-
| `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. |
102-
| `hiddenOn` | `string` | Expression for conditional hiding. |
98+
| `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. |
99+
| `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). |
100+
| `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. |
101+
| `hiddenOn` | *retired* | ⛔ Refused by name; the refusal names `hidden` (same predicate, same polarity). |
103102
| `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`. |
104-
| `disabledOn` | `string` | Expression for conditional disabling. |
103+
| `disabledOn` | *retired* | ⛔ Refused by name; the refusal names `disabled` (same predicate, same polarity). |
105104
| `testId` | `string` | Test identifier, rendered as `data-testid`. |
106105
| `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. |
107106

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

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

0 commit comments

Comments
 (0)