---
name: aircall-blocks/migrate-dashboard/dashboard-page
description: >
  Migrate a screen's page shell to @aircall/blocks — DashboardPage (standard in-app page:
  sidebar + header + optional tabs + content) vs DashboardStandalonePage (full-page focused
  flow with no sidebar, e.g. Campaign Creation, Add Contacts). Load when migrating a screen's
  outer layout, a full-page wizard/creation flow, or a page header+content scaffold.
type: sub-skill
library: aircall-blocks
requires:
  - aircall-blocks/setup
  - aircall-blocks/migrate-dashboard
sources:
  - "aircall/hydra:packages/blocks/src/components/dashboard-page.tsx"
  - "aircall/hydra:packages/blocks/src/components/dashboard-standalone-page.tsx"
---

This skill builds on aircall-blocks/migrate-dashboard.

## 1. Which shell?

| Screen kind | Use | Why |
|---|---|---|
| Standard in-app page (sidebar + header + optional tabs + content) | `DashboardPage` | Lives inside the dashboard chrome |
| Full-page focused flow — creation wizard, Add Contacts, standalone forms (no sidebar) | `DashboardStandalonePage` | Takes over the viewport; header is just title + actions |

Campaign Creation (`CampaignWizardPage`) and Add Contacts (`AddContactsPage`) are **standalone** flows → `DashboardStandalonePage`, with the multi-step form inside (see `@aircall/blocks#aircall-blocks/migrate-dashboard/form-wizard`).

## 2. DashboardStandalonePage (full-page flows)

Four-part nesting: the outer `DashboardStandalonePage` is the `page-background` shell
(8px inset, flex row); `DashboardStandalonePageContent` is the rounded content **panel**
that holds the header + scroll area; the header carries a centered title (+ optional
description) with edge-pinned actions; `DashboardStandalonePageMain` is the scroll area.

```tsx
import {
  DashboardStandalonePage,
  DashboardStandalonePageContent,
  DashboardStandalonePageHeader,
  DashboardStandalonePageTitle,
  DashboardStandalonePageDescription,
  DashboardStandalonePageActions,
  DashboardStandalonePageAction,
  DashboardStandalonePageMain
} from '@aircall/blocks';

<DashboardStandalonePage>
  <DashboardStandalonePageContent>
    <DashboardStandalonePageHeader>
      <DashboardStandalonePageTitle>Create campaign</DashboardStandalonePageTitle>
      {/* optional subtitle under the title */}
      <DashboardStandalonePageDescription>Set up your outbound campaign</DashboardStandalonePageDescription>
      {/* actions pin to an edge; side="start" pins left, default "end" pins right */}
      <DashboardStandalonePageActions>
        <DashboardStandalonePageAction onClick={onCancel}>Cancel</DashboardStandalonePageAction>
      </DashboardStandalonePageActions>
    </DashboardStandalonePageHeader>
    <DashboardStandalonePageMain>
      {/* a centered, width-capped Card — see below */}
    </DashboardStandalonePageMain>
  </DashboardStandalonePageContent>
</DashboardStandalonePage>
```

- **Header** is a fixed 64px bar; the title (+ description) is optically centered and
  actions are pinned to the left/right edges via `side` — declare them in any order.
- **Split layouts:** put a second panel (e.g. a `ChatbotPanel`) as a sibling of
  `DashboardStandalonePageContent`; they share the shell's 8px gap. Hide a secondary
  side panel below the `md` breakpoint (`className="hidden md:flex"`) so the main flow
  gets the full width on narrow screens.

### What goes inside `DashboardStandalonePageMain` — a centered, width-capped Card

`DashboardStandalonePageMain` is a full-bleed scroll area; it does **not** constrain
width. The actual content is a centered container (`mx-auto` + a `max-w-*` cap) wrapping
one or more `Card`s — the consistent pattern across the `DashboardStandalonePage` stories.
Don't let content run full-width, and don't reach for `Paper`/raw page wrappers.

