arcy.js 0.1.5 → 0.1.6

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.cjs CHANGED
@@ -361,7 +361,7 @@ var init_message = __esm({
361
361
  DESIGN_MODE_PICK = "arcy:design-mode:pick";
362
362
  DESIGN_MODE_EXIT = "arcy:design-mode:exit";
363
363
  DESIGN_MODE_SCREENSHOT = "arcy:design-mode:screenshot";
364
- SCREENSHOT_RENDER_BUDGET_MS = 8e3;
364
+ SCREENSHOT_RENDER_BUDGET_MS = 5e3;
365
365
  }
366
366
  });
367
367
 
@@ -5636,6 +5636,25 @@ function evaluateCurrentPage(node, href) {
5636
5636
  if (node.matches.length === 0) return true;
5637
5637
  return node.matches.some((pattern) => globMatches(pattern, href));
5638
5638
  }
5639
+ function parseInstant(value) {
5640
+ if (!DATE_SHAPED.test(value)) return null;
5641
+ const ms = new Date(value).getTime();
5642
+ return Number.isFinite(ms) ? ms : null;
5643
+ }
5644
+ function compareOrdered(live, expected) {
5645
+ if (live.trim() === "" || expected.trim() === "") return null;
5646
+ const liveNumber = Number(live);
5647
+ const expectedNumber = Number(expected);
5648
+ if (Number.isFinite(liveNumber) && Number.isFinite(expectedNumber)) {
5649
+ return liveNumber - expectedNumber;
5650
+ }
5651
+ const liveInstant = parseInstant(live);
5652
+ const expectedInstant = parseInstant(expected);
5653
+ if (liveInstant !== null && expectedInstant !== null) {
5654
+ return liveInstant - expectedInstant;
5655
+ }
5656
+ return null;
5657
+ }
5639
5658
  function compareText(operator, live, expected) {
5640
5659
  if (live === void 0) return false;
5641
5660
  switch (operator) {
@@ -5657,16 +5676,14 @@ function compareText(operator, live, expected) {
5657
5676
  return live === "";
5658
5677
  case "is_not_empty":
5659
5678
  return live !== "";
5660
- // Numeric compares parse both sides; either side not a number means the
5661
- // condition cannot hold (never throw, never coerce NaN into an answer).
5679
+ // Ordered compares read both sides as numbers, then as instants; either
5680
+ // side unreadable means the condition cannot hold (never throw, never
5681
+ // coerce NaN into an answer).
5662
5682
  case "greater_than":
5663
5683
  case "less_than": {
5664
- const liveNumber = Number(live);
5665
- const expectedNumber = Number(expected);
5666
- if (live.trim() === "" || !Number.isFinite(liveNumber) || !Number.isFinite(expectedNumber)) {
5667
- return false;
5668
- }
5669
- return operator === "greater_than" ? liveNumber > expectedNumber : liveNumber < expectedNumber;
5684
+ const ordered = compareOrdered(live, expected);
5685
+ if (ordered === null) return false;
5686
+ return operator === "greater_than" ? ordered > 0 : ordered < 0;
5670
5687
  }
5671
5688
  // The same `*` glob `current_page` uses, applied to a field value.
5672
5689
  case "matches_pattern":
@@ -5735,15 +5752,22 @@ function evaluateElement(node, ctx) {
5735
5752
  return false;
5736
5753
  }
5737
5754
  }
5755
+ function isEmptyValue(live) {
5756
+ if (live === void 0 || live === null || live === "") return true;
5757
+ if (Array.isArray(live)) return live.length === 0;
5758
+ if (typeof live === "object") return Object.keys(live).length === 0;
5759
+ return false;
5760
+ }
5738
5761
  function evaluateAttribute(node, ctx) {
5739
5762
  const live = ctx.getTrait?.(node.codeName);
5740
5763
  switch (node.operator) {
5741
5764
  case "has_any_value":
5742
- return live !== void 0 && live !== null && live !== "";
5765
+ return !isEmptyValue(live);
5743
5766
  case "is_empty":
5744
- return live === void 0 || live === null || live === "";
5767
+ return isEmptyValue(live);
5745
5768
  }
5746
5769
  if (live === void 0) return false;
5770
+ if (typeof live === "object") return false;
5747
5771
  const liveText = String(live);
5748
5772
  const expected = node.value === void 0 ? "" : String(node.value);
5749
5773
  return compareText(node.operator, liveText, expected);
@@ -5796,7 +5820,7 @@ function needsIdleCheck(node) {
5796
5820
  if (node.type === "group") return node.children.some(needsIdleCheck);
5797
5821
  return false;
5798
5822
  }
5799
- var MAX_CONDITION_DEPTH, CONDITION_TYPES;
5823
+ var MAX_CONDITION_DEPTH, CONDITION_TYPES, DATE_SHAPED;
5800
5824
  var init_condition = __esm({
5801
5825
  "src/flow/condition.ts"() {
5802
5826
  init_matcher();
@@ -5813,6 +5837,7 @@ var init_condition = __esm({
5813
5837
  "always_true",
5814
5838
  "group"
5815
5839
  ];
5840
+ DATE_SHAPED = /^\d{4}-\d{2}-\d{2}/;
5816
5841
  }
5817
5842
  });
5818
5843
 
@@ -8647,6 +8672,208 @@ function computeCropRect(target, viewportWidth, viewportHeight) {
8647
8672
  if (bottom - top > height) top += (bottom - top - height) / 2;
8648
8673
  return { left, top, width: Math.max(0, width), height: Math.max(0, height) };
8649
8674
  }
8675
+ function effectiveOpacity(el, view) {
8676
+ let opacity = 1;
8677
+ let current = el;
8678
+ let depth = 0;
8679
+ while (current !== null && depth < 64) {
8680
+ try {
8681
+ const value = Number.parseFloat(view.getComputedStyle(current).opacity);
8682
+ if (!Number.isNaN(value)) opacity *= value;
8683
+ } catch {
8684
+ break;
8685
+ }
8686
+ if (opacity <= 1e-3) return 0;
8687
+ current = parentOrHost(current);
8688
+ depth++;
8689
+ }
8690
+ return opacity;
8691
+ }
8692
+ function waitUntilSettled(el, view) {
8693
+ return new Promise((resolve) => {
8694
+ const raf = view.requestAnimationFrame;
8695
+ if (typeof raf !== "function") {
8696
+ resolve();
8697
+ return;
8698
+ }
8699
+ const start = Date.now();
8700
+ let previous = null;
8701
+ let stable = 0;
8702
+ const check = () => {
8703
+ try {
8704
+ if (Date.now() - start >= SETTLE_MAX_MS) {
8705
+ resolve();
8706
+ return;
8707
+ }
8708
+ const rect = el.getBoundingClientRect();
8709
+ const now = `${Math.round(rect.left)},${Math.round(rect.top)},${Math.round(rect.width)},${Math.round(rect.height)}`;
8710
+ const opaque = effectiveOpacity(el, view) >= 0.99;
8711
+ stable = opaque && now === previous ? stable + 1 : 0;
8712
+ previous = now;
8713
+ if (stable >= SETTLE_STABLE_FRAMES) {
8714
+ resolve();
8715
+ return;
8716
+ }
8717
+ raf.call(view, check);
8718
+ } catch {
8719
+ resolve();
8720
+ }
8721
+ };
8722
+ raf.call(view, check);
8723
+ });
8724
+ }
8725
+ function findRenderRoot(el, crop, root, view) {
8726
+ let covering = null;
8727
+ let cheapest = null;
8728
+ let current = el;
8729
+ let depth = 0;
8730
+ while (current !== null && current !== root && depth < 64) {
8731
+ if (isTransformed(current, view)) {
8732
+ covering = null;
8733
+ cheapest = null;
8734
+ } else {
8735
+ const rect = current.getBoundingClientRect();
8736
+ if (covering === null && covers(rect, crop)) covering = current;
8737
+ if (cheapest === null && keptRatio(rect, crop) >= MIN_CROP_KEPT) {
8738
+ cheapest = current;
8739
+ }
8740
+ }
8741
+ current = parentOrHost(current);
8742
+ depth++;
8743
+ }
8744
+ if (covering !== null && isAffordable(covering, view)) return covering;
8745
+ return cheapest ?? covering ?? root;
8746
+ }
8747
+ function isAffordable(el, view) {
8748
+ if (countElements(el) > NODE_BUDGET) return false;
8749
+ const rect = el.getBoundingClientRect();
8750
+ const viewport = Math.max(1, view.innerWidth * view.innerHeight);
8751
+ return rect.width * rect.height / viewport <= AREA_BUDGET_SCREENS;
8752
+ }
8753
+ function keptRatio(bounds, crop) {
8754
+ const whole = crop.width * crop.height;
8755
+ if (whole <= 0) return 0;
8756
+ const kept = clipCrop(crop, {
8757
+ left: bounds.left,
8758
+ top: bounds.top,
8759
+ width: bounds.width,
8760
+ height: bounds.height
8761
+ });
8762
+ return kept.width * kept.height / whole;
8763
+ }
8764
+ function countElements(el) {
8765
+ try {
8766
+ return el.getElementsByTagName("*").length;
8767
+ } catch {
8768
+ return Number.MAX_SAFE_INTEGER;
8769
+ }
8770
+ }
8771
+ function clipCrop(crop, bounds) {
8772
+ const left = Math.max(crop.left, bounds.left);
8773
+ const top = Math.max(crop.top, bounds.top);
8774
+ const right = Math.min(crop.left + crop.width, bounds.left + bounds.width);
8775
+ const bottom = Math.min(crop.top + crop.height, bounds.top + bounds.height);
8776
+ return {
8777
+ left,
8778
+ top,
8779
+ width: Math.max(0, right - left),
8780
+ height: Math.max(0, bottom - top)
8781
+ };
8782
+ }
8783
+ function isTransformed(el, view) {
8784
+ try {
8785
+ const style2 = view.getComputedStyle(el);
8786
+ return style2.transform !== "" && style2.transform !== "none" || style2.perspective !== "" && style2.perspective !== "none" || style2.translate !== "" && style2.translate !== "none" || style2.rotate !== "" && style2.rotate !== "none" || style2.scale !== "" && style2.scale !== "none";
8787
+ } catch {
8788
+ return false;
8789
+ }
8790
+ }
8791
+ function covers(rect, crop) {
8792
+ return rect.left <= crop.left + 0.5 && rect.top <= crop.top + 0.5 && rect.right >= crop.left + crop.width - 0.5 && rect.bottom >= crop.top + crop.height - 0.5;
8793
+ }
8794
+ function backgroundUnder(root, crop, doc, view) {
8795
+ const points = [
8796
+ [crop.left + crop.width / 2, crop.top + crop.height / 2],
8797
+ [crop.left + 1, crop.top + 1],
8798
+ [crop.left + crop.width - 1, crop.top + crop.height - 1]
8799
+ ];
8800
+ for (const [x, y] of points) {
8801
+ let stack = [];
8802
+ try {
8803
+ stack = doc.elementsFromPoint(x, y) ?? [];
8804
+ } catch {
8805
+ break;
8806
+ }
8807
+ for (const candidate of stack) {
8808
+ if (candidate === root || root.contains(candidate)) continue;
8809
+ if (candidate.hasAttribute(HOST_ATTRIBUTE)) continue;
8810
+ const color = opaqueBackground(candidate, view);
8811
+ if (color) return color;
8812
+ }
8813
+ }
8814
+ let current = root;
8815
+ let depth = 0;
8816
+ while (current !== null && depth < 64) {
8817
+ const color = opaqueBackground(current, view);
8818
+ if (color) return color;
8819
+ current = parentOrHost(current);
8820
+ depth++;
8821
+ }
8822
+ return "#ffffff";
8823
+ }
8824
+ function opaqueBackground(el, view) {
8825
+ try {
8826
+ const color = view.getComputedStyle(el).backgroundColor;
8827
+ if (!color || color === "transparent") return null;
8828
+ if (color.replace(/\s/g, "").startsWith("rgba(0,0,0,0)")) return null;
8829
+ return color;
8830
+ } catch {
8831
+ return null;
8832
+ }
8833
+ }
8834
+ function renderRootStyle(root, rootRect, doc, view, shiftX, shiftY) {
8835
+ const placement = {
8836
+ transform: `translate(${shiftX}px, ${shiftY}px)`,
8837
+ transformOrigin: "top left",
8838
+ margin: "0"
8839
+ };
8840
+ if (root === doc.documentElement) {
8841
+ return {
8842
+ ...placement,
8843
+ width: `${view.innerWidth}px`,
8844
+ minHeight: `${view.innerHeight}px`,
8845
+ overflow: "visible"
8846
+ };
8847
+ }
8848
+ return {
8849
+ ...placement,
8850
+ position: "static",
8851
+ inset: "auto",
8852
+ float: "none",
8853
+ // The size has to be stated, and this is the line the whole subtree render
8854
+ // stands on. The clone is laid out inside a `foreignObject` the size of
8855
+ // the CROP, and a block element with no width of its own fills its
8856
+ // parent: a 1440px section became 163px wide and the page reflowed into a
8857
+ // column, which is why the hero came back as an empty panel rather than a
8858
+ // misplaced one. `documentElement` was pinned the same way from the start,
8859
+ // which is why nobody met this until the root became something else.
8860
+ // Border-box, because a measured rect is a border box.
8861
+ boxSizing: "border-box",
8862
+ width: `${rootRect.width}px`,
8863
+ minHeight: `${rootRect.height}px`
8864
+ };
8865
+ }
8866
+ function withDeadline(work, ms, view) {
8867
+ return Promise.race([
8868
+ work,
8869
+ new Promise((resolve) => {
8870
+ try {
8871
+ view.setTimeout(() => resolve(null), ms);
8872
+ } catch {
8873
+ }
8874
+ })
8875
+ ]);
8876
+ }
8650
8877
  function isTextualField(el) {
8651
8878
  if (el instanceof HTMLTextAreaElement) return true;
8652
8879
  if (!(el instanceof HTMLInputElement)) return false;
@@ -8656,6 +8883,56 @@ function isTextualField(el) {
8656
8883
  function isCapturable(node) {
8657
8884
  return !(node instanceof Element && node.hasAttribute(HOST_ATTRIBUTE));
8658
8885
  }
8886
+ function repairAutoMargins(live, clone, view) {
8887
+ let repaired = 0;
8888
+ const walk = (liveNode, cloneNode2, depth) => {
8889
+ if (depth > 64 || repaired > 2e3) return;
8890
+ const liveKids = liveNode.children;
8891
+ const cloneKids = cloneNode2.children;
8892
+ let l = 0;
8893
+ let c = 0;
8894
+ while (l < liveKids.length && c < cloneKids.length) {
8895
+ const liveKid = liveKids[l];
8896
+ const cloneKid = cloneKids[c];
8897
+ if (liveKid.tagName !== cloneKid.tagName) {
8898
+ l++;
8899
+ continue;
8900
+ }
8901
+ if (applyUsedMargins(liveNode, liveKid, cloneKid, view)) repaired++;
8902
+ walk(liveKid, cloneKid, depth + 1);
8903
+ l++;
8904
+ c++;
8905
+ }
8906
+ };
8907
+ try {
8908
+ walk(live, clone, 0);
8909
+ } catch {
8910
+ }
8911
+ }
8912
+ function applyUsedMargins(liveParent, live, clone, view) {
8913
+ try {
8914
+ const style2 = view.getComputedStyle(live);
8915
+ if (style2.marginLeft !== "0px" || style2.marginRight !== "0px") return false;
8916
+ if (style2.position !== "static" && style2.position !== "relative") return false;
8917
+ if (style2.float !== "none") return false;
8918
+ const parentStyle = view.getComputedStyle(liveParent);
8919
+ if (!BLOCK_DISPLAYS.has(parentStyle.display)) return false;
8920
+ const rect = live.getBoundingClientRect();
8921
+ const parentRect = liveParent.getBoundingClientRect();
8922
+ const contentLeft = parentRect.left + parseFloat(parentStyle.borderLeftWidth || "0") + parseFloat(parentStyle.paddingLeft || "0");
8923
+ const contentRight = parentRect.right - parseFloat(parentStyle.borderRightWidth || "0") - parseFloat(parentStyle.paddingRight || "0");
8924
+ const left = rect.left - contentLeft;
8925
+ const right = contentRight - rect.right;
8926
+ if (left < 0.5 || right < 0.5 || Math.abs(left - right) > 1) return false;
8927
+ const target = clone;
8928
+ if (!target.style) return false;
8929
+ target.style.setProperty("margin-left", `${left}px`);
8930
+ target.style.setProperty("margin-right", `${right}px`);
8931
+ return true;
8932
+ } catch {
8933
+ return false;
8934
+ }
8935
+ }
8659
8936
  function maskClone(root) {
8660
8937
  const doc = root.ownerDocument ?? root;
8661
8938
  const view = doc.defaultView;
@@ -8688,7 +8965,8 @@ function maskClone(root) {
8688
8965
  el.value = masked;
8689
8966
  }
8690
8967
  const placeholder = el.getAttribute("placeholder");
8691
- if (placeholder) el.setAttribute("placeholder", maskPiiText(placeholder));
8968
+ if (placeholder)
8969
+ el.setAttribute("placeholder", maskPiiText(placeholder));
8692
8970
  }
8693
8971
  }
8694
8972
  node = walker.nextNode();
@@ -8728,6 +9006,8 @@ async function captureElementScreenshot(el) {
8728
9006
  const doc = el.ownerDocument;
8729
9007
  const view = doc.defaultView;
8730
9008
  if (!view || !doc.documentElement || !el.isConnected) return null;
9009
+ await waitUntilSettled(el, view);
9010
+ if (!el.isConnected) return null;
8731
9011
  const targetRectRaw = el.getBoundingClientRect();
8732
9012
  const targetRect = {
8733
9013
  left: targetRectRaw.left,
@@ -8736,31 +9016,54 @@ async function captureElementScreenshot(el) {
8736
9016
  height: targetRectRaw.height
8737
9017
  };
8738
9018
  if (targetRect.width <= 0 || targetRect.height <= 0) return null;
8739
- const cropRect = computeCropRect(targetRect, view.innerWidth, view.innerHeight);
9019
+ const idealCrop = computeCropRect(
9020
+ targetRect,
9021
+ view.innerWidth,
9022
+ view.innerHeight
9023
+ );
9024
+ if (idealCrop.width <= 0 || idealCrop.height <= 0) return null;
9025
+ const renderRoot = findRenderRoot(el, idealCrop, doc.documentElement, view);
9026
+ const rootRect = renderRoot.getBoundingClientRect();
9027
+ const rootBox = {
9028
+ left: rootRect.left,
9029
+ top: rootRect.top,
9030
+ width: rootRect.width,
9031
+ height: rootRect.height
9032
+ };
9033
+ const cropRect = clipCrop(idealCrop, rootBox);
8740
9034
  if (cropRect.width <= 0 || cropRect.height <= 0) return null;
8741
9035
  const scale = outputScale(view, cropRect);
8742
- const shiftX = -(cropRect.left + (view.scrollX || 0));
8743
- const shiftY = -(cropRect.top + (view.scrollY || 0));
8744
- const canvas = await domToCanvas(doc.documentElement, {
8745
- width: cropRect.width,
8746
- height: cropRect.height,
8747
- scale,
8748
- backgroundColor: "#ffffff",
8749
- timeout: RENDER_TIMEOUT_MS,
8750
- filter: isCapturable,
8751
- onCloneNode: (cloned) => {
8752
- maskClone(cloned);
8753
- },
8754
- font: { preferredFormat: "woff2" },
8755
- style: {
8756
- transform: `translate(${shiftX}px, ${shiftY}px)`,
8757
- transformOrigin: "top left",
8758
- width: `${view.innerWidth}px`,
8759
- minHeight: `${view.innerHeight}px`,
8760
- margin: "0",
8761
- overflow: "visible"
8762
- }
8763
- });
9036
+ const shiftX = -(cropRect.left - rootRect.left);
9037
+ const shiftY = -(cropRect.top - rootRect.top);
9038
+ const canvas = await withDeadline(
9039
+ domToCanvas(renderRoot, {
9040
+ width: cropRect.width,
9041
+ height: cropRect.height,
9042
+ scale,
9043
+ backgroundColor: backgroundUnder(renderRoot, cropRect, doc, view),
9044
+ // Per resource, not per capture. `RENDER_TIMEOUT_MS` is the total,
9045
+ // and it is the race around this call that enforces it.
9046
+ timeout: RESOURCE_TIMEOUT_MS,
9047
+ filter: isCapturable,
9048
+ onCloneNode: (cloned) => {
9049
+ if (cloned instanceof Element) {
9050
+ repairAutoMargins(renderRoot, cloned, view);
9051
+ }
9052
+ maskClone(cloned);
9053
+ },
9054
+ // No `preferredFormat`. Asking for woff2 sounds like asking for the
9055
+ // smaller file and is really a filter: a face whose chosen source is
9056
+ // not woff2 is dropped rather than downgraded, and the text renders in
9057
+ // the browser's serif fallback. On our own hero that turned the button
9058
+ // label into Times (D1258). Let the library take whatever each face
9059
+ // actually offers.
9060
+ font: {},
9061
+ style: renderRootStyle(renderRoot, rootBox, doc, view, shiftX, shiftY)
9062
+ }),
9063
+ RENDER_TIMEOUT_MS,
9064
+ view
9065
+ );
9066
+ if (!canvas) return null;
8764
9067
  const ctx = canvas.getContext("2d");
8765
9068
  if (!ctx) return null;
8766
9069
  drawOutline(ctx, targetRect, cropRect, scale);
@@ -8775,10 +9078,11 @@ async function captureElementScreenshot(el) {
8775
9078
  return null;
8776
9079
  }
8777
9080
  }
8778
- var CROP_MARGIN_RATIO, CROP_MARGIN_MAX_PX, CROP_MARGIN_MIN_PX, CROP_MAX_WIDTH, CROP_MAX_HEIGHT, OUTPUT_MAX_DPR, OUTPUT_MAX_PIXELS, DATA_URL_SOFT_MAX, RENDER_TIMEOUT_MS, PII_PATTERNS2, MASK2, OUTLINE_COLOR;
9081
+ var CROP_MARGIN_RATIO, CROP_MARGIN_MAX_PX, CROP_MARGIN_MIN_PX, CROP_MAX_WIDTH, CROP_MAX_HEIGHT, OUTPUT_MAX_DPR, OUTPUT_MAX_PIXELS, DATA_URL_SOFT_MAX, RENDER_TIMEOUT_MS, RESOURCE_TIMEOUT_MS, PII_PATTERNS2, MASK2, OUTLINE_COLOR, SETTLE_MAX_MS, SETTLE_STABLE_FRAMES, NODE_BUDGET, AREA_BUDGET_SCREENS, MIN_CROP_KEPT, BLOCK_DISPLAYS;
8779
9082
  var init_screenshot = __esm({
8780
9083
  "src/picker/screenshot.ts"() {
8781
9084
  init_dist();
9085
+ init_shadow();
8782
9086
  init_host();
8783
9087
  init_message();
8784
9088
  CROP_MARGIN_RATIO = 0.25;
@@ -8790,6 +9094,7 @@ var init_screenshot = __esm({
8790
9094
  OUTPUT_MAX_PIXELS = 26e5;
8791
9095
  DATA_URL_SOFT_MAX = 36e5;
8792
9096
  RENDER_TIMEOUT_MS = SCREENSHOT_RENDER_BUDGET_MS;
9097
+ RESOURCE_TIMEOUT_MS = 1500;
8793
9098
  PII_PATTERNS2 = [
8794
9099
  /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
8795
9100
  /\b\d{3}-\d{2}-\d{4}\b/g,
@@ -8797,6 +9102,12 @@ var init_screenshot = __esm({
8797
9102
  ];
8798
9103
  MASK2 = "[masked]";
8799
9104
  OUTLINE_COLOR = "#f04e23";
9105
+ SETTLE_MAX_MS = 900;
9106
+ SETTLE_STABLE_FRAMES = 2;
9107
+ NODE_BUDGET = 800;
9108
+ AREA_BUDGET_SCREENS = 3;
9109
+ MIN_CROP_KEPT = 0.4;
9110
+ BLOCK_DISPLAYS = /* @__PURE__ */ new Set(["block", "flow-root", "list-item", "table-cell"]);
8800
9111
  }
8801
9112
  });
8802
9113
 
@@ -8808,10 +9119,6 @@ function applySurvivalStyle(el, pointerEvents, offsets = []) {
8808
9119
  el.style.setProperty(property, value, "important");
8809
9120
  }
8810
9121
  }
8811
- function statusLabel(target) {
8812
- const { text, tag } = target.core;
8813
- return text ? `Sent "${text}" to ARCY` : `Sent <${tag}> to ARCY`;
8814
- }
8815
9122
  function createPickerOverlay(context) {
8816
9123
  let shell = createShell({ id: PICKER_HOST_ID });
8817
9124
  if (!shell) return null;
@@ -8834,28 +9141,36 @@ function createPickerOverlay(context) {
8834
9141
  for (const [property, value] of BANNER_STYLE) {
8835
9142
  banner.style.setProperty(property, value);
8836
9143
  }
8837
- const title = doc.createElement("span");
8838
- title.className = `${BANNER_CLASS}__title`;
8839
- title.textContent = "ARCY element picker";
9144
+ const mark = doc.createElement("span");
9145
+ mark.className = `${BANNER_CLASS}__mark`;
9146
+ const brand = doc.createElement("span");
9147
+ brand.className = `${BANNER_CLASS}__brand`;
9148
+ brand.textContent = "ARCY";
9149
+ const label = doc.createElement("span");
9150
+ label.className = `${BANNER_CLASS}__label`;
9151
+ label.textContent = "Element picker";
9152
+ mark.append(brand, label);
9153
+ const modes = doc.createElement("span");
9154
+ modes.className = `${BANNER_CLASS}__modes`;
9155
+ applySurvivalStyle(modes, "auto");
9156
+ modes.style.setProperty("position", "relative", "important");
8840
9157
  const pickButton = doc.createElement("button");
8841
9158
  pickButton.type = "button";
8842
9159
  pickButton.textContent = "Pick";
8843
9160
  const browseButton = doc.createElement("button");
8844
9161
  browseButton.type = "button";
8845
9162
  browseButton.textContent = "Browse";
8846
- const status = doc.createElement("span");
8847
- status.className = `${BANNER_CLASS}__status`;
8848
- status.textContent = "Click the element you want this step to target.";
9163
+ modes.append(pickButton, browseButton);
8849
9164
  const exitButton = doc.createElement("button");
8850
9165
  exitButton.type = "button";
9166
+ exitButton.className = `${BANNER_CLASS}__exit`;
8851
9167
  exitButton.textContent = "Exit";
8852
- banner.append(title, pickButton, browseButton, status, exitButton);
9168
+ banner.append(mark, modes, exitButton);
8853
9169
  shell.root.append(highlight, banner);
8854
9170
  function setMode(next) {
8855
9171
  mode = next;
8856
9172
  pickButton.classList.toggle("is-active", next === "pick");
8857
9173
  browseButton.classList.toggle("is-active", next === "browse");
8858
- status.textContent = next === "pick" ? "Click the element you want this step to target." : "Browsing. Clicks work normally; switch back to pick.";
8859
9174
  if (next === "browse") hideHighlight();
8860
9175
  }
8861
9176
  function hideHighlight() {
@@ -8938,7 +9253,6 @@ function createPickerOverlay(context) {
8938
9253
  const ranked = rankSelectors(el);
8939
9254
  fingerprint.selector = ranked.autoSelector;
8940
9255
  fingerprint.selectorCandidates = ranked.candidates;
8941
- status.textContent = statusLabel(fingerprint);
8942
9256
  hovered = el;
8943
9257
  moveHighlight(el);
8944
9258
  let matchCount = 0;
@@ -8949,17 +9263,29 @@ function createPickerOverlay(context) {
8949
9263
  context.onPick(fingerprint, matchCount);
8950
9264
  if (context.screenshotsEnabled === false) return;
8951
9265
  const token = ++screenshotToken;
8952
- void captureElementScreenshot(el).then((screenshot) => {
8953
- if (!screenshot || destroyed || token !== screenshotToken) return;
8954
- try {
8955
- context.onScreenshot?.(screenshot);
8956
- } catch {
8957
- }
9266
+ afterNextPaint(() => {
9267
+ if (destroyed || token !== screenshotToken) return;
9268
+ void captureElementScreenshot(el).then((screenshot) => {
9269
+ if (!screenshot || destroyed || token !== screenshotToken) return;
9270
+ try {
9271
+ context.onScreenshot?.(screenshot);
9272
+ } catch {
9273
+ }
9274
+ });
8958
9275
  });
8959
9276
  } catch (error) {
8960
9277
  warn(`The element picker could not read that element. ${String(error)}`);
8961
9278
  }
8962
9279
  };
9280
+ function afterNextPaint(fn) {
9281
+ const view = doc.defaultView;
9282
+ const raf = view?.requestAnimationFrame;
9283
+ if (typeof raf !== "function") {
9284
+ setTimeout(fn, 0);
9285
+ return;
9286
+ }
9287
+ raf.call(view, () => raf.call(view, fn));
9288
+ }
8963
9289
  function isEditable(target) {
8964
9290
  if (!(target instanceof Element)) return false;
8965
9291
  const tag = target.tagName;
@@ -9025,7 +9351,7 @@ function createPickerOverlay(context) {
9025
9351
  setMode("pick");
9026
9352
  return { destroy };
9027
9353
  }
9028
- var PICKER_HOST_ID, BANNER_CLASS, HIGHLIGHT_CLASS, PICKER_CSS, HIGHLIGHT_STYLE, BANNER_OFFSET, BANNER_STYLE;
9354
+ var PICKER_HOST_ID, BANNER_CLASS, HIGHLIGHT_CLASS, INK, SURFACE, HAIRLINE, TEXT, MUTED, ACCENT, ON_ACCENT, PICKER_CSS, HIGHLIGHT_STYLE, BANNER_OFFSET, BANNER_STYLE;
9029
9355
  var init_overlay = __esm({
9030
9356
  "src/picker/overlay.ts"() {
9031
9357
  init_capture();
@@ -9039,54 +9365,102 @@ var init_overlay = __esm({
9039
9365
  PICKER_HOST_ID = "arcy-picker";
9040
9366
  BANNER_CLASS = "arcy-picker-banner";
9041
9367
  HIGHLIGHT_CLASS = "arcy-picker-highlight";
9368
+ INK = "#17131A";
9369
+ SURFACE = "#201B23";
9370
+ HAIRLINE = "#332B33";
9371
+ TEXT = "#F3EEE8";
9372
+ MUTED = "#9E959A";
9373
+ ACCENT = "#FF5A3C";
9374
+ ON_ACCENT = "#17131A";
9042
9375
  PICKER_CSS = `
9043
9376
  .${BANNER_CLASS} {
9044
9377
  display: flex;
9045
9378
  align-items: center;
9046
- gap: 10px;
9047
- padding: 10px 14px;
9379
+ gap: 12px;
9380
+ padding: 7px 8px 7px 16px;
9048
9381
  border-radius: 999px;
9049
- background: #101828;
9050
- color: #ffffff;
9051
- font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
9052
- box-shadow: 0 8px 24px rgba(16, 24, 40, 0.28);
9382
+ background: ${INK};
9383
+ border: 1px solid ${HAIRLINE};
9384
+ color: ${TEXT};
9385
+ font: 400 14px/1.2 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
9386
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.38);
9053
9387
  white-space: nowrap;
9054
9388
  }
9055
- .${BANNER_CLASS}__title {
9389
+ .${BANNER_CLASS}__mark {
9390
+ display: flex;
9391
+ align-items: baseline;
9392
+ gap: 7px;
9393
+ }
9394
+ .${BANNER_CLASS}__brand {
9056
9395
  font-weight: 600;
9057
- margin-right: 2px;
9396
+ letter-spacing: 0.02em;
9058
9397
  }
9059
- .${BANNER_CLASS}__status {
9060
- max-width: 260px;
9061
- overflow: hidden;
9062
- text-overflow: ellipsis;
9063
- color: #d0d5dd;
9398
+ /* The label role from DESIGN.md: 600, 12px, uppercase, tracked wide. */
9399
+ .${BANNER_CLASS}__label {
9400
+ font-size: 12px;
9401
+ font-weight: 600;
9402
+ letter-spacing: 0.14em;
9403
+ text-transform: uppercase;
9404
+ color: ${MUTED};
9405
+ }
9406
+ /* Pick and Browse are one control with two states, not two buttons that
9407
+ * happen to disagree. The old bar drew both as outlines and coloured whichever
9408
+ * was on, which reads as two things you can press rather than as a switch. */
9409
+ .${BANNER_CLASS}__modes {
9410
+ display: flex;
9411
+ align-items: center;
9412
+ gap: 2px;
9413
+ padding: 3px;
9414
+ border-radius: 999px;
9415
+ background: ${SURFACE};
9064
9416
  }
9065
9417
  .${BANNER_CLASS} button {
9066
9418
  appearance: none;
9067
- border: 1px solid rgba(255, 255, 255, 0.25);
9419
+ border: 1px solid transparent;
9068
9420
  border-radius: 999px;
9069
9421
  background: transparent;
9070
- color: #ffffff;
9422
+ color: ${MUTED};
9071
9423
  font: inherit;
9072
- padding: 4px 12px;
9424
+ font-weight: 500;
9425
+ padding: 5px 14px;
9073
9426
  cursor: pointer;
9427
+ transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
9074
9428
  }
9075
9429
  .${BANNER_CLASS} button:hover {
9076
- background: rgba(255, 255, 255, 0.1);
9430
+ color: ${TEXT};
9431
+ }
9432
+ .${BANNER_CLASS} button:focus-visible {
9433
+ outline: 2px solid ${ACCENT};
9434
+ outline-offset: 2px;
9077
9435
  }
9078
- .${BANNER_CLASS} button.is-active {
9079
- background: #f04e23;
9080
- border-color: #f04e23;
9436
+ /* The one filled control on the bar (DESIGN.md, one accent fill per surface). */
9437
+ .${BANNER_CLASS}__modes button.is-active {
9438
+ background: ${ACCENT};
9439
+ color: ${ON_ACCENT};
9440
+ font-weight: 600;
9441
+ }
9442
+ .${BANNER_CLASS}__modes button.is-active:hover {
9443
+ color: ${ON_ACCENT};
9444
+ }
9445
+ .${BANNER_CLASS}__exit {
9446
+ border-color: ${HAIRLINE} !important;
9447
+ }
9448
+ .${BANNER_CLASS}__exit:hover {
9449
+ background: ${SURFACE};
9081
9450
  }
9082
9451
  .${HIGHLIGHT_CLASS} {
9083
- border-radius: 3px;
9452
+ border-radius: 4px;
9084
9453
  }
9085
9454
  `;
9086
9455
  HIGHLIGHT_STYLE = [
9087
9456
  ["box-sizing", "border-box"],
9088
- ["border", "2px solid #f04e23"],
9089
- ["background", "rgba(240, 78, 35, 0.08)"]
9457
+ [
9458
+ "border",
9459
+ // ADR 0171's accent border rule: an edge, not a fill. The tint behind it
9460
+ // is what keeps a thin outline readable on a busy page.
9461
+ `2px solid ${ACCENT}`
9462
+ ],
9463
+ ["background", "rgba(255, 90, 60, 0.1)"]
9090
9464
  ];
9091
9465
  BANNER_OFFSET = [
9092
9466
  ["left", "50%"],
@@ -13502,15 +13876,19 @@ function createArcyInternals(internalOptions = {}) {
13502
13876
  token,
13503
13877
  sessionToken,
13504
13878
  sessionId: telemetry?.sessionId(),
13505
- // Slice 18.1's trigger conditions read traits set via identify();
13506
- // `Attributes` also allows null and string[], neither of which
13507
- // `ConditionContext.getTrait` accepts, so both degrade to
13508
- // "unresolvable" rather than being coerced into something wrong.
13879
+ // Slice 18.1's trigger conditions read attributes set via identify().
13880
+ // Every present, non-null value is reported as it is and judged here
13881
+ // not at all (ADR 0205). Filtering lists out used to report a written
13882
+ // key as absent, which made `has_any_value` false for a list the
13883
+ // customer had sent; what a value *means* is `condition.ts`'s call and
13884
+ // is made in one place there.
13885
+ //
13886
+ // Nothing becomes comparable by arriving: `condition.ts` refuses every
13887
+ // comparison on an object or an array (ADR 0198's opaque rule), so no
13888
+ // value here can become `is "[object Object]"`.
13509
13889
  getTrait: (codeName) => {
13510
13890
  const value = identity.attributes[codeName];
13511
- if (value === void 0 || value === null || Array.isArray(value)) {
13512
- return void 0;
13513
- }
13891
+ if (value === void 0 || value === null) return void 0;
13514
13892
  return value;
13515
13893
  },
13516
13894
  // D1299: absent entirely (rather than answering false) when storage