@llui/lexical-collab

Adds real-time collaborative editing to the @llui/lexical binding. yjsCollab(...) produces a register hook you wire into lexicalForeign({ history: false, seedMode: 'deferred', register }) — or you pass it through the markdown editor's collab option, which does this for you. Sync runs over a Yjs provider you inject, so you keep control of the transport (WebSocket, WebRTC, …).

pnpm add @llui/lexical-collab @llui/lexical @lexical/yjs yjs

@lexical/yjs and yjs are peer dependencies.

What it gives you

  • CRDT sync — document state converges across clients through the injected CollabProvider.
  • Scoped undo — undo/redo is bound to each client's own edits rather than the shared global stream.
  • Presence cursors — remote users (CollabUser) surface as live selection/cursor decorations.

API

Functions

yjsCollab()

Build (but do not yet bind) a collaborative editing handle.

function yjsCollab(config: YjsCollabConfig): YjsCollab

Types

CollabProvider

A Yjs network/transport provider. Structurally identical to @lexical/yjs's Provider; re-exported so consumers type their factory without reaching into @lexical/yjs directly. y-websocket / y-webrtc / @hocuspocus/provider all satisfy it.

export type CollabProvider = Provider

Interfaces

CollabUser

Local presence identity broadcast to peers (name + caret colour).

export interface CollabUser {
  /** Display name shown on the remote caret. */
  name: string
  /** Caret / selection colour (any CSS colour). */
  color: string
  /** Arbitrary extra data merged into this client's awareness state. */
  awarenessData?: Record<string, unknown>
}

YjsCollabConfig

export interface YjsCollabConfig {
  /** Shared document id (room name). Must match across peers. */
  id: string
  /** The shared Yjs document. Created if omitted (and registered in `docMap`). */
  doc?: YDoc
  /** Doc registry shared with the provider factory. Created if omitted. */
  docMap?: Map<string, YDoc>
  /** A ready provider. Mutually exclusive with `providerFactory`. */
  provider?: CollabProvider
  /** Factory building the provider from the (id, docMap). Preferred — it lets
   * this module own doc creation/registration before the provider binds. */
  providerFactory?: (id: string, docMap: Map<string, YDoc>) => CollabProvider
  /** Local presence identity. Presence is disabled when omitted. */
  user?: CollabUser
  /** Whether THIS peer may seed an empty shared document. Default `true`.
   * In a multi-peer app exactly one peer should bootstrap (e.g. the creator);
   * the seed only runs if the shared doc is still empty after first sync. */
  shouldBootstrap?: boolean
  /** Seed an empty shared document (runs once, inside an editor update, only on
   * the bootstrapping peer). Without it an empty paragraph is inserted. */
  seed?: (editor: LexicalEditor) => void
  /** Overlay element that hosts remote carets. Created over the editor when
   * omitted; its offsetParent is made `position: relative` if it is static. */
  cursorsContainer?: HTMLElement
  /** Per-node properties excluded from CRDT sync (advanced). */
  excludedProperties?: ExcludedProperties
  /** Connect the provider at mount. Default `true`. */
  autoConnect?: boolean
  /** Connection status changed (`'connected'` ⇄ disconnected). */
  onStatus?: (connected: boolean) => void
  /** Provider sync state changed (initial document handshake complete). */
  onSync?: (synced: boolean) => void
  /** Remote peer count changed (distinct awareness states, excluding self). */
  onPeers?: (count: number) => void
}

YjsCollab

Live handle returned by {@link yjsCollab}.

export interface YjsCollab {
  /** Wire the binding onto an editor; pass as `lexicalForeign({ register })`.
   * Returns a disposer that tears down every listener, the provider connection,
   * and the cursors overlay. */
  register: (editor: LexicalEditor) => () => void
  /** The shared Yjs document. */
  readonly doc: YDoc
  /** The network provider. */
  readonly provider: CollabProvider
  /** Connect the provider (no-op if `autoConnect` already connected). */
  connect: () => void
  /** Disconnect the provider. */
  disconnect: () => void
}