@plannotator/ui 0.29.1 → 0.31.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 (74) hide show
  1. package/README.md +1 -0
  2. package/components/ActionMenu.tsx +6 -1
  3. package/components/AgentsTab.tsx +8 -9
  4. package/components/AnalysisLayerToggle.tsx +48 -0
  5. package/components/AnnotationPanel.tsx +68 -35
  6. package/components/AnnotationToolbar.tsx +28 -8
  7. package/components/AnnotationToolstrip.tsx +9 -0
  8. package/components/CommentPopover.tsx +212 -46
  9. package/components/ConfirmDialog.tsx +42 -28
  10. package/components/InlineMarkdown.tsx +3 -2
  11. package/components/KeyboardShortcuts.tsx +9 -0
  12. package/components/Landing.tsx +1 -1
  13. package/components/LookAndFeelAnnouncementDialog.tsx +147 -178
  14. package/components/MarkdownEditor/embedPicker.ts +349 -0
  15. package/components/MarkdownEditor.tsx +12 -0
  16. package/components/ModeToggle.tsx +2 -1
  17. package/components/PermissionModeSetup.tsx +24 -5
  18. package/components/PinpointOverlay.tsx +11 -6
  19. package/components/PlanHeaderMenu.tsx +140 -1
  20. package/components/SearchableSelect.tsx +2 -0
  21. package/components/Settings.tsx +136 -10
  22. package/components/SkillReferenceMenu.tsx +9 -0
  23. package/components/StickyHeaderLane.tsx +9 -2
  24. package/components/TableOfContents.tsx +9 -4
  25. package/components/TextShimmer.tsx +8 -5
  26. package/components/ThemeProvider.tsx +43 -1
  27. package/components/ThemeTab.tsx +52 -1
  28. package/components/Tooltip.tsx +3 -1
  29. package/components/Viewer.tsx +19 -5
  30. package/components/VimTargetReticle.tsx +12 -4
  31. package/components/ai/DocumentAIChatPanel.tsx +1 -0
  32. package/components/core/button.tsx +14 -6
  33. package/components/html-viewer/HtmlViewer.tsx +215 -39
  34. package/components/html-viewer/bridge-script.ts +384 -66
  35. package/components/html-viewer/composerYield.ts +1 -51
  36. package/components/html-viewer/useHtmlAnnotation.ts +179 -58
  37. package/components/plan-diff/PlanCleanDiffView.tsx +1 -0
  38. package/components/sidebar/FileBrowser.tsx +17 -5
  39. package/components/sidebar/SidebarContainer.tsx +124 -28
  40. package/components/ui/button.tsx +10 -8
  41. package/components/ui/dialog.tsx +35 -25
  42. package/config/index.ts +6 -1
  43. package/config/reviewView.ts +42 -9
  44. package/config/settings.ts +141 -0
  45. package/hooks/useAIProviderConfig.ts +8 -7
  46. package/hooks/useActiveSection.ts +6 -4
  47. package/hooks/useAgentJobs.ts +3 -0
  48. package/hooks/useAnnotationHighlighter.ts +20 -0
  49. package/hooks/useIsMobile.ts +37 -0
  50. package/hooks/useLinkedDoc.ts +7 -0
  51. package/hooks/useScrollViewport.ts +74 -0
  52. package/hooks/useViewportEnvironment.ts +350 -0
  53. package/package.json +3 -2
  54. package/shortcuts/index.ts +3 -0
  55. package/shortcuts/plan-review/annotationMode.shortcuts.ts +91 -0
  56. package/shortcuts/plan-review/documentView.shortcuts.ts +26 -0
  57. package/shortcuts/plan-review/htmlAnnotate.shortcuts.ts +24 -0
  58. package/styles.css +1 -1
  59. package/theme.css +229 -0
  60. package/types.ts +34 -0
  61. package/utils/annotateAgentTerminal.ts +36 -5
  62. package/utils/blockTargeting.ts +6 -3
  63. package/utils/composerYield.ts +45 -0
  64. package/utils/htmlChrome.ts +20 -16
  65. package/utils/lookAndFeelAnnouncement.ts +12 -8
  66. package/utils/markdownExtensions.ts +57 -0
  67. package/utils/parser.ts +37 -2
  68. package/utils/vimNavigation.ts +4 -1
  69. package/utils/vimScroll.ts +9 -4
  70. package/utils/wideMode.ts +20 -0
  71. package/components/PlanAIAnnouncementDialog.tsx +0 -187
  72. package/components/VimModeAnnouncementDialog.tsx +0 -557
  73. package/utils/planAIAnnouncement.ts +0 -17
  74. package/utils/vimModeAnnouncement.ts +0 -23
