diff --git a/.changeset/11347-components-uncast-reads.md b/.changeset/11347-components-uncast-reads.md new file mode 100644 index 0000000000..6d6390b512 --- /dev/null +++ b/.changeset/11347-components-uncast-reads.md @@ -0,0 +1,31 @@ +--- +'@object-ui/components': minor +'@object-ui/types': minor +--- + +Eight renderers stop riding `BaseSchema`'s index signature for node keys their types did not declare (objectui#11347, the `@object-ui/components` preparation for objectui#8347's removal of that signature). The installed `@objectstack/spec` has no row for any of these eight node types, so each key was decided by who authors it. Only one of them has a producer: the published `skills/objectui` expressions guide authors `title` on a `list` node. No catalog entry, example, docs page, package README or objectstack-shipped document authors any of the others. + +**One key is kept and declared.** `ListSchema.title` is now declared on both faces, as an optional string. The `list` renderer keeps drawing it as a heading above the list, and the `list` registration keeps publishing it as an input. The zod mirror now refuses a `title` that is not a string, where `.passthrough()` used to keep it unjudged. + +**Two reads move to the spelling the type already declares.** Neither move adds an alias. + +- `input-otp` draws `length` slots. It used to read an undeclared `maxLength` and ignore the declared `length`, so the published 4-digit and 8-digit examples drew six slots each. `maxLength` is no longer read. Write `length`, which defaults to 6. +- `loading` draws its message from the declared `label`. It used to read an undeclared `text` and leave `label` inert. `text` is no longer read. Write `label`. + +**Seven reads are retired, because no type declares the key and nothing authors it:** + +- `sidebar-menu-button`: `active`. The node no longer sets the button's active state. +- `drawer`: `footer`, `showClose` and `shouldScaleBackground`. The drawer draws no footer and no Close button. +- `dropdown-menu`, `popover` and `sheet`: `modal`. + +Where a registration published a retired key as an input, the input goes too: `active` on `sidebar-menu-button`, the three `drawer` keys, and `modal` on `popover` and `sheet`. `dropdown-menu` never published `modal`. `sheet`'s `defaultProps` also loses its `modal` seed. `SchemaRenderer` still spreads unread node keys onto the component as props, so an authored `modal` or `shouldScaleBackground` still reaches the Radix or vaul root, as it already did beside the named read. Refusing those keys is the strict authoring face's job, not a renderer read's. + +**Types.** In `@object-ui/types`: + +- `ListSchema` gains `title?: string` on both faces. +- The "What it renders instead" lists in the `body` / `children` refusal messages of these node types now name what the renderers read. +- The docblocks of `LoadingSchema.label`, `InputOTPSchema.length`, `ListSchema.title` and the four overlay schemas record these changes. + +Apart from `ListSchema.title`, no member is added or removed and no accept set changes. + +**minor, not patch — the published face gains a member.** `ListSchema.title` is a new member of the shipped `.d.ts` and of the mirror's `.shape`, and the mirror refuses a non-string `title` by name, the class objectui#7722 graded `minor` on this same schema. diff --git a/.changeset/8318-jsdoc-default-tag-corrections.md b/.changeset/8318-jsdoc-default-tag-corrections.md index af850f6c88..0e3f988d3e 100644 --- a/.changeset/8318-jsdoc-default-tag-corrections.md +++ b/.changeset/8318-jsdoc-default-tag-corrections.md @@ -49,3 +49,5 @@ described nothing that runs. The pin `packages/types/src/__tests__/layout-default-jsdoc-7361.test.ts` grows from 15 rows to 24, both sides derived off disk as before. + +**Correction, 2026-10-01 (objectui#11347).** The `CRUDDialogSchema` bullet above says `showClose`'s one other occurrence belongs to `DrawerSchema`. That bullet's `renderers/overlay/drawer.tsx:38` is an address into the tree at `33f4a1980`, where that line is the `drawer` registration's `showClose` read. The sentence was true when this change was written, and it no longer is: objectui#11347 retired that read, which `DrawerSchema` never declared and no producer authored, so `showClose` is now read under no node type at all. The `CRUDDialogSchema.showClose` docblock and its pin in `layout-default-jsdoc-7361.test.ts` say so. The verdict this change records is unchanged: `showClose` still publishes no `@default`. diff --git a/content/docs/components/data-display/list.mdx b/content/docs/components/data-display/list.mdx index 536cb54ef1..55b7d57183 100644 --- a/content/docs/components/data-display/list.mdx +++ b/content/docs/components/data-display/list.mdx @@ -12,6 +12,7 @@ description: "Display a list of items" ```plaintext interface ListSchema { type: 'list'; + title?: string; // heading drawn above the list items: SchemaNode[]; ordered?: boolean; className?: string; diff --git a/content/docs/components/feedback/loading.mdx b/content/docs/components/feedback/loading.mdx index e20ee05a7e..ea630374ae 100644 --- a/content/docs/components/feedback/loading.mdx +++ b/content/docs/components/feedback/loading.mdx @@ -14,7 +14,14 @@ description: "Loading spinner to indicate processing" ```plaintext interface LoadingSchema { type: 'loading'; + label?: string; // Text drawn under the spinner size?: 'sm' | 'md' | 'lg'; className?: string; } ``` + + + The message under the spinner is `label`, the key `LoadingSchema` declares. + Until objectui#11347 the renderer read an undeclared `text` instead and left + `label` inert. `text` is not a key of this node: write `label`. + diff --git a/content/docs/components/form/input-otp.mdx b/content/docs/components/form/input-otp.mdx index 20cd117136..de59b4ce9e 100644 --- a/content/docs/components/form/input-otp.mdx +++ b/content/docs/components/form/input-otp.mdx @@ -27,7 +27,7 @@ The Input OTP component provides a secure way to enter one-time passwords or ver ```plaintext interface InputOTPSchema { type: 'input-otp'; - length: number; // Number of digits + length?: number; // Number of digits (default 6) value?: string; // Current value separator?: boolean; // Show visual separator @@ -44,6 +44,13 @@ interface InputOTPSchema { } ``` + + `length` is the slot count, and the renderer now reads it. Until objectui#11347 + it read an undeclared `maxLength` instead and ignored `length`, so the 4- and + 8-digit examples above drew six slots. `maxLength` is not a key of this node: + write `length`. + + ## Examples ### In a Form diff --git a/packages/components/src/__tests__/action-bar.test.tsx b/packages/components/src/__tests__/action-bar.test.tsx index 749aaa4b03..801634b588 100644 --- a/packages/components/src/__tests__/action-bar.test.tsx +++ b/packages/components/src/__tests__/action-bar.test.tsx @@ -7,6 +7,15 @@ import { render } from '@testing-library/react'; import { ComponentRegistry } from '@object-ui/core'; import { ActionProvider } from '@object-ui/react'; import { renderComponent, validateComponentRegistration } from './test-utils'; +import type { ActionBarSchema } from '../renderers/action/action-bar'; + +/** + * Types each literal below as the `action:bar` node it is rather than as the + * `BaseSchema` `renderComponent` accepts (objectui#11347): once `BaseSchema`'s + * index signature is gone (objectui#8347), a literal checked against + * `BaseSchema` may author only `BaseSchema`'s own keys. + */ +const actionBar = (schema: ActionBarSchema): ActionBarSchema => schema; // Ensure action renderers are loaded (side-effect imports via vitest.setup.tsx) @@ -24,39 +33,39 @@ describe('ActionBar (action:bar)', () => { describe('rendering', () => { it('renders nothing when actions array is empty', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', actions: [], - }); + })); expect(container.innerHTML).toBe(''); }); it('renders action buttons for provided actions', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', actions: [ { name: 'save', label: 'Save', type: 'script', component: 'action:button' }, { name: 'cancel', label: 'Cancel', type: 'script', component: 'action:button' }, ], - }); + })); expect(container.textContent).toContain('Save'); expect(container.textContent).toContain('Cancel'); }); it('renders with role="toolbar" and aria-label', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', actions: [ { name: 'test', label: 'Test', type: 'script' }, ], - }); + })); const toolbar = container.querySelector('[role="toolbar"]'); expect(toolbar).toBeTruthy(); expect(toolbar?.getAttribute('aria-label')).toBe('Actions'); }); it('filters actions by location', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', location: 'list_toolbar', actions: [ @@ -64,7 +73,7 @@ describe('ActionBar (action:bar)', () => { { name: 'header_action', label: 'Header Action', type: 'script', locations: ['record_header'] }, { name: 'both_action', label: 'Both Action', type: 'script', locations: ['list_toolbar', 'record_header'] }, ], - }); + })); expect(container.textContent).toContain('Toolbar Action'); expect(container.textContent).not.toContain('Header Action'); expect(container.textContent).toContain('Both Action'); @@ -78,7 +87,7 @@ describe('ActionBar (action:bar)', () => { // is the declaration; no declaration, no located placement (ADR-0078 reads // an action with no `locations` as inert for exactly this reason). it('hides an action that declares no locations when filtering by location', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', location: 'record_header', actions: [ @@ -87,7 +96,7 @@ describe('ActionBar (action:bar)', () => { { name: 'other_loc', label: 'Other Location', type: 'script', locations: ['list_toolbar'] }, { name: 'here_loc', label: 'Here Location', type: 'script', locations: ['record_header'] }, ], - }); + })); expect(container.textContent).not.toContain('No Location'); // An empty array reads the same as an absent one — the third dialect // (`action:group` hid `[]` but showed `undefined`) is gone too. @@ -97,26 +106,26 @@ describe('ActionBar (action:bar)', () => { }); it('renders all actions when no location filter is set', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', actions: [ { name: 'a1', label: 'Action 1', type: 'script', locations: ['list_toolbar'] }, { name: 'a2', label: 'Action 2', type: 'script', locations: ['record_header'] }, ], - }); + })); expect(container.textContent).toContain('Action 1'); expect(container.textContent).toContain('Action 2'); }); it('deduplicates actions by name', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', actions: [ { name: 'change_status', label: 'Change Status', type: 'script', component: 'action:button' }, { name: 'assign_user', label: 'Assign User', type: 'script', component: 'action:button' }, { name: 'change_status', label: 'Change Status', type: 'script', component: 'action:button' }, ], - }); + })); const toolbar = container.querySelector('[role="toolbar"]'); expect(toolbar).toBeTruthy(); // Should only render 2 actions (duplicates removed) @@ -126,7 +135,7 @@ describe('ActionBar (action:bar)', () => { }); it('deduplicates actions after location filtering', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', location: 'record_header', actions: [ @@ -135,7 +144,7 @@ describe('ActionBar (action:bar)', () => { { name: 'change_status', label: 'Change Status', type: 'script', locations: ['record_header', 'record_more'] }, { name: 'assign_user', label: 'Assign User', type: 'script', locations: ['record_header'] }, ], - }); + })); const toolbar = container.querySelector('[role="toolbar"]'); expect(toolbar).toBeTruthy(); // Should only render 2 unique actions @@ -145,7 +154,7 @@ describe('ActionBar (action:bar)', () => { describe('overflow', () => { it('groups excess actions into overflow menu when maxVisible is exceeded', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', maxVisible: 2, actions: [ @@ -154,7 +163,7 @@ describe('ActionBar (action:bar)', () => { { name: 'a3', label: 'Action 3', type: 'script' }, { name: 'a4', label: 'Action 4', type: 'script' }, ], - }); + })); // First 2 should be visible as buttons expect(container.textContent).toContain('Action 1'); expect(container.textContent).toContain('Action 2'); @@ -167,14 +176,14 @@ describe('ActionBar (action:bar)', () => { }); it('does not show overflow when actions fit within maxVisible', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', maxVisible: 5, actions: [ { name: 'a1', label: 'Action 1', type: 'script' }, { name: 'a2', label: 'Action 2', type: 'script' }, ], - }); + })); const toolbar = container.querySelector('[role="toolbar"]'); expect(toolbar!.children.length).toBe(2); }); @@ -185,7 +194,7 @@ describe('ActionBar (action:bar)', () => { ':scope > button:not([aria-haspopup]), :scope > [role="button"]:not([aria-haspopup])'; it('orders inline actions by `order` (lower = earlier / primary slot)', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', location: 'record_header', maxVisible: 5, @@ -195,7 +204,7 @@ describe('ActionBar (action:bar)', () => { { name: 'print', label: 'Print', type: 'script', locations: ['record_header'], order: 5 }, { name: 'approve', label: 'Approve', type: 'script', locations: ['record_header'], order: -10 }, ], - }); + })); const toolbar = container.querySelector('[role="toolbar"]'); const inlineText = Array.from(toolbar!.querySelectorAll(inlineButtonsSelector)) .map(b => b.textContent) @@ -206,7 +215,7 @@ describe('ActionBar (action:bar)', () => { }); it('promotes a low-`order` action into the primary slot even when registered last', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', location: 'record_header', maxVisible: 1, @@ -214,7 +223,7 @@ describe('ActionBar (action:bar)', () => { { name: 'app_action', label: 'App Action', type: 'script', locations: ['record_header'] }, { name: 'approve', label: 'Approve', type: 'script', locations: ['record_header'], order: -100 }, ], - }); + })); const toolbar = container.querySelector('[role="toolbar"]'); // 1 inline primary button + 1 overflow menu trigger expect(toolbar!.children.length).toBe(2); @@ -226,7 +235,7 @@ describe('ActionBar (action:bar)', () => { }); it('is stable — actions without `order` keep their registration order', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', location: 'record_header', maxVisible: 5, @@ -235,7 +244,7 @@ describe('ActionBar (action:bar)', () => { { name: 'b', label: 'Bravo', type: 'script', locations: ['record_header'] }, { name: 'c', label: 'Charlie', type: 'script', locations: ['record_header'] }, ], - }); + })); const toolbar = container.querySelector('[role="toolbar"]'); const inlineText = Array.from(toolbar!.querySelectorAll(inlineButtonsSelector)) .map(b => b.textContent) @@ -245,7 +254,7 @@ describe('ActionBar (action:bar)', () => { }); it('tie-break: a `primary` action wins the primary slot when nobody sets `order`', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', location: 'record_header', maxVisible: 1, @@ -255,7 +264,7 @@ describe('ActionBar (action:bar)', () => { { name: 'export', label: 'Export', type: 'script', locations: ['record_header'] }, { name: 'submit', label: 'Submit', type: 'script', variant: 'primary', locations: ['record_header'] }, ], - }); + })); const toolbar = container.querySelector('[role="toolbar"]'); // 1 inline primary button + 1 overflow menu trigger expect(toolbar!.children.length).toBe(2); @@ -267,7 +276,7 @@ describe('ActionBar (action:bar)', () => { }); it('`order` outranks the `primary` tie-break', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', location: 'record_header', maxVisible: 5, @@ -277,7 +286,7 @@ describe('ActionBar (action:bar)', () => { { name: 'submit', label: 'Submit', type: 'script', variant: 'primary', locations: ['record_header'] }, { name: 'approve', label: 'Approve', type: 'script', order: -100, locations: ['record_header'] }, ], - }); + })); const toolbar = container.querySelector('[role="toolbar"]'); const inlineText = Array.from(toolbar!.querySelectorAll(inlineButtonsSelector)) .map(b => b.textContent) @@ -288,13 +297,13 @@ describe('ActionBar (action:bar)', () => { describe('systemActions', () => { it('renders a single overflow menu when only systemActions are provided', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', systemActions: [ { name: 'sys_duplicate', label: 'Duplicate', type: 'script' }, { name: 'sys_export', label: 'Export', type: 'script' }, ], - }); + })); const toolbar = container.querySelector('[role="toolbar"]'); expect(toolbar).toBeTruthy(); // 0 inline buttons + 1 overflow menu trigger @@ -302,7 +311,7 @@ describe('ActionBar (action:bar)', () => { }); it('merges business overflow and systemActions into ONE overflow menu', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', maxVisible: 2, actions: [ @@ -315,7 +324,7 @@ describe('ActionBar (action:bar)', () => { { name: 'sys_duplicate', label: 'Duplicate', type: 'script' }, { name: 'sys_delete', label: 'Delete', type: 'script' }, ], - }); + })); const toolbar = container.querySelector('[role="toolbar"]'); // 2 inline buttons + exactly 1 overflow menu trigger — never two expect(toolbar!.children.length).toBe(3); @@ -325,7 +334,7 @@ describe('ActionBar (action:bar)', () => { }); it('systemActions never appear inline regardless of maxVisible', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', maxVisible: 10, actions: [ @@ -334,7 +343,7 @@ describe('ActionBar (action:bar)', () => { systemActions: [ { name: 'sys_duplicate', label: 'Duplicate', type: 'script' }, ], - }); + })); const toolbar = container.querySelector('[role="toolbar"]'); // 1 inline business button + 1 overflow menu for the system action expect(toolbar!.children.length).toBe(2); @@ -345,13 +354,13 @@ describe('ActionBar (action:bar)', () => { }); it('renders overflow menu when only systemActions exist even with empty actions', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', actions: [], systemActions: [ { name: 'sys_history', label: 'History', type: 'script' }, ], - }); + })); const toolbar = container.querySelector('[role="toolbar"]'); expect(toolbar).toBeTruthy(); expect(toolbar!.children.length).toBe(1); @@ -360,36 +369,36 @@ describe('ActionBar (action:bar)', () => { describe('styling', () => { it('applies custom className', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', className: 'my-custom-bar', actions: [ { name: 'test', label: 'Test', type: 'script' }, ], - }); + })); const toolbar = container.querySelector('[role="toolbar"]'); expect(toolbar?.className).toContain('my-custom-bar'); }); it('supports vertical direction', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', direction: 'vertical', actions: [ { name: 'test', label: 'Test', type: 'script' }, ], - }); + })); const toolbar = container.querySelector('[role="toolbar"]'); expect(toolbar?.className).toContain('flex-col'); }); it('defaults to horizontal direction', () => { - const { container } = renderComponent({ + const { container } = renderComponent(actionBar({ type: 'action:bar', actions: [ { name: 'test', label: 'Test', type: 'script' }, ], - }); + })); const toolbar = container.querySelector('[role="toolbar"]'); expect(toolbar?.className).toContain('flex-row'); }); @@ -406,7 +415,7 @@ describe('ActionBar (action:bar)', () => { describe('requiredPermissions capability gate', () => { const Bar = ({ actions, systemActions }: { actions?: any[]; systemActions?: any[] }) => { const Component = ComponentRegistry.get('action:bar')!; - return ; + return ; }; const withUser = (user: unknown, props: { actions?: any[]; systemActions?: any[] }) => render(); diff --git a/packages/components/src/__tests__/basic-renderers.test.tsx b/packages/components/src/__tests__/basic-renderers.test.tsx index 62b30852b0..704553caf3 100644 --- a/packages/components/src/__tests__/basic-renderers.test.tsx +++ b/packages/components/src/__tests__/basic-renderers.test.tsx @@ -17,6 +17,30 @@ import { // cold transform is billed to `hookTimeout`, which is why this carried a raised // timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). import '../renderers'; +import type { + DivSchema, + HtmlSchema, + IconSchema, + ImageSchema, + SeparatorSchema, + TextSchema, + TextSpanSchema, +} from '@object-ui/types'; + +/** + * Each literal below is typed as the node it is rather than as the + * `BaseSchema` `renderComponent` accepts, children included (objectui#11347): + * once `BaseSchema`'s index signature is gone (objectui#8347), a literal + * checked against `BaseSchema` or `SchemaNode` may author only `BaseSchema`'s + * own keys. + */ +const textNode = (schema: TextSchema): TextSchema => schema; +const divNode = (schema: DivSchema): DivSchema => schema; +const spanNode = (schema: TextSpanSchema): TextSpanSchema => schema; +const imageNode = (schema: ImageSchema): ImageSchema => schema; +const iconNode = (schema: IconSchema): IconSchema => schema; +const separatorNode = (schema: SeparatorSchema): SeparatorSchema => schema; +const htmlNode = (schema: HtmlSchema): HtmlSchema => schema; /** * Comprehensive tests for basic renderer components @@ -32,10 +56,10 @@ describe('Basic Renderers - Display Issue Detection', () => { }); it('should render text content without issues', () => { - const { container } = renderComponent({ + const { container } = renderComponent(textNode({ type: 'text', content: 'Hello World', - }); + })); expect(container.textContent).toContain('Hello World'); const issues = getAllDisplayIssues(container); @@ -43,10 +67,10 @@ describe('Basic Renderers - Display Issue Detection', () => { }); it('should handle empty content gracefully', () => { - const { container } = renderComponent({ + const { container } = renderComponent(textNode({ type: 'text', content: '', - }); + })); const domCheck = checkDOMStructure(container); // Empty text is acceptable, just verify it doesn't crash @@ -70,7 +94,7 @@ describe('Basic Renderers - Display Issue Detection', () => { it('should render with designer props correctly', () => { const { container } = renderComponent( - { type: 'text', content: 'Designer Test' }, + textNode({ type: 'text', content: 'Designer Test' }), ); // Verify it renders without errors @@ -86,10 +110,10 @@ describe('Basic Renderers - Display Issue Detection', () => { }); it('should render container without issues', () => { - const { container } = renderComponent({ + const { container } = renderComponent(divNode({ type: 'div', className: 'test-class', - }); + })); const div = container.querySelector('div'); expect(div).toBeTruthy(); @@ -98,23 +122,23 @@ describe('Basic Renderers - Display Issue Detection', () => { }); it('should render children correctly', () => { - const { container } = renderComponent({ + const { container } = renderComponent(divNode({ type: 'div', children: [ - { type: 'text', content: 'Child 1' }, - { type: 'text', content: 'Child 2' }, + textNode({ type: 'text', content: 'Child 1' }), + textNode({ type: 'text', content: 'Child 2' }), ], - }); + })); expect(container.textContent).toContain('Child 1'); expect(container.textContent).toContain('Child 2'); }); it('should not have display issues', () => { - const { container } = renderComponent({ + const { container } = renderComponent(divNode({ type: 'div', - children: [{ type: 'text', content: 'Content' }], - }); + children: [textNode({ type: 'text', content: 'Content' })], + })); const issues = getAllDisplayIssues(container); // Should have no critical issues @@ -129,13 +153,13 @@ describe('Basic Renderers - Display Issue Detection', () => { }); it('should render inline content', () => { - const { container } = renderComponent({ + const { container } = renderComponent(spanNode({ type: 'span', // Canonical child key — what `TextSpanSchema` declares and what the // renderer reads (objectui#5027). This case used to spell `body`, the // one key nothing produced, which is what kept the defect invisible. - children: [{ type: 'text', content: 'Inline text' }], - }); + children: [textNode({ type: 'text', content: 'Inline text' })], + })); const span = container.querySelector('span'); expect(span).toBeTruthy(); @@ -150,11 +174,11 @@ describe('Basic Renderers - Display Issue Detection', () => { }); it('should render with required alt attribute', () => { - const { container } = renderComponent({ + const { container } = renderComponent(imageNode({ type: 'image', src: 'https://example.com/image.jpg', alt: 'Test image', - }); + })); const img = container.querySelector('img'); expect(img).toBeTruthy(); @@ -163,10 +187,10 @@ describe('Basic Renderers - Display Issue Detection', () => { }); it('should detect missing alt attribute', () => { - const { container } = renderComponent({ + const { container } = renderComponent(imageNode({ type: 'image', src: 'https://example.com/image.jpg', - }); + })); const img = container.querySelector('img'); // If img has alt, it's good; if not, our check should detect it @@ -196,10 +220,10 @@ describe('Basic Renderers - Display Issue Detection', () => { // the placeholder marker is what separates "resolved the requested glyph" // from "drew the dashed box that means it did not". it('should render icon without issues', () => { - const { container } = renderComponent({ + const { container } = renderComponent(iconNode({ type: 'icon', icon: 'star', - }); + })); // Icon should render an SVG const svg = container.querySelector('svg'); @@ -208,11 +232,11 @@ describe('Basic Renderers - Display Issue Detection', () => { }); it('should apply size classes correctly', () => { - const { container } = renderComponent({ + const { container } = renderComponent(iconNode({ type: 'icon', icon: 'heart', size: 24, - }); + })); const svg = container.querySelector('svg'); expect(svg).toBeTruthy(); @@ -227,24 +251,24 @@ describe('Basic Renderers - Display Issue Detection', () => { }); it('should render separator with proper role', () => { - const { container } = renderComponent({ + const { container } = renderComponent(separatorNode({ type: 'separator', - }); + })); const separator = container.querySelector('[role="separator"], hr, [data-orientation]'); expect(separator).toBeTruthy(); }); it('should support both orientations', () => { - const { container: horizontal } = renderComponent({ + const { container: horizontal } = renderComponent(separatorNode({ type: 'separator', orientation: 'horizontal', - }); + })); - const { container: vertical } = renderComponent({ + const { container: vertical } = renderComponent(separatorNode({ type: 'separator', orientation: 'vertical', - }); + })); expect(horizontal.querySelector('*')).toBeTruthy(); expect(vertical.querySelector('*')).toBeTruthy(); @@ -258,10 +282,10 @@ describe('Basic Renderers - Display Issue Detection', () => { }); it('should render HTML content safely', () => { - const { container } = renderComponent({ + const { container } = renderComponent(htmlNode({ type: 'html', html: '

