Skip to content

feat: @y/prosemirror v2 + @y/y v14 integration#2739

Draft
nperez0111 wants to merge 97 commits into
mainfrom
y-prosemirror-v14
Draft

feat: @y/prosemirror v2 + @y/y v14 integration#2739
nperez0111 wants to merge 97 commits into
mainfrom
y-prosemirror-v14

Conversation

@nperez0111

@nperez0111 nperez0111 commented May 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds support for the new @y/prosemirror v2 and @y/y v14 (Yjs 14) packages alongside the existing Yjs 13 (yjs, y-prosemirror) integration. This includes new collaboration bindings, versioning extensions, forked document support, and suggestion tracking built on the v14 APIs.

Rationale

The @y/y v14 ecosystem introduces significant improvements over Yjs 13, including better TypeScript support, a new protocol layer, and improved ProseMirror bindings. This PR adds first-class support for v14 while keeping full backward compatibility with Yjs 13.

Changes

New @blocknote/core/y entry point

  • YSync extension — ProseMirror plugin bridging @y/prosemirror v2 with TipTap/BlockNote
  • YCursorPlugin — collaborative cursor/selection rendering for v14
  • Suggestions extension — track suggested changes (insertions/deletions) via @y/prosemirror v2
  • ForkYDoc extension — fork a Yjs document for isolated editing, then merge or discard changes
  • Versioning extension — snapshot-based version history using @y/y v14 APIs
  • RelativePositionMapping — cursor position mapping across collaborative edits
  • YjsThreadStore — comment threads stored in Yjs, with REST-backed variant

Versioning system (@blocknote/core extensions)

  • Generic Versioning extension — framework-agnostic version history with preview/restore, works with pluggable backends (in-memory, Yjs 13, Yjs 14)
  • In-memory versioning — simple snapshot store for non-collaborative use
  • Yjs 13 versioning — snapshot-based version history for the existing yjs package
  • React components: VersioningSidebar, Snapshot, CurrentSnapshot

Editor improvements

  • UniqueID — skip ID rewriting for suggested-deletion blocks (nodes with y-attributed-delete mark) to avoid corrupting suggestions
  • Block infogetBlockInfoFromPos now handles nodes with marks (suggested changes)
  • PreviousBlockType — scoped to changed range instead of full document scan
  • Various fixes for block manipulation commands (moveBlocks, replaceBlocks, splitBlock, nestBlock) to handle marked/attributed nodes

Patches

  • @y/prosemirror@2.0.0-2 — extensive patch for BlockNote compatibility
  • @y/y@14.0.0-rc.16 — minor patch
  • lib0@1.0.0-rc.13 — compatibility patch

Examples

  • Versioning + Suggestions — full demo with comments sidebar, suggestion actions, version history
  • YHub — 4-mode editor demo (edit, suggest, comment, view)
  • Yjs 13 versioning snapshots — version history with Yjs 13 backend
  • Yjs 14 versioning snapshots — version history with Yjs 14 backend
  • Generic versioning — standalone versioning without collaboration

Tests

  • UniqueID duplicate handling (plain + suggested-deletion blocks)
  • ForkYDoc (fork, merge, discard for both v13 and v14)
  • Versioning (preview, restore, snapshot management for all backends)
  • RelativePositionMapping (position tracking across edits)
  • YjsThreadStore (CRUD for comment threads)
  • getBlockInfoFromPos (marked node handling)

Impact

  • No breaking changes — all @y/ packages are optional peer dependencies
  • Existing Yjs 13 integrations continue to work unchanged
  • New @blocknote/core/y export for v14 users, parallel to existing @blocknote/core/yjs

Screenshots/Video

Live examples:

Checklist

  • Code follows the project's coding standards
  • Unit tests covering the new features have been added
  • All existing tests pass
  • The documentation has been updated to reflect the new features

@vercel

vercel Bot commented May 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
blocknote Ready Ready Preview Jul 8, 2026 12:20pm
blocknote-website Ready Ready Preview Jul 8, 2026 12:20pm

Request Review

@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e5c466d1-cb9f-41d3-9fb9-c2aa33b2899c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch y-prosemirror-v14

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented May 12, 2026

Copy link
Copy Markdown

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/@blocknote/ariakit@2739

@blocknote/code-block

npm i https://pkg.pr.new/@blocknote/code-block@2739

@blocknote/core

npm i https://pkg.pr.new/@blocknote/core@2739

@blocknote/mantine

npm i https://pkg.pr.new/@blocknote/mantine@2739

@blocknote/react

npm i https://pkg.pr.new/@blocknote/react@2739

@blocknote/server-util

npm i https://pkg.pr.new/@blocknote/server-util@2739

