veo-sdk 0.6.2 → 0.6.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.
package/dist/index.cjs CHANGED
@@ -251,7 +251,8 @@ var init_constants = __esm({
251
251
  throttleMs: 100,
252
252
  maxTextLength: 200,
253
253
  captureElementMetadata: true,
254
- captureAncestors: 5
254
+ captureAncestors: 5,
255
+ captureClickQuality: true
255
256
  };
256
257
  MAX_ANCESTORS = 10;
257
258
  ALWAYS_BLOCK_SELECTORS = [
@@ -440,13 +441,14 @@ var init_activation_rules = __esm({
440
441
  });
441
442
 
442
443
  // src/plugins/guides/constants.ts
443
- var GUIDE_Z_INDEX, DEFAULT_ANCHOR_WAIT_MS, SAFE_URL_PROTOCOLS, GUIDE_HOST_ATTR, FREQUENCY_CACHE_KEY_PREFIX, DEFAULT_TRACKER_FLUSH_INTERVAL_MS, DEFAULT_TRACKER_BATCH_SIZE, DEFAULT_TRACKER_MAX_RETRIES, FREQUENCY_CACHE_MAX_ENTRIES, FREQUENCY_CACHE_TTL_MS, WALKTHROUGH_STATE_KEY_PREFIX, WALKTHROUGH_ABANDONMENT_TIMEOUT_MS, WALKTHROUGH_STEP_SELECTOR_TIMEOUT_MS;
444
+ var GUIDE_Z_INDEX, DEFAULT_ANCHOR_WAIT_MS, SAFE_URL_PROTOCOLS, GUIDE_HOST_ATTR, BADGE_MARKER_HOST_ATTR, FREQUENCY_CACHE_KEY_PREFIX, DEFAULT_TRACKER_FLUSH_INTERVAL_MS, DEFAULT_TRACKER_BATCH_SIZE, DEFAULT_TRACKER_MAX_RETRIES, FREQUENCY_CACHE_MAX_ENTRIES, FREQUENCY_CACHE_TTL_MS, WALKTHROUGH_STATE_KEY_PREFIX, WALKTHROUGH_ABANDONMENT_TIMEOUT_MS, WALKTHROUGH_STEP_SELECTOR_TIMEOUT_MS;
444
445
  var init_constants2 = __esm({
445
446
  "src/plugins/guides/constants.ts"() {
446
447
  GUIDE_Z_INDEX = 2147483640;
447
448
  DEFAULT_ANCHOR_WAIT_MS = 5e3;
448
449
  SAFE_URL_PROTOCOLS = ["http:", "https:"];
449
450
  GUIDE_HOST_ATTR = "data-veo-guide";
451
+ BADGE_MARKER_HOST_ATTR = "data-veo-badge";
450
452
  FREQUENCY_CACHE_KEY_PREFIX = "veo:freq:";
451
453
  DEFAULT_TRACKER_FLUSH_INTERVAL_MS = 3e3;
452
454
  DEFAULT_TRACKER_BATCH_SIZE = 5;
@@ -2056,6 +2058,25 @@ var init_base_renderer = __esm({
2056
2058
  this.shadow = shadow;
2057
2059
  return { host, shadow, root };
2058
2060
  }
2061
+ /**
2062
+ * Host SUELTO con los mismos estilos (shadow DOM), sin agregarlo al DOM ni
2063
+ * registrarlo como `this.host`. Para piezas que deben vivir dentro de la app
2064
+ * (el marcador de un badge, junto a su ancla) mientras la tarjeta se queda
2065
+ * en `body`: si la tarjeta viajara dentro del ancla, un ancestro con
2066
+ * `transform`/`overflow` la recortaría y su z-index quedaría atrapado.
2067
+ */
2068
+ createDetachedHost(attr = BADGE_MARKER_HOST_ATTR) {
2069
+ const host = document.createElement("div");
2070
+ host.setAttribute(attr, "");
2071
+ const shadow = host.attachShadow({ mode: "open" });
2072
+ const style = document.createElement("style");
2073
+ style.textContent = GUIDE_STYLES;
2074
+ shadow.appendChild(style);
2075
+ const root = document.createElement("div");
2076
+ shadow.appendChild(root);
2077
+ this.registerCleanup(() => host.remove());
2078
+ return { host, root };
2079
+ }
2059
2080
  /**
2060
2081
  * Aplica la tematización por datos del step (`step.style`) como variables
2061
2082
  * CSS sobre el host. Llamar tras `createHost()`. No-op si no hay host o
@@ -3592,6 +3613,18 @@ var init_snooze = __esm({
3592
3613
  };
3593
3614
  }
3594
3615
  });
3616
+
3617
+ // src/plugins/guides/z-index.ts
3618
+ function clampZIndex(value, fallback) {
3619
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
3620
+ return Math.max(-MAX_Z_INDEX, Math.min(MAX_Z_INDEX, Math.round(value)));
3621
+ }
3622
+ var MAX_Z_INDEX;
3623
+ var init_z_index = __esm({
3624
+ "src/plugins/guides/z-index.ts"() {
3625
+ MAX_Z_INDEX = 2147483647;
3626
+ }
3627
+ });
3595
3628
  function num(v, fallback) {
3596
3629
  return typeof v === "number" && Number.isFinite(v) ? v : fallback;
3597
3630
  }
@@ -3741,7 +3774,7 @@ function canNest(anchor) {
3741
3774
  }
3742
3775
  function attachMarker(anchor, host, marker) {
3743
3776
  host.style.position = "absolute";
3744
- host.style.zIndex = String(num(marker.zIndex, DEFAULT_MARKER_Z));
3777
+ host.style.zIndex = String(clampZIndex(marker.zIndex, DEFAULT_MARKER_Z));
3745
3778
  host.style.margin = "0";
3746
3779
  const position = marker.position ?? "top-right";
3747
3780
  const offset6 = { x: num(marker.offsetX, 0), y: num(marker.offsetY, 0) };
@@ -3816,6 +3849,7 @@ function attachMarker(anchor, host, marker) {
3816
3849
  var INFO_GLYPH, QUESTION_GLYPH, STAR_GLYPH, CIRCLE, TARGET_GLYPH, BADGE_ICONS, DEFAULT_MARKER_SIZE, DEFAULT_MARKER_Z, SIDE_GAP, NO_CHILDREN;
3817
3850
  var init_badge_marker = __esm({
3818
3851
  "src/plugins/guides/renderers/badge-marker.ts"() {
3852
+ init_z_index();
3819
3853
  INFO_GLYPH = "M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021";
3820
3854
  QUESTION_GLYPH = "M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75";
3821
3855
  STAR_GLYPH = "M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z";
@@ -3942,7 +3976,7 @@ function liftTarget(anchor, zIndex) {
3942
3976
  const prevPosition = el2.style.position;
3943
3977
  const prevZ = el2.style.zIndex;
3944
3978
  if (getComputedStyle(el2).position === "static") el2.style.position = "relative";
3945
- el2.style.zIndex = String(zIndex);
3979
+ el2.style.zIndex = String(clampZIndex(zIndex, 0));
3946
3980
  return () => {
3947
3981
  el2.style.position = prevPosition;
3948
3982
  el2.style.zIndex = prevZ;
@@ -4033,6 +4067,7 @@ var init_composite_renderer = __esm({
4033
4067
  init_scroll_lock();
4034
4068
  init_snooze();
4035
4069
  init_wait_for_element();
4070
+ init_z_index();
4036
4071
  init_badge_marker();
4037
4072
  init_base_renderer();
4038
4073
  init_floating_arrow();
@@ -4080,7 +4115,9 @@ var init_composite_renderer = __esm({
4080
4115
  const { host, root } = this.createHost();
4081
4116
  const doc = root.ownerDocument ?? document;
4082
4117
  const design = step.design ?? {};
4083
- if (typeof design.zIndex === "number") host.style.zIndex = String(design.zIndex);
4118
+ if (typeof design.zIndex === "number") {
4119
+ host.style.zIndex = String(clampZIndex(design.zIndex, 0));
4120
+ }
4084
4121
  const card = doc.createElement("div");
4085
4122
  card.className = "veo-card";
4086
4123
  const themeMode = themeModeFor(context.guide.theme, design);
@@ -4189,7 +4226,7 @@ var init_composite_renderer = __esm({
4189
4226
  const bg = doc.createElement("div");
4190
4227
  bg.className = "veo-composite-overlay veo-composite-overlay--dim";
4191
4228
  if (typeof design.backdropColor === "string") bg.style.background = design.backdropColor;
4192
- const bz = typeof design.backdropZIndex === "number" ? design.backdropZIndex : 2e5;
4229
+ const bz = clampZIndex(design.backdropZIndex, 2e5);
4193
4230
  bg.style.zIndex = String(bz);
4194
4231
  root.appendChild(bg);
4195
4232
  this.registerCleanup(lockScroll(doc));
@@ -4211,7 +4248,7 @@ var init_composite_renderer = __esm({
4211
4248
  overlay.style.background = design.backdropColor;
4212
4249
  }
4213
4250
  if (typeof design.backdropZIndex === "number") {
4214
- overlay.style.zIndex = String(design.backdropZIndex);
4251
+ overlay.style.zIndex = String(clampZIndex(design.backdropZIndex, 0));
4215
4252
  }
4216
4253
  overlay.addEventListener("click", (e) => {
4217
4254
  if (e.target === overlay) {
@@ -4532,6 +4569,7 @@ var init_composite_renderer = __esm({
4532
4569
  renderBadgeMode(step, card, anchor, anchorSelector, host, root, doc, context, idx, design) {
4533
4570
  const trigger = step.badge?.trigger === "click" ? "click" : "hover";
4534
4571
  const placement = step.badge?.placement === "top" || step.badge?.placement === "left" || step.badge?.placement === "right" ? step.badge.placement : "bottom";
4572
+ const { host: markerHost, root: markerRoot } = this.createDetachedHost();
4535
4573
  const badge = doc.createElement("div");
4536
4574
  badge.className = "veo-composite-badge";
4537
4575
  badge.setAttribute("role", "button");
@@ -4542,7 +4580,8 @@ var init_composite_renderer = __esm({
4542
4580
  const marker = step.badge?.marker ?? null;
4543
4581
  if (marker) badge.appendChild(buildMarkerElement(doc, marker));
4544
4582
  else this.buildContent(badge, step.badge?.blocks ?? [], doc, context, idx, [], host);
4545
- root.appendChild(badge);
4583
+ markerRoot.appendChild(badge);
4584
+ host.style.display = "block";
4546
4585
  if (design.fullWidth === true) {
4547
4586
  card.style.width = "auto";
4548
4587
  card.style.maxWidth = "320px";
@@ -4557,13 +4596,13 @@ var init_composite_renderer = __esm({
4557
4596
  wrapper.appendChild(card);
4558
4597
  root.appendChild(wrapper);
4559
4598
  if (marker) {
4560
- host.style.display = "inline-block";
4561
- this.registerCleanup(attachMarker(anchor, host, marker));
4599
+ markerHost.style.display = "inline-block";
4600
+ this.registerCleanup(attachMarker(anchor, markerHost, marker));
4562
4601
  } else {
4563
- host.style.display = "inline-block";
4602
+ markerHost.style.display = "inline-block";
4564
4603
  const pos = inlinePosOf(step.inlinePosition);
4565
- insertHost(anchor, host, pos);
4566
- this.registerCleanup(keepHostAttached(host, anchorSelector, pos));
4604
+ insertHost(anchor, markerHost, pos);
4605
+ this.registerCleanup(keepHostAttached(markerHost, anchorSelector, pos));
4567
4606
  }
4568
4607
  let open = false;
4569
4608
  let stopFloat = null;
@@ -5690,14 +5729,19 @@ function injectBuilderChrome(opts) {
5690
5729
  let previewOn = false;
5691
5730
  let hiddenOn = false;
5692
5731
  let heightCb = null;
5693
- const height = () => CHROME_TOPBAR_H + (collapsed ? 0 : CHROME_STEPS_H);
5732
+ let tagging = false;
5733
+ const height = () => CHROME_TOPBAR_H + (collapsed || tagging ? 0 : CHROME_STEPS_H);
5694
5734
  const applyHeight = () => {
5695
5735
  const h = previewOn || hiddenOn ? 0 : height();
5696
5736
  html.style.marginTop = dock === "top" ? `${h}px` : prevMarginTop;
5697
5737
  html.style.marginBottom = dock === "bottom" ? `${h}px` : prevMarginBottom;
5698
5738
  root.style.setProperty("--veo-chrome-h", `${height()}px`);
5699
5739
  root.classList.toggle("is-bottom", dock === "bottom");
5700
- stepsBar.hidden = collapsed;
5740
+ stepsBar.hidden = collapsed || tagging;
5741
+ togglePill.hidden = tagging;
5742
+ preview.hidden = tagging;
5743
+ saveGroup.hidden = tagging;
5744
+ showPanel.title = tagging ? "Mostrar el panel" : "Mostrar el editor";
5701
5745
  const open = dock === "top" ? "chevronUp" : "chevronDown";
5702
5746
  const closedIcon = dock === "top" ? "chevronDown" : "chevronUp";
5703
5747
  togglePill.innerHTML = `<span>${collapsed ? "Mostrar pasos" : "Ocultar pasos"}</span>${svg(
@@ -5750,8 +5794,13 @@ function injectBuilderChrome(opts) {
5750
5794
  const render = () => {
5751
5795
  const s = current;
5752
5796
  if (!s) return;
5797
+ const nextTagging = s.variant === "tagging";
5798
+ if (nextTagging !== tagging) {
5799
+ tagging = nextTagging;
5800
+ applyHeight();
5801
+ }
5753
5802
  const date = shortDate(s.createdAt);
5754
- nameEl.textContent = `${s.guideName.trim() || "Sin nombre"}${date ? ` \xB7 ${date}` : ""}`;
5803
+ nameEl.textContent = tagging ? s.guideName.trim() || "Modo tagging" : `${s.guideName.trim() || "Sin nombre"}${date ? ` \xB7 ${date}` : ""}`;
5755
5804
  if (s.status) {
5756
5805
  statusEl.hidden = false;
5757
5806
  statusEl.textContent = s.status.label;
@@ -5871,6 +5920,8 @@ var init_builder_chrome = __esm({
5871
5920
  font-size: 14px; color: #1c1917;
5872
5921
  }
5873
5922
  .veo-chrome[hidden] { display: none; }
5923
+ /* Modo tagging oculta controles por atributo: gana a los display de sus clases. */
5924
+ .veo-chrome [hidden], .veo-steps-toggle[hidden] { display: none !important; }
5874
5925
  .veo-chrome.is-bottom { top: auto; bottom: 0; flex-direction: column-reverse; }
5875
5926
  .veo-chrome.is-bottom .veo-tb-menu { top: auto; bottom: calc(100% + 8px); }
5876
5927
  .veo-chrome.is-bottom .veo-steps { border-bottom: 0; border-top: 1px solid #d6cfc8; }
@@ -6503,6 +6554,65 @@ var init_design_manipulator = __esm({
6503
6554
  }
6504
6555
  });
6505
6556
 
6557
+ // src/plugins/builder/interactive-elements.ts
6558
+ function isInteractiveElement(element) {
6559
+ try {
6560
+ return element.matches(INTERACTIVE_SELECTOR);
6561
+ } catch {
6562
+ return false;
6563
+ }
6564
+ }
6565
+ function closestInteractive(element) {
6566
+ try {
6567
+ return element.closest(INTERACTIVE_SELECTOR);
6568
+ } catch {
6569
+ return null;
6570
+ }
6571
+ }
6572
+ function elementLabelText(element, max = 120) {
6573
+ const clean = (raw) => (raw ?? "").replace(EMOJI_RE, "").replace(/\s+/g, " ").trim();
6574
+ const text = clean(element.textContent);
6575
+ if (text) return text.slice(0, max);
6576
+ const fallback = clean(element.getAttribute("aria-label")) || clean(element.getAttribute("title"));
6577
+ return fallback.slice(0, max);
6578
+ }
6579
+ var INTERACTIVE_ROLES, INTERACTIVE_SELECTOR, EMOJI_RE;
6580
+ var init_interactive_elements = __esm({
6581
+ "src/plugins/builder/interactive-elements.ts"() {
6582
+ INTERACTIVE_ROLES = [
6583
+ "button",
6584
+ "link",
6585
+ "tab",
6586
+ "menuitem",
6587
+ "menuitemcheckbox",
6588
+ "menuitemradio",
6589
+ "checkbox",
6590
+ "radio",
6591
+ "switch",
6592
+ "option",
6593
+ "combobox",
6594
+ "textbox",
6595
+ "searchbox",
6596
+ "slider",
6597
+ "spinbutton"
6598
+ ];
6599
+ INTERACTIVE_SELECTOR = [
6600
+ "button",
6601
+ "a[href]",
6602
+ 'input:not([type="hidden"])',
6603
+ "select",
6604
+ "textarea",
6605
+ "form",
6606
+ "summary",
6607
+ '[contenteditable="true"]',
6608
+ "[onclick]",
6609
+ "[data-veo-tag]",
6610
+ ...INTERACTIVE_ROLES.map((r) => `[role="${r}"]`)
6611
+ ].join(", ");
6612
+ EMOJI_RE = /\p{Extended_Pictographic}|\p{Emoji_Modifier}|\u{FE0F}|\u{200D}|\u{20E3}/gu;
6613
+ }
6614
+ });
6615
+
6506
6616
  // src/plugins/builder/element-picker.ts
6507
6617
  function startElementPicker(options) {
6508
6618
  if (!hasDocument()) {
@@ -6521,6 +6631,7 @@ var init_element_picker = __esm({
6521
6631
  "src/plugins/builder/element-picker.ts"() {
6522
6632
  init_safe_env();
6523
6633
  init_selector_builder();
6634
+ init_interactive_elements();
6524
6635
  PICKER_Z = 2147483646;
6525
6636
  PICKER_HOST_ATTR = "data-veo-picker";
6526
6637
  DEFAULT_MAX_ANCESTORS = 5;
@@ -6590,21 +6701,24 @@ var init_element_picker = __esm({
6590
6701
  this.label = null;
6591
6702
  this.current = null;
6592
6703
  this.maxAncestors = DEFAULT_MAX_ANCESTORS;
6704
+ this.only = null;
6593
6705
  this.onPick = null;
6594
6706
  this.onCancel = null;
6595
6707
  this.onMove = (event) => {
6596
- const target = event.target;
6597
- if (!(target instanceof Element) || this.isOwn(target)) return;
6708
+ if (!(event.target instanceof Element) || this.isOwn(event.target)) return;
6709
+ const target = this.resolveTarget(event.target);
6598
6710
  this.current = target;
6599
- this.draw(target);
6711
+ if (target) this.draw(target);
6712
+ else this.hideBox();
6600
6713
  };
6601
6714
  this.onClick = (event) => {
6602
6715
  if (event.altKey) return;
6603
- const target = this.current ?? (event.target instanceof Element ? event.target : null);
6604
- if (!target || this.isOwn(target)) return;
6716
+ if (this.isOwn(event.target)) return;
6605
6717
  event.preventDefault();
6606
6718
  event.stopPropagation();
6607
6719
  event.stopImmediatePropagation();
6720
+ const target = this.current ?? this.resolveTarget(event.target);
6721
+ if (!target) return;
6608
6722
  const picked = this.describe(target);
6609
6723
  const cb = this.onPick;
6610
6724
  this.stop();
@@ -6631,6 +6745,7 @@ var init_element_picker = __esm({
6631
6745
  start(options) {
6632
6746
  this.stop();
6633
6747
  this.maxAncestors = options.maxAncestors ?? DEFAULT_MAX_ANCESTORS;
6748
+ this.only = options.only ?? null;
6634
6749
  this.onPick = options.onPick;
6635
6750
  this.onCancel = options.onCancel ?? null;
6636
6751
  const host = document.createElement("div");
@@ -6693,9 +6808,19 @@ var init_element_picker = __esm({
6693
6808
  this.box = null;
6694
6809
  this.label = null;
6695
6810
  this.current = null;
6811
+ this.only = null;
6696
6812
  this.onPick = null;
6697
6813
  this.onCancel = null;
6698
6814
  }
6815
+ /** Elemento que se elegiría para el nodo bajo el cursor (o null si no hay). */
6816
+ resolveTarget(target) {
6817
+ if (!(target instanceof Element) || this.isOwn(target)) return null;
6818
+ return this.only === "interactive" ? closestInteractive(target) : target;
6819
+ }
6820
+ hideBox() {
6821
+ if (this.box) this.box.style.display = "none";
6822
+ if (this.label) this.label.style.display = "none";
6823
+ }
6699
6824
  /** ¿Es un nodo de nuestro propio overlay? (no debe ser seleccionable). */
6700
6825
  isOwn(target) {
6701
6826
  return target instanceof Node && this.host !== null && this.host.contains(target);
@@ -6732,7 +6857,7 @@ var init_element_picker = __esm({
6732
6857
  tag: element.tagName.toLowerCase(),
6733
6858
  id: element.id || null,
6734
6859
  classes: filterHumanClasses(element),
6735
- text: (element.textContent ?? "").trim().slice(0, 120),
6860
+ text: elementLabelText(element),
6736
6861
  rect: {
6737
6862
  x: Math.round(rect.left),
6738
6863
  y: Math.round(rect.top),
@@ -7728,7 +7853,7 @@ function initBuilderMode() {
7728
7853
  if (h && hoverCard && !previewMode) renderCardChrome(h, previewStepIndex);
7729
7854
  }
7730
7855
  panel?.setTitle(
7731
- cmd.state.section === "activation" ? "Ajustes de activaci\xF3n" : cmd.state.section === "badge" ? "Ajustes de badge" : cmd.state.section === "target" ? "Ajustes del elemento objetivo" : cmd.state.section === "row" ? "Editar fila" : cmd.state.blockLabel ? `Editor \xB7 ${cmd.state.blockLabel}` : cmd.state.steps[cmd.state.activeStep]?.label ?? "Paso"
7856
+ cmd.state.panelTitle ?? (cmd.state.section === "activation" ? "Ajustes de activaci\xF3n" : cmd.state.section === "badge" ? "Ajustes de badge" : cmd.state.section === "target" ? "Ajustes del elemento objetivo" : cmd.state.section === "row" ? "Editar fila" : cmd.state.blockLabel ? `Editor \xB7 ${cmd.state.blockLabel}` : cmd.state.steps[cmd.state.activeStep]?.label ?? "Paso")
7732
7857
  );
7733
7858
  break;
7734
7859
  case "panel-visibility":
@@ -7766,7 +7891,8 @@ function initBuilderMode() {
7766
7891
  post({ type: "pick-cancelled" });
7767
7892
  },
7768
7893
  ...cmd.maxAncestors !== void 0 ? { maxAncestors: cmd.maxAncestors } : {},
7769
- ...cmd.hint !== void 0 ? { hint: cmd.hint } : {}
7894
+ ...cmd.hint !== void 0 ? { hint: cmd.hint } : {},
7895
+ ...cmd.only !== void 0 ? { only: cmd.only } : {}
7770
7896
  });
7771
7897
  break;
7772
7898
  }
@@ -7775,6 +7901,15 @@ function initBuilderMode() {
7775
7901
  picker = null;
7776
7902
  chrome?.setHidden(false);
7777
7903
  break;
7904
+ case "scan-page":
7905
+ post({ type: "page-scan", payload: scanPage(cmd.selectors) });
7906
+ break;
7907
+ case "highlight":
7908
+ setHighlight(cmd.selector);
7909
+ break;
7910
+ case "show-confirmation":
7911
+ showConfirmation(cmd.payload);
7912
+ break;
7778
7913
  case "preview":
7779
7914
  if (!cmd.guide) break;
7780
7915
  if (manipulator?.isDragging()) pendingPreview = cmd;
@@ -7859,10 +7994,154 @@ function initBuilderMode() {
7859
7994
  post({ type: "closed" });
7860
7995
  panel?.teardown();
7861
7996
  panel = null;
7997
+ highlightBox?.remove();
7998
+ highlightBox = null;
7999
+ confirmation?.remove();
8000
+ confirmation = null;
8001
+ if (confirmationKey) document.removeEventListener("keydown", confirmationKey, true);
8002
+ confirmationKey = null;
7862
8003
  chrome?.teardown();
7863
8004
  chrome = null;
7864
8005
  clearBuilderToken();
7865
8006
  };
8007
+ const scanPage = (selectors) => {
8008
+ const present = selectors.filter((sel) => {
8009
+ try {
8010
+ return document.querySelector(sel) !== null;
8011
+ } catch {
8012
+ return false;
8013
+ }
8014
+ });
8015
+ const taggedEls = /* @__PURE__ */ new Set();
8016
+ for (const sel of present) {
8017
+ try {
8018
+ for (const e of Array.from(document.querySelectorAll(sel))) taggedEls.add(e);
8019
+ } catch {
8020
+ }
8021
+ }
8022
+ const seen = /* @__PURE__ */ new Set();
8023
+ const candidates = [];
8024
+ const nodes = document.querySelectorAll(INTERACTIVE_SELECTOR);
8025
+ for (const element of Array.from(nodes)) {
8026
+ if (candidates.length >= 200) break;
8027
+ if (element.closest("[data-veo-builder-chrome],[data-veo-builder-panel],[data-veo-picker]")) {
8028
+ continue;
8029
+ }
8030
+ if (element.getClientRects().length === 0) continue;
8031
+ let tagged = false;
8032
+ for (let n = element; n; n = n.parentElement) {
8033
+ if (taggedEls.has(n)) {
8034
+ tagged = true;
8035
+ break;
8036
+ }
8037
+ }
8038
+ if (tagged) continue;
8039
+ const selector = buildSelectorPath(element, 5);
8040
+ if (!selector || seen.has(selector)) continue;
8041
+ seen.add(selector);
8042
+ candidates.push({
8043
+ selector,
8044
+ shortSelector: buildSelectorPath(element, 0),
8045
+ tag: element.tagName.toLowerCase(),
8046
+ id: element.id || null,
8047
+ classes: filterHumanClasses(element),
8048
+ text: elementLabelText(element)
8049
+ });
8050
+ }
8051
+ const { pathname, hash } = window.location;
8052
+ const routePath = /^#\/./.test(hash) ? `${pathname}${hash}` : pathname;
8053
+ return { pagePath: pathname, routePath, candidates, present };
8054
+ };
8055
+ let confirmation = null;
8056
+ let confirmationKey = null;
8057
+ const closeConfirmation = () => {
8058
+ if (!confirmation) return;
8059
+ confirmation.remove();
8060
+ confirmation = null;
8061
+ if (confirmationKey) document.removeEventListener("keydown", confirmationKey, true);
8062
+ confirmationKey = null;
8063
+ panel?.setVisible(true);
8064
+ post({ type: "confirmation-closed" });
8065
+ };
8066
+ const showConfirmation = (card) => {
8067
+ if (confirmation) {
8068
+ confirmation.remove();
8069
+ confirmation = null;
8070
+ }
8071
+ panel?.setVisible(false);
8072
+ const host = document.createElement("div");
8073
+ host.setAttribute("data-veo-builder-chrome", "");
8074
+ host.setAttribute("data-veo-confirmation", "");
8075
+ const shadow = host.attachShadow({ mode: "open" });
8076
+ const style = document.createElement("style");
8077
+ style.textContent = CONFIRMATION_STYLES;
8078
+ const backdrop = document.createElement("div");
8079
+ backdrop.className = "veo-confirm-backdrop";
8080
+ const dialog = document.createElement("div");
8081
+ dialog.className = "veo-confirm";
8082
+ dialog.setAttribute("role", "dialog");
8083
+ dialog.setAttribute("aria-modal", "true");
8084
+ dialog.setAttribute("aria-label", card.title);
8085
+ const badge = document.createElement("div");
8086
+ badge.className = "veo-confirm-check";
8087
+ badge.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>';
8088
+ const title = document.createElement("h2");
8089
+ title.className = "veo-confirm-title";
8090
+ title.textContent = card.title;
8091
+ const message = document.createElement("p");
8092
+ message.className = "veo-confirm-msg";
8093
+ message.textContent = card.message;
8094
+ const rows = document.createElement("dl");
8095
+ rows.className = "veo-confirm-rows";
8096
+ for (const r of card.rows) {
8097
+ const row = document.createElement("div");
8098
+ row.className = "veo-confirm-row";
8099
+ const dt = document.createElement("dt");
8100
+ dt.textContent = r.label;
8101
+ const dd = document.createElement("dd");
8102
+ dd.textContent = r.value;
8103
+ row.append(dt, dd);
8104
+ rows.appendChild(row);
8105
+ }
8106
+ const button = document.createElement("button");
8107
+ button.type = "button";
8108
+ button.className = "veo-confirm-btn";
8109
+ button.textContent = card.buttonLabel ?? "Listo";
8110
+ button.addEventListener("click", closeConfirmation);
8111
+ dialog.append(badge, title, message);
8112
+ if (card.rows.length > 0) dialog.appendChild(rows);
8113
+ dialog.appendChild(button);
8114
+ shadow.append(style, backdrop, dialog);
8115
+ document.body.appendChild(host);
8116
+ confirmation = host;
8117
+ confirmationKey = (e) => {
8118
+ if (e.key !== "Escape") return;
8119
+ e.preventDefault();
8120
+ e.stopPropagation();
8121
+ closeConfirmation();
8122
+ };
8123
+ document.addEventListener("keydown", confirmationKey, true);
8124
+ button.focus();
8125
+ };
8126
+ let highlightBox = null;
8127
+ const setHighlight = (selector) => {
8128
+ highlightBox?.remove();
8129
+ highlightBox = null;
8130
+ if (!selector) return;
8131
+ let target = null;
8132
+ try {
8133
+ target = document.querySelector(selector);
8134
+ } catch {
8135
+ return;
8136
+ }
8137
+ if (!target) return;
8138
+ const r = target.getBoundingClientRect();
8139
+ const box = document.createElement("div");
8140
+ box.setAttribute("data-veo-builder-chrome", "");
8141
+ box.style.cssText = `position:fixed;left:${r.left - 3}px;top:${r.top - 3}px;width:${r.width + 6}px;height:${r.height + 6}px;border:2px solid #FF5B35;border-radius:6px;background:rgba(255,91,53,0.10);pointer-events:none;z-index:2147483646;`;
8142
+ document.body.appendChild(box);
8143
+ highlightBox = box;
8144
+ };
7866
8145
  if (ctx.panel) {
7867
8146
  chrome = injectBuilderChrome({
7868
8147
  appLabel: window.location.hostname,
@@ -7990,10 +8269,12 @@ function previewProgressFetcher() {
7990
8269
  return data.items ?? [];
7991
8270
  };
7992
8271
  }
8272
+ var CONFIRMATION_STYLES;
7993
8273
  var init_builder_mode = __esm({
7994
8274
  "src/plugins/builder/builder-mode.ts"() {
7995
8275
  init_builder_token();
7996
8276
  init_safe_env();
8277
+ init_selector_builder();
7997
8278
  init_activation_devices();
7998
8279
  init_activation_rules();
7999
8280
  init_block_builder();
@@ -8008,6 +8289,42 @@ var init_builder_mode = __esm({
8008
8289
  init_builder_panel();
8009
8290
  init_design_manipulator();
8010
8291
  init_element_picker();
8292
+ init_interactive_elements();
8293
+ CONFIRMATION_STYLES = `
8294
+ :host { all: initial; }
8295
+ .veo-confirm-backdrop {
8296
+ position: fixed; inset: 0; z-index: 2147483645;
8297
+ background: rgba(20, 16, 14, 0.45);
8298
+ }
8299
+ .veo-confirm {
8300
+ position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
8301
+ z-index: 2147483646; box-sizing: border-box;
8302
+ width: min(432px, calc(100vw - 32px)); padding: 28px 24px 24px;
8303
+ border-radius: 16px; background: #fff; color: #1d1917;
8304
+ box-shadow: 0 24px 64px rgba(0,0,0,0.28);
8305
+ font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
8306
+ text-align: center;
8307
+ }
8308
+ .veo-confirm-check {
8309
+ width: 64px; height: 64px; margin: 0 auto; border-radius: 50%;
8310
+ background: #e6f6df; color: #2f9a4e; display: flex; align-items: center; justify-content: center;
8311
+ }
8312
+ .veo-confirm-check svg { width: 30px; height: 30px; }
8313
+ .veo-confirm-title { margin: 18px 0 0; font-size: 17px; font-weight: 600; }
8314
+ .veo-confirm-msg { margin: 8px 0 0; font-size: 13px; color: #6b6461; }
8315
+ .veo-confirm-rows {
8316
+ margin: 18px 0 0; padding: 12px 14px; border-radius: 12px; background: #f6f2ef; text-align: left;
8317
+ }
8318
+ .veo-confirm-row { display: flex; justify-content: space-between; gap: 16px; font-size: 13px; padding: 4px 0; }
8319
+ .veo-confirm-row dt { color: #6b6461; margin: 0; }
8320
+ .veo-confirm-row dd { margin: 0; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
8321
+ .veo-confirm-btn {
8322
+ display: block; width: 100%; margin-top: 18px; height: 44px; border: 0; border-radius: 10px;
8323
+ background: #b74d00; color: #fff; font: inherit; font-size: 15px; font-weight: 600; cursor: pointer;
8324
+ }
8325
+ .veo-confirm-btn:hover { background: #953c00; }
8326
+ .veo-confirm-btn:focus-visible { outline: 3px solid rgba(183,77,0,0.35); outline-offset: 2px; }
8327
+ `;
8011
8328
  }
8012
8329
  });
8013
8330
 
@@ -8140,11 +8457,15 @@ var init_builder_session = __esm({
8140
8457
  // src/plugins/builder/index.ts
8141
8458
  var builder_exports = {};
8142
8459
  __export(builder_exports, {
8460
+ INTERACTIVE_SELECTOR: () => INTERACTIVE_SELECTOR,
8143
8461
  VEO_BLOCK_MIME: () => VEO_BLOCK_MIME,
8144
8462
  attachBlockDropTarget: () => attachBlockDropTarget,
8145
8463
  attachDesignManipulator: () => attachDesignManipulator,
8464
+ closestInteractive: () => closestInteractive,
8146
8465
  createBuilderSession: () => createBuilderSession,
8466
+ elementLabelText: () => elementLabelText,
8147
8467
  initBuilderMode: () => initBuilderMode,
8468
+ isInteractiveElement: () => isInteractiveElement,
8148
8469
  startElementPicker: () => startElementPicker,
8149
8470
  stopElementPicker: () => stopElementPicker
8150
8471
  });
@@ -8155,6 +8476,7 @@ var init_builder = __esm({
8155
8476
  init_builder_session();
8156
8477
  init_design_manipulator();
8157
8478
  init_element_picker();
8479
+ init_interactive_elements();
8158
8480
  }
8159
8481
  });
8160
8482
 
@@ -8624,7 +8946,7 @@ var init_detect_theme = __esm({
8624
8946
  });
8625
8947
 
8626
8948
  // src/contract/v1.ts
8627
- var SDK_VERSION = "0.0.1";
8949
+ var SDK_VERSION = "0.6.4";
8628
8950
  var DEFAULT_FLUSH_INTERVAL_MS = 5e3;
8629
8951
  var DEFAULT_FLUSH_BATCH_SIZE = 20;
8630
8952
  var DEFAULT_API_URL = "https://veo-backend-production-8c78.up.railway.app";
@@ -8694,6 +9016,298 @@ function definePlugin(plugin) {
8694
9016
 
8695
9017
  // src/plugins/autocapture/dom-listener.ts
8696
9018
  init_safe_env();
9019
+
9020
+ // src/plugins/autocapture/click-quality.ts
9021
+ init_safe_env();
9022
+ init_selector_builder();
9023
+ var NO_DEAD_TAGS = /* @__PURE__ */ new Set(["input", "textarea", "select", "option", "label"]);
9024
+ var MAX_ERROR_MESSAGE = 200;
9025
+ var HISTORY_MS = 3e3;
9026
+ var DEFAULTS = {
9027
+ reactionWindowMs: 1e3,
9028
+ errorWindowMs: 2e3,
9029
+ rageClicks: 3,
9030
+ rageWindowMs: 1e3,
9031
+ rageClicksSlow: 5,
9032
+ rageWindowSlowMs: 3e3,
9033
+ maxPerElement: 50
9034
+ };
9035
+ function describeError(value) {
9036
+ let text;
9037
+ if (value instanceof Error) text = `${value.name}: ${value.message}`;
9038
+ else if (typeof value === "string") text = value;
9039
+ else {
9040
+ try {
9041
+ text = JSON.stringify(value) ?? String(value);
9042
+ } catch {
9043
+ text = String(value);
9044
+ }
9045
+ }
9046
+ return text.slice(0, MAX_ERROR_MESSAGE);
9047
+ }
9048
+ var ClickQualityTracker = class {
9049
+ constructor(emit, options = {}) {
9050
+ this.emit = emit;
9051
+ this.installed = false;
9052
+ this.cleanups = [];
9053
+ // Sensores globales (contadores: cada observación compara su foto inicial).
9054
+ this.netCount = 0;
9055
+ this.navCount = 0;
9056
+ this.focusCount = 0;
9057
+ this.unloading = false;
9058
+ this.errors = [];
9059
+ this.history = /* @__PURE__ */ new Map();
9060
+ this.bursts = /* @__PURE__ */ new Map();
9061
+ this.emitted = /* @__PURE__ */ new Map();
9062
+ this.opts = { ...DEFAULTS, ...options };
9063
+ }
9064
+ install() {
9065
+ if (this.installed || !hasWindow()) return;
9066
+ this.installed = true;
9067
+ const on = (type, handler, capture = true) => {
9068
+ window.addEventListener(type, handler, capture);
9069
+ this.cleanups.push(() => window.removeEventListener(type, handler, capture));
9070
+ };
9071
+ on("error", (e) => {
9072
+ const err = e;
9073
+ this.pushError("error", err.error ?? err.message ?? "error");
9074
+ });
9075
+ on("unhandledrejection", (e) => {
9076
+ this.pushError("unhandledrejection", e.reason ?? "rejection");
9077
+ });
9078
+ on("popstate", () => this.navCount++);
9079
+ on("hashchange", () => this.navCount++);
9080
+ on("focusin", () => this.focusCount++);
9081
+ on("pagehide", () => {
9082
+ this.unloading = true;
9083
+ });
9084
+ on("beforeunload", () => {
9085
+ this.unloading = true;
9086
+ });
9087
+ this.patchHistory();
9088
+ this.patchConsole();
9089
+ this.patchFetch();
9090
+ this.patchXhr();
9091
+ }
9092
+ destroy() {
9093
+ for (const c of this.cleanups) c();
9094
+ this.cleanups = [];
9095
+ for (const b of this.bursts.values()) window.clearTimeout(b.timer);
9096
+ this.bursts.clear();
9097
+ this.history.clear();
9098
+ this.installed = false;
9099
+ }
9100
+ /** Llamar por cada clic real sobre un elemento interactivo, ANTES del throttle. */
9101
+ onClick(element, event) {
9102
+ if (!this.installed) return;
9103
+ const now = Date.now();
9104
+ const obs = { at: now, reacted: false };
9105
+ this.remember(element, obs);
9106
+ this.observeReaction(element, obs, event);
9107
+ this.observeErrors(element, now);
9108
+ this.trackRage(element, now);
9109
+ }
9110
+ // ---------------------------------------------------------------- dead ----
9111
+ observeReaction(element, obs, event) {
9112
+ const netAtClick = this.netCount;
9113
+ const navAtClick = this.navCount;
9114
+ const focusAtClick = this.focusCount;
9115
+ const hrefAtClick = window.location.href;
9116
+ const skipDead = this.isDeadExempt(element, event);
9117
+ let observer = null;
9118
+ if (typeof MutationObserver !== "undefined") {
9119
+ observer = new MutationObserver(() => {
9120
+ obs.reacted = true;
9121
+ observer?.disconnect();
9122
+ observer = null;
9123
+ });
9124
+ observer.observe(document.documentElement, {
9125
+ childList: true,
9126
+ attributes: true,
9127
+ characterData: true,
9128
+ subtree: true
9129
+ });
9130
+ }
9131
+ window.setTimeout(() => {
9132
+ observer?.disconnect();
9133
+ const reacted = obs.reacted || this.unloading || this.netCount !== netAtClick || this.navCount !== navAtClick || this.focusCount !== focusAtClick || window.location.href !== hrefAtClick;
9134
+ obs.reacted = reacted;
9135
+ if (!reacted && !skipDead) this.report("$dead_click", element, {});
9136
+ }, this.opts.reactionWindowMs);
9137
+ }
9138
+ isDeadExempt(element, event) {
9139
+ if (event && (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey)) return true;
9140
+ if (event && typeof event.button === "number" && event.button !== 0) return true;
9141
+ const tag = element.tagName.toLowerCase();
9142
+ if (NO_DEAD_TAGS.has(tag)) return true;
9143
+ if (element.closest('[contenteditable=""],[contenteditable="true"]')) return true;
9144
+ const anchor = element.closest("a");
9145
+ if (anchor) {
9146
+ if (anchor.target === "_blank" || anchor.hasAttribute("download")) return true;
9147
+ const href = anchor.getAttribute("href") ?? "";
9148
+ if (/^(mailto|tel|sms):/i.test(href)) return true;
9149
+ }
9150
+ return false;
9151
+ }
9152
+ // --------------------------------------------------------------- error ----
9153
+ observeErrors(element, clickedAt) {
9154
+ const deadline = clickedAt + this.opts.errorWindowMs;
9155
+ const check = () => {
9156
+ const err = this.errors.find((e) => e.at >= clickedAt && e.at <= deadline);
9157
+ if (err) {
9158
+ this.report("$click_error", element, {
9159
+ $error_source: err.source,
9160
+ $error_message: err.message
9161
+ });
9162
+ return;
9163
+ }
9164
+ if (Date.now() < deadline) window.setTimeout(check, 250);
9165
+ };
9166
+ window.setTimeout(check, 250);
9167
+ }
9168
+ pushError(source, value) {
9169
+ const message = describeError(value);
9170
+ if (message.startsWith("[veo]")) return;
9171
+ this.errors.push({ at: Date.now(), source, message });
9172
+ const cutoff = Date.now() - this.opts.errorWindowMs - 1e3;
9173
+ while (this.errors.length > 0 && this.errors[0].at < cutoff) {
9174
+ this.errors.shift();
9175
+ }
9176
+ if (this.errors.length > 50) this.errors.splice(0, this.errors.length - 50);
9177
+ }
9178
+ // ---------------------------------------------------------------- rage ----
9179
+ remember(element, obs) {
9180
+ const list = this.history.get(element) ?? [];
9181
+ const cutoff = obs.at - HISTORY_MS;
9182
+ const kept = list.filter((o) => o.at >= cutoff);
9183
+ kept.push(obs);
9184
+ this.history.set(element, kept);
9185
+ if (this.history.size > 100) {
9186
+ for (const [el2, obsList] of this.history) {
9187
+ if (obsList.every((o) => o.at < cutoff)) this.history.delete(el2);
9188
+ }
9189
+ }
9190
+ }
9191
+ trackRage(element, now) {
9192
+ const list = this.history.get(element) ?? [];
9193
+ const fast = list.filter((o) => o.at >= now - this.opts.rageWindowMs).length;
9194
+ const slow = list.filter((o) => o.at >= now - this.opts.rageWindowSlowMs).length;
9195
+ const existing = this.bursts.get(element);
9196
+ const isRage = fast >= this.opts.rageClicks || slow >= this.opts.rageClicksSlow;
9197
+ if (!existing && !isRage) return;
9198
+ const burst = existing ?? {
9199
+ startedAt: (list.find((o) => o.at >= now - this.opts.rageWindowSlowMs) ?? { at: now }).at,
9200
+ count: 0,
9201
+ timer: 0
9202
+ };
9203
+ burst.count = list.filter((o) => o.at >= burst.startedAt).length;
9204
+ window.clearTimeout(burst.timer);
9205
+ burst.timer = window.setTimeout(() => {
9206
+ this.bursts.delete(element);
9207
+ const clicks = (this.history.get(element) ?? []).filter((o) => o.at >= burst.startedAt);
9208
+ this.report("$rage_click", element, {
9209
+ $click_count: Math.max(clicks.length, burst.count),
9210
+ $reacted_clicks: clicks.filter((o) => o.reacted).length
9211
+ });
9212
+ }, this.opts.reactionWindowMs + 50);
9213
+ this.bursts.set(element, burst);
9214
+ }
9215
+ // ---------------------------------------------------------------- emit ----
9216
+ report(kind, element, extra) {
9217
+ const key = buildSelectorPath(element, 5);
9218
+ const counts = this.emitted.get(key) ?? { $dead_click: 0, $rage_click: 0, $click_error: 0 };
9219
+ if (counts[kind] >= this.opts.maxPerElement) return;
9220
+ counts[kind]++;
9221
+ this.emitted.set(key, counts);
9222
+ try {
9223
+ this.emit(kind, element, extra);
9224
+ } catch {
9225
+ }
9226
+ }
9227
+ // ------------------------------------------------------------- patches ----
9228
+ patchHistory() {
9229
+ const h = window.history;
9230
+ if (!h) return;
9231
+ const origPush = h.pushState;
9232
+ const origReplace = h.replaceState;
9233
+ const self = this;
9234
+ h.pushState = function(...args) {
9235
+ self.navCount++;
9236
+ return origPush.apply(this, args);
9237
+ };
9238
+ h.replaceState = function(...args) {
9239
+ self.navCount++;
9240
+ return origReplace.apply(this, args);
9241
+ };
9242
+ this.cleanups.push(() => {
9243
+ h.pushState = origPush;
9244
+ h.replaceState = origReplace;
9245
+ });
9246
+ }
9247
+ patchConsole() {
9248
+ const orig = console.error;
9249
+ const self = this;
9250
+ console.error = function(...args) {
9251
+ try {
9252
+ self.pushError("console", args.map(describeError).join(" "));
9253
+ } catch {
9254
+ }
9255
+ return orig.apply(this, args);
9256
+ };
9257
+ this.cleanups.push(() => {
9258
+ console.error = orig;
9259
+ });
9260
+ }
9261
+ /**
9262
+ * `fetch`: cuenta la petición (reacción) y registra respuestas ≥500 o
9263
+ * rechazos como error. Se observa con `.then(ok, err)` sobre la promesa
9264
+ * original: eso marca el rechazo como manejado para el navegador, así que
9265
+ * un fetch fallido sin catch de la app no dispara `unhandledrejection` —
9266
+ * lo registramos nosotros como error de red.
9267
+ */
9268
+ patchFetch() {
9269
+ if (typeof window.fetch !== "function") return;
9270
+ const orig = window.fetch;
9271
+ const self = this;
9272
+ window.fetch = function(...args) {
9273
+ self.netCount++;
9274
+ const p = orig.apply(this, args);
9275
+ if (p && typeof p.then === "function") {
9276
+ p.then(
9277
+ (res) => {
9278
+ if (res && res.status >= 500) self.pushError("fetch", `${res.status} ${res.url}`);
9279
+ },
9280
+ (err) => self.pushError("fetch", err)
9281
+ );
9282
+ }
9283
+ return p;
9284
+ };
9285
+ this.cleanups.push(() => {
9286
+ window.fetch = orig;
9287
+ });
9288
+ }
9289
+ patchXhr() {
9290
+ const proto = typeof XMLHttpRequest !== "undefined" ? XMLHttpRequest.prototype : null;
9291
+ if (!proto) return;
9292
+ const origSend = proto.send;
9293
+ const self = this;
9294
+ proto.send = function(...args) {
9295
+ self.netCount++;
9296
+ try {
9297
+ this.addEventListener("loadend", () => {
9298
+ if (this.status >= 500) self.pushError("xhr", `${this.status} ${this.responseURL}`);
9299
+ });
9300
+ } catch {
9301
+ }
9302
+ return origSend.apply(this, args);
9303
+ };
9304
+ this.cleanups.push(() => {
9305
+ proto.send = origSend;
9306
+ });
9307
+ }
9308
+ };
9309
+
9310
+ // src/plugins/autocapture/dom-listener.ts
8697
9311
  init_constants();
8698
9312
 
8699
9313
  // src/plugins/autocapture/element-serializer.ts
@@ -8804,6 +9418,7 @@ var DomListener = class {
8804
9418
  constructor(client, config) {
8805
9419
  this.client = client;
8806
9420
  this.cleanups = [];
9421
+ this.quality = null;
8807
9422
  const d = DEFAULT_AUTOCAPTURE_CONFIG;
8808
9423
  this.config = {
8809
9424
  enabled: config.enabled ?? d.enabled,
@@ -8816,12 +9431,20 @@ var DomListener = class {
8816
9431
  throttleMs: config.throttleMs ?? d.throttleMs,
8817
9432
  maxTextLength: config.maxTextLength ?? d.maxTextLength,
8818
9433
  captureElementMetadata: config.captureElementMetadata ?? d.captureElementMetadata,
8819
- captureAncestors: Math.min(config.captureAncestors ?? d.captureAncestors, MAX_ANCESTORS)
9434
+ captureAncestors: Math.min(config.captureAncestors ?? d.captureAncestors, MAX_ANCESTORS),
9435
+ captureClickQuality: config.captureClickQuality ?? d.captureClickQuality
8820
9436
  };
8821
9437
  this.throttle = new ElementThrottle(this.config.throttleMs);
8822
9438
  }
8823
9439
  install() {
8824
9440
  if (!hasDocument() || !this.config.enabled) return;
9441
+ if (this.config.captureClicks && this.config.captureClickQuality) {
9442
+ this.quality = new ClickQualityTracker((kind, element, extra) => {
9443
+ const payload = buildAutocapturePayload("click", element, this.config);
9444
+ this.client.track(kind, { ...payload, ...extra });
9445
+ });
9446
+ this.quality.install();
9447
+ }
8825
9448
  if (this.config.captureClicks) this.attach("click", (e) => this.handleClick(e));
8826
9449
  if (this.config.captureSubmits) this.attach("submit", (e) => this.handleSubmit(e), true);
8827
9450
  if (this.config.captureChanges) this.attach("change", (e) => this.handleChange(e), true);
@@ -8829,6 +9452,8 @@ var DomListener = class {
8829
9452
  destroy() {
8830
9453
  for (const cleanup of this.cleanups) cleanup();
8831
9454
  this.cleanups = [];
9455
+ this.quality?.destroy();
9456
+ this.quality = null;
8832
9457
  }
8833
9458
  attach(eventName, handler, useCapture = false) {
8834
9459
  const listener = (e) => {
@@ -8847,6 +9472,7 @@ var DomListener = class {
8847
9472
  const meaningful = this.findMeaningfulAncestor(target);
8848
9473
  if (!meaningful) return;
8849
9474
  if (isElementBlocked(meaningful, this.config.blockSelectors)) return;
9475
+ this.quality?.onClick(meaningful, event);
8850
9476
  if (!this.throttle.shouldFire(meaningful)) return;
8851
9477
  this.emit("click", meaningful);
8852
9478
  }
@@ -9352,6 +9978,7 @@ function stableStringify(value) {
9352
9978
  var Client = class {
9353
9979
  constructor(config) {
9354
9980
  this.config = config;
9981
+ this.version = SDK_VERSION;
9355
9982
  this.plugins = [];
9356
9983
  this.currentOrganizationId = null;
9357
9984
  this.listeners = {