@aircall/blocks 0.17.0 → 0.18.1

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.
@@ -2,51 +2,69 @@
2
2
  name: aircall-blocks/migrate-dashboard/tile
3
3
  description: >
4
4
  Migrate @dashboard/library Tile, TileHeader, TileValue (+ TileBody, TileActions,
5
- TileExtra, TileNumber, TileDuration, TileText) to @aircall/ds Card primitives
6
- (Card, CardHeader, CardTitle, CardAction, CardContent). Load when a file imports
7
- Tile, TileHeader, or TileValue from @dashboard/library.
5
+ TileExtra, TileNumber, TileDuration, TileText) to @aircall/blocks KpiCard
6
+ compositions (KpiCard, KpiValue, KpiDescription, KpiValueSkeleton,
7
+ KpiDescriptionSkeleton) plus DS CardHeader / CardTitle / CardContent / CardAction.
8
+ Load when a file imports Tile, TileHeader, or TileValue from @dashboard/library.
8
9
  type: sub-skill
9
10
  library: aircall-blocks
10
- library_version: "0.5.1"
11
11
  requires:
12
12
  - aircall-blocks/setup
13
13
  - aircall-blocks/migrate-dashboard
14
14
  sources:
15
- - "aircall/hydra:packages/ds/src/index.ts"
15
+ - "aircall/hydra:packages/blocks/src/components/kpi.tsx"
16
+ - "aircall/hydra:packages/blocks/src/helpers/format-kpi.ts"
17
+ - "aircall/hydra:packages/ds/src/components/card.tsx"
16
18
  ---
17
19
 
18
20
  This skill builds on aircall-blocks/migrate-dashboard.
19
21
 
20
22
  ## 1. Component mapping
21
23
 
22
- | @dashboard/library | @aircall/ds |
24
+ | @dashboard/library | Target |
23
25
  | --- | --- |
24
- | `Tile` (root container) | `Card` |
25
- | `TileHeader` `label` prop | `CardTitle` inside `CardHeader` |
26
- | `TileHeader` `tooltipTexts` prop | inline `Tooltip` / `TooltipTrigger` / `TooltipContent` next to `CardTitle` |
27
- | `TileBody` (metric row wrapper) | `CardContent` |
28
- | `TileActions` (top-right action overlay) | `CardAction` inside `CardHeader` |
29
- | `TileValue` (number / duration / text display) | inline JSX using `Skeleton` (loading state) + plain markup |
30
- | `TileExtra` `secondaryText` prop | plain `<span>` inside `CardContent` |
31
- | `TileExtra` `trend` prop | custom trend badge no DS equivalent; keep app-side |
32
- | `Tile` `disabled` prop | `data-disabled` attribute on `Card` + `className="opacity-50 cursor-not-allowed"` |
33
- | `TileProvider` (internal context) | removed no equivalent needed |
34
-
35
- `TileValue` has no direct DS replacement — it is a formatting component (locale number, duration, text), not a structural one. Replace it with a plain `<span>` or `<p>` carrying the formatted value, optionally wrapped in a `Skeleton` for the loading state.
36
-
37
- `TILE_VALUE` enum and `TileValueSizes` type are internal to `@dashboard/library`; delete them from the import when migrating.
26
+ | `Tile` (root container) | `KpiCard` (`@aircall/blocks`) — DS `Card` wrapper, `size="sm"` by default |
27
+ | `Tile` `disabled` prop | `KpiCard disabled` applies `opacity-50` + `cursor-not-allowed` and cascades to nested `KpiValue`s |
28
+ | `TileHeader` `label` prop | DS `CardTitle` inside DS `CardHeader` |
29
+ | `TileHeader` `tooltipTexts` prop | inline DS `Tooltip` / `TooltipTrigger` / `TooltipContent` next to `CardTitle` |
30
+ | `TileBody` (metric row wrapper) | DS `CardContent` |
31
+ | `TileActions` (top-right action overlay) | DS `CardAction` inside `CardHeader` |
32
+ | `TileValue` (number / duration / text / percent) | `KpiValue` (`@aircall/blocks`) use `kind`, `durationStyle`, `unit`, `onClick`, `active` |
33
+ | `TileValue loading` | `KpiValueSkeleton` (and `KpiDescriptionSkeleton` when a comparison line loads) |
34
+ | `TileExtra` `secondaryText` prop | `KpiDescription` children |
35
+ | `TileExtra` `trend` prop | `KpiDescription` `trend` + `tone` (`up` / `down` / `neutral` × `positive` / `negative` / `neutral`) |
36
+ | `TileProvider` (internal context) | removed — `KpiCard` provides disabled context |
37
+
38
+ `TILE_VALUE` enum and `TileValueSizes` type are internal to `@dashboard/library`; delete them from the import when migrating. Map former types via `KpiValue` `kind`:
39
+
40
+ | Former `TILE_VALUE` / type | `KpiValue` props |
41
+ | --- | --- |
42
+ | number | `kind="number"` (default for numeric `value`) |
43
+ | percent | `kind="percent"` |
44
+ | duration (long / verbose) | `kind="duration"` (`durationStyle="long"` default) |
45
+ | duration clock HH:MM | `kind="duration" durationStyle="clock" durationScale="hours"` |
46
+ | duration clock MM:SS | `kind="duration" durationStyle="clock" durationScale="minutes"` |
47
+ | opaque string | string `value` (or `kind="text"`) |
38
48
 
