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/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};
@@ -854,6 +1046,37 @@ var BaseRenderer = class {
854
1046
  this.host = null;
855
1047
  this.shadow = null;
856
1048
  this.cleanups = [];
1049
+ // ── Update in-place (preview en vivo del editor) ──
1050
+ // La subclase setea estas piezas en render() para habilitarlo: el contenedor
1051
+ // del contenido, el nodo de contenido actual y cómo reconstruirlo.
1052
+ this.liveContainer = null;
1053
+ this.liveContent = null;
1054
+ this.liveBuild = null;
1055
+ /** Clave de anclaje/estructura fijada en render(); si cambia → remontar. */
1056
+ this.liveKey = "";
1057
+ }
1058
+ /** Clave de anclaje derivada del step (override por subclase). */
1059
+ liveKeyOf(_step) {
1060
+ return "";
1061
+ }
1062
+ /** Hook post-swap (reposicionar tooltip, actualizar clase del banner…). */
1063
+ onLiveUpdate(_step) {
1064
+ }
1065
+ /**
1066
+ * Actualiza el contenido del step SIN remontar el host — así el preview del
1067
+ * editor no parpadea en cada tecla. Devuelve `false` cuando el renderer no
1068
+ * lo soporta o el cambio requiere remontar (cambió el ancla/estructura);
1069
+ * en ese caso el caller hace el re-render completo.
1070
+ */
1071
+ updateStep(step) {
1072
+ if (!this.host || !this.liveContainer || !this.liveContent || !this.liveBuild) return false;
1073
+ if (this.liveKeyOf(step) !== this.liveKey) return false;
1074
+ const next = this.liveBuild(step);
1075
+ this.liveContainer.replaceChild(next, this.liveContent);
1076
+ this.liveContent = next;
1077
+ this.applyDesign(step.style);
1078
+ this.onLiveUpdate(step);
1079
+ return true;
857
1080
  }
858
1081
  /**
859
1082
  * Crea un `<div data-veo-guide>` adjunto a `document.body`, le adjunta
@@ -882,44 +1105,404 @@ var BaseRenderer = class {
882
1105
  applyDesign(style) {
883
1106
  if (this.host) applyDesignVars(this.host, style);
884
1107
  }
885
- /**
886
- * Registra una función a ejecutar en `destroy()`. Útil para limpiar
887
- * listeners globales (scroll, resize) o intervalos.
888
- */
889
- registerCleanup(fn) {
890
- this.cleanups.push(fn);
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);
1439
+ }
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);
891
1445
  }
892
- /** Remueve el host del DOM y corre todas las funciones de cleanup. */
893
- destroy() {
894
- for (const fn of this.cleanups) {
895
- try {
896
- fn();
897
- } catch {
898
- }
899
- }
900
- this.cleanups = [];
901
- if (this.host?.parentNode) {
902
- this.host.parentNode.removeChild(this.host);
903
- }
904
- this.host = null;
905
- 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);
906
1463
  }
907
- };
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
+ }
908
1480
 
909
1481
  // src/plugins/guides/renderers/banner-renderer.ts
910
1482
  var BannerRenderer = class extends BaseRenderer {
911
- render(context) {
1483
+ async render(context) {
912
1484
  const idx = context.nav?.stepIndex ?? 0;
913
1485
  const step = context.guide.guideSteps[idx];
914
1486
  if (!step) return;
915
- 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();
916
1495
  this.applyDesign(step.style);
917
1496
  const ownerDocument = root.ownerDocument ?? document;
918
- const position = step.style?.position === "bottom" ? "bottom" : "top";
919
1497
  const banner = ownerDocument.createElement("div");
920
- banner.className = `veo-banner veo-banner-${position}`;
921
- const dismiss = (action, ctaUrl) => {
922
- 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
+ });
923
1506
  if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
924
1507
  context.onClose();
925
1508
  };
@@ -930,47 +1513,45 @@ var BannerRenderer = class extends BaseRenderer {
930
1513
  ownerDocument,
931
1514
  context.nav.callbacks
932
1515
  ) : buildStepContent(step, ownerDocument, {
933
- 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),
934
1517
  onDismiss: () => dismiss("dismissed")
935
1518
  });
936
1519
  banner.appendChild(content);
