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
17 changes: 2 additions & 15 deletions content/docs/components/feedback/toaster.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand All@@ -13,23 +11,13 @@ The Toaster component is typically added to your app's layout and doesn't requir

<SchemaExample id="components-feedback-toaster/default-toaster" />

## Providers

ObjectUI supports two toast providers:

<DemoGrid>
<SchemaExample id="components-feedback-toaster/default-provider" />
<SchemaExample id="components-feedback-toaster/sonner-provider" />
</DemoGrid>

## Schema

```plaintext
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)
Expand DownExpand Up@@ -57,7 +45,7 @@ interface ToasterSchema {
{ type: 'page', title: 'Dashboard', body: [...] },

// Toaster at root level
{ type: 'toaster', provider: 'sonner' }
{ type: 'toaster' }
]
}
```
Expand All@@ -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
20 changes: 0 additions & 20 deletions examples/schema-catalog/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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' };
Expand DownExpand Up@@ -1822,15 +1820,6 @@ const REGISTRY: Record<string, Example> = {
},
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: {
Expand All@@ -1840,15 +1829,6 @@ const REGISTRY: Record<string, Example> = {
},
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: {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,6 @@
"children": [
{
"type": "toaster",
"provider": "sonner",
"position": "top-right",
"limit": 3
}
Expand Down

This file was deleted.

This file was deleted.

Original file line numberDiff line numberDiff line change
Expand Up@@ -8,8 +8,7 @@
"onClick": "toast(\"Hello from ObjectUI!\")"
},
{
"type": "toaster",
"provider": "sonner"
"type": "toaster"
}
]
}
100 changes: 100 additions & 0 deletions examples/schema-catalog/test/component-fixture-declared-keys.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -79,6 +79,7 @@ import {
CommandItemSchema,
RadioGroupSchema,
ToastSchema,
ToasterSchema,
} from '@object-ui/types/zod';
import { allExamples, getExample } from '../src/index.js';

Expand DownExpand Up@@ -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<string, unknown> }).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([]);
});
});
Loading