@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.
- package/README.md +1 -0
- package/components/ActionMenu.tsx +6 -1
- package/components/AgentsTab.tsx +8 -9
- package/components/AnalysisLayerToggle.tsx +48 -0
- package/components/AnnotationPanel.tsx +68 -35
- package/components/AnnotationToolbar.tsx +28 -8
- package/components/AnnotationToolstrip.tsx +9 -0
- package/components/CommentPopover.tsx +212 -46
- package/components/ConfirmDialog.tsx +42 -28
- package/components/InlineMarkdown.tsx +3 -2
- package/components/KeyboardShortcuts.tsx +9 -0
- package/components/Landing.tsx +1 -1
- package/components/LookAndFeelAnnouncementDialog.tsx +147 -178
- package/components/MarkdownEditor/embedPicker.ts +349 -0
- package/components/MarkdownEditor.tsx +12 -0
- package/components/ModeToggle.tsx +2 -1
- package/components/PermissionModeSetup.tsx +24 -5
- package/components/PinpointOverlay.tsx +11 -6
- package/components/PlanHeaderMenu.tsx +140 -1
- package/components/SearchableSelect.tsx +2 -0
- package/components/Settings.tsx +136 -10
- package/components/SkillReferenceMenu.tsx +9 -0
- package/components/StickyHeaderLane.tsx +9 -2
- package/components/TableOfContents.tsx +9 -4
- package/components/TextShimmer.tsx +8 -5
- package/components/ThemeProvider.tsx +43 -1
- package/components/ThemeTab.tsx +52 -1
- package/components/Tooltip.tsx +3 -1
- package/components/Viewer.tsx +19 -5
- package/components/VimTargetReticle.tsx +12 -4
- package/components/ai/DocumentAIChatPanel.tsx +1 -0
- package/components/core/button.tsx +14 -6
- package/components/html-viewer/HtmlViewer.tsx +215 -39
- package/components/html-viewer/bridge-script.ts +384 -66
- package/components/html-viewer/composerYield.ts +1 -51
- package/components/html-viewer/useHtmlAnnotation.ts +179 -58
- package/components/plan-diff/PlanCleanDiffView.tsx +1 -0
- package/components/sidebar/FileBrowser.tsx +17 -5
- package/components/sidebar/SidebarContainer.tsx +124 -28
- package/components/ui/button.tsx +10 -8
- package/components/ui/dialog.tsx +35 -25
- package/config/index.ts +6 -1
- package/config/reviewView.ts +42 -9
- package/config/settings.ts +141 -0
- package/hooks/useAIProviderConfig.ts +8 -7
- package/hooks/useActiveSection.ts +6 -4
- package/hooks/useAgentJobs.ts +3 -0
- package/hooks/useAnnotationHighlighter.ts +20 -0
- package/hooks/useIsMobile.ts +37 -0
- package/hooks/useLinkedDoc.ts +7 -0
- package/hooks/useScrollViewport.ts +74 -0
- package/hooks/useViewportEnvironment.ts +350 -0
- package/package.json +3 -2
- package/shortcuts/index.ts +3 -0
- package/shortcuts/plan-review/annotationMode.shortcuts.ts +91 -0
- package/shortcuts/plan-review/documentView.shortcuts.ts +26 -0
- package/shortcuts/plan-review/htmlAnnotate.shortcuts.ts +24 -0
- package/styles.css +1 -1
- package/theme.css +229 -0
- package/types.ts +34 -0
- package/utils/annotateAgentTerminal.ts +36 -5
- package/utils/blockTargeting.ts +6 -3
- package/utils/composerYield.ts +45 -0
- package/utils/htmlChrome.ts +20 -16
- package/utils/lookAndFeelAnnouncement.ts +12 -8
- package/utils/markdownExtensions.ts +57 -0
- package/utils/parser.ts +37 -2
- package/utils/vimNavigation.ts +4 -1
- package/utils/vimScroll.ts +9 -4
- package/utils/wideMode.ts +20 -0
- package/components/PlanAIAnnouncementDialog.tsx +0 -187
- package/components/VimModeAnnouncementDialog.tsx +0 -557
- package/utils/planAIAnnouncement.ts +0 -17
- 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
|
-
|
|
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 ? (
|
package/components/AgentsTab.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
|
84
|
-
*
|
|
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={`
|
|
139
|
-
|
|
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={
|
|
150
|
+
style={embedded || mobilePanel ? undefined : { width: width ?? 288 }}
|
|
142
151
|
>
|
|
143
152
|
{/* Header */}
|
|
144
|
-
|
|
145
|
-
<div className="
|
|
146
|
-
<div className="flex items-center
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
{
|
|
179
|
+
{otherFileAnnotations && otherFileAnnotations.count > 0 && (
|
|
157
180
|
<button
|
|
158
|
-
onClick={
|
|
159
|
-
className="
|
|
160
|
-
title="
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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={
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
>
|