collabmd 0.1.45 → 0.1.46

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 (190) hide show
  1. package/README.md +39 -19
  2. package/bin/collabmd.js +27 -1
  3. package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-CYM5nxUi.js +1 -0
  4. package/dist/client/assets/browser-utils--H_DkyEF.js +1 -0
  5. package/dist/client/assets/chunk-ERSWUTIN-hHdvnhaY.js +41 -0
  6. package/dist/client/assets/comment-thread-store-CdcwrObc.js +1 -0
  7. package/dist/client/assets/comment-threads-9J6Em-_y.js +4 -0
  8. package/dist/client/assets/comment-ui-shared-BC4pt4w8.js +7 -0
  9. package/dist/client/assets/dist-B5HTH2BO.js +1 -0
  10. package/dist/client/assets/{dist-DhLvC8DS.js → dist-B5Jf1SLZ.js} +2 -2
  11. package/dist/client/assets/{dist-CnL7Y2sM.js → dist-BDt3rz7l.js} +1 -1
  12. package/dist/client/assets/dist-BEwK4csK.js +1 -0
  13. package/dist/client/assets/dist-BKW4PALF.js +1 -0
  14. package/dist/client/assets/dist-BWcHHOx-.js +1 -0
  15. package/dist/client/assets/{dist-CYZmbokb.js → dist-BalScOwZ.js} +9 -9
  16. package/dist/client/assets/dist-Bf8f4HmM.js +6 -0
  17. package/dist/client/assets/dist-BjHJDtyU.js +1 -0
  18. package/dist/client/assets/dist-Bm6pf9Bl.js +1 -0
  19. package/dist/client/assets/dist-BoVBdSjk.js +1 -0
  20. package/dist/client/assets/dist-CC5IrQMG.js +1 -0
  21. package/dist/client/assets/{dist-C11hHqv_.js → dist-CCP_AKPG.js} +12 -12
  22. package/dist/client/assets/dist-CZgOmxK6.js +1 -0
  23. package/dist/client/assets/{dist-D2AQ-76g.js → dist-Cqhv5LAk.js} +1 -1
  24. package/dist/client/assets/{dist-MvcV4190.js → dist-D-Z6FLem.js} +1 -1
  25. package/dist/client/assets/dist-DLMiIsNC.js +11 -0
  26. package/dist/client/assets/{dist-3bgCp7iY.js → dist-DZKu9Cs8.js} +1 -1
  27. package/dist/client/assets/dist-DaYdhcVs.js +1 -0
  28. package/dist/client/assets/dist-FWJsgGYI.js +1 -0
  29. package/dist/client/assets/dist-Kh2COra_.js +1 -0
  30. package/dist/client/assets/{dist-JYEDRrPT.js → dist-PDZ3bSqm.js} +1 -1
  31. package/dist/client/assets/{dist-Drp8uu5J.js → dist-TjMh8n1u.js} +1 -1
  32. package/dist/client/assets/{dist-DGb27Dl6.js → dist-dtAdgL39.js} +1 -1
  33. package/dist/client/assets/dist-jv4WdPnK.js +9 -0
  34. package/dist/client/assets/{dist-DK5Gs2wa.js → dist-rGBmqxC7.js} +1 -1
  35. package/dist/client/assets/{dist-DCzuX1AF.js → dist-shrl6K_0.js} +1 -1
  36. package/dist/client/assets/dist-tOaegDYS2.js +1 -0
  37. package/dist/client/assets/dist-ule2n6ak.js +1 -0
  38. package/dist/client/assets/dist-xElaxbia.js +1 -0
  39. package/dist/client/assets/{drawio-editor-ClE8jpoG.js → drawio-editor-FzY1cVw8.js} +1 -1
  40. package/dist/client/assets/drawioEditor-BjiKJqlc.js +2 -0
  41. package/dist/client/assets/editor-session-Cm9ekeNo.js +54 -0
  42. package/dist/client/assets/embedded-editor-base-CwTkk911.css +1 -0
  43. package/dist/client/assets/{preview-render-profile-BZzGSg6K.js → es-DXGV3RHd.js} +13 -13
  44. package/dist/client/assets/excalidraw-editor-B46zsTFI.js +9 -0
  45. package/dist/client/assets/{excalidraw-editor-CyGwOhLz.css → excalidraw-editor-Bt7gMSoh.css} +1 -1
  46. package/dist/client/assets/excalidraw-scene-BOben2gr.js +1 -0
  47. package/dist/client/assets/excalidrawEditor-CT2pHq7e.js +2 -0
  48. package/dist/client/assets/{export-page-CUfws2T-.js → export-page-A7NHawJY.js} +3 -3
  49. package/dist/client/assets/exportDocument-B2L8fe34.css +1 -0
  50. package/dist/client/assets/exportDocument-BaqcOAAW.js +2 -0
  51. package/dist/client/assets/file-icon-svg-CQAjlWjK.js +1 -0
  52. package/dist/client/assets/file-kind-BbqgGzpc.js +1 -0
  53. package/dist/client/assets/image-QXRYHUNP-SWEV8H0J.js +1 -0
  54. package/dist/client/assets/index-ClbjMBVt.js +2 -0
  55. package/dist/client/assets/index-Pum8ET-5.css +1 -0
  56. package/dist/client/assets/main-B3uPyqic.js +893 -0
  57. package/dist/client/assets/pdf-BL9Pr2D5.js +54 -0
  58. package/dist/client/assets/pdf.worker-CLesOks4.mjs +64857 -0
  59. package/dist/client/assets/pdf_viewer-FVkkThnz.js +551 -0
  60. package/dist/client/assets/{preview-render-compiler-BSaQO_p3.js → preview-render-compiler-BD-XhkS2.js} +2 -2
  61. package/dist/client/assets/preview-render-compiler-BKedaMxx.js +1 -0
  62. package/dist/client/assets/preview-render-profile-CE-5rVTC.js +1 -0
  63. package/dist/client/assets/{preview-render-worker-B42LWvrD.js → preview-render-worker-CiPqc9n6.js} +20 -20
  64. package/dist/client/assets/prod-Dy20X7eT.js +1 -0
  65. package/dist/client/assets/prod-hnwh7wRM.js +177 -0
  66. package/dist/client/assets/quick-switcher-controller-fqOYR7qR.js +12 -0
  67. package/dist/client/assets/runtime-config-BLrEKVO1.js +1 -0
  68. package/dist/client/assets/vault-api-client-_LpV1aIn.js +6 -0
  69. package/dist/client/drawio-editor.html +1 -1
  70. package/dist/client/excalidraw-editor.html +1 -1
  71. package/dist/client/export-document.html +2 -2
  72. package/dist/client/index.html +15 -3
  73. package/docker-compose.yml +42 -0
  74. package/package.json +17 -4
  75. package/scripts/local-structurizr-compose.mjs +101 -0
  76. package/src/client/app/index.html +13 -1
  77. package/src/client/application/app-shell/comments-feature.js +19 -10
  78. package/src/client/application/app-shell/git-feature.js +12 -0
  79. package/src/client/application/app-shell/presence-feature.js +2 -1
  80. package/src/client/application/app-shell/ui-feature-shell.js +45 -2
  81. package/src/client/application/app-shell/ui-feature-toolbar.js +2 -52
  82. package/src/client/application/app-shell/workspace-feature.js +5 -1
  83. package/src/client/application/app-shell-elements.js +2 -0
  84. package/src/client/application/pdf-preview-controller.js +992 -0
  85. package/src/client/application/structurizr-preview-controller.js +395 -0
  86. package/src/client/application/workspace-coordinator.js +25 -3
  87. package/src/client/application/workspace-preview-controller.js +91 -2
  88. package/src/client/bootstrap/collabmd-app-shell.js +31 -5
  89. package/src/client/browser-utils.js +44 -0
  90. package/src/client/domain/runtime-paths.js +26 -5
  91. package/src/client/domain/structurizr-language.js +99 -0
  92. package/src/client/domain/vault-paths.js +48 -0
  93. package/src/client/excalidraw-editor.js +833 -51
  94. package/src/client/infrastructure/browser-preferences-port.js +10 -0
  95. package/src/client/infrastructure/comment-thread-store.js +34 -13
  96. package/src/client/infrastructure/editor-session.js +10 -0
  97. package/src/client/infrastructure/editor-view-adapter.js +46 -2
  98. package/src/client/infrastructure/excalidraw-room-client.js +103 -14
  99. package/src/client/infrastructure/git-api-client.js +5 -0
  100. package/src/client/infrastructure/vault-api-client.js +26 -0
  101. package/src/client/presentation/comment-ui/comment-ui-shared.js +4 -0
  102. package/src/client/presentation/drawio-embed-controller.js +9 -102
  103. package/src/client/presentation/drawio-viewer.js +118 -0
  104. package/src/client/presentation/excalidraw-embed-controller.js +74 -8
  105. package/src/client/presentation/file-action-controller.js +102 -0
  106. package/src/client/presentation/file-explorer-view.js +10 -29
  107. package/src/client/presentation/file-icon-svg.js +40 -0
  108. package/src/client/presentation/file-tree-state.js +2 -0
  109. package/src/client/presentation/git-diff-view-controller.js +731 -3
  110. package/src/client/presentation/outline-controller.js +86 -0
  111. package/src/client/presentation/quick-switcher-controller.js +25 -13
  112. package/src/client/presentation/quick-switcher-text-search.js +1 -1
  113. package/src/client/styles/components/navigation.css +11 -1
  114. package/src/client/styles/features/diagram-preview.css +76 -0
  115. package/src/client/styles/features/diff-special.css +309 -0
  116. package/src/client/styles/foundation/themes.css +6 -0
  117. package/src/client/styles/layout/shell.css +358 -1
  118. package/src/client/styles/layout/view-modes.css +1 -0
  119. package/src/client/styles/style.css +1 -0
  120. package/src/client/styles/surfaces/excalidraw-editor.css +294 -0
  121. package/src/domain/comment-threads.js +63 -5
  122. package/src/domain/excalidraw-room-codec.js +6 -4
  123. package/src/domain/file-kind.js +35 -3
  124. package/src/server/config/env.js +79 -0
  125. package/src/server/create-app-server.js +12 -0
  126. package/src/server/domain/bases/base-definition.js +1 -1
  127. package/src/server/domain/bases/base-expression-runtime.js +1 -4
  128. package/src/server/domain/bases/base-index-snapshot-store.js +1 -4
  129. package/src/server/domain/bases/base-transform.js +1 -4
  130. package/src/server/domain/collaboration/collaboration-room.js +55 -21
  131. package/src/server/domain/comment-overview.js +15 -5
  132. package/src/server/domain/ripgrep-search-service.js +4 -0
  133. package/src/server/infrastructure/git/diff-service.js +5 -0
  134. package/src/server/infrastructure/git/git-service.js +21 -0
  135. package/src/server/infrastructure/git/history-service.js +54 -0
  136. package/src/server/infrastructure/git/image-mime.js +14 -0
  137. package/src/server/infrastructure/git/untracked-files.js +28 -1
  138. package/src/server/infrastructure/http/create-git-api-query-handler.js +31 -1
  139. package/src/server/infrastructure/http/create-request-handler.js +16 -23
  140. package/src/server/infrastructure/http/create-static-handler.js +5 -2
  141. package/src/server/infrastructure/http/create-structurizr-api-handler.js +145 -0
  142. package/src/server/infrastructure/http/create-vault-api-command-handler.js +66 -4
  143. package/src/server/infrastructure/http/create-vault-api-query-handler.js +51 -3
  144. package/src/server/infrastructure/http/http-request-helpers.js +22 -0
  145. package/src/server/infrastructure/persistence/path-utils.js +1 -1
  146. package/src/server/infrastructure/persistence/vault-file-store.js +3 -0
  147. package/src/server/infrastructure/structurizr/structurizr-embed.css +43 -0
  148. package/src/server/infrastructure/structurizr/structurizr-workspace-service.js +374 -0
  149. package/src/server/infrastructure/websocket/attach-collaboration-gateway.js +4 -22
  150. package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-CbSISC1w.js +0 -1
  151. package/dist/client/assets/browser-utils-CKtI_rZl.js +0 -1
  152. package/dist/client/assets/chunk-ERSWUTIN-ME9RVNel.js +0 -41
  153. package/dist/client/assets/comment-threads-DDegmaFO.js +0 -4
  154. package/dist/client/assets/dist-BH87JoHx.js +0 -11
  155. package/dist/client/assets/dist-BIkPRZO7.js +0 -6
  156. package/dist/client/assets/dist-BqCsFpx4.js +0 -1
  157. package/dist/client/assets/dist-C2JHCt8U.js +0 -1
  158. package/dist/client/assets/dist-CZjTtmTu.js +0 -1
  159. package/dist/client/assets/dist-Cig4Ov_X.js +0 -1
  160. package/dist/client/assets/dist-CqQEC8gR.js +0 -1
  161. package/dist/client/assets/dist-D4Rzqod1.js +0 -1
  162. package/dist/client/assets/dist-D5sn-mwk.js +0 -9
  163. package/dist/client/assets/dist-D7G-n87v.js +0 -1
  164. package/dist/client/assets/dist-DRqOrNgN.js +0 -1
  165. package/dist/client/assets/dist-DetkMS_4.js +0 -1
  166. package/dist/client/assets/dist-Dp6FVkQ1.js +0 -1
  167. package/dist/client/assets/dist-DtbjNB28.js +0 -1
  168. package/dist/client/assets/dist-DyrYcvpo2.js +0 -1
  169. package/dist/client/assets/dist-GrygD2zE.js +0 -1
  170. package/dist/client/assets/dist-HmEpWUow.js +0 -1
  171. package/dist/client/assets/dist-RTa81Km7.js +0 -1
  172. package/dist/client/assets/drawioEditor-CXaTM9Ds.js +0 -2
  173. package/dist/client/assets/editor-session-GTwZS6Df.js +0 -16
  174. package/dist/client/assets/embedded-editor-base-CiNEc8vy.css +0 -1
  175. package/dist/client/assets/excalidraw-editor-BZVM81BO.js +0 -9
  176. package/dist/client/assets/excalidraw-scene-C8n9CHsK.js +0 -176
  177. package/dist/client/assets/excalidrawEditor-DxJanCze.js +0 -2
  178. package/dist/client/assets/exportDocument-BCuxqXDY.js +0 -2
  179. package/dist/client/assets/exportDocument-DV-OX74E.css +0 -1
  180. package/dist/client/assets/file-kind-BKPOIRPE.js +0 -1
  181. package/dist/client/assets/image-QXRYHUNP-lfTHE3bb.js +0 -1
  182. package/dist/client/assets/index-C3YnUgMD.css +0 -1
  183. package/dist/client/assets/index-D2wXraJ1.js +0 -2
  184. package/dist/client/assets/main-DtiGz7uI.js +0 -795
  185. package/dist/client/assets/preview-render-compiler-csGM5ZFz.js +0 -1
  186. package/dist/client/assets/quick-switcher-controller-shW_6vPY.js +0 -12
  187. package/dist/client/assets/room-CQLoqtMo.js +0 -1
  188. package/dist/client/assets/runtime-config-DSGmGE9V.js +0 -1
  189. package/dist/client/assets/vault-api-client-C6bcGwy-.js +0 -5
  190. package/src/server/domain/collaboration/room-persistence-controller.js +0 -80
