

## Overview [#overview]

`Editor` uses BlockNote comments for range-based document comments. Cubitt
renders the sidebar and thread cards, while BlockNote owns the comment mark
model, thread store contract, and comment body format. Comments work in both
`blocknote` and `markdown` content modes.

Cubitt owns the editor integration and range marker synchronization. Consumers
own thread storage, user resolution, permissions, persistence, and workflow
state.

```tsx
<Editor.Root
  comments={{
    currentUserId,
    onThreadsChange: ({ content, threads }) =>
      saveEditorState({ markdown: content, threads }),
    resolveUsers,
    threads,
  }}
  content={content}
  contentFormat="markdown"
  onContentChange={({ content }) => saveMarkdown(content)}
>
  <Editor.View />
  <Editor.CommentsSidebar />
</Editor.Root>
```

<Preview name="EditorMarkdownCommentsExample" />

`Editor.CommentsSidebar` renders Cubitt cards for threads, comments, replies,
reactions, and resolve state. `Editor.View` keeps BlockNote's floating comment
composer enabled for creating new comments. If the sidebar is closed on a
narrow layout, pass `floatingThread` to show the same Cubitt thread card as an
in-editor popover when a highlighted range is selected.

```tsx
<Editor.View floatingThread={isCommentsSidebarClosed} />
```

## Thread Snapshots [#thread-snapshots]

`comments.threads` is the persisted thread snapshot. It stores the rich comment
thread data: comments, replies, authors, resolved state, reactions, and
metadata. Cubitt creates the BlockNote `ThreadStore` internally, normalizes
incoming snapshots, and emits comment updates through `comments.onThreadsChange`
with the current canonical content snapshot.

Creating or deleting a thread also updates the hidden range markers in the
document content. Persist the `content` and `threads` from `onThreadsChange` in
one write for comment mutations.

```tsx
const comments = {
  currentUserId,
  onThreadsChange: ({ content, contentFormat, threads }) =>
    saveEditorState({ content, contentFormat, threads }),
  resolveUsers,
  threads,
};

<Editor.Root comments={comments} contentFormat="blocknote">
  <Editor.View />
  <Editor.CommentsSidebar />
</Editor.Root>
```

## Resolve Users [#resolve-users]

`comments.resolveUsers` receives user IDs from the thread data and returns the
display users BlockNote needs for avatars and names.

```tsx
const resolveUsers = async (userIds: string[]) => {
  return userIds.map((id) => usersById[id]);
};
```

## Persistence Model [#persistence-model]

Store document content and thread data separately.

| Data             | Owner            | Notes                                                                        |
| ---------------- | ---------------- | ---------------------------------------------------------------------------- |
| Document content | Consumer         | Stored as BlockNote JSON or Markdown based on `contentFormat`.               |
| Thread data      | Consumer         | Stored as the `comments.threads` snapshot.                                   |
| Comment position | Cubitt + content | Persisted as hidden BlockNote inline marker nodes or Markdown marker tokens. |

For BlockNote JSON content, comment positions are stored as hidden inline
marker nodes in the block content. For Markdown content, comment positions are
stored as range marker tokens:

```md
Text before §{comment-start:thread_123}§commented text§{comment-end:thread_123}§ text after.
```

`Editor` hides those tokens while editing and converts them to BlockNote range
markers internally. Consumers should persist the emitted Markdown as-is so the
range can be restored on the next load.

When Markdown needs to render outside `Editor` in a surface that does not
understand Cubitt inline tokens, use `stripEditorInlineTokens` first:

```tsx
import { stripEditorInlineTokens } from "@tilt-legal/cubitt-components/editor";

const displayMarkdown = stripEditorInlineTokens(markdown);
const displayWithoutComments = stripEditorInlineTokens(markdown, {
  key: "comment",
});
```

By default the helper removes every `§{...}§` marker. Pass `key` to strip only
matching token namespaces, such as `comment` or `ref`.

