@bendyline/docblocks-react 1.1.2 → 2.0.0

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 (335) hide show
  1. package/README.md +31 -5
  2. package/dist/AppMenu/AppMenu.d.ts +29 -2
  3. package/dist/AppMenu/AppMenu.d.ts.map +1 -1
  4. package/dist/AppMenu/AppMenu.js +41 -2
  5. package/dist/AppMenu/AppMenu.js.map +1 -1
  6. package/dist/AppMenu/index.d.ts +1 -1
  7. package/dist/AppMenu/index.d.ts.map +1 -1
  8. package/dist/DocBlocksShell/DocBlocksShell.d.ts +15 -2
  9. package/dist/DocBlocksShell/DocBlocksShell.d.ts.map +1 -1
  10. package/dist/DocBlocksShell/DocBlocksShell.js +1677 -298
  11. package/dist/DocBlocksShell/DocBlocksShell.js.map +1 -1
  12. package/dist/DocBlocksShell/LazyEditorShell.d.ts +3 -0
  13. package/dist/DocBlocksShell/LazyEditorShell.d.ts.map +1 -0
  14. package/dist/DocBlocksShell/LazyEditorShell.js +3 -0
  15. package/dist/DocBlocksShell/LazyEditorShell.js.map +1 -0
  16. package/dist/DocBlocksShell/UpdateAvailableNotice.d.ts +13 -0
  17. package/dist/DocBlocksShell/UpdateAvailableNotice.d.ts.map +1 -0
  18. package/dist/DocBlocksShell/UpdateAvailableNotice.js +16 -0
  19. package/dist/DocBlocksShell/UpdateAvailableNotice.js.map +1 -0
  20. package/dist/DocBlocksShell/document-title.d.ts +3 -0
  21. package/dist/DocBlocksShell/document-title.d.ts.map +1 -0
  22. package/dist/DocBlocksShell/document-title.js +17 -0
  23. package/dist/DocBlocksShell/document-title.js.map +1 -0
  24. package/dist/DocBlocksShell/workspace-authority-barrier.d.ts +14 -0
  25. package/dist/DocBlocksShell/workspace-authority-barrier.d.ts.map +1 -0
  26. package/dist/DocBlocksShell/workspace-authority-barrier.js +23 -0
  27. package/dist/DocBlocksShell/workspace-authority-barrier.js.map +1 -0
  28. package/dist/Export/DeferredExportToolbarControls.d.ts +5 -0
  29. package/dist/Export/DeferredExportToolbarControls.d.ts.map +1 -0
  30. package/dist/Export/DeferredExportToolbarControls.js +10 -0
  31. package/dist/Export/DeferredExportToolbarControls.js.map +1 -0
  32. package/dist/Export/ExportDialog.d.ts +10 -1
  33. package/dist/Export/ExportDialog.d.ts.map +1 -1
  34. package/dist/Export/ExportDialog.js +20 -18
  35. package/dist/Export/ExportDialog.js.map +1 -1
  36. package/dist/Export/ExportToolbarControls.d.ts +21 -1
  37. package/dist/Export/ExportToolbarControls.d.ts.map +1 -1
  38. package/dist/Export/ExportToolbarControls.js +96 -11
  39. package/dist/Export/ExportToolbarControls.js.map +1 -1
  40. package/dist/Export/export-destination.d.ts +3 -0
  41. package/dist/Export/export-destination.d.ts.map +1 -0
  42. package/dist/Export/export-destination.js +32 -0
  43. package/dist/Export/export-destination.js.map +1 -0
  44. package/dist/Export/index.d.ts +8 -4
  45. package/dist/Export/index.d.ts.map +1 -1
  46. package/dist/Export/index.js +4 -1
  47. package/dist/Export/index.js.map +1 -1
  48. package/dist/Export/public-api.d.ts +12 -0
  49. package/dist/Export/public-api.d.ts.map +1 -0
  50. package/dist/Export/public-api.js +9 -0
  51. package/dist/Export/public-api.js.map +1 -0
  52. package/dist/Export/run-export.d.ts +3 -1
  53. package/dist/Export/run-export.d.ts.map +1 -1
  54. package/dist/Export/run-export.js +30 -13
  55. package/dist/Export/run-export.js.map +1 -1
  56. package/dist/ExportDialog-MEUISWZX.js +9 -0
  57. package/dist/ExportDialog-MEUISWZX.js.map +1 -0
  58. package/dist/ExportToolbarControls-D2YDWOXW.js +373 -0
  59. package/dist/ExportToolbarControls-D2YDWOXW.js.map +1 -0
  60. package/dist/FileExplorer/FileExplorer.d.ts +26 -4
  61. package/dist/FileExplorer/FileExplorer.d.ts.map +1 -1
  62. package/dist/FileExplorer/FileExplorer.js +204 -29
  63. package/dist/FileExplorer/FileExplorer.js.map +1 -1
  64. package/dist/FileExplorer/FileTreeNode.d.ts +24 -3
  65. package/dist/FileExplorer/FileTreeNode.d.ts.map +1 -1
  66. package/dist/FileExplorer/FileTreeNode.js +27 -8
  67. package/dist/FileExplorer/FileTreeNode.js.map +1 -1
  68. package/dist/FileExplorer/index.d.ts +1 -1
  69. package/dist/FileExplorer/index.d.ts.map +1 -1
  70. package/dist/FileExplorer/useFileTree.d.ts +5 -3
  71. package/dist/FileExplorer/useFileTree.d.ts.map +1 -1
  72. package/dist/FileExplorer/useFileTree.js +173 -13
  73. package/dist/FileExplorer/useFileTree.js.map +1 -1
  74. package/dist/Git/GitAuthGuidanceDialog.d.ts +18 -0
  75. package/dist/Git/GitAuthGuidanceDialog.d.ts.map +1 -0
  76. package/dist/Git/GitAuthGuidanceDialog.js +27 -0
  77. package/dist/Git/GitAuthGuidanceDialog.js.map +1 -0
  78. package/dist/Git/GitBranchesDialog.d.ts +13 -0
  79. package/dist/Git/GitBranchesDialog.d.ts.map +1 -0
  80. package/dist/Git/GitBranchesDialog.js +59 -0
  81. package/dist/Git/GitBranchesDialog.js.map +1 -0
  82. package/dist/Git/GitCloneDialog.d.ts +14 -0
  83. package/dist/Git/GitCloneDialog.d.ts.map +1 -0
  84. package/dist/Git/GitCloneDialog.js +79 -0
  85. package/dist/Git/GitCloneDialog.js.map +1 -0
  86. package/dist/Git/GitCommitDialog.d.ts +12 -0
  87. package/dist/Git/GitCommitDialog.d.ts.map +1 -0
  88. package/dist/Git/GitCommitDialog.js +38 -0
  89. package/dist/Git/GitCommitDialog.js.map +1 -0
  90. package/dist/Git/GitConflictsDialog.d.ts +12 -0
  91. package/dist/Git/GitConflictsDialog.d.ts.map +1 -0
  92. package/dist/Git/GitConflictsDialog.js +17 -0
  93. package/dist/Git/GitConflictsDialog.js.map +1 -0
  94. package/dist/Git/GitContext.d.ts +89 -0
  95. package/dist/Git/GitContext.d.ts.map +1 -0
  96. package/dist/Git/GitContext.js +15 -0
  97. package/dist/Git/GitContext.js.map +1 -0
  98. package/dist/Git/GitDiffDialog.d.ts +20 -0
  99. package/dist/Git/GitDiffDialog.d.ts.map +1 -0
  100. package/dist/Git/GitDiffDialog.js +100 -0
  101. package/dist/Git/GitDiffDialog.js.map +1 -0
  102. package/dist/Git/GitDiffView.d.ts +23 -0
  103. package/dist/Git/GitDiffView.d.ts.map +1 -0
  104. package/dist/Git/GitDiffView.js +77 -0
  105. package/dist/Git/GitDiffView.js.map +1 -0
  106. package/dist/Git/GitHistoryDialog.d.ts +15 -0
  107. package/dist/Git/GitHistoryDialog.d.ts.map +1 -0
  108. package/dist/Git/GitHistoryDialog.js +107 -0
  109. package/dist/Git/GitHistoryDialog.js.map +1 -0
  110. package/dist/Git/GitStatusBar.d.ts +8 -0
  111. package/dist/Git/GitStatusBar.d.ts.map +1 -0
  112. package/dist/Git/GitStatusBar.js +49 -0
  113. package/dist/Git/GitStatusBar.js.map +1 -0
  114. package/dist/Git/GitToolbarControl.d.ts +15 -0
  115. package/dist/Git/GitToolbarControl.d.ts.map +1 -0
  116. package/dist/Git/GitToolbarControl.js +51 -0
  117. package/dist/Git/GitToolbarControl.js.map +1 -0
  118. package/dist/Git/GitUI.d.ts +14 -0
  119. package/dist/Git/GitUI.d.ts.map +1 -0
  120. package/dist/Git/GitUI.js +21 -0
  121. package/dist/Git/GitUI.js.map +1 -0
  122. package/dist/Git/commit-selection.d.ts +22 -0
  123. package/dist/Git/commit-selection.d.ts.map +1 -0
  124. package/dist/Git/commit-selection.js +47 -0
  125. package/dist/Git/commit-selection.js.map +1 -0
  126. package/dist/Git/git-clone-utils.d.ts +8 -0
  127. package/dist/Git/git-clone-utils.d.ts.map +1 -0
  128. package/dist/Git/git-clone-utils.js +26 -0
  129. package/dist/Git/git-clone-utils.js.map +1 -0
  130. package/dist/Git/git-status.d.ts +24 -0
  131. package/dist/Git/git-status.d.ts.map +1 -0
  132. package/dist/Git/git-status.js +105 -0
  133. package/dist/Git/git-status.js.map +1 -0
  134. package/dist/Git/useGit.d.ts +15 -0
  135. package/dist/Git/useGit.d.ts.map +1 -0
  136. package/dist/Git/useGit.js +206 -0
  137. package/dist/Git/useGit.js.map +1 -0
  138. package/dist/Git/useGitStatus.d.ts +17 -0
  139. package/dist/Git/useGitStatus.d.ts.map +1 -0
  140. package/dist/Git/useGitStatus.js +63 -0
  141. package/dist/Git/useGitStatus.js.map +1 -0
  142. package/dist/GitToolbarControl-LMIR4R7I.js +117 -0
  143. package/dist/GitToolbarControl-LMIR4R7I.js.map +1 -0
  144. package/dist/GitUI-KGF5MSNV.js +1048 -0
  145. package/dist/GitUI-KGF5MSNV.js.map +1 -0
  146. package/dist/LazyEditorShell-EF2BWRXU.js +6 -0
  147. package/dist/LazyEditorShell-EF2BWRXU.js.map +1 -0
  148. package/dist/Settings/Settings.d.ts +23 -0
  149. package/dist/Settings/Settings.d.ts.map +1 -0
  150. package/dist/Settings/Settings.js +22 -0
  151. package/dist/Settings/Settings.js.map +1 -0
  152. package/dist/Settings/public-api.d.ts +3 -0
  153. package/dist/Settings/public-api.d.ts.map +1 -0
  154. package/dist/Settings/public-api.js +2 -0
  155. package/dist/Settings/public-api.js.map +1 -0
  156. package/dist/WorkspacePicker/WorkspacePicker.d.ts +6 -1
  157. package/dist/WorkspacePicker/WorkspacePicker.d.ts.map +1 -1
  158. package/dist/WorkspacePicker/WorkspacePicker.js +16 -5
  159. package/dist/WorkspacePicker/WorkspacePicker.js.map +1 -1
  160. package/dist/chunk-3SAXDU2U.js +157 -0
  161. package/dist/chunk-3SAXDU2U.js.map +1 -0
  162. package/dist/chunk-4XXSWN3T.js +13 -0
  163. package/dist/chunk-4XXSWN3T.js.map +1 -0
  164. package/dist/chunk-5A4KEWKT.js +97 -0
  165. package/dist/chunk-5A4KEWKT.js.map +1 -0
  166. package/dist/chunk-AOVTHZU7.js +321 -0
  167. package/dist/chunk-AOVTHZU7.js.map +1 -0
  168. package/dist/chunk-DD4VFJQI.js +51 -0
  169. package/dist/chunk-DD4VFJQI.js.map +1 -0
  170. package/dist/chunk-GSNZSCIK.js +55 -0
  171. package/dist/chunk-GSNZSCIK.js.map +1 -0
  172. package/dist/chunk-VHDP4IPL.js +70 -0
  173. package/dist/chunk-VHDP4IPL.js.map +1 -0
  174. package/dist/chunk-ZI56BNB7.js +270 -0
  175. package/dist/chunk-ZI56BNB7.js.map +1 -0
  176. package/dist/components/Dialog.d.ts +22 -0
  177. package/dist/components/Dialog.d.ts.map +1 -0
  178. package/dist/components/Dialog.js +33 -0
  179. package/dist/components/Dialog.js.map +1 -0
  180. package/dist/editor.d.ts +10 -0
  181. package/dist/editor.d.ts.map +1 -0
  182. package/dist/editor.js +70 -0
  183. package/dist/editor.js.map +1 -0
  184. package/dist/export/index.d.ts +87 -0
  185. package/dist/export/index.js +26 -0
  186. package/dist/export/index.js.map +1 -0
  187. package/dist/hooks/index.d.ts +2 -1
  188. package/dist/hooks/index.d.ts.map +1 -1
  189. package/dist/hooks/index.js +1 -1
  190. package/dist/hooks/index.js.map +1 -1
  191. package/dist/hooks/useDocumentSession.d.ts +16 -0
  192. package/dist/hooks/useDocumentSession.d.ts.map +1 -0
  193. package/dist/hooks/useDocumentSession.js +18 -0
  194. package/dist/hooks/useDocumentSession.js.map +1 -0
  195. package/dist/icons.d.ts +11 -6
  196. package/dist/icons.d.ts.map +1 -1
  197. package/dist/icons.js +21 -20
  198. package/dist/icons.js.map +1 -1
  199. package/dist/index.d.ts +8 -5
  200. package/dist/index.d.ts.map +1 -1
  201. package/dist/index.js +4 -2
  202. package/dist/index.js.map +1 -1
  203. package/dist/preferences/theme.d.ts +18 -0
  204. package/dist/preferences/theme.d.ts.map +1 -0
  205. package/dist/preferences/theme.js +63 -0
  206. package/dist/preferences/theme.js.map +1 -0
  207. package/dist/provider-lease.d.ts +10 -0
  208. package/dist/provider-lease.d.ts.map +1 -0
  209. package/dist/provider-lease.js +42 -0
  210. package/dist/provider-lease.js.map +1 -0
  211. package/dist/run-export-ICTZITXC.js +10 -0
  212. package/dist/run-export-ICTZITXC.js.map +1 -0
  213. package/dist/settings/index.d.ts +28 -0
  214. package/dist/settings/index.js +12 -0
  215. package/dist/settings/index.js.map +1 -0
  216. package/package.json +24 -9
  217. package/src/AppMenu/AppMenu.tsx +169 -99
  218. package/src/AppMenu/index.ts +1 -1
  219. package/src/DocBlocksShell/DocBlocksShell.tsx +2157 -537
  220. package/src/DocBlocksShell/LazyEditorShell.ts +2 -0
  221. package/src/DocBlocksShell/UpdateAvailableNotice.tsx +60 -0
  222. package/src/DocBlocksShell/document-title.ts +19 -0
  223. package/src/DocBlocksShell/workspace-authority-barrier.ts +26 -0
  224. package/src/Export/DeferredExportToolbarControls.tsx +23 -0
  225. package/src/Export/ExportDialog.tsx +74 -20
  226. package/src/Export/ExportToolbarControls.tsx +238 -40
  227. package/src/Export/export-destination.ts +30 -0
  228. package/src/Export/index.ts +18 -4
  229. package/src/Export/public-api.ts +17 -0
  230. package/src/Export/run-export.ts +36 -11
  231. package/src/FileExplorer/FileExplorer.tsx +284 -38
  232. package/src/FileExplorer/FileTreeNode.tsx +100 -8
  233. package/src/FileExplorer/index.ts +1 -1
  234. package/src/FileExplorer/useFileTree.ts +190 -16
  235. package/src/Git/GitAuthGuidanceDialog.tsx +84 -0
  236. package/src/Git/GitBranchesDialog.tsx +144 -0
  237. package/src/Git/GitCloneDialog.tsx +200 -0
  238. package/src/Git/GitCommitDialog.tsx +119 -0
  239. package/src/Git/GitConflictsDialog.tsx +62 -0
  240. package/src/Git/GitContext.tsx +88 -0
  241. package/src/Git/GitDiffDialog.tsx +147 -0
  242. package/src/Git/GitDiffView.tsx +105 -0
  243. package/src/Git/GitHistoryDialog.tsx +195 -0
  244. package/src/Git/GitStatusBar.tsx +157 -0
  245. package/src/Git/GitToolbarControl.tsx +123 -0
  246. package/src/Git/GitUI.tsx +68 -0
  247. package/src/Git/commit-selection.ts +63 -0
  248. package/src/Git/git-clone-utils.ts +23 -0
  249. package/src/Git/git-status.ts +113 -0
  250. package/src/Git/useGit.ts +279 -0
  251. package/src/Git/useGitStatus.ts +78 -0
  252. package/src/Settings/Settings.tsx +118 -0
  253. package/src/Settings/public-api.ts +8 -0
  254. package/src/WorkspacePicker/WorkspacePicker.tsx +62 -8
  255. package/src/components/Dialog.tsx +81 -0
  256. package/src/editor.ts +72 -0
  257. package/src/hooks/index.ts +2 -1
  258. package/src/hooks/useDocumentSession.ts +37 -0
  259. package/src/icons.tsx +33 -55
  260. package/src/index.ts +35 -5
  261. package/src/preferences/theme.ts +67 -0
  262. package/src/provider-lease.ts +52 -0
  263. package/src/styles/docblocks.css +2174 -79
  264. package/src/types/pwa-dom.d.ts +31 -0
  265. package/dist/hooks/useAutoSave.d.ts +0 -15
  266. package/dist/hooks/useAutoSave.d.ts.map +0 -1
  267. package/dist/hooks/useAutoSave.js +0 -83
  268. package/dist/hooks/useAutoSave.js.map +0 -1
  269. package/dist/monaco-slim.d.ts +0 -19
  270. package/dist/monaco-slim.d.ts.map +0 -1
  271. package/dist/monaco-slim.js +0 -19
  272. package/dist/monaco-slim.js.map +0 -1
  273. package/src/fonts/fonts.css +0 -362
  274. package/src/fonts/licenses/LICENSE-cormorant-garamond.txt +0 -93
  275. package/src/fonts/licenses/LICENSE-crimson-text.txt +0 -93
  276. package/src/fonts/licenses/LICENSE-dm-sans.txt +0 -93
  277. package/src/fonts/licenses/LICENSE-dm-serif-display.txt +0 -93
  278. package/src/fonts/licenses/LICENSE-hanken-grotesk.txt +0 -94
  279. package/src/fonts/licenses/LICENSE-ibm-plex-sans.txt +0 -93
  280. package/src/fonts/licenses/LICENSE-inter.txt +0 -93
  281. package/src/fonts/licenses/LICENSE-jetbrains-mono.txt +0 -93
  282. package/src/fonts/licenses/LICENSE-lora.txt +0 -93
  283. package/src/fonts/licenses/LICENSE-merriweather.txt +0 -93
  284. package/src/fonts/licenses/LICENSE-oswald.txt +0 -93
  285. package/src/fonts/licenses/LICENSE-playfair-display.txt +0 -93
  286. package/src/fonts/licenses/LICENSE-pt-serif.txt +0 -93
  287. package/src/fonts/licenses/LICENSE-roboto.txt +0 -93
  288. package/src/fonts/licenses/LICENSE-source-serif-4.txt +0 -93
  289. package/src/fonts/woff2/cormorant-garamond-400.woff2 +0 -0
  290. package/src/fonts/woff2/cormorant-garamond-700.woff2 +0 -0
  291. package/src/fonts/woff2/crimson-text-400.woff2 +0 -0
  292. package/src/fonts/woff2/crimson-text-400i.woff2 +0 -0
  293. package/src/fonts/woff2/crimson-text-700.woff2 +0 -0
  294. package/src/fonts/woff2/dm-sans-400.woff2 +0 -0
  295. package/src/fonts/woff2/dm-sans-400i.woff2 +0 -0
  296. package/src/fonts/woff2/dm-sans-500.woff2 +0 -0
  297. package/src/fonts/woff2/dm-sans-700.woff2 +0 -0
  298. package/src/fonts/woff2/dm-serif-display-400.woff2 +0 -0
  299. package/src/fonts/woff2/hanken-grotesk-400.woff2 +0 -0
  300. package/src/fonts/woff2/hanken-grotesk-400i.woff2 +0 -0
  301. package/src/fonts/woff2/hanken-grotesk-500.woff2 +0 -0
  302. package/src/fonts/woff2/hanken-grotesk-600.woff2 +0 -0
  303. package/src/fonts/woff2/hanken-grotesk-700.woff2 +0 -0
  304. package/src/fonts/woff2/ibm-plex-sans-400.woff2 +0 -0
  305. package/src/fonts/woff2/ibm-plex-sans-400i.woff2 +0 -0
  306. package/src/fonts/woff2/ibm-plex-sans-500.woff2 +0 -0
  307. package/src/fonts/woff2/ibm-plex-sans-600.woff2 +0 -0
  308. package/src/fonts/woff2/ibm-plex-sans-700.woff2 +0 -0
  309. package/src/fonts/woff2/inter-400.woff2 +0 -0
  310. package/src/fonts/woff2/inter-400i.woff2 +0 -0
  311. package/src/fonts/woff2/inter-500.woff2 +0 -0
  312. package/src/fonts/woff2/inter-600.woff2 +0 -0
  313. package/src/fonts/woff2/inter-700.woff2 +0 -0
  314. package/src/fonts/woff2/jetbrains-mono-400.woff2 +0 -0
  315. package/src/fonts/woff2/jetbrains-mono-400i.woff2 +0 -0
  316. package/src/fonts/woff2/jetbrains-mono-700.woff2 +0 -0
  317. package/src/fonts/woff2/lora-400.woff2 +0 -0
  318. package/src/fonts/woff2/lora-400i.woff2 +0 -0
  319. package/src/fonts/woff2/lora-700.woff2 +0 -0
  320. package/src/fonts/woff2/merriweather-400.woff2 +0 -0
  321. package/src/fonts/woff2/merriweather-700.woff2 +0 -0
  322. package/src/fonts/woff2/oswald-400.woff2 +0 -0
  323. package/src/fonts/woff2/oswald-700.woff2 +0 -0
  324. package/src/fonts/woff2/playfair-display-400.woff2 +0 -0
  325. package/src/fonts/woff2/playfair-display-700.woff2 +0 -0
  326. package/src/fonts/woff2/pt-serif-400.woff2 +0 -0
  327. package/src/fonts/woff2/pt-serif-700.woff2 +0 -0
  328. package/src/fonts/woff2/roboto-400.woff2 +0 -0
  329. package/src/fonts/woff2/roboto-400i.woff2 +0 -0
  330. package/src/fonts/woff2/roboto-700.woff2 +0 -0
  331. package/src/fonts/woff2/source-serif-4-400.woff2 +0 -0
  332. package/src/fonts/woff2/source-serif-4-400i.woff2 +0 -0
  333. package/src/fonts/woff2/source-serif-4-700.woff2 +0 -0
  334. package/src/hooks/useAutoSave.ts +0 -116
  335. package/src/monaco-slim.ts +0 -20
