npm package: @matops/editor
Description: Production-grade modular rich text editor for the Matops ecosystem
Latest version: 0.1.0

# @matops/editor

> Production-grade, modular rich text editor for the Matops ecosystem.  
> Built on **Tiptap v3** · **React 18** · **TypeScript 5**

---

## Overview

`@matops/editor` is a **fully standalone, backend-agnostic** editor package.  
It exposes a clean props interface and connects to your infrastructure via callbacks — zero backend code inside.

```tsx
<Editor
  features={["formatting", "code", "tables", "media"]}
  theme="dark"
  editable={true}
  onChange={handleChange}
  onPublish={handlePublish}
  onFileUploaded={handleFileUpload}
/>
```

---

## Installation

```bash
# In a monorepo — reference the workspace package:
"dependencies": {
  "@matops/editor": "workspace:*"
}

# Or as a private npm package:
npm install @matops/editor
```

**Requirements:** Node ≥ 18, React ≥ 18.

---

## Features

Enable capabilities declaratively via the `features` prop:

```tsx
<Editor
  features={[
    "history",        // Undo / redo
    "formatting",     // Bold, italic, headings, lists, blockquote…
    "code",           // Inline code + syntax-highlighted code blocks
    "tables",         // Full table editing
    "media",          // Images + links
    "task-list",      // Checkbox to-do lists
    "placeholder",    // Empty-state placeholder text
    "character-count",// Live character / word count bar
    "find-replace",   // Find & replace panel (Ctrl+F)
    "cover-image",    // Full-width cover photo
    "page-header",    // Cover image + title + separator
    "attachments",    // File attachment list
    "collaboration",  // Real-time Yjs collaboration
  ]}
/>
```

### Automatic Dependency Resolution

The **Feature Registry** resolves dependencies transparently:

| Feature | Requires |
|---|---|
| `tables` | `formatting` |
| `code` | `formatting` |
| `collaboration` | disables `history` (Yjs handles undo) |

You never need to manually manage the dependency tree.

---

## Props

```ts
interface EditorProps {
  features?: EditorFeature[]
  initialContent?: JSONContent     // body-only — structural nodes injected automatically
  theme?: "light" | "dark"
  editable?: boolean
  placeholder?: string
  collaborationProvider?: CollaborationProvider
  className?: string

  // Callbacks — all backend integration happens here
  onChange?: (json: JSONContent) => void
  onContentChange?: (html: string, json: JSONContent) => void
  onPublish?: (content: JSONContent) => void
  onFileUploaded?: (file: File) => Promise<string>
  onEditorReady?: (editor: TiptapEditor) => void
}
```

### `initialContent` is body-only

You do not need to include `cover-image`, `title`, or `attachments` nodes.
The Editor injects and manages those structural nodes automatically based on
the active feature set. Pass only your paragraph / heading / list content —
or omit `initialContent` entirely for a blank document.

---

## Viewer Mode

Render saved content without loading any editing UI:

```tsx
import { EditorViewer } from "@matops/editor";

<EditorViewer content={savedJsonContent} theme="light" />
```

Uses `@tiptap/html`'s `generateHTML` — works server-side too (SSR / Next.js).

---

## Collaboration

The editor is **100% backend-agnostic**. You bring the Yjs provider:

```tsx
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";

const ydoc = new Y.Doc();
const provider = new WebsocketProvider("ws://your-server", "room-id", ydoc);

<Editor
  features={["formatting", "collaboration"]}
  collaborationProvider={{
    provider,
    fragment: ydoc.getXmlFragment("default"),
    user: { name: "Alice", color: "#6366f1" },
  }}
/>
```

Compatible with `y-websocket`, `@hocuspocus/provider`, and any Yjs-compatible provider.

---

## Custom Feature Registration

Extend the editor with your own Tiptap extensions:

```ts
import { registerFeature } from "@matops/editor";
import MyCustomExtension from "./MyCustomExtension";

registerFeature({
  name: "my-feature",           // add to EditorFeature union type in types.ts
  extensions: [MyCustomExtension],
  dependencies: ["formatting"], // pulled in automatically
  enabled: (features) => features.includes("formatting"),
});
```

---

## Content Type Mapping

The editor has no opinion about your content model.
Map features at the application layer:

