@archie/devtools 0.0.35 → 0.0.37

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.
@@ -1258,6 +1258,11 @@
1258
1258
  // make this redundant for new indexes, but an already-built index may
1259
1259
  // still hold a stale group.)
1260
1260
  if (isInsertedSubtreeElement(inspectTarget)) {
1261
+ veTrace("shared-targets", {
1262
+ via: "inserted-subtree",
1263
+ count: 1,
1264
+ nodeId: inspectTarget.getAttribute("data-dnd-node-id") || null,
1265
+ })
1261
1266
  return [inspectTarget]
1262
1267
  }
1263
1268
 
@@ -1268,14 +1273,32 @@
1268
1273
  inspectTarget,
1269
1274
  sharedTargetIndex.sharedBySignature.get(signature) || []
1270
1275
  )
1271
- if (sharedTargets.length > 0) return sharedTargets
1276
+ if (sharedTargets.length > 0) {
1277
+ veTrace("shared-targets", {
1278
+ via: "signature",
1279
+ signature: String(signature).slice(0, 160),
1280
+ count: sharedTargets.length,
1281
+ nodeId: inspectTarget.getAttribute("data-dnd-node-id") || null,
1282
+ })
1283
+ return sharedTargets
1284
+ }
1272
1285
  }
1273
1286
 
1274
1287
  var legacyTargets = resolveLegacySharedTargets(inspectTarget)
1275
1288
  if (legacyTargets.length > 0) {
1289
+ veTrace("shared-targets", {
1290
+ via: "legacy",
1291
+ count: legacyTargets.length,
1292
+ nodeId: inspectTarget.getAttribute("data-dnd-node-id") || null,
1293
+ })
1276
1294
  return legacyTargets
1277
1295
  }
1278
1296
 
1297
+ veTrace("shared-targets", {
1298
+ via: "self-only",
1299
+ count: 1,
1300
+ nodeId: inspectTarget.getAttribute("data-dnd-node-id") || null,
1301
+ })
1279
1302
  return [inspectTarget]
1280
1303
  }
1281
1304
 
@@ -1379,10 +1402,24 @@
1379
1402
 
1380
1403
  syncBridgeStateFromWindow()
1381
1404
  if (!bridgeState.targetOrigin || bridgeState.targetOrigin === "*") {
1405
+ if (msg && msg.type && msg.type !== "ELEMENT_POSITION_UPDATE") {
1406
+ veTrace("out:" + msg.type + ":queued", {
1407
+ requestId: (msg && msg.requestId) || null,
1408
+ })
1409
+ }
1382
1410
  bridgeState.pendingParentMessages.push(msg)
1383
1411
  return
1384
1412
  }
1385
1413
 
1414
+ if (msg && msg.type && msg.type !== "ELEMENT_POSITION_UPDATE") {
1415
+ veTrace("out:" + msg.type, {
1416
+ requestId: (msg && msg.requestId) || null,
1417
+ operationId:
1418
+ (msg && msg.operationId) ||
1419
+ (msg && msg.payload && msg.payload.operationId) ||
1420
+ null,
1421
+ })
1422
+ }
1386
1423
  window.parent.postMessage(msg, bridgeState.targetOrigin)
1387
1424
  }
1388
1425
 
@@ -1455,80 +1492,207 @@
1455
1492
  }
1456
1493
  }
1457
1494
 
