veo-sdk 0.3.16 → 0.4.1

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
@@ -113,7 +113,11 @@ function readFreshFromLocal() {
113
113
  }
114
114
  function urlParams() {
115
115
  try {
116
- return new URLSearchParams(window.location.search);
116
+ const search = new URLSearchParams(window.location.search);
117
+ if (search.get(BUILDER_TOKEN_PARAM)) return search;
118
+ const hash = window.location.hash.replace(/^#/, "");
119
+ if (hash.includes(`${BUILDER_TOKEN_PARAM}=`)) return new URLSearchParams(hash);
120
+ return search;
117
121
  } catch {
118
122
  return null;
119
123
  }
@@ -178,7 +182,7 @@ var init_builder_token = __esm({
178
182
  init_builder_constants();
179
183
  CTX_KEY = "veo:builder_ctx";
180
184
  LS_PREFIX = "veo:builder_ctx:";
181
- PERSIST_TTL_MS = 12 * 60 * 60 * 1e3;
185
+ PERSIST_TTL_MS = 30 * 60 * 1e3;
182
186
  }
183
187
  });
184
188
 
@@ -374,6 +378,118 @@ var init_constants2 = __esm({
374
378
  }
375
379
  });
376
380
 
381
+ // src/plugins/guides/rich-text.ts
382
+ function renderRichText(html, doc) {
383
+ const fragment = doc.createDocumentFragment();
384
+ if (typeof html !== "string" || html.length === 0) return fragment;
385
+ let parsed;
386
+ try {
387
+ parsed = new DOMParser().parseFromString(
388
+ html.length > MAX_INPUT_LEN ? html.slice(0, MAX_INPUT_LEN) : html,
389
+ "text/html"
390
+ );
391
+ } catch {
392
+ fragment.appendChild(doc.createTextNode(html));
393
+ return fragment;
394
+ }
395
+ const budget = { nodes: 0, exceeded: false };
396
+ for (const child of Array.from(parsed.body.childNodes)) {
397
+ const rebuilt = rebuildNode(child, doc, 0, budget);
398
+ if (rebuilt) fragment.appendChild(rebuilt);
399
+ }
400
+ if (budget.exceeded) {
401
+ const plain = doc.createDocumentFragment();
402
+ plain.appendChild(doc.createTextNode(parsed.body.textContent ?? ""));
403
+ return plain;
404
+ }
405
+ return fragment;
406
+ }
407
+ function rebuildNode(node, doc, depth, budget) {
408
+ if (budget.exceeded) return null;
409
+ if (++budget.nodes > MAX_NODES || depth > MAX_DEPTH) {
410
+ budget.exceeded = true;
411
+ return null;
412
+ }
413
+ if (node.nodeType === Node.TEXT_NODE) {
414
+ return doc.createTextNode(node.textContent ?? "");
415
+ }
416
+ if (node.nodeType !== Node.ELEMENT_NODE) return null;
417
+ const el = node;
418
+ if (DROP_TAGS.has(el.tagName)) return null;
419
+ const mapped = ALLOWED_TAGS[el.tagName];
420
+ if (!mapped) {
421
+ const frag = doc.createDocumentFragment();
422
+ for (const child of Array.from(el.childNodes)) {
423
+ const rebuilt2 = rebuildNode(child, doc, depth + 1, budget);
424
+ if (rebuilt2) frag.appendChild(rebuilt2);
425
+ }
426
+ return frag;
427
+ }
428
+ if (mapped === "a") {
429
+ const href = el.getAttribute("href") ?? "";
430
+ if (!isSafeUrl(href)) {
431
+ const frag = doc.createDocumentFragment();
432
+ for (const child of Array.from(el.childNodes)) {
433
+ const rebuilt2 = rebuildNode(child, doc, depth + 1, budget);
434
+ if (rebuilt2) frag.appendChild(rebuilt2);
435
+ }
436
+ return frag;
437
+ }
438
+ const a = doc.createElement("a");
439
+ a.setAttribute("href", href);
440
+ a.setAttribute("target", "_blank");
441
+ a.setAttribute("rel", "noopener noreferrer nofollow");
442
+ for (const child of Array.from(el.childNodes)) {
443
+ const rebuilt2 = rebuildNode(child, doc, depth + 1, budget);
444
+ if (rebuilt2) a.appendChild(rebuilt2);
445
+ }
446
+ return a;
447
+ }
448
+ const rebuilt = doc.createElement(mapped);
449
+ for (const child of Array.from(el.childNodes)) {
450
+ const childNode = rebuildNode(child, doc, depth + 1, budget);
451
+ if (childNode) rebuilt.appendChild(childNode);
452
+ }
453
+ return rebuilt;
454
+ }
455
+ var ALLOWED_TAGS, DROP_TAGS, MAX_INPUT_LEN, MAX_DEPTH, MAX_NODES;
456
+ var init_rich_text = __esm({
457
+ "src/plugins/guides/rich-text.ts"() {
458
+ init_block_builder();
459
+ ALLOWED_TAGS = {
460
+ P: "p",
461
+ BR: "br",
462
+ STRONG: "strong",
463
+ B: "strong",
464
+ EM: "em",
465
+ I: "em",
466
+ U: "u",
467
+ S: "s",
468
+ UL: "ul",
469
+ OL: "ol",
470
+ LI: "li",
471
+ A: "a"
472
+ };
473
+ DROP_TAGS = /* @__PURE__ */ new Set([
474
+ "SCRIPT",
475
+ "STYLE",
476
+ "TEMPLATE",
477
+ "IFRAME",
478
+ "OBJECT",
479
+ "EMBED",
480
+ "NOSCRIPT",
481
+ "TITLE",
482
+ "TEXTAREA",
483
+ "SELECT",
484
+ "SVG",
485
+ "MATH"
486
+ ]);
487
+ MAX_INPUT_LEN = 16 * 1024;
488
+ MAX_DEPTH = 20;
489
+ MAX_NODES = 1e3;
490
+ }
491
+ });
492
+
377
493
  // src/plugins/guides/block-builder.ts
