<!-- Generated by scripts/build-agent-kit.ts for @aistrike-dev/ui@5.0.1. Do not edit. -->

# Skeleton vs Progress vs EmptyState vs Alert

<!-- use-when: Nothing to show yet: loading, running, genuinely empty, or failed. -->

Every data-driven view has four states beyond "here is the data", and they are routinely conflated — a
spinner shown where there is simply nothing to show, an "empty" message that is really a failed request.
The question that decides it is: **why is there nothing on screen?**

| State | Meaning | Component | Ends when |
| --- | --- | --- | --- |
| Loading, shape known | Data is coming, we know its layout | `Skeleton` | The data arrives |
| Working, shape unknown | Something is running | `Progress` | The operation finishes |
| Loaded, nothing to show | The request succeeded and returned nothing | `EmptyState` | The user changes something |
| Failed | The request did not succeed | `Alert` | The user retries, or it recovers |

Getting this wrong is not cosmetic. A spinner where an `EmptyState` belongs makes users wait for data
that is never coming; an `EmptyState` where an `Alert` belongs tells them there are no findings when
really the scanner is down.

## Decision flow

```
Has the request finished?
        │
        ├── No — it is in flight
        │     ├── Do we know the shape of what is coming
        │     │   (a table, a list of cards)? ─────────► Skeleton
        │     └── No, or it is an operation rather than
        │         a fetch (upload, scan, save) ─────────► Progress
        │
        └── Yes
              ├── Did it fail? ─────────────────────────► Alert (severity="error")
              ├── Did it succeed with no results? ──────► EmptyState
              └── Did it succeed with partial results? ──► the data, plus an Alert
```

## Skeleton

Placeholders in **the shape of the content that is coming**. Use it for initial loads where the layout
is known, because it prevents the layout shift a spinner causes and tells the user what to expect.

```tsx
import { Skeleton } from '@aistrike-dev/ui';
import { Stack } from '@mui/material';

<Stack spacing={1} aria-busy>
  {Array.from({ length: 5 }, (_, i) => (
    <Skeleton key={i} variant="rectangular" height={48} />
  ))}
</Stack>
```

**Practical limits:** match the real content's shape and count — five skeleton rows for a table that
renders five rows. Pick `variant` to match (`text`, `circular`, `rectangular`, `rounded`). Mark the
region `aria-busy` while loading. Never leave skeletons up after the data arrives, and never use them
to fill a genuinely empty result: that is an `EmptyState`.

## Progress

An indicator that **an operation is running**. Use it when there is no content shape to mimic, or when
the thing in progress is an action rather than a fetch.

**Reach for it when:**

- A file upload or export with a known percentage.
- A scan or long job running in the background.
- A button's own loading state (`Button` and `IconButton` have `loading` built in — prefer that).
- A page-level indeterminate bar while a route loads.

```tsx
import { Progress, Typography } from '@aistrike-dev/ui';
import { Stack } from '@mui/material';

<Stack spacing={1}>
  <Typography variant="body1">Exporting findings… {percent}%</Typography>
  <Progress variant="determinate" value={percent} />
</Stack>
```

**Practical limits:** if you know the percentage, pass `variant="determinate"` with `value` —
indeterminate progress where a percentage exists tells the user less than you know. Always give it
surrounding text for context; a bare bar does not say what is happening. For a button, use the
component's own `loading` prop rather than nesting a spinner. For a table or list load, `Skeleton`
communicates more.

## EmptyState

The request **succeeded and there is nothing to show**. This is a designed state with a title, an
explanation, and usually the action that resolves it — not an absence.

**Reach for it when:**

- A table or list has no rows.
- A search or filter returned no matches.
- First-run: the user has not created anything yet.

```tsx
import { EmptyState, Button } from '@aistrike-dev/ui';

<EmptyState
  title="No findings match these filters"
  description="Try widening the severity range or clearing the date filter."
  action={<Button variant="secondary" onClick={clearFilters}>Clear filters</Button>}
/>
```

**Practical limits:** `title` is required. Distinguish the three cases in the copy, because the user's
next move differs: *nothing exists yet* ("Add your first asset"), *nothing matches* ("Clear filters"),
*nothing is assigned to you* ("View all findings"). Give it an `action` whenever there is an obvious next
step. Use `dense` inside a widget or a small panel. Never use it to cover a load in progress, and never
to report a failure.

## Alert

The request **failed**, or succeeded only partly. See the feedback guide for `Alert` versus `Snackbar`;
what matters here is that a failure is never an empty state.

