collabmd 0.1.45 → 0.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/README.md +39 -19
  2. package/bin/collabmd.js +27 -1
  3. package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-CYM5nxUi.js +1 -0
  4. package/dist/client/assets/browser-utils--H_DkyEF.js +1 -0
  5. package/dist/client/assets/chunk-ERSWUTIN-hHdvnhaY.js +41 -0
  6. package/dist/client/assets/comment-thread-store-CdcwrObc.js +1 -0
  7. package/dist/client/assets/comment-threads-9J6Em-_y.js +4 -0
  8. package/dist/client/assets/comment-ui-shared-BC4pt4w8.js +7 -0
  9. package/dist/client/assets/dist-B5HTH2BO.js +1 -0
  10. package/dist/client/assets/{dist-DhLvC8DS.js → dist-B5Jf1SLZ.js} +2 -2
  11. package/dist/client/assets/{dist-CnL7Y2sM.js → dist-BDt3rz7l.js} +1 -1
  12. package/dist/client/assets/dist-BEwK4csK.js +1 -0
  13. package/dist/client/assets/dist-BKW4PALF.js +1 -0
  14. package/dist/client/assets/dist-BWcHHOx-.js +1 -0
  15. package/dist/client/assets/{dist-CYZmbokb.js → dist-BalScOwZ.js} +9 -9
  16. package/dist/client/assets/dist-Bf8f4HmM.js +6 -0
  17. package/dist/client/assets/dist-BjHJDtyU.js +1 -0
  18. package/dist/client/assets/dist-Bm6pf9Bl.js +1 -0
  19. package/dist/client/assets/dist-BoVBdSjk.js +1 -0
  20. package/dist/client/assets/dist-CC5IrQMG.js +1 -0
  21. package/dist/client/assets/{dist-C11hHqv_.js → dist-CCP_AKPG.js} +12 -12
  22. package/dist/client/assets/dist-CZgOmxK6.js +1 -0
  23. package/dist/client/assets/{dist-D2AQ-76g.js → dist-Cqhv5LAk.js} +1 -1
  24. package/dist/client/assets/{dist-MvcV4190.js → dist-D-Z6FLem.js} +1 -1
  25. package/dist/client/assets/dist-DLMiIsNC.js +11 -0
  26. package/dist/client/assets/{dist-3bgCp7iY.js → dist-DZKu9Cs8.js} +1 -1
  27. package/dist/client/assets/dist-DaYdhcVs.js +1 -0
  28. package/dist/client/assets/dist-FWJsgGYI.js +1 -0
  29. package/dist/client/assets/dist-Kh2COra_.js +1 -0
  30. package/dist/client/assets/{dist-JYEDRrPT.js → dist-PDZ3bSqm.js} +1 -1
  31. package/dist/client/assets/{dist-Drp8uu5J.js → dist-TjMh8n1u.js} +1 -1
  32. package/dist/client/assets/{dist-DGb27Dl6.js → dist-dtAdgL39.js} +1 -1
  33. package/dist/client/assets/dist-jv4WdPnK.js +9 -0
  34. package/dist/client/assets/{dist-DK5Gs2wa.js → dist-rGBmqxC7.js} +1 -1
  35. package/dist/client/assets/{dist-DCzuX1AF.js → dist-shrl6K_0.js} +1 -1
  36. package/dist/client/assets/dist-tOaegDYS2.js +1 -0
  37. package/dist/client/assets/dist-ule2n6ak.js +1 -0
  38. package/dist/client/assets/dist-xElaxbia.js +1 -0
  39. package/dist/client/assets/{drawio-editor-ClE8jpoG.js → drawio-editor-FzY1cVw8.js} +1 -1
  40. package/dist/client/assets/drawioEditor-BjiKJqlc.js +2 -0
  41. package/dist/client/assets/editor-session-Cm9ekeNo.js +54 -0
  42. package/dist/client/assets/embedded-editor-base-CwTkk911.css +1 -0
  43. package/dist/client/assets/{preview-render-profile-BZzGSg6K.js → es-DXGV3RHd.js} +13 -13
  44. package/dist/client/assets/excalidraw-editor-B46zsTFI.js +9 -0
  45. package/dist/client/assets/{excalidraw-editor-CyGwOhLz.css → excalidraw-editor-Bt7gMSoh.css} +1 -1
  46. package/dist/client/assets/excalidraw-scene-BOben2gr.js +1 -0
  47. package/dist/client/assets/excalidrawEditor-CT2pHq7e.js +2 -0
  48. package/dist/client/assets/{export-page-CUfws2T-.js → export-page-A7NHawJY.js} +3 -3
  49. package/dist/client/assets/exportDocument-B2L8fe34.css +1 -0
  50. package/dist/client/assets/exportDocument-BaqcOAAW.js +2 -0
  51. package/dist/client/assets/file-icon-svg-CQAjlWjK.js +1 -0
  52. package/dist/client/assets/file-kind-BbqgGzpc.js +1 -0
  53. package/dist/client/assets/image-QXRYHUNP-SWEV8H0J.js +1 -0
  54. package/dist/client/assets/index-ClbjMBVt.js +2 -0
  55. package/dist/client/assets/index-Pum8ET-5.css +1 -0
  56. package/dist/client/assets/main-B3uPyqic.js +893 -0
  57. package/dist/client/assets/pdf-BL9Pr2D5.js +54 -0
  58. package/dist/client/assets/pdf.worker-CLesOks4.mjs +64857 -0
  59. package/dist/client/assets/pdf_viewer-FVkkThnz.js +551 -0
  60. package/dist/client/assets/{preview-render-compiler-BSaQO_p3.js → preview-render-compiler-BD-XhkS2.js} +2 -2
  61. package/dist/client/assets/preview-render-compiler-BKedaMxx.js +1 -0
  62. package/dist/client/assets/preview-render-profile-CE-5rVTC.js +1 -0
  63. package/dist/client/assets/{preview-render-worker-B42LWvrD.js → preview-render-worker-CiPqc9n6.js} +20 -20
  64. package/dist/client/assets/prod-Dy20X7eT.js +1 -0
  65. package/dist/client/assets/prod-hnwh7wRM.js +177 -0
  66. package/dist/client/assets/quick-switcher-controller-fqOYR7qR.js +12 -0
  67. package/dist/client/assets/runtime-config-BLrEKVO1.js +1 -0
  68. package/dist/client/assets/vault-api-client-_LpV1aIn.js +6 -0
  69. package/dist/client/drawio-editor.html +1 -1
  70. package/dist/client/excalidraw-editor.html +1 -1
  71. package/dist/client/export-document.html +2 -2
  72. package/dist/client/index.html +15 -3
  73. package/docker-compose.yml +42 -0
  74. package/package.json +17 -4
  75. package/scripts/local-structurizr-compose.mjs +101 -0
  76. package/src/client/app/index.html +13 -1
  77. package/src/client/application/app-shell/comments-feature.js +19 -10
  78. package/src/client/application/app-shell/git-feature.js +12 -0
  79. package/src/client/application/app-shell/presence-feature.js +2 -1
  80. package/src/client/application/app-shell/ui-feature-shell.js +45 -2
  81. package/src/client/application/app-shell/ui-feature-toolbar.js +2 -52
  82. package/src/client/application/app-shell/workspace-feature.js +5 -1
  83. package/src/client/application/app-shell-elements.js +2 -0
  84. package/src/client/application/pdf-preview-controller.js +992 -0
  85. package/src/client/application/structurizr-preview-controller.js +395 -0
  86. package/src/client/application/workspace-coordinator.js +25 -3
  87. package/src/client/application/workspace-preview-controller.js +91 -2
  88. package/src/client/bootstrap/collabmd-app-shell.js +31 -5
  89. package/src/client/browser-utils.js +44 -0
  90. package/src/client/domain/runtime-paths.js +26 -5
  91. package/src/client/domain/structurizr-language.js +99 -0
  92. package/src/client/domain/vault-paths.js +48 -0
  93. package/src/client/excalidraw-editor.js +833 -51
  94. package/src/client/infrastructure/browser-preferences-port.js +10 -0
  95. package/src/client/infrastructure/comment-thread-store.js +34 -13
  96. package/src/client/infrastructure/editor-session.js +10 -0
  97. package/src/client/infrastructure/editor-view-adapter.js +46 -2
  98. package/src/client/infrastructure/excalidraw-room-client.js +103 -14
  99. package/src/client/infrastructure/git-api-client.js +5 -0
  100. package/src/client/infrastructure/vault-api-client.js +26 -0
  101. package/src/client/presentation/comment-ui/comment-ui-shared.js +4 -0
  102. package/src/client/presentation/drawio-embed-controller.js +9 -102
  103. package/src/client/presentation/drawio-viewer.js +118 -0
  104. package/src/client/presentation/excalidraw-embed-controller.js +74 -8
  105. package/src/client/presentation/file-action-controller.js +102 -0
  106. package/src/client/presentation/file-explorer-view.js +10 -29
  107. package/src/client/presentation/file-icon-svg.js +40 -0
  108. package/src/client/presentation/file-tree-state.js +2 -0
  109. package/src/client/presentation/git-diff-view-controller.js +731 -3
  110. package/src/client/presentation/outline-controller.js +86 -0
  111. package/src/client/presentation/quick-switcher-controller.js +25 -13
  112. package/src/client/presentation/quick-switcher-text-search.js +1 -1
  113. package/src/client/styles/components/navigation.css +11 -1
  114. package/src/client/styles/features/diagram-preview.css +76 -0
  115. package/src/client/styles/features/diff-special.css +309 -0
  116. package/src/client/styles/foundation/themes.css +6 -0
  117. package/src/client/styles/layout/shell.css +358 -1
  118. package/src/client/styles/layout/view-modes.css +1 -0
  119. package/src/client/styles/style.css +1 -0
  120. package/src/client/styles/surfaces/excalidraw-editor.css +294 -0
  121. package/src/domain/comment-threads.js +63 -5
  122. package/src/domain/excalidraw-room-codec.js +6 -4
  123. package/src/domain/file-kind.js +35 -3
  124. package/src/server/config/env.js +79 -0
  125. package/src/server/create-app-server.js +12 -0
  126. package/src/server/domain/bases/base-definition.js +1 -1
  127. package/src/server/domain/bases/base-expression-runtime.js +1 -4
  128. package/src/server/domain/bases/base-index-snapshot-store.js +1 -4
  129. package/src/server/domain/bases/base-transform.js +1 -4
  130. package/src/server/domain/collaboration/collaboration-room.js +55 -21
  131. package/src/server/domain/comment-overview.js +15 -5
  132. package/src/server/domain/ripgrep-search-service.js +4 -0
  133. package/src/server/infrastructure/git/diff-service.js +5 -0
  134. package/src/server/infrastructure/git/git-service.js +21 -0
  135. package/src/server/infrastructure/git/history-service.js +54 -0
  136. package/src/server/infrastructure/git/image-mime.js +14 -0
  137. package/src/server/infrastructure/git/untracked-files.js +28 -1
  138. package/src/server/infrastructure/http/create-git-api-query-handler.js +31 -1
  139. package/src/server/infrastructure/http/create-request-handler.js +16 -23
  140. package/src/server/infrastructure/http/create-static-handler.js +5 -2
  141. package/src/server/infrastructure/http/create-structurizr-api-handler.js +145 -0
  142. package/src/server/infrastructure/http/create-vault-api-command-handler.js +66 -4
  143. package/src/server/infrastructure/http/create-vault-api-query-handler.js +51 -3
  144. package/src/server/infrastructure/http/http-request-helpers.js +22 -0
  145. package/src/server/infrastructure/persistence/path-utils.js +1 -1
  146. package/src/server/infrastructure/persistence/vault-file-store.js +3 -0
  147. package/src/server/infrastructure/structurizr/structurizr-embed.css +43 -0
  148. package/src/server/infrastructure/structurizr/structurizr-workspace-service.js +374 -0
  149. package/src/server/infrastructure/websocket/attach-collaboration-gateway.js +4 -22
  150. package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-CbSISC1w.js +0 -1
  151. package/dist/client/assets/browser-utils-CKtI_rZl.js +0 -1
  152. package/dist/client/assets/chunk-ERSWUTIN-ME9RVNel.js +0 -41
  153. package/dist/client/assets/comment-threads-DDegmaFO.js +0 -4
  154. package/dist/client/assets/dist-BH87JoHx.js +0 -11
  155. package/dist/client/assets/dist-BIkPRZO7.js +0 -6
  156. package/dist/client/assets/dist-BqCsFpx4.js +0 -1
  157. package/dist/client/assets/dist-C2JHCt8U.js +0 -1
  158. package/dist/client/assets/dist-CZjTtmTu.js +0 -1
  159. package/dist/client/assets/dist-Cig4Ov_X.js +0 -1
  160. package/dist/client/assets/dist-CqQEC8gR.js +0 -1
  161. package/dist/client/assets/dist-D4Rzqod1.js +0 -1
  162. package/dist/client/assets/dist-D5sn-mwk.js +0 -9
  163. package/dist/client/assets/dist-D7G-n87v.js +0 -1
  164. package/dist/client/assets/dist-DRqOrNgN.js +0 -1
  165. package/dist/client/assets/dist-DetkMS_4.js +0 -1
  166. package/dist/client/assets/dist-Dp6FVkQ1.js +0 -1
  167. package/dist/client/assets/dist-DtbjNB28.js +0 -1
  168. package/dist/client/assets/dist-DyrYcvpo2.js +0 -1
  169. package/dist/client/assets/dist-GrygD2zE.js +0 -1
  170. package/dist/client/assets/dist-HmEpWUow.js +0 -1
  171. package/dist/client/assets/dist-RTa81Km7.js +0 -1
  172. package/dist/client/assets/drawioEditor-CXaTM9Ds.js +0 -2
  173. package/dist/client/assets/editor-session-GTwZS6Df.js +0 -16
  174. package/dist/client/assets/embedded-editor-base-CiNEc8vy.css +0 -1
  175. package/dist/client/assets/excalidraw-editor-BZVM81BO.js +0 -9
  176. package/dist/client/assets/excalidraw-scene-C8n9CHsK.js +0 -176
  177. package/dist/client/assets/excalidrawEditor-DxJanCze.js +0 -2
  178. package/dist/client/assets/exportDocument-BCuxqXDY.js +0 -2
  179. package/dist/client/assets/exportDocument-DV-OX74E.css +0 -1
  180. package/dist/client/assets/file-kind-BKPOIRPE.js +0 -1
  181. package/dist/client/assets/image-QXRYHUNP-lfTHE3bb.js +0 -1
  182. package/dist/client/assets/index-C3YnUgMD.css +0 -1
  183. package/dist/client/assets/index-D2wXraJ1.js +0 -2
  184. package/dist/client/assets/main-DtiGz7uI.js +0 -795
  185. package/dist/client/assets/preview-render-compiler-csGM5ZFz.js +0 -1
  186. package/dist/client/assets/quick-switcher-controller-shW_6vPY.js +0 -12
  187. package/dist/client/assets/room-CQLoqtMo.js +0 -1
  188. package/dist/client/assets/runtime-config-DSGmGE9V.js +0 -1
  189. package/dist/client/assets/vault-api-client-C6bcGwy-.js +0 -5
  190. package/src/server/domain/collaboration/room-persistence-controller.js +0 -80
