@aircall/blocks 0.6.0 → 0.9.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.
- package/README.md +43 -0
- package/dist/globals.css +1 -1
- package/dist/index.d.ts +308 -3
- package/dist/index.js +1021 -147
- package/package.json +17 -3
- package/skills/aircall-blocks/migrate-dashboard/SKILL.md +127 -0
- package/skills/aircall-blocks/migrate-dashboard/card/SKILL.md +364 -0
- package/skills/aircall-blocks/migrate-dashboard/combobox/SKILL.md +487 -0
- package/skills/aircall-blocks/migrate-dashboard/copy-button/SKILL.md +138 -0
- package/skills/aircall-blocks/migrate-dashboard/dashboard-page/SKILL.md +198 -0
- package/skills/aircall-blocks/migrate-dashboard/data-table/SKILL.md +395 -0
- package/skills/aircall-blocks/migrate-dashboard/empty-states/SKILL.md +347 -0
- package/skills/aircall-blocks/migrate-dashboard/form-wizard/SKILL.md +271 -0
- package/skills/aircall-blocks/migrate-dashboard/info-popup/SKILL.md +179 -0
- package/skills/aircall-blocks/migrate-dashboard/layout/SKILL.md +328 -0
- package/skills/aircall-blocks/migrate-dashboard/list/SKILL.md +474 -0
- package/skills/aircall-blocks/migrate-dashboard/loading/SKILL.md +260 -0
- package/skills/aircall-blocks/migrate-dashboard/page-header/SKILL.md +394 -0
- package/skills/aircall-blocks/migrate-dashboard/tile/SKILL.md +429 -0
- package/skills/aircall-blocks/migrate-dashboard/toggle-row/SKILL.md +177 -0
- package/skills/aircall-blocks/setup/SKILL.md +127 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aircall-blocks/migrate-dashboard/tile
|
|
3
|
+
description: >
|
|
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.
|
|
8
|
+
type: sub-skill
|
|
9
|
+
library: aircall-blocks
|
|
10
|
+
library_version: "0.5.1"
|
|
11
|
+
requires:
|
|
12
|
+
- aircall-blocks/setup
|
|
13
|
+
- aircall-blocks/migrate-dashboard
|
|
14
|
+
sources:
|
|
15
|
+
- "aircall/hydra:packages/ds/src/index.ts"
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
This skill builds on aircall-blocks/migrate-dashboard.
|
|
19
|
+
|
|
20
|
+
## 1. Component mapping
|
|
21
|
+
|
|
22
|
+
| @dashboard/library | @aircall/ds |
|
|
23
|
+
| --- | --- |
|
|
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.
|
|
38
|
+
|
|
39
|
+
## 2. Imports
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
// DS primitives — tile shell
|
|
43
|
+
import {
|
|
44
|
+
Card,
|
|
45
|
+
CardAction,
|
|
46
|
+
CardContent,
|
|
47
|
+
CardHeader,
|
|
48
|
+
CardTitle,
|
|
49
|
+
Skeleton,
|
|
50
|
+
Tooltip,
|
|
51
|
+
TooltipContent,
|
|
52
|
+
TooltipProvider,
|
|
53
|
+
TooltipTrigger
|
|
54
|
+
} from '@aircall/ds';
|
|
55
|
+
```
|
|
56
|
+
|
|
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
|
+
## 3. Before / After
|
|
61
|
+
|
|
62
|
+
### 3a. Basic tile — number value
|
|
63
|
+
|
|
64
|
+
**Before (`@dashboard/library`):**
|
|
65
|
+
```tsx
|
|
66
|
+
import { Tile, TileBody, TileHeader, TileValue } from '@dashboard/library';
|
|
67
|
+
|
|
68
|
+
function AnsweredTile() {
|
|
69
|
+
return (
|
|
70
|
+
<Tile>
|
|
71
|
+
<TileHeader label="Answered" />
|
|
72
|
+
<TileBody>
|
|
73
|
+
<TileValue value={123} />
|
|
74
|
+
</TileBody>
|
|
75
|
+
</Tile>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**After (`@aircall/ds`):**
|
|
81
|
+
```tsx
|
|
82
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@aircall/ds';
|
|
83
|
+
|
|
84
|
+
function AnsweredTile() {
|
|
85
|
+
return (
|
|
86
|
+
<Card className="min-w-36">
|
|
87
|
+
<CardHeader>
|
|
88
|
+
<CardTitle className="text-xs font-medium text-muted-foreground">Answered</CardTitle>
|
|
89
|
+
</CardHeader>
|
|
90
|
+
<CardContent>
|
|
91
|
+
<span className="text-2xl font-bold tabular-nums">123</span>
|
|
92
|
+
</CardContent>
|
|
93
|
+
</Card>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
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`.
|
|
103
|
+
|
|
104
|
+
### 3b. Tile with loading state
|
|
105
|
+
|
|
106
|
+
**Before (`@dashboard/library`):**
|
|
107
|
+
```tsx
|
|
108
|
+
import { Tile, TileBody, TileHeader, TileValue } from '@dashboard/library';
|
|
109
|
+
|
|
110
|
+
function LoadingTile() {
|
|
111
|
+
return (
|
|
112
|
+
<Tile>
|
|
113
|
+
<TileHeader label="Answered" />
|
|
114
|
+
<TileBody>
|
|
115
|
+
<TileValue loading />
|
|
116
|
+
</TileBody>
|
|
117
|
+
</Tile>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**After (`@aircall/ds`):**
|
|
123
|
+
```tsx
|
|
124
|
+
import { Card, CardContent, CardHeader, CardTitle, Skeleton } from '@aircall/ds';
|
|
125
|
+
|
|
126
|
+
function LoadingTile() {
|
|
127
|
+
return (
|
|
128
|
+
<Card className="min-w-36">
|
|
129
|
+
<CardHeader>
|
|
130
|
+
<CardTitle className="text-xs font-medium text-muted-foreground">Answered</CardTitle>
|
|
131
|
+
</CardHeader>
|
|
132
|
+
<CardContent className="flex h-8 items-center">
|
|
133
|
+
<Skeleton className="h-6 w-18" />
|
|
134
|
+
</CardContent>
|
|
135
|
+
</Card>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
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`.
|
|
142
|
+
|
|
143
|
+
### 3c. Tile with tooltip on header
|
|
144
|
+
|
|
145
|
+
**Before (`@dashboard/library`):**
|
|
146
|
+
```tsx
|
|
147
|
+
import { Tile, TileBody, TileHeader, TileValue } from '@dashboard/library';
|
|
148
|
+
|
|
149
|
+
function InfoTile() {
|
|
150
|
+
return (
|
|
151
|
+
<Tile>
|
|
152
|
+
<TileHeader label="Answered" tooltipTexts={{ info: 'Calls answered in the period' }} />
|
|
153
|
+
<TileBody>
|
|
154
|
+
<TileValue value={123} />
|
|
155
|
+
</TileBody>
|
|
156
|
+
</Tile>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**After (`@aircall/ds`):**
|
|
162
|
+
```tsx
|
|
163
|
+
import {
|
|
164
|
+
Card,
|
|
165
|
+
CardContent,
|
|
166
|
+
CardHeader,
|
|
167
|
+
CardTitle,
|
|
168
|
+
Tooltip,
|
|
169
|
+
TooltipContent,
|
|
170
|
+
TooltipProvider,
|
|
171
|
+
TooltipTrigger
|
|
172
|
+
} from '@aircall/ds';
|
|
173
|
+
import { InformationCircleIcon } from '@aircall/react-icons';
|
|
174
|
+
|
|
175
|
+
function InfoTile() {
|
|
176
|
+
return (
|
|
177
|
+
<Card className="min-w-36">
|
|
178
|
+
<CardHeader>
|
|
179
|
+
<CardTitle className="flex items-center gap-1 text-xs font-medium text-muted-foreground">
|
|
180
|
+
Answered
|
|
181
|
+
<TooltipProvider>
|
|
182
|
+
<Tooltip>
|
|
183
|
+
<TooltipTrigger asChild>
|
|
184
|
+
<InformationCircleIcon className="h-3 w-3 cursor-default" aria-hidden />
|
|
185
|
+
</TooltipTrigger>
|
|
186
|
+
<TooltipContent>Calls answered in the period</TooltipContent>
|
|
187
|
+
</Tooltip>
|
|
188
|
+
</TooltipProvider>
|
|
189
|
+
</CardTitle>
|
|
190
|
+
</CardHeader>
|
|
191
|
+
<CardContent>
|
|
192
|
+
<span className="text-2xl font-bold tabular-nums">123</span>
|
|
193
|
+
</CardContent>
|
|
194
|
+
</Card>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
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)
|
|
203
|
+
|
|
204
|
+
**Before (`@dashboard/library`):**
|
|
205
|
+
```tsx
|
|
206
|
+
import { Gap, Tile, TileActions, TileBody, TileHeader, TileValue } from '@dashboard/library';
|
|
207
|
+
import { IconButton } from '@aircall/tractor';
|
|
208
|
+
import { SettingsFilled, MenuVerticalFilled } from '@aircall/icons';
|
|
209
|
+
|
|
210
|
+
function ActionTile() {
|
|
211
|
+
return (
|
|
212
|
+
<Tile>
|
|
213
|
+
<TileActions>
|
|
214
|
+
<Gap gap="xxxs">
|
|
215
|
+
<IconButton component={SettingsFilled} size={16} onClick={() => null} type="button" />
|
|
216
|
+
<IconButton component={MenuVerticalFilled} size={16} onClick={() => null} type="button" />
|
|
217
|
+
</Gap>
|
|
218
|
+
</TileActions>
|
|
219
|
+
<TileHeader label="Answered" />
|
|
220
|
+
<TileBody>
|
|
221
|
+
<TileValue onClick={() => null} value={123} />
|
|
222
|
+
</TileBody>
|
|
223
|
+
</Tile>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**After (`@aircall/ds`):**
|
|
229
|
+
```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';
|
|
239
|
+
|
|
240
|
+
function ActionTile() {
|
|
241
|
+
return (
|
|
242
|
+
<Card className="min-w-36">
|
|
243
|
+
<CardHeader>
|
|
244
|
+
<CardTitle className="text-xs font-medium text-muted-foreground">Answered</CardTitle>
|
|
245
|
+
<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" />
|
|
248
|
+
</Button>
|
|
249
|
+
<Button variant="ghost" size="icon" aria-label="More options" onClick={() => null}>
|
|
250
|
+
<MoreVerticalIcon className="h-4 w-4" />
|
|
251
|
+
</Button>
|
|
252
|
+
</CardAction>
|
|
253
|
+
</CardHeader>
|
|
254
|
+
<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>
|
|
262
|
+
</CardContent>
|
|
263
|
+
</Card>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
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.
|
|
272
|
+
|
|
273
|
+
### 3e. Disabled tile
|
|
274
|
+
|
|
275
|
+
**Before (`@dashboard/library`):**
|
|
276
|
+
```tsx
|
|
277
|
+
import { Tile, TileBody, TileHeader, TileValue } from '@dashboard/library';
|
|
278
|
+
|
|
279
|
+
function DisabledTile() {
|
|
280
|
+
return (
|
|
281
|
+
<Tile disabled>
|
|
282
|
+
<TileHeader label="Answered" tooltipTexts={{ help: 'Feature disabled' }} />
|
|
283
|
+
<TileBody>
|
|
284
|
+
<TileValue onClick={() => null} value={123} />
|
|
285
|
+
</TileBody>
|
|
286
|
+
</Tile>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**After (`@aircall/ds`):**
|
|
292
|
+
```tsx
|
|
293
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@aircall/ds';
|
|
294
|
+
|
|
295
|
+
function DisabledTile() {
|
|
296
|
+
return (
|
|
297
|
+
<Card className="min-w-36 opacity-50" data-disabled aria-disabled>
|
|
298
|
+
<CardHeader>
|
|
299
|
+
<CardTitle className="text-xs font-medium text-muted-foreground">Answered</CardTitle>
|
|
300
|
+
</CardHeader>
|
|
301
|
+
<CardContent>
|
|
302
|
+
<span className="cursor-not-allowed text-2xl font-bold tabular-nums">123</span>
|
|
303
|
+
</CardContent>
|
|
304
|
+
</Card>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
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.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## 4. Common mistakes
|
|
316
|
+
|
|
317
|
+
### Mistake 1 — Passing Tile/TileBody flex props to Card/CardContent
|
|
318
|
+
|
|
319
|
+
```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>
|
|
323
|
+
</Card>
|
|
324
|
+
|
|
325
|
+
// ✅ Correct — use className with Tailwind utilities
|
|
326
|
+
<Card className="min-w-[228px]">
|
|
327
|
+
<CardContent className="flex items-center gap-1">…</CardContent>
|
|
328
|
+
</Card>
|
|
329
|
+
```
|
|
330
|
+
|
|
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.
|
|
334
|
+
|
|
335
|
+
Source: `packages/ds/src/components/card.tsx`
|
|
336
|
+
|
|
337
|
+
### Mistake 2 — Placing CardAction outside CardHeader
|
|
338
|
+
|
|
339
|
+
```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>
|
|
345
|
+
<CardHeader>
|
|
346
|
+
<CardTitle>Answered</CardTitle>
|
|
347
|
+
</CardHeader>
|
|
348
|
+
<CardContent>…</CardContent>
|
|
349
|
+
</Card>
|
|
350
|
+
|
|
351
|
+
// ✅ Correct — CardAction must be a child of CardHeader
|
|
352
|
+
<Card>
|
|
353
|
+
<CardHeader>
|
|
354
|
+
<CardTitle>Answered</CardTitle>
|
|
355
|
+
<CardAction>
|
|
356
|
+
<Button variant="ghost" size="icon">…</Button>
|
|
357
|
+
</CardAction>
|
|
358
|
+
</CardHeader>
|
|
359
|
+
<CardContent>…</CardContent>
|
|
360
|
+
</Card>
|
|
361
|
+
```
|
|
362
|
+
|
|
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
|
|
370
|
+
|
|
371
|
+
```tsx
|
|
372
|
+
// ❌ Wrong — keeping the enum import after TileValue is removed
|
|
373
|
+
import { TILE_VALUE, Tile, TileHeader, TileValue } from '@dashboard/library';
|
|
374
|
+
|
|
375
|
+
<TileValue type={TILE_VALUE.DURATION} value={123} />
|
|
376
|
+
|
|
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
|
+
}
|
|
386
|
+
|
|
387
|
+
<Card className="min-w-36">
|
|
388
|
+
<CardHeader>
|
|
389
|
+
<CardTitle className="text-xs font-medium text-muted-foreground">AHT</CardTitle>
|
|
390
|
+
</CardHeader>
|
|
391
|
+
<CardContent>
|
|
392
|
+
<span className="text-2xl font-bold tabular-nums">{formatDuration(123)}</span>
|
|
393
|
+
</CardContent>
|
|
394
|
+
</Card>
|
|
395
|
+
```
|
|
396
|
+
|
|
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
|
|
404
|
+
|
|
405
|
+
```tsx
|
|
406
|
+
// ❌ Wrong — CardTitle used for the big number; label lost
|
|
407
|
+
<Card>
|
|
408
|
+
<CardHeader>
|
|
409
|
+
<CardTitle>123</CardTitle>
|
|
410
|
+
</CardHeader>
|
|
411
|
+
</Card>
|
|
412
|
+
|
|
413
|
+
// ✅ Correct — CardTitle holds the label; value sits in CardContent
|
|
414
|
+
<Card className="min-w-36">
|
|
415
|
+
<CardHeader>
|
|
416
|
+
<CardTitle className="text-xs font-medium text-muted-foreground">Answered</CardTitle>
|
|
417
|
+
</CardHeader>
|
|
418
|
+
<CardContent>
|
|
419
|
+
<span className="text-2xl font-bold tabular-nums">123</span>
|
|
420
|
+
</CardContent>
|
|
421
|
+
</Card>
|
|
422
|
+
```
|
|
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`
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aircall-blocks/migrate-dashboard/toggle-row
|
|
3
|
+
description: >
|
|
4
|
+
Migrate @dashboard/library ToggleRow to @aircall/ds Field + Switch composition.
|
|
5
|
+
Load when a file imports ToggleRow from @dashboard/library.
|
|
6
|
+
type: sub-skill
|
|
7
|
+
library: aircall-blocks
|
|
8
|
+
library_version: "0.6.0"
|
|
9
|
+
requires:
|
|
10
|
+
- aircall-blocks/setup
|
|
11
|
+
- aircall-blocks/migrate-dashboard
|
|
12
|
+
sources:
|
|
13
|
+
- "aircall/hydra:packages/ds/src/index.ts"
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
This skill builds on aircall-blocks/migrate-dashboard.
|
|
17
|
+
|
|
18
|
+
## 1. Key premise
|
|
19
|
+
|
|
20
|
+
`ToggleRow` was a single component. The DS replacement is a **composition** of
|
|
21
|
+
`Field` (orientation="horizontal") + `Switch` + `FieldLabel` + optional `FieldDescription`.
|
|
22
|
+
There is no 1:1 component — you must assemble the parts.
|
|
23
|
+
|
|
24
|
+
## 2. Verified DS exports (`packages/ds/src/index.ts`)
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Field, FieldLabel, FieldDescription, FieldTitle, FieldContent
|
|
28
|
+
Switch
|
|
29
|
+
Spinner
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 3. Imports
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { Field, FieldDescription, FieldLabel, Spinner, Switch } from '@aircall/ds';
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 4. Prop mapping
|
|
39
|
+
|
|
40
|
+
| `ToggleRow` prop | DS equivalent |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| `label` | `<FieldLabel>` inside `<Field orientation="horizontal">` |
|
|
43
|
+
| `description` | `<FieldDescription>` next to `<FieldLabel>` |
|
|
44
|
+
| `value` | `<Switch checked={…}>` or `<Switch defaultChecked>` |
|
|
45
|
+
| `onChange` | `<Switch onCheckedChange={…}>` |
|
|
46
|
+
| `disabled` | `<Switch disabled>` |
|
|
47
|
+
| `loading` | Replace `<Switch>` with a `<Spinner>` in a `data-slot="switch"` wrapper (see §6) |
|
|
48
|
+
| `data-test` | Pass to `<Switch>` directly |
|
|
49
|
+
|
|
50
|
+
## 5. Structure
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<Field orientation="horizontal">
|
|
54
|
+
<Switch /> {/* or Spinner placeholder when loading */}
|
|
55
|
+
<FieldLabel>Label text</FieldLabel>
|
|
56
|
+
</Field>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
With description:
|
|
60
|
+
```tsx
|
|
61
|
+
<Field orientation="horizontal" className="max-w-sm">
|
|
62
|
+
<Switch />
|
|
63
|
+
<div className="flex flex-col gap-0.5">
|
|
64
|
+
<FieldLabel>Label text</FieldLabel>
|
|
65
|
+
<FieldDescription>Supporting description.</FieldDescription>
|
|
66
|
+
</div>
|
|
67
|
+
</Field>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 6. Before / After examples
|
|
71
|
+
|
|
72
|
+
### 6a. Label only
|
|
73
|
+
|
|
74
|
+
**Before:**
|
|
75
|
+
```tsx
|
|
76
|
+
import { ToggleRow } from '@dashboard/library';
|
|
77
|
+
|
|
78
|
+
<ToggleRow
|
|
79
|
+
label="Enable notifications"
|
|
80
|
+
value={enabled}
|
|
81
|
+
onChange={setEnabled}
|
|
82
|
+
/>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**After:**
|
|
86
|
+
```tsx
|
|
87
|
+
import { Field, FieldLabel, Switch } from '@aircall/ds';
|
|
88
|
+
|
|
89
|
+
<Field orientation="horizontal">
|
|
90
|
+
<Switch checked={enabled} onCheckedChange={setEnabled} />
|
|
91
|
+
<FieldLabel>Enable notifications</FieldLabel>
|
|
92
|
+
</Field>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 6b. With description
|
|
96
|
+
|
|
97
|
+
**Before:**
|
|
98
|
+
```tsx
|
|
99
|
+
<ToggleRow
|
|
100
|
+
label="Recording"
|
|
101
|
+
description="Automatically record all inbound calls."
|
|
102
|
+
value={recording}
|
|
103
|
+
onChange={setRecording}
|
|
104
|
+
/>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**After:**
|
|
108
|
+
```tsx
|
|
109
|
+
import { Field, FieldDescription, FieldLabel, Switch } from '@aircall/ds';
|
|
110
|
+
|
|
111
|
+
<Field orientation="horizontal" className="max-w-sm">
|
|
112
|
+
<Switch checked={recording} onCheckedChange={setRecording} />
|
|
113
|
+
<div className="flex flex-col gap-0.5">
|
|
114
|
+
<FieldLabel>Recording</FieldLabel>
|
|
115
|
+
<FieldDescription>Automatically record all inbound calls.</FieldDescription>
|
|
116
|
+
</div>
|
|
117
|
+
</Field>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 6c. Loading state
|
|
121
|
+
|
|
122
|
+
`ToggleRow` accepted a `loading` prop that replaced the toggle with a spinner.
|
|
123
|
+
In the DS, swap the `Switch` for a `Spinner` placed inside a `data-slot="switch"`
|
|
124
|
+
wrapper so Field's horizontal layout still aligns correctly:
|
|
125
|
+
|
|
126
|
+
**Before:**
|
|
127
|
+
```tsx
|
|
128
|
+
<ToggleRow label="Syncing contacts" loading={true} />
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**After:**
|
|
132
|
+
```tsx
|
|
133
|
+
import { Field, FieldLabel, Spinner } from '@aircall/ds';
|
|
134
|
+
|
|
135
|
+
<Field orientation="horizontal">
|
|
136
|
+
<div data-slot="switch" className="grid h-6 w-10 shrink-0 place-items-center">
|
|
137
|
+
<Spinner size="default" />
|
|
138
|
+
</div>
|
|
139
|
+
<FieldLabel>Syncing contacts</FieldLabel>
|
|
140
|
+
</Field>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## 7. Common mistakes
|
|
144
|
+
|
|
145
|
+
### Mistake 1: Using `Switch` without `Field`
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
// WRONG — no layout wrapper; label and switch won't align
|
|
149
|
+
<Switch checked={value} onCheckedChange={onChange} />
|
|
150
|
+
<label>Enable notifications</label>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
// CORRECT — Field orientation="horizontal" provides the flex row layout
|
|
155
|
+
<Field orientation="horizontal">
|
|
156
|
+
<Switch checked={value} onCheckedChange={onChange} />
|
|
157
|
+
<FieldLabel>Enable notifications</FieldLabel>
|
|
158
|
+
</Field>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Mistake 2: Putting description inside FieldLabel
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
// WRONG — FieldLabel is for the label text only
|
|
165
|
+
<FieldLabel>
|
|
166
|
+
Recording
|
|
167
|
+
<FieldDescription>…</FieldDescription>
|
|
168
|
+
</FieldLabel>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
// CORRECT — wrap label + description in a flex column div
|
|
173
|
+
<div className="flex flex-col gap-0.5">
|
|
174
|
+
<FieldLabel>Recording</FieldLabel>
|
|
175
|
+
<FieldDescription>…</FieldDescription>
|
|
176
|
+
</div>
|
|
177
|
+
```
|