@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,104 @@
|
|
|
1
|
+
import { documentHasPinnedState, formatIsIntrinsicallyHeavy, heavyResourcesForDocument, } from "@rightkit/document-core/app/resourceCensus";
|
|
2
|
+
/** Maximum number of ordinary document routers kept paint-warm (DocumentRouterTable's own limit). */
|
|
3
|
+
export const ORDINARY_LIVE_PANE_LIMIT = 2;
|
|
4
|
+
// Original policy (protected ∪ active ∪ previousActive) cold-remounted every tab on a A→B→C→A cycle,
|
|
5
|
+
// which reads as a bug in ordinary use even though it was the deliberate stress-test-tuned plateau from
|
|
6
|
+
// 553d6b55. An MRU window keeps recently-visited tabs mounted (not paint-warm — see DocumentRouterTable's
|
|
7
|
+
// separate, still-tight isPaintWarm) so returning to a tab you left seconds ago doesn't cold-mount it.
|
|
8
|
+
// 8 is generous headroom above the 2-pane paint-warm limit while still bounding a 50-tab stress session.
|
|
9
|
+
//
|
|
10
|
+
// The window is a resource BUDGET, not a slot count: a document currently holding a heavy runtime
|
|
11
|
+
// (docx-editor, pdf-document, html-iframe, image-viewer, …) consumes more of it than a light or
|
|
12
|
+
// medium one, so heavyweight background panes shell sooner than lightweight ones at the same MRU
|
|
13
|
+
// rank. This makes the limit resource-aware without lowering it — VR-P0-4.
|
|
14
|
+
//
|
|
15
|
+
// Measured 2026-10-11 by `runTabLivenessWindow` (scripts/qa-resources-e2e.mjs, receipt
|
|
16
|
+
// `memoryPerTab`; hidden Chrome, JS heap after a forced collection, plain-text tabs only): a live
|
|
17
|
+
// text tab costs about 0.24 MB of heap and a shelled one about 0.03 MB, so 50 tabs with 8 live sit
|
|
18
|
+
// at 11.1 MB against 8.0 MB for one tab. Heavy formats (PDF, DOCX, HTML frames) and native WebView
|
|
19
|
+
// memory are not in that figure; they are why the budget is weighted rather than raised.
|
|
20
|
+
export const LIVE_TAB_LIMIT = 8;
|
|
21
|
+
/** Heavy documents consume more of the MRU budget than light/medium ones, so they age out sooner. */
|
|
22
|
+
const HEAVY_CANDIDATE_WEIGHT = 2;
|
|
23
|
+
const ORDINARY_CANDIDATE_WEIGHT = 1;
|
|
24
|
+
/**
|
|
25
|
+
* A candidate's cost is the max of two independent signals:
|
|
26
|
+
* - intrinsic: a static fact about the document's FORMAT (html/docx/pdf/image always allocate a
|
|
27
|
+
* heavy resource while mounted), true whether or not the pane is currently mounted at all;
|
|
28
|
+
* - resident: what the census currently has allocated for this path, which is a live-only signal
|
|
29
|
+
* that goes to zero the moment a shelled pane releases its resources.
|
|
30
|
+
* Taking the max means resident information can only ever RAISE the cost above the format's floor
|
|
31
|
+
* (e.g. a doc holding more than one heavy resource kind at once), never lower a known-heavy format
|
|
32
|
+
* to ordinary weight just because it is not currently resident — see VR-P0-4 follow-up.
|
|
33
|
+
*/
|
|
34
|
+
function candidateWeight(path, format) {
|
|
35
|
+
const intrinsic = formatIsIntrinsicallyHeavy(format)
|
|
36
|
+
? HEAVY_CANDIDATE_WEIGHT
|
|
37
|
+
: ORDINARY_CANDIDATE_WEIGHT;
|
|
38
|
+
const resident = heavyResourcesForDocument(path) > 0 ? HEAVY_CANDIDATE_WEIGHT : ORDINARY_CANDIDATE_WEIGHT;
|
|
39
|
+
return Math.max(intrinsic, resident);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Dirty text sessions may leave the live set only after their authoritative source (or a
|
|
43
|
+
* structured draft snapshot) is owned above the router. Binary editors deliberately never get
|
|
44
|
+
* this escape hatch: their mounted runtime remains their only authoritative dirty state.
|
|
45
|
+
*/
|
|
46
|
+
export function dirtyPaneMayShell(format, hasExternalSourceOrDraftOwner) {
|
|
47
|
+
return (hasExternalSourceOrDraftOwner && (format === "txt" || format === "md" || format === "html"));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Active + previous-active + a resource-weighted MRU window of recently-active panes stay live.
|
|
51
|
+
* `protectedPaths` protection is deliberately additive and unbounded: a draft or pending save
|
|
52
|
+
* cannot be evicted merely because another tab became active or aged out of the MRU window.
|
|
53
|
+
*
|
|
54
|
+
* Beyond that, every candidate is checked against the resource census before it can age out:
|
|
55
|
+
* a document reporting pinned state (dirty with no external owner — `documentHasPinnedState`)
|
|
56
|
+
* is kept live no matter its MRU rank or weight, full stop — there is no limit or heuristic that
|
|
57
|
+
* overrides this. A document whose state is dirty-but-externalized, or simply clean, competes for
|
|
58
|
+
* the MRU budget like any other, weighted by `candidateWeight` so heavyweight panes shell out
|
|
59
|
+
* before lightweight ones at the same recency — and stay counted as heavyweight even once shelled,
|
|
60
|
+
* via `formatByPath`'s intrinsic-cost floor (see `candidateWeight`'s own doc comment).
|
|
61
|
+
*/
|
|
62
|
+
export function liveDocumentPaths({ activePath, previousActivePath, protectedPaths, recentPaths = [], formatByPath, }) {
|
|
63
|
+
const paths = new Set(protectedPaths);
|
|
64
|
+
let budget = LIVE_TAB_LIMIT;
|
|
65
|
+
const weightOf = (path) => candidateWeight(path, formatByPath?.get(path));
|
|
66
|
+
// Active and previous-active are unconditionally live, but they still spend from the same
|
|
67
|
+
// shared budget as everything else — otherwise the window would exceed LIVE_TAB_LIMIT whenever
|
|
68
|
+
// they also show up in recentPaths (which they normally do).
|
|
69
|
+
for (const path of [activePath, previousActivePath]) {
|
|
70
|
+
if (!path || paths.has(path))
|
|
71
|
+
continue;
|
|
72
|
+
paths.add(path);
|
|
73
|
+
budget = Math.max(0, budget - weightOf(path));
|
|
74
|
+
}
|
|
75
|
+
for (const path of recentPaths) {
|
|
76
|
+
if (paths.has(path))
|
|
77
|
+
continue;
|
|
78
|
+
if (documentHasPinnedState(path)) {
|
|
79
|
+
// Never evict a pane with pinned state. No heuristic, no limit, no exception.
|
|
80
|
+
paths.add(path);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const weight = weightOf(path);
|
|
84
|
+
if (weight > budget)
|
|
85
|
+
continue;
|
|
86
|
+
paths.add(path);
|
|
87
|
+
budget -= weight;
|
|
88
|
+
}
|
|
89
|
+
return paths;
|
|
90
|
+
}
|
|
91
|
+
/** Deterministic observability for stress tests & diagnostics; never reads wall-clock time. */
|
|
92
|
+
export function tabMountCounters(documentPaths, livePaths, protectedPaths) {
|
|
93
|
+
let protectedLiveCount = 0;
|
|
94
|
+
for (const path of documentPaths)
|
|
95
|
+
if (livePaths.has(path) && protectedPaths.has(path))
|
|
96
|
+
protectedLiveCount++;
|
|
97
|
+
const liveCount = documentPaths.filter((path) => livePaths.has(path)).length;
|
|
98
|
+
return {
|
|
99
|
+
shellCount: documentPaths.length - liveCount,
|
|
100
|
+
liveCount,
|
|
101
|
+
protectedLiveCount,
|
|
102
|
+
ordinaryLiveLimit: ORDINARY_LIVE_PANE_LIMIT,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function loadTabWarmOrder(storage?: Pick<Storage, "getItem">): string[];
|
|
2
|
+
export declare function saveTabWarmOrder(paths: readonly string[], storage?: Pick<Storage, "setItem">): void;
|
|
3
|
+
/**
|
|
4
|
+
* Restored tabs warm in prior activation order. Tabs absent from that order use proximity to the
|
|
5
|
+
* active tab, alternating forward/backward, so first idle work benefits likely next clicks.
|
|
6
|
+
*/
|
|
7
|
+
export declare function restoredTabWarmOrder(openPaths: readonly string[], activePath: string | null, savedOrder: readonly string[]): string[];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const TAB_WARM_ORDER_KEY = "vr.session.tab-warm-order";
|
|
2
|
+
const MAX_SAVED_PATHS = 64;
|
|
3
|
+
export function loadTabWarmOrder(storage = localStorage) {
|
|
4
|
+
try {
|
|
5
|
+
const parsed = JSON.parse(storage.getItem(TAB_WARM_ORDER_KEY) ?? "[]");
|
|
6
|
+
if (!Array.isArray(parsed))
|
|
7
|
+
return [];
|
|
8
|
+
return [...new Set(parsed.filter((value) => typeof value === "string"))].slice(0, MAX_SAVED_PATHS);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function saveTabWarmOrder(paths, storage = localStorage) {
|
|
15
|
+
try {
|
|
16
|
+
storage.setItem(TAB_WARM_ORDER_KEY, JSON.stringify([...new Set(paths)].slice(0, MAX_SAVED_PATHS)));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// Storage can be unavailable in hardened webviews. Warm-order persistence is optional.
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Restored tabs warm in prior activation order. Tabs absent from that order use proximity to the
|
|
24
|
+
* active tab, alternating forward/backward, so first idle work benefits likely next clicks.
|
|
25
|
+
*/
|
|
26
|
+
export function restoredTabWarmOrder(openPaths, activePath, savedOrder) {
|
|
27
|
+
const open = new Set(openPaths);
|
|
28
|
+
const result = [];
|
|
29
|
+
const add = (path) => {
|
|
30
|
+
if (!path || path === activePath || !open.has(path) || result.includes(path))
|
|
31
|
+
return;
|
|
32
|
+
result.push(path);
|
|
33
|
+
};
|
|
34
|
+
for (const path of savedOrder)
|
|
35
|
+
add(path);
|
|
36
|
+
const activeIndex = activePath ? openPaths.indexOf(activePath) : -1;
|
|
37
|
+
if (activeIndex >= 0) {
|
|
38
|
+
for (let distance = 1; distance < openPaths.length; distance++) {
|
|
39
|
+
add(openPaths[activeIndex + distance]);
|
|
40
|
+
add(openPaths[activeIndex - distance]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
for (const path of openPaths)
|
|
45
|
+
add(path);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Concave bottom-corner curves that fuse the ACTIVE titlebar tab into the reader
|
|
4
|
+
* surface — the horizontal-strip counterpart of @rightkit/react-shell FuseCurves
|
|
5
|
+
* (which bridges an active sidebar ROW's right edge). The strip fuses at both
|
|
6
|
+
* bottom corners instead: left piece = the same square-minus-quarter-disc path
|
|
7
|
+
* verbatim; right piece = the same path mirrored with scaleX(-1). Kept as SVG
|
|
8
|
+
* (not the mockup's radial-gradient pseudo-elements) because SVG antialiases the
|
|
9
|
+
* arc edge identically to the sidebar fuse and never depends on the tab's
|
|
10
|
+
* ::before/::after slots (the inactive-tab separator already uses ::before).
|
|
11
|
+
*/
|
|
12
|
+
const TAB_FUSE_PATH = "M12 0V12H0A12 12 0 0 0 12 0Z";
|
|
13
|
+
export function TabFuseCurves({ size = 12 }) {
|
|
14
|
+
return (_jsxs(_Fragment, { children: [_jsx("svg", { "data-vr-tab-fuse-curve": "left", "aria-hidden": "true", focusable: "false", width: size, height: size, viewBox: "0 0 12 12", preserveAspectRatio: "none", className: "pointer-events-none absolute bottom-0", style: { left: -size, fill: "var(--reader-surface)" }, children: _jsx("path", { d: TAB_FUSE_PATH }) }), _jsx("svg", { "data-vr-tab-fuse-curve": "right", "aria-hidden": "true", focusable: "false", width: size, height: size, viewBox: "0 0 12 12", preserveAspectRatio: "none", className: "pointer-events-none absolute bottom-0", style: { right: -size, transform: "scaleX(-1)", fill: "var(--reader-surface)" }, children: _jsx("path", { d: TAB_FUSE_PATH }) })] }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type RefObject } from "react";
|
|
2
|
+
import type { LoadedDocument } from "@rightkit/document-core/documents/types";
|
|
3
|
+
export interface TabOverviewProps {
|
|
4
|
+
docs: LoadedDocument[];
|
|
5
|
+
activePath: string | null;
|
|
6
|
+
titleFor: (doc: LoadedDocument) => string;
|
|
7
|
+
isDirty: (doc: LoadedDocument) => boolean;
|
|
8
|
+
dirtyPaths: ReadonlySet<string>;
|
|
9
|
+
subscribeDirty: (path: string, cb: () => void) => () => void;
|
|
10
|
+
subscribeEdits: (path: string, cb: () => void) => () => void;
|
|
11
|
+
getEditSnapshot: (path: string) => string | undefined;
|
|
12
|
+
computeTitle?: (doc: LoadedDocument) => string;
|
|
13
|
+
onActivate: (path: string) => void;
|
|
14
|
+
/** Bulk close for the footer action (integrator wires closeMany — dirty docs route
|
|
15
|
+
* through the Save-All/Discard-All chooser upstream). */
|
|
16
|
+
onClosePaths: (paths: string[]) => void;
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
/** Overview toggle button — Popover already restores focus to its opener, so this is a
|
|
19
|
+
* fallback only (e.g. the button unmounted between open and close). */
|
|
20
|
+
returnFocusRef: RefObject<HTMLButtonElement | null>;
|
|
21
|
+
}
|
|
22
|
+
export declare const TabOverview: import("react").MemoExoticComponent<typeof TabOverviewImpl>;
|
|
23
|
+
declare function TabOverviewImpl({ docs, activePath, titleFor, isDirty, dirtyPaths, subscribeDirty, subscribeEdits, getEditSnapshot, computeTitle, onActivate, onClosePaths, onClose, returnFocusRef, }: TabOverviewProps): import("react").JSX.Element;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Tab overview — the ⌘⇧A popover that drops out of the titlebar tab strip's right-edge
|
|
3
|
+
// button (brief Part A item 8; visual spec docs/design/viewright-tabs-explorer.html `.pop`).
|
|
4
|
+
// Built on the sidebar's shared Popover (glass panel, Escape + click-outside dismiss, and
|
|
5
|
+
// focus restore to the element that opened it). Top→bottom: search field (focused on open),
|
|
6
|
+
// type chips with counts, a scrollable list of matching tabs, and a footer with "N of M tabs"
|
|
7
|
+
// plus a bulk Close that only appears while a filter or search is active.
|
|
8
|
+
import { memo, useEffect, useRef, useState, } from "react";
|
|
9
|
+
import { moveIndex } from "@rightkit/platform-ui/headless";
|
|
10
|
+
import { isUntitled } from "@rightkit/document-core/documents/types";
|
|
11
|
+
import { FileTypeIcon } from "../../documents/FileTypeIcon.js";
|
|
12
|
+
import { LiveTitle } from "../LiveTitle.js";
|
|
13
|
+
import { Popover } from "../sidebar/SidebarPrimitives.js";
|
|
14
|
+
import { pathFolder } from "../sidebar/SidebarPathUtils.js";
|
|
15
|
+
import { useTabDirty } from "./useTabDirty.js";
|
|
16
|
+
const TYPE_GROUPS = [
|
|
17
|
+
{ id: "all", label: "All", formats: null },
|
|
18
|
+
{ id: "pdf", label: "PDF", formats: ["pdf"] },
|
|
19
|
+
{ id: "md", label: "Markdown", formats: ["md", "mdx"] },
|
|
20
|
+
{ id: "docx", label: "Word", formats: ["docx"] },
|
|
21
|
+
{ id: "image", label: "Images", formats: ["image"] },
|
|
22
|
+
{ id: "html", label: "HTML", formats: ["html"] },
|
|
23
|
+
{ id: "txt", label: "Text", formats: ["txt"] },
|
|
24
|
+
];
|
|
25
|
+
/** [singular, plural] nouns for the bulk-close label — "Close 8 PDFs", "Close 3 Word
|
|
26
|
+
* documents", "Close 5 tabs" when only a search is narrowing the set. */
|
|
27
|
+
const TYPE_NOUNS = {
|
|
28
|
+
all: ["tab", "tabs"],
|
|
29
|
+
pdf: ["PDF", "PDFs"],
|
|
30
|
+
md: ["Markdown file", "Markdown files"],
|
|
31
|
+
docx: ["Word document", "Word documents"],
|
|
32
|
+
image: ["image", "images"],
|
|
33
|
+
html: ["HTML file", "HTML files"],
|
|
34
|
+
txt: ["text file", "text files"],
|
|
35
|
+
};
|
|
36
|
+
export const TabOverview = memo(TabOverviewImpl);
|
|
37
|
+
function TabOverviewImpl({ docs, activePath, titleFor, isDirty, dirtyPaths, subscribeDirty, subscribeEdits, getEditSnapshot, computeTitle, onActivate, onClosePaths, onClose, returnFocusRef, }) {
|
|
38
|
+
const inputRef = useRef(null);
|
|
39
|
+
const listRef = useRef(null);
|
|
40
|
+
const [query, setQuery] = useState("");
|
|
41
|
+
const [typeGroup, setTypeGroup] = useState("all");
|
|
42
|
+
// Search field takes focus on open (Popover's auto-focus only targets [role=menuitem]s,
|
|
43
|
+
// which this list deliberately doesn't use — rows stay honest buttons).
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
inputRef.current?.focus();
|
|
46
|
+
}, []);
|
|
47
|
+
// If the opener vanished while we were open, land on the overview button instead of <body>.
|
|
48
|
+
useEffect(() => () => {
|
|
49
|
+
if (document.activeElement === document.body)
|
|
50
|
+
returnFocusRef.current?.focus();
|
|
51
|
+
}, [returnFocusRef]);
|
|
52
|
+
const q = query.trim().toLowerCase();
|
|
53
|
+
const formats = TYPE_GROUPS.find((g) => g.id === typeGroup)?.formats ?? null;
|
|
54
|
+
const shown = docs.filter((doc) => {
|
|
55
|
+
if (formats && !formats.includes(doc.format))
|
|
56
|
+
return false;
|
|
57
|
+
if (!q)
|
|
58
|
+
return true;
|
|
59
|
+
return (titleFor(doc).toLowerCase().includes(q) || pathFolder(doc.path).toLowerCase().includes(q));
|
|
60
|
+
});
|
|
61
|
+
const rows = () => [
|
|
62
|
+
...(listRef.current?.querySelectorAll("[data-vr-ov-row]") ?? []),
|
|
63
|
+
];
|
|
64
|
+
// ↓/↑ roam between the search field and the row list (clamped, no wrap — first row's ↑
|
|
65
|
+
// returns to the field); Enter in the field opens the first match; Enter on a row is the
|
|
66
|
+
// button's native click (activate + close). Escape is Popover's.
|
|
67
|
+
const onKeyDown = (event) => {
|
|
68
|
+
const list = rows();
|
|
69
|
+
if (event.key === "ArrowDown") {
|
|
70
|
+
if (!list.length)
|
|
71
|
+
return;
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
const at = list.indexOf(document.activeElement);
|
|
74
|
+
list[at < 0 ? 0 : moveIndex(at, list.length, "next", false)]?.focus();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (event.key === "ArrowUp") {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
const at = list.indexOf(document.activeElement);
|
|
80
|
+
if (at <= 0)
|
|
81
|
+
inputRef.current?.focus();
|
|
82
|
+
else
|
|
83
|
+
list[moveIndex(at, list.length, "prev", false)]?.focus();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (event.key === "Enter" && document.activeElement === inputRef.current && shown.length) {
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
onActivate(shown[0].path);
|
|
89
|
+
onClose();
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const activate = (path) => {
|
|
93
|
+
onActivate(path);
|
|
94
|
+
onClose();
|
|
95
|
+
};
|
|
96
|
+
const closeShown = () => {
|
|
97
|
+
onClosePaths(shown.map((d) => d.path));
|
|
98
|
+
onClose();
|
|
99
|
+
};
|
|
100
|
+
// Footer bulk action: only while a filter/search is active AND something matched —
|
|
101
|
+
// "Close 8 PDFs" / "Close 3 Word documents" / "Close 5 tabs" (search-only), with the
|
|
102
|
+
// dirty-count hint when the affected set includes unsaved tabs.
|
|
103
|
+
const filterActive = typeGroup !== "all" || q !== "";
|
|
104
|
+
const dirtyShown = shown.filter((d) => dirtyPaths.has(d.path) || isDirty(d)).length;
|
|
105
|
+
const noun = TYPE_NOUNS[typeGroup][shown.length === 1 ? 0 : 1];
|
|
106
|
+
return (_jsxs(Popover, { onClose: onClose, className: "vr-ov-pop", role: "dialog", ariaModal: false, ariaLabel: "Open tabs", children: [_jsx("input", { ref: inputRef, className: "vr-ov-q vr-focus", placeholder: "Search open tabs", "aria-label": "Search open tabs", value: query, onChange: (event) => setQuery(event.target.value), onKeyDown: onKeyDown }), _jsx("div", { className: "vr-ov-chips", role: "group", "aria-label": "Filter by type", children: TYPE_GROUPS.map((group) => {
|
|
107
|
+
const n = group.formats
|
|
108
|
+
? docs.filter((d) => group.formats?.includes(d.format)).length
|
|
109
|
+
: docs.length;
|
|
110
|
+
if (group.id !== "all" && n === 0)
|
|
111
|
+
return null; // hide zero-count chips
|
|
112
|
+
return (_jsxs("button", { type: "button", className: "vr-ov-chip vr-press vr-focus", "aria-pressed": typeGroup === group.id, onClick: () => setTypeGroup(group.id), children: [group.label, " ", n] }, group.id));
|
|
113
|
+
}) }), _jsx("div", { ref: listRef, className: "vr-ov-list", onKeyDown: onKeyDown, children: shown.length === 0 ? (_jsx("div", { className: "vr-ov-empty", children: "No open tabs match" })) : (shown.map((doc) => (_jsx(OverviewRow, { doc: doc, title: titleFor(doc), titleNode: computeTitle && isUntitled(doc.path) ? (_jsx(LiveTitle, { doc: doc, compute: computeTitle, subscribeEdits: subscribeEdits, getEditSnapshot: getEditSnapshot })) : undefined, isDirty: isDirty, dirtyHint: dirtyPaths.has(doc.path), subscribeDirty: subscribeDirty, active: doc.path === activePath, onActivate: activate }, doc.tabKey ?? doc.path)))) }), _jsxs("div", { className: "vr-ov-foot", children: [_jsxs("span", { className: "vr-ov-sum", children: [shown.length, " of ", docs.length, " ", docs.length === 1 ? "tab" : "tabs"] }), filterActive && shown.length > 0 && (_jsxs("button", { type: "button", className: "vr-ov-close vr-press vr-focus", onClick: closeShown, children: ["Close ", shown.length, " ", noun, dirtyShown > 0 ? ` · ${dirtyShown} unsaved, will ask` : ""] }))] })] }));
|
|
114
|
+
}
|
|
115
|
+
const OverviewRow = memo(OverviewRowImpl);
|
|
116
|
+
function OverviewRowImpl({ doc, title, titleNode, isDirty, dirtyHint, subscribeDirty, active, onActivate, }) {
|
|
117
|
+
const dirty = useTabDirty(doc, dirtyHint, isDirty, subscribeDirty);
|
|
118
|
+
const displayPath = doc.format === "web" ? (doc.web?.url ?? doc.path) : doc.path;
|
|
119
|
+
const folder = isUntitled(doc.path) ? "Unsaved buffer" : pathFolder(displayPath);
|
|
120
|
+
return (_jsxs("button", { type: "button", "data-vr-ov-row": true, className: `vr-ov-row${active ? " vr-ov-row-active" : ""}`, "data-active": active || undefined, title: displayPath, "aria-current": active || undefined, "aria-label": `${title}${dirty ? " — Unsaved edits" : ""}`, onClick: () => onActivate(doc.path), children: [_jsx("span", { className: "vr-ov-row-ficon", "aria-hidden": "true", children: _jsx(FileTypeIcon, { format: doc.format, path: doc.path, size: 20 }) }), _jsxs("span", { className: "vr-ov-row-text", children: [_jsx("span", { className: "vr-ov-row-name", children: titleNode ?? title }), _jsx("span", { className: "vr-ov-row-meta", children: folder })] }), dirty && _jsx("span", { className: "vr-ov-row-unsaved", children: "\u25CF Unsaved" })] }));
|
|
121
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { LoadedDocument } from "@rightkit/document-core/documents/types";
|
|
2
|
+
/**
|
|
3
|
+
* Everything the strip needs, supplied by the shell (App builds this once so the
|
|
4
|
+
* memoized strip skips per-keystroke re-renders the same way Sidebar/Toolbar do).
|
|
5
|
+
*/
|
|
6
|
+
export interface TabStripProps {
|
|
7
|
+
/** openDocs in order — single source of truth for tab order. */
|
|
8
|
+
docs: LoadedDocument[];
|
|
9
|
+
activePath: string | null;
|
|
10
|
+
sidebarCollapsed: boolean;
|
|
11
|
+
/** Dispatches "viewright:toggle-sidebar" (the sidebar owns the actual state). */
|
|
12
|
+
onToggleSidebar: () => void;
|
|
13
|
+
onActivate: (path: string) => void;
|
|
14
|
+
/** Goes through the existing dirty-confirm flow (requestClose). */
|
|
15
|
+
onRequestClose: (path: string) => void;
|
|
16
|
+
onCloseAll: () => void;
|
|
17
|
+
onCloseOthers: (path: string) => void;
|
|
18
|
+
onCloseRight: (path: string) => void;
|
|
19
|
+
onReorderTabs: (fromPath: string, toPath: string, position?: "before" | "after") => void;
|
|
20
|
+
/** New untitled txt buffer (Ctrl/Cmd+T). */
|
|
21
|
+
onNewDocument: () => void;
|
|
22
|
+
onTogglePin: (path: string) => void;
|
|
23
|
+
pinnedPaths: ReadonlySet<string>;
|
|
24
|
+
openLocalPaths: string[];
|
|
25
|
+
titleFor: (doc: LoadedDocument) => string;
|
|
26
|
+
isDirty: (doc: LoadedDocument) => boolean;
|
|
27
|
+
dirtyPaths: ReadonlySet<string>;
|
|
28
|
+
subscribeDirty: (path: string, cb: () => void) => () => void;
|
|
29
|
+
subscribeEdits: (path: string, cb: () => void) => () => void;
|
|
30
|
+
getEditSnapshot: (path: string) => string | undefined;
|
|
31
|
+
/** LiveTitle compute — keeps untitled tab names live while typing. */
|
|
32
|
+
computeTitle: (doc: LoadedDocument) => string;
|
|
33
|
+
overviewOpen: boolean;
|
|
34
|
+
onOverviewOpenChange: (open: boolean) => void;
|
|
35
|
+
/** Bulk-close an arbitrary subset (integrator wires closeMany — the overview's
|
|
36
|
+
* "Close all" footer uses it; dirty docs still go through the unsaved-changes
|
|
37
|
+
* chooser). */
|
|
38
|
+
onClosePaths: (paths: string[]) => void;
|
|
39
|
+
}
|
|
40
|
+
export declare const TabStrip: import("react").MemoExoticComponent<typeof TabStripImpl>;
|
|
41
|
+
declare function TabStripImpl({ docs, activePath, sidebarCollapsed, onActivate, onRequestClose, onCloseAll, onCloseOthers, onCloseRight, onReorderTabs, onTogglePin, pinnedPaths, openLocalPaths, titleFor, isDirty, dirtyPaths, subscribeDirty, subscribeEdits, getEditSnapshot, computeTitle, overviewOpen, onOverviewOpenChange, onClosePaths, }: TabStripProps): import("react").JSX.Element;
|
|
42
|
+
export {};
|