@archie/devtools 0.0.36 → 0.0.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -852,7 +852,11 @@ function archieDevTools(options = {}) {
852
852
  transformedByHook.add(id);
853
853
  return { code: result.code, map: result.map };
854
854
  } catch (err) {
855
- log("Babel transform failed for:", id, err);
855
+ console.error(
856
+ "[Archie DevTools] Babel transform FAILED \u2014 __editorMeta will be missing for:",
857
+ id,
858
+ err
859
+ );
856
860
  return null;
857
861
  }
858
862
  },
package/dist/index.mjs CHANGED
@@ -252,7 +252,11 @@ function archieDevTools(options = {}) {
252
252
  transformedByHook.add(id);
253
253
  return { code: result.code, map: result.map };
254
254
  } catch (err) {
255
- log("Babel transform failed for:", id, err);
255
+ console.error(
256
+ "[Archie DevTools] Babel transform FAILED \u2014 __editorMeta will be missing for:",
257
+ id,
258
+ err
259
+ );
256
260
  return null;
257
261
  }
258
262
  },
@@ -1527,80 +1527,172 @@
1527
1527
  }
1528
1528
  }
1529
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()
1530
1690
  postMessageToParent({
1531
1691
  type: "INSPECTOR_SCRIPT_LOADED",
1532
1692
  timestamp: Date.now(),
1533
- // 2.2.0 (ARCH-1065): additive only — `bindingChain` on element data,
1534
- // `QUERY_SLICE_FIELD` in the message protocol, and
1535
- // `capabilities.bindingContract` below. No existing field changed
1536
- // shape, so `protocolVersion` stays 1.
1537
- // 2.3.0 (ARCH-1065 A17-support): additive only — `QUERY_COLLECTION_ORDER`
1538
- // in the message protocol and `capabilities.collectionOrderQuery`
1539
- // below. No existing field changed shape, so `protocolVersion` stays 1.
1540
- // 2.4.0 (ARCH-883-KOI, CREW round): additive only — `origin` on
1541
- // `EditorMeta`, the usage-projected `ownMetaNodeId`/`inspectorSource`/
1542
- // `editorInstance` resolution, and `capabilities.kitOriginIdentity`
1543
- // below. No existing field changed shape, so `protocolVersion` stays 1.
1544
- version: "2.4.0",
1545
- protocolVersion: 1,
1546
- capabilities: {
1547
- // 2.1.1: function-valued __editorMeta props are invoked before any
1548
- // postMessage, so meta payloads stay structured-cloneable. Hosts can
1549
- // gate on this bit to confirm the deployed CDN artifact carries the
1550
- // fix (the 2.1.0 URL is overwritten in place on publish).
1551
- functionValuedEditorMeta: true,
1552
- inspect: true,
1553
- dndMove: true,
1554
- dndResize: true,
1555
- fiberInspectorSource: true,
1556
- hostControlledHistory: true,
1557
- arrangeStructuredHistory: true,
1558
- arrangeStrongNodeIdentity: true,
1559
- // Unified mode: SET_EDITOR_MODE "inspect" also enables the arrange
1560
- // (design) runtime — selection and drag/resize coexist. Hosts MUST
1561
- // feature-detect on this flag: a frozen 2.0.x inspector disables
1562
- // design mode on "inspect" and needs the legacy "design" mode.
1563
- unifiedInspectArrange: true,
1564
- // ARCH-1065: the M2 codemod's data-agent-* grammar is readable
1565
- // (`bindingChain` on element data) and slice-field-queryable
1566
- // (`QUERY_SLICE_FIELD`). Hosts MUST feature-detect on this flag
1567
- // before relying on either — a frozen pre-1065 inspector has
1568
- // neither.
1569
- collectionBinding: true,
1570
- // ARCH-1065 A17-support (v2.3.0): `QUERY_COLLECTION_ORDER` resolves
1571
- // the enclosing collection's order ("stable"/"derived") and row
1572
- // ids/count for a target node, off the same data-agent-* grammar
1573
- // as `collectionBinding`. Hosts MUST feature-detect on this flag —
1574
- // a frozen pre-2.3.0 inspector has no handler for the message.
1575
- collectionOrderQuery: true,
1576
- // The BUILD-TIME contract version this specific page was stamped
1577
- // with, forwarded verbatim from `<meta name="archie-binding-contract">`
1578
- // when the kit build emitted one — null on any page built before
1579
- // that meta tag existed, or that never carried collection-bound
1580
- // data. Opaque to this script: validating/interpreting the value is
1581
- // the host's job (mirrors how `slot` and `bindingContract` — plural
1582
- // concept, singular mechanism — are treated as opaque strings
1583
- // elsewhere in this file).
1584
- bindingContract: readBindingContractMeta(),
1585
- // ARCH-883-KOI F4 (v2.4.0) — this SCRIPT resolves `EditorMeta.origin`
1586
- // and projects identity across an instrumented kit boundary
1587
- // (`ownMetaNodeId`, `projectUsageMetaFromElement`,
1588
- // `getInspectorInfoFromElement`, `resolveInstanceIdentity`). Hosts
1589
- // MUST feature-detect on this flag before trusting `origin`
1590
- // (absent or `editorMeta.origin === 'kit'`) as meaningful: a frozen
1591
- // pre-2.4.0 inspector never reads the field at all, so its
1592
- // `inspectorSource`/`editorInstance` on an instrumented-kit page
1593
- // reproduce the exact F1 bug this version fixes.
1594
- //
1595
- // What this bit CANNOT tell the host: whether the PAGE's own build
1596
- // emitted `origin` on its metas in the first place — that is the
1597
- // `@archie/devtools` version the acg/kitgen esbuild path had
1598
- // installed when the screen (or the kit) was built, a completely
1599
- // separate axis from this CDN script's version (see the `origin`
1600
- // field's docstring in `types/editor-meta.ts` for the full
1601
- // reasoning, and why NO per-node signal can close that gap).
1602
- kitOriginIdentity: true,
1603
- },
1693
+ version: initialHandshake.version,
1694
+ protocolVersion: initialHandshake.protocolVersion,
1695
+ capabilities: initialHandshake.capabilities,
1604
1696
  })
