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 && (
+ {/* 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`.',
),
});