---
name: aircall-blocks/migrate-dashboard
description: >
  Migrate a file from @dashboard/library to @aircall/blocks / @aircall/ds. Load FIRST
  to get the full component-level routing table before picking a recipe. Identifies what
  has a blocks/ds equivalent, what is out of scope, and which sub-skill to load next.
type: core
library: aircall-blocks
requires:
  - aircall-blocks/setup
sources:
  - "aircall/hydra:docs/migration-guides/dashboard-lib-to-blocks/AGENTS.md"
---

# Migrate @dashboard/library → @aircall/blocks / @aircall/ds

This is the overview router for the `@dashboard/library` migration. Load it first to
identify the right sub-skill for each component you are migrating, and to determine what
falls outside the scope of blocks and ds entirely.

> **Already migrated?** To *verify* existing `@aircall/blocks` / `@aircall/ds` code is on the
> latest shared components and props (rather than convert `@dashboard/library` imports), load
> `@aircall/blocks#aircall-blocks/migrate-dashboard/catch-up-checklist`.

## How to run this migration (end-to-end)

Migrate incrementally — one screen/file at a time, shipping each green:

0. **Set up once** — load `@aircall/blocks#aircall-blocks/setup` (it builds on
   `@aircall/ds#aircall-ds/setup`): install `@aircall/blocks` + `@aircall/ds`, import both
   `globals.css` bundles, and mount the DS root providers — including `DsI18nProvider`
   (under your react-i18next provider, fed the user's language; importing `@aircall/blocks`
   registers a `blocks` i18n namespace, so it needs DS's i18n engine) and
   `NotificationQueueProvider` if you use banners. Add the jsdom test shims from setup.
   (Loaded automatically via `requires`, but do the wiring first.)
1. **Inventory** — list the file's `@dashboard/library` imports. The routing table below
   says which have a blocks/ds equivalent and which are out of scope (hooks/utils/constants
   don't migrate — leave them on `@dashboard/library`).
2. **Migrate** — load the per-area recipe from the routing table for each UI component. Any
   Tractor primitives in the same file migrate via `@aircall/ds#aircall-ds/migrate-tractor`;
   icons via `@aircall/ds#aircall-ds/migrate-icons`.
3. **Verify green** — `tsc --noEmit`, tests (DS popups/Switch need the setup jsdom shims),
   biome/lint.
4. **Repeat** until no in-scope `@dashboard/library` UI imports remain.

## Cross-cutting note

`@dashboard/library` mixes UI components with non-UI utilities. Only the UI components
migrate. When writing replacement code:

- Import block-level compositions (page layout, empty states, form layer) from
  `'@aircall/blocks'`.
- Import DS primitives (Card, Spinner, DataTable, Combobox, ItemGroup, etc.) from
  `'@aircall/ds'`.
- Never mix the two import paths for the same logical component — pick the package that
  owns it per the table below.

## Forms — never local state

Any form that collects and submits data migrates to **`@aircall/blocks` `useForm` + the
`Form*Field` wrappers** (the Storybook-proven `CommonForm` pattern) — NOT React
`useState` per field, and NOT bare ds `Field`/`Input` primitives wired by hand. The form
owns field state, validation, dirty/`canSubmit`/`isSubmitting`, and error display; those
are what drive `SubmitButton`/`CardSaveBar`. Hand-rolled `useState` bypasses all of it and
must be rewritten, not preserved, during migration. (`useState` for non-field UI — open,
active tab — is fine.) Converting a large legacy `useState` form is a deliberate refactor,
not a 1:1 swap. See `…/migrate-dashboard/form-wizard` and `@aircall/ds#aircall-ds/migrate-tractor/form-and-field`.

## Out of scope

The following `@dashboard/library` exports do NOT have an equivalent in `@aircall/blocks`
or `@aircall/ds`. Do not try to map them to a DS component. They need a shared
utils/data home or must stay in the consuming app.

**Hooks**: `useGraphQuery`, `useGraphMutation`, `useToast`, `useToggle`,
`useBroadcastChannel`

**Helpers / utils**: `generateRandomUUID`, `isTruthy`, `toFixedNumber`,
`capitalizeFirstLetter`, `getInitials`

**Constants, types, and contexts**: `ROLE_NAME`, `RESOURCE`,
`NavigationBlockerProvider`, `ClientError`

**UI with no blocks/ds equivalent yet**: some `@dashboard/library` UI components have
no target in `@aircall/blocks` or `@aircall/ds` yet (e.g. `PieChart` and other charts,
`TileLegend`). If a UI component is not in the routing table below and
not listed above, leave it imported from `@dashboard/library` for now — do not force a
migration or invent a target.

## Not yet migratable (tracked elsewhere)

Components whose migration decision is still **TBD**, **TODO**, or **Ready for dev** are not built yet — do not migrate them or invent a target. They are tracked in `dashboard-modules/COMPONENT-MIGRATION-DECISIONS.md`. Known example: `DaysPicker` (→ a future `@aircall/blocks` `DaysPicker` block, not yet built).

## Component routing table

| @dashboard/library | Target (pkg) | Recipe to load | Status |
|---|---|---|---|
| `MessageScreen` family + `UnknownError` | `ComingSoonEmptyState`, `RestrictedAccessEmptyState`, `NotFoundEmptyState`, `NoDataEmptyState`, `NoSupportEmptyState`, `UnknownErrorEmptyState` and the `EmptyState*` parts (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/empty-states | available |
| `PageHeader` | `DashboardPageHeader` (+ `DashboardPageHeaderTitle` / `DashboardPageHeaderActions` / `DashboardPageHeaderAction` / `DashboardPageHeaderNav` / `DashboardPageHeaderDescription`) (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/page-header | available |
| Page / screen shell — incl. full-page flows (Campaign Creation, Add Contacts) | `DashboardPage` (standard: sidebar + header + tabs + content) / `DashboardStandalonePage` (full-page, no sidebar) (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/dashboard-page | available |
| `Paper` / `PaperForm` (settings-page surface) | `SettingCard` (+ `SettingCardHeader` / `SettingCardTitle` / `SettingCardDescription` / `SettingCardAction` / `SettingCardContent`), plus `useForm` + `CardSaveBar` for the save bar (@aircall/blocks). Keep the `errorBoundary` (`ScopedErrorBoundary`) **outside** the card. | load @aircall/blocks#aircall-blocks/migrate-dashboard/setting-card | available |
| `Paper` (generic non-settings surface — tile/container, no title/toggle semantics) | `Card` (+ parts) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/card | available |
| `LoadMoreTable` | `DataTable` (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/data-table | available |
| `MultiSearchSelect` + `MultiInlineSearchSelect` + `MultiSelectOption` | `Combobox` with `multiple` (multi-select) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/combobox | available |
| `SingleSearchSelect` / `SearchSelect` / single-value `MultiSelect` | `Combobox` WITHOUT `multiple` (single-select) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/combobox | available |
| `FormWizard` + `useFormWizard` + `FormField` | `useForm`, `CommonForm`, `FormInputField`/`FormSelectField`/`FormComboboxField`/etc., `CardSaveBar`/`SaveBar` (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/form-wizard | available |
| `Loading` | `Spinner` (and `Skeleton` for content placeholders) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/loading | available |
| `List` + `ListItem` | `ItemGroup`, `Item`, `ItemMedia`, `ItemContent`, `ItemActions` (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/list | available |
| `Tile` + `TileHeader` + `TileValue` | `KpiCard` + `KpiValue` + `KpiDescription` (+ `KpiValueSkeleton` / `KpiDescriptionSkeleton`) with DS `CardHeader` / `CardTitle` / `CardContent` / `CardAction` (@aircall/blocks + @aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/tile | available |
| `GridLayout` + `GridItem` + `Gap` | native `<div>` + Tailwind grid/flex/gap utilities (NO component import needed — these are layout primitives) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/layout | available |
| `AudioPlayer` | `Audio`, `AudioPlayerRow`, `AudioPlayerButton`, `AudioPlayerBar`, `AudioPlayerTime`, `AudioPlayerSpeed`, `AudioPlayerSkip`, `AudioPlayerManager`, `useAudioPlayer` (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-audio-player | available |
| `InfoPopup` + `InfoPopupTrigger` + `InfoPopupContent` | `HoverCard`, `HoverCardTrigger`, `HoverCardContent` (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/info-popup | available |
| `CopyToClipboardButton` + `CopyToClipboardText` | `CopyButton`, `CopyButtonIcon`, `CopyButtonLabel` (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/copy-button | available |
| `ToggleRow` | `Field` (orientation="horizontal") + `Switch` + `FieldLabel` + optional `FieldDescription` (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/toggle-row | available |
| `AddButton` + `SaveButton` + `LoadingButton` | `Button` (@aircall/ds) — `AddButton` → `Button` with leading icon; `LoadingButton` loading state → `Button disabled` + `Spinner`; `SaveButton` saved state → `Button variant="outline"` + check icon | load @aircall/ds#aircall-ds/migrate-tractor/button | available |
| `ConditionalTooltip` | Conditional JSX: wrap children in `<Tooltip>` / `<TooltipTrigger>` / `<TooltipContent>` when condition is true, render children bare when false — no wrapper component needed (@aircall/ds) | inline — no sub-skill | available |
| `RadioBoxGroup` + `RadioBox` | `RadioGroup` + `RadioGroupItem` wrapped in `Field` + `FieldLabel` + `FieldContent` (@aircall/ds) | inline — see @aircall/ds#aircall-ds/migrate-tractor/form-and-field for Field patterns | available |
| `TagBeautified` | `Badge` with `legacyColor` prop for stored hex colors; `Badge` with `color` + `tone` props for new tags (@aircall/ds) | inline — `<Badge legacyColor="#0662B5">Sales</Badge>` | available |
| `RolesTags` | `RoleBadge` with `role` prop (`"owner"` \| `"admin"` \| `"supervisor"` \| `"agent"`) (@aircall/blocks) | inline — `<RoleBadge role="admin" />` | available |
| `Count` | `CounterBadge` — pass the capped value as children: `<CounterBadge>{n > 99 ? '99+' : n}</CounterBadge>` (@aircall/ds) | inline — no size or max props | available |
| `Avatar` | `Avatar`, `AvatarImage`, `AvatarFallback` compound (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/avatar | available |
| `ProgressBar` + `OptimisticProgressBar` | `Progress` with `value` prop (0–100) (@aircall/ds) | inline — `<Progress value={60} />` | available |
| `ShadowScrollContainer` | `ScrollArea` with `scrollFade` prop — use a fixed `h-[…]` not `max-h-[…]` on the root (@aircall/ds) | inline — `<ScrollArea scrollFade className="h-[400px] rounded-md border">` | available |
| `AccordionSection` | `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent` (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/accordion | available |
| `ConfirmationModal` | `AlertDialog` and its parts (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/dialog | available |
| `Tab` | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/tabs | available |
| `Skeleton` + `SkeletonText` | `Skeleton` (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/skeleton | available |
| `Editable` | `InlineEditInput` / `InlineEditTextarea` / `InlineEditSelect` / `InlineEditMultiselect` (+ sub-parts) (@aircall/ds) | inline — click-to-edit; single-value via `InlineEditInput`/`InlineEditTextarea`/`InlineEditSelect`, multi via `InlineEditMultiselect`. Data-driven accessor API like `DataCombobox` (`items` + `getItemValue`/`getItemLabel`) | available |
| `Dropzone` | `Dropzone` (@aircall/ds); render the dropped files with `Attachment` (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/dropzone — display files via @aircall/ds#aircall-ds/adopt-attachment | available |
| `EmojiPicker` | `EmojiPicker` (@aircall/ds) | inline — no sub-skill | available |
| `DateSelect` | `Input` + `Popover` + `Calendar` (@aircall/ds) | inline — a `Calendar` in a `Popover` behind an `Input` trigger | available |
| `TagStack` | `BadgeGroup` (@aircall/ds) | inline — `<BadgeGroup>` wrapping `Badge` children | available |
| `TagHighlightTextarea` | `RichTextarea` with `#tag` colored pills (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/rich-textarea | available |
| `AvailabilitySelect` / `Select` | `Select` (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/select | available |
| `Chevron` | `Icon` (@aircall/react-icons) | inline — import the specific directional icon (`ChevronDown`, `ChevronRight`, …); no `<Chevron>` wrapper | available |
| `InlineForm` / `InlineFormWithSingleSearchSelect` | `useForm` + DS primitives (+ `Combobox` for the search-select variant) (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/form-wizard | available |
| `TimeSlotsForm` | `useForm` + 2 blocks (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/form-wizard — too specific to extract; compose at app level | available |

## How to use this router

1. Identify the `@dashboard/library` export(s) in the file you are migrating.
2. Check the **Out of scope** section first — if the export is listed there, skip it.
3. For each UI component, find the matching row in the table above and load the
   indicated recipe sub-skill.
4. Each sub-skill is self-contained: it carries the full prop mapping, common mistakes,
   and import paths for its component family.
