@matops/editor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +277 -0
- package/dist/index.d.ts +1140 -0
- package/dist/index.esm.js +9067 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +9067 -0
- package/dist/index.js.map +1 -0
- package/dist/style.css +4710 -0
- package/package.json +99 -0
package/README.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# @matops/editor
|
|
2
|
+
|
|
3
|
+
> Production-grade, modular rich text editor for the Matops ecosystem.
|
|
4
|
+
> Built on **Tiptap v3** · **React 18** · **TypeScript 5**
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
`@matops/editor` is a **fully standalone, backend-agnostic** editor package.
|
|
11
|
+
It exposes a clean props interface and connects to your infrastructure via callbacks — zero backend code inside.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
<Editor
|
|
15
|
+
features={["formatting", "code", "tables", "media"]}
|
|
16
|
+
theme="dark"
|
|
17
|
+
editable={true}
|
|
18
|
+
onChange={handleChange}
|
|
19
|
+
onPublish={handlePublish}
|
|
20
|
+
onFileUploaded={handleFileUpload}
|
|
21
|
+
/>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# In a monorepo — reference the workspace package:
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@matops/editor": "workspace:*"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Or as a private npm package:
|
|
35
|
+
npm install @matops/editor
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Requirements:** Node ≥ 18, React ≥ 18.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
Enable capabilities declaratively via the `features` prop:
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
<Editor
|
|
48
|
+
features={[
|
|
49
|
+
"history", // Undo / redo
|
|
50
|
+
"formatting", // Bold, italic, headings, lists, blockquote…
|
|
51
|
+
"code", // Inline code + syntax-highlighted code blocks
|
|
52
|
+
"tables", // Full table editing
|
|
53
|
+
"media", // Images + links
|
|
54
|
+
"task-list", // Checkbox to-do lists
|
|
55
|
+
"placeholder", // Empty-state placeholder text
|
|
56
|
+
"character-count",// Live character / word count bar
|
|
57
|
+
"find-replace", // Find & replace panel (Ctrl+F)
|
|
58
|
+
"cover-image", // Full-width cover photo
|
|
59
|
+
"page-header", // Cover image + title + separator
|
|
60
|
+
"attachments", // File attachment list
|
|
61
|
+
"collaboration", // Real-time Yjs collaboration
|
|
62
|
+
]}
|
|
63
|
+
/>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Automatic Dependency Resolution
|
|
67
|
+
|
|
68
|
+
The **Feature Registry** resolves dependencies transparently:
|
|
69
|
+
|
|
70
|
+
| Feature | Requires |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `tables` | `formatting` |
|
|
73
|
+
| `code` | `formatting` |
|
|
74
|
+
| `collaboration` | disables `history` (Yjs handles undo) |
|
|
75
|
+
|
|
76
|
+
You never need to manually manage the dependency tree.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Props
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
interface EditorProps {
|
|
84
|
+
features?: EditorFeature[]
|
|
85
|
+
initialContent?: JSONContent // body-only — structural nodes injected automatically
|
|
86
|
+
theme?: "light" | "dark"
|
|
87
|
+
editable?: boolean
|
|
88
|
+
placeholder?: string
|
|
89
|
+
collaborationProvider?: CollaborationProvider
|
|
90
|
+
className?: string
|
|
91
|
+
|
|
92
|
+
// Callbacks — all backend integration happens here
|
|
93
|
+
onChange?: (json: JSONContent) => void
|
|
94
|
+
onContentChange?: (html: string, json: JSONContent) => void
|
|
95
|
+
onPublish?: (content: JSONContent) => void
|
|
96
|
+
onFileUploaded?: (file: File) => Promise<string>
|
|
97
|
+
onEditorReady?: (editor: TiptapEditor) => void
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### `initialContent` is body-only
|
|
102
|
+
|
|
103
|
+
You do not need to include `cover-image`, `title`, or `attachments` nodes.
|
|
104
|
+
The Editor injects and manages those structural nodes automatically based on
|
|
105
|
+
the active feature set. Pass only your paragraph / heading / list content —
|
|
106
|
+
or omit `initialContent` entirely for a blank document.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Viewer Mode
|
|
111
|
+
|
|
112
|
+
Render saved content without loading any editing UI:
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
import { EditorViewer } from "@matops/editor";
|
|
116
|
+
|
|
117
|
+
<EditorViewer content={savedJsonContent} theme="light" />
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Uses `@tiptap/html`'s `generateHTML` — works server-side too (SSR / Next.js).
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Collaboration
|
|
125
|
+
|
|
126
|
+
The editor is **100% backend-agnostic**. You bring the Yjs provider:
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
import * as Y from "yjs";
|
|
130
|
+
import { WebsocketProvider } from "y-websocket";
|
|
131
|
+
|
|
132
|
+
const ydoc = new Y.Doc();
|
|
133
|
+
const provider = new WebsocketProvider("ws://your-server", "room-id", ydoc);
|
|
134
|
+
|
|
135
|
+
<Editor
|
|
136
|
+
features={["formatting", "collaboration"]}
|
|
137
|
+
collaborationProvider={{
|
|
138
|
+
provider,
|
|
139
|
+
fragment: ydoc.getXmlFragment("default"),
|
|
140
|
+
user: { name: "Alice", color: "#6366f1" },
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Compatible with `y-websocket`, `@hocuspocus/provider`, and any Yjs-compatible provider.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Custom Feature Registration
|
|
150
|
+
|
|
151
|
+
Extend the editor with your own Tiptap extensions:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { registerFeature } from "@matops/editor";
|
|
155
|
+
import MyCustomExtension from "./MyCustomExtension";
|
|
156
|
+
|
|
157
|
+
registerFeature({
|
|
158
|
+
name: "my-feature", // add to EditorFeature union type in types.ts
|
|
159
|
+
extensions: [MyCustomExtension],
|
|
160
|
+
dependencies: ["formatting"], // pulled in automatically
|
|
161
|
+
enabled: (features) => features.includes("formatting"),
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Content Type Mapping
|
|
168
|
+
|
|
169
|
+
The editor has no opinion about your content model.
|
|
170
|
+
Map features at the application layer:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const CONTENT_TYPE_FEATURES: Record<string, EditorFeature[]> = {
|
|
174
|
+
blog_post: ["history", "formatting", "code", "tables", "media"],
|
|
175
|
+
quick_update: ["history", "formatting"],
|
|
176
|
+
deep_dive: ["history", "formatting", "code", "tables", "media"],
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
<Editor features={CONTENT_TYPE_FEATURES[post.type]} />
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Architecture
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
shared/editor
|
|
188
|
+
├─ core/
|
|
189
|
+
│ ├─ Editor.tsx ← Public <Editor> + internal <EditorCore>
|
|
190
|
+
│ ├─ EditorProvider.tsx ← React context + all hooks
|
|
191
|
+
│ ├─ featureRegistry.ts ← Plugin registry (register / resolve)
|
|
192
|
+
│ ├─ editorConfigBuilder.ts ← Extension resolver + runtime injections
|
|
193
|
+
│ └─ types.ts ← TypeScript types (single source of truth)
|
|
194
|
+
│
|
|
195
|
+
├─ extensions/
|
|
196
|
+
│ ├─ commands/ ← History, placeholder, character-count…
|
|
197
|
+
│ ├─ formatting/ ← Bold, italic, headings, lists…
|
|
198
|
+
│ ├─ media/ ← Images, links
|
|
199
|
+
│ ├─ tables/ ← Table editing
|
|
200
|
+
│ ├─ collaboration/ ← Yjs real-time
|
|
201
|
+
│ ├─ nodes/ ← CoverImageNode, TitleNode, AttachmentsNode…
|
|
202
|
+
│ └─ plugins/ ← Guard plugins, search plugin
|
|
203
|
+
│
|
|
204
|
+
├─ ui/
|
|
205
|
+
│ ├─ toolbar/ ← EditorToolbar, EditorToolbarWithActions
|
|
206
|
+
│ ├─ bubble-menu/ ← EditorBubbleMenu, ImageBubbleMenu
|
|
207
|
+
│ ├─ floating-menu/ ← EditorFloatingMenu
|
|
208
|
+
│ ├─ slash-menu/ ← SlashCommandMenu
|
|
209
|
+
│ ├─ table-menu/ ← EditorTableMenu
|
|
210
|
+
│ ├─ link-menu/ ← LinkEditPopover
|
|
211
|
+
│ ├─ modals/ ← KeyboardShortcutsModal, FindReplacePanel, ExportPanel
|
|
212
|
+
│ ├─ components/ ← CharacterCountBar, TitleSeparator, CropModal
|
|
213
|
+
│ ├─ primitives/ ← MenuButton, MenuDivider, ModalOverlay…
|
|
214
|
+
│ └─ icons/ ← DefaultSeparatorIcon, UploadIcon
|
|
215
|
+
│
|
|
216
|
+
├─ viewer/
|
|
217
|
+
│ └─ EditorViewer.tsx ← Read-only renderer
|
|
218
|
+
│
|
|
219
|
+
└─ utils/
|
|
220
|
+
├─ hooks/ ← useEditorInstance, useEditorKeyboard…
|
|
221
|
+
├─ helpers/ ← cn(), docHelpers, cropHelpers
|
|
222
|
+
└─ constants/ ← DEFAULT_FEATURES, theme classes, data attrs
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Package Versions
|
|
228
|
+
|
|
229
|
+
| Package | Version | Notes |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| `@tiptap/core` | `^3.0.0` | Latest 3.20.1 |
|
|
232
|
+
| `@tiptap/react` | `^3.0.0` | Latest 3.20.1 |
|
|
233
|
+
| `@tiptap/extension-*` | `^3.0.0` | Independent release cadence |
|
|
234
|
+
| `@tiptap/extension-collaboration-caret` | `^3.0.0` | ⚠️ Renamed from `collaboration-cursor` in v3 |
|
|
235
|
+
| `yjs` | `^13.6.27` | — |
|
|
236
|
+
| `y-websocket` | `^3.0.0` | — |
|
|
237
|
+
| `lowlight` | `^3.3.0` | — |
|
|
238
|
+
| `clsx` | `^2.1.1` | — |
|
|
239
|
+
| `tailwind-merge` | `^3.3.0` | — |
|
|
240
|
+
|
|
241
|
+
> **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.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Testing the Registry
|
|
246
|
+
|
|
247
|
+
`FeatureRegistry` exposes `unregister()` and `clear()` for test isolation:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
import { featureRegistry } from "@matops/editor";
|
|
251
|
+
|
|
252
|
+
afterEach(() => featureRegistry.clear());
|
|
253
|
+
|
|
254
|
+
test("custom feature resolves correctly", () => {
|
|
255
|
+
featureRegistry.register({ name: "my-feature", extensions: [] });
|
|
256
|
+
const { resolvedFeatures } = featureRegistry.resolve(["my-feature"]);
|
|
257
|
+
expect(resolvedFeatures).toContain("my-feature");
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Development Roadmap
|
|
264
|
+
|
|
265
|
+
| Phase | Features | Status |
|
|
266
|
+
|---|---|---|
|
|
267
|
+
| **Phase 0** | Core architecture, feature registry, config builder | ✅ Complete |
|
|
268
|
+
| **Phase 1** | Bubble menu, floating menu, slash command | 🔜 Next |
|
|
269
|
+
| **Phase 2** | Character count, word limit, AI placeholder | Planned |
|
|
270
|
+
| **Phase 3** | Full collaboration UI (cursors, presence) | Planned |
|
|
271
|
+
| **Phase 4** | Find & replace, export panel | Planned |
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## License
|
|
276
|
+
|
|
277
|
+
MIT © Matops
|