@aircall/blocks 0.6.0 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aircall/blocks",
3
- "version": "0.6.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "sideEffects": [
@@ -16,7 +16,8 @@
16
16
  "files": [
17
17
  "dist",
18
18
  "README.md",
19
- "package.json"
19
+ "package.json",
20
+ "skills"
20
21
  ],
21
22
  "exports": {
22
23
  ".": {
@@ -40,6 +41,10 @@
40
41
  "optional": true
41
42
  }
42
43
  },
44
+ "dependencies": {
45
+ "react-markdown": "9.0.3",
46
+ "remark-gfm": "4.0.1"
47
+ },
43
48
  "devDependencies": {
44
49
  "@aircall/react-icons": "*",
45
50
  "@aircall/tsconfig": "1.4.3",
@@ -55,6 +60,7 @@
55
60
  "@storybook/react-vite": "10.3.3",
56
61
  "@tailwindcss/cli": "4.1.18",
57
62
  "@tailwindcss/postcss": "4.1.18",
63
+ "@tanstack/intent": "0.1.1",
58
64
  "@tanstack/react-form": "^1.33.0",
59
65
  "@types/node": "24",
60
66
  "@types/react": "19",
@@ -75,9 +81,17 @@
75
81
  "vite": "7.3.1",
76
82
  "vitest": "4.0.17",
77
83
  "zod": "4.4.3",
78
- "@aircall/ds": "0.14.0",
84
+ "@aircall/ds": "0.16.1",
79
85
  "@aircall/hooks": "0.5.1"
80
86
  },
