@plannotator/ui 0.53.0 → 0.54.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/HANDOFF.md +284 -27
- package/README.md +84 -14
- package/components/AgentToolAnnouncementDialog.tsx +1 -1
- package/components/AgentsTab.tsx +1 -1
- package/components/AnnotationPanel.tsx +177 -34
- package/components/AnnotationSidebar.tsx +1 -1
- package/components/AnnotationToolbar.tsx +64 -9
- package/components/AnnotationToolstrip.tsx +45 -40
- package/components/ApproveDropdown.tsx +7 -9
- package/components/AskSessionAnnouncementDialog.tsx +1 -1
- package/components/AttachmentsButton.tsx +1 -1
- package/components/BlockRenderer.tsx +19 -5
- package/components/CodeFilePopout.tsx +140 -5
- package/components/CommentPopover.tsx +46 -8
- package/components/DecisionControl.tsx +31 -5
- package/components/DiagramBlock.tsx +17 -12
- package/components/EditorAnnotationCard.tsx +1 -1
- package/components/ExportModal.tsx +77 -7
- package/components/HostTitle.tsx +0 -4
- package/components/HtmlSurfaceControls.tsx +2 -1
- package/components/ImageAnnotator/Toolbar.tsx +25 -1
- package/components/ImageAnnotator/index.tsx +24 -4
- package/components/ImageLightbox.tsx +658 -120
- package/components/ImagePeek.tsx +38 -4
- package/components/ImageRegions.tsx +369 -0
- package/components/ImageThumbnail.tsx +1 -1
- package/components/ImageZoomStage.tsx +796 -0
- package/components/InboxAnnouncementDialog.tsx +202 -0
- package/components/InlineMarkdown.tsx +152 -24
- package/components/KeyboardShortcuts.tsx +23 -0
- package/components/LazyBlockBoundary.tsx +100 -0
- package/components/ListMarker.tsx +3 -3
- package/components/LookAndFeelAnnouncementDialog.tsx +12 -2
- package/components/MentionPicker.tsx +51 -0
- package/components/MenuVersionSection.tsx +31 -9
- package/components/PlanDecisionMenuExtras.tsx +129 -0
- package/components/PlanHeaderMenu.tsx +15 -1
- package/components/ProviderIcons.tsx +8 -0
- package/components/RefHoverCard.tsx +913 -0
- package/components/SendFailureToast.tsx +126 -0
- package/components/Settings.tsx +617 -126
- package/components/SkillReferenceMenu.tsx +39 -12
- package/components/TerminalToolsAnnouncementDialog.tsx +1 -1
- package/components/ThemeProvider.tsx +14 -1
- package/components/ThemeTab.tsx +16 -24
- package/components/Tooltip.tsx +49 -18
- package/components/Viewer.tsx +316 -47
- package/components/ai/DocumentAIChatPanel.tsx +29 -3
- package/components/ai/NewChatButton.tsx +74 -0
- package/components/blocks/CodeBlock.tsx +1 -1
- package/components/blocks/HtmlBlock.tsx +7 -1
- package/components/blocks/QuestionBlock.tsx +522 -62
- package/components/blocks/QuestionContext.tsx +3 -0
- package/components/diagram/DiagramComposer.tsx +5 -1
- package/components/diagram/DiagramPending.tsx +12 -2
- package/components/diagram/DiagramViewer.tsx +19 -3
- package/components/html-embed/HtmlEmbedBlock.tsx +374 -0
- package/components/html-embed/SvgFenceBlock.tsx +46 -0
- package/components/html-embed/index.ts +3 -0
- package/components/html-embed/languages.ts +15 -0
- package/components/html-embed/loadGate.ts +107 -0
- package/components/html-viewer/HtmlViewer.tsx +69 -6
- package/components/html-viewer/bridge-script.asset.js +504 -12
- package/components/html-viewer/bridge-script.lite.ts +1 -1
- package/components/html-viewer/bridge-script.ts +545 -12
- package/components/html-viewer/useHtmlAnnotation.ts +125 -0
- package/components/imageZoom.ts +265 -0
- package/components/plan-diff/PlanCleanDiffView.tsx +1 -1
- package/components/plan-diff/PlanDiffViewer.tsx +13 -2
- package/components/plan-diff/PlanRawDiffView.tsx +1 -1
- package/components/sidebar/FileBrowser.tsx +16 -2
- package/components/sidebar/SidebarTabs.tsx +3 -0
- package/config/index.ts +3 -0
- package/config/reviewView.ts +41 -1
- package/config/settings.ts +155 -4
- package/configure.ts +28 -0
- package/hooks/pfm/useCodeFilePopout.ts +7 -1
- package/hooks/useAIChat.ts +72 -25
- package/hooks/useAnnotationDraft.ts +71 -6
- package/hooks/useAnnotationHighlighter.ts +57 -3
- package/hooks/useCodeAnnotationDraft.ts +113 -17
- package/hooks/useCookieHostSetting.ts +96 -0
- package/hooks/useDocumentFont.ts +17 -0
- package/hooks/useDocumentTextSize.ts +20 -0
- package/hooks/useFileBrowser.ts +10 -1
- package/hooks/useLinkedDoc.ts +226 -13
- package/hooks/useMentionAutocomplete.ts +1 -1
- package/hooks/usePackageChangesSetting.ts +90 -0
- package/hooks/usePlanLookDialog.ts +67 -0
- package/hooks/useSharing.ts +11 -7
- package/hooks/useSkillReferenceAutocomplete.ts +7 -3
- package/hooks/useThemePaletteHalves.ts +37 -0
- package/hooks/useUpdateCheck.ts +22 -4
- package/hooks/useUpdateToast.ts +1 -1
- package/hooks/useVimSelection.ts +9 -0
- package/package.json +23 -22
- package/shortcuts/code-review/annotationToolbar.shortcuts.ts +1 -0
- package/shortcuts/code-review/explorer.shortcuts.ts +82 -0
- package/shortcuts/core.ts +63 -4
- package/shortcuts/index.ts +1 -0
- package/shortcuts/plan-review/annotationPanel.shortcuts.ts +1 -1
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +1 -0
- package/shortcuts/runtime.ts +14 -6
- package/styles.css +1 -1
- package/theme.css +179 -1
- package/themes/plannotator-graphite.css +66 -0
- package/types.ts +26 -9
- package/utils/agentSwitch.ts +41 -20
- package/utils/agentToolSetting.ts +1 -1
- package/utils/aiChatFormat.ts +23 -7
- package/utils/annotationOwnsHighlight.ts +4 -2
- package/utils/asciidocParser.ts +570 -0
- package/utils/autoUpdateNotice.ts +24 -0
- package/utils/belowTitleSlot.ts +174 -0
- package/utils/codeLineScroll.ts +100 -0
- package/utils/commentSubmitKey.ts +48 -0
- package/utils/cssFontFamily.ts +65 -0
- package/utils/decisionSpec.ts +190 -2
- package/utils/defaultNotesApp.ts +1 -1
- package/utils/diffFonts.ts +4 -1
- package/utils/documentFont.ts +68 -0
- package/utils/documentTextSize.ts +54 -0
- package/utils/fontDetect.ts +63 -0
- package/utils/inboxAnnouncement.ts +78 -0
- package/utils/lookAndFeelAnnouncement.ts +82 -5
- package/utils/markdownExtensions.ts +2 -2
- package/utils/mentions.ts +34 -5
- package/utils/mermaid.ts +20 -3
- package/utils/mermaidTheme.ts +3 -51
- package/utils/notion.ts +58 -0
- package/utils/parser.ts +28 -1
- package/utils/platform.ts +46 -0
- package/utils/refCard.ts +82 -0
- package/utils/richClipboard.ts +380 -0
- package/utils/richClipboardLists.ts +57 -0
- package/utils/richCopyPolicy.ts +22 -0
- package/utils/scrollAnchor.ts +82 -0
- package/utils/sectionRefs.ts +123 -0
- package/utils/selectionBoundary.ts +40 -0
- package/utils/sendFailure.ts +85 -0
- package/utils/sharing.ts +5 -2
- package/utils/themeRegistry.ts +10 -0
- package/utils/themeTokens.ts +74 -0
- package/utils/updateNotice.ts +30 -8
- package/utils/vscodePanel.ts +9 -0
package/components/Viewer.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { generateId } from '../utils/generateId';
|
|
2
2
|
import React, { useRef, useState, useEffect, useLayoutEffect, useMemo, forwardRef, useImperativeHandle, useCallback, lazy, Suspense } from 'react';
|
|
3
3
|
import { createPortal } from 'react-dom';
|
|
4
|
-
import { collectDocumentImages, ImageLightbox, type LightboxImage } from './ImageLightbox';
|
|
4
|
+
import { collectDocumentImages, ImageLightbox, type LightboxImage, type LightboxRegionComments } from './ImageLightbox';
|
|
5
|
+
import { imageReferenceIndex, imageSourceLine, regionOf } from './ImageRegions';
|
|
5
6
|
import { AnnotationType, type Block, type Annotation, type EditorMode, type InputMethod, type ImageAttachment, type ActionsLabelMode, type HostDraft } from '../types';
|
|
6
7
|
import { applyHighlight, codeBlockClassName, onCodeHighlightSwap } from '../utils/codeHighlight';
|
|
7
8
|
import { paintCodeBlockMark } from '../utils/codeBlockMark';
|
|
@@ -9,7 +10,9 @@ import { useFenceTheme } from '../hooks/useFenceTheme';
|
|
|
9
10
|
import { computeListIndices, groupBlocks, selectionSourceLines, type Frontmatter, type FrontmatterValue } from '../utils/parser';
|
|
10
11
|
import { buildHeadingSlugMap } from '../utils/slugify';
|
|
11
12
|
import { copyTextToClipboard } from '../utils/clipboard';
|
|
13
|
+
import { getRichCopyEnabled } from '../utils/richCopyPolicy';
|
|
12
14
|
import { BlockRenderer } from './BlockRenderer';
|
|
15
|
+
import { BELOW_TITLE_SLOT_MARKER } from '../utils/belowTitleSlot';
|
|
13
16
|
import { indexQuestionBlocks, type IndexedQuestion, type ParsedQuestion, type QuestionAnswer } from '@plannotator/core/question-block';
|
|
14
17
|
import type { QuestionBlockProps } from './blocks/QuestionBlock';
|
|
15
18
|
import { resolveQuestionAnswers } from '../utils/questionAnswers';
|
|
@@ -36,6 +39,19 @@ import { FloatingQuickLabelPicker } from './FloatingQuickLabelPicker';
|
|
|
36
39
|
*/
|
|
37
40
|
const MermaidBlock = lazy(async () => ({ default: (await import('./MermaidBlock')).MermaidBlock }));
|
|
38
41
|
const GraphvizBlock = lazy(async () => ({ default: (await import('./GraphvizBlock')).GraphvizBlock }));
|
|
42
|
+
/** ```html and ```svg fences drawn, for a host that opts in (`htmlEmbeds`); loaded only then. */
|
|
43
|
+
const HtmlEmbedBlock = lazy(async () => ({ default: (await import('./html-embed/HtmlEmbedBlock')).HtmlEmbedBlock }));
|
|
44
|
+
const SvgFenceBlock = lazy(async () => ({ default: (await import('./html-embed/SvgFenceBlock')).SvgFenceBlock }));
|
|
45
|
+
/**
|
|
46
|
+
* Each lazy block sits in a `LazyBlockBoundary`: when its chunk cannot load,
|
|
47
|
+
* the block shows its source and a Reload notice, and the document stays.
|
|
48
|
+
*/
|
|
49
|
+
const embedFallback = (block: Block) => (notice: React.ReactNode) => (
|
|
50
|
+
<div className="my-5 [&>[data-block-id]]:my-0">
|
|
51
|
+
{notice}
|
|
52
|
+
<CodeBlock block={block} isHovered={false} />
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
39
55
|
|
|
40
56
|
// Debug error boundary to catch silent toolbar crashes
|
|
41
57
|
class ToolbarErrorBoundary extends React.Component<
|
|
@@ -61,7 +77,10 @@ import { AttachmentsButton } from './AttachmentsButton';
|
|
|
61
77
|
import { MessagesIcon } from './icons/MessagesIcon';
|
|
62
78
|
import { DiagramAnchorClaims, DiagramAnchorClaimsContext } from './diagram/anchorClaims';
|
|
63
79
|
import { DiagramBlockPending } from './diagram/DiagramPending';
|
|
80
|
+
import { LazyBlockBoundary } from './LazyBlockBoundary';
|
|
64
81
|
import { isGraphvizLanguage, isMermaidLanguage } from './diagramLanguages';
|
|
82
|
+
import { isHtmlEmbedLanguage, isSvgFenceLanguage } from './html-embed/languages';
|
|
83
|
+
import type { HtmlEmbedHost } from './html-embed/HtmlEmbedBlock';
|
|
65
84
|
import { getIdentity } from '../utils/identity';
|
|
66
85
|
import { type QuickLabel } from '../utils/quickLabels';
|
|
67
86
|
import type { SelectionAction } from '../utils/selectionActions';
|
|
@@ -152,6 +171,14 @@ export interface ViewerProps {
|
|
|
152
171
|
annotationHeader?: ViewerAnnotationHeaderConfig;
|
|
153
172
|
/** Render the plan as a floating card on a grid background (shadow/border/padding). Default false. */
|
|
154
173
|
gridEnabled?: boolean;
|
|
174
|
+
/**
|
|
175
|
+
* Grow the document card to the full height the parent gives the Viewer, so
|
|
176
|
+
* a short document's card still runs to the bottom of the view instead of
|
|
177
|
+
* stopping halfway down the grid paper. The parent must give the Viewer a
|
|
178
|
+
* height (a stretched flex item does). Default false: the card sizes to its
|
|
179
|
+
* content.
|
|
180
|
+
*/
|
|
181
|
+
fillHeight?: boolean;
|
|
155
182
|
onOpenLinkedDoc?: (path: string) => void;
|
|
156
183
|
onOpenCodeFile?: (path: string) => void;
|
|
157
184
|
/**
|
|
@@ -175,6 +202,19 @@ export interface ViewerProps {
|
|
|
175
202
|
* version). Absent or false: the single image overlay, as always.
|
|
176
203
|
*/
|
|
177
204
|
imageLightboxStepping?: boolean;
|
|
205
|
+
/**
|
|
206
|
+
* Comments on part of an image (#2100, approval "image-region-comments"):
|
|
207
|
+
* the lightbox gets a Move | Region switch over each markdown image, a
|
|
208
|
+
* drag in Region mode draws a box, and Save adds an ordinary COMMENT through
|
|
209
|
+
* `onAddAnnotation` carrying `imageRegion`. Selecting such a comment from
|
|
210
|
+
* outside (the document's annotations panel) opens the lightbox zoomed to
|
|
211
|
+
* its box. Absent or false: the lightbox is unchanged.
|
|
212
|
+
*/
|
|
213
|
+
imageRegionComments?: boolean;
|
|
214
|
+
/** Delete and edit from the lightbox's drawer of region comments (with
|
|
215
|
+
* `imageRegionComments`). Absent: the drawer's cards are read-only. */
|
|
216
|
+
onDeleteAnnotation?: (id: string) => void;
|
|
217
|
+
onEditAnnotation?: (id: string, updates: Partial<Annotation>) => void;
|
|
178
218
|
/** Directory the active document lives in — used by the code-path validator
|
|
179
219
|
* so out-of-tree relative references (e.g. `../foo.ts` in a linked doc)
|
|
180
220
|
* resolve against the doc's own directory rather than only cwd. */
|
|
@@ -266,6 +306,20 @@ export interface ViewerProps {
|
|
|
266
306
|
* `Decision:` line is shown as context and no question carries a decision
|
|
267
307
|
* tag or row from it. The prompt, key and numbering are unchanged. */
|
|
268
308
|
questionDecisionLines?: boolean;
|
|
309
|
+
/** Whether `:::question` cards read the contract lines (`Stakes:`,
|
|
310
|
+
* `Shows:`, `Why:`, `Reversible:`) into the card's Stakes line, Shows
|
|
311
|
+
* chip, Why line and "Can't be undone" tag. Default false: the lines show
|
|
312
|
+
* as context (plain text), as plan review and annotate draw them. The
|
|
313
|
+
* Plannotator Inbox passes true. The prompt, key, choices and numbering
|
|
314
|
+
* are the same either way. */
|
|
315
|
+
questionContractLines?: boolean;
|
|
316
|
+
/** Whether picture choices (`- [ ] Label — `) draw as picture
|
|
317
|
+
* tiles (default true; see `QuestionBlock` `pictureChoices`). False: text
|
|
318
|
+
* rows. */
|
|
319
|
+
questionPictureChoices?: boolean;
|
|
320
|
+
/** A small label over each picture tile's picture (see `QuestionBlock`
|
|
321
|
+
* `pictureBadge`); absent: none. */
|
|
322
|
+
questionPictureBadge?: (choice: { label: string; image: { alt: string; src: string } }) => string | null;
|
|
269
323
|
/** Makes a question's "Records a decision" tag a toggle button
|
|
270
324
|
* (`aria-pressed`); a click calls this with the question's key and the
|
|
271
325
|
* next state, which the host stores and returns from
|
|
@@ -306,6 +360,13 @@ export interface ViewerProps {
|
|
|
306
360
|
/** Host content right under a question card's prompt, per question (e.g.
|
|
307
361
|
* "Replaces <an earlier question>"); null for none. Absent: nothing there. */
|
|
308
362
|
renderQuestionBelowPrompt?: (question: IndexedQuestion) => React.ReactNode;
|
|
363
|
+
/** A host's own drawing of a directive block that is not a question (ui
|
|
364
|
+
* seam for a host's own directives, such as the Plannotator Inbox's
|
|
365
|
+
* `:::action` action item card). Called per `:::kind` block; a returned node is
|
|
366
|
+
* drawn in place of the block's callout, inside a wrapper carrying the
|
|
367
|
+
* block's `data-block-id`; `undefined` keeps the callout. Absent: every
|
|
368
|
+
* such block draws as today. */
|
|
369
|
+
renderDirectiveBlock?: (block: Block) => React.ReactNode | undefined;
|
|
309
370
|
onAskAI?: CommentAskAIHandler;
|
|
310
371
|
/** Whether comment popovers offer image attachments. Hosts without an
|
|
311
372
|
* uploadTransport pass false so the attach affordance never dead-ends.
|
|
@@ -326,6 +387,32 @@ export interface ViewerProps {
|
|
|
326
387
|
* blocks and no comments on its text. Absent → unchanged.
|
|
327
388
|
*/
|
|
328
389
|
answerOnly?: boolean;
|
|
390
|
+
/**
|
|
391
|
+
* Opt-in host capability (approval "inbox-inline-html"): a host that serves
|
|
392
|
+
* embed documents (see `HtmlEmbedHost`, @plannotator/core/html-embed) draws
|
|
393
|
+
* a ```html fence as a live, sandboxed page and a ```svg fence as an
|
|
394
|
+
* image, each with a Code button for the source. Absent → both stay code,
|
|
395
|
+
* unchanged (plan review, annotate and code review pass nothing).
|
|
396
|
+
*/
|
|
397
|
+
htmlEmbeds?: HtmlEmbedHost;
|
|
398
|
+
/**
|
|
399
|
+
* Opt-in host capability, with `answerOnly`: the reader may comment on the
|
|
400
|
+
* text. A text selection opens the selection toolbar, comment-only
|
|
401
|
+
* (Comment and the one-click 👍; no Delete, no label picker), and its
|
|
402
|
+
* comment composer; `annotations` are drawn as highlights. Question cards
|
|
403
|
+
* are left out of selection (their text is not commented on), and nothing
|
|
404
|
+
* else of authoring comes back (no pinpoint, code-block toolbar, Global
|
|
405
|
+
* comment or Copy, no Vim). For a host that shows a message whose words
|
|
406
|
+
* can be commented on (the Plannotator Inbox's agent messages). Ignored
|
|
407
|
+
* without `answerOnly` and under `readOnly`. Absent → unchanged.
|
|
408
|
+
*/
|
|
409
|
+
textComments?: boolean;
|
|
410
|
+
/**
|
|
411
|
+
* Opt-in: words a text-selection comment box opens with, for the selected
|
|
412
|
+
* text (a host's suggested comment, kept with one press or written over).
|
|
413
|
+
* Returning undefined opens an empty box. Absent → unchanged.
|
|
414
|
+
*/
|
|
415
|
+
suggestComment?: (selectedText: string) => string | undefined;
|
|
329
416
|
/** Opt-in Vim-style keyboard selection. Default false for compatibility. */
|
|
330
417
|
vimModeEnabled?: boolean;
|
|
331
418
|
/** Replace the compact Vim badge with the live video-style key HUD. */
|
|
@@ -349,12 +436,32 @@ export interface ViewerProps {
|
|
|
349
436
|
* unchanged.
|
|
350
437
|
*/
|
|
351
438
|
onHostDraft?: (draft: HostDraft | null) => void;
|
|
439
|
+
/**
|
|
440
|
+
* Host content drawn right under the document's title (e.g. a host's
|
|
441
|
+
* references rows). The title is the body's first block when that is a
|
|
442
|
+
* level-1 heading; the node is drawn right after that heading. With no
|
|
443
|
+
* leading `# heading` it is drawn at the top of the body, before the first
|
|
444
|
+
* block (after the frontmatter card). The heading itself is unchanged.
|
|
445
|
+
*
|
|
446
|
+
* The wrapper (`[data-viewer-below-title]`, `data-placement` `below-title`
|
|
447
|
+
* or `top`) sits outside everything that addresses the document's text: it
|
|
448
|
+
* is `annotation-exclude` (no comment starts in it and its text is never
|
|
449
|
+
* searched or quoted), `data-pinpoint-ignore` (pinpoint and Vim skip it),
|
|
450
|
+
* `data-print-hide`, and has no `data-block-id`. Stored comment positions
|
|
451
|
+
* are kept as if it were not there, so its contents never renumber a
|
|
452
|
+
* comment's parent element. `null`, `undefined` and booleans count as
|
|
453
|
+
* absent: no wrapper is drawn and the DOM is exactly as without the prop.
|
|
454
|
+
*/
|
|
455
|
+
belowTitle?: React.ReactNode;
|
|
352
456
|
}
|
|
353
457
|
|
|
354
458
|
export interface ViewerHandle {
|
|
355
459
|
removeHighlight: (id: string) => void;
|
|
356
460
|
clearAllHighlights: () => void;
|
|
357
461
|
applySharedAnnotations: (annotations: Annotation[]) => void;
|
|
462
|
+
/** Markdown `Viewer` only: scroll a heading anchor (`#id`) into view.
|
|
463
|
+
* Returns false when it is not in this document. */
|
|
464
|
+
scrollToAnchor?: (hash: string) => boolean;
|
|
358
465
|
/** `HtmlViewer` only: move a pinned draft to the element around it
|
|
359
466
|
* (`'parent'`) or back in (`'child'`), for a host that composes
|
|
360
467
|
* (`onHostDraft`). The markdown `Viewer` does not implement it. */
|
|
@@ -453,7 +560,7 @@ const FrontmatterCard: React.FC<{ frontmatter: Frontmatter }> = ({ frontmatter }
|
|
|
453
560
|
if (entries.length === 0) return null;
|
|
454
561
|
|
|
455
562
|
return (
|
|
456
|
-
<div className="mt-4 mb-6 p-4 bg-muted/30 border border-border/50 rounded-lg">
|
|
563
|
+
<div className="mt-4 mb-6 p-4 bg-muted/30 border border-border/50 rounded-lg" data-doc-text="">
|
|
457
564
|
<div className="grid gap-2 text-sm">
|
|
458
565
|
{entries.map(([key, value]) => (
|
|
459
566
|
<FrontmatterRow key={key} field={key} value={value} />
|
|
@@ -685,6 +792,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
685
792
|
stickyActions = true,
|
|
686
793
|
annotationHeader,
|
|
687
794
|
gridEnabled = false,
|
|
795
|
+
fillHeight = false,
|
|
688
796
|
planDiffStats,
|
|
689
797
|
isPlanDiffActive,
|
|
690
798
|
onPlanDiffToggle,
|
|
@@ -700,6 +808,9 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
700
808
|
imageBaseDir,
|
|
701
809
|
onImageClick: hostImageClick,
|
|
702
810
|
imageLightboxStepping = false,
|
|
811
|
+
imageRegionComments = false,
|
|
812
|
+
onDeleteAnnotation,
|
|
813
|
+
onEditAnnotation,
|
|
703
814
|
codePathBaseDir,
|
|
704
815
|
disableCodePathValidation,
|
|
705
816
|
onRestoreReport,
|
|
@@ -719,6 +830,9 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
719
830
|
questionDecisionRecording,
|
|
720
831
|
questionDecisionScope,
|
|
721
832
|
questionDecisionLines = true,
|
|
833
|
+
questionContractLines = false,
|
|
834
|
+
questionPictureChoices = true,
|
|
835
|
+
questionPictureBadge,
|
|
722
836
|
onToggleQuestionDecisionRecording,
|
|
723
837
|
onOpenQuestionDecision,
|
|
724
838
|
questionDecisionLocked,
|
|
@@ -726,36 +840,57 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
726
840
|
questionStatusTag,
|
|
727
841
|
questionStatusLabel,
|
|
728
842
|
renderQuestionCard,
|
|
843
|
+
renderDirectiveBlock,
|
|
729
844
|
renderQuestionBelowPrompt,
|
|
730
845
|
onAskAI,
|
|
731
846
|
allowImages = true,
|
|
732
847
|
readOnly = false,
|
|
733
848
|
answerOnly = false,
|
|
849
|
+
htmlEmbeds,
|
|
850
|
+
textComments = false,
|
|
851
|
+
suggestComment,
|
|
734
852
|
vimModeEnabled = false,
|
|
735
853
|
vimHudEnabled = false,
|
|
736
854
|
vimHudKeyPanelEnabled = true,
|
|
737
855
|
onVimHudKeyPanelChange,
|
|
738
856
|
onHostDraft,
|
|
857
|
+
belowTitle,
|
|
739
858
|
}, ref) => {
|
|
740
859
|
// Annotation authoring is off when read-only, and when the host only wants
|
|
741
860
|
// answers (`answerOnly`); question cards and checkboxes follow `readOnly`.
|
|
742
861
|
const authoringOff = readOnly || answerOnly;
|
|
862
|
+
// `textComments`: an answer-only document whose words still take comments
|
|
863
|
+
// (the selection toolbar, comment-only, and its composer); everything else
|
|
864
|
+
// of authoring stays off. Without it this is exactly `!authoringOff`.
|
|
865
|
+
const textCommentsOnly = answerOnly && textComments && !readOnly;
|
|
866
|
+
const selectionCommentsOn = !authoringOff || textCommentsOnly;
|
|
743
867
|
const hostComposes = onHostDraft !== undefined && !authoringOff;
|
|
744
868
|
const viewerAnnotationHeader = authoringOff ? undefined : annotationHeader;
|
|
745
869
|
const hasViewerAnnotationHeader = viewerAnnotationHeader !== undefined;
|
|
746
870
|
// Where `#123` / `@user` link: `refRepo` when the host gave one, else `repoInfo`.
|
|
747
871
|
const linkRepo = refRepo ?? repoInfo;
|
|
748
872
|
const [copied, setCopied] = useState(false);
|
|
749
|
-
const [lightbox, setLightboxState] = useState<{ src: string; alt: string; images?: LightboxImage[] } | null>(null);
|
|
873
|
+
const [lightbox, setLightboxState] = useState<{ src: string; alt: string; images?: LightboxImage[]; focusId?: string | null } | null>(null);
|
|
874
|
+
// The document's images for the lightbox: with region comments each knows
|
|
875
|
+
// its src as written, block and line.
|
|
876
|
+
const documentImages = (): LightboxImage[] =>
|
|
877
|
+
collectDocumentImages(containerRef.current).map((image) => {
|
|
878
|
+
if (!imageRegionComments || !image.writtenSrc || !image.blockId) return image;
|
|
879
|
+
const block = blocks.find((b) => b.id === image.blockId);
|
|
880
|
+
return { ...image, line: block ? imageSourceLine(block, image.writtenSrc) : null };
|
|
881
|
+
});
|
|
750
882
|
// A host that handles image clicks (the Inbox) gets them; otherwise the lightbox opens, stepping through
|
|
751
|
-
// the document's images when the host asked for it.
|
|
883
|
+
// the document's images when the host asked for it (and with region comments, always with the list).
|
|
752
884
|
const setLightbox = (image: { src: string; alt: string }) => {
|
|
753
885
|
if (hostImageClick) {
|
|
754
886
|
hostImageClick(image.src, image.alt);
|
|
755
887
|
return;
|
|
756
888
|
}
|
|
757
|
-
setLightboxState(imageLightboxStepping ? { ...image, images:
|
|
889
|
+
setLightboxState(imageLightboxStepping || imageRegionComments ? { ...image, images: documentImages() } : image);
|
|
758
890
|
};
|
|
891
|
+
// Region comments made in the lightbox are selected by the host as they are
|
|
892
|
+
// added; that selection must not reopen or refocus the lightbox.
|
|
893
|
+
const regionCreatedHere = useRef(new Set<string>());
|
|
759
894
|
const [locationHash, setLocationHash] = useState(() => window.location.hash);
|
|
760
895
|
const globalCommentButtonRef = useRef<HTMLButtonElement>(null);
|
|
761
896
|
// Read through a ref: only the imperative removeHighlight path below needs
|
|
@@ -810,9 +945,9 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
810
945
|
// annotations that carry `questionAnswer` (matched by question key, so an
|
|
811
946
|
// answer follows its question across re-parses).
|
|
812
947
|
const questionIndex = useMemo(() => {
|
|
813
|
-
const list = indexQuestionBlocks(blocks, { decisionLines: questionDecisionLines });
|
|
948
|
+
const list = indexQuestionBlocks(blocks, { decisionLines: questionDecisionLines, contractLines: questionContractLines });
|
|
814
949
|
return { byBlock: new Map(list.map((q) => [q.blockId, q])), total: list.length };
|
|
815
|
-
}, [blocks, questionDecisionLines]);
|
|
950
|
+
}, [blocks, questionDecisionLines, questionContractLines]);
|
|
816
951
|
// A host that keeps answers itself (`questionAnswers`) replaces the
|
|
817
952
|
// annotation-derived map; each entry is validated like any other reader.
|
|
818
953
|
const answersByKey = useMemo(
|
|
@@ -883,7 +1018,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
883
1018
|
onSelectAnnotation,
|
|
884
1019
|
selectedAnnotationId,
|
|
885
1020
|
mode,
|
|
886
|
-
enabled:
|
|
1021
|
+
enabled: selectionCommentsOn,
|
|
887
1022
|
// Markdown documents drift between the moment a draft/share is written and
|
|
888
1023
|
// the moment it is restored (a plan revision, a re-rendered block, a
|
|
889
1024
|
// renderer change that adds or drops elements — #1509 hoisted an alert's
|
|
@@ -1253,6 +1388,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1253
1388
|
|
|
1254
1389
|
// Imperative handle — delegates to hook, extends removeHighlight for code blocks
|
|
1255
1390
|
useImperativeHandle(ref, () => ({
|
|
1391
|
+
scrollToAnchor,
|
|
1256
1392
|
removeHighlight: (id: string) => {
|
|
1257
1393
|
// The re-highlight below notifies the swap listener, which would happily
|
|
1258
1394
|
// paint this annotation's mark straight back in — the host has not
|
|
@@ -1280,7 +1416,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1280
1416
|
clearAllHighlights,
|
|
1281
1417
|
applySharedAnnotations: applyAnnotations,
|
|
1282
1418
|
takeSelection,
|
|
1283
|
-
}), [hookRemoveHighlight, clearAllHighlights, applyAnnotations, blocks, takeSelection]);
|
|
1419
|
+
}), [hookRemoveHighlight, clearAllHighlights, applyAnnotations, blocks, takeSelection, scrollToAnchor]);
|
|
1284
1420
|
|
|
1285
1421
|
// --- Viewer-specific: code block annotation ---
|
|
1286
1422
|
|
|
@@ -1451,6 +1587,70 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1451
1587
|
</>
|
|
1452
1588
|
);
|
|
1453
1589
|
|
|
1590
|
+
// --- Comments on part of an image (#2100) ---------------------------------
|
|
1591
|
+
const regionHost = useMemo<LightboxRegionComments | undefined>(() => {
|
|
1592
|
+
if (!imageRegionComments) return undefined;
|
|
1593
|
+
return {
|
|
1594
|
+
annotations,
|
|
1595
|
+
canCreate: !authoringOff,
|
|
1596
|
+
onAdd: (annotation) => {
|
|
1597
|
+
regionCreatedHere.current.add(annotation.id);
|
|
1598
|
+
onAddAnnotation(annotation);
|
|
1599
|
+
},
|
|
1600
|
+
onDelete: authoringOff ? undefined : onDeleteAnnotation,
|
|
1601
|
+
onEdit: authoringOff ? undefined : onEditAnnotation,
|
|
1602
|
+
focusId: lightbox?.focusId ?? null,
|
|
1603
|
+
onAskAI: authoringOff ? undefined : onAskAI,
|
|
1604
|
+
mentionSource,
|
|
1605
|
+
};
|
|
1606
|
+
}, [annotations, authoringOff, imageRegionComments, lightbox?.focusId, mentionSource, onAddAnnotation, onAskAI, onDeleteAnnotation, onEditAnnotation]);
|
|
1607
|
+
|
|
1608
|
+
const closeLightbox = () => {
|
|
1609
|
+
// Closing the viewer lets go of the region comment selected while it was
|
|
1610
|
+
// open (one it was opened on, or one just made in it), so the same card
|
|
1611
|
+
// in the panel opens it again.
|
|
1612
|
+
const selected = selectedAnnotationId ? annotations.find((a) => a.id === selectedAnnotationId) : undefined;
|
|
1613
|
+
if (selected && regionOf(selected)) onSelectAnnotation(null);
|
|
1614
|
+
regionCreatedHere.current.clear();
|
|
1615
|
+
setLightboxState(null);
|
|
1616
|
+
};
|
|
1617
|
+
|
|
1618
|
+
// A region comment selected from outside (its card in the annotations
|
|
1619
|
+
// panel) opens the lightbox on its image, zoomed to its box.
|
|
1620
|
+
useEffect(() => {
|
|
1621
|
+
if (!imageRegionComments || !selectedAnnotationId || hostImageClick) return;
|
|
1622
|
+
// The host selects what the viewer just added: skip that one selection.
|
|
1623
|
+
if (regionCreatedHere.current.delete(selectedAnnotationId)) return;
|
|
1624
|
+
const ann = annotations.find((a) => a.id === selectedAnnotationId);
|
|
1625
|
+
const region = ann ? regionOf(ann) : null;
|
|
1626
|
+
if (!region) return;
|
|
1627
|
+
if (lightbox) {
|
|
1628
|
+
setLightboxState((lb) => (lb ? { ...lb, focusId: selectedAnnotationId } : lb));
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
const images = documentImages();
|
|
1632
|
+
const target = images.find((image) => image.writtenSrc === region.src);
|
|
1633
|
+
if (!target) return;
|
|
1634
|
+
setLightboxState({ src: target.src, alt: target.alt, images, focusId: selectedAnnotationId });
|
|
1635
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1636
|
+
}, [selectedAnnotationId]);
|
|
1637
|
+
|
|
1638
|
+
// The highlighter skips region comments; one whose image is no longer in
|
|
1639
|
+
// the document (or whose region is malformed) is unanchored, and says so.
|
|
1640
|
+
useEffect(() => {
|
|
1641
|
+
if (onRestoreReport === undefined) return;
|
|
1642
|
+
const rows = annotations.filter((ann) => ann.imageRegion != null);
|
|
1643
|
+
if (rows.length === 0) return;
|
|
1644
|
+
const text = blocks.map((b) => b.content).join('\n');
|
|
1645
|
+
const unanchored = rows
|
|
1646
|
+
.filter((ann) => {
|
|
1647
|
+
const region = regionOf(ann);
|
|
1648
|
+
return region === null || imageReferenceIndex(text, region.src) < 0;
|
|
1649
|
+
})
|
|
1650
|
+
.map((ann) => ann.id);
|
|
1651
|
+
onRestoreReport({ attempted: rows.map((ann) => ann.id), unanchored });
|
|
1652
|
+
}, [annotations, blocks, onRestoreReport]);
|
|
1653
|
+
|
|
1454
1654
|
// The document's diagram blocks, in order: a diagram comment that names
|
|
1455
1655
|
// none of them (an external POST, a deleted fence) is resolved by anchor
|
|
1456
1656
|
// against each, first resolver wins (see diagram/anchorClaims).
|
|
@@ -1473,15 +1673,44 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1473
1673
|
if (ids.length > 0) onRestoreReport({ attempted: ids, unanchored: ids });
|
|
1474
1674
|
}, [annotations, diagramBlockKey, onRestoreReport]);
|
|
1475
1675
|
|
|
1676
|
+
// The host's `belowTitle` slot (see the prop). Absent → the block list is
|
|
1677
|
+
// returned as is, so the DOM is the one without the prop. The wrapper's
|
|
1678
|
+
// attribute carries utils/belowTitleSlot's per-load marker, which is how
|
|
1679
|
+
// the highlighter tells it from raw HTML an author wrote, to keep stored
|
|
1680
|
+
// positions slot-free.
|
|
1681
|
+
const belowTitleSlot = belowTitle == null || typeof belowTitle === 'boolean' ? null : belowTitle;
|
|
1682
|
+
const titleLeads = blocks[0]?.type === 'heading' && blocks[0].level === 1;
|
|
1683
|
+
const withBelowTitle = (rendered: React.ReactNode[]): React.ReactNode[] => {
|
|
1684
|
+
if (belowTitleSlot === null) return rendered;
|
|
1685
|
+
const slot = (
|
|
1686
|
+
<div
|
|
1687
|
+
key="viewer-below-title"
|
|
1688
|
+
data-viewer-below-title={BELOW_TITLE_SLOT_MARKER}
|
|
1689
|
+
data-placement={titleLeads ? 'below-title' : 'top'}
|
|
1690
|
+
data-pinpoint-ignore=""
|
|
1691
|
+
data-print-hide=""
|
|
1692
|
+
className="annotation-exclude"
|
|
1693
|
+
>
|
|
1694
|
+
{belowTitleSlot}
|
|
1695
|
+
</div>
|
|
1696
|
+
);
|
|
1697
|
+
return titleLeads ? [rendered[0], slot, ...rendered.slice(1)] : [slot, ...rendered];
|
|
1698
|
+
};
|
|
1699
|
+
|
|
1476
1700
|
return (
|
|
1477
1701
|
<CodePathValidationContext.Provider value={codePathValidation}>
|
|
1478
1702
|
<LocalLinkContext.Provider value={resolveLocalLink ?? null}>
|
|
1479
1703
|
<DiagramAnchorClaimsContext.Provider value={diagramClaims}>
|
|
1480
|
-
<div
|
|
1704
|
+
<div
|
|
1705
|
+
className={`relative z-50 w-full${fillHeight ? ' flex flex-col print:block' : ''}`}
|
|
1706
|
+
data-viewer-fill-height={fillHeight ? '' : undefined}
|
|
1707
|
+
style={maxWidth === null ? undefined : { maxWidth: maxWidth ?? 832 }}
|
|
1708
|
+
>
|
|
1481
1709
|
{taterMode && <TaterSpriteSitting />}
|
|
1482
1710
|
<article
|
|
1483
1711
|
ref={containerRef}
|
|
1484
1712
|
data-print-region="article"
|
|
1713
|
+
data-doc-text-root=""
|
|
1485
1714
|
data-vim-mode={vimModeActive ? 'enabled' : undefined}
|
|
1486
1715
|
data-vim-phase={vimModeActive ? vim.state.phase : undefined}
|
|
1487
1716
|
data-vim-focused={vimModeActive ? String(vim.focused) : undefined}
|
|
@@ -1491,7 +1720,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1491
1720
|
onFocus={vim.onFocus}
|
|
1492
1721
|
onBlur={vim.onBlur}
|
|
1493
1722
|
onMouseDown={vim.onMouseDown}
|
|
1494
|
-
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' : ''}`}
|
|
1723
|
+
className={`w-full bg-card rounded-xl py-5 md:py-8 lg:py-10 xl:py-12 relative ${fillHeight ? 'flex-1 ' : ''}${gridEnabled ? 'px-5 md:px-8 lg:px-10 xl:px-12 shadow-xl border border-border/50' : ''} ${inputMethod === 'pinpoint' ? 'cursor-pointer' : ''}`}
|
|
1495
1724
|
style={{
|
|
1496
1725
|
WebkitTouchCallout: 'none',
|
|
1497
1726
|
...(vimModeActive ? { outline: 'none' } : {}),
|
|
@@ -1560,7 +1789,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1560
1789
|
)}
|
|
1561
1790
|
{frontmatter && <><div className="clear-right md:hidden" /><FrontmatterCard frontmatter={frontmatter} /></>}
|
|
1562
1791
|
{!frontmatter && blocks.length > 0 && blocks[0].type !== 'heading' && <div className="mt-4" />}
|
|
1563
|
-
{groupBlocks(blocks).map(group =>
|
|
1792
|
+
{withBelowTitle(groupBlocks(blocks).map(group =>
|
|
1564
1793
|
group.type === 'list-group' ? (
|
|
1565
1794
|
(() => {
|
|
1566
1795
|
const indices = computeListIndices(group.blocks);
|
|
@@ -1586,34 +1815,50 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1586
1815
|
</div>
|
|
1587
1816
|
);
|
|
1588
1817
|
})()
|
|
1818
|
+
) : htmlEmbeds && group.block.type === 'code' && isHtmlEmbedLanguage(group.block.language) ? (
|
|
1819
|
+
<LazyBlockBoundary key={group.block.id} fallback={embedFallback(group.block)}>
|
|
1820
|
+
<Suspense fallback={<CodeBlock block={group.block} isHovered={false} />}>
|
|
1821
|
+
<HtmlEmbedBlock block={group.block} host={htmlEmbeds} />
|
|
1822
|
+
</Suspense>
|
|
1823
|
+
</LazyBlockBoundary>
|
|
1824
|
+
) : htmlEmbeds && group.block.type === 'code' && isSvgFenceLanguage(group.block.language) ? (
|
|
1825
|
+
<LazyBlockBoundary key={group.block.id} fallback={embedFallback(group.block)}>
|
|
1826
|
+
<Suspense fallback={<CodeBlock block={group.block} isHovered={false} />}>
|
|
1827
|
+
<SvgFenceBlock block={group.block} />
|
|
1828
|
+
</Suspense>
|
|
1829
|
+
</LazyBlockBoundary>
|
|
1589
1830
|
) : group.block.type === 'code' && isMermaidLanguage(group.block.language) ? (
|
|
1590
|
-
<
|
|
1591
|
-
<
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1831
|
+
<LazyBlockBoundary key={group.block.id} fallback={(notice) => <DiagramBlockPending block={group.block} kind="mermaid" notice={notice} />}>
|
|
1832
|
+
<Suspense fallback={<DiagramBlockPending block={group.block} kind="mermaid" />}>
|
|
1833
|
+
<MermaidBlock
|
|
1834
|
+
block={group.block}
|
|
1835
|
+
annotations={annotations}
|
|
1836
|
+
selectedAnnotationId={selectedAnnotationId}
|
|
1837
|
+
onSelectAnnotation={onSelectAnnotation}
|
|
1838
|
+
onAddAnnotation={authoringOff ? undefined : onAddAnnotation}
|
|
1839
|
+
readOnly={authoringOff}
|
|
1840
|
+
onRestoreReport={onRestoreReport}
|
|
1841
|
+
onAskAI={authoringOff ? undefined : onAskAI}
|
|
1842
|
+
askAISourcePath={linkedDocInfo?.filepath ?? sourceInfo}
|
|
1843
|
+
/>
|
|
1844
|
+
</Suspense>
|
|
1845
|
+
</LazyBlockBoundary>
|
|
1603
1846
|
) : group.block.type === 'code' && isGraphvizLanguage(group.block.language) ? (
|
|
1604
|
-
<
|
|
1605
|
-
<
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1847
|
+
<LazyBlockBoundary key={group.block.id} fallback={(notice) => <DiagramBlockPending block={group.block} kind="graphviz" notice={notice} />}>
|
|
1848
|
+
<Suspense fallback={<DiagramBlockPending block={group.block} kind="graphviz" />}>
|
|
1849
|
+
<GraphvizBlock
|
|
1850
|
+
block={group.block}
|
|
1851
|
+
annotations={annotations}
|
|
1852
|
+
selectedAnnotationId={selectedAnnotationId}
|
|
1853
|
+
onSelectAnnotation={onSelectAnnotation}
|
|
1854
|
+
onAddAnnotation={authoringOff ? undefined : onAddAnnotation}
|
|
1855
|
+
readOnly={authoringOff}
|
|
1856
|
+
onRestoreReport={onRestoreReport}
|
|
1857
|
+
onAskAI={authoringOff ? undefined : onAskAI}
|
|
1858
|
+
askAISourcePath={linkedDocInfo?.filepath ?? sourceInfo}
|
|
1859
|
+
/>
|
|
1860
|
+
</Suspense>
|
|
1861
|
+
</LazyBlockBoundary>
|
|
1617
1862
|
) : group.block.type === 'table' ? (
|
|
1618
1863
|
<TableBlock
|
|
1619
1864
|
key={group.block.id}
|
|
@@ -1694,15 +1939,17 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1694
1939
|
const question = questionIndex.byBlock.get(group.block.id);
|
|
1695
1940
|
const hostCard = question && renderQuestionCard
|
|
1696
1941
|
? renderQuestionCard(question, answersByKey.get(question.question.key))
|
|
1697
|
-
:
|
|
1942
|
+
: !question && group.block.type === 'directive' && renderDirectiveBlock
|
|
1943
|
+
? renderDirectiveBlock(group.block)
|
|
1944
|
+
: undefined;
|
|
1698
1945
|
if (hostCard !== undefined) {
|
|
1699
1946
|
return (
|
|
1700
|
-
<div key={group.block.id} data-block-id={group.block.id} data-block-type="directive">
|
|
1947
|
+
<div key={group.block.id} data-block-id={group.block.id} data-block-type="directive" className={textCommentsOnly ? 'annotation-exclude' : undefined}>
|
|
1701
1948
|
{hostCard}
|
|
1702
1949
|
</div>
|
|
1703
1950
|
);
|
|
1704
1951
|
}
|
|
1705
|
-
|
|
1952
|
+
const rendered = (
|
|
1706
1953
|
<BlockRenderer
|
|
1707
1954
|
imageBaseDir={imageBaseDir}
|
|
1708
1955
|
onImageClick={(src, alt) => setLightbox({ src, alt })}
|
|
@@ -1730,18 +1977,33 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1730
1977
|
questionStatusTag={questionStatusTag}
|
|
1731
1978
|
questionStatusLabel={questionStatusLabel}
|
|
1732
1979
|
renderQuestionBelowPrompt={renderQuestionBelowPrompt}
|
|
1980
|
+
questionContractLines={questionContractLines}
|
|
1981
|
+
questionPictureChoices={questionPictureChoices}
|
|
1982
|
+
questionPictureBadge={questionPictureBadge}
|
|
1733
1983
|
/>
|
|
1734
1984
|
);
|
|
1985
|
+
// `textComments`: a question card's words are not commented on
|
|
1986
|
+
// (its own controls already are excluded); a box-less wrapper
|
|
1987
|
+
// keeps its layout.
|
|
1988
|
+
if (question && textCommentsOnly) {
|
|
1989
|
+
return (
|
|
1990
|
+
<div key={group.block.id} className="annotation-exclude" style={{ display: 'contents' }}>
|
|
1991
|
+
{rendered}
|
|
1992
|
+
</div>
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
return rendered;
|
|
1735
1996
|
})()
|
|
1736
1997
|
)
|
|
1737
|
-
)}
|
|
1998
|
+
))}
|
|
1738
1999
|
|
|
1739
2000
|
{/* Text selection toolbar */}
|
|
1740
|
-
{
|
|
2001
|
+
{selectionCommentsOn && !hostComposes && toolbarState && (
|
|
1741
2002
|
<ToolbarErrorBoundary>
|
|
1742
2003
|
<AnnotationToolbar
|
|
1743
2004
|
element={toolbarState.element}
|
|
1744
2005
|
positionMode="center-above"
|
|
2006
|
+
commentOnly={textCommentsOnly || undefined}
|
|
1745
2007
|
onAnnotate={handleAnnotate}
|
|
1746
2008
|
onClose={handleToolbarClose}
|
|
1747
2009
|
onRequestComment={handleRequestComment}
|
|
@@ -1750,6 +2012,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1750
2012
|
selectionActionsIcon={selectionActionsIcon}
|
|
1751
2013
|
quickLabels={quickLabels}
|
|
1752
2014
|
copyText={toolbarState.selectionText}
|
|
2015
|
+
getRichCopy={getRichCopyEnabled() ? toolbarState.getRichCopy : undefined}
|
|
1753
2016
|
hideCopyButton={!isTouchDevice}
|
|
1754
2017
|
closeOnScrollOut
|
|
1755
2018
|
/>
|
|
@@ -1876,12 +2139,12 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1876
2139
|
)}
|
|
1877
2140
|
|
|
1878
2141
|
{/* Comment popover — hook handles text selection, Viewer handles global + code block */}
|
|
1879
|
-
{
|
|
2142
|
+
{selectionCommentsOn && !hostComposes && hookCommentPopover && (
|
|
1880
2143
|
<CommentPopover
|
|
1881
2144
|
anchorEl={hookCommentPopover.anchorEl}
|
|
1882
2145
|
contextText={hookCommentPopover.contextText}
|
|
1883
2146
|
isGlobal={false}
|
|
1884
|
-
initialText={hookCommentPopover.initialText}
|
|
2147
|
+
initialText={hookCommentPopover.initialText || suggestComment?.(hookCommentPopover.selectedText ?? hookCommentPopover.contextText) || undefined}
|
|
1885
2148
|
draftKey={`plan:${commentDraftScope}:${hookCommentPopover.draftKey}`}
|
|
1886
2149
|
onSubmit={hookCommentSubmit}
|
|
1887
2150
|
onClose={hookCommentClose}
|
|
@@ -1928,7 +2191,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1928
2191
|
)}
|
|
1929
2192
|
|
|
1930
2193
|
{/* Quick Label floating picker — hook handles text selection, Viewer handles code blocks */}
|
|
1931
|
-
{
|
|
2194
|
+
{selectionCommentsOn && !hostComposes && hookQuickLabelPicker && (
|
|
1932
2195
|
<FloatingQuickLabelPicker
|
|
1933
2196
|
anchorEl={hookQuickLabelPicker.anchorEl}
|
|
1934
2197
|
cursorHint={hookQuickLabelPicker.cursorHint}
|
|
@@ -1960,7 +2223,13 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1960
2223
|
|
|
1961
2224
|
{/* Image lightbox */}
|
|
1962
2225
|
{lightbox && createPortal(
|
|
1963
|
-
<ImageLightbox
|
|
2226
|
+
<ImageLightbox
|
|
2227
|
+
src={lightbox.src}
|
|
2228
|
+
alt={lightbox.alt}
|
|
2229
|
+
images={lightbox.images}
|
|
2230
|
+
regions={regionHost}
|
|
2231
|
+
onClose={closeLightbox}
|
|
2232
|
+
/>,
|
|
1964
2233
|
document.body
|
|
1965
2234
|
)}
|
|
1966
2235
|
</div>
|