# Module: `src/components/DetectionClasses.tsx`

> **Source**: `src/components/DetectionClasses.tsx` (99 lines)
> **Topo batch**: B3 (depends on B2 leaves: `api/client`, `class-colors` (via barrel), `types/index`)
> **Last refresh**: 2026-05-13 — `getClassColor` + `FALLBACK_CLASS_NAMES` import migrated from `'../features/annotations/classColors'` to `'../class-colors'` barrel by AZ-511.

## Purpose

A side-panel widget that lets the annotator pick a detection class (1–9 or click) and a photo mode (`Regular`, `Winter`, `Night`). Loads the class catalogue from the backend and falls back to a hardcoded list when the API returns empty / errors. Replaces the legacy WPF detection-class picker referenced in `_docs/legacy/wpf-era.md` §"What survived".

## Public interface

```ts
interface Props {
  selectedClassNum: number
  onSelect: (classNum: number) => void
  photoMode: number
  onPhotoModeChange: (mode: number) => void
}
export default function DetectionClasses(props: Props): JSX.Element
```

Fully controlled — the parent owns `selectedClassNum` and `photoMode`. The current contract for `photoMode` values is integer offsets `0` (Regular), `20` (Winter), `40` (Night), matching the `photoMode` field of `DetectionClass` (`src/types/index.ts`) and the offsets baked into `FALLBACK_CLASSES` below.

## Internal logic

- **Class catalogue load** (mount-only `useEffect`):
  - `api.get<DetectionClass[]>(endpoints.annotations.classes())` (= `/api/annotations/classes`, since AZ-486 / F7).
  - On a non-empty array → `setClasses(list)`.
  - On an empty array OR a thrown error → `setClasses(FALLBACK_CLASSES)`.
- **`FALLBACK_CLASSES`** is a module-private 3 × |`FALLBACK_CLASS_NAMES`| matrix:
  - For each mode offset in `[0, 20, 40]`, build one class entry per name in `FALLBACK_CLASS_NAMES` (imported from `features/annotations/classColors.ts`).
  - Each entry: `{ id: i + modeOffset, name, shortName: name.slice(0, 3), color: getClassColor(i), maxSizeM: 10, photoMode: modeOffset }`.
  - This means: in offline / API-down mode, the ID range is `0–N-1` (Regular), `20–20+N-1` (Winter), `40–40+N-1` (Night). The backend's class IDs MUST follow the same convention or fallback↔backend handoff yields ID collisions. Confirm in Step 4 via the `annotations/` service contract.
- **Numeric hotkeys 1–9** (effect keyed on `[classes, photoMode, onSelect]`):
  - `keydown` on `window`. `parseInt(e.key)` → if 1–9, picks `classes[(num - 1) + photoMode]`.
  - **Bug-shaped**: `photoMode` is `0 | 20 | 40` (an *offset*), not a row count. `classes[idx + 0]` is correct for Regular; for Winter/Night the index `idx + 20` / `idx + 40` is meaningful only when `classes` is in the contiguous `[0..N-1, 20..20+N-1, 40..40+N-1]` shape that `FALLBACK_CLASSES` produces. **If the backend returns its classes in a different order**, hotkey 1–9 will pick the wrong class. Verify backend ordering in Step 4 — this is the principal correctness risk in this module. Flag.
- **Auto-select first class on mode change** (effect keyed on `[classes, photoMode, selectedClassNum, onSelect]`):
  - Filter `classes` to the active `photoMode`.
  - If `selectedClassNum` is not within that filtered set, call `onSelect(modeClasses[0].id)`.
  - This guarantees the parent always holds a class ID consistent with the selected photo mode.
- **Render**:
  - Class list — only entries whose `photoMode` matches the active mode.
  - Photo-mode bar — three buttons (Sunny / Snowflake / Moon icons) for Regular / Winter / Night.

## Dependencies

- **Internal**:
  - `../api` (barrel) — `api`, `endpoints`. (Since AZ-485 / F4 + AZ-486 / F7.)
  - `../features/annotations/classColors` — `getClassColor(i)`, `FALLBACK_CLASS_NAMES`. (Cross-component import preserved; flagged in Consumers below.)
  - `../types` — `DetectionClass` type.
- **External**: `react`, `react-i18next`, `react-icons/md`, `react-icons/fa`.

## Consumers (intra-repo)

From the §7a dependency graph:

- `src/features/annotations/AnnotationsPage.tsx`
- `src/features/dataset/DatasetPage.tsx`

This is the **canonical example** of the cross-layer import flagged in `_docs/02_document/00_discovery.md` §8: a `components/` (shared) module importing from `features/annotations/`. Two clean fixes are in scope for Step 4 / Step 8:

1. Lift `classColors.ts` into `src/components/detection/` (or `src/shared/`) and update the two consumers.
2. Or: move `DetectionClasses` itself into `src/features/annotations/` since both consumers are in `features/`.

## Data models

`DetectionClass` (from `src/types/index.ts`) — see `_docs/02_document/modules/src__types__index.md`.

`FALLBACK_CLASSES` is module-private; see Internal logic above.

## Configuration

Endpoint: `endpoints.annotations.classes()` → `/api/annotations/classes` (typed builder from `../api/endpoints`, since AZ-486 / F7).

Photo-mode value set is `{0, 20, 40}` — hardcoded, mirrored by `FALLBACK_CLASSES`. If the backend grows a fourth mode (e.g. thermal, IR), every consumer of `photoMode` will need a coordinated change.

Tailwind tokens: `bg-az-orange` (Regular), `bg-az-blue` (Winter), `bg-purple-600` (Night). Defined in `src/index.css`.

## External integrations

- HTTP `GET endpoints.annotations.classes()` (= `/api/annotations/classes`) → `DetectionClass[]`. Backed by the `annotations/` service (`.NET`) per `nginx.conf`.

## Security

- **Silent failure on class load**: `.catch(() => setClasses(FALLBACK_CLASSES))` swallows the error and the user sees the fallback. Acceptable for UX continuity, but the lack of any user-visible signal means a misconfigured `/api/annotations/classes` deploy could go unnoticed in prod. Flag for Step 6 `security_approach.md` / Step 8.
- No input that flows back to the server here.
- The `getClassColor(i)` palette is deterministic; no PII.

## Tests

None.

## Notes / open questions

- **`getPhotoModeSuffix` redundancy** (carry-over from B1): `classColors.ts` exposes `getPhotoModeSuffix()` that derives the same suffix the typed `DetectionClass.photoMode` field already encodes. Once `classColors.ts` is repositioned (see Consumers), `getPhotoModeSuffix` is a deletion candidate. Defer to Step 8.
- **`FALLBACK_CLASS_NAMES.length`** is implicitly assumed to be ≤ 9 by the hotkey code (only 1–9 are bound). If the catalogue grows to 10+ entries, hotkeys can no longer cover the tail. Acceptable for now.
- **Mode-button colours don't use `az-` tokens for Night** (`bg-purple-600`, `text-purple-400` instead of an `az-purple` token). Cosmetic inconsistency; flag for Step 4 against `_docs/ui_design/README.md` colour palette.
- The class list is rendered with `1.`, `2.`, … prefixes derived from `i+1` — so the hotkey number always matches the visible label inside the active mode. Good.
- Does not handle modifier keys (`Shift`, `Ctrl`) on numeric hotkeys; pressing `Shift+5` will trigger the `5` branch. Fine for now.
