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
@@ -55,6 +55,21 @@
55
55
  <span class="sidebar-create-btn-label">Create</span>
56
56
  </button>
57
57
  </div>
58
+ <button
59
+ class="ui-icon-button sidebar-options-btn"
60
+ id="fileExplorerOptionsBtn"
61
+ type="button"
62
+ aria-label="File explorer options"
63
+ aria-expanded="false"
64
+ aria-haspopup="menu"
65
+ title="File explorer options"
66
+ >
67
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
68
+ <circle cx="12" cy="5" r="1"></circle>
69
+ <circle cx="12" cy="12" r="1"></circle>
70
+ <circle cx="12" cy="19" r="1"></circle>
71
+ </svg>
72
+ </button>
58
73
  </div>
59
74
  </div>
60
75
  <div class="ui-nav-tabs" id="sidebarTabs">
@@ -111,6 +126,19 @@
111
126
  <nav id="fileTree" class="file-tree" aria-label="File explorer"></nav>
112
127
  <section id="commentOverviewPanel" class="comment-overview-panel hidden" aria-label="Open comments"></section>
113
128
  <div id="gitPanel" class="git-panel hidden" aria-label="Git changes"></div>
129
+ <div
130
+ class="sidebar-resizer"
131
+ id="sidebarResizer"
132
+ role="separator"
133
+ aria-orientation="vertical"
134
+ aria-label="Resize sidebar"
135
+ aria-controls="sidebar"
136
+ aria-valuemin="220"
137
+ aria-valuemax="420"
138
+ aria-valuenow="260"
139
+ tabindex="0"
140
+ title="Drag to resize sidebar"
141
+ ></div>
114
142
  </aside>
115
143
 
116
144
  <button
@@ -138,7 +166,24 @@
138
166
  </div>
139
167
 
140
168
  <div class="toolbar-center" id="toolbarCenter">
141
- <div class="ui-segmented-control view-toggle" role="group" aria-label="View mode">
169
+ <button
170
+ class="ui-button ui-button--secondary ui-button--compact toolbar-search-trigger"
171
+ id="toolbarSearchBtn"
172
+ type="button"
173
+ aria-label="Search files and text"
174
+ aria-controls="quickSwitcher"
175
+ aria-haspopup="dialog"
176
+ aria-keyshortcuts="Meta+K Control+K"
177
+ title="Search files and text (⌘K)"
178
+ >
179
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
180
+ <circle cx="11" cy="11" r="7"></circle>
181
+ <line x1="20" y1="20" x2="16.65" y2="16.65"></line>
182
+ </svg>
183
+ <span class="toolbar-search-label">Search</span>
184
+ <kbd class="toolbar-search-kbd">⌘K</kbd>
185
+ </button>
186
+ <div class="ui-segmented-control view-toggle" id="toolbarViewToggle" role="group" aria-label="View mode">
142
187
  <button class="ui-segmented-btn active view-btn" data-view="split" aria-label="Split view" aria-pressed="true" title="Split view">
143
188
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
144
189
  <rect x="3" y="3" width="18" height="18" rx="2"></rect>
@@ -186,7 +231,7 @@
186
231
 
187
232
  <div class="user-avatars toolbar-presence-cluster" id="userAvatars"></div>
188
233
 
189
- <section class="presence-panel hidden" id="presencePanel" aria-label="Online users" aria-hidden="true">
234
+ <section class="collaboration-popover presence-panel hidden" id="presencePanel" aria-label="Online users" aria-hidden="true">
190
235
  <div class="presence-panel-header">
191
236
  <div>
192
237
  <h2 class="presence-panel-title">Online now</h2>
@@ -197,7 +242,7 @@
197
242
  </section>
198
243
 
199
244
  <div class="chat-container" id="chatContainer">
200
- <button class="ui-button ui-button--ghost ui-button--toolbar chat-toggle-btn" id="chatToggleBtn" aria-label="Open team chat" aria-expanded="false" title="Team chat">
245
+ <button type="button" class="ui-button ui-button--ghost ui-button--toolbar chat-toggle-btn" id="chatToggleBtn" aria-label="Open team chat" aria-controls="chatPanel" aria-expanded="false" title="Team chat">
201
246
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
202
247
  <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
