@object-ui/react 16.1.0 → 17.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +790 -1
  2. package/README.md +41 -0
  3. package/dist/SchemaRenderer.js +12 -2
  4. package/dist/context/AppShellContext.js +51 -23
  5. package/dist/context/InlineEditContext.d.ts +64 -0
  6. package/dist/context/InlineEditContext.js +8 -2
  7. package/dist/context/NotificationContext.d.ts +245 -17
  8. package/dist/context/NotificationContext.js +222 -20
  9. package/dist/context/ThemeContext.js +4 -8
  10. package/dist/hooks/index.d.ts +2 -1
  11. package/dist/hooks/index.js +3 -1
  12. package/dist/hooks/useAnimation.d.ts +40 -4
  13. package/dist/hooks/useAnimation.js +41 -10
  14. package/dist/hooks/useCapabilityGate.d.ts +16 -0
  15. package/dist/hooks/useCapabilityGate.js +60 -0
  16. package/dist/hooks/useClientNotifications.d.ts +0 -6
  17. package/dist/hooks/useClientNotifications.js +7 -53
  18. package/dist/hooks/useFilterScope.d.ts +30 -0
  19. package/dist/hooks/useFilterScope.js +55 -0
  20. package/dist/hooks/useNavigationOverlay.d.ts +34 -10
  21. package/dist/hooks/useNavigationOverlay.js +35 -1
  22. package/dist/hooks/useOffline.d.ts +51 -33
  23. package/dist/hooks/useOffline.js +3 -3
  24. package/dist/hooks/usePageVariables.js +23 -11
  25. package/dist/hooks/usePerformance.d.ts +3 -3
  26. package/dist/hooks/usePerformance.js +2 -2
  27. package/dist/hooks/useRecordSearch.js +98 -4
  28. package/dist/index.d.ts +2 -1
  29. package/dist/index.js +1 -1
  30. package/dist/spec-bridge/SpecBridge.d.ts +0 -4
  31. package/dist/spec-bridge/SpecBridge.js +0 -12
  32. package/dist/spec-bridge/bridges/list-view.js +5 -11
  33. package/dist/spec-bridge/index.d.ts +0 -2
  34. package/dist/spec-bridge/index.js +0 -2
  35. package/dist/spec-input.d.ts +37 -0
  36. package/dist/spec-input.js +8 -0
  37. package/dist/utils/error-message.d.ts +31 -0
  38. package/dist/utils/error-message.js +49 -0
  39. package/package.json +8 -8
  40. package/dist/hooks/useColumnSummary.d.ts +0 -38
  41. package/dist/hooks/useColumnSummary.js +0 -54
  42. package/dist/spec-bridge/bridges/dashboard.d.ts +0 -81
  43. package/dist/spec-bridge/bridges/dashboard.js +0 -121
  44. package/dist/spec-bridge/bridges/page.d.ts +0 -58
  45. package/dist/spec-bridge/bridges/page.js +0 -101
package/README.md CHANGED
@@ -182,6 +182,47 @@ The `previewMode` object contains:
182
182
  | `expiresInSeconds` | `number` | `0` | Session duration (0 = no expiry) |
183
183
  | `bannerMessage` | `string` | — | UI banner message |
184
184
 
