@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,298 @@
1
+ ---
2
+ name: aircall-ds/migrate-tractor/item
3
+ description: >
4
+ Migrate Tractor Menu/MenuItem and List/ListItem to the @aircall/ds Item family
5
+ (ItemGroup, Item, ItemMedia, ItemContent, ItemTitle, ItemDescription, ItemActions,
6
+ ItemHeader, ItemFooter, ItemSeparator). Load when a file imports Menu, MenuItem,
7
+ List, or ListItem from @aircall/tractor.
8
+ type: sub-skill
9
+ library: aircall-ds
10
+ library_version: "0.13.0"
11
+ requires:
12
+ - aircall-ds/setup
13
+ - aircall-ds/migrate-tractor
14
+ sources:
15
+ - "aircall/hydra:docs/migration-guides/tractor-to-ds/recipes/item.md"
16
+ ---
17
+
18
+ This skill builds on aircall-ds/migrate-tractor.
19
+
20
+ ## 1. Component mapping
21
+
22
+ Tractor's `Menu`/`MenuItem` and `List`/`ListItem` are two separate component families. DS collapses them into a single `Item` family. Use `DropdownMenu` instead if the list lives inside a popover/context-menu trigger.
23
+
24
+ | Tractor | @aircall/ds | Notes |
25
+ | --- | --- | --- |
26
+ | `<Menu>` / `<List>` | `<ItemGroup>` | Wrapper; manages layout and stacking |
27
+ | `<MenuItem>` / `<ListItem>` | `<Item>` | Single row; supports `variant` and `size` |
28
+ | Left icon / avatar slot | `<ItemMedia>` | `variant="icon"` for icons, `variant="image"` for avatars/photos |
29
+ | Primary text | `<ItemTitle>` | Inside `<ItemContent>` |
30
+ | Secondary text / subtext | `<ItemDescription>` | Inside `<ItemContent>`, renders as `<p>` |
31
+ | Text content wrapper | `<ItemContent>` | Grows to fill; direct parent of Title + Description |
32
+ | Right slot (buttons, badges) | `<ItemActions>` | Flex row, shrinks to content |
33
+ | — | `<ItemHeader>` | Full-width row spanning the top of the item (optional) |
34
+ | — | `<ItemFooter>` | Full-width row spanning the bottom of the item (optional) |
35
+ | — | `<ItemSeparator>` | Horizontal rule between groups |
36
+
37
+ ## 2. Imports
38
+
39
+ ```tsx
40
+ import {
41
+ Item,
42
+ ItemActions,
43
+ ItemContent,
44
+ ItemDescription,
45
+ ItemFooter,
46
+ ItemGroup,
47
+ ItemHeader,
48
+ ItemMedia,
49
+ ItemSeparator,
50
+ ItemTitle
51
+ } from '@aircall/ds';
52
+ ```
53
+
54
+ For icons, import from `@aircall/react-icons` — never from `lucide-react` directly:
55
+
56
+ ```tsx
57
+ import { PhoneCallFill } from '@aircall/react-icons';
58
+ ```
59
+
60
+ ## 3. Key props
61
+
62
+ ### ItemGroup
63
+
64
+ | Prop | Type | Default | Effect |
65
+ | --- | --- | --- | --- |
66
+ | `stackedItems` | `boolean` | `false` | Renders the group as a bordered card with separators — classic settings panel |
67
+
68
+ ### Item
69
+
70
+ | Prop | Values | Default |
71
+ | --- | --- | --- |
72
+ | `variant` | `default` \| `outline` \| `muted` | `default` |
73
+ | `size` | `xs` \| `sm` \| `default` | `default` |
74
+ | `render` | Base UI render element | — |
75
+
76
+ `size="xs"` is the dropdown-menu density. `variant="outline"` adds a visible border. `variant="muted"` adds a tinted background.
77
+
78
+ ### ItemMedia
79
+
80
+ | Prop | Values | Default | Effect |
81
+ | --- | --- | --- | --- |
82
+ | `variant` | `default` \| `icon` \| `image` | `default` | `icon` auto-sizes SVGs to 16 px; `image` gives a square slot (40 px default, responsive to item size) for avatars and photos |
83
+
84
+ ## 4. Before / After examples
85
+
86
+ ### Settings row (Tractor `Menu` with a toggle)
87
+
88
+ ```tsx
89
+ // Before
90
+ import { Menu, MenuItem } from '@aircall/tractor';
91
+
92
+ <Menu>
93
+ <MenuItem
94
+ label="Enable notifications"
95
+ description="Get a desktop alert for incoming calls."
96
+ action={<Toggle checked={enabled} onChange={setEnabled} />}
97
+ />
98
+ </Menu>
99
+ ```
100
+
101
+ ```tsx
102
+ // After
103
+ import { Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemTitle } from '@aircall/ds';
104
+ import { Switch } from '@aircall/ds';
105
+
106
+ <ItemGroup>
107
+ <Item>
108
+ <ItemContent>
109
+ <ItemTitle>Enable notifications</ItemTitle>
110
+ <ItemDescription>Get a desktop alert for incoming calls.</ItemDescription>
111
+ </ItemContent>
112
+ <ItemActions>
113
+ <Switch checked={enabled} onCheckedChange={setEnabled} />
114
+ </ItemActions>
115
+ </Item>
116
+ </ItemGroup>
117
+ ```
118
+
119
+ ### Contact list (Tractor `List` with avatar + secondary text)
120
+
121
+ ```tsx
122
+ // Before
123
+ import { List, ListItem } from '@aircall/tractor';
124
+
125
+ <List>
126
+ {contacts.map(c => (
127
+ <ListItem
128
+ key={c.id}
129
+ avatar={<img src={c.avatar} alt={c.name} />}
130
+ primaryText={c.name}
131
+ secondaryText={c.lastMessage}
132
+ />
133
+ ))}
134
+ </List>
135
+ ```
136
+
137
+ ```tsx
138
+ // After
139
+ import {
140
+ Avatar,
141
+ AvatarFallback,
142
+ AvatarImage,
143
+ Button,
144
+ Item,
145
+ ItemActions,
146
+ ItemContent,
147
+ ItemDescription,
148
+ ItemGroup,
149
+ ItemMedia,
150
+ ItemTitle
151
+ } from '@aircall/ds';
152
+ import { PhoneCallFill } from '@aircall/react-icons';
153
+
154
+ <ItemGroup>
155
+ {contacts.map(c => (
156
+ <Item key={c.id}>
157
+ <ItemMedia variant="image">
158
+ <Avatar>
159
+ <AvatarImage src={c.avatar} alt={c.name} />
160
+ <AvatarFallback>{initials(c.name)}</AvatarFallback>
161
+ </Avatar>
162
+ </ItemMedia>
163
+ <ItemContent>
164
+ <ItemTitle>{c.name}</ItemTitle>
165
+ <ItemDescription>{c.lastMessage}</ItemDescription>
166
+ </ItemContent>
167
+ <ItemActions>
168
+ <Button variant="ghost" size="icon-sm">
169
+ <PhoneCallFill className="size-4" />
170
+ </Button>
171
+ </ItemActions>
172
+ </Item>
173
+ ))}
174
+ </ItemGroup>
175
+ ```
176
+
177
+ ### Stacked settings group with dividers
178
+
179
+ ```tsx
180
+ // After
181
+ import { Item, ItemContent, ItemDescription, ItemGroup, ItemTitle } from '@aircall/ds';
182
+
183
+ <ItemGroup stackedItems>
184
+ <Item>
185
+ <ItemContent>
186
+ <ItemTitle>Notifications</ItemTitle>
187
+ <ItemDescription>Alert on incoming calls</ItemDescription>
188
+ </ItemContent>
189
+ </Item>
190
+ <Item>
191
+ <ItemContent>
192
+ <ItemTitle>Do not disturb</ItemTitle>
193
+ <ItemDescription>Silence all alerts</ItemDescription>
194
+ </ItemContent>
195
+ </Item>
196
+ </ItemGroup>
197
+ ```
198
+
199
+ `stackedItems` renders the group as a single bordered card. Adjacent items share borders — no gap between rows.
200
+
201
+ ## 5. Common mistakes
202
+
203
+ ### Mistake 1: Putting text directly in `<Item>` instead of `<ItemContent>`
204
+
205
+ ```tsx
206
+ // Wrong — text string becomes a direct flex child of Item, not inside ItemContent
207
+ <Item>
208
+ Enable notifications
209
+ </Item>
210
+
211
+ // Correct — always wrap text in ItemContent with ItemTitle / ItemDescription
212
+ <Item>
213
+ <ItemContent>
214
+ <ItemTitle>Enable notifications</ItemTitle>
215
+ </ItemContent>
216
+ </Item>
217
+ ```
218
+
219
+ `Item` is a flex container whose children are the named slot components. A raw text string sits outside any slot and is not styled as a title — it misaligns with `ItemActions` and `ItemMedia`.
220
+
221
+ Source: `packages/ds/src/components/item.tsx` — `ItemContent` carries `flex-1 flex-col gap-1` that positions the text column correctly.
222
+
223
+ ---
224
+
225
+ ### Mistake 2: Using `variant="icon"` for avatar/photo media
226
+
227
+ ```tsx
228
+ // Wrong — variant="icon" clips the avatar and applies SVG-only sizing rules
229
+ <ItemMedia variant="icon">
230
+ <Avatar>
231
+ <AvatarImage src={user.avatar} alt={user.name} />
232
+ <AvatarFallback>JD</AvatarFallback>
233
+ </Avatar>
234
+ </ItemMedia>
235
+
236
+ // Correct — use variant="image" for avatars and photos
237
+ <ItemMedia variant="image">
238
+ <Avatar>
239
+ <AvatarImage src={user.avatar} alt={user.name} />
240
+ <AvatarFallback>JD</AvatarFallback>
241
+ </Avatar>
242
+ </ItemMedia>
243
+ ```
244
+
245
+ `variant="icon"` applies `[&_svg:not([class*='size-'])]:size-4` — it auto-sizes SVGs to 16 px and does not constrain the container. `variant="image"` sets a fixed square slot (`size-10`, responsive to item size) with `overflow-hidden rounded-sm` so the avatar fills the frame correctly.
246
+
247
+ Source: `packages/ds/src/components/item.tsx` — `itemMediaVariants` defines `icon` and `image` separately.
248
+
249
+ ---
250
+
251
+ ### Mistake 3: Using `<ItemGroup>` for popover/context menus
252
+
253
+ ```tsx
254
+ // Wrong — ItemGroup is for inline lists; it has no popover, trigger, or keyboard-menu semantics
255
+ <ItemGroup>
256
+ <Item onClick={onEdit}>Edit</Item>
257
+ <Item onClick={onDelete}>Delete</Item>
258
+ </ItemGroup>
259
+
260
+ // Correct — use DropdownMenu for popover menus triggered by a button
261
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Button } from '@aircall/ds';
262
+
263
+ <DropdownMenu>
264
+ <DropdownMenuTrigger render={<Button variant="ghost" size="icon" />}>
265
+ <MoreVertical />
266
+ </DropdownMenuTrigger>
267
+ <DropdownMenuContent>
268
+ <DropdownMenuItem onSelect={onEdit}>Edit</DropdownMenuItem>
269
+ <DropdownMenuItem onSelect={onDelete}>Delete</DropdownMenuItem>
270
+ </DropdownMenuContent>
271
+ </DropdownMenu>
272
+ ```
273
+
274
+ `ItemGroup`/`Item` is a layout primitive with `role="list"` — it has no focus trap, keyboard navigation, or popover positioning. Tractor's `Menu` was sometimes used for both inline lists and popover menus; DS splits these concerns into `Item` (inline) and `DropdownMenu` (popover).
275
+
276
+ Source: `packages/ds/src/components/item.tsx` — `ItemGroup` renders a `<div role="list">` with no floating/portal behavior.
277
+
278
+ ---
279
+
280
+ ### Mistake 4: Omitting `stackedItems` and expecting automatic borders between items
281
+
282
+ ```tsx
283
+ // Wrong — without stackedItems, items are separated by a gap (not bordered/merged)
284
+ <ItemGroup>
285
+ <Item variant="outline">Row A</Item>
286
+ <Item variant="outline">Row B</Item>
287
+ </ItemGroup>
288
+
289
+ // Correct — use stackedItems to get a single bordered card with shared inner borders
290
+ <ItemGroup stackedItems>
291
+ <Item>Row A</Item>
292
+ <Item>Row B</Item>
293
+ </ItemGroup>
294
+ ```
295
+
296
+ Without `stackedItems`, `ItemGroup` uses `gap-4` (or `gap-2.5` / `gap-2` for smaller sizes) between items. The `stackedItems` prop removes the gap and applies CSS sibling selectors that collapse adjacent borders into a single shared edge, producing a classic settings-panel appearance.
297
+
298
+ Source: `packages/ds/src/components/item.tsx` — `ItemGroup` applies `stackedItems` via `data-stacked` and sibling border-collapse selectors.
@@ -0,0 +1,263 @@
1
+ ---
2
+ name: aircall-ds/migrate-tractor/link
3
+ description: >
4
+ Migrate Tractor Link (from @aircall/tractor Typography family) to @aircall/ds.
5
+ Covers the Link component exported from @aircall/tractor. Load when a file imports
6
+ Link 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:packages/ds/src/components/button.tsx"
15
+ ---
16
+
17
+ This skill builds on aircall-ds/migrate-tractor. Apply all cross-cutting rules from that skill (prop renames, `render` prop, data attributes) before the link-specific steps below.
18
+
19
+ ## 1. Key premise
20
+
21
+ `@aircall/ds` has **no `Link` component**. Tractor's `Link` was a styled inline `<a>` built on top of `Typography`. The DS equivalent is `Button` with `variant="link"` — it produces underlined primary-coloured text with no button chrome. For any navigational use (real `href`, router link), you must additionally supply a `render` prop so the output is an `<a>` element, not a `<button>`.
22
+
23
+ ## 2. Component mapping
24
+
25
+ | Tractor | @aircall/ds | Notes |
26
+ | --- | --- | --- |
27
+ | `<Link href="…">text</Link>` | `<Button variant="link" render={<a href="…" />}>text</Button>` | Preserves anchor semantics |
28
+ | `<Link href="…" target="_blank">` | `<Button variant="link" render={<a href="…" target="_blank" rel="noreferrer" />}>` | `target`/`rel` go on the rendered `<a>` |
29
+ | `<Link as={RouterLink} to="…">` | `<Button variant="link" render={<RouterLink to="…" />}>` | Router links use `render`, not `as` |
30
+ | `<Link onClick={fn}>text</Link>` | `<Button variant="link" onClick={fn}>text</Button>` | Click-only — no href, no anchor needed |
31
+
32
+ ## 3. Verified DS exports (`packages/ds/src/index.ts`)
33
+
34
+ ```
35
+ Button, buttonVariants
36
+ ```
37
+
38
+ There is no `Link` export. `PaginationLink` exists but is scoped to the Pagination compound and must not be used here.
39
+
40
+ ## 4. Imports
41
+
42
+ ```tsx
43
+ import { Button } from '@aircall/ds';
44
+ ```
45
+
46
+ Icons (if needed alongside a link):
47
+
48
+ ```tsx
49
+ import { ExternalLink } from '@aircall/react-icons';
50
+ ```
51
+
52
+ Never import icons from `lucide-react` directly — always use `@aircall/react-icons`.
53
+
54
+ ## 5. Prop mapping
55
+
56
+ | Tractor `Link` prop | DS equivalent | Notes |
57
+ | --- | --- | --- |
58
+ | `href` | Goes on `render={<a href="…" />}` | `Button` has no `href` prop |
59
+ | `target` | Goes on `render={<a target="…" />}` | Same as `href` |
60
+ | `rel` | Goes on `render={<a rel="…" />}` | Always set `rel="noreferrer"` for `target="_blank"` |
61
+ | `as={RouterLink}` | `render={<RouterLink to="…" />}` | Base UI render-prop pattern |
62
+ | `to` (router) | Goes on the element in `render` | e.g. `render={<RouterLink to="…" />}` |
63
+ | `onClick` | `onClick` | Direct prop on `Button`, unchanged |
64
+ | `variant` (Typography) | _(removed)_ | DS link style is fixed; use `className` for size overrides |
65
+ | `ellipsis` | `className="truncate"` | Tailwind utility; wrap in a sized container |
66
+ | `disabled` | `disabled` | Direct prop, unchanged |
67
+
68
+ ## 6. Before / After examples
69
+
70
+ ### 6a. Plain anchor link
71
+
72
+ **Before (Tractor):**
73
+ ```tsx
74
+ import { Link } from '@aircall/tractor';
75
+
76
+ <Link href="https://aircall.io/docs">Read the docs</Link>
77
+ ```
78
+
79
+ **After (DS):**
80
+ ```tsx
81
+ import { Button } from '@aircall/ds';
82
+
83
+ <Button variant="link" render={<a href="https://aircall.io/docs" />}>
84
+ Read the docs
85
+ </Button>
86
+ ```
87
+
88
+ ### 6b. External link (new tab)
89
+
90
+ **Before (Tractor):**
91
+ ```tsx
92
+ import { Link } from '@aircall/tractor';
93
+
94
+ <Link href="https://aircall.io" target="_blank" rel="noreferrer">
95
+ Open Aircall
96
+ </Link>
97
+ ```
98
+
99
+ **After (DS):**
100
+ ```tsx
101
+ import { Button } from '@aircall/ds';
102
+
103
+ <Button variant="link" render={<a href="https://aircall.io" target="_blank" rel="noreferrer" />}>
104
+ Open Aircall
105
+ </Button>
106
+ ```
107
+
108
+ ### 6c. React Router link
109
+
110
+ **Before (Tractor):**
111
+ ```tsx
112
+ import { Link } from '@aircall/tractor';
113
+ import { Link as RouterLink } from 'react-router-dom';
114
+
115
+ <Link as={RouterLink} to="/settings">
116
+ Settings
117
+ </Link>
118
+ ```
119
+
120
+ **After (DS):**
121
+ ```tsx
122
+ import { Button } from '@aircall/ds';
123
+ import { Link as RouterLink } from 'react-router-dom';
124
+
125
+ <Button variant="link" render={<RouterLink to="/settings" />}>
126
+ Settings
127
+ </Button>
128
+ ```
129
+
130
+ ### 6d. Click-only inline action (no navigation)
131
+
132
+ **Before (Tractor):**
133
+ ```tsx
134
+ import { Link } from '@aircall/tractor';
135
+
136
+ <Link onClick={handleRetry}>Retry</Link>
137
+ ```
138
+
139
+ **After (DS):**
140
+ ```tsx
141
+ import { Button } from '@aircall/ds';
142
+
143
+ <Button variant="link" onClick={handleRetry}>Retry</Button>
144
+ ```
145
+
146
+ > No `render` prop needed here because there is no navigation target — the default `<button>` element is semantically correct for a click action.
147
+
148
+ ### 6e. Link with trailing icon
149
+
150
+ **Before (Tractor):**
151
+ ```tsx
152
+ import { Link } from '@aircall/tractor';
153
+ import { ExternalLink } from '@aircall/react-icons';
154
+
155
+ <Link href="https://aircall.io/docs" target="_blank" rel="noreferrer">
156
+ Docs <ExternalLink size={12} />
157
+ </Link>
158
+ ```
159
+
160
+ **After (DS):**
161
+ ```tsx
162
+ import { Button } from '@aircall/ds';
163
+ import { ExternalLink } from '@aircall/react-icons';
164
+
165
+ <Button variant="link" render={<a href="https://aircall.io/docs" target="_blank" rel="noreferrer" />}>
166
+ Docs <ExternalLink />
167
+ </Button>
168
+ ```
169
+
170
+ > Drop the `size` prop from the icon — DS auto-sizes SVGs via `[&_svg:not([class*='size-'])]:size-4`. Passing a numeric `size` attribute overrides Tailwind and locks the icon to the wrong dimension.
171
+
172
+ ## 7. Common mistakes
173
+
174
+ ### Mistake 1: Importing `Link` from `@aircall/ds`
175
+
176
+ ```tsx
177
+ // WRONG — there is no Link export in @aircall/ds
178
+ import { Link } from '@aircall/ds';
179
+
180
+ <Link href="/settings">Settings</Link>
181
+ ```
182
+
183
+ ```tsx
184
+ // CORRECT — use Button with variant="link"
185
+ import { Button } from '@aircall/ds';
186
+
187
+ <Button variant="link" render={<a href="/settings" />}>Settings</Button>
188
+ ```
189
+
190
+ `@aircall/ds` exports `PaginationLink` but not a general `Link`. Importing `Link` from `@aircall/ds` produces a module error at runtime and a TypeScript named-export error at compile time. The only valid export for this use case is `Button`.
191
+
192
+ `Source: packages/ds/src/index.ts` — only `PaginationLink` exists; no standalone `Link`.
193
+
194
+ ---
195
+
196
+ ### Mistake 2: Passing `href` directly to `Button`
197
+
198
+ ```tsx
199
+ // WRONG — Button has no href prop; it is silently forwarded to a <button> element
200
+ // that ignores it, breaking navigation entirely
201
+ import { Button } from '@aircall/ds';
202
+
203
+ <Button variant="link" href="/settings">Settings</Button>
204
+ ```
205
+
206
+ ```tsx
207
+ // CORRECT — href belongs on the rendered anchor element
208
+ import { Button } from '@aircall/ds';
209
+
210
+ <Button variant="link" render={<a href="/settings" />}>Settings</Button>
211
+ ```
212
+
213
+ `Button` wraps Base UI's `ButtonPrimitive`, which renders a `<button>` by default. A `<button>` element ignores `href`. The `render` prop swaps the root element to an `<a>`, which honours `href` and restores anchor semantics (middle-click, right-click menu, browser history).
214
+
215
+ `Source: packages/ds/src/components/button.tsx` — `Button` extends `ButtonPrimitive.Props` from `@base-ui/react/button`; no `href` in `ButtonProps`.
216
+
217
+ ---
218
+
219
+ ### Mistake 3: Using `as` instead of `render` for router links
220
+
221
+ ```tsx
222
+ // WRONG — DS Button is built on Base UI, not Radix; it has no `as` prop
223
+ import { Button } from '@aircall/ds';
224
+ import { Link as RouterLink } from 'react-router-dom';
225
+
226
+ <Button variant="link" as={RouterLink} to="/settings">Settings</Button>
227
+ ```
228
+
229
+ ```tsx
230
+ // CORRECT — Base UI uses the render prop; pass router-specific props on the element
231
+ import { Button } from '@aircall/ds';
232
+ import { Link as RouterLink } from 'react-router-dom';
233
+
234
+ <Button variant="link" render={<RouterLink to="/settings" />}>Settings</Button>
235
+ ```
236
+
237
+ Base UI replaced Radix UI in `@aircall/ds`. The `asChild`/`as` pattern from Radix is gone. The `render` prop accepts a JSX element — all element-specific props (`to`, `href`, `target`) go on that element, not on `Button` directly.
238
+
239
+ `Source: packages/ds/src/components/button.tsx` — `Button` extends `ButtonPrimitive.Props` from `@base-ui/react/button`, which exposes `render`.
240
+
241
+ ---
242
+
243
+ ### Mistake 4: Omitting `render` for real anchor links
244
+
245
+ ```tsx
246
+ // WRONG — renders a <button> with no href; middle-click and right-click-open broken
247
+ import { Button } from '@aircall/ds';
248
+
249
+ <Button variant="link">Read the docs</Button>
250
+ ```
251
+
252
+ ```tsx
253
+ // CORRECT — supply render to get a real <a> element
254
+ import { Button } from '@aircall/ds';
255
+
256
+ <Button variant="link" render={<a href="https://aircall.io/docs" />}>
257
+ Read the docs
258
+ </Button>
259
+ ```
260
+
261
+ Without `render`, `Button` always outputs `<button>`. A `<button>` cannot be middle-clicked to open in a new tab, does not appear in browser link lists, and has no implicit `link` ARIA role. For any navigational use, `render={<a href="…" />}` is required to preserve anchor semantics.
262
+
263
+ `Source: packages/ds/src/components/button.tsx` — default element is `<button>` via `ButtonPrimitive`.