From 83863e14ce9b81663f6ce260491045f5a0c56f41 Mon Sep 17 00:00:00 2001 From: Poonam Dharamkar Date: Fri, 24 Jul 2026 16:51:28 -0400 Subject: [PATCH 1/5] feat(RichEditor): optional Yjs collaborative editing via a collab prop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `collab` prop to RichEditor that enables live co-editing over a `/yjs` websocket relay. Because ExtensionYjs conflicts with the default history extension, collaborative mode swaps AdvancedEditorKit for the same extensions minus `history`, plus MarkYChange + ExtensionYjs. The websocket provider threads caller-supplied query params (e.g. an auth token) so the relay can authorize the room — unlike editor-kits' own YjsEditorKit. Room is joined after the local markdown is loaded; the Yjs binding seeds an empty shared doc from that content on first join and overwrites the editor when the room already has edits, so stored markdown stays the source of truth. --- package.json | 21 +++++- src/components/RichEditor/RichEditor.tsx | 38 +++++++++- src/components/RichEditor/collabKits.ts | 92 ++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 src/components/RichEditor/collabKits.ts diff --git a/package.json b/package.json index 7b748a20d..a7f1fec3a 100644 --- a/package.json +++ b/package.json @@ -234,8 +234,11 @@ "peerDependencies": { "@kerebron/editor": ">=0.7.9", "@kerebron/editor-kits": ">=0.7.9", + "@kerebron/extension-yjs": ">=0.8.6", "@kerebron/wasm": ">=0.7.9", "@mieweb/datavis": "=1.5.0", + "y-protocols": ">=1.0.6", + "yjs": ">=13.6.0", "ag-grid-community": ">=32.0.0", "ag-grid-react": ">=32.0.0", "datavis-ace": "=4.1.0", @@ -260,9 +263,18 @@ "@kerebron/editor-kits": { "optional": true }, + "@kerebron/extension-yjs": { + "optional": true + }, "@kerebron/wasm": { "optional": true }, + "y-protocols": { + "optional": true + }, + "yjs": { + "optional": true + }, "@mieweb/datavis": { "optional": true }, @@ -341,9 +353,10 @@ "@esheet/fields": "0.0.3", "@esheet/renderer": "0.0.3", "@eslint/js": "^9.39.3", - "@kerebron/editor": "0.7.9", - "@kerebron/editor-kits": "0.7.9", - "@kerebron/wasm": "0.7.9", + "@kerebron/editor": "^0.8.6", + "@kerebron/editor-kits": "^0.8.6", + "@kerebron/extension-yjs": "^0.8.6", + "@kerebron/wasm": "^0.8.6", "@mieweb/datavis": "=1.5.0", "@monaco-editor/react": "^4.7.0", "@playwright/test": "^1.58.2", @@ -413,7 +426,9 @@ "vite": "^7.3.2", "vitest": "^3.2.6", "wavesurfer.js": "^7.12.1", + "y-protocols": "^1.0.6", "ychart": "file:./packages/ychart", + "yjs": "^13.6.30", "zod": "^4.4.3", "zustand": "^5.0.14" }, diff --git a/src/components/RichEditor/RichEditor.tsx b/src/components/RichEditor/RichEditor.tsx index 33b51443a..3c9d2b49c 100644 --- a/src/components/RichEditor/RichEditor.tsx +++ b/src/components/RichEditor/RichEditor.tsx @@ -4,6 +4,10 @@ import { CoreEditor } from '@kerebron/editor'; import { AdvancedEditorKit } from '@kerebron/editor-kits/AdvancedEditorKit'; import { createAssetLoad } from '@kerebron/wasm/web'; +import { createCollabEditorKits, type CollabConfig } from './collabKits'; + +export type { CollabConfig } from './collabKits'; + export interface RichEditorProps { /** Initial markdown content to load into the editor. */ value?: string; @@ -11,12 +15,20 @@ export interface RichEditorProps { onChange?: (value: string) => void; /** Whether to render the live markdown output preview. Defaults to `false`. */ showPreview?: boolean; + /** + * Enable live collaborative editing (Yjs) for the given room. When set, the + * editor connects to the `/yjs` websocket relay and every peer in the same + * `room` co-edits one shared document. Uncontrolled like `value` — remount via + * `key` to switch rooms. + */ + collab?: CollabConfig; } const RichEditor: React.FC = ({ value = '', onChange, showPreview = false, + collab, }) => { const editorRef = useRef(null); const editorInstance = useRef(null); @@ -28,12 +40,15 @@ const RichEditor: React.FC = ({ useEffect(() => { if (!editorRef.current) return; - // Initialize the editor + // Initialize the editor. In collaborative mode swap the default kits for + // the Yjs kits (advanced editing minus `history`, plus the CRDT sync). const editor = CoreEditor.create({ element: editorRef.current, uri: 'file:///untitled.md', assetLoad: createAssetLoad('/kerebron-wasm'), - editorKits: [new AdvancedEditorKit()], + editorKits: collab + ? createCollabEditorKits(collab) + : [new AdvancedEditorKit()], }); editorInstance.current = editor; @@ -56,13 +71,29 @@ const RichEditor: React.FC = ({ editor.addEventListener('transaction', onTransaction); // Seed initial content, then populate the preview once on mount. + // + // In collaborative mode we still load the local markdown first, then join + // the room: the Yjs binding seeds an *empty* shared document from this + // content on the first join, and overwrites the editor with the shared + // content when the room already has edits — so the stored markdown is the + // starting point without ever double-inserting. + const joinRoom = () => { + if (collab) { + (editor.run as Record boolean>).changeRoom?.( + collab.room, + ); + } + }; + if (value) { editor .loadDocumentText('text/x-markdown', value) .then(() => onTransaction()) + .then(joinRoom) .catch((err) => console.error('Failed to load markdown:', err)); } else { void onTransaction(); + joinRoom(); } // Cleanup on unmount @@ -70,7 +101,8 @@ const RichEditor: React.FC = ({ editor.removeEventListener('transaction', onTransaction); editor.destroy(); }; - // Initial `value` is intentionally only applied on mount (uncontrolled). + // Initial `value`/`collab` are intentionally only applied on mount + // (uncontrolled). Remount via `key` to switch rooms. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/RichEditor/collabKits.ts b/src/components/RichEditor/collabKits.ts new file mode 100644 index 000000000..5fdc9dab7 --- /dev/null +++ b/src/components/RichEditor/collabKits.ts @@ -0,0 +1,92 @@ +/** + * Collaborative (Yjs) editor kit assembly for {@link RichEditor}. + * + * The default {@link AdvancedEditorKit} bundles `ExtensionHistory` (undo/redo). + * `ExtensionYjs` provides its own CRDT-aware history and therefore *conflicts* + * with the `history` extension — the editor throws + * `Extension conflict: yjs vs history` if both are present. So for collaborative + * mode we reuse every advanced extension **except** `history`, then add the Yjs + * pieces on top. + * + * We deliberately do not use `@kerebron/editor-kits`' own `YjsEditorKit`: it + * constructs the `WebsocketProvider` with no query params, leaving no way to + * authenticate the socket. Here we thread caller-supplied `params` (e.g. an auth + * token) into the provider so the backend `/yjs` route can authorize the room. + */ +import type { EditorKit } from '@kerebron/editor'; +import { AdvancedEditorKit } from '@kerebron/editor-kits/AdvancedEditorKit'; +import { ExtensionYjs } from '@kerebron/extension-yjs'; +import { WebsocketProvider } from '@kerebron/extension-yjs/WebsocketProvider'; +import { MarkYChange } from '@kerebron/extension-yjs/MarkYChange'; +import * as awarenessProtocol from 'y-protocols/awareness'; +import * as Y from 'yjs'; + +export interface CollabConfig { + /** Room id — one shared document per room (e.g. a post id). */ + room: string; + /** + * WebSocket base URL for the Yjs relay. Defaults to + * `:///yjs`. The room id is appended by the provider. + */ + wsUrl?: string; + /** Extra query params for the socket (e.g. `{ token }` for auth). */ + params?: Record; +} + +/** Derive the default `/yjs` websocket URL from the current page origin. */ +function defaultWsUrl(): string { + const loc = globalThis.location; + const protocol = loc && loc.protocol === 'https:' ? 'wss:' : 'ws:'; + const host = loc ? loc.host : 'localhost'; + return `${protocol}//${host}/yjs`; +} + +/** Advanced editing extensions minus `history` (yjs supplies its own). */ +class CollabAdvancedEditorKit implements EditorKit { + name = 'advanced-editor'; + getExtensions() { + return new AdvancedEditorKit() + .getExtensions() + .filter((extension) => !('name' in extension && extension.name === 'history')); + } +} + +/** MarkYChange + ExtensionYjs, with an authenticated websocket provider. */ +class HuddleYjsKit implements EditorKit { + name = 'yjs-editor'; + constructor( + private readonly url: string, + private readonly params: Record, + ) {} + + getExtensions() { + const url = this.url; + const params = this.params; + const createYjsProvider = (roomId: string): [WebsocketProvider, Y.Doc] => { + const ydoc = new Y.Doc({ gc: false }); + // The provider's opts default is a *default parameter*, not a merge, so we + // must pass every field when we want to set `params`. + const provider = new WebsocketProvider(url, roomId, ydoc, { + connect: true, + awareness: new awarenessProtocol.Awareness(ydoc), + params, + protocols: [], + WebSocketPolyfill: WebSocket, + resyncInterval: -1, + maxBackoffTime: 2500, + disableBc: false, + }); + return [provider, ydoc]; + }; + return [new MarkYChange(), new ExtensionYjs({ createYjsProvider })]; + } +} + +/** Build the editor kits for a collaborative session. */ +export function createCollabEditorKits(config: CollabConfig): EditorKit[] { + const url = config.wsUrl ?? defaultWsUrl(); + return [ + new CollabAdvancedEditorKit(), + new HuddleYjsKit(url, config.params ?? {}), + ]; +} From 14b896d3b7721e3729d5bc80329f62b6af1d4d7c Mon Sep 17 00:00:00 2001 From: Poonam Dharamkar Date: Mon, 27 Jul 2026 10:50:29 -0400 Subject: [PATCH 2/5] fix(collab): filter autocomplete and hover extensions in collab mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both extensions store stale ProseMirror node refs in debounced callbacks. When a Yjs remote update replaces the document tree, their deferred dispatchMeta calls crash with null.matchesNode() inside EditorView.updateStateInner, leaving the view permanently broken and blocking further sync. Removing them in collab mode has no functional cost — these are UI convenience features, not required for collaborative text editing. --- src/components/RichEditor/collabKits.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/components/RichEditor/collabKits.ts b/src/components/RichEditor/collabKits.ts index 5fdc9dab7..a6101c2f4 100644 --- a/src/components/RichEditor/collabKits.ts +++ b/src/components/RichEditor/collabKits.ts @@ -41,13 +41,29 @@ function defaultWsUrl(): string { return `${protocol}//${host}/yjs`; } -/** Advanced editing extensions minus `history` (yjs supplies its own). */ +/** Advanced editing extensions minus `history`, `autocomplete`, and `hover`. + * + * `history` conflicts with ExtensionYjs (which supplies its own CRDT-aware + * undo/redo). `autocomplete` and `hover` both store stale node references in + * debounced callbacks; when a Yjs remote update replaces the document tree, + * their deferred `dispatchMeta` calls crash with `null.matchesNode()` inside + * ProseMirror's `EditorView.updateStateInner`, leaving the view in a + * permanently broken state that blocks further sync. Removing them in collab + * mode has no functional cost — autocomplete popups and node-hover tooltips are + * compositor conveniences, not required for collaborative text editing. + */ class CollabAdvancedEditorKit implements EditorKit { name = 'advanced-editor'; getExtensions() { return new AdvancedEditorKit() .getExtensions() - .filter((extension) => !('name' in extension && extension.name === 'history')); + .filter( + (extension) => + !('name' in extension && + (extension.name === 'history' || + extension.name === 'autocomplete' || + extension.name === 'hover')), + ); } } From e159b7b079d2ef9f379c28b37b78a252b03ccd01 Mon Sep 17 00:00:00 2001 From: Poonam Dharamkar Date: Mon, 27 Jul 2026 16:01:51 -0400 Subject: [PATCH 3/5] fix(RichEditor): drop teardown-unsafe extensions in plain mode too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `autocomplete` and `hover` debounce their DOM handlers by 200ms and then call `dispatchMeta`, which reads `this.editor.state` with no guard that the editor is still alive. HoverPlugin's `destroy()` hook tears down the renderer but never cancels those pending timers, so any unmount inside the debounce window lands the deferred call on a dead view and throws `null.matchesNode()` inside ProseMirror's `EditorView.updateStateInner`, leaving the view permanently broken. 14b896d removed both extensions for collaborative mode, where a Yjs remote update replacing the document tree triggers it. But the same crash fires without Yjs: remounting the editor (a `key` change) while the pointer is over it dispatches the debounced `mouseleave` after the view is destroyed. Plain mode still built a raw `AdvancedEditorKit`, so it kept both. Route both modes through `createEditorKits()` so the filter applies everywhere. `history` stays collab-only — it is dropped there because ExtensionYjs supplies its own CRDT-aware undo/redo and the editor throws `Extension conflict: yjs vs history`, which does not apply to plain mode. Renames `collabKits.ts` to `editorKits.ts` since it now assembles kits for both modes. Co-Authored-By: Claude Opus 5 --- src/components/RichEditor/RichEditor.tsx | 14 ++-- .../{collabKits.ts => editorKits.ts} | 67 ++++++++++++------- src/components/RichEditor/index.ts | 2 +- 3 files changed, 49 insertions(+), 34 deletions(-) rename src/components/RichEditor/{collabKits.ts => editorKits.ts} (55%) diff --git a/src/components/RichEditor/RichEditor.tsx b/src/components/RichEditor/RichEditor.tsx index 3c9d2b49c..87011e77e 100644 --- a/src/components/RichEditor/RichEditor.tsx +++ b/src/components/RichEditor/RichEditor.tsx @@ -1,12 +1,11 @@ import React, { useEffect, useRef, useState } from 'react'; import { CoreEditor } from '@kerebron/editor'; -import { AdvancedEditorKit } from '@kerebron/editor-kits/AdvancedEditorKit'; import { createAssetLoad } from '@kerebron/wasm/web'; -import { createCollabEditorKits, type CollabConfig } from './collabKits'; +import { createEditorKits, type CollabConfig } from './editorKits'; -export type { CollabConfig } from './collabKits'; +export type { CollabConfig } from './editorKits'; export interface RichEditorProps { /** Initial markdown content to load into the editor. */ @@ -40,15 +39,14 @@ const RichEditor: React.FC = ({ useEffect(() => { if (!editorRef.current) return; - // Initialize the editor. In collaborative mode swap the default kits for - // the Yjs kits (advanced editing minus `history`, plus the CRDT sync). + // Initialize the editor. Both modes drop the teardown-unsafe extensions; + // collaborative mode additionally swaps `history` for the Yjs CRDT sync. + // See `editorKits.ts` for why. const editor = CoreEditor.create({ element: editorRef.current, uri: 'file:///untitled.md', assetLoad: createAssetLoad('/kerebron-wasm'), - editorKits: collab - ? createCollabEditorKits(collab) - : [new AdvancedEditorKit()], + editorKits: createEditorKits(collab), }); editorInstance.current = editor; diff --git a/src/components/RichEditor/collabKits.ts b/src/components/RichEditor/editorKits.ts similarity index 55% rename from src/components/RichEditor/collabKits.ts rename to src/components/RichEditor/editorKits.ts index a6101c2f4..3ea345a2d 100644 --- a/src/components/RichEditor/collabKits.ts +++ b/src/components/RichEditor/editorKits.ts @@ -1,12 +1,10 @@ /** - * Collaborative (Yjs) editor kit assembly for {@link RichEditor}. + * Editor kit assembly for {@link RichEditor}, for both plain and collaborative + * (Yjs) mode. * - * The default {@link AdvancedEditorKit} bundles `ExtensionHistory` (undo/redo). - * `ExtensionYjs` provides its own CRDT-aware history and therefore *conflicts* - * with the `history` extension — the editor throws - * `Extension conflict: yjs vs history` if both are present. So for collaborative - * mode we reuse every advanced extension **except** `history`, then add the Yjs - * pieces on top. + * Both modes start from {@link AdvancedEditorKit} and drop the extensions that + * are unsafe for our usage — see {@link unsafeExtensions} — then collaborative + * mode adds the Yjs pieces on top. * * We deliberately do not use `@kerebron/editor-kits`' own `YjsEditorKit`: it * constructs the `WebsocketProvider` with no query params, leaving no way to @@ -41,28 +39,42 @@ function defaultWsUrl(): string { return `${protocol}//${host}/yjs`; } -/** Advanced editing extensions minus `history`, `autocomplete`, and `hover`. +/** + * Extensions dropped from {@link AdvancedEditorKit}, and why. + * + * `autocomplete` and `hover` debounce their DOM handlers (200ms) and then call + * `dispatchMeta`, which reads `this.editor.state` with no guard that the editor + * is still alive. Any teardown or document swap inside that debounce window + * lands the deferred call on a dead view and throws + * `null.matchesNode()` inside ProseMirror's `EditorView.updateStateInner`, + * leaving the view permanently broken. Two ways to hit it: + * - remount (e.g. a `key` change) while the pointer is over the editor, which + * fires the debounced `onMouseLeave` after the view is destroyed; + * - a Yjs remote update replacing the document tree under a pending callback. + * The first applies to *every* editor, so both extensions come out in both + * modes. No functional cost — autocomplete popups and node-hover tooltips are + * compositor conveniences, not required for editing. * - * `history` conflicts with ExtensionYjs (which supplies its own CRDT-aware - * undo/redo). `autocomplete` and `hover` both store stale node references in - * debounced callbacks; when a Yjs remote update replaces the document tree, - * their deferred `dispatchMeta` calls crash with `null.matchesNode()` inside - * ProseMirror's `EditorView.updateStateInner`, leaving the view in a - * permanently broken state that blocks further sync. Removing them in collab - * mode has no functional cost — autocomplete popups and node-hover tooltips are - * compositor conveniences, not required for collaborative text editing. + * `history` is collab-only: ExtensionYjs supplies its own CRDT-aware undo/redo + * and the editor throws `Extension conflict: yjs vs history` if both are + * present. Plain mode keeps it, so undo/redo still works there. */ -class CollabAdvancedEditorKit implements EditorKit { +const unsafeExtensions = ['autocomplete', 'hover'] as const; + +/** {@link AdvancedEditorKit} minus {@link unsafeExtensions} (and, for collab + * mode, minus `history`). */ +class SafeAdvancedEditorKit implements EditorKit { name = 'advanced-editor'; + constructor(private readonly forCollab: boolean) {} + getExtensions() { + const dropped: string[] = [...unsafeExtensions]; + if (this.forCollab) dropped.push('history'); + return new AdvancedEditorKit() .getExtensions() .filter( - (extension) => - !('name' in extension && - (extension.name === 'history' || - extension.name === 'autocomplete' || - extension.name === 'hover')), + (extension) => !('name' in extension && dropped.includes(extension.name)), ); } } @@ -98,11 +110,16 @@ class HuddleYjsKit implements EditorKit { } } -/** Build the editor kits for a collaborative session. */ -export function createCollabEditorKits(config: CollabConfig): EditorKit[] { +/** + * Build the editor kits for a session. Pass `config` to join a collaborative + * room; omit it for a plain local editor. + */ +export function createEditorKits(config?: CollabConfig): EditorKit[] { + if (!config) return [new SafeAdvancedEditorKit(false)]; + const url = config.wsUrl ?? defaultWsUrl(); return [ - new CollabAdvancedEditorKit(), + new SafeAdvancedEditorKit(true), new HuddleYjsKit(url, config.params ?? {}), ]; } diff --git a/src/components/RichEditor/index.ts b/src/components/RichEditor/index.ts index 5b8d53bf4..ed3d8683e 100644 --- a/src/components/RichEditor/index.ts +++ b/src/components/RichEditor/index.ts @@ -1,3 +1,3 @@ -export { RichEditor, type RichEditorProps } from './RichEditor'; +export { RichEditor, type RichEditorProps, type CollabConfig } from './RichEditor'; export { CodeEditor, type CodeEditorProps } from './CodeEditor'; From 7112a9c0768963299a9e9b408a5c34490c67f43a Mon Sep 17 00:00:00 2001 From: william garrity Date: Mon, 3 Aug 2026 18:29:41 -0400 Subject: [PATCH 4/5] =?UTF-8?q?fix(RichEditor):=20address=20PR=20#344=20re?= =?UTF-8?q?view=20=E2=80=94=20guard=20teardown=20races,=20lazy-load=20yjs?= =?UTF-8?q?=20kit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - guard all async continuations with a disposed flag so nothing touches the editor after destroy(); clear editorInstance ref on cleanup - move Yjs imports into collabKit.ts behind a dynamic import() so yjs, y-protocols and @kerebron/extension-yjs are truly optional peers - bump @kerebron/* peer minimums to >=0.8.6 to match tested devDeps - add collab-path tests: kit construction with url/params, changeRoom, plain mode never loads the kit, unmount-before-load never joins - add Collaborative story with an in-page loopback websocket relay (new WebSocketPolyfill option on CollabConfig) --- package.json | 6 +- pnpm-lock.yaml | 302 ++++++++---------- .../RichEditor/RichEditor.stories.tsx | 96 ++++++ src/components/RichEditor/RichEditor.test.tsx | 82 ++++- src/components/RichEditor/RichEditor.tsx | 86 ++--- src/components/RichEditor/collabKit.ts | 61 ++++ src/components/RichEditor/editorKits.ts | 70 ++-- src/components/RichEditor/index.ts | 6 +- 8 files changed, 444 insertions(+), 265 deletions(-) create mode 100644 src/components/RichEditor/collabKit.ts diff --git a/package.json b/package.json index b6f1758b6..3b6502162 100644 --- a/package.json +++ b/package.json @@ -232,10 +232,10 @@ "prepublishOnly": "npm run build" }, "peerDependencies": { - "@kerebron/editor": ">=0.7.9", - "@kerebron/editor-kits": ">=0.7.9", + "@kerebron/editor": ">=0.8.6", + "@kerebron/editor-kits": ">=0.8.6", "@kerebron/extension-yjs": ">=0.8.6", - "@kerebron/wasm": ">=0.7.9", + "@kerebron/wasm": ">=0.8.6", "@mieweb/datavis": "=1.6.0", "y-protocols": ">=1.0.6", "yjs": ">=13.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c5dca449..f876ba7f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -93,17 +93,20 @@ importers: specifier: ^9.39.3 version: 9.39.3 '@kerebron/editor': - specifier: 0.7.9 - version: 0.7.9 + specifier: ^0.8.6 + version: 0.8.9 '@kerebron/editor-kits': - specifier: 0.7.9 - version: 0.7.9(svelte@5.56.3(@typescript-eslint/types@8.56.1)) + specifier: ^0.8.6 + version: 0.8.9(svelte@5.56.3(@typescript-eslint/types@8.56.1)) + '@kerebron/extension-yjs': + specifier: ^0.8.6 + version: 0.8.9 '@kerebron/wasm': - specifier: 0.7.9 - version: 0.7.9 + specifier: ^0.8.6 + version: 0.8.9 '@mieweb/datavis': specifier: '=1.6.0' - version: 1.6.0(@kerebron/editor-kits@0.7.9(svelte@5.56.3(@typescript-eslint/types@8.56.1)))(@kerebron/editor@0.7.9)(@kerebron/wasm@0.7.9)(@types/react@19.2.14)(ag-grid-community@35.1.0)(ag-grid-react@35.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(datavis-ace@4.1.0)(js-yaml@4.1.1)(katex@0.17.0)(mermaid@11.15.0)(papaparse@5.5.3)(react-dom@19.2.4(react@19.2.4))(react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)(redux@5.0.1)(rehype-highlight@7.0.2)(rehype-katex@7.0.1)(rehype-sanitize@6.0.0)(remark-gfm@4.0.1)(remark-math@6.0.0)(typescript@5.9.3)(wavesurfer.js@7.12.1) + version: 1.6.0(@kerebron/editor-kits@0.8.9(svelte@5.56.3(@typescript-eslint/types@8.56.1)))(@kerebron/editor@0.8.9)(@kerebron/wasm@0.8.9)(@types/react@19.2.14)(ag-grid-community@35.1.0)(ag-grid-react@35.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(datavis-ace@4.1.0)(js-yaml@4.1.1)(katex@0.17.0)(mermaid@11.15.0)(papaparse@5.5.3)(react-dom@19.2.4(react@19.2.4))(react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)(redux@5.0.1)(rehype-highlight@7.0.2)(rehype-katex@7.0.1)(rehype-sanitize@6.0.0)(remark-gfm@4.0.1)(remark-math@6.0.0)(typescript@5.9.3)(wavesurfer.js@7.12.1) '@monaco-editor/react': specifier: ^4.7.0 version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -311,9 +314,15 @@ importers: wavesurfer.js: specifier: ^7.12.1 version: 7.12.1 + y-protocols: + specifier: ^1.0.6 + version: 1.0.6(yjs@13.6.30) ychart: specifier: file:./packages/ychart version: '@mieweb/ychart@file:packages/ychart(@popperjs/core@2.11.8)' + yjs: + specifier: ^13.6.30 + version: 13.6.30 zod: specifier: ^4.4.3 version: 4.4.3 @@ -974,53 +983,53 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@kerebron/editor-kits@0.7.9': - resolution: {integrity: sha512-O4HhtjzRZkM0fdQalWznGbvKtmT5dunXwUAPklpfQqOijktTJ2MwhSL1n5VuAY50Dum429d4QQ+hcjppPSRRWQ==} + '@kerebron/editor-kits@0.8.9': + resolution: {integrity: sha512-xCTRYeQkBjfkPSPvQc1fxV6rPwc0JwE/3x8/Q1Eb/wrHWtdcRollt0u0BMi7m0YvMZhP7b01FvZPMQAkZQ0Tcw==} - '@kerebron/editor@0.7.9': - resolution: {integrity: sha512-DqbKdAc1IJHlo52Z/IHKhYjLcPqlvQroYgPlwJjsPbhX+m0N6NoEtN/fSy1I4+ElhP8egledgZTiymiDXY04Pg==} + '@kerebron/editor@0.8.9': + resolution: {integrity: sha512-hQyrKAjyFW8RF7Khfe12LApSvLTfISyg4m0jI11SPSHXJgPe/nNQGD4KoBDkS7d2M+ng72X+FxHLNmVWZtAjEA==} - '@kerebron/extension-autocomplete@0.7.9': - resolution: {integrity: sha512-t7oclgz3cdtdf3RzDC+RPXcP5s/EuW/jEW/mAN2l6fjoGlUz3ALkS4VGBJPHvfM/fBsGvuSDyc3jHg5xgndeGg==} + '@kerebron/extension-basic-editor@0.8.9': + resolution: {integrity: sha512-pGBEJkdSWCSibtFARNRd9wKH6sSDanJvcSPDwlgj53yBmzKjCLurnK9YhVhVMD/MzJxC8id0RI8oP3U22Mc2wQ==} - '@kerebron/extension-basic-editor@0.7.9': - resolution: {integrity: sha512-HCS/JMgCyn6Kn/Oz5cxhgv3BjvgNRSW1b2161pRF+EL3jnDX3Ecp1HbFeTU/02a6V36PShO0paMr/Cz43lwdDQ==} + '@kerebron/extension-codecrock@0.8.9': + resolution: {integrity: sha512-l4AW621JzCCUcBd7S98gOiTSyaIbxEoH4Scv2yEyq2HBxaxO9kspZfnm65isnm1Q1mJEhEtYZ3xer48SHj02qw==} - '@kerebron/extension-codejar@0.7.9': - resolution: {integrity: sha512-fB3ChwHPQmSN6P838yJ9tx9ZSLBeHwOzcm4Qbb+9gJSC6sWJUVTjsQwdaRTtqdiT8vwvRkzhgLxrNdqGpB2kGQ==} + '@kerebron/extension-dev-toolkit@0.8.9': + resolution: {integrity: sha512-XRpnS7t/3NyYKFXl2jy9OYiWhbGAje7V1xPsnKYm03sLZNw9dvkvcyE1Pv3ZHUsB/zxhiNY3SstPOo4+cr3VOg==} - '@kerebron/extension-dev-toolkit@0.7.9': - resolution: {integrity: sha512-lh9+BIA3h6t8qYYuJsRQ0+MKh5bT9Gjpj8+DPqMp0GAX0Vk83FCGz+OcbIUijRFYyyxcaslbvVVbELHyIyDAlg==} + '@kerebron/extension-markdown@0.8.9': + resolution: {integrity: sha512-1/Wx9rXKA4nXTsLW51UaOLywFzUEQa+coNsRJjcqaogF6o0toZwKSl/eE7/2enSz2arojGWJqDcrmjAimxVAvg==} - '@kerebron/extension-lsp@0.7.9': - resolution: {integrity: sha512-BSisyPFXDOzc5bgoFe+zX5nDYD6cx+kUBflaAoqgIqwWbMh7t13QRlQLkGhUoPYj8IjXKgCt4uLAOgQSOikJ7A==} + '@kerebron/extension-menu-legacy@0.8.9': + resolution: {integrity: sha512-xEt3fjtXmh6zrRJvoukh1XWa8n7aoh1JrKXH2Js3ebWEsZoWo/vHCzNs6EfaIC/K2F6DOcrZVOcx5R7Z0zuCPg==} - '@kerebron/extension-markdown@0.7.9': - resolution: {integrity: sha512-DHw60zPStpHHAE4nK0KprSxYXDjEHX6Z9wU8ceE0iAN/SY2EapxgM1EafoAv2nDvxiOpDJd0M/nJVjwyP8LI8g==} + '@kerebron/extension-menu@0.8.9': + resolution: {integrity: sha512-FJNMRbv8dCQPYjJUrkSoPokshNvzO8QwFbZYQnT+ul3fXeupbQYFugBmK1Em8rWjlUNC22aDh3gHcRsApqEm7w==} - '@kerebron/extension-menu-legacy@0.7.9': - resolution: {integrity: sha512-5krTXnL6ILj5pL8QlDNMRtBV+KLonwAEKvbsRpuGnxvN018nc6gNCCIHomTCQpyEF4HoFkRC/+t0Z/K424AxgQ==} + '@kerebron/extension-odt@0.8.9': + resolution: {integrity: sha512-sjmHlMzrBwgSQufHUSbe/1NmDIAr6Z0xjQnTBV552ezPidI5sp78rneKKjY70DUzlBjPkYxB31cdSEKl1CsIOw==} - '@kerebron/extension-menu@0.7.9': - resolution: {integrity: sha512-GRmWK2546Ii0VkccWkHPA9U6dNMR7fsUWIZbO62H/wWj9apoM8iuFWu8/Res4NFYcmc2mYYHkSfMAejp/4Vn1w==} + '@kerebron/extension-tables@0.8.9': + resolution: {integrity: sha512-gU57VFLxADskuDUrZI/TVptvhUzVbBMku6NIBe8dFDn5+IZMHqNXDYZkxuVx1G7ff1Fzi6PqMeL3cJa2Vp0ayA==} - '@kerebron/extension-odt@0.7.9': - resolution: {integrity: sha512-zOKjO7i+pHeiAY31NdhH22f4rkOdA+5YYEIL8rtlIeGgblP6JSfJ5Cp/BwlNG9pp7NpTlTkXCXSc5z2T0en+Fw==} + '@kerebron/extension-ui@0.8.9': + resolution: {integrity: sha512-6hBIaQUb3OKBT1Ikywf6VjGriiVt5QlcdbCPf0a7SkAqWrPkXGS//v5Xq9XbbCnVAt40RIHVONyQLCjF9DWmIQ==} - '@kerebron/extension-tables@0.7.9': - resolution: {integrity: sha512-4z8DCUEAxf7rRZfv6lvC/cfzSXhRD2qDNT3kEuRrahf51ix3xLOmrFU/+bkiUkAqnxVLNKq6Pamtqb4htMMfvw==} + '@kerebron/extension-yjs@0.8.9': + resolution: {integrity: sha512-qnCEbudXaE8IQLXFKxv4R/1oUYupBRU64Vb1rc5Mu8ameOfviYt7dRlHwMh7VA2e2EX2kz11VZxGL6xGltpnQQ==} - '@kerebron/extension-yjs@0.7.9': - resolution: {integrity: sha512-3ddhPynQLiTyFs5NWvOwU9bMynmy472rDmEUqviTJNIrWwFkUlodFxqiDmQx+Nf+1uFwU/NmzFgul2L1wylBlQ==} + '@kerebron/odt-wasm@0.8.9': + resolution: {integrity: sha512-S5Krh+W4cmENneQ78MlkfRtYRi4RElJzjEuFKBu4ki0r6mWH4JU0KLO7hiPPdauHGTJb4HaEDKNG3NcBwqtZGw==} - '@kerebron/odt-wasm@0.7.9': - resolution: {integrity: sha512-gXFguetp3rRQCm//Axzk/kvLfJBkDJVxZvaol6cztU52PUuOXe1Zfi2o1nylLYTe3JIg1mg5j57t0uj76aomCg==} + '@kerebron/tree-sitter@0.8.9': + resolution: {integrity: sha512-ECeKvf+mIn1wq5JYrPovKqRUfppRXlM7uveKF/PXZbyR/lZRhIQP42yallcFIV5z+jIBmydcsZo/sqddaJOsVw==} - '@kerebron/tree-sitter@0.7.9': - resolution: {integrity: sha512-sAR0m6i4vjfXglJE73ER7XDQ7uhRDydyi5Di7A7qs6V4yP+fOzLJHYlE/KZ4IGeQlwh/ee9taSfEFztpaa9X0Q==} + '@kerebron/wasm@0.8.9': + resolution: {integrity: sha512-XzcDL9mHeD8oKla6G2+SDbZHv3qK76oF3jNgUe3czFG6J24MqWB5zfv1ty19ZA1MirxTxpfUdjnmTiMH6ToPew==} - '@kerebron/wasm@0.7.9': - resolution: {integrity: sha512-FjZTWTwxQ1TKDs+FHqP0g/KGLa5TE/+zLzBDC+CSsMPWYhVANzT9Fs0i/4eDdLCeXX3DtSpWpuPxEKUzH/7nmA==} + '@kerebron/workspace@0.8.9': + resolution: {integrity: sha512-BPa9pmxZcSg0bqPY1oeG2SBxN8OV97rrHqJPhV7+1gMjvlDtdlDFui7ZwocLwVaVQw7KU5YgdQDkRSa2AFeSuQ==} '@lezer/common@1.5.2': resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} @@ -4913,15 +4922,12 @@ packages: prosemirror-dev-toolkit@1.1.8: resolution: {integrity: sha512-Qi549XA+DqU5cCkn/xv+M53gy1sQbyOTjiOfiG7Gjq5gm6ZxLilGN04UITWTAYx9kzLBi7Y9RJmvmWB4xiRauA==} - prosemirror-history@1.4.1: - resolution: {integrity: sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==} - - prosemirror-model@1.25.3: - resolution: {integrity: sha512-dY2HdaNXlARknJbrManZ1WyUtos+AP97AmvqdOQtWtrrC5g4mohVX5DTi9rXNFSk09eczLq9GuNTtq3EfMeMGA==} - prosemirror-model@1.25.8: resolution: {integrity: sha512-BswA4BLSFEiORV6Vjj/yZBXDbos1zTEnhyeSSgT8psGFhstQS7UJ8/WOLiDos9Byaee27+tml0/DuMNxYR84zg==} + prosemirror-model@1.25.9: + resolution: {integrity: sha512-pRTklkDDMMRopyoAcrr9wV/8g/RYgrLHBuJAb5hlEuYZRdm5yqmPjWId83fpBwPpSFqEdja0H7Dfd7z1X/npcA==} + prosemirror-state@1.4.3: resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==} @@ -5131,9 +5137,6 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rope-sequence@1.3.4: - resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} - roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} @@ -5769,16 +5772,6 @@ packages: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} - vscode-jsonrpc@8.2.0: - resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} - engines: {node: '>=14.0.0'} - - vscode-languageserver-protocol@3.17.5: - resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} - - vscode-languageserver-types@3.17.5: - resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} - w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} @@ -5805,8 +5798,8 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} - web-tree-sitter@0.26.6: - resolution: {integrity: sha512-fSPR7VBW/fZQdUSp/bXTDLT+i/9dwtbnqgEBMzowrM4U3DzeCwDbY3MKo0584uQxID4m/1xpLflrlT/rLIRPew==} + web-tree-sitter@0.26.11: + resolution: {integrity: sha512-Q5Dm3YTIXSXuH6FxX6RuzX2Qwpc4DPGiYMU87Wg5Z8OIStiQFiUex4zMDc0vBTw78EphaYJacncJghCHzbZptg==} webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} @@ -6786,138 +6779,125 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@kerebron/editor-kits@0.7.9(svelte@5.56.3(@typescript-eslint/types@8.56.1))': - dependencies: - '@kerebron/editor': 0.7.9 - '@kerebron/extension-autocomplete': 0.7.9 - '@kerebron/extension-basic-editor': 0.7.9 - '@kerebron/extension-codejar': 0.7.9 - '@kerebron/extension-dev-toolkit': 0.7.9(svelte@5.56.3(@typescript-eslint/types@8.56.1)) - '@kerebron/extension-lsp': 0.7.9 - '@kerebron/extension-markdown': 0.7.9 - '@kerebron/extension-menu': 0.7.9 - '@kerebron/extension-menu-legacy': 0.7.9 - '@kerebron/extension-odt': 0.7.9 - '@kerebron/extension-tables': 0.7.9 - '@kerebron/extension-yjs': 0.7.9 - yjs: 13.6.30 + '@kerebron/editor-kits@0.8.9(svelte@5.56.3(@typescript-eslint/types@8.56.1))': + dependencies: + '@kerebron/editor': 0.8.9 + '@kerebron/extension-basic-editor': 0.8.9 + '@kerebron/extension-codecrock': 0.8.9 + '@kerebron/extension-dev-toolkit': 0.8.9(svelte@5.56.3(@typescript-eslint/types@8.56.1)) + '@kerebron/extension-markdown': 0.8.9 + '@kerebron/extension-menu': 0.8.9 + '@kerebron/extension-menu-legacy': 0.8.9 + '@kerebron/extension-odt': 0.8.9 + '@kerebron/extension-tables': 0.8.9 + '@kerebron/extension-ui': 0.8.9 transitivePeerDependencies: - svelte - '@kerebron/editor@0.7.9': + '@kerebron/editor@0.8.9': dependencies: - prosemirror-model: 1.25.3 + '@kerebron/workspace': 0.8.9 + prosemirror-model: 1.25.9 prosemirror-state: 1.4.3 prosemirror-transform: 1.10.4 prosemirror-view: 1.40.0 - '@kerebron/extension-autocomplete@0.7.9': + '@kerebron/extension-basic-editor@0.8.9': dependencies: - '@kerebron/editor': 0.7.9 - prosemirror-model: 1.25.3 - prosemirror-state: 1.4.3 - prosemirror-view: 1.40.0 - - '@kerebron/extension-basic-editor@0.7.9': - dependencies: - '@kerebron/editor': 0.7.9 - prosemirror-history: 1.4.1 - prosemirror-model: 1.25.3 + '@kerebron/editor': 0.8.9 + prosemirror-model: 1.25.9 prosemirror-state: 1.4.3 prosemirror-transform: 1.10.4 prosemirror-view: 1.40.0 - '@kerebron/extension-codejar@0.7.9': + '@kerebron/extension-codecrock@0.8.9': dependencies: - '@kerebron/editor': 0.7.9 - '@kerebron/extension-basic-editor': 0.7.9 - '@kerebron/extension-lsp': 0.7.9 - '@kerebron/extension-markdown': 0.7.9 - '@kerebron/tree-sitter': 0.7.9 - '@kerebron/wasm': 0.7.9 - prosemirror-model: 1.25.3 + '@kerebron/editor': 0.8.9 + '@kerebron/extension-basic-editor': 0.8.9 + '@kerebron/tree-sitter': 0.8.9 + '@kerebron/wasm': 0.8.9 + '@kerebron/workspace': 0.8.9 + prosemirror-model: 1.25.9 prosemirror-state: 1.4.3 prosemirror-view: 1.40.0 - vscode-languageserver-protocol: 3.17.5 - '@kerebron/extension-dev-toolkit@0.7.9(svelte@5.56.3(@typescript-eslint/types@8.56.1))': + '@kerebron/extension-dev-toolkit@0.8.9(svelte@5.56.3(@typescript-eslint/types@8.56.1))': dependencies: - '@kerebron/editor': 0.7.9 + '@kerebron/editor': 0.8.9 prosemirror-dev-toolkit: 1.1.8(svelte@5.56.3(@typescript-eslint/types@8.56.1)) prosemirror-view: 1.40.0 transitivePeerDependencies: - svelte - '@kerebron/extension-lsp@0.7.9': + '@kerebron/extension-markdown@0.8.9': dependencies: - '@kerebron/editor': 0.7.9 - '@kerebron/extension-autocomplete': 0.7.9 - '@kerebron/extension-markdown': 0.7.9 - prosemirror-state: 1.4.3 - prosemirror-view: 1.40.0 - vscode-languageserver-protocol: 3.17.5 - - '@kerebron/extension-markdown@0.7.9': - dependencies: - '@kerebron/editor': 0.7.9 - '@kerebron/extension-basic-editor': 0.7.9 - '@kerebron/tree-sitter': 0.7.9 - '@kerebron/wasm': 0.7.9 + '@kerebron/editor': 0.8.9 + '@kerebron/extension-basic-editor': 0.8.9 + '@kerebron/tree-sitter': 0.8.9 + '@kerebron/wasm': 0.8.9 + '@kerebron/workspace': 0.8.9 mathml2latex: 1.1.3 - prosemirror-model: 1.25.3 + prosemirror-model: 1.25.9 prosemirror-state: 1.4.3 - web-tree-sitter: 0.26.6 - '@kerebron/extension-menu-legacy@0.7.9': + '@kerebron/extension-menu-legacy@0.8.9': dependencies: - '@kerebron/editor': 0.7.9 - prosemirror-model: 1.25.3 + '@kerebron/editor': 0.8.9 + prosemirror-model: 1.25.9 prosemirror-state: 1.4.3 prosemirror-view: 1.40.0 - '@kerebron/extension-menu@0.7.9': + '@kerebron/extension-menu@0.8.9': dependencies: - '@kerebron/editor': 0.7.9 - prosemirror-model: 1.25.3 + '@kerebron/editor': 0.8.9 + prosemirror-model: 1.25.9 prosemirror-state: 1.4.3 prosemirror-view: 1.40.0 - '@kerebron/extension-odt@0.7.9': + '@kerebron/extension-odt@0.8.9': dependencies: - '@kerebron/editor': 0.7.9 - '@kerebron/odt-wasm': 0.7.9 - prosemirror-model: 1.25.3 + '@kerebron/editor': 0.8.9 + '@kerebron/odt-wasm': 0.8.9 + prosemirror-model: 1.25.9 prosemirror-state: 1.4.3 - '@kerebron/extension-tables@0.7.9': + '@kerebron/extension-tables@0.8.9': dependencies: - '@kerebron/editor': 0.7.9 - prosemirror-model: 1.25.3 + '@kerebron/editor': 0.8.9 + prosemirror-model: 1.25.9 prosemirror-state: 1.4.3 prosemirror-transform: 1.10.4 prosemirror-view: 1.40.0 - '@kerebron/extension-yjs@0.7.9': + '@kerebron/extension-ui@0.8.9': + dependencies: + '@kerebron/editor': 0.8.9 + prosemirror-model: 1.25.9 + prosemirror-state: 1.4.3 + prosemirror-view: 1.40.0 + + '@kerebron/extension-yjs@0.8.9': dependencies: - '@kerebron/editor': 0.7.9 - '@kerebron/extension-basic-editor': 0.7.9 + '@kerebron/editor': 0.8.9 + '@kerebron/extension-basic-editor': 0.8.9 lib0: 0.2.109 - prosemirror-model: 1.25.3 + prosemirror-model: 1.25.9 prosemirror-state: 1.4.3 prosemirror-view: 1.40.0 y-protocols: 1.0.6(yjs@13.6.30) yjs: 13.6.30 - '@kerebron/odt-wasm@0.7.9': {} + '@kerebron/odt-wasm@0.8.9': {} - '@kerebron/tree-sitter@0.7.9': + '@kerebron/tree-sitter@0.8.9': dependencies: - '@kerebron/editor': 0.7.9 - web-tree-sitter: 0.26.6 + web-tree-sitter: 0.26.11 - '@kerebron/wasm@0.7.9': + '@kerebron/wasm@0.8.9': dependencies: - '@kerebron/editor': 0.7.9 + '@kerebron/editor': 0.8.9 + + '@kerebron/workspace@0.8.9': {} '@lezer/common@1.5.2': {} @@ -6947,12 +6927,12 @@ snapshots: dependencies: '@chevrotain/types': 11.1.2 - '@mieweb/datavis@1.6.0(@kerebron/editor-kits@0.7.9(svelte@5.56.3(@typescript-eslint/types@8.56.1)))(@kerebron/editor@0.7.9)(@kerebron/wasm@0.7.9)(@types/react@19.2.14)(ag-grid-community@35.1.0)(ag-grid-react@35.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(datavis-ace@4.1.0)(js-yaml@4.1.1)(katex@0.17.0)(mermaid@11.15.0)(papaparse@5.5.3)(react-dom@19.2.4(react@19.2.4))(react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)(redux@5.0.1)(rehype-highlight@7.0.2)(rehype-katex@7.0.1)(rehype-sanitize@6.0.0)(remark-gfm@4.0.1)(remark-math@6.0.0)(typescript@5.9.3)(wavesurfer.js@7.12.1)': + '@mieweb/datavis@1.6.0(@kerebron/editor-kits@0.8.9(svelte@5.56.3(@typescript-eslint/types@8.56.1)))(@kerebron/editor@0.8.9)(@kerebron/wasm@0.8.9)(@types/react@19.2.14)(ag-grid-community@35.1.0)(ag-grid-react@35.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(datavis-ace@4.1.0)(js-yaml@4.1.1)(katex@0.17.0)(mermaid@11.15.0)(papaparse@5.5.3)(react-dom@19.2.4(react@19.2.4))(react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)(redux@5.0.1)(rehype-highlight@7.0.2)(rehype-katex@7.0.1)(rehype-sanitize@6.0.0)(remark-gfm@4.0.1)(remark-math@6.0.0)(typescript@5.9.3)(wavesurfer.js@7.12.1)': dependencies: '@dnd-kit/core': 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@dnd-kit/sortable': 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) '@dnd-kit/utilities': 3.2.2(react@19.2.4) - '@mieweb/ui': 0.6.1-dev.169(c4a216e8f81f6aba5f7c3be536e2255b) + '@mieweb/ui': 0.6.1-dev.169(6157a0539ef5bc1faafe665ee9520b8d) datavis-ace: 4.1.0 i18next: 26.3.4(typescript@5.9.3) lucide-react: 1.23.0(react@19.2.4) @@ -7000,7 +6980,7 @@ snapshots: datavis-ace: 4.1.0 wavesurfer.js: 7.12.1 - '@mieweb/ui@0.6.1-dev.169(c4a216e8f81f6aba5f7c3be536e2255b)': + '@mieweb/ui@0.6.1-dev.169(6157a0539ef5bc1faafe665ee9520b8d)': dependencies: '@swc/helpers': 0.5.19 '@tanstack/react-virtual': 3.14.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -7017,10 +6997,10 @@ snapshots: react-dom: 19.2.4(react@19.2.4) tailwind-merge: 2.6.1 optionalDependencies: - '@kerebron/editor': 0.7.9 - '@kerebron/editor-kits': 0.7.9(svelte@5.56.3(@typescript-eslint/types@8.56.1)) - '@kerebron/wasm': 0.7.9 - '@mieweb/datavis': 1.6.0(@kerebron/editor-kits@0.7.9(svelte@5.56.3(@typescript-eslint/types@8.56.1)))(@kerebron/editor@0.7.9)(@kerebron/wasm@0.7.9)(@types/react@19.2.14)(ag-grid-community@35.1.0)(ag-grid-react@35.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(datavis-ace@4.1.0)(js-yaml@4.1.1)(katex@0.17.0)(mermaid@11.15.0)(papaparse@5.5.3)(react-dom@19.2.4(react@19.2.4))(react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)(redux@5.0.1)(rehype-highlight@7.0.2)(rehype-katex@7.0.1)(rehype-sanitize@6.0.0)(remark-gfm@4.0.1)(remark-math@6.0.0)(typescript@5.9.3)(wavesurfer.js@7.12.1) + '@kerebron/editor': 0.8.9 + '@kerebron/editor-kits': 0.8.9(svelte@5.56.3(@typescript-eslint/types@8.56.1)) + '@kerebron/wasm': 0.8.9 + '@mieweb/datavis': 1.6.0(@kerebron/editor-kits@0.8.9(svelte@5.56.3(@typescript-eslint/types@8.56.1)))(@kerebron/editor@0.8.9)(@kerebron/wasm@0.8.9)(@types/react@19.2.14)(ag-grid-community@35.1.0)(ag-grid-react@35.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(datavis-ace@4.1.0)(js-yaml@4.1.1)(katex@0.17.0)(mermaid@11.15.0)(papaparse@5.5.3)(react-dom@19.2.4(react@19.2.4))(react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)(redux@5.0.1)(rehype-highlight@7.0.2)(rehype-katex@7.0.1)(rehype-sanitize@6.0.0)(remark-gfm@4.0.1)(remark-math@6.0.0)(typescript@5.9.3)(wavesurfer.js@7.12.1) ag-grid-community: 35.1.0 ag-grid-react: 35.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) datavis-ace: 4.1.0 @@ -11385,34 +11365,27 @@ snapshots: transitivePeerDependencies: - svelte - prosemirror-history@1.4.1: - dependencies: - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 - prosemirror-view: 1.40.0 - rope-sequence: 1.3.4 - - prosemirror-model@1.25.3: + prosemirror-model@1.25.8: dependencies: orderedmap: 2.1.1 - prosemirror-model@1.25.8: + prosemirror-model@1.25.9: dependencies: orderedmap: 2.1.1 prosemirror-state@1.4.3: dependencies: - prosemirror-model: 1.25.3 + prosemirror-model: 1.25.9 prosemirror-transform: 1.10.4 prosemirror-view: 1.40.0 prosemirror-transform@1.10.4: dependencies: - prosemirror-model: 1.25.3 + prosemirror-model: 1.25.9 prosemirror-view@1.40.0: dependencies: - prosemirror-model: 1.25.3 + prosemirror-model: 1.25.9 prosemirror-state: 1.4.3 prosemirror-transform: 1.10.4 @@ -11726,8 +11699,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 - rope-sequence@1.3.4: {} - roughjs@4.6.6: dependencies: hachure-fill: 0.5.2 @@ -12499,15 +12470,6 @@ snapshots: void-elements@3.1.0: {} - vscode-jsonrpc@8.2.0: {} - - vscode-languageserver-protocol@3.17.5: - dependencies: - vscode-jsonrpc: 8.2.0 - vscode-languageserver-types: 3.17.5 - - vscode-languageserver-types@3.17.5: {} - w3c-keyname@2.2.8: {} w3c-xmlserializer@5.0.0: @@ -12540,7 +12502,7 @@ snapshots: web-namespaces@2.0.1: {} - web-tree-sitter@0.26.6: {} + web-tree-sitter@0.26.11: {} webidl-conversions@7.0.0: {} @@ -12663,7 +12625,7 @@ snapshots: y-protocols@1.0.6(yjs@13.6.30): dependencies: - lib0: 0.2.109 + lib0: 0.2.117 yjs: 13.6.30 y18n@4.0.3: {} diff --git a/src/components/RichEditor/RichEditor.stories.tsx b/src/components/RichEditor/RichEditor.stories.tsx index 14b0ad64c..4e1874120 100644 --- a/src/components/RichEditor/RichEditor.stories.tsx +++ b/src/components/RichEditor/RichEditor.stories.tsx @@ -40,3 +40,99 @@ function CodeExample() { export const Code: Story = { render: () => , }; + +function CollabExample() { + // Unique room per mount so story remounts (and other open tabs) start fresh. + const [room] = useState( + () => `storybook-collab-${Math.random().toString(36).slice(2)}` + ); + // In-page loopback relay (no server needed). In production, omit + // `WebSocketPolyfill` and point `wsUrl` at the real `/yjs` relay, + // optionally authenticated via `params`. + const collab = { + room, + wsUrl: 'ws://loopback.invalid/yjs', + WebSocketPolyfill: + LoopbackWebSocket as unknown as typeof globalThis.WebSocket, + }; + return ( +
+
+
Peer A
+ +
+
+
Peer B
+ +
+
+ ); +} + +/** + * Two editors joined to the same Yjs room. Type in either — edits appear in + * both. This demo swaps the websocket for an in-page loopback relay so it + * works without a server; in production every peer connects to the `/yjs` + * websocket relay, optionally authenticated via `collab.params`. + */ +export const Collaborative: Story = { + render: () => , +}; + +/** + * Demo-only stand-in for the `/yjs` relay: a fake `WebSocket` that relays + * every frame to all sockets on the same URL (including the sender — the + * y-sync protocol needs an answer to its sync-step-1 even when you're alone + * in the room) and replays history to late joiners. Yjs updates are + * idempotent, so the duplicate delivery is harmless. + */ +class LoopbackWebSocket { + static rooms = new Map< + string, + { sockets: Set; history: ArrayBuffer[] } + >(); + + binaryType = 'arraybuffer'; + readyState = 0; // CONNECTING + onopen: (() => void) | null = null; + onmessage: ((event: { data: ArrayBuffer }) => void) | null = null; + onclose: ((event: { code: number }) => void) | null = null; + onerror: ((event: unknown) => void) | null = null; + + private room: { sockets: Set; history: ArrayBuffer[] }; + + constructor(url: string) { + let room = LoopbackWebSocket.rooms.get(url); + if (!room) { + room = { sockets: new Set(), history: [] }; + LoopbackWebSocket.rooms.set(url, room); + } + this.room = room; + room.sockets.add(this); + setTimeout(() => { + if (this.readyState !== 0) return; + this.readyState = 1; // OPEN + this.onopen?.(); + // Replay the room's history so late joiners catch up. + for (const frame of this.room.history) { + this.onmessage?.({ data: frame }); + } + }, 0); + } + + send(data: Uint8Array) { + const frame = data.slice().buffer as ArrayBuffer; + this.room.history.push(frame); + for (const socket of this.room.sockets) { + if (socket.readyState === 1) { + setTimeout(() => socket.onmessage?.({ data: frame }), 0); + } + } + } + + close() { + this.readyState = 3; // CLOSED + this.room.sockets.delete(this); + this.onclose?.({ code: 1000 }); + } +} diff --git a/src/components/RichEditor/RichEditor.test.tsx b/src/components/RichEditor/RichEditor.test.tsx index 25341dfba..e00d989e7 100644 --- a/src/components/RichEditor/RichEditor.test.tsx +++ b/src/components/RichEditor/RichEditor.test.tsx @@ -1,4 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { waitFor } from '@testing-library/react'; import { renderWithTheme } from '../../test/test-utils'; import { RichEditor } from './RichEditor'; import { CodeEditor } from './CodeEditor'; @@ -6,6 +7,7 @@ import { CodeEditor } from './CodeEditor'; // The Kerebron editor loads tree-sitter WASM grammars at runtime, which isn't // available under jsdom. Mock the editor so these stay fast, deterministic // smoke tests that just verify the wrappers mount/unmount without throwing. +const changeRoom = vi.fn(); const editorMock = { addEventListener: vi.fn(), removeEventListener: vi.fn(), @@ -14,13 +16,15 @@ const editorMock = { saveDocument: vi .fn() .mockResolvedValue(new globalThis.TextEncoder().encode('# hello')), + run: { changeRoom }, }; +const coreEditorCreate = vi.fn((_opts: unknown) => editorMock); vi.mock('@kerebron/editor', () => ({ - CoreEditor: { create: vi.fn(() => editorMock) }, + CoreEditor: { create: (opts: unknown) => coreEditorCreate(opts) }, })); vi.mock('@kerebron/editor-kits/AdvancedEditorKit', () => ({ - AdvancedEditorKit: vi.fn(), + AdvancedEditorKit: vi.fn(() => ({ getExtensions: () => [] })), })); vi.mock('@kerebron/editor-kits/CodeEditorKit', () => ({ CodeEditorKit: vi.fn(), @@ -28,20 +32,90 @@ vi.mock('@kerebron/editor-kits/CodeEditorKit', () => ({ vi.mock('@kerebron/wasm/web', () => ({ createAssetLoad: vi.fn(() => vi.fn()), })); +// The collab kit is lazy-imported by editorKits.ts only when `collab` is set; +// mock it so the yjs optional peers aren't needed under jsdom. +const huddleYjsKit = vi.fn(function ( + this: Record, + url: string, + params: Record +) { + this.name = 'yjs-editor'; + this.url = url; + this.params = params; + this.getExtensions = () => []; +}); +vi.mock('./collabKit', () => ({ + HuddleYjsKit: huddleYjsKit, + defaultWsUrl: () => 'ws://localhost/yjs', +})); describe('RichEditor', () => { beforeEach(() => { vi.clearAllMocks(); }); - it('renders without throwing', () => { + it('renders without throwing', async () => { const { container } = renderWithTheme(); expect(container.querySelector('.kb-component')).not.toBeNull(); + await waitFor(() => expect(coreEditorCreate).toHaveBeenCalled()); }); - it('destroys the editor on unmount', () => { + it('destroys the editor on unmount', async () => { const { unmount } = renderWithTheme(); + await waitFor(() => expect(coreEditorCreate).toHaveBeenCalled()); + unmount(); + expect(editorMock.destroy).toHaveBeenCalled(); + }); + + it('plain mode does not load the yjs collab kit', async () => { + renderWithTheme(); + await waitFor(() => expect(coreEditorCreate).toHaveBeenCalled()); + expect(huddleYjsKit).not.toHaveBeenCalled(); + expect(changeRoom).not.toHaveBeenCalled(); + }); + + it('collab mode includes the yjs kit and joins the room', async () => { + renderWithTheme( + + ); + await waitFor(() => expect(changeRoom).toHaveBeenCalledWith('room-1')); + // The yjs kit was constructed with the caller's url + auth params … + expect(huddleYjsKit).toHaveBeenCalledWith( + 'ws://example.test/yjs', + { token: 't' }, + undefined // no WebSocketPolyfill override + ); + // … and handed to the editor. + const kits = ( + coreEditorCreate.mock.calls[0][0] as { + editorKits: { name: string }[]; + } + ).editorKits; + expect(kits.map((k) => k.name)).toEqual(['advanced-editor', 'yjs-editor']); + }); + + it('does not join the room when unmounted before content loads', async () => { + let resolveLoad!: () => void; + editorMock.loadDocumentText.mockReturnValueOnce( + new Promise((resolve) => { + resolveLoad = resolve; + }) + ); + const { unmount } = renderWithTheme( + + ); + await waitFor(() => expect(editorMock.loadDocumentText).toHaveBeenCalled()); unmount(); + resolveLoad(); + await Promise.resolve(); // flush the continuation + expect(changeRoom).not.toHaveBeenCalled(); expect(editorMock.destroy).toHaveBeenCalled(); }); }); diff --git a/src/components/RichEditor/RichEditor.tsx b/src/components/RichEditor/RichEditor.tsx index 87011e77e..9ed09f2f5 100644 --- a/src/components/RichEditor/RichEditor.tsx +++ b/src/components/RichEditor/RichEditor.tsx @@ -38,18 +38,12 @@ const RichEditor: React.FC = ({ useEffect(() => { if (!editorRef.current) return; + const element = editorRef.current; - // Initialize the editor. Both modes drop the teardown-unsafe extensions; - // collaborative mode additionally swaps `history` for the Yjs CRDT sync. - // See `editorKits.ts` for why. - const editor = CoreEditor.create({ - element: editorRef.current, - uri: 'file:///untitled.md', - assetLoad: createAssetLoad('/kerebron-wasm'), - editorKits: createEditorKits(collab), - }); - - editorInstance.current = editor; + // Set on cleanup so async continuations (kit loading and the + // `loadDocumentText` chain) don't touch the editor after `destroy()`. + let disposed = false; + let editor: CoreEditor | null = null; // Listen to transactions and update markdown preview const onTransaction = async () => { @@ -66,38 +60,56 @@ const RichEditor: React.FC = ({ } }; - editor.addEventListener('transaction', onTransaction); - - // Seed initial content, then populate the preview once on mount. - // - // In collaborative mode we still load the local markdown first, then join - // the room: the Yjs binding seeds an *empty* shared document from this - // content on the first join, and overwrites the editor with the shared - // content when the room already has edits — so the stored markdown is the - // starting point without ever double-inserting. - const joinRoom = () => { - if (collab) { - (editor.run as Record boolean>).changeRoom?.( - collab.room, - ); + // Initialize the editor. Both modes drop the teardown-unsafe extensions; + // collaborative mode additionally swaps `history` for the Yjs CRDT sync + // and lazy-loads the Yjs kit (see `editorKits.ts` for why). + const setup = async () => { + const editorKits = await createEditorKits(collab); + if (disposed) return; + + editor = CoreEditor.create({ + element, + uri: 'file:///untitled.md', + assetLoad: createAssetLoad('/kerebron-wasm'), + editorKits, + }); + + editorInstance.current = editor; + editor.addEventListener('transaction', onTransaction); + + // Seed initial content, then populate the preview once on mount. + // + // In collaborative mode we still load the local markdown first, then join + // the room: the Yjs binding seeds an *empty* shared document from this + // content on the first join, and overwrites the editor with the shared + // content when the room already has edits — so the stored markdown is the + // starting point without ever double-inserting. + const joinRoom = () => { + if (collab && editor && !disposed) { + ( + editor.run as Record boolean> + ).changeRoom?.(collab.room); + } + }; + + if (value) { + await editor.loadDocumentText('text/x-markdown', value); + await onTransaction(); + joinRoom(); + } else { + void onTransaction(); + joinRoom(); } }; - if (value) { - editor - .loadDocumentText('text/x-markdown', value) - .then(() => onTransaction()) - .then(joinRoom) - .catch((err) => console.error('Failed to load markdown:', err)); - } else { - void onTransaction(); - joinRoom(); - } + setup().catch((err) => console.error('Failed to set up editor:', err)); // Cleanup on unmount return () => { - editor.removeEventListener('transaction', onTransaction); - editor.destroy(); + disposed = true; + editorInstance.current = null; // makes onTransaction's guard effective + editor?.removeEventListener('transaction', onTransaction); + editor?.destroy(); }; // Initial `value`/`collab` are intentionally only applied on mount // (uncontrolled). Remount via `key` to switch rooms. diff --git a/src/components/RichEditor/collabKit.ts b/src/components/RichEditor/collabKit.ts new file mode 100644 index 000000000..e17f2023b --- /dev/null +++ b/src/components/RichEditor/collabKit.ts @@ -0,0 +1,61 @@ +/** + * Collaborative (Yjs) editor kit for {@link RichEditor}. + * + * Kept in its own module — loaded via dynamic `import()` from `editorKits.ts` + * only when a `collab` config is passed — so `@kerebron/extension-yjs`, `yjs` + * and `y-protocols` stay truly optional peers: plain-mode consumers never load + * them and don't need them installed. + * + * We deliberately do not use `@kerebron/editor-kits`' own `YjsEditorKit`: it + * constructs the `WebsocketProvider` with no query params, leaving no way to + * authenticate the socket. Here we thread caller-supplied `params` (e.g. an + * auth token) into the provider so the backend `/yjs` route can authorize the + * room. + */ +import type { EditorKit } from '@kerebron/editor'; +import { ExtensionYjs } from '@kerebron/extension-yjs'; +import { WebsocketProvider } from '@kerebron/extension-yjs/WebsocketProvider'; +import { MarkYChange } from '@kerebron/extension-yjs/MarkYChange'; +import * as awarenessProtocol from 'y-protocols/awareness'; +import * as Y from 'yjs'; + +/** Derive the default `/yjs` websocket URL from the current page origin. */ +export function defaultWsUrl(): string { + const loc = globalThis.location; + const protocol = loc && loc.protocol === 'https:' ? 'wss:' : 'ws:'; + const host = loc ? loc.host : 'localhost'; + return `${protocol}//${host}/yjs`; +} + +/** MarkYChange + ExtensionYjs, with an authenticated websocket provider. */ +export class HuddleYjsKit implements EditorKit { + name = 'yjs-editor'; + constructor( + private readonly url: string, + private readonly params: Record, + private readonly WebSocketImpl?: typeof globalThis.WebSocket + ) {} + + getExtensions() { + const url = this.url; + const params = this.params; + const WebSocketImpl = this.WebSocketImpl ?? globalThis.WebSocket; + const createYjsProvider = (roomId: string): [WebsocketProvider, Y.Doc] => { + const ydoc = new Y.Doc({ gc: false }); + // The provider's opts default is a *default parameter*, not a merge, so we + // must pass every field when we want to set `params`. + const provider = new WebsocketProvider(url, roomId, ydoc, { + connect: true, + awareness: new awarenessProtocol.Awareness(ydoc), + params, + protocols: [], + WebSocketPolyfill: WebSocketImpl, + resyncInterval: -1, + maxBackoffTime: 2500, + disableBc: false, + }); + return [provider, ydoc]; + }; + return [new MarkYChange(), new ExtensionYjs({ createYjsProvider })]; + } +} diff --git a/src/components/RichEditor/editorKits.ts b/src/components/RichEditor/editorKits.ts index 3ea345a2d..b8a2dd960 100644 --- a/src/components/RichEditor/editorKits.ts +++ b/src/components/RichEditor/editorKits.ts @@ -6,18 +6,12 @@ * are unsafe for our usage — see {@link unsafeExtensions} — then collaborative * mode adds the Yjs pieces on top. * - * We deliberately do not use `@kerebron/editor-kits`' own `YjsEditorKit`: it - * constructs the `WebsocketProvider` with no query params, leaving no way to - * authenticate the socket. Here we thread caller-supplied `params` (e.g. an auth - * token) into the provider so the backend `/yjs` route can authorize the room. + * The Yjs pieces live in `collabKit.ts` behind a dynamic `import()`, so + * `@kerebron/extension-yjs`, `yjs` and `y-protocols` remain truly optional + * peers — plain-mode consumers never load them. */ import type { EditorKit } from '@kerebron/editor'; import { AdvancedEditorKit } from '@kerebron/editor-kits/AdvancedEditorKit'; -import { ExtensionYjs } from '@kerebron/extension-yjs'; -import { WebsocketProvider } from '@kerebron/extension-yjs/WebsocketProvider'; -import { MarkYChange } from '@kerebron/extension-yjs/MarkYChange'; -import * as awarenessProtocol from 'y-protocols/awareness'; -import * as Y from 'yjs'; export interface CollabConfig { /** Room id — one shared document per room (e.g. a post id). */ @@ -29,14 +23,12 @@ export interface CollabConfig { wsUrl?: string; /** Extra query params for the socket (e.g. `{ token }` for auth). */ params?: Record; -} - -/** Derive the default `/yjs` websocket URL from the current page origin. */ -function defaultWsUrl(): string { - const loc = globalThis.location; - const protocol = loc && loc.protocol === 'https:' ? 'wss:' : 'ws:'; - const host = loc ? loc.host : 'localhost'; - return `${protocol}//${host}/yjs`; + /** + * Custom WebSocket implementation handed to the Yjs provider — e.g. a + * loopback socket for demos/tests, or a polyfill outside the browser. + * Defaults to `globalThis.WebSocket`. + */ + WebSocketPolyfill?: typeof globalThis.WebSocket; } /** @@ -74,52 +66,30 @@ class SafeAdvancedEditorKit implements EditorKit { return new AdvancedEditorKit() .getExtensions() .filter( - (extension) => !('name' in extension && dropped.includes(extension.name)), + (extension) => + !('name' in extension && dropped.includes(extension.name)) ); } } -/** MarkYChange + ExtensionYjs, with an authenticated websocket provider. */ -class HuddleYjsKit implements EditorKit { - name = 'yjs-editor'; - constructor( - private readonly url: string, - private readonly params: Record, - ) {} - - getExtensions() { - const url = this.url; - const params = this.params; - const createYjsProvider = (roomId: string): [WebsocketProvider, Y.Doc] => { - const ydoc = new Y.Doc({ gc: false }); - // The provider's opts default is a *default parameter*, not a merge, so we - // must pass every field when we want to set `params`. - const provider = new WebsocketProvider(url, roomId, ydoc, { - connect: true, - awareness: new awarenessProtocol.Awareness(ydoc), - params, - protocols: [], - WebSocketPolyfill: WebSocket, - resyncInterval: -1, - maxBackoffTime: 2500, - disableBc: false, - }); - return [provider, ydoc]; - }; - return [new MarkYChange(), new ExtensionYjs({ createYjsProvider })]; - } -} +/** MarkYChange + ExtensionYjs live in `collabKit.ts` (lazy-loaded). */ /** * Build the editor kits for a session. Pass `config` to join a collaborative * room; omit it for a plain local editor. + * + * Async because collaborative mode lazy-loads the Yjs kit (and its optional + * peer deps) on first use; plain mode resolves immediately. */ -export function createEditorKits(config?: CollabConfig): EditorKit[] { +export async function createEditorKits( + config?: CollabConfig +): Promise { if (!config) return [new SafeAdvancedEditorKit(false)]; + const { HuddleYjsKit, defaultWsUrl } = await import('./collabKit'); const url = config.wsUrl ?? defaultWsUrl(); return [ new SafeAdvancedEditorKit(true), - new HuddleYjsKit(url, config.params ?? {}), + new HuddleYjsKit(url, config.params ?? {}, config.WebSocketPolyfill), ]; } diff --git a/src/components/RichEditor/index.ts b/src/components/RichEditor/index.ts index ed3d8683e..fb922719d 100644 --- a/src/components/RichEditor/index.ts +++ b/src/components/RichEditor/index.ts @@ -1,3 +1,7 @@ -export { RichEditor, type RichEditorProps, type CollabConfig } from './RichEditor'; +export { + RichEditor, + type RichEditorProps, + type CollabConfig, +} from './RichEditor'; export { CodeEditor, type CodeEditorProps } from './CodeEditor'; From 817cfba007dbc4b8cbcd9abec3b62622be2f4714 Mon Sep 17 00:00:00 2001 From: william garrity Date: Mon, 3 Aug 2026 19:24:37 -0400 Subject: [PATCH 5/5] fix(ci): patch @kerebron/extension-codecrock 0.8.9 for a11y test failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kerebron 0.7.9 -> 0.8.x bump exposed two upstream bugs in the static storybook build CI runs against: - CodeCrock.getSelection() invokes the native getSelection through dnt's globalThis merge-proxy, throwing "Illegal invocation" when the editor is detached during nodeview init — crashed the Code story render - the code-block language has no accessible name (axe + # select-name). Drop once fixed upstream. + "@kerebron/extension-codecrock@0.8.9": patches/@kerebron__extension-codecrock@0.8.9.patch + peerDependencyRules: # @mieweb/datavis bundles a published @mieweb/ui dev snapshot whose peer pins # an older datavis; allow the version this repo actually depends on.