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,9 @@
1
1
  import {
2
2
  getVaultFileExtension,
3
+ isVaultFilePath,
4
+ VAULT_FILE_EXTENSIONS,
3
5
  } from '../../domain/file-kind.js';
6
+ import { pickFiles } from '../browser-utils.js';
4
7
  import {
5
8
  createBaseStarter,
6
9
  composeVaultChildPath,
@@ -8,6 +11,7 @@ import {
8
11
  createMarkdownStarter,
9
12
  createMermaidStarter,
10
13
  createPlantUmlStarter,
14
+ createStructurizrStarter,
11
15
  ensureVaultExtension,
12
16
  getVaultPathLeaf,
13
17
  getVaultPathParent,
@@ -28,6 +32,8 @@ function formatCount(value, singularLabel, pluralLabel) {
28
32
  return `${value} ${value === 1 ? singularLabel : pluralLabel}`;
29
33
  }
30
34
 
35
+ const VAULT_FILE_PICKER_ACCEPT = VAULT_FILE_EXTENSIONS.join(',');
36
+
31
37
  function createEmptyExcalidrawScene() {
32
38
  return JSON.stringify({
33
39
  type: 'excalidraw',
@@ -57,7 +63,9 @@ export class FileActionController {
57
63
  : { matches: false },
58
64
  onFileDelete,
59
65
  onFileSelect,
66
+ onShowFileExtensionsChange,
60
67
  pendingWorkspaceRequestIds = null,
68
+ showFileExtensions = false,
61
69
  state,
62
70
  toastController,
63
71
  vaultClient,
@@ -66,13 +74,16 @@ export class FileActionController {
66
74
  }) {
67
75
  this.onFileDelete = onFileDelete;
68
76
  this.onFileSelect = onFileSelect;
77
+ this.onShowFileExtensionsChange = onShowFileExtensionsChange;
69
78
  this.pendingWorkspaceRequestIds = pendingWorkspaceRequestIds;
79
+ this.showFileExtensions = Boolean(showFileExtensions);
70
80
  this.state = state;
71
81
  this.toastController = toastController;
72
82
  this.vaultClient = vaultClient;
73
83
  this.view = view;
74
84
  this.refresh = refresh;
75
85
  this.createButton = document.getElementById('sidebarCreateBtn');
86
+ this.fileExplorerOptionsButton = document.getElementById('fileExplorerOptionsBtn');
76
87
  this.actionDialog = document.getElementById('fileActionDialog');
77
88
  this.actionForm = document.getElementById('fileActionForm');
78
89
  this.actionTitle = document.getElementById('fileActionTitle');
@@ -87,6 +98,9 @@ export class FileActionController {
87
98
  this.createMenu = new CreateMenuPresenter({
88
99
  mobileBreakpointQuery,
89
100
  });
101
+ this.fileExplorerOptionsMenu = new CreateMenuPresenter({
102
+ mobileBreakpointQuery,
103
+ });
90
104
  this.pendingAction = null;
91
105
  this.actionBusy = false;
92
106
  }
@@ -95,6 +109,9 @@ export class FileActionController {
95
109
  this.createButton?.addEventListener('click', () => {
96
110
  this.openRootCreateMenu({ anchor: this.createButton });
97
111
  });
112
+ this.fileExplorerOptionsButton?.addEventListener('click', () => {
113
+ this.openFileExplorerOptionsMenu({ anchor: this.fileExplorerOptionsButton });
114
+ });
98
115
  this.actionCancelButton?.addEventListener('click', () => this.closeActionDialog());
99
116
  this.actionForm?.addEventListener('submit', (event) => {
100
117
  event.preventDefault();
@@ -105,6 +122,18 @@ export class FileActionController {
105
122
 
106
123
  getCreateActions({ parentDir = '' } = {}) {
107
124
  return [
125
+ {
126
+ contextLabel: 'Upload files',
127
+ group: 'File',
128
+ hint: 'Supported vault files',
129
+ icon: this.getCreateActionIcon('upload'),
130
+ id: 'upload',
131
+ label: 'Upload files',
132
+ meta: 'Multiple',
133
+ onSelect: () => {
134
+ void this.handleUploadFiles({ parentDir });
135
+ },
136
+ },
108
137
  {
109
138
  contextLabel: 'New markdown file',
110
139
  group: 'Note',
@@ -165,6 +194,16 @@ export class FileActionController {
165
194
  meta: '.puml',
166
195
  onSelect: () => this.handleNewPlantUml({ parentDir }),
167
196
  },
197
+ {
198
+ contextLabel: 'New Structurizr workspace',
199
+ group: 'Diagram',
200
+ hint: 'Structurizr DSL',
201
+ icon: this.getCreateActionIcon('structurizr'),
202
+ id: 'structurizr',
203
+ label: 'Structurizr workspace',
204
+ meta: '.dsl',
205
+ onSelect: () => this.handleNewStructurizr({ parentDir }),
206
+ },
168
207
  {
169
208
  contextLabel: 'New folder',
170
209
  group: 'Structure',
@@ -230,10 +269,13 @@ export class FileActionController {
230
269
 
231
270
  getCreateActionIcon(kind) {
232
271
  switch (kind) {
272
+ case 'upload':
273
+ return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/><path d="M5 20h14"/></svg>';
233
274
  case 'base':
234
275
  return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4h7l5 5v11a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"/><path d="M14 4v5h5"/><path d="M8 13h8"/><path d="M8 17h6"/><path d="M8 9h3"/></svg>';
235
276
  case 'drawio':
236
277
  case 'plantuml':
278
+ case 'structurizr':
237
279
  return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="7" height="6" rx="1"/><rect x="14" y="4" width="7" height="6" rx="1"/><rect x="8.5" y="14" width="7" height="6" rx="1"/><path d="M10 7h4"/><path d="M17.5 10v2.5"/><path d="M6.5 10v2.5"/><path d="M6.5 12.5h11"/></svg>';
238
280
  case 'excalidraw':
239
281
  return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg>';
@@ -313,6 +355,7 @@ export class FileActionController {
313
355
 
314
356
  this.pendingAction = { requiresInput, emptyMessage, onSubmit };
315
357
  this.createMenu.close({ restoreFocus: false });
358
+ this.fileExplorerOptionsMenu.close({ restoreFocus: false });
316
359
  this.view?.removeContextMenu?.();
317
360
 
318
361
  this.actionTitle.textContent = title;
@@ -464,6 +507,47 @@ export class FileActionController {
464
507
  }
465
508
  }
466
509
 
510
+ async uploadVaultFiles(files = [], { parentDir = '' } = {}) {
511
+ const normalizedParentDir = normalizeVaultPathInput(parentDir);
512
+ let uploaded = false;
513
+
514
+ for (const file of Array.from(files ?? [])) {
515
+ const fileName = getVaultPathLeaf(file?.name);
516
+ const filePath = composeVaultChildPath(normalizedParentDir, fileName);
517
+ if (!fileName || !isVaultFilePath(filePath)) {
518
+ this.showToast(`Unsupported file type: ${fileName || 'unnamed file'}`);
519
+ continue;
520
+ }
521
+
522
+ try {
523
+ await this.runWorkspaceMutation((requestId) => this.vaultClient.uploadFile({
524
+ file,
525
+ path: filePath,
526
+ requestId,
527
+ }));
528
+ uploaded = true;
529
+ this.state.expandDirectoryPath(filePath, { includeLeaf: false });
530
+ } catch (error) {
531
+ this.showToast(`${fileName}: ${error?.message || 'Failed to upload file'}`);
532
+ }
533
+ }
534
+
535
+ if (uploaded) {
536
+ await this.refresh();
537
+ }
538
+
539
+ return uploaded;
540
+ }
541
+
542
+ async handleUploadFiles({ parentDir = '' } = {}) {
543
+ const files = await pickFiles({ accept: VAULT_FILE_PICKER_ACCEPT, multiple: true });
544
+ if (files.length === 0) {
545
+ return false;
546
+ }
547
+
548
+ return this.uploadVaultFiles(files, { parentDir });
549
+ }
550
+
467
551
  async createVaultFile(filePath, content, { openAfterCreate = false, errorMessage = 'Failed to create file' } = {}) {
468
552
  try {
469
553
  await this.runWorkspaceMutation((requestId) => this.vaultClient.createFile(withRequestId({
@@ -716,6 +800,7 @@ export class FileActionController {
716
800
 
717
801
  openCreateMenu({ anchor = this.createButton, parentDir = '' } = {}) {
718
802
  this.view?.removeContextMenu?.();
803
+ this.fileExplorerOptionsMenu.close({ restoreFocus: false });
719
804
  this.createMenu.toggle({
720
805
  anchor,
721
806
  items: this.getCreateActions({ parentDir }),
@@ -723,6 +808,25 @@ export class FileActionController {
723
808
  });
724
809
  }
725
810
 
811
+ openFileExplorerOptionsMenu({ anchor = this.fileExplorerOptionsButton } = {}) {
812
+ this.view?.removeContextMenu?.();
813
+ this.createMenu.close({ restoreFocus: false });
814
+ this.fileExplorerOptionsMenu.toggle({
815
+ anchor,
816
+ items: [{
817
+ icon: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12Z"></path><circle cx="12" cy="12" r="2.5"></circle></svg>',
818
+ id: 'show-file-extensions',
819
+ label: 'Show file extensions',
820
+ meta: this.showFileExtensions ? 'On' : 'Off',
821
+ onSelect: () => {
822
+ this.showFileExtensions = !this.showFileExtensions;
823
+ this.onShowFileExtensionsChange?.(this.showFileExtensions);
824
+ },
825
+ }],
826
+ title: 'File explorer options',
827
+ });
828
+ }
829
+
726
830
  handleNewFile({ parentDir = '' } = {}) {
727
831
  const context = this.getCreateContext(parentDir);
728
832
 
@@ -921,6 +1025,36 @@ export class FileActionController {
921
1025
  });
922
1026
  }
923
1027
 
1028
+ handleNewStructurizr({ parentDir = '' } = {}) {
1029
+ const context = this.getCreateContext(parentDir);
1030
+
1031
+ this.openActionDialog({
1032
+ title: 'Create Structurizr workspace',
1033
+ copy: context.normalizedParentDir
1034
+ ? 'Create a collaborative C4 workspace inside the selected folder.'
1035
+ : 'Create a collaborative C4 workspace in the vault.',
1036
+ label: `Workspace ${context.inputLabelSuffix}`,
1037
+ hint: `${context.hintPrefix} ".dsl" is added automatically. workspace.dsl is conventional; any workspace .dsl file can render.`,
1038
+ note: context.note,
1039
+ placeholder: 'workspace',
1040
+ submitLabel: 'Create workspace',
1041
+ emptyMessage: 'Workspace path is required',
1042
+ onSubmit: (value) => {
1043
+ const normalizedPath = normalizeVaultPathInput(value);
1044
+ if (!normalizedPath) {
1045
+ this.showToast('Workspace path is required');
1046
+ return false;
1047
+ }
1048
+
1049
+ const starter = createStructurizrStarter(composeVaultChildPath(context.normalizedParentDir, normalizedPath));
1050
+ return this.createVaultFile(starter.path, starter.content, {
1051
+ errorMessage: 'Failed to create Structurizr workspace',
1052
+ openAfterCreate: true,
1053
+ });
1054
+ },
1055
+ });
1056
+ }
1057
+
924
1058
  handleRenameFile(filePath) {
925
1059
  const extension = getVaultFileExtension(filePath) || '.md';
926
1060
 
@@ -9,14 +9,18 @@ export class FileExplorerController {
9
9
  onFileSelect,
10
10
  onFileDelete,
11
11
  pendingWorkspaceRequestIds = null,
12
+ onShowFileExtensionsChange,
13
+ showFileExtensions = false,
12
14
  toastController,
13
15
  vaultClient,
14
16
  }) {
15
17
  this.onFileSelect = onFileSelect;
16
18
  this.onFileDelete = onFileDelete;
19
+ this.onShowFileExtensionsChange = onShowFileExtensionsChange;
17
20
  this.toastController = toastController;
18
21
  this.vaultClient = vaultClient;
19
22
  this.state = new FileTreeState();
23
+ this.showFileExtensions = Boolean(showFileExtensions);
20
24
  this.threadCounts = new Map();
21
25
  this.view = new FileExplorerView({
22
26
  mobileBreakpointQuery,
@@ -27,7 +31,12 @@ export class FileExplorerController {
27
31
  },
28
32
  onDirectoryToggle: (pathValue) => {
29
33
  this.state.toggleDirectory(pathValue);
30
- this.renderTree();
34
+ if (!this.view.rerenderDirectoryBranch(pathValue, this.state.tree, {
35
+ activeFilePath: this.state.activeFilePath,
36
+ expandedDirs: this.state.expandedDirs,
37
+ })) {
38
+ this.renderTree();
39
+ }
31
40
  },
32
41
  onEntryDrop: (payload) => this.actionController.moveEntryByDrop(payload),
33
42
  onFileContextMenu: (event, payload) => {
@@ -54,7 +63,13 @@ export class FileExplorerController {
54
63
  mobileBreakpointQuery,
55
64
  onFileDelete: this.onFileDelete,
56
65
  onFileSelect: this.onFileSelect,
66
+ onShowFileExtensionsChange: (enabled) => {
67
+ this.showFileExtensions = Boolean(enabled);
68
+ this.onShowFileExtensionsChange?.(this.showFileExtensions);
69
+ this.renderTree();
70
+ },
57
71
  pendingWorkspaceRequestIds,
72
+ showFileExtensions: this.showFileExtensions,
58
73
  refresh: () => this.refresh(),
59
74
  state: this.state,
60
75
  toastController: this.toastController,
@@ -86,15 +101,25 @@ export class FileExplorerController {
86
101
  }
87
102
 
88
103
  setActiveFile(filePath) {
104
+ const expandedDirectoryCount = this.state.expandedDirs.size;
89
105
  this.state.setActiveFile(filePath);
90
- this.renderTree();
106
+ if (this.state.searchQuery) {
107
+ this.view.setActiveFile(filePath);
108
+ return;
109
+ }
110
+ if (
111
+ this.state.expandedDirs.size !== expandedDirectoryCount
112
+ || !this.view.setActiveFile(filePath)
113
+ ) {
114
+ this.renderTree();
115
+ }
91
116
  }
92
117
 
93
118
  setThreadCounts(threadCounts = new Map()) {
94
119
  this.threadCounts = threadCounts instanceof Map
95
120
  ? new Map(threadCounts)
96
121
  : new Map(Object.entries(threadCounts ?? {}));
97
- this.renderTree();
122
+ this.view.setThreadCounts(this.threadCounts);
98
123
  }
99
124
 
100
125
  revealFile(filePath, { clearSearch = false } = {}) {
@@ -111,6 +136,10 @@ export class FileExplorerController {
111
136
  return this.state.flatFiles;
112
137
  }
113
138
 
139
+ get fileEntries() {
140
+ return this.state.flatFileEntries;
141
+ }
142
+
114
143
  get flatDocumentFiles() {
115
144
  return this.state.flatFiles.filter((path) => !isImageAttachmentFilePath(path));
116
145
  }
@@ -126,6 +155,7 @@ export class FileExplorerController {
126
155
  reset,
127
156
  searchMatches: this.state.getSearchMatches(),
128
157
  searchQuery: this.state.searchQuery,
158
+ showFileExtensions: this.showFileExtensions,
129
159
  threadCounts: this.threadCounts,
130
160
  tree: this.state.tree,
131
161
  });
@@ -5,6 +5,7 @@ import {
5
5
  import { escapeHtml } from '../domain/vault-utils.js';
6
6
  import { getVaultPathLeaf, getVaultPathParent } from '../domain/vault-paths.js';
7
7
  import { buttonClassNames } from './components/ui/button.js';
8
+ import { getVaultFileIconSvg } from './file-icon-svg.js';
8
9
 
9
10
  function findNodeByPath(nodes = [], pathValue = '') {
10
11
  for (const node of nodes) {
@@ -52,6 +53,7 @@ export class FileExplorerView {
52
53
  this.searchInput = document.getElementById('fileSearchInput');
53
54
  this.renderedDirectoryWrappers = new Map();
54
55
  this.renderedChildContainers = new Map();
56
+ this.renderedFileItems = new Map();
55
57
  this.lastRenderMode = 'tree';
56
58
  this.longPressTimer = 0;
57
59
  this.longPressContext = null;
@@ -66,6 +68,7 @@ export class FileExplorerView {
66
68
  this.autoExpandTargetPath = '';
67
69
  this.rootDropZone = null;
68
70
  this.threadCounts = new Map();
71
+ this.showFileExtensions = false;
69
72
  }
70
73
 
71
74
  initialize() {
@@ -120,17 +123,51 @@ export class FileExplorerView {
120
123
  }
121
124
 
122
125
  revealFile(filePath) {
123
- const fileItem = Array.from(this.treeContainer?.querySelectorAll('.file-tree-file') ?? [])
124
- .find((element) => element.dataset.path === filePath);
125
- fileItem?.scrollIntoView({ block: 'nearest' });
126
+ this.renderedFileItems.get(filePath)?.scrollIntoView({ block: 'nearest' });
126
127
  }
127
128
 
128
- render({ activeFilePath, changedPaths = null, expandedDirs, reset = false, searchMatches, searchQuery, threadCounts = new Map(), tree }) {
129
+ setActiveFile(filePath) {
130
+ this.treeContainer?.querySelector('.file-tree-file.active')?.classList.remove('active');
131
+ if (!filePath) {
132
+ return true;
133
+ }
134
+
135
+ const fileItem = this.renderedFileItems.get(filePath);
136
+ fileItem?.classList.add('active');
137
+ return Boolean(fileItem);
138
+ }
139
+
140
+ setThreadCounts(threadCounts) {
141
+ this.threadCounts = threadCounts instanceof Map ? threadCounts : new Map();
142
+ this.renderedFileItems.forEach((button, filePath) => {
143
+ const threadCount = Number(this.threadCounts.get(filePath) ?? 0) || 0;
144
+ let badge = button.querySelector('.file-tree-comment-count');
145
+ button.classList.toggle('has-comments', threadCount > 0);
146
+
147
+ if (threadCount <= 0) {
148
+ delete button.dataset.threadCount;
149
+ badge?.remove();
150
+ return;
151
+ }
152
+
153
+ button.dataset.threadCount = String(threadCount);
154
+ if (!badge) {
155
+ badge = document.createElement('span');
156
+ badge.className = 'file-tree-comment-count';
157
+ button.appendChild(badge);
158
+ }
159
+ badge.textContent = String(threadCount);
160
+ badge.setAttribute('aria-label', `${threadCount} open comment thread${threadCount === 1 ? '' : 's'}`);
161
+ });
162
+ }
163
+
164
+ render({ activeFilePath, changedPaths = null, expandedDirs, reset = false, searchMatches, searchQuery, showFileExtensions = false, threadCounts = new Map(), tree }) {
129
165
  if (!this.treeContainer) {
130
166
  return;
131
167
  }
132
168
 
133
169
  this.threadCounts = threadCounts instanceof Map ? threadCounts : new Map();
170
+ this.showFileExtensions = Boolean(showFileExtensions);
134
171
  this.currentSearchQuery = String(searchQuery ?? '');
135
172
  if (this.currentSearchQuery) {
136
173
  this.clearDragFeedback();
@@ -248,6 +285,7 @@ export class FileExplorerView {
248
285
  resetTreeIndexes() {
249
286
  this.renderedDirectoryWrappers.clear();
250
287
  this.renderedChildContainers.clear();
288
+ this.renderedFileItems.clear();
251
289
  }
252
290
 
253
291
  renderNodes(nodes, container, { activeFilePath, depth, expandedDirs }) {
@@ -408,6 +446,11 @@ export class FileExplorerView {
408
446
  this.renderedChildContainers.delete(pathValue);
409
447
  }
410
448
  });
449
+ Array.from(this.renderedFileItems.keys()).forEach((pathValue) => {
450
+ if (pathValue.startsWith(prefix)) {
451
+ this.renderedFileItems.delete(pathValue);
452
+ }
453
+ });
411
454
  }
412
455
 
413
456
  createFileItem({ activeFilePath, depth, filePath, fileType = 'file', name }) {
@@ -418,8 +461,10 @@ export class FileExplorerView {
418
461
  const isExcalidraw = fileType === 'excalidraw';
419
462
  const isBase = fileType === 'base';
420
463
  const isImage = fileType === 'image';
464
+ const isPdf = fileType === 'pdf';
421
465
  const isMermaid = fileType === 'mermaid';
422
466
  const isPlantUml = fileType === 'plantuml';
467
+ const isStructurizr = fileType === 'structurizr';
423
468
 
424
469
  if (isBase) {
425
470
  button.classList.add('is-base');
@@ -433,12 +478,18 @@ export class FileExplorerView {
433
478
  if (isImage) {
434
479
  button.classList.add('is-image');
435
480
  }
481
+ if (isPdf) {
482
+ button.classList.add('is-pdf');
483
+ }
436
484
  if (isMermaid) {
437
485
  button.classList.add('is-mermaid');
438
486
  }
439
487
  if (isPlantUml) {
440
488
  button.classList.add('is-plantuml');
441
489
  }
490
+ if (isStructurizr) {
491
+ button.classList.add('is-structurizr');
492
+ }
442
493
  if (filePath === activeFilePath) {
443
494
  button.classList.add('active');
444
495
  }
@@ -453,9 +504,10 @@ export class FileExplorerView {
453
504
  if (threadCount > 0) {
454
505
  button.dataset.threadCount = String(threadCount);
455
506
  }
507
+ const displayName = this.showFileExtensions ? String(name ?? '') : stripVaultFileExtension(name);
456
508
  button.innerHTML = `
457
- ${this.getFileIconSvg({ isBase, isDrawio, isExcalidraw, isImage, isMermaid, isPlantUml })}
458
- <span class="file-tree-name">${escapeHtml(stripVaultFileExtension(name))}</span>
509
+ ${getVaultFileIconSvg(filePath)}
510
+ <span class="file-tree-name">${escapeHtml(displayName)}</span>
459
511
  ${threadCount > 0 ? `<span class="file-tree-comment-count" aria-label="${threadCount} open comment thread${threadCount === 1 ? '' : 's'}">${threadCount}</span>` : ''}
460
512
  `;
461
513
  this.configureDragSource(button, { path: filePath, type: 'file' });
@@ -474,6 +526,7 @@ export class FileExplorerView {
474
526
  this.onFileContextMenu?.(this.createLongPressEvent(button), { filePath, type: 'file' });
475
527
  });
476
528
 
529
+ this.renderedFileItems.set(filePath, button);
477
530
  return button;
478
531
  }
479
532
 
@@ -908,34 +961,6 @@ export class FileExplorerView {
908
961
  return true;
909
962
  }
910
963
 
911
- getFileIconSvg({ isBase, isDrawio, isExcalidraw, isImage, isMermaid, isPlantUml }) {
912
- if (isBase) {
913
- return '<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4h7l5 5v11a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"/><path d="M14 4v5h5"/><path d="M8 13h8"/><path d="M8 17h6"/><path d="M8 9h3"/></svg>';
914
- }
915
-
916
- if (isDrawio) {
917
- return '<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="7" height="6" rx="1"/><rect x="14" y="4" width="7" height="6" rx="1"/><rect x="8.5" y="14" width="7" height="6" rx="1"/><path d="M10 7h4"/><path d="M17.5 10v2.5"/><path d="M6.5 10v2.5"/><path d="M6.5 12.5h11"/></svg>';
918
- }
919
-
920
- if (isExcalidraw) {
921
- return '<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg>';
922
- }
923
-
924
- if (isImage) {
925
- return '<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="9" cy="10" r="1.5"/><path d="m21 16-5-5L7 20"/><path d="m14 14 2 2"/></svg>';
926
- }
927
-
928
- if (isMermaid) {
929
- return '<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 7.5c0-1.38 1.12-2.5 2.5-2.5 1.04 0 1.93.64 2.3 1.56A2.5 2.5 0 0 1 14 8.5v1"/><path d="M19 16.5c0 1.38-1.12 2.5-2.5 2.5-1.04 0-1.93-.64-2.3-1.56A2.5 2.5 0 0 1 10 15.5v-1"/><path d="M8 10.5h8"/><path d="M8 13.5h8"/><path d="M10 8.5v7"/><path d="M14 8.5v7"/></svg>';
930
- }
931
-
932
- if (isPlantUml) {
933
- return '<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="7" height="6" rx="1"/><rect x="14" y="4" width="7" height="6" rx="1"/><rect x="8.5" y="14" width="7" height="6" rx="1"/><path d="M10 7h4"/><path d="M17.5 10v2.5"/><path d="M6.5 10v2.5"/><path d="M6.5 12.5h11"/></svg>';
934
- }
935
-
936
- return '<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
937
- }
938
-
939
964
  showContextMenu(event, items) {
940
965
  this.removeContextMenu();
941
966
 
@@ -0,0 +1,40 @@
1
+ import { getVaultFileKind } from '../../domain/file-kind.js';
2
+
3
+ const SVG_ATTRIBUTES = 'width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
4
+
5
+ function createIcon(className, content) {
6
+ return `<svg class="${className}" ${SVG_ATTRIBUTES}>${content}</svg>`;
7
+ }
8
+
9
+ export function getVaultFileIconSvg(filePath, {
10
+ className = 'file-tree-icon',
11
+ } = {}) {
12
+ const kind = getVaultFileKind(filePath) ?? 'file';
13
+ const iconClass = className;
14
+
15
+ if (kind === 'base') {
16
+ return createIcon(iconClass, '<path d="M7 4h7l5 5v11a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"/><path d="M14 4v5h5"/><path d="M8 13h8"/><path d="M8 17h6"/><path d="M8 9h3"/>');
17
+ }
18
+
19
+ if (kind === 'drawio' || kind === 'plantuml' || kind === 'structurizr') {
20
+ return createIcon(iconClass, '<rect x="3" y="4" width="7" height="6" rx="1"/><rect x="14" y="4" width="7" height="6" rx="1"/><rect x="8.5" y="14" width="7" height="6" rx="1"/><path d="M10 7h4"/><path d="M17.5 10v2.5"/><path d="M6.5 10v2.5"/><path d="M6.5 12.5h11"/>');
21
+ }
22
+
23
+ if (kind === 'excalidraw') {
24
+ return createIcon(iconClass, '<path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/>');
25
+ }
26
+
27
+ if (kind === 'image') {
28
+ return createIcon(iconClass, '<rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="9" cy="10" r="1.5"/><path d="m21 16-5-5L7 20"/><path d="m14 14 2 2"/>');
29
+ }
30
+
31
+ if (kind === 'pdf') {
32
+ return createIcon(iconClass, '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><path d="M8 15h8"/><path d="M8 18h5"/>');
33
+ }
34
+
35
+ if (kind === 'mermaid') {
36
+ return createIcon(iconClass, '<path d="M5 7.5c0-1.38 1.12-2.5 2.5-2.5 1.04 0 1.93.64 2.3 1.56A2.5 2.5 0 0 1 14 8.5v1"/><path d="M19 16.5c0 1.38-1.12 2.5-2.5 2.5-1.04 0-1.93-.64-2.3-1.56A2.5 2.5 0 0 1 10 15.5v-1"/><path d="M8 10.5h8"/><path d="M8 13.5h8"/><path d="M10 8.5v7"/><path d="M14 8.5v7"/>');
37
+ }
38
+
39
+ return createIcon(iconClass, '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>');
40
+ }
@@ -1,6 +1,6 @@
1
1
  import { normalizeVaultPathInput } from '../domain/vault-paths.js';
2
2
 
3
- function flattenTree(nodes, files = [], searchEntries = []) {
3
+ function flattenTree(nodes, files = [], searchEntries = [], fileEntries = []) {
4
4
  for (const node of nodes) {
5
5
  if (!node?.path || !node?.type) {
6
6
  continue;
@@ -20,18 +20,24 @@ function flattenTree(nodes, files = [], searchEntries = []) {
20
20
  || node.type === 'drawio'
21
21
  || node.type === 'mermaid'
22
22
  || node.type === 'plantuml'
23
+ || node.type === 'structurizr'
24
+ || node.type === 'pdf'
23
25
  || node.type === 'image'
24
26
  ) {
25
27
  files.push(node.path);
28
+ fileEntries.push({
29
+ mtimeMs: node.mtimeMs,
30
+ path: node.path,
31
+ });
26
32
  continue;
27
33
  }
28
34
 
29
35
  if (node.type === 'directory' && Array.isArray(node.children)) {
30
- flattenTree(node.children, files, searchEntries);
36
+ flattenTree(node.children, files, searchEntries, fileEntries);
31
37
  }
32
38
  }
33
39
 
34
- return { files, searchEntries };
40
+ return { fileEntries, files, searchEntries };
35
41
  }
36
42
 
37
43
  function findNodeByPath(nodes, pathValue) {
@@ -82,6 +88,7 @@ export class FileTreeState {
82
88
  constructor() {
83
89
  this.tree = [];
84
90
  this.flatFiles = [];
91
+ this.flatFileEntries = [];
85
92
  this.flatSearchEntries = [];
86
93
  this.activeFilePath = null;
87
94
  this.expandedDirs = new Set();
@@ -90,8 +97,9 @@ export class FileTreeState {
90
97
 
91
98
  setTree(tree) {
92
99
  this.tree = Array.isArray(tree) ? tree : [];
93
- const flattened = flattenTree(this.tree, [], []);
100
+ const flattened = flattenTree(this.tree, [], [], []);
94
101
  this.flatFiles = flattened.files;
102
+ this.flatFileEntries = flattened.fileEntries;
95
103
  this.flatSearchEntries = flattened.searchEntries;
96
104
  }
97
105