# Presence

Import these from `@grafloria/renderer`.

## Functions

### `actorColor`

Deterministic per-actor colour.

Deterministic MATTERS: the colour must be the same on every peer's screen, or "the blue
cursor is Ana" is true for you and false for me. Derived from the actor id, so no
coordination, no allocation table, and no message on the wire to agree.

```ts
function actorColor(actor: string): string
```

### `actorInitials`

```ts
function actorInitials(name: string): string
```

### `bindPresence`

Mount a presence overlay on a diagram and feed it from a sync session.

```ts
const diagram = createDiagram(el, { nodes, edges });
const session = createSyncSession(diagram.getModel(), transport, { actor: userId });
session.join();
bindPresence(diagram, session, { name: 'Ana' });
```

```ts
function bindPresence(
  instance: DiagramInstance,
  source: PresenceSource,
  options: BindPresenceOptions = {}
): PresenceBinding
```

### `contrastingTextColor`

Black or white — whichever is actually READABLE on `background`.

---------------------------------------------------------------------------
FOUND BY AXE, IN THE a11y GATE, AFTER THE UNIT TESTS WERE ALL GREEN
---------------------------------------------------------------------------
The name badge was white text on the peer's colour. For a blue or purple actor that is
fine. For a green or yellow one — `hsl(124, 72%, 52%)` — it is white on light green, a
contrast ratio of about 2:1, and a user with low vision simply cannot read whose cursor
it is. It is a coin flip decided by a hash of the actor id, which is the worst kind of
accessibility bug: it works on your machine, for your account, every time you test it.

The unit tests could not have caught this. They assert `aria-hidden="true"`, which is
about ASSISTIVE TECH — and this is not an AT problem at all. It is a problem for someone
looking straight at the screen with their eyes. Only the real axe audit over a real page
with real badges on it could find it, which is the entire argument for that gate existing.

(Note that `aria-hidden` does NOT excuse it, and axe is right to say so: hiding text from
a screen reader does not hide it from a sighted user with poor contrast sensitivity.)

THE MATH. Pick whichever of pure black and pure white contrasts better. The two curves
cross at a background luminance of ~0.179, where BOTH give 4.58:1 — above the 4.5:1 WCAG
AA threshold for normal text. So this choice is guaranteed to pass for EVERY hue, not
merely for the ones I happened to look at.

```ts
function contrastingTextColor(background: string): string
```

## Classes

### `PresenceOverlay`

The presence layer.

Owns one `<div>` inside the diagram root, and nothing else. It does not know about the
SVGRenderer, the VNodePatcher, the RenderScheduler or the model, and it must not: the
moment it can reach the render loop, someone will make it call into it.

```ts
class PresenceOverlay
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `framesRun` |  | `0` | Interpolation frames actually run. An idle overlay must add ZERO. |

**Methods**

- `constructor(private readonly options: PresenceOverlayOptions)`
- `get element(): HTMLElement` — For the tests and the a11y audit — the DOM this owns, and nothing more.
- `get peerCount(): number`
- `setPeers(peers: readonly PresencePeer[]): void` — Publish the full peer set. Idempotent, and a peer that is gone is REMOVED — presence
has no tombstones and no history; the current picture is the entire truth.
- `remove(actor: string): void`
- `clear(): void`
- `dispose(): void`

## Constants

### `PRESENCE_LAYER_CLASS`

```ts
const PRESENCE_LAYER_CLASS: "grafloria-presence-layer"
```

## Interfaces

### `BindPresenceOptions`

```ts
interface BindPresenceOptions
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `name?` | `string` |  | Our own display name — what the OTHER peers put on our badge. |
| `color?` | `string` |  | Our own colour. Omit and one is derived deterministically from the actor id. |
| `publishCursor?` | `boolean` |  | Publish the local cursor. Off ⇒ we see others but they do not see us. |
| `publishSelection?` | `boolean` |  | Publish the local selection. |
| `smoothing?` | `number` |  | Interpolation factor for remote cursors (0 = snap). |
| `requestFrame?` | `(cb: () => void) => number` |  |  |
| `cancelFrame?` | `(handle: number) => void` |  |  |

### `PresenceBinding`

```ts
interface PresenceBinding
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `overlay` | `PresenceOverlay` |  |  |

**Members**

- `dispose(): void`

### `PresenceOverlayOptions`

```ts
interface PresenceOverlayOptions
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `root` | `HTMLElement` |  | The mounted diagram's root — `.grafloria-diagram-root`. |
| `viewport` | `ViewportController` |  |  |
| `getBounds?` | `BoundsLookup` |  | Where a selected entity is, in world space. Usually `model.getNode(id)`. |
| `smoothing?` | `number` |  | Interpolate remote cursors toward their target. 0 disables (jumps straight there). |
| `requestFrame?` | `(cb: () => void) => number` |  | Injectable rAF, so the interpolation tests are deterministic. |
| `cancelFrame?` | `(handle: number) => void` |  |  |

### `PresencePeer`

One peer, as far as the overlay is concerned. Ephemeral by construction.

```ts
interface PresencePeer
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `actor` | `string` |  |  |
| `name?` | `string` |  |  |
| `color?` | `string` |  |  |
| `cursor?` | `{ x: number; y: number } \| null` |  | WORLD coordinates — never screen: peers have different cameras. |
| `selection?` | `string[]` |  | Entity ids this peer has selected. |

### `PresenceSource`

What presence needs from a sync session — STRUCTURAL, not the concrete `SyncAdapter`.

The renderer therefore gains no hard dependency on the sync layer, a host can drive
presence from its own backend (a Firebase channel, a Phoenix presence, a server-sent
event stream) without adopting our transport at all, and a test can hand it a fake. The
real `SyncAdapter` satisfies it exactly, which is what `presence-reachability.spec.ts`
proves.

```ts
interface PresenceSource
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `actor` | `string` |  |  |
| `awareness` | `{ getPeers(): Array<{ actor: string; state: Record<string, unknown> }>; onChange(listener: () => void): () => void; }` |  |  |

**Members**

- `setAwareness(patch: Record<string, unknown>): void`

## Types

### `BoundsLookup`

World-space box of an entity, so the overlay can outline a remote selection.

```ts
type BoundsLookup = (entityId: string) => { x: number; y: number; width: number; height: number } | null;
```
