@aircall/blocks 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,474 @@
1
+ ---
2
+ name: aircall-blocks/migrate-dashboard/list
3
+ description: >
4
+ Migrate @dashboard/library List, ListItem, ListPaginated, and ListItemActions to
5
+ @aircall/ds Item primitives (ItemGroup, Item, ItemMedia, ItemContent, ItemActions,
6
+ ItemTitle, ItemDescription). Load when a file imports List, ListItem, ListPaginated,
7
+ or ListItemActions 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
+ | `List` (container) | `ItemGroup` |
25
+ | `List` `hasScroll` prop | `ScrollArea` wrapping `ItemGroup` |
26
+ | `List` `stickyFooter` prop | Sibling `div` below `ItemGroup`; no DS equivalent |
27
+ | `List` `isLoadingMoreItems` + `customLoader` | Inline `Spinner` below `ItemGroup`; no DS equivalent |
28
+ | `List` `overLoading` | Overlay `Spinner` positioned absolutely over `ItemGroup` |
29
+ | `List` `renderHeader` | Sibling element above `ItemGroup` |
30
+ | `ListItem` (row) | `Item` |
31
+ | `ListItem` `avatar` prop | `ItemMedia` with `variant="image"` inside `Item` |
32
+ | `ListItem` `primaryText` prop | `ItemTitle` inside `ItemContent` inside `Item` |
33
+ | `ListItem` `secondaryText` prop | `ItemDescription` inside `ItemContent` inside `Item` |
34
+ | `ListItem` `dropdownOptions` prop | `DropdownMenu` with `DropdownMenuItem` entries inside `ItemActions` inside `Item` |
35
+ | `ListItem` `banner` prop | Sibling element below `Item` (no direct DS slot) |
36
+ | `ListItem` arbitrary `FlexProps` (px, py, h…) | `className` on `Item`; use Tailwind utilities |
37
+ | `ListItemActions` (wrapper with action menu) | `Item` + `ItemActions` + `DropdownMenu` from `@aircall/ds` |
38
+ | `ListPaginated` | `ItemGroup` + `Pagination` (and its parts) from `@aircall/ds` |
39
+
40
+ `@dashboard/library` `List` wrapped items in `<ul>/<li>` and auto-added dividers via CSS.
41
+ `ItemGroup` uses `role="list"` and a `stackedItems` prop — set `stackedItems` to replicate the
42
+ no-gap, border-between-items visual style.
43
+
44
+ `ListItem` accepted raw `FlexProps` from `@aircall/tractor` (spacing tokens, color tokens). `Item`
45
+ extends `React.ComponentProps<'div'>` — use `className` with Tailwind utilities instead.
46
+
47
+ ## 2. Imports
48
+
49
+ ```tsx
50
+ // DS primitives — item / list family
51
+ import {
52
+ Item,
53
+ ItemActions,
54
+ ItemContent,
55
+ ItemDescription,
56
+ ItemGroup,
57
+ ItemMedia,
58
+ ItemTitle
59
+ } from '@aircall/ds';
60
+
61
+ // DS pagination (for ListPaginated)
62
+ import {
63
+ Pagination,
64
+ PaginationContent,
65
+ PaginationItem,
66
+ PaginationLink,
67
+ PaginationNext,
68
+ PaginationPrevious
69
+ } from '@aircall/ds';
70
+
71
+ // DS dropdown (for dropdownOptions)
72
+ import {
73
+ DropdownMenu,
74
+ DropdownMenuContent,
75
+ DropdownMenuItem,
76
+ DropdownMenuTrigger
77
+ } from '@aircall/ds';
78
+ ```
79
+
80
+ No `@aircall/blocks` imports are needed for this migration — `Item*` and `ItemGroup` live entirely
81
+ in `@aircall/ds`.
82
+
83
+ ## 3. Before / After
84
+
85
+ ### 3a. Basic List with avatar + text items
86
+
87
+ **Before (`@dashboard/library`):**
88
+ ```tsx
89
+ import { List, ListItem } from '@dashboard/library';
90
+
91
+ function UserList({ users }: { users: { id: string; name: string; role: string; avatar: React.ReactElement }[] }) {
92
+ return (
93
+ <List>
94
+ {users.map((u) => (
95
+ <ListItem
96
+ key={u.id}
97
+ avatar={u.avatar}
98
+ primaryText={u.name}
99
+ secondaryText={u.role}
100
+ />
101
+ ))}
102
+ </List>
103
+ );
104
+ }
105
+ ```
106
+
107
+ **After (`@aircall/ds`):**
108
+ ```tsx
109
+ import { Item, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle } from '@aircall/ds';
110
+
111
+ function UserList({ users }: { users: { id: string; name: string; role: string; avatar: React.ReactElement }[] }) {
112
+ return (
113
+ <ItemGroup stackedItems>
114
+ {users.map((u) => (
115
+ <Item key={u.id} variant="outline">
116
+ <ItemMedia variant="image">{u.avatar}</ItemMedia>
117
+ <ItemContent>
118
+ <ItemTitle>{u.name}</ItemTitle>
119
+ <ItemDescription>{u.role}</ItemDescription>
120
+ </ItemContent>
121
+ </Item>
122
+ ))}
123
+ </ItemGroup>
124
+ );
125
+ }
126
+ ```
127
+
128
+ Key changes:
129
+ - `List` → `ItemGroup` with `stackedItems` to preserve the stacked-row layout with
130
+ collapsed borders between items.
131
+ - `ListItem` → `Item` with `variant="outline"` (gives each row a visible border).
132
+ - `avatar` → `ItemMedia variant="image"` as first child of `Item`.
133
+ - `primaryText` → `ItemTitle` inside `ItemContent`.
134
+ - `secondaryText` → `ItemDescription` inside `ItemContent`.
135
+ - Drop all `FlexProps` token spreads; use `className` with Tailwind utilities instead.
136
+
137
+ ### 3b. ListItem with custom children and dropdown actions
138
+
139
+ **Before (`@dashboard/library`):**
140
+ ```tsx
141
+ import { List, ListItem } from '@dashboard/library';
142
+
143
+ function IntegrationList({ items }: { items: { id: string; name: string }[] }) {
144
+ return (
145
+ <List>
146
+ {items.map((item) => (
147
+ <ListItem
148
+ key={item.id}
149
+ primaryText={item.name}
150
+ dropdownOptions={[
151
+ { title: 'Edit', callback: () => onEdit(item.id) },
152
+ { title: 'Delete', callback: () => onDelete(item.id) }
153
+ ]}
154
+ />
155
+ ))}
156
+ </List>
157
+ );
158
+ }
159
+ ```
160
+
161
+ **After (`@aircall/ds`):**
162
+ ```tsx
163
+ import {
164
+ DropdownMenu,
165
+ DropdownMenuContent,
166
+ DropdownMenuItem,
167
+ DropdownMenuTrigger,
168
+ Item,
169
+ ItemActions,
170
+ ItemContent,
171
+ ItemGroup,
172
+ ItemTitle
173
+ } from '@aircall/ds';
174
+ import { Button } from '@aircall/ds';
175
+ import { EllipsisVertical } from '@aircall/react-icons';
176
+
177
+ function IntegrationList({ items }: { items: { id: string; name: string }[] }) {
178
+ return (
179
+ <ItemGroup stackedItems>
180
+ {items.map((item) => (
181
+ <Item key={item.id} variant="outline">
182
+ <ItemContent>
183
+ <ItemTitle>{item.name}</ItemTitle>
184
+ </ItemContent>
185
+ <ItemActions>
186
+ <DropdownMenu>
187
+ <DropdownMenuTrigger asChild>
188
+ <Button variant="ghost" size="icon" aria-label="More actions">
189
+ <EllipsisVertical />
190
+ </Button>
191
+ </DropdownMenuTrigger>
192
+ <DropdownMenuContent align="end">
193
+ <DropdownMenuItem onClick={() => onEdit(item.id)}>Edit</DropdownMenuItem>
194
+ <DropdownMenuItem onClick={() => onDelete(item.id)}>Delete</DropdownMenuItem>
195
+ </DropdownMenuContent>
196
+ </DropdownMenu>
197
+ </ItemActions>
198
+ </Item>
199
+ ))}
200
+ </ItemGroup>
201
+ );
202
+ }
203
+ ```
204
+
205
+ Key changes:
206
+ - `dropdownOptions` prop → explicit `DropdownMenu` composition inside `ItemActions`.
207
+ - `ItemActions` becomes the last child of `Item`; the flex layout positions it to the right
208
+ automatically.
209
+ - Icons: import from `@aircall/react-icons`, not `lucide-react` directly.
210
+
211
+ ### 3c. ListItemActions (card-style item with action menu)
212
+
213
+ **Before (`@dashboard/library`):**
214
+ ```tsx
215
+ import { ListItemActions } from '@dashboard/library';
216
+
217
+ function PhoneNumberRow({ number, onEdit, onDelete }: { number: string; onEdit: () => void; onDelete: () => void }) {
218
+ return (
219
+ <ListItemActions
220
+ actions={[
221
+ { name: 'Edit', onClick: onEdit },
222
+ { name: 'Delete', onClick: onDelete }
223
+ ]}
224
+ >
225
+ <span>{number}</span>
226
+ </ListItemActions>
227
+ );
228
+ }
229
+ ```
230
+
231
+ **After (`@aircall/ds`):**
232
+ ```tsx
233
+ import {
234
+ DropdownMenu,
235
+ DropdownMenuContent,
236
+ DropdownMenuItem,
237
+ DropdownMenuTrigger,
238
+ Item,
239
+ ItemActions,
240
+ ItemContent,
241
+ ItemTitle
242
+ } from '@aircall/ds';
243
+ import { Button } from '@aircall/ds';
244
+ import { EllipsisVertical } from '@aircall/react-icons';
245
+
246
+ function PhoneNumberRow({ number, onEdit, onDelete }: { number: string; onEdit: () => void; onDelete: () => void }) {
247
+ return (
248
+ <Item variant="outline">
249
+ <ItemContent>
250
+ <ItemTitle>{number}</ItemTitle>
251
+ </ItemContent>
252
+ <ItemActions>
253
+ <DropdownMenu>
254
+ <DropdownMenuTrigger asChild>
255
+ <Button variant="ghost" size="icon" aria-label="More actions">
256
+ <EllipsisVertical />
257
+ </Button>
258
+ </DropdownMenuTrigger>
259
+ <DropdownMenuContent align="end">
260
+ <DropdownMenuItem onClick={onEdit}>Edit</DropdownMenuItem>
261
+ <DropdownMenuItem onClick={onDelete}>Delete</DropdownMenuItem>
262
+ </DropdownMenuContent>
263
+ </DropdownMenu>
264
+ </ItemActions>
265
+ </Item>
266
+ );
267
+ }
268
+ ```
269
+
270
+ Key changes:
271
+ - `ListItemActions` `actions` prop → `DropdownMenu` composition inside `ItemActions`.
272
+ - `ListItemActions` `children` → `ItemContent` / `ItemTitle` inside `Item`.
273
+ - The absolute-positioned action menu is replaced by `ItemActions` flex layout.
274
+
275
+ ### 3d. ListPaginated
276
+
277
+ **Before (`@dashboard/library`):**
278
+ ```tsx
279
+ import { ListPaginated, ListItem } from '@dashboard/library';
280
+
281
+ function PaginatedUsers({ users, page, onPageChange, onPageSizeChange, total, pageSize }) {
282
+ return (
283
+ <ListPaginated
284
+ activePage={page}
285
+ onPageChange={onPageChange}
286
+ onPageSizeChange={onPageSizeChange}
287
+ pageSize={pageSize}
288
+ recordsTotalCount={total}
289
+ >
290
+ {users.map((u) => (
291
+ <ListItem key={u.id} primaryText={u.name} />
292
+ ))}
293
+ </ListPaginated>
294
+ );
295
+ }
296
+ ```
297
+
298
+ **After (`@aircall/ds`):**
299
+ ```tsx
300
+ import {
301
+ Item,
302
+ ItemContent,
303
+ ItemGroup,
304
+ ItemTitle,
305
+ Pagination,
306
+ PaginationContent,
307
+ PaginationItem,
308
+ PaginationLink,
309
+ PaginationNext,
310
+ PaginationPrevious
311
+ } from '@aircall/ds';
312
+
313
+ function PaginatedUsers({ users, page, onPageChange, total, pageSize }) {
314
+ const totalPages = Math.ceil(total / pageSize);
315
+
316
+ return (
317
+ <div className="flex flex-col gap-3">
318
+ <ItemGroup stackedItems>
319
+ {users.map((u) => (
320
+ <Item key={u.id} variant="outline">
321
+ <ItemContent>
322
+ <ItemTitle>{u.name}</ItemTitle>
323
+ </ItemContent>
324
+ </Item>
325
+ ))}
326
+ </ItemGroup>
327
+ <Pagination>
328
+ <PaginationContent>
329
+ <PaginationItem>
330
+ <PaginationPrevious
331
+ onClick={() => onPageChange(page - 1)}
332
+ aria-disabled={page <= 1}
333
+ />
334
+ </PaginationItem>
335
+ {Array.from({ length: totalPages }, (_, i) => (
336
+ <PaginationItem key={i + 1}>
337
+ <PaginationLink isActive={page === i + 1} onClick={() => onPageChange(i + 1)}>
338
+ {i + 1}
339
+ </PaginationLink>
340
+ </PaginationItem>
341
+ ))}
342
+ <PaginationItem>
343
+ <PaginationNext
344
+ onClick={() => onPageChange(page + 1)}
345
+ aria-disabled={page >= totalPages}
346
+ />
347
+ </PaginationItem>
348
+ </PaginationContent>
349
+ </Pagination>
350
+ </div>
351
+ );
352
+ }
353
+ ```
354
+
355
+ Key changes:
356
+ - `ListPaginated` has no DS equivalent — decompose into `ItemGroup` + `Pagination` parts.
357
+ - `@aircall/ds` `Pagination` is a composable primitive; build page links explicitly.
358
+ - `onPageSizeChange` has no DS equivalent — implement with a `Select` from `@aircall/ds` if needed.
359
+
360
+ ---
361
+
362
+ ## 4. Common mistakes
363
+
364
+ ### Mistake 1 — Missing stackedItems on ItemGroup
365
+
366
+ ```tsx
367
+ // ❌ Wrong — gaps between items, no stacked border collapse
368
+ <ItemGroup>
369
+ <Item variant="outline">…</Item>
370
+ <Item variant="outline">…</Item>
371
+ </ItemGroup>
372
+
373
+ // ✅ Correct — stacked rows with collapsed borders (matches List visual style)
374
+ <ItemGroup stackedItems>
375
+ <Item variant="outline">…</Item>
376
+ <Item variant="outline">…</Item>
377
+ </ItemGroup>
378
+ ```
379
+
380
+ Without `stackedItems`, `ItemGroup` applies `gap-4` between items (card-grid style). With
381
+ `stackedItems`, it collapses borders between adjacent `Item` rows and removes gap, replicating
382
+ the `List` border-separated row appearance.
383
+
384
+ Source: `packages/ds/src/components/item.tsx`
385
+
386
+ ### Mistake 2 — Spreading tractor FlexProps onto Item
387
+
388
+ ```tsx
389
+ // ❌ Wrong — tractor token props (px, py, h, color) do not exist on Item
390
+ <Item px="s" py="xxs" h="xl" color="neutral-600">
391
+ <ItemContent>…</ItemContent>
392
+ </Item>
393
+
394
+ // ✅ Correct — use className with Tailwind utilities
395
+ <Item className="px-3 py-1.5 min-h-10">
396
+ <ItemContent>…</ItemContent>
397
+ </Item>
398
+ ```
399
+
400
+ `ListItem` accepted arbitrary `FlexProps` from `@aircall/tractor` (design tokens). `Item`
401
+ extends `React.ComponentProps<'div'>` — only standard HTML attributes and `className` are
402
+ accepted. Passing token strings results in React warnings for unknown DOM attributes.
403
+
404
+ Source: `packages/ds/src/components/item.tsx`
405
+
406
+ ### Mistake 3 — Placing ItemActions outside Item
407
+
408
+ ```tsx
409
+ // ❌ Wrong — ItemActions outside Item has no flex parent to pin it right
410
+ <ItemGroup stackedItems>
411
+ <Item variant="outline">
412
+ <ItemContent><ItemTitle>Name</ItemTitle></ItemContent>
413
+ </Item>
414
+ <ItemActions>
415
+ <Button>Edit</Button>
416
+ </ItemActions>
417
+ </ItemGroup>
418
+
419
+ // ✅ Correct — ItemActions is the last child inside Item
420
+ <ItemGroup stackedItems>
421
+ <Item variant="outline">
422
+ <ItemContent><ItemTitle>Name</ItemTitle></ItemContent>
423
+ <ItemActions>
424
+ <Button>Edit</Button>
425
+ </ItemActions>
426
+ </Item>
427
+ </ItemGroup>
428
+ ```
429
+
430
+ `Item` is a flex row (`flex items-center`). `ItemActions` carries `data-slot="item-actions"` and
431
+ relies on being a flex child to align right. Outside `Item`, it renders in document flow with no
432
+ flex context.
433
+
434
+ Source: `packages/ds/src/components/item.tsx`
435
+
436
+ ### Mistake 4 — Using ListItemActions children directly as ItemTitle text
437
+
438
+ ```tsx
439
+ // ❌ Wrong — bare string child of Item is not styled as a title
440
+ <Item variant="outline">
441
+ {number}
442
+ <ItemActions>…</ItemActions>
443
+ </Item>
444
+
445
+ // ✅ Correct — wrap in ItemContent > ItemTitle for correct typography and layout slot
446
+ <Item variant="outline">
447
+ <ItemContent>
448
+ <ItemTitle>{number}</ItemTitle>
449
+ </ItemContent>
450
+ <ItemActions>…</ItemActions>
451
+ </Item>
452
+ ```
453
+
454
+ `ListItemActions` accepted `children` as a raw React node and rendered it freely. `Item` is
455
+ a flex row — raw text nodes do not receive the `ItemTitle` typography or the `flex-1` grow
456
+ behaviour that `ItemContent` provides. Without `ItemContent`, `ItemActions` may not push to
457
+ the right end of the row.
458
+
459
+ Source: `packages/ds/src/components/item.tsx`
460
+
461
+ ### Mistake 5 — Importing icons from lucide-react directly
462
+
463
+ ```tsx
464
+ // ❌ Wrong — imports lucide directly, bypassing the Aircall icon layer
465
+ import { MoreVertical } from 'lucide-react';
466
+
467
+ // ✅ Correct — route all icons through @aircall/react-icons
468
+ import { EllipsisVertical } from '@aircall/react-icons';
469
+ ```
470
+
471
+ `@aircall/react-icons` re-exports all lucide icons plus Aircall custom icons. Direct lucide
472
+ imports bypass the single source of truth and prevent Aircall icon overrides.
473
+
474
+ Source: `packages/ds/src/components/item.tsx`