---
name: aircall-blocks/migrate-dashboard/loading
description: >
  Migrate @dashboard/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 @dashboard/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

| `@dashboard/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`

| `@dashboard/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`

| `@dashboard/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 `@dashboard/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 (`@dashboard/library`):**
```tsx
import { Loading } from '@dashboard/library';

<Loading data-test="reports-loading" size={40} color="primary-500" />
```

**After (`@aircall/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 (`@dashboard/library`):**
```tsx
import { Loader } from '@dashboard/library';

<Loader size={20} color="neutral-700" />
```

**After (`@aircall/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 (`@dashboard/library`):**
```tsx
import { Spinner } from '@dashboard/library';

<Spinner size={30} />
```

**After (`@aircall/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 (`@dashboard/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 (`@aircall/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`