## Simple Mode [#simple-mode]

Use `comments.features` to turn off BlockNote comment features that are not
needed for a surface.

```tsx
<Editor.Root
  comments={{
    features: {
      reactions: false,
      replies: false,
      resolve: false,
    },
    currentUserId,
    onThreadsChange: ({ content, threads }) =>
      saveEditorState({ markdown: content, threads }),
    resolveUsers,
    threads,
  }}
  content={markdown}
  contentFormat="markdown"
>
  <Editor.View />
  <Editor.CommentsSidebar />
</Editor.Root>
```

Simple mode keeps edit and delete behavior available for the current user,
while hiding or disabling replies, reactions, and resolve controls.

<Preview name="EditorSimpleCommentsExample" />

## Disabling Comments [#disabling-comments]

Omit `comments` to disable comments entirely. `Editor.CommentsSidebar` returns
`null` when comments are disabled, so the surrounding page layout can remove
the sidebar column.

```tsx
<Editor.Root content={markdown} contentFormat="markdown">
  <Editor.View />
  <Editor.CommentsSidebar />
</Editor.Root>
```

<Preview name="EditorCommentsDisabledExample" />

## API Reference [#api-reference]

### stripEditorInlineTokens [#stripeditorinlinetokens]

```ts
function stripEditorInlineTokens(
  value: string,
  options?: { key?: string | readonly string[] }
): string;
```

Removes Cubitt inline token markers from Markdown. Without `key`, every
`§{...}§` token is removed. With `key`, only exact, colon-prefixed, or
hyphen-prefixed token namespaces are removed.

### EditorCommentsConfig [#editorcommentsconfig]

| Field             | Type                                           | Description                                                                                                                                     |
| ----------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `currentUserId`   | `string`                                       | Current comment author id.                                                                                                                      |
| `threads`         | `EditorCommentThreadData[]`                    | Persisted thread snapshot. Date fields may be `Date`, ISO strings, or timestamps.                                                               |
| `onThreadsChange` | `(event: EditorCommentsChangeEvent) => void`   | Emits `{ contentFormat, content, threads }` after comment edits. Persist this payload together so comment markers and thread data stay in sync. |
| `resolveUsers`    | `(userIds: string[]) => Promise<EditorUser[]>` | Resolves user IDs to BlockNote comment users.                                                                                                   |
| `role`            | `"editor" \| "comment"`                        | Optional BlockNote permission role for the current user.                                                                                        |
| `features`        | `EditorCommentFeatures`                        | Optional feature switches for simple comment surfaces.                                                                                          |

### Editor.CommentsSidebar [#editorcommentssidebar]

| Prop                        | Type                                          | Default      | Description                                                                    |
| --------------------------- | --------------------------------------------- | ------------ | ------------------------------------------------------------------------------ |
| `filter`                    | `"open" \| "resolved" \| "all"`               | `"all"`      | Filters visible threads.                                                       |
| `sort`                      | `"position" \| "recent-activity" \| "oldest"` | `"position"` | Orders threads by document position, recent activity, or oldest creation date. |
| `maxCommentsBeforeCollapse` | `number`                                      | `5`          | Collapses long reply chains until the card is selected or expanded.            |

### Editor.View Comment Prop [#editorview-comment-prop]

| Prop             | Type      | Default | Description                                                                                              |
| ---------------- | --------- | ------- | -------------------------------------------------------------------------------------------------------- |
| `floatingThread` | `boolean` | `false` | Shows a Cubitt floating thread card for the selected comment range. Use this when the sidebar is closed. |

### EditorCommentFeatures [#editorcommentfeatures]

| Field       | Type      | Default | Description                            |
| ----------- | --------- | ------- | -------------------------------------- |
| `replies`   | `boolean` | `true`  | Enables reply composition.             |
| `reactions` | `boolean` | `true`  | Enables emoji reactions.               |
| `resolve`   | `boolean` | `true`  | Enables resolve and unresolve actions. |
