veo-sdk 0.3.15 → 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};
@@ -1114,6 +1317,37 @@ var init_base_renderer = __esm({
1114
1317
  this.host = null;
1115
1318
  this.shadow = null;
1116
1319
  this.cleanups = [];
1320
+ // ── Update in-place (preview en vivo del editor) ──
1321
+ // La subclase setea estas piezas en render() para habilitarlo: el contenedor
1322
+ // del contenido, el nodo de contenido actual y cómo reconstruirlo.
1323
+ this.liveContainer = null;
1324
+ this.liveContent = null;
1325
+ this.liveBuild = null;
1326
+ /** Clave de anclaje/estructura fijada en render(); si cambia → remontar. */
1327
+ this.liveKey = "";
1328
+ }
1329
+ /** Clave de anclaje derivada del step (override por subclase). */
1330
+ liveKeyOf(_step) {
1331
+ return "";
1332
+ }
1333
+ /** Hook post-swap (reposicionar tooltip, actualizar clase del banner…). */
1334
+ onLiveUpdate(_step) {
1335
+ }
1336
+ /**
1337
+ * Actualiza el contenido del step SIN remontar el host — así el preview del
1338
+ * editor no parpadea en cada tecla. Devuelve `false` cuando el renderer no
1339
+ * lo soporta o el cambio requiere remontar (cambió el ancla/estructura);
1340
+ * en ese caso el caller hace el re-render completo.
1341
+ */
1342
+ updateStep(step) {
1343
+ if (!this.host || !this.liveContainer || !this.liveContent || !this.liveBuild) return false;
1344
+ if (this.liveKeyOf(step) !== this.liveKey) return false;
1345
+ const next = this.liveBuild(step);
1346
+ this.liveContainer.replaceChild(next, this.liveContent);
1347
+ this.liveContent = next;
1348
+ this.applyDesign(step.style);
1349
+ this.onLiveUpdate(step);
1350
+ return true;
1117
1351
  }
1118
1352
  /**
1119
1353
  * Crea un `<div data-veo-guide>` adjunto a `document.body`, le adjunta
@@ -1149,6 +1383,14 @@ var init_base_renderer = __esm({
1149
1383
  registerCleanup(fn) {
1150
1384
  this.cleanups.push(fn);
1151
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
+ }
1152
1394
  /** Remueve el host del DOM y corre todas las funciones de cleanup. */
1153
1395
  destroy() {
1154
1396
  for (const fn of this.cleanups) {
@@ -1168,26 +1410,402 @@ var init_base_renderer = __esm({
1168
1410
  }
1169
1411
  });
1170
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
+
1171
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
+ }
1172
1777
  var BannerRenderer;
1173
1778
  var init_banner_renderer = __esm({
1174
1779
  "src/plugins/guides/renderers/banner-renderer.ts"() {
1175
1780
  init_block_builder();
1781
+ init_inline_host();
1782
+ init_wait_for_element();
1176
1783
  init_walkthrough_block_builder();
1177
1784
  init_base_renderer();
1178
1785
  BannerRenderer = class extends BaseRenderer {
1179
- render(context) {
1786
+ async render(context) {
1180
1787
  const idx = context.nav?.stepIndex ?? 0;
1181
1788
  const step = context.guide.guideSteps[idx];
1182
1789
  if (!step) return;
1183
- 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();
1184
1798
  this.applyDesign(step.style);
1185
1799
  const ownerDocument = root.ownerDocument ?? document;
1186
- const position = step.style?.position === "bottom" ? "bottom" : "top";
1187
1800
  const banner = ownerDocument.createElement("div");
1188
- banner.className = `veo-banner veo-banner-${position}`;
1189
- const dismiss = (action, ctaUrl) => {
1190
- 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
+ });
1191
1809
  if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
1192
1810
  context.onClose();
1193
1811
  };
@@ -1198,53 +1816,43 @@ var init_banner_renderer = __esm({
1198
1816
  ownerDocument,
1199
1817
  context.nav.callbacks
1200
1818
  ) : buildStepContent(step, ownerDocument, {
1201
- 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),
1202
1820
  onDismiss: () => dismiss("dismissed")
1203
1821
  });
1204
1822
  banner.appendChild(content);
1205
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
+ }
1206
1830
  if (!context.nav) {
1831
+ this.liveContainer = banner;
1832
+ this.liveContent = content;
1833
+ this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
1834
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
1835
+ onDismiss: () => dismiss("dismissed")
1836
+ });
1837
+ this.liveKey = this.liveKeyOf(step);
1207
1838
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
1208
1839
  }
1209
1840
  }
1210
- };
1211
- }
1212
- });
1213
-
1214
- // src/plugins/guides/wait-for-element.ts
1215
- function waitForElement(selector, timeoutMs = DEFAULT_ANCHOR_WAIT_MS) {
1216
- return new Promise((resolve) => {
1217
- const safeQuery = () => {
1218
- try {
1219
- return document.querySelector(selector);
1220
- } catch {
1221
- return null;
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
+ }
1848
+ onLiveUpdate(step) {
1849
+ if (this.liveContainer) {
1850
+ const position = step.style?.position === "bottom" ? "bottom" : "top";
1851
+ const embedded = Boolean(step.selector?.trim());
1852
+ this.liveContainer.className = bannerClassOf(position, embedded);
1853
+ }
1222
1854
  }
1223
1855
  };
1224
- const existing = safeQuery();
1225
- if (existing) {
1226
- resolve(existing);
1227
- return;
1228
- }
1229
- let resolved = false;
1230
- const finish = (el) => {
1231
- if (resolved) return;
1232
- resolved = true;
1233
- observer.disconnect();
1234
- clearTimeout(timer);
1235
- resolve(el);
1236
- };
1237
- const observer = new MutationObserver(() => {
1238
- const el = safeQuery();
1239
- if (el) finish(el);
1240
- });
1241
- observer.observe(document.body, { childList: true, subtree: true });
1242
- const timer = setTimeout(() => finish(null), timeoutMs);
1243
- });
1244
- }
1245
- var init_wait_for_element = __esm({
1246
- "src/plugins/guides/wait-for-element.ts"() {
1247
- init_constants2();
1248
1856
  }
1249
1857
  });
