@aircall/blocks 0.17.0 → 0.18.1

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,258 @@
1
+ ---
2
+ name: aircall-blocks/migrate-dashboard/setting-card
3
+ description: >
4
+ Migrate @dashboard/library Paper and PaperForm (settings-page surfaces) to the
5
+ @aircall/blocks SettingCard family (SettingCard, SettingCardHeader, SettingCardTitle,
6
+ SettingCardDescription, SettingCardAction, SettingCardContent) plus useForm + CardSaveBar
7
+ for the save bar. Load when a file imports Paper or PaperForm from @dashboard/library and
8
+ the surface is a settings section (title / description / optional toggle or action /
9
+ optional save bar). The errorBoundary is handled OUTSIDE the card — it does not migrate.
10
+ type: sub-skill
11
+ library: aircall-blocks
12
+ requires:
13
+ - aircall-blocks/setup
14
+ - aircall-blocks/migrate-dashboard
15
+ sources:
16
+ - "aircall/hydra:packages/blocks/src/components/setting-card.tsx"
17
+ - "aircall/hydra:packages/blocks/src/components/card-save-bar.tsx"
18
+ ---
19
+
20
+ This skill builds on aircall-blocks/migrate-dashboard.
21
+
22
+ `Paper` / `PaperForm` are the `@dashboard/library` settings-page surfaces (title, subtitle,
23
+ optional header action, content, optional save-bar footer). Their target is **`SettingCard`**
24
+ (`@aircall/blocks`) — a thin composition over the DS `Card` that adds the settings level
25
+ backgrounds and the level-aware title. For a **non-settings** card (a tile or generic
26
+ container with no title/toggle semantics) use the raw `Card` primitives instead — load
27
+ `@aircall/blocks#aircall-blocks/migrate-dashboard/card`.
28
+
29
+ ## 1. Component mapping
30
+
31
+ | @dashboard/library | @aircall/blocks / @aircall/ds |
32
+ | --- | --- |
33
+ | `Paper` (root surface) | `SettingCard` |
34
+ | `Paper` `title` prop | `SettingCardTitle` inside `SettingCardHeader` |
35
+ | `Paper` `subtitle` prop | `SettingCardDescription` inside `SettingCardHeader` |
36
+ | `Paper` `titleSide` prop | `SettingCardAction` inside `SettingCardHeader` |
37
+ | `Paper` `children` (body) | `SettingCardContent` |
38
+ | `Paper` `footer` (save/discard bar) | `useForm` + `CardSaveBar` (form on the outer level) |
39
+ | `Paper` `footer` (generic) | plain children after `SettingCardContent`, or a DS `CardFooter` |
40
+ | `Paper` `banner` prop | inline DS `Banner` before `SettingCardHeader` |
41
+ | `Paper` `disabled` / `disabledText` | app-level — dim the card + render a lock label; `SettingCard` has no `disabled` prop |
42
+ | `Paper` `fluid` prop | content is already full-width — drop it |
43
+ | `Paper` `errorBoundary` prop | **stays in the app, OUTSIDE `SettingCard`** — see §3. Does NOT migrate. |
44
+ | `Paper` `BoxProps` spread | drop — `className` for genuine one-offs only |
45
+ | `PaperForm` (form + save bar) | `<form>` (outer) + `useForm` + `SettingCard` + `CardSaveBar` |
46
+ | `PaperForm` `formProps` (`react-final-form`) | `useForm` (TanStack Form) — `defaultValues`, `onSubmit: async ({ value })` |
47
+ | `PaperForm` `getErrorMessage` / `submitError` | catch in `onSubmit`, store in state, render an `Alert` above the content |
48
+ | `PaperForm` `shouldBlockNavigation` | app-level (navigation blocker) — not part of blocks |
49
+ | `PaperForm` `submitButtonText` / `undoButtonText` | `CardSaveBar` `submitLabel` / `resetLabel` |
50
+
51
+ ## 2. Imports
52
+
53
+ ```tsx
54
+ // blocks — SettingCard family + TanStack Form + save bar
55
+ import {
56
+ SettingCard,
57
+ SettingCardHeader,
58
+ SettingCardTitle,
59
+ SettingCardDescription,
60
+ SettingCardAction,
61
+ SettingCardContent,
62
+ useForm,
63
+ CardSaveBar
64
+ } from '@aircall/blocks';
65
+
66
+ // DS primitives used inside (Button, Switch, Alert, Banner, fields …)
67
+ import { Button, Switch, Alert } from '@aircall/ds';
68
+ ```
69
+
70
+ ## 3. The errorBoundary stays OUTSIDE SettingCard
71
+
72
+ `Paper` wrapped **its children** in `ScopedErrorBoundary` (plus an optional
73
+ `ErrorContextProvider`) via the `errorBoundary` prop. Those are **app-level**
74
+ `@dashboard/library` components — `ScopedErrorBoundary` is react-router-aware and reads
75
+ `ErrorContext`; neither migrates to blocks/ds. `SettingCard` is a presentational layout
76
+ primitive and deliberately has **no** `errorBoundary` prop.
77
+
78
+ So keep the boundary in the app and wrap the **content** (not the whole card), matching
79
+ `Paper`'s scope — a content crash still leaves the card title/description visible:
80
+
81
+ ```tsx
82
+ import { ScopedErrorBoundary, ErrorContextProvider } from '@dashboard/library';
83
+
84
+ <SettingCard>
85
+ <SettingCardHeader>
86
+ <SettingCardTitle>Redaction</SettingCardTitle>
87
+ <SettingCardDescription>Mask sensitive data in transcripts.</SettingCardDescription>
88
+ </SettingCardHeader>
89
+ <SettingCardContent>
90
+ {/* errorBoundary lives here, around the fallible content — NOT on SettingCard */}
91
+ <ScopedErrorBoundary message="Couldn't load redaction settings" showRetryButton>
92
+ <RedactionCategories />
93
+ </ScopedErrorBoundary>
94
+ </SettingCardContent>
95
+ </SettingCard>
96
+ ```
97
+
98
+ If `Paper`'s `errorBoundary` config carried a `context`, keep the
99
+ `ErrorContextProvider` wrapper too (same nesting as `Paper` did — provider outside the
100
+ boundary):
101
+
102
+ ```tsx
103
+ <ErrorContextProvider value={ctx}>
104
+ <ScopedErrorBoundary {...cfg}>{content}</ScopedErrorBoundary>
105
+ </ErrorContextProvider>
106
+ ```
107
+
108
+ > If the whole section (header included) should be replaced on error, wrap the entire
109
+ > `<SettingCard>` instead. Default to wrapping only the content — that is what `Paper` did.
110
+
111
+ ## 4. Before / After
112
+
113
+ ### 4a. Basic Paper — settings surface
114
+
115
+ **Before:**
116
+ ```tsx
117
+ import { Paper } from '@dashboard/library';
118
+
119
+ <Paper title="General settings" subtitle="Manage your workspace preferences.">
120
+ <WorkspacePreferences />
121
+ </Paper>
122
+ ```
123
+
124
+ **After:**
125
+ ```tsx
126
+ import { SettingCard, SettingCardHeader, SettingCardTitle, SettingCardDescription, SettingCardContent } from '@aircall/blocks';
127
+
128
+ <SettingCard>
129
+ <SettingCardHeader>
130
+ <SettingCardTitle>General settings</SettingCardTitle>
131
+ <SettingCardDescription>Manage your workspace preferences.</SettingCardDescription>
132
+ </SettingCardHeader>
133
+ <SettingCardContent>
134
+ <WorkspacePreferences />
135
+ </SettingCardContent>
136
+ </SettingCard>
137
+ ```
138
+
139
+ - `title` → `SettingCardTitle`, `subtitle` → `SettingCardDescription`, both inside `SettingCardHeader`.
140
+ - Body → `SettingCardContent`. Drop the `BoxProps` spread — `SettingCard` owns the surface.
141
+
142
+ ### 4b. Paper with a header action / toggle (titleSide)
143
+
144
+ **Before:**
145
+ ```tsx
146
+ <Paper
147
+ title="Call recording"
148
+ subtitle="Record inbound and outbound calls."
149
+ titleSide={<Switch checked={enabled} onChange={setEnabled} />}
150
+ >
151
+ {enabled && <RecordingOptions />}
152
+ </Paper>
153
+ ```
154
+
155
+ **After:**
156
+ ```tsx
157
+ <SettingCard>
158
+ <SettingCardHeader>
159
+ <SettingCardTitle>Call recording</SettingCardTitle>
160
+ <SettingCardDescription>Record inbound and outbound calls.</SettingCardDescription>
161
+ <SettingCardAction>
162
+ <Switch checked={enabled} onCheckedChange={setEnabled} />
163
+ </SettingCardAction>
164
+ </SettingCardHeader>
165
+ {enabled && (
166
+ <SettingCardContent>
167
+ <RecordingOptions />
168
+ </SettingCardContent>
169
+ )}
170
+ </SettingCard>
171
+ ```
172
+
173
+ - `titleSide` → `SettingCardAction` (inside `SettingCardHeader`; the header grid pins it top-right).
174
+ - DS `Switch` uses `onCheckedChange`, not tractor's `onChange`.
175
+ - **Progressive disclosure is consumer-controlled**: render `SettingCardContent` only when the toggle is on (`{enabled && …}`). `SettingCard` has no internal toggle→content coupling, so gated data/fields never mount while off.
176
+
177
+ ### 4c. Nested settings (levels are automatic)
178
+
179
+ A `SettingCard` rendered inside another card's `SettingCardContent` auto-detects **level 2**
180
+ (white background + border, 16px medium title); the top card is **level 1** (muted,
181
+ borderless, 18px bold). No prop needed. For a rare 3rd level, force `level={1}` on the
182
+ deepest card to reuse the level-1 background (its title stays 16px medium automatically).
183
+
184
+ ### 4d. PaperForm — form with save/discard bar
185
+
186
+ **Before:**
187
+ ```tsx
188
+ import { PaperForm } from '@dashboard/library';
189
+
190
+ <PaperForm
191
+ title="Profile"
192
+ subtitle="Update your display name."
193
+ formProps={{ initialValues: { name: '' }, onSubmit: async (values) => save(values) }}
194
+ submitButtonText="Save"
195
+ undoButtonText="Discard"
196
+ >
197
+ {({ values }) => <input name="name" defaultValue={values.name} />}
198
+ </PaperForm>
199
+ ```
200
+
201
+ **After:**
202
+ ```tsx
203
+ import { SettingCard, SettingCardHeader, SettingCardTitle, SettingCardDescription, SettingCardContent, useForm, FormInputField } from '@aircall/blocks';
204
+ import { FieldGroup, Input } from '@aircall/ds';
205
+
206
+ function ProfileForm() {
207
+ const form = useForm({
208
+ defaultValues: { name: '' },
209
+ onSubmit: async ({ value }) => { await save(value); }
210
+ });
211
+
212
+ return (
213
+ // Form on the OUTER level — SettingCard stays presentational.
214
+ <form onSubmit={e => { e.preventDefault(); void form.handleSubmit(); }}>
215
+ <SettingCard>
216
+ <SettingCardHeader>
217
+ <SettingCardTitle>Profile</SettingCardTitle>
218
+ <SettingCardDescription>Update your display name.</SettingCardDescription>
219
+ </SettingCardHeader>
220
+ <SettingCardContent>
221
+ <FieldGroup>
222
+ <FormInputField form={form} name="name" label="Display name">
223
+ {(_field, { inputProps }) => <Input {...inputProps} />}
224
+ </FormInputField>
225
+ </FieldGroup>
226
+ </SettingCardContent>
227
+ <form.AppForm>
228
+ <form.CardSaveBar submitLabel="Save" resetLabel="Discard" />
229
+ </form.AppForm>
230
+ </SettingCard>
231
+ </form>
232
+ );
233
+ }
234
+ ```
235
+
236
+ Key changes:
237
+ - `react-final-form` `formProps` + render-prop → `useForm` from `@aircall/blocks` (TanStack Form): `defaultValues` (not `initialValues`), `onSubmit` receives `{ value }`.
238
+ - Hand-wired inputs → `Form*Field` wrappers driven by the form (never per-field `useState`).
239
+ - Put the `<form>` on the **outer level** (wrapping `SettingCard`), and drop **`form.CardSaveBar`** in as the last child of `SettingCard`. It reads `isDirty`/`canSubmit`/`isSubmitting` from context, slides in when dirty, and carries `data-slot="card-footer"` so the card auto-collapses its bottom padding and the bar sits flush. **No `className` is needed on any part** - the card's own layout (row gap + auto `pb-0`) handles both the clean and dirty states.
240
+ - `submitButtonText`/`undoButtonText` → `CardSaveBar` `submitLabel`/`resetLabel`.
241
+
242
+ **Submit error (`getErrorMessage` / `submitError`).** No built-in equivalent — catch in
243
+ `onSubmit`, store it, and render an `Alert` above the content:
244
+
245
+ ```tsx
246
+ const [submitError, setSubmitError] = useState<string | null>(null);
247
+ const form = useForm({
248
+ defaultValues: { name: '' },
249
+ onSubmit: async ({ value }) => {
250
+ try { await save(value); setSubmitError(null); }
251
+ catch (e) { setSubmitError(getErrorMessage(e)); }
252
+ }
253
+ });
254
+ // In JSX, above the fields inside SettingCardContent:
255
+ {submitError && <Alert variant="error">{submitError}</Alert>}
256
+ ```
257
+
258
+ > DS `Alert`'s error variant is `error` (not tractor's `critical`). Full set: `default` / `info` / `success` / `warning` / `error`.