import { Canvas, Controls, Meta } from "@storybook/addon-docs/blocks";

import * as BadgeStories from "./Badge.stories";

<Meta of={BadgeStories} />

# Badge

Four repos, and rip-deck declares the **identical** `TONE_CLASS` map twice —
`VerdictBadge.tsx` and `TowerAlerts.tsx`, same five hardcoded hexes, related only by
copy-paste. mux-magic's `statusClassMap` is the same idea spelled a third way, keyed by a
bare `string`, so an unrecognised status renders with no colour and no error.

<Canvas of={BadgeStories.Playground} />

## Props

<Controls />

## Where the status half lives

`intent` covers the colour. Which *state* maps to which intent — and what it is called in
English — lives in `statusIntent.ts` as exhaustive switches over the machines
`@charcuterie/logic` ships:

```tsx
<Badge intent={getAsyncIntent(status)}>{getAsyncLabel(status)}</Badge>
```

That is the whole of mux-magic's `StatusBadge`, and adding a state to the machine is a
compile error here rather than a blank pill in production.

<Canvas of={BadgeStories.Interactive} />

## Colour a user picked: `categorical`

`intent` is six colours that each **mean** something, and that is exactly wrong for a
label somebody named "Homelab". So there is a second family — numbered, not named:

```tsx
<Badge categorical={4}>Homelab</Badge>
```

Ten indexes, each with the same seven roles an intent has, generated per variant and
contrast-gated in **both** schemes. The point of the gating is that the choice is safe:
a user picking from this palette cannot pick one that is unreadable.

<Canvas of={BadgeStories.Categorical} />

`intent` and `categorical` are **mutually exclusive in the type**. A badge is one colour,
and `<Badge intent="danger" categorical={3}>` is a question with no answer rather than a
precedence rule to remember.

### Rows that predate the feature

`getCategoricalIndex(key)` from `@charcuterie/tokens` is a pure function from a string to
an index — stable across runs, machines and a server-rendered copy — so a hundred existing
labels get colours with no migration and no `Math.random()` handing the same row a
different colour on every reload. It is a **fallback**, never an override:

```tsx
<Badge categorical={label.categorical ?? getCategoricalIndex(label.id)}>
  {label.name}
</Badge>
```

Hash the row's **id** where one exists. The function deliberately does not normalize, so
renaming a label renames its colour too — and with ten colours, two labels in twelve will
share one. That is a property of the palette, not of the hash: the colour helps you scan a
list, the text is what says which label it is.

<Canvas of={BadgeStories.CategoricalLabels} />

### When it is `Swatch` instead

`Swatch` takes a colour arriving from the world — a physical sticker on a controller, an
accent extracted off an album — which is arbitrary, unthemeable and uncheckable. This
family is a curated set the design system owns and re-themes. Ungoverned colour is
`Swatch`; a choice from a palette is `categorical`.

## No implicit role

A badge is a word about something else. It is not a live region, and `role="status"` would
make every re-render announce itself. Where a status genuinely needs announcing, that is
`LiveStatusIndicator`; where a badge needs a name beyond its text, `aria-label` still
works. Hover styling is deliberately absent too — `INTENT_HOVER_CLASS` is opt-in, so a
verdict pill cannot imply it is clickable.

<Canvas of={BadgeStories.AllVariants} />
