@archie/devtools 0.0.25 → 0.0.27

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.
@@ -367,13 +367,28 @@
367
367
  return projected && projected.nodeId ? projected.nodeId : null
368
368
  }
369
369
 
370
- // The repeated-row boundary above `el`, or null. INNERMOST repetition
371
- // wins (a tag inside a card inside a list is identified by the tag's key).
370
+ // The repeated-row boundary above `el`, or a search outcome explaining
371
+ // why none was found. INNERMOST repetition wins (a tag inside a card
372
+ // inside a list is identified by the tag's key).
373
+ //
374
+ // ARCH-1065 D8-v1/D26 — three-way outcome, matching instance-identity.ts:
375
+ // a CollectionWindow object (found), the string "exhausted" (walked to
376
+ // the true DOM root having found nothing — a COMPLETED search, safe to
377
+ // report as SINGLETON), or null (gave up without a definitive answer:
378
+ // no ancestor to compare against at all, or the depth budget ran out
379
+ // before reaching the root). The PREVIOUS version of this function
380
+ // collapsed "exhausted" and budget-exhaustion into one `null` verdict
381
+ // that `resolveInstanceIdentity` then always read as SINGLETON — more
382
+ // permissive than the TS twin ever intended: a walk that stopped early
383
+ // because it ran out of depth budget never PROVED there was no
384
+ // collection ancestor above it.
372
385
  function findCollectionWindow(el) {
386
+ if (!el.parentElement) return null
387
+
373
388
  var node = el
374
389
  for (var depth = 0; depth < MAX_ROW_ROOT_DEPTH; depth++) {
375
390
  var parent = node.parentElement
376
- if (!parent) return null
391
+ if (!parent) return "exhausted"
377
392
 
378
393
  var nodeId = ownMetaNodeId(node)
379
394
  if (nodeId !== null) {
@@ -411,12 +426,19 @@
411
426
  return null
412
427
  }
413
428
 
414
- // WHICH instance of a possibly-repeated node this element is.
429
+ // WHICH instance of a possibly-repeated node this element is, or NULL
430
+ // when resolution could not reach a definitive answer (ARCH-1065
431
+ // D8-v1/D26 — matches `resolveInstanceIdentity` in instance-identity.ts;
432
+ // this file cannot import it, so the same three-way outcome is
433
+ // re-derived here rather than left more permissive than the TS twin).
415
434
  // `scanOrdinal` is telemetry/UI only — the host must REJECT an intent
416
- // with inCollection && fiberKey === null, never fall back to it.
435
+ // with inCollection && fiberKey === null, never fall back to it, and
436
+ // must treat a NULL editorInstance exactly like that same refusal.
417
437
  function resolveInstanceIdentity(el) {
438
+ if (!(el instanceof HTMLElement)) return null
418
439
  var win = findCollectionWindow(el)
419
- if (!win) return { fiberKey: null, scanOrdinal: 0, inCollection: false }
440
+ if (win === null) return null
441
+ if (win === "exhausted") return { fiberKey: null, scanOrdinal: 0, inCollection: false }
420
442
  return {
421
443
  fiberKey: keyWithinWindow(win.rowRoot, win.container),
422
444
  scanOrdinal: win.ordinal,
@@ -453,6 +475,146 @@
453
475
  return anchors
454
476
  }
455
477
 
478
+ // ARCH-1065 M3 — the D1 grammar attributes babel-plugin-collection-binding
479
+ // stamps at build time (data-agent-entity/-row/-field/-field-state/
480
+ // -field-parts/-field-slot/-collection-order). One frame per DOM
481
+ // attribute name; a leaf typically carries only `field` (+ `fieldSlot`
482
+ // or `fieldState`/`fieldParts`), while its row-root ancestor carries
483
+ // `entity` + `row` + `collectionOrder` — reading both frames is what
484
+ // lets the host recover the full (entity, field, rowId) triple for a
485
+ // click anywhere inside a bound leaf.
486
+ var BINDING_ATTRS = [
487
+ ["entity", "data-agent-entity"],
488
+ ["row", "data-agent-row"],
489
+ ["field", "data-agent-field"],
490
+ ["fieldState", "data-agent-field-state"],
491
+ ["fieldParts", "data-agent-field-parts"],
492
+ ["fieldSlot", "data-agent-field-slot"],
493
+ ["collectionOrder", "data-agent-collection-order"],
494
+ ]
495
+
496
+ function readBindingFrame(el) {
497
+ if (!el || el.nodeType !== 1 || typeof el.getAttribute !== "function") return null
498
+ var frame = null
499
+ for (var i = 0; i < BINDING_ATTRS.length; i++) {
500
+ var value = el.getAttribute(BINDING_ATTRS[i][1])
501
+ if (value !== null) {
502
+ if (!frame) frame = {}
503
+ frame[BINDING_ATTRS[i][0]] = value
504
+ }
505
+ }
506
+ return frame
507
+ }
508
+
509
+ // Walks SELF-outward (unlike collectAncestorAnchors, which is strict
510
+ // ancestors — a click on a leaf's own `data-agent-field` must appear in
511
+ // its own chain entry), one frame per element carrying >=1 binding
512
+ // attribute, bounded by MAX_ANCHOR_CHAIN (the same bound the ARCH-906
513
+ // anchor walk uses — a binding chain never needs to reach deeper than
514
+ // that one does).
515
+ function collectBindingChain(el) {
516
+ var chain = []
517
+ var node = el
518
+ for (var depth = 0; node && depth < MAX_ANCHOR_CHAIN; depth++) {
519
+ var frame = readBindingFrame(node)
520
+ if (frame) chain.push(frame)
521
+ node = node.parentElement
522
+ }
523
+ return chain
524
+ }
525
+
526
+ // The nearest data-agent-row value SELF-outward, or null. Used to
527
+ // resolve "the currently selected row" for QUERY_SLICE_FIELD when the
528
+ // caller does not pass an explicit rowId.
529
+ function nearestRowId(el) {
530
+ var node = el
531
+ for (var depth = 0; node && depth < MAX_ANCHOR_CHAIN; depth++) {
532
+ if (node.nodeType === 1 && typeof node.getAttribute === "function") {
533
+ var rowId = node.getAttribute("data-agent-row")
534
+ if (typeof rowId === "string" && rowId !== "") return rowId
535
+ }
536
+ node = node.parentElement
537
+ }
538
+ return null
539
+ }
540
+
541
+ // ARCH-1065 — the BUILD-TIME contract version this page was stamped
542
+ // with. Opaque here; the host interprets it (SDD analog of `slot`).
543
+ function readBindingContractMeta() {
544
+ try {
545
+ var metaEl = document.querySelector('meta[name="archie-binding-contract"]')
546
+ if (metaEl && typeof metaEl.getAttribute === "function") {
547
+ var content = metaEl.getAttribute("content")
548
+ return typeof content === "string" && content !== "" ? content : null
549
+ }
550
+ } catch (e) {
551
+ // Best-effort — capability forwarding must never throw at boot.
552
+ }
553
+ return null
554
+ }
555
+
556
+ // ARCH-1065 M3 — `window.__slice` read live for the FE's typed controls
557
+ // (design D7): given an entity + field, the DISTINCT values that field
558
+ // takes across every row of that entity in the current slice (the
559
+ // vocabulary a status-SELECT control offers) and, when a row is known,
560
+ // that row's CURRENT value. Nothing about type/format is STAMPED at
561
+ // build time (per the M1/M2/M3 split) — it is read here, live, so it
562
+ // reflects the actual data the page is showing right now.
563
+ function resolveSliceEntityRows(entity) {
564
+ var slice = window.__slice
565
+ if (!slice || typeof slice !== "object") return null
566
+ var rows = slice[entity]
567
+ return Array.isArray(rows) ? rows : null
568
+ }
569
+
570
+ var ISO_DATE_PATTERN =
571
+ /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})?)?$/
572
+
573
+ function inferSliceValueType(value) {
574
+ if (value === null || value === undefined || value === "") return "empty-null"
575
+ if (typeof value === "boolean") return "boolean"
576
+ if (typeof value === "number") return "number"
577
+ if (typeof value === "string") {
578
+ return ISO_DATE_PATTERN.test(value) ? "date" : "string"
579
+ }
580
+ return "unknown"
581
+ }
582
+
583
+ function querySliceField(entity, field, rowId) {
584
+ var rows = resolveSliceEntityRows(entity)
585
+ if (!rows) return null
586
+
587
+ var distinctValues = []
588
+ var seenDistinct = {}
589
+ for (var i = 0; i < rows.length; i++) {
590
+ var candidate = rows[i] ? rows[i][field] : undefined
591
+ if (candidate === undefined) continue
592
+ var key = String(candidate)
593
+ if (!seenDistinct[key]) {
594
+ seenDistinct[key] = true
595
+ distinctValues.push(candidate)
596
+ }
597
+ }
598
+
599
+ var currentValue = null
600
+ if (rowId != null) {
601
+ for (var j = 0; j < rows.length; j++) {
602
+ if (rows[j] && String(rows[j].id) === String(rowId)) {
603
+ var rowValue = rows[j][field]
604
+ currentValue = rowValue === undefined ? null : rowValue
605
+ break
606
+ }
607
+ }
608
+ }
609
+
610
+ var typeSample = currentValue !== null ? currentValue : distinctValues[0]
611
+ return {
612
+ currentValue: currentValue,
613
+ valueType: inferSliceValueType(typeSample),
614
+ distinctValues: distinctValues,
615
+ }
616
+ }
617
+
456
618
  if (window._inspectorSharedTargetObserver) {
457
619
  try {
458
620
  window._inspectorSharedTargetObserver.disconnect()
@@ -1065,7 +1227,11 @@
1065
1227
  postMessageToParent({
1066
1228
  type: "INSPECTOR_SCRIPT_LOADED",
1067
1229
  timestamp: Date.now(),
1068
- version: "2.1.1",
1230
+ // 2.2.0 (ARCH-1065): additive only — `bindingChain` on element data,
1231
+ // `QUERY_SLICE_FIELD` in the message protocol, and
1232
+ // `capabilities.bindingContract` below. No existing field changed
1233
+ // shape, so `protocolVersion` stays 1.
1234
+ version: "2.2.0",
1069
1235
  protocolVersion: 1,
1070
1236
  capabilities: {
1071
1237
  // 2.1.1: function-valued __editorMeta props are invoked before any
@@ -1085,6 +1251,21 @@
1085
1251
  // feature-detect on this flag: a frozen 2.0.x inspector disables
1086
1252
  // design mode on "inspect" and needs the legacy "design" mode.
1087
1253
  unifiedInspectArrange: true,
1254
+ // ARCH-1065: the M2 codemod's data-agent-* grammar is readable
1255
+ // (`bindingChain` on element data) and slice-field-queryable
1256
+ // (`QUERY_SLICE_FIELD`). Hosts MUST feature-detect on this flag
1257
+ // before relying on either — a frozen pre-1065 inspector has
1258
+ // neither.
1259
+ collectionBinding: true,
1260
+ // The BUILD-TIME contract version this specific page was stamped
1261
+ // with, forwarded verbatim from `<meta name="archie-binding-contract">`
1262
+ // when the kit build emitted one — null on any page built before
1263
+ // that meta tag existed, or that never carried collection-bound
1264
+ // data. Opaque to this script: validating/interpreting the value is
1265
+ // the host's job (mirrors how `slot` and `bindingContract` — plural
1266
+ // concept, singular mechanism — are treated as opaque strings
1267
+ // elsewhere in this file).
1268
+ bindingContract: readBindingContractMeta(),
1088
1269
  },
1089
1270
  })
1090
1271
 
@@ -1684,6 +1865,30 @@
1684
1865
  // misread by the host as a stale mode (see runtimeBridgeHandler).
1685
1866
  var hostCommandedEditorMode = "off"
1686
1867
 
1868
+ // ARCH-886 — per-page runtime feature-detection for the unified
1869
+ // inspect+arrange coexistence. This inspector build is served fresh
1870
+ // from the CDN (same URL, overwritten in place — see
1871
+ // docs/deploy-order.md) to EVERY published prototype, old and new
1872
+ // alike, so it cannot assume the PAGE it is currently inspecting was
1873
+ // built with a @archie/devtools new enough to understand
1874
+ // `set-design-mode`'s unified role. The page's own DndProvider
1875
+ // registers `window.__ARCHIE_DESIGN_MODE__` synchronously on mount —
1876
+ // well before a user can trigger SET_EDITOR_MODE from the host — so
1877
+ // its presence at message-handling time is an accurate, live,
1878
+ // per-page signal (no version number or capability handshake needed).
1879
+ // Pages built before this bridge existed (pre-PR#12, e.g. the 0.0.21
1880
+ // release) already handle the legacy `set-design-mode` command (that
1881
+ // handler predates the bridge and is unchanged by it) — so
1882
+ // unconditionally dispatching it there does not no-op, it ARMS that
1883
+ // page's design-mode/arrange overlay with no selection routing,
1884
+ // producing a second, uncoordinated hit-area layer on top of the
1885
+ // inspector's own hover/selection UI. Gating on the bridge avoids
1886
+ // that: an old page just gets clean inspect-only behavior, same as
1887
+ // before unification shipped.
1888
+ function pageSupportsUnifiedDesignMode() {
1889
+ return typeof window.__ARCHIE_DESIGN_MODE__ !== "undefined"
1890
+ }
1891
+
1687
1892
  function setEditorModeFromHost(mode, options) {
1688
1893
  var historyMode =
1689
1894
  options && options.hostControlledHistory ? "host" : "local"
@@ -1704,11 +1909,14 @@
1704
1909
  hostCommandedEditorMode = mode === "inspect" || mode === "design" ? mode : "off"
1705
1910
 
1706
1911
  if (mode === "inspect") {
1707
- // Unified mode: visual editor (inspect) + arrange (design) coexist.
1708
- // The arrange overlay stays passive until the inspector publishes a
1912
+ // Unified mode: visual editor (inspect) + arrange (design) coexist
1913
+ // — gated on pageSupportsUnifiedDesignMode() (ARCH-886). The
1914
+ // arrange overlay stays passive until the inspector publishes a
1709
1915
  // single-element selection (via publishSelectionToDesignMode), at
1710
1916
  // which point it lights up the resize/drag controls on that element.
1711
- dispatchHostCommand({ type: "set-design-mode", enabled: true })
1917
+ if (pageSupportsUnifiedDesignMode()) {
1918
+ dispatchHostCommand({ type: "set-design-mode", enabled: true })
1919
+ }
1712
1920
  if (isInspecting) {
1713
1921
  // Recovery re-send (focus/visibility/online): the session is
1714
1922
  // already live. Activating again would wipe the user's current
@@ -2255,6 +2463,12 @@
2255
2463
  // nodeId is a source position and collides across instances; the
2256
2464
  // only identity that survives an insert above the edited row is
2257
2465
  // React's `key`, which exists on the fiber and nowhere else.
2466
+ // ARCH-1065 D8-v1/D26: NULL here means resolution never reached
2467
+ // a definitive answer (detached node, non-HTMLElement, or the
2468
+ // depth-bounded walk ran out before reaching the DOM root) — the
2469
+ // host MUST treat a null editorInstance exactly like the
2470
+ // existing `inCollection && fiberKey === null` refusal, never as
2471
+ // "not in a collection".
2258
2472
  //
2259
2473
  // Both are plain data, so an already-published copy of this script
2260
2474
  // forwards them through structured clone unchanged.
@@ -2276,6 +2490,12 @@
2276
2490
  // already-published copy of this script forwards it unchanged through
2277
2491
  // structured clone; a host that does not read it is unaffected.
2278
2492
  ancestorAnchors: collectAncestorAnchors(element),
2493
+ // ARCH-1065 M3 — the data-agent-* (D1 grammar) chain, self-outward,
2494
+ // for whatever binding attributes the M2 codemod (or a hand-written
2495
+ // kit component, M1) stamped. A host that does not read it is
2496
+ // unaffected; a pre-1065 page simply yields an empty array (`[]`),
2497
+ // same as `ancestorAnchors` does for a page with no anchors.
2498
+ bindingChain: collectBindingChain(element),
2279
2499
  computedStyles: {
2280
2500
  display: computed.display,
2281
2501
  position: computed.position,
@@ -3629,6 +3849,38 @@
3629
3849
  dispatchHostCommand({
3630
3850
  type: "external-drag-cancel",
3631
3851
  })
3852
+ } else if (event.data && event.data.type === "QUERY_SLICE_FIELD") {
3853
+ // ARCH-1065 M3 — {entity, field} -> {currentValue, valueType,
3854
+ // distinctValues} read live off window.__slice, for the FE's typed
3855
+ // controls (D7). `rowId` is optional: when omitted, the CURRENTLY
3856
+ // SELECTED (falling back to hovered) element's nearest
3857
+ // data-agent-row supplies it — the common case is "the user just
3858
+ // clicked a bound cell and wants its type/vocabulary", where the
3859
+ // row is already implied by that click.
3860
+ var queryEntity = event.data.entity
3861
+ var queryField = event.data.field
3862
+ var queryRowId =
3863
+ typeof event.data.rowId === "string" && event.data.rowId !== ""
3864
+ ? event.data.rowId
3865
+ : null
3866
+
3867
+ var queryResult = null
3868
+ if (typeof queryEntity === "string" && typeof queryField === "string") {
3869
+ var contextElement = queryRowId
3870
+ ? null
3871
+ : getSelectedElement() || highlightedElement
3872
+ var effectiveRowId =
3873
+ queryRowId || (contextElement ? nearestRowId(contextElement) : null)
3874
+ queryResult = querySliceField(queryEntity, queryField, effectiveRowId)
3875
+ }
3876
+
3877
+ postMessageToParent({
3878
+ type: "SLICE_FIELD_RESULT",
3879
+ requestId: event.data.requestId,
3880
+ entity: queryEntity,
3881
+ field: queryField,
3882
+ result: queryResult,
3883
+ })
3632
3884
  }
3633
3885
  }
3634
3886
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archie/devtools",
3
- "version": "0.0.25",
3
+ "version": "0.0.27",
4
4
  "description": "DevTools for Archie generated applications - Route synchronization and editor communication",
5
5
  "repository": {
6
6
  "type": "git",
@@ -24,6 +24,11 @@
24
24
  "import": "./dist/client/client.mjs",
25
25
  "require": "./dist/client/client.js"
26
26
  },
27
+ "./collection-binding": {
28
+ "types": "./dist/collection-binding.d.ts",
29
+ "import": "./dist/collection-binding.mjs",
30
+ "require": "./dist/collection-binding.js"
31
+ },
27
32
  "./inspector.standalone.js": "./dist/inspector.standalone.js"
28
33
  },
29
34
  "scripts": {