@rightkit/document-workspace 0.0.0-stage → 0.1.1
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.
- package/CHANGELOG.md +11 -0
- package/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +74 -2
- package/dist/app/App.d.ts +1 -0
- package/dist/app/App.js +6 -0
- package/dist/app/AppDialogs.d.ts +52 -0
- package/dist/app/AppDialogs.js +23 -0
- package/dist/app/AppDialogsHost.d.ts +31 -0
- package/dist/app/AppDialogsHost.js +30 -0
- package/dist/app/AppLifecycle.d.ts +19 -0
- package/dist/app/AppLifecycle.js +87 -0
- package/dist/app/AppProvider.d.ts +317 -0
- package/dist/app/AppProvider.js +217 -0
- package/dist/app/AppRoot.d.ts +8 -0
- package/dist/app/AppRoot.js +159 -0
- package/dist/app/AppShell.d.ts +2 -0
- package/dist/app/AppShell.js +81 -0
- package/dist/app/CommandPalette.d.ts +13 -0
- package/dist/app/CommandPalette.js +47 -0
- package/dist/app/DocActionBar.d.ts +36 -0
- package/dist/app/DocActionBar.js +300 -0
- package/dist/app/DocumentContextMenu.d.ts +29 -0
- package/dist/app/DocumentContextMenu.js +122 -0
- package/dist/app/DocumentPane.d.ts +52 -0
- package/dist/app/DocumentPane.js +30 -0
- package/dist/app/DocumentRouterTable.d.ts +58 -0
- package/dist/app/DocumentRouterTable.js +143 -0
- package/dist/app/ErrorBoundary.d.ts +12 -0
- package/dist/app/ErrorBoundary.js +16 -0
- package/dist/app/FindBar.d.ts +12 -0
- package/dist/app/FindBar.js +199 -0
- package/dist/app/LiveTitle.d.ts +14 -0
- package/dist/app/LiveTitle.js +14 -0
- package/dist/app/OpeningProgress.d.ts +13 -0
- package/dist/app/OpeningProgress.js +21 -0
- package/dist/app/PrintPreviewWindow.d.ts +2 -0
- package/dist/app/PrintPreviewWindow.js +57 -0
- package/dist/app/ShortcutHelp.d.ts +3 -0
- package/dist/app/ShortcutHelp.js +99 -0
- package/dist/app/Sidebar.d.ts +36 -0
- package/dist/app/Sidebar.js +121 -0
- package/dist/app/ToastViewport.d.ts +1 -0
- package/dist/app/ToastViewport.js +31 -0
- package/dist/app/Toolbar.d.ts +26 -0
- package/dist/app/Toolbar.js +63 -0
- package/dist/app/activeFormatSavers.d.ts +7 -0
- package/dist/app/activeFormatSavers.js +4 -0
- package/dist/app/clipboardPaths.d.ts +1 -0
- package/dist/app/clipboardPaths.js +31 -0
- package/dist/app/documentDrafts.d.ts +17 -0
- package/dist/app/documentDrafts.js +104 -0
- package/dist/app/draftResolution.d.ts +5 -0
- package/dist/app/draftResolution.js +25 -0
- package/dist/app/markdownPaletteActions.d.ts +8 -0
- package/dist/app/markdownPaletteActions.js +107 -0
- package/dist/app/resourceCensusDev.d.ts +41 -0
- package/dist/app/resourceCensusDev.js +518 -0
- package/dist/app/setupRecovery.d.ts +34 -0
- package/dist/app/setupRecovery.js +70 -0
- package/dist/app/shell/DocStatsPill.d.ts +18 -0
- package/dist/app/shell/DocStatsPill.js +32 -0
- package/dist/app/shell/DocumentTools.d.ts +9 -0
- package/dist/app/shell/DocumentTools.js +73 -0
- package/dist/app/shell/InfoInspector.d.ts +9 -0
- package/dist/app/shell/InfoInspector.js +67 -0
- package/dist/app/shell/StartPage.d.ts +9 -0
- package/dist/app/shell/StartPage.js +49 -0
- package/dist/app/shell/readEdit.d.ts +6 -0
- package/dist/app/shell/readEdit.js +6 -0
- package/dist/app/sidebar/SidebarDocumentMenu.d.ts +20 -0
- package/dist/app/sidebar/SidebarDocumentMenu.js +56 -0
- package/dist/app/sidebar/SidebarExpandedPanel.d.ts +9 -0
- package/dist/app/sidebar/SidebarExpandedPanel.js +78 -0
- package/dist/app/sidebar/SidebarIcons.d.ts +3 -0
- package/dist/app/sidebar/SidebarIcons.js +18 -0
- package/dist/app/sidebar/SidebarMenus.d.ts +19 -0
- package/dist/app/sidebar/SidebarMenus.js +64 -0
- package/dist/app/sidebar/SidebarPathUtils.d.ts +10 -0
- package/dist/app/sidebar/SidebarPathUtils.js +55 -0
- package/dist/app/sidebar/SidebarPrimitives.d.ts +12 -0
- package/dist/app/sidebar/SidebarPrimitives.js +45 -0
- package/dist/app/sidebar/SidebarRail.d.ts +5 -0
- package/dist/app/sidebar/SidebarRail.js +37 -0
- package/dist/app/sidebar/explorer/ExplorerRowMenu.d.ts +21 -0
- package/dist/app/sidebar/explorer/ExplorerRowMenu.js +37 -0
- package/dist/app/sidebar/explorer/ExplorerSections.d.ts +16 -0
- package/dist/app/sidebar/explorer/ExplorerSections.js +259 -0
- package/dist/app/sidebar/explorer/ExplorerTree.d.ts +46 -0
- package/dist/app/sidebar/explorer/ExplorerTree.js +387 -0
- package/dist/app/sidebar/explorer/explorerState.d.ts +37 -0
- package/dist/app/sidebar/explorer/explorerState.js +135 -0
- package/dist/app/tabMountPolicy.d.ts +45 -0
- package/dist/app/tabMountPolicy.js +104 -0
- package/dist/app/tabWarmOrder.d.ts +7 -0
- package/dist/app/tabWarmOrder.js +48 -0
- package/dist/app/tabs/TabFuseCurves.d.ts +3 -0
- package/dist/app/tabs/TabFuseCurves.js +15 -0
- package/dist/app/tabs/TabOverview.d.ts +24 -0
- package/dist/app/tabs/TabOverview.js +121 -0
- package/dist/app/tabs/TabStrip.d.ts +42 -0
- package/dist/app/tabs/TabStrip.js +380 -0
- package/dist/app/tabs/tabIds.d.ts +5 -0
- package/dist/app/tabs/tabIds.js +7 -0
- package/dist/app/tabs/useTabDirty.d.ts +3 -0
- package/dist/app/tabs/useTabDirty.js +11 -0
- package/dist/app/useAppChromeState.d.ts +21 -0
- package/dist/app/useAppChromeState.js +48 -0
- package/dist/app/useAppCommands.d.ts +66 -0
- package/dist/app/useAppCommands.js +71 -0
- package/dist/app/useAppDocumentCore.d.ts +210 -0
- package/dist/app/useAppDocumentCore.js +40 -0
- package/dist/app/useAppEffects.d.ts +18 -0
- package/dist/app/useAppEffects.js +52 -0
- package/dist/app/useAppPaneSearch.d.ts +37 -0
- package/dist/app/useAppPaneSearch.js +74 -0
- package/dist/app/useAppShellSetup.d.ts +40 -0
- package/dist/app/useAppShellSetup.js +52 -0
- package/dist/app/useAppSlots.d.ts +28 -0
- package/dist/app/useAppSlots.js +40 -0
- package/dist/app/useAppTabChrome.d.ts +51 -0
- package/dist/app/useAppTabChrome.js +66 -0
- package/dist/app/useAutoSave.d.ts +34 -0
- package/dist/app/useAutoSave.js +116 -0
- package/dist/app/useCloseAll.d.ts +52 -0
- package/dist/app/useCloseAll.js +295 -0
- package/dist/app/useClosedTabEviction.d.ts +10 -0
- package/dist/app/useClosedTabEviction.js +38 -0
- package/dist/app/useCommandPalette.d.ts +44 -0
- package/dist/app/useCommandPalette.js +254 -0
- package/dist/app/useCrossDocSearch.d.ts +7 -0
- package/dist/app/useCrossDocSearch.js +94 -0
- package/dist/app/useDistractionFree.d.ts +21 -0
- package/dist/app/useDistractionFree.js +73 -0
- package/dist/app/useDocumentCreation.d.ts +18 -0
- package/dist/app/useDocumentCreation.js +63 -0
- package/dist/app/useDocumentDirty.d.ts +31 -0
- package/dist/app/useDocumentDirty.js +46 -0
- package/dist/app/useDocumentLoader.d.ts +55 -0
- package/dist/app/useDocumentLoader.js +267 -0
- package/dist/app/useDocumentOutputActions.d.ts +6 -0
- package/dist/app/useDocumentOutputActions.js +45 -0
- package/dist/app/useDocumentRuntimeEffects.d.ts +36 -0
- package/dist/app/useDocumentRuntimeEffects.js +458 -0
- package/dist/app/useDocumentSaver.d.ts +58 -0
- package/dist/app/useDocumentSaver.js +511 -0
- package/dist/app/useDocumentTabs.d.ts +41 -0
- package/dist/app/useDocumentTabs.js +207 -0
- package/dist/app/useDocumentWatcher.d.ts +83 -0
- package/dist/app/useDocumentWatcher.js +393 -0
- package/dist/app/useDocumentZoom.d.ts +24 -0
- package/dist/app/useDocumentZoom.js +104 -0
- package/dist/app/useDocuments.d.ts +169 -0
- package/dist/app/useDocuments.js +455 -0
- package/dist/app/useDraftConfirmGate.d.ts +27 -0
- package/dist/app/useDraftConfirmGate.js +47 -0
- package/dist/app/useFileDragOverlay.d.ts +12 -0
- package/dist/app/useFileDragOverlay.js +49 -0
- package/dist/app/useFormatApis.d.ts +13 -0
- package/dist/app/useFormatApis.js +62 -0
- package/dist/app/useFormatDirtyState.d.ts +29 -0
- package/dist/app/useFormatDirtyState.js +81 -0
- package/dist/app/useGlobalShortcuts.d.ts +47 -0
- package/dist/app/useGlobalShortcuts.js +359 -0
- package/dist/app/useHtmlSession.d.ts +68 -0
- package/dist/app/useHtmlSession.js +463 -0
- package/dist/app/useHtmlSessionProps.d.ts +33 -0
- package/dist/app/useHtmlSessionProps.js +57 -0
- package/dist/app/useLibrary.d.ts +21 -0
- package/dist/app/useLibrary.js +81 -0
- package/dist/app/useOpenFromClipboard.d.ts +7 -0
- package/dist/app/useOpenFromClipboard.js +44 -0
- package/dist/app/usePaneLiveness.d.ts +29 -0
- package/dist/app/usePaneLiveness.js +199 -0
- package/dist/app/usePendingSavePaths.d.ts +19 -0
- package/dist/app/usePendingSavePaths.js +67 -0
- package/dist/app/useReadyShow.d.ts +12 -0
- package/dist/app/useReadyShow.js +142 -0
- package/dist/app/useSearchController.d.ts +22 -0
- package/dist/app/useSearchController.js +114 -0
- package/dist/app/useSetupRecovery.d.ts +41 -0
- package/dist/app/useSetupRecovery.js +153 -0
- package/dist/app/useTabChrome.d.ts +40 -0
- package/dist/app/useTabChrome.js +115 -0
- package/dist/app/useViewState.d.ts +27 -0
- package/dist/app/useViewState.js +160 -0
- package/dist/appName.d.ts +2 -0
- package/dist/appName.js +8 -0
- package/dist/documents/DocumentRouter.d.ts +102 -0
- package/dist/documents/DocumentRouter.js +153 -0
- package/dist/documents/FileTypeIcon.d.ts +6 -0
- package/dist/documents/FileTypeIcon.js +93 -0
- package/dist/documents/ImageDocument.d.ts +14 -0
- package/dist/documents/ImageDocument.js +61 -0
- package/dist/documents/capabilities.d.ts +17 -0
- package/dist/documents/capabilities.js +18 -0
- package/dist/documents/html/HtmlDocument.d.ts +15 -0
- package/dist/documents/html/HtmlDocument.js +584 -0
- package/dist/documents/html/HtmlDocumentLayout.d.ts +87 -0
- package/dist/documents/html/HtmlDocumentLayout.js +25 -0
- package/dist/documents/html/HtmlDocumentProps.d.ts +60 -0
- package/dist/documents/html/HtmlDocumentProps.js +1 -0
- package/dist/documents/html/HtmlDocumentSource.d.ts +34 -0
- package/dist/documents/html/HtmlDocumentSource.js +28 -0
- package/dist/documents/markdown/MarkdownSplitPane.d.ts +115 -0
- package/dist/documents/markdown/MarkdownSplitPane.js +352 -0
- package/dist/documents/mdx/MdxSplitPane.d.ts +32 -0
- package/dist/documents/mdx/MdxSplitPane.js +60 -0
- package/dist/documents/txt/LinkDialog.d.ts +8 -0
- package/dist/documents/txt/LinkDialog.js +34 -0
- package/dist/documents/txt/MarkdownSourceToolbar.d.ts +8 -0
- package/dist/documents/txt/MarkdownSourceToolbar.js +25 -0
- package/dist/documents/txt/SourceEditorToolbar.d.ts +12 -0
- package/dist/documents/txt/SourceEditorToolbar.js +4 -0
- package/dist/documents/txt/TxtEditor.d.ts +39 -0
- package/dist/documents/txt/TxtEditor.js +186 -0
- package/dist/documents/txt/sourceEditorTypes.d.ts +49 -0
- package/dist/documents/txt/sourceEditorTypes.js +1 -0
- package/dist/documents/txt/sourceExtensions.d.ts +38 -0
- package/dist/documents/txt/sourceExtensions.js +444 -0
- package/dist/documents/txt/sourceFormatting.d.ts +18 -0
- package/dist/documents/txt/sourceFormatting.js +183 -0
- package/dist/documents/txt/sourceTheme.d.ts +5 -0
- package/dist/documents/txt/sourceTheme.js +220 -0
- package/dist/documents/txt/useCodeMirrorSourceEditor.d.ts +69 -0
- package/dist/documents/txt/useCodeMirrorSourceEditor.js +546 -0
- package/dist/documents/txt/useSourceSearch.d.ts +39 -0
- package/dist/documents/txt/useSourceSearch.js +122 -0
- package/dist/documents/txt/useTextEditorCommands.d.ts +26 -0
- package/dist/documents/txt/useTextEditorCommands.js +151 -0
- package/dist/documents/web/LivePreviewPane.d.ts +11 -0
- package/dist/documents/web/LivePreviewPane.js +58 -0
- package/dist/env.d.ts +66 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.js +123 -0
- package/dist/settings/AppearanceMenu.d.ts +9 -0
- package/dist/settings/AppearanceMenu.js +54 -0
- package/dist/settings/SettingsPanel.d.ts +7 -0
- package/dist/settings/SettingsPanel.js +104 -0
- package/dist/settings/publishedSections.d.ts +22 -0
- package/dist/settings/publishedSections.js +79 -0
- package/dist/settings/sections/AboutSection.d.ts +1 -0
- package/dist/settings/sections/AboutSection.js +13 -0
- package/dist/settings/sections/DiagnosticsSection.d.ts +1 -0
- package/dist/settings/sections/DiagnosticsSection.js +42 -0
- package/dist/settings/sections/PrivacySection.d.ts +3 -0
- package/dist/settings/sections/PrivacySection.js +51 -0
- package/dist/settings/sections/UpdateLicenseBlock.d.ts +10 -0
- package/dist/settings/sections/UpdateLicenseBlock.js +158 -0
- package/dist/state/explorer.d.ts +32 -0
- package/dist/state/explorer.js +111 -0
- package/dist/state/legal.d.ts +51 -0
- package/dist/state/legal.js +130 -0
- package/dist/state/library.d.ts +33 -0
- package/dist/state/library.js +110 -0
- package/dist/state/updateLicense.d.ts +31 -0
- package/dist/state/updateLicense.js +340 -0
- package/dist/styles/explorer.css +219 -0
- package/dist/styles/index.css +1477 -0
- package/dist/styles/tabs.css +467 -0
- package/dist/styles.css +6 -0
- 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
|
+
}
|