@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,314 @@
1
+ ---
2
+ name: aircall-ds/migrate-tractor
3
+ description: >
4
+ Migrate a file from @aircall/tractor to @aircall/ds. Load FIRST when converting
5
+ Tractor components (Modal, Banner, Select, Button, Typography, Flex, Tooltip, Tag,
6
+ Dropdown, Form, …) to @aircall/ds. Carries the cross-cutting rules (import form,
7
+ prop renames, the render prop, data-attribute shape) and a lookup table mapping
8
+ each Tractor component to its @aircall/ds target and the recipe skill to load next.
9
+ type: core
10
+ library: aircall-ds
11
+ library_version: "0.13.0"
12
+ requires:
13
+ - aircall-ds/setup
14
+ sources:
15
+ - "aircall/hydra:docs/migration-guides/tractor-to-ds/01-global-rules.md"
16
+ - "aircall/hydra:docs/migration-guides/tractor-to-ds/04-lookup.md"
17
+ ---
18
+
19
+ # Migrating from @aircall/tractor to @aircall/ds
20
+
21
+ Load this skill first for any Tractor → DS migration. It gives the cross-cutting
22
+ rules that apply to every component and a lookup table to find the DS replacement.
23
+ Then load the component-specific recipe skill from the `recipe to load` column.
24
+
25
+ ## How to run this migration (end-to-end)
26
+
27
+ This skill and its recipes cover *converting code*. The full migration is a repeatable
28
+ loop — do it incrementally, one file/screen at a time, shipping each green:
29
+
30
+ 0. **Set up once** — load `@aircall/ds#aircall-ds/setup` and do the wiring before any
31
+ conversion: install `@aircall/ds` + `@aircall/react-icons` (>= 0.4.0), import the
32
+ precompiled `globals.css`, mount the root providers (incl. `DsI18nProvider` under
33
+ react-i18next + `NotificationQueueProvider`), keep `TractorProvider` mounted for
34
+ cohabitation, and add the jsdom test shims (selector guard for DS popups +
35
+ Switch-via-hidden-checkbox). DS and Tractor run side by side until the last Tractor
36
+ import is gone. (This skill `requires` setup so it loads automatically — but do the
37
+ install/provider/jest wiring first.)
38
+ 1. **Inventory** — list the file's `@aircall/tractor` and `@aircall/icons` imports. Each
39
+ maps to a row in the lookup table below, or to `@aircall/ds#aircall-ds/migrate-icons`
40
+ for icons.
41
+ 2. **Migrate** — apply the cross-cutting rules (§1–§3), then load the per-component recipe
42
+ from the `recipe to load` column for each component; use `migrate-icons` for icons.
43
+ 3. **Verify green** — `tsc --noEmit`, the test suite (DS popups/Switch need the setup jsdom
44
+ shims), and biome/lint. Re-screenshot if the screen changed visually.
45
+ 4. **Repeat** per file/screen until no `@aircall/tractor` / `@aircall/icons` imports remain,
46
+ then drop them from `package.json`.
47
+
48
+ ## 1. Imports
49
+
50
+ Always import from the top-level package only:
51
+
52
+ ```tsx
53
+ import { Button, Dialog, Input } from '@aircall/ds';
54
+ ```
55
+
56
+ Subpath imports (`@aircall/ds/components/button`) work only inside the hydra monorepo
57
+ and break in external apps. Use the root form everywhere.
58
+
59
+ ## 2. Drop styled-components
60
+
61
+ Remove `styled()` wrappers, `fromTheme`, `getColor`/`getSpace`/`getRadii`/`getShadow`,
62
+ and `useTheme()`. Replace them with Tailwind classes on the DS component's `className`
63
+ or a plain `<div>`:
64
+
65
+ ```tsx
66
+ // Before
67
+ const Styled = styled(Button)`font-size: ${fromTheme('typography.variants.body.fontSize')};`;
68
+ <Box mx={2} my={4} bg="primary.500" />
69
+
70
+ // After
71
+ <Button className="text-sm" />
72
+ <div className="mx-2 my-4 bg-primary" />
73
+ ```
74
+
75
+ The migration is progressive — Tractor and DS can coexist. Remove `TractorProvider`
76
+ only when the last `@aircall/tractor` import is gone.
77
+
78
+ ## 3. Standard prop renames
79
+
80
+ These apply to every component that had them in Tractor:
81
+
82
+ | Tractor | DS |
83
+ | --- | --- |
84
+ | `isOpen` | `open` |
85
+ | `onClose` | `onOpenChange` (receives `boolean`) |
86
+ | `onChange` (value-based: Select, Tabs, RadioGroup, Slider, ToggleGroup) | `onValueChange` |
87
+ | `onChange` (boolean: Checkbox, Switch) | `onCheckedChange` |
88
+ | `validationStatus="error"` | `aria-invalid={true}` |
89
+ | `variant="critical"` | `variant="destructive"` (Button) |
90
+ | `as="…"` | `render={<Element />}` |
91
+
92
+ > `onValueChange` for Select receives `(value: string | null, event)` — `null` when
93
+ > cleared. Widen your handler to accept `null`.
94
+
95
+ ## 4. The `render` prop (replaces `asChild`)
96
+
97
+ When a Trigger or Close must render as a custom element, pass it to `render`:
98
+
99
+ ```tsx
100
+ <DialogTrigger render={<Button variant="outline" />}>Open</DialogTrigger>
101
+ ```
102
+
103
+ Affected: Trigger/Close on Dialog, Sheet, Popover, Tooltip, DropdownMenu, Collapsible,
104
+ Drawer, plus `Button`, `Badge`, `Item`, `PaginationLink`, and Sidebar subcomponents.
105
+
106
+ Drawer is Base UI (via Coss UI) — same `render` rule applies. Its body wrapper is
107
+ `DrawerPopup` (not `DrawerContent`), and Tractor's `direction` becomes `position`.
108
+
109
+ ## 5. Labels live inside Groups
110
+
111
+ For `DropdownMenu` and `Select`, `Label` must be a child of a `*Group`:
112
+
113
+ ```tsx
114
+ <DropdownMenuContent>
115
+ <DropdownMenuGroup>
116
+ <DropdownMenuLabel>Account</DropdownMenuLabel>
117
+ <DropdownMenuItem>Profile</DropdownMenuItem>
118
+ </DropdownMenuGroup>
119
+ </DropdownMenuContent>
120
+ ```
121
+
122
+ ## 6. Data attributes in Tailwind classes
123
+
124
+ DS uses single-key data attributes for state (not Radix `[state=…]` form):
125
+
126
+ | Use | Not |
127
+ | --- | --- |
128
+ | `data-open:animate-in` | `data-[state=open]:animate-in` |
129
+ | `data-checked:bg-primary` | `data-[state=checked]:bg-primary` |
130
+ | `data-disabled:opacity-50` | `data-[disabled]:opacity-50` |
131
+
132
+ **Orientation is the exception** — it's a value attribute, not a flag. Style with
133
+ `data-[orientation=horizontal]:` / `data-[orientation=vertical]:` (Tabs, Slider,
134
+ Separator).
135
+
136
+ **Collapsible asymmetry:** Root uses `data-open` / `data-closed`; Trigger uses
137
+ `data-panel-open` (no closed counterpart).
138
+
139
+ ## 7. Size baselines
140
+
141
+ > These are recommended starting points. Verify with design — pixel sizes have shifted
142
+ > between Tractor and DS, so a verbatim same-size match isn't always possible.
143
+
144
+ **Button** (DS heights: `sm`=24px, `default`=32px, `lg`=40px):
145
+
146
+ | Tractor | DS | Diff |
147
+ | --- | --- | --- |
148
+ | `xSmall` (28px) | `size="default"` (32px) | +4px |
149
+ | `small` (40px) | `size="lg"` (40px) | exact |
150
+ | `regular` (48px, default) | `size="lg"` (40px) | -8px |
151
+ | `large` (56px) | `className="h-14"` | no built-in size |
152
+
153
+ **Icon Button** (DS sizes: `icon-sm`=24px, `icon`=32px, `icon-lg`=40px):
154
+
155
+ | Tractor IconButton | DS |
156
+ | --- | --- |
157
+ | Default (24px) | `size="icon"` |
158
+ | Smaller than default | `size="icon-sm"` |
159
+ | Larger than default | `size="icon-lg"` |
160
+
161
+ **Select Trigger** has two sizes: `default` (40px) and `sm` (32px). Use `default`
162
+ for any Tractor `regular` / `small`.
163
+
164
+ **Avatar** (DS sizes: `xs`=20px, `sm`=24px, `default`=32px, `lg`=40px, `xl`=48px):
165
+
166
+ | Tractor | DS |
167
+ | --- | --- |
168
+ | `small` (24px) | `sm` |
169
+ | `regular` (32px) | `default` |
170
+ | `large` (48px) | `xl` |
171
+ | `xLarge` (64px) | `xl` (-16px, no 64px size) |
172
+
173
+ **Toggle / ToggleGroupItem**: same heights as Button (`sm`/`default`/`lg`).
174
+
175
+ **No `size` prop:** Input, Textarea, Switch, Checkbox, RadioGroup, Slider. Drop the
176
+ Tractor `size` prop entirely.
177
+
178
+ ## 8. Variant naming
179
+
180
+ Tractor used `variant` + `mode`. DS uses a single `variant`.
181
+
182
+ | Tractor | DS |
183
+ | --- | --- |
184
+ | `critical` | `destructive` (Button) or `error` (Alert) |
185
+ | `informative` | `info` (Alert) |
186
+ | `primary` + `mode="fill"` | `default` (Button) |
187
+ | `primary` + `mode="outline"` | `outline` (Button) |
188
+ | `primary` + `mode="ghost"` | `ghost` (Button) |
189
+ | `primary` + `mode="link"` | `link` (Button) |
190
+
191
+ Each DS component has its own variant set — check the lookup table below.
192
+
193
+ ## 9. Icons
194
+
195
+ `@aircall/icons` → `@aircall/react-icons`. The `<Icon component={X} />` wrapper is
196
+ gone; import the icon directly:
197
+
198
+ ```tsx
199
+ // Before
200
+ import { AddOutlined, Icon } from '@aircall/tractor';
201
+ <Icon component={AddOutlined} mr={2} />
202
+
203
+ // After
204
+ import { AddCircleFill } from '@aircall/react-icons';
205
+ <AddCircleFill className="mr-2 size-4" />
206
+ ```
207
+
208
+ Some names differ significantly (e.g. `InfoCircleFilled` → `InformationCircleFill`,
209
+ `PlayFilled` → `ControlPlayFill`).
210
+
211
+ ## 10. Forms & validation
212
+
213
+ A Tractor `Form`/`FormItem` that collects and submits data migrates to **`@aircall/blocks` `useForm` + the `Form*Field` wrappers** — do NOT keep field values in React `useState`, and do NOT hand-wire the ds `Field`/`Input` primitives. The form owns state, validation, dirty/`canSubmit`/`isSubmitting`, and errors (which drive `SubmitButton`/`CardSaveBar`). Use the bare ds `Field` primitives only for non-form display. See `@aircall/ds#aircall-ds/migrate-tractor/form-and-field` and `@aircall/blocks#aircall-blocks/migrate-dashboard/form-wizard`.
214
+
215
+ Field-level validation state is `aria-invalid` — DS components style themselves from it:
216
+
217
+ ```tsx
218
+ <Input aria-invalid={hasError} />
219
+ ```
220
+
221
+ ## 11. Cleanup items
222
+
223
+ - **Base font size**: Tractor used 14px. DS uses 16px. Remove any `font-size: 14px`
224
+ on `html` or `body` — DS's `globals.css` already sets the correct base.
225
+ - **SVG workarounds**: Remove any `svg { display: block; line-height: 0; }` global CSS.
226
+
227
+ ---
228
+
229
+ ## Component lookup table
230
+
231
+ Grep your code for `@aircall/tractor` imports. For each component, find the row below.
232
+ Then load the listed recipe skill (if `available`) for the detailed swap.
233
+
234
+ | Tractor component | @aircall/ds target | recipe to load | status |
235
+ | --- | --- | --- | --- |
236
+ | `Accordion` (+ `AccordionSection`) | `Accordion` + `AccordionItem` + `AccordionTrigger` + `AccordionContent` | `@aircall/ds#aircall-ds/migrate-tractor/accordion` | available |
237
+ | `ActionMenu` | `DropdownMenu` (compound) | `@aircall/ds#aircall-ds/migrate-tractor/dropdown-menu` | available |
238
+ | `AudioPlayer` | — (no DS equivalent yet) | — | not-yet |
239
+ | `Avatar` (+ `QuickAvatar`) | `Avatar` + `AvatarImage` + `AvatarFallback` | `@aircall/ds#aircall-ds/migrate-tractor/avatar` | available |
240
+ | `Badge` | `AvatarBadge` (status dot on avatar); `Badge` (label/tag style) | `@aircall/ds#aircall-ds/migrate-tractor/badge` | available |
241
+ | `Banner` (+ `BannerHeading`/`BannerIcon`/`BannerSuffix`) | `Alert` (rounded card) / `Banner` (inline full-width) | `@aircall/ds#aircall-ds/migrate-tractor/alert` | available |
242
+ | `BannerButton` | `Button` inside `BannerAction` | `@aircall/ds#aircall-ds/migrate-tractor/alert` | available |
243
+ | `Box` | native `<div>` + Tailwind classes | `@aircall/ds#aircall-ds/migrate-tractor/styling` | available |
244
+ | `Button` | `Button` | `@aircall/ds#aircall-ds/migrate-tractor/button` | available |
245
+ | `Checkbox` | `Checkbox` + `<Label>` | — | not-yet |
246
+ | `ComboBox` | `Combobox` (compound) | `@aircall/ds#aircall-ds/migrate-tractor/combobox` | available |
247
+ | `CounterBadge` | `CounterBadge` | `@aircall/ds#aircall-ds/migrate-tractor/badge` | available |
248
+ | `DatePicker` | `Calendar` + your own `Popover` trigger | — | not-yet |
249
+ | `Divider` | `Separator` | `@aircall/ds#aircall-ds/migrate-tractor/divider` | available |
250
+ | `Drawer` | `Drawer` (compound, `DrawerPopup` body) | `@aircall/ds#aircall-ds/migrate-tractor/sheet-vs-drawer` | available |
251
+ | `Dropdown` | `DropdownMenu` (compound) | `@aircall/ds#aircall-ds/migrate-tractor/dropdown-menu` | available |
252
+ | `Flex` | native `<div className="flex …">` + Tailwind | `@aircall/ds#aircall-ds/migrate-tractor/styling` | available |
253
+ | `FlagIcon` | `CountryFlag` (prop: `countryIsoCode`) | — | not-yet |
254
+ | `Form` | native `<form>` + `Field` per row | `@aircall/ds#aircall-ds/migrate-tractor/form-and-field` | available |
255
+ | `FormItem` | `Field` + `FieldLabel` + `FieldDescription` + `FieldError` | `@aircall/ds#aircall-ds/migrate-tractor/form-and-field` | available |
256
+ | `Gauge` | `Gauge` (8-segment audio meter) | `@aircall/ds#aircall-ds/migrate-tractor/gauge` | available |
257
+ | `Grid` | native `<div className="grid …">` + Tailwind | `@aircall/ds#aircall-ds/migrate-tractor/styling` | available |
258
+ | `Icon` | direct icon import from `@aircall/react-icons` | `@aircall/ds#aircall-ds/migrate-icons` | available |
259
+ | `IconButton` | `Button` with `size` `icon` / `icon-sm` / `icon-lg` | `@aircall/ds#aircall-ds/migrate-tractor/button` | available |
260
+ | `Link` | `Link` | `@aircall/ds#aircall-ds/migrate-tractor/link` | available |
261
+ | `List` (+ `ListItem`) | `ItemGroup` + `Item` + `ItemMedia`/`ItemContent`/`ItemActions` | `@aircall/ds#aircall-ds/migrate-tractor/item` | available |
262
+ | `Menu` (+ `MenuItem`) | standalone list → `ItemGroup` + `Item`; **inside a `Dropdown`/`ActionMenu`** → `DropdownMenuContent` + `DropdownMenuItem` | `@aircall/ds#aircall-ds/migrate-tractor/item` (standalone) or `@aircall/ds#aircall-ds/migrate-tractor/dropdown-menu` (in a Dropdown) | available |
263
+ | `Modal` | `Dialog` (compound) | `@aircall/ds#aircall-ds/migrate-tractor/dialog` | available |
264
+ | `PasswordInput` | `InputGroup` recipe | `@aircall/ds#aircall-ds/migrate-tractor/input` | available |
265
+ | `Popover` | `Popover` + `PopoverTrigger` + `PopoverContent` | `@aircall/ds#aircall-ds/migrate-tractor/popover` | available |
266
+ | `Progress` | `Progress` (compound: `ProgressTrack` + `ProgressIndicator`) | — | not-yet |
267
+ | `QuickAvatar` | `Avatar` (same as Avatar row) | `@aircall/ds#aircall-ds/migrate-tractor/avatar` | available |
268
+ | `Radio` + `RadioGroup` | `RadioGroup` + `RadioGroupItem` | — | not-yet |
269
+ | `SegmentedControl` | `Tabs` (with panel) or `ToggleGroup` (visual only) | `@aircall/ds#aircall-ds/migrate-tractor/tabs` | available |
270
+ | `Select` + `SelectOption` | `Select` (compound: `SelectTrigger` + `SelectValue` + `SelectContent` + `SelectGroup` + `SelectItem`) | `@aircall/ds#aircall-ds/migrate-tractor/select` | available |
271
+ | `SidenavDropdown` | `Sidebar` + `Collapsible` (compose) | — | not-yet |
272
+ | `SidenavItem` | `Sidebar*` family | — | not-yet |
273
+ | `Skeleton` | `Skeleton` — size via Tailwind (`className="h-4 w-32"`) | `@aircall/ds#aircall-ds/migrate-tractor/skeleton` | available |
274
+ | `Slider` | `Slider` (`onValueChange`, value is `number[]`, no `size`) | — | not-yet |
275
+ | `Spacer` | `<div className="flex flex-col gap-*">` or `FieldGroup` | `@aircall/ds#aircall-ds/migrate-tractor/styling` | available |
276
+ | `SpinnerOutlined` | `Spinner` (sizes: `sm`/`default`/`lg`/`xl`, always animated) | — | not-yet |
277
+ | `Tab` (+ `Tab.Item`/`TabList`/`TabPanel`) | `Tabs` + `TabsTrigger` + `TabsList` + `TabsContent` | `@aircall/ds#aircall-ds/migrate-tractor/tabs` | available |
278
+ | `Table` (+ `ActionBar`) | `DataTable` (data-driven) or `Table` primitive (static) | `@aircall/ds#aircall-ds/migrate-tractor/data-table` | available |
279
+ | `Tag` | `Badge` (`color` + `tone` props; `legacyColor` for custom hex) | `@aircall/ds#aircall-ds/migrate-tractor/badge` | available |
280
+ | `Textarea` | `Textarea` (no `size`, use `aria-invalid`) | — | not-yet |
281
+ | `TextFieldInput` | `Input` (no `sizing`, 40px fixed, use `aria-invalid`) | `@aircall/ds#aircall-ds/migrate-tractor/input` | available |
282
+ | `Toggle` | `Switch` (`onCheckedChange`, no `size`) | — | not-yet |
283
+ | `ToggleGroup` / `TabToggle` | `ToggleGroup` (`multiple` boolean; value always `string[]`) | — | not-yet |
284
+ | `Tooltip` | `Tooltip` (compound) + `TooltipProvider` at app root | `@aircall/ds#aircall-ds/migrate-tractor/tooltip` | available |
285
+ | `Tree` | `DataTree` (data-driven) or `Tree` primitive | `@aircall/ds#aircall-ds/migrate-tractor/tree` | available |
286
+ | `TreeSelect` | no turn-key equivalent — `DataTree` inside a `Popover` | `@aircall/ds#aircall-ds/migrate-tractor/tree` | available |
287
+ | `Typography` | native HTML + Tailwind text classes | `@aircall/ds#aircall-ds/migrate-tractor/styling` | available |
288
+ | `useToast` | `toast` from `sonner` + `<Toaster />` at app root | `@aircall/ds#aircall-ds/migrate-tractor/toast` | available |
289
+ | `WhatsAppTemplatePreview` | — (no DS equivalent) | — | not-yet |
290
+
291
+ ---
292
+
293
+ ## Facts easy to get wrong
294
+
295
+ These APIs do not exist in DS. Do not propose them.
296
+
297
+ - `<Input sizing="lg" />` — Input has **no** `sizing`/`size` prop. It is 40px, period.
298
+ - `<Textarea size="…" />` — no size prop.
299
+ - `<Switch size="…" />` — no size prop.
300
+ - `<Checkbox size="…" />` — no size prop.
301
+ - `<RadioGroup size="…" />` — no size prop.
302
+ - `<Slider size="…" />` — no size prop.
303
+ - `<Tabs size="…" />` / `<TabsList variant="…" />` — no such props.
304
+ - `<Badge variant="destructive" | "success" | "warning" | "info" />` — Badge has only
305
+ `default`, `secondary`, `outline`. For semantic colors use `Alert`, `CounterBadge`,
306
+ or `className` with a semantic token.
307
+ - `<Alert variant="destructive" />` — Alert's error variant is `error`, not
308
+ `destructive`. Full set: `default` / `info` / `success` / `warning` / `error`.
309
+ - `<CounterBadge count={…} max={…} />` — no `count`/`max` props. Pass the already-
310
+ capped value as children. Variants: `default` / `secondary` / `ghost`.
311
+ - `<TooltipProvider delayDuration={…} />` — Base UI prop is `delay`, default `0`.
312
+ `delayDuration` is silently ignored.
313
+ - Subpath imports like `@aircall/ds/components/<name>` — never propose. Always use
314
+ `import { X } from '@aircall/ds'`.
@@ -0,0 +1,276 @@
1
+ ---
2
+ name: aircall-ds/migrate-tractor/accordion
3
+ description: >
4
+ Migrate Tractor Accordion and AccordionSection to the @aircall/ds Accordion
5
+ compound (AccordionItem, AccordionTrigger, AccordionContent). Load when a file
6
+ imports Accordion or AccordionSection 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/accordion.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 accordion-specific steps below.
18
+
19
+ ## 1. Component mapping
20
+
21
+ Tractor `Accordion` accepted `AccordionSection` children that bundled header, chevron, and body in a single component. DS splits each section into three named parts:
22
+
23
+ | Tractor | @aircall/ds |
24
+ | --- | --- |
25
+ | `Accordion` | `Accordion` (state owner) |
26
+ | `AccordionSection` | `AccordionItem` + `AccordionTrigger` + `AccordionContent` |
27
+ | _(built into AccordionSection)_ | `AccordionTrigger` (header + built-in chevron) |
28
+ | _(built into AccordionSection)_ | `AccordionContent` (body panel) |
29
+
30
+ ## 2. Verified DS exports (`packages/ds/src/index.ts`)
31
+
32
+ ```
33
+ Accordion, AccordionItem, AccordionTrigger, AccordionContent
34
+ ```
35
+
36
+ All names used in the Before/After section below exist in the published public API.
37
+
38
+ ## 3. Imports
39
+
40
+ ```tsx
41
+ import {
42
+ Accordion,
43
+ AccordionItem,
44
+ AccordionTrigger,
45
+ AccordionContent,
46
+ } from '@aircall/ds';
47
+ ```
48
+
49
+ ## 4. Prop changes
50
+
51
+ | Tractor | DS | Notes |
52
+ | --- | --- | --- |
53
+ | _(implicit — one open at a time)_ | default (no extra prop needed) | Single-open is the DS default |
54
+ | _(allowMultiple or equivalent)_ | `multiple` on `<Accordion>` | Boolean — allows multiple items open simultaneously |
55
+ | _(section key)_ | `value` on `<AccordionItem>` | Required; `number` or `string`; index works for static lists |
56
+ | _(section disabled)_ | `disabled` on `<AccordionItem>` | Disables the trigger and keeps the panel closed |
57
+ | `defaultExpanded` / open sections | `defaultValue` on `<Accordion>` | Always an array, even in single-open mode: `defaultValue={[0]}` |
58
+ | controlled open | `value` + `onValueChange` on `<Accordion>` | `value` is an array; `onValueChange` receives `(number \| string)[]` |
59
+
60
+ ## 5. Before / After
61
+
62
+ ### Static list (most common)
63
+
64
+ ```tsx
65
+ // Before
66
+ import { Accordion, AccordionSection } from '@aircall/tractor';
67
+
68
+ <Accordion>
69
+ <AccordionSection title="General">
70
+ <p>General settings content.</p>
71
+ </AccordionSection>
72
+ <AccordionSection title="Billing">
73
+ <p>Billing details content.</p>
74
+ </AccordionSection>
75
+ </Accordion>
76
+
77
+ // After
78
+ import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@aircall/ds';
79
+
80
+ <Accordion defaultValue={[0]}>
81
+ <AccordionItem value={0}>
82
+ <AccordionTrigger>General</AccordionTrigger>
83
+ <AccordionContent>
84
+ <p>General settings content.</p>
85
+ </AccordionContent>
86
+ </AccordionItem>
87
+ <AccordionItem value={1}>
88
+ <AccordionTrigger>Billing</AccordionTrigger>
89
+ <AccordionContent>
90
+ <p>Billing details content.</p>
91
+ </AccordionContent>
92
+ </AccordionItem>
93
+ </Accordion>
94
+ ```
95
+
96
+ Key changes:
97
+ - Each `AccordionSection` becomes three components: `AccordionItem` (identity + state), `AccordionTrigger` (header label), `AccordionContent` (body)
98
+ - `title` prop on `AccordionSection` becomes the children of `AccordionTrigger`
99
+ - Every `AccordionItem` requires a `value` prop — the index is fine for static lists
100
+ - `defaultValue` is always an array on `Accordion`
101
+
102
+ ### Dynamic list (map over data)
103
+
104
+ ```tsx
105
+ // Before
106
+ import { Accordion, AccordionSection } from '@aircall/tractor';
107
+
108
+ <Accordion>
109
+ {sections.map(section => (
110
+ <AccordionSection key={section.id} title={section.title}>
111
+ {section.body}
112
+ </AccordionSection>
113
+ ))}
114
+ </Accordion>
115
+
116
+ // After
117
+ import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@aircall/ds';
118
+
119
+ <Accordion defaultValue={[0]}>
120
+ {sections.map((section, index) => (
121
+ <AccordionItem key={section.id} value={index}>
122
+ <AccordionTrigger>{section.title}</AccordionTrigger>
123
+ <AccordionContent>{section.body}</AccordionContent>
124
+ </AccordionItem>
125
+ ))}
126
+ </Accordion>
127
+ ```
128
+
129
+ ### Multiple sections open simultaneously
130
+
131
+ ```tsx
132
+ // After — allow more than one panel open at a time
133
+ <Accordion multiple defaultValue={[0, 1]}>
134
+ <AccordionItem value={0}>
135
+ <AccordionTrigger>General</AccordionTrigger>
136
+ <AccordionContent>General settings content.</AccordionContent>
137
+ </AccordionItem>
138
+ <AccordionItem value={1}>
139
+ <AccordionTrigger>Billing</AccordionTrigger>
140
+ <AccordionContent>Billing details content.</AccordionContent>
141
+ </AccordionItem>
142
+ </Accordion>
143
+ ```
144
+
145
+ ### Controlled
146
+
147
+ ```tsx
148
+ // After — controlled open state
149
+ const [openSections, setOpenSections] = React.useState<number[]>([0]);
150
+
151
+ <Accordion value={openSections} onValueChange={setOpenSections}>
152
+ <AccordionItem value={0}>
153
+ <AccordionTrigger>General</AccordionTrigger>
154
+ <AccordionContent>General settings content.</AccordionContent>
155
+ </AccordionItem>
156
+ <AccordionItem value={1}>
157
+ <AccordionTrigger>Billing</AccordionTrigger>
158
+ <AccordionContent>Billing details content.</AccordionContent>
159
+ </AccordionItem>
160
+ </Accordion>
161
+ ```
162
+
163
+ ### Disabled section
164
+
165
+ ```tsx
166
+ // After — disable one item
167
+ <Accordion defaultValue={[0]}>
168
+ <AccordionItem value={0}>
169
+ <AccordionTrigger>General</AccordionTrigger>
170
+ <AccordionContent>General settings content.</AccordionContent>
171
+ </AccordionItem>
172
+ <AccordionItem value={1} disabled>
173
+ <AccordionTrigger>Billing</AccordionTrigger>
174
+ <AccordionContent>Billing details content.</AccordionContent>
175
+ </AccordionItem>
176
+ </Accordion>
177
+ ```
178
+
179
+ ### Width / spacing
180
+
181
+ `Accordion` has no layout props. Apply width via `className`:
182
+
183
+ ```tsx
184
+ <Accordion defaultValue={[0]} className="w-[450px]">
185
+ ...
186
+ </Accordion>
187
+ ```
188
+
189
+ ---
190
+
191
+ ## 6. Common Mistakes
192
+
193
+ ### Mistake 1 — Adding a custom chevron inside `AccordionTrigger`
194
+
195
+ ```tsx
196
+ // Wrong — AccordionTrigger renders its own chevron; adding another doubles it
197
+ <AccordionTrigger>
198
+ General
199
+ <ChevronDownIcon />
200
+ </AccordionTrigger>
201
+
202
+ // Correct — pass only the header label as children
203
+ <AccordionTrigger>General</AccordionTrigger>
204
+ ```
205
+
206
+ `AccordionTrigger` internally renders a `ChevronDownIcon` / `ChevronUpIcon` pair that swaps on open/close state. Adding your own icon duplicates it visually. Pass only the label text (or non-icon content) as children.
207
+
208
+ Source: `packages/ds/src/components/accordion.tsx`
209
+
210
+ ---
211
+
212
+ ### Mistake 2 — Passing `defaultValue` / `value` as a scalar instead of an array
213
+
214
+ ```tsx
215
+ // Wrong — scalar causes a TypeScript error and the panel does not open
216
+ <Accordion defaultValue={0}>
217
+ <AccordionItem value={0}>
218
+ <AccordionTrigger>General</AccordionTrigger>
219
+ <AccordionContent>General settings content.</AccordionContent>
220
+ </AccordionItem>
221
+ </Accordion>
222
+
223
+ // Correct — always wrap in an array, even for a single default item
224
+ <Accordion defaultValue={[0]}>
225
+ <AccordionItem value={0}>
226
+ <AccordionTrigger>General</AccordionTrigger>
227
+ <AccordionContent>General settings content.</AccordionContent>
228
+ </AccordionItem>
229
+ </Accordion>
230
+ ```
231
+
232
+ Both `defaultValue` and `value` on `Accordion` are typed as `(number | string)[]`. Passing a bare scalar is a type error and the Base UI primitive will not open the panel.
233
+
234
+ Source: `packages/ds/src/components/accordion.tsx`
235
+
236
+ ---
237
+
238
+ ### Mistake 3 — Omitting the `value` prop on `AccordionItem`
239
+
240
+ ```tsx
241
+ // Wrong — AccordionItem without value cannot be identified for open/close state
242
+ <AccordionItem>
243
+ <AccordionTrigger>General</AccordionTrigger>
244
+ <AccordionContent>General settings content.</AccordionContent>
245
+ </AccordionItem>
246
+
247
+ // Correct — every AccordionItem needs a unique value
248
+ <AccordionItem value={0}>
249
+ <AccordionTrigger>General</AccordionTrigger>
250
+ <AccordionContent>General settings content.</AccordionContent>
251
+ </AccordionItem>
252
+ ```
253
+
254
+ `value` is required by the Base UI Accordion primitive to track which items are open. Without it the item cannot be matched against `defaultValue` / `value` on the root `Accordion`, so the panel stays permanently closed and toggling has no effect.
255
+
256
+ Source: `packages/ds/src/components/accordion.tsx`
257
+
258
+ ---
259
+
260
+ ### Mistake 4 — Using `type="multiple"` instead of the `multiple` boolean
261
+
262
+ ```tsx
263
+ // Wrong — type prop does not exist on DS Accordion; silently ignored
264
+ <Accordion type="multiple" defaultValue={[0, 1]}>
265
+ ...
266
+ </Accordion>
267
+
268
+ // Correct — DS uses a boolean `multiple` prop (same convention as ToggleGroup)
269
+ <Accordion multiple defaultValue={[0, 1]}>
270
+ ...
271
+ </Accordion>
272
+ ```
273
+
274
+ Tractor (and some other libraries) use `type="single" | "multiple"` to control this behavior. DS uses the simpler boolean `multiple` prop. Passing `type="multiple"` is silently ignored — the accordion reverts to single-open mode with no TypeScript error.
275
+
276
+ Source: `packages/ds/src/components/accordion.tsx`