1495
+ /**
1496
+ * Always-on trace ring buffer, shared shape with the design-mode engine
1497
+ * (`debug-trace.ts`): reproduce a broken gesture, then read
1498
+ * `window.__ARCHIE_VE_TRACE__` / `window.__ARCHIE_VE_TRACE_DUMP__()` in
1499
+ * THIS frame's console. Console echo only with `_inspectorDebug` /
1500
+ * `__ARCHIE_VE_DEBUG__`. Payloads must be small summaries — ids, types,
1501
+ * refs — never elements or full HTML.
1502
+ */
1503
+ var VE_TRACE_MAX = 800
1504
+ function veTrace(event, data) {
1505
+ try {
1506
+ var buffer = window.__ARCHIE_VE_TRACE__
1507
+ if (!buffer) {
1508
+ buffer = window.__ARCHIE_VE_TRACE__ = []
1509
+ }
1510
+ var entry = { t: Date.now(), layer: "inspector", event: event }
1511
+ if (typeof data !== "undefined") entry.data = data
1512
+ buffer.push(entry)
1513
+ if (buffer.length > VE_TRACE_MAX) {
1514
+ buffer.splice(0, buffer.length - VE_TRACE_MAX)
1515
+ }
1516
+ if (!window.__ARCHIE_VE_TRACE_DUMP__) {
1517
+ window.__ARCHIE_VE_TRACE_DUMP__ = function () {
1518
+ return JSON.stringify(window.__ARCHIE_VE_TRACE__ || [])
1519
+ }
1520
+ }
1521
+ if (window._inspectorDebug || window.__ARCHIE_VE_DEBUG__ === true) {
1522
+ // eslint-disable-next-line no-console
1523
+ console.log("[archie-ve:inspector]", event, data || "")
1524
+ }
1525
+ } catch (e) {
1526
+ // Tracing must never break the inspector.
1527
+ }
1528
+ }
1529
+
1530
+ // ─────────────────────────────────────────────────────────────────
1531
+ // THE HANDSHAKE PAYLOAD — ONE BUILDER, THREE EMISSIONS.
1532
+ //
1533
+ // `INSPECTOR_SCRIPT_LOADED` is emitted exactly ONCE per execution of
1534
+ // this script and is never replayed. A host whose `message` listener
1535
+ // is not attached yet when it fires loses it IRRECOVERABLY, and with
1536
+ // it every `capabilities` bit — including `bindingContract`, the one
1537
+ // that decides whether a text edit on a data row is writable or gets
1538
+ // the "Regenerate this prototype" refusal. That race is real and
1539
+ // routinely lost after a host page refresh: this page is served warm
1540
+ // from the CDN while the host still resolves the async probe that
1541
+ // decides whether to mount its bridge at all.
1542
+ //
1543
+ // The cure is to make the announcement RECOVERABLE, by two
1544
+ // independent routes that both reuse THIS builder (never a
1545
+ // second, drifting literal):
1546
+ // 1. PULL — the host asks (`QUERY_CAPABILITIES`) and gets
1547
+ // `CAPABILITIES_RESULT` back, correlated by `requestId`.
1548
+ // 2. PUSH — every `SET_EDITOR_MODE`/`SYNC_EDITOR_STATE` re-announces
1549
+ // `CAPABILITIES_RESULT` (unsolicited, `requestId: null`) BEFORE
1550
+ // its `EDITOR_MODE_CHANGED`, so a host that lost the handshake
1551
+ // is healed by the message it was already going to send, without
1552
+ // knowing the new message exists.
1553
+ // Both are idempotent: they report state, they never mutate it.
1554
+ // ─────────────────────────────────────────────────────────────────
1555
+ function buildInspectorHandshakePayload() {
1556
+ return {
1557
+ // 2.2.0 (ARCH-1065): additive only — `bindingChain` on element data,
1558
+ // `QUERY_SLICE_FIELD` in the message protocol, and
1559
+ // `capabilities.bindingContract` below. No existing field changed
1560
+ // shape, so `protocolVersion` stays 1.
1561
+ // 2.3.0 (ARCH-1065 A17-support): additive only — `QUERY_COLLECTION_ORDER`
1562
+ // in the message protocol and `capabilities.collectionOrderQuery`
1563
+ // below. No existing field changed shape, so `protocolVersion` stays 1.
1564
+ // 2.4.0 (ARCH-883-KOI, CREW round): additive only — `origin` on
1565
+ // `EditorMeta`, the usage-projected `ownMetaNodeId`/`inspectorSource`/
1566
+ // `editorInstance` resolution, and `capabilities.kitOriginIdentity`
1567
+ // below. No existing field changed shape, so `protocolVersion` stays 1.
1568
+ // 2.5.0: additive only — `CONFIGURE_INSPECTOR` inbound (labels +
1569
+ // permissions), and the `CHAT_CONTEXT_REQUESTED` /
1570
+ // `ADD_COMMENT_REQUESTED` outbound pair behind the right-click menu
1571
+ // (`capabilities.contextMenuActions` below). No existing field
1572
+ // changed shape, so `protocolVersion` stays 1.
1573
+ // 2.6.0: additive only — `QUERY_CAPABILITIES` inbound, the
1574
+ // `CAPABILITIES_RESULT` outbound it answers with (also pushed
1575
+ // unsolicited on every editor-mode command), and
1576
+ // `capabilities.capabilitiesQuery` below. No existing field
1577
+ // changed shape, so `protocolVersion` stays 1.
1578
+ version: "2.6.0",
1579
+ protocolVersion: 1,
1580
+ capabilities: {
1581
+ // 2.1.1: function-valued __editorMeta props are invoked before any
1582
+ // postMessage, so meta payloads stay structured-cloneable. Hosts can
1583
+ // gate on this bit to confirm the deployed CDN artifact carries the
1584
+ // fix (the 2.1.0 URL is overwritten in place on publish).
1585
+ functionValuedEditorMeta: true,
1586
+ inspect: true,
1587
+ dndMove: true,
1588
+ dndResize: true,
1589
+ fiberInspectorSource: true,
1590
+ hostControlledHistory: true,
1591
+ arrangeStructuredHistory: true,
1592
+ arrangeStrongNodeIdentity: true,
1593
+ // Unified mode: SET_EDITOR_MODE "inspect" also enables the arrange
1594
+ // (design) runtime — selection and drag/resize coexist. Hosts MUST
1595
+ // feature-detect on this flag: a frozen 2.0.x inspector disables
1596
+ // design mode on "inspect" and needs the legacy "design" mode.
1597
+ unifiedInspectArrange: true,
1598
+ // ARCH-1065: the M2 codemod's data-agent-* grammar is readable
1599
+ // (`bindingChain` on element data) and slice-field-queryable
1600
+ // (`QUERY_SLICE_FIELD`). Hosts MUST feature-detect on this flag
1601
+ // before relying on either — a frozen pre-1065 inspector has
1602
+ // neither.
1603
+ collectionBinding: true,
1604
+ // ARCH-1065 A17-support (v2.3.0): `QUERY_COLLECTION_ORDER` resolves
1605
+ // the enclosing collection's order ("stable"/"derived") and row
1606
+ // ids/count for a target node, off the same data-agent-* grammar
1607
+ // as `collectionBinding`. Hosts MUST feature-detect on this flag —
1608
+ // a frozen pre-2.3.0 inspector has no handler for the message.
1609
+ collectionOrderQuery: true,
1610
+ // Host draw mode: the inspector can map a drawn rect to the
1611
+ // elements under it (`QUERY_ELEMENTS_IN_RECT` →
1612
+ // `ELEMENTS_IN_RECT_RESULT`) and rasterize its own viewport
1613
+ // (`CAPTURE_VIEWPORT` → `VIEWPORT_CAPTURED`). Hosts MUST
1614
+ // feature-detect: a frozen earlier inspector has no handler for
1615
+ // either message and would never reply.
1616
+ rectElementQuery: true,
1617
+ // Best-effort by construction — the foreignObject round-trip drops
1618
+ // web fonts, cross-origin images and <canvas> pixels, and answers
1619
+ // VIEWPORT_CAPTURE_FAILED when the rasterization throws.
1620
+ viewportCapture: true,
1621
+ // The BUILD-TIME contract version this specific page was stamped
1622
+ // with, forwarded verbatim from `<meta name="archie-binding-contract">`
1623
+ // when the kit build emitted one — null on any page built before
1624
+ // that meta tag existed, or that never carried collection-bound
1625
+ // data. Opaque to this script: validating/interpreting the value is
1626
+ // the host's job (mirrors how `slot` and `bindingContract` — plural
1627
+ // concept, singular mechanism — are treated as opaque strings
1628
+ // elsewhere in this file).
1629
+ bindingContract: readBindingContractMeta(),
1630
+ // ARCH-883-KOI F4 (v2.4.0) — this SCRIPT resolves `EditorMeta.origin`
1631
+ // and projects identity across an instrumented kit boundary
1632
+ // (`ownMetaNodeId`, `projectUsageMetaFromElement`,
1633
+ // `getInspectorInfoFromElement`, `resolveInstanceIdentity`). Hosts
1634
+ // MUST feature-detect on this flag before trusting `origin`
1635
+ // (absent or `editorMeta.origin === 'kit'`) as meaningful: a frozen
1636
+ // pre-2.4.0 inspector never reads the field at all, so its
1637
+ // `inspectorSource`/`editorInstance` on an instrumented-kit page
1638
+ // reproduce the exact F1 bug this version fixes.
1639
+ //
1640
+ // What this bit CANNOT tell the host: whether the PAGE's own build
1641
+ // emitted `origin` on its metas in the first place — that is the
1642
+ // `@archie/devtools` version the acg/kitgen esbuild path had
1643
+ // installed when the screen (or the kit) was built, a completely
1644
+ // separate axis from this CDN script's version (see the `origin`
1645
+ // field's docstring in `types/editor-meta.ts` for the full
1646
+ // reasoning, and why NO per-node signal can close that gap).
1647
+ kitOriginIdentity: true,
1648
+ // v2.5.0 — right-click menu in navigate AND inspect mode, emitting
1649
+ // `CHAT_CONTEXT_REQUESTED` (with the node's full metadata) and
1650
+ // `ADD_COMMENT_REQUESTED` (with the anchor target). Labels and the
1651
+ // chat-entry permission gate arrive via `CONFIGURE_INSPECTOR`.
1652
+ // Hosts MUST feature-detect: a frozen pre-2.5.0 inspector ignores
1653
+ // `CONFIGURE_INSPECTOR` and never emits either message.
1654
+ contextMenuActions: true,
1655
+ // v2.6.0 — the handshake is RECOVERABLE: `QUERY_CAPABILITIES`
1656
+ // answers with `CAPABILITIES_RESULT` carrying this exact payload,
1657
+ // and every `SET_EDITOR_MODE`/`SYNC_EDITOR_STATE` re-announces it
1658
+ // unsolicited. Hosts MUST feature-detect before WAITING on a
1659
+ // reply — a frozen pre-2.6.0 inspector has no handler for
1660
+ // `QUERY_CAPABILITIES` and never answers — but a host that lost
1661
+ // the one-shot handshake has no capabilities to detect ON, so
1662
+ // asking blindly (and treating silence as "still unknown", never
1663
+ // as "reported empty") is the correct posture.
1664
+ capabilitiesQuery: true,
1665
+ },
1666
+ }
1667
+ }
1668
+
1669
+ /**
1670
+ * Emit the capabilities payload as `CAPABILITIES_RESULT`.
1671
+ *
1672
+ * `requestId` echoes the pull's correlation id, or is `null` for the
1673
+ * unsolicited push. `reason` names which route produced it, purely
1674
+ * for host-side tracing.
1675
+ */
1676
+ function announceInspectorCapabilities(requestId, reason) {
1677
+ var handshake = buildInspectorHandshakePayload()
1678
+ postMessageToParent({
1679
+ type: "CAPABILITIES_RESULT",
1680
+ requestId:
1681
+ typeof requestId === "string" && requestId !== "" ? requestId : null,
1682
+ reason: typeof reason === "string" && reason !== "" ? reason : null,
1683
+ version: handshake.version,
1684
+ protocolVersion: handshake.protocolVersion,
1685
+ capabilities: handshake.capabilities,
1686
+ })
1687
+ }
1688
+
1689
+ var initialHandshake = buildInspectorHandshakePayload()
1458
1690
  postMessageToParent({
1459
1691
  type: "INSPECTOR_SCRIPT_LOADED",
1460
1692
  timestamp: Date.now(),
1461
- // 2.2.0 (ARCH-1065): additive only — `bindingChain` on element data,
1462
- // `QUERY_SLICE_FIELD` in the message protocol, and
1463
- // `capabilities.bindingContract` below. No existing field changed
1464
- // shape, so `protocolVersion` stays 1.
1465
- // 2.3.0 (ARCH-1065 A17-support): additive only — `QUERY_COLLECTION_ORDER`
1466
- // in the message protocol and `capabilities.collectionOrderQuery`
1467
- // below. No existing field changed shape, so `protocolVersion` stays 1.
1468
- // 2.4.0 (ARCH-883-KOI, CREW round): additive only — `origin` on
1469
- // `EditorMeta`, the usage-projected `ownMetaNodeId`/`inspectorSource`/
1470
- // `editorInstance` resolution, and `capabilities.kitOriginIdentity`
1471
- // below. No existing field changed shape, so `protocolVersion` stays 1.
1472
- version: "2.4.0",
1473
- protocolVersion: 1,
1474
- capabilities: {
1475
- // 2.1.1: function-valued __editorMeta props are invoked before any
1476
- // postMessage, so meta payloads stay structured-cloneable. Hosts can
1477
- // gate on this bit to confirm the deployed CDN artifact carries the
1478
- // fix (the 2.1.0 URL is overwritten in place on publish).
1479
- functionValuedEditorMeta: true,
1480
- inspect: true,
1481
- dndMove: true,
1482
- dndResize: true,
1483
- fiberInspectorSource: true,
1484
- hostControlledHistory: true,
1485
- arrangeStructuredHistory: true,
1486
- arrangeStrongNodeIdentity: true,
1487
- // Unified mode: SET_EDITOR_MODE "inspect" also enables the arrange
1488
- // (design) runtime — selection and drag/resize coexist. Hosts MUST
1489
- // feature-detect on this flag: a frozen 2.0.x inspector disables
1490
- // design mode on "inspect" and needs the legacy "design" mode.
1491
- unifiedInspectArrange: true,
1492
- // ARCH-1065: the M2 codemod's data-agent-* grammar is readable
1493
- // (`bindingChain` on element data) and slice-field-queryable
1494
- // (`QUERY_SLICE_FIELD`). Hosts MUST feature-detect on this flag
1495
- // before relying on either — a frozen pre-1065 inspector has
1496
- // neither.
1497
- collectionBinding: true,
1498
- // ARCH-1065 A17-support (v2.3.0): `QUERY_COLLECTION_ORDER` resolves
1499
- // the enclosing collection's order ("stable"/"derived") and row
1500
- // ids/count for a target node, off the same data-agent-* grammar
1501
- // as `collectionBinding`. Hosts MUST feature-detect on this flag —
1502
- // a frozen pre-2.3.0 inspector has no handler for the message.
1503
- collectionOrderQuery: true,
1504
- // The BUILD-TIME contract version this specific page was stamped
1505
- // with, forwarded verbatim from `<meta name="archie-binding-contract">`
1506
- // when the kit build emitted one — null on any page built before
1507
- // that meta tag existed, or that never carried collection-bound
1508
- // data. Opaque to this script: validating/interpreting the value is
1509
- // the host's job (mirrors how `slot` and `bindingContract` — plural
1510
- // concept, singular mechanism — are treated as opaque strings
1511
- // elsewhere in this file).
1512
- bindingContract: readBindingContractMeta(),
1513
- // ARCH-883-KOI F4 (v2.4.0) — this SCRIPT resolves `EditorMeta.origin`
1514
- // and projects identity across an instrumented kit boundary
1515
- // (`ownMetaNodeId`, `projectUsageMetaFromElement`,
1516
- // `getInspectorInfoFromElement`, `resolveInstanceIdentity`). Hosts
1517
- // MUST feature-detect on this flag before trusting `origin`
1518
- // (absent or `editorMeta.origin === 'kit'`) as meaningful: a frozen
1519
- // pre-2.4.0 inspector never reads the field at all, so its
1520
- // `inspectorSource`/`editorInstance` on an instrumented-kit page
1521
- // reproduce the exact F1 bug this version fixes.
1522
- //
1523
- // What this bit CANNOT tell the host: whether the PAGE's own build
1524
- // emitted `origin` on its metas in the first place — that is the
1525
- // `@archie/devtools` version the acg/kitgen esbuild path had
1526
- // installed when the screen (or the kit) was built, a completely
1527
- // separate axis from this CDN script's version (see the `origin`
1528
- // field's docstring in `types/editor-meta.ts` for the full
1529
- // reasoning, and why NO per-node signal can close that gap).
1530
- kitOriginIdentity: true,
1531
- },
1693
+ version: initialHandshake.version,
1694
+ protocolVersion: initialHandshake.protocolVersion,
1695
+ capabilities: initialHandshake.capabilities,
1532
1696
  })