package/README.md CHANGED
@@ -60,6 +60,7 @@ Building your own tooltip and removing the built-in double-click reset are host-
60
60
  const editorExtensions = [wikiLinks({ suggest, resolve, onOpen })]; // stable reference!
61
61
  <MarkdownEditor markdown={md} documentId={docId} editorHandleRef={ref} extensions={editorExtensions} />
62
62
  ```
63
+ - **`embedPicker(config)` and `embedSlashItem()` are re-exported from the same surface.** Compose the static item into `slashCommands({ items: [...] })` and pass the picker beside it in the stable `extensions` array. `getTargets`, `buildInsertLine`, optional `uploadTarget`, and optional `getNotice` stay live through callbacks. The host owns embed grammar and upload error UI; the package owns filtering, async anchor mapping, single-flight upload state, and paragraph-safe insertion through the re-exported `planEmbedInsert()`.
63
64
  - **The viewer resolves wiki-links synchronously.** `InlineMarkdown` takes `resolveLinkedDoc?: (target) => { label?; status?: 'active' | 'deleted' } | null` — called with the raw stored target (opaque ids like `doc_01XYZ`, no `.md` normalization). Return a `label` to display live titles (stored label is the fallback, target the last resort); return `status: 'deleted'` for a muted non-link ("Document deleted") instead of a live link. Absent or `null` → rendering is unchanged. Sync-only by design: back it with an in-memory cache.
64
65
 
65
66
  Requires `@plannotator/markdown-editor ^0.3.2` and `@plannotator/atomic-editor ^0.7.0`. See HANDOFF.md § "Wiki-link seams (0.27.0)".
@@ -68,6 +68,7 @@ interface ActionMenuItemProps {
68
68
  label: string;
69
69
  subtitle?: string;
70
70
  badge?: React.ReactNode;
71
+ disabled?: boolean;
71
72
  }
72
73
 
73
74
  export const ActionMenuItem: React.FC<ActionMenuItemProps> = ({
@@ -76,10 +77,14 @@ export const ActionMenuItem: React.FC<ActionMenuItemProps> = ({
76
77
  label,
77
78
  subtitle,
78
79
  badge,
80
+ disabled = false,
79
81
  }) => (
80
82
  <button
83
+ data-pn-touch-target
84
+ type="button"
81
85
  onClick={onClick}
82
- className="flex w-full items-center gap-2 px-3 py-2 text-left text-xs transition-colors hover:bg-muted"
86
+ disabled={disabled}
87
+ className="flex w-full items-center gap-2 px-3 py-2 text-left text-xs transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-45 disabled:hover:bg-transparent"
83
88
  >
84
89
  <span className="text-muted-foreground">{icon}</span>
85
90
  {subtitle ? (
@@ -15,6 +15,7 @@ import {
15
15
  } from 'lucide-react';
16
16
  import type { AgentJobInfo, AgentCapabilities } from '../types';
17
17
  import { isTerminalStatus } from '@plannotator/core/agent-jobs';
18
+ import { GUIDE_ENGINE_LABEL } from '@plannotator/core/guide';
18
19
  import { cn } from '../lib/utils';
19
20
  import { ReviewAgentsIcon } from './ReviewAgentsIcon';
20
21
  import { ClaudeIcon, CodexIcon, CopilotIcon, CursorIcon, OpenCodeIcon, PiIcon } from './icons/AgentIcons';
@@ -127,14 +128,7 @@ const ENGINE_ICON: Record<AgentEngine, React.FC<{ className?: string }>> = {
127
128
  // review surface offers the wider set (Cursor/OpenCode). Exported so the guide
128
129
  // takeover surfaces (GuideScreen, GuideEmptyState in packages/review-editor)
129
130
  // share this one source of truth instead of keeping their own copies in sync.
130
- export const REVIEW_ENGINE_LABEL: Record<ReviewEngine, string> = {
131
- claude: 'Claude',
132
- codex: 'Codex',
133
- cursor: 'Cursor',
134
- opencode: 'OpenCode',
135
- pi: 'Pi',
136
- copilot: 'Copilot',
137
- };
131
+ export const REVIEW_ENGINE_LABEL: Record<ReviewEngine, string> = GUIDE_ENGINE_LABEL;
138
132
 
139
133
  // Review-only icon map — the wide set. Tour keeps the narrow ENGINE_ICON.
140
134
  const REVIEW_ENGINE_ICON: Record<ReviewEngine, React.FC<{ className?: string }>> = {
@@ -872,7 +866,12 @@ export const AgentsTab: React.FC<AgentsTabProps> = ({
872
866
  ? { effort: tourClaudeEffort }
873
867
  : { reasoningEffort: tourCodexReasoning, ...(tourCodexFast && { fastMode: true }) }),
874
868
  });
875
- const buildGuideLaunch = (): LaunchParams => {
869
+ // Guide extra instructions (#1265) are server-stored: this surface has no
870
+ // editor, so it sends none and the server applies the stored standing
871
+ // instructions itself, keeping sidebar launches identical to launch-page
872
+ // ones without a second read path.
873
+ const buildGuideLaunch = (): LaunchParams => buildGuideEngineParams();
874
+ const buildGuideEngineParams = (): LaunchParams => {
876
875
  if (guideEngine === 'cursor') {
877
876
  // Same omission rules as buildReviewLaunch: auto/empty ⇒ engine default.
878
877
  // Guide-scoped model — deliberately NOT the shared cursorModel (see
@@ -0,0 +1,48 @@
1
+ import React from 'react';
2
+
3
+ interface AnalysisLayerToggleProps {
4
+ readonly checked: boolean;
5
+ readonly onChange: (checked: boolean) => void;
6
+ readonly label: React.ReactNode;
7
+ readonly description: string;
8
+ readonly disabled?: boolean;
9
+ readonly className?: string;
10
+ }
11
+
12
+ /** Full-row switch shared by Settings and the code-review analysis welcome. */
13
+ export function AnalysisLayerToggle({
14
+ checked,
15
+ onChange,
16
+ label,
17
+ description,
18
+ disabled = false,
19
+ className = '',
20
+ }: AnalysisLayerToggleProps) {
21
+ return (
22
+ <button
23
+ type="button"
24
+ role="switch"
25
+ aria-checked={checked}
26
+ disabled={disabled}
27
+ onClick={() => onChange(!checked)}
28
+ className={`flex min-h-11 w-full items-center justify-between gap-4 bg-transparent text-left disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
29
+ >
30
+ <span className="min-w-0">
31
+ <span className="block text-sm font-medium text-foreground">{label}</span>
32
+ <span className="mt-0.5 block text-xs leading-relaxed text-muted-foreground">{description}</span>
33
+ </span>
34
+ <span
35
+ aria-hidden="true"
36
+ className={`relative h-6 w-11 shrink-0 rounded-full transition-colors duration-150 ease-out motion-reduce:transition-none ${
37
+ checked ? 'bg-primary' : 'bg-muted'
38
+ }`}
39
+ >
40
+ <span
41
+ className={`absolute left-1 top-1 h-4 w-4 rounded-full bg-white shadow-sm transition-transform duration-150 ease-out motion-reduce:transition-none ${
42
+ checked ? 'translate-x-5' : 'translate-x-0'
43
+ }`}
44
+ />
45
+ </span>
46
+ </button>
47
+ );
48
+ }
@@ -80,9 +80,15 @@ interface PanelProps {
80
80
  * resolve UI). The panel stays presentation-only; clicks inside the slot
