UNPKG

@aircall/blocks

Version:

Aircall Blocks — higher-level UI compositions built on @aircall/ds

264 lines (211 loc) 11.1 kB
--- 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 ### HIGHUsing `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` ### HIGHPutting 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` ### MEDIUMNesting `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` ### MEDIUMRe-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`