@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,86 @@
1
+ import React from 'react';
2
+ import { Annotation, AnnotationType, Block } from '../types';
3
+
4
+ interface SidebarProps {
5
+ annotations: Annotation[];
6
+ blocks: Block[];
7
+ onSelect: (id: string) => void;
8
+ onDelete: (id: string) => void;
9
+ selectedId: string | null;
10
+ }
11
+
12
+ export const AnnotationSidebar: React.FC<SidebarProps> = ({
13
+ annotations,
14
+ blocks,
15
+ onSelect,
16
+ onDelete,
17
+ selectedId
18
+ }) => {
19
+ const sortedAnnotations = [...annotations].sort((a, b) => {
20
+ const blockA = blocks.findIndex(blk => blk.id === a.blockId);
21
+ const blockB = blocks.findIndex(blk => blk.id === b.blockId);
22
+ if (blockA !== blockB) return blockA - blockB;
23
+ return a.startOffset - b.startOffset;
24
+ });
25
+
26
+ return (
27
+ <div className="w-[min(22rem,32vw)] min-w-[16rem] border-l border-border/50 bg-card/50 backdrop-blur-sm h-full flex flex-col transition-colors">
28
+ <div className="p-4 border-b border-border/50 flex items-center justify-between">
29
+ <h2 className="font-semibold text-foreground">Review Changes</h2>
30
+ <span className="text-xs bg-muted text-muted-foreground px-2 py-1 rounded-full">
31
+ {annotations.length}
32
+ </span>
33
+ </div>
34
+
35
+ <div className="flex-1 overflow-y-auto p-4 space-y-4">
36
+ {sortedAnnotations.length === 0 ? (
37
+ <div className="text-center text-muted-foreground mt-10 text-sm">
38
+ <p>No annotations yet.</p>
39
+ <p className="mt-2 text-xs">Select text in the document to add comments or suggest changes.</p>
40
+ </div>
41
+ ) : (
42
+ sortedAnnotations.map(ann => (
43
+ <div
44
+ key={ann.id}
45
+ onClick={() => onSelect(ann.id)}
46
+ className={`
47
+ group relative p-3 rounded-lg border transition-all cursor-pointer
48
+ ${selectedId === ann.id
49
+ ? 'bg-accent/20 border-accent shadow-sm'
50
+ : 'bg-card border-border hover:border-accent hover:shadow-sm'
51
+ }
52
+ `}
53
+ >
54
+ <div className="flex justify-between items-start mb-1">
55
+ <span className={`
56
+ text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded
57
+ ${ann.type === AnnotationType.DELETION ? 'bg-destructive/20 text-destructive' :
58
+ 'bg-accent/20 text-accent'}
59
+ `}>
60
+ {ann.type}
61
+ </span>
62
+ <button
63
+ onClick={(e) => { e.stopPropagation(); onDelete(ann.id); }}
64
+ className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-destructive transition-all p-1 rounded hover:bg-destructive/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
65
+ title="Remove Annotation"
66
+ >
67
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
68
+ </button>
69
+ </div>
70
+
71
+ <div className="text-sm text-muted-foreground mb-2 font-mono bg-muted p-2 rounded-lg text-xs truncate">
72
+ "{ann.originalText}"
73
+ </div>
74
+
75
+ {(ann.text && ann.type !== AnnotationType.DELETION) && (
76
+ <div className="text-sm text-foreground font-medium pl-3 border-l-2 border-primary">
77
+ {ann.text}
78
+ </div>
79
+ )}
80
+ </div>
81
+ ))
82
+ )}
83
+ </div>
84
+ </div>
85
+ );
86
+ };
@@ -0,0 +1,323 @@
1
+ import React, { useState, useEffect, useRef, useMemo, useCallback } from "react";
2
+ import { AnnotationType } from "../types";
3
+ import { createPortal } from "react-dom";
4
+ import { useDismissOnOutsideAndEscape } from "../hooks/useDismissOnOutsideAndEscape";
5
+ import { type QuickLabel, getQuickLabels } from "../utils/quickLabels";
6
+ import { FloatingQuickLabelPicker } from "./FloatingQuickLabelPicker";
7
+
8
+ type PositionMode = 'center-above' | 'top-right';
9
+
10
+ const THUMBS_UP_LABEL: QuickLabel = {
11
+ id: 'thumbs-up',
12
+ emoji: '👍',
13
+ text: 'Looks good',
14
+ color: 'green',
15
+ };
16
+
17
+ const isEditableElement = (node: EventTarget | Element | null): boolean => {
18
+ if (!(node instanceof Element)) return false;
19
+ if (node.matches('input, textarea, select, [role="textbox"]')) return true;
20
+ if (node.closest('[contenteditable]:not([contenteditable="false"])')) return true;
21
+ return (node as HTMLElement).isContentEditable;
22
+ };
23
+
24
+ interface AnnotationToolbarProps {
25
+ element: HTMLElement;
26
+ positionMode: PositionMode;
27
+ onAnnotate: (type: AnnotationType) => void;
28
+ onClose: () => void;
29
+ /** Called when user wants to write a comment (opens CommentPopover in parent) */
30
+ onRequestComment?: (initialChar?: string) => void;
31
+ /** Called when a quick label chip is selected */
32
+ onQuickLabel?: (label: QuickLabel) => void;
33
+ /** Text to copy when the button is clicked */
34
+ copyText?: string;
35
+ /** Hide the copy button (set when a keyboard copy handler exists) */
36
+ hideCopyButton?: boolean;
37
+ /** Close toolbar when element scrolls out of viewport */
38
+ closeOnScrollOut?: boolean;
39
+ /** Exit animation state */
40
+ isExiting?: boolean;
41
+ /** Hover callbacks for code block behavior */
42
+ onMouseEnter?: () => void;
43
+ onMouseLeave?: () => void;
44
+ }
45
+
46
+ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
47
+ element,
48
+ positionMode,
49
+ onAnnotate,
50
+ onClose,
51
+ onRequestComment,
52
+ onQuickLabel,
53
+ copyText,
54
+ hideCopyButton = false,
55
+ closeOnScrollOut = false,
56
+ isExiting = false,
57
+ onMouseEnter,
58
+ onMouseLeave,
59
+ }) => {
60
+ const [position, setPosition] = useState<{ top: number; left?: number; right?: number } | null>(null);
61
+ const [copied, setCopied] = useState(false);
62
+ const [showQuickLabels, setShowQuickLabels] = useState(false);
63
+ const toolbarRef = useRef<HTMLDivElement>(null);
64
+ const zapButtonRef = useRef<HTMLButtonElement>(null);
65
+ const quickLabels = useMemo(() => getQuickLabels(), []);
66
+
67
+ useEffect(() => { setCopied(false); }, [element]);
68
+
69
+ const handleCopy = async () => {
70
+ let textToCopy = copyText;
71
+ if (!textToCopy) {
72
+ const codeEl = element.querySelector('code');
73
+ textToCopy = codeEl?.textContent || element.textContent || '';
74
+ }
75
+ try {
76
+ await navigator.clipboard.writeText(textToCopy);
77
+ } catch {
78
+ const textarea = document.createElement('textarea');
79
+ textarea.value = textToCopy;
80
+ textarea.style.cssText = 'position:fixed;opacity:0';
81
+ document.body.appendChild(textarea);
82
+ textarea.select();
83
+ document.execCommand('copy');
84
+ textarea.remove();
85
+ }
86
+ setCopied(true);
87
+ setTimeout(() => setCopied(false), 1500);
88
+ };
89
+
90
+ // Update position on scroll/resize
91
+ useEffect(() => {
92
+ const updatePosition = () => {
93
+ const rect = element.getBoundingClientRect();
94
+
95
+ if (closeOnScrollOut && (rect.bottom < 0 || rect.top > window.innerHeight)) {
96
+ onClose();
97
+ return;
98
+ }
99
+
100
+ if (positionMode === 'center-above') {
101
+ setPosition({
102
+ top: rect.top - 48,
103
+ left: rect.left + rect.width / 2,
104
+ });
105
+ } else {
106
+ setPosition({
107
+ top: rect.top - 40,
108
+ right: window.innerWidth - rect.right,
109
+ });
110
+ }
111
+ };
112
+
113
+ updatePosition();
114
+ window.addEventListener("scroll", updatePosition, true);
115
+ window.addEventListener("resize", updatePosition);
116
+
117
+ return () => {
118
+ window.removeEventListener("scroll", updatePosition, true);
119
+ window.removeEventListener("resize", updatePosition);
120
+ };
121
+ }, [element, positionMode, closeOnScrollOut, onClose]);
122
+
123
+ // Type-to-comment + Alt+N / bare digit quick label shortcuts
124
+ useEffect(() => {
125
+ const handleKeyDown = (e: KeyboardEvent) => {
126
+ if (e.isComposing) return;
127
+ if (isEditableElement(e.target) || isEditableElement(document.activeElement)) return;
128
+
129
+ // When picker is open, let FloatingQuickLabelPicker own all keyboard input
130
+ if (showQuickLabels) return;
131
+
132
+ if (e.key === "Escape") {
133
+ onClose();
134
+ return;
135
+ }
136
+
137
+ // Alt+N applies quick label (picker closed)
138
+ const isDigit = (e.code >= 'Digit1' && e.code <= 'Digit9') || e.code === 'Digit0';
139
+ if (isDigit && !e.ctrlKey && !e.metaKey && e.altKey) {
140
+ e.preventDefault();
141
+ const digit = parseInt(e.code.slice(5), 10);
142
+ const index = digit === 0 ? 9 : digit - 1;
143
+ if (index < quickLabels.length) {
144
+ onQuickLabel?.(quickLabels[index]);
145
+ }
146
+ return;
147
+ }
148
+
149
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
150
+ if (e.key === "Tab" || e.key === "Enter") return;
151
+ if (e.key.length !== 1) return;
152
+
153
+ onRequestComment?.(e.key);
154
+ };
155
+
156
+ window.addEventListener("keydown", handleKeyDown);
157
+ return () => window.removeEventListener("keydown", handleKeyDown);
158
+ }, [onClose, onRequestComment, onQuickLabel, quickLabels, showQuickLabels]);
159
+
160
+ useDismissOnOutsideAndEscape({
161
+ enabled: !showQuickLabels,
162
+ ref: toolbarRef,
163
+ onDismiss: onClose,
164
+ });
165
+
166
+ if (!position) return null;
167
+
168
+ const handleTypeSelect = (type: AnnotationType) => {
169
+ if (type === AnnotationType.DELETION) {
170
+ onAnnotate(type);
171
+ } else {
172
+ onRequestComment?.();
173
+ }
174
+ };
175
+
176
+ const isCentered = position.left !== undefined;
177
+ const translateX = isCentered ? ' translateX(-50%)' : '';
178
+
179
+ const style: React.CSSProperties = {
180
+ top: position.top,
181
+ ...(isCentered
182
+ ? { left: position.left, transform: 'translateX(-50%)' }
183
+ : { right: position.right }),
184
+ animation: isExiting
185
+ ? 'annotation-toolbar-out 0.15s ease-in forwards'
186
+ : 'annotation-toolbar-in 0.15s ease-out',
187
+ };
188
+
189
+ return createPortal(
190
+ <div
191
+ ref={toolbarRef}
192
+ className="annotation-toolbar fixed z-[100] bg-popover border border-border rounded-lg shadow-2xl"
193
+ style={style}
194
+ onMouseDown={(e) => e.stopPropagation()}
195
+ onMouseEnter={onMouseEnter}
196
+ onMouseLeave={onMouseLeave}
197
+ >
198
+ <style>{`
199
+ @keyframes annotation-toolbar-in {
200
+ from { opacity: 0; transform: translateY(12px)${translateX}; }
201
+ to { opacity: 1; transform: translateY(0)${translateX}; }
202
+ }
203
+ @keyframes annotation-toolbar-out {
204
+ from { opacity: 1; transform: translateY(0)${translateX}; }
205
+ to { opacity: 0; transform: translateY(8px)${translateX}; }
206
+ }
207
+ `}</style>
208
+ <div className="flex items-center p-1 gap-0.5">
209
+ {!hideCopyButton && (
210
+ <>
211
+ <ToolbarButton
212
+ onClick={handleCopy}
213
+ icon={copied ? <CheckIcon /> : <CopyIcon />}
214
+ label={copied ? "Copied!" : "Copy"}
215
+ className={copied ? "text-success" : "text-muted-foreground hover:bg-muted hover:text-foreground"}
216
+ />
217
+ <div className="w-px h-5 bg-border mx-0.5" />
218
+ </>
219
+ )}
220
+ <ToolbarButton
221
+ onClick={() => handleTypeSelect(AnnotationType.DELETION)}
222
+ icon={<TrashIcon />}
223
+ label="Delete"
224
+ className="text-destructive hover:bg-destructive/10"
225
+ />
226
+ <ToolbarButton
227
+ onClick={() => handleTypeSelect(AnnotationType.COMMENT)}
228
+ icon={<CommentIcon />}
229
+ label="Comment"
230
+ className="text-annotation-comment hover:bg-annotation-comment/10"
231
+ />
232
+ {onQuickLabel && (
233
+ <>
234
+ <ToolbarButton
235
+ ref={zapButtonRef}
236
+ onClick={() => setShowQuickLabels(prev => !prev)}
237
+ icon={<ZapIcon />}
238
+ label="Quick label"
239
+ className={showQuickLabels ? "text-amber-500 bg-amber-500/10" : "text-amber-500 hover:bg-amber-500/10"}
240
+ />
241
+ <ToolbarButton
242
+ onClick={() => onQuickLabel(THUMBS_UP_LABEL)}
243
+ icon={<span className="block w-4 h-4 text-sm leading-4 text-center">👍</span>}
244
+ label="Looks good"
245
+ className="hover:bg-green-500/10"
246
+ />
247
+ {showQuickLabels && zapButtonRef.current && (
248
+ <FloatingQuickLabelPicker
249
+ anchorEl={zapButtonRef.current}
250
+ onSelect={(label) => {
251
+ setShowQuickLabels(false);
252
+ onQuickLabel(label);
253
+ }}
254
+ onDismiss={() => setShowQuickLabels(false)}
255
+ />
256
+ )}
257
+ </>
258
+ )}
259
+ <div className="w-px h-5 bg-border mx-0.5" />
260
+ <ToolbarButton
261
+ onClick={onClose}
262
+ icon={<CloseIcon />}
263
+ label="Cancel"
264
+ className="text-muted-foreground hover:bg-muted"
265
+ />
266
+ </div>
267
+ </div>,
268
+ document.body
269
+ );
270
+ };
271
+
272
+ // Icons
273
+ const CopyIcon = () => (
274
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
275
+ <path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
276
+ </svg>
277
+ );
278
+
279
+ const CheckIcon = () => (
280
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
281
+ <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
282
+ </svg>
283
+ );
284
+
285
+ const TrashIcon = () => (
286
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
287
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
288
+ </svg>
289
+ );
290
+
291
+ const CommentIcon = () => (
292
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
293
+ <path strokeLinecap="round" strokeLinejoin="round" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
294
+ </svg>
295
+ );
296
+
297
+ const ZapIcon = () => (
298
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
299
+ <path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
300
+ </svg>
301
+ );
302
+
303
+ const CloseIcon = () => (
304
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
305
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
306
+ </svg>
307
+ );
308
+
309
+ const ToolbarButton = React.forwardRef<HTMLButtonElement, {
310
+ onClick: () => void;
311
+ icon: React.ReactNode;
312
+ label: string;
313
+ className: string;
314
+ }>(({ onClick, icon, label, className }, ref) => (
315
+ <button
316
+ ref={ref}
317
+ onClick={onClick}
318
+ title={label}
319
+ className={`p-1.5 rounded-md transition-colors ${className}`}
320
+ >
321
+ {icon}
322
+ </button>
323
+ ));