203
248
  </svg>
@@ -206,23 +251,44 @@
206
251
  </button>
207
252
  <div class="toast-container chat-toast-container" id="chatToastContainer" aria-live="polite" aria-atomic="true"></div>
208
253
 
209
- <section class="chat-panel hidden" id="chatPanel" aria-label="Team chat">
254
+ <section class="collaboration-popover chat-panel hidden" id="chatPanel" role="dialog" aria-labelledby="chatPanelTitle">
210
255
  <div class="chat-panel-header">
211
- <div>
212
- <h2 class="chat-panel-title">Team chat</h2>
213
- <p class="chat-panel-status" id="chatStatus">Syncing...</p>
256
+ <div class="chat-panel-heading">
257
+ <h2 class="chat-panel-title" id="chatPanelTitle">Team chat</h2>
258
+ <p class="chat-panel-status" id="chatStatus" role="status" aria-live="polite" aria-atomic="true">Syncing...</p>
259
+ </div>
260
+ <div class="chat-panel-actions">
261
+ <span class="ui-status-badge ui-status-badge--accent chat-notification-status hidden" id="chatNotificationStatus" role="status"></span>
262
+ <button
263
+ type="button"
264
+ class="ui-button ui-button--ghost ui-button--compact chat-notification-action"
265
+ id="chatNotificationBtn"
266
+ >Enable desktop alerts</button>
267
+ <button
268
+ type="button"
269
+ class="ui-icon-button chat-notification-mute-action hidden"
270
+ id="chatNotificationMuteBtn"
271
+ aria-label="Mute desktop alerts for one hour"
272
+ aria-pressed="false"
273
+ title="Mute desktop alerts for one hour"
274
+ >
275
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
276
+ <path d="M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9"></path>
277
+ <path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
278
+ <path class="chat-notification-mute-slash" d="m4 4 16 16"></path>
279
+ </svg>
280
+ </button>
281
+ <button type="button" class="ui-icon-button chat-close-btn" id="chatCloseBtn" aria-label="Close team chat" title="Close team chat">
282
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
283
+ <line x1="18" y1="6" x2="6" y2="18"></line>
284
+ <line x1="6" y1="6" x2="18" y2="18"></line>
285
+ </svg>
286
+ </button>
214
287
  </div>
215
- <button
216
- class="ui-button ui-button--secondary ui-button--compact ui-button--action ui-button--surface ui-button--pill chat-notification-action"
217
- id="chatNotificationBtn"
218
- type="button"
219
- >
220
- Enable alerts
221
- </button>
222
288
  </div>
223
289
 
224
- <div class="chat-empty-state" id="chatEmptyState">
225
- Temporary room chat for whoever is online right now.
290
+ <div class="ui-empty-state ui-empty-state--compact chat-empty-state" id="chatEmptyState">
291
+ Messages are visible to everyone in this room.
226
292
  </div>
227
293
  <div class="chat-messages hidden" id="chatMessages" role="log" aria-live="polite" aria-relevant="additions text"></div>
228
294
 
@@ -231,10 +297,11 @@
231
297
  type="text"
232
298
  class="ui-input chat-input"
233
299
  id="chatInput"
234
- placeholder="Send a quick update..."
300
+ placeholder="Message the room..."
235
301
  autocomplete="off"
236
302
  spellcheck="true"
237
303
  maxlength="280"
304
+ enterkeyhint="send"
238
305
  aria-label="Team chat message"
239
306
  >
240
307
  <button type="submit" class="ui-button ui-button--primary chat-send-btn">Send</button>
@@ -290,6 +357,18 @@
290
357
  <span class="ui-toolbar-button-label">Search files</span>
291
358
  </button>
292
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
+
293
372
  <details class="toolbar-overflow-group hidden" id="exportMenuGroup">
294
373
  <summary class="ui-button ui-button--ghost ui-button--toolbar toolbar-overflow-group-toggle" aria-label="Export options" title="Export">
295
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">
@@ -646,29 +725,32 @@
646
725
 
647
726
  <div class="toast-container" id="toastContainer"></div>
648
727
 
649
- <div class="qs-overlay" id="quickSwitcher">
728
+ <div class="qs-overlay" id="quickSwitcher" role="dialog" aria-modal="true" aria-labelledby="quickSwitcherTitle" aria-hidden="true">
650
729
  <div class="qs-modal">
