@archie/devtools 0.1.2 → 0.1.4

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.
@@ -987,6 +987,159 @@
987
987
  return canonicalTarget
988
988
  }
989
989
 
990
+ // ── "Visual unit" climb (ARCH — hit-testing bounces to a full-bleed
991
+ // child) ─────────────────────────────────────────────────────────────
992
+ // A click on a card's padding hits the CARD's only element child (a
993
+ // `flex flex-col p-6` div that fills 100% of the card's box), never the
994
+ // card itself — the child is topmost at that point AND is the first
995
+ // ancestor-or-self with its own source signature, so the walk above
996
+ // returns it immediately. That makes the card's own border/shadow/
997
+ // padding permanently unreachable by click, and a drag started there
998
+ // moves only the inner div, leaving an empty bordered shell behind.
999
+ //
1000
+ // The fix is a SEPARATE pass, applied only where a user interaction
1001
+ // commits a SELECTION (inspectorClickHandler, and the synthetic
1002
+ // mousedown Escape/double-click dispatch below) — never inside
1003
+ // `resolveExactInspectTarget`/`getElementFromPoint` themselves. Those
1004
+ // two stay leaf-exact on purpose: hover highlighting and shared/
1005
+ // mirrored-instance signature matching (`resolveSharedTargets`,
1006
+ // `findElementByInspectorSource`, APPLY_STYLE_CHANGE's mutation
1007
+ // resolution) key off the PRECISE host a signature names, and two
1008
+ // nested hosts with their own distinct metas (a real, if less common,
1009
+ // shape than the wrapper case below) must stay distinguishable there.
1010
+ //
1011
+ // Here, climb through "single-child wrapper" ancestors — each one
1012
+ // instrumented (its own direct __editorMeta), in the SAME file, not
1013
+ // kit-origin/chrome, not the screen root, whose ONLY element child is
1014
+ // the node below and whose box visually contains it — and return the
1015
+ // OUTERMOST one. A leaf clicked directly (the `<h3>` among two
1016
+ // siblings) never enters the climb: its parent has more than one
1017
+ // element child, so the loop breaks on the first iteration.
1018
+ var STRUCTURAL_COMPONENT_NAME_RE = /(?:Page|Provider|RootLayout)$/
1019
+
1020
+ function isStructuralComponentNameForUnit(name) {
1021
+ return !!name && STRUCTURAL_COMPONENT_NAME_RE.test(name)
1022
+ }
1023
+
1024
+ // Vanilla twin of isHighLevelStructuralElement (structural-policy.ts) —
1025
+ // the "screen root" a visual-unit climb must never swallow.
1026
+ function isScreenRootWrapperElement(el) {
1027
+ if (!el || el.nodeType !== 1) return true
1028
+ if (el.hasAttribute && el.hasAttribute("data-dnd-structural")) return true
1029
+ var tag = String(el.tagName || "").toLowerCase()
1030
+ if (tag === "html" || tag === "body") return true
1031
+ if (el.id === "root") return true
1032
+ var parent = el.parentElement
1033
+ if (
1034
+ tag === "main" &&
1035
+ (parent === document.body || (parent && parent.id === "root"))
1036
+ ) {
1037
+ return true
1038
+ }
1039
+ return false
1040
+ }
1041
+
1042
+ function getSoleElementChild(el) {
1043
+ var only = null
1044
+ var kids = el.children
1045
+ for (var i = 0; i < kids.length; i++) {
1046
+ if (only) return null
1047
+ only = kids[i]
1048
+ }
1049
+ return only
1050
+ }
1051
+
1052
+ // "kit-internal" (vendor template, ARCH-883) or "chrome" (app shell) —
1053
+ // neither is a legitimate visual-unit wrapper: climbing into one would
1054
+ // select vendor/shell scaffolding the user never asked to move.
1055
+ function isKitOrChromeMeta(meta) {
1056
+ return (
1057
+ !!meta &&
1058
+ (isTemplateFrame(meta) ||
1059
+ meta.isKitOrigin === true ||
1060
+ meta.isChrome === true)
1061
+ )
1062
+ }
1063
+
1064
+ // The file this element's OWN identity is written in — usage
1065
+ // projection first (same precedence as getExactSourceSignatureFromElement),
1066
+ // then direct/owner fiber meta, then the legacy source-ref attribute.
1067
+ function getOwnSourceFileForElement(element) {
1068
+ var usageMeta = projectUsageMetaFromElement(element)
1069
+ if (usageMeta && usageMeta.file) return usageMeta.file
1070
+
1071
+ var directMeta = getDirectEditorMetaFromElement(element)
1072
+ if (directMeta && directMeta.file) return directMeta.file
1073
+
1074
+ var ownerMeta = getOwnerEditorMetaFromElement(element)
1075
+ if (ownerMeta && ownerMeta.file) return ownerMeta.file
1076
+
1077
+ if (
1078
+ element &&
1079
+ typeof element.getAttribute === "function" &&
1080
+ element.hasAttribute(ARCHIE_SOURCE_REF_ATTR)
1081
+ ) {
1082
+ var raw = element.getAttribute(ARCHIE_SOURCE_REF_ATTR) || ""
1083
+ var file = raw.split("::")[0]
1084
+ if (file) return file
1085
+ }
1086
+
1087
+ return ""
1088
+ }
1089
+
1090
+ // Loose "fits inside" check (1px tolerance for subpixel rounding) — the
1091
+ // wrapper's box must visually contain the child's, not merely equal it
1092
+ // (padding/border on the wrapper is expected and fine).
1093
+ function rectContainsForUnit(outer, inner) {
1094
+ var eps = 1
1095
+ return (
1096
+ inner.left >= outer.left - eps &&
1097
+ inner.top >= outer.top - eps &&
1098
+ inner.right <= outer.right + eps &&
1099
+ inner.bottom <= outer.bottom + eps
1100
+ )
1101
+ }
1102
+
1103
+ function climbVisualUnitChain(leaf) {
1104
+ if (!leaf) return leaf
1105
+ var currentFile = getOwnSourceFileForElement(leaf)
1106
+ if (!currentFile) return leaf
1107
+
1108
+ var current = leaf
1109
+ while (true) {
1110
+ var parent = current.parentElement
1111
+ if (!parent) break
1112
+ if (parent === document.body || parent === document.documentElement) break
1113
+ if (isInspectorElement(parent)) break
1114
+ if (isScreenRootWrapperElement(parent)) break
1115
+ if (getSoleElementChild(parent) !== current) break
1116
+
1117
+ var parentMeta = getDirectEditorMetaFromElement(parent)
1118
+ if (!parentMeta || isKitOrChromeMeta(parentMeta)) break
1119
+ if (!parentMeta.file || parentMeta.file !== currentFile) break
1120
+ if (isStructuralComponentNameForUnit(parentMeta.componentName)) break
1121
+
1122
+ try {
1123
+ if (
1124
+ !rectContainsForUnit(
1125
+ parent.getBoundingClientRect(),
1126
+ current.getBoundingClientRect()
1127
+ )
1128
+ ) {
1129
+ break
1130
+ }
1131
+ } catch (rectError) {
1132
+ // Containment couldn't be verified — never climb into an
1133
+ // unverified parent; stop here (current stays the last
1134
+ // confirmed-contained unit), same as a failed containment check.
1135
+ break
1136
+ }
1137
+
1138
+ current = parent
1139
+ }
1140
+ return current
1141
+ }
1142
+
990
1143
  function resolveExactInspectTarget(element) {
991
1144
  var normalizedTarget = normalizeInspectableTarget(element)
992
1145
  if (
@@ -1031,6 +1184,92 @@
1031
1184
  return resolveCoarseInspectTarget(normalizedTarget)
1032
1185
  }
1033
1186
 
1187
+ // Nearest real DOM ancestor above `el` that carries its own source
1188
+ // signature — the "select parent" step of visual-unit navigation
1189
+ // (Escape). Climbed to its own outer unit so Escape always lands on a
1190
+ // full unit boundary, the same guarantee a click gives.
1191
+ function findParentUnitElement(el) {
1192
+ var current = el ? el.parentElement : null
1193
+ while (
1194
+ current &&
1195
+ current !== document.body &&
1196
+ current !== document.documentElement
1197
+ ) {
1198
+ if (!isInspectorElement(current) && getExactSourceSignatureFromElement(current)) {
1199
+ return climbVisualUnitChain(current)
1200
+ }
1201
+ current = current.parentElement
1202
+ }
1203
+ return null
1204
+ }
1205
+
1206
+ // The direct child of `container` that sits on the ancestor line to
1207
+ // (x, y) — the "select child toward the point" step (double click).
1208
+ function findImmediateChildTowardPoint(container, x, y) {
1209
+ var atPoint = null
1210
+ try {
1211
+ atPoint = document.elementFromPoint(x, y)
1212
+ } catch (pointError) {
1213
+ atPoint = null
1214
+ }
1215
+ if (!atPoint || isInspectorElement(atPoint)) return null
1216
+ if (!container.contains(atPoint) || atPoint === container) return null
1217
+
1218
+ var current = atPoint
1219
+ while (current && current.parentElement !== container) {
1220
+ current = current.parentElement
1221
+ }
1222
+ if (!current || current.parentElement !== container) return null
1223
+ return current
1224
+ }
1225
+
1226
+ // Consumed once by inspectorClickHandler, immediately below: a
1227
+ // double-click DESCEND already picked the exact inner element the user
1228
+ // asked for (one level down from the current unit, toward the click) —
1229
+ // re-running the climb on that synthetic mousedown would immediately
1230
+ // walk it straight back up to the same outer unit it just left. The
1231
+ // ASCEND direction (Escape) never sets this: findParentUnitElement
1232
+ // already returns a fully-climbed outer unit, so climbing it again is
1233
+ // a no-op anyway.
1234
+ var suppressUnitClimbOnce = false
1235
+
1236
+ // Reuses the exact pattern DesignModeOverlay's HitArea bypass already
1237
+ // relies on (see hit-area-click-routing.test.tsx): a synthetic
1238
+ // `mousedown` at the element's own center runs inspectorClickHandler's
1239
+ // full selection flow (ELEMENT_SELECTED, badges, design-mode bridge)
1240
+ // instead of a local, partial store flip.
1241
+ function dispatchSyntheticSelect(el, suppressClimb) {
1242
+ if (!el) return
1243
+ var rect
1244
+ try {
1245
+ rect = el.getBoundingClientRect()
1246
+ } catch (rectError) {
1247
+ // Never got to dispatch — the flag must never be set for a
1248
+ // dispatch that didn't happen.
1249
+ return
1250
+ }
1251
+ var cx = rect.left + rect.width / 2
1252
+ var cy = rect.top + rect.height / 2
1253
+
1254
+ // Set the flag as late as possible (right before the dispatch that
1255
+ // consumes it) and guarantee it never stays stuck `true` — whether
1256
+ // dispatchEvent runs inspectorClickHandler to completion (which
1257
+ // consumes it itself) or throws before/along the way.
1258
+ if (suppressClimb) suppressUnitClimbOnce = true
1259
+ try {
1260
+ el.dispatchEvent(
1261
+ new MouseEvent("mousedown", {
1262
+ bubbles: true,
1263
+ cancelable: true,
1264
+ clientX: cx,
1265
+ clientY: cy,
1266
+ })
1267
+ )
1268
+ } finally {
1269
+ suppressUnitClimbOnce = false
1270
+ }
1271
+ }
1272
+
1034
1273
  function resolveSharedGroupSignature(element) {
1035
1274
  var inspectTarget = resolveExactInspectTarget(element)
1036
1275
  if (!inspectTarget) return ""
@@ -5522,6 +5761,11 @@
5522
5761
  // Selection handler: MUST run on mousedown before blockEvent. We register at load time (before
5523
5762
  // START_INSPECTION), so we run first in capture phase; we stopImmediatePropagation so blockEvent never runs.
5524
5763
  const inspectorClickHandler = function (e) {
5764
+ // Consumed exactly once per mousedown, regardless of any early
5765
+ // return below — see suppressUnitClimbOnce's docstring above.
5766
+ var climbSuppressed = suppressUnitClimbOnce
5767
+ suppressUnitClimbOnce = false
5768
+
5525
5769
  if (!isInspecting) return
5526
5770
 
5527
5771
  // If the event originated on a design-mode (arrange) overlay element —
@@ -5554,6 +5798,12 @@
5554
5798
  return
5555
5799
  }
5556
5800
 
5801
+ // "Visual unit" climb: a click on a full-bleed single-child wrapper
5802
+ // (a card's padding div) selects the OUTERMOST wrapper of that 1:1
5803
+ // chain (the card itself), not the innermost hit. See
5804
+ // climbVisualUnitChain's docstring above.
5805
+ target = climbSuppressed ? target : climbVisualUnitChain(target)
5806
+
5557
5807
  // Block native behavior
5558
5808
  e.preventDefault()
5559
5809
  e.stopPropagation()
@@ -5954,6 +6204,38 @@
5954
6204
  // stopImmediatePropagation.
5955
6205
  document.addEventListener("mousedown", inspectorClickHandler, true)
5956
6206
 
6207
+ // Visual-unit navigation, "descend" step: double click steps ONE level
6208
+ // down from the current selection, toward wherever the double click
6209
+ // landed — the reverse of the climb in resolveExactInspectTarget. Runs
6210
+ // in capture, BEFORE setupEventBlockers' own "dblclick" blocker (both
6211
+ // are registered here, at load time, ahead of any START_INSPECTION),
6212
+ // so stopImmediatePropagation below reliably wins the race.
6213
+ document.addEventListener(
6214
+ "dblclick",
6215
+ function (e) {
6216
+ if (!isInspecting || !isElementSelected || !selectedElement) return
6217
+ if (isInspectorElement(e.target)) return
6218
+ if (
6219
+ e.target &&
6220
+ typeof e.target.closest === "function" &&
6221
+ e.target.closest("[data-design-mode-ui]")
6222
+ ) {
6223
+ return
6224
+ }
6225
+ var child = findImmediateChildTowardPoint(
6226
+ selectedElement,
6227
+ e.clientX,
6228
+ e.clientY
6229
+ )
6230
+ if (!child) return
6231
+ e.preventDefault()
6232
+ e.stopPropagation()
6233
+ e.stopImmediatePropagation()
6234
+ dispatchSyntheticSelect(child, true)
6235
+ },
6236
+ true
6237
+ )
6238
+
5957
6239
  // Block clicks when element is selected but not inspecting
5958
6240
  document.addEventListener(
5959
6241
  "click",
@@ -5971,30 +6253,59 @@
5971
6253
  true
5972
6254
  )
5973
6255
 
5974
- document.addEventListener("keydown", function (e) {
5975
- if (e.key === "Escape" && (isInspecting || isElementSelected)) {
5976
- isInspecting = false
5977
- isElementSelected = false
5978
- removeEventBlockers()
5979
- if (document.body) {
5980
- document.body.style.cursor = ""
5981
- document.body.style.userSelect = ""
6256
+ // Capture phase, matching setupEventBlockers' own capture-phase
6257
+ // "keydown" blocker: a bubble-phase listener registered here would
6258
+ // never run once isInspecting is true — that blocker's
6259
+ // stopImmediatePropagation wins the target's capture-listeners-first
6260
+ // ordering regardless of add order (also matches
6261
+ // onContextMenuDismissKey's own capture registration above).
6262
+ document.addEventListener(
6263
+ "keydown",
6264
+ function (e) {
6265
+ if (e.key !== "Escape") return
6266
+
6267
+ // Visual-unit navigation, "ascend" step: Escape steps ONE level up
6268
+ // from the current selection (to the nearest ancestor with its own
6269
+ // identity, climbed to ITS outer unit) instead of exiting selection
6270
+ // entirely. Falls through to the exit-all behavior below only once
6271
+ // there is no further ancestor to select — e.g. nothing is
6272
+ // selected, or the selection is already the outermost reachable
6273
+ // unit.
6274
+ if (isInspecting && isElementSelected && selectedElement) {
6275
+ var parentUnit = findParentUnitElement(selectedElement)
6276
+ if (parentUnit) {
6277
+ e.preventDefault()
6278
+ e.stopImmediatePropagation()
6279
+ dispatchSyntheticSelect(parentUnit)
6280
+ return
6281
+ }
5982
6282
  }
5983
- removeHighlight()
5984
- removeSelectedBadge()
5985
- selectedElements.forEach(function (el) {
5986
- if (el) removeInspectorStyles(el)
5987
- })
5988
- selectedElement = null
5989
- selectedElements = []
5990
- window._inspectorSelectedElement = null
5991
- if (window.__ARCHIE_INSPECTOR__)
5992
- window.__ARCHIE_INSPECTOR__.selectedElement = null
5993
- updateSharedHighlights()
5994
- publishSelectionToDesignMode()
5995
- postMessageToParent({ type: "INSPECTION_CANCELLED" })
5996
- }
5997
- })
6283
+
6284
+ if (isInspecting || isElementSelected) {
6285
+ isInspecting = false
6286
+ isElementSelected = false
6287
+ removeEventBlockers()
6288
+ if (document.body) {
6289
+ document.body.style.cursor = ""
6290
+ document.body.style.userSelect = ""
6291
+ }
6292
+ removeHighlight()
6293
+ removeSelectedBadge()
6294
+ selectedElements.forEach(function (el) {
6295
+ if (el) removeInspectorStyles(el)
6296
+ })
6297
+ selectedElement = null
6298
+ selectedElements = []
6299
+ window._inspectorSelectedElement = null
6300
+ if (window.__ARCHIE_INSPECTOR__)
6301
+ window.__ARCHIE_INSPECTOR__.selectedElement = null
6302
+ updateSharedHighlights()
6303
+ publishSelectionToDesignMode()
6304
+ postMessageToParent({ type: "INSPECTION_CANCELLED" })
6305
+ }
6306
+ },
6307
+ true
6308
+ )
5998
6309
  })()
5999
6310
  }
6000
6311
  if (typeof window !== "undefined") {