UNPKG

@aircall/blocks

Version:

Aircall Blocks — higher-level UI compositions built on @aircall/ds

337 lines (285 loc) 10.6 kB
--- name: aircall-blocks/migrate-dashboard/card description: > Migrate @dashboard/library Paper and PaperForm to @aircall/ds Card primitives (Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter) and @aircall/blocks CardSaveBar for save/discard bars. Load when a file imports Paper or PaperForm from @dashboard/library. type: sub-skill library: aircall-blocks requires: - aircall-blocks/setup - aircall-blocks/migrate-dashboard sources: - "aircall/hydra:packages/ds/src/index.ts" --- This skill builds on aircall-blocks/migrate-dashboard. > **Migrating a settings-page `Paper` / `PaperForm`?** Prefer `SettingCard` — load > `@aircall/blocks#aircall-blocks/migrate-dashboard/setting-card`. It is purpose-built for > the title / description / toggle / save-bar layout and gets the level backgrounds and > level-aware title for free. Use the raw `Card` primitives below only for **non-settings** > card surfaces (tiles, generic containers with no title/toggle semantics). ## 1. Component mapping | @dashboard/library | @aircall/ds / @aircall/blocks | | --- | --- | | `Paper` (root surface) | `Card` (`@aircall/ds`) | | `Paper` `title` prop | `CardTitle` inside `CardHeader` (`@aircall/ds`) | | `Paper` `subtitle` prop | `CardDescription` inside `CardHeader` (`@aircall/ds`) | | `Paper` `titleSide` prop | `CardAction` inside `CardHeader` (`@aircall/ds`) | | `Paper` `children` (body) | `CardContent` (`@aircall/ds`) | | `Paper` `footer` prop (generic) | `CardFooter` (`@aircall/ds`) | | `Paper` `footer` prop (save/discard bar) | `CardSaveBar` (`@aircall/blocks`) | | `PaperForm` (form wrapper with save bar) | `Card` + `useForm` + `CardSaveBar` (`@aircall/blocks`) | | `Paper` `banner` prop | Inline `Banner` before `CardHeader` inside `Card` (`@aircall/ds`) | | `Paper` `disabled` / `disabledText` props | `className="opacity-50 cursor-not-allowed"` on `Card` + custom label | | `Paper` `fluid` prop | `className="w-full"` on `CardContent` (default is already full-width) | `PaperForm` is a compound: it wires a `react-final-form` form, renders `Paper`, and appends a `SaveBar` footer. The replacement is `Card` with TanStack Form (`useForm` from `@aircall/blocks`) and `CardSaveBar` as a direct child of `Card` (it is the footer itself). ## 2. Imports ```tsx // DS primitives — structural card shell import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Banner, BannerTitle, BannerDescription } from '@aircall/ds'; // blocks — TanStack Form + animated save bar import { useForm, CardSaveBar } from '@aircall/blocks'; ``` ## 3. Before / After ### 3a. Basic Paper — read-only surface **Before (`@dashboard/library`):** ```tsx import { Paper } from '@dashboard/library'; function SettingsSection() { return ( <Paper title="General settings" subtitle="Manage your workspace preferences." > <p>Section content here.</p> </Paper> ); } ``` **After (`@aircall/ds`):** ```tsx import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@aircall/ds'; function SettingsSection() { return ( <Card> <CardHeader> <CardTitle>General settings</CardTitle> <CardDescription>Manage your workspace preferences.</CardDescription> </CardHeader> <CardContent> <p>Section content here.</p> </CardContent> </Card> ); } ``` Key changes: - `Paper` `title``CardTitle` inside `CardHeader`. - `Paper` `subtitle``CardDescription` inside `CardHeader`. - `Paper` `children` → wrapped in `CardContent`. - Drop `BoxProps` spread (`maxWidth`, `borderColor`, etc.) — `Card` owns the surface. ### 3b. Paper with a header action (titleSide) **Before (`@dashboard/library`):** ```tsx import { Paper } from '@dashboard/library'; import { Button } from '@aircall/tractor'; function IntegrationCard() { return ( <Paper title="Integrations" subtitle="Connect your tools." titleSide={<Button variant="primary" size="small">Add integration</Button>} > <p>Integration list here.</p> </Paper> ); } ``` **After (`@aircall/ds`):** ```tsx import { Button, Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from '@aircall/ds'; function IntegrationCard() { return ( <Card> <CardHeader> <CardTitle>Integrations</CardTitle> <CardDescription>Connect your tools.</CardDescription> <CardAction> <Button variant="default" size="sm">Add integration</Button> </CardAction> </CardHeader> <CardContent> <p>Integration list here.</p> </CardContent> </Card> ); } ``` `CardAction` must be inside `CardHeader` — the header grid (`grid-cols-[1fr_auto]`) positions it top-right automatically. ### 3c. PaperForm — form with save/discard bar **Before (`@dashboard/library`):** ```tsx import { PaperForm } from '@dashboard/library'; function ProfileForm() { return ( <PaperForm title="Profile" subtitle="Update your display name." formProps={{ initialValues: { name: '' }, onSubmit: async (values) => { /* submit */ } }} submitButtonText="Save" undoButtonText="Discard" > {({ values }) => ( <input name="name" defaultValue={values.name} /> )} </PaperForm> ); } ``` **After (`@aircall/blocks`):** ```tsx import { useForm, CardSaveBar } from '@aircall/blocks'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@aircall/ds'; const form = useForm({ defaultValues: { name: '' }, onSubmit: async ({ value }) => { /* submit */ } }); function ProfileForm() { return ( <form.AppForm> <form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }}> <Card> <CardHeader> <CardTitle>Profile</CardTitle> <CardDescription>Update your display name.</CardDescription> </CardHeader> <CardContent> <form.AppField name="name"> {(field) => ( <input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} /> )} </form.AppField> </CardContent> <CardSaveBar submitLabel="Save" resetLabel="Discard" /> </Card> </form> </form.AppForm> ); } ``` Key changes: - `PaperForm` `formProps` + `react-final-form` render-prop → `useForm` from `@aircall/blocks` (TanStack Form). - `PaperForm` `footer` (implicit `SaveBar`) → `CardSaveBar` as a **direct child of `Card`** (do NOT wrap it in a `CardFooter` — it already carries `data-slot="card-footer"` itself). - **No `className` needed on `Card`**: it already has `overflow-hidden` built in, and `CardSaveBar`'s `data-slot="card-footer"` makes the card auto-collapse its bottom padding (`has-data-[slot=card-footer]:pb-0`) so the bar sits flush. - `CardSaveBar` reads `isDirty` / `canSubmit` / `isSubmitting` from form context automatically — no extra props needed. **Submit error banner (`getErrorMessage` / `submitError`).** `PaperForm` surfaced a submit failure via `getErrorMessage` + `submitError`. There is no built-in equivalent — catch and store it in `onSubmit`, then render an inline `Alert` above `CardContent`: ```tsx const [submitError, setSubmitError] = useState<string | null>(null); const form = useForm({ defaultValues: { name: '' }, onSubmit: async ({ value }) => { try { await save(value); setSubmitError(null); } catch (e) { setSubmitError(getErrorMessage(e)); } }, }); // In JSX, above CardContent: {submitError && <Alert variant="error">{submitError}</Alert>} ``` > DS `Alert`'s error variant is `error` (not Tractor's `critical`/`destructive`). Full set: `default` / `info` / `success` / `warning` / `error`. ### 3d. Paper with a generic footer **Before (`@dashboard/library`):** ```tsx import { Paper } from '@dashboard/library'; import { Button } from '@aircall/tractor'; function BillingCard() { return ( <Paper title="Billing" footer={ <div style={{ padding: 16 }}> <Button variant="primary">Upgrade plan</Button> </div> } > <p>Current plan: Free</p> </Paper> ); } ``` **After (`@aircall/ds`):** ```tsx import { Button, Card, CardContent, CardFooter, CardHeader, CardTitle } from '@aircall/ds'; function BillingCard() { return ( <Card> <CardHeader> <CardTitle>Billing</CardTitle> </CardHeader> <CardContent> <p>Current plan: Free</p> </CardContent> <CardFooter> <Button variant="default">Upgrade plan</Button> </CardFooter> </Card> ); } ``` `CardFooter` applies `border-t bg-muted/50 p-4` by default. For a transparent footer pass `className="bg-transparent border-none"`. --- ## 4. Common mistakes ### Mistake 1 — Spreading BoxProps onto Card ```tsx // ❌ Wrong — tractor/dashboard BoxProps (maxWidth, borderRadius, backgroundColor…) on Card <Card maxWidth={1372} borderRadius="sm" backgroundColor="white"> <CardContent>Content</CardContent> </Card> // ✅ Correct — Card owns the surface; use className for true one-offs only <Card className="max-w-[1372px]"> <CardContent>Content</CardContent> </Card> ``` `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. Source: `packages/ds/src/components/card.tsx` ### Mistake 2 — Wrapping CardSaveBar in a CardFooter (or adding overflow-hidden/pb-0) ```tsx // ❌ Wrong — CardSaveBar already IS the footer; a CardFooter around it doubles // the top border and padding, and the className hints are redundant <Card className="overflow-hidden pb-0"> <CardContent>…</CardContent> <CardFooter className="p-0"> <CardSaveBar /> </CardFooter> </Card> // ✅ Correct — drop CardSaveBar in as a direct child; no wrapper, no className <Card> <CardContent>…</CardContent> <CardSaveBar /> </Card> ``` `CardSaveBar` renders its own `border-t` + padding and carries `data-slot="card-footer"`, so it already behaves as the footer. `Card` has `overflow-hidden` built in (clips the slide-in), and the `data-slot="card-footer"` triggers `has-data-[slot=card-footer]:pb-0`, so the bar sits flush with no manual `overflow-hidden`, `pb-0`, or `CardFooter` wrapper. Source: `packages/blocks/src/components/card-save-bar.tsx`