<!-- https://getkonjo.com/design/components/belt-dot · source: docs/design/components/belt-dot.md -->

# Belt dot and rank picker

`BeltDot` draws a rank as a small ringed circle. `RankPicker` lists a dojo's own ladder as rows
to choose from; `RankPickerSheet` puts that list in a sheet.

```kata-specimen
belt-sizes
```

> Source: [`BeltDot.tsx`](../../../src/components/ui/BeltDot.tsx),
> [`RankPicker.tsx`](../../../src/components/ui/RankPicker.tsx). The law behind both:
> [rank](../foundations/rank.md). The swatches above use the launch community's ladder as
> example data; a screen renders whatever ladder the dojo has.

## When to use it

- `BeltDot` beside a rank's name, or beside a person whose rank is also written nearby.
- `RankPicker` inline in a form, `RankPickerSheet` from a [select trigger](select-trigger.md).

Never hardcode a ladder, a rank name or a rank colour into a screen. `BeltRankPicker` does, which
is why it is retired: it can only draw nineteen Cuong Nhu ranks.

## Anatomy

**BeltDot**

| Part | Token |
|---|---|
| Size | `swatchSize.ranked` 10 (has a rank; legal only where the rank is also text), `swatchSize.roster` 16 (the rank matters: rosters, pickers), `swatchSize.subject` 28 (the rank is the subject) |
| Fill | the ladder's own visual: solid, stripes, split, end-cap or alternating bands — the dojo's data, not Konjo's palette |
| Ring | 1px `border.control`, every rank, both themes, no exceptions |

**RankPicker**

```kata-specimen
rank-picker
```

| Part | Token |
|---|---|
| Row | `controlHeight.row` 64 floor, `spacing.s12` padding and gap |
| Swatch | `BeltDot` at `swatchSize.roster` 16 |
| Name | `typography.rowTitle`, `text.primary` |
| Selected | a `check` at 16 in `text.primary` on the right |
| Unavailable | full contrast, with its reason in `caption` `text.secondary` on the right |
| Divider | 1px `border.hairline` on `surface.card`, inset to the text edge |

## States

```kata-specimen
rank-picker-states
```

- **Selected**: a check, never a filled row.
- **Unavailable**: not dimmed. There is no legal disabled text colour, so the row stays readable
  and says why ("Needs 6 more months").
- **Loading**: four skeleton rows of the same shape, never a centred spinner.
- **Error**: "Couldn't load the ranks", plain words, and a bordered "Try again" — the screen owns
  the primary.
- **Empty**: "No ranks set up", and where an owner fixes it.
- **Pressed**: `opacity.pressed` 0.85 on the row.

## Content

- The rank's name is always beside the swatch. Two ranks can draw identically — nidan and
  rokudan are both two red stripes on black.
- Shared components say "rank", not "kyu" or "dan": those are one lineage's words.

## Accessibility

- `BeltDot` is hidden from screen readers. The rank reaches them through the row's label;
  announcing it twice is worse than not at all.
- The ring is not decoration. Belt fills get no contrast guarantee, and against a card a white
  belt, a yellow belt and, in dark, a black belt are close to invisible. `border.control` on
  `surface.card` is 3.53:1 in light and 3.16:1 in dark, whatever colour it surrounds.
- Each picker row is one stop, role `button`, with `accessibilityState={{ selected, disabled }}`
  and a label that includes the reason when it is unavailable.

## Do and don't

```kata-specimen
belt-name
```

## Code

```tsx
import { BeltDot, RankPicker } from '../../components/ui';
import { swatchSize } from '../../theme/tokens';

<BeltDot visual={rank.visual} size={swatchSize.roster} />

<RankPicker
  ranks={ladder}
  selectedSlug={chosen?.slug}
  onSelect={setChosen}
  unavailable={{ brown: 'Needs 6 more months' }}
  loading={ladderLoading}
  error={ladderError}
  onRetry={reloadLadder}
  testID="promotion-rank-picker"
/>
```
