veo-sdk 0.6.2 → 0.6.3

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 = [
@@ -5690,14 +5691,19 @@ function injectBuilderChrome(opts) {
5690
5691
  let previewOn = false;
5691
5692
  let hiddenOn = false;
5692
5693
  let heightCb = null;
5693
- const height = () => CHROME_TOPBAR_H + (collapsed ? 0 : CHROME_STEPS_H);
5694
+ let tagging = false;
5695
+ const height = () => CHROME_TOPBAR_H + (collapsed || tagging ? 0 : CHROME_STEPS_H);
5694
5696
  const applyHeight = () => {
5695
5697
  const h = previewOn || hiddenOn ? 0 : height();
5696
5698
  html.style.marginTop = dock === "top" ? `${h}px` : prevMarginTop;
5697
5699
  html.style.marginBottom = dock === "bottom" ? `${h}px` : prevMarginBottom;
5698
5700
  root.style.setProperty("--veo-chrome-h", `${height()}px`);
5699
5701
  root.classList.toggle("is-bottom", dock === "bottom");
5700
- stepsBar.hidden = collapsed;
5702
+ stepsBar.hidden = collapsed || tagging;
5703
+ togglePill.hidden = tagging;
5704
+ preview.hidden = tagging;
5705
+ saveGroup.hidden = tagging;
5706
+ showPanel.title = tagging ? "Mostrar el panel" : "Mostrar el editor";
5701
5707
  const open = dock === "top" ? "chevronUp" : "chevronDown";
5702
5708
  const closedIcon = dock === "top" ? "chevronDown" : "chevronUp";
5703
5709
  togglePill.innerHTML = `<span>${collapsed ? "Mostrar pasos" : "Ocultar pasos"}</span>${svg(
@@ -5750,8 +5756,13 @@ function injectBuilderChrome(opts) {
5750
5756
  const render = () => {
5751
5757
  const s = current;
5752
5758
  if (!s) return;
5759
+ const nextTagging = s.variant === "tagging";
5760
+ if (nextTagging !== tagging) {
5761
+ tagging = nextTagging;
5762
+ applyHeight();
5763
+ }
5753
5764
  const date = shortDate(s.createdAt);
5754
- nameEl.textContent = `${s.guideName.trim() || "Sin nombre"}${date ? ` \xB7 ${date}` : ""}`;
5765
+ nameEl.textContent = tagging ? s.guideName.trim() || "Modo tagging" : `${s.guideName.trim() || "Sin nombre"}${date ? ` \xB7 ${date}` : ""}`;
5755
5766
  if (s.status) {
5756
5767
  statusEl.hidden = false;
5757
5768
  statusEl.textContent = s.status.label;
@@ -5871,6 +5882,8 @@ var init_builder_chrome = __esm({
5871
5882
  font-size: 14px; color: #1c1917;
5872
5883
  }
5873
5884
  .veo-chrome[hidden] { display: none; }
5885
+ /* Modo tagging oculta controles por atributo: gana a los display de sus clases. */
5886
+ .veo-chrome [hidden], .veo-steps-toggle[hidden] { display: none !important; }
5874
5887
  .veo-chrome.is-bottom { top: auto; bottom: 0; flex-direction: column-reverse; }
5875
5888
  .veo-chrome.is-bottom .veo-tb-menu { top: auto; bottom: calc(100% + 8px); }
5876
5889
  .veo-chrome.is-bottom .veo-steps { border-bottom: 0; border-top: 1px solid #d6cfc8; }
@@ -6503,6 +6516,65 @@ var init_design_manipulator = __esm({
6503
6516
  }
6504
6517
  });
6505
6518
 
6519
+ // src/plugins/builder/interactive-elements.ts
6520
+ function isInteractiveElement(element) {
6521
+ try {
6522
+ return element.matches(INTERACTIVE_SELECTOR);
6523
+ } catch {
6524
+ return false;
6525
+ }
6526
+ }
6527
+ function closestInteractive(element) {
6528
+ try {
6529
+ return element.closest(INTERACTIVE_SELECTOR);
6530
+ } catch {
6531
+ return null;
6532
+ }
6533
+ }
6534
+ function elementLabelText(element, max = 120) {
6535
+ const clean = (raw) => (raw ?? "").replace(EMOJI_RE, "").replace(/\s+/g, " ").trim();
6536
+ const text = clean(element.textContent);
6537
+ if (text) return text.slice(0, max);
6538
+ const fallback = clean(element.getAttribute("aria-label")) || clean(element.getAttribute("title"));
6539
+ return fallback.slice(0, max);
6540
+ }
6541
+ var INTERACTIVE_ROLES, INTERACTIVE_SELECTOR, EMOJI_RE;
6542
+ var init_interactive_elements = __esm({
6543
+ "src/plugins/builder/interactive-elements.ts"() {
6544
+ INTERACTIVE_ROLES = [
6545
+ "button",
6546
+ "link",
6547
+ "tab",
6548
+ "menuitem",
6549
+ "menuitemcheckbox",
6550
+ "menuitemradio",
6551
+ "checkbox",
6552
+ "radio",
6553
+ "switch",
6554
+ "option",
6555
+ "combobox",
6556
+ "textbox",
6557
+ "searchbox",
6558
+ "slider",
6559
+ "spinbutton"
6560
+ ];
6561
+ INTERACTIVE_SELECTOR = [
6562
+ "button",
6563
+ "a[href]",
6564
+ 'input:not([type="hidden"])',
6565
+ "select",
6566
+ "textarea",
6567
+ "form",
6568
+ "summary",
6569
+ '[contenteditable="true"]',
6570
+ "[onclick]",
6571
+ "[data-veo-tag]",
6572
+ ...INTERACTIVE_ROLES.map((r) => `[role="${r}"]`)
6573
+ ].join(", ");
6574
+ EMOJI_RE = /\p{Extended_Pictographic}|\p{Emoji_Modifier}|\u{FE0F}|\u{200D}|\u{20E3}/gu;
6575
+ }
6576
+ });
6577
+
6506
6578
  // src/plugins/builder/element-picker.ts
6507
6579
  function startElementPicker(options) {
6508
6580
  if (!hasDocument()) {
@@ -6521,6 +6593,7 @@ var init_element_picker = __esm({
6521
6593
  "src/plugins/builder/element-picker.ts"() {
6522
6594
  init_safe_env();
6523
6595
  init_selector_builder();
6596
+ init_interactive_elements();
6524
6597
  PICKER_Z = 2147483646;
6525
6598
  PICKER_HOST_ATTR = "data-veo-picker";
6526
6599
  DEFAULT_MAX_ANCESTORS = 5;
@@ -6590,21 +6663,24 @@ var init_element_picker = __esm({
6590
6663
  this.label = null;
6591
6664
  this.current = null;
6592
6665
  this.maxAncestors = DEFAULT_MAX_ANCESTORS;
6666
+ this.only = null;
6593
6667
  this.onPick = null;
6594
6668
  this.onCancel = null;
6595
6669
  this.onMove = (event) => {
6596
- const target = event.target;
6597
- if (!(target instanceof Element) || this.isOwn(target)) return;
6670
+ if (!(event.target instanceof Element) || this.isOwn(event.target)) return;
6671
+ const target = this.resolveTarget(event.target);
6598
6672
  this.current = target;
6599
- this.draw(target);
6673
+ if (target) this.draw(target);
6674
+ else this.hideBox();
6600
6675
  };
6601
6676
  this.onClick = (event) => {
6602
6677
  if (event.altKey) return;
6603
- const target = this.current ?? (event.target instanceof Element ? event.target : null);
6604
- if (!target || this.isOwn(target)) return;
6678
+ if (this.isOwn(event.target)) return;
6605
6679
  event.preventDefault();
6606
6680
  event.stopPropagation();
6607
6681
  event.stopImmediatePropagation();
6682
+ const target = this.current ?? this.resolveTarget(event.target);
6683
+ if (!target) return;
6608
6684
  const picked = this.describe(target);
6609
6685
  const cb = this.onPick;
6610
6686
  this.stop();
@@ -6631,6 +6707,7 @@ var init_element_picker = __esm({
6631
6707
  start(options) {
6632
6708
  this.stop();
6633
6709
  this.maxAncestors = options.maxAncestors ?? DEFAULT_MAX_ANCESTORS;
6710
+ this.only = options.only ?? null;
6634
6711
  this.onPick = options.onPick;
6635
6712
  this.onCancel = options.onCancel ?? null;
6636
6713
  const host = document.createElement("div");
@@ -6693,9 +6770,19 @@ var init_element_picker = __esm({
6693
6770
  this.box = null;
6694
6771
  this.label = null;
6695
6772
  this.current = null;
6773
+ this.only = null;
6696
6774
  this.onPick = null;
6697
6775
  this.onCancel = null;
6698
6776
  }
6777
+ /** Elemento que se elegiría para el nodo bajo el cursor (o null si no hay). */
6778
+ resolveTarget(target) {
6779
+ if (!(target instanceof Element) || this.isOwn(target)) return null;
6780
+ return this.only === "interactive" ? closestInteractive(target) : target;
6781
+ }
6782
+ hideBox() {
6783
+ if (this.box) this.box.style.display = "none";
6784
+ if (this.label) this.label.style.display = "none";
6785
+ }
6699
6786
  /** ¿Es un nodo de nuestro propio overlay? (no debe ser seleccionable). */
6700
6787
  isOwn(target) {
6701
6788
  return target instanceof Node && this.host !== null && this.host.contains(target);
@@ -6732,7 +6819,7 @@ var init_element_picker = __esm({
6732
6819
  tag: element.tagName.toLowerCase(),
6733
6820
  id: element.id || null,
6734
6821
  classes: filterHumanClasses(element),
6735
- text: (element.textContent ?? "").trim().slice(0, 120),
6822
+ text: elementLabelText(element),
6736
6823
  rect: {
6737
6824
  x: Math.round(rect.left),
6738
6825
  y: Math.round(rect.top),
@@ -7728,7 +7815,7 @@ function initBuilderMode() {
7728
7815
  if (h && hoverCard && !previewMode) renderCardChrome(h, previewStepIndex);
7729
7816
  }
7730
7817
  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"
7818
+ 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
7819
  );
7733
7820
  break;
7734
7821
  case "panel-visibility":
@@ -7766,7 +7853,8 @@ function initBuilderMode() {
7766
7853
  post({ type: "pick-cancelled" });
7767
7854
  },
7768
7855
  ...cmd.maxAncestors !== void 0 ? { maxAncestors: cmd.maxAncestors } : {},
7769
- ...cmd.hint !== void 0 ? { hint: cmd.hint } : {}
7856
+ ...cmd.hint !== void 0 ? { hint: cmd.hint } : {},
7857
+ ...cmd.only !== void 0 ? { only: cmd.only } : {}
7770
7858
  });
7771
7859
  break;
7772
7860
  }
@@ -7775,6 +7863,15 @@ function initBuilderMode() {
7775
7863
  picker = null;
7776
7864
  chrome?.setHidden(false);
7777
7865
  break;
7866
+ case "scan-page":
7867
+ post({ type: "page-scan", payload: scanPage(cmd.selectors) });
7868
+ break;
7869
+ case "highlight":
7870
+ setHighlight(cmd.selector);
7871
+ break;
7872
+ case "show-confirmation":
7873
+ showConfirmation(cmd.payload);
7874
+ break;
7778
7875
  case "preview":
7779
7876
  if (!cmd.guide) break;
7780
7877
  if (manipulator?.isDragging()) pendingPreview = cmd;
@@ -7859,10 +7956,154 @@ function initBuilderMode() {
7859
7956
  post({ type: "closed" });
7860
7957
  panel?.teardown();
7861
7958
  panel = null;
7959
+ highlightBox?.remove();
7960
+ highlightBox = null;
7961
+ confirmation?.remove();
7962
+ confirmation = null;
7963
+ if (confirmationKey) document.removeEventListener("keydown", confirmationKey, true);
7964
+ confirmationKey = null;
7862
7965
  chrome?.teardown();
7863
7966
  chrome = null;
7864
7967
  clearBuilderToken();
7865
7968
  };
7969
+ const scanPage = (selectors) => {
7970
+ const present = selectors.filter((sel) => {
7971
+ try {
7972
+ return document.querySelector(sel) !== null;
7973
+ } catch {
7974
+ return false;
7975
+ }
7976
+ });
7977
+ const taggedEls = /* @__PURE__ */ new Set();
7978
+ for (const sel of present) {
7979
+ try {
7980
+ for (const e of Array.from(document.querySelectorAll(sel))) taggedEls.add(e);
7981
+ } catch {
7982
+ }
7983
+ }
7984
+ const seen = /* @__PURE__ */ new Set();
7985
+ const candidates = [];
7986
+ const nodes = document.querySelectorAll(INTERACTIVE_SELECTOR);
7987
+ for (const element of Array.from(nodes)) {
7988
+ if (candidates.length >= 200) break;
7989
+ if (element.closest("[data-veo-builder-chrome],[data-veo-builder-panel],[data-veo-picker]")) {
7990
+ continue;
7991
+ }
7992
+ if (element.getClientRects().length === 0) continue;
7993
+ let tagged = false;
7994
+ for (let n = element; n; n = n.parentElement) {
7995
+ if (taggedEls.has(n)) {
7996
+ tagged = true;
7997
+ break;
7998
+ }
7999
+ }
8000
+ if (tagged) continue;
8001
+ const selector = buildSelectorPath(element, 5);
8002
+ if (!selector || seen.has(selector)) continue;
8003
+ seen.add(selector);
8004
+ candidates.push({
8005
+ selector,
8006
+ shortSelector: buildSelectorPath(element, 0),
8007
+ tag: element.tagName.toLowerCase(),
8008
+ id: element.id || null,
8009
+ classes: filterHumanClasses(element),
8010
+ text: elementLabelText(element)
8011
+ });
8012
+ }
8013
+ const { pathname, hash } = window.location;
8014
+ const routePath = /^#\/./.test(hash) ? `${pathname}${hash}` : pathname;
8015
+ return { pagePath: pathname, routePath, candidates, present };
8016
+ };
8017
+ let confirmation = null;
8018
+ let confirmationKey = null;
8019
+ const closeConfirmation = () => {
8020
+ if (!confirmation) return;
8021
+ confirmation.remove();
8022
+ confirmation = null;
8023
+ if (confirmationKey) document.removeEventListener("keydown", confirmationKey, true);
8024
+ confirmationKey = null;
8025
+ panel?.setVisible(true);
8026
+ post({ type: "confirmation-closed" });
8027
+ };
8028
+ const showConfirmation = (card) => {
8029
+ if (confirmation) {
8030
+ confirmation.remove();
8031
+ confirmation = null;
8032
+ }
8033
+ panel?.setVisible(false);
8034
+ const host = document.createElement("div");
8035
+ host.setAttribute("data-veo-builder-chrome", "");
8036
+ host.setAttribute("data-veo-confirmation", "");
8037
+ const shadow = host.attachShadow({ mode: "open" });
8038
+ const style = document.createElement("style");
8039
+ style.textContent = CONFIRMATION_STYLES;
8040
+ const backdrop = document.createElement("div");
8041
+ backdrop.className = "veo-confirm-backdrop";
8042
+ const dialog = document.createElement("div");
8043
+ dialog.className = "veo-confirm";
8044
+ dialog.setAttribute("role", "dialog");
8045
+ dialog.setAttribute("aria-modal", "true");
8046
+ dialog.setAttribute("aria-label", card.title);
8047
+ const badge = document.createElement("div");
8048
+ badge.className = "veo-confirm-check";
8049
+ 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>';
8050
+ const title = document.createElement("h2");
8051
+ title.className = "veo-confirm-title";
8052
+ title.textContent = card.title;
8053
+ const message = document.createElement("p");
8054
+ message.className = "veo-confirm-msg";
8055
+ message.textContent = card.message;
8056
+ const rows = document.createElement("dl");
8057
+ rows.className = "veo-confirm-rows";
8058
+ for (const r of card.rows) {
8059
+ const row = document.createElement("div");
8060
+ row.className = "veo-confirm-row";
8061
+ const dt = document.createElement("dt");
8062
+ dt.textContent = r.label;
8063
+ const dd = document.createElement("dd");
8064
+ dd.textContent = r.value;
8065
+ row.append(dt, dd);
8066
+ rows.appendChild(row);
8067
+ }
8068
+ const button = document.createElement("button");
8069
+ button.type = "button";
8070
+ button.className = "veo-confirm-btn";
8071
+ button.textContent = card.buttonLabel ?? "Listo";
8072
+ button.addEventListener("click", closeConfirmation);
8073
+ dialog.append(badge, title, message);
8074
+ if (card.rows.length > 0) dialog.appendChild(rows);
8075
+ dialog.appendChild(button);
8076
+ shadow.append(style, backdrop, dialog);
8077
+ document.body.appendChild(host);
8078
+ confirmation = host;
8079
+ confirmationKey = (e) => {
8080
+ if (e.key !== "Escape") return;
8081
+ e.preventDefault();
8082
+ e.stopPropagation();
8083
+ closeConfirmation();
8084
+ };
8085
+ document.addEventListener("keydown", confirmationKey, true);
8086
+ button.focus();
8087
+ };
8088
+ let highlightBox = null;
8089
+ const setHighlight = (selector) => {
8090
+ highlightBox?.remove();
8091
+ highlightBox = null;
8092
+ if (!selector) return;
8093
+ let target = null;
8094
+ try {
8095
+ target = document.querySelector(selector);
8096
+ } catch {
8097
+ return;
8098
+ }
8099
+ if (!target) return;
8100
+ const r = target.getBoundingClientRect();
8101
+ const box = document.createElement("div");
8102
+ box.setAttribute("data-veo-builder-chrome", "");
8103
+ 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;`;
8104
+ document.body.appendChild(box);
8105
+ highlightBox = box;
8106
+ };
7866
8107
  if (ctx.panel) {
7867
8108
  chrome = injectBuilderChrome({
7868
8109
  appLabel: window.location.hostname,
@@ -7990,10 +8231,12 @@ function previewProgressFetcher() {
7990
8231
  return data.items ?? [];
7991
8232
  };
7992
8233
  }
8234
+ var CONFIRMATION_STYLES;
7993
8235
  var init_builder_mode = __esm({
7994
8236
  "src/plugins/builder/builder-mode.ts"() {
7995
8237
  init_builder_token();
7996
8238
  init_safe_env();
8239
+ init_selector_builder();
7997
8240
  init_activation_devices();
7998
8241
  init_activation_rules();
7999
8242
  init_block_builder();
@@ -8008,6 +8251,42 @@ var init_builder_mode = __esm({
8008
8251
  init_builder_panel();
8009
8252
  init_design_manipulator();
8010
8253
  init_element_picker();
8254
+ init_interactive_elements();
8255
+ CONFIRMATION_STYLES = `
8256
+ :host { all: initial; }
8257
+ .veo-confirm-backdrop {
8258
+ position: fixed; inset: 0; z-index: 2147483645;
8259
+ background: rgba(20, 16, 14, 0.45);
8260
+ }
8261
+ .veo-confirm {
8262
+ position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
8263
+ z-index: 2147483646; box-sizing: border-box;
8264
+ width: min(432px, calc(100vw - 32px)); padding: 28px 24px 24px;
8265
+ border-radius: 16px; background: #fff; color: #1d1917;
8266
+ box-shadow: 0 24px 64px rgba(0,0,0,0.28);
8267
+ font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
8268
+ text-align: center;
8269
+ }
8270
+ .veo-confirm-check {
8271
+ width: 64px; height: 64px; margin: 0 auto; border-radius: 50%;
8272
+ background: #e6f6df; color: #2f9a4e; display: flex; align-items: center; justify-content: center;
8273
+ }
8274
+ .veo-confirm-check svg { width: 30px; height: 30px; }
8275
+ .veo-confirm-title { margin: 18px 0 0; font-size: 17px; font-weight: 600; }
8276
+ .veo-confirm-msg { margin: 8px 0 0; font-size: 13px; color: #6b6461; }
8277
+ .veo-confirm-rows {
8278
+ margin: 18px 0 0; padding: 12px 14px; border-radius: 12px; background: #f6f2ef; text-align: left;
8279
+ }
8280
+ .veo-confirm-row { display: flex; justify-content: space-between; gap: 16px; font-size: 13px; padding: 4px 0; }
8281
+ .veo-confirm-row dt { color: #6b6461; margin: 0; }
8282
+ .veo-confirm-row dd { margin: 0; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
8283
+ .veo-confirm-btn {
8284
+ display: block; width: 100%; margin-top: 18px; height: 44px; border: 0; border-radius: 10px;
8285
+ background: #b74d00; color: #fff; font: inherit; font-size: 15px; font-weight: 600; cursor: pointer;
8286
+ }
8287
+ .veo-confirm-btn:hover { background: #953c00; }
8288
+ .veo-confirm-btn:focus-visible { outline: 3px solid rgba(183,77,0,0.35); outline-offset: 2px; }
8289
+ `;
8011
8290
  }
8012
8291
  });
8013
8292
 
@@ -8140,11 +8419,15 @@ var init_builder_session = __esm({
8140
8419
  // src/plugins/builder/index.ts
8141
8420
  var builder_exports = {};
8142
8421
  __export(builder_exports, {
8422
+ INTERACTIVE_SELECTOR: () => INTERACTIVE_SELECTOR,
8143
8423
  VEO_BLOCK_MIME: () => VEO_BLOCK_MIME,
8144
8424
  attachBlockDropTarget: () => attachBlockDropTarget,
8145
8425
  attachDesignManipulator: () => attachDesignManipulator,
8426
+ closestInteractive: () => closestInteractive,
8146
8427
  createBuilderSession: () => createBuilderSession,
8428
+ elementLabelText: () => elementLabelText,
8147
8429
  initBuilderMode: () => initBuilderMode,
8430
+ isInteractiveElement: () => isInteractiveElement,
8148
8431
  startElementPicker: () => startElementPicker,
8149
8432
  stopElementPicker: () => stopElementPicker
8150
8433
  });
@@ -8155,6 +8438,7 @@ var init_builder = __esm({
8155
8438
  init_builder_session();
8156
8439
  init_design_manipulator();
8157
8440
  init_element_picker();
8441
+ init_interactive_elements();
8158
8442
  }
8159
8443
  });
8160
8444
 
@@ -8694,6 +8978,298 @@ function definePlugin(plugin) {
8694
8978
 
8695
8979
  // src/plugins/autocapture/dom-listener.ts
8696
8980
  init_safe_env();
8981
+
8982
+ // src/plugins/autocapture/click-quality.ts
8983
+ init_safe_env();
8984
+ init_selector_builder();
8985
+ var NO_DEAD_TAGS = /* @__PURE__ */ new Set(["input", "textarea", "select", "option", "label"]);
8986
+ var MAX_ERROR_MESSAGE = 200;
8987
+ var HISTORY_MS = 3e3;
8988
+ var DEFAULTS = {
8989
+ reactionWindowMs: 1e3,
8990
+ errorWindowMs: 2e3,
8991
+ rageClicks: 3,
8992
+ rageWindowMs: 1e3,
8993
+ rageClicksSlow: 5,
8994
+ rageWindowSlowMs: 3e3,
8995
+ maxPerElement: 50
8996
+ };
8997
+ function describeError(value) {
8998
+ let text;
8999
+ if (value instanceof Error) text = `${value.name}: ${value.message}`;
9000
+ else if (typeof value === "string") text = value;
9001
+ else {
9002
+ try {
9003
+ text = JSON.stringify(value) ?? String(value);
9004
+ } catch {
9005
+ text = String(value);
9006
+ }
9007
+ }
9008
+ return text.slice(0, MAX_ERROR_MESSAGE);
9009
+ }
9010
+ var ClickQualityTracker = class {
9011
+ constructor(emit, options = {}) {
9012
+ this.emit = emit;
9013
+ this.installed = false;
9014
+ this.cleanups = [];
9015
+ // Sensores globales (contadores: cada observación compara su foto inicial).
9016
+ this.netCount = 0;
9017
+ this.navCount = 0;
9018
+ this.focusCount = 0;
9019
+ this.unloading = false;
9020
+ this.errors = [];
9021
+ this.history = /* @__PURE__ */ new Map();
9022
+ this.bursts = /* @__PURE__ */ new Map();
9023
+ this.emitted = /* @__PURE__ */ new Map();
9024
+ this.opts = { ...DEFAULTS, ...options };
9025
+ }
9026
+ install() {
9027
+ if (this.installed || !hasWindow()) return;
9028
+ this.installed = true;
9029
+ const on = (type, handler, capture = true) => {
9030
+ window.addEventListener(type, handler, capture);
9031
+ this.cleanups.push(() => window.removeEventListener(type, handler, capture));
9032
+ };
9033
+ on("error", (e) => {
9034
+ const err = e;
9035
+ this.pushError("error", err.error ?? err.message ?? "error");
9036
+ });
9037
+ on("unhandledrejection", (e) => {
9038
+ this.pushError("unhandledrejection", e.reason ?? "rejection");
9039
+ });
9040
+ on("popstate", () => this.navCount++);
9041
+ on("hashchange", () => this.navCount++);
9042
+ on("focusin", () => this.focusCount++);
9043
+ on("pagehide", () => {
9044
+ this.unloading = true;
9045
+ });
9046
+ on("beforeunload", () => {
9047
+ this.unloading = true;
9048
+ });
9049
+ this.patchHistory();
9050
+ this.patchConsole();
9051
+ this.patchFetch();
9052
+ this.patchXhr();
9053
+ }
9054
+ destroy() {
9055
+ for (const c of this.cleanups) c();
9056
+ this.cleanups = [];
9057
+ for (const b of this.bursts.values()) window.clearTimeout(b.timer);
9058
+ this.bursts.clear();
9059
+ this.history.clear();
9060
+ this.installed = false;
9061
+ }
9062
+ /** Llamar por cada clic real sobre un elemento interactivo, ANTES del throttle. */
9063
+ onClick(element, event) {
9064
+ if (!this.installed) return;
9065
+ const now = Date.now();
9066
+ const obs = { at: now, reacted: false };
9067
+ this.remember(element, obs);
9068
+ this.observeReaction(element, obs, event);
9069
+ this.observeErrors(element, now);
9070
+ this.trackRage(element, now);
9071
+ }
9072
+ // ---------------------------------------------------------------- dead ----
9073
+ observeReaction(element, obs, event) {
9074
+ const netAtClick = this.netCount;
9075
+ const navAtClick = this.navCount;
9076
+ const focusAtClick = this.focusCount;
9077
+ const hrefAtClick = window.location.href;
9078
+ const skipDead = this.isDeadExempt(element, event);
9079
+ let observer = null;
9080
+ if (typeof MutationObserver !== "undefined") {
9081
+ observer = new MutationObserver(() => {
9082
+ obs.reacted = true;
9083
+ observer?.disconnect();
9084
+ observer = null;
9085
+ });
9086
+ observer.observe(document.documentElement, {
9087
+ childList: true,
9088
+ attributes: true,
9089
+ characterData: true,
9090
+ subtree: true
9091
+ });
9092
+ }
9093
+ window.setTimeout(() => {
9094
+ observer?.disconnect();
9095
+ const reacted = obs.reacted || this.unloading || this.netCount !== netAtClick || this.navCount !== navAtClick || this.focusCount !== focusAtClick || window.location.href !== hrefAtClick;
9096
+ obs.reacted = reacted;
9097
+ if (!reacted && !skipDead) this.report("$dead_click", element, {});
9098
+ }, this.opts.reactionWindowMs);
9099
+ }
9100
+ isDeadExempt(element, event) {
9101
+ if (event && (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey)) return true;
9102
+ if (event && typeof event.button === "number" && event.button !== 0) return true;
9103
+ const tag = element.tagName.toLowerCase();
9104
+ if (NO_DEAD_TAGS.has(tag)) return true;
9105
+ if (element.closest('[contenteditable=""],[contenteditable="true"]')) return true;
9106
+ const anchor = element.closest("a");
9107
+ if (anchor) {
9108
+ if (anchor.target === "_blank" || anchor.hasAttribute("download")) return true;
9109
+ const href = anchor.getAttribute("href") ?? "";
9110
+ if (/^(mailto|tel|sms):/i.test(href)) return true;
9111
+ }
9112
+ return false;
9113
+ }
9114
+ // --------------------------------------------------------------- error ----
9115
+ observeErrors(element, clickedAt) {
9116
+ const deadline = clickedAt + this.opts.errorWindowMs;
9117
+ const check = () => {
9118
+ const err = this.errors.find((e) => e.at >= clickedAt && e.at <= deadline);
9119
+ if (err) {
9120
+ this.report("$click_error", element, {
9121
+ $error_source: err.source,
9122
+ $error_message: err.message
9123
+ });
9124
+ return;
9125
+ }
9126
+ if (Date.now() < deadline) window.setTimeout(check, 250);
9127
+ };
9128
+ window.setTimeout(check, 250);
9129
+ }
9130
+ pushError(source, value) {
9131
+ const message = describeError(value);
9132
+ if (message.startsWith("[veo]")) return;
9133
+ this.errors.push({ at: Date.now(), source, message });
9134
+ const cutoff = Date.now() - this.opts.errorWindowMs - 1e3;
9135
+ while (this.errors.length > 0 && this.errors[0].at < cutoff) {
9136
+ this.errors.shift();
9137
+ }
9138
+ if (this.errors.length > 50) this.errors.splice(0, this.errors.length - 50);
9139
+ }
9140
+ // ---------------------------------------------------------------- rage ----
9141
+ remember(element, obs) {
9142
+ const list = this.history.get(element) ?? [];
9143
+ const cutoff = obs.at - HISTORY_MS;
9144
+ const kept = list.filter((o) => o.at >= cutoff);
9145
+ kept.push(obs);
9146
+ this.history.set(element, kept);
9147
+ if (this.history.size > 100) {
9148
+ for (const [el2, obsList] of this.history) {
9149
+ if (obsList.every((o) => o.at < cutoff)) this.history.delete(el2);
9150
+ }
9151
+ }
9152
+ }
9153
+ trackRage(element, now) {
9154
+ const list = this.history.get(element) ?? [];
9155
+ const fast = list.filter((o) => o.at >= now - this.opts.rageWindowMs).length;
9156
+ const slow = list.filter((o) => o.at >= now - this.opts.rageWindowSlowMs).length;
9157
+ const existing = this.bursts.get(element);
9158
+ const isRage = fast >= this.opts.rageClicks || slow >= this.opts.rageClicksSlow;
9159
+ if (!existing && !isRage) return;
9160
+ const burst = existing ?? {
9161
+ startedAt: (list.find((o) => o.at >= now - this.opts.rageWindowSlowMs) ?? { at: now }).at,
9162
+ count: 0,
9163
+ timer: 0
9164
+ };
9165
+ burst.count = list.filter((o) => o.at >= burst.startedAt).length;
9166
+ window.clearTimeout(burst.timer);
9167
+ burst.timer = window.setTimeout(() => {
9168
+ this.bursts.delete(element);
9169
+ const clicks = (this.history.get(element) ?? []).filter((o) => o.at >= burst.startedAt);
9170
+ this.report("$rage_click", element, {
9171
+ $click_count: Math.max(clicks.length, burst.count),
9172
+ $reacted_clicks: clicks.filter((o) => o.reacted).length
9173
+ });
9174
+ }, this.opts.reactionWindowMs + 50);
9175
+ this.bursts.set(element, burst);
9176
+ }
9177
+ // ---------------------------------------------------------------- emit ----
9178
+ report(kind, element, extra) {
9179
+ const key = buildSelectorPath(element, 5);
9180
+ const counts = this.emitted.get(key) ?? { $dead_click: 0, $rage_click: 0, $click_error: 0 };
9181
+ if (counts[kind] >= this.opts.maxPerElement) return;
9182
+ counts[kind]++;
9183
+ this.emitted.set(key, counts);
9184
+ try {
9185
+ this.emit(kind, element, extra);
9186
+ } catch {
9187
+ }
9188
+ }
9189
+ // ------------------------------------------------------------- patches ----
9190
+ patchHistory() {
9191
+ const h = window.history;
9192
+ if (!h) return;
9193
+ const origPush = h.pushState;
9194
+ const origReplace = h.replaceState;
9195
+ const self = this;
9196
+ h.pushState = function(...args) {
9197
+ self.navCount++;
9198
+ return origPush.apply(this, args);
9199
+ };
9200
+ h.replaceState = function(...args) {
9201
+ self.navCount++;
9202
+ return origReplace.apply(this, args);
9203
+ };
9204
+ this.cleanups.push(() => {
9205
+ h.pushState = origPush;
9206
+ h.replaceState = origReplace;
9207
+ });
9208
+ }
9209
+ patchConsole() {
9210
+ const orig = console.error;
9211
+ const self = this;
9212
+ console.error = function(...args) {
9213
+ try {
9214
+ self.pushError("console", args.map(describeError).join(" "));
9215
+ } catch {
9216
+ }
9217
+ return orig.apply(this, args);
9218
+ };
9219
+ this.cleanups.push(() => {
9220
+ console.error = orig;
9221
+ });
9222
+ }
9223
+ /**
9224
+ * `fetch`: cuenta la petición (reacción) y registra respuestas ≥500 o
9225
+ * rechazos como error. Se observa con `.then(ok, err)` sobre la promesa
9226
+ * original: eso marca el rechazo como manejado para el navegador, así que
9227
+ * un fetch fallido sin catch de la app no dispara `unhandledrejection` —
9228
+ * lo registramos nosotros como error de red.
9229
+ */
9230
+ patchFetch() {
9231
+ if (typeof window.fetch !== "function") return;
9232
+ const orig = window.fetch;
9233
+ const self = this;
9234
+ window.fetch = function(...args) {
9235
+ self.netCount++;
9236
+ const p = orig.apply(this, args);
9237
+ if (p && typeof p.then === "function") {
9238
+ p.then(
9239
+ (res) => {
9240
+ if (res && res.status >= 500) self.pushError("fetch", `${res.status} ${res.url}`);
9241
+ },
9242
+ (err) => self.pushError("fetch", err)
9243
+ );
9244
+ }
9245
+ return p;
9246
+ };
9247
+ this.cleanups.push(() => {
9248
+ window.fetch = orig;
9249
+ });
9250
+ }
9251
+ patchXhr() {
9252
+ const proto = typeof XMLHttpRequest !== "undefined" ? XMLHttpRequest.prototype : null;
9253
+ if (!proto) return;
9254
+ const origSend = proto.send;
9255
+ const self = this;
9256
+ proto.send = function(...args) {
9257
+ self.netCount++;
9258
+ try {
9259
+ this.addEventListener("loadend", () => {
9260
+ if (this.status >= 500) self.pushError("xhr", `${this.status} ${this.responseURL}`);
9261
+ });
9262
+ } catch {
9263
+ }
9264
+ return origSend.apply(this, args);
9265
+ };
9266
+ this.cleanups.push(() => {
9267
+ proto.send = origSend;
9268
+ });
9269
+ }
9270
+ };
9271
+
9272
+ // src/plugins/autocapture/dom-listener.ts
8697
9273
  init_constants();
8698
9274
 
8699
9275
  // src/plugins/autocapture/element-serializer.ts
@@ -8804,6 +9380,7 @@ var DomListener = class {
8804
9380
  constructor(client, config) {
8805
9381
  this.client = client;
8806
9382
  this.cleanups = [];
9383
+ this.quality = null;
8807
9384
  const d = DEFAULT_AUTOCAPTURE_CONFIG;
8808
9385
  this.config = {
8809
9386
  enabled: config.enabled ?? d.enabled,
@@ -8816,12 +9393,20 @@ var DomListener = class {
8816
9393
  throttleMs: config.throttleMs ?? d.throttleMs,
8817
9394
  maxTextLength: config.maxTextLength ?? d.maxTextLength,
8818
9395
  captureElementMetadata: config.captureElementMetadata ?? d.captureElementMetadata,
8819
- captureAncestors: Math.min(config.captureAncestors ?? d.captureAncestors, MAX_ANCESTORS)
9396
+ captureAncestors: Math.min(config.captureAncestors ?? d.captureAncestors, MAX_ANCESTORS),
9397
+ captureClickQuality: config.captureClickQuality ?? d.captureClickQuality
8820
9398
  };
8821
9399
  this.throttle = new ElementThrottle(this.config.throttleMs);
8822
9400
  }
8823
9401
  install() {
8824
9402
  if (!hasDocument() || !this.config.enabled) return;
9403
+ if (this.config.captureClicks && this.config.captureClickQuality) {
9404
+ this.quality = new ClickQualityTracker((kind, element, extra) => {
9405
+ const payload = buildAutocapturePayload("click", element, this.config);
9406
+ this.client.track(kind, { ...payload, ...extra });
9407
+ });
9408
+ this.quality.install();
9409
+ }
8825
9410
  if (this.config.captureClicks) this.attach("click", (e) => this.handleClick(e));
8826
9411
  if (this.config.captureSubmits) this.attach("submit", (e) => this.handleSubmit(e), true);
8827
9412
  if (this.config.captureChanges) this.attach("change", (e) => this.handleChange(e), true);
@@ -8829,6 +9414,8 @@ var DomListener = class {
8829
9414
  destroy() {
8830
9415
  for (const cleanup of this.cleanups) cleanup();
8831
9416
  this.cleanups = [];
9417
+ this.quality?.destroy();
9418
+ this.quality = null;
8832
9419
  }
8833
9420
  attach(eventName, handler, useCapture = false) {
8834
9421
  const listener = (e) => {
@@ -8847,6 +9434,7 @@ var DomListener = class {
8847
9434
  const meaningful = this.findMeaningfulAncestor(target);
8848
9435
  if (!meaningful) return;
8849
9436
  if (isElementBlocked(meaningful, this.config.blockSelectors)) return;
9437
+ this.quality?.onClick(meaningful, event);
8850
9438
  if (!this.throttle.shouldFire(meaningful)) return;
8851
9439
  this.emit("click", meaningful);
8852
9440
  }