Skip to content

Commit 2d576e4

Browse files
fix(types,components)!: retire grid's flat smColumns / mdColumns / lgColumns / xlColumns; the breakpoint object is the one spelling (objectui#11505) (#11510)
Fixes #11505 Clause-②: no (narrowing) Dev run for objectui#11505 under the `domain:spec` seat's claim (comment `5961122544`), session `https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC`. It executes triage's ruling A (comment `5960494324`): the `grid` node's flat `smColumns` / `mdColumns` / `lgColumns` / `xlColumns` channel is retired, and the breakpoint object of `columns` is the one spelling. No alias, no dual spelling, no deprecation window. Note on spelling: GitHub strips tag-shaped text from bodies, so the SDUI node tag and placeholders below are written as words (GRID-TAG, N, C). ## The accept-set change, key by key | Surface | Before (base `fcdc8ec9`, measured) | After | |:--|:--|:--| | strict authoring face (`StrictAnyComponentSchema`) | each key refused at every value as `unrecognized_keys` at the node root | refused at the key's own path, `invalid_type`, with the RETIRED message naming `columns: { md: N }` (per key's breakpoint) | | tolerant face (`safeValidateSchema`, what `objectui validate` runs) | every value passed through `BaseSchema.passthrough()`, unexamined (`mdColumns: 'wide'` validated) | the same named refusal as the strict face | | TypeScript face (`GridSchema` in `packages/types/src/layout.ts`) | assignable as `any` through the `BaseSchema` index signature | `?: never` tombstone with a RETIRED docblock, so `tsc` refuses it by name | | `grid` registration inputs | `columns`, `smColumns`, `mdColumns`, `lgColumns`, `xlColumns`, `gap`, `className`, `children` | `columns`, `gap`, `className`, `children` | | `grid` registration `defaultProps` | `columns: 1`, `mdColumns: 2`, `lgColumns: 4`, `gap: 4` | `columns: { xs: 1, md: 2, lg: 4 }`, `gap: 4` | | renderer (`grid.tsx`) | read each key over the breakpoint object; any flat key switched off the bare count's mobile-first ramp | reads none of them; a document that reaches it unvalidated draws exactly what it draws without them; never forwarded to the DOM | | SDUI manifest (`validateTree` over the live registry) | `smColumns: 13` drew `invalid-enum` (registered input) | each key draws a `warning` / `unknown-prop`, "GRID-TAG has no prop" naming the key | Widening: none. Narrowing: the four keys, which the tolerant face accepted at any value. `objectui validate`'s printer, read through the CLI's own `validate` command on this branch (a one-off probe, not committed): `{ "type": "grid", "columns": 4, "mdColumns": 2 }` and `{ "type": "grid", "mdColumns": "wide" }` both exit 1 with `Path: mdColumns`, `Code: invalid_type` and the message `RETIRED (objectui#11505, ADR-0049) — mdColumns on grid was a second spelling of the md member of columns: … Instead: write the count, 1 to 12, as the md member of the breakpoint object, columns: { md: N }, and delete the key. A bare columns: C beside it becomes the object's xs: C; with no columns at all, add xs: 2 to keep the two columns the grid drew below md.` The migrated `{ "type": "grid", "columns": { "xs": 4, "md": 2 } }` exits 0. ## Zone 2 readings, measured before trusting them **H1, the seed renders identically: confirmed.** Through the real `SchemaRenderer`, the base registration's seed (`columns: 1`, `mdColumns: 2`, `lgColumns: 4`, `gap: 4`) drew `grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4`, and the new seed `columns: { xs: 1, md: 2, lg: 4 }` draws the same string. The pin row ("the seeded grid draws the classes the flat seed drew") is green on the base and on this change. The bare `1` never ramps (the ramp needs a count above 1), so the flat overrides had nothing to interact with here. The migration the refusal prescribes was measured the same way, flat document on the base against the object document on this branch: | Flat document (base) | Drew on base | Prescribed object | Draws now | |:--|:--|:--|:--| | `columns: 1, mdColumns: 2, lgColumns: 4` | `grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4` | `{ xs: 1, md: 2, lg: 4 }` | same | | `columns: 4, mdColumns: 2` | `grid grid-cols-4 md:grid-cols-2 gap-4` (ramp switched off) | `{ xs: 4, md: 2 }` | same | | `smColumns: 3` (no `columns`) | `grid grid-cols-2 sm:grid-cols-3 gap-4` | `{ xs: 2, sm: 3 }` | same | | `columns: { xs: 1, md: 3 }, mdColumns: 2, xlColumns: 6` | `grid grid-cols-1 md:grid-cols-2 xl:grid-cols-6 gap-4` | `{ xs: 1, md: 2, xl: 6 }` | same | | `columns: 2` and all four flat keys (1, 2, 3, 4) | `grid grid-cols-2 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4` | `{ xs: 2, sm: 1, md: 2, lg: 3, xl: 4 }` | same | This is why the refusal and the changeset say "a bare `columns: C` becomes `xs: C`" and "with no `columns`, add `xs: 2`": an object without `xs` draws one base column, while an absent `columns` drew two. **H2, refusal by name on the tolerant face: done as objectui#11465 did, both faces.** Zod: a local `retiredGridColumnsKey(key, breakpoint)` over the house `retirementTombstone`, message `RETIRED (objectui#11505, ADR-0049) — KEY on grid … Instead: …`. TypeScript: `?: never` plus a RETIRED docblock. Measured that the TS half is needed: with `packages/types/src/layout.ts` reverted to the base, `tsc -p tsconfig.test.json` in `packages/types` reports 10 errors (4 `Expect` rows, 4 unused `@ts-expect-error` rows, and 2 in `zod-mirror-parity.test.ts`, whose drift ledger catches a zod-only declaration), so without the tombstones `{ type: 'grid', mdColumns: 2 }` compiles. **H3, producers: the registration was the only one, re-measured.** Census of `\b(sm|md|lg|xl)Columns\b` over every tracked file, plus the lowercase attribute spellings: | Where | Base | After | |:--|:--|:--| | renderer reads (`schema.KEY` in `packages/*/src`) | 4, all in `grid.tsx` | 0 | | registration | 4 inputs and 2 seed keys in `grid.tsx` | 0 (comments only) | | PageDesigner | none: it seeds a new grid from its own `DEFAULT_PALETTE`, and no designer, shell or console code reads a registration's `inputs` or `defaultProps` (`ComponentRegistry.getConfig` / `getMeta` in `plugin-designer`, `app-shell`, `apps/console`: zero hits) | none | | `examples/schema-catalog` documents | none | none | | docs (`content/docs`, `skills/`, READMEs, `apps/site`) | none | two retirement sentences (`grid.mdx`, `schema-reference.md`) | | tests | `grid-dom-attribute-whitelist.test.tsx` (canary), `layout-spacing-sets-11474.test.tsx` (ledger), one prose line in the catalog's `grid-columns-key.test.tsx` | re-judged below, plus the two new pins | | scripts | one illustrative comment in `check-component-surface-parity.mjs` | re-pointed at a live intersection spelling | | objectstack checkout | only `sdui.manifest.json`, the recorded console manifest (see Acceptance notes) | unchanged here | No corpus document authored a flat key, so none needed fixing. ## Fixtures and documents re-judged - `grid-dom-attribute-whitelist.test.tsx` (objectui#4787): the canary keeps its four flat keys, re-labelled as retired keys a document that skipped validation still carries; they must still not reach the DOM. Case 1b no longer clears them (they no longer override the object). Case 2's positive control now expects `md:grid-cols-4` (the bare count's ramp) and not `md:grid-cols-2`: the flat `mdColumns: 2` is read by nothing. - `layout-spacing-sets-11474.test.tsx`: `REGISTERED_NOT_DECLARED` is empty, and its two-way population row now holds it empty; the four `grid.*Columns` positions leave the enumeration because the inputs are gone. - `examples/schema-catalog/test/grid-columns-key.test.tsx`: its docblock said the registration exposes the five column inputs; it now says `columns` alone. - `content/docs/components/layout/grid.mdx`, `content/docs/api/schema-reference.md`: one sentence each naming the retirement and the object form. - `scripts/check-component-surface-parity.mjs`: the comment that illustrated the intersection-typed `schema` parameter with the grid's flat-key intersection now cites `UIActionSchema & { actionType?: string }` (`action-button.tsx`), a live one. Comment only; the script's own suite ran (below). ## Pins - `packages/types/src/__tests__/grid-flat-columns-retired-11505.test.ts`: type level, each key reads `undefined` on both faces and a `@ts-expect-error` per key; runtime, every former value refused at the key's own path on the strict face, the tolerant face and the mirror; the message is the RETIRED one with `Instead:` and `columns: { BP: N }`; the migrated documents parse on both faces; lit controls (the node without the keys parses everywhere, an undeclared key stays green on the tolerant face and is `unrecognized_keys` on the strict one). - `packages/components/src/__tests__/grid-flat-columns-retired-11505.test.tsx`: the registration's only column input is `columns`; the seed is the breakpoint object with no flat key; the seeded grid draws `grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4`; each flat key is read by nothing (five shapes); each prescribed object draws what its flat form drew on the base; the SDUI manifest answers each key with `unknown-prop`, and the object form with nothing; a flat key no longer switches the ramp off. ## Reverse verification (committed fix, base restored, then HEAD restored) From the committed head `4d08cf87`, `grid.tsx`, `zod/layout.zod.ts` and `layout.ts` were checked out from the base `fcdc8ec9` (on-disk check: `retiredGridColumnsKey` 0 hits, the `if (schema.mdColumns)` read 1 hit, `mdColumns?: never` 0 hits) and the four pin files run. Vitest resolves `@object-ui/*` to `src` through the root config's aliases, so no rebuild was involved. Result: `Tests 27 failed | 26 passed (53)`, exit 1. Red: every flat-key row in both new pins (registration, seed shape, read-by-nothing, the manifest rows, the ramp row, the zod refusals), the enumeration ledger row plus the four `grid.*Columns` positions, and whitelist cases 1b and 2. Green on the base, as intended: the seeded-classes row, every migrated-object row, the manifest's object-form control and the lit controls. Restore: `git checkout HEAD --` on the three paths, proved by blob hashes equal to HEAD's and an empty `git diff HEAD`, under an EXIT/INT/TERM trap. ## Pending changesets swept (read as text against what this diff moves) Each gets a dated, append-only note in the house form: - `.changeset/11491-grid-columns-set.md`: its components bullet named the four inputs and `validateTree` answering `smColumns: 13` with `invalid-enum`. - `.changeset/7097-grid-2xl-breakpoint.md`: "the designer's flat `smColumns`…`xlColumns` channel are unchanged". - `.changeset/8516-8556-mirror-partial-record-narrowing.md` (the card's rider): it says the CLI reports `{ xxl: 6 }` as `Path: columns → xxl`, `Code: invalid_key`. Measured through the CLI's printer on this branch, it reports `Unrecognized key: "xxl"`, `Path: columns`, `Code: unrecognized_keys`, a top-level issue. The note attributes the change to objectui#11073's zod 4.6 resolution, as the rider does; this run measured the present answer, not the transition. - `.changeset/11441-known-types-retired-layout-keys.md` (the optional rider): its migration line names `gap` with no objectui#11474 note; the note mirrors its sibling's. Every `ResponsiveGrid` gap step (0 to 6 and 8, read off `GAP_CLASSES`) is one of `grid`'s ten. Left alone, each still true as written: - `grid-dom-attribute-whitelist-4787.md`: its `mdcolumns` / `smcolumns` are a leak measurement taken at that change. - `8505-grid-columns-breakpoint-narrowing.md`: it is about the six keys of the `columns` object. - `11441-retire-nav-responsive-grid-layout.md`: it already carries the objectui#11474 and objectui#11491 notes, and the `columns` object it names is unchanged. - `11474-layout-spacing-sets.md`: `gap` only. - `7735-zod-mirrors-stop-authoring-defaults.md`: the `grid.columns` default row is still `let baseCols = 2`. - `3965-mint-box-neutral-container.md`, `9910-children-slot-declare-and-pin.md`, `6740-flex-is-container.md`, `layout-renderers-dom-props-5574.md`, `dom-leak-sweep-components-5574.md`: grid containment, DOM whitelist and the object spelling, which are unaffected. New entry: `.changeset/11505-grid-flat-columns-retired.md`, `minor` for `@object-ui/types` and `@object-ui/components`, with the breaking meaning and the migration stated. ## Gates, all at `4d08cf87` | Gate | Verdict | |:--|:--| | `pnpm --filter '@object-ui/components^...' build` (closure, includes `types`) | exit 0 | | `pnpm --filter @object-ui/types run type-check` (three legs; new pin confirmed in the test program with `--listFilesOnly`) | exit 0 | | `pnpm --filter @object-ui/components run type-check` (both new or edited pins confirmed in the program) | exit 0 | | `pnpm --filter @object-ui/example-schema-catalog run type-check` | exit 0 | | `pnpm exec vitest run packages/types/` | 342 files, 9123 tests passed | | `pnpm exec vitest run packages/components/` | 355 passed, 1 skipped (a pre-existing time-zone `skipIf`), 3612 tests passed, 24 skipped | | radius: `examples/schema-catalog/`, `packages/cli/`, `packages/sdui-parser/`, `scripts/__tests__/check-component-surface-parity.test.ts`, `check-doc-component-types.test.ts`, `check-doc-snippet-types.test.ts`, core `schema-validator.test.ts`, react `SchemaRenderer.dataPropArm-9571.test.tsx`, console `registry-inputs-spec-parity.test.ts` | 91 files, 3486 tests passed | | `pnpm check:doc-snippets` (closure built with its own `--build-filter`) | exit 0, 776 of 776 blocks judged, 0 failed | | `pnpm check:doc-examples` | exit 0 | | `pnpm check:doc-types`, `pnpm check:doc-fences` | exit 0 each | | `pnpm check:component-surface-parity` (report-only) | exit 0; `ui:grid` reads, interface and inputs now agree: `columns`, `gap`, `children` (plus `style` / `className`) | | `node scripts/check-changeset-no-major.mjs`, `check-changeset-presence.mjs`, `check-changeset-fixed.mjs` | exit 0 each | | `pnpm check:changeset-claims`, `pnpm check:pending-changeset-literals` | exit 0 each | | `pnpm check:new-line-citations`, `pnpm check:control-bytes` | exit 0 each | | eslint, narrowed to the 9 touched code files (`--no-inline-config --format json`) | 9 files, 0 errors, 1 warning (`no-explicit-any` on the renderer's props type, present at the same line on the base) | | `pnpm check:sdui-registration-pins` | NOT MEASURED: exit 2, no console build on disk. The gate pins registration KEYS a `sideEffects` array keeps at bundle time; this diff moves no array and no key, only `grid`'s inputs and seed. Left to CI. | The eslint narrowing: the population is the 9 touched `.ts` / `.tsx` / `.mjs` files (eslint's own JSON output counts 9); the root `eslint.config.js` is the only config and enables no type-aware linting (`projectService` / `parserOptions.project` absent), so this diff cannot move any untouched file's verdict. The repo-wide lint is CI's. ## Deviations - Node: the container's v22.22.0 is below jsdom's engine floor, so a nodejs.org v22.23.3 tarball (SHA256 checked against `SHASUMS256.txt`) went on `PATH` from this run's scratch directory. Nothing system-wide. ## Acceptance notes - objectstack's tracked `sdui.manifest.json` (the recorded console manifest the CLI and lint validate against) still lists `smColumns` … `xlColumns` on `grid`, as `type: number`, so it predates objectui#11491's enums too. It is re-recorded when objectstack bumps its objectui console pin; that bump carries this change there. Carrier: the next objectstack console-pin bump. - Two comments kept on purpose: `grid.tsx`'s whitelist comment and whitelist case 3 quote the `mdcolumns` / `smcolumns` attributes as measured under objectui#4787. Those are historical readings, not claims about today. --- _Generated by [Claude Code](https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 37268aa commit 2d576e4