@@ -0,0 +1,12 @@
1
+ import{g as e}from"./file-kind-BbqgGzpc.js";import{n as t}from"./vault-utils-wdQ0RNR9.js";import{t as n}from"./file-icon-svg-CQAjlWjK.js";var r=50;function i(e=0,{truncated:t=!1}={}){let n=Number(e)||0;return`${n}${t?`+`:``} ${n===1?`match`:`matches`}`}function a(e={}){let t=[];return(e.files??[]).forEach(e=>{(e.snippets??[]).forEach(n=>{t.push({column:n.column,file:e.file,kind:e.kind,line:n.line,matchLength:Math.max((n.matchEnd??0)-(n.matchStart??0),0),snippet:n})})}),t}var o=class{constructor({debounceMs:e=300,resultLimit:t=r}={}){this.debounceMs=e,this.resultLimit=t,this.controller=null,this.timer=null,this.token=0}abort({invalidate:e=!0}={}){clearTimeout(this.timer),this.timer=null,e&&(this.token+=1),this.controller?.abort(),this.controller=null}schedule({isActive:e=()=>!0,onResults:t,onState:n,query:r=``,searchConfig:i={},searchText:a=null}={}){let o=Math.max(Number(i.minQueryLength)||2,1);if(this.abort(),!i.available){n?.(`Global text search requires ripgrep on the server.`);return}if(!a){n?.(`Global text search is unavailable.`);return}if(r.length<o){n?.(`Type at least ${o} characters to search file text.`);return}n?.(`Searching...`),this.timer=setTimeout(()=>{this.run({isActive:e,onResults:t,onState:n,query:r,searchText:a})},this.debounceMs)}async run({isActive:e=()=>!0,onResults:t,onState:n,query:r=``,searchText:i}={}){let a=this.token+1;this.token=a;let o=new AbortController;this.controller=o;let s=()=>e()&&a===this.token&&this.controller===o;try{let e=await i({limit:this.resultLimit,query:r,signal:o.signal});s()&&t?.(e,r)}catch(e){if(e?.name===`AbortError`||!s())return;n?.(e?.body?.error||e?.message||`Failed to search file text.`)}finally{this.controller===o&&(this.controller=null)}}},s=30,c=2**53-1,l=`quick-switcher-file-`,u=`quick-switcher-text-`;function d(e){return String(e??``).split(`/`).pop()||String(e??``)}function f(t){let n=e(t),r=n.split(`/`).pop()||n,i=d(t);return{displayName:n,fileName:r,filePath:t,lowerDisplayName:n.toLowerCase(),lowerFileName:r.toLowerCase(),lowerPath:String(t).toLowerCase(),lowerRawFileName:i.toLowerCase()}}function p(t){let n=e(t);return n.split(`/`).pop()||n}function m(t){let n=e(t);return n.includes(`/`)?n.substring(0,n.lastIndexOf(`/`)):``}function h(e,t){return Array.from({length:t},(t,n)=>e+n)}function g(e,t){let n=[],r=0,i=0,a=0;for(let o=0;o<e.length&&r<t.length;o+=1){if(e[o]!==t[r]){a=0;continue}r+=1,n.push(o),a+=1,i+=a,(o===0||e[o-1]===`/`||e[o-1]===`-`||e[o-1]===`_`||e[o-1]===` `)&&(i+=5)}return r===t.length&&i>0?{indices:n,score:i}:null}function _(e,t){let n=e.lowerFileName.indexOf(t);if(n>=0)return{indices:h(n+(e.displayName.length-e.fileName.length),t.length),score:100+1/e.fileName.length};let r=e.lowerDisplayName.indexOf(t);if(r>=0)return{indices:h(r,t.length),score:50+1/e.displayName.length};let i=e.lowerRawFileName.indexOf(t);if(i>=0)return{indices:h(i+(e.displayName.length-e.fileName.length),t.length),score:40+1/e.lowerRawFileName.length};let a=e.lowerPath.indexOf(t);return a>=0?{indices:h(a,t.length),score:25+1/e.lowerPath.length}:g(e.lowerDisplayName,t)}function v(e,t=[]){let n=e.displayName.length-e.fileName.length;return{dirPath:t.filter(e=>e<n),fileName:t.filter(e=>e>=n).map(e=>e-n)}}function y(e,n=[]){if(n.length===0)return t(e);let r=new Set(n),i=``,a=null,o=0,s=n=>{i+=t(e.slice(o,a)),i+=`<mark>${t(e.slice(a,n+1))}</mark>`,o=n+1,a=null};for(let t=0;t<e.length;t+=1){if(!r.has(t)){a!==null&&s(t-1);continue}a===null&&(a=t)}return a!==null&&s(e.length-1),i+=t(e.slice(o)),i}var b=class{constructor({getFileList:e,getFileMetadata:t=()=>[],getRecentFiles:n=()=>[],getSearchConfig:r=()=>({}),onFileSelect:i,onTextMatchSelect:a=null,searchDebounceMs:s=300,searchText:c=null}){this.getFileList=e,this.getFileMetadata=t,this.getRecentFiles=n,this.getSearchConfig=r,this.onFileSelect=i,this.onTextMatchSelect=a,this.searchDebounceMs=s,this.searchText=c,this.overlay=document.getElementById(`quickSwitcher`),this.input=document.getElementById(`quickSwitcherInput`),this.resultsList=document.getElementById(`quickSwitcherResults`),this.hint=document.getElementById(`quickSwitcherHint`),this.scope=document.getElementById(`quickSwitcherScope`),this.modeTabs=Array.from(document.querySelectorAll?.(`[data-qs-mode]`)??[]),this.filteredFiles=[],this.fileMatches=new Map,this.fileMatchCount=0,this.fileResultsTruncated=!1,this.fileCorpus=[],this.lastFileListRef=null,this.selectedIndex=0,this.selectedTextIndex=0,this.isOpen=!1,this.previousActiveElement=null,this.mode=`files`,this.textResults=null,this.textResultItems=[],this.textSearchRunner=new o({debounceMs:this.searchDebounceMs}),this.bindEvents()}bindEvents(){this.overlay?.addEventListener(`mousedown`,e=>{e.target===this.overlay&&this.close()}),this.modeTabs.forEach(e=>{e.addEventListener(`click`,()=>{this.setMode(e.dataset.qsMode===`text`?`text`:`files`,{preserveInput:!0})})}),this.input?.addEventListener(`input`,()=>{this.handleInput()}),this.input?.addEventListener(`keydown`,e=>{switch(e.key){case`ArrowDown`:e.preventDefault(),this.moveSelection(1);break;case`ArrowUp`:e.preventDefault(),this.moveSelection(-1);break;case`Enter`:e.preventDefault(),this.confirmSelection();break;case`Escape`:e.preventDefault(),this.close();break;case`Tab`:e.preventDefault(),this.modeTabs.length>1&&!e.shiftKey?this.setMode(this.mode===`files`?`text`:`files`,{preserveInput:!0}):this.moveSelection(e.shiftKey?-1:1)}})}open(){this.overlay&&(this.previousActiveElement=document.activeElement,this.isOpen=!0,this.input.value=``,this.selectedIndex=0,this.selectedTextIndex=0,this.overlay.classList.add(`visible`),this.overlay.setAttribute(`aria-hidden`,`false`),this.setMode(`files`,{preserveInput:!0}),this._focusAfterTransition())}_focusAfterTransition(){this._cancelPendingFocus();let e=()=>{this._focusCleanup=null,this.input?.focus(),this.isOpen&&this.input&&document.activeElement!==this.input&&setTimeout(()=>this.input?.focus(),50)},t=r=>{(r.propertyName===`visibility`||r.propertyName===`opacity`)&&(this.overlay.removeEventListener(`transitionend`,t),clearTimeout(n),e())};this.overlay.addEventListener(`transitionend`,t);let n=setTimeout(()=>{this.overlay.removeEventListener(`transitionend`,t),e()},160);this._focusCleanup=()=>{this.overlay.removeEventListener(`transitionend`,t),clearTimeout(n)}}_cancelPendingFocus(){this._focusCleanup&&=(this._focusCleanup(),null)}close({restoreFocus:e=!0}={}){if(!this.overlay)return;let t=this.previousActiveElement;this._cancelPendingFocus(),this.abortTextSearch(),this.isOpen=!1,this.overlay.classList.remove(`visible`),this.overlay.setAttribute(`aria-hidden`,`true`),this.input.value=``,this.resultsList.innerHTML=``,this.setActiveDescendant(``),this.previousActiveElement=null,e&&t?.focus?.()}toggle(){this.isOpen?this.close():this.open()}setMode(e=`files`,{preserveInput:t=!1}={}){let n=e===`text`?`text`:`files`;this.mode=n,this.selectedIndex=0,this.selectedTextIndex=0,!t&&this.input&&(this.input.value=``),this.modeTabs.forEach(e=>{let t=e.dataset.qsMode===n;e.classList.toggle(`active`,t),e.setAttribute(`aria-selected`,t?`true`:`false`)}),this.scope?.classList.toggle(`hidden`,n!==`text`),this.scope?.setAttribute(`aria-hidden`,n===`text`?`false`:`true`),this.input&&(this.input.placeholder=n===`text`?`Search text in files...`:`Search files...`,this.input.setAttribute(`aria-label`,n===`text`?`Search text in files`:`Search files`)),this.handleInput()}handleInput(){if(this.mode===`text`){this.scheduleTextSearch();return}this.abortTextSearch(),this.filterFiles()}filterFiles(){let e=this.input?.value.trim().toLowerCase()??``,t=this.getFileList?.()??[];t!==this.lastFileListRef&&(this.lastFileListRef=t,this.fileCorpus=t.map(e=>f(e))),this.fileMatches.clear();let n=this.getFileMetadata?.()??[],r=new Map((Array.isArray(n)?n:[]).map(e=>[e?.path,Number.isFinite(Number(e?.mtimeMs))?Number(e.mtimeMs):0])),i=this.getRecentFiles?.()??[],a=new Map((Array.isArray(i)?i:[]).map((e,t)=>[e,t]));if(!e)this.filteredFiles=[...this.fileCorpus].sort((e,t)=>{let n=(r.get(t.filePath)??0)-(r.get(e.filePath)??0);return n===0?(a.get(e.filePath)??c)-(a.get(t.filePath)??c):n}).slice(0,s).map(e=>e.filePath),this.fileMatchCount=this.fileCorpus.length;else{let t=[],n=0;this.fileCorpus.forEach(r=>{let i=_(r,e);if(!i)return;n+=1,this.fileMatches.set(r.filePath,i);let o={filePath:r.filePath,recentRank:a.get(r.filePath)??c,score:i.score},l=!1;for(let e=0;e<t.length;e+=1){let n=t[e];if(o.score>n.score||o.score===n.score&&(o.recentRank<n.recentRank||o.recentRank===n.recentRank&&r.lowerPath<String(n.filePath).toLowerCase())){t.splice(e,0,o),l=!0;break}}!l&&t.length<s&&t.push(o),t.length>s&&(t.length=s)}),this.filteredFiles=t.map(e=>e.filePath),this.fileMatchCount=n}this.fileResultsTruncated=this.fileMatchCount>s,this.selectedIndex=0,this.renderResults(e)}renderResults(e){if(!this.resultsList)return;if(this.resultsList.innerHTML=``,this.filteredFiles.length===0){this.setActiveDescendant(``),this.hint&&(this.hint.textContent=e?`No files found`:`No files in vault`,this.hint.classList.remove(`hidden`));return}if(this.hint){let t=this.fileResultsTruncated?e?`Showing the top ${s} of ${this.fileMatchCount} matches. Refine the query to narrow results.`:`Showing the first ${s} of ${this.fileMatchCount} files.`:``;this.hint.textContent=t,this.hint.classList.toggle(`hidden`,!t)}let t=document.createDocumentFragment();this.filteredFiles.forEach((e,r)=>{let i=document.createElement(`button`);i.type=`button`,i.className=`qs-result-item`,r===this.selectedIndex&&i.classList.add(`selected`),i.id=`${l}${r}`,i.setAttribute(`role`,`option`),i.setAttribute(`aria-selected`,r===this.selectedIndex?`true`:`false`),i.dataset.index=String(r);let a=d(e),o=m(e),s=this.fileMatches.get(e),c=this.fileCorpus.find(t=>t.filePath===e),u=c?v(c,s?.indices):{dirPath:[],fileName:[]};i.innerHTML=`
2
+ ${n(e,{className:`qs-result-icon`})}
3
+ <span class="qs-result-name">${y(a,u.fileName)}</span>
4
+ ${o?`<span class="qs-result-path">${y(o,u.dirPath)}</span>`:``}
5
+ `,i.addEventListener(`click`,()=>{this.selectedIndex=r,this.confirmSelection()}),i.addEventListener(`mouseenter`,()=>{this.selectedIndex=r,this.updateSelection()}),t.appendChild(i)}),this.resultsList.appendChild(t),this.updateSelection()}abortTextSearch({invalidate:e=!0}={}){this.textSearchRunner.abort({invalidate:e})}scheduleTextSearch(){let e=this.input?.value.trim()??``,t=this.getSearchConfig?.()??{};this.textResults=null,this.textResultItems=[],this.textSearchRunner.schedule({isActive:()=>this.isOpen&&this.mode===`text`,onResults:(e,t)=>{this.textResults=e,this.textResultItems=a(e),this.selectedTextIndex=0,this.renderTextResults(t)},onState:e=>this.renderTextState(e),query:e,searchConfig:t,searchText:this.searchText})}renderTextState(e){this.resultsList&&(this.resultsList.innerHTML=``,this.resultsList.setAttribute(`aria-busy`,e===`Searching...`?`true`:`false`),this.setActiveDescendant(``),this.hint&&(this.hint.textContent=e,this.hint.classList.remove(`hidden`)))}renderTextResults(e=``){if(!this.resultsList)return;if(this.resultsList.innerHTML=``,this.resultsList.setAttribute(`aria-busy`,`false`),!this.textResults?.files?.length||this.textResultItems.length===0){this.setActiveDescendant(``),this.renderTextState(e?`No text matches found`:`Type to search file text`);return}if(this.hint){let e=this.textResults.truncated?`Showing partial results: ${this.textResults.files.length} files and ${i(this.textResults.matchCount,{truncated:!0})}. Refine the query to narrow results.`:``;this.hint.textContent=e,this.hint.classList.toggle(`hidden`,!e)}let n=document.createDocumentFragment(),r=0;this.textResults.files.forEach(e=>{let a=document.createElement(`section`);a.className=`qs-text-group`,a.setAttribute(`role`,`group`),a.setAttribute(`aria-label`,e.file);let o=document.createElement(`div`);o.className=`qs-text-group-header`,o.innerHTML=`
6
+ <span class="qs-text-file-name">${t(p(e.file))}</span>
7
+ <span class="qs-text-file-meta">${t(m(e.file))}</span>
8
+ <span class="qs-text-count">${t(i(e.matchCount,{truncated:e.truncated}))}</span>
9
+ `,a.appendChild(o),(e.snippets??[]).forEach(e=>{let n=r,i=document.createElement(`button`);i.type=`button`,i.className=`qs-text-item`,n===this.selectedTextIndex&&i.classList.add(`selected`),i.id=`${u}${n}`,i.setAttribute(`role`,`option`),i.setAttribute(`aria-selected`,n===this.selectedTextIndex?`true`:`false`),i.dataset.textIndex=String(n),i.innerHTML=`
10
+ <span class="qs-text-line">L${t(String(e.line??1))}</span>
11
+ <span class="qs-text-snippet">${this.highlightSnippet(e)}</span>
12
+ `,i.addEventListener(`click`,()=>{this.selectedTextIndex=n,this.confirmSelection()}),i.addEventListener(`mouseenter`,()=>{this.selectedTextIndex=n,this.updateSelection()}),a.appendChild(i),r+=1}),n.appendChild(a)}),this.resultsList.appendChild(n),this.updateSelection()}highlightSnippet(e={}){let n=String(e.text??``),r=Math.min(Math.max(Number(e.matchStart)||0,0),n.length),i=Math.min(Math.max(Number(e.matchEnd)||r,r),n.length);return`${t(n.slice(0,r))}<mark>${t(n.slice(r,i))}</mark>${t(n.slice(i))}`}moveSelection(e){if(this.mode===`text`){if(this.textResultItems.length===0)return;this.selectedTextIndex=(this.selectedTextIndex+e+this.textResultItems.length)%this.textResultItems.length,this.updateSelection();return}this.filteredFiles.length!==0&&(this.selectedIndex=(this.selectedIndex+e+this.filteredFiles.length)%this.filteredFiles.length,this.updateSelection())}updateSelection(){if(!this.resultsList)return;if(this.mode===`text`){let e=this.resultsList.querySelectorAll(`.qs-text-item`);e.forEach((e,t)=>{let n=t===this.selectedTextIndex;e.classList.toggle(`selected`,n),e.setAttribute(`aria-selected`,n?`true`:`false`)}),e[this.selectedTextIndex]?.scrollIntoView({block:`nearest`}),this.setActiveDescendant(e[this.selectedTextIndex]?.id??``);return}let e=this.resultsList.querySelectorAll(`.qs-result-item`);e.forEach((e,t)=>{let n=t===this.selectedIndex;e.classList.toggle(`selected`,n),e.setAttribute(`aria-selected`,n?`true`:`false`)}),e[this.selectedIndex]?.scrollIntoView({block:`nearest`}),this.setActiveDescendant(e[this.selectedIndex]?.id??``)}setActiveDescendant(e=``){this.input?.setAttribute(`aria-activedescendant`,e)}confirmSelection(){if(this.mode===`text`){let e=this.textResultItems[this.selectedTextIndex];e&&(this.close({restoreFocus:!1}),this.onTextMatchSelect?.(e));return}let e=this.filteredFiles[this.selectedIndex];e&&(this.close({restoreFocus:!1}),this.onFileSelect?.(e))}};export{b as QuickSwitcherController};
@@ -0,0 +1 @@
1
+ var e=Object.freeze({authError:`auth_error`,authPassword:`auth_password`,file:`file`,gitCommit:`git-commit`,gitDiff:`git-diff`,gitFileHistory:`git-file-history`,gitFilePreview:`git-file-preview`,gitHistory:`git-history`});function t(e=``){let t=String(e??``),n=t.startsWith(`#`)?t.slice(1):t;return new URLSearchParams(n)}function n(e=``){let t=String(e??``);return t.startsWith(`#`)?t.slice(1):t}function r(r=``){if(!n(r).trim().includes(`=`))return!1;let i=t(r);return Object.values(e).some(e=>i.has(e))}function i(t,{anchor:n=null,column:r=null,drawioMode:i=null,line:a=null,matchLength:o=null}={}){let s=new URLSearchParams;return t&&s.set(e.file,t),i&&s.set(`drawio`,i),n&&s.set(`anchor`,String(n).trim()),a!=null&&s.set(`line`,String(Math.max(1,Math.round(Number(a)||1)))),r!=null&&s.set(`column`,String(Math.max(1,Math.round(Number(r)||1)))),o!=null&&s.set(`matchLength`,String(Math.max(0,Math.round(Number(o)||0)))),s.toString()}function a(e){return e.endsWith(`/`)?e.slice(0,-1):e}function o(e){let t=a(String(e??``).trim());if(!t||t===`/`)return``;if(/^[a-zA-Z][a-zA-Z\d+.-]*:/u.test(t))try{return o(new URL(t).pathname)}catch{return``}return t.startsWith(`/`)?t:`/${t}`}function s(e,t){let n=a(String(e??``).trim());return!n||n===`/`?t:n.startsWith(`/`)?n:`/${n}`}function c(e,t){let n=o(e),r=String(t??``).trim();return r?/^[a-zA-Z][a-zA-Z\d+.-]*:/u.test(r)||n&&(r===n||r.startsWith(`${n}/`))?r:r===`/`?n||`/`:r.startsWith(`/`)?n?`${n}${r}`:r:n?`${n}/${r}`:`/${r}`:n||`/`}function l(){let e={auth:{enabled:!1,implemented:!0,loginEndpoint:`/api/auth/oidc/login`,provider:``,requiresLogin:!1,sessionEndpoint:`/api/auth/session`,statusEndpoint:`/api/auth/status`,strategy:`none`},basePath:``,build:{id:``,packageVersion:``},drawioBaseUrl:`https://embed.diagrams.net`,environment:`development`,gitEnabled:!0,publicWsBaseUrl:``,search:{available:!1,backend:`ripgrep`,minQueryLength:2,unavailableReason:`ripgrep search is unavailable`,version:``},structurizrEnabled:!1,wikiLinkAutoCreate:!0,wsBasePath:`/ws`,...window.__COLLABMD_CONFIG__??{}},t=o(e.basePath),n={enabled:!1,implemented:!0,loginEndpoint:`/api/auth/oidc/login`,provider:``,requiresLogin:!1,sessionEndpoint:`/api/auth/session`,statusEndpoint:`/api/auth/status`,strategy:`none`,...e.auth??{}},r={id:``,packageVersion:``,...e.build??{}};return{...e,auth:{...n,loginEndpoint:c(t,n.loginEndpoint),sessionEndpoint:c(t,n.sessionEndpoint),statusEndpoint:c(t,n.statusEndpoint)},basePath:t,build:r,wsBasePath:s(e.wsBasePath,`/ws`)}}function u(e=`/`,t=l()){return c(t.basePath,e)}function d(e=`/`,t=l()){return new URL(u(e,t),window.location.origin).toString()}function f(e=`/`,t=l()){let n=String(e??``).trim();return!n||n===`/`?u(`/api`,t):n.startsWith(`/api`)?u(n,t):u(`/api${n.startsWith(`/`)?n:`/${n}`}`,t)}function p(e=l()){if(![`development`,`test`].includes(e.environment))return``;let t=new URLSearchParams(window.location.search).get(`server`);if(!t)return``;try{let e=new URL(t,window.location.origin);return[`http:`,`https:`,`ws:`,`wss:`].includes(e.protocol)?a(e.toString()):``}catch{return``}}function m(e=l()){return p(e)||(e.publicWsBaseUrl?a(e.publicWsBaseUrl):`${window.location.protocol===`https:`?`wss:`:`ws:`}//${window.location.host}${u(e.wsBasePath,e)}`)}function h(){return l()}function g(){return t(window.location.hash)}function _(e){return e===`staged`||e===`all`||e===`working-tree`?e:`all`}function v(){let t=g();if(t.has(e.gitFilePreview)){let n=t.get(e.gitFilePreview)||null;return{currentFilePath:t.get(`current`)||n,filePath:n,hash:t.get(`hash`)||null,type:`git-file-preview`}}if(t.has(e.gitFileHistory))return{filePath:t.get(e.gitFileHistory)||null,type:`git-file-history`};if(t.has(e.gitHistory))return{type:`git-history`};if(t.has(e.gitCommit))return{hash:t.get(e.gitCommit)||null,historyFilePath:t.get(`history`)||null,path:t.get(`path`)||null,type:`git-commit`};if(t.has(e.gitDiff)){let n=t.get(e.gitDiff)||null;return{filePath:n,scope:_(t.get(`scope`)||(n?`working-tree`:`all`)),type:`git-diff`}}return t.has(e.file)?{anchor:t.get(`anchor`)||null,column:t.has(`column`)&&Number(t.get(`column`))||null,drawioMode:t.get(`drawio`)||null,filePath:t.get(e.file),line:t.has(`line`)&&Number(t.get(`line`))||null,matchLength:t.has(`matchLength`)&&Number(t.get(`matchLength`))||null,type:`file`}:{type:`empty`}}function y(e,{anchor:t=null,column:n=null,drawioMode:r=null,line:a=null,matchLength:o=null}={}){window.location.hash=i(e,{anchor:t,column:n,drawioMode:r,line:a,matchLength:o})}function b({filePath:e=null,scope:t=`all`}={}){let n=new URLSearchParams;n.set(`git-diff`,e??``),n.set(`scope`,_(t)),window.location.hash=n.toString()}function x({hash:e,path:t=null,historyFilePath:n=null}={}){let r=String(e??``).trim(),i=new URLSearchParams;i.set(`git-commit`,r),n&&i.set(`history`,n),t&&i.set(`path`,t),window.location.hash=i.toString()}function S(){let e=new URLSearchParams;e.set(`git-history`,`1`),window.location.hash=e.toString()}function C({filePath:e}={}){let t=new URLSearchParams;t.set(`git-file-history`,e??``),window.location.hash=t.toString()}function w({hash:e,path:t,currentFilePath:n=null}={}){let r=String(e??``).trim(),i=new URLSearchParams;i.set(`git-file-preview`,t??``),n&&i.set(`current`,n),i.set(`hash`,r),window.location.hash=i.toString()}export{b as a,S as c,d,m as f,r as g,t as h,x as i,f as l,i as m,h as n,C as o,p,y as r,w as s,v as t,u};
@@ -0,0 +1,6 @@
1
+ import{a as e,d as t,g as n,m as r,p as i,s as a}from"./file-kind-BbqgGzpc.js";import{h as o,l as s,n as c}from"./runtime-config-BLrEKVO1.js";import{i as l,n as u}from"./browser-utils--H_DkyEF.js";var d={enabled:!1,implemented:!0,loginEndpoint:`/api/auth/oidc/login`,passwordLabel:`Password`,provider:``,requiresLogin:!1,sessionEndpoint:`/api/auth/session`,statusEndpoint:`/api/auth/status`,strategy:`none`,submitLabel:`Continue`},f=`data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAIAAAABc2X6AAAGsklEQVR4Ae3cA3jsShgG4Gvbtu1js7aObdu2bdbudmvbtu1VtUg2ydw7x95mstnt5rZ5voflG80/fzL72L89bOsF94J7wWgb2dSgiAzpPHmwbfPK1qWzxDNsRdb6Ar2BgtF9hSbDRbYG4mk2beuXwm+Q89yV+TmAILgHptrb5DyPtg3LhBZjBCP+RovewNZlc2TOl8nGel0HA4LA4qPhkRSM6XcHoEYk86fIfT2AQqFzYIBjMncHofkoZBWNCM1GSq+cpdpadQIMKEoRzIeXIroEkW08TObpDAiiO8F4Tga85TBnoEc8xVKZm9UNYECS0stnBCP/Ud+AnJH/SC+eAiShPTDZ3CRZPIN9CUokC6ZSYqE2wHhmKhw/2TegR2RnSFSUaRaMJccLxg7QsAQhktkTAACaAmOxkbA80h2taKIZKWzR1BFWRAQLRvXhphYdDEtcWDxxU4sOhjdDkdVYbmrRwYAgJIums3RTNWrftbHz/AmZh5MiLBAGVqOdZ4+1790inmrFvpYZuPPEATWdknmT5f7eZEN9V2N7o9zPu3XNInQte2BlcYE6tVTbltXw4kf9h5QlhW3rliBr1QcDAODsjPFlhmelqfNvKQty4S9B0KoPVgTxmWk7Tx0COMZCF6GzA3ZC0LWMwJSsTWiOXj+O6iP382KzowAAvMOhaxmAqw/Ijr4tGPsHgnbsACwxlpNNPABIIv5LIvxpzOVlkdXPNMGK8CCudi2pFi+ovRFlwLOtc7/uUgvnxhxu05LpIyD17nTu+EAw8q9HaVtXzgcAcBUMcAER/sx9YBj5pdeExr897NLtT9TVcLgRTzVcgbyHBuc9L5n8PdLJzAEwmWP1KDCMMvTp9jWf3tbC1gdQKDgMBhRBRL0GYaojO35zxOo8fZjbz5ZAZz700Anm9pLI9g+yoY7bYKrJBWJoRpk8SJ0/fDoc13QuRuNdgMmydfTBZPkGdcDDd0q1EDkOVIKzTOiDKVGY7oPLmylVYCKlL30wIKW6D44tJlSCE76nC45+CwHXfWDfdKVKcMwHdMEJ33MC7JqEqwRHvEgXnDaYE+ArMayBB3ECDAennnVKn4tUDU74gS446k1OgB3i8Z41LHmkKHtW4eGfpRpctv5/VlomlxOsTR6kKQN1H1wnptiZHpYFv27pMbqus4kxOChbySzTz8loakfukhIkYKEBEBzw6SBXk79cLQ5lXv5XuxtJAaMDdA/v+JMydVs8irBnDvB+g9QbGeY1SUFg2gTn1JD0z+cdPIVaTbym0JemeQ6FzrtzJtdFm+DT4Rh9MD9DybxNmxL03mg3g/u0MP3cbWo6GrWjbZMBg/0Id6wqAcWwEX+F/10fVzPIe2jmRW3VTiP+VBjC4bU6KgMAID9qaQt9frl3P6hSndOaP7HrxdSYPQiH92QYhvAwTRH7KdSWBL9h5j4GeugkqDpWc1oZBqaelSGNwMWNJMLjUkHxBn//zwZeH3topr+7bWxDmia0AID17gok7aTTMrTnw1KsfZTPVMhAyj9uVl5lIexqcQLs4KFpYXjpSjQw3HwrIqCBQQ5mXsZInBWtoIOac1GOqrU4IsUJwOSllsmha5iZTf3npzXnqXka+1dFmzo4D9vZgQp2TsAZvrZUIC7729WSmRlmVfz+HGExKpUCVGJj1oSQlTd+SZ9Lm4bubqKvtTkmk+OAIRhu+zMu3jEwyqTQ1d7lofWdzV0VyWSxpPJkjpOB76z7fsPfjjOHHMhBb0QzAhMUMS18nRrgOzHkz96YdPR4toNTMT+gKjq4OtatNPBCvgfcp9PD1w/wsFP14y7WA4/zu9SudVWw8HKpUC4Zw5uOLmQ//c4fGrZL8iit+WEpvMmxAIZbtrCor7uNLpj/ubpk6N6Kh059M6tINl8Qh4UUHGa73wzjNGHQ4dj7wC6JOPtLACLqkvq4WeuE2cWi/+nLt0csOKnQ1CKP+IYMWELqgPnGiLVh6O7Go8GYZpfxpDbnwkaHjpj3JfLRV7Wgb01SARxIupc6wntyfGOG9pbikRQJmzvodRg7mRq2tkkm7IbFlhktBdaBS7SsPZx1haDIbltOSwGKXxl5px7UZGaEb8gXlerEgmmMxB2KfEciTKHRAsvbmPo0nVsST1BEdH3Kyrh9bJVlQ7wmbk05USSu0PUPPWjDOtxLg5bF7h7tM42B08x//o7U0/AmrCSV3PtYiwZpS3B13MHMS/DIz4rYaBO0VM93JpwbwaJtqNfEsbwZFgEL50VtgcJLBV6wK9aKtfd+cEkvuBfcC37k9h8VGR+csPdltgAAAABJRU5ErkJggg==`;function p(){return{...d,...c().auth??{}}}function m(){return o(window.location.hash).get(`auth_password`)||``}function h(){let e=o(window.location.hash);if(!e.has(`auth_password`))return;e.delete(`auth_password`);let t=e.toString(),n=`${window.location.pathname}${window.location.search}${t?`#${t}`:``}`;window.history.replaceState(null,``,n)}function g(e){let t=o(window.location.hash),n=t.get(e)||``;if(!n)return``;t.delete(e);let r=t.toString(),i=`${window.location.pathname}${window.location.search}${r?`#${r}`:``}`;return window.history.replaceState(null,``,i),n}function _(){return`${window.location.pathname}${window.location.search}${window.location.hash}`}async function v(e){let t=await fetch(e.statusEndpoint,{headers:{Accept:`application/json`}}),n=await t.json().catch(()=>({}));if(!t.ok)throw Error(n.error||`Failed to read auth status`);return n}function y(e){let t=e?.auth??{};if(t.strategy!==`oidc`||t.provider!==`google`)return;let n=String(e?.user?.name??``).trim();if(n)try{window.localStorage.setItem(`collabmd-user-name`,n)}catch{}}async function b(e,t){let n=await fetch(e.sessionEndpoint,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({password:t})}),r=await n.json().catch(()=>({}));if(!n.ok)throw Error(r.error||`Authentication failed`);return r}function x(){let e=document.createElement(`div`);e.className=`auth-gate-overlay`;let t=document.createElement(`section`);return t.className=`auth-gate-card`,e.append(t),{card:t,overlay:e}}function S(){let e=document.createElement(`span`);e.className=`auth-gate-button__icon`,e.setAttribute(`aria-hidden`,`true`);let t=document.createElement(`img`);return t.alt=``,t.decoding=`async`,t.height=20,t.loading=`eager`,t.src=f,t.width=20,e.append(t),e}function C(e,t,{errorMessage:n=``}){e.replaceChildren();let r=document.createElement(`h1`);r.textContent=`Authentication required`;let i=document.createElement(`p`);i.textContent=`Sign in with Google to join this session.`;let a=document.createElement(`div`);a.className=`auth-gate-actions`;let o=document.createElement(`button`);o.className=`ui-button ui-button--secondary auth-gate-button auth-gate-button--google`,o.type=`button`,o.append(S(),document.createTextNode(t.submitLabel||`Continue with Google`));let s=document.createElement(`div`);s.className=`auth-gate-error`,s.textContent=n,a.append(o),e.append(r,i,s,a),o.addEventListener(`click`,()=>{let e=new URL(t.loginEndpoint,window.location.origin);e.searchParams.set(`returnTo`,_()),window.location.assign(e.toString())})}function w(e,{title:t,body:n,secondaryActionLabel:r=``,onSecondaryAction:i=null}){e.replaceChildren();let a=document.createElement(`h1`);a.textContent=t;let o=document.createElement(`p`);if(o.textContent=n,e.append(a,o),r&&typeof i==`function`){let t=document.createElement(`button`);t.className=`ui-button ui-button--secondary auth-gate-secondary-button`,t.type=`button`,t.textContent=r,t.addEventListener(`click`,()=>{i()}),e.append(t)}}function T(e,t,{onSubmit:n}){e.replaceChildren();let r=document.createElement(`h1`);r.textContent=`Authentication required`;let i=document.createElement(`p`);i.textContent=`Enter the host password to join this shared session.`;let a=document.createElement(`form`);a.className=`auth-gate-form`;let o=document.createElement(`label`);o.className=`auth-gate-label`,o.textContent=t.passwordLabel;let s=document.createElement(`input`);s.className=`ui-input auth-gate-input`,s.type=`password`,s.name=`password`,s.autocomplete=`current-password`,s.required=!0;let c=document.createElement(`div`);c.className=`auth-gate-error`;let l=document.createElement(`div`);l.className=`auth-gate-actions`;let u=document.createElement(`button`);u.className=`ui-button ui-button--primary auth-gate-button`,u.type=`submit`,u.textContent=t.submitLabel,l.append(u),o.append(s),a.append(r,i,o,c,l),e.append(a),a.addEventListener(`submit`,e=>{e.preventDefault();let t=s.value;s.disabled=!0,u.disabled=!0,c.textContent=``,n(t).catch(e=>{c.textContent=e instanceof Error?e.message:`Authentication failed`,s.disabled=!1,u.disabled=!1,s.select(),s.focus()})}),queueMicrotask(()=>{s.focus()})}async function E(){let e=p();if(!e.enabled||e.strategy===`none`)return{authenticated:!0,auth:e};let{card:t,overlay:n}=x();document.body.append(n);let r=g(`auth_error`);return new Promise(i=>{let a=(t=null)=>{h(),y(t),n.remove(),i({authenticated:!0,auth:t?.auth??e,user:t?.user??null})};(async()=>{w(t,{body:`Checking access to this session…`,title:`CollabMD`});try{let t=await v(e);if(t.authenticated){a(t);return}}catch(e){w(t,{body:e instanceof Error?`${e.message} Refresh the page and try again.`:`Failed to contact the auth service. Refresh the page and try again.`,title:`Cannot verify access`});return}if(e.strategy===`password`){let n=m();if(n)try{h(),await b(e,n),a();return}catch{}T(t,e,{onSubmit:async t=>{await b(e,t),a()}});return}if(e.strategy===`oidc`){C(t,e,{errorMessage:r}),r=``;return}w(t,{body:`This authentication strategy is not available.`,title:`Authentication unavailable`})})()})}function D(e){return!e||e.repeat||!e.metaKey&&!e.ctrlKey||e.shiftKey||e.altKey?!1:String(e.key??``).toLowerCase()===`k`||e.code===`KeyK`}function O(e){let t=String(e??``).trim().replace(/\\/g,`/`).replace(/^\/+/u,``).split(`/`).map(e=>e.trim()).filter(Boolean);return t.some(e=>e===`.`||e===`..`)?``:t.join(`/`)}function k(e,t){let n=O(e),r=O(t);return n?r?`${n}/${r}`:n:r}function A(e=``){return String(e??``).replace(/\/+$/u,``).split(`/`).filter(Boolean).pop()||``}function j(e=``){let t=String(e??``).replace(/\/+$/u,``),n=t.lastIndexOf(`/`);return n>=0?t.slice(0,n):``}function M(e,t){return e.toLowerCase().endsWith(t.toLowerCase())?e:`${e}${t}`}function N(e){return`# ${n(String(e??``).split(`/`).pop()||``)||`Untitled`}\n\n`}function P(t){let n=O(t),r=e(n)?n:`${n}.base`;return{content:[`views:`,` - type: table`,` name: Table`,` order:`,` - file.name`,``].join(`
2
+ `),path:r}}function F(e){let n=O(e),r=t(n)?n:`${n}.mmd`;return{content:[`flowchart TD`,` A[Start] --> B{Decide}`,` B -->|Yes| C[Ship it]`,` B -->|No| D[Revise]`,``].join(`
3
+ `),path:r}}function I(e){let t=O(e),n=a(t)?t:`${t}.drawio`;return{content:[`<mxfile host="app.diagrams.net" modified="2026-01-01T00:00:00.000Z" agent="CollabMD" version="24.7.17">`,` <diagram id="page-1" name="Page-1">`,` <mxGraphModel dx="1200" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">`,` <root>`,` <mxCell id="0" />`,` <mxCell id="1" parent="0" />`,` </root>`,` </mxGraphModel>`,` </diagram>`,`</mxfile>`,``].join(`
4
+ `),path:n}}function L(e){let t=O(e),n=i(t)?t:`${t}.puml`;return{content:[`@startuml`,`Alice -> Bob: Hello`,`@enduml`,``].join(`
5
+ `),path:n}}function R(e){let t=O(e),n=r(t)?t:`${t}.dsl`;return{content:`workspace "CollabMD Example" "A small Structurizr workspace" {, model {, user = person "User" "Uses the system", system = softwareSystem "Example System" "The system being documented" {, web = container "Web Application" "Serves the user interface" "JavaScript" {, home = component "Home Page" "Shows the landing page" "React", editor = component "Editor" "Edits workspace content" "React", }, api = container "API" "Handles requests" "Node.js", }, user -> system "Uses", web -> api "Calls" "HTTPS/JSON", home -> editor "Links to", }, views {, systemContext system "Context" {, include *, autoLayout, }, container system "Containers" {, include *, autoLayout, }, component web "Components" {, include *, autoLayout, }, styles {, element "Person" {, shape person, }, }, },},`.split(`,`).join(`
6
+ `),path:n}}function z(e,t={}){let n={...t};return e&&(n[`X-CollabMD-Request-Id`]=String(e)),n}async function B(e,t){let n=await e.json().catch(()=>({}));if(!e.ok||n.ok===!1){let r=Error(n.error||t);throw r.status=e.status,r.body=n,typeof n?.code==`string`&&(r.code=n.code),r}return n}function V(e){return encodeURIComponent(String(e??``))}async function H(e,{fallbackError:t,fallbackFileName:n}={}){let r=await fetch(e);if(!r.ok){let e=await r.json().catch(()=>({}));throw Error(e.error||t)}let i=await r.blob();return u(i,l(r.headers.get(`content-disposition`)||``,n),{removeDelayMs:0,revokeDelayMs:0}),r}var U=new class{async readTree(){return B(await fetch(s(`/files`)),`Failed to load file tree`)}async readCommentOverview(){return B(await fetch(s(`/comments/overview`)),`Failed to load comment overview`)}async readFile(e){return B(await fetch(s(`/file?path=${encodeURIComponent(e)}`)),`Failed to read file`)}async readBacklinks(e,{signal:t}={}){let n=await B(await fetch(s(`/backlinks?file=${encodeURIComponent(e)}`),{signal:t}),`Failed to load backlinks`);return Array.isArray(n.backlinks)?n.backlinks:[]}async syncStructurizrWorkspace({path:e,source:t}){return B(await fetch(s(`/structurizr/sync`),{body:JSON.stringify({path:e,source:t}),headers:{"Content-Type":`application/json`},method:`POST`}),`Failed to sync Structurizr workspace`)}async renderSvg(e){let t=await B(await fetch(s(`/plantuml/render`),{body:JSON.stringify({source:e}),headers:{"Content-Type":`application/json`},method:`POST`}),`Failed to render PlantUML`);if(!t.ok||typeof t.svg!=`string`)throw Error(t.error||`Failed to render PlantUML`);return t.svg}async searchText({limit:e=50,query:t=``,signal:n=null}={}){let r=new URLSearchParams({limit:String(e),q:String(t??``)});return B(await fetch(s(`/search?${r.toString()}`),{signal:n}),`Failed to search files`)}async queryBase({activeFilePath:e=``,path:t=``,search:n=``,source:r=null,sourcePath:i=``,view:a=``}={}){return B(await fetch(s(`/base/query`),{body:JSON.stringify({activeFilePath:e,path:t,search:n,source:r,sourcePath:i,view:a}),headers:{"Content-Type":`application/json`},method:`POST`}),`Failed to query base`)}async exportBaseCsv({activeFilePath:e=``,path:t=``,search:n=``,source:r=null,sourcePath:i=``,view:a=``}={}){let o=await fetch(s(`/base/export`),{body:JSON.stringify({activeFilePath:e,path:t,search:n,source:r,sourcePath:i,view:a}),headers:{"Content-Type":`application/json`},method:`POST`});if(!o.ok){let e=await o.json().catch(()=>({}));throw Error(e.error||`Failed to export base CSV`)}return{blob:await o.blob(),contentDisposition:o.headers.get(`content-disposition`)||``}}async queryBasePropertyValues({activeFilePath:e=``,path:t=``,propertyId:n=``,query:r=``,source:i=null,sourcePath:a=``,view:o=``}={}){return B(await fetch(s(`/base/property-values`),{body:JSON.stringify({activeFilePath:e,path:t,propertyId:n,query:r,source:i,sourcePath:a,view:o}),headers:{"Content-Type":`application/json`},method:`POST`}),`Failed to load base property values`)}async transformBase({activeFilePath:e=``,mutation:t=null,path:n=``,source:r=null,sourcePath:i=``,view:a=``}={}){return B(await fetch(s(`/base/transform`),{body:JSON.stringify({activeFilePath:e,mutation:t,path:n,source:r,sourcePath:i,view:a}),headers:{"Content-Type":`application/json`},method:`POST`}),`Failed to transform base`)}async createFile({content:e,path:t,requestId:n=null}){return B(await fetch(s(`/file`),{body:JSON.stringify({content:e,path:t}),headers:z(n,{"Content-Type":`application/json`}),method:`POST`}),`Failed to create file`)}async writeFile({content:e,path:t,requestId:n=null}){return B(await fetch(s(`/file`),{body:JSON.stringify({content:e,path:t}),headers:z(n,{"Content-Type":`application/json`}),method:`PUT`}),`Failed to write file`)}async renameFile({oldPath:e,newPath:t,requestId:n=null}){return B(await fetch(s(`/file`),{body:JSON.stringify({newPath:t,oldPath:e}),headers:z(n,{"Content-Type":`application/json`}),method:`PATCH`}),`Failed to rename file`)}async deleteFile(e,{requestId:t=null}={}){return B(await fetch(s(`/file?path=${encodeURIComponent(e)}`),{headers:t?z(t):void 0,method:`DELETE`}),`Failed to delete file`)}async createDirectory(e,{requestId:t=null}={}){return B(await fetch(s(`/directory`),{body:JSON.stringify({path:e}),headers:z(t,{"Content-Type":`application/json`}),method:`POST`}),`Failed to create folder`)}async renameDirectory({oldPath:e,newPath:t,requestId:n=null}){return B(await fetch(s(`/directory`),{body:JSON.stringify({newPath:t,oldPath:e}),headers:z(n,{"Content-Type":`application/json`}),method:`PATCH`}),`Failed to rename folder`)}async deleteDirectory(e,{recursive:t=!1,requestId:n=null}={}){let r=new URLSearchParams({path:String(e??``)});return t&&r.set(`recursive`,`1`),B(await fetch(s(`/directory?${r.toString()}`),{headers:n?z(n):void 0,method:`DELETE`}),`Failed to delete folder`)}async uploadImageAttachment({file:e,fileName:t=``,sourcePath:n}){return B(await fetch(s(`/attachments`),{body:e,headers:{"Content-Type":e?.type||`application/octet-stream`,"X-CollabMD-File-Name":V(t),"X-CollabMD-Source-Path":V(n)},method:`POST`}),`Failed to upload image`)}async uploadFile({file:e,path:t,requestId:n=null}){return B(await fetch(s(`/file/upload`),{body:e,headers:z(n,{"Content-Type":e?.type||`application/octet-stream`,"X-CollabMD-File-Path":V(t)}),method:`POST`}),`Failed to upload file`)}async downloadFile(e){let t=A(e)||`download`;return H(s(`/download/file?path=${encodeURIComponent(e)}`),{fallbackError:`Failed to download file`,fallbackFileName:t})}async downloadDirectory(e){let t=A(e)||`vault`;return H(s(`/download/directory?path=${encodeURIComponent(e)}`),{fallbackError:`Failed to download folder`,fallbackFileName:`${t}.zip`})}};export{P as a,F as c,M as d,A as f,E as g,D as h,k as i,L as l,O as m,z as n,I as o,j as p,B as r,N as s,U as t,R as u};
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>draw.io — CollabMD</title>
7
- <script type="module" crossorigin src="./assets/drawioEditor-CXaTM9Ds.js"></script>
7
+ <script type="module" crossorigin src="./assets/drawioEditor-BjiKJqlc.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="./assets/modulepreload-polyfill-P2Xu9kJm.js">
9
9
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-HclGiUj8.js">
10
10
  </head>
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Excalidraw — CollabMD</title>
7
- <script type="module" crossorigin src="./assets/excalidrawEditor-DxJanCze.js"></script>
7
+ <script type="module" crossorigin src="./assets/excalidrawEditor-CT2pHq7e.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="./assets/modulepreload-polyfill-P2Xu9kJm.js">
9
9
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-HclGiUj8.js">
10
10
  </head>
@@ -4,12 +4,12 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>CollabMD Export</title>
7
- <script type="module" crossorigin src="./assets/exportDocument-BCuxqXDY.js"></script>
7
+ <script type="module" crossorigin src="./assets/exportDocument-BaqcOAAW.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="./assets/modulepreload-polyfill-P2Xu9kJm.js">
9
9
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-HclGiUj8.js">
10
10
  <link rel="modulepreload" crossorigin href="./assets/base-CxPEiHuy.js">
11
11
  <link rel="stylesheet" crossorigin href="./assets/base-CaIEJUF9.css">
12
- <link rel="stylesheet" crossorigin href="./assets/exportDocument-DV-OX74E.css">
12
+ <link rel="stylesheet" crossorigin href="./assets/exportDocument-B2L8fe34.css">
13
13
  </head>
14
14
  <body data-theme="light" data-surface="export-document">
15
15
  <main class="export-page-shell">
@@ -15,12 +15,12 @@
15
15
  document.documentElement.setAttribute('data-initial-file-requested', 'true');
16
16
  }
17
17
  </script>
18
- <script type="module" crossorigin src="./assets/index-D2wXraJ1.js"></script>
18
+ <script type="module" crossorigin src="./assets/index-ClbjMBVt.js"></script>
19
19
  <link rel="modulepreload" crossorigin href="./assets/modulepreload-polyfill-P2Xu9kJm.js">
20
20
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-HclGiUj8.js">
21
21
  <link rel="modulepreload" crossorigin href="./assets/base-CxPEiHuy.js">
22
22
  <link rel="stylesheet" crossorigin href="./assets/base-CaIEJUF9.css">
23
- <link rel="stylesheet" crossorigin href="./assets/index-C3YnUgMD.css">
23
+ <link rel="stylesheet" crossorigin href="./assets/index-Pum8ET-5.css">
24
24
  </head>
25
25
  <body>
26
26
 
@@ -363,6 +363,18 @@
363
363
  <span class="ui-toolbar-button-label">Search files</span>
364
364
  </button>
365
365
 
366
+ <button class="ui-button ui-button--ghost ui-button--toolbar ui-button--toggle toolbar-vim-button" id="toggleVimModeBtn" aria-label="Enable Vim keybindings" aria-pressed="false" title="Enable Vim keybindings">
367
+ <span class="toolbar-vim-icon" aria-hidden="true">
368
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
369
+ <rect x="3" y="4" width="18" height="16" rx="2"></rect>
370
+ <path d="m7 9 3 3-3 3"></path>
371
+ <path d="M13 15h4"></path>
372
+ </svg>
373
+ </span>
374
+ <span class="ui-toolbar-button-label">Vim</span>
375
+ <span class="ui-pill-badge ui-pill-badge--muted toolbar-vim-state" id="vimModeToggleLabel">Off</span>
376
+ </button>
377
+
366
378
  <details class="toolbar-overflow-group hidden" id="exportMenuGroup">
367
379
  <summary class="ui-button ui-button--ghost ui-button--toolbar toolbar-overflow-group-toggle" aria-label="Export options" title="Export">
368
380
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
@@ -734,7 +746,7 @@
734
746
  <input type="text" class="qs-input" id="quickSwitcherInput" placeholder="Search files..." aria-label="Search files" aria-controls="quickSwitcherResults" aria-describedby="quickSwitcherHint quickSwitcherScope" autocomplete="off" spellcheck="false">
735
747
  <kbd class="qs-kbd">esc</kbd>
736
748
  </div>
737
- <div class="qs-scope hidden" id="quickSwitcherScope" role="note" aria-hidden="true">Exact, case-insensitive text in Markdown, Base, Mermaid, PlantUML, and Draw.io files. Images and Excalidraw are excluded.</div>
749
+ <div class="qs-scope hidden" id="quickSwitcherScope" role="note" aria-hidden="true">Exact, case-insensitive text in Markdown, Base, Mermaid, PlantUML, Draw.io, and Excalidraw files. Images are excluded.</div>
738
750
  <div class="qs-results" id="quickSwitcherResults" role="listbox" aria-label="Search results" aria-busy="false" tabindex="-1"></div>
739
751
  <div class="qs-hint" id="quickSwitcherHint" role="status" aria-live="polite" aria-atomic="true">Type to search files</div>
740
752
  <div class="qs-footer">
@@ -3,6 +3,7 @@ services:
3
3
  image: ${COLLABMD_IMAGE:-ghcr.io/andes90/collabmd:latest}
4
4
  depends_on:
5
5
  - plantuml
6
+ - structurizr
6
7
  environment:
7
8
  HOST: 0.0.0.0
8
9
  PORT: 1234
@@ -19,7 +20,11 @@ services:
19
20
  BASE_PATH: ${BASE_PATH:-}
20
21
  PUBLIC_WS_BASE_URL: ${PUBLIC_WS_BASE_URL:-}
21
22
  PLANTUML_SERVER_URL: http://plantuml:8080
23
+ STRUCTURIZR_SERVER_URL: http://structurizr:8080
24
+ STRUCTURIZR_MIRROR_DIR: ${COLLABMD_VAULT_DIR:-/data}/.collabmd/structurizr
25
+ STRUCTURIZR_TRUSTED_EXECUTABLE_DSL: ${STRUCTURIZR_TRUSTED_EXECUTABLE_DSL:-false}
22
26
  COLLABMD_WIKI_LINK_AUTO_CREATE: ${COLLABMD_WIKI_LINK_AUTO_CREATE:-true}
27
+ COLLABMD_MAX_PDF_UPLOAD_BYTES: ${COLLABMD_MAX_PDF_UPLOAD_BYTES:-52428800}
23
28
  COLLABMD_VAULT_DIR: ${COLLABMD_VAULT_DIR:-/data}
24
29
  COLLABMD_GIT_REPO_URL: ${COLLABMD_GIT_REPO_URL:-}
25
30
  COLLABMD_GIT_SSH_PRIVATE_KEY_FILE: ${COLLABMD_GIT_SSH_PRIVATE_KEY_FILE:-}
@@ -36,6 +41,9 @@ services:
36
41
  ports:
37
42
  - "${COLLABMD_HOST_PORT:-1234}:1234"
38
43
  restart: unless-stopped
44
+ networks:
45
+ - default
46
+ - diagram-private
39
47
  volumes:
40
48
  - type: bind
41
49
  source: ${HOST_VAULT_DIR:-./data/vault}
@@ -52,6 +60,40 @@ services:
52
60
 
53
61
  plantuml:
54
62
  image: plantuml/plantuml-server:jetty
63
+ networks:
64
+ - diagram-private
55
65
  ports:
56
66
  - "127.0.0.1:${PLANTUML_HOST_PORT:-18080}:8080"
57
67
  restart: unless-stopped
68
+
69
+ structurizr:
70
+ image: structurizr/structurizr:2026.06.28
71
+ command: ["local", "/data"]
72
+ environment:
73
+ STRUCTURIZR_EDITABLE: "false"
74
+ STRUCTURIZR_AUTOREFRESHINTERVAL: "0"
75
+ STRUCTURIZR_AUTOSAVEINTERVAL: "0"
76
+ STRUCTURIZR_NETWORK_URLS_ALLOWED: ${STRUCTURIZR_NETWORK_URLS_ALLOWED:-}
77
+ networks:
78
+ - diagram-private
79
+ security_opt:
80
+ - no-new-privileges:true
81
+ cap_drop:
82
+ - ALL
83
+ cpus: 1.0
84
+ mem_limit: 768m
85
+ pids_limit: 256
86
+ read_only: true
87
+ restart: unless-stopped
88
+ ports:
89
+ - "127.0.0.1:${STRUCTURIZR_HOST_PORT:-19090}:8080"
90
+ tmpfs:
91
+ - /tmp
92
+ volumes:
93
+ - type: bind
94
+ source: ${HOST_VAULT_DIR:-./data/vault}/.collabmd/structurizr
95
+ target: /data
96
+
97
+ networks:
98
+ diagram-private:
99
+ driver: bridge
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "collabmd",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
4
4
  "type": "module",
5
- "description": "Collaborative markdown vault like Obsidian, but online. Serve any directory of markdown files for realtime collaborative editing.",
5
+ "description": "Collaborative markdown vault \u2014 like Obsidian, but online. Serve any directory of markdown files for realtime collaborative editing.",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/andes90/collabmd.git"
@@ -18,6 +18,7 @@
18
18
  "docker-compose.yml",
19
19
  "scripts/cloudflare-tunnel.mjs",
20
20
  "scripts/local-plantuml-compose.mjs",
21
+ "scripts/local-structurizr-compose.mjs",
21
22
  "src/"
22
23
  ],
23
24
  "bin": {
@@ -34,8 +35,11 @@
34
35
  "lint": "eslint .",
35
36
  "plantuml:up": "node scripts/local-plantuml-compose.mjs up",
36
37
  "plantuml:down": "node scripts/local-plantuml-compose.mjs down",
38
+ "structurizr:up": "node scripts/local-structurizr-compose.mjs up",
39
+ "structurizr:down": "node scripts/local-structurizr-compose.mjs down",
37
40
  "start": "npm run build && node bin/collabmd.js",
38
41
  "start:local-plantuml": "npm run build && node bin/collabmd.js --local-plantuml",
42
+ "start:local-structurizr": "npm run build && node bin/collabmd.js --local-structurizr",
39
43
  "start:prod": "node bin/collabmd.js",
40
44
  "test": "npm run test:guardrails && npm run test:unit && npm run test:integration:prebuilt && npm run test:browser && npm run test:e2e:prebuilt",
41
45
  "test:browser": "vitest run --config vitest.config.mjs --browser=chromium",
@@ -74,6 +78,7 @@
74
78
  "@codemirror/theme-one-dark": "^6.1.3",
75
79
  "@codemirror/view": "^6.43.8",
76
80
  "@excalidraw/excalidraw": "0.18.0-4872083",
81
+ "@replit/codemirror-vim": "^6.4.0",
77
82
  "@turbodocx/html-to-docx": "^1.22.0",
78
83
  "archiver": "^8.0.0",
79
84
  "highlight.js": "^11.11.1",
@@ -82,6 +87,7 @@
82
87
  "markdown-it": "^15.0.0",
83
88
  "mermaid": "^11.16.1",
84
89
  "openid-client": "^6.8.4",
90
+ "pdfjs-dist": "6.2.108",
85
91
  "react": "^19.2.8",
86
92
  "react-dom": "^19.2.8",
87
93
  "sharp": "^0.35.3",
@@ -93,8 +99,15 @@
93
99
  },
94
100
  "overrides": {
95
101
  "@excalidraw/excalidraw": {
96
- "@radix-ui/react-tabs": "1.1.13"
97
- }
102
+ "@radix-ui/react-tabs": "1.1.13",
103
+ "nanoid": "3.3.18"
104
+ },
105
+ "@excalidraw/mermaid-to-excalidraw": {
106
+ "nanoid": "5.1.16"
107
+ },
108
+ "lodash-es": "4.18.1",
109
+ "nanoid": "3.3.18",
110
+ "axios": "1.18.0"
98
111
  },
99
112
  "devDependencies": {
100
113
  "@eslint/js": "^10.0.1",
@@ -0,0 +1,101 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { spawn } from 'node:child_process';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const scriptPath = fileURLToPath(import.meta.url);
7
+ const scriptDir = dirname(scriptPath);
8
+ const projectRoot = resolve(scriptDir, '..');
9
+ const composeFile = resolve(projectRoot, 'docker-compose.yml');
10
+
11
+ function runCommand(command, args, { env = process.env } = {}) {
12
+ return new Promise((resolvePromise, rejectPromise) => {
13
+ const child = spawn(command, args, {
14
+ cwd: projectRoot,
15
+ env,
16
+ stdio: 'inherit',
17
+ });
18
+
19
+ child.on('error', rejectPromise);
20
+ child.on('exit', (code) => {
21
+ if (code === 0) {
22
+ resolvePromise();
23
+ return;
24
+ }
25
+
26
+ rejectPromise(new Error(`${command} ${args.join(' ')} exited with code ${code}`));
27
+ });
28
+ });
29
+ }
30
+
31
+ export function getLocalStructurizrHostPort() {
32
+ const rawValue = process.env.STRUCTURIZR_HOST_PORT || '19090';
33
+ const parsed = Number.parseInt(rawValue, 10);
34
+
35
+ if (!Number.isInteger(parsed) || parsed <= 0) {
36
+ throw new Error(`Invalid STRUCTURIZR_HOST_PORT: ${rawValue}`);
37
+ }
38
+
39
+ return parsed;
40
+ }
41
+
42
+ export function getLocalStructurizrServerUrl() {
43
+ return `http://127.0.0.1:${getLocalStructurizrHostPort()}`;
44
+ }
45
+
46
+ function getComposeArgs(command) {
47
+ return ['compose', '-f', composeFile, ...command];
48
+ }
49
+
50
+ export async function startLocalStructurizrComposeService({ vaultDir = process.env.COLLABMD_VAULT_DIR || '.' } = {}) {
51
+ const hostVaultDir = resolve(vaultDir);
52
+ await mkdir(resolve(hostVaultDir, '.collabmd/structurizr'), { recursive: true });
53
+ await runCommand('docker', getComposeArgs(['up', '-d', 'structurizr']), {
54
+ env: {
55
+ ...process.env,
56
+ HOST_VAULT_DIR: hostVaultDir,
57
+ STRUCTURIZR_HOST_PORT: String(getLocalStructurizrHostPort()),
58
+ },
59
+ });
60
+ return getLocalStructurizrServerUrl();
61
+ }
62
+
63
+ export async function stopLocalStructurizrComposeService() {
64
+ await runCommand('docker', getComposeArgs(['stop', 'structurizr']));
65
+ }
66
+
67
+ async function main() {
68
+ const command = process.argv[2] || 'up';
69
+
70
+ if (command === 'up') {
71
+ const url = await startLocalStructurizrComposeService();
72
+ console.log(`[structurizr] Local Structurizr renderer is available at ${url}`);
73
+ return;
74
+ }
75
+
76
+ if (command === 'down') {
77
+ await stopLocalStructurizrComposeService();
78
+ console.log('[structurizr] Local Structurizr renderer stopped');
79
+ return;
80
+ }
81
+
82
+ if (command === 'url') {
83
+ console.log(getLocalStructurizrServerUrl());
84
+ return;
85
+ }
86
+
87
+ console.error('Usage: node scripts/local-structurizr-compose.mjs [up|down|url]');
88
+ process.exit(1);
89
+ }
90
+
91
+ if (process.argv[1] && resolve(process.argv[1]) === scriptPath) {
92
+ main().catch((error) => {
93
+ if (error.code === 'ENOENT') {
94
+ console.error('[structurizr] Docker is not available. Install Docker Desktop or Docker Engine first.');
95
+ } else {
96
+ console.error(`[structurizr] ${error.message}`);
97
+ }
98
+
99
+ process.exit(1);
100
+ });
101
+ }
@@ -357,6 +357,18 @@
357
357
  <span class="ui-toolbar-button-label">Search files</span>
358
358
  </button>
359
359
 
360
+ <button class="ui-button ui-button--ghost ui-button--toolbar ui-button--toggle toolbar-vim-button" id="toggleVimModeBtn" aria-label="Enable Vim keybindings" aria-pressed="false" title="Enable Vim keybindings">
361
+ <span class="toolbar-vim-icon" aria-hidden="true">
362
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
363
+ <rect x="3" y="4" width="18" height="16" rx="2"></rect>
364
+ <path d="m7 9 3 3-3 3"></path>
365
+ <path d="M13 15h4"></path>
366
+ </svg>
367
+ </span>
368
+ <span class="ui-toolbar-button-label">Vim</span>
369
+ <span class="ui-pill-badge ui-pill-badge--muted toolbar-vim-state" id="vimModeToggleLabel">Off</span>
370
+ </button>
371
+
360
372
  <details class="toolbar-overflow-group hidden" id="exportMenuGroup">
361
373
  <summary class="ui-button ui-button--ghost ui-button--toolbar toolbar-overflow-group-toggle" aria-label="Export options" title="Export">
362
374
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
@@ -728,7 +740,7 @@
728
740
  <input type="text" class="qs-input" id="quickSwitcherInput" placeholder="Search files..." aria-label="Search files" aria-controls="quickSwitcherResults" aria-describedby="quickSwitcherHint quickSwitcherScope" autocomplete="off" spellcheck="false">