1250
1858
 
@@ -1781,40 +2389,6 @@ var init_form_renderer = __esm({
1781
2389
  }
1782
2390
  });
1783
2391
 
1784
- // src/plugins/guides/inline-host.ts
1785
- function readInlinePosition(style) {
1786
- const p = style?.inlinePosition;
1787
- return p === "before" || p === "prepend" || p === "append" ? p : "after";
1788
- }
1789
- function insertHost(anchor, host, position) {
1790
- switch (position) {
1791
- case "before":
1792
- anchor.before(host);
1793
- break;
1794
- case "after":
1795
- anchor.after(host);
1796
- break;
1797
- case "prepend":
1798
- anchor.prepend(host);
1799
- break;
1800
- case "append":
1801
- anchor.append(host);
1802
- break;
1803
- }
1804
- }
1805
- function keepHostAttached(host, selector, position) {
1806
- const id = window.setInterval(() => {
1807
- if (host.isConnected) return;
1808
- const anchor = document.querySelector(selector);
1809
- if (anchor) insertHost(anchor, host, position);
1810
- }, 1e3);
1811
- return () => window.clearInterval(id);
1812
- }
1813
- var init_inline_host = __esm({
1814
- "src/plugins/guides/inline-host.ts"() {
1815
- }
1816
- });
1817
-
1818
2392
  // src/plugins/guides/renderers/inline-custom-renderer.ts
1819
2393
  var InlineCustomRenderer;
