UNPKG

@aircall/blocks

Version:

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

347 lines (278 loc) 12 kB
--- name: aircall-blocks/migrate-dashboard/empty-states description: > Migrate @dashboard/library MessageScreen family (ComingSoonScreen, RestrictedAccessScreen, NotFoundScreen, NoDataScreen, NoSupportScreen, UnknownErrorScreen) to @aircall/blocks composable empty-state blocks. Load when a file imports any of those from @dashboard/library. type: sub-skill library: aircall-blocks requires: - aircall-blocks/setup - aircall-blocks/migrate-dashboard sources: - "aircall/hydra:docs/migration-guides/tractor-to-ds/recipes/empty-states.md" --- This skill builds on aircall-blocks/migrate-dashboard. ## 1. Variant mapping | `@dashboard/library` | `@aircall/blocks` root | Old asset | New icon | | --- | --- | --- | --- | | `ComingSoonScreen` | `ComingSoonEmptyState` | `aircall-placeholder.svg` | `Sprout` | | `RestrictedAccessScreen` | `RestrictedAccessEmptyState` | `lock.svg` | `LockKeyhole` | | `NotFoundScreen` | `NotFoundEmptyState` | `not-found.svg` | `Search` | | `NoDataScreen` | `NoDataEmptyState` | `no-data.svg` | `Search` | | `NoSupportScreen` | `NoSupportEmptyState` | `settings.svg` | `Settings` | | `UnknownErrorScreen` | `UnknownErrorEmptyState` | `warning.svg` | `TriangleAlert` | Each preset exports four individually importable parts — `*EmptyState` (root), `*Media`, `*Title`, `*Description` — all from `@aircall/blocks`. ## 2. Prop mapping | `MessageScreen` prop | New equivalent | | --- | --- | | `title` | children of `*Title` — omit to use the baked-in design copy | | `subtitle` (string or element) | children of `*Description` | | `buttonText` + `onClick` | `EmptyStateButton` inside `EmptyStateActions` | | `image` | fixed per variant — drop it | | `data-test` | pass to the root: `<NotFoundEmptyState data-test="…">` | | `w` / `h` / `backgroundColor` | drop — constrain the parent instead | ## 3. Before / After examples ### 3a. NotFoundScreen → NotFoundEmptyState **Before (`@dashboard/library`):** ```tsx import { NotFoundScreen } from '@dashboard/library'; <NotFoundScreen title="We couldn't find that page" subtitle="It may have been moved, deleted, or the link may be incorrect." buttonText="Go to inbox" onClick={goToInbox} /> ``` **After (`@aircall/blocks`):** ```tsx import { NotFoundEmptyState, NotFoundMedia, NotFoundTitle, NotFoundDescription, EmptyStateHeader, EmptyStateActions, EmptyStateButton } from '@aircall/blocks'; <NotFoundEmptyState> <EmptyStateHeader> <NotFoundMedia /> <NotFoundTitle /> <NotFoundDescription /> </EmptyStateHeader> <EmptyStateActions> <EmptyStateButton variant="outline" onClick={goToInbox}> Go to inbox </EmptyStateButton> </EmptyStateActions> </NotFoundEmptyState> ``` The `*Title` and `*Description` ship the Figma copy as defaults — drop `title`/`subtitle` props to adopt them, or override by passing children. ### 3b. ComingSoonScreen → ComingSoonEmptyState (two buttons) **Before (`@dashboard/library`):** ```tsx import { ComingSoonScreen } from '@dashboard/library'; <ComingSoonScreen title="Coming soon!" subtitle="You'll be able to access this feature in the near future." buttonText="Go back" onClick={goBack} /> ``` **After (`@aircall/blocks`):** ```tsx import { ComingSoonEmptyState, ComingSoonMedia, ComingSoonTitle, ComingSoonDescription, EmptyStateHeader, EmptyStateActions, EmptyStateButton, EmptyStateExternalLink } from '@aircall/blocks'; <ComingSoonEmptyState> <EmptyStateHeader> <ComingSoonMedia /> <ComingSoonTitle /> <ComingSoonDescription /> </EmptyStateHeader> <EmptyStateActions> <EmptyStateButton variant="outline" onClick={goBack}>Go back</EmptyStateButton> </EmptyStateActions> <EmptyStateExternalLink href={docsUrl} /> </ComingSoonEmptyState> ``` `EmptyStateExternalLink` is a sibling of `EmptyStateActions` (not inside it) so it sits on its own line below the buttons. ### 3c. UnknownErrorScreen → UnknownErrorEmptyState (error state) **Before (`@dashboard/library`):** ```tsx import { UnknownErrorScreen } from '@dashboard/library'; <UnknownErrorScreen title="Something went wrong" subtitle="Please try again." buttonText="Try again" onClick={reload} /> ``` **After (`@aircall/blocks`):** ```tsx import { UnknownErrorEmptyState, UnknownErrorMedia, UnknownErrorTitle, UnknownErrorDescription, EmptyStateHeader, EmptyStateActions, EmptyStateButton } from '@aircall/blocks'; <UnknownErrorEmptyState> <EmptyStateHeader> <UnknownErrorMedia /> <UnknownErrorTitle /> <UnknownErrorDescription /> </EmptyStateHeader> <EmptyStateActions> <EmptyStateButton variant="default" onClick={reload}>Try again</EmptyStateButton> </EmptyStateActions> </UnknownErrorEmptyState> ``` `UnknownErrorEmptyState` carries `role="alert"` — it announces assertively to screen readers when it mounts. `NoDataEmptyState` carries `role="status"` (polite). The others are static page content with no live region. ### 3d. RestrictedAccessScreen → RestrictedAccessEmptyState (learn more link) **Before (`@dashboard/library`):** ```tsx import { RestrictedAccessScreen } from '@dashboard/library'; <RestrictedAccessScreen title="You don't have access to this page." subtitle="Contact an admin on your team to get access." buttonText="Learn more" onClick={() => window.open(docsUrl)} /> ``` **After (`@aircall/blocks`):** ```tsx import { RestrictedAccessEmptyState, RestrictedAccessMedia, RestrictedAccessTitle, RestrictedAccessDescription, EmptyStateHeader, EmptyStateExternalLink } from '@aircall/blocks'; <RestrictedAccessEmptyState> <EmptyStateHeader> <RestrictedAccessMedia /> <RestrictedAccessTitle /> <RestrictedAccessDescription /> </EmptyStateHeader> <EmptyStateExternalLink href={docsUrl} /> </RestrictedAccessEmptyState> ``` Use `EmptyStateExternalLink` (renders a real `<a target="_blank">`) instead of an `onClick` that calls `window.open`. ### 3e. Custom / one-off empty state (no variant) When migrating a bespoke `MessageScreen` usage (custom `image` prop), use the generic family directly: ```tsx import { EmptyState, EmptyStateHeader, EmptyStateMedia, EmptyStateTitle, EmptyStateDescription, EmptyStateActions, EmptyStateButton } from '@aircall/blocks'; import { Sparkles } from '@aircall/react-icons'; <EmptyState> <EmptyStateHeader> <EmptyStateMedia> <Sparkles /> </EmptyStateMedia> <EmptyStateTitle>No integrations yet</EmptyStateTitle> <EmptyStateDescription>Connect your first integration to get started.</EmptyStateDescription> </EmptyStateHeader> <EmptyStateActions> <EmptyStateButton variant="default" onClick={openCatalog}>Browse catalog</EmptyStateButton> </EmptyStateActions> </EmptyState> ``` Icons must come from `@aircall/react-icons`, never from `lucide-react` directly. ## 4. Common Mistakes ### Mistake 1: Passing `title`/`subtitle` as props to the root ```tsx // WRONG — the root block has no title/subtitle props; they are silently ignored <NotFoundEmptyState title="Page not found" subtitle="Check the URL." /> // CORRECT — pass custom copy as children of the *Title / *Description parts <NotFoundEmptyState> <EmptyStateHeader> <NotFoundMedia /> <NotFoundTitle>Page not found</NotFoundTitle> <NotFoundDescription>Check the URL.</NotFoundDescription> </EmptyStateHeader> </NotFoundEmptyState> ``` Mechanism: the old `MessageScreen` was a single component that accepted flat string props. The new blocks are a composable family; the root only accepts layout-level props (`data-test`, `role`, `className`, …). Title and description are separate sub-components. Source: `packages/blocks/src/components/empty-state.tsx``EmptyState.Props` extends `React.ComponentProps<typeof Empty>`, which has no `title` or `subtitle`. --- ### Mistake 2: Placing `EmptyStateExternalLink` inside `EmptyStateActions` ```tsx // WRONG — the link ends up in the same flex row as the buttons <EmptyStateActions> <EmptyStateButton variant="outline" onClick={goBack}>Go back</EmptyStateButton> <EmptyStateExternalLink href={docsUrl} /> </EmptyStateActions> // CORRECT — place it as a sibling after EmptyStateActions <EmptyStateActions> <EmptyStateButton variant="outline" onClick={goBack}>Go back</EmptyStateButton> </EmptyStateActions> <EmptyStateExternalLink href={docsUrl} /> ``` Mechanism: `EmptyStateActions` is a flex row (`flex-row flex-wrap justify-center gap-3`). Placing the link inside it makes it appear inline with the buttons at the same level, breaking the stacked design where the link sits on its own line below. Source: `packages/blocks/src/components/empty-state.tsx``EmptyStateActions` renders `EmptyContent` with `flex-row` class; `EmptyStateExternalLink` is meant to be a sibling in the `EmptyState` root's column flow. --- ### Mistake 3: Setting `size` on `EmptyStateButton` ```tsx // WRONG — size is locked; passing it is a type error and overrides the design spec <EmptyStateButton variant="outline" size="sm" onClick={goBack}>Go back</EmptyStateButton> // CORRECT — omit size; only variant is yours to choose <EmptyStateButton variant="outline" onClick={goBack}>Go back</EmptyStateButton> ``` Mechanism: `EmptyStateButtonProps` is `Omit<React.ComponentProps<typeof Button>, 'size'>``size` is explicitly removed from the type. The button always renders at `size="default"` to keep all empty states visually consistent regardless of the surrounding UI. Source: `packages/blocks/src/components/empty-state.tsx``EmptyStateButton` wraps `Button` with `size="default"` hardcoded; the `size` key is stripped from the exported props type. --- ### Mistake 4: Carrying over `w`, `h`, or `backgroundColor` from `Gap` props ```tsx // WRONG — these props do not exist on the new blocks root <NotFoundEmptyState w="100%" h="100%" backgroundColor="neutral-100" /> // CORRECT — the block fills its container and centers automatically; constrain the parent <div className="h-full w-full"> <NotFoundEmptyState /> </div> ``` Mechanism: the old `MessageScreen` extended `GapProps` (Tractor) which exposed `w`/`h`/`backgroundColor` to fill the viewport. The new `*EmptyState` roots extend `React.ComponentProps<typeof Empty>` (DS), which is a plain `<div>` with Tailwind layout — these props do not exist. Source: `packages/blocks/src/components/empty-state.tsx``EmptyStateProps` extends `React.ComponentProps<typeof Empty>`; `packages/blocks/src/components/coming-soon.tsx``ComingSoonEmptyState` is `ComingSoon.Root` with the same props shape. --- ### Mistake 5: Importing icons from `lucide-react` directly in generic empty states ```tsx // WRONG — imports lucide-react directly, bypassing Aircall's icon layer import { Sparkles } from 'lucide-react'; import { EmptyState, EmptyStateHeader, EmptyStateMedia, EmptyStateTitle } from '@aircall/blocks'; <EmptyState> <EmptyStateHeader> <EmptyStateMedia><Sparkles /></EmptyStateMedia> <EmptyStateTitle>No integrations yet</EmptyStateTitle> </EmptyStateHeader> </EmptyState> // CORRECT — always import from @aircall/react-icons import { Sparkles } from '@aircall/react-icons'; import { EmptyState, EmptyStateHeader, EmptyStateMedia, EmptyStateTitle } from '@aircall/blocks'; <EmptyState> <EmptyStateHeader> <EmptyStateMedia><Sparkles /></EmptyStateMedia> <EmptyStateTitle>No integrations yet</EmptyStateTitle> </EmptyStateHeader> </EmptyState> ``` Mechanism: `@aircall/react-icons` re-exports all lucide icons plus Aircall custom icons; importing from `lucide-react` directly bypasses any Aircall overrides and breaks the single icon source-of-truth. All six built-in presets source their icons through `@aircall/react-icons` — custom media slots should follow the same convention. Source: `packages/blocks/src/components/coming-soon.tsx` — imports `Sprout` from `@aircall/react-icons`, not from `lucide-react`.