collabmd 0.1.45 → 0.1.47

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 (356) hide show
  1. package/README.md +40 -19
  2. package/bin/collabmd.js +27 -1
  3. package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-BPH4FJkv.js +1 -0
  4. package/dist/client/assets/{abnfDiagram-N423BO3Z-CUMTFJhI.js → abnfDiagram-VCTEODGH-BNDLzJ7L.js} +1 -1
  5. package/dist/client/assets/architecture-7GRP2DOG-BMOPAkUC.js +1 -0
  6. package/dist/client/assets/{architectureDiagram-T3A2C74G-w25m71oV.js → architectureDiagram-5GKGNRK7-I39sYjHI.js} +1 -1
  7. package/dist/client/assets/base-BYODPHdS.js +1 -0
  8. package/dist/client/assets/blockDiagram-NRAW4CY4-C6DtJy5Y.js +129 -0
  9. package/dist/client/assets/browser-BKLM0ThC-IxjwuOIz-BBqQnUEb.js +1 -0
  10. package/dist/client/assets/browser-utils--H_DkyEF.js +1 -0
  11. package/dist/client/assets/c4Diagram-UCG6FXSJ-CtWtxS9_.js +38 -0
  12. package/dist/client/assets/channel-CaoKA_We.js +1 -0
  13. package/dist/client/assets/{chunk-U6XO7XAA-BFxjP5U3.js → chunk-2ZTRR5NV-C2qZJila.js} +1 -1
  14. package/dist/client/assets/{chunk-YOTPTUD7-D0vRKqYp.js → chunk-3Z5EZCMW-DMniOlPd.js} +1 -1
  15. package/dist/client/assets/{chunk-BIQX33UG-DZEsGuSn.js → chunk-5V3GS4D5-i-ZDOEnP.js} +1 -1
  16. package/dist/client/assets/{chunk-MOZMSUNE-XVucA-n6.js → chunk-6AZGARVD-BB6FqMl-.js} +1 -1
  17. package/dist/client/assets/{chunk-5JV3BV7I-Cy5pj3QY.js → chunk-6EIED4P4-DdUJkP0f.js} +1 -1
  18. package/dist/client/assets/{chunk-R7FJI6CG-Cnn3gE-Y.js → chunk-6K3QC6MW-CqVsVn-y.js} +1 -1
  19. package/dist/client/assets/{chunk-OSBZ3O6U-CrKZOUOr.js → chunk-6TQVIW2G-DKlLbNIP.js} +1 -1
  20. package/dist/client/assets/{chunk-JG7HCLWE-BYfiSaVt.js → chunk-747NJXEK-BFkZPlab.js} +1 -1
  21. package/dist/client/assets/{chunk-NSK5VX7P-TLH37VXI.js → chunk-75Z2AOVW-CdFSaPjz.js} +1 -1
  22. package/dist/client/assets/chunk-CKBBP62Z-DG26UiBv.js +2 -0
  23. package/dist/client/assets/{chunk-3NCLNEKW-Y2Jf_LI-.js → chunk-CLGD4ZFX-DQLgbQrv.js} +1 -1
  24. package/dist/client/assets/chunk-DSNCTWBM-8xkug7fQ.js +2 -0
  25. package/dist/client/assets/chunk-DU6HZSFF-COXDzJK1.js +127 -0
  26. package/dist/client/assets/{chunk-2GRJ4B5K-DhnlUv3F.js → chunk-F27PBJKO-CNHIFhjo.js} +1 -1
  27. package/dist/client/assets/{chunk-YGJVY4SA-CjSe7Bx7.js → chunk-FLYJHZSQ-Be7lKF17.js} +1 -1
  28. package/dist/client/assets/{chunk-KEIR6QF5-BICK3FdT.js → chunk-FOHPRMQF-DHwB1DNv.js} +2 -2
  29. package/dist/client/assets/{chunk-5RXB4S5H-BWrwFqMI.js → chunk-G27WJ6UU-D9-eg537.js} +2 -2
  30. package/dist/client/assets/chunk-GMAD6QVW-CbcBqPM5.js +72 -0
  31. package/dist/client/assets/{chunk-QBLGF6JB-CxAyS2mW.js → chunk-I5DQTOEV-Hm2LfiYI.js} +1 -1
  32. package/dist/client/assets/{chunk-5FCAYU7R-Dh5nylU8.js → chunk-ICYGCRZG-749o-pGJ.js} +1 -1
  33. package/dist/client/assets/{chunk-CQNSW5MT-B8S09OIh.js → chunk-IH6LHLGP-CC6I9F3i.js} +1 -1
  34. package/dist/client/assets/chunk-JQ64N6SF-DSFNH4bh.js +1 -0
  35. package/dist/client/assets/{chunk-CYSBUYHQ-C1Rf4art.js → chunk-KI3K4JFJ-Rma-0k_M.js} +1 -1
  36. package/dist/client/assets/{chunk-GF5L2VYU-DkJpO7ff.js → chunk-LCL6LL3I-DmDkVukh.js} +1 -1
  37. package/dist/client/assets/chunk-LNGE3PJU-DWpGwHrW.js +34 -0
  38. package/dist/client/assets/chunk-OBVCFTLP-BJR7u1jh.js +1 -0
  39. package/dist/client/assets/{chunk-5TONJI2A-DQOY3q1H.js → chunk-OUJLGHUK-D0j2JRSg.js} +1 -1
  40. package/dist/client/assets/chunk-P2QGCYS3-BRpqY32H.js +1 -0
  41. package/dist/client/assets/{chunk-KBJHAD2P-CB_3zWUf.js → chunk-POPQ4Y6H-Cb4SlVZF.js} +1 -1
  42. package/dist/client/assets/{chunk-4I5QYGJK-CCB2YcIH.js → chunk-PWAF6VOD-Cz8IwBB5.js} +1 -1
  43. package/dist/client/assets/chunk-R7TYR2AO-CTd0Yj98.js +10 -0
  44. package/dist/client/assets/chunk-RHFEMEQ7-CWY8cf4H.js +168 -0
  45. package/dist/client/assets/chunk-RM4UPSZO-CWRwxNWJ.js +41 -0
  46. package/dist/client/assets/{chunk-6Q2QTUOP-CPlWzBtc.js → chunk-SVP7TREG-CyHOmcac.js} +1 -1
  47. package/dist/client/assets/chunk-TEH6E4GO-DVayPj-q.js +62 -0
  48. package/dist/client/assets/{chunk-EMLP6XTP-RLD4nXpI.js → chunk-UY3FDG6J-CnmaAkG5.js} +1 -1
  49. package/dist/client/assets/{chunk-5HE753X5-Bw-yg4kO.js → chunk-XHIXRSVI-BFQ6B8BE.js} +1 -1
  50. package/dist/client/assets/classDiagram-DTDB5LWJ-7fEmn0zV.js +1 -0
  51. package/dist/client/assets/classDiagram-v2-JRS7N3AN-7fEmn0zV.js +1 -0
  52. package/dist/client/assets/comment-thread-store-CdcwrObc.js +1 -0
  53. package/dist/client/assets/comment-threads-9J6Em-_y.js +4 -0
  54. package/dist/client/assets/comment-ui-shared-DWkk9Vh3.js +7 -0
  55. package/dist/client/assets/cynefin-OW5HDTMX-7tP7QfM3.js +1 -0
  56. package/dist/client/assets/{cynefinDiagram-MW4NZA55-CJxXfJek.js → cynefinDiagram-5FMLGOSQ-CdTPi2yO.js} +1 -1
  57. package/dist/client/assets/dagre-3AP2YEHR-5gtJtd0O.js +4 -0
  58. package/dist/client/assets/dagre-PrKaheQc.js +1 -0
  59. package/dist/client/assets/{diagram-7IWD3JNH-B2d2Bm15.js → diagram-S7CK7UJ4-BudWz9WU.js} +4 -4
  60. package/dist/client/assets/{diagram-UB23O5K3-DJcKCwv9.js → diagram-UQ7AKVKN-3v4rbH-s.js} +1 -1
  61. package/dist/client/assets/{diagram-B4RE2ZJO-NP4UAwjR.js → diagram-VSXAHHWV-CQRVEZR7.js} +1 -1
  62. package/dist/client/assets/{diagram-Q27KOJAE-DylDuwor.js → diagram-VX7I27RA-C_-rj0sw.js} +1 -1
  63. package/dist/client/assets/{diagram-LBJQPF4R-D1FJQV8t.js → diagram-Z3DM3KII-B4yxavgj.js} +1 -1
  64. package/dist/client/assets/direct-engine-DdrQKpgq-CtKfQzCt.js +5 -0
  65. package/dist/client/assets/dist-73RPdcyU.js +1 -0
  66. package/dist/client/assets/dist-BJ1bJs33.js +1 -0
  67. package/dist/client/assets/{dist-CYZmbokb.js → dist-BK9QvQrc.js} +9 -9
  68. package/dist/client/assets/dist-BPHLJ09L.js +11 -0
  69. package/dist/client/assets/dist-B_yQaEv_.js +6 -0
  70. package/dist/client/assets/dist-BeajGMhO.js +1 -0
  71. package/dist/client/assets/{dist-JYEDRrPT.js → dist-Bw3VIE_v.js} +1 -1
  72. package/dist/client/assets/dist-ByrG2qBA.js +1 -0
  73. package/dist/client/assets/dist-C5NgpdG5.js +1 -0
  74. package/dist/client/assets/dist-CH-sQu3r.js +1 -0
  75. package/dist/client/assets/dist-CKS_-are.js +1 -0
  76. package/dist/client/assets/{dist-MvcV4190.js → dist-CdmKGRGr.js} +1 -1
  77. package/dist/client/assets/{dist-DGb27Dl6.js → dist-CietwTVS.js} +1 -1
  78. package/dist/client/assets/dist-D37Hyu3h.js +9 -0
  79. package/dist/client/assets/dist-D3fehAZ2.js +2 -0
  80. package/dist/client/assets/dist-D5wCC32z.js +1 -0
  81. package/dist/client/assets/{dist-DCzuX1AF.js → dist-DDUn5zTz.js} +1 -1
  82. package/dist/client/assets/dist-DGEbEfw4.js +1 -0
  83. package/dist/client/assets/dist-DVy5woUy.js +1 -0
  84. package/dist/client/assets/{dist-3bgCp7iY.js → dist-DZAAWTG-.js} +1 -1
  85. package/dist/client/assets/{dist-Drp8uu5J.js → dist-Dir5dDKZ.js} +1 -1
  86. package/dist/client/assets/dist-Dl_tpDSr.js +1 -0
  87. package/dist/client/assets/dist-DuH3ZrHo.js +1 -0
  88. package/dist/client/assets/dist-DutPuFmC.js +1 -0
  89. package/dist/client/assets/{dist-D2AQ-76g.js → dist-LQ4kDCKz.js} +1 -1
  90. package/dist/client/assets/{dist-C11hHqv_.js → dist-RKBTaQMb.js} +12 -12
  91. package/dist/client/assets/dist-Tw-Fxpeb.js +1 -0
  92. package/dist/client/assets/dist-eBx8q6Sy.js +1 -0
  93. package/dist/client/assets/{dist-DK5Gs2wa.js → dist-mj2Ht75J.js} +1 -1
  94. package/dist/client/assets/{dist-CnL7Y2sM.js → dist-netZOQtD.js} +1 -1
  95. package/dist/client/assets/{drawio-editor-ClE8jpoG.js → drawio-editor-BjrxTe3l.js} +1 -1
  96. package/dist/client/assets/drawioEditor-DUVZjSSr.js +2 -0
  97. package/dist/client/assets/{ebnfDiagram-BXEA7PRR-nw0wmA6y.js → ebnfDiagram-PWID7BFC-OXa8VEp1.js} +1 -1
  98. package/dist/client/assets/editor-session-DfZuRgB_.js +54 -0
  99. package/dist/client/assets/embedded-editor-base-DvMynhKu.css +1 -0
  100. package/dist/client/assets/embedpdf-BmE13ZZ0.js +1 -0
  101. package/dist/client/assets/embedpdf-DA4bA119-BPqZxwjc.js +2363 -0
  102. package/dist/client/assets/{en-7NFCYGKX-BrgYZAEe.js → en-SJ2YIMFF-CJBL3nkq.js} +1 -1
  103. package/dist/client/assets/erDiagram-SSCWMZ5O-C-iy6VkX.js +99 -0
  104. package/dist/client/assets/es-CAsWR6hG.js +22 -0
  105. package/dist/client/assets/eventmodeling-NTZA5JFV-BR2FzK4R.js +1 -0
  106. package/dist/client/assets/excalidraw-editor-BlcN6umJ.css +1 -0
  107. package/dist/client/assets/excalidraw-editor-D7pnUCLH.js +9 -0
  108. package/dist/client/assets/excalidraw-scene-BOben2gr.js +1 -0
  109. package/dist/client/assets/excalidrawEditor-B0jOK3lv.js +2 -0
  110. package/dist/client/assets/export-page-Bs3xE1fg.js +57 -0
  111. package/dist/client/assets/exportDocument-Dg9MjkIk.js +2 -0
  112. package/dist/client/assets/exportDocument-PnYU3v1s.css +1 -0
  113. package/dist/client/assets/file-icon-svg-CQAjlWjK.js +1 -0
  114. package/dist/client/assets/file-kind-BbqgGzpc.js +1 -0
  115. package/dist/client/assets/flowDiagram-A5DVABFB-CiW1eN8C.js +1 -0
  116. package/dist/client/assets/{ganttDiagram-PKOTCBZU-BnM97BCT.js → ganttDiagram-EL5Y4UJY-DtKHAbez.js} +1 -1
  117. package/dist/client/assets/gitGraph-4MIJSDKK-DYlXv_yJ.js +1 -0
  118. package/dist/client/assets/{gitGraphDiagram-DS77QQ5N-BQpQhjuN.js → gitGraphDiagram-WWUBYQGX-oue8fQ5o.js} +1 -1
  119. package/dist/client/assets/github-dark.min-BV82VuKq.css +1 -0
  120. package/dist/client/assets/github.min-v3dd8CNH.css +1 -0
  121. package/dist/client/assets/image-ZVKINSPA-0zXSnE7M.js +1 -0
  122. package/dist/client/assets/index-BIpgxyGZ.css +1 -0
  123. package/dist/client/assets/index-Z5sI0Bj-.js +2 -0
  124. package/dist/client/assets/info-A6RAGUB7-BlU-8_xu.js +1 -0
  125. package/dist/client/assets/{infoDiagram-6WML65LV-DYB8HCDQ.js → infoDiagram-RXCK75RN-BMzo6KIn.js} +1 -1
  126. package/dist/client/assets/{ishikawaDiagram-WSZJBQD7-Coz_P3t6.js → ishikawaDiagram-5VMMS53U-BIM3h287.js} +1 -1
  127. package/dist/client/assets/{journeyDiagram-NVQOT4AX-B6edChce.js → journeyDiagram-EYS64GPL-mF6Lh8O-.js} +1 -1
  128. package/dist/client/assets/{kanban-definition-27J2QSJJ-BFd9-cLc.js → kanban-definition-3QL26DDD-DLN2NyoR.js} +2 -2
  129. package/dist/client/assets/{line-B_S_S8kV.js → line-BfTWJmgf.js} +1 -1
  130. package/dist/client/assets/main-Dwnj9Gaq.js +903 -0
  131. package/dist/client/assets/{mermaid-parser.core-CUVdebAV.js → mermaid-parser.core-Bl0efOMt.js} +2 -2
  132. package/dist/client/assets/mermaid.core-CLZBlpIW.js +11 -0
  133. package/dist/client/assets/{mindmap-definition-FAOFIHXS-C9PQ38Al.js → mindmap-definition-FBJOCRG2-CRyHaHTk.js} +1 -1
  134. package/dist/client/assets/packet-AYTQ26CC-BSJEH9Lb.js +1 -0
  135. package/dist/client/assets/pdfium-RAgkpwfK.wasm +0 -0
  136. package/dist/client/assets/{pegDiagram-VL7TDLO6-BMZUrSaU.js → pegDiagram-XKGWAZYB-B31fKPR2.js} +1 -1
  137. package/dist/client/assets/pie-WAS4IAKB-qxv01Y8W.js +1 -0
  138. package/dist/client/assets/{pieDiagram-7S7Q4E2Y-h8kbCL9o.js → pieDiagram-E7YTZNPT-B5OZPb88.js} +1 -1
  139. package/dist/client/assets/preview-render-compiler-BApvlLLo.js +35 -0
  140. package/dist/client/assets/preview-render-compiler-BQ_3h9vD.js +1 -0
  141. package/dist/client/assets/preview-render-profile-CE-5rVTC.js +1 -0
  142. package/dist/client/assets/preview-render-worker-BmN79bVc.js +60 -0
  143. package/dist/client/assets/prod-BiYPMR1i.js +177 -0
  144. package/dist/client/assets/prod-VxdGZ5Ly.js +1 -0
  145. package/dist/client/assets/{quadrantDiagram-CIZ2JOQS-CkElyfMi.js → quadrantDiagram-AXDQQJYC-Drrw14YH.js} +1 -1
  146. package/dist/client/assets/quick-switcher-controller-ButWPmCI.js +12 -0
  147. package/dist/client/assets/radar-RG4KPBEZ-B6h6pWOs.js +1 -0
  148. package/dist/client/assets/railroad-74A4TZTK-CGtTWdGc.js +1 -0
  149. package/dist/client/assets/railroad-abnf-HS5TGJTU-Cq7y22IA.js +1 -0
  150. package/dist/client/assets/railroad-ebnf-LZEXJU2U-V0aQVv9m.js +1 -0
  151. package/dist/client/assets/railroad-peg-WCYAUIDC-BQiIsX_p.js +1 -0
  152. package/dist/client/assets/{railroadDiagram-AXF67PYL-c75gnikD.js → railroadDiagram-O6MQD6OU-BPifP0MB.js} +1 -1
  153. package/dist/client/assets/{requirementDiagram-LRYGKXZP-mNxKLuHS.js → requirementDiagram-EFPCY7ZU-CRZG6mRB.js} +2 -2
  154. package/dist/client/assets/runtime-config-BLrEKVO1.js +1 -0
  155. package/dist/client/assets/{sankeyDiagram-W5VNT64P-0OhplksI.js → sankeyDiagram-P5KCCOFB-DvarEJpo.js} +1 -1
  156. package/dist/client/assets/{sequenceDiagram-SI44F4Z6-BJ_yAocC.js → sequenceDiagram-WJ2MYXX4-B4oWlb-w.js} +1 -1
  157. package/dist/client/assets/{stateDiagram-OKZ733FA-Cc83O86s.js → stateDiagram-HBIQ2CUA-qbHIIqRZ.js} +1 -1
  158. package/dist/client/assets/stateDiagram-v2-4QOOHH4V-B--gjrit.js +1 -0
  159. package/dist/client/assets/swimlanes-XN3QIQJK-xB6CtUGj.js +1 -0
  160. package/dist/client/assets/swimlanesDiagram-VK2B7HYN-DO8XNXEg.js +8 -0
  161. package/dist/client/assets/{timeline-definition-Z64GVDOM-BWDAGTHz.js → timeline-definition-24CTP7MA-Lj_yTNlY.js} +1 -1
  162. package/dist/client/assets/treeView-Q6P3EWNA-BTTYcvrF.js +1 -0
  163. package/dist/client/assets/treemap-WGGIJYW6-Cg7tyfH8.js +1 -0
  164. package/dist/client/assets/vault-api-client-_LpV1aIn.js +6 -0
  165. package/dist/client/assets/{vennDiagram-T6HMQDX7-LOSsnlMB.js → vennDiagram-4TSXK5OY-BekRp5Y0.js} +1 -1
  166. package/dist/client/assets/wardley-WFR3VGLG-C6N1rULY.js +1 -0
  167. package/dist/client/assets/{wardleyDiagram-T6FBY63Y-BxII3foY.js → wardleyDiagram-VM6X3IG4-kdnV_Hdm.js} +1 -1
  168. package/dist/client/assets/worker-engine-m0rlHn8R-S8mpn_O0.js +17369 -0
  169. package/dist/client/assets/{xychartDiagram-ELKLHX3M-B-pTEjju.js → xychartDiagram-S5SC5T6Z-DufGwNSB.js} +3 -3
  170. package/dist/client/drawio-editor.html +1 -1
  171. package/dist/client/excalidraw-editor.html +1 -1
  172. package/dist/client/export-document.html +3 -3
  173. package/dist/client/index.html +30 -8
  174. package/docker-compose.yml +42 -0
  175. package/package.json +32 -18
  176. package/scripts/local-structurizr-compose.mjs +101 -0
  177. package/src/client/app/index.html +27 -5
  178. package/src/client/application/app-shell/comments-feature.js +21 -13
  179. package/src/client/application/app-shell/git-feature.js +14 -2
  180. package/src/client/application/app-shell/presence-feature.js +4 -3
  181. package/src/client/application/app-shell/ui-feature-shell.js +109 -2
  182. package/src/client/application/app-shell/ui-feature-toolbar.js +2 -52
  183. package/src/client/application/app-shell/workspace-feature.js +5 -1
  184. package/src/client/application/app-shell-elements.js +3 -0
  185. package/src/client/application/pdf-preview-controller.js +75 -0
  186. package/src/client/application/preview-render-compiler.js +3 -0
  187. package/src/client/application/quick-switcher-loader.js +1 -1
  188. package/src/client/application/structurizr-preview-controller.js +395 -0
  189. package/src/client/application/workspace-coordinator.js +25 -3
  190. package/src/client/application/workspace-preview-controller.js +88 -2
  191. package/src/client/assets/vendor/highlight/github-dark.min.css +2 -10
  192. package/src/client/assets/vendor/highlight/github.min.css +2 -10
  193. package/src/client/bootstrap/collabmd-app-shell.js +39 -6
  194. package/src/client/browser-utils.js +44 -0
  195. package/src/client/domain/runtime-paths.js +26 -5
  196. package/src/client/domain/structurizr-language.js +99 -0
  197. package/src/client/domain/vault-paths.js +48 -0
  198. package/src/client/excalidraw-editor.js +844 -51
  199. package/src/client/export/export-host.js +1 -1
  200. package/src/client/export/export-page.js +6 -2
  201. package/src/client/export/export-pipeline.js +60 -1
  202. package/src/client/infrastructure/browser-preferences-port.js +10 -0
  203. package/src/client/infrastructure/comment-thread-store.js +34 -13
  204. package/src/client/infrastructure/editor-session.js +10 -0
  205. package/src/client/infrastructure/editor-view-adapter.js +46 -2
  206. package/src/client/infrastructure/excalidraw-room-client.js +103 -14
  207. package/src/client/infrastructure/git-api-client.js +5 -0
  208. package/src/client/infrastructure/vault-api-client.js +26 -0
  209. package/src/client/presentation/comment-ui/comment-ui-render.js +4 -0
  210. package/src/client/presentation/comment-ui/comment-ui-shared.js +4 -0
  211. package/src/client/presentation/comment-ui-controller.js +2 -0
  212. package/src/client/presentation/drawio-embed-controller.js +9 -102
  213. package/src/client/presentation/drawio-viewer.js +118 -0
  214. package/src/client/presentation/excalidraw-embed-controller.js +81 -8
  215. package/src/client/presentation/file-action-controller.js +102 -0
  216. package/src/client/presentation/file-explorer-controller.js +0 -5
  217. package/src/client/presentation/file-explorer-view.js +51 -73
  218. package/src/client/presentation/file-icon-svg.js +40 -0
  219. package/src/client/presentation/file-tree-state.js +6 -3
  220. package/src/client/presentation/git-diff-view-controller.js +731 -3
  221. package/src/client/presentation/git-panel-controller.js +2 -2
  222. package/src/client/presentation/outline-controller.js +86 -0
  223. package/src/client/presentation/quick-switcher-controller.js +73 -22
  224. package/src/client/presentation/quick-switcher-text-search.js +1 -1
  225. package/src/client/styles/components/navigation.css +11 -1
  226. package/src/client/styles/features/diagram-preview.css +76 -0
  227. package/src/client/styles/features/diff-special.css +309 -0
  228. package/src/client/styles/features/preview-markdown.css +25 -0
  229. package/src/client/styles/features/quick-switcher.css +8 -0
  230. package/src/client/styles/foundation/themes.css +7 -0
  231. package/src/client/styles/layout/shell.css +40 -1
  232. package/src/client/styles/layout/sidebar.css +42 -4
  233. package/src/client/styles/layout/view-modes.css +1 -0
  234. package/src/client/styles/overrides/highlightjs.css +20 -4
  235. package/src/client/styles/style.css +1 -0
  236. package/src/client/styles/surfaces/excalidraw-editor.css +294 -0
  237. package/src/domain/comment-threads.js +63 -5
  238. package/src/domain/excalidraw-room-codec.js +6 -4
  239. package/src/domain/file-kind.js +35 -3
  240. package/src/server/config/env.js +79 -0
  241. package/src/server/create-app-server.js +12 -0
  242. package/src/server/domain/bases/base-definition.js +1 -1
  243. package/src/server/domain/bases/base-expression-runtime.js +1 -4
  244. package/src/server/domain/bases/base-index-snapshot-store.js +1 -4
  245. package/src/server/domain/bases/base-transform.js +1 -4
  246. package/src/server/domain/collaboration/collaboration-room.js +55 -21
  247. package/src/server/domain/comment-overview.js +15 -5
  248. package/src/server/domain/ripgrep-search-service.js +4 -0
  249. package/src/server/infrastructure/git/diff-service.js +5 -0
  250. package/src/server/infrastructure/git/git-service.js +44 -11
  251. package/src/server/infrastructure/git/history-service.js +54 -0
  252. package/src/server/infrastructure/git/image-mime.js +14 -0
  253. package/src/server/infrastructure/git/untracked-files.js +28 -1
  254. package/src/server/infrastructure/http/create-git-api-command-handler.js +3 -1
  255. package/src/server/infrastructure/http/create-git-api-query-handler.js +31 -1
  256. package/src/server/infrastructure/http/create-request-handler.js +16 -23
  257. package/src/server/infrastructure/http/create-static-handler.js +5 -2
  258. package/src/server/infrastructure/http/create-structurizr-api-handler.js +147 -0
  259. package/src/server/infrastructure/http/create-vault-api-command-handler.js +66 -4
  260. package/src/server/infrastructure/http/create-vault-api-query-handler.js +51 -3
  261. package/src/server/infrastructure/http/http-request-helpers.js +22 -0
  262. package/src/server/infrastructure/persistence/path-utils.js +1 -1
  263. package/src/server/infrastructure/persistence/vault-file-store.js +3 -0
  264. package/src/server/infrastructure/structurizr/structurizr-embed.css +43 -0
  265. package/src/server/infrastructure/structurizr/structurizr-workspace-service.js +400 -0
  266. package/src/server/infrastructure/websocket/attach-collaboration-gateway.js +4 -22
  267. package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-CbSISC1w.js +0 -1
  268. package/dist/client/assets/architecture-TIHT7OUA-CsHfPErM.js +0 -1
  269. package/dist/client/assets/base-CxPEiHuy.js +0 -1
  270. package/dist/client/assets/blockDiagram-VBNYF7ZC-CipJt9Jl.js +0 -132
  271. package/dist/client/assets/browser-utils-CKtI_rZl.js +0 -1
  272. package/dist/client/assets/c4Diagram-5PPSVZJV-B5ktVx6g.js +0 -10
  273. package/dist/client/assets/channel-4Jkmq93I.js +0 -1
  274. package/dist/client/assets/chunk-7BUUIJ7U-Bb538aSH.js +0 -1
  275. package/dist/client/assets/chunk-7Z6QIM7H-BP9Pdsm9.js +0 -10
  276. package/dist/client/assets/chunk-ERSWUTIN-ME9RVNel.js +0 -41
  277. package/dist/client/assets/chunk-I66GZJ75-CVFiiffQ.js +0 -127
  278. package/dist/client/assets/chunk-J7OUQ5F2-4CEb7pvH.js +0 -2
  279. package/dist/client/assets/chunk-JQJVKLGR-cx8X_24v.js +0 -156
  280. package/dist/client/assets/chunk-QR6OTTB3-CDAA_5Ib.js +0 -62
  281. package/dist/client/assets/chunk-RYQCIY6F-D-a0zH8K.js +0 -1
  282. package/dist/client/assets/chunk-UBXNYLIW-Bhk-PbFK.js +0 -1
  283. package/dist/client/assets/chunk-W5SLKNZC-BdNSCxZ9.js +0 -1
  284. package/dist/client/assets/chunk-WRU74C26-h6_XkD3_.js +0 -70
  285. package/dist/client/assets/chunk-ZIRB5QZD-O39ZBUk3.js +0 -32
  286. package/dist/client/assets/classDiagram-JCYQIIEL-L7lsyW_w.js +0 -1
  287. package/dist/client/assets/classDiagram-v2-OCEON4UE-L7lsyW_w.js +0 -1
  288. package/dist/client/assets/comment-threads-DDegmaFO.js +0 -4
  289. package/dist/client/assets/cynefin-VYW2F7L2-Ds2Fw2Y0.js +0 -1
  290. package/dist/client/assets/dagre-Buvkdvvj.js +0 -1
  291. package/dist/client/assets/dagre-VZM6K2ZE-L1xArOGs.js +0 -4
  292. package/dist/client/assets/dist-BH87JoHx.js +0 -11
  293. package/dist/client/assets/dist-BIkPRZO7.js +0 -6
  294. package/dist/client/assets/dist-BqCsFpx4.js +0 -1
  295. package/dist/client/assets/dist-C2JHCt8U.js +0 -1
  296. package/dist/client/assets/dist-CZjTtmTu.js +0 -1
  297. package/dist/client/assets/dist-Cig4Ov_X.js +0 -1
  298. package/dist/client/assets/dist-CqQEC8gR.js +0 -1
  299. package/dist/client/assets/dist-D4Rzqod1.js +0 -1
  300. package/dist/client/assets/dist-D5sn-mwk.js +0 -9
  301. package/dist/client/assets/dist-D7G-n87v.js +0 -1
  302. package/dist/client/assets/dist-DRqOrNgN.js +0 -1
  303. package/dist/client/assets/dist-DetkMS_4.js +0 -1
  304. package/dist/client/assets/dist-DhLvC8DS.js +0 -2
  305. package/dist/client/assets/dist-Dp6FVkQ1.js +0 -1
  306. package/dist/client/assets/dist-DtbjNB28.js +0 -1
  307. package/dist/client/assets/dist-DyrYcvpo2.js +0 -1
  308. package/dist/client/assets/dist-GrygD2zE.js +0 -1
  309. package/dist/client/assets/dist-HmEpWUow.js +0 -1
  310. package/dist/client/assets/dist-RTa81Km7.js +0 -1
  311. package/dist/client/assets/drawioEditor-CXaTM9Ds.js +0 -2
  312. package/dist/client/assets/editor-session-GTwZS6Df.js +0 -16
  313. package/dist/client/assets/embedded-editor-base-CiNEc8vy.css +0 -1
  314. package/dist/client/assets/erDiagram-JOGREHBK-cYUpEKpf.js +0 -85
  315. package/dist/client/assets/eventmodeling-45OFAUF4-CLEiVx5s.js +0 -1
  316. package/dist/client/assets/excalidraw-editor-BZVM81BO.js +0 -9
  317. package/dist/client/assets/excalidraw-editor-CyGwOhLz.css +0 -1
  318. package/dist/client/assets/excalidraw-scene-C8n9CHsK.js +0 -176
  319. package/dist/client/assets/excalidrawEditor-DxJanCze.js +0 -2
  320. package/dist/client/assets/export-page-CUfws2T-.js +0 -39
  321. package/dist/client/assets/exportDocument-BCuxqXDY.js +0 -2
  322. package/dist/client/assets/exportDocument-DV-OX74E.css +0 -1
  323. package/dist/client/assets/file-kind-BKPOIRPE.js +0 -1
  324. package/dist/client/assets/flowDiagram-UKHOOZJN-D0ypP0Ez.js +0 -1
  325. package/dist/client/assets/gitGraph-TEB2WS4Q-CZFBqwW-.js +0 -1
  326. package/dist/client/assets/github-dark.min-Z5exjX_n.css +0 -1
  327. package/dist/client/assets/github.min-D2BCvnWf.css +0 -1
  328. package/dist/client/assets/image-QXRYHUNP-lfTHE3bb.js +0 -1
  329. package/dist/client/assets/index-C3YnUgMD.css +0 -1
  330. package/dist/client/assets/index-D2wXraJ1.js +0 -2
  331. package/dist/client/assets/info-DKCQHKI2--1InqQeT.js +0 -1
  332. package/dist/client/assets/main-DtiGz7uI.js +0 -795
  333. package/dist/client/assets/map-BaFkSB1l.js +0 -1
  334. package/dist/client/assets/mermaid.core-DN-lF51U.js +0 -11
  335. package/dist/client/assets/packet-7NZHBO7P-BGMXB0XL.js +0 -1
  336. package/dist/client/assets/pie-RZYD4A2V-DgYg3tCt.js +0 -1
  337. package/dist/client/assets/preview-render-compiler-BSaQO_p3.js +0 -35
  338. package/dist/client/assets/preview-render-compiler-csGM5ZFz.js +0 -1
  339. package/dist/client/assets/preview-render-profile-BZzGSg6K.js +0 -22
  340. package/dist/client/assets/preview-render-worker-B42LWvrD.js +0 -60
  341. package/dist/client/assets/quick-switcher-controller-shW_6vPY.js +0 -12
  342. package/dist/client/assets/radar-I7S5WNFK-Ag-zp_Rq.js +0 -1
  343. package/dist/client/assets/railroad-3IZDKUUU-aibJDIDV.js +0 -1
  344. package/dist/client/assets/railroad-abnf-AHOZXSZD-BBMEuvIl.js +0 -1
  345. package/dist/client/assets/railroad-ebnf-EBAXGLYW-Be1nseej.js +0 -1
  346. package/dist/client/assets/railroad-peg-LSFZ7HO6-DIvD-Eyb.js +0 -1
  347. package/dist/client/assets/room-CQLoqtMo.js +0 -1
  348. package/dist/client/assets/runtime-config-DSGmGE9V.js +0 -1
  349. package/dist/client/assets/stateDiagram-v2-UEYNNEHI-B1-BufUk.js +0 -1
  350. package/dist/client/assets/swimlanes-SLNWSIFB-oVTWEJIj.js +0 -2
  351. package/dist/client/assets/swimlanesDiagram-ULZ7WXOC-C5OTVwEu.js +0 -8
  352. package/dist/client/assets/treeView-QDETBFTQ-uHZ-3Fz8.js +0 -1
  353. package/dist/client/assets/treemap-6X3UGDF4-BnUKyy3l.js +0 -1
  354. package/dist/client/assets/vault-api-client-C6bcGwy-.js +0 -5
  355. package/dist/client/assets/wardley-OPB4EBWU-Da7TLC2F.js +0 -1
  356. package/src/server/domain/collaboration/room-persistence-controller.js +0 -80
