@plannotator/ui 0.28.0 → 0.29.1
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 +6 -0
- package/components/AISettingsTab.tsx +5 -4
- package/components/ActionMenu.tsx +5 -1
- package/components/AgentsTab.tsx +10 -23
- package/components/AnnotationPanel.tsx +9 -5
- package/components/AnnotationToolbar.tsx +5 -13
- package/components/AnnotationToolstrip.tsx +2 -2
- package/components/ApproveDropdown.tsx +1 -1
- package/components/BlockRenderer.tsx +1 -1
- package/components/CodeFilePopout.tsx +5 -4
- package/components/CommentPopover.tsx +391 -65
- package/components/DocBadges.tsx +29 -11
- package/components/ExportModal.tsx +16 -8
- package/components/GraphvizBlock.tsx +1 -1
- package/components/InlineMarkdown.tsx +30 -13
- package/components/KeyboardShortcuts.tsx +37 -2
- package/components/Landing.tsx +7 -7
- package/components/MenuVersionSection.tsx +4 -4
- package/components/MermaidBlock.tsx +1 -1
- package/components/ModeToggle.tsx +7 -6
- package/components/OpenInAppButton.tsx +2 -5
- package/components/PinpointOverlay.tsx +9 -7
- package/components/PlanHeaderMenu.tsx +8 -8
- package/components/PopoutDialog.tsx +6 -1
- package/components/ResizeHandle.tsx +1 -0
- package/components/Settings.tsx +172 -12
- package/components/SkillReferenceMenu.tsx +260 -0
- package/components/StickyHeaderLane.tsx +7 -0
- package/components/ThemeProvider.tsx +131 -32
- package/components/ThemeTab.tsx +123 -77
- package/components/ToolbarButtons.tsx +29 -8
- package/components/Viewer.tsx +396 -130
- package/components/VimKeyHud.tsx +695 -0
- package/components/VimModeAnnouncementDialog.tsx +557 -0
- package/components/VimModeOverlay.tsx +235 -0
- package/components/VimTargetReticle.tsx +284 -0
- package/components/ai/DocumentAIChatPanel.tsx +1 -1
- package/components/blocks/CodeBlock.tsx +18 -18
- package/components/blocks/TablePopout.tsx +7 -8
- package/components/blocks/TableToolbar.tsx +7 -8
- package/components/goal-setup/GoalSetupSurface.tsx +16 -3
- package/components/html-viewer/HtmlViewer.tsx +450 -47
- package/components/html-viewer/annotationNumbering.ts +37 -0
- package/components/html-viewer/bridge-script.ts +4051 -298
- package/components/html-viewer/composerYield.ts +51 -0
- package/components/html-viewer/srcdoc.ts +18 -3
- package/components/html-viewer/useHtmlAnnotation.ts +457 -32
- package/components/icons/themeIcons.tsx +1 -1
- package/components/plan-diff/PlanCleanDiffView.tsx +9 -9
- package/components/plan-diff/PlanDiffBadge.tsx +22 -1
- package/components/settings/HooksTab.tsx +12 -8
- package/components/sidebar/FileBrowser.tsx +4 -1
- package/components/themeModes.tsx +28 -0
- package/config/configStore.ts +76 -1
- package/config/settings.ts +152 -0
- package/configure.ts +9 -0
- package/globals.d.ts +7 -1
- package/hooks/useAIChat.ts +5 -2
- package/hooks/useAIProviderActivation.ts +47 -0
- package/hooks/useAIProviderConfig.ts +5 -1
- package/hooks/useAgentSettings.ts +64 -23
- package/hooks/useAgents.ts +4 -4
- package/hooks/useAnnotationHighlighter.ts +100 -3
- package/hooks/useArchive.ts +2 -1
- package/hooks/useFenceTheme.ts +17 -0
- package/hooks/useLinkedDoc.ts +68 -1
- package/hooks/usePinpoint.ts +76 -75
- package/hooks/usePlanDiff.ts +73 -2
- package/hooks/useSkillReferenceAutocomplete.ts +239 -0
- package/hooks/useUpdateCheck.ts +1 -2
- package/hooks/useVimDocumentFocus.ts +116 -0
- package/hooks/useVimSelection.ts +1063 -0
- package/package.json +4 -4
- package/print.css +14 -13
- package/shortcuts/core.ts +38 -13
- package/shortcuts/index.ts +10 -0
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +7 -0
- package/shortcuts/plan-review/vimSelection.shortcuts.ts +251 -0
- package/shortcuts/runtime.ts +111 -12
- package/styles.css +1 -1
- package/theme.css +504 -0
- package/themes/colorblind.css +89 -0
- package/themes/plannotator.css +2 -2
- package/types.ts +93 -10
- package/utils/agentSwitch.ts +33 -7
- package/utils/blockTargeting.ts +462 -178
- package/utils/clipboard.ts +110 -0
- package/utils/codeBlockMark.ts +50 -0
- package/utils/codeHighlight.ts +293 -0
- package/utils/codexModels.ts +79 -0
- package/utils/domSelection.ts +84 -0
- package/utils/htmlChrome.ts +73 -0
- package/utils/inputMethod.ts +79 -6
- package/utils/parser.ts +517 -21
- package/utils/preferenceTtl.ts +15 -0
- package/utils/sharing.ts +0 -1
- package/utils/skillCatalog.ts +269 -0
- package/utils/skillReferences.ts +475 -0
- package/utils/syntaxTheme.ts +83 -0
- package/utils/themeRegistry.ts +154 -0
- package/utils/vimHud.ts +263 -0
- package/utils/vimModeAnnouncement.ts +23 -0
- package/utils/vimNavigation.ts +417 -0
- package/utils/vimReticle.ts +88 -0
- package/utils/vimScroll.ts +162 -0
package/components/Viewer.tsx
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import React, { useRef, useState, useEffect, useMemo, forwardRef, useImperativeHandle, useCallback } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
|
-
import hljs from 'highlight.js';
|
|
4
3
|
import { AnnotationType, type Block, type Annotation, type EditorMode, type InputMethod, type ImageAttachment, type ActionsLabelMode } from '../types';
|
|
4
|
+
import { applyHighlight, codeBlockClassName, onCodeHighlightSwap } from '../utils/codeHighlight';
|
|
5
|
+
import { paintCodeBlockMark } from '../utils/codeBlockMark';
|
|
6
|
+
import { useFenceTheme } from '../hooks/useFenceTheme';
|
|
5
7
|
import { computeListIndices, groupBlocks, type Frontmatter } from '../utils/parser';
|
|
6
8
|
import { buildHeadingSlugMap } from '../utils/slugify';
|
|
9
|
+
import { copyTextToClipboard } from '../utils/clipboard';
|
|
7
10
|
import { BlockRenderer } from './BlockRenderer';
|
|
8
11
|
import { CodeBlock } from './blocks/CodeBlock';
|
|
9
12
|
import { TableBlock } from './blocks/TableBlock';
|
|
@@ -45,8 +48,10 @@ import { DocBadges, type LinkedDocBadgeInfo } from './DocBadges';
|
|
|
45
48
|
import { PinpointOverlay } from './PinpointOverlay';
|
|
46
49
|
import { usePinpoint } from '../hooks/usePinpoint';
|
|
47
50
|
import { useAnnotationHighlighter } from '../hooks/useAnnotationHighlighter';
|
|
51
|
+
import { useVimSelection } from '../hooks/useVimSelection';
|
|
48
52
|
import { useScrollViewport } from '../hooks/useScrollViewport';
|
|
49
53
|
import { decodeAnchorHash } from '../utils/anchors';
|
|
54
|
+
import { VimModeOverlay } from './VimModeOverlay';
|
|
50
55
|
|
|
51
56
|
interface ViewerProps {
|
|
52
57
|
blocks: Block[];
|
|
@@ -82,6 +87,10 @@ interface ViewerProps {
|
|
|
82
87
|
isPlanDiffActive?: boolean;
|
|
83
88
|
onPlanDiffToggle?: () => void;
|
|
84
89
|
hasPreviousVersion?: boolean;
|
|
90
|
+
/** Baseline suffix + tooltip for the plan-diff badge (see DocBadges) —
|
|
91
|
+
* annotate/folder sessions pass "since last review"; plan review omits. */
|
|
92
|
+
planDiffBaselineLabel?: string;
|
|
93
|
+
planDiffBaselineTooltip?: string;
|
|
85
94
|
/** Show amber "Demo" badge (portal mode, no shared content loaded) */
|
|
86
95
|
showDemoBadge?: boolean;
|
|
87
96
|
/** Max width in px for the plan card; null removes the cap entirely. */
|
|
@@ -119,6 +128,14 @@ interface ViewerProps {
|
|
|
119
128
|
* comment, attachments, checkbox toggles). Existing annotations still
|
|
120
129
|
* render and remain selectable. Default false — today's behavior. */
|
|
121
130
|
readOnly?: boolean;
|
|
131
|
+
/** Opt-in Vim-style keyboard selection. Default false for compatibility. */
|
|
132
|
+
vimModeEnabled?: boolean;
|
|
133
|
+
/** Replace the compact Vim badge with the live video-style key HUD. */
|
|
134
|
+
vimHudEnabled?: boolean;
|
|
135
|
+
/** Show the bottom-right key panel without affecting the HUD reticle. */
|
|
136
|
+
vimHudKeyPanelEnabled?: boolean;
|
|
137
|
+
/** Persist a user request to hide the bottom-right key panel. */
|
|
138
|
+
onVimHudKeyPanelChange?: (enabled: boolean) => void;
|
|
122
139
|
}
|
|
123
140
|
|
|
124
141
|
export interface ViewerHandle {
|
|
@@ -127,6 +144,12 @@ export interface ViewerHandle {
|
|
|
127
144
|
applySharedAnnotations: (annotations: Annotation[]) => void;
|
|
128
145
|
}
|
|
129
146
|
|
|
147
|
+
interface CodeBlockToolbarTarget {
|
|
148
|
+
readonly block: Block;
|
|
149
|
+
readonly element: HTMLElement;
|
|
150
|
+
readonly activation: 'pointer' | 'keyboard';
|
|
151
|
+
}
|
|
152
|
+
|
|
130
153
|
/**
|
|
131
154
|
* Renders YAML frontmatter as a styled metadata card.
|
|
132
155
|
*/
|
|
@@ -160,6 +183,12 @@ const FrontmatterCard: React.FC<{ frontmatter: Frontmatter }> = ({ frontmatter }
|
|
|
160
183
|
);
|
|
161
184
|
};
|
|
162
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Render and annotate a parsed Markdown document.
|
|
188
|
+
*
|
|
189
|
+
* Pointer and opt-in keyboard annotations share the same highlight callbacks;
|
|
190
|
+
* the imperative handle mutates only highlights owned by this viewer.
|
|
191
|
+
*/
|
|
163
192
|
export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
164
193
|
blocks,
|
|
165
194
|
markdown,
|
|
@@ -181,6 +210,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
181
210
|
isPlanDiffActive,
|
|
182
211
|
onPlanDiffToggle,
|
|
183
212
|
hasPreviousVersion,
|
|
213
|
+
planDiffBaselineLabel,
|
|
214
|
+
planDiffBaselineTooltip,
|
|
184
215
|
showDemoBadge,
|
|
185
216
|
maxWidth,
|
|
186
217
|
onOpenLinkedDoc,
|
|
@@ -200,22 +231,33 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
200
231
|
onAskAI,
|
|
201
232
|
allowImages = true,
|
|
202
233
|
readOnly = false,
|
|
234
|
+
vimModeEnabled = false,
|
|
235
|
+
vimHudEnabled = false,
|
|
236
|
+
vimHudKeyPanelEnabled = true,
|
|
237
|
+
onVimHudKeyPanelChange,
|
|
203
238
|
}, ref) => {
|
|
204
239
|
const [copied, setCopied] = useState(false);
|
|
205
240
|
const [lightbox, setLightbox] = useState<{ src: string; alt: string } | null>(null);
|
|
206
241
|
const [locationHash, setLocationHash] = useState(() => window.location.hash);
|
|
207
242
|
const globalCommentButtonRef = useRef<HTMLButtonElement>(null);
|
|
243
|
+
// Read through a ref: only the imperative removeHighlight path below needs
|
|
244
|
+
// it, and CodeBlock re-highlights itself on palette change.
|
|
245
|
+
const fenceTheme = useFenceTheme();
|
|
246
|
+
const fenceThemeRef = useRef(fenceTheme);
|
|
247
|
+
fenceThemeRef.current = fenceTheme;
|
|
208
248
|
|
|
209
249
|
const handleCopyPlan = async () => {
|
|
210
|
-
|
|
211
|
-
await navigator.clipboard.writeText(markdown);
|
|
250
|
+
if (await copyTextToClipboard(markdown)) {
|
|
212
251
|
setCopied(true);
|
|
213
252
|
setTimeout(() => setCopied(false), 2000);
|
|
214
|
-
}
|
|
215
|
-
console.error('Failed to copy
|
|
253
|
+
} else {
|
|
254
|
+
console.error('Failed to copy');
|
|
216
255
|
}
|
|
217
256
|
};
|
|
218
257
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
258
|
+
// The element that actually scrolls; shared by the Vim scroll math, the
|
|
259
|
+
// sticky-header observer, and the reticle geometry.
|
|
260
|
+
const scrollViewport = useScrollViewport();
|
|
219
261
|
// The badge cluster (repo chips / diff badge) is absolutely positioned in the
|
|
220
262
|
// card's top padding. One row fits; a second row (diff badge) or mobile
|
|
221
263
|
// wrapping outgrows the padding and lands on the document's first heading.
|
|
@@ -243,7 +285,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
243
285
|
// `-1`/`-2`/... suffixes rather than colliding on the same id.
|
|
244
286
|
const headingSlugMap = useMemo(() => buildHeadingSlugMap(blocks), [blocks]);
|
|
245
287
|
const isTouchDevice = useMemo(() => window.matchMedia('(pointer: coarse)').matches, []);
|
|
246
|
-
const [
|
|
288
|
+
const [codeBlockToolbar, setCodeBlockToolbar] =
|
|
289
|
+
useState<CodeBlockToolbarTarget | null>(null);
|
|
247
290
|
const [isCodeBlockToolbarExiting, setIsCodeBlockToolbarExiting] = useState(false);
|
|
248
291
|
const [hoveredTable, setHoveredTable] = useState<{ block: Block; element: HTMLElement } | null>(null);
|
|
249
292
|
const [isTableToolbarExiting, setIsTableToolbarExiting] = useState(false);
|
|
@@ -264,13 +307,14 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
264
307
|
codeBlock: { block: Block; element: HTMLElement };
|
|
265
308
|
} | null>(null);
|
|
266
309
|
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
310
|
+
const readOnlyRef = useRef(readOnly);
|
|
311
|
+
readOnlyRef.current = readOnly;
|
|
267
312
|
const stickySentinelRef = useRef<HTMLDivElement>(null);
|
|
268
313
|
const lastAutoScrolledHashRef = useRef<string | null>(null);
|
|
269
314
|
const [isStuck, setIsStuck] = useState(false);
|
|
270
315
|
|
|
271
316
|
// Shared annotation infrastructure via hook
|
|
272
317
|
const {
|
|
273
|
-
highlighterRef,
|
|
274
318
|
toolbarState,
|
|
275
319
|
commentPopover: hookCommentPopover,
|
|
276
320
|
quickLabelPicker: hookQuickLabelPicker,
|
|
@@ -282,6 +326,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
282
326
|
handleCommentClose: hookCommentClose,
|
|
283
327
|
handleFloatingQuickLabel: hookFloatingQuickLabel,
|
|
284
328
|
handleQuickLabelPickerDismiss: hookQuickLabelPickerDismiss,
|
|
329
|
+
highlightRange,
|
|
330
|
+
highlightMathElement,
|
|
285
331
|
removeHighlight: hookRemoveHighlight,
|
|
286
332
|
clearAllHighlights,
|
|
287
333
|
applyAnnotations,
|
|
@@ -301,17 +347,112 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
301
347
|
const modeRef = useRef<EditorMode>(mode);
|
|
302
348
|
useEffect(() => { modeRef.current = mode; }, [mode]);
|
|
303
349
|
|
|
350
|
+
const applyCodeBlockAnnotation = useCallback((
|
|
351
|
+
blockId: string,
|
|
352
|
+
codeEl: Element,
|
|
353
|
+
type: AnnotationType,
|
|
354
|
+
text?: string,
|
|
355
|
+
images?: ImageAttachment[],
|
|
356
|
+
isQuickLabel?: boolean,
|
|
357
|
+
quickLabelTip?: string,
|
|
358
|
+
) => {
|
|
359
|
+
if (readOnlyRef.current) return;
|
|
360
|
+
|
|
361
|
+
const id = `codeblock-${Date.now()}`;
|
|
362
|
+
const codeText = codeEl.textContent || '';
|
|
363
|
+
|
|
364
|
+
paintCodeBlockMark(codeEl, id, type);
|
|
365
|
+
|
|
366
|
+
const newAnnotation: Annotation = {
|
|
367
|
+
id,
|
|
368
|
+
blockId,
|
|
369
|
+
startOffset: 0,
|
|
370
|
+
endOffset: codeText.length,
|
|
371
|
+
type,
|
|
372
|
+
text,
|
|
373
|
+
originalText: codeText,
|
|
374
|
+
createdA: Date.now(),
|
|
375
|
+
author: getIdentity(),
|
|
376
|
+
images,
|
|
377
|
+
...(isQuickLabel ? { isQuickLabel: true } : {}),
|
|
378
|
+
...(quickLabelTip ? { quickLabelTip } : {}),
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
onAddAnnotationRef.current(newAnnotation);
|
|
382
|
+
window.getSelection()?.removeAllRanges();
|
|
383
|
+
}, []);
|
|
384
|
+
|
|
385
|
+
// Live annotation list for the imperative DOM paths below, which run outside
|
|
386
|
+
// React's render (highlight swaps, the imperative handle).
|
|
387
|
+
const annotationsRef = useRef(annotations);
|
|
388
|
+
annotationsRef.current = annotations;
|
|
389
|
+
|
|
390
|
+
// `removeHighlight` runs BEFORE the host drops the annotation from state and
|
|
391
|
+
// re-highlights the block on the way out, so for one tick `annotationsRef`
|
|
392
|
+
// still lists an annotation whose mark is deliberately gone. Remember those
|
|
393
|
+
// ids so the swap listener below never paints a removed annotation back in,
|
|
394
|
+
// whichever tick that block's re-highlight lands in.
|
|
395
|
+
const removedAnnotationIdsRef = useRef<Set<string>>(new Set());
|
|
396
|
+
// Retire a tombstone as soon as the host's list agrees the annotation is
|
|
397
|
+
// gone: the window it guards is only the tick between removeHighlight and
|
|
398
|
+
// the state update, and keeping it would block a later restore that brings
|
|
399
|
+
// the same annotation (same id) back from a draft.
|
|
400
|
+
for (const id of removedAnnotationIdsRef.current) {
|
|
401
|
+
if (!annotations.some((a) => a.id === id)) removedAnnotationIdsRef.current.delete(id);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// A highlight swap replaces a `<code>` element's children — that is how the
|
|
405
|
+
// palette/mode change repaints tokens, and how the first async grammar
|
|
406
|
+
// attach lands after load. It also destroys any annotation mark inside the
|
|
407
|
+
// fence. Re-paint it here, SYNCHRONOUSLY after the write, so the block ends
|
|
408
|
+
// up with both the new theme's tokens and its mark.
|
|
409
|
+
//
|
|
410
|
+
// Being driven by the swap is also what makes the restore race safe without
|
|
411
|
+
// timing: a share/draft restore that painted before the swap is
|
|
412
|
+
// re-established in the same task the swap ran in, and one that runs after
|
|
413
|
+
// it finds the mark already present and leaves it alone.
|
|
414
|
+
useEffect(() => onCodeHighlightSwap((codeEl) => {
|
|
415
|
+
const container = containerRef.current;
|
|
416
|
+
if (!container || !container.contains(codeEl)) return;
|
|
417
|
+
// The swap always clears the element, so a surviving mark means this write
|
|
418
|
+
// was not the one that owns this block's contents.
|
|
419
|
+
if (codeEl.querySelector('[data-bind-id]')) return;
|
|
420
|
+
|
|
421
|
+
const codeText = codeEl.textContent ?? '';
|
|
422
|
+
if (!codeText) return;
|
|
423
|
+
const blockId = codeEl.closest('[data-block-id]')?.getAttribute('data-block-id') ?? '';
|
|
424
|
+
|
|
425
|
+
// Fenced code is annotated all-or-nothing, so this block's annotations are
|
|
426
|
+
// exactly the ones whose originalText is its full text. Share-restored
|
|
427
|
+
// annotations arrive with an empty blockId (it is filled in during restore),
|
|
428
|
+
// so an unset blockId still counts. The last one wins, matching what
|
|
429
|
+
// annotating the same block twice does.
|
|
430
|
+
const owner = annotationsRef.current.filter((a) =>
|
|
431
|
+
a.type !== AnnotationType.GLOBAL_COMMENT
|
|
432
|
+
&& !a.diffContext
|
|
433
|
+
&& a.originalText === codeText
|
|
434
|
+
&& (a.blockId === blockId || !a.blockId)
|
|
435
|
+
&& !removedAnnotationIdsRef.current.has(a.id)
|
|
436
|
+
&& !container.querySelector(`[data-bind-id="${a.id}"], [data-highlight-id="${a.id}"]`)
|
|
437
|
+
).at(-1);
|
|
438
|
+
|
|
439
|
+
if (owner) paintCodeBlockMark(codeEl, owner.id, owner.type);
|
|
440
|
+
}), []);
|
|
441
|
+
|
|
304
442
|
// Pinpoint mode: hover + click to select elements
|
|
305
443
|
const handlePinpointCodeBlockClick = useCallback((blockId: string, element: HTMLElement) => {
|
|
444
|
+
if (readOnlyRef.current) return;
|
|
445
|
+
|
|
446
|
+
const block = blocks.find((candidate) => candidate.id === blockId);
|
|
306
447
|
const codeEl = element.querySelector('code');
|
|
307
|
-
if (!codeEl) return;
|
|
448
|
+
if (!block || !codeEl) return;
|
|
308
449
|
// In pinpoint mode, apply code block annotation based on current editor mode
|
|
309
450
|
if (modeRef.current === 'redline') {
|
|
310
451
|
applyCodeBlockAnnotation(blockId, codeEl, AnnotationType.DELETION);
|
|
311
452
|
} else if (modeRef.current === 'quickLabel') {
|
|
312
453
|
setCodeBlockQuickLabelPicker({
|
|
313
454
|
anchorEl: element,
|
|
314
|
-
codeBlock: { block
|
|
455
|
+
codeBlock: { block, element },
|
|
315
456
|
});
|
|
316
457
|
} else {
|
|
317
458
|
// Show comment popover anchored to the code block
|
|
@@ -320,18 +461,126 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
320
461
|
contextText: (codeEl.textContent || '').slice(0, 80),
|
|
321
462
|
selectedText: codeEl.textContent || '',
|
|
322
463
|
isGlobal: false,
|
|
323
|
-
codeBlock: { block
|
|
464
|
+
codeBlock: { block, element },
|
|
324
465
|
});
|
|
325
466
|
}
|
|
326
|
-
}, [blocks]);
|
|
467
|
+
}, [applyCodeBlockAnnotation, blocks]);
|
|
468
|
+
|
|
469
|
+
const handleKeyboardCodeBlockAction = useCallback((
|
|
470
|
+
blockId: string,
|
|
471
|
+
element: HTMLElement,
|
|
472
|
+
modeOverride?: EditorMode,
|
|
473
|
+
) => {
|
|
474
|
+
if (readOnlyRef.current) return;
|
|
475
|
+
|
|
476
|
+
const block = blocks.find((candidate) => candidate.id === blockId);
|
|
477
|
+
const codeEl = element.querySelector('code');
|
|
478
|
+
if (!block || !codeEl) return;
|
|
479
|
+
|
|
480
|
+
const effectiveMode = modeOverride ?? modeRef.current;
|
|
481
|
+
if (effectiveMode === 'redline') {
|
|
482
|
+
applyCodeBlockAnnotation(blockId, codeEl, AnnotationType.DELETION);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (effectiveMode === 'quickLabel') {
|
|
486
|
+
setCodeBlockQuickLabelPicker({
|
|
487
|
+
anchorEl: element,
|
|
488
|
+
codeBlock: { block, element },
|
|
489
|
+
});
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (effectiveMode === 'selection') {
|
|
493
|
+
if (hoverTimeoutRef.current) {
|
|
494
|
+
clearTimeout(hoverTimeoutRef.current);
|
|
495
|
+
hoverTimeoutRef.current = null;
|
|
496
|
+
}
|
|
497
|
+
setCodeBlockToolbar({ block, element, activation: 'keyboard' });
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
setViewerCommentPopover({
|
|
501
|
+
anchorEl: element,
|
|
502
|
+
contextText: (codeEl.textContent || '').slice(0, 80),
|
|
503
|
+
selectedText: codeEl.textContent || '',
|
|
504
|
+
isGlobal: false,
|
|
505
|
+
codeBlock: { block, element },
|
|
506
|
+
});
|
|
507
|
+
}, [applyCodeBlockAnnotation, blocks]);
|
|
508
|
+
|
|
509
|
+
const vimModeActive = vimModeEnabled && !readOnly;
|
|
510
|
+
const keyboardCodeBlockToolbarOpen = codeBlockToolbar?.activation === 'keyboard';
|
|
511
|
+
const vimBlocked = !!toolbarState
|
|
512
|
+
|| !!hookCommentPopover
|
|
513
|
+
|| !!viewerCommentPopover
|
|
514
|
+
|| !!hookQuickLabelPicker
|
|
515
|
+
|| !!codeBlockQuickLabelPicker
|
|
516
|
+
|| keyboardCodeBlockToolbarOpen
|
|
517
|
+
|| !!isPlanDiffActive
|
|
518
|
+
|| !!popoutTable
|
|
519
|
+
|| !!lightbox;
|
|
520
|
+
const clearPinpointHoverRef = useRef<() => void>(() => {});
|
|
521
|
+
const handleVimCommand = useCallback(() => {
|
|
522
|
+
clearPinpointHoverRef.current();
|
|
523
|
+
}, []);
|
|
524
|
+
const vim = useVimSelection({
|
|
525
|
+
containerRef,
|
|
526
|
+
scrollViewport,
|
|
527
|
+
enabled: vimModeActive,
|
|
528
|
+
hudEnabled: vimHudEnabled,
|
|
529
|
+
blocked: vimBlocked,
|
|
530
|
+
activeMode: mode,
|
|
531
|
+
contentVersion: blocks,
|
|
532
|
+
onHighlightRange: highlightRange,
|
|
533
|
+
onCodeBlockAction: handleKeyboardCodeBlockAction,
|
|
534
|
+
onMathAction: highlightMathElement,
|
|
535
|
+
onHandledCommand: handleVimCommand,
|
|
536
|
+
});
|
|
327
537
|
|
|
328
|
-
const { hoverTarget } = usePinpoint({
|
|
538
|
+
const { hoverTarget, clearHover: clearPinpointHover } = usePinpoint({
|
|
329
539
|
containerRef,
|
|
330
|
-
highlighterRef,
|
|
331
540
|
inputMethod,
|
|
332
|
-
enabled: !readOnly && !toolbarState && !hookCommentPopover && !viewerCommentPopover && !hookQuickLabelPicker && !codeBlockQuickLabelPicker && !(isPlanDiffActive ?? false),
|
|
541
|
+
enabled: !readOnly && !toolbarState && !hookCommentPopover && !viewerCommentPopover && !hookQuickLabelPicker && !codeBlockQuickLabelPicker && !(isPlanDiffActive ?? false) && !vim.helpOpen,
|
|
542
|
+
onSelectRange: highlightRange,
|
|
333
543
|
onCodeBlockClick: handlePinpointCodeBlockClick,
|
|
334
544
|
});
|
|
545
|
+
clearPinpointHoverRef.current = clearPinpointHover;
|
|
546
|
+
const vimOwnsHudTarget = vimHudEnabled
|
|
547
|
+
&& vim.state.phase !== 'inactive'
|
|
548
|
+
&& (vim.focused || vim.state.phase === 'action');
|
|
549
|
+
const vimOwnsDocumentNavigation = vimModeActive
|
|
550
|
+
&& vim.focused
|
|
551
|
+
&& vim.state.phase !== 'inactive'
|
|
552
|
+
&& vim.state.phase !== 'action';
|
|
553
|
+
const legacyVimTarget = !vimHudEnabled
|
|
554
|
+
&& (vim.focused || vim.state.phase === 'action')
|
|
555
|
+
? vim.activeTarget
|
|
556
|
+
: null;
|
|
557
|
+
const pinpointOverlayTarget = vimOwnsHudTarget
|
|
558
|
+
? null
|
|
559
|
+
: (inputMethod === 'pinpoint' ? hoverTarget : null) ?? legacyVimTarget;
|
|
560
|
+
|
|
561
|
+
useEffect(() => {
|
|
562
|
+
if (!readOnly) return;
|
|
563
|
+
if (hoverTimeoutRef.current) {
|
|
564
|
+
clearTimeout(hoverTimeoutRef.current);
|
|
565
|
+
hoverTimeoutRef.current = null;
|
|
566
|
+
}
|
|
567
|
+
setCodeBlockToolbar(null);
|
|
568
|
+
setIsCodeBlockToolbarExiting(false);
|
|
569
|
+
setViewerCommentPopover(null);
|
|
570
|
+
setCodeBlockQuickLabelPicker(null);
|
|
571
|
+
}, [readOnly]);
|
|
572
|
+
|
|
573
|
+
useEffect(() => {
|
|
574
|
+
if (!vimOwnsDocumentNavigation) return;
|
|
575
|
+
if (hoverTimeoutRef.current) {
|
|
576
|
+
clearTimeout(hoverTimeoutRef.current);
|
|
577
|
+
hoverTimeoutRef.current = null;
|
|
578
|
+
}
|
|
579
|
+
setCodeBlockToolbar((current) => (
|
|
580
|
+
current?.activation === 'pointer' ? null : current
|
|
581
|
+
));
|
|
582
|
+
setIsCodeBlockToolbarExiting(false);
|
|
583
|
+
}, [vimOwnsDocumentNavigation]);
|
|
335
584
|
|
|
336
585
|
// Suppress native context menu on touch devices (prevents cut/copy/paste overlay on mobile)
|
|
337
586
|
useEffect(() => {
|
|
@@ -349,16 +598,15 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
349
598
|
// Detect when sticky action bar is "stuck" to show card background.
|
|
350
599
|
// The IntersectionObserver root must be the actual scroll element — the
|
|
351
600
|
// OverlayScrollArea viewport — not the <main> host, which doesn't scroll.
|
|
352
|
-
const stickyScrollViewport = useScrollViewport();
|
|
353
601
|
useEffect(() => {
|
|
354
|
-
if (!stickyActions || !stickySentinelRef.current || !
|
|
602
|
+
if (!stickyActions || !stickySentinelRef.current || !scrollViewport) return;
|
|
355
603
|
const observer = new IntersectionObserver(
|
|
356
604
|
([entry]) => setIsStuck(!entry.isIntersecting),
|
|
357
|
-
{ root:
|
|
605
|
+
{ root: scrollViewport, threshold: 0 }
|
|
358
606
|
);
|
|
359
607
|
observer.observe(stickySentinelRef.current);
|
|
360
608
|
return () => observer.disconnect();
|
|
361
|
-
}, [stickyActions,
|
|
609
|
+
}, [stickyActions, scrollViewport]);
|
|
362
610
|
|
|
363
611
|
useEffect(() => {
|
|
364
612
|
const handleHashChange = () => {
|
|
@@ -375,7 +623,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
375
623
|
if (!anchor) return false;
|
|
376
624
|
|
|
377
625
|
const container = containerRef.current;
|
|
378
|
-
if (!container || !
|
|
626
|
+
if (!container || !scrollViewport) return false;
|
|
379
627
|
|
|
380
628
|
const target = document.getElementById(anchor);
|
|
381
629
|
if (!target || !container.contains(target)) return false;
|
|
@@ -387,27 +635,27 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
387
635
|
const headerOffset = stickyActionsEl
|
|
388
636
|
? stickyActionsEl.getBoundingClientRect().height + stickyTop
|
|
389
637
|
: 0;
|
|
390
|
-
const containerRect =
|
|
638
|
+
const containerRect = scrollViewport.getBoundingClientRect();
|
|
391
639
|
const targetRect = target.getBoundingClientRect();
|
|
392
640
|
const relativeTop = targetRect.top - containerRect.top;
|
|
393
|
-
const offsetPosition =
|
|
641
|
+
const offsetPosition = scrollViewport.scrollTop + relativeTop - headerOffset;
|
|
394
642
|
|
|
395
|
-
|
|
643
|
+
scrollViewport.scrollTo({
|
|
396
644
|
top: Math.max(0, offsetPosition),
|
|
397
645
|
behavior: 'smooth',
|
|
398
646
|
});
|
|
399
647
|
return true;
|
|
400
|
-
}, [
|
|
648
|
+
}, [scrollViewport]);
|
|
401
649
|
|
|
402
650
|
useEffect(() => {
|
|
403
|
-
if (!
|
|
651
|
+
if (!scrollViewport || !locationHash || lastAutoScrolledHashRef.current === locationHash) return;
|
|
404
652
|
const timer = window.setTimeout(() => {
|
|
405
653
|
if (scrollToAnchor(locationHash)) {
|
|
406
654
|
lastAutoScrolledHashRef.current = locationHash;
|
|
407
655
|
}
|
|
408
656
|
}, 0);
|
|
409
657
|
return () => window.clearTimeout(timer);
|
|
410
|
-
}, [blocks, locationHash, scrollToAnchor,
|
|
658
|
+
}, [blocks, locationHash, scrollToAnchor, scrollViewport]);
|
|
411
659
|
|
|
412
660
|
// Use the native copy event so clipboard writes are synchronous (Safari
|
|
413
661
|
// rejects the async navigator.clipboard API outside the user-gesture window).
|
|
@@ -431,6 +679,10 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
431
679
|
// Imperative handle — delegates to hook, extends removeHighlight for code blocks
|
|
432
680
|
useImperativeHandle(ref, () => ({
|
|
433
681
|
removeHighlight: (id: string) => {
|
|
682
|
+
// The re-highlight below notifies the swap listener, which would happily
|
|
683
|
+
// paint this annotation's mark straight back in — the host has not
|
|
684
|
+
// dropped it from state yet. Tombstone the id first.
|
|
685
|
+
removedAnnotationIdsRef.current.add(id);
|
|
434
686
|
// Code block annotations need syntax re-highlighting after removal.
|
|
435
687
|
// Must run BEFORE hookRemoveHighlight, which removes the <mark> elements.
|
|
436
688
|
const manualHighlights = containerRef.current?.querySelectorAll(`[data-bind-id="${id}"]`);
|
|
@@ -442,9 +694,9 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
442
694
|
el.remove();
|
|
443
695
|
codeEl.textContent = plainText;
|
|
444
696
|
const block = blocks.find(b => b.id === codeEl.closest('[data-block-id]')?.getAttribute('data-block-id'));
|
|
445
|
-
codeEl.
|
|
446
|
-
|
|
447
|
-
|
|
697
|
+
codeEl.className = codeBlockClassName(block?.language);
|
|
698
|
+
// Language-less fences stay plain (#1212) — applyHighlight never guesses.
|
|
699
|
+
applyHighlight(codeEl, plainText, block?.language, fenceThemeRef.current);
|
|
448
700
|
}
|
|
449
701
|
});
|
|
450
702
|
|
|
@@ -456,86 +708,47 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
456
708
|
|
|
457
709
|
// --- Viewer-specific: code block annotation ---
|
|
458
710
|
|
|
459
|
-
const applyCodeBlockAnnotation = (
|
|
460
|
-
blockId: string,
|
|
461
|
-
codeEl: Element,
|
|
462
|
-
type: AnnotationType,
|
|
463
|
-
text?: string,
|
|
464
|
-
images?: ImageAttachment[],
|
|
465
|
-
isQuickLabel?: boolean,
|
|
466
|
-
quickLabelTip?: string,
|
|
467
|
-
) => {
|
|
468
|
-
const id = `codeblock-${Date.now()}`;
|
|
469
|
-
const codeText = codeEl.textContent || '';
|
|
470
|
-
|
|
471
|
-
const wrapper = document.createElement('mark');
|
|
472
|
-
wrapper.className = `annotation-highlight ${type === AnnotationType.DELETION ? 'deletion' : type === AnnotationType.COMMENT ? 'comment' : ''}`.trim();
|
|
473
|
-
wrapper.dataset.bindId = id;
|
|
474
|
-
wrapper.textContent = codeText;
|
|
475
|
-
|
|
476
|
-
codeEl.innerHTML = '';
|
|
477
|
-
codeEl.appendChild(wrapper);
|
|
478
|
-
|
|
479
|
-
const newAnnotation: Annotation = {
|
|
480
|
-
id,
|
|
481
|
-
blockId,
|
|
482
|
-
startOffset: 0,
|
|
483
|
-
endOffset: codeText.length,
|
|
484
|
-
type,
|
|
485
|
-
text,
|
|
486
|
-
originalText: codeText,
|
|
487
|
-
createdA: Date.now(),
|
|
488
|
-
author: getIdentity(),
|
|
489
|
-
images,
|
|
490
|
-
...(isQuickLabel ? { isQuickLabel: true } : {}),
|
|
491
|
-
...(quickLabelTip ? { quickLabelTip } : {}),
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
onAddAnnotationRef.current(newAnnotation);
|
|
495
|
-
window.getSelection()?.removeAllRanges();
|
|
496
|
-
};
|
|
497
|
-
|
|
498
711
|
const handleCodeBlockAnnotate = (type: AnnotationType) => {
|
|
499
|
-
if (!
|
|
500
|
-
const codeEl =
|
|
712
|
+
if (readOnlyRef.current || !codeBlockToolbar) return;
|
|
713
|
+
const codeEl = codeBlockToolbar.element.querySelector('code');
|
|
501
714
|
if (!codeEl) return;
|
|
502
|
-
applyCodeBlockAnnotation(
|
|
503
|
-
|
|
715
|
+
applyCodeBlockAnnotation(codeBlockToolbar.block.id, codeEl, type);
|
|
716
|
+
setCodeBlockToolbar(null);
|
|
504
717
|
};
|
|
505
718
|
|
|
506
719
|
const handleCodeBlockQuickLabel = (label: QuickLabel) => {
|
|
507
|
-
if (!
|
|
508
|
-
const codeEl =
|
|
720
|
+
if (readOnlyRef.current || !codeBlockToolbar) return;
|
|
721
|
+
const codeEl = codeBlockToolbar.element.querySelector('code');
|
|
509
722
|
if (!codeEl) return;
|
|
510
723
|
applyCodeBlockAnnotation(
|
|
511
|
-
|
|
724
|
+
codeBlockToolbar.block.id, codeEl, AnnotationType.COMMENT,
|
|
512
725
|
`${label.emoji} ${label.text}`, undefined, true, label.tip
|
|
513
726
|
);
|
|
514
|
-
|
|
727
|
+
setCodeBlockToolbar(null);
|
|
515
728
|
};
|
|
516
729
|
|
|
517
730
|
const handleCodeBlockToolbarClose = () => {
|
|
518
|
-
|
|
731
|
+
setCodeBlockToolbar(null);
|
|
519
732
|
};
|
|
520
733
|
|
|
521
734
|
// Viewer-specific comment popover handlers (code blocks + global comments)
|
|
522
735
|
|
|
523
736
|
const handleCodeBlockRequestComment = (initialChar?: string) => {
|
|
524
|
-
if (!
|
|
525
|
-
const codeText =
|
|
737
|
+
if (readOnlyRef.current || !codeBlockToolbar) return;
|
|
738
|
+
const codeText = codeBlockToolbar.element.querySelector('code')?.textContent || '';
|
|
526
739
|
setViewerCommentPopover({
|
|
527
|
-
anchorEl:
|
|
740
|
+
anchorEl: codeBlockToolbar.element,
|
|
528
741
|
contextText: codeText.slice(0, 80),
|
|
529
742
|
selectedText: codeText,
|
|
530
743
|
initialText: initialChar,
|
|
531
744
|
isGlobal: false,
|
|
532
|
-
codeBlock:
|
|
745
|
+
codeBlock: codeBlockToolbar,
|
|
533
746
|
});
|
|
534
|
-
|
|
747
|
+
setCodeBlockToolbar(null);
|
|
535
748
|
};
|
|
536
749
|
|
|
537
750
|
const handleViewerCommentSubmit = (text: string, images?: ImageAttachment[]) => {
|
|
538
|
-
if (!viewerCommentPopover) return;
|
|
751
|
+
if (readOnlyRef.current || !viewerCommentPopover) return;
|
|
539
752
|
|
|
540
753
|
if (viewerCommentPopover.isGlobal) {
|
|
541
754
|
const newAnnotation: Annotation = {
|
|
@@ -574,8 +787,20 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
574
787
|
<article
|
|
575
788
|
ref={containerRef}
|
|
576
789
|
data-print-region="article"
|
|
577
|
-
|
|
578
|
-
|
|
790
|
+
data-vim-mode={vimModeActive ? 'enabled' : undefined}
|
|
791
|
+
data-vim-phase={vimModeActive ? vim.state.phase : undefined}
|
|
792
|
+
data-vim-focused={vimModeActive ? String(vim.focused) : undefined}
|
|
793
|
+
data-vim-blocked={vimModeActive ? String(vimBlocked) : undefined}
|
|
794
|
+
data-vim-target-key={vimModeActive ? vim.activeTarget?.key : undefined}
|
|
795
|
+
tabIndex={vimModeActive ? 0 : undefined}
|
|
796
|
+
onFocus={vim.onFocus}
|
|
797
|
+
onBlur={vim.onBlur}
|
|
798
|
+
onMouseDown={vim.onMouseDown}
|
|
799
|
+
className={`w-full bg-card rounded-xl py-5 md:py-8 lg:py-10 xl:py-12 relative ${gridEnabled ? 'px-5 md:px-8 lg:px-10 xl:px-12 shadow-xl border border-border/50' : ''} ${inputMethod === 'pinpoint' ? 'cursor-pointer' : ''}`}
|
|
800
|
+
style={{
|
|
801
|
+
WebkitTouchCallout: 'none',
|
|
802
|
+
...(vimModeActive ? { outline: 'none' } : {}),
|
|
803
|
+
} as React.CSSProperties}
|
|
579
804
|
>
|
|
580
805
|
{/* Repo info + plan diff badge + demo badge + linked doc badge + archive badge - top left */}
|
|
581
806
|
{(repoInfo || hasPreviousVersion || showDemoBadge || linkedDocInfo || archiveInfo || sourceInfo || openInAppPath) && (
|
|
@@ -587,6 +812,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
587
812
|
isPlanDiffActive={isPlanDiffActive}
|
|
588
813
|
hasPreviousVersion={hasPreviousVersion}
|
|
589
814
|
onPlanDiffToggle={onPlanDiffToggle}
|
|
815
|
+
planDiffBaselineLabel={planDiffBaselineLabel}
|
|
816
|
+
planDiffBaselineTooltip={planDiffBaselineTooltip}
|
|
590
817
|
showDemoBadge={showDemoBadge}
|
|
591
818
|
archiveInfo={archiveInfo}
|
|
592
819
|
linkedDocInfo={linkedDocInfo}
|
|
@@ -743,7 +970,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
743
970
|
<CodeBlock
|
|
744
971
|
key={group.block.id}
|
|
745
972
|
block={group.block}
|
|
746
|
-
onHover={inputMethod === 'pinpoint' ?
|
|
973
|
+
onHover={readOnly || inputMethod === 'pinpoint' ? undefined : (element) => {
|
|
747
974
|
// Clear any pending leave timeout
|
|
748
975
|
if (hoverTimeoutRef.current) {
|
|
749
976
|
clearTimeout(hoverTimeoutRef.current);
|
|
@@ -752,22 +979,36 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
752
979
|
// Cancel exit animation if re-entering
|
|
753
980
|
setIsCodeBlockToolbarExiting(false);
|
|
754
981
|
// Only show hover toolbar if no selection toolbar is active
|
|
755
|
-
if (
|
|
756
|
-
|
|
982
|
+
if (
|
|
983
|
+
!toolbarState
|
|
984
|
+
&& !vimOwnsDocumentNavigation
|
|
985
|
+
&& !keyboardCodeBlockToolbarOpen
|
|
986
|
+
) {
|
|
987
|
+
setCodeBlockToolbar({
|
|
988
|
+
block: group.block,
|
|
989
|
+
element,
|
|
990
|
+
activation: 'pointer',
|
|
991
|
+
});
|
|
757
992
|
}
|
|
758
993
|
}}
|
|
759
|
-
onLeave={inputMethod === 'pinpoint' ?
|
|
994
|
+
onLeave={readOnly || inputMethod === 'pinpoint' ? undefined : () => {
|
|
995
|
+
if (keyboardCodeBlockToolbarOpen) return;
|
|
760
996
|
// Delay then start exit animation
|
|
761
997
|
hoverTimeoutRef.current = setTimeout(() => {
|
|
762
998
|
setIsCodeBlockToolbarExiting(true);
|
|
763
999
|
// After exit animation, unmount
|
|
764
1000
|
setTimeout(() => {
|
|
765
|
-
|
|
1001
|
+
setCodeBlockToolbar(null);
|
|
766
1002
|
setIsCodeBlockToolbarExiting(false);
|
|
767
1003
|
}, 150);
|
|
768
1004
|
}, 100);
|
|
769
1005
|
}}
|
|
770
|
-
isHovered={
|
|
1006
|
+
isHovered={
|
|
1007
|
+
!readOnly
|
|
1008
|
+
&& inputMethod !== 'pinpoint'
|
|
1009
|
+
&& !vimOwnsDocumentNavigation
|
|
1010
|
+
&& codeBlockToolbar?.block.id === group.block.id
|
|
1011
|
+
}
|
|
771
1012
|
/>
|
|
772
1013
|
) : (
|
|
773
1014
|
<BlockRenderer imageBaseDir={imageBaseDir} onImageClick={(src, alt) => setLightbox({ src, alt })} key={group.block.id} block={group.block} onOpenLinkedDoc={onOpenLinkedDoc} onOpenCodeFile={onOpenCodeFile} onNavigateAnchor={scrollToAnchor} onToggleCheckbox={readOnly ? undefined : onToggleCheckbox} checkboxOverrides={checkboxOverrides} githubRepo={repoInfo?.display} headingAnchorId={headingSlugMap.get(group.block.id)} />
|
|
@@ -775,7 +1016,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
775
1016
|
)}
|
|
776
1017
|
|
|
777
1018
|
{/* Text selection toolbar */}
|
|
778
|
-
{toolbarState && (
|
|
1019
|
+
{!readOnly && toolbarState && (
|
|
779
1020
|
<ToolbarErrorBoundary>
|
|
780
1021
|
<AnnotationToolbar
|
|
781
1022
|
element={toolbarState.element}
|
|
@@ -826,35 +1067,40 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
826
1067
|
)}
|
|
827
1068
|
|
|
828
1069
|
{/* Code block hover toolbar */}
|
|
829
|
-
{
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
setIsCodeBlockToolbarExiting(true);
|
|
849
|
-
setTimeout(() => {
|
|
850
|
-
setHoveredCodeBlock(null);
|
|
1070
|
+
{!readOnly
|
|
1071
|
+
&& codeBlockToolbar
|
|
1072
|
+
&& !toolbarState
|
|
1073
|
+
&& !(vimOwnsDocumentNavigation && codeBlockToolbar.activation === 'pointer')
|
|
1074
|
+
&& (
|
|
1075
|
+
<ToolbarErrorBoundary>
|
|
1076
|
+
<AnnotationToolbar
|
|
1077
|
+
element={codeBlockToolbar.element}
|
|
1078
|
+
positionMode="top-right"
|
|
1079
|
+
onAnnotate={handleCodeBlockAnnotate}
|
|
1080
|
+
onClose={handleCodeBlockToolbarClose}
|
|
1081
|
+
onRequestComment={handleCodeBlockRequestComment}
|
|
1082
|
+
onQuickLabel={handleCodeBlockQuickLabel}
|
|
1083
|
+
isExiting={isCodeBlockToolbarExiting}
|
|
1084
|
+
onMouseEnter={() => {
|
|
1085
|
+
if (hoverTimeoutRef.current) {
|
|
1086
|
+
clearTimeout(hoverTimeoutRef.current);
|
|
1087
|
+
hoverTimeoutRef.current = null;
|
|
1088
|
+
}
|
|
851
1089
|
setIsCodeBlockToolbarExiting(false);
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1090
|
+
}}
|
|
1091
|
+
onMouseLeave={() => {
|
|
1092
|
+
if (codeBlockToolbar.activation === 'keyboard') return;
|
|
1093
|
+
hoverTimeoutRef.current = setTimeout(() => {
|
|
1094
|
+
setIsCodeBlockToolbarExiting(true);
|
|
1095
|
+
setTimeout(() => {
|
|
1096
|
+
setCodeBlockToolbar(null);
|
|
1097
|
+
setIsCodeBlockToolbarExiting(false);
|
|
1098
|
+
}, 150);
|
|
1099
|
+
}, 100);
|
|
1100
|
+
}}
|
|
1101
|
+
/>
|
|
1102
|
+
</ToolbarErrorBoundary>
|
|
1103
|
+
)}
|
|
858
1104
|
|
|
859
1105
|
{/* Table popout dialog — portaled into containerRef so annotations */}
|
|
860
1106
|
{/* can walk into its text nodes the same way they do the inline table. */}
|
|
@@ -874,12 +1120,35 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
874
1120
|
)}
|
|
875
1121
|
|
|
876
1122
|
{/* Pinpoint hover overlay */}
|
|
877
|
-
{inputMethod === 'pinpoint' && (
|
|
878
|
-
<PinpointOverlay
|
|
1123
|
+
{(inputMethod === 'pinpoint' || vim.activeTarget) && (
|
|
1124
|
+
<PinpointOverlay
|
|
1125
|
+
target={pinpointOverlayTarget}
|
|
1126
|
+
containerRef={containerRef}
|
|
1127
|
+
/>
|
|
1128
|
+
)}
|
|
1129
|
+
{vimModeActive && (
|
|
1130
|
+
<VimModeOverlay
|
|
1131
|
+
containerRef={containerRef}
|
|
1132
|
+
inputMethod={inputMethod}
|
|
1133
|
+
state={vim.state}
|
|
1134
|
+
focused={vim.focused}
|
|
1135
|
+
hudEnabled={vimHudEnabled}
|
|
1136
|
+
keyPanelEnabled={vimHudKeyPanelEnabled}
|
|
1137
|
+
hudCommand={vim.hudCommand}
|
|
1138
|
+
activeTarget={vim.activeTarget}
|
|
1139
|
+
helpOpen={vim.helpOpen}
|
|
1140
|
+
onHelpOpenChange={vim.onHelpOpenChange}
|
|
1141
|
+
onKeyPanelHide={
|
|
1142
|
+
onVimHudKeyPanelChange
|
|
1143
|
+
? () => onVimHudKeyPanelChange(false)
|
|
1144
|
+
: undefined
|
|
1145
|
+
}
|
|
1146
|
+
onHudFocusLeave={vim.onHudFocusLeave}
|
|
1147
|
+
/>
|
|
879
1148
|
)}
|
|
880
1149
|
|
|
881
1150
|
{/* Comment popover — hook handles text selection, Viewer handles global + code block */}
|
|
882
|
-
{hookCommentPopover && (
|
|
1151
|
+
{!readOnly && hookCommentPopover && (
|
|
883
1152
|
<CommentPopover
|
|
884
1153
|
anchorEl={hookCommentPopover.anchorEl}
|
|
885
1154
|
contextText={hookCommentPopover.contextText}
|
|
@@ -888,6 +1157,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
888
1157
|
onSubmit={hookCommentSubmit}
|
|
889
1158
|
onClose={hookCommentClose}
|
|
890
1159
|
allowImages={allowImages}
|
|
1160
|
+
skillReferences
|
|
891
1161
|
onAskAI={onAskAI}
|
|
892
1162
|
askAIContext={{
|
|
893
1163
|
kind: 'selection',
|
|
@@ -897,7 +1167,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
897
1167
|
}}
|
|
898
1168
|
/>
|
|
899
1169
|
)}
|
|
900
|
-
{viewerCommentPopover && (
|
|
1170
|
+
{!readOnly && viewerCommentPopover && (
|
|
901
1171
|
<CommentPopover
|
|
902
1172
|
anchorEl={viewerCommentPopover.anchorEl}
|
|
903
1173
|
contextText={viewerCommentPopover.contextText}
|
|
@@ -906,6 +1176,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
906
1176
|
onSubmit={handleViewerCommentSubmit}
|
|
907
1177
|
onClose={handleViewerCommentClose}
|
|
908
1178
|
allowImages={allowImages}
|
|
1179
|
+
skillReferences
|
|
909
1180
|
onAskAI={onAskAI}
|
|
910
1181
|
askAIContext={{
|
|
911
1182
|
kind: viewerCommentPopover.isGlobal ? 'general' : 'selection',
|
|
@@ -917,7 +1188,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
917
1188
|
)}
|
|
918
1189
|
|
|
919
1190
|
{/* Quick Label floating picker — hook handles text selection, Viewer handles code blocks */}
|
|
920
|
-
{hookQuickLabelPicker && (
|
|
1191
|
+
{!readOnly && hookQuickLabelPicker && (
|
|
921
1192
|
<FloatingQuickLabelPicker
|
|
922
1193
|
anchorEl={hookQuickLabelPicker.anchorEl}
|
|
923
1194
|
cursorHint={hookQuickLabelPicker.cursorHint}
|
|
@@ -925,7 +1196,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
925
1196
|
onDismiss={hookQuickLabelPickerDismiss}
|
|
926
1197
|
/>
|
|
927
1198
|
)}
|
|
928
|
-
{codeBlockQuickLabelPicker && (
|
|
1199
|
+
{!readOnly && codeBlockQuickLabelPicker && (
|
|
929
1200
|
<FloatingQuickLabelPicker
|
|
930
1201
|
anchorEl={codeBlockQuickLabelPicker.anchorEl}
|
|
931
1202
|
onSelect={(label: QuickLabel) => {
|
|
@@ -984,8 +1255,3 @@ const ImageLightbox: React.FC<{ src: string; alt: string; onClose: () => void }>
|
|
|
984
1255
|
</div>
|
|
985
1256
|
);
|
|
986
1257
|
};
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|