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/builder.cjs CHANGED
@@ -142,6 +142,112 @@ var SAFE_URL_PROTOCOLS = ["http:", "https:"];
142
142
  var GUIDE_HOST_ATTR = "data-veo-guide";
143
143
  var WALKTHROUGH_STEP_SELECTOR_TIMEOUT_MS = 5e3;
144
144
 
145
+ // src/plugins/guides/rich-text.ts
146
+ var ALLOWED_TAGS = {
147
+ P: "p",
148
+ BR: "br",
149
+ STRONG: "strong",
150
+ B: "strong",
151
+ EM: "em",
152
+ I: "em",
153
+ U: "u",
154
+ S: "s",
155
+ UL: "ul",
156
+ OL: "ol",
157
+ LI: "li",
158
+ A: "a"
159
+ };
160
+ var DROP_TAGS = /* @__PURE__ */ new Set([
161
+ "SCRIPT",
162
+ "STYLE",
163
+ "TEMPLATE",
164
+ "IFRAME",
165
+ "OBJECT",
166
+ "EMBED",
167
+ "NOSCRIPT",
168
+ "TITLE",
169
+ "TEXTAREA",
170
+ "SELECT",
171
+ "SVG",
172
+ "MATH"
173
+ ]);
174
+ var MAX_INPUT_LEN = 16 * 1024;
175
+ var MAX_DEPTH = 20;
176
+ var MAX_NODES = 1e3;
177
+ function renderRichText(html, doc) {
178
+ const fragment = doc.createDocumentFragment();
179
+ if (typeof html !== "string" || html.length === 0) return fragment;
180
+ let parsed;
181
+ try {
182
+ parsed = new DOMParser().parseFromString(
183
+ html.length > MAX_INPUT_LEN ? html.slice(0, MAX_INPUT_LEN) : html,
184
+ "text/html"
185
+ );
186
+ } catch {
187
+ fragment.appendChild(doc.createTextNode(html));
188
+ return fragment;
189
+ }
190
+ const budget = { nodes: 0, exceeded: false };
191
+ for (const child of Array.from(parsed.body.childNodes)) {
192
+ const rebuilt = rebuildNode(child, doc, 0, budget);
193
+ if (rebuilt) fragment.appendChild(rebuilt);
194
+ }
195
+ if (budget.exceeded) {
196
+ const plain = doc.createDocumentFragment();
197
+ plain.appendChild(doc.createTextNode(parsed.body.textContent ?? ""));
198
+ return plain;
199
+ }
200
+ return fragment;
201
+ }
202
+ function rebuildNode(node, doc, depth, budget) {
203
+ if (budget.exceeded) return null;
204
+ if (++budget.nodes > MAX_NODES || depth > MAX_DEPTH) {
205
+ budget.exceeded = true;
206
+ return null;
207
+ }
208
+ if (node.nodeType === Node.TEXT_NODE) {
209
+ return doc.createTextNode(node.textContent ?? "");
210
+ }
211
+ if (node.nodeType !== Node.ELEMENT_NODE) return null;
212
+ const el = node;
213
+ if (DROP_TAGS.has(el.tagName)) return null;
214
+ const mapped = ALLOWED_TAGS[el.tagName];
215
+ if (!mapped) {
216
+ const frag = doc.createDocumentFragment();
217
+ for (const child of Array.from(el.childNodes)) {
218
+ const rebuilt2 = rebuildNode(child, doc, depth + 1, budget);
219
+ if (rebuilt2) frag.appendChild(rebuilt2);
220
+ }
221
+ return frag;
222
+ }
223
+ if (mapped === "a") {
224
+ const href = el.getAttribute("href") ?? "";
225
+ if (!isSafeUrl(href)) {
226
+ const frag = doc.createDocumentFragment();
227
+ for (const child of Array.from(el.childNodes)) {
228
+ const rebuilt2 = rebuildNode(child, doc, depth + 1, budget);
229
+ if (rebuilt2) frag.appendChild(rebuilt2);
230
+ }
231
+ return frag;
232
+ }
233
+ const a = doc.createElement("a");
234
+ a.setAttribute("href", href);
235
+ a.setAttribute("target", "_blank");
236
+ a.setAttribute("rel", "noopener noreferrer nofollow");
237
+ for (const child of Array.from(el.childNodes)) {
238
+ const rebuilt2 = rebuildNode(child, doc, depth + 1, budget);
239
+ if (rebuilt2) a.appendChild(rebuilt2);
240
+ }
241
+ return a;
242
+ }
243
+ const rebuilt = doc.createElement(mapped);
244
+ for (const child of Array.from(el.childNodes)) {
245
+ const childNode = rebuildNode(child, doc, depth + 1, budget);
246
+ if (childNode) rebuilt.appendChild(childNode);
247
+ }
248
+ return rebuilt;
249
+ }
250
+
145
251
  // src/plugins/guides/block-builder.ts
