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
15 changes: 15 additions & 0 deletions .changeset/6124-handler-keys-json-refusal.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
---
"@object-ui/types": minor
---

`@object-ui/types/zod`: the 58 `on*` handler keys declared `z.function()` now refuse BY NAME (objectui#6124)

The zod mirrors declared 58 `on*` keys (26 distinct — `onClick`, `onChange`, `onOpenChange`, `onValueChange`, `onCardMove`, …) across `complex`, `data-display`, `disclosure`, `feedback`, `form`, `layout`, `navigation` and `overlay` as `z.function()`, a declaration no JSON document can satisfy on a JSON-authored vocabulary. A JSON author who wrote `onClick: { "action": "toast" }` was already refused, with zod's bare `invalid_type … expected function, received object` naming the key and nothing else.

Every one of the 58 sites is now a named refusal arm in the shape #5099 landed for `FieldConstraintsSchema.pattern.value` (`z.custom` + guidance, via `handlerKeyRefusal()` in `zod/tombstone.zod.ts`): the message names the key, says why JSON cannot author it, and points at the node-type spelling PR #6498 established (`{ "type": "toast" }`, an `action:button` node with a declared action). The same text is the key's `.describe()` metadata — one string, two channels. Deleting the keys was measured and refused: under `BaseSchema.passthrough()` an undeclared key is not refused, it is KEPT, and `onClick` rides `SDUI_DOM_PASS_THROUGH_KEYS` into the DOM listener slot where React throws at click.

**Accept-set change (Clause ②).** A live function value — which parsed green before — is now refused on the JSON mirror too. The programmatic face reaches renderers through the TypeScript interface and React props, never through `safeParse`; on this tree the only runtime `safeParse` doors into these mirrors are the CLI validators and the exported `validateSchema` / `safeValidateSchema` helpers, none of which is fed a function-bearing object. Code that ran a host-supplied function through one of these mirrors must stop doing so.

**TypeScript face, measured per key.** 36 keys whose function value reaches a renderer at runtime (read off `schema.*`, called as a React prop after `SchemaRenderer`'s spread, or spread onto a Radix root / DOM listener slot) keep their function type. 22 keys nothing reads carry the `?: never` tombstone (ADR-0049): `KanbanSchema.onColumnAdd` / `onCardAdd`, `CarouselSchema.onSlideChange`, `ChatbotSchema.onSendMessage`, `AlertSchema.onDismiss`, `ListItem.onClick`, `TreeViewSchema.onSelectChange` / `onExpandChange`, `ToastSchema.onDismiss`, `RadioGroupSchema` / `SwitchSchema` / `ToggleSchema` / `SliderSchema` / `CalendarSchema` / `ComboboxSchema` / `CommandSchema` `.onChange`, `InputOTPSchema.onComplete`, `BreadcrumbItem.onClick`, `SidebarSchema.onCollapsedChange`, `ButtonGroupButton.onClick`, `AlertDialogSchema.onConfirm` / `onCancel`. Assigning one of those is now a `tsc` error naming the key.

Out of scope, per the ruling: the four non-`on*` `z.function()` keys (`cell`, `custom`, `validate`, `renderCellEditor`) stay as they are; `EventHandlersSchema` is objectui#6910's card.
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,7 +136,14 @@ describe('toast fixtures: the registered spelling, not an action object off `onC
const result = ButtonSchema.safeParse(retired);
expect(result.success).toBe(false);
expect(result.error?.issues[0]?.path).toEqual(['onClick']);
expect(result.error?.issues[0]?.message).toContain('expected function, received object');
// Still RED, and now BY NAME: objectui#6124 replaced `ButtonSchema.onClick`'s
// bare `z.function()` (zod's "expected function, received object") with a
// named refusal arm that points at the node-type spelling this block's
// fixtures already use. The verdict this block leans on did not move; the
// message an author reads did.
expect(result.error?.issues[0]?.code).toBe('custom');
expect(result.error?.issues[0]?.message).toContain('`onClick` is a RUNTIME SLOT');
expect(result.error?.issues[0]?.message).toContain('{ "type": "toast"');
});
});

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,12 +125,28 @@ describe('ChatbotSchema: the ten local-display/legacy keys are declared, not ano
autoResponse: true,
autoResponseText: 'Thanks!',
autoResponseDelay: 1000,
onSend: () => {},
});

expect(result.success).toBe(true);
});

it('`onSend` is a RUNTIME SLOT the Zod mirror refuses by name; the TypeScript face above is its channel (objectui#6124)', () => {
// `onSend: () => {}` used to sit in the green fixture above. objectui#6124
// replaced every `on*: z.function()` arm with a named refusal: a JSON face
// has no function value, and `plugin-chatbot` reads `schema.onSend` through
// the TypeScript interface (which keeps the callable member — see the first
// `it` in this file), never through `safeParse`.
const result = ChatbotZodSchema.safeParse({
type: 'chatbot',
messages: [{ id: '1', role: 'user', content: 'hi' }],
onSend: () => {},
});
expect(result.success).toBe(false);
const issue = result.error?.issues.find((i) => String(i.path[0]) === 'onSend');
expect(issue?.code).toBe('custom');
expect(issue?.message).toContain('`onSend` is a RUNTIME SLOT');
});

