veo-sdk 0.3.16 → 0.4.1

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