@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.
Files changed (76) hide show
  1. package/HANDOFF.md +284 -29
  2. package/README.md +45 -5
  3. package/components/AnnotationPanel.tsx +195 -118
  4. package/components/AnnotationToolbar.tsx +17 -14
  5. package/components/AnnotationToolstrip.tsx +54 -40
  6. package/components/BlockRenderer.tsx +20 -14
  7. package/components/CodeFilePopout.tsx +132 -69
  8. package/components/CommentPopover.tsx +8 -349
  9. package/components/ComposerTokenTextarea.tsx +379 -0
  10. package/components/ExportModal.tsx +6 -1
  11. package/components/HostTitle.tsx +108 -0
  12. package/components/HtmlSurfaceControls.tsx +2 -1
  13. package/components/ImageLightbox.tsx +288 -17
  14. package/components/ImagePeek.tsx +255 -0
  15. package/components/ImageResolverContext.tsx +31 -0
  16. package/components/InlineMarkdown.tsx +123 -15
  17. package/components/LocalLinkContext.tsx +49 -0
  18. package/components/MentionPicker.tsx +51 -0
  19. package/components/MenuVersionSection.tsx +51 -25
  20. package/components/OpenInAppButton.tsx +18 -49
  21. package/components/PlanHeaderMenu.tsx +2 -8
  22. package/components/ProviderIcons.tsx +10 -4
  23. package/components/RenderedMarkdown.tsx +40 -4
  24. package/components/Settings.tsx +33 -5
  25. package/components/SkillReferenceMenu.tsx +67 -18
  26. package/components/Tooltip.tsx +51 -4
  27. package/components/Viewer.tsx +220 -49
  28. package/components/ai/AIProviderBar.tsx +11 -0
  29. package/components/ai/AskSessionSetupHint.tsx +12 -0
  30. package/components/ai/DocumentAIChatPanel.tsx +17 -5
  31. package/components/ai/SessionAskNotice.tsx +4 -3
  32. package/components/blocks/BlockquoteBlock.tsx +159 -0
  33. package/components/blocks/HtmlBlock.tsx +121 -9
  34. package/components/blocks/QuestionBlock.tsx +183 -96
  35. package/components/blocks/QuestionContext.tsx +3 -0
  36. package/components/diagram/DiagramViewer.tsx +41 -4
  37. package/components/diagram/index.ts +1 -1
  38. package/components/html-viewer/HtmlViewer.tsx +95 -3
  39. package/components/html-viewer/bridge-script.asset.js +413 -7
  40. package/components/html-viewer/bridge-script.lite.ts +1 -1
  41. package/components/html-viewer/bridge-script.ts +454 -7
  42. package/components/html-viewer/useHtmlAnnotation.ts +51 -3
  43. package/components/icons/AgentIcons.tsx +9 -7
  44. package/components/plan-diff/PlanCleanDiffView.tsx +239 -36
  45. package/components/plan-diff/PlanDiffViewer.tsx +6 -1
  46. package/configure.ts +27 -1
  47. package/hooks/useAgentJobs.ts +8 -3
  48. package/hooks/useAnnotationHighlighter.ts +128 -23
  49. package/hooks/useExternalAnnotations.ts +6 -0
  50. package/hooks/useMentionAutocomplete.ts +1 -1
  51. package/hooks/useOpenInApps.ts +88 -0
  52. package/hooks/useSkillReferenceAutocomplete.ts +7 -3
  53. package/hooks/useUpdateCheck.ts +38 -98
  54. package/hooks/useUpdateCheckSetting.ts +43 -0
  55. package/hooks/useUpdateToast.ts +101 -0
  56. package/hooks/useVimSelection.ts +9 -0
  57. package/package.json +2 -2
  58. package/shortcuts/code-review/markdownPreview.shortcuts.ts +18 -0
  59. package/shortcuts/index.ts +1 -0
  60. package/shortcuts/runtime.ts +10 -3
  61. package/styles.css +1 -1
  62. package/types.ts +48 -0
  63. package/utils/agentClosed.ts +26 -0
  64. package/utils/agentToolSetting.ts +16 -9
  65. package/utils/aiProvider.ts +7 -0
  66. package/utils/askSessionSetupHint.ts +27 -0
  67. package/utils/blockTargeting.ts +14 -3
  68. package/utils/mentions.ts +34 -5
  69. package/utils/parser.ts +65 -51
  70. package/utils/pasteInsert.ts +234 -0
  71. package/utils/planDiffEngine.ts +272 -2
  72. package/utils/skillCatalog.ts +15 -2
  73. package/utils/skillReferences.ts +50 -13
  74. package/utils/storage.ts +5 -0
  75. package/utils/textCut.ts +41 -0
  76. package/utils/updateNotice.ts +120 -0
@@ -225,6 +225,8 @@
225
225
  // marker buttons keep their own clicks. BOTH surface kinds start ARMED —
226
226
  // Esc (or the header pen) drops to Interact.
227
227
  var annotateModeActive = true;
