|
| 1 | +--- |
| 2 | +title: Notifications |
| 3 | +description: The five spec display types and the surface that renders each |
| 4 | +--- |
| 5 | + |
| 6 | +# Notifications |
| 7 | + |
| 8 | +ObjectUI's notification system implements the spec `NotificationSchema` |
| 9 | +(`@objectstack/spec` → `ui/notification.zod.ts`). A notification carries two |
| 10 | +independent axes: |
| 11 | + |
| 12 | +- **`severity`** — `info` / `success` / `warning` / `error`. Picks the icon and tone. |
| 13 | +- **`displayType`** — `toast` / `snackbar` / `banner` / `alert` / `inline`. Picks the |
| 14 | + **surface**: where and how it appears. |
| 15 | + |
| 16 | +`displayType` used to be stored and never read, so every type surfaced as a |
| 17 | +toast — an author asking for a `banner` got a transient overlay. Each type now |
| 18 | +has a presentation of its own. |
| 19 | + |
| 20 | +## The five presentations |
| 21 | + |
| 22 | +| `displayType` | Presentation | Rendered by | Persists | |
| 23 | +|---|---|---|---| |
| 24 | +| `toast` | Transient overlay | the host's `onToast` delegate (sonner in the console) | no — auto-dismiss | |
| 25 | +| `snackbar` | Bottom-anchored bar, one at a time, at most one action | `<NotificationSnackbar />` | no — auto-dismiss | |
| 26 | +| `banner` | Page-width strip **in the content flow** | `<NotificationBanners />` | yes — until dismissed | |
| 27 | +| `alert` | Blocking acknowledgement dialog, FIFO queue | `<NotificationAlerts />` | yes — until acknowledged | |
| 28 | +| `inline` | In place, at the surface that raised it | `<NotificationInline />` | yes — until dismissed | |
| 29 | + |
| 30 | +Auto-dismiss follows the presentation, not a single global timer: `toast` and |
| 31 | +`snackbar` are transient (`config.defaultDuration`, 5s by default), the other |
| 32 | +three stay until dismissed. An explicit `duration` always wins — including |
| 33 | +`duration: 0`, which makes a toast persistent. |
| 34 | + |
| 35 | +## Mounting the surfaces |
| 36 | + |
| 37 | +`toast` is delegated to the host; the other four are React components that |
| 38 | +subscribe to the provider. Placement is deliberately yours: a banner needs a |
| 39 | +slot in the content area and an inline notification belongs next to the thing |
| 40 | +that raised it, so neither can be positioned by a global overlay. |
| 41 | + |
| 42 | +```tsx |
| 43 | +import { |
| 44 | + NotificationAlerts, |
| 45 | + NotificationBanners, |
| 46 | + NotificationInline, |
| 47 | + NotificationSnackbar, |
| 48 | +} from '@object-ui/components'; |
| 49 | +import { NotificationProvider } from '@object-ui/react'; |
| 50 | +import { toast } from 'sonner'; |
| 51 | + |
| 52 | +<NotificationProvider |
| 53 | + config={{ position: 'top_right', defaultDuration: 5000, maxVisible: 5 }} |
| 54 | + onToast={(n) => toast[n.severity](n.title, { description: n.message })} |
| 55 | +> |
| 56 | + <main> |
| 57 | + <NotificationBanners /> {/* top of the content area */} |
| 58 | + <Outlet /> |
| 59 | + </main> |
| 60 | + <NotificationSnackbar /> |
| 61 | + <NotificationAlerts /> |
| 62 | +</NotificationProvider> |
| 63 | +``` |
| 64 | + |
| 65 | +A provider with **no** `onToast` is a supported "notification centre" mode: items |
| 66 | +are collected in `notifications` / `unreadCount` for a bell or list, and nothing |
| 67 | +is overlaid. Raising one of the other four types with its surface unmounted is a |
| 68 | +mistake, though — dev builds warn, naming the component to mount. |
| 69 | + |
| 70 | +## Raising notifications |
| 71 | + |
| 72 | +```tsx |
| 73 | +const { notify } = useNotifications(); |
| 74 | + |
| 75 | +notify({ title: 'Saved', severity: 'success' }); // toast (spec default) |
| 76 | +notify({ title: 'Row deleted', severity: 'info', displayType: 'snackbar', |
| 77 | + actions: [{ label: 'Undo', onClick: undo }] }); // one action |
| 78 | +notify({ title: 'Viewing a draft', severity: 'warning', displayType: 'banner' }); |
| 79 | +notify({ title: 'Session expired', severity: 'error', displayType: 'alert' }); |
| 80 | +``` |
| 81 | + |
| 82 | +### `inline` and `scope` |
| 83 | + |
| 84 | +An inline notification is rendered by the surface that raised it. `scope` is the |
| 85 | +routing key that pairs the two, so two forms on one page don't show each other's |
| 86 | +messages: |
| 87 | + |
| 88 | +```tsx |
| 89 | +notify({ |
| 90 | + title: 'Fix 2 fields', severity: 'error', |
| 91 | + displayType: 'inline', scope: 'contact-form', |
| 92 | +}); |
| 93 | + |
| 94 | +<NotificationInline scope="contact-form" className="mb-4" /> |
| 95 | +``` |
| 96 | + |
| 97 | +Omit `scope` on both ends for a page-level inline outlet. `scope` is renderer-local |
| 98 | +routing metadata, not a spec field — the spec describes what a notification *is*, |
| 99 | +not which React subtree hosts it. |
| 100 | + |
| 101 | +### `dismissible` |
| 102 | + |
| 103 | +Defaults to `true`. On the persistent presentations, `dismissible: false` removes |
| 104 | +the dismiss control (a banner you must resolve rather than wave away). An `alert` |
| 105 | +always keeps its acknowledge button — `dismissible: false` only closes the Escape |
| 106 | +route, never the way out. |
| 107 | + |
| 108 | +## Notes |
| 109 | + |
| 110 | +- **`alert` is not the action system's modal.** `ModalHandler` resolves a page or |
| 111 | + object, renders it, and reports an `ActionResult` back to the `ActionRunner`. A |
| 112 | + notification alert has no schema, no target and no result — it is a message and |
| 113 | + an acknowledgement, so it renders through the `AlertDialog` primitive instead. |
| 114 | +- **`snackbar` is not a toast variant.** It supersedes rather than stacks, anchors |
| 115 | + to the bottom regardless of the toast position config, and carries at most one |
| 116 | + action. |
| 117 | +- Adding a member to the spec `NotificationTypeSchema` fails type-check in |
| 118 | + `NOTIFICATION_PRESENTATIONS` (`@object-ui/react`) until its presentation is |
| 119 | + decided — new types cannot silently fall back to a toast. |
| 120 | + |
| 121 | +## Server-side notifications |
| 122 | + |
| 123 | +`useClientNotifications` bridges the `@objectstack/client` notifications API into |
| 124 | +the same provider (ADR-0030). Fetched items are persistent and default to `toast`, |
| 125 | +so a host that renders a bell from `notifications` needs no surface at all. |
0 commit comments