16 files changed

Lines changed: 528 additions & 87 deletions

‎.changeset/11441-known-types-retired-layout-keys.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@ The registered-types ratchet (`REFUSED_AT_TYPE`) falls from 20 to 18, and its na
1616
**Clause-②: yes**, released as `minor` with this banner.
1717

1818
⚠️ **Dated note, 2026-10-02 — `grid` takes column counts 1 to 12, not any number — objectui#11491.** At this change "the same breakpoint `columns` object" carried any count; now each count in a `grid`'s `columns` is one of 1 to 12, the counts the `grid` renderer maps, and `objectui validate` refuses any other with that set named. Every count `ResponsiveGrid` drew (1, 2, 3, 4, 6 and 12) is one of them, so the migration above holds for every `responsive-grid` that drew its columns. `.changeset/11491-grid-columns-set.md` states what ships. The rest of this entry is kept as the reading of this change.
19+
20+
⚠️ **Dated note, 2026-10-02 — `grid` takes one of ten `gap` steps, not any number — objectui#11474.** At this change "the same breakpoint `columns` object and `gap`" carried any `gap` number; now `grid`'s `gap` is one of 0, 1, 2, 3, 4, 5, 6, 8, 10 and 12, the steps the `grid` renderer maps, and `objectui validate` refuses any other with that set named. Every step `ResponsiveGrid`'s own class map drew (0 to 6 and 8) is one of them, so the migration above holds for every `responsive-grid` whose `gap` drew a class. `.changeset/11474-layout-spacing-sets.md` states what ships. The rest of this entry is kept as the reading of this change.