1533
1697
 
1534
1698
  if (window._inspectorMessageHandler) {
@@ -1618,6 +1782,10 @@
1618
1782
  element === selectedElementBadge ||
1619
1783
  element.id === "inspector-ping-effect" ||
1620
1784
  (element.closest && element.closest("[data-inspector-badge]")) ||
1785
+ // Right-click menu: its buttons must survive setupEventBlockers'
1786
+ // "click" blocker and never be treated as an inspect target.
1787
+ (element.closest &&
1788
+ element.closest("[data-inspector-context-menu]")) ||
1621
1789
  // Design-mode (arrange) overlay elements: HitArea, resize handles,
1622
1790
  // drop-line, etc. These belong to the unified visual editor and must
1623
1791
  // pass mousedown/click events through to React (dnd-kit) instead of
@@ -2152,6 +2320,15 @@
2152
2320
  }
2153
2321
 
2154
2322
  function setEditorModeFromHost(mode, options) {
2323
+ // PUSH route of the recoverable handshake (see
2324
+ // `buildInspectorHandshakePayload`). Emitted BEFORE the
2325
+ // `EDITOR_MODE_CHANGED` echo below — deliberately, not incidentally:
2326
+ // a host that lost the one-shot `INSPECTOR_SCRIPT_LOADED` refuses
2327
+ // every inbound message until it knows who it is talking to, so the
2328
+ // capabilities have to land FIRST or the very `EDITOR_MODE_CHANGED`
2329
+ // this command exists to confirm would be the message dropped.
2330
+ announceInspectorCapabilities(null, "editor-mode")
2331
+
2155
2332
  var historyMode =
2156
2333
  options && options.hostControlledHistory ? "host" : "local"
2157
2334
 
@@ -2658,9 +2835,338 @@
2658
2835
  } catch (e) {}
