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,395 @@
1
+ import { resolveAppPath } from '../domain/runtime-paths.js';
2
+ import { setDiagramActionButtonIcon } from '../domain/diagram-action-icons.js';
3
+
4
+ const SYNC_DEBOUNCE_MS = 300;
5
+ const FRAME_FIT_ATTEMPTS = 20;
6
+ const FRAME_FIT_INTERVAL_MS = 50;
7
+
8
+ function decodeViewKey(hash = '') {
9
+ const normalized = String(hash ?? '').replace(/^#/, '');
10
+ if (!normalized) {
11
+ return '';
12
+ }
13
+
14
+ try {
15
+ return decodeURIComponent(normalized);
16
+ } catch {
17
+ return normalized;
18
+ }
19
+ }
20
+
21
+ function readFrameState(iframe) {
22
+ try {
23
+ const frameDocument = iframe?.contentDocument;
24
+ const layer = frameDocument?.querySelector('.joint-layers');
25
+ const viewport = frameDocument?.querySelector('#diagram-viewport');
26
+ const zoom = layer ? new DOMMatrixReadOnly(getComputedStyle(layer).transform).a : 0;
27
+ return {
28
+ scrollLeft: viewport?.scrollLeft ?? 0,
29
+ scrollTop: viewport?.scrollTop ?? 0,
30
+ zoom: Number.isFinite(zoom) && zoom > 0 ? zoom : null,
31
+ };
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ export class StructurizrPreviewController {
38
+ constructor({
39
+ enabled = false,
40
+ syncWorkspace,
41
+ }) {
42
+ this.enabled = Boolean(enabled);
43
+ this.syncWorkspace = syncWorkspace;
44
+ this.iframe = null;
45
+ this.shell = null;
46
+ this.status = null;
47
+ this.maximizeButton = null;
48
+ this.filePath = null;
49
+ this.viewKey = '';
50
+ this.lastVersion = null;
51
+ this.syncTimer = null;
52
+ this.syncToken = 0;
53
+ this.frameFitTimer = null;
54
+ this.frameHost = null;
55
+ this.pendingIframe = null;
56
+ this.pendingVersion = null;
57
+ this.handleFrameLoad = (event) => {
58
+ const iframe = event?.currentTarget || this.iframe;
59
+ if (!iframe) {
60
+ return;
61
+ }
62
+
63
+ try {
64
+ if (iframe === this.iframe) {
65
+ this.viewKey = decodeViewKey(iframe.contentWindow?.location?.hash || '') || this.viewKey;
66
+ iframe.contentWindow?.addEventListener('hashchange', this.handleFrameHashChange);
67
+ }
68
+ this.prepareFrame(iframe);
69
+ } catch {
70
+ if (iframe === this.pendingIframe) {
71
+ this.commitPendingIframe(iframe);
72
+ }
73
+ }
74
+ };
75
+ this.handleFrameHashChange = () => {
76
+ try {
77
+ this.viewKey = decodeViewKey(this.iframe?.contentWindow?.location?.hash || '') || this.viewKey;
78
+ } catch {
79
+ // Ignore inaccessible frame locations.
80
+ }
81
+ };
82
+ }
83
+
84
+ setStatus(message = '', { error = false } = {}) {
85
+ if (!this.status) {
86
+ return;
87
+ }
88
+
89
+ this.status.textContent = message;
90
+ this.status.hidden = !message;
91
+ this.status.classList.toggle('is-error', error);
92
+ }
93
+
94
+ prepareFrame(iframe = this.iframe) {
95
+ const frameDocument = iframe?.contentDocument;
96
+ if (!frameDocument?.head) {
97
+ if (iframe === this.pendingIframe) {
98
+ this.commitPendingIframe(iframe);
99
+ }
100
+ return;
101
+ }
102
+
103
+ iframe.contentWindow?.dispatchEvent(new Event('resize'));
104
+ this.scheduleFrameFit(iframe, iframe === this.pendingIframe ? () => this.commitPendingIframe(iframe) : null);
105
+ }
106
+
107
+ fitFrameToCanvas(iframe = this.iframe) {
108
+ const frameDocument = iframe?.contentDocument;
109
+ const canvas = frameDocument?.querySelector('#diagram-canvas');
110
+ if (!canvas) {
111
+ return false;
112
+ }
113
+ if (iframe === this.pendingIframe && !frameDocument.querySelector('.joint-layers')) {
114
+ return false;
115
+ }
116
+
117
+ try {
118
+ const diagram = iframe?.contentWindow?.structurizr?.diagram;
119
+ diagram?.setEmbedded?.(true);
120
+ diagram?.resize?.();
121
+ diagram?.zoomToWidthOrHeight?.();
122
+ } catch {
123
+ // The frame can be cross-origin in custom deployments; the CSS crop still applies.
124
+ }
125
+
126
+ const { width, height } = canvas.getBoundingClientRect();
127
+ if (!width || !height) {
128
+ return false;
129
+ }
130
+
131
+ return true;
132
+ }
133
+
134
+ disposeFrame(iframe) {
135
+ if (!iframe) {
136
+ return;
137
+ }
138
+
139
+ try {
140
+ iframe.contentWindow?.removeEventListener?.('hashchange', this.handleFrameHashChange);
141
+ } catch {
142
+ // Ignore inaccessible frame windows during cleanup.
143
+ }
144
+ iframe.removeEventListener('load', this.handleFrameLoad);
145
+ iframe.remove();
146
+ }
147
+
148
+ discardPendingIframe() {
149
+ if (!this.pendingIframe) {
150
+ return;
151
+ }
152
+
153
+ clearTimeout(this.frameFitTimer);
154
+ this.frameFitTimer = null;
155
+ this.disposeFrame(this.pendingIframe);
156
+ this.pendingIframe = null;
157
+ this.pendingVersion = null;
158
+ }
159
+
160
+ commitPendingIframe(iframe) {
161
+ if (iframe !== this.pendingIframe || !this.shell?.isConnected) {
162
+ return;
163
+ }
164
+
165
+ const previousIframe = this.iframe;
166
+ const nextVersion = this.pendingVersion;
167
+ const frameState = readFrameState(previousIframe);
168
+ this.pendingIframe = null;
169
+ this.pendingVersion = null;
170
+ if (frameState?.zoom) {
171
+ try {
172
+ iframe.contentWindow?.structurizr?.diagram?.zoomTo?.(frameState.zoom);
173
+ } catch {
174
+ // Keep the new diagram usable when a custom viewer does not expose zoom controls.
175
+ }
176
+ }
177
+ try {
178
+ const viewport = iframe.contentDocument?.querySelector('#diagram-viewport');
179
+ if (viewport && frameState) {
180
+ viewport.scrollLeft = frameState.scrollLeft;
181
+ viewport.scrollTop = frameState.scrollTop;
182
+ }
183
+ } catch {
184
+ // Cross-origin custom viewers cannot restore viewport coordinates.
185
+ }
186
+ this.disposeFrame(previousIframe);
187
+ iframe.style.visibility = '';
188
+ iframe.style.pointerEvents = '';
189
+ iframe.removeAttribute('aria-hidden');
190
+ this.iframe = iframe;
191
+ this.lastVersion = nextVersion;
192
+
193
+ try {
194
+ this.viewKey = decodeViewKey(iframe.contentWindow?.location?.hash || '') || this.viewKey;
195
+ iframe.contentWindow?.addEventListener('hashchange', this.handleFrameHashChange);
196
+ } catch {
197
+ // Cross-origin deployments cannot expose the frame hash.
198
+ }
199
+ this.setStatus('');
200
+ }
201
+
202
+ createIframe() {
203
+ const iframe = document.createElement('iframe');
204
+ iframe.className = 'structurizr-preview-iframe';
205
+ iframe.title = 'Structurizr architecture diagram';
206
+ iframe.loading = 'eager';
207
+ iframe.referrerPolicy = 'same-origin';
208
+ iframe.addEventListener('load', this.handleFrameLoad);
209
+ return iframe;
210
+ }
211
+
212
+ syncMaximizeButtonState() {
213
+ if (!this.maximizeButton || !this.shell) {
214
+ return;
215
+ }
216
+
217
+ const isMaximized = this.shell.classList.contains('is-maximized');
218
+ setDiagramActionButtonIcon(this.maximizeButton, isMaximized ? 'restore' : 'maximize');
219
+ const label = isMaximized ? 'Restore diagram size' : 'Maximize diagram';
220
+ this.maximizeButton.title = label;
221
+ this.maximizeButton.setAttribute('aria-label', label);
222
+ }
223
+
224
+ setMaximized(shouldMaximize) {
225
+ const isMaximized = Boolean(shouldMaximize);
226
+ this.shell?.classList.toggle('is-maximized', isMaximized);
227
+ document.body?.classList.toggle('structurizr-maximized-open', isMaximized);
228
+ this.syncMaximizeButtonState();
229
+ this.iframe?.contentWindow?.dispatchEvent(new Event('resize'));
230
+ }
231
+
232
+ scheduleFrameFit(iframe = this.iframe, onReady = null) {
233
+ if (!iframe) {
234
+ return;
235
+ }
236
+
237
+ clearTimeout(this.frameFitTimer);
238
+ let attempts = 0;
239
+ const fit = () => {
240
+ this.frameFitTimer = null;
241
+ if (this.fitFrameToCanvas(iframe) || attempts >= FRAME_FIT_ATTEMPTS) {
242
+ onReady?.();
243
+ return;
244
+ }
245
+
246
+ attempts += 1;
247
+ this.frameFitTimer = setTimeout(fit, FRAME_FIT_INTERVAL_MS);
248
+ };
249
+ fit();
250
+ }
251
+
252
+ mount(renderHost, filePath) {
253
+ const fileChanged = Boolean(this.filePath && this.filePath !== filePath);
254
+ if (fileChanged) {
255
+ this.setMaximized(false);
256
+ this.discardPendingIframe();
257
+ this.disposeFrame(this.iframe);
258
+ this.iframe = null;
259
+ this.lastVersion = null;
260
+ this.viewKey = '';
261
+ }
262
+
263
+ this.filePath = filePath;
264
+
265
+ if (this.shell?.isConnected && this.shell.parentElement === renderHost) {
266
+ return;
267
+ }
268
+
269
+ clearTimeout(this.frameFitTimer);
270
+ this.frameFitTimer = null;
271
+ this.shell = document.createElement('section');
272
+ this.shell.className = 'structurizr-preview-shell diagram-preview-shell';
273
+ this.shell.dataset.structurizrPreview = 'true';
274
+
275
+ const header = document.createElement('div');
276
+ header.className = 'structurizr-preview-header diagram-preview-toolbar';
277
+
278
+ const label = document.createElement('span');
279
+ label.className = 'structurizr-preview-label';
280
+ label.textContent = String(filePath).split('/').pop()?.replace(/\.dsl$/i, '') || 'Structurizr';
281
+
282
+ this.maximizeButton = document.createElement('button');
283
+ this.maximizeButton.type = 'button';
284
+ this.maximizeButton.className = 'structurizr-preview-btn ui-preview-action ui-preview-action--icon-only';
285
+ this.maximizeButton.dataset.action = 'toggle-maximize';
286
+ this.maximizeButton.addEventListener('click', (event) => {
287
+ event.preventDefault();
288
+ this.setMaximized(!this.shell?.classList.contains('is-maximized'));
289
+ });
290
+ header.append(label, this.maximizeButton);
291
+ this.syncMaximizeButtonState();
292
+
293
+ this.status = document.createElement('div');
294
+ this.status.className = 'structurizr-preview-status';
295
+ this.status.setAttribute('aria-live', 'polite');
296
+ this.status.setAttribute('role', 'status');
297
+ this.status.hidden = true;
298
+
299
+ this.frameHost = document.createElement('div');
300
+ this.frameHost.className = 'structurizr-preview-stage';
301
+ this.shell.append(header, this.status, this.frameHost);
302
+
303
+ renderHost?.replaceChildren(this.shell);
304
+ this.iframe = null;
305
+ this.pendingIframe = null;
306
+ this.pendingVersion = null;
307
+ this.lastVersion = null;
308
+ }
309
+
310
+ async render({ filePath, renderHost, source = '' }) {
311
+ this.mount(renderHost, filePath);
312
+ return this.syncNow({ filePath, source: String(source ?? '') });
313
+ }
314
+
315
+ queueSync({ filePath, source = '' } = {}) {
316
+ clearTimeout(this.syncTimer);
317
+ this.syncTimer = setTimeout(() => {
318
+ this.syncTimer = null;
319
+ void this.syncNow({ filePath, source: String(source ?? '') });
320
+ }, SYNC_DEBOUNCE_MS);
321
+ }
322
+
323
+ async syncNow({ filePath, source = '' } = {}) {
324
+ if (filePath !== this.filePath) {
325
+ return false;
326
+ }
327
+
328
+ const token = ++this.syncToken;
329
+ if (!this.enabled) {
330
+ this.setStatus('Structurizr preview is not configured. Start CollabMD with --local-structurizr.', { error: true });
331
+ return false;
332
+ }
333
+
334
+ if (!this.iframe) {
335
+ this.setStatus('Validating Structurizr workspace…');
336
+ }
337
+
338
+ try {
339
+ const payload = await this.syncWorkspace({ path: filePath, source });
340
+ if (token !== this.syncToken || filePath !== this.filePath) {
341
+ return false;
342
+ }
343
+
344
+ const nextVersion = payload?.version || null;
345
+ const route = resolveAppPath('/workspace/1/diagrams');
346
+ const hash = this.viewKey ? `#${encodeURIComponent(this.viewKey)}` : '';
347
+ if (!this.iframe) {
348
+ const iframe = this.createIframe();
349
+ this.iframe = iframe;
350
+ this.frameHost?.appendChild(iframe);
351
+ iframe.src = `${route}${hash}`;
352
+ this.lastVersion = nextVersion;
353
+ } else if (nextVersion !== this.lastVersion) {
354
+ this.discardPendingIframe();
355
+ const iframe = this.createIframe();
356
+ iframe.style.visibility = 'hidden';
357
+ iframe.style.pointerEvents = 'none';
358
+ iframe.setAttribute('aria-hidden', 'true');
359
+ this.pendingIframe = iframe;
360
+ this.pendingVersion = nextVersion;
361
+ this.frameHost?.appendChild(iframe);
362
+ iframe.src = `${route}${hash}`;
363
+ }
364
+
365
+ this.setStatus('');
366
+ return true;
367
+ } catch (error) {
368
+ if (token !== this.syncToken || filePath !== this.filePath) {
369
+ return false;
370
+ }
371
+
372
+ this.setStatus(error?.message || 'Failed to load Structurizr workspace.', { error: true });
373
+ return false;
374
+ }
375
+ }
376
+
377
+ reset() {
378
+ this.setMaximized(false);
379
+ clearTimeout(this.syncTimer);
380
+ this.syncTimer = null;
381
+ clearTimeout(this.frameFitTimer);
382
+ this.frameFitTimer = null;
383
+ this.syncToken += 1;
384
+ this.discardPendingIframe();
385
+ this.disposeFrame(this.iframe);
386
+ this.iframe = null;
387
+ this.frameHost = null;
388
+ this.shell = null;
389
+ this.status = null;
390
+ this.maximizeButton = null;
391
+ this.filePath = null;
392
+ this.lastVersion = null;
393
+ this.viewKey = '';
394
+ }
395
+ }
@@ -21,6 +21,7 @@ export class WorkspaceCoordinator {
21
21
  getDisplayName,
22
22
  getFileList,
23
23
  getLineWrappingEnabled,
24
+ getVimModeEnabled,
24
25
  getLocalUser,
25
26
  getStoredUserName,
26
27
  getTheme,
@@ -28,8 +29,10 @@ export class WorkspaceCoordinator {
28
29
  isDrawioFile,
29
30
  isExcalidrawFile,
30
31
  isImageFile,
32
+ isPdfFile,
31
33
  isMermaidFile,
32
34
  isPlantUmlFile,
35
+ isStructurizrWorkspaceFile,
33
36
  isTabActive,
34
37
  loadBootstrapContent = null,
35
38
  loadEditorSessionClass,
@@ -49,6 +52,8 @@ export class WorkspaceCoordinator {
49
52
  onRenderExcalidrawPreview,
50
53
  onRenderDrawioPreview,
51
54
  onRenderImagePreview,
55
+ onRenderPdfPreview,
56
+ onRenderStructurizrPreview,
52
57
  onSyncWrapToggle,
53
58
  onUpdateActiveFile,
54
59
  onUpdateCurrentFile,
@@ -68,6 +73,7 @@ export class WorkspaceCoordinator {
68
73
  this.getDisplayName = getDisplayName;
69
74
  this.getFileList = getFileList;
70
75
  this.getLineWrappingEnabled = getLineWrappingEnabled;
76
+ this.getVimModeEnabled = getVimModeEnabled ?? (() => false);
71
77
  this.getLocalUser = getLocalUser;
72
78
  this.getStoredUserName = getStoredUserName;
73
79
  this.getTheme = getTheme;
@@ -75,8 +81,10 @@ export class WorkspaceCoordinator {
75
81
  this.isDrawioFile = isDrawioFile ?? (() => false);
76
82
  this.isExcalidrawFile = isExcalidrawFile ?? (() => false);
77
83
  this.isImageFile = isImageFile ?? (() => false);
84
+ this.isPdfFile = isPdfFile ?? (() => false);
78
85
  this.isMermaidFile = isMermaidFile ?? (() => false);
79
86
  this.isPlantUmlFile = isPlantUmlFile ?? (() => false);
87
+ this.isStructurizrWorkspaceFile = isStructurizrWorkspaceFile ?? (() => false);
80
88
  this.isTabActive = isTabActive;
81
89
  this.loadBootstrapContent = loadBootstrapContent;
82
90
  this.loadEditorSessionClassPort = loadEditorSessionClass;
@@ -96,6 +104,8 @@ export class WorkspaceCoordinator {
96
104
  this.onRenderDrawioPreview = onRenderDrawioPreview;
97
105
  this.onRenderExcalidrawPreview = onRenderExcalidrawPreview;
98
106
  this.onRenderImagePreview = onRenderImagePreview;
107
+ this.onRenderPdfPreview = onRenderPdfPreview;
108
+ this.onRenderStructurizrPreview = onRenderStructurizrPreview;
99
109
  this.onSyncWrapToggle = onSyncWrapToggle;
100
110
  this.onUpdateActiveFile = onUpdateActiveFile;
101
111
  this.onUpdateCurrentFile = onUpdateCurrentFile;
@@ -171,12 +181,19 @@ export class WorkspaceCoordinator {
171
181
  filePath,
172
182
  isExcalidraw = false,
173
183
  isImage = false,
184
+ isPdf = false,
174
185
  supportsBacklinks,
175
186
  }) {
176
187
  if (isExcalidraw) this.onRenderExcalidrawPreview(filePath);
177
188
  if (isBase || isBaseFilePath(filePath)) this.onRenderBasePreview(filePath);
178
189
  if (isDrawio) this.onRenderDrawioPreview(filePath);
179
190
  if (isImage) this.onRenderImagePreview(filePath);
191
+ if (isPdf) this.onRenderPdfPreview(filePath);
192
+ if (this.isStructurizrWorkspaceFile(filePath)) {
193
+ this.onRenderStructurizrPreview(filePath, {
194
+ source: this.session?.getText?.() ?? '',
195
+ });
196
+ }
180
197
  this.onSyncWrapToggle();
181
198
  if (supportsBacklinks) this.loadBacklinks(filePath);
182
199
  }
@@ -192,13 +209,15 @@ export class WorkspaceCoordinator {
192
209
  const isExcalidraw = this.isExcalidrawFile(filePath);
193
210
  const isBase = this.isBaseFile(filePath);
194
211
  const isImage = this.isImageFile(filePath);
212
+ const isPdf = this.isPdfFile(filePath);
195
213
  const isMermaid = this.isMermaidFile(filePath);
196
214
  const isPlantUml = this.isPlantUmlFile(filePath);
215
+ const isStructurizrWorkspace = this.isStructurizrWorkspaceFile(filePath);
197
216
 
198
217
  if (
199
218
  filePath === this.stateStore.currentFilePath
200
219
  && normalizedDrawioMode === currentDrawioMode
201
- && (this.session || isDrawio || isExcalidraw || isImage)
220
+ && (this.session || isDrawio || isExcalidraw || isImage || isPdf)
202
221
  ) {
203
222
  this.onUpdateActiveFile(filePath);
204
223
  this.onUpdateLobbyCurrentFile(filePath);
@@ -211,11 +230,11 @@ export class WorkspaceCoordinator {
211
230
  this.cleanupSession();
212
231
  const chromeState = this.prepareForFileOpen(filePath, {
213
232
  drawioMode: normalizedDrawioMode,
214
- resetConnectionState: !isDrawio && !isExcalidraw && !isImage,
233
+ resetConnectionState: !isDrawio && !isExcalidraw && !isImage && !isPdf,
215
234
  });
216
235
  this.reportFileOpenMetric('open_started', loadToken, { filePath });
217
236
 
218
- if (isDrawio || isExcalidraw || isImage) {
237
+ if (isDrawio || isExcalidraw || isImage || isPdf) {
219
238
  this.onSessionAssigned?.(null);
220
239
 
221
240
  if (loadToken !== this.stateStore.sessionLoadToken) {
@@ -229,6 +248,7 @@ export class WorkspaceCoordinator {
229
248
  isDrawio,
230
249
  isExcalidraw,
231
250
  isImage,
251
+ isPdf,
232
252
  session: null,
233
253
  supportsBacklinks: chromeState.supportsBacklinks,
234
254
  });
@@ -241,6 +261,7 @@ export class WorkspaceCoordinator {
241
261
  getFileList: this.getFileList,
242
262
  lineWrappingEnabled: this.getLineWrappingEnabled(),
243
263
  localUser: this.getLocalUser(),
264
+ vimModeEnabled: this.getVimModeEnabled(),
244
265
  onAwarenessChange: (users) => this.onFileAwarenessChange(users),
245
266
  onConnectionChange: (state) => this.onConnectionChange(state),
246
267
  onCommentsChange: (threads) => this.onCommentsChange?.(threads),
@@ -253,6 +274,7 @@ export class WorkspaceCoordinator {
253
274
  isBase,
254
275
  isMermaid,
255
276
  isPlantUml,
277
+ isStructurizrWorkspace,
256
278
  });
257
279
  },
258
280
  onImagePaste: (file) => this.onImagePaste?.(file),
@@ -2,6 +2,7 @@ import {
2
2
  isBaseFilePath,
3
3
  isDiagramFilePath,
4
4
  isMarkdownFilePath,
5
+ isStructurizrFilePath,
5
6
  } from '../../domain/file-kind.js';
6
7
  import { resolveApiUrl } from '../domain/runtime-paths.js';
7
8
 
@@ -18,13 +19,16 @@ export class WorkspacePreviewController {
18
19
  isExcalidrawFile,
19
20
  isBaseFile,
20
21
  isImageFile,
22
+ isPdfFile,
21
23
  isMermaidFile,
22
24
  isPlantUmlFile,
23
25
  layoutController,
24
26
  outlineController,
25
27
  previewRenderer,
28
+ pdfPreview = null,
26
29
  schedulePreviewLayoutSync,
27
30
  scrollSyncController,
31
+ structurizrPreview = null,
28
32
  videoEmbed,
29
33
  }) {
30
34
  this.backlinksPanel = backlinksPanel;
@@ -46,13 +50,20 @@ export class WorkspacePreviewController {
46
50
  this.isExcalidrawFile = isExcalidrawFile ?? (() => false);
47
51
  this.isBaseFile = isBaseFile ?? isBaseFilePath;
48
52
  this.isImageFile = isImageFile ?? (() => false);
53
+ this.isPdfFile = isPdfFile ?? (() => false);
49
54
  this.isMermaidFile = isMermaidFile ?? (() => false);
50
55
  this.isPlantUmlFile = isPlantUmlFile ?? (() => false);
51
56
  this.layoutController = layoutController;
52
57
  this.outlineController = outlineController;
53
58
  this.previewRenderer = previewRenderer;
59
+ this.pdfPreview = pdfPreview ?? { cancel() {}, render() {} };
54
60
  this.schedulePreviewLayoutSyncCallback = schedulePreviewLayoutSync;
55
61
  this.scrollSyncController = scrollSyncController;
62
+ this.structurizrPreview = structurizrPreview ?? {
63
+ queueSync() {},
64
+ render: async () => false,
65
+ reset() {},
66
+ };
56
67
  this.videoEmbed = videoEmbed;
57
68
  }
58
69
 
@@ -81,12 +92,17 @@ export class WorkspacePreviewController {
81
92
  }
82
93
 
83
94
  resetPreviewMode() {
95
+ this.pdfPreview.cancel();
96
+ this.elements.previewContainer?.classList?.remove('is-pdf-file-preview');
84
97
  this.elements.previewContent?.classList.remove('is-drawio-file-preview');
85
98
  this.elements.previewContent?.classList.remove('is-excalidraw-file-preview');
86
99
  this.elements.previewContent?.classList.remove('is-base-file-preview');
87
100
  this.elements.previewContent?.classList.remove('is-image-file-preview');
101
+ this.elements.previewContent?.classList.remove('is-pdf-file-preview');
88
102
  this.elements.previewContent?.classList.remove('is-mermaid-file-preview');
89
103
  this.elements.previewContent?.classList.remove('is-plantuml-file-preview');
104
+ this.elements.previewContent?.classList.remove('is-structurizr-file-preview');
105
+ this.structurizrPreview.reset();
90
106
  }
91
107
 
92
108
  syncFileChrome(filePath, { drawioMode = null, preferPreviewForBase = false } = {}) {
@@ -94,9 +110,11 @@ export class WorkspacePreviewController {
94
110
  const isExcalidraw = this.isExcalidrawFile(filePath);
95
111
  const isBase = this.isBaseFile(filePath);
96
112
  const isImage = this.isImageFile(filePath);
113
+ const isPdf = this.isPdfFile(filePath);
97
114
  const isMarkdown = isMarkdownFilePath(filePath);
98
115
  const isMermaid = this.isMermaidFile(filePath);
99
116
  const isPlantUml = this.isPlantUmlFile(filePath);
117
+ const isStructurizr = isStructurizrFilePath(filePath);
100
118
  const isDiagramFile = isDiagramFilePath(filePath);
101
119
  const usesHeaderBacklinks = isExcalidraw || (isDrawio && drawioMode !== 'text');
102
120
 
@@ -105,11 +123,17 @@ export class WorkspacePreviewController {
105
123
  this.elements.editorFindButton?.classList.toggle('hidden', !isMarkdown);
106
124
  this.elements.markdownToolbar?.classList.toggle('hidden', !isMarkdown);
107
125
  this.elements.exportMenuGroup?.classList.toggle('hidden', !isMarkdown);
108
- this.elements.outlineToggle?.classList.toggle('hidden', isDiagramFile || isImage || isBase);
126
+ this.elements.outlineToggle?.classList.toggle('hidden', isDiagramFile || isImage || isPdf || isBase);
109
127
  this.elements.previewContent?.classList.toggle('is-mermaid-file-preview', isMermaid);
110
128
  this.elements.previewContent?.classList.toggle('is-plantuml-file-preview', isPlantUml);
129
+ this.elements.previewContent?.classList.toggle('is-structurizr-file-preview', isStructurizr);
130
+
131
+ if (isStructurizr) {
132
+ this.outlineController.close();
133
+ this.backlinksPanel.clear();
134
+ }
111
135
 
112
- if ((isDrawio && drawioMode !== 'text') || isExcalidraw || isImage || (isBase && preferPreviewForBase)) {
136
+ if ((isDrawio && drawioMode !== 'text') || isExcalidraw || isImage || isPdf || (isBase && preferPreviewForBase)) {
113
137
  this.layoutController.setView('preview', { persist: false });
114
138
  this.outlineController.close();
115
139
  this.backlinksPanel.clear();
@@ -238,6 +262,39 @@ export class WorkspacePreviewController {
238
262
  this.schedulePreviewLayoutSyncCallback({ delayMs: 0 });
239
263
  }
240
264
 
265
+ renderPdfFilePreview(filePath) {
266
+ const previewElement = this.elements.previewContent;
267
+ if (!previewElement) {
268
+ return;
269
+ }
270
+
271
+ this.videoEmbed?.detachForCommit();
272
+ this.drawioEmbed.detachForCommit();
273
+ this.excalidrawEmbed.detachForCommit();
274
+ this.resetPreviewMode();
275
+ this.elements.previewContainer?.classList?.add('is-pdf-file-preview');
276
+ previewElement.classList.add('is-pdf-file-preview');
277
+ const renderHost = this.previewRenderer.ensureRenderHost();
278
+ this.previewRenderer.normalizePreviewChildren(renderHost);
279
+
280
+ if (renderHost) {
281
+ this.pdfPreview.render({
282
+ displayName: this.getDisplayName(filePath),
283
+ filePath,
284
+ renderHost,
285
+ });
286
+ renderHost.style.minHeight = '';
287
+ }
288
+
289
+ previewElement.dataset.renderPhase = 'ready';
290
+ this.outlineController.close();
291
+ this.backlinksPanel.clear();
292
+ this.scrollSyncController.setLargeDocumentMode(false);
293
+ this.scrollSyncController.invalidatePreviewBlocks();
294
+ this.videoEmbed?.reconcileEmbeds(previewElement);
295
+ this.schedulePreviewLayoutSyncCallback({ delayMs: 0 });
296
+ }
297
+
241
298
  async renderBaseFilePreview(filePath, { source = null } = {}) {
242
299
  const previewElement = this.elements.previewContent;
243
300
  if (!previewElement) {
@@ -272,6 +329,38 @@ export class WorkspacePreviewController {
272
329
  this.schedulePreviewLayoutSyncCallback({ delayMs: 0 });
273
330
  }
274
331
 
332
+ async renderStructurizrFilePreview(filePath, { source = null } = {}) {
333
+ const previewElement = this.elements.previewContent;
334
+ if (!previewElement) {
335
+ return;
336
+ }
337
+
338
+ this.videoEmbed?.detachForCommit();
339
+ this.drawioEmbed.detachForCommit();
340
+ this.excalidrawEmbed.detachForCommit();
341
+ this.resetPreviewMode();
342
+ previewElement.classList.add('is-structurizr-file-preview');
343
+ const renderHost = this.previewRenderer.ensureRenderHost();
344
+ this.previewRenderer.normalizePreviewChildren(renderHost);
345
+ if (renderHost) {
346
+ renderHost.style.minHeight = '';
347
+ }
348
+
349
+ await this.structurizrPreview.render({
350
+ filePath,
351
+ renderHost,
352
+ source: typeof source === 'string' ? source : (this.getSession()?.getText?.() ?? ''),
353
+ });
354
+
355
+ previewElement.dataset.renderPhase = 'ready';
356
+ this.outlineController.close();
357
+ this.backlinksPanel.clear();
358
+ this.scrollSyncController.setLargeDocumentMode(false);
359
+ this.scrollSyncController.invalidatePreviewBlocks();
360
+ this.videoEmbed?.reconcileEmbeds(previewElement);
361
+ this.schedulePreviewLayoutSyncCallback({ delayMs: 0 });
362
+ }
363
+
275
364
  renderTextFilePreview({ content = '' } = {}) {
276
365
  const previewElement = this.elements.previewContent;
277
366
  if (!previewElement) {