diff --git a/.changeset/6250-toast-demo-shapes.md b/.changeset/6250-toast-demo-shapes.md
new file mode 100644
index 0000000000..88aa0717a3
--- /dev/null
+++ b/.changeset/6250-toast-demo-shapes.md
@@ -0,0 +1,18 @@
+---
+---
+
+Docs and fixtures only: the seven `components-feedback-toast/*` and seven
+`components-feedback-sonner/*` `SchemaExample` fixtures hung an action object off
+`onClick` (`{"type":"button", …, "onClick":{"action":"toast", …}}`) — a shape
+`ButtonSchema` declares as a FUNCTION and no dispatcher reads, so all fourteen were a
+RED `safeParse` on the envelope and clicking the rendered demo raised no toast
+(objectui#6250). They now author the registered spellings the engine already executes,
+`type: 'toast'` and `type: 'sonner'`, whose renderers draw their own trigger button and
+call sonner's `toast()` from it. `feedback/toast.mdx` and `feedback/sonner.mdx` follow,
+including two keys sonner's page taught that neither `SonnerSchema` declares nor its
+renderer reads (`duration`, `action`).
+
+No package source or behaviour change; fixtures, docs and pins only. Two things the
+fixture correction deliberately does NOT do, both left to the maintainer: declare an
+action union on `ButtonSchema.onClick` and build a dispatcher for it, and give the toast
+renderers the in-toast action button and promise form the removed demos implied.
diff --git a/content/docs/components/feedback/sonner.mdx b/content/docs/components/feedback/sonner.mdx
index d3867eebab..185724b609 100644
--- a/content/docs/components/feedback/sonner.mdx
+++ b/content/docs/components/feedback/sonner.mdx
@@ -9,6 +9,11 @@ The Sonner component provides beautiful toast notifications with a rich API.
## Basic Usage
+A `sonner` node renders its own trigger button. Clicking the button raises the toast the
+node declares, so the whole demo is one schema node with no handler to wire up.
+`buttonLabel` and `buttonVariant` style that trigger; `message`, `description` and
+`variant` describe the toast it raises.
+
## Toast Types
@@ -20,7 +25,9 @@ The Sonner component provides beautiful toast notifications with a rich API.
-## With Action
+## With Description
+
+`description` adds a secondary line under the message.
@@ -29,21 +36,19 @@ The Sonner component provides beautiful toast notifications with a rich API.
```plaintext
interface SonnerSchema {
type: 'sonner';
- message: string; // Toast message
+ message?: string; // Toast message
+ title?: string; // Alias for message
description?: string; // Additional description
variant?: 'default' | 'success' | 'error' | 'warning' | 'info';
- duration?: number; // Auto-close duration (ms)
- // Action
- action?: {
- label: string;
- onClick: string | ActionConfig;
- };
+ // Trigger button
+ buttonLabel?: string; // Trigger button text (default: 'Show Toast')
+ buttonVariant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link';
}
```
## Examples
-### Promise Toast
+### Custom Trigger Button
diff --git a/content/docs/components/feedback/toast.mdx b/content/docs/components/feedback/toast.mdx
index b67e1391f1..4440f2d5ed 100644
--- a/content/docs/components/feedback/toast.mdx
+++ b/content/docs/components/feedback/toast.mdx
@@ -9,19 +9,25 @@ The Toast component displays brief notifications to users that appear temporaril
## Basic Usage
+A `toast` node renders its own trigger button. Clicking the button raises the toast the
+node declares — the title, description, variant and duration are all read off the node
+itself, so the whole demo is one schema node with no handler to wire up.
+
## Variants
+`variant` selects the toast style. `ToastSchema` declares exactly five members —
+`default`, `success`, `warning`, `error` and `info`; the four below plus `success`,
+which the Success Message example uses.
+
+
+
-## With Action
-
-
-
## Schema
```plaintext
@@ -51,7 +57,3 @@ interface ToastSchema {
### Error Message
-
-### With Undo Action
-
-
diff --git a/examples/schema-catalog/src/catalog-meta.json b/examples/schema-catalog/src/catalog-meta.json
index 892662de1f..962f765a3d 100644
--- a/examples/schema-catalog/src/catalog-meta.json
+++ b/examples/schema-catalog/src/catalog-meta.json
@@ -36,6 +36,51 @@
"verification"
]
},
+ "components-feedback-sonner/promise-based-toast": {
+ "title": "Custom Trigger Button",
+ "description": "A success toast whose trigger button is styled through buttonVariant.",
+ "tags": [
+ "toast",
+ "sonner",
+ "button"
+ ]
+ },
+ "components-feedback-sonner/toast-with-action": {
+ "title": "Message With Description",
+ "description": "A sonner toast carrying both a message and a secondary description line.",
+ "tags": [
+ "toast",
+ "sonner",
+ "description"
+ ]
+ },
+ "components-feedback-toast/destructive": {
+ "title": "Error Variant",
+ "description": "The 'error' toast variant. 'destructive' is a button variant, not a toast variant.",
+ "tags": [
+ "toast",
+ "variant",
+ "error"
+ ]
+ },
+ "components-feedback-toast/toast-with-action": {
+ "title": "Info Variant",
+ "description": "The 'info' toast variant, used here for an update notice.",
+ "tags": [
+ "toast",
+ "variant",
+ "info"
+ ]
+ },
+ "components-feedback-toast/toast-with-undo": {
+ "title": "Warning Variant",
+ "description": "The 'warning' toast variant, used here for a deletion notice.",
+ "tags": [
+ "toast",
+ "variant",
+ "warning"
+ ]
+ },
"components-form-form/basic-form": {
"title": "Basic Form",
"description": "A hand-built `form` node from `@object-ui/components`: name, email, country and newsletter fields declared inline, with no object behind them. For a form generated from an object's own metadata, see the `plugin-form` examples.",
diff --git a/examples/schema-catalog/src/index.ts b/examples/schema-catalog/src/index.ts
index d3bffaee23..b62c0857c8 100644
--- a/examples/schema-catalog/src/index.ts
+++ b/examples/schema-catalog/src/index.ts
@@ -1658,9 +1658,10 @@ const REGISTRY: Record = {
'components-feedback-sonner/promise-based-toast': {
id: 'components-feedback-sonner/promise-based-toast',
meta: {
- title: "Promise Based Toast",
- description: "",
+ title: "Custom Trigger Button",
+ description: "A success toast whose trigger button is styled through buttonVariant.",
category: 'components-feedback-sonner',
+ tags: ["toast", "sonner", "button"],
},
schema: components_feedback_sonner_promise_based_toast,
},
@@ -1676,9 +1677,10 @@ const REGISTRY: Record = {
'components-feedback-sonner/toast-with-action': {
id: 'components-feedback-sonner/toast-with-action',
meta: {
- title: "Toast With Action",
- description: "",
+ title: "Message With Description",
+ description: "A sonner toast carrying both a message and a secondary description line.",
category: 'components-feedback-sonner',
+ tags: ["toast", "sonner", "description"],
},
schema: components_feedback_sonner_toast_with_action,
},
@@ -1766,9 +1768,10 @@ const REGISTRY: Record = {
'components-feedback-toast/destructive': {
id: 'components-feedback-toast/destructive',
meta: {
- title: "Destructive",
- description: "",
+ title: "Error Variant",
+ description: "The 'error' toast variant. 'destructive' is a button variant, not a toast variant.",
category: 'components-feedback-toast',
+ tags: ["toast", "variant", "error"],
},
schema: components_feedback_toast_destructive,
},
@@ -1793,18 +1796,20 @@ const REGISTRY: Record = {
'components-feedback-toast/toast-with-action': {
id: 'components-feedback-toast/toast-with-action',
meta: {
- title: "Toast With Action",
- description: "",
+ title: "Info Variant",
+ description: "The 'info' toast variant, used here for an update notice.",
category: 'components-feedback-toast',
+ tags: ["toast", "variant", "info"],
},
schema: components_feedback_toast_toast_with_action,
},
'components-feedback-toast/toast-with-undo': {
id: 'components-feedback-toast/toast-with-undo',
meta: {
- title: "Toast With Undo",
- description: "",
+ title: "Warning Variant",
+ description: "The 'warning' toast variant, used here for a deletion notice.",
category: 'components-feedback-toast',
+ tags: ["toast", "variant", "warning"],
},
schema: components_feedback_toast_toast_with_undo,
},
diff --git a/examples/schema-catalog/src/schemas/components-feedback-sonner/basic-sonner-toast.json b/examples/schema-catalog/src/schemas/components-feedback-sonner/basic-sonner-toast.json
index 910c93c0b1..10a40eea45 100644
--- a/examples/schema-catalog/src/schemas/components-feedback-sonner/basic-sonner-toast.json
+++ b/examples/schema-catalog/src/schemas/components-feedback-sonner/basic-sonner-toast.json
@@ -1,8 +1,5 @@
{
- "type": "button",
- "label": "Show Sonner Toast",
- "onClick": {
- "action": "sonner",
- "message": "Event has been created"
- }
+ "type": "sonner",
+ "message": "Event has been created",
+ "buttonLabel": "Show Sonner Toast"
}
diff --git a/examples/schema-catalog/src/schemas/components-feedback-sonner/error.json b/examples/schema-catalog/src/schemas/components-feedback-sonner/error.json
index 7ed636d87e..63a355c0a9 100644
--- a/examples/schema-catalog/src/schemas/components-feedback-sonner/error.json
+++ b/examples/schema-catalog/src/schemas/components-feedback-sonner/error.json
@@ -1,10 +1,7 @@
{
- "type": "button",
- "label": "Error",
- "variant": "destructive",
- "onClick": {
- "action": "sonner",
- "type": "error",
- "message": "Something went wrong"
- }
+ "type": "sonner",
+ "variant": "error",
+ "message": "Something went wrong",
+ "buttonLabel": "Error",
+ "buttonVariant": "destructive"
}
diff --git a/examples/schema-catalog/src/schemas/components-feedback-sonner/info.json b/examples/schema-catalog/src/schemas/components-feedback-sonner/info.json
index 1ab137baf8..c115d4aef5 100644
--- a/examples/schema-catalog/src/schemas/components-feedback-sonner/info.json
+++ b/examples/schema-catalog/src/schemas/components-feedback-sonner/info.json
@@ -1,9 +1,6 @@
{
- "type": "button",
- "label": "Info",
- "onClick": {
- "action": "sonner",
- "type": "info",
- "message": "New update available"
- }
+ "type": "sonner",
+ "variant": "info",
+ "message": "New update available",
+ "buttonLabel": "Info"
}
diff --git a/examples/schema-catalog/src/schemas/components-feedback-sonner/promise-based-toast.json b/examples/schema-catalog/src/schemas/components-feedback-sonner/promise-based-toast.json
index 9d911be845..cf3ec6b51a 100644
--- a/examples/schema-catalog/src/schemas/components-feedback-sonner/promise-based-toast.json
+++ b/examples/schema-catalog/src/schemas/components-feedback-sonner/promise-based-toast.json
@@ -1,11 +1,7 @@
{
- "type": "button",
- "label": "Save Changes",
- "onClick": {
- "action": "sonner",
- "type": "promise",
- "loading": "Saving changes...",
- "success": "Changes saved!",
- "error": "Failed to save changes"
- }
+ "type": "sonner",
+ "variant": "success",
+ "message": "Changes saved!",
+ "buttonLabel": "Save Changes",
+ "buttonVariant": "outline"
}
diff --git a/examples/schema-catalog/src/schemas/components-feedback-sonner/success.json b/examples/schema-catalog/src/schemas/components-feedback-sonner/success.json
index 556e85c813..57f55da277 100644
--- a/examples/schema-catalog/src/schemas/components-feedback-sonner/success.json
+++ b/examples/schema-catalog/src/schemas/components-feedback-sonner/success.json
@@ -1,9 +1,6 @@
{
- "type": "button",
- "label": "Success",
- "onClick": {
- "action": "sonner",
- "type": "success",
- "message": "Operation completed successfully"
- }
+ "type": "sonner",
+ "variant": "success",
+ "message": "Operation completed successfully",
+ "buttonLabel": "Success"
}
diff --git a/examples/schema-catalog/src/schemas/components-feedback-sonner/toast-with-action.json b/examples/schema-catalog/src/schemas/components-feedback-sonner/toast-with-action.json
index b550a43c02..b37e3d88f6 100644
--- a/examples/schema-catalog/src/schemas/components-feedback-sonner/toast-with-action.json
+++ b/examples/schema-catalog/src/schemas/components-feedback-sonner/toast-with-action.json
@@ -1,12 +1,7 @@
{
- "type": "button",
- "label": "Show with Action",
- "onClick": {
- "action": {
- "label": "View",
- "onClick": "viewFile"
- },
- "message": "File uploaded",
- "description": "Your file has been uploaded successfully"
- }
+ "type": "sonner",
+ "variant": "success",
+ "message": "File uploaded",
+ "description": "Your file has been uploaded successfully",
+ "buttonLabel": "Upload File"
}
diff --git a/examples/schema-catalog/src/schemas/components-feedback-sonner/warning.json b/examples/schema-catalog/src/schemas/components-feedback-sonner/warning.json
index ce5a7529a5..971133ccf3 100644
--- a/examples/schema-catalog/src/schemas/components-feedback-sonner/warning.json
+++ b/examples/schema-catalog/src/schemas/components-feedback-sonner/warning.json
@@ -1,9 +1,6 @@
{
- "type": "button",
- "label": "Warning",
- "onClick": {
- "action": "sonner",
- "type": "warning",
- "message": "Please review your input"
- }
+ "type": "sonner",
+ "variant": "warning",
+ "message": "Please review your input",
+ "buttonLabel": "Warning"
}
diff --git a/examples/schema-catalog/src/schemas/components-feedback-toast/basic-toast.json b/examples/schema-catalog/src/schemas/components-feedback-toast/basic-toast.json
index 733b8dbb05..48dd40a2ea 100644
--- a/examples/schema-catalog/src/schemas/components-feedback-toast/basic-toast.json
+++ b/examples/schema-catalog/src/schemas/components-feedback-toast/basic-toast.json
@@ -1,9 +1,5 @@
{
- "type": "button",
- "label": "Show Toast",
- "onClick": {
- "action": "toast",
- "title": "Success",
- "description": "Your changes have been saved."
- }
+ "type": "toast",
+ "title": "Success",
+ "description": "Your changes have been saved."
}
diff --git a/examples/schema-catalog/src/schemas/components-feedback-toast/default.json b/examples/schema-catalog/src/schemas/components-feedback-toast/default.json
index 80fd896e82..a2c694b8d2 100644
--- a/examples/schema-catalog/src/schemas/components-feedback-toast/default.json
+++ b/examples/schema-catalog/src/schemas/components-feedback-toast/default.json
@@ -1,9 +1,6 @@
{
- "type": "button",
- "label": "Default Toast",
- "onClick": {
- "action": "toast",
- "title": "Notification",
- "description": "This is a default toast message."
- }
+ "type": "toast",
+ "variant": "default",
+ "title": "Notification",
+ "description": "This is a default toast message."
}
diff --git a/examples/schema-catalog/src/schemas/components-feedback-toast/destructive.json b/examples/schema-catalog/src/schemas/components-feedback-toast/destructive.json
index 8a7145bec0..b4a37b1c14 100644
--- a/examples/schema-catalog/src/schemas/components-feedback-toast/destructive.json
+++ b/examples/schema-catalog/src/schemas/components-feedback-toast/destructive.json
@@ -1,11 +1,6 @@
{
- "type": "button",
- "label": "Destructive Toast",
- "variant": "destructive",
- "onClick": {
- "action": "toast",
- "variant": "error",
- "title": "Error",
- "description": "Something went wrong."
- }
+ "type": "toast",
+ "variant": "error",
+ "title": "Error",
+ "description": "Something went wrong."
}
diff --git a/examples/schema-catalog/src/schemas/components-feedback-toast/error-toast.json b/examples/schema-catalog/src/schemas/components-feedback-toast/error-toast.json
index 36ede0b2c6..433188c257 100644
--- a/examples/schema-catalog/src/schemas/components-feedback-toast/error-toast.json
+++ b/examples/schema-catalog/src/schemas/components-feedback-toast/error-toast.json
@@ -1,11 +1,6 @@
{
- "type": "button",
- "label": "Submit",
- "variant": "destructive",
- "onClick": {
- "action": "toast",
- "variant": "error",
- "title": "Submission Failed",
- "description": "Please check your input and try again."
- }
+ "type": "toast",
+ "variant": "error",
+ "title": "Submission Failed",
+ "description": "Please check your input and try again."
}
diff --git a/examples/schema-catalog/src/schemas/components-feedback-toast/success-toast.json b/examples/schema-catalog/src/schemas/components-feedback-toast/success-toast.json
index 8c9da4200e..9309c6a395 100644
--- a/examples/schema-catalog/src/schemas/components-feedback-toast/success-toast.json
+++ b/examples/schema-catalog/src/schemas/components-feedback-toast/success-toast.json
@@ -1,10 +1,7 @@
{
- "type": "button",
- "label": "Save",
- "onClick": {
- "action": "toast",
- "title": "Saved Successfully",
- "description": "Your changes have been saved.",
- "duration": 3000
- }
+ "type": "toast",
+ "variant": "success",
+ "title": "Saved Successfully",
+ "description": "Your changes have been saved.",
+ "duration": 3000
}
diff --git a/examples/schema-catalog/src/schemas/components-feedback-toast/toast-with-action.json b/examples/schema-catalog/src/schemas/components-feedback-toast/toast-with-action.json
index 7b1a18313c..c312784c33 100644
--- a/examples/schema-catalog/src/schemas/components-feedback-toast/toast-with-action.json
+++ b/examples/schema-catalog/src/schemas/components-feedback-toast/toast-with-action.json
@@ -1,10 +1,6 @@
{
- "type": "button",
- "label": "Show Toast with Action",
- "onClick": {
- "action": "toast",
- "title": "Update Available",
- "description": "A new version is available.",
- "actionLabel": "Update Now"
- }
+ "type": "toast",
+ "variant": "info",
+ "title": "Update Available",
+ "description": "A new version is available."
}
diff --git a/examples/schema-catalog/src/schemas/components-feedback-toast/toast-with-undo.json b/examples/schema-catalog/src/schemas/components-feedback-toast/toast-with-undo.json
index 81ecfd93fc..0a6df6ac3c 100644
--- a/examples/schema-catalog/src/schemas/components-feedback-toast/toast-with-undo.json
+++ b/examples/schema-catalog/src/schemas/components-feedback-toast/toast-with-undo.json
@@ -1,10 +1,6 @@
{
- "type": "button",
- "label": "Delete Item",
- "onClick": {
- "action": "toast",
- "title": "Item Deleted",
- "description": "The item has been removed.",
- "actionLabel": "Undo"
- }
+ "type": "toast",
+ "variant": "warning",
+ "title": "Item Deleted",
+ "description": "The item has been removed."
}
diff --git a/examples/schema-catalog/test/component-fixture-declared-keys.test.ts b/examples/schema-catalog/test/component-fixture-declared-keys.test.ts
index b1b2a60cf5..3aa9e7784b 100644
--- a/examples/schema-catalog/test/component-fixture-declared-keys.test.ts
+++ b/examples/schema-catalog/test/component-fixture-declared-keys.test.ts
@@ -43,15 +43,26 @@
* `direction`. The probe must be structural — asserting `.success` here
* would assert nothing at all.
*
- * ## The one key on these fixtures that is CORRECT and must stay
+ * ## SUPERSEDED by objectui#6250 — where the toast fixtures went
*
- * Both toast fixtures ALSO carry a **top-level** `"variant": "destructive"`.
- * That one is a genuine `ButtonSchema` member (`form.d.ts:30`,
- * `zod/form.zod.js:153`) — the demo's button really is destructive-styled. Only
- * the occurrence nested inside the `onClick` toast payload was invented. A
- * blanket find-and-replace over `destructive` in these two files breaks two
- * working buttons; the first `describe` block below exists to make that
- * mistake turn this file red.
+ * The two toast fixtures used to be `{ "type": "button", …, "onClick": { action:
+ * 'toast', … } }`, and the block that used to head this file asserted that the
+ * **top-level** `"variant": "destructive"` — a genuine `ButtonSchema` member —
+ * survived any correction to the nested payload.
+ *
+ * #6250 removed the envelope that fact was about. `ButtonSchema.onClick` is
+ * `z.function()`, so all fourteen `components-feedback-toast/*` and
+ * `components-feedback-sonner/*` fixtures were a RED `safeParse` on the
+ * ENVELOPE, and nothing anywhere read a handler key as an action object — the
+ * demos are now the registered `type: 'toast'` / `type: 'sonner'` nodes their
+ * own renderers execute. There is no button schema left to carry a button
+ * variant, so that block is REPLACED rather than reworded: what #6157 actually
+ * established — that the toast variant VALUE is a declared `ToastSchema`
+ * member, with `destructive` refused — is carried forward below against the
+ * top-level `variant` those nodes now declare, counter-probe and all.
+ *
+ * `toast-demo-dispatch-6250.test.tsx` carries the other half: that clicking
+ * each corrected demo raises a real toast, which no parse can see.
*
* ## What this file deliberately does not assert
*
@@ -69,7 +80,7 @@ import {
RadioGroupSchema,
ToastSchema,
} from '@object-ui/types/zod';
-import { getExample } from '../src/index.js';
+import { allExamples, getExample } from '../src/index.js';
type Json = Record;
@@ -96,45 +107,53 @@ const TOAST_FIXTURES = [
'components-feedback-toast/error-toast',
] as const;
-describe('toast fixtures: the top-level button variant is CORRECT and stays (objectui#6157 rider)', () => {
- const buttonVariants = enumOptionsOf(ButtonSchema.shape.variant);
-
- it('ButtonSchema really does declare `destructive` — the control for this whole block', () => {
- expect(buttonVariants).toContain('destructive');
+describe('toast fixtures: the registered spelling, not an action object off `onClick` (objectui#6250)', () => {
+ it.each(TOAST_FIXTURES)('%s is a `toast` node that parses green under ToastSchema', (id) => {
+ const fixture = schemaOf(id);
+ expect(fixture.type).toBe('toast');
+ expect(fixture).not.toHaveProperty('onClick');
+ const result = ToastSchema.safeParse(fixture);
+ expect(result.error?.issues ?? []).toEqual([]);
+ expect(result.success).toBe(true);
});
- it.each(TOAST_FIXTURES)('%s keeps a top-level destructive button', (id) => {
- const fixture = schemaOf(id);
- expect(fixture.type).toBe('button');
- expect(fixture.variant).toBe('destructive');
- expect(buttonVariants).toContain(fixture.variant as string);
+ it('counter-probe: the retired envelope is still RED under ButtonSchema, so the block above bites', () => {
+ const retired = {
+ type: 'button',
+ label: 'Destructive Toast',
+ variant: 'destructive',
+ onClick: {
+ action: 'toast',
+ variant: 'error',
+ title: 'Error',
+ description: 'Something went wrong.',
+ },
+ };
+ 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');
});
});
-describe('toast fixtures: the nested onClick payload uses a declared toast variant', () => {
- /**
- * The payload is `{ action: 'toast', ... }` hung off `onClick`. The shipped
- * surface declares `ButtonSchema.onClick` as a FUNCTION, so this action-object
- * idiom has no declared type of its own — `ToastSchema` is the nearest
- * governing declaration, and it is unambiguous here: the payload's other keys
- * (`title`, `description`, `duration`) are exactly ToastSchema's.
- */
- const asToast = (fixture: Json): Json => {
- const { action, ...rest } = fixture.onClick as Json;
- expect(action).toBe('toast');
- return { type: 'toast', ...rest };
- };
-
- it.each(TOAST_FIXTURES)('%s payload parses green under ToastSchema', (id) => {
- const result = ToastSchema.safeParse(asToast(schemaOf(id)));
- expect(result.error?.issues ?? []).toEqual([]);
- expect(result.success).toBe(true);
+describe('toast fixtures: the variant value is a declared member (objectui#6157, carried forward)', () => {
+ const toastVariants = enumOptionsOf(ToastSchema.shape.variant);
+
+ it('ToastSchema declares `error` and refuses `destructive` — the control for this block', () => {
+ expect(toastVariants).toContain('error');
+ expect(toastVariants).not.toContain('destructive');
+ });
+
+ it.each(TOAST_FIXTURES)('%s declares a variant ToastSchema knows', (id) => {
+ const fixture = schemaOf(id);
+ expect(fixture.variant).toBe('error');
+ expect(toastVariants).toContain(fixture.variant as string);
});
it.each(TOAST_FIXTURES)(
'%s counter-probe: the pre-#6157 value is still refused, so the assertion above bites',
(id) => {
- const payload = { ...asToast(schemaOf(id)), variant: 'destructive' };
+ const payload = { ...schemaOf(id), variant: 'destructive' };
const result = ToastSchema.safeParse(payload);
expect(result.success).toBe(false);
expect(result.error?.issues[0]?.path).toEqual(['variant']);
@@ -199,3 +218,57 @@ describe('radio-group fixtures: the layout key is the declared one', () => {
expect(RadioGroupSchema.safeParse(fixture).success).toBe(true);
});
});
+
+/**
+ * objectui#6250, generalized past the two pages that reported it.
+ *
+ * The card's shape is "an action object hung off a handler key", and a grep for
+ * one literal (`"action": "toast"`) under-counts it by construction — the sonner
+ * half spells the same shape `{ action: 'sonner', … }`, and one fixture spells
+ * it `{ action: { label, onClick } }`. So the sweep is over the SHAPE: every key
+ * that reads as a handler slot, at every depth, in every entry.
+ *
+ * What it deliberately does NOT cover: a handler key holding a STRING. That is
+ * the handler-EXPRESSION dialect — whether an expression is a supported handler
+ * form is objectui#6182's open decision, not this card's — and the corpus still
+ * has exactly one, which is what the positive control below pins. A sweep that
+ * banned handler keys outright would be answering #6182 by accident.
+ */
+describe('catalog corpus: no fixture hangs an action object off a handler key (objectui#6250)', () => {
+ type Handler = { where: string; value: unknown };
+
+ const isHandlerKey = (key: string) => /^on[A-Z]/.test(key) || key === 'events';
+
+ function collect(node: unknown, where: string, acc: Handler[] = []): Handler[] {
+ if (Array.isArray(node)) {
+ node.forEach((n, i) => collect(n, `${where}[${i}]`, acc));
+ return acc;
+ }
+ if (!node || typeof node !== 'object') return acc;
+ for (const [key, value] of Object.entries(node as Record)) {
+ if (isHandlerKey(key)) acc.push({ where: `${where}.${key}`, value });
+ collect(value, `${where}.${key}`, acc);
+ }
+ return acc;
+ }
+
+ const entries = allExamples();
+ const handlers = entries.flatMap((e) => collect(e.schema, e.id));
+
+ it('the walker descends into nested children — positive control', () => {
+ // A zero result from a walker that never descends is an untested tool, not
+ // a measurement. This hit is two levels down, inside `children`, and it is
+ // the handler-EXPRESSION case #6182 owns.
+ expect(handlers.map((h) => h.where)).toContain(
+ 'components-feedback-toaster/with-toast-trigger.children[0].onClick',
+ );
+ expect(entries.length).toBeGreaterThan(400);
+ });
+
+ it('every handler value in the corpus is a string expression, never an action object', () => {
+ const objectValued = handlers
+ .filter((h) => h.value !== null && typeof h.value === 'object')
+ .map((h) => `${h.where} = ${JSON.stringify(h.value)}`);
+ expect(objectValued).toEqual([]);
+ });
+});
diff --git a/examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx b/examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx
index 6cce6f9ff7..1d05d12765 100644
--- a/examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx
+++ b/examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx
@@ -116,7 +116,12 @@ const MEASURED_TYPES = [
* early reads CLEAN below and has earned nothing.
*/
const NODE_CENSUS: Readonly> = {
- button: { rendered: 140, noElement: 0 },
+ // 140 -> 126 with objectui#6250: the fourteen `components-feedback-toast/*`
+ // and `components-feedback-sonner/*` demos were `type: 'button'` nodes
+ // carrying an action object on `onClick`, and are now the registered
+ // `toast` / `sonner` nodes their own renderers execute. Catalog-authored,
+ // no renderer touched — the case this table's header sanctions.
+ button: { rendered: 126, noElement: 0 },
input: { rendered: 48, noElement: 0 },
checkbox: { rendered: 12, noElement: 0 },
switch: { rendered: 7, noElement: 0 },
diff --git a/examples/schema-catalog/test/toast-demo-dispatch-6250.test.tsx b/examples/schema-catalog/test/toast-demo-dispatch-6250.test.tsx
new file mode 100644
index 0000000000..18fe42532d
--- /dev/null
+++ b/examples/schema-catalog/test/toast-demo-dispatch-6250.test.tsx
@@ -0,0 +1,197 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/**
+ * objectui#6250 — the fourteen `components-feedback-toast/*` and
+ * `components-feedback-sonner/*` demos hung an action object off `onClick`:
+ *
+ * { "type": "button", "label": "Destructive Toast", "variant": "destructive",
+ * "onClick": { "action": "toast", "variant": "error", "title": "Error", … } }
+ *
+ * That shape is declared nowhere and executed nowhere, and the docs page prints
+ * `JSON.stringify(schema)` right beside the demo
+ * (`apps/site/app/components/InteractiveDemo.tsx`), so it is the literal
+ * copy-paste surface for every reader and every few-shot retriever.
+ *
+ * ## The half this file carries
+ *
+ * The DECLARED half — `ButtonSchema.onClick` is `z.function()`
+ * (`zod/form.zod.ts`), so all fourteen were a RED `safeParse` on the ENVELOPE —
+ * belongs to `component-fixture-declared-keys.test.ts`. This file carries the
+ * EXECUTED half, which no schema parse can see.
+ *
+ * `ActionRunner`'s runnable vocabulary is `script | url | modal | flow | api |
+ * form | navigation` (`builtinExecutors`, `core/src/actions/ActionRunner.ts`) —
+ * there is no `toast` and no `sonner` executor, and nothing anywhere reads a
+ * handler key as an action object. So `{ action: 'toast', … }` had no
+ * dispatcher on any tree.
+ *
+ * ⚠️ It was WORSE than inert, and that is measured below rather than asserted.
+ * `onClick` is a member of `SDUI_DOM_PASS_THROUGH_KEYS`
+ * (`core/src/utils/dom-props.ts`), so the action object was forwarded to the
+ * rendered `