diff --git a/content/docs/components/feedback/toaster.mdx b/content/docs/components/feedback/toaster.mdx
index 3c7d1d69f9..e4d0b0b298 100644
--- a/content/docs/components/feedback/toaster.mdx
+++ b/content/docs/components/feedback/toaster.mdx
@@ -3,8 +3,6 @@ title: "Toaster"
description: "Container for displaying toast notifications"
---
-import { DemoGrid } from '@/app/components/ComponentDemo';
-
The Toaster component provides a container for toast notifications. It should be placed once at the root level of your application to enable toast functionality.
## Basic Usage
@@ -13,15 +11,6 @@ The Toaster component is typically added to your app's layout and doesn't requir
-## Providers
-
-ObjectUI supports two toast providers:
-
-
-
-
-
-
## Schema
```plaintext
@@ -29,7 +18,6 @@ interface ToasterSchema {
type: 'toaster';
// Configuration
- provider?: 'default' | 'sonner'; // Toast provider (default: 'default')
position?: 'top-left' | 'top-center' | 'top-right' |
'bottom-left' | 'bottom-center' | 'bottom-right'; // Toast position
limit?: number; // Maximum number of toasts (default: 5)
@@ -57,7 +45,7 @@ interface ToasterSchema {
{ type: 'page', title: 'Dashboard', body: [...] },
// Toaster at root level
- { type: 'toaster', provider: 'sonner' }
+ { type: 'toaster' }
]
}
```
@@ -75,7 +63,6 @@ The Toaster component works in conjunction with Toast components to display noti
## Notes
- Place the Toaster component once in your app's root layout
-- The `default` provider uses the standard Shadcn/UI toast system
-- The `sonner` provider uses Sonner for more advanced toast features
+- ObjectUI ships a single toaster implementation, backed by Sonner
- Toast notifications are triggered separately using toast functions or Toast components
- The Toaster manages the positioning and lifecycle of all toasts
diff --git a/examples/schema-catalog/src/index.ts b/examples/schema-catalog/src/index.ts
index b62c0857c8..36c05203bd 100644
--- a/examples/schema-catalog/src/index.ts
+++ b/examples/schema-catalog/src/index.ts
@@ -163,9 +163,7 @@ import components_feedback_toast_success_toast from './schemas/components-feedba
import components_feedback_toast_toast_with_action from './schemas/components-feedback-toast/toast-with-action.json' with { type: 'json' };
import components_feedback_toast_toast_with_undo from './schemas/components-feedback-toast/toast-with-undo.json' with { type: 'json' };
import components_feedback_toaster_custom_position_limit from './schemas/components-feedback-toaster/custom-position-limit.json' with { type: 'json' };
-import components_feedback_toaster_default_provider from './schemas/components-feedback-toaster/default-provider.json' with { type: 'json' };
import components_feedback_toaster_default_toaster from './schemas/components-feedback-toaster/default-toaster.json' with { type: 'json' };
-import components_feedback_toaster_sonner_provider from './schemas/components-feedback-toaster/sonner-provider.json' with { type: 'json' };
import components_feedback_toaster_with_toast_trigger from './schemas/components-feedback-toaster/with-toast-trigger.json' with { type: 'json' };
import components_form_button_button_sizes from './schemas/components-form-button/button-sizes.json' with { type: 'json' };
import components_form_button_button_with_icon from './schemas/components-form-button/button-with-icon.json' with { type: 'json' };
@@ -1822,15 +1820,6 @@ const REGISTRY: Record = {
},
schema: components_feedback_toaster_custom_position_limit,
},
- 'components-feedback-toaster/default-provider': {
- id: 'components-feedback-toaster/default-provider',
- meta: {
- title: "Default Provider",
- description: "",
- category: 'components-feedback-toaster',
- },
- schema: components_feedback_toaster_default_provider,
- },
'components-feedback-toaster/default-toaster': {
id: 'components-feedback-toaster/default-toaster',
meta: {
@@ -1840,15 +1829,6 @@ const REGISTRY: Record = {
},
schema: components_feedback_toaster_default_toaster,
},
- 'components-feedback-toaster/sonner-provider': {
- id: 'components-feedback-toaster/sonner-provider',
- meta: {
- title: "Sonner Provider",
- description: "",
- category: 'components-feedback-toaster',
- },
- schema: components_feedback_toaster_sonner_provider,
- },
'components-feedback-toaster/with-toast-trigger': {
id: 'components-feedback-toaster/with-toast-trigger',
meta: {
diff --git a/examples/schema-catalog/src/schemas/components-feedback-toaster/custom-position-limit.json b/examples/schema-catalog/src/schemas/components-feedback-toaster/custom-position-limit.json
index f436290a3a..ec2650ab76 100644
--- a/examples/schema-catalog/src/schemas/components-feedback-toaster/custom-position-limit.json
+++ b/examples/schema-catalog/src/schemas/components-feedback-toaster/custom-position-limit.json
@@ -3,7 +3,6 @@
"children": [
{
"type": "toaster",
- "provider": "sonner",
"position": "top-right",
"limit": 3
}
diff --git a/examples/schema-catalog/src/schemas/components-feedback-toaster/default-provider.json b/examples/schema-catalog/src/schemas/components-feedback-toaster/default-provider.json
deleted file mode 100644
index a353c05ebd..0000000000
--- a/examples/schema-catalog/src/schemas/components-feedback-toaster/default-provider.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "toaster",
- "provider": "default"
-}
diff --git a/examples/schema-catalog/src/schemas/components-feedback-toaster/sonner-provider.json b/examples/schema-catalog/src/schemas/components-feedback-toaster/sonner-provider.json
deleted file mode 100644
index 6507d58272..0000000000
--- a/examples/schema-catalog/src/schemas/components-feedback-toaster/sonner-provider.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "toaster",
- "provider": "sonner"
-}
diff --git a/examples/schema-catalog/src/schemas/components-feedback-toaster/with-toast-trigger.json b/examples/schema-catalog/src/schemas/components-feedback-toaster/with-toast-trigger.json
index 4cdee24bb0..35f50078f6 100644
--- a/examples/schema-catalog/src/schemas/components-feedback-toaster/with-toast-trigger.json
+++ b/examples/schema-catalog/src/schemas/components-feedback-toaster/with-toast-trigger.json
@@ -8,8 +8,7 @@
"onClick": "toast(\"Hello from ObjectUI!\")"
},
{
- "type": "toaster",
- "provider": "sonner"
+ "type": "toaster"
}
]
}
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 3aa9e7784b..62106968a7 100644
--- a/examples/schema-catalog/test/component-fixture-declared-keys.test.ts
+++ b/examples/schema-catalog/test/component-fixture-declared-keys.test.ts
@@ -79,6 +79,7 @@ import {
CommandItemSchema,
RadioGroupSchema,
ToastSchema,
+ ToasterSchema,
} from '@object-ui/types/zod';
import { allExamples, getExample } from '../src/index.js';
@@ -272,3 +273,102 @@ describe('catalog corpus: no fixture hangs an action object off a handler key (o
expect(objectValued).toEqual([]);
});
});
+
+
+/**
+ * objectui#6494 — the same charter as #6250 ("docs teach only what runs"), one
+ * component over and one shape across.
+ *
+ * `provider` was authored by four toaster fixtures and taught by five sites on
+ * `components/feedback/toaster.mdx`, and `ToasterSchema` declares no such key.
+ * The renderer (`renderers/feedback/toaster.tsx`) reads exactly `position` and
+ * `limit` and mounts sonner unconditionally, so the page's "ObjectUI supports
+ * two toast providers" claim was false on the tree that shipped it: the two
+ * provider demos rendered byte-identically.
+ *
+ * ## Why this sweep is over toaster NODES and not over the two demo files
+ *
+ * The card was filed naming two fixtures. A premise re-verification on merged
+ * `main` corrected it to three — by SWAPPING one nested hit for another rather
+ * than adding it. The tree actually carried FOUR, because BOTH nested nodes had
+ * the key: `custom-position-limit.children[0]` and
+ * `with-toast-trigger.children[1]`. Two successive demo-shaped censuses each
+ * saw one of the two and reported a complete face.
+ *
+ * That is the lesson the block above already wrote down — a sweep written for
+ * one shape is blind to another in the same file — and the hit it missed sat in
+ * the very file that block pins as its own positive control. So this block is
+ * structural and depth-first over the corpus: every node whose `type` is
+ * `toaster`, wherever it sits, must carry only keys the shipped `ToasterSchema`
+ * declares. A per-file or root-only assertion would re-inherit the exact
+ * blindness that produced the undercount twice.
+ *
+ * ## Why `.success` is not the probe here, and neither is round-trip equality
+ *
+ * This is the file's class 3 (declared-elsewhere, refused by neither), and it is
+ * strictly worse than the radio-group case. `BaseSchema` is `.passthrough()`, so
+ * zod does not merely ACCEPT `provider` — it PRESERVES it. Measured on the built
+ * dist: `ToasterSchema.safeParse({ type: 'toaster', provider: 'sonner' })`
+ * returns success with `provider` still on `.data`. So the class-2 probe —
+ * round-trip equality, which is what catches the stripped `shortcut` key above —
+ * is blind here too. Only a structural key-subset assertion bites, and the
+ * counter-probe below pins both blindnesses so this block cannot be "simplified"
+ * into a parse.
+ */
+describe('catalog corpus: no toaster node carries a key ToasterSchema does not declare (objectui#6494)', () => {
+ type ToasterNode = { where: string; node: Json };
+
+ function collectToasters(node: unknown, where: string, acc: ToasterNode[] = []): ToasterNode[] {
+ if (Array.isArray(node)) {
+ node.forEach((n, i) => collectToasters(n, `${where}[${i}]`, acc));
+ return acc;
+ }
+ if (!node || typeof node !== 'object') return acc;
+ const record = node as Json;
+ if (record.type === 'toaster') acc.push({ where, node: record });
+ for (const [key, value] of Object.entries(record)) {
+ collectToasters(value, `${where}.${key}`, acc);
+ }
+ return acc;
+ }
+
+ const declaredKeys = Object.keys(
+ (ToasterSchema as unknown as { shape: Record }).shape,
+ );
+ const toasters = allExamples().flatMap((e) => collectToasters(e.schema, e.id));
+
+ it('`position` and `limit` are declared and `provider` is not — the control for this block', () => {
+ expect(declaredKeys).toContain('position');
+ expect(declaredKeys).toContain('limit');
+ expect(declaredKeys).not.toContain('provider');
+ });
+
+ it('counter-probe: passthrough ACCEPTS and PRESERVES `provider`, so no parse can be the probe', () => {
+ const authored = { type: 'toaster', provider: 'sonner' };
+ const result = ToasterSchema.safeParse(authored);
+ expect(result.success).toBe(true);
+ // ...and unlike the stripped `shortcut` key above, round-trip equality holds.
+ expect(result.data).toEqual(authored);
+ });
+
+ it('the walker reaches NESTED toaster nodes — positive control', () => {
+ // Both of these sit inside `children`, and both carried `provider` before
+ // this card. A census that only read root-level nodes would report a clean
+ // sweep of the two named demos and miss exactly these two.
+ expect(toasters.map((t) => t.where)).toEqual(
+ expect.arrayContaining([
+ 'components-feedback-toaster/custom-position-limit.children[0]',
+ 'components-feedback-toaster/with-toast-trigger.children[1]',
+ ]),
+ );
+ });
+
+ it('every toaster node in the corpus carries only declared keys', () => {
+ const undeclared = toasters.flatMap(({ where, node }) =>
+ Object.keys(node)
+ .filter((key) => !declaredKeys.includes(key))
+ .map((key) => `${where}.${key}`),
+ );
+ expect(undeclared).toEqual([]);
+ });
+});