@@ -0,0 +1,2 @@
1
+ /** Deferred boundary for the large Squisq editor implementation. */
2
+ export { EditorShell as default } from '@bendyline/squisq-editor-react';
@@ -0,0 +1,60 @@
1
+ import { useState } from 'react';
2
+
3
+ const UPDATE_PROMPT_ID = 'db-update-available-prompt';
4
+
5
+ export interface UpdateAvailableNoticeProps {
6
+ available: boolean;
7
+ onApplyUpdate?: () => void;
8
+ blocked?: boolean;
9
+ statusBarVisible: boolean;
10
+ }
11
+
12
+ /**
13
+ * Keeps PWA updates unobtrusive until the user asks to review one. The
14
+ * notice is positioned over the editor status bar by DocBlocksShell CSS;
15
+ * the existing Reload/Later prompt remains a fixed shell-level card.
16
+ */
17
+ export function UpdateAvailableNotice({
18
+ available,
19
+ onApplyUpdate,
20
+ blocked = false,
21
+ statusBarVisible,
22
+ }: UpdateAvailableNoticeProps) {
23
+ const [promptOpen, setPromptOpen] = useState(false);
24
+
25
+ if (!available || !onApplyUpdate) return null;
26
+
27
+ const promptVisible = promptOpen && !blocked;
28
+
29
+ return (
30
+ <>
31
+ <button
32
+ type="button"
33
+ className={`db-update-status-notice${
34
+ statusBarVisible ? '' : ' db-update-status-notice--floating'
35
+ }`}
36
+ onClick={() => setPromptOpen(true)}
37
+ disabled={blocked}
38
+ aria-controls={UPDATE_PROMPT_ID}
39
+ aria-expanded={promptVisible}
40
+ aria-label="A new version of DocBlocks is available. Show update options."
41
+ title="A new version of DocBlocks is available"
42
+ >
43
+ Update available
44
+ </button>
45
+ {promptVisible && (
46
+ <div id={UPDATE_PROMPT_ID} className="db-update-banner" role="alert">
47
+ <span>A new version of DocBlocks is available.</span>
48
+ <div className="db-update-banner-actions">
49
+ <button type="button" onClick={onApplyUpdate}>
50
+ Reload
51
+ </button>
52
+ <button type="button" onClick={() => setPromptOpen(false)}>
53
+ Later
54
+ </button>
55
+ </div>
56
+ </div>
57
+ )}
58
+ </>
59
+ );
60
+ }
@@ -0,0 +1,19 @@
1
+ import { useEffect } from 'react';
2
+
3
+ const APP_TITLE = 'DocBlocks';
4
+
5
+ export function titleForSelectedFile(selectedFile: string | null): string {
6
+ if (!selectedFile) return APP_TITLE;
7
+
8
+ const filename = selectedFile.replace(/^\/+/, '').split('/').pop();
9
+ const name = filename?.replace(/\.[^.]+$/, '');
10
+ return name ? `${name} - ${APP_TITLE}` : APP_TITLE;
11
+ }
12
+
13
+ export function useDocumentTitle(selectedFile: string | null): void {
14
+ useEffect(() => {
15
+ if (typeof document !== 'undefined') {
16
+ document.title = titleForSelectedFile(selectedFile);
17
+ }
18
+ }, [selectedFile]);
19
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Renderer-side barrier for main-authoritative Electron workspace identity.
3
+ * OS requests may arrive before IndexedDB descriptors have been reconciled;
4
+ * they must retain navigation priority while waiting for that reconciliation.
5
+ */
6
+ export class WorkspaceAuthorityBarrier {
7
+ private ready = false;
8
+ private readonly readyPromise: Promise<void>;
9
+ private resolveReady!: () => void;
10
+
11
+ public constructor() {
12
+ this.readyPromise = new Promise<void>((resolve) => {
13
+ this.resolveReady = resolve;
14
+ });
15
+ }
16
+
17
+ public markReady(): void {
18
+ if (this.ready) return;
19
+ this.ready = true;
20
+ this.resolveReady();
21
+ }
22
+
23
+ public wait(): Promise<void> {
24
+ return this.readyPromise;
25
+ }
26
+ }
@@ -0,0 +1,23 @@
1
+ import { lazy, Suspense } from 'react';
2
+ import type {
3
+ ExportDestinationAdapter,
4
+ ExportDestinationTarget,
5
+ ExportToolbarControlsProps,
6
+ } from './ExportToolbarControls.js';
7
+
8
+ const ExportToolbarControlsImplementation = lazy(() =>
9
+ import('./ExportToolbarControls.js').then((module) => ({
10
+ default: module.ExportToolbarControls,
11
+ })),
12
+ );
13
+
14
+ /** Public toolbar control with its editor-coupled implementation deferred. */
15
+ export function ExportToolbarControls(props: ExportToolbarControlsProps) {
16
+ return (
17
+ <Suspense fallback={null}>
18
+ <ExportToolbarControlsImplementation {...props} />
19
+ </Suspense>
20
+ );
21
+ }
22
+
23
+ export type { ExportDestinationAdapter, ExportDestinationTarget, ExportToolbarControlsProps };
@@ -2,7 +2,7 @@
2
2
  * ExportDialog — modal for choosing export format and options.
3
3
  */
4
4
 
5
- import { useState, useCallback, useEffect, useRef } from 'react';
5
+ import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
6
6
  import { getThemeSummaries } from '@bendyline/squisq/schemas';
7
7
  import type { ExportFormat, ExportOptions, HtmlBundle, HtmlStyle } from './export-options.js';
8
8
  import { FORMAT_LABELS, saveExportOptions } from './export-options.js';
@@ -13,12 +13,22 @@ export interface ExportDialogProps {
13
13
  initial: ExportOptions;
14
14
  /** Whether the export is currently running. */
15
15
  exporting: boolean;
16
+ /** Optional host-provided destination path control. */
17
+ destination?: ExportDestinationControl;
16
18
  /** Called when the user confirms the export. */
17
19
  onExport: (options: ExportOptions) => void;
20
+ /** Called whenever the currently selected options change. */
21
+ onOptionsChange?: (options: ExportOptions) => void;
18
22
  /** Called when the dialog is dismissed. */
19
23
  onClose: () => void;
20
24
  }
21
25
 
26
+ export interface ExportDestinationControl {
27
+ value: string;
28
+ onPick: (options: ExportOptions) => void | Promise<void>;
29
+ hint?: string;
30
+ }
31
+
22
32
  const FORMATS: ExportFormat[] = ['pdf', 'docx', 'pptx', 'html', 'md'];
23
33
 
24
34
  /** Short labels for the Format chip row. The fuller `FORMAT_LABELS` list
@@ -96,7 +106,14 @@ function ChipRadioGroup<T extends string>({
96
106
  );
97
107
  }
98
108
 
99
- export function ExportDialog({ initial, exporting, onExport, onClose }: ExportDialogProps) {
109
+ export function ExportDialog({
110
+ initial,
111
+ exporting,
112
+ destination,
113
+ onExport,
114
+ onOptionsChange,
115
+ onClose,
116
+ }: ExportDialogProps) {
100
117
  const [format, setFormat] = useState<ExportFormat>(initial.format);
101
118
  const [themeId, setThemeId] = useState(initial.themeId);
102
119
  const [transformStyle, setTransformStyle] = useState(initial.transformStyle);
@@ -156,8 +173,8 @@ export function ExportDialog({ initial, exporting, onExport, onClose }: ExportDi
156
173
  };
157
174
  }, [showTransform]);
158
175
 
159
- const handleExport = useCallback(() => {
160
- const opts: ExportOptions = {
176
+ const currentOptions = useMemo<ExportOptions>(
177
+ () => ({
161
178
  format,
162
179
  themeId,
163
180
  transformStyle,
@@ -166,22 +183,29 @@ export function ExportDialog({ initial, exporting, onExport, onClose }: ExportDi
166
183
  htmlBundle,
167
184
  includeLinkedDocs: showIncludeLinked && includeLinkedDocs,
168
185
  entryAsIndex: showEntryAsIndex && entryAsIndex,
169
- };
170
- saveExportOptions(opts);
171
- onExport(opts);
172
- }, [
173
- format,
174
- themeId,
175
- transformStyle,
176
- pageSize,
177
- htmlStyle,
178
- htmlBundle,
179
- includeLinkedDocs,
180
- entryAsIndex,
181
- showIncludeLinked,
182
- showEntryAsIndex,
183
- onExport,
184
- ]);
186
+ }),
187
+ [
188
+ format,
189
+ themeId,
190
+ transformStyle,
191
+ pageSize,
192
+ htmlStyle,
193
+ htmlBundle,
194
+ includeLinkedDocs,
195
+ entryAsIndex,
196
+ showIncludeLinked,
197
+ showEntryAsIndex,
198
+ ],
199
+ );
200
+
201
+ useEffect(() => {
202
+ onOptionsChange?.(currentOptions);
203
+ }, [currentOptions, onOptionsChange]);
204
+
205
+ const handleExport = useCallback(() => {
206
+ saveExportOptions(currentOptions);
207
+ onExport(currentOptions);
208
+ }, [currentOptions, onExport]);
185
209
 
186
210
  // Close on Escape
187
211
  useEffect(() => {
@@ -226,6 +250,36 @@ export function ExportDialog({ initial, exporting, onExport, onClose }: ExportDi
226
250
  />
227
251
  </div>
228
252
 
253
+ {destination && (
254
+ <div className="db-export-field">
255
+ <label className="db-export-label" htmlFor="db-export-path">
256
+ Export to
257
+ </label>
258
+ <div className="db-export-path-row">
259
+ <input
260
+ id="db-export-path"
261
+ className="db-export-path-input"
262
+ value={destination.value}
263
+ readOnly
264
+ disabled={exporting}
265
+ spellCheck={false}
266
+ autoComplete="off"
267
+ />
268
+ <button
269
+ type="button"
270
+ className="db-export-path-pick"
271
+ onClick={() => void destination.onPick(currentOptions)}
272
+ disabled={exporting}
273
+ aria-label="Choose export location"
274
+ title="Choose export location"
275
+ >
276
+ ...
277
+ </button>
278
+ </div>
279
+ {destination.hint && <span className="db-export-hint">{destination.hint}</span>}
280
+ </div>
281
+ )}
282
+
229
283
  {/* HTML style + bundle (HTML only) — also chip rows */}
