@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,355 @@
1
+ import React, { useState, useRef, useLayoutEffect, useEffect } from 'react';
2
+ import type { EditorMode, InputMethod } from '../types';
3
+ import { TaterSpritePullup } from './TaterSpritePullup';
4
+
5
+ interface AnnotationToolstripProps {
6
+ inputMethod: InputMethod;
7
+ onInputMethodChange: (method: InputMethod) => void;
8
+ mode: EditorMode;
9
+ onModeChange: (mode: EditorMode) => void;
10
+ taterMode?: boolean;
11
+ /**
12
+ * Compact mode: used inside the sticky header lane. Buttons only expand for
13
+ * the active mode (no hover expansion), gap is tightened, and the help link
14
+ * is hidden.
15
+ */
16
+ compact?: boolean;
17
+ /**
18
+ * Show the "how does this work?" help link. Default true; hidden on the
19
+ * floating HTML toolstrip where space is tight.
20
+ */
21
+ showHelpLink?: boolean;
22
+ /**
23
+ * Icon-only mode: no button ever expands to show a label, even the active
24
+ * one. Used in the sticky header lane on mobile so the toolstrip stays
25
+ * narrow and leaves room for the diff badges.
26
+ */
27
+ iconOnly?: boolean;
28
+ }
29
+
30
+ export const AnnotationToolstrip: React.FC<AnnotationToolstripProps> = ({
31
+ inputMethod,
32
+ onInputMethodChange,
33
+ mode,
34
+ onModeChange,
35
+ taterMode,
36
+ compact = false,
37
+ showHelpLink = true,
38
+ iconOnly = false,
39
+ }) => {
40
+ const [showHelp, setShowHelp] = useState(false);
41
+ const [helpTab, setHelpTab] = useState<'selection' | 'plannotator'>('selection');
42
+ const [mounted, setMounted] = useState(false);
43
+
44
+ // Enable transitions only after first paint
45
+ useEffect(() => {
46
+ requestAnimationFrame(() => setMounted(true));
47
+ }, []);
48
+
49
+ return (
50
+ <>
51
+ <div className={`flex items-center flex-wrap ${compact ? 'gap-1' : 'gap-1.5'}`}>
52
+ {/* Input method group */}
53
+ <div className="inline-flex items-center gap-0.5 bg-muted/50 rounded-lg p-0.5 border border-border/30">
54
+ <ToolstripButton
55
+ active={inputMethod === 'drag'}
56
+ onClick={() => onInputMethodChange('drag')}
57
+ label="Select"
58
+ color="primary"
59
+ mounted={mounted}
60
+ compact={compact}
61
+ iconOnly={iconOnly}
62
+ icon={
63
+ <svg className="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
64
+ <path d="M12 20h-1a2 2 0 0 1-2-2 2 2 0 0 1-2 2H6"/>
65
+ <path d="M13 8h7a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-7"/>
66
+ <path d="M5 16H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h1"/>
67
+ <path d="M6 4h1a2 2 0 0 1 2 2 2 2 0 0 1 2-2h1"/>
68
+ <path d="M9 6v12"/>
69
+ </svg>
70
+ }
71
+ />
72
+ <ToolstripButton
73
+ active={inputMethod === 'pinpoint'}
74
+ onClick={() => onInputMethodChange('pinpoint')}
75
+ label="Pinpoint"
76
+ color="primary"
77
+ mounted={mounted}
78
+ compact={compact}
79
+ iconOnly={iconOnly}
80
+ icon={
81
+ <svg className="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
82
+ <circle cx="12" cy="12" r="10" />
83
+ <circle cx="12" cy="12" r="4" />
84
+ <path d="M12 2v4" />
85
+ <path d="M12 18v4" />
86
+ <path d="M2 12h4" />
87
+ <path d="M18 12h4" />
88
+ </svg>
89
+ }
90
+ />
91
+ </div>
92
+
93
+ {/* Action mode group */}
94
+ <div className="inline-flex items-center gap-0.5 bg-muted/50 rounded-lg p-0.5 border border-border/30">
95
+ <ToolstripButton
96
+ active={mode === 'selection'}
97
+ onClick={() => onModeChange('selection')}
98
+ label="Markup"
99
+ color="secondary"
100
+ mounted={mounted}
101
+ compact={compact}
102
+ iconOnly={iconOnly}
103
+ icon={
104
+ <svg className="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
105
+ <path d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
106
+ </svg>
107
+ }
108
+ />
109
+ <ToolstripButton
110
+ active={mode === 'comment'}
111
+ onClick={() => onModeChange('comment')}
112
+ label="Comment"
113
+ color="accent"
114
+ mounted={mounted}
115
+ compact={compact}
116
+ iconOnly={iconOnly}
117
+ icon={
118
+ <svg className="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
119
+ <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" />
120
+ </svg>
121
+ }
122
+ />
123
+ <ToolstripButton
124
+ active={mode === 'redline'}
125
+ onClick={() => onModeChange('redline')}
126
+ label="Redline"
127
+ color="destructive"
128
+ mounted={mounted}
129
+ compact={compact}
130
+ iconOnly={iconOnly}
131
+ icon={
132
+ <svg className="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
133
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 9.75L14.25 12m0 0l2.25 2.25M14.25 12l2.25-2.25M14.25 12L12 14.25m-2.58 4.92l-6.375-6.375a1.125 1.125 0 010-1.59L9.42 4.83c.211-.211.498-.33.796-.33H19.5a2.25 2.25 0 012.25 2.25v10.5a2.25 2.25 0 01-2.25 2.25h-9.284c-.298 0-.585-.119-.796-.33z" />
134
+ </svg>
135
+ }
136
+ />
137
+ <ToolstripButton
138
+ active={mode === 'quickLabel'}
139
+ onClick={() => onModeChange('quickLabel')}
140
+ label="Label"
141
+ color="warning"
142
+ mounted={mounted}
143
+ compact={compact}
144
+ iconOnly={iconOnly}
145
+ icon={
146
+ <svg className="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
147
+ <path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
148
+ </svg>
149
+ }
150
+ />
151
+ </div>
152
+
153
+ {/* Help */}
154
+ {!compact && showHelpLink && (
155
+ <button
156
+ onClick={() => setShowHelp(true)}
157
+ className="ml-2 text-[10px] text-muted-foreground/60 hover:text-muted-foreground transition-colors hidden sm:block"
158
+ >
159
+ how does this work?
160
+ </button>
161
+ )}
162
+ </div>
163
+
164
+ {/* Help Video Dialog */}
165
+ {showHelp && (
166
+ <div
167
+ className="fixed inset-0 z-[100] flex items-center justify-center bg-background/80 backdrop-blur-sm p-4"
168
+ onClick={() => setShowHelp(false)}
169
+ >
170
+ <div
171
+ className="bg-card border border-border rounded-xl w-full max-w-2xl shadow-2xl relative"
172
+ onClick={e => e.stopPropagation()}
173
+ >
174
+ {taterMode && <TaterSpritePullup />}
175
+ <div className="flex items-center justify-between p-4 border-b border-border">
176
+ <div className="flex items-center gap-1 bg-muted/50 rounded-lg p-0.5">
177
+ <button
178
+ onClick={() => setHelpTab('selection')}
179
+ className={`px-3 py-1.5 rounded-md text-xs font-medium transition-colors ${
180
+ helpTab === 'selection'
181
+ ? 'bg-background text-foreground shadow-sm'
182
+ : 'text-muted-foreground hover:text-foreground'
183
+ }`}
184
+ >
185
+ Selection Modes
186
+ </button>
187
+ <button
188
+ onClick={() => setHelpTab('plannotator')}
189
+ className={`px-3 py-1.5 rounded-md text-xs font-medium transition-colors ${
190
+ helpTab === 'plannotator'
191
+ ? 'bg-background text-foreground shadow-sm'
192
+ : 'text-muted-foreground hover:text-foreground'
193
+ }`}
194
+ >
195
+ How Plannotator Works
196
+ </button>
197
+ </div>
198
+ <button
199
+ onClick={() => setShowHelp(false)}
200
+ className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
201
+ >
202
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
203
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
204
+ </svg>
205
+ </button>
206
+ </div>
207
+ <div className="aspect-video">
208
+ <iframe
209
+ key={helpTab}
210
+ width="100%"
211
+ height="100%"
212
+ src={helpTab === 'selection'
213
+ ? 'https://www.youtube.com/embed/ZNt9jtfx9TY?autoplay=1'
214
+ : 'https://www.youtube.com/embed/a_AT7cEN_9I?autoplay=1'
215
+ }
216
+ title={helpTab === 'selection' ? 'How Selection Modes Work' : 'How Plannotator Works'}
217
+ frameBorder="0"
218
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
219
+ allowFullScreen
220
+ />
221
+ </div>
222
+ </div>
223
+ </div>
224
+ )}
225
+ </>
226
+ );
227
+ };
228
+
229
+ /* ─── Color system ─── */
230
+
231
+ const colorStyles = {
232
+ primary: {
233
+ active: 'bg-background text-foreground shadow-sm',
234
+ hover: 'text-primary/80 bg-primary/8',
235
+ inactive: 'text-muted-foreground hover:text-foreground',
236
+ },
237
+ secondary: {
238
+ active: 'bg-background text-foreground shadow-sm',
239
+ hover: 'text-secondary/80 bg-secondary/8',
240
+ inactive: 'text-muted-foreground hover:text-foreground',
241
+ },
242
+ accent: {
243
+ active: 'bg-background text-foreground shadow-sm',
244
+ hover: 'text-annotation-comment/80 bg-annotation-comment/8',
245
+ inactive: 'text-muted-foreground hover:text-foreground',
246
+ },
247
+ destructive: {
248
+ active: 'bg-background text-foreground shadow-sm',
249
+ hover: 'text-destructive/80 bg-destructive/8',
250
+ inactive: 'text-muted-foreground hover:text-foreground',
251
+ },
252
+ warning: {
253
+ active: 'bg-background text-foreground shadow-sm',
254
+ hover: 'text-amber-500/80 bg-amber-500/8',
255
+ inactive: 'text-muted-foreground hover:text-foreground',
256
+ },
257
+ } as const;
258
+
259
+ type ButtonColor = keyof typeof colorStyles;
260
+
261
+ /* ─── Constants ─── */
262
+
263
+ const ICON_SIZE = 28; // collapsed button width (px)
264
+ const H_PAD = 10; // horizontal padding when expanded (px) — matches px-2.5
265
+ const GAP = 6; // gap between icon and label (px) — matches gap-1.5
266
+ const ICON_INNER = 14; // icon element width (px)
267
+ const DURATION = 180; // transition ms
268
+
269
+ /* ─── Button ─── */
270
+
271
+ const ToolstripButton: React.FC<{
272
+ active: boolean;
273
+ onClick: () => void;
274
+ icon: React.ReactNode;
275
+ label: string;
276
+ color: ButtonColor;
277
+ mounted: boolean;
278
+ compact?: boolean;
279
+ iconOnly?: boolean;
280
+ }> = ({ active, onClick, icon, label, color, mounted, compact = false, iconOnly = false }) => {
281
+ const [hovered, setHovered] = useState(false);
282
+ const [labelWidth, setLabelWidth] = useState(0);
283
+ const measureRef = useRef<HTMLSpanElement>(null);
284
+ const styles = colorStyles[color];
285
+ const [isTouchDevice] = useState(() => 'ontouchstart' in window || navigator.maxTouchPoints > 0);
286
+
287
+ // Measure label text width synchronously before first paint
288
+ useLayoutEffect(() => {
289
+ if (measureRef.current) {
290
+ setLabelWidth(measureRef.current.offsetWidth);
291
+ }
292
+ }, [label]);
293
+
294
+ // iconOnly: never expand (mobile sticky lane).
295
+ // compact: only active expands (sm+ sticky lane — shows current mode).
296
+ // default: active or hovered expands (top-of-doc full toolstrip).
297
+ const expanded = iconOnly
298
+ ? false
299
+ : compact
300
+ ? active
301
+ : (active || hovered || isTouchDevice);
302
+ const expandedWidth = H_PAD + ICON_INNER + GAP + labelWidth + H_PAD;
303
+ const currentWidth = expanded ? expandedWidth : ICON_SIZE;
304
+
305
+ const colorClass = active
306
+ ? styles.active
307
+ : hovered
308
+ ? styles.hover
309
+ : styles.inactive;
310
+
311
+ const transition = mounted
312
+ ? `width ${DURATION}ms cubic-bezier(0.25, 0.46, 0.45, 0.94), background-color ${DURATION}ms ease, color ${DURATION}ms ease, box-shadow ${DURATION}ms ease`
313
+ : 'none';
314
+
315
+ const innerTransition = mounted
316
+ ? `padding-left ${DURATION}ms cubic-bezier(0.25, 0.46, 0.45, 0.94)`
317
+ : 'none';
318
+
319
+ return (
320
+ <button
321
+ onClick={onClick}
322
+ onMouseEnter={() => setHovered(true)}
323
+ onMouseLeave={() => setHovered(false)}
324
+ className={`relative flex items-center h-7 rounded-md overflow-hidden ${colorClass}`}
325
+ style={{ width: currentWidth, transition }}
326
+ >
327
+ {/* Inner flex container — fixed layout, no layout-shifting properties animated */}
328
+ <div
329
+ className="flex items-center whitespace-nowrap"
330
+ style={{ paddingLeft: expanded ? H_PAD : (ICON_SIZE - ICON_INNER) / 2, gap: GAP, transition: innerTransition }}
331
+ >
332
+ {icon}
333
+ <span
334
+ className="text-xs font-medium"
335
+ style={{
336
+ opacity: expanded ? 1 : 0,
337
+ transition: mounted ? `opacity ${expanded ? DURATION : DURATION * 0.6}ms ease ${expanded ? '60ms' : '0ms'}` : 'none',
338
+ }}
339
+ >
340
+ {label}
341
+ </span>
342
+ </div>
343
+
344
+ {/* Hidden measurement span — rendered offscreen to get label pixel width */}
345
+ <span
346
+ ref={measureRef}
347
+ className="text-xs font-medium absolute pointer-events-none"
348
+ style={{ visibility: 'hidden', position: 'absolute', left: -9999 }}
349
+ aria-hidden
350
+ >
351
+ {label}
352
+ </span>
353
+ </button>
354
+ );
355
+ };
@@ -0,0 +1,170 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import type { Agent } from '../hooks/useAgents';
3
+ import { getAgentSwitchSettings, saveAgentSwitchSettings, type AgentSwitchSettings } from '../utils/agentSwitch';
4
+
5
+ interface ApproveDropdownProps {
6
+ onApprove: () => void;
7
+ agents: Agent[];
8
+ disabled?: boolean;
9
+ isLoading?: boolean;
10
+ }
11
+
12
+ function getSelectedLabel(setting: AgentSwitchSettings, agents: Agent[]): string | null {
13
+ if (setting.switchTo === 'disabled') return null;
14
+ if (setting.switchTo === 'custom' && setting.customName) {
15
+ return setting.customName;
16
+ }
17
+ const match = agents.find(a => a.id.toLowerCase() === setting.switchTo.toLowerCase());
18
+ return match?.name ?? setting.switchTo;
19
+ }
20
+
21
+ function isSelected(agentId: string, setting: AgentSwitchSettings): boolean {
22
+ if (setting.switchTo === 'custom') return false;
23
+ if (setting.switchTo === 'disabled') return false;
24
+ return agentId.toLowerCase() === setting.switchTo.toLowerCase();
25
+ }
26
+
27
+ const Checkmark = () => (
28
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
29
+ <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
30
+ </svg>
31
+ );
32
+
33
+ export const ApproveDropdown: React.FC<ApproveDropdownProps> = ({
34
+ onApprove,
35
+ agents,
36
+ disabled = false,
37
+ isLoading = false,
38
+ }) => {
39
+ const [setting, setSetting] = useState<AgentSwitchSettings>(() => getAgentSwitchSettings());
40
+ const [isOpen, setIsOpen] = useState(false);
41
+ const dropdownRef = useRef<HTMLDivElement>(null);
42
+
43
+ useEffect(() => {
44
+ const handleClickOutside = (event: PointerEvent) => {
45
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
46
+ setIsOpen(false);
47
+ }
48
+ };
49
+ const handleEscape = (event: KeyboardEvent) => {
50
+ if (event.key === 'Escape') setIsOpen(false);
51
+ };
52
+ document.addEventListener('pointerdown', handleClickOutside);
53
+ document.addEventListener('keydown', handleEscape);
54
+ return () => {
55
+ document.removeEventListener('pointerdown', handleClickOutside);
56
+ document.removeEventListener('keydown', handleEscape);
57
+ };
58
+ }, []);
59
+
60
+ const handleSelect = (newSetting: AgentSwitchSettings) => {
61
+ setSetting(newSetting);
62
+ saveAgentSwitchSettings(newSetting);
63
+ setIsOpen(false);
64
+ };
65
+
66
+ const agentLabel = getSelectedLabel(setting, agents);
67
+ const isNoSwitch = setting.switchTo === 'disabled';
68
+ const isCustom = setting.switchTo === 'custom';
69
+ const notFound = agentLabel && !isNoSwitch && !isCustom
70
+ && !agents.some(a => a.id.toLowerCase() === setting.switchTo.toLowerCase());
71
+
72
+ const baseClasses = disabled
73
+ ? 'opacity-50 cursor-not-allowed bg-muted text-muted-foreground'
74
+ : 'bg-success text-success-foreground hover:opacity-90';
75
+
76
+ const handleApproveClick = () => {
77
+ setIsOpen(false);
78
+ onApprove();
79
+ };
80
+
81
+ return (
82
+ <div className="relative" ref={dropdownRef}>
83
+ {/* Mobile: simple button */}
84
+ <button
85
+ onClick={handleApproveClick}
86
+ disabled={disabled}
87
+ className={`md:hidden px-2 py-1 rounded-md text-xs font-medium transition-all ${baseClasses}`}
88
+ >
89
+ {isLoading ? '...' : 'OK'}
90
+ </button>
91
+
92
+ {/* Desktop: split button */}
93
+ <div className="hidden md:flex items-stretch">
94
+ <button
95
+ onClick={handleApproveClick}
96
+ disabled={disabled}
97
+ className={`px-2.5 py-1 rounded-l-md text-xs font-medium transition-all ${baseClasses}`}
98
+ >
99
+ {isLoading ? 'Approving...' : (
100
+ agentLabel ? (
101
+ <span className="flex items-center gap-1">
102
+ Approve
103
+ <span className="opacity-60">&rarr;</span>
104
+ <span className="max-w-[120px] truncate">{agentLabel}</span>
105
+ {notFound && <span className="opacity-60 text-[10px]">(?)</span>}
106
+ </span>
107
+ ) : 'Approve'
108
+ )}
109
+ </button>
110
+ <button
111
+ onClick={() => setIsOpen(!isOpen)}
112
+ disabled={disabled}
113
+ className={`px-1.5 py-1 rounded-r-md border-l border-success-foreground/20 text-xs transition-all ${baseClasses}`}
114
+ >
115
+ <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
116
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
117
+ </svg>
118
+ </button>
119
+ </div>
120
+
121
+ {/* Dropdown */}
122
+ {isOpen && (
123
+ <div className="absolute right-0 top-full mt-1 w-52 rounded-lg border border-border bg-popover shadow-xl z-[70] overflow-hidden py-1">
124
+ <div className="px-3 py-1.5 text-[10px] uppercase tracking-wider text-muted-foreground/70 font-medium">
125
+ Switch to agent
126
+ </div>
127
+ {agents.map((agent) => {
128
+ const selected = isSelected(agent.id, setting);
129
+ return (
130
+ <button
131
+ key={agent.id}
132
+ onClick={() => handleSelect({ switchTo: agent.id })}
133
+ className={`w-full px-3 py-1.5 text-left text-xs transition-colors flex items-center gap-2 ${
134
+ selected
135
+ ? 'text-primary bg-primary/10 font-medium'
136
+ : 'text-popover-foreground hover:bg-muted'
137
+ }`}
138
+ >
139
+ <span className="w-4 flex-shrink-0">{selected && <Checkmark />}</span>
140
+ <span className="truncate">{agent.name}</span>
141
+ </button>
142
+ );
143
+ })}
144
+ {isCustom && setting.customName && (
145
+ <button
146
+ onClick={() => setIsOpen(false)}
147
+ className="w-full px-3 py-1.5 text-left text-xs transition-colors flex items-center gap-2 text-primary bg-primary/10 font-medium"
148
+ >
149
+ <span className="w-4 flex-shrink-0"><Checkmark /></span>
150
+ <span className="truncate">{setting.customName}</span>
151
+ <span className="text-[10px] text-muted-foreground ml-auto">(custom)</span>
152
+ </button>
153
+ )}
154
+ <div className="border-t border-border my-1" />
155
+ <button
156
+ onClick={() => handleSelect({ switchTo: 'disabled' })}
157
+ className={`w-full px-3 py-1.5 text-left text-xs transition-colors flex items-center gap-2 ${
158
+ isNoSwitch
159
+ ? 'text-primary bg-primary/10 font-medium'
160
+ : 'text-popover-foreground hover:bg-muted'
161
+ }`}
162
+ >
163
+ <span className="w-4 flex-shrink-0">{isNoSwitch && <Checkmark />}</span>
164
+ No switch
165
+ </button>
166
+ </div>
167
+ )}
168
+ </div>
169
+ );
170
+ };