Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fbb628e
fix: setup for yjs 14
nperez0111 May 14, 2026
053c9a5
feat: allow user colors on suggestion marks
nperez0111 May 13, 2026
221ad3c
feat: working `@y/prosemirror` demo
nperez0111 May 15, 2026
da657a4
fix: have `@blocknote/core/y` implement `RelativePositionMappingExten…
nperez0111 May 18, 2026
39551bc
fix: slightly better working fork doc plugin
nperez0111 May 18, 2026
571b4f3
feat: more progress into the version history demo
nperez0111 May 19, 2026
59489e9
test: get tests passing again
nperez0111 May 19, 2026
609763d
chore: attrs?
nperez0111 May 19, 2026
9673246
chore: pnpm-lock
nperez0111 May 20, 2026
ca9dd8f
chore: mark `@y/y` as optional
nperez0111 May 21, 2026
3b13bdb
test: copy over RelativePositionMapping tests to @y/y and fix positio…
nperez0111 May 21, 2026
6c3a177
feat: add versioning extension, Yjs thread store, and versioning exam…
nperez0111 May 22, 2026
9acc555
fix: widen forEachAttr callback key type to match @y/y's YType signature
nperez0111 May 22, 2026
89669d2
chore: update excludes & mapAttributionToMark
nperez0111 May 26, 2026
3d29abd
fix: make table content support attribution marks
nperez0111 May 26, 2026
800735c
chore: update `@y/prosemirror` patch
nperez0111 May 28, 2026
1dac678
fix: move modification styles from xl-ai to core
nperez0111 May 29, 2026
d1697e4
chore: update to latest @y/prosemirror patch
nperez0111 May 29, 2026
b3fad88
chore: incldue built deps
nperez0111 May 29, 2026
c738388
build: patch in the latest changes for y-prosemirror
nperez0111 Jun 5, 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
9 changes: 8 additions & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,14 @@
"tailwind-merge": "^3.4.0",
"y-partykit": "^0.0.25",
"yjs": "^13.6.27",
"zod": "^4.3.5"
"zod": "^4.3.5",
"@y/protocols": "^1.0.6-rc.1",
"@y/websocket": "^4.0.0-3",
"@y/y": "^14.0.0-rc.16",
"@y/prosemirror": "^2.0.0-2",
"@floating-ui/react": "^0.27.18",
"lib0": "1.0.0-rc.13",
"y-websocket": "^2.1.0"
},
"devDependencies": {
"@blocknote/code-block": "workspace:*",
Expand Down
14 changes: 14 additions & 0 deletions examples/07-collaboration/10-versioning/.bnexample.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"playground": true,
"docs": true,
"author": "matthewlipski",
"tags": ["Advanced", "Development", "Collaboration"],
"dependencies": {
"@y/protocols": "^1.0.6-rc.1",
"@y/websocket": "^4.0.0-3",
"@y/y": "^14.0.0-rc.16",
"react-icons": "5.6.0",
"@floating-ui/react": "^0.27.18",
"lib0": "1.0.0-rc.13"
}
}
15 changes: 15 additions & 0 deletions examples/07-collaboration/10-versioning/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Collaborative Editing Features Showcase

In this example, you can play with all of the collaboration features BlockNote has to offer:

**Comments**: Add comments to parts of the document - other users can then view, reply to, react to, and resolve them.

**Versioning**: Save snapshots of the document - later preview saved snapshots and restore them to ensure work is never lost.

**Suggestions**: Suggest changes directly in the editor - users can choose to then apply or reject those changes.

**Relevant Docs:**

- [Editor Setup](/docs/getting-started/editor-setup)
- [Comments](/docs/features/collaboration/comments)
- [Real-time collaboration](/docs/features/collaboration)
14 changes: 14 additions & 0 deletions examples/07-collaboration/10-versioning/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Collaborative Editing Features Showcase</title>
<script>
<!-- AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY -->
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
11 changes: 11 additions & 0 deletions examples/07-collaboration/10-versioning/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./src/App.jsx";

const root = createRoot(document.getElementById("root")!);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
36 changes: 36 additions & 0 deletions examples/07-collaboration/10-versioning/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@blocknote/example-collaboration-versioning",
"description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
"type": "module",
"private": true,
"version": "0.12.4",
"scripts": {
"start": "vite",
"dev": "vite",
"build:prod": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@blocknote/ariakit": "latest",
"@blocknote/core": "latest",
"@blocknote/mantine": "latest",
"@blocknote/react": "latest",
"@blocknote/shadcn": "latest",
"@mantine/core": "^9.0.2",
"@mantine/hooks": "^9.0.2",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"@y/protocols": "^1.0.6-rc.1",
"@y/websocket": "^4.0.0-3",
"@y/y": "^14.0.0-rc.16",
"react-icons": "5.6.0",
"@floating-ui/react": "^0.27.18",
"lib0": "1.0.0-rc.13"
},
"devDependencies": {
"@types/react": "^19.2.3",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"vite": "^8.0.8"
}
}
225 changes: 225 additions & 0 deletions examples/07-collaboration/10-versioning/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import "@blocknote/core/fonts/inter.css";
import {
withCollaboration,
SuggestionsExtension,
} from "@blocknote/core/y";
import { localStorageEndpoints } from "./localStorageEndpoints.js";
import { VersioningExtension } from "@blocknote/core/extensions";
import {
BlockNoteViewEditor,
FloatingComposerController,
useCreateBlockNote,
useEditorState,
useExtension,
useExtensionState,
} from "@blocknote/react";
import { BlockNoteView } from "@blocknote/mantine";
import "@blocknote/mantine/style.css";
import { useEffect, useMemo, useState } from "react";
import * as Y from "@y/y";
import { WebsocketProvider } from "@y/websocket";

import { getRandomColor, HARDCODED_USERS, MyUserType } from "./userdata";
import { SettingsSelect } from "./SettingsSelect";
import "./style.css";
import {
DefaultThreadStoreAuth,
CommentsExtension,
} from "@blocknote/core/comments";
import { YjsThreadStore } from "@blocknote/core/y";

import { CommentsSidebar } from "./CommentsSidebar";
import { VersionHistorySidebar } from "./VersionHistorySidebar";
import { SuggestionActions } from "./SuggestionActions";
import { SuggestionActionsPopup } from "./SuggestionActionsPopup";

const roomName = "blocknote-versioning-example";
const doc = new Y.Doc();
const provider = new WebsocketProvider(
"wss://demos.yjs.dev/ws",
roomName,
doc,
{ connect: false },
);
provider.connectBc();
doc.on("update", () => {
console.log("doc-update", doc.get().toJSON());
});

const suggestionModeDoc = new Y.Doc({ isSuggestionDoc: true });
suggestionModeDoc.on("update", () => {
console.log("suggestion-update", suggestionModeDoc.get().toJSON());
});
const suggestionModeProvider = new WebsocketProvider(
"wss://demos.yjs.dev/ws",
roomName + "-suggestions",
suggestionModeDoc,
{ connect: false },
);
const suggestionModeAttributionManager = Y.createAttributionManagerFromDiff(
doc,
suggestionModeDoc,
// {
// attrs: [
// // Y.createAttributionItem("insert", ["John Doe"]),
// // Y.createAttributionItem("delete", ["John Doe"]),
// ],
// },
);
suggestionModeProvider.connectBc();

async function resolveUsers(userIds: string[]) {
// fake a (slow) network request
await new Promise((resolve) => setTimeout(resolve, 1000));

return HARDCODED_USERS.filter((user) => userIds.includes(user.id));
}

export default function App() {
const [activeUser, setActiveUser] = useState<MyUserType>(HARDCODED_USERS[0]);

const threadStore = useMemo(() => {
return new YjsThreadStore(
activeUser.id,
doc.get("threads"),
new DefaultThreadStoreAuth(activeUser.id, activeUser.role),
);
}, [doc, activeUser]);

const editor = useCreateBlockNote(
withCollaboration({
collaboration: {
provider,
suggestionDoc: suggestionModeDoc,
attributionManager: suggestionModeAttributionManager,
fragment: doc.get(),
user: { color: getRandomColor(), name: activeUser.username },
versioningEndpoints: localStorageEndpoints,
},
extensions: [CommentsExtension({ threadStore, resolveUsers })],
}),
);

const {
enableSuggestions,
disableSuggestions,
showSuggestions,
checkUnresolvedSuggestions,
} = useExtension(SuggestionsExtension, { editor });
const hasUnresolvedSuggestions = useEditorState({
selector: () => checkUnresolvedSuggestions(),
editor,
});

const { previewedSnapshotId } = useExtensionState(VersioningExtension, {
editor,
});

const [editingMode, setEditingMode] = useState<
"editing" | "suggestions" | "view-suggestions"
>("editing");
useEffect(() => {
if (editingMode !== "editing") {
disableSuggestions();
setEditingMode("editing");
}
}, [previewedSnapshotId]);
const [sidebar, setSidebar] = useState<"comments" | "versionHistory">(
"versionHistory",
);

return (
<div className="wrapper">
<BlockNoteView
className={"full-collaboration"}
editor={editor}
editable={
previewedSnapshotId === undefined && activeUser.role === "editor"
}
renderEditor={false}
comments={sidebar !== "comments"}
>
<div className="layout">
<div className="editor-panel">
{previewedSnapshotId === undefined && (
<div className={"settings"}>
<SettingsSelect
label={"User"}
items={HARDCODED_USERS.map((user) => ({
text: `${user.username} (${
user.role === "editor" ? "Editor" : "Commenter"
})`,
icon: null,
onClick: () => {
setActiveUser(user);
},
isSelected: user.id === activeUser.id,
}))}
/>
{activeUser.role === "editor" && (
<SettingsSelect
label={"Mode"}
items={[
{
text: "Editing",
icon: null,
onClick: () => {
disableSuggestions();
setEditingMode("editing");
},
isSelected: editingMode === "editing",
},
{
text: "Editing + Viewing Suggestions",
icon: null,
onClick: () => {
showSuggestions();
setEditingMode("view-suggestions");
},
isSelected: editingMode === "view-suggestions",
},
{
text: "Suggesting",
icon: null,
onClick: () => {
enableSuggestions();
setEditingMode("suggestions");
},
isSelected: editingMode === "suggestions",
},
]}
/>
)}
<SettingsSelect
label={"Sidebar"}
items={[
{
text: "Version History",
icon: null,
onClick: () => setSidebar("versionHistory"),
isSelected: sidebar === "versionHistory",
},
{
text: "Comments",
icon: null,
onClick: () => setSidebar("comments"),
isSelected: sidebar === "comments",
},
]}
/>
{activeUser.role === "editor" &&
editingMode === "suggestions" &&
hasUnresolvedSuggestions && <SuggestionActions />}
</div>
)}
<BlockNoteViewEditor />
<SuggestionActionsPopup />
{sidebar === "comments" && <FloatingComposerController />}
</div>
{sidebar === "comments" && <CommentsSidebar />}
{sidebar === "versionHistory" && <VersionHistorySidebar />}
</div>
</BlockNoteView>
</div>
);
}
Loading
Loading