‎.changeset/11491-grid-columns-set.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,5 @@ nobody authored.
4040

4141
Migration: replace the count with the one you meant, from 1 to 12. For a grid with no columns
4242
at a breakpoint, leave that breakpoint out; for a single column, write `1`.
43+
44+
⚠️ **Dated note, 2026-10-02 — `grid` has no flat column inputs any more — objectui#11505.** At this change the `grid` registration published `smColumns`, `mdColumns`, `lgColumns` and `xlColumns` as closed enums of the twelve counts, and `validateTree` answered `smColumns: 13` with `invalid-enum`. Now the registration publishes `columns` as its only column input, the renderer no longer reads the four keys, both zod faces refuse them by name, and `validateTree` answers each with an `unknown-prop` warning. A per-breakpoint count is a member of the `columns` object, which this entry's closed set still governs. `.changeset/11505-grid-flat-columns-retired.md` states what ships. The rest of this entry is kept as the reading of this change.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@object-ui/types': minor
3+
'@object-ui/components': minor
4+
---
5+
6+
A `grid` node sets a column count per breakpoint in one way: the breakpoint object of `columns`.
7+
The flat `smColumns`, `mdColumns`, `lgColumns` and `xlColumns` keys are retired, with no alias
8+
and no deprecation window (objectui#11505).
9+
10+
**Breaking for a `grid` that carries a flat column key.** The `grid` registration offered the four
11+
keys as inputs and seeded two of them, and the renderer read each one over the breakpoint object.
12+
No face of `GridSchema` declared them: the strict authoring face refused each key at every value,
13+
and `safeValidateSchema` (what `objectui validate` runs) passed any value through unexamined, so
14+
`"mdColumns": "wide"` validated.
15+
16+
- `@object-ui/types`: `GridSchema` declares the four keys as refusals on both faces. On the
17+
TypeScript face each is a `?: never` tombstone, so `tsc` refuses it by name. The zod mirror
18+
refuses every value at the key's own path with a message that names the breakpoint object member
19+
to write instead (`columns: { md: N }`), on `safeValidateSchema` and on the strict authoring face
20+
alike. The strict face answered `unrecognized_keys` before; it now gives that named refusal.
21+
- `@object-ui/components`: the `grid` registration publishes `columns` as its only column input,
22+
and seeds `columns: { xs: 1, md: 2, lg: 4 }` in place of `columns: 1`, `mdColumns: 2` and
23+
`lgColumns: 4`. The seeded grid draws the same classes as before,
24+
`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4`. The renderer no longer reads the four
25+
keys: a document that reaches it unvalidated draws exactly what it draws without them, and the
26+
keys are never forwarded to the DOM. In the SDUI manifest, `validateTree` answers each with an
27+
`unknown-prop` warning, where `smColumns: 13` drew `invalid-enum` before.
28+
29+
Migration: move each count into the `columns` object under its breakpoint. A bare `columns: C`
30+
beside a flat key becomes the object's `xs: C`, because a flat key switched off the bare count's
31+
mobile-first ramp. With no `columns` at all, add `xs: 2` to keep the two columns the grid drew
32+
below that breakpoint. So `{ "columns": 1, "mdColumns": 2, "lgColumns": 4 }` becomes
33+
`{ "columns": { "xs": 1, "md": 2, "lg": 4 } }`, and `{ "smColumns": 3 }` becomes
34+
`{ "columns": { "xs": 2, "sm": 3 } }`. Each migrated form draws the classes its flat form drew.

‎.changeset/7097-grid-2xl-breakpoint.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,5 @@ compiled stylesheet, with the twelve `xl:grid-cols-*` rules unchanged as the con
3131
Nothing that rendered before renders differently: the other five tiers, the bare-number
3232
mobile-first ramp, and the designer's flat `smColumns`…`xlColumns` channel are
3333
unchanged.
34+
35+
⚠️ **Dated note, 2026-10-02 — the flat column channel is retired — objectui#11505.** At this change the designer's flat `smColumns`…`xlColumns` channel was unchanged, as the last paragraph says. Now it is gone: the `grid` registration no longer offers the four keys, the renderer no longer reads them, and both zod faces refuse them by name. A per-breakpoint count, `2xl` included, is a member of the `columns` object. `.changeset/11505-grid-flat-columns-retired.md` states what ships. The rest of this entry is kept as the reading of this change.

‎.changeset/8516-8556-mirror-partial-record-narrowing.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,5 @@ measurement is pinned executably in
6262
`__tests__/mirror-partial-record-narrowing-8516.test.ts`, at compile time (the
6363
inferred map is `Partial<Record<…>>`, not `Record<…>`) and at run time (one
6464
accepting row per member), so it cannot rot into folklore.
65+
66+
⚠️ **Dated note, 2026-10-02 — the CLI reports `{ xxl: 6 }` as `unrecognized_keys` at `columns` — objectui#11073.** The Migration paragraph says the refusal reads `Path: columns → xxl`, `Code: invalid_key`, through `@object-ui/cli`'s union-arm expansion. Measured through `objectui validate`'s printer on objectui#11505's branch, it now reads `1. Unrecognized key: "xxl"`, `Path: columns`, `Code: unrecognized_keys`, as a top-level issue with no union-arm line under it. The verdict is unchanged: `{ xxl: 6 }` is refused, and the message still names `xxl`. The change of code is attributed to objectui#11073's zod 4.6 resolution (the rider on objectui#11505 names it); this note measured the present answer, not the transition. The rest of this entry is kept as the reading of this change.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ A responsive grid layout. Columns can be a fixed number or responsive breakpoint
246246

247247
| Property | Type | Description |
248248
|----------|------|-------------|
249-
| `columns` | `ColumnCount \| Partial<Record<BreakpointName, ColumnCount>>` | A column count from 1 to 12 (`ColumnCount`), or a responsive map of such counts keyed by breakpoint (`xs`, `sm`, `md`, `lg`, `xl`, `2xl`), e.g. `{ sm: 1, md: 2, lg: 3 }`. Any other count is refused with the set named (objectui#11491). |
249+
| `columns` | `ColumnCount \| Partial<Record<BreakpointName, ColumnCount>>` | A column count from 1 to 12 (`ColumnCount`), or a responsive map of such counts keyed by breakpoint (`xs`, `sm`, `md`, `lg`, `xl`, `2xl`), e.g. `{ sm: 1, md: 2, lg: 3 }`. Any other count is refused with the set named (objectui#11491). The flat `smColumns` / `mdColumns` / `lgColumns` / `xlColumns` keys are retired and refused by name; the breakpoint map is the one spelling (objectui#11505). |
250250
| `gap` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 8 \| 10 \| 12` | Gap step between grid items; `0` is none (default `4`). Any other number is refused with the set named (objectui#11474). |
251251
| `children` | `SchemaNode \| SchemaNode[]` | Grid items. |
252252

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ count used to draw no column class where it was authored: `13` lost its `md` cou
3131
two columns. A bare count above 1 is one column on a phone and that count from `md` up; an object
3232
keeps full control of every breakpoint.
3333

34+
The object is the only way to set a count per breakpoint. The flat `smColumns`, `mdColumns`,
35+
`lgColumns` and `xlColumns` keys are retired and refused by name (objectui#11505): write the count
36+
as that breakpoint's member, so `"columns": 1, "mdColumns": 2` becomes `"columns": { "xs": 1, "md": 2 }`.
37+
3438
`gap` is a step on the grid's spacing scale, and `0` means none. The ten steps are the ones
3539
the renderer maps to a gap class, so they are the only values validation accepts: `"gap": 9`
3640
or `"gap": 16` is refused with the set named (objectui#11474). For such a number the renderer

‎examples/schema-catalog/test/grid-columns-key.test.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
* never `cols` (objectui#4001).
44
*
55
* `GridSchema` declares `columns` (`packages/types/src/layout.ts`), the `grid`
6-
* renderer reads only `schema.columns`, and the registered designer `inputs`
7-
* expose only `columns` / `smColumns` / `mdColumns` / `lgColumns` / `xlColumns`.
6+
* renderer reads only `schema.columns`, and the registered `inputs` expose only
7+
* `columns` as a column key: its breakpoint object is the one per-breakpoint
8+
* spelling, since objectui#11505 retired the flat `smColumns` … `xlColumns`.
89
* `cols` was never declared by the spec, the types, or the registry — nothing in
910
* `core` / `react` / `types` normalizes it. Thirteen catalog examples spelled it
1011
* anyway, so the engine dropped the value on the floor: `baseCols` fell back to

‎packages/components/src/__tests__/grid-dom-attribute-whitelist.test.tsx‎

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,19 @@ const isAllowedAttribute = (name: string): boolean =>
6767

6868
/**
6969
* A grid node carrying, deliberately, one of every category the leak drew from:
70-
* the renderer's own vocabulary in both `columns` spellings, SDUI node metadata,
71-
* the `props` container, an unknown authored key, and the legitimately forwarded
72-
* DOM channels.
70+
* the renderer's own vocabulary, the four flat column keys it used to read,
71+
* SDUI node metadata, the `props` container, an unknown authored key, and the
72+
* legitimately forwarded DOM channels.
7373
*/
7474
const CANARY_NODE = {
7575
type: 'grid',
7676
// Renderer vocabulary — CONSUMED off `schema`, never forwarded.
7777
columns: 4,
7878
gap: 4,
79+
// The flat column keys, retired by objectui#11505: both zod faces refuse
80+
// them by name, and the renderer no longer reads them. They stay in the
81+
// canary because a document that skipped validation still carries them, and
82+
// they must reach the DOM no more than any other key the renderer ignores.
7983
smColumns: 2,
8084
mdColumns: 2,
8185
lgColumns: 3,
@@ -131,12 +135,11 @@ describe('ui:grid — schema keys never reach the DOM (objectui#4787)', () => {
131135
});
132136

133137
it('case 1b: a responsive `columns` OBJECT does not become columns="[object Object]"', () => {
134-
// The flat `smColumns`/`mdColumns`/… keys deliberately OVERRIDE the object form
135-
// in this renderer, so they are cleared here to exercise the object path itself.
136-
const el = renderCanary({
137-
columns: { xs: 1, md: 3 },
138-
smColumns: undefined, mdColumns: undefined, lgColumns: undefined, xlColumns: undefined,
139-
});
138+
// The canary's flat `smColumns`/`mdColumns`/… keys are read by nothing since
139+
// objectui#11505, so they stay on the node: the object path drives the
140+
// classes alone. Until then they OVERRODE the object form, and this case
141+
// had to clear them.
142+
const el = renderCanary({ columns: { xs: 1, md: 3 } });
140143

141144
expect(el.hasAttribute('columns')).toBe(false);
142145
expect(el.outerHTML).not.toContain('[object Object]');
@@ -154,9 +157,13 @@ describe('ui:grid — schema keys never reach the DOM (objectui#4787)', () => {
154157

155158
// Computed layout classes — the renderer's real output, merged with the
156159
// authored className. A filter that dropped `className` would fail here.
160+
// The bare `columns: 4` draws its mobile-first ramp: the canary's flat
161+
// `mdColumns: 2` is read by nothing (objectui#11505), so `md` is the bare
162+
// count, not the flat one.
157163
expect(el.className).toContain('grid');
158164
expect(el.className).toContain('sm:grid-cols-2');
159-
expect(el.className).toContain('md:grid-cols-2');
165+
expect(el.className).toContain('md:grid-cols-4');
166+
expect(el.className).not.toContain('md:grid-cols-2');
160167
expect(el.className).toContain('gap-4');
161168
expect(el.className).toContain('authored-class');
162169

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* objectui#11505 — the `grid` registration and renderer drop the flat
11+
* per-breakpoint column channel: no `smColumns` / `mdColumns` / `lgColumns` /
12+
* `xlColumns` input, no read, and a seed in the breakpoint object, the one
13+
* spelling.
14+
*
15+
* ## What was split
16+
*
17+
* The registration offered the four keys as inputs and seeded two of them
18+
* (`columns: 1, mdColumns: 2, lgColumns: 4`); the renderer read each one OVER
19+
* the breakpoint object and let it switch off the bare count's mobile-first
20+
* ramp. No face of `GridSchema` declared them. Triage's ruling A retired the
21+
* channel; the declaration half (both faces refuse the keys by name) is pinned
22+
* in `types/src/__tests__/grid-flat-columns-retired-11505.test.ts`.
23+
*
24+
* ## The pins, through the real `SchemaRenderer`
25+
*
26+
* - The registration publishes `columns` as its only column input.
27+
* - The seed is the breakpoint object, with no flat key, and draws the classes
28+
* the flat seed drew. That class list was read off the flat seed on this
29+
* card's base, through this file's own `classesOf`; this row is green on the
30+
* base and on the change, which is the measurement that the seed moved and
31+
* the rendering did not.
32+
* - Each flat key is read by nothing: a document carrying one draws exactly
33+
* what it draws without it.
34+
* - The migration the zod refusal prescribes draws what the flat document drew
35+
* on the base, row by row: a bare `columns: C` beside a flat key becomes the
36+
* object's `xs: C`, and with no `columns` the object takes `xs: 2`.
37+
* - The SDUI manifest built from the live registry answers a flat key the way
38+
* it answers any prop the node does not declare, `unknown-prop`; it answered
39+
* `smColumns: 13` with `invalid-enum` while the input was registered.
40+
*
41+
* Module-scope import of the renderers, not `beforeAll` (AGENTS.md §测试纪律).
42+
*/
43+
import { describe, it, expect } from 'vitest';
44+
import { render } from '@testing-library/react';
45+
import '../renderers';
46+
import { SchemaRenderer } from '@object-ui/react';
47+
import { ComponentRegistry } from '@object-ui/core';
48+
import { manifestFromConfigs, validateTree } from '@object-ui/sdui-parser';
49+
import type { Diagnostic, SchemaElement } from '@object-ui/sdui-parser';
50+
51+
const FLAT_KEYS = ['smColumns', 'mdColumns', 'lgColumns', 'xlColumns'] as const;
52+
53+
/** The root element's classes for a `grid` document, as rendered. */
54+
function classesOf(doc: Record<string, unknown>): string {
55+
const { container, unmount } = render(<SchemaRenderer schema={{ type: 'grid', children: [], ...doc } as never} />);
56+
const className = (container.firstElementChild as HTMLElement | null)?.className ?? '';
57+
unmount();
58+
return className;
59+
}
60+
61+
const withoutFlatKeys = (doc: Record<string, unknown>) =>
62+
Object.fromEntries(Object.entries(doc).filter(([key]) => !(FLAT_KEYS as readonly string[]).includes(key)));
63+
64+
const config = ComponentRegistry.getConfig('grid');
65+
66+
/**
67+
* The manifest the running app validates against, keyed by every known
68+
* registry tag, the way `layout-containers-declare-containment.test.tsx`
69+
* builds it (the app's own builder in `renderers/layout/page.tsx` is
70+
* module-private).
71+
*/
72+
const diagnose = (schema: Record<string, unknown>): Diagnostic[] => {
73+
const configs = ComponentRegistry.getKnownTypes().map((t) => {
74+
const meta = ComponentRegistry.getMeta(t);
75+
return { type: t, namespace: meta?.namespace, isContainer: meta?.isContainer, inputs: meta?.inputs };
76+
});
77+
const manifest = manifestFromConfigs(configs as unknown as Parameters<typeof manifestFromConfigs>[0]);
78+
return validateTree(schema as SchemaElement, manifest).diagnostics;
79+
};
80+
81+
/**
82+
* The class list the registration's flat seed (`columns: 1`, `mdColumns: 2`,
83+
* `lgColumns: 4`, `gap: 4`) drew on this card's base, measured through
84+
* {@link classesOf}.
85+
*/
86+
const SEED_CLASSES = 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4';
87+
88+
/**
89+
* Flat documents, the object document the refusal prescribes for each, and
90+
* the classes the FLAT document drew on this card's base (read through
91+
* {@link classesOf} there). Each row is one migration shape.
92+
*/
93+
const MIGRATIONS: ReadonlyArray<readonly [string, Record<string, unknown>, Record<string, unknown>, string]> = [
94+
[
95+
'the old seed: a bare count with two flat keys',
96+
{ columns: 1, mdColumns: 2, lgColumns: 4 },
97+
{ columns: { xs: 1, md: 2, lg: 4 } },
98+
'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4',
99+
],
100+
[
101+
'a bare count above 1: the flat key switched its ramp off',
102+
{ columns: 4, mdColumns: 2 },
103+
{ columns: { xs: 4, md: 2 } },
104+
'grid grid-cols-4 md:grid-cols-2 gap-4',
105+
],
106+
[
107+
'no `columns` at all: the grid drew its default two below the flat key',
108+
{ smColumns: 3 },
109+
{ columns: { xs: 2, sm: 3 } },
110+
'grid grid-cols-2 sm:grid-cols-3 gap-4',
111+
],
112+
[
113+
'a breakpoint object: the flat key won at its breakpoint',
114+
{ columns: { xs: 1, md: 3 }, mdColumns: 2, xlColumns: 6 },
115+
{ columns: { xs: 1, md: 2, xl: 6 } },
116+
'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-6 gap-4',
117+
],
118+
[
119+
'all four flat keys beside a bare count',
120+
{ columns: 2, smColumns: 1, mdColumns: 2, lgColumns: 3, xlColumns: 4 },
121+
{ columns: { xs: 2, sm: 1, md: 2, lg: 3, xl: 4 } },
122+
'grid grid-cols-2 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4',
123+
],
124+
];
125+
126+
describe('`grid` reads and publishes no flat column key (objectui#11505)', () => {
127+
it('the registration publishes `columns` as its one column input', () => {
128+
expect(config, 'the `grid` registration').toBeDefined();
129+
const names = (config!.inputs ?? []).map((input) => input.name);
130+
expect(names.filter((name) => /columns$/i.test(name))).toEqual(['columns']);
131+
// Non-vacuity: the inputs were read, and the live neighbours are there.
132+
expect(names).toEqual(expect.arrayContaining(['columns', 'gap', 'children']));
133+
});
134+
135+
it('the seed is the breakpoint object, with no flat key', () => {
136+
const seed = (config!.defaultProps ?? {}) as Record<string, unknown>;
137+
expect(Object.keys(seed).filter((key) => /columns$/i.test(key))).toEqual(['columns']);
138+
expect(seed.columns).toEqual({ xs: 1, md: 2, lg: 4 });
139+
});
140+
141+
it('the seeded grid draws the classes the flat seed drew', () => {
142+
const seed = (config!.defaultProps ?? {}) as Record<string, unknown>;
143+
expect(classesOf(seed)).toBe(SEED_CLASSES);
144+
});
145+
146+
it.each(MIGRATIONS)('%s — each flat key is read by nothing', (_label, flat) => {
147+
expect(classesOf(flat)).toBe(classesOf(withoutFlatKeys(flat)));
148+
});
149+
150+
it.each(MIGRATIONS)('%s — the prescribed object draws what the flat document drew', (_label, _flat, migrated, drew) => {
151+
expect(classesOf(migrated)).toBe(drew);
152+
});
153+
154+
it.each(FLAT_KEYS)('the SDUI manifest answers `%s` as a prop `grid` does not have', (key) => {
155+
expect(diagnose({ type: 'grid', [key]: 2 }).map((d) => [d.severity, d.code, d.message])).toEqual([
156+
['warning', 'unknown-prop', `<grid> has no prop "${key}"`],
157+
]);
158+
});
159+
160+
it('the SDUI manifest takes the breakpoint object those keys spelled a second time', () => {
161+
// Lit control for the row above: the same node, in the one spelling.
162+
expect(diagnose({ type: 'grid', columns: { xs: 1, sm: 2, md: 2, lg: 4, xl: 6 } })).toEqual([]);
163+
});
164+
165+
it('a flat key no longer switches the bare count\'s ramp off', () => {
166+
// Lit control for the rows above: the ramp is what a bare `4` draws.
167+
expect(classesOf({ columns: 4 })).toBe('grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4');
168+
expect(classesOf({ columns: 4, mdColumns: 2 })).toBe('grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4');
169+
});
170+
});

0 commit comments

Comments
 (0)