@aircall/blocks
Version:
Aircall Blocks — higher-level UI compositions built on @aircall/ds
264 lines (211 loc) • 11.1 kB
Markdown
---
name: aircall-blocks/migrate-dashboard/dashboard-page
description: >
Migrate a screen's page shell to /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 `/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 '/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 '/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 `/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 (`/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 '/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`