1820
2394
  var init_inline_custom_renderer = __esm({
@@ -1907,24 +2481,42 @@ var init_inline_renderer = __esm({
1907
2481
  const ownerDocument = root.ownerDocument ?? document;
1908
2482
  const card = ownerDocument.createElement("div");
1909
2483
  card.className = "veo-inline";
1910
- const dismiss = (action, ctaUrl) => {
1911
- 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
+ });
1912
2491
  if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
1913
2492
  context.onClose();
1914
2493
  };
1915
2494
  const content = buildStepContent(step, ownerDocument, {
1916
- 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),
1917
2496
  onDismiss: () => dismiss("dismissed")
1918
2497
  });
1919
2498
  card.appendChild(content);
1920
2499
  root.appendChild(card);
2500
+ this.liveContainer = card;
2501
+ this.liveContent = content;
2502
+ this.liveKey = `${selector}|${position}`;
2503
+ this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
2504
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
2505
+ onDismiss: () => dismiss("dismissed")
2506
+ });
1921
2507
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
1922
2508
  }
2509
+ liveKeyOf(step) {
2510
+ return `${step.selector ?? ""}|${readInlinePosition(step.style)}`;
2511
+ }
1923
2512
  };
1924
2513
  }
1925
2514
  });
1926
2515
 
1927
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
+ }
1928
2520
  var ModalRenderer;
1929
2521
  var init_modal_renderer = __esm({
1930
2522
  "src/plugins/guides/renderers/modal-renderer.ts"() {
@@ -1932,6 +2524,10 @@ var init_modal_renderer = __esm({
1932
2524
  init_walkthrough_block_builder();
1933
2525
  init_base_renderer();
1934
2526
  ModalRenderer = class extends BaseRenderer {
2527
+ constructor() {
2528
+ super(...arguments);
2529
+ this.overlay = null;
2530
+ }
1935
2531
  render(context) {
1936
2532
  const idx = context.nav?.stepIndex ?? 0;
1937
2533
  const step = context.guide.guideSteps[idx];
@@ -1940,11 +2536,17 @@ var init_modal_renderer = __esm({
1940
2536
  this.applyDesign(step.style);
1941
2537
  const ownerDocument = root.ownerDocument ?? document;
1942
2538
  const overlay = ownerDocument.createElement("div");
1943
- overlay.className = "veo-modal-overlay";
2539
+ overlay.className = overlayClassOf(step);
2540
+ this.overlay = overlay;
1944
2541
  const card = ownerDocument.createElement("div");
1945
2542
  card.className = "veo-modal-card";
1946
- const dismiss = (action, ctaUrl) => {
1947
- 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
+ });
1948
2550
  if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
1949
2551
  context.onClose();
1950
2552
  };
@@ -1959,12 +2561,20 @@ var init_modal_renderer = __esm({
1959
2561
  ownerDocument,
1960
2562
  context.nav.callbacks
1961
2563
  ) : buildStepContent(step, ownerDocument, {
1962
- 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),
1963
2565
  onDismiss: () => dismiss("dismissed")
1964
2566
  });
1965
2567
  card.appendChild(content);
1966
2568
  overlay.appendChild(card);
1967
2569
  root.appendChild(overlay);
2570
+ if (!context.nav) {
2571
+ this.liveContainer = card;
2572
+ this.liveContent = content;
2573
+ this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
2574
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
2575
+ onDismiss: () => dismiss("dismissed")
2576
+ });
2577
+ }
1968
2578
  overlay.addEventListener("click", (e) => {
1969
2579
  if (e.target === overlay) onBackdropOrEsc();
1970
2580
  });
@@ -1977,28 +2587,13 @@ var init_modal_renderer = __esm({
1977
2587
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
1978
2588
  }
1979
2589
  }
1980
- };
1981
- }
1982
- });
1983
-
1984
- // src/plugins/guides/renderers/floating-arrow.ts
1985
- function positionArrow(arrowEl, placement, data) {
1986
- const side = STATIC_SIDE[placement.split("-")[0] ?? "bottom"] ?? "top";
1987
- for (const prop of ["top", "bottom", "left", "right"]) {
1988
- arrowEl.style.setProperty(prop, "");
1989
- }
1990
- if (data?.x != null) arrowEl.style.setProperty("left", `${data.x}px`);
1991
- if (data?.y != null) arrowEl.style.setProperty("top", `${data.y}px`);
1992
- arrowEl.style.setProperty(side, "-6px");
1993
- }
1994
- var STATIC_SIDE;
1995
- var init_floating_arrow = __esm({
1996
- "src/plugins/guides/renderers/floating-arrow.ts"() {
1997
- STATIC_SIDE = {
1998
- top: "bottom",
1999
- bottom: "top",
2000
- left: "right",
2001
- 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
+ }
2002
2597
  };
2003
2598
  }
2004
2599
  });
