@plannotator/ui 0.38.2 → 0.40.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 +99 -9
- package/README.md +4 -4
- package/components/AnnotationPanel.tsx +244 -13
- package/components/CommentPopover.tsx +91 -2
- package/components/DecisionControl.tsx +0 -10
- package/components/HtmlSurfaceControls.tsx +188 -23
- package/components/ListMarker.tsx +10 -1
- package/components/MermaidBlock.tsx +39 -5
- package/components/TableOfContents.tsx +5 -1
- package/components/TerminalToolsAnnouncementDialog.tsx +454 -0
- package/components/Viewer.tsx +25 -1
- package/components/blocks/AlertBlock.tsx +7 -2
- package/components/html-viewer/HtmlViewer.tsx +32 -0
- package/components/html-viewer/bridge-script.asset.js +533 -9
- package/components/html-viewer/bridge-script.ts +533 -9
- package/components/html-viewer/useHtmlAnnotation.ts +71 -4
- package/hooks/useAnnotationHighlighter.ts +464 -14
- package/hooks/useLinkedDoc.ts +100 -9
- package/package.json +3 -3
- package/shortcuts/plan-review/htmlAnnotate.shortcuts.ts +21 -7
- package/styles.css +1 -1
- package/theme.css +62 -0
- package/types.ts +45 -0
- package/utils/annotationScope.ts +159 -0
- package/utils/cssColor.ts +463 -0
- package/utils/decisionSpec.ts +1 -4
- package/utils/htmlChrome.ts +70 -5
- package/utils/htmlLinkNavigation.ts +196 -0
- package/utils/mermaid-eager.ts +13 -11
- package/utils/mermaid.ts +19 -10
- package/utils/mermaidTheme.ts +732 -0
- package/utils/parser.ts +150 -4
- package/utils/terminalToolsAnnouncement.ts +76 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useRef, type RefObject } from "react";
|
|
2
|
-
import { AnnotationType, type Annotation, type EditorMode, type HtmlAnnotationTarget, type HtmlElementAnchor, type ImageAttachment } from "../../types";
|
|
2
|
+
import { AnnotationType, type Annotation, type EditorMode, type HtmlAnnotationTarget, type HtmlElementAnchor, type HtmlElementContext, type ImageAttachment } from "../../types";
|
|
3
3
|
import { THUMBS_UP_LABEL, type QuickLabel } from "../../utils/quickLabels";
|
|
4
4
|
import { getIdentity } from "../../utils/identity";
|
|
5
5
|
import type {
|
|
@@ -9,6 +9,11 @@ import type {
|
|
|
9
9
|
UseAnnotationHighlighterReturn,
|
|
10
10
|
} from "../../hooks/useAnnotationHighlighter";
|
|
11
11
|
import { BRIDGE_PROTOCOL_VERSION } from "./bridge-script";
|
|
12
|
+
import {
|
|
13
|
+
parseHtmlElementContext,
|
|
14
|
+
MAX_ELEMENT_CONTEXT_BYTES,
|
|
15
|
+
MAX_PAGE_URL_LENGTH,
|
|
16
|
+
} from "@plannotator/core/html-anchor";
|
|
12
17
|
|
|
13
18
|
const PREFIX = "plannotator-bridge-";
|
|
14
19
|
|
|
@@ -83,6 +88,8 @@ interface BridgeSelectionMessage {
|
|
|
83
88
|
targetKey?: string;
|
|
84
89
|
/** Semantic label from the pinpoint hover cascade (chips + export). */
|
|
85
90
|
targetLabel?: string;
|
|
91
|
+
/** Agent-facing element description (pinpoint clicks) — validated, size-capped. */
|
|
92
|
+
context?: HtmlElementContext;
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
/** One draft target in an in-flight multi-select comment. Index 0 is primary. */
|
|
@@ -91,6 +98,7 @@ export interface HtmlDraftTarget {
|
|
|
91
98
|
label?: string;
|
|
92
99
|
text: string;
|
|
93
100
|
anchor: HtmlElementAnchor | null;
|
|
101
|
+
context?: HtmlElementContext;
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
interface BridgeMultiTargetAddedMessage {
|
|
@@ -99,6 +107,7 @@ interface BridgeMultiTargetAddedMessage {
|
|
|
99
107
|
label?: string;
|
|
100
108
|
text: string;
|
|
101
109
|
anchor?: HtmlElementAnchor;
|
|
110
|
+
context?: HtmlElementContext;
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
interface BridgeRect {
|
|
@@ -119,7 +128,8 @@ type BridgeMessage =
|
|
|
119
128
|
| { type: `${typeof PREFIX}mark-click`; id: string }
|
|
120
129
|
| { type: `${typeof PREFIX}unanchored`; ids: string[] }
|
|
121
130
|
| { type: `${typeof PREFIX}resize`; height: number }
|
|
122
|
-
| { type: `${typeof PREFIX}page-change`; pageUrl: string }
|
|
131
|
+
| { type: `${typeof PREFIX}page-change`; pageUrl: string }
|
|
132
|
+
| { type: `${typeof PREFIX}link-click`; href: string };
|
|
123
133
|
|
|
124
134
|
/** Live proxied-app session credentials: the proxy origin messages must come
|
|
125
135
|
* from, and the per-session token every message must echo. */
|
|
@@ -128,8 +138,10 @@ export interface HtmlLiveSession {
|
|
|
128
138
|
token: string;
|
|
129
139
|
}
|
|
130
140
|
|
|
131
|
-
/** Cap for
|
|
132
|
-
|
|
141
|
+
/** Cap for a link href relayed out of the framed document. */
|
|
142
|
+
const MAX_LINK_HREF_LENGTH = 2048;
|
|
143
|
+
/** Control characters never appear in a real href; they are how structure gets smuggled. */
|
|
144
|
+
const LINK_CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
133
145
|
|
|
134
146
|
/** True when a live-session message event fails the origin or token check.
|
|
135
147
|
* Exported for protocol tests. */
|
|
@@ -178,6 +190,12 @@ export interface UseHtmlAnnotationOptions {
|
|
|
178
190
|
* the bridge on arm-multi-select so the in-page toggle stops at the
|
|
179
191
|
* same number. Absent: the package's 16, and the arm message is unchanged. */
|
|
180
192
|
maxAdditionalTargets?: number;
|
|
193
|
+
/** A link the framed document swallowed rather than navigating to. The raw
|
|
194
|
+
* href, already bounded and screened; the host resolves it (see
|
|
195
|
+
* `resolveHtmlLinkIntent`). Delivered in readOnly mode too — navigating is
|
|
196
|
+
* a read action — and never fired in live sessions, which navigate the
|
|
197
|
+
* proxied app for real. */
|
|
198
|
+
onLinkClick?: (href: string) => void;
|
|
181
199
|
/** scrollIntoView behavior for scroll-to (selecting an annotation).
|
|
182
200
|
* Absent: smooth, as before; pass 'auto' to honor reduced motion. */
|
|
183
201
|
scrollBehavior?: 'smooth' | 'auto';
|
|
@@ -310,6 +328,15 @@ function parseTargetLabel(value: unknown): string | undefined {
|
|
|
310
328
|
: collapsed;
|
|
311
329
|
}
|
|
312
330
|
|
|
331
|
+
// Re-exported so `components/html-viewer` stays the one import site a host
|
|
332
|
+
// needs for the parent trust boundary; the definitions live in
|
|
333
|
+
// `@plannotator/core/html-anchor`, never mirrored here.
|
|
334
|
+
export {
|
|
335
|
+
parseHtmlElementContext,
|
|
336
|
+
MAX_ELEMENT_CONTEXT_BYTES,
|
|
337
|
+
MAX_PAGE_URL_LENGTH,
|
|
338
|
+
};
|
|
339
|
+
|
|
313
340
|
function parseBridgeRect(value: unknown): BridgeRect | null {
|
|
314
341
|
if (!isRecord(value)) return null;
|
|
315
342
|
const { top, left, width, height } = value;
|
|
@@ -338,6 +365,7 @@ export function parseBridgeMessage(value: unknown): BridgeMessage | null {
|
|
|
338
365
|
pinpoint: value.pinpoint === true,
|
|
339
366
|
targetKey: parseTargetKey(value.targetKey) ?? undefined,
|
|
340
367
|
targetLabel: parseTargetLabel(value.targetLabel),
|
|
368
|
+
context: parseHtmlElementContext(value.context),
|
|
341
369
|
};
|
|
342
370
|
}
|
|
343
371
|
case `${PREFIX}multi-target-added`: {
|
|
@@ -349,6 +377,7 @@ export function parseBridgeMessage(value: unknown): BridgeMessage | null {
|
|
|
349
377
|
label: parseTargetLabel(value.label),
|
|
350
378
|
text: capSelectionText(value.text),
|
|
351
379
|
anchor: parseHtmlElementAnchor(value.anchor) ?? undefined,
|
|
380
|
+
context: parseHtmlElementContext(value.context),
|
|
352
381
|
};
|
|
353
382
|
}
|
|
354
383
|
case `${PREFIX}multi-target-removed`: {
|
|
@@ -393,6 +422,16 @@ export function parseBridgeMessage(value: unknown): BridgeMessage | null {
|
|
|
393
422
|
return typeof value.height === "number" && Number.isFinite(value.height)
|
|
394
423
|
? { type: value.type, height: value.height }
|
|
395
424
|
: null;
|
|
425
|
+
case `${PREFIX}link-click`: {
|
|
426
|
+
// The raw href of a link the framed document just swallowed. It is
|
|
427
|
+
// page-controlled text, so it is bounded and screened here — the trust
|
|
428
|
+
// boundary — before the host resolves it into a path or a URL.
|
|
429
|
+
if (typeof value.href !== "string") return null;
|
|
430
|
+
const href = value.href.trim();
|
|
431
|
+
if (!href || href.length > MAX_LINK_HREF_LENGTH) return null;
|
|
432
|
+
if (LINK_CONTROL_CHARS.test(href)) return null;
|
|
433
|
+
return { type: value.type, href };
|
|
434
|
+
}
|
|
396
435
|
case `${PREFIX}page-change`:
|
|
397
436
|
// Live-mode SPA navigation report. Bounded like every bridge string.
|
|
398
437
|
return typeof value.pageUrl === "string"
|
|
@@ -421,6 +460,7 @@ export function useHtmlAnnotation({
|
|
|
421
460
|
onResize,
|
|
422
461
|
live,
|
|
423
462
|
onPageChange,
|
|
463
|
+
onLinkClick,
|
|
424
464
|
onBridgePointer,
|
|
425
465
|
onUnanchoredChange,
|
|
426
466
|
maxAdditionalTargets,
|
|
@@ -455,6 +495,7 @@ export function useHtmlAnnotation({
|
|
|
455
495
|
// Element anchor for the pending pinpoint selection — committed onto the
|
|
456
496
|
// annotation so restoration can resolve the exact element again.
|
|
457
497
|
const pendingAnchorRef = useRef<HtmlElementAnchor | null>(null);
|
|
498
|
+
const pendingContextRef = useRef<HtmlElementContext | null>(null);
|
|
458
499
|
const draftTargetsRef = useRef<HtmlDraftTarget[]>(draftTargets);
|
|
459
500
|
draftTargetsRef.current = draftTargets;
|
|
460
501
|
const onBridgePointerRef = useRef(onBridgePointer);
|
|
@@ -493,6 +534,8 @@ export function useHtmlAnnotation({
|
|
|
493
534
|
liveRef.current = live ?? null;
|
|
494
535
|
const onPageChangeRef = useRef(onPageChange);
|
|
495
536
|
onPageChangeRef.current = onPageChange;
|
|
537
|
+
const onLinkClickRef = useRef(onLinkClick);
|
|
538
|
+
onLinkClickRef.current = onLinkClick;
|
|
496
539
|
// The effective cap and whether the host set one: only an explicit cap
|
|
497
540
|
// rides on arm-multi-select, so an unconfigured viewer posts today's message.
|
|
498
541
|
const maxTargetsRef = useRef(resolveMaxAdditionalTargets(maxAdditionalTargets));
|
|
@@ -534,6 +577,7 @@ export function useHtmlAnnotation({
|
|
|
534
577
|
setCommentPopover(null);
|
|
535
578
|
pendingTextRef.current = "";
|
|
536
579
|
pendingAnchorRef.current = null;
|
|
580
|
+
pendingContextRef.current = null;
|
|
537
581
|
return;
|
|
538
582
|
}
|
|
539
583
|
if (index === 0) {
|
|
@@ -542,6 +586,7 @@ export function useHtmlAnnotation({
|
|
|
542
586
|
const next = remaining[0]!;
|
|
543
587
|
pendingTextRef.current = next.text;
|
|
544
588
|
pendingAnchorRef.current = next.anchor;
|
|
589
|
+
pendingContextRef.current = next.context ?? null;
|
|
545
590
|
setCommentPopover((prev) =>
|
|
546
591
|
prev ? { ...prev, contextText: next.text, selectedText: next.text } : prev,
|
|
547
592
|
);
|
|
@@ -603,6 +648,8 @@ export function useHtmlAnnotation({
|
|
|
603
648
|
&& type !== `${PREFIX}resize`
|
|
604
649
|
// Page identity is navigation state, not an annotation mutation.
|
|
605
650
|
&& type !== `${PREFIX}page-change`
|
|
651
|
+
// Following a link is a read action; a read-only document still navigates.
|
|
652
|
+
&& type !== `${PREFIX}link-click`
|
|
606
653
|
) {
|
|
607
654
|
return;
|
|
608
655
|
}
|
|
@@ -610,6 +657,7 @@ export function useHtmlAnnotation({
|
|
|
610
657
|
if (type === `${PREFIX}selection`) {
|
|
611
658
|
pendingTextRef.current = message.text;
|
|
612
659
|
pendingAnchorRef.current = message.anchor ?? null;
|
|
660
|
+
pendingContextRef.current = message.context ?? null;
|
|
613
661
|
setDraftTargets([]); // a new selection always starts a fresh draft
|
|
614
662
|
const anchor = positionAnchor(message.rect);
|
|
615
663
|
if (!anchor) return;
|
|
@@ -654,6 +702,7 @@ export function useHtmlAnnotation({
|
|
|
654
702
|
label: message.targetLabel,
|
|
655
703
|
text: message.text,
|
|
656
704
|
anchor: message.anchor ?? null,
|
|
705
|
+
context: message.context,
|
|
657
706
|
},
|
|
658
707
|
]);
|
|
659
708
|
post({
|
|
@@ -689,6 +738,7 @@ export function useHtmlAnnotation({
|
|
|
689
738
|
label: message.label,
|
|
690
739
|
text: message.text,
|
|
691
740
|
anchor: message.anchor ?? null,
|
|
741
|
+
context: message.context,
|
|
692
742
|
},
|
|
693
743
|
]);
|
|
694
744
|
setComposerFocusToken((t) => t + 1);
|
|
@@ -711,6 +761,7 @@ export function useHtmlAnnotation({
|
|
|
711
761
|
if (!commentPopoverRef.current && !quickLabelPickerRef.current) {
|
|
712
762
|
pendingTextRef.current = "";
|
|
713
763
|
pendingAnchorRef.current = null;
|
|
764
|
+
pendingContextRef.current = null;
|
|
714
765
|
}
|
|
715
766
|
}
|
|
716
767
|
|
|
@@ -765,6 +816,10 @@ export function useHtmlAnnotation({
|
|
|
765
816
|
if (type === `${PREFIX}page-change`) {
|
|
766
817
|
onPageChangeRef.current?.(message.pageUrl);
|
|
767
818
|
}
|
|
819
|
+
|
|
820
|
+
if (type === `${PREFIX}link-click`) {
|
|
821
|
+
onLinkClickRef.current?.(message.href);
|
|
822
|
+
}
|
|
768
823
|
}
|
|
769
824
|
|
|
770
825
|
window.addEventListener("message", handler);
|
|
@@ -826,11 +881,13 @@ export function useHtmlAnnotation({
|
|
|
826
881
|
author: getIdentity(),
|
|
827
882
|
createdA: Date.now(),
|
|
828
883
|
htmlAnchor: pendingAnchorRef.current ?? undefined,
|
|
884
|
+
elementContext: pendingContextRef.current ?? undefined,
|
|
829
885
|
});
|
|
830
886
|
|
|
831
887
|
setToolbarState(null);
|
|
832
888
|
pendingTextRef.current = "";
|
|
833
889
|
pendingAnchorRef.current = null;
|
|
890
|
+
pendingContextRef.current = null;
|
|
834
891
|
},
|
|
835
892
|
[post],
|
|
836
893
|
);
|
|
@@ -869,6 +926,7 @@ export function useHtmlAnnotation({
|
|
|
869
926
|
label: t.label,
|
|
870
927
|
text: t.text,
|
|
871
928
|
anchor: t.anchor ?? undefined,
|
|
929
|
+
...(t.context ? { context: t.context } : {}),
|
|
872
930
|
}))
|
|
873
931
|
: undefined;
|
|
874
932
|
|
|
@@ -887,6 +945,7 @@ export function useHtmlAnnotation({
|
|
|
887
945
|
createdA: Date.now(),
|
|
888
946
|
images,
|
|
889
947
|
htmlAnchor: pendingAnchorRef.current ?? undefined,
|
|
948
|
+
elementContext: pendingContextRef.current ?? undefined,
|
|
890
949
|
htmlAdditionalTargets: additionalTargets,
|
|
891
950
|
});
|
|
892
951
|
|
|
@@ -894,6 +953,7 @@ export function useHtmlAnnotation({
|
|
|
894
953
|
setDraftTargets([]);
|
|
895
954
|
pendingTextRef.current = "";
|
|
896
955
|
pendingAnchorRef.current = null;
|
|
956
|
+
pendingContextRef.current = null;
|
|
897
957
|
},
|
|
898
958
|
[post],
|
|
899
959
|
);
|
|
@@ -915,6 +975,7 @@ export function useHtmlAnnotation({
|
|
|
915
975
|
label: t.label,
|
|
916
976
|
text: t.text,
|
|
917
977
|
anchor: t.anchor ?? undefined,
|
|
978
|
+
...(t.context ? { context: t.context } : {}),
|
|
918
979
|
}))
|
|
919
980
|
: undefined;
|
|
920
981
|
|
|
@@ -934,6 +995,7 @@ export function useHtmlAnnotation({
|
|
|
934
995
|
author: getIdentity(),
|
|
935
996
|
createdA: Date.now(),
|
|
936
997
|
htmlAnchor: pendingAnchorRef.current ?? undefined,
|
|
998
|
+
elementContext: pendingContextRef.current ?? undefined,
|
|
937
999
|
htmlAdditionalTargets: additionalTargets,
|
|
938
1000
|
});
|
|
939
1001
|
|
|
@@ -941,6 +1003,7 @@ export function useHtmlAnnotation({
|
|
|
941
1003
|
setDraftTargets([]);
|
|
942
1004
|
pendingTextRef.current = "";
|
|
943
1005
|
pendingAnchorRef.current = null;
|
|
1006
|
+
pendingContextRef.current = null;
|
|
944
1007
|
}, [post]);
|
|
945
1008
|
|
|
946
1009
|
const handleCommentClose = useCallback(() => {
|
|
@@ -949,6 +1012,7 @@ export function useHtmlAnnotation({
|
|
|
949
1012
|
setDraftTargets([]);
|
|
950
1013
|
pendingTextRef.current = "";
|
|
951
1014
|
pendingAnchorRef.current = null;
|
|
1015
|
+
pendingContextRef.current = null;
|
|
952
1016
|
}, [post]);
|
|
953
1017
|
|
|
954
1018
|
const removeDraftTarget = useCallback(
|
|
@@ -971,6 +1035,7 @@ export function useHtmlAnnotation({
|
|
|
971
1035
|
setToolbarState(null);
|
|
972
1036
|
pendingTextRef.current = "";
|
|
973
1037
|
pendingAnchorRef.current = null;
|
|
1038
|
+
pendingContextRef.current = null;
|
|
974
1039
|
}, [post]);
|
|
975
1040
|
|
|
976
1041
|
const applyQuickLabel = useCallback(
|
|
@@ -994,10 +1059,12 @@ export function useHtmlAnnotation({
|
|
|
994
1059
|
author: getIdentity(),
|
|
995
1060
|
createdA: Date.now(),
|
|
996
1061
|
htmlAnchor: pendingAnchorRef.current ?? undefined,
|
|
1062
|
+
elementContext: pendingContextRef.current ?? undefined,
|
|
997
1063
|
});
|
|
998
1064
|
clearState();
|
|
999
1065
|
pendingTextRef.current = "";
|
|
1000
1066
|
pendingAnchorRef.current = null;
|
|
1067
|
+
pendingContextRef.current = null;
|
|
1001
1068
|
},
|
|
1002
1069
|
[post],
|
|
1003
1070
|
);
|