@blocknote/shadcn

npm i https://pkg.pr.new/@blocknote/shadcn@2739

@blocknote/xl-ai

npm i https://pkg.pr.new/@blocknote/xl-ai@2739

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/@blocknote/xl-docx-exporter@2739

@blocknote/xl-email-exporter

npm i https://pkg.pr.new/@blocknote/xl-email-exporter@2739

@blocknote/xl-multi-column

npm i https://pkg.pr.new/@blocknote/xl-multi-column@2739

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/@blocknote/xl-odt-exporter@2739

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/@blocknote/xl-pdf-exporter@2739

commit: 4b849df

@nperez0111 nperez0111 force-pushed the y-prosemirror-v14 branch from 00569fc to 38d3e13 Compare May 13, 2026 16:09
@nperez0111 nperez0111 force-pushed the y-prosemirror-v14 branch from 38d3e13 to a56bb0d Compare May 13, 2026 16:12
@nperez0111 nperez0111 changed the base branch from main to decouple-yjs May 13, 2026 16:25
@nperez0111 nperez0111 force-pushed the decouple-yjs branch 3 times, most recently from a9f7067 to 2ad90dc Compare May 13, 2026 17:34
@nperez0111 nperez0111 force-pushed the y-prosemirror-v14 branch from a56bb0d to 6b9a6f6 Compare May 13, 2026 20:12
@nperez0111 nperez0111 force-pushed the decouple-yjs branch 4 times, most recently from e44394d to 17ab49f Compare May 14, 2026 05:20
@nperez0111 nperez0111 force-pushed the y-prosemirror-v14 branch from 6b9a6f6 to d13fcac Compare May 14, 2026 05:23
YousefED and others added 30 commits July 2, 2026 17:12
The user cache is no longer a BlockNote extension. It's now a plain
`createUserStore(resolveUsers)` factory (moved out of `extensions/` to
`src/user/`), and the extensions that need it build one and expose it on
their instance instead of locating it through the extension registry.

- comments & collaboration accept `resolveUsers: callback | UserStore`,
  normalized via a shared `normalizeToUserStore` helper, so one de-duped
  store can be shared across comments, suggestions and versioning
- CollaborationExtension normalizes once and hands the same store down to
  YSync (suggestion tooltips + author colors), Suggestions and versioning
- YSync suggestion colors now derive from the user store, falling back to
  the deterministic palette when a user isn't resolved
- yhub versioning reads the store off the YSync extension instance
- React drops the generic useUsers/useUser hook in favour of a
  comments-local helper that reads the comments extension's store

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate the attribution tooltip from an imperative DOM implementation to
the standard BlockNote pattern: the core SuggestionMarksExtension now
drives state through a store and stays agnostic about rendering, while a
React controller subscribes to it and positions the tooltip with
floating-ui (GenericPopover), portaling through a themeable
SuggestionMarksTooltip component slot (mantine/ariakit/shadcn).

Also add an optional getSuggestionMarkClassName collaboration option to
color suggestion marks by change type (e.g. green insertions, red
deletions) instead of by author. It can return a single class (applied to
both the mark content and its tooltip) or { content, tooltip } to style
them independently; when set, the per-user color styling is dropped so the
class fully controls appearance. Default dark-mode suggestion text is now
white for better contrast.

SuggestionMarksExtension's userStore option is now optional
(normalized to an empty store), fixing no-arg call sites (xl-ai).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Yjs `mapAttributionToMark` baked each author's color into the
`y-attributed-*` mark attributes, resolved through the user store at sync
time. That made the mapper non-deterministic: once a user's real color
resolved asynchronously the mapper's output changed for the same
(format, attribution) input, so the @y/prosemirror reconcile diff was
never empty and fired phantom reconcile dispatches in a loop.

Marks now carry only stable identity (`userIds`, plus `format` for the
modification mark). Author colors are applied as a reactive ProseMirror
decoration layer in SuggestionMarksExtension, resolved from the user
store and rebuilt when users/colors resolve (via a store subscription),
so colors load independently of the deterministic mark representation.

An inline decoration nests inside the mark's content span, so the
`--user-color-*` custom properties would land below the paint element.
The decoration span therefore carries the `.bn-suggestion-mark(--delete)`
paint class itself (and the mark view keeps its own content span
structural), so the existing Block.css rules resolve the colors on the
element that actually has them. Block-level marks use a node decoration,
which lands the properties directly on the painted `.bn-suggestion-node`
child, so no CSS changes were needed. When `getSuggestionMarkClassName`
supplies an override class, no per-user color decoration is emitted.

The suggestion-mark tooltip now resolves its color from the user store
(the same helper) instead of the removed `data-user-color-*` attribute.

