diff --git a/.changeset/mapdensity-dead-rowheight-spellings-4352.md b/.changeset/mapdensity-dead-rowheight-spellings-4352.md new file mode 100644 index 0000000000..8d6e986408 --- /dev/null +++ b/.changeset/mapdensity-dead-rowheight-spellings-4352.md @@ -0,0 +1,36 @@ +--- +'@object-ui/react': minor +--- + +`bridgeListView` maps the five row heights the spec admits, and only those — the four dead spellings are gone + +`mapDensity` carried a nine-key table: `compact`, `short`, `comfortable`, +`spacious`, `small`, `medium`, `large`, `tall`, `extra_tall`. `RowHeightSchema` +in `@objectstack/spec` admits five — `short | compact | medium | tall | +extra_tall` — so `comfortable`, `spacious`, `small` and `large` were unreachable +from any spec-valid list view. The bridge's own parameter type said as much +(`Partial< ListView >`), and `mapDensity` widened it back to `rowHeight?: string` +to let them in. They survived because the fixture asserting three of them +compiled against nothing: the package's build tsconfig excludes tests and no +other `tsc` read them, so four branches of renderer-side dialect read as live +capability (objectui#4352, surfaced by objectui#4040 / PR #4351). + +They are deleted. The parameter takes the spec type honestly, and the table is +now `Record< RowHeight, … >`, so a row height added upstream fails the build here +instead of arriving with no density. This is AGENTS.md #0.1: a lenient reading +for off-spec metadata is a second de-facto contract, and one strict contract +beats N dialects — a bad `rowHeight` gets fixed at the producer, where the schema +already rejects it. + +**Breaking semantics, deliberately graded `minor`** (this repo never publishes +`major` — its major tracks `@objectstack`). Nothing narrows in the published type +surface: `mapDensity` is module-local and never appeared in the emitted `.d.ts`, +and `bridgeListView`'s declaration is unchanged. What changes is runtime output, +and only for input the spec already rejects: a host handing the bridge +`rowHeight: 'comfortable'` (or `'spacious'` / `'small'` / `'large'`) used to get +`density: 'comfortable'` / `'spacious'` / `'compact'` back, and now gets no +`density` key at all, so the renderer's own default applies. A sweep of this repo, +the `objectstack` example apps and the console's view metadata found zero authored +uses of any of the four; the legacy `densityMode` alias cannot produce one either, +since `DENSITY_MODE_TO_ROW_HEIGHT` is typed `Record< DensityMode, RowHeight >` and +folds onto `compact` / `medium` / `tall`. diff --git a/packages/react/src/spec-bridge/__tests__/SpecBridge.test.ts b/packages/react/src/spec-bridge/__tests__/SpecBridge.test.ts index db62e05042..67cd477d7c 100644 --- a/packages/react/src/spec-bridge/__tests__/SpecBridge.test.ts +++ b/packages/react/src/spec-bridge/__tests__/SpecBridge.test.ts @@ -183,6 +183,46 @@ describe('SpecBridge', () => { expect(extraTall.density).toBe('spacious'); }); + // The other half of that enum: `comfortable` / `spacious` / `small` / + // `large` used to be mapped too, so four values `RowHeightSchema` does not + // admit read as live capability. They are gone (objectui#4352) — AGENTS.md + // #0.1, one strict contract beats N dialects — and an off-spec `rowHeight` + // now falls through to no density at all rather than being quietly + // rehabilitated into one. + // + // Routed through `SpecBridge.transformListView`, whose parameter is `any`: + // that untyped boundary is the one a host's stored JSON actually crosses, + // and it is the only way left to get these values into the bridge. Writing + // them on `bridgeListView` directly no longer type-checks, which is the + // static half of the same fix. + it.each(['comfortable', 'spacious', 'small', 'large'])( + 'leaves density unset for the off-spec rowHeight %s', + (rowHeight) => { + const node = new SpecBridge().transformListView({ + name: 'off_spec_density', + rowHeight, + }); + + expect(node.density).toBeUndefined(); + // Not merely undefined — the key is never written, so the renderer's + // own default applies instead of an explicit `density: undefined`. + expect('density' in node).toBe(false); + }, + ); + + // Control: a string in neither vocabulary already fell through before the + // four keys were deleted, and still does. Green on both sides of the + // change — it pins the fall-through itself, not the deletion. + it('leaves density unset for a rowHeight in no vocabulary at all', () => { + const node = new SpecBridge().transformListView({ + name: 'off_spec_density', + rowHeight: 'gargantuan', + }); + + expect(node.density).toBeUndefined(); + expect('density' in node).toBe(false); + }); + it('includes optional list properties', () => { const node = bridgeListView( { diff --git a/packages/react/src/spec-bridge/bridges/list-view.ts b/packages/react/src/spec-bridge/bridges/list-view.ts index b526a84a98..ee4b859b4e 100644 --- a/packages/react/src/spec-bridge/bridges/list-view.ts +++ b/packages/react/src/spec-bridge/bridges/list-view.ts @@ -8,7 +8,7 @@ import type { SchemaNode } from '@object-ui/core'; import type { BridgeContext, BridgeFn } from '../types'; -import type { ListView, ListColumn } from '@objectstack/spec/ui'; +import type { ListView, ListColumn, RowHeight } from '@objectstack/spec/ui'; /** * Bridge input: the spec-canonical ListView (#2231 — the former hand-written @@ -45,22 +45,36 @@ function mapColumn(col: ListColumn | string): Record { return mapped; } +/** + * The spec's five row heights collapsed onto the renderer's three densities. + * + * `Record` on purpose (objectui#4352): the key set is the spec's, + * so a row height added upstream fails the build here instead of silently + * arriving with no density. The table used to carry four more keys — + * `comfortable`, `spacious`, `small`, `large` — which `RowHeightSchema` does + * not admit, so no spec-valid list view could ever reach them; they only + * survived because the parameter was widened back to `string` and the fixture + * asserting three of them compiled against nothing. Deleting them is AGENTS.md + * #0.1: a renderer-side dialect for off-spec metadata is a second de-facto + * contract, and one strict contract beats N. An off-spec `rowHeight` now falls + * through to `undefined` — the producer is where it gets fixed. + */ +const ROW_HEIGHT_TO_DENSITY: Record< + RowHeight, + 'compact' | 'comfortable' | 'spacious' +> = { + compact: 'compact', + short: 'compact', + medium: 'comfortable', + tall: 'spacious', + extra_tall: 'spacious', +}; + function mapDensity( - rowHeight?: string, + rowHeight?: RowHeight, ): 'compact' | 'comfortable' | 'spacious' | undefined { if (!rowHeight) return undefined; - const map: Record = { - compact: 'compact', - short: 'compact', - comfortable: 'comfortable', - spacious: 'spacious', - small: 'compact', - medium: 'comfortable', - large: 'spacious', - tall: 'spacious', - extra_tall: 'spacious', - }; - return map[rowHeight]; + return ROW_HEIGHT_TO_DENSITY[rowHeight]; } /** Transforms a ListView spec into a DataTable SchemaNode */