UNPKG

@aircall/blocks

Version:

Aircall Blocks — higher-level UI compositions built on @aircall/ds

474 lines (410 loc) 14.4 kB
--- name: aircall-blocks/migrate-dashboard/list description: > Migrate @dashboard/library List, ListItem, ListPaginated, and ListItemActions to @aircall/ds Item primitives (ItemGroup, Item, ItemMedia, ItemContent, ItemActions, ItemTitle, ItemDescription). Load when a file imports List, ListItem, ListPaginated, or ListItemActions from @dashboard/library. type: sub-skill library: aircall-blocks requires: - aircall-blocks/setup - aircall-blocks/migrate-dashboard sources: - "aircall/hydra:packages/ds/src/index.ts" --- This skill builds on aircall-blocks/migrate-dashboard. ## 1. Component mapping | @dashboard/library | @aircall/ds | | --- | --- | | `List` (container) | `ItemGroup` | | `List` `hasScroll` prop | `ScrollArea` wrapping `ItemGroup` | | `List` `stickyFooter` prop | Sibling `div` below `ItemGroup`; no DS equivalent | | `List` `isLoadingMoreItems` + `customLoader` | Inline `Spinner` below `ItemGroup`; no DS equivalent | | `List` `overLoading` | Overlay `Spinner` positioned absolutely over `ItemGroup` | | `List` `renderHeader` | Sibling element above `ItemGroup` | | `ListItem` (row) | `Item` | | `ListItem` `avatar` prop | `ItemMedia` with `variant="image"` inside `Item` | | `ListItem` `primaryText` prop | `ItemTitle` inside `ItemContent` inside `Item` | | `ListItem` `secondaryText` prop | `ItemDescription` inside `ItemContent` inside `Item` | | `ListItem` `dropdownOptions` prop | `DropdownMenu` with `DropdownMenuItem` entries inside `ItemActions` inside `Item` | | `ListItem` `banner` prop | Sibling element below `Item` (no direct DS slot) | | `ListItem` arbitrary `FlexProps` (px, py, h) | `className` on `Item`; use Tailwind utilities | | `ListItemActions` (wrapper with action menu) | `Item` + `ItemActions` + `DropdownMenu` from `@aircall/ds` | | `ListPaginated` | `ItemGroup` + `Pagination` (and its parts) from `@aircall/ds` | `@dashboard/library` `List` wrapped items in `<ul>/<li>` and auto-added dividers via CSS. `ItemGroup` uses `role="list"` and a `stackedItems` prop — set `stackedItems` to replicate the no-gap, border-between-items visual style. `ListItem` accepted raw `FlexProps` from `@aircall/tractor` (spacing tokens, color tokens). `Item` extends `React.ComponentProps<'div'>` — use `className` with Tailwind utilities instead. ## 2. Imports ```tsx // DS primitives — item / list family import { Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle } from '@aircall/ds'; // DS pagination (for ListPaginated) import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from '@aircall/ds'; // DS dropdown (for dropdownOptions) import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@aircall/ds'; ``` No `@aircall/blocks` imports are needed for this migration — `Item*` and `ItemGroup` live entirely in `@aircall/ds`. ## 3. Before / After ### 3a. Basic List with avatar + text items **Before (`@dashboard/library`):** ```tsx import { List, ListItem } from '@dashboard/library'; function UserList({ users }: { users: { id: string; name: string; role: string; avatar: React.ReactElement }[] }) { return ( <List> {users.map((u) => ( <ListItem key={u.id} avatar={u.avatar} primaryText={u.name} secondaryText={u.role} /> ))} </List> ); } ``` **After (`@aircall/ds`):** ```tsx import { Item, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle } from '@aircall/ds'; function UserList({ users }: { users: { id: string; name: string; role: string; avatar: React.ReactElement }[] }) { return ( <ItemGroup stackedItems> {users.map((u) => ( <Item key={u.id} variant="outline"> <ItemMedia variant="image">{u.avatar}</ItemMedia> <ItemContent> <ItemTitle>{u.name}</ItemTitle> <ItemDescription>{u.role}</ItemDescription> </ItemContent> </Item> ))} </ItemGroup> ); } ``` Key changes: - `List` `ItemGroup` with `stackedItems` to preserve the stacked-row layout with collapsed borders between items. - `ListItem` `Item` with `variant="outline"` (gives each row a visible border). - `avatar` `ItemMedia variant="image"` as first child of `Item`. - `primaryText` `ItemTitle` inside `ItemContent`. - `secondaryText` `ItemDescription` inside `ItemContent`. - Drop all `FlexProps` token spreads; use `className` with Tailwind utilities instead. ### 3b. ListItem with custom children and dropdown actions **Before (`@dashboard/library`):** ```tsx import { List, ListItem } from '@dashboard/library'; function IntegrationList({ items }: { items: { id: string; name: string }[] }) { return ( <List> {items.map((item) => ( <ListItem key={item.id} primaryText={item.name} dropdownOptions={[ { title: 'Edit', callback: () => onEdit(item.id) }, { title: 'Delete', callback: () => onDelete(item.id) } ]} /> ))} </List> ); } ``` **After (`@aircall/ds`):** ```tsx import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Item, ItemActions, ItemContent, ItemGroup, ItemTitle } from '@aircall/ds'; import { Button } from '@aircall/ds'; import { EllipsisVertical } from '@aircall/react-icons'; function IntegrationList({ items }: { items: { id: string; name: string }[] }) { return ( <ItemGroup stackedItems> {items.map((item) => ( <Item key={item.id} variant="outline"> <ItemContent> <ItemTitle>{item.name}</ItemTitle> </ItemContent> <ItemActions> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" size="icon" aria-label="More actions"> <EllipsisVertical /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuItem onClick={() => onEdit(item.id)}>Edit</DropdownMenuItem> <DropdownMenuItem onClick={() => onDelete(item.id)}>Delete</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </ItemActions> </Item> ))} </ItemGroup> ); } ``` Key changes: - `dropdownOptions` prop explicit `DropdownMenu` composition inside `ItemActions`. - `ItemActions` becomes the last child of `Item`; the flex layout positions it to the right automatically. - Icons: import from `@aircall/react-icons`, not `lucide-react` directly. ### 3c. ListItemActions (card-style item with action menu) **Before (`@dashboard/library`):** ```tsx import { ListItemActions } from '@dashboard/library'; function PhoneNumberRow({ number, onEdit, onDelete }: { number: string; onEdit: () => void; onDelete: () => void }) { return ( <ListItemActions actions={[ { name: 'Edit', onClick: onEdit }, { name: 'Delete', onClick: onDelete } ]} > <span>{number}</span> </ListItemActions> ); } ``` **After (`@aircall/ds`):** ```tsx import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Item, ItemActions, ItemContent, ItemTitle } from '@aircall/ds'; import { Button } from '@aircall/ds'; import { EllipsisVertical } from '@aircall/react-icons'; function PhoneNumberRow({ number, onEdit, onDelete }: { number: string; onEdit: () => void; onDelete: () => void }) { return ( <Item variant="outline"> <ItemContent> <ItemTitle>{number}</ItemTitle> </ItemContent> <ItemActions> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" size="icon" aria-label="More actions"> <EllipsisVertical /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuItem onClick={onEdit}>Edit</DropdownMenuItem> <DropdownMenuItem onClick={onDelete}>Delete</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </ItemActions> </Item> ); } ``` Key changes: - `ListItemActions` `actions` prop `DropdownMenu` composition inside `ItemActions`. - `ListItemActions` `children` `ItemContent` / `ItemTitle` inside `Item`. - The absolute-positioned action menu is replaced by `ItemActions` flex layout. ### 3d. ListPaginated **Before (`@dashboard/library`):** ```tsx import { ListPaginated, ListItem } from '@dashboard/library'; function PaginatedUsers({ users, page, onPageChange, onPageSizeChange, total, pageSize }) { return ( <ListPaginated activePage={page} onPageChange={onPageChange} onPageSizeChange={onPageSizeChange} pageSize={pageSize} recordsTotalCount={total} > {users.map((u) => ( <ListItem key={u.id} primaryText={u.name} /> ))} </ListPaginated> ); } ``` **After (`@aircall/ds`):** ```tsx import { Item, ItemContent, ItemGroup, ItemTitle, Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from '@aircall/ds'; function PaginatedUsers({ users, page, onPageChange, total, pageSize }) { const totalPages = Math.ceil(total / pageSize); return ( <div className="flex flex-col gap-3"> <ItemGroup stackedItems> {users.map((u) => ( <Item key={u.id} variant="outline"> <ItemContent> <ItemTitle>{u.name}</ItemTitle> </ItemContent> </Item> ))} </ItemGroup> <Pagination> <PaginationContent> <PaginationItem> <PaginationPrevious onClick={() => onPageChange(page - 1)} aria-disabled={page <= 1} /> </PaginationItem> {Array.from({ length: totalPages }, (_, i) => ( <PaginationItem key={i + 1}> <PaginationLink isActive={page === i + 1} onClick={() => onPageChange(i + 1)}> {i + 1} </PaginationLink> </PaginationItem> ))} <PaginationItem> <PaginationNext onClick={() => onPageChange(page + 1)} aria-disabled={page >= totalPages} /> </PaginationItem> </PaginationContent> </Pagination> </div> ); } ``` Key changes: - `ListPaginated` has no DS equivalent — decompose into `ItemGroup` + `Pagination` parts. - `@aircall/ds` `Pagination` is a composable primitive; build page links explicitly. - `onPageSizeChange` has no DS equivalent — implement with a `Select` from `@aircall/ds` if needed. --- ## 4. Common mistakes ### Mistake 1Missing stackedItems on ItemGroup ```tsx // ❌ Wrong — gaps between items, no stacked border collapse <ItemGroup> <Item variant="outline">…</Item> <Item variant="outline">…</Item> </ItemGroup> // ✅ Correct — stacked rows with collapsed borders (matches List visual style) <ItemGroup stackedItems> <Item variant="outline">…</Item> <Item variant="outline">…</Item> </ItemGroup> ``` Without `stackedItems`, `ItemGroup` applies `gap-4` between items (card-grid style). With `stackedItems`, it collapses borders between adjacent `Item` rows and removes gap, replicating the `List` border-separated row appearance. Source: `packages/ds/src/components/item.tsx` ### Mistake 2Spreading tractor FlexProps onto Item ```tsx // ❌ Wrong — tractor token props (px, py, h, color) do not exist on Item <Item px="s" py="xxs" h="xl" color="neutral-600"> <ItemContent>…</ItemContent> </Item> // ✅ Correct — use className with Tailwind utilities <Item className="px-3 py-1.5 min-h-10"> <ItemContent>…</ItemContent> </Item> ``` `ListItem` accepted arbitrary `FlexProps` from `@aircall/tractor` (design tokens). `Item` extends `React.ComponentProps<'div'>` — only standard HTML attributes and `className` are accepted. Passing token strings results in React warnings for unknown DOM attributes. Source: `packages/ds/src/components/item.tsx` ### Mistake 3Placing ItemActions outside Item ```tsx // ❌ Wrong — ItemActions outside Item has no flex parent to pin it right <ItemGroup stackedItems> <Item variant="outline"> <ItemContent><ItemTitle>Name</ItemTitle></ItemContent> </Item> <ItemActions> <Button>Edit</Button> </ItemActions> </ItemGroup> // ✅ Correct — ItemActions is the last child inside Item <ItemGroup stackedItems> <Item variant="outline"> <ItemContent><ItemTitle>Name</ItemTitle></ItemContent> <ItemActions> <Button>Edit</Button> </ItemActions> </Item> </ItemGroup> ``` `Item` is a flex row (`flex items-center`). `ItemActions` carries `data-slot="item-actions"` and relies on being a flex child to align right. Outside `Item`, it renders in document flow with no flex context. Source: `packages/ds/src/components/item.tsx` ### Mistake 4Using ListItemActions children directly as ItemTitle text ```tsx // ❌ Wrong — bare string child of Item is not styled as a title <Item variant="outline"> {number} <ItemActions>…</ItemActions> </Item> // ✅ Correct — wrap in ItemContent > ItemTitle for correct typography and layout slot <Item variant="outline"> <ItemContent> <ItemTitle>{number}</ItemTitle> </ItemContent> <ItemActions>…</ItemActions> </Item> ``` `ListItemActions` accepted `children` as a raw React node and rendered it freely. `Item` is a flex row — raw text nodes do not receive the `ItemTitle` typography or the `flex-1` grow behaviour that `ItemContent` provides. Without `ItemContent`, `ItemActions` may not push to the right end of the row. Source: `packages/ds/src/components/item.tsx` ### Mistake 5Importing icons from lucide-react directly ```tsx // ❌ Wrong — imports lucide directly, bypassing the Aircall icon layer import { MoreVertical } from 'lucide-react'; // ✅ Correct — route all icons through @aircall/react-icons import { EllipsisVertical } from '@aircall/react-icons'; ``` `@aircall/react-icons` re-exports all lucide icons plus Aircall custom icons. Direct lucide imports bypass the single source of truth and prevent Aircall icon overrides. Source: `packages/ds/src/components/item.tsx`