@@ -0,0 +1,118 @@
1
+ const DRAWIO_VIEWER_SCRIPT_URL = 'https://viewer.diagrams.net/js/viewer-static.min.js';
2
+
3
+ let drawioViewerLoadPromise = null;
4
+
5
+ export function ensureDrawioViewerLoaded() {
6
+ if (window.GraphViewer?.processElements) {
7
+ return Promise.resolve(window.GraphViewer);
8
+ }
9
+
10
+ if (drawioViewerLoadPromise) {
11
+ return drawioViewerLoadPromise;
12
+ }
13
+
14
+ drawioViewerLoadPromise = new Promise((resolve, reject) => {
15
+ const existingScript = document.querySelector('script[data-collabmd-drawio-viewer]');
16
+ const script = existingScript instanceof HTMLScriptElement
17
+ ? existingScript
18
+ : document.createElement('script');
19
+
20
+ const cleanup = () => {
21
+ script.removeEventListener('error', handleError);
22
+ script.removeEventListener('load', handleLoad);
23
+ };
24
+
25
+ const handleError = () => {
26
+ cleanup();
27
+ drawioViewerLoadPromise = null;
28
+ reject(new Error('Failed to load draw.io viewer'));
29
+ };
30
+
31
+ const handleLoad = () => {
32
+ cleanup();
33
+ if (!window.GraphViewer?.processElements) {
34
+ drawioViewerLoadPromise = null;
35
+ reject(new Error('draw.io viewer did not initialize'));
36
+ return;
37
+ }
38
+
39
+ resolve(window.GraphViewer);
40
+ };
41
+
42
+ script.addEventListener('error', handleError, { once: true });
43
+ script.addEventListener('load', handleLoad, { once: true });
44
+
45
+ if (!existingScript) {
46
+ script.src = DRAWIO_VIEWER_SCRIPT_URL;
47
+ script.async = true;
48
+ script.dataset.collabmdDrawioViewer = 'true';
49
+ document.head.append(script);
50
+ } else if (window.GraphViewer?.processElements) {
51
+ handleLoad();
52
+ }
53
+ });
54
+
55
+ return drawioViewerLoadPromise;
56
+ }
57
+
58
+ export function createDrawioViewerElement({
59
+ ariaLabel = '',
60
+ className = 'drawio-viewer-frame',
61
+ onActivate = null,
62
+ source = '',
63
+ theme = 'dark',
64
+ } = {}) {
65
+ const graphElement = document.createElement('div');
66
+ graphElement.className = className;
67
+ if (ariaLabel) {
68
+ graphElement.setAttribute('aria-label', ariaLabel);
69
+ }
70
+
71
+ if (typeof onActivate === 'function') {
72
+ graphElement.dataset.action = 'open-file';
73
+ graphElement.setAttribute('role', 'button');
74
+ graphElement.setAttribute('tabindex', '0');
75
+ graphElement.addEventListener('click', (event) => {
76
+ event.preventDefault();
77
+ onActivate();
78
+ });
79
+ graphElement.addEventListener('keydown', (event) => {
80
+ if (event.key !== 'Enter' && event.key !== ' ') {
81
+ return;
82
+ }
83
+
84
+ event.preventDefault();
85
+ onActivate();
86
+ });
87
+ }
88
+
89
+ graphElement.dataset.mxgraph = JSON.stringify({
90
+ 'check-visible-state': false,
91
+ center: true,
92
+ border: 0,
93
+ 'dark-mode': theme === 'light' ? 'light' : 'dark',
94
+ editable: false,
95
+ fit: 1,
96
+ lightbox: false,
97
+ nav: false,
98
+ resize: false,
99
+ tooltips: false,
100
+ xml: String(source ?? ''),
101
+ });
102
+
103
+ return graphElement;
104
+ }
105
+
106
+ export async function renderDrawioViewer(host, options = {}) {
107
+ const viewer = await ensureDrawioViewerLoaded();
108
+ const graphElement = createDrawioViewerElement(options);
109
+ host.replaceChildren(graphElement);
110
+
111
+ if (typeof viewer.createViewerForElement === 'function') {
112
+ viewer.createViewerForElement(graphElement);
113
+ } else {
114
+ viewer.processElements();
115
+ }
116
+
117
+ return graphElement;
118
+ }
@@ -1,6 +1,6 @@
1
1
  import { setDiagramActionButtonIcon } from '../domain/diagram-action-icons.js';
