arcy.js 0.1.4 → 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.
@@ -1,8 +1,8 @@
1
1
  import { PICKER_CONTRACT, PICKER_GLOBAL } from './chunk-DKYMIAYF.js';
2
- import { SCREENSHOT_RENDER_BUDGET_MS } from './chunk-GDGE3HLE.js';
2
+ import { SCREENSHOT_RENDER_BUDGET_MS } from './chunk-7S5DA67F.js';
3
3
  import { createShell, injectStyles, warn, HOST_ATTRIBUTE } from './chunk-LGWYJSYX.js';
4
- import { matchTarget } from './chunk-NIHUDSWK.js';
5
- import { resolveInteractiveTarget, createTargetFingerprint, captureEventFingerprint, closestAcrossShadow, isStableId, containsPii, SELECTOR_CANDIDATES_MAX, collectElementsDeep, querySelectorAllDeep } from './chunk-NY3NXM2V.js';
4
+ import { matchTarget } from './chunk-QDCO2MRA.js';
5
+ import { resolveInteractiveTarget, createTargetFingerprint, captureEventFingerprint, closestAcrossShadow, isStableId, containsPii, SELECTOR_CANDIDATES_MAX, parentOrHost, collectElementsDeep, querySelectorAllDeep } from './chunk-AW7DHYSA.js';
6
6
 
7
7
  /* arcy.js — https://arcyai.com */
8
8
 
@@ -1667,6 +1667,7 @@ var OUTPUT_MAX_DPR = 2;
1667
1667
  var OUTPUT_MAX_PIXELS = 26e5;
1668
1668
  var DATA_URL_SOFT_MAX = 36e5;
1669
1669
  var RENDER_TIMEOUT_MS = SCREENSHOT_RENDER_BUDGET_MS;
