The demo creates two provider endpoints, two editor Y.Doc instances, and one
central room document. The room seeds its document before either endpoint
attaches, so both editor bindings omit seed. The provider is credential-free
and development-only.
'use client';
import { RefreshCwIcon, Redo2Icon, Undo2Icon, UnplugIcon } from 'lucide-react';
import { createEditor, EditorRoot, useEditorRuntimeState } from 'platejs/react';
import {
yjs,
type YjsAwarenessChange,
type YjsAwarenessState,
} from 'platejs/yjs';
import { useYjsAdmissionStatus } from 'platejs/yjs/react';
import * as React from 'react';
import * as Y from 'yjs';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { BasicNodesKit } from '@/components/editor/basic-nodes';
import { Editor, EditorContainer } from '@/components/editor/editor';
import { CollaborationPlugin } from '@/components/editor/remote-cursor-overlay';
const ROOT_NAME = 'plate-collaboration-demo';
const SCHEMA = {
id: 'plate-collaboration-demo',
version: 1,
} as const;
const INITIAL_VALUE = [
{
children: [
{
text: 'Ada and Lin edit independent documents through a local Yjs room.',
},
],
type: 'paragraph',
},
];
type DemoPeer = {
clientId: number;
color: string;
id: 'ada' | 'lin';
name: string;
};
const PEERS = [
{
clientId: 101,
color: '#7C3AED',
id: 'ada',
name: 'Ada',
},
{
clientId: 202,
color: '#0891B2',
id: 'lin',
name: 'Lin',
},
] as const satisfies readonly DemoPeer[];
const cloneInitialValue = () => structuredClone(INITIAL_VALUE);
class DemoAwareness {
readonly clientID: number;
readonly doc: Y.Doc;
private readonly listeners = new Set<(event: YjsAwarenessChange) => void>();
private readonly publish: () => void;
private localState: YjsAwarenessState | null = null;
private readonly states = new Map<number, YjsAwarenessState>();
private destroyed = false;
constructor(doc: Y.Doc, publish: () => void) {
this.clientID = doc.clientID;
this.doc = doc;
this.publish = publish;
}
destroy() {
this.destroyed = true;
this.localState = null;
this.states.clear();
this.listeners.clear();
}
getLocalState() {
return this.localState;
}
getStates() {
return this.states;
}
listenerCount() {
return this.listeners.size;
}
off(_event: 'change', handler: (event: YjsAwarenessChange) => void) {
this.listeners.delete(handler);
}
on(_event: 'change', handler: (event: YjsAwarenessChange) => void) {
this.listeners.add(handler);
}
removeRemoteState(clientId: number) {
if (!this.states.delete(clientId)) return;
this.emit({ added: [], removed: [clientId], updated: [] });
}
removeRemoteStates() {
const removed = [...this.states.keys()].filter(
(clientId) => clientId !== this.clientID
);
if (removed.length === 0) return;
for (const clientId of removed) {
this.states.delete(clientId);
}
this.emit({ added: [], removed, updated: [] });
}
setLocalStateField(field: string, value: unknown) {
if (this.destroyed) return;
const hadLocalState = this.localState !== null;
this.localState = Object.freeze({
...this.localState,
[field]: value,
});
this.states.set(this.clientID, this.localState);
this.emit({
added: hadLocalState ? [] : [this.clientID],
removed: [],
updated: hadLocalState ? [this.clientID] : [],
});
this.publish();
}
setRemoteState(clientId: number, state: YjsAwarenessState) {
if (this.destroyed) return;
const added = !this.states.has(clientId);
this.states.set(clientId, state);
this.emit({
added: added ? [clientId] : [],
removed: [],
updated: added ? [] : [clientId],
});
}
private emit(event: YjsAwarenessChange) {
for (const listener of this.listeners) {
listener(event);
}
}
}
type DemoProviderSnapshot = Readonly<{
status: 'connected' | 'connecting' | 'disconnected';
synced: boolean;
}>;
class DemoProvider {
readonly awareness: DemoAwareness;
readonly doc: Y.Doc;
readonly peer: DemoPeer;
private readonly listeners = new Set<() => void>();
private readonly room: DemoRoom;
private destroyed = false;
private snapshot: DemoProviderSnapshot = Object.freeze({
status: 'disconnected',
synced: false,
});
private updateListenerAttached = true;
constructor(peer: DemoPeer, room: DemoRoom) {
this.peer = peer;
this.room = room;
this.doc = new Y.Doc();
this.doc.clientID = peer.clientId;
this.awareness = new DemoAwareness(this.doc, () => {
this.room.publishAwareness(this);
});
this.doc.on('update', this.handleDocumentUpdate);
}
connect() {
if (this.destroyed || this.snapshot.status === 'connected') return;
this.setSnapshot({ status: 'connecting', synced: false });
this.setSnapshot({ status: 'connected', synced: false });
this.room.connect(this);
this.setSnapshot({ status: 'connected', synced: true });
}
destroy() {
if (this.destroyed) return;
this.destroyed = true;
this.room.unregister(this);
if (this.updateListenerAttached) {
this.doc.off('update', this.handleDocumentUpdate);
this.updateListenerAttached = false;
}
this.awareness.destroy();
this.listeners.clear();
this.doc.destroy();
}
disconnect() {
if (this.destroyed || this.snapshot.status === 'disconnected') return;
this.room.disconnect(this);
this.setSnapshot({ status: 'disconnected', synced: false });
}
getSnapshot = () => this.snapshot;
listenerCount() {
const count = this.updateListenerAttached ? 1 : 0;
return count + this.listeners.size + this.awareness.listenerCount();
}
subscribe = (listener: () => void) => {
this.listeners.add(listener);
let active = true;
return () => {
if (!active) return;
active = false;
this.listeners.delete(listener);
};
};
receive(update: Uint8Array) {
if (this.destroyed || this.snapshot.status !== 'connected') return;
Y.applyUpdate(this.doc, update, this.room);
}
private readonly handleDocumentUpdate = (
update: Uint8Array,
origin: unknown
) => {
if (
!this.destroyed &&
this.snapshot.status === 'connected' &&
origin !== this.room
) {
this.room.publishDocument(this, update);
}
};
private setSnapshot(snapshot: DemoProviderSnapshot) {
if (
snapshot.status === this.snapshot.status &&
snapshot.synced === this.snapshot.synced
) {
return;
}
this.snapshot = Object.freeze(snapshot);
for (const listener of this.listeners) listener();
}
}
const seedRoomDocument = (doc: Y.Doc) => {
const editor = createEditor({
id: 'collaboration-demo-seed-owner',
initialValue: cloneInitialValue(),
plugins: BasicNodesKit,
schema: SCHEMA,
});
const uninstall = editor.install(
yjs({
doc,
initialReady: true,
rootName: ROOT_NAME,
seed: true,
})
);
uninstall();
};
class DemoRoom {
readonly providers: readonly [DemoProvider, DemoProvider];
private readonly endpointProviders = new Set<DemoProvider>();
private readonly roomDoc = new Y.Doc();
private destroyed = false;
constructor() {
seedRoomDocument(this.roomDoc);
const providers = PEERS.map((peer) => new DemoProvider(peer, this)) as [
DemoProvider,
DemoProvider,
];
this.providers = providers;
for (const provider of providers) {
this.endpointProviders.add(provider);
provider.connect();
}
if (
providers[0] === providers[1] ||
providers[0].doc === providers[1].doc ||
providers.some((provider) => provider.doc === this.roomDoc)
) {
throw new Error(
'The collaboration demo requires distinct providers and Y.Docs.'
);
}
}
connect(provider: DemoProvider) {
if (this.destroyed || !this.endpointProviders.has(provider)) return;
const outbound = Y.encodeStateAsUpdate(
provider.doc,
Y.encodeStateVector(this.roomDoc)
);
this.publishDocument(provider, outbound);
provider.receive(
Y.encodeStateAsUpdate(this.roomDoc, Y.encodeStateVector(provider.doc))
);
for (const source of this.endpointProviders) {
if (source === provider || source.getSnapshot().status !== 'connected') {
continue;
}
const localState = source.awareness.getLocalState();
if (localState) {
provider.awareness.setRemoteState(source.peer.clientId, localState);
}
}
this.publishAwareness(provider);
}
destroy() {
if (this.destroyed) return;
this.destroyed = true;
for (const provider of this.providers) {
provider.destroy();
}
this.roomDoc.destroy();
const listenerCount = this.providers.reduce(
(count, provider) => count + provider.listenerCount(),
0
);
if (this.endpointProviders.size !== 0 || listenerCount !== 0) {
throw new Error(
`Collaboration demo teardown leaked ${this.endpointProviders.size} providers and ${listenerCount} listeners.`
);
}
}
disconnect(provider: DemoProvider) {
if (!this.endpointProviders.has(provider)) return;
provider.awareness.removeRemoteStates();
for (const target of this.endpointProviders) {
if (target !== provider && target.getSnapshot().status === 'connected') {
target.awareness.removeRemoteState(provider.peer.clientId);
}
}
}
publishAwareness(source: DemoProvider) {
if (this.destroyed || !this.endpointProviders.has(source)) return;
const localState = source.awareness.getLocalState();
for (const target of this.endpointProviders) {
if (target === source || target.getSnapshot().status !== 'connected') {
continue;
}
if (source.getSnapshot().status === 'connected' && localState) {
target.awareness.setRemoteState(source.peer.clientId, localState);
} else {
target.awareness.removeRemoteState(source.peer.clientId);
}
}
}
publishDocument(source: DemoProvider, update: Uint8Array) {
if (
this.destroyed ||
source.getSnapshot().status !== 'connected' ||
!this.endpointProviders.has(source)
) {
return;
}
Y.applyUpdate(this.roomDoc, update, source);
for (const target of this.endpointProviders) {
if (target !== source) {
target.receive(update);
}
}
}
snapshot() {
return Y.encodeStateAsUpdate(this.roomDoc);
}
unregister(provider: DemoProvider) {
this.endpointProviders.delete(provider);
}
}
const createPeerEditor = (
provider: DemoProvider,
schemaVersion = SCHEMA.version
) => {
const plugins = [
...BasicNodesKit,
CollaborationPlugin.create({
awareness: provider.awareness,
cursorData: {
validate: (value): value is { color: string; name: string } =>
typeof value === 'object' &&
value !== null &&
'color' in value &&
typeof value.color === 'string' &&
'name' in value &&
typeof value.name === 'string',
},
doc: provider.doc,
initialReady: true,
rootName: ROOT_NAME,
}),
] as const;
return createEditor({
id: `collaboration-demo-${provider.peer.id}-${schemaVersion}`,
initialValue: cloneInitialValue(),
plugins,
schema: {
id: SCHEMA.id,
version: schemaVersion,
},
});
};
type DemoEditor = ReturnType<typeof createPeerEditor>;
type DemoRuntime = {
editors: readonly [DemoEditor, DemoEditor];
room: DemoRoom;
};
const createDemoRuntime = (): DemoRuntime => {
const room = new DemoRoom();
try {
return {
editors: [
createPeerEditor(room.providers[0]),
createPeerEditor(room.providers[1]),
],
room,
};
} catch (error) {
room.destroy();
throw error;
}
};
const testSchemaJoin = (room: DemoRoom, version: number) => {
const doc = new Y.Doc();
Y.applyUpdate(doc, room.snapshot());
try {
const editor = createEditor({
id: `collaboration-demo-schema-probe-${version}`,
initialValue: cloneInitialValue(),
plugins: BasicNodesKit,
schema: { id: SCHEMA.id, version },
});
const uninstall = editor.install(
yjs({
doc,
initialReady: true,
rootName: ROOT_NAME,
})
);
uninstall();
} finally {
doc.destroy();
}
};
export default function CollaborativeEditingDemo(): React.ReactNode {
const [runtime, setRuntime] = React.useState<DemoRuntime | null>(null);
React.useEffect(() => {
const nextRuntime = createDemoRuntime();
let active = true;
queueMicrotask(() => {
if (active) {
setRuntime(nextRuntime);
}
});
return () => {
active = false;
nextRuntime.room.destroy();
};
}, []);
if (!runtime) {
return (
<output className="flex min-h-96 items-center justify-center text-sm text-muted-foreground">
Starting a local collaboration room…
</output>
);
}
return <CollaborationRoom runtime={runtime} />;
}
function CollaborationRoom({ runtime }: { runtime: DemoRuntime }) {
const [schemaResult, setSchemaResult] = React.useState<{
kind: 'recovered' | 'rejected';
message: string;
} | null>(null);
const rejectIncompatibleSchema = () => {
try {
testSchemaJoin(runtime.room, SCHEMA.version + 1);
setSchemaResult({
kind: 'rejected',
message: 'The incompatible schema unexpectedly joined the room.',
});
} catch (error) {
setSchemaResult({
kind: 'rejected',
message:
error instanceof Error ? error.message : 'Schema join rejected.',
});
}
};
const recoverCompatibleSchema = () => {
try {
testSchemaJoin(runtime.room, SCHEMA.version);
setSchemaResult({
kind: 'recovered',
message:
'Schema v1 joined a fresh endpoint without replacing the editors.',
});
} catch (error) {
setSchemaResult({
kind: 'rejected',
message:
error instanceof Error ? error.message : 'Schema recovery failed.',
});
}
};
return (
<div
className="flex min-w-0 flex-col gap-4 p-4"
data-collaboration-demo="local-yjs-room"
data-doc-count="3"
data-editor-count="2"
data-provider-count="2"
>
<Alert>
<AlertTitle>Development provider boundary</AlertTitle>
<AlertDescription>
This credential-free room uses two provider endpoints and three
distinct Y.Docs. Production apps own transport, auth, persistence, and
room names.
</AlertDescription>
</Alert>
<div className="grid min-w-0 grid-cols-1 gap-4 lg:grid-cols-2">
{runtime.editors.map((editor, index) => {
const peer = PEERS[index];
return (
<EditorRoot editor={editor} key={peer.id}>
<PeerCard
editor={editor}
peer={peer}
provider={runtime.room.providers[index]}
/>
</EditorRoot>
);
})}
</div>
<div className="flex flex-wrap gap-2">
<Button
data-schema-action="reject"
onClick={rejectIncompatibleSchema}
size="sm"
variant="outline"
>
Reject schema v2
</Button>
<Button
data-schema-action="recover"
onClick={recoverCompatibleSchema}
size="sm"
variant="outline"
>
<RefreshCwIcon data-icon="inline-start" />
Recover with schema v1
</Button>
</div>
{schemaResult && (
<Alert
data-schema-status={schemaResult.kind}
variant={schemaResult.kind === 'rejected' ? 'destructive' : 'default'}
>
<AlertTitle>
{schemaResult.kind === 'rejected'
? 'Incompatible schema rejected'
: 'Compatible schema recovered'}
</AlertTitle>
<AlertDescription>{schemaResult.message}</AlertDescription>
</Alert>
)}
</div>
);
}
function PeerCard({
editor,
peer,
provider,
}: {
editor: DemoEditor;
peer: DemoPeer;
provider: DemoProvider;
}) {
const admission = useYjsAdmissionStatus(editor);
const providerSnapshot = React.useSyncExternalStore(
provider.subscribe,
provider.getSnapshot,
provider.getSnapshot
);
const connected = providerSnapshot.status === 'connected';
const ready = admission.state === 'ready';
return (
<Card className="min-w-0" data-peer={peer.id}>
<CardHeader>
<div className="flex items-start justify-between gap-3">
<div className="flex flex-col gap-1">
<CardTitle>{peer.name}</CardTitle>
<CardDescription>Independent editor and Y.Doc</CardDescription>
</div>
<Badge
data-peer-status={providerSnapshot.status}
variant={connected ? 'secondary' : 'destructive'}
>
{connected && providerSnapshot.synced ? 'Synced' : 'Disconnected'}
</Badge>
<Badge
data-admission-status={admission.state}
variant={ready ? 'secondary' : 'outline'}
>
{ready ? 'Ready' : admission.state}
</Badge>
</div>
</CardHeader>
<CardContent>
{admission.state === 'error' && (
<Alert className="mb-3" variant="destructive">
<AlertTitle>Collaboration import failed</AlertTitle>
<AlertDescription className="flex items-center justify-between gap-3">
<span>
{admission.cause instanceof Error
? admission.cause.message
: String(admission.cause)}
</span>
<Button
onClick={() => editor.api.yjs.retryImport()}
size="sm"
variant="outline"
>
Retry
</Button>
</AlertDescription>
</Alert>
)}
<EditorContainer className="h-64">
<Editor
aria-label={`${peer.name} collaborative editor`}
aria-busy={admission.state === 'waiting'}
className="px-6 py-4"
readOnly={!ready}
variant="demo"
/>
</EditorContainer>
</CardContent>
<PeerControls
connected={connected}
editor={editor}
peer={peer}
provider={provider}
/>
</Card>
);
}
function PeerControls({
connected,
editor,
peer,
provider,
}: {
connected: boolean;
editor: DemoEditor;
peer: DemoPeer;
provider: DemoProvider;
}) {
const history = useEditorRuntimeState(editor, (state) => state.history());
const redoDepth = history.redos.length;
const undoDepth = history.undos.length;
React.useEffect(() => {
editor.api.yjs.setCursorData({
color: peer.color,
name: peer.name,
});
}, [editor, peer.color, peer.name]);
const toggleConnection = () => {
if (connected) {
editor.api.yjs.clearSelection();
provider.disconnect();
} else {
provider.connect();
if (editor.api.yjs.admissionStatus().state === 'ready') {
editor.api.yjs.syncSelection();
}
}
};
return (
<CardFooter className="flex flex-wrap gap-2">
<Button
aria-label={`Undo ${peer.name}`}
data-history-action="undo"
disabled={undoDepth === 0}
onMouseDown={(event) => {
event.preventDefault();
}}
onClick={() => {
editor.api.history.undo();
}}
size="sm"
variant="outline"
>
<Undo2Icon data-icon="inline-start" />
Undo {undoDepth}
</Button>
<Button
aria-label={`Redo ${peer.name}`}
data-history-action="redo"
disabled={redoDepth === 0}
onMouseDown={(event) => {
event.preventDefault();
}}
onClick={() => {
editor.api.history.redo();
}}
size="sm"
variant="outline"
>
<Redo2Icon data-icon="inline-start" />
Redo {redoDepth}
</Button>
<Button
data-connection-action={connected ? 'disconnect' : 'connect'}
onClick={toggleConnection}
size="sm"
variant="outline"
>
{connected ? <UnplugIcon data-icon="inline-start" /> : null}
{connected ? 'Disconnect' : 'Reconnect'}
</Button>
</CardFooter>
);
}'use client';
import { RefreshCwIcon, Redo2Icon, Undo2Icon, UnplugIcon } from 'lucide-react';
import { createEditor, EditorRoot, useEditorRuntimeState } from 'platejs/react';
import {
yjs,
type YjsAwarenessChange,
type YjsAwarenessState,
} from 'platejs/yjs';
import { useYjsAdmissionStatus } from 'platejs/yjs/react';
import * as React from 'react';
import * as Y from 'yjs';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge'
| Piece | Owner | Notes |
|---|---|---|
CollaborationPlugin | Copied remote-cursor-overlay file | Binds an explicit document and readiness source, then adds selection highlights, carets, and labels. |
| Provider endpoints | App infrastructure | Own connection state, transport, persistence, and cleanup outside the editor plugin. |
| Local room | Registry example | Preseeds one central document, then exchanges document and awareness updates between two independent peers. |
| Production room state | App | Keep room ids, auth tokens, persistence, and provider URLs outside Plate packages. |
| Provider controls | App | Clear selection, then disconnect the provider; reconnect it directly, then synchronize selection. |
| Admission state | editor.api.yjs | Gate editing on waiting, ready, or error independently from provider status. |
| Undo and redo | platejs/history | Replay canonical editor changes through the same Yjs bridge. |
Use the full Yjs guide when wiring a real provider.
Remote cursor UI belongs at the app edge. The reusable registry component is
remote-cursor-overlay.
'use client';
import type { EditableSiblingProps } from 'platejs/react';
import {
YjsPlugin,
useYjsRemoteCursor,
useYjsRemoteCursorGeometry,
useYjsRemoteCursorIds,
} from 'platejs/yjs/react';
import React from 'react';
type CursorData = {
color?: unknown;
name?: unknown;
};
type CursorEditor = Parameters<typeof useYjsRemoteCursor>[0];
const FALLBACK_COLORS = ['#7C3AED', '#0891B2', '#DB2777', '#4F46E5'];
const HEX_COLOR = /^#[\dA-Fa-f]{6}$/;
const cursorColor = (clientId: number, data: CursorData | undefined) =>
typeof data?.color === 'string' && HEX_COLOR.test(data.color)
? data.color
: FALLBACK_COLORS[Math.abs(clientId) % FALLBACK_COLORS.length];
const cursorName = (clientId: number, data: CursorData | undefined) =>
typeof data?.name === 'string' && data.name.trim().length > 0
? data.name
: `Guest ${clientId}`;
/** Creates Yjs collaboration with copied selection, caret, and label UI. */
export const CollaborationPlugin = YjsPlugin.require('awareness').map(
({ api, editor }) => ({
decorate: {
attributes: ({ decoration }) => {
const clientId = Number(decoration.key);
const cursor = api.remoteCursor(clientId);
return {
style: {
backgroundColor: `${cursorColor(clientId, cursor?.data)}33`,
},
};
},
},
slots: {
afterEditable: ({ editableRef }) => (
<RemoteCursorOverlay editableRef={editableRef} editor={editor} />
),
},
})
);
function RemoteCursor({
clientId,
editableRef,
editor,
}: EditableSiblingProps & {
readonly clientId: number;
readonly editor: CursorEditor;
}) {
const cursor = useYjsRemoteCursor(editor, clientId);
const geometry = useYjsRemoteCursorGeometry(editor, clientId, {
editableRef,
});
const labelRef = React.useRef<HTMLSpanElement>(null);
const [labelOffset, setLabelOffset] = React.useState(0);
const focusLeft = geometry?.focusRect?.left ?? null;
const name = cursorName(clientId, cursor?.data);
React.useLayoutEffect(() => {
const label = labelRef.current;
if (!label || focusLeft === null) return;
const inset = 4;
const viewportWidth = label.ownerDocument.documentElement.clientWidth;
const labelWidth = label.getBoundingClientRect().width;
const maxLeft = Math.max(inset, viewportWidth - labelWidth - inset);
const nextLeft = Math.min(Math.max(focusLeft, inset), maxLeft);
const nextOffset = nextLeft - focusLeft;
setLabelOffset((current) =>
current === nextOffset ? current : nextOffset
);
}, [focusLeft, name]);
if (!cursor?.selection || !geometry?.focusRect) return null;
const color = cursorColor(clientId, cursor.data);
const { focusRect } = geometry;
return (
<span
className="pointer-events-none fixed w-0.5"
data-client-id={clientId}
data-remote-caret=""
style={{
backgroundColor: color,
height: Math.max(focusRect.height, 16),
left: focusRect.left,
top: focusRect.top,
}}
>
<span
ref={labelRef}
className="absolute top-0 left-0 max-w-[calc(100vw-0.5rem)] truncate rounded-t-sm rounded-br-sm px-1.5 py-0.5 text-xs text-white"
data-remote-cursor-label=""
style={{
backgroundColor: color,
transform: `translate(${labelOffset}px, -100%)`,
}}
>
{name}
</span>
</span>
);
}
export function RemoteCursorOverlay({
editableRef,
editor,
}: EditableSiblingProps & { readonly editor: CursorEditor }) {
const clientIds = useYjsRemoteCursorIds(editor);
return (
<div
aria-hidden="true"
className="pointer-events-none fixed inset-0"
data-remote-cursor-overlay=""
>
{clientIds.map((clientId) => (
<RemoteCursor
clientId={clientId}
editableRef={editableRef}
editor={editor}
key={clientId}
/>
))}
</div>
);
}'use client';
import type { EditableSiblingProps } from 'platejs/react';
import {
YjsPlugin,
useYjsRemoteCursor,
useYjsRemoteCursorGeometry,
useYjsRemoteCursorIds,
} from 'platejs/yjs/react';
import React from 'react';
type CursorData = {
color?: unknown;
name?: unknown;
};
type CursorEditor = Parameters<typeof useYjsRemoteCursor>[0];
const FALLBACK_COLORS = ['#7C3AED'
Call CollaborationPlugin.create() from the copied file with doc,
initialReady, and awareness from your app-owned provider. It styles remote
selections and installs the caret overlay beside the Editable. Edit this file to
customize colors, opacity, and labels.
The overlay reads cursor state and DOM geometry from platejs/yjs/react,
excludes the local cursor, and ignores unresolved ranges.
Type in either editor to see remote document and cursor updates. Disconnect one peer to queue local work, reconnect it, then use the standard history controls to undo or redo only that peer's local change. Each card reports provider and admission status separately. The schema controls show a rejected incompatible join and a compatible recovery without replacing either editor.