@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,278 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aircall-ds/migrate-tractor/card
|
|
3
|
+
description: >
|
|
4
|
+
Migrate bespoke hand-rolled card components (plain div/Box with border/shadow/
|
|
5
|
+
padding) to the @aircall/ds Card compound (Card, CardHeader, CardTitle,
|
|
6
|
+
CardDescription, CardAction, CardContent, CardFooter). Load when a file
|
|
7
|
+
contains an in-app card implementation or imports card-like primitives from
|
|
8
|
+
@aircall/tractor.
|
|
9
|
+
type: sub-skill
|
|
10
|
+
library: aircall-ds
|
|
11
|
+
library_version: "0.13.0"
|
|
12
|
+
requires:
|
|
13
|
+
- aircall-ds/setup
|
|
14
|
+
- aircall-ds/migrate-tractor
|
|
15
|
+
sources:
|
|
16
|
+
- "aircall/hydra:docs/migration-guides/tractor-to-ds/recipes/card.md"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
This skill builds on aircall-ds/migrate-tractor.
|
|
20
|
+
|
|
21
|
+
## 1. Component mapping
|
|
22
|
+
|
|
23
|
+
Tractor had no `Card` component. Apps grew bespoke card implementations — `<div>` or Tractor `<Box>` elements with hand-rolled borders, shadows, padding, and per-use-case variations. DS `Card` is the single component to unify all of them.
|
|
24
|
+
|
|
25
|
+
| App pattern (Tractor-era) | @aircall/ds |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| Root `<div>` / `<Box>` with border, shadow, radius, bg, padding | `Card` |
|
|
28
|
+
| Title element inside the card header region | `CardTitle` (inside `CardHeader`) |
|
|
29
|
+
| Subtitle / description element | `CardDescription` (inside `CardHeader`) |
|
|
30
|
+
| Header region (`<div>` wrapping title + subtitle) | `CardHeader` |
|
|
31
|
+
| Top-right control (icon button, menu trigger) | `CardAction` (inside `CardHeader`) |
|
|
32
|
+
| Body / content region | `CardContent` |
|
|
33
|
+
| Footer / actions region | `CardFooter` |
|
|
34
|
+
|
|
35
|
+
## 2. Verified DS exports (`packages/ds/src/index.ts`)
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 3. Imports
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import {
|
|
45
|
+
Card,
|
|
46
|
+
CardAction,
|
|
47
|
+
CardContent,
|
|
48
|
+
CardDescription,
|
|
49
|
+
CardFooter,
|
|
50
|
+
CardHeader,
|
|
51
|
+
CardTitle,
|
|
52
|
+
Button
|
|
53
|
+
} from '@aircall/ds';
|
|
54
|
+
import { MoreVertical } from '@aircall/react-icons';
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Never import icons from `lucide-react` directly — always use `@aircall/react-icons`.
|
|
58
|
+
|
|
59
|
+
## 4. Size prop
|
|
60
|
+
|
|
61
|
+
`Card` accepts a `size` prop (`"default" | "sm"`). Setting it on the root propagates via `data-size` + Tailwind group selectors to **all** sub-components — padding, gap, and title font size all scale together. Never add per-part `p-*` overrides to achieve a compact variant; use `size="sm"` instead.
|
|
62
|
+
|
|
63
|
+
| DS `size` | Padding | Gap | Title size |
|
|
64
|
+
| --- | --- | --- | --- |
|
|
65
|
+
| `"default"` (omit prop) | `py-4` / `px-4` | `gap-4` | `text-base` |
|
|
66
|
+
| `"sm"` | `py-3` / `px-3` | `gap-3` | `text-sm` |
|
|
67
|
+
|
|
68
|
+
## 5. Before / After
|
|
69
|
+
|
|
70
|
+
### 5a. Basic card — replacing a bespoke surface
|
|
71
|
+
|
|
72
|
+
**Before (Tractor-era bespoke component):**
|
|
73
|
+
```tsx
|
|
74
|
+
import { Box, Typography } from '@aircall/tractor';
|
|
75
|
+
|
|
76
|
+
<Box
|
|
77
|
+
className="rounded-xl border border-neutral-200 bg-white p-4 shadow-sm"
|
|
78
|
+
display="flex"
|
|
79
|
+
flexDirection="column"
|
|
80
|
+
gap="space-16"
|
|
81
|
+
>
|
|
82
|
+
<Box display="flex" flexDirection="column" gap="space-4">
|
|
83
|
+
<Typography variant="headingSmall">Card title</Typography>
|
|
84
|
+
<Typography variant="bodySmall" color="neutral.600">
|
|
85
|
+
A short supporting description.
|
|
86
|
+
</Typography>
|
|
87
|
+
</Box>
|
|
88
|
+
<Box>
|
|
89
|
+
<p>Card body content goes here.</p>
|
|
90
|
+
</Box>
|
|
91
|
+
<Box display="flex" justifyContent="flex-end">
|
|
92
|
+
<Button variant="primary" size="small">Action</Button>
|
|
93
|
+
</Box>
|
|
94
|
+
</Box>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**After (DS):**
|
|
98
|
+
```tsx
|
|
99
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Button } from '@aircall/ds';
|
|
100
|
+
|
|
101
|
+
<Card className="w-[350px]">
|
|
102
|
+
<CardHeader>
|
|
103
|
+
<CardTitle>Card title</CardTitle>
|
|
104
|
+
<CardDescription>A short supporting description.</CardDescription>
|
|
105
|
+
</CardHeader>
|
|
106
|
+
<CardContent>
|
|
107
|
+
<p className="text-sm">Card body content goes here.</p>
|
|
108
|
+
</CardContent>
|
|
109
|
+
<CardFooter>
|
|
110
|
+
<Button variant="default" size="lg">Action</Button>
|
|
111
|
+
</CardFooter>
|
|
112
|
+
</Card>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Key changes:
|
|
116
|
+
- Delete all hand-rolled surface styles (border, shadow, radius, bg, padding) — `Card` owns all of it.
|
|
117
|
+
- Map title/subtitle to `CardTitle` + `CardDescription` inside `CardHeader`.
|
|
118
|
+
- Map body to `CardContent`; actions to `CardFooter`.
|
|
119
|
+
- `CardFooter` renders with `border-t bg-muted/50` by default (distinct action bar). For a plain footer add `className="bg-transparent border-none"`.
|
|
120
|
+
|
|
121
|
+
### 5b. Compact card using `size="sm"`
|
|
122
|
+
|
|
123
|
+
**Before:**
|
|
124
|
+
```tsx
|
|
125
|
+
import { Box, Typography } from '@aircall/tractor';
|
|
126
|
+
|
|
127
|
+
<Box className="rounded-xl border border-neutral-200 bg-white p-3 shadow-sm flex flex-col gap-3">
|
|
128
|
+
<Typography variant="headingXSmall">Compact card</Typography>
|
|
129
|
+
<p className="text-xs">Body content.</p>
|
|
130
|
+
</Box>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**After:**
|
|
134
|
+
```tsx
|
|
135
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@aircall/ds';
|
|
136
|
+
|
|
137
|
+
<Card size="sm">
|
|
138
|
+
<CardHeader>
|
|
139
|
+
<CardTitle>Compact card</CardTitle>
|
|
140
|
+
</CardHeader>
|
|
141
|
+
<CardContent>
|
|
142
|
+
<p className="text-sm">Body content.</p>
|
|
143
|
+
</CardContent>
|
|
144
|
+
</Card>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Setting `size="sm"` on `Card` scales all sub-components — no per-part padding overrides needed.
|
|
148
|
+
|
|
149
|
+
### 5c. Card with a header action
|
|
150
|
+
|
|
151
|
+
**Before:**
|
|
152
|
+
```tsx
|
|
153
|
+
import { Box, Typography } from '@aircall/tractor';
|
|
154
|
+
import { IconButton } from '@aircall/tractor';
|
|
155
|
+
import { MoreVertical } from '@aircall/react-icons';
|
|
156
|
+
|
|
157
|
+
<Box className="rounded-xl border bg-white p-4 relative">
|
|
158
|
+
<Box display="flex" justifyContent="space-between" alignItems="flex-start">
|
|
159
|
+
<Typography variant="headingSmall">With action</Typography>
|
|
160
|
+
<IconButton component={MoreVertical} aria-label="Open menu" />
|
|
161
|
+
</Box>
|
|
162
|
+
</Box>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**After:**
|
|
166
|
+
```tsx
|
|
167
|
+
import { Card, CardAction, CardHeader, CardTitle, Button } from '@aircall/ds';
|
|
168
|
+
import { MoreVertical } from '@aircall/react-icons';
|
|
169
|
+
|
|
170
|
+
<Card>
|
|
171
|
+
<CardHeader>
|
|
172
|
+
<CardTitle>With action</CardTitle>
|
|
173
|
+
<CardAction>
|
|
174
|
+
<Button variant="ghost" size="icon" aria-label="Open menu">
|
|
175
|
+
<MoreVertical />
|
|
176
|
+
</Button>
|
|
177
|
+
</CardAction>
|
|
178
|
+
</CardHeader>
|
|
179
|
+
</Card>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`CardAction` must live **inside** `CardHeader` — the header is a CSS grid that auto-places it in the top-right (`col-start-2 row-span-2`). Rendered elsewhere it loses positioning entirely.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## 6. Common mistakes
|
|
187
|
+
|
|
188
|
+
### Mistake 1 — Keeping hand-rolled surface styles on the root element
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
// ❌ Wrong — double-styles the surface; shadow and border clash with Card's ring
|
|
192
|
+
<Card className="border border-neutral-200 rounded-xl shadow-sm bg-white p-4">
|
|
193
|
+
<CardContent>Content</CardContent>
|
|
194
|
+
</Card>
|
|
195
|
+
|
|
196
|
+
// ✅ Correct — Card owns border, radius, bg, and padding; only true one-offs belong in className
|
|
197
|
+
<Card className="w-[350px]">
|
|
198
|
+
<CardContent>Content</CardContent>
|
|
199
|
+
</Card>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`Card` already applies `rounded-xl border bg-card py-4` (plus ring, text color, and overflow). Adding these again creates double borders and mismatched shadows that diverge from the design token.
|
|
203
|
+
|
|
204
|
+
Source: `packages/ds/src/components/card.tsx`
|
|
205
|
+
|
|
206
|
+
### Mistake 2 — Placing `CardAction` outside `CardHeader`
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
// ❌ Wrong — CardAction is not positioned; it renders inline with body content
|
|
210
|
+
<Card>
|
|
211
|
+
<CardHeader>
|
|
212
|
+
<CardTitle>Title</CardTitle>
|
|
213
|
+
</CardHeader>
|
|
214
|
+
<CardAction>
|
|
215
|
+
<Button variant="ghost" size="icon" aria-label="Menu"><MoreVertical /></Button>
|
|
216
|
+
</CardAction>
|
|
217
|
+
<CardContent>Body</CardContent>
|
|
218
|
+
</Card>
|
|
219
|
+
|
|
220
|
+
// ✅ Correct — CardAction inside CardHeader uses the header grid to pin top-right
|
|
221
|
+
<Card>
|
|
222
|
+
<CardHeader>
|
|
223
|
+
<CardTitle>Title</CardTitle>
|
|
224
|
+
<CardAction>
|
|
225
|
+
<Button variant="ghost" size="icon" aria-label="Menu"><MoreVertical /></Button>
|
|
226
|
+
</CardAction>
|
|
227
|
+
</CardHeader>
|
|
228
|
+
<CardContent>Body</CardContent>
|
|
229
|
+
</Card>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`CardHeader` uses `grid auto-rows-min has-data-[slot=card-action]:grid-cols-[1fr_auto]`. `CardAction` carries `data-slot="card-action"` and is placed via `col-start-2 row-span-2`. Outside `CardHeader`, no grid parent exists and the action renders in normal flow.
|
|
233
|
+
|
|
234
|
+
Source: `packages/ds/src/components/card.tsx`
|
|
235
|
+
|
|
236
|
+
### Mistake 3 — Using per-part padding overrides instead of `size="sm"`
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
// ❌ Wrong — overriding padding per part breaks the sizing contract and diverges from tokens
|
|
240
|
+
<Card>
|
|
241
|
+
<CardHeader className="px-3 py-2">
|
|
242
|
+
<CardTitle className="text-sm">Compact</CardTitle>
|
|
243
|
+
</CardHeader>
|
|
244
|
+
<CardContent className="px-3">Body</CardContent>
|
|
245
|
+
</Card>
|
|
246
|
+
|
|
247
|
+
// ✅ Correct — size="sm" on the root scales all sub-components consistently
|
|
248
|
+
<Card size="sm">
|
|
249
|
+
<CardHeader>
|
|
250
|
+
<CardTitle>Compact</CardTitle>
|
|
251
|
+
</CardHeader>
|
|
252
|
+
<CardContent>Body</CardContent>
|
|
253
|
+
</Card>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`Card` propagates `data-size="sm"` to sub-components via the `group/card` modifier. Each part responds with `group-data-[size=sm]/card:px-3`, `group-data-[size=sm]/card:py-3`, and `group-data-[size=sm]/card:text-sm`. Overriding padding individually bypasses this mechanism and produces inconsistent spacing.
|
|
257
|
+
|
|
258
|
+
Source: `packages/ds/src/components/card.tsx`
|
|
259
|
+
|
|
260
|
+
### Mistake 4 — Adding bottom padding to `Card` when a footer is present
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
// ❌ Wrong — adds visible gap between footer and card bottom edge
|
|
264
|
+
<Card className="pb-4">
|
|
265
|
+
<CardContent>Body</CardContent>
|
|
266
|
+
<CardFooter>Actions</CardFooter>
|
|
267
|
+
</Card>
|
|
268
|
+
|
|
269
|
+
// ✅ Correct — Card auto-removes bottom padding when CardFooter is present
|
|
270
|
+
<Card>
|
|
271
|
+
<CardContent>Body</CardContent>
|
|
272
|
+
<CardFooter>Actions</CardFooter>
|
|
273
|
+
</Card>
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`Card` uses `has-data-[slot=card-footer]:pb-0` to remove its own bottom padding when a `CardFooter` is present, so the footer's rounded corners (`rounded-b-xl`) flush with the card edge. Forcing `pb-4` overrides this and leaves a visible gap.
|
|
277
|
+
|
|
278
|
+
Source: `packages/ds/src/components/card.tsx`
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aircall-ds/migrate-tractor/combobox
|
|
3
|
+
description: >
|
|
4
|
+
Migrate Tractor ComboBox (and the old Command + Popover search pattern) to the
|
|
5
|
+
@aircall/ds Combobox compound. Load when a file imports ComboBox, Command, or a
|
|
6
|
+
Command+Popover search combo 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/combobox.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 combobox-specific steps below.
|
|
18
|
+
|
|
19
|
+
## 1. When to use Combobox vs Select
|
|
20
|
+
|
|
21
|
+
Use `Combobox` when the user needs to **search or filter** options. For a short, fixed-enum list with no search, use `Select` instead (`aircall-ds/migrate-tractor/select`).
|
|
22
|
+
|
|
23
|
+
## 2. Component mapping
|
|
24
|
+
|
|
25
|
+
Tractor `ComboBox` was a single self-contained component. DS uses a compound structure where every visual slot is a named part. The old "Command + Popover" search recipe is fully replaced by this same compound.
|
|
26
|
+
|
|
27
|
+
| Tractor / old pattern | DS compound part | Role |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `<ComboBox>` | `<Combobox>` | State owner (`value`, `onValueChange`, `multiple`) |
|
|
30
|
+
| _(input built-in)_ | `<ComboboxInput>` | Visible text field (includes trigger chevron via `showTrigger`) |
|
|
31
|
+
| _(trigger built-in)_ | `<ComboboxTrigger>` | Standalone chevron button (used inside custom layouts) |
|
|
32
|
+
| _(no equivalent)_ | `<ComboboxContent>` | Popover container — handles portal + positioning |
|
|
33
|
+
| _(no equivalent)_ | `<ComboboxList>` | Scrollable list inside the popover |
|
|
34
|
+
| `<ComboBoxOption>` / `<CommandItem>` | `<ComboboxItem>` | Individual option |
|
|
35
|
+
| _(no equivalent)_ | `<ComboboxEmpty>` | Shown automatically when the filtered list is empty |
|
|
36
|
+
| _(no equivalent)_ | `<ComboboxGroup>` | Logical section wrapper |
|
|
37
|
+
| _(no equivalent)_ | `<ComboboxLabel>` | Section heading (must be inside a `ComboboxGroup`) |
|
|
38
|
+
| _(no equivalent)_ | `<ComboboxChips>` | Multi-select chip container (replaces custom tag inputs) |
|
|
39
|
+
| _(no equivalent)_ | `<ComboboxChip>` | Individual removable chip inside `ComboboxChips` |
|
|
40
|
+
| _(no equivalent)_ | `<ComboboxChipsInput>` | Inline input rendered inside `ComboboxChips` |
|
|
41
|
+
| _(no equivalent)_ | `useComboboxAnchor()` | Returns a ref used to anchor `ComboboxContent` to a chips container |
|
|
42
|
+
|
|
43
|
+
## 3. Verified DS exports (`packages/ds/src/index.ts`)
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput,
|
|
47
|
+
ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxGroup,
|
|
48
|
+
ComboboxInput, ComboboxItem, ComboboxLabel, ComboboxList,
|
|
49
|
+
ComboboxSeparator, ComboboxTrigger, ComboboxValue,
|
|
50
|
+
useComboboxAnchor
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
All names used in the Before/After examples below exist in the published public API.
|
|
54
|
+
|
|
55
|
+
## 4. Key prop differences
|
|
56
|
+
|
|
57
|
+
| Tractor / old pattern | DS equivalent | Notes |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| `onChange` | `onValueChange` | Signature: `(value: string \| null) => void` (single); `(value: string[]) => void` (multiple) |
|
|
60
|
+
| `options={[…]}` | `<ComboboxItem>` children | Items are rendered declaratively, not via a data prop |
|
|
61
|
+
| `multi` / `isMulti` | `multiple` on `<Combobox>` | Boolean flag; `value` and `onValueChange` then use `string[]` |
|
|
62
|
+
| _(manual filter logic)_ | Built-in filtering | DS filters items automatically against typed input; opt out for async search by controlling `value` + `onChange` on `ComboboxInput` |
|
|
63
|
+
| `showClear` _(absent)_ | `showClear` on `<ComboboxInput>` | Renders an X button that calls `ComboboxPrimitive.Clear` |
|
|
64
|
+
|
|
65
|
+
## 5. Before / After
|
|
66
|
+
|
|
67
|
+
### 5a. Single-select
|
|
68
|
+
|
|
69
|
+
#### Before (Tractor)
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { ComboBox, ComboBoxOption } from '@aircall/tractor';
|
|
73
|
+
|
|
74
|
+
<ComboBox
|
|
75
|
+
value={country}
|
|
76
|
+
onChange={setCountry}
|
|
77
|
+
placeholder="Search countries"
|
|
78
|
+
>
|
|
79
|
+
{countries.map(c => (
|
|
80
|
+
<ComboBoxOption key={c.iso} value={c.iso}>
|
|
81
|
+
{c.name}
|
|
82
|
+
</ComboBoxOption>
|
|
83
|
+
))}
|
|
84
|
+
</ComboBox>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### After (DS)
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
import {
|
|
91
|
+
Combobox,
|
|
92
|
+
ComboboxContent,
|
|
93
|
+
ComboboxEmpty,
|
|
94
|
+
ComboboxGroup,
|
|
95
|
+
ComboboxInput,
|
|
96
|
+
ComboboxItem,
|
|
97
|
+
ComboboxLabel,
|
|
98
|
+
ComboboxList,
|
|
99
|
+
} from '@aircall/ds';
|
|
100
|
+
|
|
101
|
+
<Combobox value={country} onValueChange={setCountry}>
|
|
102
|
+
<ComboboxInput placeholder="Search countries" showTrigger />
|
|
103
|
+
<ComboboxContent>
|
|
104
|
+
<ComboboxList>
|
|
105
|
+
<ComboboxEmpty>No results.</ComboboxEmpty>
|
|
106
|
+
<ComboboxGroup>
|
|
107
|
+
<ComboboxLabel>Suggestions</ComboboxLabel>
|
|
108
|
+
{countries.map(c => (
|
|
109
|
+
<ComboboxItem key={c.iso} value={c.iso}>
|
|
110
|
+
{c.name}
|
|
111
|
+
</ComboboxItem>
|
|
112
|
+
))}
|
|
113
|
+
</ComboboxGroup>
|
|
114
|
+
</ComboboxList>
|
|
115
|
+
</ComboboxContent>
|
|
116
|
+
</Combobox>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 5b. Multi-select with chips
|
|
120
|
+
|
|
121
|
+
#### Before (Tractor — custom tag input pattern)
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
import { ComboBox } from '@aircall/tractor';
|
|
125
|
+
|
|
126
|
+
<ComboBox
|
|
127
|
+
multi
|
|
128
|
+
value={selected}
|
|
129
|
+
onChange={setSelected}
|
|
130
|
+
placeholder="Add a tag"
|
|
131
|
+
options={items}
|
|
132
|
+
/>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### After (DS)
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
import {
|
|
139
|
+
Combobox,
|
|
140
|
+
ComboboxChip,
|
|
141
|
+
ComboboxChips,
|
|
142
|
+
ComboboxChipsInput,
|
|
143
|
+
ComboboxContent,
|
|
144
|
+
ComboboxEmpty,
|
|
145
|
+
ComboboxItem,
|
|
146
|
+
ComboboxList,
|
|
147
|
+
useComboboxAnchor,
|
|
148
|
+
} from '@aircall/ds';
|
|
149
|
+
|
|
150
|
+
function TagPicker() {
|
|
151
|
+
const [selected, setSelected] = React.useState<string[]>([]);
|
|
152
|
+
const anchorRef = useComboboxAnchor();
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<Combobox multiple value={selected} onValueChange={setSelected}>
|
|
156
|
+
<ComboboxChips ref={anchorRef}>
|
|
157
|
+
{selected.map(val => (
|
|
158
|
+
<ComboboxChip key={val} value={val}>
|
|
159
|
+
{items.find(i => i.value === val)?.label}
|
|
160
|
+
</ComboboxChip>
|
|
161
|
+
))}
|
|
162
|
+
<ComboboxChipsInput placeholder="Add a tag" />
|
|
163
|
+
</ComboboxChips>
|
|
164
|
+
<ComboboxContent anchor={anchorRef}>
|
|
165
|
+
<ComboboxList>
|
|
166
|
+
<ComboboxEmpty>No results.</ComboboxEmpty>
|
|
167
|
+
{items.map(item => (
|
|
168
|
+
<ComboboxItem key={item.value} value={item.value}>
|
|
169
|
+
{item.label}
|
|
170
|
+
</ComboboxItem>
|
|
171
|
+
))}
|
|
172
|
+
</ComboboxList>
|
|
173
|
+
</ComboboxContent>
|
|
174
|
+
</Combobox>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 5c. Async / server-driven options
|
|
180
|
+
|
|
181
|
+
Control the input value yourself and disable the built-in filter by rendering only the results you fetch.
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
import {
|
|
185
|
+
Combobox,
|
|
186
|
+
ComboboxContent,
|
|
187
|
+
ComboboxEmpty,
|
|
188
|
+
ComboboxInput,
|
|
189
|
+
ComboboxItem,
|
|
190
|
+
ComboboxList,
|
|
191
|
+
} from '@aircall/ds';
|
|
192
|
+
|
|
193
|
+
function ContactPicker({ onPick }: { onPick: (id: string) => void }) {
|
|
194
|
+
const [query, setQuery] = React.useState('');
|
|
195
|
+
const { data: results, loading } = useSearch(query);
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<Combobox onValueChange={onPick}>
|
|
199
|
+
<ComboboxInput
|
|
200
|
+
placeholder="Search contacts"
|
|
201
|
+
value={query}
|
|
202
|
+
onChange={e => setQuery(e.target.value)}
|
|
203
|
+
/>
|
|
204
|
+
<ComboboxContent>
|
|
205
|
+
<ComboboxList>
|
|
206
|
+
{loading && <ComboboxEmpty>Searching…</ComboboxEmpty>}
|
|
207
|
+
{!loading && results.length === 0 && (
|
|
208
|
+
<ComboboxEmpty>No matches.</ComboboxEmpty>
|
|
209
|
+
)}
|
|
210
|
+
{results.map(r => (
|
|
211
|
+
<ComboboxItem key={r.id} value={r.id}>
|
|
212
|
+
{r.name}
|
|
213
|
+
</ComboboxItem>
|
|
214
|
+
))}
|
|
215
|
+
</ComboboxList>
|
|
216
|
+
</ComboboxContent>
|
|
217
|
+
</Combobox>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## 6. Common Mistakes
|
|
223
|
+
|
|
224
|
+
### Mistake 1 — Omitting `ComboboxList` and placing items directly in `ComboboxContent`
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
// Wrong
|
|
228
|
+
<ComboboxContent>
|
|
229
|
+
<ComboboxEmpty>No results.</ComboboxEmpty>
|
|
230
|
+
<ComboboxItem value="a">Alpha</ComboboxItem>
|
|
231
|
+
</ComboboxContent>
|
|
232
|
+
|
|
233
|
+
// Correct
|
|
234
|
+
<ComboboxContent>
|
|
235
|
+
<ComboboxList>
|
|
236
|
+
<ComboboxEmpty>No results.</ComboboxEmpty>
|
|
237
|
+
<ComboboxItem value="a">Alpha</ComboboxItem>
|
|
238
|
+
</ComboboxList>
|
|
239
|
+
</ComboboxContent>
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`ComboboxContent` is the popover shell (portal + positioner + popup). The scrollable, filterable list — including `ComboboxEmpty` visibility logic — lives in `ComboboxList`. Placing items directly in `ComboboxContent` bypasses the scroll container and breaks the empty-state `group-data-empty` CSS selector that shows `ComboboxEmpty`.
|
|
243
|
+
Source: `packages/ds/src/components/combobox.tsx`
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
### Mistake 2 — Forgetting `useComboboxAnchor` / not passing `anchor` for multi-select chips
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
// Wrong — popover anchors to the document body, not the chips container
|
|
251
|
+
<Combobox multiple value={selected} onValueChange={setSelected}>
|
|
252
|
+
<ComboboxChips>
|
|
253
|
+
{selected.map(val => <ComboboxChip key={val} value={val}>{val}</ComboboxChip>)}
|
|
254
|
+
<ComboboxChipsInput placeholder="Add a tag" />
|
|
255
|
+
</ComboboxChips>
|
|
256
|
+
<ComboboxContent>
|
|
257
|
+
<ComboboxList>
|
|
258
|
+
{items.map(i => <ComboboxItem key={i.value} value={i.value}>{i.label}</ComboboxItem>)}
|
|
259
|
+
</ComboboxList>
|
|
260
|
+
</ComboboxContent>
|
|
261
|
+
</Combobox>
|
|
262
|
+
|
|
263
|
+
// Correct
|
|
264
|
+
const anchorRef = useComboboxAnchor();
|
|
265
|
+
|
|
266
|
+
<Combobox multiple value={selected} onValueChange={setSelected}>
|
|
267
|
+
<ComboboxChips ref={anchorRef}>
|
|
268
|
+
{selected.map(val => <ComboboxChip key={val} value={val}>{val}</ComboboxChip>)}
|
|
269
|
+
<ComboboxChipsInput placeholder="Add a tag" />
|
|
270
|
+
</ComboboxChips>
|
|
271
|
+
<ComboboxContent anchor={anchorRef}>
|
|
272
|
+
<ComboboxList>
|
|
273
|
+
{items.map(i => <ComboboxItem key={i.value} value={i.value}>{i.label}</ComboboxItem>)}
|
|
274
|
+
</ComboboxList>
|
|
275
|
+
</ComboboxContent>
|
|
276
|
+
</Combobox>
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Without `anchor={anchorRef}`, `ComboboxContent` uses the default Combobox root as its positional reference, so the dropdown opens at the wrong position (above or misaligned with the chip container). `useComboboxAnchor()` returns a stable `ref` that must be attached to `ComboboxChips` and forwarded to `ComboboxContent`.
|
|
280
|
+
Source: `packages/ds/src/components/combobox.tsx`
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
### Mistake 3 — Using a manual filter loop instead of the built-in filtering for static lists
|
|
285
|
+
|
|
286
|
+
```tsx
|
|
287
|
+
// Wrong — manual filter duplicates work the DS already does
|
|
288
|
+
const [query, setQuery] = React.useState('');
|
|
289
|
+
const visible = countries.filter(c =>
|
|
290
|
+
c.name.toLowerCase().includes(query.toLowerCase())
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
<Combobox value={country} onValueChange={setCountry}>
|
|
294
|
+
<ComboboxInput
|
|
295
|
+
placeholder="Search countries"
|
|
296
|
+
value={query}
|
|
297
|
+
onChange={e => setQuery(e.target.value)}
|
|
298
|
+
/>
|
|
299
|
+
<ComboboxContent>
|
|
300
|
+
<ComboboxList>
|
|
301
|
+
{visible.map(c => (
|
|
302
|
+
<ComboboxItem key={c.iso} value={c.iso}>{c.name}</ComboboxItem>
|
|
303
|
+
))}
|
|
304
|
+
</ComboboxList>
|
|
305
|
+
</ComboboxContent>
|
|
306
|
+
</Combobox>
|
|
307
|
+
|
|
308
|
+
// Correct — render all items; DS filters automatically
|
|
309
|
+
<Combobox value={country} onValueChange={setCountry}>
|
|
310
|
+
<ComboboxInput placeholder="Search countries" showTrigger />
|
|
311
|
+
<ComboboxContent>
|
|
312
|
+
<ComboboxList>
|
|
313
|
+
<ComboboxEmpty>No results.</ComboboxEmpty>
|
|
314
|
+
{countries.map(c => (
|
|
315
|
+
<ComboboxItem key={c.iso} value={c.iso}>{c.name}</ComboboxItem>
|
|
316
|
+
))}
|
|
317
|
+
</ComboboxList>
|
|
318
|
+
</ComboboxContent>
|
|
319
|
+
</Combobox>
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
For static option lists, the DS Combobox (via Base UI) filters rendered items automatically against what the user types — no state or `filter` callback needed. Manual filtering is only required for async/server-driven lists where you control the results yourself.
|
|
323
|
+
Source: `packages/ds/src/components/combobox.tsx`
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
### Mistake 4 — Placing `ComboboxLabel` directly in `ComboboxList` without a `ComboboxGroup`
|
|
328
|
+
|
|
329
|
+
```tsx
|
|
330
|
+
// Wrong
|
|
331
|
+
<ComboboxList>
|
|
332
|
+
<ComboboxLabel>Suggestions</ComboboxLabel>
|
|
333
|
+
<ComboboxItem value="a">Alpha</ComboboxItem>
|
|
334
|
+
</ComboboxList>
|
|
335
|
+
|
|
336
|
+
// Correct
|
|
337
|
+
<ComboboxList>
|
|
338
|
+
<ComboboxGroup>
|
|
339
|
+
<ComboboxLabel>Suggestions</ComboboxLabel>
|
|
340
|
+
<ComboboxItem value="a">Alpha</ComboboxItem>
|
|
341
|
+
</ComboboxGroup>
|
|
342
|
+
</ComboboxList>
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
`ComboboxLabel` maps to Base UI's `ComboboxPrimitive.GroupLabel` — it is semantically scoped to a `ComboboxGroup`. Rendering it outside a group breaks the ARIA `group` association that screen readers rely on to announce the section heading.
|
|
346
|
+
Source: `packages/ds/src/components/combobox.tsx`
|