e2e inline snapshots updated to drop the color attrs from the serialized
doc; screenshots are unchanged (visual parity confirmed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename the suggestion-mark presentation/schema surface to a consistent
"attribution" vocabulary, distinct from the attribution *managers* that
produce the data:

- SuggestionMarksExtension -> AttributionExtension (key: "attribution");
  the color-decoration + hover-tooltip resolver.
- YSuggestionMarksExtension -> YAttributionMarksExtension
  (key: "yAttributionMarks"), plus its styling API
  (get/GetSuggestionMarkClassName, resolveSuggestionMarkClassName,
  SuggestionMarkStyleInfo/ClassNames -> Attribution* equivalents).
- SuggestionMarksTooltip component family -> AttributionTooltip across
  react/mantine/ariakit/shadcn (component, Props, Controller, the
  Components slot, the suggestionMarksTooltip prop/portal target, and the
  containing folders).
- xl-ai SuggestionMarksExtension -> AttributionMarksExtension
  (key: "aiAttributionMarks").

No back-compat aliases. Types regenerate on build; all packages typecheck.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the reactive decoration layer that colored the y-attributed-*
suggestion marks with per-user CSS custom properties, so coloring no
longer depends on async user-store resolution driving a decoration
rebuild.

The mark wrapper now sets the generic `--user-color-*` properties the
Block.css rules read, sourcing them from the author's per-user variable
via `var(--user-color-<id>-*, <fallback>)`. AttributionExtension writes
those `--user-color-<id>-*` variables on the editor root as users
resolve, so the CSS cascade recolors marks from the deterministic
palette fallback to the author's own color — no decoration, no doc
transaction. This restores the pre-decoration mark-view structure
(colors on the wrapper) while keeping the marks color-free in their
attrs (the determinism the sync reconcile requires). No CSS changes.

The per-user variable name is a fixed-width FNV-1a hash of the id
(lib0/hash/fnv1a), safe to embed in a custom-property name.

Authors are loaded incrementally: a small plugin scans each
doc-changing transaction's changed span (via @tiptap/core
`getChangedRanges`, which unlike `Transaction.changedRange()` also
covers the mark-only steps suggestion mode adds over existing text).
Adds a unit test covering that a change adding an attribution mark
loads its author.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move formatChangeLabel out of the core AttributionExtension and into the
React layer, where rendering the tooltip's text is a view concern. Core now
emits the raw change context (parsed `format` keys) on AttributionTooltipState
instead of a pre-baked label, mirroring how getAttributionMarkClassName
receives structured info for categorization. The internal grouping key for
nested marks becomes an i18n-free `attributionIdentity`.

React composes the fully-localized tooltip text — "Inserted by: x",
"Deleted by: x", "Formatting change (Bold, Italic) by: x" — via new
function-valued `suggestion_changes` dictionary entries, translated across all
locales, plus a configurable `formatChangeLabel` for the format list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sentinel

Rework the versioning contract for clarity and consistency:

- Rename endpoint + extension methods to a bare-verb scheme:
  updateSnapshotName -> rename, deleteSnapshot -> remove; extension
  methods/flags follow (canCreate, canRestore, canRename, canRemove,
  canCompare, canPreviewCurrent).
- Rename the two "current" option callbacks to getCurrentDocument
  (live handle, Input) and serializeCurrentContent (detached copy,
  Output), and generics I/O/A -> Input/Output/Attributions.
- Collapse the current-version sentinel to a single exported
  constant: CURRENT_VERSION_ID is now a `unique symbol` used as both
  the store sentinel and the synthetic list-entry id (VersionSnapshot.id
  widened to `string | typeof CURRENT_VERSION_ID`). Removes the former
  CURRENT_VERSION_ENTRY_ID string twin. React derives a local string
  key; YHub builds the current entry directly instead of via the
  string-typed wire helper.
- Tighten JSDoc across the versioning files (inline-arg docs, less
  prose, {@link} cross-references).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an opt-in DiffVersioningExtension (@blocknote/core/y) that renders a
read-only diff between two Block[] states with y-attributed-insert/delete
marks — the same visual result as the Yjs collaboration adapter, but driven
from plain block arrays with no server. Changes are attributed to a static
User A / User B via a serverless Y.Attributions store; colors and hover
tooltips reuse the composed AttributionExtension.