2659
2836
  }
2660
2837
 
2838
+ veTrace("resolve-mutation-element", {
2839
+ selector: hasSelector ? selector : null,
2840
+ inspectorSource:
2841
+ inspectorSource && typeof inspectorSource === "object"
2842
+ ? (inspectorSource.file || "") +
2843
+ ":" +
2844
+ (inspectorSource.line || "") +
2845
+ ":" +
2846
+ (inspectorSource.column || "")
2847
+ : null,
2848
+ matchedSelector: !!selectorElement,
2849
+ resolvedVia: !element
2850
+ ? "none"
2851
+ : element === selectorElement
2852
+ ? "selector"
2853
+ : "inspector-source-or-fallback",
2854
+ resolvedNodeId: element
2855
+ ? element.getAttribute("data-dnd-node-id") || null
2856
+ : null,
2857
+ resolvedSourceRef: element
2858
+ ? element.getAttribute("data-archie-source-ref") || null
2859
+ : null,
2860
+ resolvedTag: element ? String(element.tagName || "").toLowerCase() : null,
2861
+ })
2862
+
2661
2863
  return element
2662
2864
  }
2663
2865
 
2866
+ // ── Annotation support (host draw mode) ──────────────────────────────
2867
+ // Two OPTIONAL queries the host uses to turn a drawing over the preview
2868
+ // into actionable context. Both are advertised as capabilities
2869
+ // (`rectElementQuery`, `viewportCapture`); a host that sees neither
2870
+ // degrades to sending the raw drawing, so failures here must never
2871
+ // throw into the shared message handler.
2872
+
2873
+ var ANNOTATION_MAX_ELEMENTS = 8
2874
+ var ANNOTATION_CAPTURE_TIMEOUT_MS = 5000
2875
+ var ANNOTATION_MIN_SIDE_PX = 4
2876
+ var ANNOTATION_SKIP_TAGS = {
2877
+ HTML: true,
2878
+ HEAD: true,
2879
+ SCRIPT: true,
2880
+ STYLE: true,
2881
+ META: true,
2882
+ LINK: true,
2883
+ TITLE: true,
2884
+ BR: true,
2885
+ }
2886
+
2887
+ function annotationIntersection(a, b) {
2888
+ var x = Math.max(a.left, b.x)
2889
+ var y = Math.max(a.top, b.y)
2890
+ var right = Math.min(a.right, b.x + b.width)
2891
+ var bottom = Math.min(a.bottom, b.y + b.height)
2892
+ if (right <= x || bottom <= y) return 0
2893
+ return (right - x) * (bottom - y)
2894
+ }
2895
+
2896
+ /**
2897
+ * Elements intersecting a viewport-relative rect, most specific first.
2898
+ *
2899
+ * "Most specific" is deliberately NOT smallest-area: a mark drawn around
2900
+ * a card should report the card, not the innermost text node inside it.
2901
+ * Ranking therefore puts nodes carrying an inspector source (i.e. ones
2902
+ * the agent can actually edit) ahead of anonymous wrappers, then favours
2903
+ * higher coverage of the drawn rect.
2904
+ */
2905
+ function collectElementsInRect(rect) {
2906
+ if (
2907
+ !rect ||
2908
+ typeof rect.x !== "number" ||
2909
+ typeof rect.y !== "number" ||
2910
+ !(rect.width > 0) ||
2911
+ !(rect.height > 0)
2912
+ ) {
2913
+ return []
2914
+ }
2915
+
2916
+ var all = _rawDocumentQuerySelectorAll.call(document, "body *")
2917
+ var scored = []
2918
+
2919
+ for (var i = 0; i < all.length; i++) {
2920
+ var el = all[i]
2921
+ if (ANNOTATION_SKIP_TAGS[el.tagName]) continue
2922
+ if (el.closest && el.closest("[data-inspector-badge]")) continue
2923
+
2924
+ var box = el.getBoundingClientRect()
2925
+ if (
2926
+ box.width < ANNOTATION_MIN_SIDE_PX ||
2927
+ box.height < ANNOTATION_MIN_SIDE_PX
2928
+ ) {
2929
+ continue
2930
+ }
2931
+
2932
+ var overlap = annotationIntersection(box, rect)
2933
+ if (overlap <= 0) continue
2934
+
2935
+ var style = window.getComputedStyle(el)
2936
+ if (
2937
+ style.visibility === "hidden" ||
2938
+ style.display === "none" ||
2939
+ style.opacity === "0"
2940
+ ) {
2941
+ continue
2942
+ }
2943
+
2944
+ var elementArea = box.width * box.height
2945
+ var rectArea = rect.width * rect.height
2946
+ // Skip nodes the mark barely grazes, and page-sized wrappers the
2947
+ // mark covers a negligible fraction of.
2948
+ if (overlap / elementArea < 0.25 && overlap / rectArea < 0.5) continue
2949
+
2950
+ var info = null
2951
+ try {
2952
+ info = getInspectorInfoFromElement(el)
2953
+ } catch (error) {
2954
+ info = null
2955
+ }
2956
+
2957
+ scored.push({
2958
+ element: el,
2959
+ info: info,
2960
+ hasSource: info && info.file ? 1 : 0,
2961
+ coverage: overlap / rectArea,
2962
+ })
2963
+ }
2964
+
2965
+ scored.sort(function (a, b) {
2966
+ if (a.hasSource !== b.hasSource) return b.hasSource - a.hasSource
2967
+ return b.coverage - a.coverage
2968
+ })
2969
+
2970
+ var result = []
2971
+ var seenSelectors = {}
2972
+ for (var j = 0; j < scored.length; j++) {
2973
+ if (result.length >= ANNOTATION_MAX_ELEMENTS) break
2974
+ var entry = scored[j]
2975
+ var selector = getStableSelector(entry.element)
2976
+ if (!selector || seenSelectors[selector]) continue
2977
+ seenSelectors[selector] = true
2978
+
2979
+ var box2 = entry.element.getBoundingClientRect()
2980
+ var text = entry.element.textContent
2981
+ result.push({
2982
+ cssSelector: selector,
2983
+ tagName: String(entry.element.tagName || "").toLowerCase(),
2984
+ textPreview: text ? text.trim().slice(0, 120) : "",
2985
+ sourceRef:
2986
+ entry.info && entry.info.file
2987
+ ? entry.info.file +
2988
+ ":" +
2989
+ (entry.info.line || 0) +
2990
+ ":" +
2991
+ (entry.info.column || 0)
2992
+ : undefined,
2993
+ componentName:
2994
+ entry.info && entry.info.componentName
2995
+ ? entry.info.componentName
2996
+ : undefined,
2997
+ boundingRect: {
2998
+ x: box2.x,
2999
+ y: box2.y,
3000
+ width: box2.width,
3001
+ height: box2.height,
3002
+ },
3003
+ })
3004
+ }
3005
+
3006
+ return result
3007
+ }
3008
+
3009
+ /** Same-origin CSS text, concatenated. Cross-origin sheets throw on
3010
+ * `cssRules` and are skipped — their styling is simply absent from the
3011
+ * capture, which is why the host treats it as best-effort. */
3012
+ function collectSameOriginCss() {
3013
+ var chunks = []
3014
+ for (var i = 0; i < document.styleSheets.length; i++) {
3015
+ var sheet = document.styleSheets[i]
3016
+ var rules = null
3017
+ try {
3018
+ rules = sheet.cssRules
3019
+ } catch (error) {
3020
+ continue
3021
+ }
3022
+ if (!rules) continue
3023
+ for (var j = 0; j < rules.length; j++) {
3024
+ chunks.push(rules[j].cssText)
3025
+ }
3026
+ }
3027
+ return chunks.join("\n")
3028
+ }
3029
+
3030
+ function fetchAsDataUrl(url) {
3031
+ return fetch(url, { mode: "same-origin" })
3032
+ .then(function (response) {
3033
+ if (!response.ok) throw new Error("bad status")
3034
+ return response.blob()
3035
+ })
3036
+ .then(function (blob) {
3037
+ return new Promise(function (resolve, reject) {
3038
+ var reader = new FileReader()
3039
+ reader.onload = function () {
3040
+ resolve(String(reader.result))
3041
+ }
3042
+ reader.onerror = reject
3043
+ reader.readAsDataURL(blob)
3044
+ })
3045
+ })
3046
+ }
3047
+
3048
+ /** Rasterizing an SVG cannot fetch subresources, so same-origin <img>
3049
+ * sources are inlined first. Failures leave the image blank rather
3050
+ * than aborting the whole capture. */
3051
+ function inlineImages(root) {
3052
+ var images = root.querySelectorAll("img")
3053
+ var jobs = []
3054
+ for (var i = 0; i < images.length; i++) {
3055
+ ;(function (img) {
3056
+ var src = img.getAttribute("src")
3057
+ if (!src || src.indexOf("data:") === 0) return
3058
+ var absolute
3059
+ try {
3060
+ absolute = new URL(src, document.baseURI)
3061
+ } catch (error) {
3062
+ return
3063
+ }
3064
+ if (absolute.origin !== window.location.origin) return
3065
+ jobs.push(
3066
+ fetchAsDataUrl(absolute.href)
3067
+ .then(function (dataUrl) {
3068
+ img.setAttribute("src", dataUrl)
3069
+ })
3070
+ .catch(function () {
3071
+ img.removeAttribute("src")
3072
+ })
3073
+ )
3074
+ })(images[i])
3075
+ }
3076
+ return Promise.all(jobs)
3077
+ }
3078
+
3079
+ /**
3080
+ * Rasterizes the CURRENT viewport into a PNG data URL via an
3081
+ * `<svg><foreignObject>` round-trip. Best-effort by construction: web
3082
+ * fonts, cross-origin images and <canvas> pixels do not survive it. The
3083
+ * host composes the result with the user's drawing, and falls back to
3084
+ * the drawing alone when this rejects.
3085
+ */
3086
+ function captureViewportDataUrl(pixelRatio) {
3087
+ var width = window.innerWidth
3088
+ var height = window.innerHeight
3089
+ var ratio = Math.min(Math.max(Number(pixelRatio) || 1, 1), 2)
3090
+
3091
+ var clone = document.documentElement.cloneNode(true)
3092
+ var strip = clone.querySelectorAll(
3093
+ "script, noscript, [data-inspector-badge], #archie-inspector-styles"
3094
+ )
3095
+ for (var i = 0; i < strip.length; i++) {
3096
+ if (strip[i].parentNode) strip[i].parentNode.removeChild(strip[i])
3097
+ }
3098
+
3099
+ var styleEl = document.createElement("style")
3100
+ styleEl.textContent = collectSameOriginCss()
3101
+ var head = clone.querySelector("head")
3102
+ if (head) head.appendChild(styleEl)
3103
+ else clone.insertBefore(styleEl, clone.firstChild)
3104
+
3105
+ return inlineImages(clone).then(function () {
3106
+ var markup = new XMLSerializer().serializeToString(clone)
3107
+ var svg =
3108
+ '<svg xmlns="http://www.w3.org/2000/svg" width="' +
3109
+ width +
3110
+ '" height="' +
3111
+ height +
3112
+ '">' +
3113
+ '<foreignObject x="0" y="0" width="' +
3114
+ width +
3115
+ '" height="' +
3116
+ height +
3117
+ '">' +
3118
+ '<div xmlns="http://www.w3.org/1999/xhtml" style="width:' +
3119
+ width +
3120
+ "px;transform:translate(" +
3121
+ -window.scrollX +
3122
+ "px," +
3123
+ -window.scrollY +
3124
+ 'px)">' +
3125
+ markup +
3126
+ "</div></foreignObject></svg>"
3127
+
3128
+ return new Promise(function (resolve, reject) {
3129
+ var image = new Image()
3130
+ // An SVG the browser refuses to decode can fire NEITHER onload nor
3131
+ // onerror (malformed serialization, blocked subresource). Without
3132
+ // this guard the host would sit on the request until its own
3133
+ // timeout with no diagnostic; the deadline turns that into an
3134
+ // explicit VIEWPORT_CAPTURE_FAILED.
3135
+ var settled = false
3136
+ var deadline = setTimeout(function () {
3137
+ if (settled) return
3138
+ settled = true
3139
+ reject(new Error("svg rasterization timed out"))
3140
+ }, ANNOTATION_CAPTURE_TIMEOUT_MS)
3141
+ var finish = function (fn, value) {
3142
+ if (settled) return
3143
+ settled = true
3144
+ clearTimeout(deadline)
3145
+ fn(value)
3146
+ }
3147
+ image.onload = function () {
3148
+ try {
3149
+ var canvas = document.createElement("canvas")
3150
+ canvas.width = Math.round(width * ratio)
3151
+ canvas.height = Math.round(height * ratio)
3152
+ var context = canvas.getContext("2d")
3153
+ context.fillStyle = "#ffffff"
3154
+ context.fillRect(0, 0, canvas.width, canvas.height)
3155
+ context.drawImage(image, 0, 0, canvas.width, canvas.height)
3156
+ finish(resolve, canvas.toDataURL("image/png"))
3157
+ } catch (error) {
3158
+ finish(reject, error)
3159
+ }
3160
+ }
3161
+ image.onerror = function () {
3162
+ finish(reject, new Error("svg rasterization failed"))
3163
+ }
3164
+ image.src =
3165
+ "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg)
3166
+ })
3167
+ })
3168
+ }
3169
+
2664
3170
  function extractMetadata(element) {
2665
3171
  if (!element) return null
2666
3172
 
@@ -3207,6 +3713,10 @@
3207
3713
  return
3208
3714
  }