185
+ ### useNotifications / useNotificationsByPresentation
186
+
187
+ `NotificationProvider` implements the spec `NotificationSchema`. A notification's
188
+ `severity` picks its icon and tone; its `displayType` picks the **surface** that
189
+ renders it — and each of the five spec types has a distinct one:
190
+
191
+ | `displayType` | Presentation | Rendered by | Auto-dismiss |
192
+ | --- | --- | --- | --- |
193
+ | `toast` | transient overlay | the `onToast` delegate | yes |
194
+ | `snackbar` | bottom-anchored bar, one at a time, one action | `<NotificationSnackbar />` | yes |
195
+ | `banner` | page-width strip in the content flow | `<NotificationBanners />` | no |
196
+ | `alert` | blocking acknowledgement dialog (FIFO) | `<NotificationAlerts />` | no |
197
+ | `inline` | in place, at the raising surface | `<NotificationInline />` | no |
198
+
199
+ The surface components ship in `@object-ui/components`; mount them where they
200
+ belong (a banner is in flow, an inline notification sits next to its raiser).
201
+ `onToast` receives **only** `toast` items — it used to receive all five, which is
202
+ why every type looked like a toast.
203
+
204
+ ```tsx
205
+ const { notify } = useNotifications()
206
+
207
+ notify({ title: 'Saved', severity: 'success' }) // toast (spec default)
208
+ notify({ title: 'Viewing a draft', severity: 'warning', displayType: 'banner' })
209
+ notify({ title: 'Fix 2 fields', severity: 'error', displayType: 'inline', scope: 'contact-form' })
210
+ ```
211
+
212
+ A surface component subscribes with `useNotificationsByPresentation(type, scope?)`,
213
+ which also registers the surface — raising a `banner` with no banner surface
214
+ mounted warns in dev instead of vanishing.
215
+
216
+ `config` is the spec `NotificationConfigSchema` (`defaultPosition`,
217
+ `defaultDuration`, `maxVisible`, `stackDirection`, `pauseOnHover`); the legacy
218
+ `position` / `stacking` spellings still resolve through
219
+ `resolveNotificationConfig`. Three helpers apply it so every surface agrees:
220
+ `resolveNotificationPosition` (a declared position always wins; nothing declared
221
+ leaves the surface on its own anchor), `visibleNotificationStack` (`maxVisible` +
222
+ `stackDirection`), and the context's `pauseAutoDismiss` / `resumeAutoDismiss`
223
+ (`pauseOnHover`). See the
224
+ [notifications guide](https://objectui.org/docs/guide/notifications).
225
+
185
226
  ## API Reference
186
227
 
187
228
  See [full documentation](https://objectui.org/api/react) for detailed API reference.
@@ -136,9 +136,19 @@ export class SchemaErrorBoundary extends Component {
136
136
  return this.props.children;
137
137
  }
138
138
  }
139
+ /**
140
+ * Shared "no data source" fallback. MUST be a module constant: it feeds the
141
+ * `evaluatedSchema` memo below, and a fresh `{}` per render defeats that memo
142
+ * for every SchemaRenderer without a provider above it — re-cloning the schema
143
+ * and re-running the ExpressionEvaluator on each render, and handing a new
144
+ * schema identity to children that memoise on it. For a `kind:'react'` page
145
+ * that identity IS the compile key, so the page was silently remounted and its
146
+ * `useState` wiped (objectui#2954's latent hazard, made real by this line).
147
+ */
148
+ const NO_DATA_SOURCE = {};
139
149
  export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
140
150
  const context = useContext(SchemaRendererContext);
141
- const dataSource = context?.dataSource || {};
151
+ const dataSource = context?.dataSource || NO_DATA_SOURCE;
142
152
  // Ambient host scope (user / app / features), fed by app-shell's
143
153
  // ExpressionProvider. Threaded into `visible`/expression evaluation so
144
154
  // component predicates can gate on the signed-in user & deployment flags.
@@ -233,7 +243,7 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
233
243
  if (newSchema.visibleOn !== undefined) {
234
244
  return !evaluator.evaluateCondition(newSchema.visibleOn);
235
245
  }
236
- // @deprecated ADR-0089 → `visibleWhen` (was PageComponentSchema.visibility,
246
+ // @deprecated ADR-0089 → `visibleWhen` (was PageNodeSchema.visibility,
237
247
  // an ExpressionInput) — show-when-truthy, same semantics as `visibleOn`.
238
248
  if (newSchema.visibility !== undefined) {
239
249
  return !evaluator.evaluateCondition(newSchema.visibility);
@@ -7,30 +7,50 @@ export function useAdapter() {
7
7
  return useContext(AdapterCtx);
8
8
  }
9
9
  export const MetadataCtx = createContext(null);
10
+ /** Shared frozen empty list for the fallback below — see {@link NO_METADATA_PROVIDER}. */
11
+ const NO_ITEMS = Object.freeze([]);
12
+ /**
13
+ * The no-provider fallback, as a MODULE-LEVEL SINGLETON.
14
+ *
15
+ * Identity matters here, not just shape. `useMetadata()` used to build this
16
+ * object inline on every call, so outside a `<MetadataProvider>` every render
17
+ * produced a new `getItem` — and `useMetadataItem` lists `getItem` in its
18
+ * effect deps and calls `setState({...})` with a fresh object on every run. New
19
+ * identity → effect re-runs → new state object → re-render → new identity: an
20
+ * unbreakable render loop, synchronous enough to hang inside `render()`.
21
+ *
22
+ * So the fallback documented as the graceful path for consumers mounted outside
23
+ * a provider was the one thing that made those consumers impossible to mount —
24
+ * `record:alert` and `record:quick_actions` both call `useMetadataItem`
25
+ * unconditionally, and both spin. Found by
26
+ * `apps/console/src/__tests__/record-block-record-reach.test.tsx`
27
+ * (objectui#3149), which could not mount them at all until this was frozen.
28
+ *
29
+ * Frozen so a consumer cannot mutate the shared fallback out from under every
30
+ * other consumer of it.
31
+ */
32
+ const NO_METADATA_PROVIDER = Object.freeze({
33
+ apps: NO_ITEMS,
34
+ objects: NO_ITEMS,
35
+ dashboards: NO_ITEMS,
36
+ reports: NO_ITEMS,
37
+ pages: NO_ITEMS,
38
+ loading: false,
39
+ error: null,
40
+ refresh: async () => { },
41
+ invalidate: () => { },
42
+ ensureType: async () => [],
43
+ getItem: async () => null,
44
+ getItemsByType: () => [],
45
+ getTypeStatus: () => 'ready',
46
+ });
10
47
  export function useMetadata() {
11
48
  const ctx = useContext(MetadataCtx);
12
- if (!ctx) {
13
- // Graceful fallback: when a consumer is rendered outside a MetadataProvider
14
- // (common in unit tests that only need to assert on rendering), return an
15
- // empty no-op implementation rather than crash. Production code paths
16
- // should always wrap in <MetadataProvider>.
17
- return {
18
- apps: [],
19
- objects: [],
20
- dashboards: [],
21
- reports: [],
22
- pages: [],
23
- loading: false,
24
- error: null,
25
- refresh: async () => { },
26
- invalidate: () => { },
27
- ensureType: async () => [],
28
- getItem: async () => null,
29
- getItemsByType: () => [],
30
- getTypeStatus: () => 'ready',
31
- };
32
- }
33
- return ctx;
49
+ // Graceful fallback: a consumer rendered outside a MetadataProvider (unit
50
+ // tests that only assert on rendering, standalone previews) gets an empty
51
+ // no-op implementation rather than a crash. Production code paths should
52
+ // always wrap in <MetadataProvider>.
53
+ return ctx ?? NO_METADATA_PROVIDER;
34
54
  }
35
55
  export function useMetadataItem(type, name) {
36
56
  const { getItem } = useMetadata();
@@ -41,7 +61,15 @@ export function useMetadataItem(type, name) {
41
61
  });
42
62
  useEffect(() => {
43
63
  if (!name) {
44
- setState({ item: null, loading: false, error: null });
64
+ // Bail out when already cleared instead of installing a fresh object.
65
+ // Belt-and-braces against the loop the frozen fallback above fixes at the
66
+ // source: `getItem` is an effect dep, so ANY caller whose context value
67
+ // is rebuilt per render — a hand-rolled one in a test, which this
68
+ // interface explicitly invites — would otherwise re-run this effect,
69
+ // re-set an equal-but-new state object, and re-render forever.
70
+ setState((s) => (s.item === null && !s.loading && s.error === null
71
+ ? s
72
+ : { item: null, loading: false, error: null }));
45
73
  return;
46
74
  }
47
75
  let cancelled = false;
@@ -22,6 +22,34 @@
22
22
  * context for the draft/editing flags and drives `saving`/`error`/`reset`.
23
23
  */
24
24
  import React from 'react';
25
+ /**
26
+ * Server-computed decision aggregation of the record's pending approval node
27
+ * (framework#3266, `decision_progress` on `GET /approvals/requests/:id`).
28
+ *
29
+ * A multi-approver node does NOT finalize on the first decision: `unanimous`
30
+ * needs everyone, `quorum` needs `minApprovals` of the slate, `per_group` (会签)
31
+ * needs each named group to sign. Without the tally, an approver looking at the
32
+ * record cannot tell whether their own click completes the step or is one of
33
+ * three — the fact the whole node is about (objectstack#4478). The server does
34
+ * the counting so every client renders the same "2 of 3" the engine enforces.
35
+ *
36
+ * Absent for `first_response` nodes (one decision finalizes, so there is no
37
+ * progress to show) and for a backend that predates the enrichment.
38
+ */
39
+ export interface ApprovalProgress {
40
+ behavior: 'unanimous' | 'quorum' | 'per_group';
41
+ /** Approvals recorded so far — satisfied GROUPS when `behavior` is `per_group`. */
42
+ got: number;
43
+ /** Approvals required to finalize — total GROUPS when `behavior` is `per_group`. */
44
+ need: number;
45
+ /** Per-group tally, `per_group` only. */
46
+ groups?: Array<{
47
+ group: string;
48
+ got: number;
49
+ need: number;
50
+ satisfied: boolean;
51
+ }>;
52
+ }
25
53
  export interface InlineEditContextValue {
26
54
  /** True while the record is in inline-edit mode. */
27
55
  editing: boolean;
@@ -43,6 +71,30 @@ export interface InlineEditContextValue {
43
71
  * renderer DataSource-agnostic. Defaults to `false`.
44
72
  */
45
73
  locked: boolean;
74
+ /**
75
+ * Whether an approval is in flight on this record, INDEPENDENT of whether it
76
+ * locks the record (objectui#2902). An approval node may declare
77
+ * `lockRecord: false`, in which case the backend accepts edits while the node
78
+ * waits — `approvalPending` is true and `locked` is false.
79
+ *
80
+ * Kept separate because the two drive different affordances: the recall
81
+ * button and the "an approval is running" status belong to `approvalPending`,
82
+ * while only `locked` may suppress editing. Consumers that conflate them
83
+ * either hide recall on an editable record or claim a lock that isn't there.
84
+ *
85
+ * `locked` implies an approval is pending, so hosts that thread only `locked`
86
+ * still get a coherent band. Defaults to `false`.
87
+ */
88
+ approvalPending: boolean;
89
+ /**
90
+ * Quorum / per-group tally of the pending approval node, when it aggregates
91
+ * more than one decision (objectstack#4478). Threaded verbatim from the
92
+ * host's approvals read so the band renders the server's count instead of
93
+ * re-deriving the engine's tally rules. Undefined when no approval is
94
+ * running, when the node finalizes on the first response, or when the host
95
+ * doesn't resolve approvals at all.
96
+ */
97
+ approvalProgress?: ApprovalProgress;
46
98
  /**
47
99
  * Human-readable reason for the approval lock, surfaced as the band's
48
100
  * tooltip. Optional — consumers fall back to their own localized default
@@ -88,6 +140,18 @@ export interface InlineEditProviderProps {
88
140
  * from a record field the backend may not materialize. Defaults to `false`.
89
141
  */
90
142
  locked?: boolean;
143
+ /**
144
+ * Whether an approval is pending on the record, whether or not it locks it
145
+ * (objectui#2902). Omitted ⇒ falls back to `locked`, which keeps hosts that
146
+ * only know about the lock rendering exactly as before.
147
+ */
148
+ approvalPending?: boolean;
149
+ /**
150
+ * The pending node's server-computed decision tally (objectstack#4478).
151
+ * Surfaced verbatim so the band can show "2 of 3" / per-group ticks. Omitted
152
+ * for `first_response` nodes and for hosts that don't read approvals.
153
+ */
154
+ approvalProgress?: ApprovalProgress;
91
155
  /** Optional human-readable lock reason, surfaced as the band tooltip. */
92
156
  lockedReason?: string;
93
157
  children: React.ReactNode;
@@ -24,7 +24,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
24
24
  */
25
25
  import React from 'react';
26
26
  const InlineEditContext = React.createContext(null);
27
- export const InlineEditProvider = ({ canEdit = true, locked = false, lockedReason, children, }) => {
27
+ export const InlineEditProvider = ({ canEdit = true, locked = false, approvalPending, approvalProgress, lockedReason, children, }) => {
28
+ // A locked record always has an approval running; the converse is what
29
+ // #2902 added. Defaulting this way means an un-migrated host keeps its
30
+ // previous behavior with no code change.
31
+ const pending = approvalPending ?? locked;
28
32
  const [editing, setEditing] = React.useState(false);
29
33
  const [draft, setDraft] = React.useState({});
30
34
  const [autoFocusField, setAutoFocusField] = React.useState(null);
@@ -57,6 +61,8 @@ export const InlineEditProvider = ({ canEdit = true, locked = false, lockedReaso
57
61
  editing,
58
62
  canEdit,
59
63
  locked,
64
+ approvalPending: pending,
65
+ approvalProgress,
60
66
  lockedReason,
61
67
  draft,
62
68
  autoFocusField,
@@ -68,7 +74,7 @@ export const InlineEditProvider = ({ canEdit = true, locked = false, lockedReaso
68
74
  reset: teardown,
69
75
  setSaving,
70
76
  setError,
71
- }), [editing, canEdit, locked, lockedReason, draft, autoFocusField, saving, error, enter, setField, teardown]);
77
+ }), [editing, canEdit, locked, pending, approvalProgress, lockedReason, draft, autoFocusField, saving, error, enter, setField, teardown]);
72
78
  return _jsx(InlineEditContext.Provider, { value: value, children: children });
73
79
  };
74
80
  /**
@@ -15,15 +15,100 @@
15
15
  import React from 'react';
16
16
  /** Notification severity levels aligned with NotificationSeveritySchema */
17
17
  export type NotificationSeverityLevel = 'info' | 'success' | 'warning' | 'error';
18
- /** Notification display type aligned with NotificationTypeSchema */
19
- export type NotificationDisplayType = 'toast' | 'banner' | 'snackbar' | 'modal';
20
- /** Notification position aligned with NotificationPositionSchema */
21
- export type NotificationPositionValue = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
18
+ /**
19
+ * Notification presentation — the spec `NotificationTypeSchema`
20
+ * (`ui/notification.zod.ts`: toast / snackbar / banner / alert / inline).
21
+ * Every member has a DISTINCT presentation; see {@link NOTIFICATION_PRESENTATIONS}.
22
+ */
23
+ export type NotificationPresentation = 'toast' | 'snackbar' | 'banner' | 'alert' | 'inline';
24
+ /**
25
+ * Notification display type as AUTHORED. Identical to
26
+ * {@link NotificationPresentation} plus the deprecated renderer-local `modal`
27
+ * spelling, which stored items may still carry (#2942) and which resolves to
28
+ * `alert`. Use {@link resolveNotificationPresentation} to get the presentation.
29
+ */
30
+ export type NotificationDisplayType = NotificationPresentation
31
+ /** @deprecated renderer dialect — never in the spec; presented as `alert` */
32
+ | 'modal';
33
+ /**
34
+ * Where a presentation is rendered.
35
+ *
36
+ * - `delegate` — handed to the provider's `onToast` callback; the host owns the
37
+ * overlay (sonner in the console). Historically EVERY type took this path,
38
+ * which is why a `banner` surfaced as a toast (#3014).
39
+ * - everything else names a surface component that subscribes to the context
40
+ * and renders the items in place. `@object-ui/components` ships one per
41
+ * surface (`NotificationSnackbar` / `NotificationBanners` /
42
+ * `NotificationAlerts` / `NotificationInline`).
43
+ */
44
+ export type NotificationSurface = 'delegate' | 'snackbar' | 'banner' | 'alert' | 'inline';
45
+ export interface NotificationPresentationBehavior {
46
+ /** The surface responsible for rendering this presentation. */
47
+ surface: NotificationSurface;
48
+ /**
49
+ * Whether the presentation auto-dismisses when the raiser declares no
50
+ * `duration`. A toast/snackbar is transient by definition; a banner is a
51
+ * persistent strip and an alert is a blocking acknowledgement, so neither may
52
+ * evaporate on the shared 5s timer. An explicit `duration` always wins.
53
+ */
54
+ transient: boolean;
55
+ }
56
+ /**
57
+ * The presentation table — one entry per spec `NotificationTypeSchema` member.
58
+ *
59
+ * Typed as `Record<NotificationPresentation, …>`, so adding a member to the
60
+ * spec enum fails type-check here until its presentation is decided, rather
61
+ * than silently falling back to a toast.
62
+ */
63
+ export declare const NOTIFICATION_PRESENTATIONS: Record<NotificationPresentation, NotificationPresentationBehavior>;
64
+ /**
65
+ * Resolve an authored `displayType` to the presentation that renders it:
66
+ * the spec default (`toast`) when absent, and `alert` for the deprecated
67
+ * `modal` dialect.
68
+ */
69
+ export declare function resolveNotificationPresentation(displayType?: NotificationDisplayType): NotificationPresentation;
70
+ /**
71
+ * Notification position — the spec `NotificationPositionSchema`
72
+ * (underscore spellings). The hyphen forms are this context's historical
73
+ * dialect, kept accepted for stored items.
74
+ */
75
+ export type NotificationPositionValue = 'top_left' | 'top_center' | 'top_right' | 'bottom_left' | 'bottom_center' | 'bottom_right'
76
+ /** @deprecated legacy spelling — use `top_left` */
77
+ | 'top-left'
78
+ /** @deprecated legacy spelling — use `top_center` */
79
+ | 'top-center'
80
+ /** @deprecated legacy spelling — use `top_right` */
81
+ | 'top-right'
82
+ /** @deprecated legacy spelling — use `bottom_left` */
83
+ | 'bottom-left'
84
+ /** @deprecated legacy spelling — use `bottom_center` */
85
+ | 'bottom-center'
86
+ /** @deprecated legacy spelling — use `bottom_right` */
87
+ | 'bottom-right';
88
+ /**
89
+ * The spec vocabularies this context implements — exported for the parity
90
+ * tests (#2942), which fail the moment `NotificationTypeSchema` /
91
+ * `NotificationPositionSchema` and these sets drift in either direction.
92
+ */
93
+ export declare const SUPPORTED_NOTIFICATION_DISPLAY_TYPES: ReadonlySet<string>;
94
+ export declare const SUPPORTED_NOTIFICATION_POSITIONS: ReadonlySet<string>;
95
+ /**
96
+ * Action button variant — the spec `NotificationActionSchema.variant`
97
+ * (`primary | secondary | link`, default `primary`).
98
+ *
99
+ * This used to be the shadcn Button vocabulary (`default | destructive |
100
+ * outline`) — a renderer-local fork of a spec enum, i.e. a second de-facto
101
+ * contract. Surfaces map these three onto their own button styling instead.
102
+ */
103
+ export type NotificationActionVariant = 'primary' | 'secondary' | 'link';
104
+ /** The spec vocabulary this context implements — exported for the parity test. */
105
+ export declare const SUPPORTED_NOTIFICATION_ACTION_VARIANTS: ReadonlySet<string>;
22
106
  /** Action button on a notification */
23
107
  export interface NotificationActionButton {
24
108
  label: string;
25
109
  onClick: () => void;
26
- variant?: 'default' | 'destructive' | 'outline';
110
+ /** Spec `NotificationActionSchema.variant`. Default: `primary`. */
111
+ variant?: NotificationActionVariant;
27
112
  }
28
113
  /** A single notification item */
29
114
  export interface NotificationItem {
@@ -31,10 +116,40 @@ export interface NotificationItem {
31
116
  title: string;
32
117
  message?: string;
33
118
  severity: NotificationSeverityLevel;
119
+ /**
120
+ * The presentation the raiser asked for. Materialized by `notify()` — stored
121
+ * items always carry a resolved {@link NotificationPresentation}, never
122
+ * `modal` and never `undefined`.
123
+ */
34
124
  displayType?: NotificationDisplayType;
35
125
  actions?: NotificationActionButton[];
36
- /** Duration in ms (0 = persistent). Default: 5000 */
126
+ /**
127
+ * Duration in ms (0 = persistent). Defaults to the configured duration for
128
+ * TRANSIENT presentations (toast / snackbar) and to persistent for the rest —
129
+ * a banner that evaporates after 5s is not a banner.
130
+ */
37
131
  duration?: number;
132
+ /**
133
+ * Whether the surface offers a dismiss control (spec `dismissible`, default
134
+ * `true`). Only meaningful for the persistent presentations — a transient
135
+ * one dismisses itself.
136
+ */
137
+ dismissible?: boolean;
138
+ /**
139
+ * `inline` only — names the surface that raised it, so the matching
140
+ * `<NotificationInline scope="…" />` renders it in place instead of every
141
+ * inline outlet on the page showing every inline notification. Renderer-local
142
+ * routing metadata, not a spec field: the spec describes what a notification
143
+ * IS, not which React subtree hosts it.
144
+ */
145
+ scope?: string;
146
+ /**
147
+ * Spec `NotificationSchema.position` — "Override default position", falling
148
+ * back to `config.defaultPosition`. Honored by the FLOATING presentations
149
+ * (toast, snackbar); `banner` / `inline` / `alert` are anchored by what they
150
+ * are (content top / in place / centered modal) and ignore it.
151
+ */
152
+ position?: NotificationPositionValue;
38
153
  /** Whether the notification has been read */
39
154
  read?: boolean;
40
155
  /** Timestamp */
@@ -42,23 +157,75 @@ export interface NotificationItem {
42
157
  /** Optional icon name (lucide icon) */
43
158
  icon?: string;
44
159
  }
45
- /** Configuration for the notification system */
160
+ /**
161
+ * Configuration for the notification system — the spec
162
+ * `NotificationConfigSchema` (`defaultPosition` / `defaultDuration` /
163
+ * `maxVisible` / `stackDirection` / `pauseOnHover`).
164
+ *
165
+ * The field names used to fork from it (`position`, and a renderer-local
166
+ * `stacking` boolean with no spec counterpart), and only `defaultDuration` was
167
+ * ever read — the other three were stored and ignored. The legacy names are
168
+ * still accepted; see {@link resolveNotificationConfig}.
169
+ */
46
170
  export interface NotificationSystemConfig {
47
- /** Default position for toast notifications */
48
- position?: NotificationPositionValue;
49
- /** Default duration in ms */
171
+ /**
172
+ * Default screen position for the FLOATING presentations (toast, snackbar).
173
+ * A notification's own `position` overrides it.
174
+ *
175
+ * Deliberately undefined by default: "the host didn't say" has to be
176
+ * representable, otherwise a fabricated default silently competes with the
177
+ * host's own toast chrome (which also serves non-notification `toast.*`
178
+ * calls). Declared wins; undeclared defers to the surface's own anchor.
179
+ */
180
+ defaultPosition?: NotificationPositionValue;
181
+ /** Default duration in ms for the transient presentations. */
50
182
  defaultDuration?: number;
51
- /** Maximum number of visible notifications */
183
+ /** Maximum number of notifications a stacking surface renders at once. */
52
184
  maxVisible?: number;
53
- /** Whether to stack notifications */
185
+ /** Which way a stack grows: `down` puts the newest below (spec default). */
186
+ stackDirection?: 'up' | 'down';
187
+ /** Pause a transient notification's auto-dismiss timer while hovered. */
188
+ pauseOnHover?: boolean;
189
+ /** @deprecated renderer dialect — use `defaultPosition` */
190
+ position?: NotificationPositionValue;
191
+ /**
192
+ * @deprecated renderer dialect with no spec counterpart. Read as "show only
193
+ * the newest" (`maxVisible: 1`) when false, so it is honored rather than
194
+ * fossilized; declare `maxVisible` instead.
195
+ */
54
196
  stacking?: boolean;
55
197
  }
198
+ /** Config with the legacy spellings folded in and the spec defaults applied. */
199
+ export interface ResolvedNotificationConfig {
200
+ defaultPosition?: NotificationPositionValue;
201
+ defaultDuration: number;
202
+ maxVisible: number;
203
+ stackDirection: 'up' | 'down';
204
+ pauseOnHover: boolean;
205
+ }
206
+ /**
207
+ * Fold the deprecated spellings into the spec ones and apply the spec defaults
208
+ * (`NotificationConfigSchema`: 5000ms / 5 visible / stack down / pause on
209
+ * hover). `defaultPosition` gets no fabricated default — see the field docs.
210
+ */
211
+ export declare function resolveNotificationConfig(config?: NotificationSystemConfig): ResolvedNotificationConfig;
56
212
  export interface NotificationProviderProps {
57
213
  children: React.ReactNode;
58
214
  /** System configuration */
59
215
  config?: NotificationSystemConfig;
60
- /** External toast handler (e.g., Sonner) for rendering toasts */
61
- onToast?: (notification: NotificationItem) => void;
216
+ /**
217
+ * External toast handler (e.g. Sonner) — the `toast` presentation's surface.
218
+ *
219
+ * Called ONLY for `displayType: 'toast'`. It used to receive every
220
+ * notification, which is why the other four spec types all surfaced as
221
+ * toasts (#3014); they now render through their own surface components,
222
+ * which subscribe via {@link useNotificationsByPresentation}.
223
+ *
224
+ * The resolved config comes with it so the delegate can honor the parts of
225
+ * the contract only it can apply — `defaultPosition` above all, since the
226
+ * toast overlay is the host's.
227
+ */
228
+ onToast?: (notification: NotificationItem, config: ResolvedNotificationConfig) => void;
62
229
  }
63
230
  interface NotificationContextValue {
64
231
  /** All notifications (including history) */
@@ -83,19 +250,44 @@ interface NotificationContextValue {
83
250
  dismiss: (id: string) => void;
84
251
  /** Clear all notifications */
85
252
  clearAll: () => void;
86
- /** System configuration */
87
- config: NotificationSystemConfig;
253
+ /** System configuration, spec-normalized (see {@link resolveNotificationConfig}). */
254
+ config: ResolvedNotificationConfig;
255
+ /**
256
+ * Hold a transient notification's auto-dismiss timer (spec `pauseOnHover`).
257
+ * A surface calls this on pointer-enter; no-op when the notification is
258
+ * persistent or the config opted out.
259
+ */
260
+ pauseAutoDismiss: (id: string) => void;
261
+ /** Resume a held timer with the time it had left. */
262
+ resumeAutoDismiss: (id: string) => void;
263
+ /**
264
+ * Announce that a surface is mounted and will render `surface` items.
265
+ * Returns the unregister callback. Surfaces call this from an effect; the
266
+ * provider uses it only to warn (dev builds) when a notification is raised
267
+ * with nothing on screen able to present it.
268
+ */
269
+ registerSurface: (surface: NotificationSurface) => () => void;
88
270
  }
89
271
  /**
90
272
  * NotificationProvider — Provides a spec-driven notification system.
91
273
  *
274
+ * Each spec display type is presented by its own surface. `toast` is delegated
275
+ * to the host (`onToast`); the other four are rendered by surface components
276
+ * that subscribe to this context — mount them where they belong:
277
+ * `NotificationSnackbar` / `NotificationAlerts` anywhere inside the provider,
278
+ * `NotificationBanners` at the top of the content area, and
279
+ * `NotificationInline` in the surface that raises the notification.
280
+ *
92
281
  * @example
93
282
  * ```tsx
94
283
  * <NotificationProvider
95
- * config={{ position: 'top-right', defaultDuration: 5000, maxVisible: 5 }}
284
+ * config={{ position: 'top_right', defaultDuration: 5000, maxVisible: 5 }}
96
285
  * onToast={(n) => toast[n.severity](n.title, { description: n.message })}
97
286
  * >
287
+ * <NotificationBanners />
98
288
  * <App />
289
+ * <NotificationSnackbar />
290
+ * <NotificationAlerts />
99
291
  * </NotificationProvider>
100
292
  * ```
101
293
  */
@@ -110,4 +302,40 @@ export declare function useNotifications(): NotificationContextValue;
110
302
  * Hook to check if a NotificationProvider is available.
111
303
  */
112
304
  export declare function useHasNotificationProvider(): boolean;
305
+ /**
306
+ * Apply the config's stack rules to a surface's items: keep the newest
307
+ * `maxVisible`, then order them so the stack GROWS in `stackDirection`
308
+ * (`down`, the spec default, puts the newest below the older ones).
309
+ *
310
+ * Shared by the stacking surfaces (banner, inline) so `maxVisible` means one
311
+ * thing. It used to mean nothing at all — the config carried it and no surface
312
+ * read it, while `NotificationBanners` capped at a hard-coded 3 of its own.
313
+ *
314
+ * @param items the surface's notifications, newest-first
315
+ */
316
+ export declare function visibleNotificationStack(items: NotificationItem[], config: Pick<ResolvedNotificationConfig, 'maxVisible' | 'stackDirection'>): NotificationItem[];
317
+ /**
318
+ * The screen position a FLOATING notification (toast, snackbar) should take:
319
+ * its own `position`, else the configured `defaultPosition`, else `undefined`.
320
+ *
321
+ * `undefined` is a meaningful answer, not a missing one — it means "nothing was
322
+ * declared", and the surface should keep its own anchor (a snackbar's bottom
323
+ * edge) or defer to the host's toast chrome, rather than being moved by a
324
+ * default nobody asked for. Always returns the spec (underscore) spelling.
325
+ */
326
+ export declare function resolveNotificationPosition(notification: Pick<NotificationItem, 'position'>, config?: Pick<ResolvedNotificationConfig, 'defaultPosition'>): NotificationPositionValue | undefined;
327
+ /**
328
+ * Subscribe to the live notifications for ONE presentation — the contract a
329
+ * surface component implements (`@object-ui/components` ships one per spec
330
+ * type). Registering also tells the provider the surface exists, so raising a
331
+ * `banner` with no banner surface mounted warns in dev instead of vanishing.
332
+ *
333
+ * Items come back newest-first, matching `notifications`. A surface that wants
334
+ * FIFO order (an `alert` queue) reverses them itself.
335
+ *
336
+ * @param presentation the spec display type this surface renders
337
+ * @param scope optional `inline` routing key — when set, only items raised with
338
+ * the same `scope` are returned; when omitted, only unscoped items are.
339
+ */
340
+ export declare function useNotificationsByPresentation(presentation: NotificationPresentation, scope?: string): NotificationItem[];
113
341
  export {};