Skip to content

Comments & Annotations

Editu provides a comment/annotation system that lets you add comments to specific text selections in the editor.

Overview

Comments enable:

  • Text annotation — add comments to any text selection
  • Reply threads — discuss specific passages
  • Resolve/reopen — track comment status
  • Storage flexibility — use localStorage or a custom backend
  • Active highlighting — click a comment to highlight its text

Quick Start

React

tsx
import { useEdituEditor, useEdituComment, EdituProvider, EdituEditor } from "@editu/react";

function Editor() {
  const editor = useEdituEditor({
    features: { comment: true },
  });
  const { comments, addComment, resolveComment, removeComment, setActiveComment } =
    useEdituComment(() => editor, {
      key: "my-doc-comments",
    });

  const handleAddComment = () => {
    const text = prompt("Enter comment:");
    if (text) addComment(text, "Author");
  };

  return (
    <EdituProvider editor={editor}>
      <EdituEditor />
      <button onClick={handleAddComment}>Add Comment</button>
      <ul>
        {comments.map((c) => (
          <li key={c.id} onClick={() => setActiveComment(c.id)}>
            {c.text} {c.resolved ? "(resolved)" : ""}
            <button onClick={() => resolveComment(c.id)}>Resolve</button>
            <button onClick={() => removeComment(c.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </EdituProvider>
  );
}

Enabling the Feature

The comment feature is disabled by default. You can enable it via features.comment:

typescript
const editor = useEdituEditor({
  features: {
    comment: true, // Enable with defaults
  },
});

// Or with options
const editor = useEdituEditor({
  features: {
    comment: {
      onCommentClick: (commentId) => {
        console.log("Clicked comment:", commentId);
      },
    },
  },
});

Comment Options

typescript
interface EdituCommentOptions {
  /** Enable comments (default: true) */
  enabled?: boolean;
  /** Storage backend (default: 'localStorage') */
  storage?: EdituCommentStorage;
  /** Storage key for localStorage (default: 'editu-comments') */
  key?: string;
  /** Callback when a comment is added */
  onAdd?: (comment: EdituComment) => void;
  /** Callback when a comment is removed */
  onRemove?: (commentId: string) => void;
  /** Callback when a comment is resolved */
  onResolve?: (comment: EdituComment) => void;
  /** Callback when a comment is reopened */
  onReopen?: (comment: EdituComment) => void;
  /** Callback when an error occurs */
  onError?: (error: Error) => void;
}

Custom Storage Backend

typescript
useEdituComment(() => editor, {
  storage: {
    save: async (comments) => {
      await fetch("/api/comments", {
        method: "PUT",
        body: JSON.stringify(comments),
      });
    },
    load: async () => {
      const res = await fetch("/api/comments");
      return res.json();
    },
  },
});

Data Model

Comment

typescript
interface EdituComment {
  /** Unique identifier */
  id: string;
  /** Comment text */
  text: string;
  /** Optional author name */
  author?: string;
  /** Unix timestamp (milliseconds) */
  createdAt: number;
  /** Whether the comment is resolved */
  resolved: boolean;
  /** Replies to this comment */
  replies: EdituCommentReply[];
}

Reply

typescript
interface EdituCommentReply {
  /** Unique identifier */
  id: string;
  /** Reply text */
  text: string;
  /** Optional author name */
  author?: string;
  /** Unix timestamp (milliseconds) */
  createdAt: number;
}

API Reference

Hook / Composable / Rune

FrameworkFunction
ReactuseEdituComment(getEditor, options)

Return Values

PropertyTypeDescription
commentsEdituComment[]All stored comments (newest first)
activeCommentIdstring | nullCurrently active comment ID
isLoadingbooleanWhether comments are loading
errorError | nullLast error that occurred
addComment(text, author?)Promise<EdituComment | null>Add a comment to the selection
removeComment(id)Promise<void>Remove a comment and its mark
resolveComment(id)Promise<boolean>Mark a comment as resolved
reopenComment(id)Promise<boolean>Reopen a resolved comment
replyToComment(id, text, author?)Promise<EdituCommentReply | null>Add a reply
setActiveComment(id)voidSet the active comment
loadComments()Promise<EdituComment[]>Reload from storage
getCommentById(id)EdituComment | undefinedGet a comment by ID

Styling

Comment highlights use these CSS classes:

ClassDescription
.editu-comment-markerBase highlight for commented text
.editu-comment-marker--activeHighlight for the active comment

CSS Custom Properties

css
:root {
  /* Comment highlight */
  --editu-comment-bg: rgba(255, 212, 100, 0.3);
  --editu-comment-border: rgba(255, 180, 50, 0.6);
  --editu-comment-hover-bg: rgba(255, 212, 100, 0.5);

  /* Active comment */
  --editu-comment-active-bg: rgba(255, 180, 50, 0.5);
  --editu-comment-active-border: rgba(255, 150, 0, 0.8);
  --editu-comment-active-outline: rgba(255, 150, 0, 0.4);
}

Core Handlers

For advanced use cases, you can use the core handlers directly:

typescript
import {
  createEdituCommentHandlers,
  type EdituCommentState,
} from "@editu/core";

const handlers = createEdituCommentHandlers(
  () => editor,
  { key: "my-comments" },
  (state: Partial<EdituCommentState>) => {
    // Handle state changes
  }
);

await handlers.addComment("Needs review", "Alice");
await handlers.replyToComment(commentId, "Fixed!", "Bob");
await handlers.resolveComment(commentId);

Released under the MIT License.