3209
3715
 
3716
+ veTrace("runtime-event:" + detail.type, {
3717
+ operationId: detail.operationId || null,
3718
+ })
3719
+
3210
3720
  if (detail.type === "design-mode-changed") {
3211
3721
  // In unified inspect the arrange overlay flips its internal
3212
3722
  // design-mode store as a side effect ("design" on mount, "off" on
@@ -3216,6 +3726,9 @@
3216
3726
  // only needs runtime-originated mode changes in the legacy
3217
3727
  // design-only flow.
3218
3728
  if (hostCommandedEditorMode === "inspect") {
3729
+ veTrace("runtime-event:design-mode-changed:suppressed", {
3730
+ hostCommandedEditorMode: hostCommandedEditorMode,
3731
+ })
3219
3732
  return
3220
3733
  }
3221
3734
  postMessageToParent({
@@ -3304,9 +3817,28 @@
3304
3817
  event.data.type,
3305
3818
  event.data
3306
3819
  )
3820
+ if (event.data.type !== "EXTERNAL_DRAG_MOVE") {
3821
+ veTrace("in:" + event.data.type, {
3822
+ requestId: event.data.requestId || null,
3823
+ selector: event.data.selector || null,
3824
+ })
3825
+ }
3307
3826
  }
3308
3827
 
3309
- if (event.data && event.data.type === "SET_EDITOR_MODE") {
3828
+ if (event.data && event.data.type === "CONFIGURE_INSPECTOR") {
3829
+ // Origin adoption already happened above; this carries the
3830
+ // right-click menu's i18n labels and permission gate.
3831
+ applyInspectorConfiguration(event.data)
3832
+ } else if (event.data && event.data.type === "QUERY_CAPABILITIES") {
3833
+ // PULL route of the recoverable handshake (v2.6.0). Idempotent and
3834
+ // side-effect free: it reports the SAME payload
3835
+ // `INSPECTOR_SCRIPT_LOADED` carried, rebuilt from the one builder,
3836
+ // so a host that lost the one-shot announcement can recover it on
3837
+ // demand instead of living forever with "capabilities unknown".
3838
+ // Mirrors QUERY_SLICE_FIELD / QUERY_COLLECTION_ORDER's standalone
3839
+ // request/response envelope (`requestId` echoed on the reply).
3840
+ announceInspectorCapabilities(event.data.requestId, "query")
3841
+ } else if (event.data && event.data.type === "SET_EDITOR_MODE") {
3310
3842
  setEditorModeFromHost(event.data.mode || "off", {
3311
3843
  hostControlledHistory: event.data.hostControlledHistory !== false,
3312
3844
  theme: event.data.theme || null,
@@ -3479,6 +4011,59 @@
3479
4011
  },
3480
4012
  })
3481
4013
  }
4014
+ } else if (
4015
+ event.data &&
4016
+ event.data.type === "QUERY_ELEMENTS_IN_RECT"
4017
+ ) {
4018
+ var rectRequestId = event.data.requestId
4019
+ var queryElements = []
4020
+ try {
4021
+ queryElements = collectElementsInRect(event.data.rect)
4022
+ var rectLimit = Number(event.data.limit)
4023
+ if (rectLimit > 0 && queryElements.length > rectLimit) {
4024
+ queryElements = queryElements.slice(0, rectLimit)
4025
+ }
4026
+ } catch (error) {
4027
+ inspectorWarn("[Inspector] QUERY_ELEMENTS_IN_RECT failed", error)
4028
+ queryElements = []
4029
+ }
4030
+ // Always answers — an empty list is a valid result, and the host's
4031
+ // pending request would otherwise sit until its timeout.
4032
+ postMessageToParent({
4033
+ type: "ELEMENTS_IN_RECT_RESULT",
4034
+ requestId: rectRequestId,
4035
+ elements: queryElements,
4036
+ })
4037
+ } else if (event.data && event.data.type === "CAPTURE_VIEWPORT") {
4038
+ var captureRequestId = event.data.requestId
4039
+ try {
4040
+ captureViewportDataUrl(event.data.pixelRatio)
4041
+ .then(function (dataUrl) {
4042
+ postMessageToParent({
4043
+ type: "VIEWPORT_CAPTURED",
4044
+ requestId: captureRequestId,
4045
+ dataUrl: dataUrl,
4046
+ width: window.innerWidth,
4047
+ height: window.innerHeight,
4048
+ scrollX: window.scrollX,
4049
+ scrollY: window.scrollY,
4050
+ })
4051
+ })
4052
+ .catch(function (error) {
4053
+ inspectorWarn("[Inspector] CAPTURE_VIEWPORT failed", error)
4054
+ postMessageToParent({
4055
+ type: "VIEWPORT_CAPTURE_FAILED",
4056
+ requestId: captureRequestId,
4057
+ error: String((error && error.message) || error),
4058
+ })
4059
+ })
4060
+ } catch (error) {
4061
+ postMessageToParent({
4062
+ type: "VIEWPORT_CAPTURE_FAILED",
4063
+ requestId: captureRequestId,
4064
+ error: String((error && error.message) || error),
4065
+ })
4066
+ }
3482
4067
  } else if (event.data && event.data.type === "APPLY_STYLE_CHANGE") {
3483
4068
  const { selector, property, value } = event.data
3484
4069
 
@@ -4393,8 +4978,289 @@
4393
4978
  }