146
252
  function buildStepContent(step, doc, callbacks) {
147
253
  const container = doc.createElement("div");
@@ -236,9 +342,13 @@ function buildContentBlock(block, doc) {
236
342
  return el;
237
343
  }
238
344
  case "text": {
239
- const el = doc.createElement("p");
345
+ const el = doc.createElement("div");
240
346
  el.className = "veo-guide-text";
241
- el.textContent = typeof block.text === "string" ? block.text : "";
347
+ if (typeof block.html === "string" && block.html) {
348
+ el.appendChild(renderRichText(block.html, doc));
349
+ } else {
350
+ el.textContent = typeof block.text === "string" ? block.text : "";
351
+ }
242
352
  applyBlockStyle(el, block.style, "text");
243
353
  return el;
244
354
  }
@@ -264,7 +374,11 @@ function buildButtonBlock(block, doc, callbacks) {
264
374
  btn.addEventListener("click", () => {
265
375
  const action = block.action ?? "dismiss";
266
376
  const url = action === "url" && typeof block.url === "string" && isSafeUrl(block.url) ? block.url : void 0;
267
- callbacks.onCtaClick(action, url);
377
+ const meta = {
378
+ ...typeof block.id === "string" && block.id ? { buttonId: block.id } : {},
379
+ ...typeof block.text === "string" && block.text ? { buttonText: block.text } : {}
380
+ };
381
+ callbacks.onCtaClick(action, url, Object.keys(meta).length ? meta : void 0);
268
382
  });
269
383
  return btn;
270
384
  }
@@ -279,6 +393,10 @@ function clampNum(n, min, max) {
279
393
  function applyBlockStyle(el, style, kind) {
280
394
  if (!style || typeof style !== "object") return;
281
395
  const s = style;
396
+ if (kind === "image" && s.bleed === true) {
397
+ el.classList.add("veo-guide-image--bleed");
398
+ return;
399
+ }
282
400
  const align = typeof s.align === "string" ? s.align : null;
283
401
  if (align && (align === "left" || align === "center" || align === "right")) {
284
402
  if (kind === "text") el.style.textAlign = align;
@@ -326,78 +444,6 @@ function readCtaUrl(step) {
326
444
  return isSafeUrl(candidate) ? candidate : void 0;
327
445
  }
328
446
 
329
- // src/plugins/guides/walkthrough-block-builder.ts
330
- function buildWalkthroughStepContent(step, stepIndex, totalSteps, doc, callbacks) {
331
- const container = doc.createElement("div");
332
- container.className = "veo-guide-content";
333
- const counter = doc.createElement("div");
334
- counter.className = "veo-walkthrough-counter";
335
- counter.textContent = `Paso ${stepIndex + 1} de ${totalSteps}`;
336
- container.appendChild(counter);
337
- const progress = doc.createElement("div");
338
- progress.className = "veo-walkthrough-progress";
339
- for (let i = 0; i < totalSteps; i++) {
340
- const dot = doc.createElement("span");
341
- dot.className = "veo-walkthrough-progress-dot";
342
- if (i < stepIndex) dot.classList.add("completed");
343
- if (i === stepIndex) dot.classList.add("active");
344
- progress.appendChild(dot);
345
- }
346
- container.appendChild(progress);
347
- if (typeof step.imageUrl === "string" && step.imageUrl && isSafeUrl(step.imageUrl)) {
348
- const img = doc.createElement("img");
349
- img.className = "veo-guide-image";
350
- img.src = step.imageUrl;
351
- img.alt = typeof step.title === "string" ? step.title : "";
352
- container.appendChild(img);
353
- }
354
- if (typeof step.title === "string" && step.title) {
355
- const heading = doc.createElement("h2");
356
- heading.className = "veo-guide-title";
357
- heading.textContent = step.title;
358
- container.appendChild(heading);
359
- }
360
- if (typeof step.content === "string" && step.content) {
361
- const paragraph = doc.createElement("p");
362
- paragraph.className = "veo-guide-text";
363
- paragraph.textContent = step.content;
364
- container.appendChild(paragraph);
365
- }
366
- const actions = doc.createElement("div");
367
- actions.className = "veo-walkthrough-actions";
368
- const skipBtn = doc.createElement("button");
369
- skipBtn.type = "button";
370
- skipBtn.className = "veo-walkthrough-skip";
371
- skipBtn.textContent = "Omitir";
372
- skipBtn.addEventListener("click", () => callbacks.onSkip());
373
- actions.appendChild(skipBtn);
374
- const rightGroup = doc.createElement("div");
375
- rightGroup.className = "veo-walkthrough-actions-right";
376
- if (stepIndex > 0) {
377
- const backBtn = doc.createElement("button");
378
- backBtn.type = "button";
379
- backBtn.className = "veo-walkthrough-btn-secondary";
380
- backBtn.textContent = "Atr\xE1s";
381
- backBtn.addEventListener("click", () => callbacks.onBack());
382
- rightGroup.appendChild(backBtn);
383
- }
384
- const isLastStep = stepIndex === totalSteps - 1;
385
- const primaryBtn = doc.createElement("button");
386
- primaryBtn.type = "button";
387
- primaryBtn.className = "veo-guide-cta";
388
- const defaultLabel = isLastStep ? "Finalizar" : "Siguiente";
389
- primaryBtn.textContent = typeof step.ctaText === "string" && step.ctaText ? step.ctaText : defaultLabel;
390
- primaryBtn.addEventListener("click", () => {
391
- if (isLastStep) callbacks.onComplete();
392
- else callbacks.onNext();
393
- });
394
- rightGroup.appendChild(primaryBtn);
395
- actions.appendChild(rightGroup);
396
- container.appendChild(actions);
397
- container.appendChild(createCloseButton(doc, () => callbacks.onSkip()));
398
- return container;
399
- }
400
-
401
447
  // src/plugins/guides/guide-design.ts
402
448
  var DARK_THEME = {
403
449
  "--veo-bg": "#1f2937",
@@ -470,6 +516,9 @@ function applyDesignVars(host, style) {
470
516
  if (typeof s.width === "number" && Number.isFinite(s.width)) {
471
517
  host.style.setProperty("--veo-width", `${clamp(s.width, 220, 720)}px`);
472
518
  }
519
+ if (typeof s.height === "number" && Number.isFinite(s.height)) {
520
+ host.style.setProperty("--veo-min-h", `${clamp(s.height, 120, 900)}px`);
521
+ }
473
522
  if (s.align === "left" || s.align === "center" || s.align === "right") {
474
523
  host.style.setProperty("--veo-actions-justify", ALIGN_JUSTIFY[s.align]);
475
524
  }
@@ -555,6 +604,68 @@ function applyElementVars(host, prefix, raw) {
555
604
  }
556
605
  }
557
606
 
607
+ // src/plugins/guides/inline-host.ts
608
+ function readInlinePosition(style) {
609
+ const p = style?.inlinePosition;
610
+ return p === "before" || p === "prepend" || p === "append" ? p : "after";
611
+ }
612
+ function insertHost(anchor, host, position) {
613
+ switch (position) {
614
+ case "before":
615
+ anchor.before(host);
616
+ break;
617
+ case "after":
618
+ anchor.after(host);
619
+ break;
620
+ case "prepend":
621
+ anchor.prepend(host);
622
+ break;
623
+ case "append":
624
+ anchor.append(host);
625
+ break;
626
+ }
627
+ }
628
+ function keepHostAttached(host, selector, position) {
629
+ const id = window.setInterval(() => {
630
+ if (host.isConnected) return;
631
+ const anchor = document.querySelector(selector);
632
+ if (anchor) insertHost(anchor, host, position);
633
+ }, 1e3);
634
+ return () => window.clearInterval(id);
635
+ }
636
+
637
+ // src/plugins/guides/wait-for-element.ts
638
+ function waitForElement(selector, timeoutMs = DEFAULT_ANCHOR_WAIT_MS) {
639
+ return new Promise((resolve) => {
640
+ const safeQuery = () => {
641
+ try {
642
+ return document.querySelector(selector);
643
+ } catch {
644
+ return null;
645
+ }
646
+ };
647
+ const existing = safeQuery();
648
+ if (existing) {
649
+ resolve(existing);
650
+ return;
651
+ }
652
+ let resolved = false;
653
+ const finish = (el) => {
654
+ if (resolved) return;
655
+ resolved = true;
656
+ observer.disconnect();
657
+ clearTimeout(timer);
658
+ resolve(el);
659
+ };
660
+ const observer = new MutationObserver(() => {
661
+ const el = safeQuery();
662
+ if (el) finish(el);
663
+ });
664
+ observer.observe(document.body, { childList: true, subtree: true });
665
+ const timer = setTimeout(() => finish(null), timeoutMs);
666
+ });
667
+ }
668
+
558
669
  // src/plugins/guides/styles.ts
559
670
  var GUIDE_STYLES = `
560
671
  :host {
@@ -576,6 +687,13 @@ var GUIDE_STYLES = `
576
687
  z-index: ${GUIDE_Z_INDEX};
577
688
  animation: veo-fade-in 180ms ease-out;
578
689
  }
690
+ /* Sin backdrop (style.backdrop === false): la app queda usable detr\xE1s; solo la
691
+ tarjeta captura el mouse. El click-en-backdrop deja de cerrar (no hay backdrop). */
692
+ .veo-modal-overlay--none {
693
+ background: transparent;
694
+ pointer-events: none;
695
+ }
696
+ .veo-modal-overlay--none .veo-modal-card { pointer-events: auto; }
579
697
  /*
580
698
  * Posici\xF3n libre/preset: --veo-pos-x/y son porcentajes (default 50% = centro).
581
699
  * El truco translate(-pos) alinea la MISMA fracci\xF3n de la tarjeta con esa
@@ -592,6 +710,7 @@ var GUIDE_STYLES = `
592
710
  border: var(--veo-border-width, 0) solid var(--veo-border-color, transparent);
593
711
  padding: var(--veo-pad, 24px);
594
712
  max-width: var(--veo-width); width: 90%;
713
+ min-height: var(--veo-min-h, auto);
595
714
  box-shadow: var(--veo-shadow);
596
715
  animation: veo-fade-in 180ms ease-out;
597
716
  }
@@ -607,6 +726,14 @@ var GUIDE_STYLES = `
607
726
  }
608
727
  .veo-banner-top { top: 0; }
609
728
  .veo-banner-bottom { bottom: 0; }
729
+ /* Banner EMBEBIDO en un contenedor (step.selector): fluye dentro del contenedor
730
+ y empuja su contenido, en vez de flotar fijo sobre la pantalla. */
731
+ .veo-banner-embedded {
732
+ position: static;
733
+ left: auto; right: auto;
734
+ width: 100%;
735
+ border-radius: var(--veo-radius, 0);
736
+ }
610
737
 
611
738
  .veo-tooltip {
612
739
  position: absolute;
@@ -660,6 +787,21 @@ var GUIDE_STYLES = `
660
787
  border-radius: var(--veo-image-radius, 8px);
661
788
  margin: var(--veo-image-mt, 0) 0 var(--veo-image-mb, 12px);
662
789
  }
790
+ /* Imagen A SANGRE: rompe el padding de la tarjeta y ocupa el ancho completo
791
+ (hero estilo anuncio). Como primer bloque, hereda el redondeo superior. */
792
+ .veo-guide-image--bleed {
793
+ width: calc(100% + var(--veo-pad, 24px) * 2);
794
+ max-width: none;
795
+ max-height: 280px;
796
+ align-self: auto;
797
+ border-radius: 0;
798
+ margin: 0 calc(var(--veo-pad, 24px) * -1) 12px;
799
+ }
800
+ .veo-guide-content > .veo-guide-image--bleed:first-child {
801
+ margin-top: calc(var(--veo-pad, 24px) * -1);
802
+ border-radius: calc(var(--veo-radius, 12px) - var(--veo-border-width, 0px))
803
+ calc(var(--veo-radius, 12px) - var(--veo-border-width, 0px)) 0 0;
804
+ }
663
805
  .veo-guide-title {
664
806
  font-size: var(--veo-title-size, 18px); font-weight: 600; line-height: 1.3;
665
807
  text-align: var(--veo-title-align, left);
@@ -672,6 +814,18 @@ var GUIDE_STYLES = `
672
814
  margin: var(--veo-text-mt, 0) 0 var(--veo-text-mb, 16px);
673
815
  color: var(--veo-text-color, var(--veo-text-secondary));
674
816
  }
817
+ /* Rich text dentro de un bloque de texto (p/listas/links/\xE9nfasis). */
818
+ .veo-guide-text p { margin: 0 0 8px; }
819
+ .veo-guide-text p:last-child { margin-bottom: 0; }
820
+ .veo-guide-text ul, .veo-guide-text ol { margin: 0 0 8px; padding-left: 20px; }
821
+ .veo-guide-text ul { list-style: disc; }
822
+ .veo-guide-text ol { list-style: decimal; }
823
+ .veo-guide-text li { margin: 2px 0; display: list-item; }
824
+ .veo-guide-text a { color: var(--veo-primary); text-decoration: underline; cursor: pointer; }
825
+ .veo-guide-text strong { font-weight: 600; }
826
+ .veo-guide-text em { font-style: italic; }
827
+ .veo-guide-text u { text-decoration: underline; }
828
+ .veo-guide-text s { text-decoration: line-through; }
675
829
  .veo-guide-actions {
676
830
  display: flex; gap: 8px; justify-content: var(--veo-actions-justify);
677
831
  }
@@ -810,6 +964,44 @@ var GUIDE_STYLES = `
810
964
  }
811
965
  .veo-walkthrough-skip:hover { color: var(--veo-text); }
812
966
 
967
+ /* \u2500\u2500 Badge (elemento inyectado junto al ancla que abre un tooltip) \u2500\u2500 */
968
+ .veo-badge {
969
+ display: inline-flex; align-items: center; justify-content: center;
970
+ border: none; padding: 0; margin: 0 4px;
971
+ background: none; cursor: pointer;
972
+ line-height: 1; vertical-align: middle;
973
+ font-family: inherit;
974
+ }
975
+ .veo-badge:focus-visible { outline: 2px solid var(--veo-primary); outline-offset: 2px; }
976
+ .veo-badge--icon {
977
+ border-radius: 50%;
978
+ background: color-mix(in srgb, var(--veo-primary) 14%, transparent);
979
+ color: var(--veo-primary);
980
+ font-weight: 600;
981
+ }
982
+ .veo-badge--dot {
983
+ border-radius: 50%;
984
+ background: var(--veo-primary);
985
+ animation: veo-badge-pulse 2s ease-out infinite;
986
+ }
987
+ .veo-badge--pill {
988
+ border-radius: 999px;
989
+ background: var(--veo-primary);
990
+ color: #fff;
991
+ font-weight: 600;
992
+ padding: 3px 9px;
993
+ white-space: nowrap;
994
+ }
995
+ .veo-badge--image img { display: block; border-radius: 4px; object-fit: cover; }
996
+ /* El tooltip del badge usa strategy fixed (el host vive dentro del flujo del
997
+ cliente; un absoluto se recortar\xEDa con overflow de ancestros). */
998
+ .veo-badge-tooltip { position: fixed; }
999
+ @keyframes veo-badge-pulse {
1000
+ 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--veo-primary) 45%, transparent); }
1001
+ 70% { box-shadow: 0 0 0 7px transparent; }
1002
+ 100% { box-shadow: 0 0 0 0 transparent; }
1003
+ }
1004
+
813
1005
  .veo-custom-floating {
814
1006
  position: fixed;
815
1007
  z-index: ${GUIDE_Z_INDEX};
@@ -905,52 +1097,412 @@ var BaseRenderer = class {
905
1097
  this.shadow = shadow;
906
1098
  return { host, shadow, root };
907
1099
  }
908
- /**
909
- * Aplica la tematización por datos del step (`step.style`) como variables
910
- * CSS sobre el host. Llamar tras `createHost()`. No-op si no hay host o
911
- * style. Ver `guide-design.ts`.
912
- */
913
- applyDesign(style) {
914
- if (this.host) applyDesignVars(this.host, style);
1100
+ /**
1101
+ * Aplica la tematización por datos del step (`step.style`) como variables
1102
+ * CSS sobre el host. Llamar tras `createHost()`. No-op si no hay host o
1103
+ * style. Ver `guide-design.ts`.
1104
+ */
1105
+ applyDesign(style) {
1106
+ if (this.host) applyDesignVars(this.host, style);
1107
+ }
1108
+ /**
1109
+ * Registra una función a ejecutar en `destroy()`. Útil para limpiar
1110
+ * listeners globales (scroll, resize) o intervalos.
1111
+ */
1112
+ registerCleanup(fn) {
1113
+ this.cleanups.push(fn);
1114
+ }
1115
+ /**
1116
+ * Host de la guía montada (o `null` si aún no se montó / ya se destruyó).
1117
+ * Lo usa el modo builder para adjuntar manipulación directa (drag/resize)
1118
+ * sobre el shadow root abierto sin tocar los renderers.
1119
+ */
1120
+ hostElement() {
1121
+ return this.host;
1122
+ }
1123
+ /** Remueve el host del DOM y corre todas las funciones de cleanup. */
1124
+ destroy() {
1125
+ for (const fn of this.cleanups) {
1126
+ try {
1127
+ fn();
1128
+ } catch {
1129
+ }
1130
+ }
1131
+ this.cleanups = [];
1132
+ if (this.host?.parentNode) {
1133
+ this.host.parentNode.removeChild(this.host);
1134
+ }
1135
+ this.host = null;
1136
+ this.shadow = null;
1137
+ }
1138
+ };
1139
+
1140
+ // src/plugins/guides/renderers/floating-arrow.ts
1141
+ var STATIC_SIDE = {
1142
+ top: "bottom",
1143
+ bottom: "top",
1144
+ left: "right",
1145
+ right: "left"
1146
+ };
1147
+ function positionArrow(arrowEl, placement, data) {
1148
+ const side = STATIC_SIDE[placement.split("-")[0] ?? "bottom"] ?? "top";
1149
+ for (const prop of ["top", "bottom", "left", "right"]) {
1150
+ arrowEl.style.setProperty(prop, "");
1151
+ }
1152
+ if (data?.x != null) arrowEl.style.setProperty("left", `${data.x}px`);
1153
+ if (data?.y != null) arrowEl.style.setProperty("top", `${data.y}px`);
1154
+ arrowEl.style.setProperty(side, "-6px");
1155
+ }
1156
+
1157
+ // src/plugins/guides/renderers/badge-renderer.ts
1158
+ var HOVER_CLOSE_DELAY_MS = 150;
1159
+ var BadgeRenderer = class extends BaseRenderer {
1160
+ constructor() {
1161
+ super(...arguments);
1162
+ this.tooltip = null;
1163
+ this.arrowEl = null;
1164
+ this.badgeBtn = null;
1165
+ this.open = false;
1166
+ this.shownEmitted = false;
1167
+ this.trigger = "hover";
1168
+ this.side = "top";
1169
+ this.closeTimer = null;
1170
+ this.stopFloat = null;
1171
+ }
1172
+ async render(context) {
1173
+ const step = context.guide.guideSteps[0];
1174
+ if (!step) return;
1175
+ const selector = step.selector ?? context.guide.activationRules.selector;
1176
+ if (typeof selector !== "string" || selector.length === 0) return;
1177
+ const anchor = await waitForElement(selector);
1178
+ if (!anchor) return;
1179
+ const { host, root } = this.createHost();
1180
+ const ownerDocument = root.ownerDocument ?? document;
1181
+ this.applyDesign(step.style);
1182
+ this.trigger = step.style?.badgeTrigger === "click" ? "click" : "hover";
1183
+ const rawSide = step.style?.tooltipPlacement;
1184
+ this.side = rawSide === "bottom" || rawSide === "left" || rawSide === "right" ? rawSide : "top";
1185
+ const position = readInlinePosition(step.style);
1186
+ host.style.display = "inline-flex";
1187
+ host.style.verticalAlign = "middle";
1188
+ const badge = buildBadgeElement(readBadgeConfig(step), ownerDocument);
1189
+ root.appendChild(badge);
1190
+ this.badgeBtn = badge;
1191
+ insertHost(anchor, host, position);
1192
+ this.registerCleanup(keepHostAttached(host, selector, position));
1193
+ const tooltip = ownerDocument.createElement("div");
1194
+ tooltip.className = "veo-tooltip veo-badge-tooltip";
1195
+ tooltip.style.display = "none";
1196
+ const emit = (action, meta) => {
1197
+ context.onInteraction({
1198
+ guideId: context.guide.guideId,
1199
+ stepIndex: 0,
1200
+ action,
1201
+ ...meta ? { metadata: meta } : {}
1202
+ });
1203
+ };
1204
+ const buildTooltipContent = (s) => buildStepContent(s, ownerDocument, {
1205
+ onCtaClick: (action, url, meta) => {
1206
+ emit("cta_clicked", meta);
1207
+ if (action === "url" && url) window.open(url, "_blank", "noopener,noreferrer");
1208
+ if (action === "dismiss") {
1209
+ emit("dismissed");
1210
+ context.onClose();
1211
+ return;
1212
+ }
1213
+ this.closeTooltip();
1214
+ },
1215
+ onDismiss: () => {
1216
+ emit("dismissed");
1217
+ context.onClose();
1218
+ }
1219
+ });
1220
+ const content = buildTooltipContent(step);
1221
+ tooltip.appendChild(content);
1222
+ const arrowEl = ownerDocument.createElement("div");
1223
+ arrowEl.className = "veo-tooltip-arrow";
1224
+ tooltip.appendChild(arrowEl);
1225
+ root.appendChild(tooltip);
1226
+ this.tooltip = tooltip;
1227
+ this.arrowEl = arrowEl;
1228
+ this.liveContainer = tooltip;
1229
+ this.liveContent = content;
1230
+ this.liveKey = this.liveKeyOf(step);
1231
+ this.liveBuild = buildTooltipContent;
1232
+ const openNow = () => {
1233
+ this.cancelClose();
1234
+ if (!this.open) {
1235
+ this.openTooltip(badge);
1236
+ if (!this.shownEmitted) {
1237
+ this.shownEmitted = true;
1238
+ emit("shown");
1239
+ }
1240
+ }
1241
+ };
1242
+ if (this.trigger === "hover") {
1243
+ const scheduleClose = () => {
1244
+ this.cancelClose();
1245
+ this.closeTimer = window.setTimeout(() => this.closeTooltip(), HOVER_CLOSE_DELAY_MS);
1246
+ };
1247
+ badge.addEventListener("mouseenter", openNow);
1248
+ badge.addEventListener("focus", openNow);
1249
+ badge.addEventListener("mouseleave", scheduleClose);
1250
+ badge.addEventListener("blur", scheduleClose);
1251
+ tooltip.addEventListener("mouseenter", () => this.cancelClose());
1252
+ tooltip.addEventListener("mouseleave", scheduleClose);
1253
+ } else {
1254
+ badge.addEventListener("click", () => {
1255
+ if (this.open) this.closeTooltip();
1256
+ else openNow();
1257
+ });
1258
+ const onDocClick = (e) => {
1259
+ if (!this.open) return;
1260
+ if (e.composedPath().includes(host)) return;
1261
+ this.closeTooltip();
1262
+ };
1263
+ document.addEventListener("click", onDocClick, true);
1264
+ this.registerCleanup(() => document.removeEventListener("click", onDocClick, true));
1265
+ }
1266
+ const onKey = (e) => {
1267
+ if (e.key === "Escape" && this.open) this.closeTooltip();
1268
+ };
1269
+ document.addEventListener("keydown", onKey);
1270
+ this.registerCleanup(() => document.removeEventListener("keydown", onKey));
1271
+ if (context.isPreview) openNow();
1272
+ }
1273
+ /** Cambiar de ancla/posición/trigger requiere re-montar y re-cablear. */
1274
+ liveKeyOf(step) {
1275
+ const selector = step.selector ?? "";
1276
+ const position = readInlinePosition(step.style);
1277
+ const trigger = step.style?.badgeTrigger === "click" ? "click" : "hover";
1278
+ return `${selector}|${position}|${trigger}`;
1279
+ }
1280
+ onLiveUpdate(step) {
1281
+ if (this.badgeBtn) {
1282
+ const doc = this.badgeBtn.ownerDocument;
1283
+ const next = buildBadgeElement(readBadgeConfig(step), doc);
1284
+ this.badgeBtn.className = next.className;
1285
+ this.badgeBtn.setAttribute("style", next.getAttribute("style") ?? "");
1286
+ this.badgeBtn.replaceChildren(...Array.from(next.childNodes));
1287
+ }
1288
+ const rawSide = step.style?.tooltipPlacement;
1289
+ this.side = rawSide === "bottom" || rawSide === "left" || rawSide === "right" ? rawSide : "top";
1290
+ if (this.open && this.badgeBtn) this.position(this.badgeBtn);
1291
+ }
1292
+ destroy() {
1293
+ this.cancelClose();
1294
+ this.stopFloat?.();
1295
+ this.stopFloat = null;
1296
+ this.tooltip = null;
1297
+ this.arrowEl = null;
1298
+ this.badgeBtn = null;
1299
+ this.open = false;
1300
+ super.destroy();
1301
+ }
1302
+ openTooltip(badge) {
1303
+ if (!this.tooltip) return;
1304
+ this.tooltip.style.display = "block";
1305
+ this.open = true;
1306
+ void this.position(badge);
1307
+ const reposition = () => {
1308
+ void this.position(badge);
1309
+ };
1310
+ window.addEventListener("scroll", reposition, true);
1311
+ window.addEventListener("resize", reposition);
1312
+ this.stopFloat = () => {
1313
+ window.removeEventListener("scroll", reposition, true);
1314
+ window.removeEventListener("resize", reposition);
1315
+ };
1316
+ }
1317
+ closeTooltip() {
1318
+ this.cancelClose();
1319
+ if (!this.tooltip || !this.open) return;
1320
+ this.tooltip.style.display = "none";
1321
+ this.open = false;
1322
+ this.stopFloat?.();
1323
+ this.stopFloat = null;
1324
+ }
1325
+ cancelClose() {
1326
+ if (this.closeTimer !== null) {
1327
+ window.clearTimeout(this.closeTimer);
1328
+ this.closeTimer = null;
1329
+ }
1330
+ }
1331
+ async position(badge) {
1332
+ const tooltip = this.tooltip;
1333
+ const arrowEl = this.arrowEl;
1334
+ if (!tooltip || !arrowEl) return;
1335
+ const { x, y, placement, middlewareData } = await dom.computePosition(badge, tooltip, {
1336
+ strategy: "fixed",
1337
+ placement: this.side,
1338
+ middleware: [dom.offset(8), dom.flip(), dom.shift({ padding: 8 }), dom.arrow({ element: arrowEl })]
1339
+ });
1340
+ tooltip.style.left = `${x}px`;
1341
+ tooltip.style.top = `${y}px`;
1342
+ positionArrow(arrowEl, placement, middlewareData.arrow);
1343
+ }
1344
+ };
1345
+ function readBadgeConfig(step) {
1346
+ const raw = step.style?.badge;
1347
+ if (!raw || typeof raw !== "object") return { kind: "icon" };
1348
+ const b = raw;
1349
+ const kind = b.kind === "dot" || b.kind === "pill" || b.kind === "image" || b.kind === "icon" ? b.kind : "icon";
1350
+ return {
1351
+ kind,
1352
+ icon: typeof b.icon === "string" ? b.icon : null,
1353
+ text: typeof b.text === "string" ? b.text : null,
1354
+ imageUrl: typeof b.imageUrl === "string" ? b.imageUrl : null,
1355
+ color: typeof b.color === "string" && COLOR_RE.test(b.color.trim()) ? b.color.trim() : null,
1356
+ size: typeof b.size === "number" && Number.isFinite(b.size) ? b.size : null
1357
+ };
1358
+ }
1359
+ function clamp2(n, min, max) {
1360
+ return Math.min(max, Math.max(min, n));
1361
+ }
1362
+ function buildBadgeElement(config, doc) {
1363
+ const btn = doc.createElement("button");
1364
+ btn.type = "button";
1365
+ btn.className = `veo-badge veo-badge--${config.kind}`;
1366
+ btn.setAttribute("aria-label", "M\xE1s informaci\xF3n");
1367
+ const size = clamp2(config.size ?? 18, 8, 48);
1368
+ switch (config.kind) {
1369
+ case "dot":
1370
+ btn.style.width = `${size}px`;
1371
+ btn.style.height = `${size}px`;
1372
+ if (config.color) btn.style.background = config.color;
1373
+ break;
1374
+ case "pill": {
1375
+ btn.textContent = config.text?.slice(0, 24) || "Nuevo";
1376
+ btn.style.fontSize = `${clamp2(Math.round(size * 0.62), 8, 24)}px`;
1377
+ if (config.color) btn.style.background = config.color;
1378
+ break;
1379
+ }
1380
+ case "image": {
1381
+ if (config.imageUrl && isSafeUrl(config.imageUrl)) {
1382
+ const img = doc.createElement("img");
1383
+ img.src = config.imageUrl;
1384
+ img.alt = "";
1385
+ img.style.width = `${size}px`;
1386
+ img.style.height = `${size}px`;
1387
+ btn.appendChild(img);
1388
+ } else {
1389
+ btn.className = "veo-badge veo-badge--icon";
1390
+ btn.textContent = "?";
1391
+ btn.style.width = `${size}px`;
1392
+ btn.style.height = `${size}px`;
1393
+ btn.style.fontSize = `${clamp2(Math.round(size * 0.62), 8, 32)}px`;
1394
+ }
1395
+ break;
1396
+ }
1397
+ default: {
1398
+ btn.textContent = (config.icon || "\u2139\uFE0F").slice(0, 4);
1399
+ btn.style.width = `${size}px`;
1400
+ btn.style.height = `${size}px`;
1401
+ btn.style.fontSize = `${clamp2(Math.round(size * 0.62), 8, 32)}px`;
1402
+ if (config.color) btn.style.color = config.color;
1403
+ break;
1404
+ }
1405
+ }
1406
+ return btn;
1407
+ }
1408
+
1409
+ // src/plugins/guides/walkthrough-block-builder.ts
1410
+ function buildWalkthroughStepContent(step, stepIndex, totalSteps, doc, callbacks) {
1411
+ const container = doc.createElement("div");
1412
+ container.className = "veo-guide-content";
1413
+ const counter = doc.createElement("div");
1414
+ counter.className = "veo-walkthrough-counter";
1415
+ counter.textContent = `Paso ${stepIndex + 1} de ${totalSteps}`;
1416
+ container.appendChild(counter);
1417
+ const progress = doc.createElement("div");
1418
+ progress.className = "veo-walkthrough-progress";
1419
+ for (let i = 0; i < totalSteps; i++) {
1420
+ const dot = doc.createElement("span");
1421
+ dot.className = "veo-walkthrough-progress-dot";
1422
+ if (i < stepIndex) dot.classList.add("completed");
1423
+ if (i === stepIndex) dot.classList.add("active");
1424
+ progress.appendChild(dot);
1425
+ }
1426
+ container.appendChild(progress);
1427
+ if (typeof step.imageUrl === "string" && step.imageUrl && isSafeUrl(step.imageUrl)) {
1428
+ const img = doc.createElement("img");
1429
+ img.className = "veo-guide-image";
1430
+ img.src = step.imageUrl;
1431
+ img.alt = typeof step.title === "string" ? step.title : "";
1432
+ container.appendChild(img);
1433
+ }
1434
+ if (typeof step.title === "string" && step.title) {
1435
+ const heading = doc.createElement("h2");
1436
+ heading.className = "veo-guide-title";
1437
+ heading.textContent = step.title;
1438
+ container.appendChild(heading);
915
1439
  }
916
- /**
917
- * Registra una función a ejecutar en `destroy()`. Útil para limpiar
918
- * listeners globales (scroll, resize) o intervalos.
919
- */
920
- registerCleanup(fn) {
921
- this.cleanups.push(fn);
1440
+ if (typeof step.content === "string" && step.content) {
1441
+ const paragraph = doc.createElement("p");
1442
+ paragraph.className = "veo-guide-text";
1443
+ paragraph.textContent = step.content;
1444
+ container.appendChild(paragraph);
922
1445
  }
923
- /** Remueve el host del DOM y corre todas las funciones de cleanup. */
924
- destroy() {
925
- for (const fn of this.cleanups) {
926
- try {
927
- fn();
928
- } catch {
929
- }
930
- }
931
- this.cleanups = [];
932
- if (this.host?.parentNode) {
933
- this.host.parentNode.removeChild(this.host);
934
- }
935
- this.host = null;
936
- this.shadow = null;
1446
+ const actions = doc.createElement("div");
1447
+ actions.className = "veo-walkthrough-actions";
1448
+ const skipBtn = doc.createElement("button");
1449
+ skipBtn.type = "button";
1450
+ skipBtn.className = "veo-walkthrough-skip";
1451
+ skipBtn.textContent = "Omitir";
1452
+ skipBtn.addEventListener("click", () => callbacks.onSkip());
1453
+ actions.appendChild(skipBtn);
1454
+ const rightGroup = doc.createElement("div");
1455
+ rightGroup.className = "veo-walkthrough-actions-right";
1456
+ if (stepIndex > 0) {
1457
+ const backBtn = doc.createElement("button");
1458
+ backBtn.type = "button";
1459
+ backBtn.className = "veo-walkthrough-btn-secondary";
1460
+ backBtn.textContent = "Atr\xE1s";
1461
+ backBtn.addEventListener("click", () => callbacks.onBack());
1462
+ rightGroup.appendChild(backBtn);
937
1463
  }
938
- };
1464
+ const isLastStep = stepIndex === totalSteps - 1;
1465
+ const primaryBtn = doc.createElement("button");
1466
+ primaryBtn.type = "button";
1467
+ primaryBtn.className = "veo-guide-cta";
1468
+ const defaultLabel = isLastStep ? "Finalizar" : "Siguiente";
1469
+ primaryBtn.textContent = typeof step.ctaText === "string" && step.ctaText ? step.ctaText : defaultLabel;
1470
+ primaryBtn.addEventListener("click", () => {
1471
+ if (isLastStep) callbacks.onComplete();
1472
+ else callbacks.onNext();
1473
+ });
1474
+ rightGroup.appendChild(primaryBtn);
1475
+ actions.appendChild(rightGroup);
1476
+ container.appendChild(actions);
1477
+ container.appendChild(createCloseButton(doc, () => callbacks.onSkip()));
1478
+ return container;
1479
+ }
939
1480
 
940
1481
  // src/plugins/guides/renderers/banner-renderer.ts
941
1482
  var BannerRenderer = class extends BaseRenderer {
942
- render(context) {
1483
+ async render(context) {
943
1484
  const idx = context.nav?.stepIndex ?? 0;
944
1485
  const step = context.guide.guideSteps[idx];
945
1486
  if (!step) return;
946
- const { root } = this.createHost();
1487
+ const position = step.style?.position === "bottom" ? "bottom" : "top";
1488
+ const selector = context.nav ? null : step.selector?.trim() ?? null;
1489
+ let anchor = null;
1490
+ if (selector) {
1491
+ anchor = await waitForElement(selector);
1492
+ if (!anchor) return;
1493
+ }
1494
+ const { host, root } = this.createHost();
947
1495
  this.applyDesign(step.style);
948
1496
  const ownerDocument = root.ownerDocument ?? document;
949
- const position = step.style?.position === "bottom" ? "bottom" : "top";
950
1497
  const banner = ownerDocument.createElement("div");
951
- banner.className = `veo-banner veo-banner-${position}`;
952
- const dismiss = (action, ctaUrl) => {
953
- context.onInteraction({ guideId: context.guide.guideId, stepIndex: idx, action });
1498
+ banner.className = bannerClassOf(position, Boolean(anchor));
1499
+ const dismiss = (action, ctaUrl, meta) => {
1500
+ context.onInteraction({
1501
+ guideId: context.guide.guideId,
1502
+ stepIndex: idx,
1503
+ action,
1504
+ ...meta ? { metadata: meta } : {}
1505
+ });
954
1506
  if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
955
1507
  context.onClose();
956
1508
  };
@@ -961,59 +1513,45 @@ var BannerRenderer = class extends BaseRenderer {
961
1513
  ownerDocument,
962
1514
  context.nav.callbacks
963
1515
  ) : buildStepContent(step, ownerDocument, {
964
- onCtaClick: (action, url) => dismiss("cta_clicked", action === "url" ? url : void 0),
1516
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
965
1517
  onDismiss: () => dismiss("dismissed")
966
1518
  });
967
1519
  banner.appendChild(content);
968
1520
  root.appendChild(banner);
1521
+ if (selector && anchor) {
1522
+ host.style.display = "block";
1523
+ const insertAt = position === "top" ? "prepend" : "append";
1524
+ insertHost(anchor, host, insertAt);
1525
+ this.registerCleanup(keepHostAttached(host, selector, insertAt));
1526
+ }
969
1527
  if (!context.nav) {
970
1528
  this.liveContainer = banner;
971
1529
  this.liveContent = content;
972
1530
  this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
973
- onCtaClick: (action, url) => dismiss("cta_clicked", action === "url" ? url : void 0),
1531
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
974
1532
  onDismiss: () => dismiss("dismissed")
975
1533
  });
1534
+ this.liveKey = this.liveKeyOf(step);
976
1535
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
977
1536
  }
978
1537
  }
1538
+ /** Cambiar de contenedor o de top/bottom embebido requiere re-insertar → remontar. */
1539
+ liveKeyOf(step) {
1540
+ const selector = step.selector?.trim() ?? "";
1541
+ if (!selector) return "";
1542
+ const position = step.style?.position === "bottom" ? "bottom" : "top";
1543
+ return `${selector}|${position}`;
1544
+ }
979
1545
  onLiveUpdate(step) {
980
1546
  if (this.liveContainer) {
981
1547
  const position = step.style?.position === "bottom" ? "bottom" : "top";
982
- this.liveContainer.className = `veo-banner veo-banner-${position}`;
1548
+ const embedded = Boolean(step.selector?.trim());
1549
+ this.liveContainer.className = bannerClassOf(position, embedded);
983
1550
  }
984
1551
  }
985
1552
  };
986
-
987
- // src/plugins/guides/wait-for-element.ts
988
- function waitForElement(selector, timeoutMs = DEFAULT_ANCHOR_WAIT_MS) {
989
- return new Promise((resolve) => {
990
- const safeQuery = () => {
991
- try {
992
- return document.querySelector(selector);
993
- } catch {
994
- return null;
995
- }
996
- };
997
- const existing = safeQuery();
998
- if (existing) {
999
- resolve(existing);
1000
- return;
1001
- }
1002
- let resolved = false;
1003
- const finish = (el) => {
1004
- if (resolved) return;
1005
- resolved = true;
1006
- observer.disconnect();
1007
- clearTimeout(timer);
1008
- resolve(el);
1009
- };
1010
- const observer = new MutationObserver(() => {
1011
- const el = safeQuery();
1012
- if (el) finish(el);
1013
- });
1014
- observer.observe(document.body, { childList: true, subtree: true });
1015
- const timer = setTimeout(() => finish(null), timeoutMs);
1016
- });
1553
+ function bannerClassOf(position, embedded) {
1554
+ return embedded ? "veo-banner veo-banner-embedded" : `veo-banner veo-banner-${position}`;
1017
1555
  }
1018
1556
 
1019
1557
  // src/utils/uuid.ts
@@ -1548,36 +2086,6 @@ var FormRenderer = class extends BaseRenderer {
1548
2086
  }
1549
2087
  };
1550
2088
 
1551
- // src/plugins/guides/inline-host.ts
1552
- function readInlinePosition(style) {
1553
- const p = style?.inlinePosition;
1554
- return p === "before" || p === "prepend" || p === "append" ? p : "after";
1555
- }
1556
- function insertHost(anchor, host, position) {
1557
- switch (position) {
1558
- case "before":
1559
- anchor.before(host);
1560
- break;
1561
- case "after":
1562
- anchor.after(host);
1563
- break;
1564
- case "prepend":
1565
- anchor.prepend(host);
1566
- break;
1567
- case "append":
1568
- anchor.append(host);
1569
- break;
1570
- }
1571
- }
1572
- function keepHostAttached(host, selector, position) {
1573
- const id = window.setInterval(() => {
1574
- if (host.isConnected) return;
1575
- const anchor = document.querySelector(selector);
1576
- if (anchor) insertHost(anchor, host, position);
1577
- }, 1e3);
1578
- return () => window.clearInterval(id);
1579
- }
1580
-
1581
2089
  // src/plugins/guides/renderers/inline-custom-renderer.ts
1582
2090
  var InlineCustomRenderer = class extends BaseRenderer {
1583
2091
  async render(context) {
@@ -1645,13 +2153,18 @@ var InlineRenderer = class extends BaseRenderer {
1645
2153
  const ownerDocument = root.ownerDocument ?? document;
1646
2154
  const card = ownerDocument.createElement("div");
1647
2155
  card.className = "veo-inline";
1648
- const dismiss = (action, ctaUrl) => {
1649
- context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action });
2156
+ const dismiss = (action, ctaUrl, meta) => {
2157
+ context.onInteraction({
2158
+ guideId: context.guide.guideId,
2159
+ stepIndex: 0,
2160
+ action,
2161
+ ...meta ? { metadata: meta } : {}
2162
+ });
1650
2163
  if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
1651
2164
  context.onClose();
1652
2165
  };
1653
2166
  const content = buildStepContent(step, ownerDocument, {
1654
- onCtaClick: (action, url) => dismiss("cta_clicked", action === "url" ? url : void 0),
2167
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
1655
2168
  onDismiss: () => dismiss("dismissed")
1656
2169
  });
1657
2170
  card.appendChild(content);
@@ -1660,7 +2173,7 @@ var InlineRenderer = class extends BaseRenderer {
1660
2173
  this.liveContent = content;
1661
2174
  this.liveKey = `${selector}|${position}`;
1662
2175
  this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
1663
- onCtaClick: (action, url) => dismiss("cta_clicked", action === "url" ? url : void 0),
2176
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
1664
2177
  onDismiss: () => dismiss("dismissed")
1665
2178
  });
1666
2179
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
@@ -1671,7 +2184,14 @@ var InlineRenderer = class extends BaseRenderer {
1671
2184
  };
1672
2185
 
1673
2186
  // src/plugins/guides/renderers/modal-renderer.ts
2187
+ function overlayClassOf(step) {
2188
+ return step.style?.backdrop === false ? "veo-modal-overlay veo-modal-overlay--none" : "veo-modal-overlay";
2189
+ }
1674
2190
  var ModalRenderer = class extends BaseRenderer {
2191
+ constructor() {
2192
+ super(...arguments);
2193
+ this.overlay = null;
2194
+ }
1675
2195
  render(context) {
1676
2196
  const idx = context.nav?.stepIndex ?? 0;
1677
2197
  const step = context.guide.guideSteps[idx];
@@ -1680,11 +2200,17 @@ var ModalRenderer = class extends BaseRenderer {
1680
2200
  this.applyDesign(step.style);
1681
2201
  const ownerDocument = root.ownerDocument ?? document;
1682
2202
  const overlay = ownerDocument.createElement("div");
1683
- overlay.className = "veo-modal-overlay";
2203
+ overlay.className = overlayClassOf(step);
2204
+ this.overlay = overlay;
1684
2205
  const card = ownerDocument.createElement("div");
1685
2206
  card.className = "veo-modal-card";
1686
- const dismiss = (action, ctaUrl) => {
1687
- context.onInteraction({ guideId: context.guide.guideId, stepIndex: idx, action });
2207
+ const dismiss = (action, ctaUrl, meta) => {
2208
+ context.onInteraction({
2209
+ guideId: context.guide.guideId,
2210
+ stepIndex: idx,
2211
+ action,
2212
+ ...meta ? { metadata: meta } : {}
2213
+ });
1688
2214
  if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
1689
2215
  context.onClose();
1690
2216
  };
@@ -1699,7 +2225,7 @@ var ModalRenderer = class extends BaseRenderer {
1699
2225
  ownerDocument,
1700
2226
  context.nav.callbacks
1701
2227
  ) : buildStepContent(step, ownerDocument, {
1702
- onCtaClick: (action, url) => dismiss("cta_clicked", action === "url" ? url : void 0),
2228
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
1703
2229
  onDismiss: () => dismiss("dismissed")
1704
2230
  });
1705
2231
  card.appendChild(content);
@@ -1709,7 +2235,7 @@ var ModalRenderer = class extends BaseRenderer {
1709
2235
  this.liveContainer = card;
1710
2236
  this.liveContent = content;
1711
2237
  this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
1712
- onCtaClick: (action, url) => dismiss("cta_clicked", action === "url" ? url : void 0),
2238
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
1713
2239
  onDismiss: () => dismiss("dismissed")
1714
2240
  });
1715
2241
  }
@@ -1725,26 +2251,14 @@ var ModalRenderer = class extends BaseRenderer {
1725
2251
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
1726
2252
  }
1727
2253
  }
1728
- };
1729
-
1730
- // src/plugins/guides/renderers/floating-arrow.ts
1731
- var STATIC_SIDE = {
1732
- top: "bottom",
1733
- bottom: "top",
1734
- left: "right",
1735
- right: "left"
1736
- };
1737
- function positionArrow(arrowEl, placement, data) {
1738
- const side = STATIC_SIDE[placement.split("-")[0] ?? "bottom"] ?? "top";
1739
- for (const prop of ["top", "bottom", "left", "right"]) {
1740
- arrowEl.style.setProperty(prop, "");
2254
+ onLiveUpdate(step) {
2255
+ if (this.overlay) this.overlay.className = overlayClassOf(step);
1741
2256
  }
1742
- if (data?.x != null) arrowEl.style.setProperty("left", `${data.x}px`);
1743
- if (data?.y != null) arrowEl.style.setProperty("top", `${data.y}px`);
1744
- arrowEl.style.setProperty(side, "-6px");
1745
- }
1746
-
1747
- // src/plugins/guides/renderers/tooltip-renderer.ts
2257
+ destroy() {
2258
+ this.overlay = null;
2259
+ super.destroy();
2260
+ }
2261
+ };
1748
2262
  var TooltipRenderer = class extends BaseRenderer {
1749
2263
  constructor() {
1750
2264
  super(...arguments);
@@ -1766,18 +2280,19 @@ var TooltipRenderer = class extends BaseRenderer {
1766
2280
  const ownerDocument = root.ownerDocument ?? document;
1767
2281
  const tooltip = ownerDocument.createElement("div");
1768
2282
  tooltip.className = "veo-tooltip";
1769
- const dismiss = (action, ctaUrl) => {
2283
+ const dismiss = (action, ctaUrl, meta) => {
1770
2284
  context.onInteraction({
1771
2285
  guideId: context.guide.guideId,
1772
2286
  stepIndex: 0,
1773
- action
2287
+ action,
2288
+ ...meta ? { metadata: meta } : {}
1774
2289
  });
1775
2290
  if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
1776
2291
  context.onClose();
1777
2292
  };
1778
2293
  const content = buildStepContent(step, ownerDocument, {
1779
- onCtaClick: (action, url) => {
1780
- dismiss("cta_clicked", action === "url" ? url : void 0);
2294
+ onCtaClick: (action, url, meta) => {
2295
+ dismiss("cta_clicked", action === "url" ? url : void 0, meta);
1781
2296
  },
1782
2297
  onDismiss: () => dismiss("dismissed")
1783
2298
  });
@@ -1810,7 +2325,7 @@ var TooltipRenderer = class extends BaseRenderer {
1810
2325
  this.liveContent = content;
1811
2326
  this.liveKey = selector;
1812
2327
  this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
1813
- onCtaClick: (action, url) => dismiss("cta_clicked", action === "url" ? url : void 0),
2328
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
1814
2329
  onDismiss: () => dismiss("dismissed")
1815
2330
  });
1816
2331
  context.onInteraction({
@@ -1894,14 +2409,26 @@ var GuidePreviewController = class {
1894
2409
  const nextStep = input.guideSteps[0];
1895
2410
  if (this.singleStep && this.input && input.guideType === this.input.guideType && input.guideType !== "walkthrough" && nextStep && this.singleStep.updateStep(nextStep)) {
1896
2411
  this.input = input;
1897
- return { close: () => this.close(), ready: Promise.resolve({ rendered: true }) };
2412
+ return {
2413
+ close: () => this.close(),
2414
+ ready: Promise.resolve({ rendered: true }),
2415
+ host: () => this.activeHost()
2416
+ };
1898
2417
  }
1899
2418
  this.close();
1900
2419
  this.input = input;
1901
2420
  const guide = normalize(input);
1902
2421
  const start = typeof input.startStepIndex === "number" ? input.startStepIndex : 0;
1903
2422
  const ready = guide.guideType === "walkthrough" ? this.startWalkthrough(guide, start) : this.renderSingleStep(guide);
1904
- return { close: () => this.close(), ready };
2423
+ return { close: () => this.close(), ready, host: () => this.activeHost() };
2424
+ }
2425
+ /** Host de la guía montada (single-step o paso de walkthrough activo). */
2426
+ activeHost() {
2427
+ if (this.singleStep) return this.singleStep.hostElement();
2428
+ if (this.walkthrough instanceof BaseRenderer) {
2429
+ return this.walkthrough.hostElement();
2430
+ }
2431
+ return null;
1905
2432
  }
1906
2433
  close() {
1907
2434
  if (this.singleStep) {
@@ -2022,7 +2549,7 @@ var activeController = null;
2022
2549
  function previewGuide(input) {
2023
2550
  if (!hasDocument()) {
2024
2551
  return { close: () => {
2025
- }, ready: Promise.resolve({ rendered: false }) };
2552
+ }, ready: Promise.resolve({ rendered: false }), host: () => null };
2026
2553
  }
2027
2554
  if (!activeController) activeController = new GuidePreviewController();
2028
2555
  return activeController.preview(input);
@@ -2065,6 +2592,8 @@ function createSingleStepRenderer(type) {
2065
2592
  return new FormRenderer();
2066
2593
  case "inline-form":
2067
2594
  return new InlineFormRenderer();
2595
+ case "badge":
2596
+ return new BadgeRenderer();
2068
2597
  case "walkthrough":
2069
2598
  return null;
2070
2599
  }
@@ -2170,8 +2699,8 @@ function injectBuilderPanel(opts) {
2170
2699
  const ot = top;
2171
2700
  iframe.style.pointerEvents = "none";
2172
2701
  const move = (ev) => {
2173
- left = clamp2(ol + (ev.clientX - sx), 0, Math.max(0, window.innerWidth - card.offsetWidth));
2174
- top = clamp2(ot + (ev.clientY - sy), 0, Math.max(0, window.innerHeight - 40));
2702
+ left = clamp3(ol + (ev.clientX - sx), 0, Math.max(0, window.innerWidth - card.offsetWidth));
2703
+ top = clamp3(ot + (ev.clientY - sy), 0, Math.max(0, window.innerHeight - 40));
2175
2704
  card.style.left = `${left}px`;
2176
2705
  card.style.top = `${top}px`;
2177
2706
  };
@@ -2192,8 +2721,8 @@ function injectBuilderPanel(opts) {
2192
2721
  const sh = card.offsetHeight;
2193
2722
  iframe.style.pointerEvents = "none";
2194
2723
  const move = (ev) => {
2195
- card.style.width = `${clamp2(sw + (ev.clientX - sx), MIN_W, window.innerWidth)}px`;
2196
- card.style.height = `${clamp2(sh + (ev.clientY - sy), MIN_H, window.innerHeight)}px`;
2724
+ card.style.width = `${clamp3(sw + (ev.clientX - sx), MIN_W, window.innerWidth)}px`;
2725
+ card.style.height = `${clamp3(sh + (ev.clientY - sy), MIN_H, window.innerHeight)}px`;
2197
2726
  };
2198
2727
  const up = () => {
2199
2728
  window.removeEventListener("mousemove", move);
@@ -2212,7 +2741,7 @@ function injectBuilderPanel(opts) {
2212
2741
  }
2213
2742
  };
2214
2743
  }
2215
- function clamp2(v, min, max) {
2744
+ function clamp3(v, min, max) {
2216
2745
  return Math.min(Math.max(v, min), max);
2217
2746
  }
2218
2747
  function buildPanelUrl(opts) {
@@ -2264,6 +2793,237 @@ var PANEL_CSS = `
2264
2793
  }
2265
2794
  `;
2266
2795
 
2796
+ // src/plugins/builder/design-manipulator.ts
2797
+ var MIN_W2 = 220;
2798
+ var MAX_W = 720;
2799
+ var MIN_H2 = 120;
2800
+ var MAX_H = 900;
2801
+ var EDGE = 8;
2802
+ var RESIZE_DIRS = ["n", "s", "e", "w", "ne", "nw", "se", "sw"];
2803
+ var MANIPULATOR_CSS = `
2804
+ .veo-mnp-card {
2805
+ outline: 1.5px dashed rgba(255, 91, 53, 0.75);
2806
+ outline-offset: 2px;
2807
+ }
2808
+ .veo-mnp-card:hover { cursor: move; }
2809
+ .veo-mnp-handle {
2810
+ position: absolute;
2811
+ z-index: 10;
2812
+ background: transparent;
2813
+ }
2814
+ .veo-mnp-handle::after {
2815
+ content: '';
2816
+ position: absolute;
2817
+ width: 8px; height: 8px;
2818
+ background: #fff;
2819
+ border: 1.5px solid rgba(255, 91, 53, 0.9);
2820
+ border-radius: 2px;
2821
+ top: 50%; left: 50%;
2822
+ transform: translate(-50%, -50%);
2823
+ opacity: 0;
2824
+ transition: opacity 100ms ease;
2825
+ }
2826
+ .veo-mnp-card:hover .veo-mnp-handle::after,
2827
+ .veo-mnp-handle:hover::after { opacity: 1; }
2828
+ .veo-mnp-n { top: -${EDGE / 2}px; left: ${EDGE}px; right: ${EDGE}px; height: ${EDGE}px; cursor: ns-resize; }
2829
+ .veo-mnp-s { bottom: -${EDGE / 2}px; left: ${EDGE}px; right: ${EDGE}px; height: ${EDGE}px; cursor: ns-resize; }
2830
+ .veo-mnp-e { right: -${EDGE / 2}px; top: ${EDGE}px; bottom: ${EDGE}px; width: ${EDGE}px; cursor: ew-resize; }
2831
+ .veo-mnp-w { left: -${EDGE / 2}px; top: ${EDGE}px; bottom: ${EDGE}px; width: ${EDGE}px; cursor: ew-resize; }
2832
+ .veo-mnp-ne { top: -${EDGE / 2}px; right: -${EDGE / 2}px; width: ${EDGE * 1.5}px; height: ${EDGE * 1.5}px; cursor: nesw-resize; }
2833
+ .veo-mnp-nw { top: -${EDGE / 2}px; left: -${EDGE / 2}px; width: ${EDGE * 1.5}px; height: ${EDGE * 1.5}px; cursor: nwse-resize; }
2834
+ .veo-mnp-se { bottom: -${EDGE / 2}px; right: -${EDGE / 2}px; width: ${EDGE * 1.5}px; height: ${EDGE * 1.5}px; cursor: nwse-resize; }
2835
+ .veo-mnp-sw { bottom: -${EDGE / 2}px; left: -${EDGE / 2}px; width: ${EDGE * 1.5}px; height: ${EDGE * 1.5}px; cursor: nesw-resize; }
2836
+ .veo-mnp-banner { outline: 1.5px dashed rgba(255, 91, 53, 0.75); outline-offset: -2px; cursor: grab; }
2837
+ .veo-mnp-banner:active { cursor: grabbing; }
2838
+ `;
2839
+ function clamp4(v, min, max) {
2840
+ return Math.min(Math.max(v, min), max);
2841
+ }
2842
+ function round3(v) {
2843
+ return Math.round(v * 1e3) / 1e3;
2844
+ }
2845
+ function isInteractive(target) {
2846
+ return Boolean(
2847
+ target instanceof Element && target.closest("button, a, input, textarea, select, label")
2848
+ );
2849
+ }
2850
+ function attachDesignManipulator(opts) {
2851
+ const shadow = opts.host.shadowRoot;
2852
+ if (!shadow) return null;
2853
+ const card = shadow.querySelector(".veo-modal-card");
2854
+ if (card) return attachModal(opts, shadow, card);
2855
+ const banner = shadow.querySelector(".veo-banner");
2856
+ if (banner && !banner.classList.contains("veo-banner-embedded")) {
2857
+ return attachBanner(opts, shadow, banner);
2858
+ }
2859
+ return null;
2860
+ }
2861
+ function attachModal(opts, shadow, card) {
2862
+ if (card.dataset.veoManipulated === "1") return null;
2863
+ card.dataset.veoManipulated = "1";
2864
+ card.classList.add("veo-mnp-card");
2865
+ const doc = card.ownerDocument;
2866
+ const style = doc.createElement("style");
2867
+ style.textContent = MANIPULATOR_CSS;
2868
+ shadow.appendChild(style);
2869
+ const handles = [];
2870
+ for (const dir of RESIZE_DIRS) {
2871
+ const h = doc.createElement("div");
2872
+ h.className = `veo-mnp-handle veo-mnp-${dir}`;
2873
+ h.dataset.veoDir = dir;
2874
+ card.appendChild(h);
2875
+ handles.push(h);
2876
+ }
2877
+ let dragging = false;
2878
+ let detached = false;
2879
+ let patch = {};
2880
+ const setVars = (v) => {
2881
+ const host = opts.host;
2882
+ if (v.px !== void 0) host.style.setProperty("--veo-pos-x", `${v.px * 100}%`);
2883
+ if (v.py !== void 0) host.style.setProperty("--veo-pos-y", `${v.py * 100}%`);
2884
+ if (v.w !== void 0) host.style.setProperty("--veo-width", `${v.w}px`);
2885
+ if (v.h !== void 0) host.style.setProperty("--veo-min-h", `${v.h}px`);
2886
+ };
2887
+ const startGesture = (e, apply) => {
2888
+ e.preventDefault();
2889
+ e.stopPropagation();
2890
+ dragging = true;
2891
+ patch = {};
2892
+ const start = card.getBoundingClientRect();
2893
+ const sx = e.clientX;
2894
+ const sy = e.clientY;
2895
+ let raf = 0;
2896
+ let lastEv = null;
2897
+ const flush = () => {
2898
+ raf = 0;
2899
+ if (!lastEv) return;
2900
+ const next = apply(lastEv.clientX - sx, lastEv.clientY - sy, start);
2901
+ patch = { ...patch, ...next };
2902
+ setVars({
2903
+ ...next.posX !== void 0 ? { px: next.posX } : {},
2904
+ ...next.posY !== void 0 ? { py: next.posY } : {},
2905
+ ...next.width !== void 0 ? { w: next.width } : {},
2906
+ ...next.height !== void 0 ? { h: next.height } : {}
2907
+ });
2908
+ };
2909
+ const move = (ev) => {
2910
+ lastEv = ev;
2911
+ if (typeof window.requestAnimationFrame !== "function") flush();
2912
+ else if (!raf) raf = window.requestAnimationFrame(flush);
2913
+ };
2914
+ const up = () => {
2915
+ window.removeEventListener("mousemove", move);
2916
+ window.removeEventListener("mouseup", up);
2917
+ if (raf) window.cancelAnimationFrame(raf);
2918
+ flush();
2919
+ dragging = false;
2920
+ if (Object.keys(patch).length > 0 && !detached) {
2921
+ opts.onCommit({ stepIndex: opts.stepIndex, style: patch });
2922
+ }
2923
+ opts.onGestureEnd?.();
2924
+ };
2925
+ window.addEventListener("mousemove", move);
2926
+ window.addEventListener("mouseup", up);
2927
+ };
2928
+ const posXFor = (left, w) => {
2929
+ const span = window.innerWidth - w;
2930
+ return span <= 0 ? 0.5 : round3(clamp4(left / span, 0, 1));
2931
+ };
2932
+ const posYFor = (top, h) => {
2933
+ const span = window.innerHeight - h;
2934
+ return span <= 0 ? 0.5 : round3(clamp4(top / span, 0, 1));
2935
+ };
2936
+ const onCardDown = (e) => {
2937
+ if (isInteractive(e.target)) return;
2938
+ if (e.target instanceof Element && e.target.closest(".veo-mnp-handle")) return;
2939
+ startGesture(e, (dx, dy, start) => ({
2940
+ posX: posXFor(start.left + dx, start.width),
2941
+ posY: posYFor(start.top + dy, start.height)
2942
+ }));
2943
+ };
2944
+ const onHandleDown = (e) => {
2945
+ const dir = e.currentTarget.dataset.veoDir;
2946
+ startGesture(e, (dx, dy, start) => {
2947
+ const out = {};
2948
+ if (dir.includes("e")) out.width = Math.round(clamp4(start.width + dx, MIN_W2, MAX_W));
2949
+ if (dir.includes("w")) {
2950
+ out.width = Math.round(clamp4(start.width - dx, MIN_W2, MAX_W));
2951
+ out.posX = posXFor(start.right - out.width, out.width);
2952
+ }
2953
+ if (dir.includes("s")) out.height = Math.round(clamp4(start.height + dy, MIN_H2, MAX_H));
2954
+ if (dir.includes("n")) {
2955
+ out.height = Math.round(clamp4(start.height - dy, MIN_H2, MAX_H));
2956
+ out.posY = posYFor(start.bottom - out.height, out.height);
2957
+ }
2958
+ return out;
2959
+ });
2960
+ };
2961
+ card.addEventListener("mousedown", onCardDown);
2962
+ for (const h of handles) h.addEventListener("mousedown", onHandleDown);
2963
+ return {
2964
+ isDragging: () => dragging,
2965
+ detach: () => {
2966
+ if (detached) return;
2967
+ detached = true;
2968
+ card.removeEventListener("mousedown", onCardDown);
2969
+ for (const h of handles) h.remove();
2970
+ style.remove();
2971
+ card.classList.remove("veo-mnp-card");
2972
+ delete card.dataset.veoManipulated;
2973
+ }
2974
+ };
2975
+ }
2976
+ function attachBanner(opts, shadow, banner) {
2977
+ if (banner.dataset.veoManipulated === "1") return null;
2978
+ banner.dataset.veoManipulated = "1";
2979
+ banner.classList.add("veo-mnp-banner");
2980
+ const doc = banner.ownerDocument;
2981
+ const style = doc.createElement("style");
2982
+ style.textContent = MANIPULATOR_CSS;
2983
+ shadow.appendChild(style);
2984
+ let dragging = false;
2985
+ let detached = false;
2986
+ const positionOf = () => banner.classList.contains("veo-banner-bottom") ? "bottom" : "top";
2987
+ const onDown = (e) => {
2988
+ if (isInteractive(e.target)) return;
2989
+ e.preventDefault();
2990
+ dragging = true;
2991
+ const startPos = positionOf();
2992
+ let current = startPos;
2993
+ const move = (ev) => {
2994
+ const next = ev.clientY < window.innerHeight / 2 ? "top" : "bottom";
2995
+ if (next !== current) {
2996
+ current = next;
2997
+ banner.classList.remove("veo-banner-top", "veo-banner-bottom");
2998
+ banner.classList.add(`veo-banner-${next}`);
2999
+ }
3000
+ };
3001
+ const up = () => {
3002
+ window.removeEventListener("mousemove", move);
3003
+ window.removeEventListener("mouseup", up);
3004
+ dragging = false;
3005
+ if (current !== startPos && !detached) {
3006
+ opts.onCommit({ stepIndex: opts.stepIndex, style: { position: current } });
3007
+ }
3008
+ opts.onGestureEnd?.();
3009
+ };
3010
+ window.addEventListener("mousemove", move);
3011
+ window.addEventListener("mouseup", up);
3012
+ };
3013
+ banner.addEventListener("mousedown", onDown);
3014
+ return {
3015
+ isDragging: () => dragging,
3016
+ detach: () => {
3017
+ if (detached) return;
3018
+ detached = true;
3019
+ banner.removeEventListener("mousedown", onDown);
3020
+ style.remove();
3021
+ banner.classList.remove("veo-mnp-banner");
3022
+ delete banner.dataset.veoManipulated;
3023
+ }
3024
+ };
3025
+ }
3026
+
2267
3027
  // src/plugins/autocapture/constants.ts
2268
3028
  var HASHED_CLASS_PATTERNS = [
2269
3029
  /^css-[a-z0-9]{4,}$/i,
@@ -2556,10 +3316,35 @@ function initBuilderMode() {
2556
3316
  let picker = null;
2557
3317
  let panel = null;
2558
3318
  let staticTarget = null;
3319
+ let manipulator = null;
3320
+ let pendingPreview = null;
2559
3321
  const getTarget = () => panel ? panel.target() : staticTarget;
2560
3322
  const post = (event) => {
2561
3323
  getTarget()?.postMessage({ source: VEO_BUILDER_SOURCE, token, ...event }, dashboardOrigin);
2562
3324
  };
3325
+ const applyPreview = (cmd) => {
3326
+ manipulator?.detach();
3327
+ manipulator = null;
3328
+ const handle = previewGuide(cmd.guide);
3329
+ if (!cmd.editable) return;
3330
+ void handle.ready.then((result) => {
3331
+ if (!result.rendered) return;
3332
+ const host = handle.host();
3333
+ if (!host) return;
3334
+ manipulator = attachDesignManipulator({
3335
+ host,
3336
+ guideType: cmd.guide.guideType,
3337
+ stepIndex: cmd.guide.startStepIndex ?? 0,
3338
+ onCommit: (payload) => post({ type: "design-updated", payload }),
3339
+ onGestureEnd: () => {
3340
+ if (!pendingPreview) return;
3341
+ const queued = pendingPreview;
3342
+ pendingPreview = null;
3343
+ applyPreview(queued);
3344
+ }
3345
+ });
3346
+ });
3347
+ };
2563
3348
  const handleCommand = (cmd) => {
2564
3349
  switch (cmd.type) {
2565
3350
  case "panel-ready":
@@ -2585,9 +3370,14 @@ function initBuilderMode() {
2585
3370
  picker = null;
2586
3371
  break;
2587
3372
  case "preview":
2588
- if (cmd.guide) void previewGuide(cmd.guide).ready;
3373
+ if (!cmd.guide) break;
3374
+ if (manipulator?.isDragging()) pendingPreview = cmd;
3375
+ else applyPreview(cmd);
2589
3376
  break;
2590
3377
  case "close-preview":
3378
+ manipulator?.detach();
3379
+ manipulator = null;
3380
+ pendingPreview = null;
2591
3381
  closeGuidePreview();
2592
3382
  break;
2593
3383
  case "teardown":
@@ -2607,6 +3397,9 @@ function initBuilderMode() {
2607
3397
  window.removeEventListener("pagehide", onPageHide);
2608
3398
  picker?.stop();
2609
3399
  picker = null;
3400
+ manipulator?.detach();
3401
+ manipulator = null;
3402
+ pendingPreview = null;
2610
3403
  closeGuidePreview();
2611
3404
  post({ type: "closed" });
2612
3405
  panel?.teardown();