@aircall/blocks 0.13.0 → 0.14.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/dist/globals.css +1 -1
- package/dist/index.d.ts +200 -153
- package/dist/index.js +75 -17
- package/package.json +2 -2
- package/skills/aircall-blocks/migrate-dashboard/SKILL.md +2 -1
- package/skills/aircall-blocks/migrate-dashboard/dashboard-page/SKILL.md +41 -14
- package/skills/aircall-blocks/migrate-dashboard/form-wizard/SKILL.md +113 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Anchor, Badge, Button, CounterBadge, DropdownMenu, DropdownMenuAddon, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, Field, FieldDescription, FieldError, FieldLabel, FieldLabelAside, FieldLabelInfo, FieldLabelRow, HoverCard, HoverCardContent, HoverCardTrigger, InputGroup, InputGroupButton, InputGroupTextarea, Item, ItemActions, ItemContent, ItemGroup, ItemMedia, ItemTitle, Popover, PopoverContent, PopoverTrigger, Separator, Sidebar, SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem, SidebarProvider, Spinner, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, TabsList, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipTrigger, cn, registerI18nNamespace, useDsTranslation, useSidebar } from "@aircall/ds";
|
|
1
|
+
import { Anchor, Badge, Button, CounterBadge, DropdownMenu, DropdownMenuAddon, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, Field, FieldContent, FieldDescription, FieldError, FieldLabel, FieldLabelAside, FieldLabelInfo, FieldLabelRow, HoverCard, HoverCardContent, HoverCardTrigger, InputGroup, InputGroupButton, InputGroupTextarea, Item, ItemActions, ItemContent, ItemGroup, ItemMedia, ItemTitle, Popover, PopoverContent, PopoverTrigger, Separator, Sidebar, SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem, SidebarProvider, Spinner, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, TabsList, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipTrigger, cn, registerI18nNamespace, useDsTranslation, useSidebar } from "@aircall/ds";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -1734,17 +1734,27 @@ const DashboardStandalonePage = React.forwardRef((componentProps, forwardRef) =>
|
|
|
1734
1734
|
return /* @__PURE__ */ jsx("div", {
|
|
1735
1735
|
ref: forwardRef,
|
|
1736
1736
|
"data-slot": "dashboard-standalone-page",
|
|
1737
|
-
className: cn("flex h-screen
|
|
1737
|
+
className: cn("flex h-screen gap-2 overflow-hidden bg-page-background p-2", className),
|
|
1738
1738
|
...props
|
|
1739
1739
|
});
|
|
1740
1740
|
});
|
|
1741
1741
|
DashboardStandalonePage.displayName = "DashboardStandalonePage";
|
|
1742
|
+
const DashboardStandalonePageContent = React.forwardRef((componentProps, forwardRef) => {
|
|
1743
|
+
const { className, ...props } = componentProps;
|
|
1744
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1745
|
+
ref: forwardRef,
|
|
1746
|
+
"data-slot": "dashboard-standalone-page-content",
|
|
1747
|
+
className: cn("flex min-w-0 flex-1 flex-col overflow-hidden rounded-xl bg-page-surface border border-border", className),
|
|
1748
|
+
...props
|
|
1749
|
+
});
|
|
1750
|
+
});
|
|
1751
|
+
DashboardStandalonePageContent.displayName = "DashboardStandalonePageContent";
|
|
1742
1752
|
const DashboardStandalonePageHeader = React.forwardRef((componentProps, forwardRef) => {
|
|
1743
1753
|
const { className, ...props } = componentProps;
|
|
1744
1754
|
return /* @__PURE__ */ jsx("header", {
|
|
1745
1755
|
ref: forwardRef,
|
|
1746
1756
|
"data-slot": "dashboard-standalone-page-header",
|
|
1747
|
-
className: cn("
|
|
1757
|
+
className: cn("relative flex h-16 shrink-0 flex-col items-center justify-center gap-0.5 border-b border-border bg-background px-16 text-center", className),
|
|
1748
1758
|
...props
|
|
1749
1759
|
});
|
|
1750
1760
|
});
|
|
@@ -1754,18 +1764,28 @@ const DashboardStandalonePageTitle = React.forwardRef((componentProps, forwardRe
|
|
|
1754
1764
|
return /* @__PURE__ */ jsx("h1", {
|
|
1755
1765
|
ref: forwardRef,
|
|
1756
1766
|
"data-slot": "dashboard-standalone-page-title",
|
|
1757
|
-
className: cn("
|
|
1767
|
+
className: cn("max-w-xl truncate text-center text-xl leading-tight font-bold text-foreground", className),
|
|
1758
1768
|
...props
|
|
1759
1769
|
});
|
|
1760
1770
|
});
|
|
1761
1771
|
DashboardStandalonePageTitle.displayName = "DashboardStandalonePageTitle";
|
|
1772
|
+
const DashboardStandalonePageDescription = React.forwardRef((componentProps, forwardRef) => {
|
|
1773
|
+
const { className, ...props } = componentProps;
|
|
1774
|
+
return /* @__PURE__ */ jsx("p", {
|
|
1775
|
+
ref: forwardRef,
|
|
1776
|
+
"data-slot": "dashboard-standalone-page-description",
|
|
1777
|
+
className: cn("max-w-xl truncate text-center text-sm leading-tight font-medium text-muted-foreground", className),
|
|
1778
|
+
...props
|
|
1779
|
+
});
|
|
1780
|
+
});
|
|
1781
|
+
DashboardStandalonePageDescription.displayName = "DashboardStandalonePageDescription";
|
|
1762
1782
|
const DashboardStandalonePageActions = React.forwardRef((componentProps, forwardRef) => {
|
|
1763
1783
|
const { side = "end", className, ...props } = componentProps;
|
|
1764
1784
|
return /* @__PURE__ */ jsx("div", {
|
|
1765
1785
|
ref: forwardRef,
|
|
1766
1786
|
"data-slot": "dashboard-standalone-page-actions",
|
|
1767
1787
|
"data-side": side,
|
|
1768
|
-
className: cn("
|
|
1788
|
+
className: cn("absolute inset-y-0 flex items-center gap-2", side === "start" ? "left-4" : "right-4", className),
|
|
1769
1789
|
...props
|
|
1770
1790
|
});
|
|
1771
1791
|
});
|
|
@@ -1780,16 +1800,16 @@ const DashboardStandalonePageAction = React.forwardRef((componentProps, forwardR
|
|
|
1780
1800
|
});
|
|
1781
1801
|
});
|
|
1782
1802
|
DashboardStandalonePageAction.displayName = "DashboardStandalonePageAction";
|
|
1783
|
-
const
|
|
1803
|
+
const DashboardStandalonePageMain = React.forwardRef((componentProps, forwardRef) => {
|
|
1784
1804
|
const { className, ...props } = componentProps;
|
|
1785
1805
|
return /* @__PURE__ */ jsx("main", {
|
|
1786
1806
|
ref: forwardRef,
|
|
1787
|
-
"data-slot": "dashboard-standalone-page-
|
|
1807
|
+
"data-slot": "dashboard-standalone-page-main",
|
|
1788
1808
|
className: cn("min-h-0 flex-1 overflow-auto p-4 md:p-6", className),
|
|
1789
1809
|
...props
|
|
1790
1810
|
});
|
|
1791
1811
|
});
|
|
1792
|
-
|
|
1812
|
+
DashboardStandalonePageMain.displayName = "DashboardStandalonePageMain";
|
|
1793
1813
|
|
|
1794
1814
|
//#endregion
|
|
1795
1815
|
//#region src/components/dashboard-page.tsx
|
|
@@ -2433,7 +2453,7 @@ function toFieldErrors(errors) {
|
|
|
2433
2453
|
|
|
2434
2454
|
//#endregion
|
|
2435
2455
|
//#region src/components/form-field.tsx
|
|
2436
|
-
function FormFieldShell({ label, description, necessityIndicator, id, children }) {
|
|
2456
|
+
function FormFieldShell({ label, description, necessityIndicator, id, children, orientation = "vertical", controlPosition = "end", controlVariant = "fill" }) {
|
|
2437
2457
|
const errors = useFieldContext().state.meta.errors;
|
|
2438
2458
|
const labelContent = typeof label === "string" ? label : label?.content;
|
|
2439
2459
|
const labelInfo = typeof label === "object" ? label.info : void 0;
|
|
@@ -2455,16 +2475,44 @@ function FormFieldShell({ label, description, necessityIndicator, id, children }
|
|
|
2455
2475
|
variant: descriptionVariant,
|
|
2456
2476
|
children: descriptionContent
|
|
2457
2477
|
});
|
|
2458
|
-
|
|
2459
|
-
|
|
2478
|
+
const invalid = errors.length > 0 ? true : void 0;
|
|
2479
|
+
const instructionalDesc = descriptionVariant === "instructional" ? descriptionNode : null;
|
|
2480
|
+
const contextualDesc = descriptionVariant === "contextual" ? descriptionNode : null;
|
|
2481
|
+
const errorNode = /* @__PURE__ */ jsx(FieldError, { errors: toFieldErrors(errors) });
|
|
2482
|
+
if (orientation === "vertical") return /* @__PURE__ */ jsxs(Field, {
|
|
2483
|
+
"data-invalid": invalid,
|
|
2460
2484
|
children: [
|
|
2461
2485
|
labelRow,
|
|
2462
|
-
|
|
2486
|
+
instructionalDesc,
|
|
2463
2487
|
children,
|
|
2464
|
-
|
|
2465
|
-
|
|
2488
|
+
contextualDesc,
|
|
2489
|
+
errorNode
|
|
2466
2490
|
]
|
|
2467
2491
|
});
|
|
2492
|
+
if (controlVariant === "inline") {
|
|
2493
|
+
const textSide = descriptionContent !== void 0 || errors.length > 0 ? /* @__PURE__ */ jsxs(FieldContent, { children: [
|
|
2494
|
+
labelRow,
|
|
2495
|
+
instructionalDesc,
|
|
2496
|
+
contextualDesc,
|
|
2497
|
+
errorNode
|
|
2498
|
+
] }) : labelRow;
|
|
2499
|
+
return /* @__PURE__ */ jsxs(Field, {
|
|
2500
|
+
orientation,
|
|
2501
|
+
"data-invalid": invalid,
|
|
2502
|
+
children: [controlPosition === "start" ? children : textSide, controlPosition === "start" ? textSide : children]
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
const labelCol = /* @__PURE__ */ jsxs(FieldContent, { children: [labelRow, instructionalDesc] });
|
|
2506
|
+
const controlCol = /* @__PURE__ */ jsxs(FieldContent, { children: [
|
|
2507
|
+
children,
|
|
2508
|
+
contextualDesc,
|
|
2509
|
+
errorNode
|
|
2510
|
+
] });
|
|
2511
|
+
return /* @__PURE__ */ jsxs(Field, {
|
|
2512
|
+
orientation,
|
|
2513
|
+
"data-invalid": invalid,
|
|
2514
|
+
children: [controlPosition === "start" ? controlCol : labelCol, controlPosition === "start" ? labelCol : controlCol]
|
|
2515
|
+
});
|
|
2468
2516
|
}
|
|
2469
2517
|
/**
|
|
2470
2518
|
* Shared runtime binder for the per-primitive wrappers (FormSelectField, FormInputField, …).
|
|
@@ -2472,7 +2520,7 @@ function FormFieldShell({ label, description, necessityIndicator, id, children }
|
|
|
2472
2520
|
* the AppField + shared shell. `TData`/`TControl` carry the precise field + bundle shapes through.
|
|
2473
2521
|
*/
|
|
2474
2522
|
function FormFieldBase(props) {
|
|
2475
|
-
const { form, name, validators, listeners, defaultValue, asyncDebounceMs, asyncAlways, label, description, necessityIndicator, buildControl, children } = props;
|
|
2523
|
+
const { form, name, validators, listeners, defaultValue, asyncDebounceMs, asyncAlways, label, description, necessityIndicator, orientation, controlPosition, controlVariant, buildControl, children } = props;
|
|
2476
2524
|
const id = `${React.useId()}-${name}`;
|
|
2477
2525
|
const AppField = form.AppField;
|
|
2478
2526
|
return /* @__PURE__ */ jsx(AppField, {
|
|
@@ -2486,6 +2534,9 @@ function FormFieldBase(props) {
|
|
|
2486
2534
|
label,
|
|
2487
2535
|
description,
|
|
2488
2536
|
necessityIndicator,
|
|
2537
|
+
orientation,
|
|
2538
|
+
controlPosition,
|
|
2539
|
+
controlVariant,
|
|
2489
2540
|
id,
|
|
2490
2541
|
children: children(field, buildControl(field, id))
|
|
2491
2542
|
})
|
|
@@ -2791,14 +2842,21 @@ function switchControl(field, id) {
|
|
|
2791
2842
|
* with a `switchProps` bundle. `name` is restricted to the form's **boolean** fields — a
|
|
2792
2843
|
* string/number `name` is a type error.
|
|
2793
2844
|
*
|
|
2845
|
+
* In a horizontal/responsive layout the switch is a bare, natural-width control beside its label
|
|
2846
|
+
* (`controlPosition` picks the side — defaults to `'start'`, i.e. switch on the left).
|
|
2847
|
+
*
|
|
2794
2848
|
* @example
|
|
2795
2849
|
* <FormSwitchField form={form} name="enabled" label="Enabled" validators={{ onChange }}>
|
|
2796
2850
|
* {(field, { switchProps }) => <Switch {...switchProps} />}
|
|
2797
2851
|
* </FormSwitchField>
|
|
2798
2852
|
*/
|
|
2799
2853
|
function FormSwitchField(props) {
|
|
2854
|
+
const { orientation = "horizontal", controlPosition = "start", ...rest } = props;
|
|
2800
2855
|
return /* @__PURE__ */ jsx(FormFieldBase, {
|
|
2801
|
-
...
|
|
2856
|
+
...rest,
|
|
2857
|
+
orientation,
|
|
2858
|
+
controlPosition,
|
|
2859
|
+
controlVariant: "inline",
|
|
2802
2860
|
buildControl: switchControl
|
|
2803
2861
|
});
|
|
2804
2862
|
}
|
|
@@ -3222,4 +3280,4 @@ function DashboardSidebarNav({ groups, renderLink }) {
|
|
|
3222
3280
|
}
|
|
3223
3281
|
|
|
3224
3282
|
//#endregion
|
|
3225
|
-
export { BetaBadge, CardSaveBar, ChatbotExpandSuggestion, ChatbotInput, ChatbotPage, ChatbotPageBody, ChatbotPageContent, ChatbotPageConversation, ChatbotPageFooter, ChatbotPageHeading, ChatbotPageInputBar, ChatbotPanel, ChatbotPanelHeader, ChatbotPanelSuggestion, ChatbotPanelTrigger, ChatbotResponseBlock, ChatbotResponseLoading, ChatbotSidebar, ChatbotUserMessage, ComingSoonDescription, ComingSoonEmptyState, ComingSoonMedia, ComingSoonTitle, CommonForm, CopyButton, CopyButtonIcon, CopyButtonLabel, DashboardPage, DashboardPageBanner, DashboardPageContent, DashboardPageHeader, DashboardPageHeaderAction, DashboardPageHeaderActions, DashboardPageHeaderDescription, DashboardPageHeaderNav, DashboardPageHeaderNavBack, DashboardPageHeaderPrefix, DashboardPageHeaderSubtitle, DashboardPageHeaderTitle, DashboardPageHeaderTitleGroup, DashboardPageMain, DashboardPageTabs, DashboardSidebar, DashboardSidebarContent, DashboardSidebarFooter, DashboardSidebarGroup, DashboardSidebarGroupLabel, DashboardSidebarHeader, DashboardSidebarMenu, DashboardSidebarMenuBadge, DashboardSidebarMenuButton, DashboardSidebarMenuItem, DashboardSidebarNav, DashboardSidebarProvider, DashboardSidebarSubmenu, DashboardSidebarSubmenuItem, DashboardSidebarSubmenuSeparator, DashboardSidebarTrigger, DashboardStandalonePage, DashboardStandalonePageAction, DashboardStandalonePageActions, DashboardStandalonePageContent, DashboardStandalonePageHeader, DashboardStandalonePageTitle, DeprecatedBadge, EmptyState, EmptyStateActions, EmptyStateButton, EmptyStateDescription, EmptyStateExternalLink, EmptyStateHeader, EmptyStateMedia, EmptyStateTitle, FormArrayField, FormComboboxField, FormFieldBase, FormFieldShell, FormInputField, FormMultiComboboxField, FormNumericField, FormOTPField, FormRadioGroupField, FormSelectField, FormSliderField, FormSwitchField, FormTextareaField, FormToggleGroupField, NewBadge, NoDataDescription, NoDataEmptyState, NoDataMedia, NoDataTitle, NoSupportDescription, NoSupportEmptyState, NoSupportMedia, NoSupportTitle, NotFoundDescription, NotFoundEmptyState, NotFoundMedia, NotFoundTitle, ProfessionalBadge, RestrictedAccessDescription, RestrictedAccessEmptyState, RestrictedAccessMedia, RestrictedAccessTitle, RoleBadge, SaveBar, SubmitButton, TrialBadge, UnknownErrorDescription, UnknownErrorEmptyState, UnknownErrorMedia, UnknownErrorTitle, fieldContext, formContext, toFieldErrors, useCopyToClipboard, useSidebar as useDashboardSidebar, useFieldContext, useForm, useFormContext, withFieldGroup, withForm };
|
|
3283
|
+
export { BetaBadge, CardSaveBar, ChatbotExpandSuggestion, ChatbotInput, ChatbotPage, ChatbotPageBody, ChatbotPageContent, ChatbotPageConversation, ChatbotPageFooter, ChatbotPageHeading, ChatbotPageInputBar, ChatbotPanel, ChatbotPanelHeader, ChatbotPanelSuggestion, ChatbotPanelTrigger, ChatbotResponseBlock, ChatbotResponseLoading, ChatbotSidebar, ChatbotUserMessage, ComingSoonDescription, ComingSoonEmptyState, ComingSoonMedia, ComingSoonTitle, CommonForm, CopyButton, CopyButtonIcon, CopyButtonLabel, DashboardPage, DashboardPageBanner, DashboardPageContent, DashboardPageHeader, DashboardPageHeaderAction, DashboardPageHeaderActions, DashboardPageHeaderDescription, DashboardPageHeaderNav, DashboardPageHeaderNavBack, DashboardPageHeaderPrefix, DashboardPageHeaderSubtitle, DashboardPageHeaderTitle, DashboardPageHeaderTitleGroup, DashboardPageMain, DashboardPageTabs, DashboardSidebar, DashboardSidebarContent, DashboardSidebarFooter, DashboardSidebarGroup, DashboardSidebarGroupLabel, DashboardSidebarHeader, DashboardSidebarMenu, DashboardSidebarMenuBadge, DashboardSidebarMenuButton, DashboardSidebarMenuItem, DashboardSidebarNav, DashboardSidebarProvider, DashboardSidebarSubmenu, DashboardSidebarSubmenuItem, DashboardSidebarSubmenuSeparator, DashboardSidebarTrigger, DashboardStandalonePage, DashboardStandalonePageAction, DashboardStandalonePageActions, DashboardStandalonePageContent, DashboardStandalonePageDescription, DashboardStandalonePageHeader, DashboardStandalonePageMain, DashboardStandalonePageTitle, DeprecatedBadge, EmptyState, EmptyStateActions, EmptyStateButton, EmptyStateDescription, EmptyStateExternalLink, EmptyStateHeader, EmptyStateMedia, EmptyStateTitle, FormArrayField, FormComboboxField, FormFieldBase, FormFieldShell, FormInputField, FormMultiComboboxField, FormNumericField, FormOTPField, FormRadioGroupField, FormSelectField, FormSliderField, FormSwitchField, FormTextareaField, FormToggleGroupField, NewBadge, NoDataDescription, NoDataEmptyState, NoDataMedia, NoDataTitle, NoSupportDescription, NoSupportEmptyState, NoSupportMedia, NoSupportTitle, NotFoundDescription, NotFoundEmptyState, NotFoundMedia, NotFoundTitle, ProfessionalBadge, RestrictedAccessDescription, RestrictedAccessEmptyState, RestrictedAccessMedia, RestrictedAccessTitle, RoleBadge, SaveBar, SubmitButton, TrialBadge, UnknownErrorDescription, UnknownErrorEmptyState, UnknownErrorMedia, UnknownErrorTitle, fieldContext, formContext, toFieldErrors, useCopyToClipboard, useSidebar as useDashboardSidebar, useFieldContext, useForm, useFormContext, withFieldGroup, withForm };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aircall/blocks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"sideEffects": [
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"vite": "7.3.1",
|
|
82
82
|
"vitest": "4.0.17",
|
|
83
83
|
"zod": "4.4.3",
|
|
84
|
-
"@aircall/ds": "0.
|
|
84
|
+
"@aircall/ds": "0.20.0",
|
|
85
85
|
"@aircall/hooks": "0.5.1"
|
|
86
86
|
},
|
|
87
87
|
"keywords": [
|
|
@@ -80,7 +80,7 @@ utils/data home or must stay in the consuming app.
|
|
|
80
80
|
|
|
81
81
|
**UI with no blocks/ds equivalent yet**: some `@dashboard/library` UI components have
|
|
82
82
|
no target in `@aircall/blocks` or `@aircall/ds` yet (e.g. `PieChart` and other charts,
|
|
83
|
-
`
|
|
83
|
+
`TileLegend`). If a UI component is not in the routing table below and
|
|
84
84
|
not listed above, leave it imported from `@dashboard/library` for now — do not force a
|
|
85
85
|
migration or invent a target.
|
|
86
86
|
|
|
@@ -100,6 +100,7 @@ migration or invent a target.
|
|
|
100
100
|
| `List` + `ListItem` | `ItemGroup`, `Item`, `ItemMedia`, `ItemContent`, `ItemActions` (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/list | available |
|
|
101
101
|
| `Tile` + `TileHeader` + `TileValue` | `Card` (+ parts) as the tile container (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/tile | available |
|
|
102
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
|
+
| `AudioPlayer` | `Audio`, `AudioPlayerRow`, `AudioPlayerButton`, `AudioPlayerBar`, `AudioPlayerTime`, `AudioPlayerSpeed`, `AudioPlayerSkip`, `AudioPlayerManager`, `useAudioPlayer` (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-audio-player | available |
|
|
103
104
|
| `InfoPopup` + `InfoPopupTrigger` + `InfoPopupContent` | `HoverCard`, `HoverCardTrigger`, `HoverCardContent` (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/info-popup | available |
|
|
104
105
|
| `CopyToClipboardButton` + `CopyToClipboardText` | `CopyButton`, `CopyButtonIcon`, `CopyButtonLabel` (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/copy-button | available |
|
|
105
106
|
| `ToggleRow` | `Field` (orientation="horizontal") + `Switch` + `FieldLabel` + optional `FieldDescription` (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/toggle-row | available |
|
|
@@ -29,32 +29,51 @@ Campaign Creation (`CampaignWizardPage`) and Add Contacts (`AddContactsPage`) ar
|
|
|
29
29
|
|
|
30
30
|
## 2. DashboardStandalonePage (full-page flows)
|
|
31
31
|
|
|
32
|
+
Four-part nesting: the outer `DashboardStandalonePage` is the `page-background` shell
|
|
33
|
+
(8px inset, flex row); `DashboardStandalonePageContent` is the rounded content **panel**
|
|
34
|
+
that holds the header + scroll area; the header carries a centered title (+ optional
|
|
35
|
+
description) with edge-pinned actions; `DashboardStandalonePageMain` is the scroll area.
|
|
36
|
+
|
|
32
37
|
```tsx
|
|
33
38
|
import {
|
|
34
39
|
DashboardStandalonePage,
|
|
40
|
+
DashboardStandalonePageContent,
|
|
35
41
|
DashboardStandalonePageHeader,
|
|
36
42
|
DashboardStandalonePageTitle,
|
|
43
|
+
DashboardStandalonePageDescription,
|
|
37
44
|
DashboardStandalonePageActions,
|
|
38
45
|
DashboardStandalonePageAction,
|
|
39
|
-
|
|
46
|
+
DashboardStandalonePageMain
|
|
40
47
|
} from '@aircall/blocks';
|
|
41
48
|
|
|
42
49
|
<DashboardStandalonePage>
|
|
43
|
-
<DashboardStandalonePageHeader>
|
|
44
|
-
<DashboardStandalonePageTitle>Create campaign</DashboardStandalonePageTitle>
|
|
45
|
-
<DashboardStandalonePageActions>
|
|
46
|
-
<DashboardStandalonePageAction onClick={onCancel}>Cancel</DashboardStandalonePageAction>
|
|
47
|
-
</DashboardStandalonePageActions>
|
|
48
|
-
</DashboardStandalonePageHeader>
|
|
49
50
|
<DashboardStandalonePageContent>
|
|
50
|
-
|
|
51
|
+
<DashboardStandalonePageHeader>
|
|
52
|
+
<DashboardStandalonePageTitle>Create campaign</DashboardStandalonePageTitle>
|
|
53
|
+
{/* optional subtitle under the title */}
|
|
54
|
+
<DashboardStandalonePageDescription>Set up your outbound campaign</DashboardStandalonePageDescription>
|
|
55
|
+
{/* actions pin to an edge; side="start" pins left, default "end" pins right */}
|
|
56
|
+
<DashboardStandalonePageActions>
|
|
57
|
+
<DashboardStandalonePageAction onClick={onCancel}>Cancel</DashboardStandalonePageAction>
|
|
58
|
+
</DashboardStandalonePageActions>
|
|
59
|
+
</DashboardStandalonePageHeader>
|
|
60
|
+
<DashboardStandalonePageMain>
|
|
61
|
+
{/* a centered, width-capped Card — see below */}
|
|
62
|
+
</DashboardStandalonePageMain>
|
|
51
63
|
</DashboardStandalonePageContent>
|
|
52
64
|
</DashboardStandalonePage>
|
|
53
65
|
```
|
|
54
66
|
|
|
55
|
-
|
|
67
|
+
- **Header** is a fixed 64px bar; the title (+ description) is optically centered and
|
|
68
|
+
actions are pinned to the left/right edges via `side` — declare them in any order.
|
|
69
|
+
- **Split layouts:** put a second panel (e.g. a `ChatbotPanel`) as a sibling of
|
|
70
|
+
`DashboardStandalonePageContent`; they share the shell's 8px gap. Hide a secondary
|
|
71
|
+
side panel below the `md` breakpoint (`className="hidden md:flex"`) so the main flow
|
|
72
|
+
gets the full width on narrow screens.
|
|
73
|
+
|
|
74
|
+
### What goes inside `DashboardStandalonePageMain` — a centered, width-capped Card
|
|
56
75
|
|
|
57
|
-
`
|
|
76
|
+
`DashboardStandalonePageMain` is a full-bleed scroll area; it does **not** constrain
|
|
58
77
|
width. The actual content is a centered container (`mx-auto` + a `max-w-*` cap) wrapping
|
|
59
78
|
one or more `Card`s — the consistent pattern across the `DashboardStandalonePage` stories.
|
|
60
79
|
Don't let content run full-width, and don't reach for `Paper`/raw page wrappers.
|
|
@@ -62,7 +81,7 @@ Don't let content run full-width, and don't reach for `Paper`/raw page wrappers.
|
|
|
62
81
|
```tsx
|
|
63
82
|
import { Card, CardHeader, CardContent, CardFooter } from '@aircall/ds';
|
|
64
83
|
|
|
65
|
-
<
|
|
84
|
+
<DashboardStandalonePageMain>
|
|
66
85
|
{/* single-column form / wizard */}
|
|
67
86
|
<div className="mx-auto max-w-2xl pt-4">
|
|
68
87
|
<Card>
|
|
@@ -71,13 +90,19 @@ import { Card, CardHeader, CardContent, CardFooter } from '@aircall/ds';
|
|
|
71
90
|
<CardFooter className="justify-end gap-2">{/* Back / Next / Submit */}</CardFooter>
|
|
72
91
|
</Card>
|
|
73
92
|
</div>
|
|
74
|
-
</
|
|
93
|
+
</DashboardStandalonePageMain>
|
|
75
94
|
```
|
|
76
95
|
|
|
77
96
|
Width cap by layout:
|
|
78
97
|
- **Single-column form / wizard** → `mx-auto max-w-2xl pt-4` around one `Card` (CampaignWizard pattern).
|
|
79
98
|
- **Wide / two-column** (form + summary aside) → `mx-auto flex max-w-7xl flex-col gap-6 pt-8 lg:flex-row`, a `Card` per column.
|
|
80
99
|
|
|
100
|
+
> **API note (renamed):** `DashboardStandalonePageContent` is now the rounded **panel**
|
|
101
|
+
> wrapper (header + main), and the scroll area is `DashboardStandalonePageMain` — matching
|
|
102
|
+
> the `DashboardPage` family (`Page → Content → Main`). Earlier code where
|
|
103
|
+
> `DashboardStandalonePageContent` was the scroll area must move that content into
|
|
104
|
+
> `DashboardStandalonePageMain` and wrap header+main in `DashboardStandalonePageContent`.
|
|
105
|
+
|
|
81
106
|
## 3. DashboardPage (standard in-app page)
|
|
82
107
|
|
|
83
108
|
```tsx
|
|
@@ -154,8 +179,10 @@ Wrong:
|
|
|
154
179
|
Correct:
|
|
155
180
|
```tsx
|
|
156
181
|
<DashboardStandalonePage>
|
|
157
|
-
<
|
|
158
|
-
|
|
182
|
+
<DashboardStandalonePageContent>
|
|
183
|
+
<DashboardStandalonePageHeader>...</DashboardStandalonePageHeader>
|
|
184
|
+
<DashboardStandalonePageMain>{/* wizard */}</DashboardStandalonePageMain>
|
|
185
|
+
</DashboardStandalonePageContent>
|
|
159
186
|
</DashboardStandalonePage>
|
|
160
187
|
```
|
|
161
188
|
|
|
@@ -155,6 +155,31 @@ Why one form: `useFormWizard().data` was a single accumulator across pages — a
|
|
|
155
155
|
submits the complete object. Per-step `useForm`s would fragment that state and lose
|
|
156
156
|
cross-step validation.
|
|
157
157
|
|
|
158
|
+
## Validation UX — validate on click, don't disable
|
|
159
|
+
|
|
160
|
+
The `goNext` above is the **validate-on-click** pattern, and it's the one to reach for:
|
|
161
|
+
the Next button stays enabled, and clicking it validates the step and reveals what's
|
|
162
|
+
wrong. This is usually the product requirement ("never disable the button — tell me on
|
|
163
|
+
click"). It works because a **`'submit'`-cause validation also runs the field's
|
|
164
|
+
`onChange` validator** — so `form.validateField(name, 'submit')` populates errors even
|
|
165
|
+
for fields the user never touched, and each `Form*Field` renders them through its own
|
|
166
|
+
`FieldError`. (Verify against `form-core`'s `defaultValidationLogic`: the `submit` case
|
|
167
|
+
runs `[onChange, onBlur, onSubmit, …]`.)
|
|
168
|
+
|
|
169
|
+
- **Cross-field rules** ("at least one of A or B") can't be a single-field validator.
|
|
170
|
+
Put them on the parent `useForm`'s `validators`, or `safeParse` a step schema inside
|
|
171
|
+
`goNext`, and render the message where it belongs (e.g. a section-level `FieldError`).
|
|
172
|
+
- **i18n:** function validators can `return t(key)` directly; if you use a schema
|
|
173
|
+
library, build the schema in a factory that receives `t` (module-scope schemas can't
|
|
174
|
+
call `t`).
|
|
175
|
+
- If the design genuinely wants the button **disabled** until valid, subscribe
|
|
176
|
+
reactively instead — but this is the exception, not the default:
|
|
177
|
+
```tsx
|
|
178
|
+
<form.Subscribe selector={s => isStepValid(s.values)}>
|
|
179
|
+
{ok => <Button disabled={!ok} onClick={() => setStep(next.id)}>Next</Button>}
|
|
180
|
+
</form.Subscribe>
|
|
181
|
+
```
|
|
182
|
+
|
|
158
183
|
## Prop mapping
|
|
159
184
|
|
|
160
185
|
| `FormWizardProps` / `useFormWizard` | After |
|
|
@@ -192,6 +217,22 @@ one field.
|
|
|
192
217
|
**`necessityIndicator="required" | "optional"`** — mark only the exceptions (the few optional fields
|
|
193
218
|
in a mostly-required form, or vice versa); never mix both markers in one form.
|
|
194
219
|
|
|
220
|
+
**Layout — `orientation` / `controlPosition`** — every wrapper defaults to a **vertical** field
|
|
221
|
+
(label above the control). Two shared props flip to a side-by-side layout, reusing the DS `Field`
|
|
222
|
+
orientation:
|
|
223
|
+
- `orientation="horizontal"` puts the label/description beside the control. `orientation="responsive"`
|
|
224
|
+
is vertical on narrow screens and horizontal once wide — it needs a `FieldGroup` ancestor (which
|
|
225
|
+
provides the `@container/field-group` the responsive variant reads).
|
|
226
|
+
- `controlPosition="start" | "end"` picks the control's side. Defaults per control:
|
|
227
|
+
`FormSwitchField` → `'start'` (switch left of its label), text/select/etc. → `'end'`.
|
|
228
|
+
- `FormSwitchField` **defaults to the inline (horizontal) layout** — a switch stacked under its label
|
|
229
|
+
is rarely wanted. A settings toggle: `<FormSwitchField label description>` (switch on the left);
|
|
230
|
+
pin the switch to the right with `controlPosition="end"`. Force the old stacked look with
|
|
231
|
+
`orientation="vertical"`.
|
|
232
|
+
- A responsive text field (label + description on the left, input + error on the right on desktop,
|
|
233
|
+
stacked on mobile): `<FormInputField orientation="responsive" label description … />` inside a
|
|
234
|
+
`FieldGroup`.
|
|
235
|
+
|
|
195
236
|
## Fields reference
|
|
196
237
|
|
|
197
238
|
`@aircall/blocks` ships typed wrappers for all common DS controls; each takes `form`,
|
|
@@ -204,9 +245,45 @@ spread the control bundle onto the matching DS primitive.
|
|
|
204
245
|
| text/email/password | `FormInputField` | radio group | `FormRadioGroupField` |
|
|
205
246
|
| select | `FormSelectField` | OTP | `FormOTPField` |
|
|
206
247
|
| combobox (single) | `FormComboboxField` | switch | `FormSwitchField` |
|
|
207
|
-
| combobox (multi) | `FormMultiComboboxField` | number | `
|
|
248
|
+
| combobox (multi) | `FormMultiComboboxField` | number | `FormNumericField` |
|
|
208
249
|
| textarea | `FormTextareaField` | slider | `FormSliderField` |
|
|
209
250
|
|
|
251
|
+
## Multi-step gotchas
|
|
252
|
+
|
|
253
|
+
The `Stepper` **unmounts inactive `StepperContent`** (there's no `forceMount`). That's
|
|
254
|
+
usually fine — but two consequences bite:
|
|
255
|
+
|
|
256
|
+
- Field **values** persist in the form store across unmount, and so does field **meta**
|
|
257
|
+
(`isDirty` / `isTouched`) — a field's `mount()` cleanup is a no-op. So you can read
|
|
258
|
+
`form.state.fieldMeta['step1.field'].isDirty` while on step 3, and edits made on an
|
|
259
|
+
earlier step are still there when you submit. Add `forceMount` only if a step must
|
|
260
|
+
stay in the DOM.
|
|
261
|
+
- **Re-basing a saved draft: do NOT use `form.reset(values)`.** `reset` clears
|
|
262
|
+
`isTouched`, and when an unmounted step later remounts its field re-seeds to the
|
|
263
|
+
(empty) default — silently wiping the user's value. To mark the current values as
|
|
264
|
+
"synced" (e.g. after an autosave / draft save) while keeping them editable, clear
|
|
265
|
+
`isDirty` per field instead:
|
|
266
|
+
```tsx
|
|
267
|
+
for (const path of Object.keys(form.state.fieldMeta))
|
|
268
|
+
form.setFieldMeta(path, m => ({ ...m, isDirty: false }));
|
|
269
|
+
```
|
|
270
|
+
Now `isDirty` marks exactly what changed since the last sync — ideal for sending a
|
|
271
|
+
minimal update patch on the next save.
|
|
272
|
+
|
|
273
|
+
## Field groups for large steps (optional)
|
|
274
|
+
|
|
275
|
+
For steps with many fields you can extract each into a `withFieldGroup` bound to a
|
|
276
|
+
subtree of the form, instead of listing everything in one flat `useForm`. Two things
|
|
277
|
+
to know:
|
|
278
|
+
|
|
279
|
+
- `fields` accepts a **subtree key** (`fields="general"`) **or** a **`FieldsMap`**
|
|
280
|
+
(`fields={{ groupField: 'form.path' }}`) — the map lets a group bind onto a flat
|
|
281
|
+
model without restructuring it.
|
|
282
|
+
- A field group has **no group-level `validators`**. Put cross-field rules on a
|
|
283
|
+
field-level validator that reads its sibling via `fieldApi.form.getFieldValue(...)`,
|
|
284
|
+
or on the parent `useForm`. Use `group.Subscribe` for reactive reads inside the group
|
|
285
|
+
(its `state.values` is the group's subset).
|
|
286
|
+
|
|
210
287
|
## Common Mistakes
|
|
211
288
|
|
|
212
289
|
### 1. Fabricating a `Stepper` instance API (`useStepper({steps})`, `nextStep`, `stepper=` prop)
|
|
@@ -288,3 +365,38 @@ Correct:
|
|
|
288
365
|
`SubmitButton`/`CardSaveBar` are registered form components — they read `canSubmit`/`isSubmitting` from form context and must be rendered via `form.AppForm`, not handed a `form` prop. They must also sit inside the `<form>` so their `type="submit"` triggers `onSubmit`.
|
|
289
366
|
|
|
290
367
|
Source: `packages/blocks/src/form/use-form.ts`
|
|
368
|
+
|
|
369
|
+
### 5. Re-basing a multi-step draft with `form.reset`
|
|
370
|
+
|
|
371
|
+
Wrong:
|
|
372
|
+
```tsx
|
|
373
|
+
await saveDraft(form.state.values);
|
|
374
|
+
form.reset(form.state.values); // clears isTouched → remounted steps re-seed to empty
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Correct:
|
|
378
|
+
```tsx
|
|
379
|
+
await saveDraft(form.state.values);
|
|
380
|
+
for (const path of Object.keys(form.state.fieldMeta))
|
|
381
|
+
form.setFieldMeta(path, m => ({ ...m, isDirty: false })); // keeps values + isTouched
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
`form.reset` is right for a single-screen form, but inside a `Stepper` (which unmounts inactive steps) it makes fields re-seed to their defaults on the next remount, wiping user input. Clearing `isDirty` re-bases the "changed since last sync" baseline without touching values.
|
|
385
|
+
|
|
386
|
+
Source: `packages/blocks/src/form/use-form.ts`
|
|
387
|
+
|
|
388
|
+
## Testing (jsdom)
|
|
389
|
+
|
|
390
|
+
- **Drive navigation one step at a time.** `Next` handlers are async (validation runs
|
|
391
|
+
before advancing), so fire the click, **wait for the next step to render**, then
|
|
392
|
+
continue. Two back-to-back clicks race the async nav and both land on the same step.
|
|
393
|
+
```tsx
|
|
394
|
+
fireEvent.click(getByTestId('next'));
|
|
395
|
+
await screen.findByText('Role'); // step 2 is on screen
|
|
396
|
+
fireEvent.click(getByTestId('next'));
|
|
397
|
+
```
|
|
398
|
+
- **Assert errors without filling every field.** Because a `'submit'`-cause validation
|
|
399
|
+
runs the `onChange` validators (see "Validation UX"), clicking Next on an empty step
|
|
400
|
+
surfaces each field's `FieldError` — assert on those directly.
|
|
401
|
+
- **Field meta survives step changes**, so a test can edit a field on step 1, navigate
|
|
402
|
+
to a later step, submit, and still see the step-1 value in the payload.
|