39
49
  ## 2. Imports
40
50
 
41
51
  ```tsx
42
- // DS primitives tile shell
52
+ // BlocksKPI composition
43
53
  import {
44
- Card,
54
+ KpiCard,
55
+ KpiValue,
56
+ KpiDescription,
57
+ KpiValueSkeleton,
58
+ KpiDescriptionSkeleton
59
+ } from '@aircall/blocks';
60
+
61
+ // DS — card slots + optional tooltip / actions
62
+ import {
63
+ Button,
45
64
  CardAction,
46
65
  CardContent,
47
66
  CardHeader,
48
67
  CardTitle,
49
- Skeleton,
50
68
  Tooltip,
51
69
  TooltipContent,
52
70
  TooltipProvider,
@@ -54,9 +72,6 @@ import {
54
72
  } from '@aircall/ds';
55
73
  ```
56
74
 
57
- No `@aircall/blocks` import is required for the tile container migration. Blocks are only
58
- needed if the tile sits inside a larger page layout from that package.
59
-
60
75
  ## 3. Before / After
61
76
 
62
77
  ### 3a. Basic tile — number value
@@ -77,29 +92,30 @@ function AnsweredTile() {
77
92
  }
78
93
  ```
79
94
 
80
- **After (`@aircall/ds`):**
95
+ **After (`@aircall/blocks` + `@aircall/ds`):**
81
96
  ```tsx
82
- import { Card, CardContent, CardHeader, CardTitle } from '@aircall/ds';
97
+ import { CardContent, CardHeader, CardTitle } from '@aircall/ds';
98
+ import { KpiCard, KpiValue } from '@aircall/blocks';
83
99
 
84
100
  function AnsweredTile() {
85
101
  return (
86
- <Card className="min-w-36">
102
+ <KpiCard className="min-w-36">
87
103
  <CardHeader>
88
- <CardTitle className="text-xs font-medium text-muted-foreground">Answered</CardTitle>
104
+ <CardTitle>Answered</CardTitle>
89
105
  </CardHeader>
90
106
  <CardContent>
91
- <span className="text-2xl font-bold tabular-nums">123</span>
107
+ <KpiValue value={123} kind="number" />
92
108
  </CardContent>
93
- </Card>
109
+ </KpiCard>
94
110
  );
95
111
  }
96
112
  ```
97
113
 
98
114
  Key changes:
99
- - `Tile` → `Card`. The `minWidth` prop (default 144 px) becomes `className="min-w-36"`.
100
- - `TileHeader label` → `CardTitle` inside `CardHeader`. Apply `text-xs font-medium text-muted-foreground` to match the original `supportingMediumXS` / `text-interactive-neutral` visual style.
101
- - `TileBody` → `CardContent`. Flex alignment was internal; use `className="flex items-center gap-1"` on `CardContent` if you need the row layout.
102
- - `TileValue` → inline `<span>` with the formatted value. The `size="regular"` style maps to `text-2xl font-bold tabular-nums`.
115
+ - `Tile` → `KpiCard`. Optional `minWidth` (default 144 px) becomes `className="min-w-36"`.
116
+ - `TileHeader label` → `CardTitle` inside `CardHeader`.
117
+ - `TileBody` → `CardContent`.
118
+ - `TileValue` → `KpiValue` (formatting + typography owned by the block).
103
119
 
104
120
  ### 3b. Tile with loading state
105
121
 
@@ -119,26 +135,27 @@ function LoadingTile() {
119
135
  }
120
136
  ```