The core inMemoryVersioning preview controller discovers the extension by key
(type-only import, so the base @blocknote/core bundle keeps zero runtime @y/*
dependency) and delegates diff rendering to it, falling back to a static
document swap when it isn't registered. supportsComparison / canCompare are
lazy getters gated on the extension's presence, so comparison UI is only
offered when a diff can actually be rendered — independent of extension
registration order.

Move the shared docDiffToDelta and getProseMirrorTrFromYFragment helpers into
y/utils.ts and reuse them from the Yjs adapter and snapshot builder.

Wire the extension into the 02-versioning example.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…xample

snapshotBuilder.ts and seed.ts were only used by the versioning-yjs14
example but shipped in @blocknote/core's output. Move them into the
example's src/, drop their exports from the y extension/versioning
barrels, and delete their tests (demo-only, no coverage needed). Add
@y/prosemirror to the example deps since buildSnapshots uses it directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Show the version history sidebar by default (no click-to-open), match the
wrapper/layout/editor-panel structure used by the collaboration versioning
examples, and use the shared VersioningSidebar directly. Drop the Filter
dropdown along with the now-unused SettingsSelect/VersionHistorySidebar
components and the react-icons dependency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…w context

A version diff has a single conceptual author — the version that introduced
the changes — not two collaborators. Replace the static User A / User B model
in DiffVersioningExtension with the previewed version's name: renderDiff takes
a version label and attributes the y-attributed-* marks to a synthetic id that
encodes it (`version:<label>`), so each version's tooltip resolves to its own
name (the encoded id also keeps the user-store cache from showing a stale name
when switching comparisons). Tooltips read "…by {versionName}".

Thread the version metadata through a new `context` argument on
PreviewController.enterPreview (the previewed + compared-against snapshots the
extension already has in hand) instead of smuggling a display label through the
getAttributions channel — which is meant for who/when authorship data. The
in-memory controller reads snapshot.name directly, so the synthetic
getAttributions and InMemoryDiffAttribution type are gone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… into user/

Extract User/UserStore/createUserStore from user/index.ts into user/UserStore.ts,
turning index.ts into a barrel. Move userColors.ts out of y/extensions/ (it has no
Yjs coupling — it operates on the generic User/UserStore) into user/, alongside the
identity types it extends. Repoint the two consumers (AttributionExtension,
YAttributionMarks) at the new path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Version snapshots now carry raw author user-ids on a new `by` field
instead of a pre-resolved `secondaryLabel` string, and resolving those
ids to usernames becomes a view concern:

- `VersioningExtension` accepts `resolveUsers` and exposes the
  normalized `userStore` on its instance (mirroring CommentsExtension);
  `CollaborationExtension` passes its shared store in.
- The YHub endpoints just split the activity `by` field into ids — no
  more reaching into the attribution extension's user store. `create`
  stamps the optimistic snapshot with the cursor user's id.
- New `useVersionUsers`/`useSnapshotLabel` React hooks (mirroring
  `useCommentUsers`) resolve `by` ids reactively, so author labels
  update as user info loads into the store — no re-list needed.
- `UserStore` gains `setUser` and resolvers now receive the store, so a
  resolver can return partial info synchronously and fill in the rest
  later.
Convert y-prosemirror e2e inline snapshots to file-based snapshots to
work around a vite-plus-test inline-snapshot writer bug that misplaces
values across assertions when many snapshots in a file change at once,
making the suite non-deterministic run-to-run.

Also make the suggestion fixture deterministic: fixed Y.Doc clientIDs,
serialize ydocXml with a stable Y.baseRenderer, sort marks/attrs, and
strengthen the settle-gate in waitForSuggestion. Skip the known
runaway-loop concurrent table test and the large-diff-delete-all
versioning scenario.
- yhub.ts: add per-response index to history snapshot ids
  (history-${to}-${index}) so duplicate activity `to` values no longer
  produce duplicate React keys; default activityLimit=50 and
  groupMaxGap=1000 and only send group/groupMaxDuration when defined so
  the server no longer receives String(undefined) (parseInt->NaN disabled
  grouping); guard getVersionNamesMap fragment access with optional chain
- YSync.ts: expose fragment on the ySync extension so
  getExtension("ySync").fragment resolves
- yhub.test.ts: update history id assertion to new scheme and add ySync
  stub for the __bn_version_names name-store tests
- versioning-yjs14 example + versioning sidebar/styles: seed/reconcile
  cleanup and UI updates
…js14 example

Read groupMaxGap/groupMaxDuration fresh from options in the YHub list()
endpoint so a caller mutating the passed options object reconfigures
grouping on the next refresh (default groupMaxGap lowered to 10000ms).

Add a floating gear-toggled "Configuration" panel to the yjs14 versioning
example with sliders + number inputs for both grouping knobs, re-fetching
the history sidebar live against the same document. Space seeded edits
30s-1h apart and assign authors in contiguous runs so same-author streaks
actually group, and open the sidebar already well-grouped for a short,
readable history.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants