Skip to content

Commit 4abc0aa

Browse files
fix(types,components,core)!: stack, flex and grid gap are each one of their renderer's steps, on both faces and the registrations; one enumeration pin for every layout spacing key (objectui#11474) (#11489)
Fixes #11474 Clause-②: no Why `no` (a narrowing): `stack.gap`, `flex.gap` and `grid.gap` shrink from every number to each renderer's mapped set, and the `flex` / `grid` describes stop advertising "0-8". Nothing widens. The changeset is `minor` and states the breaking authoring meaning, because objectui never declares `major` (AGENTS.md section 9, version alignment). Dispatched by the `domain:spec @ objectui` seat (objectui#10217) under triage's family direction (comment `5955082167`) and the seat's claim (comment `5956674102`), session `https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC`. This is the family's last step after objectui#10286 (`container.maxWidth`) and objectui#11424 (`container.padding`). ## The accept-set change | surface | before (base `50c73fed0`) | after (head `1db960ca`) | |---|---|---| | zod `StackSchema.gap` | `z.number()` | literal set 0, 1, 2, 3, 4, 5, 6, 8, 10; the message lists the set | | zod `FlexSchema.gap`, and the authored bag (`properties.gap`, the same schema object by reference) | `z.number()`, describe "Tailwind scale 0-8" | literal set 0 to 8; describe "Gap step, one of 0, 1, 2, 3, 4, 5, 6, 7, 8; 0 is none (default 2)" | | zod `GridSchema.gap` | `z.number()`, describe "Tailwind scale 0-8" | literal set 0, 1, 2, 3, 4, 5, 6, 8, 10, 12 | | TS `FlexLayoutProps.gap` (shared by `FlexSchema` and the bag type `FlexBlockNode`) | `number` | literal union 0 to 8, `@default 2` kept | | TS `StackSchema.gap` | `number`, inherited from `FlexLayoutProps` | its own literal union 0 to 6, 8, 10; the heritage clause is `Omit` of `gap` over `FlexLayoutProps` (no index signature there, so no member is erased; `stack-schema-emitted-members.test.ts` measures the emitted declaration and stays green) | | TS `GridSchema.gap` | `number` | literal union of the ten grid steps | | `stack` / `flex` / `grid` registration, `gap` input | `type: 'number'` | `type: 'enum'`, `{ label, value }` entries with numeric values, in `container.padding`'s object form | | `GridBuilder.gap()` / `FlexBuilder.gap()` in `@object-ui/core` | `number` | the declared set (`NonNullable` of the member) | | renderers | `gap === N` branches; `grid`'s `GAPS` map and its runtime-built fallback | unchanged; comments only at the registration inputs. Nothing rounds or clamps. | `container.padding` moved onto the same shared helper (`rendererSpacingSteps` in `layout.zod.ts`, the dispatch's suggested route: the spelling now repeats four times). Its refusal text is byte-identical to base (compared against the built `dist`), and its describe is unchanged. The three refusals as stored (read from the built `dist` at head): - stack: "`gap` on a `stack` is one of 0, 1, 2, 3, 4, 5, 6, 8, 10 (objectui#11474): those are the steps the renderer maps to a gap class, and `0` means none. Any other number drew NO gap class at all, not even the default `2`, so it is refused here rather than rendered with no gap. Pick the step you meant from that set." - flex: the same sentence with the set 0 to 8. - grid: "... Any other number built a class at runtime that no compiled stylesheet defines, so the grid rendered with no gap at all, not even the default `4`; it is refused here instead. ..." ## Measurements (the dispatch's zone 2) **H1, the three sets, derived by rendering** (a throwaway probe through the real `SchemaRenderer` and registry at base `50c73fed0`, candidates 0 to 32 plus -1, 0.5, 1.5, 2.5, 9.5): `stack` draws a gap class for 0 to 6, 8, 10 and none for 7, 9, 11 to 32 or any fraction. `flex` (flat or bag) draws one for 0 to 8 and none for 9 upward. `grid` draws `gap-0` to `gap-6`, `gap-8`, `gap-10`, `gap-12` from its map and a runtime-built `gap-[N*0.25rem]` for everything else (`gap-[2.25rem]` for 9, `gap-[-0.25rem]` for -1). At base, every candidate on all four spellings parsed green on `safeValidateSchema` and on `StrictAnyComponentSchema`. H1 holds as the dispatch stated it. **H3, `grid.gap` measured first: closed.** Instrument: both Tailwind entry sheets compiled exactly as their builds compile them (`postcss` plus `@tailwindcss/postcss`, `from` set to the entry so its `@source` lines resolve): `apps/console/src/index.css` (482,775 bytes) and `packages/components/src/index.css`, the sheet the package ships as `style.css` (158,414 bytes). Control: every mapped grid class (`gap-0` ... `gap-12`), `md:gap-10` and `gap-1.5` are present in both sheets. The arbitrary-value lookup is live: `max-w-[16rem]` and `px-[0.3rem]` are found in both by the same matcher. Reading: all 28 runtime-built classes (for 7, 9, 11, 13 to 32, -1, 0.5, 1.5, 2.5, 9.5) are absent from both. So an unmapped grid gap reaches no rule, and `grid.gap` closes like the other two. The pin re-derives this against the package's own sheet on every run. The console reading was taken once and nothing re-derives it. **H2, the flex bag.** Closing `FlexSchema.gap` flowed into the bag and into `FlexBlockNode` with no edit there. `{ type: 'flex', properties: { gap: 9 } }` is refused at `properties.gap` on both faces with the set in the issue. `{ type: 'flex', gap: 9 }` stays refused by name toward `properties.gap`, as every flat `flex` prop already was. `tsc` refuses `{ type: 'flex', properties: { gap: 9 } }` typed as `FlexBlockNode` (a compile-time pin under the package's `type-check`). **H4, the enumeration.** Derived by the new pin: every registration in `@object-ui/components` with `isContainer`, and every input on it whose values are numbers. It finds `aspect-ratio.ratio`, `container.padding`, `flex.gap`, `grid.columns`, `grid.smColumns`, `grid.mdColumns`, `grid.lgColumns`, `grid.xlColumns`, `grid.gap` and `stack.gap`. Rendering classifies four as spacing keys: `container.padding`, `flex.gap`, `grid.gap`, `stack.gap`. One other open member of the family is reported, ⛔ not closed here: see "Reported, not closed" below. `card` has no numeric input, and no layout node has a responsive `gap` object. **H5, corpus.** `git grep` of every `gap` literal in tracked files (CHANGELOGs and the lockfile excluded), triaged by node. No corpus document authors an unmapped `gap` on a `stack`, `flex` or `grid`. The off-set literals are inline CSS (`style` objects in `content/docs/guide/react-pages.md`, an old changeset, two plugin demos, `DatasetReportRenderer.tsx`), `DashboardConfigSchema.gap` (a different schema), and a prose comment in the schema-catalog test. The catalog values (0, 1, 2, 3, 4, 6, 8) sit inside every set. The same sweep over the objectstack checkout (`examples/**`, `packages/*/src`) found inline CSS only. Re-judged by running: `examples/schema-catalog` (40 files, including `safe-validate-corpus-6318.test.ts`), `pnpm check:doc-snippets`, `pnpm check:doc-examples` and `pnpm check:skill-examples`, all green. ## The pins - **`packages/components/src/__tests__/layout-spacing-sets-11474.test.tsx`** generalises `container-padding-set-11424.test.tsx`, which is deleted. Its assertions are held here in general form; the absent-key control now compares with the registration's default step instead of a literal class list. It enumerates as above. A candidate counts as MAPPED when the spacing utilities its value draws are all rules in the package's compiled stylesheet. Each spacing key is then held to three things. Its declaration accepts exactly that set on the tolerant and strict faces, at the authored spelling: flat, or the `properties` bag when the node refuses the key flat; the spelling is derived from the declaration. Its registration input is a closed enum of exactly that set. An absent key draws exactly what the registration's default step draws. An unmapped number draws no spacing rule, so nothing is rounded or clamped. The non-spacing numeric inputs are classified and held to nothing else. Lit controls: the stylesheet reader sees a variant, an escaped dot and an arbitrary value; the enumeration finds `container.padding`. - **`packages/types/src/__tests__/layout-gap-sets-11474.test.ts`**: per key and per face, unmapped numbers (including a fraction and -1) are refused at the key with code `invalid_value`, the set in `issue.values` and in the message. Every mapped step parses, and an absent key parses. The flat `flex` spelling stays refused by name. The describes state the set and no longer say "0-8". Compile-time pins check that the TS faces refuse 7 on `stack`, 9 and 10 on `flex` (node, `FlexLayoutProps` and `FlexBlockNode`), and 9 on `grid`, and that each zod and TS pair states one set. - **Fixture re-judged**: `flex-properties-bag-11276.test.ts` pinned `gap: '4'` in the bag as `invalid_type`. A literal union judges by value, so it is now `invalid_value` at the same path. The assertion's purpose (the bag keeps the mirror's verdict, at its own path) is unchanged, so the code was updated in place. `container-padding-set-11424.test.ts` and `container.tsx` only had their pointer to the deleted file repointed. ## Ablations (committed implementation, `node ../objectstack/scripts/ablation-replace.mjs` wrap mode, trap-armed restore, each restore proven by blob equal to HEAD and an empty `git diff HEAD`) | leg | mutation (landed on disk: anchor count and blob moved) | expected | observed | |---|---|---|---| | A1b | `stack` registration drops `10` from its enum | red | red: `stack.gap registration enum: expected [ +0, 1, 2, 3, 4, 5, 6, 8 ] to deeply equal [ +0, 1, 2, 3, 4, 5, 6, 8, 10 ]` | | A2b | `stack.tsx` gains a `gap === 7` branch | red | red: `stack.gap on the tolerant face: expected [ +0, 1, 2, 3, 4, 5, 6, 8, 10 ] to deeply equal [ +0, 1, 2, 3, 4, 5, 6, 7, 8, 10 ]` | | A3 | `STACK_GAP_STEPS` gains 7 | red | red in both pins (types: mirror set, describe, and each "refuses gap N" on both faces; components: `stack.gap`) | | A4 | `grid`'s `GAPS` map drops `10` (10 then builds `gap-[2.5rem]`) | red: shows the stylesheet criterion is live, not just "a class string is present" | red: `grid.gap` (1 failed, 11 passed) | | A5 | TS `StackSchema.gap` gains 7 | `tsc -p tsconfig.test.json` red | red: unused `@ts-expect-error` and `true` not assignable to `false` in the new types pin, plus the existing `zod-mirror-parity.test.ts` parity pin | First attempts at A1 and A2 were no-ops. The tool refused both before running anything: A1 was a delete passed as an empty replacement, and A2's anchor was a substring of its replacement. Both were re-run with corrected anchors as A1b and A2b above. ## Gates at head `1db960ca` - Build first: `pnpm --filter '@object-ui/components^...' --filter @object-ui/components run build` (9 packages), then `turbo run build --filter='./packages/*' --concurrency=2` (39 tasks) for the doc gates. - `type-check` (script name echoed): `@object-ui/types` 0, `@object-ui/core` 0, `@object-ui/components` 0. The test tsconfigs include the new pins (`--listFilesOnly`). - vitest from the worktree root: `packages/types/` 334 files, 8840 passed. `packages/core/` 193 files, 3844 passed, 27 skipped. `packages/components/` 353 passed, 1 skipped; 3584 tests passed. `examples/schema-catalog/` 40 files, 2259 passed. `packages/sdui-parser/` 20 files, 289 passed. Registry-reading suites (9 files, 584 passed): console `component-input-union-specimens`, `ga-honoured-inputs-author-reach`, `html-tier-manifest`, `public-contract`, `registry-inputs-spec-parity`; app-shell `widget-dom-leak-sweep`; layout `containment-declared-slot-9910`; plugin-designer `designerRegistrationInputs-11434`; `scripts/__tests__/check-component-surface-parity.test.ts`. - exit 0: `check:component-surface-parity`, `check:doc-types`, `check:doc-snippets` (776 of 776 judged, 0 failed), `check:doc-examples`, `check:skill-examples`, `check:doc-fences`, `check:new-line-citations`, `check:control-bytes`, `check:changeset-claims`, `check:pending-changeset-literals`, `check-changeset-no-major.mjs`, `check-changeset-presence.mjs`, `check:i18n-designer-parity`, `check:i18n-keys`, `check:designer-field-key-parity`, `check:registry-bare-names`, `check:unreferenced-sources`, `check:test-path-roots`. - SDUI manifest, measured once by a throwaway probe (real registry, `manifestFromConfigs` plus `validateTree`): `stack` gap 7, `flex` gap 9, `grid` gap 9 and `container` padding 9 each answer `invalid-enum` naming the set. Controls `stack` 4, `flex` 8, `grid` 12 and `container` 8 answer nothing. - lint, narrowed: `eslint --format json` over the 11 changed `.ts`/`.tsx` files. All 11 are linted by the root config (`isPathIgnored` false for each), and the JSON reports 11 files with 0 errors. The 9 warnings are all on untouched lines (`no-explicit-any`, `react-refresh`). The config enables no type-aware linting, so this diff cannot move a verdict on an untouched file. The repo-wide `pnpm lint` belongs to CI. NOT MEASURED: `check:sdui-registration-pins`. Reason: it weighs `apps/console/dist/assets`, which needs a full console build. It pins that the registration keys named by the `sideEffects` arrays survive bundling. This diff moves no `sideEffects` array and no registration key, only an input's type. CI runs it. ## Reported, not closed - **`grid.columns`** (and the four flat `smColumns` to `xlColumns` inputs, and the responsive object's values). This is the same family shape: declared `z.number()` / `number`, while the renderer maps 1 to 12 through its `GRID_COLS*` maps. Readings through the real `SchemaRenderer`: `columns: 13` draws `grid-cols-1 sm:grid-cols-2`, so the md count is silently dropped. `columns: 0` and `columns: -1` draw `grid-cols-2`, a substituted value. `columns: { md: 13 }` draws `grid-cols-1`. All four parse green on both faces. The dispatch says report, ⛔ not close; the report names it for the seat. ## Acceptance notes (observations, not filed) - `examples/schema-catalog/test/layout-props-conversion.test.tsx` keeps its own hand-written `GAP_LADDER` and `CONTAINER_PADDING` sets. They agree with the derived sets today and could now read the declarations instead. That is unexercised drift, so it is noted here and not filed. Carrier: none. - `grid.tsx`'s gap line still carries the comment "Fallback for arbitrary values if not in map", which no longer describes a reachable authored case and never produced a compiled rule. It sits on a renderer branch line, which the claim's file surface excludes. Carrier: none. --- _Generated by [Claude Code](https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f560ded commit 4abc0aa

