@aircall/ds 0.13.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 +94 -33
  4. package/dist/index.js +292 -42
  5. package/package.json +16 -3
  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,248 @@
1
+ ---
2
+ name: aircall-ds/migrate-tractor/gauge
3
+ description: >
4
+ Migrate Tractor Gauge (segmented audio level meter) to @aircall/ds Gauge and
5
+ optionally replace custom audio plumbing with the useAudioGauge hook. Load when
6
+ a file imports Gauge 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/gauge.md"
15
+ ---
16
+
17
+ This skill builds on aircall-ds/migrate-tractor.
18
+
19
+ ## 1. Component mapping
20
+
21
+ | Tractor part | @aircall/ds part | Notes |
22
+ | --- | --- | --- |
23
+ | `Gauge` | `Gauge` | Same name; value scale and segment count changed — see below |
24
+ | `gauges` prop | _(removed)_ | DS gauge is always 8 segments; prop is gone |
25
+ | Custom audio plumbing | `useAudioGauge` | Optional drop-in hook for mic-level meters |
26
+
27
+ ## 2. Verified DS exports (`packages/ds/src/index.ts`)
28
+
29
+ ```ts
30
+ // line 223
31
+ export { Gauge } from './components/gauge';
32
+ // line 358
33
+ export * from './hooks/use-audio-gauge'; // exports: useAudioGauge, UseAudioGaugeOptions, UseAudioGaugeResult
34
+ ```
35
+
36
+ Public names usable as `import { … } from '@aircall/ds'`: `Gauge`, `useAudioGauge`.
37
+
38
+ ## 3. Imports
39
+
40
+ **Before (Tractor):**
41
+ ```tsx
42
+ import { Gauge } from '@aircall/tractor';
43
+ ```
44
+
45
+ **After (DS):**
46
+ ```tsx
47
+ import { Gauge } from '@aircall/ds';
48
+ // When using the built-in mic hook:
49
+ import { Gauge, useAudioGauge } from '@aircall/ds';
50
+ ```
51
+
52
+ ## 4. Prop changes
53
+
54
+ | Tractor prop | DS prop | Action |
55
+ | --- | --- | --- |
56
+ | `value: number` | `value: number` | Rescale to 0–8 range (see §5) |
57
+ | `gauges?: number` | _(removed)_ | Delete the prop — DS always renders 8 segments |
58
+
59
+ The DS `Gauge` also accepts all standard `div` props (`className`, `aria-label`, etc.) except `children`.
60
+
61
+ ## 5. Value scale
62
+
63
+ Tractor's `value` mapped directly to "this many segments filled" out of a configurable `gauges` count. The DS `Gauge` fixes the segment count at 8 and clamps `value` to `[0, 8]`.
64
+
65
+ If your code was computing a raw segment count from a `[0, 1]` level signal, multiply by 8 instead:
66
+
67
+ ```tsx
68
+ // Before — mapped to 16 segments with gauges=16
69
+ const value = Math.trunc(level * 16);
70
+ <Gauge value={value} />
71
+
72
+ // After — DS gauge is always 8 segments; apply ×2 gain to preserve speech sensitivity
73
+ const value = Math.round(level * 8 * 2);
74
+ <Gauge value={value} />
75
+ ```
76
+
77
+ The `× 2` gain (speech calibration) matches the behavior users see today: typical speech reaches ~50% of the mic's digital range, so a pure `× 8` mapping would only nudge 1–2 segments during normal conversation. With `× 2`, loud speech saturates the meter.
78
+
79
+ ## 6. Visual falloff (automatic)
80
+
81
+ The DS gauge adds a soft leading-edge falloff to mimic an analog meter feel:
82
+
83
+ - **Head segment** (topmost filled): 35% opacity
84
+ - **Trailing segment** (one below head): 65% opacity
85
+ - **All other filled segments**: 100% opacity
86
+ - When `value === 8` all segments are fully filled — no falloff
87
+
88
+ This is automatic and not opt-out. No prop is needed or available.
89
+
90
+ ## 7. `useAudioGauge` hook (optional replacement for custom audio plumbing)
91
+
92
+ When the component owns its own microphone connection, replace the custom `getUserMedia` + analyser pipeline with the DS hook. It returns `value` already calibrated to the 0–8 range.
93
+
94
+ ```tsx
95
+ import { Gauge, useAudioGauge } from '@aircall/ds';
96
+
97
+ function MicLevel() {
98
+ const { value, error } = useAudioGauge();
99
+ return (
100
+ <>
101
+ <Gauge value={value} />
102
+ {error && <p>Microphone unavailable: {error.message}</p>}
103
+ </>
104
+ );
105
+ }
106
+ ```
107
+
108
+ Hook options (all optional):
109
+
110
+ | Option | Default | Purpose |
111
+ | --- | --- | --- |
112
+ | `intervalMs` | `16` | Polling interval (~60 Hz) |
113
+ | `fftSize` | `32` | `AnalyserNode.fftSize` — power of 2 in [32, 32768] |
114
+ | `smoothing` | `0.3` | `AnalyserNode.smoothingTimeConstant` (0–1; higher = slower) |
115
+ | `gain` | `2` | Multiplier on the natural 0–8 mapping; `1` = pure linear |
116
+
117
+ If the component receives a `MediaStream` from outside (e.g. from a shared `useAudioInputVolume` hook), keep that hook and only rescale the value — do not replace it with `useAudioGauge`.
118
+
119
+ ## 8. Accessibility
120
+
121
+ The DS gauge renders `role="meter"` with `aria-valuenow`, `aria-valuemin={0}`, and `aria-valuemax={8}` automatically on the root element. No extra wiring needed — Tractor's gauge had no accessibility attributes.
122
+
123
+ ## 9. Styling
124
+
125
+ Pass Tailwind utilities via `className` for layout constraints (e.g. `className="max-w-64"`). The segment colors come from DS tokens (`bg-muted` for empty, `bg-primary` for filled) and are not configurable per-segment.
126
+
127
+ ## 10. Before / After examples
128
+
129
+ ### 10a. Gauge driven by a custom audio hook
130
+
131
+ **Before (Tractor):**
132
+ ```tsx
133
+ import { Gauge } from '@aircall/tractor';
134
+ import { useAudioInputVolume } from '@/utils/audio/useAudioInputVolume';
135
+
136
+ export const InputGauge = () => {
137
+ const level = useAudioInputVolume();
138
+ const value = Math.trunc(level * 16);
139
+ return <Gauge value={value} />;
140
+ };
141
+ ```
142
+
143
+ **After (DS — keep the custom hook, fix the scale):**
144
+ ```tsx
145
+ import { Gauge } from '@aircall/ds';
146
+ import { useAudioInputVolume } from '@/utils/audio/useAudioInputVolume';
147
+
148
+ export const InputGauge = () => {
149
+ const level = useAudioInputVolume();
150
+ // 8 segments × 2 speech-calibration gain = same perceived sensitivity as before
151
+ const value = Math.round(level * 8 * 2);
152
+ return <Gauge value={value} />;
153
+ };
154
+ ```
155
+
156
+ ### 10b. Gauge with a fixed `gauges` count removed
157
+
158
+ **Before (Tractor):**
159
+ ```tsx
160
+ import { Gauge } from '@aircall/tractor';
161
+
162
+ export const SignalStrength = ({ level }: { level: number }) => (
163
+ <Gauge value={Math.trunc(level * 5)} gauges={5} />
164
+ );
165
+ ```
166
+
167
+ **After (DS — remove `gauges`, rescale to 8):**
168
+ ```tsx
169
+ import { Gauge } from '@aircall/ds';
170
+
171
+ export const SignalStrength = ({ level }: { level: number }) => (
172
+ <Gauge value={Math.round(level * 8)} />
173
+ );
174
+ ```
175
+
176
+ ### 10c. Replacing custom audio plumbing with `useAudioGauge`
177
+
178
+ **Before (Tractor):**
179
+ ```tsx
180
+ import { Gauge } from '@aircall/tractor';
181
+ import { useAudioInputVolume } from '@/utils/audio/useAudioInputVolume';
182
+
183
+ export const InputGauge = () => {
184
+ const level = useAudioInputVolume();
185
+ const value = Math.trunc(level * 16);
186
+ return <Gauge value={value} />;
187
+ };
188
+ ```
189
+
190
+ **After (DS — use the built-in hook directly):**
191
+ ```tsx
192
+ import { Gauge, useAudioGauge } from '@aircall/ds';
193
+
194
+ export const InputGauge = () => {
195
+ const { value } = useAudioGauge();
196
+ return <Gauge value={value} />;
197
+ };
198
+ ```
199
+
200
+ ## 11. Common mistakes
201
+
202
+ ### Mistake 1: Keeping the old value scale without rescaling
203
+
204
+ ```tsx
205
+ // WRONG — multiplying by 16 overfills the DS gauge; values past 8 are clamped to 8
206
+ const value = Math.trunc(level * 16);
207
+ <Gauge value={value} />
208
+
209
+ // CORRECT — DS gauge range is 0–8; multiply by 8 (×2 for speech calibration)
210
+ const value = Math.round(level * 8 * 2);
211
+ <Gauge value={value} />
212
+ ```
213
+
214
+ The DS `Gauge` clamps `value` to `[0, 8]` internally. Passing `16` always renders a fully saturated meter — the level meter appears stuck at maximum regardless of the actual mic level.
215
+
216
+ Source: `packages/ds/src/components/gauge.tsx` — `Math.max(0, Math.min(value, GAUGES))` where `GAUGES = 8`.
217
+
218
+ ---
219
+
220
+ ### Mistake 2: Passing the `gauges` prop
221
+
222
+ ```tsx
223
+ // WRONG — DS Gauge has no `gauges` prop; the value is silently spread as a DOM attribute
224
+ <Gauge value={4} gauges={8} />
225
+
226
+ // CORRECT — remove the prop entirely; DS always renders 8 segments
227
+ <Gauge value={4} />
228
+ ```
229
+
230
+ `gauges` is not part of `GaugeProps`. Because `Gauge` spreads remaining props onto the root `<div>`, `gauges={8}` becomes a non-standard HTML attribute in the DOM, triggering a React warning and potentially causing test failures.
231
+
232
+ Source: `packages/ds/src/components/gauge.tsx` — `GaugeProps` extends `Omit<React.ComponentProps<'div'>, 'children'>` with only `value` added; no `gauges` field.
233
+
234
+ ---
235
+
236
+ ### Mistake 3: Expecting to opt out of the leading-edge falloff
237
+
238
+ ```tsx
239
+ // WRONG — there is no prop to disable falloff; this prop is silently ignored
240
+ <Gauge value={5} disableFalloff />
241
+
242
+ // CORRECT — the falloff is automatic and not configurable; no prop is needed
243
+ <Gauge value={5} />
244
+ ```
245
+
246
+ The head/trailing opacity is applied unconditionally via data-attribute CSS (`data-[state=head]:opacity-35`, `data-[state=trailing]:opacity-65`). There is no prop to disable it — accept the falloff or style the segments via a custom `className` on the root only.
247
+
248
+ Source: `packages/ds/src/components/gauge.tsx` — `getSegmentState` always returns `'head'` / `'trailing'` states; no bypass path exists.
@@ -0,0 +1,261 @@
1
+ ---
2
+ name: aircall-ds/migrate-tractor/input
3
+ description: >
4
+ Migrate Tractor TextFieldInput and PasswordInput to @aircall/ds Input, InputGroup,
5
+ InputGroupAddon, InputGroupButton, and InputGroupInput. Load when a file imports
6
+ TextFieldInput, PasswordInput, or InputGroup 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/input.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 input-specific steps below.
18
+
19
+ ## 1. Component mapping
20
+
21
+ | Tractor component | DS replacement | Notes |
22
+ | --- | --- | --- |
23
+ | `<TextFieldInput>` | `<Input>` | Drop-in for plain text inputs |
24
+ | `<PasswordInput>` | `<InputGroup>` composition | No turn-key password component in DS — compose manually |
25
+ | Tractor input with prefix/suffix icon | `<InputGroup>` composition | Use `InputGroupAddon` for decorations |
26
+
27
+ ## 2. Verified DS exports (`packages/ds/src/index.ts`)
28
+
29
+ ```
30
+ Input,
31
+ InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput,
32
+ InputGroupText, InputGroupTextarea
33
+ ```
34
+
35
+ All names used in the Before/After sections below exist in the published public API.
36
+
37
+ ## 3. Imports
38
+
39
+ **Plain input:**
40
+ ```tsx
41
+ import { Input } from '@aircall/ds';
42
+ ```
43
+
44
+ **InputGroup composition (password, prefix/suffix):**
45
+ ```tsx
46
+ import {
47
+ InputGroup,
48
+ InputGroupAddon,
49
+ InputGroupButton,
50
+ InputGroupInput,
51
+ } from '@aircall/ds';
52
+ import { VisibilityOffFill, VisibilityOnFill } from '@aircall/react-icons';
53
+ ```
54
+
55
+ Never import icons from `lucide-react` directly — always use `@aircall/react-icons`.
56
+
57
+ ## 4. Prop renames (TextFieldInput → Input)
58
+
59
+ | Tractor prop | DS prop | Notes |
60
+ | --- | --- | --- |
61
+ | `size="regular"` / `size="small"` | _(removed)_ | DS `Input` is always 40px; drop the prop entirely |
62
+ | `validationStatus="error"` | `aria-invalid={true}` | DS reads ARIA attributes for error styling |
63
+ | `value` | `value` | Unchanged |
64
+ | `onChange` | `onChange` | Unchanged |
65
+ | `placeholder` | `placeholder` | Unchanged |
66
+ | `type` | `type` | Unchanged |
67
+ | `disabled` | `disabled` | Unchanged |
68
+ | `readOnly` | `readOnly` | Unchanged |
69
+ | `required` | `required` | Unchanged |
70
+ | `autoFocus` | `autoFocus` | Unchanged |
71
+
72
+ ## 5. Before / After examples
73
+
74
+ ### 5a. Plain text input
75
+
76
+ **Before (Tractor):**
77
+ ```tsx
78
+ import { TextFieldInput } from '@aircall/tractor';
79
+
80
+ <TextFieldInput
81
+ type="email"
82
+ placeholder="Enter email"
83
+ value={value}
84
+ onChange={onChange}
85
+ validationStatus="error"
86
+ size="regular"
87
+ required
88
+ />
89
+ ```
90
+
91
+ **After (DS):**
92
+ ```tsx
93
+ import { Input } from '@aircall/ds';
94
+
95
+ <Input
96
+ type="email"
97
+ placeholder="Enter email"
98
+ value={value}
99
+ onChange={onChange}
100
+ aria-invalid={true}
101
+ required
102
+ />
103
+ ```
104
+
105
+ > The `size` prop is dropped — DS `Input` is always 40px. The `validationStatus="error"` prop becomes `aria-invalid={true}`; DS picks up the red-border error styling from the ARIA attribute automatically.
106
+
107
+ ### 5b. PasswordInput → InputGroup composition
108
+
109
+ Tractor's `PasswordInput` shipped a built-in eye-toggle button. DS has no turn-key password input; compose one with `InputGroup`, `InputGroupInput`, `InputGroupAddon`, and `InputGroupButton`.
110
+
111
+ **Before (Tractor):**
112
+ ```tsx
113
+ import { PasswordInput } from '@aircall/tractor';
114
+
115
+ <PasswordInput
116
+ id="password"
117
+ value={password}
118
+ onChange={onChange}
119
+ placeholder="Enter password"
120
+ />
121
+ ```
122
+
123
+ **After (DS):**
124
+ ```tsx
125
+ import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '@aircall/ds';
126
+ import { VisibilityOffFill, VisibilityOnFill } from '@aircall/react-icons';
127
+ import { useState } from 'react';
128
+
129
+ function PasswordInput({ id, ...props }: React.ComponentProps<typeof InputGroupInput>) {
130
+ const [shown, setShown] = useState(false);
131
+
132
+ return (
133
+ <InputGroup>
134
+ <InputGroupInput
135
+ id={id}
136
+ type={shown ? 'text' : 'password'}
137
+ {...props}
138
+ />
139
+ <InputGroupAddon align="inline-end">
140
+ <InputGroupButton
141
+ size="icon-xs"
142
+ onClick={() => setShown(s => !s)}
143
+ aria-label={shown ? 'Hide password' : 'Show password'}
144
+ >
145
+ {shown ? <VisibilityOffFill className="size-4" /> : <VisibilityOnFill className="size-4" />}
146
+ </InputGroupButton>
147
+ </InputGroupAddon>
148
+ </InputGroup>
149
+ );
150
+ }
151
+ ```
152
+
153
+ > `InputGroupInput` is the borderless `Input` variant — the border lives on the surrounding `InputGroup`, so there is no double frame.
154
+
155
+ ### 5c. Input with a prefix icon (search, currency, etc.)
156
+
157
+ **Before (Tractor):**
158
+ ```tsx
159
+ import { TextFieldInput } from '@aircall/tractor';
160
+ import { SearchOutline } from '@aircall/react-icons';
161
+
162
+ <div style={{ position: 'relative' }}>
163
+ <SearchOutline style={{ position: 'absolute', left: 8, top: 12 }} />
164
+ <TextFieldInput placeholder="Search…" style={{ paddingLeft: 28 }} />
165
+ </div>
166
+ ```
167
+
168
+ **After (DS):**
169
+ ```tsx
170
+ import { InputGroup, InputGroupAddon, InputGroupInput } from '@aircall/ds';
171
+ import { SearchOutline } from '@aircall/react-icons';
172
+
173
+ <InputGroup>
174
+ <InputGroupAddon align="inline-start">
175
+ <SearchOutline className="size-4 text-muted-foreground" />
176
+ </InputGroupAddon>
177
+ <InputGroupInput placeholder="Search…" />
178
+ </InputGroup>
179
+ ```
180
+
181
+ > No manual positioning or padding overrides needed — `InputGroupAddon` with `align="inline-start"` shifts the inner input's left padding automatically via a CSS `:has()` rule on the group.
182
+
183
+ ## 6. Common Mistakes
184
+
185
+ ### Mistake 1 — Keeping `validationStatus="error"` instead of `aria-invalid`
186
+
187
+ ```tsx
188
+ // Wrong — DS Input has no validationStatus prop; the red border never appears
189
+ <Input value={value} onChange={onChange} validationStatus="error" />
190
+
191
+ // Correct — error styling is driven by the ARIA attribute
192
+ <Input value={value} onChange={onChange} aria-invalid={true} />
193
+ ```
194
+
195
+ DS `Input` does not accept a `validationStatus` prop — it is silently ignored. The error border and focus-ring color are controlled by `aria-invalid`; without it the field looks visually valid even when it is not.
196
+
197
+ Source: `packages/ds/src/components/input.tsx` — `inputVariants` keys on `aria-invalid` via `aria-invalid:border-destructive`.
198
+
199
+ ---
200
+
201
+ ### Mistake 2 — Using `<Input>` inside `<InputGroup>` instead of `<InputGroupInput>`
202
+
203
+ ```tsx
204
+ // Wrong — produces a double border: one from Input, one from InputGroup
205
+ <InputGroup>
206
+ <Input type="password" placeholder="Enter password" />
207
+ <InputGroupAddon align="inline-end">…</InputGroupAddon>
208
+ </InputGroup>
209
+
210
+ // Correct — InputGroupInput strips its own border so the group border shows once
211
+ <InputGroup>
212
+ <InputGroupInput type="password" placeholder="Enter password" />
213
+ <InputGroupAddon align="inline-end">…</InputGroupAddon>
214
+ </InputGroup>
215
+ ```
216
+
217
+ `InputGroupInput` renders a borderless, shadow-free, background-transparent `Input` (`border-0 shadow-none bg-transparent`). Using the plain `Input` inside an `InputGroup` causes a visible double frame because the regular `Input` keeps its own `border-input` border on top of the group's border.
218
+
219
+ Source: `packages/ds/src/components/input-group.tsx` — `InputGroupInput` applies `border-0 shadow-none bg-transparent` via `cn`.
220
+
221
+ ---
222
+
223
+ ### Mistake 3 — Dropping `size` and forgetting to remove it when it's passed as a variable
224
+
225
+ ```tsx
226
+ // Wrong — a `size` prop on DS Input is passed to the underlying <input> element
227
+ // and renders as an HTML attribute that controls the visible character width
228
+ <Input size={fieldSize} value={value} onChange={onChange} />
229
+
230
+ // Correct — remove the size prop entirely; DS Input is always 40px
231
+ <Input value={value} onChange={onChange} />
232
+ ```
233
+
234
+ DS `Input` extends `React.ComponentProps<'input'>`, so an unrecognized `size` prop flows through to the DOM `<input size="...">` attribute, which controls the visible character width in some browsers — not a style variant. Drop any dynamic `size` variable inherited from Tractor code.
235
+
236
+ Source: `packages/ds/src/components/input.tsx` — `Input` spreads `...props` onto `<input>` with no `size` interception.
237
+
238
+ ---
239
+
240
+ ### Mistake 4 — Placing a decorative icon directly inside `<InputGroup>` instead of inside `<InputGroupAddon>`
241
+
242
+ ```tsx
243
+ // Wrong — a bare icon as a direct child of InputGroup receives no positioning,
244
+ // padding, or cursor-text click-to-focus behavior
245
+ <InputGroup>
246
+ <SearchOutline className="size-4" />
247
+ <InputGroupInput placeholder="Search…" />
248
+ </InputGroup>
249
+
250
+ // Correct — wrap the icon in InputGroupAddon with the correct align value
251
+ <InputGroup>
252
+ <InputGroupAddon align="inline-start">
253
+ <SearchOutline className="size-4 text-muted-foreground" />
254
+ </InputGroupAddon>
255
+ <InputGroupInput placeholder="Search…" />
256
+ </InputGroup>
257
+ ```
258
+
259
+ `InputGroup` is a flex container that relies on `data-align` set by `InputGroupAddon` to apply the correct inner padding to the sibling `InputGroupInput` via a CSS `:has()` rule. A bare icon child is not an `InputGroupAddon` and sets no `data-align`, so the input's padding is not adjusted and the icon overlaps the text.
260
+
261
+ Source: `packages/ds/src/components/input-group.tsx` — `has-[>[data-align=inline-start]]:[&>input]:pl-2` depends on `data-align` from `InputGroupAddon`.