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

> **Source**: `src/components/ConfirmDialog.tsx` (47 lines)
> **Topo batch**: B3 (uses `react-i18next` only; no intra-repo deps)

## Purpose

A controlled, single-message confirm dialog used wherever the SPA needs an "Are you sure?" gate. Replaces the legacy WPF `MessageBox.Show(..., MessageBoxButton.YesNo, ...)` pattern (`_docs/legacy/wpf-era.md` §4 / §"What survived").

## Public interface

```ts
interface Props {
  open: boolean
  title: string
  message?: string
  onConfirm: () => void
  onCancel: () => void
}
export default function ConfirmDialog(props: Props): JSX.Element | null
```

When `open === false`, returns `null`. When `open === true`, renders a centered modal over a dimmed backdrop. The component is fully controlled — it owns no `open` state itself.

## Internal logic

- **Auto-focus**: when `open` flips to `true`, the Cancel button is focused via `cancelRef.current?.focus()` (see `useEffect` keyed on `open`). This makes Cancel the default keyboard target — a common pattern for destructive confirmations.
- **Escape-to-cancel**: a `keydown` handler is attached to `window` while `open === true`; pressing `Escape` calls `onCancel()`. The listener is removed on unmount or when `open` flips to `false`. This is the **only** dismiss path other than the explicit Cancel/Confirm buttons (no backdrop click handler).
- **Translation**: button labels use `t('common.cancel')` and `t('common.confirm')`. Title and message are passed in by the caller — the caller is responsible for translation.

## Dependencies

- **Internal**: none.
- **External**: `react` (`useEffect`, `useRef`), `react-i18next` (`useTranslation`).

## Consumers (intra-repo)

From the §7a dependency graph in `_docs/02_document/00_discovery.md`:

- `src/features/admin/AdminPage.tsx` — confirm before deleting users.
- `src/features/annotations/MediaList.tsx` — confirm before deleting media.
- `src/features/flights/FlightsPage.tsx` — confirm before deleting a flight.
- `src/features/dataset/DatasetPage.tsx` — confirm before deleting dataset items.

(`HelpModal.tsx` is the *non-confirm* sibling; it does NOT use `ConfirmDialog` and notably does **not** have Escape-to-close — see `src__components__HelpModal.md` §Notes.)

## Data models

None.

## Configuration

Tailwind tokens used: `bg-az-panel`, `border-az-border`, `text-az-text`, `bg-az-red`, `bg-az-bg`, `text-white`. All defined in `src/index.css` (per `_docs/02_document/00_discovery.md` §2a).

`z-[100]` is the chosen stacking layer. No other modal currently competes with it; if a third-party library introduces a portal at `z-[200]+`, layering will need a docs entry.

## External integrations

None.

## Security

- **No backdrop dismissal**: clicking outside the dialog does NOT cancel. Combined with Escape-to-cancel and a default-focused Cancel button, this gives a deliberate, keyboard-safe destructive-action flow.
- **No `aria-modal` / role="dialog"**: not annotated for screen readers; flag for Step 4 verification against the `_docs/ui_design/README.md` §"Confirmation dialogs" spec (which specifies modal semantics).

## Tests

None.

## Notes / open questions

- The "destructive" colour (`bg-az-red`) is hardcoded into the Confirm button. Some callers use `ConfirmDialog` for non-destructive confirms (e.g. flight selection in some flows — verify in B7); a future variant prop (`destructive: boolean`) would be cleaner. Defer to Step 8.
- No `width` prop — the dialog is fixed at `w-80` (320px). On the mobile breakpoint defined in `_docs/ui_design/README.md` §"Responsive Breakpoints" (640px), this fits, but very long titles or multi-line messages will overflow. Flag for Step 4 cosmetic verification.
- Escape handler attaches to `window`, not the dialog DOM. If two `ConfirmDialog`s are mounted simultaneously (the SPA never does this today, but it's not enforced), both would call their `onCancel()` on a single Escape press. Acceptable under current usage.