87
+ "keywords": [
88
+ "tanstack-intent"
89
+ ],
90
+ "repository": {
91
+ "type": "git",
92
+ "url": "git+https://gitlab.com/aircall/shared/hydra.git",
93
+ "directory": "packages/blocks"
94
+ },
81
95
  "scripts": {
82
96
  "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
83
97
  "build:js": "tsdown --config ./tsdown.config.ts",
@@ -0,0 +1,127 @@
1
+ ---
2
+ name: aircall-blocks/migrate-dashboard
3
+ description: >
4
+ Migrate a file from @dashboard/library to @aircall/blocks / @aircall/ds. Load FIRST
5
+ to get the full component-level routing table before picking a recipe. Identifies what
6
+ has a blocks/ds equivalent, what is out of scope, and which sub-skill to load next.
7
+ type: core
8
+ library: aircall-blocks
9
+ library_version: "0.5.1"
10
+ requires:
11
+ - aircall-blocks/setup
12
+ sources:
13
+ - "aircall/hydra:docs/migration-guides/dashboard-lib-to-blocks/AGENTS.md"
14
+ ---
15
+
16
+ # Migrate @dashboard/library → @aircall/blocks / @aircall/ds
17
+
18
+ This is the overview router for the `@dashboard/library` migration. Load it first to
19
+ identify the right sub-skill for each component you are migrating, and to determine what
20
+ falls outside the scope of blocks and ds entirely.
21
+
22
+ ## How to run this migration (end-to-end)
23
+
24
+ Migrate incrementally — one screen/file at a time, shipping each green:
25
+
26
+ 0. **Set up once** — load `@aircall/blocks#aircall-blocks/setup` (it builds on
27
+ `@aircall/ds#aircall-ds/setup`): install `@aircall/blocks` + `@aircall/ds`, import both
28
+ `globals.css` bundles, and mount the DS root providers — including `DsI18nProvider`
29
+ (under your react-i18next provider, fed the user's language; importing `@aircall/blocks`
30
+ registers a `blocks` i18n namespace, so it needs DS's i18n engine) and
31
+ `NotificationQueueProvider` if you use banners. Add the jsdom test shims from setup.
32
+ (Loaded automatically via `requires`, but do the wiring first.)
33
+ 1. **Inventory** — list the file's `@dashboard/library` imports. The routing table below
34
+ says which have a blocks/ds equivalent and which are out of scope (hooks/utils/constants
35
+ don't migrate — leave them on `@dashboard/library`).
36
+ 2. **Migrate** — load the per-area recipe from the routing table for each UI component. Any
37
+ Tractor primitives in the same file migrate via `@aircall/ds#aircall-ds/migrate-tractor`;
38
+ icons via `@aircall/ds#aircall-ds/migrate-icons`.
39
+ 3. **Verify green** — `tsc --noEmit`, tests (DS popups/Switch need the setup jsdom shims),
40
+ biome/lint.
41
+ 4. **Repeat** until no in-scope `@dashboard/library` UI imports remain.
42
+
43
+ ## Cross-cutting note
44
+
45
+ `@dashboard/library` mixes UI components with non-UI utilities. Only the UI components
46
+ migrate. When writing replacement code:
47
+
48
+ - Import block-level compositions (page layout, empty states, form layer) from
49
+ `'@aircall/blocks'`.
50
+ - Import DS primitives (Card, Spinner, DataTable, Combobox, ItemGroup, etc.) from
51
+ `'@aircall/ds'`.
52
+ - Never mix the two import paths for the same logical component — pick the package that
53
+ owns it per the table below.
54
+
55
+ ## Forms — never local state
56
+
57
+ Any form that collects and submits data migrates to **`@aircall/blocks` `useForm` + the
58
+ `Form*Field` wrappers** (the Storybook-proven `CommonForm` pattern) — NOT React
59
+ `useState` per field, and NOT bare ds `Field`/`Input` primitives wired by hand. The form
60
+ owns field state, validation, dirty/`canSubmit`/`isSubmitting`, and error display; those
61
+ are what drive `SubmitButton`/`CardSaveBar`. Hand-rolled `useState` bypasses all of it and
62
+ must be rewritten, not preserved, during migration. (`useState` for non-field UI — open,
63
+ active tab — is fine.) Converting a large legacy `useState` form is a deliberate refactor,
64
+ not a 1:1 swap. See `…/migrate-dashboard/form-wizard` and `@aircall/ds#aircall-ds/migrate-tractor/form-and-field`.
65
+
66
+ ## Out of scope
67
+
68
+ The following `@dashboard/library` exports do NOT have an equivalent in `@aircall/blocks`
69
+ or `@aircall/ds`. Do not try to map them to a DS component. They need a shared
70
+ utils/data home or must stay in the consuming app.
71
+
72
+ **Hooks**: `useGraphQuery`, `useGraphMutation`, `useToast`, `useToggle`,
73
+ `useBroadcastChannel`
74
+
75
+ **Helpers / utils**: `generateRandomUUID`, `isTruthy`, `toFixedNumber`,
76
+ `capitalizeFirstLetter`, `getInitials`
77
+
78
+ **Constants, types, and contexts**: `ROLE_NAME`, `RESOURCE`,
79
+ `NavigationBlockerProvider`, `ClientError`
80
+
81
+ **UI with no blocks/ds equivalent yet**: some `@dashboard/library` UI components have
82
+ no target in `@aircall/blocks` or `@aircall/ds` yet (e.g. `PieChart` and other charts,
83
+ `AudioPlayer`, `TileLegend`). If a UI component is not in the routing table below and
84
+ not listed above, leave it imported from `@dashboard/library` for now — do not force a
85
+ migration or invent a target.
86
+
87
+ ## Component routing table
88
+
89
+ | @dashboard/library | Target (pkg) | Recipe to load | Status |
90
+ |---|---|---|---|
91
+ | `MessageScreen` family + `UnknownError` | `ComingSoonEmptyState`, `RestrictedAccessEmptyState`, `NotFoundEmptyState`, `NoDataEmptyState`, `NoSupportEmptyState`, `UnknownErrorEmptyState` and the `EmptyState*` parts (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/empty-states | available |
92
+ | `PageHeader` | `DashboardPageHeader` (+ `DashboardPageHeaderTitle` / `DashboardPageHeaderActions` / `DashboardPageHeaderAction` / `DashboardPageHeaderNav` / `DashboardPageHeaderDescription`) (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/page-header | available |
93
+ | Page / screen shell — incl. full-page flows (Campaign Creation, Add Contacts) | `DashboardPage` (standard: sidebar + header + tabs + content) / `DashboardStandalonePage` (full-page, no sidebar) (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/dashboard-page | available |
94
+ | `Paper` | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`, `CardAction` (and @aircall/blocks `CardSaveBar` for a save bar) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/card | available |
95
+ | `LoadMoreTable` | `DataTable` (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/data-table | available |
96
+ | `MultiSearchSelect` + `MultiInlineSearchSelect` + `MultiSelectOption` | `Combobox` with `multiple` (multi-select) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/combobox | available |
97
+ | `SingleSearchSelect` / `SearchSelect` / single-value `MultiSelect` | `Combobox` WITHOUT `multiple` (single-select) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/combobox | available |
98
+ | `FormWizard` + `useFormWizard` + `FormField` | `useForm`, `CommonForm`, `FormInputField`/`FormSelectField`/`FormComboboxField`/etc., `CardSaveBar`/`SaveBar` (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/form-wizard | available |
99
+ | `Loading` | `Spinner` (and `Skeleton` for content placeholders) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/loading | available |
100
+ | `List` + `ListItem` | `ItemGroup`, `Item`, `ItemMedia`, `ItemContent`, `ItemActions` (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/list | available |
101
+ | `Tile` + `TileHeader` + `TileValue` | `Card` (+ parts) as the tile container (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/tile | available |
102
+ | `GridLayout` + `GridItem` + `Gap` | native `<div>` + Tailwind grid/flex/gap utilities (NO component import needed — these are layout primitives) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/layout | available |
103
+ | `InfoPopup` + `InfoPopupTrigger` + `InfoPopupContent` | `HoverCard`, `HoverCardTrigger`, `HoverCardContent` (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/info-popup | available |
104
+ | `CopyToClipboardButton` + `CopyToClipboardText` | `CopyButton`, `CopyButtonIcon`, `CopyButtonLabel` (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/copy-button | available |
105
+ | `ToggleRow` | `Field` (orientation="horizontal") + `Switch` + `FieldLabel` + optional `FieldDescription` (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/toggle-row | available |
106
+ | `AddButton` + `SaveButton` + `LoadingButton` | `Button` (@aircall/ds) — `AddButton` → `Button` with leading icon; `LoadingButton` loading state → `Button disabled` + `Spinner`; `SaveButton` saved state → `Button variant="outline"` + check icon | load @aircall/ds#aircall-ds/migrate-tractor/button | available |
107
+ | `ConditionalTooltip` | Conditional JSX: wrap children in `<Tooltip>` / `<TooltipTrigger>` / `<TooltipContent>` when condition is true, render children bare when false — no wrapper component needed (@aircall/ds) | inline — no sub-skill | available |
108
+ | `RadioBoxGroup` + `RadioBox` | `RadioGroup` + `RadioGroupItem` wrapped in `Field` + `FieldLabel` + `FieldContent` (@aircall/ds) | inline — see @aircall/ds#aircall-ds/migrate-tractor/form-and-field for Field patterns | available |
109
+ | `TagBeautified` | `Badge` with `legacyColor` prop for stored hex colors; `Badge` with `color` + `tone` props for new tags (@aircall/ds) | inline — `<Badge legacyColor="#0761b5">Sales</Badge>` | available |
110
+ | `RolesTags` | `RoleBadge` with `role` prop (`"owner"` \| `"admin"` \| `"supervisor"` \| `"agent"`) (@aircall/blocks) | inline — `<RoleBadge role="admin" />` | available |
111
+ | `Count` | `CounterBadge` — pass the capped value as children: `<CounterBadge>{n > 99 ? '99+' : n}</CounterBadge>` (@aircall/ds) | inline — no size or max props | available |
112
+ | `Avatar` | `Avatar`, `AvatarImage`, `AvatarFallback` compound (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/avatar | available |
113
+ | `ProgressBar` + `OptimisticProgressBar` | `Progress` with `value` prop (0–100) (@aircall/ds) | inline — `<Progress value={60} />` | available |
114
+ | `ShadowScrollContainer` | `ScrollArea` with `scrollFade` prop — use a fixed `h-[…]` not `max-h-[…]` on the root (@aircall/ds) | inline — `<ScrollArea scrollFade className="h-[400px] rounded-md border">` | available |
115
+ | `AccordionSection` | `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent` (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/accordion | available |
116
+ | `ConfirmationModal` | `AlertDialog` and its parts (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/dialog | available |
117
+ | `Tab` | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/tabs | available |
118
+ | `Skeleton` + `SkeletonText` | `Skeleton` (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/skeleton | available |
119
+
120
+ ## How to use this router
121
+
122
+ 1. Identify the `@dashboard/library` export(s) in the file you are migrating.
123
+ 2. Check the **Out of scope** section first — if the export is listed there, skip it.
124
+ 3. For each UI component, find the matching row in the table above and load the
125
+ indicated recipe sub-skill.
126
+ 4. Each sub-skill is self-contained: it carries the full prop mapping, common mistakes,
127
+ and import paths for its component family.
@@ -0,0 +1,364 @@
1
+ ---
2
+ name: aircall-blocks/migrate-dashboard/card
3
+ description: >
4
+ Migrate @dashboard/library Paper and PaperForm to @aircall/ds Card primitives
5
+ (Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter)
6
+ and @aircall/blocks CardSaveBar for save/discard bars. Load when a file imports
7
+ Paper or PaperForm from @dashboard/library.
8
+ type: sub-skill
9
+ library: aircall-blocks
10
+ library_version: "0.5.1"
11
+ requires:
12
+ - aircall-blocks/setup
13
+ - aircall-blocks/migrate-dashboard
14
+ sources:
15
+ - "aircall/hydra:packages/ds/src/index.ts"
16
+ ---
17
+
18
+ This skill builds on aircall-blocks/migrate-dashboard.
19
+
20
+ ## 1. Component mapping
21
+
22
+ | @dashboard/library | @aircall/ds / @aircall/blocks |
23
+ | --- | --- |
24
+ | `Paper` (root surface) | `Card` (`@aircall/ds`) |
25
+ | `Paper` `title` prop | `CardTitle` inside `CardHeader` (`@aircall/ds`) |
26
+ | `Paper` `subtitle` prop | `CardDescription` inside `CardHeader` (`@aircall/ds`) |
27
+ | `Paper` `titleSide` prop | `CardAction` inside `CardHeader` (`@aircall/ds`) |
28
+ | `Paper` `children` (body) | `CardContent` (`@aircall/ds`) |
29
+ | `Paper` `footer` prop (generic) | `CardFooter` (`@aircall/ds`) |
30
+ | `Paper` `footer` prop (save/discard bar) | `CardSaveBar` (`@aircall/blocks`) |
31
+ | `PaperForm` (form wrapper with save bar) | `Card` + `useForm` + `CardSaveBar` (`@aircall/blocks`) |
32
+ | `Paper` `banner` prop | Inline `Banner` before `CardHeader` inside `Card` (`@aircall/ds`) |
33
+ | `Paper` `disabled` / `disabledText` props | `className="opacity-50 cursor-not-allowed"` on `Card` + custom label |
34
+ | `Paper` `fluid` prop | `className="w-full"` on `CardContent` (default is already full-width) |
35
+
36
+ `PaperForm` is a compound: it wires a `react-final-form` form, renders `Paper`, and
37
+ appends a `SaveBar` footer. The replacement is `Card` with TanStack Form (`useForm` from
38
+ `@aircall/blocks`) and `CardSaveBar` in a `CardFooter`.
39
+
40
+ ## 2. Imports
41
+
42
+ ```tsx
43
+ // DS primitives — structural card shell
44
+ import {
45
+ Card,
46
+ CardAction,
47
+ CardContent,
48
+ CardDescription,
49
+ CardFooter,
50
+ CardHeader,
51
+ CardTitle,
52
+ Banner,
53
+ BannerTitle,
54
+ BannerDescription
55
+ } from '@aircall/ds';
56
+
57
+ // blocks — TanStack Form + animated save bar
58
+ import { useForm, CardSaveBar } from '@aircall/blocks';
59
+ ```
60
+
61
+ ## 3. Before / After
62
+
63
+ ### 3a. Basic Paper — read-only surface
64
+
65
+ **Before (`@dashboard/library`):**
66
+ ```tsx
67
+ import { Paper } from '@dashboard/library';
68
+
69
+ function SettingsSection() {
70
+ return (
71
+ <Paper
72
+ title="General settings"
73
+ subtitle="Manage your workspace preferences."
74
+ >
75
+ <p>Section content here.</p>
76
+ </Paper>
77
+ );
78
+ }
79
+ ```
80
+
81
+ **After (`@aircall/ds`):**
82
+ ```tsx
83
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@aircall/ds';
84
+
85
+ function SettingsSection() {
86
+ return (
87
+ <Card>
88
+ <CardHeader>
89
+ <CardTitle>General settings</CardTitle>
90
+ <CardDescription>Manage your workspace preferences.</CardDescription>
91
+ </CardHeader>
92
+ <CardContent>
93
+ <p>Section content here.</p>
94
+ </CardContent>
95
+ </Card>
96
+ );
97
+ }
98
+ ```
99
+
100
+ Key changes:
101
+ - `Paper` `title` → `CardTitle` inside `CardHeader`.
102
+ - `Paper` `subtitle` → `CardDescription` inside `CardHeader`.
103
+ - `Paper` `children` → wrapped in `CardContent`.
104
+ - Drop `BoxProps` spread (`maxWidth`, `borderColor`, etc.) — `Card` owns the surface.
105
+
106
+ ### 3b. Paper with a header action (titleSide)
107
+
108
+ **Before (`@dashboard/library`):**
109
+ ```tsx
110
+ import { Paper } from '@dashboard/library';
111
+ import { Button } from '@aircall/tractor';
112
+
113
+ function IntegrationCard() {
114
+ return (
115
+ <Paper
116
+ title="Integrations"
117
+ subtitle="Connect your tools."
118
+ titleSide={<Button variant="primary" size="small">Add integration</Button>}
119
+ >
120
+ <p>Integration list here.</p>
121
+ </Paper>
122
+ );
123
+ }
124
+ ```
125
+
126
+ **After (`@aircall/ds`):**
127
+ ```tsx
128
+ import { Button, Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from '@aircall/ds';
129
+
130
+ function IntegrationCard() {
131
+ return (
132
+ <Card>
133
+ <CardHeader>
134
+ <CardTitle>Integrations</CardTitle>
135
+ <CardDescription>Connect your tools.</CardDescription>
136
+ <CardAction>
137
+ <Button variant="default" size="sm">Add integration</Button>
138
+ </CardAction>
139
+ </CardHeader>
140
+ <CardContent>
141
+ <p>Integration list here.</p>
142
+ </CardContent>
143
+ </Card>
144
+ );
145
+ }
146
+ ```
147
+
148
+ `CardAction` must be inside `CardHeader` — the header grid (`grid-cols-[1fr_auto]`) positions it top-right automatically.
149
+
150
+ ### 3c. PaperForm — form with save/discard bar
151
+
152
+ **Before (`@dashboard/library`):**
153
+ ```tsx
154
+ import { PaperForm } from '@dashboard/library';
155
+
156
+ function ProfileForm() {
157
+ return (
158
+ <PaperForm
159
+ title="Profile"
160
+ subtitle="Update your display name."
161
+ formProps={{
162
+ initialValues: { name: '' },
163
+ onSubmit: async (values) => { /* submit */ }
164
+ }}
165
+ submitButtonText="Save"
166
+ undoButtonText="Discard"
167
+ >
168
+ {({ values }) => (
169
+ <input name="name" defaultValue={values.name} />
170
+ )}
171
+ </PaperForm>
172
+ );
173
+ }
174
+ ```
175
+
176
+ **After (`@aircall/blocks`):**
177
+ ```tsx
178
+ import { useForm, CardSaveBar } from '@aircall/blocks';
179
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@aircall/ds';
180
+
181
+ const form = useForm({
182
+ defaultValues: { name: '' },
183
+ onSubmit: async ({ value }) => { /* submit */ }
184
+ });
185
+
186
+ function ProfileForm() {
187
+ return (
188
+ <form.AppForm>
189
+ <form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }}>
190
+ <Card className="overflow-hidden pb-0">
191
+ <CardHeader>
192
+ <CardTitle>Profile</CardTitle>
193
+ <CardDescription>Update your display name.</CardDescription>
194
+ </CardHeader>
195
+ <CardContent>
196
+ <form.AppField name="name">
197
+ {(field) => (
198
+ <input
199
+ value={field.state.value}
200
+ onChange={(e) => field.handleChange(e.target.value)}
201
+ />
202
+ )}
203
+ </form.AppField>
204
+ </CardContent>
205
+ <CardFooter className="p-0">
206
+ <CardSaveBar submitLabel="Save" resetLabel="Discard" />
207
+ </CardFooter>
208
+ </Card>
209
+ </form>
210
+ </form.AppForm>
211
+ );
212
+ }
213
+ ```
214
+
215
+ Key changes:
216
+ - `PaperForm` `formProps` + `react-final-form` render-prop → `useForm` from `@aircall/blocks` (TanStack Form).
217
+ - `PaperForm` `footer` (implicit `SaveBar`) → `CardSaveBar` inside `CardFooter`.
218
+ - Add `className="overflow-hidden pb-0"` to `Card` so the animated save bar's border sits flush at the card edge.
219
+ - Add `className="p-0"` to `CardFooter` to let `CardSaveBar` own its padding.
220
+ - `CardSaveBar` reads `isDirty` / `canSubmit` / `isSubmitting` from form context automatically — no extra props needed.
221
+
222
+ ### 3d. Paper with a generic footer
223
+
224
+ **Before (`@dashboard/library`):**
225
+ ```tsx
226
+ import { Paper } from '@dashboard/library';
227
+ import { Button } from '@aircall/tractor';
228
+
229
+ function BillingCard() {
230
+ return (
231
+ <Paper
232
+ title="Billing"
233
+ footer={
234
+ <div style={{ padding: 16 }}>
235
+ <Button variant="primary">Upgrade plan</Button>
236
+ </div>
237
+ }
238
+ >
239
+ <p>Current plan: Free</p>
240
+ </Paper>
241
+ );
242
+ }
243
+ ```
244
+
245
+ **After (`@aircall/ds`):**
246
+ ```tsx
247
+ import { Button, Card, CardContent, CardFooter, CardHeader, CardTitle } from '@aircall/ds';
248
+
249
+ function BillingCard() {
250
+ return (
251
+ <Card>
252
+ <CardHeader>
253
+ <CardTitle>Billing</CardTitle>
254
+ </CardHeader>
255
+ <CardContent>
256
+ <p>Current plan: Free</p>
257
+ </CardContent>
258
+ <CardFooter>
259
+ <Button variant="default">Upgrade plan</Button>
260
+ </CardFooter>
261
+ </Card>
262
+ );
263
+ }
264
+ ```
265
+
266
+ `CardFooter` applies `border-t bg-muted/50 p-4` by default. For a transparent footer pass `className="bg-transparent border-none"`.
267
+
268
+ ---
269
+
270
+ ## 4. Common mistakes
271
+
272
+ ### Mistake 1 — Spreading BoxProps onto Card
273
+
274
+ ```tsx
275
+ // ❌ Wrong — tractor/dashboard BoxProps (maxWidth, borderRadius, backgroundColor…) on Card
276
+ <Card maxWidth={1372} borderRadius="sm" backgroundColor="white">
277
+ <CardContent>Content</CardContent>
278
+ </Card>
279
+
280
+ // ✅ Correct — Card owns the surface; use className for true one-offs only
281
+ <Card className="max-w-[1372px]">
282
+ <CardContent>Content</CardContent>
283
+ </Card>
284
+ ```
285
+
286
+ `Paper` accepted arbitrary `BoxProps` from `@aircall/tractor`. `Card` extends `React.ComponentProps<'div'>` — it accepts `className`, not tractor spacing/color tokens. Spread them and React will warn on unknown HTML attributes.
287
+
288
+ Source: `packages/ds/src/components/card.tsx`
289
+
290
+ ### Mistake 2 — Placing CardAction outside CardHeader
291
+
292
+ ```tsx
293
+ // ❌ Wrong — renders in normal flow, loses top-right pinning
294
+ <Card>
295
+ <CardHeader>
296
+ <CardTitle>Title</CardTitle>
297
+ </CardHeader>
298
+ <CardAction>
299
+ <Button variant="ghost" size="icon" aria-label="Menu">…</Button>
300
+ </CardAction>
301
+ <CardContent>Body</CardContent>
302
+ </Card>
303
+
304
+ // ✅ Correct — CardAction inside CardHeader uses the header grid
305
+ <Card>
306
+ <CardHeader>
307
+ <CardTitle>Title</CardTitle>
308
+ <CardAction>
309
+ <Button variant="ghost" size="icon" aria-label="Menu">…</Button>
310
+ </CardAction>
311
+ </CardHeader>
312
+ <CardContent>Body</CardContent>
313
+ </Card>
314
+ ```
315
+
316
+ `CardHeader` uses `grid auto-rows-min has-data-[slot=card-action]:grid-cols-[1fr_auto]`. `CardAction` carries `data-slot="card-action"` and is placed at `col-start-2 row-span-2`. Outside `CardHeader`, no grid parent exists and the action renders in document flow.
317
+
318
+ Source: `packages/ds/src/components/card.tsx`
319
+
320
+ ### Mistake 3 — Omitting overflow-hidden/pb-0 on Card when using CardSaveBar
321
+
322
+ ```tsx
323
+ // ❌ Wrong — animated bar peeks out of the card's rounded corners
324
+ <Card>
325
+ <CardContent>…</CardContent>
326
+ <CardFooter className="p-0">
327
+ <CardSaveBar />
328
+ </CardFooter>
329
+ </Card>
330
+
331
+ // ✅ Correct — overflow-hidden clips the bar; pb-0 removes the gap below the footer
332
+ <Card className="overflow-hidden pb-0">
333
+ <CardContent>…</CardContent>
334
+ <CardFooter className="p-0">
335
+ <CardSaveBar />
336
+ </CardFooter>
337
+ </Card>
338
+ ```
339
+
340
+ `CardSaveBar` slides in via a `grid-rows-[0fr→1fr]` transition. Without `overflow-hidden` on `Card`, the collapsed bar overflows the card's rounded corners during animation. Without `pb-0` the card's own `py-4` leaves a gap between the footer and the card bottom edge.
341
+
342
+ Source: `packages/blocks/src/components/card-save-bar.tsx`
343
+
344
+ ### Mistake 4 — Passing react-final-form formProps to useForm
345
+
346
+ ```tsx
347
+ // ❌ Wrong — react-final-form API; useForm from @aircall/blocks is TanStack Form
348
+ import { useForm } from '@aircall/blocks';
349
+ const form = useForm({
350
+ initialValues: { name: '' }, // ← react-final-form key
351
+ onSubmit: (values) => save(values) // ← sync signature; TanStack expects async
352
+ });
353
+
354
+ // ✅ Correct — TanStack Form API
355
+ import { useForm } from '@aircall/blocks';
356
+ const form = useForm({
357
+ defaultValues: { name: '' },
358
+ onSubmit: async ({ value }) => { await save(value); }
359
+ });
360
+ ```
361
+
362
+ `useForm` from `@aircall/blocks` wraps TanStack Form, not `react-final-form`. The key differences: `defaultValues` (not `initialValues`), and `onSubmit` receives `{ value }` (not the bare values object).
363
+
364
+ Source: `packages/blocks/src/components/card-save-bar.tsx`