@plannotator/ui 0.52.0 → 0.53.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 +317 -29
- package/README.md +35 -3
- package/components/AnnotationPanel.tsx +159 -107
- package/components/AnnotationToolbar.tsx +17 -14
- package/components/AnnotationToolstrip.tsx +9 -0
- package/components/BlockRenderer.tsx +26 -14
- package/components/CodeFilePopout.tsx +131 -68
- package/components/CommentPopover.tsx +6 -349
- package/components/ComposerTokenTextarea.tsx +379 -0
- package/components/ExportModal.tsx +6 -1
- package/components/HostTitle.tsx +108 -0
- package/components/ImageLightbox.tsx +288 -17
- package/components/ImagePeek.tsx +255 -0
- package/components/ImageResolverContext.tsx +31 -0
- package/components/InlineMarkdown.tsx +123 -15
- package/components/LocalLinkContext.tsx +49 -0
- package/components/MenuVersionSection.tsx +51 -25
- package/components/OpenInAppButton.tsx +18 -49
- package/components/PlanHeaderMenu.tsx +2 -8
- package/components/ProviderIcons.tsx +10 -4
- package/components/RenderedMarkdown.tsx +40 -4
- package/components/Settings.tsx +33 -5
- package/components/SkillReferenceMenu.tsx +28 -6
- package/components/Tooltip.tsx +51 -4
- package/components/Viewer.tsx +214 -55
- package/components/ai/AIProviderBar.tsx +11 -0
- package/components/ai/AskSessionSetupHint.tsx +12 -0
- package/components/ai/DocumentAIChatPanel.tsx +17 -5
- package/components/ai/SessionAskNotice.tsx +4 -3
- package/components/blocks/BlockquoteBlock.tsx +159 -0
- package/components/blocks/HtmlBlock.tsx +121 -9
- package/components/blocks/QuestionBlock.tsx +253 -104
- package/components/diagram/DiagramViewer.tsx +41 -4
- package/components/diagram/index.ts +1 -1
- package/components/html-viewer/HtmlViewer.tsx +73 -3
- package/components/html-viewer/bridge-script.asset.js +86 -0
- package/components/html-viewer/bridge-script.ts +86 -0
- package/components/html-viewer/useHtmlAnnotation.ts +51 -3
- package/components/icons/AgentIcons.tsx +9 -7
- package/components/plan-diff/PlanCleanDiffView.tsx +239 -36
- package/components/plan-diff/PlanDiffViewer.tsx +6 -1
- package/configure.ts +27 -1
- package/hooks/useAgentJobs.ts +8 -3
- package/hooks/useAnnotationHighlighter.ts +128 -23
- package/hooks/useExternalAnnotations.ts +6 -0
- package/hooks/useOpenInApps.ts +88 -0
- package/hooks/useUpdateCheck.ts +38 -98
- package/hooks/useUpdateCheckSetting.ts +43 -0
- package/hooks/useUpdateToast.ts +101 -0
- package/package.json +2 -2
- package/shortcuts/code-review/markdownPreview.shortcuts.ts +18 -0
- package/shortcuts/index.ts +1 -0
- package/styles.css +1 -1
- package/types.ts +48 -0
- package/utils/agentClosed.ts +26 -0
- package/utils/agentToolSetting.ts +16 -9
- package/utils/aiProvider.ts +7 -0
- package/utils/askSessionSetupHint.ts +27 -0
- package/utils/blockTargeting.ts +14 -3
- package/utils/parser.ts +65 -51
- package/utils/pasteInsert.ts +234 -0
- package/utils/planDiffEngine.ts +272 -2
- package/utils/skillCatalog.ts +15 -2
- package/utils/skillReferences.ts +50 -13
- package/utils/storage.ts +5 -0
- package/utils/textCut.ts +41 -0
- package/utils/updateNotice.ts +120 -0
|
@@ -15,6 +15,7 @@ import { ROOT_DOCUMENT_GROUP_KEY } from '../utils/annotationScope';
|
|
|
15
15
|
import type { AnnotationScope, AnnotationDocumentGroup } from '../utils/annotationScope';
|
|
16
16
|
import type { QuestionPanelRow } from '../utils/questionAnswers';
|
|
17
17
|
import { QuestionsPanelSection } from './QuestionsPanelSection';
|
|
18
|
+
import { HostTitle } from './HostTitle';
|
|
18
19
|
|
|
19
20
|
// Card type-word colors. Deletion uses `destructive` (reliably red on every
|
|
20
21
|
// theme, matching the in-document .deletion highlight). Comment uses the
|
|
@@ -101,6 +102,8 @@ interface DirectEditsPanelItem {
|
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
interface PanelProps {
|
|
105
|
+
/** The second line of the empty state. Default "Select text to annotate"; a host whose document is an image or code says how to comment there. */
|
|
106
|
+
emptyHint?: string;
|
|
104
107
|
isOpen: boolean;
|
|
105
108
|
annotations: Annotation[];
|
|
106
109
|
blocks: Block[];
|
|
@@ -189,9 +192,32 @@ interface PanelProps {
|
|
|
189
192
|
questionRows?: readonly QuestionPanelRow[];
|
|
190
193
|
/** A Questions row was clicked (the host scrolls to the card). */
|
|
191
194
|
onSelectQuestion?: (row: QuestionPanelRow) => void;
|
|
195
|
+
/**
|
|
196
|
+
* The order of the comment timeline (and of each document group), for a
|
|
197
|
+
* host whose comments have an order of their own (the Inbox's video
|
|
198
|
+
* comments, by the moment they are at). Ties keep creation order. Absent:
|
|
199
|
+
* creation order, exactly as before.
|
|
200
|
+
*/
|
|
201
|
+
compareAnnotations?: (a: Annotation, b: Annotation) => number;
|
|
202
|
+
/**
|
|
203
|
+
* The panel's title (default "Annotations") and a quiet note at the right
|
|
204
|
+
* of its header (the Inbox's "By time" for a video's comments). Absent:
|
|
205
|
+
* the header is exactly as before.
|
|
206
|
+
*/
|
|
207
|
+
title?: string;
|
|
208
|
+
titleNote?: string;
|
|
209
|
+
/**
|
|
210
|
+
* Which built-in parts a plan-annotation card draws: its quoted text
|
|
211
|
+
* (`quote`) and its attached images under the comment (`images`). A host
|
|
212
|
+
* that draws them itself (the Inbox's video comments show the moment and
|
|
213
|
+
* the frame in `renderCardHeader`) turns them off per card. Absent: both
|
|
214
|
+
* drawn, exactly as before.
|
|
215
|
+
*/
|
|
216
|
+
cardParts?: (annotation: Annotation) => { quote?: boolean; images?: boolean };
|
|
192
217
|
}
|
|
193
218
|
|
|
194
219
|
export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
220
|
+
emptyHint = 'Select text to annotate',
|
|
195
221
|
isOpen,
|
|
196
222
|
annotations,
|
|
197
223
|
onSelect,
|
|
@@ -226,6 +252,10 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
226
252
|
onEditInDocument,
|
|
227
253
|
questionRows,
|
|
228
254
|
onSelectQuestion,
|
|
255
|
+
compareAnnotations,
|
|
256
|
+
title = 'Annotations',
|
|
257
|
+
titleNote,
|
|
258
|
+
cardParts,
|
|
229
259
|
}) => {
|
|
230
260
|
const isMobile = useIsMobile();
|
|
231
261
|
const embedded = presentation === 'embedded';
|
|
@@ -237,7 +267,9 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
237
267
|
const timelineAnnotations = questionRows !== undefined
|
|
238
268
|
? annotations.filter((a) => a.questionAnswer == null)
|
|
239
269
|
: annotations;
|
|
240
|
-
const
|
|
270
|
+
const byCreation = (a: Annotation, b: Annotation) => a.createdA - b.createdA;
|
|
271
|
+
const byHost = compareAnnotations ? (a: Annotation, b: Annotation) => compareAnnotations(a, b) || byCreation(a, b) : byCreation;
|
|
272
|
+
const sortedAnnotations = [...timelineAnnotations].sort(byHost);
|
|
241
273
|
const sortedCodeAnnotations = [...codeAnnotations].sort((a, b) => a.createdAt - b.createdAt);
|
|
242
274
|
// Replies (`inReplyTo`) thread under their parent: each reply is lifted to
|
|
243
275
|
// sit right after its parent (and the parent's earlier replies) at the
|
|
@@ -248,13 +280,15 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
248
280
|
// comparator with a linear parent lookup was O(n^2 log n) and froze the
|
|
249
281
|
// tab on a few thousand threaded comments.
|
|
250
282
|
const threadRootTs = resolveThreadRootTimestamps(sortedAnnotations);
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
283
|
+
const planEntries = threadedAnnotations.map(({ annotation, isReply }) => ({ kind: 'plan' as const, ts: annotation.createdA, threadTs: threadRootTs.get(annotation.id) ?? annotation.createdA, annotation, isReply }));
|
|
284
|
+
const codeEntries = sortedCodeAnnotations.map(annotation => ({ kind: 'code' as const, ts: annotation.createdAt, threadTs: annotation.createdAt, annotation, isReply: false }));
|
|
285
|
+
// A host order keeps the plan comments as the host sorted them (code comments after).
|
|
286
|
+
const timelineEntries = compareAnnotations
|
|
287
|
+
? [...planEntries, ...codeEntries]
|
|
288
|
+
: [...planEntries, ...codeEntries].sort((a, b) => {
|
|
289
|
+
if (a.threadTs !== b.threadTs) return a.threadTs - b.threadTs;
|
|
290
|
+
return a.ts - b.ts;
|
|
291
|
+
});
|
|
258
292
|
const totalCount = annotations.length + codeAnnotations.length + (editorAnnotations?.length ?? 0);
|
|
259
293
|
const timelineCount = timelineAnnotations.length + codeAnnotations.length + (editorAnnotations?.length ?? 0);
|
|
260
294
|
|
|
@@ -332,7 +366,7 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
332
366
|
<div className="flex h-10 items-center justify-between px-3">
|
|
333
367
|
<div className="flex items-center gap-2">
|
|
334
368
|
<h2 className="text-xs font-medium text-foreground">
|
|
335
|
-
|
|
369
|
+
{title}
|
|
336
370
|
</h2>
|
|
337
371
|
{headerCount > 0 && (
|
|
338
372
|
<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">
|
|
@@ -340,17 +374,19 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
340
374
|
</span>
|
|
341
375
|
)}
|
|
342
376
|
</div>
|
|
377
|
+
{titleNote && <span data-annotation-panel-note="" className="ml-auto mr-1 text-[11px] text-muted-foreground">{titleNote}</span>}
|
|
343
378
|
{mobilePanel && onClose && (
|
|
344
|
-
<
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
379
|
+
<HostTitle title="Close panel">
|
|
380
|
+
<button
|
|
381
|
+
onClick={onClose}
|
|
382
|
+
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"
|
|
383
|
+
aria-label="Close panel"
|
|
384
|
+
>
|
|
385
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
386
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
387
|
+
</svg>
|
|
388
|
+
</button>
|
|
389
|
+
</HostTitle>
|
|
354
390
|
)}
|
|
355
391
|
</div>
|
|
356
392
|
{scopeEnabled ? (
|
|
@@ -364,13 +400,14 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
364
400
|
</div>
|
|
365
401
|
) : (
|
|
366
402
|
otherFileAnnotations && otherFileAnnotations.count > 0 && (
|
|
367
|
-
<
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
403
|
+
<HostTitle title="Show annotated files in sidebar">
|
|
404
|
+
<button
|
|
405
|
+
onClick={onOtherFileAnnotationsClick}
|
|
406
|
+
className="px-3 pb-2 text-[10px] text-primary/70 hover:text-primary transition-colors cursor-pointer"
|
|
407
|
+
>
|
|
408
|
+
+{otherFileAnnotations.count} in {otherFileAnnotations.files} other file{otherFileAnnotations.files === 1 ? '' : 's'}
|
|
409
|
+
</button>
|
|
410
|
+
</HostTitle>
|
|
374
411
|
)
|
|
375
412
|
)}
|
|
376
413
|
</div>
|
|
@@ -386,15 +423,16 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
386
423
|
)}
|
|
387
424
|
|
|
388
425
|
{embedded && !scopeEnabled && otherFileAnnotations && otherFileAnnotations.count > 0 && (
|
|
389
|
-
<
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
426
|
+
<HostTitle title="Show annotated files in navigator">
|
|
427
|
+
<button
|
|
428
|
+
type="button"
|
|
429
|
+
data-pn-touch-target="true"
|
|
430
|
+
onClick={onOtherFileAnnotationsClick}
|
|
431
|
+
className="min-h-11 flex-shrink-0 border-b border-border/50 px-3 text-left text-xs text-primary/80 active:bg-muted"
|
|
432
|
+
>
|
|
433
|
+
{otherFileAnnotations.count} more in {otherFileAnnotations.files} other file{otherFileAnnotations.files === 1 ? '' : 's'}
|
|
434
|
+
</button>
|
|
435
|
+
</HostTitle>
|
|
398
436
|
)}
|
|
399
437
|
|
|
400
438
|
{/* List */}
|
|
@@ -424,7 +462,7 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
424
462
|
const groupAnnotations = group.isCurrent && questionRows !== undefined
|
|
425
463
|
? group.annotations.filter((a) => a.questionAnswer == null)
|
|
426
464
|
: group.annotations;
|
|
427
|
-
const sorted = [...groupAnnotations].sort(
|
|
465
|
+
const sorted = [...groupAnnotations].sort(byHost);
|
|
428
466
|
const threaded = threadReplies(sorted);
|
|
429
467
|
return (
|
|
430
468
|
<section key={group.path} data-annotation-group={group.path}>
|
|
@@ -444,14 +482,13 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
444
482
|
>
|
|
445
483
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
446
484
|
</svg>
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
</span>
|
|
485
|
+
{/* The pathless root document (plan review) is keyed by a
|
|
486
|
+
sentinel, which is not something to show a reader. */}
|
|
487
|
+
<HostTitle title={group.path === ROOT_DOCUMENT_GROUP_KEY ? group.label : group.path}>
|
|
488
|
+
<span className={cn('min-w-0 flex-1 truncate font-mono text-[10px]', group.isCurrent ? 'text-foreground' : 'text-muted-foreground')}>
|
|
489
|
+
{group.label}
|
|
490
|
+
</span>
|
|
491
|
+
</HostTitle>
|
|
455
492
|
{group.isCurrent && (
|
|
456
493
|
<span className="flex-shrink-0 text-[9px] text-muted-foreground/50">open</span>
|
|
457
494
|
)}
|
|
@@ -479,6 +516,7 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
479
516
|
readOnly={readOnly}
|
|
480
517
|
footer={group.isCurrent ? renderCardFooter?.(annotation) : undefined}
|
|
481
518
|
header={group.isCurrent ? renderCardHeader?.(annotation) : undefined}
|
|
519
|
+
parts={group.isCurrent ? cardParts?.(annotation) : undefined}
|
|
482
520
|
mentionSource={mentionSource}
|
|
483
521
|
unanchored={group.isCurrent ? (unanchoredIds?.has(annotation.id) ?? false) : false}
|
|
484
522
|
/>
|
|
@@ -534,16 +572,17 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
534
572
|
</>
|
|
535
573
|
)}
|
|
536
574
|
{otherGroups.length > 0 && onOtherFileAnnotationsClick && (
|
|
537
|
-
<
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
575
|
+
<HostTitle title="Show annotated files in the navigator">
|
|
576
|
+
<button
|
|
577
|
+
type="button"
|
|
578
|
+
data-pn-touch-target="true"
|
|
579
|
+
data-annotation-show-in-files="true"
|
|
580
|
+
onClick={onOtherFileAnnotationsClick}
|
|
581
|
+
className="mt-1 cursor-pointer self-start px-1.5 text-[10px] text-muted-foreground/60 transition-colors hover:text-primary"
|
|
582
|
+
>
|
|
583
|
+
Show in files
|
|
584
|
+
</button>
|
|
585
|
+
</HostTitle>
|
|
547
586
|
)}
|
|
548
587
|
</>
|
|
549
588
|
) : timelineCount === 0 && showQuestions ? (
|
|
@@ -557,7 +596,7 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
557
596
|
No annotations yet
|
|
558
597
|
</p>
|
|
559
598
|
<p className="mt-1 text-[11px] text-muted-foreground/40">
|
|
560
|
-
|
|
599
|
+
{emptyHint}
|
|
561
600
|
</p>
|
|
562
601
|
{scopeEnabled && otherGroupCount > 0 && (
|
|
563
602
|
<button
|
|
@@ -592,6 +631,7 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
592
631
|
readOnly={readOnly}
|
|
593
632
|
footer={renderCardFooter?.(entry.annotation)}
|
|
594
633
|
header={renderCardHeader?.(entry.annotation)}
|
|
634
|
+
parts={cardParts?.(entry.annotation)}
|
|
595
635
|
mentionSource={mentionSource}
|
|
596
636
|
unanchored={unanchoredIds?.has(entry.annotation.id) ?? false}
|
|
597
637
|
/>
|
|
@@ -608,6 +648,7 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
|
608
648
|
readOnly={readOnly}
|
|
609
649
|
footer={renderCardFooter?.(entry.annotation)}
|
|
610
650
|
header={renderCardHeader?.(entry.annotation)}
|
|
651
|
+
parts={cardParts?.(entry.annotation)}
|
|
611
652
|
mentionSource={mentionSource}
|
|
612
653
|
unanchored={unanchoredIds?.has(entry.annotation.id) ?? false}
|
|
613
654
|
/>
|
|
@@ -764,9 +805,9 @@ const DirectEditsCard: React.FC<{
|
|
|
764
805
|
<div className="flex items-center gap-1.5">
|
|
765
806
|
<span className="text-[11px] font-medium text-primary">{title}</span>
|
|
766
807
|
{label && (
|
|
767
|
-
<
|
|
768
|
-
{label}
|
|
769
|
-
</
|
|
808
|
+
<HostTitle title={label} cut>
|
|
809
|
+
<span className="min-w-0 truncate text-[10px] text-muted-foreground">{label}</span>
|
|
810
|
+
</HostTitle>
|
|
770
811
|
)}
|
|
771
812
|
<span className="font-mono text-[10px] tabular-nums">
|
|
772
813
|
<span className="text-success">+{added}</span>
|
|
@@ -925,7 +966,9 @@ const AnnotationCard: React.FC<{
|
|
|
925
966
|
mentionSource?: MentionSource;
|
|
926
967
|
/** The annotation has no live location in the document (host-reported). */
|
|
927
968
|
unanchored?: boolean;
|
|
928
|
-
|
|
969
|
+
/** Built-in parts to draw (see `cardParts`). */
|
|
970
|
+
parts?: { quote?: boolean; images?: boolean };
|
|
971
|
+
}> = ({ annotation, isSelected, isMe, onSelect, onDelete, onEdit, readOnly = false, footer, header, mentionSource, unanchored = false, parts }) => {
|
|
929
972
|
const [isEditing, setIsEditing] = useState(false);
|
|
930
973
|
const [editText, setEditText] = useState(annotation.text || '');
|
|
931
974
|
|
|
@@ -1000,21 +1043,18 @@ const AnnotationCard: React.FC<{
|
|
|
1000
1043
|
</span>
|
|
1001
1044
|
)}
|
|
1002
1045
|
{annotation.pageUrl && (
|
|
1003
|
-
<
|
|
1004
|
-
className="text-[9px] px-1.5 py-0.5 rounded font-medium bg-muted text-muted-foreground truncate max-w-[10rem]"
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
</span>
|
|
1046
|
+
<HostTitle title={annotation.pageUrl}>
|
|
1047
|
+
<span className="text-[9px] px-1.5 py-0.5 rounded font-medium bg-muted text-muted-foreground truncate max-w-[10rem]">
|
|
1048
|
+
{annotation.pageUrl}
|
|
1049
|
+
</span>
|
|
1050
|
+
</HostTitle>
|
|
1009
1051
|
)}
|
|
1010
1052
|
{unanchored && (
|
|
1011
|
-
<
|
|
1012
|
-
data-annotation-unanchored="true"
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
>
|
|
1016
|
-
Unanchored
|
|
1017
|
-
</span>
|
|
1053
|
+
<HostTitle title="This comment no longer matches a location in the document">
|
|
1054
|
+
<span data-annotation-unanchored="true" className="text-[9px] px-1.5 py-0.5 rounded font-medium bg-muted text-muted-foreground">
|
|
1055
|
+
Unanchored
|
|
1056
|
+
</span>
|
|
1057
|
+
</HostTitle>
|
|
1018
1058
|
)}
|
|
1019
1059
|
<span className="text-[10px] text-muted-foreground/50 truncate">
|
|
1020
1060
|
{annotation.author ? `${annotation.author}${isMe ? ' (me)' : ''} · ` : ''}{formatTimestamp(annotation.createdA)}
|
|
@@ -1035,23 +1075,25 @@ const AnnotationCard: React.FC<{
|
|
|
1035
1075
|
{!readOnly && (
|
|
1036
1076
|
<div className="ml-auto flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 [@media(hover:none)]:opacity-100">
|
|
1037
1077
|
{onEdit && annotation.type !== AnnotationType.DELETION && !isEditing && (
|
|
1078
|
+
<HostTitle title="Edit annotation" name>
|
|
1079
|
+
<button
|
|
1080
|
+
type="button"
|
|
1081
|
+
onClick={handleStartEdit}
|
|
1082
|
+
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-foreground"
|
|
1083
|
+
>
|
|
1084
|
+
<PencilIcon />
|
|
1085
|
+
</button>
|
|
1086
|
+
</HostTitle>
|
|
1087
|
+
)}
|
|
1088
|
+
<HostTitle title="Delete annotation" name>
|
|
1038
1089
|
<button
|
|
1039
1090
|
type="button"
|
|
1040
|
-
onClick={
|
|
1041
|
-
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-
|
|
1042
|
-
title="Edit annotation"
|
|
1091
|
+
onClick={(e: React.MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); onDelete(); }}
|
|
1092
|
+
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-destructive"
|
|
1043
1093
|
>
|
|
1044
|
-
<
|
|
1094
|
+
<TrashCardIcon />
|
|
1045
1095
|
</button>
|
|
1046
|
-
|
|
1047
|
-
<button
|
|
1048
|
-
type="button"
|
|
1049
|
-
onClick={(e: React.MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); onDelete(); }}
|
|
1050
|
-
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-destructive"
|
|
1051
|
-
title="Delete annotation"
|
|
1052
|
-
>
|
|
1053
|
-
<TrashCardIcon />
|
|
1054
|
-
</button>
|
|
1096
|
+
</HostTitle>
|
|
1055
1097
|
</div>
|
|
1056
1098
|
)}
|
|
1057
1099
|
</div>
|
|
@@ -1068,9 +1110,11 @@ const AnnotationCard: React.FC<{
|
|
|
1068
1110
|
) : (
|
|
1069
1111
|
<>
|
|
1070
1112
|
{/* Quote — the annotated text */}
|
|
1071
|
-
|
|
1072
|
-
"
|
|
1073
|
-
|
|
1113
|
+
{parts?.quote !== false && (
|
|
1114
|
+
<p className="mb-1.5 line-clamp-2 whitespace-pre-wrap font-mono text-[11px] leading-relaxed text-muted-foreground/80">
|
|
1115
|
+
"{annotation.originalText}"
|
|
1116
|
+
</p>
|
|
1117
|
+
)}
|
|
1074
1118
|
|
|
1075
1119
|
{/* Comment/Replacement Text */}
|
|
1076
1120
|
{annotation.type !== AnnotationType.DELETION && (
|
|
@@ -1088,7 +1132,7 @@ const AnnotationCard: React.FC<{
|
|
|
1088
1132
|
)}
|
|
1089
1133
|
|
|
1090
1134
|
{/* Attached Images */}
|
|
1091
|
-
{annotation.images && annotation.images.length > 0 && (
|
|
1135
|
+
{parts?.images !== false && annotation.images && annotation.images.length > 0 && (
|
|
1092
1136
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
1093
1137
|
{annotation.images.map((img, idx) => (
|
|
1094
1138
|
<div key={idx} className="text-center">
|
|
@@ -1097,7 +1141,9 @@ const AnnotationCard: React.FC<{
|
|
|
1097
1141
|
size="sm"
|
|
1098
1142
|
showRemove={false}
|
|
1099
1143
|
/>
|
|
1100
|
-
<
|
|
1144
|
+
<HostTitle title={img.name} cut>
|
|
1145
|
+
<div className="text-[9px] text-muted-foreground truncate max-w-[3rem]">{img.name}</div>
|
|
1146
|
+
</HostTitle>
|
|
1101
1147
|
</div>
|
|
1102
1148
|
))}
|
|
1103
1149
|
</div>
|
|
@@ -1184,31 +1230,35 @@ const CodeAnnotationCard: React.FC<{
|
|
|
1184
1230
|
{!readOnly && (
|
|
1185
1231
|
<div className="ml-auto flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 [@media(hover:none)]:opacity-100">
|
|
1186
1232
|
{onEdit && !isEditing && (
|
|
1233
|
+
<HostTitle title="Edit annotation" name>
|
|
1234
|
+
<button
|
|
1235
|
+
type="button"
|
|
1236
|
+
onClick={(e) => { e.stopPropagation(); setIsEditing(true); }}
|
|
1237
|
+
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-foreground"
|
|
1238
|
+
>
|
|
1239
|
+
<PencilIcon />
|
|
1240
|
+
</button>
|
|
1241
|
+
</HostTitle>
|
|
1242
|
+
)}
|
|
1243
|
+
<HostTitle title="Delete annotation" name>
|
|
1187
1244
|
<button
|
|
1188
1245
|
type="button"
|
|
1189
|
-
onClick={(e) => { e.stopPropagation();
|
|
1190
|
-
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-
|
|
1191
|
-
title="Edit annotation"
|
|
1246
|
+
onClick={(e) => { e.stopPropagation(); onDelete(); }}
|
|
1247
|
+
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-destructive"
|
|
1192
1248
|
>
|
|
1193
|
-
<
|
|
1249
|
+
<TrashCardIcon />
|
|
1194
1250
|
</button>
|
|
1195
|
-
|
|
1196
|
-
<button
|
|
1197
|
-
type="button"
|
|
1198
|
-
onClick={(e) => { e.stopPropagation(); onDelete(); }}
|
|
1199
|
-
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-destructive"
|
|
1200
|
-
title="Delete annotation"
|
|
1201
|
-
>
|
|
1202
|
-
<TrashCardIcon />
|
|
1203
|
-
</button>
|
|
1251
|
+
</HostTitle>
|
|
1204
1252
|
</div>
|
|
1205
1253
|
)}
|
|
1206
1254
|
</div>
|
|
1207
1255
|
|
|
1208
1256
|
{/* File / line meta */}
|
|
1209
|
-
<
|
|
1210
|
-
|
|
1211
|
-
|
|
1257
|
+
<HostTitle title={annotation.filePath}>
|
|
1258
|
+
<div className="rounded px-2 py-1 bg-surface-1 font-mono text-[11px] text-muted-foreground truncate">
|
|
1259
|
+
{fileName} · {lineRange}
|
|
1260
|
+
</div>
|
|
1261
|
+
</HostTitle>
|
|
1212
1262
|
|
|
1213
1263
|
{annotation.originalCode && (
|
|
1214
1264
|
<p className="mt-1.5 line-clamp-2 whitespace-pre-wrap font-mono text-[11px] leading-relaxed text-muted-foreground/80">
|
|
@@ -1255,7 +1305,9 @@ const CodeAnnotationCard: React.FC<{
|
|
|
1255
1305
|
{annotation.images.map((img) => (
|
|
1256
1306
|
<div key={img.path} className="text-center">
|
|
1257
1307
|
<ImageThumbnail path={img.path} size="sm" showRemove={false} />
|
|
1258
|
-
<
|
|
1308
|
+
<HostTitle title={img.name} cut>
|
|
1309
|
+
<div className="text-[9px] text-muted-foreground truncate max-w-[3rem]">{img.name}</div>
|
|
1310
|
+
</HostTitle>
|
|
1259
1311
|
</div>
|
|
1260
1312
|
))}
|
|
1261
1313
|
</div>
|
|
@@ -8,6 +8,7 @@ import { acquireTypeToCommentCapture } from "../shortcuts/plan-review/annotation
|
|
|
8
8
|
import { FloatingQuickLabelPicker } from "./FloatingQuickLabelPicker";
|
|
9
9
|
import { FloatingSelectionActionsPicker } from "./SelectionActionsDropdown";
|
|
10
10
|
import { buildSelectionActionContext, type SelectionAction } from "../utils/selectionActions";
|
|
11
|
+
import { HostTitle } from './HostTitle';
|
|
11
12
|
|
|
12
13
|
type PositionMode = 'center-above' | 'top-right';
|
|
13
14
|
|
|
@@ -399,18 +400,20 @@ const ToolbarButton = React.forwardRef<HTMLButtonElement, {
|
|
|
399
400
|
/** Extra DOM attributes (e.g. data-selection-actions). Absent adds nothing. */
|
|
400
401
|
dataAttributes?: Record<string, string>;
|
|
401
402
|
}>(({ onClick, icon, label, className, dataAttributes }, ref) => (
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
403
|
+
// The label is the button's only name: as a component tooltip (HostTitle) it also becomes its aria-label.
|
|
404
|
+
<HostTitle title={label} name>
|
|
405
|
+
<button
|
|
406
|
+
ref={ref}
|
|
407
|
+
// Icon-only controls: the markers are inert outside the compact touch
|
|
408
|
+
// scope, where theme.css grows them to var(--pn-touch-target). Desktop
|
|
409
|
+
// geometry is unchanged.
|
|
410
|
+
data-pn-touch-target="true"
|
|
411
|
+
data-pn-touch-target-icon="true"
|
|
412
|
+
onClick={onClick}
|
|
413
|
+
className={`p-1.5 rounded-md transition-colors ${className}`}
|
|
414
|
+
{...dataAttributes}
|
|
415
|
+
>
|
|
416
|
+
{icon}
|
|
417
|
+
</button>
|
|
418
|
+
</HostTitle>
|
|
416
419
|
));
|
|
@@ -36,6 +36,12 @@ export interface AnnotationToolstripProps {
|
|
|
36
36
|
* retain the complete action-mode group.
|
|
37
37
|
*/
|
|
38
38
|
hideQuickLabel?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Omit only the Redline action. Defaults to false. For a host whose
|
|
41
|
+
* composer is its own (the Inbox surface on a phone draws Select, Pinpoint,
|
|
42
|
+
* Markup and Comment).
|
|
43
|
+
*/
|
|
44
|
+
hideRedline?: boolean;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
/** Render the shared input-method and annotation-mode controls. */
|
|
@@ -50,6 +56,7 @@ export const AnnotationToolstrip: React.FC<AnnotationToolstripProps> = ({
|
|
|
50
56
|
iconOnly = false,
|
|
51
57
|
hideInputMethodSwitch = false,
|
|
52
58
|
hideQuickLabel = false,
|
|
59
|
+
hideRedline = false,
|
|
53
60
|
}) => {
|
|
54
61
|
const [showHelp, setShowHelp] = useState(false);
|
|
55
62
|
const [helpTab, setHelpTab] = useState<'selection' | 'plannotator'>('selection');
|
|
@@ -136,6 +143,7 @@ export const AnnotationToolstrip: React.FC<AnnotationToolstripProps> = ({
|
|
|
136
143
|
</svg>
|
|
137
144
|
}
|
|
138
145
|
/>
|
|
146
|
+
{!hideRedline && (
|
|
139
147
|
<ToolstripButton
|
|
140
148
|
active={mode === 'redline'}
|
|
141
149
|
onClick={() => onModeChange('redline')}
|
|
@@ -150,6 +158,7 @@ export const AnnotationToolstrip: React.FC<AnnotationToolstripProps> = ({
|
|
|
150
158
|
</svg>
|
|
151
159
|
}
|
|
152
160
|
/>
|
|
161
|
+
)}
|
|
153
162
|
{!hideQuickLabel && (
|
|
154
163
|
<ToolstripButton
|
|
155
164
|
active={mode === 'quickLabel'}
|
|
@@ -6,6 +6,7 @@ import { CodeBlock } from "./blocks/CodeBlock";
|
|
|
6
6
|
import { HtmlBlock } from "./blocks/HtmlBlock";
|
|
7
7
|
import { Callout } from "./blocks/Callout";
|
|
8
8
|
import { AlertBlock } from "./blocks/AlertBlock";
|
|
9
|
+
import { BlockquoteBlock } from "./blocks/BlockquoteBlock";
|
|
9
10
|
import { TableBlock } from "./blocks/TableBlock";
|
|
10
11
|
import { MathBlock } from "./blocks/MathBlock";
|
|
11
12
|
import { QuestionBlock, type QuestionBlockProps } from "./blocks/QuestionBlock";
|
|
@@ -53,11 +54,19 @@ export const BlockRenderer: React.FC<{
|
|
|
53
54
|
renderQuestionFooter?: QuestionBlockProps['renderFooter'];
|
|
54
55
|
/** Whether answering this question records a decision (see `QuestionBlock`). */
|
|
55
56
|
questionDecisionRecording?: QuestionBlockProps['decisionRecording'];
|
|
57
|
+
/** `'any'` lets a question without a decision line carry the tag (see `QuestionBlock`). */
|
|
58
|
+
questionDecisionScope?: QuestionBlockProps['decisionScope'];
|
|
56
59
|
/** Makes the "Records a decision" tag a toggle (see `QuestionBlock`). */
|
|
57
60
|
onToggleQuestionDecisionRecording?: QuestionBlockProps['onToggleDecisionRecording'];
|
|
61
|
+
/** Opens the host's decision card from the tag's words (see `QuestionBlock`). */
|
|
62
|
+
onOpenQuestionDecision?: QuestionBlockProps['onOpenDecision'];
|
|
58
63
|
/** `'none'` hides the card's own status tag (see `QuestionBlock`). */
|
|
59
64
|
questionStatusTag?: QuestionBlockProps['statusTag'];
|
|
60
|
-
|
|
65
|
+
/** The host's words for a question's status tag (see `QuestionBlock`). */
|
|
66
|
+
questionStatusLabel?: QuestionBlockProps['statusLabel'];
|
|
67
|
+
/** Host content under a question's prompt (see `QuestionBlock`). */
|
|
68
|
+
renderQuestionBelowPrompt?: QuestionBlockProps['renderBelowPrompt'];
|
|
69
|
+
}> = ({ block, onOpenLinkedDoc, onOpenCodeFile, imageBaseDir, onImageClick, onToggleCheckbox, checkboxOverrides, orderedIndex, githubRepo, repoHost, headingAnchorId, onNavigateAnchor, question, questionTotal, questionAnswer, onAnswerQuestion, onSaveQuestionAnswer, saveQuestionAnswerLabel, renderQuestionFooter, questionDecisionRecording, questionDecisionScope, onToggleQuestionDecisionRecording, onOpenQuestionDecision, questionStatusTag, questionStatusLabel, renderQuestionBelowPrompt }) => {
|
|
61
70
|
switch (block.type) {
|
|
62
71
|
case 'heading': {
|
|
63
72
|
const Tag = `h${block.level || 1}` as React.ElementType;
|
|
@@ -95,20 +104,19 @@ export const BlockRenderer: React.FC<{
|
|
|
95
104
|
/>
|
|
96
105
|
);
|
|
97
106
|
}
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
const paragraphs = block.content.split(/\n\n+/);
|
|
107
|
+
// One quote whatever it holds: its content is the quote's own markdown
|
|
108
|
+
// (paragraphs, code fences, lists, nested quotes), see BlockquoteBlock.
|
|
101
109
|
return (
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
{
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
<BlockquoteBlock
|
|
111
|
+
block={block}
|
|
112
|
+
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
113
|
+
onOpenCodeFile={onOpenCodeFile}
|
|
114
|
+
imageBaseDir={imageBaseDir}
|
|
115
|
+
onImageClick={onImageClick}
|
|
116
|
+
githubRepo={githubRepo}
|
|
117
|
+
repoHost={repoHost}
|
|
118
|
+
onNavigateAnchor={onNavigateAnchor}
|
|
119
|
+
/>
|
|
112
120
|
);
|
|
113
121
|
}
|
|
114
122
|
|
|
@@ -185,8 +193,12 @@ export const BlockRenderer: React.FC<{
|
|
|
185
193
|
saveLabel={saveQuestionAnswerLabel}
|
|
186
194
|
renderFooter={renderQuestionFooter}
|
|
187
195
|
decisionRecording={questionDecisionRecording}
|
|
196
|
+
decisionScope={questionDecisionScope}
|
|
188
197
|
onToggleDecisionRecording={onToggleQuestionDecisionRecording}
|
|
198
|
+
onOpenDecision={onOpenQuestionDecision}
|
|
189
199
|
statusTag={questionStatusTag}
|
|
200
|
+
statusLabel={questionStatusLabel}
|
|
201
|
+
renderBelowPrompt={renderQuestionBelowPrompt}
|
|
190
202
|
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
191
203
|
onOpenCodeFile={onOpenCodeFile}
|
|
192
204
|
imageBaseDir={imageBaseDir}
|