@tangle-network/ui 11.5.0 → 11.6.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/CHANGELOG.md +16 -0
- package/README.md +27 -2
- package/dist/{chunk-7O22XQ4D.js → chunk-PLWRAS5Z.js} +1 -1
- package/dist/{chunk-NZLLDHEG.js → chunk-QGTIBEPX.js} +608 -420
- package/dist/{document-editor-pane-MX3FIZSE.js → document-editor-pane-A3JURPW5.js} +1 -1
- package/dist/{document-editor-pane-C3cYEDWZ.d.ts → document-editor-pane-IAEQn_Dl.d.ts} +8 -4
- package/dist/editor.d.ts +5 -4
- package/dist/editor.js +1 -1
- package/dist/files.d.ts +1 -1
- package/dist/files.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/editor/document-editor-pane.test.tsx +58 -0
- package/src/editor/editor-lazy.test.tsx +102 -0
- package/src/editor/editor-lazy.ts +40 -0
- package/src/editor/editor-loading.tsx +25 -0
- package/src/editor/editor-peers.test.ts +250 -0
- package/src/editor/editor-peers.ts +272 -0
- package/src/editor/editor-provider.test.tsx +226 -0
- package/src/editor/editor-provider.tsx +301 -265
- package/src/editor/editor-toolbar.tsx +1 -1
- package/src/editor/markdown-document-editor.test.tsx +121 -0
- package/src/editor/markdown-document-editor.tsx +114 -82
- package/src/editor/tiptap-editor.test.tsx +228 -0
- package/src/editor/tiptap-editor.tsx +162 -130
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { AnyExtension } from "@tiptap/core";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import { type Editor, EditorContent, useEditor } from "@tiptap/react";
|
|
7
|
-
import StarterKit from "@tiptap/starter-kit";
|
|
8
|
-
import { useEffect, useMemo } from "react";
|
|
4
|
+
import type { Editor } from "@tiptap/react";
|
|
5
|
+
import { type ComponentType, Suspense, useEffect, useMemo } from "react";
|
|
9
6
|
import { cn } from "../lib/utils";
|
|
10
7
|
import { type Collaborator, useEditorContext } from "./editor-provider";
|
|
8
|
+
import { retryableLazyEditor } from "./editor-lazy";
|
|
9
|
+
import { EditorLoadingPlaceholder } from "./editor-loading";
|
|
10
|
+
import { type CollaborationPeers, loadCollaborationPeers } from "./editor-peers";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Props for TiptapEditor component.
|
|
@@ -57,140 +57,149 @@ function getCursorColors(color: string) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
60
|
+
* Builds the collaborative editor against loaded tiptap namespaces. The peers
|
|
61
|
+
* arrive as an argument so that this module reaches them through type-only
|
|
62
|
+
* imports, which a bundler erases.
|
|
62
63
|
*/
|
|
63
|
-
export function
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
onUpdate,
|
|
71
|
-
onSelectionUpdate,
|
|
72
|
-
onReady,
|
|
73
|
-
}: TiptapEditorProps) {
|
|
74
|
-
const { doc, provider, connectionState } = useEditorContext();
|
|
75
|
-
|
|
76
|
-
// Y.js fragment for the editor content
|
|
77
|
-
const fragment = useMemo(() => doc.getXmlFragment("prosemirror"), [doc]);
|
|
78
|
-
|
|
79
|
-
// Configure Tiptap extensions
|
|
80
|
-
const extensions = useMemo(() => {
|
|
81
|
-
const baseExtensions: AnyExtension[] = [
|
|
82
|
-
StarterKit.configure({
|
|
83
|
-
// Disable history - Y.js handles undo/redo
|
|
84
|
-
...({ history: false } as any),
|
|
85
|
-
// Configure code block for syntax highlighting placeholder
|
|
86
|
-
codeBlock: {
|
|
87
|
-
HTMLAttributes: {
|
|
88
|
-
class: "hljs",
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
}),
|
|
92
|
-
Collaboration.configure({
|
|
93
|
-
fragment,
|
|
94
|
-
}),
|
|
95
|
-
];
|
|
96
|
-
|
|
97
|
-
// Add collaboration cursor if provider is available
|
|
98
|
-
if (provider?.awareness) {
|
|
99
|
-
baseExtensions.push(
|
|
100
|
-
CollaborationCaret.configure({
|
|
101
|
-
provider,
|
|
102
|
-
user: provider.awareness.getLocalState()?.user ?? {
|
|
103
|
-
name: "Anonymous",
|
|
104
|
-
color: "#808080",
|
|
105
|
-
},
|
|
106
|
-
render: (user: { name: string; color: string }) => {
|
|
107
|
-
const { background, text } = getCursorColors(user.color);
|
|
64
|
+
export function createTiptapEditor(
|
|
65
|
+
peers: CollaborationPeers,
|
|
66
|
+
): ComponentType<TiptapEditorProps> {
|
|
67
|
+
const { EditorContent, useEditor } = peers.react;
|
|
68
|
+
const StarterKit = peers.starterKit.default;
|
|
69
|
+
const Collaboration = peers.collaboration.default;
|
|
70
|
+
const CollaborationCaret = peers.collaborationCaret.default;
|
|
108
71
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
72
|
+
return function TiptapEditor({
|
|
73
|
+
initialContent,
|
|
74
|
+
placeholder = "Start writing...",
|
|
75
|
+
readOnly = false,
|
|
76
|
+
autoFocus = false,
|
|
77
|
+
className,
|
|
78
|
+
contentClassName,
|
|
79
|
+
onUpdate,
|
|
80
|
+
onSelectionUpdate,
|
|
81
|
+
onReady,
|
|
82
|
+
}: TiptapEditorProps) {
|
|
83
|
+
const { doc, provider, connectionState } = useEditorContext();
|
|
112
84
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
label.style.backgroundColor = background;
|
|
116
|
-
label.style.color = text;
|
|
117
|
-
label.textContent = user.name;
|
|
85
|
+
// Y.js fragment for the editor content
|
|
86
|
+
const fragment = useMemo(() => doc.getXmlFragment("prosemirror"), [doc]);
|
|
118
87
|
|
|
119
|
-
|
|
120
|
-
|
|
88
|
+
// Configure Tiptap extensions
|
|
89
|
+
const extensions = useMemo(() => {
|
|
90
|
+
const baseExtensions: AnyExtension[] = [
|
|
91
|
+
StarterKit.configure({
|
|
92
|
+
// Disable history - Y.js handles undo/redo
|
|
93
|
+
...({ history: false } as any),
|
|
94
|
+
// Configure code block for syntax highlighting placeholder
|
|
95
|
+
codeBlock: {
|
|
96
|
+
HTMLAttributes: {
|
|
97
|
+
class: "hljs",
|
|
98
|
+
},
|
|
121
99
|
},
|
|
122
100
|
}),
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
101
|
+
Collaboration.configure({
|
|
102
|
+
fragment,
|
|
103
|
+
}),
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
// Add collaboration cursor if provider is available
|
|
107
|
+
if (provider?.awareness) {
|
|
108
|
+
baseExtensions.push(
|
|
109
|
+
CollaborationCaret.configure({
|
|
110
|
+
provider,
|
|
111
|
+
user: provider.awareness.getLocalState()?.user ?? {
|
|
112
|
+
name: "Anonymous",
|
|
113
|
+
color: "#808080",
|
|
114
|
+
},
|
|
115
|
+
render: (user: { name: string; color: string }) => {
|
|
116
|
+
const { background, text } = getCursorColors(user.color);
|
|
117
|
+
|
|
118
|
+
const cursor = document.createElement("span");
|
|
119
|
+
cursor.className = "collaboration-cursor";
|
|
120
|
+
cursor.style.borderColor = background;
|
|
121
|
+
|
|
122
|
+
const label = document.createElement("span");
|
|
123
|
+
label.className = "collaboration-cursor-label";
|
|
124
|
+
label.style.backgroundColor = background;
|
|
125
|
+
label.style.color = text;
|
|
126
|
+
label.textContent = user.name;
|
|
127
|
+
|
|
128
|
+
cursor.appendChild(label);
|
|
129
|
+
return cursor;
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return baseExtensions;
|
|
136
|
+
}, [fragment, provider]);
|
|
137
|
+
|
|
138
|
+
// Initialize Tiptap editor
|
|
139
|
+
const editor = useEditor({
|
|
140
|
+
extensions,
|
|
141
|
+
editable: !readOnly,
|
|
142
|
+
autofocus: autoFocus,
|
|
143
|
+
editorProps: {
|
|
144
|
+
attributes: {
|
|
145
|
+
class: cn(
|
|
146
|
+
"prose prose-sm sm:prose-base dark:prose-invert max-w-none",
|
|
147
|
+
"focus:outline-none",
|
|
148
|
+
contentClassName,
|
|
149
|
+
),
|
|
150
|
+
"data-placeholder": placeholder,
|
|
151
|
+
},
|
|
142
152
|
},
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// Update editable state when readOnly changes
|
|
156
|
-
useEffect(() => {
|
|
157
|
-
if (editor) {
|
|
158
|
-
editor.setEditable(!readOnly);
|
|
159
|
-
}
|
|
160
|
-
}, [editor, readOnly]);
|
|
161
|
-
|
|
162
|
-
// Handle initial content (only for new documents)
|
|
163
|
-
useEffect(() => {
|
|
164
|
-
if (
|
|
165
|
-
editor &&
|
|
166
|
-
initialContent &&
|
|
167
|
-
connectionState === "synced" &&
|
|
168
|
-
editor.isEmpty
|
|
169
|
-
) {
|
|
170
|
-
editor.commands.setContent(initialContent);
|
|
171
|
-
}
|
|
172
|
-
}, [editor, initialContent, connectionState]);
|
|
153
|
+
onUpdate: ({ editor: ed }) => {
|
|
154
|
+
onUpdate?.(ed);
|
|
155
|
+
},
|
|
156
|
+
onSelectionUpdate: ({ editor: ed }) => {
|
|
157
|
+
onSelectionUpdate?.(ed);
|
|
158
|
+
},
|
|
159
|
+
onCreate: ({ editor: ed }) => {
|
|
160
|
+
onReady?.(ed);
|
|
161
|
+
},
|
|
162
|
+
});
|
|
173
163
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
)}
|
|
181
|
-
>
|
|
182
|
-
{/* Connection status indicator */}
|
|
183
|
-
<div className="absolute top-2 right-2 z-10">
|
|
184
|
-
<ConnectionIndicator state={connectionState} />
|
|
185
|
-
</div>
|
|
164
|
+
// Update editable state when readOnly changes
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (editor) {
|
|
167
|
+
editor.setEditable(!readOnly);
|
|
168
|
+
}
|
|
169
|
+
}, [editor, readOnly]);
|
|
186
170
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
171
|
+
// Handle initial content (only for new documents)
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (
|
|
174
|
+
editor &&
|
|
175
|
+
initialContent &&
|
|
176
|
+
connectionState === "synced" &&
|
|
177
|
+
editor.isEmpty
|
|
178
|
+
) {
|
|
179
|
+
editor.commands.setContent(initialContent);
|
|
180
|
+
}
|
|
181
|
+
}, [editor, initialContent, connectionState]);
|
|
191
182
|
|
|
192
|
-
|
|
193
|
-
<
|
|
183
|
+
return (
|
|
184
|
+
<div
|
|
185
|
+
className={cn(
|
|
186
|
+
"relative min-h-[200px] w-full rounded-lg border border-border",
|
|
187
|
+
"bg-background",
|
|
188
|
+
className,
|
|
189
|
+
)}
|
|
190
|
+
>
|
|
191
|
+
{/* Connection status indicator */}
|
|
192
|
+
<div className="absolute top-2 right-2 z-10">
|
|
193
|
+
<ConnectionIndicator state={connectionState} />
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{/* Editor content */}
|
|
197
|
+
<div className="p-4 pt-10">
|
|
198
|
+
<EditorContent editor={editor} />
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{/* Placeholder when empty */}
|
|
202
|
+
<style>{`
|
|
194
203
|
.ProseMirror p.is-editor-empty:first-child::before {
|
|
195
204
|
content: attr(data-placeholder);
|
|
196
205
|
float: left;
|
|
@@ -253,7 +262,30 @@ export function TiptapEditor({
|
|
|
253
262
|
font-size: inherit;
|
|
254
263
|
}
|
|
255
264
|
`}</style>
|
|
256
|
-
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const lazyTiptapEditor = retryableLazyEditor(async () =>
|
|
271
|
+
createTiptapEditor(await loadCollaborationPeers()),
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* TiptapEditor - Collaborative markdown editor with Y.js sync.
|
|
276
|
+
* Must be used within an EditorProvider. Loads its tiptap, Hocuspocus and
|
|
277
|
+
* yjs peers on first render.
|
|
278
|
+
*/
|
|
279
|
+
export function TiptapEditor(props: TiptapEditorProps) {
|
|
280
|
+
const LazyTiptapEditor = lazyTiptapEditor();
|
|
281
|
+
return (
|
|
282
|
+
<Suspense
|
|
283
|
+
fallback={
|
|
284
|
+
<EditorLoadingPlaceholder className={cn("min-h-[200px]", props.className)} />
|
|
285
|
+
}
|
|
286
|
+
>
|
|
287
|
+
<LazyTiptapEditor {...props} />
|
|
288
|
+
</Suspense>
|
|
257
289
|
);
|
|
258
290
|
}
|
|
259
291
|
|