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