@aircall/blocks 0.17.0 → 0.18.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aircall/blocks",
3
- "version": "0.17.0",
3
+ "version": "0.18.1",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "sideEffects": [
@@ -82,7 +82,7 @@
82
82
  "vite": "7.3.1",
83
83
  "vitest": "4.0.17",
84
84
  "zod": "4.4.3",
85
- "@aircall/ds": "0.26.0",
85
+ "@aircall/ds": "0.28.1",
86
86
  "@aircall/hooks": "0.6.0"
87
87
  },
88
88
  "keywords": [
@@ -6,7 +6,6 @@ description: >
6
6
  has a blocks/ds equivalent, what is out of scope, and which sub-skill to load next.
7
7
  type: core
8
8
  library: aircall-blocks
9
- library_version: "0.5.1"
10
9
  requires:
11
10
  - aircall-blocks/setup
12
11
  sources:
@@ -19,6 +18,10 @@ This is the overview router for the `@dashboard/library` migration. Load it firs
19
18
  identify the right sub-skill for each component you are migrating, and to determine what
20
19
  falls outside the scope of blocks and ds entirely.
21
20
 
21
+ > **Already migrated?** To *verify* existing `@aircall/blocks` / `@aircall/ds` code is on the
22
+ > latest shared components and props (rather than convert `@dashboard/library` imports), load
23
+ > `@aircall/blocks#aircall-blocks/migrate-dashboard/catch-up-checklist`.
24
+
22
25
  ## How to run this migration (end-to-end)
23
26
 
24
27
  Migrate incrementally — one screen/file at a time, shipping each green:
@@ -84,6 +87,10 @@ no target in `@aircall/blocks` or `@aircall/ds` yet (e.g. `PieChart` and other c
84
87
  not listed above, leave it imported from `@dashboard/library` for now — do not force a
85
88
  migration or invent a target.
86
89
 
90
+ ## Not yet migratable (tracked elsewhere)
91
+
92
+ 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).
93
+
87
94
  ## Component routing table
88
95
 
89
96
  | @dashboard/library | Target (pkg) | Recipe to load | Status |
@@ -91,14 +98,15 @@ migration or invent a target.
91
98
  | `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 |
92
99
  | `PageHeader` | `DashboardPageHeader` (+ `DashboardPageHeaderTitle` / `DashboardPageHeaderActions` / `DashboardPageHeaderAction` / `DashboardPageHeaderNav` / `DashboardPageHeaderDescription`) (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/page-header | available |
93
100
  | 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 |
94
- | `Paper` | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`, `CardAction` (and @aircall/blocks `CardSaveBar` for a save bar) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/card | available |
101
+ | `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 |
102
+ | `Paper` (generic non-settings surface — tile/container, no title/toggle semantics) | `Card` (+ parts) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/card | available |
95
103
  | `LoadMoreTable` | `DataTable` (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/data-table | available |
96
104
  | `MultiSearchSelect` + `MultiInlineSearchSelect` + `MultiSelectOption` | `Combobox` with `multiple` (multi-select) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/combobox | available |
97
105
  | `SingleSearchSelect` / `SearchSelect` / single-value `MultiSelect` | `Combobox` WITHOUT `multiple` (single-select) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/combobox | available |
98
106
  | `FormWizard` + `useFormWizard` + `FormField` | `useForm`, `CommonForm`, `FormInputField`/`FormSelectField`/`FormComboboxField`/etc., `CardSaveBar`/`SaveBar` (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/form-wizard | available |
99
107
  | `Loading` | `Spinner` (and `Skeleton` for content placeholders) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/loading | available |
100
108
  | `List` + `ListItem` | `ItemGroup`, `Item`, `ItemMedia`, `ItemContent`, `ItemActions` (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/list | available |
101
- | `Tile` + `TileHeader` + `TileValue` | `Card` (+ parts) as the tile container (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/tile | available |
109
+ | `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 |
102
110
  | `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 |
103
111
  | `AudioPlayer` | `Audio`, `AudioPlayerRow`, `AudioPlayerButton`, `AudioPlayerBar`, `AudioPlayerTime`, `AudioPlayerSpeed`, `AudioPlayerSkip`, `AudioPlayerManager`, `useAudioPlayer` (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-audio-player | available |
104
112
  | `InfoPopup` + `InfoPopupTrigger` + `InfoPopupContent` | `HoverCard`, `HoverCardTrigger`, `HoverCardContent` (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/info-popup | available |
@@ -117,6 +125,15 @@ migration or invent a target.
117
125
  | `ConfirmationModal` | `AlertDialog` and its parts (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/dialog | available |
118
126
  | `Tab` | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/tabs | available |
119
127
  | `Skeleton` + `SkeletonText` | `Skeleton` (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/skeleton | available |
128
+ | `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 |
129
+ | `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 |
130
+ | `EmojiPicker` | `EmojiPicker` (@aircall/ds) | inline — no sub-skill | available |
131
+ | `DateSelect` | `Input` + `Popover` + `Calendar` (@aircall/ds) | inline — a `Calendar` in a `Popover` behind an `Input` trigger | available |
132
+ | `TagStack` | `BadgeGroup` (@aircall/ds) | inline — `<BadgeGroup>` wrapping `Badge` children | available |
133
+ | `AvailabilitySelect` / `Select` | `Select` (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/select | available |
134
+ | `Chevron` | `Icon` (@aircall/react-icons) | inline — import the specific directional icon (`ChevronDown`, `ChevronRight`, …); no `<Chevron>` wrapper | available |
135
+ | `InlineForm` / `InlineFormWithSingleSearchSelect` | `useForm` + DS primitives (+ `Combobox` for the search-select variant) (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/form-wizard | available |
136
+ | `TimeSlotsForm` | `useForm` + 2 blocks (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/form-wizard — too specific to extract; compose at app level | available |
120
137
 
121
138
  ## How to use this router
122
139
 
@@ -7,7 +7,6 @@ description: >
7
7
  Paper or PaperForm from @dashboard/library.
8
8
  type: sub-skill
9
9
  library: aircall-blocks
10
- library_version: "0.5.1"
11
10
  requires:
12
11
  - aircall-blocks/setup
13
12
  - aircall-blocks/migrate-dashboard
@@ -17,6 +16,12 @@ sources:
17
16
 
18
17
  This skill builds on aircall-blocks/migrate-dashboard.
19
18
 
19
+ > **Migrating a settings-page `Paper` / `PaperForm`?** Prefer `SettingCard` — load
20
+ > `@aircall/blocks#aircall-blocks/migrate-dashboard/setting-card`. It is purpose-built for
21
+ > the title / description / toggle / save-bar layout and gets the level backgrounds and
22
+ > level-aware title for free. Use the raw `Card` primitives below only for **non-settings**
23
+ > card surfaces (tiles, generic containers with no title/toggle semantics).
24
+
20
25
  ## 1. Component mapping
21
26
 
22
27
  | @dashboard/library | @aircall/ds / @aircall/blocks |
@@ -35,7 +40,7 @@ This skill builds on aircall-blocks/migrate-dashboard.
35
40
 
36
41
  `PaperForm` is a compound: it wires a `react-final-form` form, renders `Paper`, and
37
42
  appends a `SaveBar` footer. The replacement is `Card` with TanStack Form (`useForm` from
38
- `@aircall/blocks`) and `CardSaveBar` in a `CardFooter`.
43
+ `@aircall/blocks`) and `CardSaveBar` as a direct child of `Card` (it is the footer itself).
39
44
 
