From f00048c1b3c600b70019fbacd5f4db2d0184c3bc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 02:08:21 +0000 Subject: [PATCH 1/2] docs(types): drop the shared `@default 'row'` on FlexLayoutProps.direction and state both consumers in prose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `FlexLayoutProps.direction` is declared once (objectui#6151) so that `FlexSchema` and `StackSchema` name it from both sides, but its two consumers deliberately diverge on what they apply when the key is omitted: `flex.tsx` reads `schema.direction || 'row'`, and `stack.tsx` reads `schema.direction || 'col'` under its own `// Default to column for Stack`. The single published `@default 'row'` was therefore correct for `flex` and wrong for `stack` — whose registration agrees with its renderer (`defaultProps.direction: 'col'`), not with the tag. The renderers are the authority — they are what runs — so nothing but the docblock moved. The remedy is the one objectui#7361 already landed two members down on this same interface for `align`: remove the tag rather than pick a second conditionally-true value, and state the per-type values in prose so no parser reads a value only one consumer applies. The prose form is copied from `align`'s so two sibling members on one interface do not read as two different situations. The criterion is DIVERGENCE, not sharedness. `justify` is shared by the same two consumers and both read `|| 'start'`, so its tag is correct and stays. That control is now derived off disk in the pin instead of asserted from memory, so it turns red if those two reads ever part company. Row 3 of the objectui#7361 census lands in objectui#7361's own pin (`layout-default-jsdoc-7361.test.ts`) rather than a second file: every expected value is extracted from the renderer source, so the pin fails if either side moves. Card objectui#7734. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0114Ytxr5sM1vdW19Y9WAx6E --- .../layout-direction-default-jsdoc-7734.md | 5 + .../layout-default-jsdoc-7361.test.ts | 95 +++++++++++++++---- packages/types/src/layout.ts | 19 +++- 3 files changed, 101 insertions(+), 18 deletions(-) create mode 100644 .changeset/layout-direction-default-jsdoc-7734.md diff --git a/.changeset/layout-direction-default-jsdoc-7734.md b/.changeset/layout-direction-default-jsdoc-7734.md new file mode 100644 index 0000000000..e8943954ec --- /dev/null +++ b/.changeset/layout-direction-default-jsdoc-7734.md @@ -0,0 +1,5 @@ +--- +'@object-ui/types': patch +--- + +The published `@default` documentation on `FlexLayoutProps.direction` no longer states a value that only one of its two consumers applies. The member is declared once (objectui#6151) but `flex.tsx` reads `schema.direction || 'row'` while `stack.tsx` reads `schema.direction || 'col'` ("Default to column for Stack"), so the single `@default 'row'` was correct for `flex` and wrong for `stack` — whose own `defaultProps.direction` is `'col'`. The renderers are unchanged — they are the authority for what runs — so only the docblock moved: `direction` now names both consumers in prose, the same remedy objectui#7361 applied to the sibling `align`. `justify` is shared by the same two consumers and both read `|| 'start'`, so its tag is correct and stays: the criterion is a DIVERGENT shared member, not a shared one. diff --git a/packages/types/src/__tests__/layout-default-jsdoc-7361.test.ts b/packages/types/src/__tests__/layout-default-jsdoc-7361.test.ts index 070467ff34..e9f89264b8 100644 --- a/packages/types/src/__tests__/layout-default-jsdoc-7361.test.ts +++ b/packages/types/src/__tests__/layout-default-jsdoc-7361.test.ts @@ -1,27 +1,40 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. /** - * The `@default` documentation on two `layout.ts` members agrees with the value - * the renderer actually applies (objectui#7361). + * The `@default` documentation on three `layout.ts` members agrees with the value + * the renderer actually applies (objectui#7361 rows 1-2, objectui#7734 row 3). * - * Two published docblocks described a default no renderer ever applied: + * Three published docblocks described a default that at least one consuming + * renderer does not apply: * - * | member | tag said | renderer applies | - * |----------------------------|------------|-------------------------------------| - * | `ContainerSchema.maxWidth` | `'lg'` | `container.tsx`: `?? 'xl'` | - * | `FlexLayoutProps.align` | `'center'` | `flex.tsx`: `|| 'start'`, | - * | | | `stack.tsx`: `|| 'stretch'` | + * | member | tag said | renderer applies | + * |-----------------------------|------------|------------------------------------| + * | `ContainerSchema.maxWidth` | `'lg'` | `container.tsx`: `?? 'xl'` | + * | `FlexLayoutProps.align` | `'center'` | `flex.tsx`: `|| 'start'`, | + * | | | `stack.tsx`: `|| 'stretch'` | + * | `FlexLayoutProps.direction` | `'row'` | `flex.tsx`: `|| 'row'`, | + * | | | `stack.tsx`: `|| 'col'` | * * The renderers are the authority — they are what runs — so the tags moved, not * the reads. Changing the reads to match the tags would relayout every existing * page that omits either key, which is a behaviour change and a separate ruling. * - * `FlexLayoutProps.align` is the structurally interesting half. The member is - * declared ONCE (objectui#6151 — see the interface docblock for why `StackSchema` - * cannot derive it with an `Omit`), but `flex` and `stack` deliberately diverge - * on it: that divergence is most of what distinguishes the two component types. - * So no single `@default` value can be correct there, and the fix is the absence - * of a tag plus prose naming both consumers — not a second wrong single value. + * `FlexLayoutProps.align` and `FlexLayoutProps.direction` are the structurally + * interesting rows. Each member is declared ONCE (objectui#6151 — see the + * interface docblock for why `StackSchema` cannot derive them with an `Omit`), + * but `flex` and `stack` deliberately diverge on both: that divergence is most of + * what distinguishes the two component types. So no single `@default` value can + * be correct there, and the fix is the absence of a tag plus prose naming both + * consumers — not a second wrong single value. `direction` is the sharper case: + * its tag was not wrong for everybody the way `align`'s `'center'` was — `flex` + * really does apply `'row'` — which is exactly why "a shared member's tag is only + * conditionally true" is the defect, not "the value is wrong". + * + * The criterion is DIVERGENCE, not sharedness. `FlexLayoutProps.justify` is + * shared by the same two consumers and both read `|| 'start'`, so its tag is + * correct and must stay — it is pinned below as a negative control, and it is + * what stops this row-by-row fix from generalising into "shared members carry no + * tags". * * ## Why this pin reads both sides off disk * @@ -55,6 +68,10 @@ const STACK = 'packages/components/src/renderers/layout/stack.tsx'; const CONTAINER_MAXWIDTH = /schema\.maxWidth\s*\?\?\s*'([^']+)'/; const FLEX_ALIGN = /schema\.align\s*\|\|\s*'([^']+)'/; const STACK_ALIGN = /schema\.align\s*\|\|\s*'([^']+)'/; +const FLEX_DIRECTION = /schema\.direction\s*\|\|\s*'([^']+)'/; +const STACK_DIRECTION = /schema\.direction\s*\|\|\s*'([^']+)'/; +const FLEX_JUSTIFY = /schema\.justify\s*\|\|\s*'([^']+)'/; +const STACK_JUSTIFY = /schema\.justify\s*\|\|\s*'([^']+)'/; /** The body of a named `export interface`, so member lookups cannot stray. */ function interfaceBody(src: string, name: string): string { @@ -91,17 +108,25 @@ describe('layout.ts `@default` docs agree with the renderer fallbacks (objectui# expect(CONTAINER_MAXWIDTH.exec(read(CONTAINER))).not.toBeNull(); expect(FLEX_ALIGN.exec(read(FLEX))).not.toBeNull(); expect(STACK_ALIGN.exec(read(STACK))).not.toBeNull(); + expect(FLEX_DIRECTION.exec(read(FLEX))).not.toBeNull(); + expect(STACK_DIRECTION.exec(read(STACK))).not.toBeNull(); + expect(FLEX_JUSTIFY.exec(read(FLEX))).not.toBeNull(); + expect(STACK_JUSTIFY.exec(read(STACK))).not.toBeNull(); }); it('each extracted fallback is a member of the union the type declares', () => { const maxWidth = CONTAINER_MAXWIDTH.exec(read(CONTAINER))![1]; const flexAlign = FLEX_ALIGN.exec(read(FLEX))![1]; const stackAlign = STACK_ALIGN.exec(read(STACK))![1]; + const flexDirection = FLEX_DIRECTION.exec(read(FLEX))![1]; + const stackDirection = STACK_DIRECTION.exec(read(STACK))![1]; const container = interfaceBody(types, 'ContainerSchema'); const flexProps = interfaceBody(types, 'FlexLayoutProps'); expect(container).toContain(`'${maxWidth}'`); expect(flexProps).toContain(`'${flexAlign}'`); expect(flexProps).toContain(`'${stackAlign}'`); + expect(flexProps).toContain(`'${flexDirection}'`); + expect(flexProps).toContain(`'${stackDirection}'`); }); }); @@ -146,15 +171,53 @@ describe('layout.ts `@default` docs agree with the renderer fallbacks (objectui# }); }); + describe('row 3 — FlexLayoutProps.direction (shared member, two divergent consumers)', () => { + const flexDirection = FLEX_DIRECTION.exec(read(FLEX))![1]; + const stackDirection = STACK_DIRECTION.exec(read(STACK))![1]; + + it('the two consumers really do diverge — the reason a single tag cannot be right', () => { + expect(flexDirection).not.toEqual(stackDirection); + }); + + it('publishes NO single-value `@default` block tag', () => { + const doc = docblockFor(interfaceBody(types, 'FlexLayoutProps'), 'direction'); + expect(defaultTags(doc)).toEqual([]); + }); + + it('names BOTH consumers and the fallback each one applies', () => { + const doc = docblockFor(interfaceBody(types, 'FlexLayoutProps'), 'direction'); + expect(doc).toContain('flex.tsx'); + expect(doc).toContain('stack.tsx'); + expect(doc).toContain(`'${flexDirection}'`); + expect(doc).toContain(`'${stackDirection}'`); + }); + + it('no longer publishes the value only ONE of the two consumers applies', () => { + const doc = docblockFor(interfaceBody(types, 'FlexLayoutProps'), 'direction'); + expect(defaultTags(doc)).not.toContain(`'${flexDirection}'`); + }); + }); + describe('negative controls — neighbouring `@default` tags are untouched', () => { it('ContainerSchema.centered still reads `@default true`', () => { const doc = docblockFor(interfaceBody(types, 'ContainerSchema'), 'centered'); expect(defaultTags(doc)).toEqual(['true']); }); - it('FlexLayoutProps.justify still reads `@default \'start\'`', () => { + /** + * The load-bearing control. `justify` is shared by the same two consumers as + * `align` and `direction`, so it is what makes the criterion DIVERGENCE and + * not sharedness: both renderers read `|| 'start'`, so one tag IS right for + * both and must stay. Derived off disk like the rows above, so it turns red + * if the consumers ever diverge here — at which point the tag becomes a + * fourth instance of this defect rather than a control. + */ + it('FlexLayoutProps.justify keeps its tag — its two consumers AGREE', () => { + const flexJustify = FLEX_JUSTIFY.exec(read(FLEX))![1]; + const stackJustify = STACK_JUSTIFY.exec(read(STACK))![1]; + expect(flexJustify).toEqual(stackJustify); const doc = docblockFor(interfaceBody(types, 'FlexLayoutProps'), 'justify'); - expect(defaultTags(doc)).toEqual(["'start'"]); + expect(defaultTags(doc)).toEqual([`'${flexJustify}'`]); }); it('FlexLayoutProps.gap still reads `@default 2`', () => { diff --git a/packages/types/src/layout.ts b/packages/types/src/layout.ts index 933639a613..66960d3203 100644 --- a/packages/types/src/layout.ts +++ b/packages/types/src/layout.ts @@ -286,8 +286,23 @@ export interface ContainerSchema extends BaseSchema { */ export interface FlexLayoutProps { /** - * Flex direction - * @default 'row' + * Flex direction. + * + * Deliberately carries NO `@default` tag. The member is declared once here + * (see this interface's docblock and objectui#6151), but the two component + * types that consume it diverge on the value they apply when it is omitted: + * `flex.tsx` reads `schema.direction || 'row'`, `stack.tsx` reads + * `schema.direction || 'col'` ("Default to column for Stack"). One tag on a + * shared member cannot be right for both — it would publish a single default + * that only one consumer applies, which is the defect objectui#7734 records + * (the tag here used to read `'row'`, which `flex` applies and `stack` + * deliberately does not — a column is what the `stack` type is FOR). The + * per-type values are stated in prose so no parser reads a value that is only + * conditionally true. + * + * The criterion is a DIVERGENT shared member, not a shared one: the sibling + * `justify` keeps its `@default 'start'` because both consumers read + * `|| 'start'`, and that tag is correct for both. */ direction?: 'row' | 'col' | 'row-reverse' | 'col-reverse'; /** From 4317adc82017fcf6135ff77b8c0c02fcbb469c32 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 02:22:48 +0000 Subject: [PATCH 2/2] docs(types): lift the divergent-vs-shared criterion to the FlexLayoutProps docblock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sentence naming `justify` as the positive control — that a shared member carries an `@default` only when both consumers apply the same value — was sitting on `direction`'s member docblock. It is a claim about this interface, not about `direction`: on the interface docblock it reaches the reader of every member instead of one, and it restores the parity between `align` and `direction`, which are meant to read as two instances of one situation rather than as two situations. Re-voiced for its new home so it covers all three shared members at once: `align` and `direction` diverge and state their per-type values in prose, `justify` keeps its tag because `flex.tsx` and `stack.tsx` both read `|| 'start'`. Prose only. No assertion in `layout-default-jsdoc-7361.test.ts` was touched or loosened: the interface docblock sits ahead of `export interface`, which is where `interfaceBody()` starts its slice, so no member-docblock lookup can see the moved text. Card objectui#7734. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0114Ytxr5sM1vdW19Y9WAx6E --- packages/types/src/layout.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/types/src/layout.ts b/packages/types/src/layout.ts index 66960d3203..056147b8f7 100644 --- a/packages/types/src/layout.ts +++ b/packages/types/src/layout.ts @@ -279,6 +279,12 @@ export interface ContainerSchema extends BaseSchema { * Types of property 'type' are incompatible.`). Lifting the shared members out * of the inheritance path is what keeps them nameable from both sides. * + * A member here carries an `@default` tag only when BOTH consumers apply the + * same value — the criterion is a DIVERGENT shared member, not a shared one: + * `align` and `direction` diverge and state their per-type values in prose + * instead, while `justify` keeps its `@default 'start'` because `flex.tsx` and + * `stack.tsx` both read `|| 'start'`. + * * Pinned by `__tests__/stack-schema-emitted-members.test.ts`, which asserts * against the EMITTED declaration rather than this source — a source-level * assertion passes while the emitted declaration is empty, and that gap is @@ -299,10 +305,6 @@ export interface FlexLayoutProps { * deliberately does not — a column is what the `stack` type is FOR). The * per-type values are stated in prose so no parser reads a value that is only * conditionally true. - * - * The criterion is a DIVERGENT shared member, not a shared one: the sibling - * `justify` keeps its `@default 'start'` because both consumers read - * `|| 'start'`, and that tag is correct for both. */ direction?: 'row' | 'col' | 'row-reverse' | 'col-reverse'; /**