Skip to content

Rich Text Editor

@xnet/editor is a collaborative rich text editor built on TipTap v3 with Yjs for real-time CRDT sync. It provides three entry points:

ImportContents
@xnet/editorCore Editor class (framework-agnostic)
@xnet/editor/reactReact components, hooks, node views
@xnet/editor/extensionsAll TipTap extensions and utilities
import { RichTextEditor } from '@xnet/editor/react'
<RichTextEditor
ydoc={ydoc} // Required: Yjs document
field="content" // Y.XmlFragment field name
placeholder="Start writing..."
awareness={awareness} // Cursor presence
did={myDID} // Local user's DID for cursor color
showToolbar={true}
readOnly={false}
extensions={pluginExtensions} // Additional TipTap extensions
slashCommands={customCommands} // Custom slash commands
onNavigate={(docId) => router.push(`/doc/${docId}`)}
onImageUpload={handleImageUpload}
onFileUpload={handleFileUpload}
onEditorReady={(editor) => { ... }}
/>
BlockDescriptionInput
ParagraphDefault text block
Heading (1-6)Section headings with # syntax preview# , ## , etc.
Code BlockSyntax-highlighted code with language selector```
BlockquoteQuote with > prefix preview>
Bullet ListUnordered list- or *
Ordered ListNumbered list1.
Task ListCheckbox list with nesting[ ]
Horizontal RuleDivider---
ImageCID-based with paste/drop upload, resize handles, alignmentPaste or /image
FileGeneric file attachment with upload progressDrop or /file
EmbedAuto-embed for YouTube, Vimeo, Spotify, Twitter, Figma, CodeSandbox, LoomPaste URL
Callout6 types (info, tip, warning, caution, note, quote), collapsible> [!info]
ToggleCollapsible details/summary sections> [toggle]
Database EmbedInline database views (table/board/list/calendar/gallery/timeline)/database
MermaidDiagram blocks (via plugin)```mermaid

Bold, italic, strikethrough, code, link, wikilink ([[page-name]]), and comment.

Obsidian-style inline markdown syntax rendering — bold markers (**), italic (*), strikethrough (~~), and code (`) are shown as decorations while typing and hidden when the cursor moves away.

Type / to open the command palette. Built-in command groups:

  • Basic Blocks — Text, H1, H2, H3
  • Lists — Bullet, Numbered, Task
  • Blocks — Quote, Code Block, Divider
  • Media — Image, File, Embed
  • Callouts — Info, Tip, Warning, Caution, Note
  • Toggles — Toggle section
  • Data — Database

Plugins can add custom slash commands via contributes.slashCommands.

The toolbar appears on text selection:

  • Desktop — Bubble menu floating near the selection
  • Mobile — Fixed bottom bar, horizontally scrollable

Built-in buttons: Bold, Italic, Strike, Code, Comment | H1, H2, H3 | Bullet, Ordered, Task | Quote, Code Block, Divider.

Plugins can add toolbar buttons via EditorContribution.toolbar.

Three integrated plugins handle block-level drag and drop:

  • DragHandle — Shows a grip icon on hover at the left edge of blocks
  • DragDropPlugin — Handles block reordering via drag
  • DropIndicator — Shows a visual insertion line during drag

The editor binds to a Y.XmlFragment inside the Y.Doc via TipTap’s Collaboration extension. When you pass an awareness instance, cursor presence is shown automatically:

  • Remote cursors rendered as colored carets with DID-derived identicon avatars
  • Selections shown as translucent highlights in the user’s color
  • Hover over a cursor to see the user label

Comments are positioned using Yjs relative positions (captureTextAnchor / resolveTextAnchor). This means comment positions survive concurrent edits — if someone inserts text before your comment, the anchor moves with the text.

Plugins contribute editor extensions through the contribution system:

// In a plugin manifest
contributes: {
editorExtensions: [{
id: 'my-extension',
extension: MyTipTapExtension,
priority: 100,
toolbar: {
icon: MyIcon,
title: 'My Tool',
group: 'insert',
action: (editor) => editor.chain().focus().insertMyThing().run()
}
}],
slashCommands: [{
id: 'my-command',
name: 'My Block',
execute: ({ editor, range }) => { ... }
}]
}

The Electron app collects plugin extensions via useEditorExtensionsSafe() and passes them to <RichTextEditor extensions={...} />. Plugin extensions are loaded before the editor mounts to prevent crashes when Yjs content contains nodes from unregistered extensions.

URLs pasted into the editor are automatically detected and embedded for 7 services:

ProviderURL Pattern
YouTubeyoutube.com/watch, youtu.be
Vimeovimeo.com
Spotifyopen.spotify.com
Twitter/Xtwitter.com, x.com
Figmafigma.com
CodeSandboxcodesandbox.io
Loomloom.com