@tangle-network/ui 11.4.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 +22 -0
- package/README.md +27 -2
- package/dist/chat.js +3 -3
- package/dist/{chunk-4ORVXSMU.js → chunk-J2G7KCYH.js} +13 -14
- package/dist/{chunk-7O22XQ4D.js → chunk-PLWRAS5Z.js} +1 -1
- package/dist/{chunk-NZLLDHEG.js → chunk-QGTIBEPX.js} +608 -420
- package/dist/{chunk-SRAKCVPO.js → chunk-R5GVQDL3.js} +1 -1
- package/dist/{chunk-IH2VRPGZ.js → chunk-RKC3GXHE.js} +2 -2
- package/dist/{chunk-J3YZIQNI.js → chunk-XPYPZMYT.js} +1 -1
- package/dist/{chunk-HW4ROISY.js → chunk-XROQW5J7.js} +1 -1
- 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/hooks.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +12 -8
- package/dist/run.d.ts +48 -1
- package/dist/run.js +9 -5
- package/dist/sdk-hooks.js +3 -3
- 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
- package/src/run/index.ts +6 -0
- /package/dist/{chunk-47XH56SV.js → chunk-LQS34IGP.js} +0 -0
|
@@ -19,16 +19,12 @@ import { useEffect as useEffect5, useMemo as useMemo4, useState as useState3 } f
|
|
|
19
19
|
import { PencilLine, Save, Users, Wifi, WifiOff } from "lucide-react";
|
|
20
20
|
|
|
21
21
|
// src/editor/tiptap-editor.tsx
|
|
22
|
-
import
|
|
23
|
-
import CollaborationCaret from "@tiptap/extension-collaboration-caret";
|
|
24
|
-
import { EditorContent, useEditor } from "@tiptap/react";
|
|
25
|
-
import StarterKit from "@tiptap/starter-kit";
|
|
26
|
-
import { useEffect as useEffect2, useMemo as useMemo2 } from "react";
|
|
22
|
+
import { Suspense as Suspense2, useEffect as useEffect2, useMemo as useMemo2 } from "react";
|
|
27
23
|
|
|
28
24
|
// src/editor/editor-provider.tsx
|
|
29
|
-
import { HocuspocusProvider } from "@hocuspocus/provider";
|
|
30
25
|
import {
|
|
31
26
|
createContext,
|
|
27
|
+
Suspense,
|
|
32
28
|
useCallback,
|
|
33
29
|
useContext,
|
|
34
30
|
useEffect,
|
|
@@ -36,7 +32,137 @@ import {
|
|
|
36
32
|
useRef,
|
|
37
33
|
useState
|
|
38
34
|
} from "react";
|
|
39
|
-
|
|
35
|
+
|
|
36
|
+
// src/editor/editor-lazy.ts
|
|
37
|
+
import { lazy } from "react";
|
|
38
|
+
|
|
39
|
+
// src/editor/editor-peers.ts
|
|
40
|
+
var DOCUMENT_PEERS_MISSING = "@tangle-network/ui/editor needs its optional editor peers. Install @tiptap/react and @tiptap/starter-kit.";
|
|
41
|
+
var PROVIDER_PEERS_MISSING = "@tangle-network/ui/editor needs its optional collaboration transport peers. Install @hocuspocus/provider and yjs.";
|
|
42
|
+
var COLLABORATION_PEERS_MISSING = "@tangle-network/ui/editor needs its optional collaboration peers. Install @tiptap/react, @tiptap/starter-kit, @tiptap/extension-collaboration, @tiptap/extension-collaboration-caret, @hocuspocus/provider and yjs.";
|
|
43
|
+
var MissingEditorPeersError = class extends Error {
|
|
44
|
+
constructor(message, options) {
|
|
45
|
+
super(message, options);
|
|
46
|
+
this.name = "MissingEditorPeersError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
function isMissingEditorPeersError(error) {
|
|
50
|
+
return error instanceof Error && error.name === "MissingEditorPeersError";
|
|
51
|
+
}
|
|
52
|
+
var RESOLUTION_FAILURE = /could not resolve|cannot find (?:module|package)|can't resolve|failed to resolve|module not found/i;
|
|
53
|
+
function isMissingPeerError(error) {
|
|
54
|
+
return error instanceof Error && RESOLUTION_FAILURE.test(error.message);
|
|
55
|
+
}
|
|
56
|
+
function rethrow(error) {
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
var DEFERRED_PEERS = [
|
|
60
|
+
"@tiptap/core",
|
|
61
|
+
"@tiptap/react",
|
|
62
|
+
"@tiptap/starter-kit",
|
|
63
|
+
"@tiptap/extension-collaboration",
|
|
64
|
+
"@tiptap/extension-collaboration-caret",
|
|
65
|
+
"@hocuspocus/provider",
|
|
66
|
+
"yjs"
|
|
67
|
+
];
|
|
68
|
+
function unresolvedPeersFrom(error) {
|
|
69
|
+
if (!(error instanceof Error)) return [];
|
|
70
|
+
const named = DEFERRED_PEERS.filter((name) => error.message.includes(name));
|
|
71
|
+
return named.filter(
|
|
72
|
+
(name) => !named.some((other) => other !== name && other.includes(name))
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
function asMissingEditorPeersError(error, missingMessage) {
|
|
76
|
+
if (!isMissingPeerError(error)) return error;
|
|
77
|
+
const unresolved = unresolvedPeersFrom(error);
|
|
78
|
+
if (unresolved.length === 0) return error;
|
|
79
|
+
const detail = unresolved.length === 1 ? ` ${unresolved[0]} did not resolve.` : "";
|
|
80
|
+
return new MissingEditorPeersError(missingMessage + detail, { cause: error });
|
|
81
|
+
}
|
|
82
|
+
async function loadPeers(load, missingMessage) {
|
|
83
|
+
try {
|
|
84
|
+
return await load();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw asMissingEditorPeersError(error, missingMessage);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function isConfigurableExtension(value) {
|
|
90
|
+
return typeof value?.configure === "function";
|
|
91
|
+
}
|
|
92
|
+
function assertDocumentEditorPeers(peers, missingMessage) {
|
|
93
|
+
if (typeof peers.react.useEditor !== "function" || peers.react.EditorContent === void 0 || !isConfigurableExtension(peers.starterKit.default)) {
|
|
94
|
+
throw new MissingEditorPeersError(missingMessage);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function assertEditorProviderPeers(peers, missingMessage) {
|
|
98
|
+
if (typeof peers.hocuspocus.HocuspocusProvider !== "function" || typeof peers.yjs.Doc !== "function") {
|
|
99
|
+
throw new MissingEditorPeersError(missingMessage);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function assertCollaborationPeers(peers) {
|
|
103
|
+
assertDocumentEditorPeers(peers, COLLABORATION_PEERS_MISSING);
|
|
104
|
+
assertEditorProviderPeers(peers, COLLABORATION_PEERS_MISSING);
|
|
105
|
+
if (!isConfigurableExtension(peers.collaboration.default) || !isConfigurableExtension(peers.collaborationCaret.default)) {
|
|
106
|
+
throw new MissingEditorPeersError(COLLABORATION_PEERS_MISSING);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function importDocumentEditorPeers() {
|
|
110
|
+
const [react, starterKit] = await Promise.all([
|
|
111
|
+
import("@tiptap/react").catch(rethrow),
|
|
112
|
+
import("@tiptap/starter-kit").catch(rethrow)
|
|
113
|
+
]);
|
|
114
|
+
return { react, starterKit };
|
|
115
|
+
}
|
|
116
|
+
async function importEditorProviderPeers() {
|
|
117
|
+
const [hocuspocus, yjs] = await Promise.all([
|
|
118
|
+
import("@hocuspocus/provider").catch(rethrow),
|
|
119
|
+
import("yjs").catch(rethrow)
|
|
120
|
+
]);
|
|
121
|
+
return { hocuspocus, yjs };
|
|
122
|
+
}
|
|
123
|
+
async function loadEditorProviderPeers() {
|
|
124
|
+
const peers = await loadPeers(importEditorProviderPeers, PROVIDER_PEERS_MISSING);
|
|
125
|
+
assertEditorProviderPeers(peers, PROVIDER_PEERS_MISSING);
|
|
126
|
+
return peers;
|
|
127
|
+
}
|
|
128
|
+
async function loadDocumentEditorPeers() {
|
|
129
|
+
const peers = await loadPeers(importDocumentEditorPeers, DOCUMENT_PEERS_MISSING);
|
|
130
|
+
assertDocumentEditorPeers(peers, DOCUMENT_PEERS_MISSING);
|
|
131
|
+
return peers;
|
|
132
|
+
}
|
|
133
|
+
async function loadCollaborationPeers() {
|
|
134
|
+
const peers = await loadPeers(async () => {
|
|
135
|
+
const [documentPeers, providerPeers, collaboration, collaborationCaret] = await Promise.all([
|
|
136
|
+
importDocumentEditorPeers(),
|
|
137
|
+
importEditorProviderPeers(),
|
|
138
|
+
import("@tiptap/extension-collaboration").catch(rethrow),
|
|
139
|
+
import("@tiptap/extension-collaboration-caret").catch(rethrow)
|
|
140
|
+
]);
|
|
141
|
+
return { ...documentPeers, ...providerPeers, collaboration, collaborationCaret };
|
|
142
|
+
}, COLLABORATION_PEERS_MISSING);
|
|
143
|
+
assertCollaborationPeers(peers);
|
|
144
|
+
return peers;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/editor/editor-lazy.ts
|
|
148
|
+
function retryableLazyEditor(loadComponent) {
|
|
149
|
+
function build() {
|
|
150
|
+
return lazy(async () => {
|
|
151
|
+
try {
|
|
152
|
+
return { default: await loadComponent() };
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (!isMissingEditorPeersError(error)) {
|
|
155
|
+
current = build();
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
let current = build();
|
|
162
|
+
return () => current;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/editor/editor-provider.tsx
|
|
40
166
|
import { jsx } from "react/jsx-runtime";
|
|
41
167
|
var EditorContext = createContext(null);
|
|
42
168
|
function generateUserColor() {
|
|
@@ -64,225 +190,236 @@ function generateUserColor() {
|
|
|
64
190
|
];
|
|
65
191
|
return colors[Math.floor(Math.random() * colors.length)];
|
|
66
192
|
}
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
tokenExpiresAt,
|
|
72
|
-
user,
|
|
73
|
-
autoConnect = true,
|
|
74
|
-
autoReconnect = true,
|
|
75
|
-
maxReconnectAttempts = 5,
|
|
76
|
-
onConnectionChange,
|
|
77
|
-
onSync,
|
|
78
|
-
onAuthError,
|
|
79
|
-
onRefreshToken,
|
|
80
|
-
children
|
|
81
|
-
}) {
|
|
82
|
-
const [connectionState, setConnectionState] = useState("disconnected");
|
|
83
|
-
const [collaborators, setCollaborators] = useState([]);
|
|
84
|
-
const [isSynced, setIsSynced] = useState(false);
|
|
85
|
-
const docRef = useRef(null);
|
|
86
|
-
const providerRef = useRef(null);
|
|
87
|
-
const reconnectAttemptsRef = useRef(0);
|
|
88
|
-
const tokenRef = useRef(token);
|
|
89
|
-
const tokenExpiryRef = useRef(tokenExpiresAt);
|
|
90
|
-
const refreshPromiseRef = useRef(null);
|
|
91
|
-
const refreshTimerRef = useRef(null);
|
|
92
|
-
tokenRef.current = token;
|
|
93
|
-
tokenExpiryRef.current = tokenExpiresAt;
|
|
94
|
-
if (!docRef.current) {
|
|
95
|
-
docRef.current = new Y.Doc();
|
|
96
|
-
}
|
|
97
|
-
const doc = docRef.current;
|
|
98
|
-
const userColor = useMemo(
|
|
99
|
-
() => user.color ?? generateUserColor(),
|
|
100
|
-
[user.color]
|
|
101
|
-
);
|
|
102
|
-
const updateConnectionState = useCallback(
|
|
103
|
-
(state) => {
|
|
104
|
-
setConnectionState(state);
|
|
105
|
-
onConnectionChange?.(state);
|
|
106
|
-
},
|
|
107
|
-
[onConnectionChange]
|
|
108
|
-
);
|
|
109
|
-
const updateCollaborators = useCallback(
|
|
110
|
-
(awareness) => {
|
|
111
|
-
if (!awareness) return;
|
|
112
|
-
const states = awareness.getStates();
|
|
113
|
-
const collabs = [];
|
|
114
|
-
states.forEach((state, clientId) => {
|
|
115
|
-
if (clientId === awareness.clientID) return;
|
|
116
|
-
if (state.user) {
|
|
117
|
-
collabs.push({
|
|
118
|
-
clientId,
|
|
119
|
-
user: state.user
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
setCollaborators(collabs);
|
|
124
|
-
},
|
|
125
|
-
[]
|
|
126
|
-
);
|
|
127
|
-
const clearRefreshTimer = useCallback(() => {
|
|
128
|
-
if (refreshTimerRef.current != null) {
|
|
129
|
-
window.clearTimeout(refreshTimerRef.current);
|
|
130
|
-
refreshTimerRef.current = null;
|
|
131
|
-
}
|
|
132
|
-
}, []);
|
|
133
|
-
const refreshToken = useCallback(async () => {
|
|
134
|
-
if (!onRefreshToken) {
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
if (refreshPromiseRef.current) {
|
|
138
|
-
return refreshPromiseRef.current;
|
|
139
|
-
}
|
|
140
|
-
const refreshPromise = (async () => {
|
|
141
|
-
const next = await onRefreshToken();
|
|
142
|
-
const resolvedToken = typeof next === "string" ? next : next.token;
|
|
143
|
-
const resolvedExpiry = typeof next === "string" ? void 0 : next.expiresAt;
|
|
144
|
-
tokenRef.current = resolvedToken;
|
|
145
|
-
tokenExpiryRef.current = resolvedExpiry;
|
|
146
|
-
return resolvedToken;
|
|
147
|
-
})().catch((error) => {
|
|
148
|
-
onAuthError?.(
|
|
149
|
-
error instanceof Error ? error : new Error(String(error))
|
|
150
|
-
);
|
|
151
|
-
return null;
|
|
152
|
-
}).finally(() => {
|
|
153
|
-
refreshPromiseRef.current = null;
|
|
154
|
-
});
|
|
155
|
-
refreshPromiseRef.current = refreshPromise;
|
|
156
|
-
return refreshPromise;
|
|
157
|
-
}, [onAuthError, onRefreshToken]);
|
|
158
|
-
const scheduleTokenRefresh = useCallback(() => {
|
|
159
|
-
clearRefreshTimer();
|
|
160
|
-
if (!tokenExpiryRef.current || !onRefreshToken || typeof window === "undefined") {
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
const refreshAtMs = tokenExpiryRef.current * 1e3 - 6e4;
|
|
164
|
-
const delay = refreshAtMs - Date.now();
|
|
165
|
-
if (delay <= 0) {
|
|
166
|
-
void refreshToken();
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
refreshTimerRef.current = window.setTimeout(() => {
|
|
170
|
-
void refreshToken();
|
|
171
|
-
}, delay);
|
|
172
|
-
}, [clearRefreshTimer, onRefreshToken, refreshToken]);
|
|
173
|
-
const connect = useCallback(() => {
|
|
174
|
-
if (providerRef.current) {
|
|
175
|
-
providerRef.current.connect();
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
updateConnectionState("connecting");
|
|
179
|
-
const provider = new HocuspocusProvider({
|
|
180
|
-
url: websocketUrl,
|
|
181
|
-
name: documentName,
|
|
182
|
-
document: doc,
|
|
183
|
-
token: async () => tokenRef.current,
|
|
184
|
-
onConnect: () => {
|
|
185
|
-
reconnectAttemptsRef.current = 0;
|
|
186
|
-
updateConnectionState("connected");
|
|
187
|
-
scheduleTokenRefresh();
|
|
188
|
-
},
|
|
189
|
-
onSynced: () => {
|
|
190
|
-
setIsSynced(true);
|
|
191
|
-
updateConnectionState("synced");
|
|
192
|
-
onSync?.();
|
|
193
|
-
},
|
|
194
|
-
onDisconnect: () => {
|
|
195
|
-
updateConnectionState("disconnected");
|
|
196
|
-
setIsSynced(false);
|
|
197
|
-
clearRefreshTimer();
|
|
198
|
-
if (autoReconnect && reconnectAttemptsRef.current < maxReconnectAttempts) {
|
|
199
|
-
reconnectAttemptsRef.current += 1;
|
|
200
|
-
const delay = Math.min(
|
|
201
|
-
1e3 * 2 ** reconnectAttemptsRef.current,
|
|
202
|
-
3e4
|
|
203
|
-
);
|
|
204
|
-
setTimeout(() => {
|
|
205
|
-
if (providerRef.current && !providerRef.current.isConnected) {
|
|
206
|
-
providerRef.current.connect();
|
|
207
|
-
}
|
|
208
|
-
}, delay);
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
onAuthenticationFailed: ({ reason }) => {
|
|
212
|
-
const error = new Error(reason ?? "Authentication failed");
|
|
213
|
-
updateConnectionState("disconnected");
|
|
214
|
-
clearRefreshTimer();
|
|
215
|
-
if (onRefreshToken) {
|
|
216
|
-
void refreshToken().then((nextToken) => {
|
|
217
|
-
if (nextToken && providerRef.current) {
|
|
218
|
-
providerRef.current.connect();
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
onAuthError?.(error);
|
|
222
|
-
});
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
onAuthError?.(error);
|
|
226
|
-
},
|
|
227
|
-
onAwarenessUpdate: () => {
|
|
228
|
-
updateCollaborators(provider.awareness);
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
provider.awareness?.setLocalStateField("user", {
|
|
232
|
-
name: user.name,
|
|
233
|
-
color: userColor,
|
|
234
|
-
userId: user.userId
|
|
235
|
-
});
|
|
236
|
-
providerRef.current = provider;
|
|
237
|
-
}, [
|
|
193
|
+
function createEditorProvider(peers) {
|
|
194
|
+
const { Doc } = peers.yjs;
|
|
195
|
+
const { HocuspocusProvider } = peers.hocuspocus;
|
|
196
|
+
return function EditorProvider2({
|
|
238
197
|
websocketUrl,
|
|
239
198
|
documentName,
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
user
|
|
243
|
-
|
|
244
|
-
autoReconnect,
|
|
245
|
-
maxReconnectAttempts,
|
|
246
|
-
|
|
247
|
-
updateConnectionState,
|
|
248
|
-
updateCollaborators,
|
|
199
|
+
token,
|
|
200
|
+
tokenExpiresAt,
|
|
201
|
+
user,
|
|
202
|
+
autoConnect = true,
|
|
203
|
+
autoReconnect = true,
|
|
204
|
+
maxReconnectAttempts = 5,
|
|
205
|
+
onConnectionChange,
|
|
249
206
|
onSync,
|
|
250
207
|
onAuthError,
|
|
251
208
|
onRefreshToken,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
209
|
+
children
|
|
210
|
+
}) {
|
|
211
|
+
const [connectionState, setConnectionState] = useState("disconnected");
|
|
212
|
+
const [collaborators, setCollaborators] = useState([]);
|
|
213
|
+
const [isSynced, setIsSynced] = useState(false);
|
|
214
|
+
const docRef = useRef(null);
|
|
215
|
+
const providerRef = useRef(null);
|
|
216
|
+
const reconnectAttemptsRef = useRef(0);
|
|
217
|
+
const tokenRef = useRef(token);
|
|
218
|
+
const tokenExpiryRef = useRef(tokenExpiresAt);
|
|
219
|
+
const refreshPromiseRef = useRef(null);
|
|
220
|
+
const refreshTimerRef = useRef(null);
|
|
221
|
+
tokenRef.current = token;
|
|
222
|
+
tokenExpiryRef.current = tokenExpiresAt;
|
|
223
|
+
if (!docRef.current) {
|
|
224
|
+
docRef.current = new Doc();
|
|
264
225
|
}
|
|
265
|
-
|
|
226
|
+
const doc = docRef.current;
|
|
227
|
+
const userColor = useMemo(
|
|
228
|
+
() => user.color ?? generateUserColor(),
|
|
229
|
+
[user.color]
|
|
230
|
+
);
|
|
231
|
+
const updateConnectionState = useCallback(
|
|
232
|
+
(state) => {
|
|
233
|
+
setConnectionState(state);
|
|
234
|
+
onConnectionChange?.(state);
|
|
235
|
+
},
|
|
236
|
+
[onConnectionChange]
|
|
237
|
+
);
|
|
238
|
+
const updateCollaborators = useCallback(
|
|
239
|
+
(awareness) => {
|
|
240
|
+
if (!awareness) return;
|
|
241
|
+
const states = awareness.getStates();
|
|
242
|
+
const collabs = [];
|
|
243
|
+
states.forEach((state, clientId) => {
|
|
244
|
+
if (clientId === awareness.clientID) return;
|
|
245
|
+
if (state.user) {
|
|
246
|
+
collabs.push({
|
|
247
|
+
clientId,
|
|
248
|
+
user: state.user
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
setCollaborators(collabs);
|
|
253
|
+
},
|
|
254
|
+
[]
|
|
255
|
+
);
|
|
256
|
+
const clearRefreshTimer = useCallback(() => {
|
|
257
|
+
if (refreshTimerRef.current != null) {
|
|
258
|
+
window.clearTimeout(refreshTimerRef.current);
|
|
259
|
+
refreshTimerRef.current = null;
|
|
260
|
+
}
|
|
261
|
+
}, []);
|
|
262
|
+
const refreshToken = useCallback(async () => {
|
|
263
|
+
if (!onRefreshToken) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
if (refreshPromiseRef.current) {
|
|
267
|
+
return refreshPromiseRef.current;
|
|
268
|
+
}
|
|
269
|
+
const refreshPromise = (async () => {
|
|
270
|
+
const next = await onRefreshToken();
|
|
271
|
+
const resolvedToken = typeof next === "string" ? next : next.token;
|
|
272
|
+
const resolvedExpiry = typeof next === "string" ? void 0 : next.expiresAt;
|
|
273
|
+
tokenRef.current = resolvedToken;
|
|
274
|
+
tokenExpiryRef.current = resolvedExpiry;
|
|
275
|
+
return resolvedToken;
|
|
276
|
+
})().catch((error) => {
|
|
277
|
+
onAuthError?.(
|
|
278
|
+
error instanceof Error ? error : new Error(String(error))
|
|
279
|
+
);
|
|
280
|
+
return null;
|
|
281
|
+
}).finally(() => {
|
|
282
|
+
refreshPromiseRef.current = null;
|
|
283
|
+
});
|
|
284
|
+
refreshPromiseRef.current = refreshPromise;
|
|
285
|
+
return refreshPromise;
|
|
286
|
+
}, [onAuthError, onRefreshToken]);
|
|
287
|
+
const scheduleTokenRefresh = useCallback(() => {
|
|
266
288
|
clearRefreshTimer();
|
|
289
|
+
if (!tokenExpiryRef.current || !onRefreshToken || typeof window === "undefined") {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const refreshAtMs = tokenExpiryRef.current * 1e3 - 6e4;
|
|
293
|
+
const delay = refreshAtMs - Date.now();
|
|
294
|
+
if (delay <= 0) {
|
|
295
|
+
void refreshToken();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
refreshTimerRef.current = window.setTimeout(() => {
|
|
299
|
+
void refreshToken();
|
|
300
|
+
}, delay);
|
|
301
|
+
}, [clearRefreshTimer, onRefreshToken, refreshToken]);
|
|
302
|
+
const connect = useCallback(() => {
|
|
267
303
|
if (providerRef.current) {
|
|
268
|
-
providerRef.current.
|
|
269
|
-
|
|
304
|
+
providerRef.current.connect();
|
|
305
|
+
return;
|
|
270
306
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
307
|
+
updateConnectionState("connecting");
|
|
308
|
+
const provider = new HocuspocusProvider({
|
|
309
|
+
url: websocketUrl,
|
|
310
|
+
name: documentName,
|
|
311
|
+
document: doc,
|
|
312
|
+
token: async () => tokenRef.current,
|
|
313
|
+
onConnect: () => {
|
|
314
|
+
reconnectAttemptsRef.current = 0;
|
|
315
|
+
updateConnectionState("connected");
|
|
316
|
+
scheduleTokenRefresh();
|
|
317
|
+
},
|
|
318
|
+
onSynced: () => {
|
|
319
|
+
setIsSynced(true);
|
|
320
|
+
updateConnectionState("synced");
|
|
321
|
+
onSync?.();
|
|
322
|
+
},
|
|
323
|
+
onDisconnect: () => {
|
|
324
|
+
updateConnectionState("disconnected");
|
|
325
|
+
setIsSynced(false);
|
|
326
|
+
clearRefreshTimer();
|
|
327
|
+
if (autoReconnect && reconnectAttemptsRef.current < maxReconnectAttempts) {
|
|
328
|
+
reconnectAttemptsRef.current += 1;
|
|
329
|
+
const delay = Math.min(
|
|
330
|
+
1e3 * 2 ** reconnectAttemptsRef.current,
|
|
331
|
+
3e4
|
|
332
|
+
);
|
|
333
|
+
setTimeout(() => {
|
|
334
|
+
if (providerRef.current && !providerRef.current.isConnected) {
|
|
335
|
+
providerRef.current.connect();
|
|
336
|
+
}
|
|
337
|
+
}, delay);
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
onAuthenticationFailed: ({ reason }) => {
|
|
341
|
+
const error = new Error(reason ?? "Authentication failed");
|
|
342
|
+
updateConnectionState("disconnected");
|
|
343
|
+
clearRefreshTimer();
|
|
344
|
+
if (onRefreshToken) {
|
|
345
|
+
void refreshToken().then((nextToken) => {
|
|
346
|
+
if (nextToken && providerRef.current) {
|
|
347
|
+
providerRef.current.connect();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
onAuthError?.(error);
|
|
351
|
+
});
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
onAuthError?.(error);
|
|
355
|
+
},
|
|
356
|
+
onAwarenessUpdate: () => {
|
|
357
|
+
updateCollaborators(provider.awareness);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
provider.awareness?.setLocalStateField("user", {
|
|
361
|
+
name: user.name,
|
|
362
|
+
color: userColor,
|
|
363
|
+
userId: user.userId
|
|
364
|
+
});
|
|
365
|
+
providerRef.current = provider;
|
|
366
|
+
}, [
|
|
367
|
+
websocketUrl,
|
|
368
|
+
documentName,
|
|
275
369
|
doc,
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
370
|
+
user.name,
|
|
371
|
+
user.userId,
|
|
372
|
+
userColor,
|
|
373
|
+
autoReconnect,
|
|
374
|
+
maxReconnectAttempts,
|
|
375
|
+
clearRefreshTimer,
|
|
376
|
+
updateConnectionState,
|
|
377
|
+
updateCollaborators,
|
|
378
|
+
onSync,
|
|
379
|
+
onAuthError,
|
|
380
|
+
onRefreshToken,
|
|
381
|
+
refreshToken,
|
|
382
|
+
scheduleTokenRefresh
|
|
383
|
+
]);
|
|
384
|
+
const disconnect = useCallback(() => {
|
|
385
|
+
if (providerRef.current) {
|
|
386
|
+
providerRef.current.disconnect();
|
|
387
|
+
updateConnectionState("disconnected");
|
|
388
|
+
}
|
|
389
|
+
}, [updateConnectionState]);
|
|
390
|
+
useEffect(() => {
|
|
391
|
+
if (autoConnect) {
|
|
392
|
+
connect();
|
|
393
|
+
}
|
|
394
|
+
return () => {
|
|
395
|
+
clearRefreshTimer();
|
|
396
|
+
if (providerRef.current) {
|
|
397
|
+
providerRef.current.destroy();
|
|
398
|
+
providerRef.current = null;
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
}, [autoConnect, clearRefreshTimer, connect]);
|
|
402
|
+
const contextValue = useMemo(
|
|
403
|
+
() => ({
|
|
404
|
+
doc,
|
|
405
|
+
provider: providerRef.current,
|
|
406
|
+
connectionState,
|
|
407
|
+
collaborators,
|
|
408
|
+
isSynced,
|
|
409
|
+
connect,
|
|
410
|
+
disconnect
|
|
411
|
+
}),
|
|
412
|
+
[doc, connectionState, collaborators, isSynced, connect, disconnect]
|
|
413
|
+
);
|
|
414
|
+
return /* @__PURE__ */ jsx(EditorContext.Provider, { value: contextValue, children });
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
var lazyEditorProvider = retryableLazyEditor(
|
|
418
|
+
async () => createEditorProvider(await loadEditorProviderPeers())
|
|
419
|
+
);
|
|
420
|
+
function EditorProvider(props) {
|
|
421
|
+
const LazyEditorProvider = lazyEditorProvider();
|
|
422
|
+
return /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(LazyEditorProvider, { ...props }) });
|
|
286
423
|
}
|
|
287
424
|
function useEditorContext() {
|
|
288
425
|
const context = useContext(EditorContext);
|
|
@@ -292,8 +429,25 @@ function useEditorContext() {
|
|
|
292
429
|
return context;
|
|
293
430
|
}
|
|
294
431
|
|
|
295
|
-
// src/editor/editor-
|
|
432
|
+
// src/editor/editor-loading.tsx
|
|
296
433
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
434
|
+
function EditorLoadingPlaceholder({ className }) {
|
|
435
|
+
return /* @__PURE__ */ jsx2(
|
|
436
|
+
"div",
|
|
437
|
+
{
|
|
438
|
+
role: "status",
|
|
439
|
+
"aria-live": "polite",
|
|
440
|
+
className: cn(
|
|
441
|
+
"flex w-full items-center justify-center rounded-lg border border-border border-dashed bg-muted text-muted-foreground text-sm",
|
|
442
|
+
className
|
|
443
|
+
),
|
|
444
|
+
children: "Loading editor\u2026"
|
|
445
|
+
}
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/editor/editor-toolbar.tsx
|
|
450
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
297
451
|
function EditorToolbar({
|
|
298
452
|
editor,
|
|
299
453
|
className
|
|
@@ -375,7 +529,7 @@ function EditorToolbar({
|
|
|
375
529
|
isActive: editor.isActive("blockquote")
|
|
376
530
|
}
|
|
377
531
|
];
|
|
378
|
-
return /* @__PURE__ */
|
|
532
|
+
return /* @__PURE__ */ jsx3(
|
|
379
533
|
"div",
|
|
380
534
|
{
|
|
381
535
|
className: cn(
|
|
@@ -384,9 +538,9 @@ function EditorToolbar({
|
|
|
384
538
|
),
|
|
385
539
|
children: buttons.map((button) => {
|
|
386
540
|
if ("type" in button && button.type === "separator") {
|
|
387
|
-
return /* @__PURE__ */
|
|
541
|
+
return /* @__PURE__ */ jsx3("div", { className: "mx-1 h-6 w-px bg-border" }, button.id);
|
|
388
542
|
}
|
|
389
|
-
return /* @__PURE__ */
|
|
543
|
+
return /* @__PURE__ */ jsx3(
|
|
390
544
|
"button",
|
|
391
545
|
{
|
|
392
546
|
onClick: button.action,
|
|
@@ -407,7 +561,7 @@ function EditorToolbar({
|
|
|
407
561
|
}
|
|
408
562
|
|
|
409
563
|
// src/editor/tiptap-editor.tsx
|
|
410
|
-
import { jsx as
|
|
564
|
+
import { jsx as jsx4, jsxs } from "react/jsx-runtime";
|
|
411
565
|
var cursorColors = {
|
|
412
566
|
"#FF6B6B": { background: "#FF6B6B", text: "#FFFFFF" },
|
|
413
567
|
"#4ECDC4": { background: "#4ECDC4", text: "#000000" },
|
|
@@ -423,107 +577,112 @@ var cursorColors = {
|
|
|
423
577
|
function getCursorColors(color) {
|
|
424
578
|
return cursorColors[color] ?? { background: color, text: "#FFFFFF" };
|
|
425
579
|
}
|
|
426
|
-
function
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
];
|
|
455
|
-
if (provider?.awareness) {
|
|
456
|
-
baseExtensions.push(
|
|
457
|
-
CollaborationCaret.configure({
|
|
458
|
-
provider,
|
|
459
|
-
user: provider.awareness.getLocalState()?.user ?? {
|
|
460
|
-
name: "Anonymous",
|
|
461
|
-
color: "#808080"
|
|
462
|
-
},
|
|
463
|
-
render: (user) => {
|
|
464
|
-
const { background, text } = getCursorColors(user.color);
|
|
465
|
-
const cursor = document.createElement("span");
|
|
466
|
-
cursor.className = "collaboration-cursor";
|
|
467
|
-
cursor.style.borderColor = background;
|
|
468
|
-
const label = document.createElement("span");
|
|
469
|
-
label.className = "collaboration-cursor-label";
|
|
470
|
-
label.style.backgroundColor = background;
|
|
471
|
-
label.style.color = text;
|
|
472
|
-
label.textContent = user.name;
|
|
473
|
-
cursor.appendChild(label);
|
|
474
|
-
return cursor;
|
|
580
|
+
function createTiptapEditor(peers) {
|
|
581
|
+
const { EditorContent, useEditor } = peers.react;
|
|
582
|
+
const StarterKit = peers.starterKit.default;
|
|
583
|
+
const Collaboration = peers.collaboration.default;
|
|
584
|
+
const CollaborationCaret = peers.collaborationCaret.default;
|
|
585
|
+
return function TiptapEditor2({
|
|
586
|
+
initialContent,
|
|
587
|
+
placeholder = "Start writing...",
|
|
588
|
+
readOnly = false,
|
|
589
|
+
autoFocus = false,
|
|
590
|
+
className,
|
|
591
|
+
contentClassName,
|
|
592
|
+
onUpdate,
|
|
593
|
+
onSelectionUpdate,
|
|
594
|
+
onReady
|
|
595
|
+
}) {
|
|
596
|
+
const { doc, provider, connectionState } = useEditorContext();
|
|
597
|
+
const fragment = useMemo2(() => doc.getXmlFragment("prosemirror"), [doc]);
|
|
598
|
+
const extensions = useMemo2(() => {
|
|
599
|
+
const baseExtensions = [
|
|
600
|
+
StarterKit.configure({
|
|
601
|
+
// Disable history - Y.js handles undo/redo
|
|
602
|
+
...{ history: false },
|
|
603
|
+
// Configure code block for syntax highlighting placeholder
|
|
604
|
+
codeBlock: {
|
|
605
|
+
HTMLAttributes: {
|
|
606
|
+
class: "hljs"
|
|
607
|
+
}
|
|
475
608
|
}
|
|
609
|
+
}),
|
|
610
|
+
Collaboration.configure({
|
|
611
|
+
fragment
|
|
476
612
|
})
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
613
|
+
];
|
|
614
|
+
if (provider?.awareness) {
|
|
615
|
+
baseExtensions.push(
|
|
616
|
+
CollaborationCaret.configure({
|
|
617
|
+
provider,
|
|
618
|
+
user: provider.awareness.getLocalState()?.user ?? {
|
|
619
|
+
name: "Anonymous",
|
|
620
|
+
color: "#808080"
|
|
621
|
+
},
|
|
622
|
+
render: (user) => {
|
|
623
|
+
const { background, text } = getCursorColors(user.color);
|
|
624
|
+
const cursor = document.createElement("span");
|
|
625
|
+
cursor.className = "collaboration-cursor";
|
|
626
|
+
cursor.style.borderColor = background;
|
|
627
|
+
const label = document.createElement("span");
|
|
628
|
+
label.className = "collaboration-cursor-label";
|
|
629
|
+
label.style.backgroundColor = background;
|
|
630
|
+
label.style.color = text;
|
|
631
|
+
label.textContent = user.name;
|
|
632
|
+
cursor.appendChild(label);
|
|
633
|
+
return cursor;
|
|
634
|
+
}
|
|
635
|
+
})
|
|
636
|
+
);
|
|
493
637
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
638
|
+
return baseExtensions;
|
|
639
|
+
}, [fragment, provider]);
|
|
640
|
+
const editor = useEditor({
|
|
641
|
+
extensions,
|
|
642
|
+
editable: !readOnly,
|
|
643
|
+
autofocus: autoFocus,
|
|
644
|
+
editorProps: {
|
|
645
|
+
attributes: {
|
|
646
|
+
class: cn(
|
|
647
|
+
"prose prose-sm sm:prose-base dark:prose-invert max-w-none",
|
|
648
|
+
"focus:outline-none",
|
|
649
|
+
contentClassName
|
|
650
|
+
),
|
|
651
|
+
"data-placeholder": placeholder
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
onUpdate: ({ editor: ed }) => {
|
|
655
|
+
onUpdate?.(ed);
|
|
656
|
+
},
|
|
657
|
+
onSelectionUpdate: ({ editor: ed }) => {
|
|
658
|
+
onSelectionUpdate?.(ed);
|
|
659
|
+
},
|
|
660
|
+
onCreate: ({ editor: ed }) => {
|
|
661
|
+
onReady?.(ed);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
useEffect2(() => {
|
|
665
|
+
if (editor) {
|
|
666
|
+
editor.setEditable(!readOnly);
|
|
667
|
+
}
|
|
668
|
+
}, [editor, readOnly]);
|
|
669
|
+
useEffect2(() => {
|
|
670
|
+
if (editor && initialContent && connectionState === "synced" && editor.isEmpty) {
|
|
671
|
+
editor.commands.setContent(initialContent);
|
|
672
|
+
}
|
|
673
|
+
}, [editor, initialContent, connectionState]);
|
|
674
|
+
return /* @__PURE__ */ jsxs(
|
|
675
|
+
"div",
|
|
676
|
+
{
|
|
677
|
+
className: cn(
|
|
678
|
+
"relative min-h-[200px] w-full rounded-lg border border-border",
|
|
679
|
+
"bg-background",
|
|
680
|
+
className
|
|
681
|
+
),
|
|
682
|
+
children: [
|
|
683
|
+
/* @__PURE__ */ jsx4("div", { className: "absolute top-2 right-2 z-10", children: /* @__PURE__ */ jsx4(ConnectionIndicator, { state: connectionState }) }),
|
|
684
|
+
/* @__PURE__ */ jsx4("div", { className: "p-4 pt-10", children: /* @__PURE__ */ jsx4(EditorContent, { editor }) }),
|
|
685
|
+
/* @__PURE__ */ jsx4("style", { children: `
|
|
527
686
|
.ProseMirror p.is-editor-empty:first-child::before {
|
|
528
687
|
content: attr(data-placeholder);
|
|
529
688
|
float: left;
|
|
@@ -586,7 +745,21 @@ function TiptapEditor({
|
|
|
586
745
|
font-size: inherit;
|
|
587
746
|
}
|
|
588
747
|
` })
|
|
589
|
-
|
|
748
|
+
]
|
|
749
|
+
}
|
|
750
|
+
);
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
var lazyTiptapEditor = retryableLazyEditor(
|
|
754
|
+
async () => createTiptapEditor(await loadCollaborationPeers())
|
|
755
|
+
);
|
|
756
|
+
function TiptapEditor(props) {
|
|
757
|
+
const LazyTiptapEditor = lazyTiptapEditor();
|
|
758
|
+
return /* @__PURE__ */ jsx4(
|
|
759
|
+
Suspense2,
|
|
760
|
+
{
|
|
761
|
+
fallback: /* @__PURE__ */ jsx4(EditorLoadingPlaceholder, { className: cn("min-h-[200px]", props.className) }),
|
|
762
|
+
children: /* @__PURE__ */ jsx4(LazyTiptapEditor, { ...props })
|
|
590
763
|
}
|
|
591
764
|
);
|
|
592
765
|
}
|
|
@@ -612,8 +785,8 @@ function ConnectionIndicator({
|
|
|
612
785
|
}
|
|
613
786
|
}[state];
|
|
614
787
|
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-muted-foreground text-xs", children: [
|
|
615
|
-
/* @__PURE__ */
|
|
616
|
-
/* @__PURE__ */
|
|
788
|
+
/* @__PURE__ */ jsx4("span", { className: cn("h-2 w-2 rounded-full", config.color) }),
|
|
789
|
+
/* @__PURE__ */ jsx4("span", { children: config.label })
|
|
617
790
|
] });
|
|
618
791
|
}
|
|
619
792
|
function CollaboratorsList({
|
|
@@ -624,7 +797,7 @@ function CollaboratorsList({
|
|
|
624
797
|
return null;
|
|
625
798
|
}
|
|
626
799
|
return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-1", className), children: [
|
|
627
|
-
collaborators.slice(0, 5).map((collab) => /* @__PURE__ */
|
|
800
|
+
collaborators.slice(0, 5).map((collab) => /* @__PURE__ */ jsx4(
|
|
628
801
|
"div",
|
|
629
802
|
{
|
|
630
803
|
className: "flex h-6 w-6 items-center justify-center rounded-full font-medium text-xs",
|
|
@@ -642,9 +815,7 @@ function CollaboratorsList({
|
|
|
642
815
|
}
|
|
643
816
|
|
|
644
817
|
// src/editor/markdown-document-editor.tsx
|
|
645
|
-
import {
|
|
646
|
-
import StarterKit2 from "@tiptap/starter-kit";
|
|
647
|
-
import { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2 } from "react";
|
|
818
|
+
import { Suspense as Suspense3, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2 } from "react";
|
|
648
819
|
|
|
649
820
|
// src/editor/markdown-conversion.ts
|
|
650
821
|
import { marked } from "marked";
|
|
@@ -666,85 +837,88 @@ function normalizeMarkdown(markdown) {
|
|
|
666
837
|
}
|
|
667
838
|
|
|
668
839
|
// src/editor/markdown-document-editor.tsx
|
|
669
|
-
import { jsx as
|
|
670
|
-
function
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
840
|
+
import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
841
|
+
function createMarkdownDocumentEditor(peers) {
|
|
842
|
+
const { EditorContent, useEditor } = peers.react;
|
|
843
|
+
const StarterKit = peers.starterKit.default;
|
|
844
|
+
return function MarkdownDocumentEditor2({
|
|
845
|
+
value = "",
|
|
846
|
+
placeholder = "Start writing...",
|
|
847
|
+
readOnly = false,
|
|
848
|
+
autoFocus = false,
|
|
849
|
+
className,
|
|
850
|
+
contentClassName,
|
|
851
|
+
onChange,
|
|
852
|
+
onReady
|
|
853
|
+
}) {
|
|
854
|
+
const initialHtml = useMemo3(() => markdownToHtml(value), []);
|
|
855
|
+
const lastAppliedMarkdownRef = useRef2(normalizeMarkdown(value));
|
|
856
|
+
const editor = useEditor({
|
|
857
|
+
extensions: [
|
|
858
|
+
StarterKit.configure({
|
|
859
|
+
codeBlock: {
|
|
860
|
+
HTMLAttributes: {
|
|
861
|
+
class: "hljs"
|
|
862
|
+
}
|
|
688
863
|
}
|
|
864
|
+
})
|
|
865
|
+
],
|
|
866
|
+
content: initialHtml,
|
|
867
|
+
editable: !readOnly,
|
|
868
|
+
autofocus: autoFocus,
|
|
869
|
+
editorProps: {
|
|
870
|
+
attributes: {
|
|
871
|
+
class: cn(
|
|
872
|
+
"prose prose-sm sm:prose-base max-w-none focus:outline-none",
|
|
873
|
+
"prose-headings:text-foreground prose-p:text-foreground prose-li:text-foreground",
|
|
874
|
+
"prose-strong:text-foreground prose-code:text-foreground prose-pre:text-foreground",
|
|
875
|
+
contentClassName
|
|
876
|
+
),
|
|
877
|
+
"data-placeholder": placeholder
|
|
689
878
|
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
"prose prose-sm sm:prose-base max-w-none focus:outline-none",
|
|
699
|
-
"prose-headings:text-foreground prose-p:text-foreground prose-li:text-foreground",
|
|
700
|
-
"prose-strong:text-foreground prose-code:text-foreground prose-pre:text-foreground",
|
|
701
|
-
contentClassName
|
|
702
|
-
),
|
|
703
|
-
"data-placeholder": placeholder
|
|
879
|
+
},
|
|
880
|
+
onUpdate: ({ editor: currentEditor }) => {
|
|
881
|
+
const nextMarkdown = normalizeMarkdown(htmlToMarkdown(currentEditor.getHTML()));
|
|
882
|
+
lastAppliedMarkdownRef.current = nextMarkdown;
|
|
883
|
+
onChange?.(nextMarkdown, currentEditor);
|
|
884
|
+
},
|
|
885
|
+
onCreate: ({ editor: currentEditor }) => {
|
|
886
|
+
onReady?.(currentEditor);
|
|
704
887
|
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
},
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
editor.commands.setContent(markdownToHtml(value), { emitUpdate: false });
|
|
729
|
-
lastAppliedMarkdownRef.current = normalizedValue;
|
|
730
|
-
}, [editor, value]);
|
|
731
|
-
return /* @__PURE__ */ jsxs2(
|
|
732
|
-
"div",
|
|
733
|
-
{
|
|
734
|
-
className: cn(
|
|
735
|
-
"flex min-h-[14rem] w-full flex-col overflow-hidden rounded-lg border border-border bg-background",
|
|
736
|
-
className
|
|
737
|
-
),
|
|
738
|
-
children: [
|
|
739
|
-
/* @__PURE__ */ jsx4(
|
|
740
|
-
EditorToolbar,
|
|
741
|
-
{
|
|
742
|
-
editor,
|
|
743
|
-
className: "border-border bg-card px-2 py-2"
|
|
744
|
-
}
|
|
888
|
+
});
|
|
889
|
+
useEffect3(() => {
|
|
890
|
+
if (editor) {
|
|
891
|
+
editor.setEditable(!readOnly);
|
|
892
|
+
}
|
|
893
|
+
}, [editor, readOnly]);
|
|
894
|
+
useEffect3(() => {
|
|
895
|
+
if (!editor) {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const normalizedValue = normalizeMarkdown(value);
|
|
899
|
+
if (normalizedValue === lastAppliedMarkdownRef.current) {
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
editor.commands.setContent(markdownToHtml(value), { emitUpdate: false });
|
|
903
|
+
lastAppliedMarkdownRef.current = normalizedValue;
|
|
904
|
+
}, [editor, value]);
|
|
905
|
+
return /* @__PURE__ */ jsxs2(
|
|
906
|
+
"div",
|
|
907
|
+
{
|
|
908
|
+
className: cn(
|
|
909
|
+
"flex min-h-[14rem] w-full flex-col overflow-hidden rounded-lg border border-border bg-background",
|
|
910
|
+
className
|
|
745
911
|
),
|
|
746
|
-
|
|
747
|
-
|
|
912
|
+
children: [
|
|
913
|
+
/* @__PURE__ */ jsx5(
|
|
914
|
+
EditorToolbar,
|
|
915
|
+
{
|
|
916
|
+
editor,
|
|
917
|
+
className: "border-border bg-card px-2 py-2"
|
|
918
|
+
}
|
|
919
|
+
),
|
|
920
|
+
/* @__PURE__ */ jsx5("div", { className: "min-h-0 flex-1 overflow-auto px-5 py-4", children: /* @__PURE__ */ jsx5(EditorContent, { editor }) }),
|
|
921
|
+
/* @__PURE__ */ jsx5("style", { children: `
|
|
748
922
|
.ProseMirror p.is-editor-empty:first-child::before {
|
|
749
923
|
content: attr(data-placeholder);
|
|
750
924
|
float: left;
|
|
@@ -772,7 +946,21 @@ function MarkdownDocumentEditor({
|
|
|
772
946
|
padding: 0;
|
|
773
947
|
}
|
|
774
948
|
` })
|
|
775
|
-
|
|
949
|
+
]
|
|
950
|
+
}
|
|
951
|
+
);
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
var lazyMarkdownDocumentEditor = retryableLazyEditor(
|
|
955
|
+
async () => createMarkdownDocumentEditor(await loadDocumentEditorPeers())
|
|
956
|
+
);
|
|
957
|
+
function MarkdownDocumentEditor(props) {
|
|
958
|
+
const LazyMarkdownDocumentEditor = lazyMarkdownDocumentEditor();
|
|
959
|
+
return /* @__PURE__ */ jsx5(
|
|
960
|
+
Suspense3,
|
|
961
|
+
{
|
|
962
|
+
fallback: /* @__PURE__ */ jsx5(EditorLoadingPlaceholder, { className: cn("min-h-[14rem]", props.className) }),
|
|
963
|
+
children: /* @__PURE__ */ jsx5(LazyMarkdownDocumentEditor, { ...props })
|
|
776
964
|
}
|
|
777
965
|
);
|
|
778
966
|
}
|
|
@@ -926,7 +1114,7 @@ function useAwareness() {
|
|
|
926
1114
|
}
|
|
927
1115
|
|
|
928
1116
|
// src/editor/document-editor-pane.tsx
|
|
929
|
-
import { jsx as
|
|
1117
|
+
import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
930
1118
|
function connectionTone(state) {
|
|
931
1119
|
switch (state) {
|
|
932
1120
|
case "synced":
|
|
@@ -991,21 +1179,21 @@ function CollaborativeDocumentSurface({
|
|
|
991
1179
|
connectionTone(state)
|
|
992
1180
|
),
|
|
993
1181
|
children: [
|
|
994
|
-
state === "disconnected" ? /* @__PURE__ */
|
|
1182
|
+
state === "disconnected" ? /* @__PURE__ */ jsx6(WifiOff, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx6(Wifi, { className: "h-3.5 w-3.5" }),
|
|
995
1183
|
connectionLabel(state)
|
|
996
1184
|
]
|
|
997
1185
|
}
|
|
998
1186
|
),
|
|
999
1187
|
/* @__PURE__ */ jsxs3("span", { className: "inline-flex items-center gap-1.5 rounded-full border border-border bg-background px-2.5 py-1", children: [
|
|
1000
|
-
/* @__PURE__ */
|
|
1188
|
+
/* @__PURE__ */ jsx6(Users, { className: "h-3.5 w-3.5" }),
|
|
1001
1189
|
collaborators.length === 0 ? "Solo editing" : `${collaboratorCount} active`
|
|
1002
1190
|
] })
|
|
1003
1191
|
] }),
|
|
1004
|
-
/* @__PURE__ */
|
|
1192
|
+
/* @__PURE__ */ jsx6("p", { className: "text-xs text-muted-foreground", children: connectionDescription(state, collaborators.length, readOnly) })
|
|
1005
1193
|
] }),
|
|
1006
|
-
/* @__PURE__ */
|
|
1194
|
+
/* @__PURE__ */ jsx6(CollaboratorsList, { collaborators })
|
|
1007
1195
|
] }),
|
|
1008
|
-
/* @__PURE__ */
|
|
1196
|
+
/* @__PURE__ */ jsx6(
|
|
1009
1197
|
TiptapEditor,
|
|
1010
1198
|
{
|
|
1011
1199
|
initialContent,
|
|
@@ -1080,7 +1268,7 @@ function DocumentEditorPane({
|
|
|
1080
1268
|
variant: "underline",
|
|
1081
1269
|
className: "h-auto gap-4 border-0 bg-transparent p-0 text-muted-foreground",
|
|
1082
1270
|
children: [
|
|
1083
|
-
/* @__PURE__ */
|
|
1271
|
+
/* @__PURE__ */ jsx6(
|
|
1084
1272
|
TabsTrigger,
|
|
1085
1273
|
{
|
|
1086
1274
|
value: "preview",
|
|
@@ -1096,7 +1284,7 @@ function DocumentEditorPane({
|
|
|
1096
1284
|
variant: "underline",
|
|
1097
1285
|
className: "flex items-center gap-2 pb-2 data-[state=active]:border-primary data-[state=active]:text-foreground",
|
|
1098
1286
|
children: [
|
|
1099
|
-
/* @__PURE__ */
|
|
1287
|
+
/* @__PURE__ */ jsx6(PencilLine, { className: "h-3.5 w-3.5" }),
|
|
1100
1288
|
isCollaborative ? "Live edit" : "Edit"
|
|
1101
1289
|
]
|
|
1102
1290
|
}
|
|
@@ -1106,8 +1294,8 @@ function DocumentEditorPane({
|
|
|
1106
1294
|
),
|
|
1107
1295
|
/* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground", children: [
|
|
1108
1296
|
toolbar,
|
|
1109
|
-
/* @__PURE__ */
|
|
1110
|
-
/* @__PURE__ */
|
|
1297
|
+
/* @__PURE__ */ jsx6("span", { className: "rounded-full border border-border bg-card px-2.5 py-1 font-medium", children: isCollaborative ? "Live document" : "Local draft" }),
|
|
1298
|
+
/* @__PURE__ */ jsx6("span", { className: "rounded-full border border-border bg-background px-2.5 py-1", children: saveStateLabel }),
|
|
1111
1299
|
onSave && !readOnly && /* @__PURE__ */ jsxs3(
|
|
1112
1300
|
"button",
|
|
1113
1301
|
{
|
|
@@ -1116,14 +1304,14 @@ function DocumentEditorPane({
|
|
|
1116
1304
|
disabled: saving || !isDirty,
|
|
1117
1305
|
className: "inline-flex items-center gap-2 rounded-[var(--radius-full)] border border-border bg-card px-3 py-1.5 text-xs font-semibold text-foreground transition-colors hover:border-primary/40 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
1118
1306
|
children: [
|
|
1119
|
-
/* @__PURE__ */
|
|
1307
|
+
/* @__PURE__ */ jsx6(Save, { className: "h-3.5 w-3.5" }),
|
|
1120
1308
|
saving ? "Saving..." : saveLabel
|
|
1121
1309
|
]
|
|
1122
1310
|
}
|
|
1123
1311
|
)
|
|
1124
1312
|
] })
|
|
1125
1313
|
] });
|
|
1126
|
-
const preview = /* @__PURE__ */
|
|
1314
|
+
const preview = /* @__PURE__ */ jsx6(
|
|
1127
1315
|
"div",
|
|
1128
1316
|
{
|
|
1129
1317
|
className: cn(
|
|
@@ -1133,10 +1321,10 @@ function DocumentEditorPane({
|
|
|
1133
1321
|
"m-4 rounded-[var(--radius-lg)] border border-border bg-card p-5",
|
|
1134
1322
|
previewClassName
|
|
1135
1323
|
),
|
|
1136
|
-
children: /* @__PURE__ */
|
|
1324
|
+
children: /* @__PURE__ */ jsx6(Markdown, { children: draft })
|
|
1137
1325
|
}
|
|
1138
1326
|
);
|
|
1139
|
-
const localEditor = /* @__PURE__ */
|
|
1327
|
+
const localEditor = /* @__PURE__ */ jsx6(
|
|
1140
1328
|
MarkdownDocumentEditor,
|
|
1141
1329
|
{
|
|
1142
1330
|
value: draft,
|
|
@@ -1149,7 +1337,7 @@ function DocumentEditorPane({
|
|
|
1149
1337
|
className: editorClassName
|
|
1150
1338
|
}
|
|
1151
1339
|
);
|
|
1152
|
-
const collaborativeEditor = collaboration ? /* @__PURE__ */
|
|
1340
|
+
const collaborativeEditor = collaboration ? /* @__PURE__ */ jsx6(EditorProvider, { ...collaboration, children: /* @__PURE__ */ jsx6(
|
|
1153
1341
|
CollaborativeDocumentSurface,
|
|
1154
1342
|
{
|
|
1155
1343
|
markdown: draft,
|
|
@@ -1160,7 +1348,7 @@ function DocumentEditorPane({
|
|
|
1160
1348
|
onChange: handleChange
|
|
1161
1349
|
}
|
|
1162
1350
|
) }, collaboration.documentName) : localEditor;
|
|
1163
|
-
return /* @__PURE__ */
|
|
1351
|
+
return /* @__PURE__ */ jsx6(
|
|
1164
1352
|
Tabs,
|
|
1165
1353
|
{
|
|
1166
1354
|
value: activeMode,
|
|
@@ -1182,8 +1370,8 @@ function DocumentEditorPane({
|
|
|
1182
1370
|
toolbar: editorToolbar,
|
|
1183
1371
|
hideTitleBlock,
|
|
1184
1372
|
children: [
|
|
1185
|
-
/* @__PURE__ */
|
|
1186
|
-
/* @__PURE__ */
|
|
1373
|
+
/* @__PURE__ */ jsx6(TabsContent, { value: "preview", className: "mt-0 h-full", children: preview }),
|
|
1374
|
+
/* @__PURE__ */ jsx6(TabsContent, { value: "edit", className: "mt-0 h-full px-4 py-4", children: isCollaborative ? collaborativeEditor : localEditor })
|
|
1187
1375
|
]
|
|
1188
1376
|
}
|
|
1189
1377
|
)
|