@@ -2010,6 +2605,12 @@ var init_tooltip_renderer = __esm({
2010
2605
  init_base_renderer();
2011
2606
  init_floating_arrow();
2012
2607
  TooltipRenderer = class extends BaseRenderer {
2608
+ constructor() {
2609
+ super(...arguments);
2610
+ /** Lado preferido (mutable: el update in-place puede cambiarlo). */
2611
+ this.side = "bottom";
2612
+ this.reposition = null;
2613
+ }
2013
2614
  async render(context) {
2014
2615
  const step = context.guide.guideSteps[0];
2015
2616
  if (!step) return;
@@ -2018,24 +2619,25 @@ var init_tooltip_renderer = __esm({
2018
2619
  const anchor = await waitForElement(selector);
2019
2620
  if (!anchor) return;
2020
2621
  const rawSide = step.style?.tooltipPlacement;
2021
- const side = rawSide === "top" || rawSide === "left" || rawSide === "right" ? rawSide : "bottom";
2622
+ this.side = rawSide === "top" || rawSide === "left" || rawSide === "right" ? rawSide : "bottom";
2022
2623
  const { root } = this.createHost();
2023
2624
  this.applyDesign(step.style);
2024
2625
  const ownerDocument = root.ownerDocument ?? document;
2025
2626
  const tooltip = ownerDocument.createElement("div");
2026
2627
  tooltip.className = "veo-tooltip";
2027
- const dismiss = (action, ctaUrl) => {
2628
+ const dismiss = (action, ctaUrl, meta) => {
2028
2629
  context.onInteraction({
2029
2630
  guideId: context.guide.guideId,
2030
2631
  stepIndex: 0,
2031
- action
2632
+ action,
2633
+ ...meta ? { metadata: meta } : {}
2032
2634
  });
2033
2635
  if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
2034
2636
  context.onClose();
2035
2637
  };
2036
2638
  const content = buildStepContent(step, ownerDocument, {
2037
- onCtaClick: (action, url) => {
2038
- dismiss("cta_clicked", action === "url" ? url : void 0);
2639
+ onCtaClick: (action, url, meta) => {
2640
+ dismiss("cta_clicked", action === "url" ? url : void 0, meta);
2039
2641
  },
2040
2642
  onDismiss: () => dismiss("dismissed")
2041
2643
  });
@@ -2046,7 +2648,7 @@ var init_tooltip_renderer = __esm({
2046
2648
  root.appendChild(tooltip);
2047
2649
  const updatePosition = async () => {
2048
2650
  const { x, y, placement, middlewareData } = await dom.computePosition(anchor, tooltip, {
2049
- placement: side,
2651
+ placement: this.side,
2050
2652
  middleware: [dom.offset(10), dom.flip(), dom.shift({ padding: 8 }), dom.arrow({ element: arrowEl })]
2051
2653
  });
2052
2654
  tooltip.style.left = `${x}px`;
@@ -2057,18 +2659,34 @@ var init_tooltip_renderer = __esm({
2057
2659
  const reposition = () => {
2058
2660
  void updatePosition();
2059
2661
  };
2662
+ this.reposition = reposition;
2060
2663
  window.addEventListener("scroll", reposition, true);
2061
2664
  window.addEventListener("resize", reposition);
2062
2665
  this.registerCleanup(() => {
2063
2666
  window.removeEventListener("scroll", reposition, true);
2064
2667
  window.removeEventListener("resize", reposition);
2065
2668
  });
2669
+ this.liveContainer = tooltip;
2670
+ this.liveContent = content;
2671
+ this.liveKey = selector;
2672
+ this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
2673
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
2674
+ onDismiss: () => dismiss("dismissed")
2675
+ });
2066
2676
  context.onInteraction({
2067
2677
  guideId: context.guide.guideId,
2068
2678
  stepIndex: 0,
2069
2679
  action: "shown"
2070
2680
  });
2071
2681
  }
2682
+ liveKeyOf(step) {
2683
+ return step.selector ?? "";
2684
+ }
2685
+ onLiveUpdate(step) {
2686
+ const raw = step.style?.tooltipPlacement;
2687
+ this.side = raw === "top" || raw === "left" || raw === "right" ? raw : "bottom";
2688
+ this.reposition?.();
2689
+ }
2072
2690
  };
2073
2691
  }
2074
2692
  });
@@ -2136,7 +2754,7 @@ var init_walkthrough_renderer = __esm({
2136
2754
  function previewGuide(input) {
2137
2755
  if (!hasDocument()) {
2138
2756
  return { close: () => {
2139
- }, ready: Promise.resolve({ rendered: false }) };
2757
+ }, ready: Promise.resolve({ rendered: false }), host: () => null };
2140
2758
  }
2141
2759
  if (!activeController) activeController = new GuidePreviewController();
2142
2760
  return activeController.preview(input);
@@ -2179,6 +2797,8 @@ function createSingleStepRenderer(type) {
2179
2797
  return new FormRenderer();
2180
2798
  case "inline-form":
2181
2799
  return new InlineFormRenderer();
2800
+ case "badge":
2801
+ return new BadgeRenderer();
2182
2802
  case "walkthrough":
2183
2803
  return null;
2184
2804
  }
@@ -2193,7 +2813,9 @@ var PREVIEW_GUIDE_ID, GuidePreviewController, activeController;
2193
2813
  var init_guide_preview = __esm({
2194
2814
  "src/plugins/guides/guide-preview.ts"() {
2195
2815
  init_safe_env();
2816
+ init_badge_renderer();
2196
2817
  init_banner_renderer();
2818
+ init_base_renderer();
2197
2819
  init_custom_renderer();
2198
2820
  init_form_renderer();
2199
2821
  init_inline_custom_renderer();
@@ -2214,12 +2836,29 @@ var init_guide_preview = __esm({
2214
2836
  this.input = null;
2215
2837
  }
2216
2838
  preview(input) {
2839
+ const nextStep = input.guideSteps[0];
2840
+ if (this.singleStep && this.input && input.guideType === this.input.guideType && input.guideType !== "walkthrough" && nextStep && this.singleStep.updateStep(nextStep)) {
2841
+ this.input = input;
2842
+ return {
2843
+ close: () => this.close(),
2844
+ ready: Promise.resolve({ rendered: true }),
2845
+ host: () => this.activeHost()
2846
+ };
2847
+ }
2217
2848
  this.close();
2218
2849
  this.input = input;
2219
2850
  const guide = normalize(input);
2220
2851
  const start = typeof input.startStepIndex === "number" ? input.startStepIndex : 0;
2221
2852
  const ready = guide.guideType === "walkthrough" ? this.startWalkthrough(guide, start) : this.renderSingleStep(guide);
2222
- 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;
2223
2862
  }
2224
2863
  close() {
2225
2864
  if (this.singleStep) {
@@ -2434,8 +3073,8 @@ function injectBuilderPanel(opts) {
2434
3073
  const ot = top;
2435
3074
  iframe.style.pointerEvents = "none";
2436
3075
  const move = (ev) => {
2437
- left = clamp2(ol + (ev.clientX - sx), 0, Math.max(0, window.innerWidth - card.offsetWidth));
2438
- 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));
2439
3078
  card.style.left = `${left}px`;
2440
3079
  card.style.top = `${top}px`;
2441
3080
  };
@@ -2456,8 +3095,8 @@ function injectBuilderPanel(opts) {
2456
3095
  const sh = card.offsetHeight;
2457
3096
  iframe.style.pointerEvents = "none";
2458
3097
  const move = (ev) => {
2459
- card.style.width = `${clamp2(sw + (ev.clientX - sx), MIN_W, window.innerWidth)}px`;
2460
- 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`;
2461
3100
  };
2462
3101
  const up = () => {
2463
3102
  window.removeEventListener("mousemove", move);
@@ -2476,7 +3115,7 @@ function injectBuilderPanel(opts) {
2476
3115
  }
2477
3116
  };
2478
3117
  }
2479
- function clamp2(v, min, max) {
3118
+ function clamp3(v, min, max) {
2480
3119
  return Math.min(Math.max(v, min), max);
2481
3120
  }
2482
3121
  function buildPanelUrl(opts) {
@@ -2540,6 +3179,242 @@ var init_builder_panel = __esm({
2540
3179
  }
2541
3180
  });
2542
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
+
2543
3418
  // src/plugins/builder/element-picker.ts
2544
3419
  function startElementPicker(options) {
2545
3420
  if (!hasDocument()) {
@@ -2753,10 +3628,35 @@ function initBuilderMode() {
2753
3628
  let picker = null;
2754
3629
  let panel = null;
2755
3630
  let staticTarget = null;
3631
+ let manipulator = null;
3632
+ let pendingPreview = null;
2756
3633
  const getTarget = () => panel ? panel.target() : staticTarget;
2757
3634
  const post = (event) => {
2758
3635
  getTarget()?.postMessage({ source: VEO_BUILDER_SOURCE, token, ...event }, dashboardOrigin);
2759
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
+ };
2760
3660
  const handleCommand = (cmd) => {
2761
3661
  switch (cmd.type) {
2762
3662
  case "panel-ready":
@@ -2782,9 +3682,14 @@ function initBuilderMode() {
2782
3682
  picker = null;
2783
3683
  break;
2784
3684
  case "preview":
2785
- if (cmd.guide) void previewGuide(cmd.guide).ready;
3685
+ if (!cmd.guide) break;
3686
+ if (manipulator?.isDragging()) pendingPreview = cmd;
3687
+ else applyPreview(cmd);
2786
3688
  break;
2787
3689
  case "close-preview":
3690
+ manipulator?.detach();
3691
+ manipulator = null;
3692
+ pendingPreview = null;
2788
3693
  closeGuidePreview();
2789
3694
  break;
2790
3695
  case "teardown":
@@ -2804,6 +3709,9 @@ function initBuilderMode() {
2804
3709
  window.removeEventListener("pagehide", onPageHide);
2805
3710
  picker?.stop();
2806
3711
  picker = null;
3712
+ manipulator?.detach();
3713
+ manipulator = null;
3714
+ pendingPreview = null;
2807
3715
  closeGuidePreview();
2808
3716
  post({ type: "closed" });
2809
3717
  panel?.teardown();
@@ -2883,6 +3791,7 @@ var init_builder_mode = __esm({
2883
3791
  init_guide_preview();
2884
3792
  init_bridge_protocol();
2885
3793
  init_builder_panel();
3794
+ init_design_manipulator();
2886
3795
  init_element_picker();
2887
3796
  }
2888
3797
  });
@@ -3016,6 +3925,7 @@ var init_builder_session = __esm({
3016
3925
  // src/plugins/builder/index.ts
3017
3926
  var builder_exports = {};
3018
3927
  __export(builder_exports, {
3928
+ attachDesignManipulator: () => attachDesignManipulator,
3019
3929
  createBuilderSession: () => createBuilderSession,
3020
3930
  initBuilderMode: () => initBuilderMode,
3021
3931
  startElementPicker: () => startElementPicker,
@@ -3025,6 +3935,7 @@ var init_builder = __esm({
3025
3935
  "src/plugins/builder/index.ts"() {
3026
3936
  init_builder_mode();
3027
3937
  init_builder_session();
3938
+ init_design_manipulator();
3028
3939
  init_element_picker();
3029
3940
  }
3030
3941
  });
@@ -3291,8 +4202,8 @@ var DomListener = class {
3291
4202
  let depth = 0;
3292
4203
  while (current && current !== document.documentElement && depth < 5) {
3293
4204
  const tag = current.tagName.toLowerCase();
3294
- 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";
3295
- 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;
3296
4207
  current = current.parentElement;
3297
4208
  depth++;
3298
4209
  }
@@ -3414,7 +4325,8 @@ var Queue = class {
3414
4325
  return;
3415
4326
  }
3416
4327
  this.retryCount++;
3417
- 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);
3418
4330
  if (this.retryTimer) clearTimeout(this.retryTimer);
3419
4331
  this.retryTimer = setTimeout(() => {
3420
4332
  void this.flush();
@@ -4093,6 +5005,7 @@ function toWire(payload) {
4093
5005
  if (typeof payload.stepIndex === "number") wire.stepPosition = payload.stepIndex;
4094
5006
  if (payload.pageUrl) wire.pageUrl = payload.pageUrl;
4095
5007
  if (payload.pagePath) wire.pagePath = payload.pagePath;
5008
+ if (payload.metadata) wire.metadata = payload.metadata;
4096
5009
  return wire;
4097
5010
  }
4098
5011
  var GuideTrackerClient = class {
@@ -4140,6 +5053,8 @@ var GuideTrackerClient = class {
4140
5053
  };
4141
5054
 
4142
5055
  // src/plugins/guides/guide-tracker-queue.ts
5056
+ var RETRY_BASE_MS = 3e3;
5057
+ var RETRY_MAX_DELAY_MS2 = 3e4;
4143
5058
  var GuideTrackerQueue = class {
4144
5059
  constructor(config) {
4145
5060
  this.config = config;
@@ -4176,20 +5091,23 @@ var GuideTrackerQueue = class {
4176
5091
  */
4177
5092
  async flush() {
4178
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;
4179
5097
  this.flushing = true;
4180
- 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));
4181
5100
  const results = await Promise.allSettled(
4182
- batch.map(
4183
- (item) => this.config.client.send({ guideId: item.guideId, payload: item.payload })
4184
- )
5101
+ due.map((item) => this.config.client.send({ guideId: item.guideId, payload: item.payload }))
4185
5102
  );
4186
5103
  for (let i = 0; i < results.length; i++) {
4187
5104
  const result = results[i];
4188
- const item = batch[i];
5105
+ const item = due[i];
4189
5106
  if (!result || !item) continue;
4190
5107
  if (result.status === "fulfilled") continue;
4191
5108
  item.attempts += 1;
4192
5109
  if (item.attempts < this.config.maxRetries) {
5110
+ item.nextRetryAt = Date.now() + this.retryDelayMs(item.attempts);
4193
5111
  this.buffer.push(item);
4194
5112
  } else {
4195
5113
  this.safeOnError(result.reason, item);
@@ -4197,6 +5115,15 @@ var GuideTrackerQueue = class {
4197
5115
  }
4198
5116
  this.flushing = false;
4199
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
+ }
4200
5127
  /**
4201
5128
  * Drena el buffer vía `sendBeacon`. Síncrono, sin retry. Para pagehide.
4202
5129
  */
@@ -4243,6 +5170,7 @@ var GuideTrackerQueue = class {
4243
5170
  };
4244
5171
 
4245
5172
  // src/plugins/guides/guides-controller.ts
5173
+ init_badge_renderer();
4246
5174
  init_banner_renderer();
4247
5175
  init_custom_renderer();
4248
5176
  init_form_renderer();
@@ -4370,6 +5298,7 @@ var WalkthroughStateManager = class {
4370
5298
  };
4371
5299
 
4372
5300
  // src/plugins/guides/guides-controller.ts
5301
+ var RESOLVE_CACHE_TTL_MS = 3e4;
4373
5302
  var STATUS_BY_ACTION = {
4374
5303
  shown: "shown",
4375
5304
  dismissed: "dismissed",
@@ -4396,6 +5325,10 @@ var GuidesController = class {
4396
5325
  */
4397
5326
  this.activeWalkthroughRenderer = null;
4398
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;
4399
5332
  this.debug = config.debug === true;
4400
5333
  this.apiUrl = config.apiUrl;
4401
5334
  this.apiKey = config.apiKey;
@@ -4447,7 +5380,7 @@ var GuidesController = class {
4447
5380
  }
4448
5381
  let guides;
4449
5382
  try {
4450
- guides = await this.resolver.resolve(endUserId, pageUrl);
5383
+ guides = await this.resolveGuides(endUserId, pageUrl);
4451
5384
  } catch (err) {
4452
5385
  if (this.debug) console.error("[veo] guides resolver threw:", err);
4453
5386
  return;
@@ -4474,6 +5407,36 @@ var GuidesController = class {
4474
5407
  this.runSingleStepRender(guide, sessionId, pageUrl);
4475
5408
  }
4476
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
+ }
4477
5440
  /** Cierra todas las guías activas, limpia cache de dispatch y para timers. */
4478
5441
  destroy() {
4479
5442
  for (const renderer of this.activeRenderers) {
@@ -4494,6 +5457,8 @@ var GuidesController = class {
4494
5457
  this.activeWalkthroughGuide = null;
4495
5458
  }
4496
5459
  this.dispatched.clear();
5460
+ this.resolveCache = null;
5461
+ this.inflightResolve = null;
4497
5462
  this.trackerQueue.destroy();
4498
5463
  }
4499
5464
  runSingleStepRender(guide, sessionId, pageUrl) {
@@ -4718,7 +5683,8 @@ var GuidesController = class {
4718
5683
  * por si el usuario se identificó después de que la guía apareció.
4719
5684
  */
4720
5685
  handleInteraction(guide, sessionId, pageUrl, event) {
4721
- const dedupKey = `${guide.guideId}:${event.action}`;
5686
+ const buttonKey = event.metadata?.buttonId ? `:${event.metadata.buttonId}` : "";
5687
+ const dedupKey = `${guide.guideId}:${event.action}${buttonKey}`;
4722
5688
  if (this.dispatched.has(dedupKey)) {
4723
5689
  if (this.debug) console.log(`[veo] guides: dedup hit ${dedupKey}`);
4724
5690
  return;
@@ -4740,7 +5706,8 @@ var GuidesController = class {
4740
5706
  action: event.action,
4741
5707
  stepIndex: event.stepIndex,
4742
5708
  pageUrl,
4743
- pagePath: extractPath(pageUrl)
5709
+ pagePath: extractPath(pageUrl),
5710
+ ...event.metadata ? { metadata: event.metadata } : {}
4744
5711
  }
4745
5712
  });
4746
5713
  }
@@ -4791,6 +5758,8 @@ var GuidesController = class {
4791
5758
  return new FormRenderer();
4792
5759
  case "inline-form":
4793
5760
  return new InlineFormRenderer();
5761
+ case "badge":
5762
+ return new BadgeRenderer();
4794
5763
  case "walkthrough":
4795
5764
  return null;
4796
5765
  }