@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,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`