937
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
+ }
938
1527
  if (!context.nav) {
1528
+ this.liveContainer = banner;
1529
+ this.liveContent = content;
1530
+ this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
1531
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
1532
+ onDismiss: () => dismiss("dismissed")
1533
+ });
1534
+ this.liveKey = this.liveKeyOf(step);
939
1535
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
940
1536
  }
941
1537
  }
942
- };
943
-
944
- // src/plugins/guides/wait-for-element.ts
945
- function waitForElement(selector, timeoutMs = DEFAULT_ANCHOR_WAIT_MS) {
946
- return new Promise((resolve) => {
947
- const safeQuery = () => {
948
- try {
949
- return document.querySelector(selector);
950
- } catch {
951
- return null;
952
- }
953
- };
954
- const existing = safeQuery();
955
- if (existing) {
956
- resolve(existing);
957
- return;
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
+ }
1545
+ onLiveUpdate(step) {
1546
+ if (this.liveContainer) {
1547
+ const position = step.style?.position === "bottom" ? "bottom" : "top";
1548
+ const embedded = Boolean(step.selector?.trim());
1549
+ this.liveContainer.className = bannerClassOf(position, embedded);
958
1550
  }
959
- let resolved = false;
960
- const finish = (el) => {
961
- if (resolved) return;
962
- resolved = true;
963
- observer.disconnect();
964
- clearTimeout(timer);
965
- resolve(el);
966
- };
967
- const observer = new MutationObserver(() => {
968
- const el = safeQuery();
969
- if (el) finish(el);
970
- });
971
- observer.observe(document.body, { childList: true, subtree: true });
972
- const timer = setTimeout(() => finish(null), timeoutMs);
973
- });
1551
+ }
1552
+ };
1553
+ function bannerClassOf(position, embedded) {
1554
+ return embedded ? "veo-banner veo-banner-embedded" : `veo-banner veo-banner-${position}`;
974
1555
  }
975
1556
 
976
1557
  // src/utils/uuid.ts
@@ -1505,36 +2086,6 @@ var FormRenderer = class extends BaseRenderer {
1505
2086
  }
1506
2087
  };
1507
2088
 
1508
- // src/plugins/guides/inline-host.ts
1509
- function readInlinePosition(style) {
1510
- const p = style?.inlinePosition;
1511
- return p === "before" || p === "prepend" || p === "append" ? p : "after";
1512
- }
1513
- function insertHost(anchor, host, position) {
1514
- switch (position) {
1515
- case "before":
1516
- anchor.before(host);
1517
- break;
1518
- case "after":
1519
- anchor.after(host);
1520
- break;
1521
- case "prepend":
1522
- anchor.prepend(host);
1523
- break;
1524
- case "append":
1525
- anchor.append(host);
1526
- break;
1527
- }
1528
- }
1529
- function keepHostAttached(host, selector, position) {
1530
- const id = window.setInterval(() => {
1531
- if (host.isConnected) return;
1532
- const anchor = document.querySelector(selector);
1533
- if (anchor) insertHost(anchor, host, position);
1534
- }, 1e3);
1535
- return () => window.clearInterval(id);
1536
- }
1537
-
1538
2089
  // src/plugins/guides/renderers/inline-custom-renderer.ts
1539
2090
  var InlineCustomRenderer = class extends BaseRenderer {
1540
2091
  async render(context) {
@@ -1602,23 +2153,45 @@ var InlineRenderer = class extends BaseRenderer {
1602
2153
  const ownerDocument = root.ownerDocument ?? document;
1603
2154
  const card = ownerDocument.createElement("div");
1604
2155
  card.className = "veo-inline";
1605
- const dismiss = (action, ctaUrl) => {
1606
- 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
+ });
1607
2163
  if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
1608
2164
  context.onClose();
1609
2165
  };
1610
2166
  const content = buildStepContent(step, ownerDocument, {
1611
- 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),
1612
2168
  onDismiss: () => dismiss("dismissed")
1613
2169
  });
1614
2170
  card.appendChild(content);
1615
2171
  root.appendChild(card);
2172
+ this.liveContainer = card;
2173
+ this.liveContent = content;
2174
+ this.liveKey = `${selector}|${position}`;
2175
+ this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
2176
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
2177
+ onDismiss: () => dismiss("dismissed")
2178
+ });
1616
2179
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
1617
2180
  }
2181
+ liveKeyOf(step) {
2182
+ return `${step.selector ?? ""}|${readInlinePosition(step.style)}`;
2183
+ }
1618
2184
  };
1619
2185
 
