@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,386 @@
1
+ import React, {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from "react";
10
+ import { createPortal } from "react-dom";
11
+ import type { Annotation, EditorMode, ImageAttachment, InputMethod } from "../../types";
12
+ import { AnnotationType } from "../../types";
13
+ import { getIdentity } from "../../utils/identity";
14
+ import { AnnotationToolbar } from "../AnnotationToolbar";
15
+ import { AttachmentsButton } from "../AttachmentsButton";
16
+ import { CommentPopover, type CommentAskAIHandler } from "../CommentPopover";
17
+ import { FloatingQuickLabelPicker } from "../FloatingQuickLabelPicker";
18
+ import type { ViewerHandle } from "../Viewer";
19
+ import { useHtmlAnnotation } from "./useHtmlAnnotation";
20
+ import { ANNOTATION_HIGHLIGHT_CSS, BRIDGE_SCRIPT } from "./bridge-script";
21
+
22
+ const PREFIX = "plannotator-bridge-";
23
+
24
+ const THEME_TOKENS = [
25
+ "--background",
26
+ "--foreground",
27
+ "--card",
28
+ "--card-foreground",
29
+ "--primary",
30
+ "--primary-foreground",
31
+ "--secondary",
32
+ "--secondary-foreground",
33
+ "--muted",
34
+ "--muted-foreground",
35
+ "--accent",
36
+ "--accent-foreground",
37
+ "--destructive",
38
+ "--destructive-foreground",
39
+ "--success",
40
+ "--success-foreground",
41
+ "--warning",
42
+ "--warning-foreground",
43
+ "--border",
44
+ "--input",
45
+ "--ring",
46
+ "--code-bg",
47
+ "--focus-highlight",
48
+ "--font-sans",
49
+ "--font-mono",
50
+ "--radius",
51
+ ] as const;
52
+
53
+ function readThemeTokens(): Record<string, string> {
54
+ const style = getComputedStyle(document.documentElement);
55
+ const tokens: Record<string, string> = {};
56
+ for (const key of THEME_TOKENS) {
57
+ const val = style.getPropertyValue(key).trim();
58
+ if (val) tokens[key] = val;
59
+ }
60
+ return tokens;
61
+ }
62
+
63
+ function isLightTheme(): boolean {
64
+ return document.documentElement.classList.contains("light");
65
+ }
66
+
67
+ export interface HtmlViewerProps {
68
+ rawHtml: string;
69
+ annotations: Annotation[];
70
+ onAddAnnotation: (ann: Annotation) => void;
71
+ onSelectAnnotation: (id: string | null) => void;
72
+ selectedAnnotationId: string | null;
73
+ mode: EditorMode;
74
+ /** Input method: 'drag' = text selection, 'pinpoint' = click an element. */
75
+ inputMethod: InputMethod;
76
+ globalAttachments?: ImageAttachment[];
77
+ onAddGlobalAttachment?: (image: ImageAttachment) => void;
78
+ onRemoveGlobalAttachment?: (path: string) => void;
79
+ maxWidth?: number | null;
80
+ /** Render edge-to-edge: fill the viewport, drop the card chrome + action bar,
81
+ * and let the iframe own the full height instead of auto-resizing to content. */
82
+ fullViewport?: boolean;
83
+ /** Hide the floating doc-level controls (attachments + global comment) in
84
+ * full-viewport mode, so the user can read the page unobstructed. */
85
+ hideControls?: boolean;
86
+ /** A version diff (vs the previous version) is available to toggle. */
87
+ diffAvailable?: boolean;
88
+ /** Whether the diff-highlighted HTML is currently shown. */
89
+ diffActive?: boolean;
90
+ /** Toggle the diff-highlighted view on/off. */
91
+ onToggleDiff?: () => void;
92
+ onAskAI?: CommentAskAIHandler;
93
+ }
94
+
95
+ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
96
+ (
97
+ {
98
+ rawHtml,
99
+ annotations,
100
+ onAddAnnotation,
101
+ onSelectAnnotation,
102
+ selectedAnnotationId,
103
+ mode,
104
+ inputMethod,
105
+ globalAttachments = [],
106
+ onAddGlobalAttachment,
107
+ onRemoveGlobalAttachment,
108
+ maxWidth,
109
+ fullViewport,
110
+ hideControls,
111
+ diffAvailable,
112
+ diffActive,
113
+ onToggleDiff,
114
+ onAskAI,
115
+ },
116
+ ref,
117
+ ) => {
118
+ const iframeRef = useRef<HTMLIFrameElement>(null);
119
+ const globalCommentButtonRef = useRef<HTMLButtonElement>(null);
120
+ const [iframeHeight, setIframeHeight] = useState(600);
121
+ const [iframeReady, setIframeReady] = useState(false);
122
+ const [globalCommentPopover, setGlobalCommentPopover] = useState<{
123
+ anchorEl: HTMLElement;
124
+ contextText: string;
125
+ } | null>(null);
126
+
127
+ const srcdoc = useMemo(() => {
128
+ const tokens = readThemeTokens();
129
+ let themeCSS = ":root {\n";
130
+ for (const [key, val] of Object.entries(tokens)) {
131
+ themeCSS += ` ${key}: ${val};\n`;
132
+ }
133
+ themeCSS += "}\n";
134
+ if (isLightTheme()) themeCSS += ":root { color-scheme: light; }\n:root.light, :root { }\n";
135
+
136
+ // Version-diff highlights: htmlDiff wraps changed text in <ins>/<del>.
137
+ const diffCSS =
138
+ "ins{background:#e6ffec;color:#0a7d33;text-decoration:none;border-radius:2px;box-shadow:0 0 0 1px #abf2bc inset}" +
139
+ "del{background:#ffebe9;color:#b31d28;text-decoration:line-through;border-radius:2px;box-shadow:0 0 0 1px #ffc1bc inset}";
140
+ const injection = `<style>${themeCSS}${ANNOTATION_HIGHLIGHT_CSS}${diffCSS}</style><script>${BRIDGE_SCRIPT}</script>`;
141
+ const headClose = rawHtml.indexOf("</head>");
142
+ if (headClose !== -1) {
143
+ return rawHtml.slice(0, headClose) + injection + rawHtml.slice(headClose);
144
+ }
145
+ return injection + rawHtml;
146
+ }, [rawHtml]);
147
+
148
+ const handleResize = useCallback((height: number) => {
149
+ setIframeHeight(height);
150
+ }, []);
151
+
152
+ const hook = useHtmlAnnotation({
153
+ iframeRef,
154
+ annotations,
155
+ onAddAnnotation,
156
+ onSelectAnnotation,
157
+ selectedAnnotationId,
158
+ mode,
159
+ onResize: handleResize,
160
+ });
161
+
162
+ useEffect(() => {
163
+ function handler(e: MessageEvent) {
164
+ if (e.data?.type === `${PREFIX}ready`) {
165
+ setIframeReady(true);
166
+ }
167
+ }
168
+ window.addEventListener("message", handler);
169
+ return () => window.removeEventListener("message", handler);
170
+ }, []);
171
+
172
+ useEffect(() => {
173
+ if (!iframeReady) return;
174
+ if (annotations.length > 0) {
175
+ hook.applyAnnotations(annotations);
176
+ }
177
+ }, [iframeReady]); // eslint-disable-line react-hooks/exhaustive-deps
178
+
179
+ // Tell the bridge the current input method (drag vs pinpoint). Re-posts on
180
+ // ready (fresh iframe) and whenever the user switches it in the toolstrip.
181
+ useEffect(() => {
182
+ if (!iframeReady) return;
183
+ iframeRef.current?.contentWindow?.postMessage(
184
+ { type: `${PREFIX}set-input-method`, method: inputMethod },
185
+ "*",
186
+ );
187
+ }, [iframeReady, inputMethod]);
188
+
189
+ useEffect(() => {
190
+ if (!iframeReady) return;
191
+ function sendTheme() {
192
+ const tokens = readThemeTokens();
193
+ iframeRef.current?.contentWindow?.postMessage(
194
+ { type: `${PREFIX}theme`, tokens, isLight: isLightTheme() },
195
+ "*",
196
+ );
197
+ }
198
+ sendTheme();
199
+ const observer = new MutationObserver(sendTheme);
200
+ observer.observe(document.documentElement, {
201
+ attributes: true,
202
+ attributeFilter: ["class", "style"],
203
+ });
204
+ return () => observer.disconnect();
205
+ }, [iframeReady]);
206
+
207
+ useImperativeHandle(ref, () => ({
208
+ removeHighlight: hook.removeHighlight,
209
+ clearAllHighlights: hook.clearAllHighlights,
210
+ applySharedAnnotations: hook.applyAnnotations,
211
+ }));
212
+
213
+ const handleGlobalCommentSubmit = useCallback(
214
+ (text: string, images?: ImageAttachment[]) => {
215
+ onAddAnnotation({
216
+ id: `global-${Date.now()}`,
217
+ blockId: "",
218
+ startOffset: 0,
219
+ endOffset: 0,
220
+ type: AnnotationType.GLOBAL_COMMENT,
221
+ text: text.trim(),
222
+ originalText: "",
223
+ author: getIdentity(),
224
+ createdA: Date.now(),
225
+ images,
226
+ });
227
+ setGlobalCommentPopover(null);
228
+ },
229
+ [onAddAnnotation],
230
+ );
231
+
232
+ // Document-level controls (attachments + global comment). Shared between the
233
+ // normal layout (bar above the card) and full-viewport (floating overlay), so
234
+ // edge-to-edge HTML keeps these affordances rather than dropping them.
235
+ const actionButtons = (
236
+ <>
237
+ {diffAvailable && onToggleDiff && (
238
+ <button
239
+ onClick={onToggleDiff}
240
+ className={`flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-md transition-colors cursor-pointer ${diffActive ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground bg-muted/50 hover:bg-muted"}`}
241
+ title={diffActive ? "Hide changes vs previous version" : "Show changes vs previous version"}
242
+ >
243
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
244
+ <path strokeLinecap="round" strokeLinejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-9L21 3m0 0l-4.5 4.5M21 3H7.5" />
245
+ </svg>
246
+ <span>{diffActive ? "Hide changes" : "Show changes"}</span>
247
+ </button>
248
+ )}
249
+ {onAddGlobalAttachment && onRemoveGlobalAttachment && (
250
+ <AttachmentsButton
251
+ images={globalAttachments}
252
+ onAdd={onAddGlobalAttachment}
253
+ onRemove={onRemoveGlobalAttachment}
254
+ variant="toolbar"
255
+ />
256
+ )}
257
+ <button
258
+ ref={globalCommentButtonRef}
259
+ onClick={() => {
260
+ setGlobalCommentPopover({
261
+ anchorEl: globalCommentButtonRef.current!,
262
+ contextText: "",
263
+ });
264
+ }}
265
+ className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground bg-muted/50 hover:bg-muted rounded-md transition-colors cursor-pointer"
266
+ title="Add global comment"
267
+ >
268
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
269
+ <path strokeLinecap="round" strokeLinejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.076-4.076a1.526 1.526 0 011.037-.443 48.282 48.282 0 005.68-.494c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" />
270
+ </svg>
271
+ <span>Comment</span>
272
+ </button>
273
+ </>
274
+ );
275
+
276
+ return (
277
+ <>
278
+ <div
279
+ className={`relative w-full${fullViewport ? " h-full flex flex-col" : ""}`}
280
+ style={fullViewport ? undefined : { maxWidth: maxWidth ?? undefined }}
281
+ >
282
+ {/* Action bar — above the iframe in normal mode (outside overflow:hidden). */}
283
+ {!fullViewport && (
284
+ <div data-print-hide className="flex justify-end gap-1 md:gap-2 mb-2">
285
+ {actionButtons}
286
+ </div>
287
+ )}
288
+
289
+ <article
290
+ data-print-region="article"
291
+ className={fullViewport ? "relative overflow-hidden w-full flex-1" : "relative bg-card rounded-xl shadow-xl overflow-hidden w-full"}
292
+ >
293
+ {/* Full-viewport mode has no card chrome, so float the same controls
294
+ over the top-right of the iframe (with a backdrop so they read over
295
+ any HTML). The selection toolbar is portaled separately. */}
296
+ {fullViewport && !hideControls && (
297
+ <div
298
+ data-print-hide
299
+ className="absolute top-3 right-3 z-10 flex items-center gap-1 md:gap-2 rounded-lg border border-border/50 bg-background/80 px-1.5 py-1 shadow-md backdrop-blur-sm"
300
+ >
301
+ {actionButtons}
302
+ </div>
303
+ )}
304
+ <iframe
305
+ ref={iframeRef}
306
+ srcDoc={srcdoc}
307
+ sandbox="allow-scripts"
308
+ style={{
309
+ width: "100%",
310
+ height: fullViewport ? "100%" : `${iframeHeight}px`,
311
+ border: "none",
312
+ display: "block",
313
+ colorScheme: "auto",
314
+ }}
315
+ title="HTML Plan Viewer"
316
+ />
317
+ </article>
318
+ </div>
319
+
320
+ {/* Toolbar portal */}
321
+ {hook.toolbarState &&
322
+ createPortal(
323
+ <AnnotationToolbar
324
+ positionMode="center-above"
325
+ element={hook.toolbarState.element}
326
+ copyText={hook.toolbarState.selectionText}
327
+ onAnnotate={hook.handleAnnotate}
328
+ onRequestComment={hook.handleRequestComment}
329
+ onQuickLabel={hook.handleQuickLabel}
330
+ onClose={hook.handleToolbarClose}
331
+ />,
332
+ document.body,
333
+ )}
334
+
335
+ {/* Comment popover portal */}
336
+ {hook.commentPopover &&
337
+ createPortal(
338
+ <CommentPopover
339
+ anchorEl={hook.commentPopover.anchorEl}
340
+ contextText={hook.commentPopover.contextText}
341
+ initialText={hook.commentPopover.initialText}
342
+ isGlobal={false}
343
+ onSubmit={hook.handleCommentSubmit}
344
+ onClose={hook.handleCommentClose}
345
+ onAskAI={onAskAI}
346
+ askAIContext={{
347
+ kind: "selection",
348
+ label: "Selected HTML",
349
+ text: hook.commentPopover.selectedText ?? hook.commentPopover.contextText,
350
+ }}
351
+ />,
352
+ document.body,
353
+ )}
354
+
355
+ {/* Quick label picker portal */}
356
+ {hook.quickLabelPicker &&
357
+ createPortal(
358
+ <FloatingQuickLabelPicker
359
+ anchorEl={hook.quickLabelPicker.anchorEl}
360
+ cursorHint={hook.quickLabelPicker.cursorHint}
361
+ onSelect={hook.handleFloatingQuickLabel}
362
+ onDismiss={hook.handleQuickLabelPickerDismiss}
363
+ />,
364
+ document.body,
365
+ )}
366
+
367
+ {/* Global comment popover portal */}
368
+ {globalCommentPopover &&
369
+ createPortal(
370
+ <CommentPopover
371
+ anchorEl={globalCommentPopover.anchorEl}
372
+ contextText={globalCommentPopover.contextText}
373
+ isGlobal={true}
374
+ onSubmit={handleGlobalCommentSubmit}
375
+ onClose={() => setGlobalCommentPopover(null)}
376
+ onAskAI={onAskAI}
377
+ askAIContext={{ kind: "general", label: "Document" }}
378
+ />,
379
+ document.body,
380
+ )}
381
+ </>
382
+ );
383
+ },
384
+ );
385
+
386
+ HtmlViewer.displayName = "HtmlViewer";