228
+ // A native host decides link navigations itself (HtmlViewer hostNavigates).
229
+ var hostNavigates = false;
228
230
  function updatePinpointCursor() {
229
231
  if (!document.body) return;
230
232
  if (annotateModeActive && currentInputMethod === 'pinpoint') {
@@ -530,6 +532,21 @@
530
532
  flashMultiTarget(typeof e.data.key === 'string' ? e.data.key : '');
531
533
  }
532
534
 
535
+ else if (type === PREFIX + 'step-pin') {
536
+ // A host's Parent or Child (a phone's comment sheet): move the pinned
537
+ // draft to the element around it, or back in. Re-pins through
538
+ // annotateElement, so the parent sees an ordinary pinpoint selection.
539
+ stepPin(e.data.direction === 'child' ? 'child' : 'parent');
540
+ }
541
+
542
+ else if (type === PREFIX + 'reveal-pin') {
543
+ // A host whose own panel covers the page's lower part (a phone's comment
544
+ // sheet) shrank the frame: bring the pinned draft back into view.
545
+ if (pendingPinEl && pendingPinEl.isConnected) {
546
+ try { pendingPinEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch (ex) {}
547
+ }
548
+ }
549
+
533
550
  else if (type === PREFIX + 'scroll-to') {
534
551
  // Selecting an annotation scrolls its first resolved target into view
535
552
  // and flashes the overlay focus highlight over EVERY rect of EVERY
@@ -573,6 +590,10 @@
573
590
  if (vimEnabled) updateVimUi();
574
591
  }
575
592
 
593
+ else if (type === PREFIX + 'set-host-navigates') {
594
+ hostNavigates = e.data.active === true;
595
+ }
596
+
576
597
  else if (type === PREFIX + 'set-annotate-mode') {
577
598
  var nextAnnotateActive = e.data.active === true;
578
599
  if (nextAnnotateActive !== annotateModeActive) {
@@ -1100,6 +1121,14 @@
1100
1121
  // changes); shared by mousemove and the scroll/resize reconcile pass.
1101
1122
  // Per-event hit-testing only — never builds the semantic graph.
1102
1123
  function updatePinpointHover(x, y, fallbackNode) {
1124
+ // A link a plain click follows shows no pin outline unless Cmd/Ctrl is
1125
+ // held: the outline always matches what a click there will do.
1126
+ if (!pinModifierHeld && !pendingPinEl && (followableLinkAt(fallbackNode) || followableLinkAt(deepElementFromPoint(x, y)))) {
1127
+ pinpointHover = null;
1128
+ hidePinpointBox();
1129
+ hidePinpointLabel();
1130
+ return;
1131
+ }
1103
1132
  var el = pinpointLeafAt(x, y, fallbackNode);
1104
1133
  // The 25px marker bubble owns clicks over it (they select its comment),
1105
1134
  // so hover must advertise the MARKER's own identity — no annotate box,
@@ -1130,9 +1159,36 @@
1130
1159
  positionPinpointLabel(el, pinpointHoverLabel(el));
1131
1160
  }
1132
1161
 
1162
+ // Cmd or Ctrl held (the pin modifier): a link under the pointer shows the
1163
+ // pin outline and the crosshair instead of the follow pointer. Read from
1164
+ // the key itself (as the token-hover gate does) and from every pointer
1165
+ // move, so a key pressed with focus outside this frame still counts.
1166
+ var pinModifierHeld = false;
1167
+ function setPinModifier(held) {
1168
+ if (held === pinModifierHeld) return;
1169
+ pinModifierHeld = held;
1170
+ if (document.body) {
1171
+ if (held) document.body.setAttribute('data-plannotator-pin-modifier', '');
1172
+ else document.body.removeAttribute('data-plannotator-pin-modifier');
1173
+ }
1174
+ if (annotateModeActive && currentInputMethod === 'pinpoint' && lastPointer && !(vimEnabled && vimPhase !== 'inactive')) {
1175
+ updatePinpointHover(lastPointer.x, lastPointer.y, lastPointerNode || pinpointHover);
1176
+ }
1177
+ }
1178
+ document.addEventListener('keydown', function(e) {
1179
+ if (e.key === 'Meta' || e.key === 'Control') setPinModifier(true);
1180
+ }, true);
1181
+ document.addEventListener('keyup', function(e) {
1182
+ if (e.key === 'Meta' || e.key === 'Control') setPinModifier(e.metaKey || e.ctrlKey);
1183
+ }, true);
1184
+ window.addEventListener('blur', function() { setPinModifier(false); });
1185
+
1133
1186
  var lastPointer = null;
1187
+ var lastPointerNode = null;
1134
1188
  document.addEventListener('mousemove', function(e) {
1135
1189
  lastPointer = { x: e.clientX, y: e.clientY };
1190
+ lastPointerNode = e.target && e.target.nodeType === 1 ? e.target : null;
1191
+ if (!!(e.metaKey || e.ctrlKey) !== pinModifierHeld) setPinModifier(!!(e.metaKey || e.ctrlKey));
1136
1192
  updateDragYield(e);
1137
1193
  if (!annotateModeActive) return; // Interact mode: no hover affordances
1138
1194
  if (currentInputMethod !== 'pinpoint') {
@@ -3524,6 +3580,41 @@
3524
3580
  });
3525
3581
  }
3526
3582
 
3583
+ // Parent and Child for a pinned draft. The trail holds the elements stepped
3584
+ // out of, so Child goes back the way Parent came; past it, Child takes the
3585
+ // first element child. A new pin by click starts a new trail.
3586
+ var pinStepTrail = [];
3587
+ var steppingPin = false;
3588
+ function stepPin(direction) {
3589
+ if (!pendingPinEl || !pendingPinEl.isConnected) return;
3590
+ var from = pendingPinEl;
3591
+ var next = null;
3592
+ if (direction === 'parent') {
3593
+ var up = from.parentElement;
3594
+ if (up && up !== document.body && up !== document.documentElement) next = up;
3595
+ } else {
3596
+ var back = pinStepTrail[pinStepTrail.length - 1];
3597
+ if (back && back.isConnected && from.contains(back)) next = back;
3598
+ else {
3599
+ for (var child = from.firstElementChild; child; child = child.nextElementSibling) {
3600
+ if (isViewerOverlayNode(child)) continue;
3601
+ if (/^(SCRIPT|STYLE|TEMPLATE|NOSCRIPT)$/.test(child.tagName)) continue;
3602
+ next = child;
3603
+ break;
3604
+ }
3605
+ }
3606
+ }
3607
+ if (!next) return;
3608
+ var trail = pinStepTrail.slice();
3609
+ if (direction === 'parent') trail.push(from);
3610
+ else if (trail[trail.length - 1] === next) trail.pop();
3611
+ else trail = [];
3612
+ steppingPin = true;
3613
+ try { annotateElement(next, undefined, pendingPinViaPinpoint, null); }
3614
+ finally { steppingPin = false; }
3615
+ pinStepTrail = trail;
3616
+ }
3617
+
3527
3618
  // Pin an element: select its text if possible (so a <mark> can wrap it), else
3528
3619
  // post its text + box directly so the toolbar still anchors (e.g. an SVG node,
3529
3620
  // whose <text> doesn't select like HTML text). Either way the element stays
@@ -3531,6 +3622,7 @@
3531
3622
  // rides along so the annotation can restore to this exact element later.
3532
3623
  function annotateElement(el, modeOverride, viaPinpoint, clickPoint) {
3533
3624
  if (!el) return false;
3625
+ if (!steppingPin) pinStepTrail = [];
3534
3626
  pinpointHover = null;
3535
3627
  hidePinpointLabel();
3536
3628
  clearMultiTargets(); // a new primary starts a fresh draft
@@ -3606,6 +3698,230 @@
3606
3698
  return true;
3607
3699
  }
3608
3700
 
3701
+ // --- Touch: a tap on a link follows it, press-and-hold pins it ---
3702
+ // The owner's amendment to html-link-click (2026-10-20): on a phone there is
3703
+ // no Cmd/Ctrl, so while armed a TAP on a link follows it (the click handlers
3704
+ // below take it as a plain click) and a PRESS-AND-HOLD of HOLD_PIN_MS opens
3705
+ // a comment on it, pinned exactly as a Cmd-click pins it. Every surface
3706
+ // alike: srcdoc pages (annotate, plan review's HTML, the Inbox) and live apps.
3707
+ // A finger that moves past HOLD_SLOP_PX (a scroll), a second finger, or a
3708
+ // scroll cancels the hold. The release of a hold that pinned never follows
3709
+ // the link and never reaches the page: its pointerup and touchend are
3710
+ // stopped at the window (registered before a live app's own listeners),
3711
+ // touchend is cancelled so no click is synthesized, and a click that still
3712
+ // arrives is swallowed once. That swallow ends at the next touch, mouse
3713
+ // press or key, so it never eats an unrelated click.
3714
+ // While a hold is in progress, link text is not selectable (the CSS above),
3715
+ // so the platform's long-press starts no native selection under the finger;
3716
+ // Android's contextmenu, its long-press signal, fires the hold at once.
3717
+ var HOLD_PIN_MS = 500;
3718
+ var HOLD_SLOP_PX = 10;
3719
+ var HOLD_SWALLOW_MAX_MS = 10000; // a release never seen (an app stopped touchend)
3720
+ var linkHold = null; // { timer, x, y, id, target }
3721
+ // After a hold pinned: { pointerId, released, until } until its release's
3722
+ // click (if any) is swallowed or the next gesture starts.
3723
+ var heldGesture = null;
3724
+ var lastTouchPointerId = null;
3725
+ function holdArmed() {
3726
+ return annotateModeActive && currentInputMethod === 'pinpoint' && !(vimEnabled && vimPhase !== 'inactive');
3727
+ }
3728
+ function setLinkHoldStyle(on) {
3729
+ if (!document.body) return;
3730
+ if (on) document.body.setAttribute('data-plannotator-link-hold', '');
3731
+ else document.body.removeAttribute('data-plannotator-link-hold');
3732
+ }
3733
+ function cancelLinkHold() {
3734
+ if (!linkHold) return;
3735
+ clearTimeout(linkHold.timer);
3736
+ linkHold = null;
3737
+ setLinkHoldStyle(false);
3738
+ }
3739
+ function fireLinkHold() {
3740
+ var hold = linkHold;
3741
+ if (!hold) return false;
3742
+ clearTimeout(hold.timer);
3743
+ linkHold = null;
3744
+ if (!holdArmed() || !hold.target.isConnected) {
3745
+ setLinkHoldStyle(false);
3746
+ return false;
3747
+ }
3748
+ var el = resolvePinpointTargetAt(hold.x, hold.y, hold.target);
3749
+ if (!el) {
3750
+ setLinkHoldStyle(false);
3751
+ return false;
3752
+ }
3753
+ heldGesture = { pointerId: lastTouchPointerId, released: false, pointerUpSeen: false, pointerEnded: false, touchEnded: false, target: hold.target, touchId: hold.id, x: hold.x, y: hold.y, until: Date.now() + HOLD_SWALLOW_MAX_MS };
3754
+ // The same pin a Cmd-click makes: the link's own words as a text range.
3755
+ // Selection is back on for that, then off again until the finger lifts.
3756
+ setLinkHoldStyle(false);
3757
+ try { window.getSelection().removeAllRanges(); } catch (ex) {}
3758
+ annotateElement(el, undefined, true, { x: hold.x, y: hold.y });
3759
+ setLinkHoldStyle(true);
3760
+ return true;
3761
+ }
3762
+ function endHeldGesture() {
3763
+ heldGesture = null;
3764
+ }
3765
+ function touchById(list, id) {
3766
+ for (var i = 0; list && i < list.length; i++) if (list[i].identifier === id) return list[i];
3767
+ return null;
3768
+ }
3769
+ // The release of a hold is stopped, so the page never sees it end: a press
3770
+ // library (React Aria's usePress, Motion's whileTap, dnd-kit) would keep the
3771
+ // press open. In its place the page gets a pointercancel and a touchcancel
3772
+ // on the element the finger went down on, which end a press without
3773
+ // activating it. The bridge's own listeners ignore those two.
3774
+ var bridgeSynthetic = typeof WeakSet === 'function' ? new WeakSet() : null;
3775
+ function isBridgeSynthetic(e) {
3776
+ return !!(bridgeSynthetic && bridgeSynthetic.has(e));
3777
+ }
3778
+ function dispatchSynthetic(target, event) {
3779
+ if (bridgeSynthetic) bridgeSynthetic.add(event);
3780
+ try { target.dispatchEvent(event); } catch (ex) {}
3781
+ }
3782
+ function sendPointerCancel(g, pointerId) {
3783
+ if (g.pointerEnded || pointerId === null || pointerId === undefined) return;
3784
+ g.pointerEnded = true;
3785
+ var target = g.target && g.target.isConnected ? g.target : document.body;
3786
+ if (!target) return;
3787
+ var init = { bubbles: true, cancelable: false, composed: true, pointerId: pointerId, pointerType: 'touch', isPrimary: true, clientX: g.x, clientY: g.y };
3788
+ var event = null;
3789
+ try { if (typeof PointerEvent === 'function') event = new PointerEvent('pointercancel', init); } catch (ex) {}
3790
+ if (!event) {
3791
+ event = new Event('pointercancel', { bubbles: true, cancelable: false, composed: true });
3792
+ try {
3793
+ Object.defineProperty(event, 'pointerId', { value: pointerId });
3794
+ Object.defineProperty(event, 'pointerType', { value: 'touch' });
3795
+ Object.defineProperty(event, 'isPrimary', { value: true });
3796
+ } catch (ex) {}
3797
+ }
3798
+ dispatchSynthetic(target, event);
3799
+ }
3800
+ function sendTouchCancel(g) {
3801
+ if (g.touchEnded) return;
3802
+ g.touchEnded = true;
3803
+ var target = g.target && g.target.isConnected ? g.target : document.body;
3804
+ if (!target) return;
3805
+ var event = null;
3806
+ try {
3807
+ if (typeof TouchEvent === 'function' && typeof Touch === 'function') {
3808
+ var t = new Touch({ identifier: g.touchId, target: target, clientX: g.x, clientY: g.y });
3809
+ event = new TouchEvent('touchcancel', { bubbles: true, cancelable: false, composed: true, touches: [], targetTouches: [], changedTouches: [t] });
3810
+ }
3811
+ } catch (ex) { event = null; }
3812
+ if (!event) {
3813
+ event = new Event('touchcancel', { bubbles: true, cancelable: false, composed: true });
3814
+ var point = { identifier: g.touchId, target: target, clientX: g.x, clientY: g.y };
3815
+ try {
3816
+ Object.defineProperty(event, 'touches', { value: [] });
3817
+ Object.defineProperty(event, 'targetTouches', { value: [] });
3818
+ Object.defineProperty(event, 'changedTouches', { value: [point] });
3819
+ } catch (ex) {}
3820
+ }
3821
+ dispatchSynthetic(target, event);
3822
+ }
3823
+ // Pointer events come before touch events; the touch's pointerId names the
3824
+ // release's pointerup.
3825
+ window.addEventListener('pointerdown', function(e) {
3826
+ if (isBridgeSynthetic(e)) return;
3827
+ if (e.pointerType === 'touch') {
3828
+ lastTouchPointerId = e.pointerId;
3829
+ if (heldGesture && heldGesture.released) endHeldGesture();
3830
+ return;
3831
+ }
3832
+ // A mouse or pen press: whatever click comes next is its own.
3833
+ endHeldGesture();
3834
+ }, true);
3835
+ window.addEventListener('keydown', function() { if (heldGesture && heldGesture.released) endHeldGesture(); }, true);
3836
+ function heldPointer(e) {
3837
+ if (!heldGesture || e.pointerType !== 'touch') return false;
3838
+ return heldGesture.pointerId === null || e.pointerId === heldGesture.pointerId;
3839
+ }
3840
+ window.addEventListener('pointerup', function(e) {
3841
+ if (isBridgeSynthetic(e) || !heldPointer(e) || heldGesture.pointerUpSeen) return;
3842
+ heldGesture.pointerUpSeen = true;
3843
+ e.stopImmediatePropagation();
3844
+ sendPointerCancel(heldGesture, e.pointerId);
3845
+ }, true);
3846
+ // The browser ended the held pointer itself (it took the touch): nothing to stand in for.
3847
+ window.addEventListener('pointercancel', function(e) {
3848
+ if (isBridgeSynthetic(e) || !heldPointer(e)) return;
3849
+ heldGesture.pointerUpSeen = true;
3850
+ heldGesture.pointerEnded = true;
3851
+ }, true);
3852
+ window.addEventListener('touchend', function(e) {
3853
+ if (isBridgeSynthetic(e)) return;
3854
+ if (heldGesture && !heldGesture.released) {
3855
+ // The release of the hold that pinned: no click, no navigation, and
3856
+ // the page's own touchend never runs on it; it gets cancels instead.
3857
+ if (e.cancelable) e.preventDefault();
3858
+ e.stopImmediatePropagation();
3859
+ var g = heldGesture;
3860
+ g.released = true;
3861
+ g.until = Date.now() + 800;
3862
+ setLinkHoldStyle(false);
3863
+ // Engines that send touchend before pointerup: the pointer ends here
3864
+ // (its own pointerup, when it comes, is still stopped).
3865
+ sendPointerCancel(g, g.pointerId);
3866
+ sendTouchCancel(g);
3867
+ return;
3868
+ }
3869
+ cancelLinkHold(); // a tap: the click that follows it follows the link
3870
+ }, { capture: true, passive: false });
3871
+ window.addEventListener('touchcancel', function(e) {
3872
+ if (isBridgeSynthetic(e)) return;
3873
+ cancelLinkHold();
3874
+ setLinkHoldStyle(false);
3875
+ if (heldGesture) {
3876
+ heldGesture.released = true;
3877
+ heldGesture.touchEnded = true;
3878
+ heldGesture.until = Date.now() + 800;
3879
+ }
3880
+ }, true);
3881
+ document.addEventListener('touchstart', function(e) {
3882
+ if (document.body && !document.body.hasAttribute('data-plannotator-touch')) {
3883
+ document.body.setAttribute('data-plannotator-touch', '');
3884
+ }
3885
+ cancelLinkHold();
3886
+ // A new touch: whatever click comes next is its own, even if the last
3887
+ // hold's release was never seen.
3888
+ endHeldGesture();
3889
+ setLinkHoldStyle(false);
3890
+ if (!holdArmed() || !e.touches || e.touches.length !== 1) return;
3891
+ if (isViewerOverlayNode(e.target) || !followableLinkAt(e.target)) return;
3892
+ var t = e.touches[0];
3893
+ linkHold = { x: t.clientX, y: t.clientY, id: t.identifier, target: e.target, timer: 0 };
3894
+ linkHold.timer = setTimeout(fireLinkHold, HOLD_PIN_MS);
3895
+ setLinkHoldStyle(true);
3896
+ }, { capture: true, passive: true });
3897
+ document.addEventListener('touchmove', function(e) {
3898
+ if (!linkHold) return;
3899
+ var t = touchById(e.touches, linkHold.id);
3900
+ if (!t || e.touches.length !== 1 || Math.abs(t.clientX - linkHold.x) > HOLD_SLOP_PX || Math.abs(t.clientY - linkHold.y) > HOLD_SLOP_PX) {
3901
+ cancelLinkHold();
3902
+ }
3903
+ }, { capture: true, passive: true });
3904
+ window.addEventListener('scroll', cancelLinkHold, true);
3905
+ document.addEventListener('contextmenu', function(e) {
3906
+ if (linkHold) {
3907
+ e.preventDefault();
3908
+ fireLinkHold();
3909
+ return;
3910
+ }
3911
+ if (heldGesture && Date.now() < heldGesture.until) e.preventDefault();
3912
+ }, true);
3913
+ // On the window, before every other click handler of the bridge and a live
3914
+ // app's own, so the release of a hold reaches neither the link handler nor
3915
+ // the pinpoint one, nor the page (a live app would navigate).
3916
+ window.addEventListener('click', function(e) {
3917
+ if (!heldGesture) return;
3918
+ var swallow = Date.now() < heldGesture.until;
3919
+ endHeldGesture();
3920
+ if (!swallow) return;
3921
+ e.preventDefault();
3922
+ e.stopImmediatePropagation();
3923
+ }, true);
3924
+
3609
3925
  // --- Local-site link navigation (srcdoc sessions only) ---
3610
3926
  // A srcdoc document has no URL of its own: its base URL is the PARENT page's,
3611
3927
  // which is the Plannotator server. So a plain link to 02-detail.html resolves
@@ -3617,8 +3933,14 @@
3617
3933
  // The frame therefore never navigates itself. In-page fragments scroll here;
3618
3934
  // everything else is handed to the parent, which owns resolution against the
3619
3935
  // current document's directory and is the trust boundary for the href.
3620
- // Registered BEFORE the pinpoint handler and never stopping propagation, so
3621
- // an armed click still pins the link element exactly as it always did.
3936
+ // Registered BEFORE the pinpoint handler and never stopping propagation.
3937
+ //
3938
+ // One rule while armed (the owner's ruling, html-link-click, 2026-10-20):
3939
+ // a plain click on a link FOLLOWS it, exactly as in Interact; a click with
3940
+ // the platform modifier (Cmd on macOS, Ctrl elsewhere: either is taken)
3941
+ // PINS it for a comment through the pinpoint handler below, as does a
3942
+ // shift-click that adds it to an open multi-select draft. A drag that ended
3943
+ // on the link owns its click: it neither follows nor pins.
3622
3944
  //
3623
3945
  // Live app sessions are excluded outright: they navigate a real origin
3624
3946
  // through the proxy, which is the whole point of that surface.
@@ -3641,6 +3963,25 @@
3641
3963
  return true;
3642
3964
  }
3643
3965
 
3966
+ // While armed, whether a click on a link pins it instead of following it:
3967
+ // the platform modifier (Cmd or Ctrl), a shift-click adding to an open
3968
+ // multi-select draft, or the trailing click of a drag that selected text.
3969
+ function armedClickPins(e) {
3970
+ if (e.metaKey || e.ctrlKey || dragEndedClick) return true;
3971
+ return !!(e.shiftKey && pendingPinEl && multiSelectArmed && pendingSelection);
3972
+ }
3973
+
3974
+ // A link with a real destination: one a plain click follows while armed.
3975
+ // No href, "", "#" and javascript: go nowhere, so a click on them pins
3976
+ // the element as on any other (agents' mocks use <a href="#"> for buttons).
3977
+ function isFollowableHref(raw) {
3978
+ return !!raw && raw !== '#' && !/^javascript:/i.test(raw);
3979
+ }
3980
+
3981
+ function followableLinkAt(node) {
3982
+ return !!node && isFollowableHref(navigableLinkHref(node));
3983
+ }
3984
+
3644
3985
  function navigableLinkHref(node) {
3645
3986
  var el = node && node.nodeType === 1 ? node : node && node.parentElement;
3646
3987
  if (!el || !el.closest) return '';
@@ -3660,16 +4001,30 @@
3660
4001
  if (!raw) return;
3661
4002
  // The page's own scripting, not a navigation: leave it alone.
3662
4003
  if (/^javascript:/i.test(raw)) return;
4004
+ // Armed and pinning (a link that goes nowhere, the modifier, or a
4005
+ // drag's trailing click): the frame never navigates, and the pinpoint
4006
+ // handler below takes the click.
4007
+ if (annotateModeActive && currentInputMethod === 'pinpoint' && (!isFollowableHref(raw) || armedClickPins(e))) {
4008
+ e.preventDefault();
4009
+ return;
4010
+ }
3663
4011
  if (raw.charAt(0) === '#') {
3664
4012
  e.preventDefault();
3665
4013
  scrollToLocalFragment(raw.slice(1));
3666
4014
  return;
3667
4015
  }
3668
4016
  e.preventDefault();
3669
- // Armed pinpoint: the click belongs to annotation, and the capture-phase
3670
- // pinpoint handler below is about to pin this element. Navigation is
3671
- // already suppressed above, which is all this surface owes the click.
3672
- if (annotateModeActive && currentInputMethod === 'pinpoint') return;
4017
+ // Only a person's click follows a link: a page's own script calling
4018
+ // a.click() never navigates the viewer or pushes the host's history.
4019
+ if (!e.isTrusted) return;
4020
+ // A native host decides links itself: a person's tap opens the link as a
4021
+ // new window, which the host's web view hands to its own delegate (the
4022
+ // frame never navigates; the host page's CSP would refuse it anyway).
4023
+ if (hostNavigates) {
4024
+ var target = (e.target && e.target.closest) ? e.target.closest('a,area') : null;
4025
+ try { window.open(target && target.href ? target.href : raw, '_blank', 'noopener'); } catch (ex) {}
4026
+ return;
4027
+ }
3673
4028
  postToParent({ type: PREFIX + 'link-click', href: raw.slice(0, 2048) });
3674
4029
  }, true);
3675
4030
  }
@@ -3688,7 +4043,12 @@
3688
4043
  // and mousedown clears a prior pin's leftover selection, so stale
3689
4044
  // selection state never blocks the next plain re-pin click. One-shot:
3690
4045
  // only the drag's own trailing click is suppressed.
3691
- if (dragEndedClick) { dragEndedClick = false; return; }
4046
+ if (dragEndedClick) {
4047
+ dragEndedClick = false;
4048
+ // Never a navigation either (a live app's link would otherwise follow).
4049
+ if (navigableLinkHref(e.target)) e.preventDefault();
4050
+ return;
4051
+ }
3692
4052
  // Shift-click while an ARMED pinpoint draft is open: toggle the element
3693
4053
  // in/out of the SAME draft comment instead of replacing the selection.
3694
4054
  // Unarmed drafts (modes the parent does not mirror, e.g. quickLabel)
@@ -3702,6 +4062,9 @@
3702
4062
  if (multiEl) toggleMultiTarget(multiEl, { x: e.clientX, y: e.clientY });
3703
4063
  return;
3704
4064
  }
