@aircall/ds 0.13.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 +94 -33
  4. package/dist/index.js +292 -42
  5. package/package.json +16 -3
  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,206 @@
1
+ ---
2
+ name: aircall-ds/migrate-tractor/dialog
3
+ description: >
4
+ Migrate Tractor Modal (ModalDialog, ModalHeader, ModalTitle, ModalBody,
5
+ ModalFooter) to the @aircall/ds Dialog compound. Load when a file imports Modal*
6
+ from @aircall/tractor.
7
+ type: sub-skill
8
+ library: aircall-ds
9
+ library_version: "0.13.0"
10
+ requires:
11
+ - aircall-ds/setup
12
+ - aircall-ds/migrate-tractor
13
+ sources:
14
+ - "aircall/hydra:docs/migration-guides/tractor-to-ds/recipes/dialog.md"
15
+ ---
16
+
17
+ This skill builds on aircall-ds/migrate-tractor. Apply all cross-cutting rules from that skill (prop renames, `render` prop, data attributes) before the dialog-specific steps below.
18
+
19
+ ## 1. Component mapping
20
+
21
+ | Tractor | @aircall/ds |
22
+ | --- | --- |
23
+ | `Modal` (state owner + panel) | `Dialog` (state owner) + `DialogContent` (panel) |
24
+ | `ModalHeader` | `DialogHeader` |
25
+ | `ModalTitle` | `DialogTitle` |
26
+ | `ModalBody` | — (plain children inside `DialogContent`, no wrapper) |
27
+ | `ModalFooter` | `DialogFooter` |
28
+ | — | `DialogTrigger` (uncontrolled open trigger) |
29
+ | — | `DialogClose` (any button that closes the dialog) |
30
+ | — | `DialogDescription` (optional accessible subtitle) |
31
+
32
+ ## 2. Imports
33
+
34
+ ```tsx
35
+ import {
36
+ Dialog,
37
+ DialogClose,
38
+ DialogContent,
39
+ DialogDescription,
40
+ DialogFooter,
41
+ DialogHeader,
42
+ DialogTitle,
43
+ DialogTrigger,
44
+ Button
45
+ } from '@aircall/ds';
46
+ ```
47
+
48
+ ## 3. Before / After
49
+
50
+ ### Controlled (most common — replaces `<Modal isOpen={…} onClose={…}>`)
51
+
52
+ ```tsx
53
+ // Before
54
+ import { Modal, ModalHeader, ModalTitle, ModalBody, ModalFooter, Button } from '@aircall/tractor';
55
+
56
+ <Modal isOpen={isOpen} onClose={() => setIsOpen(false)} size="regular">
57
+ <ModalHeader>
58
+ <ModalTitle>Confirm delete</ModalTitle>
59
+ </ModalHeader>
60
+ <ModalBody>
61
+ <p>This will permanently remove the contact. Are you sure?</p>
62
+ </ModalBody>
63
+ <ModalFooter>
64
+ <Button variant="outline" onClick={() => setIsOpen(false)}>Cancel</Button>
65
+ <Button variant="critical" onClick={onConfirm}>Delete</Button>
66
+ </ModalFooter>
67
+ </Modal>
68
+
69
+ // After
70
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose, Button } from '@aircall/ds';
71
+
72
+ <Dialog open={isOpen} onOpenChange={setIsOpen}>
73
+ <DialogContent>
74
+ <DialogHeader>
75
+ <DialogTitle>Confirm delete</DialogTitle>
76
+ <DialogDescription>
77
+ This will permanently remove the contact.
78
+ </DialogDescription>
79
+ </DialogHeader>
80
+
81
+ <p className="text-sm">Are you sure?</p>
82
+
83
+ <DialogFooter>
84
+ <DialogClose render={<Button variant="outline" size="lg" />}>
85
+ Cancel
86
+ </DialogClose>
87
+ <Button variant="destructive" size="lg" onClick={onConfirm}>
88
+ Delete
89
+ </Button>
90
+ </DialogFooter>
91
+ </DialogContent>
92
+ </Dialog>
93
+ ```
94
+
95
+ Key changes:
96
+ - `isOpen` → `open`; `onClose(reason)` → `onOpenChange(boolean)`
97
+ - `ModalBody` is removed — body content is plain children inside `DialogContent`
98
+ - `DialogClose` wraps any button that should close the dialog; pass the DS `Button` via `render` and put the label as children of `DialogClose`, not of the inner Button
99
+ - `variant="critical"` → `variant="destructive"` (Button)
100
+
101
+ ### Uncontrolled with a trigger
102
+
103
+ ```tsx
104
+ // After
105
+ <Dialog>
106
+ <DialogTrigger render={<Button size="lg" />}>Open settings</DialogTrigger>
107
+ <DialogContent>
108
+ <DialogHeader>
109
+ <DialogTitle>Settings</DialogTitle>
110
+ </DialogHeader>
111
+ {/* body content */}
112
+ <DialogFooter>
113
+ <DialogClose render={<Button variant="outline" size="lg" />}>Close</DialogClose>
114
+ </DialogFooter>
115
+ </DialogContent>
116
+ </Dialog>
117
+ ```
118
+
119
+ ### Size
120
+
121
+ `Dialog` has no built-in `size` prop. For the Tractor `size="large"` case, set width on `DialogContent`:
122
+
123
+ ```tsx
124
+ // Tractor: size="large"
125
+ <DialogContent className="max-w-2xl">…</DialogContent>
126
+ ```
127
+
128
+ ### Hiding the default close button
129
+
130
+ `DialogContent` renders a default "×" close button in the top-right. Hide it when you want to force the user to use a footer action:
131
+
132
+ ```tsx
133
+ <DialogContent showCloseButton={false}>…</DialogContent>
134
+ ```
135
+
136
+ ---
137
+
138
+ ## 4. Common mistakes
139
+
140
+ ### Mistake 1 — Passing `onClose` instead of `onOpenChange`
141
+
142
+ ```tsx
143
+ // ❌ Wrong — onClose is a Tractor prop; DS ignores it silently
144
+ <Dialog open={isOpen} onClose={() => setIsOpen(false)}>…</Dialog>
145
+
146
+ // ✅ Correct — onOpenChange receives the new boolean state
147
+ <Dialog open={isOpen} onOpenChange={setIsOpen}>…</Dialog>
148
+ ```
149
+
150
+ `onOpenChange` fires with `false` when the dialog closes (backdrop click, Escape, or `DialogClose`). Widen your handler if you were using the Tractor `reason` argument — DS does not pass a reason.
151
+
152
+ Source: `packages/ds/src/components/dialog.tsx`
153
+
154
+ ### Mistake 2 — Keeping `ModalBody` or wrapping body content in `DialogBody`
155
+
156
+ ```tsx
157
+ // ❌ Wrong — neither ModalBody nor DialogBody exists in @aircall/ds
158
+ <DialogContent>
159
+ <DialogHeader>…</DialogHeader>
160
+ <DialogBody>
161
+ <p>Content here</p>
162
+ </DialogBody>
163
+ <DialogFooter>…</DialogFooter>
164
+ </DialogContent>
165
+
166
+ // ✅ Correct — body content is plain children between DialogHeader and DialogFooter
167
+ <DialogContent>
168
+ <DialogHeader>…</DialogHeader>
169
+ <p className="text-sm">Content here</p>
170
+ <DialogFooter>…</DialogFooter>
171
+ </DialogContent>
172
+ ```
173
+
174
+ DS's `Dialog` is a compound with no `DialogBody` export. The body is just children of `DialogContent`. Remove `ModalBody` and render content directly.
175
+
176
+ Source: `packages/ds/src/components/dialog.tsx`
177
+
178
+ ### Mistake 3 — Putting the label inside the `render` Button instead of `DialogClose`
179
+
180
+ ```tsx
181
+ // ❌ Wrong — the label ends up inside the Button's children, not DialogClose's
182
+ <DialogClose render={<Button variant="outline" size="lg">Cancel</Button>} />
183
+
184
+ // ✅ Correct — label is a child of DialogClose; DialogClose clones it into the rendered Button
185
+ <DialogClose render={<Button variant="outline" size="lg" />}>
186
+ Cancel
187
+ </DialogClose>
188
+ ```
189
+
190
+ `DialogClose` uses the Base UI `render` prop pattern: it renders the element you pass as `render`, merging its own click handler. The label must be the child of `DialogClose`, not of the inner Button.
191
+
192
+ Source: `packages/ds/src/components/dialog.tsx`
193
+
194
+ ### Mistake 4 — Using `variant="critical"` on the Button
195
+
196
+ ```tsx
197
+ // ❌ Wrong — critical is a Tractor variant; DS Button doesn't have it
198
+ <Button variant="critical" onClick={onConfirm}>Delete</Button>
199
+
200
+ // ✅ Correct — DS Button uses destructive
201
+ <Button variant="destructive" size="lg" onClick={onConfirm}>Delete</Button>
202
+ ```
203
+
204
+ DS renamed `critical` to `destructive` for Button (and other components). The Tractor prop is silently dropped; no TypeScript error is shown because the variant may fall through to the default style.
205
+
206
+ Source: `packages/ds/src/components/dialog.tsx`
@@ -0,0 +1,226 @@
1
+ ---
2
+ name: aircall-ds/migrate-tractor/divider
3
+ description: >
4
+ Migrate Tractor Divider to the @aircall/ds Separator. Load when a file imports
5
+ Divider from @aircall/tractor. Maps orientation (vertical|horizontal), drops the
6
+ size and bg props, and replaces Box-level color overrides with Tailwind className.
7
+ type: sub-skill
8
+ library: aircall-ds
9
+ library_version: "0.13.0"
10
+ requires:
11
+ - aircall-ds/setup
12
+ - aircall-ds/migrate-tractor
13
+ sources:
14
+ - "aircall/hydra:packages/ds/src/components/separator.tsx"
15
+ ---
16
+
17
+ This skill builds on aircall-ds/migrate-tractor.
18
+
19
+ ## 1. Component mapping
20
+
21
+ | Tractor part | @aircall/ds part | Notes |
22
+ | --- | --- | --- |
23
+ | `<Divider>` | `<Separator>` | Name change; orientation prop is preserved |
24
+ | `size` prop | _(removed)_ | DS Separator is always 1 px; no thickness control |
25
+ | `bg` prop | `className` | Pass a Tailwind background utility to override the border color |
26
+ | Extends `BoxProps` | Extends `SeparatorPrimitive.Props` | DS root is a Base UI element, not a styled Box |
27
+
28
+ ## 2. Verified DS export (`packages/ds/src/index.ts`)
29
+
30
+ ```
31
+ Separator
32
+ ```
33
+
34
+ ## 3. Imports
35
+
36
+ **Before (Tractor):**
37
+ ```tsx
38
+ import { Divider } from '@aircall/tractor';
39
+ ```
40
+
41
+ **After (DS):**
42
+ ```tsx
43
+ import { Separator } from '@aircall/ds';
44
+ ```
45
+
46
+ ## 4. Prop changes
47
+
48
+ | Tractor prop | DS prop | Action |
49
+ | --- | --- | --- |
50
+ | `orientation?: 'vertical' \| 'horizontal'` | `orientation?: 'vertical' \| 'horizontal'` | Same values — pass through as-is |
51
+ | `size?: 'xSmall' \| 'small'` | _(removed)_ | Delete the prop; DS Separator is always 1 px thick |
52
+ | `bg?: string` | `className` | Replace with a Tailwind `bg-*` utility on `className` |
53
+
54
+ The DS `Separator` accepts all standard HTML attributes (`id`, `aria-*`, `data-*`, `style`, `className`) plus `orientation`. It does not accept `children`.
55
+
56
+ ### Default orientation difference
57
+
58
+ Tractor's `Divider` defaulted to `orientation="vertical"`. The DS `Separator` defaults to `orientation="horizontal"`. Always pass `orientation` explicitly on both sides of the migration to avoid a silent layout change.
59
+
60
+ ## 5. Sizing behavior
61
+
62
+ Tractor's `Divider` used a `size` prop to control thickness (`xSmall` = 1 px, `small` = 2 px). The DS `Separator` is always 1 px thick via Tailwind classes applied internally (`data-[orientation=horizontal]:h-px` / `data-[orientation=vertical]:w-px`). There is no thickness prop — if you need a 2 px separator, add `className="data-[orientation=horizontal]:h-0.5 data-[orientation=vertical]:w-0.5"`.
63
+
64
+ ## 6. Color behavior
65
+
66
+ Tractor's `Divider` accepted a Tractor design-token string via `bg` (e.g. `"graphic-default"`, `"neutral-700"`). The DS `Separator` uses `bg-border` by default (mapped to the DS border token). To override, pass a Tailwind utility class via `className`:
67
+
68
+ ```tsx
69
+ // Tractor custom color
70
+ <Divider orientation="vertical" bg="neutral-700" />
71
+
72
+ // DS equivalent
73
+ <Separator orientation="vertical" className="bg-neutral-700" />
74
+ ```
75
+
76
+ ## 7. Before / After examples
77
+
78
+ ### 7a. Horizontal separator between sections (default use case)
79
+
80
+ **Before (Tractor):**
81
+ ```tsx
82
+ import { Divider } from '@aircall/tractor';
83
+
84
+ export function SectionBreak() {
85
+ return <Divider orientation="horizontal" />;
86
+ }
87
+ ```
88
+
89
+ **After (DS):**
90
+ ```tsx
91
+ import { Separator } from '@aircall/ds';
92
+
93
+ export function SectionBreak() {
94
+ return <Separator orientation="horizontal" />;
95
+ }
96
+ ```
97
+
98
+ ### 7b. Vertical separator between inline items
99
+
100
+ **Before (Tractor):**
101
+ ```tsx
102
+ import { Divider } from '@aircall/tractor';
103
+
104
+ export function InlineActions() {
105
+ return (
106
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
107
+ <span>Calls</span>
108
+ <Divider orientation="vertical" />
109
+ <span>Contacts</span>
110
+ </div>
111
+ );
112
+ }
113
+ ```
114
+
115
+ **After (DS):**
116
+ ```tsx
117
+ import { Separator } from '@aircall/ds';
118
+
119
+ export function InlineActions() {
120
+ return (
121
+ <div className="flex items-center gap-2">
122
+ <span>Calls</span>
123
+ <Separator orientation="vertical" />
124
+ <span>Contacts</span>
125
+ </div>
126
+ );
127
+ }
128
+ ```
129
+
130
+ ### 7c. Thin divider with `size="xSmall"` dropped
131
+
132
+ **Before (Tractor):**
133
+ ```tsx
134
+ import { Divider } from '@aircall/tractor';
135
+
136
+ export function MenuDivider() {
137
+ return <Divider orientation="horizontal" size="xSmall" />;
138
+ }
139
+ ```
140
+
141
+ **After (DS):**
142
+ ```tsx
143
+ import { Separator } from '@aircall/ds';
144
+
145
+ export function MenuDivider() {
146
+ return <Separator orientation="horizontal" />;
147
+ }
148
+ ```
149
+
150
+ > Both `xSmall` (1 px) and DS default (1 px) are identical. Drop `size` without any replacement.
151
+
152
+ ### 7d. Custom color via `bg` → `className`
153
+
154
+ **Before (Tractor):**
155
+ ```tsx
156
+ import { Divider } from '@aircall/tractor';
157
+
158
+ export function BoldDivider() {
159
+ return <Divider orientation="horizontal" size="small" bg="neutral-700" />;
160
+ }
161
+ ```
162
+
163
+ **After (DS):**
164
+ ```tsx
165
+ import { Separator } from '@aircall/ds';
166
+
167
+ export function BoldDivider() {
168
+ return (
169
+ <Separator
170
+ orientation="horizontal"
171
+ className="bg-neutral-700 data-[orientation=horizontal]:h-0.5"
172
+ />
173
+ );
174
+ }
175
+ ```
176
+
177
+ > `size="small"` (2 px) has no direct DS equivalent — use `data-[orientation=horizontal]:h-0.5` to restore the 2 px thickness.
178
+
179
+ ## 8. Common mistakes
180
+
181
+ ### Mistake 1: Relying on the default orientation matching Tractor's default
182
+
183
+ ```tsx
184
+ // WRONG — Tractor default is "vertical"; DS default is "horizontal"; omitting orientation
185
+ // silently changes the rendered layout
186
+ <Separator />
187
+
188
+ // CORRECT — always pass orientation explicitly
189
+ <Separator orientation="vertical" />
190
+ ```
191
+
192
+ Tractor's `Divider` defaulted to `orientation="vertical"`. The DS `Separator` defaults to `orientation="horizontal"` (matching the Base UI primitive default). Omitting the prop after renaming the component causes a horizontal line to appear where a vertical one was expected — no warning, no error.
193
+
194
+ Source: `packages/ds/src/components/separator.tsx` — `orientation = 'horizontal'` is the destructured default.
195
+
196
+ ---
197
+
198
+ ### Mistake 2: Passing the `size` prop expecting thickness control
199
+
200
+ ```tsx
201
+ // WRONG — DS Separator has no `size` prop; it spreads onto the DOM element as a string attribute
202
+ <Separator orientation="horizontal" size="small" />
203
+
204
+ // CORRECT — drop `size`; for 2 px thickness add a Tailwind class
205
+ <Separator orientation="horizontal" className="data-[orientation=horizontal]:h-0.5" />
206
+ ```
207
+
208
+ `SeparatorProps` extends `SeparatorPrimitive.Props`, which has no `size` field. Because `Separator` spreads unknown props onto the Base UI primitive, `size="small"` becomes a non-standard DOM attribute, triggering a React warning and failing strict DOM validation.
209
+
210
+ Source: `packages/ds/src/components/separator.tsx` — `SeparatorProps` extends `SeparatorPrimitive.Props` with no additional fields.
211
+
212
+ ---
213
+
214
+ ### Mistake 3: Passing a Tractor design-token string to `bg` or `color`
215
+
216
+ ```tsx
217
+ // WRONG — DS Separator has no `bg` prop; passing it spreads a raw string onto the DOM
218
+ <Separator orientation="vertical" bg="graphic-default" />
219
+
220
+ // CORRECT — use a Tailwind bg-* utility via className
221
+ <Separator orientation="vertical" className="bg-border" />
222
+ ```
223
+
224
+ Tractor's `Divider` was built on `Box`, which accepted Tractor design-token strings for `bg`. `Separator` is a Base UI element with no styled-system layer — there is no `bg` prop. Passing it results in `bg="graphic-default"` becoming an invalid HTML attribute on the rendered DOM element.
225
+
226
+ Source: `packages/ds/src/components/separator.tsx` — root renders `<SeparatorPrimitive>` directly; `bg` is not in `SeparatorPrimitive.Props`.
@@ -0,0 +1,266 @@
1
+ ---
2
+ name: aircall-ds/migrate-tractor/dropdown-menu
3
+ description: >
4
+ Migrate Tractor Dropdown and ActionMenu to the @aircall/ds DropdownMenu compound.
5
+ Load when a file imports Dropdown, ActionMenu, or ActionMenuItem from @aircall/tractor.
6
+ Covers trigger render prop, destructive item variant, icon placement, and the Group-wraps-Label rule.
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/dropdown-menu.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 dropdown-menu-specific steps below.
18
+
19
+ ## 1. Component mapping
20
+
21
+ Tractor `Dropdown` (labelled button trigger) and `ActionMenu` (icon-only "•••" trigger) both map to the same DS `DropdownMenu` compound. The visual difference is achieved through the `Button` variant/size passed to `DropdownMenuTrigger`'s `render` prop.
22
+
23
+ | Tractor part | DS compound part | Role |
24
+ | --- | --- | --- |
25
+ | `<Dropdown>` / `<ActionMenu>` | `<DropdownMenu>` | State owner (open, modal) |
26
+ | _(trigger button)_ | `<DropdownMenuTrigger>` | Clickable trigger — accepts any button via `render` |
27
+ | `<DropdownItem>` / `<ActionMenuItem>` | `<DropdownMenuItem>` | Individual action item |
28
+ | _(no equivalent)_ | `<DropdownMenuContent>` | Floating panel container |
29
+ | _(no equivalent)_ | `<DropdownMenuGroup>` | Required wrapper around a labelled section |
30
+ | _(no equivalent)_ | `<DropdownMenuLabel>` | Section heading — must be inside `DropdownMenuGroup` |
31
+ | _(no equivalent)_ | `<DropdownMenuSeparator>` | Horizontal rule between sections |
32
+ | _(no equivalent)_ | `<DropdownMenuAddon>` | Trailing right-aligned slot inside an item |
33
+
34
+ ## 2. Verified DS exports (`packages/ds/src/index.ts`)
35
+
36
+ ```
37
+ DropdownMenu, DropdownMenuContent, DropdownMenuGroup,
38
+ DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator,
39
+ DropdownMenuTrigger, DropdownMenuAddon,
40
+ DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent,
41
+ DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem,
42
+ DropdownMenuPortal, DropdownMenuShortcut,
43
+ Button
44
+ ```
45
+
46
+ ## 3. Imports
47
+
48
+ ```tsx
49
+ import {
50
+ DropdownMenu,
51
+ DropdownMenuContent,
52
+ DropdownMenuGroup,
53
+ DropdownMenuItem,
54
+ DropdownMenuLabel,
55
+ DropdownMenuSeparator,
56
+ DropdownMenuTrigger,
57
+ Button,
58
+ } from '@aircall/ds';
59
+ ```
60
+
61
+ For ActionMenu (icon trigger + icon items), also import from `@aircall/react-icons`:
62
+
63
+ ```tsx
64
+ import { MoreVerticalFill, EditPencilFill, DeleteTrashFill } from '@aircall/react-icons';
65
+ ```
66
+
67
+ ## 4. Prop and pattern changes
68
+
69
+ | Tractor | DS | Notes |
70
+ | --- | --- | --- |
71
+ | `<Dropdown label="Options">` | `<DropdownMenuTrigger render={<Button variant="outline" size="lg" />}>Options</DropdownMenuTrigger>` | Trigger button is composed via `render` prop |
72
+ | `<ActionMenu>` | `<DropdownMenuTrigger render={<Button variant="ghost" size="icon" />}><MoreVerticalFill /></DropdownMenuTrigger>` | Icon-only trigger uses `size="icon"` |
73
+ | `<ActionMenuItem variant="critical">` | `<DropdownMenuItem variant="destructive">` | Use `variant` prop, not `className` |
74
+ | `icon={EditOutlined}` on `ActionMenuItem` | Icon as first child of `DropdownMenuItem` | Render icon directly inside the item |
75
+ | `<DropdownSection label="X">` | `<DropdownMenuGroup><DropdownMenuLabel>X</DropdownMenuLabel>…</DropdownMenuGroup>` | Label must be inside a `DropdownMenuGroup` |
76
+
77
+ ## 5. Before / After — plain Dropdown
78
+
79
+ ### Before (Tractor)
80
+
81
+ ```tsx
82
+ import { Dropdown, DropdownItem } from '@aircall/tractor';
83
+
84
+ <Dropdown label="Options">
85
+ <DropdownItem onClick={handleEdit}>Edit</DropdownItem>
86
+ <DropdownItem onClick={handleDelete}>Delete</DropdownItem>
87
+ </Dropdown>
88
+ ```
89
+
90
+ ### After (DS)
91
+
92
+ ```tsx
93
+ import {
94
+ DropdownMenu,
95
+ DropdownMenuContent,
96
+ DropdownMenuItem,
97
+ DropdownMenuTrigger,
98
+ Button,
99
+ } from '@aircall/ds';
100
+
101
+ <DropdownMenu>
102
+ <DropdownMenuTrigger render={<Button variant="outline" size="lg" />}>
103
+ Options
104
+ </DropdownMenuTrigger>
105
+ <DropdownMenuContent>
106
+ <DropdownMenuItem onClick={handleEdit}>Edit</DropdownMenuItem>
107
+ <DropdownMenuItem onClick={handleDelete}>Delete</DropdownMenuItem>
108
+ </DropdownMenuContent>
109
+ </DropdownMenu>
110
+ ```
111
+
112
+ ## 6. Before / After — ActionMenu (icon trigger + icon items)
113
+
114
+ ### Before (Tractor)
115
+
116
+ ```tsx
117
+ import { ActionMenu, ActionMenuItem } from '@aircall/tractor';
118
+
119
+ <ActionMenu>
120
+ <ActionMenuItem icon={EditOutlined} onClick={handleEdit}>Edit</ActionMenuItem>
121
+ <ActionMenuItem icon={DeleteOutlined} variant="critical" onClick={handleDelete}>Delete</ActionMenuItem>
122
+ </ActionMenu>
123
+ ```
124
+
125
+ ### After (DS)
126
+
127
+ ```tsx
128
+ import {
129
+ DropdownMenu,
130
+ DropdownMenuContent,
131
+ DropdownMenuItem,
132
+ DropdownMenuTrigger,
133
+ Button,
134
+ } from '@aircall/ds';
135
+ import { MoreVerticalFill, EditPencilFill, DeleteTrashFill } from '@aircall/react-icons';
136
+
137
+ <DropdownMenu>
138
+ <DropdownMenuTrigger render={<Button variant="ghost" size="icon" />}>
139
+ <MoreVerticalFill className="size-4" />
140
+ </DropdownMenuTrigger>
141
+ <DropdownMenuContent>
142
+ <DropdownMenuItem onClick={handleEdit}>
143
+ <EditPencilFill className="mr-2 size-4" />
144
+ Edit
145
+ </DropdownMenuItem>
146
+ <DropdownMenuItem variant="destructive" onClick={handleDelete}>
147
+ <DeleteTrashFill className="mr-2 size-4" />
148
+ Delete
149
+ </DropdownMenuItem>
150
+ </DropdownMenuContent>
151
+ </DropdownMenu>
152
+ ```
153
+
154
+ ## 7. Sections with labels
155
+
156
+ `DropdownMenuLabel` must be placed inside a `DropdownMenuGroup` alongside its items. A label as a direct child of `DropdownMenuContent` is not accessibility-correct in Base UI.
157
+
158
+ ```tsx
159
+ import {
160
+ DropdownMenu,
161
+ DropdownMenuContent,
162
+ DropdownMenuGroup,
163
+ DropdownMenuItem,
164
+ DropdownMenuLabel,
165
+ DropdownMenuSeparator,
166
+ DropdownMenuTrigger,
167
+ Button,
168
+ } from '@aircall/ds';
169
+
170
+ <DropdownMenu>
171
+ <DropdownMenuTrigger render={<Button variant="outline" size="lg" />}>
172
+ Account
173
+ </DropdownMenuTrigger>
174
+ <DropdownMenuContent>
175
+ <DropdownMenuGroup>
176
+ <DropdownMenuLabel>Account</DropdownMenuLabel>
177
+ <DropdownMenuItem>Profile</DropdownMenuItem>
178
+ <DropdownMenuItem>Settings</DropdownMenuItem>
179
+ </DropdownMenuGroup>
180
+ <DropdownMenuSeparator />
181
+ <DropdownMenuItem>Logout</DropdownMenuItem>
182
+ </DropdownMenuContent>
183
+ </DropdownMenu>
184
+ ```
185
+
186
+ ## 8. Common Mistakes
187
+
188
+ ### Mistake 1 — Using `className="text-destructive"` instead of `variant="destructive"`
189
+
190
+ ```tsx
191
+ // Wrong
192
+ <DropdownMenuItem className="text-destructive" onClick={handleDelete}>
193
+ Delete
194
+ </DropdownMenuItem>
195
+
196
+ // Correct
197
+ <DropdownMenuItem variant="destructive" onClick={handleDelete}>
198
+ Delete
199
+ </DropdownMenuItem>
200
+ ```
201
+
202
+ `DropdownMenuItem` has a first-class `variant` prop (`'default' | 'destructive'`). Using `variant="destructive"` applies the correct destructive text colour, hover background, and dark-mode overrides via `data-[variant=destructive]` selectors. A bare `className="text-destructive"` only sets the text colour and loses the focus-state and dark-mode styling baked into the variant.
203
+ Source: `packages/ds/src/components/dropdown-menu.tsx`
204
+
205
+ ---
206
+
207
+ ### Mistake 2 — Placing `DropdownMenuLabel` as a direct child of `DropdownMenuContent`
208
+
209
+ ```tsx
210
+ // Wrong
211
+ <DropdownMenuContent>
212
+ <DropdownMenuLabel>Account</DropdownMenuLabel>
213
+ <DropdownMenuItem>Profile</DropdownMenuItem>
214
+ </DropdownMenuContent>
215
+
216
+ // Correct
217
+ <DropdownMenuContent>
218
+ <DropdownMenuGroup>
219
+ <DropdownMenuLabel>Account</DropdownMenuLabel>
220
+ <DropdownMenuItem>Profile</DropdownMenuItem>
221
+ </DropdownMenuGroup>
222
+ </DropdownMenuContent>
223
+ ```
224
+
225
+ `DropdownMenuLabel` renders a Base UI `MenuPrimitive.GroupLabel`, which must be a sibling of its items inside a `MenuPrimitive.Group`. Placing it directly in `DropdownMenuContent` breaks the ARIA group association that screen readers depend on.
226
+ Source: `packages/ds/src/components/dropdown-menu.tsx`
227
+
228
+ ---
229
+
230
+ ### Mistake 3 — Omitting the `render` prop on `DropdownMenuTrigger`
231
+
232
+ ```tsx
233
+ // Wrong
234
+ <DropdownMenuTrigger>
235
+ Options
236
+ </DropdownMenuTrigger>
237
+
238
+ // Correct
239
+ <DropdownMenuTrigger render={<Button variant="outline" size="lg" />}>
240
+ Options
241
+ </DropdownMenuTrigger>
242
+ ```
243
+
244
+ Without a `render` prop, `DropdownMenuTrigger` renders a plain unstyled `<button>`. In the DS, trigger appearance is composed by passing a `Button` via the Base UI `render` prop — the button's variant and size control the visual style. This follows the DS-wide `render` prop pattern that replaces the old `asChild` prop.
245
+ Source: `packages/ds/src/components/dropdown-menu.tsx`
246
+
247
+ ---
248
+
249
+ ### Mistake 4 — Passing icon as a prop instead of rendering it as a child
250
+
251
+ ```tsx
252
+ // Wrong
253
+ import { ActionMenu, ActionMenuItem } from '@aircall/tractor';
254
+ <ActionMenuItem icon={EditOutlined}>Edit</ActionMenuItem>
255
+
256
+ // Correct
257
+ import { DropdownMenuItem } from '@aircall/ds';
258
+ import { EditPencilFill } from '@aircall/react-icons';
259
+ <DropdownMenuItem>
260
+ <EditPencilFill className="mr-2 size-4" />
261
+ Edit
262
+ </DropdownMenuItem>
263
+ ```
264
+
265
+ Tractor `ActionMenuItem` accepted an `icon` prop. DS `DropdownMenuItem` has no `icon` prop — the icon is placed as the first child. The `[&_svg:not([class*='size-'])]:size-4` selector on the item auto-sizes icon children that lack an explicit size class.
266
+ Source: `packages/ds/src/components/dropdown-menu.tsx`