Context
Three planning docs (editor-big-plan.md, plan-editor-document-features.md, plan-editor-migration-steps.md) describe the same vision: use the editor to render all document content (read-only and editable), replacing BlocksContent entirely.
The document lifecycle machine (XState v5) is already built and wired into resource pages on branch unified-document-lifecycle-machine. This plan merges all three docs into one.
Key Decisions
Extensions before swap: Core extensions must reach feature parity before replacing BlocksContent
Single editor instance: Toggle
isEditablerather than swapping componentsrenderType + editable: Constructor takes
{ renderType: 'document' | 'embed' | 'comment', editable: boolean }.renderTypeis context metadata; plugin loading controlled byeditableonly (for now)Core 4 extensions + Supernumbers: Block Highlighting, Image Gallery, Hover Actions, Range Selection, Supernumbers — Collapsed Blocks deferred
Editing blocked on renderer swap: In-place editing only after editor is the renderer
Click-to-edit: Single click on text block or bottom empty area enters edit mode (not a button). Click-drag = text selection (shows range selection bubble), NOT edit trigger
Draft auto-load: If user has edit access and a draft exists, show draft in edit mode automatically
Account switching: Save draft + exit edit mode when switching to non-editor account; enter edit mode (or allow it) when switching to editor account
Supernumbers: Top-right of block, shown in all modes when count > 0. Click emits event from editor, app decides behavior (desktop: open right panel focused on block)
Already Done (this branch)
✅ Document machine (
document-machine.ts, 465 lines)✅ React hooks/provider (
use-document-machine.ts)✅ Debug drawer + inspect tools
✅ Wired into
resource-page-common.tsxwithDocumentMachineProvider✅ Desktop/web pages pass
canEdit,existingDraftId✅ Old draft page +
draftMachineuntouched and functional
Dependency Graph
Phase 1: renderType + editable + readOnly fixes
│
├──→ Phase 2a: Block Highlighting ext ──┐
├──→ Phase 2b: Image Gallery ext ──┤
├──→ Phase 2c: Block Hover Actions ext ──┤ (parallel, separate PRs)
├──→ Phase 2d: Range Selection ext ──┤
└──→ Phase 2e: Supernumbers ext ──┘
│
▼
Phase 3: Renderer Swap
(BlocksContent → editor readOnly)
│
▼
Phase 4: In-place Editing
(click-to-edit + machine wiring + account switching)
│
▼
Phase 5: Publishing via Machine
│
▼
Phase 6: Cleanup
│
▼
Phase 7 (later): Collapsed Blocks ext
Phase 1: renderType + editable + ReadOnly Fixes
Goal: Introduce renderType + editable to editor constructor, fix blocks that misbehave in readOnly, guard markdown shortcuts.
1a. Editor constructor changes
Add to
BlockNoteEditor.tsoptions:renderType: 'document' | 'embed' | 'comment'and keep existingeditable: booleanrenderTypestored on editor instance for extensions to read (context only, does not affect plugin loading yet)editablecontrols plugin loading as beforeDefault:
renderType: 'document',editable: true
1b. ReadOnly block fixes
1c. Markdown shortcut guards
Guard
#→ heading,-→ bullet list,1.→ numbered list triggers behindeditor.isEditableThese are likely TipTap/ProseMirror input rules — find and guard them
Location:
BlockNode.tshandleTextInput (lines 444-460) and/or TipTap extension input rulesAlso check
MarkdownExtension.ts(lines 160-217) paste handler
1d. Toolbar suppression when not editable
editor-view.tsx: conditionally renderFormattingToolbarPositioner,SlashMenuPositioner,LinkMenuPositioner,HyperlinkToolbarPositioneronly wheneditableSideMenu: only when
editableKey files:
frontend/packages/editor/src/blocknote/core/BlockNoteEditor.tsfrontend/packages/editor/src/blocknote/core/extensions/Blocks/nodes/BlockNode.tsfrontend/packages/editor/src/blocknote/core/extensions/Markdown/MarkdownExtension.tsfrontend/packages/editor/src/editor-view.tsxfrontend/packages/editor/src/math.tsxfrontend/packages/editor/src/mentions-plugin.tsxfrontend/packages/editor/src/media-container.tsx
Verify: Render editor with
editable: falsecontaining math, images, mentions. No interactive controls, no markdown shortcuts fire, no toolbars.pnpm -C frontend typecheck.
Phase 2: Editor Extensions (parallel, separate PRs)
All follow the pattern: *Plugin.ts (ProseMirror plugin) + *Positioner.tsx (React UI). Registered in BlockNoteEditor.ts. Reference patterns: SideMenuPlugin.ts, FormattingToolbarPlugin.ts.
2a. Block Highlighting
Highlight block from URL
#blockId, yellow highlights for citation rangesProseMirror decoration plugin adding CSS classes
Source:
blocks-content.tsxfocusBlockId+ highlight logicNew:
editor/src/extensions/BlockHighlight/BlockHighlightPlugin.ts
2b. Image Gallery
Double-click image when
!editable→ full-screen overlay with keyboard/swipe navSource:
blocks-content.tsxImageGalleryProvider(lines 163-320)Reuse:
collectImageBlocks,resolveGalleryNavigation,resolveSwipeDirection(already exported)New:
editor/src/extensions/ImageGallery/ImageGalleryPlugin.ts,react/ImageGallery/ImageGalleryOverlay.tsxModify:
image.tsx(double-click handler when!isEditable)
2c. Block Hover Actions
Hover block → floating card positioned top-right of block: copy block link, start comment
Mouse tracking +
editor.view.posAtCoords()→ emit to React positionerSource:
blocks-content.tsxBlockNodeContenthover card (lines 550-700)Version-aware links: In edit mode, block hover uses
publishedVersionfor existing blocks (blocks that existed before editing started). For new blocks (added during current edit session), copy link/reference uses path + blockRef without version. Editor emits a genericonBlockActionevent; the app resolves the correct version.New:
editor/src/extensions/BlockHoverActions/BlockHoverActionsPlugin.ts,react/BlockHoverActions/BlockHoverActionsPositioner.tsxContext needed:
resourceId,publishedVersion,onBlockActioncallback
2d. Range Selection / Citation Bubble
Select text (click-drag) when
!editable→ bubble with "cite" and "comment" actionsActive only when
!isEditableIn edit mode: click-drag selects text normally (formatting toolbar), does NOT trigger citation bubble
Source:
blocks-content.tsxrange selection,useRangeSelectionfrom@shm/sharedNew:
editor/src/extensions/RangeSelection/RangeSelectionPlugin.ts,react/RangeSelection/RangeSelectionPositioner.tsx
2e. Supernumbers
Positioned top-right of block (near/combined with hover actions area)
Shows
citations + commentscount as a small badge/numberVisible in ALL renderTypes and ALL editable states when count > 0, hidden when 0
Click emits
onSupernumberClick({ blockId })event — editor does NOT decide what happens, the consuming app does (desktop: opens right panel focused on block)Data source:
blockCitationsrecord passed via editor options/context (same as currentblocks-content.tsxline 133)New:
editor/src/extensions/Supernumbers/SupernumbersPlugin.ts,react/Supernumbers/SupernumbersPositioner.tsxVerify each: Render published document with editor
editable: false. Feature works.pnpm -C frontend typecheck. Visual comparison with current BlocksContent.
Phase 3: Renderer Swap
Goal: Replace <BlocksContent> with editor (editable: false, renderType: 'document') in resource-page-common.tsx.
What to do
Add
@shm/editoras dependency of@shm/ui(frontend/packages/ui/package.json)Create
ReadOnlyEditorcomponent in@shm/ui:useBlockNote({ editable: false, renderType: 'document', blockSchema: hmBlockSchema })Convert blocks via
hmBlocksToEditorContent()Populate via
editor.replaceBlocks()on mount / when blocks changeRender
<BlockNoteView>— no toolbars (suppressed byeditable: false)
In
resource-page-common.tsx→ContentViewWithOutline: swap<BlocksContentProvider><BlocksContent>→<ReadOnlyEditor>Keep
BlocksContentfor other consumers (comments, previews) — remove only from document viewMove
setGroupTypes()fromdesktop/src/models/editor-utils.tsto shared locationKey files:
frontend/packages/ui/package.jsonfrontend/packages/ui/src/read-only-editor.tsx(NEW)frontend/packages/ui/src/resource-page-common.tsx@seed-hypermedia/client/hmblock-to-editorblock
Verify: Open any published document → renders via editor. All block types correct. All 5 extensions work. SSR works on web.
pnpm -C frontend typecheck.
Phase 4: In-Place Editing
Goal: Click on text block → editor becomes editable. Wire machine editing states. Handle account switching.
4a. Click-to-edit behavior
Text blocks only: Single click on paragraph/heading/code-block places cursor → sends
edit.startto machine →editabletoggles totrue, toolbars appearBottom of editor: Click on empty area below last block → same behavior (creates empty paragraph, enters edit mode)
Non-text blocks (image, video, embed): single click does NOT trigger edit mode
Click-drag (text selection): does NOT trigger edit mode — shows range selection/citation bubble instead
Implementation: ProseMirror plugin that intercepts click events on text nodes, checks if
!editable && canEdit, then sendsedit.start
4b. Draft auto-load
When navigating to a document where user has edit access:
Query
findByEditfor existing draftIf draft exists → pass
existingDraftId→ machine auto-transitionsloaded → editing→ show draft content in edit modeIf no draft → show published content in read-only (user clicks to edit)
4c. Machine wiring
useDocumentEditorhook in desktop app:Wraps
useBlockNotewith machine event wiringonEditorContentChange→actorRef.send({type: 'change'})Toggle
editor.isEditablebased onselectIsEditingShow/hide toolbars based on editing state
Create
writeDraftactor factory (extract fromdocuments.ts:611-652)Provide machine via
documentMachine.provide({actors: {writeDraft}})indesktop-resource.tsxWire navigation guard for unsaved changes
4d. Account switching
Add
capability.changedevent to document machineuseSelectedAccountCapability()already reacts to account changesWhen capability changes:
Editor → non-editor: Machine receives
capability.changed { canEdit: false }→ if ineditingstate: auto-save draft, transition toloaded, seteditable: falseNon-editor → editor: Machine receives
capability.changed { canEdit: true }→ updatecanEditin context, user can now click-to-edit (or auto-enter editing if draft exists)
Key files:
frontend/apps/desktop/src/pages/desktop-resource.tsxfrontend/apps/desktop/src/models/documents.ts(lines 479, 611-652)frontend/packages/shared/src/models/document-machine.tsfrontend/packages/shared/src/models/capabilities.ts
Verify:
Click text block → editor becomes editable, toolbars appear, cursor placed
Click image → nothing (stays read-only)
Click-drag to select → range selection bubble, NOT edit mode
Click bottom empty area → enters edit mode
Type → autosave creates draft
Navigate to doc with existing draft + edit access → auto-editing with draft content
Switch account to non-editor while editing → draft saved, exits edit mode
Switch account to editor → can click-to-edit again
Old
/draftpage still works as fallback
Phase 5: Publishing via Machine
Goal: publish.start → publishing.inProgress → cleaningUp → loaded.
What to do
Extract publish pipeline from
publish-draft-button.tsxintopublishDocumentactorProvide via
.provide()indesktop-resource.tsxUpdate publish button to read machine state + send
publish.startcontext.depsused asbaseVersionKey files:
frontend/apps/desktop/src/publish-draft-button.tsxfrontend/apps/desktop/src/pages/desktop-resource.tsx
Verify: Full flow: view → click to edit → change → save → publish → back to loaded. Publish error → editing, draft intact. Parent auto-link + push to peers work.
Phase 6: Cleanup
Goal: Remove old code paths.
Redirect
/draft/:idroutes to document routes with editing stateRemove
draft-machine.ts,draft.tsx,useDraftEditorRemove
BlocksContentfrom document view (keep for comments/previews if still needed)Remove the three planning docs, replace with this single macro plan
Verify: No references to old draft machine. All existing tests pass. pnpm -C frontend typecheck.
Phase 7 (Later): Collapsed Blocks Extension
Collapse/expand block children (headings, nested content)
Source:
blocks-content.tsxcollapsedBlocksstate (lines 344-355, 967-978)ProseMirror plugin managing collapsed state set + decorations
Not blocking any other phase
PR Strategy
Phase 1: 1 PR (renderType + editable + readOnly fixes + markdown guards)
Phase 2: 5 separate PRs/commits (one per extension, independent)
Phase 3: 1 PR (renderer swap)
Phase 4: 1 PR (click-to-edit + machine wiring + account switching)
Phase 5: 1 PR (publishing)
Phase 6: 1 PR (cleanup)
Supersedes
This plan merges and replaces:
docs/plans/editor-big-plan.md(Phases 1-4 done, 5-8 absorbed here)docs/plans/plan-editor-document-features.md(extensions + EditorMode absorbed here)docs/plans/plan-editor-migration-steps.md(steps 1-10 absorbed here)
Those docs can be removed in Phase 6 cleanup.
Key Reference Files
Do you like what you are reading? Subscribe to receive updates.
Unsubscribe anytime