4065
+ // A plain click on a link follows it (the link handler above posted it;
4066
+ // in a live app the browser navigates for real). The modifier pins it.
4067
+ if (isFollowableHref(navigableLinkHref(e.target)) && !armedClickPins(e)) return;
3705
4068
  // Click what the hover box shows: the currently hovered element is the
3706
4069
  // target the user aimed at. Fall back to a fresh hit-test at the click
3707
4070
  // point (e.g. a click with no preceding mousemove).
@@ -3729,12 +4092,36 @@
3729
4092
  window.getSelection().removeAllRanges();
3730
4093
  renderAnnotationOverlay();
3731
4094
  }
4095
+ // An Escape in Interact that the page did not use (no handler cancelled it
4096
+ // or stopped it on its way up, focus not in one of its text fields) is
4097
+ // offered to the host as page-escape, after every handler had its turn: the
4098
+ // Inbox leaves full screen with it. The page still got the key first.
4099
+ var escapeReachedWindow = null;
4100
+ window.addEventListener('keydown', function(e) {
4101
+ if (e.key === 'Escape') escapeReachedWindow = e;
4102
+ });
4103
+ function offerUnusedEscape(e) {
4104
+ if (e.repeat || e.isComposing || e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return;
4105
+ var t = e.target;
4106
+ if (t && t.nodeType === 1) {
4107
+ var tag = t.tagName;
4108
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || t.isContentEditable) return;
4109
+ if (t.closest && t.closest('[contenteditable]:not([contenteditable="false"])')) return;
4110
+ }
4111
+ // The page's own dialog closes on this Escape (the browser's default, not a handler).
4112
+ if (document.querySelector('dialog[open]')) return;
4113
+ setTimeout(function() {
4114
+ if (escapeReachedWindow !== e || e.defaultPrevented) return;
4115
+ postToParent({ type: PREFIX + 'page-escape' });
4116
+ }, 0);
4117
+ }
3732
4118
  document.addEventListener('keydown', function(e) {
3733
4119
  if (e.key !== 'Escape' || vimEnabled) return;
3734
4120
  if (!annotateModeActive) {
3735
4121
  // Interact mode: Esc belongs to the page — except an open drag-comment
3736
4122
  // draft (drag-selection stays live in Interact) still closes first.
3737
4123
  if (pendingSelection) closePendingDraft();
4124
+ else offerUnusedEscape(e);
3738
4125
  return;
3739
4126
  }
3740
4127
  if (pendingSelection) {
@@ -3766,6 +4153,25 @@
3766
4153
  postToParent({ type: PREFIX + message });
3767
4154
  }, true);
3768
4155
 
4156
+ // A plain ] or [ while armed (Annotate): the host may step to its next or
4157
+ // previous file (the Inbox steps through a thread's attachments). Never in
4158
+ // Interact (the page owns its keys), never while the page's focus is in a
4159
+ // text field, with a modifier, on auto-repeat or mid-composition. The page
4160
+ // still receives the key; a host without a step handler ignores the
4161
+ // message. Capture phase, like the chords above.
4162
+ document.addEventListener('keydown', function(e) {
4163
+ if (e.key !== ']' && e.key !== '[') return;
4164
+ if (!annotateModeActive || vimEnabled) return;
4165
+ if (e.metaKey || e.ctrlKey || e.altKey || e.repeat || e.isComposing) return;
4166
+ var t = e.target;
4167
+ if (t && t.nodeType === 1) {
4168
+ var tag = t.tagName;
4169
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || t.isContentEditable) return;
4170
+ if (t.closest && t.closest('[contenteditable]:not([contenteditable="false"])')) return;
4171
+ }
4172
+ postToParent({ type: PREFIX + 'step', dir: e.key === ']' ? 1 : -1 });
4173
+ }, true);
4174
+
3769
4175
  // Author opt-in: a plain click on any element tagged [data-annotate] pops the