81
81
  * do not select the card. Default: nothing rendered. */
82
82
  renderCardFooter?: (annotation: Annotation) => React.ReactNode;
83
- /** Hide every mutation affordance (delete/edit, direct-edit discard, and host card footers).
84
- * Selection and scrolling still work. Default false — today's behavior. */
83
+ /** Hide every built-in mutation affordance (delete/edit, direct-edit
84
+ * discard). The host footer slot still renders: its contents are
85
+ * host-owned and may be read affordances (replies, links), so the host
86
+ * gates what belongs in it. Selection and scrolling still work.
87
+ * Default false — today's behavior. */
85
88
  readOnly?: boolean;
89
+ /** Embed only the timeline body in a host-owned stage. The host owns the
90
+ * title, close control, visible-viewport geometry, and focus boundary. */
91
+ presentation?: 'panel' | 'embedded';
86
92
  }
87
93
 
88
94
  export const AnnotationPanel: React.FC<PanelProps> = ({
@@ -108,8 +114,11 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
108
114
  directEdits = null,
109
115
  renderCardFooter,
110
116
  readOnly = false,
117
+ presentation = 'panel',
111
118
  }) => {
112
119
  const isMobile = useIsMobile();
120
+ const embedded = presentation === 'embedded';
121
+ const mobilePanel = isMobile && !embedded;
113
122
  const [copiedText, setCopiedText] = useState(false);
114
123
  const listRef = useRef<HTMLDivElement>(null);
115
124
  const sortedAnnotations = [...annotations].sort((a, b) => a.createdA - b.createdA);
@@ -135,47 +144,61 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
135
144
  <aside
136
145
  data-annotation-panel="true"
137
146
  data-plan-sidebar="right"
138
- className={`border-l border-border/50 bg-card flex flex-col flex-shrink-0 ${
139
- isMobile ? 'fixed top-12 bottom-0 right-0 z-[60] w-full max-w-sm shadow-2xl bg-card' : ''
147
+ className={`bg-card flex flex-col ${embedded ? 'h-full min-h-0 w-full flex-1' : 'flex-shrink-0 border-l border-border/50'} ${
148
+ mobilePanel ? 'fixed top-12 bottom-0 right-0 z-[60] w-full max-w-sm shadow-2xl bg-card' : ''
140
149
  }`}
141
- style={isMobile ? undefined : { width: width ?? 288 }}
150
+ style={embedded || mobilePanel ? undefined : { width: width ?? 288 }}
142
151
  >
143
152
  {/* Header */}
144
- <div className="border-b border-border/50">
145
- <div className="flex h-10 items-center justify-between px-3">
146
- <div className="flex items-center gap-2">
147
- <h2 className="text-xs font-medium text-foreground">
148
- Annotations
149
- </h2>
150
- {totalCount > 0 && (
151
- <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">
152
- {totalCount}
153
- </span>
153
+ {!embedded && (
154
+ <div className="border-b border-border/50">
155
+ <div className="flex h-10 items-center justify-between px-3">
156
+ <div className="flex items-center gap-2">
157
+ <h2 className="text-xs font-medium text-foreground">
158
+ Annotations
159
+ </h2>
160
+ {totalCount > 0 && (
161
+ <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">
162
+ {totalCount}
163
+ </span>
164
+ )}
165
+ </div>
166
+ {mobilePanel && onClose && (
167
+ <button
168
+ onClick={onClose}
169
+ 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"
170
+ title="Close panel"
171
+ aria-label="Close panel"
172
+ >
173
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
174
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
175
+ </svg>
176
+ </button>
154
177
  )}
155
178
  </div>
156
- {isMobile && onClose && (
179
+ {otherFileAnnotations && otherFileAnnotations.count > 0 && (
157
180
  <button
158
- onClick={onClose}
159
- 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"
160
- title="Close panel"
161
- aria-label="Close panel"
181
+ onClick={onOtherFileAnnotationsClick}
182
+ className="px-3 pb-2 text-[10px] text-primary/70 hover:text-primary transition-colors cursor-pointer"
183
+ title="Show annotated files in sidebar"
162
184
  >
163
- <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
164
- <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
165
- </svg>
185
+ +{otherFileAnnotations.count} in {otherFileAnnotations.files} other file{otherFileAnnotations.files === 1 ? '' : 's'}
166
186
  </button>
167
187
  )}
168
188
  </div>
169
- {otherFileAnnotations && otherFileAnnotations.count > 0 && (
170
- <button
171
- onClick={onOtherFileAnnotationsClick}
172
- className="px-3 pb-2 text-[10px] text-primary/70 hover:text-primary transition-colors cursor-pointer"
173
- title="Show annotated files in sidebar"
174
- >
175
- +{otherFileAnnotations.count} in {otherFileAnnotations.files} other file{otherFileAnnotations.files === 1 ? '' : 's'}
176
- </button>
177
- )}
178
- </div>
189
+ )}
190
+
191
+ {embedded && otherFileAnnotations && otherFileAnnotations.count > 0 && (
192
+ <button
193
+ type="button"
194
+ data-pn-touch-target="true"
195
+ onClick={onOtherFileAnnotationsClick}
196
+ className="min-h-11 flex-shrink-0 border-b border-border/50 px-3 text-left text-xs text-primary/80 active:bg-muted"
197
+ title="Show annotated files in navigator"
198
+ >
199
+ {otherFileAnnotations.count} more in {otherFileAnnotations.files} other file{otherFileAnnotations.files === 1 ? '' : 's'}
200
+ </button>
201
+ )}
179
202
 
180
203
  {/* List */}
181
204
  <OverlayScrollArea className="flex-1 min-h-0">
@@ -207,7 +230,7 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
207
230
  onDelete={() => onDelete(entry.annotation.id)}
208
231
  onEdit={onEdit ? (updates: Partial<Annotation>) => onEdit(entry.annotation.id, updates) : undefined}
209
232
  readOnly={readOnly}
210
- footer={readOnly ? undefined : renderCardFooter?.(entry.annotation)}
233
+ footer={renderCardFooter?.(entry.annotation)}
211
234
  />
212
235
  ) : (
213
236
  <CodeAnnotationCard
@@ -294,7 +317,7 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
294
317
  </aside>
295
318
  );
296
319
 
297
- if (isMobile) {
320
+ if (mobilePanel) {
298
321
  return (
299
322
  <>
300
323
  <div
@@ -490,6 +513,7 @@ const AnnotationCard: React.FC<{
490
513
  const editComposer = (
491
514
  <div onClick={(e: React.MouseEvent) => e.stopPropagation()}>
492
515
  <textarea
516
+ data-pn-mobile-editable="true"
493
517
  ref={textareaRef}
494
518
  value={editText}
495
519
  onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setEditText(e.target.value)}
@@ -531,6 +555,14 @@ const AnnotationCard: React.FC<{
531
555
  diff
532
556
  </span>
533
557
  )}
558
+ {annotation.pageUrl && (
559
+ <span
560
+ className="text-[9px] px-1.5 py-0.5 rounded font-medium bg-muted text-muted-foreground truncate max-w-[10rem]"
561
+ title={annotation.pageUrl}
562
+ >
563
+ {annotation.pageUrl}
564
+ </span>
565
+ )}
534
566
  <span className="text-[10px] text-muted-foreground/50 truncate">
535
567
  {annotation.author ? `${annotation.author}${isMe ? ' (me)' : ''} · ` : ''}{formatTimestamp(annotation.createdA)}
536
568
  </span>
@@ -721,6 +753,7 @@ const CodeAnnotationCard: React.FC<{
721
753
  {isEditing ? (
722
754
  <div className="mt-1.5" onClick={(e) => e.stopPropagation()}>
723
755
  <textarea
756
+ data-pn-mobile-editable="true"
724
757
  ref={textareaRef}
725
758
  value={editText}
726
759
  onChange={(e) => setEditText(e.target.value)}
@@ -4,6 +4,7 @@ import { createPortal } from "react-dom";
4
4
  import { useDismissOnOutsideAndEscape } from "../hooks/useDismissOnOutsideAndEscape";
5
5
  import { type QuickLabel, getQuickLabels } from "../utils/quickLabels";
6
6
  import { copyTextToClipboard } from "../utils/clipboard";
7
+ import { acquireTypeToCommentCapture } from "../shortcuts/plan-review/annotationMode.shortcuts";
7
8
  import { FloatingQuickLabelPicker } from "./FloatingQuickLabelPicker";
8
9
 
9
10
  type PositionMode = 'center-above' | 'top-right';
@@ -33,6 +34,10 @@ interface AnnotationToolbarProps {
33
34
  onQuickLabel?: (label: QuickLabel) => void;
34
35
  /** Text to copy when the button is clicked */
35
36
  copyText?: string;
37
+ /** Comment-only surfaces (HTML / live-app viewer): hide the Delete action.
38
+ * Markdown surfaces keep the full toolbar. Quick labels are already gated
39
+ * by the presence of onQuickLabel. */
40
+ commentOnly?: boolean;
36
41
  /** Hide the copy button (set when a keyboard copy handler exists) */
37
42
  hideCopyButton?: boolean;
38
43
  /** Close toolbar when element scrolls out of viewport */
@@ -52,6 +57,7 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
52
57
  onRequestComment,
53
58
  onQuickLabel,
54
59
  copyText,
60
+ commentOnly = false,
55
61
  hideCopyButton = false,
56
62
  closeOnScrollOut = false,
57
63
  isExiting = false,
@@ -146,7 +152,14 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
146
152
  };
147
153
 
148
154
  window.addEventListener("keydown", handleKeyDown);
149
- return () => window.removeEventListener("keydown", handleKeyDown);
155
+ // While this listener owns printable keys, the Shift+1..4 annotation-mode
156
+ // shortcuts must not fire: Shift+3 is "#", and a user typing "#" into a
157
+ // starting comment must not silently arm Redline (#1244 follow-up).
158
+ const releaseCapture = acquireTypeToCommentCapture();
159
+ return () => {
160
+ window.removeEventListener("keydown", handleKeyDown);
161
+ releaseCapture();
162
+ };
150
163
  }, [onClose, onRequestComment, onQuickLabel, quickLabels, showQuickLabels]);
151
164
 
152
165
  useDismissOnOutsideAndEscape({
@@ -197,7 +210,7 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
197
210
  to { opacity: 0; transform: translateY(8px)${translateX}; }
198
211
  }
199
212
  `}</style>
200
- <div className="flex items-center p-1 gap-0.5">
213
+ <div data-pn-annotation-toolbar-row="true" className="flex items-center p-1 gap-0.5">
201
214
  {!hideCopyButton && (
202
215
  <>
203
216
  <ToolbarButton
@@ -209,12 +222,14 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
209
222
  <div className="w-px h-5 bg-border mx-0.5" />
210
223
  </>
211
224
  )}
212
- <ToolbarButton
213
- onClick={() => handleTypeSelect(AnnotationType.DELETION)}
214
- icon={<TrashIcon />}
215
- label="Delete"
216
- className="text-destructive hover:bg-destructive/10"
217
- />
225
+ {!commentOnly && (
226
+ <ToolbarButton
227
+ onClick={() => handleTypeSelect(AnnotationType.DELETION)}
228
+ icon={<TrashIcon />}
229
+ label="Delete"
230
+ className="text-destructive hover:bg-destructive/10"
231
+ />
232
+ )}
218
233
  <ToolbarButton
219
234
  onClick={() => handleTypeSelect(AnnotationType.COMMENT)}
220
235
  icon={<CommentIcon />}
@@ -306,6 +321,11 @@ const ToolbarButton = React.forwardRef<HTMLButtonElement, {
306
321
  }>(({ onClick, icon, label, className }, ref) => (
307
322
  <button
308
323
  ref={ref}
324
+ // Icon-only controls: the markers are inert outside the compact touch
325
+ // scope, where theme.css grows them to var(--pn-touch-target). Desktop
326
+ // geometry is unchanged.
327
+ data-pn-touch-target="true"
328
+ data-pn-touch-target-icon="true"
309
329
  onClick={onClick}
310
330
  title={label}
311
331
  className={`p-1.5 rounded-md transition-colors ${className}`}
@@ -25,6 +25,11 @@ interface AnnotationToolstripProps {
25
25
  * narrow and leaves room for the diff badges.
26
26
  */
27
27
  iconOnly?: boolean;
28
+ /**
29
+ * Hide the drag/pinpoint input group entirely. Live app sessions are
30
+ * pinpoint-only, so the switch would be a dead control there.
31
+ */
32
+ hideInputMethodSwitch?: boolean;
28
33
  }
29
34
 
30
35
  export const AnnotationToolstrip: React.FC<AnnotationToolstripProps> = ({
@@ -36,6 +41,7 @@ export const AnnotationToolstrip: React.FC<AnnotationToolstripProps> = ({
36
41
  compact = false,
37
42
  showHelpLink = true,
38
43
  iconOnly = false,
44
+ hideInputMethodSwitch = false,
39
45
  }) => {
40
46
  const [showHelp, setShowHelp] = useState(false);
41
47
  const [helpTab, setHelpTab] = useState<'selection' | 'plannotator'>('selection');
@@ -50,6 +56,7 @@ export const AnnotationToolstrip: React.FC<AnnotationToolstripProps> = ({
50
56
  <>
51
57
  <div className={`flex items-center flex-wrap ${compact ? 'gap-1' : 'gap-1.5'}`}>
52
58
  {/* Input method group */}
59
+ {!hideInputMethodSwitch && (
53
60
  <div className="inline-flex items-center gap-0.5 bg-muted/50 rounded-lg p-0.5 border border-border/30">
54
61
  <ToolstripButton
55
62
  active={inputMethod === 'drag'}
@@ -89,6 +96,7 @@ export const AnnotationToolstrip: React.FC<AnnotationToolstripProps> = ({
89
96
  }
90
97
  />
91
98
  </div>
99
+ )}
92
100
 
93
101
  {/* Action mode group */}
94
102
  <div className="inline-flex items-center gap-0.5 bg-muted/50 rounded-lg p-0.5 border border-border/30">
@@ -321,6 +329,7 @@ const ToolstripButton: React.FC<{
321
329
  onClick={onClick}
322
330
  onMouseEnter={() => setHovered(true)}
323
331
  onMouseLeave={() => setHovered(false)}
332
+ aria-pressed={active}
324
333
  className={`relative flex items-center h-7 rounded-md overflow-hidden ${colorClass}`}
325
334
  style={{ width: currentWidth, transition }}
326
335
  >