it('refuses a wrong-typed value on a declared key through the Zod mirror (was silently passed through before)', () => {
const result = ChatbotZodSchema.safeParse({
type: 'chatbot',
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
15 changes: 15 additions & 0 deletions .changeset/6124-handler-keys-json-refusal.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
---
"@object-ui/types": minor
---

`@object-ui/types/zod`: the 58 `on*` handler keys declared `z.function()` now refuse BY NAME (objectui#6124)

The zod mirrors declared 58 `on*` keys (26 distinct — `onClick`, `onChange`, `onOpenChange`, `onValueChange`, `onCardMove`, …) across `complex`, `data-display`, `disclosure`, `feedback`, `form`, `layout`, `navigation` and `overlay` as `z.function()`, a declaration no JSON document can satisfy on a JSON-authored vocabulary. A JSON author who wrote `onClick: { "action": "toast" }` was already refused, with zod's bare `invalid_type … expected function, received object` naming the key and nothing else.

Every one of the 58 sites is now a named refusal arm in the shape #5099 landed for `FieldConstraintsSchema.pattern.value` (`z.custom` + guidance, via `handlerKeyRefusal()` in `zod/tombstone.zod.ts`): the message names the key, says why JSON cannot author it, and points at the node-type spelling PR #6498 established (`{ "type": "toast" }`, an `action:button` node with a declared action). The same text is the key's `.describe()` metadata — one string, two channels. Deleting the keys was measured and refused: under `BaseSchema.passthrough()` an undeclared key is not refused, it is KEPT, and `onClick` rides `SDUI_DOM_PASS_THROUGH_KEYS` into the DOM listener slot where React throws at click.

**Accept-set change (Clause ②).** A live function value — which parsed green before — is now refused on the JSON mirror too. The programmatic face reaches renderers through the TypeScript interface and React props, never through `safeParse`; on this tree the only runtime `safeParse` doors into these mirrors are the CLI validators and the exported `validateSchema` / `safeValidateSchema` helpers, none of which is fed a function-bearing object. Code that ran a host-supplied function through one of these mirrors must stop doing so.

**TypeScript face, measured per key.** 36 keys whose function value reaches a renderer at runtime (read off `schema.*`, called as a React prop after `SchemaRenderer`'s spread, or spread onto a Radix root / DOM listener slot) keep their function type. 22 keys nothing reads carry the `?: never` tombstone (ADR-0049): `KanbanSchema.onColumnAdd` / `onCardAdd`, `CarouselSchema.onSlideChange`, `ChatbotSchema.onSendMessage`, `AlertSchema.onDismiss`, `ListItem.onClick`, `TreeViewSchema.onSelectChange` / `onExpandChange`, `ToastSchema.onDismiss`, `RadioGroupSchema` / `SwitchSchema` / `ToggleSchema` / `SliderSchema` / `CalendarSchema` / `ComboboxSchema` / `CommandSchema` `.onChange`, `InputOTPSchema.onComplete`, `BreadcrumbItem.onClick`, `SidebarSchema.onCollapsedChange`, `ButtonGroupButton.onClick`, `AlertDialogSchema.onConfirm` / `onCancel`. Assigning one of those is now a `tsc` error naming the key.

Out of scope, per the ruling: the four non-`on*` `z.function()` keys (`cell`, `custom`, `validate`, `renderCellEditor`) stay as they are; `EventHandlersSchema` is objectui#6910's card.
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,7 +136,14 @@ describe('toast fixtures: the registered spelling, not an action object off `onC
const result = ButtonSchema.safeParse(retired);
expect(result.success).toBe(false);
expect(result.error?.issues[0]?.path).toEqual(['onClick']);
expect(result.error?.issues[0]?.message).toContain('expected function, received object');
// Still RED, and now BY NAME: objectui#6124 replaced `ButtonSchema.onClick`'s
// bare `z.function()` (zod's "expected function, received object") with a
// named refusal arm that points at the node-type spelling this block's
// fixtures already use. The verdict this block leans on did not move; the
// message an author reads did.
expect(result.error?.issues[0]?.code).toBe('custom');
expect(result.error?.issues[0]?.message).toContain('`onClick` is a RUNTIME SLOT');
expect(result.error?.issues[0]?.message).toContain('{ "type": "toast"');
});
});

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,12 +125,28 @@ describe('ChatbotSchema: the ten local-display/legacy keys are declared, not ano
autoResponse: true,
autoResponseText: 'Thanks!',
autoResponseDelay: 1000,
onSend: () => {},
});

expect(result.success).toBe(true);
});

it('`onSend` is a RUNTIME SLOT the Zod mirror refuses by name; the TypeScript face above is its channel (objectui#6124)', () => {
// `onSend: () => {}` used to sit in the green fixture above. objectui#6124
// replaced every `on*: z.function()` arm with a named refusal: a JSON face
// has no function value, and `plugin-chatbot` reads `schema.onSend` through
// the TypeScript interface (which keeps the callable member — see the first
// `it` in this file), never through `safeParse`.
const result = ChatbotZodSchema.safeParse({
type: 'chatbot',
messages: [{ id: '1', role: 'user', content: 'hi' }],
onSend: () => {},
});
expect(result.success).toBe(false);
const issue = result.error?.issues.find((i) => String(i.path[0]) === 'onSend');
expect(issue?.code).toBe('custom');
expect(issue?.message).toContain('`onSend` is a RUNTIME SLOT');
});

