Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

Commit 138ad45

Browse files
feat(types,core)!: an authored flex takes its props in the properties bag; the flat spelling is refused by name (objectui#11276) (#11406)
Fixes #11276 Clause-②: yes `Clause-②: yes`, copied from the PM claim `5940199465`: the strict face starts accepting the `properties` bag on an authored `flex`, which widens it, while the flat spelling narrows. This is objectui's narrowing-as-`minor` rule with a **BREAKING (authoring)** banner in the changeset, as in PR objectui#11328 and PR objectui#11399. The `flex` batch of objectui#11276, the last one. It executes triage's addendum `5926377520` under the maintainer's ruling on objectui#11300, verbatim: 「A」. With this PR the card's three arms (`object-chart`, `object-grid`, `flex`) all take the spec's bag. ## What changed - `packages/types/src/zod/layout.zod.ts`: new `FlexBlockSchema`, the authored `flex` arm, built the way `ObjectChartBlockSchema` is (PR objectui#11328): `BaseSchema` + the `flex` literal + `NODE_ENVELOPE` + `properties` through `propsBag`. - `@objectstack/spec` 17.5.0 has **no** `ComponentPropsMap['flex']` row (55 rows, none `flex`; `object-grid` present as the control), so no row is invented. The bag's `direction`, `justify`, `align`, `gap` and `wrap` are the flat `FlexSchema` mirror's own members, the same schema objects. The bag keeps the mirror's `.passthrough()` posture. - The child list is in the bag, `properties.children`, where the showcase writes it and the spec's page walk reads it. It keeps the mirror's accept set (one node, or a list), but the LIST's entries are left to the page-walk judgment `AnyComponentSchema` already runs at every `properties.children` (objectui#11223), as on the `page:` containers. Measured before choosing this: with the mirror's own `children` member in the bag, a refused child was reported twice (an `invalid_union` at `properties.children` plus the child's own issue) and every nesting level was judged twice. A single node, which the walk does not descend, is judged by `SchemaNodeSchema`. - Flat members are refused by name through the shared `flatPropRefusals` (`public-blocks.zod.ts`), the helper PR objectui#11399 used. It gains an optional third argument, the refusal detail for a bag that is not a spec row, because its default detail names `ComponentPropsMap['KEY']`, a row `flex` does not have. Every existing caller is unchanged. `body` is refused toward `properties.children`, replacing the mirror's refusal, whose remedy (a node-level `children`) this arm refuses. - `FlexBlockSchema` replaces `FlexSchema` in `LayoutSchema`. `FlexSchema` stays exported, unchanged in shape: it is the node as the `flex` renderer reads it after the hoist, and as code composes it. - `packages/types/src/layout.ts`: the TypeScript `FlexSchema` is **re-declared, not retired** (docblock only). `FlexLayoutProps`, already exported, is the bag's TypeScript type; typed authored nodes check their bag with `satisfies FlexLayoutProps`. - `packages/core/src/builder/schema-builder.ts`: `flex()` builds `{ type: 'flex', properties: { … } }`; `build()` is typed `BaseSchema & { type: 'flex'; properties: FlexLayoutProps }`. - objectui's own producers, moved to the bag (census below). - objectui#6751's fence (`5460852791`, and `5542677253`'s "`flex` declares its own keys") is revoked for `flex` only. Its pins flip to bag pins; its `props`-envelope half (the deliberate counter-examples in `skills/**`) stands and is untouched. - New pins: `packages/types/src/__tests__/flex-properties-bag-11276.test.ts` (43 rows plus a type-level block) and `packages/components/src/__tests__/flex-properties-bag-render-11276.test.tsx` (render through the real `SchemaRenderer` and registry: a bag node and a **stored flat node** draw the same class string and subtree; `SchemaRenderer`'s hoist is untouched). - Two changesets: `.changeset/11276-flex-properties-bag.md` (`@object-ui/types` minor, BREAKING (authoring), names the migration) and `.changeset/11276-core-flex-builder-properties-bag.md` (`@object-ui/core` minor, the builder's output shape). - `skills/objectui/guides/page-builder.md` §3b ("Pick the right layout primitive"), commit `56d5f7fd`, under the seat's claim amendment `5941574748`: the section said the layout primitives all "take `children` and read every key off the node" and tabulated `flex`'s props, which after this PR is a refused shape. Two sentences change: the intro now says `stack`, `grid`, `container` and `box` keep reading every key off the node while `flex` takes its props and `children` in `properties`, and the `flex` / `stack` intent sentence names the refusal (`objectui validate` refuses flat `flex` props). The table's "Key props (renderer defaults)" column is unchanged: those stay the renderer's defaults, only where they are authored moved. Net `skills/**` lines 0 (file 353 → 353; bundle 4806 → 4806). Measured first at `5af4d2e7` through the built faces: the flat `flex` as §3b taught it is refused by `safeValidateSchema` and `StrictAnyComponentSchema` (four `invalid_type` refusals, each naming `properties.KEY`), the bag and the showcase leaf shape pass both, a flat `stack` / `grid` / `container` / `box` passes both (a bag `stack` is refused by the strict face: the bag is not the contract there). §3b holds no fence and the bundle holds no `flex` node (grep over `skills/`, `.claude/`, `docs/adr` is empty; `object-grid` hits `page-builder.md` as the control). Three-way `git merge-file` against draft PR objectui#11404's `page-builder.md` (head `d83d2748`): exit 0, 0 markers, both edits present. With this file the guard answers GOVERNED (`skills/**` x1 of 94 paths), so the PR takes the governed path: draft until an authorized APPROVED review, as the amendment says. **Files beyond the claimed surface, each forced:** census rows the new export forces (`zod-mirror-parity.test.ts`, one `EXCLUSIONS` row); fixture triage where a reader reads `flex` props off a catalog node (`examples/schema-catalog/test/layout-props-conversion.test.tsx` gains a `propsOf` reader and the nine `space-x` site paths now step through `properties.children`; `badge-demo-label-6829.test.tsx` locations); the guide's remedy fixture in `page-actions-refusal-7926.test.ts`; `packages/types/src/zod/README.md`. ## Zone 2, measured first 1. **Held.** At base `6aa029b63f`, through the built faces: a bag `flex` node (`{ type: 'flex', properties: { direction: 'col', gap: 2, children: [ … ] } }`) and the showcase shape (`{ id, type: 'flex', responsiveStyles, properties: { children: [] } }`) passed `safeValidateSchema` and were refused by the strict face (`unrecognized_keys` naming `properties`); the flat spelling passed both. The spec's `PageComponentSchema` accepted the bag node and refused the flat one (`unrecognized_keys` naming `direction`, `gap`, `children`); control: a flat `action:button` `actionType` is refused the same way. 2. **No row.** `ComponentPropsMap` in installed 17.5.0 has no `flex` key, so the bag is judged by objectui's mirror members (`direction`, `justify`, `align`, `gap`, `wrap`, and the child list as described above). 3. **Census** at base `6aa029b63f`. Instrument: `git grep -lE` for a `type` key equal to `flex` in any quoting, minus tests / snapshots / e2e: 84 non-test files; plus a search for code that emits a `flex` node without that literal. | class | files | action | |:--|:--|:--| | catalog documents | 70 under `examples/schema-catalog/src/schemas`, 248 nodes (a JSON walk: node keys were only `type`, `gap`, `wrap`, `children`, `align`, `justify`, `className`, `direction`) | moved: 245 nodes in 68 canonically formatted files by a script (JSON round trip, byte-identical outside the moved nodes), 3 nodes in `auth/login-simple.json` and `auth/signup.json` by hand to keep their compact lines | | guides / reference | `content/docs/guide/layout.md` (3 nodes), `content/docs/components/layout/flex.mdx` (its Schema block) | moved; `flex.mdx` now shows a bag JSON example and the bag's members, with the full declared `align` / `justify` vocabularies | | `@object-ui/types` docs and examples | `README.md` (2 nodes), `examples/dashboard.ts` (2 nodes), `examples/data-display-examples.json` (1 node) | moved; the TS ones use `BaseSchema` plus `satisfies FlexLayoutProps` | | code that emits a node | `@object-ui/core`'s `flex()` builder (`super('flex')`, missed by the literal grep) | emits the bag | | not an authored node | `packages/plugin-designer/src/PageDesigner.tsx` (a palette entry that builds a `DesignerComponent`, the designer canvas model, carrying no props); `packages/sdui-parser/verify.ts` (a manifest entry for the `kind: 'html'` JSX tier, whose compiled nodes never meet the zod face); the `flex` registration's `defaultProps` (a designer / manifest seed; no composer turns it into a document node on the zod path) | kept | | declarations | `packages/types/src/layout.ts` | docblock only | | prose / history | 3 pending changesets, 3 CHANGELOGs | not edited; the pending ones are answered in the new changeset | | governed | `skills/objectui/guides/page-builder.md` §3b: prose that taught `flex`'s props on the node. No governed file holds a `flex` node (the same grep over `skills/`, `AGENTS.md`, `CLAUDE.md`, `.claude/`, `docs/adr/` exits 1, while the pattern for other types hits `skills/`, the control) | edited in commit `56d5f7fd` under the seat's amendment `5941574748`, so this PR is GOVERNED | The census's "16 tests in 8 files" re-derives differently because this PR moves the catalog together with the arm: red first at `b453ead8` before the pins flipped was `Tests 30 failed` in 7 files (the 6751, 8761 and 10872 pins, the 7926 remedy row, the parity census row, and two catalog readers that read `flex` props off the node); the catalog-through-the-face pins the census simulation turned red (8499, 6939, 6942, 8284) stay green because the documents moved too. 4. **`layout.md` and PR objectui#11069.** `git merge-file` of this branch's `layout.md` against PR objectui#11069's head `a11f73347d` over their merge base: exit 0, 0 conflict markers, both edits present. `git merge-tree` of this head against that PR: the only conflict is `packages/cli/src/commands/check.ts`, the same one `main` at base has with it; `layout.md` auto-merges. 5. **The showcase.** objectstack `origin/main` `3a7b6eb0` has 15 `type: 'flex'` literal sites in `command-center.page.ts` and `styling-gallery.page.ts`, plus 1 taught node in `skills/objectstack-ui/rules/pages.md`: the 16. Evaluated with their helpers they are 60 runtime nodes in the two pages plus the taught one (a scratch instrument, not committed): | reading | base `6aa029b63f` | this head | |:--|:--|:--| | each `flex` node's own verdict, tolerant face | 61 / 61 clean | 61 / 61 clean | | each `flex` node's own verdict, strict face (the face PR objectui#11069 wires) | 0 / 61 (`properties` refused) | **61 / 61 clean** | | whole subtree, both faces | 58 / 61 tolerant, 0 / 61 strict | 58 / 61 both | | spec `PageComponentSchema` | 61 / 61 | 61 / 61 | The 3 whole-subtree refusals at head are not `flex`: they are the command-center root, its KPI panel and its KPI band, whose `object-metric` tiles write `filter` as a record (`{ status: 'active' }`), which the spec's own `ComponentPropsMap['object-metric']` row refuses ("takes the ViewFilterRule ARRAY form"). See Acceptance notes. PR objectui#11069's own census script does not read objectstack, so its count is read through the strict face it wires, which accepts all 61 `flex` nodes. Through the real `objectui validate` binary (`packages/cli/dist/cli.js`, built from this tree): a flat `flex` exits 1 with three named refusals pointing at `properties.children`, `properties.justify` and `properties.gap`; the bag node and the showcase leaf shape exit 0. ## Construct reused, and why PR objectui#11328's (`object-chart`: no spec row, the bag is the mirror's own members by reference, the description says so) rather than PR objectui#11399's (`object-grid`: a spec row by reference), because `flex` has no row. The refusals come from PR objectui#11399's shared `flatPropRefusals` rather than a per-arm map, so there is one copy; its new optional detail is the no-row counterpart of `propsBag`'s optional description, which PR objectui#11328 added for the same reason. ## Tests (heavy runs through `os-verify-lock.sh`, slot `issue-11276-flex`; Node v22.22.2 from `/opt/nvm`) - **Union at `b453ead8`/`bacbb4d7`** (the content of this head; the later commits merge `main`, which touched only `packages/layout`, and edit one changeset sentence): `pnpm exec vitest run packages/types/ examples/schema-catalog/ packages/cli/ packages/core/src/builder/ packages/components/src/__tests__/flex-properties-bag-render-11276.test.tsx`: `Test Files 384 passed (384)`, `Tests 11192 passed (11192)`, VERDICT 0. - **Consumer sweeps:** 42 files naming the catalog, the moved docs, `FlexSchema`, `LayoutSchema`, the builder, plus `packages/sdui-parser/`: `Test Files 61 passed (61)`, `Tests 1035 passed, 16 skipped` (the skips are `TimelineGantt.dateOnlyZone-10866`'s timezone-gated rows). 73 more files naming the faces, `content/docs` or a `flex` literal: `Test Files 72 passed (72)`, `Tests 1089 passed`. `scripts/__tests__/`: `Test Files 177 passed, 2 skipped (179)`, `Tests 5417 passed, 2 skipped`. `packages/core/`, `packages/components/src/__tests__/`, `packages/sdui-parser/`, `packages/plugin-designer/`: `Test Files 441 passed, 1 skipped (442)`, `Tests 6294 passed, 34 skipped`. Each VERDICT 0. - **Build and type-check:** `pnpm --filter @object-ui/types build`, `type-check` (all three legs: the examples program holds `examples/dashboard.ts`, the test program holds the new pin, read with `--listFilesOnly`), `pnpm --filter @object-ui/core build`, `type-check`: VERDICT 0. - **Runtime ablation** (objectstack `scripts/ablation-replace.mjs`, WRAP mode with its EXIT/INT/TERM restore on the absolute path): `FlexSchema` back in `LayoutSchema` in place of `FlexBlockSchema` (anchor x1 to x0, blob `1c3af5f2` to `4fbf4fb8`). Over 7 files: `Tests 37 failed, 218 passed (255)`: red = 29 rows of the new pin, 6 of the 10872 file, the builder row, the 8761 counter-probe; green = the `object-chart` and `object-grid` pins (controls) and the 6751 file, which parses `FlexBlockSchema` directly. Restored: blob == HEAD `1c3af5f2`, `git diff HEAD` empty. - **Ablation of the child-list choice:** the bag's `children` returned to the mirror's own member (anchor x1 to x0). Exactly the 3 judged-once rows go red; restored, blob == HEAD. - **Type-level reverse check:** the `flatPropRefusals` spread deleted, `tsc -p tsconfig.test.json` exits 2 with exactly 2 TS2344 errors, the two flat-keys-take-nothing rows of the new pin. Restored, blob == HEAD. - **Corpus sweeps** (scratch instruments): every catalog document holding a `flex` node (70), both faces: the refused set is identical at base and head (2 tolerant, 4 strict, the same documents, codes and keys: carousel `items`, resizable `panels.N.id`, input-otp `separator`, label `required`), only the paths gaining `properties.`. Every JSON fence in tracked `.md` / `.mdx` holding a `flex` node: base 3, head 4, refused 0 at both. `objectui check` over the repository (the same binary at both trees): base 267 validated / 166 recognised / 1 did not validate; head 287 / 146 / 1 (the same file, the vscode-extension JSON schema): root-`flex` documents that carry no other structural key are now parsed in full, and pass. - **Changed TS doc blocks:** the README nesting block and the `flex.mdx` interface compile against the built types (`tsc --strict`, exit 0); control: `direction: 'column'` in the README bag is TS2322, exit 2. ## Gates (exit codes captured before any pipe) - exit 0 at `25844284`: `check:new-line-citations` ("VERDICT new-cross-file-line-citations: 0 new citation(s)"), `check:control-bytes` ("OK (scanned 9930 tracked text file(s); skipped 85 binary)"), `markdown-test-inputs --audit` ("109 candidate test files, all adjudicated; 81 declared entries, all present"), `check-changeset-presence` ("13 source file(s) of 3 released package(s) changed, and this change declares 2 changeset(s)"), `changeset:check`, `check:spec-symbols`, `check:doc-types`, `check:doc-example-ids` ("415 real reference(s) all resolve"), `check:doc-fences`, `docs:check-links`, `check:handler-key-reads`, `check:pending-changeset-literals`, `check:test-path-roots`, `check-governed-queue-guard.mjs --test` over the 93 paths ("NOT GOVERNED"; at `56d5f7fd`, with §3b added, GOVERNED over 94 paths). Re-run at `5af4d2e7` (a changeset sentence only): presence, `changeset:check`, `check:control-bytes`, `check:new-line-citations`, each exit 0. - exit 0 at `bacbb4d7`: `check:vi-mock-specifiers`, `check:component-surface-parity`, `check:element-data-source-declaration`, `check:installed-pin-claims`, `check:prompt-keys`, `check:doc-example-readers`, `check:governed-queue-guard`, `check:changeset-claims` (report-only; its re-read list holds no paragraph this change falsifies). - ESLint narrowing: the config's population is `**/*.{ts,tsx}`; `eslint --no-inline-config --format json` over the 16 changed `.ts` / `.tsx` files: 16 files, 0 errors, 5 `no-explicit-any` warnings, none on an added line. `eslint.config.js` sets no `projectService` / `parserOptions.project`, so no untouched file's verdict can move. The full `pnpm lint` is CI's. - NOT MEASURED, PREREQUISITE NOT MET (unbuilt packages): `check:doc-snippets` (exit 2), `check:doc-examples` (exit 2), `check:readme-exports` (exit 1, "the population COLLAPSED"; among what it judged, 0 wrong-path, 0 fabricated). The changed TS blocks were compiled separately (above). CI builds first. ## Deviations - **The bag's `children` is not the mirror's member by reference.** It keeps the mirror's accept set and hands the list's entries to the page-walk judgment, because the mirror's member double-reports every nested refusal (measured, and pinned by the child-list ablation above). One edge differs from the mirror and matches the `page:` containers: an entry that is itself an array is not visited by the walk. - **`flatPropRefusals` gained an optional argument** (the no-row refusal detail). Existing callers and their messages are unchanged. - **Two changesets**, as the order allows when another package's published files change: `@object-ui/core` ships the builder. - **A detached comparison worktree at base** (`../objectui-issue-11276-flex-base`, removed after use) carried the before readings; a copy of this tree's built CLI (its code is unchanged by this PR) ran `objectui check` there. ## Acceptance notes - **The showcase's 3 non-`flex` refusals.** The command-center KPI tiles (`object-metric`) write `filter: { status: 'active' }` and `{ status: { $ne: 'done' } }`; the installed spec row `ComponentPropsMap['object-metric']` refuses the record form by its own message, and so do both objectui faces, at the tiles' `properties.filter`. This is independent of `flex` and of this PR (it was already refused at base on the tolerant face). Whether `os validate`'s props gate refuses it too is NOT MEASURED. Filed by the seat as objectstack-ai/objectstack#21251. - **The governed `skills/objectui/guides/page-builder.md`** §3b said the layout primitives "take `children` and read every key off the node" and tabulated `flex`'s props. It holds no `flex` node, but after this PR it would teach the flat spelling for `flex`, so it is fixed in this PR (commit `56d5f7fd`, under the seat's amendment `5941574748`). - **`scripts/measure-strict-authoring-face.mjs`** (PR objectui#11069's M1 instrument) reads 3 documents cleaner than the shipped strict face at this head (whole-tree strict refusals 69 to 66: 2 catalog, 1 authored), because its strict twin does not close components nested at a bag's child list. The shipped face refuses all 3, unchanged from base (checked on `examples/dashboard.ts`: the pre-existing card `content`, at `properties.children.1.properties.children.1.children.0`). Its `flex` row reads `FlexBlockSchema`, 250 documents, 0 strict-refused. - **Pending changesets** that describe a flat `flex` (`10872-flat-arm-responsive-styles`, `6751-flex-props-envelope-lift`, `8284-content-channel-per-component`, and the `7926` remedy) describe the flat mirror, which still behaves as they say; the new changeset says so. ## Overlap Merged `main` at `7728c67c` (one commit, `packages/layout` only) before opening. Open PRs touching these files: PR objectui#11069 (`layout.md`, measured above). Session: `https://claude.ai/code/session_01JG2jy8a9su7ia4Hx7zxv42` (dispatched by seat `domain:ui#2`; this run is its subagent on branch `claude/issue-11276-flex-properties-bag`). ## 维护者速读(草稿) **改了什么。** ObjectUI 页面里的 `flex`(横向排布容器)从此和其他业务组件一样,把自己的参数(方向、对齐、间距、换行)和子节点统一写进 `properties` 包里;平铺在节点上的旧写法被 `objectui validate` 按键名拒绝,错误信息逐键指出新位置。配套一并改到位:仓内 70 份示例目录文档(248 个 `flex` 节点)、`layout.md` 与 `flex.mdx` 两份文档、`@object-ui/types` 的 README 与示例、`@object-ui/core` 的 `flex()` 构建器全部改成新写法;新增两组 pin 测试(校验面 43 行 + 渲染 1 份);发布给客户 agent 的技能包 `skills/objectui/guides/page-builder.md` §3b 改两句话,说明 `flex` 的参数去向(技能包净行数 0)。渲染端一个字节不动:已存的旧节点照样画出来,且与新写法画得完全一致。 **为什么改。** 这是您在 objectui#11300 的裁决「A」的落地。`@objectstack/spec` 的页面契约要求组件自己的参数住在 `properties` 包里;ObjectUI 过去的 `flex` 却只认平铺写法,于是 objectstack showcase 里按 spec 写的 16 处 `flex` 节点在严格校验面上全部被拒,而 ObjectUI 自己文档教的写法又被 `os validate` 拒绝 —— 两端各说一套,AI 按任一端写都会在另一端报错。统一到 spec 的写法后,showcase 的 61 个运行时 `flex` 节点按节点自身判定在两个校验面上全部通过(整树读数 58/61,余下 3 处是 `object-metric` 的 `filter` 写法,与 `flex` 无关,已另立 objectstack-ai/objectstack#21251);平铺写法被按键名拒绝并给出改法。 **风险与代价(含回滚)。** 对按 ObjectUI 旧文档写页面的外部作者是 authoring 层面的 breaking:他们的 `flex` 节点要把参数移进 `properties`,错误信息会逐键指出目标位置;changeset 已标 BREAKING(authoring)并写明迁移。运行时零风险:`SchemaRenderer` 的 hoist 不变,已存的平铺节点与新写法渲染结果逐字节一致(有渲染 pin)。本 PR 超过 5000 行人工合并阈值(大头是 70 份示例文档的机械迁移),故停在 draft 等您批准。回滚代价低:整体 revert 这一个 squash commit 即可,无数据迁移、无存储格式变化;注意 `skills/**` 那两句与代码改动同 PR 落地,只回滚其中一半会让技能包教一个被拒绝的写法。 **席位意见。** **你要做的。** 在 PR objectui#11406 上留下一条 APPROVED review(或回复「不批」并说明理由)—— 一个动作。 --- _Generated by [Claude Code](https://claude.ai/code/session_01JG2jy8a9su7ia4Hx7zxv42)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 64c173d commit 138ad45

94 files changed

Lines changed: 6084 additions & 4703 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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@object-ui/core': minor
3+
---
4+
5+
feat(core): the `flex()` builder emits its props in the `properties` bag (objectui#11276)
6+
7+
**BREAKING (output shape):** `flex()` now builds `{ type: 'flex', properties: { direction, justify, align, gap, children } }` instead of writing those keys on the node. That is the spelling `@object-ui/types`' authoring faces accept for an authored `flex` node, which now refuse the flat keys by name (objectui#11276, the maintainer's ruling A on objectui#11300); without this change, `objectui validate` would refuse what the builder builds. The base setters (`id`, `className`, `visible`, `disabled`, `testId`, `visibleOn`) still write on the node. Code that reads a built node's props reads them from `properties` (`flex().direction('col').build().properties.direction`); `build()` is typed `BaseSchema & { type: 'flex'; properties: FlexLayoutProps }`. Rendering is unchanged: `SchemaRenderer` hoists the bag onto the node before the `flex` renderer reads it.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@object-ui/types': minor
3+
---
4+
5+
feat(types): an authored `flex` takes its props in the spec's `properties` bag; the flat spelling is refused by name (objectui#11276)
6+
7+
**BREAKING (authoring):** a `flex` node that writes `direction`, `justify`, `align`, `gap`, `wrap` or its child list `children` on the node itself is now refused by `safeValidateSchema`, `validateSchema` and the strict authoring face, and so by `objectui validate`. Move each of them, the child list included, into the node's `properties` bag:
8+
9+
```json
10+
{ "type": "flex", "className": "p-4", "properties": { "direction": "col", "gap": 4, "children": [{ "type": "text", "content": "First" }] } }
11+
```
12+
13+
`id`, `className`, `style`, `visible` and the other base props, and the node-level `responsiveStyles`, stay on the node. Nothing changes at render time: `SchemaRenderer` hoists every `properties` key onto the node before the `flex` renderer runs, so the bag node draws what the flat one drew, and a stored flat node, or a node built in code, keeps rendering with its flat keys. In TypeScript, `FlexLayoutProps` is the bag's type: check an authored bag with `satisfies FlexLayoutProps`.
14+
15+
**Clause-②: yes** — the accept set narrows (a flat `flex` node that parsed is refused) and widens (a spec-shaped bag node that the strict face refused parses). Released as `minor` under the objectui narrowing rule, with this banner.
16+
17+
**Why.** `@objectstack/spec`'s strict `PageComponentSchema` refuses a prop written on a page component itself as mis-layered (ADR-0089 D3a), for every component type, and the maintainer ruled A on objectui#11300: the `properties` bag is the contract on `flex` too. The arm here was the flat mirror of the TypeScript `FlexSchema`, so the two validators disagreed both ways: the strict face refused the objectstack showcase's layout boxes, written in the bag, and both faces accepted the flat node `os validate` refuses. objectui#6751's guidance that `flex` declares its keys on the node is withdrawn for `flex` only; `stack` and every other layout arm keep their props on the node.
18+
19+
**What changed, in observable terms.**
20+
21+
- `@object-ui/types/zod` exports `FlexBlockSchema`, built the way `ObjectChartBlockSchema` is: `BaseSchema`, the `flex` literal, the node-level `responsiveStyles`, and `properties`. `@objectstack/spec` has no `ComponentPropsMap['flex']` row, so the bag's `direction`, `justify`, `align`, `gap` and `wrap` are the flat `FlexSchema` mirror's own members: their values are judged exactly as before, now in the bag. A key `flex` does not declare stays unjudged in the bag by the tolerant face and is refused there by the strict face, as it was on the node.
22+
- The child list is `properties.children`: one node or a list. Each component in the list is judged once, at its own path (`properties.children.0…`), by the same page walk that judges a `page:` container's child list.
23+
- Each of the six props written flat on the node is refused on both faces, with a message that names its bag member (`Did you mean \`gap\` → \`properties.gap\`?`). `body` is refused toward `properties.children`.
24+
- `FlexBlockSchema` is the `flex` arm of `AnyComponentSchema`. `FlexSchema` leaves the union.
25+
26+
**The same release's changesets** that describe what the validators say about a `flex` written flat — `.changeset/10872-flat-arm-responsive-styles.md` (its example node writes `direction` flat), `.changeset/6751-flex-props-envelope-lift.md` (the keys lifted onto the node), `.changeset/8284-content-channel-per-component.md` (`body` refused toward `children`) and the remedy example in `.changeset/7926-page-node-refuses-actions.md` — describe the flat mirror, which still behaves as they say. On an authored node the props now live in the bag: `responsiveStyles` stays on the node as `.changeset/10872-flat-arm-responsive-styles.md` says, and its value checks hold there; the other props, the child list included, move into `properties`.
27+
28+
**What did not move.** The TypeScript `FlexSchema` and its zod mirror `FlexSchema` stay published and unchanged in shape. They are the node as the `flex` renderer reads it after the hoist, and as code composes it.

‎content/docs/components/layout/flex.mdx‎

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,46 @@ description: "Flexible box layout container"
99

1010
## Schema
1111

12+
A `flex` node takes its props — the child list included — in its `properties` bag, the
13+
spelling `@objectstack/spec`'s page component declares. `objectui validate` refuses a `flex`
14+
prop written on the node itself, by name, and points at its bag member
15+
(`properties.direction`, `properties.children`, …). `className` and the other base props stay
16+
on the node.
17+
18+
```json
19+
{
20+
"type": "flex",
21+
"className": "p-4",
22+
"properties": {
23+
"direction": "col",
24+
"gap": 4,
25+
"children": [
26+
{ "type": "text", "content": "First" },
27+
{ "type": "text", "content": "Second" }
28+
]
29+
}
30+
}
31+
```
32+
33+
The bag's members are `FlexLayoutProps` from `@object-ui/types`:
34+
1235
```ts
1336
import type { SchemaNode } from '@object-ui/types';
1437

15-
interface FlexSchema {
38+
interface FlexNode {
1639
type: 'flex';
17-
direction?: 'row' | 'col' | 'row-reverse' | 'col-reverse';
18-
gap?: number;
19-
align?: 'start' | 'center' | 'end';
20-
justify?: 'start' | 'center' | 'end' | 'between';
21-
wrap?: boolean;
22-
children: SchemaNode[];
2340
className?: string;
41+
properties?: {
42+
direction?: 'row' | 'col' | 'row-reverse' | 'col-reverse';
43+
gap?: number;
44+
align?: 'start' | 'end' | 'center' | 'baseline' | 'stretch';
45+
justify?: 'start' | 'end' | 'center' | 'between' | 'around' | 'evenly';
46+
wrap?: boolean;
47+
children?: SchemaNode[];
48+
};
2449
}
2550
```
51+
52+
Nothing changes at render time: `SchemaRenderer` hoists every `properties` key onto the node
53+
before the `flex` renderer reads it, so a stored node that still writes these props flat keeps
54+
rendering.

‎content/docs/guide/layout.md‎

Lines changed: 50 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -154,22 +154,24 @@ A `page` node has no action row of its own. Buttons are NODES, and they go in `c
154154
"children": [
155155
{
156156
"type": "flex",
157-
"justify": "end",
158-
"gap": 2,
159-
"children": [
160-
{
161-
"type": "button",
162-
"label": "Add Product",
163-
"variant": "default",
164-
"icon": "plus"
165-
},
166-
{
167-
"type": "button",
168-
"label": "Export",
169-
"variant": "outline",
170-
"icon": "download"
171-
}
172-
]
157+
"properties": {
158+
"justify": "end",
159+
"gap": 2,
160+
"children": [
161+
{
162+
"type": "button",
163+
"label": "Add Product",
164+
"variant": "default",
165+
"icon": "plus"
166+
},
167+
{
168+
"type": "button",
169+
"label": "Export",
170+
"variant": "outline",
171+
"icon": "download"
172+
}
173+
]
174+
}
173175
},
174176
{
175177
"type": "object-grid",
@@ -541,30 +543,32 @@ Same rule as above, and it governs the trail too: the breadcrumb and the buttons
541543
},
542544
{
543545
"type": "flex",
544-
"justify": "end",
545-
"gap": 2,
546-
"children": [
547-
{
548-
"type": "action:button",
549-
"properties": {
550-
"name": "edit_record",
551-
"label": "Edit",
552-
"variant": "default",
553-
"icon": "pencil",
554-
"actionType": "editRecord"
546+
"properties": {
547+
"justify": "end",
548+
"gap": 2,
549+
"children": [
550+
{
551+
"type": "action:button",
552+
"properties": {
553+
"name": "edit_record",
554+
"label": "Edit",
555+
"variant": "default",
556+
"icon": "pencil",
557+
"actionType": "editRecord"
558+
}
559+
},
560+
{
561+
"type": "action:button",
562+
"properties": {
563+
"name": "delete_record",
564+
"label": "Delete",
565+
"variant": "destructive",
566+
"icon": "trash",
567+
"actionType": "deleteRecord"
568+
}
555569
}
556-
},
557-
{
558-
"type": "action:button",
559-
"properties": {
560-
"name": "delete_record",
561-
"label": "Delete",
562-
"variant": "destructive",
563-
"icon": "trash",
564-
"actionType": "deleteRecord"
565-
}
566-
}
567-
]
570+
]
571+
}
568572
},
569573
{
570574
"type": "card",
@@ -726,11 +730,13 @@ Place primary actions in the first `children` node, so they sit above the conten
726730
"children": [
727731
{
728732
"type": "flex",
729-
"justify": "end",
730-
"gap": 2,
731-
"children": [
732-
{ "type": "button", "label": "New Order", "variant": "default" }
733-
]
733+
"properties": {
734+
"justify": "end",
735+
"gap": 2,
736+
"children": [
737+
{ "type": "button", "label": "New Order", "variant": "default" }
738+
]
739+
}
734740
}
735741
]
736742
}

‎examples/schema-catalog/src/schemas/actions/action-button-variants.json‎

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,35 @@
44
"children": [
55
{
66
"type": "flex",
7-
"gap": 2,
8-
"wrap": true,
9-
"children": [
10-
{
11-
"type": "button",
12-
"label": "Submit",
13-
"variant": "default",
14-
"icon": "send"
15-
},
16-
{
17-
"type": "button",
18-
"label": "Save Draft",
19-
"variant": "outline",
20-
"icon": "save"
21-
},
22-
{
23-
"type": "button",
24-
"label": "Delete",
25-
"variant": "destructive",
26-
"icon": "trash"
27-
},
28-
{
29-
"type": "button",
30-
"label": "Cancel",
31-
"variant": "ghost"
32-
}
33-
]
7+
"properties": {
8+
"gap": 2,
9+
"wrap": true,
10+
"children": [
11+
{
12+
"type": "button",
13+
"label": "Submit",
14+
"variant": "default",
15+
"icon": "send"
16+
},
17+
{
18+
"type": "button",
19+
"label": "Save Draft",
20+
"variant": "outline",
21+
"icon": "save"
22+
},
23+
{
24+
"type": "button",
25+
"label": "Delete",
26+
"variant": "destructive",
27+
"icon": "trash"
28+
},
29+
{
30+
"type": "button",
31+
"label": "Cancel",
32+
"variant": "ghost"
33+
}
34+
]
35+
}
3436
}
3537
]
3638
}

0 commit comments

Comments
 (0)