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.
tsfunction actorColor(actor: string): string
actorInitials
tsfunction actorInitials(name: string): string
bindPresence
Mount a presence overlay on a diagram and feed it from a sync session.
tsconst diagram = createDiagram(el, { nodes, edges });
const session = createSyncSession(diagram.getModel(), transport, { actor: userId });
session.join();
bindPresence(diagram, session, { name: 'Ana' });
tsfunction 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.
tsfunction 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.
tsclass 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(): numbersetPeers(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): voidclear(): voiddispose(): void
Constants
PRESENCE_LAYER_CLASS
tsconst PRESENCE_LAYER_CLASS: "grafloria-presence-layer"
Interfaces
BindPresenceOptions
tsinterface 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
tsinterface PresenceBinding
Properties
| Name | Type | Default | Description |
|---|---|---|---|
overlay | PresenceOverlay |
Members
dispose(): void
PresenceOverlayOptions
tsinterface 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.
tsinterface 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.
tsinterface 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.
tstype BoundsLookup = (entityId: string) => { x: number; y: number; width: number; height: number } | null;
Was this page helpful?