@tangle-network/sandbox-ui 0.2.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 +68 -0
- package/dist/auth.d.ts +57 -0
- package/dist/auth.js +14 -0
- package/dist/branding-DCi5VEik.d.ts +13 -0
- package/dist/button-BidTtuRS.d.ts +15 -0
- package/dist/chat.d.ts +121 -0
- package/dist/chat.js +25 -0
- package/dist/chunk-2UHPE5T7.js +201 -0
- package/dist/chunk-4EIWPJMJ.js +545 -0
- package/dist/chunk-6MQIDUPA.js +502 -0
- package/dist/chunk-B26TQ7SA.js +47 -0
- package/dist/chunk-E6FS7R4X.js +109 -0
- package/dist/chunk-GRYHFH5O.js +110 -0
- package/dist/chunk-HMND7JPA.js +868 -0
- package/dist/chunk-HRMUF35V.js +19 -0
- package/dist/chunk-HYEAX3DC.js +822 -0
- package/dist/chunk-KMXV7DDX.js +174 -0
- package/dist/chunk-KYY2X6LY.js +318 -0
- package/dist/chunk-L6ZDH5F4.js +334 -0
- package/dist/chunk-LTFK464G.js +103 -0
- package/dist/chunk-M34OA6PQ.js +233 -0
- package/dist/chunk-M6VLC32S.js +219 -0
- package/dist/chunk-MCGKDCOR.js +173 -0
- package/dist/chunk-NI2EI43H.js +294 -0
- package/dist/chunk-OU4TRNQZ.js +173 -0
- package/dist/chunk-QD4QE5P5.js +40 -0
- package/dist/chunk-QSQBDR3N.js +180 -0
- package/dist/chunk-RQHJBTEU.js +10 -0
- package/dist/chunk-U62G5TS7.js +472 -0
- package/dist/chunk-ZOL2TR5M.js +475 -0
- package/dist/dashboard.d.ts +111 -0
- package/dist/dashboard.js +26 -0
- package/dist/editor.d.ts +196 -0
- package/dist/editor.js +713 -0
- package/dist/expanded-tool-detail-OkXGqTHe.d.ts +52 -0
- package/dist/files.d.ts +66 -0
- package/dist/files.js +11 -0
- package/dist/hooks.d.ts +22 -0
- package/dist/hooks.js +107 -0
- package/dist/index.d.ts +107 -0
- package/dist/index.js +551 -0
- package/dist/markdown.d.ts +55 -0
- package/dist/markdown.js +17 -0
- package/dist/pages.d.ts +89 -0
- package/dist/pages.js +1181 -0
- package/dist/parts-CyGkM6Fp.d.ts +50 -0
- package/dist/primitives.d.ts +189 -0
- package/dist/primitives.js +161 -0
- package/dist/run-CtFZ6s-D.d.ts +41 -0
- package/dist/run.d.ts +14 -0
- package/dist/run.js +29 -0
- package/dist/sidecar-CFU2W9j1.d.ts +8 -0
- package/dist/stores.d.ts +28 -0
- package/dist/stores.js +49 -0
- package/dist/terminal.d.ts +44 -0
- package/dist/terminal.js +160 -0
- package/dist/tool-call-feed-D5Ume-Pt.d.ts +66 -0
- package/dist/tool-display-BvsVW_Ur.d.ts +32 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.js +0 -0
- package/dist/usage-chart-DINgSVL5.d.ts +60 -0
- package/dist/use-sidecar-auth-Bb0-w3lX.d.ts +339 -0
- package/dist/utils.d.ts +28 -0
- package/dist/utils.js +28 -0
- package/dist/workspace.d.ts +113 -0
- package/dist/workspace.js +15 -0
- package/package.json +174 -0
- package/src/styles/globals.css +230 -0
- package/src/styles/tokens.css +73 -0
- package/tailwind.config.cjs +99 -0
package/dist/editor.js
ADDED
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cn
|
|
3
|
+
} from "./chunk-RQHJBTEU.js";
|
|
4
|
+
|
|
5
|
+
// src/editor/editor-provider.tsx
|
|
6
|
+
import { HocuspocusProvider } from "@hocuspocus/provider";
|
|
7
|
+
import {
|
|
8
|
+
createContext,
|
|
9
|
+
useCallback,
|
|
10
|
+
useContext,
|
|
11
|
+
useEffect,
|
|
12
|
+
useMemo,
|
|
13
|
+
useRef,
|
|
14
|
+
useState
|
|
15
|
+
} from "react";
|
|
16
|
+
import * as Y from "yjs";
|
|
17
|
+
import { jsx } from "react/jsx-runtime";
|
|
18
|
+
var EditorContext = createContext(null);
|
|
19
|
+
function generateUserColor() {
|
|
20
|
+
const colors = [
|
|
21
|
+
"#FF6B6B",
|
|
22
|
+
// Red
|
|
23
|
+
"#4ECDC4",
|
|
24
|
+
// Teal
|
|
25
|
+
"#45B7D1",
|
|
26
|
+
// Blue
|
|
27
|
+
"#96CEB4",
|
|
28
|
+
// Green
|
|
29
|
+
"#FFEAA7",
|
|
30
|
+
// Yellow
|
|
31
|
+
"#DDA0DD",
|
|
32
|
+
// Plum
|
|
33
|
+
"#98D8C8",
|
|
34
|
+
// Mint
|
|
35
|
+
"#F7DC6F",
|
|
36
|
+
// Gold
|
|
37
|
+
"#BB8FCE",
|
|
38
|
+
// Purple
|
|
39
|
+
"#85C1E9"
|
|
40
|
+
// Sky
|
|
41
|
+
];
|
|
42
|
+
return colors[Math.floor(Math.random() * colors.length)];
|
|
43
|
+
}
|
|
44
|
+
function EditorProvider({
|
|
45
|
+
websocketUrl,
|
|
46
|
+
documentName,
|
|
47
|
+
token,
|
|
48
|
+
user,
|
|
49
|
+
autoConnect = true,
|
|
50
|
+
autoReconnect = true,
|
|
51
|
+
maxReconnectAttempts = 5,
|
|
52
|
+
onConnectionChange,
|
|
53
|
+
onSync,
|
|
54
|
+
onAuthError,
|
|
55
|
+
children
|
|
56
|
+
}) {
|
|
57
|
+
const [connectionState, setConnectionState] = useState("disconnected");
|
|
58
|
+
const [collaborators, setCollaborators] = useState([]);
|
|
59
|
+
const [isSynced, setIsSynced] = useState(false);
|
|
60
|
+
const docRef = useRef(null);
|
|
61
|
+
const providerRef = useRef(null);
|
|
62
|
+
const reconnectAttemptsRef = useRef(0);
|
|
63
|
+
if (!docRef.current) {
|
|
64
|
+
docRef.current = new Y.Doc();
|
|
65
|
+
}
|
|
66
|
+
const doc = docRef.current;
|
|
67
|
+
const userColor = useMemo(
|
|
68
|
+
() => user.color ?? generateUserColor(),
|
|
69
|
+
[user.color]
|
|
70
|
+
);
|
|
71
|
+
const updateConnectionState = useCallback(
|
|
72
|
+
(state) => {
|
|
73
|
+
setConnectionState(state);
|
|
74
|
+
onConnectionChange?.(state);
|
|
75
|
+
},
|
|
76
|
+
[onConnectionChange]
|
|
77
|
+
);
|
|
78
|
+
const updateCollaborators = useCallback(
|
|
79
|
+
(awareness) => {
|
|
80
|
+
if (!awareness) return;
|
|
81
|
+
const states = awareness.getStates();
|
|
82
|
+
const collabs = [];
|
|
83
|
+
states.forEach((state, clientId) => {
|
|
84
|
+
if (clientId === awareness.clientID) return;
|
|
85
|
+
if (state.user) {
|
|
86
|
+
collabs.push({
|
|
87
|
+
clientId,
|
|
88
|
+
user: state.user
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
setCollaborators(collabs);
|
|
93
|
+
},
|
|
94
|
+
[]
|
|
95
|
+
);
|
|
96
|
+
const connect = useCallback(() => {
|
|
97
|
+
if (providerRef.current) {
|
|
98
|
+
providerRef.current.connect();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
updateConnectionState("connecting");
|
|
102
|
+
const provider = new HocuspocusProvider({
|
|
103
|
+
url: websocketUrl,
|
|
104
|
+
name: documentName,
|
|
105
|
+
document: doc,
|
|
106
|
+
token,
|
|
107
|
+
// @ts-expect-error -- connect is valid at runtime but missing from type defs
|
|
108
|
+
connect: true,
|
|
109
|
+
onConnect: () => {
|
|
110
|
+
reconnectAttemptsRef.current = 0;
|
|
111
|
+
updateConnectionState("connected");
|
|
112
|
+
},
|
|
113
|
+
onSynced: () => {
|
|
114
|
+
setIsSynced(true);
|
|
115
|
+
updateConnectionState("synced");
|
|
116
|
+
onSync?.();
|
|
117
|
+
},
|
|
118
|
+
onDisconnect: () => {
|
|
119
|
+
updateConnectionState("disconnected");
|
|
120
|
+
setIsSynced(false);
|
|
121
|
+
if (autoReconnect && reconnectAttemptsRef.current < maxReconnectAttempts) {
|
|
122
|
+
reconnectAttemptsRef.current += 1;
|
|
123
|
+
const delay = Math.min(
|
|
124
|
+
1e3 * 2 ** reconnectAttemptsRef.current,
|
|
125
|
+
3e4
|
|
126
|
+
);
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
if (providerRef.current && !providerRef.current.isConnected) {
|
|
129
|
+
providerRef.current.connect();
|
|
130
|
+
}
|
|
131
|
+
}, delay);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
onAuthenticationFailed: ({ reason }) => {
|
|
135
|
+
const error = new Error(reason ?? "Authentication failed");
|
|
136
|
+
onAuthError?.(error);
|
|
137
|
+
updateConnectionState("disconnected");
|
|
138
|
+
},
|
|
139
|
+
onAwarenessUpdate: () => {
|
|
140
|
+
updateCollaborators(provider.awareness);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
provider.awareness?.setLocalStateField("user", {
|
|
144
|
+
name: user.name,
|
|
145
|
+
color: userColor,
|
|
146
|
+
userId: user.userId
|
|
147
|
+
});
|
|
148
|
+
providerRef.current = provider;
|
|
149
|
+
}, [
|
|
150
|
+
websocketUrl,
|
|
151
|
+
documentName,
|
|
152
|
+
doc,
|
|
153
|
+
token,
|
|
154
|
+
user.name,
|
|
155
|
+
user.userId,
|
|
156
|
+
userColor,
|
|
157
|
+
autoReconnect,
|
|
158
|
+
maxReconnectAttempts,
|
|
159
|
+
updateConnectionState,
|
|
160
|
+
updateCollaborators,
|
|
161
|
+
onSync,
|
|
162
|
+
onAuthError
|
|
163
|
+
]);
|
|
164
|
+
const disconnect = useCallback(() => {
|
|
165
|
+
if (providerRef.current) {
|
|
166
|
+
providerRef.current.disconnect();
|
|
167
|
+
updateConnectionState("disconnected");
|
|
168
|
+
}
|
|
169
|
+
}, [updateConnectionState]);
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (autoConnect) {
|
|
172
|
+
connect();
|
|
173
|
+
}
|
|
174
|
+
return () => {
|
|
175
|
+
if (providerRef.current) {
|
|
176
|
+
providerRef.current.destroy();
|
|
177
|
+
providerRef.current = null;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}, [autoConnect, connect]);
|
|
181
|
+
const contextValue = useMemo(
|
|
182
|
+
() => ({
|
|
183
|
+
doc,
|
|
184
|
+
provider: providerRef.current,
|
|
185
|
+
connectionState,
|
|
186
|
+
collaborators,
|
|
187
|
+
isSynced,
|
|
188
|
+
connect,
|
|
189
|
+
disconnect
|
|
190
|
+
}),
|
|
191
|
+
[doc, connectionState, collaborators, isSynced, connect, disconnect]
|
|
192
|
+
);
|
|
193
|
+
return /* @__PURE__ */ jsx(EditorContext.Provider, { value: contextValue, children });
|
|
194
|
+
}
|
|
195
|
+
function useEditorContext() {
|
|
196
|
+
const context = useContext(EditorContext);
|
|
197
|
+
if (!context) {
|
|
198
|
+
throw new Error("useEditorContext must be used within an EditorProvider");
|
|
199
|
+
}
|
|
200
|
+
return context;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/editor/tiptap-editor.tsx
|
|
204
|
+
import Collaboration from "@tiptap/extension-collaboration";
|
|
205
|
+
import CollaborationCursor from "@tiptap/extension-collaboration-cursor";
|
|
206
|
+
import { EditorContent, useEditor } from "@tiptap/react";
|
|
207
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
208
|
+
import { useEffect as useEffect2, useMemo as useMemo2 } from "react";
|
|
209
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
210
|
+
var cursorColors = {
|
|
211
|
+
"#FF6B6B": { background: "#FF6B6B", text: "#FFFFFF" },
|
|
212
|
+
"#4ECDC4": { background: "#4ECDC4", text: "#000000" },
|
|
213
|
+
"#45B7D1": { background: "#45B7D1", text: "#000000" },
|
|
214
|
+
"#96CEB4": { background: "#96CEB4", text: "#000000" },
|
|
215
|
+
"#FFEAA7": { background: "#FFEAA7", text: "#000000" },
|
|
216
|
+
"#DDA0DD": { background: "#DDA0DD", text: "#000000" },
|
|
217
|
+
"#98D8C8": { background: "#98D8C8", text: "#000000" },
|
|
218
|
+
"#F7DC6F": { background: "#F7DC6F", text: "#000000" },
|
|
219
|
+
"#BB8FCE": { background: "#BB8FCE", text: "#000000" },
|
|
220
|
+
"#85C1E9": { background: "#85C1E9", text: "#000000" }
|
|
221
|
+
};
|
|
222
|
+
function getCursorColors(color) {
|
|
223
|
+
return cursorColors[color] ?? { background: color, text: "#FFFFFF" };
|
|
224
|
+
}
|
|
225
|
+
function TiptapEditor({
|
|
226
|
+
initialContent,
|
|
227
|
+
placeholder = "Start writing...",
|
|
228
|
+
readOnly = false,
|
|
229
|
+
autoFocus = false,
|
|
230
|
+
className,
|
|
231
|
+
contentClassName,
|
|
232
|
+
onUpdate,
|
|
233
|
+
onSelectionUpdate,
|
|
234
|
+
onReady
|
|
235
|
+
}) {
|
|
236
|
+
const { doc, provider, connectionState } = useEditorContext();
|
|
237
|
+
const fragment = useMemo2(() => doc.getXmlFragment("prosemirror"), [doc]);
|
|
238
|
+
const extensions = useMemo2(() => {
|
|
239
|
+
const baseExtensions = [
|
|
240
|
+
StarterKit.configure({
|
|
241
|
+
// Disable history - Y.js handles undo/redo
|
|
242
|
+
...{ history: false },
|
|
243
|
+
// Configure code block for syntax highlighting placeholder
|
|
244
|
+
codeBlock: {
|
|
245
|
+
HTMLAttributes: {
|
|
246
|
+
class: "hljs"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}),
|
|
250
|
+
Collaboration.configure({
|
|
251
|
+
fragment
|
|
252
|
+
})
|
|
253
|
+
];
|
|
254
|
+
if (provider?.awareness) {
|
|
255
|
+
baseExtensions.push(
|
|
256
|
+
CollaborationCursor.configure({
|
|
257
|
+
provider,
|
|
258
|
+
user: provider.awareness.getLocalState()?.user ?? {
|
|
259
|
+
name: "Anonymous",
|
|
260
|
+
color: "#808080"
|
|
261
|
+
},
|
|
262
|
+
render: (user) => {
|
|
263
|
+
const { background, text } = getCursorColors(user.color);
|
|
264
|
+
const cursor = document.createElement("span");
|
|
265
|
+
cursor.className = "collaboration-cursor";
|
|
266
|
+
cursor.style.borderColor = background;
|
|
267
|
+
const label = document.createElement("span");
|
|
268
|
+
label.className = "collaboration-cursor-label";
|
|
269
|
+
label.style.backgroundColor = background;
|
|
270
|
+
label.style.color = text;
|
|
271
|
+
label.textContent = user.name;
|
|
272
|
+
cursor.appendChild(label);
|
|
273
|
+
return cursor;
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
return baseExtensions;
|
|
279
|
+
}, [fragment, provider]);
|
|
280
|
+
const editor = useEditor({
|
|
281
|
+
extensions,
|
|
282
|
+
editable: !readOnly,
|
|
283
|
+
autofocus: autoFocus,
|
|
284
|
+
editorProps: {
|
|
285
|
+
attributes: {
|
|
286
|
+
class: cn(
|
|
287
|
+
"prose prose-sm sm:prose-base dark:prose-invert max-w-none",
|
|
288
|
+
"focus:outline-none",
|
|
289
|
+
contentClassName
|
|
290
|
+
),
|
|
291
|
+
"data-placeholder": placeholder
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
onUpdate: ({ editor: ed }) => {
|
|
295
|
+
onUpdate?.(ed);
|
|
296
|
+
},
|
|
297
|
+
onSelectionUpdate: ({ editor: ed }) => {
|
|
298
|
+
onSelectionUpdate?.(ed);
|
|
299
|
+
},
|
|
300
|
+
onCreate: ({ editor: ed }) => {
|
|
301
|
+
onReady?.(ed);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
useEffect2(() => {
|
|
305
|
+
if (editor) {
|
|
306
|
+
editor.setEditable(!readOnly);
|
|
307
|
+
}
|
|
308
|
+
}, [editor, readOnly]);
|
|
309
|
+
useEffect2(() => {
|
|
310
|
+
if (editor && initialContent && connectionState === "synced" && editor.isEmpty) {
|
|
311
|
+
editor.commands.setContent(initialContent);
|
|
312
|
+
}
|
|
313
|
+
}, [editor, initialContent, connectionState]);
|
|
314
|
+
return /* @__PURE__ */ jsxs(
|
|
315
|
+
"div",
|
|
316
|
+
{
|
|
317
|
+
className: cn(
|
|
318
|
+
"relative min-h-[200px] w-full rounded-lg border border-border",
|
|
319
|
+
"bg-background",
|
|
320
|
+
className
|
|
321
|
+
),
|
|
322
|
+
children: [
|
|
323
|
+
/* @__PURE__ */ jsx2("div", { className: "absolute top-2 right-2 z-10", children: /* @__PURE__ */ jsx2(ConnectionIndicator, { state: connectionState }) }),
|
|
324
|
+
/* @__PURE__ */ jsx2("div", { className: "p-4 pt-10", children: /* @__PURE__ */ jsx2(EditorContent, { editor }) }),
|
|
325
|
+
/* @__PURE__ */ jsx2("style", { children: `
|
|
326
|
+
.ProseMirror p.is-editor-empty:first-child::before {
|
|
327
|
+
content: attr(data-placeholder);
|
|
328
|
+
float: left;
|
|
329
|
+
color: var(--muted-foreground, #999);
|
|
330
|
+
pointer-events: none;
|
|
331
|
+
height: 0;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.collaboration-cursor {
|
|
335
|
+
position: relative;
|
|
336
|
+
border-left: 2px solid;
|
|
337
|
+
margin-left: -1px;
|
|
338
|
+
margin-right: -1px;
|
|
339
|
+
pointer-events: none;
|
|
340
|
+
word-break: normal;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.collaboration-cursor-label {
|
|
344
|
+
position: absolute;
|
|
345
|
+
top: -1.4em;
|
|
346
|
+
left: -1px;
|
|
347
|
+
font-size: 12px;
|
|
348
|
+
font-style: normal;
|
|
349
|
+
font-weight: 600;
|
|
350
|
+
line-height: normal;
|
|
351
|
+
padding: 2px 6px;
|
|
352
|
+
border-radius: 4px 4px 4px 0;
|
|
353
|
+
white-space: nowrap;
|
|
354
|
+
user-select: none;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.ProseMirror pre {
|
|
358
|
+
background: var(--muted, #f4f4f5);
|
|
359
|
+
border-radius: 0.375rem;
|
|
360
|
+
padding: 0.75rem 1rem;
|
|
361
|
+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
362
|
+
font-size: 0.875rem;
|
|
363
|
+
overflow-x: auto;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.dark .ProseMirror pre {
|
|
367
|
+
background: var(--muted, #27272a);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.ProseMirror code {
|
|
371
|
+
background: var(--muted, #f4f4f5);
|
|
372
|
+
border-radius: 0.25rem;
|
|
373
|
+
padding: 0.125rem 0.25rem;
|
|
374
|
+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
375
|
+
font-size: 0.875em;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.dark .ProseMirror code {
|
|
379
|
+
background: var(--muted, #27272a);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.ProseMirror pre code {
|
|
383
|
+
background: transparent;
|
|
384
|
+
padding: 0;
|
|
385
|
+
font-size: inherit;
|
|
386
|
+
}
|
|
387
|
+
` })
|
|
388
|
+
]
|
|
389
|
+
}
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
function ConnectionIndicator({
|
|
393
|
+
state
|
|
394
|
+
}) {
|
|
395
|
+
const config = {
|
|
396
|
+
disconnected: {
|
|
397
|
+
color: "bg-red-500",
|
|
398
|
+
label: "Disconnected"
|
|
399
|
+
},
|
|
400
|
+
connecting: {
|
|
401
|
+
color: "bg-yellow-500 animate-pulse",
|
|
402
|
+
label: "Connecting..."
|
|
403
|
+
},
|
|
404
|
+
connected: {
|
|
405
|
+
color: "bg-blue-500",
|
|
406
|
+
label: "Connected"
|
|
407
|
+
},
|
|
408
|
+
synced: {
|
|
409
|
+
color: "bg-green-500",
|
|
410
|
+
label: "Synced"
|
|
411
|
+
}
|
|
412
|
+
}[state];
|
|
413
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-muted-foreground text-xs", children: [
|
|
414
|
+
/* @__PURE__ */ jsx2("span", { className: cn("h-2 w-2 rounded-full", config.color) }),
|
|
415
|
+
/* @__PURE__ */ jsx2("span", { children: config.label })
|
|
416
|
+
] });
|
|
417
|
+
}
|
|
418
|
+
function CollaboratorsList({
|
|
419
|
+
collaborators,
|
|
420
|
+
className
|
|
421
|
+
}) {
|
|
422
|
+
if (collaborators.length === 0) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-1", className), children: [
|
|
426
|
+
collaborators.slice(0, 5).map((collab) => /* @__PURE__ */ jsx2(
|
|
427
|
+
"div",
|
|
428
|
+
{
|
|
429
|
+
className: "flex h-6 w-6 items-center justify-center rounded-full font-medium text-xs",
|
|
430
|
+
style: { backgroundColor: collab.user.color },
|
|
431
|
+
title: collab.user.name,
|
|
432
|
+
children: collab.user.name.charAt(0).toUpperCase()
|
|
433
|
+
},
|
|
434
|
+
collab.clientId
|
|
435
|
+
)),
|
|
436
|
+
collaborators.length > 5 && /* @__PURE__ */ jsxs("div", { className: "flex h-6 w-6 items-center justify-center rounded-full bg-muted font-medium text-xs", children: [
|
|
437
|
+
"+",
|
|
438
|
+
collaborators.length - 5
|
|
439
|
+
] })
|
|
440
|
+
] });
|
|
441
|
+
}
|
|
442
|
+
function EditorToolbar({
|
|
443
|
+
editor,
|
|
444
|
+
className
|
|
445
|
+
}) {
|
|
446
|
+
if (!editor) {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
const buttons = [
|
|
450
|
+
{
|
|
451
|
+
id: "bold",
|
|
452
|
+
label: "Bold",
|
|
453
|
+
action: () => editor.chain().focus().toggleBold().run(),
|
|
454
|
+
isActive: editor.isActive("bold"),
|
|
455
|
+
shortcut: "Ctrl+B"
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
id: "italic",
|
|
459
|
+
label: "Italic",
|
|
460
|
+
action: () => editor.chain().focus().toggleItalic().run(),
|
|
461
|
+
isActive: editor.isActive("italic"),
|
|
462
|
+
shortcut: "Ctrl+I"
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
id: "strike",
|
|
466
|
+
label: "Strike",
|
|
467
|
+
action: () => editor.chain().focus().toggleStrike().run(),
|
|
468
|
+
isActive: editor.isActive("strike"),
|
|
469
|
+
shortcut: "Ctrl+Shift+X"
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
id: "code",
|
|
473
|
+
label: "Code",
|
|
474
|
+
action: () => editor.chain().focus().toggleCode().run(),
|
|
475
|
+
isActive: editor.isActive("code"),
|
|
476
|
+
shortcut: "Ctrl+E"
|
|
477
|
+
},
|
|
478
|
+
{ id: "sep-1", type: "separator" },
|
|
479
|
+
{
|
|
480
|
+
id: "h1",
|
|
481
|
+
label: "H1",
|
|
482
|
+
action: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
|
483
|
+
isActive: editor.isActive("heading", { level: 1 })
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
id: "h2",
|
|
487
|
+
label: "H2",
|
|
488
|
+
action: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
|
489
|
+
isActive: editor.isActive("heading", { level: 2 })
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
id: "h3",
|
|
493
|
+
label: "H3",
|
|
494
|
+
action: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
|
495
|
+
isActive: editor.isActive("heading", { level: 3 })
|
|
496
|
+
},
|
|
497
|
+
{ id: "sep-2", type: "separator" },
|
|
498
|
+
{
|
|
499
|
+
id: "bullet-list",
|
|
500
|
+
label: "Bullet List",
|
|
501
|
+
action: () => editor.chain().focus().toggleBulletList().run(),
|
|
502
|
+
isActive: editor.isActive("bulletList")
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
id: "ordered-list",
|
|
506
|
+
label: "Ordered List",
|
|
507
|
+
action: () => editor.chain().focus().toggleOrderedList().run(),
|
|
508
|
+
isActive: editor.isActive("orderedList")
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
id: "code-block",
|
|
512
|
+
label: "Code Block",
|
|
513
|
+
action: () => editor.chain().focus().toggleCodeBlock().run(),
|
|
514
|
+
isActive: editor.isActive("codeBlock")
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
id: "blockquote",
|
|
518
|
+
label: "Blockquote",
|
|
519
|
+
action: () => editor.chain().focus().toggleBlockquote().run(),
|
|
520
|
+
isActive: editor.isActive("blockquote")
|
|
521
|
+
}
|
|
522
|
+
];
|
|
523
|
+
return /* @__PURE__ */ jsx2(
|
|
524
|
+
"div",
|
|
525
|
+
{
|
|
526
|
+
className: cn(
|
|
527
|
+
"flex items-center gap-1 border-border border-b bg-muted/50 p-2",
|
|
528
|
+
className
|
|
529
|
+
),
|
|
530
|
+
children: buttons.map((button) => {
|
|
531
|
+
if ("type" in button && button.type === "separator") {
|
|
532
|
+
return /* @__PURE__ */ jsx2("div", { className: "mx-1 h-6 w-px bg-border" }, button.id);
|
|
533
|
+
}
|
|
534
|
+
return /* @__PURE__ */ jsx2(
|
|
535
|
+
"button",
|
|
536
|
+
{
|
|
537
|
+
onClick: button.action,
|
|
538
|
+
type: "button",
|
|
539
|
+
title: "shortcut" in button ? `${button.label} (${button.shortcut})` : button.label,
|
|
540
|
+
className: cn(
|
|
541
|
+
"rounded px-2 py-1 font-medium text-xs transition-colors",
|
|
542
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
543
|
+
button.isActive && "bg-accent text-accent-foreground"
|
|
544
|
+
),
|
|
545
|
+
children: button.label
|
|
546
|
+
},
|
|
547
|
+
button.id
|
|
548
|
+
);
|
|
549
|
+
})
|
|
550
|
+
}
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/editor/use-editor.ts
|
|
555
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useState as useState2 } from "react";
|
|
556
|
+
function useEditorConnection() {
|
|
557
|
+
const { connectionState, isSynced, connect, disconnect } = useEditorContext();
|
|
558
|
+
const isConnected = connectionState === "connected" || connectionState === "synced";
|
|
559
|
+
const isConnecting = connectionState === "connecting";
|
|
560
|
+
const isDisconnected = connectionState === "disconnected";
|
|
561
|
+
return {
|
|
562
|
+
/** Current connection state string */
|
|
563
|
+
state: connectionState,
|
|
564
|
+
/** Whether connected to the server (connected or synced) */
|
|
565
|
+
isConnected,
|
|
566
|
+
/** Whether currently attempting to connect */
|
|
567
|
+
isConnecting,
|
|
568
|
+
/** Whether disconnected from the server */
|
|
569
|
+
isDisconnected,
|
|
570
|
+
/** Whether the document is synced with the server */
|
|
571
|
+
isSynced,
|
|
572
|
+
/** Connect to the collaboration server */
|
|
573
|
+
connect,
|
|
574
|
+
/** Disconnect from the collaboration server */
|
|
575
|
+
disconnect
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function useCollaborators() {
|
|
579
|
+
const { collaborators } = useEditorContext();
|
|
580
|
+
const count = collaborators.length;
|
|
581
|
+
const hasOthers = count > 0;
|
|
582
|
+
return {
|
|
583
|
+
/** List of active collaborators (excluding self) */
|
|
584
|
+
collaborators,
|
|
585
|
+
/** Number of other collaborators */
|
|
586
|
+
count,
|
|
587
|
+
/** Whether there are other collaborators present */
|
|
588
|
+
hasOthers
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function useCollaboratorPresence(userId) {
|
|
592
|
+
const { collaborators } = useEditorContext();
|
|
593
|
+
return collaborators.find((c) => c.user.userId === userId) ?? null;
|
|
594
|
+
}
|
|
595
|
+
function useYjsState(key, initialValue) {
|
|
596
|
+
const { doc } = useEditorContext();
|
|
597
|
+
const [value, setLocalValue] = useState2(initialValue);
|
|
598
|
+
const metaMap = doc.getMap("metadata");
|
|
599
|
+
useEffect3(() => {
|
|
600
|
+
const updateValue = () => {
|
|
601
|
+
const stored = metaMap.get(key);
|
|
602
|
+
if (stored !== void 0) {
|
|
603
|
+
setLocalValue(stored);
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
updateValue();
|
|
607
|
+
metaMap.observe(updateValue);
|
|
608
|
+
return () => {
|
|
609
|
+
metaMap.unobserve(updateValue);
|
|
610
|
+
};
|
|
611
|
+
}, [metaMap, key]);
|
|
612
|
+
const setValue = useCallback2(
|
|
613
|
+
(newValue) => {
|
|
614
|
+
doc.transact(() => {
|
|
615
|
+
metaMap.set(key, newValue);
|
|
616
|
+
});
|
|
617
|
+
},
|
|
618
|
+
[doc, metaMap, key]
|
|
619
|
+
);
|
|
620
|
+
return [value, setValue];
|
|
621
|
+
}
|
|
622
|
+
function useDocumentChanges(onSave) {
|
|
623
|
+
const { doc } = useEditorContext();
|
|
624
|
+
const [isDirty, setIsDirty] = useState2(false);
|
|
625
|
+
const [isSaving, setIsSaving] = useState2(false);
|
|
626
|
+
const [lastSaved, setLastSaved] = useState2(null);
|
|
627
|
+
useEffect3(() => {
|
|
628
|
+
const handleUpdate = () => {
|
|
629
|
+
setIsDirty(true);
|
|
630
|
+
};
|
|
631
|
+
doc.on("update", handleUpdate);
|
|
632
|
+
return () => {
|
|
633
|
+
doc.off("update", handleUpdate);
|
|
634
|
+
};
|
|
635
|
+
}, [doc]);
|
|
636
|
+
const save = useCallback2(async () => {
|
|
637
|
+
if (!onSave || isSaving) return;
|
|
638
|
+
setIsSaving(true);
|
|
639
|
+
try {
|
|
640
|
+
await onSave();
|
|
641
|
+
setIsDirty(false);
|
|
642
|
+
setLastSaved(/* @__PURE__ */ new Date());
|
|
643
|
+
} finally {
|
|
644
|
+
setIsSaving(false);
|
|
645
|
+
}
|
|
646
|
+
}, [onSave, isSaving]);
|
|
647
|
+
return {
|
|
648
|
+
/** Whether there are unsaved changes */
|
|
649
|
+
isDirty,
|
|
650
|
+
/** Whether save is in progress */
|
|
651
|
+
isSaving,
|
|
652
|
+
/** When the document was last saved */
|
|
653
|
+
lastSaved,
|
|
654
|
+
/** Save the document */
|
|
655
|
+
save
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
function useAwareness() {
|
|
659
|
+
const { provider } = useEditorContext();
|
|
660
|
+
const [localState, setLocalStateValue] = useState2(
|
|
661
|
+
{}
|
|
662
|
+
);
|
|
663
|
+
const awareness = provider?.awareness;
|
|
664
|
+
useEffect3(() => {
|
|
665
|
+
if (!awareness) return;
|
|
666
|
+
const updateState = () => {
|
|
667
|
+
setLocalStateValue(awareness.getLocalState() ?? {});
|
|
668
|
+
};
|
|
669
|
+
updateState();
|
|
670
|
+
awareness.on("change", updateState);
|
|
671
|
+
return () => {
|
|
672
|
+
awareness.off("change", updateState);
|
|
673
|
+
};
|
|
674
|
+
}, [awareness]);
|
|
675
|
+
const setLocalState = useCallback2(
|
|
676
|
+
(state) => {
|
|
677
|
+
if (!awareness) return;
|
|
678
|
+
const current = awareness.getLocalState() ?? {};
|
|
679
|
+
awareness.setLocalState({ ...current, ...state });
|
|
680
|
+
},
|
|
681
|
+
[awareness]
|
|
682
|
+
);
|
|
683
|
+
const setLocalStateField = useCallback2(
|
|
684
|
+
(field, value) => {
|
|
685
|
+
if (!awareness) return;
|
|
686
|
+
awareness.setLocalStateField(field, value);
|
|
687
|
+
},
|
|
688
|
+
[awareness]
|
|
689
|
+
);
|
|
690
|
+
return {
|
|
691
|
+
/** Current local awareness state */
|
|
692
|
+
localState,
|
|
693
|
+
/** Set the entire local state (merges with existing) */
|
|
694
|
+
setLocalState,
|
|
695
|
+
/** Set a single field in the local state */
|
|
696
|
+
setLocalStateField,
|
|
697
|
+
/** The raw awareness instance */
|
|
698
|
+
awareness
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
export {
|
|
702
|
+
CollaboratorsList,
|
|
703
|
+
EditorProvider,
|
|
704
|
+
EditorToolbar,
|
|
705
|
+
TiptapEditor,
|
|
706
|
+
useAwareness,
|
|
707
|
+
useCollaboratorPresence,
|
|
708
|
+
useCollaborators,
|
|
709
|
+
useDocumentChanges,
|
|
710
|
+
useEditorConnection,
|
|
711
|
+
useEditorContext,
|
|
712
|
+
useYjsState
|
|
713
|
+
};
|