730
+ <h2 class="sr-only" id="quickSwitcherTitle">Search files and text</h2>
651
731
  <div class="qs-mode-tabs" role="tablist" aria-label="Search mode">
652
- <button class="qs-mode-tab active" type="button" role="tab" aria-selected="true" data-qs-mode="files">Files</button>
653
- <button class="qs-mode-tab" type="button" role="tab" aria-selected="false" data-qs-mode="text">Text</button>
732
+ <button class="qs-mode-tab active" id="quickSwitcherFilesTab" type="button" role="tab" aria-controls="quickSwitcherResults" aria-selected="true" data-qs-mode="files">Files</button>
733
+ <button class="qs-mode-tab" id="quickSwitcherTextTab" type="button" role="tab" aria-controls="quickSwitcherResults" aria-selected="false" data-qs-mode="text">Text</button>
654
734
  </div>
655
735
  <div class="qs-input-wrap">
656
736
  <svg class="qs-search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
657
737
  <circle cx="11" cy="11" r="8"></circle>
658
738
  <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
659
739
  </svg>
660
- <input type="text" class="qs-input" id="quickSwitcherInput" placeholder="Search files..." autocomplete="off" spellcheck="false">
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">
661
741
  <kbd class="qs-kbd">esc</kbd>
662
742
  </div>
663
- <div class="qs-results" id="quickSwitcherResults"></div>
664
- <div class="qs-hint" id="quickSwitcherHint">Type to search files</div>
665
- <div class="qs-footer" aria-hidden="true">
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>
744
+ <div class="qs-results" id="quickSwitcherResults" role="listbox" aria-label="Search results" aria-busy="false" tabindex="-1"></div>
745
+ <div class="qs-hint" id="quickSwitcherHint" role="status" aria-live="polite" aria-atomic="true">Type to search files</div>
746
+ <div class="qs-footer">
666
747
  <span><kbd>↑</kbd><kbd>↓</kbd> Navigate</span>
667
748
  <span><kbd>↵</kbd> Open</span>
668
749
  <span><kbd>Tab</kbd> Switch mode</span>
669
750
  </div>
670
751
  </div>
671
752
  </div>
753
+ <script vite-ignore src="./app-config.js"></script>
672
754
  <script type="module" src="./main-entry.js"></script>
673
755
  </body>
674
756
  </html>
@@ -1,12 +1,8 @@
1
1
  import darkHighlightThemeUrl from '../assets/vendor/highlight/github-dark.min.css?url';
2
2
  import lightHighlightThemeUrl from '../assets/vendor/highlight/github.min.css?url';
3
- import '@fontsource-variable/inter';
4
- import '@fontsource/jetbrains-mono';
5
3
  import '../styles/base.css';
6
4
  import '../styles/style.css';
7
5
 