121
137
 
122
- **After (`@aircall/ds`):**
138
+ **After:**
123
139
  ```tsx
124
- import { Card, CardContent, CardHeader, CardTitle, Skeleton } from '@aircall/ds';
140
+ import { CardContent, CardHeader, CardTitle } from '@aircall/ds';
141
+ import { KpiCard, KpiValueSkeleton } from '@aircall/blocks';
125
142
 
126
143
  function LoadingTile() {
127
144
  return (
128
- <Card className="min-w-36">
145
+ <KpiCard className="min-w-36">
129
146
  <CardHeader>
130
- <CardTitle className="text-xs font-medium text-muted-foreground">Answered</CardTitle>
147
+ <CardTitle>Answered</CardTitle>
131
148
  </CardHeader>
132
- <CardContent className="flex h-8 items-center">
133
- <Skeleton className="h-6 w-18" />
149
+ <CardContent>
150
+ <KpiValueSkeleton />
134
151
  </CardContent>
135
- </Card>
152
+ </KpiCard>
136
153
  );
137
154
  }
138
155
  ```
139
156
 
140
- `TileValue loading` rendered a `Skeleton` at a fixed height container (32 px for `size="regular"`).
141
- Replicate with `className="flex h-8 items-center"` on `CardContent` and `className="h-6 w-18"` on `Skeleton`.
157
+ When a comparison line also loads, pair with `KpiDescriptionSkeleton` inside
158
+ `CardContent className="flex flex-col gap-1"`.
142
159
 
143
160
  ### 3c. Tile with tooltip on header
144
161
 
@@ -158,10 +175,9 @@ function InfoTile() {
158
175
  }
159
176
  ```
160
177
 
161
- **After (`@aircall/ds`):**
178
+ **After:**
162
179
  ```tsx
