@plannotator/ui 0.22.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 (305) hide show
  1. package/README.md +60 -0
  2. package/assets/diff-options.png +0 -0
  3. package/assets/icon-codex.png +0 -0
  4. package/assets/look-flat.png +0 -0
  5. package/assets/look-grid.png +0 -0
  6. package/assets/review-sections.png +0 -0
  7. package/assets/review-tree.png +0 -0
  8. package/assets/workspaces.webp +0 -0
  9. package/components/AISettingsTab.tsx +145 -0
  10. package/components/ActionMenu.tsx +101 -0
  11. package/components/AgentControls.tsx +216 -0
  12. package/components/AgentsTab.tsx +1294 -0
  13. package/components/AnnotationPanel.tsx +731 -0
  14. package/components/AnnotationSidebar.tsx +86 -0
  15. package/components/AnnotationToolbar.tsx +323 -0
  16. package/components/AnnotationToolstrip.tsx +355 -0
  17. package/components/ApproveDropdown.tsx +170 -0
  18. package/components/AttachmentsButton.tsx +410 -0
  19. package/components/BlockRenderer.tsx +163 -0
  20. package/components/BorderTrail.tsx +38 -0
  21. package/components/CodeFilePicker.tsx +64 -0
  22. package/components/CodeFilePopout.tsx +602 -0
  23. package/components/CodePathValidationContext.tsx +14 -0
  24. package/components/CommentPopover.tsx +512 -0
  25. package/components/CompletionOverlay.tsx +99 -0
  26. package/components/ConfirmDialog.tsx +128 -0
  27. package/components/DocBadges.tsx +231 -0
  28. package/components/EditorAnnotationCard.tsx +65 -0
  29. package/components/ExportModal.tsx +565 -0
  30. package/components/FloatingQuickLabelPicker.tsx +144 -0
  31. package/components/GitHubIcon.tsx +16 -0
  32. package/components/GitLabIcon.tsx +30 -0
  33. package/components/GraphvizBlock.tsx +511 -0
  34. package/components/ImageAnnotator/Canvas.tsx +118 -0
  35. package/components/ImageAnnotator/Toolbar.tsx +218 -0
  36. package/components/ImageAnnotator/index.tsx +272 -0
  37. package/components/ImageAnnotator/types.ts +39 -0
  38. package/components/ImageAnnotator/utils.ts +147 -0
  39. package/components/ImageThumbnail.tsx +126 -0
  40. package/components/ImportModal.tsx +146 -0
  41. package/components/InlineMarkdown.tsx +1094 -0
  42. package/components/KeyboardShortcuts.tsx +208 -0
  43. package/components/Landing.tsx +496 -0
  44. package/components/ListItemBody.tsx +62 -0
  45. package/components/ListMarker.tsx +73 -0
  46. package/components/LookAndFeelAnnouncementDialog.tsx +230 -0
  47. package/components/MarkdownEditor.tsx +44 -0
  48. package/components/MenuVersionSection.tsx +91 -0
  49. package/components/MermaidBlock.tsx +579 -0
  50. package/components/ModeToggle.tsx +68 -0
  51. package/components/OpenInAppButton.tsx +299 -0
  52. package/components/OverlayScrollArea.tsx +82 -0
  53. package/components/PermissionModeSetup.tsx +92 -0
  54. package/components/PinpointOverlay.tsx +99 -0
  55. package/components/PlanAIAnnouncementDialog.tsx +187 -0
  56. package/components/PlanHeaderMenu.tsx +299 -0
  57. package/components/PopoutDialog.tsx +91 -0
  58. package/components/Popover.tsx +27 -0
  59. package/components/ProviderIcons.tsx +51 -0
  60. package/components/PullRequestIcon.tsx +12 -0
  61. package/components/QuickLabelDropdown.tsx +63 -0
  62. package/components/RenderedMarkdown.tsx +54 -0
  63. package/components/RepoIcon.tsx +12 -0
  64. package/components/ResizeHandle.tsx +86 -0
  65. package/components/ReviewAgentsIcon.tsx +18 -0
  66. package/components/SearchableSelect.tsx +157 -0
  67. package/components/Settings.tsx +2220 -0
  68. package/components/SparklesIcon.tsx +45 -0
  69. package/components/StickyHeaderLane.tsx +260 -0
  70. package/components/TableOfContents.tsx +157 -0
  71. package/components/TaterSpritePullup.tsx +34 -0
  72. package/components/TaterSpriteRunning.tsx +54 -0
  73. package/components/TaterSpriteSitting.tsx +37 -0
  74. package/components/TextShimmer.tsx +57 -0
  75. package/components/ThemeProvider.tsx +162 -0
  76. package/components/ThemeTab.tsx +139 -0
  77. package/components/ToolbarButtons.tsx +116 -0
  78. package/components/Tooltip.tsx +45 -0
  79. package/components/Viewer.tsx +977 -0
  80. package/components/ai/AIProviderBar.tsx +95 -0
  81. package/components/ai/DocumentAIChatPanel.tsx +313 -0
  82. package/components/blocks/AlertBlock.tsx +58 -0
  83. package/components/blocks/Callout.tsx +60 -0
  84. package/components/blocks/CodeBlock.tsx +75 -0
  85. package/components/blocks/HtmlBlock.tsx +122 -0
  86. package/components/blocks/MathBlock.tsx +36 -0
  87. package/components/blocks/TableBlock.tsx +146 -0
  88. package/components/blocks/TablePopout.tsx +279 -0
  89. package/components/blocks/TableToolbar.tsx +153 -0
  90. package/components/blocks/proseBody.tsx +102 -0
  91. package/components/core/button.tsx +44 -0
  92. package/components/core/textarea.tsx +26 -0
  93. package/components/diagramLanguages.ts +14 -0
  94. package/components/goal-setup/GoalSetupSurface.tsx +1354 -0
  95. package/components/html-viewer/HtmlViewer.tsx +386 -0
  96. package/components/html-viewer/bridge-script.ts +505 -0
  97. package/components/html-viewer/index.ts +1 -0
  98. package/components/html-viewer/useHtmlAnnotation.ts +400 -0
  99. package/components/icons/AgentIcons.tsx +66 -0
  100. package/components/icons/AppIcon.tsx +56 -0
  101. package/components/icons/MessagesIcon.tsx +11 -0
  102. package/components/icons/ObsidianIcons.tsx +208 -0
  103. package/components/icons/app/android-studio.svg +369 -0
  104. package/components/icons/app/antigravity.svg +97 -0
  105. package/components/icons/app/cursor.svg +16 -0
  106. package/components/icons/app/file-explorer.svg +20 -0
  107. package/components/icons/app/finder.png +0 -0
  108. package/components/icons/app/ghostty.svg +13 -0
  109. package/components/icons/app/iterm2.svg +13 -0
  110. package/components/icons/app/powershell.svg +14 -0
  111. package/components/icons/app/sublime-text.svg +17 -0
  112. package/components/icons/app/terminal.png +0 -0
  113. package/components/icons/app/textmate.png +0 -0
  114. package/components/icons/app/vscode.svg +39 -0
  115. package/components/icons/app/warp.png +0 -0
  116. package/components/icons/app/xcode.png +0 -0
  117. package/components/icons/app/zed-dark.svg +15 -0
  118. package/components/icons/app/zed.svg +15 -0
  119. package/components/icons/themeIcons.tsx +47 -0
  120. package/components/mermaidSvg.ts +33 -0
  121. package/components/plan-diff/PlanCleanDiffView.tsx +884 -0
  122. package/components/plan-diff/PlanDiffBadge.tsx +48 -0
  123. package/components/plan-diff/PlanDiffModeSwitcher.tsx +103 -0
  124. package/components/plan-diff/PlanDiffViewer.tsx +214 -0
  125. package/components/plan-diff/PlanRawDiffView.tsx +102 -0
  126. package/components/plan-diff/VSCodeIcon.tsx +132 -0
  127. package/components/settings/HooksTab.tsx +208 -0
  128. package/components/sidebar/ArchiveBrowser.tsx +98 -0
  129. package/components/sidebar/CountBadge.tsx +12 -0
  130. package/components/sidebar/FileBrowser.tsx +498 -0
  131. package/components/sidebar/MessagesBrowser.tsx +109 -0
  132. package/components/sidebar/SidebarContainer.tsx +353 -0
  133. package/components/sidebar/SidebarTabs.tsx +155 -0
  134. package/components/sidebar/VersionBrowser.tsx +141 -0
  135. package/components/types.d.ts +7 -0
  136. package/components/ui/badge.tsx +44 -0
  137. package/components/ui/button.tsx +83 -0
  138. package/components/ui/card.tsx +58 -0
  139. package/components/ui/dialog.tsx +106 -0
  140. package/components/ui/dropdown-menu.tsx +240 -0
  141. package/components/ui/state-pill.tsx +45 -0
  142. package/components/ui/tabs.tsx +46 -0
  143. package/components/ui/textarea.tsx +25 -0
  144. package/config/configStore.ts +214 -0
  145. package/config/index.ts +4 -0
  146. package/config/reviewView.ts +42 -0
  147. package/config/settings.ts +310 -0
  148. package/config/useConfig.ts +20 -0
  149. package/configure.ts +69 -0
  150. package/globals.d.ts +14 -0
  151. package/hooks/pfm/useCodeFilePopout.ts +111 -0
  152. package/hooks/useAIChat.ts +532 -0
  153. package/hooks/useAIProviderConfig.ts +115 -0
  154. package/hooks/useActiveSection.ts +78 -0
  155. package/hooks/useAgentJobs.ts +306 -0
  156. package/hooks/useAgentSettings.ts +579 -0
  157. package/hooks/useAgents.ts +90 -0
  158. package/hooks/useAnnotationDraft.ts +523 -0
  159. package/hooks/useAnnotationHighlighter.ts +1170 -0
  160. package/hooks/useArchive.ts +171 -0
  161. package/hooks/useAutoClose.ts +104 -0
  162. package/hooks/useCodeAnnotationDraft.ts +194 -0
  163. package/hooks/useCodeFilePopout.ts +1 -0
  164. package/hooks/useDismissOnOutsideAndEscape.ts +50 -0
  165. package/hooks/useDraggable.ts +108 -0
  166. package/hooks/useEditorAnnotations.ts +64 -0
  167. package/hooks/useExternalAnnotationHighlights.ts +105 -0
  168. package/hooks/useExternalAnnotations.ts +268 -0
  169. package/hooks/useFileBrowser.ts +402 -0
  170. package/hooks/useInputMethodSwitch.ts +89 -0
  171. package/hooks/useIsMobile.ts +17 -0
  172. package/hooks/useLinkedDoc.ts +494 -0
  173. package/hooks/useOverlayViewport.ts +36 -0
  174. package/hooks/usePinpoint.ts +182 -0
  175. package/hooks/usePlanDiff.ts +176 -0
  176. package/hooks/usePrintMode.ts +27 -0
  177. package/hooks/useResizablePanel.ts +173 -0
  178. package/hooks/useScrollViewport.ts +38 -0
  179. package/hooks/useSharing.ts +441 -0
  180. package/hooks/useSidebar.ts +55 -0
  181. package/hooks/useUpdateCheck.ts +132 -0
  182. package/hooks/useValidatedCodePaths.ts +94 -0
  183. package/icons/GitUser.tsx +17 -0
  184. package/lib/utils.ts +11 -0
  185. package/package.json +117 -0
  186. package/plannotator.webp +0 -0
  187. package/print.css +427 -0
  188. package/shortcuts/code-review/ai.shortcuts.ts +25 -0
  189. package/shortcuts/code-review/allFilesDiff.shortcuts.ts +46 -0
  190. package/shortcuts/code-review/annotationToolbar.shortcuts.ts +30 -0
  191. package/shortcuts/code-review/fileTree.shortcuts.ts +35 -0
  192. package/shortcuts/code-review/prComments.shortcuts.ts +18 -0
  193. package/shortcuts/code-review/suggestionModal.shortcuts.ts +25 -0
  194. package/shortcuts/code-review/tourDialog.shortcuts.ts +18 -0
  195. package/shortcuts/core.ts +399 -0
  196. package/shortcuts/index.ts +21 -0
  197. package/shortcuts/plan-review/annotationPanel.shortcuts.ts +25 -0
  198. package/shortcuts/plan-review/annotationToolbar.shortcuts.ts +39 -0
  199. package/shortcuts/plan-review/commentPopover.shortcuts.ts +21 -0
  200. package/shortcuts/plan-review/goalSetup.shortcuts.ts +10 -0
  201. package/shortcuts/plan-review/imageAnnotator.shortcuts.ts +42 -0
  202. package/shortcuts/plan-review/inputMethod.shortcuts.ts +30 -0
  203. package/shortcuts/plan-review/sidebar.shortcuts.ts +34 -0
  204. package/shortcuts/plan-review/viewer.shortcuts.ts +24 -0
  205. package/shortcuts/runtime.ts +259 -0
  206. package/sprite_package_additional/index.html +34 -0
  207. package/sprite_package_additional/sprite.png +0 -0
  208. package/sprite_package_new/index.html +34 -0
  209. package/sprite_package_new/sprite.png +0 -0
  210. package/sprite_package_pulluphang/index.html +34 -0
  211. package/sprite_package_pulluphang/sprite.png +0 -0
  212. package/styles.css +1 -0
  213. package/theme.css +893 -0
  214. package/themes/adwaita.css +62 -0
  215. package/themes/andromeeda.css +66 -0
  216. package/themes/aurora-x.css +66 -0
  217. package/themes/ayu-dark.css +66 -0
  218. package/themes/caffeine.css +113 -0
  219. package/themes/catppuccin.css +62 -0
  220. package/themes/claude-plus.css +60 -0
  221. package/themes/cursor-hc.css +34 -0
  222. package/themes/cursor-midnight.css +34 -0
  223. package/themes/cursor.css +62 -0
  224. package/themes/dark-plus.css +66 -0
  225. package/themes/doom-64.css +109 -0
  226. package/themes/dracula.css +33 -0
  227. package/themes/everforest-hard.css +62 -0
  228. package/themes/everforest-soft.css +62 -0
  229. package/themes/everforest.css +62 -0
  230. package/themes/github.css +66 -0
  231. package/themes/gruvbox.css +62 -0
  232. package/themes/houston.css +66 -0
  233. package/themes/kanagawa-dragon.css +34 -0
  234. package/themes/kanagawa-lotus.css +34 -0
  235. package/themes/kanagawa-wave.css +34 -0
  236. package/themes/laserwave.css +66 -0
  237. package/themes/material.css +62 -0
  238. package/themes/min.css +66 -0
  239. package/themes/monokai-pro.css +34 -0
  240. package/themes/neutral.css +59 -0
  241. package/themes/night-owl.css +66 -0
  242. package/themes/nord.css +66 -0
  243. package/themes/one-dark-pro.css +66 -0
  244. package/themes/one-light.css +66 -0
  245. package/themes/paulmillr.css +34 -0
  246. package/themes/plannotator.css +60 -0
  247. package/themes/plastic.css +66 -0
  248. package/themes/poimandres.css +66 -0
  249. package/themes/quantum-rose.css +109 -0
  250. package/themes/red.css +66 -0
  251. package/themes/rose-pine.css +62 -0
  252. package/themes/simple.css +124 -0
  253. package/themes/slack.css +66 -0
  254. package/themes/snazzy-light.css +66 -0
  255. package/themes/soft-pop.css +60 -0
  256. package/themes/solar-dusk.css +109 -0
  257. package/themes/solarized.css +66 -0
  258. package/themes/synthwave-84.css +34 -0
  259. package/themes/terminal.css +62 -0
  260. package/themes/tinacious.css +57 -0
  261. package/themes/tokyo-night.css +62 -0
  262. package/themes/vesper.css +62 -0
  263. package/themes/vitesse-black.css +66 -0
  264. package/themes/vitesse.css +62 -0
  265. package/types.ts +264 -0
  266. package/utils/agentSwitch.ts +71 -0
  267. package/utils/aiChatFormat.ts +33 -0
  268. package/utils/aiPrompt.ts +29 -0
  269. package/utils/aiProvider.ts +217 -0
  270. package/utils/anchors.ts +11 -0
  271. package/utils/annotateAgentTerminal.ts +29 -0
  272. package/utils/annotationHelpers.ts +101 -0
  273. package/utils/bear.ts +59 -0
  274. package/utils/blockTargeting.ts +240 -0
  275. package/utils/callback.ts +99 -0
  276. package/utils/commentContent.ts +8 -0
  277. package/utils/defaultNotesApp.ts +20 -0
  278. package/utils/diffFonts.ts +33 -0
  279. package/utils/editorMode.ts +31 -0
  280. package/utils/fileBrowser.ts +40 -0
  281. package/utils/generateId.ts +7 -0
  282. package/utils/generateIdentity.ts +29 -0
  283. package/utils/identity.ts +112 -0
  284. package/utils/inlineTransforms.ts +41 -0
  285. package/utils/inputMethod.ts +17 -0
  286. package/utils/lookAndFeelAnnouncement.ts +18 -0
  287. package/utils/obsidian.ts +201 -0
  288. package/utils/octarine.ts +53 -0
  289. package/utils/parser.ts +991 -0
  290. package/utils/permissionMode.ts +77 -0
  291. package/utils/planAIAnnouncement.ts +17 -0
  292. package/utils/planAgentInstructions.ts +137 -0
  293. package/utils/planDiffEngine.ts +590 -0
  294. package/utils/planSave.ts +49 -0
  295. package/utils/platform.ts +11 -0
  296. package/utils/quickLabels.ts +77 -0
  297. package/utils/reviewAgentInstructions.ts +182 -0
  298. package/utils/sanitizeHtml.ts +45 -0
  299. package/utils/sharing.ts +350 -0
  300. package/utils/slugify.ts +37 -0
  301. package/utils/storage.ts +141 -0
  302. package/utils/themeRegistry.ts +567 -0
  303. package/utils/uiPreferences.ts +34 -0
  304. package/utils/upload.ts +56 -0
  305. package/utils/wideMode.ts +48 -0