8
- import { ensureRuntimeConfigLoaded } from '../infrastructure/runtime-config-loader.js';
9
-
10
6
  function ensureHighlightThemeStylesheet() {
11
7
  let themeStylesheet = document.getElementById('hljs-theme');
12
8
  if (!(themeStylesheet instanceof HTMLLinkElement)) {
@@ -21,6 +17,5 @@ function ensureHighlightThemeStylesheet() {
21
17
  themeStylesheet.dataset.lightHref = lightHighlightThemeUrl;
22
18
  }
23
19
 
24
- await ensureRuntimeConfigLoaded();
25
20
  ensureHighlightThemeStylesheet();
26
21
  await import('../main.js');
@@ -1,3 +1,41 @@
1
+ import { getUserAvatarTextColor } from '../../domain/room.js';
2
+
3
+ const CHAT_ALERT_MUTE_STORAGE_KEY = 'collabmd-chat-alerts-muted-until';
4
+ const CHAT_ALERT_MUTE_DURATION_MS = 60 * 60 * 1000;
5
+
6
+ function getChatConnectionState(context) {
7
+ return context.lobby?.getConnectionState?.() ?? { status: 'connected', unreachable: false };
8
+ }
9
+
10
+ function isChatAtBottom(list) {
11
+ return list.scrollHeight - list.scrollTop - list.clientHeight <= 24;
12
+ }
13
+
14
+ function getChatAlertMuteUntil() {
15
+ try {
16
+ const mutedUntil = Number(globalThis.localStorage?.getItem(CHAT_ALERT_MUTE_STORAGE_KEY));
17
+ if (!Number.isFinite(mutedUntil) || mutedUntil <= Date.now()) {
18
+ globalThis.localStorage?.removeItem(CHAT_ALERT_MUTE_STORAGE_KEY);
19
+ return 0;
20
+ }
21
+ return mutedUntil;
22
+ } catch {
23
+ return 0;
24
+ }
25
+ }
26
+
27
+ function setChatAlertMuteUntil(mutedUntil) {
28
+ try {
29
+ if (mutedUntil > 0) {
30
+ globalThis.localStorage?.setItem(CHAT_ALERT_MUTE_STORAGE_KEY, String(mutedUntil));
31
+ } else {
32
+ globalThis.localStorage?.removeItem(CHAT_ALERT_MUTE_STORAGE_KEY);
33
+ }
34
+ } catch {
35
+ // Local storage can be unavailable in private or restricted browsing modes.
36
+ }
37
+ }
38
+
1
39
  export const chatFeature = {
2
40
  updateChatMessages(messages, { initial = false } = {}) {
3
41
  const previousIds = new Set(this.chatMessageIds);
@@ -36,7 +74,7 @@ export const chatFeature = {
36
74
 
37
75
  toggleChatPanel() {
38
76
  if (this.chatIsOpen) {
39
- this.closeChatPanel();
77
+ this.closeChatPanel({ restoreFocus: true });
40
78
  return;
41
79
  }
42
80
 
@@ -46,24 +84,27 @@ export const chatFeature = {
46
84
  openChatPanel() {
47
85
  this.chatIsOpen = true;
48
86
  this.chatUnreadCount = 0;
49
- this.renderChat();
87
+ this.renderChat({ stickToBottom: true });
50
88
  requestAnimationFrame(() => {
51
89
  this.elements.chatInput?.focus();
52
- this.scrollChatToBottom();
53
90
  });
54
91
  },
55
92
 
56
- closeChatPanel() {
93
+ closeChatPanel({ restoreFocus = false } = {}) {
57
94
  if (!this.chatIsOpen) {
58
95
  return;
59
96
  }
60
97
 
61
98
  this.chatIsOpen = false;
62
- this.renderChat();
99
+ this.renderChat({ messagesChanged: false });
100
+ if (restoreFocus) {
101
+ requestAnimationFrame(() => this.elements.chatToggleButton?.focus());
102
+ }
63
103
  },
64
104
 
65
105
  handleChatSubmit() {
66
- if (!this.isTabActive) {
106
+ const connectionState = getChatConnectionState(this);
107
+ if (this.isTabActive === false || connectionState.status !== 'connected') {
67
108
  return;
68
109
  }
69
110
 
@@ -84,32 +125,41 @@ export const chatFeature = {
84
125
  return;
85
126
  }
86
127
 
87
- this.renderChat();
128
+ this.renderChat({ stickToBottom: true });
88
129
  },
89
130
 
90
- renderChat() {
131
+ renderChat({ messagesChanged = true, stickToBottom = false } = {}) {
91
132
  this.elements.chatContainer?.classList.toggle('is-open', this.chatIsOpen);
92
133
  this.elements.chatPanel?.classList.toggle('hidden', !this.chatIsOpen);
93
134
 
94
135
  this.syncChatToggleButton();
95
136
  this.syncChatNotificationButton();
96
-
97
137
  const list = this.elements.chatMessages;
98
138
  const emptyState = this.elements.chatEmptyState;
139
+ const connectionState = getChatConnectionState(this);
140
+ const isConnected = connectionState.status === 'connected';
141
+ const canSend = isConnected && this.chatInitialSyncComplete && this.isTabActive !== false;
99
142
 
100
143
  if (this.elements.chatStatus) {
101
- this.elements.chatStatus.textContent = this.chatInitialSyncComplete
102
- ? `${this.globalUsers.length} online`
103
- : 'Syncing...';
144
+ this.elements.chatStatus.textContent = !this.chatInitialSyncComplete
145
+ ? 'Syncing...'
146
+ : !isConnected
147
+ ? connectionState.unreachable ? 'Server unreachable' : 'Reconnecting...'
148
+ : `${this.globalUsers.length} online`;
104
149
  }
105
150
 
106
- if (!list) {
107
- return;
151
+ const sendButton = this.elements.chatForm?.querySelector?.('button[type="submit"]');
152
+ if (sendButton) {
153
+ sendButton.disabled = !canSend;
154
+ sendButton.title = canSend ? 'Send message' : 'Chat is unavailable while disconnected';
108
155
  }
109
156
 
110
- list.replaceChildren();
157
+ if (!list || !this.chatIsOpen || !messagesChanged) {
158
+ return;
159
+ }
111
160
 
112
161
  if (this.chatMessages.length === 0) {
162
+ list.replaceChildren();
113
163
  emptyState?.classList.remove('hidden');
114
164
  list.classList.add('hidden');
115
165
  return;
@@ -118,13 +168,14 @@ export const chatFeature = {
118
168
  emptyState?.classList.add('hidden');
119
169
  list.classList.remove('hidden');
120
170
 
171
+ const wasAtBottom = stickToBottom || isChatAtBottom(list);
121
172
  const fragment = document.createDocumentFragment();
122
173
  this.chatMessages.forEach((message) => {
123
174
  fragment.appendChild(this.createChatMessageElement(message));
124
175
  });
125
- list.appendChild(fragment);
176
+ list.replaceChildren(fragment);
126
177
 
127
- if (this.chatIsOpen) {
178
+ if (wasAtBottom) {
128
179
  this.scrollChatToBottom();
129
180
  }
130
181
  },
@@ -137,7 +188,9 @@ export const chatFeature = {
137
188
 
138
189
  const avatar = document.createElement('div');
139
190
  avatar.className = 'chat-message-avatar';
140
- avatar.style.backgroundColor = message.userColor || 'var(--color-primary)';
191
+ const avatarColor = message.userColor || 'var(--color-primary-active)';
192
+ avatar.style.backgroundColor = avatarColor;
193
+ avatar.style.color = getUserAvatarTextColor(message.userColor);
141
194
  avatar.textContent = (message.userName || '?').charAt(0).toUpperCase();
142
195
  avatar.setAttribute('aria-hidden', 'true');
143
196
 
@@ -160,7 +213,7 @@ export const chatFeature = {
160
213
  const fileLabel = this.getChatMessageFileLabel(message.filePath);
161
214
  if (fileLabel) {
162
215
  const file = document.createElement('span');
163
- file.className = 'chat-message-file';
216
+ file.className = 'ui-pill chat-message-file';
164
217
  file.textContent = fileLabel;
165
218
  meta.append(file);
166
219
  }
@@ -212,6 +265,92 @@ export const chatFeature = {
212
265
  return `${sender}: ${compactText}`;
213
266
  },
214
267
 
268
+ syncChatNotificationButton() {
269
+ const button = this.elements.chatNotificationButton;
270
+ const muteButton = this.elements.chatNotificationMuteButton;
271
+ const status = this.elements.chatNotificationStatus;
272
+ if (!button && !muteButton && !status) {
273
+ return;
274
+ }
275
+
276
+ const permission = this.notifications?.getPermission?.() ?? 'unsupported';
277
+ const enabled = permission === 'granted';
278
+ const muted = enabled && getChatAlertMuteUntil() > 0;
279
+ const blocked = permission === 'denied';
280
+ const statusState = enabled
281
+ ? {
282
+ text: muted ? 'Alerts muted' : 'Alerts on',
283
+ label: muted ? 'Desktop alerts muted' : 'Desktop alerts enabled',
284
+ }
285
+ : blocked
286
+ ? {
287
+ text: 'Alerts blocked',
288
+ label: 'Desktop alerts blocked in browser settings',
289
+ }
290
+ : null;
291
+
292
+ if (button) {
293
+ button.classList.toggle('hidden', permission !== 'default');
294
+ button.disabled = false;
295
+ button.removeAttribute('aria-pressed');
296
+ button.textContent = 'Enable desktop alerts';
297
+ button.title = 'Allow desktop alerts for new chat messages';
298
+ }
299
+
300
+ if (muteButton) {
301
+ muteButton.classList.toggle('hidden', !enabled);
302
+ muteButton.disabled = false;
303
+ muteButton.setAttribute('aria-pressed', String(muted));
304
+ const muteLabel = muted ? 'Unmute desktop alerts' : 'Mute desktop alerts for one hour';
305
+ muteButton.setAttribute('aria-label', muteLabel);
306
+ muteButton.title = muteLabel;
307
+ }
308
+
309
+ if (status) {
310
+ status.classList.toggle('hidden', !statusState);
311
+ status.textContent = statusState?.text ?? '';
312
+ if (statusState) {
313
+ status.setAttribute('aria-label', statusState.label);
314
+ status.title = statusState.label;
315
+ } else {
316
+ status.removeAttribute('aria-label');
317
+ status.removeAttribute('title');
318
+ }
319
+ status.classList.toggle('ui-status-badge--accent', enabled && !muted);
320
+ status.classList.toggle('ui-status-badge--muted', blocked || muted);
321
+ }
322
+ },
323
+
324
+ async handleChatNotificationToggle() {
325
+ const permission = await this.notifications?.requestPermission?.();
326
+ this.syncChatNotificationButton();
327
+
328
+ if (permission === 'granted') {
329
+ return;
330
+ }
331
+
332
+ if (permission === 'denied') {
333
+ (this.chatToastController ?? this.toastController)?.show(
334
+ 'Desktop alerts are blocked. Allow them in browser site settings.',
335
+ { duration: 5000, tone: 'warning' },
336
+ );
337
+ return;
338
+ }
339
+
340
+ if (permission === 'unsupported') {
341
+ (this.chatToastController ?? this.toastController)?.show(
342
+ 'This browser does not support desktop alerts.',
343
+ { duration: 5000, tone: 'warning' },
344
+ );
345
+ }
346
+ },
347
+
348
+ toggleChatNotificationMute() {
349
+ const mutedUntil = getChatAlertMuteUntil();
350
+ setChatAlertMuteUntil(mutedUntil > 0 ? 0 : Date.now() + CHAT_ALERT_MUTE_DURATION_MS);
351
+ this.renderChat({ messagesChanged: false });
352
+ },
353
+
215
354
  syncChatToggleButton() {
216
355
  const button = this.elements.chatToggleButton;
217
356
  const badge = this.elements.chatToggleBadge;
@@ -224,6 +363,7 @@ export const chatFeature = {
224
363
 
225
364
  button.classList.toggle('is-active', this.chatIsOpen);
226
365
  button.classList.toggle('is-unread', shouldEmphasizeUnread);
366
+ button.setAttribute('aria-controls', 'chatPanel');
227
367
  button.setAttribute('aria-expanded', String(this.chatIsOpen));
228
368
  button.setAttribute(
229
369
  'aria-label',
@@ -241,123 +381,39 @@ export const chatFeature = {
241
381
  badge.textContent = this.chatUnreadCount > 9 ? '9+' : String(this.chatUnreadCount);
242
382
  },
243
383
 
244
- syncChatNotificationButton() {
245
- const button = this.elements.chatNotificationButton;
246
- if (!button) {
247
- return;
248
- }
249
-
250
- const permission = this.notifications.getPermission();
251
- this.chatNotificationPermission = permission;
252
-
253
- let label = 'Enable alerts';
254
- let title = 'Enable browser notifications for new chat messages';
255
- let pressed = false;
256
-
257
- if (permission === 'unsupported') {
258
- label = 'Alerts unavailable';
259
- title = 'Browser notifications are unavailable here';
260
- } else if (permission === 'denied') {
261
- label = 'Alerts off';
262
- title = 'Browser notifications are blocked for this site';
263
- } else if (permission === 'granted') {
264
- pressed = this.chatNotificationsEnabled;
265
- label = pressed ? 'Alerts on' : 'Alerts off';
266
- title = pressed
267
- ? 'Disable browser notifications for chat'
268
- : 'Enable browser notifications for chat';
269
- }
270
-
271
- button.textContent = label;
272
- button.title = title;
273
- button.setAttribute('aria-pressed', String(pressed));
274
- button.classList.toggle('is-enabled', pressed);
275
- button.classList.toggle('is-blocked', permission === 'denied');
276
- },
277
-
278
- async handleChatNotificationToggle() {
279
- const permission = this.notifications.getPermission();
280
- this.chatNotificationPermission = permission;
281
-
282
- if (permission === 'unsupported') {
283
- this.toastController.show('Browser notifications are unavailable here');
284
- this.syncChatNotificationButton();
285
- return;
286
- }
287
-
288
- if (permission === 'denied') {
289
- this.chatNotificationsEnabled = false;
290
- this.preferences.setChatNotificationsEnabled(false);
291
- this.toastController.show('Browser notifications are blocked for this site');
292
- this.syncChatNotificationButton();
293
- return;
294
- }
295
-
296
- if (permission === 'default') {
297
- const nextPermission = await this.notifications.requestPermission();
298
- this.chatNotificationPermission = nextPermission;
299
-
300
- if (nextPermission !== 'granted') {
301
- this.chatNotificationsEnabled = false;
302
- this.preferences.setChatNotificationsEnabled(false);
303
- if (nextPermission === 'denied') {
304
- this.toastController.show('Browser notifications were blocked');
305
- }
306
- this.syncChatNotificationButton();
307
- return;
308
- }
309
-
310
- this.chatNotificationsEnabled = true;
311
- this.preferences.setChatNotificationsEnabled(true);
312
- this.syncChatNotificationButton();
313
- return;
314
- }
315
-
316
- this.chatNotificationsEnabled = !this.chatNotificationsEnabled;
317
- this.preferences.setChatNotificationsEnabled(this.chatNotificationsEnabled);
318
- this.syncChatNotificationButton();
319
- },
320
-
321
384
  maybeNotifyChatMessage(message) {
322
385
  if (!this.chatInitialSyncComplete) {
323
386
  return;
324
387
  }
325
388
 
326
- if (document.hidden) {
327
- this.maybeShowBrowserChatNotification(message);
389
+ if (this.isTabActive === false || this.chatIsOpen || getChatAlertMuteUntil() > 0) {
328
390
  return;
329
391
  }
330
392
 
331
- if (!this.isTabActive || this.chatIsOpen) {
332
- return;
393
+ const pageHidden = Boolean(
394
+ globalThis.document?.hidden || globalThis.document?.visibilityState === 'hidden',
395
+ );
396
+ const pageFocused = typeof globalThis.document?.hasFocus === 'function'
397
+ ? globalThis.document.hasFocus()
398
+ : !pageHidden;
399
+ let notification = null;
400
+ if (pageHidden || !pageFocused) {
401
+ notification = this.notifications?.show?.({
402
+ body: String(message?.text ?? '').replace(/\s+/g, ' ').trim(),
403
+ onClick: () => {
404
+ window.focus?.();
405
+ notification?.close?.();
406
+ this.openChatPanel();
407
+ },
408
+ tag: `collabmd-chat-${message?.id ?? 'message'}`,
409
+ title: `New message from ${message?.userName || 'Someone'}`,
410
+ });
333
411
  }
334
412
 
335
- (this.chatToastController ?? this.toastController).show(this.formatChatToastMessage(message), 4000);
336
- },
337
-
338
- maybeShowBrowserChatNotification(message) {
339
- if (!this.chatNotificationsEnabled || this.notifications.getPermission() !== 'granted') {
413
+ if (notification) {
340
414
  return;
341
415
  }
342
416
 
343
- const title = `CollabMD chat • ${message.userName}`;
344
- const fileLabel = this.getChatMessageFileLabel(message.filePath);
345
- const body = fileLabel ? `${fileLabel}: ${message.text}` : message.text;
346
- const notification = this.notifications.createNotification({
347
- body,
348
- onClick: () => {
349
- if (message.filePath) {
350
- this.navigation.navigateToFile(message.filePath);
351
- }
352
- },
353
- tag: `collabmd-chat-${message.id}`,
354
- title,
355
- });
356
-
357
- if (notification) {
358
- setTimeout(() => {
359
- notification.close?.();
360
- }, 6000);
361
- }
417
+ (this.chatToastController ?? this.toastController).show(this.formatChatToastMessage(message), 4000);
362
418
  },
363
419
  };