@aircall/blocks 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,260 @@
1
+ ---
2
+ name: aircall-blocks/migrate-dashboard/loading
3
+ description: >
4
+ Migrate @dashboard/library Loading, Loader, and Spinner to @aircall/ds
5
+ Spinner (animated spinner) and Skeleton (content placeholder). Load when a
6
+ file imports Loading, Loader, or Spinner from @dashboard/library.
7
+ type: sub-skill
8
+ library: aircall-blocks
9
+ library_version: "0.5.1"
10
+ requires:
11
+ - aircall-blocks/setup
12
+ - aircall-blocks/migrate-dashboard
13
+ sources:
14
+ - "aircall/hydra:packages/ds/src/index.ts"
15
+ ---
16
+
17
+ This skill builds on aircall-blocks/migrate-dashboard.
18
+
19
+ ## 1. Component mapping
20
+
21
+ | `@dashboard/library` | `@aircall/ds` | Notes |
22
+ | --- | --- | --- |
23
+ | `Loading` | `Spinner` | Full-area centered spinner — replace the flex wrapper with a parent `div` using Tailwind layout classes |
24
+ | `Loader` | `Spinner` | Inline spinning icon — direct 1-to-1 replacement |
25
+ | `Spinner` | `Spinner` | Already a spinner — direct 1-to-1 replacement |
26
+
27
+ **Content placeholders:** when `Loading` was used while async content loads (e.g. inside a card or list panel), prefer `Skeleton` over `Spinner` — it better communicates that content is incoming rather than a blocking operation.
28
+
29
+ ## 2. Verified DS exports (`packages/ds/src/index.ts`)
30
+
31
+ ```
32
+ Spinner, spinnerVariants
33
+ Skeleton
34
+ ```
35
+
36
+ ## 3. Prop mapping
37
+
38
+ ### `Loading` → `Spinner`
39
+
40
+ | `@dashboard/library` prop | DS equivalent | Action |
41
+ | --- | --- | --- |
42
+ | `size` (number, e.g. `40`) | `size` (`"sm"` \| `"default"` \| `"lg"` \| `"xl"`) | Map to the nearest token (see table below) |
43
+ | `color` (tractor color token, e.g. `"primary-500"`) | `className` | Use a Tailwind text-color class |
44
+ | `data-test` | `data-test` | Pass through as-is |
45
+ | Flex layout (`FlexProps`) | Tailwind parent | Wrap `Spinner` in a container `<div className="flex h-full w-full items-center justify-center">` |
46
+
47
+ **Size mapping:**
48
+
49
+ | Old `size` (px) | New `size` token |
50
+ | --- | --- |
51
+ | ≤ 12 | `"sm"` (12 px) |
52
+ | 13–16 | `"default"` (16 px) |
53
+ | 17–20 | `"lg"` (20 px) |
54
+ | ≥ 21 | `"xl"` (24 px) |
55
+
56
+ When `size` was not set, `Loading` defaulted to `40` and `Spinner` defaulted to `30` — both map to `"xl"`.
57
+
58
+ ### `Loader` → `Spinner`
59
+
60
+ | `@dashboard/library` prop | DS equivalent | Action |
61
+ | --- | --- | --- |
62
+ | `size` (number) | `size` (token string) | Map using the table above |
63
+ | `color` | `className` | Use a Tailwind text-color class |
64
+ | `data-test` | `data-test` | Pass through as-is |
65
+
66
+ ### `Skeleton`
67
+
68
+ `Skeleton` has no direct `@dashboard/library` predecessor — it is introduced as a
69
+ replacement for `Loading` when used during content fetch. It accepts a plain `<div>`
70
+ `className` for sizing and shape.
71
+
72
+ ## 4. Before / After examples
73
+
74
+ ### 4a. `Loading` (full-area centered spinner)
75
+
76
+ **Before (`@dashboard/library`):**
77
+ ```tsx
78
+ import { Loading } from '@dashboard/library';
79
+
80
+ <Loading data-test="reports-loading" size={40} color="primary-500" />
81
+ ```
82
+
83
+ **After (`@aircall/ds`):**
84
+ ```tsx
85
+ import { Spinner } from '@aircall/ds';
86
+
87
+ <div
88
+ data-test="reports-loading"
89
+ className="flex h-full w-full items-center justify-center"
90
+ >
91
+ <Spinner size="xl" className="text-primary" />
92
+ </div>
93
+ ```
94
+
95
+ Key changes:
96
+ - The flex-center wrapper that `Loading` rendered internally must now be an explicit parent `<div>`.
97
+ - Numeric `size` becomes a size token string.
98
+ - Tractor color token (`"primary-500"`) becomes a Tailwind text-color class (`"text-primary"`).
99
+
100
+ ### 4b. `Loader` (inline spinner icon)
101
+
102
+ **Before (`@dashboard/library`):**
103
+ ```tsx
104
+ import { Loader } from '@dashboard/library';
105
+
106
+ <Loader size={20} color="neutral-700" />
107
+ ```
108
+
109
+ **After (`@aircall/ds`):**
110
+ ```tsx
111
+ import { Spinner } from '@aircall/ds';
112
+
113
+ <Spinner size="lg" className="text-neutral-700" />
114
+ ```
115
+
116
+ `Loader` was already a bare spinning icon with no layout wrapper — `Spinner` is the direct replacement.
117
+
118
+ ### 4c. `Spinner` (bare spinning icon)
119
+
120
+ **Before (`@dashboard/library`):**
121
+ ```tsx
122
+ import { Spinner } from '@dashboard/library';
123
+
124
+ <Spinner size={30} />
125
+ ```
126
+
127
+ **After (`@aircall/ds`):**
128
+ ```tsx
129
+ import { Spinner } from '@aircall/ds';
130
+
131
+ <Spinner size="xl" />
132
+ ```
133
+
134
+ The API is nearly identical; only the `size` type changes from a numeric pixel value to a string token.
135
+
136
+ ### 4d. `Loading` as a content placeholder (prefer `Skeleton`)
137
+
138
+ **Before (`@dashboard/library`):**
139
+ ```tsx
140
+ import { Loading } from '@dashboard/library';
141
+
142
+ function StatsCard({ isLoading, value }) {
143
+ return (
144
+ <div className="p-4">
145
+ {isLoading ? <Loading size={24} /> : <span>{value}</span>}
146
+ </div>
147
+ );
148
+ }
149
+ ```
150
+
151
+ **After (`@aircall/ds`):**
152
+ ```tsx
153
+ import { Skeleton } from '@aircall/ds';
154
+
155
+ function StatsCard({ isLoading, value }) {
156
+ return (
157
+ <div className="p-4">
158
+ {isLoading ? <Skeleton className="h-6 w-24" /> : <span>{value}</span>}
159
+ </div>
160
+ );
161
+ }
162
+ ```
163
+
164
+ Use `Skeleton` when the spinner was filling the space of specific content that will
165
+ appear once data loads. `Skeleton` communicates the shape of the incoming content, which
166
+ reduces layout shift and improves perceived performance compared to a generic spinner.
167
+
168
+ ## 5. Common Mistakes
169
+
170
+ ### Mistake 1 — Keeping the `Loading` flex wrapper implicit
171
+
172
+ ```tsx
173
+ // WRONG — Spinner has no built-in flex-center layout; it renders as an inline SVG
174
+ import { Spinner } from '@aircall/ds';
175
+
176
+ <Spinner size="xl" className="h-full w-full" />
177
+
178
+ // CORRECT — wrap in an explicit flex container to center in the available area
179
+ import { Spinner } from '@aircall/ds';
180
+
181
+ <div className="flex h-full w-full items-center justify-center">
182
+ <Spinner size="xl" />
183
+ </div>
184
+ ```
185
+
186
+ `Loading` rendered its own `<Flex … alignItems="center" justifyContent="center">` internally. DS `Spinner` is a bare SVG icon; the centering layout must be provided by the caller.
187
+
188
+ Source: `packages/ds/src/components/spinner.tsx`
189
+
190
+ ---
191
+
192
+ ### Mistake 2 — Passing a numeric pixel value to `size`
193
+
194
+ ```tsx
195
+ // WRONG — size accepts only the four named tokens; a number is a type error
196
+ import { Spinner } from '@aircall/ds';
197
+
198
+ <Spinner size={30} />
199
+
200
+ // CORRECT — use the nearest token string
201
+ import { Spinner } from '@aircall/ds';
202
+
203
+ <Spinner size="xl" />
204
+ ```
205
+
206
+ `Spinner.Props` extends `VariantProps<typeof spinnerVariants>` where `size` is
207
+ `"sm" | "default" | "lg" | "xl" | null | undefined`. Passing a number does not satisfy
208
+ the type and will error at compile time.
209
+
210
+ Source: `packages/ds/src/components/spinner.tsx`
211
+
212
+ ---
213
+
214
+ ### Mistake 3 — Passing a Tractor color token to `className`
215
+
216
+ ```tsx
217
+ // WRONG — Tractor color tokens are not valid Tailwind classes and produce no styling
218
+ import { Spinner } from '@aircall/ds';
219
+
220
+ <Spinner className="primary-500" />
221
+
222
+ // CORRECT — use a Tailwind text-color utility class
223
+ import { Spinner } from '@aircall/ds';
224
+
225
+ <Spinner className="text-primary" />
226
+ ```
227
+
228
+ `Spinner` renders a `Loader2Icon` SVG that inherits its color from the CSS `currentColor`
229
+ (the `text-current` Tailwind class is baked in via `spinnerVariants`). To tint it, apply
230
+ a `text-*` Tailwind class — a bare Tractor token string like `"primary-500"` is not a
231
+ recognized utility and has no effect.
232
+
233
+ Source: `packages/ds/src/components/spinner.tsx`
234
+
235
+ ---
236
+
237
+ ### Mistake 4 — Using `Spinner` for content-area placeholders instead of `Skeleton`
238
+
239
+ ```tsx
240
+ // WRONG — a spinner implies an active blocking operation, not an inline content gap
241
+ import { Spinner } from '@aircall/ds';
242
+
243
+ function UserName({ isLoading, name }) {
244
+ return isLoading ? <Spinner size="sm" /> : <span>{name}</span>;
245
+ }
246
+
247
+ // CORRECT — use Skeleton to represent the shape of the incoming content
248
+ import { Skeleton } from '@aircall/ds';
249
+
250
+ function UserName({ isLoading, name }) {
251
+ return isLoading ? <Skeleton className="h-4 w-32" /> : <span>{name}</span>;
252
+ }
253
+ ```
254
+
255
+ `Skeleton` (an animated pulse `<div>`) is semantically and visually correct for
256
+ inline content placeholders because it mirrors the dimensions of the element it replaces.
257
+ `Spinner` is intended for full-area or button loading states where the action source is
258
+ explicit.
259
+
260
+ Source: `packages/ds/src/components/skeleton.tsx`
@@ -0,0 +1,394 @@
1
+ ---
2
+ name: aircall-blocks/migrate-dashboard/page-header
3
+ description: >
4
+ Migrate @dashboard/library PageHeader, PageHeaderLink, and GaramondTitleTypography
5
+ to @aircall/blocks DashboardPageHeader and its sub-components (DashboardPageHeaderTitle,
6
+ DashboardPageHeaderActions, DashboardPageHeaderAction, DashboardPageHeaderNav,
7
+ DashboardPageHeaderNavBack, DashboardPageHeaderPrefix, DashboardPageHeaderTitleGroup,
8
+ DashboardPageHeaderSubtitle, DashboardPageHeaderDescription). Load when a file
9
+ imports PageHeader or PageHeaderLink from @dashboard/library.
10
+ type: sub-skill
11
+ library: aircall-blocks
12
+ library_version: "0.5.1"
13
+ requires:
14
+ - aircall-blocks/setup
15
+ - aircall-blocks/migrate-dashboard
16
+ sources:
17
+ - "aircall/hydra:packages/blocks/src/index.ts"
18
+ ---
19
+
20
+ This skill builds on aircall-blocks/migrate-dashboard.
21
+
22
+ ## 1. Component mapping
23
+
24
+ | @dashboard/library | @aircall/blocks |
25
+ | --- | --- |
26
+ | `PageHeader` (root container) | `DashboardPageHeader` |
27
+ | `PageHeader` `title` prop (small heading) | `DashboardPageHeaderTitle size="sm"` |
28
+ | `PageHeader` `largeTitle` prop (large heading) | `DashboardPageHeaderTitle size="lg"` |
29
+ | `PageHeader` `subtitle` prop | `DashboardPageHeaderDescription` inside `DashboardPageHeaderSubtitle` inside `DashboardPageHeaderTitleGroup` |
30
+ | `PageHeader` `renderTitleRight` render prop | `DashboardPageHeaderActions` + `DashboardPageHeaderAction` |
31
+ | `PageHeader` `renderExtra` render prop | Extra children appended directly inside `DashboardPageHeader` |
32
+ | `PageHeader` `renderSubLeft` / `renderSubRight` render props | Children inside `DashboardPageHeaderSubtitle` |
33
+ | `PageHeader` `gobackLinkRoute` + `gobackLinkText` props | `DashboardPageHeaderNav` + `DashboardPageHeaderNavBack` |
34
+ | `PageHeaderLink` (standalone back-link component) | `DashboardPageHeaderNavBack` inside `DashboardPageHeaderNav` |
35
+ | `PageHeader` `icon` prop (tractor `Icon` component) | `DashboardPageHeaderPrefix` (put icon/flag/avatar inside) |
36
+ | `PageHeader` `avatarSrc` / `avatarInitials` / `renderAvatar` props | `DashboardPageHeaderPrefix` (put `Avatar` or equivalent inside) |
37
+ | `PageHeader` `activeTabId` / `onTabChange` / `Tab.*` children | No direct equivalent — implement tabs separately below the header |
38
+ | `GaramondTitleTypography` / `useGaramondFont` prop | Not reproduced — use `DashboardPageHeaderTitle size="lg"` (system font) |
39
+ | `PageHeader` `titleProps` (BoxProps spread) | Remove — `DashboardPageHeaderTitleGroup` handles layout |
40
+
41
+ `DashboardPageHeader` is a flat-children composition: sub-components self-place via CSS
42
+ grid areas — declaration order determines render order within each area. You do **not** pass
43
+ data via props on the root; every piece of content is a named sub-component child.
44
+
45
+ ## 2. Imports
46
+
47
+ ```tsx
48
+ // All DashboardPageHeader parts from @aircall/blocks
49
+ import {
50
+ DashboardPageHeader,
51
+ DashboardPageHeaderTitle,
52
+ DashboardPageHeaderTitleGroup,
53
+ DashboardPageHeaderSubtitle,
54
+ DashboardPageHeaderDescription,
55
+ DashboardPageHeaderActions,
56
+ DashboardPageHeaderAction,
57
+ DashboardPageHeaderNav,
58
+ DashboardPageHeaderNavBack,
59
+ DashboardPageHeaderPrefix,
60
+ } from '@aircall/blocks';
61
+
62
+ // DS primitives used alongside (Tooltip, DropdownMenu, etc.)
63
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@aircall/ds';
64
+ ```
65
+
66
+ ## 3. Before / After
67
+
68
+ ### 3a. Simple title-only header
69
+
70
+ **Before (`@dashboard/library`):**
71
+ ```tsx
72
+ import { PageHeader } from '@dashboard/library';
73
+
74
+ function AnalyticsHeader() {
75
+ return <PageHeader largeTitle="Analytics" />;
76
+ }
77
+ ```
78
+
79
+ **After (`@aircall/blocks`):**
80
+ ```tsx
81
+ import { DashboardPageHeader, DashboardPageHeaderTitle } from '@aircall/blocks';
82
+
83
+ function AnalyticsHeader() {
84
+ return (
85
+ <DashboardPageHeader>
86
+ <DashboardPageHeaderTitle size="lg">Analytics</DashboardPageHeaderTitle>
87
+ </DashboardPageHeader>
88
+ );
89
+ }
90
+ ```
91
+
92
+ Key changes:
93
+ - `largeTitle` string prop → `DashboardPageHeaderTitle size="lg"` child with the string as children.
94
+ - `title` string prop → `DashboardPageHeaderTitle size="sm"` (level-2 pages that have a nav row).
95
+ - No `data-test` prop — set `data-test` directly on `DashboardPageHeader` if needed.
96
+
97
+ ---
98
+
99
+ ### 3b. Title with actions (renderTitleRight)
100
+
101
+ **Before (`@dashboard/library`):**
102
+ ```tsx
103
+ import { PageHeader } from '@dashboard/library';
104
+ import { Button } from '@aircall/tractor';
105
+
106
+ function CampaignsHeader() {
107
+ return (
108
+ <PageHeader
109
+ largeTitle="Campaigns"
110
+ renderTitleRight={() => (
111
+ <Button variant="primary" size="regular">Create campaign</Button>
112
+ )}
113
+ />
114
+ );
115
+ }
116
+ ```
117
+
118
+ **After (`@aircall/blocks`):**
119
+ ```tsx
120
+ import {
121
+ DashboardPageHeader,
122
+ DashboardPageHeaderTitle,
123
+ DashboardPageHeaderActions,
124
+ DashboardPageHeaderAction,
125
+ } from '@aircall/blocks';
126
+
127
+ function CampaignsHeader() {
128
+ return (
129
+ <DashboardPageHeader>
130
+ <DashboardPageHeaderTitle size="lg">Campaigns</DashboardPageHeaderTitle>
131
+ <DashboardPageHeaderActions>
132
+ <DashboardPageHeaderAction variant="default">Create campaign</DashboardPageHeaderAction>
133
+ </DashboardPageHeaderActions>
134
+ </DashboardPageHeader>
135
+ );
136
+ }
137
+ ```
138
+
139
+ Key changes:
140
+ - `renderTitleRight` render prop → `DashboardPageHeaderActions` child containing `DashboardPageHeaderAction` elements.
141
+ - Tractor `Button variant="primary"` → `DashboardPageHeaderAction variant="default"` (size is fixed by the component).
142
+ - Icon-only action buttons: `DashboardPageHeaderAction variant="outline" size="icon-lg"`.
143
+
144
+ ---
145
+
146
+ ### 3c. Back navigation (gobackLinkRoute / gobackLinkText)
147
+
148
+ **Before (`@dashboard/library`):**
149
+ ```tsx
150
+ import { PageHeader } from '@dashboard/library';
151
+
152
+ function NumberDetailHeader() {
153
+ return (
154
+ <PageHeader
155
+ title="Number settings"
156
+ gobackLinkRoute="/numbers"
157
+ gobackLinkText="Back to numbers"
158
+ renderTitleRight={() => <button>Save</button>}
159
+ />
160
+ );
161
+ }
162
+ ```
163
+
164
+ **After (`@aircall/blocks`):**
165
+ ```tsx
166
+ import {
167
+ DashboardPageHeader,
168
+ DashboardPageHeaderNav,
169
+ DashboardPageHeaderNavBack,
170
+ DashboardPageHeaderTitle,
171
+ DashboardPageHeaderActions,
172
+ DashboardPageHeaderAction,
173
+ } from '@aircall/blocks';
174
+ import { Link } from 'react-router-dom';
175
+
176
+ function NumberDetailHeader() {
177
+ return (
178
+ <DashboardPageHeader>
179
+ <DashboardPageHeaderNav>
180
+ <DashboardPageHeaderNavBack render={<Link to="/numbers" />}>
181
+ Back to numbers
182
+ </DashboardPageHeaderNavBack>
183
+ </DashboardPageHeaderNav>
184
+ <DashboardPageHeaderTitle size="sm">Number settings</DashboardPageHeaderTitle>
185
+ <DashboardPageHeaderActions>
186
+ <DashboardPageHeaderAction variant="default">Save</DashboardPageHeaderAction>
187
+ </DashboardPageHeaderActions>
188
+ </DashboardPageHeader>
189
+ );
190
+ }
191
+ ```
192
+
193
+ Key changes:
194
+ - `gobackLinkRoute` + `gobackLinkText` props → `DashboardPageHeaderNav` > `DashboardPageHeaderNavBack` with `render={<Link to="…" />}` (Base UI render prop — avoids a nested `<a><button>` violation).
195
+ - When nav is present, use `size="sm"` on `DashboardPageHeaderTitle` (level-2 pattern).
196
+ - The `PageHeaderLink` standalone export is replaced the same way.
197
+
198
+ ---
199
+
200
+ ### 3d. Header with prefix (icon / avatar) and subtitle
201
+
202
+ **Before (`@dashboard/library`):**
203
+ ```tsx
204
+ import { PageHeader } from '@dashboard/library';
205
+ import { PhoneOutlined } from '@aircall/icons';
206
+
207
+ function NumberHeader({ number, description }) {
208
+ return (
209
+ <PageHeader
210
+ largeTitle={number.name}
211
+ subtitle={description}
212
+ icon={PhoneOutlined}
213
+ renderSubRight={() => <span>{number.countryCode}</span>}
214
+ />
215
+ );
216
+ }
217
+ ```
218
+
219
+ **After (`@aircall/blocks`):**
220
+ ```tsx
221
+ import {
222
+ DashboardPageHeader,
223
+ DashboardPageHeaderPrefix,
224
+ DashboardPageHeaderTitleGroup,
225
+ DashboardPageHeaderTitle,
226
+ DashboardPageHeaderSubtitle,
227
+ DashboardPageHeaderDescription,
228
+ } from '@aircall/blocks';
229
+ import { Phone } from '@aircall/react-icons';
230
+
231
+ function NumberHeader({ number, description }) {
232
+ return (
233
+ <DashboardPageHeader>
234
+ <DashboardPageHeaderPrefix>
235
+ <Phone className="size-5 text-muted-foreground" />
236
+ </DashboardPageHeaderPrefix>
237
+ <DashboardPageHeaderTitleGroup>
238
+ <DashboardPageHeaderTitle size="lg">{number.name}</DashboardPageHeaderTitle>
239
+ <DashboardPageHeaderSubtitle>
240
+ <DashboardPageHeaderDescription>{description}</DashboardPageHeaderDescription>
241
+ <span className="text-sm text-muted-foreground">{number.countryCode}</span>
242
+ </DashboardPageHeaderSubtitle>
243
+ </DashboardPageHeaderTitleGroup>
244
+ </DashboardPageHeader>
245
+ );
246
+ }
247
+ ```
248
+
249
+ Key changes:
250
+ - `icon` prop → `DashboardPageHeaderPrefix` containing the icon element; icons come from `@aircall/react-icons`, not `@aircall/icons`.
251
+ - `subtitle` prop → `DashboardPageHeaderDescription` inside `DashboardPageHeaderSubtitle` inside `DashboardPageHeaderTitleGroup`.
252
+ - `renderSubLeft` / `renderSubRight` → siblings of `DashboardPageHeaderDescription` inside `DashboardPageHeaderSubtitle`.
253
+ - When `DashboardPageHeaderPrefix` is present, `DashboardPageHeaderTitleGroup` resets its left padding automatically (CSS group selector).
254
+ - `avatarSrc` / `avatarInitials` / `renderAvatar` → render your avatar element inside `DashboardPageHeaderPrefix`.
255
+
256
+ ---
257
+
258
+ ### 3e. Tabs (activeTabId / onTabChange / Tab.Menu children)
259
+
260
+ `PageHeader` accepted `Tab.Menu` / `Tab.Content` children and wired them into a
261
+ `Tab.Container`. `DashboardPageHeader` has no built-in tab support.
262
+
263
+ Pattern: render a DS `Tabs` compound below `DashboardPageHeader`, outside of it.
264
+
265
+ ```tsx
266
+ // Before
267
+ import { PageHeader, Tab } from '@dashboard/library';
268
+ <PageHeader largeTitle="Reports" activeTabId="overview" onTabChange={setTab}>
269
+ <Tab.Menu tabId="overview">Overview</Tab.Menu>
270
+ <Tab.Menu tabId="calls">Calls</Tab.Menu>
271
+ <Tab.Content tabId="overview"><Overview /></Tab.Content>
272
+ <Tab.Content tabId="calls"><Calls /></Tab.Content>
273
+ </PageHeader>
274
+
275
+ // After — use DS Tabs below the header
276
+ import { DashboardPageHeader, DashboardPageHeaderTitle } from '@aircall/blocks';
277
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@aircall/ds';
278
+
279
+ <DashboardPageHeader>
280
+ <DashboardPageHeaderTitle size="lg">Reports</DashboardPageHeaderTitle>
281
+ </DashboardPageHeader>
282
+ <Tabs value={tab} onValueChange={setTab}>
283
+ <TabsList>
284
+ <TabsTrigger value="overview">Overview</TabsTrigger>
285
+ <TabsTrigger value="calls">Calls</TabsTrigger>
286
+ </TabsList>
287
+ <TabsContent value="overview"><Overview /></TabsContent>
288
+ <TabsContent value="calls"><Calls /></TabsContent>
289
+ </Tabs>
290
+ ```
291
+
292
+ ---
293
+
294
+ ## 4. Common mistakes
295
+
296
+ ### Mistake 1 — Passing title/largeTitle as props instead of children
297
+
298
+ ```tsx
299
+ // ❌ Wrong — PageHeader prop API; DashboardPageHeader ignores unknown props
300
+ <DashboardPageHeader largeTitle="Analytics" title="Overview" />
301
+
302
+ // ✅ Correct — title is a child component
303
+ <DashboardPageHeader>
304
+ <DashboardPageHeaderTitle size="lg">Analytics</DashboardPageHeaderTitle>
305
+ </DashboardPageHeader>
306
+ ```
307
+
308
+ `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.
309
+
310
+ Source: `packages/blocks/src/components/dashboard-page-header.tsx`
311
+
312
+ ### Mistake 2 — Using a Router Link directly instead of the `render` prop on DashboardPageHeaderNavBack
313
+
314
+ ```tsx
315
+ // ❌ Wrong — nests <a> inside <button>, invalid HTML
316
+ <DashboardPageHeaderNavBack>
317
+ <Link to="/numbers">Back to numbers</Link>
318
+ </DashboardPageHeaderNavBack>
319
+
320
+ // ✅ Correct — render prop replaces the <button> element with the Link
321
+ <DashboardPageHeaderNavBack render={<Link to="/numbers" />}>
322
+ Back to numbers
323
+ </DashboardPageHeaderNavBack>
324
+ ```
325
+
326
+ `DashboardPageHeaderNavBack` renders a `Button` (which renders a `<button>`). Nesting a
327
+ `<Link>` (`<a>`) inside a `<button>` is invalid HTML and breaks keyboard navigation.
328
+ The `render` prop (Base UI pattern) polymorphically replaces the root element while
329
+ keeping all button behavior.
330
+
331
+ Source: `packages/blocks/src/components/dashboard-page-header.tsx`
332
+
333
+ ### Mistake 3 — Placing DashboardPageHeaderActions outside DashboardPageHeader
334
+
335
+ ```tsx
336
+ // ❌ Wrong — outside the @container context; grid-area:actions has no effect
337
+ <DashboardPageHeader>
338
+ <DashboardPageHeaderTitle size="lg">Analytics</DashboardPageHeaderTitle>
339
+ </DashboardPageHeader>
340
+ <DashboardPageHeaderActions>
341
+ <DashboardPageHeaderAction variant="default">Export</DashboardPageHeaderAction>
342
+ </DashboardPageHeaderActions>
343
+
344
+ // ✅ Correct — all sub-components are children of DashboardPageHeader
345
+ <DashboardPageHeader>
346
+ <DashboardPageHeaderTitle size="lg">Analytics</DashboardPageHeaderTitle>
347
+ <DashboardPageHeaderActions>
348
+ <DashboardPageHeaderAction variant="default">Export</DashboardPageHeaderAction>
349
+ </DashboardPageHeaderActions>
350
+ </DashboardPageHeader>
351
+ ```
352
+
353
+ Sub-components use `[grid-area:actions]` Tailwind classes that only resolve inside the
354
+ CSS grid established by `DashboardPageHeader`'s inner grid `div`. Rendered outside, they
355
+ appear in normal document flow and lose their placement entirely.
356
+
357
+ Source: `packages/blocks/src/components/dashboard-page-header.tsx`
358
+
359
+ ### Mistake 4 — Importing icons from @aircall/icons instead of @aircall/react-icons
360
+
361
+ ```tsx
362
+ // ❌ Wrong — @aircall/icons is the old tractor-era icon package
363
+ import { PhoneOutlined } from '@aircall/icons';
364
+
365
+ // ✅ Correct — @aircall/react-icons is the current icon package
366
+ import { Phone } from '@aircall/react-icons';
367
+ ```
368
+
369
+ `DashboardPageHeader` and the DS components it composes (`Button`, etc.) are built for
370
+ `@aircall/react-icons` (lucide-based). The `@aircall/icons` package ships SVG components
371
+ with a different size/color API (`size` / `color` props vs `className`).
372
+
373
+ Source: `packages/blocks/src/components/dashboard-page-header.tsx`
374
+
375
+ ### Mistake 5 — Using GaramondTitleTypography / useGaramondFont in the new stack
376
+
377
+ ```tsx
378
+ // ❌ Wrong — GaramondTitleTypography and useGaramondFont are @dashboard/library internals
379
+ import { GaramondTitleTypography } from '@dashboard/library';
380
+ <GaramondTitleTypography>{title}</GaramondTitleTypography>
381
+
382
+ // ❌ Wrong — no equivalent prop on DashboardPageHeaderTitle
383
+ <DashboardPageHeaderTitle size="lg" useGaramondFont>Analytics</DashboardPageHeaderTitle>
384
+
385
+ // ✅ Correct — use DashboardPageHeaderTitle with size="lg" (system font, trimmed baseline)
386
+ <DashboardPageHeaderTitle size="lg">Analytics</DashboardPageHeaderTitle>
387
+ ```
388
+
389
+ `GaramondTitleTypography` and `useGaramondFont` were `@dashboard/library`-only overrides
390
+ for the ITC Garamond Narrow font. `DashboardPageHeaderTitle size="lg"` renders `text-3xl
391
+ font-bold` in the system font stack with `leading-10`, matching the design spec without
392
+ a custom typeface dependency.
393
+
394
+ Source: `packages/blocks/src/components/dashboard-page-header.tsx`