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..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 @@ -286,8 +292,19 @@ 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. */ direction?: 'row' | 'col' | 'row-reverse' | 'col-reverse'; /**