```tsx
import { Card, CardHeader, CardContent, CardFooter } from '@aircall/ds';

<DashboardStandalonePageMain>
  {/* single-column form / wizard */}
  <div className="mx-auto max-w-2xl pt-4">
    <Card>
      <CardHeader>{/* title, or Stepper progress for a wizard */}</CardHeader>
      <CardContent>{/* fields */}</CardContent>
      <CardFooter className="justify-end gap-2">{/* Back / Next / Submit */}</CardFooter>
    </Card>
  </div>
</DashboardStandalonePageMain>
```

Width cap by layout:
- **Single-column form / wizard** → `mx-auto max-w-2xl pt-4` around one `Card` (CampaignWizard pattern).
- **Wide / two-column** (form + summary aside) → `mx-auto flex max-w-7xl flex-col gap-6 pt-8 lg:flex-row`, a `Card` per column.

> **API note (renamed):** `DashboardStandalonePageContent` is now the rounded **panel**
> wrapper (header + main), and the scroll area is `DashboardStandalonePageMain` — matching
> the `DashboardPage` family (`Page → Content → Main`). Earlier code where
> `DashboardStandalonePageContent` was the scroll area must move that content into
> `DashboardStandalonePageMain` and wrap header+main in `DashboardStandalonePageContent`.

## 3. DashboardPage (standard in-app page)

```tsx
import {
  DashboardPage,
  DashboardPageContent,
  DashboardPageMain,
  DashboardPageTabs,
  DashboardPageHeader,
  DashboardPageHeaderTitle,
  DashboardPageHeaderActions,
  DashboardPageHeaderAction,
  DashboardSidebarProvider
} from '@aircall/blocks';

<DashboardPage>
  <DashboardSidebarProvider defaultOpen>
    {/* your <DashboardSidebar> */}
    <DashboardPageContent>
      <DashboardPageHeader>
        <DashboardPageHeaderTitle size="lg">Campaigns</DashboardPageHeaderTitle>
        <DashboardPageHeaderActions>
          <DashboardPageHeaderAction variant="default">New campaign</DashboardPageHeaderAction>
        </DashboardPageHeaderActions>
      </DashboardPageHeader>
      {/* optional: <DashboardPageTabs> here, as a sibling of the header */}
      <DashboardPageMain>{/* page content */}</DashboardPageMain>
    </DashboardPageContent>
  </DashboardSidebarProvider>
</DashboardPage>
```

`DashboardPageBanner` is available for a page-level banner. The page header itself migrates per `@aircall/blocks#aircall-blocks/migrate-dashboard/page-header`.

### Extension pages — start at `DashboardPageContent`, not `DashboardPage`

The block above is the **full** shell, which is what the host renders. Inside an
**extension** you do **not** render `DashboardPage` / `DashboardSidebarProvider` /
`DashboardSidebar` yourself — that outer shell is provided by the Sandbox during local
dev (`@dashboard/extension-script`) and by dashboard-v4 in integrate mode. Each extension
page starts at `DashboardPageContent` and returns just the content card: header +
`DashboardPageMain`.

Do **not** pass `className="h-full"` — the height is now baked into `DashboardPageContent`
at the component level. Just pass your `data-test`.

```tsx
// conversation-center-ext/src/pages/contact_insights/ContactInsights.page.tsx
import {
  DashboardPageContent,
  DashboardPageHeader,
  DashboardPageHeaderTitle,
  DashboardPageMain
} from '@aircall/blocks';

export function ContactInsightsPage() {
  const { t } = useTranslation();

  return (
    <DashboardPageContent data-test="contact-insights-page">
      <DashboardPageHeader>
        <DashboardPageHeaderTitle size="lg">
          {t('ai_assist.live_assist.pre_call_insights.title')}
        </DashboardPageHeaderTitle>
      </DashboardPageHeader>
      <DashboardPageMain className="flex flex-col gap-4 overflow-scroll">
        <ContactInsights hasTitle={false} />
      </DashboardPageMain>
    </DashboardPageContent>
  );
}
```

