@aircall/blocks 0.5.1 → 0.7.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.
@@ -0,0 +1,487 @@
1
+ ---
2
+ name: aircall-blocks/migrate-dashboard/combobox
3
+ description: >
4
+ Migrate @dashboard/library search-selects to @aircall/ds Combobox — multi-select
5
+ (MultiSearchSelect, MultiInlineSearchSelect, MultiSelectOption, multi MultiSelect) via
6
+ the `multiple` prop, and single-select (SingleSearchSelect, SearchSelect) by omitting
7
+ it. Load when a file imports any *SearchSelect / MultiSelect from @dashboard/library.
8
+ type: sub-skill
9
+ library: aircall-blocks
10
+ library_version: "0.5.1"
11
+ requires:
12
+ - aircall-blocks/setup
13
+ - aircall-blocks/migrate-dashboard
14
+ sources:
15
+ - "aircall/hydra:packages/ds/src/index.ts"
16
+ ---
17
+
18
+ This skill builds on aircall-blocks/migrate-dashboard.
19
+
20
+ ## 1. Component mapping
21
+
22
+ | @dashboard/library | @aircall/ds |
23
+ | --- | --- |
24
+ | `MultiSearchSelect` (root with search input + dropdown) | `Combobox` (root, `multiple` prop) + `ComboboxChips` + `ComboboxChipsInput` + `ComboboxContent` + `ComboboxList` + `ComboboxItem` |
25
+ | `MultiInlineSearchSelect` (root with inline chips + search box) | `Combobox` (root, `multiple` prop) + `ComboboxChips` + `ComboboxChipsInput` + `ComboboxContent` + `ComboboxList` + `ComboboxItem` |
26
+ | `MultiSelectOption` (option shape `{ value, label }`) | Consumer-defined plain object — DS has no equivalent type; define `{ value: string; label: string }` locally |
27
+ | `options` prop (array of options for the list) | `items` prop on `Combobox` |
28
+ | `onSelect` prop (receives `selectedKeys: string[]`) | `onValueChange` prop on `Combobox` (receives option objects, not raw keys) |
29
+ | `selectedKeys` / `defaultSelectedKeys` prop | `value` / `defaultValue` prop on `Combobox` (option objects, not strings) |
30
+ | `selectionMode="multiple"` | `multiple` boolean prop on `Combobox` |
31
+ | _single-select_ (`SingleSearchSelect`, `SearchSelect`, single-value `MultiSelect`) | **omit** `multiple` — `Combobox` is single by default. Use `ComboboxInput` instead of `ComboboxChips`/`ComboboxChipsInput`; `value`/`onValueChange` carry one option object (or `null` when cleared) |
32
+ | `renderTag` prop (custom chip renderer) | `ComboboxValue` render-prop inside `ComboboxChips`; render `ComboboxChip` per item |
33
+ | `renderItem` prop (custom list-row renderer) | `children` of `ComboboxItem` |
34
+ | `renderItemPrefix` / `renderItemSuffix` | Inline JSX children of `ComboboxItem` (before / after the label) |
35
+ | `renderItemDescription` | Inline `ItemDescription` children of `ComboboxItem` |
36
+ | `emptyLabel` prop | `ComboboxEmpty` children |
37
+ | `placeholder` prop | `placeholder` on `ComboboxChipsInput` |
38
+ | `loading` prop | Conditional render above `ComboboxList` (no built-in loading state) |
39
+ | `disabled` prop | `disabled` prop on `Combobox` |
40
+ | `onSearch` / `debounceDuration` | Caller-owned debounced state; filter `items` before passing to `Combobox` |
41
+ | `hideClearAll` / `onClearAll` | No direct equivalent — `ComboboxChip` ships individual removals; a clear-all button is custom UI beside `ComboboxChips` |
42
+ | `hideSelectedOptions` | Filter `items` in consumer state before passing to `Combobox` |
43
+ | `maxMenuHeight` / `maxSearchBoxHeight` | `className` on `ComboboxContent` / `ComboboxChips` (Tailwind `max-h-*`) |
44
+
45
+ ## 2. Imports
46
+
47
+ ```tsx
48
+ // All multi-select combobox primitives from @aircall/ds
49
+ import {
50
+ Combobox,
51
+ ComboboxChip,
52
+ ComboboxChips,
53
+ ComboboxChipsInput,
54
+ ComboboxContent,
55
+ ComboboxEmpty,
56
+ ComboboxItem,
57
+ ComboboxList,
58
+ ComboboxValue,
59
+ useComboboxAnchor
60
+ } from '@aircall/ds';
61
+ ```
62
+
63
+ Note: `ComboboxMultiSelect` is a recipe in `src/recipes/` and is intentionally **not** exported from `@aircall/ds`. Wire the primitives above directly.
64
+
65
+ ## 3. Before / After
66
+
67
+ ### 3a. MultiSearchSelect — dropdown multi-select with search
68
+
69
+ **Before (`@dashboard/library`):**
70
+ ```tsx
71
+ import { MultiSearchSelect, MultiSelectOption } from '@dashboard/library';
72
+
73
+ interface TeamOption extends MultiSelectOption<string> {
74
+ emoji: string;
75
+ }
76
+
77
+ const options: TeamOption[] = [
78
+ { value: 'eng', label: 'Engineering', emoji: '🛠' },
79
+ { value: 'sales', label: 'Sales', emoji: '📈' },
80
+ ];
81
+
82
+ function TeamPicker() {
83
+ const [selected, setSelected] = useState<string[]>(['eng']);
84
+ const [loading, setLoading] = useState(false);
85
+
86
+ const handleSearch = async (query: string) => {
87
+ // fetch options remotely
88
+ };
89
+
90
+ return (
91
+ <MultiSearchSelect<TeamOption>
92
+ options={options}
93
+ loading={loading}
94
+ defaultSelectedKeys={['eng']}
95
+ onSearch={handleSearch}
96
+ onSelect={setSelected}
97
+ renderItemPrefix={(o) => o.emoji}
98
+ texts={{ placeholder: 'Select teams', item: 'team', items: 'teams' }}
99
+ debounceDuration={300}
100
+ />
101
+ );
102
+ }
103
+ ```
104
+
105
+ **After (`@aircall/ds`):**
106
+ ```tsx
107
+ import {
108
+ Combobox,
109
+ ComboboxChip,
110
+ ComboboxChips,
111
+ ComboboxChipsInput,
112
+ ComboboxContent,
113
+ ComboboxEmpty,
114
+ ComboboxItem,
115
+ ComboboxList,
116
+ ComboboxValue,
117
+ useComboboxAnchor
118
+ } from '@aircall/ds';
119
+ import { useMemo, useState, useRef } from 'react';
120
+
121
+ type TeamOption = { value: string; label: string; emoji: string };
122
+
123
+ const allOptions: TeamOption[] = [
124
+ { value: 'eng', label: 'Engineering', emoji: '🛠' },
125
+ { value: 'sales', label: 'Sales', emoji: '📈' },
126
+ ];
127
+
128
+ function TeamPicker() {
129
+ const [selected, setSelected] = useState<TeamOption[]>([allOptions[0]]);
130
+ const [filteredOptions, setFilteredOptions] = useState(allOptions);
131
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
132
+
133
+ const anchor = useComboboxAnchor();
134
+
135
+ const handleSearch = (query: string) => {
136
+ if (debounceRef.current) clearTimeout(debounceRef.current);
137
+ debounceRef.current = setTimeout(async () => {
138
+ // fetch or filter remotely, then update filteredOptions
139
+ setFilteredOptions(
140
+ allOptions.filter((o) => o.label.toLowerCase().includes(query.toLowerCase()))
141
+ );
142
+ }, 300);
143
+ };
144
+
145
+ return (
146
+ <Combobox
147
+ multiple
148
+ autoHighlight
149
+ items={filteredOptions}
150
+ itemToStringValue={(item: TeamOption) => item.value}
151
+ value={selected}
152
+ onValueChange={setSelected}
153
+ >
154
+ <ComboboxChips ref={anchor}>
155
+ <ComboboxValue>
156
+ {(values: TeamOption[]) => (
157
+ <>
158
+ {values.map((o) => (
159
+ <ComboboxChip key={o.value}>{o.label}</ComboboxChip>
160
+ ))}
161
+ <ComboboxChipsInput
162
+ placeholder={selected.length === 0 ? 'Select teams' : ''}
163
+ onChange={(e) => handleSearch(e.target.value)}
164
+ />
165
+ </>
166
+ )}
167
+ </ComboboxValue>
168
+ </ComboboxChips>
169
+ <ComboboxContent anchor={anchor}>
170
+ <ComboboxEmpty>No teams found.</ComboboxEmpty>
171
+ <ComboboxList>
172
+ {(item: TeamOption) => (
173
+ <ComboboxItem key={item.value} value={item}>
174
+ <span>{item.emoji}</span>
175
+ {item.label}
176
+ </ComboboxItem>
177
+ )}
178
+ </ComboboxList>
179
+ </ComboboxContent>
180
+ </Combobox>
181
+ );
182
+ }
183
+ ```
184
+
185
+ Key changes:
186
+ - `options` → `items` on `Combobox`.
187
+ - `selectionMode="multiple"` → `multiple` boolean.
188
+ - `selectedKeys: string[]` → `value: TeamOption[]` (full option objects, not raw keys).
189
+ - `onSelect(keys)` → `onValueChange(options)` — caller must derive keys from `options.map(o => o.value)` if needed downstream.
190
+ - `onSearch` + `debounceDuration` → caller-owned debounced `onChange` on `ComboboxChipsInput`; filter `items` in state.
191
+ - `renderItemPrefix` → inline JSX sibling before the label inside `ComboboxItem`.
192
+ - `texts.placeholder` → `placeholder` on `ComboboxChipsInput`.
193
+ - `loading` → conditional render; DS has no built-in loading state on Combobox.
194
+
195
+ ### 3b. MultiInlineSearchSelect — inline chips with search
196
+
197
+ **Before (`@dashboard/library`):**
198
+ ```tsx
199
+ import { MultiInlineSearchSelect } from '@dashboard/library';
200
+
201
+ type UserOption = { value: string; label: string; email: string };
202
+
203
+ function UserPicker() {
204
+ const [selected, setSelected] = useState<string[]>([]);
205
+ const [options, setOptions] = useState<UserOption[]>(allUsers);
206
+
207
+ const handleSearch = (text: string) => {
208
+ setOptions(allUsers.filter((u) => u.label.includes(text)));
209
+ };
210
+
211
+ return (
212
+ <MultiInlineSearchSelect<UserOption>
213
+ placeholder="Search users..."
214
+ options={options}
215
+ selectedKeys={selected}
216
+ loading={false}
217
+ onSelect={setSelected}
218
+ onSearch={handleSearch}
219
+ renderItemDescription={(o) => o.email}
220
+ emptyLabel="No users found"
221
+ />
222
+ );
223
+ }
224
+ ```
225
+
226
+ **After (`@aircall/ds`):**
227
+ ```tsx
228
+ import {
229
+ Combobox,
230
+ ComboboxChip,
231
+ ComboboxChips,
232
+ ComboboxChipsInput,
233
+ ComboboxContent,
234
+ ComboboxEmpty,
235
+ ComboboxItem,
236
+ ComboboxList,
237
+ ComboboxValue,
238
+ useComboboxAnchor,
239
+ ItemDescription
240
+ } from '@aircall/ds';
241
+
242
+ type UserOption = { value: string; label: string; email: string };
243
+
244
+ function UserPicker() {
245
+ const [selected, setSelected] = useState<UserOption[]>([]);
246
+ const anchor = useComboboxAnchor();
247
+
248
+ // Base UI filters `items` internally as the user types — no manual filter needed
249
+ // for local data. For remote data, see the async pattern in §3a.
250
+ return (
251
+ <Combobox
252
+ multiple
253
+ autoHighlight
254
+ items={allUsers}
255
+ itemToStringValue={(item: UserOption) => item.value}
256
+ value={selected}
257
+ onValueChange={setSelected}
258
+ >
259
+ <ComboboxChips ref={anchor}>
260
+ <ComboboxValue>
261
+ {(values: UserOption[]) => (
262
+ <>
263
+ {values.map((u) => (
264
+ <ComboboxChip key={u.value}>{u.label}</ComboboxChip>
265
+ ))}
266
+ <ComboboxChipsInput
267
+ placeholder={selected.length === 0 ? 'Search users...' : ''}
268
+ />
269
+ </>
270
+ )}
271
+ </ComboboxValue>
272
+ </ComboboxChips>
273
+ <ComboboxContent anchor={anchor}>
274
+ <ComboboxEmpty>No users found</ComboboxEmpty>
275
+ <ComboboxList>
276
+ {(item: UserOption) => (
277
+ <ComboboxItem key={item.value} value={item}>
278
+ {item.label}
279
+ <ItemDescription>{item.email}</ItemDescription>
280
+ </ComboboxItem>
281
+ )}
282
+ </ComboboxList>
283
+ </ComboboxContent>
284
+ </Combobox>
285
+ );
286
+ }
287
+ ```
288
+
289
+ Key changes:
290
+ - `selectedKeys: string[]` controlled prop → `value: UserOption[]` (objects).
291
+ - `onSelect(keys)` → `onValueChange(options)`.
292
+ - `onSearch` (local filtering) → omitted; Base UI filters `items` internally as the user types in `ComboboxChipsInput`. For remote async data, use the debounced fetch pattern from §3a.
293
+ - `renderItemDescription` → `ItemDescription` from `@aircall/ds` as a child of `ComboboxItem`.
294
+ - `emptyLabel` → `ComboboxEmpty` children.
295
+ - `hideSelectedOptions` logic → pre-filter `items` before passing to `Combobox`.
296
+
297
+ ---
298
+
299
+ ## 4. Common mistakes
300
+
301
+ ### Mistake 1 — Using `selectedKeys: string[]` instead of `value: Option[]`
302
+
303
+ Wrong block:
304
+ ```tsx
305
+ <Combobox multiple items={options} selectedKeys={['eng', 'sales']} onSelect={setKeys}>
306
+ ```
307
+
308
+ Correct block:
309
+ ```tsx
310
+ <Combobox
311
+ multiple
312
+ items={options}
313
+ value={[{ value: 'eng', label: 'Engineering' }, { value: 'sales', label: 'Sales' }]}
314
+ onValueChange={setSelected}
315
+ >
316
+ ```
317
+
318
+ `Combobox` from `@aircall/ds` is a Base UI primitive. Its `value` / `onValueChange` carry full option objects, not raw string keys. Passing `selectedKeys` is a no-op (unknown prop silently ignored); the selection state will never be controlled.
319
+
320
+ Source: `packages/ds/src/components/combobox.tsx`
321
+
322
+ ### Mistake 2 — Importing ComboboxMultiSelect from @aircall/ds
323
+
324
+ Wrong block:
325
+ ```tsx
326
+ import { ComboboxMultiSelect } from '@aircall/ds';
327
+
328
+ <ComboboxMultiSelect options={options} placeholder="Select..." />
329
+ ```
330
+
331
+ Correct block:
332
+ ```tsx
333
+ import {
334
+ Combobox,
335
+ ComboboxChips,
336
+ ComboboxChipsInput,
337
+ ComboboxContent,
338
+ ComboboxList,
339
+ ComboboxItem,
340
+ ComboboxValue,
341
+ ComboboxChip,
342
+ useComboboxAnchor
343
+ } from '@aircall/ds';
344
+
345
+ // Wire primitives directly (see §3 above)
346
+ ```
347
+
348
+ `ComboboxMultiSelect` lives in `packages/ds/src/recipes/` and is intentionally excluded from `@aircall/ds`'s public `index.ts`. It is a copy-paste template, not a published export. The import resolves to `undefined` at runtime, crashing silently.
349
+
350
+ Source: `packages/ds/src/components/combobox.tsx`
351
+
352
+ ### Mistake 3 — Rendering `<ComboboxClear>` as a JSX element
353
+
354
+ Wrong block:
355
+ ```tsx
356
+ import { ComboboxClear } from '@aircall/ds';
357
+
358
+ // inside ComboboxChips:
359
+ <ComboboxClear onClick={onClearAll} />
360
+ ```
361
+
362
+ Correct block:
363
+ ```tsx
364
+ // ComboboxClear is type-only in @aircall/ds.
365
+ // Use a plain button for a custom clear-all action:
366
+ <button type="button" onClick={() => setSelected([])}>Clear all</button>
367
+ ```
368
+
369
+ `ComboboxClear` is exported from `packages/ds/src/index.ts` with the `type` modifier (`type ComboboxClear`) — it is a TypeScript type alias, not a React component value. Rendering it as JSX produces a `React.createElement(undefined, …)` call and throws at runtime.
370
+
371
+ Source: `packages/ds/src/components/combobox.tsx`
372
+
373
+ ### Mistake 4 — Passing `onSearch` / `debounceDuration` directly to Combobox
374
+
375
+ Wrong block:
376
+ ```tsx
377
+ <Combobox
378
+ multiple
379
+ items={options}
380
+ onSearch={handleSearch}
381
+ debounceDuration={300}
382
+ >
383
+ ```
384
+
385
+ Correct block:
386
+ ```tsx
387
+ // Debounce in the caller; drive filtering through `items`
388
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
389
+
390
+ const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
391
+ if (debounceRef.current) clearTimeout(debounceRef.current);
392
+ debounceRef.current = setTimeout(() => {
393
+ fetchOrFilter(e.target.value).then(setOptions);
394
+ }, 300);
395
+ };
396
+
397
+ <Combobox multiple items={options} ...>
398
+ <ComboboxChipsInput onChange={handleSearch} />
399
+ ```
400
+
401
+ `Combobox` has no `onSearch` or `debounceDuration` prop. Search and debounce are caller-owned: attach `onChange` to `ComboboxChipsInput`, debounce the callback, and update `items` via state.
402
+
403
+ Source: `packages/ds/src/components/combobox.tsx`
404
+
405
+ ---
406
+
407
+ ## 5. Inside a form — use `FormMultiComboboxField`, not a raw `Combobox`
408
+
409
+ When the multi-select is a **field of an `@aircall/blocks` `useForm`** (the common case
410
+ when migrating a `MultiInlineSearchSelect` that lived in a `FormWizard` / `FormField`),
411
+ don't wire a raw `Combobox` + `useState`. Use `FormMultiComboboxField` from
412
+ `@aircall/blocks` — it owns the value, validation, and error wiring (see
413
+ `aircall-blocks/migrate-dashboard/form-wizard`). It changes three things vs §3:
414
+
415
+ - **The bound field is `string[]` (ids), not option objects.** `FormMultiComboboxField`
416
+ binds `name: DeepKeysOfType<values, string[]>` and its render-prop control bundle gives
417
+ `comboboxProps = { value: string[]; onValueChange: (string[]) => void }`. Spread it onto
418
+ `Combobox` and pass `items={ids}` (string ids) — the inverse of §3's option-object model.
419
+ - **Keep an id→label cache** (`useRef(new Map())`) populated from each server-search page,
420
+ because a selected id may fall outside the current page; chips/options read
421
+ `cache.get(id)?.label ?? id`.
422
+ - **Reshape rich domain types to `string[]` and reconstruct at the submit boundary.** A
423
+ field like `assignees: {id,type}[]` or `callerIdNumbers: {id,phoneNumber}[]` can't bind.
424
+ Store `xIds: string[]` and rebuild the payload in your create/update mapper — either
425
+ collapse to a constant (`assignedUserIds: ids.map(Number)`, all teammates) or reconstruct
426
+ a schema-required richer shape from a lookup map (`ids → {ID, phoneNumber}` from the
427
+ eligibility query, read cache-first at the page level). An already-`string[]` field (e.g.
428
+ `outcomeIds`) needs no reshape.
429
+
430
+ ```tsx
431
+ import { FormMultiComboboxField } from '@aircall/blocks';
432
+ import {
433
+ Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput,
434
+ ComboboxContent, ComboboxEmpty, ComboboxItem, ComboboxList,
435
+ ComboboxValue, useComboboxAnchor,
436
+ } from '@aircall/ds';
437
+
438
+ const [search, setSearch] = useState('');
439
+ const anchor = useComboboxAnchor();
440
+ const { data } = useTeamSearch(search); // server search
441
+ const ids = useMemo(() => (data?.items ?? []).map(t => t.ID), [data]);
442
+ const labels = useRef(new Map<string, string>());
443
+ for (const t of data?.items ?? []) labels.current.set(t.ID, t.name);
444
+
445
+ <FormMultiComboboxField form={form} name="assignedTeamIds" label="Teams">
446
+ {(_field, { comboboxProps, comboboxInputProps }) => (
447
+ <Combobox multiple items={ids} filter={null}
448
+ inputValue={search} onInputValueChange={setSearch} {...comboboxProps}>
449
+ <ComboboxChips ref={anchor}>
450
+ <ComboboxValue>
451
+ {(selected: string[]) => (
452
+ <>
453
+ {selected.map(id => (
454
+ <ComboboxChip key={id}>{labels.current.get(id) ?? id}</ComboboxChip>
455
+ ))}
456
+ <ComboboxChipsInput {...comboboxInputProps} placeholder={selected.length ? '' : 'Search teams'} />
457
+ </>
458
+ )}
459
+ </ComboboxValue>
460
+ </ComboboxChips>
461
+ <ComboboxContent anchor={anchor}>
462
+ <ComboboxEmpty>No teams found</ComboboxEmpty>
463
+ <ComboboxList>
464
+ {(id: string) => (
465
+ <ComboboxItem key={id} value={id}>{labels.current.get(id) ?? id}</ComboboxItem>
466
+ )}
467
+ </ComboboxList>
468
+ </ComboboxContent>
469
+ </Combobox>
470
+ )}
471
+ </FormMultiComboboxField>
472
+ ```
473
+
474
+ `filter={null}` = manual/server search (else Base UI double-filters the server-filtered list).
475
+
476
+ **More form-bound specifics:**
477
+ - **Coloured chips** (e.g. outcomes): cache `{name, color}`, pass the hex to `ComboboxChip`'s
478
+ `legacyColor?: string` (guard null) — `legacyColor={cache.get(id)?.color ?? undefined}`.
479
+ - **Cap at N:** hide options at the limit (`items={isAtLimit ? [] : ids}`, `isAtLimit` from
480
+ `field.state.value.length` in the render-prop) **and** slice any other write path (inline create-and-select).
481
+ - **Switch-gated:** render under `<form.Subscribe selector={s => s.values.flag}>`; clear on
482
+ disable by wrapping `switchProps.onCheckedChange` → `form.setFieldValue('xIds', [])`.
483
+
484
+ > Testing: opening these comboboxes (and toggling the switch) in jsdom needs the selector guard +
485
+ > hidden-checkbox toggle from `aircall-ds/setup`. Without them the popup crashes on open.
486
+
487
+ Source: `packages/blocks/src/form/form-field.tsx`
@@ -0,0 +1,138 @@
1
+ ---
2
+ name: aircall-blocks/migrate-dashboard/copy-button
3
+ description: >
4
+ Migrate @dashboard/library CopyToClipboardButton and CopyToClipboardText to
5
+ @aircall/blocks CopyButton compound. Load when a file imports
6
+ CopyToClipboardButton or CopyToClipboardText from @dashboard/library.
7
+ type: sub-skill
8
+ library: aircall-blocks
9
+ library_version: "0.6.0"
10
+ requires:
11
+ - aircall-blocks/setup
12
+ - aircall-blocks/migrate-dashboard
13
+ sources:
14
+ - "aircall/hydra:packages/blocks/src/index.ts"
15
+ ---
16
+
17
+ This skill builds on aircall-blocks/migrate-dashboard.
18
+
19
+ ## 1. Component mapping
20
+
21
+ | `@dashboard/library` | `@aircall/blocks` |
22
+ | --- | --- |
23
+ | `CopyToClipboardButton` | `CopyButton` (outline variant, icon + label) |
24
+ | `CopyToClipboardText` | `CopyButton` (ghost variant, inline text + icon) |
25
+
26
+ Both map to the same `CopyButton` compound from `@aircall/blocks`. The visual
27
+ difference (button vs. inline text) is controlled by the `variant` prop.
28
+
29
+ ## 2. Verified blocks exports
30
+
31
+ ```
32
+ CopyButton, CopyButtonIcon, CopyButtonLabel
33
+ ```
34
+
35
+ Import from `@aircall/blocks`, not `@aircall/ds`.
36
+
37
+ ## 3. Imports
38
+
39
+ ```tsx
40
+ import { CopyButton, CopyButtonIcon, CopyButtonLabel } from '@aircall/blocks';
41
+ ```
42
+
43
+ ## 4. Prop mapping
44
+
45
+ ### `CopyToClipboardButton`
46
+
47
+ | `CopyToClipboardButton` prop | `CopyButton` equivalent |
48
+ | --- | --- |
49
+ | `content` | `value` prop on `<CopyButton>` |
50
+ | `texts.copy` | `label` on `<CopyButtonLabel>` |
51
+ | `texts.copied` | `copiedLabel` on `<CopyButtonLabel>` |
52
+ | `variant` (button variant) | `variant` on `<CopyButton>` (default: `"outline"`) |
53
+
54
+ ### `CopyToClipboardText`
55
+
56
+ | `CopyToClipboardText` prop | `CopyButton` equivalent |
57
+ | --- | --- |
58
+ | `children` (the text to display) | text node child inside `<CopyButton>` |
59
+ | `tooltipTexts.copy` | _(tooltip not used — CopyButton has no built-in tooltip)_ |
60
+ | content to copy | `value` prop on `<CopyButton>` |
61
+ | inline text appearance | `variant="ghost"` on `<CopyButton>` |
62
+
63
+ ## 5. Before / After examples
64
+
65
+ ### 5a. CopyToClipboardButton → CopyButton (outline)
66
+
67
+ **Before:**
68
+ ```tsx
69
+ import { CopyToClipboardButton } from '@dashboard/library';
70
+
71
+ <CopyToClipboardButton
72
+ content="+33 6 12 34 56 78"
73
+ texts={{ copy: 'Copy number', copied: 'Copied!' }}
74
+ />
75
+ ```
76
+
77
+ **After:**
78
+ ```tsx
79
+ import { CopyButton, CopyButtonIcon, CopyButtonLabel } from '@aircall/blocks';
80
+
81
+ <CopyButton value="+33 6 12 34 56 78">
82
+ <CopyButtonIcon />
83
+ <CopyButtonLabel label="Copy number" copiedLabel="Copied!" />
84
+ </CopyButton>
85
+ ```
86
+
87
+ ### 5b. CopyToClipboardText → CopyButton (ghost, inline)
88
+
89
+ **Before:**
90
+ ```tsx
91
+ import { CopyToClipboardText } from '@dashboard/library';
92
+
93
+ <CopyToClipboardText tooltipTexts={{ copy: 'Copy', copied: 'Copied' }}>
94
+ +33 6 12 34 56 78
95
+ </CopyToClipboardText>
96
+ ```
97
+
98
+ **After:**
99
+ ```tsx
100
+ import { CopyButton, CopyButtonIcon } from '@aircall/blocks';
101
+
102
+ <CopyButton value="+33 6 12 34 56 78" variant="ghost">
103
+ +33 6 12 34 56 78
104
+ <CopyButtonIcon />
105
+ </CopyButton>
106
+ ```
107
+
108
+ > The `tooltipTexts` prop had no functional role in `CopyToClipboardText` beyond
109
+ > tooltip copy — `CopyButton` has no built-in tooltip, so drop it.
110
+
111
+ ## 6. Common mistakes
112
+
113
+ ### Mistake 1: Importing from `@aircall/ds`
114
+
115
+ ```tsx
116
+ // WRONG — CopyButton lives in @aircall/blocks, not @aircall/ds
117
+ import { CopyButton } from '@aircall/ds';
118
+ ```
119
+
120
+ ```tsx
121
+ // CORRECT
122
+ import { CopyButton, CopyButtonIcon, CopyButtonLabel } from '@aircall/blocks';
123
+ ```
124
+
125
+ ### Mistake 2: Passing text content directly without CopyButtonLabel
126
+
127
+ ```tsx
128
+ // WRONG — plain text as children won't update to "Copied!" state
129
+ <CopyButton value="…">Copy number</CopyButton>
130
+ ```
131
+
132
+ ```tsx
133
+ // CORRECT — CopyButtonLabel handles the copy/copied toggle
134
+ <CopyButton value="…">
135
+ <CopyButtonIcon />
136
+ <CopyButtonLabel label="Copy number" copiedLabel="Copied!" />
137
+ </CopyButton>
138
+ ```