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.