it('refuses a wrong-typed value on a declared key through the Zod mirror (was silently passed through before)', () => {
const result = ChatbotZodSchema.safeParse({
type: 'chatbot',
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
15 changes: 15 additions & 0 deletions .changeset/6124-handler-keys-json-refusal.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
---
"@object-ui/types": minor
---

`@object-ui/types/zod`: the 58 `on*` handler keys declared `z.function()` now refuse BY NAME (objectui#6124)

The zod mirrors declared 58 `on*` keys (26 distinct — `onClick`, `onChange`, `onOpenChange`, `onValueChange`, `onCardMove`, …) across `complex`, `data-display`, `disclosure`, `feedback`, `form`, `layout`, `navigation` and `overlay` as `z.function()`, a declaration no JSON document can satisfy on a JSON-authored vocabulary. A JSON author who wrote `onClick: { "action": "toast" }` was already refused, with zod's bare `invalid_type … expected function, received object` naming the key and nothing else.

Every one of the 58 sites is now a named refusal arm in the shape #5099 landed for `FieldConstraintsSchema.pattern.value` (`z.custom` + guidance, via `handlerKeyRefusal()` in `zod/tombstone.zod.ts`): the message names the key, says why JSON cannot author it, and points at the node-type spelling PR #6498 established (`{ "type": "toast" }`, an `action:button` node with a declared action). The same text is the key's `.describe()` metadata — one string, two channels. Deleting the keys was measured and refused: under `BaseSchema.passthrough()` an undeclared key is not refused, it is KEPT, and `onClick` rides `SDUI_DOM_PASS_THROUGH_KEYS` into the DOM listener slot where React throws at click.

**Accept-set change (Clause ②).** A live function value — which parsed green before — is now refused on the JSON mirror too. The programmatic face reaches renderers through the TypeScript interface and React props, never through `safeParse`; on this tree the only runtime `safeParse` doors into these mirrors are the CLI validators and the exported `validateSchema` / `safeValidateSchema` helpers, none of which is fed a function-bearing object. Code that ran a host-supplied function through one of these mirrors must stop doing so.

**TypeScript face, measured per key.** 36 keys whose function value reaches a renderer at runtime (read off `schema.*`, called as a React prop after `SchemaRenderer`'s spread, or spread onto a Radix root / DOM listener slot) keep their function type. 22 keys nothing reads carry the `?: never` tombstone (ADR-0049): `KanbanSchema.onColumnAdd` / `onCardAdd`, `CarouselSchema.onSlideChange`, `ChatbotSchema.onSendMessage`, `AlertSchema.onDismiss`, `ListItem.onClick`, `TreeViewSchema.onSelectChange` / `onExpandChange`, `ToastSchema.onDismiss`, `RadioGroupSchema` / `SwitchSchema` / `ToggleSchema` / `SliderSchema` / `CalendarSchema` / `ComboboxSchema` / `CommandSchema` `.onChange`, `InputOTPSchema.onComplete`, `BreadcrumbItem.onClick`, `SidebarSchema.onCollapsedChange`, `ButtonGroupButton.onClick`, `AlertDialogSchema.onConfirm` / `onCancel`. Assigning one of those is now a `tsc` error naming the key.

Out of scope, per the ruling: the four non-`on*` `z.function()` keys (`cell`, `custom`, `validate`, `renderCellEditor`) stay as they are; `EventHandlersSchema` is objectui#6910's card.
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,7 +136,14 @@ describe('toast fixtures: the registered spelling, not an action object off `onC
const result = ButtonSchema.safeParse(retired);
expect(result.success).toBe(false);
expect(result.error?.issues[0]?.path).toEqual(['onClick']);
expect(result.error?.issues[0]?.message).toContain('expected function, received object');
// Still RED, and now BY NAME: objectui#6124 replaced `ButtonSchema.onClick`'s
// bare `z.function()` (zod's "expected function, received object") with a
// named refusal arm that points at the node-type spelling this block's
// fixtures already use. The verdict this block leans on did not move; the
// message an author reads did.
expect(result.error?.issues[0]?.code).toBe('custom');
expect(result.error?.issues[0]?.message).toContain('`onClick` is a RUNTIME SLOT');
expect(result.error?.issues[0]?.message).toContain('{ "type": "toast"');
});
});

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,12 +125,28 @@ describe('ChatbotSchema: the ten local-display/legacy keys are declared, not ano
autoResponse: true,
autoResponseText: 'Thanks!',
autoResponseDelay: 1000,
onSend: () => {},
});

expect(result.success).toBe(true);
});

it('`onSend` is a RUNTIME SLOT the Zod mirror refuses by name; the TypeScript face above is its channel (objectui#6124)', () => {
// `onSend: () => {}` used to sit in the green fixture above. objectui#6124
// replaced every `on*: z.function()` arm with a named refusal: a JSON face
// has no function value, and `plugin-chatbot` reads `schema.onSend` through
// the TypeScript interface (which keeps the callable member — see the first
// `it` in this file), never through `safeParse`.
const result = ChatbotZodSchema.safeParse({
type: 'chatbot',
messages: [{ id: '1', role: 'user', content: 'hi' }],
onSend: () => {},
});
expect(result.success).toBe(false);
const issue = result.error?.issues.find((i) => String(i.path[0]) === 'onSend');
expect(issue?.code).toBe('custom');
expect(issue?.message).toContain('`onSend` is a RUNTIME SLOT');
});

