@aircall/blocks 0.6.0 → 0.7.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.
@@ -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`