# Comments

Import these from `@grafloria/renderer`.

## Functions

### `commentPinAccessibleName`

The accessible name of a pin. This is the ONLY thing an AT user hears about a comment
before they decide whether to open it, so it has to answer the question they are
actually asking: what is this about, is it live, and is any of it new to me?

```ts
function commentPinAccessibleName(thread: CommentThreadView): string
```

### `renderCommentPins`

Build the pin layer.

Returns a `<g>` even when empty: a stable element keeps the patcher's keyed diff
simple, and an empty `<g>` costs nothing to reconcile.

```ts
function renderCommentPins(
  threads: readonly CommentThreadView[],
  options: CommentPinsOptions = {}
): VNode
```

## Classes

### `CommentOverlayController`

```ts
class CommentOverlayController implements CommentSource
```

**Methods**

- `constructor( private readonly store: CommentStore, private readonly renderer: CommentRendererHost, private readonly options: CommentOverlayOptions = {} )`
- `getThreads(): readonly CommentThreadView[]` — The threads to draw. Called once per BUILT frame (never on a skipped one).
- `getPinOptions(): Pick<CommentPinsOptions, 'selectedThreadId' | 'showResolved' | 'radius'>` — Local view state — selection and the resolved filter.

NOT keyboard focus. That is the RENDERER's `a11yFocus`, which already owns the roving
tabindex for nodes and edges, and a comment pin is just a third kind of thing in the
same one-tab-stop widget. Two authorities for "what is focused" is how you get two
elements with `tabindex=0`, which is the exact bug the roving tabindex exists to
prevent — so there is one, and it is the one that already existed.
- `select(threadId: string | null): void` — Open a thread. Opening it is READING it, which is what "unread" means.
- `getSelected(): string | null`
- `setShowResolved(show: boolean): void`
- `markRead(threadId: string): void` — Mark a thread read WITHOUT opening it (the "mark all read" affordance).
- `getInvalidationCount(): number` — How many times the picture has been declared stale. A test asserts this moves.
- `dispose(): void`

### `CommentPanelView`

```ts
class CommentPanelView
```

**Methods**

- `constructor( container: HTMLElement, private readonly store: CommentStore, options: CommentPanelOptions = {} )`
- `getElement(): HTMLElement`
- `getRebuildCount(): number`
- `select(threadId: string | null, opts?: { focus?: boolean }): void` — Which thread is open. Setting it re-renders and moves focus onto the conversation.
- `getSelected(): string | null`
- `update(): boolean` — Rebuild if — and only if — something the panel SHOWS has changed.

Safe on every frame. A drag, a pan and a zoom all change the diagram and change
nothing here, and must therefore cost zero DOM operations.
- `dispose(): void`

## Interfaces

### `CommentOverlayOptions`

```ts
interface CommentOverlayOptions
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `showResolved?` | `boolean` |  |  |
| `onSelectionChange?` | `(threadId: string \| null) => void` |  | Notified when the open thread changes (a host pans to it, opens the panel, …). |

### `CommentPanelOptions`

```ts
interface CommentPanelOptions
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `label?` | `string` |  | Accessible name of the region. |
| `showResolved?` | `boolean` |  | Show resolved threads too. Default false — a resolved thread is an answered one. |
| `onSelect?` | `(threadId: string \| null) => void` |  | Called when the user picks a thread (the host selects it and pans to it). |
| `onDismiss?` | `() => void` |  | Called on Escape. The host MUST return focus to the canvas. |
| `formatTime?` | `(ms: number) => string` |  | Render a wall-clock timestamp. Injectable so tests are not clock-dependent. |

### `CommentPinsOptions`

```ts
interface CommentPinsOptions
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `visibleRect?` | `{ x: number; y: number; width: number; height: number }` |  | World rect currently on screen. Pins outside it are not built. |
| `zoom?` | `number` |  | Current zoom, so the pin can be counter-scaled to a constant screen size. |
| `selectedThreadId?` | `string \| null` |  | The thread whose panel is open — drawn selected, and `aria-expanded=true`. |
| `focusedThreadId?` | `string \| null` |  | The comment the roving tabindex has focused, if any. |
| `showResolved?` | `boolean` |  | Hide resolved threads (the default view — a resolved thread is answered). |
| `radius?` | `number` |  | Radius in SCREEN pixels. |

### `CommentRendererHost`

The slice of the renderer this controller drives. Structural, so it is trivial to fake.

```ts
interface CommentRendererHost
```

**Members**

- `setCommentSource(source: CommentSource | null): void`
- `invalidateFrame(): void`

### `CommentSource`

What the renderer needs from a comment source. Deliberately tiny — the renderer must not
know what a thread IS, only where the pins go and what they are called.

```ts
interface CommentSource
```

**Members**

- `getThreads(): readonly CommentThreadView[]` — The threads to draw. Called once per BUILT frame (never on a skipped one).
- `getPinOptions(): Pick<CommentPinsOptions, 'selectedThreadId' | 'showResolved' | 'radius'>` — Local view state — selection and the resolved filter.

NOT keyboard focus. That is the RENDERER's `a11yFocus`, which already owns the roving
tabindex for nodes and edges, and a comment pin is just a third kind of thing in the
same one-tab-stop widget. Two authorities for "what is focused" is how you get two
elements with `tabindex=0`, which is the exact bug the roving tabindex exists to
prevent — so there is one, and it is the one that already existed.