1620
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
+ }
1621
2190
  var ModalRenderer = class extends BaseRenderer {
2191
+ constructor() {
2192
+ super(...arguments);
2193
+ this.overlay = null;
2194
+ }
1622
2195
  render(context) {
1623
2196
  const idx = context.nav?.stepIndex ?? 0;
1624
2197
  const step = context.guide.guideSteps[idx];
@@ -1627,11 +2200,17 @@ var ModalRenderer = class extends BaseRenderer {
1627
2200
  this.applyDesign(step.style);
1628
2201
  const ownerDocument = root.ownerDocument ?? document;
1629
2202
  const overlay = ownerDocument.createElement("div");
1630
- overlay.className = "veo-modal-overlay";
2203
+ overlay.className = overlayClassOf(step);
2204
+ this.overlay = overlay;
1631
2205
  const card = ownerDocument.createElement("div");
1632
2206
  card.className = "veo-modal-card";
1633
- const dismiss = (action, ctaUrl) => {
1634
- 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
+ });
1635
2214
  if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
1636
2215
  context.onClose();
1637
2216
  };
@@ -1646,12 +2225,20 @@ var ModalRenderer = class extends BaseRenderer {
1646
2225
  ownerDocument,
1647
2226
  context.nav.callbacks
1648
2227
  ) : buildStepContent(step, ownerDocument, {
1649
- 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),
1650
2229
  onDismiss: () => dismiss("dismissed")
1651
2230
  });
1652
2231
  card.appendChild(content);
1653
2232
  overlay.appendChild(card);
1654
2233
  root.appendChild(overlay);
2234
+ if (!context.nav) {
2235
+ this.liveContainer = card;
2236
+ this.liveContent = content;
2237
+ this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
2238
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
2239
+ onDismiss: () => dismiss("dismissed")
2240
+ });
2241
+ }
1655
2242
  overlay.addEventListener("click", (e) => {
1656
2243
  if (e.target === overlay) onBackdropOrEsc();
1657
2244
  });
@@ -1664,27 +2251,21 @@ var ModalRenderer = class extends BaseRenderer {
1664
2251
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
1665
2252
  }
1666
2253
  }