it('refuses a wrong-typed value on a declared key through the Zod mirror (was silently passed through before)', () => {
const result = ChatbotZodSchema.safeParse({
type: 'chatbot',
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
15 changes: 15 additions & 0 deletions .changeset/6124-handler-keys-json-refusal.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
---
"@object-ui/types": minor
---

`@object-ui/types/zod`: the 58 `on*` handler keys declared `z.function()` now refuse BY NAME (objectui#6124)

The zod mirrors declared 58 `on*` keys (26 distinct — `onClick`, `onChange`, `onOpenChange`, `onValueChange`, `onCardMove`, …) across `complex`, `data-display`, `disclosure`, `feedback`, `form`, `layout`, `navigation` and `overlay` as `z.function()`, a declaration no JSON document can satisfy on a JSON-authored vocabulary. A JSON author who wrote `onClick: { "action": "toast" }` was already refused, with zod's bare `invalid_type … expected function, received object` naming the key and nothing else.

Every one of the 58 sites is now a named refusal arm in the shape #5099 landed for `FieldConstraintsSchema.pattern.value` (`z.custom` + guidance, via `handlerKeyRefusal()` in `zod/tombstone.zod.ts`): the message names the key, says why JSON cannot author it, and points at the node-type spelling PR #6498 established (`{ "type": "toast" }`, an `action:button` node with a declared action). The same text is the key's `.describe()` metadata — one string, two channels. Deleting the keys was measured and refused: under `BaseSchema.passthrough()` an undeclared key is not refused, it is KEPT, and `onClick` rides `SDUI_DOM_PASS_THROUGH_KEYS` into the DOM listener slot where React throws at click.

**Accept-set change (Clause ②).** A live function value — which parsed green before — is now refused on the JSON mirror too. The programmatic face reaches renderers through the TypeScript interface and React props, never through `safeParse`; on this tree the only runtime `safeParse` doors into these mirrors are the CLI validators and the exported `validateSchema` / `safeValidateSchema` helpers, none of which is fed a function-bearing object. Code that ran a host-supplied function through one of these mirrors must stop doing so.

**TypeScript face, measured per key.** 36 keys whose function value reaches a renderer at runtime (read off `schema.*`, called as a React prop after `SchemaRenderer`'s spread, or spread onto a Radix root / DOM listener slot) keep their function type. 22 keys nothing reads carry the `?: never` tombstone (ADR-0049): `KanbanSchema.onColumnAdd` / `onCardAdd`, `CarouselSchema.onSlideChange`, `ChatbotSchema.onSendMessage`, `AlertSchema.onDismiss`, `ListItem.onClick`, `TreeViewSchema.onSelectChange` / `onExpandChange`, `ToastSchema.onDismiss`, `RadioGroupSchema` / `SwitchSchema` / `ToggleSchema` / `SliderSchema` / `CalendarSchema` / `ComboboxSchema` / `CommandSchema` `.onChange`, `InputOTPSchema.onComplete`, `BreadcrumbItem.onClick`, `SidebarSchema.onCollapsedChange`, `ButtonGroupButton.onClick`, `AlertDialogSchema.onConfirm` / `onCancel`. Assigning one of those is now a `tsc` error naming the key.

Out of scope, per the ruling: the four non-`on*` `z.function()` keys (`cell`, `custom`, `validate`, `renderCellEditor`) stay as they are; `EventHandlersSchema` is objectui#6910's card.
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,7 +136,14 @@ describe('toast fixtures: the registered spelling, not an action object off `onC
const result = ButtonSchema.safeParse(retired);
expect(result.success).toBe(false);
expect(result.error?.issues[0]?.path).toEqual(['onClick']);
expect(result.error?.issues[0]?.message).toContain('expected function, received object');
// Still RED, and now BY NAME: objectui#6124 replaced `ButtonSchema.onClick`'s
// bare `z.function()` (zod's "expected function, received object") with a
// named refusal arm that points at the node-type spelling this block's
// fixtures already use. The verdict this block leans on did not move; the
// message an author reads did.
expect(result.error?.issues[0]?.code).toBe('custom');
expect(result.error?.issues[0]?.message).toContain('`onClick` is a RUNTIME SLOT');
expect(result.error?.issues[0]?.message).toContain('{ "type": "toast"');
});
});

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,12 +125,28 @@ describe('ChatbotSchema: the ten local-display/legacy keys are declared, not ano
autoResponse: true,
autoResponseText: 'Thanks!',
autoResponseDelay: 1000,
onSend: () => {},
});

expect(result.success).toBe(true);
});

it('`onSend` is a RUNTIME SLOT the Zod mirror refuses by name; the TypeScript face above is its channel (objectui#6124)', () => {
// `onSend: () => {}` used to sit in the green fixture above. objectui#6124
// replaced every `on*: z.function()` arm with a named refusal: a JSON face
// has no function value, and `plugin-chatbot` reads `schema.onSend` through
// the TypeScript interface (which keeps the callable member — see the first
// `it` in this file), never through `safeParse`.
const result = ChatbotZodSchema.safeParse({
type: 'chatbot',
messages: [{ id: '1', role: 'user', content: 'hi' }],
onSend: () => {},
});
expect(result.success).toBe(false);
const issue = result.error?.issues.find((i) => String(i.path[0]) === 'onSend');
expect(issue?.code).toBe('custom');
expect(issue?.message).toContain('`onSend` is a RUNTIME SLOT');
});

