Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .changeset/6150-undeclared-but-consumed-keys.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
---
'@object-ui/types': minor
---

Declare the 13 renderer-read keys that no shipped type declared (objectui#6150)

**This is a published-surface change on `@object-ui/types` and its `zod` mirrors,
and it moves the accept set in TWO directions.** Read the next two paragraphs
before reading the list — they are what the change actually is.

**Key membership is NOT widened — it was never narrow.** All eight touched mirrors
extend `BaseSchema`, which is `.passthrough()`, and `.extend()` carries that policy
through (measured on the built mirrors: `catchall` is `z.unknown()` on all eight).
So before this change every one of the 13 keys already parsed green and already
SURVIVED the parse — admitted unexamined, neither refused nor stripped. Nothing
that parsed before stops parsing because a key became known.

**Value enforcement IS widened, which in the value dimension is a NARROWING.** For
the 12 keys that gained a zod mirror entry, the value is now validated against the
declared type: `{ type: 'text', content: 42 }` parsed green before and is refused
now, at `content`. That is the point of declaring them — `declared === enforced` —
but it is a behaviour change for documents that carried a wrong-typed value under
one of these 13 names. Keys OUTSIDE the 13 are untouched: an undeclared key of any
type is still admitted unexamined on all eight mirrors, pinned per mirror.

The 13, each with the renderer read site the declaration records:

| type | key | declared as | read at |
|---|---|---|---|
| `TextSchema` | `content` | `string` | `renderers/basic/text.tsx` — `{schema.content \|\| schema.value}` |
| `CarouselSchema` | `opts` | `Record<string, unknown>` | `complex/carousel.tsx` — `opts={schema.opts}` |
| `CarouselSchema` | `orientation` | `'horizontal' \| 'vertical'` | `complex/carousel.tsx` |
| `CarouselSchema` | `itemClassName` | `string` | `complex/carousel.tsx` — per-slide class |
| `FilterBuilderSchema` | `wrapperClass` | `string` | `complex/filter-builder.tsx` |
| `TreeViewSchema` | `nodes` | `TreeNode[]` | `data-display/tree-view.tsx` |
| `TreeViewSchema` | `title` | `string` | `data-display/tree-view.tsx` |
| `TreeViewSchema` | `onNodeClick` | `(node: TreeNode) => void` | `data-display/tree-view.tsx` — INVOKED |
| `CheckboxSchema` | `required` | `boolean` | `form/checkbox.tsx` — drives the `*` marker |
| `FileUploadSchema` | `buttonText` | `string` | `form/file-upload.tsx` |
| `FileUploadSchema` | `wrapperClass` | `string` | `form/file-upload.tsx` |
| `HoverCardSchema` | `align` | `OverlayAlignment` | `overlay/hover-card.tsx` |
| `ContextMenuSchema` | `trigger` | `SchemaNode \| SchemaNode[]` | `overlay/context-menu.tsx` |

These compiled before only because `BaseSchema` ends with `[key: string]: any`
(objectui#5155), so the docs page was the single place in the repo recording each
capability, and the one place with no mechanical guard.

Three declarations are deliberately not what "declare what is read" would produce
on its own, and each says so in its own doc comment:

- `CarouselSchema.opts` stays an OPEN bag rather than the docs page's
`{ loop?, align? }` pair. The renderer forwards the whole bag to embla, so
narrowing it to two keys would refuse authored documents that work today.
- `ContextMenuSchema.trigger` is OPTIONAL although the docs page shows it
required; the renderer substitutes a placeholder, so trigger-less documents are
legal today.
- `TreeViewSchema.onNodeClick` gets NO zod mirror. It is invoked, not read as a
value, so it cannot appear in an authored JSON document; objectui#6152 ruled
that class is recorded in `zod-mirror-parity.test.ts`'s `RuntimeOnlyDeclared`
instead, and it is (the first pair to sit there without also sitting in
`UnmirroredDeclared`, so that file's two counts move with it).

Two of the 13 declare a SECOND spelling for a slot that already had one —
`TextSchema.content` beside `value`, `TreeViewSchema.nodes` beside `data` — because
that is what the renderers read. Retiring either spelling is an ADR-0049
enforce-or-remove question and is deliberately not decided here. Declaring `nodes`
also does not by itself make a `nodes`-only tree-view document legal: `data` stays
required on both faces.

Large diffs are not rendered by default.

37 changes: 29 additions & 8 deletions packages/types/src/__tests__/zod-mirror-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,9 +65,11 @@
* Anything citing "121" as the mirroring debt is citing a number that changed
* meaning — the comparable figure is 97 + 1 mirrored + 23 reclassified. The
* full statement is on that ledger.
* - **6 entries** in `RuntimeOnlyDeclared`, **23 keys** across them — a strict
* subset of the 16 pairs above, so the "no entry in either" population is
* unchanged.
* - **7 entries** in `RuntimeOnlyDeclared`, **24 keys** across them. Six of the
* seven are a subset of the 16 pairs above; `TreeViewSchema` is NOT — it is
* the first pair whose ONLY ledger entry is a runtime-only one
* (objectui#6150 declared `onNodeClick` on an otherwise clean pair), so the
* "no entry in either" population dropped by one to 141.
* - 158 − 12 = **146**, the "pairs with no entry" `LedgerMismatch` speaks of.
*
* ## Two ratchets, because the forward comparison has two halves
Expand DownExpand Up@@ -248,7 +250,7 @@ export type ReconcileAgainstLedger< K, Measured, Recorded > =
export type assertionRatchetAcceptsAgreement =
Expect< Equal< ReconcileAgainstLedger< 'p', 'a', 'a' >, never > >;

/** …and so does a clean pair with no entry, which is the case for 142 of the 158. */
/** …and so does a clean pair with no entry, which is the case for 141 of the 158. */
export type assertionRatchetAcceptsCleanPair =
Expect< Equal< ReconcileAgainstLedger< 'p', never, never >, never > >;

Expand DownExpand Up@@ -1040,6 +1042,23 @@ interface RuntimeOnlyDeclared {
* pair. POLICY group.
*/
'objectql.zod.ts#ObjectViewSchema': 'onNavigate';
/**
* `TreeViewSchema`'s ONLY entry in either ledger — the pair was clean before
* objectui#6150 and this key is the whole of its debt.
*
* OVERSIGHT group by mirror shape (`onSelectChange` and `onExpandChange` are
* mirrored beside it as `z.function()`), but it arrives here as a DECLARATION,
* not a discovery: objectui#6150's census measured `schema.onNodeClick` INVOKED
* at `renderers/data-display/tree-view.tsx:98,99` against a type that declared
* nothing, and the card declared it. A function cannot appear in an authored
* JSON document, so the key is a runtime slot and objectui#6152's ruling routes
* it here rather than to a mirror — the step-3 exception in the header above,
* used exactly as written.
*
* ⚠️ This is the first pair to sit in `RuntimeOnlyDeclared` without also sitting
* in `UnmirroredDeclared`; the two counts in the file header record that.
*/
'data-display.zod.ts#TreeViewSchema': 'onNodeClick';
/**
* 3 of `DetailViewSchema`'s former 14 — the exact three the 2026-07 audit named. By
* mirror SHAPE this is the oversight group (`onBack` is mirrored, as `z.string()`),
Expand DownExpand Up@@ -1169,10 +1188,12 @@ export const assertionDriftMatchesLedger: never = 0 as unknown as LedgerMismatch

/**
* The SECOND half of the forward comparison: every pair's declared-but-unmirrored
* key set equals what the two ledgers TOGETHER record for it — `never` for the 142
* pairs with no entry in either (158 − 16; `RuntimeOnlyDeclared`'s 6 pairs are a
* measured subset of `UnmirroredDeclared`'s 16, so the clean population is unchanged
* by objectui#6152's reclassification).
* key set equals what the two ledgers TOGETHER record for it — `never` for the 141
* pairs with no entry in either (158 − 17). Six of `RuntimeOnlyDeclared`'s seven
* pairs are a measured subset of `UnmirroredDeclared`'s 16, so objectui#6152's
* reclassification left the clean population unchanged; objectui#6150 then added
* `TreeViewSchema`, whose only entry is runtime-only, which is why the union is 17
* pairs and not 16.
*
* ⚠️ **The discriminating signal is the PER-PAIR set, not this file's exit code.**
* The exit code is a whole-file verdict, so it moves only while the rest of the
Expand Down
43 changes: 43 additions & 0 deletions packages/types/src/complex.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -347,6 +347,16 @@ export interface FilterBuilderSchema extends BaseSchema {
* @default 3
*/
maxDepth?: number;
/**
* Tailwind classes on the outermost wrapper `div`.
*
* READ SITE: `packages/components/src/renderers/complex/filter-builder.tsx:37`
* — `className={schema.wrapperClass || ''}`.
*
* Distinct from {@link BaseSchema.className}, which this renderer applies
* further in. Declared by objectui#6150.
*/
wrapperClass?: string;
}

/**
Expand DownExpand Up@@ -398,6 +408,39 @@ export interface CarouselSchema extends BaseSchema {
* Carousel items
*/
items: CarouselItem[];
/**
* Option bag forwarded VERBATIM to the underlying embla carousel.
*
* READ SITE: `packages/components/src/renderers/complex/carousel.tsx:23` —
* `opts={schema.opts}`, passed straight through to the `Carousel`
* primitive, whose own `opts` is embla's `EmblaOptionsType`.
*
* ⚠️ Deliberately declared OPEN rather than as the two-key shape the docs
* page shows (`{ loop?, align? }`). The renderer forwards the whole bag, so
* every other embla option authored today reaches the library and works;
* declaring the documented pair would REFUSE those authored documents —
* a narrowing of a published surface, which is a ruling and not a
* declaration. objectui#6150 records the capability as it is; picking a
* narrower shape is escalated with that card.
*/
opts?: Record<string, unknown>;
/**
* Scroll axis.
*
* READ SITE: `renderers/complex/carousel.tsx:24` —
* `orientation={schema.orientation || 'horizontal'}`.
*
* @default 'horizontal'
*/
orientation?: 'horizontal' | 'vertical';
/**
* Tailwind classes applied to EACH slide (not the container — that is
* {@link BaseSchema.className}).
*
* READ SITE: `renderers/complex/carousel.tsx:30` —
* `className={schema.itemClassName}` on every `CarouselItem`.
*/
itemClassName?: string;
/**
* Auto-play interval (ms)
*/
Expand Down
43 changes: 42 additions & 1 deletion packages/types/src/data-display.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1111,9 +1111,35 @@ export interface TreeNode {
export interface TreeViewSchema extends BaseSchema {
type: 'tree-view';
/**
* Tree data
* Tree data — the fallback spelling, read only when
* {@link TreeViewSchema.nodes} is absent.
*
* READ SITE: `packages/components/src/renderers/data-display/tree-view.tsx:105`
* — `const rawNodes = boundData || schema.nodes || schema.data || []`.
*/
data: TreeNode[];
/**
* Tree data — the spelling the renderer reads FIRST.
*
* READ SITE: `renderers/data-display/tree-view.tsx:105`, the middle limb of
* `boundData || schema.nodes || schema.data || []`, so `nodes` WINS over
* {@link TreeViewSchema.data} when both are authored (and a `bind`-resolved
* value wins over both).
*
* ⚠️ Declaring `nodes` does NOT by itself make `{ type: 'tree-view', nodes }`
* a legal document: {@link TreeViewSchema.data} stays REQUIRED on both faces,
* so the validator still demands `data`. Relaxing that is an accept-set
* change and a separate ruling — objectui#6150 declares the read, nothing
* more.
*/
nodes?: TreeNode[];
/**
* Heading rendered above the tree.
*
* READ SITE: `renderers/data-display/tree-view.tsx:115` (presence gate) and
* `:117` (the `h3` body).
*/
title?: string;
/**
* Default expanded node IDs
*/
Expand DownExpand Up@@ -1148,6 +1174,21 @@ export interface TreeViewSchema extends BaseSchema {
* Node expand handler
*/
onExpandChange?: (expandedIds: string[]) => void;
/**
* Node click handler — INVOKED, not merely read, so this is a call
* signature and not a value shape.
*
* READ SITE: `packages/components/src/renderers/data-display/tree-view.tsx:98`
* (presence gate `if (schema.onNodeClick)`) and `:99` (the call
* `schema.onNodeClick(node)`), where `node` is the clicked
* {@link TreeNode}. The handler's return value is discarded.
*
* ⚠️ NOT mirrored in `../zod/data-display.zod.ts`, deliberately: a function
* cannot appear in an authored JSON document, so it is a runtime slot.
* objectui#6152 ruled that class never gets a mirror; it is recorded in
* `__tests__/zod-mirror-parity.test.ts`'s `RuntimeOnlyDeclared` instead.
*/
onNodeClick?: (node: TreeNode) => void;
}

/**
Expand Down
27 changes: 27 additions & 0 deletions packages/types/src/form.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -345,6 +345,18 @@ export interface CheckboxSchema extends BaseSchema {
* Error message
*/
error?: string;
/**
* Whether the box must be checked — drives a VISIBLE affordance, not just
* form semantics.
*
* READ SITES: `packages/components/src/renderers/form/checkbox.tsx:45` —
* `required={schema.required}` on the Radix `Checkbox` — and `:49`, where it
* gates the label's required marker
* (`schema.required && "text-destructive after:content-['*']"`).
*
* Declared by objectui#6150; one of the two behavioural keys in that census.
*/
required?: boolean;
/**
* Change handler
*/
Expand DownExpand Up@@ -592,6 +604,21 @@ export interface FileUploadSchema extends BaseSchema {
* Error message
*/
error?: string;
/**
* Label on the drop zone / upload button.
*
* READ SITE: `packages/components/src/renderers/form/file-upload.tsx:123` —
* `{isUploading ? "…" : (schema.buttonText || "DROP PAYLOAD OR CLICK TO UPLOAD")}`.
*/
buttonText?: string;
/**
* Tailwind classes appended to the outer wrapper `div`.
*
* READ SITE: `renderers/form/file-upload.tsx:78` — appended to the
* renderer's own grid classes as
* `` `grid w-full … ${schema.wrapperClass || ''}` ``.
*/
wrapperClass?: string;
/**
* Change handler (receives FileList or File[])
*/
Expand Down
25 changes: 24 additions & 1 deletion packages/types/src/layout.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -76,7 +76,30 @@ export interface TextSpanSchema extends BaseSchema {
export interface TextSchema extends BaseSchema {
type: 'text';
/**
* Text content to display
* Text content to display — the spelling the renderer reads FIRST.
*
* READ SITE: `packages/components/src/renderers/basic/text.tsx:51` (the
* wrapped `span` arm, taken when the node carries a designer id or a
* className) and `:56` (the bare fragment arm), both as
* `{schema.content || schema.value}`. `content` therefore WINS over
* {@link TextSchema.value} whenever both are authored.
*
* Declared by objectui#6150 (undeclared-but-consumed census). Before that
* card the renderer read this key through `BaseSchema`'s
* `[key: string]: any` (objectui#5155) and no shipped type mentioned it —
* the docs page was the only record of a capability that works.
*
* ⚠️ Two spellings for one slot is a dialect, not a design. Retiring one of
* them is an ADR-0049 enforce-or-remove question and is deliberately NOT
* decided here; this declaration records what the renderer does today.
*/
content?: string;
/**
* Text content — the fallback spelling, read only when
* {@link TextSchema.content} is absent or falsy.
*
* READ SITE: `renderers/basic/text.tsx:51,56`, the right-hand side of
* `{schema.content || schema.value}`.
*/
value?: string;
/**
Expand Down
26 changes: 26 additions & 0 deletions packages/types/src/overlay.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -318,6 +318,17 @@ export interface HoverCardSchema extends BaseSchema {
* @default 300
*/
closeDelay?: number;
/**
* Alignment of the card against its trigger.
*
* READ SITE: `packages/components/src/renderers/overlay/hover-card.tsx:24` —
* `align={schema.align}` on `HoverCardContent`, beside the already-declared
* `side={schema.side}`.
*
* Same vocabulary as {@link DropdownMenuSchema.align} and
* {@link PopoverSchema.align}; declared by objectui#6150.
*/
align?: OverlayAlignment;
/**
* Open state change handler
*/
Expand DownExpand Up@@ -458,6 +469,21 @@ export interface ContextMenuSchema extends BaseSchema {
* Element to attach context menu to
*/
children: SchemaNode | SchemaNode[];
/**
* The right-clickable area's content.
*
* READ SITE: `packages/components/src/renderers/overlay/context-menu.tsx:95`
* — `renderChildren(schema.trigger || { type: 'text', value: 'Right click here' })`
* inside `ContextMenuTrigger`. ⚠️ Note the renderer renders `trigger`, NOT
* the declared {@link ContextMenuSchema.children}, which no read site
* consumes.
*
* Declared OPTIONAL although the docs page shows it required: the renderer
* substitutes a placeholder when it is absent, so every document without a
* `trigger` is legal today and declaring it required would refuse them.
* Declared by objectui#6150.
*/
trigger?: SchemaNode | SchemaNode[];
}

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/types/src/zod/complex.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -214,6 +214,8 @@ export const FilterBuilderSchema = BaseSchema.extend({
onChange: z.function().optional().describe('Change handler'),
allowGroups: z.boolean().optional().describe('Allow grouped conditions'),
maxDepth: z.number().optional().describe('Maximum nesting depth'),
wrapperClass: z.string().optional()
.describe("Outer wrapper classes, read at renderers/complex/filter-builder.tsx:37 — `className={schema.wrapperClass || ''}` (objectui#6150)"),
});

/**
Expand All@@ -230,6 +232,12 @@ export const CarouselItemSchema = z.object({
export const CarouselSchema = BaseSchema.extend({
type: z.literal('carousel'),
items: z.array(CarouselItemSchema).describe('Carousel items'),
opts: z.record(z.string(), z.unknown()).optional()
.describe("Embla option bag forwarded verbatim at renderers/complex/carousel.tsx:23 — `opts={schema.opts}`. Left OPEN on purpose: the renderer passes the whole bag through, so narrowing it to the docs' `{loop, align}` pair would refuse authored documents that work today (objectui#6150)"),
orientation: z.enum(['horizontal', 'vertical']).optional()
.describe("Scroll axis, read at renderers/complex/carousel.tsx:24 — `orientation={schema.orientation || 'horizontal'}` (objectui#6150)"),
itemClassName: z.string().optional()
.describe('Per-slide Tailwind classes, read at renderers/complex/carousel.tsx:30 — `className={schema.itemClassName}` on every CarouselItem (objectui#6150)'),
autoPlay: z.number().optional().describe('Auto-play interval (ms)'),
showArrows: z.boolean().optional().describe('Show navigation arrows'),
showDots: z.boolean().optional().describe('Show navigation dots'),
Expand Down
7 changes: 6 additions & 1 deletion packages/types/src/zod/data-display.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -298,7 +298,12 @@ export const TreeNodeSchema: z.ZodType<any> = z.lazy(() =>
*/
export const TreeViewSchema = BaseSchema.extend({
type: z.literal('tree-view'),
data: z.array(TreeNodeSchema).describe('Tree data'),
data: z.array(TreeNodeSchema)
.describe('Tree data, read as the fallback limb of `boundData || schema.nodes || schema.data || []` at renderers/data-display/tree-view.tsx:105'),
nodes: z.array(TreeNodeSchema).optional()
.describe('Tree data, read FIRST at renderers/data-display/tree-view.tsx:105 — the middle limb of `boundData || schema.nodes || schema.data || []`, so it wins over `data`. ⚠️ `data` stays REQUIRED here: declaring `nodes` does not by itself make a `nodes`-only document legal (objectui#6150)'),
title: z.string().optional()
.describe('Heading above the tree, read at renderers/data-display/tree-view.tsx:115 (presence gate) and :117 (the h3 body) (objectui#6150)'),
defaultExpandedIds: z.array(z.string()).optional().describe('Default expanded node IDs'),
defaultSelectedIds: z.array(z.string()).optional().describe('Default selected node IDs'),
expandedIds: z.array(z.string()).optional().describe('Controlled expanded node IDs'),
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .changeset/6150-undeclared-but-consumed-keys.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
---
'@object-ui/types': minor
---

Declare the 13 renderer-read keys that no shipped type declared (objectui#6150)

**This is a published-surface change on `@object-ui/types` and its `zod` mirrors,
and it moves the accept set in TWO directions.** Read the next two paragraphs
before reading the list — they are what the change actually is.

**Key membership is NOT widened — it was never narrow.** All eight touched mirrors
extend `BaseSchema`, which is `.passthrough()`, and `.extend()` carries that policy
through (measured on the built mirrors: `catchall` is `z.unknown()` on all eight).
So before this change every one of the 13 keys already parsed green and already
SURVIVED the parse — admitted unexamined, neither refused nor stripped. Nothing
that parsed before stops parsing because a key became known.

**Value enforcement IS widened, which in the value dimension is a NARROWING.** For
the 12 keys that gained a zod mirror entry, the value is now validated against the
declared type: `{ type: 'text', content: 42 }` parsed green before and is refused
now, at `content`. That is the point of declaring them — `declared === enforced` —
but it is a behaviour change for documents that carried a wrong-typed value under
one of these 13 names. Keys OUTSIDE the 13 are untouched: an undeclared key of any
type is still admitted unexamined on all eight mirrors, pinned per mirror.

The 13, each with the renderer read site the declaration records:

| type | key | declared as | read at |
|---|---|---|---|
| `TextSchema` | `content` | `string` | `renderers/basic/text.tsx` — `{schema.content \|\| schema.value}` |
| `CarouselSchema` | `opts` | `Record<string, unknown>` | `complex/carousel.tsx` — `opts={schema.opts}` |
| `CarouselSchema` | `orientation` | `'horizontal' \| 'vertical'` | `complex/carousel.tsx` |
| `CarouselSchema` | `itemClassName` | `string` | `complex/carousel.tsx` — per-slide class |
| `FilterBuilderSchema` | `wrapperClass` | `string` | `complex/filter-builder.tsx` |
| `TreeViewSchema` | `nodes` | `TreeNode[]` | `data-display/tree-view.tsx` |
| `TreeViewSchema` | `title` | `string` | `data-display/tree-view.tsx` |
| `TreeViewSchema` | `onNodeClick` | `(node: TreeNode) => void` | `data-display/tree-view.tsx` — INVOKED |
| `CheckboxSchema` | `required` | `boolean` | `form/checkbox.tsx` — drives the `*` marker |
| `FileUploadSchema` | `buttonText` | `string` | `form/file-upload.tsx` |
| `FileUploadSchema` | `wrapperClass` | `string` | `form/file-upload.tsx` |
| `HoverCardSchema` | `align` | `OverlayAlignment` | `overlay/hover-card.tsx` |
| `ContextMenuSchema` | `trigger` | `SchemaNode \| SchemaNode[]` | `overlay/context-menu.tsx` |

These compiled before only because `BaseSchema` ends with `[key: string]: any`
(objectui#5155), so the docs page was the single place in the repo recording each
capability, and the one place with no mechanical guard.

Three declarations are deliberately not what "declare what is read" would produce
on its own, and each says so in its own doc comment:

- `CarouselSchema.opts` stays an OPEN bag rather than the docs page's
`{ loop?, align? }` pair. The renderer forwards the whole bag to embla, so
narrowing it to two keys would refuse authored documents that work today.
- `ContextMenuSchema.trigger` is OPTIONAL although the docs page shows it
required; the renderer substitutes a placeholder, so trigger-less documents are
legal today.
- `TreeViewSchema.onNodeClick` gets NO zod mirror. It is invoked, not read as a
value, so it cannot appear in an authored JSON document; objectui#6152 ruled
that class is recorded in `zod-mirror-parity.test.ts`'s `RuntimeOnlyDeclared`
instead, and it is (the first pair to sit there without also sitting in
`UnmirroredDeclared`, so that file's two counts move with it).

Two of the 13 declare a SECOND spelling for a slot that already had one —
`TextSchema.content` beside `value`, `TreeViewSchema.nodes` beside `data` — because
that is what the renderers read. Retiring either spelling is an ADR-0049
enforce-or-remove question and is deliberately not decided here. Declaring `nodes`
also does not by itself make a `nodes`-only tree-view document legal: `data` stays
required on both faces.

Large diffs are not rendered by default.

37 changes: 29 additions & 8 deletions packages/types/src/__tests__/zod-mirror-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,9 +65,11 @@
* Anything citing "121" as the mirroring debt is citing a number that changed
* meaning — the comparable figure is 97 + 1 mirrored + 23 reclassified. The
* full statement is on that ledger.
* - **6 entries** in `RuntimeOnlyDeclared`, **23 keys** across them — a strict
* subset of the 16 pairs above, so the "no entry in either" population is
* unchanged.
* - **7 entries** in `RuntimeOnlyDeclared`, **24 keys** across them. Six of the
* seven are a subset of the 16 pairs above; `TreeViewSchema` is NOT — it is
* the first pair whose ONLY ledger entry is a runtime-only one
* (objectui#6150 declared `onNodeClick` on an otherwise clean pair), so the
* "no entry in either" population dropped by one to 141.
* - 158 − 12 = **146**, the "pairs with no entry" `LedgerMismatch` speaks of.
*
* ## Two ratchets, because the forward comparison has two halves
Expand DownExpand Up@@ -248,7 +250,7 @@ export type ReconcileAgainstLedger< K, Measured, Recorded > =
export type assertionRatchetAcceptsAgreement =
Expect< Equal< ReconcileAgainstLedger< 'p', 'a', 'a' >, never > >;

/** …and so does a clean pair with no entry, which is the case for 142 of the 158. */
/** …and so does a clean pair with no entry, which is the case for 141 of the 158. */
export type assertionRatchetAcceptsCleanPair =
Expect< Equal< ReconcileAgainstLedger< 'p', never, never >, never > >;

Expand DownExpand Up@@ -1040,6 +1042,23 @@ interface RuntimeOnlyDeclared {
* pair. POLICY group.
*/
'objectql.zod.ts#ObjectViewSchema': 'onNavigate';
/**
* `TreeViewSchema`'s ONLY entry in either ledger — the pair was clean before
* objectui#6150 and this key is the whole of its debt.
*
* OVERSIGHT group by mirror shape (`onSelectChange` and `onExpandChange` are
* mirrored beside it as `z.function()`), but it arrives here as a DECLARATION,
* not a discovery: objectui#6150's census measured `schema.onNodeClick` INVOKED
* at `renderers/data-display/tree-view.tsx:98,99` against a type that declared
* nothing, and the card declared it. A function cannot appear in an authored
* JSON document, so the key is a runtime slot and objectui#6152's ruling routes
* it here rather than to a mirror — the step-3 exception in the header above,
* used exactly as written.
*
* ⚠️ This is the first pair to sit in `RuntimeOnlyDeclared` without also sitting
* in `UnmirroredDeclared`; the two counts in the file header record that.
*/
'data-display.zod.ts#TreeViewSchema': 'onNodeClick';
/**
* 3 of `DetailViewSchema`'s former 14 — the exact three the 2026-07 audit named. By
* mirror SHAPE this is the oversight group (`onBack` is mirrored, as `z.string()`),
Expand DownExpand Up@@ -1169,10 +1188,12 @@ export const assertionDriftMatchesLedger: never = 0 as unknown as LedgerMismatch

/**
* The SECOND half of the forward comparison: every pair's declared-but-unmirrored
* key set equals what the two ledgers TOGETHER record for it — `never` for the 142
* pairs with no entry in either (158 − 16; `RuntimeOnlyDeclared`'s 6 pairs are a
* measured subset of `UnmirroredDeclared`'s 16, so the clean population is unchanged
* by objectui#6152's reclassification).
* key set equals what the two ledgers TOGETHER record for it — `never` for the 141
* pairs with no entry in either (158 − 17). Six of `RuntimeOnlyDeclared`'s seven
* pairs are a measured subset of `UnmirroredDeclared`'s 16, so objectui#6152's
* reclassification left the clean population unchanged; objectui#6150 then added
* `TreeViewSchema`, whose only entry is runtime-only, which is why the union is 17
* pairs and not 16.
*
* ⚠️ **The discriminating signal is the PER-PAIR set, not this file's exit code.**
* The exit code is a whole-file verdict, so it moves only while the rest of the
Expand Down
43 changes: 43 additions & 0 deletions packages/types/src/complex.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -347,6 +347,16 @@ export interface FilterBuilderSchema extends BaseSchema {
* @default 3
*/
maxDepth?: number;
/**
* Tailwind classes on the outermost wrapper `div`.
*
* READ SITE: `packages/components/src/renderers/complex/filter-builder.tsx:37`
* — `className={schema.wrapperClass || ''}`.
*
* Distinct from {@link BaseSchema.className}, which this renderer applies
* further in. Declared by objectui#6150.
*/
wrapperClass?: string;
}

/**
Expand DownExpand Up@@ -398,6 +408,39 @@ export interface CarouselSchema extends BaseSchema {
* Carousel items
*/
items: CarouselItem[];
/**
* Option bag forwarded VERBATIM to the underlying embla carousel.
*
* READ SITE: `packages/components/src/renderers/complex/carousel.tsx:23` —
* `opts={schema.opts}`, passed straight through to the `Carousel`
* primitive, whose own `opts` is embla's `EmblaOptionsType`.
*
* ⚠️ Deliberately declared OPEN rather than as the two-key shape the docs
* page shows (`{ loop?, align? }`). The renderer forwards the whole bag, so
* every other embla option authored today reaches the library and works;
* declaring the documented pair would REFUSE those authored documents —
* a narrowing of a published surface, which is a ruling and not a
* declaration. objectui#6150 records the capability as it is; picking a
* narrower shape is escalated with that card.
*/
opts?: Record<string, unknown>;
/**
* Scroll axis.
*
* READ SITE: `renderers/complex/carousel.tsx:24` —
* `orientation={schema.orientation || 'horizontal'}`.
*
* @default 'horizontal'
*/
orientation?: 'horizontal' | 'vertical';
/**
* Tailwind classes applied to EACH slide (not the container — that is
* {@link BaseSchema.className}).
*
* READ SITE: `renderers/complex/carousel.tsx:30` —
* `className={schema.itemClassName}` on every `CarouselItem`.
*/
itemClassName?: string;
/**
* Auto-play interval (ms)
*/
Expand Down
43 changes: 42 additions & 1 deletion packages/types/src/data-display.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1111,9 +1111,35 @@ export interface TreeNode {
export interface TreeViewSchema extends BaseSchema {
type: 'tree-view';
/**
* Tree data
* Tree data — the fallback spelling, read only when
* {@link TreeViewSchema.nodes} is absent.
*
* READ SITE: `packages/components/src/renderers/data-display/tree-view.tsx:105`
* — `const rawNodes = boundData || schema.nodes || schema.data || []`.
*/
data: TreeNode[];
/**
* Tree data — the spelling the renderer reads FIRST.
*
* READ SITE: `renderers/data-display/tree-view.tsx:105`, the middle limb of
* `boundData || schema.nodes || schema.data || []`, so `nodes` WINS over
* {@link TreeViewSchema.data} when both are authored (and a `bind`-resolved
* value wins over both).
*
* ⚠️ Declaring `nodes` does NOT by itself make `{ type: 'tree-view', nodes }`
* a legal document: {@link TreeViewSchema.data} stays REQUIRED on both faces,
* so the validator still demands `data`. Relaxing that is an accept-set
* change and a separate ruling — objectui#6150 declares the read, nothing
* more.
*/
nodes?: TreeNode[];
/**
* Heading rendered above the tree.
*
* READ SITE: `renderers/data-display/tree-view.tsx:115` (presence gate) and
* `:117` (the `h3` body).
*/
title?: string;
/**
* Default expanded node IDs
*/
Expand DownExpand Up@@ -1148,6 +1174,21 @@ export interface TreeViewSchema extends BaseSchema {
* Node expand handler
*/
onExpandChange?: (expandedIds: string[]) => void;
/**
* Node click handler — INVOKED, not merely read, so this is a call
* signature and not a value shape.
*
* READ SITE: `packages/components/src/renderers/data-display/tree-view.tsx:98`
* (presence gate `if (schema.onNodeClick)`) and `:99` (the call
* `schema.onNodeClick(node)`), where `node` is the clicked
* {@link TreeNode}. The handler's return value is discarded.
*
* ⚠️ NOT mirrored in `../zod/data-display.zod.ts`, deliberately: a function
* cannot appear in an authored JSON document, so it is a runtime slot.
* objectui#6152 ruled that class never gets a mirror; it is recorded in
* `__tests__/zod-mirror-parity.test.ts`'s `RuntimeOnlyDeclared` instead.
*/
onNodeClick?: (node: TreeNode) => void;
}

/**
Expand Down
27 changes: 27 additions & 0 deletions packages/types/src/form.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -345,6 +345,18 @@ export interface CheckboxSchema extends BaseSchema {
* Error message
*/
error?: string;
/**
* Whether the box must be checked — drives a VISIBLE affordance, not just
* form semantics.
*
* READ SITES: `packages/components/src/renderers/form/checkbox.tsx:45` —
* `required={schema.required}` on the Radix `Checkbox` — and `:49`, where it
* gates the label's required marker
* (`schema.required && "text-destructive after:content-['*']"`).
*
* Declared by objectui#6150; one of the two behavioural keys in that census.
*/
required?: boolean;
/**
* Change handler
*/
Expand DownExpand Up@@ -592,6 +604,21 @@ export interface FileUploadSchema extends BaseSchema {
* Error message
*/
error?: string;
/**
* Label on the drop zone / upload button.
*
* READ SITE: `packages/components/src/renderers/form/file-upload.tsx:123` —
* `{isUploading ? "…" : (schema.buttonText || "DROP PAYLOAD OR CLICK TO UPLOAD")}`.
*/
buttonText?: string;
/**
* Tailwind classes appended to the outer wrapper `div`.
*
* READ SITE: `renderers/form/file-upload.tsx:78` — appended to the
* renderer's own grid classes as
* `` `grid w-full … ${schema.wrapperClass || ''}` ``.
*/
wrapperClass?: string;
/**
* Change handler (receives FileList or File[])
*/
Expand Down
25 changes: 24 additions & 1 deletion packages/types/src/layout.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -76,7 +76,30 @@ export interface TextSpanSchema extends BaseSchema {
export interface TextSchema extends BaseSchema {
type: 'text';
/**
* Text content to display
* Text content to display — the spelling the renderer reads FIRST.
*
* READ SITE: `packages/components/src/renderers/basic/text.tsx:51` (the
* wrapped `span` arm, taken when the node carries a designer id or a
* className) and `:56` (the bare fragment arm), both as
* `{schema.content || schema.value}`. `content` therefore WINS over
* {@link TextSchema.value} whenever both are authored.
*
* Declared by objectui#6150 (undeclared-but-consumed census). Before that
* card the renderer read this key through `BaseSchema`'s
* `[key: string]: any` (objectui#5155) and no shipped type mentioned it —
* the docs page was the only record of a capability that works.
*
* ⚠️ Two spellings for one slot is a dialect, not a design. Retiring one of
* them is an ADR-0049 enforce-or-remove question and is deliberately NOT
* decided here; this declaration records what the renderer does today.
*/
content?: string;
/**
* Text content — the fallback spelling, read only when
* {@link TextSchema.content} is absent or falsy.
*
* READ SITE: `renderers/basic/text.tsx:51,56`, the right-hand side of
* `{schema.content || schema.value}`.
*/
value?: string;
/**
Expand Down
26 changes: 26 additions & 0 deletions packages/types/src/overlay.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -318,6 +318,17 @@ export interface HoverCardSchema extends BaseSchema {
* @default 300
*/
closeDelay?: number;
/**
* Alignment of the card against its trigger.
*
* READ SITE: `packages/components/src/renderers/overlay/hover-card.tsx:24` —
* `align={schema.align}` on `HoverCardContent`, beside the already-declared
* `side={schema.side}`.
*
* Same vocabulary as {@link DropdownMenuSchema.align} and
* {@link PopoverSchema.align}; declared by objectui#6150.
*/
align?: OverlayAlignment;
/**
* Open state change handler
*/
Expand DownExpand Up@@ -458,6 +469,21 @@ export interface ContextMenuSchema extends BaseSchema {
* Element to attach context menu to
*/
children: SchemaNode | SchemaNode[];
/**
* The right-clickable area's content.
*
* READ SITE: `packages/components/src/renderers/overlay/context-menu.tsx:95`
* — `renderChildren(schema.trigger || { type: 'text', value: 'Right click here' })`
* inside `ContextMenuTrigger`. ⚠️ Note the renderer renders `trigger`, NOT
* the declared {@link ContextMenuSchema.children}, which no read site
* consumes.
*
* Declared OPTIONAL although the docs page shows it required: the renderer
* substitutes a placeholder when it is absent, so every document without a
* `trigger` is legal today and declaring it required would refuse them.
* Declared by objectui#6150.
*/
trigger?: SchemaNode | SchemaNode[];
}

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/types/src/zod/complex.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -214,6 +214,8 @@ export const FilterBuilderSchema = BaseSchema.extend({
onChange: z.function().optional().describe('Change handler'),
allowGroups: z.boolean().optional().describe('Allow grouped conditions'),
maxDepth: z.number().optional().describe('Maximum nesting depth'),
wrapperClass: z.string().optional()
.describe("Outer wrapper classes, read at renderers/complex/filter-builder.tsx:37 — `className={schema.wrapperClass || ''}` (objectui#6150)"),
});

/**
Expand All@@ -230,6 +232,12 @@ export const CarouselItemSchema = z.object({
export const CarouselSchema = BaseSchema.extend({
type: z.literal('carousel'),
items: z.array(CarouselItemSchema).describe('Carousel items'),
opts: z.record(z.string(), z.unknown()).optional()
.describe("Embla option bag forwarded verbatim at renderers/complex/carousel.tsx:23 — `opts={schema.opts}`. Left OPEN on purpose: the renderer passes the whole bag through, so narrowing it to the docs' `{loop, align}` pair would refuse authored documents that work today (objectui#6150)"),
orientation: z.enum(['horizontal', 'vertical']).optional()
.describe("Scroll axis, read at renderers/complex/carousel.tsx:24 — `orientation={schema.orientation || 'horizontal'}` (objectui#6150)"),
itemClassName: z.string().optional()
.describe('Per-slide Tailwind classes, read at renderers/complex/carousel.tsx:30 — `className={schema.itemClassName}` on every CarouselItem (objectui#6150)'),
autoPlay: z.number().optional().describe('Auto-play interval (ms)'),
showArrows: z.boolean().optional().describe('Show navigation arrows'),
showDots: z.boolean().optional().describe('Show navigation dots'),
Expand Down
7 changes: 6 additions & 1 deletion packages/types/src/zod/data-display.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -298,7 +298,12 @@ export const TreeNodeSchema: z.ZodType<any> = z.lazy(() =>
*/
export const TreeViewSchema = BaseSchema.extend({
type: z.literal('tree-view'),
data: z.array(TreeNodeSchema).describe('Tree data'),
data: z.array(TreeNodeSchema)
.describe('Tree data, read as the fallback limb of `boundData || schema.nodes || schema.data || []` at renderers/data-display/tree-view.tsx:105'),
nodes: z.array(TreeNodeSchema).optional()
.describe('Tree data, read FIRST at renderers/data-display/tree-view.tsx:105 — the middle limb of `boundData || schema.nodes || schema.data || []`, so it wins over `data`. ⚠️ `data` stays REQUIRED here: declaring `nodes` does not by itself make a `nodes`-only document legal (objectui#6150)'),
title: z.string().optional()
.describe('Heading above the tree, read at renderers/data-display/tree-view.tsx:115 (presence gate) and :117 (the h3 body) (objectui#6150)'),
defaultExpandedIds: z.array(z.string()).optional().describe('Default expanded node IDs'),
defaultSelectedIds: z.array(z.string()).optional().describe('Default selected node IDs'),
expandedIds: z.array(z.string()).optional().describe('Controlled expanded node IDs'),
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .changeset/6150-undeclared-but-consumed-keys.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
---
'@object-ui/types': minor
---

Declare the 13 renderer-read keys that no shipped type declared (objectui#6150)

**This is a published-surface change on `@object-ui/types` and its `zod` mirrors,
and it moves the accept set in TWO directions.** Read the next two paragraphs
before reading the list — they are what the change actually is.

**Key membership is NOT widened — it was never narrow.** All eight touched mirrors
extend `BaseSchema`, which is `.passthrough()`, and `.extend()` carries that policy
through (measured on the built mirrors: `catchall` is `z.unknown()` on all eight).
So before this change every one of the 13 keys already parsed green and already
SURVIVED the parse — admitted unexamined, neither refused nor stripped. Nothing
that parsed before stops parsing because a key became known.

**Value enforcement IS widened, which in the value dimension is a NARROWING.** For
the 12 keys that gained a zod mirror entry, the value is now validated against the
declared type: `{ type: 'text', content: 42 }` parsed green before and is refused
now, at `content`. That is the point of declaring them — `declared === enforced` —
but it is a behaviour change for documents that carried a wrong-typed value under
one of these 13 names. Keys OUTSIDE the 13 are untouched: an undeclared key of any
type is still admitted unexamined on all eight mirrors, pinned per mirror.

The 13, each with the renderer read site the declaration records:

| type | key | declared as | read at |
|---|---|---|---|
| `TextSchema` | `content` | `string` | `renderers/basic/text.tsx` — `{schema.content \|\| schema.value}` |
| `CarouselSchema` | `opts` | `Record<string, unknown>` | `complex/carousel.tsx` — `opts={schema.opts}` |
| `CarouselSchema` | `orientation` | `'horizontal' \| 'vertical'` | `complex/carousel.tsx` |
| `CarouselSchema` | `itemClassName` | `string` | `complex/carousel.tsx` — per-slide class |
| `FilterBuilderSchema` | `wrapperClass` | `string` | `complex/filter-builder.tsx` |
| `TreeViewSchema` | `nodes` | `TreeNode[]` | `data-display/tree-view.tsx` |
| `TreeViewSchema` | `title` | `string` | `data-display/tree-view.tsx` |
| `TreeViewSchema` | `onNodeClick` | `(node: TreeNode) => void` | `data-display/tree-view.tsx` — INVOKED |
| `CheckboxSchema` | `required` | `boolean` | `form/checkbox.tsx` — drives the `*` marker |
| `FileUploadSchema` | `buttonText` | `string` | `form/file-upload.tsx` |
| `FileUploadSchema` | `wrapperClass` | `string` | `form/file-upload.tsx` |
| `HoverCardSchema` | `align` | `OverlayAlignment` | `overlay/hover-card.tsx` |
| `ContextMenuSchema` | `trigger` | `SchemaNode \| SchemaNode[]` | `overlay/context-menu.tsx` |

These compiled before only because `BaseSchema` ends with `[key: string]: any`
(objectui#5155), so the docs page was the single place in the repo recording each
capability, and the one place with no mechanical guard.

Three declarations are deliberately not what "declare what is read" would produce
on its own, and each says so in its own doc comment:

- `CarouselSchema.opts` stays an OPEN bag rather than the docs page's
`{ loop?, align? }` pair. The renderer forwards the whole bag to embla, so
narrowing it to two keys would refuse authored documents that work today.
- `ContextMenuSchema.trigger` is OPTIONAL although the docs page shows it
required; the renderer substitutes a placeholder, so trigger-less documents are
legal today.
- `TreeViewSchema.onNodeClick` gets NO zod mirror. It is invoked, not read as a
value, so it cannot appear in an authored JSON document; objectui#6152 ruled
that class is recorded in `zod-mirror-parity.test.ts`'s `RuntimeOnlyDeclared`
instead, and it is (the first pair to sit there without also sitting in
`UnmirroredDeclared`, so that file's two counts move with it).

Two of the 13 declare a SECOND spelling for a slot that already had one —
`TextSchema.content` beside `value`, `TreeViewSchema.nodes` beside `data` — because
that is what the renderers read. Retiring either spelling is an ADR-0049
enforce-or-remove question and is deliberately not decided here. Declaring `nodes`
also does not by itself make a `nodes`-only tree-view document legal: `data` stays
required on both faces.

Large diffs are not rendered by default.

37 changes: 29 additions & 8 deletions packages/types/src/__tests__/zod-mirror-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,9 +65,11 @@
* Anything citing "121" as the mirroring debt is citing a number that changed
* meaning — the comparable figure is 97 + 1 mirrored + 23 reclassified. The
* full statement is on that ledger.
* - **6 entries** in `RuntimeOnlyDeclared`, **23 keys** across them — a strict
* subset of the 16 pairs above, so the "no entry in either" population is
* unchanged.
* - **7 entries** in `RuntimeOnlyDeclared`, **24 keys** across them. Six of the
* seven are a subset of the 16 pairs above; `TreeViewSchema` is NOT — it is
* the first pair whose ONLY ledger entry is a runtime-only one
* (objectui#6150 declared `onNodeClick` on an otherwise clean pair), so the
* "no entry in either" population dropped by one to 141.
* - 158 − 12 = **146**, the "pairs with no entry" `LedgerMismatch` speaks of.
*
* ## Two ratchets, because the forward comparison has two halves
Expand DownExpand Up@@ -248,7 +250,7 @@ export type ReconcileAgainstLedger< K, Measured, Recorded > =
export type assertionRatchetAcceptsAgreement =
Expect< Equal< ReconcileAgainstLedger< 'p', 'a', 'a' >, never > >;

/** …and so does a clean pair with no entry, which is the case for 142 of the 158. */
/** …and so does a clean pair with no entry, which is the case for 141 of the 158. */
export type assertionRatchetAcceptsCleanPair =
Expect< Equal< ReconcileAgainstLedger< 'p', never, never >, never > >;

Expand DownExpand Up@@ -1040,6 +1042,23 @@ interface RuntimeOnlyDeclared {
* pair. POLICY group.
*/
'objectql.zod.ts#ObjectViewSchema': 'onNavigate';
/**
* `TreeViewSchema`'s ONLY entry in either ledger — the pair was clean before
* objectui#6150 and this key is the whole of its debt.
*
* OVERSIGHT group by mirror shape (`onSelectChange` and `onExpandChange` are
* mirrored beside it as `z.function()`), but it arrives here as a DECLARATION,
* not a discovery: objectui#6150's census measured `schema.onNodeClick` INVOKED
* at `renderers/data-display/tree-view.tsx:98,99` against a type that declared
* nothing, and the card declared it. A function cannot appear in an authored
* JSON document, so the key is a runtime slot and objectui#6152's ruling routes
* it here rather than to a mirror — the step-3 exception in the header above,
* used exactly as written.
*
* ⚠️ This is the first pair to sit in `RuntimeOnlyDeclared` without also sitting
* in `UnmirroredDeclared`; the two counts in the file header record that.
*/
'data-display.zod.ts#TreeViewSchema': 'onNodeClick';
/**
* 3 of `DetailViewSchema`'s former 14 — the exact three the 2026-07 audit named. By
* mirror SHAPE this is the oversight group (`onBack` is mirrored, as `z.string()`),
Expand DownExpand Up@@ -1169,10 +1188,12 @@ export const assertionDriftMatchesLedger: never = 0 as unknown as LedgerMismatch

/**
* The SECOND half of the forward comparison: every pair's declared-but-unmirrored
* key set equals what the two ledgers TOGETHER record for it — `never` for the 142
* pairs with no entry in either (158 − 16; `RuntimeOnlyDeclared`'s 6 pairs are a
* measured subset of `UnmirroredDeclared`'s 16, so the clean population is unchanged
* by objectui#6152's reclassification).
* key set equals what the two ledgers TOGETHER record for it — `never` for the 141
* pairs with no entry in either (158 − 17). Six of `RuntimeOnlyDeclared`'s seven
* pairs are a measured subset of `UnmirroredDeclared`'s 16, so objectui#6152's
* reclassification left the clean population unchanged; objectui#6150 then added
* `TreeViewSchema`, whose only entry is runtime-only, which is why the union is 17
* pairs and not 16.
*
* ⚠️ **The discriminating signal is the PER-PAIR set, not this file's exit code.**
* The exit code is a whole-file verdict, so it moves only while the rest of the
Expand Down
43 changes: 43 additions & 0 deletions packages/types/src/complex.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -347,6 +347,16 @@ export interface FilterBuilderSchema extends BaseSchema {
* @default 3
*/
maxDepth?: number;
/**
* Tailwind classes on the outermost wrapper `div`.
*
* READ SITE: `packages/components/src/renderers/complex/filter-builder.tsx:37`
* — `className={schema.wrapperClass || ''}`.
*
* Distinct from {@link BaseSchema.className}, which this renderer applies
* further in. Declared by objectui#6150.
*/
wrapperClass?: string;
}

/**
Expand DownExpand Up@@ -398,6 +408,39 @@ export interface CarouselSchema extends BaseSchema {
* Carousel items
*/
items: CarouselItem[];
/**
* Option bag forwarded VERBATIM to the underlying embla carousel.
*
* READ SITE: `packages/components/src/renderers/complex/carousel.tsx:23` —
* `opts={schema.opts}`, passed straight through to the `Carousel`
* primitive, whose own `opts` is embla's `EmblaOptionsType`.
*
* ⚠️ Deliberately declared OPEN rather than as the two-key shape the docs
* page shows (`{ loop?, align? }`). The renderer forwards the whole bag, so
* every other embla option authored today reaches the library and works;
* declaring the documented pair would REFUSE those authored documents —
* a narrowing of a published surface, which is a ruling and not a
* declaration. objectui#6150 records the capability as it is; picking a
* narrower shape is escalated with that card.
*/
opts?: Record<string, unknown>;
/**
* Scroll axis.
*
* READ SITE: `renderers/complex/carousel.tsx:24` —
* `orientation={schema.orientation || 'horizontal'}`.
*
* @default 'horizontal'
*/
orientation?: 'horizontal' | 'vertical';
/**
* Tailwind classes applied to EACH slide (not the container — that is
* {@link BaseSchema.className}).
*
* READ SITE: `renderers/complex/carousel.tsx:30` —
* `className={schema.itemClassName}` on every `CarouselItem`.
*/
itemClassName?: string;
/**
* Auto-play interval (ms)
*/
Expand Down
43 changes: 42 additions & 1 deletion packages/types/src/data-display.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1111,9 +1111,35 @@ export interface TreeNode {
export interface TreeViewSchema extends BaseSchema {
type: 'tree-view';
/**
* Tree data
* Tree data — the fallback spelling, read only when
* {@link TreeViewSchema.nodes} is absent.
*
* READ SITE: `packages/components/src/renderers/data-display/tree-view.tsx:105`
* — `const rawNodes = boundData || schema.nodes || schema.data || []`.
*/
data: TreeNode[];
/**
* Tree data — the spelling the renderer reads FIRST.
*
* READ SITE: `renderers/data-display/tree-view.tsx:105`, the middle limb of
* `boundData || schema.nodes || schema.data || []`, so `nodes` WINS over
* {@link TreeViewSchema.data} when both are authored (and a `bind`-resolved
* value wins over both).
*
* ⚠️ Declaring `nodes` does NOT by itself make `{ type: 'tree-view', nodes }`
* a legal document: {@link TreeViewSchema.data} stays REQUIRED on both faces,
* so the validator still demands `data`. Relaxing that is an accept-set
* change and a separate ruling — objectui#6150 declares the read, nothing
* more.
*/
nodes?: TreeNode[];
/**
* Heading rendered above the tree.
*
* READ SITE: `renderers/data-display/tree-view.tsx:115` (presence gate) and
* `:117` (the `h3` body).
*/
title?: string;
/**
* Default expanded node IDs
*/
Expand DownExpand Up@@ -1148,6 +1174,21 @@ export interface TreeViewSchema extends BaseSchema {
* Node expand handler
*/
onExpandChange?: (expandedIds: string[]) => void;
/**
* Node click handler — INVOKED, not merely read, so this is a call
* signature and not a value shape.
*
* READ SITE: `packages/components/src/renderers/data-display/tree-view.tsx:98`
* (presence gate `if (schema.onNodeClick)`) and `:99` (the call
* `schema.onNodeClick(node)`), where `node` is the clicked
* {@link TreeNode}. The handler's return value is discarded.
*
* ⚠️ NOT mirrored in `../zod/data-display.zod.ts`, deliberately: a function
* cannot appear in an authored JSON document, so it is a runtime slot.
* objectui#6152 ruled that class never gets a mirror; it is recorded in
* `__tests__/zod-mirror-parity.test.ts`'s `RuntimeOnlyDeclared` instead.
*/
onNodeClick?: (node: TreeNode) => void;
}

/**
Expand Down
27 changes: 27 additions & 0 deletions packages/types/src/form.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -345,6 +345,18 @@ export interface CheckboxSchema extends BaseSchema {
* Error message
*/
error?: string;
/**
* Whether the box must be checked — drives a VISIBLE affordance, not just
* form semantics.
*
* READ SITES: `packages/components/src/renderers/form/checkbox.tsx:45` —
* `required={schema.required}` on the Radix `Checkbox` — and `:49`, where it
* gates the label's required marker
* (`schema.required && "text-destructive after:content-['*']"`).
*
* Declared by objectui#6150; one of the two behavioural keys in that census.
*/
required?: boolean;
/**
* Change handler
*/
Expand DownExpand Up@@ -592,6 +604,21 @@ export interface FileUploadSchema extends BaseSchema {
* Error message
*/
error?: string;
/**
* Label on the drop zone / upload button.
*
* READ SITE: `packages/components/src/renderers/form/file-upload.tsx:123` —
* `{isUploading ? "…" : (schema.buttonText || "DROP PAYLOAD OR CLICK TO UPLOAD")}`.
*/
buttonText?: string;
/**
* Tailwind classes appended to the outer wrapper `div`.
*
* READ SITE: `renderers/form/file-upload.tsx:78` — appended to the
* renderer's own grid classes as
* `` `grid w-full … ${schema.wrapperClass || ''}` ``.
*/
wrapperClass?: string;
/**
* Change handler (receives FileList or File[])
*/
Expand Down
25 changes: 24 additions & 1 deletion packages/types/src/layout.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -76,7 +76,30 @@ export interface TextSpanSchema extends BaseSchema {
export interface TextSchema extends BaseSchema {
type: 'text';
/**
* Text content to display
* Text content to display — the spelling the renderer reads FIRST.
*
* READ SITE: `packages/components/src/renderers/basic/text.tsx:51` (the
* wrapped `span` arm, taken when the node carries a designer id or a
* className) and `:56` (the bare fragment arm), both as
* `{schema.content || schema.value}`. `content` therefore WINS over
* {@link TextSchema.value} whenever both are authored.
*
* Declared by objectui#6150 (undeclared-but-consumed census). Before that
* card the renderer read this key through `BaseSchema`'s
* `[key: string]: any` (objectui#5155) and no shipped type mentioned it —
* the docs page was the only record of a capability that works.
*
* ⚠️ Two spellings for one slot is a dialect, not a design. Retiring one of
* them is an ADR-0049 enforce-or-remove question and is deliberately NOT
* decided here; this declaration records what the renderer does today.
*/
content?: string;
/**
* Text content — the fallback spelling, read only when
* {@link TextSchema.content} is absent or falsy.
*
* READ SITE: `renderers/basic/text.tsx:51,56`, the right-hand side of
* `{schema.content || schema.value}`.
*/
value?: string;
/**
Expand Down
26 changes: 26 additions & 0 deletions packages/types/src/overlay.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -318,6 +318,17 @@ export interface HoverCardSchema extends BaseSchema {
* @default 300
*/
closeDelay?: number;
/**
* Alignment of the card against its trigger.
*
* READ SITE: `packages/components/src/renderers/overlay/hover-card.tsx:24` —
* `align={schema.align}` on `HoverCardContent`, beside the already-declared
* `side={schema.side}`.
*
* Same vocabulary as {@link DropdownMenuSchema.align} and
* {@link PopoverSchema.align}; declared by objectui#6150.
*/
align?: OverlayAlignment;
/**
* Open state change handler
*/
Expand DownExpand Up@@ -458,6 +469,21 @@ export interface ContextMenuSchema extends BaseSchema {
* Element to attach context menu to
*/
children: SchemaNode | SchemaNode[];
/**
* The right-clickable area's content.
*
* READ SITE: `packages/components/src/renderers/overlay/context-menu.tsx:95`
* — `renderChildren(schema.trigger || { type: 'text', value: 'Right click here' })`
* inside `ContextMenuTrigger`. ⚠️ Note the renderer renders `trigger`, NOT
* the declared {@link ContextMenuSchema.children}, which no read site
* consumes.
*
* Declared OPTIONAL although the docs page shows it required: the renderer
* substitutes a placeholder when it is absent, so every document without a
* `trigger` is legal today and declaring it required would refuse them.
* Declared by objectui#6150.
*/
trigger?: SchemaNode | SchemaNode[];
}

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/types/src/zod/complex.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -214,6 +214,8 @@ export const FilterBuilderSchema = BaseSchema.extend({
onChange: z.function().optional().describe('Change handler'),
allowGroups: z.boolean().optional().describe('Allow grouped conditions'),
maxDepth: z.number().optional().describe('Maximum nesting depth'),
wrapperClass: z.string().optional()
.describe("Outer wrapper classes, read at renderers/complex/filter-builder.tsx:37 — `className={schema.wrapperClass || ''}` (objectui#6150)"),
});

/**
Expand All@@ -230,6 +232,12 @@ export const CarouselItemSchema = z.object({
export const CarouselSchema = BaseSchema.extend({
type: z.literal('carousel'),
items: z.array(CarouselItemSchema).describe('Carousel items'),
opts: z.record(z.string(), z.unknown()).optional()
.describe("Embla option bag forwarded verbatim at renderers/complex/carousel.tsx:23 — `opts={schema.opts}`. Left OPEN on purpose: the renderer passes the whole bag through, so narrowing it to the docs' `{loop, align}` pair would refuse authored documents that work today (objectui#6150)"),
orientation: z.enum(['horizontal', 'vertical']).optional()
.describe("Scroll axis, read at renderers/complex/carousel.tsx:24 — `orientation={schema.orientation || 'horizontal'}` (objectui#6150)"),
itemClassName: z.string().optional()
.describe('Per-slide Tailwind classes, read at renderers/complex/carousel.tsx:30 — `className={schema.itemClassName}` on every CarouselItem (objectui#6150)'),
autoPlay: z.number().optional().describe('Auto-play interval (ms)'),
showArrows: z.boolean().optional().describe('Show navigation arrows'),
showDots: z.boolean().optional().describe('Show navigation dots'),
Expand Down
7 changes: 6 additions & 1 deletion packages/types/src/zod/data-display.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -298,7 +298,12 @@ export const TreeNodeSchema: z.ZodType<any> = z.lazy(() =>
*/
export const TreeViewSchema = BaseSchema.extend({
type: z.literal('tree-view'),
data: z.array(TreeNodeSchema).describe('Tree data'),
data: z.array(TreeNodeSchema)
.describe('Tree data, read as the fallback limb of `boundData || schema.nodes || schema.data || []` at renderers/data-display/tree-view.tsx:105'),
nodes: z.array(TreeNodeSchema).optional()
.describe('Tree data, read FIRST at renderers/data-display/tree-view.tsx:105 — the middle limb of `boundData || schema.nodes || schema.data || []`, so it wins over `data`. ⚠️ `data` stays REQUIRED here: declaring `nodes` does not by itself make a `nodes`-only document legal (objectui#6150)'),
title: z.string().optional()
.describe('Heading above the tree, read at renderers/data-display/tree-view.tsx:115 (presence gate) and :117 (the h3 body) (objectui#6150)'),
defaultExpandedIds: z.array(z.string()).optional().describe('Default expanded node IDs'),
defaultSelectedIds: z.array(z.string()).optional().describe('Default selected node IDs'),
expandedIds: z.array(z.string()).optional().describe('Controlled expanded node IDs'),
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .changeset/6150-undeclared-but-consumed-keys.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
---
'@object-ui/types': minor
---

Declare the 13 renderer-read keys that no shipped type declared (objectui#6150)

**This is a published-surface change on `@object-ui/types` and its `zod` mirrors,
and it moves the accept set in TWO directions.** Read the next two paragraphs
before reading the list — they are what the change actually is.

**Key membership is NOT widened — it was never narrow.** All eight touched mirrors
extend `BaseSchema`, which is `.passthrough()`, and `.extend()` carries that policy
through (measured on the built mirrors: `catchall` is `z.unknown()` on all eight).
So before this change every one of the 13 keys already parsed green and already
SURVIVED the parse — admitted unexamined, neither refused nor stripped. Nothing
that parsed before stops parsing because a key became known.

**Value enforcement IS widened, which in the value dimension is a NARROWING.** For
the 12 keys that gained a zod mirror entry, the value is now validated against the
declared type: `{ type: 'text', content: 42 }` parsed green before and is refused
now, at `content`. That is the point of declaring them — `declared === enforced` —
but it is a behaviour change for documents that carried a wrong-typed value under
one of these 13 names. Keys OUTSIDE the 13 are untouched: an undeclared key of any
type is still admitted unexamined on all eight mirrors, pinned per mirror.

The 13, each with the renderer read site the declaration records:

| type | key | declared as | read at |
|---|---|---|---|
| `TextSchema` | `content` | `string` | `renderers/basic/text.tsx` — `{schema.content \|\| schema.value}` |
| `CarouselSchema` | `opts` | `Record<string, unknown>` | `complex/carousel.tsx` — `opts={schema.opts}` |
| `CarouselSchema` | `orientation` | `'horizontal' \| 'vertical'` | `complex/carousel.tsx` |
| `CarouselSchema` | `itemClassName` | `string` | `complex/carousel.tsx` — per-slide class |
| `FilterBuilderSchema` | `wrapperClass` | `string` | `complex/filter-builder.tsx` |
| `TreeViewSchema` | `nodes` | `TreeNode[]` | `data-display/tree-view.tsx` |
| `TreeViewSchema` | `title` | `string` | `data-display/tree-view.tsx` |
| `TreeViewSchema` | `onNodeClick` | `(node: TreeNode) => void` | `data-display/tree-view.tsx` — INVOKED |
| `CheckboxSchema` | `required` | `boolean` | `form/checkbox.tsx` — drives the `*` marker |
| `FileUploadSchema` | `buttonText` | `string` | `form/file-upload.tsx` |
| `FileUploadSchema` | `wrapperClass` | `string` | `form/file-upload.tsx` |
| `HoverCardSchema` | `align` | `OverlayAlignment` | `overlay/hover-card.tsx` |
| `ContextMenuSchema` | `trigger` | `SchemaNode \| SchemaNode[]` | `overlay/context-menu.tsx` |

These compiled before only because `BaseSchema` ends with `[key: string]: any`
(objectui#5155), so the docs page was the single place in the repo recording each
capability, and the one place with no mechanical guard.

Three declarations are deliberately not what "declare what is read" would produce
on its own, and each says so in its own doc comment:

- `CarouselSchema.opts` stays an OPEN bag rather than the docs page's
`{ loop?, align? }` pair. The renderer forwards the whole bag to embla, so
narrowing it to two keys would refuse authored documents that work today.
- `ContextMenuSchema.trigger` is OPTIONAL although the docs page shows it
required; the renderer substitutes a placeholder, so trigger-less documents are
legal today.
- `TreeViewSchema.onNodeClick` gets NO zod mirror. It is invoked, not read as a
value, so it cannot appear in an authored JSON document; objectui#6152 ruled
that class is recorded in `zod-mirror-parity.test.ts`'s `RuntimeOnlyDeclared`
instead, and it is (the first pair to sit there without also sitting in
`UnmirroredDeclared`, so that file's two counts move with it).

Two of the 13 declare a SECOND spelling for a slot that already had one —
`TextSchema.content` beside `value`, `TreeViewSchema.nodes` beside `data` — because
that is what the renderers read. Retiring either spelling is an ADR-0049
enforce-or-remove question and is deliberately not decided here. Declaring `nodes`
also does not by itself make a `nodes`-only tree-view document legal: `data` stays
required on both faces.

Large diffs are not rendered by default.

37 changes: 29 additions & 8 deletions packages/types/src/__tests__/zod-mirror-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,9 +65,11 @@
* Anything citing "121" as the mirroring debt is citing a number that changed
* meaning — the comparable figure is 97 + 1 mirrored + 23 reclassified. The
* full statement is on that ledger.
* - **6 entries** in `RuntimeOnlyDeclared`, **23 keys** across them — a strict
* subset of the 16 pairs above, so the "no entry in either" population is
* unchanged.
* - **7 entries** in `RuntimeOnlyDeclared`, **24 keys** across them. Six of the
* seven are a subset of the 16 pairs above; `TreeViewSchema` is NOT — it is
* the first pair whose ONLY ledger entry is a runtime-only one
* (objectui#6150 declared `onNodeClick` on an otherwise clean pair), so the
* "no entry in either" population dropped by one to 141.
* - 158 − 12 = **146**, the "pairs with no entry" `LedgerMismatch` speaks of.
*
* ## Two ratchets, because the forward comparison has two halves
Expand DownExpand Up@@ -248,7 +250,7 @@ export type ReconcileAgainstLedger< K, Measured, Recorded > =
export type assertionRatchetAcceptsAgreement =
Expect< Equal< ReconcileAgainstLedger< 'p', 'a', 'a' >, never > >;

/** …and so does a clean pair with no entry, which is the case for 142 of the 158. */
/** …and so does a clean pair with no entry, which is the case for 141 of the 158. */
export type assertionRatchetAcceptsCleanPair =
Expect< Equal< ReconcileAgainstLedger< 'p', never, never >, never > >;

Expand DownExpand Up@@ -1040,6 +1042,23 @@ interface RuntimeOnlyDeclared {
* pair. POLICY group.
*/
'objectql.zod.ts#ObjectViewSchema': 'onNavigate';
/**
* `TreeViewSchema`'s ONLY entry in either ledger — the pair was clean before
* objectui#6150 and this key is the whole of its debt.
*
* OVERSIGHT group by mirror shape (`onSelectChange` and `onExpandChange` are
* mirrored beside it as `z.function()`), but it arrives here as a DECLARATION,
* not a discovery: objectui#6150's census measured `schema.onNodeClick` INVOKED
* at `renderers/data-display/tree-view.tsx:98,99` against a type that declared
* nothing, and the card declared it. A function cannot appear in an authored
* JSON document, so the key is a runtime slot and objectui#6152's ruling routes
* it here rather than to a mirror — the step-3 exception in the header above,
* used exactly as written.
*
* ⚠️ This is the first pair to sit in `RuntimeOnlyDeclared` without also sitting
* in `UnmirroredDeclared`; the two counts in the file header record that.
*/
'data-display.zod.ts#TreeViewSchema': 'onNodeClick';
/**
* 3 of `DetailViewSchema`'s former 14 — the exact three the 2026-07 audit named. By
* mirror SHAPE this is the oversight group (`onBack` is mirrored, as `z.string()`),
Expand DownExpand Up@@ -1169,10 +1188,12 @@ export const assertionDriftMatchesLedger: never = 0 as unknown as LedgerMismatch

/**
* The SECOND half of the forward comparison: every pair's declared-but-unmirrored
* key set equals what the two ledgers TOGETHER record for it — `never` for the 142
* pairs with no entry in either (158 − 16; `RuntimeOnlyDeclared`'s 6 pairs are a
* measured subset of `UnmirroredDeclared`'s 16, so the clean population is unchanged
* by objectui#6152's reclassification).
* key set equals what the two ledgers TOGETHER record for it — `never` for the 141
* pairs with no entry in either (158 − 17). Six of `RuntimeOnlyDeclared`'s seven
* pairs are a measured subset of `UnmirroredDeclared`'s 16, so objectui#6152's
* reclassification left the clean population unchanged; objectui#6150 then added
* `TreeViewSchema`, whose only entry is runtime-only, which is why the union is 17
* pairs and not 16.
*
* ⚠️ **The discriminating signal is the PER-PAIR set, not this file's exit code.**
* The exit code is a whole-file verdict, so it moves only while the rest of the
Expand Down
43 changes: 43 additions & 0 deletions packages/types/src/complex.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -347,6 +347,16 @@ export interface FilterBuilderSchema extends BaseSchema {
* @default 3
*/
maxDepth?: number;
/**
* Tailwind classes on the outermost wrapper `div`.
*
* READ SITE: `packages/components/src/renderers/complex/filter-builder.tsx:37`
* — `className={schema.wrapperClass || ''}`.
*
* Distinct from {@link BaseSchema.className}, which this renderer applies
* further in. Declared by objectui#6150.
*/
wrapperClass?: string;
}

/**
Expand DownExpand Up@@ -398,6 +408,39 @@ export interface CarouselSchema extends BaseSchema {
* Carousel items
*/
items: CarouselItem[];
/**
* Option bag forwarded VERBATIM to the underlying embla carousel.
*
* READ SITE: `packages/components/src/renderers/complex/carousel.tsx:23` —
* `opts={schema.opts}`, passed straight through to the `Carousel`
* primitive, whose own `opts` is embla's `EmblaOptionsType`.
*
* ⚠️ Deliberately declared OPEN rather than as the two-key shape the docs
* page shows (`{ loop?, align? }`). The renderer forwards the whole bag, so
* every other embla option authored today reaches the library and works;
* declaring the documented pair would REFUSE those authored documents —
* a narrowing of a published surface, which is a ruling and not a
* declaration. objectui#6150 records the capability as it is; picking a
* narrower shape is escalated with that card.
*/
opts?: Record<string, unknown>;
/**
* Scroll axis.
*
* READ SITE: `renderers/complex/carousel.tsx:24` —
* `orientation={schema.orientation || 'horizontal'}`.
*
* @default 'horizontal'
*/
orientation?: 'horizontal' | 'vertical';
/**
* Tailwind classes applied to EACH slide (not the container — that is
* {@link BaseSchema.className}).
*
* READ SITE: `renderers/complex/carousel.tsx:30` —
* `className={schema.itemClassName}` on every `CarouselItem`.
*/
itemClassName?: string;
/**
* Auto-play interval (ms)
*/
Expand Down
43 changes: 42 additions & 1 deletion packages/types/src/data-display.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1111,9 +1111,35 @@ export interface TreeNode {
export interface TreeViewSchema extends BaseSchema {
type: 'tree-view';
/**
* Tree data
* Tree data — the fallback spelling, read only when
* {@link TreeViewSchema.nodes} is absent.
*
* READ SITE: `packages/components/src/renderers/data-display/tree-view.tsx:105`
* — `const rawNodes = boundData || schema.nodes || schema.data || []`.
*/
data: TreeNode[];
/**
* Tree data — the spelling the renderer reads FIRST.
*
* READ SITE: `renderers/data-display/tree-view.tsx:105`, the middle limb of
* `boundData || schema.nodes || schema.data || []`, so `nodes` WINS over
* {@link TreeViewSchema.data} when both are authored (and a `bind`-resolved
* value wins over both).
*
* ⚠️ Declaring `nodes` does NOT by itself make `{ type: 'tree-view', nodes }`
* a legal document: {@link TreeViewSchema.data} stays REQUIRED on both faces,
* so the validator still demands `data`. Relaxing that is an accept-set
* change and a separate ruling — objectui#6150 declares the read, nothing
* more.
*/
nodes?: TreeNode[];
/**
* Heading rendered above the tree.
*
* READ SITE: `renderers/data-display/tree-view.tsx:115` (presence gate) and
* `:117` (the `h3` body).
*/
title?: string;
/**
* Default expanded node IDs
*/
Expand DownExpand Up@@ -1148,6 +1174,21 @@ export interface TreeViewSchema extends BaseSchema {
* Node expand handler
*/
onExpandChange?: (expandedIds: string[]) => void;
/**
* Node click handler — INVOKED, not merely read, so this is a call
* signature and not a value shape.
*
* READ SITE: `packages/components/src/renderers/data-display/tree-view.tsx:98`
* (presence gate `if (schema.onNodeClick)`) and `:99` (the call
* `schema.onNodeClick(node)`), where `node` is the clicked
* {@link TreeNode}. The handler's return value is discarded.
*
* ⚠️ NOT mirrored in `../zod/data-display.zod.ts`, deliberately: a function
* cannot appear in an authored JSON document, so it is a runtime slot.
* objectui#6152 ruled that class never gets a mirror; it is recorded in
* `__tests__/zod-mirror-parity.test.ts`'s `RuntimeOnlyDeclared` instead.
*/
onNodeClick?: (node: TreeNode) => void;
}

/**
Expand Down
27 changes: 27 additions & 0 deletions packages/types/src/form.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -345,6 +345,18 @@ export interface CheckboxSchema extends BaseSchema {
* Error message
*/
error?: string;
/**
* Whether the box must be checked — drives a VISIBLE affordance, not just
* form semantics.
*
* READ SITES: `packages/components/src/renderers/form/checkbox.tsx:45` —
* `required={schema.required}` on the Radix `Checkbox` — and `:49`, where it
* gates the label's required marker
* (`schema.required && "text-destructive after:content-['*']"`).
*
* Declared by objectui#6150; one of the two behavioural keys in that census.
*/
required?: boolean;
/**
* Change handler
*/
Expand DownExpand Up@@ -592,6 +604,21 @@ export interface FileUploadSchema extends BaseSchema {
* Error message
*/
error?: string;
/**
* Label on the drop zone / upload button.
*
* READ SITE: `packages/components/src/renderers/form/file-upload.tsx:123` —
* `{isUploading ? "…" : (schema.buttonText || "DROP PAYLOAD OR CLICK TO UPLOAD")}`.
*/
buttonText?: string;
/**
* Tailwind classes appended to the outer wrapper `div`.
*
* READ SITE: `renderers/form/file-upload.tsx:78` — appended to the
* renderer's own grid classes as
* `` `grid w-full … ${schema.wrapperClass || ''}` ``.
*/
wrapperClass?: string;
/**
* Change handler (receives FileList or File[])
*/
Expand Down
25 changes: 24 additions & 1 deletion packages/types/src/layout.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -76,7 +76,30 @@ export interface TextSpanSchema extends BaseSchema {
export interface TextSchema extends BaseSchema {
type: 'text';
/**
* Text content to display
* Text content to display — the spelling the renderer reads FIRST.
*
* READ SITE: `packages/components/src/renderers/basic/text.tsx:51` (the
* wrapped `span` arm, taken when the node carries a designer id or a
* className) and `:56` (the bare fragment arm), both as
* `{schema.content || schema.value}`. `content` therefore WINS over
* {@link TextSchema.value} whenever both are authored.
*
* Declared by objectui#6150 (undeclared-but-consumed census). Before that
* card the renderer read this key through `BaseSchema`'s
* `[key: string]: any` (objectui#5155) and no shipped type mentioned it —
* the docs page was the only record of a capability that works.
*
* ⚠️ Two spellings for one slot is a dialect, not a design. Retiring one of
* them is an ADR-0049 enforce-or-remove question and is deliberately NOT
* decided here; this declaration records what the renderer does today.
*/
content?: string;
/**
* Text content — the fallback spelling, read only when
* {@link TextSchema.content} is absent or falsy.
*
* READ SITE: `renderers/basic/text.tsx:51,56`, the right-hand side of
* `{schema.content || schema.value}`.
*/
value?: string;
/**
Expand Down
26 changes: 26 additions & 0 deletions packages/types/src/overlay.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -318,6 +318,17 @@ export interface HoverCardSchema extends BaseSchema {
* @default 300
*/
closeDelay?: number;
/**
* Alignment of the card against its trigger.
*
* READ SITE: `packages/components/src/renderers/overlay/hover-card.tsx:24` —
* `align={schema.align}` on `HoverCardContent`, beside the already-declared
* `side={schema.side}`.
*
* Same vocabulary as {@link DropdownMenuSchema.align} and
* {@link PopoverSchema.align}; declared by objectui#6150.
*/
align?: OverlayAlignment;
/**
* Open state change handler
*/
Expand DownExpand Up@@ -458,6 +469,21 @@ export interface ContextMenuSchema extends BaseSchema {
* Element to attach context menu to
*/
children: SchemaNode | SchemaNode[];
/**
* The right-clickable area's content.
*
* READ SITE: `packages/components/src/renderers/overlay/context-menu.tsx:95`
* — `renderChildren(schema.trigger || { type: 'text', value: 'Right click here' })`
* inside `ContextMenuTrigger`. ⚠️ Note the renderer renders `trigger`, NOT
* the declared {@link ContextMenuSchema.children}, which no read site
* consumes.
*
* Declared OPTIONAL although the docs page shows it required: the renderer
* substitutes a placeholder when it is absent, so every document without a
* `trigger` is legal today and declaring it required would refuse them.
* Declared by objectui#6150.
*/
trigger?: SchemaNode | SchemaNode[];
}

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/types/src/zod/complex.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -214,6 +214,8 @@ export const FilterBuilderSchema = BaseSchema.extend({
onChange: z.function().optional().describe('Change handler'),
allowGroups: z.boolean().optional().describe('Allow grouped conditions'),
maxDepth: z.number().optional().describe('Maximum nesting depth'),
wrapperClass: z.string().optional()
.describe("Outer wrapper classes, read at renderers/complex/filter-builder.tsx:37 — `className={schema.wrapperClass || ''}` (objectui#6150)"),
});

/**
Expand All@@ -230,6 +232,12 @@ export const CarouselItemSchema = z.object({
export const CarouselSchema = BaseSchema.extend({
type: z.literal('carousel'),
items: z.array(CarouselItemSchema).describe('Carousel items'),
opts: z.record(z.string(), z.unknown()).optional()
.describe("Embla option bag forwarded verbatim at renderers/complex/carousel.tsx:23 — `opts={schema.opts}`. Left OPEN on purpose: the renderer passes the whole bag through, so narrowing it to the docs' `{loop, align}` pair would refuse authored documents that work today (objectui#6150)"),
orientation: z.enum(['horizontal', 'vertical']).optional()
.describe("Scroll axis, read at renderers/complex/carousel.tsx:24 — `orientation={schema.orientation || 'horizontal'}` (objectui#6150)"),
itemClassName: z.string().optional()
.describe('Per-slide Tailwind classes, read at renderers/complex/carousel.tsx:30 — `className={schema.itemClassName}` on every CarouselItem (objectui#6150)'),
autoPlay: z.number().optional().describe('Auto-play interval (ms)'),
showArrows: z.boolean().optional().describe('Show navigation arrows'),
showDots: z.boolean().optional().describe('Show navigation dots'),
Expand Down
7 changes: 6 additions & 1 deletion packages/types/src/zod/data-display.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -298,7 +298,12 @@ export const TreeNodeSchema: z.ZodType<any> = z.lazy(() =>
*/
export const TreeViewSchema = BaseSchema.extend({
type: z.literal('tree-view'),
data: z.array(TreeNodeSchema).describe('Tree data'),
data: z.array(TreeNodeSchema)
.describe('Tree data, read as the fallback limb of `boundData || schema.nodes || schema.data || []` at renderers/data-display/tree-view.tsx:105'),
nodes: z.array(TreeNodeSchema).optional()
.describe('Tree data, read FIRST at renderers/data-display/tree-view.tsx:105 — the middle limb of `boundData || schema.nodes || schema.data || []`, so it wins over `data`. ⚠️ `data` stays REQUIRED here: declaring `nodes` does not by itself make a `nodes`-only document legal (objectui#6150)'),
title: z.string().optional()
.describe('Heading above the tree, read at renderers/data-display/tree-view.tsx:115 (presence gate) and :117 (the h3 body) (objectui#6150)'),
defaultExpandedIds: z.array(z.string()).optional().describe('Default expanded node IDs'),
defaultSelectedIds: z.array(z.string()).optional().describe('Default selected node IDs'),
expandedIds: z.array(z.string()).optional().describe('Controlled expanded node IDs'),
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .changeset/6150-undeclared-but-consumed-keys.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
---
'@object-ui/types': minor
---

Declare the 13 renderer-read keys that no shipped type declared (objectui#6150)

**This is a published-surface change on `@object-ui/types` and its `zod` mirrors,
and it moves the accept set in TWO directions.** Read the next two paragraphs
before reading the list — they are what the change actually is.

**Key membership is NOT widened — it was never narrow.** All eight touched mirrors
extend `BaseSchema`, which is `.passthrough()`, and `.extend()` carries that policy
through (measured on the built mirrors: `catchall` is `z.unknown()` on all eight).
So before this change every one of the 13 keys already parsed green and already
SURVIVED the parse — admitted unexamined, neither refused nor stripped. Nothing
that parsed before stops parsing because a key became known.

**Value enforcement IS widened, which in the value dimension is a NARROWING.** For
the 12 keys that gained a zod mirror entry, the value is now validated against the
declared type: `{ type: 'text', content: 42 }` parsed green before and is refused
now, at `content`. That is the point of declaring them — `declared === enforced` —
but it is a behaviour change for documents that carried a wrong-typed value under
one of these 13 names. Keys OUTSIDE the 13 are untouched: an undeclared key of any
type is still admitted unexamined on all eight mirrors, pinned per mirror.

The 13, each with the renderer read site the declaration records:

| type | key | declared as | read at |
|---|---|---|---|
| `TextSchema` | `content` | `string` | `renderers/basic/text.tsx` — `{schema.content \|\| schema.value}` |
| `CarouselSchema` | `opts` | `Record<string, unknown>` | `complex/carousel.tsx` — `opts={schema.opts}` |
| `CarouselSchema` | `orientation` | `'horizontal' \| 'vertical'` | `complex/carousel.tsx` |
| `CarouselSchema` | `itemClassName` | `string` | `complex/carousel.tsx` — per-slide class |
| `FilterBuilderSchema` | `wrapperClass` | `string` | `complex/filter-builder.tsx` |
| `TreeViewSchema` | `nodes` | `TreeNode[]` | `data-display/tree-view.tsx` |
| `TreeViewSchema` | `title` | `string` | `data-display/tree-view.tsx` |
| `TreeViewSchema` | `onNodeClick` | `(node: TreeNode) => void` | `data-display/tree-view.tsx` — INVOKED |
| `CheckboxSchema` | `required` | `boolean` | `form/checkbox.tsx` — drives the `*` marker |
| `FileUploadSchema` | `buttonText` | `string` | `form/file-upload.tsx` |
| `FileUploadSchema` | `wrapperClass` | `string` | `form/file-upload.tsx` |
| `HoverCardSchema` | `align` | `OverlayAlignment` | `overlay/hover-card.tsx` |
| `ContextMenuSchema` | `trigger` | `SchemaNode \| SchemaNode[]` | `overlay/context-menu.tsx` |

These compiled before only because `BaseSchema` ends with `[key: string]: any`
(objectui#5155), so the docs page was the single place in the repo recording each
capability, and the one place with no mechanical guard.

Three declarations are deliberately not what "declare what is read" would produce
on its own, and each says so in its own doc comment:

- `CarouselSchema.opts` stays an OPEN bag rather than the docs page's
`{ loop?, align? }` pair. The renderer forwards the whole bag to embla, so
narrowing it to two keys would refuse authored documents that work today.
- `ContextMenuSchema.trigger` is OPTIONAL although the docs page shows it
required; the renderer substitutes a placeholder, so trigger-less documents are
legal today.
- `TreeViewSchema.onNodeClick` gets NO zod mirror. It is invoked, not read as a
value, so it cannot appear in an authored JSON document; objectui#6152 ruled
that class is recorded in `zod-mirror-parity.test.ts`'s `RuntimeOnlyDeclared`
instead, and it is (the first pair to sit there without also sitting in
`UnmirroredDeclared`, so that file's two counts move with it).

Two of the 13 declare a SECOND spelling for a slot that already had one —
`TextSchema.content` beside `value`, `TreeViewSchema.nodes` beside `data` — because
that is what the renderers read. Retiring either spelling is an ADR-0049
enforce-or-remove question and is deliberately not decided here. Declaring `nodes`
also does not by itself make a `nodes`-only tree-view document legal: `data` stays
required on both faces.

Large diffs are not rendered by default.

37 changes: 29 additions & 8 deletions packages/types/src/__tests__/zod-mirror-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,9 +65,11 @@
* Anything citing "121" as the mirroring debt is citing a number that changed
* meaning — the comparable figure is 97 + 1 mirrored + 23 reclassified. The
* full statement is on that ledger.
* - **6 entries** in `RuntimeOnlyDeclared`, **23 keys** across them — a strict
* subset of the 16 pairs above, so the "no entry in either" population is
* unchanged.
* - **7 entries** in `RuntimeOnlyDeclared`, **24 keys** across them. Six of the
* seven are a subset of the 16 pairs above; `TreeViewSchema` is NOT — it is
* the first pair whose ONLY ledger entry is a runtime-only one
* (objectui#6150 declared `onNodeClick` on an otherwise clean pair), so the
* "no entry in either" population dropped by one to 141.
* - 158 − 12 = **146**, the "pairs with no entry" `LedgerMismatch` speaks of.
*
* ## Two ratchets, because the forward comparison has two halves
Expand DownExpand Up@@ -248,7 +250,7 @@ export type ReconcileAgainstLedger< K, Measured, Recorded > =
export type assertionRatchetAcceptsAgreement =
Expect< Equal< ReconcileAgainstLedger< 'p', 'a', 'a' >, never > >;

/** …and so does a clean pair with no entry, which is the case for 142 of the 158. */
/** …and so does a clean pair with no entry, which is the case for 141 of the 158. */
export type assertionRatchetAcceptsCleanPair =
Expect< Equal< ReconcileAgainstLedger< 'p', never, never >, never > >;

Expand DownExpand Up@@ -1040,6 +1042,23 @@ interface RuntimeOnlyDeclared {
* pair. POLICY group.
*/
'objectql.zod.ts#ObjectViewSchema': 'onNavigate';
/**
* `TreeViewSchema`'s ONLY entry in either ledger — the pair was clean before
* objectui#6150 and this key is the whole of its debt.
*
* OVERSIGHT group by mirror shape (`onSelectChange` and `onExpandChange` are
* mirrored beside it as `z.function()`), but it arrives here as a DECLARATION,
* not a discovery: objectui#6150's census measured `schema.onNodeClick` INVOKED
* at `renderers/data-display/tree-view.tsx:98,99` against a type that declared
* nothing, and the card declared it. A function cannot appear in an authored
* JSON document, so the key is a runtime slot and objectui#6152's ruling routes
* it here rather than to a mirror — the step-3 exception in the header above,
* used exactly as written.
*
* ⚠️ This is the first pair to sit in `RuntimeOnlyDeclared` without also sitting
* in `UnmirroredDeclared`; the two counts in the file header record that.
*/
'data-display.zod.ts#TreeViewSchema': 'onNodeClick';
/**
* 3 of `DetailViewSchema`'s former 14 — the exact three the 2026-07 audit named. By
* mirror SHAPE this is the oversight group (`onBack` is mirrored, as `z.string()`),
Expand DownExpand Up@@ -1169,10 +1188,12 @@ export const assertionDriftMatchesLedger: never = 0 as unknown as LedgerMismatch

/**
* The SECOND half of the forward comparison: every pair's declared-but-unmirrored
* key set equals what the two ledgers TOGETHER record for it — `never` for the 142
* pairs with no entry in either (158 − 16; `RuntimeOnlyDeclared`'s 6 pairs are a
* measured subset of `UnmirroredDeclared`'s 16, so the clean population is unchanged
* by objectui#6152's reclassification).
* key set equals what the two ledgers TOGETHER record for it — `never` for the 141
* pairs with no entry in either (158 − 17). Six of `RuntimeOnlyDeclared`'s seven
* pairs are a measured subset of `UnmirroredDeclared`'s 16, so objectui#6152's
* reclassification left the clean population unchanged; objectui#6150 then added
* `TreeViewSchema`, whose only entry is runtime-only, which is why the union is 17
* pairs and not 16.
*
* ⚠️ **The discriminating signal is the PER-PAIR set, not this file's exit code.**
* The exit code is a whole-file verdict, so it moves only while the rest of the
Expand Down
43 changes: 43 additions & 0 deletions packages/types/src/complex.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -347,6 +347,16 @@ export interface FilterBuilderSchema extends BaseSchema {
* @default 3
*/
maxDepth?: number;
/**
* Tailwind classes on the outermost wrapper `div`.
*
* READ SITE: `packages/components/src/renderers/complex/filter-builder.tsx:37`
* — `className={schema.wrapperClass || ''}`.
*
* Distinct from {@link BaseSchema.className}, which this renderer applies
* further in. Declared by objectui#6150.
*/
wrapperClass?: string;
}

/**
Expand DownExpand Up@@ -398,6 +408,39 @@ export interface CarouselSchema extends BaseSchema {
* Carousel items
*/
items: CarouselItem[];
/**
* Option bag forwarded VERBATIM to the underlying embla carousel.
*
* READ SITE: `packages/components/src/renderers/complex/carousel.tsx:23` —
* `opts={schema.opts}`, passed straight through to the `Carousel`
* primitive, whose own `opts` is embla's `EmblaOptionsType`.
*
* ⚠️ Deliberately declared OPEN rather than as the two-key shape the docs
* page shows (`{ loop?, align? }`). The renderer forwards the whole bag, so
* every other embla option authored today reaches the library and works;
* declaring the documented pair would REFUSE those authored documents —
* a narrowing of a published surface, which is a ruling and not a
* declaration. objectui#6150 records the capability as it is; picking a
* narrower shape is escalated with that card.
*/
opts?: Record<string, unknown>;
/**
* Scroll axis.
*
* READ SITE: `renderers/complex/carousel.tsx:24` —
* `orientation={schema.orientation || 'horizontal'}`.
*
* @default 'horizontal'
*/
orientation?: 'horizontal' | 'vertical';
/**
* Tailwind classes applied to EACH slide (not the container — that is
* {@link BaseSchema.className}).
*
* READ SITE: `renderers/complex/carousel.tsx:30` —
* `className={schema.itemClassName}` on every `CarouselItem`.
*/
itemClassName?: string;
/**
* Auto-play interval (ms)
*/
Expand Down
43 changes: 42 additions & 1 deletion packages/types/src/data-display.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1111,9 +1111,35 @@ export interface TreeNode {
export interface TreeViewSchema extends BaseSchema {
type: 'tree-view';
/**
* Tree data
* Tree data — the fallback spelling, read only when
* {@link TreeViewSchema.nodes} is absent.
*
* READ SITE: `packages/components/src/renderers/data-display/tree-view.tsx:105`
* — `const rawNodes = boundData || schema.nodes || schema.data || []`.
*/
data: TreeNode[];
/**
* Tree data — the spelling the renderer reads FIRST.
*
* READ SITE: `renderers/data-display/tree-view.tsx:105`, the middle limb of
* `boundData || schema.nodes || schema.data || []`, so `nodes` WINS over
* {@link TreeViewSchema.data} when both are authored (and a `bind`-resolved
* value wins over both).
*
* ⚠️ Declaring `nodes` does NOT by itself make `{ type: 'tree-view', nodes }`
* a legal document: {@link TreeViewSchema.data} stays REQUIRED on both faces,
* so the validator still demands `data`. Relaxing that is an accept-set
* change and a separate ruling — objectui#6150 declares the read, nothing
* more.
*/
nodes?: TreeNode[];
/**
* Heading rendered above the tree.
*
* READ SITE: `renderers/data-display/tree-view.tsx:115` (presence gate) and
* `:117` (the `h3` body).
*/
title?: string;
/**
* Default expanded node IDs
*/
Expand DownExpand Up@@ -1148,6 +1174,21 @@ export interface TreeViewSchema extends BaseSchema {
* Node expand handler
*/
onExpandChange?: (expandedIds: string[]) => void;
/**
* Node click handler — INVOKED, not merely read, so this is a call
* signature and not a value shape.
*
* READ SITE: `packages/components/src/renderers/data-display/tree-view.tsx:98`
* (presence gate `if (schema.onNodeClick)`) and `:99` (the call
* `schema.onNodeClick(node)`), where `node` is the clicked
* {@link TreeNode}. The handler's return value is discarded.
*
* ⚠️ NOT mirrored in `../zod/data-display.zod.ts`, deliberately: a function
* cannot appear in an authored JSON document, so it is a runtime slot.
* objectui#6152 ruled that class never gets a mirror; it is recorded in
* `__tests__/zod-mirror-parity.test.ts`'s `RuntimeOnlyDeclared` instead.
*/
onNodeClick?: (node: TreeNode) => void;
}

/**
Expand Down
27 changes: 27 additions & 0 deletions packages/types/src/form.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -345,6 +345,18 @@ export interface CheckboxSchema extends BaseSchema {
* Error message
*/
error?: string;
/**
* Whether the box must be checked — drives a VISIBLE affordance, not just
* form semantics.
*
* READ SITES: `packages/components/src/renderers/form/checkbox.tsx:45` —
* `required={schema.required}` on the Radix `Checkbox` — and `:49`, where it
* gates the label's required marker
* (`schema.required && "text-destructive after:content-['*']"`).
*
* Declared by objectui#6150; one of the two behavioural keys in that census.
*/
required?: boolean;
/**
* Change handler
*/
Expand DownExpand Up@@ -592,6 +604,21 @@ export interface FileUploadSchema extends BaseSchema {
* Error message
*/
error?: string;
/**
* Label on the drop zone / upload button.
*
* READ SITE: `packages/components/src/renderers/form/file-upload.tsx:123` —
* `{isUploading ? "…" : (schema.buttonText || "DROP PAYLOAD OR CLICK TO UPLOAD")}`.
*/
buttonText?: string;
/**
* Tailwind classes appended to the outer wrapper `div`.
*
* READ SITE: `renderers/form/file-upload.tsx:78` — appended to the
* renderer's own grid classes as
* `` `grid w-full … ${schema.wrapperClass || ''}` ``.
*/
wrapperClass?: string;
/**
* Change handler (receives FileList or File[])
*/
Expand Down
25 changes: 24 additions & 1 deletion packages/types/src/layout.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -76,7 +76,30 @@ export interface TextSpanSchema extends BaseSchema {
export interface TextSchema extends BaseSchema {
type: 'text';
/**
* Text content to display
* Text content to display — the spelling the renderer reads FIRST.
*
* READ SITE: `packages/components/src/renderers/basic/text.tsx:51` (the
* wrapped `span` arm, taken when the node carries a designer id or a
* className) and `:56` (the bare fragment arm), both as
* `{schema.content || schema.value}`. `content` therefore WINS over
* {@link TextSchema.value} whenever both are authored.
*
* Declared by objectui#6150 (undeclared-but-consumed census). Before that
* card the renderer read this key through `BaseSchema`'s
* `[key: string]: any` (objectui#5155) and no shipped type mentioned it —
* the docs page was the only record of a capability that works.
*
* ⚠️ Two spellings for one slot is a dialect, not a design. Retiring one of
* them is an ADR-0049 enforce-or-remove question and is deliberately NOT
* decided here; this declaration records what the renderer does today.
*/
content?: string;
/**
* Text content — the fallback spelling, read only when
* {@link TextSchema.content} is absent or falsy.
*
* READ SITE: `renderers/basic/text.tsx:51,56`, the right-hand side of
* `{schema.content || schema.value}`.
*/
value?: string;
/**
Expand Down
26 changes: 26 additions & 0 deletions packages/types/src/overlay.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -318,6 +318,17 @@ export interface HoverCardSchema extends BaseSchema {
* @default 300
*/
closeDelay?: number;
/**
* Alignment of the card against its trigger.
*
* READ SITE: `packages/components/src/renderers/overlay/hover-card.tsx:24` —
* `align={schema.align}` on `HoverCardContent`, beside the already-declared
* `side={schema.side}`.
*
* Same vocabulary as {@link DropdownMenuSchema.align} and
* {@link PopoverSchema.align}; declared by objectui#6150.
*/
align?: OverlayAlignment;
/**
* Open state change handler
*/
Expand DownExpand Up@@ -458,6 +469,21 @@ export interface ContextMenuSchema extends BaseSchema {
* Element to attach context menu to
*/
children: SchemaNode | SchemaNode[];
/**
* The right-clickable area's content.
*
* READ SITE: `packages/components/src/renderers/overlay/context-menu.tsx:95`
* — `renderChildren(schema.trigger || { type: 'text', value: 'Right click here' })`
* inside `ContextMenuTrigger`. ⚠️ Note the renderer renders `trigger`, NOT
* the declared {@link ContextMenuSchema.children}, which no read site
* consumes.
*
* Declared OPTIONAL although the docs page shows it required: the renderer
* substitutes a placeholder when it is absent, so every document without a
* `trigger` is legal today and declaring it required would refuse them.
* Declared by objectui#6150.
*/
trigger?: SchemaNode | SchemaNode[];
}

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/types/src/zod/complex.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -214,6 +214,8 @@ export const FilterBuilderSchema = BaseSchema.extend({
onChange: z.function().optional().describe('Change handler'),
allowGroups: z.boolean().optional().describe('Allow grouped conditions'),
maxDepth: z.number().optional().describe('Maximum nesting depth'),
wrapperClass: z.string().optional()
.describe("Outer wrapper classes, read at renderers/complex/filter-builder.tsx:37 — `className={schema.wrapperClass || ''}` (objectui#6150)"),
});

/**
Expand All@@ -230,6 +232,12 @@ export const CarouselItemSchema = z.object({
export const CarouselSchema = BaseSchema.extend({
type: z.literal('carousel'),
items: z.array(CarouselItemSchema).describe('Carousel items'),
opts: z.record(z.string(), z.unknown()).optional()
.describe("Embla option bag forwarded verbatim at renderers/complex/carousel.tsx:23 — `opts={schema.opts}`. Left OPEN on purpose: the renderer passes the whole bag through, so narrowing it to the docs' `{loop, align}` pair would refuse authored documents that work today (objectui#6150)"),
orientation: z.enum(['horizontal', 'vertical']).optional()
.describe("Scroll axis, read at renderers/complex/carousel.tsx:24 — `orientation={schema.orientation || 'horizontal'}` (objectui#6150)"),
itemClassName: z.string().optional()
.describe('Per-slide Tailwind classes, read at renderers/complex/carousel.tsx:30 — `className={schema.itemClassName}` on every CarouselItem (objectui#6150)'),
autoPlay: z.number().optional().describe('Auto-play interval (ms)'),
showArrows: z.boolean().optional().describe('Show navigation arrows'),
showDots: z.boolean().optional().describe('Show navigation dots'),
Expand Down
7 changes: 6 additions & 1 deletion packages/types/src/zod/data-display.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -298,7 +298,12 @@ export const TreeNodeSchema: z.ZodType<any> = z.lazy(() =>
*/
export const TreeViewSchema = BaseSchema.extend({
type: z.literal('tree-view'),
data: z.array(TreeNodeSchema).describe('Tree data'),
data: z.array(TreeNodeSchema)
.describe('Tree data, read as the fallback limb of `boundData || schema.nodes || schema.data || []` at renderers/data-display/tree-view.tsx:105'),
nodes: z.array(TreeNodeSchema).optional()
.describe('Tree data, read FIRST at renderers/data-display/tree-view.tsx:105 — the middle limb of `boundData || schema.nodes || schema.data || []`, so it wins over `data`. ⚠️ `data` stays REQUIRED here: declaring `nodes` does not by itself make a `nodes`-only document legal (objectui#6150)'),
title: z.string().optional()
.describe('Heading above the tree, read at renderers/data-display/tree-view.tsx:115 (presence gate) and :117 (the h3 body) (objectui#6150)'),
defaultExpandedIds: z.array(z.string()).optional().describe('Default expanded node IDs'),
defaultSelectedIds: z.array(z.string()).optional().describe('Default selected node IDs'),
expandedIds: z.array(z.string()).optional().describe('Controlled expanded node IDs'),
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .changeset/6150-undeclared-but-consumed-keys.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
---
'@object-ui/types': minor
---

Declare the 13 renderer-read keys that no shipped type declared (objectui#6150)

**This is a published-surface change on `@object-ui/types` and its `zod` mirrors,
and it moves the accept set in TWO directions.** Read the next two paragraphs
before reading the list — they are what the change actually is.

**Key membership is NOT widened — it was never narrow.** All eight touched mirrors
extend `BaseSchema`, which is `.passthrough()`, and `.extend()` carries that policy
through (measured on the built mirrors: `catchall` is `z.unknown()` on all eight).
So before this change every one of the 13 keys already parsed green and already
SURVIVED the parse — admitted unexamined, neither refused nor stripped. Nothing
that parsed before stops parsing because a key became known.

**Value enforcement IS widened, which in the value dimension is a NARROWING.** For
the 12 keys that gained a zod mirror entry, the value is now validated against the
declared type: `{ type: 'text', content: 42 }` parsed green before and is refused
now, at `content`. That is the point of declaring them — `declared === enforced` —
but it is a behaviour change for documents that carried a wrong-typed value under
one of these 13 names. Keys OUTSIDE the 13 are untouched: an undeclared key of any
type is still admitted unexamined on all eight mirrors, pinned per mirror.

The 13, each with the renderer read site the declaration records:

| type | key | declared as | read at |
|---|---|---|---|
| `TextSchema` | `content` | `string` | `renderers/basic/text.tsx` — `{schema.content \|\| schema.value}` |
| `CarouselSchema` | `opts` | `Record<string, unknown>` | `complex/carousel.tsx` — `opts={schema.opts}` |
| `CarouselSchema` | `orientation` | `'horizontal' \| 'vertical'` | `complex/carousel.tsx` |
| `CarouselSchema` | `itemClassName` | `string` | `complex/carousel.tsx` — per-slide class |
| `FilterBuilderSchema` | `wrapperClass` | `string` | `complex/filter-builder.tsx` |
| `TreeViewSchema` | `nodes` | `TreeNode[]` | `data-display/tree-view.tsx` |
| `TreeViewSchema` | `title` | `string` | `data-display/tree-view.tsx` |
| `TreeViewSchema` | `onNodeClick` | `(node: TreeNode) => void` | `data-display/tree-view.tsx` — INVOKED |
| `CheckboxSchema` | `required` | `boolean` | `form/checkbox.tsx` — drives the `*` marker |
| `FileUploadSchema` | `buttonText` | `string` | `form/file-upload.tsx` |
| `FileUploadSchema` | `wrapperClass` | `string` | `form/file-upload.tsx` |
| `HoverCardSchema` | `align` | `OverlayAlignment` | `overlay/hover-card.tsx` |
| `ContextMenuSchema` | `trigger` | `SchemaNode \| SchemaNode[]` | `overlay/context-menu.tsx` |

These compiled before only because `BaseSchema` ends with `[key: string]: any`
(objectui#5155), so the docs page was the single place in the repo recording each
capability, and the one place with no mechanical guard.

Three declarations are deliberately not what "declare what is read" would produce
on its own, and each says so in its own doc comment:

- `CarouselSchema.opts` stays an OPEN bag rather than the docs page's
`{ loop?, align? }` pair. The renderer forwards the whole bag to embla, so
narrowing it to two keys would refuse authored documents that work today.
- `ContextMenuSchema.trigger` is OPTIONAL although the docs page shows it
required; the renderer substitutes a placeholder, so trigger-less documents are
legal today.
- `TreeViewSchema.onNodeClick` gets NO zod mirror. It is invoked, not read as a
value, so it cannot appear in an authored JSON document; objectui#6152 ruled
that class is recorded in `zod-mirror-parity.test.ts`'s `RuntimeOnlyDeclared`
instead, and it is (the first pair to sit there without also sitting in
`UnmirroredDeclared`, so that file's two counts move with it).

Two of the 13 declare a SECOND spelling for a slot that already had one —
`TextSchema.content` beside `value`, `TreeViewSchema.nodes` beside `data` — because
that is what the renderers read. Retiring either spelling is an ADR-0049
enforce-or-remove question and is deliberately not decided here. Declaring `nodes`
also does not by itself make a `nodes`-only tree-view document legal: `data` stays
required on both faces.

Large diffs are not rendered by default.

37 changes: 29 additions & 8 deletions packages/types/src/__tests__/zod-mirror-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,9 +65,11 @@
* Anything citing "121" as the mirroring debt is citing a number that changed
* meaning — the comparable figure is 97 + 1 mirrored + 23 reclassified. The
* full statement is on that ledger.
* - **6 entries** in `RuntimeOnlyDeclared`, **23 keys** across them — a strict
* subset of the 16 pairs above, so the "no entry in either" population is
* unchanged.
* - **7 entries** in `RuntimeOnlyDeclared`, **24 keys** across them. Six of the
* seven are a subset of the 16 pairs above; `TreeViewSchema` is NOT — it is
* the first pair whose ONLY ledger entry is a runtime-only one
* (objectui#6150 declared `onNodeClick` on an otherwise clean pair), so the
* "no entry in either" population dropped by one to 141.
* - 158 − 12 = **146**, the "pairs with no entry" `LedgerMismatch` speaks of.
*
* ## Two ratchets, because the forward comparison has two halves
Expand DownExpand Up@@ -248,7 +250,7 @@ export type ReconcileAgainstLedger< K, Measured, Recorded > =
export type assertionRatchetAcceptsAgreement =
Expect< Equal< ReconcileAgainstLedger< 'p', 'a', 'a' >, never > >;

/** …and so does a clean pair with no entry, which is the case for 142 of the 158. */
/** …and so does a clean pair with no entry, which is the case for 141 of the 158. */
export type assertionRatchetAcceptsCleanPair =
Expect< Equal< ReconcileAgainstLedger< 'p', never, never >, never > >;

Expand DownExpand Up@@ -1040,6 +1042,23 @@ interface RuntimeOnlyDeclared {
* pair. POLICY group.
*/
'objectql.zod.ts#ObjectViewSchema': 'onNavigate';
/**
* `TreeViewSchema`'s ONLY entry in either ledger — the pair was clean before
* objectui#6150 and this key is the whole of its debt.
*
* OVERSIGHT group by mirror shape (`onSelectChange` and `onExpandChange` are
* mirrored beside it as `z.function()`), but it arrives here as a DECLARATION,
* not a discovery: objectui#6150's census measured `schema.onNodeClick` INVOKED
* at `renderers/data-display/tree-view.tsx:98,99` against a type that declared
* nothing, and the card declared it. A function cannot appear in an authored
* JSON document, so the key is a runtime slot and objectui#6152's ruling routes
* it here rather than to a mirror — the step-3 exception in the header above,
* used exactly as written.
*
* ⚠️ This is the first pair to sit in `RuntimeOnlyDeclared` without also sitting
* in `UnmirroredDeclared`; the two counts in the file header record that.
*/
'data-display.zod.ts#TreeViewSchema': 'onNodeClick';
/**
* 3 of `DetailViewSchema`'s former 14 — the exact three the 2026-07 audit named. By
* mirror SHAPE this is the oversight group (`onBack` is mirrored, as `z.string()`),
Expand DownExpand Up@@ -1169,10 +1188,12 @@ export const assertionDriftMatchesLedger: never = 0 as unknown as LedgerMismatch

/**
* The SECOND half of the forward comparison: every pair's declared-but-unmirrored
* key set equals what the two ledgers TOGETHER record for it — `never` for the 142
* pairs with no entry in either (158 − 16; `RuntimeOnlyDeclared`'s 6 pairs are a
* measured subset of `UnmirroredDeclared`'s 16, so the clean population is unchanged
* by objectui#6152's reclassification).
* key set equals what the two ledgers TOGETHER record for it — `never` for the 141
* pairs with no entry in either (158 − 17). Six of `RuntimeOnlyDeclared`'s seven
* pairs are a measured subset of `UnmirroredDeclared`'s 16, so objectui#6152's
* reclassification left the clean population unchanged; objectui#6150 then added
* `TreeViewSchema`, whose only entry is runtime-only, which is why the union is 17
* pairs and not 16.
*
* ⚠️ **The discriminating signal is the PER-PAIR set, not this file's exit code.**
* The exit code is a whole-file verdict, so it moves only while the rest of the
Expand Down
43 changes: 43 additions & 0 deletions packages/types/src/complex.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -347,6 +347,16 @@ export interface FilterBuilderSchema extends BaseSchema {
* @default 3
*/
maxDepth?: number;
/**
* Tailwind classes on the outermost wrapper `div`.
*
* READ SITE: `packages/components/src/renderers/complex/filter-builder.tsx:37`
* — `className={schema.wrapperClass || ''}`.
*
* Distinct from {@link BaseSchema.className}, which this renderer applies
* further in. Declared by objectui#6150.
*/
wrapperClass?: string;
}

/**
Expand DownExpand Up@@ -398,6 +408,39 @@ export interface CarouselSchema extends BaseSchema {
* Carousel items
*/
items: CarouselItem[];
/**
* Option bag forwarded VERBATIM to the underlying embla carousel.
*
* READ SITE: `packages/components/src/renderers/complex/carousel.tsx:23` —
* `opts={schema.opts}`, passed straight through to the `Carousel`
* primitive, whose own `opts` is embla's `EmblaOptionsType`.
*
* ⚠️ Deliberately declared OPEN rather than as the two-key shape the docs
* page shows (`{ loop?, align? }`). The renderer forwards the whole bag, so
* every other embla option authored today reaches the library and works;
* declaring the documented pair would REFUSE those authored documents —
* a narrowing of a published surface, which is a ruling and not a
* declaration. objectui#6150 records the capability as it is; picking a
* narrower shape is escalated with that card.
*/
opts?: Record<string, unknown>;
/**
* Scroll axis.
*
* READ SITE: `renderers/complex/carousel.tsx:24` —
* `orientation={schema.orientation || 'horizontal'}`.
*
* @default 'horizontal'
*/
orientation?: 'horizontal' | 'vertical';
/**
* Tailwind classes applied to EACH slide (not the container — that is
* {@link BaseSchema.className}).
*
* READ SITE: `renderers/complex/carousel.tsx:30` —
* `className={schema.itemClassName}` on every `CarouselItem`.
*/
itemClassName?: string;
/**
* Auto-play interval (ms)
*/
Expand Down
43 changes: 42 additions & 1 deletion packages/types/src/data-display.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1111,9 +1111,35 @@ export interface TreeNode {
export interface TreeViewSchema extends BaseSchema {
type: 'tree-view';
/**
* Tree data
* Tree data — the fallback spelling, read only when
* {@link TreeViewSchema.nodes} is absent.
*
* READ SITE: `packages/components/src/renderers/data-display/tree-view.tsx:105`
* — `const rawNodes = boundData || schema.nodes || schema.data || []`.
*/
data: TreeNode[];
/**
* Tree data — the spelling the renderer reads FIRST.
*
* READ SITE: `renderers/data-display/tree-view.tsx:105`, the middle limb of
* `boundData || schema.nodes || schema.data || []`, so `nodes` WINS over
* {@link TreeViewSchema.data} when both are authored (and a `bind`-resolved
* value wins over both).
*
* ⚠️ Declaring `nodes` does NOT by itself make `{ type: 'tree-view', nodes }`
* a legal document: {@link TreeViewSchema.data} stays REQUIRED on both faces,
* so the validator still demands `data`. Relaxing that is an accept-set
* change and a separate ruling — objectui#6150 declares the read, nothing
* more.
*/
nodes?: TreeNode[];
/**
* Heading rendered above the tree.
*
* READ SITE: `renderers/data-display/tree-view.tsx:115` (presence gate) and
* `:117` (the `h3` body).
*/
title?: string;
/**
* Default expanded node IDs
*/
Expand DownExpand Up@@ -1148,6 +1174,21 @@ export interface TreeViewSchema extends BaseSchema {
* Node expand handler
*/
onExpandChange?: (expandedIds: string[]) => void;
/**
* Node click handler — INVOKED, not merely read, so this is a call
* signature and not a value shape.
*
* READ SITE: `packages/components/src/renderers/data-display/tree-view.tsx:98`
* (presence gate `if (schema.onNodeClick)`) and `:99` (the call
* `schema.onNodeClick(node)`), where `node` is the clicked
* {@link TreeNode}. The handler's return value is discarded.
*
* ⚠️ NOT mirrored in `../zod/data-display.zod.ts`, deliberately: a function
* cannot appear in an authored JSON document, so it is a runtime slot.
* objectui#6152 ruled that class never gets a mirror; it is recorded in
* `__tests__/zod-mirror-parity.test.ts`'s `RuntimeOnlyDeclared` instead.
*/
onNodeClick?: (node: TreeNode) => void;
}

/**
Expand Down
27 changes: 27 additions & 0 deletions packages/types/src/form.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -345,6 +345,18 @@ export interface CheckboxSchema extends BaseSchema {
* Error message
*/
error?: string;
/**
* Whether the box must be checked — drives a VISIBLE affordance, not just
* form semantics.
*
* READ SITES: `packages/components/src/renderers/form/checkbox.tsx:45` —
* `required={schema.required}` on the Radix `Checkbox` — and `:49`, where it
* gates the label's required marker
* (`schema.required && "text-destructive after:content-['*']"`).
*
* Declared by objectui#6150; one of the two behavioural keys in that census.
*/
required?: boolean;
/**
* Change handler
*/
Expand DownExpand Up@@ -592,6 +604,21 @@ export interface FileUploadSchema extends BaseSchema {
* Error message
*/
error?: string;
/**
* Label on the drop zone / upload button.
*
* READ SITE: `packages/components/src/renderers/form/file-upload.tsx:123` —
* `{isUploading ? "…" : (schema.buttonText || "DROP PAYLOAD OR CLICK TO UPLOAD")}`.
*/
buttonText?: string;
/**
* Tailwind classes appended to the outer wrapper `div`.
*
* READ SITE: `renderers/form/file-upload.tsx:78` — appended to the
* renderer's own grid classes as
* `` `grid w-full … ${schema.wrapperClass || ''}` ``.
*/
wrapperClass?: string;
/**
* Change handler (receives FileList or File[])
*/
Expand Down
25 changes: 24 additions & 1 deletion packages/types/src/layout.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -76,7 +76,30 @@ export interface TextSpanSchema extends BaseSchema {
export interface TextSchema extends BaseSchema {
type: 'text';
/**
* Text content to display
* Text content to display — the spelling the renderer reads FIRST.
*
* READ SITE: `packages/components/src/renderers/basic/text.tsx:51` (the
* wrapped `span` arm, taken when the node carries a designer id or a
* className) and `:56` (the bare fragment arm), both as
* `{schema.content || schema.value}`. `content` therefore WINS over
* {@link TextSchema.value} whenever both are authored.
*
* Declared by objectui#6150 (undeclared-but-consumed census). Before that
* card the renderer read this key through `BaseSchema`'s
* `[key: string]: any` (objectui#5155) and no shipped type mentioned it —
* the docs page was the only record of a capability that works.
*
* ⚠️ Two spellings for one slot is a dialect, not a design. Retiring one of
* them is an ADR-0049 enforce-or-remove question and is deliberately NOT
* decided here; this declaration records what the renderer does today.
*/
content?: string;
/**
* Text content — the fallback spelling, read only when
* {@link TextSchema.content} is absent or falsy.
*
* READ SITE: `renderers/basic/text.tsx:51,56`, the right-hand side of
* `{schema.content || schema.value}`.
*/
value?: string;
/**
Expand Down
26 changes: 26 additions & 0 deletions packages/types/src/overlay.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -318,6 +318,17 @@ export interface HoverCardSchema extends BaseSchema {
* @default 300
*/
closeDelay?: number;
/**
* Alignment of the card against its trigger.
*
* READ SITE: `packages/components/src/renderers/overlay/hover-card.tsx:24` —
* `align={schema.align}` on `HoverCardContent`, beside the already-declared
* `side={schema.side}`.
*
* Same vocabulary as {@link DropdownMenuSchema.align} and
* {@link PopoverSchema.align}; declared by objectui#6150.
*/
align?: OverlayAlignment;
/**
* Open state change handler
*/
Expand DownExpand Up@@ -458,6 +469,21 @@ export interface ContextMenuSchema extends BaseSchema {
* Element to attach context menu to
*/
children: SchemaNode | SchemaNode[];
/**
* The right-clickable area's content.
*
* READ SITE: `packages/components/src/renderers/overlay/context-menu.tsx:95`
* — `renderChildren(schema.trigger || { type: 'text', value: 'Right click here' })`
* inside `ContextMenuTrigger`. ⚠️ Note the renderer renders `trigger`, NOT
* the declared {@link ContextMenuSchema.children}, which no read site
* consumes.
*
* Declared OPTIONAL although the docs page shows it required: the renderer
* substitutes a placeholder when it is absent, so every document without a
* `trigger` is legal today and declaring it required would refuse them.
* Declared by objectui#6150.
*/
trigger?: SchemaNode | SchemaNode[];
}

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/types/src/zod/complex.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -214,6 +214,8 @@ export const FilterBuilderSchema = BaseSchema.extend({
onChange: z.function().optional().describe('Change handler'),
allowGroups: z.boolean().optional().describe('Allow grouped conditions'),
maxDepth: z.number().optional().describe('Maximum nesting depth'),
wrapperClass: z.string().optional()
.describe("Outer wrapper classes, read at renderers/complex/filter-builder.tsx:37 — `className={schema.wrapperClass || ''}` (objectui#6150)"),
});

/**
Expand All@@ -230,6 +232,12 @@ export const CarouselItemSchema = z.object({
export const CarouselSchema = BaseSchema.extend({
type: z.literal('carousel'),
items: z.array(CarouselItemSchema).describe('Carousel items'),
opts: z.record(z.string(), z.unknown()).optional()
.describe("Embla option bag forwarded verbatim at renderers/complex/carousel.tsx:23 — `opts={schema.opts}`. Left OPEN on purpose: the renderer passes the whole bag through, so narrowing it to the docs' `{loop, align}` pair would refuse authored documents that work today (objectui#6150)"),
orientation: z.enum(['horizontal', 'vertical']).optional()
.describe("Scroll axis, read at renderers/complex/carousel.tsx:24 — `orientation={schema.orientation || 'horizontal'}` (objectui#6150)"),
itemClassName: z.string().optional()
.describe('Per-slide Tailwind classes, read at renderers/complex/carousel.tsx:30 — `className={schema.itemClassName}` on every CarouselItem (objectui#6150)'),
autoPlay: z.number().optional().describe('Auto-play interval (ms)'),
showArrows: z.boolean().optional().describe('Show navigation arrows'),
showDots: z.boolean().optional().describe('Show navigation dots'),
Expand Down
7 changes: 6 additions & 1 deletion packages/types/src/zod/data-display.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -298,7 +298,12 @@ export const TreeNodeSchema: z.ZodType<any> = z.lazy(() =>
*/
export const TreeViewSchema = BaseSchema.extend({
type: z.literal('tree-view'),
data: z.array(TreeNodeSchema).describe('Tree data'),
data: z.array(TreeNodeSchema)
.describe('Tree data, read as the fallback limb of `boundData || schema.nodes || schema.data || []` at renderers/data-display/tree-view.tsx:105'),
nodes: z.array(TreeNodeSchema).optional()
.describe('Tree data, read FIRST at renderers/data-display/tree-view.tsx:105 — the middle limb of `boundData || schema.nodes || schema.data || []`, so it wins over `data`. ⚠️ `data` stays REQUIRED here: declaring `nodes` does not by itself make a `nodes`-only document legal (objectui#6150)'),
title: z.string().optional()
.describe('Heading above the tree, read at renderers/data-display/tree-view.tsx:115 (presence gate) and :117 (the h3 body) (objectui#6150)'),
defaultExpandedIds: z.array(z.string()).optional().describe('Default expanded node IDs'),
defaultSelectedIds: z.array(z.string()).optional().describe('Default selected node IDs'),
expandedIds: z.array(z.string()).optional().describe('Controlled expanded node IDs'),
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .changeset/6150-undeclared-but-consumed-keys.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
---
'@object-ui/types': minor
---

Declare the 13 renderer-read keys that no shipped type declared (objectui#6150)

**This is a published-surface change on `@object-ui/types` and its `zod` mirrors,
and it moves the accept set in TWO directions.** Read the next two paragraphs
before reading the list — they are what the change actually is.

**Key membership is NOT widened — it was never narrow.** All eight touched mirrors
extend `BaseSchema`, which is `.passthrough()`, and `.extend()` carries that policy
through (measured on the built mirrors: `catchall` is `z.unknown()` on all eight).
So before this change every one of the 13 keys already parsed green and already
SURVIVED the parse — admitted unexamined, neither refused nor stripped. Nothing
that parsed before stops parsing because a key became known.

**Value enforcement IS widened, which in the value dimension is a NARROWING.** For
the 12 keys that gained a zod mirror entry, the value is now validated against the
declared type: `{ type: 'text', content: 42 }` parsed green before and is refused
now, at `content`. That is the point of declaring them — `declared === enforced` —
but it is a behaviour change for documents that carried a wrong-typed value under
one of these 13 names. Keys OUTSIDE the 13 are untouched: an undeclared key of any
type is still admitted unexamined on all eight mirrors, pinned per mirror.

The 13, each with the renderer read site the declaration records:

| type | key | declared as | read at |
|---|---|---|---|
| `TextSchema` | `content` | `string` | `renderers/basic/text.tsx` — `{schema.content \|\| schema.value}` |
| `CarouselSchema` | `opts` | `Record<string, unknown>` | `complex/carousel.tsx` — `opts={schema.opts}` |
| `CarouselSchema` | `orientation` | `'horizontal' \| 'vertical'` | `complex/carousel.tsx` |
| `CarouselSchema` | `itemClassName` | `string` | `complex/carousel.tsx` — per-slide class |
| `FilterBuilderSchema` | `wrapperClass` | `string` | `complex/filter-builder.tsx` |
| `TreeViewSchema` | `nodes` | `TreeNode[]` | `data-display/tree-view.tsx` |
| `TreeViewSchema` | `title` | `string` | `data-display/tree-view.tsx` |
| `TreeViewSchema` | `onNodeClick` | `(node: TreeNode) => void` | `data-display/tree-view.tsx` — INVOKED |
| `CheckboxSchema` | `required` | `boolean` | `form/checkbox.tsx` — drives the `*` marker |
| `FileUploadSchema` | `buttonText` | `string` | `form/file-upload.tsx` |
| `FileUploadSchema` | `wrapperClass` | `string` | `form/file-upload.tsx` |
| `HoverCardSchema` | `align` | `OverlayAlignment` | `overlay/hover-card.tsx` |
| `ContextMenuSchema` | `trigger` | `SchemaNode \| SchemaNode[]` | `overlay/context-menu.tsx` |

These compiled before only because `BaseSchema` ends with `[key: string]: any`
(objectui#5155), so the docs page was the single place in the repo recording each
capability, and the one place with no mechanical guard.

Three declarations are deliberately not what "declare what is read" would produce
on its own, and each says so in its own doc comment:

- `CarouselSchema.opts` stays an OPEN bag rather than the docs page's
`{ loop?, align? }` pair. The renderer forwards the whole bag to embla, so
narrowing it to two keys would refuse authored documents that work today.
- `ContextMenuSchema.trigger` is OPTIONAL although the docs page shows it
required; the renderer substitutes a placeholder, so trigger-less documents are
legal today.
- `TreeViewSchema.onNodeClick` gets NO zod mirror. It is invoked, not read as a
value, so it cannot appear in an authored JSON document; objectui#6152 ruled
that class is recorded in `zod-mirror-parity.test.ts`'s `RuntimeOnlyDeclared`
instead, and it is (the first pair to sit there without also sitting in
`UnmirroredDeclared`, so that file's two counts move with it).

Two of the 13 declare a SECOND spelling for a slot that already had one —
`TextSchema.content` beside `value`, `TreeViewSchema.nodes` beside `data` — because
that is what the renderers read. Retiring either spelling is an ADR-0049
enforce-or-remove question and is deliberately not decided here. Declaring `nodes`
also does not by itself make a `nodes`-only tree-view document legal: `data` stays
required on both faces.

Large diffs are not rendered by default.

37 changes: 29 additions & 8 deletions packages/types/src/__tests__/zod-mirror-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,9 +65,11 @@
* Anything citing "121" as the mirroring debt is citing a number that changed
* meaning — the comparable figure is 97 + 1 mirrored + 23 reclassified. The
* full statement is on that ledger.
* - **6 entries** in `RuntimeOnlyDeclared`, **23 keys** across them — a strict
* subset of the 16 pairs above, so the "no entry in either" population is
* unchanged.
* - **7 entries** in `RuntimeOnlyDeclared`, **24 keys** across them. Six of the
* seven are a subset of the 16 pairs above; `TreeViewSchema` is NOT — it is
* the first pair whose ONLY ledger entry is a runtime-only one
* (objectui#6150 declared `onNodeClick` on an otherwise clean pair), so the
* "no entry in either" population dropped by one to 141.
* - 158 − 12 = **146**, the "pairs with no entry" `LedgerMismatch` speaks of.
*
* ## Two ratchets, because the forward comparison has two halves
Expand DownExpand Up@@ -248,7 +250,7 @@ export type ReconcileAgainstLedger< K, Measured, Recorded > =
export type assertionRatchetAcceptsAgreement =
Expect< Equal< ReconcileAgainstLedger< 'p', 'a', 'a' >, never > >;

/** …and so does a clean pair with no entry, which is the case for 142 of the 158. */
/** …and so does a clean pair with no entry, which is the case for 141 of the 158. */
export type assertionRatchetAcceptsCleanPair =
Expect< Equal< ReconcileAgainstLedger< 'p', never, never >, never > >;

Expand DownExpand Up@@ -1040,6 +1042,23 @@ interface RuntimeOnlyDeclared {
* pair. POLICY group.
*/
'objectql.zod.ts#ObjectViewSchema': 'onNavigate';
/**
* `TreeViewSchema`'s ONLY entry in either ledger — the pair was clean before
* objectui#6150 and this key is the whole of its debt.
*
* OVERSIGHT group by mirror shape (`onSelectChange` and `onExpandChange` are
* mirrored beside it as `z.function()`), but it arrives here as a DECLARATION,
* not a discovery: objectui#6150's census measured `schema.onNodeClick` INVOKED
* at `renderers/data-display/tree-view.tsx:98,99` against a type that declared
* nothing, and the card declared it. A function cannot appear in an authored
* JSON document, so the key is a runtime slot and objectui#6152's ruling routes
* it here rather than to a mirror — the step-3 exception in the header above,
* used exactly as written.
*
* ⚠️ This is the first pair to sit in `RuntimeOnlyDeclared` without also sitting
* in `UnmirroredDeclared`; the two counts in the file header record that.
*/
'data-display.zod.ts#TreeViewSchema': 'onNodeClick';
/**
* 3 of `DetailViewSchema`'s former 14 — the exact three the 2026-07 audit named. By
* mirror SHAPE this is the oversight group (`onBack` is mirrored, as `z.string()`),
Expand DownExpand Up@@ -1169,10 +1188,12 @@ export const assertionDriftMatchesLedger: never = 0 as unknown as LedgerMismatch

/**
* The SECOND half of the forward comparison: every pair's declared-but-unmirrored
* key set equals what the two ledgers TOGETHER record for it — `never` for the 142
* pairs with no entry in either (158 − 16; `RuntimeOnlyDeclared`'s 6 pairs are a
* measured subset of `UnmirroredDeclared`'s 16, so the clean population is unchanged
* by objectui#6152's reclassification).
* key set equals what the two ledgers TOGETHER record for it — `never` for the 141
* pairs with no entry in either (158 − 17). Six of `RuntimeOnlyDeclared`'s seven
* pairs are a measured subset of `UnmirroredDeclared`'s 16, so objectui#6152's
* reclassification left the clean population unchanged; objectui#6150 then added
* `TreeViewSchema`, whose only entry is runtime-only, which is why the union is 17
* pairs and not 16.
*
* ⚠️ **The discriminating signal is the PER-PAIR set, not this file's exit code.**
* The exit code is a whole-file verdict, so it moves only while the rest of the
Expand Down
43 changes: 43 additions & 0 deletions packages/types/src/complex.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -347,6 +347,16 @@ export interface FilterBuilderSchema extends BaseSchema {
* @default 3
*/
maxDepth?: number;
/**
* Tailwind classes on the outermost wrapper `div`.
*
* READ SITE: `packages/components/src/renderers/complex/filter-builder.tsx:37`
* — `className={schema.wrapperClass || ''}`.
*
* Distinct from {@link BaseSchema.className}, which this renderer applies
* further in. Declared by objectui#6150.
*/
wrapperClass?: string;
}

/**
Expand DownExpand Up@@ -398,6 +408,39 @@ export interface CarouselSchema extends BaseSchema {
* Carousel items
*/
items: CarouselItem[];
/**
* Option bag forwarded VERBATIM to the underlying embla carousel.
*
* READ SITE: `packages/components/src/renderers/complex/carousel.tsx:23` —
* `opts={schema.opts}`, passed straight through to the `Carousel`
* primitive, whose own `opts` is embla's `EmblaOptionsType`.
*
* ⚠️ Deliberately declared OPEN rather than as the two-key shape the docs
* page shows (`{ loop?, align? }`). The renderer forwards the whole bag, so
* every other embla option authored today reaches the library and works;
* declaring the documented pair would REFUSE those authored documents —
* a narrowing of a published surface, which is a ruling and not a
* declaration. objectui#6150 records the capability as it is; picking a
* narrower shape is escalated with that card.
*/
opts?: Record<string, unknown>;
/**
* Scroll axis.
*
* READ SITE: `renderers/complex/carousel.tsx:24` —
* `orientation={schema.orientation || 'horizontal'}`.
*
* @default 'horizontal'
*/
orientation?: 'horizontal' | 'vertical';
/**
* Tailwind classes applied to EACH slide (not the container — that is
* {@link BaseSchema.className}).
*
* READ SITE: `renderers/complex/carousel.tsx:30` —
* `className={schema.itemClassName}` on every `CarouselItem`.
*/
itemClassName?: string;
/**
* Auto-play interval (ms)
*/
Expand Down
43 changes: 42 additions & 1 deletion packages/types/src/data-display.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1111,9 +1111,35 @@ export interface TreeNode {
export interface TreeViewSchema extends BaseSchema {
type: 'tree-view';
/**
* Tree data
* Tree data — the fallback spelling, read only when
* {@link TreeViewSchema.nodes} is absent.
*
* READ SITE: `packages/components/src/renderers/data-display/tree-view.tsx:105`
* — `const rawNodes = boundData || schema.nodes || schema.data || []`.
*/
data: TreeNode[];
/**
* Tree data — the spelling the renderer reads FIRST.
*
* READ SITE: `renderers/data-display/tree-view.tsx:105`, the middle limb of
* `boundData || schema.nodes || schema.data || []`, so `nodes` WINS over
* {@link TreeViewSchema.data} when both are authored (and a `bind`-resolved
* value wins over both).
*
* ⚠️ Declaring `nodes` does NOT by itself make `{ type: 'tree-view', nodes }`
* a legal document: {@link TreeViewSchema.data} stays REQUIRED on both faces,
* so the validator still demands `data`. Relaxing that is an accept-set
* change and a separate ruling — objectui#6150 declares the read, nothing
* more.
*/
nodes?: TreeNode[];
/**
* Heading rendered above the tree.
*
* READ SITE: `renderers/data-display/tree-view.tsx:115` (presence gate) and
* `:117` (the `h3` body).
*/
title?: string;
/**
* Default expanded node IDs
*/
Expand DownExpand Up@@ -1148,6 +1174,21 @@ export interface TreeViewSchema extends BaseSchema {
* Node expand handler
*/
onExpandChange?: (expandedIds: string[]) => void;
/**
* Node click handler — INVOKED, not merely read, so this is a call
* signature and not a value shape.
*
* READ SITE: `packages/components/src/renderers/data-display/tree-view.tsx:98`
* (presence gate `if (schema.onNodeClick)`) and `:99` (the call
* `schema.onNodeClick(node)`), where `node` is the clicked
* {@link TreeNode}. The handler's return value is discarded.
*
* ⚠️ NOT mirrored in `../zod/data-display.zod.ts`, deliberately: a function
* cannot appear in an authored JSON document, so it is a runtime slot.
* objectui#6152 ruled that class never gets a mirror; it is recorded in
* `__tests__/zod-mirror-parity.test.ts`'s `RuntimeOnlyDeclared` instead.
*/
onNodeClick?: (node: TreeNode) => void;
}

/**
Expand Down
27 changes: 27 additions & 0 deletions packages/types/src/form.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -345,6 +345,18 @@ export interface CheckboxSchema extends BaseSchema {
* Error message
*/
error?: string;
/**
* Whether the box must be checked — drives a VISIBLE affordance, not just
* form semantics.
*
* READ SITES: `packages/components/src/renderers/form/checkbox.tsx:45` —
* `required={schema.required}` on the Radix `Checkbox` — and `:49`, where it
* gates the label's required marker
* (`schema.required && "text-destructive after:content-['*']"`).
*
* Declared by objectui#6150; one of the two behavioural keys in that census.
*/
required?: boolean;
/**
* Change handler
*/
Expand DownExpand Up@@ -592,6 +604,21 @@ export interface FileUploadSchema extends BaseSchema {
* Error message
*/
error?: string;
/**
* Label on the drop zone / upload button.
*
* READ SITE: `packages/components/src/renderers/form/file-upload.tsx:123` —
* `{isUploading ? "…" : (schema.buttonText || "DROP PAYLOAD OR CLICK TO UPLOAD")}`.
*/
buttonText?: string;
/**
* Tailwind classes appended to the outer wrapper `div`.
*
* READ SITE: `renderers/form/file-upload.tsx:78` — appended to the
* renderer's own grid classes as
* `` `grid w-full … ${schema.wrapperClass || ''}` ``.
*/
wrapperClass?: string;
/**
* Change handler (receives FileList or File[])
*/
Expand Down
25 changes: 24 additions & 1 deletion packages/types/src/layout.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -76,7 +76,30 @@ export interface TextSpanSchema extends BaseSchema {
export interface TextSchema extends BaseSchema {
type: 'text';
/**
* Text content to display
* Text content to display — the spelling the renderer reads FIRST.
*
* READ SITE: `packages/components/src/renderers/basic/text.tsx:51` (the
* wrapped `span` arm, taken when the node carries a designer id or a
* className) and `:56` (the bare fragment arm), both as
* `{schema.content || schema.value}`. `content` therefore WINS over
* {@link TextSchema.value} whenever both are authored.
*
* Declared by objectui#6150 (undeclared-but-consumed census). Before that
* card the renderer read this key through `BaseSchema`'s
* `[key: string]: any` (objectui#5155) and no shipped type mentioned it —
* the docs page was the only record of a capability that works.
*
* ⚠️ Two spellings for one slot is a dialect, not a design. Retiring one of
* them is an ADR-0049 enforce-or-remove question and is deliberately NOT
* decided here; this declaration records what the renderer does today.
*/
content?: string;
/**
* Text content — the fallback spelling, read only when
* {@link TextSchema.content} is absent or falsy.
*
* READ SITE: `renderers/basic/text.tsx:51,56`, the right-hand side of
* `{schema.content || schema.value}`.
*/
value?: string;
/**
Expand Down
26 changes: 26 additions & 0 deletions packages/types/src/overlay.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -318,6 +318,17 @@ export interface HoverCardSchema extends BaseSchema {
* @default 300
*/
closeDelay?: number;
/**
* Alignment of the card against its trigger.
*
* READ SITE: `packages/components/src/renderers/overlay/hover-card.tsx:24` —
* `align={schema.align}` on `HoverCardContent`, beside the already-declared
* `side={schema.side}`.
*
* Same vocabulary as {@link DropdownMenuSchema.align} and
* {@link PopoverSchema.align}; declared by objectui#6150.
*/
align?: OverlayAlignment;
/**
* Open state change handler
*/
Expand DownExpand Up@@ -458,6 +469,21 @@ export interface ContextMenuSchema extends BaseSchema {
* Element to attach context menu to
*/
children: SchemaNode | SchemaNode[];
/**
* The right-clickable area's content.
*
* READ SITE: `packages/components/src/renderers/overlay/context-menu.tsx:95`
* — `renderChildren(schema.trigger || { type: 'text', value: 'Right click here' })`
* inside `ContextMenuTrigger`. ⚠️ Note the renderer renders `trigger`, NOT
* the declared {@link ContextMenuSchema.children}, which no read site
* consumes.
*
* Declared OPTIONAL although the docs page shows it required: the renderer
* substitutes a placeholder when it is absent, so every document without a
* `trigger` is legal today and declaring it required would refuse them.
* Declared by objectui#6150.
*/
trigger?: SchemaNode | SchemaNode[];
}

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/types/src/zod/complex.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -214,6 +214,8 @@ export const FilterBuilderSchema = BaseSchema.extend({
onChange: z.function().optional().describe('Change handler'),
allowGroups: z.boolean().optional().describe('Allow grouped conditions'),
maxDepth: z.number().optional().describe('Maximum nesting depth'),
wrapperClass: z.string().optional()
.describe("Outer wrapper classes, read at renderers/complex/filter-builder.tsx:37 — `className={schema.wrapperClass || ''}` (objectui#6150)"),
});

/**
Expand All@@ -230,6 +232,12 @@ export const CarouselItemSchema = z.object({
export const CarouselSchema = BaseSchema.extend({
type: z.literal('carousel'),
items: z.array(CarouselItemSchema).describe('Carousel items'),
opts: z.record(z.string(), z.unknown()).optional()
.describe("Embla option bag forwarded verbatim at renderers/complex/carousel.tsx:23 — `opts={schema.opts}`. Left OPEN on purpose: the renderer passes the whole bag through, so narrowing it to the docs' `{loop, align}` pair would refuse authored documents that work today (objectui#6150)"),
orientation: z.enum(['horizontal', 'vertical']).optional()
.describe("Scroll axis, read at renderers/complex/carousel.tsx:24 — `orientation={schema.orientation || 'horizontal'}` (objectui#6150)"),
itemClassName: z.string().optional()
.describe('Per-slide Tailwind classes, read at renderers/complex/carousel.tsx:30 — `className={schema.itemClassName}` on every CarouselItem (objectui#6150)'),
autoPlay: z.number().optional().describe('Auto-play interval (ms)'),
showArrows: z.boolean().optional().describe('Show navigation arrows'),
showDots: z.boolean().optional().describe('Show navigation dots'),
Expand Down
7 changes: 6 additions & 1 deletion packages/types/src/zod/data-display.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -298,7 +298,12 @@ export const TreeNodeSchema: z.ZodType<any> = z.lazy(() =>
*/
export const TreeViewSchema = BaseSchema.extend({
type: z.literal('tree-view'),
data: z.array(TreeNodeSchema).describe('Tree data'),
data: z.array(TreeNodeSchema)
.describe('Tree data, read as the fallback limb of `boundData || schema.nodes || schema.data || []` at renderers/data-display/tree-view.tsx:105'),
nodes: z.array(TreeNodeSchema).optional()
.describe('Tree data, read FIRST at renderers/data-display/tree-view.tsx:105 — the middle limb of `boundData || schema.nodes || schema.data || []`, so it wins over `data`. ⚠️ `data` stays REQUIRED here: declaring `nodes` does not by itself make a `nodes`-only document legal (objectui#6150)'),
title: z.string().optional()
.describe('Heading above the tree, read at renderers/data-display/tree-view.tsx:115 (presence gate) and :117 (the h3 body) (objectui#6150)'),
defaultExpandedIds: z.array(z.string()).optional().describe('Default expanded node IDs'),
defaultSelectedIds: z.array(z.string()).optional().describe('Default selected node IDs'),
expandedIds: z.array(z.string()).optional().describe('Controlled expanded node IDs'),
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .changeset/6150-undeclared-but-consumed-keys.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
---
'@object-ui/types': minor
---

Declare the 13 renderer-read keys that no shipped type declared (objectui#6150)

**This is a published-surface change on `@object-ui/types` and its `zod` mirrors,
and it moves the accept set in TWO directions.** Read the next two paragraphs
before reading the list — they are what the change actually is.

**Key membership is NOT widened — it was never narrow.** All eight touched mirrors
extend `BaseSchema`, which is `.passthrough()`, and `.extend()` carries that policy
through (measured on the built mirrors: `catchall` is `z.unknown()` on all eight).
So before this change every one of the 13 keys already parsed green and already
SURVIVED the parse — admitted unexamined, neither refused nor stripped. Nothing
that parsed before stops parsing because a key became known.

**Value enforcement IS widened, which in the value dimension is a NARROWING.** For
the 12 keys that gained a zod mirror entry, the value is now validated against the
declared type: `{ type: 'text', content: 42 }` parsed green before and is refused
now, at `content`. That is the point of declaring them — `declared === enforced` —
but it is a behaviour change for documents that carried a wrong-typed value under
one of these 13 names. Keys OUTSIDE the 13 are untouched: an undeclared key of any
type is still admitted unexamined on all eight mirrors, pinned per mirror.

The 13, each with the renderer read site the declaration records:

| type | key | declared as | read at |
|---|---|---|---|
| `TextSchema` | `content` | `string` | `renderers/basic/text.tsx` — `{schema.content \|\| schema.value}` |
| `CarouselSchema` | `opts` | `Record<string, unknown>` | `complex/carousel.tsx` — `opts={schema.opts}` |
| `CarouselSchema` | `orientation` | `'horizontal' \| 'vertical'` | `complex/carousel.tsx` |
| `CarouselSchema` | `itemClassName` | `string` | `complex/carousel.tsx` — per-slide class |
| `FilterBuilderSchema` | `wrapperClass` | `string` | `complex/filter-builder.tsx` |
| `TreeViewSchema` | `nodes` | `TreeNode[]` | `data-display/tree-view.tsx` |
| `TreeViewSchema` | `title` | `string` | `data-display/tree-view.tsx` |
| `TreeViewSchema` | `onNodeClick` | `(node: TreeNode) => void` | `data-display/tree-view.tsx` — INVOKED |
| `CheckboxSchema` | `required` | `boolean` | `form/checkbox.tsx` — drives the `*` marker |
| `FileUploadSchema` | `buttonText` | `string` | `form/file-upload.tsx` |
| `FileUploadSchema` | `wrapperClass` | `string` | `form/file-upload.tsx` |
| `HoverCardSchema` | `align` | `OverlayAlignment` | `overlay/hover-card.tsx` |
| `ContextMenuSchema` | `trigger` | `SchemaNode \| SchemaNode[]` | `overlay/context-menu.tsx` |

These compiled before only because `BaseSchema` ends with `[key: string]: any`
(objectui#5155), so the docs page was the single place in the repo recording each
capability, and the one place with no mechanical guard.

Three declarations are deliberately not what "declare what is read" would produce
on its own, and each says so in its own doc comment:

- `CarouselSchema.opts` stays an OPEN bag rather than the docs page's
`{ loop?, align? }` pair. The renderer forwards the whole bag to embla, so
narrowing it to two keys would refuse authored documents that work today.
- `ContextMenuSchema.trigger` is OPTIONAL although the docs page shows it
required; the renderer substitutes a placeholder, so trigger-less documents are
legal today.
- `TreeViewSchema.onNodeClick` gets NO zod mirror. It is invoked, not read as a
value, so it cannot appear in an authored JSON document; objectui#6152 ruled
that class is recorded in `zod-mirror-parity.test.ts`'s `RuntimeOnlyDeclared`
instead, and it is (the first pair to sit there without also sitting in
`UnmirroredDeclared`, so that file's two counts move with it).

Two of the 13 declare a SECOND spelling for a slot that already had one —
`TextSchema.content` beside `value`, `TreeViewSchema.nodes` beside `data` — because
that is what the renderers read. Retiring either spelling is an ADR-0049
enforce-or-remove question and is deliberately not decided here. Declaring `nodes`
also does not by itself make a `nodes`-only tree-view document legal: `data` stays
required on both faces.

Large diffs are not rendered by default.

37 changes: 29 additions & 8 deletions packages/types/src/__tests__/zod-mirror-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,9 +65,11 @@
* Anything citing "121" as the mirroring debt is citing a number that changed
* meaning — the comparable figure is 97 + 1 mirrored + 23 reclassified. The
* full statement is on that ledger.
* - **6 entries** in `RuntimeOnlyDeclared`, **23 keys** across them — a strict
* subset of the 16 pairs above, so the "no entry in either" population is
* unchanged.
* - **7 entries** in `RuntimeOnlyDeclared`, **24 keys** across them. Six of the
* seven are a subset of the 16 pairs above; `TreeViewSchema` is NOT — it is
* the first pair whose ONLY ledger entry is a runtime-only one
* (objectui#6150 declared `onNodeClick` on an otherwise clean pair), so the
* "no entry in either" population dropped by one to 141.
* - 158 − 12 = **146**, the "pairs with no entry" `LedgerMismatch` speaks of.
*
* ## Two ratchets, because the forward comparison has two halves
Expand DownExpand Up@@ -248,7 +250,7 @@ export type ReconcileAgainstLedger< K, Measured, Recorded > =
export type assertionRatchetAcceptsAgreement =
Expect< Equal< ReconcileAgainstLedger< 'p', 'a', 'a' >, never > >;

/** …and so does a clean pair with no entry, which is the case for 142 of the 158. */
/** …and so does a clean pair with no entry, which is the case for 141 of the 158. */
export type assertionRatchetAcceptsCleanPair =
Expect< Equal< ReconcileAgainstLedger< 'p', never, never >, never > >;

Expand DownExpand Up@@ -1040,6 +1042,23 @@ interface RuntimeOnlyDeclared {
* pair. POLICY group.
*/
'objectql.zod.ts#ObjectViewSchema': 'onNavigate';
/**
* `TreeViewSchema`'s ONLY entry in either ledger — the pair was clean before
* objectui#6150 and this key is the whole of its debt.
*
* OVERSIGHT group by mirror shape (`onSelectChange` and `onExpandChange` are
* mirrored beside it as `z.function()`), but it arrives here as a DECLARATION,
* not a discovery: objectui#6150's census measured `schema.onNodeClick` INVOKED
* at `renderers/data-display/tree-view.tsx:98,99` against a type that declared
* nothing, and the card declared it. A function cannot appear in an authored
* JSON document, so the key is a runtime slot and objectui#6152's ruling routes
* it here rather than to a mirror — the step-3 exception in the header above,
* used exactly as written.
*
* ⚠️ This is the first pair to sit in `RuntimeOnlyDeclared` without also sitting
* in `UnmirroredDeclared`; the two counts in the file header record that.
*/
'data-display.zod.ts#TreeViewSchema': 'onNodeClick';
/**
* 3 of `DetailViewSchema`'s former 14 — the exact three the 2026-07 audit named. By
* mirror SHAPE this is the oversight group (`onBack` is mirrored, as `z.string()`),
Expand DownExpand Up@@ -1169,10 +1188,12 @@ export const assertionDriftMatchesLedger: never = 0 as unknown as LedgerMismatch

/**
* The SECOND half of the forward comparison: every pair's declared-but-unmirrored
* key set equals what the two ledgers TOGETHER record for it — `never` for the 142
* pairs with no entry in either (158 − 16; `RuntimeOnlyDeclared`'s 6 pairs are a
* measured subset of `UnmirroredDeclared`'s 16, so the clean population is unchanged
* by objectui#6152's reclassification).
* key set equals what the two ledgers TOGETHER record for it — `never` for the 141
* pairs with no entry in either (158 − 17). Six of `RuntimeOnlyDeclared`'s seven
* pairs are a measured subset of `UnmirroredDeclared`'s 16, so objectui#6152's
* reclassification left the clean population unchanged; objectui#6150 then added
* `TreeViewSchema`, whose only entry is runtime-only, which is why the union is 17
* pairs and not 16.
*
* ⚠️ **The discriminating signal is the PER-PAIR set, not this file's exit code.**
* The exit code is a whole-file verdict, so it moves only while the rest of the
Expand Down
43 changes: 43 additions & 0 deletions packages/types/src/complex.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -347,6 +347,16 @@ export interface FilterBuilderSchema extends BaseSchema {
* @default 3
*/
maxDepth?: number;
/**
* Tailwind classes on the outermost wrapper `div`.
*
* READ SITE: `packages/components/src/renderers/complex/filter-builder.tsx:37`
* — `className={schema.wrapperClass || ''}`.
*
* Distinct from {@link BaseSchema.className}, which this renderer applies
* further in. Declared by objectui#6150.
*/
wrapperClass?: string;
}

/**
Expand DownExpand Up@@ -398,6 +408,39 @@ export interface CarouselSchema extends BaseSchema {
* Carousel items
*/
items: CarouselItem[];
/**
* Option bag forwarded VERBATIM to the underlying embla carousel.
*
* READ SITE: `packages/components/src/renderers/complex/carousel.tsx:23` —
* `opts={schema.opts}`, passed straight through to the `Carousel`
* primitive, whose own `opts` is embla's `EmblaOptionsType`.
*
* ⚠️ Deliberately declared OPEN rather than as the two-key shape the docs
* page shows (`{ loop?, align? }`). The renderer forwards the whole bag, so
* every other embla option authored today reaches the library and works;
* declaring the documented pair would REFUSE those authored documents —
* a narrowing of a published surface, which is a ruling and not a
* declaration. objectui#6150 records the capability as it is; picking a
* narrower shape is escalated with that card.
*/
opts?: Record<string, unknown>;
/**
* Scroll axis.
*
* READ SITE: `renderers/complex/carousel.tsx:24` —
* `orientation={schema.orientation || 'horizontal'}`.
*
* @default 'horizontal'
*/
orientation?: 'horizontal' | 'vertical';
/**
* Tailwind classes applied to EACH slide (not the container — that is
* {@link BaseSchema.className}).
*
* READ SITE: `renderers/complex/carousel.tsx:30` —
* `className={schema.itemClassName}` on every `CarouselItem`.
*/
itemClassName?: string;
/**
* Auto-play interval (ms)
*/
Expand Down
43 changes: 42 additions & 1 deletion packages/types/src/data-display.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1111,9 +1111,35 @@ export interface TreeNode {
export interface TreeViewSchema extends BaseSchema {
type: 'tree-view';
/**
* Tree data
* Tree data — the fallback spelling, read only when
* {@link TreeViewSchema.nodes} is absent.
*
* READ SITE: `packages/components/src/renderers/data-display/tree-view.tsx:105`
* — `const rawNodes = boundData || schema.nodes || schema.data || []`.
*/
data: TreeNode[];
/**
* Tree data — the spelling the renderer reads FIRST.
*
* READ SITE: `renderers/data-display/tree-view.tsx:105`, the middle limb of
* `boundData || schema.nodes || schema.data || []`, so `nodes` WINS over
* {@link TreeViewSchema.data} when both are authored (and a `bind`-resolved
* value wins over both).
*
* ⚠️ Declaring `nodes` does NOT by itself make `{ type: 'tree-view', nodes }`
* a legal document: {@link TreeViewSchema.data} stays REQUIRED on both faces,
* so the validator still demands `data`. Relaxing that is an accept-set
* change and a separate ruling — objectui#6150 declares the read, nothing
* more.
*/
nodes?: TreeNode[];
/**
* Heading rendered above the tree.
*
* READ SITE: `renderers/data-display/tree-view.tsx:115` (presence gate) and
* `:117` (the `h3` body).
*/
title?: string;
/**
* Default expanded node IDs
*/
Expand DownExpand Up@@ -1148,6 +1174,21 @@ export interface TreeViewSchema extends BaseSchema {
* Node expand handler
*/
onExpandChange?: (expandedIds: string[]) => void;
/**
* Node click handler — INVOKED, not merely read, so this is a call
* signature and not a value shape.
*
* READ SITE: `packages/components/src/renderers/data-display/tree-view.tsx:98`
* (presence gate `if (schema.onNodeClick)`) and `:99` (the call
* `schema.onNodeClick(node)`), where `node` is the clicked
* {@link TreeNode}. The handler's return value is discarded.
*
* ⚠️ NOT mirrored in `../zod/data-display.zod.ts`, deliberately: a function
* cannot appear in an authored JSON document, so it is a runtime slot.
* objectui#6152 ruled that class never gets a mirror; it is recorded in
* `__tests__/zod-mirror-parity.test.ts`'s `RuntimeOnlyDeclared` instead.
*/
onNodeClick?: (node: TreeNode) => void;
}

/**
Expand Down
27 changes: 27 additions & 0 deletions packages/types/src/form.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -345,6 +345,18 @@ export interface CheckboxSchema extends BaseSchema {
* Error message
*/
error?: string;
/**
* Whether the box must be checked — drives a VISIBLE affordance, not just
* form semantics.
*
* READ SITES: `packages/components/src/renderers/form/checkbox.tsx:45` —
* `required={schema.required}` on the Radix `Checkbox` — and `:49`, where it
* gates the label's required marker
* (`schema.required && "text-destructive after:content-['*']"`).
*
* Declared by objectui#6150; one of the two behavioural keys in that census.
*/
required?: boolean;
/**
* Change handler
*/
Expand DownExpand Up@@ -592,6 +604,21 @@ export interface FileUploadSchema extends BaseSchema {
* Error message
*/
error?: string;
/**
* Label on the drop zone / upload button.
*
* READ SITE: `packages/components/src/renderers/form/file-upload.tsx:123` —
* `{isUploading ? "…" : (schema.buttonText || "DROP PAYLOAD OR CLICK TO UPLOAD")}`.
*/
buttonText?: string;
/**
* Tailwind classes appended to the outer wrapper `div`.
*
* READ SITE: `renderers/form/file-upload.tsx:78` — appended to the
* renderer's own grid classes as
* `` `grid w-full … ${schema.wrapperClass || ''}` ``.
*/
wrapperClass?: string;
/**
* Change handler (receives FileList or File[])
*/
Expand Down
25 changes: 24 additions & 1 deletion packages/types/src/layout.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -76,7 +76,30 @@ export interface TextSpanSchema extends BaseSchema {
export interface TextSchema extends BaseSchema {
type: 'text';
/**
* Text content to display
* Text content to display — the spelling the renderer reads FIRST.
*
* READ SITE: `packages/components/src/renderers/basic/text.tsx:51` (the
* wrapped `span` arm, taken when the node carries a designer id or a
* className) and `:56` (the bare fragment arm), both as
* `{schema.content || schema.value}`. `content` therefore WINS over
* {@link TextSchema.value} whenever both are authored.
*
* Declared by objectui#6150 (undeclared-but-consumed census). Before that
* card the renderer read this key through `BaseSchema`'s
* `[key: string]: any` (objectui#5155) and no shipped type mentioned it —
* the docs page was the only record of a capability that works.
*
* ⚠️ Two spellings for one slot is a dialect, not a design. Retiring one of
* them is an ADR-0049 enforce-or-remove question and is deliberately NOT
* decided here; this declaration records what the renderer does today.
*/
content?: string;
/**
* Text content — the fallback spelling, read only when
* {@link TextSchema.content} is absent or falsy.
*
* READ SITE: `renderers/basic/text.tsx:51,56`, the right-hand side of
* `{schema.content || schema.value}`.
*/
value?: string;
/**
Expand Down
26 changes: 26 additions & 0 deletions packages/types/src/overlay.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -318,6 +318,17 @@ export interface HoverCardSchema extends BaseSchema {
* @default 300
*/
closeDelay?: number;
/**
* Alignment of the card against its trigger.
*
* READ SITE: `packages/components/src/renderers/overlay/hover-card.tsx:24` —
* `align={schema.align}` on `HoverCardContent`, beside the already-declared
* `side={schema.side}`.
*
* Same vocabulary as {@link DropdownMenuSchema.align} and
* {@link PopoverSchema.align}; declared by objectui#6150.
*/
align?: OverlayAlignment;
/**
* Open state change handler
*/
Expand DownExpand Up@@ -458,6 +469,21 @@ export interface ContextMenuSchema extends BaseSchema {
* Element to attach context menu to
*/
children: SchemaNode | SchemaNode[];
/**
* The right-clickable area's content.
*
* READ SITE: `packages/components/src/renderers/overlay/context-menu.tsx:95`
* — `renderChildren(schema.trigger || { type: 'text', value: 'Right click here' })`
* inside `ContextMenuTrigger`. ⚠️ Note the renderer renders `trigger`, NOT
* the declared {@link ContextMenuSchema.children}, which no read site
* consumes.
*
* Declared OPTIONAL although the docs page shows it required: the renderer
* substitutes a placeholder when it is absent, so every document without a
* `trigger` is legal today and declaring it required would refuse them.
* Declared by objectui#6150.
*/
trigger?: SchemaNode | SchemaNode[];
}

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/types/src/zod/complex.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -214,6 +214,8 @@ export const FilterBuilderSchema = BaseSchema.extend({
onChange: z.function().optional().describe('Change handler'),
allowGroups: z.boolean().optional().describe('Allow grouped conditions'),
maxDepth: z.number().optional().describe('Maximum nesting depth'),
wrapperClass: z.string().optional()
.describe("Outer wrapper classes, read at renderers/complex/filter-builder.tsx:37 — `className={schema.wrapperClass || ''}` (objectui#6150)"),
});

/**
Expand All@@ -230,6 +232,12 @@ export const CarouselItemSchema = z.object({
export const CarouselSchema = BaseSchema.extend({
type: z.literal('carousel'),
items: z.array(CarouselItemSchema).describe('Carousel items'),
opts: z.record(z.string(), z.unknown()).optional()
.describe("Embla option bag forwarded verbatim at renderers/complex/carousel.tsx:23 — `opts={schema.opts}`. Left OPEN on purpose: the renderer passes the whole bag through, so narrowing it to the docs' `{loop, align}` pair would refuse authored documents that work today (objectui#6150)"),
orientation: z.enum(['horizontal', 'vertical']).optional()
.describe("Scroll axis, read at renderers/complex/carousel.tsx:24 — `orientation={schema.orientation || 'horizontal'}` (objectui#6150)"),
itemClassName: z.string().optional()
.describe('Per-slide Tailwind classes, read at renderers/complex/carousel.tsx:30 — `className={schema.itemClassName}` on every CarouselItem (objectui#6150)'),
autoPlay: z.number().optional().describe('Auto-play interval (ms)'),
showArrows: z.boolean().optional().describe('Show navigation arrows'),
showDots: z.boolean().optional().describe('Show navigation dots'),
Expand Down
7 changes: 6 additions & 1 deletion packages/types/src/zod/data-display.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -298,7 +298,12 @@ export const TreeNodeSchema: z.ZodType<any> = z.lazy(() =>
*/
export const TreeViewSchema = BaseSchema.extend({
type: z.literal('tree-view'),
data: z.array(TreeNodeSchema).describe('Tree data'),
data: z.array(TreeNodeSchema)
.describe('Tree data, read as the fallback limb of `boundData || schema.nodes || schema.data || []` at renderers/data-display/tree-view.tsx:105'),
nodes: z.array(TreeNodeSchema).optional()
.describe('Tree data, read FIRST at renderers/data-display/tree-view.tsx:105 — the middle limb of `boundData || schema.nodes || schema.data || []`, so it wins over `data`. ⚠️ `data` stays REQUIRED here: declaring `nodes` does not by itself make a `nodes`-only document legal (objectui#6150)'),
title: z.string().optional()
.describe('Heading above the tree, read at renderers/data-display/tree-view.tsx:115 (presence gate) and :117 (the h3 body) (objectui#6150)'),
defaultExpandedIds: z.array(z.string()).optional().describe('Default expanded node IDs'),
defaultSelectedIds: z.array(z.string()).optional().describe('Default selected node IDs'),
expandedIds: z.array(z.string()).optional().describe('Controlled expanded node IDs'),
Expand Down
Loading
Loading