```ts
const CONTENT_TYPE_FEATURES: Record<string, EditorFeature[]> = {
  blog_post:    ["history", "formatting", "code", "tables", "media"],
  quick_update: ["history", "formatting"],
  deep_dive:    ["history", "formatting", "code", "tables", "media"],
};

<Editor features={CONTENT_TYPE_FEATURES[post.type]} />
```

---

## Architecture

```
shared/editor
 ├─ core/
 │   ├─ Editor.tsx              ← Public <Editor> + internal <EditorCore>
 │   ├─ EditorProvider.tsx      ← React context + all hooks
 │   ├─ featureRegistry.ts      ← Plugin registry (register / resolve)
 │   ├─ editorConfigBuilder.ts  ← Extension resolver + runtime injections
 │   └─ types.ts                ← TypeScript types (single source of truth)
 │
 ├─ extensions/
 │   ├─ commands/               ← History, placeholder, character-count…
 │   ├─ formatting/             ← Bold, italic, headings, lists…
 │   ├─ media/                  ← Images, links
 │   ├─ tables/                 ← Table editing
 │   ├─ collaboration/          ← Yjs real-time
 │   ├─ nodes/                  ← CoverImageNode, TitleNode, AttachmentsNode…
 │   └─ plugins/                ← Guard plugins, search plugin
 │
 ├─ ui/
 │   ├─ toolbar/                ← EditorToolbar, EditorToolbarWithActions
 │   ├─ bubble-menu/            ← EditorBubbleMenu, ImageBubbleMenu
 │   ├─ floating-menu/          ← EditorFloatingMenu
 │   ├─ slash-menu/             ← SlashCommandMenu
 │   ├─ table-menu/             ← EditorTableMenu
 │   ├─ link-menu/              ← LinkEditPopover
 │   ├─ modals/                 ← KeyboardShortcutsModal, FindReplacePanel, ExportPanel
 │   ├─ components/             ← CharacterCountBar, TitleSeparator, CropModal
 │   ├─ primitives/             ← MenuButton, MenuDivider, ModalOverlay…
 │   └─ icons/                  ← DefaultSeparatorIcon, UploadIcon
 │
 ├─ viewer/
 │   └─ EditorViewer.tsx        ← Read-only renderer
 │
 └─ utils/
     ├─ hooks/                  ← useEditorInstance, useEditorKeyboard…
     ├─ helpers/                ← cn(), docHelpers, cropHelpers
     └─ constants/              ← DEFAULT_FEATURES, theme classes, data attrs
```

---

## Package Versions

| Package | Version | Notes |
|---|---|---|
| `@tiptap/core` | `^3.0.0` | Latest 3.20.1 |
| `@tiptap/react` | `^3.0.0` | Latest 3.20.1 |
| `@tiptap/extension-*` | `^3.0.0` | Independent release cadence |
| `@tiptap/extension-collaboration-caret` | `^3.0.0` | ⚠️ Renamed from `collaboration-cursor` in v3 |
| `yjs` | `^13.6.27` | — |
| `y-websocket` | `^3.0.0` | — |
| `lowlight` | `^3.3.0` | — |
| `clsx` | `^2.1.1` | — |
| `tailwind-merge` | `^3.3.0` | — |

> **Why `^3.0.0` and not `^3.20.1`?** Tiptap core and individual extensions publish independently. Pinning to `^3.20.1` fails when any extension is still at `3.10.x`. The `^3.0.0` range resolves safely to the latest `3.x` for each package.

---

## Testing the Registry

`FeatureRegistry` exposes `unregister()` and `clear()` for test isolation:

```ts
import { featureRegistry } from "@matops/editor";

afterEach(() => featureRegistry.clear());

test("custom feature resolves correctly", () => {
  featureRegistry.register({ name: "my-feature", extensions: [] });
  const { resolvedFeatures } = featureRegistry.resolve(["my-feature"]);
  expect(resolvedFeatures).toContain("my-feature");
});
```

---

## Development Roadmap

| Phase | Features | Status |
|---|---|---|
| **Phase 0** | Core architecture, feature registry, config builder | ✅ Complete |
| **Phase 1** | Bubble menu, floating menu, slash command | 🔜 Next |
| **Phase 2** | Character count, word limit, AI placeholder | Planned |
| **Phase 3** | Full collaboration UI (cursors, presence) | Planned |
| **Phase 4** | Find & replace, export panel | Planned |

---

## License

MIT © Matops