@rightkit/document-workspace 0.0.0-stage → 0.1.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 (262) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE-APACHE +201 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +51 -2
  5. package/dist/app/App.d.ts +1 -0
  6. package/dist/app/App.js +6 -0
  7. package/dist/app/AppDialogs.d.ts +52 -0
  8. package/dist/app/AppDialogs.js +23 -0
  9. package/dist/app/AppDialogsHost.d.ts +31 -0
  10. package/dist/app/AppDialogsHost.js +30 -0
  11. package/dist/app/AppLifecycle.d.ts +19 -0
  12. package/dist/app/AppLifecycle.js +87 -0
  13. package/dist/app/AppProvider.d.ts +317 -0
  14. package/dist/app/AppProvider.js +217 -0
  15. package/dist/app/AppRoot.d.ts +8 -0
  16. package/dist/app/AppRoot.js +159 -0
  17. package/dist/app/AppShell.d.ts +2 -0
  18. package/dist/app/AppShell.js +81 -0
  19. package/dist/app/CommandPalette.d.ts +13 -0
  20. package/dist/app/CommandPalette.js +47 -0
  21. package/dist/app/DocActionBar.d.ts +36 -0
  22. package/dist/app/DocActionBar.js +300 -0
  23. package/dist/app/DocumentContextMenu.d.ts +29 -0
  24. package/dist/app/DocumentContextMenu.js +122 -0
  25. package/dist/app/DocumentPane.d.ts +52 -0
  26. package/dist/app/DocumentPane.js +30 -0
  27. package/dist/app/DocumentRouterTable.d.ts +58 -0
  28. package/dist/app/DocumentRouterTable.js +143 -0
  29. package/dist/app/ErrorBoundary.d.ts +12 -0
  30. package/dist/app/ErrorBoundary.js +16 -0
  31. package/dist/app/FindBar.d.ts +12 -0
  32. package/dist/app/FindBar.js +199 -0
  33. package/dist/app/LiveTitle.d.ts +14 -0
  34. package/dist/app/LiveTitle.js +14 -0
  35. package/dist/app/OpeningProgress.d.ts +13 -0
  36. package/dist/app/OpeningProgress.js +21 -0
  37. package/dist/app/PrintPreviewWindow.d.ts +2 -0
  38. package/dist/app/PrintPreviewWindow.js +57 -0
  39. package/dist/app/ShortcutHelp.d.ts +3 -0
  40. package/dist/app/ShortcutHelp.js +99 -0
  41. package/dist/app/Sidebar.d.ts +36 -0
  42. package/dist/app/Sidebar.js +121 -0
  43. package/dist/app/ToastViewport.d.ts +1 -0
  44. package/dist/app/ToastViewport.js +31 -0
  45. package/dist/app/Toolbar.d.ts +26 -0
  46. package/dist/app/Toolbar.js +63 -0
  47. package/dist/app/activeFormatSavers.d.ts +7 -0
  48. package/dist/app/activeFormatSavers.js +4 -0
  49. package/dist/app/clipboardPaths.d.ts +1 -0
  50. package/dist/app/clipboardPaths.js +31 -0
  51. package/dist/app/documentDrafts.d.ts +17 -0
  52. package/dist/app/documentDrafts.js +104 -0
  53. package/dist/app/draftResolution.d.ts +5 -0
  54. package/dist/app/draftResolution.js +25 -0
  55. package/dist/app/markdownPaletteActions.d.ts +8 -0
  56. package/dist/app/markdownPaletteActions.js +107 -0
  57. package/dist/app/resourceCensusDev.d.ts +41 -0
  58. package/dist/app/resourceCensusDev.js +518 -0
  59. package/dist/app/setupRecovery.d.ts +34 -0
  60. package/dist/app/setupRecovery.js +70 -0
  61. package/dist/app/shell/DocStatsPill.d.ts +18 -0
  62. package/dist/app/shell/DocStatsPill.js +32 -0
  63. package/dist/app/shell/DocumentTools.d.ts +9 -0
  64. package/dist/app/shell/DocumentTools.js +73 -0
  65. package/dist/app/shell/InfoInspector.d.ts +9 -0
  66. package/dist/app/shell/InfoInspector.js +67 -0
  67. package/dist/app/shell/StartPage.d.ts +9 -0
  68. package/dist/app/shell/StartPage.js +49 -0
  69. package/dist/app/shell/readEdit.d.ts +6 -0
  70. package/dist/app/shell/readEdit.js +6 -0
  71. package/dist/app/sidebar/SidebarDocumentMenu.d.ts +20 -0
  72. package/dist/app/sidebar/SidebarDocumentMenu.js +56 -0
  73. package/dist/app/sidebar/SidebarExpandedPanel.d.ts +9 -0
  74. package/dist/app/sidebar/SidebarExpandedPanel.js +78 -0
  75. package/dist/app/sidebar/SidebarIcons.d.ts +3 -0
  76. package/dist/app/sidebar/SidebarIcons.js +18 -0
  77. package/dist/app/sidebar/SidebarMenus.d.ts +19 -0
  78. package/dist/app/sidebar/SidebarMenus.js +64 -0
  79. package/dist/app/sidebar/SidebarPathUtils.d.ts +10 -0
  80. package/dist/app/sidebar/SidebarPathUtils.js +55 -0
  81. package/dist/app/sidebar/SidebarPrimitives.d.ts +12 -0
  82. package/dist/app/sidebar/SidebarPrimitives.js +45 -0
  83. package/dist/app/sidebar/SidebarRail.d.ts +5 -0
  84. package/dist/app/sidebar/SidebarRail.js +37 -0
  85. package/dist/app/sidebar/explorer/ExplorerRowMenu.d.ts +21 -0
  86. package/dist/app/sidebar/explorer/ExplorerRowMenu.js +37 -0
  87. package/dist/app/sidebar/explorer/ExplorerSections.d.ts +16 -0
  88. package/dist/app/sidebar/explorer/ExplorerSections.js +259 -0
  89. package/dist/app/sidebar/explorer/ExplorerTree.d.ts +46 -0
  90. package/dist/app/sidebar/explorer/ExplorerTree.js +387 -0
  91. package/dist/app/sidebar/explorer/explorerState.d.ts +37 -0
  92. package/dist/app/sidebar/explorer/explorerState.js +135 -0
  93. package/dist/app/tabMountPolicy.d.ts +45 -0
  94. package/dist/app/tabMountPolicy.js +104 -0
  95. package/dist/app/tabWarmOrder.d.ts +7 -0
  96. package/dist/app/tabWarmOrder.js +48 -0
  97. package/dist/app/tabs/TabFuseCurves.d.ts +3 -0
  98. package/dist/app/tabs/TabFuseCurves.js +15 -0
  99. package/dist/app/tabs/TabOverview.d.ts +24 -0
  100. package/dist/app/tabs/TabOverview.js +121 -0
  101. package/dist/app/tabs/TabStrip.d.ts +42 -0
  102. package/dist/app/tabs/TabStrip.js +380 -0
  103. package/dist/app/tabs/tabIds.d.ts +5 -0
  104. package/dist/app/tabs/tabIds.js +7 -0
  105. package/dist/app/tabs/useTabDirty.d.ts +3 -0
  106. package/dist/app/tabs/useTabDirty.js +11 -0
  107. package/dist/app/useAppChromeState.d.ts +21 -0
  108. package/dist/app/useAppChromeState.js +48 -0
  109. package/dist/app/useAppCommands.d.ts +66 -0
  110. package/dist/app/useAppCommands.js +71 -0
  111. package/dist/app/useAppDocumentCore.d.ts +210 -0
  112. package/dist/app/useAppDocumentCore.js +40 -0
  113. package/dist/app/useAppEffects.d.ts +18 -0
  114. package/dist/app/useAppEffects.js +52 -0
  115. package/dist/app/useAppPaneSearch.d.ts +37 -0
  116. package/dist/app/useAppPaneSearch.js +74 -0
  117. package/dist/app/useAppShellSetup.d.ts +40 -0
  118. package/dist/app/useAppShellSetup.js +52 -0
  119. package/dist/app/useAppSlots.d.ts +28 -0
  120. package/dist/app/useAppSlots.js +40 -0
  121. package/dist/app/useAppTabChrome.d.ts +51 -0
  122. package/dist/app/useAppTabChrome.js +66 -0
  123. package/dist/app/useAutoSave.d.ts +34 -0
  124. package/dist/app/useAutoSave.js +116 -0
  125. package/dist/app/useCloseAll.d.ts +52 -0
  126. package/dist/app/useCloseAll.js +295 -0
  127. package/dist/app/useClosedTabEviction.d.ts +10 -0
  128. package/dist/app/useClosedTabEviction.js +38 -0
  129. package/dist/app/useCommandPalette.d.ts +44 -0
  130. package/dist/app/useCommandPalette.js +254 -0
  131. package/dist/app/useCrossDocSearch.d.ts +7 -0
  132. package/dist/app/useCrossDocSearch.js +94 -0
  133. package/dist/app/useDistractionFree.d.ts +21 -0
  134. package/dist/app/useDistractionFree.js +73 -0
  135. package/dist/app/useDocumentCreation.d.ts +18 -0
  136. package/dist/app/useDocumentCreation.js +63 -0
  137. package/dist/app/useDocumentDirty.d.ts +31 -0
  138. package/dist/app/useDocumentDirty.js +46 -0
  139. package/dist/app/useDocumentLoader.d.ts +55 -0
  140. package/dist/app/useDocumentLoader.js +267 -0
  141. package/dist/app/useDocumentOutputActions.d.ts +6 -0
  142. package/dist/app/useDocumentOutputActions.js +45 -0
  143. package/dist/app/useDocumentRuntimeEffects.d.ts +36 -0
  144. package/dist/app/useDocumentRuntimeEffects.js +458 -0
  145. package/dist/app/useDocumentSaver.d.ts +58 -0
  146. package/dist/app/useDocumentSaver.js +511 -0
  147. package/dist/app/useDocumentTabs.d.ts +41 -0
  148. package/dist/app/useDocumentTabs.js +207 -0
  149. package/dist/app/useDocumentWatcher.d.ts +83 -0
  150. package/dist/app/useDocumentWatcher.js +393 -0
  151. package/dist/app/useDocumentZoom.d.ts +24 -0
  152. package/dist/app/useDocumentZoom.js +104 -0
  153. package/dist/app/useDocuments.d.ts +169 -0
  154. package/dist/app/useDocuments.js +455 -0
  155. package/dist/app/useDraftConfirmGate.d.ts +27 -0
  156. package/dist/app/useDraftConfirmGate.js +47 -0
  157. package/dist/app/useFileDragOverlay.d.ts +12 -0
  158. package/dist/app/useFileDragOverlay.js +49 -0
  159. package/dist/app/useFormatApis.d.ts +13 -0
  160. package/dist/app/useFormatApis.js +62 -0
  161. package/dist/app/useFormatDirtyState.d.ts +29 -0
  162. package/dist/app/useFormatDirtyState.js +81 -0
  163. package/dist/app/useGlobalShortcuts.d.ts +47 -0
  164. package/dist/app/useGlobalShortcuts.js +359 -0
  165. package/dist/app/useHtmlSession.d.ts +68 -0
  166. package/dist/app/useHtmlSession.js +463 -0
  167. package/dist/app/useHtmlSessionProps.d.ts +33 -0
  168. package/dist/app/useHtmlSessionProps.js +57 -0
  169. package/dist/app/useLibrary.d.ts +21 -0
  170. package/dist/app/useLibrary.js +81 -0
  171. package/dist/app/useOpenFromClipboard.d.ts +7 -0
  172. package/dist/app/useOpenFromClipboard.js +44 -0
  173. package/dist/app/usePaneLiveness.d.ts +29 -0
  174. package/dist/app/usePaneLiveness.js +199 -0
  175. package/dist/app/usePendingSavePaths.d.ts +19 -0
  176. package/dist/app/usePendingSavePaths.js +67 -0
  177. package/dist/app/useReadyShow.d.ts +12 -0
  178. package/dist/app/useReadyShow.js +142 -0
  179. package/dist/app/useSearchController.d.ts +22 -0
  180. package/dist/app/useSearchController.js +114 -0
  181. package/dist/app/useSetupRecovery.d.ts +41 -0
  182. package/dist/app/useSetupRecovery.js +153 -0
  183. package/dist/app/useTabChrome.d.ts +40 -0
  184. package/dist/app/useTabChrome.js +115 -0
  185. package/dist/app/useViewState.d.ts +27 -0
  186. package/dist/app/useViewState.js +160 -0
  187. package/dist/appName.d.ts +2 -0
  188. package/dist/appName.js +8 -0
  189. package/dist/documents/DocumentRouter.d.ts +102 -0
  190. package/dist/documents/DocumentRouter.js +153 -0
  191. package/dist/documents/FileTypeIcon.d.ts +6 -0
  192. package/dist/documents/FileTypeIcon.js +93 -0
  193. package/dist/documents/ImageDocument.d.ts +14 -0
  194. package/dist/documents/ImageDocument.js +61 -0
  195. package/dist/documents/capabilities.d.ts +17 -0
  196. package/dist/documents/capabilities.js +18 -0
  197. package/dist/documents/html/HtmlDocument.d.ts +15 -0
  198. package/dist/documents/html/HtmlDocument.js +584 -0
  199. package/dist/documents/html/HtmlDocumentLayout.d.ts +87 -0
  200. package/dist/documents/html/HtmlDocumentLayout.js +25 -0
  201. package/dist/documents/html/HtmlDocumentProps.d.ts +60 -0
  202. package/dist/documents/html/HtmlDocumentProps.js +1 -0
  203. package/dist/documents/html/HtmlDocumentSource.d.ts +34 -0
  204. package/dist/documents/html/HtmlDocumentSource.js +28 -0
  205. package/dist/documents/markdown/MarkdownSplitPane.d.ts +115 -0
  206. package/dist/documents/markdown/MarkdownSplitPane.js +352 -0
  207. package/dist/documents/mdx/MdxSplitPane.d.ts +32 -0
  208. package/dist/documents/mdx/MdxSplitPane.js +60 -0
  209. package/dist/documents/txt/LinkDialog.d.ts +8 -0
  210. package/dist/documents/txt/LinkDialog.js +34 -0
  211. package/dist/documents/txt/MarkdownSourceToolbar.d.ts +8 -0
  212. package/dist/documents/txt/MarkdownSourceToolbar.js +25 -0
  213. package/dist/documents/txt/SourceEditorToolbar.d.ts +12 -0
  214. package/dist/documents/txt/SourceEditorToolbar.js +4 -0
  215. package/dist/documents/txt/TxtEditor.d.ts +39 -0
  216. package/dist/documents/txt/TxtEditor.js +186 -0
  217. package/dist/documents/txt/sourceEditorTypes.d.ts +49 -0
  218. package/dist/documents/txt/sourceEditorTypes.js +1 -0
  219. package/dist/documents/txt/sourceExtensions.d.ts +38 -0
  220. package/dist/documents/txt/sourceExtensions.js +444 -0
  221. package/dist/documents/txt/sourceFormatting.d.ts +18 -0
  222. package/dist/documents/txt/sourceFormatting.js +183 -0
  223. package/dist/documents/txt/sourceTheme.d.ts +5 -0
  224. package/dist/documents/txt/sourceTheme.js +220 -0
  225. package/dist/documents/txt/useCodeMirrorSourceEditor.d.ts +69 -0
  226. package/dist/documents/txt/useCodeMirrorSourceEditor.js +546 -0
  227. package/dist/documents/txt/useSourceSearch.d.ts +39 -0
  228. package/dist/documents/txt/useSourceSearch.js +122 -0
  229. package/dist/documents/txt/useTextEditorCommands.d.ts +26 -0
  230. package/dist/documents/txt/useTextEditorCommands.js +151 -0
  231. package/dist/documents/web/LivePreviewPane.d.ts +11 -0
  232. package/dist/documents/web/LivePreviewPane.js +58 -0
  233. package/dist/env.d.ts +66 -0
  234. package/dist/index.d.ts +121 -0
  235. package/dist/index.js +123 -0
  236. package/dist/settings/AppearanceMenu.d.ts +9 -0
  237. package/dist/settings/AppearanceMenu.js +54 -0
  238. package/dist/settings/SettingsPanel.d.ts +7 -0
  239. package/dist/settings/SettingsPanel.js +104 -0
  240. package/dist/settings/publishedSections.d.ts +22 -0
  241. package/dist/settings/publishedSections.js +79 -0
  242. package/dist/settings/sections/AboutSection.d.ts +1 -0
  243. package/dist/settings/sections/AboutSection.js +13 -0
  244. package/dist/settings/sections/DiagnosticsSection.d.ts +1 -0
  245. package/dist/settings/sections/DiagnosticsSection.js +42 -0
  246. package/dist/settings/sections/PrivacySection.d.ts +3 -0
  247. package/dist/settings/sections/PrivacySection.js +51 -0
  248. package/dist/settings/sections/UpdateLicenseBlock.d.ts +10 -0
  249. package/dist/settings/sections/UpdateLicenseBlock.js +158 -0
  250. package/dist/state/explorer.d.ts +32 -0
  251. package/dist/state/explorer.js +111 -0
  252. package/dist/state/legal.d.ts +51 -0
  253. package/dist/state/legal.js +130 -0
  254. package/dist/state/library.d.ts +33 -0
  255. package/dist/state/library.js +110 -0
  256. package/dist/state/updateLicense.d.ts +31 -0
  257. package/dist/state/updateLicense.js +340 -0
  258. package/dist/styles/explorer.css +219 -0
  259. package/dist/styles/index.css +1477 -0
  260. package/dist/styles/tabs.css +467 -0
  261. package/dist/styles.css +6 -0
  262. package/package.json +88 -3
