@rightkit/document-workspace 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE-APACHE +201 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +51 -2
  5. package/dist/app/App.d.ts +1 -0
  6. package/dist/app/App.js +6 -0
  7. package/dist/app/AppDialogs.d.ts +52 -0
  8. package/dist/app/AppDialogs.js +23 -0
  9. package/dist/app/AppDialogsHost.d.ts +31 -0
  10. package/dist/app/AppDialogsHost.js +30 -0
  11. package/dist/app/AppLifecycle.d.ts +19 -0
  12. package/dist/app/AppLifecycle.js +87 -0
  13. package/dist/app/AppProvider.d.ts +317 -0
  14. package/dist/app/AppProvider.js +217 -0
  15. package/dist/app/AppRoot.d.ts +8 -0
  16. package/dist/app/AppRoot.js +159 -0
  17. package/dist/app/AppShell.d.ts +2 -0
  18. package/dist/app/AppShell.js +81 -0
  19. package/dist/app/CommandPalette.d.ts +13 -0
  20. package/dist/app/CommandPalette.js +47 -0
  21. package/dist/app/DocActionBar.d.ts +36 -0
  22. package/dist/app/DocActionBar.js +300 -0
  23. package/dist/app/DocumentContextMenu.d.ts +29 -0
  24. package/dist/app/DocumentContextMenu.js +122 -0
  25. package/dist/app/DocumentPane.d.ts +52 -0
  26. package/dist/app/DocumentPane.js +30 -0
  27. package/dist/app/DocumentRouterTable.d.ts +58 -0
  28. package/dist/app/DocumentRouterTable.js +143 -0
  29. package/dist/app/ErrorBoundary.d.ts +12 -0
  30. package/dist/app/ErrorBoundary.js +16 -0
  31. package/dist/app/FindBar.d.ts +12 -0
  32. package/dist/app/FindBar.js +199 -0
  33. package/dist/app/LiveTitle.d.ts +14 -0
  34. package/dist/app/LiveTitle.js +14 -0
  35. package/dist/app/OpeningProgress.d.ts +13 -0
  36. package/dist/app/OpeningProgress.js +21 -0
  37. package/dist/app/PrintPreviewWindow.d.ts +2 -0
  38. package/dist/app/PrintPreviewWindow.js +57 -0
  39. package/dist/app/ShortcutHelp.d.ts +3 -0
  40. package/dist/app/ShortcutHelp.js +99 -0
  41. package/dist/app/Sidebar.d.ts +36 -0
  42. package/dist/app/Sidebar.js +121 -0
  43. package/dist/app/ToastViewport.d.ts +1 -0
  44. package/dist/app/ToastViewport.js +31 -0
  45. package/dist/app/Toolbar.d.ts +26 -0
  46. package/dist/app/Toolbar.js +63 -0
  47. package/dist/app/activeFormatSavers.d.ts +7 -0
  48. package/dist/app/activeFormatSavers.js +4 -0
  49. package/dist/app/clipboardPaths.d.ts +1 -0
  50. package/dist/app/clipboardPaths.js +31 -0
  51. package/dist/app/documentDrafts.d.ts +17 -0
  52. package/dist/app/documentDrafts.js +104 -0
  53. package/dist/app/draftResolution.d.ts +5 -0
  54. package/dist/app/draftResolution.js +25 -0
  55. package/dist/app/markdownPaletteActions.d.ts +8 -0
  56. package/dist/app/markdownPaletteActions.js +107 -0
  57. package/dist/app/resourceCensusDev.d.ts +41 -0
  58. package/dist/app/resourceCensusDev.js +518 -0
  59. package/dist/app/setupRecovery.d.ts +34 -0
  60. package/dist/app/setupRecovery.js +70 -0
  61. package/dist/app/shell/DocStatsPill.d.ts +18 -0
  62. package/dist/app/shell/DocStatsPill.js +32 -0
  63. package/dist/app/shell/DocumentTools.d.ts +9 -0
  64. package/dist/app/shell/DocumentTools.js +73 -0
  65. package/dist/app/shell/InfoInspector.d.ts +9 -0
  66. package/dist/app/shell/InfoInspector.js +67 -0
  67. package/dist/app/shell/StartPage.d.ts +9 -0
  68. package/dist/app/shell/StartPage.js +49 -0
  69. package/dist/app/shell/readEdit.d.ts +6 -0
  70. package/dist/app/shell/readEdit.js +6 -0
  71. package/dist/app/sidebar/SidebarDocumentMenu.d.ts +20 -0
  72. package/dist/app/sidebar/SidebarDocumentMenu.js +56 -0
  73. package/dist/app/sidebar/SidebarExpandedPanel.d.ts +9 -0
  74. package/dist/app/sidebar/SidebarExpandedPanel.js +78 -0
  75. package/dist/app/sidebar/SidebarIcons.d.ts +3 -0
  76. package/dist/app/sidebar/SidebarIcons.js +18 -0
  77. package/dist/app/sidebar/SidebarMenus.d.ts +19 -0
  78. package/dist/app/sidebar/SidebarMenus.js +64 -0
  79. package/dist/app/sidebar/SidebarPathUtils.d.ts +10 -0
  80. package/dist/app/sidebar/SidebarPathUtils.js +55 -0
  81. package/dist/app/sidebar/SidebarPrimitives.d.ts +12 -0
  82. package/dist/app/sidebar/SidebarPrimitives.js +45 -0
  83. package/dist/app/sidebar/SidebarRail.d.ts +5 -0
  84. package/dist/app/sidebar/SidebarRail.js +37 -0
  85. package/dist/app/sidebar/explorer/ExplorerRowMenu.d.ts +21 -0
  86. package/dist/app/sidebar/explorer/ExplorerRowMenu.js +37 -0
  87. package/dist/app/sidebar/explorer/ExplorerSections.d.ts +16 -0
  88. package/dist/app/sidebar/explorer/ExplorerSections.js +259 -0
  89. package/dist/app/sidebar/explorer/ExplorerTree.d.ts +46 -0
  90. package/dist/app/sidebar/explorer/ExplorerTree.js +387 -0
  91. package/dist/app/sidebar/explorer/explorerState.d.ts +37 -0
  92. package/dist/app/sidebar/explorer/explorerState.js +135 -0
  93. package/dist/app/tabMountPolicy.d.ts +45 -0
  94. package/dist/app/tabMountPolicy.js +104 -0
  95. package/dist/app/tabWarmOrder.d.ts +7 -0
  96. package/dist/app/tabWarmOrder.js +48 -0
  97. package/dist/app/tabs/TabFuseCurves.d.ts +3 -0
  98. package/dist/app/tabs/TabFuseCurves.js +15 -0
  99. package/dist/app/tabs/TabOverview.d.ts +24 -0
  100. package/dist/app/tabs/TabOverview.js +121 -0
  101. package/dist/app/tabs/TabStrip.d.ts +42 -0
  102. package/dist/app/tabs/TabStrip.js +380 -0
  103. package/dist/app/tabs/tabIds.d.ts +5 -0
  104. package/dist/app/tabs/tabIds.js +7 -0
  105. package/dist/app/tabs/useTabDirty.d.ts +3 -0
  106. package/dist/app/tabs/useTabDirty.js +11 -0
  107. package/dist/app/useAppChromeState.d.ts +21 -0
  108. package/dist/app/useAppChromeState.js +48 -0
  109. package/dist/app/useAppCommands.d.ts +66 -0
  110. package/dist/app/useAppCommands.js +71 -0
  111. package/dist/app/useAppDocumentCore.d.ts +210 -0
  112. package/dist/app/useAppDocumentCore.js +40 -0
  113. package/dist/app/useAppEffects.d.ts +18 -0
  114. package/dist/app/useAppEffects.js +52 -0
  115. package/dist/app/useAppPaneSearch.d.ts +37 -0
  116. package/dist/app/useAppPaneSearch.js +74 -0
  117. package/dist/app/useAppShellSetup.d.ts +40 -0
  118. package/dist/app/useAppShellSetup.js +52 -0
  119. package/dist/app/useAppSlots.d.ts +28 -0
  120. package/dist/app/useAppSlots.js +40 -0
  121. package/dist/app/useAppTabChrome.d.ts +51 -0
  122. package/dist/app/useAppTabChrome.js +66 -0
  123. package/dist/app/useAutoSave.d.ts +34 -0
  124. package/dist/app/useAutoSave.js +116 -0
  125. package/dist/app/useCloseAll.d.ts +52 -0
  126. package/dist/app/useCloseAll.js +295 -0
  127. package/dist/app/useClosedTabEviction.d.ts +10 -0
  128. package/dist/app/useClosedTabEviction.js +38 -0
  129. package/dist/app/useCommandPalette.d.ts +44 -0
  130. package/dist/app/useCommandPalette.js +254 -0
  131. package/dist/app/useCrossDocSearch.d.ts +7 -0
  132. package/dist/app/useCrossDocSearch.js +94 -0
  133. package/dist/app/useDistractionFree.d.ts +21 -0
  134. package/dist/app/useDistractionFree.js +73 -0
  135. package/dist/app/useDocumentCreation.d.ts +18 -0
  136. package/dist/app/useDocumentCreation.js +63 -0
  137. package/dist/app/useDocumentDirty.d.ts +31 -0
  138. package/dist/app/useDocumentDirty.js +46 -0
  139. package/dist/app/useDocumentLoader.d.ts +55 -0
  140. package/dist/app/useDocumentLoader.js +267 -0
  141. package/dist/app/useDocumentOutputActions.d.ts +6 -0
  142. package/dist/app/useDocumentOutputActions.js +45 -0
  143. package/dist/app/useDocumentRuntimeEffects.d.ts +36 -0
  144. package/dist/app/useDocumentRuntimeEffects.js +458 -0
  145. package/dist/app/useDocumentSaver.d.ts +58 -0
  146. package/dist/app/useDocumentSaver.js +511 -0
  147. package/dist/app/useDocumentTabs.d.ts +41 -0
  148. package/dist/app/useDocumentTabs.js +207 -0
  149. package/dist/app/useDocumentWatcher.d.ts +83 -0
  150. package/dist/app/useDocumentWatcher.js +393 -0
  151. package/dist/app/useDocumentZoom.d.ts +24 -0
  152. package/dist/app/useDocumentZoom.js +104 -0
  153. package/dist/app/useDocuments.d.ts +169 -0
  154. package/dist/app/useDocuments.js +455 -0
  155. package/dist/app/useDraftConfirmGate.d.ts +27 -0
  156. package/dist/app/useDraftConfirmGate.js +47 -0
  157. package/dist/app/useFileDragOverlay.d.ts +12 -0
  158. package/dist/app/useFileDragOverlay.js +49 -0
  159. package/dist/app/useFormatApis.d.ts +13 -0
  160. package/dist/app/useFormatApis.js +62 -0
  161. package/dist/app/useFormatDirtyState.d.ts +29 -0
  162. package/dist/app/useFormatDirtyState.js +81 -0
  163. package/dist/app/useGlobalShortcuts.d.ts +47 -0
  164. package/dist/app/useGlobalShortcuts.js +359 -0
  165. package/dist/app/useHtmlSession.d.ts +68 -0
  166. package/dist/app/useHtmlSession.js +463 -0
  167. package/dist/app/useHtmlSessionProps.d.ts +33 -0
  168. package/dist/app/useHtmlSessionProps.js +57 -0
  169. package/dist/app/useLibrary.d.ts +21 -0
  170. package/dist/app/useLibrary.js +81 -0
  171. package/dist/app/useOpenFromClipboard.d.ts +7 -0
  172. package/dist/app/useOpenFromClipboard.js +44 -0
  173. package/dist/app/usePaneLiveness.d.ts +29 -0
  174. package/dist/app/usePaneLiveness.js +199 -0
  175. package/dist/app/usePendingSavePaths.d.ts +19 -0
  176. package/dist/app/usePendingSavePaths.js +67 -0
  177. package/dist/app/useReadyShow.d.ts +12 -0
  178. package/dist/app/useReadyShow.js +142 -0
  179. package/dist/app/useSearchController.d.ts +22 -0
  180. package/dist/app/useSearchController.js +114 -0
  181. package/dist/app/useSetupRecovery.d.ts +41 -0
  182. package/dist/app/useSetupRecovery.js +153 -0
  183. package/dist/app/useTabChrome.d.ts +40 -0
  184. package/dist/app/useTabChrome.js +115 -0
  185. package/dist/app/useViewState.d.ts +27 -0
  186. package/dist/app/useViewState.js +160 -0
  187. package/dist/appName.d.ts +2 -0
  188. package/dist/appName.js +8 -0
  189. package/dist/documents/DocumentRouter.d.ts +102 -0
  190. package/dist/documents/DocumentRouter.js +153 -0
  191. package/dist/documents/FileTypeIcon.d.ts +6 -0
  192. package/dist/documents/FileTypeIcon.js +93 -0
  193. package/dist/documents/ImageDocument.d.ts +14 -0
  194. package/dist/documents/ImageDocument.js +61 -0
  195. package/dist/documents/capabilities.d.ts +17 -0
  196. package/dist/documents/capabilities.js +18 -0
  197. package/dist/documents/html/HtmlDocument.d.ts +15 -0
  198. package/dist/documents/html/HtmlDocument.js +584 -0
  199. package/dist/documents/html/HtmlDocumentLayout.d.ts +87 -0
  200. package/dist/documents/html/HtmlDocumentLayout.js +25 -0
  201. package/dist/documents/html/HtmlDocumentProps.d.ts +60 -0
  202. package/dist/documents/html/HtmlDocumentProps.js +1 -0
  203. package/dist/documents/html/HtmlDocumentSource.d.ts +34 -0
  204. package/dist/documents/html/HtmlDocumentSource.js +28 -0
  205. package/dist/documents/markdown/MarkdownSplitPane.d.ts +115 -0
  206. package/dist/documents/markdown/MarkdownSplitPane.js +352 -0
  207. package/dist/documents/mdx/MdxSplitPane.d.ts +32 -0
  208. package/dist/documents/mdx/MdxSplitPane.js +60 -0
  209. package/dist/documents/txt/LinkDialog.d.ts +8 -0
  210. package/dist/documents/txt/LinkDialog.js +34 -0
  211. package/dist/documents/txt/MarkdownSourceToolbar.d.ts +8 -0
  212. package/dist/documents/txt/MarkdownSourceToolbar.js +25 -0
  213. package/dist/documents/txt/SourceEditorToolbar.d.ts +12 -0
  214. package/dist/documents/txt/SourceEditorToolbar.js +4 -0
  215. package/dist/documents/txt/TxtEditor.d.ts +39 -0
  216. package/dist/documents/txt/TxtEditor.js +186 -0
  217. package/dist/documents/txt/sourceEditorTypes.d.ts +49 -0
  218. package/dist/documents/txt/sourceEditorTypes.js +1 -0
  219. package/dist/documents/txt/sourceExtensions.d.ts +38 -0
  220. package/dist/documents/txt/sourceExtensions.js +444 -0
  221. package/dist/documents/txt/sourceFormatting.d.ts +18 -0
  222. package/dist/documents/txt/sourceFormatting.js +183 -0
  223. package/dist/documents/txt/sourceTheme.d.ts +5 -0
  224. package/dist/documents/txt/sourceTheme.js +220 -0
  225. package/dist/documents/txt/useCodeMirrorSourceEditor.d.ts +69 -0
  226. package/dist/documents/txt/useCodeMirrorSourceEditor.js +546 -0
  227. package/dist/documents/txt/useSourceSearch.d.ts +39 -0
  228. package/dist/documents/txt/useSourceSearch.js +122 -0
  229. package/dist/documents/txt/useTextEditorCommands.d.ts +26 -0
  230. package/dist/documents/txt/useTextEditorCommands.js +151 -0
  231. package/dist/documents/web/LivePreviewPane.d.ts +11 -0
  232. package/dist/documents/web/LivePreviewPane.js +58 -0
  233. package/dist/env.d.ts +66 -0
  234. package/dist/index.d.ts +121 -0
  235. package/dist/index.js +123 -0
  236. package/dist/settings/AppearanceMenu.d.ts +9 -0
  237. package/dist/settings/AppearanceMenu.js +54 -0
  238. package/dist/settings/SettingsPanel.d.ts +7 -0
  239. package/dist/settings/SettingsPanel.js +104 -0
  240. package/dist/settings/publishedSections.d.ts +22 -0
  241. package/dist/settings/publishedSections.js +79 -0
  242. package/dist/settings/sections/AboutSection.d.ts +1 -0
  243. package/dist/settings/sections/AboutSection.js +13 -0
  244. package/dist/settings/sections/DiagnosticsSection.d.ts +1 -0
  245. package/dist/settings/sections/DiagnosticsSection.js +42 -0
  246. package/dist/settings/sections/PrivacySection.d.ts +3 -0
  247. package/dist/settings/sections/PrivacySection.js +51 -0
  248. package/dist/settings/sections/UpdateLicenseBlock.d.ts +10 -0
  249. package/dist/settings/sections/UpdateLicenseBlock.js +158 -0
  250. package/dist/state/explorer.d.ts +32 -0
  251. package/dist/state/explorer.js +111 -0
  252. package/dist/state/legal.d.ts +51 -0
  253. package/dist/state/legal.js +130 -0
  254. package/dist/state/library.d.ts +33 -0
  255. package/dist/state/library.js +110 -0
  256. package/dist/state/updateLicense.d.ts +31 -0
  257. package/dist/state/updateLicense.js +340 -0
  258. package/dist/styles/explorer.css +219 -0
  259. package/dist/styles/index.css +1477 -0
  260. package/dist/styles/tabs.css +467 -0
  261. package/dist/styles.css +6 -0
  262. package/package.json +88 -3
