# Types

Import these from `@grafloria/renderer`.

## Types

### `ContainmentAction`

Also has every member of `String`, listed on its own entry.

FOCUS CONTAINMENT — focus must never rest on geometry the user cannot see.

This is a real WCAG failure (2.4.11 Focus Not Obscured / 2.4.7 Focus Visible)
and, before this wave, we failed it outright: keyboard focus walked the whole
graph while the camera sat still, so tabbing past the edge of the viewport
left the focus ring on a node that was scrolled off-screen — announced, but
invisible. A sighted keyboard user simply lost the cursor.

The fix, in order of escalation:

1. Already fully visible (with padding) → DO NOTHING. Not panning is the
     most important case: a stationary camera is what makes the diagram
     readable, and gratuitous re-centring on every focus step is nauseating.
  2. Off-screen but it FITS at the current zoom → pan the minimum distance
     that brings it inside the padded box. Minimum, not centring: the user
     keeps their mental map.
  3. Too big to fit at the current zoom → zoom OUT via `fitToBounds`, which
     is the existing camera maths. We do not fork it.

Motion honours `prefers-reduced-motion`: an animated pan becomes an instant
jump. All camera writes go through {@link ViewportController} — this class
owns policy, not geometry.

.

```ts
type ContainmentAction = 'none' | 'pan' | 'zoom';
```

### `Politeness`

Also has every member of `String`, listed on its own entry.

The managed `aria-live` REGION — the canvas's voice.

Two live regions, not one: an AT will not reliably re-announce a node whose
politeness changed, and mixing an error into the polite queue means it waits
behind whatever selection chatter is already in flight. So:

- `polite`    — selection, movement, focus, connect/disconnect;
  - `assertive` — validation failures and errors, which interrupt.

DE-DUPLICATION is the whole design problem. A naive live region wired to a
render loop says "Process node, 2 connections" sixty times a second and is
worse than useless — the user turns it off. Three defences, all here:

1. IDENTICAL-MESSAGE SUPPRESSION. The same text in the same channel is
     dropped, unless `force` is set (a repeated action the user *did* repeat
     — nudging twice — is legitimately worth re-announcing).
  2. COALESCING. Announcements inside a short window replace, rather than
     queue behind, the previous one. Holding an arrow key produces the FINAL
     position, not fifty intermediate ones.
  3. MODEL-CHANGE DRIVEN. The controller is called on model change, never
     from the render loop. `getSpeakCount()` lets a test prove a quiet frame
     speaks zero times.

Framework-free: takes a container, owns two divs, no Angular anywhere.

.

```ts
type Politeness = 'polite' | 'assertive';
```
