@aircall/blocks 0.16.0 → 0.18.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.
@@ -7,7 +7,6 @@ description: >
7
7
  FormWizard, useFormWizard, or FormField from @dashboard/library.
8
8
  type: sub-skill
9
9
  library: aircall-blocks
10
- library_version: "0.5.1"
11
10
  requires:
12
11
  - aircall-blocks/setup
13
12
  - aircall-blocks/migrate-dashboard
@@ -247,6 +246,46 @@ spread the control bundle onto the matching DS primitive.
247
246
  | combobox (single) | `FormComboboxField` | switch | `FormSwitchField` |
248
247
  | combobox (multi) | `FormMultiComboboxField` | number | `FormNumericField` |
249
248
  | textarea | `FormTextareaField` | slider | `FormSliderField` |
249
+ | toggle group | `FormToggleGroupField` | | |
250
+
251
+ ### Building a wrapper for an unlisted primitive (`FormFieldBase`)
252
+
253
+ For a DS primitive without a shipped `Form*Field`, build one on `FormFieldBase` — it owns the field binding + the shared `Field`/`Label`/`Description`/`Error` shell; you supply a `buildControl` that maps the typed field to the control bundle. This is the shape every shipped wrapper follows (here re-creating `FormSwitchField`, which already ships — copy the pattern for a genuinely new primitive):
254
+
255
+ ```tsx
256
+ import { FormFieldBase, type BoundForm, type FormFieldValidators, type TypedField } from '@aircall/blocks';
257
+ import { Switch } from '@aircall/ds';
258
+ import type { DeepKeysOfType } from '@tanstack/react-form';
259
+
260
+ type SwitchControl = {
261
+ switchProps: { checked: boolean; onCheckedChange: (checked: boolean) => void; onBlur: () => void };
262
+ };
263
+
264
+ function switchControl(field: TypedField<boolean>): SwitchControl {
265
+ return {
266
+ switchProps: {
267
+ checked: field.state.value,
268
+ onCheckedChange: checked => field.handleChange(checked),
269
+ onBlur: field.handleBlur,
270
+ },
271
+ };
272
+ }
273
+
274
+ function FormSwitchField<
275
+ TForm extends BoundForm,
276
+ TName extends DeepKeysOfType<TForm['state']['values'], boolean> // only boolean fields
277
+ >(props: {
278
+ form: TForm;
279
+ name: TName;
280
+ label: string;
281
+ validators?: FormFieldValidators<TForm['state']['values'], TName>;
282
+ children: (field: TypedField<boolean>, control: SwitchControl) => React.ReactNode;
283
+ }) {
284
+ return <FormFieldBase<boolean, SwitchControl> {...props} buildControl={switchControl} />;
285
+ }
286
+ ```
287
+
288
+ `DeepKeysOfType<…, boolean>` restricts `name` to fields whose value type matches the control (here `boolean`), so the form stays type-safe.
250
289
 
251
290
  ## Multi-step gotchas
252
291
 
@@ -6,7 +6,6 @@ description: >
6
6
  or InfoPopupContent from @dashboard/library or from the local shared info-popup.
7
7
  type: sub-skill
8
8
  library: aircall-blocks
9
- library_version: "0.6.0"
10
9
  requires:
11
10
  - aircall-blocks/setup
12
11
  - aircall-blocks/migrate-dashboard
@@ -6,7 +6,6 @@ description: >
6
6
  Load when a file imports GridLayout, GridItem, or Gap from @dashboard/library.
7
7
  type: sub-skill
8
8
  library: aircall-blocks
9
- library_version: "0.5.1"
10
9
  requires:
11
10
  - aircall-blocks/setup
12
11
  - aircall-blocks/migrate-dashboard
@@ -7,7 +7,6 @@ description: >
7
7
  or ListItemActions from @dashboard/library.
8
8
  type: sub-skill
9
9
  library: aircall-blocks
10
- library_version: "0.5.1"
11
10
  requires:
12
11
  - aircall-blocks/setup
13
12
  - aircall-blocks/migrate-dashboard
@@ -6,7 +6,6 @@ description: >
6
6
  file imports Loading, Loader, or Spinner from @dashboard/library.
7
7
  type: sub-skill
8
8
  library: aircall-blocks
9
- library_version: "0.5.1"
10
9
  requires:
11
10
  - aircall-blocks/setup
12
11
  - aircall-blocks/migrate-dashboard
@@ -9,7 +9,6 @@ description: >
9
9
  imports PageHeader or PageHeaderLink from @dashboard/library.
10
10
  type: sub-skill
11
11
  library: aircall-blocks
12
- library_version: "0.5.1"
13
12
  requires:
14
13
  - aircall-blocks/setup
15
14
  - aircall-blocks/migrate-dashboard
@@ -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`.
@@ -7,7 +7,6 @@ description: >
7
7
  Tile, TileHeader, or TileValue from @dashboard/library.
8
8
  type: sub-skill
9
9
  library: aircall-blocks
10
- library_version: "0.5.1"
11
10
  requires:
12
11
  - aircall-blocks/setup
13
12
  - aircall-blocks/migrate-dashboard
@@ -5,7 +5,6 @@ description: >
5
5
  Load when a file imports ToggleRow from @dashboard/library.
6
6
  type: sub-skill
7
7
  library: aircall-blocks
8
- library_version: "0.6.0"
9
8
  requires:
10
9
  - aircall-blocks/setup
11
10
  - aircall-blocks/migrate-dashboard
@@ -9,7 +9,6 @@ description: >
9
9
  consumers where the host already loads DS globals.
10
10
  type: core
11
11
  library: aircall-blocks
12
- library_version: "0.5.1"
13
12
  requires:
14
13
  - aircall-ds/setup
15
14
  sources:
@@ -58,6 +57,28 @@ import '@aircall/ds/globals.css';
58
57
  import '@aircall/blocks/globals.css';
59
58
  ```
60
59
 
60
+ #### Single-compilation via `theme.css` (recommended for DS + blocks)
61
+
62
+ The two precompiled bundles above each re-emit the same Tailwind utilities into `@layer
63
+ utilities`; at equal specificity the last-loaded bundle can clobber the other's responsive
64
+ variants (e.g. blocks' plain `.text-base` beating DS's `md:text-sm` on `Textarea`), and **no
65
+ import order fixes it**. To avoid this, import the directive-preserving `theme.css` artifacts
66
+ (tokens + dark variant + reset + keyframes only — **no** Preflight, **no** utilities) and let a
67
+ single Tailwind pass generate the utilities by `@source`-scanning the library dist:
68
+
69
+ ```css
70
+ /* style.css */
71
+ @import 'tailwindcss'; /* consumer owns the single Preflight + engine */
72
+ @import '@aircall/ds/theme.css';
73
+ @import '@aircall/blocks/theme.css';
74
+ @source '../node_modules/@aircall/ds/dist/index.js';
75
+ @source '../node_modules/@aircall/blocks/dist/index.js';
76
+ ```
77
+
78
+ Here the full `@import 'tailwindcss'` is **correct** (unlike the `globals.css` case) because
79
+ `theme.css` ships no Preflight/utilities. `globals.css` still ships unchanged for precompiled-
80
+ bundle consumers. Full rationale: `packages/ds/docs/single-compilation-theme-css.md`.
81
+
61
82
  ### Module-federation consumer (host already owns the Preflight)
62
83
 
63
84
  When your app is a remote loaded inside a host that already imports DS globals