```tsx
import { Alert, AlertTitle, Button } from '@aistrike-dev/ui';

<Alert severity="error" action={<Button variant="ghost" size="small" onClick={retry}>Retry</Button>}>
  <AlertTitle>Could not load findings</AlertTitle>
  The scanner is unreachable. Showing the last successful run.
</Alert>
```

**Practical limits:** say what failed and what the user can do. Include a retry action when retrying is
possible. When the data loaded but is incomplete, render the data *and* an `Alert` — do not hide partial
results behind an error.

## Worked examples

| Scenario | Component | Why |
| --- | --- | --- |
| Findings table on first paint | `Skeleton` rows | Shape is known; avoids layout shift |
| Dashboard widgets on first paint | `Skeleton` in each widget | Known layout, per-widget |
| Filters applied, request in flight | `Skeleton`, or keep the old rows dimmed | Shape is known |
| CSV export, 40% done | `Progress variant="determinate"` | Percentage is known |
| Background scan running | `Progress` (indeterminate) with context text | No shape, no percentage |
| Save button waiting on the server | `Button loading` | The component ships this |
| Table loaded, zero rows, no filters | `EmptyState` "No assets yet" + Add action | First-run |
| Table loaded, zero rows, filters set | `EmptyState` "No matches" + Clear filters | Different next step |
| Search returned nothing | `EmptyState` | Succeeded with no results |
| Request returned 500 | `Alert severity="error"` + Retry | It failed |
| Data loaded, 2 of 5 sources timed out | The data + `Alert severity="warning"` | Partial success |
| Table loaded, zero rows, because the API failed | `Alert`, not `EmptyState` | It failed; "empty" would be a lie |

## Anti-patterns

- **An `EmptyState` after a failed request.** It tells the user there is nothing when really the fetch
  broke. Failures are `Alert`s.
- **A `Skeleton` left up after data arrives**, or shown for a result that is genuinely empty.
- **A bare spinner for a table load.** `Skeleton` says what is coming and does not shift the layout.
- **Indeterminate `Progress` when the percentage is known.** Pass `variant="determinate"` and `value`.
- **A `Progress` with no surrounding text.** A bar alone does not say what is happening.
- **A spinner inside a `Button`.** Use its `loading` prop.
- **The same empty copy for every case.** "No results" gives the user no idea whether to clear a filter
  or create their first record.
- **An `EmptyState` with no action** when there is an obvious next step.
- **Hiding partial data behind an error.** Show what loaded, plus an `Alert` about what did not.
- **A hand-rolled "no data" `Box` with centred text.** `EmptyState` exists, and it announces itself.

## Accessibility

- `Skeleton` conveys nothing to assistive tech on its own. Mark the loading region `aria-busy` and make
  sure the content is announced when it arrives.
- `Progress` exposes `aria-valuenow` in its determinate form. Give it a label so the value has context.
- `EmptyState` renders `role="status"`, so the message is announced when it appears — another reason not
  to hand-roll one.
- `Alert` renders `role="alert"` and is announced immediately. Place it where the user will find the
  thing it refers to.
- Never rely on colour or a moving graphic alone to communicate state; the text is what gets read.

## When you are unsure, ask

The ambiguous cases are **an empty result that might be a silent failure**, and **which loading
treatment fits a mixed screen** where some regions know their shape and others do not. **If you cannot
confidently choose, stop and ask, naming the options and the one you lean toward.**

> "The endpoint returns `[]` both when there are genuinely no findings and when the scanner is
> unreachable. I'm rendering an `EmptyState` for now, but that would tell the user 'no findings' during
> an outage. Can the API distinguish the two, or should I treat an empty response as an error?"

> "This dashboard has four widgets that each know their layout and one live activity feed that doesn't.
> I'm leaning toward `Skeleton`s in the widgets and an indeterminate `Progress` in the feed, rather than
> one page-level spinner. Does that match what you want?"

A single clarifying question is far cheaper than shipping a control whose behaviour surprises the user.

## References

- **Atoms → Skeleton**, **Molecules → Progress**, **Organisms → EmptyState**, **Molecules → Alert** —
  stories for each.
- **Guidelines → Choosing → Feedback** — `Alert` versus `Snackbar` for messages.
- [MUI Skeleton](https://mui.com/material-ui/react-skeleton/) · [MUI Progress](https://mui.com/material-ui/react-progress/) · [MUI Alert](https://mui.com/material-ui/react-alert/)
