@plannotator/ui 0.29.1 → 0.31.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.
Files changed (74) hide show
  1. package/README.md +1 -0
  2. package/components/ActionMenu.tsx +6 -1
  3. package/components/AgentsTab.tsx +8 -9
  4. package/components/AnalysisLayerToggle.tsx +48 -0
  5. package/components/AnnotationPanel.tsx +68 -35
  6. package/components/AnnotationToolbar.tsx +28 -8
  7. package/components/AnnotationToolstrip.tsx +9 -0
  8. package/components/CommentPopover.tsx +212 -46
  9. package/components/ConfirmDialog.tsx +42 -28
  10. package/components/InlineMarkdown.tsx +3 -2
  11. package/components/KeyboardShortcuts.tsx +9 -0
  12. package/components/Landing.tsx +1 -1
  13. package/components/LookAndFeelAnnouncementDialog.tsx +147 -178
  14. package/components/MarkdownEditor/embedPicker.ts +349 -0
  15. package/components/MarkdownEditor.tsx +12 -0
  16. package/components/ModeToggle.tsx +2 -1
  17. package/components/PermissionModeSetup.tsx +24 -5
  18. package/components/PinpointOverlay.tsx +11 -6
  19. package/components/PlanHeaderMenu.tsx +140 -1
  20. package/components/SearchableSelect.tsx +2 -0
  21. package/components/Settings.tsx +136 -10
  22. package/components/SkillReferenceMenu.tsx +9 -0
  23. package/components/StickyHeaderLane.tsx +9 -2
  24. package/components/TableOfContents.tsx +9 -4
  25. package/components/TextShimmer.tsx +8 -5
  26. package/components/ThemeProvider.tsx +43 -1
  27. package/components/ThemeTab.tsx +52 -1
  28. package/components/Tooltip.tsx +3 -1
  29. package/components/Viewer.tsx +19 -5
  30. package/components/VimTargetReticle.tsx +12 -4
  31. package/components/ai/DocumentAIChatPanel.tsx +1 -0
  32. package/components/core/button.tsx +14 -6
  33. package/components/html-viewer/HtmlViewer.tsx +215 -39
  34. package/components/html-viewer/bridge-script.ts +384 -66
  35. package/components/html-viewer/composerYield.ts +1 -51
  36. package/components/html-viewer/useHtmlAnnotation.ts +179 -58
  37. package/components/plan-diff/PlanCleanDiffView.tsx +1 -0
  38. package/components/sidebar/FileBrowser.tsx +17 -5
  39. package/components/sidebar/SidebarContainer.tsx +124 -28
  40. package/components/ui/button.tsx +10 -8
  41. package/components/ui/dialog.tsx +35 -25
  42. package/config/index.ts +6 -1
  43. package/config/reviewView.ts +42 -9
  44. package/config/settings.ts +141 -0
  45. package/hooks/useAIProviderConfig.ts +8 -7
  46. package/hooks/useActiveSection.ts +6 -4
  47. package/hooks/useAgentJobs.ts +3 -0
  48. package/hooks/useAnnotationHighlighter.ts +20 -0
  49. package/hooks/useIsMobile.ts +37 -0
  50. package/hooks/useLinkedDoc.ts +7 -0
  51. package/hooks/useScrollViewport.ts +74 -0
  52. package/hooks/useViewportEnvironment.ts +350 -0
  53. package/package.json +3 -2
  54. package/shortcuts/index.ts +3 -0
  55. package/shortcuts/plan-review/annotationMode.shortcuts.ts +91 -0
  56. package/shortcuts/plan-review/documentView.shortcuts.ts +26 -0
  57. package/shortcuts/plan-review/htmlAnnotate.shortcuts.ts +24 -0
  58. package/styles.css +1 -1
  59. package/theme.css +229 -0
  60. package/types.ts +34 -0
  61. package/utils/annotateAgentTerminal.ts +36 -5
  62. package/utils/blockTargeting.ts +6 -3
  63. package/utils/composerYield.ts +45 -0
  64. package/utils/htmlChrome.ts +20 -16
  65. package/utils/lookAndFeelAnnouncement.ts +12 -8
  66. package/utils/markdownExtensions.ts +57 -0
  67. package/utils/parser.ts +37 -2
  68. package/utils/vimNavigation.ts +4 -1
  69. package/utils/vimScroll.ts +9 -4
  70. package/utils/wideMode.ts +20 -0
  71. package/components/PlanAIAnnouncementDialog.tsx +0 -187
  72. package/components/VimModeAnnouncementDialog.tsx +0 -557
  73. package/utils/planAIAnnouncement.ts +0 -17
  74. package/utils/vimModeAnnouncement.ts +0 -23
