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
@@ -9,6 +9,8 @@ export class OutlineController {
9
9
  this.activeHeadingId = null;
10
10
  this.headings = [];
11
11
  this.pinnedHeadingId = null;
12
+ this.pdfOutline = null;
13
+ this.onNavigateToPdfOutline = null;
12
14
  this.onNavigateToHeading = onNavigateToHeading;
13
15
  this.onWillOpen = onWillOpen;
14
16
  this.panel = document.getElementById('outlinePanel');
@@ -81,6 +83,11 @@ export class OutlineController {
81
83
  return;
82
84
  }
83
85
 
86
+ if (this.pdfOutline) {
87
+ this.renderPdfOutline();
88
+ return;
89
+ }
90
+
84
91
  const previewContent = document.getElementById('previewContent');
85
92
  const headings = previewContent?.querySelectorAll('h1, h2, h3, h4, h5, h6') ?? [];
86
93
 
@@ -124,6 +131,85 @@ export class OutlineController {
124
131
  this.observeHeadings(Array.from(headings));
125
132
  }
126
133
 
134
+ setPdfOutline(outline, onNavigate) {
135
+ this.cleanup();
136
+ this.pdfOutline = Array.isArray(outline) && outline.length > 0 ? outline : null;
137
+ this.onNavigateToPdfOutline = this.pdfOutline ? onNavigate : null;
138
+ this.activeHeadingId = null;
139
+ this.toggleButton?.classList.toggle('hidden', !this.pdfOutline);
140
+
141
+ if (!this.pdfOutline) {
142
+ this.navigation?.replaceChildren();
143
+ this.close();
144
+ return;
145
+ }
146
+
147
+ if (this.outlineOpen) {
148
+ this.renderPdfOutline();
149
+ }
150
+ }
151
+
152
+ clearPdfOutline() {
153
+ this.pdfOutline = null;
154
+ this.onNavigateToPdfOutline = null;
155
+ this.activeHeadingId = null;
156
+ this.pinnedHeadingId = null;
157
+ this.navigation?.replaceChildren();
158
+ this.toggleButton?.classList.add('hidden');
159
+ this.close();
160
+ }
161
+
162
+ renderPdfOutline() {
163
+ if (!this.navigation || !this.pdfOutline) {
164
+ return;
165
+ }
166
+
167
+ this.cleanup();
168
+ this.navigation.replaceChildren();
169
+ const fragment = document.createDocumentFragment();
170
+ let itemIndex = 0;
171
+ const appendItems = (items, level = 1) => {
172
+ items.forEach((item) => {
173
+ if (!item || typeof item !== 'object') {
174
+ return;
175
+ }
176
+
177
+ const title = String(item.title ?? '').trim();
178
+ if (title) {
179
+ const id = `pdf-outline-${itemIndex}`;
180
+ const button = document.createElement('button');
181
+ button.type = 'button';
182
+ button.className = 'outline-item';
183
+ button.dataset.level = String(Math.min(level, 6));
184
+ button.dataset.target = id;
185
+ button.title = title;
186
+ button.textContent = title;
187
+ button.addEventListener('click', () => {
188
+ this.setActiveItem(id, { behavior: 'auto' });
189
+ this.onNavigateToPdfOutline?.(item);
190
+ if (this.mobileBreakpointQuery.matches) {
191
+ this.close();
192
+ }
193
+ });
194
+ fragment.appendChild(button);
195
+ itemIndex += 1;
196
+ }
197
+
198
+ if (Array.isArray(item.items)) {
199
+ appendItems(item.items, Math.min(level + 1, 6));
200
+ }
201
+ });
202
+ };
203
+
204
+ appendItems(this.pdfOutline);
205
+ if (fragment.childNodes.length === 0) {
206
+ this.navigation.innerHTML = '<div class="outline-empty">No outline found</div>';
207
+ return;
208
+ }
209
+
210
+ this.navigation.appendChild(fragment);
211
+ }
212
+
127
213
  cleanup({ preservePinnedHeading = false } = {}) {
128
214
  if (this.activeHeadingFrame) {
129
215
  cancelAnimationFrame(this.activeHeadingFrame);
@@ -1,5 +1,6 @@
1
1
  import { stripVaultFileExtension } from '../../domain/file-kind.js';
2
2
  import { escapeHtml } from '../domain/vault-utils.js';
3
+ import { getVaultFileIconSvg } from './file-icon-svg.js';
3
4
  import {
4
5
  DEFAULT_SEARCH_DEBOUNCE_MS,
5
6
  QuickSwitcherTextSearchRunner,
@@ -7,11 +8,19 @@ import {
7
8
  formatMatchCount,
8
9
  } from './quick-switcher-text-search.js';
9
10
 
10
- const MAX_VISIBLE_RESULTS = 12;
11
+ const MAX_VISIBLE_RESULTS = 30;
12
+ const NO_RECENT_FILE_RANK = Number.MAX_SAFE_INTEGER;
13
+ const FILE_RESULT_ID_PREFIX = 'quick-switcher-file-';
14
+ const TEXT_RESULT_ID_PREFIX = 'quick-switcher-text-';
15
+
16
+ function getRawFileName(filePath) {
17
+ return String(filePath ?? '').split('/').pop() || String(filePath ?? '');
18
+ }
11
19
 
12
20
  function createCorpusEntry(filePath) {
13
21
  const displayName = stripVaultFileExtension(filePath);
14
22
  const fileName = displayName.split('/').pop() || displayName;
23
+ const rawFileName = getRawFileName(filePath);
15
24
 
16
25
  return {
17
26
  displayName,
@@ -20,6 +29,7 @@ function createCorpusEntry(filePath) {
20
29
  lowerDisplayName: displayName.toLowerCase(),
21
30
  lowerFileName: fileName.toLowerCase(),
22
31
  lowerPath: String(filePath).toLowerCase(),
32
+ lowerRawFileName: rawFileName.toLowerCase(),
23
33
  };
24
34
  }
25
35
 
@@ -33,9 +43,135 @@ function getDirPath(filePath) {
33
43
  return displayName.includes('/') ? displayName.substring(0, displayName.lastIndexOf('/')) : '';
34
44
  }
35
45
 
46
+ function createRangeIndices(start, length) {
47
+ return Array.from({ length }, (_, index) => start + index);
48
+ }
49
+
50
+ function findFuzzyMatch(text, query) {
51
+ const indices = [];
52
+ let queryIndex = 0;
53
+ let score = 0;
54
+ let consecutiveBonus = 0;
55
+
56
+ for (let index = 0; index < text.length && queryIndex < query.length; index += 1) {
57
+ if (text[index] !== query[queryIndex]) {
58
+ consecutiveBonus = 0;
59
+ continue;
60
+ }
61
+
62
+ queryIndex += 1;
63
+ indices.push(index);
64
+ consecutiveBonus += 1;
65
+ score += consecutiveBonus;
66
+
67
+ if (
68
+ index === 0
69
+ || text[index - 1] === '/'
70
+ || text[index - 1] === '-'
71
+ || text[index - 1] === '_'
72
+ || text[index - 1] === ' '
73
+ ) {
74
+ score += 5;
75
+ }
76
+ }
77
+
78
+ return queryIndex === query.length && score > 0
79
+ ? { indices, score }
80
+ : null;
81
+ }
82
+
83
+ function findCorpusMatch(entry, query) {
84
+ const fileIndex = entry.lowerFileName.indexOf(query);
85
+ if (fileIndex >= 0) {
86
+ return {
87
+ indices: createRangeIndices(fileIndex + (entry.displayName.length - entry.fileName.length), query.length),
88
+ score: 100 + (1 / entry.fileName.length),
89
+ };
90
+ }
91
+
92
+ const pathIndex = entry.lowerDisplayName.indexOf(query);
93
+ if (pathIndex >= 0) {
94
+ return {
95
+ indices: createRangeIndices(pathIndex, query.length),
96
+ score: 50 + (1 / entry.displayName.length),
97
+ };
98
+ }
99
+
100
+ const rawFileIndex = entry.lowerRawFileName.indexOf(query);
101
+ if (rawFileIndex >= 0) {
102
+ return {
103
+ indices: createRangeIndices(
104
+ rawFileIndex + (entry.displayName.length - entry.fileName.length),
105
+ query.length,
106
+ ),
107
+ score: 40 + (1 / entry.lowerRawFileName.length),
108
+ };
109
+ }
110
+
111
+ const rawPathIndex = entry.lowerPath.indexOf(query);
112
+ if (rawPathIndex >= 0) {
113
+ return {
114
+ indices: createRangeIndices(rawPathIndex, query.length),
115
+ score: 25 + (1 / entry.lowerPath.length),
116
+ };
117
+ }
118
+
119
+ return findFuzzyMatch(entry.lowerDisplayName, query);
120
+ }
121
+
122
+ function splitMatchIndices(entry, indices = []) {
123
+ const fileNameStart = entry.displayName.length - entry.fileName.length;
124
+ return {
125
+ dirPath: indices.filter((index) => index < fileNameStart),
126
+ fileName: indices
127
+ .filter((index) => index >= fileNameStart)
128
+ .map((index) => index - fileNameStart),
129
+ };
130
+ }
131
+
132
+ function highlightText(text, indices = []) {
133
+ if (indices.length === 0) {
134
+ return escapeHtml(text);
135
+ }
136
+
137
+ const matchedIndices = new Set(indices);
138
+ let result = '';
139
+ let runStart = null;
140
+ let cursor = 0;
141
+
142
+ const appendRun = (runEnd) => {
143
+ result += escapeHtml(text.slice(cursor, runStart));
144
+ result += `<mark>${escapeHtml(text.slice(runStart, runEnd + 1))}</mark>`;
145
+ cursor = runEnd + 1;
146
+ runStart = null;
147
+ };
148
+
149
+ for (let index = 0; index < text.length; index += 1) {
150
+ if (!matchedIndices.has(index)) {
151
+ if (runStart !== null) {
152
+ appendRun(index - 1);
153
+ }
154
+ continue;
155
+ }
156
+
157
+ if (runStart === null) {
158
+ runStart = index;
159
+ }
160
+ }
161
+
162
+ if (runStart !== null) {
163
+ appendRun(text.length - 1);
164
+ }
165
+
166
+ result += escapeHtml(text.slice(cursor));
167
+ return result;
168
+ }
169
+
36
170
  export class QuickSwitcherController {
37
171
  constructor({
38
172
  getFileList,
173
+ getFileMetadata = () => [],
174
+ getRecentFiles = () => [],
39
175
  getSearchConfig = () => ({}),
40
176
  onFileSelect,
41
177
  onTextMatchSelect = null,
@@ -43,6 +179,8 @@ export class QuickSwitcherController {
43
179
  searchText = null,
44
180
  }) {
45
181
  this.getFileList = getFileList;
182
+ this.getFileMetadata = getFileMetadata;
183
+ this.getRecentFiles = getRecentFiles;
46
184
  this.getSearchConfig = getSearchConfig;
47
185
  this.onFileSelect = onFileSelect;
48
186
  this.onTextMatchSelect = onTextMatchSelect;
@@ -53,14 +191,19 @@ export class QuickSwitcherController {
53
191
  this.input = document.getElementById('quickSwitcherInput');
54
192
  this.resultsList = document.getElementById('quickSwitcherResults');
55
193
  this.hint = document.getElementById('quickSwitcherHint');
194
+ this.scope = document.getElementById('quickSwitcherScope');
56
195
  this.modeTabs = Array.from(document.querySelectorAll?.('[data-qs-mode]') ?? []);
57
196
 
58
197
  this.filteredFiles = [];
198
+ this.fileMatches = new Map();
199
+ this.fileMatchCount = 0;
200
+ this.fileResultsTruncated = false;
59
201
  this.fileCorpus = [];
60
202
  this.lastFileListRef = null;
61
203
  this.selectedIndex = 0;
62
204
  this.selectedTextIndex = 0;
63
205
  this.isOpen = false;
206
+ this.previousActiveElement = null;
64
207
  this.mode = 'files';
65
208
  this.textResults = null;
66
209
  this.textResultItems = [];
@@ -119,11 +262,13 @@ export class QuickSwitcherController {
119
262
  open() {
120
263
  if (!this.overlay) return;
121
264
 
265
+ this.previousActiveElement = document.activeElement;
122
266
  this.isOpen = true;
123
267
  this.input.value = '';
124
268
  this.selectedIndex = 0;
125
269
  this.selectedTextIndex = 0;
126
270
  this.overlay.classList.add('visible');
271
+ this.overlay.setAttribute('aria-hidden', 'false');
127
272
  this.setMode('files', { preserveInput: true });
128
273
 
129
274
  // The overlay transitions visibility hidden→visible over 120ms.
@@ -171,15 +316,23 @@ export class QuickSwitcherController {
171
316
  }
172
317
  }
173
318
 
174
- close() {
319
+ close({ restoreFocus = true } = {}) {
175
320
  if (!this.overlay) return;
176
321
 
322
+ const previousActiveElement = this.previousActiveElement;
177
323
  this._cancelPendingFocus();
178
324
  this.abortTextSearch();
179
325
  this.isOpen = false;
180
326
  this.overlay.classList.remove('visible');
327
+ this.overlay.setAttribute('aria-hidden', 'true');
181
328
  this.input.value = '';
182
329
  this.resultsList.innerHTML = '';
330
+ this.setActiveDescendant('');
331
+ this.previousActiveElement = null;
332
+
333
+ if (restoreFocus) {
334
+ previousActiveElement?.focus?.();
335
+ }
183
336
  }
184
337
 
185
338
  toggle() {
@@ -206,10 +359,17 @@ export class QuickSwitcherController {
206
359
  tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
207
360
  });
208
361
 
362
+ this.scope?.classList.toggle('hidden', normalizedMode !== 'text');
363
+ this.scope?.setAttribute('aria-hidden', normalizedMode === 'text' ? 'false' : 'true');
364
+
209
365
  if (this.input) {
210
366
  this.input.placeholder = normalizedMode === 'text'
211
367
  ? 'Search text in files...'
212
368
  : 'Search files...';
369
+ this.input.setAttribute(
370
+ 'aria-label',
371
+ normalizedMode === 'text' ? 'Search text in files' : 'Search files',
372
+ );
213
373
  }
214
374
 
215
375
  this.handleInput();
@@ -233,23 +393,63 @@ export class QuickSwitcherController {
233
393
  this.fileCorpus = allFiles.map((filePath) => createCorpusEntry(filePath));
234
394
  }
235
395
 
396
+ this.fileMatches.clear();
397
+ const fileMetadata = this.getFileMetadata?.() ?? [];
398
+ const modifiedTimes = new Map(
399
+ (Array.isArray(fileMetadata) ? fileMetadata : []).map((entry) => [
400
+ entry?.path,
401
+ Number.isFinite(Number(entry?.mtimeMs)) ? Number(entry.mtimeMs) : 0,
402
+ ]),
403
+ );
404
+ const recentFiles = this.getRecentFiles?.() ?? [];
405
+ const recentRanks = new Map(
406
+ (Array.isArray(recentFiles) ? recentFiles : []).map((filePath, index) => [filePath, index]),
407
+ );
236
408
  if (!query) {
237
- this.filteredFiles = this.fileCorpus
409
+ this.filteredFiles = [...this.fileCorpus]
410
+ .sort((left, right) => {
411
+ const modifiedDelta = (modifiedTimes.get(right.filePath) ?? 0) - (modifiedTimes.get(left.filePath) ?? 0);
412
+ if (modifiedDelta !== 0) {
413
+ return modifiedDelta;
414
+ }
415
+
416
+ return (recentRanks.get(left.filePath) ?? NO_RECENT_FILE_RANK)
417
+ - (recentRanks.get(right.filePath) ?? NO_RECENT_FILE_RANK);
418
+ })
238
419
  .slice(0, MAX_VISIBLE_RESULTS)
239
420
  .map((entry) => entry.filePath);
421
+ this.fileMatchCount = this.fileCorpus.length;
240
422
  } else {
241
423
  const ranked = [];
424
+ let fileMatchCount = 0;
242
425
  this.fileCorpus.forEach((entry) => {
243
- const score = this.fuzzyScore(entry, query);
244
- if (score <= 0) {
426
+ const match = findCorpusMatch(entry, query);
427
+ if (!match) {
245
428
  return;
246
429
  }
247
430
 
248
- const rankedEntry = { filePath: entry.filePath, score };
431
+ fileMatchCount += 1;
432
+ this.fileMatches.set(entry.filePath, match);
433
+ const rankedEntry = {
434
+ filePath: entry.filePath,
435
+ recentRank: recentRanks.get(entry.filePath) ?? NO_RECENT_FILE_RANK,
436
+ score: match.score,
437
+ };
249
438
  let inserted = false;
250
439
  for (let index = 0; index < ranked.length; index += 1) {
251
440
  const current = ranked[index];
252
- if (score > current.score || (score === current.score && entry.lowerPath < String(current.filePath).toLowerCase())) {
441
+ const isBetter = rankedEntry.score > current.score
442
+ || (
443
+ rankedEntry.score === current.score
444
+ && (
445
+ rankedEntry.recentRank < current.recentRank
446
+ || (
447
+ rankedEntry.recentRank === current.recentRank
448
+ && entry.lowerPath < String(current.filePath).toLowerCase()
449
+ )
450
+ )
451
+ );
452
+ if (isBetter) {
253
453
  ranked.splice(index, 0, rankedEntry);
254
454
  inserted = true;
255
455
  break;
@@ -266,45 +466,20 @@ export class QuickSwitcherController {
266
466
  });
267
467
 
268
468
  this.filteredFiles = ranked.map((entry) => entry.filePath);
469
+ this.fileMatchCount = fileMatchCount;
269
470
  }
270
471
 
472
+ this.fileResultsTruncated = this.fileMatchCount > MAX_VISIBLE_RESULTS;
271
473
  this.selectedIndex = 0;
272
474
  this.renderResults(query);
273
475
  }
274
476
 
275
- fuzzyScore(entry, query) {
276
- const name = entry.lowerDisplayName;
277
- const nameOnly = entry.lowerFileName;
278
-
279
- if (nameOnly.includes(query)) return 100 + (1 / nameOnly.length);
280
- if (name.includes(query)) return 50 + (1 / name.length);
281
-
282
- let queryIndex = 0;
283
- let score = 0;
284
- let consecutiveBonus = 0;
285
-
286
- for (let i = 0; i < name.length && queryIndex < query.length; i++) {
287
- if (name[i] === query[queryIndex]) {
288
- queryIndex++;
289
- consecutiveBonus += 1;
290
- score += consecutiveBonus;
291
-
292
- if (i === 0 || name[i - 1] === '/' || name[i - 1] === '-' || name[i - 1] === '_' || name[i - 1] === ' ') {
293
- score += 5;
294
- }
295
- } else {
296
- consecutiveBonus = 0;
297
- }
298
- }
299
-
300
- return queryIndex === query.length ? score : 0;
301
- }
302
-
303
477
  renderResults(query) {
304
478
  if (!this.resultsList) return;
305
479
  this.resultsList.innerHTML = '';
306
480
 
307
481
  if (this.filteredFiles.length === 0) {
482
+ this.setActiveDescendant('');
308
483
  if (this.hint) {
309
484
  this.hint.textContent = query ? 'No files found' : 'No files in vault';
310
485
  this.hint.classList.remove('hidden');
@@ -313,7 +488,13 @@ export class QuickSwitcherController {
313
488
  }
314
489
 
315
490
  if (this.hint) {
316
- this.hint.classList.add('hidden');
491
+ const hint = this.fileResultsTruncated
492
+ ? query
493
+ ? `Showing the top ${MAX_VISIBLE_RESULTS} of ${this.fileMatchCount} matches. Refine the query to narrow results.`
494
+ : `Showing the first ${MAX_VISIBLE_RESULTS} of ${this.fileMatchCount} files.`
495
+ : '';
496
+ this.hint.textContent = hint;
497
+ this.hint.classList.toggle('hidden', !hint);
317
498
  }
318
499
 
319
500
  const fragment = document.createDocumentFragment();
@@ -325,15 +506,21 @@ export class QuickSwitcherController {
325
506
  if (index === this.selectedIndex) {
326
507
  item.classList.add('selected');
327
508
  }
509
+ item.id = `${FILE_RESULT_ID_PREFIX}${index}`;
510
+ item.setAttribute('role', 'option');
511
+ item.setAttribute('aria-selected', index === this.selectedIndex ? 'true' : 'false');
328
512
  item.dataset.index = String(index);
329
513
 
330
- const fileName = getFileName(filePath);
514
+ const fileName = getRawFileName(filePath);
331
515
  const dirPath = getDirPath(filePath);
516
+ const match = this.fileMatches.get(filePath);
517
+ const corpusEntry = this.fileCorpus.find((entry) => entry.filePath === filePath);
518
+ const matchIndices = corpusEntry ? splitMatchIndices(corpusEntry, match?.indices) : { dirPath: [], fileName: [] };
332
519
 
333
520
  item.innerHTML = `
334
- <svg class="qs-result-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
335
- <span class="qs-result-name">${this.highlightMatch(fileName, query)}</span>
336
- ${dirPath ? `<span class="qs-result-path">${escapeHtml(dirPath)}</span>` : ''}
521
+ ${getVaultFileIconSvg(filePath, { className: 'qs-result-icon' })}
522
+ <span class="qs-result-name">${highlightText(fileName, matchIndices.fileName)}</span>
523
+ ${dirPath ? `<span class="qs-result-path">${highlightText(dirPath, matchIndices.dirPath)}</span>` : ''}
337
524
  `;
338
525
 
339
526
  item.addEventListener('click', () => {
@@ -350,32 +537,7 @@ export class QuickSwitcherController {
350
537
  });
351
538
 
352
539
  this.resultsList.appendChild(fragment);
353
- }
354
-
355
- highlightMatch(text, query) {
356
- if (!query) return escapeHtml(text);
357
-
358
- const lower = text.toLowerCase();
359
- const idx = lower.indexOf(query);
360
-
361
- if (idx >= 0) {
362
- const before = text.slice(0, idx);
363
- const match = text.slice(idx, idx + query.length);
364
- const after = text.slice(idx + query.length);
365
- return `${escapeHtml(before)}<mark>${escapeHtml(match)}</mark>${escapeHtml(after)}`;
366
- }
367
-
368
- let result = '';
369
- let queryIdx = 0;
370
- for (let i = 0; i < text.length; i++) {
371
- if (queryIdx < query.length && text[i].toLowerCase() === query[queryIdx]) {
372
- result += `<mark>${escapeHtml(text[i])}</mark>`;
373
- queryIdx++;
374
- } else {
375
- result += escapeHtml(text[i]);
376
- }
377
- }
378
- return result;
540
+ this.updateSelection();
379
541
  }
380
542
 
381
543
  abortTextSearch({ invalidate = true } = {}) {
@@ -406,6 +568,8 @@ export class QuickSwitcherController {
406
568
  renderTextState(message) {
407
569
  if (!this.resultsList) return;
408
570
  this.resultsList.innerHTML = '';
571
+ this.resultsList.setAttribute('aria-busy', message === 'Searching...' ? 'true' : 'false');
572
+ this.setActiveDescendant('');
409
573
  if (this.hint) {
410
574
  this.hint.textContent = message;
411
575
  this.hint.classList.remove('hidden');
@@ -415,17 +579,20 @@ export class QuickSwitcherController {
415
579
  renderTextResults(query = '') {
416
580
  if (!this.resultsList) return;
417
581
  this.resultsList.innerHTML = '';
582
+ this.resultsList.setAttribute('aria-busy', 'false');
418
583
 
419
584
  if (!this.textResults?.files?.length || this.textResultItems.length === 0) {
585
+ this.setActiveDescendant('');
420
586
  this.renderTextState(query ? 'No text matches found' : 'Type to search file text');
421
587
  return;
422
588
  }
423
589
 
424
590
  if (this.hint) {
425
- this.hint.classList.toggle('hidden', !this.textResults.truncated);
426
- this.hint.textContent = this.textResults.truncated
427
- ? 'Showing the first matches. Refine the query to narrow results.'
591
+ const hint = this.textResults.truncated
592
+ ? `Showing partial results: ${this.textResults.files.length} files and ${formatMatchCount(this.textResults.matchCount, { truncated: true })}. Refine the query to narrow results.`
428
593
  : '';
594
+ this.hint.textContent = hint;
595
+ this.hint.classList.toggle('hidden', !hint);
429
596
  }
430
597
 
431
598
  const fragment = document.createDocumentFragment();
@@ -434,13 +601,15 @@ export class QuickSwitcherController {
434
601
  this.textResults.files.forEach((fileGroup) => {
435
602
  const group = document.createElement('section');
436
603
  group.className = 'qs-text-group';
604
+ group.setAttribute('role', 'group');
605
+ group.setAttribute('aria-label', fileGroup.file);
437
606
 
438
607
  const header = document.createElement('div');
439
608
  header.className = 'qs-text-group-header';
440
609
  header.innerHTML = `
441
610
  <span class="qs-text-file-name">${escapeHtml(getFileName(fileGroup.file))}</span>
442
611
  <span class="qs-text-file-meta">${escapeHtml(getDirPath(fileGroup.file))}</span>
443
- <span class="qs-text-count">${escapeHtml(formatMatchCount(fileGroup.matchCount))}</span>
612
+ <span class="qs-text-count">${escapeHtml(formatMatchCount(fileGroup.matchCount, { truncated: fileGroup.truncated }))}</span>
444
613
  `;
445
614
  group.appendChild(header);
446
615
 
@@ -452,6 +621,9 @@ export class QuickSwitcherController {
452
621
  if (itemIndex === this.selectedTextIndex) {
453
622
  item.classList.add('selected');
454
623
  }
624
+ item.id = `${TEXT_RESULT_ID_PREFIX}${itemIndex}`;
625
+ item.setAttribute('role', 'option');
626
+ item.setAttribute('aria-selected', itemIndex === this.selectedTextIndex ? 'true' : 'false');
455
627
  item.dataset.textIndex = String(itemIndex);
456
628
  item.innerHTML = `
457
629
  <span class="qs-text-line">L${escapeHtml(String(snippet.line ?? 1))}</span>
@@ -473,6 +645,7 @@ export class QuickSwitcherController {
473
645
  });
474
646
 
475
647
  this.resultsList.appendChild(fragment);
648
+ this.updateSelection();
476
649
  }
477
650
 
478
651
  highlightSnippet(snippet = {}) {
@@ -501,25 +674,35 @@ export class QuickSwitcherController {
501
674
  if (this.mode === 'text') {
502
675
  const items = this.resultsList.querySelectorAll('.qs-text-item');
503
676
  items.forEach((item, i) => {
504
- item.classList.toggle('selected', i === this.selectedTextIndex);
677
+ const isSelected = i === this.selectedTextIndex;
678
+ item.classList.toggle('selected', isSelected);
679
+ item.setAttribute('aria-selected', isSelected ? 'true' : 'false');
505
680
  });
506
681
  items[this.selectedTextIndex]?.scrollIntoView({ block: 'nearest' });
682
+ this.setActiveDescendant(items[this.selectedTextIndex]?.id ?? '');
507
683
  return;
508
684
  }
509
685
 
510
686
  const items = this.resultsList.querySelectorAll('.qs-result-item');
511
687
  items.forEach((item, i) => {
512
- item.classList.toggle('selected', i === this.selectedIndex);
688
+ const isSelected = i === this.selectedIndex;
689
+ item.classList.toggle('selected', isSelected);
690
+ item.setAttribute('aria-selected', isSelected ? 'true' : 'false');
513
691
  });
514
692
 
515
693
  items[this.selectedIndex]?.scrollIntoView({ block: 'nearest' });
694
+ this.setActiveDescendant(items[this.selectedIndex]?.id ?? '');
695
+ }
696
+
697
+ setActiveDescendant(id = '') {
698
+ this.input?.setAttribute('aria-activedescendant', id);
516
699
  }
517
700
 
518
701
  confirmSelection() {
519
702
  if (this.mode === 'text') {
520
703
  const match = this.textResultItems[this.selectedTextIndex];
521
704
  if (match) {
522
- this.close();
705
+ this.close({ restoreFocus: false });
523
706
  this.onTextMatchSelect?.(match);
524
707
  }
525
708
  return;
@@ -527,7 +710,7 @@ export class QuickSwitcherController {
527
710
 
528
711
  const filePath = this.filteredFiles[this.selectedIndex];
529
712
  if (filePath) {
530
- this.close();
713
+ this.close({ restoreFocus: false });
531
714
  this.onFileSelect?.(filePath);
532
715
  }
533
716
  }
@@ -1,9 +1,9 @@
1
- export const DEFAULT_SEARCH_DEBOUNCE_MS = 220;
1
+ export const DEFAULT_SEARCH_DEBOUNCE_MS = 300;
2
2
  const TEXT_RESULT_LIMIT = 50;
3
3
 
4
- export function formatMatchCount(count = 0) {
4
+ export function formatMatchCount(count = 0, { truncated = false } = {}) {
5
5
  const normalized = Number(count) || 0;
6
- return `${normalized} ${normalized === 1 ? 'match' : 'matches'}`;
6
+ return `${normalized}${truncated ? '+' : ''} ${normalized === 1 ? 'match' : 'matches'}`;
7
7
  }
8
8
 
9
9
  export function flattenTextResults(payload = {}) {
@@ -16,6 +16,9 @@ export class ToastController {
16
16
  : 0;
17
17
  const toast = document.createElement('div');
18
18
  toast.className = 'toast';
19
+ if (typeof normalizedOptions.tone === 'string' && normalizedOptions.tone) {
20
+ toast.dataset.tone = normalizedOptions.tone;
21
+ }
19
22
  const content = document.createElement('span');
20
23
  content.className = 'toast__message';
21
24
  content.textContent = message;