230
284
  {showHtmlOptions && (
231
285
  <>
@@ -7,7 +7,16 @@
7
7
  * Must be rendered inside <EditorProvider> so useEditorContext() works.
8
8
  */
9
9
 
10
- import { useState, useCallback, useEffect, useRef, useMemo, type ComponentType } from 'react';
10
+ import {
11
+ lazy,
12
+ Suspense,
13
+ useState,
14
+ useCallback,
15
+ useEffect,
16
+ useRef,
17
+ useMemo,
18
+ type ComponentType,
19
+ } from 'react';
11
20
  import { useEditorContext } from '@bendyline/squisq-editor-react';
12
21
  import { getThemeSummaries } from '@bendyline/squisq/schemas';
13
22
  import { parseMarkdown } from '@bendyline/squisq/markdown';
@@ -20,15 +29,53 @@ import {
20
29
  loadLastExportOptions,
21
30
  saveExportOptions,
22
31
  } from './export-options.js';
23
- import { ExportDialog } from './ExportDialog.js';
24
- import { runExport } from './run-export.js';
32
+ import type { ExportBlobSaver } from './run-export.js';
25
33
  import { loadTransformStyleSummaries, type ExportSummaryOption } from './transform-summaries.js';
26
34
 
35
+ const ExportDialog = lazy(() =>
36
+ import('./ExportDialog.js').then((module) => ({ default: module.ExportDialog })),
37
+ );
38
+
39
+ let runExportModulePromise: Promise<typeof import('./run-export.js')> | null = null;
40
+
41
+ function loadRunExportModule(): Promise<typeof import('./run-export.js')> {
42
+ runExportModulePromise ??= import('./run-export.js');
43
+ return runExportModulePromise;
44
+ }
45
+
27
46
  export interface ExportToolbarControlsProps {
28
47
  /** Currently selected file path — used to derive the download filename. */
29
48
  selectedFile: string | null;
30
49
  /** Media container for resolving images during export. */
31
50
  mediaContainer?: ContentContainer | null;
51
+ /** Override the default browser download behavior for host-provided save flows. */
52
+ saveBlob?: ExportBlobSaver;
53
+ /** Optional host adapter for displaying, picking, and saving to a native target path. */
54
+ destinationAdapter?: ExportDestinationAdapter;
55
+ /** Render the trigger as a direct Export button instead of the overflow menu. */
56
+ trigger?: 'menu' | 'button';
57
+ /** Whether to show video export in the overflow menu. */
58
+ showVideoExport?: boolean;
59
+ }
60
+
61
+ /** Host-specific operations behind the shared export destination control. */
62
+ export interface ExportDestinationTarget {
63
+ grantId: string | null;
64
+ displayPath: string;
65
+ }
66
+
67
+ export interface ExportDestinationAdapter {
68
+ resolveTarget: (filename: string) => Promise<ExportDestinationTarget>;
69
+ pickTarget: (
70
+ filename: string,
71
+ currentTarget?: ExportDestinationTarget | null,
72
+ ) => Promise<ExportDestinationTarget | null>;
73
+ saveBlob: (
74
+ blob: Blob,
75
+ filename: string,
76
+ target?: ExportDestinationTarget | null,
77
+ ) => Promise<ExportDestinationTarget | null>;
78
+ hint?: string;
32
79
  }
33
80
 
34
81
  type ParsedMarkdown = ReturnType<typeof parseMarkdown>;
@@ -92,6 +139,10 @@ function quickLabel(opts: ExportOptions, transformSummaries: ExportSummaryOption
92
139
  export function ExportToolbarControls({
93
140
  selectedFile,
94
141
  mediaContainer,
142
+ saveBlob,
143
+ destinationAdapter,
144
+ trigger = 'menu',
145
+ showVideoExport = true,
95
146
  }: ExportToolbarControlsProps) {
96
147
  const { markdownSource, markdownDoc } = useEditorContext();
97
148
  const [menuOpen, setMenuOpen] = useState(false);
@@ -103,6 +154,8 @@ export function ExportToolbarControls({
103
154
  const [videoDoc, setVideoDoc] = useState<VideoExportModalProps['doc'] | null>(null);
104
155
  const [transformSummaries, setTransformSummaries] = useState<ExportSummaryOption[]>([]);
105
156
  const [exporting, setExporting] = useState(false);
157
+ const [destinationTarget, setDestinationTarget] = useState<ExportDestinationTarget | null>(null);
158
+ const destinationRequestRef = useRef(0);
106
159
  const menuRef = useRef<HTMLDivElement>(null);
107
160
 
108
161
  const lastOptions = loadLastExportOptions();
@@ -174,10 +227,30 @@ export function ExportToolbarControls({
174
227
  setMenuOpen((prev) => !prev);
175
228
  }, []);
176
229
 
230
+ const refreshDestination = useCallback(
231
+ async (options: ExportOptions) => {
232
+ if (!destinationAdapter) return;
233
+
234
+ const requestId = destinationRequestRef.current + 1;
235
+ destinationRequestRef.current = requestId;
236
+
237
+ try {
238
+ const { buildExportFilename } = await loadRunExportModule();
239
+ const filename = buildExportFilename(selectedFile, options);
240
+ const target = await destinationAdapter.resolveTarget(filename);
241
+ if (destinationRequestRef.current === requestId) setDestinationTarget(target);
242
+ } catch {
243
+ if (destinationRequestRef.current === requestId) setDestinationTarget(null);
244
+ }
245
+ },
246
+ [destinationAdapter, selectedFile],
247
+ );
248
+
177
249
  const handleOpenDialog = useCallback(() => {
178
250
  setMenuOpen(false);
179
251
  setDialogOpen(true);
180
- }, []);
252
+ void refreshDestination(dialogInitial);
253
+ }, [dialogInitial, refreshDestination]);
181
254
 
182
255
  const handleCloseDialog = useCallback(() => {
183
256
  setDialogOpen(false);
@@ -206,17 +279,70 @@ export function ExportToolbarControls({
206
279
  setVideoLoadError(null);
207
280
  }, []);
208
281
 
282
+ const handleOptionsChange = useCallback(
283
+ (options: ExportOptions) => {
284
+ void refreshDestination(options);
285
+ },
286
+ [refreshDestination],
287
+ );
288
+
289
+ const handlePickDestination = useCallback(
290
+ async (options: ExportOptions) => {
291
+ if (!destinationAdapter) return;
292
+ try {
293
+ const { buildExportFilename } = await loadRunExportModule();
294
+ const filename = buildExportFilename(selectedFile, options);
295
+ const pickedTarget = await destinationAdapter.pickTarget(filename, destinationTarget);
296
+ if (pickedTarget === null) return;
297
+ setDestinationTarget(pickedTarget);
298
+ } catch {
299
+ // Native hosts surface picker failures through their own UI channel.
300
+ }
301
+ },
302
+ [destinationAdapter, destinationTarget, selectedFile],
303
+ );
304
+
305
+ const saveToDestination = useCallback(
306
+ async (blob: Blob, filename: string, target: ExportDestinationTarget | null) => {
307
+ if (!destinationAdapter) return;
308
+ const savedTarget = await destinationAdapter.saveBlob(blob, filename, target);
309
+ if (savedTarget) setDestinationTarget(savedTarget);
310
+ },
311
+ [destinationAdapter],
312
+ );
313
+
314
+ const handleDestinationSaveBlob = useCallback(
315
+ async (blob: Blob, filename: string) => {
316
+ await saveToDestination(blob, filename, destinationTarget);
317
+ },
318
+ [destinationTarget, saveToDestination],
319
+ );
320
+
209
321
  const handleExport = useCallback(
210
322
  async (opts: ExportOptions) => {
211
323
  setExporting(true);
212
324
  try {
213
- await runExport(markdownSource, selectedFile, opts, mediaContainer);
325
+ const { runExport } = await loadRunExportModule();
326
+ await runExport(
327
+ markdownSource,
328
+ selectedFile,
329
+ opts,
330
+ mediaContainer,
331
+ destinationAdapter ? handleDestinationSaveBlob : saveBlob,
332
+ );
214
333
  } finally {
215
334
  setExporting(false);
216
335
  setDialogOpen(false);
217
336
  }
218
337
  },
219
- [markdownSource, selectedFile, mediaContainer],
338
+ [
339
+ markdownSource,
340
+ selectedFile,
341
+ mediaContainer,
342
+ destinationAdapter,
343
+ handleDestinationSaveBlob,
344
+ saveBlob,
345
+ ],
220
346
  );
221
347
 
222
348
  const handleQuickExport = useCallback(async () => {
@@ -225,55 +351,108 @@ export function ExportToolbarControls({
225
351
  setExporting(true);
226
352
  try {
227
353
  saveExportOptions(lastOptions);
228
- await runExport(markdownSource, selectedFile, lastOptions, mediaContainer);
354
+ const { runExport } = await loadRunExportModule();
355
+ let quickTarget: ExportDestinationTarget | null = null;
356
+ if (destinationAdapter) {
357
+ const { buildExportFilename } = await loadRunExportModule();
358
+ quickTarget = await destinationAdapter.resolveTarget(
359
+ buildExportFilename(selectedFile, lastOptions),
360
+ );
361
+ }
362
+ await runExport(
363
+ markdownSource,
364
+ selectedFile,
365
+ lastOptions,
366
+ mediaContainer,
367
+ destinationAdapter
368
+ ? async (blob, filename) => saveToDestination(blob, filename, quickTarget)
369
+ : saveBlob,
370
+ );
229
371
  } finally {
230
372
  setExporting(false);
231
373
  }
232
- }, [lastOptions, markdownSource, selectedFile, mediaContainer]);
374
+ }, [
375
+ destinationAdapter,
376
+ lastOptions,
377
+ markdownSource,
378
+ mediaContainer,
379
+ saveBlob,
380
+ saveToDestination,
381
+ selectedFile,
382
+ ]);
233
383
 
234
384
  const LoadedVideoExportModal = videoModules?.Modal;
235
385
 
236
386
  return (
237
387
  <>
238
- <div className="db-toolbar-menu" ref={menuRef}>
388
+ {trigger === 'button' ? (
239
389
  <button
240
- className="db-toolbar-menu-trigger"
241
- onClick={handleToggleMenu}
242
- aria-label="More actions"
243
- title="More actions"
390
+ type="button"
391
+ className="squisq-toolbar-button db-toolbar-export-trigger"
392
+ onClick={handleOpenDialog}
393
+ disabled={exporting}
394
+ aria-label="Export document"
395
+ data-tooltip={exporting ? 'Exporting...' : 'Export document'}
244
396
  >
245
- &middot;&middot;&middot;
397
+ <ExportGlyph />
246
398
  </button>
399
+ ) : (
400
+ <div className="db-toolbar-menu" ref={menuRef}>
401
+ <button
402
+ className="db-toolbar-menu-trigger"
403
+ onClick={handleToggleMenu}
404
+ aria-label="More actions"
405
+ title="More actions"
406
+ >
407
+ &middot;&middot;&middot;
408
+ </button>
247
409
 
248
- {menuOpen && (
249
- <div className="db-toolbar-menu-dropdown">
250
- {lastOptions && (
251
- <button
252
- className="db-toolbar-menu-item"
253
- onClick={handleQuickExport}
254
- disabled={exporting}
255
- >
256
- {quickLabel(lastOptions, transformSummaries)}
410
+ {menuOpen && (
411
+ <div className="db-toolbar-menu-dropdown">
412
+ {lastOptions && (
413
+ <button
414
+ className="db-toolbar-menu-item"
415
+ onClick={handleQuickExport}
416
+ disabled={exporting}
417
+ >
418
+ {quickLabel(lastOptions, transformSummaries)}
419
+ </button>
420
+ )}
421
+ <button className="db-toolbar-menu-item" onClick={handleOpenDialog}>
422
+ Export...
257
423
  </button>
258
- )}
259
- <button className="db-toolbar-menu-item" onClick={handleOpenDialog}>
260
- Export...
261
- </button>
262
- <div className="db-toolbar-menu-divider" />
263
- <button className="db-toolbar-menu-item" onClick={handleOpenVideoModal}>
264
- Export Video...
265
- </button>
266
- </div>
267
- )}
268
- </div>
424
+ {showVideoExport && (
425
+ <>
426
+ <div className="db-toolbar-menu-divider" />
427
+ <button className="db-toolbar-menu-item" onClick={handleOpenVideoModal}>
428
+ Export Video...
429
+ </button>
430
+ </>
431
+ )}
432
+ </div>
433
+ )}
434
+ </div>
435
+ )}
269
436
 
270
437
  {dialogOpen && (
271
- <ExportDialog
272
- initial={dialogInitial}
273
- exporting={exporting}
274
- onExport={handleExport}
275
- onClose={handleCloseDialog}
276
- />
438
+ <Suspense fallback={null}>
439
+ <ExportDialog
440
+ initial={dialogInitial}
441
+ exporting={exporting}
442
+ destination={
443
+ destinationAdapter
444
+ ? {
445
+ value: destinationTarget?.displayPath ?? '',
446
+ onPick: handlePickDestination,
447
+ hint: destinationAdapter.hint,
448
+ }
449
+ : undefined
450
+ }
451
+ onExport={handleExport}
452
+ onOptionsChange={destinationAdapter ? handleOptionsChange : undefined}
453
+ onClose={handleCloseDialog}
454
+ />
455
+ </Suspense>
277
456
  )}
278
457
 
279
458
  {videoModalOpen && (videoLoading || videoLoadError) && (
@@ -311,3 +490,22 @@ export function ExportToolbarControls({
311
490
  </>
312
491
  );
313
492
  }
493
+
494
+ function ExportGlyph() {
495
+ return (
496
+ <svg
497
+ viewBox="0 0 16 16"
498
+ fill="none"
499
+ stroke="currentColor"
500
+ strokeWidth="1.5"
501
+ strokeLinecap="round"
502
+ strokeLinejoin="round"
503
+ aria-hidden="true"
504
+ >
505
+ <path d="M4.5 1.75h4.25L12 5v7.25a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 3 12.25v-9a1.5 1.5 0 0 1 1.5-1.5z" />
506
+ <path d="M8.75 1.75V5h3.25" />
507
+ <path d="M6 8.75h4.5" />
508
+ <path d="M8.75 7 10.5 8.75 8.75 10.5" />
509
+ </svg>
510
+ );
511
+ }
@@ -0,0 +1,30 @@
1
+ /** Keep a user-edited target name while switching the selected export format. */
2
+ export function updateExportTargetExtension(targetPath: string, suggestedFilename: string): string {
3
+ const nextExtension = extensionOf(suggestedFilename);
4
+ if (!nextExtension) return targetPath;
5
+ if (!targetPath.trim()) return suggestedFilename;
6
+
7
+ const suffixIndex = firstSuffixIndex(targetPath);
8
+ const pathPart = suffixIndex === -1 ? targetPath : targetPath.slice(0, suffixIndex);
9
+ const suffix = suffixIndex === -1 ? '' : targetPath.slice(suffixIndex);
10
+ const separatorIndex = Math.max(pathPart.lastIndexOf('/'), pathPart.lastIndexOf('\\'));
11
+ const dotIndex = pathPart.lastIndexOf('.');
12
+ const stem = dotIndex > separatorIndex ? pathPart.slice(0, dotIndex) : pathPart;
13
+ return `${stem}${nextExtension}${suffix}`;
14
+ }
15
+
16
+ function extensionOf(value: string): string {
17
+ const suffixIndex = firstSuffixIndex(value);
18
+ const pathPart = suffixIndex === -1 ? value : value.slice(0, suffixIndex);
19
+ const separatorIndex = Math.max(pathPart.lastIndexOf('/'), pathPart.lastIndexOf('\\'));
20
+ const dotIndex = pathPart.lastIndexOf('.');
21
+ return dotIndex > separatorIndex ? pathPart.slice(dotIndex) : '';
22
+ }
23
+
24
+ function firstSuffixIndex(value: string): number {
25
+ const query = value.indexOf('?');
26
+ const fragment = value.indexOf('#');
27
+ if (query === -1) return fragment;
28
+ if (fragment === -1) return query;
29
+ return Math.min(query, fragment);
30
+ }