3770
4176
  // toolbar — no pinpoint mode. Lets an HTML doc (e.g. a flow graph) wire its own
3771
4177
  // nodes to Plannotator's toolbar. Bubble phase so the page's own click handlers
@@ -3,7 +3,7 @@
3
3
  // annotation CSS, protocol version and live bootstrap as ./bridge-script, with
4
4
  // the inline bridge literal stubbed out so a bundler drops it from the viewer
5
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/* Armed pinpoint over an EMBEDDED local document: the embed is one element\n * from the outer page's point of view, and the bridge is never injected into a\n * nested frame, so a click inside it would simply vanish into another document.\n * Making frames transparent to the pointer while armed is what lets that click\n * pin the <iframe>/<embed>/<object> itself. Interact (Esc, the header pen or\n * Mod+Shift+A) restores native interaction inside the embed — which is also\n * the only state a link inside it can be followed from.\n * Live-app sessions never set this attribute: they annotate a real app whose\n * own nested frames belong to it. */\nbody[data-plannotator-frame-inert] :is(iframe, frame, embed, object) {\n pointer-events: none !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";
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/* A plain click on a link with a real destination follows it while armed\n * (Cmd/Ctrl-click pins it), so such a link keeps the pointer that says so,\n * until the modifier is held. A link that goes nowhere (no href, \"\", \"#\",\n * javascript:) is not followed and keeps the crosshair. SVG links may carry\n * only xlink:href, hence the any-namespace attribute. */\nbody[data-plannotator-pinpoint-cursor]:not([data-plannotator-pin-modifier]) :is(a, area)[href]:not([href=\"\"]):not([href=\"#\"]):not([href^=\"javascript:\" i]),\nbody[data-plannotator-pinpoint-cursor]:not([data-plannotator-pin-modifier]) :is(a, area)[href]:not([href=\"\"]):not([href=\"#\"]):not([href^=\"javascript:\" i]) *,\nbody[data-plannotator-pinpoint-cursor]:not([data-plannotator-pin-modifier]) svg a:not([href])[*|href]:not([*|href=\"\"]):not([*|href=\"#\"]):not([*|href^=\"javascript:\" i]),\nbody[data-plannotator-pinpoint-cursor]:not([data-plannotator-pin-modifier]) svg a:not([href])[*|href]:not([*|href=\"\"]):not([*|href=\"#\"]):not([*|href^=\"javascript:\" i]) * {\n cursor: pointer !important;\n}\n/* Touch, armed: a tap on a link follows it and a press-and-hold pins it for\n * a comment. iOS's link callout would take that hold, so it is off on links\n * while armed, once a touch is seen (data-plannotator-touch) or on screens\n * without hover. The callout is all: link text stays selectable, so a drag\n * across a link (a trackpad, a touchscreen laptop) still quotes its words.\n * Only while a hold on a link is in progress (data-plannotator-link-hold,\n * from its touchstart to its release) is selection off too, so the platform's\n * long-press does not start a native text selection under the finger. */\nbody[data-plannotator-pinpoint-cursor][data-plannotator-touch] :is(a, area),\nbody[data-plannotator-pinpoint-cursor][data-plannotator-touch] :is(a, area) *,\nbody[data-plannotator-pinpoint-cursor][data-plannotator-touch] svg a,\nbody[data-plannotator-pinpoint-cursor][data-plannotator-touch] svg a * {\n -webkit-touch-callout: none !important;\n}\n@media (hover: none) {\n body[data-plannotator-pinpoint-cursor] :is(a, area),\n body[data-plannotator-pinpoint-cursor] :is(a, area) *,\n body[data-plannotator-pinpoint-cursor] svg a,\n body[data-plannotator-pinpoint-cursor] svg a * {\n -webkit-touch-callout: none !important;\n }\n}\nbody[data-plannotator-link-hold] :is(a, area),\nbody[data-plannotator-link-hold] :is(a, area) *,\nbody[data-plannotator-link-hold] svg a,\nbody[data-plannotator-link-hold] svg a * {\n -webkit-touch-callout: none !important;\n -webkit-user-select: none !important;\n user-select: none !important;\n}\n/* Armed pinpoint over an EMBEDDED local document: the embed is one element\n * from the outer page's point of view, and the bridge is never injected into a\n * nested frame, so a click inside it would simply vanish into another document.\n * Making frames transparent to the pointer while armed is what lets that click\n * pin the <iframe>/<embed>/<object> itself. Interact (Esc, the header pen or\n * Mod+Shift+A) restores native interaction inside the embed — which is also\n * the only state a link inside it can be followed from.\n * Live-app sessions never set this attribute: they annotate a real app whose\n * own nested frames belong to it. */\nbody[data-plannotator-frame-inert] :is(iframe, frame, embed, object) {\n pointer-events: none !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
7
  export const BRIDGE_PROTOCOL_VERSION = 1;
8
8
  export const BRIDGE_SCRIPT = "";
9
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})();";