@@ -0,0 +1,44 @@
1
+ import { useCallback } from "react";
2
+ import { toast } from "@rightkit/platform-ui/headless";
3
+ import { normalizeLivePreviewUrl } from "@rightkit/document-core/documents/web/webUrl";
4
+ import { addRecentUrl } from "@rightkit/document-core/state/recentUrls";
5
+ import { readClipboardContent, openLinkedDocument } from "@rightkit/document-core/state/tauriBridge";
6
+ import { errorMessage } from "@rightkit/document-core/state/errors";
7
+ import { localPathFromClipboardText } from "./clipboardPaths.js";
8
+ /** Open from clipboard: copied files (CF_HDROP) open like a drag-drop; nothing → a toast. */
9
+ export function useOpenFromClipboard({ open, openLiveUrl, duplicateAsDraft, setError, }) {
10
+ return useCallback(async () => {
11
+ const clip = await readClipboardContent();
12
+ switch (clip.kind) {
13
+ case "files":
14
+ for (const p of clip.paths)
15
+ void open(p);
16
+ return;
17
+ case "image":
18
+ void open(clip.path);
19
+ return;
20
+ case "text":
21
+ if (normalizeLivePreviewUrl(clip.text)) {
22
+ addRecentUrl(clip.text.trim());
23
+ void openLiveUrl(clip.text);
24
+ return;
25
+ }
26
+ {
27
+ const localPath = localPathFromClipboardText(clip.text);
28
+ if (localPath) {
29
+ const granted = await openLinkedDocument(localPath).catch((e) => {
30
+ setError(errorMessage(e));
31
+ return null;
32
+ });
33
+ if (granted)
34
+ void open(granted);
35
+ return;
36
+ }
37
+ }
38
+ duplicateAsDraft(clip.text, "Pasted from clipboard");
39
+ return;
40
+ default:
41
+ toast("Nothing on the clipboard to open", "info");
42
+ }
43
+ }, [open, openLiveUrl, duplicateAsDraft, setError]);
44
+ }
@@ -0,0 +1,29 @@
1
+ import type { LoadedDocument } from "@rightkit/document-core/documents/types";
2
+ import type { BlockDraftRegistry } from "@rightkit/document-core/app/blockDrafts";
3
+ interface PaneLivenessDeps {
4
+ openDocs: LoadedDocument[];
5
+ activePath: string | null;
6
+ isDirty: (doc: LoadedDocument) => boolean;
7
+ /** In-flight saves (useDocuments' pendingSavePaths) — a saving tab's router stays live. */
8
+ pendingSavePaths: ReadonlySet<string>;
9
+ /** True while the launch restore is in flight; gates the post-restore warm-up walk. */
10
+ restoringSession: boolean;
11
+ /** Block draft registry: a tab with any open block draft is never shelled (editor-owned state). */
12
+ blockDrafts?: BlockDraftRegistry;
13
+ }
14
+ /**
15
+ * Warm-MRU pane liveness: which open tabs keep a live router vs a lightweight shell. Every open
16
+ * tab keeps a shell; only active + most-recent ordinary panes stay live, and writes or dirty state
17
+ * without an app-owned source/draft snapshot protect a path until safe to evict. Owns the MRU
18
+ * history (`recentPaths`), the previous-active tracker, the warm-order persistence + post-restore
19
+ * warm-up walk, `protectedPaths`/`formatByPath`/`desiredLivePaths`, and the deferred rendered/live
20
+ * reconciliation. `pruneClosedPaths` is called by App's shared closed-tab eviction effect so
21
+ * rendered/live/recent markers die with their tab. Pure policy lives in tabMountPolicy.ts /
22
+ * tabWarmOrder.ts. Extracted verbatim from App.tsx; bodies are unchanged.
23
+ */
24
+ export declare function usePaneLiveness({ openDocs, activePath, isDirty, pendingSavePaths, restoringSession, blockDrafts, }: PaneLivenessDeps): {
25
+ livePaths: Set<string>;
26
+ renderedPaths: Set<string>;
27
+ pruneClosedPaths: (closedPaths: string[]) => void;
28
+ };
29
+ export {};
@@ -0,0 +1,199 @@
1
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2
+ import { documentHasPinnedState } from "@rightkit/document-core/app/resourceCensus";
3
+ import { dirtyPaneMayShell, liveDocumentPaths } from "./tabMountPolicy.js";
4
+ import { loadTabWarmOrder, restoredTabWarmOrder, saveTabWarmOrder } from "./tabWarmOrder.js";
5
+ import { nativeAutomationOwnsSession } from "./useDocumentRuntimeEffects.js";
6
+ import { scheduleAfterPaintOrDelay } from "@rightkit/document-core/app/deferredMount";
7
+ /**
8
+ * Warm-MRU pane liveness: which open tabs keep a live router vs a lightweight shell. Every open
9
+ * tab keeps a shell; only active + most-recent ordinary panes stay live, and writes or dirty state
10
+ * without an app-owned source/draft snapshot protect a path until safe to evict. Owns the MRU
11
+ * history (`recentPaths`), the previous-active tracker, the warm-order persistence + post-restore
12
+ * warm-up walk, `protectedPaths`/`formatByPath`/`desiredLivePaths`, and the deferred rendered/live
13
+ * reconciliation. `pruneClosedPaths` is called by App's shared closed-tab eviction effect so
14
+ * rendered/live/recent markers die with their tab. Pure policy lives in tabMountPolicy.ts /
15
+ * tabWarmOrder.ts. Extracted verbatim from App.tsx; bodies are unchanged.
16
+ */
17
+ export function usePaneLiveness({ openDocs, activePath, isDirty, pendingSavePaths, restoringSession, blockDrafts, }) {
18
+ const previousActiveRef = useRef(activePath);
19
+ const [previousActivePath, setPreviousActivePath] = useState(null);
20
+ useLayoutEffect(() => {
21
+ if (previousActiveRef.current === activePath)
22
+ return;
23
+ setPreviousActivePath(previousActiveRef.current);
24
+ previousActiveRef.current = activePath;
25
+ }, [activePath]);
26
+ // Presence-only subscription: re-derive protection when a document gains or loses a block draft
27
+ // (open / commit / discard). Typing inside a draft does not fire it.
28
+ const [draftPresenceTick, setDraftPresenceTick] = useState(0);
29
+ useEffect(() => {
30
+ if (!blockDrafts)
31
+ return;
32
+ setDraftPresenceTick((tick) => tick + 1);
33
+ return blockDrafts.subscribePresence(() => setDraftPresenceTick((tick) => tick + 1));
34
+ }, [blockDrafts]);
35
+ // Every open tab keeps a lightweight shell. Only active + most-recent ordinary panes stay live;
36
+ // writes or dirty state without an app-owned source/draft snapshot add a path until safe to evict.
37
+ // biome-ignore lint/correctness/useExhaustiveDependencies: draftPresenceTick re-derives the set when a block draft opens or closes
38
+ const protectedPaths = useMemo(() => new Set(openDocs
39
+ .filter((doc) => {
40
+ // Switching tabs is the re-render boundary for ref-backed text edits. Read the just-
41
+ // departed active document here before after-paint reconciliation can evict its router.
42
+ const dirty = isDirty(doc);
43
+ if (doc.path === activePath && dirty)
44
+ return true;
45
+ // An open block draft (clean or modified) lives in its mounted editor, not in editsRef;
46
+ // shelling would orphan it. Protection ends when the draft is committed or discarded.
47
+ if (blockDrafts?.hasDraft(doc.path))
48
+ return true;
49
+ // TXT/Markdown sources are synchronously authoritative in editsRef; HTML entry and
50
+ // sub-page sources are synchronously authoritative in htmlPagesRef. Modified rich
51
+ // blocks use the app-owned registry. Those formats may therefore shell while dirty,
52
+ // but only through this narrow, explicit allow-list. DOCX/PDF fail closed; image
53
+ // remains governed by its recipe census.
54
+ const hasExternalSourceOrDraftOwner = doc.format === "txt" || doc.format === "md" || doc.format === "html";
55
+ const dirtyProtection = doc.format === "image"
56
+ ? documentHasPinnedState(doc.path)
57
+ : dirty && !dirtyPaneMayShell(doc.format, hasExternalSourceOrDraftOwner);
58
+ return dirtyProtection || pendingSavePaths.has(doc.path);
59
+ })
60
+ .map((doc) => doc.path)), [activePath, blockDrafts, draftPresenceTick, isDirty, openDocs, pendingSavePaths]);
61
+ // Background tabs keep lightweight shells. Active mounts synchronously in DocumentRouterTable;
62
+ // these sets only record visited paths & reconcile active/MRU/protected ownership after paint.
63
+ const [renderedPaths, setRenderedPaths] = useState(() => new Set());
64
+ const [livePaths, setLivePaths] = useState(() => new Set());
65
+ // Most-recently-active paths, newest first. Held as state (not a ref) because `desiredLivePaths`
66
+ // memoizes on it: a ref would mutate without re-running the memo, so a revisited tab would never
67
+ // re-enter the live set. Updated only when the active tab actually changes, so its identity is
68
+ // stable across unrelated renders and the after-paint reconciliation below cannot loop.
69
+ const automationOwnsSession = nativeAutomationOwnsSession();
70
+ const savedWarmOrder = useRef(automationOwnsSession ? [] : loadTabWarmOrder());
71
+ const restoreStarted = useRef(false);
72
+ const [warmOrderReady, setWarmOrderReady] = useState(false);
73
+ const [recentPaths, setRecentPaths] = useState(() => []);
74
+ useEffect(() => {
75
+ if (!activePath)
76
+ return;
77
+ setRecentPaths((prev) => {
78
+ if (prev[0] === activePath)
79
+ return prev;
80
+ return [activePath, ...prev.filter((path) => path !== activePath)];
81
+ });
82
+ }, [activePath]);
83
+ // Session restore loads file bytes & chunks in the background, but a restored router still used
84
+ // to cold-mount on first click because its in-memory MRU vanished at quit. Rebuild that bounded
85
+ // warm set after restore, one pane per quiet interval: prior activation order first, then tabs
86
+ // nearest the active one. User activity cancels remaining speculative work.
87
+ useEffect(() => {
88
+ if (restoringSession) {
89
+ restoreStarted.current = true;
90
+ return;
91
+ }
92
+ if (!restoreStarted.current)
93
+ return;
94
+ restoreStarted.current = false;
95
+ if (automationOwnsSession)
96
+ return;
97
+ setWarmOrderReady(true);
98
+ const queue = restoredTabWarmOrder(openDocs.map((doc) => doc.path), activePath, savedWarmOrder.current);
99
+ let index = 0;
100
+ let timer = 0;
101
+ const warmNext = () => {
102
+ const path = queue[index++];
103
+ if (!path)
104
+ return;
105
+ setRecentPaths((prev) => (prev.includes(path) ? prev : [...prev, path]));
106
+ timer = window.setTimeout(warmNext, 250);
107
+ };
108
+ timer = window.setTimeout(warmNext, 250);
109
+ return () => window.clearTimeout(timer);
110
+ }, [activePath, automationOwnsSession, openDocs, restoringSession]);
111
+ useEffect(() => {
112
+ if (!warmOrderReady || automationOwnsSession)
113
+ return;
114
+ saveTabWarmOrder(recentPaths);
115
+ }, [automationOwnsSession, recentPaths, warmOrderReady]);
116
+ useEffect(() => {
117
+ const openPaths = new Set(openDocs.map((doc) => doc.path));
118
+ setRecentPaths((prev) => {
119
+ const next = prev.filter((path) => openPaths.has(path));
120
+ return next.length === prev.length ? prev : next;
121
+ });
122
+ }, [openDocs]);
123
+ // Format per open path, so a SHELLED heavyweight document's mount-policy cost doesn't decay to
124
+ // ordinary weight merely because its census resources were released on eviction — see
125
+ // `candidateWeight` in tabMountPolicy.ts.
126
+ const formatByPath = useMemo(() => {
127
+ const map = new Map();
128
+ for (const doc of openDocs) {
129
+ if (doc.format === "txt" ||
130
+ doc.format === "md" ||
131
+ doc.format === "mdx" ||
132
+ doc.format === "html" ||
133
+ doc.format === "web" ||
134
+ doc.format === "docx" ||
135
+ doc.format === "pdf" ||
136
+ doc.format === "image")
137
+ map.set(doc.path, doc.format);
138
+ }
139
+ return map;
140
+ }, [openDocs]);
141
+ const desiredLivePaths = useMemo(() => liveDocumentPaths({
142
+ activePath,
143
+ previousActivePath,
144
+ protectedPaths,
145
+ recentPaths: recentPaths.filter((path) => formatByPath.has(path)),
146
+ formatByPath,
147
+ }), [activePath, previousActivePath, protectedPaths, recentPaths, formatByPath]);
148
+ // Active routers bypass this deferred state below, so a tab click mounts its viewer in the same
149
+ // commit and never paints an empty shell. Reconciliation remains deferred so eviction/cleanup
150
+ // cannot compete with activation work on the click frame.
151
+ useEffect(() => {
152
+ const target = desiredLivePaths;
153
+ return scheduleAfterPaintOrDelay(() => {
154
+ setRenderedPaths((prev) => {
155
+ const next = new Set(prev);
156
+ for (const path of target)
157
+ next.add(path);
158
+ if (next.size === prev.size && [...next].every((path) => prev.has(path)))
159
+ return prev;
160
+ return next;
161
+ });
162
+ setLivePaths((prev) => {
163
+ if (prev.size === target.size && [...prev].every((path) => target.has(path)))
164
+ return prev;
165
+ return new Set(target);
166
+ });
167
+ });
168
+ }, [desiredLivePaths]);
169
+ // T5-2: drop closed tabs' keep-alive markers — otherwise renderedPaths grows for the life of the
170
+ // app instead of tracking only the currently-open tabs, and without the recentPaths prune the MRU
171
+ // window fills with closed paths (which both starves live slots from the tabs actually open and
172
+ // lets a reopened path inherit stale recency it did not earn). Called by App's shared closed-tab
173
+ // eviction effect once it has diffed openDocs, so all per-path cache/liveness cleanup still runs
174
+ // in the same pass.
175
+ const pruneClosedPaths = useCallback((closedPaths) => {
176
+ setRenderedPaths((prevRendered) => {
177
+ if (!closedPaths.some((p) => prevRendered.has(p)))
178
+ return prevRendered;
179
+ const next = new Set(prevRendered);
180
+ for (const p of closedPaths)
181
+ next.delete(p);
182
+ return next;
183
+ });
184
+ setLivePaths((prevLive) => {
185
+ if (!closedPaths.some((p) => prevLive.has(p)))
186
+ return prevLive;
187
+ const next = new Set(prevLive);
188
+ for (const p of closedPaths)
189
+ next.delete(p);
190
+ return next;
191
+ });
192
+ setRecentPaths((prevRecent) => {
193
+ if (!closedPaths.some((p) => prevRecent.includes(p)))
194
+ return prevRecent;
195
+ return prevRecent.filter((p) => !closedPaths.includes(p));
196
+ });
197
+ }, []);
198
+ return { livePaths, renderedPaths, pruneClosedPaths };
199
+ }
@@ -0,0 +1,19 @@
1
+ interface PendingSavePathsDeps {
2
+ /** The raw DOCX persist (useFormatDirtyState's onDocxSave) — wrapped here with pending tracking. */
3
+ persistDocx: (path: string, buffer: ArrayBuffer) => Promise<void>;
4
+ /** useDocumentSaver's own pending set (text/image saves in flight). */
5
+ saverPendingSavePaths: ReadonlySet<string>;
6
+ }
7
+ /**
8
+ * The union of all in-flight save paths across the three save surfaces — the saver's own pending
9
+ * set (text/image writes), tracked DOCX binary writes, and per-page HTML session writes — exposed
10
+ * as `pendingSavePaths` so the shell keeps a tab's router pinned while a save is in flight.
11
+ * Extracted verbatim from useDocuments; bodies are unchanged.
12
+ */
13
+ export declare function usePendingSavePaths({ persistDocx, saverPendingSavePaths }: PendingSavePathsDeps): {
14
+ onDocxSave: (path: string, buffer: ArrayBuffer) => Promise<void>;
15
+ onHtmlSaveActivity: (entryPath: string, delta: 1 | -1) => void;
16
+ pendingSavePaths: Set<string>;
17
+ pendingSavePathsRef: import("react").RefObject<ReadonlySet<string>>;
18
+ };
19
+ export {};
@@ -0,0 +1,67 @@
1
+ import { useCallback, useMemo, useRef, useState } from "react";
2
+ /**
3
+ * The union of all in-flight save paths across the three save surfaces — the saver's own pending
4
+ * set (text/image writes), tracked DOCX binary writes, and per-page HTML session writes — exposed
5
+ * as `pendingSavePaths` so the shell keeps a tab's router pinned while a save is in flight.
6
+ * Extracted verbatim from useDocuments; bodies are unchanged.
7
+ */
8
+ export function usePendingSavePaths({ persistDocx, saverPendingSavePaths }) {
9
+ const pendingDocxSaveCounts = useRef(new Map());
10
+ const [pendingDocxSavePaths, setPendingDocxSavePaths] = useState(() => new Set());
11
+ const pendingHtmlSaveCounts = useRef(new Map());
12
+ const [pendingHtmlSavePaths, setPendingHtmlSavePaths] = useState(() => new Set());
13
+ const pendingSavePathsRef = useRef(new Set());
14
+ const onHtmlSaveActivity = useCallback((entryPath, delta) => {
15
+ const nextCount = Math.max(0, (pendingHtmlSaveCounts.current.get(entryPath) ?? 0) + delta);
16
+ if (nextCount > 0)
17
+ pendingHtmlSaveCounts.current.set(entryPath, nextCount);
18
+ else
19
+ pendingHtmlSaveCounts.current.delete(entryPath);
20
+ const pendingNow = new Set(pendingSavePathsRef.current);
21
+ if (nextCount > 0)
22
+ pendingNow.add(entryPath);
23
+ else
24
+ pendingNow.delete(entryPath);
25
+ pendingSavePathsRef.current = pendingNow;
26
+ setPendingHtmlSavePaths((prev) => {
27
+ if (nextCount > 0 === prev.has(entryPath))
28
+ return prev;
29
+ const next = new Set(prev);
30
+ if (nextCount > 0)
31
+ next.add(entryPath);
32
+ else
33
+ next.delete(entryPath);
34
+ return next;
35
+ });
36
+ }, []);
37
+ const onDocxSave = useCallback(async (path, buffer) => {
38
+ pendingDocxSaveCounts.current.set(path, (pendingDocxSaveCounts.current.get(path) ?? 0) + 1);
39
+ const pendingNow = new Set(pendingSavePathsRef.current);
40
+ pendingNow.add(path);
41
+ pendingSavePathsRef.current = pendingNow;
42
+ setPendingDocxSavePaths((prev) => (prev.has(path) ? prev : new Set(prev).add(path)));
43
+ try {
44
+ await persistDocx(path, buffer);
45
+ }
46
+ finally {
47
+ const remaining = (pendingDocxSaveCounts.current.get(path) ?? 1) - 1;
48
+ if (remaining > 0)
49
+ pendingDocxSaveCounts.current.set(path, remaining);
50
+ else {
51
+ pendingDocxSaveCounts.current.delete(path);
52
+ const pendingNow = new Set(pendingSavePathsRef.current);
53
+ pendingNow.delete(path);
54
+ pendingSavePathsRef.current = pendingNow;
55
+ setPendingDocxSavePaths((prev) => {
56
+ if (!prev.has(path))
57
+ return prev;
58
+ const next = new Set(prev);
59
+ next.delete(path);
60
+ return next;
61
+ });
62
+ }
63
+ }
64
+ }, [persistDocx]);
65
+ const pendingSavePaths = useMemo(() => new Set([...saverPendingSavePaths, ...pendingDocxSavePaths, ...pendingHtmlSavePaths]), [saverPendingSavePaths, pendingDocxSavePaths, pendingHtmlSavePaths]);
66
+ return { onDocxSave, onHtmlSaveActivity, pendingSavePaths, pendingSavePathsRef };
67
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The window launches hidden (tauri.conf.json `"visible": false`) and is revealed the instant React
3
+ * commits its first frame. We use useLayoutEffect: it runs synchronously right after the DOM is
4
+ * committed and — unlike requestAnimationFrame — fires even while the window is hidden. A hidden
5
+ * macOS WKWebView never schedules rAF (no compositing off-screen), so the old rAF-gated reveal always
6
+ * fell through to the safety timeout and cost ~4s on every macOS launch. Committing-then-showing is
7
+ * deterministic and instant on both macOS and Windows; the native surface is pre-painted to the theme
8
+ * color in showWindow() so the reveal never flashes white. Idempotent (shows once) so StrictMode's
9
+ * double-invoke in dev is harmless.
10
+ *
11
+ */
12
+ export declare function useReadyShow(beforeShow?: () => void | Promise<void>): void;
@@ -0,0 +1,142 @@
1
+ import { useLayoutEffect } from "react";
2
+ import { getCurrentWindow } from "@tauri-apps/api/window";
3
+ import { logError, logPerf } from "@rightkit/document-core/state/diagnostics";
4
+ // Captured at module load (early in the import graph) so app:shown measures from near-boot.
5
+ const bootStartedAt = performance.now();
6
+ let shown = false;
7
+ // WebView2 can briefly reveal the native surface while maximizing. Match it to the selected reading
8
+ // pane so exposed resize pixels blend into the document instead of flashing white.
9
+ function nativeColor(value) {
10
+ const channels = value.match(/^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/i);
11
+ if (!channels)
12
+ return value;
13
+ const byte = (channel) => Math.round(Math.max(0, Math.min(255, Number(channel))));
14
+ const alpha = channels[4]
15
+ ? Math.round(Math.max(0, Math.min(1, channels[4].endsWith("%")
16
+ ? Number(channels[4].slice(0, -1)) / 100
17
+ : Number(channels[4]))) * 255)
18
+ : 255;
19
+ return [byte(channels[1]), byte(channels[2]), byte(channels[3]), alpha];
20
+ }
21
+ function currentReaderBackground() {
22
+ try {
23
+ const htmlSurface = document
24
+ .querySelector('[data-vr-pane-state="active"] iframe[data-vr-html-surface]')
25
+ ?.dataset.vrHtmlSurface?.trim();
26
+ if (htmlSurface)
27
+ return nativeColor(htmlSurface);
28
+ const v = getComputedStyle(document.documentElement)
29
+ .getPropertyValue("--reader-surface")
30
+ .trim();
31
+ return v || "#F7F3EC";
32
+ }
33
+ catch {
34
+ return "#F7F3EC";
35
+ }
36
+ }
37
+ async function setNativeBackground() {
38
+ await getCurrentWindow().setBackgroundColor(currentReaderBackground());
39
+ }
40
+ function showWindow(beforeShow) {
41
+ if (shown)
42
+ return;
43
+ shown = true;
44
+ void Promise.resolve()
45
+ .then(async () => {
46
+ const win = getCurrentWindow();
47
+ // Set the native surface to the theme color FIRST, then reveal — so even the single composite
48
+ // between show() and the webview's first paint is themed, never white.
49
+ try {
50
+ await win.setBackgroundColor(currentReaderBackground());
51
+ }
52
+ catch {
53
+ if ("__TAURI_INTERNALS__" in window) {
54
+ logError("window:background-color-failed");
55
+ }
56
+ }
57
+ let resizeStartedAt;
58
+ let resizeTimer;
59
+ void win
60
+ .onResized(() => {
61
+ resizeStartedAt ??= performance.now();
62
+ window.clearTimeout(resizeTimer);
63
+ resizeTimer = window.setTimeout(() => {
64
+ const startedAt = resizeStartedAt;
65
+ if (startedAt === undefined)
66
+ return;
67
+ logPerf("window:resize", startedAt);
68
+ resizeStartedAt = undefined;
69
+ }, 150);
70
+ })
71
+ .then((unlisten) => window.addEventListener("beforeunload", unlisten, { once: true }))
72
+ .catch(() => {
73
+ if ("__TAURI_INTERNALS__" in window) {
74
+ logError("window:resize-listener-failed");
75
+ }
76
+ });
77
+ // Caller-supplied pre-reveal step (e.g. the legal gate resizing/centering the window to its
78
+ // own 960x560 before it becomes visible) so the FIRST thing the user ever sees is already the
79
+ // right size — never a flash of the full app window followed by a shrink.
80
+ if (beforeShow) {
81
+ try {
82
+ await beforeShow();
83
+ }
84
+ catch {
85
+ // Best-effort — a failed resize must never block the reveal itself.
86
+ }
87
+ }
88
+ await win.show();
89
+ })
90
+ .then(() => logPerf("app:shown", bootStartedAt))
91
+ .catch(() => {
92
+ // Browser QA has no Tauri window; the app remains visible in that environment.
93
+ });
94
+ }
95
+ /**
96
+ * The window launches hidden (tauri.conf.json `"visible": false`) and is revealed the instant React
97
+ * commits its first frame. We use useLayoutEffect: it runs synchronously right after the DOM is
98
+ * committed and — unlike requestAnimationFrame — fires even while the window is hidden. A hidden
99
+ * macOS WKWebView never schedules rAF (no compositing off-screen), so the old rAF-gated reveal always
100
+ * fell through to the safety timeout and cost ~4s on every macOS launch. Committing-then-showing is
101
+ * deterministic and instant on both macOS and Windows; the native surface is pre-painted to the theme
102
+ * color in showWindow() so the reveal never flashes white. Idempotent (shows once) so StrictMode's
103
+ * double-invoke in dev is harmless.
104
+ *
105
+ */
106
+ export function useReadyShow(beforeShow) {
107
+ useLayoutEffect(() => {
108
+ showWindow(beforeShow);
109
+ const isTauri = "__TAURI_INTERNALS__" in window;
110
+ let color = JSON.stringify(currentReaderBackground());
111
+ const refreshBackground = () => {
112
+ const next = currentReaderBackground();
113
+ const serialized = JSON.stringify(next);
114
+ if (serialized === color)
115
+ return;
116
+ color = serialized;
117
+ void setNativeBackground().catch(() => {
118
+ if (isTauri)
119
+ logError("window:background-color-failed");
120
+ });
121
+ };
122
+ const rootObserver = new MutationObserver(refreshBackground);
123
+ rootObserver.observe(document.documentElement, {
124
+ attributes: true,
125
+ attributeFilter: ["style"],
126
+ });
127
+ const paneObserver = new MutationObserver(refreshBackground);
128
+ paneObserver.observe(document.documentElement, {
129
+ attributes: true,
130
+ subtree: true,
131
+ attributeFilter: ["data-vr-pane-state", "data-vr-html-surface"],
132
+ });
133
+ // beforeShow is intentionally a dependency: re-running this effect after the window has already
134
+ // shown is a harmless no-op (`showWindow`'s module-level `shown` guard short-circuits it), so callers
135
+ // that pass a beforeShow whose identity changes (e.g. AppRoot's resize callback, stable via
136
+ // useCallback but re-created when the legal-gate/app decision flips) never risk a stale closure.
137
+ return () => {
138
+ rootObserver.disconnect();
139
+ paneObserver.disconnect();
140
+ };
141
+ }, [beforeShow]);
142
+ }
@@ -0,0 +1,22 @@
1
+ import type { LoadedDocument, SearchProvider } from "@rightkit/document-core/documents/types";
2
+ export type SearchScope = "current" | "all";
3
+ /**
4
+ * Shared find/replace controller. With keep-alive, several viewers can be mounted at once, so search
5
+ * providers are registered PATH-SCOPED (each viewer registers its own path's provider on mount /
6
+ * null on unmount); the LIVE provider that Find uses is the active tab's, re-selected on tab switch
7
+ * (NOT on remount). Also owns find-bar open/replace state and the single toolbar FindBar element.
8
+ */
9
+ export declare function useSearchController(activePath: string | null, active: LoadedDocument | null, openDocs?: LoadedDocument[], activate?: (path: string) => Promise<void>): {
10
+ searchProvider: SearchProvider | null;
11
+ registerSearchProvider: (path: string, provider: SearchProvider | null) => void;
12
+ openFind: (replace: boolean) => void;
13
+ closeFind: () => void;
14
+ toggleFind: (replace: boolean) => void;
15
+ openFindAll: () => void;
16
+ findBarEl: import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | null;
17
+ canOpenFind: boolean;
18
+ findOpen: boolean;
19
+ findMode: "find" | "replace" | undefined;
20
+ scope: SearchScope;
21
+ setScope: import("react").Dispatch<import("react").SetStateAction<SearchScope>>;
22
+ };
@@ -0,0 +1,114 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { FindBar } from "./FindBar.js";
4
+ import { buildCrossDocProvider } from "./useCrossDocSearch.js";
5
+ /** Formats whose find surface registers asynchronously, so Find may open before its provider exists. */
6
+ const EARLY_FIND_FORMATS = new Set(["html", "docx", "md", "mdx"]);
7
+ /**
8
+ * Shared find/replace controller. With keep-alive, several viewers can be mounted at once, so search
9
+ * providers are registered PATH-SCOPED (each viewer registers its own path's provider on mount /
10
+ * null on unmount); the LIVE provider that Find uses is the active tab's, re-selected on tab switch
11
+ * (NOT on remount). Also owns find-bar open/replace state and the single toolbar FindBar element.
12
+ */
13
+ export function useSearchController(activePath, active, openDocs, activate) {
14
+ const [searchProvider, setSearchProvider] = useState(null);
15
+ const [findState, setFindState] = useState({ open: false, replace: false });
16
+ const [scope, setScope] = useState("current");
17
+ const providers = useRef(new Map());
18
+ const activePathRef = useRef(activePath);
19
+ activePathRef.current = activePath;
20
+ const crossDocRef = useRef(null);
21
+ // Latest state mirrors for callbacks that must keep a stable identity across a find session.
22
+ const searchProviderRef = useRef(null);
23
+ searchProviderRef.current = searchProvider;
24
+ const findStateRef = useRef(findState);
25
+ findStateRef.current = findState;
26
+ // A viewer registers (provider) / unregisters (null) its OWN path. If it's the active tab, it also
27
+ // becomes the live provider immediately.
28
+ const registerSearchProvider = useCallback((path, provider) => {
29
+ if (provider)
30
+ providers.current.set(path, provider);
31
+ else
32
+ providers.current.delete(path);
33
+ if (path === activePathRef.current)
34
+ setSearchProvider(provider);
35
+ }, []);
36
+ // On tab switch, swap to that tab's already-registered provider (warm panes don't remount).
37
+ // In "all" scope, build a cross-doc provider aggregating all warm tabs. It is rebuilt only when
38
+ // the set of open tabs changes: stepping to a match in another tab activates that tab, and
39
+ // rebuilding then would reset the match position to the first tab.
40
+ const openDocsRef = useRef(openDocs);
41
+ openDocsRef.current = openDocs;
42
+ const activateRef = useRef(activate);
43
+ activateRef.current = activate;
44
+ const crossDocScope = scope === "all" && !!openDocs && !!activate;
45
+ const openPathsKey = openDocs?.map((doc) => doc.path).join("\0") ?? "";
46
+ const followedPath = crossDocScope ? null : activePath;
47
+ useEffect(() => {
48
+ const docs = openDocsRef.current;
49
+ if (crossDocScope && docs && openPathsKey && providers.current.size >= 2) {
50
+ // Tear down previous cross-doc provider.
51
+ crossDocRef.current?.close();
52
+ const crossDoc = buildCrossDocProvider(providers.current, docs, (path) => activateRef.current ? activateRef.current(path) : Promise.resolve());
53
+ crossDocRef.current = crossDoc;
54
+ setSearchProvider(crossDoc);
55
+ }
56
+ else {
57
+ crossDocRef.current?.close();
58
+ crossDocRef.current = null;
59
+ setSearchProvider(providers.current.get(followedPath ?? activePathRef.current ?? "") ?? null);
60
+ }
61
+ }, [crossDocScope, openPathsKey, followedPath]);
62
+ const openFind = useCallback((replace) => {
63
+ setFindState({ open: true, replace });
64
+ }, []);
65
+ const openFindAll = useCallback(() => {
66
+ setScope("all");
67
+ setFindState({ open: true, replace: false });
68
+ }, []);
69
+ // Open even if the provider is still racing into place (HTML Safe render registers on iframe load;
70
+ // Interactive registers a postMessage-driven preview provider; DOCX registers when its editor view
71
+ // is ready; Markdown/MDX register once the ProseMirror view mounts). When the HTML source drawer is
72
+ // open it registers a "source" provider instead.
73
+ //
74
+ // Every format that owns a find surface must be listed in EARLY_FIND_FORMATS. Markdown was
75
+ // missing, so Cmd/Ctrl+F was a silent no-op on a .md tab until its editor happened to win the
76
+ // race — the failure mode is invisible (no error, nothing opens), which is why it went unnoticed.
77
+ const canOpenFind = !!searchProvider || (!!active?.format && EARLY_FIND_FORMATS.has(active.format));
78
+ const canCrossDoc = !!openDocs && openDocs.length >= 2;
79
+ // closeFind/toggleFind read live state through refs, so their identities stay stable for the
80
+ // whole find session instead of changing on every provider or open/replace transition.
81
+ const closeFind = useCallback(() => {
82
+ setFindState((current) => ({ ...current, open: false }));
83
+ crossDocRef.current?.close();
84
+ crossDocRef.current = null;
85
+ searchProviderRef.current?.close();
86
+ }, []);
87
+ const toggleFind = useCallback((replace) => {
88
+ const current = findStateRef.current;
89
+ if (current.open && current.replace === replace) {
90
+ closeFind();
91
+ return;
92
+ }
93
+ setFindState({ open: true, replace });
94
+ }, [closeFind]);
95
+ const findBarEl = active && findState.open && searchProvider ? (_jsx(FindBar, { provider: searchProvider, placement: active.format === "txt" ? "floating" : "toolbar", initialReplaceMode: findState.replace, scope: scope, onScopeChange: setScope, canCrossDoc: canCrossDoc, onClose: closeFind })) : null;
96
+ return {
97
+ searchProvider,
98
+ registerSearchProvider,
99
+ openFind,
100
+ closeFind,
101
+ toggleFind,
102
+ openFindAll,
103
+ findBarEl,
104
+ canOpenFind,
105
+ findOpen: findState.open,
106
+ findMode: findState.open
107
+ ? findState.replace
108
+ ? "replace"
109
+ : "find"
110
+ : undefined,
111
+ scope,
112
+ setScope,
113
+ };
114
+ }