collabmd 0.1.31 → 0.1.32

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 (93) hide show
  1. package/dist/client/assets/{drawio-editor-B4sHHwWJ.js → drawio-editor-Br4qyh3r.js} +1 -1
  2. package/dist/client/assets/drawioEditor-ClmnTx5J.js +2 -0
  3. package/dist/client/assets/{editor-session-BKHEYkbY.js → editor-session-Cf-iV5-3.js} +2 -2
  4. package/dist/client/assets/embedded-editor-base-CgEkjNYn.css +1 -0
  5. package/dist/client/assets/{excalidraw-editor-CHbeNZu6.js → excalidraw-editor-CXAxVhlw.js} +2 -2
  6. package/dist/client/assets/{excalidrawEditor-BhaTX2Ko.js → excalidrawEditor-DoH_kMdu.js} +2 -2
  7. package/dist/client/assets/exportDocument-9wTrBZNS.css +1 -0
  8. package/dist/client/assets/{index-kPgEYvEX.js → index-C8QZYHvV.js} +2 -2
  9. package/dist/client/assets/index-CxSbUTs2.css +1 -0
  10. package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  11. package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  12. package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  13. package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  14. package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  15. package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  16. package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  17. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  18. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  19. package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  20. package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  21. package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  22. package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  23. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  24. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  25. package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  26. package/dist/client/assets/main-BjHHMlv0.js +1249 -0
  27. package/dist/client/assets/vault-api-client-Bf9tB_GW.js +1 -0
  28. package/dist/client/assets/vault-api-client-D7Ck0CvR.css +1 -0
  29. package/dist/client/drawio-editor.html +1 -1
  30. package/dist/client/excalidraw-editor.html +1 -1
  31. package/dist/client/export-document.html +2 -2
  32. package/dist/client/index.html +2 -2
  33. package/package.json +4 -1
  34. package/src/client/app/main-entry.js +2 -0
  35. package/src/client/application/app-shell/git-feature.js +28 -36
  36. package/src/client/application/app-shell/presence-feature.js +3 -1
  37. package/src/client/application/diagram-preview-hydrator.js +24 -15
  38. package/src/client/application/mermaid-preview-hydrator.js +121 -4
  39. package/src/client/application/plantuml-preview-hydrator.js +6 -16
  40. package/src/client/application/preview-renderer.js +11 -2
  41. package/src/client/bootstrap/collabmd-app-shell.js +176 -50
  42. package/src/client/domain/excalidraw-document-switch.js +3 -0
  43. package/src/client/excalidraw-editor.js +187 -84
  44. package/src/client/infrastructure/auth-client.js +4 -4
  45. package/src/client/infrastructure/backlinks-api-client.js +18 -0
  46. package/src/client/infrastructure/comment-thread-store.js +4 -0
  47. package/src/client/infrastructure/editor-session.js +2 -0
  48. package/src/client/infrastructure/git-api-client.js +134 -0
  49. package/src/client/infrastructure/plantuml-api-client.js +21 -0
  50. package/src/client/infrastructure/vault-api-client.js +67 -0
  51. package/src/client/presentation/backlinks-panel.js +6 -9
  52. package/src/client/presentation/bases-preview-controller.js +26 -7
  53. package/src/client/presentation/excalidraw-embed-controller.js +251 -15
  54. package/src/client/presentation/excalidraw-embed-reconciler.js +0 -1
  55. package/src/client/presentation/file-action-controller.js +97 -0
  56. package/src/client/presentation/file-explorer-controller.js +2 -0
  57. package/src/client/presentation/file-explorer-view.js +368 -0
  58. package/src/client/presentation/file-history-view-controller.js +41 -40
  59. package/src/client/presentation/git-diff-view-controller.js +70 -53
  60. package/src/client/presentation/git-panel-controller.js +12 -30
  61. package/src/client/styles/export-document.css +45 -2
  62. package/src/client/styles/features/auth-gate.css +13 -24
  63. package/src/client/styles/features/comment-card.css +6 -0
  64. package/src/client/styles/features/comments-drawer.css +38 -18
  65. package/src/client/styles/features/diagram-preview.css +33 -0
  66. package/src/client/styles/features/embedded-preview.css +37 -11
  67. package/src/client/styles/features/preview-markdown.css +103 -28
  68. package/src/client/styles/foundation/themes.css +4 -0
  69. package/src/client/styles/foundation/tokens.css +4 -3
  70. package/src/client/styles/layout/sidebar.css +72 -0
  71. package/src/server/auth/auth-common.js +224 -0
  72. package/src/server/auth/auth-constants.js +30 -0
  73. package/src/server/auth/auth-strategies.js +320 -0
  74. package/src/server/auth/create-auth-service.js +24 -545
  75. package/src/server/domain/bases/base-definition.js +255 -0
  76. package/src/server/domain/bases/base-expression-runtime.js +1159 -0
  77. package/src/server/domain/bases/base-index-snapshot-store.js +418 -0
  78. package/src/server/domain/bases/base-query-results.js +205 -0
  79. package/src/server/domain/bases/base-query-service.js +68 -1965
  80. package/src/server/domain/collaboration/collaboration-room.js +36 -1
  81. package/src/server/domain/collaboration/room-client-state-store.js +1 -0
  82. package/src/server/domain/collaboration/room-persistence-controller.js +1 -1
  83. package/src/server/infrastructure/http/create-vault-api-query-handler.js +109 -1
  84. package/src/server/infrastructure/http/http-response.js +56 -0
  85. package/src/server/infrastructure/persistence/vault-file-store.js +110 -0
  86. package/dist/client/assets/drawioEditor-DGpo2hcx.js +0 -2
  87. package/dist/client/assets/embedded-editor-base-DgmRcsR8.css +0 -1
  88. package/dist/client/assets/exportDocument-DmV7Gngb.css +0 -1
  89. package/dist/client/assets/index-D69HOZHr.css +0 -1
  90. package/dist/client/assets/main-DSxNTtD-.js +0 -1243
  91. package/dist/client/assets/vault-api-client-BzHQseMN.js +0 -1
  92. package/dist/client/assets/vault-api-client-De7kMvrE.css +0 -1
  93. /package/dist/client/assets/{exportDocument-CtZKWE2c.js → exportDocument-Bia2hI25.js} +0 -0