@@ -0,0 +1,494 @@
1
+ /**
2
+ * Linked Document Hook
3
+ *
4
+ * Manages same-view navigation to local .md files referenced in plans.
5
+ * Handles state swapping (save plan state, load doc, restore on back),
6
+ * annotation caching per filepath, and highlight re-application.
7
+ */
8
+
9
+ import { useState, useCallback, useRef } from "react";
10
+ import type { Annotation, ImageAttachment } from "../types";
11
+ import type { ViewerHandle } from "../components/Viewer";
12
+ import type { SidebarTab } from "./useSidebar";
13
+ import type { SourceSaveCapability } from "@plannotator/core/source-save";
14
+
15
+ export interface LinkedDocLoadData {
16
+ markdown?: string;
17
+ filepath?: string;
18
+ isConverted?: boolean;
19
+ renderAs?: 'markdown' | 'html';
20
+ rawHtml?: string;
21
+ shareHtml?: string;
22
+ sourceSave?: SourceSaveCapability;
23
+ }
24
+
25
+ export interface UseLinkedDocOptions {
26
+ markdown: string;
27
+ annotations: Annotation[];
28
+ selectedAnnotationId: string | null;
29
+ globalAttachments: ImageAttachment[];
30
+ setMarkdown: (md: string) => void;
31
+ setAnnotations: (anns: Annotation[]) => void;
32
+ setSelectedAnnotationId: (id: string | null) => void;
33
+ setGlobalAttachments: (att: ImageAttachment[]) => void;
34
+ /** Current render mode + raw HTML of the base document. An HTML linked/folder file
35
+ * swaps these to render raw; back() restores the base values from this snapshot. */
36
+ renderAs: 'markdown' | 'html';
37
+ rawHtml: string;
38
+ shareHtml: string;
39
+ setRenderAs: (r: 'markdown' | 'html') => void;
40
+ setRawHtml: (html: string) => void;
41
+ setShareHtml: (html: string) => void;
42
+ viewerRef: React.RefObject<ViewerHandle | null>;
43
+ sidebar: { open: (tab?: SidebarTab) => void };
44
+ /** Absolute path of the primary document — enables getDocAnnotations() to include
45
+ * stashed original-file annotations when viewing a linked doc. */
46
+ sourceFilePath?: string;
47
+ /** Whether the primary document was converted from HTML/URL — propagated to the
48
+ * stashed entry so feedback caveats survive cross-doc navigation. */
49
+ sourceConverted?: boolean;
50
+ /** Snapshot live editor state before linked/folder navigation swaps documents. */
51
+ onBeforeNavigate?: () => void;
52
+ /** Let the host initialize/restore editable document state and optionally
53
+ * override the markdown displayed for this file. */
54
+ onDocumentLoaded?: (doc: LinkedDocLoadData) => string | undefined;
55
+ /** Read current host-owned text when caching a linked doc. */
56
+ getDocumentMarkdown?: (filepath: string, fallback?: string) => string | undefined;
57
+ /** Let the host restore any state that was suspended while a linked doc was active. */
58
+ onAfterBack?: () => void;
59
+ }
60
+
61
+ interface SavedPlanState {
62
+ markdown: string;
63
+ annotations: Annotation[];
64
+ selectedAnnotationId: string | null;
65
+ globalAttachments: ImageAttachment[];
66
+ renderAs: 'markdown' | 'html';
67
+ rawHtml: string;
68
+ shareHtml: string;
69
+ }
70
+
71
+ export interface CachedDocState {
72
+ annotations: Annotation[];
73
+ globalAttachments: ImageAttachment[];
74
+ markdown?: string;
75
+ isConverted?: boolean;
76
+ }
77
+
78
+ export interface LinkedDocSessionState {
79
+ root: SavedPlanState;
80
+ docs: Map<string, CachedDocState>;
81
+ }
82
+
83
+ export interface UseLinkedDocReturn {
84
+ /** Whether a linked doc is currently active */
85
+ isActive: boolean;
86
+ /** Resolved filepath of the active linked doc */
87
+ filepath: string | null;
88
+ /** Error from the last open attempt */
89
+ error: string | null;
90
+ /** Whether a fetch is in progress */
91
+ isLoading: boolean;
92
+ /** Open a linked document by path (saves plan state, fetches doc, swaps) */
93
+ open: (docPath: string, buildUrl?: (path: string) => string, targetTab?: SidebarTab) => Promise<void>;
94
+ /** Open an already-loaded linked document without refetching from disk */
95
+ openLoaded: (
96
+ doc: LinkedDocLoadData & { filepath: string },
97
+ targetTab?: SidebarTab,
98
+ options?: { notifyDocumentLoaded?: boolean },
99
+ ) => void;
100
+ /** Return to the plan (caches doc annotations, restores plan state) */
101
+ back: () => void;
102
+ /** Dismiss the current error */
103
+ dismissError: () => void;
104
+ /** All linked doc annotations including the active doc's live state (keyed by filepath) */
105
+ getDocAnnotations: () => Map<string, CachedDocState>;
106
+ /** Snapshot the root document plus linked-doc cache for cross-document session swaps */
107
+ snapshotSession: () => LinkedDocSessionState;
108
+ /** Restore a root document plus linked-doc cache, closing any active linked document */
109
+ restoreSession: (state: LinkedDocSessionState) => void;
110
+ /** Reactive count of annotations on non-active documents (updates on open() and back()) */
111
+ docAnnotationCount: number;
112
+ }
113
+
114
+ const HIGHLIGHT_REAPPLY_DELAY = 100;
115
+
116
+ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn {
117
+ const {
118
+ markdown,
119
+ annotations,
120
+ selectedAnnotationId,
121
+ globalAttachments,
122
+ setMarkdown,
123
+ setAnnotations,
124
+ setSelectedAnnotationId,
125
+ setGlobalAttachments,
126
+ renderAs,
127
+ rawHtml,
128
+ shareHtml,
129
+ setRenderAs,
130
+ setRawHtml,
131
+ setShareHtml,
132
+ viewerRef,
133
+ sidebar,
134
+ sourceFilePath,
135
+ sourceConverted,
136
+ onBeforeNavigate,
137
+ onDocumentLoaded,
138
+ getDocumentMarkdown,
139
+ onAfterBack,
140
+ } = options;
141
+
142
+ const [linkedDoc, setLinkedDoc] = useState<{ filepath: string; isConverted?: boolean; markdown?: string } | null>(null);
143
+ const [error, setError] = useState<string | null>(null);
144
+ const [isLoading, setIsLoading] = useState(false);
145
+ const [docAnnotationCount, setDocAnnotationCount] = useState(0);
146
+
147
+ // Stash plan state when navigating to a linked doc
148
+ const savedPlanState = useRef<SavedPlanState | null>(null);
149
+
150
+ // Cache linked doc annotations keyed by filepath (persists across back/forth within session)
151
+ const docCache = useRef<Map<string, CachedDocState>>(new Map());
152
+
153
+ const defaultBuildUrl = useCallback(
154
+ (path: string) => `/api/doc?path=${encodeURIComponent(path)}`,
155
+ []
156
+ );
157
+
158
+ const back = useCallback(() => {
159
+ if (!savedPlanState.current) return;
160
+ onBeforeNavigate?.();
161
+
162
+ // Clear web-highlighter marks before swapping content to prevent React DOM mismatch
163
+ viewerRef.current?.clearAllHighlights();
164
+
165
+ // Cache current linked doc annotations
166
+ if (linkedDoc) {
167
+ docCache.current.set(linkedDoc.filepath, {
168
+ annotations: [...annotations],
169
+ globalAttachments: [...globalAttachments],
170
+ markdown: getDocumentMarkdown?.(linkedDoc.filepath, linkedDoc.markdown) ?? linkedDoc.markdown,
171
+ isConverted: linkedDoc.isConverted,
172
+ });
173
+ // Update reactive count so button labels can respond
174
+ let total = 0;
175
+ for (const cached of docCache.current.values()) {
176
+ total += cached.annotations.length + cached.globalAttachments.length;
177
+ }
178
+ setDocAnnotationCount(total);
179
+ }
180
+
181
+ // Restore plan state (including render mode — an HTML base restores to HTML)
182
+ const saved = savedPlanState.current;
183
+ setRenderAs(saved.renderAs);
184
+ setRawHtml(saved.rawHtml);
185
+ setShareHtml(saved.shareHtml);
186
+ setMarkdown(saved.markdown);
187
+ setAnnotations(saved.annotations);
188
+ setGlobalAttachments(saved.globalAttachments);
189
+ setSelectedAnnotationId(saved.selectedAnnotationId);
190
+ setLinkedDoc(null);
191
+ setError(null);
192
+ savedPlanState.current = null;
193
+ onAfterBack?.();
194
+
195
+ // Re-apply plan annotation highlights after DOM settles
196
+ if (saved.annotations.length) {
197
+ setTimeout(() => {
198
+ viewerRef.current?.clearAllHighlights();
199
+ viewerRef.current?.applySharedAnnotations(saved.annotations);
200
+ }, HIGHLIGHT_REAPPLY_DELAY);
201
+ }
202
+ }, [
203
+ linkedDoc,
204
+ annotations,
205
+ globalAttachments,
206
+ setMarkdown,
207
+ setAnnotations,
208
+ setSelectedAnnotationId,
209
+ setGlobalAttachments,
210
+ setRenderAs,
211
+ setRawHtml,
212
+ setShareHtml,
213
+ viewerRef,
214
+ onBeforeNavigate,
215
+ getDocumentMarkdown,
216
+ onAfterBack,
217
+ ]);
218
+
219
+ const activateDocument = useCallback((
220
+ data: LinkedDocLoadData & { filepath: string },
221
+ targetTab?: SidebarTab,
222
+ options: { snapshotCurrent?: boolean; notifyDocumentLoaded?: boolean } = {},
223
+ ) => {
224
+ const snapshotCurrent = options.snapshotCurrent ?? true;
225
+ const notifyDocumentLoaded = options.notifyDocumentLoaded ?? true;
226
+ if (snapshotCurrent) onBeforeNavigate?.();
227
+
228
+ // Backlink detection: if a linked doc links back to the source file (e.g.,
229
+ // original.md → design.md → link back to original.md), opening it as a linked
230
+ // doc would create two competing Map entries for the same filepath in
231
+ // getDocAnnotations(), and the empty linked-doc entry would overwrite the
232
+ // stashed annotations. Instead, treat the backlink as a back() navigation —
233
+ // the current linked doc gets cached and the source file restores with its
234
+ // annotations intact.
235
+ if (sourceFilePath && data.filepath === sourceFilePath && savedPlanState.current) {
236
+ back();
237
+ return;
238
+ }
239
+
240
+ // Clear web-highlighter marks before swapping content to prevent React DOM mismatch
241
+ viewerRef.current?.clearAllHighlights();
242
+
243
+ // Save current state (plan or another linked doc)
244
+ if (!savedPlanState.current) {
245
+ savedPlanState.current = {
246
+ markdown,
247
+ annotations: [...annotations],
248
+ selectedAnnotationId,
249
+ globalAttachments: [...globalAttachments],
250
+ renderAs,
251
+ rawHtml,
252
+ shareHtml,
253
+ };
254
+ let total = annotations.length + globalAttachments.length;
255
+ for (const [fp, cached] of docCache.current.entries()) {
256
+ if (fp === data.filepath) continue; // destination becomes active — don't double-count
257
+ total += cached.annotations.length + cached.globalAttachments.length;
258
+ }
259
+ setDocAnnotationCount(total);
260
+ } else if (linkedDoc) {
261
+ // Already viewing a linked doc — cache its annotations before moving on
262
+ docCache.current.set(linkedDoc.filepath, {
263
+ annotations: [...annotations],
264
+ globalAttachments: [...globalAttachments],
265
+ markdown: getDocumentMarkdown?.(linkedDoc.filepath, linkedDoc.markdown) ?? linkedDoc.markdown,
266
+ isConverted: linkedDoc.isConverted,
267
+ });
268
+ let total = 0;
269
+ for (const [fp, cached] of docCache.current.entries()) {
270
+ if (fp === data.filepath) continue; // destination becomes active — don't double-count
271
+ total += cached.annotations.length + cached.globalAttachments.length;
272
+ }
273
+ if (savedPlanState.current) {
274
+ total += savedPlanState.current.annotations.length + savedPlanState.current.globalAttachments.length;
275
+ }
276
+ setDocAnnotationCount(total);
277
+ }
278
+
279
+ // Check cache for previous annotations on this file
280
+ const cached = docCache.current.get(data.filepath);
281
+
282
+ // Swap to linked doc — an .html file renders raw (HtmlViewer), a markdown
283
+ // file parses to blocks (Viewer). Drive renderAs/rawHtml per file so the
284
+ // App's renderAs === 'html' ? HtmlViewer : Viewer switch flips automatically.
285
+ const docRenderAs = data.renderAs === 'html' ? 'html' : 'markdown';
286
+ const hostMarkdown = docRenderAs === 'html' || !notifyDocumentLoaded ? undefined : onDocumentLoaded?.(data);
287
+ const nextMarkdown = notifyDocumentLoaded
288
+ ? hostMarkdown ?? cached?.markdown ?? data.markdown ?? ''
289
+ : data.markdown ?? cached?.markdown ?? '';
290
+ setRenderAs(docRenderAs);
291
+ setRawHtml(docRenderAs === 'html' ? (data.rawHtml ?? '') : '');
292
+ setShareHtml(docRenderAs === 'html' ? (data.shareHtml ?? '') : '');
293
+ setMarkdown(docRenderAs === 'html' ? '' : nextMarkdown);
294
+ setAnnotations(cached?.annotations ?? []);
295
+ setGlobalAttachments(cached?.globalAttachments ?? []);
296
+ setSelectedAnnotationId(null);
297
+ setLinkedDoc({
298
+ filepath: data.filepath,
299
+ isConverted: !!data.isConverted,
300
+ markdown: nextMarkdown,
301
+ });
302
+ setError(null);
303
+ sidebar.open(targetTab ?? "toc");
304
+
305
+ // Re-apply cached annotations after DOM settles
306
+ if (cached?.annotations.length) {
307
+ setTimeout(() => {
308
+ viewerRef.current?.clearAllHighlights();
309
+ viewerRef.current?.applySharedAnnotations(cached.annotations);
310
+ }, HIGHLIGHT_REAPPLY_DELAY);
311
+ }
312
+ }, [
313
+ markdown,
314
+ annotations,
315
+ selectedAnnotationId,
316
+ globalAttachments,
317
+ renderAs,
318
+ rawHtml,
319
+ shareHtml,
320
+ linkedDoc,
321
+ setMarkdown,
322
+ setAnnotations,
323
+ setSelectedAnnotationId,
324
+ setGlobalAttachments,
325
+ setRenderAs,
326
+ setRawHtml,
327
+ setShareHtml,
328
+ viewerRef,
329
+ sidebar,
330
+ sourceFilePath,
331
+ onBeforeNavigate,
332
+ onDocumentLoaded,
333
+ getDocumentMarkdown,
334
+ back,
335
+ ]);
336
+
337
+ const openLoaded = useCallback((
338
+ doc: LinkedDocLoadData & { filepath: string },
339
+ targetTab?: SidebarTab,
340
+ options?: { notifyDocumentLoaded?: boolean },
341
+ ) => {
342
+ activateDocument(doc, targetTab, {
343
+ snapshotCurrent: true,
344
+ notifyDocumentLoaded: options?.notifyDocumentLoaded,
345
+ });
346
+ }, [activateDocument]);
347
+
348
+ const open = useCallback(
349
+ async (docPath: string, buildUrl?: (path: string) => string, targetTab?: SidebarTab) => {
350
+ onBeforeNavigate?.();
351
+ setIsLoading(true);
352
+ setError(null);
353
+
354
+ try {
355
+ const url = (buildUrl ?? defaultBuildUrl)(docPath);
356
+ const res = await fetch(url);
357
+ const data = (await res.json()) as LinkedDocLoadData & {
358
+ error?: string;
359
+ matches?: string[];
360
+ };
361
+
362
+ if (!res.ok || data.error) {
363
+ setError(data.error || "Failed to load document");
364
+ return;
365
+ }
366
+
367
+ if (!data.filepath) {
368
+ setError("Failed to load document");
369
+ return;
370
+ }
371
+ activateDocument({ ...data, filepath: data.filepath }, targetTab, { snapshotCurrent: false });
372
+ } catch {
373
+ setError("Failed to connect to server");
374
+ } finally {
375
+ setIsLoading(false);
376
+ }
377
+ },
378
+ [
379
+ onBeforeNavigate,
380
+ activateDocument,
381
+ ]
382
+ );
383
+
384
+ const dismissError = useCallback(() => setError(null), []);
385
+
386
+ const snapshotSession = useCallback((): LinkedDocSessionState => {
387
+ const docs = new Map(docCache.current);
388
+ if (linkedDoc) {
389
+ docs.set(linkedDoc.filepath, {
390
+ annotations: [...annotations],
391
+ globalAttachments: [...globalAttachments],
392
+ markdown: getDocumentMarkdown?.(linkedDoc.filepath, linkedDoc.markdown) ?? linkedDoc.markdown,
393
+ isConverted: linkedDoc.isConverted,
394
+ });
395
+ }
396
+
397
+ const root = savedPlanState.current
398
+ ? {
399
+ markdown: savedPlanState.current.markdown,
400
+ renderAs: savedPlanState.current.renderAs,
401
+ rawHtml: savedPlanState.current.rawHtml,
402
+ shareHtml: savedPlanState.current.shareHtml,
403
+ annotations: [...savedPlanState.current.annotations],
404
+ selectedAnnotationId: savedPlanState.current.selectedAnnotationId,
405
+ globalAttachments: [...savedPlanState.current.globalAttachments],
406
+ }
407
+ : {
408
+ markdown,
409
+ renderAs,
410
+ rawHtml,
411
+ shareHtml,
412
+ annotations: [...annotations],
413
+ selectedAnnotationId,
414
+ globalAttachments: [...globalAttachments],
415
+ };
416
+
417
+ return { root, docs };
418
+ }, [linkedDoc, annotations, globalAttachments, markdown, renderAs, rawHtml, shareHtml, selectedAnnotationId, getDocumentMarkdown]);
419
+
420
+ const restoreSession = useCallback((state: LinkedDocSessionState) => {
421
+ viewerRef.current?.clearAllHighlights();
422
+
423
+ savedPlanState.current = null;
424
+ docCache.current = new Map(state.docs);
425
+ let total = 0;
426
+ for (const cached of docCache.current.values()) {
427
+ total += cached.annotations.length + cached.globalAttachments.length;
428
+ }
429
+ setDocAnnotationCount(total);
430
+
431
+ setMarkdown(state.root.markdown);
432
+ setRenderAs(state.root.renderAs);
433
+ setRawHtml(state.root.rawHtml);
434
+ setShareHtml(state.root.shareHtml);
435
+ setAnnotations([...state.root.annotations]);
436
+ setGlobalAttachments([...state.root.globalAttachments]);
437
+ setSelectedAnnotationId(state.root.selectedAnnotationId);
438
+ setLinkedDoc(null);
439
+ setError(null);
440
+
441
+ if (state.root.annotations.length) {
442
+ setTimeout(() => {
443
+ viewerRef.current?.clearAllHighlights();
444
+ viewerRef.current?.applySharedAnnotations(state.root.annotations);
445
+ }, HIGHLIGHT_REAPPLY_DELAY);
446
+ }
447
+ }, [
448
+ setMarkdown,
449
+ setAnnotations,
450
+ setSelectedAnnotationId,
451
+ setGlobalAttachments,
452
+ setRenderAs,
453
+ setRawHtml,
454
+ setShareHtml,
455
+ viewerRef,
456
+ ]);
457
+
458
+ const getDocAnnotations = useCallback((): Map<string, CachedDocState> => {
459
+ const result = new Map(docCache.current);
460
+ // Include stashed original-file annotations when viewing a linked doc
461
+ if (linkedDoc && savedPlanState.current && sourceFilePath) {
462
+ result.set(sourceFilePath, {
463
+ annotations: [...savedPlanState.current.annotations],
464
+ globalAttachments: [...savedPlanState.current.globalAttachments],
465
+ markdown: savedPlanState.current.markdown,
466
+ isConverted: !!sourceConverted,
467
+ });
468
+ }
469
+ if (linkedDoc) {
470
+ result.set(linkedDoc.filepath, {
471
+ annotations: [...annotations],
472
+ globalAttachments: [...globalAttachments],
473
+ markdown: getDocumentMarkdown?.(linkedDoc.filepath, linkedDoc.markdown) ?? linkedDoc.markdown,
474
+ isConverted: linkedDoc.isConverted,
475
+ });
476
+ }
477
+ return result;
478
+ }, [linkedDoc, annotations, globalAttachments, sourceFilePath, sourceConverted, getDocumentMarkdown]);
479
+
480
+ return {
481
+ isActive: linkedDoc !== null,
482
+ filepath: linkedDoc?.filepath ?? null,
483
+ error,
484
+ isLoading,
485
+ open,
486
+ openLoaded,
487
+ back,
488
+ dismissError,
489
+ getDocAnnotations,
490
+ snapshotSession,
491
+ restoreSession,
492
+ docAnnotationCount,
493
+ };
494
+ }
@@ -0,0 +1,36 @@
1
+ import { useCallback, useRef, useState } from 'react';
2
+
3
+ /**
4
+ * Bridges a scroll element (delivered by `OverlayScrollArea`'s `onViewportReady`,
5
+ * now a native scroll node) into React state and a ref at the same time, for
6
+ * components that need to both:
7
+ *
8
+ * 1. Re-run effects when the viewport becomes available (state), and
9
+ * 2. Access the viewport imperatively without re-rendering (ref).
10
+ *
11
+ * Usage:
12
+ *
13
+ * const { ref, viewport, onViewportReady } = useOverlayViewport();
14
+ * // ...
15
+ * <OverlayScrollArea onViewportReady={onViewportReady}> ... </OverlayScrollArea>
16
+ *
17
+ * Then use `ref.current` inside event handlers / imperative assignments
18
+ * (`ref.current.scrollTop = ...`), and include `viewport` in effect deps so
19
+ * the effect re-runs once the library has attached its viewport div.
20
+ *
21
+ * Without this hook, components that only use a ref silently no-op on first
22
+ * render because the ref is populated after mount and nothing retriggers
23
+ * dependent effects.
24
+ */
25
+ export function useOverlayViewport<T extends HTMLElement = HTMLElement>() {
26
+ const ref = useRef<T | null>(null);
27
+ const [viewport, setViewport] = useState<T | null>(null);
28
+
29
+ const onViewportReady = useCallback((next: HTMLElement | null) => {
30
+ const el = next as T | null;
31
+ ref.current = el;
32
+ setViewport(el);
33
+ }, []);
34
+
35
+ return { ref, viewport, onViewportReady };
36
+ }