@@ -0,0 +1,55 @@
1
+ import { toast } from "@rightkit/platform-ui/headless";
2
+ import { writeClipboardText } from "@rightkit/document-core/state/tauriBridge";
3
+ export function isLocalFileDoc(doc) {
4
+ return doc.format !== "web" && !doc.path.startsWith("untitled:");
5
+ }
6
+ export function pathFolder(path) {
7
+ const lastSlash = Math.max(path.lastIndexOf("\\"), path.lastIndexOf("/"));
8
+ return lastSlash <= 0 ? "" : path.slice(0, lastSlash);
9
+ }
10
+ export function pathBase(path) {
11
+ const lastSlash = Math.max(path.lastIndexOf("\\"), path.lastIndexOf("/"));
12
+ return lastSlash < 0 ? path : path.slice(lastSlash + 1);
13
+ }
14
+ export function pathSeparator(path) {
15
+ return path.includes("\\") ? "\\" : "/";
16
+ }
17
+ export function splitPath(path) {
18
+ return path.split(/[\\/]+/).filter(Boolean);
19
+ }
20
+ export function commonFolder(paths) {
21
+ if (paths.length < 2)
22
+ return pathFolder(paths[0] ?? "");
23
+ const sep = pathSeparator(paths[0]);
24
+ const dirs = paths.map((path) => splitPath(pathFolder(path)));
25
+ const first = dirs[0] ?? [];
26
+ const common = [];
27
+ for (let i = 0; i < first.length; i++) {
28
+ const part = first[i];
29
+ if (dirs.every((dir) => dir[i]?.toLowerCase() === part.toLowerCase()))
30
+ common.push(part);
31
+ else
32
+ break;
33
+ }
34
+ return common.join(sep);
35
+ }
36
+ export function relativePath(path, openLocalPaths) {
37
+ const base = commonFolder(openLocalPaths);
38
+ if (!base)
39
+ return pathBase(path);
40
+ const next = path.slice(base.length).replace(/^[\\/]+/, "");
41
+ return next || pathBase(path);
42
+ }
43
+ export function breadcrumbPath(path) {
44
+ return splitPath(path).join(" > ");
45
+ }
46
+ export async function copyText(text) {
47
+ try {
48
+ await writeClipboardText(text);
49
+ toast("Copied to clipboard", "success");
50
+ }
51
+ catch {
52
+ // Clipboard can reject (permissions / no focus) — don't leak an unhandled rejection.
53
+ toast("Couldn't copy to clipboard", "error");
54
+ }
55
+ }
@@ -0,0 +1,12 @@
1
+ export declare const menuItemClass = "vr-press vr-focus flex w-full items-center px-3 py-1.5 text-left font-sans text-sm text-paper/85 hover:bg-ink/50 hover:text-paper focus-visible:bg-ink/50 focus-visible:text-paper";
2
+ export declare const compactMenuItemClass = "vr-press vr-focus flex w-full items-center px-2.5 py-1 text-left font-sans text-xs text-paper/85 hover:bg-ink/50 hover:text-paper focus-visible:bg-ink/50 focus-visible:text-paper";
3
+ /** Classes every shell-menu panel in the sidebar shares (the glass panel; position comes from the shell). */
4
+ export declare const menuPanelClass = "vr-pop no-print vr-glass-panel overflow-hidden rounded-lg";
5
+ export declare function Popover({ onClose, className, children, role, ariaModal, ariaLabel, }: {
6
+ onClose: () => void;
7
+ className: string;
8
+ children: React.ReactNode;
9
+ role?: "menu" | "dialog";
10
+ ariaModal?: boolean;
11
+ ariaLabel?: string;
12
+ }): import("react").JSX.Element;
@@ -0,0 +1,45 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Shared small presentational pieces reused by the sidebar's popovers (the tab overview) and by the collapsed rail / expanded header icon buttons.
3
+ // Extracted verbatim from Sidebar.tsx as part of the 2026-07-02 decomposition plan (V-K).
4
+ import { useDismiss, useExitTransition, useFocusTrap, useListNavigation } from "@rightkit/platform-ui/headless";
5
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
6
+ const menuItems = (root) => [
7
+ ...(root?.querySelectorAll('[role="menuitem"]:not([disabled])') ?? []),
8
+ ];
9
+ export const menuItemClass = "vr-press vr-focus flex w-full items-center px-3 py-1.5 text-left font-sans text-sm text-paper/85 hover:bg-ink/50 hover:text-paper focus-visible:bg-ink/50 focus-visible:text-paper";
10
+ export const compactMenuItemClass = "vr-press vr-focus flex w-full items-center px-2.5 py-1 text-left font-sans text-xs text-paper/85 hover:bg-ink/50 hover:text-paper focus-visible:bg-ink/50 focus-visible:text-paper";
11
+ /** Classes every shell-menu panel in the sidebar shares (the glass panel; position comes from the shell). */
12
+ export const menuPanelClass = "vr-pop no-print vr-glass-panel overflow-hidden rounded-lg";
13
+ export function Popover({ onClose, className, children, role = "menu", ariaModal, ariaLabel, }) {
14
+ const panelRef = useRef(null);
15
+ const backdropRef = useRef(null);
16
+ const exit = useExitTransition({ exitMs: 80, onClosed: onClose });
17
+ const close = () => void exit.close();
18
+ // Escape and backdrop press dismiss; focus lands on the first item and returns to the opener.
19
+ useDismiss(close, { outside: "click-target", backdrop: backdropRef });
20
+ useFocusTrap(panelRef, true, { autoFocus: false, trap: false });
21
+ const [count, setCount] = useState(0);
22
+ useLayoutEffect(() => {
23
+ const n = menuItems(panelRef.current).length;
24
+ if (n !== count)
25
+ setCount(n);
26
+ });
27
+ const nav = useListNavigation({
28
+ count,
29
+ role: "menu",
30
+ focusOwner: "list",
31
+ homeEnd: false,
32
+ activateKeys: [],
33
+ keepIndexOnItemsChange: true,
34
+ });
35
+ // biome-ignore lint/correctness/useExhaustiveDependencies: re-run once the items are counted so the first item takes focus on open
36
+ useEffect(() => {
37
+ menuItems(panelRef.current)[nav.activeIndex]?.focus();
38
+ }, [nav.activeIndex, count]);
39
+ const { onKeyDown } = nav.getListProps();
40
+ return (_jsxs(_Fragment, { children: [_jsx("div", { ref: backdropRef, className: "no-print fixed inset-0 z-30", "aria-hidden": "true" }), _jsx("div", { ref: panelRef, onKeyDown: onKeyDown, onFocus: (event) => {
41
+ const at = menuItems(panelRef.current).indexOf(event.target);
42
+ if (at >= 0)
43
+ nav.setActiveIndex(at);
44
+ }, role: role, ...(role === "dialog" ? { "aria-modal": ariaModal, "aria-label": ariaLabel } : {}), className: `vr-pop vr-pop-menu-tr${exit.closing ? " vr-pop-exit" : ""} no-print vr-glass-panel absolute z-40 overflow-hidden rounded-lg py-1 ${className}`, children: children })] }));
45
+ }
@@ -0,0 +1,5 @@
1
+ import { type ReactNode } from "react";
2
+ import type { SidebarProps } from "../Sidebar.js";
3
+ export declare function SidebarRail({ activePath, recents, pinnedEntries, onActivate, onOpenRecent, onOpenPath, onClearRecents, onOpenDialog, onOpenUrl, onOpenClipboard, onNew, onOpenSettings, openPaths, setOpen, }: SidebarProps & {
4
+ setOpen: (open: boolean) => void;
5
+ }): ReactNode;
@@ -0,0 +1,37 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // The collapsed sidebar: a thin always-visible rail fused with the titlebar, so the everyday
3
+ // actions never move into the tab strip. Top: expand, New (format/open menu), Recent. Middle:
4
+ // pinned files (one tap opens). Bottom: Settings. Open documents stay in the titlebar tabs.
5
+ import { useRef, useState } from "react";
6
+ import { IconButton } from "@rightkit/react-shell/react";
7
+ import { Gear } from "@phosphor-icons/react/dist/icons/Gear";
8
+ import { FolderSimple } from "@phosphor-icons/react/dist/icons/FolderSimple";
9
+ import { FileTypeIcon } from "../../documents/FileTypeIcon.js";
10
+ import { shortcutLabel } from "@rightkit/document-core/platform";
11
+ import { ClockIcon, PanelIcon, PlusIcon } from "./SidebarIcons.js";
12
+ import { NewOpenMenu, RecentsMenu } from "./SidebarMenus.js";
13
+ const RAIL_WIDTH = 52;
14
+ export function SidebarRail({ activePath, recents, pinnedEntries, onActivate, onOpenRecent, onOpenPath, onClearRecents, onOpenDialog, onOpenUrl, onOpenClipboard, onNew, onOpenSettings, openPaths, setOpen, }) {
15
+ const [menu, setMenu] = useState(null);
16
+ const anchorRef = useRef(null);
17
+ const toggle = (which, button) => {
18
+ anchorRef.current = button;
19
+ setMenu((m) => (m === which ? null : which));
20
+ };
21
+ return (_jsxs("aside", { "data-vr-sidebar": "rail", "aria-label": "Sidebar", className: "no-print vr-chrome vr-sidebar-shell relative flex shrink-0 flex-col items-center bg-panel", style: { width: RAIL_WIDTH }, children: [_jsx("div", { "data-tauri-drag-region": true, className: "h-[52px] w-full shrink-0" }), _jsxs("div", { className: "flex flex-col items-center gap-1", children: [_jsx(RailButton, { label: `Show sidebar (${shortcutLabel("mod+\\")})`, onClick: () => setOpen(true), children: _jsx(PanelIcon, {}) }), _jsx(RailButton, { label: "New document", expanded: menu === "new", onClick: (button) => toggle("new", button), children: _jsx(PlusIcon, {}) }), _jsx(RailButton, { label: "Recent files", expanded: menu === "recents", onClick: (button) => toggle("recents", button), children: _jsx(ClockIcon, {}) })] }), pinnedEntries.length > 0 && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", className: "my-2 h-px w-6 shrink-0 bg-line" }), _jsx("nav", { "aria-label": "Pinned", className: "flex min-h-0 flex-1 flex-col items-center gap-1 overflow-y-auto overflow-x-hidden [direction:rtl] [&>*]:[direction:ltr]", children: pinnedEntries.map((pin) => {
22
+ const isDir = pin.kind === "dir";
23
+ const open = openPaths.has(pin.path);
24
+ return (_jsx(RailButton, { label: pin.displayName, current: pin.path === activePath, onClick: () => {
25
+ // A pinned folder needs the explorer to browse it; a file opens directly.
26
+ if (isDir)
27
+ setOpen(true);
28
+ else if (open)
29
+ onActivate(pin.path);
30
+ else
31
+ onOpenPath(pin.path);
32
+ }, children: isDir ? (_jsx(FolderSimple, { size: 16, weight: "regular", "aria-hidden": true })) : (_jsx(FileTypeIcon, { format: pin.format, path: pin.path, size: 16 })) }, pin.path));
33
+ }) })] })), _jsx("span", { className: "flex-1" }), _jsx("div", { className: "mb-2 flex shrink-0 flex-col items-center", children: _jsx(RailButton, { label: "Settings", onClick: onOpenSettings, children: _jsx(Gear, { size: 15, weight: "regular" }) }) }), menu === "new" && (_jsx(NewOpenMenu, { anchor: anchorRef.current ?? undefined, onClose: () => setMenu(null), onNew: onNew, onOpenDialog: onOpenDialog, onOpenUrl: onOpenUrl, onOpenClipboard: onOpenClipboard })), menu === "recents" && (_jsx(RecentsMenu, { anchor: anchorRef.current ?? undefined, onClose: () => setMenu(null), recents: recents, onOpenRecent: (entry) => void onOpenRecent(entry), onClearRecents: onClearRecents }))] }));
34
+ }
35
+ function RailButton({ label, onClick, expanded, current, children, }) {
36
+ return (_jsx(IconButton, { label: label, onClick: (event) => onClick(event.currentTarget), "aria-expanded": expanded, "aria-current": current ? "page" : undefined, "data-vr-tooltip": label, "data-vr-tooltip-side": "right", className: `vr-icon-tooltip vr-press vr-focus flex h-9 w-9 shrink-0 items-center justify-center rounded-lg text-muted hover:bg-ink/40 hover:text-paper${expanded || current ? " bg-ink/40 text-paper" : ""}`, children: children }));
37
+ }
@@ -0,0 +1,21 @@
1
+ export interface ExplorerRowMenuState {
2
+ x: number;
3
+ y: number;
4
+ name: string;
5
+ path: string;
6
+ isDir: boolean;
7
+ /** Row kind ("dir" | format id | "other") — drives which items are offered. */
8
+ kind: string;
9
+ pinned: boolean;
10
+ /** Show Pin/Unpin. Hidden for "other" files: their paths carry no broker grant, so the backend
11
+ * toggle_pin would refuse them anyway. */
12
+ canPin: boolean;
13
+ /** "Open" handler — expand for dirs, open/activate for files. Absent for "other" rows. */
14
+ onOpen?: () => void;
15
+ }
16
+ export declare function ExplorerRowMenu({ menu, onClose, onTogglePin, onTogglePinDir, }: {
17
+ menu: ExplorerRowMenuState;
18
+ onClose: () => void;
19
+ onTogglePin: (path: string) => void;
20
+ onTogglePinDir: (path: string, label: string) => void;
21
+ }): import("react").JSX.Element;
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // Right-click / "…" context menu for explorer tree rows (locations, listing entries, pinned rows)
3
+ // and recent rows: Open, Reveal in Finder/File Explorer, Pin/Unpin, Copy Path.
4
+ // Same chrome as the document tab menu (SidebarDocumentMenu): the shell's ContextMenu with the
5
+ // glass panel and compact rows.
6
+ import { useMemo, useRef } from "react";
7
+ import { ContextMenu } from "@rightkit/react-shell/react";
8
+ import { explorerReveal } from "../../../state/explorer.js";
9
+ import { isMac } from "@rightkit/document-core/platform";
10
+ import { compactMenuItemClass, menuPanelClass } from "../SidebarPrimitives.js";
11
+ import { copyText } from "../SidebarPathUtils.js";
12
+ export function ExplorerRowMenu({ menu, onClose, onTogglePin, onTogglePinDir, }) {
13
+ // Callbacks (and menu.onOpen) change identity per render; items must not (exitMs treats a new
14
+ // items identity as a re-open), so run() reads the latest through a ref.
15
+ const cb = useRef({ onOpen: menu.onOpen, onTogglePin, onTogglePinDir });
16
+ cb.current = { onOpen: menu.onOpen, onTogglePin, onTogglePinDir };
17
+ const { path, name, isDir, pinned, canPin } = menu;
18
+ const hasOpen = !!menu.onOpen;
19
+ const items = useMemo(() => [
20
+ ...(hasOpen ? [{ id: "open", label: "Open", run: () => cb.current.onOpen?.() }] : []),
21
+ { id: "reveal", label: isMac() ? "Reveal in Finder" : "Reveal in File Explorer", run: () => explorerReveal(path) },
22
+ ...(canPin
23
+ ? [
24
+ {
25
+ id: "pin",
26
+ label: pinned ? "Unpin" : "Pin",
27
+ run: () => (isDir ? cb.current.onTogglePinDir(path, name) : cb.current.onTogglePin(path)),
28
+ },
29
+ ]
30
+ : []),
31
+ { id: "copy-path", label: "Copy Path", separatorBefore: true, run: () => copyText(path) },
32
+ ], [hasOpen, path, name, isDir, pinned, canPin]);
33
+ return (_jsx(ContextMenu, { x: menu.x, y: menu.y, items: items, onClose: onClose, label: `${menu.name} options`, width: 224, itemAs: "button", showLead: false, renderItem: (item) => item.label, className: `${menuPanelClass} overflow-y-auto py-0.5`, scrimClassName: "no-print", exitMs: 80, exitClassName: "vr-pop-exit", slots: {
34
+ item: `${compactMenuItemClass} [&.is-active]:bg-ink/50 [&.is-active]:text-paper`,
35
+ separator: "my-1 h-px bg-line",
36
+ } }));
37
+ }
@@ -0,0 +1,16 @@
1
+ import type { FileEntry, PinEntry } from "../../../state/library.js";
2
+ export interface ExplorerSectionsProps {
3
+ searchQuery?: string;
4
+ activePath: string | null;
5
+ openPaths: ReadonlySet<string>;
6
+ recents: FileEntry[];
7
+ pinnedPaths: ReadonlySet<string>;
8
+ pinnedEntries: PinEntry[];
9
+ onActivate: (path: string) => void;
10
+ onOpenRecent: (entry: FileEntry) => Promise<unknown> | undefined;
11
+ onOpenPath: (path: string) => void;
12
+ onTogglePin: (path: string) => void;
13
+ onTogglePinDir: (path: string, label: string) => void;
14
+ onClearRecents: () => void;
15
+ }
16
+ export declare function ExplorerSections({ searchQuery, activePath, openPaths, recents, pinnedPaths, pinnedEntries, onActivate, onOpenRecent, onOpenPath, onTogglePin, onTogglePinDir, onClearRecents, }: ExplorerSectionsProps): import("react").JSX.Element;
@@ -0,0 +1,259 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // The explorer sidebar body: three collapsible sections — Pinned (dirs first, then files;
3
+ // pinned dirs expand like tree nodes), Recent (first 5 + "Show more…"), and Locations (the lazy
4
+ // directory tree) — inside the sidebar's outer-left-scrollbar nav. Owns section open/closed state,
5
+ // the persisted explorer record, active-file reveal, and the row context menu.
6
+ import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
7
+ import { CaretDown } from "@phosphor-icons/react/dist/icons/CaretDown";
8
+ import { FileTypeIcon } from "../../../documents/FileTypeIcon.js";
9
+ import { baseName, formatFromPath } from "@rightkit/document-core/documents/types";
10
+ import { explorerLocations } from "../../../state/explorer.js";
11
+ import { ancestorDirs, explorerPersistedSync, loadExplorerPersisted, pathInside, saveExplorerPersisted, } from "./explorerState.js";
12
+ import { ExplorerTree, useExplorerTree } from "./ExplorerTree.js";
13
+ import { ExplorerRowMenu } from "./ExplorerRowMenu.js";
14
+ const RECENT_LIMIT = 5;
15
+ function Section({ id, label, count, open, onToggle, children, }) {
16
+ const bodyId = `vr-x-sec-${id}`;
17
+ return (_jsxs("div", { className: `vr-x-sec${open ? "" : " closed"}`, children: [_jsxs("button", { type: "button", className: "vr-x-sec-h vr-press", "aria-expanded": open, "aria-controls": bodyId, onClick: () => onToggle(id), children: [_jsx(CaretDown, { size: 10, weight: "regular", className: "vr-x-sec-caret", "aria-hidden": true }), _jsx("span", { className: "vr-x-sec-label", children: label }), count != null && count > 0 && _jsx("span", { className: "vr-x-sec-count", children: count })] }), _jsx("div", { id: bodyId, className: "vr-x-sec-b", children: children })] }));
18
+ }
19
+ export function ExplorerSections({ searchQuery = "", activePath, openPaths, recents, pinnedPaths, pinnedEntries, onActivate, onOpenRecent, onOpenPath, onTogglePin, onTogglePinDir, onClearRecents, }) {
20
+ const tree = useExplorerTree(activePath);
21
+ const [locations, setLocations] = useState([]);
22
+ const [sections, setSections] = useState({
23
+ pinned: true,
24
+ recent: true,
25
+ locations: true,
26
+ });
27
+ const [hydrated, setHydrated] = useState(false);
28
+ const [showAllRecent, setShowAllRecent] = useState(false);
29
+ const [menu, setMenu] = useState(null);
30
+ const [revealPath, setRevealPath] = useState(null);
31
+ const scrollRef = useRef(null);
32
+ const scrollRestored = useRef(false);
33
+ const hydratedRef = useRef(false);
34
+ const revealAttempts = useRef(0);
35
+ // Initial load: location roots + the persisted explorer record (sections + expanded dirs).
36
+ useEffect(() => {
37
+ let live = true;
38
+ void Promise.all([explorerLocations(), loadExplorerPersisted()]).then(([locs, persisted]) => {
39
+ if (!live)
40
+ return;
41
+ setLocations(locs);
42
+ setSections(persisted.sections);
43
+ tree.expandPaths(persisted.expanded);
44
+ hydratedRef.current = true;
45
+ setHydrated(true);
46
+ });
47
+ return () => {
48
+ live = false;
49
+ };
50
+ // eslint-disable-next-line react-hooks/exhaustive-deps
51
+ }, [tree.expandPaths]);
52
+ // Persist section + expansion changes (debounced inside saveExplorerPersisted); never write
53
+ // before hydration or the defaults would stomp the stored record.
54
+ useEffect(() => {
55
+ if (!hydratedRef.current)
56
+ return;
57
+ saveExplorerPersisted({ sections });
58
+ }, [sections]);
59
+ useEffect(() => {
60
+ if (!hydratedRef.current)
61
+ return;
62
+ saveExplorerPersisted({ expanded: [...tree.expanded] });
63
+ }, [tree.expanded]);
64
+ // Restore scroll position once, after hydration.
65
+ useEffect(() => {
66
+ if (!hydrated || scrollRestored.current)
67
+ return;
68
+ scrollRestored.current = true;
69
+ const el = scrollRef.current;
70
+ const persisted = explorerPersistedSync();
71
+ if (el && persisted.scroll > 0)
72
+ el.scrollTop = persisted.scroll;
73
+ }, [hydrated]);
74
+ const onScroll = useCallback((e) => {
75
+ if (searchQuery.trim())
76
+ return;
77
+ saveExplorerPersisted({ scroll: Math.round(e.currentTarget.scrollTop) });
78
+ }, [searchQuery]);
79
+ const toggleSection = useCallback((id) => {
80
+ setSections((prev) => ({ ...prev, [id]: !prev[id] }));
81
+ }, []);
82
+ // Supported files: focus the tab when already open, else open through the broker-granted path.
83
+ const activateFile = useCallback((path) => {
84
+ if (openPaths.has(path))
85
+ onActivate(path);
86
+ else
87
+ onOpenPath(path);
88
+ }, [openPaths, onActivate, onOpenPath]);
89
+ const activateNode = useCallback((node) => activateFile(node.path), [activateFile]);
90
+ const openMenu = useCallback((node, x, y) => {
91
+ const isDir = node.kind === "dir";
92
+ setMenu({
93
+ x,
94
+ y,
95
+ name: node.name,
96
+ path: node.path,
97
+ isDir,
98
+ kind: node.kind === "file" ? (node.format ?? "file") : node.kind,
99
+ pinned: pinnedPaths.has(node.path),
100
+ canPin: node.kind !== "other",
101
+ onOpen: isDir
102
+ ? () => tree.expandDir(node.path)
103
+ : node.kind === "file"
104
+ ? () => activateFile(node.path)
105
+ : undefined,
106
+ });
107
+ }, [pinnedPaths, tree, activateFile]);
108
+ const openRecentMenu = useCallback((entry, x, y) => {
109
+ setMenu({
110
+ x,
111
+ y,
112
+ name: entry.displayName,
113
+ path: entry.token,
114
+ isDir: false,
115
+ kind: entry.format,
116
+ pinned: pinnedPaths.has(entry.token),
117
+ canPin: true,
118
+ onOpen: () => void onOpenRecent(entry),
119
+ });
120
+ }, [pinnedPaths, onOpenRecent]);
121
+ // Root specs.
122
+ const locationRoots = useMemo(() => locations.map((l) => ({
123
+ path: l.dir,
124
+ name: l.label,
125
+ kind: "dir",
126
+ icon: l.kind,
127
+ })), [locations]);
128
+ const pinnedRoots = useMemo(() => pinnedEntries.map((p) => p.kind === "dir"
129
+ ? { path: p.path, name: p.displayName, kind: "dir" }
130
+ : { path: p.path, name: p.displayName, kind: "file", format: p.format }), [pinnedEntries]);
131
+ const expandPaths = tree.expandPaths;
132
+ // Active-file reveal: expand the ancestor chain under whichever location contains the file,
133
+ // then scroll the row into view once it exists. Expansion is keyed on dir paths, so the chain
134
+ // can be expanded before any level has finished listing.
135
+ useEffect(() => {
136
+ if (!activePath || locations.length === 0) {
137
+ setRevealPath(null);
138
+ return;
139
+ }
140
+ const root = locations.find((l) => pathInside(l.dir, activePath));
141
+ if (!root) {
142
+ setRevealPath(null);
143
+ return;
144
+ }
145
+ expandPaths(ancestorDirs(activePath, root.dir));
146
+ revealAttempts.current = 0;
147
+ setRevealPath(activePath);
148
+ // Keyed on the active file only: depending on the whole tree object re-ran this on every
149
+ // expand/collapse and re-opened the active file's folders the moment the user closed them.
150
+ }, [activePath, locations, expandPaths]);
151
+ // Whenever the tree changes (children listed, dirs expanded), try to land the reveal scroll.
152
+ // biome-ignore lint/correctness/useExhaustiveDependencies: tree.dirs/tree.expanded are deliberate triggers — the reveal retries scrollIntoView as lazy listings land
153
+ useEffect(() => {
154
+ if (!revealPath)
155
+ return;
156
+ const el = scrollRef.current;
157
+ if (!el)
158
+ return;
159
+ const rows = el.querySelectorAll("[data-vr-tree-path]");
160
+ for (const row of rows) {
161
+ if (row.dataset.vrTreePath === revealPath) {
162
+ row.scrollIntoView?.({ block: "nearest" });
163
+ setRevealPath(null);
164
+ return;
165
+ }
166
+ }
167
+ // The row may appear only after several lazy listings; don't retry forever.
168
+ revealAttempts.current += 1;
169
+ if (revealAttempts.current > 12)
170
+ setRevealPath(null);
171
+ }, [revealPath, tree.dirs, tree.expanded]);
172
+ // A pinned file never appears in Recent again — one row per file, Pinned wins.
173
+ const recentFiles = useMemo(() => recents.filter((r) => !pinnedPaths.has(r.token)), [recents, pinnedPaths]);
174
+ const visibleRecents = showAllRecent ? recentFiles : recentFiles.slice(0, RECENT_LIMIT);
175
+ // Filename search reuses known broker entries; it never reads document contents.
176
+ const searchResults = useMemo(() => {
177
+ const files = new Map();
178
+ for (const pin of pinnedEntries) {
179
+ if (pin.kind !== "file")
180
+ continue;
181
+ files.set(pin.path, {
182
+ path: pin.path,
183
+ name: pin.displayName,
184
+ format: pin.format,
185
+ open: () => activateFile(pin.path),
186
+ });
187
+ }
188
+ for (const entry of recents) {
189
+ const path = entry.displayPath ?? entry.token;
190
+ if (files.has(path))
191
+ continue;
192
+ files.set(path, {
193
+ path,
194
+ name: entry.displayName,
195
+ format: entry.format,
196
+ open: () => (openPaths.has(path) ? onActivate(path) : void onOpenRecent(entry)),
197
+ });
198
+ }
199
+ for (const listing of tree.dirs.values()) {
200
+ for (const entry of listing.entries) {
201
+ if (entry.kind === "dir" || entry.kind === "other" || files.has(entry.path))
202
+ continue;
203
+ files.set(entry.path, {
204
+ path: entry.path,
205
+ name: entry.name,
206
+ format: entry.kind,
207
+ open: () => activateFile(entry.path),
208
+ });
209
+ }
210
+ }
211
+ for (const path of openPaths) {
212
+ if (files.has(path) || path.startsWith("untitled:") || path.startsWith("web:"))
213
+ continue;
214
+ files.set(path, {
215
+ path,
216
+ name: baseName(path),
217
+ format: formatFromPath(path),
218
+ open: () => onActivate(path),
219
+ });
220
+ }
221
+ const query = searchQuery.trim().toLocaleLowerCase();
222
+ return [...files.values()].filter((file) => file.name.toLocaleLowerCase().includes(query));
223
+ }, [
224
+ searchQuery,
225
+ pinnedEntries,
226
+ recents,
227
+ tree.dirs,
228
+ openPaths,
229
+ activateFile,
230
+ onActivate,
231
+ onOpenRecent,
232
+ ]);
233
+ // The active file gets exactly ONE highlighted row: the first section that contains it
234
+ // (Pinned → Recent → Locations). Later occurrences fall back to the plain "open in a
235
+ // tab" dot the rows already render for openPaths.
236
+ const activeInPinned = !!activePath &&
237
+ pinnedEntries.some((p) => p.path === activePath || (p.kind === "dir" && pathInside(p.path, activePath)));
238
+ const activeInRecent = !!activePath && recentFiles.some((r) => r.token === activePath);
239
+ const pinnedActivePath = activeInPinned ? activePath : null;
240
+ const recentActivePath = !activeInPinned && activeInRecent ? activePath : null;
241
+ const locationsActivePath = !activeInPinned && !activeInRecent ? activePath : null;
242
+ const onRecentKey = (entry) => (e) => {
243
+ if (e.key !== "Enter" && e.key !== " ")
244
+ return;
245
+ e.preventDefault();
246
+ void onOpenRecent(entry);
247
+ };
248
+ return (_jsxs("nav", { ref: scrollRef, "aria-label": "Files", "data-vr-sidebar-explorer": true, onScroll: onScroll, className: "vr-explorer min-h-0 flex-1 overflow-x-hidden overflow-y-auto font-sans text-paper [direction:rtl] [&>*]:[direction:ltr]", children: [searchQuery.trim() ? (_jsxs("div", { "data-vr-document-search-results": true, children: [_jsx("p", { className: "vr-x-empty", role: "status", children: searchResults.length
249
+ ? `${searchResults.length} ${searchResults.length === 1 ? "document" : "documents"}`
250
+ : "No matching documents" }), searchResults.map((file) => (_jsxs("button", { type: "button", title: file.path, "data-vr-document-search-path": file.path, "aria-current": file.path === activePath || undefined, className: `vr-x-row vr-x-lrow w-full text-left${file.path === activePath ? " vr-x-sel" : ""}`, onClick: file.open, children: [_jsx("span", { className: "vr-x-caret", "aria-hidden": true }), _jsx("span", { className: "vr-x-icn", "aria-hidden": true, children: _jsx(FileTypeIcon, { format: file.format, path: file.path, size: 14 }) }), _jsx("span", { className: "vr-x-name", children: file.name })] }, file.path)))] })) : (_jsxs(_Fragment, { children: [pinnedRoots.length > 0 && (_jsx(Section, { id: "pinned", label: "Pinned", count: pinnedEntries.length, open: sections.pinned, onToggle: toggleSection, children: _jsx(ExplorerTree, { ariaLabel: "Pinned", roots: pinnedRoots, tree: tree, activePath: pinnedActivePath, openPaths: openPaths, onActivateRow: activateNode, onOpenMenu: openMenu }) })), _jsx(Section, { id: "recent", label: "Recent", count: recentFiles.length, open: sections.recent, onToggle: toggleSection, children: recentFiles.length === 0 ? (_jsx("div", { className: "vr-x-empty", children: "No recent files" })) : (_jsxs(_Fragment, { children: [visibleRecents.map((r) => {
251
+ const open = r.token !== recentActivePath && openPaths.has(r.token);
252
+ return (_jsxs("div", { role: "button", tabIndex: 0, title: r.token, "aria-label": r.displayName, "aria-description": open ? `${r.token} (open in a tab)` : r.token, "data-vr-recent-path": r.token, "aria-current": r.token === recentActivePath || undefined, className: `vr-x-row vr-x-lrow${r.token === recentActivePath ? " vr-x-sel" : ""}`, onClick: () => void onOpenRecent(r), onKeyDown: onRecentKey(r), onContextMenu: (e) => {
253
+ e.preventDefault();
254
+ openRecentMenu(r, e.clientX, e.clientY);
255
+ }, children: [_jsx("span", { className: "vr-x-caret", "aria-hidden": true }), _jsx("span", { className: "vr-x-icn", "aria-hidden": true, children: _jsx(FileTypeIcon, { format: r.format, path: r.token, size: 14 }) }), _jsx("span", { className: "vr-x-name", children: r.displayName }), open && _jsx("span", { className: "vr-x-open", title: "Open in a tab", "aria-hidden": true })] }, r.token));
256
+ }), recentFiles.length > RECENT_LIMIT && (_jsx("button", { type: "button", className: "vr-x-more vr-press vr-focus", onClick: () => setShowAllRecent((v) => !v), children: showAllRecent
257
+ ? "Show less"
258
+ : `Show ${recentFiles.length - RECENT_LIMIT} more…` })), _jsx("button", { type: "button", className: "vr-x-more vr-press vr-focus", onClick: onClearRecents, children: "Clear recent files" })] })) }), _jsx(Section, { id: "locations", label: "Locations", open: sections.locations, onToggle: toggleSection, children: locationRoots.length === 0 ? (_jsx("div", { className: "vr-x-empty", children: "No locations" })) : (_jsx(ExplorerTree, { ariaLabel: "Locations", roots: locationRoots, tree: tree, activePath: locationsActivePath, revealPath: revealPath, openPaths: openPaths, onActivateRow: activateNode, onOpenMenu: openMenu })) })] })), menu && (_jsx(ExplorerRowMenu, { menu: menu, onClose: () => setMenu(null), onTogglePin: onTogglePin, onTogglePinDir: onTogglePinDir }))] }));
259
+ }
@@ -0,0 +1,46 @@
1
+ import { type ExplorerEntry } from "../../../state/explorer.js";
2
+ /** A node the tree can render: a root (location / pinned dir / pinned file) or a listing entry. */
3
+ export interface ExplorerNodeSpec {
4
+ path: string;
5
+ name: string;
6
+ kind: "dir" | "file" | "other";
7
+ /** Format id for file rows (FileTypeIcon); undefined for dirs/others. */
8
+ format?: string;
9
+ /** Location kind ("home", "desktop", …) driving the root icon; undefined for plain dirs. */
10
+ icon?: string;
11
+ }
12
+ export interface ExplorerDirListing {
13
+ status: "loading" | "ready" | "error";
14
+ entries: ExplorerEntry[];
15
+ truncated: boolean;
16
+ message?: string;
17
+ }
18
+ export interface ExplorerTreeApi {
19
+ /** Dir paths currently expanded (persisted under vr.explorer.v1). */
20
+ expanded: ReadonlySet<string>;
21
+ /** dir path → latest listing state. */
22
+ dirs: ReadonlyMap<string, ExplorerDirListing>;
23
+ expandDir: (path: string) => void;
24
+ collapseDir: (path: string) => void;
25
+ toggleDir: (path: string) => void;
26
+ expandPaths: (paths: Iterable<string>) => void;
27
+ /** Force a re-list of a dir (watcher events; also retries after an error). */
28
+ relistDir: (path: string) => void;
29
+ }
30
+ /** `wantedPath` is the active file; a folder on its way is listed with the next segment's name so
31
+ * the backend keeps that row even past its entry cap (otherwise the reveal stops at that folder). */
32
+ export declare function useExplorerTree(wantedPath?: string | null): ExplorerTreeApi;
33
+ /** One ARIA tree (roving tabindex, arrow keys) over a set of root nodes. Used for the Locations
34
+ * section (roots = explorer_locations) and for the Pinned section (roots = pinned dirs+files). */
35
+ export declare function ExplorerTree({ ariaLabel, roots, tree, activePath, revealPath, openPaths, onActivateRow, onOpenMenu, }: {
36
+ ariaLabel: string;
37
+ roots: ExplorerNodeSpec[];
38
+ tree: ExplorerTreeApi;
39
+ activePath: string | null;
40
+ /** File the sidebar is currently scrolling to (see ExplorerSections' active-file reveal). */
41
+ revealPath?: string | null;
42
+ openPaths: ReadonlySet<string>;
43
+ /** Leaf activation — supported file rows only (dirs toggle internally). */
44
+ onActivateRow: (node: ExplorerNodeSpec) => void;
45
+ onOpenMenu: (node: ExplorerNodeSpec, x: number, y: number) => void;
46
+ }): import("react").JSX.Element;