@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.
- package/README.md +31 -0
- package/dist/globals.css +1 -1
- package/dist/index.d.ts +94 -33
- package/dist/index.js +292 -42
- package/package.json +16 -3
- 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,277 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aircall-ds/migrate-tractor/button
|
|
3
|
+
description: >
|
|
4
|
+
Migrate Tractor Button and IconButton to the @aircall/ds Button. Load when a file
|
|
5
|
+
imports Button or IconButton from @aircall/tractor. Maps Tractor variant/mode/size
|
|
6
|
+
to the ds Button variant/size (default|outline|secondary|ghost|destructive|link;
|
|
7
|
+
default|sm|lg|icon|icon-sm|icon-lg) and the block prop.
|
|
8
|
+
type: sub-skill
|
|
9
|
+
library: aircall-ds
|
|
10
|
+
library_version: "0.13.0"
|
|
11
|
+
requires:
|
|
12
|
+
- aircall-ds/setup
|
|
13
|
+
- aircall-ds/migrate-tractor
|
|
14
|
+
sources:
|
|
15
|
+
- "aircall/hydra:packages/ds/src/components/button.tsx"
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
This skill builds on aircall-ds/migrate-tractor. Apply all cross-cutting rules from that skill (prop renames, `render` prop, data attributes) before the button-specific steps below.
|
|
19
|
+
|
|
20
|
+
## 1. Component mapping
|
|
21
|
+
|
|
22
|
+
Tractor had two separate components: `Button` (text + optional icon) and `IconButton` (icon-only, square). DS unifies both into a single `Button` component — the `size` prop controls the icon-only square shape.
|
|
23
|
+
|
|
24
|
+
| Tractor component | DS replacement | Notes |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| `<Button>` | `<Button>` | Drop-in with prop renames (see tables below) |
|
|
27
|
+
| `<IconButton component={X}>` | `<Button size="icon"><X /></Button>` | Icon becomes a child; `component` prop is removed |
|
|
28
|
+
|
|
29
|
+
## 2. Verified DS export (`packages/ds/src/index.ts`)
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
Button, buttonVariants
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 3. Imports
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { Button } from '@aircall/ds';
|
|
39
|
+
import { SomeIcon } from '@aircall/react-icons';
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Never import icons from `lucide-react` directly — always use `@aircall/react-icons`.
|
|
43
|
+
|
|
44
|
+
## 4. Variant mapping (Tractor → DS)
|
|
45
|
+
|
|
46
|
+
Tractor had two orthogonal props — `variant` (color role) and `mode` (fill style). DS collapses both into a single `variant` prop. Use the combined (variant × mode) pair to pick the DS variant.
|
|
47
|
+
|
|
48
|
+
| Tractor `variant` | Tractor `mode` | DS `variant` |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| `primary` | _(default / filled)_ | `default` |
|
|
51
|
+
| `primary` | `outline` | `outline` |
|
|
52
|
+
| `secondary` | _(default / filled)_ | `secondary` |
|
|
53
|
+
| `secondary` | `outline` | `outline` |
|
|
54
|
+
| `secondary` | `ghost` | `ghost` |
|
|
55
|
+
| `danger` | _(default / filled)_ | `destructive` |
|
|
56
|
+
| `danger` | `ghost` | `ghost` + `className="text-destructive"` |
|
|
57
|
+
| _(no equivalent)_ | _(no equivalent)_ | `link` |
|
|
58
|
+
|
|
59
|
+
> When both `variant` and `mode` appear on the same Tractor button, derive the DS variant from the **combination**, not from either prop alone. Dropping `mode` without updating `variant` is the most common migration error.
|
|
60
|
+
|
|
61
|
+
> `danger` + `ghost` has no single DS variant: DS `destructive` is filled-only. Map to `ghost` and add `className="text-destructive"` to keep the destructive color — plain `ghost` silently loses it.
|
|
62
|
+
|
|
63
|
+
## 5. Size mapping (Tractor → DS)
|
|
64
|
+
|
|
65
|
+
| Tractor `size` | DS `size` | Note |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| `xSmall` (28px) | `size="default"` (32px) | +4px |
|
|
68
|
+
| `small` (40px) | `size="lg"` (40px) | exact |
|
|
69
|
+
| `regular` (48px, default) | `size="lg"` (40px) | −8px |
|
|
70
|
+
| `large` (56px) | `className="h-14"` | no built-in size |
|
|
71
|
+
| _(icon-only button)_ | `icon` | size-8 |
|
|
72
|
+
| _(icon-only, small)_ | `icon-sm` | size-6 |
|
|
73
|
+
| _(icon-only, large)_ | `icon-lg` | size-10 |
|
|
74
|
+
|
|
75
|
+
## 6. Other prop changes
|
|
76
|
+
|
|
77
|
+
| Tractor prop | DS equivalent | Notes |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| `block` | `block` | Identical — `true` makes the button full-width |
|
|
80
|
+
| `component={X}` (IconButton) | _(removed)_ | Icon becomes a JSX child instead |
|
|
81
|
+
| `isLoading` | _(removed)_ | Render your own spinner as a child |
|
|
82
|
+
| `leftIcon` / `rightIcon` | _(removed)_ | Pass the icon as a child; position is inferred from DOM order |
|
|
83
|
+
| `as` | `render` | Base UI render-prop pattern (see cross-cutting skill) |
|
|
84
|
+
|
|
85
|
+
## 7. Before / After examples
|
|
86
|
+
|
|
87
|
+
### 7a. Standard button (variant + mode → DS variant)
|
|
88
|
+
|
|
89
|
+
**Before (Tractor):**
|
|
90
|
+
```tsx
|
|
91
|
+
import { Button } from '@aircall/tractor';
|
|
92
|
+
|
|
93
|
+
<Button variant="secondary" mode="outline" size="small">
|
|
94
|
+
Cancel
|
|
95
|
+
</Button>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**After (DS):**
|
|
99
|
+
```tsx
|
|
100
|
+
import { Button } from '@aircall/ds';
|
|
101
|
+
|
|
102
|
+
<Button variant="outline" size="lg">
|
|
103
|
+
Cancel
|
|
104
|
+
</Button>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
> Tractor `small` = 40px → DS `size="lg"` = 40px (exact match). Do not use `size="sm"` (32px).
|
|
108
|
+
|
|
109
|
+
### 7b. Primary button with icon child
|
|
110
|
+
|
|
111
|
+
**Before (Tractor):**
|
|
112
|
+
```tsx
|
|
113
|
+
import { Play } from '@aircall/react-icons';
|
|
114
|
+
import { Button } from '@aircall/tractor';
|
|
115
|
+
|
|
116
|
+
<Button variant="primary" size="small" onClick={onLaunch}>
|
|
117
|
+
<Icon component={Play} size={16} />
|
|
118
|
+
Launch
|
|
119
|
+
</Button>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**After (DS):**
|
|
123
|
+
```tsx
|
|
124
|
+
import { Play } from '@aircall/react-icons';
|
|
125
|
+
import { Button } from '@aircall/ds';
|
|
126
|
+
|
|
127
|
+
<Button variant="default" size="lg" onClick={onLaunch}>
|
|
128
|
+
<Play />
|
|
129
|
+
Launch
|
|
130
|
+
</Button>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
> Tractor `small` = 40px → DS `size="lg"` = 40px (exact match).
|
|
134
|
+
|
|
135
|
+
> DS sizes SVGs automatically via `[&_svg:not([class*='size-'])]:size-4`. Do not pass a `size` prop to the icon.
|
|
136
|
+
|
|
137
|
+
### 7c. IconButton → icon-only Button
|
|
138
|
+
|
|
139
|
+
**Before (Tractor):**
|
|
140
|
+
```tsx
|
|
141
|
+
import { MoreVertical } from '@aircall/react-icons';
|
|
142
|
+
import { IconButton } from '@aircall/tractor';
|
|
143
|
+
|
|
144
|
+
<IconButton
|
|
145
|
+
component={MoreVertical}
|
|
146
|
+
aria-label="More actions"
|
|
147
|
+
disabled={isLoading}
|
|
148
|
+
/>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**After (DS):**
|
|
152
|
+
```tsx
|
|
153
|
+
import { MoreVertical } from '@aircall/react-icons';
|
|
154
|
+
import { Button } from '@aircall/ds';
|
|
155
|
+
|
|
156
|
+
<Button size="icon" aria-label="More actions" disabled={isLoading}>
|
|
157
|
+
<MoreVertical />
|
|
158
|
+
</Button>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 7d. Ghost icon-only button (mode + variant combination)
|
|
162
|
+
|
|
163
|
+
**Before (Tractor):**
|
|
164
|
+
```tsx
|
|
165
|
+
import { Pause } from '@aircall/react-icons';
|
|
166
|
+
import { Button } from '@aircall/tractor';
|
|
167
|
+
|
|
168
|
+
<Button mode="ghost" variant="secondary" size="xSmall" aria-label="Pause">
|
|
169
|
+
<Pause />
|
|
170
|
+
</Button>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**After (DS):**
|
|
174
|
+
```tsx
|
|
175
|
+
import { Pause } from '@aircall/react-icons';
|
|
176
|
+
import { Button } from '@aircall/ds';
|
|
177
|
+
|
|
178
|
+
<Button variant="ghost" size="icon-sm" aria-label="Pause">
|
|
179
|
+
<Pause />
|
|
180
|
+
</Button>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 7e. Link-style button using `render` prop
|
|
184
|
+
|
|
185
|
+
**Before (Tractor):**
|
|
186
|
+
```tsx
|
|
187
|
+
import { Button } from '@aircall/tractor';
|
|
188
|
+
import { Link } from 'react-router-dom';
|
|
189
|
+
|
|
190
|
+
<Button as={Link} to="/settings" variant="secondary" mode="ghost">
|
|
191
|
+
Settings
|
|
192
|
+
</Button>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**After (DS):**
|
|
196
|
+
```tsx
|
|
197
|
+
import { Button } from '@aircall/ds';
|
|
198
|
+
import { Link } from 'react-router-dom';
|
|
199
|
+
|
|
200
|
+
<Button variant="ghost" render={<Link to="/settings" />}>
|
|
201
|
+
Settings
|
|
202
|
+
</Button>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## 8. Common mistakes
|
|
206
|
+
|
|
207
|
+
### Mistake 1: Keeping `mode` as a separate prop
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
// WRONG — DS Button has no `mode` prop; it is silently ignored
|
|
211
|
+
<Button variant="secondary" mode="outline">Cancel</Button>
|
|
212
|
+
|
|
213
|
+
// CORRECT — fold mode into variant
|
|
214
|
+
<Button variant="outline">Cancel</Button>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Mechanism:** DS collapses Tractor's orthogonal (variant × mode) space into a single `variant`. Passing `mode` passes an unknown prop that does nothing — the button renders with the `secondary` fill instead of the outline style.
|
|
218
|
+
|
|
219
|
+
`Source: packages/ds/src/components/button.tsx` — `buttonVariants` has no `mode` key.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
### Mistake 2: Icon-only button without an `icon*` size
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
// WRONG — size="default" gives h-8 px-2 padding; the button won't be square
|
|
227
|
+
<Button aria-label="More actions">
|
|
228
|
+
<MoreVertical />
|
|
229
|
+
</Button>
|
|
230
|
+
|
|
231
|
+
// CORRECT — icon-only buttons must use size="icon", "icon-sm", or "icon-lg"
|
|
232
|
+
<Button size="icon" aria-label="More actions">
|
|
233
|
+
<MoreVertical />
|
|
234
|
+
</Button>
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Mechanism:** The `icon` sizes set `size-8` (width AND height), producing a square. Non-icon sizes add horizontal padding and a fixed height only — the button will be wider than it is tall, misaligning the icon visually.
|
|
238
|
+
|
|
239
|
+
`Source: packages/ds/src/components/button.tsx` — `size.icon = 'size-8'`, `size.default = 'h-8 gap-1.5 px-2 …'`.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
### Mistake 3: Using `as` instead of `render` for polymorphic buttons
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
// WRONG — DS Button is Base UI, not Radix; it has no `as` prop
|
|
247
|
+
<Button as={Link} to="/settings" variant="ghost">Settings</Button>
|
|
248
|
+
|
|
249
|
+
// CORRECT — use the Base UI `render` prop
|
|
250
|
+
<Button render={<Link to="/settings" />} variant="ghost">Settings</Button>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Mechanism:** Base UI primitives replaced Radix UI in `@aircall/ds`. The `asChild`/`as` pattern from Radix is gone; Base UI uses a `render` prop that accepts a JSX element. Additional props (like `to`) go on the element passed to `render`, not on `Button` itself.
|
|
254
|
+
|
|
255
|
+
`Source: packages/ds/src/components/button.tsx` — `Button` extends `ButtonPrimitive.Props` from `@base-ui/react/button`, which exposes `render`.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
### Mistake 4: Passing a `size` number to the icon child
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
// WRONG — explicit size prop overrides DS auto-sizing and breaks icon-lg layout
|
|
263
|
+
<Button size="lg">
|
|
264
|
+
<Play size={16} />
|
|
265
|
+
Launch
|
|
266
|
+
</Button>
|
|
267
|
+
|
|
268
|
+
// CORRECT — let DS size the icon automatically
|
|
269
|
+
<Button size="lg">
|
|
270
|
+
<Play />
|
|
271
|
+
Launch
|
|
272
|
+
</Button>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Mechanism:** DS applies `[&_svg:not([class*='size-'])]:size-4` (or `size-6` for `lg`/`icon-lg`) to auto-size SVGs. A `size` attribute on the icon sets an inline `width`/`height` that wins over Tailwind utility classes, locking the icon to the wrong size.
|
|
276
|
+
|
|
277
|
+
`Source: packages/ds/src/components/button.tsx` — base class includes `[&_svg:not([class*='size-'])]:size-4`.
|
|
@@ -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`
|