it('refuses a wrong-typed value on a declared key through the Zod mirror (was silently passed through before)', () => {
const result = ChatbotZodSchema.safeParse({
type: 'chatbot',
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
15 changes: 15 additions & 0 deletions .changeset/6124-handler-keys-json-refusal.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
---
"@object-ui/types": minor
---

`@object-ui/types/zod`: the 58 `on*` handler keys declared `z.function()` now refuse BY NAME (objectui#6124)

The zod mirrors declared 58 `on*` keys (26 distinct — `onClick`, `onChange`, `onOpenChange`, `onValueChange`, `onCardMove`, …) across `complex`, `data-display`, `disclosure`, `feedback`, `form`, `layout`, `navigation` and `overlay` as `z.function()`, a declaration no JSON document can satisfy on a JSON-authored vocabulary. A JSON author who wrote `onClick: { "action": "toast" }` was already refused, with zod's bare `invalid_type … expected function, received object` naming the key and nothing else.

Every one of the 58 sites is now a named refusal arm in the shape #5099 landed for `FieldConstraintsSchema.pattern.value` (`z.custom` + guidance, via `handlerKeyRefusal()` in `zod/tombstone.zod.ts`): the message names the key, says why JSON cannot author it, and points at the node-type spelling PR #6498 established (`{ "type": "toast" }`, an `action:button` node with a declared action). The same text is the key's `.describe()` metadata — one string, two channels. Deleting the keys was measured and refused: under `BaseSchema.passthrough()` an undeclared key is not refused, it is KEPT, and `onClick` rides `SDUI_DOM_PASS_THROUGH_KEYS` into the DOM listener slot where React throws at click.

**Accept-set change (Clause ②).** A live function value — which parsed green before — is now refused on the JSON mirror too. The programmatic face reaches renderers through the TypeScript interface and React props, never through `safeParse`; on this tree the only runtime `safeParse` doors into these mirrors are the CLI validators and the exported `validateSchema` / `safeValidateSchema` helpers, none of which is fed a function-bearing object. Code that ran a host-supplied function through one of these mirrors must stop doing so.

**TypeScript face, measured per key.** 36 keys whose function value reaches a renderer at runtime (read off `schema.*`, called as a React prop after `SchemaRenderer`'s spread, or spread onto a Radix root / DOM listener slot) keep their function type. 22 keys nothing reads carry the `?: never` tombstone (ADR-0049): `KanbanSchema.onColumnAdd` / `onCardAdd`, `CarouselSchema.onSlideChange`, `ChatbotSchema.onSendMessage`, `AlertSchema.onDismiss`, `ListItem.onClick`, `TreeViewSchema.onSelectChange` / `onExpandChange`, `ToastSchema.onDismiss`, `RadioGroupSchema` / `SwitchSchema` / `ToggleSchema` / `SliderSchema` / `CalendarSchema` / `ComboboxSchema` / `CommandSchema` `.onChange`, `InputOTPSchema.onComplete`, `BreadcrumbItem.onClick`, `SidebarSchema.onCollapsedChange`, `ButtonGroupButton.onClick`, `AlertDialogSchema.onConfirm` / `onCancel`. Assigning one of those is now a `tsc` error naming the key.

Out of scope, per the ruling: the four non-`on*` `z.function()` keys (`cell`, `custom`, `validate`, `renderCellEditor`) stay as they are; `EventHandlersSchema` is objectui#6910's card.
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,7 +136,14 @@ describe('toast fixtures: the registered spelling, not an action object off `onC
const result = ButtonSchema.safeParse(retired);
expect(result.success).toBe(false);
expect(result.error?.issues[0]?.path).toEqual(['onClick']);
expect(result.error?.issues[0]?.message).toContain('expected function, received object');
// Still RED, and now BY NAME: objectui#6124 replaced `ButtonSchema.onClick`'s
// bare `z.function()` (zod's "expected function, received object") with a
// named refusal arm that points at the node-type spelling this block's
// fixtures already use. The verdict this block leans on did not move; the
// message an author reads did.
expect(result.error?.issues[0]?.code).toBe('custom');
expect(result.error?.issues[0]?.message).toContain('`onClick` is a RUNTIME SLOT');
expect(result.error?.issues[0]?.message).toContain('{ "type": "toast"');
});
});

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,12 +125,28 @@ describe('ChatbotSchema: the ten local-display/legacy keys are declared, not ano
autoResponse: true,
autoResponseText: 'Thanks!',
autoResponseDelay: 1000,
onSend: () => {},
});

expect(result.success).toBe(true);
});

it('`onSend` is a RUNTIME SLOT the Zod mirror refuses by name; the TypeScript face above is its channel (objectui#6124)', () => {
// `onSend: () => {}` used to sit in the green fixture above. objectui#6124
// replaced every `on*: z.function()` arm with a named refusal: a JSON face
// has no function value, and `plugin-chatbot` reads `schema.onSend` through
// the TypeScript interface (which keeps the callable member — see the first
// `it` in this file), never through `safeParse`.
const result = ChatbotZodSchema.safeParse({
type: 'chatbot',
messages: [{ id: '1', role: 'user', content: 'hi' }],
onSend: () => {},
});
expect(result.success).toBe(false);
const issue = result.error?.issues.find((i) => String(i.path[0]) === 'onSend');
expect(issue?.code).toBe('custom');
expect(issue?.message).toContain('`onSend` is a RUNTIME SLOT');
});

