@plannotator/ui 0.52.1 → 0.53.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/HANDOFF.md +284 -29
- package/README.md +45 -5
- package/components/AnnotationPanel.tsx +195 -118
- package/components/AnnotationToolbar.tsx +17 -14
- package/components/AnnotationToolstrip.tsx +54 -40
- package/components/BlockRenderer.tsx +20 -14
- package/components/CodeFilePopout.tsx +132 -69
- package/components/CommentPopover.tsx +8 -349
- package/components/ComposerTokenTextarea.tsx +379 -0
- package/components/ExportModal.tsx +6 -1
- package/components/HostTitle.tsx +108 -0
- package/components/HtmlSurfaceControls.tsx +2 -1
- 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/MentionPicker.tsx +51 -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 +67 -18
- package/components/Tooltip.tsx +51 -4
- package/components/Viewer.tsx +220 -49
- 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 +183 -96
- package/components/blocks/QuestionContext.tsx +3 -0
- package/components/diagram/DiagramViewer.tsx +41 -4
- package/components/diagram/index.ts +1 -1
- package/components/html-viewer/HtmlViewer.tsx +95 -3
- package/components/html-viewer/bridge-script.asset.js +413 -7
- package/components/html-viewer/bridge-script.lite.ts +1 -1
- package/components/html-viewer/bridge-script.ts +454 -7
- 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/useMentionAutocomplete.ts +1 -1
- package/hooks/useOpenInApps.ts +88 -0
- package/hooks/useSkillReferenceAutocomplete.ts +7 -3
- package/hooks/useUpdateCheck.ts +38 -98
- package/hooks/useUpdateCheckSetting.ts +43 -0
- package/hooks/useUpdateToast.ts +101 -0
- package/hooks/useVimSelection.ts +9 -0
- package/package.json +2 -2
- package/shortcuts/code-review/markdownPreview.shortcuts.ts +18 -0
- package/shortcuts/index.ts +1 -0
- package/shortcuts/runtime.ts +10 -3
- 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/mentions.ts +34 -5
- 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
|
/>
|
|
@@ -725,9 +766,38 @@ function formatTimestamp(ts: number): string {
|
|
|
725
766
|
if (hours < 24) return `${hours}h`;
|
|
726
767
|
if (days < 7) return `${days}d`;
|
|
727
768
|
|
|
728
|
-
|
|
769
|
+
// An older date names its year: "Oct 20" alone reads as this October.
|
|
770
|
+
const date = new Date(ts);
|
|
771
|
+
const thisYear = date.getFullYear() === new Date(now).getFullYear();
|
|
772
|
+
return date.toLocaleDateString(
|
|
773
|
+
undefined,
|
|
774
|
+
thisYear ? { month: 'short', day: 'numeric' } : { month: 'short', day: 'numeric', year: 'numeric' },
|
|
775
|
+
);
|
|
729
776
|
}
|
|
730
777
|
|
|
778
|
+
/**
|
|
779
|
+
* A card's "author · time" line. The author truncates (its full name in the
|
|
780
|
+
* title) while the time never shrinks, so a long name cannot push the time
|
|
781
|
+
* out; `<bdi>` keeps a right-to-left name from reordering the time around it.
|
|
782
|
+
* The text reads exactly as before: "Alice (me) · 5m".
|
|
783
|
+
*/
|
|
784
|
+
const CardByline: React.FC<{ author?: string; isMe: boolean; timestamp: number }> = ({ author, isMe, timestamp }) => (
|
|
785
|
+
<span data-annotation-byline="true" className="flex min-w-0 items-center text-[10px] text-muted-foreground/50">
|
|
786
|
+
{author ? (
|
|
787
|
+
<>
|
|
788
|
+
<HostTitle title={author} cut>
|
|
789
|
+
<span data-annotation-author="true" className="min-w-0 truncate">
|
|
790
|
+
<bdi>{author}</bdi>
|
|
791
|
+
</span>
|
|
792
|
+
</HostTitle>
|
|
793
|
+
{isMe ? <span data-annotation-me="true" className="shrink-0 whitespace-pre"> (me)</span> : null}
|
|
794
|
+
<span className="shrink-0 whitespace-pre"> · </span>
|
|
795
|
+
</>
|
|
796
|
+
) : null}
|
|
797
|
+
<span data-annotation-time="true" className="shrink-0">{formatTimestamp(timestamp)}</span>
|
|
798
|
+
</span>
|
|
799
|
+
);
|
|
800
|
+
|
|
731
801
|
/** Pinned card for committed direct edits: +N/−M summary, expandable unified
|
|
732
802
|
* diff, and a two-step discard. Not part of the annotation timeline — edits
|
|
733
803
|
* are document state, not a selection-anchored note. */
|
|
@@ -764,9 +834,9 @@ const DirectEditsCard: React.FC<{
|
|
|
764
834
|
<div className="flex items-center gap-1.5">
|
|
765
835
|
<span className="text-[11px] font-medium text-primary">{title}</span>
|
|
766
836
|
{label && (
|
|
767
|
-
<
|
|
768
|
-
{label}
|
|
769
|
-
</
|
|
837
|
+
<HostTitle title={label} cut>
|
|
838
|
+
<span className="min-w-0 truncate text-[10px] text-muted-foreground">{label}</span>
|
|
839
|
+
</HostTitle>
|
|
770
840
|
)}
|
|
771
841
|
<span className="font-mono text-[10px] tabular-nums">
|
|
772
842
|
<span className="text-success">+{added}</span>
|
|
@@ -925,7 +995,9 @@ const AnnotationCard: React.FC<{
|
|
|
925
995
|
mentionSource?: MentionSource;
|
|
926
996
|
/** The annotation has no live location in the document (host-reported). */
|
|
927
997
|
unanchored?: boolean;
|
|
928
|
-
|
|
998
|
+
/** Built-in parts to draw (see `cardParts`). */
|
|
999
|
+
parts?: { quote?: boolean; images?: boolean };
|
|
1000
|
+
}> = ({ annotation, isSelected, isMe, onSelect, onDelete, onEdit, readOnly = false, footer, header, mentionSource, unanchored = false, parts }) => {
|
|
929
1001
|
const [isEditing, setIsEditing] = useState(false);
|
|
930
1002
|
const [editText, setEditText] = useState(annotation.text || '');
|
|
931
1003
|
|
|
@@ -1000,25 +1072,20 @@ const AnnotationCard: React.FC<{
|
|
|
1000
1072
|
</span>
|
|
1001
1073
|
)}
|
|
1002
1074
|
{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>
|
|
1075
|
+
<HostTitle title={annotation.pageUrl}>
|
|
1076
|
+
<span className="text-[9px] px-1.5 py-0.5 rounded font-medium bg-muted text-muted-foreground truncate max-w-[10rem]">
|
|
1077
|
+
{annotation.pageUrl}
|
|
1078
|
+
</span>
|
|
1079
|
+
</HostTitle>
|
|
1009
1080
|
)}
|
|
1010
1081
|
{unanchored && (
|
|
1011
|
-
<
|
|
1012
|
-
data-annotation-unanchored="true"
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
>
|
|
1016
|
-
Unanchored
|
|
1017
|
-
</span>
|
|
1082
|
+
<HostTitle title="This comment no longer matches a location in the document">
|
|
1083
|
+
<span data-annotation-unanchored="true" className="text-[9px] px-1.5 py-0.5 rounded font-medium bg-muted text-muted-foreground">
|
|
1084
|
+
Unanchored
|
|
1085
|
+
</span>
|
|
1086
|
+
</HostTitle>
|
|
1018
1087
|
)}
|
|
1019
|
-
<
|
|
1020
|
-
{annotation.author ? `${annotation.author}${isMe ? ' (me)' : ''} · ` : ''}{formatTimestamp(annotation.createdA)}
|
|
1021
|
-
</span>
|
|
1088
|
+
<CardByline author={annotation.author} isMe={isMe} timestamp={annotation.createdA} />
|
|
1022
1089
|
{/* Host header slot (a status stamp etc.) — interactions inside it
|
|
1023
1090
|
must not toggle card selection, exactly like the footer slot.
|
|
1024
1091
|
Rendered under readOnly too: its contents are host-owned. */}
|
|
@@ -1035,23 +1102,25 @@ const AnnotationCard: React.FC<{
|
|
|
1035
1102
|
{!readOnly && (
|
|
1036
1103
|
<div className="ml-auto flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 [@media(hover:none)]:opacity-100">
|
|
1037
1104
|
{onEdit && annotation.type !== AnnotationType.DELETION && !isEditing && (
|
|
1105
|
+
<HostTitle title="Edit annotation" name>
|
|
1106
|
+
<button
|
|
1107
|
+
type="button"
|
|
1108
|
+
onClick={handleStartEdit}
|
|
1109
|
+
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-foreground"
|
|
1110
|
+
>
|
|
1111
|
+
<PencilIcon />
|
|
1112
|
+
</button>
|
|
1113
|
+
</HostTitle>
|
|
1114
|
+
)}
|
|
1115
|
+
<HostTitle title="Delete annotation" name>
|
|
1038
1116
|
<button
|
|
1039
1117
|
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"
|
|
1118
|
+
onClick={(e: React.MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); onDelete(); }}
|
|
1119
|
+
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-destructive"
|
|
1043
1120
|
>
|
|
1044
|
-
<
|
|
1121
|
+
<TrashCardIcon />
|
|
1045
1122
|
</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>
|
|
1123
|
+
</HostTitle>
|
|
1055
1124
|
</div>
|
|
1056
1125
|
)}
|
|
1057
1126
|
</div>
|
|
@@ -1061,16 +1130,18 @@ const AnnotationCard: React.FC<{
|
|
|
1061
1130
|
isEditing ? (
|
|
1062
1131
|
editComposer
|
|
1063
1132
|
) : (
|
|
1064
|
-
<p className="whitespace-pre-wrap text-[13px] leading-relaxed text-foreground/90">
|
|
1133
|
+
<p className="whitespace-pre-wrap [overflow-wrap:anywhere] text-[13px] leading-relaxed text-foreground/90">
|
|
1065
1134
|
{annotation.text}
|
|
1066
1135
|
</p>
|
|
1067
1136
|
)
|
|
1068
1137
|
) : (
|
|
1069
1138
|
<>
|
|
1070
1139
|
{/* Quote — the annotated text */}
|
|
1071
|
-
|
|
1072
|
-
"
|
|
1073
|
-
|
|
1140
|
+
{parts?.quote !== false && (
|
|
1141
|
+
<p className="mb-1.5 line-clamp-2 whitespace-pre-wrap [overflow-wrap:anywhere] font-mono text-[11px] leading-relaxed text-muted-foreground/80">
|
|
1142
|
+
"{annotation.originalText}"
|
|
1143
|
+
</p>
|
|
1144
|
+
)}
|
|
1074
1145
|
|
|
1075
1146
|
{/* Comment/Replacement Text */}
|
|
1076
1147
|
{annotation.type !== AnnotationType.DELETION && (
|
|
@@ -1078,7 +1149,7 @@ const AnnotationCard: React.FC<{
|
|
|
1078
1149
|
editComposer
|
|
1079
1150
|
) : (
|
|
1080
1151
|
annotation.text && (
|
|
1081
|
-
<p className="whitespace-pre-wrap text-[13px] leading-relaxed text-foreground/90">
|
|
1152
|
+
<p className="whitespace-pre-wrap [overflow-wrap:anywhere] text-[13px] leading-relaxed text-foreground/90">
|
|
1082
1153
|
{annotation.text}
|
|
1083
1154
|
</p>
|
|
1084
1155
|
)
|
|
@@ -1088,7 +1159,7 @@ const AnnotationCard: React.FC<{
|
|
|
1088
1159
|
)}
|
|
1089
1160
|
|
|
1090
1161
|
{/* Attached Images */}
|
|
1091
|
-
{annotation.images && annotation.images.length > 0 && (
|
|
1162
|
+
{parts?.images !== false && annotation.images && annotation.images.length > 0 && (
|
|
1092
1163
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
1093
1164
|
{annotation.images.map((img, idx) => (
|
|
1094
1165
|
<div key={idx} className="text-center">
|
|
@@ -1097,7 +1168,9 @@ const AnnotationCard: React.FC<{
|
|
|
1097
1168
|
size="sm"
|
|
1098
1169
|
showRemove={false}
|
|
1099
1170
|
/>
|
|
1100
|
-
<
|
|
1171
|
+
<HostTitle title={img.name} cut>
|
|
1172
|
+
<div className="text-[9px] text-muted-foreground truncate max-w-[3rem]">{img.name}</div>
|
|
1173
|
+
</HostTitle>
|
|
1101
1174
|
</div>
|
|
1102
1175
|
))}
|
|
1103
1176
|
</div>
|
|
@@ -1178,40 +1251,42 @@ const CodeAnnotationCard: React.FC<{
|
|
|
1178
1251
|
{/* Header: type word + author · time + actions */}
|
|
1179
1252
|
<div className="mb-1.5 flex items-center gap-1.5">
|
|
1180
1253
|
<span className="text-[11px] font-medium text-primary">Code</span>
|
|
1181
|
-
<
|
|
1182
|
-
{annotation.author ? `${annotation.author}${isMe ? ' (me)' : ''} · ` : ''}{formatTimestamp(annotation.createdAt)}
|
|
1183
|
-
</span>
|
|
1254
|
+
<CardByline author={annotation.author} isMe={isMe} timestamp={annotation.createdAt} />
|
|
1184
1255
|
{!readOnly && (
|
|
1185
1256
|
<div className="ml-auto flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 [@media(hover:none)]:opacity-100">
|
|
1186
1257
|
{onEdit && !isEditing && (
|
|
1258
|
+
<HostTitle title="Edit annotation" name>
|
|
1259
|
+
<button
|
|
1260
|
+
type="button"
|
|
1261
|
+
onClick={(e) => { e.stopPropagation(); setIsEditing(true); }}
|
|
1262
|
+
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-foreground"
|
|
1263
|
+
>
|
|
1264
|
+
<PencilIcon />
|
|
1265
|
+
</button>
|
|
1266
|
+
</HostTitle>
|
|
1267
|
+
)}
|
|
1268
|
+
<HostTitle title="Delete annotation" name>
|
|
1187
1269
|
<button
|
|
1188
1270
|
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"
|
|
1271
|
+
onClick={(e) => { e.stopPropagation(); onDelete(); }}
|
|
1272
|
+
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-destructive"
|
|
1192
1273
|
>
|
|
1193
|
-
<
|
|
1274
|
+
<TrashCardIcon />
|
|
1194
1275
|
</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>
|
|
1276
|
+
</HostTitle>
|
|
1204
1277
|
</div>
|
|
1205
1278
|
)}
|
|
1206
1279
|
</div>
|
|
1207
1280
|
|
|
1208
1281
|
{/* File / line meta */}
|
|
1209
|
-
<
|
|
1210
|
-
|
|
1211
|
-
|
|
1282
|
+
<HostTitle title={annotation.filePath}>
|
|
1283
|
+
<div className="rounded px-2 py-1 bg-surface-1 font-mono text-[11px] text-muted-foreground truncate">
|
|
1284
|
+
{fileName} · {lineRange}
|
|
1285
|
+
</div>
|
|
1286
|
+
</HostTitle>
|
|
1212
1287
|
|
|
1213
1288
|
{annotation.originalCode && (
|
|
1214
|
-
<p className="mt-1.5 line-clamp-2 whitespace-pre-wrap font-mono text-[11px] leading-relaxed text-muted-foreground/80">
|
|
1289
|
+
<p className="mt-1.5 line-clamp-2 whitespace-pre-wrap [overflow-wrap:anywhere] font-mono text-[11px] leading-relaxed text-muted-foreground/80">
|
|
1215
1290
|
{annotation.originalCode}
|
|
1216
1291
|
</p>
|
|
1217
1292
|
)}
|
|
@@ -1244,7 +1319,7 @@ const CodeAnnotationCard: React.FC<{
|
|
|
1244
1319
|
</div>
|
|
1245
1320
|
) : (
|
|
1246
1321
|
annotation.text && (
|
|
1247
|
-
<p className="mt-1.5 whitespace-pre-wrap text-[13px] leading-relaxed text-foreground/90">
|
|
1322
|
+
<p className="mt-1.5 whitespace-pre-wrap [overflow-wrap:anywhere] text-[13px] leading-relaxed text-foreground/90">
|
|
1248
1323
|
{annotation.text}
|
|
1249
1324
|
</p>
|
|
1250
1325
|
)
|
|
@@ -1255,7 +1330,9 @@ const CodeAnnotationCard: React.FC<{
|
|
|
1255
1330
|
{annotation.images.map((img) => (
|
|
1256
1331
|
<div key={img.path} className="text-center">
|
|
1257
1332
|
<ImageThumbnail path={img.path} size="sm" showRemove={false} />
|
|
1258
|
-
<
|
|
1333
|
+
<HostTitle title={img.name} cut>
|
|
1334
|
+
<div className="text-[9px] text-muted-foreground truncate max-w-[3rem]">{img.name}</div>
|
|
1335
|
+
</HostTitle>
|
|
1259
1336
|
</div>
|
|
1260
1337
|
))}
|
|
1261
1338
|
</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
|
));
|