# Page state

What a page shows instead of its content: nothing here yet (empty), nothing matches the filters (no-match), something broke (error), or it can't be shown to you (unavailable). The words are the caller's, since they are the product's. An error carries the request id to quote; "unavailable" is for both "doesn't exist" and "not yours", so it never reveals which.

- Registry name: `page-state` (`@candor/page-state`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/page-state

## Install

```bash
npx candor-ui add page-state
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/page-state`.
It brings in: button, copy-field, locale, skeleton, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import { PageSkeleton, PageState } from "@/components/page-state"

export default function Example() {
  return (
    <div className="grid max-w-3xl gap-4 sm:grid-cols-2">
      <div className="flex rounded-lg border">
        <PageState
          kind="no-match"
          title="No migrations match these filters"
          detail="Nothing on Staging was rolled back in the last 30 days."
          secondary={{ label: "Clear filters", onClick: () => {} }}
        />
      </div>
      <div className="flex rounded-lg border">
        <PageState
          kind="error"
          title="The flags couldn’t load"
          detail="Keel couldn’t reach the flag service. Try again, or quote this id to support."
          requestId="req_7f3a91c2"
          action={{ label: "Try again", onClick: () => {} }}
        />
      </div>
      <div className="flex h-72 overflow-hidden rounded-lg border sm:col-span-2">
        <PageSkeleton label="Loading repositories" />
      </div>
    </div>
  )
}
```

## <PageState>

Props:

- `kind: PageStateKind`: empty, no-match, error or unavailable; sets the icon, and an error is announced.
- `title: string`: What's going on, in the product's words.
- `detail?: React.ReactNode | undefined`: More on why, or what to do, under the title.
- `action?: { label: string; onClick: () => void } | undefined`: The one thing to do next.
- `secondary?: { label: string; onClick: () => void } | undefined`: A quieter second option, e.g. clear filters.
- `requestId?: string | undefined`: For errors: the id support needs.
- `Icon?: LucideIcon | undefined`: Replaces the kind's icon.
- `className?: string | undefined`

## <PageSkeleton>

A page that's loading: the shape of what's coming, announced once.

Props:

- `variant?: "list" | "detail" | undefined` (default `"list"`): The shape to sketch: rows of a list (the default) or one item's detail.
- `label?: string | undefined`: What's loading, announced to screen readers. Defaults to "Loading".
- `className?: string | undefined`

## Strings (`pageState`)

The fixed words, English by default. Override any of them under `pageState` in `<LocaleProvider strings={{ pageState: { … } }}>`; the table is exported as `pageStateStrings`.

- `requestId`: Request id
- `loading`: Loading

## Types

### PageStateKind

```ts
"empty" | "no-match" | "error" | "unavailable"
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