it('refuses a wrong-typed value on a declared key through the Zod mirror (was silently passed through before)', () => {
const result = ChatbotZodSchema.safeParse({
type: 'chatbot',
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
15 changes: 15 additions & 0 deletions .changeset/6124-handler-keys-json-refusal.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
---
"@object-ui/types": minor
---

`@object-ui/types/zod`: the 58 `on*` handler keys declared `z.function()` now refuse BY NAME (objectui#6124)

The zod mirrors declared 58 `on*` keys (26 distinct — `onClick`, `onChange`, `onOpenChange`, `onValueChange`, `onCardMove`, …) across `complex`, `data-display`, `disclosure`, `feedback`, `form`, `layout`, `navigation` and `overlay` as `z.function()`, a declaration no JSON document can satisfy on a JSON-authored vocabulary. A JSON author who wrote `onClick: { "action": "toast" }` was already refused, with zod's bare `invalid_type … expected function, received object` naming the key and nothing else.

Every one of the 58 sites is now a named refusal arm in the shape #5099 landed for `FieldConstraintsSchema.pattern.value` (`z.custom` + guidance, via `handlerKeyRefusal()` in `zod/tombstone.zod.ts`): the message names the key, says why JSON cannot author it, and points at the node-type spelling PR #6498 established (`{ "type": "toast" }`, an `action:button` node with a declared action). The same text is the key's `.describe()` metadata — one string, two channels. Deleting the keys was measured and refused: under `BaseSchema.passthrough()` an undeclared key is not refused, it is KEPT, and `onClick` rides `SDUI_DOM_PASS_THROUGH_KEYS` into the DOM listener slot where React throws at click.

**Accept-set change (Clause ②).** A live function value — which parsed green before — is now refused on the JSON mirror too. The programmatic face reaches renderers through the TypeScript interface and React props, never through `safeParse`; on this tree the only runtime `safeParse` doors into these mirrors are the CLI validators and the exported `validateSchema` / `safeValidateSchema` helpers, none of which is fed a function-bearing object. Code that ran a host-supplied function through one of these mirrors must stop doing so.

**TypeScript face, measured per key.** 36 keys whose function value reaches a renderer at runtime (read off `schema.*`, called as a React prop after `SchemaRenderer`'s spread, or spread onto a Radix root / DOM listener slot) keep their function type. 22 keys nothing reads carry the `?: never` tombstone (ADR-0049): `KanbanSchema.onColumnAdd` / `onCardAdd`, `CarouselSchema.onSlideChange`, `ChatbotSchema.onSendMessage`, `AlertSchema.onDismiss`, `ListItem.onClick`, `TreeViewSchema.onSelectChange` / `onExpandChange`, `ToastSchema.onDismiss`, `RadioGroupSchema` / `SwitchSchema` / `ToggleSchema` / `SliderSchema` / `CalendarSchema` / `ComboboxSchema` / `CommandSchema` `.onChange`, `InputOTPSchema.onComplete`, `BreadcrumbItem.onClick`, `SidebarSchema.onCollapsedChange`, `ButtonGroupButton.onClick`, `AlertDialogSchema.onConfirm` / `onCancel`. Assigning one of those is now a `tsc` error naming the key.

Out of scope, per the ruling: the four non-`on*` `z.function()` keys (`cell`, `custom`, `validate`, `renderCellEditor`) stay as they are; `EventHandlersSchema` is objectui#6910's card.
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,7 +136,14 @@ describe('toast fixtures: the registered spelling, not an action object off `onC
const result = ButtonSchema.safeParse(retired);
expect(result.success).toBe(false);
expect(result.error?.issues[0]?.path).toEqual(['onClick']);
expect(result.error?.issues[0]?.message).toContain('expected function, received object');
// Still RED, and now BY NAME: objectui#6124 replaced `ButtonSchema.onClick`'s
// bare `z.function()` (zod's "expected function, received object") with a
// named refusal arm that points at the node-type spelling this block's
// fixtures already use. The verdict this block leans on did not move; the
// message an author reads did.
expect(result.error?.issues[0]?.code).toBe('custom');
expect(result.error?.issues[0]?.message).toContain('`onClick` is a RUNTIME SLOT');
expect(result.error?.issues[0]?.message).toContain('{ "type": "toast"');
});
});

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,12 +125,28 @@ describe('ChatbotSchema: the ten local-display/legacy keys are declared, not ano
autoResponse: true,
autoResponseText: 'Thanks!',
autoResponseDelay: 1000,
onSend: () => {},
});

expect(result.success).toBe(true);
});

it('`onSend` is a RUNTIME SLOT the Zod mirror refuses by name; the TypeScript face above is its channel (objectui#6124)', () => {
// `onSend: () => {}` used to sit in the green fixture above. objectui#6124
// replaced every `on*: z.function()` arm with a named refusal: a JSON face
// has no function value, and `plugin-chatbot` reads `schema.onSend` through
// the TypeScript interface (which keeps the callable member — see the first
// `it` in this file), never through `safeParse`.
const result = ChatbotZodSchema.safeParse({
type: 'chatbot',
messages: [{ id: '1', role: 'user', content: 'hi' }],
onSend: () => {},
});
expect(result.success).toBe(false);
const issue = result.error?.issues.find((i) => String(i.path[0]) === 'onSend');
expect(issue?.code).toBe('custom');
expect(issue?.message).toContain('`onSend` is a RUNTIME SLOT');
});

