@aircall/blocks 0.5.1 → 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.
- package/README.md +43 -0
- package/dist/globals.css +1 -1
- package/dist/index.d.ts +81 -24
- package/dist/index.js +493 -113
- package/package.json +20 -4
- package/skills/aircall-blocks/migrate-dashboard/SKILL.md +127 -0
- package/skills/aircall-blocks/migrate-dashboard/card/SKILL.md +364 -0
- package/skills/aircall-blocks/migrate-dashboard/combobox/SKILL.md +487 -0
- package/skills/aircall-blocks/migrate-dashboard/copy-button/SKILL.md +138 -0
- package/skills/aircall-blocks/migrate-dashboard/dashboard-page/SKILL.md +198 -0
- package/skills/aircall-blocks/migrate-dashboard/data-table/SKILL.md +395 -0
- package/skills/aircall-blocks/migrate-dashboard/empty-states/SKILL.md +347 -0
- package/skills/aircall-blocks/migrate-dashboard/form-wizard/SKILL.md +271 -0
- package/skills/aircall-blocks/migrate-dashboard/info-popup/SKILL.md +179 -0
- package/skills/aircall-blocks/migrate-dashboard/layout/SKILL.md +328 -0
- package/skills/aircall-blocks/migrate-dashboard/list/SKILL.md +474 -0
- package/skills/aircall-blocks/migrate-dashboard/loading/SKILL.md +260 -0
- package/skills/aircall-blocks/migrate-dashboard/page-header/SKILL.md +394 -0
- package/skills/aircall-blocks/migrate-dashboard/tile/SKILL.md +429 -0
- package/skills/aircall-blocks/migrate-dashboard/toggle-row/SKILL.md +177 -0
- package/skills/aircall-blocks/setup/SKILL.md +127 -0
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aircall/blocks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"sideEffects": [
|
|
7
|
-
"**/*.css"
|
|
7
|
+
"**/*.css",
|
|
8
|
+
"**/i18n/register.ts"
|
|
8
9
|
],
|
|
9
10
|
"description": "Aircall Blocks — higher-level UI compositions built on @aircall/ds",
|
|
10
11
|
"author": "Aircall",
|
|
@@ -15,7 +16,8 @@
|
|
|
15
16
|
"files": [
|
|
16
17
|
"dist",
|
|
17
18
|
"README.md",
|
|
18
|
-
"package.json"
|
|
19
|
+
"package.json",
|
|
20
|
+
"skills"
|
|
19
21
|
],
|
|
20
22
|
"exports": {
|
|
21
23
|
".": {
|
|
@@ -39,6 +41,10 @@
|
|
|
39
41
|
"optional": true
|
|
40
42
|
}
|
|
41
43
|
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"react-markdown": "9.0.3",
|
|
46
|
+
"remark-gfm": "4.0.1"
|
|
47
|
+
},
|
|
42
48
|
"devDependencies": {
|
|
43
49
|
"@aircall/react-icons": "*",
|
|
44
50
|
"@aircall/tsconfig": "1.4.3",
|
|
@@ -54,6 +60,7 @@
|
|
|
54
60
|
"@storybook/react-vite": "10.3.3",
|
|
55
61
|
"@tailwindcss/cli": "4.1.18",
|
|
56
62
|
"@tailwindcss/postcss": "4.1.18",
|
|
63
|
+
"@tanstack/intent": "0.1.1",
|
|
57
64
|
"@tanstack/react-form": "^1.33.0",
|
|
58
65
|
"@types/node": "24",
|
|
59
66
|
"@types/react": "19",
|
|
@@ -74,14 +81,23 @@
|
|
|
74
81
|
"vite": "7.3.1",
|
|
75
82
|
"vitest": "4.0.17",
|
|
76
83
|
"zod": "4.4.3",
|
|
77
|
-
"@aircall/ds": "0.
|
|
84
|
+
"@aircall/ds": "0.15.0",
|
|
78
85
|
"@aircall/hooks": "0.5.1"
|
|
79
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
|
+
},
|
|
80
95
|
"scripts": {
|
|
81
96
|
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
|
|
82
97
|
"build:js": "tsdown --config ./tsdown.config.ts",
|
|
83
98
|
"build:css": "tailwindcss -i ./src/styles/globals.css -o ./dist/globals.css --minify",
|
|
84
99
|
"build:package": "pnpm run build:js && pnpm run build:css",
|
|
100
|
+
"typecheck": "tsc --noEmit",
|
|
85
101
|
"lint": "eslint . --max-warnings 0",
|
|
86
102
|
"sb:dev": "storybook dev -p 6009",
|
|
87
103
|
"sb:build": "storybook build",
|
|
@@ -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`
|