@@ -213,19 +213,83 @@ body[data-plannotator-vim-focus-owner]:focus {
213
213
  export const BRIDGE_SCRIPT = `(function() {
214
214
  var PREFIX = 'plannotator-bridge-';
215
215
 
216
+ // --- Live mode (proxied local app) ---
217
+ // Srcdoc sessions carry no config: LIVE stays null and every branch below is
218
+ // inert, keeping srcdoc behavior byte-for-byte identical. The proxy injects
219
+ // this script into EVERY HTML response, so a frame gate deactivates the
220
+ // bridge when the proxied page is opened directly (not framed) and inside
221
+ // nested same-origin subframes: only the frame whose parent IS the editor
222
+ // may run.
223
+ var LIVE = window.__plannotatorLiveConfig || null;
224
+ if (LIVE && (window === window.parent || window.parent !== window.top)) return;
225
+ // The server cannot know which origin form (localhost or 127.0.0.1) the
226
+ // editor tab was opened on, so live outbound messages are posted once per
227
+ // listed editor origin: the browser delivers only the post whose
228
+ // targetOrigin matches the parent document and silently drops the rest,
229
+ // so exactly one copy arrives. Inbound accepts any listed origin. Srcdoc
230
+ // keeps targetOrigin '*' and no token.
231
+ function isEditorOrigin(origin) {
232
+ if (!LIVE) return true;
233
+ var list = LIVE.editorOrigins || [];
234
+ for (var i = 0; i < list.length; i++) { if (list[i] === origin) return true; }
235
+ return false;
236
+ }
237
+ function postToParent(msg) {
238
+ if (LIVE) {
239
+ msg.token = LIVE.token;
240
+ var origins = LIVE.editorOrigins || [];
241
+ for (var o = 0; o < origins.length; o++) parent.postMessage(msg, origins[o]);
242
+ return;
243
+ }
244
+ parent.postMessage(msg, '*');
245
+ }
246
+ // Page identity for multi-page live sessions: annotations are stamped with
247
+ // the page they were made on, and restore filters to the current page.
248
+ function currentPageUrl() {
249
+ return (location.pathname + location.search).slice(0, 2048);
250
+ }
251
+ if (LIVE) {
252
+ // SPA navigation: report history changes so the parent can re-filter the
253
+ // restored set. Coalesced with a microtask flag so a pushState burst posts
254
+ // once. Full reloads need nothing: the proxy re-injects and the fresh
255
+ // document posts ready again.
256
+ var pageChangeQueued = false;
257
+ var postPageChange = function() {
258
+ if (pageChangeQueued) return;
259
+ pageChangeQueued = true;
260
+ Promise.resolve().then(function() {
261
+ pageChangeQueued = false;
262
+ postToParent({ type: PREFIX + 'page-change', pageUrl: currentPageUrl() });
263
+ });
264
+ };
265
+ var wrapHistory = function(name) {
266
+ var original = history[name];
267
+ if (typeof original !== 'function') return;
268
+ history[name] = function() {
269
+ var result = original.apply(this, arguments);
270
+ postPageChange();
271
+ return result;
272
+ };
273
+ };
274
+ wrapHistory('pushState');
275
+ wrapHistory('replaceState');
276
+ window.addEventListener('popstate', postPageChange);
277
+ }
278
+
216
279
  // --- Theme ---
217
280
  // The author owns this document. Unless it opted in to host theming
218
281
  // (hostTheme), only viewer-namespaced --pn-* properties may be written to its
219
282
  // root, and its class list is never touched.
220
283
  window.addEventListener('message', function(e) {
221
284
  if (e.source !== parent) return;
285
+ if (LIVE && (!isEditorOrigin(e.origin) || !e.data || e.data.token !== LIVE.token)) return;
222
286
  if (!e.data) return;
223
287
  if (e.data.type === PREFIX + 'set-vim-help') {
224
288
  vimHelpOpen = !!e.data.open;
225
- parent.postMessage({
289
+ postToParent({
226
290
  type: PREFIX + 'vim-help',
227
291
  open: vimHelpOpen
228
- }, '*');
292
+ });
229
293
  return;
230
294
  }
231
295
  if (e.data.type !== PREFIX + 'theme') return;
@@ -246,11 +310,12 @@ export const BRIDGE_SCRIPT = `(function() {
246
310
  // --- Resize ---
247
311
  var lastHeight = 0;
248
312
  function postResize() {
313
+ if (LIVE) return; // live surfaces render full-viewport; the parent ignores height
249
314
  if (!document.body) return;
250
315
  var h = document.body.scrollHeight;
251
316
  if (h !== lastHeight) {
252
317
  lastHeight = h;
253
- parent.postMessage({ type: PREFIX + 'resize', height: h }, '*');
318
+ postToParent({ type: PREFIX + 'resize', height: h });
254
319
  }
255
320
  }
256
321
  window.addEventListener('load', postResize);
@@ -274,7 +339,28 @@ export const BRIDGE_SCRIPT = `(function() {
274
339
  var pendingMultiTargets = []; // { key, el, anchor, label, text, box }
275
340
  var multiTargetSeq = 0;
276
341
  var MAX_MULTI_TARGETS = 16;
277
- var currentInputMethod = 'drag'; // 'drag' = text selection, 'pinpoint' = click an element
342
+ // Live mode clamps the INPUT METHOD to pinpoint (click = element). Text
343
+ // drag-selection is a separate, always-on channel — see the mouseup handler
344
+ // — so the clamp only decides what a plain click does, never whether text
345
+ // can be selected and commented.
346
+ var currentInputMethod = LIVE ? 'pinpoint' : 'drag'; // 'drag' = text selection, 'pinpoint' = click an element
347
+ // Interact/Annotate mode. While INACTIVE the bridge keeps clicks native: no
348
+ // pinpoint capture, no hover outline, no [data-annotate] click, no
349
+ // committed-highlight click interception — clicks, forms, and SPA
350
+ // navigation reach the page untouched. Text drag-selection commenting stays
351
+ // LIVE in both modes (a real drag opens the comment toolbar even in
352
+ // Interact), committed overlay artifacts stay visible in both modes, and
353
+ // marker buttons keep their own clicks. BOTH surface kinds start ARMED —
354
+ // Esc (or the header pen) drops to Interact.
355
+ var annotateModeActive = true;
356
+ function updatePinpointCursor() {
357
+ if (!document.body) return;
358
+ if (annotateModeActive && currentInputMethod === 'pinpoint') {
359
+ document.body.setAttribute('data-plannotator-pinpoint-cursor', '');
360
+ } else {
361
+ document.body.removeAttribute('data-plannotator-pinpoint-cursor');
362
+ }
363
+ }
278
364
  var pinpointHover = null;
279
365
  var vimEnabled = false;
280
366
  var vimHudEnabled = false;
@@ -295,10 +381,9 @@ export const BRIDGE_SCRIPT = `(function() {
295
381
  // and immediately clear it. This flag suppresses that one trailing clear.
296
382
  var skipNextClear = false;
297
383
 
298
- document.addEventListener('mouseup', function(e) {
299
- if (currentInputMethod === 'pinpoint') return; // pinpoint uses click, not drag-select
300
- setTimeout(handleSelection, 10);
301
- });
384
+ // Drag-selection commenting is handled by the merged capture-phase mouseup
385
+ // listener below (after the drag-yield state it reads) — it is ALWAYS live:
386
+ // both surfaces, armed or Interact, drag or pinpoint input method.
302
387
 
303
388
  // The page fully controls element text, so everything posted as a selection
304
389
  // is bounded here before it crosses the bridge (the parent enforces the same
@@ -322,7 +407,7 @@ export const BRIDGE_SCRIPT = `(function() {
322
407
  // Trailing clear from a plain-click element annotation — consume it once.
323
408
  if (skipNextClear) { skipNextClear = false; return; }
324
409
  if (pendingSelection) {
325
- parent.postMessage({ type: PREFIX + 'selection-clear' }, '*');
410
+ postToParent({ type: PREFIX + 'selection-clear' });
326
411
  pendingSelection = null;
327
412
  pendingRange = null;
328
413
  clearMultiTargets();
@@ -350,7 +435,7 @@ export const BRIDGE_SCRIPT = `(function() {
350
435
  endOffset: range.endOffset
351
436
  };
352
437
 
353
- parent.postMessage({
438
+ postToParent({
354
439
  type: PREFIX + 'selection',
355
440
  text: text,
356
441
  modeOverride: modeOverride || undefined,
@@ -359,7 +444,7 @@ export const BRIDGE_SCRIPT = `(function() {
359
444
  targetKey: (extras && extras.targetKey) || undefined,
360
445
  targetLabel: (extras && extras.targetLabel) || undefined,
361
446
  rect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height }
362
- }, '*');
447
+ });
363
448
  renderAnnotationOverlay(); // draft selection highlight (overlay-projected)
364
449
  return true;
365
450
  }
@@ -382,17 +467,17 @@ export const BRIDGE_SCRIPT = `(function() {
382
467
  // Drag selections keep the existing close-on-scroll-out behavior.
383
468
  if (pendingPinViaPinpoint || pendingMultiTargets.length > 0) return;
384
469
  // Selection scrolled out of view — close the toolbar (matches markdown).
385
- parent.postMessage({ type: PREFIX + 'selection-clear' }, '*');
470
+ postToParent({ type: PREFIX + 'selection-clear' });
386
471
  pendingSelection = null;
387
472
  pendingRange = null;
388
473
  clearPendingPin();
389
474
  renderAnnotationOverlay();
390
475
  return;
391
476
  }
392
- parent.postMessage({
477
+ postToParent({
393
478
  type: PREFIX + 'selection-rect',
394
479
  rect: { top: r.top, left: r.left, width: r.width, height: r.height }
395
- }, '*');
480
+ });
396
481
  }
397
482
  window.addEventListener('scroll', function() {
398
483
  if (!pendingSelection) return;
@@ -402,6 +487,7 @@ export const BRIDGE_SCRIPT = `(function() {
402
487
  // --- Mark Creation ---
403
488
  window.addEventListener('message', function(e) {
404
489
  if (e.source !== parent) return;
490
+ if (LIVE && (!isEditorOrigin(e.origin) || !e.data || e.data.token !== LIVE.token)) return;
405
491
  if (!e.data || !e.data.type) return;
406
492
  var type = e.data.type;
407
493
 
@@ -483,11 +569,11 @@ export const BRIDGE_SCRIPT = `(function() {
483
569
  e.data.anchor,
484
570
  e.data.additionalAnchors
485
571
  );
486
- parent.postMessage({
572
+ postToParent({
487
573
  type: PREFIX + 'mark-applied',
488
574
  id: e.data.id,
489
575
  success: found
490
- }, '*');
576
+ });
491
577
  }
492
578
 
493
579
  else if (type === PREFIX + 'remove-mark') {
@@ -499,6 +585,7 @@ export const BRIDGE_SCRIPT = `(function() {
499
585
  annRecords = [];
500
586
  annNumbers = null; // stale synced numbers must not leak onto future records
501
587
  focusedAnnotationId = null;
588
+ restoreFailedIds.clear();
502
589
  renderAnnotationOverlay();
503
590
  }
504
591
 
@@ -577,18 +664,44 @@ export const BRIDGE_SCRIPT = `(function() {
577
664
  }
578
665
 
579
666
  else if (type === PREFIX + 'set-input-method') {
580
- currentInputMethod = e.data.method === 'pinpoint' ? 'pinpoint' : 'drag';
667
+ // Live mode clamps the input method to pinpoint (what a plain click
668
+ // does); text drag-selection commenting stays live regardless.
669
+ currentInputMethod = (LIVE || e.data.method === 'pinpoint') ? 'pinpoint' : 'drag';
581
670
  if (currentInputMethod === 'pinpoint') {
582
671
  clearHoverHighlight(); // pinpoint owns clicks; drop the select affordance (and any pending hit test)
583
- if (document.body) document.body.setAttribute('data-plannotator-pinpoint-cursor', '');
584
672
  } else {
585
- if (document.body) document.body.removeAttribute('data-plannotator-pinpoint-cursor');
586
673
  clearPinpointHover();
587
674
  }
675
+ updatePinpointCursor(); // cursor affordance is mode-gated: never in Interact
588
676
  if (vimEnabled) updateVimUi();
589
677
  }
590
678
 
679
+ else if (type === PREFIX + 'set-annotate-mode') {
680
+ var nextAnnotateActive = e.data.active === true;
681
+ if (nextAnnotateActive !== annotateModeActive) {
682
+ annotateModeActive = nextAnnotateActive;
683
+ if (!annotateModeActive) {
684
+ // Disarm tears down every PENDING affordance; committed markers and
685
+ // highlights stay visible, and marker buttons keep their clicks.
686
+ if (pendingSelection) postToParent({ type: PREFIX + 'selection-clear' });
687
+ pendingSelection = null;
688
+ pendingRange = null;
689
+ skipNextClear = false;
690
+ clearMultiTargets();
691
+ clearPendingPin();
692
+ try { window.getSelection().removeAllRanges(); } catch (ex) {}
693
+ clearPinpointHover();
694
+ clearHoverHighlight();
695
+ renderAnnotationOverlay();
696
+ }
697
+ updatePinpointCursor();
698
+ }
699
+ }
700
+
591
701
  else if (type === PREFIX + 'set-vim-mode') {
702
+ // Vim is off in live mode: it writes classes onto author elements and
703
+ // captures keys the app needs.
704
+ if (LIVE) return;
592
705
  var wasVimEnabled = vimEnabled;
593
706
  var wasVimHudEnabled = vimHudEnabled;
594
707
  vimEnabled = e.data.enabled === true;
@@ -1095,6 +1208,7 @@ export const BRIDGE_SCRIPT = `(function() {
1095
1208
  document.addEventListener('mousemove', function(e) {
1096
1209
  lastPointer = { x: e.clientX, y: e.clientY };
1097
1210
  updateDragYield(e);
1211
+ if (!annotateModeActive) return; // Interact mode: no hover affordances
1098
1212
  if (currentInputMethod !== 'pinpoint') {
1099
1213
  // Click-to-select hover affordance (drag mode owns highlight clicks):
1100
1214
  // cheap cached-rect hit test, rAF-throttled, cleared while a draft or
@@ -1107,6 +1221,9 @@ export const BRIDGE_SCRIPT = `(function() {
1107
1221
  return;
1108
1222
  }
1109
1223
  if (vimEnabled && vimPhase !== 'inactive') return;
1224
+ // Mid-drag (text selection in progress) the element hover box is noise:
1225
+ // the drag owns the surface until mouseup resolves it.
1226
+ if (dragYieldActive) { clearPinpointHover(); return; }
1110
1227
  // Hit-test at the pointer (e.target only backstops engines without
1111
1228
  // elementFromPoint) so the same code path serves the scroll re-hit-test.
1112
1229
  updatePinpointHover(e.clientX, e.clientY, e.target);
@@ -1136,7 +1253,7 @@ export const BRIDGE_SCRIPT = `(function() {
1136
1253
  renderAnnotationOverlay();
1137
1254
  // Scroll under a stationary pointer moves the committed rects: re-run
1138
1255
  // the cached-rect hover test so the affordance tracks reality.
1139
- if (currentInputMethod !== 'pinpoint' && lastPointer && !pendingPinEl && !pendingSelection) {
1256
+ if (annotateModeActive && currentInputMethod !== 'pinpoint' && lastPointer && !pendingPinEl && !pendingSelection) {
1140
1257
  setHoverHighlight(committedHighlightIdAtCached(lastPointer.x, lastPointer.y));
1141
1258
  }
1142
1259
  positionMultiTargetBoxes();
@@ -1144,7 +1261,7 @@ export const BRIDGE_SCRIPT = `(function() {
1144
1261
  positionPinpointBox(pendingPinEl);
1145
1262
  return;
1146
1263
  }
1147
- if (currentInputMethod !== 'pinpoint') return;
1264
+ if (!annotateModeActive || currentInputMethod !== 'pinpoint') return;
1148
1265
  if (vimEnabled && vimPhase !== 'inactive') return;
1149
1266
  if (lastPointer) {
1150
1267
  updatePinpointHover(lastPointer.x, lastPointer.y, pinpointHover);
@@ -1258,15 +1375,21 @@ export const BRIDGE_SCRIPT = `(function() {
1258
1375
  }
1259
1376
  }
1260
1377
 
1261
- // Drag-selection marker yield: while a text drag is in progress in drag
1262
- // mode, placed markers drop pointer input (the same data-pn-hittest CSS the
1263
- // hit-test yield uses) so a 25px bubble sitting over the text cannot
1264
- // capture the selection mid-drag. Armed only by a >4px move with the
1265
- // primary button held from a non-overlay mousedown, so marker clicks
1266
- // (mousedown ON the marker) and plain click-to-select (no drag) are
1267
- // untouched; disarmed on mouseup or when the button is seen released.
1378
+ // Drag-selection marker yield: while a text drag is in progress, placed
1379
+ // markers drop pointer input (the same data-pn-hittest CSS the hit-test
1380
+ // yield uses) so a 25px bubble sitting over the text cannot capture the
1381
+ // selection mid-drag. Armed only by a >4px move with the primary button
1382
+ // held from a non-overlay mousedown, so marker clicks (mousedown ON the
1383
+ // marker) and plain click-to-select (no drag) are untouched; disarmed on
1384
+ // mouseup or when the button is seen released. The same >4px arming is
1385
+ // what tells the mouseup handler below that a REAL drag happened, which
1386
+ // is how drag-selection commenting stays live in pinpoint/armed mode
1387
+ // without a plain pinpoint click re-posting its own selection.
1268
1388
  var dragYieldStart = null;
1269
1389
  var dragYieldActive = false;
1390
+ // True between a drag's terminating mouseup and the next mousedown: the
1391
+ // click event that follows a completed drag must not pinpoint-annotate.
1392
+ var dragEndedClick = false;
1270
1393
  function endDragYield() {
1271
1394
  dragYieldStart = null;
1272
1395
  if (dragYieldActive) {
@@ -1291,13 +1414,37 @@ export const BRIDGE_SCRIPT = `(function() {
1291
1414
  }
1292
1415
  }
1293
1416
  document.addEventListener('mousedown', function(e) {
1294
- if (currentInputMethod === 'pinpoint') return;
1417
+ dragEndedClick = false;
1295
1418
  if (e.button !== 0) return;
1296
1419
  if (isViewerOverlayNode(e.target)) return;
1297
1420
  dragYieldStart = { x: e.clientX, y: e.clientY };
1298
1421
  }, true);
1299
1422
  document.addEventListener('mouseup', function() {
1423
+ // Text drag-selection commenting is ALWAYS live: both surfaces, armed or
1424
+ // Interact. In armed pinpoint a plain click belongs to the pinpoint
1425
+ // handler — only a drag that actually PRODUCED a text selection owns its
1426
+ // trailing click, so annotateElement's own selection is never re-posted
1427
+ // by its trailing mouseup. The >4px drift alone is NOT enough to arm the
1428
+ // click suppression: a drifted click (common on trackpads) that selected
1429
+ // nothing must stay a click, or armed pinpoint would silently swallow it
1430
+ // while the unprevented click leaks through to the page. Browsers clear
1431
+ // a prior selection on the mousedown that starts the drag, so the
1432
+ // selection observed here is the one THIS drag produced. Everywhere else
1433
+ // the classic always-schedule behavior stays: handleSelection only acts
1434
+ // on a real selection, posts the clear that dismisses a stale draft, and
1435
+ // never preventDefaults — a plain click is never swallowed.
1436
+ var dragged = dragYieldActive;
1300
1437
  endDragYield();
1438
+ var draggedSelection = false;
1439
+ if (dragged) {
1440
+ try {
1441
+ var s = window.getSelection();
1442
+ draggedSelection = !!(s && !s.isCollapsed && s.rangeCount && (s.toString() || '').trim());
1443
+ } catch (ex) {}
1444
+ }
1445
+ dragEndedClick = draggedSelection;
1446
+ if (annotateModeActive && currentInputMethod === 'pinpoint' && !draggedSelection) return;
1447
+ setTimeout(handleSelection, 10);
1301
1448
  }, true);
1302
1449
 
1303
1450
  // One record per committed annotation. Its targets are live projections;
@@ -1327,9 +1474,47 @@ export const BRIDGE_SCRIPT = `(function() {
1327
1474
  for (var i = annRecords.length - 1; i >= 0; i--) {
1328
1475
  if (annRecords[i].id === id) annRecords.splice(i, 1);
1329
1476
  }
1477
+ restoreFailedIds['delete'](id);
1330
1478
  if (focusedAnnotationId === id) focusedAnnotationId = null;
1331
1479
  }
1332
1480
 
1481
+ // Fail-closed transparency (host ask): markers for dead targets are
1482
+ // omitted, never guessed — this names WHICH annotations currently have no
1483
+ // live representation on the page (every target dead, or the restore never
1484
+ // resolved anything) so the host can tell the user instead of letting them
1485
+ // silently vanish. Emitted only when the set changes, and only from
1486
+ // complete overlay passes — budget-starved passes reschedule themselves
1487
+ // and would flap the set. restoreFailedIds carries total restore failures,
1488
+ // whose records are removed and therefore invisible to the per-pass scan.
1489
+ var lastUnanchoredKey = '[]';
1490
+ var restoreFailedIds = new Set();
1491
+ function emitUnanchored(deadRecordIds) {
1492
+ var seen = new Set();
1493
+ var combined = [];
1494
+ for (var deadIndex = 0; deadIndex < deadRecordIds.length; deadIndex++) {
1495
+ if (!seen.has(deadRecordIds[deadIndex])) {
1496
+ seen.add(deadRecordIds[deadIndex]);
1497
+ combined.push(deadRecordIds[deadIndex]);
1498
+ }
1499
+ }
1500
+ restoreFailedIds.forEach(function(failedId) {
1501
+ if (!seen.has(failedId)) {
1502
+ seen.add(failedId);
1503
+ combined.push(failedId);
1504
+ }
1505
+ });
1506
+ combined.sort();
1507
+ if (combined.length > 512) combined = combined.slice(0, 512);
1508
+ var key = JSON.stringify(combined);
1509
+ if (key === lastUnanchoredKey) return;
1510
+ lastUnanchoredKey = key;
1511
+ // postToParent, not a raw '*' post: live sessions stamp the session
1512
+ // token and post only to the listed editor origins, and the parent
1513
+ // drops untokened live messages — a raw post would silently disable
1514
+ // unanchored reporting exactly where restores fail most (live pages).
1515
+ postToParent({ type: PREFIX + 'unanchored', ids: combined });
1516
+ }
1517
+
1333
1518
  function validNormalizedPoint(p) {
1334
1519
  if (!p || typeof p.x !== 'number' || typeof p.y !== 'number') return null;
1335
1520
  if (!isFinite(p.x) || !isFinite(p.y)) return null;
@@ -1467,7 +1652,50 @@ export const BRIDGE_SCRIPT = `(function() {
1467
1652
  }
1468
1653
  }
1469
1654
  }
1470
- if (!record.targets.length) removeAnnRecord(id);
1655
+ if (!record.targets.length && LIVE) {
1656
+ // Live pages render late (lazy routes, data-dependent trees): a
1657
+ // restore that resolves nothing keeps its record, seeded with
1658
+ // unresolved placeholder targets built from the durable params, so
1659
+ // the mutation-driven reconcile re-acquires them when the elements
1660
+ // appear. Srcdoc documents are static (nothing would ever
1661
+ // re-resolve), so the record is dropped below exactly as before.
1662
+ if (anchor) {
1663
+ record.targets.push({
1664
+ kind: 'element',
1665
+ element: null,
1666
+ anchor: anchor,
1667
+ point: normalizedPointOf(anchor, null)
1668
+ });
1669
+ }
1670
+ if (originalText) {
1671
+ record.targets.push({
1672
+ kind: 'range',
1673
+ range: null,
1674
+ text: originalText,
1675
+ markerless: !!anchor
1676
+ });
1677
+ }
1678
+ if (additionalAnchors && additionalAnchors.length) {
1679
+ var lateCount = Math.min(additionalAnchors.length, MAX_MULTI_TARGETS);
1680
+ for (var lateIndex = 0; lateIndex < lateCount; lateIndex++) {
1681
+ if (!additionalAnchors[lateIndex]) continue;
1682
+ record.targets.push({
1683
+ kind: 'element',
1684
+ element: null,
1685
+ anchor: additionalAnchors[lateIndex],
1686
+ point: normalizedPointOf(additionalAnchors[lateIndex], null)
1687
+ });
1688
+ }
1689
+ }
1690
+ }
1691
+ if (!record.targets.length) {
1692
+ // The record is removed (nothing to retry), so the per-pass dead scan
1693
+ // cannot see this id: track it separately for the unanchored report.
1694
+ // Live sessions only reach here when the durable params seeded no
1695
+ // placeholder targets at all (nothing will ever re-resolve).
1696
+ removeAnnRecord(id);
1697
+ restoreFailedIds.add(id);
1698
+ }
1471
1699
  // rAF-coalesced render (B3): restoring N annotations posts N
1472
1700
  // find-and-mark messages, and a synchronous render here made a batch
1473
1701
  // restore O(N^2) full overlay passes. The searches above stay
@@ -2027,7 +2255,7 @@ export const BRIDGE_SCRIPT = `(function() {
2027
2255
  btn.addEventListener('click', function(clickEvent) {
2028
2256
  clickEvent.preventDefault();
2029
2257
  clickEvent.stopPropagation();
2030
- parent.postMessage({ type: PREFIX + 'mark-click', id: annId }, '*');
2258
+ postToParent({ type: PREFIX + 'mark-click', id: annId });
2031
2259
  });
2032
2260
  overlayNodes.add(btn);
2033
2261
  return btn;
@@ -2094,10 +2322,17 @@ export const BRIDGE_SCRIPT = `(function() {
2094
2322
 
2095
2323
  function renderAnnotationOverlay() {
2096
2324
  var hasDraftRange = !!(pendingSelection && pendingRange);
2097
- if (!annRecords.length && !hasDraftRange && !overlayHostEl) return;
2325
+ if (!annRecords.length && !hasDraftRange && !overlayHostEl) {
2326
+ // Nothing to project, but total restore failures must still report:
2327
+ // a session whose only annotations failed to restore never builds the
2328
+ // overlay host, and silence here would hide exactly that case.
2329
+ emitUnanchored([]);
2330
+ return;
2331
+ }
2098
2332
  ensureOverlayHost();
2099
2333
  beginDeadSearchPass();
2100
2334
  queuedHighlights.length = 0;
2335
+ var unanchoredThisPass = [];
2101
2336
  var markers = [];
2102
2337
  // Fallback numbering by first-seen registration order — used only until
2103
2338
  // the parent's ordered sync arrives. Numbering NEVER derives from target
@@ -2111,6 +2346,20 @@ export const BRIDGE_SCRIPT = `(function() {
2111
2346
  for (var recordIndex = 0; recordIndex < annRecords.length; recordIndex++) {
2112
2347
  var record = annRecords[recordIndex];
2113
2348
  refreshRecordTargets(record);
2349
+ // Live means connected/findable — clipped, offscreen, or style-hidden
2350
+ // targets are still anchored (their content exists; it just isn't
2351
+ // currently visible) and must not report as unanchored.
2352
+ var recordAnchored = false;
2353
+ for (var liveIndex = 0; liveIndex < record.targets.length; liveIndex++) {
2354
+ var liveTarget = record.targets[liveIndex];
2355
+ if (liveTarget.kind === 'element'
2356
+ ? (liveTarget.element && liveTarget.element.isConnected)
2357
+ : rangeAlive(liveTarget.range)) {
2358
+ recordAnchored = true;
2359
+ break;
2360
+ }
2361
+ }
2362
+ if (!recordAnchored) unanchoredThisPass.push(record.id);
2114
2363
  var number = annNumbers && annNumbers.has(record.id)
2115
2364
  ? annNumbers.get(record.id)
2116
2365
  : fallbackNumbers.get(record.id);
@@ -2219,6 +2468,9 @@ export const BRIDGE_SCRIPT = `(function() {
2219
2468
  // record.
2220
2469
  flushQueuedHighlights();
2221
2470
  placeMarkers(markers);
2471
+ // Budget-starved passes reschedule below and may still revive targets,
2472
+ // so only a complete pass may update the unanchored report.
2473
+ if (!deadSearchSkipped) emitUnanchored(unanchoredThisPass);
2222
2474
  // Eligible dead-target searches skipped for budget get a follow-up pass;
2223
2475
  // the loop terminates once every eligible target has been attempted at
2224
2476
  // the current generation (its failedGeneration then blocks it).
@@ -2544,7 +2796,7 @@ export const BRIDGE_SCRIPT = `(function() {
2544
2796
  clearPendingPin();
2545
2797
  try { window.getSelection().removeAllRanges(); } catch (ex) {}
2546
2798
  renderAnnotationOverlay();
2547
- if (echo) parent.postMessage({ type: PREFIX + 'multi-target-removed', key: key }, '*');
2799
+ if (echo) postToParent({ type: PREFIX + 'multi-target-removed', key: key });
2548
2800
  return;
2549
2801
  }
2550
2802
  var next = pendingMultiTargets.shift();
@@ -2564,14 +2816,14 @@ export const BRIDGE_SCRIPT = `(function() {
2564
2816
  mainBox.classList.remove('pn-pin-enter');
2565
2817
  if (pendingPinEl && pendingPinEl.isConnected) positionPinpointBox(pendingPinEl);
2566
2818
  renderAnnotationOverlay();
2567
- if (echo) parent.postMessage({ type: PREFIX + 'multi-target-removed', key: key }, '*');
2819
+ if (echo) postToParent({ type: PREFIX + 'multi-target-removed', key: key });
2568
2820
  return;
2569
2821
  }
2570
2822
  for (var i = 0; i < pendingMultiTargets.length; i++) {
2571
2823
  if (pendingMultiTargets[i].key === key) {
2572
2824
  destroyMultiTargetBox(pendingMultiTargets[i].box);
2573
2825
  pendingMultiTargets.splice(i, 1);
2574
- if (echo) parent.postMessage({ type: PREFIX + 'multi-target-removed', key: key }, '*');
2826
+ if (echo) postToParent({ type: PREFIX + 'multi-target-removed', key: key });
2575
2827
  return;
2576
2828
  }
2577
2829
  }
@@ -2614,13 +2866,13 @@ export const BRIDGE_SCRIPT = `(function() {
2614
2866
  var key = makeTargetKey();
2615
2867
  var box = createMultiTargetBox(el);
2616
2868
  pendingMultiTargets.push({ key: key, el: el, anchor: anchor, label: label, text: text, point: point, box: box });
2617
- parent.postMessage({
2869
+ postToParent({
2618
2870
  type: PREFIX + 'multi-target-added',
2619
2871
  key: key,
2620
2872
  label: label,
2621
2873
  text: text,
2622
2874
  anchor: anchor || undefined
2623
- }, '*');
2875
+ });
2624
2876
  }
2625
2877
 
2626
2878
  /** Chip hover in the composer: flash the corresponding pinned outline. */
@@ -2684,12 +2936,12 @@ export const BRIDGE_SCRIPT = `(function() {
2684
2936
  pointerRelayRaf = requestAnimationFrame(function() {
2685
2937
  pointerRelayRaf = 0;
2686
2938
  if (!pendingPinEl || !pointerRelayPos) return;
2687
- parent.postMessage({
2939
+ postToParent({
2688
2940
  type: PREFIX + 'pointer',
2689
2941
  x: pointerRelayPos.x,
2690
2942
  y: pointerRelayPos.y,
2691
2943
  shift: pointerRelayPos.shift
2692
- }, '*');
2944
+ });
2693
2945
  });
2694
2946
  }
2695
2947
 
@@ -2759,22 +3011,31 @@ export const BRIDGE_SCRIPT = `(function() {
2759
3011
  pendingSelection = { element: true };
2760
3012
  pendingRange = null;
2761
3013
  skipNextClear = true; // don't let this click's mouseup clear the toolbar we just opened
2762
- parent.postMessage({ type: PREFIX + 'selection', text: elText,
3014
+ postToParent({ type: PREFIX + 'selection', text: elText,
2763
3015
  modeOverride: modeOverride || undefined,
2764
3016
  anchor: pendingPinAnchor || undefined,
2765
3017
  pinpoint: !!viaPinpoint || undefined,
2766
3018
  targetKey: pendingPinKey || undefined,
2767
3019
  targetLabel: pendingPinLabel || undefined,
2768
- rect: { top: r.top, left: r.left, width: r.width, height: r.height } }, '*');
3020
+ rect: { top: r.top, left: r.left, width: r.width, height: r.height } });
2769
3021
  return true;
2770
3022
  }
2771
3023
 
2772
3024
  document.addEventListener('click', function(e) {
2773
- if (currentInputMethod !== 'pinpoint') return;
3025
+ if (!annotateModeActive || currentInputMethod !== 'pinpoint') return;
2774
3026
  // Real placed markers (and any other viewer overlay) own their clicks —
2775
3027
  // checked by IDENTITY, not selector, so a page element spoofing
2776
3028
  // [data-plannotator-marker] stays an ordinary annotatable target.
2777
3029
  if (isViewerOverlayNode(e.target)) return;
3030
+ // A drag that ended in this click owns the surface: the drag-selection
3031
+ // pass is about to post the selected text, and pinpoint-annotating the
3032
+ // element under the pointer would clobber it (annotateElement rewrites
3033
+ // the selection). Armed at mouseup only when the drag actually produced
3034
+ // a selection — a drifted click arms nothing and pins normally below —
3035
+ // and mousedown clears a prior pin's leftover selection, so stale
3036
+ // selection state never blocks the next plain re-pin click. One-shot:
3037
+ // only the drag's own trailing click is suppressed.
3038
+ if (dragEndedClick) { dragEndedClick = false; return; }
2778
3039
  // Shift-click while an ARMED pinpoint draft is open: toggle the element
2779
3040
  // in/out of the SAME draft comment instead of replacing the selection.
2780
3041
  // Unarmed drafts (modes the parent does not mirror, e.g. quickLabel)
@@ -2801,24 +3062,51 @@ export const BRIDGE_SCRIPT = `(function() {
2801
3062
  annotateElement(el, undefined, true, { x: e.clientX, y: e.clientY });
2802
3063
  }, true);
2803
3064
 
2804
- // Escape while pinpointing (outside vim, which has its own ladder): cancel a
2805
- // pending pin, else just drop the hover outline.
3065
+ // Escape ladder (outside vim, which has its own): a pending draft closes
3066
+ // first, then the hover outline clears, then Esc EXITS Annotate back to
3067
+ // Interact — the parent owns the mode, so the final rung only posts
3068
+ // annotate-exit and waits for set-annotate-mode to come back down.
3069
+ function closePendingDraft() {
3070
+ postToParent({ type: PREFIX + 'selection-clear' });
3071
+ pendingSelection = null;
3072
+ pendingRange = null;
3073
+ skipNextClear = false;
3074
+ clearMultiTargets();
3075
+ clearPendingPin();
3076
+ window.getSelection().removeAllRanges();
3077
+ renderAnnotationOverlay();
3078
+ }
2806
3079
  document.addEventListener('keydown', function(e) {
2807
3080
  if (e.key !== 'Escape' || vimEnabled) return;
3081
+ if (!annotateModeActive) {
3082
+ // Interact mode: Esc belongs to the page — except an open drag-comment
3083
+ // draft (drag-selection stays live in Interact) still closes first.
3084
+ if (pendingSelection) closePendingDraft();
3085
+ return;
3086
+ }
2808
3087
  if (pendingSelection) {
2809
- parent.postMessage({ type: PREFIX + 'selection-clear' }, '*');
2810
- pendingSelection = null;
2811
- pendingRange = null;
2812
- skipNextClear = false;
2813
- clearMultiTargets();
2814
- clearPendingPin();
2815
- window.getSelection().removeAllRanges();
2816
- renderAnnotationOverlay();
2817
- } else if (currentInputMethod === 'pinpoint') {
2818
- clearPinpointHover();
3088
+ closePendingDraft();
3089
+ } else {
3090
+ // Hover-clear is NOT a rung: the hover outline is a pointer
3091
+ // affordance, not a state the user perceives as a step, so clearing
3092
+ // it and exiting to Interact happen on the SAME press. Only an open
3093
+ // draft earns its own press.
3094
+ if (currentInputMethod === 'pinpoint' && pinpointHover) clearPinpointHover();
3095
+ postToParent({ type: PREFIX + 'annotate-exit' });
2819
3096
  }
2820
3097
  });
2821
3098
 
3099
+ // Mod+Shift+A toggles Interact/Annotate from inside the iframe (the parent
3100
+ // registers the same chord, but focus usually lives in here on live apps).
3101
+ // Capture phase so the page cannot swallow the reserved chord; the parent
3102
+ // answers with set-annotate-mode.
3103
+ document.addEventListener('keydown', function(e) {
3104
+ if (!(e.metaKey || e.ctrlKey) || !e.shiftKey || e.altKey) return;
3105
+ if (e.key !== 'a' && e.key !== 'A') return;
3106
+ e.preventDefault();
3107
+ postToParent({ type: PREFIX + 'annotate-toggle' });
3108
+ }, true);
3109
+
2822
3110
  // Author opt-in: a plain click on any element tagged [data-annotate] pops the
2823
3111
  // toolbar — no pinpoint mode. Lets an HTML doc (e.g. a flow graph) wire its own
2824
3112
  // nodes to Plannotator's toolbar. Bubble phase so the page's own click handlers
@@ -2826,6 +3114,7 @@ export const BRIDGE_SCRIPT = `(function() {
2826
3114
  // a committed highlight selects the annotation instead (the pre-overlay
2827
3115
  // handler deferred to '.annotation-highlight' the same way).
2828
3116
  document.addEventListener('click', function(e) {
3117
+ if (!annotateModeActive) return; // Interact mode: [data-annotate] stays a page element
2829
3118
  if (currentInputMethod === 'pinpoint') return; // pinpoint handler covers this
2830
3119
  if (isViewerOverlayNode(e.target)) return; // placed markers own their clicks
2831
3120
  var t = e.target && e.target.closest && e.target.closest('[data-annotate]');
@@ -2869,6 +3158,10 @@ export const BRIDGE_SCRIPT = `(function() {
2869
3158
  }
2870
3159
 
2871
3160
  document.addEventListener('click', function(e) {
3161
+ // Interact mode: highlight rects are pointer-transparent projections, so a
3162
+ // page click landing on one goes to the page. Markers (real buttons) stay
3163
+ // the affordance for opening a committed comment in Interact.
3164
+ if (!annotateModeActive) return;
2872
3165
  if (e.shiftKey) return; // shift belongs to multi-select
2873
3166
  if (isViewerOverlayNode(e.target)) return; // markers own their clicks
2874
3167
  if (pendingPinEl) return; // an open pinpoint draft owns the surface
@@ -2877,7 +3170,7 @@ export const BRIDGE_SCRIPT = `(function() {
2877
3170
  var hitId = committedHighlightAt(e.clientX, e.clientY);
2878
3171
  if (!hitId) return;
2879
3172
  e.stopPropagation();
2880
- parent.postMessage({ type: PREFIX + 'mark-click', id: hitId }, '*');
3173
+ postToParent({ type: PREFIX + 'mark-click', id: hitId });
2881
3174
  });
2882
3175
 
2883
3176
  // --- Optional Vim navigation ---
@@ -3591,10 +3884,10 @@ export const BRIDGE_SCRIPT = `(function() {
3591
3884
  if (!vimEnabled) return;
3592
3885
  if (vimHudEnabled && vimLastPostedPhase !== vimPhase) {
3593
3886
  vimLastPostedPhase = vimPhase;
3594
- parent.postMessage({
3887
+ postToParent({
3595
3888
  type: PREFIX + 'vim-state',
3596
3889
  phase: vimPhase
3597
- }, '*');
3890
+ });
3598
3891
  }
3599
3892
  var badge = document.querySelector('[data-plannotator-vim-badge]');
3600
3893
  if (!vimHudEnabled && !badge) badge = getVimBadgeEl();
@@ -3656,10 +3949,10 @@ export const BRIDGE_SCRIPT = `(function() {
3656
3949
 
3657
3950
  function toggleVimHelp() {
3658
3951
  vimHelpOpen = !vimHelpOpen;
3659
- parent.postMessage({
3952
+ postToParent({
3660
3953
  type: PREFIX + 'vim-help',
3661
3954
  open: vimHelpOpen
3662
- }, '*');
3955
+ });
3663
3956
  }
3664
3957
 
3665
3958
  function clearVimUi() {
@@ -3679,10 +3972,10 @@ export const BRIDGE_SCRIPT = `(function() {
3679
3972
 
3680
3973
  function copyVimText(text) {
3681
3974
  if (!text) return;
3682
- parent.postMessage({
3975
+ postToParent({
3683
3976
  type: PREFIX + 'vim-copy',
3684
3977
  text: text
3685
- }, '*');
3978
+ });
3686
3979
  }
3687
3980
 
3688
3981
  function vimActionMode(key) {
@@ -3744,7 +4037,7 @@ export const BRIDGE_SCRIPT = `(function() {
3744
4037
  handled = true;
3745
4038
  } else if (key === 'Escape') {
3746
4039
  if (pendingSelection) {
3747
- parent.postMessage({ type: PREFIX + 'selection-clear' }, '*');
4040
+ postToParent({ type: PREFIX + 'selection-clear' });
3748
4041
  pendingSelection = null;
3749
4042
  pendingRange = null;
3750
4043
  restoreVimSemanticTarget();
@@ -3961,12 +4254,12 @@ export const BRIDGE_SCRIPT = `(function() {
3961
4254
  vimLastActionId = vimActionId;
3962
4255
  vimLastActionContext = vimCommandContext;
3963
4256
  updateVimReticle();
3964
- parent.postMessage({
4257
+ postToParent({
3965
4258
  type: PREFIX + 'vim-command',
3966
4259
  actionId: vimActionId,
3967
4260
  key: hudKey,
3968
4261
  context: vimCommandContext
3969
- }, '*');
4262
+ });
3970
4263
  }
3971
4264
  e.preventDefault();
3972
4265
  e.stopImmediatePropagation();
@@ -4000,7 +4293,7 @@ export const BRIDGE_SCRIPT = `(function() {
4000
4293
  if (e.metaKey || e.ctrlKey || e.altKey) return;
4001
4294
  if (!e.key || e.key.length !== 1) return; // single printable char only
4002
4295
  e.preventDefault();
4003
- parent.postMessage({ type: PREFIX + 'keytype', key: e.key }, '*');
4296
+ postToParent({ type: PREFIX + 'keytype', key: e.key });
4004
4297
  // Hand keyboard focus back to the parent window so the comment textarea can
4005
4298
  // take it. Blurring the <iframe> from the parent isn't enough — the inner
4006
4299
  // document keeps focus — so the iframe must relinquish it. parent.focus() is
@@ -4241,7 +4534,13 @@ export const BRIDGE_SCRIPT = `(function() {
4241
4534
  }).observe(document.body);
4242
4535
  }
4243
4536
  watchPageMutations();
4244
- parent.postMessage({ type: PREFIX + 'ready' }, '*');
4537
+ // Armed is the default on both surfaces, and live sessions default to
4538
+ // pinpoint: show the cursor affordance immediately instead of waiting for
4539
+ // the parent's first set-input-method/set-annotate-mode round trip.
4540
+ updatePinpointCursor();
4541
+ var readyMsg = { type: PREFIX + 'ready' };
4542
+ if (LIVE) readyMsg.pageUrl = currentPageUrl();
4543
+ postToParent(readyMsg);
4245
4544
  }
4246
4545
  if (document.readyState === 'loading') {
4247
4546
  document.addEventListener('DOMContentLoaded', onReady);
@@ -4269,3 +4568,22 @@ export const BRIDGE_SCRIPT = `(function() {
4269
4568
  }
4270
4569
  };
4271
4570
  })();`;
4571
+
4572
+ /**
4573
+ * Live-mode bootstrap, prepended to BRIDGE_SCRIPT by the annotate server when
4574
+ * composing the proxy-served bridge body. Reads the JSON config prelude
4575
+ * (window.__plannotatorLiveConfig) and installs the annotation CSS that srcdoc
4576
+ * mode splices as a <style> tag. Runs before the bridge IIFE and before its
4577
+ * MutationObserver exists, so this write never feeds the reconcile loop.
4578
+ * Same escaping rules as BRIDGE_SCRIPT: a dependency-free string constant.
4579
+ */
4580
+ export const LIVE_BRIDGE_BOOTSTRAP = `(function() {
4581
+ var config = window.__plannotatorLiveConfig;
4582
+ if (!config || typeof config.css !== 'string') return;
4583
+ try {
4584
+ var style = document.createElement('style');
4585
+ style.setAttribute('data-plannotator-live-css', '');
4586
+ style.appendChild(document.createTextNode(config.css));
4587
+ (document.head || document.documentElement).appendChild(style);
4588
+ } catch (ex) {}
4589
+ })();`;