4394
4979
  }
4395
4980
 
4981
+ // ── Right-click context menu ────────────────────────────────────────
4982
+ // Two entries — pin the node as chat context, or open a comment
4983
+ // anchored to it. Active in plain navigate mode as well as inspect, so
4984
+ // the listeners are registered here at load time: setupEventBlockers()
4985
+ // blocks "contextmenu" with stopImmediatePropagation once inspection
4986
+ // starts, and only a listener registered earlier in the capture phase
4987
+ // still runs.
4988
+
4989
+ var inspectorLabels = null
4990
+ var inspectorPermissions = { sendToChat: true }
4991
+ var contextMenuEl = null
4992
+ var contextMenuChatButton = null
4993
+ var contextMenuCommentButton = null
4994
+ var contextMenuTarget = null
4995
+ // Snapshotted when the menu opens: the comment anchor needs the
4996
+ // right-click POINT, which is gone by the time the entry is activated.
4997
+ var contextMenuTargetPayload = null
4998
+
4999
+ function getSendToChatLabel() {
5000
+ var configured =
5001
+ inspectorLabels &&
5002
+ inspectorLabels.badge &&
5003
+ typeof inspectorLabels.badge.sendToChat === "string"
5004
+ ? inspectorLabels.badge.sendToChat.trim()
5005
+ : ""
5006
+ return configured || "Send to chat"
5007
+ }
5008
+
5009
+ function getAddCommentLabel() {
5010
+ var configured =
5011
+ inspectorLabels &&
5012
+ inspectorLabels.contextMenu &&
5013
+ typeof inspectorLabels.contextMenu.addComment === "string"
5014
+ ? inspectorLabels.contextMenu.addComment.trim()
5015
+ : ""
5016
+ return configured || "Add a comment"
5017
+ }
5018
+
5019
+ // CONFIGURE_INSPECTOR payload — i18n labels plus the host's permission
5020
+ // gate for the chat entry (a viewer who can't send chat messages gets
5021
+ // the comment entry only).
5022
+ function applyInspectorConfiguration(data) {
5023
+ if (data && data.labels && typeof data.labels === "object") {
5024
+ inspectorLabels = data.labels
5025
+ }
5026
+ if (data && data.permissions && typeof data.permissions === "object") {
5027
+ inspectorPermissions = {
5028
+ sendToChat: data.permissions.sendToChat !== false,
5029
+ }
5030
+ }
5031
+ syncContextMenuLabels()
5032
+ }
5033
+
5034
+ function syncContextMenuLabels() {
5035
+ if (contextMenuChatButton) {
5036
+ contextMenuChatButton.textContent = getSendToChatLabel()
5037
+ contextMenuChatButton.style.display = inspectorPermissions.sendToChat
5038
+ ? "block"
5039
+ : "none"
5040
+ }
5041
+ if (contextMenuCommentButton) {
5042
+ contextMenuCommentButton.textContent = getAddCommentLabel()
5043
+ }
5044
+ }
5045
+
5046
+ function hideSelectionContextMenu() {
5047
+ if (contextMenuEl) contextMenuEl.style.display = "none"
5048
+ }
5049
+
5050
+ function ensureSelectionContextMenu() {
5051
+ if (contextMenuEl && document.contains(contextMenuEl)) {
5052
+ return contextMenuEl
5053
+ }
5054
+ // A previous run's node (or one orphaned by a body wipe) would
5055
+ // otherwise linger with dead click handlers.
5056
+ var orphan = document.getElementById("__archie-inspector-context-menu__")
5057
+ if (orphan && orphan.parentNode) orphan.parentNode.removeChild(orphan)
5058
+
5059
+ var menu = document.createElement("div")
5060
+ menu.id = "__archie-inspector-context-menu__"
5061
+ menu.setAttribute("data-inspector-context-menu", "true")
5062
+ menu.style.cssText =
5063
+ "position:fixed;display:none;z-index:2147483647;box-sizing:border-box;min-width:136px;padding:4px;border:1px solid rgba(15,23,42,0.16);border-radius:6px;background:#fff;box-shadow:0 10px 30px rgba(15,23,42,0.18);font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;"
5064
+
5065
+ function createMenuButton(labelText, onActivate) {
5066
+ var button = document.createElement("button")
5067
+ button.type = "button"
5068
+ button.textContent = labelText
5069
+ button.style.cssText =
5070
+ "display:block;width:100%;border:0;border-radius:4px;background:transparent;padding:6px 8px;text-align:left;color:#0f172a;font-size:12px;line-height:16px;cursor:pointer;"
5071
+ button.addEventListener("mouseenter", function () {
5072
+ button.style.background = inspectorTheme.accentSoft
5073
+ })
5074
+ button.addEventListener("mouseleave", function () {
5075
+ button.style.background = "transparent"
5076
+ })
5077
+ button.addEventListener("click", function (event) {
5078
+ event.preventDefault()
5079
+ event.stopPropagation()
5080
+ event.stopImmediatePropagation()
5081
+ try {
5082
+ onActivate()
5083
+ } catch (err) {
5084
+ inspectorWarn("[archie-inspector] context menu action failed", err)
5085
+ }
5086
+ hideSelectionContextMenu()
5087
+ })
5088
+ return button
5089
+ }
5090
+
5091
+ contextMenuChatButton = createMenuButton(
5092
+ getSendToChatLabel(),
5093
+ function () {
5094
+ if (!contextMenuTarget) return
5095
+ // TWO messages on purpose — the host splits this request in two
5096
+ // and neither half alone is visible:
5097
+ //
5098
+ // bare -> arms the editor surface ("inspect"), which is
5099
+ // what makes the chat's source bar render at all.
5100
+ // + metadata-> pins the node as the chat context and opens the
5101
+ // chat, but leaves the surface untouched.
5102
+ //
5103
+ // Sending only the metadata one pinned an element behind a bar
5104
+ // that was never rendered, which is why the entry looked dead.
5105
+ // The bare half is skipped while inspecting so it can't yank an
5106
+ // open visual-editor session into source-selection mode.
5107
+ if (!isInspecting) {
5108
+ postMessageToParent({ type: "CHAT_CONTEXT_REQUESTED" })
5109
+ }
5110
+ // `extractMetadata` carries the node's whole attribute map (the
5111
+ // `data-archie-id` the plain-render prototype stamps included) plus
5112
+ // `inspectorSource`; the host derives the chip's file from the
5113
+ // latter and its componentId from the former, falling back to
5114
+ // `data-dnd-node-id` on kit pages that stamp no archie id.
5115
+ postMessageToParent({
5116
+ type: "CHAT_CONTEXT_REQUESTED",
5117
+ metadata: extractMetadata(contextMenuTarget),
5118
+ })
5119
+ }
5120
+ )
5121
+
5122
+ contextMenuCommentButton = createMenuButton(
5123
+ getAddCommentLabel(),
5124
+ function () {
5125
+ postMessageToParent({
5126
+ type: "ADD_COMMENT_REQUESTED",
5127
+ target: contextMenuTargetPayload,
5128
+ })
5129
+ }
5130
+ )
5131
+
5132
+ menu.appendChild(contextMenuChatButton)
5133
+ menu.appendChild(contextMenuCommentButton)
5134
+ ;(document.body || document.documentElement).appendChild(menu)
5135
+ contextMenuEl = menu
5136
+ syncContextMenuLabels()
5137
+ return menu
5138
+ }
5139
+
5140
+ function buildCommentTarget(element, event) {
5141
+ if (!element) return null
5142
+ try {
5143
+ var rect = element.getBoundingClientRect()
5144
+ var viewportWidth =
5145
+ window.innerWidth || document.documentElement.clientWidth || 0
5146
+ var viewportHeight =
5147
+ window.innerHeight || document.documentElement.clientHeight || 0
5148
+ var preview = element.textContent
5149
+ ? element.textContent.trim().slice(0, 120)
5150
+ : ""
5151
+ return {
5152
+ cssSelector: getStableSelector(element),
5153
+ elementTag: String(element.tagName || "").toLowerCase(),
5154
+ elementPreview: preview || null,
5155
+ rect: {
5156
+ x: rect.left,
5157
+ y: rect.top,
5158
+ width: rect.width,
5159
+ height: rect.height,
5160
+ },
5161
+ // Fractions of the iframe viewport — the host re-projects them
5162
+ // against the iframe's own rect (see AddCommentRequestTarget).
5163
+ relativeX: viewportWidth ? event.clientX / viewportWidth : 0,
5164
+ relativeY: viewportHeight ? event.clientY / viewportHeight : 0,
5165
+ viewportWidth: viewportWidth,
5166
+ viewportHeight: viewportHeight,
5167
+ scrollTop:
5168
+ window.scrollY || document.documentElement.scrollTop || 0,
5169
+ }
5170
+ } catch (err) {
5171
+ return null
5172
+ }
5173
+ }
5174
+
5175
+ function showSelectionContextMenu(target, event) {
5176
+ var menu = ensureSelectionContextMenu()
5177
+ contextMenuTarget = target
5178
+ contextMenuTargetPayload = buildCommentTarget(target, event)
5179
+ menu.style.left =
5180
+ Math.min(event.clientX, window.innerWidth - 150) + "px"
5181
+ menu.style.top = Math.min(event.clientY, window.innerHeight - 60) + "px"
5182
+ menu.style.display = "block"
5183
+ }
5184
+
5185
+ var onContextMenuOpen = function (e) {
5186
+ var target = e.target
5187
+ if (!target || target === document.documentElement) return
5188
+ if (contextMenuEl && contextMenuEl.contains(target)) return
5189
+ if (isInspectorElement(target)) return
5190
+
5191
+ var resolved = resolveExactInspectTarget(target)
5192
+ if (!isValidContentElement(resolved)) {
5193
+ resolved = getElementFromPoint(e.clientX, e.clientY, target)
5194
+ }
5195
+ if (!isValidContentElement(resolved)) return
5196
+
5197
+ e.preventDefault()
5198
+ e.stopPropagation()
5199
+ // Beat setupEventBlockers' "contextmenu" blocker, which would
5200
+ // otherwise swallow the event before it reaches us again.
5201
+ e.stopImmediatePropagation()
5202
+ showSelectionContextMenu(resolved, e)
5203
+ }
5204
+
5205
+ var onContextMenuDismissPointer = function (e) {
5206
+ if (contextMenuEl && contextMenuEl.contains(e.target)) return
5207
+ hideSelectionContextMenu()
5208
+ }
5209
+
5210
+ var onContextMenuDismissKey = function (e) {
5211
+ if (e.key === "Escape") hideSelectionContextMenu()
5212
+ }
5213
+
5214
+ // Each runInspector() call builds a fresh closure, so drop the previous
5215
+ // run's listeners (and its orphaned menu node) the same way the message
5216
+ // handler above is deduped — otherwise a re-run stacks handlers that
5217
+ // each open their own menu.
5218
+ var previousContextMenuHandlers =
5219
+ window.__ARCHIE_INSPECTOR_CONTEXT_MENU_HANDLERS__
5220
+ if (previousContextMenuHandlers) {
5221
+ try {
5222
+ document.removeEventListener(
5223
+ "contextmenu",
5224
+ previousContextMenuHandlers.contextmenu,
5225
+ true
5226
+ )
5227
+ document.removeEventListener(
5228
+ "mousedown",
5229
+ previousContextMenuHandlers.mousedown,
5230
+ true
5231
+ )
5232
+ window.removeEventListener(
5233
+ "scroll",
5234
+ previousContextMenuHandlers.scroll,
5235
+ true
5236
+ )
5237
+ document.removeEventListener(
5238
+ "keydown",
5239
+ previousContextMenuHandlers.keydown,
5240
+ true
5241
+ )
5242
+ } catch (err) {
5243
+ // Ignore cleanup errors
5244
+ }
5245
+ }
5246
+
5247
+ document.addEventListener("contextmenu", onContextMenuOpen, true)
5248
+ document.addEventListener("mousedown", onContextMenuDismissPointer, true)
5249
+ window.addEventListener("scroll", hideSelectionContextMenu, true)
5250
+ document.addEventListener("keydown", onContextMenuDismissKey, true)
5251
+
5252
+ window.__ARCHIE_INSPECTOR_CONTEXT_MENU_HANDLERS__ = {
5253
+ contextmenu: onContextMenuOpen,
5254
+ mousedown: onContextMenuDismissPointer,
5255
+ scroll: hideSelectionContextMenu,
5256
+ keydown: onContextMenuDismissKey,
5257
+ }
5258
+
4396
5259
  // Register inspector click handler BEFORE event blockers
4397
- // This ensures it runs first and can mark events as handled
5260
+ // This ensures it runs first and can mark events as handled.
5261
+ // It stays AFTER the context-menu mousedown listener above, which must
5262
+ // still run (to dismiss the menu) even when a selection click calls
5263
+ // stopImmediatePropagation.
4398
5264
  document.addEventListener("mousedown", inspectorClickHandler, true)
4399
5265
 
4400
5266
  // Block clicks when element is selected but not inspecting