Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b7d78ae
feat(core): add composable read-only restrictions
nperez0111 Sep 15, 2026
72a1702
feat(core)!: refine attribution colors and deleted-block styling
nperez0111 Sep 15, 2026
621902e
refactor(core)!: unify versioning state and preview lifecycle
nperez0111 Sep 15, 2026
11b4d59
feat(react)!: rebuild the version history sidebar
nperez0111 Sep 15, 2026
9be8d14
chore(examples): refresh versioning demos and setup
nperez0111 Sep 15, 2026
f2857ad
Improve versioning sidebar menu customization
nperez0111 Sep 15, 2026
f8ecfc9
Polish suggestion gallery styling and expand versioning coverage
nperez0111 Sep 15, 2026
194f69f
Fix build and snapshot runner dependency conflicts
nperez0111 Sep 15, 2026
48bec82
Fix version history seeding retries and preview state
nperez0111 Sep 15, 2026
a82f062
refactor: unify editability in ReadOnly extension
nperez0111 Sep 15, 2026
9cb2b5b
fix: animate versioning highlights without DOM mutations
nperez0111 Sep 16, 2026
b60ac63
Upgrade Yjs and y-prosemirror
nperez0111 Sep 16, 2026
2f9b255
Add attribution for block property changes
nperez0111 Sep 16, 2026
e4a5779
Improve version history keyboard accessibility and remove Save action
nperez0111 Sep 16, 2026
459b4db
Refactor YHub versioning and add safe history refresh
nperez0111 Sep 16, 2026
10062b6
Fix YHub attribution bounds for current version previews
nperez0111 Sep 16, 2026
449b9c5
Require synchronous version previews and simplify adapter contracts
nperez0111 Sep 16, 2026
e754cb6
Keep version selection visible when deletion clears a name
nperez0111 Sep 16, 2026
72fd1e5
Share versioning UI primitives and simplify sidebar state
nperez0111 Sep 16, 2026
c7fe526
Deduplicate versioning example history fixtures
nperez0111 Sep 16, 2026
afec519
refactor(react): move versioning to a dedicated sub-export
nperez0111 Sep 16, 2026
c9bc61b
fix: use read-only
nperez0111 Sep 16, 2026
6105796
chore: yjs versions
nperez0111 Sep 16, 2026
e537940
refactor: simplify
nperez0111 Sep 16, 2026
4dd8c83
refactor: inline version history sidebar, default editable true, use …
nperez0111 Sep 16, 2026
6939146
refactor: simplify scrollToFirstChange
nperez0111 Sep 16, 2026
26312d6
refactor: i18n versionLabel via blocknote dictionary
nperez0111 Sep 16, 2026
60fbd84
fix(core): report ReadOnly state from isEditable while unmounted
nperez0111 Sep 16, 2026
c492792
refactor(react): fixed header over scrolling version list, drop focus…
nperez0111 Sep 16, 2026
2f583c7
fix(react): return to Current when deleted version leaves the list
nperez0111 Sep 16, 2026
4a26842
chore: rm
nperez0111 Sep 16, 2026
8aa535a
fix(react): dedupe generic formatting-change tooltip label
nperez0111 Sep 16, 2026
ad01251
docs(gallery): mark textcolor-vs-bgcolor prop attribution as resolved
nperez0111 Sep 16, 2026
b93aa89
fix(core): stable empty-doc binding in new y/ collab stack
nperez0111 Sep 16, 2026
ba4e246
refactor(core): split y snapshot preview into codec + render modules
nperez0111 Sep 17, 2026
f96a8a6
refactor(core): drop yhub restore refresh delay, guard empty doc
nperez0111 Sep 17, 2026
1ce3379
refactor(core): rework Versioning list/preview sessions, add sub-modu…
nperez0111 Sep 17, 2026
44ffe1c
fix(core): flatten legacy anonymous text nodes when diffing snapshots
nperez0111 Sep 17, 2026
15ec810
test(e2e): refresh snapshots for pristine empty docs and trailing block
nperez0111 Sep 17, 2026
3c5ee8a
refactor(core): render version diffs without clearing the document
nperez0111 Sep 17, 2026
b29be57
refactor(versioning): derive loading state instead of storing it
nperez0111 Sep 17, 2026
ae21c0b
refactor(core): render version diffs via patched @y/prosemirror fragm…
nperez0111 Sep 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@
"yjs": "^13.6.27",
"zod": "^4.3.5",
"@blocknote/xl-typst-exporter": "workspace:*",
"@blocknote/xl-typst-compiler": "workspace:*"
"@blocknote/xl-typst-compiler": "workspace:*",
"y-prosemirror": "^1.3.7"
},
"devDependencies": {
"@blocknote/code-block": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
"react": "^19.2.3",
"react-dom": "^19.2.3",
"@y/protocols": "^1.0.6-rc.1",
"@y/y": "^14.0.0-rc.23",
"@y/prosemirror": "^2.0.0-6",
"@y/y": "^14.0.0-rc.26",
"@y/prosemirror": "^2.0.0-11",
"@y/websocket": "^4.0.0-rc.2"
},
"devDependencies": {
Expand Down
12 changes: 7 additions & 5 deletions examples/07-collaboration/10-suggestion-multi-editor/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ provider2.awareness.setLocalStateField("user", {
color: "#6eeb83",
});

const attrs = new Y.Attributions();
const attrs = Y.createContentMap();

// Batch timestamps: reuse the same timestamp for edits from the same user
// within a 10-second window of inactivity.
Expand Down Expand Up @@ -62,7 +62,7 @@ function getBatchedTimestamp(userName: string): number {
function trackAttributions(
trackedDoc: Y.Doc,
userName: string,
attributions: Y.Attributions,
attributions: Y.ContentMap,
) {
trackedDoc.on(
"update",
Expand Down Expand Up @@ -105,7 +105,9 @@ suggestingProvider.awareness.setLocalStateField("user", {
name: "Charlie",
color: "#ffbc42",
});
const suggestingRenderer = Y.createDiffRenderer(doc, suggestingDoc, { attrs });
const suggestingRenderer = Y.createDiffRenderer(doc, suggestingDoc, {
attributions: attrs,
});
suggestingRenderer.suggestionMode = false;

const suggestionModeDoc = new Y.Doc({ isSuggestionDoc: true });
Expand All @@ -117,7 +119,7 @@ suggestionModeProvider.awareness.setLocalStateField("user", {
color: "#ee6352",
});
const suggestionModeRenderer = Y.createDiffRenderer(doc, suggestionModeDoc, {
attrs,
attributions: attrs,
});
suggestionModeRenderer.suggestionMode = true;

Expand Down Expand Up @@ -155,7 +157,7 @@ function Editor({
userName,
userColor,
}: {
fragment: Y.Type;
fragment: Y.Node;
provider: { awareness?: Awareness };
renderer?: Y.DiffRenderer;
userName: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"dependencies": {
"y-websocket": "^2.1.0",
"yjs": "^13.6.27",
"lib0": "^0.2.99"
"lib0": "^0.2.99",
"y-prosemirror": "^1.3.7"
}
}
2 changes: 1 addition & 1 deletion examples/07-collaboration/11-versioning-yjs13/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This example shows how to use the `VersioningExtension` with collaborative editing using `yjs` (v13). Snapshots are stored in localStorage using Yjs state updates.

**Try it out:** Edit the document, then click the "Version History" button to open the sidebar. From there you can save snapshots, preview older versions, rename them, and restore them.
The sidebar opens on a document with a few versions already in its history, so you can preview them, rename them, and restore them right away. The editor is read-only while the sidebar is open: close it to edit the document, then reopen it with the "History" button and press "Save version" to add a version of your own.

**Relevant Docs:**

Expand Down
3 changes: 2 additions & 1 deletion examples/07-collaboration/11-versioning-yjs13/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"react-dom": "^19.2.3",
"y-websocket": "^2.1.0",
"yjs": "^13.6.27",
"lib0": "^0.2.99"
"lib0": "^0.2.99",
"y-prosemirror": "^1.3.7"
},
"devDependencies": {
"@types/react": "^19.2.3",
Expand Down
69 changes: 48 additions & 21 deletions examples/07-collaboration/11-versioning-yjs13/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,61 @@ import "@blocknote/core/fonts/inter.css";
import { withCollaboration } from "@blocknote/core/yjs";
import { VersioningExtension } from "@blocknote/core/extensions";
import { createYjsVersioningAdapter } from "@blocknote/core/yjs";
import { localStorageEndpoints } from "./localStorageEndpoints";
import {
BlockNoteViewEditor,
useCreateBlockNote,
useExtensionState,
} from "@blocknote/react";
hasStoredVersions,
localStorageEndpoints,
storeVersions,
} from "./localStorageEndpoints";
import { BlockNoteViewEditor, useCreateBlockNote } from "@blocknote/react";
import { BlockNoteView } from "@blocknote/mantine";
import "@blocknote/mantine/style.css";
import { useState } from "react";

import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
import { toBase64, fromBase64 } from "lib0/buffer";

import { VersionHistorySidebar } from "./VersionHistorySidebar";
import { VersioningSidebar } from "@blocknote/react/versioning";
import {
blocksToUpdate,
DAY_MS,
LIVE_DOCUMENT,
SAMPLE_HISTORY,
} from "./sampleVersions";
import "./style.css";

const roomName = "blocknote-versioning-yjs-example";
const FRAGMENT_NAME = "document-store";
// localStorage key for the live ("current version") document. Snapshots are
// persisted separately by `localStorageEndpoints`; this keeps the live doc
// itself across refreshes since the demo has no server-side persistence.
const DOC_STORAGE_KEY = "blocknote-versioning-yjs-current-doc";
const doc = new Y.Doc();
const fragment = doc.getXmlFragment("document-store");
const fragment = doc.getXmlFragment(FRAGMENT_NAME);

// Persist the full document state on every change.
doc.on("update", () => {
localStorage.setItem(DOC_STORAGE_KEY, toBase64(Y.encodeStateAsUpdate(doc)));
});

// Restore the persisted live document before the editor is created, so it
// adopts the stored content instead of starting empty.
const persistedDoc = localStorage.getItem(DOC_STORAGE_KEY);
if (persistedDoc) {
Y.applyUpdate(doc, fromBase64(persistedDoc));
} else if (!hasStoredVersions()) {
// First visit: seed a few named versions so the history has something to
// show, and open on the newest state of the same document.
storeVersions(
SAMPLE_HISTORY.map((version) => ({
name: version.name,
createdAt: Date.now() - version.daysAgo * DAY_MS,
content: blocksToUpdate(version.blocks, FRAGMENT_NAME),
})),
);
Y.applyUpdate(doc, blocksToUpdate(LIVE_DOCUMENT, FRAGMENT_NAME));
}

// Persist the full document state on every change.
doc.on("update", () => {
localStorage.setItem(DOC_STORAGE_KEY, toBase64(Y.encodeStateAsUpdate(doc)));
});

const provider = new WebsocketProvider(
"wss://demos.yjs.dev/ws",
roomName,
Expand Down Expand Up @@ -66,22 +85,30 @@ export default function App() {
}),
);

const { previewedSnapshotId } = useExtensionState(VersioningExtension, {
editor,
});
const [showSidebar, setShowSidebar] = useState(true);

return (
<div className="wrapper">
<BlockNoteView
editor={editor}
editable={previewedSnapshotId === undefined}
renderEditor={false}
>
{/* No `editable` prop: the sidebar makes the editor read-only for as long
as it's open, and restores it on close. */}
<BlockNoteView editor={editor} renderEditor={false}>
<div className="layout">
<div className="editor-panel">
<BlockNoteViewEditor />
{!showSidebar && (
<button
className="show-history-button"
onClick={() => setShowSidebar(true)}
>
History
</button>
)}
</div>
<VersionHistorySidebar />
{showSidebar && (
<div className={"sidebar-section"}>
<VersioningSidebar onClose={() => setShowSidebar(false)} />
</div>
)}
</div>
</BlockNoteView>
</div>
Expand Down

This file was deleted.

This file was deleted.

Loading
Loading