@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.
@@ -1,13 +1,13 @@
1
1
  "use client";
2
2
 
3
3
  import type { AnyExtension } from "@tiptap/core";
4
- import Collaboration from "@tiptap/extension-collaboration";
5
- import CollaborationCaret from "@tiptap/extension-collaboration-caret";
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
- * TiptapEditor - Collaborative markdown editor with Y.js sync.
61
- * Must be used within an EditorProvider.
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 TiptapEditor({
64
- initialContent,
65
- placeholder = "Start writing...",
66
- readOnly = false,
67
- autoFocus = false,
68
- className,
69
- contentClassName,
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
- const cursor = document.createElement("span");
110
- cursor.className = "collaboration-cursor";
111
- cursor.style.borderColor = background;
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
- const label = document.createElement("span");
114
- label.className = "collaboration-cursor-label";
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
- cursor.appendChild(label);
120
- return cursor;
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
- return baseExtensions;
127
- }, [fragment, provider]);
128
-
129
- // Initialize Tiptap editor
130
- const editor = useEditor({
131
- extensions,
132
- editable: !readOnly,
133
- autofocus: autoFocus,
134
- editorProps: {
135
- attributes: {
136
- class: cn(
137
- "prose prose-sm sm:prose-base dark:prose-invert max-w-none",
138
- "focus:outline-none",
139
- contentClassName,
140
- ),
141
- "data-placeholder": placeholder,
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
- onUpdate: ({ editor: ed }) => {
145
- onUpdate?.(ed);
146
- },
147
- onSelectionUpdate: ({ editor: ed }) => {
148
- onSelectionUpdate?.(ed);
149
- },
150
- onCreate: ({ editor: ed }) => {
151
- onReady?.(ed);
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
- return (
175
- <div
176
- className={cn(
177
- "relative min-h-[200px] w-full rounded-lg border border-border",
178
- "bg-background",
179
- className,
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
- {/* Editor content */}
188
- <div className="p-4 pt-10">
189
- <EditorContent editor={editor} />
190
- </div>
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
- {/* Placeholder when empty */}
193
- <style>{`
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
- </div>
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