1667
- };
1668
-
1669
- // src/plugins/guides/renderers/floating-arrow.ts
1670
- var STATIC_SIDE = {
1671
- top: "bottom",
1672
- bottom: "top",
1673
- left: "right",
1674
- right: "left"
1675
- };
1676
- function positionArrow(arrowEl, placement, data) {
1677
- const side = STATIC_SIDE[placement.split("-")[0] ?? "bottom"] ?? "top";
1678
- for (const prop of ["top", "bottom", "left", "right"]) {
1679
- arrowEl.style.setProperty(prop, "");
2254
+ onLiveUpdate(step) {
2255
+ if (this.overlay) this.overlay.className = overlayClassOf(step);
1680
2256
  }
1681
- if (data?.x != null) arrowEl.style.setProperty("left", `${data.x}px`);
1682
- if (data?.y != null) arrowEl.style.setProperty("top", `${data.y}px`);
1683
- arrowEl.style.setProperty(side, "-6px");
1684
- }
1685
-
1686
- // src/plugins/guides/renderers/tooltip-renderer.ts
2257
+ destroy() {
2258
+ this.overlay = null;
2259
+ super.destroy();
2260
+ }
2261
+ };
1687
2262
  var TooltipRenderer = class extends BaseRenderer {
2263
+ constructor() {
2264
+ super(...arguments);
2265
+ /** Lado preferido (mutable: el update in-place puede cambiarlo). */
2266
+ this.side = "bottom";
2267
+ this.reposition = null;
2268
+ }
1688
2269
  async render(context) {
1689
2270
  const step = context.guide.guideSteps[0];
1690
2271
  if (!step) return;
@@ -1693,24 +2274,25 @@ var TooltipRenderer = class extends BaseRenderer {
1693
2274
  const anchor = await waitForElement(selector);
1694
2275
  if (!anchor) return;
1695
2276
  const rawSide = step.style?.tooltipPlacement;
1696
- const side = rawSide === "top" || rawSide === "left" || rawSide === "right" ? rawSide : "bottom";
2277
+ this.side = rawSide === "top" || rawSide === "left" || rawSide === "right" ? rawSide : "bottom";
1697
2278
  const { root } = this.createHost();
1698
2279
  this.applyDesign(step.style);
1699
2280
  const ownerDocument = root.ownerDocument ?? document;
1700
2281
  const tooltip = ownerDocument.createElement("div");
1701
2282
  tooltip.className = "veo-tooltip";
1702
- const dismiss = (action, ctaUrl) => {
2283
+ const dismiss = (action, ctaUrl, meta) => {
1703
2284
  context.onInteraction({
1704
2285
  guideId: context.guide.guideId,
1705
2286
  stepIndex: 0,
1706
- action
2287
+ action,
2288
+ ...meta ? { metadata: meta } : {}
1707
2289
  });
1708
2290
  if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
1709
2291
  context.onClose();
1710
2292
  };
1711
2293
  const content = buildStepContent(step, ownerDocument, {
1712
- onCtaClick: (action, url) => {
1713
- dismiss("cta_clicked", action === "url" ? url : void 0);
2294
+ onCtaClick: (action, url, meta) => {
2295
+ dismiss("cta_clicked", action === "url" ? url : void 0, meta);
1714
2296
  },
1715
2297
  onDismiss: () => dismiss("dismissed")
1716
2298
  });
@@ -1721,7 +2303,7 @@ var TooltipRenderer = class extends BaseRenderer {
1721
2303
  root.appendChild(tooltip);
1722
2304
  const updatePosition = async () => {
1723
2305
  const { x, y, placement, middlewareData } = await dom.computePosition(anchor, tooltip, {
1724
- placement: side,
2306
+ placement: this.side,
1725
2307
  middleware: [dom.offset(10), dom.flip(), dom.shift({ padding: 8 }), dom.arrow({ element: arrowEl })]
1726
2308
  });
1727
2309
  tooltip.style.left = `${x}px`;
@@ -1732,18 +2314,34 @@ var TooltipRenderer = class extends BaseRenderer {
1732
2314
  const reposition = () => {
1733
2315
  void updatePosition();
1734
2316
  };
2317
+ this.reposition = reposition;
1735
2318
  window.addEventListener("scroll", reposition, true);
1736
2319
  window.addEventListener("resize", reposition);
1737
2320
  this.registerCleanup(() => {
1738
2321
  window.removeEventListener("scroll", reposition, true);
1739
2322
  window.removeEventListener("resize", reposition);
1740
2323
  });
2324
+ this.liveContainer = tooltip;
2325
+ this.liveContent = content;
2326
+ this.liveKey = selector;
2327
+ this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
2328
+ onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
2329
+ onDismiss: () => dismiss("dismissed")
2330
+ });
1741
2331
  context.onInteraction({
1742
2332
  guideId: context.guide.guideId,
1743
2333
  stepIndex: 0,
1744
2334
  action: "shown"
1745
2335
  });
1746
2336
  }
2337
+ liveKeyOf(step) {
2338
+ return step.selector ?? "";
2339
+ }
2340
+ onLiveUpdate(step) {
2341
+ const raw = step.style?.tooltipPlacement;
2342
+ this.side = raw === "top" || raw === "left" || raw === "right" ? raw : "bottom";
2343
+ this.reposition?.();
2344
+ }
1747
2345
  };
1748
2346
  var WalkthroughRenderer = class extends BaseRenderer {
1749
2347
  async render(context) {
@@ -1808,12 +2406,29 @@ var GuidePreviewController = class {
1808
2406
  this.input = null;
1809
2407
  }
1810
2408
  preview(input) {
2409
+ const nextStep = input.guideSteps[0];
2410
+ if (this.singleStep && this.input && input.guideType === this.input.guideType && input.guideType !== "walkthrough" && nextStep && this.singleStep.updateStep(nextStep)) {
2411
+ this.input = input;
2412
+ return {
2413
+ close: () => this.close(),
2414
+ ready: Promise.resolve({ rendered: true }),
2415
+ host: () => this.activeHost()
2416
+ };
2417
+ }
1811
2418
  this.close();
1812
2419
  this.input = input;
1813
2420
  const guide = normalize(input);
1814
2421
  const start = typeof input.startStepIndex === "number" ? input.startStepIndex : 0;
1815
2422
  const ready = guide.guideType === "walkthrough" ? this.startWalkthrough(guide, start) : this.renderSingleStep(guide);
1816
- 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;
1817
2432
  }
1818
2433
  close() {
1819
2434
  if (this.singleStep) {
@@ -1934,7 +2549,7 @@ var activeController = null;
1934
2549
  function previewGuide(input) {
1935
2550
  if (!hasDocument()) {
1936
2551
  return { close: () => {
1937
- }, ready: Promise.resolve({ rendered: false }) };
2552
+ }, ready: Promise.resolve({ rendered: false }), host: () => null };
1938
2553
  }
1939
2554
  if (!activeController) activeController = new GuidePreviewController();
1940
2555
  return activeController.preview(input);
@@ -1977,6 +2592,8 @@ function createSingleStepRenderer(type) {
1977
2592
  return new FormRenderer();
1978
2593
  case "inline-form":
1979
2594
  return new InlineFormRenderer();
2595
+ case "badge":
2596
+ return new BadgeRenderer();
1980
2597
  case "walkthrough":
1981
2598
  return null;
1982
2599
  }
@@ -2082,8 +2699,8 @@ function injectBuilderPanel(opts) {
2082
2699
  const ot = top;
2083
2700
  iframe.style.pointerEvents = "none";
2084
2701
  const move = (ev) => {
2085
- left = clamp2(ol + (ev.clientX - sx), 0, Math.max(0, window.innerWidth - card.offsetWidth));
2086
- 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));
2087
2704
  card.style.left = `${left}px`;
2088
2705
  card.style.top = `${top}px`;
2089
2706
  };
@@ -2104,8 +2721,8 @@ function injectBuilderPanel(opts) {
2104
2721
  const sh = card.offsetHeight;
2105
2722
  iframe.style.pointerEvents = "none";
2106
2723
  const move = (ev) => {
2107
- card.style.width = `${clamp2(sw + (ev.clientX - sx), MIN_W, window.innerWidth)}px`;
2108
- 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`;
2109
2726
  };
2110
2727
  const up = () => {
2111
2728
  window.removeEventListener("mousemove", move);
@@ -2124,7 +2741,7 @@ function injectBuilderPanel(opts) {
2124
2741
  }
2125
2742
  };
2126
2743
  }
2127
- function clamp2(v, min, max) {
2744
+ function clamp3(v, min, max) {
2128
2745
  return Math.min(Math.max(v, min), max);
2129
2746
  }
2130
2747
  function buildPanelUrl(opts) {
@@ -2176,6 +2793,237 @@ var PANEL_CSS = `
2176
2793
  }
2177
2794
  `;
2178
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
+
2179
3027
  // src/plugins/autocapture/constants.ts
2180
3028
  var HASHED_CLASS_PATTERNS = [
2181
3029
  /^css-[a-z0-9]{4,}$/i,
@@ -2468,10 +3316,35 @@ function initBuilderMode() {
2468
3316
  let picker = null;
2469
3317
  let panel = null;
2470
3318
  let staticTarget = null;
3319
+ let manipulator = null;
3320
+ let pendingPreview = null;
2471
3321
  const getTarget = () => panel ? panel.target() : staticTarget;
2472
3322
  const post = (event) => {
2473
3323
  getTarget()?.postMessage({ source: VEO_BUILDER_SOURCE, token, ...event }, dashboardOrigin);
2474
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
+ };
2475
3348
  const handleCommand = (cmd) => {
2476
3349
  switch (cmd.type) {
2477
3350
  case "panel-ready":
@@ -2497,9 +3370,14 @@ function initBuilderMode() {
2497
3370
  picker = null;
2498
3371
  break;
2499
3372
  case "preview":
2500
- if (cmd.guide) void previewGuide(cmd.guide).ready;
3373
+ if (!cmd.guide) break;
3374
+ if (manipulator?.isDragging()) pendingPreview = cmd;
3375
+ else applyPreview(cmd);
2501
3376
  break;
2502
3377
  case "close-preview":
3378
+ manipulator?.detach();
3379
+ manipulator = null;
3380
+ pendingPreview = null;
2503
3381
  closeGuidePreview();
2504
3382
  break;
2505
3383
  case "teardown":
@@ -2519,6 +3397,9 @@ function initBuilderMode() {
2519
3397
  window.removeEventListener("pagehide", onPageHide);
2520
3398
  picker?.stop();
2521
3399
  picker = null;
3400
+ manipulator?.detach();
3401
+ manipulator = null;
3402
+ pendingPreview = null;
2522
3403
  closeGuidePreview();
2523
3404
  post({ type: "closed" });
2524
3405
  panel?.teardown();