@aircall/blocks
Version:
Aircall Blocks — higher-level UI compositions built on @aircall/ds
394 lines (323 loc) • 14.6 kB
Markdown
---
name: aircall-blocks/migrate-dashboard/page-header
description: >
Migrate /library PageHeader, PageHeaderLink, and GaramondTitleTypography
to /blocks DashboardPageHeader and its sub-components (DashboardPageHeaderTitle,
DashboardPageHeaderActions, DashboardPageHeaderAction, DashboardPageHeaderNav,
DashboardPageHeaderNavBack, DashboardPageHeaderPrefix, DashboardPageHeaderTitleGroup,
DashboardPageHeaderSubtitle, DashboardPageHeaderDescription). Load when a file
imports PageHeader or PageHeaderLink from /library.
type: sub-skill
library: aircall-blocks
requires:
- aircall-blocks/setup
- aircall-blocks/migrate-dashboard
sources:
- "aircall/hydra:packages/blocks/src/index.ts"
---
This skill builds on aircall-blocks/migrate-dashboard.
## 1. Component mapping
| /library | @aircall/blocks |
| --- | --- |
| `PageHeader` (root container) | `DashboardPageHeader` |
| `PageHeader` `title` prop (small heading) | `DashboardPageHeaderTitle size="sm"` |
| `PageHeader` `largeTitle` prop (large heading) | `DashboardPageHeaderTitle size="lg"` |
| `PageHeader` `subtitle` prop | `DashboardPageHeaderDescription` inside `DashboardPageHeaderSubtitle` inside `DashboardPageHeaderTitleGroup` |
| `PageHeader` `renderTitleRight` render prop | `DashboardPageHeaderActions` + `DashboardPageHeaderAction` |
| `PageHeader` `renderExtra` render prop | Extra children appended directly inside `DashboardPageHeader` |
| `PageHeader` `renderSubLeft` / `renderSubRight` render props | Children inside `DashboardPageHeaderSubtitle` |
| `PageHeader` `gobackLinkRoute` + `gobackLinkText` props | `DashboardPageHeaderNav` + `DashboardPageHeaderNavBack` |
| `PageHeaderLink` (standalone back-link component) | `DashboardPageHeaderNavBack` inside `DashboardPageHeaderNav` |
| `PageHeader` `icon` prop (tractor `Icon` component) | `DashboardPageHeaderPrefix` (put icon/flag/avatar inside) |
| `PageHeader` `avatarSrc` / `avatarInitials` / `renderAvatar` props | `DashboardPageHeaderPrefix` (put `Avatar` or equivalent inside) |
| `PageHeader` `activeTabId` / `onTabChange` / `Tab.*` children | No direct equivalent — implement tabs separately below the header |
| `GaramondTitleTypography` / `useGaramondFont` prop | Not reproduced — use `DashboardPageHeaderTitle size="lg"` (system font) |
| `PageHeader` `titleProps` (BoxProps spread) | Remove — `DashboardPageHeaderTitleGroup` handles layout |
`DashboardPageHeader` is a flat-children composition: sub-components self-place via CSS
grid areas — declaration order determines render order within each area. You do **not** pass
data via props on the root; every piece of content is a named sub-component child.
## 2. Imports
```tsx
// All DashboardPageHeader parts from @aircall/blocks
import {
DashboardPageHeader,
DashboardPageHeaderTitle,
DashboardPageHeaderTitleGroup,
DashboardPageHeaderSubtitle,
DashboardPageHeaderDescription,
DashboardPageHeaderActions,
DashboardPageHeaderAction,
DashboardPageHeaderNav,
DashboardPageHeaderNavBack,
DashboardPageHeaderPrefix,
} from '/blocks';
// DS primitives used alongside (Tooltip, DropdownMenu, etc.)
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@aircall/ds';
```
## 3. Before / After
### 3a. Simple title-only header
**Before (`/library`):**
```tsx
import { PageHeader } from '@dashboard/library';
function AnalyticsHeader() {
return <PageHeader largeTitle="Analytics" />;
}
```
**After (`/blocks`):**
```tsx
import { DashboardPageHeader, DashboardPageHeaderTitle } from '@aircall/blocks';
function AnalyticsHeader() {
return (
<DashboardPageHeader>
<DashboardPageHeaderTitle size="lg">Analytics</DashboardPageHeaderTitle>
</DashboardPageHeader>
);
}
```
Key changes:
- `largeTitle` string prop → `DashboardPageHeaderTitle size="lg"` child with the string as children.
- `title` string prop → `DashboardPageHeaderTitle size="sm"` (level-2 pages that have a nav row).
- No `data-test` prop — set `data-test` directly on `DashboardPageHeader` if needed.
---
### 3b. Title with actions (renderTitleRight)
**Before (`/library`):**
```tsx
import { PageHeader } from '@dashboard/library';
import { Button } from '@aircall/tractor';
function CampaignsHeader() {
return (
<PageHeader
largeTitle="Campaigns"
renderTitleRight={() => (
<Button variant="primary" size="regular">Create campaign</Button>
)}
/>
);
}
```
**After (`/blocks`):**
```tsx
import {
DashboardPageHeader,
DashboardPageHeaderTitle,
DashboardPageHeaderActions,
DashboardPageHeaderAction,
} from '/blocks';
function CampaignsHeader() {
return (
<DashboardPageHeader>
<DashboardPageHeaderTitle size="lg">Campaigns</DashboardPageHeaderTitle>
<DashboardPageHeaderActions>
<DashboardPageHeaderAction variant="default">Create campaign</DashboardPageHeaderAction>
</DashboardPageHeaderActions>
</DashboardPageHeader>
);
}
```
Key changes:
- `renderTitleRight` render prop → `DashboardPageHeaderActions` child containing `DashboardPageHeaderAction` elements.
- Tractor `Button variant="primary"` → `DashboardPageHeaderAction variant="default"` (size is fixed by the component).
- Icon-only action buttons: `DashboardPageHeaderAction variant="outline" size="icon-lg"`.
---
### 3c. Back navigation (gobackLinkRoute / gobackLinkText)
**Before (`/library`):**
```tsx
import { PageHeader } from '@dashboard/library';
function NumberDetailHeader() {
return (
<PageHeader
title="Number settings"
gobackLinkRoute="/numbers"
gobackLinkText="Back to numbers"
renderTitleRight={() => <button>Save</button>}
/>
);
}
```
**After (`/blocks`):**
```tsx
import {
DashboardPageHeader,
DashboardPageHeaderNav,
DashboardPageHeaderNavBack,
DashboardPageHeaderTitle,
DashboardPageHeaderActions,
DashboardPageHeaderAction,
} from '/blocks';
import { Link } from 'react-router-dom';
function NumberDetailHeader() {
return (
<DashboardPageHeader>
<DashboardPageHeaderNav>
<DashboardPageHeaderNavBack render={<Link to="/numbers" />}>
Back to numbers
</DashboardPageHeaderNavBack>
</DashboardPageHeaderNav>
<DashboardPageHeaderTitle size="sm">Number settings</DashboardPageHeaderTitle>
<DashboardPageHeaderActions>
<DashboardPageHeaderAction variant="default">Save</DashboardPageHeaderAction>
</DashboardPageHeaderActions>
</DashboardPageHeader>
);
}
```
Key changes:
- `gobackLinkRoute` + `gobackLinkText` props → `DashboardPageHeaderNav` > `DashboardPageHeaderNavBack` with `render={<Link to="…" />}` (Base UI render prop — avoids a nested `<a><button>` violation).
- When nav is present, use `size="sm"` on `DashboardPageHeaderTitle` (level-2 pattern).
- The `PageHeaderLink` standalone export is replaced the same way.
---
### 3d. Header with prefix (icon / avatar) and subtitle
**Before (`/library`):**
```tsx
import { PageHeader } from '@dashboard/library';
import { PhoneOutlined } from '@aircall/icons';
function NumberHeader({ number, description }) {
return (
<PageHeader
largeTitle={number.name}
subtitle={description}
icon={PhoneOutlined}
renderSubRight={() => <span>{number.countryCode}</span>}
/>
);
}
```
**After (`/blocks`):**
```tsx
import {
DashboardPageHeader,
DashboardPageHeaderPrefix,
DashboardPageHeaderTitleGroup,
DashboardPageHeaderTitle,
DashboardPageHeaderSubtitle,
DashboardPageHeaderDescription,
} from '/blocks';
import { Phone } from '@aircall/react-icons';
function NumberHeader({ number, description }) {
return (
<DashboardPageHeader>
<DashboardPageHeaderPrefix>
<Phone className="size-5 text-muted-foreground" />
</DashboardPageHeaderPrefix>
<DashboardPageHeaderTitleGroup>
<DashboardPageHeaderTitle size="lg">{number.name}</DashboardPageHeaderTitle>
<DashboardPageHeaderSubtitle>
<DashboardPageHeaderDescription>{description}</DashboardPageHeaderDescription>
<span className="text-sm text-muted-foreground">{number.countryCode}</span>
</DashboardPageHeaderSubtitle>
</DashboardPageHeaderTitleGroup>
</DashboardPageHeader>
);
}
```
Key changes:
- `icon` prop → `DashboardPageHeaderPrefix` containing the icon element; icons come from `/react-icons`, not `@aircall/icons`.
- `subtitle` prop → `DashboardPageHeaderDescription` inside `DashboardPageHeaderSubtitle` inside `DashboardPageHeaderTitleGroup`.
- `renderSubLeft` / `renderSubRight` → siblings of `DashboardPageHeaderDescription` inside `DashboardPageHeaderSubtitle`.
- When `DashboardPageHeaderPrefix` is present, `DashboardPageHeaderTitleGroup` resets its left padding automatically (CSS group selector).
- `avatarSrc` / `avatarInitials` / `renderAvatar` → render your avatar element inside `DashboardPageHeaderPrefix`.
---
### 3e. Tabs (activeTabId / onTabChange / Tab.Menu children)
`PageHeader` accepted `Tab.Menu` / `Tab.Content` children and wired them into a
`Tab.Container`. `DashboardPageHeader` has no built-in tab support.
Pattern: render a DS `Tabs` compound below `DashboardPageHeader`, outside of it.
```tsx
// Before
import { PageHeader, Tab } from '@dashboard/library';
<PageHeader largeTitle="Reports" activeTabId="overview" onTabChange={setTab}>
<Tab.Menu tabId="overview">Overview</Tab.Menu>
<Tab.Menu tabId="calls">Calls</Tab.Menu>
<Tab.Content tabId="overview"><Overview /></Tab.Content>
<Tab.Content tabId="calls"><Calls /></Tab.Content>
</PageHeader>
// After — use DS Tabs below the header
import { DashboardPageHeader, DashboardPageHeaderTitle } from '@aircall/blocks';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@aircall/ds';
<DashboardPageHeader>
<DashboardPageHeaderTitle size="lg">Reports</DashboardPageHeaderTitle>
</DashboardPageHeader>
<Tabs value={tab} onValueChange={setTab}>
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="calls">Calls</TabsTrigger>
</TabsList>
<TabsContent value="overview"><Overview /></TabsContent>
<TabsContent value="calls"><Calls /></TabsContent>
</Tabs>
```
---
## 4. Common mistakes
### Mistake 1 — Passing title/largeTitle as props instead of children
```tsx
// ❌ Wrong — PageHeader prop API; DashboardPageHeader ignores unknown props
<DashboardPageHeader largeTitle="Analytics" title="Overview" />
// ✅ Correct — title is a child component
<DashboardPageHeader>
<DashboardPageHeaderTitle size="lg">Analytics</DashboardPageHeaderTitle>
</DashboardPageHeader>
```
`DashboardPageHeader` extends `React.ComponentProps<'div'>` — it has no `title`, `largeTitle`, or `subtitle` props. Passing them results in unknown HTML attributes forwarded to the DOM and no visible heading rendered.
Source: `packages/blocks/src/components/dashboard-page-header.tsx`
### Mistake 2 — Using a Router Link directly instead of the `render` prop on DashboardPageHeaderNavBack
```tsx
// ❌ Wrong — nests <a> inside <button>, invalid HTML
<DashboardPageHeaderNavBack>
<Link to="/numbers">Back to numbers</Link>
</DashboardPageHeaderNavBack>
// ✅ Correct — render prop replaces the <button> element with the Link
<DashboardPageHeaderNavBack render={<Link to="/numbers" />}>
Back to numbers
</DashboardPageHeaderNavBack>
```
`DashboardPageHeaderNavBack` renders a `Button` (which renders a `<button>`). Nesting a
`<Link>` (`<a>`) inside a `<button>` is invalid HTML and breaks keyboard navigation.
The `render` prop (Base UI pattern) polymorphically replaces the root element while
keeping all button behavior.
Source: `packages/blocks/src/components/dashboard-page-header.tsx`
### Mistake 3 — Placing DashboardPageHeaderActions outside DashboardPageHeader
```tsx
// ❌ Wrong — outside the @container context; grid-area:actions has no effect
<DashboardPageHeader>
<DashboardPageHeaderTitle size="lg">Analytics</DashboardPageHeaderTitle>
</DashboardPageHeader>
<DashboardPageHeaderActions>
<DashboardPageHeaderAction variant="default">Export</DashboardPageHeaderAction>
</DashboardPageHeaderActions>
// ✅ Correct — all sub-components are children of DashboardPageHeader
<DashboardPageHeader>
<DashboardPageHeaderTitle size="lg">Analytics</DashboardPageHeaderTitle>
<DashboardPageHeaderActions>
<DashboardPageHeaderAction variant="default">Export</DashboardPageHeaderAction>
</DashboardPageHeaderActions>
</DashboardPageHeader>
```
Sub-components use `[grid-area:actions]` Tailwind classes that only resolve inside the
CSS grid established by `DashboardPageHeader`'s inner grid `div`. Rendered outside, they
appear in normal document flow and lose their placement entirely.
Source: `packages/blocks/src/components/dashboard-page-header.tsx`
### Mistake 4 — Importing icons from /icons instead of @aircall/react-icons
```tsx
// ❌ Wrong — @aircall/icons is the old tractor-era icon package
import { PhoneOutlined } from '@aircall/icons';
// ✅ Correct — @aircall/react-icons is the current icon package
import { Phone } from '@aircall/react-icons';
```
`DashboardPageHeader` and the DS components it composes (`Button`, etc.) are built for
`/react-icons` (lucide-based). The `@aircall/icons` package ships SVG components
with a different size/color API (`size` / `color` props vs `className`).
Source: `packages/blocks/src/components/dashboard-page-header.tsx`
### Mistake 5 — Using GaramondTitleTypography / useGaramondFont in the new stack
```tsx
// ❌ Wrong — GaramondTitleTypography and useGaramondFont are @dashboard/library internals
import { GaramondTitleTypography } from '@dashboard/library';
<GaramondTitleTypography>{title}</GaramondTitleTypography>
// ❌ Wrong — no equivalent prop on DashboardPageHeaderTitle
<DashboardPageHeaderTitle size="lg" useGaramondFont>Analytics</DashboardPageHeaderTitle>
// ✅ Correct — use DashboardPageHeaderTitle with size="lg" (system font, trimmed baseline)
<DashboardPageHeaderTitle size="lg">Analytics</DashboardPageHeaderTitle>
```
`GaramondTitleTypography` and `useGaramondFont` were `/library`-only overrides
for the ITC Garamond Narrow font. `DashboardPageHeaderTitle size="lg"` renders `text-3xl
font-bold` in the system font stack with `leading-10`, matching the design spec without
a custom typeface dependency.
Source: `packages/blocks/src/components/dashboard-page-header.tsx`