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