18 files changed

Lines changed: 771 additions & 143 deletions

‎.changeset/11441-retire-nav-responsive-grid-layout.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ Migration, measured against `objectui validate` on both of its faces:
1515
- `basePath` has no app-document spelling: the strict face refuses it as an unrecognized key. It belongs to the shell, as `AppSchemaRenderer`'s `basePath` prop.
1616

1717
**Clause-②: yes** — two registrations leave the runtime (narrowing), released as `minor` with this banner.
18+
19+
⚠️ **Dated note, 2026-10-02 — `grid` takes one of ten `gap` steps, not any number — objectui#11474.** At this change `grid` accepted any `gap` number; now it accepts 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 `G` at `gap` on both faces with that set named: 7, 9, 11, a number above 12, a negative number or a fraction. For such a number `grid` drew no gap anyway, because the class it built at runtime is in no compiled stylesheet. So in the migration above `G` must be one of those ten steps; each step `ResponsiveGrid`'s own class map drew (0 to 6 and 8) is one of them. `.changeset/11474-layout-spacing-sets.md` states what ships. The rest of this entry is kept as the reading of this change.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
'@object-ui/types': minor
3+
'@object-ui/components': minor
4+
'@object-ui/core': minor
5+
---
6+
7+
The `gap` of a `stack`, a `flex` and a `grid` node is one of the steps its renderer maps.
8+
Any other number is refused at validation, with the set named (objectui#11474).
9+
10+
| node | accepted `gap` steps | default |
11+
|---|---|---|
12+
| `stack` | 0, 1, 2, 3, 4, 5, 6, 8, 10 | 2 |
13+
| `flex` | 0, 1, 2, 3, 4, 5, 6, 7, 8 | 2 |
14+
| `grid` | 0, 1, 2, 3, 4, 5, 6, 8, 10, 12 | 4 |
15+
16+
**Breaking for a `stack`, `flex` or `grid` that carries any other `gap` number.** The key
17+
was declared as any number, and the `flex` and `grid` descriptions advertised "Tailwind
18+
scale 0-8". But each renderer has one gap class per step and nothing for the rest:
19+
`{ "type": "stack", "gap": 7 }` and `{ "type": "flex", "properties": { "gap": 9 } }` parsed
20+
clean and rendered with no gap class at all, not even the default, because the default
21+
applies only when the key is absent. A `grid` built a gap class at runtime for such a
22+
number, and no compiled stylesheet defines a class built that way, so it rendered with no
23+
gap either.
24+
25+
- `@object-ui/types`: `StackSchema.gap`, `FlexLayoutProps.gap` (which `FlexSchema` and the
26+
authored `flex` bag share) and `GridSchema.gap` are literal unions of the steps above on
27+
the TypeScript face, so `tsc` refuses any other number. The zod mirrors refuse one at the
28+
key (`invalid_value`, with the steps in the issue), with a message that lists the set. For
29+
`flex` that is `properties.gap`, and the flat spelling stays refused by name.
30+
`safeValidateSchema` (what `objectui validate` runs) and the strict authoring face both
31+
give that refusal. A `gap` that is not a number at all is now reported as `invalid_value`
32+
rather than `invalid_type`.
33+
- `@object-ui/components`: the `gap` input of the `stack`, `flex` and `grid` registrations
34+
changes from `type: 'number'` to a closed `enum` of the same steps, in the object form the
35+
`container` registration's `padding` already uses. In the SDUI manifest, `validateTree`
36+
now answers an unlisted number with `invalid-enum`. The renderers are unchanged: they do
37+
not round or clamp, and an absent key still renders the default step.
38+
- `@object-ui/core`: `GridBuilder.gap()` and `FlexBuilder.gap()` take the declared steps
39+
instead of any number.
40+
41+
Migration: replace the number with the step you meant from that node's set. `0` means no
42+
gap.

‎.changeset/6151-stack-schema-omit-collapse.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,17 @@ package's own tsconfig and asserts (1) `StackSchema` declares exactly what `Flex
5050
declares, and (2) no member of the `LayoutSchema` union has lost any of `BaseSchema`'s
5151
named members — so the next heritage clause that collapses under the index signature reds
5252
for the whole class, not just for this one interface.
53+
54+
⚠️ **Dated note, 2026-10-02 — `StackSchema` declares its own `gap` — objectui#11474.**
55+
At this change `gap` was a `number` member of `FlexLayoutProps`, declared once and inherited by
56+
`FlexSchema` and `StackSchema` alike, and `stack.tsx` was read as feeding it to a Tailwind
57+
numeric scale; now each node's `gap` is the closed set of steps its renderer maps.
58+
`FlexLayoutProps.gap` is `0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8`, shared by `FlexSchema` and the
59+
authored `flex` bag. `StackSchema` extends `BaseSchema` and `Omit<FlexLayoutProps, 'gap'>` and
60+
declares its own `gap?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10`, the nine steps `stack.tsx` has a
61+
gap class for. So `gap` is declared twice while the other members stay declared once, and `tsc`
62+
refuses any other number on a `stack`, as it refuses `gap: 'large'`. That `Omit` crosses no
63+
index signature (`FlexLayoutProps` carries none), so it erases no member name, and
64+
`stack-schema-emitted-members.test.ts`, which measures the emitted declaration, still passes.
65+
`.changeset/11474-layout-spacing-sets.md` states what ships. The rest of this entry is kept as
66+
the reading of this change.

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ interface FlexNode {
4040
className?: string;
4141
properties?: {
4242
direction?: 'row' | 'col' | 'row-reverse' | 'col-reverse';
43-
gap?: number;
43+
gap?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8; // default: 2
4444
align?: 'start' | 'end' | 'center' | 'baseline' | 'stretch';
4545
justify?: 'start' | 'end' | 'center' | 'between' | 'around' | 'evenly';
4646
wrap?: boolean;
@@ -52,3 +52,8 @@ interface FlexNode {
5252
Nothing changes at render time: `SchemaRenderer` hoists every `properties` key onto the node
5353
before the `flex` renderer reads it, so a stored node that still writes these props flat keeps
5454
rendering.
55+
56+
`gap` is a step on the flex spacing scale, 0 to 8, and `0` means none. Those are the steps the
57+
renderer maps to a gap class, so they are the only values validation accepts:
58+
`"properties": { "gap": 9 }` is refused with the set named (objectui#11474). Such a number
59+
used to pass validation and then render with no gap at all, not even the default.

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,14 @@ import type { SchemaNode } from '@object-ui/types';
1515
interface GridSchema {
1616
type: 'grid';
1717
columns?: number;
18-
gap?: number;
18+
gap?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12; // default: 4
1919
children: SchemaNode[];
2020
className?: string;
2121
}
2222
```
23+
24+
`gap` is a step on the grid's spacing scale, and `0` means none. The ten steps are the ones
25+
the renderer maps to a gap class, so they are the only values validation accepts: `"gap": 9`
26+
or `"gap": 16` is refused with the set named (objectui#11474). For such a number the renderer
27+
used to build a gap class at runtime that no compiled stylesheet defines, so the grid rendered
28+
with no gap at all.

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,14 @@ import type { SchemaNode } from '@object-ui/types';
1414

1515
interface StackSchema {
1616
type: 'stack';
17-
gap?: number;
17+
gap?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10; // default: 2
1818
children: SchemaNode[];
1919
className?: string;
2020
}
2121
```
22+
23+
`gap` is a step on the stack's spacing scale, and `0` means none. The nine steps are the
24+
ones the renderer maps to a gap class, so they are the only values validation accepts:
25+
`"gap": 7` or `"gap": 9` is refused with the set named (objectui#11474). Such a number used
26+
to pass validation and then render with no gap at all, not even the default. A `flex` maps a
27+
different set: it has `7` and no `10`.

‎packages/components/src/__tests__/container-padding-set-11424.test.tsx‎

Lines changed: 0 additions & 88 deletions
This file was deleted.

0 commit comments

Comments
 (0)