@@ -21,17 +21,22 @@ import {
21
21
  parseSceneJson,
22
22
  sceneToInitialData,
23
23
  } from './domain/excalidraw-scene.js';
24
+ import {
25
+ normalizeDocumentMode,
26
+ } from './domain/excalidraw-document-switch.js';
24
27
  import { ensureClientAuthenticated } from './infrastructure/auth-client.js';
25
28
  import { ExcalidrawRoomClient } from './infrastructure/excalidraw-room-client.js';
26
29
  import { vaultApiClient } from './infrastructure/vault-api-client.js';
27
30
 
28
31
  const params = new URLSearchParams(window.location.search);
29
- const filePath = params.get('file');
30
32
  const isTestMode = params.get('test') === '1';
31
- const isPreviewMode = params.get('mode') === 'preview';
32
33
  const parentOrigin = window.location.origin;
33
34
  const syncTimeoutMs = Number.parseInt(params.get('syncTimeoutMs') || '', 10);
34
35
 
36
+ let currentDocument = {
37
+ filePath: params.get('file') || '',
38
+ mode: normalizeDocumentMode(params.get('mode')),
39
+ };
35
40
  let excalidrawAPI = null;
36
41
  let currentTheme = params.get('theme') || 'dark';
37
42
  let localAwarenessUser = resolveLocalAwarenessUser({
@@ -55,22 +60,104 @@ let apiStateCleanupCallbacks = [];
55
60
  let collaboratorRenderFrame = 0;
56
61
  let queuedCollaborators = null;
57
62
  let initialViewportFitPending = true;
58
- const roomClient = new ExcalidrawRoomClient({
59
- filePath,
60
- onCollaboratorsChange: (collaborators) => {
61
- if (!collabReady) {
62
- pendingCollaborators = collaborators;
63
- return;
64
- }
63
+ let roomClient = null;
64
+ let roomClientGeneration = 0;
65
+ let reactRoot = null;
65
66
 
66
- queueCollaboratorsRender(collaborators);
67
- },
68
- onRemoteSceneJson: (sceneJson) => {
69
- applySceneFromJson(sceneJson);
70
- },
71
- syncTimeoutMs: Number.isFinite(syncTimeoutMs) ? syncTimeoutMs : undefined,
72
- vaultClient: vaultApiClient,
73
- });
67
+ function getDocumentViewState(mode = currentDocument.mode) {
68
+ const normalizedMode = normalizeDocumentMode(mode);
69
+ return {
70
+ viewModeEnabled: normalizedMode === 'preview',
71
+ zenModeEnabled: normalizedMode === 'preview',
72
+ };
73
+ }
74
+
75
+ function createRoomClient(filePath) {
76
+ const generation = ++roomClientGeneration;
77
+ const client = new ExcalidrawRoomClient({
78
+ filePath,
79
+ onCollaboratorsChange: (collaborators) => {
80
+ if (generation !== roomClientGeneration) {
81
+ return;
82
+ }
83
+
84
+ if (!collabReady) {
85
+ pendingCollaborators = collaborators;
86
+ return;
87
+ }
88
+
89
+ queueCollaboratorsRender(collaborators);
90
+ },
91
+ onRemoteSceneJson: (sceneJson) => {
92
+ if (generation !== roomClientGeneration) {
93
+ return;
94
+ }
95
+
96
+ applySceneFromJson(sceneJson);
97
+ },
98
+ syncTimeoutMs: Number.isFinite(syncTimeoutMs) ? syncTimeoutMs : undefined,
99
+ vaultClient: vaultApiClient,
100
+ });
101
+
102
+ return {
103
+ client,
104
+ generation,
105
+ };
106
+ }
107
+
108
+ function buildExcalidrawProps({ initialData } = {}) {
109
+ const props = {
110
+ onMount: handleEditorMount,
111
+ onInitialize: (api) => {
112
+ initializeEditor(api);
113
+ },
114
+ onUnmount: () => {
115
+ clearEditorApiStateBindings();
116
+ disconnectRealtimeRoom({ preserveEditorBindings: true });
117
+ excalidrawAPI = null;
118
+ collabReady = false;
119
+ },
120
+ aiEnabled: false,
121
+ isCollaborating: true,
122
+ onChange: (elements, appState, files) => {
123
+ scheduleSyncToRoom(elements, appState, files);
124
+ roomClient?.syncLocalSelectionAwareness(appState);
125
+ },
126
+ onPointerUpdate: (payload) => {
127
+ roomClient?.scheduleLocalPointerAwareness(payload);
128
+ },
129
+ theme: currentTheme,
130
+ ...getDocumentViewState(),
131
+ UIOptions: {
132
+ canvasActions: {
133
+ export: false,
134
+ loadScene: false,
135
+ saveToActiveFile: false,
136
+ toggleTheme: false,
137
+ },
138
+ },
139
+ };
140
+
141
+ if (initialData) {
142
+ props.initialData = initialData;
143
+ }
144
+
145
+ return props;
146
+ }
147
+
148
+ function renderExcalidrawApp({ initialData } = {}) {
149
+ if (!reactRoot) {
150
+ return;
151
+ }
152
+
153
+ reactRoot.render(
154
+ React.createElement(
155
+ 'div',
156
+ { style: { height: '100vh', width: '100%' } },
157
+ React.createElement(Excalidraw, buildExcalidrawProps({ initialData })),
158
+ ),
159
+ );
160
+ }
74
161
 
75
162
  function applySurfaceTheme(theme = currentTheme) {
76
163
  document.body.dataset.theme = theme === 'light' ? 'light' : 'dark';
@@ -108,7 +195,7 @@ function applyLocalUserPatch(nextUser = {}) {
108
195
  currentUser: localAwarenessUser,
109
196
  nextUser,
110
197
  });
111
- roomClient.setLocalUser(localAwarenessUser);
198
+ roomClient?.setLocalUser(localAwarenessUser);
112
199
  }
113
200
 
114
201
  if (isTestMode) {
@@ -149,13 +236,13 @@ if (isTestMode) {
149
236
  } : null;
150
237
  },
151
238
  isViewMode: () => Boolean(excalidrawAPI?.getAppState?.().viewModeEnabled),
152
- getSceneJson: () => roomClient.getLastSceneJson(),
239
+ getSceneJson: () => roomClient?.getLastSceneJson?.() || '',
153
240
  isAuthoritativeReady: () => (
154
241
  Boolean(excalidrawAPI)
155
242
  && collabReady
156
- && roomClient.canWriteToRoom === true
157
- && roomClient.waitingForAuthoritativeSync === false
158
- && roomClient.isApplyingSharedSnapshot() === false
243
+ && roomClient?.canWriteToRoom === true
244
+ && roomClient?.waitingForAuthoritativeSync === false
245
+ && roomClient?.isApplyingSharedSnapshot?.() === false
159
246
  ),
160
247
  isReady: () => collabReady && Boolean(excalidrawAPI) && Boolean(getNativeHistoryButton('undo')) && Boolean(getNativeHistoryButton('redo')),
161
248
  redoShared: () => triggerNativeHistory('redo'),
@@ -243,7 +330,7 @@ function releaseOnChangeSuppressionAfterPaint({ trackedSharedSnapshot = false }
243
330
  requestAnimationFrame(() => {
244
331
  pendingSuppressionReleases = Math.max(0, pendingSuppressionReleases - 1);
245
332
  if (trackedSharedSnapshot) {
246
- roomClient.endApplyingSharedSnapshot();
333
+ roomClient?.endApplyingSharedSnapshot();
247
334
  }
248
335
  if (pendingSuppressionReleases === 0) {
249
336
  suppressOnChange = false;
@@ -264,20 +351,29 @@ function releaseViewportBroadcastSuppressionAfterPaint() {
264
351
  });
265
352
  }
266
353
 
267
- function buildApiSceneUpdate(scene) {
354
+ function buildApiSceneUpdate(scene, {
355
+ appStateOverrides = {},
356
+ replaceElements = false,
357
+ } = {}) {
268
358
  const currentAppState = excalidrawAPI.getAppState();
269
- const currentElements = excalidrawAPI.getSceneElementsIncludingDeleted?.() || excalidrawAPI.getSceneElements();
359
+ const currentElements = replaceElements
360
+ ? []
361
+ : (excalidrawAPI.getSceneElementsIncludingDeleted?.() || excalidrawAPI.getSceneElements());
270
362
  const restoredElements = restoreElements(scene?.elements || [], currentElements, {
271
363
  repairBindings: true,
272
364
  });
273
365
  const restoredAppState = restoreAppState(scene?.appState || {}, currentAppState);
274
- const reconciledElements = reconcileElements(currentElements, restoredElements, currentAppState);
366
+ const reconciledElements = replaceElements
367
+ ? restoredElements
368
+ : reconcileElements(currentElements, restoredElements, currentAppState);
275
369
 
276
370
  return {
277
371
  appState: {
278
372
  theme: currentTheme,
279
373
  viewBackgroundColor: restoredAppState.viewBackgroundColor ?? '#ffffff',
280
374
  gridSize: restoredAppState.gridSize ?? null,
375
+ ...getDocumentViewState(),
376
+ ...appStateOverrides,
281
377
  },
282
378
  elements: reconciledElements,
283
379
  files: scene?.files || {},
@@ -285,14 +381,19 @@ function buildApiSceneUpdate(scene) {
285
381
  }
286
382
 
287
383
  function updateApiScene(scene, {
384
+ appStateOverrides = {},
288
385
  captureUpdate = CaptureUpdateAction.NEVER,
386
+ replaceElements = false,
289
387
  trackedSharedSnapshot = true,
290
388
  } = {}) {
291
- const nextSceneUpdate = buildApiSceneUpdate(scene);
389
+ const nextSceneUpdate = buildApiSceneUpdate(scene, {
390
+ appStateOverrides,
391
+ replaceElements,
392
+ });
292
393
 
293
394
  suppressOnChange = true;
294
395
  if (trackedSharedSnapshot) {
295
- roomClient.beginApplyingSharedSnapshot();
396
+ roomClient?.beginApplyingSharedSnapshot();
296
397
  }
297
398
  try {
298
399
  excalidrawAPI.updateScene({
@@ -325,14 +426,14 @@ function applyLocalScene(scene, {
325
426
  });
326
427
 
327
428
  if (suppressOnChange) {
328
- roomClient.commitSceneJson(normalizedJson, {
429
+ roomClient?.commitSceneJson(normalizedJson, {
329
430
  origin: 'excalidraw-local-scene-apply',
330
431
  });
331
432
  }
332
433
  }
333
434
 
334
435
  function onRoomTextUpdate() {
335
- applySceneFromJson(roomClient.getLastSceneJson());
436
+ applySceneFromJson(roomClient?.getLastSceneJson?.() || '');
336
437
  }
337
438
 
338
439
  function postToParent(type, payload = {}) {
@@ -367,14 +468,14 @@ function scheduleInitialViewportFit() {
367
468
  animate: false,
368
469
  fitToViewport: true,
369
470
  maxZoom: 2,
370
- viewportZoomFactor: isPreviewMode ? 0.92 : 0.88,
471
+ viewportZoomFactor: normalizeDocumentMode(currentDocument.mode) === 'preview' ? 0.92 : 0.88,
371
472
  });
372
473
  });
373
474
  });
374
475
  }
375
476
 
376
477
  function syncLocalViewportToRoom() {
377
- if (!collabReady || !excalidrawAPI || suppressViewportBroadcast) {
478
+ if (!collabReady || !excalidrawAPI || !roomClient || suppressViewportBroadcast) {
378
479
  return;
379
480
  }
380
481
 
@@ -462,8 +563,16 @@ function applyHostFollowRequest(peerId) {
462
563
  pendingHostFollowPeerId = null;
463
564
  }
464
565
 
465
- function disconnectRealtimeRoom() {
566
+ function clearEditorApiStateBindings() {
567
+ apiStateCleanupCallbacks.forEach((cleanup) => cleanup());
568
+ apiStateCleanupCallbacks = [];
569
+ }
570
+
571
+ function resetRealtimeRoomState() {
466
572
  collabReady = false;
573
+ suppressOnChange = false;
574
+ pendingRemoteSceneJson = '';
575
+ pendingSuppressionReleases = 0;
467
576
  pendingCollaborators = null;
468
577
  activeCollaborators = new Map();
469
578
  followedSocketId = null;
@@ -476,9 +585,19 @@ function disconnectRealtimeRoom() {
476
585
  }
477
586
  collaboratorRenderFrame = 0;
478
587
  queuedCollaborators = null;
479
- apiStateCleanupCallbacks.forEach((cleanup) => cleanup());
480
- apiStateCleanupCallbacks = [];
481
- roomClient.disconnect();
588
+ }
589
+
590
+ function disconnectRealtimeRoom({ preserveEditorBindings = false } = {}) {
591
+ const activeRoomClient = roomClient;
592
+ roomClient = null;
593
+ roomClientGeneration += 1;
594
+
595
+ resetRealtimeRoomState();
596
+ if (!preserveEditorBindings) {
597
+ clearEditorApiStateBindings();
598
+ }
599
+
600
+ activeRoomClient?.disconnect();
482
601
  }
483
602
 
484
603
  let didDisconnectRealtimeRoom = false;
@@ -499,7 +618,7 @@ async function waitForPendingRoomWrites({
499
618
  const startedAt = performance.now();
500
619
 
501
620
  while ((performance.now() - startedAt) < maxWaitMs) {
502
- const ws = roomClient.provider?.ws;
621
+ const ws = roomClient?.provider?.ws;
503
622
  if (!ws || ws.readyState !== WebSocket.OPEN || ws.bufferedAmount === 0) {
504
623
  return;
505
624
  }
@@ -509,10 +628,23 @@ async function waitForPendingRoomWrites({
509
628
  }
510
629
 
511
630
  async function prepareRealtimeRoomDisconnect() {
512
- roomClient.flushSceneSync();
631
+ roomClient?.flushSceneSync();
513
632
  await waitForPendingRoomWrites();
514
633
  }
515
634
 
635
+ async function connectDocumentClient(filePath) {
636
+ const { client, generation } = createRoomClient(filePath);
637
+ roomClient = client;
638
+ const scene = await client.connect({ initialUser: localAwarenessUser });
639
+
640
+ if (generation !== roomClientGeneration || roomClient !== client) {
641
+ client.disconnect();
642
+ return null;
643
+ }
644
+
645
+ return scene;
646
+ }
647
+
516
648
  window.addEventListener('pagehide', disconnectRealtimeRoomOnce);
517
649
 
518
650
  window.addEventListener('message', (event) => {
@@ -536,6 +668,7 @@ window.addEventListener('message', (event) => {
536
668
  });
537
669
  releaseOnChangeSuppressionAfterPaint();
538
670
  }
671
+ renderExcalidrawApp();
539
672
  return;
540
673
  }
541
674
 
@@ -556,11 +689,19 @@ window.addEventListener('message', (event) => {
556
689
  requestId: message.requestId || '',
557
690
  });
558
691
  })();
692
+ return;
693
+ }
694
+
695
+ if (message.type === 'park-room') {
696
+ void (async () => {
697
+ await prepareRealtimeRoomDisconnect();
698
+ disconnectRealtimeRoom({ preserveEditorBindings: true });
699
+ })();
559
700
  }
560
701
  });
561
702
 
562
703
  function scheduleSyncToRoom(elements, appState, files) {
563
- if (!collabReady || suppressOnChange) {
704
+ if (!collabReady || !roomClient || suppressOnChange) {
564
705
  return;
565
706
  }
566
707
 
@@ -589,7 +730,7 @@ function initializeEditor(api) {
589
730
  setFollowedSocket(null, { force: true });
590
731
  }));
591
732
 
592
- const sceneJson = pendingRemoteSceneJson || roomClient.getLastSceneJson();
733
+ const sceneJson = pendingRemoteSceneJson || roomClient?.getLastSceneJson?.() || '';
593
734
  pendingRemoteSceneJson = '';
594
735
  updateApiScene(parseSceneJson(sceneJson));
595
736
 
@@ -622,53 +763,15 @@ async function init() {
622
763
  try {
623
764
  applySurfaceTheme(currentTheme);
624
765
  await ensureClientAuthenticated();
625
- const initialScene = await roomClient.connect({ initialUser: localAwarenessUser });
766
+ const initialScene = await connectDocumentClient(currentDocument.filePath);
767
+ if (!initialScene) {
768
+ throw new Error('Failed to connect initial Excalidraw document');
769
+ }
626
770
  const initialData = sceneToInitialData(initialScene, { theme: currentTheme });
627
771
 
628
772
  loadingElement?.remove();
629
-
630
- const excalidrawProps = {
631
- onMount: handleEditorMount,
632
- onInitialize: (api) => {
633
- initializeEditor(api);
634
- },
635
- onUnmount: () => {
636
- apiStateCleanupCallbacks.forEach((cleanup) => cleanup());
637
- apiStateCleanupCallbacks = [];
638
- excalidrawAPI = null;
639
- collabReady = false;
640
- },
641
- initialData,
642
- aiEnabled: false,
643
- isCollaborating: true,
644
- onChange: (elements, appState, files) => {
645
- scheduleSyncToRoom(elements, appState, files);
646
- roomClient.syncLocalSelectionAwareness(appState);
647
- },
648
- onPointerUpdate: (payload) => {
649
- roomClient.scheduleLocalPointerAwareness(payload);
650
- },
651
- theme: currentTheme,
652
- viewModeEnabled: isPreviewMode,
653
- zenModeEnabled: isPreviewMode,
654
- UIOptions: {
655
- canvasActions: {
656
- export: false,
657
- loadScene: false,
658
- saveToActiveFile: false,
659
- toggleTheme: false,
660
- },
661
- },
662
- };
663
-
664
- const App = () => React.createElement(
665
- 'div',
666
- { style: { height: '100vh', width: '100%' } },
667
- React.createElement(Excalidraw, excalidrawProps),
668
- );
669
-
670
- const root = createRoot(document.getElementById('root'));
671
- root.render(React.createElement(App));
773
+ reactRoot = createRoot(document.getElementById('root'));
774
+ renderExcalidrawApp({ initialData });
672
775
  } catch (error) {
673
776
  console.error('[excalidraw] Failed to initialize:', error);
674
777
  postToParent('error', {
@@ -156,7 +156,7 @@ function renderOidcPrompt(card, config, {
156
156
  actions.className = 'auth-gate-actions';
157
157
 
158
158
  const signInButton = document.createElement('button');
159
- signInButton.className = 'auth-gate-button auth-gate-button--google';
159
+ signInButton.className = 'ui-button ui-button--secondary auth-gate-button auth-gate-button--google';
160
160
  signInButton.type = 'button';
161
161
  signInButton.append(createGoogleIcon(), document.createTextNode(config.submitLabel || 'Continue with Google'));
162
162
 
@@ -191,7 +191,7 @@ function renderStatusCard(card, {
191
191
 
192
192
  if (secondaryActionLabel && typeof onSecondaryAction === 'function') {
193
193
  const button = document.createElement('button');
194
- button.className = 'auth-gate-secondary-button';
194
+ button.className = 'ui-button ui-button--secondary auth-gate-secondary-button';
195
195
  button.type = 'button';
196
196
  button.textContent = secondaryActionLabel;
197
197
  button.addEventListener('click', () => {
@@ -220,7 +220,7 @@ function renderPasswordPrompt(card, config, {
220
220
  label.textContent = config.passwordLabel;
221
221
 
222
222
  const input = document.createElement('input');
223
- input.className = 'auth-gate-input';
223
+ input.className = 'ui-input auth-gate-input';
224
224
  input.type = 'password';
225
225
  input.name = 'password';
226
226
  input.autocomplete = 'current-password';
@@ -233,7 +233,7 @@ function renderPasswordPrompt(card, config, {
233
233
  actions.className = 'auth-gate-actions';
234
234
 
235
235
  const submitButton = document.createElement('button');
236
- submitButton.className = 'auth-gate-button';
236
+ submitButton.className = 'ui-button ui-button--primary auth-gate-button';
237
237
  submitButton.type = 'submit';
238
238
  submitButton.textContent = config.submitLabel;
239
239
 
@@ -0,0 +1,18 @@
1
+ import { resolveApiUrl } from '../domain/runtime-paths.js';
2
+
3
+ export class BacklinksApiClient {
4
+ async readBacklinks(filePath, { signal } = {}) {
5
+ const response = await fetch(
6
+ resolveApiUrl(`/backlinks?file=${encodeURIComponent(filePath)}`),
7
+ { signal },
8
+ );
9
+ const data = await response.json().catch(() => ({}));
10
+ if (!response.ok) {
11
+ throw new Error(data.error || 'Failed to load backlinks');
12
+ }
13
+
14
+ return Array.isArray(data.backlinks) ? data.backlinks : [];
15
+ }
16
+ }
17
+
18
+ export const backlinksApiClient = new BacklinksApiClient();
@@ -134,6 +134,10 @@ export class CommentThreadStore {
134
134
  this.onCommentsChange?.(this.getCommentThreads());
135
135
  }
136
136
 
137
+ refreshComments() {
138
+ this.emitCommentsChange();
139
+ }
140
+
137
141
  getCommentThreads() {
138
142
  if (!this.commentThreads) {
139
143
  return [];
@@ -93,6 +93,7 @@ export class EditorSession {
93
93
  undoManager: this.pendingCollaborativeBindings.undoManager,
94
94
  ytext: this.pendingCollaborativeBindings.ytext,
95
95
  });
96
+ this.commentThreadStore.refreshComments();
96
97
  this.pendingCollaborativeBindings = null;
97
98
  this.bootstrapContent = null;
98
99
  return true;
@@ -109,6 +110,7 @@ export class EditorSession {
109
110
  content: this.bootstrapContent,
110
111
  filePath: this.activeFilePath,
111
112
  });
113
+ this.commentThreadStore.refreshComments();
112
114
  return this.emitContentChange();
113
115
  }
114
116
 
@@ -0,0 +1,134 @@
1
+ import { resolveApiUrl } from '../domain/runtime-paths.js';
2
+
3
+ function createRequestHeaders(requestId, headers = {}) {
4
+ const nextHeaders = { ...headers };
5
+ if (requestId) {
6
+ nextHeaders['X-CollabMD-Request-Id'] = String(requestId);
7
+ }
8
+
9
+ return nextHeaders;
10
+ }
11
+
12
+ async function parseJsonResponse(response, fallbackError) {
13
+ const data = await response.json().catch(() => ({}));
14
+ if (!response.ok || data.ok === false) {
15
+ const error = new Error(data.error || fallbackError);
16
+ error.status = response.status;
17
+ error.body = data;
18
+ if (typeof data?.code === 'string') {
19
+ error.code = data.code;
20
+ }
21
+ throw error;
22
+ }
23
+
24
+ return data;
25
+ }
26
+
27
+ function createSearchParams(values = {}) {
28
+ const params = new URLSearchParams();
29
+ Object.entries(values).forEach(([key, value]) => {
30
+ if (value == null || value === '') {
31
+ return;
32
+ }
33
+
34
+ params.set(key, String(value));
35
+ });
36
+ return params;
37
+ }
38
+
39
+ export class GitApiClient {
40
+ async readStatus({ force = false } = {}) {
41
+ const response = await fetch(resolveApiUrl(`/git/status${force ? '?force=true' : ''}`));
42
+ return parseJsonResponse(response, 'Failed to load git status');
43
+ }
44
+
45
+ async readPullBackups() {
46
+ const response = await fetch(resolveApiUrl('/git/pull-backups'));
47
+ return parseJsonResponse(response, 'Failed to load pull backups');
48
+ }
49
+
50
+ async readHistory({ limit, offset } = {}) {
51
+ const params = createSearchParams({ limit, offset });
52
+ const response = await fetch(resolveApiUrl(`/git/history?${params.toString()}`));
53
+ return parseJsonResponse(response, 'Failed to load git history');
54
+ }
55
+
56
+ async readFileHistory({ path, limit, offset } = {}) {
57
+ const params = createSearchParams({ limit, offset, path });
58
+ const response = await fetch(resolveApiUrl(`/git/file-history?${params.toString()}`));
59
+ return parseJsonResponse(response, 'Failed to load file history');
60
+ }
61
+
62
+ async readDiff({
63
+ allowLargePatch = false,
64
+ metaOnly = false,
65
+ path = null,
66
+ scope = 'all',
67
+ } = {}) {
68
+ const params = createSearchParams({
69
+ allowLargePatch: allowLargePatch ? 'true' : null,
70
+ metaOnly: metaOnly ? 'true' : null,
71
+ path,
72
+ scope,
73
+ });
74
+ const response = await fetch(resolveApiUrl(`/git/diff?${params.toString()}`));
75
+ return parseJsonResponse(response, 'Failed to load git diff');
76
+ }
77
+
78
+ async readCommit({
79
+ allowLargePatch = false,
80
+ hash,
81
+ metaOnly = false,
82
+ path = null,
83
+ } = {}) {
84
+ const params = createSearchParams({
85
+ allowLargePatch: allowLargePatch ? 'true' : null,
86
+ hash,
87
+ metaOnly: metaOnly ? 'true' : null,
88
+ path,
89
+ });
90
+ const response = await fetch(resolveApiUrl(`/git/commit?${params.toString()}`));
91
+ return parseJsonResponse(response, 'Failed to load git commit');
92
+ }
93
+
94
+ async readFileSnapshot({ hash, path } = {}) {
95
+ const params = createSearchParams({ hash, path });
96
+ const response = await fetch(resolveApiUrl(`/git/file-snapshot?${params.toString()}`));
97
+ return parseJsonResponse(response, 'Failed to load historical file preview');
98
+ }
99
+
100
+ async stageFile({ path, requestId = null } = {}) {
101
+ return this.#postJson('/git/stage', { path }, { requestId, fallbackError: 'Failed to stage file' });
102
+ }
103
+
104
+ async unstageFile({ path, requestId = null } = {}) {
105
+ return this.#postJson('/git/unstage', { path }, { requestId, fallbackError: 'Failed to unstage file' });
106
+ }
107
+
108
+ async pushBranch({ requestId = null } = {}) {
109
+ return this.#postJson('/git/push', {}, { requestId, fallbackError: 'Failed to push branch' });
110
+ }
111
+
112
+ async pullBranch({ requestId = null } = {}) {
113
+ return this.#postJson('/git/pull', {}, { requestId, fallbackError: 'Failed to pull branch' });
114
+ }
115
+
116
+ async resetFile({ path, requestId = null } = {}) {
117
+ return this.#postJson('/git/reset-file', { path }, { requestId, fallbackError: 'Failed to reset file' });
118
+ }
119
+
120
+ async commit({ message, requestId = null } = {}) {
121
+ return this.#postJson('/git/commit', { message }, { requestId, fallbackError: 'Failed to commit staged changes' });
122
+ }
123
+
124
+ async #postJson(path, payload, { fallbackError, requestId = null } = {}) {
125
+ const response = await fetch(resolveApiUrl(path), {
126
+ body: JSON.stringify(payload),
127
+ headers: createRequestHeaders(requestId, { 'Content-Type': 'application/json' }),
128
+ method: 'POST',
129
+ });
130
+ return parseJsonResponse(response, fallbackError);
131
+ }
132
+ }
133
+
134
+ export const gitApiClient = new GitApiClient();
@@ -0,0 +1,21 @@
1
+ import { resolveApiUrl } from '../domain/runtime-paths.js';
2
+
3
+ export class PlantUmlApiClient {
4
+ async renderSvg(source) {
5
+ const response = await fetch(resolveApiUrl('/plantuml/render'), {
6
+ body: JSON.stringify({ source }),
7
+ headers: {
8
+ 'Content-Type': 'application/json',
9
+ },
10
+ method: 'POST',
11
+ });
12
+ const data = await response.json().catch(() => null);
13
+ if (!response.ok || !data?.ok || typeof data.svg !== 'string') {
14
+ throw new Error(data?.error || 'Failed to render PlantUML');
15
+ }
16
+
17
+ return data.svg;
18
+ }
19
+ }
20
+
21
+ export const plantUmlApiClient = new PlantUmlApiClient();