collabmd 0.1.44 → 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 (291) 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/{chunk-CZQZEIFA-DT3N9fo7.js → chunk-YGJVY4SA-CjSe7Bx7.js} +2 -2
  7. package/dist/client/assets/comment-thread-store-CdcwrObc.js +1 -0
  8. package/dist/client/assets/comment-threads-9J6Em-_y.js +4 -0
  9. package/dist/client/assets/comment-ui-shared-BC4pt4w8.js +7 -0
  10. package/dist/client/assets/dist-B5HTH2BO.js +1 -0
  11. package/dist/client/assets/{dist-DhLvC8DS.js → dist-B5Jf1SLZ.js} +2 -2
  12. package/dist/client/assets/{dist-CnL7Y2sM.js → dist-BDt3rz7l.js} +1 -1
  13. package/dist/client/assets/dist-BEwK4csK.js +1 -0
  14. package/dist/client/assets/dist-BKW4PALF.js +1 -0
  15. package/dist/client/assets/dist-BWcHHOx-.js +1 -0
  16. package/dist/client/assets/{dist-CYZmbokb.js → dist-BalScOwZ.js} +9 -9
  17. package/dist/client/assets/dist-Bf8f4HmM.js +6 -0
  18. package/dist/client/assets/dist-BjHJDtyU.js +1 -0
  19. package/dist/client/assets/dist-Bm6pf9Bl.js +1 -0
  20. package/dist/client/assets/dist-BoVBdSjk.js +1 -0
  21. package/dist/client/assets/dist-CC5IrQMG.js +1 -0
  22. package/dist/client/assets/{dist-C11hHqv_.js → dist-CCP_AKPG.js} +12 -12
  23. package/dist/client/assets/dist-CZgOmxK6.js +1 -0
  24. package/dist/client/assets/{dist-D2AQ-76g.js → dist-Cqhv5LAk.js} +1 -1
  25. package/dist/client/assets/{dist-MvcV4190.js → dist-D-Z6FLem.js} +1 -1
  26. package/dist/client/assets/dist-DLMiIsNC.js +11 -0
  27. package/dist/client/assets/{dist-3bgCp7iY.js → dist-DZKu9Cs8.js} +1 -1
  28. package/dist/client/assets/dist-DaYdhcVs.js +1 -0
  29. package/dist/client/assets/dist-FWJsgGYI.js +1 -0
  30. package/dist/client/assets/dist-Kh2COra_.js +1 -0
  31. package/dist/client/assets/{dist-JYEDRrPT.js → dist-PDZ3bSqm.js} +1 -1
  32. package/dist/client/assets/{dist-Drp8uu5J.js → dist-TjMh8n1u.js} +1 -1
  33. package/dist/client/assets/{dist-DGb27Dl6.js → dist-dtAdgL39.js} +1 -1
  34. package/dist/client/assets/dist-jv4WdPnK.js +9 -0
  35. package/dist/client/assets/{dist-DK5Gs2wa.js → dist-rGBmqxC7.js} +1 -1
  36. package/dist/client/assets/{dist-DCzuX1AF.js → dist-shrl6K_0.js} +1 -1
  37. package/dist/client/assets/dist-tOaegDYS2.js +1 -0
  38. package/dist/client/assets/dist-ule2n6ak.js +1 -0
  39. package/dist/client/assets/dist-xElaxbia.js +1 -0
  40. package/dist/client/assets/drawio-editor-FzY1cVw8.js +9 -0
  41. package/dist/client/assets/drawioEditor-BjiKJqlc.js +2 -0
  42. package/dist/client/assets/editor-session-Cm9ekeNo.js +54 -0
  43. package/dist/client/assets/embedded-editor-base-CwTkk911.css +1 -0
  44. package/dist/client/assets/{en-GADW4C6J-DQ6zyCEM.js → en-7NFCYGKX-BrgYZAEe.js} +1 -1
  45. package/dist/client/assets/{preview-render-profile-BZzGSg6K.js → es-DXGV3RHd.js} +13 -13
  46. package/dist/client/assets/excalidraw-editor-B46zsTFI.js +9 -0
  47. package/dist/client/assets/excalidraw-editor-Bt7gMSoh.css +1 -0
  48. package/dist/client/assets/excalidraw-scene-BOben2gr.js +1 -0
  49. package/dist/client/assets/excalidrawEditor-CT2pHq7e.js +2 -0
  50. package/dist/client/assets/export-page-A7NHawJY.js +39 -0
  51. package/dist/client/assets/exportDocument-B2L8fe34.css +1 -0
  52. package/dist/client/assets/exportDocument-BaqcOAAW.js +2 -0
  53. package/dist/client/assets/file-icon-svg-CQAjlWjK.js +1 -0
  54. package/dist/client/assets/file-kind-BbqgGzpc.js +1 -0
  55. package/dist/client/assets/image-QXRYHUNP-SWEV8H0J.js +1 -0
  56. package/dist/client/assets/index-ClbjMBVt.js +2 -0
  57. package/dist/client/assets/index-Pum8ET-5.css +1 -0
  58. package/dist/client/assets/main-B3uPyqic.js +893 -0
  59. package/dist/client/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
  60. package/dist/client/assets/pdf-BL9Pr2D5.js +54 -0
  61. package/dist/client/assets/pdf.worker-CLesOks4.mjs +64857 -0
  62. package/dist/client/assets/pdf_viewer-FVkkThnz.js +551 -0
  63. package/dist/client/assets/preview-diagram-utils-txBCoW8d.js +1 -0
  64. package/dist/client/assets/{preview-render-compiler-BSaQO_p3.js → preview-render-compiler-BD-XhkS2.js} +2 -2
  65. package/dist/client/assets/preview-render-compiler-BKedaMxx.js +1 -0
  66. package/dist/client/assets/preview-render-profile-CE-5rVTC.js +1 -0
  67. package/dist/client/assets/{preview-render-worker-B42LWvrD.js → preview-render-worker-CiPqc9n6.js} +20 -20
  68. package/dist/client/assets/prod-Dy20X7eT.js +1 -0
  69. package/dist/client/assets/prod-hnwh7wRM.js +177 -0
  70. package/dist/client/assets/quick-switcher-controller-fqOYR7qR.js +12 -0
  71. package/dist/client/assets/runtime-config-BLrEKVO1.js +1 -0
  72. package/dist/client/assets/vault-api-client-_LpV1aIn.js +6 -0
  73. package/dist/client/assets/{yjs-provider-reset-guard-Cox4nxwu.js → yjs-provider-reset-guard-DE0vINDP.js} +1 -1
  74. package/dist/client/drawio-editor.html +3 -2
  75. package/dist/client/excalidraw-editor.html +3 -2
  76. package/dist/client/export-document.html +4 -3
  77. package/dist/client/index.html +109 -27
  78. package/docker-compose.yml +42 -0
  79. package/package.json +26 -15
  80. package/scripts/local-structurizr-compose.mjs +101 -0
  81. package/src/client/app/drawio-editor-entry.js +0 -3
  82. package/src/client/app/drawio-editor.html +1 -0
  83. package/src/client/app/excalidraw-editor-entry.js +0 -3
  84. package/src/client/app/excalidraw-editor.html +1 -0
  85. package/src/client/app/export-document-entry.js +0 -3
  86. package/src/client/app/export-document.html +1 -0
  87. package/src/client/app/index.html +106 -24
  88. package/src/client/app/main-entry.js +0 -5
  89. package/src/client/application/app-shell/chat-feature.js +180 -124
  90. package/src/client/application/app-shell/comments-feature.js +20 -11
  91. package/src/client/application/app-shell/git-feature.js +16 -9
  92. package/src/client/application/app-shell/presence-feature.js +18 -11
  93. package/src/client/application/app-shell/ui-feature-shell.js +78 -17
  94. package/src/client/application/app-shell/ui-feature-sidebar.js +74 -1
  95. package/src/client/application/app-shell/ui-feature-tab-activity.js +2 -2
  96. package/src/client/application/app-shell/ui-feature-toolbar.js +2 -52
  97. package/src/client/application/app-shell/workspace-feature.js +27 -84
  98. package/src/client/application/app-shell-elements.js +9 -1
  99. package/src/client/application/diagram-chrome.js +161 -166
  100. package/src/client/application/diagram-preview-export.js +7 -34
  101. package/src/client/application/diagram-preview-hydrator.js +1 -19
  102. package/src/client/application/markdown-frontmatter.js +0 -2
  103. package/src/client/application/mermaid-preview-hydrator.js +1 -14
  104. package/src/client/application/pdf-preview-controller.js +992 -0
  105. package/src/client/application/plantuml-preview-hydrator.js +0 -10
  106. package/src/client/application/preview-diagram-utils.js +31 -2
  107. package/src/client/application/preview-render-compiler.js +2 -1
  108. package/src/client/application/preview-renderer.js +2 -2
  109. package/src/client/application/quick-switcher-loader.js +3 -1
  110. package/src/client/application/structurizr-preview-controller.js +395 -0
  111. package/src/client/application/workspace-coordinator.js +25 -3
  112. package/src/client/application/workspace-preview-controller.js +91 -5
  113. package/src/client/bootstrap/collabmd-app-shell.js +155 -43
  114. package/src/client/browser-utils.js +44 -0
  115. package/src/client/domain/excalidraw-collaboration.js +0 -2
  116. package/src/client/domain/room.js +38 -0
  117. package/src/client/domain/runtime-paths.js +26 -5
  118. package/src/client/domain/structurizr-language.js +99 -0
  119. package/src/client/domain/vault-paths.js +48 -0
  120. package/src/client/excalidraw-editor.js +833 -51
  121. package/src/client/export/export-pipeline.js +6 -34
  122. package/src/client/infrastructure/auth-client.js +4 -10
  123. package/src/client/infrastructure/browser-notification-port.js +33 -20
  124. package/src/client/infrastructure/browser-preferences-port.js +46 -9
  125. package/src/client/infrastructure/comment-thread-store.js +34 -13
  126. package/src/client/infrastructure/editor-session.js +10 -0
  127. package/src/client/infrastructure/editor-view-adapter.js +133 -5
  128. package/src/client/infrastructure/excalidraw-room-client.js +103 -14
  129. package/src/client/infrastructure/git-api-client.js +5 -0
  130. package/src/client/infrastructure/lobby-presence.js +1 -0
  131. package/src/client/infrastructure/vault-api-client.js +51 -0
  132. package/src/client/infrastructure/workspace-sync-client.js +13 -1
  133. package/src/client/presentation/comment-overview-controller.js +18 -53
  134. package/src/client/presentation/comment-ui/comment-ui-card.js +1 -12
  135. package/src/client/presentation/comment-ui/comment-ui-layout.js +242 -219
  136. package/src/client/presentation/comment-ui/comment-ui-render.js +32 -45
  137. package/src/client/presentation/comment-ui/comment-ui-shared.js +68 -13
  138. package/src/client/presentation/comment-ui-controller.js +5 -1
  139. package/src/client/presentation/drawio-embed-controller.js +9 -102
  140. package/src/client/presentation/drawio-viewer.js +118 -0
  141. package/src/client/presentation/excalidraw-embed-controller.js +74 -8
  142. package/src/client/presentation/file-action-controller.js +134 -0
  143. package/src/client/presentation/file-explorer-controller.js +33 -3
  144. package/src/client/presentation/file-explorer-view.js +59 -34
  145. package/src/client/presentation/file-icon-svg.js +40 -0
  146. package/src/client/presentation/file-tree-state.js +12 -4
  147. package/src/client/presentation/git-diff-view-controller.js +731 -3
  148. package/src/client/presentation/outline-controller.js +86 -0
  149. package/src/client/presentation/quick-switcher-controller.js +257 -74
  150. package/src/client/presentation/quick-switcher-text-search.js +3 -3
  151. package/src/client/presentation/toast-controller.js +3 -0
  152. package/src/client/styles/components/feedback.css +6 -0
  153. package/src/client/styles/components/navigation.css +11 -1
  154. package/src/client/styles/features/collaboration-chat.css +49 -53
  155. package/src/client/styles/features/collaboration-presence.css +9 -7
  156. package/src/client/styles/features/comment-card.css +25 -11
  157. package/src/client/styles/features/comment-overlays.css +0 -4
  158. package/src/client/styles/features/comment-overview.css +44 -21
  159. package/src/client/styles/features/comments-drawer.css +2 -100
  160. package/src/client/styles/features/diagram-preview.css +81 -18
  161. package/src/client/styles/features/diff-special.css +309 -0
  162. package/src/client/styles/features/embedded-preview.css +8 -33
  163. package/src/client/styles/features/quick-switcher.css +8 -0
  164. package/src/client/styles/foundation/themes.css +6 -0
  165. package/src/client/styles/foundation/tokens.css +1 -0
  166. package/src/client/styles/layout/editor-page.css +40 -0
  167. package/src/client/styles/layout/responsive.css +31 -1
  168. package/src/client/styles/layout/shell.css +383 -7
  169. package/src/client/styles/layout/sidebar.css +47 -15
  170. package/src/client/styles/layout/view-modes.css +1 -0
  171. package/src/client/styles/overrides/codemirror.css +217 -15
  172. package/src/client/styles/style.css +1 -0
  173. package/src/client/styles/surfaces/excalidraw-editor.css +294 -0
  174. package/src/domain/comment-threads.js +63 -5
  175. package/src/domain/excalidraw-room-codec.js +6 -4
  176. package/src/domain/file-kind.js +35 -3
  177. package/src/server/application/workspace-reconciliation.js +1 -1
  178. package/src/server/config/env.js +79 -0
  179. package/src/server/create-app-server.js +14 -3
  180. package/src/server/domain/bases/base-definition.js +1 -1
  181. package/src/server/domain/bases/base-expression-runtime.js +1 -4
  182. package/src/server/domain/bases/base-index-snapshot-store.js +1 -4
  183. package/src/server/domain/bases/base-query-service.js +1 -77
  184. package/src/server/domain/bases/base-transform.js +1 -4
  185. package/src/server/domain/collaboration/collaboration-room.js +55 -21
  186. package/src/server/domain/comment-overview.js +25 -8
  187. package/src/server/domain/docx-exporter.js +24 -33
  188. package/src/server/domain/ripgrep-search-service.js +7 -1
  189. package/src/server/domain/workspace-state.js +32 -11
  190. package/src/server/infrastructure/git/diff-service.js +29 -13
  191. package/src/server/infrastructure/git/git-service.js +61 -10
  192. package/src/server/infrastructure/git/history-service.js +96 -29
  193. package/src/server/infrastructure/git/image-mime.js +14 -0
  194. package/src/server/infrastructure/git/path-utils.js +1 -1
  195. package/src/server/infrastructure/git/responses.js +0 -95
  196. package/src/server/infrastructure/git/untracked-files.js +28 -1
  197. package/src/server/infrastructure/github/github-app-client.js +22 -15
  198. package/src/server/infrastructure/http/create-git-api-query-handler.js +31 -1
  199. package/src/server/infrastructure/http/create-request-handler.js +18 -31
  200. package/src/server/infrastructure/http/create-static-handler.js +5 -2
  201. package/src/server/infrastructure/http/create-structurizr-api-handler.js +145 -0
  202. package/src/server/infrastructure/http/create-vault-api-command-handler.js +70 -8
  203. package/src/server/infrastructure/http/create-vault-api-query-handler.js +51 -3
  204. package/src/server/infrastructure/http/http-errors.js +7 -1
  205. package/src/server/infrastructure/http/http-request-helpers.js +22 -0
  206. package/src/server/infrastructure/persistence/path-utils.js +1 -1
  207. package/src/server/infrastructure/persistence/vault-file-store.js +3 -0
  208. package/src/server/infrastructure/structurizr/structurizr-embed.css +43 -0
  209. package/src/server/infrastructure/structurizr/structurizr-workspace-service.js +374 -0
  210. package/src/server/infrastructure/websocket/attach-collaboration-gateway.js +4 -22
  211. package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-CbSISC1w.js +0 -1
  212. package/dist/client/assets/bases-preview-controller-2pLHk0uk.js +0 -143
  213. package/dist/client/assets/browser-utils-CKtI_rZl.js +0 -1
  214. package/dist/client/assets/button-BCEiw4Rw.js +0 -1
  215. package/dist/client/assets/chunk-AXR3VHVW-0GTk_evH.js +0 -41
  216. package/dist/client/assets/comment-threads-D-iKphIp.js +0 -4
  217. package/dist/client/assets/diagram-action-icons-aa_KOv9h.js +0 -36
  218. package/dist/client/assets/dist-BH87JoHx.js +0 -11
  219. package/dist/client/assets/dist-BIkPRZO7.js +0 -6
  220. package/dist/client/assets/dist-BqCsFpx4.js +0 -1
  221. package/dist/client/assets/dist-C2JHCt8U.js +0 -1
  222. package/dist/client/assets/dist-CZjTtmTu.js +0 -1
  223. package/dist/client/assets/dist-Cig4Ov_X.js +0 -1
  224. package/dist/client/assets/dist-CqQEC8gR.js +0 -1
  225. package/dist/client/assets/dist-D4Rzqod1.js +0 -1
  226. package/dist/client/assets/dist-D5sn-mwk.js +0 -9
  227. package/dist/client/assets/dist-D7G-n87v.js +0 -1
  228. package/dist/client/assets/dist-DRqOrNgN.js +0 -1
  229. package/dist/client/assets/dist-DetkMS_4.js +0 -1
  230. package/dist/client/assets/dist-Dp6FVkQ1.js +0 -1
  231. package/dist/client/assets/dist-DtbjNB28.js +0 -1
  232. package/dist/client/assets/dist-DyrYcvpo2.js +0 -1
  233. package/dist/client/assets/dist-GrygD2zE.js +0 -1
  234. package/dist/client/assets/dist-HmEpWUow.js +0 -1
  235. package/dist/client/assets/dist-RTa81Km7.js +0 -1
  236. package/dist/client/assets/drawio-editor-DDvFoarP.js +0 -9
  237. package/dist/client/assets/drawio-embed-controller-BD1GTRwn.js +0 -1
  238. package/dist/client/assets/drawioEditor-Bu5grlH5.js +0 -2
  239. package/dist/client/assets/editor-session-DGlpiAsK.js +0 -16
  240. package/dist/client/assets/embedded-editor-base-C9roKYpW.css +0 -1
  241. package/dist/client/assets/excalidraw-editor-BHu6k8d4.js +0 -9
  242. package/dist/client/assets/excalidraw-editor-Cr_184EF.css +0 -1
  243. package/dist/client/assets/excalidraw-embed-controller-y2fenN_g.js +0 -1
  244. package/dist/client/assets/excalidraw-embed-reconciler-DGiagExg.js +0 -1
  245. package/dist/client/assets/excalidraw-scene-BvsQ6mLP.js +0 -176
  246. package/dist/client/assets/excalidrawEditor-4vFE--Uw.js +0 -2
  247. package/dist/client/assets/export-page-Cby8JzPO.js +0 -39
  248. package/dist/client/assets/exportDocument-C9v-bc7I.css +0 -1
  249. package/dist/client/assets/exportDocument-CRHCfsdo.js +0 -2
  250. package/dist/client/assets/file-history-view-controller-BDjEV9xJ.js +0 -112
  251. package/dist/client/assets/file-kind-BKPOIRPE.js +0 -1
  252. package/dist/client/assets/git-diff-view-controller-DgdM1aqb.js +0 -150
  253. package/dist/client/assets/git-panel-controller-BDTR2HVp.js +0 -207
  254. package/dist/client/assets/image-XGD7JQR6-XDUWHRpO.js +0 -1
  255. package/dist/client/assets/index-7xOyXDhP.css +0 -1
  256. package/dist/client/assets/index-Bz1KuQ6y.js +0 -2
  257. package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  258. package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  259. package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  260. package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  261. package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  262. package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  263. package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  264. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  265. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  266. package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  267. package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  268. package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  269. package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  270. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  271. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  272. package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  273. package/dist/client/assets/main-Ci07m9oP.js +0 -152
  274. package/dist/client/assets/preview-diagram-utils-n_bbJA9V.js +0 -1
  275. package/dist/client/assets/preview-render-compiler-csGM5ZFz.js +0 -1
  276. package/dist/client/assets/quick-switcher-controller-CoiYvllm.js +0 -12
  277. package/dist/client/assets/room-CDrmIdDt.js +0 -1
  278. package/dist/client/assets/runtime-config-CDgeuPWN.js +0 -1
  279. package/dist/client/assets/runtime-config-loader-BHGniHP5.js +0 -1
  280. package/dist/client/assets/runtime-paths-DvpihCEq.js +0 -1
  281. package/dist/client/assets/segmented-control-DJrHSz8M.js +0 -1
  282. package/dist/client/assets/vault-api-client-sLB2bo56.js +0 -1
  283. package/dist/client/assets/vault-paths-CVkuh0j-.js +0 -5
  284. package/src/client/bootstrap/lazy-controller-feature.js +0 -433
  285. package/src/client/infrastructure/backlinks-api-client.js +0 -16
  286. package/src/client/infrastructure/plantuml-api-client.js +0 -19
  287. package/src/client/infrastructure/runtime-config-loader.js +0 -56
  288. package/src/server/domain/collaboration/room-persistence-controller.js +0 -80
  289. package/src/server/infrastructure/git/command-runner.js +0 -49
  290. package/src/server/infrastructure/git/errors.js +0 -8
  291. package/src/server/infrastructure/http/create-esm-proxy-handler.js +0 -130