729
741
  <kbd class="qs-kbd">esc</kbd>
730
742
  </div>
731
- <div class="qs-scope hidden" id="quickSwitcherScope" role="note" aria-hidden="true">Exact, case-insensitive text in Markdown, Base, Mermaid, PlantUML, and Draw.io files. Images and Excalidraw are excluded.</div>
743
+ <div class="qs-scope hidden" id="quickSwitcherScope" role="note" aria-hidden="true">Exact, case-insensitive text in Markdown, Base, Mermaid, PlantUML, Draw.io, and Excalidraw files. Images are excluded.</div>
732
744
  <div class="qs-results" id="quickSwitcherResults" role="listbox" aria-label="Search results" aria-busy="false" tabindex="-1"></div>
733
745
  <div class="qs-hint" id="quickSwitcherHint" role="status" aria-live="polite" aria-atomic="true">Type to search files</div>
734
746
  <div class="qs-footer">
@@ -1,4 +1,7 @@
1
- import { getVaultFileKind, supportsCommentsForFilePath } from '../../../domain/file-kind.js';
1
+ import {
2
+ getVaultFileKind,
3
+ supportsCommentsForFilePath,
4
+ } from '../../../domain/file-kind.js';
2
5
 
3
6
  export const commentsFeature = {
4
7
  createCommentThread(payload) {
@@ -82,19 +85,20 @@ export const commentsFeature = {
82
85
  return;
83
86
  }
84
87
 
88
+ const isDiagram = getVaultFileKind(filePath) === 'excalidraw';
85
89
  const line = Number.isFinite(anchor?.startLine) ? anchor.startLine : null;
86
- this._pendingCommentOverviewFocus = {
87
- attempts: 0,
88
- filePath,
89
- line,
90
- threadId,
91
- };
90
+ if (!isDiagram) {
91
+ this._pendingCommentOverviewFocus = {
92
+ attempts: 0,
93
+ filePath,
94
+ line,
95
+ threadId,
96
+ };
97
+ }
92
98
  this.setSidebarTab('comments');
93
99
  this.setSidebarVisibility(true);
94
100
  this.workspaceRouteController?.preserveSidebarTabForNextFileRoute?.(filePath);
95
- this.navigation.navigateToFile(filePath, {
96
- line,
97
- });
101
+ this.navigation.navigateToFile(filePath, isDiagram ? undefined : { line });
98
102
 
99
103
  if (this.currentFilePath !== filePath || !this.session) {
100
104
  await this.workspaceRouteController.openFile(filePath);
@@ -102,6 +106,11 @@ export const commentsFeature = {
102
106
 
103
107
  this.setSidebarTab('comments');
104
108
  this.setSidebarVisibility(true);
109
+ if (isDiagram) {
110
+ this.excalidrawEmbed?.openCommentThread?.(filePath, threadId);
111
+ return;
112
+ }
113
+
105
114
  this.focusPendingCommentOverviewThread();
106
115
  },
107
116
 
@@ -446,6 +446,18 @@ export const gitFeature = {
446
446
  showToast: true,
447
447
  });
448
448
 
449
+ const structurizrDependencyChanged = [
450
+ ...workspaceChange.changedPaths,
451
+ ...workspaceChange.deletedPaths,
452
+ ...workspaceChange.renamedPaths.flatMap((entry) => [entry.oldPath, entry.newPath]),
453
+ ].some((pathValue) => /(?:\.dsl$|(?:^|\/)workspace\.json$)/iu.test(pathValue));
454
+ if (this.currentFilePath && this.isStructurizrWorkspaceFile?.(this.currentFilePath) && structurizrDependencyChanged) {
455
+ this.structurizrPreview?.queueSync({
456
+ filePath: this.currentFilePath,
457
+ source: this.session?.getText?.() ?? '',
458
+ });
459
+ }
460
+
449
461
  if (
450
462
  event.origin === 'filesystem'
451
463
  && this.currentFilePath
@@ -13,8 +13,9 @@ function getConnectedPresenceState(context) {
13
13
  const isDrawioRoute = Boolean(context.currentFilePath && context.isDrawioFile?.(context.currentFilePath));
14
14
  const isExcalidrawRoute = Boolean(context.currentFilePath && context.isExcalidrawFile?.(context.currentFilePath));
15
15
  const isImageRoute = Boolean(context.currentFilePath && context.isImageFile?.(context.currentFilePath));
16
+ const isPdfRoute = Boolean(context.currentFilePath && context.isPdfFile?.(context.currentFilePath));
16
17
  const shouldUseLobbyState = !hasEditorSession
17
- && (!context.currentFilePath || isDrawioRoute || isExcalidrawRoute || isImageRoute);
18
+ && (!context.currentFilePath || isDrawioRoute || isExcalidrawRoute || isImageRoute || isPdfRoute);
18
19
  return shouldUseLobbyState
19
20
  ? context.lobby?.getConnectionState?.() ?? context.connectionState
20
21
  : context.connectionState;
@@ -62,6 +62,7 @@ function initialize() {
62
62
  this.syncIdentityManagementUi();
63
63
  this.syncCurrentUserName();
64
64
  this.syncWrapToggle();
65
+ this.syncVimModeToggle();
65
66
  this.syncToolbarOverflowVisibility?.();
66
67
  this.syncFileHistoryButton({ mode: 'empty' });
67
68
  this.renderChat();
@@ -297,6 +298,10 @@ function bindEvents() {
297
298
  this.toggleLineWrapping();
298
299
  });
299
300
 
301
+ this.elements.toggleVimModeButton?.addEventListener('click', () => {
302
+ this.toggleVimMode();
303
+ });
304
+
300
305
  this.elements.userCount?.addEventListener('click', (event) => {
301
306
  event.preventDefault();
302
307
  this.togglePresencePanel?.();
@@ -313,7 +318,8 @@ function bindEvents() {
313
318
  });
314
319
 
315
320
  this.elements.toolbarOverflowMenu?.addEventListener('click', (event) => {
316
- if (event.target instanceof Element && event.target.closest('button')) {
321
+ const button = event.target instanceof Element ? event.target.closest('button') : null;
322
+ if (button && button !== this.elements.toggleVimModeButton) {
317
323
  this.closeToolbarOverflowMenu();
318
324
  }
319
325
  });
@@ -731,7 +737,12 @@ function toggleToolbarOverflowMenu() {
731
737
  /** @this {UiShellContext} */
732
738
  function handleThemeChange(theme) {
733
739
  this.previewRenderer.applyTheme(theme);
734
- if (!this.isExcalidrawFile(this.currentFilePath) && !this.isImageFile?.(this.currentFilePath)) {
740
+ if (
741
+ !this.isExcalidrawFile(this.currentFilePath)
742
+ && !this.isImageFile?.(this.currentFilePath)
743
+ && !this.isPdfFile?.(this.currentFilePath)
744
+ && !this.isStructurizrWorkspaceFile?.(this.currentFilePath)
745
+ ) {
735
746
  this.previewRenderer.queueRender();
736
747
  }
737
748
  this.session?.applyTheme(theme);
@@ -773,6 +784,35 @@ function getStoredLineWrapping() {
773
784
  return this.preferences.getLineWrappingEnabled();
774
785
  }
775
786
 
787
+ /** @this {UiShellContext} */
788
+ function toggleVimMode() {
789
+ const currentState = this.session?.isVimModeEnabled?.() ?? this.getStoredVimMode();
790
+ const nextState = !currentState;
791
+
792
+ this.session?.setVimMode?.(nextState);
793
+ this.preferences.setVimModeEnabled(nextState);
794
+ this.syncVimModeToggle(nextState);
795
+ }
796
+
797
+ /** @this {UiShellContext} */
798
+ function getStoredVimMode() {
799
+ return this.preferences.getVimModeEnabled();
800
+ }
801
+
802
+ /** @this {UiShellContext} */
803
+ function syncVimModeToggle(state) {
804
+ const enabled = state ?? this.session?.isVimModeEnabled?.() ?? this.getStoredVimMode();
805
+ const label = this.elements.vimModeToggleLabel;
806
+ const button = this.elements.toggleVimModeButton;
807
+ const nextLabel = enabled ? 'On' : 'Off';
808
+ if (label) label.textContent = nextLabel;
809
+ if (button) {
810
+ button.setAttribute('aria-label', `${enabled ? 'Disable' : 'Enable'} Vim keybindings`);
811
+ button.setAttribute('title', `${enabled ? 'Disable' : 'Enable'} Vim keybindings`);
812
+ button.setAttribute('aria-pressed', String(enabled));
813
+ }
814
+ }
815
+
776
816
  /** @this {UiShellContext} */
777
817
  function syncWrapToggle(state) {
778
818
  const enabled = state ?? this.session?.isLineWrappingEnabled() ?? this.getStoredLineWrapping();
@@ -836,6 +876,7 @@ export const uiFeatureShellMethods = {
836
876
  copyPreviewHeadingLink,
837
877
  createPreviewHeadingLinkUrl,
838
878
  getStoredLineWrapping,
879
+ getStoredVimMode,
839
880
  handleConnectionChange,
840
881
  handleDocumentKeydown,
841
882
  handleDocumentPointerDown,
@@ -856,7 +897,9 @@ export const uiFeatureShellMethods = {
856
897
  syncPreviewHeadingLinkButtons,
857
898
  syncVisualViewportBounds,
858
899
  syncToolbarOverflowVisibility,
900
+ syncVimModeToggle,
859
901
  syncWrapToggle,
860
902
  toggleToolbarOverflowMenu,
861
903
  toggleLineWrapping,
904
+ toggleVimMode,
862
905
  };