@aircall/blocks
Version:
Aircall Blocks — higher-level UI compositions built on @aircall/ds
260 lines (186 loc) • 7.87 kB
Markdown
---
name: aircall-blocks/migrate-dashboard/loading
description: >
Migrate /library Loading, Loader, and Spinner to @aircall/ds
Spinner (animated spinner) and Skeleton (content placeholder). Load when a
file imports Loading, Loader, or Spinner from /library.
type: sub-skill
library: aircall-blocks
requires:
- aircall-blocks/setup
- aircall-blocks/migrate-dashboard
sources:
- "aircall/hydra:packages/ds/src/index.ts"
---
This skill builds on aircall-blocks/migrate-dashboard.
## 1. Component mapping
| `/library` | `@aircall/ds` | Notes |
| --- | --- | --- |
| `Loading` | `Spinner` | Full-area centered spinner — replace the flex wrapper with a parent `div` using Tailwind layout classes |
| `Loader` | `Spinner` | Inline spinning icon — direct 1-to-1 replacement |
| `Spinner` | `Spinner` | Already a spinner — direct 1-to-1 replacement |
**Content placeholders:** when `Loading` was used while async content loads (e.g. inside a card or list panel), prefer `Skeleton` over `Spinner` — it better communicates that content is incoming rather than a blocking operation.
## 2. Verified DS exports (`packages/ds/src/index.ts`)
```
Spinner, spinnerVariants
Skeleton
```
## 3. Prop mapping
### `Loading` → `Spinner`
| `/library` prop | DS equivalent | Action |
| --- | --- | --- |
| `size` (number, e.g. `40`) | `size` (`"sm"` \| `"default"` \| `"lg"` \| `"xl"`) | Map to the nearest token (see table below) |
| `color` (tractor color token, e.g. `"primary-500"`) | `className` | Use a Tailwind text-color class |
| `data-test` | `data-test` | Pass through as-is |
| Flex layout (`FlexProps`) | Tailwind parent | Wrap `Spinner` in a container `<div className="flex h-full w-full items-center justify-center">` |
**Size mapping:**
| Old `size` (px) | New `size` token |
| --- | --- |
| ≤ 12 | `"sm"` (12 px) |
| 13–16 | `"default"` (16 px) |
| 17–20 | `"lg"` (20 px) |
| ≥ 21 | `"xl"` (24 px) |
When `size` was not set, `Loading` defaulted to `40` and `Spinner` defaulted to `30` — both map to `"xl"`.
### `Loader` → `Spinner`
| `/library` prop | DS equivalent | Action |
| --- | --- | --- |
| `size` (number) | `size` (token string) | Map using the table above |
| `color` | `className` | Use a Tailwind text-color class |
| `data-test` | `data-test` | Pass through as-is |
### `Skeleton`
`Skeleton` has no direct `/library` predecessor — it is introduced as a
replacement for `Loading` when used during content fetch. It accepts a plain `<div>`
`className` for sizing and shape.
## 4. Before / After examples
### 4a. `Loading` (full-area centered spinner)
**Before (`/library`):**
```tsx
import { Loading } from '@dashboard/library';
<Loading data-test="reports-loading" size={40} color="primary-500" />
```
**After (`/ds`):**
```tsx
import { Spinner } from '@aircall/ds';
<div
data-test="reports-loading"
className="flex h-full w-full items-center justify-center"
>
<Spinner size="xl" className="text-primary" />
</div>
```
Key changes:
- The flex-center wrapper that `Loading` rendered internally must now be an explicit parent `<div>`.
- Numeric `size` becomes a size token string.
- Tractor color token (`"primary-500"`) becomes a Tailwind text-color class (`"text-primary"`).
### 4b. `Loader` (inline spinner icon)
**Before (`/library`):**
```tsx
import { Loader } from '@dashboard/library';
<Loader size={20} color="neutral-700" />
```
**After (`/ds`):**
```tsx
import { Spinner } from '@aircall/ds';
<Spinner size="lg" className="text-neutral-700" />
```
`Loader` was already a bare spinning icon with no layout wrapper — `Spinner` is the direct replacement.
### 4c. `Spinner` (bare spinning icon)
**Before (`/library`):**
```tsx
import { Spinner } from '@dashboard/library';
<Spinner size={30} />
```
**After (`/ds`):**
```tsx
import { Spinner } from '@aircall/ds';
<Spinner size="xl" />
```
The API is nearly identical; only the `size` type changes from a numeric pixel value to a string token.
### 4d. `Loading` as a content placeholder (prefer `Skeleton`)
**Before (`/library`):**
```tsx
import { Loading } from '@dashboard/library';
function StatsCard({ isLoading, value }) {
return (
<div className="p-4">
{isLoading ? <Loading size={24} /> : <span>{value}</span>}
</div>
);
}
```
**After (`/ds`):**
```tsx
import { Skeleton } from '@aircall/ds';
function StatsCard({ isLoading, value }) {
return (
<div className="p-4">
{isLoading ? <Skeleton className="h-6 w-24" /> : <span>{value}</span>}
</div>
);
}
```
Use `Skeleton` when the spinner was filling the space of specific content that will
appear once data loads. `Skeleton` communicates the shape of the incoming content, which
reduces layout shift and improves perceived performance compared to a generic spinner.
## 5. Common Mistakes
### Mistake 1 — Keeping the `Loading` flex wrapper implicit
```tsx
// WRONG — Spinner has no built-in flex-center layout; it renders as an inline SVG
import { Spinner } from '@aircall/ds';
<Spinner size="xl" className="h-full w-full" />
// CORRECT — wrap in an explicit flex container to center in the available area
import { Spinner } from '@aircall/ds';
<div className="flex h-full w-full items-center justify-center">
<Spinner size="xl" />
</div>
```
`Loading` rendered its own `<Flex … alignItems="center" justifyContent="center">` internally. DS `Spinner` is a bare SVG icon; the centering layout must be provided by the caller.
Source: `packages/ds/src/components/spinner.tsx`
---
### Mistake 2 — Passing a numeric pixel value to `size`
```tsx
// WRONG — size accepts only the four named tokens; a number is a type error
import { Spinner } from '@aircall/ds';
<Spinner size={30} />
// CORRECT — use the nearest token string
import { Spinner } from '@aircall/ds';
<Spinner size="xl" />
```
`Spinner.Props` extends `VariantProps<typeof spinnerVariants>` where `size` is
`"sm" | "default" | "lg" | "xl" | null | undefined`. Passing a number does not satisfy
the type and will error at compile time.
Source: `packages/ds/src/components/spinner.tsx`
---
### Mistake 3 — Passing a Tractor color token to `className`
```tsx
// WRONG — Tractor color tokens are not valid Tailwind classes and produce no styling
import { Spinner } from '@aircall/ds';
<Spinner className="primary-500" />
// CORRECT — use a Tailwind text-color utility class
import { Spinner } from '@aircall/ds';
<Spinner className="text-primary" />
```
`Spinner` renders a `Loader2Icon` SVG that inherits its color from the CSS `currentColor`
(the `text-current` Tailwind class is baked in via `spinnerVariants`). To tint it, apply
a `text-*` Tailwind class — a bare Tractor token string like `"primary-500"` is not a
recognized utility and has no effect.
Source: `packages/ds/src/components/spinner.tsx`
---
### Mistake 4 — Using `Spinner` for content-area placeholders instead of `Skeleton`
```tsx
// WRONG — a spinner implies an active blocking operation, not an inline content gap
import { Spinner } from '@aircall/ds';
function UserName({ isLoading, name }) {
return isLoading ? <Spinner size="sm" /> : <span>{name}</span>;
}
// CORRECT — use Skeleton to represent the shape of the incoming content
import { Skeleton } from '@aircall/ds';
function UserName({ isLoading, name }) {
return isLoading ? <Skeleton className="h-4 w-32" /> : <span>{name}</span>;
}
```
`Skeleton` (an animated pulse `<div>`) is semantically and visually correct for
inline content placeholders because it mirrors the dimensions of the element it replaces.
`Spinner` is intended for full-area or button loading states where the action source is
explicit.
Source: `packages/ds/src/components/skeleton.tsx`