@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.
- package/CHANGELOG.md +5 -0
- package/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +51 -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,143 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
3
|
+
import { effectiveReadEdit } from "./shell/readEdit.js";
|
|
4
|
+
import { DocumentRouter } from "../documents/DocumentRouter.js";
|
|
5
|
+
import { capabilitiesFor } from "../documents/capabilities.js";
|
|
6
|
+
import { isUntitled } from "@rightkit/document-core/documents/types";
|
|
7
|
+
import { PaneActiveContext } from "@rightkit/document-core/app/shell/docBar";
|
|
8
|
+
import { hostAppName } from "../appName.js";
|
|
9
|
+
// Inactive DOCX/PDF panes must not publish toolbar or view-state updates, but their callbacks also
|
|
10
|
+
// must not change identity on every App render (their effects depend on these props).
|
|
11
|
+
const inactiveDocxToolbar = (_api) => { };
|
|
12
|
+
const inactivePdfViewState = (_view) => { };
|
|
13
|
+
const inactivePdfPrint = (_fn) => { };
|
|
14
|
+
const inactiveMarkdownExport = (_api) => { };
|
|
15
|
+
// Keystrokes live inside CodeMirror + editsRef and should not re-render the whole viewer tree.
|
|
16
|
+
// External writes still bump App once, so this snapshot re-syncs reload/save/restore paths without
|
|
17
|
+
// subscribing the active editor shell to every keypress.
|
|
18
|
+
function LiveDocumentRouter({ doc, getEditSnapshot, ...props }) {
|
|
19
|
+
// Local keystrokes already live inside each editor + edit store. Freeze this input until disk/doc
|
|
20
|
+
// identity changes; otherwise unrelated shell bookkeeping can feed a newer snapshot back through
|
|
21
|
+
// DocumentRouter and repaint a heavy viewer mid-edit. Cold remounts still read latest snapshot.
|
|
22
|
+
const routerText = useMemo(() => getEditSnapshot(doc.path) ?? doc.content, [doc, getEditSnapshot]);
|
|
23
|
+
return _jsx(DocumentRouter, { ...props, doc: doc, text: routerText });
|
|
24
|
+
}
|
|
25
|
+
// T2-3: memoized so an App re-render unrelated to the open-tab set (zoomHud toggle, sidebar collapse,
|
|
26
|
+
// settings dialog, etc.) doesn't re-run this component's `.map()` over every mounted doc. The bigger win
|
|
27
|
+
// is downstream — each DocumentRouter instance below already bails via its own memo comparator
|
|
28
|
+
// (documentRouterPropsEqual) — but that only helps if THIS component doesn't rebuild every child element
|
|
29
|
+
// from scratch first. Most handler props here are already useCallback-stable from their owning hooks
|
|
30
|
+
// (useDocuments/useDocumentTabs/useViewState); App.tsx now also stabilizes onDocumentWheel and
|
|
31
|
+
// onOpenPath, which used to be rebuilt (or an inline arrow) on every render.
|
|
32
|
+
function DocumentRouterTableImpl({ mountedDocs, activePath, livePaths, renderedPaths, views, contentScrollRef, onContentScroll, onDocumentWheel, subscribeEdits, getEditSnapshot, getExternalSeq, sourceViewStateRef, onTxtChange, onSave, onDocxSaveAs, onDocxSave, onDocxChange, docxSaver, imageSaver, docxSaversRef, pdfSaversRef, pdfSavePlansRef, imageSaversRef, setDocxMenu, setPdfPrint, setMarkdownExport, onImageChange, onPdfDirtyChange, openImageSibling, registerSearchProviderFor, onPdfViewState, findBarEl, findMode, canReplaceSearch, onChangeView, toggleFind, closeFind, onOpenPath, trackMdxGrant, htmlPropsFor, }) {
|
|
33
|
+
const activePathRef = useRef(activePath);
|
|
34
|
+
const [previousActivePath, setPreviousActivePath] = useState(null);
|
|
35
|
+
const [resizeActive, setResizeActive] = useState(false);
|
|
36
|
+
useLayoutEffect(() => {
|
|
37
|
+
if (activePathRef.current === activePath)
|
|
38
|
+
return;
|
|
39
|
+
setPreviousActivePath(activePathRef.current);
|
|
40
|
+
activePathRef.current = activePath;
|
|
41
|
+
}, [activePath]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
let timer;
|
|
44
|
+
let frame;
|
|
45
|
+
const onResize = () => {
|
|
46
|
+
setResizeActive(true);
|
|
47
|
+
if (timer)
|
|
48
|
+
window.clearTimeout(timer);
|
|
49
|
+
if (frame)
|
|
50
|
+
window.cancelAnimationFrame(frame);
|
|
51
|
+
timer = window.setTimeout(() => {
|
|
52
|
+
frame = window.requestAnimationFrame(() => setResizeActive(false));
|
|
53
|
+
}, 120);
|
|
54
|
+
};
|
|
55
|
+
window.addEventListener("resize", onResize);
|
|
56
|
+
return () => {
|
|
57
|
+
window.removeEventListener("resize", onResize);
|
|
58
|
+
if (timer)
|
|
59
|
+
window.clearTimeout(timer);
|
|
60
|
+
if (frame)
|
|
61
|
+
window.cancelAnimationFrame(frame);
|
|
62
|
+
};
|
|
63
|
+
}, []);
|
|
64
|
+
// During the activation render, the ref still points at the pane that was active in the previous
|
|
65
|
+
// commit. Keep that router live synchronously so a fast switch cannot unmount it before App's
|
|
66
|
+
// after-paint MRU bookkeeping catches up.
|
|
67
|
+
const transitionPreviousPath = activePathRef.current !== activePath ? activePathRef.current : previousActivePath;
|
|
68
|
+
return (_jsx(_Fragment, { children: mountedDocs.map((doc) => {
|
|
69
|
+
const isActive = doc.path === activePath;
|
|
70
|
+
const dView = views[doc.path] ?? "preview";
|
|
71
|
+
const markdownOwnsScroll = doc.format === "md";
|
|
72
|
+
const dTextLike = capabilitiesFor(doc.format).canReplace;
|
|
73
|
+
// Background/restored tabs stay as lightweight shells. Active bypasses deferred bookkeeping:
|
|
74
|
+
// its router mounts in this same commit, so first visit and cold revisit never paint an empty
|
|
75
|
+
// shell. App defers only eviction/cleanup after activation has settled.
|
|
76
|
+
const hasRendered = isActive || renderedPaths.has(doc.path);
|
|
77
|
+
const isLive = isActive ||
|
|
78
|
+
doc.path === transitionPreviousPath ||
|
|
79
|
+
(hasRendered && livePaths.has(doc.path));
|
|
80
|
+
// Keep only the immediately previous pane paint-warm. Warming every rendered document makes
|
|
81
|
+
// each resize reflow every open Markdown/HTML tree & blocks window IPC for seconds.
|
|
82
|
+
const isPaintWarm = !resizeActive && doc.path === transitionPreviousPath;
|
|
83
|
+
return (_jsx("div", { "aria-hidden": isActive ? undefined : true, inert: isActive ? undefined : true, "data-vr-document-path": doc.path, "data-vr-pane-state": isActive ? "active" : isPaintWarm ? "warm" : "cold", "data-vr-router-state": isLive ? "live" : hasRendered ? "shell" : "deferred", id: isActive ? "vr-main-content" : undefined, tabIndex: isActive ? -1 : undefined, ref: isActive && !markdownOwnsScroll ? contentScrollRef : undefined, onScroll: isActive && !markdownOwnsScroll ? onContentScroll : undefined, onWheelCapture: isActive ? onDocumentWheel : undefined, ...(isActive && !markdownOwnsScroll ? { "data-document-scroll": "" } : {}), style: isActive
|
|
84
|
+
? {
|
|
85
|
+
contentVisibility: "visible",
|
|
86
|
+
visibility: "visible",
|
|
87
|
+
opacity: 1,
|
|
88
|
+
willChange: "opacity",
|
|
89
|
+
zIndex: 1,
|
|
90
|
+
}
|
|
91
|
+
: isPaintWarm
|
|
92
|
+
? {
|
|
93
|
+
contentVisibility: "visible",
|
|
94
|
+
visibility: "visible",
|
|
95
|
+
opacity: 0,
|
|
96
|
+
willChange: "opacity",
|
|
97
|
+
zIndex: 0,
|
|
98
|
+
pointerEvents: "none",
|
|
99
|
+
}
|
|
100
|
+
: {
|
|
101
|
+
contentVisibility: "hidden",
|
|
102
|
+
visibility: "hidden",
|
|
103
|
+
zIndex: 0,
|
|
104
|
+
pointerEvents: "none",
|
|
105
|
+
}, className: `absolute inset-0 outline-none ${markdownOwnsScroll ? "overflow-hidden" : "overflow-auto"}`, children: isLive ? (_jsxs(PaneActiveContext.Provider, { value: isActive, children: [dTextLike && doc.lossy ? (_jsxs("div", { className: "mx-4 mt-4 rounded border border-copper/40 bg-copper/5 p-3 font-sans text-xs text-muted", children: ["This file couldn\u2019t be decoded cleanly even after detecting its encoding (", doc.encoding, "); some bytes were shown as replacement characters. Keep a backup if the exact bytes matter."] })) : (dTextLike &&
|
|
106
|
+
doc.encoding &&
|
|
107
|
+
doc.encoding !== "UTF-8" && (_jsxs("div", { className: "mx-4 mt-4 rounded border border-line bg-panel/40 p-3 font-sans text-xs text-muted", children: ["Detected ", _jsx("span", { className: "text-copper-hi", children: doc.encoding }), " encoding.", hostAppName(), " preserves it when you save (it switches to UTF-8 only if you add characters that encoding can\u2019t represent)."] }))), _jsx(LiveDocumentRouter, { doc: doc, active: isActive, view: dView, readEdit: effectiveReadEdit(doc), subscribeEdits: subscribeEdits, getEditSnapshot: getEditSnapshot, getExternalSeq: getExternalSeq, sourceViewStateRef: sourceViewStateRef, onChange: (text) => onTxtChange(doc.path, doc.content, text), onSave: () => onSave(doc), onDocxSave: (buf) => {
|
|
108
|
+
if (isUntitled(doc.path)) {
|
|
109
|
+
void onDocxSaveAs(doc, buf);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
void onDocxSave(doc.path, buf);
|
|
113
|
+
}
|
|
114
|
+
}, onDocxChange: () => onDocxChange(doc.path), registerDocxSave: (fn) => {
|
|
115
|
+
if (fn)
|
|
116
|
+
docxSaversRef.current?.set(doc.path, fn);
|
|
117
|
+
else
|
|
118
|
+
docxSaversRef.current?.delete(doc.path);
|
|
119
|
+
// Compare by path instead of closure-captured isActive so the active-pointer
|
|
120
|
+
// update works regardless of when the viewer mounts (deferred by App.tsx).
|
|
121
|
+
if (doc.path === activePath)
|
|
122
|
+
docxSaver.current = fn;
|
|
123
|
+
}, registerImageSave: (fn) => {
|
|
124
|
+
if (fn)
|
|
125
|
+
imageSaversRef.current?.set(doc.path, fn);
|
|
126
|
+
else
|
|
127
|
+
imageSaversRef.current?.delete(doc.path);
|
|
128
|
+
if (doc.path === activePath)
|
|
129
|
+
imageSaver.current = fn;
|
|
130
|
+
}, onDocxToolbar: isActive ? setDocxMenu : inactiveDocxToolbar, registerPdfPrint: isActive ? setPdfPrint : inactivePdfPrint, registerPdfSave: (fn) => {
|
|
131
|
+
if (fn)
|
|
132
|
+
pdfSaversRef.current?.set(doc.path, fn);
|
|
133
|
+
else
|
|
134
|
+
pdfSaversRef.current?.delete(doc.path);
|
|
135
|
+
}, registerPdfSavePlan: (fn) => {
|
|
136
|
+
if (fn)
|
|
137
|
+
pdfSavePlansRef.current?.set(doc.path, fn);
|
|
138
|
+
else
|
|
139
|
+
pdfSavePlansRef.current?.delete(doc.path);
|
|
140
|
+
}, onPdfDirtyChange: (dirty) => onPdfDirtyChange(doc.path, dirty), registerMarkdownExport: isActive ? setMarkdownExport : inactiveMarkdownExport, markdownScrollRef: isActive && markdownOwnsScroll ? contentScrollRef : undefined, onMarkdownScroll: isActive && markdownOwnsScroll ? onContentScroll : undefined, onImageChange: () => onImageChange(doc.path), onOpenImageSibling: openImageSibling, registerSearchProvider: registerSearchProviderFor(doc.path), pdfInitialView: doc.viewMeta, onPdfViewState: isActive ? onPdfViewState : inactivePdfViewState, findBar: isActive ? findBarEl : undefined, findMode: isActive ? findMode : undefined, onChangeView: onChangeView, onCloseFind: closeFind, onRequestFind: () => toggleFind(false), onRequestReplace: isActive && canReplaceSearch ? () => toggleFind(true) : undefined, onOpenPath: (p, opts) => void onOpenPath(p, opts), onMdxImportGranted: trackMdxGrant, html: doc.format === "html" ? htmlPropsFor(doc.path) : undefined })] })) : null }, doc.tabKey ?? doc.path));
|
|
141
|
+
}) }));
|
|
142
|
+
}
|
|
143
|
+
export const DocumentRouterTable = memo(DocumentRouterTableImpl);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
interface Props {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
label?: string;
|
|
5
|
+
/** The shell boundary clears a shown error when this value changes. */
|
|
6
|
+
resetKey?: string;
|
|
7
|
+
/** Path of the document tab this boundary guards; enables the "Close tab" action. */
|
|
8
|
+
closeTabPath?: string;
|
|
9
|
+
}
|
|
10
|
+
/** the host app crash screen over the shell's boundary (`resetKey`, reset, `onError`). */
|
|
11
|
+
export declare function ErrorBoundary({ children, label, resetKey, closeTabPath }: Props): import("react").JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ErrorBoundary as ShellErrorBoundary } from "@rightkit/react-shell/react";
|
|
3
|
+
import { logError } from "@rightkit/document-core/state/diagnostics";
|
|
4
|
+
import { hostAppName } from "../appName.js";
|
|
5
|
+
/** the host app crash screen over the shell's boundary (`resetKey`, reset, `onError`). */
|
|
6
|
+
export function ErrorBoundary({ children, label, resetKey, closeTabPath }) {
|
|
7
|
+
const name = label ?? hostAppName();
|
|
8
|
+
const report = (error, info) => {
|
|
9
|
+
console.error(`${hostAppName()} render error`, { label, error, info });
|
|
10
|
+
// Static label only — no message, stack or paths — so a crash actually reaches the local
|
|
11
|
+
// diagnostics log instead of vanishing (this boundary previously only reached `console.error`,
|
|
12
|
+
// invisible to anyone not watching devtools at the moment it happened).
|
|
13
|
+
logError("react:boundary");
|
|
14
|
+
};
|
|
15
|
+
return (_jsx(ShellErrorBoundary, { label: name, resetKey: resetKey, onError: report, fallback: (_error, reset) => (_jsx("div", { className: "flex h-full min-h-0 items-center justify-center bg-ink p-6", children: _jsxs("div", { role: "alert", className: "max-w-md rounded border border-line bg-panel-2 p-5 text-sm text-muted shadow-xl", children: [_jsxs("p", { className: "mb-1 font-serif text-lg font-semibold text-paper", children: ["Something went wrong in ", name] }), _jsx("p", { className: "mb-4", children: "This view couldn't be displayed. Your unsaved edits are kept in memory and nothing was written to your file. Reload the view to try again, or close the tab (you will be asked about unsaved changes)." }), _jsx("button", { type: "button", "aria-label": `Reload ${name}`, onClick: reset, className: "vr-press vr-focus rounded bg-copper/20 px-3 py-1.5 font-sans text-chrome-sm text-copper-hi hover:bg-copper/30", children: "Reload" }), closeTabPath ? (_jsx("button", { type: "button", "aria-label": "Close tab", onClick: () => window.dispatchEvent(new CustomEvent("viewright:close-tab", { detail: { path: closeTabPath } })), className: "vr-press vr-focus ml-2 rounded px-3 py-1.5 font-sans text-chrome-sm text-muted hover:bg-ink/40 hover:text-paper", children: "Close tab" })) : null] }) })), children: children }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SearchProvider } from "@rightkit/document-core/documents/types";
|
|
2
|
+
interface FindBarProps {
|
|
3
|
+
provider: SearchProvider | null;
|
|
4
|
+
onClose: () => void;
|
|
5
|
+
initialReplaceMode?: boolean;
|
|
6
|
+
scope?: "current" | "all";
|
|
7
|
+
onScopeChange?: (scope: "current" | "all") => void;
|
|
8
|
+
canCrossDoc?: boolean;
|
|
9
|
+
placement?: "floating" | "toolbar";
|
|
10
|
+
}
|
|
11
|
+
export declare function FindBar({ provider, onClose, initialReplaceMode, scope, onScopeChange, canCrossDoc, placement, }: FindBarProps): import("react").JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { CaretDown } from "@phosphor-icons/react/dist/icons/CaretDown";
|
|
4
|
+
import { CaretUp } from "@phosphor-icons/react/dist/icons/CaretUp";
|
|
5
|
+
import { MagnifyingGlass } from "@phosphor-icons/react/dist/icons/MagnifyingGlass";
|
|
6
|
+
import { Swap } from "@phosphor-icons/react/dist/icons/Swap";
|
|
7
|
+
import { useDismiss } from "@rightkit/platform-ui/headless";
|
|
8
|
+
import { createShortcutRegistry } from "@rightkit/react-shell";
|
|
9
|
+
import { isValidSearchRegex } from "@rightkit/document-core/documents/searchUtils";
|
|
10
|
+
const EMPTY = { count: 0, activeIndex: -1 };
|
|
11
|
+
// Session-only recent-query history (no localStorage — nothing typed into find outlives the session).
|
|
12
|
+
const findHistory = [];
|
|
13
|
+
function rememberQuery(query) {
|
|
14
|
+
const value = query.trim();
|
|
15
|
+
if (!value)
|
|
16
|
+
return;
|
|
17
|
+
const at = findHistory.indexOf(value);
|
|
18
|
+
if (at >= 0)
|
|
19
|
+
findHistory.splice(at, 1);
|
|
20
|
+
findHistory.unshift(value);
|
|
21
|
+
if (findHistory.length > 12)
|
|
22
|
+
findHistory.length = 12;
|
|
23
|
+
}
|
|
24
|
+
function PrevIcon() {
|
|
25
|
+
return _jsx(CaretUp, { size: 14, weight: "regular", "aria-hidden": "true" });
|
|
26
|
+
}
|
|
27
|
+
function NextIcon() {
|
|
28
|
+
return _jsx(CaretDown, { size: 14, weight: "regular", "aria-hidden": "true" });
|
|
29
|
+
}
|
|
30
|
+
export function FindBar({ provider, onClose, initialReplaceMode = false, scope = "current", onScopeChange, canCrossDoc = false, placement = "floating", }) {
|
|
31
|
+
const [query, setQuery] = useState("");
|
|
32
|
+
const [replaceText, setReplaceText] = useState("");
|
|
33
|
+
const [result, setResult] = useState(EMPTY); // provider-owned count + activeIndex
|
|
34
|
+
const [showReplace, setShowReplace] = useState(initialReplaceMode);
|
|
35
|
+
const [matchCase, setMatchCase] = useState(false);
|
|
36
|
+
const [wholeWord, setWholeWord] = useState(false);
|
|
37
|
+
const [regex, setRegex] = useState(false);
|
|
38
|
+
const queryInputRef = useRef(null);
|
|
39
|
+
const replaceInputRef = useRef(null);
|
|
40
|
+
const canReplace = !!provider?.canReplace;
|
|
41
|
+
const showCaseToggle = !!provider?.supportsMatchCase;
|
|
42
|
+
const showWordToggle = !!provider?.supportsWholeWord;
|
|
43
|
+
const showRegexToggle = !!provider?.supportsRegex;
|
|
44
|
+
const regexActive = regex && showRegexToggle;
|
|
45
|
+
const regexInvalid = regexActive && query !== "" && !isValidSearchRegex(query);
|
|
46
|
+
// Focus + select the query field the moment the bar opens, so keystrokes after Ctrl+F land in the
|
|
47
|
+
// search box, not the document body. useLayoutEffect (not useEffect) runs synchronously before
|
|
48
|
+
// paint — ahead of editors that re-grab focus via requestAnimationFrame on mount (the CodeMirror
|
|
49
|
+
// source editor's autofocus, ProseMirror). The rAF re-assert reclaims focus if such a competitor's
|
|
50
|
+
// callback still fires after us; it only acts when focus actually drifted off the input.
|
|
51
|
+
useLayoutEffect(() => {
|
|
52
|
+
const input = queryInputRef.current;
|
|
53
|
+
if (!input)
|
|
54
|
+
return;
|
|
55
|
+
input.focus();
|
|
56
|
+
input.select();
|
|
57
|
+
const raf = requestAnimationFrame(() => {
|
|
58
|
+
if (queryInputRef.current === input && document.activeElement !== input) {
|
|
59
|
+
input.focus();
|
|
60
|
+
input.select();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return () => cancelAnimationFrame(raf);
|
|
64
|
+
}, []);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
setShowReplace(initialReplaceMode && !!provider?.canReplace);
|
|
67
|
+
}, [initialReplaceMode, provider]);
|
|
68
|
+
// Escape closes the bar. Bubble phase on the window, like the handler it replaces, so a modal
|
|
69
|
+
// that takes Escape in the capture phase (the Link dialog) keeps Find open beneath it.
|
|
70
|
+
const windowTarget = useRef(typeof window === "undefined" ? null : window);
|
|
71
|
+
useDismiss(() => onClose(), {
|
|
72
|
+
escapeTarget: windowTarget,
|
|
73
|
+
outside: false,
|
|
74
|
+
preventDefault: true,
|
|
75
|
+
stopPropagation: false,
|
|
76
|
+
imeGuard: false,
|
|
77
|
+
});
|
|
78
|
+
// Alt+R toggles regex through a registry scoped to this bar: registered while it is open,
|
|
79
|
+
// dispatching in the bubble phase on the window like the matcher it replaces.
|
|
80
|
+
const regexToggleAvailableRef = useRef(showRegexToggle);
|
|
81
|
+
regexToggleAvailableRef.current = showRegexToggle;
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
const registry = createShortcutRegistry();
|
|
84
|
+
registry.register({
|
|
85
|
+
id: "find.toggle-regex",
|
|
86
|
+
label: "Toggle regular expression",
|
|
87
|
+
chord: "alt+r",
|
|
88
|
+
hidden: true,
|
|
89
|
+
when: () => regexToggleAvailableRef.current,
|
|
90
|
+
allowDefaultPrevented: true,
|
|
91
|
+
allowComposing: true,
|
|
92
|
+
run: () => setRegex((v) => !v),
|
|
93
|
+
});
|
|
94
|
+
return registry.attach(window);
|
|
95
|
+
}, []);
|
|
96
|
+
// Run find when the query or the options change. Provider returns the authoritative
|
|
97
|
+
// {count, activeIndex}; we never compute the active index locally. Non-empty queries are
|
|
98
|
+
// debounced so large docs (and PDF page extraction / DOCX scans) don't run on every keystroke.
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!provider)
|
|
101
|
+
return;
|
|
102
|
+
let current = true;
|
|
103
|
+
const opts = { matchCase, wholeWord, regex: regexActive };
|
|
104
|
+
const run = async () => {
|
|
105
|
+
// An invalid pattern matches nothing: clear provider highlights via an empty query.
|
|
106
|
+
const res = regexActive && query && !isValidSearchRegex(query)
|
|
107
|
+
? await provider.find("", opts)
|
|
108
|
+
: await provider.find(query, opts);
|
|
109
|
+
if (current)
|
|
110
|
+
setResult(query ? res : EMPTY);
|
|
111
|
+
};
|
|
112
|
+
if (!query) {
|
|
113
|
+
void run();
|
|
114
|
+
return () => {
|
|
115
|
+
current = false;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const t = setTimeout(run, 180);
|
|
119
|
+
return () => {
|
|
120
|
+
current = false;
|
|
121
|
+
clearTimeout(t);
|
|
122
|
+
};
|
|
123
|
+
}, [query, provider, matchCase, wholeWord, regexActive]);
|
|
124
|
+
// Providers can change their match set without a FindBar action (document undo/redo, external
|
|
125
|
+
// reconciliation). Keep displayed provider-owned count/index synchronized with those changes.
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (!provider?.subscribe)
|
|
128
|
+
return;
|
|
129
|
+
return provider.subscribe((providerQuery, next) => {
|
|
130
|
+
if (providerQuery === query)
|
|
131
|
+
setResult(query ? next : EMPTY);
|
|
132
|
+
});
|
|
133
|
+
}, [provider, query]);
|
|
134
|
+
const step = async (dir) => {
|
|
135
|
+
if (!provider || result.count === 0)
|
|
136
|
+
return;
|
|
137
|
+
setResult(await (dir === "next" ? provider.next() : provider.prev()));
|
|
138
|
+
};
|
|
139
|
+
// F3 / Shift+F3 (dispatched by the global shortcut when the bar is already open) step the match
|
|
140
|
+
// through the bar itself, so its displayed count/index stays the source of truth. The listener is
|
|
141
|
+
// registered ONCE and reads the latest `step` via a ref (avoids re-subscribing every render).
|
|
142
|
+
const stepRef = useRef(step);
|
|
143
|
+
stepRef.current = step;
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
const onStep = (e) => {
|
|
146
|
+
const dir = e.detail?.dir;
|
|
147
|
+
void stepRef.current(dir && dir < 0 ? "prev" : "next");
|
|
148
|
+
};
|
|
149
|
+
window.addEventListener("viewright:find-step", onStep);
|
|
150
|
+
return () => window.removeEventListener("viewright:find-step", onStep);
|
|
151
|
+
}, []);
|
|
152
|
+
const doReplace = async () => {
|
|
153
|
+
if (!provider?.replace)
|
|
154
|
+
return;
|
|
155
|
+
setResult(await provider.replace(replaceText));
|
|
156
|
+
};
|
|
157
|
+
const doReplaceAll = async () => {
|
|
158
|
+
if (!provider?.replaceAll)
|
|
159
|
+
return;
|
|
160
|
+
setResult(await provider.replaceAll(replaceText));
|
|
161
|
+
};
|
|
162
|
+
const resultStatus = regexInvalid
|
|
163
|
+
? "Invalid regular expression"
|
|
164
|
+
: result.count > 0
|
|
165
|
+
? `Match ${result.activeIndex + 1} of ${result.count}`
|
|
166
|
+
: query.trim()
|
|
167
|
+
? "No matches"
|
|
168
|
+
: "No search results";
|
|
169
|
+
const handleQueryKeyDown = (e) => {
|
|
170
|
+
if (e.key === "Enter") {
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
rememberQuery(query);
|
|
173
|
+
void step(e.shiftKey ? "prev" : "next");
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const handleReplaceKeyDown = (e) => {
|
|
177
|
+
if (e.key === "Enter") {
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
void doReplace();
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
// Shared inner field: rounded capsule on the recessed ink surface, copper hairline while any
|
|
183
|
+
// control inside is focused (the inputs themselves stay outline-free).
|
|
184
|
+
const fieldCls = "flex h-8 min-w-[120px] flex-1 basis-[160px] items-center gap-2 rounded-full border border-transparent bg-ink/70 pl-3 pr-2 transition-colors focus-within:border-copper/60";
|
|
185
|
+
const inputCls = "min-w-0 flex-1 bg-transparent text-chrome-md text-paper outline-none placeholder:text-muted-2";
|
|
186
|
+
const iconBtnCls = (on, disabled = false) => `vr-press vr-focus flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-chrome-xs ${on
|
|
187
|
+
? "bg-copper/[0.14] text-copper-hi"
|
|
188
|
+
: disabled
|
|
189
|
+
? "text-vr-disabled"
|
|
190
|
+
: "text-muted hover:bg-ink/40 hover:text-paper"}`;
|
|
191
|
+
const pillBtnCls = "vr-press vr-focus flex h-8 shrink-0 items-center justify-center rounded-full px-3 text-chrome-sm font-medium";
|
|
192
|
+
return (_jsxs("div", { "data-vr-find-placement": placement, role: "search", "aria-label": "Find in document", className: "no-print relative flex w-full max-w-full flex-col gap-2 font-sans text-chrome-md select-none", children: [_jsxs("div", { className: "vr-capsule min-h-11 flex-wrap gap-1 px-1.5 py-1", children: [_jsxs("div", { className: `${fieldCls}${regexInvalid ? " !border-red-500/70" : ""}`, children: [_jsx(MagnifyingGlass, { size: 14, weight: "regular", "aria-hidden": "true", className: "shrink-0 text-muted-2" }), _jsx("input", { ref: queryInputRef, type: "text", value: query, onChange: (e) => setQuery(e.target.value), onKeyDown: handleQueryKeyDown, "aria-label": "Find text", "aria-invalid": regexInvalid, placeholder: "Find...", list: "vr-find-history", className: inputCls }), _jsx("datalist", { id: "vr-find-history", children: findHistory.map((q) => (_jsx("option", { value: q }, q))) }), _jsx("span", { className: "shrink-0 whitespace-nowrap pl-1 text-chrome-xs text-muted tabular-nums", children: regexInvalid
|
|
193
|
+
? "Invalid"
|
|
194
|
+
: result.count > 0
|
|
195
|
+
? `${result.activeIndex + 1} / ${result.count}`
|
|
196
|
+
: "0 / 0" })] }), (showCaseToggle || showWordToggle || showRegexToggle) && (_jsxs("div", { className: "flex shrink-0 items-center gap-0.5", children: [showCaseToggle && (_jsx("button", { type: "button", onClick: () => setMatchCase((v) => !v), "aria-label": "Match case", "aria-pressed": matchCase, className: iconBtnCls(matchCase), title: "Match case", children: "Aa" })), showWordToggle && (_jsx("button", { type: "button", onClick: () => setWholeWord((v) => !v), "aria-label": "Whole word", "aria-pressed": wholeWord, className: iconBtnCls(wholeWord), title: "Whole word", children: "W" })), showRegexToggle && (_jsx("button", { type: "button", onClick: () => setRegex((v) => !v), "aria-label": "Regex", "aria-pressed": regex, "aria-keyshortcuts": "Alt+R", className: iconBtnCls(regex), title: "Regular expression (Alt+R)", children: ".*" }))] })), canCrossDoc && onScopeChange && (_jsx("button", { type: "button", onClick: () => onScopeChange(scope === "current" ? "all" : "current"), "aria-label": scope === "all" ? "Searching all tabs" : "Search this tab only", "aria-pressed": scope === "all", className: `${iconBtnCls(scope === "all")} w-auto px-2 text-chrome-xs`, title: scope === "all"
|
|
197
|
+
? "Searching all tabs — click to limit to this tab"
|
|
198
|
+
: "Search this tab only — click to search all tabs", children: scope === "all" ? "all" : "1" })), _jsx("button", { type: "button", onClick: () => void step("prev"), disabled: result.count === 0, "aria-label": "Previous match", title: "Previous match", className: iconBtnCls(false, result.count === 0), children: _jsx(PrevIcon, {}) }), _jsx("button", { type: "button", onClick: () => void step("next"), disabled: result.count === 0, "aria-label": "Next match", title: "Next match", className: iconBtnCls(false, result.count === 0), children: _jsx(NextIcon, {}) }), canReplace && (_jsx("button", { type: "button", onClick: () => setShowReplace((v) => !v), "aria-pressed": showReplace, "aria-expanded": showReplace, title: showReplace ? "Hide replace field" : "Show replace field", className: `${pillBtnCls} ${showReplace ? "bg-copper/[0.14] text-copper-hi" : "text-muted hover:bg-ink/40 hover:text-paper"}`, children: "Replace" })), _jsx("button", { type: "button", onClick: onClose, title: "Close find", className: `${pillBtnCls} text-paper/85 hover:bg-ink/40 hover:text-paper`, children: "Done" })] }), showReplace && canReplace && (_jsxs("div", { "data-vr-find-replace-row": "", className: "vr-capsule min-h-11 flex-wrap gap-1 px-1.5 py-1", children: [_jsxs("div", { className: fieldCls, children: [_jsx(Swap, { size: 14, weight: "regular", "aria-hidden": "true", className: "shrink-0 text-muted-2" }), _jsx("input", { ref: replaceInputRef, type: "text", value: replaceText, onChange: (e) => setReplaceText(e.target.value), onKeyDown: handleReplaceKeyDown, "aria-label": "Replace with", placeholder: "Replace with...", className: inputCls })] }), _jsx("button", { type: "button", onClick: () => void doReplace(), disabled: result.count === 0, title: "Replace current match", className: `${pillBtnCls} text-muted hover:bg-ink/40 hover:text-paper disabled:text-vr-disabled disabled:hover:bg-transparent`, children: "Replace" }), _jsx("button", { type: "button", onClick: () => void doReplaceAll(), disabled: result.count === 0, title: "Replace all matches", className: `${pillBtnCls} bg-copper text-white hover:bg-copper/90 disabled:bg-panel-2 disabled:text-vr-disabled`, children: "All" })] })), _jsx("span", { className: "sr-only", role: "status", "aria-live": "polite", "aria-atomic": "true", children: resultStatus })] }));
|
|
199
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { LoadedDocument } from "@rightkit/document-core/documents/types";
|
|
2
|
+
/**
|
|
3
|
+
* A document title that stays live as you type — for untitled buffers whose name is derived from their
|
|
4
|
+
* first line. It subscribes to its OWN doc's edit slice (via the edits store), so it re-renders on a
|
|
5
|
+
* keystroke without re-rendering the memoized Sidebar/Toolbar around it. `compute` is the host's title
|
|
6
|
+
* function (Sidebar's `titleFor`, the Toolbar's titlebar variant); it reads the same editsRef the
|
|
7
|
+
* subscription tracks, so calling it after the subscription fires yields the fresh title.
|
|
8
|
+
*/
|
|
9
|
+
export declare function LiveTitle({ doc, compute, subscribeEdits, getEditSnapshot, }: {
|
|
10
|
+
doc: LoadedDocument;
|
|
11
|
+
compute: (doc: LoadedDocument) => string;
|
|
12
|
+
subscribeEdits: (path: string, cb: () => void) => () => void;
|
|
13
|
+
getEditSnapshot: (path: string) => string | undefined;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEditText } from "@rightkit/document-core/app/useEditText";
|
|
3
|
+
/**
|
|
4
|
+
* A document title that stays live as you type — for untitled buffers whose name is derived from their
|
|
5
|
+
* first line. It subscribes to its OWN doc's edit slice (via the edits store), so it re-renders on a
|
|
6
|
+
* keystroke without re-rendering the memoized Sidebar/Toolbar around it. `compute` is the host's title
|
|
7
|
+
* function (Sidebar's `titleFor`, the Toolbar's titlebar variant); it reads the same editsRef the
|
|
8
|
+
* subscription tracks, so calling it after the subscription fires yields the fresh title.
|
|
9
|
+
*/
|
|
10
|
+
export function LiveTitle({ doc, compute, subscribeEdits, getEditSnapshot, }) {
|
|
11
|
+
// Subscribe for the re-render; the value itself is unused (compute reads the live ref).
|
|
12
|
+
useEditText(subscribeEdits, getEditSnapshot, doc.path);
|
|
13
|
+
return _jsx(_Fragment, { children: compute(doc) });
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Delay before the indicator appears, so fast opens never flash a progress bar. */
|
|
2
|
+
export declare const OPENING_PROGRESS_DELAY_MS = 300;
|
|
3
|
+
interface OpeningProgressProps {
|
|
4
|
+
fileName: string;
|
|
5
|
+
delayMs?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Progress indicator for a document load that is still running. Mounted only while a load is in
|
|
9
|
+
* flight; it stays hidden until the load has lasted `delayMs`, then shows an indeterminate bar.
|
|
10
|
+
* The backend read reports no byte counts, so the bar makes no percentage claim.
|
|
11
|
+
*/
|
|
12
|
+
export declare function OpeningProgress({ fileName, delayMs }: OpeningProgressProps): import("react").JSX.Element | null;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
/** Delay before the indicator appears, so fast opens never flash a progress bar. */
|
|
4
|
+
export const OPENING_PROGRESS_DELAY_MS = 300;
|
|
5
|
+
/**
|
|
6
|
+
* Progress indicator for a document load that is still running. Mounted only while a load is in
|
|
7
|
+
* flight; it stays hidden until the load has lasted `delayMs`, then shows an indeterminate bar.
|
|
8
|
+
* The backend read reports no byte counts, so the bar makes no percentage claim.
|
|
9
|
+
*/
|
|
10
|
+
export function OpeningProgress({ fileName, delayMs = OPENING_PROGRESS_DELAY_MS }) {
|
|
11
|
+
const [visible, setVisible] = useState(false);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
setVisible(false);
|
|
14
|
+
const timer = window.setTimeout(() => setVisible(true), delayMs);
|
|
15
|
+
return () => window.clearTimeout(timer);
|
|
16
|
+
}, [delayMs]);
|
|
17
|
+
if (!visible)
|
|
18
|
+
return null;
|
|
19
|
+
const label = `Opening ${fileName || "document"}`;
|
|
20
|
+
return (_jsxs("div", { role: "status", "aria-live": "polite", className: "pointer-events-none absolute inset-x-0 top-0 z-50 flex flex-col gap-1 border-b border-line bg-panel/95 px-4 py-2 font-mono text-xs text-muted shadow-sm", children: [_jsxs("span", { children: [label, "\u2026"] }), _jsx("div", { role: "progressbar", "aria-label": label, "aria-busy": "true", className: "relative h-1 w-full overflow-hidden rounded bg-line", children: _jsx("div", { className: "absolute inset-y-0 left-0 w-1/3 animate-pulse rounded bg-copper" }) })] }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { useEffect, useState } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { getCurrentWindow } from "@tauri-apps/api/window";
|
|
5
|
+
import { printCurrentWebview } from "@rightkit/document-core/state/tauriBridge";
|
|
6
|
+
import { takePrintPreviewPayload } from "@rightkit/document-core/app/nativePrintPreview";
|
|
7
|
+
export function isPrintPreviewWindow() {
|
|
8
|
+
return new URLSearchParams(window.location.search).has("viewright-print-preview");
|
|
9
|
+
}
|
|
10
|
+
function PrintPreviewWindow() {
|
|
11
|
+
const [error, setError] = useState(null);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
let disposed = false;
|
|
14
|
+
const currentWindow = getCurrentWindow();
|
|
15
|
+
const close = () => void currentWindow.close();
|
|
16
|
+
window.addEventListener("afterprint", close, { once: true });
|
|
17
|
+
void (async () => {
|
|
18
|
+
try {
|
|
19
|
+
const payload = await takePrintPreviewPayload();
|
|
20
|
+
if (disposed)
|
|
21
|
+
return;
|
|
22
|
+
const host = document.createElement("div");
|
|
23
|
+
host.dataset.vrPrintHost = "";
|
|
24
|
+
host.innerHTML = payload.html;
|
|
25
|
+
if (payload.shadowHtml !== null) {
|
|
26
|
+
host.attachShadow({ mode: "open" }).innerHTML = payload.shadowHtml;
|
|
27
|
+
}
|
|
28
|
+
document.body.appendChild(host);
|
|
29
|
+
document.body.setAttribute("data-vr-print-active", "");
|
|
30
|
+
await currentWindow.show();
|
|
31
|
+
await currentWindow.setFocus();
|
|
32
|
+
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
33
|
+
await printCurrentWebview();
|
|
34
|
+
}
|
|
35
|
+
catch (cause) {
|
|
36
|
+
if (!disposed)
|
|
37
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
38
|
+
await currentWindow.show();
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
41
|
+
return () => {
|
|
42
|
+
disposed = true;
|
|
43
|
+
window.removeEventListener("afterprint", close);
|
|
44
|
+
};
|
|
45
|
+
}, []);
|
|
46
|
+
return (_jsx("main", { style: {
|
|
47
|
+
minHeight: "100vh",
|
|
48
|
+
display: "grid",
|
|
49
|
+
placeItems: "center",
|
|
50
|
+
background: "#11110f",
|
|
51
|
+
color: "#f2efe8",
|
|
52
|
+
font: "600 15px/1.5 sans-serif",
|
|
53
|
+
}, children: error ? `Print preview failed: ${error}` : "Preparing print preview..." }));
|
|
54
|
+
}
|
|
55
|
+
export function mountPrintPreviewWindow(root) {
|
|
56
|
+
createRoot(root).render(_jsx(React.StrictMode, { children: _jsx(PrintPreviewWindow, {}) }));
|
|
57
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
3
|
+
import { createShortcutRegistry } from "@rightkit/react-shell";
|
|
4
|
+
import { ShortcutHelp as ShellShortcutHelp } from "@rightkit/react-shell/react";
|
|
5
|
+
// Single source of truth for the discoverable shortcuts. Mirrors useGlobalShortcuts + the source
|
|
6
|
+
// editor keymap so the overlay can't silently drift from the real bindings.
|
|
7
|
+
//
|
|
8
|
+
// Combos are authored in Windows vocabulary and rendered per-OS (workspace rule: one chord, per-OS
|
|
9
|
+
// presentation). The handler itself accepts Ctrl OR Cmd everywhere, so this is purely what we SHOW
|
|
10
|
+
// — but showing "Ctrl" to a Mac user is still wrong, because nobody reaches for it there.
|
|
11
|
+
// Combos are authored as space-separated Windows vocabulary; convert to a canonical chord and let
|
|
12
|
+
// RightKit render it for the current platform.
|
|
13
|
+
function comboChord(combo) {
|
|
14
|
+
return combo
|
|
15
|
+
.split(" ")
|
|
16
|
+
.map((key) => (key === "Ctrl" ? "mod" : key.toLowerCase()))
|
|
17
|
+
.join("+");
|
|
18
|
+
}
|
|
19
|
+
const GROUPS = [
|
|
20
|
+
{
|
|
21
|
+
title: "Files & tabs",
|
|
22
|
+
items: [
|
|
23
|
+
["Ctrl N", "New text document"],
|
|
24
|
+
["Ctrl T", "New tab"],
|
|
25
|
+
["Ctrl O", "Open file…"],
|
|
26
|
+
["Ctrl W", "Close tab"],
|
|
27
|
+
["Ctrl Shift T", "Reopen closed tab"],
|
|
28
|
+
["Ctrl Tab", "Next tab"],
|
|
29
|
+
["Ctrl Shift Tab", "Previous tab"],
|
|
30
|
+
["Ctrl 1–9", "Jump to tab (9 = last)"],
|
|
31
|
+
["Ctrl Shift A", "Show all tabs"],
|
|
32
|
+
["Ctrl E", "Toggle sidebar"],
|
|
33
|
+
["Ctrl \\", "Toggle sidebar"],
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
title: "Edit & save",
|
|
38
|
+
items: [
|
|
39
|
+
["Ctrl S", "Save"],
|
|
40
|
+
["Ctrl Shift S", "Save as…"],
|
|
41
|
+
["Ctrl P", "Print"],
|
|
42
|
+
["Ctrl F", "Find"],
|
|
43
|
+
["F3", "Find next"],
|
|
44
|
+
["Shift F3", "Find previous"],
|
|
45
|
+
["Ctrl H", "Replace"],
|
|
46
|
+
["Alt Z", "Toggle word wrap"],
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
title: "View",
|
|
51
|
+
items: [
|
|
52
|
+
["Ctrl 0", "Reset font size"],
|
|
53
|
+
["Ctrl Scroll", "Zoom document"],
|
|
54
|
+
["Ctrl Shift E", "Toggle Read / Edit (Markup)"],
|
|
55
|
+
["Ctrl Shift F", "Distraction-free reading"],
|
|
56
|
+
["Ctrl Shift P", "Command palette"],
|
|
57
|
+
["?", "This help"],
|
|
58
|
+
["Esc", "Close find / dialog / help"],
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
title: "Inspect",
|
|
63
|
+
items: [
|
|
64
|
+
// Authored as Ctrl+Alt+I so it renders as ⌘ ⌥ I on macOS — the chord every Mac browser uses.
|
|
65
|
+
// The handler also accepts Ctrl+Shift+I (the Windows/Linux convention). Preview frames own
|
|
66
|
+
// both inspect paths; the host app surrounding shell always consumes them without opening.
|
|
67
|
+
["Ctrl Alt I", "Open HTML devtools"],
|
|
68
|
+
["F12", "Open HTML devtools"],
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
/** A display-only registry: the real bindings live in useGlobalShortcuts, so `run` does nothing. */
|
|
73
|
+
function helpRegistry() {
|
|
74
|
+
const registry = createShortcutRegistry();
|
|
75
|
+
for (const group of GROUPS) {
|
|
76
|
+
for (const [combo, label] of group.items) {
|
|
77
|
+
registry.register({ id: `help:${combo}`, label, group: group.title, chord: comboChord(combo), run: () => { } });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return registry;
|
|
81
|
+
}
|
|
82
|
+
const SLOTS = {
|
|
83
|
+
root: "vr-pop relative max-h-full w-full max-w-2xl overflow-auto rounded-xl border border-line bg-panel p-5 shadow-2xl",
|
|
84
|
+
title: "font-serif text-lg font-semibold text-paper",
|
|
85
|
+
grid: "grid grid-cols-1 gap-x-8 gap-y-5 sm:grid-cols-2",
|
|
86
|
+
section: "[&_h3]:mb-2 [&_h3]:font-sans [&_h3]:text-chrome-xs [&_h3]:uppercase [&_h3]:tracking-[0.2em] [&_h3]:text-muted-2 [&_ul]:space-y-1.5",
|
|
87
|
+
item: "gap-3 text-sm text-paper/85 [&>span:first-child]:min-w-0 [&>span:first-child]:truncate",
|
|
88
|
+
keys: "shrink-0 gap-1 [&_.rightkit-kbd-chord]:gap-1 [&_kbd]:rounded [&_kbd]:border [&_kbd]:border-line [&_kbd]:bg-ink/60 [&_kbd]:px-1.5 [&_kbd]:py-0.5 [&_kbd]:font-mono [&_kbd]:text-chrome-xs [&_kbd]:leading-none [&_kbd]:text-paper",
|
|
89
|
+
};
|
|
90
|
+
export function ShortcutHelp({ onClose }) {
|
|
91
|
+
const registry = useMemo(helpRegistry, []);
|
|
92
|
+
const host = useRef(null);
|
|
93
|
+
// Journeys address this dialog as [role="dialog"][aria-label="Keyboard shortcuts"]; the shell
|
|
94
|
+
// names it with aria-labelledby only, so mirror the same text as aria-label.
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
host.current?.querySelector('[role="dialog"]')?.setAttribute("aria-label", "Keyboard shortcuts");
|
|
97
|
+
}, []);
|
|
98
|
+
return (_jsx("div", { ref: host, className: "contents", children: _jsx(ShellShortcutHelp, { registry: registry, onClose: onClose, title: "Keyboard shortcuts", slots: SLOTS }) }));
|
|
99
|
+
}
|