Guides

Authors and comments

Mark who wrote which text and let readers discuss selections in threads, with a margin or inline layout.

Both features belong to the advanced editor (not the simple one) and need a collaborative surface for the user's identity: :collaboration="{ ydoc, provider, user: { id, name, color } }" (AuthorIdentity). Entries: @skaile/forge-editor/authors/vue or /authors/react (the framework-free /authors and /authors/schema hold the model), @skaile/forge-editor/comments/vue or /comments/react (framework-free /comments, /comments/schema); CSS styles/authors.css and styles/comments.css (after core.css; the Nuxt module adds them).

import { authorsFeature } from '@skaile/forge-editor/authors/vue'
import { collabFeature } from '@skaile/forge-editor/collab/vue'
import { commentsFeature } from '@skaile/forge-editor/comments/vue'
const preset = defineEditor({ features: [coreContentFeature(), coreUiFeature(), collabFeature(), authorsFeature(), commentsFeature()] })

On React the same list comes from /authors/react, /collab/react and /comments/react, and collaboration is a prop of the same name.

authorsFeature() (no dependency, inert without a user)

  • Every inserted text run carries an author mark { id, name, color } (persisted, invisible, not in Markdown). Text typed inside someone else's run takes the typist's mark; remote edits and Yjs undo/redo keep the original author. documentAuthors(doc) lists the authors with their character counts; currentAuthor(editor) is the user the editor writes as.
  • The colouring mode is a per-user preference: editing.authorColors (off by default) and editing.authorColorStyle ('background' tint or 'text' colour). It is set in the „Autorenfarben“ settings tab and in the toolbar's palette button, whose popover also holds a legend of the document's authors (AuthorColorsPanel).

commentsFeature({ model? }) (requires collab unless model is given)

  • model: (getEditor) => CommentsModel backs the comments with the host's own data (REST threads, a database) instead of the Y.Doc: the feature then requires no collab, the Comments plugin reads the user, the thread ids and the ranges from model.state (a snapshot with available, canComment, user, threads, ranges, …), and useComments(editor) resolves the model of the editor's surface (commentsModelOf(ctx); the factory runs once for the plugin and once per hook consumer, so share state outside it). CommentsSnapshot.canComment gates the selection bubble: the default model says available and editable, a host model may allow readers. CommentsModel, CommentsSnapshot, CommentsActions, DocRange are exported from /comments.
  • Select text and choose „Kommentar“ in the selection bubble; others reply, anyone resolves or reopens a thread („Erledigt“ threads lose their highlight), authors edit or delete their own comments. A thread whose text was deleted stays as orphan („Textstelle gelöscht“). Threads live in the Y.Doc map comments (createCommentsModel(getEditor) is the framework-free model; useComments(editor) is the thin Vue hook, returning the CommentsStore, and the React binding has the same hook), the range is the comment mark (the schema mark is always registered, so stored marks round-trip even without the feature's UI).
  • editing.commentsLayout ('margin' default, 'inline') is a per-user setting in the „Kommentare“ settings tab:
    • margin („Randspalte“): cards beside the commented text. The host places CommentsMargin (props editor, container, open) inside the editor's scroll container (relative flex, editor wrapper min-w-0 flex-1) and binds v-model:comments-open on the surface (React: commentsOpen/onCommentsOpenChange). The toolbar's „Kommentare“ button and Mod-Shift-m toggle it; opening it closes the AI sidebar and vice versa.
    • Layout „Aktiver Faden“: the active thread (or the draft composer) is pinned exactly to the top of its text's line and shows in full; every other open thread is a one-line card (author, excerpt, reply count) that moves up or down out of its way, as close to its own line as its neighbours allow, never overlapping. Without an active thread the cards stack top-down at their lines. A click on a card activates it and reveals its text. Resolved threads are hidden behind the „Erledigt (n)“ toggle at the bottom of the column. A thread whose text was deleted stays as a greyed-out card at the deletion point, labelled „Text gelöscht“ (its position comes from the thread's anchor, a Yjs relative position kept in the Y.Map; a thread without one goes to the end); undoing the deletion attaches it to its text again. The rule is framework-free and testable on its own: layoutMargin(entries, activeId, { gap, minTop }) maps measured cards (MarginEntry: id, line top or null, height) to tops, marginCards(...) builds the cards in document order, and createMarginController(source) does the measuring (coordsAtPos, card heights, one layout per animation frame); both CommentsMargin components only render it (all from @skaile/forge-editor/comments).
    • inline: CommentsPopover (mounted by the feature as an overlay) opens at the commented text; the toolbar's CommentsListMenu lists all threads with the open count. Esc closes the popover.
  • Without collaboration the toolbar button is disabled with an explanation. Switching the layout closes any open thread or draft.
<div ref="scrollEl" class="relative flex overflow-auto">
  <div class="min-w-0 flex-1">
    <AdvancedEditor v-model:comments-open="commentsOpen" :collaboration="{ ydoc, provider, user: { id, name, color } }" />
  </div>
  <CommentsMargin :editor="editor" :container="scrollEl" :open="commentsOpen" />
</div>

React, same layout:

<main ref={setScrollEl} className="relative flex overflow-auto">
  <div className="min-w-0 flex-1">
    <AdvancedEditor commentsOpen={commentsOpen} onCommentsOpenChange={setCommentsOpen} collaboration={{ ydoc, provider, user: { id, name, color } }} />
  </div>
  <CommentsMargin editor={editor} container={scrollEl} open={commentsOpen} />
</main>

bun run check:editor-entries proves the isolation with the authors and comments fixtures; the marks are part of the server schema snapshot.

Copyright © 2026