@plannotator/ui 0.31.0 → 0.33.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 +664 -0
- package/README.md +57 -1
- package/components/AnnotationPanel.tsx +96 -4
- package/components/AnnotationToolbar.tsx +25 -25
- package/components/CommentPopover.tsx +26 -0
- package/components/GraphvizBlock.tsx +86 -7
- package/components/HtmlSurfaceControls.tsx +170 -0
- package/components/InlineMarkdown.tsx +22 -2
- package/components/MermaidBlock.tsx +60 -26
- package/components/Settings.tsx +40 -1
- package/components/blocks/MathBlock.tsx +26 -14
- package/components/html-viewer/HtmlViewer.tsx +289 -6
- package/components/html-viewer/bridge-script.asset.js +4392 -0
- package/components/html-viewer/bridge-script.lite.ts +9 -0
- package/components/html-viewer/bridge-script.ts +54 -8
- package/components/html-viewer/hostThreads.ts +37 -0
- package/components/html-viewer/index.ts +22 -1
- package/components/html-viewer/srcdoc.ts +70 -1
- package/components/html-viewer/unanchored.ts +47 -0
- package/components/html-viewer/useHtmlAnnotation.ts +132 -5
- package/configure.ts +32 -0
- package/hooks/useHtmlRefresh.ts +149 -0
- package/hooks/useMathRenderer.ts +30 -0
- package/hooks/useSharing.ts +31 -5
- package/package.json +9 -3
- package/styles.css +1 -1
- package/types.ts +1 -0
- package/utils/generateIdentity.ts +64 -14
- package/utils/identity-tater.ts +36 -0
- package/utils/math-default-loader.ts +24 -0
- package/utils/math-eager.ts +25 -0
- package/utils/math.ts +149 -0
- package/utils/mermaid-eager.ts +28 -0
- package/utils/mermaid.ts +132 -0
- package/utils/parser.ts +38 -0
- package/utils/quickLabels.ts +13 -0
- package/webmcp/activity.ts +46 -0
- package/webmcp/changes.ts +227 -0
- package/webmcp/index.ts +72 -0
- package/webmcp/modelContext.ts +103 -0
- package/webmcp/nudges.ts +174 -0
- package/webmcp/policy.ts +50 -0
- package/webmcp/preference.ts +50 -0
- package/webmcp/schema.ts +81 -0
- package/webmcp/toolset.ts +337 -0
- package/webmcp/useToolset.ts +74 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @generated by scripts/build-bridge-assets.ts from ./bridge-script.ts. DO NOT EDIT.
|
|
2
|
+
// Alias target for hosts on the HtmlViewer `bridgeScriptUrl` path: the same
|
|
3
|
+
// annotation CSS, protocol version and live bootstrap as ./bridge-script, with
|
|
4
|
+
// the inline bridge literal stubbed out so a bundler drops it from the viewer
|
|
5
|
+
// chunk. Rendering an HtmlViewer WITHOUT bridgeScriptUrl under this alias throws.
|
|
6
|
+
export const ANNOTATION_HIGHLIGHT_CSS = "\n/* Committed annotation visuals (highlight rectangles + numbered placed\n * markers) render inside a shadow-rooted fixed overlay host — see OVERLAY_CSS\n * in the bridge script. Nothing annotation-related is ever wrapped into or\n * styled onto the author's own elements. */\n/* Vim pinpoint target tint. The MOUSE pinpoint path no longer mutates author\n * elements — it draws the dedicated overlay box below — but keyboard (vim)\n * navigation keeps this class-based visual. */\n.plannotator-pinpoint-hover {\n background-color: oklch(from var(--pn-focus-highlight, #4493f8) l c h / 0.12) !important;\n border-radius: 3px;\n cursor: pointer !important;\n}\n/* SVG groups can't render a CSS background, so use a soft glow instead. */\n.plannotator-pinpoint-hover:is(g, svg) {\n filter: drop-shadow(0 0 4px oklch(from var(--pn-focus-highlight, #4493f8) l c h / 0.55));\n}\n/* Mouse pinpoint hover: a fixed-position outline box sized to the hovered\n * element's rect. Never a class/style write on the page's own elements. */\n[data-plannotator-pinpoint-box] {\n position: fixed;\n z-index: 2147483643;\n pointer-events: none;\n display: none;\n box-sizing: border-box;\n border: 2px solid oklch(from var(--pn-focus-highlight, #4493f8) l c h / 0.85);\n border-radius: 5px;\n background: oklch(from var(--pn-focus-highlight, #4493f8) l c h / 0.06);\n}\n[data-plannotator-pinpoint-box].pn-pin-enter {\n animation: pn-pinpoint-in 0.12s ease-out;\n}\n[data-plannotator-pinpoint-box][data-pinned] {\n border-color: var(--pn-accent, #d97757);\n background: oklch(from var(--pn-accent, #d97757) l c h / 0.08);\n}\n@keyframes pn-pinpoint-in {\n from { opacity: 0; transform: scale(0.985); }\n to { opacity: 1; transform: scale(1); }\n}\n/* Pinpoint mode affordance: crosshair everywhere. Placed markers live in the\n * shadow overlay and keep their own pointer cursor there. */\nbody[data-plannotator-pinpoint-cursor],\nbody[data-plannotator-pinpoint-cursor] * {\n cursor: crosshair !important;\n}\n@media (prefers-reduced-motion: reduce) {\n [data-plannotator-pinpoint-box].pn-pin-enter {\n animation: none;\n }\n}\n@media print {\n /* Viewer overlays are review chrome, not page content: never bake pinpoint\n boxes/labels or vim UI into a printed page. The outer app chrome is\n print-hidden by print.css, but this CSS lives inside the iframe's own\n document and must carry its own rule. The annotation overlay host carries\n its own print rule inside its shadow root. */\n [data-plannotator-pinpoint-box],\n [data-plannotator-pinpoint-label],\n [data-plannotator-vim-ui],\n [data-plannotator-vim-cursor] {\n display: none !important;\n }\n}\n/* Print-parity layer: committed highlight rects re-projected into an\n * absolute-positioned light-DOM layer built on beforeprint and torn down on\n * afterprint (the fixed overlay cannot paginate). Guarded here so it can\n * never flash on screen even if an afterprint teardown is missed. */\n@media screen {\n [data-plannotator-print-layer] {\n display: none !important;\n }\n}\nbody[data-plannotator-vim-focus-owner]:focus {\n outline: none !important;\n}\n[data-plannotator-vim-cursor] {\n position: fixed;\n z-index: 2147483646;\n width: 2px;\n min-height: 1em;\n border-radius: 2px;\n background: var(--pn-focus-highlight, #4493f8);\n pointer-events: none;\n}\n[data-plannotator-vim-reticle] {\n position: fixed;\n z-index: 2147483645;\n inset: 0;\n overflow: visible;\n pointer-events: none;\n}\n[data-plannotator-vim-reticle] [data-vim-reticle-fill],\n[data-plannotator-vim-reticle] [data-vim-reticle-corner],\n[data-plannotator-vim-reticle] [data-vim-reticle-label] {\n position: absolute;\n top: 0;\n left: 0;\n will-change: transform;\n transition: transform 90ms cubic-bezier(.22,1,.36,1);\n}\n[data-plannotator-vim-reticle] [data-vim-reticle-fill] {\n width: 100px;\n height: 100px;\n transform-origin: 0 0;\n border-radius: 8px;\n background: rgba(167,139,250,.045);\n box-shadow:\n inset 0 0 0 1px rgba(196,181,253,.16),\n 0 0 42px rgba(139,92,246,.12);\n}\n[data-plannotator-vim-reticle] [data-vim-reticle-corner] {\n width: 28px;\n height: 28px;\n border-color: #c4b5fd;\n filter:\n drop-shadow(0 0 6px rgba(167,139,250,.92))\n drop-shadow(0 0 18px rgba(124,58,237,.42));\n}\n[data-plannotator-vim-reticle] [data-vim-reticle-corner=\"top-left\"] {\n border-top: 3px solid;\n border-left: 3px solid;\n border-top-left-radius: 8px;\n}\n[data-plannotator-vim-reticle] [data-vim-reticle-corner=\"top-right\"] {\n border-top: 3px solid;\n border-right: 3px solid;\n border-top-right-radius: 8px;\n}\n[data-plannotator-vim-reticle] [data-vim-reticle-corner=\"bottom-left\"] {\n border-bottom: 3px solid;\n border-left: 3px solid;\n border-bottom-left-radius: 8px;\n}\n[data-plannotator-vim-reticle] [data-vim-reticle-corner=\"bottom-right\"] {\n border-right: 3px solid;\n border-bottom: 3px solid;\n border-bottom-right-radius: 8px;\n}\n[data-plannotator-vim-reticle] [data-vim-reticle-label] {\n z-index: 1;\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 118px;\n height: 30px;\n max-width: min(280px, calc(100vw - 24px));\n padding: 0 11px;\n overflow: hidden;\n border: 1px solid rgba(216,206,255,.42);\n border-radius: 9px;\n color: #f6f2ff;\n background: rgba(18,14,28,.84);\n box-shadow:\n 0 10px 28px rgba(0,0,0,.42),\n 0 0 20px rgba(139,92,246,.18);\n backdrop-filter: blur(10px);\n font: 700 10px/1 ui-monospace, SFMono-Regular, Menlo, monospace;\n letter-spacing: .13em;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n[data-plannotator-vim-reticle] [data-vim-reticle-label]::before {\n width: 7px;\n height: 7px;\n flex: 0 0 auto;\n border-radius: 999px;\n background: #c4b5fd;\n box-shadow: 0 0 12px rgba(167,139,250,.94);\n content: \"\";\n}\n@media (prefers-reduced-motion: reduce) {\n [data-plannotator-vim-reticle] [data-vim-reticle-fill],\n [data-plannotator-vim-reticle] [data-vim-reticle-corner],\n [data-plannotator-vim-reticle] [data-vim-reticle-label] {\n transition: none;\n }\n}\n[data-plannotator-vim-badge] {\n position: fixed;\n z-index: 2147483647;\n left: 50%;\n bottom: 12px;\n transform: translateX(-50%);\n padding: 4px 9px;\n border: 1px solid color-mix(in srgb, var(--pn-focus-highlight, #4493f8) 35%, transparent);\n border-radius: 6px;\n background: color-mix(in srgb, var(--pn-background, #111) 94%, transparent);\n color: var(--pn-focus-highlight, #4493f8);\n box-shadow: 0 4px 18px rgba(0,0,0,.25);\n font: 700 10px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace;\n letter-spacing: .04em;\n pointer-events: none;\n}\n";
|
|
7
|
+
export const BRIDGE_PROTOCOL_VERSION = 1;
|
|
8
|
+
export const BRIDGE_SCRIPT = "";
|
|
9
|
+
export const LIVE_BRIDGE_BOOTSTRAP = "(function() {\n var config = window.__plannotatorLiveConfig;\n if (!config || typeof config.css !== 'string') return;\n try {\n var style = document.createElement('style');\n style.setAttribute('data-plannotator-live-css', '');\n style.appendChild(document.createTextNode(config.css));\n (document.head || document.documentElement).appendChild(style);\n } catch (ex) {}\n})();";
|
|
@@ -210,6 +210,18 @@ body[data-plannotator-vim-focus-owner]:focus {
|
|
|
210
210
|
}
|
|
211
211
|
`;
|
|
212
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Bridge protocol version. Stamped on the bridge's `ready` message
|
|
215
|
+
* (`protocolVersion`) and compared by the parent (HtmlViewer) against this
|
|
216
|
+
* same constant. Bump it whenever a message shape changes in a way an older
|
|
217
|
+
* bridge or an older parent would misread. The inline srcdoc path and the
|
|
218
|
+
* live proxy always ship the bridge from the same bundle as the parent, so
|
|
219
|
+
* they match by construction; the check exists for hosts that serve the
|
|
220
|
+
* generated `bridge-script.asset.js` separately (`bridgeScriptUrl`), where a
|
|
221
|
+
* cached asset from a previous package version can outlive the parent code.
|
|
222
|
+
*/
|
|
223
|
+
export const BRIDGE_PROTOCOL_VERSION = 1;
|
|
224
|
+
|
|
213
225
|
export const BRIDGE_SCRIPT = `(function() {
|
|
214
226
|
var PREFIX = 'plannotator-bridge-';
|
|
215
227
|
|
|
@@ -339,6 +351,17 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
339
351
|
var pendingMultiTargets = []; // { key, el, anchor, label, text, box }
|
|
340
352
|
var multiTargetSeq = 0;
|
|
341
353
|
var MAX_MULTI_TARGETS = 16;
|
|
354
|
+
// Per-draft cap on additional targets: the parent may lower it on
|
|
355
|
+
// arm-multi-select ({ max }) to its product cap so the toggle stops where
|
|
356
|
+
// the saved annotation would. Never above MAX_MULTI_TARGETS; reset with
|
|
357
|
+
// the arm on every draft.
|
|
358
|
+
var multiSelectMax = MAX_MULTI_TARGETS;
|
|
359
|
+
function clampMultiSelectMax(value) {
|
|
360
|
+
if (typeof value !== 'number' || !isFinite(value)) return MAX_MULTI_TARGETS;
|
|
361
|
+
var whole = Math.floor(value);
|
|
362
|
+
if (whole < 0) return 0;
|
|
363
|
+
return whole > MAX_MULTI_TARGETS ? MAX_MULTI_TARGETS : whole;
|
|
364
|
+
}
|
|
342
365
|
// Live mode clamps the INPUT METHOD to pinpoint (click = element). Text
|
|
343
366
|
// drag-selection is a separate, always-on channel — see the mouseup handler
|
|
344
367
|
// — so the clamp only decides what a plain click does, never whether text
|
|
@@ -637,6 +660,10 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
637
660
|
&& e.data.key === pendingPinKey
|
|
638
661
|
) {
|
|
639
662
|
multiSelectArmed = true;
|
|
663
|
+
// Optional product cap for THIS draft; absent keeps the bridge's own.
|
|
664
|
+
multiSelectMax = e.data.max === undefined
|
|
665
|
+
? MAX_MULTI_TARGETS
|
|
666
|
+
: clampMultiSelectMax(e.data.max);
|
|
640
667
|
}
|
|
641
668
|
}
|
|
642
669
|
|
|
@@ -655,14 +682,25 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
655
682
|
// Selecting an annotation scrolls its first resolved target into view
|
|
656
683
|
// and flashes the overlay focus highlight over EVERY rect of EVERY
|
|
657
684
|
// target — never a class write on page elements, and never only the
|
|
658
|
-
// first fragment of a multi-paragraph selection.
|
|
659
|
-
|
|
685
|
+
// first fragment of a multi-paragraph selection. The optional
|
|
686
|
+
// behavior lets the parent pass its reduced-motion preference across
|
|
687
|
+
// the boundary; absent means smooth, as before.
|
|
688
|
+
scrollToAnnotation(e.data.id, e.data.behavior === 'auto' ? 'auto' : 'smooth');
|
|
660
689
|
}
|
|
661
690
|
|
|
662
691
|
else if (type === PREFIX + 'focus-mark') {
|
|
663
692
|
focusAnnotationRecord(typeof e.data.id === 'string' ? e.data.id : null, false);
|
|
664
693
|
}
|
|
665
694
|
|
|
695
|
+
else if (type === PREFIX + 'report-unanchored') {
|
|
696
|
+
// The parent posted its restore batch and wants the complete set once
|
|
697
|
+
// the next complete overlay pass has run, even if the set is unchanged
|
|
698
|
+
// (empty included). Messages are processed in order, so the pass this
|
|
699
|
+
// schedules sees every find-and-mark posted before this request.
|
|
700
|
+
unanchoredReportRequested = true;
|
|
701
|
+
schedulePinpointReconcile();
|
|
702
|
+
}
|
|
703
|
+
|
|
666
704
|
else if (type === PREFIX + 'set-input-method') {
|
|
667
705
|
// Live mode clamps the input method to pinpoint (what a plain click
|
|
668
706
|
// does); text drag-selection commenting stays live regardless.
|
|
@@ -1488,6 +1526,10 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1488
1526
|
// whose records are removed and therefore invisible to the per-pass scan.
|
|
1489
1527
|
var lastUnanchoredKey = '[]';
|
|
1490
1528
|
var restoreFailedIds = new Set();
|
|
1529
|
+
// Set by report-unanchored: the parent asks for the complete set after
|
|
1530
|
+
// its restore batch, so the next COMPLETE pass emits even when the set
|
|
1531
|
+
// did not change (an all-restored document reports its empty set once).
|
|
1532
|
+
var unanchoredReportRequested = false;
|
|
1491
1533
|
function emitUnanchored(deadRecordIds) {
|
|
1492
1534
|
var seen = new Set();
|
|
1493
1535
|
var combined = [];
|
|
@@ -1506,7 +1548,8 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
1506
1548
|
combined.sort();
|
|
1507
1549
|
if (combined.length > 512) combined = combined.slice(0, 512);
|
|
1508
1550
|
var key = JSON.stringify(combined);
|
|
1509
|
-
if (key === lastUnanchoredKey) return;
|
|
1551
|
+
if (key === lastUnanchoredKey && !unanchoredReportRequested) return;
|
|
1552
|
+
unanchoredReportRequested = false;
|
|
1510
1553
|
lastUnanchoredKey = key;
|
|
1511
1554
|
// postToParent, not a raw '*' post: live sessions stamp the session
|
|
1512
1555
|
// token and post only to the listed editor origins, and the parent
|
|
@@ -2495,7 +2538,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2495
2538
|
}
|
|
2496
2539
|
}
|
|
2497
2540
|
|
|
2498
|
-
function scrollToAnnotation(id) {
|
|
2541
|
+
function scrollToAnnotation(id, behavior) {
|
|
2499
2542
|
var record = findAnnRecord(id);
|
|
2500
2543
|
if (!record) return;
|
|
2501
2544
|
beginDeadSearchPass(Infinity); // user-initiated one-shot: never budget-starved
|
|
@@ -2511,7 +2554,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2511
2554
|
}
|
|
2512
2555
|
}
|
|
2513
2556
|
if (scrollEl) {
|
|
2514
|
-
try { scrollEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch (ex) {}
|
|
2557
|
+
try { scrollEl.scrollIntoView({ behavior: behavior || 'smooth', block: 'center' }); } catch (ex) {}
|
|
2515
2558
|
}
|
|
2516
2559
|
focusAnnotationRecord(id, true);
|
|
2517
2560
|
}
|
|
@@ -2693,6 +2736,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2693
2736
|
pendingPinPoint = null;
|
|
2694
2737
|
pendingPinViaPinpoint = false;
|
|
2695
2738
|
multiSelectArmed = false;
|
|
2739
|
+
multiSelectMax = MAX_MULTI_TARGETS;
|
|
2696
2740
|
hidePinpointBox();
|
|
2697
2741
|
}
|
|
2698
2742
|
|
|
@@ -2857,8 +2901,9 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2857
2901
|
}
|
|
2858
2902
|
}
|
|
2859
2903
|
}
|
|
2860
|
-
// Cap at the source: never grow the draft past the parent-side DTO cap
|
|
2861
|
-
|
|
2904
|
+
// Cap at the source: never grow the draft past the parent-side DTO cap
|
|
2905
|
+
// (or the lower product cap the parent armed this draft with).
|
|
2906
|
+
if (pendingMultiTargets.length >= multiSelectMax) return;
|
|
2862
2907
|
var point = normalizePointInElement(el, clickPoint);
|
|
2863
2908
|
if (anchor && point) anchor.point = point;
|
|
2864
2909
|
var label = pinpointHoverLabel(el);
|
|
@@ -2961,6 +3006,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
2961
3006
|
// drafts (comment -> quick label) leaves a stale arm and the bridge
|
|
2962
3007
|
// accumulates pins the saved annotation will not carry.
|
|
2963
3008
|
multiSelectArmed = false;
|
|
3009
|
+
multiSelectMax = MAX_MULTI_TARGETS;
|
|
2964
3010
|
pendingPinEl = el;
|
|
2965
3011
|
pendingPinAnchor = buildElementAnchor(el);
|
|
2966
3012
|
pendingPinKey = makeTargetKey();
|
|
@@ -4538,7 +4584,7 @@ export const BRIDGE_SCRIPT = `(function() {
|
|
|
4538
4584
|
// pinpoint: show the cursor affordance immediately instead of waiting for
|
|
4539
4585
|
// the parent's first set-input-method/set-annotate-mode round trip.
|
|
4540
4586
|
updatePinpointCursor();
|
|
4541
|
-
var readyMsg = { type: PREFIX + 'ready' };
|
|
4587
|
+
var readyMsg = { type: PREFIX + 'ready', protocolVersion: ${BRIDGE_PROTOCOL_VERSION} };
|
|
4542
4588
|
if (LIVE) readyMsg.pageUrl = currentPageUrl();
|
|
4543
4589
|
postToParent(readyMsg);
|
|
4544
4590
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side helpers for the raw-HTML viewer, re-exported from
|
|
3
|
+
* `@plannotator/core/html-anchor` with the package's `Annotation` type.
|
|
4
|
+
*
|
|
5
|
+
* `projectHostThreads` turns a host's stored rows into the `annotations`
|
|
6
|
+
* prop (output order == marker numbering); `buildPersistedHtmlAnchor` trims a
|
|
7
|
+
* composed comment's anchor to a bounded record the host can persist. Both
|
|
8
|
+
* are pure and dependency-free (they live in `@plannotator/core`).
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
projectHostThreads as projectHostThreadsCore,
|
|
12
|
+
type HostThread,
|
|
13
|
+
type ProjectHostThreadsOptions,
|
|
14
|
+
} from "@plannotator/core/html-anchor";
|
|
15
|
+
import type { Annotation } from "../../types";
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
buildPersistedHtmlAnchor,
|
|
19
|
+
type BuildPersistedHtmlAnchorOptions,
|
|
20
|
+
type HostThread,
|
|
21
|
+
type PersistedHtmlAnchor,
|
|
22
|
+
type PersistedHtmlAnchorResult,
|
|
23
|
+
type ProjectHostThreadsOptions,
|
|
24
|
+
} from "@plannotator/core/html-anchor";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Project stored host rows onto the viewer's `annotations` prop, in the
|
|
28
|
+
* host's order (which becomes the marker numbering). See the core function
|
|
29
|
+
* for the projection rules; the `type` literals it emits are the string
|
|
30
|
+
* values of `AnnotationType`, so the cast below is representation-exact.
|
|
31
|
+
*/
|
|
32
|
+
export function projectHostThreads(
|
|
33
|
+
threads: readonly HostThread[],
|
|
34
|
+
options?: ProjectHostThreadsOptions,
|
|
35
|
+
): Annotation[] {
|
|
36
|
+
return projectHostThreadsCore(threads, options) as unknown as Annotation[];
|
|
37
|
+
}
|
|
@@ -1 +1,22 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
DEFAULT_BRIDGE_READY_TIMEOUT_MS,
|
|
3
|
+
HtmlViewer,
|
|
4
|
+
formatBridgeUnavailableMessage,
|
|
5
|
+
type BridgeUnavailableInfo,
|
|
6
|
+
type HtmlViewerProps,
|
|
7
|
+
} from "./HtmlViewer";
|
|
8
|
+
export { BRIDGE_PROTOCOL_VERSION } from "./bridge-script";
|
|
9
|
+
export {
|
|
10
|
+
checkBridgeProtocolVersion,
|
|
11
|
+
formatBridgeProtocolWarning,
|
|
12
|
+
type BridgeProtocolVerdict,
|
|
13
|
+
} from "./useHtmlAnnotation";
|
|
14
|
+
export {
|
|
15
|
+
buildPersistedHtmlAnchor,
|
|
16
|
+
projectHostThreads,
|
|
17
|
+
type BuildPersistedHtmlAnchorOptions,
|
|
18
|
+
type HostThread,
|
|
19
|
+
type PersistedHtmlAnchor,
|
|
20
|
+
type PersistedHtmlAnchorResult,
|
|
21
|
+
type ProjectHostThreadsOptions,
|
|
22
|
+
} from "./hostThreads";
|
|
@@ -96,6 +96,68 @@ export interface SrcdocInjectionOptions {
|
|
|
96
96
|
hostTheme: boolean;
|
|
97
97
|
/** The version-diff view is showing (rawHtml is htmlDiff output). */
|
|
98
98
|
diffActive: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Load the bridge through a classic `<script src>` from this URL instead of
|
|
101
|
+
* inlining `BRIDGE_SCRIPT`. Absent or empty: inline, exactly as before. The
|
|
102
|
+
* tag takes the inline script's place, so placement is identical on both
|
|
103
|
+
* paths: at the end of `<head>`, before the body (head scripts of the page
|
|
104
|
+
* run first, body scripts after the bridge). No `crossorigin` attribute is
|
|
105
|
+
* set: the srcdoc frame is an opaque origin, and a classic script needs no
|
|
106
|
+
* CORS to execute. Callers must pass an ABSOLUTE URL (see
|
|
107
|
+
* {@link resolveBridgeScriptUrl}): the framed page may carry its own
|
|
108
|
+
* `<base href>`, which precedes the injected tag and would re-anchor a
|
|
109
|
+
* relative URL onto an attacker-chosen origin.
|
|
110
|
+
*/
|
|
111
|
+
bridgeScriptUrl?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resolve a host-supplied bridge URL against the PARENT document (its
|
|
116
|
+
* `document.baseURI`), never against the framed page. The injection lands at
|
|
117
|
+
* the end of the page's `<head>`, after any `<base href>` the page declares,
|
|
118
|
+
* so a relative URL written into the srcdoc would resolve against that base:
|
|
119
|
+
* a hostile document could point the viewer at an attacker-served bridge and
|
|
120
|
+
* defeat the version check. Resolving here pins the URL before it is written.
|
|
121
|
+
* An unparsable input is returned unchanged (nothing loads, the ready timeout
|
|
122
|
+
* reports it) rather than throwing during render.
|
|
123
|
+
*/
|
|
124
|
+
export function resolveBridgeScriptUrl(bridgeScriptUrl: string, parentBaseUrl: string): string {
|
|
125
|
+
try {
|
|
126
|
+
return new URL(bridgeScriptUrl, parentBaseUrl).href;
|
|
127
|
+
} catch {
|
|
128
|
+
return bridgeScriptUrl;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Escape a string for a double-quoted HTML attribute value. */
|
|
133
|
+
function escapeAttribute(value: string): string {
|
|
134
|
+
return value
|
|
135
|
+
.replace(/&/g, "&")
|
|
136
|
+
.replace(/"/g, """)
|
|
137
|
+
.replace(/</g, "<")
|
|
138
|
+
.replace(/>/g, ">");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The bridge `<script>` element, the ONE injection point for both delivery
|
|
143
|
+
* paths. Inline is the default and Plannotator's only path; the URL form is
|
|
144
|
+
* the opt-in for hosts that serve the generated `bridge-script.asset.js`.
|
|
145
|
+
*/
|
|
146
|
+
export function buildBridgeScriptTag(bridgeScriptUrl?: string): string {
|
|
147
|
+
if (bridgeScriptUrl) {
|
|
148
|
+
return `<script src="${escapeAttribute(bridgeScriptUrl)}"></script>`;
|
|
149
|
+
}
|
|
150
|
+
if (!BRIDGE_SCRIPT) {
|
|
151
|
+
// Only reachable when a host aliased `./bridge-script` to the generated
|
|
152
|
+
// `bridge-script.lite` module (which stubs the inline literal) and then
|
|
153
|
+
// rendered an HtmlViewer without `bridgeScriptUrl`: an empty inline
|
|
154
|
+
// script would be a silently dead surface, so fail loudly instead.
|
|
155
|
+
throw new Error(
|
|
156
|
+
"@plannotator/ui HtmlViewer: the inline bridge script is stubbed out "
|
|
157
|
+
+ "(bridge-script.lite alias) but no bridgeScriptUrl was passed.",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return `<script>${BRIDGE_SCRIPT}</script>`;
|
|
99
161
|
}
|
|
100
162
|
|
|
101
163
|
/** The `<style>` + `<script>` block spliced into the document's head. */
|
|
@@ -104,6 +166,7 @@ export function buildSrcdocInjection({
|
|
|
104
166
|
isLight,
|
|
105
167
|
hostTheme,
|
|
106
168
|
diffActive,
|
|
169
|
+
bridgeScriptUrl,
|
|
107
170
|
}: SrcdocInjectionOptions): string {
|
|
108
171
|
const payload = buildThemeTokenPayload(tokens, hostTheme);
|
|
109
172
|
let themeCSS = ":root {\n";
|
|
@@ -117,7 +180,7 @@ export function buildSrcdocInjection({
|
|
|
117
180
|
themeCSS += `:root { color-scheme: ${isLight ? "light" : "dark"}; }\n`;
|
|
118
181
|
}
|
|
119
182
|
const diffCSS = diffActive ? DIFF_HIGHLIGHT_CSS : "";
|
|
120
|
-
return `<style>${themeCSS}${ANNOTATION_HIGHLIGHT_CSS}${diffCSS}</style
|
|
183
|
+
return `<style>${themeCSS}${ANNOTATION_HIGHLIGHT_CSS}${diffCSS}</style>${buildBridgeScriptTag(bridgeScriptUrl)}`;
|
|
121
184
|
}
|
|
122
185
|
|
|
123
186
|
/**
|
|
@@ -126,6 +189,12 @@ export function buildSrcdocInjection({
|
|
|
126
189
|
* script and disables annotation entirely. The iframe `sandbox` attribute is
|
|
127
190
|
* the security boundary for the annotate surface; the page's CSP was written
|
|
128
191
|
* for its standalone context, so it is removed before injection.
|
|
192
|
+
*
|
|
193
|
+
* The package itself never adds a CSP `<meta>` to the srcdoc document (the
|
|
194
|
+
* injection is one `<style>` and one `<script>`), so nothing here blocks a
|
|
195
|
+
* `<script src>` on the URL path. A CSP delivered as an HTTP header on the
|
|
196
|
+
* HOST page is inherited by the srcdoc document, and the host must allow
|
|
197
|
+
* `script-src` for the origin the asset is served from.
|
|
129
198
|
*/
|
|
130
199
|
const META_CSP_RE =
|
|
131
200
|
/<meta\s[^>]*http-equiv\s*=\s*["']?\s*content-security-policy\s*["']?[^>]*\/?>/gi;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { AnnotationType, type Annotation } from "../../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A page-anchored row the viewer can never post to the bridge: nothing to
|
|
5
|
+
* find it by (no quoted text, no element anchor, no additional target
|
|
6
|
+
* anchor). Document-level comments are excluded on purpose: a
|
|
7
|
+
* GLOBAL_COMMENT has no page location by design and is not "unanchored".
|
|
8
|
+
*/
|
|
9
|
+
export function isTextlessPageAnnotation(annotation: Annotation): boolean {
|
|
10
|
+
if (annotation.type === AnnotationType.GLOBAL_COMMENT) return false;
|
|
11
|
+
if (annotation.originalText) return false;
|
|
12
|
+
if (annotation.htmlAnchor) return false;
|
|
13
|
+
return !(annotation.htmlAdditionalTargets ?? []).some((target) => !!target.anchor);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The host-facing unanchored set: the bridge's report (ids with no live
|
|
18
|
+
* representation on the page) completed with what the bridge cannot see.
|
|
19
|
+
*
|
|
20
|
+
* - Textless page rows are added: they were never posted, so the bridge
|
|
21
|
+
* cannot report them, yet they have no marker and no highlight.
|
|
22
|
+
* - An id this viewer minted for a locally created comment (`create-mark`)
|
|
23
|
+
* that the host never carried in `annotations`, or has since swapped out
|
|
24
|
+
* for its own id, is dropped: the host holds no card for it, so naming it
|
|
25
|
+
* would be noise. Every other bridge id passes through untouched, so a
|
|
26
|
+
* host that paints through the imperative handle keeps today's delivery.
|
|
27
|
+
*
|
|
28
|
+
* Sorted and deduplicated like the bridge's own emission. With no textless
|
|
29
|
+
* rows and no swapped-out minted ids the result is exactly the bridge list.
|
|
30
|
+
*/
|
|
31
|
+
export function mergeUnanchoredIds(input: {
|
|
32
|
+
bridgeIds: readonly string[];
|
|
33
|
+
annotations: readonly Annotation[];
|
|
34
|
+
createdIds: ReadonlySet<string>;
|
|
35
|
+
}): string[] {
|
|
36
|
+
const known = new Set<string>();
|
|
37
|
+
for (const annotation of input.annotations) known.add(annotation.id);
|
|
38
|
+
const out = new Set<string>();
|
|
39
|
+
for (const id of input.bridgeIds) {
|
|
40
|
+
if (input.createdIds.has(id) && !known.has(id)) continue;
|
|
41
|
+
out.add(id);
|
|
42
|
+
}
|
|
43
|
+
for (const annotation of input.annotations) {
|
|
44
|
+
if (isTextlessPageAnnotation(annotation)) out.add(annotation.id);
|
|
45
|
+
}
|
|
46
|
+
return [...out].sort();
|
|
47
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useRef, type RefObject } from "react";
|
|
2
2
|
import { AnnotationType, type Annotation, type EditorMode, type HtmlAnnotationTarget, type HtmlElementAnchor, type ImageAttachment } from "../../types";
|
|
3
|
-
import type
|
|
3
|
+
import { THUMBS_UP_LABEL, type QuickLabel } from "../../utils/quickLabels";
|
|
4
4
|
import { getIdentity } from "../../utils/identity";
|
|
5
5
|
import type {
|
|
6
6
|
ToolbarState,
|
|
@@ -8,9 +8,46 @@ import type {
|
|
|
8
8
|
QuickLabelPickerState,
|
|
9
9
|
UseAnnotationHighlighterReturn,
|
|
10
10
|
} from "../../hooks/useAnnotationHighlighter";
|
|
11
|
+
import { BRIDGE_PROTOCOL_VERSION } from "./bridge-script";
|
|
11
12
|
|
|
12
13
|
const PREFIX = "plannotator-bridge-";
|
|
13
14
|
|
|
15
|
+
/** Outcome of comparing a bridge `ready` message's stamp with this parent. */
|
|
16
|
+
export interface BridgeProtocolVerdict {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
/** The version this parent bundle speaks (`BRIDGE_PROTOCOL_VERSION`). */
|
|
19
|
+
expected: number;
|
|
20
|
+
/** The version the bridge reported; `undefined` when the ready carried none
|
|
21
|
+
* (a bridge asset built before the stamp existed, or a forged message). */
|
|
22
|
+
reported: number | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Compare a `ready` message against the parent's protocol version. A missing
|
|
27
|
+
* stamp counts as a mismatch: the only way it can be absent is a bridge asset
|
|
28
|
+
* older than the parent (or a page forging the message), which is exactly
|
|
29
|
+
* the drift this check exists to name.
|
|
30
|
+
*/
|
|
31
|
+
export function checkBridgeProtocolVersion(data: unknown): BridgeProtocolVerdict {
|
|
32
|
+
const raw = isRecord(data) ? data.protocolVersion : undefined;
|
|
33
|
+
const reported = typeof raw === "number" && Number.isFinite(raw) ? raw : undefined;
|
|
34
|
+
return {
|
|
35
|
+
ok: reported === BRIDGE_PROTOCOL_VERSION,
|
|
36
|
+
expected: BRIDGE_PROTOCOL_VERSION,
|
|
37
|
+
reported,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The one console warning a mismatch produces; names both versions. */
|
|
42
|
+
export function formatBridgeProtocolWarning(
|
|
43
|
+
verdict: BridgeProtocolVerdict,
|
|
44
|
+
bridgeScriptUrl?: string,
|
|
45
|
+
): string {
|
|
46
|
+
const reported = verdict.reported === undefined ? "none" : String(verdict.reported);
|
|
47
|
+
const source = bridgeScriptUrl ? `the bridge script at ${bridgeScriptUrl}` : "the bridge script";
|
|
48
|
+
return `[plannotator] HTML bridge protocol version mismatch: this viewer expects ${verdict.expected}, ${source} reported ${reported}. Serve the bridge-script asset from the same @plannotator/ui version as the viewer.`;
|
|
49
|
+
}
|
|
50
|
+
|
|
14
51
|
// Collision-proof annotation ids. `Date.now()` alone repeats within a millisecond,
|
|
15
52
|
// so two quick annotations could share a data-bind-id and clobber each other.
|
|
16
53
|
let htmlAnnSeq = 0;
|
|
@@ -18,6 +55,13 @@ function nextHtmlAnnId(): string {
|
|
|
18
55
|
return `html-ann-${Date.now().toString(36)}-${(htmlAnnSeq++).toString(36)}`;
|
|
19
56
|
}
|
|
20
57
|
|
|
58
|
+
/** Ids minted by this module for locally created annotations (create-mark).
|
|
59
|
+
* Module-scoped like the sequence above: a host that swaps a local id for
|
|
60
|
+
* its own server id keeps the local mark until it removes it, and the
|
|
61
|
+
* unanchored union needs to recognise such ids whichever viewer instance
|
|
62
|
+
* minted them. Bounded: only ids from this page load, one entry per create. */
|
|
63
|
+
const mintedHtmlAnnIds = new Set<string>();
|
|
64
|
+
|
|
21
65
|
function htmlCommentDraftKey(
|
|
22
66
|
text: string,
|
|
23
67
|
anchor?: HtmlElementAnchor | null,
|
|
@@ -129,6 +173,20 @@ export interface UseHtmlAnnotationOptions {
|
|
|
129
173
|
* empty on recovery. Delivered in readOnly mode too: view-only surfaces
|
|
130
174
|
* are exactly where silently missing markers would go unnoticed. */
|
|
131
175
|
onUnanchoredChange?: (ids: string[]) => void;
|
|
176
|
+
/** Product cap on additional (shift-click) targets per comment, 0..16.
|
|
177
|
+
* Applied at the trust boundary, on submit, on restore, and carried to
|
|
178
|
+
* the bridge on arm-multi-select so the in-page toggle stops at the
|
|
179
|
+
* same number. Absent: the package's 16, and the arm message is unchanged. */
|
|
180
|
+
maxAdditionalTargets?: number;
|
|
181
|
+
/** scrollIntoView behavior for scroll-to (selecting an annotation).
|
|
182
|
+
* Absent: smooth, as before; pass 'auto' to honor reduced motion. */
|
|
183
|
+
scrollBehavior?: 'smooth' | 'auto';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Clamp a host cap into the package's bound; anything unusable is the default. */
|
|
187
|
+
export function resolveMaxAdditionalTargets(value: number | undefined): number {
|
|
188
|
+
if (value === undefined || !Number.isFinite(value)) return MAX_ADDITIONAL_TARGETS;
|
|
189
|
+
return Math.max(0, Math.min(MAX_ADDITIONAL_TARGETS, Math.floor(value)));
|
|
132
190
|
}
|
|
133
191
|
|
|
134
192
|
function postToIframe(
|
|
@@ -365,6 +423,8 @@ export function useHtmlAnnotation({
|
|
|
365
423
|
onPageChange,
|
|
366
424
|
onBridgePointer,
|
|
367
425
|
onUnanchoredChange,
|
|
426
|
+
maxAdditionalTargets,
|
|
427
|
+
scrollBehavior,
|
|
368
428
|
}: UseHtmlAnnotationOptions): Omit<
|
|
369
429
|
UseAnnotationHighlighterReturn,
|
|
370
430
|
"highlighterRef" | "highlightRange" | "highlightMathElement"
|
|
@@ -377,6 +437,13 @@ export function useHtmlAnnotation({
|
|
|
377
437
|
flashDraftTarget: (key: string) => void;
|
|
378
438
|
/** Bumped after every target add/remove so the composer can refocus its textarea. */
|
|
379
439
|
composerFocusToken: number;
|
|
440
|
+
/** Composer one-click "Looks good": submits the hardcoded positive label
|
|
441
|
+
* with the same anchor and multi-select targets a typed comment would carry. */
|
|
442
|
+
handleCommentLooksGood: () => void;
|
|
443
|
+
/** Ids this module minted for locally created annotations (create-mark),
|
|
444
|
+
* for the unanchored union: a minted id the host never listed is a
|
|
445
|
+
* swapped-out local mark, not a host row. Read-only, stable identity. */
|
|
446
|
+
createdAnnotationIds: ReadonlySet<string>;
|
|
380
447
|
} {
|
|
381
448
|
const [toolbarState, setToolbarState] = useState<ToolbarState | null>(null);
|
|
382
449
|
const [commentPopover, setCommentPopover] = useState<CommentPopoverState | null>(null);
|
|
@@ -417,6 +484,12 @@ export function useHtmlAnnotation({
|
|
|
417
484
|
liveRef.current = live ?? null;
|
|
418
485
|
const onPageChangeRef = useRef(onPageChange);
|
|
419
486
|
onPageChangeRef.current = onPageChange;
|
|
487
|
+
// The effective cap and whether the host set one: only an explicit cap
|
|
488
|
+
// rides on arm-multi-select, so an unconfigured viewer posts today's message.
|
|
489
|
+
const maxTargetsRef = useRef(resolveMaxAdditionalTargets(maxAdditionalTargets));
|
|
490
|
+
maxTargetsRef.current = resolveMaxAdditionalTargets(maxAdditionalTargets);
|
|
491
|
+
const hostCapRef = useRef(maxAdditionalTargets !== undefined);
|
|
492
|
+
hostCapRef.current = maxAdditionalTargets !== undefined;
|
|
420
493
|
|
|
421
494
|
const anchorRef = useRef<HTMLDivElement | null>(null);
|
|
422
495
|
|
|
@@ -577,6 +650,7 @@ export function useHtmlAnnotation({
|
|
|
577
650
|
post({
|
|
578
651
|
type: `${PREFIX}arm-multi-select`,
|
|
579
652
|
key: message.targetKey,
|
|
653
|
+
...(hostCapRef.current ? { max: maxTargetsRef.current } : {}),
|
|
580
654
|
});
|
|
581
655
|
}
|
|
582
656
|
} else {
|
|
@@ -596,7 +670,7 @@ export function useHtmlAnnotation({
|
|
|
596
670
|
if (
|
|
597
671
|
commentPopoverRef.current
|
|
598
672
|
&& targets.length > 0
|
|
599
|
-
&& targets.length < 1 +
|
|
673
|
+
&& targets.length < 1 + maxTargetsRef.current
|
|
600
674
|
&& !targets.some((t) => t.key === message.key)
|
|
601
675
|
) {
|
|
602
676
|
setDraftTargets([
|
|
@@ -712,6 +786,9 @@ export function useHtmlAnnotation({
|
|
|
712
786
|
post({
|
|
713
787
|
type: `${PREFIX}scroll-to`,
|
|
714
788
|
id: selectedAnnotationId,
|
|
789
|
+
// Only an explicit host preference rides along; the default message
|
|
790
|
+
// is unchanged and the bridge scrolls smoothly as before.
|
|
791
|
+
...(scrollBehavior ? { behavior: scrollBehavior } : {}),
|
|
715
792
|
});
|
|
716
793
|
} else {
|
|
717
794
|
post({
|
|
@@ -719,7 +796,7 @@ export function useHtmlAnnotation({
|
|
|
719
796
|
id: null,
|
|
720
797
|
});
|
|
721
798
|
}
|
|
722
|
-
}, [selectedAnnotationId, post]);
|
|
799
|
+
}, [selectedAnnotationId, post, scrollBehavior]);
|
|
723
800
|
|
|
724
801
|
const handleAnnotate = useCallback(
|
|
725
802
|
(type: AnnotationType) => {
|
|
@@ -728,6 +805,7 @@ export function useHtmlAnnotation({
|
|
|
728
805
|
if (!text || type !== AnnotationType.DELETION) return;
|
|
729
806
|
|
|
730
807
|
const id = nextHtmlAnnId();
|
|
808
|
+
mintedHtmlAnnIds.add(id);
|
|
731
809
|
post({ type: `${PREFIX}create-mark`, id, annotationType: "deletion" });
|
|
732
810
|
onAddRef.current?.({
|
|
733
811
|
id,
|
|
@@ -778,7 +856,7 @@ export function useHtmlAnnotation({
|
|
|
778
856
|
const targets = draftTargetsRef.current;
|
|
779
857
|
const additionalTargets: HtmlAnnotationTarget[] | undefined =
|
|
780
858
|
targets.length > 1
|
|
781
|
-
? targets.slice(1, 1 +
|
|
859
|
+
? targets.slice(1, 1 + maxTargetsRef.current).map((t) => ({
|
|
782
860
|
label: t.label,
|
|
783
861
|
text: t.text,
|
|
784
862
|
anchor: t.anchor ?? undefined,
|
|
@@ -786,6 +864,7 @@ export function useHtmlAnnotation({
|
|
|
786
864
|
: undefined;
|
|
787
865
|
|
|
788
866
|
const id = nextHtmlAnnId();
|
|
867
|
+
mintedHtmlAnnIds.add(id);
|
|
789
868
|
post({ type: `${PREFIX}create-mark`, id, annotationType: "comment" });
|
|
790
869
|
onAddRef.current?.({
|
|
791
870
|
id,
|
|
@@ -810,6 +889,51 @@ export function useHtmlAnnotation({
|
|
|
810
889
|
[post],
|
|
811
890
|
);
|
|
812
891
|
|
|
892
|
+
// The composer's one-click "Looks good" (the restored thumbs-up for
|
|
893
|
+
// comment-only surfaces, where pinpoint clicks land straight in the
|
|
894
|
+
// composer and never see the selection toolbar). Mirrors
|
|
895
|
+
// handleCommentSubmit — same anchor, same multi-select targets — but
|
|
896
|
+
// emits the hardcoded positive label instead of typed prose.
|
|
897
|
+
const handleCommentLooksGood = useCallback(() => {
|
|
898
|
+
if (!enabledRef.current) return;
|
|
899
|
+
const text = commentPopoverRef.current?.selectedText || pendingTextRef.current;
|
|
900
|
+
if (!text) return;
|
|
901
|
+
|
|
902
|
+
const targets = draftTargetsRef.current;
|
|
903
|
+
const additionalTargets: HtmlAnnotationTarget[] | undefined =
|
|
904
|
+
targets.length > 1
|
|
905
|
+
? targets.slice(1, 1 + maxTargetsRef.current).map((t) => ({
|
|
906
|
+
label: t.label,
|
|
907
|
+
text: t.text,
|
|
908
|
+
anchor: t.anchor ?? undefined,
|
|
909
|
+
}))
|
|
910
|
+
: undefined;
|
|
911
|
+
|
|
912
|
+
const id = nextHtmlAnnId();
|
|
913
|
+
mintedHtmlAnnIds.add(id);
|
|
914
|
+
post({ type: `${PREFIX}create-mark`, id, annotationType: "comment" });
|
|
915
|
+
onAddRef.current?.({
|
|
916
|
+
id,
|
|
917
|
+
blockId: "",
|
|
918
|
+
startOffset: 0,
|
|
919
|
+
endOffset: 0,
|
|
920
|
+
type: AnnotationType.COMMENT,
|
|
921
|
+
text: THUMBS_UP_LABEL.text,
|
|
922
|
+
originalText: text,
|
|
923
|
+
isQuickLabel: true,
|
|
924
|
+
quickLabelTip: THUMBS_UP_LABEL.tip,
|
|
925
|
+
author: getIdentity(),
|
|
926
|
+
createdA: Date.now(),
|
|
927
|
+
htmlAnchor: pendingAnchorRef.current ?? undefined,
|
|
928
|
+
htmlAdditionalTargets: additionalTargets,
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
setCommentPopover(null);
|
|
932
|
+
setDraftTargets([]);
|
|
933
|
+
pendingTextRef.current = "";
|
|
934
|
+
pendingAnchorRef.current = null;
|
|
935
|
+
}, [post]);
|
|
936
|
+
|
|
813
937
|
const handleCommentClose = useCallback(() => {
|
|
814
938
|
post({ type: `${PREFIX}cancel-selection` });
|
|
815
939
|
setCommentPopover(null);
|
|
@@ -846,6 +970,7 @@ export function useHtmlAnnotation({
|
|
|
846
970
|
const text = pendingTextRef.current;
|
|
847
971
|
if (!text) return;
|
|
848
972
|
const id = nextHtmlAnnId();
|
|
973
|
+
mintedHtmlAnnIds.add(id);
|
|
849
974
|
post({ type: `${PREFIX}create-mark`, id, annotationType: "comment" });
|
|
850
975
|
onAddRef.current?.({
|
|
851
976
|
id,
|
|
@@ -906,7 +1031,7 @@ export function useHtmlAnnotation({
|
|
|
906
1031
|
const additionalAnchors = (ann.htmlAdditionalTargets ?? [])
|
|
907
1032
|
.map((t) => t.anchor)
|
|
908
1033
|
.filter((a): a is HtmlElementAnchor => !!a)
|
|
909
|
-
.slice(0,
|
|
1034
|
+
.slice(0, maxTargetsRef.current);
|
|
910
1035
|
post({
|
|
911
1036
|
type: `${PREFIX}find-and-mark`,
|
|
912
1037
|
id: ann.id,
|
|
@@ -931,6 +1056,7 @@ export function useHtmlAnnotation({
|
|
|
931
1056
|
handleToolbarClose,
|
|
932
1057
|
handleRequestComment,
|
|
933
1058
|
handleCommentSubmit,
|
|
1059
|
+
handleCommentLooksGood,
|
|
934
1060
|
handleCommentClose,
|
|
935
1061
|
handleFloatingQuickLabel,
|
|
936
1062
|
handleQuickLabelPickerDismiss,
|
|
@@ -941,5 +1067,6 @@ export function useHtmlAnnotation({
|
|
|
941
1067
|
removeDraftTarget,
|
|
942
1068
|
flashDraftTarget,
|
|
943
1069
|
composerFocusToken,
|
|
1070
|
+
createdAnnotationIds: mintedHtmlAnnIds,
|
|
944
1071
|
};
|
|
945
1072
|
}
|