@aircall/blocks 0.6.0 → 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,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
+ ```