@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,346 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aircall-ds/migrate-tractor/tree
|
|
3
|
+
description: >
|
|
4
|
+
Migrate Tractor Tree and TreeSelect to the @aircall/ds DataTree (data-driven)
|
|
5
|
+
or Tree/TreeItem/TreeItemLabel primitives (full control). Load when a file
|
|
6
|
+
imports Tree or TreeSelect 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/tree.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 tree-specific steps below.
|
|
18
|
+
|
|
19
|
+
## 1. Component mapping
|
|
20
|
+
|
|
21
|
+
| Tractor component | DS replacement | When to use |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `<Tree nodes={[…]} />` | `<DataTree items={…} getKey={…} getLabel={…} />` | Default — keep your data shape, point accessors at it |
|
|
24
|
+
| `<Tree nodes={[…]} />` | `<Tree>` + `<TreeItem>` + `<TreeItemLabel>` + `useTree` | Custom rows, drag-and-drop, search highlight, inline rename |
|
|
25
|
+
| `<TreeSelect />` | `<DataTree>` inside a `<Popover>` | No turn-key equivalent — compose manually |
|
|
26
|
+
|
|
27
|
+
## 2. Verified DS exports (`packages/ds/src/index.ts`)
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
DataTree
|
|
31
|
+
Tree, TreeItem, TreeItemLabel, TreeDragLine
|
|
32
|
+
Popover, PopoverContent, PopoverTrigger
|
|
33
|
+
Button
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 3. Imports
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
// Data-driven usage (most cases)
|
|
40
|
+
import { DataTree } from '@aircall/ds';
|
|
41
|
+
|
|
42
|
+
// Primitive usage (full control)
|
|
43
|
+
import { Tree, TreeItem, TreeItemLabel } from '@aircall/ds';
|
|
44
|
+
|
|
45
|
+
// Icons — always from @aircall/react-icons, never lucide-react directly
|
|
46
|
+
import { FolderIcon, FolderOpenIcon, FileIcon } from '@aircall/react-icons';
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 4. Prop mapping — Tractor `Tree` → DS `DataTree`
|
|
50
|
+
|
|
51
|
+
| Tractor `Tree` prop | DS `DataTree` prop | Notes |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| `nodes` (fixed shape `{ id, label, children }`) | `items` + `getKey` + `getLabel` + `getChildren` | Keep your own data shape; accessors replace the forced schema |
|
|
54
|
+
| node `prefix` (leading icon) | `getIcon={(node, { isFolder, isExpanded }) => …}` | Icon gutter is reserved; iconless rows still align |
|
|
55
|
+
| node `suffix` / `description` | Return JSX from `getLabel` | `getLabel` accepts `React.ReactNode`, not just a string |
|
|
56
|
+
| `selectionMode` (`'none' \| 'single' \| 'multiple'`) | `selectionMode` | Same values |
|
|
57
|
+
| `selectedKeys` | `selectedKeys` | Now typed `string[]` (not `K extends PrimitiveValue`) |
|
|
58
|
+
| `defaultSelectedKeys` | `defaultSelectedKeys` | Same |
|
|
59
|
+
| `onItemSelected: (key, allKeys) => void` | `onSelectionChange: (keys: string[]) => void` | Receives the **full** key set, not the toggled key |
|
|
60
|
+
| `expandedKeys` | `expandedKeys` | Unchanged |
|
|
61
|
+
| `defaultExpandedKeys` | `defaultExpandedKeys` | Unchanged |
|
|
62
|
+
| `onExpandChange` | `onExpandedChange` | Renamed — note the `d` |
|
|
63
|
+
| `bordered` / `readOnly` / `disabled` | No direct equivalent | Style via `className`; remove or omit |
|
|
64
|
+
|
|
65
|
+
## 5. Selection behaviour changes
|
|
66
|
+
|
|
67
|
+
- **`selectionMode="multiple"`** — renders a **tri-state checkbox** per row with parent↔child cascade. Checking a folder checks every descendant; unchecking one child flips the folder to indeterminate.
|
|
68
|
+
- **`selectionMode="single"`** — highlights one row on click.
|
|
69
|
+
- **`selectionMode="none"`** (default) — no selection; row click toggles expansion.
|
|
70
|
+
|
|
71
|
+
`onSelectionChange` receives the **full** current key set every time, not just the key that changed. If you were using the first `(key, allKeys)` argument to identify the toggled item, switch to diffing the new set against your previous state.
|
|
72
|
+
|
|
73
|
+
## 6. Before / After
|
|
74
|
+
|
|
75
|
+
### 6a. Basic data-driven tree
|
|
76
|
+
|
|
77
|
+
**Before (Tractor):**
|
|
78
|
+
```tsx
|
|
79
|
+
import { Tree } from '@aircall/tractor';
|
|
80
|
+
|
|
81
|
+
const nodes = [
|
|
82
|
+
{ id: 'eng', label: 'Engineering', children: [{ id: 'fe', label: 'Frontend' }] }
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
<Tree nodes={nodes} />
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**After (DS):**
|
|
89
|
+
```tsx
|
|
90
|
+
import { DataTree } from '@aircall/ds';
|
|
91
|
+
|
|
92
|
+
const items = [
|
|
93
|
+
{ id: 'eng', label: 'Engineering', children: [{ id: 'fe', label: 'Frontend' }] }
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
<DataTree
|
|
97
|
+
items={items}
|
|
98
|
+
getKey={node => node.id}
|
|
99
|
+
getLabel={node => node.label}
|
|
100
|
+
getChildren={node => node.children}
|
|
101
|
+
/>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 6b. Icons and rich labels (`prefix` / `suffix` / `description`)
|
|
105
|
+
|
|
106
|
+
**Before (Tractor):**
|
|
107
|
+
```tsx
|
|
108
|
+
import { Tree } from '@aircall/tractor';
|
|
109
|
+
import { FolderIcon } from '@aircall/react-icons';
|
|
110
|
+
|
|
111
|
+
const nodes = [
|
|
112
|
+
{
|
|
113
|
+
id: 'eng',
|
|
114
|
+
label: 'Engineering',
|
|
115
|
+
prefix: <FolderIcon />,
|
|
116
|
+
description: 'Main team',
|
|
117
|
+
children: [{ id: 'fe', label: 'Frontend' }]
|
|
118
|
+
}
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
<Tree nodes={nodes} />
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**After (DS):**
|
|
125
|
+
```tsx
|
|
126
|
+
import { DataTree } from '@aircall/ds';
|
|
127
|
+
import { FolderIcon, FolderOpenIcon, FileIcon } from '@aircall/react-icons';
|
|
128
|
+
|
|
129
|
+
const items = [
|
|
130
|
+
{ id: 'eng', label: 'Engineering', description: 'Main team', children: [{ id: 'fe', label: 'Frontend' }] }
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
<DataTree
|
|
134
|
+
items={items}
|
|
135
|
+
getKey={n => n.id}
|
|
136
|
+
getChildren={n => n.children}
|
|
137
|
+
getIcon={(n, { isFolder, isExpanded }) =>
|
|
138
|
+
isFolder ? (isExpanded ? <FolderOpenIcon /> : <FolderIcon />) : <FileIcon />
|
|
139
|
+
}
|
|
140
|
+
getLabel={n => (
|
|
141
|
+
<span className="flex flex-col">
|
|
142
|
+
<span>{n.label}</span>
|
|
143
|
+
{n.description ? (
|
|
144
|
+
<span className="text-xs text-muted-foreground">{n.description}</span>
|
|
145
|
+
) : null}
|
|
146
|
+
</span>
|
|
147
|
+
)}
|
|
148
|
+
/>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 6c. Controlled selection and expansion
|
|
152
|
+
|
|
153
|
+
**Before (Tractor):**
|
|
154
|
+
```tsx
|
|
155
|
+
import { Tree } from '@aircall/tractor';
|
|
156
|
+
|
|
157
|
+
<Tree
|
|
158
|
+
nodes={nodes}
|
|
159
|
+
selectionMode="multiple"
|
|
160
|
+
selectedKeys={selected}
|
|
161
|
+
expandedKeys={expanded}
|
|
162
|
+
onItemSelected={(key, allKeys) => setSelected(allKeys)}
|
|
163
|
+
onExpandChange={keys => setExpanded(keys)}
|
|
164
|
+
/>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**After (DS):**
|
|
168
|
+
```tsx
|
|
169
|
+
import { DataTree } from '@aircall/ds';
|
|
170
|
+
|
|
171
|
+
<DataTree
|
|
172
|
+
items={items}
|
|
173
|
+
getKey={n => n.id}
|
|
174
|
+
getLabel={n => n.label}
|
|
175
|
+
getChildren={n => n.children}
|
|
176
|
+
selectionMode="multiple"
|
|
177
|
+
selectedKeys={selected}
|
|
178
|
+
expandedKeys={expanded}
|
|
179
|
+
onSelectionChange={keys => setSelected(keys)}
|
|
180
|
+
onExpandedChange={keys => setExpanded(keys)}
|
|
181
|
+
/>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### 6d. Full-control primitives (custom rows, drag-and-drop)
|
|
185
|
+
|
|
186
|
+
**Before (Tractor):**
|
|
187
|
+
```tsx
|
|
188
|
+
import { Tree } from '@aircall/tractor';
|
|
189
|
+
|
|
190
|
+
<Tree nodes={nodes} />
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**After (DS — primitives):**
|
|
194
|
+
```tsx
|
|
195
|
+
import { Tree, TreeItem, TreeItemLabel } from '@aircall/ds';
|
|
196
|
+
import { useTree } from '@headless-tree/react';
|
|
197
|
+
import { syncDataLoaderFeature, hotkeysCoreFeature } from '@headless-tree/core';
|
|
198
|
+
|
|
199
|
+
const tree = useTree<Item>({
|
|
200
|
+
rootItemId: 'root',
|
|
201
|
+
dataLoader: {
|
|
202
|
+
getItem: id => itemMap[id],
|
|
203
|
+
getChildren: id => childIds[id] ?? []
|
|
204
|
+
},
|
|
205
|
+
features: [syncDataLoaderFeature, hotkeysCoreFeature]
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
<Tree indent={20} tree={tree}>
|
|
209
|
+
{tree.getItems().map(item => (
|
|
210
|
+
<TreeItem item={item} key={item.getId()}>
|
|
211
|
+
<TreeItemLabel />
|
|
212
|
+
</TreeItem>
|
|
213
|
+
))}
|
|
214
|
+
</Tree>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## 7. TreeSelect
|
|
218
|
+
|
|
219
|
+
Tractor's `TreeSelect` has no turn-key DS equivalent. Compose `DataTree` inside a `Popover`:
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
import { DataTree, Popover, PopoverContent, PopoverTrigger, Button } from '@aircall/ds';
|
|
223
|
+
|
|
224
|
+
<Popover>
|
|
225
|
+
<PopoverTrigger asChild>
|
|
226
|
+
<Button variant="outline">Select team…</Button>
|
|
227
|
+
</PopoverTrigger>
|
|
228
|
+
<PopoverContent className="w-72 p-2">
|
|
229
|
+
<DataTree
|
|
230
|
+
items={items}
|
|
231
|
+
getKey={n => n.id}
|
|
232
|
+
getLabel={n => n.label}
|
|
233
|
+
getChildren={n => n.children}
|
|
234
|
+
selectionMode="single"
|
|
235
|
+
selectedKeys={selected}
|
|
236
|
+
onSelectionChange={keys => {
|
|
237
|
+
setSelected(keys);
|
|
238
|
+
}}
|
|
239
|
+
/>
|
|
240
|
+
</PopoverContent>
|
|
241
|
+
</Popover>
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## 8. Common Mistakes
|
|
245
|
+
|
|
246
|
+
### Mistake 1 — Using `onItemSelected` instead of `onSelectionChange`
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
// Wrong — onItemSelected does not exist on DataTree
|
|
250
|
+
<DataTree
|
|
251
|
+
items={items}
|
|
252
|
+
getKey={n => n.id}
|
|
253
|
+
getLabel={n => n.label}
|
|
254
|
+
onItemSelected={(key, allKeys) => setSelected(allKeys)}
|
|
255
|
+
/>
|
|
256
|
+
|
|
257
|
+
// Correct — onSelectionChange receives the full key set
|
|
258
|
+
<DataTree
|
|
259
|
+
items={items}
|
|
260
|
+
getKey={n => n.id}
|
|
261
|
+
getLabel={n => n.label}
|
|
262
|
+
onSelectionChange={keys => setSelected(keys)}
|
|
263
|
+
/>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Tractor's `onItemSelected` passed `(toggledKey, allKeys)` as two arguments. DS `onSelectionChange` passes the full current key set as a single `string[]`. Passing `onItemSelected` to `DataTree` passes an unknown prop that is silently ignored — the selection callback never fires.
|
|
267
|
+
|
|
268
|
+
Source: `packages/ds/src/components/data-tree.tsx`
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
### Mistake 2 — Using `onExpandChange` instead of `onExpandedChange`
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
// Wrong — onExpandChange (no 'd') is not a DataTree prop
|
|
276
|
+
<DataTree
|
|
277
|
+
items={items}
|
|
278
|
+
getKey={n => n.id}
|
|
279
|
+
getLabel={n => n.label}
|
|
280
|
+
expandedKeys={expanded}
|
|
281
|
+
onExpandChange={keys => setExpanded(keys)}
|
|
282
|
+
/>
|
|
283
|
+
|
|
284
|
+
// Correct — onExpandedChange (with 'd')
|
|
285
|
+
<DataTree
|
|
286
|
+
items={items}
|
|
287
|
+
getKey={n => n.id}
|
|
288
|
+
getLabel={n => n.label}
|
|
289
|
+
expandedKeys={expanded}
|
|
290
|
+
onExpandedChange={keys => setExpanded(keys)}
|
|
291
|
+
/>
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The Tractor callback was `onExpandChange`; DS renamed it to `onExpandedChange` to match the pattern of other controlled callbacks in the library. Passing the old name results in a silent no-op — the `expandedKeys` state is never updated.
|
|
295
|
+
|
|
296
|
+
Source: `packages/ds/src/components/data-tree.tsx`
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
### Mistake 3 — Passing `nodes` instead of `items` + accessor props
|
|
301
|
+
|
|
302
|
+
```tsx
|
|
303
|
+
// Wrong — DataTree has no `nodes` prop
|
|
304
|
+
<DataTree nodes={myNodes} />
|
|
305
|
+
|
|
306
|
+
// Correct — pass items and accessor functions
|
|
307
|
+
<DataTree
|
|
308
|
+
items={myNodes}
|
|
309
|
+
getKey={n => n.id}
|
|
310
|
+
getLabel={n => n.label}
|
|
311
|
+
getChildren={n => n.children}
|
|
312
|
+
/>
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Tractor forced callers to reshape their data into `{ id, label, children }`. `DataTree` accepts your data as-is via `items` and resolves identifiers and hierarchy through the `getKey` / `getLabel` / `getChildren` accessor functions. Passing `nodes` is an unknown prop — `DataTree` renders nothing because its required `items` prop is absent.
|
|
316
|
+
|
|
317
|
+
Source: `packages/ds/src/components/data-tree.tsx`
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
### Mistake 4 — Placing `suffix`/`description` as node-level props instead of returning JSX from `getLabel`
|
|
322
|
+
|
|
323
|
+
```tsx
|
|
324
|
+
// Wrong — DataTree does not read suffix or description from node objects
|
|
325
|
+
<DataTree
|
|
326
|
+
items={[{ id: '1', label: 'Frontend', suffix: <Badge>3</Badge> }]}
|
|
327
|
+
getKey={n => n.id}
|
|
328
|
+
getLabel={n => n.label}
|
|
329
|
+
/>
|
|
330
|
+
|
|
331
|
+
// Correct — embed rich content directly in getLabel's return value
|
|
332
|
+
<DataTree
|
|
333
|
+
items={[{ id: '1', label: 'Frontend', badge: 3 }]}
|
|
334
|
+
getKey={n => n.id}
|
|
335
|
+
getLabel={n => (
|
|
336
|
+
<span className="flex items-center gap-2">
|
|
337
|
+
{n.label}
|
|
338
|
+
<Badge>{n.badge}</Badge>
|
|
339
|
+
</span>
|
|
340
|
+
)}
|
|
341
|
+
/>
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Tractor reserved `suffix` and `description` as fixed node-shape keys that the tree rendered for you. `DataTree`'s `getLabel` returns `React.ReactNode`, so all rich content goes there — the node object carries only your domain data, not presentation slots.
|
|
345
|
+
|
|
346
|
+
Source: `packages/ds/src/components/data-tree.tsx`
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aircall-ds/setup
|
|
3
|
+
description: >
|
|
4
|
+
Set up @aircall/ds (and @aircall/blocks) in an app migrating off @aircall/tractor.
|
|
5
|
+
Load when wiring DS into a project for the first time: installing the package,
|
|
6
|
+
importing the precompiled globals.css, configuring Tailwind v4 / PostCSS,
|
|
7
|
+
mounting ThemeProvider / TooltipProvider / Toaster, and running DS and Tractor
|
|
8
|
+
side by side during a progressive migration.
|
|
9
|
+
type: core
|
|
10
|
+
library: aircall-ds
|
|
11
|
+
library_version: "0.13.0"
|
|
12
|
+
sources:
|
|
13
|
+
- "aircall/hydra:docs/migration-guides/tractor-to-ds/00-setup.md"
|
|
14
|
+
- "aircall/hydra:packages/ds/package.json"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Setting up @aircall/ds
|
|
18
|
+
|
|
19
|
+
Get the app to a state where DS and Tractor render side by side. The migration is
|
|
20
|
+
progressive: each migrated screen ships independently while `@aircall/tractor` stays
|
|
21
|
+
installed until the last Tractor import is gone.
|
|
22
|
+
|
|
23
|
+
All imports use the top-level `from '@aircall/ds'` form — the only surface the
|
|
24
|
+
published package exposes.
|
|
25
|
+
|
|
26
|
+
## Setup
|
|
27
|
+
|
|
28
|
+
Install the design system and icon packages (keep Tractor and icons for now):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pnpm add @aircall/ds @aircall/react-icons
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
> **Version floor — `@aircall/react-icons` must be `>= 0.4.0`** (the version `@aircall/ds`
|
|
35
|
+
> is built against). `@aircall/ds` imports country-flag icons (`CountryFlag` → `FlagUs`,
|
|
36
|
+
> `FlagFr`, …) from `@aircall/react-icons`, and the ds barrel pulls them eagerly — so an
|
|
37
|
+
> older react-icons (≤ 0.3.0, which lacks those exports) makes the **ds bundle fail to
|
|
38
|
+
> resolve at build time** (`FlagUs is not exported`), even in files that never use
|
|
39
|
+
> `CountryFlag`. ds declares the peer loosely as `*`, so npm/pnpm won't warn — upgrade
|
|
40
|
+
> explicitly: `pnpm add @aircall/react-icons@latest`.
|
|
41
|
+
|
|
42
|
+
Import the **precompiled** DS bundle once from your JS/TS entry. It already contains
|
|
43
|
+
the reset, tokens, and every utility the DS components use — import it directly, do
|
|
44
|
+
not prepend `@import 'tailwindcss'` and do not add `@source` for the DS package:
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// main.tsx
|
|
48
|
+
import '@aircall/ds/globals.css';
|
|
49
|
+
import '@aircall/blocks/globals.css'; // only if you use @aircall/blocks compositions
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
DS has a set of root providers, all per-feature — none is mandatory. A first migration that
|
|
53
|
+
swaps only a few components needs none of them and renders on the default light theme. Add
|
|
54
|
+
each one only when you use the feature it backs. Keep `TractorProvider` mounted alongside
|
|
55
|
+
whichever DS providers you add:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { I18nextProvider, useTranslation } from 'react-i18next';
|
|
59
|
+
import {
|
|
60
|
+
ThemeProvider,
|
|
61
|
+
Toaster,
|
|
62
|
+
TooltipProvider,
|
|
63
|
+
DsI18nProvider,
|
|
64
|
+
NotificationQueueProvider,
|
|
65
|
+
NotificationSlot,
|
|
66
|
+
} from '@aircall/ds';
|
|
67
|
+
import { TractorProvider } from '@aircall/tractor';
|
|
68
|
+
import { i18n } from './i18n'; // your app's react-i18next instance
|
|
69
|
+
|
|
70
|
+
function App() {
|
|
71
|
+
return (
|
|
72
|
+
<I18nextProvider i18n={i18n}>
|
|
73
|
+
<Providers>{/* Router, queries, your tree */}</Providers>
|
|
74
|
+
</I18nextProvider>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// DsI18nProvider MUST sit *under* react-i18next and be fed the user's active language,
|
|
79
|
+
// so DS (and @aircall/blocks) strings follow the same locale. Reading it via
|
|
80
|
+
// useTranslation() keeps it reactive — it re-runs when the user switches language.
|
|
81
|
+
function Providers({ children }: React.PropsWithChildren) {
|
|
82
|
+
const { i18n } = useTranslation();
|
|
83
|
+
return (
|
|
84
|
+
<DsI18nProvider language={i18n.language}>
|
|
85
|
+
<TractorProvider>
|
|
86
|
+
<ThemeProvider defaultTheme="light" storageKey="my-app-theme">
|
|
87
|
+
<TooltipProvider delay={0}>
|
|
88
|
+
<NotificationQueueProvider>
|
|
89
|
+
{children}
|
|
90
|
+
<NotificationSlot slot="page" />
|
|
91
|
+
<Toaster />
|
|
92
|
+
</NotificationQueueProvider>
|
|
93
|
+
</TooltipProvider>
|
|
94
|
+
</ThemeProvider>
|
|
95
|
+
</TractorProvider>
|
|
96
|
+
</DsI18nProvider>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| Provider | Required for | Notes |
|
|
102
|
+
| ----------------- | ----------------------------- | ----- |
|
|
103
|
+
| `DsI18nProvider` | DS + blocks strings following the user's language | Props: `{ language?: string; children }`. Mount it **as a descendant of your react-i18next `I18nextProvider`** and pass the user's active language (`language={i18n.language}` via `useTranslation`), so DS tracks the same locale. The DS i18n singleton self-initializes at import (falls back to `navigator.language`, then `en`), so strings still render without it — the provider drives language *changes*. Mount **exactly one**, and use **either** `DsI18nProvider` **or** `syncDsLanguage(i18n)` (mirrors + follows an i18next instance; for non-React startup) — never both (they fight over the same singleton). |
|
|
104
|
+
| `ThemeProvider` | dark/light/system themes | `storageKey` is the per-app localStorage key. Host-mounted extensions: skip it — it writes `data-theme` on `<html>` and fights the host's theme control. |
|
|
105
|
+
| `TooltipProvider` | every `<Tooltip>` in the tree | Prop is `delay` (Base UI), default `0`. Optional — tooltips still open without it; it only supplies the shared open-delay / grouping. |
|
|
106
|
+
| `Toaster` | `toast()` calls | Render once at the root. |
|
|
107
|
+
| `NotificationQueueProvider` | the notification queue / banners | Props: `{ children }`. Wrap it above every `useNotification` / `useNotificationQueue` caller and every `NotificationSlot`. |
|
|
108
|
+
| `NotificationSlot` | rendering queued notifications | Props: `{ slot: string; className? }`. Not a provider — the output sink; one per slot/area (`slot="page"` at the root). Must be a descendant of `NotificationQueueProvider` or it throws. |
|
|
109
|
+
|
|
110
|
+
## Core Patterns
|
|
111
|
+
|
|
112
|
+
### Author your own Tailwind classes alongside DS — required once you write any
|
|
113
|
+
|
|
114
|
+
The precompiled `@aircall/ds/globals.css` contains **only the utilities DS's own components
|
|
115
|
+
use** — not arbitrary Tailwind classes. Any utility *your* code authors that DS doesn't
|
|
116
|
+
itself ship (e.g. `px-0`, a one-off `max-w-3xl`, a custom `gap`) produces **no CSS** unless
|
|
117
|
+
you run your own Tailwind build — the class sits in your JSX, the element renders unstyled,
|
|
118
|
+
and nothing errors. A Tractor → DS migration almost always authors such classes (the
|
|
119
|
+
`migrate-tractor/styling` recipe converts `<Flex p={2}>` / xstyled props → Tailwind
|
|
120
|
+
utilities), so this is effectively **required**, not optional.
|
|
121
|
+
|
|
122
|
+
Set up Tailwind v4 for *your* sources and point `@source` at your files, not at DS:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pnpm add -D tailwindcss @tailwindcss/postcss
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
// postcss.config.mjs (auto-detected by PostCSS bundlers: Vite, rsbuild/Rspack, Next, webpack…)
|
|
130
|
+
export default { plugins: { '@tailwindcss/postcss': {} } };
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```css
|
|
134
|
+
/* style.css — imported once from your JS/TS entry */
|
|
135
|
+
/* Import ONLY theme + utilities — NOT the full `@import 'tailwindcss'`, which would add a
|
|
136
|
+
SECOND Preflight on top of the one @aircall/ds already ships (see the Common Mistake). */
|
|
137
|
+
@import 'tailwindcss/theme';
|
|
138
|
+
@import 'tailwindcss/utilities';
|
|
139
|
+
@import '@aircall/ds/globals.css'; /* DS bundle: the single Preflight + tokens + DS utilities */
|
|
140
|
+
@import '@aircall/blocks/globals.css'; /* if you use @aircall/blocks */
|
|
141
|
+
@source "src"; /* scan YOUR code so its classes are generated */
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
DS **design-token** utilities (`text-muted-foreground`, `bg-card`, …) come from the
|
|
145
|
+
precompiled DS bundle; your build adds only the **core** utilities (`px-0`, `max-w-2xl`,
|
|
146
|
+
layout, spacing) DS doesn't ship. Splitting into `tailwindcss/theme` + `tailwindcss/utilities`
|
|
147
|
+
(rather than the full `tailwindcss`) is what keeps the **single** Preflight — the theme gives
|
|
148
|
+
your utilities their scale, `@source` scans your code, and no base reset is duplicated.
|
|
149
|
+
|
|
150
|
+
### Peers you provide
|
|
151
|
+
|
|
152
|
+
`@aircall/ds` ships its internal-only libraries (`react-day-picker`, `@tanstack/react-table`,
|
|
153
|
+
`date-fns`, `embla-carousel-react`, `sonner`, …) as regular **dependencies** — they install
|
|
154
|
+
automatically with ds; nothing to add. The only peers YOU must provide:
|
|
155
|
+
|
|
156
|
+
- `react` / `react-dom` (`^18 || ^19`)
|
|
157
|
+
- **`@aircall/react-icons` (>= 0.4.0)** — see the version-floor note above.
|
|
158
|
+
- `@aircall/numbers` / `@aircall/hooks` (Aircall registry; `auto-install-peers` may fetch them).
|
|
159
|
+
|
|
160
|
+
## Common Mistakes
|
|
161
|
+
|
|
162
|
+
### HIGH — Full `@import 'tailwindcss'` on top of the DS bundle (duplicated Preflight)
|
|
163
|
+
|
|
164
|
+
Wrong — the full import re-applies Tailwind's Preflight (base reset) on top of the one the
|
|
165
|
+
DS bundle already ships:
|
|
166
|
+
|
|
167
|
+
```css
|
|
168
|
+
@import 'tailwindcss'; /* ← also pulls in Preflight = a SECOND base reset */
|
|
169
|
+
@import '@aircall/ds/globals.css';
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Correct — **not authoring your own classes:** just import the self-contained bundle:
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
import '@aircall/ds/globals.css';
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Correct — **authoring your own classes** (the migration case): import only the theme +
|
|
179
|
+
utilities layers, never the full `tailwindcss`:
|
|
180
|
+
|
|
181
|
+
```css
|
|
182
|
+
@import 'tailwindcss/theme';
|
|
183
|
+
@import 'tailwindcss/utilities';
|
|
184
|
+
@import '@aircall/ds/globals.css';
|
|
185
|
+
@source "src";
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`@aircall/ds/globals.css` is a precompiled Tailwind v4 bundle that already includes
|
|
189
|
+
Preflight. The full `@import 'tailwindcss'` ALWAYS pulls Preflight in again — a duplicated
|
|
190
|
+
base reset that flips `border-color` to `currentColor` (dark borders) and changes base
|
|
191
|
+
padding/margins, breaking the DS styling in light **and** dark mode. The
|
|
192
|
+
`theme` + `utilities` split generates your authored utilities with **no** second reset.
|
|
193
|
+
|
|
194
|
+
Source: aircall/hydra:docs/migration-guides/tractor-to-ds/00-setup.md (§3)
|
|
195
|
+
|
|
196
|
+
### HIGH — Using `delayDuration` on TooltipProvider
|
|
197
|
+
|
|
198
|
+
Wrong:
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
<TooltipProvider delayDuration={0}>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Correct:
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
<TooltipProvider delay={0}>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
DS tooltips are Base UI, not Radix — the prop is `delay`. `delayDuration` is silently ignored, so tooltips keep the default timing instead of the value you set.
|
|
211
|
+
|
|
212
|
+
Source: aircall/hydra:docs/migration-guides/tractor-to-ds/00-setup.md (§4)
|
|
213
|
+
|
|
214
|
+
### MEDIUM — Mounting ThemeProvider inside a host-mounted extension
|
|
215
|
+
|
|
216
|
+
Wrong:
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
// extension mounted inside the dashboard host
|
|
220
|
+
<ThemeProvider defaultTheme="light" storageKey="ext-theme">
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Correct:
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
// no ThemeProvider — inherit the host's data-theme on <html>
|
|
227
|
+
<TooltipProvider delay={0}>{children}</TooltipProvider>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`ThemeProvider` writes `data-theme` on `<html>`; inside a host dashboard it fights the host's own theme control, causing the extension to flip themes independently. DS reads the same `data-theme` the host sets.
|
|
231
|
+
|
|
232
|
+
Source: aircall/hydra:docs/migration-guides/tractor-to-ds/00-setup.md (§4)
|
|
233
|
+
|
|
234
|
+
### MEDIUM — Forgetting the DS reset is global during cohabitation
|
|
235
|
+
|
|
236
|
+
Wrong: assuming DS styles only affect migrated screens.
|
|
237
|
+
|
|
238
|
+
Correct: budget a one-time pass to fix small Tractor base-element breakage when DS first lands; keep both providers mounted until the last Tractor import is removed.
|
|
239
|
+
|
|
240
|
+
`@aircall/ds/globals.css` carries a Tailwind v4 preflight reset applied to the whole document — it is not scoped — so introducing DS can shift the appearance of un-migrated Tractor UI. Fix breakage as it surfaces rather than sandboxing the reset.
|
|
241
|
+
|
|
242
|
+
Source: aircall/hydra:docs/migration-guides/tractor-to-ds/00-setup.md (§5)
|
|
243
|
+
|
|
244
|
+
### HIGH — Jest/jsdom: ESM transform + `userEvent` on DS components
|
|
245
|
+
|
|
246
|
+
`@aircall/ds` ships ESM and uses Tailwind v4 named-group classes (`group/input-group`, `button.group`). Default Jest setups break two ways:
|
|
247
|
+
|
|
248
|
+
Wrong: leaving Jest's defaults — `import { Button } from '@aircall/ds'` fails to parse (ESM), and `userEvent.click(...)` on DS components throws in jsdom (its `nwsapi` CSS engine cannot parse the named-group selectors).
|
|
249
|
+
|
|
250
|
+
Correct:
|
|
251
|
+
|
|
252
|
+
```js
|
|
253
|
+
// jest.config.mjs
|
|
254
|
+
transformIgnorePatterns: ['node_modules/(?!(?:@aircall/ds|@aircall/blocks)/)'],
|
|
255
|
+
moduleNameMapper: { '^react-day-picker$': '<rootDir>/test/stub.js' } // stub optional peers you don't use
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
and prefer `fireEvent.*` over `userEvent.*` when interacting with DS components in jsdom.
|
|
259
|
+
|
|
260
|
+
Source: aircall/hydra:packages/ds/package.json
|
|
261
|
+
|
|
262
|
+
### HIGH — Opening a Base UI popup in jsdom crashes even with `fireEvent` — add a selector guard
|
|
263
|
+
|
|
264
|
+
`fireEvent` over `userEvent` (above) is necessary but **not sufficient** for any DS
|
|
265
|
+
component that opens a floating popup: `Combobox`, `Select`, `DropdownMenu`, `Popover`,
|
|
266
|
+
`Tooltip`. The moment the popup opens, the test throws:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
SyntaxError: 'div.group,,field >svg' is not a valid selector
|
|
270
|
+
at nwsapi … getComputedStyle (jsdom) ← @floating-ui/dom getOverflowAncestors ← Base UI useAnchorPositioning
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Cause: on open, `@floating-ui/dom` calls `getComputedStyle()` to find the element's
|
|
274
|
+
overflow ancestors; jsdom matches **every** stylesheet rule against the element, and for
|
|
275
|
+
a DS Tailwind v4 `:has()` / `:is(:where())` rule `nwsapi` resolves the assertion via a
|
|
276
|
+
nested `querySelector` whose mangled inner selector it rejects — and that `SyntaxError`
|
|
277
|
+
escapes jsdom's own `matchesDontThrow`. Real browsers handle these selectors fine.
|
|
278
|
+
|
|
279
|
+
Fix — guard `querySelector`/`querySelectorAll` in your Jest setup to swallow ONLY the
|
|
280
|
+
"is not a valid selector" error (the unsupported `:has()` assertion becomes "no match"):
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
// jest setupFilesAfterEnv
|
|
284
|
+
const isBadSelector = (e: unknown) =>
|
|
285
|
+
e instanceof Error && e.message.includes('is not a valid selector');
|
|
286
|
+
const EMPTY = document.createElement('div').querySelectorAll('x-none');
|
|
287
|
+
|
|
288
|
+
for (const proto of [Element.prototype, Document.prototype, DocumentFragment.prototype]) {
|
|
289
|
+
const qs = proto.querySelector;
|
|
290
|
+
const qsa = proto.querySelectorAll;
|
|
291
|
+
proto.querySelector = function (s: string) {
|
|
292
|
+
try { return qs.call(this, s); } catch (e) { if (isBadSelector(e)) return null; throw e; }
|
|
293
|
+
};
|
|
294
|
+
proto.querySelectorAll = function (s: string) {
|
|
295
|
+
try { return qsa.call(this, s); } catch (e) { if (isBadSelector(e)) return EMPTY; throw e; }
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
One ~20-line shim unblocks every DS popup at once. Without it, any test that opens a
|
|
301
|
+
combobox/select/menu silently fails with "Unable to find element" after the crash aborts
|
|
302
|
+
the render.
|
|
303
|
+
|
|
304
|
+
Source: aircall/hydra:packages/ds/package.json
|
|
305
|
+
|
|
306
|
+
### HIGH — DS `Switch` (and Base UI toggles) won't flip via `click` in jsdom
|
|
307
|
+
|
|
308
|
+
Wrong:
|
|
309
|
+
|
|
310
|
+
```tsx
|
|
311
|
+
fireEvent.click(screen.getByTestId('my-switch')); // aria-checked never changes
|
|
312
|
+
await userEvent.click(screen.getByTestId('my-switch')); // also no-op
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Correct — fire on the hidden checkbox the Switch renders as a sibling:
|
|
316
|
+
|
|
317
|
+
```tsx
|
|
318
|
+
const root = screen.getByTestId('my-switch');
|
|
319
|
+
const input = root.parentElement!.querySelector('input[type="checkbox"]');
|
|
320
|
+
fireEvent.click(input!); // toggles + fires onCheckedChange
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
DS `Switch` is a Base UI `<span role="switch">` whose `onClick` calls `preventDefault()`
|
|
324
|
+
then re-dispatches a synthetic `PointerEvent('click')` to a visually-hidden sibling
|
|
325
|
+
`<input type="checkbox">` — jsdom doesn't complete that re-dispatch, so the visible span
|
|
326
|
+
never updates. The hidden input's `onChange` is what actually fires `onCheckedChange`, so
|
|
327
|
+
drive it directly. Same applies to any Base UI control with a hidden form input.
|
|
328
|
+
|
|
329
|
+
Source: aircall/hydra:packages/ds/src/components/switch.tsx
|
|
330
|
+
|
|
331
|
+
### MEDIUM — Import `DataTable` column types from `@aircall/ds`, not `@tanstack/react-table`
|
|
332
|
+
|
|
333
|
+
`@aircall/ds` ships the react-table runtime as its own dependency and **re-exports** the types you author (`ColumnDef`, `SortingState`, `RowSelectionState`, `OnChangeFn`). `@tanstack/react-table` is therefore not a direct dependency of your app, so importing from it won't resolve.
|
|
334
|
+
|
|
335
|
+
Wrong:
|
|
336
|
+
|
|
337
|
+
```tsx
|
|
338
|
+
import type { ColumnDef } from '@tanstack/react-table'; // not in your node_modules
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Correct:
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
import { DataTable, type ColumnDef } from '@aircall/ds';
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Source: aircall/hydra:packages/ds/src/index.ts
|