40
45
  ## 2. Imports
41
46
 
@@ -176,7 +181,7 @@ function ProfileForm() {
176
181
  **After (`@aircall/blocks`):**
177
182
  ```tsx
178
183
  import { useForm, CardSaveBar } from '@aircall/blocks';
179
- import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@aircall/ds';
184
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@aircall/ds';
180
185
 
181
186
  const form = useForm({
182
187
  defaultValues: { name: '' },
@@ -187,7 +192,7 @@ function ProfileForm() {
187
192
  return (
188
193
  <form.AppForm>
189
194
  <form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }}>
190
- <Card className="overflow-hidden pb-0">
195
+ <Card>
191
196
  <CardHeader>
192
197
  <CardTitle>Profile</CardTitle>
193
198
  <CardDescription>Update your display name.</CardDescription>
@@ -202,9 +207,7 @@ function ProfileForm() {
202
207
  )}
203
208
  </form.AppField>
204
209
  </CardContent>
205
- <CardFooter className="p-0">
206
- <CardSaveBar submitLabel="Save" resetLabel="Discard" />
207
- </CardFooter>
210
+ <CardSaveBar submitLabel="Save" resetLabel="Discard" />
208
211
  </Card>
209
212
  </form>
210
213
  </form.AppForm>
@@ -214,11 +217,33 @@ function ProfileForm() {
214
217
 
215
218
  Key changes:
216
219
  - `PaperForm` `formProps` + `react-final-form` render-prop → `useForm` from `@aircall/blocks` (TanStack Form).
217
- - `PaperForm` `footer` (implicit `SaveBar`) → `CardSaveBar` inside `CardFooter`.
218
- - Add `className="overflow-hidden pb-0"` to `Card` so the animated save bar's border sits flush at the card edge.
219
- - Add `className="p-0"` to `CardFooter` to let `CardSaveBar` own its padding.
220
+ - `PaperForm` `footer` (implicit `SaveBar`) → `CardSaveBar` as a **direct child of `Card`** (do NOT wrap it in a `CardFooter` — it already carries `data-slot="card-footer"` itself).
221
+ - **No `className` needed on `Card`**: it already has `overflow-hidden` built in, and `CardSaveBar`'s `data-slot="card-footer"` makes the card auto-collapse its bottom padding (`has-data-[slot=card-footer]:pb-0`) so the bar sits flush.
220
222
  - `CardSaveBar` reads `isDirty` / `canSubmit` / `isSubmitting` from form context automatically — no extra props needed.
221
223
 
224
+ **Submit error banner (`getErrorMessage` / `submitError`).** `PaperForm` surfaced a submit failure via `getErrorMessage` + `submitError`. There is no built-in equivalent — catch and store it in `onSubmit`, then render an inline `Alert` above `CardContent`:
225
+
226
+ ```tsx
227
+ const [submitError, setSubmitError] = useState<string | null>(null);
228
+
229
+ const form = useForm({
230
+ defaultValues: { name: '' },
231
+ onSubmit: async ({ value }) => {
232
+ try {
233
+ await save(value);
234
+ setSubmitError(null);
235
+ } catch (e) {
236
+ setSubmitError(getErrorMessage(e));
237
+ }
238
+ },
239
+ });
240
+
241
+ // In JSX, above CardContent:
242
+ {submitError && <Alert variant="error">{submitError}</Alert>}
243
+ ```
244
+
245
+ > DS `Alert`'s error variant is `error` (not Tractor's `critical`/`destructive`). Full set: `default` / `info` / `success` / `warning` / `error`.
246
+
222
247
  ### 3d. Paper with a generic footer
223
248
 
224
249
  **Before (`@dashboard/library`):**
@@ -287,78 +312,25 @@ function BillingCard() {
287
312
 
288
313
  Source: `packages/ds/src/components/card.tsx`
289
314
 
290
- ### Mistake 2 — Placing CardAction outside CardHeader
291
-
292
- ```tsx
293
- // ❌ Wrong — renders in normal flow, loses top-right pinning
294
- <Card>
295
- <CardHeader>
296
- <CardTitle>Title</CardTitle>
297
- </CardHeader>
298
- <CardAction>
299
- <Button variant="ghost" size="icon" aria-label="Menu">…</Button>
300
- </CardAction>
301
- <CardContent>Body</CardContent>
302
- </Card>
303
-
304
- // ✅ Correct — CardAction inside CardHeader uses the header grid
305
- <Card>
306
- <CardHeader>
307
- <CardTitle>Title</CardTitle>
308
- <CardAction>
309
- <Button variant="ghost" size="icon" aria-label="Menu">…</Button>
310
- </CardAction>
311
- </CardHeader>
312
- <CardContent>Body</CardContent>
313
- </Card>
314
- ```
315
-
316
- `CardHeader` uses `grid auto-rows-min has-data-[slot=card-action]:grid-cols-[1fr_auto]`. `CardAction` carries `data-slot="card-action"` and is placed at `col-start-2 row-span-2`. Outside `CardHeader`, no grid parent exists and the action renders in document flow.
317
-
318
- Source: `packages/ds/src/components/card.tsx`
319
-
320
- ### Mistake 3 — Omitting overflow-hidden/pb-0 on Card when using CardSaveBar
315
+ ### Mistake 2 — Wrapping CardSaveBar in a CardFooter (or adding overflow-hidden/pb-0)
321
316
 
322
317
  ```tsx
323
- // ❌ Wrong — animated bar peeks out of the card's rounded corners
324
- <Card>
318
+ // ❌ Wrong — CardSaveBar already IS the footer; a CardFooter around it doubles
319
+ // the top border and padding, and the className hints are redundant
320
+ <Card className="overflow-hidden pb-0">
325
321
  <CardContent>…</CardContent>
326
322
  <CardFooter className="p-0">
327
323
  <CardSaveBar />
328
324
  </CardFooter>
329
325
  </Card>
330
326
 
331
- // ✅ Correct — overflow-hidden clips the bar; pb-0 removes the gap below the footer
332
- <Card className="overflow-hidden pb-0">
327
+ // ✅ Correct — drop CardSaveBar in as a direct child; no wrapper, no className
328
+ <Card>
333
329
  <CardContent>…</CardContent>
334
- <CardFooter className="p-0">
335
- <CardSaveBar />
336
- </CardFooter>
330
+ <CardSaveBar />
337
331
  </Card>
338
332
  ```
339
333
 
340
- `CardSaveBar` slides in via a `grid-rows-[0fr→1fr]` transition. Without `overflow-hidden` on `Card`, the collapsed bar overflows the card's rounded corners during animation. Without `pb-0` the card's own `py-4` leaves a gap between the footer and the card bottom edge.
341
-
342
- Source: `packages/blocks/src/components/card-save-bar.tsx`
343
-
344
- ### Mistake 4 — Passing react-final-form formProps to useForm
345
-
346
- ```tsx
347
- // ❌ Wrong — react-final-form API; useForm from @aircall/blocks is TanStack Form
348
- import { useForm } from '@aircall/blocks';
349
- const form = useForm({
350
- initialValues: { name: '' }, // ← react-final-form key
351
- onSubmit: (values) => save(values) // ← sync signature; TanStack expects async
352
- });
353
-
354
- // ✅ Correct — TanStack Form API
355
- import { useForm } from '@aircall/blocks';
356
- const form = useForm({
357
- defaultValues: { name: '' },
358
- onSubmit: async ({ value }) => { await save(value); }
359
- });
360
- ```
361
-
362
- `useForm` from `@aircall/blocks` wraps TanStack Form, not `react-final-form`. The key differences: `defaultValues` (not `initialValues`), and `onSubmit` receives `{ value }` (not the bare values object).
334
+ `CardSaveBar` renders its own `border-t` + padding and carries `data-slot="card-footer"`, so it already behaves as the footer. `Card` has `overflow-hidden` built in (clips the slide-in), and the `data-slot="card-footer"` triggers `has-data-[slot=card-footer]:pb-0`, so the bar sits flush with no manual `overflow-hidden`, `pb-0`, or `CardFooter` wrapper.
363
335
 
364
336
  Source: `packages/blocks/src/components/card-save-bar.tsx`
@@ -0,0 +1,109 @@
1
+ ---
2
+ name: aircall-blocks/migrate-dashboard/catch-up-checklist
3
+ description: >
4
+ Verify that already-migrated dashboard / extension code uses the CURRENT shared
5
+ @aircall/ds + @aircall/blocks components and props — not an earlier or interim
6
+ pattern. Load when someone wants to double-check a codebase that is already off
7
+ @dashboard/library against the latest components (e.g. after a DS/blocks release),
8
+ or to confirm shared-component adoption. This is a VERIFICATION pass, NOT a
9
+ migration — to convert @dashboard/library imports, use aircall-blocks/migrate-dashboard.
10
+ type: sub-skill
11
+ library: aircall-blocks
12
+ requires:
13
+ - aircall-blocks/migrate-dashboard
14
+ ---
15
+
16
+ # Catch-up checklist — are you using the shared component, the latest way?
17
+
18
+ This is **not** a migration skill. It is for code that is **already** on `@aircall/ds` /
19
+ `@aircall/blocks` (the extension especially). Newer releases keep adding components and
20
+ props that supersede patterns written earlier — this checklist confirms each area uses the
21
+ current shared component the intended way, and points you to the skill that does the swap.
22
+
23
+ ## How to run
24
+
25
+ For each row:
26
+
27
+ 1. **`grep`** the hint to find candidates in the code.
28
+ 2. **Check** whether the current pattern is missing or on an older shape (the "Red flag" column).
29
+ 3. **Apply** the shared component/prop; open the linked skill for the detailed swap.
30
+ 4. **Verify green** — `tsc --noEmit`, tests, lint.
31
+
32
+ Skip a row cleanly if the area isn't present in the codebase — this is a spot-check list, not a required sweep.
33
+
34
+ ## Keeping this list evolutive (maintenance rules)
35
+
36
+ - **Add a row** whenever a release supersedes an existing pattern — a new component *or* a new prop/capability on one you already use.
37
+ - **`Added`** is write-once: the month a row was introduced. Never bump it — it lets someone who ran the checklist last quarter jump to what's new. (This is not a version stamp on the skill; rows are keyed by pattern, not version.)
38
+ - **Retire a row** once its old pattern is effectively gone from consumers — prune, don't accumulate.
39
+ - Keep every row **one line**. Deep detail lives in the linked skill, not here. When a group gets long, split it.
40
+
41
+ ## Layout & navigation
42
+
43
+ | Use (current) | Red flag to look for | Detail / skill | Added |
44
+ | --- | --- | --- | --- |
45
+ | `DashboardPageHeader` | a hand-rolled header `<div>` with title + actions, or the legacy `PageHeader` from `@dashboard/library` | `@aircall/blocks#aircall-blocks/migrate-dashboard/page-header` | 2026-07 |
46
+ | `StandalonePage` | a full-screen page not wrapped in it | `@aircall/blocks#aircall-blocks/migrate-dashboard/dashboard-page` | 2026-07 |
47
+ | `DashboardPageContent` at extension page level | an extension page that skips it, re-renders its own outer `DashboardPage`/sidebar shell, or still passes `className="h-full"` (now baked into the component) | in an extension the `DashboardPage` shell is provided by the Sandbox and (in integrate mode) by dashboard-v4, so each page returns just `<DashboardPageContent data-test="…">` + `DashboardPageHeader` + `DashboardPageMain`; `@aircall/blocks#aircall-blocks/migrate-dashboard/dashboard-page` | 2026-07 |
48
+ | `Tabs` (`@aircall/ds`) | `@aircall/tractor` Tab imports or a custom tab bar (low priority — mostly done) | `@aircall/ds#aircall-ds/migrate-tractor/tabs` | 2026-07 |
49
+
50
+ ## Data display
51
+
52
+ | Use (current) | Red flag to look for | Detail / skill | Added |
53
+ | --- | --- | --- | --- |
54
+ | `Card` (`@aircall/ds`) | a custom card `<div>` with border/shadow/padding on grid pages | `@aircall/blocks#aircall-blocks/migrate-dashboard/card` | 2026-07 |
55
+ | `SettingCard` (`@aircall/blocks`) | `Paper` / `PaperForm` from `@dashboard/library`, or a settings section already migrated to raw `Card` with a hand-rolled title/description/toggle or a hand-placed save bar | use `SettingCard` (+ parts): auto level backgrounds + level-aware title, nesting auto-detected, `form.CardSaveBar` as a direct child; `@aircall/blocks#aircall-blocks/migrate-dashboard/setting-card` | 2026-07 |
56
+ | `DataTable` infinite scroll | a "Load more" button, or `DataTable` without `virtualizeRows`+`fillHeight` on a paginated table | pass `virtualizeRows fillHeight` + `onFetchMore`; `@aircall/blocks#aircall-blocks/migrate-dashboard/data-table` | 2026-07 |
57
+ | `DataTable` selection → `ActionBar` | old `enableRowSelection`/`rowSelection`, or a custom bulk-action bar | use `selectionMode`/`selectedKeys` (drives `ActionBar`); `@aircall/blocks#aircall-blocks/migrate-dashboard/data-table` | 2026-07 |
58
+ | `HoverCard` | a custom info-on-hover popup (old `InfoPopup`) | `@aircall/blocks#aircall-blocks/migrate-dashboard/info-popup` | 2026-07 |
59
+ | Re-evaluate `List`/`ListItem` | an `ItemGroup`/`Item` list that is really editable data or tabular data | **check with a designer**: could be `InlineEditMultiselect` or `Table`/`DataTable` instead — `@aircall/blocks#aircall-blocks/migrate-dashboard/data-table` | 2026-07 |
60
+
61
+ ## Forms
62
+
63
+ | Use (current) | Red flag to look for | Detail / skill | Added |
64
+ | --- | --- | --- | --- |
65
+ | `Form*Field` family | `useState`-per-field, or bare ds `Field`/`Input` wired by hand | input/select/combobox/multi-combobox/textarea/switch/radio/slider/toggle-group/OTP/numeric; `@aircall/blocks#aircall-blocks/migrate-dashboard/form-wizard` | 2026-07 |
66
+ | `FormArrayField` | manual add/remove-row state for dynamic lists | `@aircall/blocks#aircall-blocks/migrate-dashboard/form-wizard` | 2026-07 |
67
+ | `SubmitButton` | manual `disabled`/`loading` wiring on a submit button | reads `canSubmit`/`isSubmitting` automatically; `@aircall/blocks#aircall-blocks/migrate-dashboard/form-wizard` | 2026-07 |
68
+ | Field label/description affordances | labels without the newer affordances | `orientation`/`controlPosition` inline layouts, `FieldLabelInfo` (ⓘ), `FieldLabelAside`, contextual vs instructional descriptions; `@aircall/ds#aircall-ds/migrate-tractor/form-and-field` | 2026-07 |
69
+ | `DataCombobox` | a hand-rolled async/paginated combobox, or a custom table filter | `@aircall/ds#aircall-ds/migrate-tractor/combobox` (§5c) | 2026-07 |
70
+
71
+ ## Inputs
72
+
73
+ | Use (current) | Red flag to look for | Detail / skill | Added |
74
+ | --- | --- | --- | --- |
75
+ | `NumericInput` | `<Input type="number">`, a custom stepper, or the legacy `NumberInput` from `@dashboard/library`, for numeric fields | prefix/suffix + up/down controls; via `FormNumericField` in forms | 2026-07 |
76
+
77
+ ## File upload
78
+
79
+ | Use (current) | Red flag to look for | Detail / skill | Added |
80
+ | --- | --- | --- | --- |
81
+ | `Dropzone` | `react-dropzone` / a custom drag-and-drop wrapper/ Dropzone from dashboard/library | `@aircall/ds#aircall-ds/migrate-tractor/dropzone` | 2026-07 |
82
+ | `Attachment` | a custom file-row/list after uploading a file via dropzone | `AttachmentGroup` + `Attachment`; `@aircall/ds#aircall-ds/adopt-attachment` | 2026-07 |
83
+
84
+ ## Links & navigation
85
+
86
+ | Use (current) | Red flag to look for | Detail / skill | Added |
87
+ | --- | --- | --- | --- |
88
+ | `Anchor` or `Button variant="ghost"` | `Button variant="link"` | `Anchor` if it navigates to a page; `Button variant="ghost"` (or another variant) if it triggers an action — `@aircall/ds#aircall-ds/migrate-tractor/link` | 2026-07 |
89
+
90
+ ## Audio player
91
+
92
+ | Use (current) | Red flag to look for | Detail / skill | Added |
93
+ | --- | --- | --- | --- |
94
+ | Audio primitives (`@aircall/ds`) | an old Tractor or custom audio player | refer to `@aircall/ds#aircall-ds/migrate-audio-player` and pick the pattern that best fits the current implementation; a skip button is available, and multiple players wrap in `AudioPlayerManager` | 2026-07 |
95
+
96
+ ## Editable
97
+
98
+ | Use (current) | Red flag to look for | Detail / skill | Added |
99
+ | --- | --- | --- | --- |
100
+ | `InlineEdit` family | `EditableInput` / `EditableTextarea` / `EditableSelect` from `@dashboard/library`, or a custom click-to-edit control | `InlineEditInput`/`InlineEditTextarea`/`InlineEditSelect`, multi via `InlineEditMultiselect` (data-driven accessor API like `DataCombobox`) | 2026-07 |
101
+
102
+ ## Business tags
103
+
104
+ | Use (current) | Red flag to look for | Detail / skill | Added |
105
+ | --- | --- | --- | --- |
106
+ | `SentimentBadge` | a custom mood/sentiment pill | `@aircall/blocks` leaf component | 2026-07 |
107
+ | `ScoreBadge` | a custom score/percentage pill | `@aircall/blocks` leaf component | 2026-07 |
108
+ | `RoleBadge` | a custom role tag | `<RoleBadge role="admin" />` (`@aircall/blocks`) | 2026-07 |
109
+ | `Badge` / `BadgeGroup` | custom tag chips or a stacked tag row | `Badge` with `legacyColor` (stored hex) or `color`+`tone`; `BadgeGroup` for a stack — `@aircall/ds#aircall-ds/migrate-tractor/badge` | 2026-07 |
@@ -7,7 +7,6 @@ description: >
7
7
  it. Load when a file imports any *SearchSelect / MultiSelect from @dashboard/library.
8
8
  type: sub-skill
9
9
  library: aircall-blocks
10
- library_version: "0.5.1"
11
10
  requires:
12
11
  - aircall-blocks/setup
13
12
  - aircall-blocks/migrate-dashboard
@@ -6,7 +6,6 @@ description: >
6
6
  CopyToClipboardButton or CopyToClipboardText from @dashboard/library.
7
7
  type: sub-skill
8
8
  library: aircall-blocks
9
- library_version: "0.6.0"
10
9
  requires:
11
10
  - aircall-blocks/setup
12
11
  - aircall-blocks/migrate-dashboard
@@ -7,7 +7,6 @@ description: >
7
7
  outer layout, a full-page wizard/creation flow, or a page header+content scaffold.
8
8
  type: sub-skill
9
9
  library: aircall-blocks
10
- library_version: "0.5.1"
11
10
  requires:
12
11
  - aircall-blocks/setup
13
12
  - aircall-blocks/migrate-dashboard
@@ -137,6 +136,45 @@ import {
137
136
 
138
137
  `DashboardPageBanner` is available for a page-level banner. The page header itself migrates per `@aircall/blocks#aircall-blocks/migrate-dashboard/page-header`.
139
138
 
139
+ ### Extension pages — start at `DashboardPageContent`, not `DashboardPage`
140
+
141
+ The block above is the **full** shell, which is what the host renders. Inside an
142
+ **extension** you do **not** render `DashboardPage` / `DashboardSidebarProvider` /
143
+ `DashboardSidebar` yourself — that outer shell is provided by the Sandbox during local
144
+ dev (`@dashboard/extension-script`) and by dashboard-v4 in integrate mode. Each extension
145
+ page starts at `DashboardPageContent` and returns just the content card: header +
146
+ `DashboardPageMain`.
147
+
148
+ Do **not** pass `className="h-full"` — the height is now baked into `DashboardPageContent`
149
+ at the component level. Just pass your `data-test`.
150
+
151
+ ```tsx
152
+ // conversation-center-ext/src/pages/contact_insights/ContactInsights.page.tsx
153
+ import {
154
+ DashboardPageContent,
155
+ DashboardPageHeader,
156
+ DashboardPageHeaderTitle,
157
+ DashboardPageMain
158
+ } from '@aircall/blocks';
159
+
160
+ export function ContactInsightsPage() {
161
+ const { t } = useTranslation();
162
+
163
+ return (
164
+ <DashboardPageContent data-test="contact-insights-page">
165
+ <DashboardPageHeader>
166
+ <DashboardPageHeaderTitle size="lg">
167
+ {t('ai_assist.live_assist.pre_call_insights.title')}
168
+ </DashboardPageHeaderTitle>
169
+ </DashboardPageHeader>
170
+ <DashboardPageMain className="flex flex-col gap-4 overflow-scroll">
171
+ <ContactInsights hasTitle={false} />
172
+ </DashboardPageMain>
173
+ </DashboardPageContent>
174
+ );
175
+ }
176
+ ```
177
+
140
178
  ### Page-level notices (beta/feature/trial banners) → `NotificationQueue`
141
179
 
142
180
  For app/page-level notices (e.g. a "beta feature" banner), prefer the ds **`NotificationQueue`** over an inline `Banner` — it stacks, dedupes, and orders by priority, and renders wherever you place a `NotificationSlot` (e.g. at the very top of the page, above the header). Mechanism (from `packages/ds/src/components/notification-queue.tsx`):
@@ -6,7 +6,6 @@ description: >
6
6
  Load when a file imports LoadMoreTable or LoadMoreStrategy from @dashboard/library.
7
7
  type: sub-skill
8
8
  library: aircall-blocks
9
- library_version: "0.5.1"
10
9
  requires:
11
10
  - aircall-blocks/setup
12
11
  - aircall-blocks/migrate-dashboard
@@ -166,6 +165,16 @@ Key changes:
166
165
  - `h="420px"` (Tractor Flex prop) → wrap in a flex container with a bounded height + pass `fillHeight`
167
166
  - `noDataMessage` string → `emptyState` ReactNode
168
167
 
168
+ > **Row virtualization (large accumulated lists).** For an infinite-scroll table that
169
+ > piles up many pages, also pass `virtualizeRows` so only visible rows are in the DOM
170
+ > (requires `fillHeight`). **Tradeoff:** `virtualizeRows` implies `fixedLayout`
171
+ > (`table-layout: fixed`) — every column then takes its `size`, and columns with no
172
+ > `size` fall back to TanStack's **150px** default. A table that relied on content-based
173
+ > auto widths (e.g. a narrow icon column) will visibly snap to 150px columns. Add an
174
+ > explicit `size` to each `ColumnDef` to keep the intended widths, and check them in the
175
+ > browser (`tsc`/tests won't catch a width regression). See
176
+ > `@aircall/ds#aircall-ds/migrate-tractor/data-table` §8 for details.
177
+
169
178
  ### 4b. Button strategy
170
179
 
171
180
  `LoadMoreStrategy.Button` has no direct prop equivalent in `DataTable`. Render the
@@ -7,7 +7,6 @@ description: >
7
7
  Load when a file imports any of those from @dashboard/library.
8
8
  type: sub-skill
9
9
  library: aircall-blocks
10
- library_version: "0.5.1"
11
10
  requires:
12
11
  - aircall-blocks/setup
13
12
  - aircall-blocks/migrate-dashboard
@@ -7,7 +7,6 @@ description: >
7
7
  FormWizard, useFormWizard, or FormField from @dashboard/library.
8
8
  type: sub-skill
9
9
  library: aircall-blocks
10
- library_version: "0.5.1"
11
10
  requires:
12
11
  - aircall-blocks/setup
13
12
  - aircall-blocks/migrate-dashboard
@@ -247,6 +246,46 @@ spread the control bundle onto the matching DS primitive.
247
246
  | combobox (single) | `FormComboboxField` | switch | `FormSwitchField` |
248
247
  | combobox (multi) | `FormMultiComboboxField` | number | `FormNumericField` |
249
248
  | textarea | `FormTextareaField` | slider | `FormSliderField` |
249
+ | toggle group | `FormToggleGroupField` | | |
250
+
251
+ ### Building a wrapper for an unlisted primitive (`FormFieldBase`)
252
+
253
+ For a DS primitive without a shipped `Form*Field`, build one on `FormFieldBase` — it owns the field binding + the shared `Field`/`Label`/`Description`/`Error` shell; you supply a `buildControl` that maps the typed field to the control bundle. This is the shape every shipped wrapper follows (here re-creating `FormSwitchField`, which already ships — copy the pattern for a genuinely new primitive):
254
+
255
+ ```tsx
256
+ import { FormFieldBase, type BoundForm, type FormFieldValidators, type TypedField } from '@aircall/blocks';
257
+ import { Switch } from '@aircall/ds';
258
+ import type { DeepKeysOfType } from '@tanstack/react-form';
259
+
260
+ type SwitchControl = {
261
+ switchProps: { checked: boolean; onCheckedChange: (checked: boolean) => void; onBlur: () => void };
262
+ };
263
+
264
+ function switchControl(field: TypedField<boolean>): SwitchControl {
265
+ return {
266
+ switchProps: {
267
+ checked: field.state.value,
268
+ onCheckedChange: checked => field.handleChange(checked),
269
+ onBlur: field.handleBlur,
270
+ },
271
+ };
272
+ }
273
+
274
+ function FormSwitchField<
275
+ TForm extends BoundForm,
276
+ TName extends DeepKeysOfType<TForm['state']['values'], boolean> // only boolean fields
277
+ >(props: {
278
+ form: TForm;
279
+ name: TName;
280
+ label: string;
281
+ validators?: FormFieldValidators<TForm['state']['values'], TName>;
282
+ children: (field: TypedField<boolean>, control: SwitchControl) => React.ReactNode;
283
+ }) {
284
+ return <FormFieldBase<boolean, SwitchControl> {...props} buildControl={switchControl} />;
285
+ }
286
+ ```
287
+
288
+ `DeepKeysOfType<…, boolean>` restricts `name` to fields whose value type matches the control (here `boolean`), so the form stays type-safe.
250
289
 
251
290
  ## Multi-step gotchas
252
291
 
@@ -6,7 +6,6 @@ description: >
6
6
  or InfoPopupContent from @dashboard/library or from the local shared info-popup.
7
7
  type: sub-skill
8
8
  library: aircall-blocks
9
- library_version: "0.6.0"
10
9
  requires:
11
10
  - aircall-blocks/setup
12
11
  - aircall-blocks/migrate-dashboard
@@ -6,7 +6,6 @@ description: >
6
6
  Load when a file imports GridLayout, GridItem, or Gap from @dashboard/library.
7
7
  type: sub-skill
8
8
  library: aircall-blocks
9
- library_version: "0.5.1"
10
9
  requires:
11
10
  - aircall-blocks/setup
12
11
  - aircall-blocks/migrate-dashboard
@@ -7,7 +7,6 @@ description: >
7
7
  or ListItemActions from @dashboard/library.
8
8
  type: sub-skill
9
9
  library: aircall-blocks
10
- library_version: "0.5.1"
11
10
  requires:
12
11
  - aircall-blocks/setup
13
12
  - aircall-blocks/migrate-dashboard
@@ -6,7 +6,6 @@ description: >
6
6
  file imports Loading, Loader, or Spinner from @dashboard/library.
7
7
  type: sub-skill
8
8
  library: aircall-blocks
9
- library_version: "0.5.1"
10
9
  requires:
11
10
  - aircall-blocks/setup
12
11
  - aircall-blocks/migrate-dashboard
@@ -9,7 +9,6 @@ description: >
9
9
  imports PageHeader or PageHeaderLink from @dashboard/library.
10
10
  type: sub-skill
11
11
  library: aircall-blocks
12
- library_version: "0.5.1"
13
12
  requires:
14
13
  - aircall-blocks/setup
15
14
  - aircall-blocks/migrate-dashboard