From 3f7d644312f2752753258502dae1e5ce071fb930 Mon Sep 17 00:00:00 2001
From: Shayan
Date: Wed, 17 Jun 2026 16:36:23 +0330
Subject: [PATCH] docs(collab): use site code-registry for snippets + live demo
Register the collaboration code samples in codes.tsx (picked up by
generate:registry / prebuild) and render the docs page with the site's
SimpleCodeBlock + DynamicCodeExample components, so snippets are
syntax-highlighted and the y-webrtc demo shows source + live preview
consistently with the other extension pages.
---
.../CollaborationExtension/codes.tsx | 143 +++++++++++++++++
.../CollaborationExtension/page.client.tsx | 148 +++++-------------
2 files changed, 180 insertions(+), 111 deletions(-)
create mode 100644 apps/web/app/(docs)/docs/extensions/CollaborationExtension/codes.tsx
diff --git a/apps/web/app/(docs)/docs/extensions/CollaborationExtension/codes.tsx b/apps/web/app/(docs)/docs/extensions/CollaborationExtension/codes.tsx
new file mode 100644
index 0000000..e62f322
--- /dev/null
+++ b/apps/web/app/(docs)/docs/extensions/CollaborationExtension/codes.tsx
@@ -0,0 +1,143 @@
+import { RegisteredCodeSnippet } from "../../../lib/types";
+
+export const COLLABORATION_EXTENSION_CODES: RegisteredCodeSnippet[] = [
+ {
+ id: "collaboration-install",
+ code: `# Pick a transport: y-webrtc (P2P), y-websocket (server), or y-indexeddb (offline)
+pnpm add yjs y-webrtc`,
+ language: "bash",
+ title: "Install",
+ description:
+ "yjs + a transport are optional peer deps — an editor without the extension pulls in nothing collaboration-related.",
+ },
+ {
+ id: "collaboration-quick-start",
+ code: `import {
+ createEditorSystem,
+ richTextExtension,
+ collaborationExtension,
+ type CollaborationProvider,
+} from "@lexkit/editor";
+import { WebrtcProvider } from "y-webrtc";
+import * as Y from "yjs";
+
+const extensions = [
+ richTextExtension,
+ collaborationExtension.configure({
+ id: "my-room",
+ username: "Ada",
+ cursorColor: "#e11d48",
+ providerFactory: (id, docMap) => {
+ let doc = docMap.get(id);
+ if (!doc) { doc = new Y.Doc(); docMap.set(id, doc); }
+ return new WebrtcProvider(id, doc) as unknown as CollaborationProvider;
+ },
+ }),
+] as const;
+
+const { Provider, useEditor } = createEditorSystem();`,
+ language: "tsx",
+ title: "Quick start",
+ description:
+ "Collaboration is just another extension. The only required option is providerFactory — the single seam where you plug in a transport.",
+ highlightLines: [11, 12, 13, 14, 15, 16, 17, 18, 19],
+ },
+ {
+ id: "collaboration-y-websocket",
+ code: `import { WebsocketProvider } from "y-websocket";
+
+providerFactory: (id, docMap) => {
+ let doc = docMap.get(id);
+ if (!doc) { doc = new Y.Doc(); docMap.set(id, doc); }
+ return new WebsocketProvider("wss://your-server", id, doc) as any;
+}`,
+ language: "typescript",
+ title: "y-websocket (client + server)",
+ description: "Production-style client/server transport.",
+ },
+ {
+ id: "collaboration-y-webrtc",
+ code: `import { WebrtcProvider } from "y-webrtc";
+
+providerFactory: (id, docMap) => {
+ let doc = docMap.get(id);
+ if (!doc) { doc = new Y.Doc(); docMap.set(id, doc); }
+ // Same-origin tabs sync via BroadcastChannel with no signaling at all.
+ // Add signaling servers for cross-device collaboration.
+ return new WebrtcProvider(id, doc, {
+ signaling: ["wss://your-signaling-server"],
+ }) as any;
+}`,
+ language: "typescript",
+ title: "y-webrtc (peer-to-peer)",
+ description: "Zero-infra for same-origin tabs; add signaling for cross-device.",
+ },
+ {
+ id: "collaboration-y-indexeddb",
+ code: `import { IndexeddbPersistence } from "y-indexeddb";
+
+providerFactory: (id, docMap) => {
+ let doc = docMap.get(id);
+ if (!doc) { doc = new Y.Doc(); docMap.set(id, doc); }
+ // Persist locally so edits survive reloads and replay when back online.
+ new IndexeddbPersistence(id, doc);
+ return new WebsocketProvider("wss://your-server", id, doc) as any;
+}`,
+ language: "typescript",
+ title: "y-indexeddb (offline persistence)",
+ description: "Combine with another provider to persist a client's edits locally.",
+ },
+ {
+ id: "collaboration-presence-hook",
+ code: `import { useCollaborators } from "@lexkit/editor";
+
+function PresenceBar() {
+ const { users, isConnected } = useCollaborators();
+ return (
+
+ {isConnected ? "live" : "offline"} · {users.length} online
+ {users.map((u) => (
+
+ {u.name}{u.isLocal ? " (you)" : ""}
+
+ ))}
+
+ );
+}`,
+ language: "tsx",
+ title: "Presence with useCollaborators()",
+ description: "Build any presence UI from the headless awareness API.",
+ highlightLines: [4, 8, 9, 10],
+ },
+ {
+ id: "collaboration-commands",
+ code: `const { commands, activeStates } = useEditor();
+
+commands.collab.connect();
+commands.collab.disconnect();
+commands.collab.toggle();
+commands.collab.setLocalUser({ name: "Ada", color: "#e11d48", awarenessData: { avatar } });
+commands.collab.getUsers(); // CollaboratorState[]
+commands.collab.onUsersChange(cb); // subscribe; returns unsubscribe
+activeStates.isCollaborating; // boolean`,
+ language: "typescript",
+ title: "Imperative API",
+ description: "Drive collaboration through commands and state queries.",
+ },
+ {
+ id: "collaboration-theme",
+ code: `const theme = {
+ collab: {
+ cursorsContainer: "my-cursors-layer",
+ caret: "my-collab-caret",
+ label: "my-collab-label",
+ selection: "my-collab-selection",
+ },
+};`,
+ language: "typescript",
+ title: "Themeable cursors",
+ description: "Style remote carets via theme classes; color comes from cursorColor.",
+ },
+];
+
+export default COLLABORATION_EXTENSION_CODES;
diff --git a/apps/web/app/(docs)/docs/extensions/CollaborationExtension/page.client.tsx b/apps/web/app/(docs)/docs/extensions/CollaborationExtension/page.client.tsx
index c45f16f..9dfc96b 100644
--- a/apps/web/app/(docs)/docs/extensions/CollaborationExtension/page.client.tsx
+++ b/apps/web/app/(docs)/docs/extensions/CollaborationExtension/page.client.tsx
@@ -1,6 +1,9 @@
"use client";
import dynamic from "next/dynamic";
+import { DynamicCodeExample } from "../../../components/dynamic-code-example";
+import { SimpleCodeBlock } from "../../../components/simple-code-block";
+import { getHighlightedCode, getRawCode } from "@/lib/generated/code-registry";
import { Badge } from "@repo/ui/components/badge";
import { Users, Wifi, Palette, Plug } from "lucide-react";
@@ -21,18 +24,13 @@ const BasicCollaborativeEditor = dynamic(
},
);
-function Code({ children, title }: { children: string; title?: string }) {
+function Snippet({ id, title }: { id: string; title?: string }) {
return (
-
- {title ? (
-
- {title}
-
- ) : null}
-
- {children}
-
-
+
);
}
@@ -74,7 +72,15 @@ export default function CollaborationExtensionPageClient() {
tabs sync peer-to-peer via the browser's BroadcastChannel, so this
demo needs no server at all.
-
+ }
+ tabs={["preview", "Editor"]}
+ />
{/* Install */}
@@ -82,49 +88,20 @@ export default function CollaborationExtensionPageClient() {
Install
The extension ships with LexKit. You add yjs and a
- transport of your choice (they are optional peer dependencies — an
- editor without the extension pulls in nothing collaboration-related).
+ transport of your choice — they are optional peer dependencies.
- {`# pick a transport: y-webrtc (P2P), y-websocket (server), or y-indexeddb (offline)
-pnpm add yjs y-webrtc`}
+
{/* Quick start */}
Quick start
- Collaboration is just another extension. The only required option is{" "}
- providerFactory — the single seam where you plug in a
- transport. LexKit never bundles one.
-
- {`import {
- createEditorSystem,
- richTextExtension,
- collaborationExtension,
- type CollaborationProvider,
-} from "@lexkit/editor";
-import { WebrtcProvider } from "y-webrtc";
-import * as Y from "yjs";
-
-const extensions = [
- richTextExtension,
- collaborationExtension.configure({
- id: "my-room",
- username: "Ada",
- cursorColor: "#e11d48",
- providerFactory: (id, docMap) => {
- let doc = docMap.get(id);
- if (!doc) { doc = new Y.Doc(); docMap.set(id, doc); }
- return new WebrtcProvider(id, doc) as unknown as CollaborationProvider;
- },
- }),
-] as const;
-
-const { Provider, useEditor } = createEditorSystem();`}
-
- The extension stays inert until providerFactory is set, so
- it's safe to leave in the array and toggle collaboration on later.
+ The only required option is providerFactory — the single
+ seam where you plug in a transport. LexKit never bundles one, and the
+ extension stays inert until it's set.
+
{/* Providers */}
@@ -133,65 +110,21 @@ const { Provider, useEditor } = createEditorSystem();`}
Anything implementing the Yjs Provider interface works.
- {`import { WebsocketProvider } from "y-websocket";
-
-providerFactory: (id, docMap) => {
- let doc = docMap.get(id);
- if (!doc) { doc = new Y.Doc(); docMap.set(id, doc); }
- return new WebsocketProvider("wss://your-server", id, doc) as any;
-}`}
- {`import { WebrtcProvider } from "y-webrtc";
-
-providerFactory: (id, docMap) => {
- let doc = docMap.get(id);
- if (!doc) { doc = new Y.Doc(); docMap.set(id, doc); }
- return new WebrtcProvider(id, doc, {
- signaling: ["wss://your-signaling-server"],
- }) as any;
-}`}
- {`import { IndexeddbPersistence } from "y-indexeddb";
-
-providerFactory: (id, docMap) => {
- let doc = docMap.get(id);
- if (!doc) { doc = new Y.Doc(); docMap.set(id, doc); }
- // Persist locally so edits survive reloads and replay when back online.
- new IndexeddbPersistence(id, doc);
- return new WebsocketProvider("wss://your-server", id, doc) as any;
-}`}
+
+
+
{/* Presence */}
Presence
- Build any presence UI with the headless useCollaborators(){" "}
- hook, or drive it imperatively through{" "}
+ Build any presence UI with the headless{" "}
+ useCollaborators() hook, or drive it imperatively through{" "}
commands.collab.
- {`import { useCollaborators } from "@lexkit/editor";
-
-function PresenceBar() {
- const { users, isConnected } = useCollaborators();
- return (
-
- {isConnected ? "live" : "offline"} · {users.length} online
- {users.map((u) => (
-
- {u.name}{u.isLocal ? " (you)" : ""}
-
- ))}
-
- );
-}`}
- {`const { commands, activeStates } = useEditor();
-
-commands.collab.connect();
-commands.collab.disconnect();
-commands.collab.toggle();
-commands.collab.setLocalUser({ name: "Ada", color: "#e11d48", awarenessData: { avatar } });
-commands.collab.getUsers(); // CollaboratorState[]
-commands.collab.onUsersChange(cb); // subscribe; returns unsubscribe
-activeStates.isCollaborating; // boolean`}
+
+
{/* Theming cursors */}
@@ -203,14 +136,7 @@ activeStates.isCollaborating; // boolean`}
color comes from cursorColor. Supply your own{" "}
cursorsContainerRef to portal carets anywhere.
- {`const theme = {
- collab: {
- cursorsContainer: "my-cursors-layer",
- caret: "my-collab-caret",
- label: "my-collab-label",
- selection: "my-collab-selection",
- },
-};`}
+
{/* Offline */}
@@ -219,11 +145,11 @@ activeStates.isCollaborating; // boolean`}
Yjs is a CRDT: when a client drops offline and keeps editing, then
reconnects, all edits merge automatically with no data loss and every
- replica converges to the same state. Add{" "}
- y-indexeddb to persist a client's edits locally so
- they survive reloads and replay on reconnect. This behaviour is
- identical no matter how content is imported (Markdown, HTML or JSON) —
- collaboration syncs the editor state itself.
+ replica converges to the same state. Add y-indexeddb to
+ persist a client's edits locally so they survive reloads and
+ replay on reconnect. This behaviour is identical no matter how content
+ is imported (Markdown, HTML or JSON) — collaboration syncs the editor
+ state itself.