378
494
  function buildStepContent(step, doc, callbacks) {
379
495
  const container = doc.createElement("div");
@@ -468,9 +584,13 @@ function buildContentBlock(block, doc) {
468
584
  return el;
469
585
  }
470
586
  case "text": {
471
- const el = doc.createElement("p");
587
+ const el = doc.createElement("div");
472
588
  el.className = "veo-guide-text";
473
- el.textContent = typeof block.text === "string" ? block.text : "";
589
+ if (typeof block.html === "string" && block.html) {
590
+ el.appendChild(renderRichText(block.html, doc));
591
+ } else {
592
+ el.textContent = typeof block.text === "string" ? block.text : "";
593
+ }
474
594
  applyBlockStyle(el, block.style, "text");
475
595
  return el;
476
596
  }
@@ -496,7 +616,11 @@ function buildButtonBlock(block, doc, callbacks) {
496
616
  btn.addEventListener("click", () => {
497
617
  const action = block.action ?? "dismiss";
498
618
  const url = action === "url" && typeof block.url === "string" && isSafeUrl(block.url) ? block.url : void 0;
499
- callbacks.onCtaClick(action, url);
619
+ const meta = {
620
+ ...typeof block.id === "string" && block.id ? { buttonId: block.id } : {},
621
+ ...typeof block.text === "string" && block.text ? { buttonText: block.text } : {}
622
+ };
623
+ callbacks.onCtaClick(action, url, Object.keys(meta).length ? meta : void 0);
500
624
  });
501
625
  return btn;
502
626
  }
@@ -506,6 +630,10 @@ function clampNum(n, min, max) {
506
630
  function applyBlockStyle(el, style, kind) {
507
631
  if (!style || typeof style !== "object") return;
508
632
  const s = style;
633
+ if (kind === "image" && s.bleed === true) {
634
+ el.classList.add("veo-guide-image--bleed");
635
+ return;
636
+ }
509
637
  const align = typeof s.align === "string" ? s.align : null;
510
638
  if (align && (align === "left" || align === "center" || align === "right")) {
511
639
  if (kind === "text") el.style.textAlign = align;
@@ -556,6 +684,7 @@ var BLOCK_ALIGN_SELF;
556
684
  var init_block_builder = __esm({
557
685
  "src/plugins/guides/block-builder.ts"() {
558
686
  init_constants2();
687
+ init_rich_text();
559
688
  BLOCK_ALIGN_SELF = {
560
689
  left: "flex-start",
561
690
  center: "center",
@@ -564,83 +693,6 @@ var init_block_builder = __esm({
564
693
  }
565
694
  });
566
695
 
567
- // src/plugins/guides/walkthrough-block-builder.ts
568
- function buildWalkthroughStepContent(step, stepIndex, totalSteps, doc, callbacks) {
569
- const container = doc.createElement("div");
570
- container.className = "veo-guide-content";
571
- const counter = doc.createElement("div");
572
- counter.className = "veo-walkthrough-counter";
573
- counter.textContent = `Paso ${stepIndex + 1} de ${totalSteps}`;
574
- container.appendChild(counter);
575
- const progress = doc.createElement("div");
576
- progress.className = "veo-walkthrough-progress";
577
- for (let i = 0; i < totalSteps; i++) {
578
- const dot = doc.createElement("span");
579
- dot.className = "veo-walkthrough-progress-dot";
580
- if (i < stepIndex) dot.classList.add("completed");
581
- if (i === stepIndex) dot.classList.add("active");
582
- progress.appendChild(dot);
583
- }
584
- container.appendChild(progress);
585
- if (typeof step.imageUrl === "string" && step.imageUrl && isSafeUrl(step.imageUrl)) {
586
- const img = doc.createElement("img");
587
- img.className = "veo-guide-image";
588
- img.src = step.imageUrl;
589
- img.alt = typeof step.title === "string" ? step.title : "";
590
- container.appendChild(img);
591
- }
592
- if (typeof step.title === "string" && step.title) {
593
- const heading = doc.createElement("h2");
594
- heading.className = "veo-guide-title";
595
- heading.textContent = step.title;
596
- container.appendChild(heading);
597
- }
598
- if (typeof step.content === "string" && step.content) {
599
- const paragraph = doc.createElement("p");
600
- paragraph.className = "veo-guide-text";
601
- paragraph.textContent = step.content;
602
- container.appendChild(paragraph);
603
- }
604
- const actions = doc.createElement("div");
605
- actions.className = "veo-walkthrough-actions";
606
- const skipBtn = doc.createElement("button");
607
- skipBtn.type = "button";
608
- skipBtn.className = "veo-walkthrough-skip";
609
- skipBtn.textContent = "Omitir";
610
- skipBtn.addEventListener("click", () => callbacks.onSkip());
611
- actions.appendChild(skipBtn);
612
- const rightGroup = doc.createElement("div");
613
- rightGroup.className = "veo-walkthrough-actions-right";
614
- if (stepIndex > 0) {
615
- const backBtn = doc.createElement("button");
616
- backBtn.type = "button";
617
- backBtn.className = "veo-walkthrough-btn-secondary";
618
- backBtn.textContent = "Atr\xE1s";
619
- backBtn.addEventListener("click", () => callbacks.onBack());
620
- rightGroup.appendChild(backBtn);
621
- }
622
- const isLastStep = stepIndex === totalSteps - 1;
623
- const primaryBtn = doc.createElement("button");
624
- primaryBtn.type = "button";
625
- primaryBtn.className = "veo-guide-cta";
626
- const defaultLabel = isLastStep ? "Finalizar" : "Siguiente";
627
- primaryBtn.textContent = typeof step.ctaText === "string" && step.ctaText ? step.ctaText : defaultLabel;
628
- primaryBtn.addEventListener("click", () => {
629
- if (isLastStep) callbacks.onComplete();
630
- else callbacks.onNext();
631
- });
632
- rightGroup.appendChild(primaryBtn);
633
- actions.appendChild(rightGroup);
634
- container.appendChild(actions);
635
- container.appendChild(createCloseButton(doc, () => callbacks.onSkip()));
636
- return container;
637
- }
638
- var init_walkthrough_block_builder = __esm({
639
- "src/plugins/guides/walkthrough-block-builder.ts"() {
640
- init_block_builder();
641
- }
642
- });
643
-
644
696
  // src/plugins/guides/guide-design.ts
645
697
  function clamp(n, min, max) {
646
698
  return Math.min(max, Math.max(min, n));
@@ -674,6 +726,9 @@ function applyDesignVars(host, style) {
674
726
  if (typeof s.width === "number" && Number.isFinite(s.width)) {
675
727
  host.style.setProperty("--veo-width", `${clamp(s.width, 220, 720)}px`);
676
728
  }
729
+ if (typeof s.height === "number" && Number.isFinite(s.height)) {
730
+ host.style.setProperty("--veo-min-h", `${clamp(s.height, 120, 900)}px`);
731
+ }
677
732
  if (s.align === "left" || s.align === "center" || s.align === "right") {
678
733
  host.style.setProperty("--veo-actions-justify", ALIGN_JUSTIFY[s.align]);
679
734
  }
@@ -803,6 +858,77 @@ var init_guide_design = __esm({
803
858
  }
804
859
  });
805
860
 
861
+ // src/plugins/guides/inline-host.ts
862
+ function readInlinePosition(style) {
863
+ const p = style?.inlinePosition;
864
+ return p === "before" || p === "prepend" || p === "append" ? p : "after";
865
+ }
866
+ function insertHost(anchor, host, position) {
867
+ switch (position) {
868
+ case "before":
869
+ anchor.before(host);
870
+ break;
871
+ case "after":
872
+ anchor.after(host);
873
+ break;
874
+ case "prepend":
875
+ anchor.prepend(host);
876
+ break;
877
+ case "append":
878
+ anchor.append(host);
879
+ break;
880
+ }
881
+ }
882
+ function keepHostAttached(host, selector, position) {
883
+ const id = window.setInterval(() => {
884
+ if (host.isConnected) return;
885
+ const anchor = document.querySelector(selector);
886
+ if (anchor) insertHost(anchor, host, position);
887
+ }, 1e3);
888
+ return () => window.clearInterval(id);
889
+ }
890
+ var init_inline_host = __esm({
891
+ "src/plugins/guides/inline-host.ts"() {
892
+ }
893
+ });
894
+
895
+ // src/plugins/guides/wait-for-element.ts
896
+ function waitForElement(selector, timeoutMs = DEFAULT_ANCHOR_WAIT_MS) {
897
+ return new Promise((resolve) => {
898
+ const safeQuery = () => {
899
+ try {
900
+ return document.querySelector(selector);
901
+ } catch {
902
+ return null;
903
+ }
904
+ };
905
+ const existing = safeQuery();
906
+ if (existing) {
907
+ resolve(existing);
908
+ return;
909
+ }
910
+ let resolved = false;
911
+ const finish = (el) => {
912
+ if (resolved) return;
913
+ resolved = true;
914
+ observer.disconnect();
915
+ clearTimeout(timer);
916
+ resolve(el);
917
+ };
918
+ const observer = new MutationObserver(() => {
919
+ const el = safeQuery();
920
+ if (el) finish(el);
921
+ });
922
+ observer.observe(document.body, { childList: true, subtree: true });
923
+ const timer = setTimeout(() => finish(null), timeoutMs);
924
+ });
925
+ }
926
+ var init_wait_for_element = __esm({
927
+ "src/plugins/guides/wait-for-element.ts"() {
928
+ init_constants2();
929
+ }
930
+ });
931
+
806
932
  // src/plugins/guides/styles.ts
807
933
  var GUIDE_STYLES;
808
934
  var init_styles = __esm({
@@ -828,6 +954,13 @@ var init_styles = __esm({
828
954
  z-index: ${GUIDE_Z_INDEX};
829
955
  animation: veo-fade-in 180ms ease-out;
830
956
  }
957
+ /* Sin backdrop (style.backdrop === false): la app queda usable detr\xE1s; solo la
958
+ tarjeta captura el mouse. El click-en-backdrop deja de cerrar (no hay backdrop). */
959
+ .veo-modal-overlay--none {
960
+ background: transparent;
961
+ pointer-events: none;
962
+ }
963
+ .veo-modal-overlay--none .veo-modal-card { pointer-events: auto; }
831
964
  /*
832
965
  * Posici\xF3n libre/preset: --veo-pos-x/y son porcentajes (default 50% = centro).
833
966
  * El truco translate(-pos) alinea la MISMA fracci\xF3n de la tarjeta con esa
@@ -844,6 +977,7 @@ var init_styles = __esm({
844
977
  border: var(--veo-border-width, 0) solid var(--veo-border-color, transparent);
845
978
  padding: var(--veo-pad, 24px);
846
979
  max-width: var(--veo-width); width: 90%;
980
+ min-height: var(--veo-min-h, auto);
847
981
  box-shadow: var(--veo-shadow);
848
982
  animation: veo-fade-in 180ms ease-out;
849
983
  }
@@ -859,6 +993,14 @@ var init_styles = __esm({
859
993
  }
860
994
  .veo-banner-top { top: 0; }
861
995
  .veo-banner-bottom { bottom: 0; }
996
+ /* Banner EMBEBIDO en un contenedor (step.selector): fluye dentro del contenedor
997
+ y empuja su contenido, en vez de flotar fijo sobre la pantalla. */
998
+ .veo-banner-embedded {
999
+ position: static;
1000
+ left: auto; right: auto;
1001
+ width: 100%;
1002
+ border-radius: var(--veo-radius, 0);
1003
+ }
862
1004
 
863
1005
  .veo-tooltip {
864
1006
  position: absolute;
@@ -912,6 +1054,21 @@ var init_styles = __esm({
912
1054
  border-radius: var(--veo-image-radius, 8px);
913
1055
  margin: var(--veo-image-mt, 0) 0 var(--veo-image-mb, 12px);
914
1056
  }
1057
+ /* Imagen A SANGRE: rompe el padding de la tarjeta y ocupa el ancho completo
1058
+ (hero estilo anuncio). Como primer bloque, hereda el redondeo superior. */
1059
+ .veo-guide-image--bleed {
1060
+ width: calc(100% + var(--veo-pad, 24px) * 2);
1061
+ max-width: none;
1062
+ max-height: 280px;
1063
+ align-self: auto;
1064
+ border-radius: 0;
1065
+ margin: 0 calc(var(--veo-pad, 24px) * -1) 12px;
1066
+ }
1067
+ .veo-guide-content > .veo-guide-image--bleed:first-child {
1068
+ margin-top: calc(var(--veo-pad, 24px) * -1);
1069
+ border-radius: calc(var(--veo-radius, 12px) - var(--veo-border-width, 0px))
1070
+ calc(var(--veo-radius, 12px) - var(--veo-border-width, 0px)) 0 0;
1071
+ }
915
1072
  .veo-guide-title {
916
1073
  font-size: var(--veo-title-size, 18px); font-weight: 600; line-height: 1.3;
917
1074
  text-align: var(--veo-title-align, left);
@@ -924,6 +1081,18 @@ var init_styles = __esm({
924
1081
  margin: var(--veo-text-mt, 0) 0 var(--veo-text-mb, 16px);
925
1082
  color: var(--veo-text-color, var(--veo-text-secondary));
926
1083
  }
1084
+ /* Rich text dentro de un bloque de texto (p/listas/links/\xE9nfasis). */
1085
+ .veo-guide-text p { margin: 0 0 8px; }
1086
+ .veo-guide-text p:last-child { margin-bottom: 0; }
1087
+ .veo-guide-text ul, .veo-guide-text ol { margin: 0 0 8px; padding-left: 20px; }
1088
+ .veo-guide-text ul { list-style: disc; }
1089
+ .veo-guide-text ol { list-style: decimal; }
1090
+ .veo-guide-text li { margin: 2px 0; display: list-item; }
1091
+ .veo-guide-text a { color: var(--veo-primary); text-decoration: underline; cursor: pointer; }
1092
+ .veo-guide-text strong { font-weight: 600; }
1093
+ .veo-guide-text em { font-style: italic; }
1094
+ .veo-guide-text u { text-decoration: underline; }
1095
+ .veo-guide-text s { text-decoration: line-through; }
927
1096
  .veo-guide-actions {
928
1097
  display: flex; gap: 8px; justify-content: var(--veo-actions-justify);
929
1098
  }
@@ -1062,6 +1231,44 @@ var init_styles = __esm({
1062
1231
  }
1063
1232
  .veo-walkthrough-skip:hover { color: var(--veo-text); }
1064
1233
 
1234
+ /* \u2500\u2500 Badge (elemento inyectado junto al ancla que abre un tooltip) \u2500\u2500 */
1235
+ .veo-badge {
1236
+ display: inline-flex; align-items: center; justify-content: center;
1237
+ border: none; padding: 0; margin: 0 4px;
1238
+ background: none; cursor: pointer;
1239
+ line-height: 1; vertical-align: middle;
1240
+ font-family: inherit;
1241
+ }
1242
+ .veo-badge:focus-visible { outline: 2px solid var(--veo-primary); outline-offset: 2px; }
1243
+ .veo-badge--icon {
1244
+ border-radius: 50%;
1245
+ background: color-mix(in srgb, var(--veo-primary) 14%, transparent);
1246
+ color: var(--veo-primary);
1247
+ font-weight: 600;
1248
+ }
1249
+ .veo-badge--dot {
1250
+ border-radius: 50%;
1251
+ background: var(--veo-primary);
1252
+ animation: veo-badge-pulse 2s ease-out infinite;
1253
+ }
1254
+ .veo-badge--pill {
1255
+ border-radius: 999px;
1256
+ background: var(--veo-primary);
1257
+ color: #fff;
1258
+ font-weight: 600;
1259
+ padding: 3px 9px;
1260
+ white-space: nowrap;
1261
+ }
1262
+ .veo-badge--image img { display: block; border-radius: 4px; object-fit: cover; }
1263
+ /* El tooltip del badge usa strategy fixed (el host vive dentro del flujo del
1264
+ cliente; un absoluto se recortar\xEDa con overflow de ancestros). */
1265
+ .veo-badge-tooltip { position: fixed; }
1266
+ @keyframes veo-badge-pulse {
1267
+ 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--veo-primary) 45%, transparent); }
1268
+ 70% { box-shadow: 0 0 0 7px transparent; }
1269
+ 100% { box-shadow: 0 0 0 0 transparent; }
1270
+ }
1271
+
1065
1272
  .veo-custom-floating {
1066
1273
  position: fixed;
1067
1274
  z-index: ${GUIDE_Z_INDEX};
@@ -1180,6 +1387,14 @@ var init_base_renderer = __esm({
1180
1387
  registerCleanup(fn) {
1181
1388
  this.cleanups.push(fn);
1182
1389
  }
1390
+ /**
1391
+ * Host de la guía montada (o `null` si aún no se montó / ya se destruyó).
1392
+ * Lo usa el modo builder para adjuntar manipulación directa (drag/resize)
1393
+ * sobre el shadow root abierto sin tocar los renderers.
1394
+ */
1395
+ hostElement() {
1396
+ return this.host;
1397
+ }
1183
1398
  /** Remueve el host del DOM y corre todas las funciones de cleanup. */
1184
1399
  destroy() {
1185
1400
  for (const fn of this.cleanups) {
@@ -1199,26 +1414,402 @@ var init_base_renderer = __esm({
1199
1414
  }
1200
1415
  });
1201
1416
 
1417
+ // src/plugins/guides/renderers/floating-arrow.ts
1418
+ function positionArrow(arrowEl, placement, data) {
1419
+ const side = STATIC_SIDE[placement.split("-")[0] ?? "bottom"] ?? "top";
1420
+ for (const prop of ["top", "bottom", "left", "right"]) {
1421
+ arrowEl.style.setProperty(prop, "");
1422
+ }
1423
+ if (data?.x != null) arrowEl.style.setProperty("left", `${data.x}px`);
1424
+ if (data?.y != null) arrowEl.style.setProperty("top", `${data.y}px`);
1425
+ arrowEl.style.setProperty(side, "-6px");
1426
+ }
1427
+ var STATIC_SIDE;
1428
+ var init_floating_arrow = __esm({
1429
+ "src/plugins/guides/renderers/floating-arrow.ts"() {
1430
+ STATIC_SIDE = {
1431
+ top: "bottom",
1432
+ bottom: "top",
1433
+ left: "right",
1434
+ right: "left"
1435
+ };
1436
+ }
1437
+ });
1438
+ function readBadgeConfig(step) {
1439
+ const raw = step.style?.badge;
1440
+ if (!raw || typeof raw !== "object") return { kind: "icon" };
1441
+ const b = raw;
1442
+ const kind = b.kind === "dot" || b.kind === "pill" || b.kind === "image" || b.kind === "icon" ? b.kind : "icon";
1443
+ return {
1444
+ kind,
1445
+ icon: typeof b.icon === "string" ? b.icon : null,
1446
+ text: typeof b.text === "string" ? b.text : null,
1447
+ imageUrl: typeof b.imageUrl === "string" ? b.imageUrl : null,
1448
+ color: typeof b.color === "string" && COLOR_RE.test(b.color.trim()) ? b.color.trim() : null,
1449
+ size: typeof b.size === "number" && Number.isFinite(b.size) ? b.size : null
1450
+ };
1451
+ }
1452
+ function clamp2(n, min, max) {
1453
+ return Math.min(max, Math.max(min, n));
1454
+ }
1455
+ function buildBadgeElement(config, doc) {
1456
+ const btn = doc.createElement("button");
1457
+ btn.type = "button";
1458
+ btn.className = `veo-badge veo-badge--${config.kind}`;
1459
+ btn.setAttribute("aria-label", "M\xE1s informaci\xF3n");
1460
+ const size = clamp2(config.size ?? 18, 8, 48);
1461
+ switch (config.kind) {
1462
+ case "dot":
1463
+ btn.style.width = `${size}px`;
1464
+ btn.style.height = `${size}px`;
1465
+ if (config.color) btn.style.background = config.color;
1466
+ break;
1467
+ case "pill": {
1468
+ btn.textContent = config.text?.slice(0, 24) || "Nuevo";
1469
+ btn.style.fontSize = `${clamp2(Math.round(size * 0.62), 8, 24)}px`;
1470
+ if (config.color) btn.style.background = config.color;
1471
+ break;
1472
+ }
1473
+ case "image": {
1474
+ if (config.imageUrl && isSafeUrl(config.imageUrl)) {
1475
+ const img = doc.createElement("img");
1476
+ img.src = config.imageUrl;
1477
+ img.alt = "";
1478
+ img.style.width = `${size}px`;
1479
+ img.style.height = `${size}px`;
1480
+ btn.appendChild(img);
1481
+ } else {
1482
+ btn.className = "veo-badge veo-badge--icon";
1483
+ btn.textContent = "?";
1484
+ btn.style.width = `${size}px`;
1485
+ btn.style.height = `${size}px`;
1486
+ btn.style.fontSize = `${clamp2(Math.round(size * 0.62), 8, 32)}px`;
1487
+ }
1488
+ break;
1489
+ }
1490
+ default: {
1491
+ btn.textContent = (config.icon || "\u2139\uFE0F").slice(0, 4);
1492
+ btn.style.width = `${size}px`;
1493
+ btn.style.height = `${size}px`;
1494
+ btn.style.fontSize = `${clamp2(Math.round(size * 0.62), 8, 32)}px`;
1495
+ if (config.color) btn.style.color = config.color;
1496
+ break;
1497
+ }
1498
+ }
1499
+ return btn;
1500
+ }
1501
+ var HOVER_CLOSE_DELAY_MS, BadgeRenderer;
1502
+ var init_badge_renderer = __esm({
1503
+ "src/plugins/guides/renderers/badge-renderer.ts"() {
1504
+ init_block_builder();
1505
+ init_guide_design();
1506
+ init_inline_host();
1507
+ init_wait_for_element();
1508
+ init_base_renderer();
1509
+ init_floating_arrow();
1510
+ HOVER_CLOSE_DELAY_MS = 150;
1511
+ BadgeRenderer = class extends BaseRenderer {
1512
+ constructor() {
1513
+ super(...arguments);
1514
+ this.tooltip = null;
1515
+ this.arrowEl = null;
1516
+ this.badgeBtn = null;
1517
+ this.open = false;
1518
+ this.shownEmitted = false;
1519
+ this.trigger = "hover";
1520
+ this.side = "top";
1521
+ this.closeTimer = null;
1522
+ this.stopFloat = null;
1523
+ }
1524
+ async render(context) {
1525
+ const step = context.guide.guideSteps[0];
1526
+ if (!step) return;
1527
+ const selector = step.selector ?? context.guide.activationRules.selector;
1528
+ if (typeof selector !== "string" || selector.length === 0) return;
1529
+ const anchor = await waitForElement(selector);
1530
+ if (!anchor) return;
1531
+ const { host, root } = this.createHost();
1532
+ const ownerDocument = root.ownerDocument ?? document;
1533
+ this.applyDesign(step.style);
1534
+ this.trigger = step.style?.badgeTrigger === "click" ? "click" : "hover";
1535
+ const rawSide = step.style?.tooltipPlacement;
1536
+ this.side = rawSide === "bottom" || rawSide === "left" || rawSide === "right" ? rawSide : "top";
1537
+ const position = readInlinePosition(step.style);
1538
+ host.style.display = "inline-flex";
1539
+ host.style.verticalAlign = "middle";
1540
+ const badge = buildBadgeElement(readBadgeConfig(step), ownerDocument);
1541
+ root.appendChild(badge);
1542
+ this.badgeBtn = badge;
1543
+ insertHost(anchor, host, position);
1544
+ this.registerCleanup(keepHostAttached(host, selector, position));
1545
+ const tooltip = ownerDocument.createElement("div");
1546
+ tooltip.className = "veo-tooltip veo-badge-tooltip";
1547
+ tooltip.style.display = "none";
1548
+ const emit = (action, meta) => {
1549
+ context.onInteraction({
1550
+ guideId: context.guide.guideId,
1551
+ stepIndex: 0,
1552
+ action,
1553
+ ...meta ? { metadata: meta } : {}
1554
+ });
1555
+ };
1556
+ const buildTooltipContent = (s) => buildStepContent(s, ownerDocument, {
1557
+ onCtaClick: (action, url, meta) => {
1558
+ emit("cta_clicked", meta);
1559
+ if (action === "url" && url) window.open(url, "_blank", "noopener,noreferrer");
1560
+ if (action === "dismiss") {
1561
+ emit("dismissed");
1562
+ context.onClose();
1563
+ return;
1564
+ }
1565
+ this.closeTooltip();
1566
+ },
1567
+ onDismiss: () => {
1568
+ emit("dismissed");
1569
+ context.onClose();
1570
+ }
1571
+ });
1572
+ const content = buildTooltipContent(step);
1573
+ tooltip.appendChild(content);
1574
+ const arrowEl = ownerDocument.createElement("div");
1575
+ arrowEl.className = "veo-tooltip-arrow";
1576
+ tooltip.appendChild(arrowEl);
1577
+ root.appendChild(tooltip);
1578
+ this.tooltip = tooltip;
1579
+ this.arrowEl = arrowEl;
1580
+ this.liveContainer = tooltip;
1581
+ this.liveContent = content;
1582
+ this.liveKey = this.liveKeyOf(step);
1583
+ this.liveBuild = buildTooltipContent;
1584
+ const openNow = () => {
1585
+ this.cancelClose();
1586
+ if (!this.open) {
1587
+ this.openTooltip(badge);
1588
+ if (!this.shownEmitted) {
1589
+ this.shownEmitted = true;
1590
+ emit("shown");
1591
+ }
1592
+ }
1593
+ };
1594
+ if (this.trigger === "hover") {
1595
+ const scheduleClose = () => {
1596
+ this.cancelClose();
1597
+ this.closeTimer = window.setTimeout(() => this.closeTooltip(), HOVER_CLOSE_DELAY_MS);
1598
+ };
1599
+ badge.addEventListener("mouseenter", openNow);
1600
+ badge.addEventListener("focus", openNow);
1601
+ badge.addEventListener("mouseleave", scheduleClose);
1602
+ badge.addEventListener("blur", scheduleClose);
1603
+ tooltip.addEventListener("mouseenter", () => this.cancelClose());
1604
+ tooltip.addEventListener("mouseleave", scheduleClose);
1605
+ } else {
1606
+ badge.addEventListener("click", () => {
1607
+ if (this.open) this.closeTooltip();
1608
+ else openNow();
1609
+ });
1610
+ const onDocClick = (e) => {
1611
+ if (!this.open) return;
1612
+ if (e.composedPath().includes(host)) return;
1613
+ this.closeTooltip();
1614
+ };
1615
+ document.addEventListener("click", onDocClick, true);
1616
+ this.registerCleanup(() => document.removeEventListener("click", onDocClick, true));
1617
+ }
1618
+ const onKey = (e) => {
1619
+ if (e.key === "Escape" && this.open) this.closeTooltip();
1620
+ };
1621
+ document.addEventListener("keydown", onKey);
1622
+ this.registerCleanup(() => document.removeEventListener("keydown", onKey));
1623
+ if (context.isPreview) openNow();
1624
+ }
1625
+ /** Cambiar de ancla/posición/trigger requiere re-montar y re-cablear. */
1626
+ liveKeyOf(step) {
1627
+ const selector = step.selector ?? "";
1628
+ const position = readInlinePosition(step.style);
1629
+ const trigger = step.style?.badgeTrigger === "click" ? "click" : "hover";
1630
+ return `${selector}|${position}|${trigger}`;
1631
+ }
1632
+ onLiveUpdate(step) {
1633
+ if (this.badgeBtn) {
1634
+ const doc = this.badgeBtn.ownerDocument;
1635
+ const next = buildBadgeElement(readBadgeConfig(step), doc);
1636
+ this.badgeBtn.className = next.className;
1637
+ this.badgeBtn.setAttribute("style", next.getAttribute("style") ?? "");
1638
+ this.badgeBtn.replaceChildren(...Array.from(next.childNodes));
1639
+ }
1640
+ const rawSide = step.style?.tooltipPlacement;
1641
+ this.side = rawSide === "bottom" || rawSide === "left" || rawSide === "right" ? rawSide : "top";
1642
+ if (this.open && this.badgeBtn) this.position(this.badgeBtn);
1643
+ }
1644
+ destroy() {
1645
+ this.cancelClose();
1646
+ this.stopFloat?.();
1647
+ this.stopFloat = null;
1648
+ this.tooltip = null;
1649
+ this.arrowEl = null;
1650
+ this.badgeBtn = null;
1651
+ this.open = false;
1652
+ super.destroy();
1653
+ }
1654
+ openTooltip(badge) {
1655
+ if (!this.tooltip) return;
1656
+ this.tooltip.style.display = "block";
1657
+ this.open = true;
1658
+ void this.position(badge);
1659
+ const reposition = () => {
1660
+ void this.position(badge);
1661
+ };
1662
+ window.addEventListener("scroll", reposition, true);
1663
+ window.addEventListener("resize", reposition);
1664
+ this.stopFloat = () => {
1665
+ window.removeEventListener("scroll", reposition, true);
1666
+ window.removeEventListener("resize", reposition);
1667
+ };
1668
+ }
1669
+ closeTooltip() {
1670
+ this.cancelClose();
1671
+ if (!this.tooltip || !this.open) return;
1672
+ this.tooltip.style.display = "none";
1673
+ this.open = false;
1674
+ this.stopFloat?.();
1675
+ this.stopFloat = null;
1676
+ }
1677
+ cancelClose() {
1678
+ if (this.closeTimer !== null) {
1679
+ window.clearTimeout(this.closeTimer);
1680
+ this.closeTimer = null;
1681
+ }
1682
+ }
1683
+ async position(badge) {
1684
+ const tooltip = this.tooltip;
1685
+ const arrowEl = this.arrowEl;
1686
+ if (!tooltip || !arrowEl) return;
1687
+ const { x, y, placement, middlewareData } = await dom.computePosition(badge, tooltip, {
1688
+ strategy: "fixed",
1689
+ placement: this.side,
1690
+ middleware: [dom.offset(8), dom.flip(), dom.shift({ padding: 8 }), dom.arrow({ element: arrowEl })]
1691
+ });
1692
+ tooltip.style.left = `${x}px`;
1693
+ tooltip.style.top = `${y}px`;
1694
+ positionArrow(arrowEl, placement, middlewareData.arrow);
1695
+ }
1696
+ };
1697
+ }
1698
+ });
1699
+
1700
+ // src/plugins/guides/walkthrough-block-builder.ts
1701
+ function buildWalkthroughStepContent(step, stepIndex, totalSteps, doc, callbacks) {
1702
+ const container = doc.createElement("div");
1703
+ container.className = "veo-guide-content";
1704
+ const counter = doc.createElement("div");
1705
+ counter.className = "veo-walkthrough-counter";
1706
+ counter.textContent = `Paso ${stepIndex + 1} de ${totalSteps}`;
1707
+ container.appendChild(counter);
1708
+ const progress = doc.createElement("div");
1709
+ progress.className = "veo-walkthrough-progress";
1710
+ for (let i = 0; i < totalSteps; i++) {
1711
+ const dot = doc.createElement("span");
1712
+ dot.className = "veo-walkthrough-progress-dot";
1713
+ if (i < stepIndex) dot.classList.add("completed");
1714
+ if (i === stepIndex) dot.classList.add("active");
1715
+ progress.appendChild(dot);
1716
+ }
1717
+ container.appendChild(progress);
1718
+ if (typeof step.imageUrl === "string" && step.imageUrl && isSafeUrl(step.imageUrl)) {
1719
+ const img = doc.createElement("img");
1720
+ img.className = "veo-guide-image";
1721
+ img.src = step.imageUrl;
1722
+ img.alt = typeof step.title === "string" ? step.title : "";
1723
+ container.appendChild(img);
1724
+ }
1725
+ if (typeof step.title === "string" && step.title) {
1726
+ const heading = doc.createElement("h2");
1727
+ heading.className = "veo-guide-title";
1728
+ heading.textContent = step.title;
1729
+ container.appendChild(heading);
1730
+ }
1731
+ if (typeof step.content === "string" && step.content) {
1732
+ const paragraph = doc.createElement("p");
1733
+ paragraph.className = "veo-guide-text";
1734
+ paragraph.textContent = step.content;
1735
+ container.appendChild(paragraph);
1736
+ }
1737
+ const actions = doc.createElement("div");
1738
+ actions.className = "veo-walkthrough-actions";
1739
+ const skipBtn = doc.createElement("button");
1740
+ skipBtn.type = "button";
1741
+ skipBtn.className = "veo-walkthrough-skip";
1742
+ skipBtn.textContent = "Omitir";
1743
+ skipBtn.addEventListener("click", () => callbacks.onSkip());
1744
+ actions.appendChild(skipBtn);
1745
+ const rightGroup = doc.createElement("div");
1746
+ rightGroup.className = "veo-walkthrough-actions-right";
1747
+ if (stepIndex > 0) {
1748
+ const backBtn = doc.createElement("button");
1749
+ backBtn.type = "button";
1750
+ backBtn.className = "veo-walkthrough-btn-secondary";
1751
+ backBtn.textContent = "Atr\xE1s";
1752
+ backBtn.addEventListener("click", () => callbacks.onBack());
1753
+ rightGroup.appendChild(backBtn);
1754
+ }
1755
+ const isLastStep = stepIndex === totalSteps - 1;
1756
+ const primaryBtn = doc.createElement("button");
1757
+ primaryBtn.type = "button";
1758
+ primaryBtn.className = "veo-guide-cta";
1759
+ const defaultLabel = isLastStep ? "Finalizar" : "Siguiente";
1760
+ primaryBtn.textContent = typeof step.ctaText === "string" && step.ctaText ? step.ctaText : defaultLabel;
1761
+ primaryBtn.addEventListener("click", () => {
1762
+ if (isLastStep) callbacks.onComplete();
1763
+ else callbacks.onNext();
1764
+ });
1765
+ rightGroup.appendChild(primaryBtn);
1766
+ actions.appendChild(rightGroup);
1767
+ container.appendChild(actions);
1768
+ container.appendChild(createCloseButton(doc, () => callbacks.onSkip()));
1769
+ return container;
1770
+ }
1771
+ var init_walkthrough_block_builder = __esm({
1772
+ "src/plugins/guides/walkthrough-block-builder.ts"() {
1773
+ init_block_builder();
1774
+ }
1775
+ });
1776
+
1202
1777
  // src/plugins/guides/renderers/banner-renderer.ts
1778
+ function bannerClassOf(position, embedded) {
1779
+ return embedded ? "veo-banner veo-banner-embedded" : `veo-banner veo-banner-${position}`;
1780
+ }
1203
1781
  var BannerRenderer;
1204
1782
  var init_banner_renderer = __esm({
1205
1783
  "src/plugins/guides/renderers/banner-renderer.ts"() {
1206
1784
  init_block_builder();
1785
+ init_inline_host();
1786
+ init_wait_for_element();
1207
1787
  init_walkthrough_block_builder();
1208
1788
  init_base_renderer();
1209
1789
  BannerRenderer = class extends BaseRenderer {
1210
- render(context) {
1790
+ async render(context) {
1211
1791
  const idx = context.nav?.stepIndex ?? 0;
1212
1792
  const step = context.guide.guideSteps[idx];
1213
1793
  if (!step) return;
1214
- const { root } = this.createHost();
1794
+ const position = step.style?.position === "bottom" ? "bottom" : "top";
1795
+ const selector = context.nav ? null : step.selector?.trim() ?? null;
1796
+ let anchor = null;
1797
+ if (selector) {
1798
+ anchor = await waitForElement(selector);
1799
+ if (!anchor) return;
1800
+ }
1801
+ const { host, root } = this.createHost();
1215
1802
  this.applyDesign(step.style);
1216
1803
  const ownerDocument = root.ownerDocument ?? document;
1217
- const position = step.style?.position === "bottom" ? "bottom" : "top";
1218
1804
  const banner = ownerDocument.createElement("div");
1219
- banner.className = `veo-banner veo-banner-${position}`;
1220
- const dismiss = (action, ctaUrl) => {
1221
- context.onInteraction({ guideId: context.guide.guideId, stepIndex: idx, action });
1805
+ banner.className = bannerClassOf(position, Boolean(anchor));
1806
+ const dismiss = (action, ctaUrl, meta) => {
1807
+ context.onInteraction({
1808
+ guideId: context.guide.guideId,
1809
+ stepIndex: idx,
1810
+ action,
1811
+ ...meta ? { metadata: meta } : {}
1812
+ });
1222
1813
  if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
1223
1814
  context.onClose();
1224
1815
  };
@@ -1229,68 +1820,46 @@ var init_banner_renderer = __esm({
1229
1820
  ownerDocument,
1230
1821
  context.nav.callbacks
1231
1822
  ) : buildStepContent(step, ownerDocument, {
1232
- onCtaClick: (action, url) => dismiss("cta_clicked", action === "url" ? url : void 0),
1823
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
1233
1824
  onDismiss: () => dismiss("dismissed")
1234
1825
  });
1235
1826
  banner.appendChild(content);
1236
1827
  root.appendChild(banner);
1828
+ if (selector && anchor) {
1829
+ host.style.display = "block";
1830
+ const insertAt = position === "top" ? "prepend" : "append";
1831
+ insertHost(anchor, host, insertAt);
1832
+ this.registerCleanup(keepHostAttached(host, selector, insertAt));
1833
+ }
1237
1834
  if (!context.nav) {
1238
1835
  this.liveContainer = banner;
1239
1836
  this.liveContent = content;
1240
1837
  this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
1241
- onCtaClick: (action, url) => dismiss("cta_clicked", action === "url" ? url : void 0),
1838
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
1242
1839
  onDismiss: () => dismiss("dismissed")
1243
1840
  });
1841
+ this.liveKey = this.liveKeyOf(step);
1244
1842
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
1245
1843
  }
1246
1844
  }
1845
+ /** Cambiar de contenedor o de top/bottom embebido requiere re-insertar → remontar. */
1846
+ liveKeyOf(step) {
1847
+ const selector = step.selector?.trim() ?? "";
1848
+ if (!selector) return "";
1849
+ const position = step.style?.position === "bottom" ? "bottom" : "top";
1850
+ return `${selector}|${position}`;
1851
+ }
1247
1852
  onLiveUpdate(step) {
1248
1853
  if (this.liveContainer) {
1249
1854
  const position = step.style?.position === "bottom" ? "bottom" : "top";
1250
- this.liveContainer.className = `veo-banner veo-banner-${position}`;
1855
+ const embedded = Boolean(step.selector?.trim());
1856
+ this.liveContainer.className = bannerClassOf(position, embedded);
1251
1857
  }
1252
1858
  }
1253
1859
  };
1254
1860
  }
1255
1861
  });
1256
1862
 
1257
- // src/plugins/guides/wait-for-element.ts
1258
- function waitForElement(selector, timeoutMs = DEFAULT_ANCHOR_WAIT_MS) {
1259
- return new Promise((resolve) => {
1260
- const safeQuery = () => {
1261
- try {
1262
- return document.querySelector(selector);
1263
- } catch {
1264
- return null;
1265
- }
1266
- };
1267
- const existing = safeQuery();
1268
- if (existing) {
1269
- resolve(existing);
1270
- return;
1271
- }
1272
- let resolved = false;
1273
- const finish = (el) => {
1274
- if (resolved) return;
1275
- resolved = true;
1276
- observer.disconnect();
1277
- clearTimeout(timer);
1278
- resolve(el);
1279
- };
1280
- const observer = new MutationObserver(() => {
1281
- const el = safeQuery();
1282
- if (el) finish(el);
1283
- });
1284
- observer.observe(document.body, { childList: true, subtree: true });
1285
- const timer = setTimeout(() => finish(null), timeoutMs);
1286
- });
1287
- }
1288
- var init_wait_for_element = __esm({
1289
- "src/plugins/guides/wait-for-element.ts"() {
1290
- init_constants2();
1291
- }
1292
- });
1293
-
1294
1863
  // src/plugins/guides/renderers/custom-frame.ts
1295
1864
  function mountCustomFrame(doc, step, context, opts) {
1296
1865
  const iframe = doc.createElement("iframe");
@@ -1824,40 +2393,6 @@ var init_form_renderer = __esm({
1824
2393
  }
1825
2394
  });
1826
2395
 
1827
- // src/plugins/guides/inline-host.ts
1828
- function readInlinePosition(style) {
1829
- const p = style?.inlinePosition;
1830
- return p === "before" || p === "prepend" || p === "append" ? p : "after";
1831
- }
1832
- function insertHost(anchor, host, position) {
1833
- switch (position) {
1834
- case "before":
1835
- anchor.before(host);
1836
- break;
1837
- case "after":
1838
- anchor.after(host);
1839
- break;
1840
- case "prepend":
1841
- anchor.prepend(host);
1842
- break;
1843
- case "append":
1844
- anchor.append(host);
1845
- break;
1846
- }
1847
- }
1848
- function keepHostAttached(host, selector, position) {
1849
- const id = window.setInterval(() => {
1850
- if (host.isConnected) return;
1851
- const anchor = document.querySelector(selector);
1852
- if (anchor) insertHost(anchor, host, position);
1853
- }, 1e3);
1854
- return () => window.clearInterval(id);
1855
- }
1856
- var init_inline_host = __esm({
1857
- "src/plugins/guides/inline-host.ts"() {
1858
- }
1859
- });
1860
-
1861
2396
  // src/plugins/guides/renderers/inline-custom-renderer.ts
1862
2397
  var InlineCustomRenderer;
1863
2398
  var init_inline_custom_renderer = __esm({
@@ -1950,13 +2485,18 @@ var init_inline_renderer = __esm({
1950
2485
  const ownerDocument = root.ownerDocument ?? document;
1951
2486
  const card = ownerDocument.createElement("div");
1952
2487
  card.className = "veo-inline";
1953
- const dismiss = (action, ctaUrl) => {
1954
- context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action });
2488
+ const dismiss = (action, ctaUrl, meta) => {
2489
+ context.onInteraction({
2490
+ guideId: context.guide.guideId,
2491
+ stepIndex: 0,
2492
+ action,
2493
+ ...meta ? { metadata: meta } : {}
2494
+ });
1955
2495
  if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
1956
2496
  context.onClose();
1957
2497
  };
1958
2498
  const content = buildStepContent(step, ownerDocument, {
1959
- onCtaClick: (action, url) => dismiss("cta_clicked", action === "url" ? url : void 0),
2499
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
1960
2500
  onDismiss: () => dismiss("dismissed")
1961
2501
  });
1962
2502
  card.appendChild(content);
@@ -1965,7 +2505,7 @@ var init_inline_renderer = __esm({
1965
2505
  this.liveContent = content;
1966
2506
  this.liveKey = `${selector}|${position}`;
1967
2507
  this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
1968
- onCtaClick: (action, url) => dismiss("cta_clicked", action === "url" ? url : void 0),
2508
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
1969
2509
  onDismiss: () => dismiss("dismissed")
1970
2510
  });
1971
2511
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
@@ -1978,6 +2518,9 @@ var init_inline_renderer = __esm({
1978
2518
  });
1979
2519
 
1980
2520
  // src/plugins/guides/renderers/modal-renderer.ts
2521
+ function overlayClassOf(step) {
2522
+ return step.style?.backdrop === false ? "veo-modal-overlay veo-modal-overlay--none" : "veo-modal-overlay";
2523
+ }
1981
2524
  var ModalRenderer;
1982
2525
  var init_modal_renderer = __esm({
1983
2526
  "src/plugins/guides/renderers/modal-renderer.ts"() {
@@ -1985,6 +2528,10 @@ var init_modal_renderer = __esm({
1985
2528
  init_walkthrough_block_builder();
1986
2529
  init_base_renderer();
1987
2530
  ModalRenderer = class extends BaseRenderer {
2531
+ constructor() {
2532
+ super(...arguments);
2533
+ this.overlay = null;
2534
+ }
1988
2535
  render(context) {
1989
2536
  const idx = context.nav?.stepIndex ?? 0;
1990
2537
  const step = context.guide.guideSteps[idx];
@@ -1993,11 +2540,17 @@ var init_modal_renderer = __esm({
1993
2540
  this.applyDesign(step.style);
1994
2541
  const ownerDocument = root.ownerDocument ?? document;
1995
2542
  const overlay = ownerDocument.createElement("div");
1996
- overlay.className = "veo-modal-overlay";
2543
+ overlay.className = overlayClassOf(step);
2544
+ this.overlay = overlay;
1997
2545
  const card = ownerDocument.createElement("div");
1998
2546
  card.className = "veo-modal-card";
1999
- const dismiss = (action, ctaUrl) => {
2000
- context.onInteraction({ guideId: context.guide.guideId, stepIndex: idx, action });
2547
+ const dismiss = (action, ctaUrl, meta) => {
2548
+ context.onInteraction({
2549
+ guideId: context.guide.guideId,
2550
+ stepIndex: idx,
2551
+ action,
2552
+ ...meta ? { metadata: meta } : {}
2553
+ });
2001
2554
  if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
2002
2555
  context.onClose();
2003
2556
  };
@@ -2012,7 +2565,7 @@ var init_modal_renderer = __esm({
2012
2565
  ownerDocument,
2013
2566
  context.nav.callbacks
2014
2567
  ) : buildStepContent(step, ownerDocument, {
2015
- onCtaClick: (action, url) => dismiss("cta_clicked", action === "url" ? url : void 0),
2568
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
2016
2569
  onDismiss: () => dismiss("dismissed")
2017
2570
  });
2018
2571
  card.appendChild(content);
@@ -2022,7 +2575,7 @@ var init_modal_renderer = __esm({
2022
2575
  this.liveContainer = card;
2023
2576
  this.liveContent = content;
2024
2577
  this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
2025
- onCtaClick: (action, url) => dismiss("cta_clicked", action === "url" ? url : void 0),
2578
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
2026
2579
  onDismiss: () => dismiss("dismissed")
2027
2580
  });
2028
2581
  }
@@ -2038,28 +2591,13 @@ var init_modal_renderer = __esm({
2038
2591
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
2039
2592
  }
2040
2593
  }
2041
- };
2042
- }
2043
- });
2044
-
2045
- // src/plugins/guides/renderers/floating-arrow.ts
2046
- function positionArrow(arrowEl, placement, data) {
2047
- const side = STATIC_SIDE[placement.split("-")[0] ?? "bottom"] ?? "top";
2048
- for (const prop of ["top", "bottom", "left", "right"]) {
2049
- arrowEl.style.setProperty(prop, "");
2050
- }
2051
- if (data?.x != null) arrowEl.style.setProperty("left", `${data.x}px`);
2052
- if (data?.y != null) arrowEl.style.setProperty("top", `${data.y}px`);
2053
- arrowEl.style.setProperty(side, "-6px");
2054
- }
2055
- var STATIC_SIDE;
2056
- var init_floating_arrow = __esm({
2057
- "src/plugins/guides/renderers/floating-arrow.ts"() {
2058
- STATIC_SIDE = {
2059
- top: "bottom",
2060
- bottom: "top",
2061
- left: "right",
2062
- right: "left"
2594
+ onLiveUpdate(step) {
2595
+ if (this.overlay) this.overlay.className = overlayClassOf(step);
2596
+ }
2597
+ destroy() {
2598
+ this.overlay = null;
2599
+ super.destroy();
2600
+ }
2063
2601
  };
2064
2602
  }
2065
2603
  });
@@ -2091,18 +2629,19 @@ var init_tooltip_renderer = __esm({
2091
2629
  const ownerDocument = root.ownerDocument ?? document;
2092
2630
  const tooltip = ownerDocument.createElement("div");
2093
2631
  tooltip.className = "veo-tooltip";
2094
- const dismiss = (action, ctaUrl) => {
2632
+ const dismiss = (action, ctaUrl, meta) => {
2095
2633
  context.onInteraction({
2096
2634
  guideId: context.guide.guideId,
2097
2635
  stepIndex: 0,
2098
- action
2636
+ action,
2637
+ ...meta ? { metadata: meta } : {}
2099
2638
  });
2100
2639
  if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
2101
2640
  context.onClose();
2102
2641
  };
2103
2642
  const content = buildStepContent(step, ownerDocument, {
2104
- onCtaClick: (action, url) => {
2105
- dismiss("cta_clicked", action === "url" ? url : void 0);
2643
+ onCtaClick: (action, url, meta) => {
2644
+ dismiss("cta_clicked", action === "url" ? url : void 0, meta);
2106
2645
  },
2107
2646
  onDismiss: () => dismiss("dismissed")
2108
2647
  });
@@ -2135,7 +2674,7 @@ var init_tooltip_renderer = __esm({
2135
2674
  this.liveContent = content;
2136
2675
  this.liveKey = selector;
2137
2676
  this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
2138
- onCtaClick: (action, url) => dismiss("cta_clicked", action === "url" ? url : void 0),
2677
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
2139
2678
  onDismiss: () => dismiss("dismissed")
2140
2679
  });
2141
2680
  context.onInteraction({
@@ -2219,7 +2758,7 @@ var init_walkthrough_renderer = __esm({
2219
2758
  function previewGuide(input) {
2220
2759
  if (!hasDocument()) {
2221
2760
  return { close: () => {
2222
- }, ready: Promise.resolve({ rendered: false }) };
2761
+ }, ready: Promise.resolve({ rendered: false }), host: () => null };
2223
2762
  }
2224
2763
  if (!activeController) activeController = new GuidePreviewController();
2225
2764
  return activeController.preview(input);
@@ -2262,6 +2801,8 @@ function createSingleStepRenderer(type) {
2262
2801
  return new FormRenderer();
2263
2802
  case "inline-form":
2264
2803
  return new InlineFormRenderer();
2804
+ case "badge":
2805
+ return new BadgeRenderer();
2265
2806
  case "walkthrough":
2266
2807
  return null;
2267
2808
  }
@@ -2276,7 +2817,9 @@ var PREVIEW_GUIDE_ID, GuidePreviewController, activeController;
2276
2817
  var init_guide_preview = __esm({
2277
2818
  "src/plugins/guides/guide-preview.ts"() {
2278
2819
  init_safe_env();
2820
+ init_badge_renderer();
2279
2821
  init_banner_renderer();
2822
+ init_base_renderer();
2280
2823
  init_custom_renderer();
2281
2824
  init_form_renderer();
2282
2825
  init_inline_custom_renderer();
@@ -2300,14 +2843,26 @@ var init_guide_preview = __esm({
2300
2843
  const nextStep = input.guideSteps[0];
2301
2844
  if (this.singleStep && this.input && input.guideType === this.input.guideType && input.guideType !== "walkthrough" && nextStep && this.singleStep.updateStep(nextStep)) {
2302
2845
  this.input = input;
2303
- return { close: () => this.close(), ready: Promise.resolve({ rendered: true }) };
2846
+ return {
2847
+ close: () => this.close(),
2848
+ ready: Promise.resolve({ rendered: true }),
2849
+ host: () => this.activeHost()
2850
+ };
2304
2851
  }
2305
2852
  this.close();
2306
2853
  this.input = input;
2307
2854
  const guide = normalize(input);
2308
2855
  const start = typeof input.startStepIndex === "number" ? input.startStepIndex : 0;
2309
2856
  const ready = guide.guideType === "walkthrough" ? this.startWalkthrough(guide, start) : this.renderSingleStep(guide);
2310
- return { close: () => this.close(), ready };
2857
+ return { close: () => this.close(), ready, host: () => this.activeHost() };
2858
+ }
2859
+ /** Host de la guía montada (single-step o paso de walkthrough activo). */
2860
+ activeHost() {
2861
+ if (this.singleStep) return this.singleStep.hostElement();
2862
+ if (this.walkthrough instanceof BaseRenderer) {
2863
+ return this.walkthrough.hostElement();
2864
+ }
2865
+ return null;
2311
2866
  }
2312
2867
  close() {
2313
2868
  if (this.singleStep) {
@@ -2522,8 +3077,8 @@ function injectBuilderPanel(opts) {
2522
3077
  const ot = top;
2523
3078
  iframe.style.pointerEvents = "none";
2524
3079
  const move = (ev) => {
2525
- left = clamp2(ol + (ev.clientX - sx), 0, Math.max(0, window.innerWidth - card.offsetWidth));
2526
- top = clamp2(ot + (ev.clientY - sy), 0, Math.max(0, window.innerHeight - 40));
3080
+ left = clamp3(ol + (ev.clientX - sx), 0, Math.max(0, window.innerWidth - card.offsetWidth));
3081
+ top = clamp3(ot + (ev.clientY - sy), 0, Math.max(0, window.innerHeight - 40));
2527
3082
  card.style.left = `${left}px`;
2528
3083
  card.style.top = `${top}px`;
2529
3084
  };
@@ -2544,8 +3099,8 @@ function injectBuilderPanel(opts) {
2544
3099
  const sh = card.offsetHeight;
2545
3100
  iframe.style.pointerEvents = "none";
2546
3101
  const move = (ev) => {
2547
- card.style.width = `${clamp2(sw + (ev.clientX - sx), MIN_W, window.innerWidth)}px`;
2548
- card.style.height = `${clamp2(sh + (ev.clientY - sy), MIN_H, window.innerHeight)}px`;
3102
+ card.style.width = `${clamp3(sw + (ev.clientX - sx), MIN_W, window.innerWidth)}px`;
3103
+ card.style.height = `${clamp3(sh + (ev.clientY - sy), MIN_H, window.innerHeight)}px`;
2549
3104
  };
2550
3105
  const up = () => {
2551
3106
  window.removeEventListener("mousemove", move);
@@ -2564,7 +3119,7 @@ function injectBuilderPanel(opts) {
2564
3119
  }
2565
3120
  };
2566
3121
  }
2567
- function clamp2(v, min, max) {
3122
+ function clamp3(v, min, max) {
2568
3123
  return Math.min(Math.max(v, min), max);
2569
3124
  }
2570
3125
  function buildPanelUrl(opts) {
@@ -2628,6 +3183,242 @@ var init_builder_panel = __esm({
2628
3183
  }
2629
3184
  });
2630
3185
 
3186
+ // src/plugins/builder/design-manipulator.ts
3187
+ function clamp4(v, min, max) {
3188
+ return Math.min(Math.max(v, min), max);
3189
+ }
3190
+ function round3(v) {
3191
+ return Math.round(v * 1e3) / 1e3;
3192
+ }
3193
+ function isInteractive(target) {
3194
+ return Boolean(
3195
+ target instanceof Element && target.closest("button, a, input, textarea, select, label")
3196
+ );
3197
+ }
3198
+ function attachDesignManipulator(opts) {
3199
+ const shadow = opts.host.shadowRoot;
3200
+ if (!shadow) return null;
3201
+ const card = shadow.querySelector(".veo-modal-card");
3202
+ if (card) return attachModal(opts, shadow, card);
3203
+ const banner = shadow.querySelector(".veo-banner");
3204
+ if (banner && !banner.classList.contains("veo-banner-embedded")) {
3205
+ return attachBanner(opts, shadow, banner);
3206
+ }
3207
+ return null;
3208
+ }
3209
+ function attachModal(opts, shadow, card) {
3210
+ if (card.dataset.veoManipulated === "1") return null;
3211
+ card.dataset.veoManipulated = "1";
3212
+ card.classList.add("veo-mnp-card");
3213
+ const doc = card.ownerDocument;
3214
+ const style = doc.createElement("style");
3215
+ style.textContent = MANIPULATOR_CSS;
3216
+ shadow.appendChild(style);
3217
+ const handles = [];
3218
+ for (const dir of RESIZE_DIRS) {
3219
+ const h = doc.createElement("div");
3220
+ h.className = `veo-mnp-handle veo-mnp-${dir}`;
3221
+ h.dataset.veoDir = dir;
3222
+ card.appendChild(h);
3223
+ handles.push(h);
3224
+ }
3225
+ let dragging = false;
3226
+ let detached = false;
3227
+ let patch = {};
3228
+ const setVars = (v) => {
3229
+ const host = opts.host;
3230
+ if (v.px !== void 0) host.style.setProperty("--veo-pos-x", `${v.px * 100}%`);
3231
+ if (v.py !== void 0) host.style.setProperty("--veo-pos-y", `${v.py * 100}%`);
3232
+ if (v.w !== void 0) host.style.setProperty("--veo-width", `${v.w}px`);
3233
+ if (v.h !== void 0) host.style.setProperty("--veo-min-h", `${v.h}px`);
3234
+ };
3235
+ const startGesture = (e, apply) => {
3236
+ e.preventDefault();
3237
+ e.stopPropagation();
3238
+ dragging = true;
3239
+ patch = {};
3240
+ const start = card.getBoundingClientRect();
3241
+ const sx = e.clientX;
3242
+ const sy = e.clientY;
3243
+ let raf = 0;
3244
+ let lastEv = null;
3245
+ const flush = () => {
3246
+ raf = 0;
3247
+ if (!lastEv) return;
3248
+ const next = apply(lastEv.clientX - sx, lastEv.clientY - sy, start);
3249
+ patch = { ...patch, ...next };
3250
+ setVars({
3251
+ ...next.posX !== void 0 ? { px: next.posX } : {},
3252
+ ...next.posY !== void 0 ? { py: next.posY } : {},
3253
+ ...next.width !== void 0 ? { w: next.width } : {},
3254
+ ...next.height !== void 0 ? { h: next.height } : {}
3255
+ });
3256
+ };
3257
+ const move = (ev) => {
3258
+ lastEv = ev;
3259
+ if (typeof window.requestAnimationFrame !== "function") flush();
3260
+ else if (!raf) raf = window.requestAnimationFrame(flush);
3261
+ };
3262
+ const up = () => {
3263
+ window.removeEventListener("mousemove", move);
3264
+ window.removeEventListener("mouseup", up);
3265
+ if (raf) window.cancelAnimationFrame(raf);
3266
+ flush();
3267
+ dragging = false;
3268
+ if (Object.keys(patch).length > 0 && !detached) {
3269
+ opts.onCommit({ stepIndex: opts.stepIndex, style: patch });
3270
+ }
3271
+ opts.onGestureEnd?.();
3272
+ };
3273
+ window.addEventListener("mousemove", move);
3274
+ window.addEventListener("mouseup", up);
3275
+ };
3276
+ const posXFor = (left, w) => {
3277
+ const span = window.innerWidth - w;
3278
+ return span <= 0 ? 0.5 : round3(clamp4(left / span, 0, 1));
3279
+ };
3280
+ const posYFor = (top, h) => {
3281
+ const span = window.innerHeight - h;
3282
+ return span <= 0 ? 0.5 : round3(clamp4(top / span, 0, 1));
3283
+ };
3284
+ const onCardDown = (e) => {
3285
+ if (isInteractive(e.target)) return;
3286
+ if (e.target instanceof Element && e.target.closest(".veo-mnp-handle")) return;
3287
+ startGesture(e, (dx, dy, start) => ({
3288
+ posX: posXFor(start.left + dx, start.width),
3289
+ posY: posYFor(start.top + dy, start.height)
3290
+ }));
3291
+ };
3292
+ const onHandleDown = (e) => {
3293
+ const dir = e.currentTarget.dataset.veoDir;
3294
+ startGesture(e, (dx, dy, start) => {
3295
+ const out = {};
3296
+ if (dir.includes("e")) out.width = Math.round(clamp4(start.width + dx, MIN_W2, MAX_W));
3297
+ if (dir.includes("w")) {
3298
+ out.width = Math.round(clamp4(start.width - dx, MIN_W2, MAX_W));
3299
+ out.posX = posXFor(start.right - out.width, out.width);
3300
+ }
3301
+ if (dir.includes("s")) out.height = Math.round(clamp4(start.height + dy, MIN_H2, MAX_H));
3302
+ if (dir.includes("n")) {
3303
+ out.height = Math.round(clamp4(start.height - dy, MIN_H2, MAX_H));
3304
+ out.posY = posYFor(start.bottom - out.height, out.height);
3305
+ }
3306
+ return out;
3307
+ });
3308
+ };
3309
+ card.addEventListener("mousedown", onCardDown);
3310
+ for (const h of handles) h.addEventListener("mousedown", onHandleDown);
3311
+ return {
3312
+ isDragging: () => dragging,
3313
+ detach: () => {
3314
+ if (detached) return;
3315
+ detached = true;
3316
+ card.removeEventListener("mousedown", onCardDown);
3317
+ for (const h of handles) h.remove();
3318
+ style.remove();
3319
+ card.classList.remove("veo-mnp-card");
3320
+ delete card.dataset.veoManipulated;
3321
+ }
3322
+ };
3323
+ }
3324
+ function attachBanner(opts, shadow, banner) {
3325
+ if (banner.dataset.veoManipulated === "1") return null;
3326
+ banner.dataset.veoManipulated = "1";
3327
+ banner.classList.add("veo-mnp-banner");
3328
+ const doc = banner.ownerDocument;
3329
+ const style = doc.createElement("style");
3330
+ style.textContent = MANIPULATOR_CSS;
3331
+ shadow.appendChild(style);
3332
+ let dragging = false;
3333
+ let detached = false;
3334
+ const positionOf = () => banner.classList.contains("veo-banner-bottom") ? "bottom" : "top";
3335
+ const onDown = (e) => {
3336
+ if (isInteractive(e.target)) return;
3337
+ e.preventDefault();
3338
+ dragging = true;
3339
+ const startPos = positionOf();
3340
+ let current = startPos;
3341
+ const move = (ev) => {
3342
+ const next = ev.clientY < window.innerHeight / 2 ? "top" : "bottom";
3343
+ if (next !== current) {
3344
+ current = next;
3345
+ banner.classList.remove("veo-banner-top", "veo-banner-bottom");
3346
+ banner.classList.add(`veo-banner-${next}`);
3347
+ }
3348
+ };
3349
+ const up = () => {
3350
+ window.removeEventListener("mousemove", move);
3351
+ window.removeEventListener("mouseup", up);
3352
+ dragging = false;
3353
+ if (current !== startPos && !detached) {
3354
+ opts.onCommit({ stepIndex: opts.stepIndex, style: { position: current } });
3355
+ }
3356
+ opts.onGestureEnd?.();
3357
+ };
3358
+ window.addEventListener("mousemove", move);
3359
+ window.addEventListener("mouseup", up);
3360
+ };
3361
+ banner.addEventListener("mousedown", onDown);
3362
+ return {
3363
+ isDragging: () => dragging,
3364
+ detach: () => {
3365
+ if (detached) return;
3366
+ detached = true;
3367
+ banner.removeEventListener("mousedown", onDown);
3368
+ style.remove();
3369
+ banner.classList.remove("veo-mnp-banner");
3370
+ delete banner.dataset.veoManipulated;
3371
+ }
3372
+ };
3373
+ }
3374
+ var MIN_W2, MAX_W, MIN_H2, MAX_H, EDGE, RESIZE_DIRS, MANIPULATOR_CSS;
3375
+ var init_design_manipulator = __esm({
3376
+ "src/plugins/builder/design-manipulator.ts"() {
3377
+ MIN_W2 = 220;
3378
+ MAX_W = 720;
3379
+ MIN_H2 = 120;
3380
+ MAX_H = 900;
3381
+ EDGE = 8;
3382
+ RESIZE_DIRS = ["n", "s", "e", "w", "ne", "nw", "se", "sw"];
3383
+ MANIPULATOR_CSS = `
3384
+ .veo-mnp-card {
3385
+ outline: 1.5px dashed rgba(255, 91, 53, 0.75);
3386
+ outline-offset: 2px;
3387
+ }
3388
+ .veo-mnp-card:hover { cursor: move; }
3389
+ .veo-mnp-handle {
3390
+ position: absolute;
3391
+ z-index: 10;
3392
+ background: transparent;
3393
+ }
3394
+ .veo-mnp-handle::after {
3395
+ content: '';
3396
+ position: absolute;
3397
+ width: 8px; height: 8px;
3398
+ background: #fff;
3399
+ border: 1.5px solid rgba(255, 91, 53, 0.9);
3400
+ border-radius: 2px;
3401
+ top: 50%; left: 50%;
3402
+ transform: translate(-50%, -50%);
3403
+ opacity: 0;
3404
+ transition: opacity 100ms ease;
3405
+ }
3406
+ .veo-mnp-card:hover .veo-mnp-handle::after,
3407
+ .veo-mnp-handle:hover::after { opacity: 1; }
3408
+ .veo-mnp-n { top: -${EDGE / 2}px; left: ${EDGE}px; right: ${EDGE}px; height: ${EDGE}px; cursor: ns-resize; }
3409
+ .veo-mnp-s { bottom: -${EDGE / 2}px; left: ${EDGE}px; right: ${EDGE}px; height: ${EDGE}px; cursor: ns-resize; }
3410
+ .veo-mnp-e { right: -${EDGE / 2}px; top: ${EDGE}px; bottom: ${EDGE}px; width: ${EDGE}px; cursor: ew-resize; }
3411
+ .veo-mnp-w { left: -${EDGE / 2}px; top: ${EDGE}px; bottom: ${EDGE}px; width: ${EDGE}px; cursor: ew-resize; }
3412
+ .veo-mnp-ne { top: -${EDGE / 2}px; right: -${EDGE / 2}px; width: ${EDGE * 1.5}px; height: ${EDGE * 1.5}px; cursor: nesw-resize; }
3413
+ .veo-mnp-nw { top: -${EDGE / 2}px; left: -${EDGE / 2}px; width: ${EDGE * 1.5}px; height: ${EDGE * 1.5}px; cursor: nwse-resize; }
3414
+ .veo-mnp-se { bottom: -${EDGE / 2}px; right: -${EDGE / 2}px; width: ${EDGE * 1.5}px; height: ${EDGE * 1.5}px; cursor: nwse-resize; }
3415
+ .veo-mnp-sw { bottom: -${EDGE / 2}px; left: -${EDGE / 2}px; width: ${EDGE * 1.5}px; height: ${EDGE * 1.5}px; cursor: nesw-resize; }
3416
+ .veo-mnp-banner { outline: 1.5px dashed rgba(255, 91, 53, 0.75); outline-offset: -2px; cursor: grab; }
3417
+ .veo-mnp-banner:active { cursor: grabbing; }
3418
+ `;
3419
+ }
3420
+ });
3421
+
2631
3422
  // src/plugins/builder/element-picker.ts
2632
3423
  function startElementPicker(options) {
2633
3424
  if (!hasDocument()) {
@@ -2841,10 +3632,35 @@ function initBuilderMode() {
2841
3632
  let picker = null;
2842
3633
  let panel = null;
2843
3634
  let staticTarget = null;
3635
+ let manipulator = null;
3636
+ let pendingPreview = null;
2844
3637
  const getTarget = () => panel ? panel.target() : staticTarget;
2845
3638
  const post = (event) => {
2846
3639
  getTarget()?.postMessage({ source: VEO_BUILDER_SOURCE, token, ...event }, dashboardOrigin);
2847
3640
  };
3641
+ const applyPreview = (cmd) => {
3642
+ manipulator?.detach();
3643
+ manipulator = null;
3644
+ const handle = previewGuide(cmd.guide);
3645
+ if (!cmd.editable) return;
3646
+ void handle.ready.then((result) => {
3647
+ if (!result.rendered) return;
3648
+ const host = handle.host();
3649
+ if (!host) return;
3650
+ manipulator = attachDesignManipulator({
3651
+ host,
3652
+ guideType: cmd.guide.guideType,
3653
+ stepIndex: cmd.guide.startStepIndex ?? 0,
3654
+ onCommit: (payload) => post({ type: "design-updated", payload }),
3655
+ onGestureEnd: () => {
3656
+ if (!pendingPreview) return;
3657
+ const queued = pendingPreview;
3658
+ pendingPreview = null;
3659
+ applyPreview(queued);
3660
+ }
3661
+ });
3662
+ });
3663
+ };
2848
3664
  const handleCommand = (cmd) => {
2849
3665
  switch (cmd.type) {
2850
3666
  case "panel-ready":
@@ -2870,9 +3686,14 @@ function initBuilderMode() {
2870
3686
  picker = null;
2871
3687
  break;
2872
3688
  case "preview":
2873
- if (cmd.guide) void previewGuide(cmd.guide).ready;
3689
+ if (!cmd.guide) break;
3690
+ if (manipulator?.isDragging()) pendingPreview = cmd;
3691
+ else applyPreview(cmd);
2874
3692
  break;
2875
3693
  case "close-preview":
3694
+ manipulator?.detach();
3695
+ manipulator = null;
3696
+ pendingPreview = null;
2876
3697
  closeGuidePreview();
2877
3698
  break;
2878
3699
  case "teardown":
@@ -2892,6 +3713,9 @@ function initBuilderMode() {
2892
3713
  window.removeEventListener("pagehide", onPageHide);
2893
3714
  picker?.stop();
2894
3715
  picker = null;
3716
+ manipulator?.detach();
3717
+ manipulator = null;
3718
+ pendingPreview = null;
2895
3719
  closeGuidePreview();
2896
3720
  post({ type: "closed" });
2897
3721
  panel?.teardown();
@@ -2971,6 +3795,7 @@ var init_builder_mode = __esm({
2971
3795
  init_guide_preview();
2972
3796
  init_bridge_protocol();
2973
3797
  init_builder_panel();
3798
+ init_design_manipulator();
2974
3799
  init_element_picker();
2975
3800
  }
2976
3801
  });
@@ -3104,6 +3929,7 @@ var init_builder_session = __esm({
3104
3929
  // src/plugins/builder/index.ts
3105
3930
  var builder_exports = {};
3106
3931
  __export(builder_exports, {
3932
+ attachDesignManipulator: () => attachDesignManipulator,
3107
3933
  createBuilderSession: () => createBuilderSession,
3108
3934
  initBuilderMode: () => initBuilderMode,
3109
3935
  startElementPicker: () => startElementPicker,
@@ -3113,6 +3939,7 @@ var init_builder = __esm({
3113
3939
  "src/plugins/builder/index.ts"() {
3114
3940
  init_builder_mode();
3115
3941
  init_builder_session();
3942
+ init_design_manipulator();
3116
3943
  init_element_picker();
3117
3944
  }
3118
3945
  });
@@ -3379,8 +4206,8 @@ var DomListener = class {
3379
4206
  let depth = 0;
3380
4207
  while (current && current !== document.documentElement && depth < 5) {
3381
4208
  const tag = current.tagName.toLowerCase();
3382
- const isInteractive = tag === "button" || tag === "a" || tag === "input" || tag === "select" || tag === "textarea" || current.hasAttribute("data-veo-tag") || current.hasAttribute("role") || current.hasAttribute("onclick") || current instanceof HTMLElement && current.style.cursor === "pointer";
3383
- if (isInteractive) return current;
4209
+ const isInteractive2 = tag === "button" || tag === "a" || tag === "input" || tag === "select" || tag === "textarea" || current.hasAttribute("data-veo-tag") || current.hasAttribute("role") || current.hasAttribute("onclick") || current instanceof HTMLElement && current.style.cursor === "pointer";
4210
+ if (isInteractive2) return current;
3384
4211
  current = current.parentElement;
3385
4212
  depth++;
3386
4213
  }
@@ -3502,7 +4329,8 @@ var Queue = class {
3502
4329
  return;
3503
4330
  }
3504
4331
  this.retryCount++;
3505
- const delayMs = Math.min(1e3 * 2 ** (this.retryCount - 1), RETRY_MAX_DELAY_MS);
4332
+ const base = Math.min(1e3 * 2 ** (this.retryCount - 1), RETRY_MAX_DELAY_MS);
4333
+ const delayMs = base / 2 + Math.random() * (base / 2);
3506
4334
  if (this.retryTimer) clearTimeout(this.retryTimer);
3507
4335
  this.retryTimer = setTimeout(() => {
3508
4336
  void this.flush();
@@ -4181,6 +5009,7 @@ function toWire(payload) {
4181
5009
  if (typeof payload.stepIndex === "number") wire.stepPosition = payload.stepIndex;
4182
5010
  if (payload.pageUrl) wire.pageUrl = payload.pageUrl;
4183
5011
  if (payload.pagePath) wire.pagePath = payload.pagePath;
5012
+ if (payload.metadata) wire.metadata = payload.metadata;
4184
5013
  return wire;
4185
5014
  }
4186
5015
  var GuideTrackerClient = class {
@@ -4228,6 +5057,8 @@ var GuideTrackerClient = class {
4228
5057
  };
4229
5058
 
4230
5059
  // src/plugins/guides/guide-tracker-queue.ts
5060
+ var RETRY_BASE_MS = 3e3;
5061
+ var RETRY_MAX_DELAY_MS2 = 3e4;
4231
5062
  var GuideTrackerQueue = class {
4232
5063
  constructor(config) {
4233
5064
  this.config = config;
@@ -4264,20 +5095,23 @@ var GuideTrackerQueue = class {
4264
5095
  */
4265
5096
  async flush() {
4266
5097
  if (this.flushing || this.buffer.length === 0) return;
5098
+ const now = Date.now();
5099
+ const due = this.buffer.filter((item) => (item.nextRetryAt ?? 0) <= now);
5100
+ if (due.length === 0) return;
4267
5101
  this.flushing = true;
4268
- const batch = this.buffer.splice(0, this.buffer.length);
5102
+ const dueSet = new Set(due);
5103
+ this.buffer = this.buffer.filter((item) => !dueSet.has(item));
4269
5104
  const results = await Promise.allSettled(
4270
- batch.map(
4271
- (item) => this.config.client.send({ guideId: item.guideId, payload: item.payload })
4272
- )
5105
+ due.map((item) => this.config.client.send({ guideId: item.guideId, payload: item.payload }))
4273
5106
  );
4274
5107
  for (let i = 0; i < results.length; i++) {
4275
5108
  const result = results[i];
4276
- const item = batch[i];
5109
+ const item = due[i];
4277
5110
  if (!result || !item) continue;
4278
5111
  if (result.status === "fulfilled") continue;
4279
5112
  item.attempts += 1;
4280
5113
  if (item.attempts < this.config.maxRetries) {
5114
+ item.nextRetryAt = Date.now() + this.retryDelayMs(item.attempts);
4281
5115
  this.buffer.push(item);
4282
5116
  } else {
4283
5117
  this.safeOnError(result.reason, item);
@@ -4285,6 +5119,15 @@ var GuideTrackerQueue = class {
4285
5119
  }
4286
5120
  this.flushing = false;
4287
5121
  }
5122
+ /**
5123
+ * Backoff exponencial (base = intervalo de flush) con equal-jitter: mitad
5124
+ * fija + mitad aleatoria. Desincroniza los reintentos entre clientes ante
5125
+ * una caída del backend.
5126
+ */
5127
+ retryDelayMs(attempts) {
5128
+ const base = Math.min(RETRY_BASE_MS * 2 ** (attempts - 1), RETRY_MAX_DELAY_MS2);
5129
+ return base / 2 + Math.random() * (base / 2);
5130
+ }
4288
5131
  /**
4289
5132
  * Drena el buffer vía `sendBeacon`. Síncrono, sin retry. Para pagehide.
4290
5133
  */
@@ -4331,6 +5174,7 @@ var GuideTrackerQueue = class {
4331
5174
  };
4332
5175
 
4333
5176
  // src/plugins/guides/guides-controller.ts
5177
+ init_badge_renderer();
4334
5178
  init_banner_renderer();
4335
5179
  init_custom_renderer();
4336
5180
  init_form_renderer();
@@ -4458,6 +5302,7 @@ var WalkthroughStateManager = class {
4458
5302
  };
4459
5303
 
4460
5304
  // src/plugins/guides/guides-controller.ts
5305
+ var RESOLVE_CACHE_TTL_MS = 3e4;
4461
5306
  var STATUS_BY_ACTION = {
4462
5307
  shown: "shown",
4463
5308
  dismissed: "dismissed",
@@ -4484,6 +5329,10 @@ var GuidesController = class {
4484
5329
  */
4485
5330
  this.activeWalkthroughRenderer = null;
4486
5331
  this.activeWalkthroughGuide = null;
5332
+ /** Última resolución cacheada (mismo `(endUserId, url)` → sin red). */
5333
+ this.resolveCache = null;
5334
+ /** Resolución idéntica en vuelo, para coalescer llamadas concurrentes. */
5335
+ this.inflightResolve = null;
4487
5336
  this.debug = config.debug === true;
4488
5337
  this.apiUrl = config.apiUrl;
4489
5338
  this.apiKey = config.apiKey;
@@ -4535,7 +5384,7 @@ var GuidesController = class {
4535
5384
  }
4536
5385
  let guides;
4537
5386
  try {
4538
- guides = await this.resolver.resolve(endUserId, pageUrl);
5387
+ guides = await this.resolveGuides(endUserId, pageUrl);
4539
5388
  } catch (err) {
4540
5389
  if (this.debug) console.error("[veo] guides resolver threw:", err);
4541
5390
  return;
@@ -4562,6 +5411,36 @@ var GuidesController = class {
4562
5411
  this.runSingleStepRender(guide, sessionId, pageUrl);
4563
5412
  }
4564
5413
  }
5414
+ /**
5415
+ * `resolve` con cache de corta duración + coalescing:
5416
+ * - Mismo `(endUserId, url)` resuelto hace < {@link RESOLVE_CACHE_TTL_MS} →
5417
+ * se reusa sin red. Colapsa el doble disparo identify+pageview y los
5418
+ * pageviews repetidos de una SPA hacia la misma URL.
5419
+ * - Si ya hay un resolve idéntico en vuelo, se espera ESE (no se abre otro).
5420
+ * El re-render sigue siendo idempotente (dedupe por `activeByGuideId` +
5421
+ * `frequencyCache`), así que reusar el resultado no duplica guías.
5422
+ */
5423
+ async resolveGuides(endUserId, pageUrl) {
5424
+ const key = `${endUserId}
5425
+ ${pageUrl}`;
5426
+ const now = Date.now();
5427
+ if (this.resolveCache?.key === key && now - this.resolveCache.at < RESOLVE_CACHE_TTL_MS) {
5428
+ if (this.debug) console.log("[veo] guides: resolve cache hit");
5429
+ return this.resolveCache.guides;
5430
+ }
5431
+ if (this.inflightResolve?.key === key) {
5432
+ return this.inflightResolve.promise;
5433
+ }
5434
+ const promise = this.resolver.resolve(endUserId, pageUrl);
5435
+ this.inflightResolve = { key, promise };
5436
+ try {
5437
+ const guides = await promise;
5438
+ this.resolveCache = { key, at: Date.now(), guides };
5439
+ return guides;
5440
+ } finally {
5441
+ if (this.inflightResolve?.key === key) this.inflightResolve = null;
5442
+ }
5443
+ }
4565
5444
  /** Cierra todas las guías activas, limpia cache de dispatch y para timers. */
4566
5445
  destroy() {
4567
5446
  for (const renderer of this.activeRenderers) {
@@ -4582,6 +5461,8 @@ var GuidesController = class {
4582
5461
  this.activeWalkthroughGuide = null;
4583
5462
  }
4584
5463
  this.dispatched.clear();
5464
+ this.resolveCache = null;
5465
+ this.inflightResolve = null;
4585
5466
  this.trackerQueue.destroy();
4586
5467
  }
4587
5468
  runSingleStepRender(guide, sessionId, pageUrl) {
@@ -4806,7 +5687,8 @@ var GuidesController = class {
4806
5687
  * por si el usuario se identificó después de que la guía apareció.
4807
5688
  */
4808
5689
  handleInteraction(guide, sessionId, pageUrl, event) {
4809
- const dedupKey = `${guide.guideId}:${event.action}`;
5690
+ const buttonKey = event.metadata?.buttonId ? `:${event.metadata.buttonId}` : "";
5691
+ const dedupKey = `${guide.guideId}:${event.action}${buttonKey}`;
4810
5692
  if (this.dispatched.has(dedupKey)) {
4811
5693
  if (this.debug) console.log(`[veo] guides: dedup hit ${dedupKey}`);
4812
5694
  return;
@@ -4828,7 +5710,8 @@ var GuidesController = class {
4828
5710
  action: event.action,
4829
5711
  stepIndex: event.stepIndex,
4830
5712
  pageUrl,
4831
- pagePath: extractPath(pageUrl)
5713
+ pagePath: extractPath(pageUrl),
5714
+ ...event.metadata ? { metadata: event.metadata } : {}
4832
5715
  }
4833
5716
  });
4834
5717
  }
@@ -4879,6 +5762,8 @@ var GuidesController = class {
4879
5762
  return new FormRenderer();
4880
5763
  case "inline-form":
4881
5764
  return new InlineFormRenderer();
5765
+ case "badge":
5766
+ return new BadgeRenderer();
4882
5767
  case "walkthrough":
4883
5768
  return null;
4884
5769
  }