it('refuses a wrong-typed value on a declared key through the Zod mirror (was silently passed through before)', () => {
const result = ChatbotZodSchema.safeParse({
type: 'chatbot',
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
15 changes: 15 additions & 0 deletions .changeset/6124-handler-keys-json-refusal.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
---
"@object-ui/types": minor
---

`@object-ui/types/zod`: the 58 `on*` handler keys declared `z.function()` now refuse BY NAME (objectui#6124)

The zod mirrors declared 58 `on*` keys (26 distinct — `onClick`, `onChange`, `onOpenChange`, `onValueChange`, `onCardMove`, …) across `complex`, `data-display`, `disclosure`, `feedback`, `form`, `layout`, `navigation` and `overlay` as `z.function()`, a declaration no JSON document can satisfy on a JSON-authored vocabulary. A JSON author who wrote `onClick: { "action": "toast" }` was already refused, with zod's bare `invalid_type … expected function, received object` naming the key and nothing else.

Every one of the 58 sites is now a named refusal arm in the shape #5099 landed for `FieldConstraintsSchema.pattern.value` (`z.custom` + guidance, via `handlerKeyRefusal()` in `zod/tombstone.zod.ts`): the message names the key, says why JSON cannot author it, and points at the node-type spelling PR #6498 established (`{ "type": "toast" }`, an `action:button` node with a declared action). The same text is the key's `.describe()` metadata — one string, two channels. Deleting the keys was measured and refused: under `BaseSchema.passthrough()` an undeclared key is not refused, it is KEPT, and `onClick` rides `SDUI_DOM_PASS_THROUGH_KEYS` into the DOM listener slot where React throws at click.

**Accept-set change (Clause ②).** A live function value — which parsed green before — is now refused on the JSON mirror too. The programmatic face reaches renderers through the TypeScript interface and React props, never through `safeParse`; on this tree the only runtime `safeParse` doors into these mirrors are the CLI validators and the exported `validateSchema` / `safeValidateSchema` helpers, none of which is fed a function-bearing object. Code that ran a host-supplied function through one of these mirrors must stop doing so.

**TypeScript face, measured per key.** 36 keys whose function value reaches a renderer at runtime (read off `schema.*`, called as a React prop after `SchemaRenderer`'s spread, or spread onto a Radix root / DOM listener slot) keep their function type. 22 keys nothing reads carry the `?: never` tombstone (ADR-0049): `KanbanSchema.onColumnAdd` / `onCardAdd`, `CarouselSchema.onSlideChange`, `ChatbotSchema.onSendMessage`, `AlertSchema.onDismiss`, `ListItem.onClick`, `TreeViewSchema.onSelectChange` / `onExpandChange`, `ToastSchema.onDismiss`, `RadioGroupSchema` / `SwitchSchema` / `ToggleSchema` / `SliderSchema` / `CalendarSchema` / `ComboboxSchema` / `CommandSchema` `.onChange`, `InputOTPSchema.onComplete`, `BreadcrumbItem.onClick`, `SidebarSchema.onCollapsedChange`, `ButtonGroupButton.onClick`, `AlertDialogSchema.onConfirm` / `onCancel`. Assigning one of those is now a `tsc` error naming the key.

Out of scope, per the ruling: the four non-`on*` `z.function()` keys (`cell`, `custom`, `validate`, `renderCellEditor`) stay as they are; `EventHandlersSchema` is objectui#6910's card.
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,7 +136,14 @@ describe('toast fixtures: the registered spelling, not an action object off `onC
const result = ButtonSchema.safeParse(retired);
expect(result.success).toBe(false);
expect(result.error?.issues[0]?.path).toEqual(['onClick']);
expect(result.error?.issues[0]?.message).toContain('expected function, received object');
// Still RED, and now BY NAME: objectui#6124 replaced `ButtonSchema.onClick`'s
// bare `z.function()` (zod's "expected function, received object") with a
// named refusal arm that points at the node-type spelling this block's
// fixtures already use. The verdict this block leans on did not move; the
// message an author reads did.
expect(result.error?.issues[0]?.code).toBe('custom');
expect(result.error?.issues[0]?.message).toContain('`onClick` is a RUNTIME SLOT');
expect(result.error?.issues[0]?.message).toContain('{ "type": "toast"');
});
});

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,12 +125,28 @@ describe('ChatbotSchema: the ten local-display/legacy keys are declared, not ano
autoResponse: true,
autoResponseText: 'Thanks!',
autoResponseDelay: 1000,
onSend: () => {},
});

expect(result.success).toBe(true);
});

it('`onSend` is a RUNTIME SLOT the Zod mirror refuses by name; the TypeScript face above is its channel (objectui#6124)', () => {
// `onSend: () => {}` used to sit in the green fixture above. objectui#6124
// replaced every `on*: z.function()` arm with a named refusal: a JSON face
// has no function value, and `plugin-chatbot` reads `schema.onSend` through
// the TypeScript interface (which keeps the callable member — see the first
// `it` in this file), never through `safeParse`.
const result = ChatbotZodSchema.safeParse({
type: 'chatbot',
messages: [{ id: '1', role: 'user', content: 'hi' }],
onSend: () => {},
});
expect(result.success).toBe(false);
const issue = result.error?.issues.find((i) => String(i.path[0]) === 'onSend');
expect(issue?.code).toBe('custom');
expect(issue?.message).toContain('`onSend` is a RUNTIME SLOT');
});