1605
1697
 
1606
1698
  if (window._inspectorMessageHandler) {
@@ -1690,6 +1782,10 @@
1690
1782
  element === selectedElementBadge ||
1691
1783
  element.id === "inspector-ping-effect" ||
1692
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]")) ||
1693
1789
  // Design-mode (arrange) overlay elements: HitArea, resize handles,
1694
1790
  // drop-line, etc. These belong to the unified visual editor and must
1695
1791
  // pass mousedown/click events through to React (dnd-kit) instead of
@@ -2224,6 +2320,15 @@
2224
2320
  }
2225
2321
 
2226
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
+
2227
2332
  var historyMode =
2228
2333
  options && options.hostControlledHistory ? "host" : "local"
2229
2334
 
@@ -2758,6 +2863,310 @@
2758
2863
  return element
2759
2864
  }
2760
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
+
2761
3170
  function extractMetadata(element) {
2762
3171
  if (!element) return null
2763
3172
 
@@ -3416,7 +3825,20 @@
3416
3825
  }
3417
3826
  }
3418
3827
 
3419
- 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") {
3420
3842
  setEditorModeFromHost(event.data.mode || "off", {
3421
3843
  hostControlledHistory: event.data.hostControlledHistory !== false,
3422
3844
  theme: event.data.theme || null,
@@ -3589,6 +4011,59 @@
3589
4011
  },
3590
4012
  })
3591
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
+ }
3592
4067
  } else if (event.data && event.data.type === "APPLY_STYLE_CHANGE") {
3593
4068
  const { selector, property, value } = event.data
3594
4069
 
@@ -4503,8 +4978,289 @@
4503
4978
  }
4504
4979
  }
4505
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
+
4506
5259
  // Register inspector click handler BEFORE event blockers
4507
- // 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.
4508
5264
  document.addEventListener("mousedown", inspectorClickHandler, true)
4509
5265
 
4510
5266
  // Block clicks when element is selected but not inspecting
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archie/devtools",
3
- "version": "0.0.36",
3
+ "version": "0.0.38",
4
4
  "description": "DevTools for Archie generated applications - Route synchronization and editor communication",
5
5
  "repository": {
6
6
  "type": "git",
@@ -52,8 +52,6 @@
52
52
  "author": "8base",
53
53
  "license": "UNLICENSED",
54
54
  "devDependencies": {
55
- "@babel/core": "7.29.0",
56
- "@babel/types": "7.29.0",
57
55
  "@dnd-kit/core": "6.3.1",
58
56
  "@dnd-kit/sortable": "10.0.0",
59
57
  "@dnd-kit/utilities": "3.2.2",
@@ -74,7 +72,10 @@
74
72
  },
75
73
  "peerDependencies": {
76
74
  "react": ">=18.0.0",
77
- "react-dom": ">=18.0.0",
78
- "react-router-dom": ">=6.4.0"
75
+ "react-dom": ">=18.0.0"
76
+ },
77
+ "dependencies": {
78
+ "@babel/core": "7.29.0",
79
+ "@babel/types": "7.29.0"
79
80
  }
80
81
  }