@aircall/ds 0.14.0 → 0.15.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.
- package/README.md +31 -0
- package/dist/globals.css +1 -1
- package/dist/index.d.ts +28 -28
- package/dist/index.js +1 -1
- package/package.json +12 -2
- package/skills/aircall-ds/migrate-icons/SKILL.md +346 -0
- package/skills/aircall-ds/migrate-tractor/SKILL.md +314 -0
- package/skills/aircall-ds/migrate-tractor/accordion/SKILL.md +276 -0
- package/skills/aircall-ds/migrate-tractor/alert/SKILL.md +225 -0
- package/skills/aircall-ds/migrate-tractor/avatar/SKILL.md +272 -0
- package/skills/aircall-ds/migrate-tractor/badge/SKILL.md +274 -0
- package/skills/aircall-ds/migrate-tractor/button/SKILL.md +277 -0
- package/skills/aircall-ds/migrate-tractor/card/SKILL.md +278 -0
- package/skills/aircall-ds/migrate-tractor/combobox/SKILL.md +346 -0
- package/skills/aircall-ds/migrate-tractor/data-table/SKILL.md +333 -0
- package/skills/aircall-ds/migrate-tractor/dialog/SKILL.md +206 -0
- package/skills/aircall-ds/migrate-tractor/divider/SKILL.md +226 -0
- package/skills/aircall-ds/migrate-tractor/dropdown-menu/SKILL.md +266 -0
- package/skills/aircall-ds/migrate-tractor/dropzone/SKILL.md +338 -0
- package/skills/aircall-ds/migrate-tractor/form-and-field/SKILL.md +325 -0
- package/skills/aircall-ds/migrate-tractor/gauge/SKILL.md +248 -0
- package/skills/aircall-ds/migrate-tractor/input/SKILL.md +261 -0
- package/skills/aircall-ds/migrate-tractor/item/SKILL.md +298 -0
- package/skills/aircall-ds/migrate-tractor/link/SKILL.md +263 -0
- package/skills/aircall-ds/migrate-tractor/popover/SKILL.md +214 -0
- package/skills/aircall-ds/migrate-tractor/select/SKILL.md +245 -0
- package/skills/aircall-ds/migrate-tractor/sheet-vs-drawer/SKILL.md +272 -0
- package/skills/aircall-ds/migrate-tractor/skeleton/SKILL.md +190 -0
- package/skills/aircall-ds/migrate-tractor/styling/SKILL.md +421 -0
- package/skills/aircall-ds/migrate-tractor/tabs/SKILL.md +250 -0
- package/skills/aircall-ds/migrate-tractor/toast/SKILL.md +322 -0
- package/skills/aircall-ds/migrate-tractor/tooltip/SKILL.md +204 -0
- package/skills/aircall-ds/migrate-tractor/tree/SKILL.md +346 -0
- package/skills/aircall-ds/setup/SKILL.md +347 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aircall-ds/migrate-tractor/data-table
|
|
3
|
+
description: >
|
|
4
|
+
Migrate Tractor Table and TableColumn to the @aircall/ds DataTable (TanStack
|
|
5
|
+
Table-backed). Load when a file imports Table or TableColumn from @aircall/tractor.
|
|
6
|
+
Covers column definitions, sorting, row selection, loading states, and empty states.
|
|
7
|
+
type: sub-skill
|
|
8
|
+
library: aircall-ds
|
|
9
|
+
library_version: "0.13.0"
|
|
10
|
+
requires:
|
|
11
|
+
- aircall-ds/setup
|
|
12
|
+
- aircall-ds/migrate-tractor
|
|
13
|
+
sources:
|
|
14
|
+
- "aircall/hydra:packages/ds/src/components/data-table.tsx"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
This skill builds on aircall-ds/migrate-tractor. Apply all cross-cutting rules from that skill (prop renames, `render` prop, data attributes) before the data-table-specific steps below.
|
|
18
|
+
|
|
19
|
+
## 1. Architecture change
|
|
20
|
+
|
|
21
|
+
Tractor `Table` was a self-contained component: you passed `data`, `columns` (a `TableColumn[]` descriptor array), and optional callbacks — Tractor handled layout, sorting state, and selection state internally.
|
|
22
|
+
|
|
23
|
+
DS `DataTable` is built on **TanStack Table v8**. Columns are defined as `ColumnDef<TData>[]` (from `@tanstack/react-table`), sorting and selection state are controlled externally by the caller, and loading is expressed as a single `loadingState` string rather than a boolean `loading` flag.
|
|
24
|
+
|
|
25
|
+
There is no sub-component API (no `<TableColumn>`) — everything is declared in `columns`.
|
|
26
|
+
|
|
27
|
+
## 2. Component mapping
|
|
28
|
+
|
|
29
|
+
| Tractor | @aircall/ds |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| `<Table data columns loading ...>` | `<DataTable data columns loadingState ...>` |
|
|
32
|
+
| `TableColumn<T>` (object descriptor) | `ColumnDef<TData, unknown>` from `@tanstack/react-table` |
|
|
33
|
+
| `columns[n].id` | `ColumnDef.id` (or accessor key) |
|
|
34
|
+
| `columns[n].label` | `ColumnDef.header` |
|
|
35
|
+
| `columns[n].renderer` | `ColumnDef.cell` (receives `{ row }`) |
|
|
36
|
+
| `columns[n].sortable` | `ColumnDef.enableSorting` |
|
|
37
|
+
| `loading={true}` | `loadingState="loading"` |
|
|
38
|
+
| `noDataMessage` | `emptyState` (accepts any `ReactNode`) |
|
|
39
|
+
| `onRowClick(record, event)` | `onRowClick(row)` (no `event` argument) |
|
|
40
|
+
| `bulkActions` + `verticalScrollingParent` | `enableRowSelection` + `actionBar` |
|
|
41
|
+
| `onOrderChange(columnId)` | `onSortingChange` (receives `SortingState`) + `sorting` |
|
|
42
|
+
| `order` (`OrderInput`) | `sorting` (`SortingState`) |
|
|
43
|
+
| `footer` | column-level `ColumnDef.footer` (auto-renders `<tfoot>`) |
|
|
44
|
+
| _(no equivalent)_ | `loadingState="sorting"` / `"filtering"` (overlay spinner) |
|
|
45
|
+
| _(no equivalent)_ | `loadingState="loadingMore"` (infinite scroll spinner row) |
|
|
46
|
+
| _(no equivalent)_ | `onFetchMore` + `hasMore` (IntersectionObserver sentinel) |
|
|
47
|
+
| _(no equivalent)_ | `fillHeight` (sticky header, internal scroll) |
|
|
48
|
+
|
|
49
|
+
## 3. Verified DS exports (`packages/ds/src/index.ts`)
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
DataTable, type DataTableLoadingState
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`@aircall/ds` **re-exports** the TanStack Table types you author — `ColumnDef`, `SortingState`, `RowSelectionState`, `OnChangeFn` — so **import them from `@aircall/ds`**, not `@tanstack/react-table`. The react-table runtime ships as a regular `@aircall/ds` dependency (auto-installed; `DataTable` owns the table instance internally), so there is nothing to install and no version to keep in sync.
|
|
56
|
+
|
|
57
|
+
## 4. Imports
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { DataTable, type DataTableLoadingState } from '@aircall/ds';
|
|
61
|
+
import type { ColumnDef, SortingState, RowSelectionState, OnChangeFn } from '@aircall/ds';
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 5. Column definitions
|
|
65
|
+
|
|
66
|
+
Tractor used a descriptor array (`TableColumn[]`) with a `renderer` function. DS uses `ColumnDef<TData>[]` from TanStack Table. The key changes:
|
|
67
|
+
|
|
68
|
+
| Tractor `TableColumn` field | TanStack `ColumnDef` equivalent |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `id` | `accessorKey` (auto-creates accessor) or `id` + `accessorFn` |
|
|
71
|
+
| `label` | `header` (`ReactNode` or render function) |
|
|
72
|
+
| `renderer(rowData, value, rowIndex)` | `cell: ({ row }) => row.original.<field>` |
|
|
73
|
+
| `sortable: false` | `enableSorting: false` |
|
|
74
|
+
| _(no equivalent)_ | `footer` (triggers `<tfoot>` rendering) |
|
|
75
|
+
| _(no equivalent)_ | `size`, `minSize`, `maxSize` (for `enableColumnResizing`) |
|
|
76
|
+
|
|
77
|
+
## 6. Before / After
|
|
78
|
+
|
|
79
|
+
### 6a. Basic read-only table
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
// Before
|
|
83
|
+
import { Table, TableColumn } from '@aircall/tractor';
|
|
84
|
+
|
|
85
|
+
type Person = { id: string; firstName: string; lastName: string; email: string };
|
|
86
|
+
|
|
87
|
+
const columns: TableColumn<Person>[] = [
|
|
88
|
+
{ id: 'firstName', label: 'First name' },
|
|
89
|
+
{ id: 'lastName', label: 'Last name' },
|
|
90
|
+
{
|
|
91
|
+
id: 'email',
|
|
92
|
+
label: 'Email',
|
|
93
|
+
renderer: ({ email }) => <a href={`mailto:${email}`}>{email}</a>
|
|
94
|
+
}
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
<Table data={people} columns={columns} loading={isLoading} noDataMessage="No people found." />
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
// After
|
|
102
|
+
import { DataTable } from '@aircall/ds';
|
|
103
|
+
import type { ColumnDef } from '@aircall/ds';
|
|
104
|
+
|
|
105
|
+
type Person = { id: string; firstName: string; lastName: string; email: string };
|
|
106
|
+
|
|
107
|
+
const columns: ColumnDef<Person>[] = [
|
|
108
|
+
{ accessorKey: 'firstName', header: 'First name' },
|
|
109
|
+
{ accessorKey: 'lastName', header: 'Last name' },
|
|
110
|
+
{
|
|
111
|
+
accessorKey: 'email',
|
|
112
|
+
header: 'Email',
|
|
113
|
+
cell: ({ row }) => <a href={`mailto:${row.original.email}`}>{row.original.email}</a>
|
|
114
|
+
}
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
<DataTable
|
|
118
|
+
data={people}
|
|
119
|
+
columns={columns}
|
|
120
|
+
loadingState={isLoading ? 'loading' : 'idle'}
|
|
121
|
+
emptyState={<p className="text-sm text-muted-foreground">No people found.</p>}
|
|
122
|
+
/>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 6b. Server-side sortable table
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
// Before
|
|
129
|
+
import { Table, TableColumn, OrderInput } from '@aircall/tractor';
|
|
130
|
+
import { OrderDirection } from '@aircall/tractor';
|
|
131
|
+
|
|
132
|
+
const [order, setOrder] = React.useState<OrderInput>({ field: 'firstName', direction: OrderDirection.Asc });
|
|
133
|
+
|
|
134
|
+
const columns: TableColumn<Person>[] = [
|
|
135
|
+
{ id: 'firstName', label: 'First name', sortable: true },
|
|
136
|
+
{ id: 'lastName', label: 'Last name', sortable: true },
|
|
137
|
+
{ id: 'email', label: 'Email', sortable: false }
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
<Table
|
|
141
|
+
data={people}
|
|
142
|
+
columns={columns}
|
|
143
|
+
order={order}
|
|
144
|
+
onOrderChange={(columnId) => {
|
|
145
|
+
setOrder(prev => ({
|
|
146
|
+
field: columnId,
|
|
147
|
+
direction: prev.field === columnId && prev.direction === OrderDirection.Asc
|
|
148
|
+
? OrderDirection.Desc
|
|
149
|
+
: OrderDirection.Asc
|
|
150
|
+
}));
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
// After
|
|
157
|
+
import { DataTable } from '@aircall/ds';
|
|
158
|
+
import type { ColumnDef, SortingState, OnChangeFn } from '@aircall/ds';
|
|
159
|
+
|
|
160
|
+
const [sorting, setSorting] = React.useState<SortingState>([{ id: 'firstName', desc: false }]);
|
|
161
|
+
|
|
162
|
+
const columns: ColumnDef<Person>[] = [
|
|
163
|
+
{ accessorKey: 'firstName', header: 'First name', enableSorting: true },
|
|
164
|
+
{ accessorKey: 'lastName', header: 'Last name', enableSorting: true },
|
|
165
|
+
{ accessorKey: 'email', header: 'Email', enableSorting: false }
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
<DataTable
|
|
169
|
+
data={people}
|
|
170
|
+
columns={columns}
|
|
171
|
+
sorting={sorting}
|
|
172
|
+
onSortingChange={setSorting}
|
|
173
|
+
loadingState={isSorting ? 'sorting' : 'idle'}
|
|
174
|
+
/>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Key changes:
|
|
178
|
+
- `order: OrderInput` → `sorting: SortingState` (array of `{ id, desc }` objects)
|
|
179
|
+
- `onOrderChange(columnId)` → `onSortingChange: OnChangeFn<SortingState>` (receives the new full state)
|
|
180
|
+
- A `loadingState="sorting"` dims the existing rows with an overlay spinner while the server re-queries — no skeleton flash
|
|
181
|
+
|
|
182
|
+
### 6c. Selectable table with bulk action bar
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
// Before
|
|
186
|
+
import { Table, TableColumn } from '@aircall/tractor';
|
|
187
|
+
|
|
188
|
+
const parentRef = React.useRef<HTMLDivElement | null>(null);
|
|
189
|
+
|
|
190
|
+
<div ref={parentRef} style={{ height: '500px', overflowY: 'auto' }}>
|
|
191
|
+
<Table
|
|
192
|
+
data={people}
|
|
193
|
+
columns={columns}
|
|
194
|
+
verticalScrollingParent={parentRef}
|
|
195
|
+
bulkActions={[
|
|
196
|
+
{ label: 'Delete', onExecute: (records) => deleteRecords(records) }
|
|
197
|
+
]}
|
|
198
|
+
/>
|
|
199
|
+
</div>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
// After
|
|
204
|
+
import { DataTable } from '@aircall/ds';
|
|
205
|
+
import { ActionBar, ActionBarButton } from '@aircall/ds';
|
|
206
|
+
import type { RowSelectionState, OnChangeFn } from '@aircall/ds';
|
|
207
|
+
|
|
208
|
+
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
|
209
|
+
|
|
210
|
+
<DataTable
|
|
211
|
+
data={people}
|
|
212
|
+
columns={columns}
|
|
213
|
+
getRowId={(row) => row.id}
|
|
214
|
+
enableRowSelection
|
|
215
|
+
rowSelection={rowSelection}
|
|
216
|
+
onRowSelectionChange={setRowSelection}
|
|
217
|
+
actionBar={
|
|
218
|
+
<ActionBar>
|
|
219
|
+
<ActionBarButton variant="destructive" onClick={() => deleteSelected()}>
|
|
220
|
+
Delete
|
|
221
|
+
</ActionBarButton>
|
|
222
|
+
</ActionBar>
|
|
223
|
+
}
|
|
224
|
+
/>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Key changes:
|
|
228
|
+
- `verticalScrollingParent` is not needed — `DataTable` manages its own ActionBar positioning
|
|
229
|
+
- `bulkActions` array → pass an `<ActionBar>` node to the `actionBar` prop
|
|
230
|
+
- `onSelectionChange(selectedRows, count)` is replaced by controlled `rowSelection` state — derive selected records from `people.filter(p => rowSelection[p.id])`
|
|
231
|
+
- `getRowId` is required when selection must survive data refreshes (server pagination)
|
|
232
|
+
|
|
233
|
+
## 7. Common mistakes
|
|
234
|
+
|
|
235
|
+
### Mistake 1 — Using `renderer` from `TableColumn` on a `ColumnDef`
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
// Wrong — renderer is a Tractor TableColumn field; ColumnDef ignores it
|
|
239
|
+
const columns: ColumnDef<Person>[] = [
|
|
240
|
+
{
|
|
241
|
+
accessorKey: 'email',
|
|
242
|
+
header: 'Email',
|
|
243
|
+
renderer: ({ email }) => <a href={`mailto:${email}`}>{email}</a>
|
|
244
|
+
}
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
// Correct — ColumnDef uses cell; the cell function receives the TanStack cell context
|
|
248
|
+
const columns: ColumnDef<Person>[] = [
|
|
249
|
+
{
|
|
250
|
+
accessorKey: 'email',
|
|
251
|
+
header: 'Email',
|
|
252
|
+
cell: ({ row }) => <a href={`mailto:${row.original.email}`}>{row.original.email}</a>
|
|
253
|
+
}
|
|
254
|
+
];
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`ColumnDef` has no `renderer` field — passing it adds an unrecognised property that TypeScript will not catch (the field just becomes dead code). The cell always renders the raw accessor value. Use `cell: ({ row }) => …` and access typed row data via `row.original`.
|
|
258
|
+
|
|
259
|
+
Source: `packages/ds/src/components/data-table.tsx`
|
|
260
|
+
|
|
261
|
+
### Mistake 2 — Passing `loading={true}` instead of `loadingState="loading"`
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
// Wrong — loading is a Tractor prop; DataTable has no loading prop
|
|
265
|
+
<DataTable data={[]} columns={columns} loading={isLoading} />
|
|
266
|
+
|
|
267
|
+
// Correct — use loadingState with one of the four async-state values
|
|
268
|
+
<DataTable
|
|
269
|
+
data={[]}
|
|
270
|
+
columns={columns}
|
|
271
|
+
loadingState={isLoading ? 'loading' : 'idle'}
|
|
272
|
+
/>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
DS replaced the boolean `loading` flag with a discriminated `loadingState` string so that different async phases (`'loading'`, `'sorting'`, `'filtering'`, `'loadingMore'`) can produce different UX (skeleton rows vs. overlay spinner vs. bottom spinner row). Passing `loading` as a prop is silently ignored — the table renders the empty state or no rows instead of skeleton rows.
|
|
276
|
+
|
|
277
|
+
Source: `packages/ds/src/components/data-table.tsx`
|
|
278
|
+
|
|
279
|
+
### Mistake 3 — Deriving selected records from `onSelectionChange` instead of `rowSelection` state
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
// Wrong — onSelectionChange does not exist on DataTable; selection is controlled via state
|
|
283
|
+
<DataTable
|
|
284
|
+
enableRowSelection
|
|
285
|
+
onSelectionChange={(selectedRows) => setSelectedPeople(selectedRows)}
|
|
286
|
+
columns={columns}
|
|
287
|
+
data={people}
|
|
288
|
+
/>
|
|
289
|
+
|
|
290
|
+
// Correct — hold RowSelectionState; derive selected records from data + state when needed
|
|
291
|
+
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
|
292
|
+
const selectedPeople = people.filter(p => rowSelection[p.id]);
|
|
293
|
+
|
|
294
|
+
<DataTable
|
|
295
|
+
enableRowSelection
|
|
296
|
+
rowSelection={rowSelection}
|
|
297
|
+
onRowSelectionChange={setRowSelection}
|
|
298
|
+
getRowId={(row) => row.id}
|
|
299
|
+
columns={columns}
|
|
300
|
+
data={people}
|
|
301
|
+
/>
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Tractor called `onSelectionChange(rows, count)` with the actual row objects. DS selection is fully controlled: `onRowSelectionChange` receives the new `RowSelectionState` (a record of `{ [rowId]: boolean }`). Derive selected rows by filtering your data array against the state object. Always provide `getRowId` so the IDs in `RowSelectionState` match your data's primary key.
|
|
305
|
+
|
|
306
|
+
Source: `packages/ds/src/components/data-table.tsx`
|
|
307
|
+
|
|
308
|
+
### Mistake 4 — Passing `noDataMessage` as a string instead of an `emptyState` node
|
|
309
|
+
|
|
310
|
+
```tsx
|
|
311
|
+
// Wrong — noDataMessage is a Tractor prop; DataTable renders nothing for it
|
|
312
|
+
<DataTable data={[]} columns={columns} noDataMessage="No records found." />
|
|
313
|
+
|
|
314
|
+
// Correct — emptyState accepts any ReactNode; render an Empty component for rich states
|
|
315
|
+
import { Empty, EmptyContent, EmptyTitle, EmptyDescription } from '@aircall/ds';
|
|
316
|
+
|
|
317
|
+
<DataTable
|
|
318
|
+
data={[]}
|
|
319
|
+
columns={columns}
|
|
320
|
+
emptyState={
|
|
321
|
+
<Empty>
|
|
322
|
+
<EmptyContent>
|
|
323
|
+
<EmptyTitle>No records found</EmptyTitle>
|
|
324
|
+
<EmptyDescription>Try adjusting your filters.</EmptyDescription>
|
|
325
|
+
</EmptyContent>
|
|
326
|
+
</Empty>
|
|
327
|
+
}
|
|
328
|
+
/>
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Tractor `noDataMessage` accepted a `ReactNode` but was declared as a simple prop. DS requires an `emptyState` prop; `noDataMessage` is silently ignored. For plain text, wrap it in a `<p>` with `text-sm text-muted-foreground`. For a full empty state with an icon and a call-to-action, use the `Empty` compound from `@aircall/ds`.
|
|
332
|
+
|
|
333
|
+
Source: `packages/ds/src/components/data-table.tsx`
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aircall-ds/migrate-tractor/dialog
|
|
3
|
+
description: >
|
|
4
|
+
Migrate Tractor Modal (ModalDialog, ModalHeader, ModalTitle, ModalBody,
|
|
5
|
+
ModalFooter) to the @aircall/ds Dialog compound. Load when a file imports Modal*
|
|
6
|
+
from @aircall/tractor.
|
|
7
|
+
type: sub-skill
|
|
8
|
+
library: aircall-ds
|
|
9
|
+
library_version: "0.13.0"
|
|
10
|
+
requires:
|
|
11
|
+
- aircall-ds/setup
|
|
12
|
+
- aircall-ds/migrate-tractor
|
|
13
|
+
sources:
|
|
14
|
+
- "aircall/hydra:docs/migration-guides/tractor-to-ds/recipes/dialog.md"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
This skill builds on aircall-ds/migrate-tractor. Apply all cross-cutting rules from that skill (prop renames, `render` prop, data attributes) before the dialog-specific steps below.
|
|
18
|
+
|
|
19
|
+
## 1. Component mapping
|
|
20
|
+
|
|
21
|
+
| Tractor | @aircall/ds |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `Modal` (state owner + panel) | `Dialog` (state owner) + `DialogContent` (panel) |
|
|
24
|
+
| `ModalHeader` | `DialogHeader` |
|
|
25
|
+
| `ModalTitle` | `DialogTitle` |
|
|
26
|
+
| `ModalBody` | — (plain children inside `DialogContent`, no wrapper) |
|
|
27
|
+
| `ModalFooter` | `DialogFooter` |
|
|
28
|
+
| — | `DialogTrigger` (uncontrolled open trigger) |
|
|
29
|
+
| — | `DialogClose` (any button that closes the dialog) |
|
|
30
|
+
| — | `DialogDescription` (optional accessible subtitle) |
|
|
31
|
+
|
|
32
|
+
## 2. Imports
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import {
|
|
36
|
+
Dialog,
|
|
37
|
+
DialogClose,
|
|
38
|
+
DialogContent,
|
|
39
|
+
DialogDescription,
|
|
40
|
+
DialogFooter,
|
|
41
|
+
DialogHeader,
|
|
42
|
+
DialogTitle,
|
|
43
|
+
DialogTrigger,
|
|
44
|
+
Button
|
|
45
|
+
} from '@aircall/ds';
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 3. Before / After
|
|
49
|
+
|
|
50
|
+
### Controlled (most common — replaces `<Modal isOpen={…} onClose={…}>`)
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
// Before
|
|
54
|
+
import { Modal, ModalHeader, ModalTitle, ModalBody, ModalFooter, Button } from '@aircall/tractor';
|
|
55
|
+
|
|
56
|
+
<Modal isOpen={isOpen} onClose={() => setIsOpen(false)} size="regular">
|
|
57
|
+
<ModalHeader>
|
|
58
|
+
<ModalTitle>Confirm delete</ModalTitle>
|
|
59
|
+
</ModalHeader>
|
|
60
|
+
<ModalBody>
|
|
61
|
+
<p>This will permanently remove the contact. Are you sure?</p>
|
|
62
|
+
</ModalBody>
|
|
63
|
+
<ModalFooter>
|
|
64
|
+
<Button variant="outline" onClick={() => setIsOpen(false)}>Cancel</Button>
|
|
65
|
+
<Button variant="critical" onClick={onConfirm}>Delete</Button>
|
|
66
|
+
</ModalFooter>
|
|
67
|
+
</Modal>
|
|
68
|
+
|
|
69
|
+
// After
|
|
70
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose, Button } from '@aircall/ds';
|
|
71
|
+
|
|
72
|
+
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
73
|
+
<DialogContent>
|
|
74
|
+
<DialogHeader>
|
|
75
|
+
<DialogTitle>Confirm delete</DialogTitle>
|
|
76
|
+
<DialogDescription>
|
|
77
|
+
This will permanently remove the contact.
|
|
78
|
+
</DialogDescription>
|
|
79
|
+
</DialogHeader>
|
|
80
|
+
|
|
81
|
+
<p className="text-sm">Are you sure?</p>
|
|
82
|
+
|
|
83
|
+
<DialogFooter>
|
|
84
|
+
<DialogClose render={<Button variant="outline" size="lg" />}>
|
|
85
|
+
Cancel
|
|
86
|
+
</DialogClose>
|
|
87
|
+
<Button variant="destructive" size="lg" onClick={onConfirm}>
|
|
88
|
+
Delete
|
|
89
|
+
</Button>
|
|
90
|
+
</DialogFooter>
|
|
91
|
+
</DialogContent>
|
|
92
|
+
</Dialog>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Key changes:
|
|
96
|
+
- `isOpen` → `open`; `onClose(reason)` → `onOpenChange(boolean)`
|
|
97
|
+
- `ModalBody` is removed — body content is plain children inside `DialogContent`
|
|
98
|
+
- `DialogClose` wraps any button that should close the dialog; pass the DS `Button` via `render` and put the label as children of `DialogClose`, not of the inner Button
|
|
99
|
+
- `variant="critical"` → `variant="destructive"` (Button)
|
|
100
|
+
|
|
101
|
+
### Uncontrolled with a trigger
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
// After
|
|
105
|
+
<Dialog>
|
|
106
|
+
<DialogTrigger render={<Button size="lg" />}>Open settings</DialogTrigger>
|
|
107
|
+
<DialogContent>
|
|
108
|
+
<DialogHeader>
|
|
109
|
+
<DialogTitle>Settings</DialogTitle>
|
|
110
|
+
</DialogHeader>
|
|
111
|
+
{/* body content */}
|
|
112
|
+
<DialogFooter>
|
|
113
|
+
<DialogClose render={<Button variant="outline" size="lg" />}>Close</DialogClose>
|
|
114
|
+
</DialogFooter>
|
|
115
|
+
</DialogContent>
|
|
116
|
+
</Dialog>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Size
|
|
120
|
+
|
|
121
|
+
`Dialog` has no built-in `size` prop. For the Tractor `size="large"` case, set width on `DialogContent`:
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
// Tractor: size="large"
|
|
125
|
+
<DialogContent className="max-w-2xl">…</DialogContent>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Hiding the default close button
|
|
129
|
+
|
|
130
|
+
`DialogContent` renders a default "×" close button in the top-right. Hide it when you want to force the user to use a footer action:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
<DialogContent showCloseButton={false}>…</DialogContent>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 4. Common mistakes
|
|
139
|
+
|
|
140
|
+
### Mistake 1 — Passing `onClose` instead of `onOpenChange`
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
// ❌ Wrong — onClose is a Tractor prop; DS ignores it silently
|
|
144
|
+
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>…</Dialog>
|
|
145
|
+
|
|
146
|
+
// ✅ Correct — onOpenChange receives the new boolean state
|
|
147
|
+
<Dialog open={isOpen} onOpenChange={setIsOpen}>…</Dialog>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`onOpenChange` fires with `false` when the dialog closes (backdrop click, Escape, or `DialogClose`). Widen your handler if you were using the Tractor `reason` argument — DS does not pass a reason.
|
|
151
|
+
|
|
152
|
+
Source: `packages/ds/src/components/dialog.tsx`
|
|
153
|
+
|
|
154
|
+
### Mistake 2 — Keeping `ModalBody` or wrapping body content in `DialogBody`
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
// ❌ Wrong — neither ModalBody nor DialogBody exists in @aircall/ds
|
|
158
|
+
<DialogContent>
|
|
159
|
+
<DialogHeader>…</DialogHeader>
|
|
160
|
+
<DialogBody>
|
|
161
|
+
<p>Content here</p>
|
|
162
|
+
</DialogBody>
|
|
163
|
+
<DialogFooter>…</DialogFooter>
|
|
164
|
+
</DialogContent>
|
|
165
|
+
|
|
166
|
+
// ✅ Correct — body content is plain children between DialogHeader and DialogFooter
|
|
167
|
+
<DialogContent>
|
|
168
|
+
<DialogHeader>…</DialogHeader>
|
|
169
|
+
<p className="text-sm">Content here</p>
|
|
170
|
+
<DialogFooter>…</DialogFooter>
|
|
171
|
+
</DialogContent>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
DS's `Dialog` is a compound with no `DialogBody` export. The body is just children of `DialogContent`. Remove `ModalBody` and render content directly.
|
|
175
|
+
|
|
176
|
+
Source: `packages/ds/src/components/dialog.tsx`
|
|
177
|
+
|
|
178
|
+
### Mistake 3 — Putting the label inside the `render` Button instead of `DialogClose`
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
// ❌ Wrong — the label ends up inside the Button's children, not DialogClose's
|
|
182
|
+
<DialogClose render={<Button variant="outline" size="lg">Cancel</Button>} />
|
|
183
|
+
|
|
184
|
+
// ✅ Correct — label is a child of DialogClose; DialogClose clones it into the rendered Button
|
|
185
|
+
<DialogClose render={<Button variant="outline" size="lg" />}>
|
|
186
|
+
Cancel
|
|
187
|
+
</DialogClose>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`DialogClose` uses the Base UI `render` prop pattern: it renders the element you pass as `render`, merging its own click handler. The label must be the child of `DialogClose`, not of the inner Button.
|
|
191
|
+
|
|
192
|
+
Source: `packages/ds/src/components/dialog.tsx`
|
|
193
|
+
|
|
194
|
+
### Mistake 4 — Using `variant="critical"` on the Button
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
// ❌ Wrong — critical is a Tractor variant; DS Button doesn't have it
|
|
198
|
+
<Button variant="critical" onClick={onConfirm}>Delete</Button>
|
|
199
|
+
|
|
200
|
+
// ✅ Correct — DS Button uses destructive
|
|
201
|
+
<Button variant="destructive" size="lg" onClick={onConfirm}>Delete</Button>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
DS renamed `critical` to `destructive` for Button (and other components). The Tractor prop is silently dropped; no TypeScript error is shown because the variant may fall through to the default style.
|
|
205
|
+
|
|
206
|
+
Source: `packages/ds/src/components/dialog.tsx`
|