diff --git a/.changeset/console-mounts-notification-surfaces.md b/.changeset/console-mounts-notification-surfaces.md
new file mode 100644
index 0000000000..e4d85468a6
--- /dev/null
+++ b/.changeset/console-mounts-notification-surfaces.md
@@ -0,0 +1,43 @@
+---
+"@object-ui/app-shell": minor
+---
+
+feat(app-shell): the console mounts the notification surfaces, so `displayType` works there (#3014 follow-up)
+
+#3071 gave each spec `NotificationTypeSchema` member its own presentation, but no
+host mounted `NotificationProvider` — the capability existed and the console
+could not reach it. `ConsoleShell` now mounts the provider and the surfaces with
+a single global home; `ConsoleLayout` mounts the one that belongs in the content
+area:
+
+| `displayType` | Surface | Mounted by |
+|---|---|---|
+| `toast` | sonner, via the new `presentNotificationToast` | `ConsoleShell` |
+| `snackbar` | `` | `ConsoleShell` |
+| `alert` | `` | `ConsoleShell` |
+| `banner` | `` | `ConsoleLayout`, beside the draft / unpublished bars |
+| `inline` | `` | the raising surface — **not** mounted globally |
+
+`inline` is left out deliberately: rendering in place at the raiser is the whole
+difference between it and a banner, so a global inline outlet would collapse the
+two again.
+
+`presentNotificationToast` is the single place a notification becomes a sonner
+call — severity → variant, `duration: 0` → `Infinity` (the contract's
+"persistent", which passed through raw would have made the toast vanish on the
+next tick), first action → the one action slot sonner offers, an absent duration
+left to the `ConsoleToaster` default rather than reinvented. Its severity table
+is `Record`, so a new spec severity fails
+type-check instead of silently rendering neutral.
+
+The banners go through `ConsoleNotificationBanners`, which gates on
+`useHasNotificationProvider()`. `ConsoleShell` is deliberately a set of
+composable pieces a host assembles in its own `App.tsx`, so `ConsoleLayout` can
+legitimately render without the provider above it — and `useNotifications()`
+throws there, which would white-screen the whole app instead of simply showing
+no banners.
+
+Both pieces are exported (`presentNotificationToast`, `ConsoleNotificationBanners`)
+for hand-assembled shells. The provider's `defaultDuration` matches
+`ConsoleToaster`'s 4s, so a snackbar and a toast raised together disappear
+together.
diff --git a/content/docs/guide/notifications.md b/content/docs/guide/notifications.md
index dbfec1a648..c96d7d66a9 100644
--- a/content/docs/guide/notifications.md
+++ b/content/docs/guide/notifications.md
@@ -67,6 +67,23 @@ are collected in `notifications` / `unreadCount` for a bell or list, and nothing
is overlaid. Raising one of the other four types with its surface unmounted is a
mistake, though — dev builds warn, naming the component to mount.
+### In the console
+
+`@object-ui/app-shell` already does this wiring — a console route can call
+`useNotifications()` and every display type presents correctly with no setup:
+
+| Surface | Mounted by |
+|---|---|
+| `toast` | `ConsoleShell`, via `presentNotificationToast` → sonner (`ConsoleToaster`) |
+| `snackbar`, `alert` | `ConsoleShell` — both have a single global home |
+| `banner` | `ConsoleLayout`, at the top of the content area, beside the draft / unpublished bars |
+| `inline` | nothing, by contract — the raising surface mounts its own `` |
+
+Assembling a shell by hand? Both pieces are exported: `presentNotificationToast`
+for the `onToast` delegate, and `ConsoleNotificationBanners` — `NotificationBanners`
+guarded by `useHasNotificationProvider()`, so a layout rendered without the
+provider above it renders no banners instead of throwing.
+
## Raising notifications
```tsx
diff --git a/packages/app-shell/README.md b/packages/app-shell/README.md
index ac084c1290..a5d86eceac 100644
--- a/packages/app-shell/README.md
+++ b/packages/app-shell/README.md
@@ -64,6 +64,29 @@ function MyDashboard() {
- **Full-Page AI Workspace**: The `/ai` surface provides a responsive chat
workspace with a desktop conversation rail, mobile Chats drawer, and a
constrained reading width for long conversations
+- **Notification Surfaces**: `ConsoleShell` mounts `NotificationProvider` and
+ every spec `displayType` presents distinctly — no per-app wiring
+
+## Notifications
+
+`ConsoleShell` mounts `NotificationProvider`, so any console route can call
+`useNotifications()` and get the presentation it asked for:
+
+| `displayType` | Surface | Mounted by |
+| --- | --- | --- |
+| `toast` | sonner, via `presentNotificationToast` | `ConsoleShell` |
+| `snackbar` | `` | `ConsoleShell` |
+| `alert` | `` | `ConsoleShell` |
+| `banner` | `` | `ConsoleLayout`, top of the content area |
+| `inline` | `` | the surface that raises it |
+
+`inline` is deliberately not mounted globally — rendering in place at the raiser
+is the whole difference between it and a banner. Both console-side pieces are
+exported for hand-assembled shells: `presentNotificationToast` (the `onToast`
+delegate) and `ConsoleNotificationBanners` (the banners, guarded by
+`useHasNotificationProvider()` so a layout without the provider renders no
+banners instead of throwing). See the
+[notifications guide](https://objectui.org/docs/guide/notifications).
## Components
diff --git a/packages/app-shell/src/chrome/index.ts b/packages/app-shell/src/chrome/index.ts
index 494668776d..58945ce555 100644
--- a/packages/app-shell/src/chrome/index.ts
+++ b/packages/app-shell/src/chrome/index.ts
@@ -8,3 +8,4 @@ export { ErrorBoundary } from './ErrorBoundary';
export { LoadingScreen } from './LoadingScreen';
export { ThemeProvider, useTheme } from './ThemeProvider';
export { toastWithUndo, type ToastWithUndoOptions } from './toast-helpers';
+export { presentNotificationToast } from './notificationToast';
diff --git a/packages/app-shell/src/chrome/notificationToast.test.ts b/packages/app-shell/src/chrome/notificationToast.test.ts
new file mode 100644
index 0000000000..36b2854dc7
--- /dev/null
+++ b/packages/app-shell/src/chrome/notificationToast.test.ts
@@ -0,0 +1,104 @@
+/**
+ * The console's `toast` surface — the one display type `NotificationProvider`
+ * delegates instead of rendering itself (#3014). These pin the mapping onto
+ * sonner; which notifications ever get here is covered by
+ * `console/__tests__/ConsoleShell.notifications.test.tsx`.
+ */
+
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import type { NotificationItem } from '@object-ui/react';
+
+vi.mock('sonner', () => ({
+ toast: Object.assign(vi.fn(), {
+ info: vi.fn(), success: vi.fn(), warning: vi.fn(), error: vi.fn(),
+ }),
+}));
+
+import { toast } from 'sonner';
+import { presentNotificationToast } from './notificationToast';
+
+function item(overrides: Partial = {}): NotificationItem {
+ return {
+ id: 'n1',
+ title: 'Saved',
+ severity: 'success',
+ createdAt: new Date(0),
+ ...overrides,
+ };
+}
+
+describe('presentNotificationToast', () => {
+ beforeEach(() => { vi.clearAllMocks(); });
+
+ it('maps each severity to its sonner variant', () => {
+ presentNotificationToast(item({ severity: 'info' }));
+ presentNotificationToast(item({ severity: 'success' }));
+ presentNotificationToast(item({ severity: 'warning' }));
+ presentNotificationToast(item({ severity: 'error' }));
+
+ expect(toast.info).toHaveBeenCalledTimes(1);
+ expect(toast.success).toHaveBeenCalledTimes(1);
+ expect(toast.warning).toHaveBeenCalledTimes(1);
+ expect(toast.error).toHaveBeenCalledTimes(1);
+ });
+
+ it('passes the message through as the toast description', () => {
+ presentNotificationToast(item({ message: 'Record updated' }));
+ expect(toast.success).toHaveBeenCalledWith('Saved', expect.objectContaining({
+ description: 'Record updated',
+ }));
+ });
+
+ it('turns duration 0 into a persistent toast', () => {
+ // `0 = persistent` is the notification contract; sonner spells it Infinity.
+ // Passing the 0 through would make the toast vanish on the next tick.
+ presentNotificationToast(item({ duration: 0 }));
+ expect(toast.success).toHaveBeenCalledWith('Saved', expect.objectContaining({
+ duration: Infinity,
+ }));
+ });
+
+ it('leaves an unset duration to the ConsoleToaster default', () => {
+ presentNotificationToast(item());
+ expect(toast.success).toHaveBeenCalledWith('Saved', expect.objectContaining({
+ duration: undefined,
+ }));
+ });
+
+ it('forwards an explicit duration unchanged', () => {
+ presentNotificationToast(item({ duration: 12_000 }));
+ expect(toast.success).toHaveBeenCalledWith('Saved', expect.objectContaining({
+ duration: 12_000,
+ }));
+ });
+
+ it('maps the first action to the toast action button', () => {
+ const onClick = vi.fn();
+ presentNotificationToast(item({
+ actions: [
+ { label: 'Undo', onClick },
+ { label: 'Ignored — a toast has one action slot', onClick: vi.fn() },
+ ],
+ }));
+
+ const options = vi.mocked(toast.success).mock.calls[0][1] as {
+ action?: { label: string; onClick: () => void };
+ };
+ expect(options.action?.label).toBe('Undo');
+ options.action?.onClick();
+ expect(onClick).toHaveBeenCalledTimes(1);
+ });
+
+ it('omits the action slot entirely when none is declared', () => {
+ presentNotificationToast(item());
+ const options = vi.mocked(toast.success).mock.calls[0][1] as Record;
+ expect(options).not.toHaveProperty('action');
+ });
+
+ it('forwards dismissible', () => {
+ presentNotificationToast(item({ dismissible: false }));
+ expect(toast.success).toHaveBeenCalledWith('Saved', expect.objectContaining({
+ dismissible: false,
+ }));
+ });
+});
diff --git a/packages/app-shell/src/chrome/notificationToast.ts b/packages/app-shell/src/chrome/notificationToast.ts
new file mode 100644
index 0000000000..45aaa937df
--- /dev/null
+++ b/packages/app-shell/src/chrome/notificationToast.ts
@@ -0,0 +1,50 @@
+/**
+ * The console's `toast` surface.
+ *
+ * `NotificationProvider` presents four of the five spec display types through
+ * surface components (#3014); `toast` is the one it delegates, and in the
+ * console that delegate is sonner. This module is that bridge — it is the
+ * ONLY place a notification becomes a sonner call, so the mapping (severity →
+ * variant, `duration: 0` → persistent, first action → the toast's action
+ * button) lives in one testable function rather than inline in the shell.
+ *
+ * Non-toast display types never reach here: the provider routes them to
+ * `NotificationBanners` / `NotificationSnackbar` / `NotificationAlerts` /
+ * `NotificationInline` instead.
+ */
+
+import { toast } from 'sonner';
+import type { NotificationItem, NotificationSeverityLevel } from '@object-ui/react';
+
+/**
+ * Typed as `Record` so a new severity in the spec
+ * `NotificationSeveritySchema` fails type-check here until it has a variant,
+ * rather than silently rendering as a neutral toast.
+ */
+const TOAST_BY_SEVERITY: Record unknown> = {
+ info: (message, data) => toast.info(message, data as never),
+ success: (message, data) => toast.success(message, data as never),
+ warning: (message, data) => toast.warning(message, data as never),
+ error: (message, data) => toast.error(message, data as never),
+};
+
+/**
+ * Present one `displayType: 'toast'` notification through sonner.
+ *
+ * Duration follows the notification contract: `0` means persistent (sonner's
+ * `Infinity`), and an absent duration defers to the `ConsoleToaster` default
+ * rather than being invented here.
+ */
+export function presentNotificationToast(notification: NotificationItem): void {
+ const show = TOAST_BY_SEVERITY[notification.severity] ?? TOAST_BY_SEVERITY.info;
+ // A toast carries at most one action button — sonner has one `action` slot,
+ // and a notification needing more than one belongs on a banner or an alert.
+ const action = notification.actions?.[0];
+
+ show(notification.title, {
+ description: notification.message,
+ duration: notification.duration === 0 ? Infinity : notification.duration,
+ dismissible: notification.dismissible,
+ ...(action ? { action: { label: action.label, onClick: action.onClick } } : {}),
+ });
+}
diff --git a/packages/app-shell/src/console/ConsoleShell.tsx b/packages/app-shell/src/console/ConsoleShell.tsx
index fd45cf11c8..eeb7c42f42 100644
--- a/packages/app-shell/src/console/ConsoleShell.tsx
+++ b/packages/app-shell/src/console/ConsoleShell.tsx
@@ -14,7 +14,9 @@ import { Suspense, useEffect, useRef, useState, type ReactNode } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { AuthGuard, useAuth, createAuthenticatedFetch } from '@object-ui/auth';
import { useObjectTranslation } from '@object-ui/i18n';
-import { SchemaRendererProvider, ActionProvider } from '@object-ui/react';
+import { SchemaRendererProvider, ActionProvider, NotificationProvider } from '@object-ui/react';
+import { NotificationAlerts, NotificationSnackbar } from '@object-ui/components';
+import { presentNotificationToast } from '../chrome/notificationToast';
import { useActionModal } from '../hooks/useActionModal';
import { useConsoleActionRuntime } from '../hooks/useConsoleActionRuntime';
import { createObjectStackUserStateAdapter } from '@object-ui/data-objectstack';
@@ -101,8 +103,9 @@ function GlobalActionRuntimeProvider({ dataSource, children }: { dataSource: unk
/**
* ConsoleShell — top-level provider stack shared by every console route.
* Wraps children in ThemeProvider + NavigationProvider + FavoritesProvider +
- * Suspense so lazy route components get a default loading fallback and
- * dark/light/system theme switching works out of the box.
+ * NotificationProvider + Suspense so lazy route components get a default
+ * loading fallback, dark/light/system theme switching, and a notification
+ * system that honors the spec `displayType`.
*
* Place this inside a and around your :
*
@@ -111,23 +114,43 @@ function GlobalActionRuntimeProvider({ dataSource, children }: { dataSource: unk
* ...
*
*
+ *
+ * ── Notification surfaces (#3014) ──
+ * Each spec display type has its own presentation, so the shell mounts the ones
+ * with a single global home and leaves the rest to their owners:
+ *
+ * toast → `presentNotificationToast` (sonner, via `onToast`)
+ * snackbar → here — it anchors itself bottom-center
+ * alert → here — a blocking dialog is global
+ * banner → in `ConsoleLayout`, at the top of the
+ * content area, next to the draft /
+ * unpublished bars it sits with
+ * inline → nothing here, by contract — an inline notification is rendered
+ * by the surface that RAISED it (``),
+ * which is the whole difference between it and a banner
*/
export function ConsoleShell({ children }: { children: ReactNode }) {
return (
-
-
-
-
-
- }>{children}
- {/* ADR-0069 — full-screen gate (expired password / required MFA) above all routes */}
-
-
-
-
-
-
+ {/* `defaultDuration` matches ConsoleToaster's 4s toast default, so a
+ snackbar and a toast raised together disappear together. */}
+
+
+
+
+
+
+ }>{children}
+ {/* ADR-0069 — full-screen gate (expired password / required MFA) above all routes */}
+
+
+
+
+
+
+
+
+
);
}
diff --git a/packages/app-shell/src/console/__tests__/ConsoleShell.notifications.test.tsx b/packages/app-shell/src/console/__tests__/ConsoleShell.notifications.test.tsx
new file mode 100644
index 0000000000..1f73df9f28
--- /dev/null
+++ b/packages/app-shell/src/console/__tests__/ConsoleShell.notifications.test.tsx
@@ -0,0 +1,132 @@
+/**
+ * The console's notification surfaces (#3014 follow-up).
+ *
+ * `ConsoleShell` mounts `NotificationProvider` plus the surfaces with a single
+ * global home (snackbar, alert); `ConsoleLayout` mounts the banners in the
+ * content area. These tests pin the wiring: each spec `displayType` raised from
+ * inside the console reaches its OWN presentation, and `toast` — and only
+ * `toast` — reaches sonner.
+ */
+
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { useEffect } from 'react';
+import { render, screen, act } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { useNotifications, type NotificationItem } from '@object-ui/react';
+import { ConsoleShell } from '../ConsoleShell';
+import { ConsoleNotificationBanners } from '../../layout/ConsoleNotificationBanners';
+
+// The `toast` surface. Mocked so the assertion is "sonner was asked to show
+// exactly this", not "sonner rendered something in jsdom".
+vi.mock('sonner', () => ({
+ toast: Object.assign(vi.fn(), {
+ info: vi.fn(), success: vi.fn(), warning: vi.fn(), error: vi.fn(),
+ dismiss: vi.fn(), custom: vi.fn(), loading: vi.fn(), promise: vi.fn(),
+ }),
+ Toaster: () => null,
+}));
+
+// ADR-0069 gate — unrelated to notifications and needs an AuthProvider above it.
+vi.mock('../RemediationOverlay', () => ({ RemediationOverlay: () => null }));
+
+import { toast } from 'sonner';
+
+type Notify = (input: Omit) => string;
+
+/** Populated once the shell has mounted; the tests raise through it. */
+const notifier: { current: Notify | null } = { current: null };
+
+function Raiser() {
+ const { notify } = useNotifications();
+ useEffect(() => { notifier.current = notify; }, [notify]);
+ return ROUTE CONTENT
;
+}
+
+const raise: Notify = (input) => notifier.current!(input);
+
+/** ConsoleShell with the content-area banners `ConsoleLayout` contributes. */
+function renderConsole() {
+ return render(
+
+
+
+ ,
+ );
+}
+
+/** The surface element a notification title actually rendered into, if any. */
+function surfaceOf(title: string): string | null {
+ const host = screen.queryByText(title)?.closest('[data-notification-surface]');
+ return host?.getAttribute('data-notification-surface') ?? null;
+}
+
+describe('ConsoleShell notification surfaces', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ notifier.current = null;
+ renderConsole();
+ });
+
+ it('mounts the provider so any console surface can notify', () => {
+ expect(screen.getByText('ROUTE CONTENT')).toBeInTheDocument();
+ expect(notifier.current).toBeTypeOf('function');
+ });
+
+ it('sends a toast to sonner and to no surface', () => {
+ act(() => { raise({ title: 'Saved', message: 'Record updated', severity: 'success' }); });
+
+ expect(toast.success).toHaveBeenCalledWith('Saved', expect.objectContaining({
+ description: 'Record updated',
+ }));
+ expect(screen.queryByText('Saved')).not.toBeInTheDocument();
+ });
+
+ it('renders a banner in the content area, not through sonner', () => {
+ act(() => { raise({ title: 'Viewing a draft', severity: 'warning', displayType: 'banner' }); });
+
+ expect(surfaceOf('Viewing a draft')).toBe('banner');
+ expect(toast.warning).not.toHaveBeenCalled();
+ });
+
+ it('renders a snackbar bottom-anchored, not through sonner', () => {
+ act(() => { raise({ title: 'Row deleted', severity: 'info', displayType: 'snackbar' }); });
+
+ expect(surfaceOf('Row deleted')).toBe('snackbar');
+ expect(toast.info).not.toHaveBeenCalled();
+ });
+
+ it('renders an alert as a blocking dialog', async () => {
+ const user = userEvent.setup();
+ act(() => { raise({ title: 'Session expired', severity: 'error', displayType: 'alert' }); });
+
+ const dialog = await screen.findByRole('alertdialog');
+ expect(dialog).toHaveAttribute('data-notification-surface', 'alert');
+ expect(toast.error).not.toHaveBeenCalled();
+
+ await user.click(screen.getByRole('button', { name: 'OK' }));
+ expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument();
+ });
+
+ it('keeps the four presentations distinct when raised together', () => {
+ act(() => {
+ raise({ title: 'N toast', severity: 'info', displayType: 'toast' });
+ raise({ title: 'N snackbar', severity: 'info', displayType: 'snackbar' });
+ raise({ title: 'N banner', severity: 'info', displayType: 'banner' });
+ raise({ title: 'N alert', severity: 'info', displayType: 'alert' });
+ });
+
+ const seen = ['N snackbar', 'N banner', 'N alert'].map(surfaceOf);
+ expect(seen).toEqual(['snackbar', 'banner', 'alert']);
+ expect(toast.info).toHaveBeenCalledTimes(1);
+ expect(toast.info).toHaveBeenCalledWith('N toast', expect.anything());
+ });
+});
+
+describe('ConsoleNotificationBanners outside a NotificationProvider', () => {
+ it('renders nothing instead of throwing', () => {
+ // `ConsoleLayout` is a composable piece — a host may assemble it without
+ // `ConsoleShell`. `useNotifications()` throws there, which would white-screen
+ // the app; the guard has to keep that from happening.
+ expect(() => render()).not.toThrow();
+ });
+});
diff --git a/packages/app-shell/src/index.ts b/packages/app-shell/src/index.ts
index 2bb5521d28..4c0ec8f9a7 100644
--- a/packages/app-shell/src/index.ts
+++ b/packages/app-shell/src/index.ts
@@ -71,6 +71,7 @@ export type { AiSurfaceState } from './hooks/useAiSurface';
// Layout chrome
export {
ConsoleLayout,
+ ConsoleNotificationBanners,
AppHeader,
AppSidebar,
UnifiedSidebar,
@@ -91,6 +92,7 @@ export {
OnboardingWalkthrough,
ConditionalAuthWrapper,
ConsoleToaster,
+ presentNotificationToast,
RouteFader,
toastWithUndo,
type ToastWithUndoOptions,
diff --git a/packages/app-shell/src/layout/ConsoleLayout.tsx b/packages/app-shell/src/layout/ConsoleLayout.tsx
index 1bf9f2d549..136a562aa8 100644
--- a/packages/app-shell/src/layout/ConsoleLayout.tsx
+++ b/packages/app-shell/src/layout/ConsoleLayout.tsx
@@ -25,6 +25,7 @@ import {
} from './chatDockState';
import { DraftPreviewBar } from '../preview/DraftPreviewBar';
import { UnpublishedAppBar } from '../preview/UnpublishedAppBar';
+import { ConsoleNotificationBanners } from './ConsoleNotificationBanners';
import { UnifiedSidebar } from './UnifiedSidebar';
import { AppHeader } from './AppHeader';
import { MobileViewSwitcherProvider } from './MobileViewSwitcherContext';
@@ -185,6 +186,10 @@ export function ConsoleLayout({
{/* ADR-0045: materialized-but-unlisted app — real and interactive,
invisible to end users until the Publish visibility flip. */}
+ {/* #3014: `displayType: 'banner'` notifications. In flow, above the
+ route content and below the two preview bars — a banner states a
+ standing condition, same as its neighbours here. */}
+
{children}
diff --git a/packages/app-shell/src/layout/ConsoleNotificationBanners.tsx b/packages/app-shell/src/layout/ConsoleNotificationBanners.tsx
new file mode 100644
index 0000000000..15e2af4963
--- /dev/null
+++ b/packages/app-shell/src/layout/ConsoleNotificationBanners.tsx
@@ -0,0 +1,28 @@
+/**
+ * ConsoleNotificationBanners
+ *
+ * `` for the console content area, guarded so
+ * `ConsoleLayout` stays usable outside a `NotificationProvider`.
+ *
+ * The guard is not defensive noise: `ConsoleShell` is deliberately a set of
+ * composable pieces consumers assemble in their own `App.tsx` (see that
+ * module's header), so a host can legitimately render `ConsoleLayout` without
+ * the shell above it. `useNotifications()` THROWS outside its provider — that
+ * would turn "no notification system configured" into a white screen for the
+ * whole app. `useHasNotificationProvider()` reads the context without throwing,
+ * so the banners simply don't render.
+ * @module
+ */
+
+import { useHasNotificationProvider } from '@object-ui/react';
+import { NotificationBanners } from '@object-ui/components';
+
+export function ConsoleNotificationBanners() {
+ // Must gate on RENDERING , not on an early return
+ // inside it — its own hooks throw before any guard we could put there.
+ const hasProvider = useHasNotificationProvider();
+ if (!hasProvider) return null;
+ return ;
+}
+
+ConsoleNotificationBanners.displayName = 'ConsoleNotificationBanners';
diff --git a/packages/app-shell/src/layout/index.ts b/packages/app-shell/src/layout/index.ts
index 6bddabc1ad..facf88ca1f 100644
--- a/packages/app-shell/src/layout/index.ts
+++ b/packages/app-shell/src/layout/index.ts
@@ -1,4 +1,5 @@
export { ConsoleLayout } from './ConsoleLayout';
+export { ConsoleNotificationBanners } from './ConsoleNotificationBanners';
export { AppHeader } from './AppHeader';
export { AppSidebar } from './AppSidebar';
export { UnifiedSidebar } from './UnifiedSidebar';