@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.
Files changed (34) hide show
  1. package/README.md +31 -0
  2. package/dist/globals.css +1 -1
  3. package/dist/index.d.ts +28 -28
  4. package/dist/index.js +1 -1
  5. package/package.json +12 -2
  6. package/skills/aircall-ds/migrate-icons/SKILL.md +346 -0
  7. package/skills/aircall-ds/migrate-tractor/SKILL.md +314 -0
  8. package/skills/aircall-ds/migrate-tractor/accordion/SKILL.md +276 -0
  9. package/skills/aircall-ds/migrate-tractor/alert/SKILL.md +225 -0
  10. package/skills/aircall-ds/migrate-tractor/avatar/SKILL.md +272 -0
  11. package/skills/aircall-ds/migrate-tractor/badge/SKILL.md +274 -0
  12. package/skills/aircall-ds/migrate-tractor/button/SKILL.md +277 -0
  13. package/skills/aircall-ds/migrate-tractor/card/SKILL.md +278 -0
  14. package/skills/aircall-ds/migrate-tractor/combobox/SKILL.md +346 -0
  15. package/skills/aircall-ds/migrate-tractor/data-table/SKILL.md +333 -0
  16. package/skills/aircall-ds/migrate-tractor/dialog/SKILL.md +206 -0
  17. package/skills/aircall-ds/migrate-tractor/divider/SKILL.md +226 -0
  18. package/skills/aircall-ds/migrate-tractor/dropdown-menu/SKILL.md +266 -0
  19. package/skills/aircall-ds/migrate-tractor/dropzone/SKILL.md +338 -0
  20. package/skills/aircall-ds/migrate-tractor/form-and-field/SKILL.md +325 -0
  21. package/skills/aircall-ds/migrate-tractor/gauge/SKILL.md +248 -0
  22. package/skills/aircall-ds/migrate-tractor/input/SKILL.md +261 -0
  23. package/skills/aircall-ds/migrate-tractor/item/SKILL.md +298 -0
  24. package/skills/aircall-ds/migrate-tractor/link/SKILL.md +263 -0
  25. package/skills/aircall-ds/migrate-tractor/popover/SKILL.md +214 -0
  26. package/skills/aircall-ds/migrate-tractor/select/SKILL.md +245 -0
  27. package/skills/aircall-ds/migrate-tractor/sheet-vs-drawer/SKILL.md +272 -0
  28. package/skills/aircall-ds/migrate-tractor/skeleton/SKILL.md +190 -0
  29. package/skills/aircall-ds/migrate-tractor/styling/SKILL.md +421 -0
  30. package/skills/aircall-ds/migrate-tractor/tabs/SKILL.md +250 -0
  31. package/skills/aircall-ds/migrate-tractor/toast/SKILL.md +322 -0
  32. package/skills/aircall-ds/migrate-tractor/tooltip/SKILL.md +204 -0
  33. package/skills/aircall-ds/migrate-tractor/tree/SKILL.md +346 -0
  34. 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