@tangle-network/sandbox-ui 0.3.11 → 0.3.13

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.
Files changed (37) hide show
  1. package/README.md +7 -1
  2. package/dist/auth.js +2 -3
  3. package/dist/{chunk-CREVWUCA.js → chunk-DJEZKF5A.js} +3 -2
  4. package/dist/chunk-DLCFZDGX.js +182 -0
  5. package/dist/{chunk-FOQTE67I.js → chunk-FJLS7PNT.js} +9 -4
  6. package/dist/chunk-HXEA7L2T.js +1401 -0
  7. package/dist/{chunk-6NYG2R7V.js → chunk-HYLTXGOI.js} +1 -1
  8. package/dist/{chunk-MCGKDCOR.js → chunk-IW2JZCOC.js} +55 -14
  9. package/dist/{chunk-PCTEG6HR.js → chunk-OHMO7NUX.js} +2 -4
  10. package/dist/{chunk-DMYYQXPN.js → chunk-SMBF6HB5.js} +646 -465
  11. package/dist/dashboard.d.ts +1 -1
  12. package/dist/dashboard.js +40 -6
  13. package/dist/{document-editor-pane-AFBP2KFT.js → document-editor-pane-5TN2VWGZ.js} +1 -1
  14. package/dist/{document-editor-pane-Xnl8SmA7.d.ts → document-editor-pane-A70-EhdQ.d.ts} +1 -1
  15. package/dist/editor.d.ts +2 -2
  16. package/dist/editor.js +1 -1
  17. package/dist/files.d.ts +1 -1
  18. package/dist/files.js +1 -1
  19. package/dist/hooks.d.ts +1 -1
  20. package/dist/hooks.js +2 -2
  21. package/dist/index-D7_ZDkwB.d.ts +375 -0
  22. package/dist/index.d.ts +5 -3
  23. package/dist/index.js +74 -16
  24. package/dist/pages.d.ts +12 -2
  25. package/dist/pages.js +60 -5
  26. package/dist/primitives.js +4 -6
  27. package/dist/sdk-hooks.js +1 -1
  28. package/dist/terminal.d.ts +2 -2
  29. package/dist/terminal.js +9 -39
  30. package/dist/{use-pty-session-DeZSxOCN.d.ts → use-pty-session-COzVkhtc.d.ts} +1 -1
  31. package/dist/workspace.d.ts +3 -1
  32. package/dist/workspace.js +2 -2
  33. package/package.json +1 -1
  34. package/dist/chunk-B26TQ7SA.js +0 -47
  35. package/dist/chunk-BOBXH6CH.js +0 -10981
  36. package/dist/chunk-GRYHFH5O.js +0 -110
  37. package/dist/index-BJIPTCKk.d.ts +0 -264
