@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,731 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { Annotation, AnnotationType, Block, type CodeAnnotation, type EditorAnnotation } from '../types';
3
+ import { isCurrentUser } from '../utils/identity';
4
+ import { ImageThumbnail } from './ImageThumbnail';
5
+ import { EditorAnnotationCard } from './EditorAnnotationCard';
6
+ import { useIsMobile } from '../hooks/useIsMobile';
7
+ import { OverlayScrollArea } from './OverlayScrollArea';
8
+ import { Button } from './ui/button';
9
+ import { cn } from '../lib/utils';
10
+
11
+ // Card type-word colors. Deletion uses `destructive` (reliably red on every
12
+ // theme, matching the in-document .deletion highlight). Comment uses the
13
+ // `annotation-comment` token, which defaults to each theme's --accent (so it
14
+ // stays consistent with the .comment highlight) but is overridden to a legible
15
+ // blue in neutral themes whose accent is a low-contrast gray (e.g. "simple").
16
+ // Global has no in-document highlight, so it uses a fixed legible purple.
17
+ const TYPE_COLOR: Record<AnnotationType, string> = {
18
+ [AnnotationType.DELETION]: 'text-destructive',
19
+ [AnnotationType.COMMENT]: 'text-annotation-comment',
20
+ [AnnotationType.GLOBAL_COMMENT]: 'text-purple-500',
21
+ };
22
+
23
+ const TYPE_LABEL: Record<AnnotationType, string> = {
24
+ [AnnotationType.DELETION]: 'Deletion',
25
+ [AnnotationType.COMMENT]: 'Comment',
26
+ [AnnotationType.GLOBAL_COMMENT]: 'Global',
27
+ };
28
+
29
+ const PencilIcon = () => (
30
+ <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
31
+ <path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
32
+ </svg>
33
+ );
34
+
35
+ const TrashCardIcon = () => (
36
+ <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
37
+ <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" />
38
+ </svg>
39
+ );
40
+
41
+ interface DirectEditsPanelItem {
42
+ id: string;
43
+ title?: string;
44
+ label?: string;
45
+ added: number;
46
+ removed: number;
47
+ diffText: string;
48
+ description?: string;
49
+ onDiscard?: () => void;
50
+ }
51
+
52
+ interface PanelProps {
53
+ isOpen: boolean;
54
+ annotations: Annotation[];
55
+ blocks: Block[];
56
+ onSelect: (id: string) => void;
57
+ onDelete: (id: string) => void;
58
+ onEdit?: (id: string, updates: Partial<Annotation>) => void;
59
+ selectedId: string | null;
60
+ codeAnnotations?: CodeAnnotation[];
61
+ onSelectCodeAnnotation?: (id: string) => void;
62
+ onDeleteCodeAnnotation?: (id: string) => void;
63
+ onEditCodeAnnotation?: (id: string, updates: Partial<CodeAnnotation>) => void;
64
+ sharingEnabled?: boolean;
65
+ width?: number | string;
66
+ editorAnnotations?: EditorAnnotation[];
67
+ onDeleteEditorAnnotation?: (id: string) => void;
68
+ onClose?: () => void;
69
+ onQuickCopy?: () => Promise<void>;
70
+ onShare?: () => void;
71
+ otherFileAnnotations?: { count: number; files: number };
72
+ onOtherFileAnnotationsClick?: () => void;
73
+ /** Committed direct edits to one or more documents. Rendered as pinned cards
74
+ * above the annotation timeline with expandable unified diffs. */
75
+ directEdits?: DirectEditsPanelItem[] | null;
76
+ }
77
+
78
+ export const AnnotationPanel: React.FC<PanelProps> = ({
79
+ isOpen,
80
+ annotations,
81
+ blocks,
82
+ onSelect,
83
+ onDelete,
84
+ onEdit,
85
+ selectedId,
86
+ codeAnnotations = [],
87
+ onSelectCodeAnnotation,
88
+ onDeleteCodeAnnotation,
89
+ onEditCodeAnnotation,
90
+ sharingEnabled = true,
91
+ width,
92
+ editorAnnotations,
93
+ onDeleteEditorAnnotation,
94
+ onClose,
95
+ onQuickCopy,
96
+ onShare,
97
+ otherFileAnnotations,
98
+ onOtherFileAnnotationsClick,
99
+ directEdits = null,
100
+ }) => {
101
+ const isMobile = useIsMobile();
102
+ const [copiedText, setCopiedText] = useState(false);
103
+ const listRef = useRef<HTMLDivElement>(null);
104
+ const sortedAnnotations = [...annotations].sort((a, b) => a.createdA - b.createdA);
105
+ const sortedCodeAnnotations = [...codeAnnotations].sort((a, b) => a.createdAt - b.createdAt);
106
+ const timelineEntries = [
107
+ ...sortedAnnotations.map(annotation => ({ kind: 'plan' as const, ts: annotation.createdA, annotation })),
108
+ ...sortedCodeAnnotations.map(annotation => ({ kind: 'code' as const, ts: annotation.createdAt, annotation })),
109
+ ].sort((a, b) => a.ts - b.ts);
110
+ const totalCount = annotations.length + codeAnnotations.length + (editorAnnotations?.length ?? 0);
111
+
112
+ // Scroll selected annotation card into view
113
+ useEffect(() => {
114
+ if (!selectedId || !listRef.current) return;
115
+ const card = listRef.current.querySelector(`[data-annotation-id="${selectedId}"]`);
116
+ if (card) {
117
+ card.scrollIntoView({ behavior: 'smooth', block: 'center' });
118
+ }
119
+ }, [selectedId]);
120
+
121
+ if (!isOpen) return null;
122
+
123
+ const panel = (
124
+ <aside
125
+ data-annotation-panel="true"
126
+ data-plan-sidebar="right"
127
+ className={`border-l border-border/50 bg-card flex flex-col flex-shrink-0 ${
128
+ isMobile ? 'fixed top-12 bottom-0 right-0 z-[60] w-full max-w-sm shadow-2xl bg-card' : ''
129
+ }`}
130
+ style={isMobile ? undefined : { width: width ?? 288 }}
131
+ >
132
+ {/* Header */}
133
+ <div className="border-b border-border/50">
134
+ <div className="flex h-10 items-center justify-between px-3">
135
+ <div className="flex items-center gap-2">
136
+ <h2 className="text-xs font-medium text-foreground">
137
+ Annotations
138
+ </h2>
139
+ {totalCount > 0 && (
140
+ <span className="flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-primary/10 px-1 font-mono text-[10px] font-medium tabular-nums text-primary">
141
+ {totalCount}
142
+ </span>
143
+ )}
144
+ </div>
145
+ {isMobile && onClose && (
146
+ <button
147
+ onClick={onClose}
148
+ className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-foreground md:hidden"
149
+ title="Close panel"
150
+ aria-label="Close panel"
151
+ >
152
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
153
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
154
+ </svg>
155
+ </button>
156
+ )}
157
+ </div>
158
+ {otherFileAnnotations && otherFileAnnotations.count > 0 && (
159
+ <button
160
+ onClick={onOtherFileAnnotationsClick}
161
+ className="px-3 pb-2 text-[10px] text-primary/70 hover:text-primary transition-colors cursor-pointer"
162
+ title="Show annotated files in sidebar"
163
+ >
164
+ +{otherFileAnnotations.count} in {otherFileAnnotations.files} other file{otherFileAnnotations.files === 1 ? '' : 's'}
165
+ </button>
166
+ )}
167
+ </div>
168
+
169
+ {/* List */}
170
+ <OverlayScrollArea className="flex-1 min-h-0">
171
+ <div ref={listRef} className="p-2 flex flex-col gap-1.5">
172
+ {directEdits?.map((item) => (
173
+ <DirectEditsCard key={item.id} {...item} />
174
+ ))}
175
+ {totalCount === 0 ? (
176
+ (!directEdits || directEdits.length === 0) && (
177
+ <div className="flex flex-col items-center justify-center px-4 py-16 text-center">
178
+ <p className="text-xs text-muted-foreground/60">
179
+ No annotations yet
180
+ </p>
181
+ <p className="mt-1 text-[11px] text-muted-foreground/40">
182
+ Select text to annotate
183
+ </p>
184
+ </div>
185
+ )
186
+ ) : (
187
+ <>
188
+ {timelineEntries.map(entry => (
189
+ entry.kind === 'plan' ? (
190
+ <AnnotationCard
191
+ key={entry.annotation.id}
192
+ annotation={entry.annotation}
193
+ isSelected={selectedId === entry.annotation.id}
194
+ isMe={isCurrentUser(entry.annotation.author)}
195
+ onSelect={() => onSelect(entry.annotation.id)}
196
+ onDelete={() => onDelete(entry.annotation.id)}
197
+ onEdit={onEdit ? (updates: Partial<Annotation>) => onEdit(entry.annotation.id, updates) : undefined}
198
+ />
199
+ ) : (
200
+ <CodeAnnotationCard
201
+ key={entry.annotation.id}
202
+ annotation={entry.annotation}
203
+ isSelected={selectedId === entry.annotation.id}
204
+ isMe={isCurrentUser(entry.annotation.author)}
205
+ onSelect={() => onSelectCodeAnnotation?.(entry.annotation.id)}
206
+ onDelete={() => onDeleteCodeAnnotation?.(entry.annotation.id)}
207
+ onEdit={onEditCodeAnnotation ? (updates: Partial<CodeAnnotation>) => onEditCodeAnnotation(entry.annotation.id, updates) : undefined}
208
+ />
209
+ )
210
+ ))}
211
+ {editorAnnotations && editorAnnotations.length > 0 && (
212
+ <>
213
+ {timelineEntries.length > 0 && (
214
+ <div className="flex items-center gap-2 pt-2 pb-1">
215
+ <div className="flex-1 border-t border-border/30" />
216
+ <span className="text-[9px] font-semibold uppercase tracking-wider text-muted-foreground/60">Editor</span>
217
+ <div className="flex-1 border-t border-border/30" />
218
+ </div>
219
+ )}
220
+ {editorAnnotations.map(ann => (
221
+ <EditorAnnotationCard
222
+ key={ann.id}
223
+ annotation={ann}
224
+ onDelete={() => onDeleteEditorAnnotation?.(ann.id)}
225
+ />
226
+ ))}
227
+ </>
228
+ )}
229
+
230
+ </>
231
+ )}
232
+ </div>
233
+ </OverlayScrollArea>
234
+
235
+ {/* Quick Actions Footer */}
236
+ {totalCount > 0 && (
237
+ <div className="border-t border-border/50 px-3 py-2 flex gap-1.5">
238
+ {onQuickCopy && (
239
+ <button
240
+ onClick={async () => {
241
+ await onQuickCopy();
242
+ setCopiedText(true);
243
+ setTimeout(() => setCopiedText(false), 2000);
244
+ }}
245
+ className={`flex-1 flex items-center justify-center gap-1.5 rounded-lg py-2 text-xs font-medium transition-colors ${
246
+ copiedText ? 'text-green-500' : 'text-muted-foreground hover:bg-surface-1 hover:text-foreground'
247
+ }`}
248
+ >
249
+ {copiedText ? (
250
+ <>
251
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
252
+ <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
253
+ </svg>
254
+ Copied
255
+ </>
256
+ ) : (
257
+ <>
258
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
259
+ <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" />
260
+ </svg>
261
+ Copy
262
+ </>
263
+ )}
264
+ </button>
265
+ )}
266
+ {sharingEnabled && onShare && (
267
+ <button
268
+ onClick={onShare}
269
+ className="flex-1 flex items-center justify-center gap-1.5 rounded-lg py-2 text-xs font-medium transition-colors text-muted-foreground hover:bg-surface-1 hover:text-foreground"
270
+ >
271
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
272
+ <path strokeLinecap="round" strokeLinejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
273
+ </svg>
274
+ Share
275
+ </button>
276
+ )}
277
+ </div>
278
+ )}
279
+ </aside>
280
+ );
281
+
282
+ if (isMobile) {
283
+ return (
284
+ <>
285
+ <div
286
+ className="fixed inset-0 z-[59] bg-background/60 backdrop-blur-sm"
287
+ onClick={onClose}
288
+ />
289
+ {panel}
290
+ </>
291
+ );
292
+ }
293
+
294
+ return panel;
295
+ };
296
+
297
+ function formatTimestamp(ts: number): string {
298
+ const now = Date.now();
299
+ const diff = now - ts;
300
+ const seconds = Math.floor(diff / 1000);
301
+ const minutes = Math.floor(seconds / 60);
302
+ const hours = Math.floor(minutes / 60);
303
+ const days = Math.floor(hours / 24);
304
+
305
+ if (seconds < 60) return 'now';
306
+ if (minutes < 60) return `${minutes}m`;
307
+ if (hours < 24) return `${hours}h`;
308
+ if (days < 7) return `${days}d`;
309
+
310
+ return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
311
+ }
312
+
313
+ /** Pinned card for committed direct edits: +N/−M summary, expandable unified
314
+ * diff, and a two-step discard. Not part of the annotation timeline — edits
315
+ * are document state, not a selection-anchored note. */
316
+ const DirectEditsCard: React.FC<{
317
+ title?: string;
318
+ label?: string;
319
+ added: number;
320
+ removed: number;
321
+ diffText: string;
322
+ description?: string;
323
+ onDiscard?: () => void;
324
+ }> = ({ title = 'Edits', label, added, removed, diffText, description, onDiscard }) => {
325
+ const [expanded, setExpanded] = useState(false);
326
+ const [confirmDiscard, setConfirmDiscard] = useState(false);
327
+
328
+ // Auto-cancel the discard confirmation after a beat.
329
+ useEffect(() => {
330
+ if (!confirmDiscard) return;
331
+ const t = setTimeout(() => setConfirmDiscard(false), 3000);
332
+ return () => clearTimeout(t);
333
+ }, [confirmDiscard]);
334
+
335
+ // Show from the first hunk header; the ---/+++ preamble is noise here.
336
+ const diffLines = React.useMemo(() => {
337
+ const lines = diffText.split('\n');
338
+ const fileSeparators = lines.filter((l) => l.startsWith('===')).length;
339
+ if (fileSeparators > 1) return lines;
340
+ const firstHunk = lines.findIndex((l) => l.startsWith('@@'));
341
+ return firstHunk === -1 ? lines : lines.slice(firstHunk);
342
+ }, [diffText]);
343
+
344
+ return (
345
+ <div className="w-full rounded-lg px-3 py-2.5 bg-surface-1/40 ring-1 ring-border/40">
346
+ <div className="flex items-center gap-1.5">
347
+ <span className="text-[11px] font-medium text-primary">{title}</span>
348
+ {label && (
349
+ <span className="min-w-0 truncate text-[10px] text-muted-foreground" title={label}>
350
+ {label}
351
+ </span>
352
+ )}
353
+ <span className="font-mono text-[10px] tabular-nums">
354
+ <span className="text-success">+{added}</span>
355
+ <span className="text-muted-foreground/40">/</span>
356
+ <span className="text-destructive">-{removed}</span>
357
+ </span>
358
+ <div className="ml-auto flex items-center gap-1">
359
+ <button
360
+ type="button"
361
+ onClick={() => setExpanded((v) => !v)}
362
+ className="cursor-pointer rounded px-1.5 py-0.5 text-[10px] text-muted-foreground transition-colors hover:bg-surface-1 hover:text-foreground"
363
+ aria-expanded={expanded}
364
+ >
365
+ {expanded ? 'Hide diff' : 'Diff'}
366
+ </button>
367
+ {onDiscard && (
368
+ <button
369
+ type="button"
370
+ onClick={() => {
371
+ if (confirmDiscard) {
372
+ setConfirmDiscard(false);
373
+ onDiscard();
374
+ } else {
375
+ setConfirmDiscard(true);
376
+ }
377
+ }}
378
+ className={cn(
379
+ 'cursor-pointer rounded px-1.5 py-0.5 text-[10px] transition-colors',
380
+ confirmDiscard
381
+ ? 'bg-destructive/15 text-destructive hover:bg-destructive/25'
382
+ : 'text-muted-foreground hover:bg-surface-1 hover:text-destructive',
383
+ )}
384
+ >
385
+ {confirmDiscard ? 'Confirm?' : 'Discard'}
386
+ </button>
387
+ )}
388
+ </div>
389
+ </div>
390
+ <p className="mt-1 text-[10px] leading-snug text-muted-foreground/60">
391
+ {description ?? 'Your text changes — sent with the feedback as a diff.'}
392
+ </p>
393
+ {expanded && (
394
+ <pre className="mt-2 max-h-56 overflow-auto rounded-md bg-muted/40 p-2 font-mono text-[10px] leading-relaxed">
395
+ {diffLines.map((line, i) => (
396
+ <div
397
+ key={i}
398
+ className={
399
+ line.startsWith('+') ? 'text-success'
400
+ : line.startsWith('-') ? 'text-destructive'
401
+ : line.startsWith('@@') ? 'text-primary/70'
402
+ : 'text-muted-foreground'
403
+ }
404
+ >
405
+ {line.length === 0 ? ' ' : line}
406
+ </div>
407
+ ))}
408
+ </pre>
409
+ )}
410
+ </div>
411
+ );
412
+ };
413
+
414
+ const AnnotationCard: React.FC<{
415
+ annotation: Annotation;
416
+ isSelected: boolean;
417
+ isMe: boolean;
418
+ onSelect: () => void;
419
+ onDelete: () => void;
420
+ onEdit?: (updates: Partial<Annotation>) => void;
421
+ }> = ({ annotation, isSelected, isMe, onSelect, onDelete, onEdit }) => {
422
+ const [isEditing, setIsEditing] = useState(false);
423
+ const [editText, setEditText] = useState(annotation.text || '');
424
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
425
+
426
+ useEffect(() => {
427
+ if (isEditing && textareaRef.current) {
428
+ textareaRef.current.focus();
429
+ textareaRef.current.select();
430
+ }
431
+ }, [isEditing]);
432
+
433
+ // Update editText when annotation.text changes
434
+ useEffect(() => {
435
+ if (!isEditing) {
436
+ setEditText(annotation.text || '');
437
+ }
438
+ }, [annotation.text, isEditing]);
439
+
440
+ const handleStartEdit = (e: React.MouseEvent) => {
441
+ e.stopPropagation();
442
+ setEditText(annotation.text || '');
443
+ setIsEditing(true);
444
+ };
445
+
446
+ const handleSaveEdit = () => {
447
+ if (onEdit) {
448
+ onEdit({ text: editText });
449
+ }
450
+ setIsEditing(false);
451
+ };
452
+
453
+ const handleCancelEdit = () => {
454
+ setEditText(annotation.text || '');
455
+ setIsEditing(false);
456
+ };
457
+
458
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
459
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !e.nativeEvent.isComposing) {
460
+ e.preventDefault();
461
+ handleSaveEdit();
462
+ } else if (e.key === 'Escape') {
463
+ e.preventDefault();
464
+ handleCancelEdit();
465
+ }
466
+ };
467
+
468
+ const typeColor = TYPE_COLOR[annotation.type] ?? 'text-muted-foreground';
469
+ const typeLabel = TYPE_LABEL[annotation.type] ?? 'Note';
470
+ const isGlobal = annotation.type === AnnotationType.GLOBAL_COMMENT;
471
+
472
+ // Shared edit textarea — matches the prototype composer primitive
473
+ const editComposer = (
474
+ <div onClick={(e: React.MouseEvent) => e.stopPropagation()}>
475
+ <textarea
476
+ ref={textareaRef}
477
+ value={editText}
478
+ onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setEditText(e.target.value)}
479
+ onKeyDown={handleKeyDown}
480
+ placeholder="Add your comment..."
481
+ aria-label="Annotation comment"
482
+ className="w-full resize-none rounded-lg border border-border/50 bg-card px-2.5 py-2 text-base leading-relaxed text-foreground outline-none transition-colors placeholder:text-muted-foreground/50 focus:border-primary/40 focus:ring-1 focus:ring-primary/20"
483
+ style={{ fieldSizing: 'content', minHeight: 44 } as React.CSSProperties}
484
+ />
485
+ <div className="mt-1.5 flex justify-end gap-1.5">
486
+ <Button variant="ghost" size="xxs" onClick={handleCancelEdit}>Cancel</Button>
487
+ <Button size="xxs" disabled={!editText.trim()} onClick={handleSaveEdit}>Save</Button>
488
+ </div>
489
+ </div>
490
+ );
491
+
492
+ return (
493
+ <div
494
+ data-annotation-id={annotation.id}
495
+ role="button"
496
+ tabIndex={0}
497
+ onClick={onSelect}
498
+ onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => {
499
+ if ((e.key === 'Enter' || e.key === ' ') && e.target === e.currentTarget) {
500
+ e.preventDefault();
501
+ onSelect();
502
+ }
503
+ }}
504
+ className={cn(
505
+ 'group w-full cursor-pointer rounded-lg px-3 py-2.5 text-left transition-colors duration-150 outline-none focus-visible:ring-2 focus-visible:ring-ring/50',
506
+ isSelected ? 'bg-surface-1 ring-1 ring-border/50' : 'hover:bg-surface-1/50',
507
+ )}
508
+ >
509
+ {/* Header: type word + author · time + actions */}
510
+ <div className="mb-1.5 flex items-center gap-1.5">
511
+ <span className={cn('text-[11px] font-medium', typeColor)}>{typeLabel}</span>
512
+ {annotation.diffContext && (
513
+ <span className="text-[9px] px-1.5 py-0.5 rounded font-medium bg-muted text-muted-foreground">
514
+ diff
515
+ </span>
516
+ )}
517
+ <span className="text-[10px] text-muted-foreground/50 truncate">
518
+ {annotation.author ? `${annotation.author}${isMe ? ' (me)' : ''} · ` : ''}{formatTimestamp(annotation.createdA)}
519
+ </span>
520
+ <div className="ml-auto flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 [@media(hover:none)]:opacity-100">
521
+ {onEdit && annotation.type !== AnnotationType.DELETION && !isEditing && (
522
+ <button
523
+ type="button"
524
+ onClick={handleStartEdit}
525
+ className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-foreground"
526
+ title="Edit annotation"
527
+ >
528
+ <PencilIcon />
529
+ </button>
530
+ )}
531
+ <button
532
+ type="button"
533
+ onClick={(e: React.MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); onDelete(); }}
534
+ className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-destructive"
535
+ title="Delete annotation"
536
+ >
537
+ <TrashCardIcon />
538
+ </button>
539
+ </div>
540
+ </div>
541
+
542
+ {/* Global Comment - show text directly */}
543
+ {isGlobal ? (
544
+ isEditing ? (
545
+ editComposer
546
+ ) : (
547
+ <p className="whitespace-pre-wrap text-[13px] leading-relaxed text-foreground/90">
548
+ {annotation.text}
549
+ </p>
550
+ )
551
+ ) : (
552
+ <>
553
+ {/* Quote — the annotated text */}
554
+ <p className="mb-1.5 line-clamp-2 whitespace-pre-wrap font-mono text-[11px] leading-relaxed text-muted-foreground/80">
555
+ "{annotation.originalText}"
556
+ </p>
557
+
558
+ {/* Comment/Replacement Text */}
559
+ {annotation.type !== AnnotationType.DELETION && (
560
+ isEditing ? (
561
+ editComposer
562
+ ) : (
563
+ annotation.text && (
564
+ <p className="whitespace-pre-wrap text-[13px] leading-relaxed text-foreground/90">
565
+ {annotation.text}
566
+ </p>
567
+ )
568
+ )
569
+ )}
570
+ </>
571
+ )}
572
+
573
+ {/* Attached Images */}
574
+ {annotation.images && annotation.images.length > 0 && (
575
+ <div className="mt-2 flex flex-wrap gap-1.5">
576
+ {annotation.images.map((img, idx) => (
577
+ <div key={idx} className="text-center">
578
+ <ImageThumbnail
579
+ path={img.path}
580
+ size="sm"
581
+ showRemove={false}
582
+ />
583
+ <div className="text-[9px] text-muted-foreground truncate max-w-[3rem]" title={img.name}>{img.name}</div>
584
+ </div>
585
+ ))}
586
+ </div>
587
+ )}
588
+ </div>
589
+ );
590
+ };
591
+
592
+ const CodeAnnotationCard: React.FC<{
593
+ annotation: CodeAnnotation;
594
+ isSelected: boolean;
595
+ isMe: boolean;
596
+ onSelect: () => void;
597
+ onDelete: () => void;
598
+ onEdit?: (updates: Partial<CodeAnnotation>) => void;
599
+ }> = ({ annotation, isSelected, isMe, onSelect, onDelete, onEdit }) => {
600
+ const [isEditing, setIsEditing] = useState(false);
601
+ const [editText, setEditText] = useState(annotation.text || '');
602
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
603
+
604
+ useEffect(() => {
605
+ if (isEditing) {
606
+ textareaRef.current?.focus();
607
+ textareaRef.current?.select();
608
+ }
609
+ }, [isEditing]);
610
+
611
+ useEffect(() => {
612
+ if (!isEditing) setEditText(annotation.text || '');
613
+ }, [annotation.text, isEditing]);
614
+
615
+ const handleSaveEdit = () => {
616
+ onEdit?.({ text: editText });
617
+ setIsEditing(false);
618
+ };
619
+
620
+ const lineRange = annotation.lineStart === annotation.lineEnd
621
+ ? `line ${annotation.lineStart}`
622
+ : `lines ${annotation.lineStart}-${annotation.lineEnd}`;
623
+ const fileName = annotation.filePath.split('/').pop() || annotation.filePath;
624
+
625
+ const handleCancelEdit = () => {
626
+ setIsEditing(false);
627
+ setEditText(annotation.text || '');
628
+ };
629
+
630
+ return (
631
+ <div
632
+ data-annotation-id={annotation.id}
633
+ role="button"
634
+ tabIndex={0}
635
+ onClick={onSelect}
636
+ onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => {
637
+ if ((e.key === 'Enter' || e.key === ' ') && e.target === e.currentTarget) {
638
+ e.preventDefault();
639
+ onSelect();
640
+ }
641
+ }}
642
+ className={cn(
643
+ 'group w-full cursor-pointer rounded-lg px-3 py-2.5 text-left transition-colors duration-150 outline-none focus-visible:ring-2 focus-visible:ring-ring/50',
644
+ isSelected ? 'bg-surface-1 ring-1 ring-border/50' : 'hover:bg-surface-1/50',
645
+ )}
646
+ >
647
+ {/* Header: type word + author · time + actions */}
648
+ <div className="mb-1.5 flex items-center gap-1.5">
649
+ <span className="text-[11px] font-medium text-primary">Code</span>
650
+ <span className="text-[10px] text-muted-foreground/50 truncate">
651
+ {annotation.author ? `${annotation.author}${isMe ? ' (me)' : ''} · ` : ''}{formatTimestamp(annotation.createdAt)}
652
+ </span>
653
+ <div className="ml-auto flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 [@media(hover:none)]:opacity-100">
654
+ {onEdit && !isEditing && (
655
+ <button
656
+ type="button"
657
+ onClick={(e) => { e.stopPropagation(); setIsEditing(true); }}
658
+ className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-foreground"
659
+ title="Edit annotation"
660
+ >
661
+ <PencilIcon />
662
+ </button>
663
+ )}
664
+ <button
665
+ type="button"
666
+ onClick={(e) => { e.stopPropagation(); onDelete(); }}
667
+ className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-destructive"
668
+ title="Delete annotation"
669
+ >
670
+ <TrashCardIcon />
671
+ </button>
672
+ </div>
673
+ </div>
674
+
675
+ {/* File / line meta */}
676
+ <div className="rounded px-2 py-1 bg-surface-1 font-mono text-[11px] text-muted-foreground truncate" title={annotation.filePath}>
677
+ {fileName} · {lineRange}
678
+ </div>
679
+
680
+ {annotation.originalCode && (
681
+ <p className="mt-1.5 line-clamp-2 whitespace-pre-wrap font-mono text-[11px] leading-relaxed text-muted-foreground/80">
682
+ {annotation.originalCode}
683
+ </p>
684
+ )}
685
+
686
+ {isEditing ? (
687
+ <div className="mt-1.5" onClick={(e) => e.stopPropagation()}>
688
+ <textarea
689
+ ref={textareaRef}
690
+ value={editText}
691
+ onChange={(e) => setEditText(e.target.value)}
692
+ onKeyDown={(e) => {
693
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !e.nativeEvent.isComposing) {
694
+ e.preventDefault();
695
+ handleSaveEdit();
696
+ } else if (e.key === 'Escape') {
697
+ e.preventDefault();
698
+ handleCancelEdit();
699
+ }
700
+ }}
701
+ placeholder="Add your comment..."
702
+ aria-label="Annotation comment"
703
+ className="w-full resize-none rounded-lg border border-border/50 bg-card px-2.5 py-2 text-base leading-relaxed text-foreground outline-none transition-colors placeholder:text-muted-foreground/50 focus:border-primary/40 focus:ring-1 focus:ring-primary/20"
704
+ style={{ fieldSizing: 'content', minHeight: 44 } as React.CSSProperties}
705
+ />
706
+ <div className="mt-1.5 flex justify-end gap-1.5">
707
+ <Button variant="ghost" size="xxs" onClick={handleCancelEdit}>Cancel</Button>
708
+ <Button size="xxs" disabled={!editText.trim()} onClick={handleSaveEdit}>Save</Button>
709
+ </div>
710
+ </div>
711
+ ) : (
712
+ annotation.text && (
713
+ <p className="mt-1.5 whitespace-pre-wrap text-[13px] leading-relaxed text-foreground/90">
714
+ {annotation.text}
715
+ </p>
716
+ )
717
+ )}
718
+
719
+ {annotation.images && annotation.images.length > 0 && (
720
+ <div className="mt-2 flex flex-wrap gap-1.5">
721
+ {annotation.images.map((img) => (
722
+ <div key={img.path} className="text-center">
723
+ <ImageThumbnail path={img.path} size="sm" showRemove={false} />
724
+ <div className="text-[9px] text-muted-foreground truncate max-w-[3rem]" title={img.name}>{img.name}</div>
725
+ </div>
726
+ ))}
727
+ </div>
728
+ )}
729
+ </div>
730
+ );
731
+ };