it('refuses a wrong-typed value on a declared key through the Zod mirror (was silently passed through before)', () => {
const result = ChatbotZodSchema.safeParse({
type: 'chatbot',
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
15 changes: 15 additions & 0 deletions .changeset/6124-handler-keys-json-refusal.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
---
"@object-ui/types": minor
---

`@object-ui/types/zod`: the 58 `on*` handler keys declared `z.function()` now refuse BY NAME (objectui#6124)

The zod mirrors declared 58 `on*` keys (26 distinct — `onClick`, `onChange`, `onOpenChange`, `onValueChange`, `onCardMove`, …) across `complex`, `data-display`, `disclosure`, `feedback`, `form`, `layout`, `navigation` and `overlay` as `z.function()`, a declaration no JSON document can satisfy on a JSON-authored vocabulary. A JSON author who wrote `onClick: { "action": "toast" }` was already refused, with zod's bare `invalid_type … expected function, received object` naming the key and nothing else.

Every one of the 58 sites is now a named refusal arm in the shape #5099 landed for `FieldConstraintsSchema.pattern.value` (`z.custom` + guidance, via `handlerKeyRefusal()` in `zod/tombstone.zod.ts`): the message names the key, says why JSON cannot author it, and points at the node-type spelling PR #6498 established (`{ "type": "toast" }`, an `action:button` node with a declared action). The same text is the key's `.describe()` metadata — one string, two channels. Deleting the keys was measured and refused: under `BaseSchema.passthrough()` an undeclared key is not refused, it is KEPT, and `onClick` rides `SDUI_DOM_PASS_THROUGH_KEYS` into the DOM listener slot where React throws at click.

**Accept-set change (Clause ②).** A live function value — which parsed green before — is now refused on the JSON mirror too. The programmatic face reaches renderers through the TypeScript interface and React props, never through `safeParse`; on this tree the only runtime `safeParse` doors into these mirrors are the CLI validators and the exported `validateSchema` / `safeValidateSchema` helpers, none of which is fed a function-bearing object. Code that ran a host-supplied function through one of these mirrors must stop doing so.

**TypeScript face, measured per key.** 36 keys whose function value reaches a renderer at runtime (read off `schema.*`, called as a React prop after `SchemaRenderer`'s spread, or spread onto a Radix root / DOM listener slot) keep their function type. 22 keys nothing reads carry the `?: never` tombstone (ADR-0049): `KanbanSchema.onColumnAdd` / `onCardAdd`, `CarouselSchema.onSlideChange`, `ChatbotSchema.onSendMessage`, `AlertSchema.onDismiss`, `ListItem.onClick`, `TreeViewSchema.onSelectChange` / `onExpandChange`, `ToastSchema.onDismiss`, `RadioGroupSchema` / `SwitchSchema` / `ToggleSchema` / `SliderSchema` / `CalendarSchema` / `ComboboxSchema` / `CommandSchema` `.onChange`, `InputOTPSchema.onComplete`, `BreadcrumbItem.onClick`, `SidebarSchema.onCollapsedChange`, `ButtonGroupButton.onClick`, `AlertDialogSchema.onConfirm` / `onCancel`. Assigning one of those is now a `tsc` error naming the key.

Out of scope, per the ruling: the four non-`on*` `z.function()` keys (`cell`, `custom`, `validate`, `renderCellEditor`) stay as they are; `EventHandlersSchema` is objectui#6910's card.
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,7 +136,14 @@ describe('toast fixtures: the registered spelling, not an action object off `onC
const result = ButtonSchema.safeParse(retired);
expect(result.success).toBe(false);
expect(result.error?.issues[0]?.path).toEqual(['onClick']);
expect(result.error?.issues[0]?.message).toContain('expected function, received object');
// Still RED, and now BY NAME: objectui#6124 replaced `ButtonSchema.onClick`'s
// bare `z.function()` (zod's "expected function, received object") with a
// named refusal arm that points at the node-type spelling this block's
// fixtures already use. The verdict this block leans on did not move; the
// message an author reads did.
expect(result.error?.issues[0]?.code).toBe('custom');
expect(result.error?.issues[0]?.message).toContain('`onClick` is a RUNTIME SLOT');
expect(result.error?.issues[0]?.message).toContain('{ "type": "toast"');
});
});

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,12 +125,28 @@ describe('ChatbotSchema: the ten local-display/legacy keys are declared, not ano
autoResponse: true,
autoResponseText: 'Thanks!',
autoResponseDelay: 1000,
onSend: () => {},
});

expect(result.success).toBe(true);
});

it('`onSend` is a RUNTIME SLOT the Zod mirror refuses by name; the TypeScript face above is its channel (objectui#6124)', () => {
// `onSend: () => {}` used to sit in the green fixture above. objectui#6124
// replaced every `on*: z.function()` arm with a named refusal: a JSON face
// has no function value, and `plugin-chatbot` reads `schema.onSend` through
// the TypeScript interface (which keeps the callable member — see the first
// `it` in this file), never through `safeParse`.
const result = ChatbotZodSchema.safeParse({
type: 'chatbot',
messages: [{ id: '1', role: 'user', content: 'hi' }],
onSend: () => {},
});
expect(result.success).toBe(false);
const issue = result.error?.issues.find((i) => String(i.path[0]) === 'onSend');
expect(issue?.code).toBe('custom');
expect(issue?.message).toContain('`onSend` is a RUNTIME SLOT');
});

it('refuses a wrong-typed value on a declared key through the Zod mirror (was silently passed through before)', () => {
const result = ChatbotZodSchema.safeParse({
type: 'chatbot',
Expand Down
Loading
Loading