2
2
  import { reconcileEmbedEntries } from './excalidraw-embed-reconciler.js';
3
- import { resolveAppUrl } from '../domain/runtime-paths.js';
3
+ import { resolveAppUrl, resolveWsServerOverride } from '../domain/runtime-paths.js';
4
4
  import {
5
5
  cancelIdleRender,
6
6
  isNearViewport,
@@ -15,6 +15,13 @@ const MAX_IFRAME_BOOT_ATTEMPTS = 3;
15
15
  const MIN_HEIGHT = 200;
16
16
  const EDITOR_IFRAME_PATH = '/excalidraw-editor.html';
17
17
 
18
+ function findEmbedPlaceholder(previewElement, key) {
19
+ const placeholders = previewElement?.querySelectorAll?.(
20
+ '.excalidraw-embed-placeholder[data-embed-key]',
21
+ );
22
+ return Array.from(placeholders || []).find((placeholder) => placeholder.dataset?.embedKey === key) || null;
23
+ }
24
+
18
25
  export class ExcalidrawEmbedController {
19
26
  constructor({
20
27
  getTheme,
@@ -70,12 +77,16 @@ export class ExcalidrawEmbedController {
70
77
 
71
78
  this.embedEntries.forEach((entry) => {
72
79
  this._clearEntryBootTimeout(entry);
80
+ entry.resizerCleanup?.();
81
+ entry.resizerCleanup = null;
73
82
  entry.wrapper?.remove();
74
83
  entry.placeholder = null;
75
84
  });
76
85
  this.embedEntries.clear();
77
86
  if (this.warmEntry) {
78
87
  this._clearEntryBootTimeout(this.warmEntry);
88
+ this.warmEntry.resizerCleanup?.();
89
+ this.warmEntry.resizerCleanup = null;
79
90
  this.warmEntry.wrapper?.remove();
80
91
  this.warmEntry = null;
81
92
  }
@@ -219,6 +230,25 @@ export class ExcalidrawEmbedController {
219
230
  });
220
231
  }
221
232
 
233
+ openCommentThread(filePath, threadId) {
234
+ if (!filePath || !threadId) {
235
+ return false;
236
+ }
237
+
238
+ const entry = this._findEntryByFilePath(filePath);
239
+ if (!entry) {
240
+ return false;
241
+ }
242
+
243
+ entry.pendingCommentThreadId = threadId;
244
+ if (entry.isReady) {
245
+ this._postPendingCommentThread(entry);
246
+ } else if (!entry.wrapper) {
247
+ this._enqueueHydration(entry, { prioritize: true });
248
+ }
249
+ return true;
250
+ }
251
+
222
252
  prepareFileDisconnect(filePath, { timeoutMs = 10000 } = {}) {
223
253
  const entry = this._findEntryByFilePath(filePath);
224
254
  if (!entry?.iframe?.contentWindow) {
@@ -394,6 +424,7 @@ export class ExcalidrawEmbedController {
394
424
  entry.maxButton = mount.maxButton;
395
425
  entry.openButton = mount.openButton;
396
426
  entry.instanceId = mount.instanceId;
427
+ entry.resizerCleanup = mount.resizerCleanup;
397
428
  entry.isReady = false;
398
429
  entry.bootFilePath = entry.filePath;
399
430
  entry.bootMode = this._getEntryMode(entry);
@@ -414,7 +445,7 @@ export class ExcalidrawEmbedController {
414
445
  _attachWrapper(entry) {
415
446
  const placeholder = entry.placeholder?.isConnected
416
447
  ? entry.placeholder
417
- : this.previewElement?.querySelector(`.excalidraw-embed-placeholder[data-embed-key="${entry.key}"]`);
448
+ : findEmbedPlaceholder(this.previewElement, entry.key);
418
449
 
419
450
  if (!placeholder) {
420
451
  return;
@@ -476,6 +507,8 @@ export class ExcalidrawEmbedController {
476
507
  }
477
508
 
478
509
  this._clearEntryBootTimeout(entry);
510
+ entry.resizerCleanup?.();
511
+ entry.resizerCleanup = null;
479
512
  if (this.warmEntry === entry) {
480
513
  this.warmEntry = null;
481
514
  }
@@ -634,7 +667,7 @@ export class ExcalidrawEmbedController {
634
667
  }
635
668
 
636
669
  const hostSearchParams = new URLSearchParams(window.location.search);
637
- const serverOverride = hostSearchParams.get('server');
670
+ const serverOverride = resolveWsServerOverride();
638
671
  if (serverOverride) {
639
672
  iframeUrl.searchParams.set('server', serverOverride);
640
673
  }
@@ -702,7 +735,7 @@ export class ExcalidrawEmbedController {
702
735
  const resizer = document.createElement('div');
703
736
  resizer.className = 'excalidraw-embed-resizer';
704
737
  resizer.title = 'Drag to resize';
705
- this._setupResizer(resizer, iframe, () => {
738
+ const resizerCleanup = this._setupResizer(resizer, iframe, () => {
706
739
  this._syncEntryLayout(entry);
707
740
  this._requestPreviewViewportFit(entry);
708
741
  });
@@ -769,6 +802,7 @@ export class ExcalidrawEmbedController {
769
802
  labelElement: label,
770
803
  maxButton: maxBtn,
771
804
  openButton: openBtn,
805
+ resizerCleanup,
772
806
  wrapper,
773
807
  };
774
808
  }
@@ -793,6 +827,7 @@ export class ExcalidrawEmbedController {
793
827
  _setupResizer(resizer, iframe, onResizeEnd = null) {
794
828
  let startY = 0;
795
829
  let startHeight = 0;
830
+ let isDragging = false;
796
831
 
797
832
  const onPointerMove = (event) => {
798
833
  const delta = event.clientY - startY;
@@ -801,20 +836,37 @@ export class ExcalidrawEmbedController {
801
836
  iframe.style.pointerEvents = 'none';
802
837
  };
803
838
 
804
- const onPointerUp = () => {
839
+ const stopDragging = () => {
840
+ if (!isDragging) {
841
+ return;
842
+ }
843
+
844
+ isDragging = false;
805
845
  iframe.style.pointerEvents = '';
806
846
  document.removeEventListener('pointermove', onPointerMove);
807
847
  document.removeEventListener('pointerup', onPointerUp);
848
+ };
849
+
850
+ const onPointerUp = () => {
851
+ stopDragging();
808
852
  onResizeEnd?.();
809
853
  };
810
854
 
811
- resizer.addEventListener('pointerdown', (event) => {
855
+ const onPointerDown = (event) => {
812
856
  event.preventDefault();
857
+ isDragging = true;
813
858
  startY = event.clientY;
814
859
  startHeight = iframe.offsetHeight;
815
860
  document.addEventListener('pointermove', onPointerMove);
816
861
  document.addEventListener('pointerup', onPointerUp);
817
- });
862
+ };
863
+
864
+ resizer.addEventListener('pointerdown', onPointerDown);
865
+
866
+ return () => {
867
+ resizer.removeEventListener?.('pointerdown', onPointerDown);
868
+ stopDragging();
869
+ };
818
870
  }
819
871
 
820
872
  _ensureOverlayRoot() {
@@ -1064,7 +1116,7 @@ export class ExcalidrawEmbedController {
1064
1116
 
1065
1117
  const placeholder = entry.placeholder?.isConnected
1066
1118
  ? entry.placeholder
1067
- : this.previewElement?.querySelector(`.excalidraw-embed-placeholder[data-embed-key="${entry.key}"]`);
1119
+ : findEmbedPlaceholder(this.previewElement, entry.key);
1068
1120
 
1069
1121
  if (!placeholder) {
1070
1122
  return;
@@ -1163,6 +1215,7 @@ export class ExcalidrawEmbedController {
1163
1215
  theme: this.getTheme?.() || 'dark',
1164
1216
  });
1165
1217
  this._syncEntryFollowState(entry);
1218
+ this._postPendingCommentThread?.(entry);
1166
1219
  if (this._entryNeedsHardReload(entry)) {
1167
1220
  void this._hydrateEntry(entry);
1168
1221
  }
@@ -1227,6 +1280,19 @@ export class ExcalidrawEmbedController {
1227
1280
  entry?.iframe?.contentWindow?.postMessage(payload, window.location.origin);
1228
1281
  }
1229
1282
 
1283
+ _postPendingCommentThread(entry) {
1284
+ if (!entry?.isReady || !entry.pendingCommentThreadId) {
1285
+ return;
1286
+ }
1287
+
1288
+ this._postMessageToEntry(entry, {
1289
+ source: 'collabmd-host',
1290
+ threadId: entry.pendingCommentThreadId,
1291
+ type: 'open-comment-thread',
1292
+ });
1293
+ entry.pendingCommentThreadId = null;
1294
+ }
1295
+
1230
1296
  _requestPreviewViewportFit(entry) {
1231
1297
  if (this._isFilePreviewEntry(entry)) {
1232
1298
  return;
@@ -1,6 +1,9 @@
1
1
  import {
2
2
  getVaultFileExtension,
3
+ isVaultFilePath,
4
+ VAULT_FILE_EXTENSIONS,
3
5
  } from '../../domain/file-kind.js';
6
+ import { pickFiles } from '../browser-utils.js';
4
7
  import {
5
8
  createBaseStarter,
6
9
  composeVaultChildPath,
@@ -8,6 +11,7 @@ import {
8
11
  createMarkdownStarter,
9
12
  createMermaidStarter,
10
13
  createPlantUmlStarter,
14
+ createStructurizrStarter,
11
15
  ensureVaultExtension,
12
16
  getVaultPathLeaf,
13
17
  getVaultPathParent,
@@ -28,6 +32,8 @@ function formatCount(value, singularLabel, pluralLabel) {
28
32
  return `${value} ${value === 1 ? singularLabel : pluralLabel}`;
29
33
  }
30
34
 
35
+ const VAULT_FILE_PICKER_ACCEPT = VAULT_FILE_EXTENSIONS.join(',');
36
+
31
37
  function createEmptyExcalidrawScene() {
32
38
  return JSON.stringify({
33
39
  type: 'excalidraw',
@@ -116,6 +122,18 @@ export class FileActionController {
116
122
 
117
123
  getCreateActions({ parentDir = '' } = {}) {
118
124
  return [
125
+ {
126
+ contextLabel: 'Upload files',
127
+ group: 'File',
128
+ hint: 'Supported vault files',
129
+ icon: this.getCreateActionIcon('upload'),
130
+ id: 'upload',
131
+ label: 'Upload files',
132
+ meta: 'Multiple',
133
+ onSelect: () => {
134
+ void this.handleUploadFiles({ parentDir });
135
+ },
136
+ },
119
137
  {
120
138
  contextLabel: 'New markdown file',
121
139
  group: 'Note',
@@ -176,6 +194,16 @@ export class FileActionController {
176
194
  meta: '.puml',
177
195
  onSelect: () => this.handleNewPlantUml({ parentDir }),
178
196
  },
197
+ {
198
+ contextLabel: 'New Structurizr workspace',
199
+ group: 'Diagram',
200
+ hint: 'Structurizr DSL',
201
+ icon: this.getCreateActionIcon('structurizr'),
202
+ id: 'structurizr',
203
+ label: 'Structurizr workspace',
204
+ meta: '.dsl',
205
+ onSelect: () => this.handleNewStructurizr({ parentDir }),
206
+ },
179
207
  {
180
208
  contextLabel: 'New folder',
181
209
  group: 'Structure',
@@ -241,10 +269,13 @@ export class FileActionController {
241
269
 
242
270
  getCreateActionIcon(kind) {
243
271
  switch (kind) {
272
+ case 'upload':
273
+ return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/><path d="M5 20h14"/></svg>';
244
274
  case 'base':
245
275
  return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4h7l5 5v11a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"/><path d="M14 4v5h5"/><path d="M8 13h8"/><path d="M8 17h6"/><path d="M8 9h3"/></svg>';
246
276
  case 'drawio':
247
277
  case 'plantuml':
278
+ case 'structurizr':
248
279
  return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="7" height="6" rx="1"/><rect x="14" y="4" width="7" height="6" rx="1"/><rect x="8.5" y="14" width="7" height="6" rx="1"/><path d="M10 7h4"/><path d="M17.5 10v2.5"/><path d="M6.5 10v2.5"/><path d="M6.5 12.5h11"/></svg>';
249
280
  case 'excalidraw':
250
281
  return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg>';
@@ -476,6 +507,47 @@ export class FileActionController {
476
507
  }
477
508
  }
478
509
 
510
+ async uploadVaultFiles(files = [], { parentDir = '' } = {}) {
511
+ const normalizedParentDir = normalizeVaultPathInput(parentDir);
512
+ let uploaded = false;
513
+
514
+ for (const file of Array.from(files ?? [])) {
515
+ const fileName = getVaultPathLeaf(file?.name);
516
+ const filePath = composeVaultChildPath(normalizedParentDir, fileName);
517
+ if (!fileName || !isVaultFilePath(filePath)) {
518
+ this.showToast(`Unsupported file type: ${fileName || 'unnamed file'}`);
519
+ continue;
520
+ }
521
+
522
+ try {
523
+ await this.runWorkspaceMutation((requestId) => this.vaultClient.uploadFile({
524
+ file,
525
+ path: filePath,
526
+ requestId,
527
+ }));
528
+ uploaded = true;
529
+ this.state.expandDirectoryPath(filePath, { includeLeaf: false });
530
+ } catch (error) {
531
+ this.showToast(`${fileName}: ${error?.message || 'Failed to upload file'}`);
532
+ }
533
+ }
534
+
535
+ if (uploaded) {
536
+ await this.refresh();
537
+ }
538
+
539
+ return uploaded;
540
+ }
541
+
542
+ async handleUploadFiles({ parentDir = '' } = {}) {
543
+ const files = await pickFiles({ accept: VAULT_FILE_PICKER_ACCEPT, multiple: true });
544
+ if (files.length === 0) {
545
+ return false;
546
+ }
547
+
548
+ return this.uploadVaultFiles(files, { parentDir });
549
+ }
550
+
479
551
  async createVaultFile(filePath, content, { openAfterCreate = false, errorMessage = 'Failed to create file' } = {}) {
480
552
  try {
481
553
  await this.runWorkspaceMutation((requestId) => this.vaultClient.createFile(withRequestId({
@@ -953,6 +1025,36 @@ export class FileActionController {
953
1025
  });
954
1026
  }
955
1027
 
1028
+ handleNewStructurizr({ parentDir = '' } = {}) {
1029
+ const context = this.getCreateContext(parentDir);
1030
+
1031
+ this.openActionDialog({
1032
+ title: 'Create Structurizr workspace',
1033
+ copy: context.normalizedParentDir
1034
+ ? 'Create a collaborative C4 workspace inside the selected folder.'
1035
+ : 'Create a collaborative C4 workspace in the vault.',
1036
+ label: `Workspace ${context.inputLabelSuffix}`,
1037
+ hint: `${context.hintPrefix} ".dsl" is added automatically. workspace.dsl is conventional; any workspace .dsl file can render.`,
1038
+ note: context.note,
1039
+ placeholder: 'workspace',
1040
+ submitLabel: 'Create workspace',
1041
+ emptyMessage: 'Workspace path is required',
1042
+ onSubmit: (value) => {
1043
+ const normalizedPath = normalizeVaultPathInput(value);
1044
+ if (!normalizedPath) {
1045
+ this.showToast('Workspace path is required');
1046
+ return false;
1047
+ }
1048
+
1049
+ const starter = createStructurizrStarter(composeVaultChildPath(context.normalizedParentDir, normalizedPath));
1050
+ return this.createVaultFile(starter.path, starter.content, {
1051
+ errorMessage: 'Failed to create Structurizr workspace',
1052
+ openAfterCreate: true,
1053
+ });
1054
+ },
1055
+ });
1056
+ }
1057
+
956
1058
  handleRenameFile(filePath) {
957
1059
  const extension = getVaultFileExtension(filePath) || '.md';
958
1060
 
@@ -5,6 +5,7 @@ import {
5
5
  import { escapeHtml } from '../domain/vault-utils.js';
6
6
  import { getVaultPathLeaf, getVaultPathParent } from '../domain/vault-paths.js';
7
7
  import { buttonClassNames } from './components/ui/button.js';
8
+ import { getVaultFileIconSvg } from './file-icon-svg.js';
8
9
 
9
10
  function findNodeByPath(nodes = [], pathValue = '') {
10
11
  for (const node of nodes) {
@@ -460,8 +461,10 @@ export class FileExplorerView {
460
461
  const isExcalidraw = fileType === 'excalidraw';
461
462
  const isBase = fileType === 'base';
462
463
  const isImage = fileType === 'image';
464
+ const isPdf = fileType === 'pdf';
463
465
  const isMermaid = fileType === 'mermaid';
464
466
  const isPlantUml = fileType === 'plantuml';
467
+ const isStructurizr = fileType === 'structurizr';
465
468
 
466
469
  if (isBase) {
467
470
  button.classList.add('is-base');
@@ -475,12 +478,18 @@ export class FileExplorerView {
475
478
  if (isImage) {
476
479
  button.classList.add('is-image');
477
480
  }
481
+ if (isPdf) {
482
+ button.classList.add('is-pdf');
483
+ }
478
484
  if (isMermaid) {
479
485
  button.classList.add('is-mermaid');
480
486
  }
481
487
  if (isPlantUml) {
482
488
  button.classList.add('is-plantuml');
483
489
  }
490
+ if (isStructurizr) {
491
+ button.classList.add('is-structurizr');
492
+ }
484
493
  if (filePath === activeFilePath) {
485
494
  button.classList.add('active');
486
495
  }
@@ -497,7 +506,7 @@ export class FileExplorerView {
497
506
  }
498
507
  const displayName = this.showFileExtensions ? String(name ?? '') : stripVaultFileExtension(name);
499
508
  button.innerHTML = `
500
- ${this.getFileIconSvg({ isBase, isDrawio, isExcalidraw, isImage, isMermaid, isPlantUml })}
509
+ ${getVaultFileIconSvg(filePath)}
501
510
  <span class="file-tree-name">${escapeHtml(displayName)}</span>
502
511
  ${threadCount > 0 ? `<span class="file-tree-comment-count" aria-label="${threadCount} open comment thread${threadCount === 1 ? '' : 's'}">${threadCount}</span>` : ''}
503
512
  `;
@@ -952,34 +961,6 @@ export class FileExplorerView {
952
961
  return true;
953
962
  }
954
963
 
955
- getFileIconSvg({ isBase, isDrawio, isExcalidraw, isImage, isMermaid, isPlantUml }) {
956
- if (isBase) {
957
- return '<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4h7l5 5v11a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"/><path d="M14 4v5h5"/><path d="M8 13h8"/><path d="M8 17h6"/><path d="M8 9h3"/></svg>';
958
- }
959
-
960
- if (isDrawio) {
961
- return '<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="7" height="6" rx="1"/><rect x="14" y="4" width="7" height="6" rx="1"/><rect x="8.5" y="14" width="7" height="6" rx="1"/><path d="M10 7h4"/><path d="M17.5 10v2.5"/><path d="M6.5 10v2.5"/><path d="M6.5 12.5h11"/></svg>';
962
- }
963
-
964
- if (isExcalidraw) {
965
- return '<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg>';
966
- }
967
-
968
- if (isImage) {
969
- return '<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="9" cy="10" r="1.5"/><path d="m21 16-5-5L7 20"/><path d="m14 14 2 2"/></svg>';
970
- }
971
-
972
- if (isMermaid) {
973
- return '<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 7.5c0-1.38 1.12-2.5 2.5-2.5 1.04 0 1.93.64 2.3 1.56A2.5 2.5 0 0 1 14 8.5v1"/><path d="M19 16.5c0 1.38-1.12 2.5-2.5 2.5-1.04 0-1.93-.64-2.3-1.56A2.5 2.5 0 0 1 10 15.5v-1"/><path d="M8 10.5h8"/><path d="M8 13.5h8"/><path d="M10 8.5v7"/><path d="M14 8.5v7"/></svg>';
974
- }
975
-
976
- if (isPlantUml) {
977
- return '<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="7" height="6" rx="1"/><rect x="14" y="4" width="7" height="6" rx="1"/><rect x="8.5" y="14" width="7" height="6" rx="1"/><path d="M10 7h4"/><path d="M17.5 10v2.5"/><path d="M6.5 10v2.5"/><path d="M6.5 12.5h11"/></svg>';
978
- }
979
-
980
- return '<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
981
- }
982
-
983
964
  showContextMenu(event, items) {
984
965
  this.removeContextMenu();
985
966
 
@@ -0,0 +1,40 @@
1
+ import { getVaultFileKind } from '../../domain/file-kind.js';
2
+
3
+ const SVG_ATTRIBUTES = 'width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
4
+
5
+ function createIcon(className, content) {
6
+ return `<svg class="${className}" ${SVG_ATTRIBUTES}>${content}</svg>`;
7
+ }
8
+
9
+ export function getVaultFileIconSvg(filePath, {
10
+ className = 'file-tree-icon',
11
+ } = {}) {
12
+ const kind = getVaultFileKind(filePath) ?? 'file';
13
+ const iconClass = className;
14
+
15
+ if (kind === 'base') {
16
+ return createIcon(iconClass, '<path d="M7 4h7l5 5v11a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"/><path d="M14 4v5h5"/><path d="M8 13h8"/><path d="M8 17h6"/><path d="M8 9h3"/>');
17
+ }
18
+
19
+ if (kind === 'drawio' || kind === 'plantuml' || kind === 'structurizr') {
20
+ return createIcon(iconClass, '<rect x="3" y="4" width="7" height="6" rx="1"/><rect x="14" y="4" width="7" height="6" rx="1"/><rect x="8.5" y="14" width="7" height="6" rx="1"/><path d="M10 7h4"/><path d="M17.5 10v2.5"/><path d="M6.5 10v2.5"/><path d="M6.5 12.5h11"/>');
21
+ }
22
+
23
+ if (kind === 'excalidraw') {
24
+ return createIcon(iconClass, '<path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/>');
25
+ }
26
+
27
+ if (kind === 'image') {
28
+ return createIcon(iconClass, '<rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="9" cy="10" r="1.5"/><path d="m21 16-5-5L7 20"/><path d="m14 14 2 2"/>');
29
+ }
30
+
31
+ if (kind === 'pdf') {
32
+ return createIcon(iconClass, '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><path d="M8 15h8"/><path d="M8 18h5"/>');
33
+ }
34
+
35
+ if (kind === 'mermaid') {
36
+ return createIcon(iconClass, '<path d="M5 7.5c0-1.38 1.12-2.5 2.5-2.5 1.04 0 1.93.64 2.3 1.56A2.5 2.5 0 0 1 14 8.5v1"/><path d="M19 16.5c0 1.38-1.12 2.5-2.5 2.5-1.04 0-1.93-.64-2.3-1.56A2.5 2.5 0 0 1 10 15.5v-1"/><path d="M8 10.5h8"/><path d="M8 13.5h8"/><path d="M10 8.5v7"/><path d="M14 8.5v7"/>');
37
+ }
38
+
39
+ return createIcon(iconClass, '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>');
40
+ }
@@ -20,6 +20,8 @@ function flattenTree(nodes, files = [], searchEntries = [], fileEntries = []) {
20
20
  || node.type === 'drawio'
21
21
  || node.type === 'mermaid'
22
22
  || node.type === 'plantuml'
23
+ || node.type === 'structurizr'
24
+ || node.type === 'pdf'
23
25
  || node.type === 'image'
24
26
  ) {
25
27
  files.push(node.path);