veo-sdk 0.3.16 → 0.4.0

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