HTML Content

', - }); + })); // HTML renderer uses dangerouslySetInnerHTML const hasContent = container.querySelector('p') || container.textContent?.includes('HTML Content'); diff --git a/packages/components/src/__tests__/data-table-row-click.test.tsx b/packages/components/src/__tests__/data-table-row-click.test.tsx index 85b603b582..32cbcc031c 100644 --- a/packages/components/src/__tests__/data-table-row-click.test.tsx +++ b/packages/components/src/__tests__/data-table-row-click.test.tsx @@ -25,6 +25,15 @@ import { renderComponent } from './test-utils'; // cold transform is billed to `hookTimeout`, which is why this carried a raised // timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). import '../renderers'; +import type { DataTableSchema } from '@object-ui/types'; + +/** + * Types a literal below as the node it is rather than as the `BaseSchema` + * `renderComponent` accepts (objectui#11347): once `BaseSchema`'s index + * signature is gone (objectui#8347), a literal checked against `BaseSchema` + * may author only `BaseSchema`'s own keys. + */ +const dataTableNode = (schema: DataTableSchema): DataTableSchema => schema; describe('data-table — row click heuristic ignores overlay items', () => { const baseSchema = { @@ -85,12 +94,12 @@ describe('data-table — row click heuristic ignores overlay items', () => { it('clicking a plain text cell still calls onRowClick (sanity)', () => { const onRowClick = vi.fn(); - const { container } = renderComponent({ + const { container } = renderComponent(dataTableNode({ type: 'data-table', columns: [{ header: 'Name', accessorKey: 'name' }], data: [{ id: '1', name: 'Alice' }], onRowClick, - }); + })); // Click on the name cell text (not a button/link/menu). const cell = Array.from(container.querySelectorAll('td')).find((td) => diff --git a/packages/components/src/__tests__/empty-action-declared-7105.test.tsx b/packages/components/src/__tests__/empty-action-declared-7105.test.tsx index 85e5e58b14..be9d2a61de 100644 --- a/packages/components/src/__tests__/empty-action-declared-7105.test.tsx +++ b/packages/components/src/__tests__/empty-action-declared-7105.test.tsx @@ -72,7 +72,7 @@ import { render } from '@testing-library/react'; import { ComponentRegistry } from '@object-ui/core'; import { SchemaRenderer } from '@object-ui/react'; import { EmptySchema as EmptyMirror } from '@object-ui/types/zod'; -import type { EmptySchema, SchemaNode } from '@object-ui/types'; +import type { ButtonSchema, EmptySchema, SchemaNode } from '@object-ui/types'; import '../renderers'; const HERE = dirname(fileURLToPath(import.meta.url)); @@ -101,16 +101,20 @@ type _ActionIsSchemaNode = Expect< Equal< EmptySchema['action'], SchemaNode > >; * The shipped demo, as a literal the compiler checks against the declared type. * The first test asserts it is byte-identical to the file the docs page renders. */ +const DEMO_ACTION: ButtonSchema = { + type: 'button', + label: 'Create Project', + variant: 'default', +}; +// `DEMO_ACTION` is typed as the `button` node it is (objectui#11347): the slot is +// `SchemaNode`, and once `BaseSchema`'s index signature is gone (objectui#8347) +// a literal checked against it may author only `BaseSchema`'s own keys. const DEMO = { type: 'empty', icon: 'folder-plus', title: 'No projects yet', description: 'Get started by creating your first project', - action: { - type: 'button', - label: 'Create Project', - variant: 'default', - }, + action: DEMO_ACTION, } satisfies EmptySchema; const readDemo = (): unknown => JSON.parse(readFileSync(DEMO_PATH, 'utf8')); diff --git a/packages/components/src/__tests__/form-renderers.test.tsx b/packages/components/src/__tests__/form-renderers.test.tsx index 58353e83c6..5371a8e0c1 100644 --- a/packages/components/src/__tests__/form-renderers.test.tsx +++ b/packages/components/src/__tests__/form-renderers.test.tsx @@ -18,6 +18,31 @@ import { // cold transform is billed to `hookTimeout`, which is why this carried a raised // timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). import '../renderers'; +import type { + ButtonSchema, + InputSchema, + LabelSchema, + RadioGroupSchema, + SelectSchema, + SliderSchema, + TextSchema, +} from '@object-ui/types'; + +/** + * Types a literal below as the node it is rather than as the `BaseSchema` + * `renderComponent` accepts, children included (objectui#11347): once + * `BaseSchema`'s index signature is gone (objectui#8347), a literal checked + * against `BaseSchema` or `SchemaNode` may author only `BaseSchema`'s own keys. + * The literals that deliberately author a retired or undeclared key stay + * untyped, on purpose. + */ +const buttonNode = (schema: ButtonSchema): ButtonSchema => schema; +const inputNode = (schema: InputSchema): InputSchema => schema; +const labelNode = (schema: LabelSchema): LabelSchema => schema; +const radioGroupNode = (schema: RadioGroupSchema): RadioGroupSchema => schema; +const selectNode = (schema: SelectSchema): SelectSchema => schema; +const sliderNode = (schema: SliderSchema): SliderSchema => schema; +const textNode = (schema: TextSchema): TextSchema => schema; /** * Comprehensive tests for form renderer components @@ -47,14 +72,14 @@ describe('Form Renderers - Display Issue Detection', () => { }); it('should support different variants', () => { - const variants = ['default', 'secondary', 'destructive', 'outline', 'ghost', 'link']; + const variants: NonNullable[] = ['default', 'secondary', 'destructive', 'outline', 'ghost', 'link']; variants.forEach(variant => { - const { container } = renderComponent({ + const { container } = renderComponent(buttonNode({ type: 'button', label: 'Test', variant, - }); + })); const button = container.querySelector('button'); expect(button).toBeTruthy(); @@ -62,14 +87,14 @@ describe('Form Renderers - Display Issue Detection', () => { }); it('should support different sizes', () => { - const sizes = ['default', 'sm', 'lg', 'icon']; + const sizes: NonNullable[] = ['default', 'sm', 'lg', 'icon']; sizes.forEach(size => { - const { container } = renderComponent({ + const { container } = renderComponent(buttonNode({ type: 'button', label: 'Test', size, - }); + })); const button = container.querySelector('button'); expect(button).toBeTruthy(); @@ -77,10 +102,10 @@ describe('Form Renderers - Display Issue Detection', () => { }); it('should render children when no label provided', () => { - const { container } = renderComponent({ + const { container } = renderComponent(buttonNode({ type: 'button', - children: [{ type: 'text', content: 'Child Content' }], - }); + children: [textNode({ type: 'text', content: 'Child Content' })], + })); expect(container.textContent).toContain('Child Content'); }); @@ -110,12 +135,12 @@ describe('Form Renderers - Display Issue Detection', () => { }); it('should show required indicator when required', () => { - const { container } = renderComponent({ + const { container } = renderComponent(inputNode({ type: 'input', label: 'Required Field', name: 'required', required: true, - }); + })); const label = container.querySelector('label'); // The label should have a required indicator (*) @@ -123,14 +148,14 @@ describe('Form Renderers - Display Issue Detection', () => { }); it('should support different input types', () => { - const types = ['text', 'email', 'password', 'number', 'tel', 'url', 'date']; + const types: NonNullable[] = ['text', 'email', 'password', 'number', 'tel', 'url', 'date']; types.forEach(inputType => { - const { container } = renderComponent({ + const { container } = renderComponent(inputNode({ type: 'input', label: 'Test', inputType, - }); + })); const input = container.querySelector('input'); expect(input?.getAttribute('type')).toBe(inputType); @@ -148,11 +173,11 @@ describe('Form Renderers - Display Issue Detection', () => { }); it('should display error message when provided', () => { - const { container } = renderComponent({ + const { container } = renderComponent(inputNode({ type: 'input', label: 'Field', error: 'This field has an error', - }); + })); expect(container.textContent).toContain('This field has an error'); const error = container.querySelector('.text-destructive'); @@ -177,11 +202,11 @@ describe('Form Renderers - Display Issue Detection', () => { }); it('should handle readonly state', () => { - const { container } = renderComponent({ + const { container } = renderComponent(inputNode({ type: 'input', label: 'Field', readOnly: true, - }); + })); const input = container.querySelector('input'); expect(input?.hasAttribute('readonly')).toBe(true); @@ -261,14 +286,14 @@ describe('Form Renderers - Display Issue Detection', () => { }); it('should render select with options', () => { - const { container } = renderComponent({ + const { container } = renderComponent(selectNode({ type: 'select', label: 'Choose option', options: [ { value: '1', label: 'Option 1' }, { value: '2', label: 'Option 2' }, ], - }); + })); // Select component should be present expect(container.querySelector('[role="combobox"]') || container.querySelector('select')).toBeTruthy(); @@ -317,14 +342,14 @@ describe('Form Renderers - Display Issue Detection', () => { }); it('should render radio group with options', () => { - const { container } = renderComponent({ + const { container } = renderComponent(radioGroupNode({ type: 'radio-group', label: 'Choose one', options: [ { value: 'a', label: 'Option A' }, { value: 'b', label: 'Option B' }, ], - }); + })); const radioGroup = container.querySelector('[role="radiogroup"]'); expect(radioGroup).toBeTruthy(); @@ -338,12 +363,12 @@ describe('Form Renderers - Display Issue Detection', () => { }); it('should render slider component', () => { - const { container } = renderComponent({ + const { container } = renderComponent(sliderNode({ type: 'slider', label: 'Volume', min: 0, max: 100, - }); + })); const slider = container.querySelector('[role="slider"], input[type="range"]'); expect(slider).toBeTruthy(); @@ -357,10 +382,10 @@ describe('Form Renderers - Display Issue Detection', () => { }); it('should render label element', () => { - const { container } = renderComponent({ + const { container } = renderComponent(labelNode({ type: 'label', text: 'Form Label', - }); + })); const label = container.querySelector('label'); expect(label).toBeTruthy(); diff --git a/packages/components/src/__tests__/list-title-declared-11347.test.tsx b/packages/components/src/__tests__/list-title-declared-11347.test.tsx new file mode 100644 index 0000000000..5ebd3b09f2 --- /dev/null +++ b/packages/components/src/__tests__/list-title-declared-11347.test.tsx @@ -0,0 +1,76 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * `list` draws an authored `title`, and `ListSchema` declares it (objectui#11347). + * + * The renderer has read `schema.title` all along, drawing it as a heading above + * the list, and the `list` registration publishes it as an input. Until + * objectui#11347 no declaration carried it, so it survived only on + * `BaseSchema`'s index signature, which objectui#8347 removes. The installed + * `@objectstack/spec` has no row for `list`, and a published producer authors + * the key: the `skills/objectui` expressions guide teaches + * `{ "type": "list", "title": "Team", "ordered": true, ... }`. So the key is + * declared where the renderer and the registry already honour it, rather than + * retired. + * + * Three readers pin it: + * - the runtime: the guide's literal, rendered through the real + * `SchemaRenderer`, draws its title, and a node without one draws none; + * - the zod mirror: a string `title` parses, and a number is refused at `title`; + * - the compiler: `tsc -p tsconfig.test.json` reads the typed literal and + * the `@ts-expect-error` line below. Vitest cannot see either. + */ + +import { describe, it, expect } from 'vitest'; +import { render } from '@testing-library/react'; +import { SchemaRenderer } from '@object-ui/react'; +import { ListSchema as ListMirror } from '@object-ui/types/zod'; +import type { ListSchema } from '@object-ui/types'; +// Registers the renderers at module scope, NOT inside a `beforeAll`: there the +// cold transform is billed to `hookTimeout` (object-ui/no-dynamic-import-in-test-hook, +// objectui#3010). +import '../renderers'; + +/** The literal the expressions guide teaches, typed against the declaration. */ +const GUIDE_LIST: ListSchema = { + type: 'list', + title: 'Team', + ordered: true, + items: [{ content: 'Ada' }, { content: 'Linus' }], +}; + +// The declaration is a STRING: a number is refused on the TypeScript face, with +// or without `BaseSchema`'s index signature, because a declared member wins. +// @ts-expect-error -- `title` is declared `string` +const _NUMERIC_TITLE: ListSchema = { type: 'list', items: [], title: 42 }; +void _NUMERIC_TITLE; + +describe('list draws its declared `title` (objectui#11347)', () => { + it("draws the expressions guide's title as the heading above the list", () => { + const { container } = render(); + const heading = container.querySelector('h3'); + expect(heading?.textContent).toBe('Team'); + // The list itself still renders, under the heading. + expect(container.querySelectorAll('ol > li')).toHaveLength(2); + }); + + it('LIVE CONTROL: a list without a title draws no heading', () => { + const untitled: ListSchema = { ...GUIDE_LIST, title: undefined }; + const { container } = render(); + expect(container.querySelector('h3')).toBeNull(); + expect(container.querySelectorAll('ol > li')).toHaveLength(2); + }); + + it('the zod mirror declares it: a string parses, and a number is refused at `title`', () => { + expect(ListMirror.safeParse(GUIDE_LIST).success).toBe(true); + const refused = ListMirror.safeParse({ type: 'list', items: [], title: 42 }); + expect(refused.success).toBe(false); + expect(refused.error?.issues.map((i) => i.path.join('.'))).toContain('title'); + }); +}); diff --git a/packages/components/src/__tests__/required-marker-markup.test.tsx b/packages/components/src/__tests__/required-marker-markup.test.tsx index 893f1db926..b9ff081338 100644 --- a/packages/components/src/__tests__/required-marker-markup.test.tsx +++ b/packages/components/src/__tests__/required-marker-markup.test.tsx @@ -54,6 +54,18 @@ import { renderComponent } from './test-utils'; // Registers the renderers at module scope, not in a hook // (object-ui/no-dynamic-import-in-test-hook, objectui#3010). import '../renderers'; +import type { CheckboxSchema, InputSchema, SelectSchema, TextareaSchema } from '@object-ui/types'; + +/** + * Types a literal below as the node it is rather than as the `BaseSchema` + * `renderComponent` accepts (objectui#11347): once `BaseSchema`'s index + * signature is gone (objectui#8347), a literal checked against `BaseSchema` + * may author only `BaseSchema`'s own keys. + */ +const inputNode = (schema: InputSchema): InputSchema => schema; +const textareaNode = (schema: TextareaSchema): TextareaSchema => schema; +const checkboxNode = (schema: CheckboxSchema): CheckboxSchema => schema; +const selectNode = (schema: SelectSchema): SelectSchema => schema; afterEach(cleanup); @@ -106,19 +118,19 @@ const SITES: Site[] = [ }, { site: 'input (renderers/form/input.tsx)', - render: (required) => renderComponent({ type: 'input', id: 'in-ctl', label: 'Title', required }), + render: (required) => renderComponent(inputNode({ type: 'input', id: 'in-ctl', label: 'Title', required })), label: () => document.querySelector('label[for="in-ctl"]'), expectRequiredState: () => expect(document.getElementById('in-ctl')).toHaveAttribute('required'), }, { site: 'textarea (renderers/form/textarea.tsx)', - render: (required) => renderComponent({ type: 'textarea', id: 'ta-ctl', label: 'Title', required }), + render: (required) => renderComponent(textareaNode({ type: 'textarea', id: 'ta-ctl', label: 'Title', required })), label: () => document.querySelector('label[for="ta-ctl"]'), expectRequiredState: () => expect(document.getElementById('ta-ctl')).toHaveAttribute('required'), }, { site: 'checkbox (renderers/form/checkbox.tsx)', - render: (required) => renderComponent({ type: 'checkbox', id: 'cb-ctl', label: 'Title', required }), + render: (required) => renderComponent(checkboxNode({ type: 'checkbox', id: 'cb-ctl', label: 'Title', required })), label: () => document.querySelector('label[for="cb-ctl"]'), // Radix writes `aria-required` on the role=checkbox button from `required`. expectRequiredState: () => expect(screen.getByRole('checkbox')).toHaveAttribute('aria-required', 'true'), @@ -126,13 +138,13 @@ const SITES: Site[] = [ { site: 'select (renderers/form/select.tsx)', render: (required) => - renderComponent({ + renderComponent(selectNode({ type: 'select', id: 'sel-ctl', label: 'Title', required, options: [{ label: 'A', value: 'a' }], - }), + })), // The label names the trigger through `for` since objectui#10435, whose // own pin (`select-label-association.test.tsx`) reads the resulting name. label: () => document.querySelector('label[for="sel-ctl"]'), diff --git a/packages/components/src/__tests__/span-children-rendering.test.tsx b/packages/components/src/__tests__/span-children-rendering.test.tsx index 7372d93211..2345834e4f 100644 --- a/packages/components/src/__tests__/span-children-rendering.test.tsx +++ b/packages/components/src/__tests__/span-children-rendering.test.tsx @@ -36,12 +36,20 @@ import { describe, it, expect } from 'vitest'; import { render } from '@testing-library/react'; import { SchemaRenderer } from '@object-ui/react'; -import type { TextSpanSchema } from '@object-ui/types'; +import type { TextSchema, TextSpanSchema } from '@object-ui/types'; // Registers the renderers at module scope, NOT inside a `beforeAll` — there the // cold transform is billed to `hookTimeout`. See // object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). import '../renderers'; +/** + * Types a child literal below as the `text` node it is (objectui#11347): a + * `children` entry is checked against `SchemaNode`, and once `BaseSchema`'s + * index signature is gone (objectui#8347) a literal checked against it may + * author only `BaseSchema`'s own keys. + */ +const textNode = (schema: TextSchema): TextSchema => schema; + /** Renders a `kind:'html'` page — source compiled by the parser, then rendered. */ function renderHtmlPage(source: string) { return render(); @@ -69,7 +77,7 @@ describe('span renders its canonical child key (#5027)', () => { const schema: TextSpanSchema = { type: 'span', className: 'json-authored', - children: [{ type: 'text', content: 'inline from children' }], + children: [textNode({ type: 'text', content: 'inline from children' })], }; const { container } = render(); @@ -83,7 +91,7 @@ describe('span renders its canonical child key (#5027)', () => { const schema: TextSpanSchema = { type: 'span', className: 'single-child', - children: { type: 'text', content: 'lone child' }, + children: textNode({ type: 'text', content: 'lone child' }), }; const { container } = render(); diff --git a/packages/components/src/__tests__/span-value-fallback.test.tsx b/packages/components/src/__tests__/span-value-fallback.test.tsx index c576729567..e621594a81 100644 --- a/packages/components/src/__tests__/span-value-fallback.test.tsx +++ b/packages/components/src/__tests__/span-value-fallback.test.tsx @@ -43,12 +43,20 @@ import { describe, it, expect } from 'vitest'; import { render } from '@testing-library/react'; import { SchemaRenderer } from '@object-ui/react'; -import type { TextSpanSchema } from '@object-ui/types'; +import type { TextSchema, TextSpanSchema } from '@object-ui/types'; // Registers the renderers at module scope, NOT inside a `beforeAll` — there the // cold transform is billed to `hookTimeout`. See // object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). import '../renderers'; +/** + * Types a child literal below as the `text` node it is (objectui#11347): a + * `children` entry is checked against `SchemaNode`, and once `BaseSchema`'s + * index signature is gone (objectui#8347) a literal checked against it may + * author only `BaseSchema`'s own keys. + */ +const textNode = (schema: TextSchema): TextSchema => schema; + describe('span reads its declared `value` key (#5050)', () => { it('renders the plain `{ type: "span", value }` the card reports rendering empty', () => { // The reproduction from the card, written exactly as `TextSpanSchema` and @@ -76,7 +84,7 @@ describe('span reads its declared `value` key (#5050)', () => { type: 'span', className: 'both-keys', value: 'value must not render', - children: [{ type: 'text', content: 'children win' }], + children: [textNode({ type: 'text', content: 'children win' })], }; const { container } = render(); @@ -95,7 +103,7 @@ describe('span reads its declared `value` key (#5050)', () => { type: 'span', className: 'single-child-wins', value: 'value must not render', - children: { type: 'text', content: 'lone child wins' }, + children: textNode({ type: 'text', content: 'lone child wins' }), }; const { container } = render(); diff --git a/packages/components/src/renderers/__tests__/node-slot-numeric-falsy.test.tsx b/packages/components/src/renderers/__tests__/node-slot-numeric-falsy.test.tsx index 0c6c1fa0ea..16d2b57762 100644 --- a/packages/components/src/renderers/__tests__/node-slot-numeric-falsy.test.tsx +++ b/packages/components/src/renderers/__tests__/node-slot-numeric-falsy.test.tsx @@ -102,6 +102,10 @@ interface Site { * `body` STAYS — that key is `PageCardProps.body`, a read-only back-compat path * for stored documents under a different retirement (objectstack#5775, ADR-0087 * D2), and it is still a live node slot. + * + * ⚠️ `drawer footer` LEFT this table the same way: objectui#11347 retired the + * drawer's `footer` read, so the slot ceased to exist rather than stopped + * leaking. */ const SITES: readonly Site[] = [ { name: 'container children', type: 'container', namespace: 'ui', slot: 'children' }, @@ -115,7 +119,6 @@ const SITES: readonly Site[] = [ { name: 'page:card footer', type: 'card', namespace: 'page', slot: 'footer' }, { name: 'dialog footer', type: 'dialog', namespace: 'ui', slot: 'footer', base: { defaultOpen: true } }, { name: 'sheet footer', type: 'sheet', namespace: 'ui', slot: 'footer', base: { defaultOpen: true } }, - { name: 'drawer footer', type: 'drawer', namespace: 'ui', slot: 'footer', base: { defaultOpen: true } }, { name: 'table footer', type: 'table', diff --git a/packages/components/src/renderers/data-display/list.tsx b/packages/components/src/renderers/data-display/list.tsx index 21ff7dc82a..f88567b468 100644 --- a/packages/components/src/renderers/data-display/list.tsx +++ b/packages/components/src/renderers/data-display/list.tsx @@ -25,6 +25,9 @@ ComponentRegistry.register('list', return (
+ {/* `title` is a declared `ListSchema` member (objectui#11347): the + published `skills/objectui` expressions guide authors it, so the + read is kept and the type now says so. */} {schema.title && (

{schema.title} diff --git a/packages/components/src/renderers/feedback/loading.tsx b/packages/components/src/renderers/feedback/loading.tsx index 5318aad515..11bf6eb5c2 100644 --- a/packages/components/src/renderers/feedback/loading.tsx +++ b/packages/components/src/renderers/feedback/loading.tsx @@ -26,8 +26,13 @@ ComponentRegistry.register('loading', size === ('xl' as any) && 'h-16 w-16' )} /> - {schema.text && ( -

{schema.text}

+ {/* The message is the DECLARED `label` (`LoadingSchema.label`, "Loading + text/message", on both faces). The renderer used to read an + undeclared `text` here and leave `label` inert; no producer authors + either spelling, so the read moved to the declared one and `text` + is retired, with no alias (objectui#11347). */} + {schema.label && ( +

{schema.label}

)}

); @@ -53,7 +58,7 @@ ComponentRegistry.register('loading', namespace: 'ui', label: 'Loading', inputs: [ - { name: 'text', type: 'string' }, + { name: 'label', type: 'string' }, { name: 'size', type: 'enum', @@ -64,7 +69,7 @@ ComponentRegistry.register('loading', { name: 'className', type: 'string' } ], defaultProps: { - text: 'Loading...', + label: 'Loading...', size: 'md', fullscreen: false } diff --git a/packages/components/src/renderers/form/__tests__/select-label-association.test.tsx b/packages/components/src/renderers/form/__tests__/select-label-association.test.tsx index d5941deae0..7e5bbd85d8 100644 --- a/packages/components/src/renderers/form/__tests__/select-label-association.test.tsx +++ b/packages/components/src/renderers/form/__tests__/select-label-association.test.tsx @@ -35,6 +35,15 @@ import { renderComponent } from '../../../__tests__/test-utils'; // Registers `select` at module scope, not in a hook // (object-ui/no-dynamic-import-in-test-hook, objectui#3010). import '../../../renderers'; +import type { SelectSchema } from '@object-ui/types'; + +/** + * Types a literal below as the node it is rather than as the `BaseSchema` + * `renderComponent` and `SchemaRenderer` accept (objectui#11347): once `BaseSchema`'s index + * signature is gone (objectui#8347), a literal checked against `BaseSchema` + * may author only `BaseSchema`'s own keys. + */ +const selectNode = (schema: SelectSchema): SelectSchema => schema; afterEach(cleanup); @@ -45,7 +54,7 @@ const OPTIONS = [ describe('ui:select — label names the combobox (objectui#10435)', () => { it('is found by getByRole combobox with the label as its name', () => { - renderComponent({ type: 'select', id: 'title', label: 'Title', options: OPTIONS }); + renderComponent(selectNode({ type: 'select', id: 'title', label: 'Title', options: OPTIONS })); const combobox = screen.getByRole('combobox', { name: 'Title' }); expect(combobox).toHaveAccessibleName('Title'); @@ -53,7 +62,7 @@ describe('ui:select — label names the combobox (objectui#10435)', () => { }); it('keeps the name `Title` when required: the aria-hidden marker stays out of it', () => { - renderComponent({ type: 'select', id: 'title', label: 'Title', required: true, options: OPTIONS }); + renderComponent(selectNode({ type: 'select', id: 'title', label: 'Title', required: true, options: OPTIONS })); // The marker is still drawn inside the label (objectui#10368) ... const label = document.querySelector('label[for="title"]'); @@ -67,7 +76,7 @@ describe('ui:select — label names the combobox (objectui#10435)', () => { }); it('points `for` at an id that really exists on the trigger', () => { - renderComponent({ type: 'select', id: 'title', label: 'Title', options: OPTIONS }); + renderComponent(selectNode({ type: 'select', id: 'title', label: 'Title', options: OPTIONS })); const label = document.querySelector('label[for]'); expect(label).not.toBeNull(); @@ -81,7 +90,7 @@ describe('ui:select — label names the combobox (objectui#10435)', () => { }); it('names the combobox with the label, not the placeholder text inside it', () => { - renderComponent({ type: 'select', id: 'title', label: 'Title', placeholder: 'Choose…', options: OPTIONS }); + renderComponent(selectNode({ type: 'select', id: 'title', label: 'Title', placeholder: 'Choose…', options: OPTIONS })); const combobox = screen.getByRole('combobox'); expect(combobox).toHaveTextContent('Choose…'); @@ -89,7 +98,7 @@ describe('ui:select — label names the combobox (objectui#10435)', () => { }); it('names the combobox with the label, not the selected value text inside it', () => { - renderComponent({ type: 'select', id: 'title', label: 'Title', value: 'ms', options: OPTIONS }); + renderComponent(selectNode({ type: 'select', id: 'title', label: 'Title', value: 'ms', options: OPTIONS })); const combobox = screen.getByRole('combobox'); expect(combobox).toHaveTextContent('Ms'); @@ -101,7 +110,7 @@ describe('ui:select — label names the combobox (objectui#10435)', () => { // `schema.id`, so without one the renderer neither writes `for` nor mints // an `id`, and the combobox stays unnamed, as before this change. Pinned so // a later change to that answer is a visible decision, not a drift. - renderComponent({ type: 'select', label: 'Title', placeholder: 'Choose…', options: OPTIONS }); + renderComponent(selectNode({ type: 'select', label: 'Title', placeholder: 'Choose…', options: OPTIONS })); const combobox = screen.getByRole('combobox'); expect(combobox).toHaveTextContent('Choose…'); @@ -115,7 +124,7 @@ describe('ui:select — label names the combobox (objectui#10435)', () => { it('survives the real render path through SchemaRenderer', () => { render( , ); diff --git a/packages/components/src/renderers/form/input-otp.tsx b/packages/components/src/renderers/form/input-otp.tsx index b24d8c02e8..96851d019f 100644 --- a/packages/components/src/renderers/form/input-otp.tsx +++ b/packages/components/src/renderers/form/input-otp.tsx @@ -16,7 +16,12 @@ ComponentRegistry.register('input-otp', // `style` forwarded by name; the rest through the form-control // declaration (objectui#5632). const { style, ...otpProps } = props; - const length = schema.maxLength || 6; + // The slot count is the DECLARED `length` (`InputOTPSchema.length`, + // `@default 6`), the spelling every catalog entry and the docs page author. + // This used to read an undeclared `maxLength` and ignore `length`, so the + // published 4- and 8-digit examples drew six slots (objectui#11347). One + // spelling, no alias. + const length = schema.length || 6; const slots = Array.from({ length }); const handleChange = (val: string) => { @@ -46,11 +51,11 @@ ComponentRegistry.register('input-otp', namespace: 'ui', label: 'Input OTP', inputs: [ - { name: 'maxLength', type: 'number' }, + { name: 'length', type: 'number' }, { name: 'className', type: 'string' } ], defaultProps: { - maxLength: 6 + length: 6 } } ); diff --git a/packages/components/src/renderers/navigation/sidebar.tsx b/packages/components/src/renderers/navigation/sidebar.tsx index 3a6ebe4df5..bb186aed6c 100644 --- a/packages/components/src/renderers/navigation/sidebar.tsx +++ b/packages/components/src/renderers/navigation/sidebar.tsx @@ -176,9 +176,13 @@ ComponentRegistry.register('sidebar-menu-button', // different mechanism group and a different card. const { style, ...buttonProps } = props; + // ⛔ No `isActive` from the node (objectui#11347). `active` was read off a + // `schema` typed `BaseSchema` itself: no type declares it for this node, + // the installed `@objectstack/spec` has no row for `sidebar-menu-button`, + // and no catalog entry, example, doc page or README authors it, so the read + // rode the index signature alone and was retired. return ( @@ -190,7 +194,6 @@ ComponentRegistry.register('sidebar-menu-button', namespace: 'ui', label: 'Sidebar Menu Button', inputs: [ - { name: 'active', type: 'boolean' }, { name: 'size', type: 'enum', enum: ['default', 'sm', 'lg'] }, { name: 'tooltip', type: 'string' }, CHILDREN_SLOT diff --git a/packages/components/src/renderers/overlay/drawer.tsx b/packages/components/src/renderers/overlay/drawer.tsx index b34c8815a2..5e8abbe192 100644 --- a/packages/components/src/renderers/overlay/drawer.tsx +++ b/packages/components/src/renderers/overlay/drawer.tsx @@ -13,16 +13,24 @@ import { DrawerTrigger, DrawerContent, DrawerHeader, - DrawerFooter, DrawerTitle, - DrawerDescription, - DrawerClose + DrawerDescription } from '../../ui'; -import { renderChildren, renderNodeSlot, renderTriggerSlot } from '../../lib/utils'; +import { renderChildren, renderTriggerSlot } from '../../lib/utils'; ComponentRegistry.register('drawer', ({ schema, className, ...props }: { schema: DrawerSchema; className?: string; [key: string]: any }) => ( - + // ⛔ Three reads retired here (objectui#11347): `shouldScaleBackground`, + // `footer` and `showClose`. `DrawerSchema` declares none of them, the + // installed `@objectstack/spec` has no row for `drawer`, and no catalog + // entry, example, doc page or README authors any of them, so each read + // rode `BaseSchema`'s index signature alone. The drawer therefore draws no + // footer and no Close button from the node. `shouldScaleBackground` is the + // primitive's own prop and still defaults inside the `Drawer` wrapper; the + // node no longer names it, and what an authored one does through + // `SchemaRenderer`'s prop spread is the strict face's question, not this + // read's. + {renderTriggerSlot(DrawerTrigger, schema.trigger)} @@ -30,20 +38,6 @@ ComponentRegistry.register('drawer', {schema.description && {schema.description}} {renderChildren(schema.content)} -{/* ⛔ No `&&` guard on a node slot (objectui#9162): `&&` evaluates to the - slot, so a legal authored `footer: 0` painted the character "0" — - and it short-circuits, so `renderChildren`'s own falsy leg never - ran. `renderNodeSlot` runs the wrapper only when the slot has - content, so the chrome disappears with it. */} - {renderNodeSlot(schema.footer, (footer) => ( - - {renderChildren(footer)} - {/* `showClose` is a declared BOOLEAN, not a node slot — its `&&` - is not the objectui#9162 construct and stays. Its coupling to - `footer` is pre-existing behaviour and is unchanged here. */} - {schema.showClose && } - - ))} ), @@ -53,18 +47,13 @@ ComponentRegistry.register('drawer', inputs: [ { name: 'title', type: 'string' }, { name: 'description', type: 'string' }, - { name: 'shouldScaleBackground', type: 'boolean' }, { name: 'defaultOpen', type: 'boolean' }, - { name: 'showClose', type: 'boolean' }, { name: 'trigger', type: 'slot' }, { name: 'content', type: 'slot' }, - { - name: 'footer', - type: 'slot' }, { name: 'className', type: 'string' } ], defaultProps: { diff --git a/packages/components/src/renderers/overlay/dropdown-menu.tsx b/packages/components/src/renderers/overlay/dropdown-menu.tsx index 692738b467..7476558664 100644 --- a/packages/components/src/renderers/overlay/dropdown-menu.tsx +++ b/packages/components/src/renderers/overlay/dropdown-menu.tsx @@ -83,6 +83,10 @@ const renderMenuItems = (items: MenuItem[] | undefined) => { }); }; +// ⛔ No `modal` read (objectui#11347). `DropdownMenuSchema` declares no `modal`, the +// installed `@objectstack/spec` has no row for `dropdown-menu`, and no catalog entry, +// example, doc page or README authors one, so `modal={schema.modal}` rode +// `BaseSchema`'s index signature alone and was retired. ComponentRegistry.register('dropdown-menu', ({ schema, className, ...props }: { schema: DropdownMenuSchema; className?: string; [key: string]: any }) => { // Read-time resolution against the display locale (objectui#4580 revised @@ -92,7 +96,7 @@ ComponentRegistry.register('dropdown-menu', // `DropdownMenuContent` lazily. The body became a block only to host this hook. const locale = useDisplayLocale(); return ( - + {renderTriggerSlot(DropdownMenuTrigger, schema.trigger)} {schema.label && ( diff --git a/packages/components/src/renderers/overlay/popover.tsx b/packages/components/src/renderers/overlay/popover.tsx index 5e453619ee..9ab2ae4495 100644 --- a/packages/components/src/renderers/overlay/popover.tsx +++ b/packages/components/src/renderers/overlay/popover.tsx @@ -15,9 +15,13 @@ import { } from '../../ui'; import { renderChildren, renderTriggerSlot } from '../../lib/utils'; +// ⛔ No `modal` read (objectui#11347). `PopoverSchema` declares no `modal`, the +// installed `@objectstack/spec` has no row for `popover`, and no catalog entry, +// example, doc page or README authors one, so `modal={schema.modal}` rode +// `BaseSchema`'s index signature alone and was retired. ComponentRegistry.register('popover', ({ schema, className, ...props }: { schema: PopoverSchema; className?: string; [key: string]: any }) => ( - + {renderTriggerSlot(PopoverTrigger, schema.trigger)} {renderChildren(schema.content)} @@ -28,7 +32,6 @@ ComponentRegistry.register('popover', namespace: 'ui', label: 'Popover', inputs: [ - { name: 'modal', type: 'boolean' }, { name: 'defaultOpen', type: 'boolean' }, { name: 'align', type: 'enum', enum: ['start', 'center', 'end'] }, { name: 'side', type: 'enum', enum: ['top', 'right', 'bottom', 'left'] }, diff --git a/packages/components/src/renderers/overlay/sheet.tsx b/packages/components/src/renderers/overlay/sheet.tsx index dc027f7909..b4b95baef6 100644 --- a/packages/components/src/renderers/overlay/sheet.tsx +++ b/packages/components/src/renderers/overlay/sheet.tsx @@ -19,9 +19,13 @@ import { } from '../../ui'; import { renderChildren, renderNodeSlot, renderTriggerSlot } from '../../lib/utils'; +// ⛔ No `modal` read (objectui#11347). `SheetSchema` declares no `modal`, the +// installed `@objectstack/spec` has no row for `sheet`, and no catalog entry, +// example, doc page or README authors one, so `modal={schema.modal}` rode +// `BaseSchema`'s index signature alone and was retired. ComponentRegistry.register('sheet', ({ schema, className, ...props }: { schema: SheetSchema; className?: string; [key: string]: any }) => ( - + {renderTriggerSlot(SheetTrigger, schema.trigger)} @@ -49,7 +53,6 @@ ComponentRegistry.register('sheet', { name: 'title', type: 'string' }, { name: 'description', type: 'string' }, { name: 'side', type: 'enum', enum: ['top', 'right', 'bottom', 'left'] }, - { name: 'modal', type: 'boolean' }, { name: 'defaultOpen', type: 'boolean' }, { name: 'trigger', @@ -68,7 +71,6 @@ ComponentRegistry.register('sheet', title: 'Sheet Title', description: 'Sheet description', side: 'right', - modal: true, trigger: [{ type: 'button', label: 'Open Sheet' }], content: [{ type: 'text', content: 'Sheet content goes here' }] } 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 15e5a86db8..187bcfe873 100644 --- a/packages/types/src/__tests__/layout-default-jsdoc-7361.test.ts +++ b/packages/types/src/__tests__/layout-default-jsdoc-7361.test.ts @@ -706,8 +706,8 @@ describe('objectui#8318 — the `@default`s the "no renderer reads it" list got * | `CRUDDialogSchema.size` | no `crud-dialog` registration at all; | * | `.closeOnOutsideClick` | plus, per key, its own name census: | * | `.closeOnEscape` | zero occurrences for the middle two, and | - * | `.showClose` | for `showClose` one occurrence that | - * | | belongs to `DrawerSchema` | + * | `.showClose` | for `showClose` a look-alike `drawer` | + * | | read, retired since objectui#11347 | * * ## Derivation — the two `loading` rows * @@ -746,8 +746,9 @@ describe('objectui#8318 — the `@default`s the "no renderer reads it" list got * header of `handler-keys-string-any-mirrors-7344.test.ts`. What IS derived here * is everything reachable from a named file: that the `card` primitive does not * consume `variant`, that `page`'s allow-list does not carry `isDefault`, that - * `ActionRunner` reads `method` but not `level`, and that the one `showClose` - * read in the tree belongs to `DrawerSchema`. + * `ActionRunner` reads `method` but not `level`, and that the `drawer` + * renderer, whose look-alike `showClose` read this row once had to rule out, + * no longer reads the key at all (objectui#11347 retired it). */ const UI_CARD = 'packages/components/src/ui/card.tsx'; const UI_ALERT = 'packages/components/src/ui/alert.tsx'; @@ -965,21 +966,24 @@ describe('objectui#8318 ruling — nine `@default`s corrected against a measured }); /** - * The one of the four whose census is NOT empty, which is why its wording - * differs: crediting this declaration with `DrawerSchema`'s reader is the - * exact mistake a name-only census makes. + * The one of the four whose census was NOT empty, which is why its wording + * differs: crediting this declaration with the `drawer` renderer's reader + * was the exact mistake a name-only census makes. objectui#11347 retired + * that look-alike read, and the docblock says so. */ - it('row 24 — `showClose` publishes no tag and names the look-alike reader', () => { + it('row 24 — `showClose` publishes no tag and names the retired look-alike reader', () => { expectAbsentTagWithEvidence(interfaceBody(crud, 'CRUDDialogSchema'), 'showClose', [ 'drawer.tsx', 'DrawerSchema', + 'objectui#11347', ]); }); - it('the look-alike really exists, and really is another declaration', () => { + it('the look-alike read is retired: the drawer renderer no longer reads `schema.showClose`', () => { const drawer = read(DRAWER); - expect(drawer).toMatch(/schema\.showClose/); + // FIRING CONTROL: the file was read, and it is still the drawer renderer. expect(drawer).toContain('DrawerSchema'); + expect(drawer).not.toMatch(/schema\.showClose/); }); }); }); diff --git a/packages/types/src/crud.ts b/packages/types/src/crud.ts index a43874558d..03fb811ff9 100644 --- a/packages/types/src/crud.ts +++ b/packages/types/src/crud.ts @@ -481,15 +481,12 @@ export interface CRUDDialogSchema extends BaseSchema { * Show close button. * * NO `@default`, deliberately (objectui#8318): the tag published `true` with - * no reader to apply it. This key is the one on this interface whose census - * is NOT empty, and that is exactly why it needs saying: `showClose` has one - * occurrence outside this declaration and its zod twin -- - * `packages/components/src/renderers/overlay/drawer.tsx:38`, - * `{schema.showClose && ...}` -- and that read belongs to - * `DrawerSchema.showClose`, a different member on a different, registered - * node type. Counting it here would be crediting this declaration with - * another one's reader. The missing registration is in the interface docblock - * above. + * no reader to apply it. When that was written this key's census was NOT + * empty: `packages/components/src/renderers/overlay/drawer.tsx` read a + * `showClose` of its own, which was never this declaration's reader (and was + * never declared on `DrawerSchema` either). objectui#11347 retired that + * read, so no renderer reads `showClose` under any node type now. The + * missing registration is in the interface docblock above. */ showClose?: boolean; } diff --git a/packages/types/src/data-display.ts b/packages/types/src/data-display.ts index 6326ec8e93..9c07110390 100644 --- a/packages/types/src/data-display.ts +++ b/packages/types/src/data-display.ts @@ -285,6 +285,19 @@ export interface AvatarSchema extends BaseSchema { */ export interface ListSchema extends BaseSchema { type: 'list'; + /** + * Heading drawn above the list. + * + * READ SITE: `packages/components/src/renderers/data-display/list.tsx`, which + * draws `schema.title` in an `h3` above the list when it is set; the `list` + * registration publishes it as an input. Undeclared until objectui#11347, + * surviving only on `BaseSchema`'s index signature. The installed + * `@objectstack/spec` has no row for `list`, and the published + * `skills/objectui` expressions guide authors `title` on a `list` node, so + * the key is declared where the renderer and the registry already honour it + * rather than retired. + */ + title?: string; /** * List items */ @@ -307,7 +320,7 @@ export interface ListSchema extends BaseSchema { /** * Classes on the wrapper `div` around the title and the list. * - * READ SITE: `packages/components/src/renderers/data-display/list.tsx:27` — + * READ SITE: `packages/components/src/renderers/data-display/list.tsx` — * `cn("space-y-2", schema.wrapperClass)`. Undeclared until * objectui#7722, surviving only on `BaseSchema`'s index signature: the same * key, on the same class of read, that `CheckboxSchema` (`b74a8598d`), diff --git a/packages/types/src/feedback.ts b/packages/types/src/feedback.ts index e80a83f600..8486958dd4 100644 --- a/packages/types/src/feedback.ts +++ b/packages/types/src/feedback.ts @@ -23,7 +23,12 @@ import type { BaseSchema, SchemaNode } from './base.js'; export interface LoadingSchema extends BaseSchema { type: 'loading'; /** - * Loading text/message + * Loading text/message, drawn under the spinner. + * + * READ SITE: `packages/components/src/renderers/feedback/loading.tsx`, which + * draws `schema.label` in a `p` under the spinner. Inert until objectui#11347: + * the renderer read an undeclared `text` instead, which no producer + * authored, so the read moved to this declared spelling with no alias. */ label?: string; /** @@ -52,7 +57,9 @@ export interface LoadingSchema extends BaseSchema { * scores. Every read is filed under the TYPE of the object it is read from; * this declaration carries none. What the renderer DOES read off this node: * `fullscreen`, `size`, `text` (in - * `packages/components/src/renderers/feedback/loading.tsx`). + * `packages/components/src/renderers/feedback/loading.tsx`). The undeclared + * `text` read has since moved to the declared {@link LoadingSchema.label} + * (objectui#11347). * * Before objectui#9256 tombstoned them here, `body` and `children` were both * inherited-and-optional from {@link BaseSchema} — so authoring either here @@ -78,7 +85,9 @@ export interface LoadingSchema extends BaseSchema { * scores. Every read is filed under the TYPE of the object it is read from; * this declaration carries none. What the renderer DOES read off this node: * `fullscreen`, `size`, `text` (in - * `packages/components/src/renderers/feedback/loading.tsx`). + * `packages/components/src/renderers/feedback/loading.tsx`). The undeclared + * `text` read has since moved to the declared {@link LoadingSchema.label} + * (objectui#11347). * * Before objectui#9256 tombstoned them here, `body` and `children` were both * inherited-and-optional from {@link BaseSchema} — so authoring either here diff --git a/packages/types/src/form.ts b/packages/types/src/form.ts index 2fce3dbb30..1738781cc8 100644 --- a/packages/types/src/form.ts +++ b/packages/types/src/form.ts @@ -1443,6 +1443,12 @@ export interface InputOTPSchema extends BaseSchema { label?: string; /** * Number of OTP digits + * + * READ SITE: `packages/components/src/renderers/form/input-otp.tsx`, as the + * slot count. Inert until objectui#11347: the renderer read an undeclared + * `maxLength` and drew six slots whatever `length` said, while every + * catalog entry and the docs page author `length`. The read moved to this + * spelling with no alias, which also makes the `@default` below true. * @default 6 */ length?: number; @@ -1492,7 +1498,9 @@ export interface InputOTPSchema extends BaseSchema { * scores. Every read is filed under the TYPE of the object it is read from; * this declaration carries none. What the renderer DOES read off this node: * `maxLength`, `value` (in - * `packages/components/src/renderers/form/input-otp.tsx`). + * `packages/components/src/renderers/form/input-otp.tsx`). The undeclared + * `maxLength` read has since moved to the declared {@link InputOTPSchema.length} + * (objectui#11347). * * Before objectui#9256 tombstoned them here, `body` and `children` were both * inherited-and-optional from {@link BaseSchema} — so authoring either here @@ -1519,7 +1527,9 @@ export interface InputOTPSchema extends BaseSchema { * scores. Every read is filed under the TYPE of the object it is read from; * this declaration carries none. What the renderer DOES read off this node: * `maxLength`, `value` (in - * `packages/components/src/renderers/form/input-otp.tsx`). + * `packages/components/src/renderers/form/input-otp.tsx`). The undeclared + * `maxLength` read has since moved to the declared {@link InputOTPSchema.length} + * (objectui#11347). * * Before objectui#9256 tombstoned them here, `body` and `children` were both * inherited-and-optional from {@link BaseSchema} — so authoring either here diff --git a/packages/types/src/overlay.ts b/packages/types/src/overlay.ts index ff5fe4f04a..065e71cca6 100644 --- a/packages/types/src/overlay.ts +++ b/packages/types/src/overlay.ts @@ -462,6 +462,8 @@ export interface SheetSchema extends BaseSchema { * `content`, `defaultOpen`, `description`, `footer`, `modal`, `side`, * `title`, `trigger` (in * `packages/components/src/renderers/overlay/sheet.tsx`). + * The `modal` read has since been retired (objectui#11347): it was never a + * member here and no producer authored it. * * Before objectui#9256 tombstoned them here, `body` and `children` were both * inherited-and-optional from {@link BaseSchema} — so authoring either here @@ -489,6 +491,8 @@ export interface SheetSchema extends BaseSchema { * `content`, `defaultOpen`, `description`, `footer`, `modal`, `side`, * `title`, `trigger` (in * `packages/components/src/renderers/overlay/sheet.tsx`). + * The `modal` read has since been retired (objectui#11347): it was never a + * member here and no producer authored it. * * Before objectui#9256 tombstoned them here, `body` and `children` were both * inherited-and-optional from {@link BaseSchema} — so authoring either here @@ -569,6 +573,9 @@ export interface DrawerSchema extends BaseSchema { * `content`, `defaultOpen`, `description`, `footer`, `shouldScaleBackground`, * `showClose`, `title`, `trigger` (in * `packages/components/src/renderers/overlay/drawer.tsx`). + * The `footer`, `shouldScaleBackground` and `showClose` reads have since + * been retired (objectui#11347): none was ever a member here and no producer + * authored any of them. * * Before objectui#9256 tombstoned them here, `body` and `children` were both * inherited-and-optional from {@link BaseSchema} — so authoring either here @@ -596,6 +603,9 @@ export interface DrawerSchema extends BaseSchema { * `content`, `defaultOpen`, `description`, `footer`, `shouldScaleBackground`, * `showClose`, `title`, `trigger` (in * `packages/components/src/renderers/overlay/drawer.tsx`). + * The `footer`, `shouldScaleBackground` and `showClose` reads have since + * been retired (objectui#11347): none was ever a member here and no producer + * authored any of them. * * Before objectui#9256 tombstoned them here, `body` and `children` were both * inherited-and-optional from {@link BaseSchema} — so authoring either here @@ -672,6 +682,8 @@ export interface PopoverSchema extends BaseSchema { * this declaration carries none. What the renderer DOES read off this node: * `align`, `content`, `defaultOpen`, `modal`, `side`, `trigger` (in * `packages/components/src/renderers/overlay/popover.tsx`). + * The `modal` read has since been retired (objectui#11347): it was never a + * member here and no producer authored it. * * Before objectui#9256 tombstoned them here, `body` and `children` were both * inherited-and-optional from {@link BaseSchema} — so authoring either here @@ -698,6 +710,8 @@ export interface PopoverSchema extends BaseSchema { * this declaration carries none. What the renderer DOES read off this node: * `align`, `content`, `defaultOpen`, `modal`, `side`, `trigger` (in * `packages/components/src/renderers/overlay/popover.tsx`). + * The `modal` read has since been retired (objectui#11347): it was never a + * member here and no producer authored it. * * Before objectui#9256 tombstoned them here, `body` and `children` were both * inherited-and-optional from {@link BaseSchema} — so authoring either here @@ -1083,6 +1097,8 @@ export interface DropdownMenuSchema extends BaseSchema { * this declaration carries none. What the renderer DOES read off this node: * `align`, `defaultOpen`, `items`, `label`, `modal`, `side`, `trigger` (in * `packages/components/src/renderers/overlay/dropdown-menu.tsx`). + * The `modal` read has since been retired (objectui#11347): it was never a + * member here and no producer authored it. * * Before objectui#9256 tombstoned them here, `body` and `children` were both * inherited-and-optional from {@link BaseSchema} — so authoring either here @@ -1110,6 +1126,8 @@ export interface DropdownMenuSchema extends BaseSchema { * this declaration carries none. What the renderer DOES read off this node: * `align`, `defaultOpen`, `items`, `label`, `modal`, `side`, `trigger` (in * `packages/components/src/renderers/overlay/dropdown-menu.tsx`). + * The `modal` read has since been retired (objectui#11347): it was never a + * member here and no producer authored it. * * Before objectui#9256 tombstoned them here, `body` and `children` were both * inherited-and-optional from {@link BaseSchema} — so authoring either here diff --git a/packages/types/src/zod/data-display.zod.ts b/packages/types/src/zod/data-display.zod.ts index 271026475a..7f1775359d 100644 --- a/packages/types/src/zod/data-display.zod.ts +++ b/packages/types/src/zod/data-display.zod.ts @@ -185,6 +185,8 @@ export const ListItemSchema = z.object({ */ export const ListSchema = BaseSchema.extend({ type: z.literal('list'), + title: z.string().optional() + .describe('Heading drawn above the list (objectui#11347)'), items: z.array(ListItemSchema).describe('List items'), ordered: z.boolean().optional().describe('Whether list is ordered'), dividers: z.boolean().optional().describe('Show dividers between items'), diff --git a/packages/types/src/zod/feedback.zod.ts b/packages/types/src/zod/feedback.zod.ts index a8b64ae05b..e237e49dd7 100644 --- a/packages/types/src/zod/feedback.zod.ts +++ b/packages/types/src/zod/feedback.zod.ts @@ -34,14 +34,14 @@ export const LoadingSchema = BaseSchema.extend({ + 'TypeScript type checker across all 24 registering packages, no renderer read consumes `body` or ' + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' - + 'What it renders instead: `fullscreen`, `size`, `text`.', + + 'What it renders instead: `fullscreen`, `label`, `size`.', ), children: retirementTombstone( 'REFUSED (objectui#9256, ADR-0049) — `loading` reads NEITHER content channel: measured with the ' + 'TypeScript type checker across all 24 registering packages, no renderer read consumes `body` or ' + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' - + 'What it renders instead: `fullscreen`, `size`, `text`.', + + 'What it renders instead: `fullscreen`, `label`, `size`.', ), }); diff --git a/packages/types/src/zod/form.zod.ts b/packages/types/src/zod/form.zod.ts index 0912ec2e9a..9eed89603e 100644 --- a/packages/types/src/zod/form.zod.ts +++ b/packages/types/src/zod/form.zod.ts @@ -759,14 +759,14 @@ export const InputOTPSchema = BaseSchema.extend({ + 'TypeScript type checker across all 24 registering packages, no renderer read consumes `body` or ' + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' - + 'What it renders instead: `maxLength`, `value`.', + + 'What it renders instead: `length`, `value`.', ), children: retirementTombstone( 'REFUSED (objectui#9256, ADR-0049) — `input-otp` reads NEITHER content channel: measured with the ' + 'TypeScript type checker across all 24 registering packages, no renderer read consumes `body` or ' + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' - + 'What it renders instead: `maxLength`, `value`.', + + 'What it renders instead: `length`, `value`.', ), }); diff --git a/packages/types/src/zod/overlay.zod.ts b/packages/types/src/zod/overlay.zod.ts index 64a1d4b2d7..13f3abe0d4 100644 --- a/packages/types/src/zod/overlay.zod.ts +++ b/packages/types/src/zod/overlay.zod.ts @@ -232,7 +232,7 @@ export const SheetSchema = BaseSchema.extend({ + 'TypeScript type checker across all 24 registering packages, no renderer read consumes `body` or ' + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' - + 'What it renders instead: `content`, `defaultOpen`, `description`, `footer`, `modal`, `side`, ' + + 'What it renders instead: `content`, `defaultOpen`, `description`, `footer`, `side`, ' + '`title`, `trigger`.', ), children: retirementTombstone( @@ -240,7 +240,7 @@ export const SheetSchema = BaseSchema.extend({ + 'TypeScript type checker across all 24 registering packages, no renderer read consumes `body` or ' + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' - + 'What it renders instead: `content`, `defaultOpen`, `description`, `footer`, `modal`, `side`, ' + + 'What it renders instead: `content`, `defaultOpen`, `description`, `footer`, `side`, ' + '`title`, `trigger`.', ), }); @@ -263,16 +263,14 @@ export const DrawerSchema = BaseSchema.extend({ + 'TypeScript type checker across all 24 registering packages, no renderer read consumes `body` or ' + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' - + 'What it renders instead: `content`, `defaultOpen`, `description`, `footer`, ' - + '`shouldScaleBackground`, `showClose`, `title`, `trigger`.', + + 'What it renders instead: `content`, `defaultOpen`, `description`, `title`, `trigger`.', ), children: retirementTombstone( 'REFUSED (objectui#9256, ADR-0049) — `drawer` reads NEITHER content channel: measured with the ' + 'TypeScript type checker across all 24 registering packages, no renderer read consumes `body` or ' + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' - + 'What it renders instead: `content`, `defaultOpen`, `description`, `footer`, ' - + '`shouldScaleBackground`, `showClose`, `title`, `trigger`.', + + 'What it renders instead: `content`, `defaultOpen`, `description`, `title`, `trigger`.', ), }); @@ -293,14 +291,14 @@ export const PopoverSchema = BaseSchema.extend({ + 'TypeScript type checker across all 24 registering packages, no renderer read consumes `body` or ' + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' - + 'What it renders instead: `align`, `content`, `defaultOpen`, `modal`, `side`, `trigger`.', + + 'What it renders instead: `align`, `content`, `defaultOpen`, `side`, `trigger`.', ), children: retirementTombstone( 'REFUSED (objectui#9256, ADR-0049) — `popover` reads NEITHER content channel: measured with the ' + 'TypeScript type checker across all 24 registering packages, no renderer read consumes `body` or ' + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' - + 'What it renders instead: `align`, `content`, `defaultOpen`, `modal`, `side`, `trigger`.', + + 'What it renders instead: `align`, `content`, `defaultOpen`, `side`, `trigger`.', ), }); @@ -477,14 +475,14 @@ export const DropdownMenuSchema = BaseSchema.extend({ + 'TypeScript type checker across all 24 registering packages, no renderer read consumes `body` or ' + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' - + 'What it renders instead: `align`, `defaultOpen`, `items`, `label`, `modal`, `side`, `trigger`.', + + 'What it renders instead: `align`, `defaultOpen`, `items`, `label`, `side`, `trigger`.', ), children: retirementTombstone( 'REFUSED (objectui#9256, ADR-0049) — `dropdown-menu` reads NEITHER content channel: measured with the ' + 'TypeScript type checker across all 24 registering packages, no renderer read consumes `body` or ' + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' - + 'What it renders instead: `align`, `defaultOpen`, `items`, `label`, `modal`, `side`, `trigger`.', + + 'What it renders instead: `align`, `defaultOpen`, `items`, `label`, `side`, `trigger`.', ), });