@aircall/blocks 0.18.0 → 0.19.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.
|
@@ -2,50 +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/
|
|
6
|
-
(
|
|
7
|
-
|
|
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
11
|
requires:
|
|
11
12
|
- aircall-blocks/setup
|
|
12
13
|
- aircall-blocks/migrate-dashboard
|
|
13
14
|
sources:
|
|
14
|
-
- "aircall/hydra:packages/
|
|
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"
|
|
15
18
|
---
|
|
16
19
|
|
|
17
20
|
This skill builds on aircall-blocks/migrate-dashboard.
|
|
18
21
|
|
|
19
22
|
## 1. Component mapping
|
|
20
23
|
|
|
21
|
-
| @dashboard/library |
|
|
24
|
+
| @dashboard/library | Target |
|
|
22
25
|
| --- | --- |
|
|
23
|
-
| `Tile` (root container) | `Card` |
|
|
24
|
-
| `
|
|
25
|
-
| `TileHeader` `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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"`) |
|
|
37
48
|
|
|
38
49
|
## 2. Imports
|
|
39
50
|
|
|
40
51
|
```tsx
|
|
41
|
-
//
|
|
52
|
+
// Blocks — KPI composition
|
|
42
53
|
import {
|
|
43
|
-
|
|
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,
|
|
44
64
|
CardAction,
|
|
45
65
|
CardContent,
|
|
46
66
|
CardHeader,
|
|
47
67
|
CardTitle,
|
|
48
|
-
Skeleton,
|
|
49
68
|
Tooltip,
|
|
50
69
|
TooltipContent,
|
|
51
70
|
TooltipProvider,
|
|
@@ -53,9 +72,6 @@ import {
|
|
|
53
72
|
} from '@aircall/ds';
|
|
54
73
|
```
|
|
55
74
|
|
|
56
|
-
No `@aircall/blocks` import is required for the tile container migration. Blocks are only
|
|
57
|
-
needed if the tile sits inside a larger page layout from that package.
|
|
58
|
-
|
|
59
75
|
## 3. Before / After
|
|
60
76
|
|
|
61
77
|
### 3a. Basic tile — number value
|
|
@@ -76,29 +92,30 @@ function AnsweredTile() {
|
|
|
76
92
|
}
|
|
77
93
|
```
|
|
78
94
|
|
|
79
|
-
**After (`@aircall/ds`):**
|
|
95
|
+
**After (`@aircall/blocks` + `@aircall/ds`):**
|
|
80
96
|
```tsx
|
|
81
|
-
import {
|
|
97
|
+
import { CardContent, CardHeader, CardTitle } from '@aircall/ds';
|
|
98
|
+
import { KpiCard, KpiValue } from '@aircall/blocks';
|
|
82
99
|
|
|
83
100
|
function AnsweredTile() {
|
|
84
101
|
return (
|
|
85
|
-
<
|
|
102
|
+
<KpiCard className="min-w-36">
|
|
86
103
|
<CardHeader>
|
|
87
|
-
<CardTitle
|
|
104
|
+
<CardTitle>Answered</CardTitle>
|
|
88
105
|
</CardHeader>
|
|
89
106
|
<CardContent>
|
|
90
|
-
<
|
|
107
|
+
<KpiValue value={123} kind="number" />
|
|
91
108
|
</CardContent>
|
|
92
|
-
</
|
|
109
|
+
</KpiCard>
|
|
93
110
|
);
|
|
94
111
|
}
|
|
95
112
|
```
|
|
96
113
|
|
|
97
114
|
Key changes:
|
|
98
|
-
- `Tile` → `
|
|
99
|
-
- `TileHeader label` → `CardTitle` inside `CardHeader`.
|
|
100
|
-
- `TileBody` → `CardContent`.
|
|
101
|
-
- `TileValue` →
|
|
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).
|
|
102
119
|
|
|
103
120
|
### 3b. Tile with loading state
|
|
104
121
|
|
|
@@ -118,26 +135,27 @@ function LoadingTile() {
|
|
|
118
135
|
}
|
|
119
136
|
```
|
|
120
137
|
|
|
121
|
-
**After
|
|
138
|
+
**After:**
|
|
122
139
|
```tsx
|
|
123
|
-
import {
|
|
140
|
+
import { CardContent, CardHeader, CardTitle } from '@aircall/ds';
|
|
141
|
+
import { KpiCard, KpiValueSkeleton } from '@aircall/blocks';
|
|
124
142
|
|
|
125
143
|
function LoadingTile() {
|
|
126
144
|
return (
|
|
127
|
-
<
|
|
145
|
+
<KpiCard className="min-w-36">
|
|
128
146
|
<CardHeader>
|
|
129
|
-
<CardTitle
|
|
147
|
+
<CardTitle>Answered</CardTitle>
|
|
130
148
|
</CardHeader>
|
|
131
|
-
<CardContent
|
|
132
|
-
<
|
|
149
|
+
<CardContent>
|
|
150
|
+
<KpiValueSkeleton />
|
|
133
151
|
</CardContent>
|
|
134
|
-
</
|
|
152
|
+
</KpiCard>
|
|
135
153
|
);
|
|
136
154
|
}
|
|
137
155
|
```
|
|
138
156
|
|
|
139
|
-
|
|
140
|
-
|
|
157
|
+
When a comparison line also loads, pair with `KpiDescriptionSkeleton` inside
|
|
158
|
+
`CardContent className="flex flex-col gap-1"`.
|
|
141
159
|
|
|
142
160
|
### 3c. Tile with tooltip on header
|
|
143
161
|
|
|
@@ -157,10 +175,9 @@ function InfoTile() {
|
|
|
157
175
|
}
|
|
158
176
|
```
|
|
159
177
|
|
|
160
|
-
**After
|
|
178
|
+
**After:**
|
|
161
179
|
```tsx
|
|
162
180
|
import {
|
|
163
|
-
Card,
|
|
164
181
|
CardContent,
|
|
165
182
|
CardHeader,
|
|
166
183
|
CardTitle,
|
|
@@ -170,16 +187,19 @@ import {
|
|
|
170
187
|
TooltipTrigger
|
|
171
188
|
} from '@aircall/ds';
|
|
172
189
|
import { InformationCircleIcon } from '@aircall/react-icons';
|
|
190
|
+
import { KpiCard, KpiValue } from '@aircall/blocks';
|
|
173
191
|
|
|
174
192
|
function InfoTile() {
|
|
175
193
|
return (
|
|
176
|
-
<
|
|
194
|
+
<KpiCard className="min-w-36">
|
|
177
195
|
<CardHeader>
|
|
178
|
-
<CardTitle className="flex items-center gap-1
|
|
196
|
+
<CardTitle className="flex items-center gap-1">
|
|
179
197
|
Answered
|
|
180
198
|
<TooltipProvider>
|
|
181
199
|
<Tooltip>
|
|
182
|
-
<TooltipTrigger
|
|
200
|
+
<TooltipTrigger
|
|
201
|
+
render={<button type="button" className="inline-flex" aria-label="More info" />}
|
|
202
|
+
>
|
|
183
203
|
<InformationCircleIcon className="h-3 w-3 cursor-default" aria-hidden />
|
|
184
204
|
</TooltipTrigger>
|
|
185
205
|
<TooltipContent>Calls answered in the period</TooltipContent>
|
|
@@ -188,17 +208,14 @@ function InfoTile() {
|
|
|
188
208
|
</CardTitle>
|
|
189
209
|
</CardHeader>
|
|
190
210
|
<CardContent>
|
|
191
|
-
<
|
|
211
|
+
<KpiValue value={123} />
|
|
192
212
|
</CardContent>
|
|
193
|
-
</
|
|
213
|
+
</KpiCard>
|
|
194
214
|
);
|
|
195
215
|
}
|
|
196
216
|
```
|
|
197
217
|
|
|
198
|
-
|
|
199
|
-
`Tooltip` is hover-driven by default — no extra state needed.
|
|
200
|
-
|
|
201
|
-
### 3d. Tile with actions (TileActions)
|
|
218
|
+
### 3d. Tile with actions + clickable value
|
|
202
219
|
|
|
203
220
|
**Before (`@dashboard/library`):**
|
|
204
221
|
```tsx
|
|
@@ -224,50 +241,37 @@ function ActionTile() {
|
|
|
224
241
|
}
|
|
225
242
|
```
|
|
226
243
|
|
|
227
|
-
**After
|
|
244
|
+
**After:**
|
|
228
245
|
```tsx
|
|
229
|
-
import {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
CardAction,
|
|
233
|
-
CardContent,
|
|
234
|
-
CardHeader,
|
|
235
|
-
CardTitle
|
|
236
|
-
} from '@aircall/ds';
|
|
237
|
-
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';
|
|
238
249
|
|
|
239
250
|
function ActionTile() {
|
|
240
251
|
return (
|
|
241
|
-
<
|
|
252
|
+
<KpiCard className="min-w-36">
|
|
242
253
|
<CardHeader>
|
|
243
|
-
<CardTitle
|
|
254
|
+
<CardTitle>Answered</CardTitle>
|
|
244
255
|
<CardAction className="flex items-center gap-1">
|
|
245
|
-
<Button variant="ghost" size="icon" aria-label="Settings" onClick={() => null}>
|
|
246
|
-
<
|
|
256
|
+
<Button variant="ghost" size="icon-sm" aria-label="Settings" onClick={() => null}>
|
|
257
|
+
<Settings />
|
|
247
258
|
</Button>
|
|
248
|
-
<Button variant="ghost" size="icon" aria-label="More options" onClick={() => null}>
|
|
249
|
-
<
|
|
259
|
+
<Button variant="ghost" size="icon-sm" aria-label="More options" onClick={() => null}>
|
|
260
|
+
<EllipsisVertical />
|
|
250
261
|
</Button>
|
|
251
262
|
</CardAction>
|
|
252
263
|
</CardHeader>
|
|
253
264
|
<CardContent>
|
|
254
|
-
<
|
|
255
|
-
type="button"
|
|
256
|
-
className="text-2xl font-bold tabular-nums underline decoration-dashed"
|
|
257
|
-
onClick={() => null}
|
|
258
|
-
>
|
|
259
|
-
123
|
|
260
|
-
</button>
|
|
265
|
+
<KpiValue value={123} onClick={() => null} />
|
|
261
266
|
</CardContent>
|
|
262
|
-
</
|
|
267
|
+
</KpiCard>
|
|
263
268
|
);
|
|
264
269
|
}
|
|
265
270
|
```
|
|
266
271
|
|
|
267
272
|
Key changes:
|
|
268
|
-
- `TileActions` → `CardAction` inside `CardHeader`.
|
|
269
|
-
- `
|
|
270
|
-
- `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).
|
|
271
275
|
|
|
272
276
|
### 3e. Disabled tile
|
|
273
277
|
|
|
@@ -278,7 +282,7 @@ import { Tile, TileBody, TileHeader, TileValue } from '@dashboard/library';
|
|
|
278
282
|
function DisabledTile() {
|
|
279
283
|
return (
|
|
280
284
|
<Tile disabled>
|
|
281
|
-
<TileHeader label="Answered"
|
|
285
|
+
<TileHeader label="Answered" />
|
|
282
286
|
<TileBody>
|
|
283
287
|
<TileValue onClick={() => null} value={123} />
|
|
284
288
|
</TileBody>
|
|
@@ -287,142 +291,189 @@ function DisabledTile() {
|
|
|
287
291
|
}
|
|
288
292
|
```
|
|
289
293
|
|
|
290
|
-
**After
|
|
294
|
+
**After:**
|
|
291
295
|
```tsx
|
|
292
|
-
import {
|
|
296
|
+
import { CardContent, CardHeader, CardTitle } from '@aircall/ds';
|
|
297
|
+
import { KpiCard, KpiValue } from '@aircall/blocks';
|
|
293
298
|
|
|
294
299
|
function DisabledTile() {
|
|
295
300
|
return (
|
|
296
|
-
<
|
|
301
|
+
<KpiCard disabled className="min-w-36">
|
|
297
302
|
<CardHeader>
|
|
298
|
-
<CardTitle
|
|
303
|
+
<CardTitle>Answered</CardTitle>
|
|
299
304
|
</CardHeader>
|
|
300
305
|
<CardContent>
|
|
301
|
-
<
|
|
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>
|
|
302
336
|
</CardContent>
|
|
303
|
-
</
|
|
337
|
+
</KpiCard>
|
|
304
338
|
);
|
|
305
339
|
}
|
|
306
340
|
```
|
|
307
341
|
|
|
308
|
-
`
|
|
309
|
-
|
|
310
|
-
|
|
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.
|
|
311
362
|
|
|
312
363
|
---
|
|
313
364
|
|
|
314
365
|
## 4. Common mistakes
|
|
315
366
|
|
|
316
|
-
### Mistake 1 —
|
|
367
|
+
### Mistake 1 — Using raw DS `Card` instead of `KpiCard`
|
|
317
368
|
|
|
318
369
|
```tsx
|
|
319
|
-
// ❌ Wrong —
|
|
320
|
-
<Card
|
|
321
|
-
<
|
|
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>
|
|
322
378
|
</Card>
|
|
323
379
|
|
|
324
|
-
// ✅ Correct
|
|
325
|
-
<
|
|
326
|
-
<
|
|
327
|
-
</
|
|
380
|
+
// ✅ Correct
|
|
381
|
+
<KpiCard>
|
|
382
|
+
<CardHeader>
|
|
383
|
+
<CardTitle>Answered</CardTitle>
|
|
384
|
+
</CardHeader>
|
|
385
|
+
<CardContent>
|
|
386
|
+
<KpiValue value={123} />
|
|
387
|
+
</CardContent>
|
|
388
|
+
</KpiCard>
|
|
328
389
|
```
|
|
329
390
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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>
|
|
333
410
|
|
|
334
|
-
|
|
411
|
+
// ✅ Correct — Tailwind via className
|
|
412
|
+
<KpiCard className="min-w-[228px]">
|
|
413
|
+
<CardContent className="flex flex-col gap-1">…</CardContent>
|
|
414
|
+
</KpiCard>
|
|
415
|
+
```
|
|
335
416
|
|
|
336
|
-
### Mistake
|
|
417
|
+
### Mistake 4 — Placing CardAction outside CardHeader
|
|
337
418
|
|
|
338
419
|
```tsx
|
|
339
|
-
// ❌ Wrong
|
|
340
|
-
<
|
|
341
|
-
<CardAction>
|
|
342
|
-
<Button variant="ghost" size="icon">…</Button>
|
|
343
|
-
</CardAction>
|
|
420
|
+
// ❌ Wrong
|
|
421
|
+
<KpiCard>
|
|
422
|
+
<CardAction>…</CardAction>
|
|
344
423
|
<CardHeader>
|
|
345
424
|
<CardTitle>Answered</CardTitle>
|
|
346
425
|
</CardHeader>
|
|
347
426
|
<CardContent>…</CardContent>
|
|
348
|
-
</
|
|
427
|
+
</KpiCard>
|
|
349
428
|
|
|
350
429
|
// ✅ Correct — CardAction must be a child of CardHeader
|
|
351
|
-
<
|
|
430
|
+
<KpiCard>
|
|
352
431
|
<CardHeader>
|
|
353
432
|
<CardTitle>Answered</CardTitle>
|
|
354
|
-
<CardAction>
|
|
355
|
-
<Button variant="ghost" size="icon">…</Button>
|
|
356
|
-
</CardAction>
|
|
433
|
+
<CardAction>…</CardAction>
|
|
357
434
|
</CardHeader>
|
|
358
435
|
<CardContent>…</CardContent>
|
|
359
|
-
</
|
|
436
|
+
</KpiCard>
|
|
360
437
|
```
|
|
361
438
|
|
|
362
|
-
|
|
363
|
-
`CardAction` carries `data-slot="card-action"` and sits at `col-start-2 row-span-2`. Outside
|
|
364
|
-
`CardHeader` there is no grid parent; the action renders in document flow at full width.
|
|
365
|
-
|
|
366
|
-
Source: `packages/ds/src/components/card.tsx`
|
|
367
|
-
|
|
368
|
-
### Mistake 3 — Importing TILE_VALUE enum or TileValue from @dashboard/library after migration
|
|
439
|
+
### Mistake 5 — Keeping TILE_VALUE / TileValue imports after migration
|
|
369
440
|
|
|
370
441
|
```tsx
|
|
371
|
-
// ❌ Wrong
|
|
442
|
+
// ❌ Wrong
|
|
372
443
|
import { TILE_VALUE, Tile, TileHeader, TileValue } from '@dashboard/library';
|
|
373
|
-
|
|
374
444
|
<TileValue type={TILE_VALUE.DURATION} value={123} />
|
|
375
445
|
|
|
376
|
-
// ✅ Correct
|
|
377
|
-
import {
|
|
378
|
-
|
|
379
|
-
// Duration formatting (seconds → "2m 03s") — implement inline or extract to a util
|
|
380
|
-
function formatDuration(seconds: number): string {
|
|
381
|
-
const m = Math.floor(seconds / 60);
|
|
382
|
-
const s = seconds % 60;
|
|
383
|
-
return `${m}m ${String(s).padStart(2, '0')}s`;
|
|
384
|
-
}
|
|
446
|
+
// ✅ Correct
|
|
447
|
+
import { KpiCard, KpiValue } from '@aircall/blocks';
|
|
448
|
+
import { CardContent, CardHeader, CardTitle } from '@aircall/ds';
|
|
385
449
|
|
|
386
|
-
<
|
|
450
|
+
<KpiCard>
|
|
387
451
|
<CardHeader>
|
|
388
|
-
<CardTitle
|
|
452
|
+
<CardTitle>AHT</CardTitle>
|
|
389
453
|
</CardHeader>
|
|
390
454
|
<CardContent>
|
|
391
|
-
<
|
|
455
|
+
<KpiValue value={123} kind="duration" />
|
|
392
456
|
</CardContent>
|
|
393
|
-
</
|
|
457
|
+
</KpiCard>
|
|
394
458
|
```
|
|
395
459
|
|
|
396
|
-
|
|
397
|
-
seconds via an internal `formatSecondsToDuration` helper. That helper is not exported from
|
|
398
|
-
`@dashboard/library`'s public API. Extract the formatting logic to a local utility function.
|
|
399
|
-
|
|
400
|
-
Source: `packages/ds/src/components/card.tsx`
|
|
401
|
-
|
|
402
|
-
### 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
|
|
403
461
|
|
|
404
462
|
```tsx
|
|
405
|
-
// ❌ Wrong — CardTitle used for the big number
|
|
406
|
-
<
|
|
463
|
+
// ❌ Wrong — CardTitle used for the big number
|
|
464
|
+
<KpiCard>
|
|
407
465
|
<CardHeader>
|
|
408
466
|
<CardTitle>123</CardTitle>
|
|
409
467
|
</CardHeader>
|
|
410
|
-
</
|
|
468
|
+
</KpiCard>
|
|
411
469
|
|
|
412
|
-
// ✅ Correct — CardTitle
|
|
413
|
-
<
|
|
470
|
+
// ✅ Correct — CardTitle = label; KpiValue = metric
|
|
471
|
+
<KpiCard>
|
|
414
472
|
<CardHeader>
|
|
415
|
-
<CardTitle
|
|
473
|
+
<CardTitle>Answered</CardTitle>
|
|
416
474
|
</CardHeader>
|
|
417
475
|
<CardContent>
|
|
418
|
-
<
|
|
476
|
+
<KpiValue value={123} />
|
|
419
477
|
</CardContent>
|
|
420
|
-
</
|
|
478
|
+
</KpiCard>
|
|
421
479
|
```
|
|
422
|
-
|
|
423
|
-
In `@dashboard/library`, `TileHeader` rendered the human-readable label ("Answered") and
|
|
424
|
-
`TileValue` rendered the metric number. In the DS mapping, `CardTitle` takes the label and
|
|
425
|
-
`CardContent` holds the value element. Swapping them produces an inverted visual hierarchy
|
|
426
|
-
and breaks assistive-technology reading order.
|
|
427
|
-
|
|
428
|
-
Source: `packages/ds/src/components/card.tsx`
|