Skip to content
D
Documentation

Presence

reference
4 min readUpdated

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

NameTypeDefaultDescription
framesRun0Interpolation 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

NameTypeDefaultDescription
name?stringOur own display name — what the OTHER peers put on our badge.
color?stringOur own colour. Omit and one is derived deterministically from the actor id.
publishCursor?booleanPublish the local cursor. Off ⇒ we see others but they do not see us.
publishSelection?booleanPublish the local selection.
smoothing?numberInterpolation factor for remote cursors (0 = snap).
requestFrame?(cb: () => void) => number
cancelFrame?(handle: number) => void

PresenceBinding

ts
interface PresenceBinding

Properties

NameTypeDefaultDescription
overlayPresenceOverlay

Members

  • dispose(): void

PresenceOverlayOptions

ts
interface PresenceOverlayOptions

Properties

NameTypeDefaultDescription
rootHTMLElementThe mounted diagram's root — .grafloria-diagram-root.
viewportViewportController
getBounds?BoundsLookupWhere a selected entity is, in world space. Usually model.getNode(id).
smoothing?numberInterpolate remote cursors toward their target. 0 disables (jumps straight there).
requestFrame?(cb: () => void) => numberInjectable 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

NameTypeDefaultDescription
actorstring
name?string
color?string
cursor?{ x: number; y: number } | nullWORLD 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

NameTypeDefaultDescription
actorstring
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;

Was this page helpful?

Presence — Grafloria