@@ -6,7 +6,9 @@ import {
6
6
  reconcileElements,
7
7
  restoreAppState,
8
8
  restoreElements,
9
+ sceneCoordsToViewportCoords,
9
10
  } from '@excalidraw/excalidraw';
11
+ import { getElementBounds } from '@excalidraw/element';
10
12
  import '@excalidraw/excalidraw/index.css';
11
13
 
12
14
  import {
@@ -17,6 +19,9 @@ import {
17
19
  resolveLocalAwarenessUser,
18
20
  } from './domain/excalidraw-collaboration.js';
19
21
  import './styles/surfaces/embedded-editor-base.css';
22
+ import './styles/features/comment-markdown.css';
23
+ import './styles/features/comment-overview.css';
24
+ import './styles/features/comments-drawer.css';
20
25
  import './styles/surfaces/excalidraw-editor.css';
21
26
  import {
22
27
  applySceneUpdateWithFiles,
@@ -27,6 +32,11 @@ import {
27
32
  parseSceneJson,
28
33
  sceneToInitialData,
29
34
  } from './domain/excalidraw-scene.js';
35
+ import {
36
+ createCommentOverviewThread,
37
+ formatAnchorLabel,
38
+ getLatestMessage,
39
+ } from './presentation/comment-ui/comment-ui-shared.js';
30
40
  import {
31
41
  buildReconciledExcalidrawSceneUpdate,
32
42
  } from './domain/excalidraw-scene-reconcile.js';
@@ -76,17 +86,39 @@ let apiStateCleanupCallbacks = [];
76
86
  let collaboratorRenderFrame = 0;
77
87
  let queuedCollaborators = null;
78
88
  let initialViewportFitPending = true;
89
+ let viewportFitGeneration = 0;
79
90
  let previewViewportFitTimerId = 0;
91
+ let previewViewportFitRetryTimerId = 0;
80
92
  let roomClient = null;
81
93
  let roomClientGeneration = 0;
82
94
  let reactRoot = null;
83
95
  let editorRenderKey = 0;
84
96
  let skipRoomDisconnectOnUnmount = false;
85
97
  let roomConnectionState = EXCALIDRAW_ROOM_CONNECTION_STATE.CONNECTING;
98
+ let diagramCommentThreads = [];
99
+ let pendingDiagramCommentFocus = null;
100
+ let diagramCommentFocusRequestId = 0;
86
101
  let parkRequestedWhileBlocked = false;
87
102
  const pendingDisconnectRequestIds = new Set();
88
103
  const reportedFileConflictSignatures = new Set();
89
104
 
105
+ function getMountedExcalidrawAPI() {
106
+ return excalidrawAPI && !excalidrawAPI.isDestroyed ? excalidrawAPI : null;
107
+ }
108
+
109
+ function requestDiagramCommentFocus(threadId) {
110
+ const normalizedThreadId = String(threadId ?? '').trim();
111
+ if (!normalizedThreadId) {
112
+ return;
113
+ }
114
+
115
+ pendingDiagramCommentFocus = {
116
+ requestId: ++diagramCommentFocusRequestId,
117
+ threadId: normalizedThreadId,
118
+ };
119
+ renderExcalidrawApp();
120
+ }
121
+
90
122
  if (diagnostics.enabled) {
91
123
  window.__COLLABMD_EXCALIDRAW_DEBUG__ = diagnostics;
92
124
  }
@@ -120,6 +152,213 @@ function getAuthorityBannerText() {
120
152
  return '';
121
153
  }
122
154
 
155
+ function getSelectedDiagramElement() {
156
+ const api = getMountedExcalidrawAPI();
157
+ const selectedElementIds = api?.getAppState?.()?.selectedElementIds || {};
158
+ const selectedElements = api?.getSceneElementsIncludingDeleted?.()
159
+ ?.filter((element) => selectedElementIds[element.id] && !element.isDeleted) ?? [];
160
+ return selectedElements.length === 1 ? selectedElements[0] : null;
161
+ }
162
+
163
+ function normalizeDiagramLabelText(value, maxLength = 72) {
164
+ const text = typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
165
+ return text.length > maxLength ? `${text.slice(0, maxLength - 1)}…` : text;
166
+ }
167
+
168
+ function getDiagramElementContent(element, api = getMountedExcalidrawAPI()) {
169
+ const directText = normalizeDiagramLabelText(element?.text);
170
+ if (directText) {
171
+ return directText;
172
+ }
173
+
174
+ const sceneElements = api?.getSceneElementsIncludingDeleted?.() || [];
175
+ const boundElementIds = new Set(
176
+ Array.isArray(element?.boundElements)
177
+ ? element.boundElements.map((boundElement) => boundElement?.id).filter(Boolean)
178
+ : [],
179
+ );
180
+ if (boundElementIds.size === 0) {
181
+ return '';
182
+ }
183
+
184
+ return normalizeDiagramLabelText(
185
+ sceneElements
186
+ .filter((sceneElement) => boundElementIds.has(sceneElement.id) && !sceneElement.isDeleted)
187
+ .map((sceneElement) => sceneElement.text)
188
+ .filter((text) => typeof text === 'string' && text.trim())
189
+ .join(' '),
190
+ );
191
+ }
192
+
193
+ function formatDiagramAnchorLabel(type, content = '') {
194
+ const normalizedType = typeof type === 'string' && type.trim() ? type.trim() : 'element';
195
+ const typeLabel = normalizedType === 'element'
196
+ ? 'Diagram element'
197
+ : `${normalizedType.charAt(0).toUpperCase()}${normalizedType.slice(1)} element`;
198
+ const normalizedContent = normalizeDiagramLabelText(content);
199
+ return normalizedContent ? `${typeLabel} · ${normalizedContent}` : typeLabel;
200
+ }
201
+
202
+ function getDiagramAnchorLabel(thread) {
203
+ const snapshot = thread?.anchorSnapshot;
204
+ const element = getMountedExcalidrawAPI()?.getSceneElementsIncludingDeleted?.()
205
+ ?.find((candidate) => candidate.id === thread?.elementId && !candidate.isDeleted);
206
+ const elementContent = normalizeDiagramLabelText(snapshot?.text) || getDiagramElementContent(element);
207
+ const firstMessageContent = thread?.messages?.[0]?.body;
208
+ return formatDiagramAnchorLabel(snapshot?.type || element?.type, elementContent || firstMessageContent);
209
+ }
210
+
211
+ const diagramCommentTimeFormatter = new Intl.DateTimeFormat(undefined, {
212
+ day: 'numeric',
213
+ hour: 'numeric',
214
+ minute: '2-digit',
215
+ month: 'short',
216
+ });
217
+
218
+ function formatDiagramCommentTimestamp(value) {
219
+ if (!Number.isFinite(value)) {
220
+ return '';
221
+ }
222
+
223
+ try {
224
+ return diagramCommentTimeFormatter.format(new Date(value));
225
+ } catch {
226
+ return '';
227
+ }
228
+ }
229
+
230
+ function renderDiagramCommentOverviewThread(thread, activeThreadId, onOpen) {
231
+ const latestMessage = getLatestMessage(thread?.messages ?? []);
232
+ const messageCount = Array.isArray(thread?.messages) ? thread.messages.length : 0;
233
+ const button = createCommentOverviewThread({
234
+ authorName: latestMessage?.userName || thread?.createdByName || 'Anonymous',
235
+ buttonClassName: 'comment-overview-thread comments-drawer-item',
236
+ footerClassName: 'comment-overview-thread-footer comments-drawer-item-footer',
237
+ headerClassName: 'comment-overview-thread-header comments-drawer-item-header',
238
+ lineClassName: 'comment-overview-thread-line comments-drawer-item-title',
239
+ lineLabel: formatAnchorLabel(thread),
240
+ messageCount,
241
+ previewBody: latestMessage?.body || '',
242
+ previewClassName: 'comment-markdown comment-overview-thread-preview comments-drawer-item-preview',
243
+ quote: thread?.anchorQuote || thread?.anchorSnapshot?.text || 'Diagram element',
244
+ quoteClassName: 'comment-overview-thread-quote comments-drawer-item-quote',
245
+ timestamp: formatDiagramCommentTimestamp(latestMessage?.createdAt),
246
+ });
247
+ button.classList.toggle('is-active', thread.id === activeThreadId);
248
+
249
+ return React.createElement('button', {
250
+ className: button.className,
251
+ dangerouslySetInnerHTML: { __html: button.innerHTML },
252
+ key: thread.id,
253
+ onClick: () => onOpen(thread.id),
254
+ type: 'button',
255
+ });
256
+ }
257
+
258
+ function DiagramCommentIcon({ add = false }) {
259
+ const paths = [React.createElement('path', {
260
+ d: 'M3 4.75A1.75 1.75 0 0 1 4.75 3h6.5A1.75 1.75 0 0 1 13 4.75v4.5A1.75 1.75 0 0 1 11.25 11H8.9L6.5 13v-2H4.75A1.75 1.75 0 0 1 3 9.25v-4.5Z',
261
+ fill: 'none',
262
+ key: 'bubble',
263
+ stroke: 'currentColor',
264
+ strokeLinejoin: 'round',
265
+ strokeWidth: '1.35',
266
+ })];
267
+ if (add) {
268
+ paths.push(React.createElement('path', {
269
+ d: 'M8 5.5v3.5M6.25 7.25h3.5',
270
+ fill: 'none',
271
+ key: 'plus',
272
+ stroke: 'currentColor',
273
+ strokeLinecap: 'round',
274
+ strokeWidth: '1.35',
275
+ }));
276
+ }
277
+
278
+ return React.createElement('svg', {
279
+ 'aria-hidden': 'true',
280
+ className: 'diagram-comment-icon',
281
+ fill: 'none',
282
+ focusable: 'false',
283
+ viewBox: '0 0 16 16',
284
+ }, paths);
285
+ }
286
+
287
+ function getDiagramCommentMarkerPosition(thread, api = getMountedExcalidrawAPI()) {
288
+ if (!api || thread?.anchorKind !== 'diagram-element') {
289
+ return null;
290
+ }
291
+
292
+ let scenePoint = thread.anchorPoint;
293
+ const element = api.getSceneElementsIncludingDeleted?.()
294
+ ?.find((candidate) => candidate.id === thread.elementId && !candidate.isDeleted);
295
+ if (element) {
296
+ try {
297
+ const bounds = getElementBounds(element, api.getSceneElementsMapIncludingDeleted?.());
298
+ scenePoint = {
299
+ x: bounds[2],
300
+ y: bounds[1],
301
+ };
302
+ } catch {
303
+ scenePoint = thread.anchorPoint;
304
+ }
305
+ }
306
+
307
+ const x = Number(scenePoint?.x);
308
+ const y = Number(scenePoint?.y);
309
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
310
+ return null;
311
+ }
312
+
313
+ const appState = api.getAppState?.();
314
+ if (!appState?.zoom || !Number.isFinite(appState.scrollX) || !Number.isFinite(appState.scrollY)) {
315
+ return null;
316
+ }
317
+
318
+ return sceneCoordsToViewportCoords(
319
+ { sceneX: x, sceneY: y },
320
+ {
321
+ offsetLeft: Number.isFinite(appState.offsetLeft) ? appState.offsetLeft : 0,
322
+ offsetTop: Number.isFinite(appState.offsetTop) ? appState.offsetTop : 0,
323
+ scrollX: appState.scrollX,
324
+ scrollY: appState.scrollY,
325
+ zoom: appState.zoom,
326
+ },
327
+ );
328
+ }
329
+
330
+ function focusDiagramCommentThread(thread) {
331
+ const api = getMountedExcalidrawAPI();
332
+ const element = api?.getSceneElementsIncludingDeleted?.()
333
+ ?.find((candidate) => candidate.id === thread?.elementId && !candidate.isDeleted);
334
+ if (!api || !element) {
335
+ return;
336
+ }
337
+
338
+ api.updateScene({
339
+ appState: {
340
+ selectedElementIds: { [element.id]: true },
341
+ },
342
+ captureUpdate: CaptureUpdateAction.NEVER,
343
+ });
344
+
345
+ if (typeof api.setViewport !== 'function') {
346
+ return;
347
+ }
348
+
349
+ suppressViewportBroadcast = true;
350
+ try {
351
+ api.setViewport({
352
+ animation: false,
353
+ fit: 'contain',
354
+ offsets: { ui: true },
355
+ target: element,
356
+ });
357
+ } finally {
358
+ releaseViewportBroadcastSuppressionAfterPaint();
359
+ }
360
+ }
361
+
123
362
  function applyDocumentMode(mode = currentDocument.mode) {
124
363
  document.body.dataset.documentMode = normalizeDocumentMode(mode);
125
364
  }
@@ -140,6 +379,16 @@ function createRoomClient(filePath) {
140
379
 
141
380
  queueCollaboratorsRender(collaborators);
142
381
  },
382
+ onCommentThreadsChange: (threads) => {
383
+ if (generation !== roomClientGeneration) {
384
+ return;
385
+ }
386
+
387
+ diagramCommentThreads = Array.isArray(threads) ? threads : [];
388
+ if (reactRoot) {
389
+ renderExcalidrawApp();
390
+ }
391
+ },
143
392
  onConnectionStateChange: (event) => {
144
393
  if (generation !== roomClientGeneration) {
145
394
  return;
@@ -164,7 +413,7 @@ function createRoomClient(filePath) {
164
413
  };
165
414
  }
166
415
 
167
- function buildExcalidrawProps({ initialData } = {}) {
416
+ function buildExcalidrawProps({ initialData, renderTopRightUI, viewModeEnabled } = {}) {
168
417
  const props = {
169
418
  onMount: handleEditorMount,
170
419
  onInitialize: (api) => {
@@ -172,6 +421,7 @@ function buildExcalidrawProps({ initialData } = {}) {
172
421
  },
173
422
  onUnmount: () => {
174
423
  clearEditorApiStateBindings();
424
+ clearPreviewViewportFitTimers();
175
425
  if (skipRoomDisconnectOnUnmount) {
176
426
  skipRoomDisconnectOnUnmount = false;
177
427
  } else {
@@ -189,12 +439,18 @@ function buildExcalidrawProps({ initialData } = {}) {
189
439
  onPointerUpdate: (payload) => {
190
440
  roomClient?.scheduleLocalPointerAwareness(payload);
191
441
  },
442
+ onPointerDown: () => {
443
+ initialViewportFitPending = false;
444
+ stopFollowingFromLocalInteraction();
445
+ },
446
+ renderTopRightUI,
192
447
  historyOptions: {
193
448
  traversal: 'single-entry',
194
449
  },
195
450
  onHistoryAction: handleHistoryAction,
196
451
  theme: currentTheme,
197
452
  ...getDocumentViewState(),
453
+ ...(typeof viewModeEnabled === 'boolean' ? { viewModeEnabled } : {}),
198
454
  UIOptions: {
199
455
  canvasActions: {
200
456
  export: false,
@@ -212,6 +468,425 @@ function buildExcalidrawProps({ initialData } = {}) {
212
468
  return props;
213
469
  }
214
470
 
471
+ const diagramCommentsContext = React.createContext(null);
472
+
473
+ function DiagramCommentsToolbar() {
474
+ const context = React.useContext(diagramCommentsContext);
475
+ if (!context?.visible || !context.room) {
476
+ return null;
477
+ }
478
+
479
+ const addCommentButton = context.selectedElement
480
+ ? React.createElement('button', {
481
+ 'aria-label': 'Add comment',
482
+ className: 'diagram-comment-add sidebar-trigger',
483
+ 'data-testid': 'diagram-add-comment',
484
+ disabled: !context.canWrite,
485
+ key: 'add',
486
+ onClick: context.openComposer,
487
+ title: context.canWrite ? 'Add comment to selected element' : 'Reconnect to add a comment',
488
+ type: 'button',
489
+ }, React.createElement(DiagramCommentIcon, { add: true }))
490
+ : null;
491
+
492
+ return React.createElement('div', {
493
+ 'aria-label': 'Diagram comments',
494
+ className: 'diagram-comments-toolbar',
495
+ role: 'toolbar',
496
+ }, [
497
+ addCommentButton,
498
+ React.createElement('button', {
499
+ 'aria-expanded': context.drawerOpen,
500
+ 'aria-label': 'Comments',
501
+ className: `diagram-comment-button sidebar-trigger${context.drawerOpen ? ' active' : ''}`,
502
+ 'data-testid': 'diagram-comments-toggle',
503
+ key: 'toggle',
504
+ onClick: context.toggleDrawer,
505
+ title: context.drawerOpen ? 'Close comments' : 'Open comments',
506
+ type: 'button',
507
+ }, [
508
+ React.createElement(DiagramCommentIcon, { key: 'icon' }),
509
+ context.threads.length > 0
510
+ ? React.createElement('span', {
511
+ className: 'diagram-comment-count',
512
+ key: 'count',
513
+ }, String(context.threads.length))
514
+ : null,
515
+ ]),
516
+ ]);
517
+ }
518
+
519
+ const DiagramCommentsExcalidraw = React.memo(function DiagramCommentsExcalidraw({ initialData = null, renderKey = 0, viewModeEnabled }) {
520
+ const renderTopRightUI = React.useCallback(
521
+ () => React.createElement(DiagramCommentsToolbar),
522
+ [],
523
+ );
524
+
525
+ return React.createElement(Excalidraw, {
526
+ key: `editor-${renderKey}`,
527
+ ...buildExcalidrawProps({ initialData, renderTopRightUI, viewModeEnabled }),
528
+ });
529
+ });
530
+
531
+ function useCloseDiagramCommentsOnCanvas(drawerOpen, setDrawerOpen) {
532
+ React.useEffect(() => {
533
+ if (!drawerOpen) {
534
+ return undefined;
535
+ }
536
+
537
+ const closeOnCanvasPointerDown = (event) => {
538
+ if (!(event.target instanceof Element) || !event.target.closest('canvas.excalidraw__canvas')) {
539
+ return;
540
+ }
541
+
542
+ setDrawerOpen(false);
543
+ };
544
+ const closeOnEscape = (event) => {
545
+ if (event.key !== 'Escape') {
546
+ return;
547
+ }
548
+
549
+ event.preventDefault();
550
+ event.stopPropagation();
551
+ setDrawerOpen(false);
552
+ };
553
+
554
+ document.addEventListener('pointerdown', closeOnCanvasPointerDown, true);
555
+ document.addEventListener('keydown', closeOnEscape, true);
556
+ return () => {
557
+ document.removeEventListener('pointerdown', closeOnCanvasPointerDown, true);
558
+ document.removeEventListener('keydown', closeOnEscape, true);
559
+ };
560
+ }, [drawerOpen, setDrawerOpen]);
561
+ }
562
+
563
+ function DiagramCommentsEditor({ apiId = '', canWrite = false, focusRequest = null, initialData = null, room = null, threads = [], visible = false }) {
564
+ const [activeThreadId, setActiveThreadId] = React.useState(null);
565
+ const [composerOpen, setComposerOpen] = React.useState(false);
566
+ const [drawerOpen, setDrawerOpen] = React.useState(false);
567
+ const [draft, setDraft] = React.useState('');
568
+ const [replyDraft, setReplyDraft] = React.useState('');
569
+ const [, setViewportRevision] = React.useState(0);
570
+
571
+ React.useEffect(() => {
572
+ const api = getMountedExcalidrawAPI();
573
+ if (!api) {
574
+ return undefined;
575
+ }
576
+
577
+ const refresh = () => setViewportRevision((revision) => revision + 1);
578
+ const cleanups = [
579
+ api.onChange?.(refresh),
580
+ api.onScrollChange?.(refresh),
581
+ ].filter((cleanup) => typeof cleanup === 'function');
582
+ window.addEventListener('resize', refresh);
583
+ return () => {
584
+ cleanups.forEach((cleanup) => cleanup());
585
+ window.removeEventListener('resize', refresh);
586
+ };
587
+ }, [apiId]);
588
+
589
+ useCloseDiagramCommentsOnCanvas(drawerOpen, setDrawerOpen);
590
+
591
+ React.useEffect(() => {
592
+ if (activeThreadId && !threads.some((thread) => thread.id === activeThreadId)) {
593
+ setActiveThreadId(null);
594
+ }
595
+ }, [activeThreadId, threads]);
596
+
597
+ React.useEffect(() => {
598
+ const threadId = focusRequest?.threadId;
599
+ const thread = threads.find((candidate) => candidate.id === threadId);
600
+ if (!thread) {
601
+ return;
602
+ }
603
+
604
+ setDrawerOpen(true);
605
+ setComposerOpen(false);
606
+ setActiveThreadId(threadId);
607
+ setReplyDraft('');
608
+ focusDiagramCommentThread(thread);
609
+ }, [focusRequest?.requestId, threads]);
610
+
611
+ const selectedElement = getSelectedDiagramElement();
612
+ const activeThread = threads.find((thread) => thread.id === activeThreadId) ?? null;
613
+ const openComposer = () => {
614
+ if (!canWrite || !selectedElement) {
615
+ return;
616
+ }
617
+
618
+ setDrawerOpen(true);
619
+ setComposerOpen(true);
620
+ setActiveThreadId(null);
621
+ };
622
+ const openThread = (threadId) => {
623
+ setDrawerOpen(true);
624
+ setComposerOpen(false);
625
+ setActiveThreadId(threadId);
626
+ setReplyDraft('');
627
+ };
628
+ const submitComposer = (event) => {
629
+ event.preventDefault();
630
+ const selectedElementForAnchor = getSelectedDiagramElement();
631
+ const elementContent = getDiagramElementContent(selectedElementForAnchor);
632
+ const threadId = room.createCommentThread({
633
+ body: draft,
634
+ element: elementContent
635
+ ? { ...selectedElementForAnchor, text: elementContent }
636
+ : selectedElementForAnchor,
637
+ });
638
+ if (!threadId) {
639
+ return;
640
+ }
641
+
642
+ setDraft('');
643
+ setComposerOpen(false);
644
+ setActiveThreadId(threadId);
645
+ };
646
+ const submitReply = (event) => {
647
+ event.preventDefault();
648
+ if (!activeThread) {
649
+ return;
650
+ }
651
+
652
+ const messageId = room.replyToCommentThread(activeThread.id, replyDraft);
653
+ if (!messageId) {
654
+ return;
655
+ }
656
+
657
+ setReplyDraft('');
658
+ };
659
+ const resolveThread = () => {
660
+ if (!activeThread || !room.deleteCommentThread(activeThread.id)) {
661
+ return;
662
+ }
663
+
664
+ setActiveThreadId(null);
665
+ };
666
+ const renderComposer = () => React.createElement('form', {
667
+ className: 'diagram-comment-form',
668
+ onSubmit: submitComposer,
669
+ }, [
670
+ React.createElement('p', {
671
+ className: 'diagram-comment-anchor-label',
672
+ key: 'anchor',
673
+ }, selectedElement
674
+ ? `On ${formatDiagramAnchorLabel(selectedElement.type, getDiagramElementContent(selectedElement))}`
675
+ : 'On selected element'),
676
+ React.createElement('textarea', {
677
+ 'aria-label': 'Comment',
678
+ autoFocus: true,
679
+ className: 'diagram-comment-input',
680
+ key: 'input',
681
+ maxLength: 2000,
682
+ onChange: (event) => setDraft(event.target.value),
683
+ placeholder: 'Add context, feedback, or a question…',
684
+ rows: 4,
685
+ value: draft,
686
+ }),
687
+ React.createElement('div', {
688
+ className: 'diagram-comment-form-actions',
689
+ key: 'actions',
690
+ }, [
691
+ React.createElement('button', {
692
+ className: 'diagram-comment-button is-secondary',
693
+ key: 'cancel',
694
+ onClick: () => setComposerOpen(false),
695
+ type: 'button',
696
+ }, 'Cancel'),
697
+ React.createElement('button', {
698
+ className: 'diagram-comment-button is-primary',
699
+ disabled: !draft.trim() || !canWrite,
700
+ key: 'submit',
701
+ type: 'submit',
702
+ }, 'Post comment'),
703
+ ]),
704
+ ]);
705
+ const renderThread = () => React.createElement(React.Fragment, null, [
706
+ React.createElement('button', {
707
+ className: 'diagram-comment-back',
708
+ key: 'back',
709
+ onClick: () => setActiveThreadId(null),
710
+ type: 'button',
711
+ }, '← All comments'),
712
+ React.createElement('p', {
713
+ className: 'diagram-comment-anchor-label',
714
+ key: 'label',
715
+ }, getDiagramAnchorLabel(activeThread)),
716
+ React.createElement('div', {
717
+ className: 'diagram-comment-messages',
718
+ key: 'messages',
719
+ }, activeThread.messages?.map((message) => React.createElement('article', {
720
+ className: 'diagram-comment-message',
721
+ key: message.id,
722
+ }, [
723
+ React.createElement('div', {
724
+ className: 'diagram-comment-message-meta',
725
+ key: 'meta',
726
+ }, message.userName || 'Anonymous'),
727
+ React.createElement('p', {
728
+ className: 'diagram-comment-message-body',
729
+ key: 'body',
730
+ }, message.body),
731
+ ]))),
732
+ React.createElement('form', {
733
+ className: 'diagram-comment-form',
734
+ key: 'reply-form',
735
+ onSubmit: submitReply,
736
+ }, [
737
+ React.createElement('textarea', {
738
+ 'aria-label': 'Reply',
739
+ className: 'diagram-comment-input',
740
+ disabled: !canWrite,
741
+ key: 'reply-input',
742
+ maxLength: 2000,
743
+ onChange: (event) => setReplyDraft(event.target.value),
744
+ placeholder: canWrite ? 'Reply…' : 'Reconnect to reply',
745
+ rows: 3,
746
+ value: replyDraft,
747
+ }),
748
+ React.createElement('div', {
749
+ className: 'diagram-comment-form-actions',
750
+ key: 'reply-actions',
751
+ }, [
752
+ React.createElement('button', {
753
+ className: 'diagram-comment-button is-primary',
754
+ disabled: !replyDraft.trim() || !canWrite,
755
+ type: 'submit',
756
+ }, 'Reply'),
757
+ React.createElement('button', {
758
+ className: 'diagram-comment-button is-danger',
759
+ disabled: !canWrite,
760
+ onClick: resolveThread,
761
+ type: 'button',
762
+ }, 'Resolve'),
763
+ ]),
764
+ ]),
765
+ ]);
766
+ const renderDrawer = () => {
767
+ if (!drawerOpen) {
768
+ return null;
769
+ }
770
+
771
+ const isListView = !composerOpen && !activeThread;
772
+ const content = composerOpen
773
+ ? renderComposer()
774
+ : activeThread
775
+ ? renderThread()
776
+ : threads.length > 0
777
+ ? threads.map((thread) => renderDiagramCommentOverviewThread(thread, activeThreadId, openThread))
778
+ : React.createElement('div', {
779
+ className: 'comments-drawer-empty ui-empty-state ui-empty-state--compact',
780
+ }, [
781
+ React.createElement('p', {
782
+ className: 'ui-empty-state-title',
783
+ key: 'title',
784
+ }, 'No comments yet'),
785
+ React.createElement('p', {
786
+ className: 'ui-empty-state-copy',
787
+ key: 'copy',
788
+ }, 'Select an element and add the first comment.'),
789
+ ]);
790
+ const drawerAddButton = selectedElement && !composerOpen
791
+ ? React.createElement('button', {
792
+ 'aria-label': 'Add comment',
793
+ className: 'diagram-comment-add',
794
+ disabled: !canWrite,
795
+ key: 'add',
796
+ onClick: openComposer,
797
+ title: canWrite ? 'Add comment to selected element' : 'Reconnect to add a comment',
798
+ type: 'button',
799
+ }, React.createElement(DiagramCommentIcon, { add: true }))
800
+ : null;
801
+
802
+ return React.createElement('aside', {
803
+ 'aria-label': 'Diagram comments',
804
+ className: 'diagram-comments-drawer',
805
+ 'data-testid': 'diagram-comments-drawer',
806
+ }, [
807
+ React.createElement('div', {
808
+ className: 'comments-drawer-header diagram-comments-drawer-header',
809
+ key: 'header',
810
+ }, [
811
+ React.createElement('span', {
812
+ className: 'comments-drawer-title',
813
+ key: 'title',
814
+ }, composerOpen ? 'New comment' : activeThread ? 'Comment thread' : 'Comments'),
815
+ React.createElement('div', {
816
+ className: 'diagram-comments-drawer-header-actions',
817
+ key: 'actions',
818
+ }, [
819
+ drawerAddButton,
820
+ React.createElement('button', {
821
+ 'aria-label': 'Close comments',
822
+ className: 'diagram-comment-close',
823
+ key: 'close',
824
+ onClick: () => setDrawerOpen(false),
825
+ type: 'button',
826
+ }, '×'),
827
+ ]),
828
+ ]),
829
+ React.createElement('div', {
830
+ className: `diagram-comments-drawer-content${isListView ? ' comments-drawer-list is-list' : ''}`,
831
+ key: 'content',
832
+ }, content),
833
+ ]);
834
+ };
835
+
836
+ const renderCommentsOverlay = visible && room
837
+ ? React.createElement('div', {
838
+ 'aria-label': 'Diagram comments overlay',
839
+ className: 'diagram-comments-overlay',
840
+ key: 'overlay',
841
+ }, [
842
+ React.createElement('div', {
843
+ className: 'diagram-comment-markers',
844
+ key: 'markers',
845
+ }, threads.map((thread) => {
846
+ const position = getDiagramCommentMarkerPosition(thread);
847
+ if (!position) {
848
+ return null;
849
+ }
850
+
851
+ return React.createElement('button', {
852
+ 'aria-label': `Open comment on ${getDiagramAnchorLabel(thread)}`,
853
+ className: `diagram-comment-marker${thread.id === activeThreadId ? ' is-active' : ''}`,
854
+ 'data-comment-thread-id': thread.id,
855
+ key: thread.id,
856
+ onClick: () => openThread(thread.id),
857
+ style: {
858
+ left: `${position.x}px`,
859
+ top: `${position.y}px`,
860
+ },
861
+ type: 'button',
862
+ }, React.createElement(DiagramCommentIcon));
863
+ })),
864
+ renderDrawer(),
865
+ ])
866
+ : null;
867
+
868
+ return React.createElement(diagramCommentsContext.Provider, {
869
+ value: {
870
+ canWrite,
871
+ drawerOpen,
872
+ openComposer,
873
+ room,
874
+ selectedElement,
875
+ threads,
876
+ toggleDrawer: () => setDrawerOpen((open) => !open),
877
+ visible,
878
+ },
879
+ }, [
880
+ React.createElement(DiagramCommentsExcalidraw, {
881
+ initialData,
882
+ key: 'excalidraw',
883
+ renderKey: editorRenderKey,
884
+ viewModeEnabled: getDocumentViewState().viewModeEnabled,
885
+ }),
886
+ renderCommentsOverlay,
887
+ ]);
888
+ }
889
+
215
890
  function renderExcalidrawApp({ initialData } = {}) {
216
891
  if (!reactRoot) {
217
892
  return;
@@ -221,9 +896,14 @@ function renderExcalidrawApp({ initialData } = {}) {
221
896
  React.createElement(
222
897
  'div',
223
898
  { className: 'excalidraw-editor-shell' },
224
- React.createElement(Excalidraw, {
225
- key: `editor-${editorRenderKey}`,
226
- ...buildExcalidrawProps({ initialData }),
899
+ React.createElement(DiagramCommentsEditor, {
900
+ apiId: getMountedExcalidrawAPI()?.id || '',
901
+ canWrite: roomConnectionState === EXCALIDRAW_ROOM_CONNECTION_STATE.AUTHORITATIVE,
902
+ focusRequest: pendingDiagramCommentFocus,
903
+ initialData,
904
+ room: roomClient,
905
+ threads: diagramCommentThreads,
906
+ visible: normalizeDocumentMode(currentDocument.mode) !== 'preview',
227
907
  }),
228
908
  getAuthorityBannerText()
229
909
  ? React.createElement('div', {
@@ -260,12 +940,13 @@ function recordSceneDiagnostic(event, details = {}, sceneJson = '') {
260
940
 
261
941
  function handleHistoryAction({ action = '', outcome = '' } = {}) {
262
942
  recordSceneDiagnostic('history-action', { action, outcome });
263
- if (outcome !== 'no-visible-change' || !excalidrawAPI) {
943
+ const api = getMountedExcalidrawAPI();
944
+ if (outcome !== 'no-visible-change' || !api) {
264
945
  return;
265
946
  }
266
947
 
267
948
  const label = action === 'redo' ? 'Redo' : 'Undo';
268
- excalidrawAPI.setToast?.({
949
+ api.setToast?.({
269
950
  message: `${label} skipped: a collaborator changed that item`,
270
951
  });
271
952
  }
@@ -298,7 +979,7 @@ function handleRoomConnectionStateChange({
298
979
  roomConnectionState === EXCALIDRAW_ROOM_CONNECTION_STATE.AUTHORITATIVE
299
980
  && previousState === EXCALIDRAW_ROOM_CONNECTION_STATE.RECONNECTING_READONLY
300
981
  ) {
301
- excalidrawAPI?.setToast?.({ message: 'Live diagram reconnected' });
982
+ getMountedExcalidrawAPI()?.setToast?.({ message: 'Live diagram reconnected' });
302
983
  }
303
984
 
304
985
  if (roomConnectionState === EXCALIDRAW_ROOM_CONNECTION_STATE.AUTHORITATIVE) {
@@ -349,7 +1030,7 @@ if (isTestMode) {
349
1030
  window.__COLLABMD_EXCALIDRAW_TEST__ = {
350
1031
  disconnectTransport: () => roomClient?.provider?.disconnect?.(),
351
1032
  getElementBounds: (elementId) => {
352
- const element = excalidrawAPI?.getSceneElementsIncludingDeleted?.()?.find((entry) => entry.id === elementId && !entry.isDeleted);
1033
+ const element = getMountedExcalidrawAPI()?.getSceneElementsIncludingDeleted?.()?.find((entry) => entry.id === elementId && !entry.isDeleted);
353
1034
  if (!element) {
354
1035
  return null;
355
1036
  }
@@ -364,49 +1045,66 @@ if (isTestMode) {
364
1045
  };
365
1046
  },
366
1047
  getElementCount: () => (
367
- excalidrawAPI?.getSceneElementsIncludingDeleted?.()?.filter((element) => !element.isDeleted).length ?? 0
1048
+ getMountedExcalidrawAPI()?.getSceneElementsIncludingDeleted?.()?.filter((element) => !element.isDeleted).length ?? 0
368
1049
  ),
369
1050
  getElementIds: () => (
370
- excalidrawAPI?.getSceneElementsIncludingDeleted?.()
1051
+ getMountedExcalidrawAPI()?.getSceneElementsIncludingDeleted?.()
371
1052
  ?.filter((element) => !element.isDeleted)
372
1053
  .map((element) => element.id)
373
1054
  .sort() ?? []
374
1055
  ),
375
1056
  getElementStatus: (elementId) => (
376
- excalidrawAPI?.getSceneElementsIncludingDeleted?.()
1057
+ getMountedExcalidrawAPI()?.getSceneElementsIncludingDeleted?.()
377
1058
  ?.find((entry) => entry.id === elementId && !entry.isDeleted)?.status ?? null
378
1059
  ),
379
1060
  getFileIds: () => (
380
- Object.keys(excalidrawAPI?.getFiles?.() || {}).sort()
1061
+ Object.keys(getMountedExcalidrawAPI()?.getFiles?.() || {}).sort()
381
1062
  ),
382
- getFileVersion: (fileId) => excalidrawAPI?.getFiles?.()?.[fileId]?.version ?? null,
383
- getEditorId: () => excalidrawAPI?.id || null,
1063
+ getFileVersion: (fileId) => getMountedExcalidrawAPI()?.getFiles?.()?.[fileId]?.version ?? null,
1064
+ getEditorId: () => getMountedExcalidrawAPI()?.id || null,
384
1065
  getForkCapabilities: () => ({
385
- replaceFiles: typeof excalidrawAPI?.replaceFiles === 'function',
1066
+ replaceFiles: typeof getMountedExcalidrawAPI()?.replaceFiles === 'function',
386
1067
  }),
387
1068
  getAuthorityState: () => roomConnectionState,
1069
+ getCommentThreads: () => roomClient?.getCommentThreads?.() || [],
388
1070
  getDiagnosticTrace: () => diagnostics.exportTrace(),
389
1071
  getHistoryState: () => getNativeHistoryState(),
390
1072
  getLocalUserName: () => localAwarenessUser?.name || '',
391
1073
  getLocalPeerId: () => localAwarenessUser?.peerId || '',
392
1074
  getViewport: () => {
393
- const appState = excalidrawAPI?.getAppState?.();
1075
+ const appState = getMountedExcalidrawAPI()?.getAppState?.();
394
1076
  return appState ? {
1077
+ offsetLeft: appState.offsetLeft,
1078
+ offsetTop: appState.offsetTop,
395
1079
  scrollX: appState.scrollX,
396
1080
  scrollY: appState.scrollY,
397
1081
  zoom: appState.zoom?.value ?? null,
398
1082
  } : null;
399
1083
  },
400
- isViewMode: () => Boolean(excalidrawAPI?.getAppState?.().viewModeEnabled),
1084
+ isViewMode: () => Boolean(getMountedExcalidrawAPI()?.getAppState?.().viewModeEnabled),
1085
+ selectElement: (elementId) => {
1086
+ const api = getMountedExcalidrawAPI();
1087
+ if (!api || !elementId) {
1088
+ return false;
1089
+ }
1090
+
1091
+ api.updateScene({
1092
+ appState: {
1093
+ selectedElementIds: { [elementId]: true },
1094
+ },
1095
+ captureUpdate: CaptureUpdateAction.NEVER,
1096
+ });
1097
+ return true;
1098
+ },
401
1099
  getSceneJson: () => roomClient?.getLastSceneJson?.() || '',
402
1100
  isAuthoritativeReady: () => (
403
- Boolean(excalidrawAPI)
1101
+ Boolean(getMountedExcalidrawAPI())
404
1102
  && collabReady
405
1103
  && roomClient?.canWriteToRoom === true
406
1104
  && roomClient?.waitingForAuthoritativeSync === false
407
1105
  && roomClient?.isApplyingSharedSnapshot?.() === false
408
1106
  ),
409
- isReady: () => collabReady && Boolean(excalidrawAPI) && Boolean(getNativeHistoryButton('undo')) && Boolean(getNativeHistoryButton('redo')),
1107
+ isReady: () => collabReady && Boolean(getMountedExcalidrawAPI()),
410
1108
  redoShared: () => triggerNativeHistory('redo'),
411
1109
  reconnectTransport: () => roomClient?.provider?.connect?.(),
412
1110
  setScene: (scene) => {
@@ -415,12 +1113,13 @@ if (isTestMode) {
415
1113
  });
416
1114
  },
417
1115
  setViewport: (viewport) => {
418
- if (!excalidrawAPI) {
1116
+ const api = getMountedExcalidrawAPI();
1117
+ if (!api) {
419
1118
  return;
420
1119
  }
421
1120
 
422
- const currentAppState = excalidrawAPI.getAppState();
423
- excalidrawAPI.updateScene({
1121
+ const currentAppState = api.getAppState();
1122
+ api.updateScene({
424
1123
  appState: {
425
1124
  scrollX: Number.isFinite(viewport?.scrollX) ? viewport.scrollX : currentAppState.scrollX,
426
1125
  scrollY: Number.isFinite(viewport?.scrollY) ? viewport.scrollY : currentAppState.scrollY,
@@ -440,14 +1139,15 @@ function applyCollaborators(collaborators) {
440
1139
  const renderableCollaborators = buildRenderableCollaboratorsMap(activeCollaborators);
441
1140
  const renderSignature = getCollaboratorsRenderSignature(renderableCollaborators);
442
1141
 
443
- if (!excalidrawAPI) {
1142
+ const api = getMountedExcalidrawAPI();
1143
+ if (!api) {
444
1144
  pendingCollaborators = activeCollaborators;
445
1145
  return;
446
1146
  }
447
1147
 
448
1148
  if (renderSignature !== lastRenderedCollaboratorsSignature) {
449
1149
  lastRenderedCollaboratorsSignature = renderSignature;
450
- excalidrawAPI.updateScene({
1150
+ api.updateScene({
451
1151
  collaborators: renderableCollaborators,
452
1152
  captureUpdate: CaptureUpdateAction.NEVER,
453
1153
  });
@@ -476,11 +1176,11 @@ function queueCollaboratorsRender(collaborators) {
476
1176
  }
477
1177
 
478
1178
  function isEditingTextElement() {
479
- return Boolean(excalidrawAPI?.getAppState?.()?.editingTextElement);
1179
+ return Boolean(getMountedExcalidrawAPI()?.getAppState?.()?.editingTextElement);
480
1180
  }
481
1181
 
482
1182
  function flushPendingRemoteScene() {
483
- if (!pendingRemoteSceneJson || !excalidrawAPI || !collabReady || isEditingTextElement()) {
1183
+ if (!pendingRemoteSceneJson || !getMountedExcalidrawAPI() || !collabReady || isEditingTextElement()) {
484
1184
  return false;
485
1185
  }
486
1186
 
@@ -504,7 +1204,7 @@ function applySceneFromJson(rawJson, {
504
1204
  appliedSceneJson = normalizedJson;
505
1205
  recordSceneDiagnostic('remote-scene-received', {}, normalizedJson);
506
1206
 
507
- if (!excalidrawAPI || !collabReady) {
1207
+ if (!getMountedExcalidrawAPI() || !collabReady) {
508
1208
  pendingRemoteSceneJson = normalizedJson;
509
1209
  return;
510
1210
  }
@@ -531,9 +1231,14 @@ function releaseViewportBroadcastSuppressionAfterPaint() {
531
1231
 
532
1232
  function buildApiSceneUpdate(scene, {
533
1233
  appStateOverrides = {},
1234
+ api = getMountedExcalidrawAPI(),
534
1235
  } = {}) {
535
- const currentAppState = excalidrawAPI.getAppState();
536
- const currentElements = excalidrawAPI.getSceneElementsIncludingDeleted?.() || excalidrawAPI.getSceneElements();
1236
+ if (!api) {
1237
+ return null;
1238
+ }
1239
+
1240
+ const currentAppState = api.getAppState();
1241
+ const currentElements = api.getSceneElementsIncludingDeleted?.() || api.getSceneElements();
537
1242
 
538
1243
  return buildReconciledExcalidrawSceneUpdate({
539
1244
  appStateOverrides,
@@ -575,6 +1280,7 @@ function requestEditorRemount(scene) {
575
1280
  appliedSceneJson = normalizedJson;
576
1281
  pendingCollaborators = activeCollaborators;
577
1282
  initialViewportFitPending = true;
1283
+ clearPreviewViewportFitTimers();
578
1284
  clearEditorApiStateBindings();
579
1285
  skipRoomDisconnectOnUnmount = true;
580
1286
  editorRenderKey += 1;
@@ -589,8 +1295,14 @@ function applySceneToMountedApi(scene, {
589
1295
  captureUpdate = CaptureUpdateAction.NEVER,
590
1296
  trackedSharedSnapshot = false,
591
1297
  } = {}) {
1298
+ const api = getMountedExcalidrawAPI();
1299
+ if (!api) {
1300
+ return { skipped: true };
1301
+ }
1302
+
592
1303
  const nextSceneUpdate = buildApiSceneUpdate(scene, {
593
1304
  appStateOverrides,
1305
+ api,
594
1306
  });
595
1307
  let applyResult;
596
1308
 
@@ -599,7 +1311,7 @@ function applySceneToMountedApi(scene, {
599
1311
  }
600
1312
 
601
1313
  try {
602
- applyResult = applySceneUpdateWithFiles(excalidrawAPI, {
1314
+ applyResult = applySceneUpdateWithFiles(api, {
603
1315
  captureUpdate,
604
1316
  files: scene?.files || {},
605
1317
  sceneUpdate: nextSceneUpdate,
@@ -643,7 +1355,7 @@ function applyLocalScene(scene, {
643
1355
 
644
1356
  appliedSceneJson = normalizedJson;
645
1357
 
646
- if (!excalidrawAPI || !collabReady) {
1358
+ if (!getMountedExcalidrawAPI() || !collabReady) {
647
1359
  pendingRemoteSceneJson = normalizedJson;
648
1360
  return;
649
1361
  }
@@ -661,14 +1373,24 @@ function onRoomTextUpdate() {
661
1373
  }
662
1374
 
663
1375
  function getLiveSceneElementsForSync(fallbackElements = []) {
664
- return excalidrawAPI?.getSceneElementsIncludingDeleted?.() || fallbackElements;
1376
+ return getMountedExcalidrawAPI()?.getSceneElementsIncludingDeleted?.() || fallbackElements;
665
1377
  }
666
1378
 
667
1379
  function postToParent(type, payload = {}) {
668
1380
  window.parent.postMessage({ source: 'excalidraw-editor', type, ...payload }, parentOrigin);
669
1381
  }
670
1382
 
1383
+ function stopFollowingFromLocalInteraction() {
1384
+ if (!pendingHostFollowPeerId && !followedSocketId) {
1385
+ return;
1386
+ }
1387
+
1388
+ applyHostFollowRequest(null);
1389
+ postToParent('stop-following');
1390
+ }
1391
+
671
1392
  function handleQuickSwitcherKeyDown(event) {
1393
+ stopFollowingFromLocalInteraction();
672
1394
  if (!isPlainQuickSwitcherShortcut(event)) {
673
1395
  return;
674
1396
  }
@@ -679,8 +1401,9 @@ function handleQuickSwitcherKeyDown(event) {
679
1401
  }
680
1402
 
681
1403
  function getSceneElementsForPreviewFit() {
1404
+ const api = getMountedExcalidrawAPI();
682
1405
  return (
683
- excalidrawAPI?.getSceneElementsIncludingDeleted?.()
1406
+ api?.getSceneElementsIncludingDeleted?.()
684
1407
  ?.filter((element) => !element.isDeleted) ?? []
685
1408
  );
686
1409
  }
@@ -691,7 +1414,8 @@ function scheduleViewportFit({
691
1414
  consumeInitialFit = false,
692
1415
  } = {}) {
693
1416
  const normalizedMode = normalizeDocumentMode(currentDocument.mode);
694
- if (!excalidrawAPI || (forcePreview && normalizedMode !== 'preview')) {
1417
+ const api = getMountedExcalidrawAPI();
1418
+ if (!api || (forcePreview && normalizedMode !== 'preview')) {
695
1419
  return;
696
1420
  }
697
1421
 
@@ -699,6 +1423,11 @@ function scheduleViewportFit({
699
1423
  return;
700
1424
  }
701
1425
 
1426
+ if (!forcePreview && api.getAppState?.().cursorButton === 'down') {
1427
+ initialViewportFitPending = false;
1428
+ return;
1429
+ }
1430
+
702
1431
  const elements = getSceneElementsForPreviewFit();
703
1432
  if (elements.length === 0) {
704
1433
  return;
@@ -707,15 +1436,42 @@ function scheduleViewportFit({
707
1436
  initialViewportFitPending = false;
708
1437
  }
709
1438
 
1439
+ if (!forcePreview && delayMs === 0) {
1440
+ api.setViewport({
1441
+ target: elements,
1442
+ animation: false,
1443
+ fit: 'contain',
1444
+ });
1445
+ return;
1446
+ }
1447
+
1448
+ const generation = ++viewportFitGeneration;
1449
+
710
1450
  if (previewViewportFitTimerId) {
711
1451
  window.clearTimeout(previewViewportFitTimerId);
712
1452
  }
713
1453
 
714
1454
  previewViewportFitTimerId = window.setTimeout(() => {
715
1455
  previewViewportFitTimerId = 0;
1456
+ if (generation !== viewportFitGeneration) {
1457
+ return;
1458
+ }
1459
+
716
1460
  requestAnimationFrame(() => {
1461
+ if (generation !== viewportFitGeneration) {
1462
+ return;
1463
+ }
1464
+
717
1465
  requestAnimationFrame(() => {
718
- if (!excalidrawAPI) {
1466
+ if (generation !== viewportFitGeneration) {
1467
+ return;
1468
+ }
1469
+
1470
+ const api = getMountedExcalidrawAPI();
1471
+ if (!api) {
1472
+ return;
1473
+ }
1474
+ if (!forcePreview && api.getAppState?.().cursorButton === 'down') {
719
1475
  return;
720
1476
  }
721
1477
 
@@ -728,11 +1484,10 @@ function scheduleViewportFit({
728
1484
  suppressViewportBroadcast = true;
729
1485
  }
730
1486
 
731
- excalidrawAPI.scrollToContent(latestElements, {
732
- animate: false,
733
- fitToViewport: true,
734
- maxZoom: 2,
735
- viewportZoomFactor: normalizedMode === 'preview' ? 0.92 : 0.88,
1487
+ api.setViewport({
1488
+ target: latestElements,
1489
+ animation: false,
1490
+ fit: 'contain',
736
1491
  });
737
1492
 
738
1493
  if (forcePreview) {
@@ -748,18 +1503,38 @@ function scheduleInitialViewportFit() {
748
1503
  }
749
1504
 
750
1505
  function schedulePreviewViewportFit() {
1506
+ if (previewViewportFitRetryTimerId) {
1507
+ window.clearTimeout(previewViewportFitRetryTimerId);
1508
+ }
1509
+
751
1510
  scheduleViewportFit({ forcePreview: true, delayMs: 80 });
752
- window.setTimeout(() => {
1511
+ previewViewportFitRetryTimerId = window.setTimeout(() => {
1512
+ previewViewportFitRetryTimerId = 0;
753
1513
  scheduleViewportFit({ forcePreview: true });
754
1514
  }, 240);
755
1515
  }
756
1516
 
1517
+ function clearPreviewViewportFitTimers() {
1518
+ viewportFitGeneration += 1;
1519
+
1520
+ if (previewViewportFitTimerId) {
1521
+ window.clearTimeout(previewViewportFitTimerId);
1522
+ previewViewportFitTimerId = 0;
1523
+ }
1524
+
1525
+ if (previewViewportFitRetryTimerId) {
1526
+ window.clearTimeout(previewViewportFitRetryTimerId);
1527
+ previewViewportFitRetryTimerId = 0;
1528
+ }
1529
+ }
1530
+
757
1531
  function syncLocalViewportToRoom() {
758
- if (!collabReady || !excalidrawAPI || !roomClient || suppressViewportBroadcast) {
1532
+ const api = getMountedExcalidrawAPI();
1533
+ if (!collabReady || !api || !roomClient || suppressViewportBroadcast) {
759
1534
  return;
760
1535
  }
761
1536
 
762
- const appState = excalidrawAPI.getAppState();
1537
+ const appState = api.getAppState();
763
1538
  roomClient.scheduleLocalViewportAwareness({
764
1539
  scrollX: appState.scrollX,
765
1540
  scrollY: appState.scrollY,
@@ -781,7 +1556,8 @@ function setFollowedSocket(nextSocketId, { force = false } = {}) {
781
1556
  }
782
1557
 
783
1558
  function applyFollowedViewport(collaborators = activeCollaborators, { force = false } = {}) {
784
- if (!excalidrawAPI || !followedSocketId) {
1559
+ const api = getMountedExcalidrawAPI();
1560
+ if (!api || !followedSocketId) {
785
1561
  return;
786
1562
  }
787
1563
 
@@ -798,7 +1574,7 @@ function applyFollowedViewport(collaborators = activeCollaborators, { force = fa
798
1574
 
799
1575
  lastAppliedFollowViewportSignature = nextSignature;
800
1576
  suppressViewportBroadcast = true;
801
- excalidrawAPI.updateScene({
1577
+ api.updateScene({
802
1578
  appState: {
803
1579
  scrollX: viewport.scrollX,
804
1580
  scrollY: viewport.scrollY,
@@ -811,12 +1587,13 @@ function applyFollowedViewport(collaborators = activeCollaborators, { force = fa
811
1587
 
812
1588
  function applyHostFollowRequest(peerId) {
813
1589
  pendingHostFollowPeerId = peerId || null;
814
- if (!excalidrawAPI) {
1590
+ const api = getMountedExcalidrawAPI();
1591
+ if (!api) {
815
1592
  return;
816
1593
  }
817
1594
 
818
1595
  if (!peerId) {
819
- excalidrawAPI.updateScene({
1596
+ api.updateScene({
820
1597
  appState: { userToFollow: null },
821
1598
  captureUpdate: CaptureUpdateAction.NEVER,
822
1599
  });
@@ -830,7 +1607,7 @@ function applyHostFollowRequest(peerId) {
830
1607
  return;
831
1608
  }
832
1609
 
833
- excalidrawAPI.updateScene({
1610
+ api.updateScene({
834
1611
  appState: {
835
1612
  userToFollow: {
836
1613
  socketId: collaborator.socketId,
@@ -853,6 +1630,8 @@ function resetRealtimeRoomState() {
853
1630
  pendingRemoteSceneJson = '';
854
1631
  pendingCollaborators = null;
855
1632
  activeCollaborators = new Map();
1633
+ diagramCommentThreads = [];
1634
+ pendingDiagramCommentFocus = null;
856
1635
  followedSocketId = null;
857
1636
  pendingHostFollowPeerId = null;
858
1637
  suppressViewportBroadcast = false;
@@ -984,6 +1763,10 @@ window.addEventListener('message', (event) => {
984
1763
  return;
985
1764
  }
986
1765
 
1766
+ if (event.source !== window.parent) {
1767
+ return;
1768
+ }
1769
+
987
1770
  const message = event.data;
988
1771
  if (!message || message.source !== 'collabmd-host') {
989
1772
  return;
@@ -992,8 +1775,9 @@ window.addEventListener('message', (event) => {
992
1775
  if (message.type === 'set-theme') {
993
1776
  currentTheme = message.theme || 'dark';
994
1777
  applySurfaceTheme(currentTheme);
995
- if (excalidrawAPI) {
996
- excalidrawAPI.updateScene({
1778
+ const api = getMountedExcalidrawAPI();
1779
+ if (api) {
1780
+ api.updateScene({
997
1781
  appState: { theme: currentTheme },
998
1782
  captureUpdate: CaptureUpdateAction.NEVER,
999
1783
  });
@@ -1007,6 +1791,11 @@ window.addEventListener('message', (event) => {
1007
1791
  return;
1008
1792
  }
1009
1793
 
1794
+ if (message.type === 'open-comment-thread') {
1795
+ requestDiagramCommentFocus(message.threadId);
1796
+ return;
1797
+ }
1798
+
1010
1799
  if (message.type === 'follow-user') {
1011
1800
  applyHostFollowRequest(message.peerId || null);
1012
1801
  return;
@@ -1102,6 +1891,9 @@ function initializeEditor(api) {
1102
1891
 
1103
1892
  setFollowedSocket(null, { force: true });
1104
1893
  }));
1894
+ apiStateCleanupCallbacks.push(api.onStateChange('selectedElementIds', () => {
1895
+ renderExcalidrawApp();
1896
+ }));
1105
1897
  apiStateCleanupCallbacks.push(api.onStateChange('editingTextElement', (editingTextElement) => {
1106
1898
  if (!editingTextElement) {
1107
1899
  requestAnimationFrame(() => {
@@ -1121,7 +1913,7 @@ function initializeEditor(api) {
1121
1913
  if (pendingCollaborators) {
1122
1914
  const renderableCollaborators = buildRenderableCollaboratorsMap(pendingCollaborators);
1123
1915
  lastRenderedCollaboratorsSignature = getCollaboratorsRenderSignature(renderableCollaborators);
1124
- excalidrawAPI.updateScene({
1916
+ api.updateScene({
1125
1917
  collaborators: renderableCollaborators,
1126
1918
  captureUpdate: CaptureUpdateAction.NEVER,
1127
1919
  });
@@ -1137,6 +1929,7 @@ function initializeEditor(api) {
1137
1929
 
1138
1930
  scheduleInitialViewportFit();
1139
1931
  postToParent('ready');
1932
+ renderExcalidrawApp();
1140
1933
  }
1141
1934
 
1142
1935
  function handleEditorMount({ excalidrawAPI: api }) {