### Page-level notices (beta/feature/trial banners) → `NotificationQueue`

For app/page-level notices (e.g. a "beta feature" banner), prefer the ds **`NotificationQueue`** over an inline `Banner` — it stacks, dedupes, and orders by priority, and renders wherever you place a `NotificationSlot` (e.g. at the very top of the page, above the header). Mechanism (from `packages/ds/src/components/notification-queue.tsx`):

```tsx
import { NotificationQueueProvider, NotificationSlot, useNotification } from '@aircall/ds';

// 1. Wrap the screen (or app) once:
<NotificationQueueProvider>
  <NotificationSlot slot="page" />        {/* render output at the top of the page */}
  {/* …DashboardPage / DashboardStandalonePage… */}
</NotificationQueueProvider>

// 2. Declare the notice (lifecycle-bound) from any descendant:
useNotification({
  id: 'campaigns-beta',
  slot: 'page',
  priority: 'promotional',                 // 'error' | 'warning' | 'success' | 'info' | 'promotional'
  render: () => <>You're using a beta feature 🚧 …</>
});
```

Use `useNotification(...)` for persistent/conditional notices (beta banner); `useNotificationQueue().push(...)` for imperative, event-driven ones. `error` priority renders without a dismiss CTA.

Source: `packages/ds/src/components/notification-queue.tsx`

## 4. Common Mistakes

### HIGH — Using `DashboardPage` (with sidebar) for a full-page creation flow

Wrong:
```tsx
<DashboardPage>
  <DashboardSidebarProvider>{/* sidebar */}
    <DashboardPageContent>{/* Campaign Creation wizard */}</DashboardPageContent>
  </DashboardSidebarProvider>
</DashboardPage>
```

Correct:
```tsx
<DashboardStandalonePage>
  <DashboardStandalonePageContent>
    <DashboardStandalonePageHeader>...</DashboardStandalonePageHeader>
    <DashboardStandalonePageMain>{/* wizard */}</DashboardStandalonePageMain>
  </DashboardStandalonePageContent>
</DashboardStandalonePage>
```

Focused flows (creation, Add Contacts) take over the viewport and have no sidebar. Wrapping them in `DashboardPage` renders the dashboard chrome/sidebar around the flow and needlessly pulls in `DashboardSidebarProvider`.

Source: `packages/blocks/src/components/dashboard-standalone-page.tsx`

### HIGH — Putting the header outside `DashboardPageContent`

Wrong:
```tsx
<DashboardPage>
  <DashboardPageHeader>...</DashboardPageHeader>   {/* direct child of DashboardPage */}
  <DashboardPageContent>...</DashboardPageContent>
</DashboardPage>
```

Correct: the header lives **inside** `DashboardPageContent`, alongside `DashboardPageMain`.

`DashboardPageContent` owns the header/main grid (and a `:has()` rule manages the header border). A header outside it loses that layout and the border handling.

Source: `packages/blocks/src/components/dashboard-page.tsx`

### MEDIUM — Nesting `DashboardPageTabs` inside the header

Wrong: `<DashboardPageHeader><DashboardPageTabs/></DashboardPageHeader>`.

Correct: `DashboardPageTabs` is a **sibling** of `DashboardPageHeader` inside `DashboardPageContent`.

`DashboardPageTabs` provides its own `border-b`; nesting it in the header double-borders and misplaces the tab strip.

Source: `packages/blocks/src/components/dashboard-page.tsx`

### MEDIUM — Re-creating page chrome with raw `div`/`Paper` instead of the shell

Wrong: a hand-rolled `<div className="page">` + a `Paper`/`Card` page header.

Correct: use the `DashboardPage` / `DashboardStandalonePage` shell so spacing, borders, and the header/content grid match the rest of the dashboard.

Source: `packages/blocks/src/components/dashboard-page.tsx`
