@aircall/ds 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/dist/globals.css +1 -1
- package/dist/index.d.ts +28 -28
- package/dist/index.js +1 -1
- package/package.json +12 -2
- package/skills/aircall-ds/migrate-icons/SKILL.md +346 -0
- package/skills/aircall-ds/migrate-tractor/SKILL.md +314 -0
- package/skills/aircall-ds/migrate-tractor/accordion/SKILL.md +276 -0
- package/skills/aircall-ds/migrate-tractor/alert/SKILL.md +225 -0
- package/skills/aircall-ds/migrate-tractor/avatar/SKILL.md +272 -0
- package/skills/aircall-ds/migrate-tractor/badge/SKILL.md +274 -0
- package/skills/aircall-ds/migrate-tractor/button/SKILL.md +277 -0
- package/skills/aircall-ds/migrate-tractor/card/SKILL.md +278 -0
- package/skills/aircall-ds/migrate-tractor/combobox/SKILL.md +346 -0
- package/skills/aircall-ds/migrate-tractor/data-table/SKILL.md +333 -0
- package/skills/aircall-ds/migrate-tractor/dialog/SKILL.md +206 -0
- package/skills/aircall-ds/migrate-tractor/divider/SKILL.md +226 -0
- package/skills/aircall-ds/migrate-tractor/dropdown-menu/SKILL.md +266 -0
- package/skills/aircall-ds/migrate-tractor/dropzone/SKILL.md +338 -0
- package/skills/aircall-ds/migrate-tractor/form-and-field/SKILL.md +325 -0
- package/skills/aircall-ds/migrate-tractor/gauge/SKILL.md +248 -0
- package/skills/aircall-ds/migrate-tractor/input/SKILL.md +261 -0
- package/skills/aircall-ds/migrate-tractor/item/SKILL.md +298 -0
- package/skills/aircall-ds/migrate-tractor/link/SKILL.md +263 -0
- package/skills/aircall-ds/migrate-tractor/popover/SKILL.md +214 -0
- package/skills/aircall-ds/migrate-tractor/select/SKILL.md +245 -0
- package/skills/aircall-ds/migrate-tractor/sheet-vs-drawer/SKILL.md +272 -0
- package/skills/aircall-ds/migrate-tractor/skeleton/SKILL.md +190 -0
- package/skills/aircall-ds/migrate-tractor/styling/SKILL.md +421 -0
- package/skills/aircall-ds/migrate-tractor/tabs/SKILL.md +250 -0
- package/skills/aircall-ds/migrate-tractor/toast/SKILL.md +322 -0
- package/skills/aircall-ds/migrate-tractor/tooltip/SKILL.md +204 -0
- package/skills/aircall-ds/migrate-tractor/tree/SKILL.md +346 -0
- package/skills/aircall-ds/setup/SKILL.md +347 -0
|
@@ -0,0 +1,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`.
|