163
180
  import {
164
- Card,
165
181
  CardContent,
166
182
  CardHeader,
167
183
  CardTitle,
@@ -171,16 +187,19 @@ import {
171
187
  TooltipTrigger
172
188
  } from '@aircall/ds';
173
189
  import { InformationCircleIcon } from '@aircall/react-icons';
190
+ import { KpiCard, KpiValue } from '@aircall/blocks';
174
191
 
175
192
  function InfoTile() {
176
193
  return (
177
- <Card className="min-w-36">
194
+ <KpiCard className="min-w-36">
178
195
  <CardHeader>
179
- <CardTitle className="flex items-center gap-1 text-xs font-medium text-muted-foreground">
196
+ <CardTitle className="flex items-center gap-1">
180
197
  Answered
181
198
  <TooltipProvider>
182
199
  <Tooltip>
183
- <TooltipTrigger asChild>
200
+ <TooltipTrigger
201
+ render={<button type="button" className="inline-flex" aria-label="More info" />}
202
+ >
184
203
  <InformationCircleIcon className="h-3 w-3 cursor-default" aria-hidden />
185
204
  </TooltipTrigger>
186
205
  <TooltipContent>Calls answered in the period</TooltipContent>
@@ -189,17 +208,14 @@ function InfoTile() {
189
208
  </CardTitle>
190
209
  </CardHeader>
191
210
  <CardContent>
192
- <span className="text-2xl font-bold tabular-nums">123</span>
211
+ <KpiValue value={123} />
193
212
  </CardContent>
194
- </Card>
213
+ </KpiCard>
195
214
  );
196
215
  }
197
216
  ```
198
217
 
199
- `TileHeader` showed the tooltip icon only on hover (`hovering` from `TileProvider`). The DS
200
- `Tooltip` is hover-driven by default — no extra state needed.
201
-
202
- ### 3d. Tile with actions (TileActions)
218
+ ### 3d. Tile with actions + clickable value
203
219
 
204
220
  **Before (`@dashboard/library`):**
205
221
  ```tsx
@@ -225,50 +241,37 @@ function ActionTile() {
225
241
  }
226
242
  ```
227
243
 
228
- **After (`@aircall/ds`):**
244
+ **After:**
229
245
  ```tsx
230
- import {
231
- Button,
232
- Card,
233
- CardAction,
234
- CardContent,
235
- CardHeader,
236
- CardTitle
237
- } from '@aircall/ds';
238
- import { SettingsIcon, MoreVerticalIcon } from '@aircall/react-icons';
246
+ import { Button, CardAction, CardContent, CardHeader, CardTitle } from '@aircall/ds';
247
+ import { EllipsisVertical, Settings } from '@aircall/react-icons';
248
+ import { KpiCard, KpiValue } from '@aircall/blocks';
239
249
 
240
250
  function ActionTile() {
241
251
  return (
242
- <Card className="min-w-36">
252
+ <KpiCard className="min-w-36">
243
253
  <CardHeader>
244
- <CardTitle className="text-xs font-medium text-muted-foreground">Answered</CardTitle>
254
+ <CardTitle>Answered</CardTitle>
245
255
  <CardAction className="flex items-center gap-1">
246
- <Button variant="ghost" size="icon" aria-label="Settings" onClick={() => null}>
247
- <SettingsIcon className="h-4 w-4" />
256
+ <Button variant="ghost" size="icon-sm" aria-label="Settings" onClick={() => null}>
257
+ <Settings />
248
258
  </Button>
249
- <Button variant="ghost" size="icon" aria-label="More options" onClick={() => null}>
250
- <MoreVerticalIcon className="h-4 w-4" />
259
+ <Button variant="ghost" size="icon-sm" aria-label="More options" onClick={() => null}>
260
+ <EllipsisVertical />
251
261
  </Button>
252
262
  </CardAction>
253
263
  </CardHeader>
254
264
  <CardContent>
255
- <button
256
- type="button"
257
- className="text-2xl font-bold tabular-nums underline decoration-dashed"
258
- onClick={() => null}
259
- >
260
- 123
261
- </button>
265
+ <KpiValue value={123} onClick={() => null} />
262
266
  </CardContent>
263
- </Card>
267
+ </KpiCard>
264
268
  );
265
269
  }
266
270
  ```
267
271
 
268
272
  Key changes:
269
- - `TileActions` → `CardAction` inside `CardHeader`. `CardAction` carries `data-slot="card-action"` and is pinned top-right by the header grid automatically.
270
- - `Gap` + `IconButton` from tractor `Button variant="ghost" size="icon"` from `@aircall/ds`.
271
- - `TileValue onClick` (clickable value) → plain `<button>` element styled to match.
273
+ - `TileActions` → `CardAction` inside `CardHeader`.
274
+ - `TileValue onClick` `KpiValue onClick` (dotted underline; `active` for solid underline).
272
275
 
273
276
  ### 3e. Disabled tile
274
277
 
@@ -279,7 +282,7 @@ import { Tile, TileBody, TileHeader, TileValue } from '@dashboard/library';
279
282
  function DisabledTile() {
280
283
  return (
281
284
  <Tile disabled>
282
- <TileHeader label="Answered" tooltipTexts={{ help: 'Feature disabled' }} />
285
+ <TileHeader label="Answered" />
283
286
  <TileBody>
284
287
  <TileValue onClick={() => null} value={123} />
285
288
  </TileBody>
@@ -288,142 +291,189 @@ function DisabledTile() {
288
291
  }
289
292
  ```
290
293
 
291
- **After (`@aircall/ds`):**
294
+ **After:**
292
295
  ```tsx
293
- import { Card, CardContent, CardHeader, CardTitle } from '@aircall/ds';
296
+ import { CardContent, CardHeader, CardTitle } from '@aircall/ds';
297
+ import { KpiCard, KpiValue } from '@aircall/blocks';
294
298
 
295
299
  function DisabledTile() {
296
300
  return (
297
- <Card className="min-w-36 opacity-50" data-disabled aria-disabled>
301
+ <KpiCard disabled className="min-w-36">
298
302
  <CardHeader>
299
- <CardTitle className="text-xs font-medium text-muted-foreground">Answered</CardTitle>
303
+ <CardTitle>Answered</CardTitle>
300
304
  </CardHeader>
301
305
  <CardContent>
302
- <span className="cursor-not-allowed text-2xl font-bold tabular-nums">123</span>
306
+ <KpiValue value={123} onClick={() => null} />
307
+ </CardContent>
308
+ </KpiCard>
309
+ );
310
+ }
311
+ ```
312
+
313
+ `KpiCard disabled` applies `opacity-50` + `cursor-not-allowed` and cascades so nested
314
+ `KpiValue` buttons cannot be activated — no need to strip `onClick` or style the value by hand.
315
+
316
+ ### 3f. Comparison / trend line (TileExtra)
317
+
318
+ **Before:** `TileExtra` with `secondaryText` + `trend`.
319
+
320
+ **After:**
321
+ ```tsx
322
+ import { CardContent, CardHeader, CardTitle } from '@aircall/ds';
323
+ import { KpiCard, KpiDescription, KpiValue } from '@aircall/blocks';
324
+
325
+ function OutboundCallsTile() {
326
+ return (
327
+ <KpiCard className="min-w-36">
328
+ <CardHeader>
329
+ <CardTitle>Outbound calls</CardTitle>
330
+ </CardHeader>
331
+ <CardContent className="flex flex-col gap-1">
332
+ <KpiValue value={87} onClick={() => null} />
333
+ <KpiDescription trend="up" tone="positive">
334
+ +16.3% vs previous period
335
+ </KpiDescription>
303
336
  </CardContent>
304
- </Card>
337
+ </KpiCard>
305
338
  );
306
339
  }
307
340
  ```
308
341
 
309
- `Tile disabled` set `disabled` on `TileProvider` context, which `TileValue` read to disable its
310
- internal button. Replace by setting `opacity-50` + `aria-disabled` on `Card` and
311
- `cursor-not-allowed` on the value element; remove the `onClick` handler.
342
+ `trend` is icon direction only; `tone` is color only an upward change that is bad uses
343
+ `trend="up" tone="negative"`.
344
+
345
+ ### 3g. Duration values
346
+
347
+ ```tsx
348
+ {/* Long: "1h 2min 3s" */}
349
+ <KpiValue value={3723} kind="duration" />
350
+
351
+ {/* Clock hours: "01:38 hr/min" */}
352
+ <KpiValue value={5880} kind="duration" durationStyle="clock" durationScale="hours" />
353
+
354
+ {/* Clock minutes: "05:04 min/sec" */}
355
+ <KpiValue value={304} kind="duration" durationStyle="clock" durationScale="minutes" />
356
+
357
+ {/* Duration + unit label */}
358
+ <KpiValue value={112} kind="duration" unit="avg" />
359
+ ```
360
+
361
+ Do not reimplement `formatSecondsToDuration` in the app — `KpiValue` owns formatting.
312
362
 
313
363
  ---
314
364
 
315
365
  ## 4. Common mistakes
316
366
 
317
- ### Mistake 1 — Passing Tile/TileBody flex props to Card/CardContent
367
+ ### Mistake 1 — Using raw DS `Card` instead of `KpiCard`
318
368
 
319
369
  ```tsx
320
- // ❌ Wrong — tractor FlexProps (px, py, minWidth, flexDirection…) on Card
321
- <Card minWidth="228px" px="xxs" py="xxs" flexDirection="column">
322
- <CardContent alignItems="center" gap="xxxs">…</CardContent>
370
+ // ❌ Wrong — loses disabled cascade and KPI defaults (size="sm", gap)
371
+ <Card size="sm">
372
+ <CardHeader>
373
+ <CardTitle>Answered</CardTitle>
374
+ </CardHeader>
375
+ <CardContent>
376
+ <KpiValue value={123} />
377
+ </CardContent>
323
378
  </Card>
324
379
 
325
- // ✅ Correct — use className with Tailwind utilities
326
- <Card className="min-w-[228px]">
327
- <CardContent className="flex items-center gap-1">…</CardContent>
328
- </Card>
380
+ // ✅ Correct
381
+ <KpiCard>
382
+ <CardHeader>
383
+ <CardTitle>Answered</CardTitle>
384
+ </CardHeader>
385
+ <CardContent>
386
+ <KpiValue value={123} />
387
+ </CardContent>
388
+ </KpiCard>
329
389
  ```
330
390
 
331
- `Tile` extended `FlexProps` from `@aircall/tractor` (spacing tokens like `px`, `py`, `minWidth`).
332
- `Card` and `CardContent` extend `React.ComponentProps<'div'>` — passing tractor tokens results in
333
- unknown HTML attribute warnings at runtime and no styling effect.
391
+ ### Mistake 2 Hand-formatting values or using raw `Skeleton`
392
+
393
+ ```tsx
394
+ // ❌ Wrong
395
+ <span className="text-3xl font-bold">{formatDuration(123)}</span>
396
+ <Skeleton className="h-6 w-18" />
397
+
398
+ // ✅ Correct
399
+ <KpiValue value={123} kind="duration" />
400
+ <KpiValueSkeleton />
401
+ ```
402
+
403
+ ### Mistake 3 — Passing Tile/TileBody flex props to KpiCard / CardContent
404
+
405
+ ```tsx
406
+ // ❌ Wrong — tractor FlexProps on the card
407
+ <KpiCard minWidth="228px" px="xxs" py="xxs">
408
+ <CardContent alignItems="center" gap="xxxs">…</CardContent>
409
+ </KpiCard>
334
410
 
335
- Source: `packages/ds/src/components/card.tsx`
411
+ // ✅ Correct — Tailwind via className
412
+ <KpiCard className="min-w-[228px]">
413
+ <CardContent className="flex flex-col gap-1">…</CardContent>
414
+ </KpiCard>
415
+ ```
336
416
 
337
- ### Mistake 2 — Placing CardAction outside CardHeader
417
+ ### Mistake 4 — Placing CardAction outside CardHeader
338
418
 
339
419
  ```tsx
340
- // ❌ Wrong — action renders in document flow; loses top-right pinning
341
- <Card>
342
- <CardAction>
343
- <Button variant="ghost" size="icon">…</Button>
344
- </CardAction>
420
+ // ❌ Wrong
421
+ <KpiCard>
422
+ <CardAction>…</CardAction>
345
423
  <CardHeader>
346
424
  <CardTitle>Answered</CardTitle>
347
425
  </CardHeader>
348
426
  <CardContent>…</CardContent>
349
- </Card>
427
+ </KpiCard>
350
428
 
351
429
  // ✅ Correct — CardAction must be a child of CardHeader
352
- <Card>
430
+ <KpiCard>
353
431
  <CardHeader>
354
432
  <CardTitle>Answered</CardTitle>
355
- <CardAction>
356
- <Button variant="ghost" size="icon">…</Button>
357
- </CardAction>
433
+ <CardAction>…</CardAction>
358
434
  </CardHeader>
359
435
  <CardContent>…</CardContent>
360
- </Card>
436
+ </KpiCard>
361
437
  ```
362
438
 
363
- `CardHeader` uses `grid auto-rows-min has-data-[slot=card-action]:grid-cols-[1fr_auto]`.
364
- `CardAction` carries `data-slot="card-action"` and sits at `col-start-2 row-span-2`. Outside
365
- `CardHeader` there is no grid parent; the action renders in document flow at full width.
366
-
367
- Source: `packages/ds/src/components/card.tsx`
368
-
369
- ### Mistake 3 — Importing TILE_VALUE enum or TileValue from @dashboard/library after migration
439
+ ### Mistake 5 Keeping TILE_VALUE / TileValue imports after migration
370
440
 
371
441
  ```tsx
372
- // ❌ Wrong — keeping the enum import after TileValue is removed
442
+ // ❌ Wrong
373
443
  import { TILE_VALUE, Tile, TileHeader, TileValue } from '@dashboard/library';
374
-
375
444
  <TileValue type={TILE_VALUE.DURATION} value={123} />
376
445
 
377
- // ✅ Correct — format the value in the consuming component; no TILE_VALUE needed
378
- import { Card, CardContent, CardHeader, CardTitle } from '@aircall/ds';
379
-
380
- // Duration formatting (seconds → "2m 03s") — implement inline or extract to a util
381
- function formatDuration(seconds: number): string {
382
- const m = Math.floor(seconds / 60);
383
- const s = seconds % 60;
384
- return `${m}m ${String(s).padStart(2, '0')}s`;
385
- }
446
+ // ✅ Correct
447
+ import { KpiCard, KpiValue } from '@aircall/blocks';
448
+ import { CardContent, CardHeader, CardTitle } from '@aircall/ds';
386
449
 
387
- <Card className="min-w-36">
450
+ <KpiCard>
388
451
  <CardHeader>
389
- <CardTitle className="text-xs font-medium text-muted-foreground">AHT</CardTitle>
452
+ <CardTitle>AHT</CardTitle>
390
453
  </CardHeader>
391
454
  <CardContent>
392
- <span className="text-2xl font-bold tabular-nums">{formatDuration(123)}</span>
455
+ <KpiValue value={123} kind="duration" />
393
456
  </CardContent>
394
- </Card>
457
+ </KpiCard>
395
458
  ```
396
459
 
397
- `TileValue` with `type={TILE_VALUE.DURATION}` delegated to `TileDuration`, which formatted
398
- seconds via an internal `formatSecondsToDuration` helper. That helper is not exported from
399
- `@dashboard/library`'s public API. Extract the formatting logic to a local utility function.
400
-
401
- Source: `packages/ds/src/components/card.tsx`
402
-
403
- ### Mistake 4 — Using CardTitle for the metric value instead of the label
460
+ ### Mistake 6 Using CardTitle for the metric value instead of the label
404
461
 
405
462
  ```tsx
406
- // ❌ Wrong — CardTitle used for the big number; label lost
407
- <Card>
463
+ // ❌ Wrong — CardTitle used for the big number
464
+ <KpiCard>
408
465
  <CardHeader>
409
466
  <CardTitle>123</CardTitle>
410
467
  </CardHeader>
411
- </Card>
468
+ </KpiCard>
412
469
 
413
- // ✅ Correct — CardTitle holds the label; value sits in CardContent
414
- <Card className="min-w-36">
470
+ // ✅ Correct — CardTitle = label; KpiValue = metric
471
+ <KpiCard>
415
472
  <CardHeader>
416
- <CardTitle className="text-xs font-medium text-muted-foreground">Answered</CardTitle>
473
+ <CardTitle>Answered</CardTitle>
417
474
  </CardHeader>
418
475
  <CardContent>
419
- <span className="text-2xl font-bold tabular-nums">123</span>
476
+ <KpiValue value={123} />
420
477
  </CardContent>
421
- </Card>
478
+ </KpiCard>
422
479
  ```
423
-
424
- In `@dashboard/library`, `TileHeader` rendered the human-readable label ("Answered") and
425
- `TileValue` rendered the metric number. In the DS mapping, `CardTitle` takes the label and
426
- `CardContent` holds the value element. Swapping them produces an inverted visual hierarchy
427
- and breaks assistive-technology reading order.
428
-
429
- Source: `packages/ds/src/components/card.tsx`
@@ -5,7 +5,6 @@ description: >
5
5
  Load when a file imports ToggleRow from @dashboard/library.
6
6
  type: sub-skill
7
7
  library: aircall-blocks
8
- library_version: "0.6.0"
9
8
  requires:
10
9
  - aircall-blocks/setup
11
10
  - aircall-blocks/migrate-dashboard
@@ -9,7 +9,6 @@ description: >
9
9
  consumers where the host already loads DS globals.
10
10
  type: core
11
11
  library: aircall-blocks
12
- library_version: "0.5.1"
13
12
  requires:
14
13
  - aircall-ds/setup
15
14
  sources:
@@ -58,6 +57,28 @@ import '@aircall/ds/globals.css';
58
57
  import '@aircall/blocks/globals.css';
59
58
  ```
60
59
 
60
+ #### Single-compilation via `theme.css` (recommended for DS + blocks)
61
+
62
+ The two precompiled bundles above each re-emit the same Tailwind utilities into `@layer
63
+ utilities`; at equal specificity the last-loaded bundle can clobber the other's responsive
64
+ variants (e.g. blocks' plain `.text-base` beating DS's `md:text-sm` on `Textarea`), and **no
65
+ import order fixes it**. To avoid this, import the directive-preserving `theme.css` artifacts
66
+ (tokens + dark variant + reset + keyframes only — **no** Preflight, **no** utilities) and let a
67
+ single Tailwind pass generate the utilities by `@source`-scanning the library dist:
68
+
69
+ ```css
70
+ /* style.css */
71
+ @import 'tailwindcss'; /* consumer owns the single Preflight + engine */
72
+ @import '@aircall/ds/theme.css';
73
+ @import '@aircall/blocks/theme.css';
74
+ @source '../node_modules/@aircall/ds/dist/index.js';
75
+ @source '../node_modules/@aircall/blocks/dist/index.js';
76
+ ```
77
+
78
+ Here the full `@import 'tailwindcss'` is **correct** (unlike the `globals.css` case) because
79
+ `theme.css` ships no Preflight/utilities. `globals.css` still ships unchanged for precompiled-
80
+ bundle consumers. Full rationale: `packages/ds/docs/single-compilation-theme-css.md`.
81
+
61
82
  ### Module-federation consumer (host already owns the Preflight)
62
83
 
63
84
  When your app is a remote loaded inside a host that already imports DS globals