UNPKG

@aircall/blocks

Version:

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

480 lines (409 loc) 13 kB
--- name: aircall-blocks/migrate-dashboard/tile description: > Migrate @dashboard/library Tile, TileHeader, TileValue (+ TileBody, TileActions, TileExtra, TileNumber, TileDuration, TileText) to @aircall/blocks KpiCard compositions (KpiCard, KpiValue, KpiDescription, KpiValueSkeleton, KpiDescriptionSkeleton) plus DS CardHeader / CardTitle / CardContent / CardAction. Load when a file imports Tile, TileHeader, or TileValue from @dashboard/library. type: sub-skill library: aircall-blocks requires: - aircall-blocks/setup - aircall-blocks/migrate-dashboard sources: - "aircall/hydra:packages/blocks/src/components/kpi.tsx" - "aircall/hydra:packages/blocks/src/helpers/format-kpi.ts" - "aircall/hydra:packages/ds/src/components/card.tsx" --- This skill builds on aircall-blocks/migrate-dashboard. ## 1. Component mapping | @dashboard/library | Target | | --- | --- | | `Tile` (root container) | `KpiCard` (`@aircall/blocks`) — DS `Card` wrapper, `size="sm"` by default | | `Tile` `disabled` prop | `KpiCard disabled` — applies `opacity-50` + `cursor-not-allowed` and cascades to nested `KpiValue`s | | `TileHeader` `label` prop | DS `CardTitle` inside DS `CardHeader` | | `TileHeader` `tooltipTexts` prop | inline DS `Tooltip` / `TooltipTrigger` / `TooltipContent` next to `CardTitle` | | `TileBody` (metric row wrapper) | DS `CardContent` | | `TileActions` (top-right action overlay) | DS `CardAction` inside `CardHeader` | | `TileValue` (number / duration / text / percent) | `KpiValue` (`@aircall/blocks`) — use `kind`, `durationStyle`, `unit`, `onClick`, `active` | | `TileValue loading` | `KpiValueSkeleton` (and `KpiDescriptionSkeleton` when a comparison line loads) | | `TileExtra` `secondaryText` prop | `KpiDescription` children | | `TileExtra` `trend` prop | `KpiDescription` `trend` + `tone` (`up` / `down` / `neutral` × `positive` / `negative` / `neutral`) | | `TileProvider` (internal context) | removed — `KpiCard` provides disabled context | `TILE_VALUE` enum and `TileValueSizes` type are internal to `@dashboard/library`; delete them from the import when migrating. Map former types via `KpiValue` `kind`: | Former `TILE_VALUE` / type | `KpiValue` props | | --- | --- | | number | `kind="number"` (default for numeric `value`) | | percent | `kind="percent"` | | duration (long / verbose) | `kind="duration"` (`durationStyle="long"` default) | | duration clock HH:MM | `kind="duration" durationStyle="clock" durationScale="hours"` | | duration clock MM:SS | `kind="duration" durationStyle="clock" durationScale="minutes"` | | opaque string | string `value` (or `kind="text"`) | ## 2. Imports ```tsx // Blocks — KPI composition import { KpiCard, KpiValue, KpiDescription, KpiValueSkeleton, KpiDescriptionSkeleton } from '@aircall/blocks'; // DS — card slots + optional tooltip / actions import { Button, CardAction, CardContent, CardHeader, CardTitle, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@aircall/ds'; ``` ## 3. Before / After ### 3a. Basic tile — number value **Before (`@dashboard/library`):** ```tsx import { Tile, TileBody, TileHeader, TileValue } from '@dashboard/library'; function AnsweredTile() { return ( <Tile> <TileHeader label="Answered" /> <TileBody> <TileValue value={123} /> </TileBody> </Tile> ); } ``` **After (`@aircall/blocks` + `@aircall/ds`):** ```tsx import { CardContent, CardHeader, CardTitle } from '@aircall/ds'; import { KpiCard, KpiValue } from '@aircall/blocks'; function AnsweredTile() { return ( <KpiCard className="min-w-36"> <CardHeader> <CardTitle>Answered</CardTitle> </CardHeader> <CardContent> <KpiValue value={123} kind="number" /> </CardContent> </KpiCard> ); } ``` Key changes: - `Tile` → `KpiCard`. Optional `minWidth` (default 144 px) becomes `className="min-w-36"`. - `TileHeader label` → `CardTitle` inside `CardHeader`. - `TileBody` → `CardContent`. - `TileValue` → `KpiValue` (formatting + typography owned by the block). ### 3b. Tile with loading state **Before (`@dashboard/library`):** ```tsx import { Tile, TileBody, TileHeader, TileValue } from '@dashboard/library'; function LoadingTile() { return ( <Tile> <TileHeader label="Answered" /> <TileBody> <TileValue loading /> </TileBody> </Tile> ); } ``` **After:** ```tsx import { CardContent, CardHeader, CardTitle } from '@aircall/ds'; import { KpiCard, KpiValueSkeleton } from '@aircall/blocks'; function LoadingTile() { return ( <KpiCard className="min-w-36"> <CardHeader> <CardTitle>Answered</CardTitle> </CardHeader> <CardContent> <KpiValueSkeleton /> </CardContent> </KpiCard> ); } ``` When a comparison line also loads, pair with `KpiDescriptionSkeleton` inside `CardContent className="flex flex-col gap-1"`. ### 3c. Tile with tooltip on header **Before (`@dashboard/library`):** ```tsx import { Tile, TileBody, TileHeader, TileValue } from '@dashboard/library'; function InfoTile() { return ( <Tile> <TileHeader label="Answered" tooltipTexts={{ info: 'Calls answered in the period' }} /> <TileBody> <TileValue value={123} /> </TileBody> </Tile> ); } ``` **After:** ```tsx import { CardContent, CardHeader, CardTitle, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@aircall/ds'; import { InformationCircleIcon } from '@aircall/react-icons'; import { KpiCard, KpiValue } from '@aircall/blocks'; function InfoTile() { return ( <KpiCard className="min-w-36"> <CardHeader> <CardTitle className="flex items-center gap-1"> Answered <TooltipProvider> <Tooltip> <TooltipTrigger render={<button type="button" className="inline-flex" aria-label="More info" />} > <InformationCircleIcon className="h-3 w-3 cursor-default" aria-hidden /> </TooltipTrigger> <TooltipContent>Calls answered in the period</TooltipContent> </Tooltip> </TooltipProvider> </CardTitle> </CardHeader> <CardContent> <KpiValue value={123} /> </CardContent> </KpiCard> ); } ``` ### 3d. Tile with actions + clickable value **Before (`@dashboard/library`):** ```tsx import { Gap, Tile, TileActions, TileBody, TileHeader, TileValue } from '@dashboard/library'; import { IconButton } from '@aircall/tractor'; import { SettingsFilled, MenuVerticalFilled } from '@aircall/icons'; function ActionTile() { return ( <Tile> <TileActions> <Gap gap="xxxs"> <IconButton component={SettingsFilled} size={16} onClick={() => null} type="button" /> <IconButton component={MenuVerticalFilled} size={16} onClick={() => null} type="button" /> </Gap> </TileActions> <TileHeader label="Answered" /> <TileBody> <TileValue onClick={() => null} value={123} /> </TileBody> </Tile> ); } ``` **After:** ```tsx import { Button, CardAction, CardContent, CardHeader, CardTitle } from '@aircall/ds'; import { EllipsisVertical, Settings } from '@aircall/react-icons'; import { KpiCard, KpiValue } from '@aircall/blocks'; function ActionTile() { return ( <KpiCard className="min-w-36"> <CardHeader> <CardTitle>Answered</CardTitle> <CardAction className="flex items-center gap-1"> <Button variant="ghost" size="icon-sm" aria-label="Settings" onClick={() => null}> <Settings /> </Button> <Button variant="ghost" size="icon-sm" aria-label="More options" onClick={() => null}> <EllipsisVertical /> </Button> </CardAction> </CardHeader> <CardContent> <KpiValue value={123} onClick={() => null} /> </CardContent> </KpiCard> ); } ``` Key changes: - `TileActions` → `CardAction` inside `CardHeader`. - `TileValue onClick` → `KpiValue onClick` (dotted underline; `active` for solid underline). ### 3e. Disabled tile **Before (`@dashboard/library`):** ```tsx import { Tile, TileBody, TileHeader, TileValue } from '@dashboard/library'; function DisabledTile() { return ( <Tile disabled> <TileHeader label="Answered" /> <TileBody> <TileValue onClick={() => null} value={123} /> </TileBody> </Tile> ); } ``` **After:** ```tsx import { CardContent, CardHeader, CardTitle } from '@aircall/ds'; import { KpiCard, KpiValue } from '@aircall/blocks'; function DisabledTile() { return ( <KpiCard disabled className="min-w-36"> <CardHeader> <CardTitle>Answered</CardTitle> </CardHeader> <CardContent> <KpiValue value={123} onClick={() => null} /> </CardContent> </KpiCard> ); } ``` `KpiCard disabled` applies `opacity-50` + `cursor-not-allowed` and cascades so nested `KpiValue` buttons cannot be activated — no need to strip `onClick` or style the value by hand. ### 3f. Comparison / trend line (TileExtra) **Before:** `TileExtra` with `secondaryText` + `trend`. **After:** ```tsx import { CardContent, CardHeader, CardTitle } from '@aircall/ds'; import { KpiCard, KpiDescription, KpiValue } from '@aircall/blocks'; function OutboundCallsTile() { return ( <KpiCard className="min-w-36"> <CardHeader> <CardTitle>Outbound calls</CardTitle> </CardHeader> <CardContent className="flex flex-col gap-1"> <KpiValue value={87} onClick={() => null} /> <KpiDescription trend="up" tone="positive"> +16.3% vs previous period </KpiDescription> </CardContent> </KpiCard> ); } ``` `trend` is icon direction only; `tone` is color only — an upward change that is bad uses `trend="up" tone="negative"`. ### 3g. Duration values ```tsx {/* Long: "1h 2min 3s" */} <KpiValue value={3723} kind="duration" /> {/* Clock hours: "01:38 hr/min" */} <KpiValue value={5880} kind="duration" durationStyle="clock" durationScale="hours" /> {/* Clock minutes: "05:04 min/sec" */} <KpiValue value={304} kind="duration" durationStyle="clock" durationScale="minutes" /> {/* Duration + unit label */} <KpiValue value={112} kind="duration" unit="avg" /> ``` Do not reimplement `formatSecondsToDuration` in the app — `KpiValue` owns formatting. --- ## 4. Common mistakes ### Mistake 1 — Using raw DS `Card` instead of `KpiCard` ```tsx // ❌ Wrong — loses disabled cascade and KPI defaults (size="sm", gap) <Card size="sm"> <CardHeader> <CardTitle>Answered</CardTitle> </CardHeader> <CardContent> <KpiValue value={123} /> </CardContent> </Card> // ✅ Correct <KpiCard> <CardHeader> <CardTitle>Answered</CardTitle> </CardHeader> <CardContent> <KpiValue value={123} /> </CardContent> </KpiCard> ``` ### Mistake 2 — Hand-formatting values or using raw `Skeleton` ```tsx // ❌ Wrong <span className="text-3xl font-bold">{formatDuration(123)}</span> <Skeleton className="h-6 w-18" /> // ✅ Correct <KpiValue value={123} kind="duration" /> <KpiValueSkeleton /> ``` ### Mistake 3 — Passing Tile/TileBody flex props to KpiCard / CardContent ```tsx // ❌ Wrong — tractor FlexProps on the card <KpiCard minWidth="228px" px="xxs" py="xxs"> <CardContent alignItems="center" gap="xxxs"></CardContent> </KpiCard> // ✅ Correct — Tailwind via className <KpiCard className="min-w-[228px]"> <CardContent className="flex flex-col gap-1"></CardContent> </KpiCard> ``` ### Mistake 4 — Placing CardAction outside CardHeader ```tsx // ❌ Wrong <KpiCard> <CardAction></CardAction> <CardHeader> <CardTitle>Answered</CardTitle> </CardHeader> <CardContent></CardContent> </KpiCard> // ✅ Correct — CardAction must be a child of CardHeader <KpiCard> <CardHeader> <CardTitle>Answered</CardTitle> <CardAction></CardAction> </CardHeader> <CardContent></CardContent> </KpiCard> ``` ### Mistake 5 — Keeping TILE_VALUE / TileValue imports after migration ```tsx // ❌ Wrong import { TILE_VALUE, Tile, TileHeader, TileValue } from '@dashboard/library'; <TileValue type={TILE_VALUE.DURATION} value={123} /> // ✅ Correct import { KpiCard, KpiValue } from '@aircall/blocks'; import { CardContent, CardHeader, CardTitle } from '@aircall/ds'; <KpiCard> <CardHeader> <CardTitle>AHT</CardTitle> </CardHeader> <CardContent> <KpiValue value={123} kind="duration" /> </CardContent> </KpiCard> ``` ### Mistake 6 — Using CardTitle for the metric value instead of the label ```tsx // ❌ Wrong — CardTitle used for the big number <KpiCard> <CardHeader> <CardTitle>123</CardTitle> </CardHeader> </KpiCard> // ✅ Correct — CardTitle = label; KpiValue = metric <KpiCard> <CardHeader> <CardTitle>Answered</CardTitle> </CardHeader> <CardContent> <KpiValue value={123} /> </CardContent> </KpiCard> ```