@@ -1,6 +1,11 @@
1
1
  import { exportToSvg as exportExcalidrawToSvg } from '@excalidraw/excalidraw';
2
2
 
3
- import { sanitizeSvgMarkup } from '../application/preview-diagram-utils.js';
3
+ import {
4
+ encodeSvgDataUrl,
5
+ loadImage,
6
+ resolveSvgDimensions,
7
+ sanitizeSvgMarkup,
8
+ } from '../application/preview-diagram-utils.js';
4
9
  import { compilePreviewDocument } from '../application/preview-render-compiler.js';
5
10
  import { stripVaultFileExtension } from '../../domain/file-kind.js';
6
11
  import { parseSceneJson, sceneToInitialData } from '../domain/excalidraw-scene.js';
@@ -60,10 +65,6 @@ function createDocumentTitle(filePath, title = '') {
60
65
  return stripVaultFileExtension(leaf) || 'document';
61
66
  }
62
67
 
63
- function encodeSvgDataUrl(svgMarkup) {
64
- return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgMarkup)}`;
65
- }
66
-
67
68
  function encodeSvgBase64DataUrl(svgMarkup) {
68
69
  const bytes = new TextEncoder().encode(String(svgMarkup ?? ''));
69
70
  const chunkSize = 0x8000;
@@ -87,35 +88,6 @@ function blobToDataUrl(blob) {
87
88
  });
88
89
  }
89
90
 
90
- function loadImage(src) {
91
- return new Promise((resolve, reject) => {
92
- const image = new Image();
93
- image.decoding = 'async';
94
- image.addEventListener('load', () => resolve(image), { once: true });
95
- image.addEventListener('error', () => reject(new Error(`Failed to load image: ${src}`)), { once: true });
96
- image.src = src;
97
- });
98
- }
99
-
100
- function resolveSvgDimensions(svgElement) {
101
- const widthAttr = Number.parseFloat(svgElement.getAttribute('width') || '');
102
- const heightAttr = Number.parseFloat(svgElement.getAttribute('height') || '');
103
- const viewBox = svgElement.getAttribute('viewBox') || '';
104
- const viewBoxParts = viewBox.split(/\s+/).map((value) => Number.parseFloat(value));
105
-
106
- const width = Number.isFinite(widthAttr) && widthAttr > 0
107
- ? widthAttr
108
- : (Number.isFinite(viewBoxParts[2]) && viewBoxParts[2] > 0 ? viewBoxParts[2] : 1200);
109
- const height = Number.isFinite(heightAttr) && heightAttr > 0
110
- ? heightAttr
111
- : (Number.isFinite(viewBoxParts[3]) && viewBoxParts[3] > 0 ? viewBoxParts[3] : 800);
112
-
113
- return {
114
- height,
115
- width,
116
- };
117
- }
118
-
119
91
  function trimSvgCanvas(svgMarkup, { padding = 16 } = {}) {
120
92
  const probe = document.createElement('div');
121
93
  probe.style.position = 'fixed';
@@ -1,5 +1,6 @@
1
1
  import '../styles/features/auth-gate.css';
2
2
 
3
+ import { getHashParamsFromRaw } from '../domain/hash-routes.js';
3
4
  import { getRuntimeConfig } from './runtime-config.js';
4
5
 
5
6
  const DEFAULT_AUTH_CONFIG = {
@@ -24,19 +25,12 @@ function getClientAuthConfig() {
24
25
  };
25
26
  }
26
27
 
27
- function getHashParams() {
28
- const rawHash = window.location.hash.startsWith('#')
29
- ? window.location.hash.slice(1)
30
- : window.location.hash;
31
- return new URLSearchParams(rawHash);
32
- }
33
-
34
28
  function getPasswordFromHash() {
35
- return getHashParams().get('auth_password') || '';
29
+ return getHashParamsFromRaw(window.location.hash).get('auth_password') || '';
36
30
  }
37
31
 
38
32
  function removePasswordFromHash() {
39
- const params = getHashParams();
33
+ const params = getHashParamsFromRaw(window.location.hash);
40
34
  if (!params.has('auth_password')) {
41
35
  return;
42
36
  }
@@ -48,7 +42,7 @@ function removePasswordFromHash() {
48
42
  }
49
43
 
50
44
  function consumeHashParam(name) {
51
- const params = getHashParams();
45
+ const params = getHashParamsFromRaw(window.location.hash);
52
46
  const value = params.get(name) || '';
53
47
  if (!value) {
54
48
  return '';
@@ -1,36 +1,49 @@
1
1
  export class BrowserNotificationPort {
2
- constructor({
3
- NotificationImpl = globalThis.Notification,
4
- focusWindow = () => globalThis.window?.focus?.(),
5
- } = {}) {
6
- this.NotificationImpl = NotificationImpl;
7
- this.focusWindow = focusWindow;
2
+ getApi() {
3
+ return typeof globalThis.Notification === 'function'
4
+ ? globalThis.Notification
5
+ : null;
8
6
  }
9
7
 
10
8
  getPermission() {
11
- return typeof this.NotificationImpl === 'function'
12
- ? this.NotificationImpl.permission
13
- : 'unsupported';
9
+ return this.getApi()?.permission ?? 'unsupported';
14
10
  }
15
11
 
16
12
  async requestPermission() {
17
- return typeof this.NotificationImpl === 'function'
18
- ? this.NotificationImpl.requestPermission()
19
- : 'unsupported';
13
+ const api = this.getApi();
14
+ if (!api || typeof api.requestPermission !== 'function') {
15
+ return 'unsupported';
16
+ }
17
+
18
+ if (api.permission !== 'default') {
19
+ return api.permission;
20
+ }
21
+
22
+ try {
23
+ return await api.requestPermission();
24
+ } catch {
25
+ return api.permission ?? 'default';
26
+ }
20
27
  }
21
28
 
22
- createNotification({ body, onClick, tag, title }) {
23
- if (typeof this.NotificationImpl !== 'function') {
29
+ show({ body, onClick, tag, title } = {}) {
30
+ const api = this.getApi();
31
+ if (!api || api.permission !== 'granted' || !body) {
24
32
  return null;
25
33
  }
26
34
 
27
- const notification = new this.NotificationImpl(title, { body, tag });
28
- if (typeof onClick === 'function') {
29
- notification.addEventListener('click', () => {
30
- this.focusWindow();
31
- onClick();
35
+ try {
36
+ const notification = new api(title, {
37
+ body,
38
+ renotify: true,
39
+ tag,
32
40
  });
41
+ if (onClick) {
42
+ notification.onclick = onClick;
43
+ }
44
+ return notification;
45
+ } catch {
46
+ return null;
33
47
  }
34
- return notification;
35
48
  }
36
49
  }
@@ -15,19 +15,25 @@ function writeStorage(storage, key, value) {
15
15
  }
16
16
  }
17
17
 
18
+ const MAX_RECENT_FILES = 20;
19
+
18
20
  export class BrowserPreferencesPort {
19
21
  constructor({
20
- chatNotificationsKey,
22
+ fileTreeShowExtensionsKey,
21
23
  lineWrappingKey,
24
+ recentFilesKey = 'collabmd-recent-files',
25
+ vimModeKey = 'collabmd-editor-vim-mode',
22
26
  sidebarVisibleKey,
23
27
  userNameKey,
24
28
  storage = globalThis.localStorage,
25
29
  }) {
26
- this.chatNotificationsKey = chatNotificationsKey;
30
+ this.fileTreeShowExtensionsKey = fileTreeShowExtensionsKey;
27
31
  this.lineWrappingKey = lineWrappingKey;
32
+ this.recentFilesKey = recentFilesKey;
28
33
  this.sidebarVisibleKey = sidebarVisibleKey;
29
34
  this.storage = storage;
30
35
  this.userNameKey = userNameKey;
36
+ this.vimModeKey = vimModeKey;
31
37
  }
32
38
 
33
39
  getUserName() {
@@ -38,6 +44,14 @@ export class BrowserPreferencesPort {
38
44
  writeStorage(this.storage, this.userNameKey, name);
39
45
  }
40
46
 
47
+ getFileTreeShowExtensions() {
48
+ return readStorage(this.storage, this.fileTreeShowExtensionsKey, null) === 'true';
49
+ }
50
+
51
+ setFileTreeShowExtensions(showFileExtensions) {
52
+ writeStorage(this.storage, this.fileTreeShowExtensionsKey, showFileExtensions ? 'true' : 'false');
53
+ }
54
+
41
55
  getLineWrappingEnabled() {
42
56
  return readStorage(this.storage, this.lineWrappingKey, null) !== 'false';
43
57
  }
@@ -46,6 +60,36 @@ export class BrowserPreferencesPort {
46
60
  writeStorage(this.storage, this.lineWrappingKey, String(enabled));
47
61
  }
48
62
 
63
+ getVimModeEnabled() {
64
+ return readStorage(this.storage, this.vimModeKey, null) === 'true';
65
+ }
66
+
67
+ setVimModeEnabled(enabled) {
68
+ writeStorage(this.storage, this.vimModeKey, String(enabled));
69
+ }
70
+
71
+ getRecentFiles() {
72
+ const stored = readStorage(this.storage, this.recentFilesKey, '[]');
73
+ try {
74
+ const recentFiles = JSON.parse(stored);
75
+ return Array.isArray(recentFiles)
76
+ ? [...new Set(recentFiles.filter((filePath) => typeof filePath === 'string' && filePath))]
77
+ : [];
78
+ } catch {
79
+ return [];
80
+ }
81
+ }
82
+
83
+ recordRecentFile(filePath) {
84
+ if (!filePath) return;
85
+
86
+ const recentFiles = [
87
+ filePath,
88
+ ...this.getRecentFiles().filter((recentFilePath) => recentFilePath !== filePath),
89
+ ];
90
+ writeStorage(this.storage, this.recentFilesKey, JSON.stringify(recentFiles.slice(0, MAX_RECENT_FILES)));
91
+ }
92
+
49
93
  getSidebarVisible() {
50
94
  return readStorage(this.storage, this.sidebarVisibleKey, null);
51
95
  }
@@ -54,11 +98,4 @@ export class BrowserPreferencesPort {
54
98
  writeStorage(this.storage, this.sidebarVisibleKey, showSidebar ? 'true' : 'false');
55
99
  }
56
100
 
57
- getChatNotificationsEnabled() {
58
- return readStorage(this.storage, this.chatNotificationsKey, null) === 'true';
59
- }
60
-
61
- setChatNotificationsEnabled(enabled) {
62
- writeStorage(this.storage, this.chatNotificationsKey, String(enabled));
63
- }
64
101
  }
@@ -83,15 +83,21 @@ function normalizeSelectionAnchorPayload(payload, state) {
83
83
 
84
84
  export class CommentThreadStore {
85
85
  constructor({
86
+ canWrite = () => true,
87
+ createAnchor = null,
86
88
  getDoc,
87
89
  getEditorState,
88
90
  getLocalUser,
89
91
  onCommentsChange = null,
92
+ resolveThread = null,
90
93
  }) {
94
+ this.canWrite = canWrite;
95
+ this.createAnchor = createAnchor;
91
96
  this.getDoc = getDoc;
92
97
  this.getEditorState = getEditorState;
93
98
  this.getLocalUser = getLocalUser;
94
99
  this.onCommentsChange = onCommentsChange;
100
+ this.resolveThread = resolveThread;
95
101
  this.commentThreads = null;
96
102
  this.ydoc = null;
97
103
  this.ytext = null;
@@ -144,33 +150,44 @@ export class CommentThreadStore {
144
150
  }
145
151
 
146
152
  return serializeCommentThreads(this.commentThreads)
147
- .map((thread) => this.resolveCommentThread(thread))
153
+ .map((thread) => this.resolveThread
154
+ ? this.resolveThread(thread)
155
+ : this.resolveCommentThread(thread))
148
156
  .filter(Boolean);
149
157
  }
150
158
 
151
159
  createCommentThread({ anchor, body }) {
152
- const state = this.getEditorState();
153
- if (!state || !this.commentThreads || !this.ytext || !this.ydoc) {
160
+ if (!this.canWrite() || !this.commentThreads || !this.ydoc) {
154
161
  return null;
155
162
  }
156
163
 
164
+ const state = this.getEditorState?.();
157
165
  const normalizedBody = normalizeCommentBody(body);
158
- const normalizedAnchor = normalizeSelectionAnchorPayload(anchor, state);
166
+ if (!this.createAnchor && (!state || !this.ytext)) {
167
+ return null;
168
+ }
169
+
170
+ const normalizedAnchor = this.createAnchor
171
+ ? this.createAnchor(anchor)
172
+ : normalizeSelectionAnchorPayload(anchor, state);
159
173
  if (!normalizedBody || !normalizedAnchor) {
160
174
  return null;
161
175
  }
162
176
 
163
177
  const thread = createCommentThreadSharedType({
164
- anchorEnd: Y.relativePositionToJSON(
165
- Y.createRelativePositionFromTypeIndex(this.ytext, normalizedAnchor.endIndex),
166
- ),
167
- anchorEndLine: normalizedAnchor.anchorEndLine,
178
+ ...normalizedAnchor,
179
+ ...(this.createAnchor ? {} : {
180
+ anchorEnd: Y.relativePositionToJSON(
181
+ Y.createRelativePositionFromTypeIndex(this.ytext, normalizedAnchor.endIndex),
182
+ ),
183
+ }),
168
184
  anchorKind: normalizedAnchor.anchorKind,
169
185
  anchorQuote: normalizedAnchor.anchorQuote,
170
- anchorStart: Y.relativePositionToJSON(
171
- Y.createRelativePositionFromTypeIndex(this.ytext, normalizedAnchor.startIndex),
172
- ),
173
- anchorStartLine: normalizedAnchor.anchorStartLine,
186
+ ...(this.createAnchor ? {} : {
187
+ anchorStart: Y.relativePositionToJSON(
188
+ Y.createRelativePositionFromTypeIndex(this.ytext, normalizedAnchor.startIndex),
189
+ ),
190
+ }),
174
191
  createdAt: Date.now(),
175
192
  createdByColor: this.getLocalUser()?.color ?? '',
176
193
  createdByName: this.getLocalUser()?.name ?? 'Anonymous',
@@ -194,6 +211,10 @@ export class CommentThreadStore {
194
211
  }
195
212
 
196
213
  replyToCommentThread(threadId, body) {
214
+ if (!this.canWrite()) {
215
+ return null;
216
+ }
217
+
197
218
  const normalizedBody = normalizeCommentBody(body);
198
219
  if (!normalizedBody || !this.ydoc) {
199
220
  return null;
@@ -291,7 +312,7 @@ export class CommentThreadStore {
291
312
  }
292
313
 
293
314
  deleteCommentThread(threadId) {
294
- if (!this.commentThreads || !this.ydoc) {
315
+ if (!this.canWrite() || !this.commentThreads || !this.ydoc) {
295
316
  return false;
296
317
  }
297
318
 
@@ -8,6 +8,7 @@ export class EditorSession {
8
8
  constructor({
9
9
  editorContainer,
10
10
  lineWrappingEnabled = true,
11
+ vimModeEnabled = false,
11
12
  initialTheme,
12
13
  lineInfoElement,
13
14
  onAwarenessChange,
@@ -44,6 +45,7 @@ export class EditorSession {
44
45
  initialTheme,
45
46
  lineInfoElement,
46
47
  lineWrappingEnabled,
48
+ vimModeEnabled,
47
49
  onDocChanged: () => {
48
50
  this.emitContentChange();
49
51
  },
@@ -210,6 +212,14 @@ export class EditorSession {
210
212
  return this.viewAdapter.isLineWrappingEnabled();
211
213
  }
212
214
 
215
+ isVimModeEnabled() {
216
+ return this.viewAdapter.isVimModeEnabled();
217
+ }
218
+
219
+ setVimMode(enabled) {
220
+ return this.viewAdapter.setVimMode(enabled);
221
+ }
222
+
213
223
  setLineWrapping(enabled) {
214
224
  return this.viewAdapter.setLineWrapping(enabled);
215
225
  }
@@ -1,3 +1,4 @@
1
+ import { vim } from '@replit/codemirror-vim';
1
2
  import { autocompletion, closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
2
3
  import {
3
4
  defaultKeymap,
@@ -20,7 +21,14 @@ import {
20
21
  syntaxHighlighting,
21
22
  } from '@codemirror/language';
22
23
  import { languages } from '@codemirror/language-data';
23
- import { highlightSelectionMatches, openSearchPanel, searchKeymap } from '@codemirror/search';
24
+ import {
25
+ findNext,
26
+ getSearchQuery,
27
+ highlightSelectionMatches,
28
+ openSearchPanel,
29
+ searchKeymap,
30
+ searchPanelOpen,
31
+ } from '@codemirror/search';
24
32
  import { Compartment, EditorSelection, EditorState, Prec, StateEffect, StateField } from '@codemirror/state';
25
33
  import { oneDark } from '@codemirror/theme-one-dark';
26
34
  import {
@@ -38,15 +46,26 @@ import {
38
46
  } from '@codemirror/view';
39
47
  import { yCollab } from 'y-codemirror.next';
40
48
 
41
- import { isBaseFilePath, isMermaidFilePath, isPlantUmlFilePath } from '../../domain/file-kind.js';
49
+ import {
50
+ isBaseFilePath,
51
+ isMermaidFilePath,
52
+ isPlantUmlFilePath,
53
+ isStructurizrFilePath,
54
+ } from '../../domain/file-kind.js';
42
55
  import { normalizeCommentQuote } from '../../domain/comment-threads.js';
43
56
  import { createMarkdownToolbarEdit } from '../domain/markdown-formatting.js';
44
57
  import { mermaidLanguage, mermaidLanguageDescription } from '../domain/mermaid-language.js';
45
58
  import { wikiLinkCompletions } from '../domain/wiki-link-completions.js';
46
59
  import { plantUmlLanguage, plantUmlLanguageDescription } from '../domain/plantuml-language.js';
60
+ import { structurizrLanguage, structurizrLanguageDescription } from '../domain/structurizr-language.js';
47
61
  import { handleImagePasteEvent } from './editor-paste-utils.js';
48
62
 
49
- const markdownCodeLanguages = [...languages, mermaidLanguageDescription, plantUmlLanguageDescription];
63
+ const markdownCodeLanguages = [
64
+ ...languages,
65
+ mermaidLanguageDescription,
66
+ plantUmlLanguageDescription,
67
+ structurizrLanguageDescription,
68
+ ];
50
69
  const pairedMatchingBracketMark = Decoration.mark({ class: 'cm-matchingBracket cm-matchingBracket-paired' });
51
70
  const nonmatchingBracketMark = Decoration.mark({ class: 'cm-nonmatchingBracket' });
52
71
  const remoteUpdateMark = Decoration.mark({ class: 'cm-remoteUpdateFlash' });
@@ -54,10 +73,82 @@ const REMOTE_UPDATE_FLASH_DURATION_MS = 1350;
54
73
  const REMOTE_UPDATE_CARET_MAX_LENGTH = 160;
55
74
  const RECENT_LOCAL_INPUT_WINDOW_MS = 900;
56
75
  const TASK_LIST_MARKER_PATTERN = /^(\s*(?:>\s*)*(?:[-+*]|\d+[.)])\s+\[)( |x|X)(\])(\s.*)?$/;
76
+
77
+ function openSearchPanelPreservingViewport(view) {
78
+ const scrollTop = view.scrollDOM.scrollTop;
79
+ const selection = view.state.selection;
80
+ const applied = openSearchPanel(view);
81
+
82
+ if (applied) {
83
+ requestAnimationFrame(() => {
84
+ if (view.dom.isConnected && view.state.selection.eq(selection)) {
85
+ view.scrollDOM.scrollTop = scrollTop;
86
+ }
87
+ });
88
+ }
89
+
90
+ return applied;
91
+ }
92
+
93
+ function getSearchQueryKey(query) {
94
+ return JSON.stringify([query.search, query.caseSensitive, query.regexp, query.wholeWord]);
95
+ }
96
+
97
+ function createSearchNavigationListener() {
98
+ let lastQueryKey = '';
99
+ let panelWasOpen = false;
100
+ let pendingFrame = 0;
101
+
102
+ return EditorView.updateListener.of((update) => {
103
+ const panelOpen = searchPanelOpen(update.state);
104
+ const query = getSearchQuery(update.state);
105
+ const queryKey = getSearchQueryKey(query);
106
+ const shouldReveal = panelOpen
107
+ && query.valid
108
+ && (!panelWasOpen || queryKey !== lastQueryKey);
109
+
110
+ panelWasOpen = panelOpen;
111
+ lastQueryKey = queryKey;
112
+
113
+ if (!shouldReveal) {
114
+ return;
115
+ }
116
+
117
+ if (pendingFrame) {
118
+ cancelAnimationFrame(pendingFrame);
119
+ }
120
+
121
+ pendingFrame = requestAnimationFrame(() => {
122
+ pendingFrame = 0;
123
+ if (!update.view.dom.isConnected) {
124
+ return;
125
+ }
126
+
127
+ const currentQuery = getSearchQuery(update.view.state);
128
+ if (
129
+ !searchPanelOpen(update.view.state)
130
+ || !currentQuery.valid
131
+ || getSearchQueryKey(currentQuery) !== queryKey
132
+ ) {
133
+ return;
134
+ }
135
+
136
+ const searchInput = update.view.dom.querySelector('.cm-search input[name="search"]');
137
+ const inputSelection = searchInput === update.view.root.activeElement && searchInput
138
+ ? { end: searchInput.selectionEnd, start: searchInput.selectionStart }
139
+ : null;
140
+ findNext(update.view);
141
+ if (inputSelection && searchInput?.setSelectionRange) {
142
+ searchInput.setSelectionRange(inputSelection.start, inputSelection.end);
143
+ }
144
+ });
145
+ });
146
+ }
147
+
57
148
  const EDITOR_COMMANDS = Object.freeze({
58
149
  indentLess,
59
150
  indentMore,
60
- openSearch: openSearchPanel,
151
+ openSearch: openSearchPanelPreservingViewport,
61
152
  redo,
62
153
  undo,
63
154
  });
@@ -308,6 +399,10 @@ export function createLanguageExtension(filePath) {
308
399
  return plantUmlLanguage;
309
400
  }
310
401
 
402
+ if (isStructurizrFilePath(filePath)) {
403
+ return structurizrLanguage;
404
+ }
405
+
311
406
  if (isBaseFilePath(filePath)) {
312
407
  return yaml();
313
408
  }
@@ -329,6 +424,7 @@ export class EditorViewAdapter {
329
424
  initialTheme,
330
425
  lineInfoElement,
331
426
  lineWrappingEnabled = true,
427
+ vimModeEnabled = false,
332
428
  onDocChanged = null,
333
429
  onImagePaste = null,
334
430
  onSelectionChanged = null,
@@ -339,6 +435,7 @@ export class EditorViewAdapter {
339
435
  this.initialTheme = initialTheme;
340
436
  this.lineInfoElement = lineInfoElement;
341
437
  this.lineWrappingEnabled = lineWrappingEnabled;
438
+ this.vimModeEnabled = Boolean(vimModeEnabled);
342
439
  this.onDocChanged = onDocChanged;
343
440
  this.onImagePaste = onImagePaste;
344
441
  this.onSelectionChanged = onSelectionChanged;
@@ -347,6 +444,7 @@ export class EditorViewAdapter {
347
444
  this.themeCompartment = new Compartment();
348
445
  this.syntaxThemeCompartment = new Compartment();
349
446
  this.lineWrappingCompartment = new Compartment();
447
+ this.vimModeCompartment = new Compartment();
350
448
  this.viewportFrame = 0;
351
449
  this.remoteUpdateFlashTimer = 0;
352
450
  this.lastLocalInputAt = 0;
@@ -398,11 +496,17 @@ export class EditorViewAdapter {
398
496
  crosshairCursor(),
399
497
  highlightActiveLine(),
400
498
  highlightSelectionMatches(),
499
+ createSearchNavigationListener(),
500
+ this.vimModeCompartment.of(!readOnly && this.vimModeEnabled ? vim({ status: true }) : []),
401
501
  keymap.of([
402
502
  ...closeBracketsKeymap,
403
503
  ...defaultKeymap,
404
504
  ...historyKeymap,
405
- ...searchKeymap,
505
+ ...searchKeymap.map((binding) => (
506
+ binding.run === openSearchPanel
507
+ ? { ...binding, run: openSearchPanelPreservingViewport }
508
+ : binding
509
+ )),
406
510
  ...foldKeymap,
407
511
  indentWithTab,
408
512
  ]),
@@ -561,6 +665,30 @@ export class EditorViewAdapter {
561
665
  return this.lineWrappingEnabled;
562
666
  }
563
667
 
668
+ isVimModeEnabled() {
669
+ return this.vimModeEnabled;
670
+ }
671
+
672
+ setVimMode(enabled) {
673
+ const nextEnabled = Boolean(enabled);
674
+ if (nextEnabled === this.vimModeEnabled) {
675
+ return nextEnabled;
676
+ }
677
+
678
+ this.vimModeEnabled = nextEnabled;
679
+ if (!this.editorView || this.editorView.state.readOnly) {
680
+ return nextEnabled;
681
+ }
682
+
683
+ this.editorView.dispatch({
684
+ effects: this.vimModeCompartment.reconfigure(
685
+ nextEnabled ? vim({ status: true }) : [],
686
+ ),
687
+ });
688
+ this.editorView.focus();
689
+ return nextEnabled;
690
+ }
691
+
564
692
  setLineWrapping(enabled) {
565
693
  this.lineWrappingEnabled = Boolean(enabled);
566
694