@@ -0,0 +1,1401 @@
1
+ import {
2
+ Tabs,
3
+ TabsContent,
4
+ TabsList,
5
+ TabsTrigger
6
+ } from "./chunk-Q56BYXQF.js";
7
+ import {
8
+ ArtifactPane
9
+ } from "./chunk-W4LM3QYZ.js";
10
+ import {
11
+ Markdown
12
+ } from "./chunk-LTFK464G.js";
13
+ import {
14
+ cn
15
+ } from "./chunk-RQHJBTEU.js";
16
+
17
+ // src/editor/document-editor-pane.tsx
18
+ import { useEffect as useEffect5, useMemo as useMemo4, useState as useState3 } from "react";
19
+ import { PencilLine, Save, Users, Wifi, WifiOff } from "lucide-react";
20
+
21
+ // src/editor/tiptap-editor.tsx
22
+ import Collaboration from "@tiptap/extension-collaboration";
23
+
24
+ // node_modules/@tiptap/extension-collaboration-caret/dist/index.js
25
+ import { Extension } from "@tiptap/core";
26
+ import { defaultSelectionBuilder, yCursorPlugin } from "@tiptap/y-tiptap";
27
+ var awarenessStatesToArray = (states) => {
28
+ return Array.from(states.entries()).map(([key, value]) => {
29
+ return {
30
+ clientId: key,
31
+ ...value.user
32
+ };
33
+ });
34
+ };
35
+ var defaultOnUpdate = () => null;
36
+ var CollaborationCaret = Extension.create({
37
+ name: "collaborationCaret",
38
+ priority: 999,
39
+ addOptions() {
40
+ return {
41
+ provider: null,
42
+ user: {
43
+ name: null,
44
+ color: null
45
+ },
46
+ render: (user) => {
47
+ const cursor = document.createElement("span");
48
+ cursor.classList.add("collaboration-carets__caret");
49
+ cursor.setAttribute("style", `border-color: ${user.color}`);
50
+ const label = document.createElement("div");
51
+ label.classList.add("collaboration-carets__label");
52
+ label.setAttribute("style", `background-color: ${user.color}`);
53
+ label.insertBefore(document.createTextNode(user.name), null);
54
+ cursor.insertBefore(label, null);
55
+ return cursor;
56
+ },
57
+ selectionRender: defaultSelectionBuilder,
58
+ onUpdate: defaultOnUpdate
59
+ };
60
+ },
61
+ onCreate() {
62
+ if (this.options.onUpdate !== defaultOnUpdate) {
63
+ console.warn(
64
+ '[tiptap warn]: DEPRECATED: The "onUpdate" option is deprecated. Please use `editor.storage.collaborationCaret.users` instead. Read more: https://tiptap.dev/api/extensions/collaboration-caret'
65
+ );
66
+ }
67
+ if (!this.options.provider) {
68
+ throw new Error('The "provider" option is required for the CollaborationCaret extension');
69
+ }
70
+ },
71
+ addStorage() {
72
+ return {
73
+ users: []
74
+ };
75
+ },
76
+ addCommands() {
77
+ return {
78
+ updateUser: (attributes) => () => {
79
+ this.options.provider.awareness.setLocalStateField("user", attributes);
80
+ return true;
81
+ },
82
+ user: (attributes) => ({ editor }) => {
83
+ console.warn(
84
+ '[tiptap warn]: DEPRECATED: The "user" command is deprecated. Please use "updateUser" instead. Read more: https://tiptap.dev/api/extensions/collaboration-caret'
85
+ );
86
+ return editor.commands.updateUser(attributes);
87
+ }
88
+ };
89
+ },
90
+ addProseMirrorPlugins() {
91
+ return [
92
+ yCursorPlugin(
93
+ (() => {
94
+ this.options.provider.awareness.setLocalStateField("user", this.options.user);
95
+ this.storage.users = awarenessStatesToArray(this.options.provider.awareness.states);
96
+ this.options.provider.awareness.on("update", () => {
97
+ this.storage.users = awarenessStatesToArray(this.options.provider.awareness.states);
98
+ });
99
+ return this.options.provider.awareness;
100
+ })(),
101
+ {
102
+ cursorBuilder: this.options.render,
103
+ selectionBuilder: this.options.selectionRender
104
+ }
105
+ )
106
+ ];
107
+ }
108
+ });
109
+ var index_default = CollaborationCaret;
110
+
111
+ // src/editor/tiptap-editor.tsx
112
+ import { EditorContent, useEditor } from "@tiptap/react";
113
+ import StarterKit from "@tiptap/starter-kit";
114
+ import { useEffect as useEffect2, useMemo as useMemo2 } from "react";
115
+
116
+ // src/editor/editor-provider.tsx
117
+ import { HocuspocusProvider } from "@hocuspocus/provider";
118
+ import {
119
+ createContext,
120
+ useCallback,
121
+ useContext,
122
+ useEffect,
123
+ useMemo,
124
+ useRef,
125
+ useState
126
+ } from "react";
127
+ import * as Y from "yjs";
128
+ import { jsx } from "react/jsx-runtime";
129
+ var EditorContext = createContext(null);
130
+ function generateUserColor() {
131
+ const colors = [
132
+ "#FF6B6B",
133
+ // Red
134
+ "#4ECDC4",
135
+ // Teal
136
+ "#45B7D1",
137
+ // Blue
138
+ "#96CEB4",
139
+ // Green
140
+ "#FFEAA7",
141
+ // Yellow
142
+ "#DDA0DD",
143
+ // Plum
144
+ "#98D8C8",
145
+ // Mint
146
+ "#F7DC6F",
147
+ // Gold
148
+ "#BB8FCE",
149
+ // Purple
150
+ "#85C1E9"
151
+ // Sky
152
+ ];
153
+ return colors[Math.floor(Math.random() * colors.length)];
154
+ }
155
+ function EditorProvider({
156
+ websocketUrl,
157
+ documentName,
158
+ token,
159
+ tokenExpiresAt,
160
+ user,
161
+ autoConnect = true,
162
+ autoReconnect = true,
163
+ maxReconnectAttempts = 5,
164
+ onConnectionChange,
165
+ onSync,
166
+ onAuthError,
167
+ onRefreshToken,
168
+ children
169
+ }) {
170
+ const [connectionState, setConnectionState] = useState("disconnected");
171
+ const [collaborators, setCollaborators] = useState([]);
172
+ const [isSynced, setIsSynced] = useState(false);
173
+ const docRef = useRef(null);
174
+ const providerRef = useRef(null);
175
+ const reconnectAttemptsRef = useRef(0);
176
+ const tokenRef = useRef(token);
177
+ const tokenExpiryRef = useRef(tokenExpiresAt);
178
+ const refreshPromiseRef = useRef(null);
179
+ const refreshTimerRef = useRef(null);
180
+ tokenRef.current = token;
181
+ tokenExpiryRef.current = tokenExpiresAt;
182
+ if (!docRef.current) {
183
+ docRef.current = new Y.Doc();
184
+ }
185
+ const doc = docRef.current;
186
+ const userColor = useMemo(
187
+ () => user.color ?? generateUserColor(),
188
+ [user.color]
189
+ );
190
+ const updateConnectionState = useCallback(
191
+ (state) => {
192
+ setConnectionState(state);
193
+ onConnectionChange?.(state);
194
+ },
195
+ [onConnectionChange]
196
+ );
197
+ const updateCollaborators = useCallback(
198
+ (awareness) => {
199
+ if (!awareness) return;
200
+ const states = awareness.getStates();
201
+ const collabs = [];
202
+ states.forEach((state, clientId) => {
203
+ if (clientId === awareness.clientID) return;
204
+ if (state.user) {
205
+ collabs.push({
206
+ clientId,
207
+ user: state.user
208
+ });
209
+ }
210
+ });
211
+ setCollaborators(collabs);
212
+ },
213
+ []
214
+ );
215
+ const clearRefreshTimer = useCallback(() => {
216
+ if (refreshTimerRef.current != null) {
217
+ window.clearTimeout(refreshTimerRef.current);
218
+ refreshTimerRef.current = null;
219
+ }
220
+ }, []);
221
+ const refreshToken = useCallback(async () => {
222
+ if (!onRefreshToken) {
223
+ return null;
224
+ }
225
+ if (refreshPromiseRef.current) {
226
+ return refreshPromiseRef.current;
227
+ }
228
+ const refreshPromise = (async () => {
229
+ const next = await onRefreshToken();
230
+ const resolvedToken = typeof next === "string" ? next : next.token;
231
+ const resolvedExpiry = typeof next === "string" ? void 0 : next.expiresAt;
232
+ tokenRef.current = resolvedToken;
233
+ tokenExpiryRef.current = resolvedExpiry;
234
+ return resolvedToken;
235
+ })().catch((error) => {
236
+ onAuthError?.(
237
+ error instanceof Error ? error : new Error(String(error))
238
+ );
239
+ return null;
240
+ }).finally(() => {
241
+ refreshPromiseRef.current = null;
242
+ });
243
+ refreshPromiseRef.current = refreshPromise;
244
+ return refreshPromise;
245
+ }, [onAuthError, onRefreshToken]);
246
+ const scheduleTokenRefresh = useCallback(() => {
247
+ clearRefreshTimer();
248
+ if (!tokenExpiryRef.current || !onRefreshToken || typeof window === "undefined") {
249
+ return;
250
+ }
251
+ const refreshAtMs = tokenExpiryRef.current * 1e3 - 6e4;
252
+ const delay = refreshAtMs - Date.now();
253
+ if (delay <= 0) {
254
+ void refreshToken();
255
+ return;
256
+ }
257
+ refreshTimerRef.current = window.setTimeout(() => {
258
+ void refreshToken();
259
+ }, delay);
260
+ }, [clearRefreshTimer, onRefreshToken, refreshToken]);
261
+ const connect = useCallback(() => {
262
+ if (providerRef.current) {
263
+ providerRef.current.connect();
264
+ return;
265
+ }
266
+ updateConnectionState("connecting");
267
+ const provider = new HocuspocusProvider({
268
+ url: websocketUrl,
269
+ name: documentName,
270
+ document: doc,
271
+ token: async () => tokenRef.current,
272
+ // @ts-expect-error -- connect is valid at runtime but missing from type defs
273
+ connect: true,
274
+ onConnect: () => {
275
+ reconnectAttemptsRef.current = 0;
276
+ updateConnectionState("connected");
277
+ scheduleTokenRefresh();
278
+ },
279
+ onSynced: () => {
280
+ setIsSynced(true);
281
+ updateConnectionState("synced");
282
+ onSync?.();
283
+ },
284
+ onDisconnect: () => {
285
+ updateConnectionState("disconnected");
286
+ setIsSynced(false);
287
+ clearRefreshTimer();
288
+ if (autoReconnect && reconnectAttemptsRef.current < maxReconnectAttempts) {
289
+ reconnectAttemptsRef.current += 1;
290
+ const delay = Math.min(
291
+ 1e3 * 2 ** reconnectAttemptsRef.current,
292
+ 3e4
293
+ );
294
+ setTimeout(() => {
295
+ if (providerRef.current && !providerRef.current.isConnected) {
296
+ providerRef.current.connect();
297
+ }
298
+ }, delay);
299
+ }
300
+ },
301
+ onAuthenticationFailed: ({ reason }) => {
302
+ const error = new Error(reason ?? "Authentication failed");
303
+ updateConnectionState("disconnected");
304
+ clearRefreshTimer();
305
+ if (onRefreshToken) {
306
+ void refreshToken().then((nextToken) => {
307
+ if (nextToken && providerRef.current) {
308
+ providerRef.current.connect();
309
+ return;
310
+ }
311
+ onAuthError?.(error);
312
+ });
313
+ return;
314
+ }
315
+ onAuthError?.(error);
316
+ },
317
+ onAwarenessUpdate: () => {
318
+ updateCollaborators(provider.awareness);
319
+ }
320
+ });
321
+ provider.awareness?.setLocalStateField("user", {
322
+ name: user.name,
323
+ color: userColor,
324
+ userId: user.userId
325
+ });
326
+ providerRef.current = provider;
327
+ }, [
328
+ websocketUrl,
329
+ documentName,
330
+ doc,
331
+ user.name,
332
+ user.userId,
333
+ userColor,
334
+ autoReconnect,
335
+ maxReconnectAttempts,
336
+ clearRefreshTimer,
337
+ updateConnectionState,
338
+ updateCollaborators,
339
+ onSync,
340
+ onAuthError,
341
+ onRefreshToken,
342
+ refreshToken,
343
+ scheduleTokenRefresh
344
+ ]);
345
+ const disconnect = useCallback(() => {
346
+ if (providerRef.current) {
347
+ providerRef.current.disconnect();
348
+ updateConnectionState("disconnected");
349
+ }
350
+ }, [updateConnectionState]);
351
+ useEffect(() => {
352
+ if (autoConnect) {
353
+ connect();
354
+ }
355
+ return () => {
356
+ clearRefreshTimer();
357
+ if (providerRef.current) {
358
+ providerRef.current.destroy();
359
+ providerRef.current = null;
360
+ }
361
+ };
362
+ }, [autoConnect, clearRefreshTimer, connect]);
363
+ const contextValue = useMemo(
364
+ () => ({
365
+ doc,
366
+ provider: providerRef.current,
367
+ connectionState,
368
+ collaborators,
369
+ isSynced,
370
+ connect,
371
+ disconnect
372
+ }),
373
+ [doc, connectionState, collaborators, isSynced, connect, disconnect]
374
+ );
375
+ return /* @__PURE__ */ jsx(EditorContext.Provider, { value: contextValue, children });
376
+ }
377
+ function useEditorContext() {
378
+ const context = useContext(EditorContext);
379
+ if (!context) {
380
+ throw new Error("useEditorContext must be used within an EditorProvider");
381
+ }
382
+ return context;
383
+ }
384
+
385
+ // src/editor/tiptap-editor.tsx
386
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
387
+ var cursorColors = {
388
+ "#FF6B6B": { background: "#FF6B6B", text: "#FFFFFF" },
389
+ "#4ECDC4": { background: "#4ECDC4", text: "#000000" },
390
+ "#45B7D1": { background: "#45B7D1", text: "#000000" },
391
+ "#96CEB4": { background: "#96CEB4", text: "#000000" },
392
+ "#FFEAA7": { background: "#FFEAA7", text: "#000000" },
393
+ "#DDA0DD": { background: "#DDA0DD", text: "#000000" },
394
+ "#98D8C8": { background: "#98D8C8", text: "#000000" },
395
+ "#F7DC6F": { background: "#F7DC6F", text: "#000000" },
396
+ "#BB8FCE": { background: "#BB8FCE", text: "#000000" },
397
+ "#85C1E9": { background: "#85C1E9", text: "#000000" }
398
+ };
399
+ function getCursorColors(color) {
400
+ return cursorColors[color] ?? { background: color, text: "#FFFFFF" };
401
+ }
402
+ function TiptapEditor({
403
+ initialContent,
404
+ placeholder = "Start writing...",
405
+ readOnly = false,
406
+ autoFocus = false,
407
+ className,
408
+ contentClassName,
409
+ onUpdate,
410
+ onSelectionUpdate,
411
+ onReady
412
+ }) {
413
+ const { doc, provider, connectionState } = useEditorContext();
414
+ const fragment = useMemo2(() => doc.getXmlFragment("prosemirror"), [doc]);
415
+ const extensions = useMemo2(() => {
416
+ const baseExtensions = [
417
+ StarterKit.configure({
418
+ // Disable history - Y.js handles undo/redo
419
+ ...{ history: false },
420
+ // Configure code block for syntax highlighting placeholder
421
+ codeBlock: {
422
+ HTMLAttributes: {
423
+ class: "hljs"
424
+ }
425
+ }
426
+ }),
427
+ Collaboration.configure({
428
+ fragment
429
+ })
430
+ ];
431
+ if (provider?.awareness) {
432
+ baseExtensions.push(
433
+ index_default.configure({
434
+ provider,
435
+ user: provider.awareness.getLocalState()?.user ?? {
436
+ name: "Anonymous",
437
+ color: "#808080"
438
+ },
439
+ render: (user) => {
440
+ const { background, text } = getCursorColors(user.color);
441
+ const cursor = document.createElement("span");
442
+ cursor.className = "collaboration-cursor";
443
+ cursor.style.borderColor = background;
444
+ const label = document.createElement("span");
445
+ label.className = "collaboration-cursor-label";
446
+ label.style.backgroundColor = background;
447
+ label.style.color = text;
448
+ label.textContent = user.name;
449
+ cursor.appendChild(label);
450
+ return cursor;
451
+ }
452
+ })
453
+ );
454
+ }
455
+ return baseExtensions;
456
+ }, [fragment, provider]);
457
+ const editor = useEditor({
458
+ extensions,
459
+ editable: !readOnly,
460
+ autofocus: autoFocus,
461
+ editorProps: {
462
+ attributes: {
463
+ class: cn(
464
+ "prose prose-sm sm:prose-base dark:prose-invert max-w-none",
465
+ "focus:outline-none",
466
+ contentClassName
467
+ ),
468
+ "data-placeholder": placeholder
469
+ }
470
+ },
471
+ onUpdate: ({ editor: ed }) => {
472
+ onUpdate?.(ed);
473
+ },
474
+ onSelectionUpdate: ({ editor: ed }) => {
475
+ onSelectionUpdate?.(ed);
476
+ },
477
+ onCreate: ({ editor: ed }) => {
478
+ onReady?.(ed);
479
+ }
480
+ });
481
+ useEffect2(() => {
482
+ if (editor) {
483
+ editor.setEditable(!readOnly);
484
+ }
485
+ }, [editor, readOnly]);
486
+ useEffect2(() => {
487
+ if (editor && initialContent && connectionState === "synced" && editor.isEmpty) {
488
+ editor.commands.setContent(initialContent);
489
+ }
490
+ }, [editor, initialContent, connectionState]);
491
+ return /* @__PURE__ */ jsxs(
492
+ "div",
493
+ {
494
+ className: cn(
495
+ "relative min-h-[200px] w-full rounded-lg border border-border",
496
+ "bg-background",
497
+ className
498
+ ),
499
+ children: [
500
+ /* @__PURE__ */ jsx2("div", { className: "absolute top-2 right-2 z-10", children: /* @__PURE__ */ jsx2(ConnectionIndicator, { state: connectionState }) }),
501
+ /* @__PURE__ */ jsx2("div", { className: "p-4 pt-10", children: /* @__PURE__ */ jsx2(EditorContent, { editor }) }),
502
+ /* @__PURE__ */ jsx2("style", { children: `
503
+ .ProseMirror p.is-editor-empty:first-child::before {
504
+ content: attr(data-placeholder);
505
+ float: left;
506
+ color: var(--muted-foreground, #999);
507
+ pointer-events: none;
508
+ height: 0;
509
+ }
510
+
511
+ .collaboration-cursor {
512
+ position: relative;
513
+ border-left: 2px solid;
514
+ margin-left: -1px;
515
+ margin-right: -1px;
516
+ pointer-events: none;
517
+ word-break: normal;
518
+ }
519
+
520
+ .collaboration-cursor-label {
521
+ position: absolute;
522
+ top: -1.4em;
523
+ left: -1px;
524
+ font-size: 12px;
525
+ font-style: normal;
526
+ font-weight: 600;
527
+ line-height: normal;
528
+ padding: 2px 6px;
529
+ border-radius: 4px 4px 4px 0;
530
+ white-space: nowrap;
531
+ user-select: none;
532
+ }
533
+
534
+ .ProseMirror pre {
535
+ background: var(--muted, #f4f4f5);
536
+ border-radius: 0.375rem;
537
+ padding: 0.75rem 1rem;
538
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
539
+ font-size: 0.875rem;
540
+ overflow-x: auto;
541
+ }
542
+
543
+ .dark .ProseMirror pre {
544
+ background: var(--muted, #27272a);
545
+ }
546
+
547
+ .ProseMirror code {
548
+ background: var(--muted, #f4f4f5);
549
+ border-radius: 0.25rem;
550
+ padding: 0.125rem 0.25rem;
551
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
552
+ font-size: 0.875em;
553
+ }
554
+
555
+ .dark .ProseMirror code {
556
+ background: var(--muted, #27272a);
557
+ }
558
+
559
+ .ProseMirror pre code {
560
+ background: transparent;
561
+ padding: 0;
562
+ font-size: inherit;
563
+ }
564
+ ` })
565
+ ]
566
+ }
567
+ );
568
+ }
569
+ function ConnectionIndicator({
570
+ state
571
+ }) {
572
+ const config = {
573
+ disconnected: {
574
+ color: "bg-red-500",
575
+ label: "Disconnected"
576
+ },
577
+ connecting: {
578
+ color: "bg-yellow-500 animate-pulse",
579
+ label: "Connecting..."
580
+ },
581
+ connected: {
582
+ color: "bg-blue-500",
583
+ label: "Connected"
584
+ },
585
+ synced: {
586
+ color: "bg-green-500",
587
+ label: "Synced"
588
+ }
589
+ }[state];
590
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-muted-foreground text-xs", children: [
591
+ /* @__PURE__ */ jsx2("span", { className: cn("h-2 w-2 rounded-full", config.color) }),
592
+ /* @__PURE__ */ jsx2("span", { children: config.label })
593
+ ] });
594
+ }
595
+ function CollaboratorsList({
596
+ collaborators,
597
+ className
598
+ }) {
599
+ if (collaborators.length === 0) {
600
+ return null;
601
+ }
602
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-1", className), children: [
603
+ collaborators.slice(0, 5).map((collab) => /* @__PURE__ */ jsx2(
604
+ "div",
605
+ {
606
+ className: "flex h-6 w-6 items-center justify-center rounded-full font-medium text-xs",
607
+ style: { backgroundColor: collab.user.color },
608
+ title: collab.user.name,
609
+ children: collab.user.name.charAt(0).toUpperCase()
610
+ },
611
+ collab.clientId
612
+ )),
613
+ 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: [
614
+ "+",
615
+ collaborators.length - 5
616
+ ] })
617
+ ] });
618
+ }
619
+ function EditorToolbar({
620
+ editor,
621
+ className
622
+ }) {
623
+ if (!editor) {
624
+ return null;
625
+ }
626
+ const buttons = [
627
+ {
628
+ id: "bold",
629
+ label: "Bold",
630
+ action: () => editor.chain().focus().toggleBold().run(),
631
+ isActive: editor.isActive("bold"),
632
+ shortcut: "Ctrl+B"
633
+ },
634
+ {
635
+ id: "italic",
636
+ label: "Italic",
637
+ action: () => editor.chain().focus().toggleItalic().run(),
638
+ isActive: editor.isActive("italic"),
639
+ shortcut: "Ctrl+I"
640
+ },
641
+ {
642
+ id: "strike",
643
+ label: "Strike",
644
+ action: () => editor.chain().focus().toggleStrike().run(),
645
+ isActive: editor.isActive("strike"),
646
+ shortcut: "Ctrl+Shift+X"
647
+ },
648
+ {
649
+ id: "code",
650
+ label: "Code",
651
+ action: () => editor.chain().focus().toggleCode().run(),
652
+ isActive: editor.isActive("code"),
653
+ shortcut: "Ctrl+E"
654
+ },
655
+ { id: "sep-1", type: "separator" },
656
+ {
657
+ id: "h1",
658
+ label: "H1",
659
+ action: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
660
+ isActive: editor.isActive("heading", { level: 1 })
661
+ },
662
+ {
663
+ id: "h2",
664
+ label: "H2",
665
+ action: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
666
+ isActive: editor.isActive("heading", { level: 2 })
667
+ },
668
+ {
669
+ id: "h3",
670
+ label: "H3",
671
+ action: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
672
+ isActive: editor.isActive("heading", { level: 3 })
673
+ },
674
+ { id: "sep-2", type: "separator" },
675
+ {
676
+ id: "bullet-list",
677
+ label: "Bullet List",
678
+ action: () => editor.chain().focus().toggleBulletList().run(),
679
+ isActive: editor.isActive("bulletList")
680
+ },
681
+ {
682
+ id: "ordered-list",
683
+ label: "Ordered List",
684
+ action: () => editor.chain().focus().toggleOrderedList().run(),
685
+ isActive: editor.isActive("orderedList")
686
+ },
687
+ {
688
+ id: "code-block",
689
+ label: "Code Block",
690
+ action: () => editor.chain().focus().toggleCodeBlock().run(),
691
+ isActive: editor.isActive("codeBlock")
692
+ },
693
+ {
694
+ id: "blockquote",
695
+ label: "Blockquote",
696
+ action: () => editor.chain().focus().toggleBlockquote().run(),
697
+ isActive: editor.isActive("blockquote")
698
+ }
699
+ ];
700
+ return /* @__PURE__ */ jsx2(
701
+ "div",
702
+ {
703
+ className: cn(
704
+ "flex items-center gap-1 border-border border-b bg-muted/50 p-2",
705
+ className
706
+ ),
707
+ children: buttons.map((button) => {
708
+ if ("type" in button && button.type === "separator") {
709
+ return /* @__PURE__ */ jsx2("div", { className: "mx-1 h-6 w-px bg-border" }, button.id);
710
+ }
711
+ return /* @__PURE__ */ jsx2(
712
+ "button",
713
+ {
714
+ onClick: button.action,
715
+ type: "button",
716
+ title: "shortcut" in button ? `${button.label} (${button.shortcut})` : button.label,
717
+ className: cn(
718
+ "rounded px-2 py-1 font-medium text-xs transition-colors",
719
+ "hover:bg-accent hover:text-accent-foreground",
720
+ button.isActive && "bg-accent text-accent-foreground"
721
+ ),
722
+ children: button.label
723
+ },
724
+ button.id
725
+ );
726
+ })
727
+ }
728
+ );
729
+ }
730
+
731
+ // src/editor/markdown-document-editor.tsx
732
+ import { EditorContent as EditorContent2, useEditor as useEditor2 } from "@tiptap/react";
733
+ import StarterKit2 from "@tiptap/starter-kit";
734
+ import { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2 } from "react";
735
+
736
+ // src/editor/editor-toolbar.tsx
737
+ import { jsx as jsx3 } from "react/jsx-runtime";
738
+ function EditorToolbar2({
739
+ editor,
740
+ className
741
+ }) {
742
+ if (!editor) {
743
+ return null;
744
+ }
745
+ const buttons = [
746
+ {
747
+ id: "bold",
748
+ label: "Bold",
749
+ action: () => editor.chain().focus().toggleBold().run(),
750
+ isActive: editor.isActive("bold"),
751
+ shortcut: "Ctrl+B"
752
+ },
753
+ {
754
+ id: "italic",
755
+ label: "Italic",
756
+ action: () => editor.chain().focus().toggleItalic().run(),
757
+ isActive: editor.isActive("italic"),
758
+ shortcut: "Ctrl+I"
759
+ },
760
+ {
761
+ id: "strike",
762
+ label: "Strike",
763
+ action: () => editor.chain().focus().toggleStrike().run(),
764
+ isActive: editor.isActive("strike"),
765
+ shortcut: "Ctrl+Shift+X"
766
+ },
767
+ {
768
+ id: "code",
769
+ label: "Code",
770
+ action: () => editor.chain().focus().toggleCode().run(),
771
+ isActive: editor.isActive("code"),
772
+ shortcut: "Ctrl+E"
773
+ },
774
+ { id: "sep-1", type: "separator" },
775
+ {
776
+ id: "h1",
777
+ label: "H1",
778
+ action: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
779
+ isActive: editor.isActive("heading", { level: 1 })
780
+ },
781
+ {
782
+ id: "h2",
783
+ label: "H2",
784
+ action: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
785
+ isActive: editor.isActive("heading", { level: 2 })
786
+ },
787
+ {
788
+ id: "h3",
789
+ label: "H3",
790
+ action: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
791
+ isActive: editor.isActive("heading", { level: 3 })
792
+ },
793
+ { id: "sep-2", type: "separator" },
794
+ {
795
+ id: "bullet-list",
796
+ label: "Bullet List",
797
+ action: () => editor.chain().focus().toggleBulletList().run(),
798
+ isActive: editor.isActive("bulletList")
799
+ },
800
+ {
801
+ id: "ordered-list",
802
+ label: "Ordered List",
803
+ action: () => editor.chain().focus().toggleOrderedList().run(),
804
+ isActive: editor.isActive("orderedList")
805
+ },
806
+ {
807
+ id: "code-block",
808
+ label: "Code Block",
809
+ action: () => editor.chain().focus().toggleCodeBlock().run(),
810
+ isActive: editor.isActive("codeBlock")
811
+ },
812
+ {
813
+ id: "blockquote",
814
+ label: "Blockquote",
815
+ action: () => editor.chain().focus().toggleBlockquote().run(),
816
+ isActive: editor.isActive("blockquote")
817
+ }
818
+ ];
819
+ return /* @__PURE__ */ jsx3(
820
+ "div",
821
+ {
822
+ className: cn(
823
+ "flex items-center gap-1 border-border border-b bg-muted/50 p-2",
824
+ className
825
+ ),
826
+ children: buttons.map((button) => {
827
+ if ("type" in button && button.type === "separator") {
828
+ return /* @__PURE__ */ jsx3("div", { className: "mx-1 h-6 w-px bg-border" }, button.id);
829
+ }
830
+ return /* @__PURE__ */ jsx3(
831
+ "button",
832
+ {
833
+ onClick: button.action,
834
+ type: "button",
835
+ title: "shortcut" in button ? `${button.label} (${button.shortcut})` : button.label,
836
+ className: cn(
837
+ "rounded px-2 py-1 font-medium text-xs transition-colors",
838
+ "hover:bg-accent hover:text-accent-foreground",
839
+ button.isActive && "bg-accent text-accent-foreground"
840
+ ),
841
+ children: button.label
842
+ },
843
+ button.id
844
+ );
845
+ })
846
+ }
847
+ );
848
+ }
849
+
850
+ // src/editor/markdown-conversion.ts
851
+ import { marked } from "marked";
852
+ import TurndownService from "turndown";
853
+ var turndown = new TurndownService({
854
+ bulletListMarker: "-",
855
+ codeBlockStyle: "fenced",
856
+ emDelimiter: "*",
857
+ headingStyle: "atx"
858
+ });
859
+ function markdownToHtml(markdown) {
860
+ return String(marked.parse(markdown, { async: false, gfm: true }));
861
+ }
862
+ function htmlToMarkdown(html) {
863
+ return turndown.turndown(html);
864
+ }
865
+ function normalizeMarkdown(markdown) {
866
+ return markdown.replace(/\r\n/g, "\n").trimEnd();
867
+ }
868
+
869
+ // src/editor/markdown-document-editor.tsx
870
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
871
+ function MarkdownDocumentEditor({
872
+ value = "",
873
+ placeholder = "Start writing...",
874
+ readOnly = false,
875
+ autoFocus = false,
876
+ className,
877
+ contentClassName,
878
+ onChange,
879
+ onReady
880
+ }) {
881
+ const initialHtml = useMemo3(() => markdownToHtml(value), []);
882
+ const lastAppliedMarkdownRef = useRef2(normalizeMarkdown(value));
883
+ const editor = useEditor2({
884
+ extensions: [
885
+ StarterKit2.configure({
886
+ codeBlock: {
887
+ HTMLAttributes: {
888
+ class: "hljs"
889
+ }
890
+ }
891
+ })
892
+ ],
893
+ content: initialHtml,
894
+ editable: !readOnly,
895
+ autofocus: autoFocus,
896
+ editorProps: {
897
+ attributes: {
898
+ class: cn(
899
+ "prose prose-sm sm:prose-base max-w-none focus:outline-none",
900
+ "prose-headings:text-[var(--text-primary)] prose-p:text-[var(--text-secondary)] prose-li:text-[var(--text-secondary)]",
901
+ "prose-strong:text-[var(--text-primary)] prose-code:text-[var(--text-primary)] prose-pre:text-[var(--text-secondary)]",
902
+ contentClassName
903
+ ),
904
+ "data-placeholder": placeholder
905
+ }
906
+ },
907
+ onUpdate: ({ editor: currentEditor }) => {
908
+ const nextMarkdown = normalizeMarkdown(htmlToMarkdown(currentEditor.getHTML()));
909
+ lastAppliedMarkdownRef.current = nextMarkdown;
910
+ onChange?.(nextMarkdown, currentEditor);
911
+ },
912
+ onCreate: ({ editor: currentEditor }) => {
913
+ onReady?.(currentEditor);
914
+ }
915
+ });
916
+ useEffect3(() => {
917
+ if (editor) {
918
+ editor.setEditable(!readOnly);
919
+ }
920
+ }, [editor, readOnly]);
921
+ useEffect3(() => {
922
+ if (!editor) {
923
+ return;
924
+ }
925
+ const normalizedValue = normalizeMarkdown(value);
926
+ if (normalizedValue === lastAppliedMarkdownRef.current) {
927
+ return;
928
+ }
929
+ editor.commands.setContent(markdownToHtml(value), { emitUpdate: false });
930
+ lastAppliedMarkdownRef.current = normalizedValue;
931
+ }, [editor, value]);
932
+ return /* @__PURE__ */ jsxs2(
933
+ "div",
934
+ {
935
+ className: cn(
936
+ "flex min-h-[24rem] w-full flex-col overflow-hidden rounded-[var(--radius-lg)] border border-[var(--border-subtle)] bg-[var(--bg-input)]",
937
+ className
938
+ ),
939
+ children: [
940
+ /* @__PURE__ */ jsx4(
941
+ EditorToolbar2,
942
+ {
943
+ editor,
944
+ className: "border-[var(--border-subtle)] bg-[var(--bg-card)]/80 px-2 py-2"
945
+ }
946
+ ),
947
+ /* @__PURE__ */ jsx4("div", { className: "min-h-0 flex-1 overflow-auto px-5 py-4", children: /* @__PURE__ */ jsx4(EditorContent2, { editor }) }),
948
+ /* @__PURE__ */ jsx4("style", { children: `
949
+ .ProseMirror p.is-editor-empty:first-child::before {
950
+ content: attr(data-placeholder);
951
+ float: left;
952
+ color: var(--text-muted, #999);
953
+ pointer-events: none;
954
+ height: 0;
955
+ }
956
+
957
+ .ProseMirror pre {
958
+ background: var(--bg-elevated, #16161a);
959
+ border: 1px solid var(--border-subtle, rgba(255,255,255,0.08));
960
+ border-radius: 0.75rem;
961
+ padding: 0.875rem 1rem;
962
+ overflow-x: auto;
963
+ }
964
+
965
+ .ProseMirror code {
966
+ background: var(--bg-hover, rgba(255,255,255,0.05));
967
+ border-radius: 0.35rem;
968
+ padding: 0.12rem 0.3rem;
969
+ }
970
+
971
+ .ProseMirror pre code {
972
+ background: transparent;
973
+ padding: 0;
974
+ }
975
+ ` })
976
+ ]
977
+ }
978
+ );
979
+ }
980
+
981
+ // src/editor/use-editor.ts
982
+ import { useCallback as useCallback2, useEffect as useEffect4, useState as useState2 } from "react";
983
+ function useEditorConnection() {
984
+ const { connectionState, isSynced, connect, disconnect } = useEditorContext();
985
+ const isConnected = connectionState === "connected" || connectionState === "synced";
986
+ const isConnecting = connectionState === "connecting";
987
+ const isDisconnected = connectionState === "disconnected";
988
+ return {
989
+ /** Current connection state string */
990
+ state: connectionState,
991
+ /** Whether connected to the server (connected or synced) */
992
+ isConnected,
993
+ /** Whether currently attempting to connect */
994
+ isConnecting,
995
+ /** Whether disconnected from the server */
996
+ isDisconnected,
997
+ /** Whether the document is synced with the server */
998
+ isSynced,
999
+ /** Connect to the collaboration server */
1000
+ connect,
1001
+ /** Disconnect from the collaboration server */
1002
+ disconnect
1003
+ };
1004
+ }
1005
+ function useCollaborators() {
1006
+ const { collaborators } = useEditorContext();
1007
+ const count = collaborators.length;
1008
+ const hasOthers = count > 0;
1009
+ return {
1010
+ /** List of active collaborators (excluding self) */
1011
+ collaborators,
1012
+ /** Number of other collaborators */
1013
+ count,
1014
+ /** Whether there are other collaborators present */
1015
+ hasOthers
1016
+ };
1017
+ }
1018
+ function useCollaboratorPresence(userId) {
1019
+ const { collaborators } = useEditorContext();
1020
+ return collaborators.find((c) => c.user.userId === userId) ?? null;
1021
+ }
1022
+ function useYjsState(key, initialValue) {
1023
+ const { doc } = useEditorContext();
1024
+ const [value, setLocalValue] = useState2(initialValue);
1025
+ const metaMap = doc.getMap("metadata");
1026
+ useEffect4(() => {
1027
+ const updateValue = () => {
1028
+ const stored = metaMap.get(key);
1029
+ if (stored !== void 0) {
1030
+ setLocalValue(stored);
1031
+ }
1032
+ };
1033
+ updateValue();
1034
+ metaMap.observe(updateValue);
1035
+ return () => {
1036
+ metaMap.unobserve(updateValue);
1037
+ };
1038
+ }, [metaMap, key]);
1039
+ const setValue = useCallback2(
1040
+ (newValue) => {
1041
+ doc.transact(() => {
1042
+ metaMap.set(key, newValue);
1043
+ });
1044
+ },
1045
+ [doc, metaMap, key]
1046
+ );
1047
+ return [value, setValue];
1048
+ }
1049
+ function useDocumentChanges(onSave) {
1050
+ const { doc } = useEditorContext();
1051
+ const [isDirty, setIsDirty] = useState2(false);
1052
+ const [isSaving, setIsSaving] = useState2(false);
1053
+ const [lastSaved, setLastSaved] = useState2(null);
1054
+ useEffect4(() => {
1055
+ const handleUpdate = () => {
1056
+ setIsDirty(true);
1057
+ };
1058
+ doc.on("update", handleUpdate);
1059
+ return () => {
1060
+ doc.off("update", handleUpdate);
1061
+ };
1062
+ }, [doc]);
1063
+ const save = useCallback2(async () => {
1064
+ if (!onSave || isSaving) return;
1065
+ setIsSaving(true);
1066
+ try {
1067
+ await onSave();
1068
+ setIsDirty(false);
1069
+ setLastSaved(/* @__PURE__ */ new Date());
1070
+ } finally {
1071
+ setIsSaving(false);
1072
+ }
1073
+ }, [onSave, isSaving]);
1074
+ return {
1075
+ /** Whether there are unsaved changes */
1076
+ isDirty,
1077
+ /** Whether save is in progress */
1078
+ isSaving,
1079
+ /** When the document was last saved */
1080
+ lastSaved,
1081
+ /** Save the document */
1082
+ save
1083
+ };
1084
+ }
1085
+ function useAwareness() {
1086
+ const { provider } = useEditorContext();
1087
+ const [localState, setLocalStateValue] = useState2(
1088
+ {}
1089
+ );
1090
+ const awareness = provider?.awareness;
1091
+ useEffect4(() => {
1092
+ if (!awareness) return;
1093
+ const updateState = () => {
1094
+ setLocalStateValue(awareness.getLocalState() ?? {});
1095
+ };
1096
+ updateState();
1097
+ awareness.on("change", updateState);
1098
+ return () => {
1099
+ awareness.off("change", updateState);
1100
+ };
1101
+ }, [awareness]);
1102
+ const setLocalState = useCallback2(
1103
+ (state) => {
1104
+ if (!awareness) return;
1105
+ const current = awareness.getLocalState() ?? {};
1106
+ awareness.setLocalState({ ...current, ...state });
1107
+ },
1108
+ [awareness]
1109
+ );
1110
+ const setLocalStateField = useCallback2(
1111
+ (field, value) => {
1112
+ if (!awareness) return;
1113
+ awareness.setLocalStateField(field, value);
1114
+ },
1115
+ [awareness]
1116
+ );
1117
+ return {
1118
+ /** Current local awareness state */
1119
+ localState,
1120
+ /** Set the entire local state (merges with existing) */
1121
+ setLocalState,
1122
+ /** Set a single field in the local state */
1123
+ setLocalStateField,
1124
+ /** The raw awareness instance */
1125
+ awareness
1126
+ };
1127
+ }
1128
+
1129
+ // src/editor/document-editor-pane.tsx
1130
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
1131
+ function connectionTone(state) {
1132
+ switch (state) {
1133
+ case "synced":
1134
+ return "text-emerald-300 border-emerald-400/30 bg-emerald-400/10";
1135
+ case "connected":
1136
+ case "connecting":
1137
+ return "text-sky-300 border-sky-400/30 bg-sky-400/10";
1138
+ case "disconnected":
1139
+ default:
1140
+ return "text-amber-200 border-amber-300/30 bg-amber-300/10";
1141
+ }
1142
+ }
1143
+ function connectionLabel(state) {
1144
+ switch (state) {
1145
+ case "synced":
1146
+ return "Live synced";
1147
+ case "connected":
1148
+ return "Connected";
1149
+ case "connecting":
1150
+ return "Connecting";
1151
+ case "disconnected":
1152
+ default:
1153
+ return "Offline";
1154
+ }
1155
+ }
1156
+ function connectionDescription(state, collaborators, readOnly) {
1157
+ if (readOnly) {
1158
+ return state === "disconnected" ? "Live access is paused. You can keep reading while the editor reconnects." : "You are viewing the live document in read-only mode.";
1159
+ }
1160
+ switch (state) {
1161
+ case "synced":
1162
+ return collaborators > 0 ? `You and ${collaborators} collaborator${collaborators === 1 ? "" : "s"} are editing the same document.` : "You are editing the live document. Changes sync automatically.";
1163
+ case "connected":
1164
+ case "connecting":
1165
+ return "Connecting the live document. Local edits stay in place while sync catches up.";
1166
+ case "disconnected":
1167
+ default:
1168
+ return "Live updates are paused. You can keep editing and reconnect when the transport is healthy again.";
1169
+ }
1170
+ }
1171
+ function CollaborativeDocumentSurface({
1172
+ markdown,
1173
+ placeholder,
1174
+ autoFocus,
1175
+ readOnly,
1176
+ className,
1177
+ onChange
1178
+ }) {
1179
+ const { state } = useEditorConnection();
1180
+ const { collaborators } = useCollaborators();
1181
+ const initialContent = useMemo4(() => markdownToHtml(markdown), [markdown]);
1182
+ const collaboratorCount = collaborators.length + 1;
1183
+ return /* @__PURE__ */ jsxs3("div", { className: cn("flex h-full min-h-0 flex-col gap-3", className), children: [
1184
+ /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap items-center justify-between gap-3 rounded-[var(--radius-lg)] border border-[var(--border-subtle)] bg-[var(--bg-card)]/80 px-3 py-2", children: [
1185
+ /* @__PURE__ */ jsxs3("div", { className: "min-w-0 space-y-2", children: [
1186
+ /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap items-center gap-2 text-xs text-[var(--text-muted)]", children: [
1187
+ /* @__PURE__ */ jsxs3(
1188
+ "span",
1189
+ {
1190
+ className: cn(
1191
+ "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 font-medium",
1192
+ connectionTone(state)
1193
+ ),
1194
+ children: [
1195
+ state === "disconnected" ? /* @__PURE__ */ jsx5(WifiOff, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx5(Wifi, { className: "h-3.5 w-3.5" }),
1196
+ connectionLabel(state)
1197
+ ]
1198
+ }
1199
+ ),
1200
+ /* @__PURE__ */ jsxs3("span", { className: "inline-flex items-center gap-1.5 rounded-full border border-[var(--border-subtle)] bg-[var(--bg-input)] px-2.5 py-1", children: [
1201
+ /* @__PURE__ */ jsx5(Users, { className: "h-3.5 w-3.5" }),
1202
+ collaborators.length === 0 ? "Solo editing" : `${collaboratorCount} active`
1203
+ ] })
1204
+ ] }),
1205
+ /* @__PURE__ */ jsx5("p", { className: "text-xs text-[var(--text-muted)]", children: connectionDescription(state, collaborators.length, readOnly) })
1206
+ ] }),
1207
+ /* @__PURE__ */ jsx5(CollaboratorsList, { collaborators })
1208
+ ] }),
1209
+ /* @__PURE__ */ jsx5(
1210
+ TiptapEditor,
1211
+ {
1212
+ initialContent,
1213
+ placeholder,
1214
+ autoFocus,
1215
+ readOnly,
1216
+ className: cn("h-full min-h-[28rem]", className),
1217
+ onUpdate: (editor) => {
1218
+ onChange?.(normalizeMarkdown(htmlToMarkdown(editor.getHTML())));
1219
+ }
1220
+ }
1221
+ )
1222
+ ] });
1223
+ }
1224
+ function DocumentEditorPane({
1225
+ eyebrow,
1226
+ title,
1227
+ subtitle,
1228
+ meta,
1229
+ headerActions,
1230
+ footer,
1231
+ className,
1232
+ contentClassName,
1233
+ tabs,
1234
+ toolbar,
1235
+ markdown = "",
1236
+ mode,
1237
+ defaultMode = "preview",
1238
+ onModeChange,
1239
+ backend = "local",
1240
+ placeholder = "Start writing...",
1241
+ autoFocus = false,
1242
+ readOnly = false,
1243
+ onChange,
1244
+ onSave,
1245
+ saving = false,
1246
+ saveLabel = "Save changes",
1247
+ previewClassName,
1248
+ editorClassName,
1249
+ collaboration
1250
+ }) {
1251
+ const [draft, setDraft] = useState3(markdown);
1252
+ const [uncontrolledMode, setUncontrolledMode] = useState3(defaultMode);
1253
+ const activeMode = mode ?? uncontrolledMode;
1254
+ const isCollaborative = backend === "collaborative" && Boolean(collaboration);
1255
+ const isDirty = normalizeMarkdown(draft) !== normalizeMarkdown(markdown);
1256
+ const saveStateLabel = readOnly ? "Read only" : isCollaborative ? isDirty ? "Snapshot pending" : "Live document current" : isDirty ? "Unsaved changes" : "Saved";
1257
+ useEffect5(() => {
1258
+ setDraft(markdown);
1259
+ }, [markdown]);
1260
+ useEffect5(() => {
1261
+ if (mode === void 0) {
1262
+ setUncontrolledMode(defaultMode);
1263
+ }
1264
+ }, [defaultMode, mode]);
1265
+ const setMode = (nextMode) => {
1266
+ if (mode === void 0) {
1267
+ setUncontrolledMode(nextMode);
1268
+ }
1269
+ onModeChange?.(nextMode);
1270
+ };
1271
+ const handleChange = (nextMarkdown) => {
1272
+ setDraft(nextMarkdown);
1273
+ onChange?.(nextMarkdown);
1274
+ };
1275
+ const editorToolbar = /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
1276
+ /* @__PURE__ */ jsxs3(
1277
+ TabsList,
1278
+ {
1279
+ variant: "underline",
1280
+ className: "h-auto gap-4 border-0 bg-transparent p-0 text-[var(--text-muted)]",
1281
+ children: [
1282
+ /* @__PURE__ */ jsx5(
1283
+ TabsTrigger,
1284
+ {
1285
+ value: "preview",
1286
+ variant: "underline",
1287
+ className: "pb-2 data-[state=active]:border-[var(--brand-cool)] data-[state=active]:text-[var(--text-primary)]",
1288
+ children: "Preview"
1289
+ }
1290
+ ),
1291
+ /* @__PURE__ */ jsxs3(
1292
+ TabsTrigger,
1293
+ {
1294
+ value: "edit",
1295
+ variant: "underline",
1296
+ className: "flex items-center gap-2 pb-2 data-[state=active]:border-[var(--brand-cool)] data-[state=active]:text-[var(--text-primary)]",
1297
+ children: [
1298
+ /* @__PURE__ */ jsx5(PencilLine, { className: "h-3.5 w-3.5" }),
1299
+ isCollaborative ? "Live edit" : "Edit"
1300
+ ]
1301
+ }
1302
+ )
1303
+ ]
1304
+ }
1305
+ ),
1306
+ /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap items-center gap-2 text-xs text-[var(--text-muted)]", children: [
1307
+ toolbar,
1308
+ /* @__PURE__ */ jsx5("span", { className: "rounded-full border border-[var(--border-subtle)] bg-[var(--bg-card)] px-2.5 py-1 font-medium", children: isCollaborative ? "Live document" : "Local draft" }),
1309
+ /* @__PURE__ */ jsx5("span", { className: "rounded-full border border-[var(--border-subtle)] bg-[var(--bg-input)] px-2.5 py-1", children: saveStateLabel }),
1310
+ onSave && !readOnly && /* @__PURE__ */ jsxs3(
1311
+ "button",
1312
+ {
1313
+ type: "button",
1314
+ onClick: () => void onSave(draft),
1315
+ disabled: saving || !isDirty,
1316
+ className: "inline-flex items-center gap-2 rounded-[var(--radius-full)] border border-[var(--border-subtle)] bg-[var(--bg-card)] px-3 py-1.5 text-xs font-semibold text-[var(--text-secondary)] transition-colors hover:border-[var(--brand-cool)]/40 hover:text-[var(--text-primary)] disabled:cursor-not-allowed disabled:opacity-50",
1317
+ children: [
1318
+ /* @__PURE__ */ jsx5(Save, { className: "h-3.5 w-3.5" }),
1319
+ saving ? "Saving..." : saveLabel
1320
+ ]
1321
+ }
1322
+ )
1323
+ ] })
1324
+ ] });
1325
+ const preview = /* @__PURE__ */ jsx5(
1326
+ "div",
1327
+ {
1328
+ className: cn(
1329
+ "rounded-[var(--radius-lg)] border border-[var(--border-subtle)] bg-[var(--bg-input)] p-5",
1330
+ previewClassName
1331
+ ),
1332
+ children: /* @__PURE__ */ jsx5(Markdown, { className: "prose-sm max-w-none", children: draft })
1333
+ }
1334
+ );
1335
+ const localEditor = /* @__PURE__ */ jsx5(
1336
+ MarkdownDocumentEditor,
1337
+ {
1338
+ value: draft,
1339
+ placeholder,
1340
+ autoFocus,
1341
+ readOnly,
1342
+ onChange: (nextMarkdown) => {
1343
+ handleChange(nextMarkdown);
1344
+ },
1345
+ className: editorClassName
1346
+ }
1347
+ );
1348
+ const collaborativeEditor = collaboration ? /* @__PURE__ */ jsx5(EditorProvider, { ...collaboration, children: /* @__PURE__ */ jsx5(
1349
+ CollaborativeDocumentSurface,
1350
+ {
1351
+ markdown: draft,
1352
+ placeholder,
1353
+ autoFocus,
1354
+ readOnly,
1355
+ className: editorClassName,
1356
+ onChange: handleChange
1357
+ }
1358
+ ) }, collaboration.documentName) : localEditor;
1359
+ return /* @__PURE__ */ jsx5(
1360
+ Tabs,
1361
+ {
1362
+ value: activeMode,
1363
+ onValueChange: (nextValue) => setMode(nextValue),
1364
+ className: "h-full",
1365
+ children: /* @__PURE__ */ jsxs3(
1366
+ ArtifactPane,
1367
+ {
1368
+ eyebrow,
1369
+ title,
1370
+ subtitle,
1371
+ meta,
1372
+ headerActions,
1373
+ footer,
1374
+ tabs,
1375
+ className,
1376
+ contentClassName,
1377
+ toolbar: editorToolbar,
1378
+ children: [
1379
+ /* @__PURE__ */ jsx5(TabsContent, { value: "preview", className: "mt-0 h-full px-4 py-4", children: preview }),
1380
+ /* @__PURE__ */ jsx5(TabsContent, { value: "edit", className: "mt-0 h-full px-4 py-4", children: isCollaborative ? collaborativeEditor : localEditor })
1381
+ ]
1382
+ }
1383
+ )
1384
+ }
1385
+ );
1386
+ }
1387
+
1388
+ export {
1389
+ EditorProvider,
1390
+ useEditorContext,
1391
+ TiptapEditor,
1392
+ CollaboratorsList,
1393
+ EditorToolbar,
1394
+ useEditorConnection,
1395
+ useCollaborators,
1396
+ useCollaboratorPresence,
1397
+ useYjsState,
1398
+ useDocumentChanges,
1399
+ useAwareness,
1400
+ DocumentEditorPane
1401
+ };