---
name: aircall-blocks/migrate-dashboard/page-header
description: >
  Migrate @dashboard/library PageHeader, PageHeaderLink, and GaramondTitleTypography
  to @aircall/blocks DashboardPageHeader and its sub-components (DashboardPageHeaderTitle,
  DashboardPageHeaderActions, DashboardPageHeaderAction, DashboardPageHeaderNav,
  DashboardPageHeaderNavBack, DashboardPageHeaderPrefix, DashboardPageHeaderTitleGroup,
  DashboardPageHeaderSubtitle, DashboardPageHeaderDescription). Load when a file
  imports PageHeader or PageHeaderLink from @dashboard/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

| @dashboard/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 '@aircall/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 (`@dashboard/library`):**
```tsx
import { PageHeader } from '@dashboard/library';

function AnalyticsHeader() {
  return <PageHeader largeTitle="Analytics" />;
}
```

**After (`@aircall/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 (`@dashboard/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 (`@aircall/blocks`):**
```tsx
import {
  DashboardPageHeader,
  DashboardPageHeaderTitle,
  DashboardPageHeaderActions,
  DashboardPageHeaderAction,
} from '@aircall/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 (`@dashboard/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 (`@aircall/blocks`):**
```tsx
import {
  DashboardPageHeader,
  DashboardPageHeaderNav,
  DashboardPageHeaderNavBack,
  DashboardPageHeaderTitle,
  DashboardPageHeaderActions,
  DashboardPageHeaderAction,
} from '@aircall/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 (`@dashboard/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 (`@aircall/blocks`):**
```tsx
import {
  DashboardPageHeader,
  DashboardPageHeaderPrefix,
  DashboardPageHeaderTitleGroup,
  DashboardPageHeaderTitle,
  DashboardPageHeaderSubtitle,
  DashboardPageHeaderDescription,
} from '@aircall/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 `@aircall/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 @aircall/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
`@aircall/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 `@dashboard/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`
