@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.
Files changed (262) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE-APACHE +201 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +74 -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,36 @@
1
+ import { type ReactNode } from "react";
2
+ import type { FileEntry, PinEntry } from "../state/library.js";
3
+ export interface SidebarProps {
4
+ activePath: string | null;
5
+ /** Paths of documents that currently have a tab — used for "open in a tab" row styling and for
6
+ * activate-instead-of-open when a pinned/explorer file is already open. */
7
+ openPaths: ReadonlySet<string>;
8
+ recents: FileEntry[];
9
+ /** File+dir pins (integrator keeps it current, dirs included). */
10
+ pinnedPaths: ReadonlySet<string>;
11
+ /** Full pin rows for the Pinned section (dirs first, as the backend returns them). */
12
+ pinnedEntries: PinEntry[];
13
+ /** Focus an already-open doc's tab. */
14
+ onActivate: (path: string) => void;
15
+ /** Open a recents entry — same path the recents flow uses (token-based). */
16
+ onOpenRecent: (entry: FileEntry) => Promise<unknown> | undefined;
17
+ /** Open a broker-granted explorer path (integrator makes it open-or-activate). */
18
+ onOpenPath: (path: string) => void;
19
+ /** FILE pin toggle (existing app callback). */
20
+ onTogglePin: (path: string) => void;
21
+ /** DIRECTORY pin toggle. */
22
+ onTogglePinDir: (path: string, label: string) => void;
23
+ onClearRecents: () => void;
24
+ onOpenDialog: () => void;
25
+ onOpenUrl: () => void;
26
+ onOpenClipboard: () => void;
27
+ onNew: () => void;
28
+ onOpenSettings: () => void;
29
+ onCollapsedChange: (collapsed: boolean) => void;
30
+ /** In-window menus (Windows/Linux File · Edit · View) shown at the left of the sidebar's
31
+ * titlebar row while the sidebar is open — the window's far left, like macOS traffic lights. */
32
+ titlebarSlot?: ReactNode;
33
+ }
34
+ export declare const Sidebar: import("react").MemoExoticComponent<typeof SidebarImpl>;
35
+ declare function SidebarImpl(props: SidebarProps): import("react").JSX.Element | null;
36
+ export {};
@@ -0,0 +1,121 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // The sidebar shell. Open documents moved to the titlebar tab strip — the sidebar is now a file
3
+ // explorer (Pinned / Recent / Locations). Collapsed shows a thin rail (expand, New, Recent, pinned
4
+ // files, Settings); "viewright:toggle-sidebar" + "vr.sidebar.pinned" still drive it.
5
+ import { memo, useEffect, useRef, useState, } from "react";
6
+ import { useMediaQuery } from "@rightkit/react-shell/react";
7
+ import { loadExplorerPersisted, saveExplorerPersisted } from "./sidebar/explorer/explorerState.js";
8
+ import { SidebarExpandedPanel } from "./sidebar/SidebarExpandedPanel.js";
9
+ import { SidebarRail } from "./sidebar/SidebarRail.js";
10
+ const SIDEBAR_DEFAULT_WIDTH = 256;
11
+ const SIDEBAR_MIN_WIDTH = 224;
12
+ const SIDEBAR_MAX_WIDTH = 420;
13
+ const SIDEBAR_COLLAPSE_WIDTH = 80;
14
+ function initialSidebarWidth() {
15
+ if (typeof localStorage === "undefined")
16
+ return SIDEBAR_DEFAULT_WIDTH;
17
+ const raw = localStorage.getItem("vr.sidebar.width");
18
+ if (raw == null)
19
+ return SIDEBAR_DEFAULT_WIDTH;
20
+ const stored = Number(raw);
21
+ if (!Number.isFinite(stored))
22
+ return SIDEBAR_DEFAULT_WIDTH;
23
+ return Math.max(SIDEBAR_MIN_WIDTH, Math.min(SIDEBAR_MAX_WIDTH, stored));
24
+ }
25
+ // Memoized: App re-renders on every keystroke (edits state), but the sidebar's inputs don't change
26
+ // on a keystroke, so it must skip that re-render instead of reconciling the whole explorer tree.
27
+ export const Sidebar = memo(SidebarImpl);
28
+ function SidebarImpl(props) {
29
+ const { onCollapsedChange } = props;
30
+ const widthDrag = useRef(null);
31
+ const [sidebarWidth, setSidebarWidth] = useState(initialSidebarWidth);
32
+ // Manual open/close only — no hover auto-expand. Collapsed = hidden entirely (no rail).
33
+ const [open, setOpen] = useState(() => localStorage.getItem("vr.sidebar.pinned") !== "0");
34
+ const compact = useMediaQuery("(max-width: 640px)");
35
+ const collapsed = !open || compact;
36
+ useEffect(() => {
37
+ localStorage.setItem("vr.sidebar.pinned", open ? "1" : "0");
38
+ }, [open]);
39
+ // Ctrl+E (App dispatches this) toggles the sidebar from anywhere in the app.
40
+ useEffect(() => {
41
+ const toggle = () => setOpen((o) => !o);
42
+ window.addEventListener("viewright:toggle-sidebar", toggle);
43
+ return () => window.removeEventListener("viewright:toggle-sidebar", toggle);
44
+ }, []);
45
+ useEffect(() => {
46
+ onCollapsedChange?.(collapsed);
47
+ }, [collapsed, onCollapsedChange]);
48
+ // Hydrate the width from the explorer settings mirror only when localStorage has nothing —
49
+ // localStorage always wins so a migrated/reset profile can't stomp the fast path.
50
+ useEffect(() => {
51
+ let live = true;
52
+ void loadExplorerPersisted().then((persisted) => {
53
+ if (!live)
54
+ return;
55
+ if (localStorage.getItem("vr.sidebar.width") != null)
56
+ return;
57
+ if (persisted.width >= SIDEBAR_MIN_WIDTH && persisted.width <= SIDEBAR_MAX_WIDTH) {
58
+ setSidebarWidth(persisted.width);
59
+ }
60
+ });
61
+ return () => {
62
+ live = false;
63
+ };
64
+ }, []);
65
+ const startWidthResize = (event) => {
66
+ event.preventDefault();
67
+ event.currentTarget.setPointerCapture?.(event.pointerId);
68
+ widthDrag.current = {
69
+ pointerId: event.pointerId,
70
+ startX: event.clientX,
71
+ startWidth: sidebarWidth,
72
+ };
73
+ document.body.style.cursor = "col-resize";
74
+ document.body.style.userSelect = "none";
75
+ };
76
+ useEffect(() => {
77
+ const onMove = (event) => {
78
+ const drag = widthDrag.current;
79
+ if (!drag || event.pointerId !== drag.pointerId)
80
+ return;
81
+ const raw = drag.startWidth + event.clientX - drag.startX;
82
+ if (raw <= SIDEBAR_COLLAPSE_WIDTH) {
83
+ setOpen(false);
84
+ return;
85
+ }
86
+ const next = Math.max(SIDEBAR_MIN_WIDTH, Math.min(SIDEBAR_MAX_WIDTH, raw));
87
+ setSidebarWidth(next);
88
+ drag.lastWidth = next;
89
+ };
90
+ const finish = (event) => {
91
+ const drag = widthDrag.current;
92
+ if (!drag || event.pointerId !== drag.pointerId)
93
+ return;
94
+ widthDrag.current = null;
95
+ if (drag.lastWidth !== undefined) {
96
+ const width = Math.round(drag.lastWidth);
97
+ localStorage.setItem("vr.sidebar.width", String(width));
98
+ saveExplorerPersisted({ width });
99
+ }
100
+ document.body.style.cursor = "";
101
+ document.body.style.userSelect = "";
102
+ };
103
+ window.addEventListener("pointermove", onMove);
104
+ window.addEventListener("pointerup", finish);
105
+ window.addEventListener("pointercancel", finish);
106
+ return () => {
107
+ window.removeEventListener("pointermove", onMove);
108
+ window.removeEventListener("pointerup", finish);
109
+ window.removeEventListener("pointercancel", finish);
110
+ if (widthDrag.current) {
111
+ document.body.style.cursor = "";
112
+ document.body.style.userSelect = "";
113
+ }
114
+ widthDrag.current = null;
115
+ };
116
+ }, []);
117
+ // Collapsed = the thin rail (narrow/compact windows hide the sidebar entirely).
118
+ if (collapsed)
119
+ return compact ? null : _jsx(SidebarRail, { ...props, setOpen: setOpen });
120
+ return (_jsx(SidebarExpandedPanel, { ...props, sidebarWidth: sidebarWidth, startWidthResize: startWidthResize, setOpen: setOpen, SIDEBAR_MIN_WIDTH: SIDEBAR_MIN_WIDTH, SIDEBAR_MAX_WIDTH: SIDEBAR_MAX_WIDTH }));
121
+ }
@@ -0,0 +1 @@
1
+ export declare function ToastViewport(): import("react").JSX.Element;
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CheckCircle } from "@phosphor-icons/react/dist/icons/CheckCircle";
3
+ import { Info } from "@phosphor-icons/react/dist/icons/Info";
4
+ import { WarningCircle } from "@phosphor-icons/react/dist/icons/WarningCircle";
5
+ import { defaultToastBus } from "@rightkit/platform-ui/headless";
6
+ import { ToastViewport as ShellToastViewport, } from "@rightkit/react-shell/react";
7
+ const KIND_ICON = {
8
+ info: Info,
9
+ success: CheckCircle,
10
+ warning: WarningCircle,
11
+ error: WarningCircle,
12
+ };
13
+ // the host app row markup, unchanged. The shell supplies the timers, bus subscription, stack cap
14
+ // and exit timing; `leaving` ignores a second dismiss during the exit animation, as before.
15
+ function renderToastRow({ toast, leaving, dismiss, role }) {
16
+ const KindIcon = KIND_ICON[toast.kind];
17
+ return (_jsxs("div", { role: role, className: `vr-toast-row${leaving ? " vr-toast-exit" : ""} vr-capsule vr-over-content pointer-events-auto h-10 w-fit max-w-full gap-2.5 pl-4 pr-2 font-sans text-chrome-md font-medium text-paper`, children: [_jsx(KindIcon, { size: 16, weight: "regular", "aria-hidden": "true", className: "shrink-0 text-copper" }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: toast.message }), toast.action && (_jsx("button", { type: "button", onClick: () => {
18
+ if (leaving)
19
+ return;
20
+ toast.action?.run();
21
+ dismiss();
22
+ }, className: "vr-press vr-focus flex h-6 shrink-0 items-center rounded-full px-2.5 text-copper hover:bg-ink/40 hover:text-paper", children: toast.action.label })), _jsx("button", { type: "button", onClick: () => {
23
+ if (!leaving)
24
+ dismiss();
25
+ }, "aria-label": "Dismiss notification", className: "vr-press vr-focus flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-muted-2 hover:bg-ink/40 hover:text-paper", children: "\u2715" })] }));
26
+ }
27
+ // Single mount point for the toast bus. Centred bottom capsules; the live region announces new
28
+ // toasts to assistive tech (polite for info/success, the error rows carry role="alert").
29
+ export function ToastViewport() {
30
+ return (_jsx(ShellToastViewport, { bus: defaultToastBus, className: "no-print pointer-events-none fixed bottom-4 left-1/2 z-[95] flex max-h-[calc(100vh-2rem)] w-[min(24rem,calc(100vw-2rem))] -translate-x-1/2 flex-col items-center gap-2 overflow-y-auto", roles: { error: "alert", info: null, success: null, warning: null }, renderRow: renderToastRow }));
31
+ }
@@ -0,0 +1,26 @@
1
+ import { type ReactNode } from "react";
2
+ import type { LoadedDocument } from "@rightkit/document-core/documents/types";
3
+ import { type TabStripProps } from "./tabs/TabStrip.js";
4
+ interface Props {
5
+ doc: LoadedDocument | null;
6
+ title: string;
7
+ menuSlot?: ReactNode;
8
+ modeSlot?: ReactNode;
9
+ /** Centre-of-toolbar slot (Wave 2 Read/Edit control) — floats inside the drag region. */
10
+ centerSlot?: ReactNode;
11
+ sidebarCollapsed?: boolean;
12
+ compute?: (doc: LoadedDocument) => string;
13
+ subscribeEdits?: (path: string, cb: () => void) => () => void;
14
+ getEditSnapshot?: (path: string) => string | undefined;
15
+ isDirty?: (doc: LoadedDocument) => boolean;
16
+ dirtyPaths?: ReadonlySet<string>;
17
+ subscribeDirty?: (path: string, cb: () => void) => () => void;
18
+ /** Titlebar tab strip (Part A tabs+explorer): when provided, the middle stretch renders
19
+ * open-document tabs instead of empty drag space, and the left title block is dropped —
20
+ * the strip's active tab carries the document name. Absent = standalone/test layout. */
21
+ tabs?: TabStripProps;
22
+ }
23
+ export declare const KIND_LABEL: Record<LoadedDocument["format"], string>;
24
+ export declare const Toolbar: import("react").MemoExoticComponent<typeof ToolbarImpl>;
25
+ declare function ToolbarImpl({ doc, title, menuSlot, modeSlot, centerSlot, sidebarCollapsed, compute, subscribeEdits, getEditSnapshot, isDirty, dirtyPaths, subscribeDirty, tabs, }: Props): import("react").JSX.Element;
26
+ export {};
@@ -0,0 +1,63 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { memo } from "react";
3
+ import { createWindowControlLabels } from "@rightkit/platform-ui";
4
+ import { usePlatform } from "@rightkit/platform-ui/react";
5
+ import { Titlebar } from "@rightkit/react-shell/react";
6
+ import { getCurrentWindow } from "@tauri-apps/api/window";
7
+ import { inTauri } from "@rightkit/document-core/state/tauriBridge";
8
+ import { LiveTitle } from "./LiveTitle.js";
9
+ import { useTabDirty } from "./tabs/useTabDirty.js";
10
+ import { TabStrip } from "./tabs/TabStrip.js";
11
+ const NO_DIRTY = new Set();
12
+ // Subtitle kind labels — the human format names from the Wave 1 title-bar spec. Exported for the
13
+ // Wave 3 Info inspector, which must show the same "Kind" the title subtitle shows.
14
+ export const KIND_LABEL = {
15
+ md: "Markdown",
16
+ mdx: "Markdown",
17
+ txt: "Text",
18
+ pdf: "PDF Document",
19
+ docx: "Word Document",
20
+ html: "HTML",
21
+ image: "Image",
22
+ web: "Web Preview",
23
+ unknown: "Document",
24
+ };
25
+ const windowAdapter = {
26
+ minimize: () => getCurrentWindow().minimize(),
27
+ toggleMaximize: () => getCurrentWindow().toggleMaximize(),
28
+ close: () => getCurrentWindow().close(),
29
+ };
30
+ const windowControlLabels = createWindowControlLabels({
31
+ minimize: "Minimize",
32
+ maximize: "Maximize",
33
+ close: "Close",
34
+ });
35
+ // Title bar: document menus, the left-aligned document title block (name over kind/dirty subtitle),
36
+ // and window controls. On macOS the sidebar tells us when the native traffic lights are inside the
37
+ // content titlebar; only that collapsed state needs padding.
38
+ export const Toolbar = memo(ToolbarImpl);
39
+ function ToolbarImpl({ doc, title, menuSlot, modeSlot, centerSlot, sidebarCollapsed = false, compute, subscribeEdits, getEditSnapshot, isDirty, dirtyPaths, subscribeDirty, tabs, }) {
40
+ const mac = usePlatform() === "macos";
41
+ // Collapsed-mac traffic-light clearance: standalone mode pads the left menu cluster;
42
+ // with tabs, the strip's own leading cluster carries that inset instead.
43
+ const menuPadding = mac && sidebarCollapsed && !tabs ? "pl-24" : "pl-3";
44
+ const titleNode = doc && compute && subscribeEdits && getEditSnapshot ? (_jsx(LiveTitle, { doc: doc, compute: compute, subscribeEdits: subscribeEdits, getEditSnapshot: getEditSnapshot })) : (title);
45
+ const hasTitle = !!(tabs || centerSlot);
46
+ // `Titlebar` owns the drag-region header, the macOS traffic-light slot (hidden here: the sidebar,
47
+ // or the tab strip's lead spacer, already clears the lights) and the Windows/Linux caption buttons
48
+ // (drawn only in a native window; macOS has its own overlay title bar).
49
+ return (_jsx(Titlebar, { className: "no-print vr-chrome relative bg-panel", startWidth: "auto", windowControls: inTauri() ? windowAdapter : undefined, windowControlLabels: windowControlLabels, slots: {
50
+ side: `relative z-10 min-w-0 ${menuPadding}`,
51
+ lights: "hidden",
52
+ title: "relative h-full min-w-3 flex-1 self-stretch",
53
+ drag: hasTitle ? "hidden" : "",
54
+ end: "relative z-10",
55
+ }, start: _jsxs(_Fragment, { children: [(!tabs || sidebarCollapsed) && menuSlot, doc && !tabs ? (_jsx(ToolbarTitle, { doc: doc, titleNode: titleNode, isDirty: isDirty, dirtyPaths: dirtyPaths, subscribeDirty: subscribeDirty })) : null] }), title: hasTitle ? (_jsxs(_Fragment, { children: [tabs ? _jsx(TabStrip, { ...tabs }) : null, centerSlot ? (_jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center", children: _jsx("div", { className: "pointer-events-auto", children: centerSlot }) })) : null] })) : undefined, end: modeSlot ? _jsx("div", { className: "mr-3 flex items-center", children: modeSlot }) : undefined }));
56
+ }
57
+ // Left-aligned document title: 15px semibold name over an 11px muted `<Kind> — Edited` subtitle.
58
+ // Subscribes to per-tab dirty changes so text-buffer edits flip "— Edited" without an App render.
59
+ function ToolbarTitle({ doc, titleNode, isDirty, dirtyPaths, subscribeDirty, }) {
60
+ const dirty = useTabDirty(doc, (dirtyPaths ?? NO_DIRTY).has(doc.path), isDirty ?? (() => false), subscribeDirty ?? (() => () => { }));
61
+ const kind = KIND_LABEL[doc.format] ?? "Document";
62
+ return (_jsxs("div", { className: "ml-2 flex min-w-0 flex-col justify-center leading-tight", "data-vr-doc-title": true, children: [_jsx("span", { className: "truncate font-sans text-[15px] font-semibold text-paper", children: titleNode }), _jsxs("span", { className: "truncate font-sans text-chrome-xs text-muted", children: [kind, dirty ? " — Edited" : ""] })] }));
63
+ }
@@ -0,0 +1,7 @@
1
+ import type { ImageSaveFunction } from "@rightkit/media-ui";
2
+ type MutableCurrent<T> = {
3
+ current: T;
4
+ };
5
+ type DocxSaveFunction = () => Promise<ArrayBuffer | null>;
6
+ export declare function syncActiveFormatSavers(activePath: string | null, docxSaver: MutableCurrent<DocxSaveFunction | null>, imageSaver: MutableCurrent<ImageSaveFunction | null>, docxSavers: ReadonlyMap<string, DocxSaveFunction>, imageSavers: ReadonlyMap<string, ImageSaveFunction>): void;
7
+ export {};
@@ -0,0 +1,4 @@
1
+ export function syncActiveFormatSavers(activePath, docxSaver, imageSaver, docxSavers, imageSavers) {
2
+ docxSaver.current = activePath ? (docxSavers.get(activePath) ?? null) : null;
3
+ imageSaver.current = activePath ? (imageSavers.get(activePath) ?? null) : null;
4
+ }
@@ -0,0 +1 @@
1
+ export declare function localPathFromClipboardText(text: string): string | null;
@@ -0,0 +1,31 @@
1
+ import { formatFromPath } from "@rightkit/document-core/documents/types";
2
+ export function localPathFromClipboardText(text) {
3
+ const trimmed = text.trim();
4
+ if (!trimmed || /\r|\n/.test(trimmed))
5
+ return null;
6
+ const unquoted = trimmed.replace(/^(['"`])(.+)\1$/, "$2").trim();
7
+ const path = fileUrlToLocalPath(unquoted) ?? unquoted;
8
+ const isWindowsDrive = /^[a-z]:[\\/].+/i.test(path);
9
+ const isUnc = /^\\\\[^\\\/]+[\\\/][^\\\/]+[\\\/].+/.test(path);
10
+ const isPosixAbsolute = /^\/[^/].+/.test(path);
11
+ if (!isWindowsDrive && !isUnc && !isPosixAbsolute)
12
+ return null;
13
+ return formatFromPath(path) === "unknown" ? null : path;
14
+ }
15
+ function fileUrlToLocalPath(input) {
16
+ let url;
17
+ try {
18
+ url = new URL(input);
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ if (url.protocol !== "file:")
24
+ return null;
25
+ const decoded = decodeURIComponent(url.pathname);
26
+ if (/^\/[a-z]:\//i.test(decoded))
27
+ return decoded.slice(1).replace(/\//g, "\\");
28
+ if (url.hostname)
29
+ return `\\\\${url.hostname}${decoded.replace(/\//g, "\\")}`;
30
+ return decoded;
31
+ }
@@ -0,0 +1,17 @@
1
+ export declare const draftKey: (path: string) => string;
2
+ export declare function readDraft(path: string): string | null;
3
+ export declare function writeDraft(path: string, text: string): void;
4
+ export declare function clearDraft(path: string): void;
5
+ export interface FileDraft {
6
+ base: string;
7
+ text: string;
8
+ }
9
+ export declare function readFileDraft(path: string): FileDraft | null;
10
+ export declare function writeFileDraft(path: string, draft: FileDraft): void;
11
+ export declare function clearFileDraft(path: string): void;
12
+ export declare function writeDraftDebounced(path: string, text: string): void;
13
+ export declare function writeFileDraftDebounced(path: string, draft: FileDraft): void;
14
+ /** Synchronously commit path's pending debounced write (if any) right now. Idempotent/no-op if nothing pending. */
15
+ export declare function flushDraft(path: string): void;
16
+ /** Synchronously commit every pending debounced draft write. Call before the process may exit. */
17
+ export declare function flushDrafts(): void;
@@ -0,0 +1,104 @@
1
+ // ── Debounced writers (T2-1) ────────────────────────────────────────────────────────────────────
2
+ // writeDraft/writeFileDraft below are synchronous localStorage.setItem calls — JSON.stringify-ing the
3
+ // whole document on every keystroke is O(doc) blocking I/O per char. writeDraftDebounced/
4
+ // writeFileDraftDebounced (further down) are what onTxtChange calls instead: they coalesce rapid
5
+ // keystrokes into one write ~400ms after typing pauses (matches the useViewState house debounce),
6
+ // keyed per-path so two files being edited in quick succession never clobber each other's pending
7
+ // write. The pending-write maps live up top so clearDraft/clearFileDraft can cancel them.
8
+ //
9
+ // Data-loss guardrails:
10
+ // - clearDraft/clearFileDraft cancel any pending write for that path before removing the key, so a
11
+ // save/discard can never be undone by a stale debounced write landing afterward.
12
+ // - flushDraft/flushDrafts synchronously commit a pending write immediately; callers invoke these
13
+ // before an operation that may read the draft back or before the process may exit (save, close,
14
+ // beforeunload/visibilitychange-hidden) so no in-flight edit is silently dropped.
15
+ const DRAFT_DEBOUNCE_MS = 400;
16
+ const untitledPending = new Map();
17
+ const fileDraftPending = new Map();
18
+ export const draftKey = (path) => `vr.draft.${path}`;
19
+ export function readDraft(path) {
20
+ return localStorage.getItem(draftKey(path));
21
+ }
22
+ export function writeDraft(path, text) {
23
+ localStorage.setItem(draftKey(path), text);
24
+ }
25
+ export function clearDraft(path) {
26
+ const pending = untitledPending.get(path);
27
+ if (pending) {
28
+ clearTimeout(pending.timer);
29
+ untitledPending.delete(path);
30
+ }
31
+ localStorage.removeItem(draftKey(path));
32
+ }
33
+ const fileDraftKey = (path) => `vr.filedraft.${path}`;
34
+ export function readFileDraft(path) {
35
+ try {
36
+ const raw = localStorage.getItem(fileDraftKey(path));
37
+ if (!raw)
38
+ return null;
39
+ const d = JSON.parse(raw);
40
+ if (typeof d.base === "string" && typeof d.text === "string") {
41
+ return { base: d.base, text: d.text };
42
+ }
43
+ return null;
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ export function writeFileDraft(path, draft) {
50
+ try {
51
+ localStorage.setItem(fileDraftKey(path), JSON.stringify(draft));
52
+ }
53
+ catch {
54
+ /* storage full — skip */
55
+ }
56
+ }
57
+ export function clearFileDraft(path) {
58
+ const pending = fileDraftPending.get(path);
59
+ if (pending) {
60
+ clearTimeout(pending.timer);
61
+ fileDraftPending.delete(path);
62
+ }
63
+ localStorage.removeItem(fileDraftKey(path));
64
+ }
65
+ export function writeDraftDebounced(path, text) {
66
+ const prior = untitledPending.get(path);
67
+ if (prior)
68
+ clearTimeout(prior.timer);
69
+ const timer = setTimeout(() => {
70
+ untitledPending.delete(path);
71
+ writeDraft(path, text);
72
+ }, DRAFT_DEBOUNCE_MS);
73
+ untitledPending.set(path, { timer, text });
74
+ }
75
+ export function writeFileDraftDebounced(path, draft) {
76
+ const prior = fileDraftPending.get(path);
77
+ if (prior)
78
+ clearTimeout(prior.timer);
79
+ const timer = setTimeout(() => {
80
+ fileDraftPending.delete(path);
81
+ writeFileDraft(path, draft);
82
+ }, DRAFT_DEBOUNCE_MS);
83
+ fileDraftPending.set(path, { timer, draft });
84
+ }
85
+ /** Synchronously commit path's pending debounced write (if any) right now. Idempotent/no-op if nothing pending. */
86
+ export function flushDraft(path) {
87
+ const untitled = untitledPending.get(path);
88
+ if (untitled) {
89
+ clearTimeout(untitled.timer);
90
+ untitledPending.delete(path);
91
+ writeDraft(path, untitled.text);
92
+ }
93
+ const file = fileDraftPending.get(path);
94
+ if (file) {
95
+ clearTimeout(file.timer);
96
+ fileDraftPending.delete(path);
97
+ writeFileDraft(path, file.draft);
98
+ }
99
+ }
100
+ /** Synchronously commit every pending debounced draft write. Call before the process may exit. */
101
+ export function flushDrafts() {
102
+ for (const path of new Set([...untitledPending.keys(), ...fileDraftPending.keys()]))
103
+ flushDraft(path);
104
+ }
@@ -0,0 +1,5 @@
1
+ import type { BlockDraftDecision, BlockDraftIntent, BlockDraftRegistry } from "@rightkit/document-core/app/blockDrafts";
2
+ /** Resolve every modified draft for one document in order; first cancellation/failure wins. */
3
+ export declare function resolveDocumentDrafts(registry: BlockDraftRegistry, documentPath: string, intent: BlockDraftIntent, decision: BlockDraftDecision): Promise<"resolved" | "cancelled" | "failed">;
4
+ /** Resolve every modified draft in the registry (close-all / quit / setup replacement). */
5
+ export declare function resolveAllDrafts(registry: BlockDraftRegistry, intent: BlockDraftIntent, decision: BlockDraftDecision): Promise<"resolved" | "cancelled" | "failed">;
@@ -0,0 +1,25 @@
1
+ /** Resolve every modified draft for one document in order; first cancellation/failure wins. */
2
+ export async function resolveDocumentDrafts(registry, documentPath, intent, decision) {
3
+ const drafts = registry.snapshotModified(documentPath);
4
+ for (const draft of drafts) {
5
+ const result = await registry.resolve(draft.identity, intent, decision);
6
+ if (result.outcome === "cancelled")
7
+ return "cancelled";
8
+ if (result.outcome === "failed")
9
+ return "failed";
10
+ // done / discarded / missing → continue to the next draft
11
+ }
12
+ return "resolved";
13
+ }
14
+ /** Resolve every modified draft in the registry (close-all / quit / setup replacement). */
15
+ export async function resolveAllDrafts(registry, intent, decision) {
16
+ const drafts = registry.snapshotModified();
17
+ for (const draft of drafts) {
18
+ const result = await registry.resolve(draft.identity, intent, decision);
19
+ if (result.outcome === "cancelled")
20
+ return "cancelled";
21
+ if (result.outcome === "failed")
22
+ return "failed";
23
+ }
24
+ return "resolved";
25
+ }
@@ -0,0 +1,8 @@
1
+ import type { LoadedDocument } from "@rightkit/document-core/documents/types";
2
+ import type { MarkdownExportApi } from "@rightkit/markdown-editor/documents/markdown/export/markdownExportTypes";
3
+ /** Command-palette "Export as…" action for md/mdx, or null for other formats. */
4
+ export declare function buildExportMarkdown(active: LoadedDocument | null, markdownExport: MarkdownExportApi | null, activeText: string): ((target: "html" | "txt") => void) | null;
5
+ /** Command-palette "Copy rendered" action for md/mdx, or null for other formats. */
6
+ export declare function buildCopyRendered(active: LoadedDocument | null, markdownExport: MarkdownExportApi | null, activeText: string): (() => void) | null;
7
+ /** Command-palette "Copy as Markdown" for PDF and DOCX (converted from document text/model). */
8
+ export declare function buildCopyAsMarkdown(active: LoadedDocument | null): (() => void) | null;
@@ -0,0 +1,107 @@
1
+ import { toast } from "@rightkit/platform-ui/headless";
2
+ import { isMarkdownFormat } from "../documents/capabilities.js";
3
+ import { baseName } from "@rightkit/document-core/documents/types";
4
+ import { saveFileDialog, writeDocument } from "@rightkit/document-core/state/tauriBridge";
5
+ import { errorMessage } from "@rightkit/document-core/state/errors";
6
+ import { writeClipboardText } from "@rightkit/document-core/state/tauriBridge";
7
+ const isMarkdownLike = (active) => !!active && isMarkdownFormat(active?.format);
8
+ /** Command-palette "Export as…" action for md/mdx, or null for other formats. */
9
+ export function buildExportMarkdown(active, markdownExport, activeText) {
10
+ if (!isMarkdownLike(active))
11
+ return null;
12
+ return (target) => {
13
+ const stem = baseName(active.path).replace(/\.(md|mdx|markdown)$/i, "");
14
+ if (active.format === "md") {
15
+ if (markdownExport?.documentPath !== active.path) {
16
+ toast("Markdown renderer is still loading", "info");
17
+ return;
18
+ }
19
+ void markdownExport
20
+ .createSnapshot()
21
+ .then(async (snapshot) => {
22
+ const path = await saveFileDialog(`${stem}.${target === "html" ? "html" : "txt"}`, target === "html" ? "html" : "txt");
23
+ if (!path)
24
+ return;
25
+ await writeDocument(path, target === "html" ? snapshot.htmlDocument : snapshot.plainText);
26
+ toast(snapshot.warnings.length
27
+ ? `Exported ${baseName(path)} with ${snapshot.warnings.length} warning${snapshot.warnings.length === 1 ? "" : "s"}`
28
+ : `Exported ${baseName(path)}`, snapshot.warnings.length ? "info" : "success");
29
+ })
30
+ .catch((error) => toast(`Export failed: ${errorMessage(error)}`, "error"));
31
+ return;
32
+ }
33
+ // Lazy: exportMarkdown pulls the whole unified/remark toolchain. Only pay for it once the
34
+ // user actually asks to export MDX, whose sandbox renderer is separate from Markdown.
35
+ void import("@rightkit/markdown-editor/documents/markdown/exportMarkdown").then(({ markdownToHtml, markdownToPlainText }) => {
36
+ const output = target === "html" ? markdownToHtml(activeText, stem) : markdownToPlainText(activeText);
37
+ void saveFileDialog(`${stem}.${target === "html" ? "html" : "txt"}`, target === "html" ? "html" : "txt").then((path) => {
38
+ if (!path)
39
+ return;
40
+ writeDocument(path, output)
41
+ .then(() => toast(`Exported ${baseName(path)}`, "success"))
42
+ .catch((e) => toast(`Export failed: ${errorMessage(e)}`, "error"));
43
+ });
44
+ });
45
+ };
46
+ }
47
+ /** Command-palette "Copy rendered" action for md/mdx, or null for other formats. */
48
+ export function buildCopyRendered(active, markdownExport, activeText) {
49
+ if (!isMarkdownLike(active))
50
+ return null;
51
+ return () => {
52
+ const copy = (html, text) => {
53
+ const clip = navigator.clipboard;
54
+ const ok = () => toast("Rendered Markdown copied", "success");
55
+ const fail = () => toast("Couldn't copy to clipboard", "error");
56
+ if (clip?.write && typeof ClipboardItem !== "undefined") {
57
+ void clip
58
+ .write([
59
+ new ClipboardItem({
60
+ "text/html": new Blob([html], { type: "text/html" }),
61
+ "text/plain": new Blob([text], { type: "text/plain" }),
62
+ }),
63
+ ])
64
+ .then(ok)
65
+ .catch(() => void clip.writeText(text).then(ok).catch(fail));
66
+ }
67
+ else {
68
+ void clip?.writeText(text).then(ok).catch(fail);
69
+ }
70
+ };
71
+ if (active.format === "md") {
72
+ if (markdownExport?.documentPath !== active.path) {
73
+ toast("Markdown renderer is still loading", "info");
74
+ return;
75
+ }
76
+ void markdownExport
77
+ .createSnapshot()
78
+ .then((snapshot) => copy(snapshot.htmlFragment, snapshot.plainText))
79
+ .catch(() => toast("Couldn't copy to clipboard", "error"));
80
+ return;
81
+ }
82
+ void import("@rightkit/markdown-editor/documents/markdown/exportMarkdown").then(({ markdownToHtmlFragment, markdownToPlainText }) => {
83
+ copy(markdownToHtmlFragment(activeText), markdownToPlainText(activeText));
84
+ });
85
+ };
86
+ }
87
+ /** Command-palette "Copy as Markdown" for PDF and DOCX (converted from document text/model). */
88
+ export function buildCopyAsMarkdown(active) {
89
+ if (!active || (active.format !== "pdf" && active.format !== "docx"))
90
+ return null;
91
+ const { format, assetUrl, content } = active;
92
+ // DOCX without an asset URL carries its bytes as base64 content (DocumentRouter's initialBase64).
93
+ if (!assetUrl && (format === "pdf" || !content))
94
+ return null;
95
+ return () => {
96
+ const convert = format === "pdf"
97
+ ? import("@rightkit/document-core/documents/pdf/pdfToMarkdown").then((m) => m.pdfUrlToMarkdown(assetUrl))
98
+ : (assetUrl
99
+ ? fetch(assetUrl).then((r) => r.arrayBuffer())
100
+ : import("@rightkit/docx-editor/documents/docx/blankDocx").then((m) => m.base64ToBuffer(content)))
101
+ .then((buf) => import("@rightkit/docx-editor/documents/docx/docxToMarkdown").then((m) => m.docxBytesToMarkdown(buf)));
102
+ void convert
103
+ .then((md) => writeClipboardText(md))
104
+ .then(() => toast("Markdown copied", "success"))
105
+ .catch((e) => toast(`Couldn't copy as Markdown: ${errorMessage(e)}`, "error"));
106
+ };
107
+ }