1670
+ var RESOURCE_TIMEOUT_MS = 1500;
1670
1671
  var PII_PATTERNS = [
1671
1672
  /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
1672
1673
  /\b\d{3}-\d{2}-\d{4}\b/g,
@@ -1701,6 +1702,213 @@ function computeCropRect(target, viewportWidth, viewportHeight) {
1701
1702
  if (bottom - top > height) top += (bottom - top - height) / 2;
1702
1703
  return { left, top, width: Math.max(0, width), height: Math.max(0, height) };
1703
1704
  }
1705
+ var SETTLE_MAX_MS = 900;
1706
+ var SETTLE_STABLE_FRAMES = 2;
1707
+ function effectiveOpacity(el, view) {
1708
+ let opacity = 1;
1709
+ let current = el;
1710
+ let depth = 0;
1711
+ while (current !== null && depth < 64) {
1712
+ try {
1713
+ const value = Number.parseFloat(view.getComputedStyle(current).opacity);
1714
+ if (!Number.isNaN(value)) opacity *= value;
1715
+ } catch {
1716
+ break;
1717
+ }
1718
+ if (opacity <= 1e-3) return 0;
1719
+ current = parentOrHost(current);
1720
+ depth++;
1721
+ }
1722
+ return opacity;
1723
+ }
1724
+ function waitUntilSettled(el, view) {
1725
+ return new Promise((resolve) => {
1726
+ const raf = view.requestAnimationFrame;
1727
+ if (typeof raf !== "function") {
1728
+ resolve();
1729
+ return;
1730
+ }
1731
+ const start = Date.now();
1732
+ let previous = null;
1733
+ let stable = 0;
1734
+ const check = () => {
1735
+ try {
1736
+ if (Date.now() - start >= SETTLE_MAX_MS) {
1737
+ resolve();
1738
+ return;
1739
+ }
1740
+ const rect = el.getBoundingClientRect();
1741
+ const now = `${Math.round(rect.left)},${Math.round(rect.top)},${Math.round(rect.width)},${Math.round(rect.height)}`;
1742
+ const opaque = effectiveOpacity(el, view) >= 0.99;
1743
+ stable = opaque && now === previous ? stable + 1 : 0;
1744
+ previous = now;
1745
+ if (stable >= SETTLE_STABLE_FRAMES) {
1746
+ resolve();
1747
+ return;
1748
+ }
1749
+ raf.call(view, check);
1750
+ } catch {
1751
+ resolve();
1752
+ }
1753
+ };
1754
+ raf.call(view, check);
1755
+ });
1756
+ }
1757
+ function findRenderRoot(el, crop, root, view) {
1758
+ let covering = null;
1759
+ let cheapest = null;
1760
+ let current = el;
1761
+ let depth = 0;
1762
+ while (current !== null && current !== root && depth < 64) {
1763
+ if (isTransformed(current, view)) {
1764
+ covering = null;
1765
+ cheapest = null;
1766
+ } else {
1767
+ const rect = current.getBoundingClientRect();
1768
+ if (covering === null && covers(rect, crop)) covering = current;
1769
+ if (cheapest === null && keptRatio(rect, crop) >= MIN_CROP_KEPT) {
1770
+ cheapest = current;
1771
+ }
1772
+ }
1773
+ current = parentOrHost(current);
1774
+ depth++;
1775
+ }
1776
+ if (covering !== null && isAffordable(covering, view)) return covering;
1777
+ return cheapest ?? covering ?? root;
1778
+ }
1779
+ function isAffordable(el, view) {
1780
+ if (countElements(el) > NODE_BUDGET) return false;
1781
+ const rect = el.getBoundingClientRect();
1782
+ const viewport = Math.max(1, view.innerWidth * view.innerHeight);
1783
+ return rect.width * rect.height / viewport <= AREA_BUDGET_SCREENS;
1784
+ }
1785
+ var NODE_BUDGET = 800;
1786
+ var AREA_BUDGET_SCREENS = 3;
1787
+ var MIN_CROP_KEPT = 0.4;
1788
+ function keptRatio(bounds, crop) {
1789
+ const whole = crop.width * crop.height;
1790
+ if (whole <= 0) return 0;
1791
+ const kept = clipCrop(crop, {
1792
+ left: bounds.left,
1793
+ top: bounds.top,
1794
+ width: bounds.width,
1795
+ height: bounds.height
1796
+ });
1797
+ return kept.width * kept.height / whole;
1798
+ }
1799
+ function countElements(el) {
1800
+ try {
1801
+ return el.getElementsByTagName("*").length;
1802
+ } catch {
1803
+ return Number.MAX_SAFE_INTEGER;
1804
+ }
1805
+ }
1806
+ function clipCrop(crop, bounds) {
1807
+ const left = Math.max(crop.left, bounds.left);
1808
+ const top = Math.max(crop.top, bounds.top);
1809
+ const right = Math.min(crop.left + crop.width, bounds.left + bounds.width);
1810
+ const bottom = Math.min(crop.top + crop.height, bounds.top + bounds.height);
1811
+ return {
1812
+ left,
1813
+ top,
1814
+ width: Math.max(0, right - left),
1815
+ height: Math.max(0, bottom - top)
1816
+ };
1817
+ }
1818
+ function isTransformed(el, view) {
1819
+ try {
1820
+ const style = view.getComputedStyle(el);
1821
+ return style.transform !== "" && style.transform !== "none" || style.perspective !== "" && style.perspective !== "none" || style.translate !== "" && style.translate !== "none" || style.rotate !== "" && style.rotate !== "none" || style.scale !== "" && style.scale !== "none";
1822
+ } catch {
1823
+ return false;
1824
+ }
1825
+ }
1826
+ function covers(rect, crop) {
1827
+ 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;
1828
+ }
1829
+ function backgroundUnder(root, crop, doc, view) {
1830
+ const points = [
1831
+ [crop.left + crop.width / 2, crop.top + crop.height / 2],
1832
+ [crop.left + 1, crop.top + 1],
1833
+ [crop.left + crop.width - 1, crop.top + crop.height - 1]
1834
+ ];
1835
+ for (const [x, y] of points) {
1836
+ let stack = [];
1837
+ try {
1838
+ stack = doc.elementsFromPoint(x, y) ?? [];
1839
+ } catch {
1840
+ break;
1841
+ }
1842
+ for (const candidate of stack) {
1843
+ if (candidate === root || root.contains(candidate)) continue;
1844
+ if (candidate.hasAttribute(HOST_ATTRIBUTE)) continue;
1845
+ const color = opaqueBackground(candidate, view);
1846
+ if (color) return color;
1847
+ }
1848
+ }
1849
+ let current = root;
1850
+ let depth = 0;
1851
+ while (current !== null && depth < 64) {
1852
+ const color = opaqueBackground(current, view);
1853
+ if (color) return color;
1854
+ current = parentOrHost(current);
1855
+ depth++;
1856
+ }
1857
+ return "#ffffff";
1858
+ }
1859
+ function opaqueBackground(el, view) {
1860
+ try {
1861
+ const color = view.getComputedStyle(el).backgroundColor;
1862
+ if (!color || color === "transparent") return null;
1863
+ if (color.replace(/\s/g, "").startsWith("rgba(0,0,0,0)")) return null;
1864
+ return color;
1865
+ } catch {
1866
+ return null;
1867
+ }
1868
+ }
1869
+ function renderRootStyle(root, rootRect, doc, view, shiftX, shiftY) {
1870
+ const placement = {
1871
+ transform: `translate(${shiftX}px, ${shiftY}px)`,
1872
+ transformOrigin: "top left",
1873
+ margin: "0"
1874
+ };
1875
+ if (root === doc.documentElement) {
1876
+ return {
1877
+ ...placement,
1878
+ width: `${view.innerWidth}px`,
1879
+ minHeight: `${view.innerHeight}px`,
1880
+ overflow: "visible"
1881
+ };
1882
+ }
1883
+ return {
1884
+ ...placement,
1885
+ position: "static",
1886
+ inset: "auto",
1887
+ float: "none",
1888
+ // The size has to be stated, and this is the line the whole subtree render
1889
+ // stands on. The clone is laid out inside a `foreignObject` the size of
1890
+ // the CROP, and a block element with no width of its own fills its
1891
+ // parent: a 1440px section became 163px wide and the page reflowed into a
1892
+ // column, which is why the hero came back as an empty panel rather than a
1893
+ // misplaced one. `documentElement` was pinned the same way from the start,
1894
+ // which is why nobody met this until the root became something else.
1895
+ // Border-box, because a measured rect is a border box.
1896
+ boxSizing: "border-box",
1897
+ width: `${rootRect.width}px`,
1898
+ minHeight: `${rootRect.height}px`
1899
+ };
1900
+ }
1901
+ function withDeadline(work, ms, view) {
1902
+ return Promise.race([
1903
+ work,
1904
+ new Promise((resolve) => {
1905
+ try {
1906
+ view.setTimeout(() => resolve(null), ms);
1907
+ } catch {
1908
+ }
1909
+ })
1910
+ ]);
1911
+ }
1704
1912
  function isTextualField(el) {
1705
1913
  if (el instanceof HTMLTextAreaElement) return true;
1706
1914
  if (!(el instanceof HTMLInputElement)) return false;
@@ -1710,6 +1918,57 @@ function isTextualField(el) {
1710
1918
  function isCapturable(node) {
1711
1919
  return !(node instanceof Element && node.hasAttribute(HOST_ATTRIBUTE));
1712
1920
  }
1921
+ function repairAutoMargins(live, clone, view) {
1922
+ let repaired = 0;
1923
+ const walk = (liveNode, cloneNode2, depth) => {
1924
+ if (depth > 64 || repaired > 2e3) return;
1925
+ const liveKids = liveNode.children;
1926
+ const cloneKids = cloneNode2.children;
1927
+ let l = 0;
1928
+ let c = 0;
1929
+ while (l < liveKids.length && c < cloneKids.length) {
1930
+ const liveKid = liveKids[l];
1931
+ const cloneKid = cloneKids[c];
1932
+ if (liveKid.tagName !== cloneKid.tagName) {
1933
+ l++;
1934
+ continue;
1935
+ }
1936
+ if (applyUsedMargins(liveNode, liveKid, cloneKid, view)) repaired++;
1937
+ walk(liveKid, cloneKid, depth + 1);
1938
+ l++;
1939
+ c++;
1940
+ }
1941
+ };
1942
+ try {
1943
+ walk(live, clone, 0);
1944
+ } catch {
1945
+ }
1946
+ }
1947
+ var BLOCK_DISPLAYS = /* @__PURE__ */ new Set(["block", "flow-root", "list-item", "table-cell"]);
1948
+ function applyUsedMargins(liveParent, live, clone, view) {
1949
+ try {
1950
+ const style = view.getComputedStyle(live);
1951
+ if (style.marginLeft !== "0px" || style.marginRight !== "0px") return false;
1952
+ if (style.position !== "static" && style.position !== "relative") return false;
1953
+ if (style.float !== "none") return false;
1954
+ const parentStyle = view.getComputedStyle(liveParent);
1955
+ if (!BLOCK_DISPLAYS.has(parentStyle.display)) return false;
1956
+ const rect = live.getBoundingClientRect();
1957
+ const parentRect = liveParent.getBoundingClientRect();
1958
+ const contentLeft = parentRect.left + parseFloat(parentStyle.borderLeftWidth || "0") + parseFloat(parentStyle.paddingLeft || "0");
1959
+ const contentRight = parentRect.right - parseFloat(parentStyle.borderRightWidth || "0") - parseFloat(parentStyle.paddingRight || "0");
1960
+ const left = rect.left - contentLeft;
1961
+ const right = contentRight - rect.right;
1962
+ if (left < 0.5 || right < 0.5 || Math.abs(left - right) > 1) return false;
1963
+ const target = clone;
1964
+ if (!target.style) return false;
1965
+ target.style.setProperty("margin-left", `${left}px`);
1966
+ target.style.setProperty("margin-right", `${right}px`);
1967
+ return true;
1968
+ } catch {
1969
+ return false;
1970
+ }
1971
+ }
1713
1972
  function maskClone(root) {
1714
1973
  const doc = root.ownerDocument ?? root;
1715
1974
  const view = doc.defaultView;
@@ -1742,7 +2001,8 @@ function maskClone(root) {
1742
2001
  el.value = masked;
1743
2002
  }
1744
2003
  const placeholder = el.getAttribute("placeholder");
1745
- if (placeholder) el.setAttribute("placeholder", maskPiiText(placeholder));
2004
+ if (placeholder)
2005
+ el.setAttribute("placeholder", maskPiiText(placeholder));
1746
2006
  }
1747
2007
  }
1748
2008
  node = walker.nextNode();
@@ -1782,6 +2042,8 @@ async function captureElementScreenshot(el) {
1782
2042
  const doc = el.ownerDocument;
1783
2043
  const view = doc.defaultView;
1784
2044
  if (!view || !doc.documentElement || !el.isConnected) return null;
2045
+ await waitUntilSettled(el, view);
2046
+ if (!el.isConnected) return null;
1785
2047
  const targetRectRaw = el.getBoundingClientRect();
1786
2048
  const targetRect = {
1787
2049
  left: targetRectRaw.left,
@@ -1790,31 +2052,54 @@ async function captureElementScreenshot(el) {
1790
2052
  height: targetRectRaw.height
1791
2053
  };
1792
2054
  if (targetRect.width <= 0 || targetRect.height <= 0) return null;
1793
- const cropRect = computeCropRect(targetRect, view.innerWidth, view.innerHeight);
2055
+ const idealCrop = computeCropRect(
2056
+ targetRect,
2057
+ view.innerWidth,
2058
+ view.innerHeight
2059
+ );
2060
+ if (idealCrop.width <= 0 || idealCrop.height <= 0) return null;
2061
+ const renderRoot = findRenderRoot(el, idealCrop, doc.documentElement, view);
2062
+ const rootRect = renderRoot.getBoundingClientRect();
2063
+ const rootBox = {
2064
+ left: rootRect.left,
2065
+ top: rootRect.top,
2066
+ width: rootRect.width,
2067
+ height: rootRect.height
2068
+ };
2069
+ const cropRect = clipCrop(idealCrop, rootBox);
1794
2070
  if (cropRect.width <= 0 || cropRect.height <= 0) return null;
1795
2071
  const scale = outputScale(view, cropRect);
1796
- const shiftX = -(cropRect.left + (view.scrollX || 0));
1797
- const shiftY = -(cropRect.top + (view.scrollY || 0));
1798
- const canvas = await domToCanvas(doc.documentElement, {
1799
- width: cropRect.width,
1800
- height: cropRect.height,
1801
- scale,
1802
- backgroundColor: "#ffffff",
1803
- timeout: RENDER_TIMEOUT_MS,
1804
- filter: isCapturable,
1805
- onCloneNode: (cloned) => {
1806
- maskClone(cloned);
1807
- },
1808
- font: { preferredFormat: "woff2" },
1809
- style: {
1810
- transform: `translate(${shiftX}px, ${shiftY}px)`,
1811
- transformOrigin: "top left",
1812
- width: `${view.innerWidth}px`,
1813
- minHeight: `${view.innerHeight}px`,
1814
- margin: "0",
1815
- overflow: "visible"
1816
- }
1817
- });
2072
+ const shiftX = -(cropRect.left - rootRect.left);
2073
+ const shiftY = -(cropRect.top - rootRect.top);
2074
+ const canvas = await withDeadline(
2075
+ domToCanvas(renderRoot, {
2076
+ width: cropRect.width,
2077
+ height: cropRect.height,
2078
+ scale,
2079
+ backgroundColor: backgroundUnder(renderRoot, cropRect, doc, view),
2080
+ // Per resource, not per capture. `RENDER_TIMEOUT_MS` is the total,
2081
+ // and it is the race around this call that enforces it.
2082
+ timeout: RESOURCE_TIMEOUT_MS,
2083
+ filter: isCapturable,
2084
+ onCloneNode: (cloned) => {
2085
+ if (cloned instanceof Element) {
2086
+ repairAutoMargins(renderRoot, cloned, view);
2087
+ }
2088
+ maskClone(cloned);
2089
+ },
2090
+ // No `preferredFormat`. Asking for woff2 sounds like asking for the
2091
+ // smaller file and is really a filter: a face whose chosen source is
2092
+ // not woff2 is dropped rather than downgraded, and the text renders in
2093
+ // the browser's serif fallback. On our own hero that turned the button
2094
+ // label into Times (D1258). Let the library take whatever each face
2095
+ // actually offers.
2096
+ font: {},
2097
+ style: renderRootStyle(renderRoot, rootBox, doc, view, shiftX, shiftY)
2098
+ }),
2099
+ RENDER_TIMEOUT_MS,
2100
+ view
2101
+ );
2102
+ if (!canvas) return null;
1818
2103
  const ctx = canvas.getContext("2d");
1819
2104
  if (!ctx) return null;
1820
2105
  drawOutline(ctx, targetRect, cropRect, scale);
@@ -1834,48 +2119,91 @@ async function captureElementScreenshot(el) {
1834
2119
  var PICKER_HOST_ID = "arcy-picker";
1835
2120
  var BANNER_CLASS = "arcy-picker-banner";
1836
2121
  var HIGHLIGHT_CLASS = "arcy-picker-highlight";
2122
+ var INK = "#17131A";
2123
+ var SURFACE = "#201B23";
2124
+ var HAIRLINE = "#332B33";
2125
+ var TEXT = "#F3EEE8";
2126
+ var MUTED = "#9E959A";
2127
+ var ACCENT = "#FF5A3C";
2128
+ var ON_ACCENT = "#17131A";
1837
2129
  var PICKER_CSS = `
1838
2130
  .${BANNER_CLASS} {
1839
2131
  display: flex;
1840
2132
  align-items: center;
1841
- gap: 10px;
1842
- padding: 10px 14px;
2133
+ gap: 12px;
2134
+ padding: 7px 8px 7px 16px;
1843
2135
  border-radius: 999px;
1844
- background: #101828;
1845
- color: #ffffff;
1846
- font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
1847
- box-shadow: 0 8px 24px rgba(16, 24, 40, 0.28);
2136
+ background: ${INK};
2137
+ border: 1px solid ${HAIRLINE};
2138
+ color: ${TEXT};
2139
+ font: 400 14px/1.2 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
2140
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.38);
1848
2141
  white-space: nowrap;
1849
2142
  }
1850
- .${BANNER_CLASS}__title {
2143
+ .${BANNER_CLASS}__mark {
2144
+ display: flex;
2145
+ align-items: baseline;
2146
+ gap: 7px;
2147
+ }
2148
+ .${BANNER_CLASS}__brand {
1851
2149
  font-weight: 600;
1852
- margin-right: 2px;
2150
+ letter-spacing: 0.02em;
1853
2151
  }
1854
- .${BANNER_CLASS}__status {
1855
- max-width: 260px;
1856
- overflow: hidden;
1857
- text-overflow: ellipsis;
1858
- color: #d0d5dd;
2152
+ /* The label role from DESIGN.md: 600, 12px, uppercase, tracked wide. */
2153
+ .${BANNER_CLASS}__label {
2154
+ font-size: 12px;
2155
+ font-weight: 600;
2156
+ letter-spacing: 0.14em;
2157
+ text-transform: uppercase;
2158
+ color: ${MUTED};
2159
+ }
2160
+ /* Pick and Browse are one control with two states, not two buttons that
2161
+ * happen to disagree. The old bar drew both as outlines and coloured whichever
2162
+ * was on, which reads as two things you can press rather than as a switch. */
2163
+ .${BANNER_CLASS}__modes {
2164
+ display: flex;
2165
+ align-items: center;
2166
+ gap: 2px;
2167
+ padding: 3px;
2168
+ border-radius: 999px;
2169
+ background: ${SURFACE};
1859
2170
  }
1860
2171
  .${BANNER_CLASS} button {
1861
2172
  appearance: none;
1862
- border: 1px solid rgba(255, 255, 255, 0.25);
2173
+ border: 1px solid transparent;
1863
2174
  border-radius: 999px;
1864
2175
  background: transparent;
1865
- color: #ffffff;
2176
+ color: ${MUTED};
1866
2177
  font: inherit;
1867
- padding: 4px 12px;
2178
+ font-weight: 500;
2179
+ padding: 5px 14px;
1868
2180
  cursor: pointer;
2181
+ transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
1869
2182
  }
1870
2183
  .${BANNER_CLASS} button:hover {
1871
- background: rgba(255, 255, 255, 0.1);
2184
+ color: ${TEXT};
2185
+ }
2186
+ .${BANNER_CLASS} button:focus-visible {
2187
+ outline: 2px solid ${ACCENT};
2188
+ outline-offset: 2px;
1872
2189
  }
1873
- .${BANNER_CLASS} button.is-active {
1874
- background: #f04e23;
1875
- border-color: #f04e23;
2190
+ /* The one filled control on the bar (DESIGN.md, one accent fill per surface). */
2191
+ .${BANNER_CLASS}__modes button.is-active {
2192
+ background: ${ACCENT};
2193
+ color: ${ON_ACCENT};
2194
+ font-weight: 600;
2195
+ }
2196
+ .${BANNER_CLASS}__modes button.is-active:hover {
2197
+ color: ${ON_ACCENT};
2198
+ }
2199
+ .${BANNER_CLASS}__exit {
2200
+ border-color: ${HAIRLINE} !important;
2201
+ }
2202
+ .${BANNER_CLASS}__exit:hover {
2203
+ background: ${SURFACE};
1876
2204
  }
1877
2205
  .${HIGHLIGHT_CLASS} {
1878
- border-radius: 3px;
2206
+ border-radius: 4px;
1879
2207
  }
1880
2208
  `;
1881
2209
  function applySurvivalStyle(el, pointerEvents, offsets = []) {
@@ -1887,8 +2215,13 @@ function applySurvivalStyle(el, pointerEvents, offsets = []) {
1887
2215
  }
1888
2216
  var HIGHLIGHT_STYLE = [
1889
2217
  ["box-sizing", "border-box"],
1890
- ["border", "2px solid #f04e23"],
1891
- ["background", "rgba(240, 78, 35, 0.08)"]
2218
+ [
2219
+ "border",
2220
+ // ADR 0171's accent border rule: an edge, not a fill. The tint behind it
2221
+ // is what keeps a thin outline readable on a busy page.
2222
+ `2px solid ${ACCENT}`
2223
+ ],
2224
+ ["background", "rgba(255, 90, 60, 0.1)"]
1892
2225
  ];
1893
2226
  var BANNER_OFFSET = [
1894
2227
  ["left", "50%"],
@@ -1897,10 +2230,6 @@ var BANNER_OFFSET = [
1897
2230
  var BANNER_STYLE = [
1898
2231
  ["transform", "translateX(-50%)"]
1899
2232
  ];
1900
- function statusLabel(target) {
1901
- const { text, tag } = target.core;
1902
- return text ? `Sent "${text}" to ARCY` : `Sent <${tag}> to ARCY`;
1903
- }
1904
2233
  function createPickerOverlay(context) {
1905
2234
  let shell = createShell({ id: PICKER_HOST_ID });
1906
2235
  if (!shell) return null;
@@ -1923,28 +2252,36 @@ function createPickerOverlay(context) {
1923
2252
  for (const [property, value] of BANNER_STYLE) {
1924
2253
  banner.style.setProperty(property, value);
1925
2254
  }
1926
- const title = doc.createElement("span");
1927
- title.className = `${BANNER_CLASS}__title`;
1928
- title.textContent = "ARCY element picker";
2255
+ const mark = doc.createElement("span");
2256
+ mark.className = `${BANNER_CLASS}__mark`;
2257
+ const brand = doc.createElement("span");
2258
+ brand.className = `${BANNER_CLASS}__brand`;
2259
+ brand.textContent = "ARCY";
2260
+ const label = doc.createElement("span");
2261
+ label.className = `${BANNER_CLASS}__label`;
2262
+ label.textContent = "Element picker";
2263
+ mark.append(brand, label);
2264
+ const modes = doc.createElement("span");
2265
+ modes.className = `${BANNER_CLASS}__modes`;
2266
+ applySurvivalStyle(modes, "auto");
2267
+ modes.style.setProperty("position", "relative", "important");
1929
2268
  const pickButton = doc.createElement("button");
1930
2269
  pickButton.type = "button";
1931
2270
  pickButton.textContent = "Pick";
1932
2271
  const browseButton = doc.createElement("button");
1933
2272
  browseButton.type = "button";
1934
2273
  browseButton.textContent = "Browse";
1935
- const status = doc.createElement("span");
1936
- status.className = `${BANNER_CLASS}__status`;
1937
- status.textContent = "Click the element you want this step to target.";
2274
+ modes.append(pickButton, browseButton);
1938
2275
  const exitButton = doc.createElement("button");
1939
2276
  exitButton.type = "button";
2277
+ exitButton.className = `${BANNER_CLASS}__exit`;
1940
2278
  exitButton.textContent = "Exit";
1941
- banner.append(title, pickButton, browseButton, status, exitButton);
2279
+ banner.append(mark, modes, exitButton);
1942
2280
  shell.root.append(highlight, banner);
1943
2281
  function setMode(next) {
1944
2282
  mode = next;
1945
2283
  pickButton.classList.toggle("is-active", next === "pick");
1946
2284
  browseButton.classList.toggle("is-active", next === "browse");
1947
- status.textContent = next === "pick" ? "Click the element you want this step to target." : "Browsing. Clicks work normally; switch back to pick.";
1948
2285
  if (next === "browse") hideHighlight();
1949
2286
  }
1950
2287
  function hideHighlight() {
@@ -2027,7 +2364,6 @@ function createPickerOverlay(context) {
2027
2364
  const ranked = rankSelectors(el);
2028
2365
  fingerprint.selector = ranked.autoSelector;
2029
2366
  fingerprint.selectorCandidates = ranked.candidates;
2030
- status.textContent = statusLabel(fingerprint);
2031
2367
  hovered = el;
2032
2368
  moveHighlight(el);
2033
2369
  let matchCount = 0;
@@ -2038,17 +2374,29 @@ function createPickerOverlay(context) {
2038
2374
  context.onPick(fingerprint, matchCount);
2039
2375
  if (context.screenshotsEnabled === false) return;
2040
2376
  const token = ++screenshotToken;
2041
- void captureElementScreenshot(el).then((screenshot) => {
2042
- if (!screenshot || destroyed || token !== screenshotToken) return;
2043
- try {
2044
- context.onScreenshot?.(screenshot);
2045
- } catch {
2046
- }
2377
+ afterNextPaint(() => {
2378
+ if (destroyed || token !== screenshotToken) return;
2379
+ void captureElementScreenshot(el).then((screenshot) => {
2380
+ if (!screenshot || destroyed || token !== screenshotToken) return;
2381
+ try {
2382
+ context.onScreenshot?.(screenshot);
2383
+ } catch {
2384
+ }
2385
+ });
2047
2386
  });
2048
2387
  } catch (error) {
2049
2388
  warn(`The element picker could not read that element. ${String(error)}`);
2050
2389
  }
2051
2390
  };
2391
+ function afterNextPaint(fn) {
2392
+ const view = doc.defaultView;
2393
+ const raf = view?.requestAnimationFrame;
2394
+ if (typeof raf !== "function") {
2395
+ setTimeout(fn, 0);
2396
+ return;
2397
+ }
2398
+ raf.call(view, () => raf.call(view, fn));
2399
+ }
2052
2400
  function isEditable(target) {
2053
2401
  if (!(target instanceof Element)) return false;
2054
2402
  const tag = target.tagName;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcy.js",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "ARCY — the universal JavaScript utility for embedding an AI agent that talks to, learns from, and acts on behalf of your users.",
5
5
  "license": "MIT",
6
6
  "type": "module",