veo-sdk 0.1.0 → 0.2.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.
@@ -0,0 +1,2728 @@
1
+ import { computePosition, offset, flip, shift, arrow } from '@floating-ui/dom';
2
+
3
+ // src/utils/safe-env.ts
4
+ function hasWindow() {
5
+ return typeof window !== "undefined";
6
+ }
7
+ function hasDocument() {
8
+ return typeof document !== "undefined";
9
+ }
10
+ function hasLocalStorage() {
11
+ try {
12
+ if (typeof localStorage === "undefined") return false;
13
+ const testKey = "__veo_test__";
14
+ localStorage.setItem(testKey, "1");
15
+ localStorage.removeItem(testKey);
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+ function hasNavigatorSendBeacon() {
22
+ return typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function";
23
+ }
24
+
25
+ // src/plugins/guides/constants.ts
26
+ var GUIDE_Z_INDEX = 2147483640;
27
+ var DEFAULT_ANCHOR_WAIT_MS = 5e3;
28
+ var SAFE_URL_PROTOCOLS = ["http:", "https:"];
29
+ var GUIDE_HOST_ATTR = "data-veo-guide";
30
+ var FREQUENCY_CACHE_KEY_PREFIX = "veo:freq:";
31
+ var DEFAULT_TRACKER_FLUSH_INTERVAL_MS = 3e3;
32
+ var DEFAULT_TRACKER_BATCH_SIZE = 5;
33
+ var DEFAULT_TRACKER_MAX_RETRIES = 3;
34
+ var FREQUENCY_CACHE_MAX_ENTRIES = 500;
35
+ var FREQUENCY_CACHE_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
36
+ var WALKTHROUGH_STATE_KEY_PREFIX = "veo:walkthrough_state:";
37
+ var WALKTHROUGH_ABANDONMENT_TIMEOUT_MS = 30 * 60 * 1e3;
38
+ var WALKTHROUGH_STEP_SELECTOR_TIMEOUT_MS = 5e3;
39
+
40
+ // src/plugins/guides/block-builder.ts
41
+ function buildStepContent(step, doc, callbacks) {
42
+ const container = doc.createElement("div");
43
+ container.className = "veo-guide-content";
44
+ if (typeof step.imageUrl === "string" && step.imageUrl && isSafeUrl(step.imageUrl)) {
45
+ const img = doc.createElement("img");
46
+ img.className = "veo-guide-image";
47
+ img.src = step.imageUrl;
48
+ img.alt = typeof step.title === "string" ? step.title : "";
49
+ container.appendChild(img);
50
+ }
51
+ if (typeof step.title === "string" && step.title) {
52
+ const heading = doc.createElement("h2");
53
+ heading.className = "veo-guide-title";
54
+ heading.textContent = step.title;
55
+ container.appendChild(heading);
56
+ }
57
+ if (typeof step.content === "string" && step.content) {
58
+ const paragraph = doc.createElement("p");
59
+ paragraph.className = "veo-guide-text";
60
+ paragraph.textContent = step.content;
61
+ container.appendChild(paragraph);
62
+ }
63
+ const actions = doc.createElement("div");
64
+ actions.className = "veo-guide-actions";
65
+ if (typeof step.ctaText === "string" && step.ctaText) {
66
+ const cta = doc.createElement("button");
67
+ cta.type = "button";
68
+ cta.className = "veo-guide-cta";
69
+ cta.textContent = step.ctaText;
70
+ cta.addEventListener("click", () => {
71
+ const action = step.ctaAction ?? "dismiss";
72
+ const ctaUrl = readCtaUrl(step);
73
+ callbacks.onCtaClick(action, ctaUrl);
74
+ });
75
+ actions.appendChild(cta);
76
+ }
77
+ container.appendChild(actions);
78
+ const closeBtn = doc.createElement("button");
79
+ closeBtn.type = "button";
80
+ closeBtn.className = "veo-guide-close";
81
+ closeBtn.setAttribute("aria-label", "Cerrar");
82
+ closeBtn.textContent = "\xD7";
83
+ closeBtn.addEventListener("click", () => callbacks.onDismiss());
84
+ container.appendChild(closeBtn);
85
+ return container;
86
+ }
87
+ function isSafeUrl(url) {
88
+ if (typeof url !== "string" || url.length === 0) return false;
89
+ try {
90
+ const base = typeof window !== "undefined" ? window.location.href : "http://localhost/";
91
+ const parsed = new URL(url, base);
92
+ return SAFE_URL_PROTOCOLS.includes(parsed.protocol);
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+ function readCtaUrl(step) {
98
+ const candidate = step.style?.ctaUrl;
99
+ if (typeof candidate !== "string") return void 0;
100
+ return isSafeUrl(candidate) ? candidate : void 0;
101
+ }
102
+
103
+ // src/plugins/guides/guide-design.ts
104
+ var DARK_THEME = {
105
+ "--veo-bg": "#1f2937",
106
+ "--veo-text": "#f9fafb",
107
+ "--veo-text-secondary": "#d1d5db",
108
+ "--veo-shadow": "0 20px 60px rgba(0, 0, 0, 0.55)"
109
+ };
110
+ var ALIGN_JUSTIFY = {
111
+ left: "flex-start",
112
+ center: "center",
113
+ right: "flex-end"
114
+ };
115
+ var SHADOW = {
116
+ none: "none",
117
+ soft: "0 4px 14px rgba(0, 0, 0, 0.10)",
118
+ medium: "0 8px 30px rgba(0, 0, 0, 0.18)",
119
+ strong: "0 24px 60px rgba(0, 0, 0, 0.32)"
120
+ };
121
+ var PRESET_POS = {
122
+ center: [0.5, 0.5],
123
+ top: [0.5, 0],
124
+ bottom: [0.5, 1],
125
+ left: [0, 0.5],
126
+ right: [1, 0.5],
127
+ "top-left": [0, 0],
128
+ "top-right": [1, 0],
129
+ "bottom-left": [0, 1],
130
+ "bottom-right": [1, 1]
131
+ };
132
+ var COLOR_RE = /^#[0-9a-f]{3,8}$|^(rgb|rgba|hsl|hsla)\([\d.,%/\sdeg]+\)$|^[a-z]{3,20}$/i;
133
+ function clamp(n, min, max) {
134
+ return Math.min(max, Math.max(min, n));
135
+ }
136
+ function applyDesignVars(host, style) {
137
+ if (!style || typeof style !== "object") return;
138
+ const s = style;
139
+ if (typeof s.accentColor === "string" && COLOR_RE.test(s.accentColor.trim())) {
140
+ host.style.setProperty("--veo-primary", s.accentColor.trim());
141
+ }
142
+ if (s.theme === "dark") {
143
+ for (const [key, value] of Object.entries(DARK_THEME)) {
144
+ host.style.setProperty(key, value);
145
+ }
146
+ }
147
+ if (typeof s.radius === "number" && Number.isFinite(s.radius)) {
148
+ host.style.setProperty("--veo-radius", `${clamp(s.radius, 0, 48)}px`);
149
+ }
150
+ if (typeof s.width === "number" && Number.isFinite(s.width)) {
151
+ host.style.setProperty("--veo-width", `${clamp(s.width, 220, 720)}px`);
152
+ }
153
+ if (s.align === "left" || s.align === "center" || s.align === "right") {
154
+ host.style.setProperty("--veo-actions-justify", ALIGN_JUSTIFY[s.align]);
155
+ }
156
+ if (typeof s.padding === "number" && Number.isFinite(s.padding)) {
157
+ host.style.setProperty("--veo-pad", `${clamp(s.padding, 0, 64)}px`);
158
+ }
159
+ if (typeof s.margin === "number" && Number.isFinite(s.margin)) {
160
+ host.style.setProperty("--veo-margin", `${clamp(s.margin, 0, 64)}px`);
161
+ }
162
+ if (typeof s.borderWidth === "number" && Number.isFinite(s.borderWidth)) {
163
+ host.style.setProperty("--veo-border-width", `${clamp(s.borderWidth, 0, 16)}px`);
164
+ }
165
+ if (typeof s.borderColor === "string" && COLOR_RE.test(s.borderColor.trim())) {
166
+ host.style.setProperty("--veo-border-color", s.borderColor.trim());
167
+ }
168
+ if (typeof s.shadow === "string" && SHADOW[s.shadow]) {
169
+ host.style.setProperty("--veo-shadow", SHADOW[s.shadow]);
170
+ }
171
+ let px;
172
+ let py;
173
+ if (typeof s.posX === "number" && typeof s.posY === "number") {
174
+ px = clamp(s.posX, 0, 1);
175
+ py = clamp(s.posY, 0, 1);
176
+ } else if (typeof s.overlayPosition === "string" && PRESET_POS[s.overlayPosition]) {
177
+ [px, py] = PRESET_POS[s.overlayPosition];
178
+ }
179
+ if (px !== void 0 && py !== void 0) {
180
+ host.style.setProperty("--veo-pos-x", `${px * 100}%`);
181
+ host.style.setProperty("--veo-pos-y", `${py * 100}%`);
182
+ }
183
+ }
184
+
185
+ // src/plugins/guides/styles.ts
186
+ var GUIDE_STYLES = `
187
+ :host {
188
+ --veo-primary: #4f46e5;
189
+ --veo-text: #1f2937;
190
+ --veo-text-secondary: #4b5563;
191
+ --veo-bg: #ffffff;
192
+ --veo-radius: 12px;
193
+ --veo-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
194
+ --veo-width: 420px;
195
+ --veo-actions-justify: flex-end;
196
+ all: initial;
197
+ }
198
+ * { box-sizing: border-box; font-family: -apple-system, system-ui, "Segoe UI", Roboto, sans-serif; }
199
+
200
+ .veo-modal-overlay {
201
+ position: fixed; inset: 0;
202
+ background: rgba(15, 15, 30, 0.45);
203
+ z-index: ${GUIDE_Z_INDEX};
204
+ animation: veo-fade-in 180ms ease-out;
205
+ }
206
+ /*
207
+ * Posici\xF3n libre/preset: --veo-pos-x/y son porcentajes (default 50% = centro).
208
+ * El truco translate(-pos) alinea la MISMA fracci\xF3n de la tarjeta con esa
209
+ * fracci\xF3n del overlay, as\xED la tarjeta nunca se sale (0% = pegada izquierda,
210
+ * 100% = pegada derecha, 50% = centrada) sea cual sea su ancho.
211
+ */
212
+ .veo-modal-card {
213
+ position: absolute;
214
+ left: var(--veo-pos-x, 50%);
215
+ top: var(--veo-pos-y, 50%);
216
+ transform: translate(calc(var(--veo-pos-x, 50%) * -1), calc(var(--veo-pos-y, 50%) * -1));
217
+ background: var(--veo-bg);
218
+ border-radius: var(--veo-radius);
219
+ border: var(--veo-border-width, 0) solid var(--veo-border-color, transparent);
220
+ padding: var(--veo-pad, 24px);
221
+ max-width: var(--veo-width); width: 90%;
222
+ box-shadow: var(--veo-shadow);
223
+ animation: veo-fade-in 180ms ease-out;
224
+ }
225
+
226
+ .veo-banner {
227
+ position: fixed; left: 0; right: 0;
228
+ background: var(--veo-bg);
229
+ border: var(--veo-border-width, 0) solid var(--veo-border-color, transparent);
230
+ padding: var(--veo-pad, 16px);
231
+ z-index: ${GUIDE_Z_INDEX};
232
+ box-shadow: var(--veo-shadow, 0 2px 12px rgba(0, 0, 0, 0.12));
233
+ animation: veo-slide-down 200ms ease-out;
234
+ }
235
+ .veo-banner-top { top: 0; }
236
+ .veo-banner-bottom { bottom: 0; }
237
+
238
+ .veo-tooltip {
239
+ position: absolute;
240
+ background: var(--veo-bg);
241
+ border-radius: var(--veo-radius);
242
+ border: var(--veo-border-width, 0) solid var(--veo-border-color, transparent);
243
+ padding: var(--veo-pad, 15px);
244
+ max-width: 300px;
245
+ z-index: ${GUIDE_Z_INDEX};
246
+ box-shadow: var(--veo-shadow, 0 8px 30px rgba(0, 0, 0, 0.18));
247
+ animation: veo-fade-in 150ms ease-out;
248
+ }
249
+
250
+ .veo-inline {
251
+ background: var(--veo-bg);
252
+ border-radius: var(--veo-radius);
253
+ border: var(--veo-border-width, 0) solid var(--veo-border-color, transparent);
254
+ padding: var(--veo-pad, 20px);
255
+ max-width: var(--veo-width);
256
+ margin: var(--veo-margin, 12px) 0;
257
+ position: relative;
258
+ box-shadow: var(--veo-shadow);
259
+ animation: veo-fade-in 160ms ease-out;
260
+ }
261
+
262
+ .veo-guide-content {
263
+ position: relative;
264
+ display: flex; flex-direction: column;
265
+ }
266
+ .veo-guide-image {
267
+ display: block; width: 100%; max-height: 180px;
268
+ object-fit: cover; border-radius: 8px;
269
+ margin-bottom: 12px;
270
+ }
271
+ .veo-guide-title {
272
+ font-size: 18px; font-weight: 600; line-height: 1.3;
273
+ margin: 0 0 6px; color: var(--veo-text);
274
+ }
275
+ .veo-guide-text {
276
+ font-size: 14px; line-height: 1.5;
277
+ margin: 0 0 16px; color: var(--veo-text-secondary);
278
+ }
279
+ .veo-guide-actions {
280
+ display: flex; gap: 8px; justify-content: var(--veo-actions-justify);
281
+ }
282
+ .veo-guide-cta {
283
+ background: var(--veo-primary); color: #fff; border: none;
284
+ padding: 9px 18px; border-radius: 8px;
285
+ font-size: 14px; font-weight: 500; cursor: pointer;
286
+ transition: opacity 120ms ease;
287
+ }
288
+ .veo-guide-cta:hover { opacity: 0.9; }
289
+ .veo-guide-cta:active { transform: translateY(1px); }
290
+ .veo-guide-close {
291
+ position: absolute; top: -6px; right: -6px;
292
+ width: 24px; height: 24px;
293
+ background: none; border: none;
294
+ font-size: 22px; line-height: 1;
295
+ cursor: pointer; color: #9ca3af; padding: 0;
296
+ }
297
+ .veo-guide-close:hover { color: var(--veo-text); }
298
+
299
+ .veo-form { display: flex; flex-direction: column; gap: 14px; margin-bottom: 16px; }
300
+ .veo-form-field { display: flex; flex-direction: column; gap: 6px; }
301
+ .veo-form-label { font-size: 13px; font-weight: 500; color: var(--veo-text); }
302
+ .veo-form-input {
303
+ font-size: 14px; color: var(--veo-text);
304
+ background: var(--veo-bg);
305
+ border: 1px solid #d1d5db; border-radius: 8px;
306
+ padding: 8px 10px; width: 100%;
307
+ }
308
+ .veo-form-input:focus { outline: 2px solid var(--veo-primary); outline-offset: -1px; border-color: transparent; }
309
+ .veo-form-options { display: flex; flex-direction: column; gap: 6px; }
310
+ .veo-form-option {
311
+ display: flex; align-items: center; gap: 8px;
312
+ font-size: 14px; color: var(--veo-text); cursor: pointer;
313
+ }
314
+ .veo-form-option input { accent-color: var(--veo-primary); margin: 0; cursor: pointer; }
315
+ .veo-form-scale { display: flex; gap: 4px; flex-wrap: wrap; }
316
+ .veo-nps-btn {
317
+ min-width: 30px; height: 30px; padding: 0 4px;
318
+ font-size: 13px; font-weight: 500; color: var(--veo-text);
319
+ background: var(--veo-bg); border: 1px solid #d1d5db; border-radius: 6px;
320
+ cursor: pointer; transition: all 100ms ease;
321
+ }
322
+ .veo-nps-btn:hover { border-color: var(--veo-primary); }
323
+ .veo-rating-btn {
324
+ background: none; border: none; padding: 0 2px;
325
+ font-size: 26px; line-height: 1; color: #d1d5db;
326
+ cursor: pointer; transition: color 100ms ease;
327
+ }
328
+ .veo-scale-active.veo-nps-btn { background: var(--veo-primary); color: #fff; border-color: var(--veo-primary); }
329
+ .veo-scale-active.veo-rating-btn { color: #f59e0b; }
330
+ .veo-form-error { font-size: 13px; color: #dc2626; margin: 0; }
331
+ .veo-form-thanks {
332
+ text-align: center; padding: 24px 8px;
333
+ font-size: 16px; font-weight: 600; color: var(--veo-text);
334
+ }
335
+ .veo-form-preview-note {
336
+ text-align: center; margin: 0 0 12px;
337
+ font-size: 12px; color: #b45309;
338
+ }
339
+
340
+ .veo-walkthrough {
341
+ position: absolute;
342
+ background: var(--veo-bg);
343
+ border-radius: var(--veo-radius);
344
+ border: var(--veo-border-width, 0) solid var(--veo-border-color, transparent);
345
+ padding: var(--veo-pad, 16px);
346
+ max-width: 340px;
347
+ z-index: ${GUIDE_Z_INDEX};
348
+ box-shadow: var(--veo-shadow, 0 8px 30px rgba(0, 0, 0, 0.2));
349
+ animation: veo-fade-in 150ms ease-out;
350
+ }
351
+ .veo-walkthrough-arrow {
352
+ position: absolute;
353
+ width: 12px;
354
+ height: 12px;
355
+ background: var(--veo-bg);
356
+ transform: rotate(45deg);
357
+ pointer-events: none;
358
+ }
359
+ .veo-walkthrough-counter {
360
+ font-size: 11px; color: #9ca3af;
361
+ margin-bottom: 8px; letter-spacing: 0.02em;
362
+ }
363
+ .veo-walkthrough-progress {
364
+ display: flex; gap: 4px;
365
+ margin-bottom: 12px;
366
+ }
367
+ .veo-walkthrough-progress-dot {
368
+ width: 8px; height: 8px;
369
+ border-radius: 50%;
370
+ background: #e5e7eb;
371
+ transition: background 200ms ease;
372
+ }
373
+ .veo-walkthrough-progress-dot.active { background: var(--veo-primary); }
374
+ .veo-walkthrough-progress-dot.completed { background: var(--veo-primary); opacity: 0.5; }
375
+ .veo-walkthrough-actions {
376
+ display: flex; align-items: center;
377
+ margin-top: 16px; gap: 8px;
378
+ }
379
+ .veo-walkthrough-actions-right {
380
+ display: flex; gap: 8px;
381
+ margin-left: auto;
382
+ }
383
+ .veo-walkthrough-btn-secondary {
384
+ background: transparent; color: #6b7280;
385
+ border: 1px solid #e5e7eb;
386
+ padding: 8px 16px; border-radius: 8px;
387
+ font-size: 13px; cursor: pointer;
388
+ transition: background 120ms ease;
389
+ }
390
+ .veo-walkthrough-btn-secondary:hover { background: #f9fafb; }
391
+ .veo-walkthrough-skip {
392
+ background: transparent; color: #9ca3af;
393
+ border: none; padding: 4px 8px;
394
+ font-size: 12px; cursor: pointer;
395
+ }
396
+ .veo-walkthrough-skip:hover { color: var(--veo-text); }
397
+
398
+ .veo-custom-floating {
399
+ position: fixed;
400
+ z-index: ${GUIDE_Z_INDEX};
401
+ max-width: calc(100vw - 16px);
402
+ max-height: calc(100vh - 16px);
403
+ animation: veo-fade-in 160ms ease;
404
+ }
405
+ .veo-custom-anchored {
406
+ position: fixed;
407
+ top: 0; left: 0;
408
+ z-index: ${GUIDE_Z_INDEX};
409
+ max-width: calc(100vw - 16px);
410
+ animation: veo-fade-in 160ms ease;
411
+ }
412
+ .veo-custom-inline {
413
+ display: block;
414
+ margin: 12px 0;
415
+ }
416
+ .veo-custom-frame {
417
+ display: block;
418
+ border: 0;
419
+ width: 360px;
420
+ background: transparent;
421
+ color-scheme: normal;
422
+ }
423
+
424
+ @keyframes veo-fade-in { from { opacity: 0; } to { opacity: 1; } }
425
+ @keyframes veo-slide-up {
426
+ from { transform: translateY(10px); opacity: 0; }
427
+ to { transform: translateY(0); opacity: 1; }
428
+ }
429
+ @keyframes veo-slide-down {
430
+ from { transform: translateY(-10px); opacity: 0; }
431
+ to { transform: translateY(0); opacity: 1; }
432
+ }
433
+ `;
434
+
435
+ // src/plugins/guides/renderers/base-renderer.ts
436
+ var BaseRenderer = class {
437
+ constructor() {
438
+ this.host = null;
439
+ this.shadow = null;
440
+ this.cleanups = [];
441
+ }
442
+ /**
443
+ * Crea un `<div data-veo-guide>` adjunto a `document.body`, le adjunta
444
+ * un ShadowRoot abierto e inyecta los estilos. Retorna el nodo raíz
445
+ * donde la subclase puede insertar el contenido específico.
446
+ */
447
+ createHost() {
448
+ const host = document.createElement("div");
449
+ host.setAttribute(GUIDE_HOST_ATTR, "");
450
+ const shadow = host.attachShadow({ mode: "open" });
451
+ const style = document.createElement("style");
452
+ style.textContent = GUIDE_STYLES;
453
+ shadow.appendChild(style);
454
+ const root = document.createElement("div");
455
+ shadow.appendChild(root);
456
+ document.body.appendChild(host);
457
+ this.host = host;
458
+ this.shadow = shadow;
459
+ return { host, shadow, root };
460
+ }
461
+ /**
462
+ * Aplica la tematización por datos del step (`step.style`) como variables
463
+ * CSS sobre el host. Llamar tras `createHost()`. No-op si no hay host o
464
+ * style. Ver `guide-design.ts`.
465
+ */
466
+ applyDesign(style) {
467
+ if (this.host) applyDesignVars(this.host, style);
468
+ }
469
+ /**
470
+ * Registra una función a ejecutar en `destroy()`. Útil para limpiar
471
+ * listeners globales (scroll, resize) o intervalos.
472
+ */
473
+ registerCleanup(fn) {
474
+ this.cleanups.push(fn);
475
+ }
476
+ /** Remueve el host del DOM y corre todas las funciones de cleanup. */
477
+ destroy() {
478
+ for (const fn of this.cleanups) {
479
+ try {
480
+ fn();
481
+ } catch {
482
+ }
483
+ }
484
+ this.cleanups = [];
485
+ if (this.host?.parentNode) {
486
+ this.host.parentNode.removeChild(this.host);
487
+ }
488
+ this.host = null;
489
+ this.shadow = null;
490
+ }
491
+ };
492
+
493
+ // src/plugins/guides/renderers/banner-renderer.ts
494
+ var BannerRenderer = class extends BaseRenderer {
495
+ render(context) {
496
+ const step = context.guide.guideSteps[0];
497
+ if (!step) return;
498
+ const { root } = this.createHost();
499
+ this.applyDesign(step.style);
500
+ const ownerDocument = root.ownerDocument ?? document;
501
+ const position = step.style?.position === "bottom" ? "bottom" : "top";
502
+ const banner = ownerDocument.createElement("div");
503
+ banner.className = `veo-banner veo-banner-${position}`;
504
+ const dismiss = (action, ctaUrl) => {
505
+ context.onInteraction({
506
+ guideId: context.guide.guideId,
507
+ stepIndex: 0,
508
+ action
509
+ });
510
+ if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
511
+ context.onClose();
512
+ };
513
+ const content = buildStepContent(step, ownerDocument, {
514
+ onCtaClick: (action, url) => {
515
+ dismiss("cta_clicked", action === "url" ? url : void 0);
516
+ },
517
+ onDismiss: () => dismiss("dismissed")
518
+ });
519
+ banner.appendChild(content);
520
+ root.appendChild(banner);
521
+ context.onInteraction({
522
+ guideId: context.guide.guideId,
523
+ stepIndex: 0,
524
+ action: "shown"
525
+ });
526
+ }
527
+ };
528
+
529
+ // src/plugins/guides/wait-for-element.ts
530
+ function waitForElement(selector, timeoutMs = DEFAULT_ANCHOR_WAIT_MS) {
531
+ return new Promise((resolve) => {
532
+ const safeQuery = () => {
533
+ try {
534
+ return document.querySelector(selector);
535
+ } catch {
536
+ return null;
537
+ }
538
+ };
539
+ const existing = safeQuery();
540
+ if (existing) {
541
+ resolve(existing);
542
+ return;
543
+ }
544
+ let resolved = false;
545
+ const finish = (el) => {
546
+ if (resolved) return;
547
+ resolved = true;
548
+ observer.disconnect();
549
+ clearTimeout(timer);
550
+ resolve(el);
551
+ };
552
+ const observer = new MutationObserver(() => {
553
+ const el = safeQuery();
554
+ if (el) finish(el);
555
+ });
556
+ observer.observe(document.body, { childList: true, subtree: true });
557
+ const timer = setTimeout(() => finish(null), timeoutMs);
558
+ });
559
+ }
560
+
561
+ // src/utils/uuid.ts
562
+ function uuidv7() {
563
+ const timestamp = Date.now();
564
+ const timeHex = timestamp.toString(16).padStart(12, "0");
565
+ const random = new Uint8Array(10);
566
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
567
+ crypto.getRandomValues(random);
568
+ } else {
569
+ for (let i = 0; i < random.length; i++) {
570
+ random[i] = Math.floor(Math.random() * 256);
571
+ }
572
+ }
573
+ random[0] = (random[0] ?? 0) & 15 | 112;
574
+ random[2] = (random[2] ?? 0) & 63 | 128;
575
+ const hex = Array.from(random, (b) => b.toString(16).padStart(2, "0")).join("");
576
+ return [
577
+ timeHex.slice(0, 8),
578
+ timeHex.slice(8, 12),
579
+ hex.slice(0, 4),
580
+ hex.slice(4, 8),
581
+ hex.slice(8, 20)
582
+ ].join("-");
583
+ }
584
+
585
+ // src/plugins/guides/renderers/custom-frame.ts
586
+ function mountCustomFrame(doc, step, context, opts) {
587
+ const iframe = doc.createElement("iframe");
588
+ iframe.className = "veo-custom-frame";
589
+ iframe.setAttribute("sandbox", "allow-scripts");
590
+ iframe.setAttribute("title", context.guide.guideName || "Veo");
591
+ iframe.style.width = opts.width;
592
+ const hasFixedHeight = opts.fixedHeight !== void 0 && opts.fixedHeight !== null;
593
+ if (hasFixedHeight) iframe.style.height = toCssSize(opts.fixedHeight);
594
+ const token = uuidv7();
595
+ iframe.srcdoc = buildSrcdoc(step.html ?? "", step.css ?? "", step.js ?? "", token);
596
+ const close = () => context.onClose();
597
+ const onMessage = (event) => {
598
+ if (event.source !== iframe.contentWindow) return;
599
+ const data = event.data;
600
+ if (!data || typeof data !== "object" || data.__veo !== token) return;
601
+ switch (data.type) {
602
+ case "dismiss":
603
+ context.onInteraction({
604
+ guideId: context.guide.guideId,
605
+ stepIndex: 0,
606
+ action: "dismissed"
607
+ });
608
+ close();
609
+ break;
610
+ case "cta":
611
+ context.onInteraction({
612
+ guideId: context.guide.guideId,
613
+ stepIndex: 0,
614
+ action: "cta_clicked"
615
+ });
616
+ if (typeof data.url === "string" && isSafeUrl(data.url)) {
617
+ window.open(data.url, "_blank", "noopener,noreferrer");
618
+ }
619
+ if (data.close !== false) close();
620
+ break;
621
+ case "navigate":
622
+ if (typeof data.url === "string" && isSafeUrl(data.url)) {
623
+ window.location.assign(data.url);
624
+ }
625
+ break;
626
+ case "track":
627
+ if (typeof data.name === "string" && context.onTrack) {
628
+ context.onTrack(data.name, isRecord(data.props) ? data.props : void 0);
629
+ }
630
+ break;
631
+ case "resize":
632
+ if (!hasFixedHeight && typeof data.height === "number" && data.height > 0) {
633
+ iframe.style.height = `${Math.ceil(data.height)}px`;
634
+ }
635
+ break;
636
+ }
637
+ };
638
+ window.addEventListener("message", onMessage);
639
+ return { iframe, cleanup: () => window.removeEventListener("message", onMessage) };
640
+ }
641
+ function toCssSize(value) {
642
+ return typeof value === "number" ? `${value}px` : value;
643
+ }
644
+ function isRecord(value) {
645
+ return typeof value === "object" && value !== null && !Array.isArray(value);
646
+ }
647
+ function buildSrcdoc(html, css, js, token) {
648
+ const bridge = `(function(){var T=${JSON.stringify(token)};function P(m){m.__veo=T;parent.postMessage(m,'*');}window.veo={dismiss:function(){P({type:'dismiss'});},cta:function(u){P({type:'cta',url:u});},track:function(n,p){P({type:'track',name:n,props:p||{}});},navigate:function(u){P({type:'navigate',url:u});}};document.addEventListener('click',function(e){var a=e.target&&e.target.closest?e.target.closest('a[href]'):null;if(!a)return;var h=a.getAttribute('href');if(!h||h.charAt(0)==='#'||/^(javascript|mailto|tel):/i.test(h))return;e.preventDefault();if(a.target==='_blank'){P({type:'cta',url:a.href,close:false});}else{P({type:'navigate',url:h});}},true);function H(){P({type:'resize',height:Math.ceil(document.documentElement.scrollHeight)});}window.addEventListener('load',H);try{if(typeof ResizeObserver!=='undefined'){new ResizeObserver(H).observe(document.documentElement);}}catch(e){}})();`;
649
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"><style>html,body{margin:0;padding:0;background:transparent;}</style><style>${escapeClosing(css, "style")}</style></head><body>${html}<script>${bridge}</script>` + (js ? `<script>${escapeClosing(js, "script")}</script>` : "") + "</body></html>";
650
+ }
651
+ function escapeClosing(code, tag) {
652
+ const rx = tag === "script" ? /<\/script/gi : /<\/style/gi;
653
+ return code.replace(rx, `<\\/${tag}`);
654
+ }
655
+
656
+ // src/plugins/guides/renderers/custom-renderer.ts
657
+ var DEFAULT_PLACEMENT = { mode: "floating", position: "bottom-right" };
658
+ var DEFAULT_OFFSET = 16;
659
+ var DEFAULT_WIDTH = 360;
660
+ var CustomRenderer = class extends BaseRenderer {
661
+ async render(context) {
662
+ const step = context.guide.guideSteps[0];
663
+ if (!step || typeof step.html !== "string" || step.html.length === 0) return;
664
+ const placement = step.placement ?? DEFAULT_PLACEMENT;
665
+ let anchor = null;
666
+ if (placement.mode === "anchored") {
667
+ const selector = placement.selector ?? step.selector ?? context.guide.activationRules.selector;
668
+ if (typeof selector !== "string" || selector.length === 0) return;
669
+ anchor = await waitForElement(selector);
670
+ if (!anchor) return;
671
+ }
672
+ const { root } = this.createHost();
673
+ const doc = root.ownerDocument ?? document;
674
+ const wrapper = doc.createElement("div");
675
+ wrapper.className = placement.mode === "anchored" ? "veo-custom-anchored" : "veo-custom-floating";
676
+ const { iframe, cleanup } = mountCustomFrame(doc, step, context, {
677
+ width: toCssSize(placement.width ?? DEFAULT_WIDTH),
678
+ fixedHeight: placement.height ?? null
679
+ });
680
+ this.registerCleanup(cleanup);
681
+ wrapper.appendChild(iframe);
682
+ root.appendChild(wrapper);
683
+ if (placement.mode === "floating") {
684
+ applyFloatingPosition(wrapper, placement);
685
+ } else if (anchor) {
686
+ this.registerCleanup(await this.setupAnchoredPosition(anchor, wrapper, placement));
687
+ }
688
+ context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
689
+ }
690
+ async setupAnchoredPosition(anchor, wrapper, placement) {
691
+ const side = placement.side ?? "bottom";
692
+ const update = async () => {
693
+ const { x, y } = await computePosition(anchor, wrapper, {
694
+ placement: side,
695
+ strategy: "fixed",
696
+ middleware: [offset(placement.offsetY ?? 8), flip(), shift({ padding: 8 })]
697
+ });
698
+ wrapper.style.left = `${x}px`;
699
+ wrapper.style.top = `${y}px`;
700
+ };
701
+ await update();
702
+ const reposition = () => {
703
+ void update();
704
+ };
705
+ window.addEventListener("scroll", reposition, true);
706
+ window.addEventListener("resize", reposition);
707
+ return () => {
708
+ window.removeEventListener("scroll", reposition, true);
709
+ window.removeEventListener("resize", reposition);
710
+ };
711
+ }
712
+ };
713
+ function applyFloatingPosition(wrapper, placement) {
714
+ const position = placement.position ?? "bottom-right";
715
+ const ox = `${placement.offsetX ?? DEFAULT_OFFSET}px`;
716
+ const oy = `${placement.offsetY ?? DEFAULT_OFFSET}px`;
717
+ const s = wrapper.style;
718
+ if (position === "center") {
719
+ s.top = "50%";
720
+ s.left = "50%";
721
+ s.transform = "translate(-50%, -50%)";
722
+ return;
723
+ }
724
+ const [vertical, horizontal] = position.split("-");
725
+ if (vertical === "top") s.top = oy;
726
+ else s.bottom = oy;
727
+ if (horizontal === "left") {
728
+ s.left = ox;
729
+ } else if (horizontal === "right") {
730
+ s.right = ox;
731
+ } else {
732
+ s.left = "50%";
733
+ s.transform = "translateX(-50%)";
734
+ }
735
+ }
736
+
737
+ // src/plugins/guides/form-content.ts
738
+ function mountFormContent(card, context, doc) {
739
+ const step = context.guide.guideSteps[0];
740
+ if (!step) return;
741
+ const fields = step.fields ?? [];
742
+ const dismiss = () => {
743
+ context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "dismissed" });
744
+ context.onClose();
745
+ };
746
+ const content = doc.createElement("div");
747
+ content.className = "veo-guide-content";
748
+ if (step.title) {
749
+ const heading = doc.createElement("h2");
750
+ heading.className = "veo-guide-title";
751
+ heading.textContent = step.title;
752
+ content.appendChild(heading);
753
+ }
754
+ if (step.content) {
755
+ const paragraph = doc.createElement("p");
756
+ paragraph.className = "veo-guide-text";
757
+ paragraph.textContent = step.content;
758
+ content.appendChild(paragraph);
759
+ }
760
+ const readers = [];
761
+ const form = doc.createElement("form");
762
+ form.className = "veo-form";
763
+ form.noValidate = true;
764
+ for (const field of fields) {
765
+ const { wrapper, read } = buildField(field, doc);
766
+ readers.push({ field, read });
767
+ form.appendChild(wrapper);
768
+ }
769
+ const error = doc.createElement("p");
770
+ error.className = "veo-form-error";
771
+ error.style.display = "none";
772
+ form.appendChild(error);
773
+ const actions = doc.createElement("div");
774
+ actions.className = "veo-guide-actions";
775
+ const submit = doc.createElement("button");
776
+ submit.type = "submit";
777
+ submit.className = "veo-guide-cta";
778
+ submit.textContent = step.ctaText || "Enviar";
779
+ actions.appendChild(submit);
780
+ form.appendChild(actions);
781
+ form.addEventListener("submit", (e) => {
782
+ e.preventDefault();
783
+ error.style.display = "none";
784
+ const answers = {};
785
+ for (const { field, read } of readers) {
786
+ const value = read();
787
+ const empty = value === void 0 || value === null || value === "";
788
+ if (empty) {
789
+ if (field.required) {
790
+ error.textContent = `Complet\xE1 "${field.label}"`;
791
+ error.style.display = "block";
792
+ return;
793
+ }
794
+ continue;
795
+ }
796
+ answers[field.key] = value;
797
+ }
798
+ if (Object.keys(answers).length === 0) {
799
+ error.textContent = "Respond\xE9 al menos un campo";
800
+ error.style.display = "block";
801
+ return;
802
+ }
803
+ submit.disabled = true;
804
+ const submitFn = context.onFormSubmit ?? (() => Promise.resolve(true));
805
+ void submitFn(answers).then((ok) => {
806
+ if (!ok) {
807
+ submit.disabled = false;
808
+ error.textContent = "No se pudo enviar. Prob\xE1 de nuevo.";
809
+ error.style.display = "block";
810
+ return;
811
+ }
812
+ context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "completed" });
813
+ const note = context.isPreview ? context.previewNote ?? "Vista previa: la respuesta NO se guard\xF3." : null;
814
+ showThanks(card, doc, note);
815
+ window.setTimeout(() => context.onClose(), note ? 2200 : 1400);
816
+ });
817
+ });
818
+ content.appendChild(form);
819
+ const closeBtn = doc.createElement("button");
820
+ closeBtn.type = "button";
821
+ closeBtn.className = "veo-guide-close";
822
+ closeBtn.setAttribute("aria-label", "Cerrar");
823
+ closeBtn.textContent = "\xD7";
824
+ closeBtn.addEventListener("click", dismiss);
825
+ content.appendChild(closeBtn);
826
+ card.appendChild(content);
827
+ }
828
+ function showThanks(card, doc, note) {
829
+ while (card.firstChild) card.removeChild(card.firstChild);
830
+ const thanks = doc.createElement("div");
831
+ thanks.className = "veo-form-thanks";
832
+ thanks.textContent = "\u2713 \xA1Gracias por tu respuesta!";
833
+ card.appendChild(thanks);
834
+ if (note) {
835
+ const noteEl = doc.createElement("p");
836
+ noteEl.className = "veo-form-preview-note";
837
+ noteEl.textContent = note;
838
+ card.appendChild(noteEl);
839
+ }
840
+ }
841
+ function buildField(field, doc) {
842
+ const wrapper = doc.createElement("div");
843
+ wrapper.className = "veo-form-field";
844
+ const label = doc.createElement("label");
845
+ label.className = "veo-form-label";
846
+ label.textContent = field.required ? `${field.label} *` : field.label;
847
+ wrapper.appendChild(label);
848
+ switch (field.type) {
849
+ case "textarea": {
850
+ const input = doc.createElement("textarea");
851
+ input.className = "veo-form-input";
852
+ input.rows = 3;
853
+ if (field.placeholder) input.placeholder = field.placeholder;
854
+ wrapper.appendChild(input);
855
+ return { wrapper, read: () => input.value.trim() };
856
+ }
857
+ case "number": {
858
+ const input = doc.createElement("input");
859
+ input.type = "number";
860
+ input.className = "veo-form-input";
861
+ if (field.placeholder) input.placeholder = field.placeholder;
862
+ wrapper.appendChild(input);
863
+ return {
864
+ wrapper,
865
+ read: () => input.value.trim() === "" ? void 0 : Number(input.value)
866
+ };
867
+ }
868
+ case "select": {
869
+ const select = doc.createElement("select");
870
+ select.className = "veo-form-input";
871
+ const blank = doc.createElement("option");
872
+ blank.value = "";
873
+ blank.textContent = field.placeholder || "Eleg\xED una opci\xF3n\u2026";
874
+ select.appendChild(blank);
875
+ for (const opt of field.options ?? []) {
876
+ const option = doc.createElement("option");
877
+ option.value = opt;
878
+ option.textContent = opt;
879
+ select.appendChild(option);
880
+ }
881
+ wrapper.appendChild(select);
882
+ return { wrapper, read: () => select.value || void 0 };
883
+ }
884
+ case "radio": {
885
+ const group = doc.createElement("div");
886
+ group.className = "veo-form-options";
887
+ const name = `veo-${field.key}`;
888
+ const inputs = [];
889
+ for (const opt of field.options ?? []) {
890
+ const optionLabel = doc.createElement("label");
891
+ optionLabel.className = "veo-form-option";
892
+ const radio = doc.createElement("input");
893
+ radio.type = "radio";
894
+ radio.name = name;
895
+ radio.value = opt;
896
+ inputs.push(radio);
897
+ const text = doc.createElement("span");
898
+ text.textContent = opt;
899
+ optionLabel.appendChild(radio);
900
+ optionLabel.appendChild(text);
901
+ group.appendChild(optionLabel);
902
+ }
903
+ wrapper.appendChild(group);
904
+ return { wrapper, read: () => inputs.find((i) => i.checked)?.value };
905
+ }
906
+ case "multiselect": {
907
+ const group = doc.createElement("div");
908
+ group.className = "veo-form-options";
909
+ const inputs = [];
910
+ for (const opt of field.options ?? []) {
911
+ const optionLabel = doc.createElement("label");
912
+ optionLabel.className = "veo-form-option";
913
+ const checkbox = doc.createElement("input");
914
+ checkbox.type = "checkbox";
915
+ checkbox.value = opt;
916
+ inputs.push(checkbox);
917
+ const text = doc.createElement("span");
918
+ text.textContent = opt;
919
+ optionLabel.appendChild(checkbox);
920
+ optionLabel.appendChild(text);
921
+ group.appendChild(optionLabel);
922
+ }
923
+ wrapper.appendChild(group);
924
+ return {
925
+ wrapper,
926
+ read: () => {
927
+ const values = inputs.filter((i) => i.checked).map((i) => i.value);
928
+ return values.length > 0 ? values : void 0;
929
+ }
930
+ };
931
+ }
932
+ case "checkbox": {
933
+ const optionLabel = doc.createElement("label");
934
+ optionLabel.className = "veo-form-option";
935
+ const checkbox = doc.createElement("input");
936
+ checkbox.type = "checkbox";
937
+ const text = doc.createElement("span");
938
+ text.textContent = field.placeholder || "S\xED";
939
+ optionLabel.appendChild(checkbox);
940
+ optionLabel.appendChild(text);
941
+ wrapper.appendChild(optionLabel);
942
+ return { wrapper, read: () => checkbox.checked ? true : field.required ? "" : false };
943
+ }
944
+ case "yesno": {
945
+ const group = doc.createElement("div");
946
+ group.className = "veo-form-options";
947
+ const name = `veo-${field.key}`;
948
+ const [yesLabel, noLabel] = field.options?.length === 2 ? field.options : ["S\xED", "No"];
949
+ const inputs = [];
950
+ for (const { label: optText, value } of [
951
+ { label: yesLabel, value: true },
952
+ { label: noLabel, value: false }
953
+ ]) {
954
+ const optionLabel = doc.createElement("label");
955
+ optionLabel.className = "veo-form-option";
956
+ const radio = doc.createElement("input");
957
+ radio.type = "radio";
958
+ radio.name = name;
959
+ inputs.push({ input: radio, value });
960
+ const text = doc.createElement("span");
961
+ text.textContent = optText;
962
+ optionLabel.appendChild(radio);
963
+ optionLabel.appendChild(text);
964
+ group.appendChild(optionLabel);
965
+ }
966
+ wrapper.appendChild(group);
967
+ return { wrapper, read: () => inputs.find((i) => i.input.checked)?.value };
968
+ }
969
+ case "nps":
970
+ return buildScale(wrapper, doc, 0, 10, "veo-nps-btn");
971
+ case "rating":
972
+ return buildScale(wrapper, doc, 1, 5, "veo-rating-btn", "\u2605");
973
+ default: {
974
+ const input = doc.createElement("input");
975
+ input.type = "text";
976
+ input.className = "veo-form-input";
977
+ if (field.placeholder) input.placeholder = field.placeholder;
978
+ wrapper.appendChild(input);
979
+ return { wrapper, read: () => input.value.trim() };
980
+ }
981
+ }
982
+ }
983
+ function buildScale(wrapper, doc, min, max, btnClass, symbol) {
984
+ const row = doc.createElement("div");
985
+ row.className = "veo-form-scale";
986
+ let selected;
987
+ const buttons = [];
988
+ for (let n = min; n <= max; n++) {
989
+ const btn = doc.createElement("button");
990
+ btn.type = "button";
991
+ btn.className = btnClass;
992
+ btn.textContent = symbol ?? String(n);
993
+ btn.setAttribute("aria-label", String(n));
994
+ btn.addEventListener("click", () => {
995
+ selected = n;
996
+ buttons.forEach((b, i) => {
997
+ const active2 = symbol ? i + min <= n : i + min === n;
998
+ b.classList.toggle("veo-scale-active", active2);
999
+ });
1000
+ });
1001
+ buttons.push(btn);
1002
+ row.appendChild(btn);
1003
+ }
1004
+ wrapper.appendChild(row);
1005
+ return { wrapper, read: () => selected };
1006
+ }
1007
+
1008
+ // src/plugins/guides/renderers/form-renderer.ts
1009
+ var FormRenderer = class extends BaseRenderer {
1010
+ render(context) {
1011
+ const step = context.guide.guideSteps[0];
1012
+ if (!step || (step.fields ?? []).length === 0) return;
1013
+ const { root } = this.createHost();
1014
+ this.applyDesign(step.style);
1015
+ const doc = root.ownerDocument ?? document;
1016
+ const overlay = doc.createElement("div");
1017
+ overlay.className = "veo-modal-overlay";
1018
+ const card = doc.createElement("div");
1019
+ card.className = "veo-modal-card";
1020
+ mountFormContent(card, context, doc);
1021
+ overlay.appendChild(card);
1022
+ root.appendChild(overlay);
1023
+ const dismiss = () => {
1024
+ context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "dismissed" });
1025
+ context.onClose();
1026
+ };
1027
+ overlay.addEventListener("click", (e) => {
1028
+ if (e.target === overlay) dismiss();
1029
+ });
1030
+ const onKey = (e) => {
1031
+ if (e.key === "Escape") dismiss();
1032
+ };
1033
+ document.addEventListener("keydown", onKey);
1034
+ this.registerCleanup(() => document.removeEventListener("keydown", onKey));
1035
+ context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
1036
+ }
1037
+ };
1038
+
1039
+ // src/plugins/guides/inline-host.ts
1040
+ function readInlinePosition(style) {
1041
+ const p = style?.inlinePosition;
1042
+ return p === "before" || p === "prepend" || p === "append" ? p : "after";
1043
+ }
1044
+ function insertHost(anchor, host, position) {
1045
+ switch (position) {
1046
+ case "before":
1047
+ anchor.before(host);
1048
+ break;
1049
+ case "after":
1050
+ anchor.after(host);
1051
+ break;
1052
+ case "prepend":
1053
+ anchor.prepend(host);
1054
+ break;
1055
+ case "append":
1056
+ anchor.append(host);
1057
+ break;
1058
+ }
1059
+ }
1060
+ function keepHostAttached(host, selector, position) {
1061
+ const id = window.setInterval(() => {
1062
+ if (host.isConnected) return;
1063
+ const anchor = document.querySelector(selector);
1064
+ if (anchor) insertHost(anchor, host, position);
1065
+ }, 1e3);
1066
+ return () => window.clearInterval(id);
1067
+ }
1068
+
1069
+ // src/plugins/guides/renderers/inline-custom-renderer.ts
1070
+ var InlineCustomRenderer = class extends BaseRenderer {
1071
+ async render(context) {
1072
+ const step = context.guide.guideSteps[0];
1073
+ if (!step || typeof step.html !== "string" || step.html.length === 0) return;
1074
+ const selector = step.selector ?? context.guide.activationRules.selector;
1075
+ if (typeof selector !== "string" || selector.length === 0) return;
1076
+ const anchor = await waitForElement(selector);
1077
+ if (!anchor) return;
1078
+ const { host, root } = this.createHost();
1079
+ host.style.display = "block";
1080
+ const position = readInlinePosition(step.style);
1081
+ insertHost(anchor, host, position);
1082
+ this.registerCleanup(keepHostAttached(host, selector, position));
1083
+ const doc = root.ownerDocument ?? document;
1084
+ const wrapper = doc.createElement("div");
1085
+ wrapper.className = "veo-custom-inline";
1086
+ const { iframe, cleanup } = mountCustomFrame(doc, step, context, { width: "100%" });
1087
+ this.registerCleanup(cleanup);
1088
+ wrapper.appendChild(iframe);
1089
+ root.appendChild(wrapper);
1090
+ context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
1091
+ }
1092
+ };
1093
+
1094
+ // src/plugins/guides/renderers/inline-form-renderer.ts
1095
+ var InlineFormRenderer = class extends BaseRenderer {
1096
+ async render(context) {
1097
+ const step = context.guide.guideSteps[0];
1098
+ if (!step || (step.fields ?? []).length === 0) return;
1099
+ const selector = step.selector ?? context.guide.activationRules.selector;
1100
+ if (typeof selector !== "string" || selector.length === 0) return;
1101
+ const anchor = await waitForElement(selector);
1102
+ if (!anchor) return;
1103
+ const { host, root } = this.createHost();
1104
+ host.style.display = "block";
1105
+ const position = readInlinePosition(step.style);
1106
+ insertHost(anchor, host, position);
1107
+ this.registerCleanup(keepHostAttached(host, selector, position));
1108
+ this.applyDesign(step.style);
1109
+ const doc = root.ownerDocument ?? document;
1110
+ const card = doc.createElement("div");
1111
+ card.className = "veo-inline";
1112
+ mountFormContent(card, context, doc);
1113
+ root.appendChild(card);
1114
+ context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
1115
+ }
1116
+ };
1117
+
1118
+ // src/plugins/guides/renderers/inline-renderer.ts
1119
+ var InlineRenderer = class extends BaseRenderer {
1120
+ async render(context) {
1121
+ const step = context.guide.guideSteps[0];
1122
+ if (!step) return;
1123
+ const selector = step.selector ?? context.guide.activationRules.selector;
1124
+ if (typeof selector !== "string" || selector.length === 0) return;
1125
+ const anchor = await waitForElement(selector);
1126
+ if (!anchor) return;
1127
+ const { host, root } = this.createHost();
1128
+ host.style.display = "block";
1129
+ const position = readInlinePosition(step.style);
1130
+ insertHost(anchor, host, position);
1131
+ this.registerCleanup(keepHostAttached(host, selector, position));
1132
+ this.applyDesign(step.style);
1133
+ const ownerDocument = root.ownerDocument ?? document;
1134
+ const card = ownerDocument.createElement("div");
1135
+ card.className = "veo-inline";
1136
+ const dismiss = (action, ctaUrl) => {
1137
+ context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action });
1138
+ if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
1139
+ context.onClose();
1140
+ };
1141
+ const content = buildStepContent(step, ownerDocument, {
1142
+ onCtaClick: (action, url) => dismiss("cta_clicked", action === "url" ? url : void 0),
1143
+ onDismiss: () => dismiss("dismissed")
1144
+ });
1145
+ card.appendChild(content);
1146
+ root.appendChild(card);
1147
+ context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
1148
+ }
1149
+ };
1150
+
1151
+ // src/plugins/guides/renderers/modal-renderer.ts
1152
+ var ModalRenderer = class extends BaseRenderer {
1153
+ render(context) {
1154
+ const step = context.guide.guideSteps[0];
1155
+ if (!step) return;
1156
+ const { root } = this.createHost();
1157
+ this.applyDesign(step.style);
1158
+ const ownerDocument = root.ownerDocument ?? document;
1159
+ const overlay = ownerDocument.createElement("div");
1160
+ overlay.className = "veo-modal-overlay";
1161
+ const card = ownerDocument.createElement("div");
1162
+ card.className = "veo-modal-card";
1163
+ const dismiss = (action, ctaUrl) => {
1164
+ context.onInteraction({
1165
+ guideId: context.guide.guideId,
1166
+ stepIndex: 0,
1167
+ action
1168
+ });
1169
+ if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
1170
+ context.onClose();
1171
+ };
1172
+ const content = buildStepContent(step, ownerDocument, {
1173
+ onCtaClick: (action, url) => {
1174
+ dismiss("cta_clicked", action === "url" ? url : void 0);
1175
+ },
1176
+ onDismiss: () => dismiss("dismissed")
1177
+ });
1178
+ card.appendChild(content);
1179
+ overlay.appendChild(card);
1180
+ root.appendChild(overlay);
1181
+ overlay.addEventListener("click", (e) => {
1182
+ if (e.target === overlay) dismiss("dismissed");
1183
+ });
1184
+ const onKey = (e) => {
1185
+ if (e.key === "Escape") dismiss("dismissed");
1186
+ };
1187
+ document.addEventListener("keydown", onKey);
1188
+ this.registerCleanup(() => document.removeEventListener("keydown", onKey));
1189
+ context.onInteraction({
1190
+ guideId: context.guide.guideId,
1191
+ stepIndex: 0,
1192
+ action: "shown"
1193
+ });
1194
+ }
1195
+ };
1196
+ var TooltipRenderer = class extends BaseRenderer {
1197
+ async render(context) {
1198
+ const step = context.guide.guideSteps[0];
1199
+ if (!step) return;
1200
+ const selector = step.selector ?? context.guide.activationRules.selector;
1201
+ if (typeof selector !== "string" || selector.length === 0) return;
1202
+ const anchor = await waitForElement(selector);
1203
+ if (!anchor) return;
1204
+ const { root } = this.createHost();
1205
+ this.applyDesign(step.style);
1206
+ const ownerDocument = root.ownerDocument ?? document;
1207
+ const tooltip = ownerDocument.createElement("div");
1208
+ tooltip.className = "veo-tooltip";
1209
+ const dismiss = (action, ctaUrl) => {
1210
+ context.onInteraction({
1211
+ guideId: context.guide.guideId,
1212
+ stepIndex: 0,
1213
+ action
1214
+ });
1215
+ if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
1216
+ context.onClose();
1217
+ };
1218
+ const content = buildStepContent(step, ownerDocument, {
1219
+ onCtaClick: (action, url) => {
1220
+ dismiss("cta_clicked", action === "url" ? url : void 0);
1221
+ },
1222
+ onDismiss: () => dismiss("dismissed")
1223
+ });
1224
+ tooltip.appendChild(content);
1225
+ root.appendChild(tooltip);
1226
+ const updatePosition = async () => {
1227
+ const { x, y } = await computePosition(anchor, tooltip, {
1228
+ placement: "bottom",
1229
+ middleware: [offset(8), flip(), shift({ padding: 8 })]
1230
+ });
1231
+ tooltip.style.left = `${x}px`;
1232
+ tooltip.style.top = `${y}px`;
1233
+ };
1234
+ await updatePosition();
1235
+ const reposition = () => {
1236
+ void updatePosition();
1237
+ };
1238
+ window.addEventListener("scroll", reposition, true);
1239
+ window.addEventListener("resize", reposition);
1240
+ this.registerCleanup(() => {
1241
+ window.removeEventListener("scroll", reposition, true);
1242
+ window.removeEventListener("resize", reposition);
1243
+ });
1244
+ context.onInteraction({
1245
+ guideId: context.guide.guideId,
1246
+ stepIndex: 0,
1247
+ action: "shown"
1248
+ });
1249
+ }
1250
+ };
1251
+
1252
+ // src/plugins/guides/walkthrough-block-builder.ts
1253
+ function buildWalkthroughStepContent(step, stepIndex, totalSteps, doc, callbacks) {
1254
+ const container = doc.createElement("div");
1255
+ container.className = "veo-guide-content";
1256
+ const counter = doc.createElement("div");
1257
+ counter.className = "veo-walkthrough-counter";
1258
+ counter.textContent = `Paso ${stepIndex + 1} de ${totalSteps}`;
1259
+ container.appendChild(counter);
1260
+ const progress = doc.createElement("div");
1261
+ progress.className = "veo-walkthrough-progress";
1262
+ for (let i = 0; i < totalSteps; i++) {
1263
+ const dot = doc.createElement("span");
1264
+ dot.className = "veo-walkthrough-progress-dot";
1265
+ if (i < stepIndex) dot.classList.add("completed");
1266
+ if (i === stepIndex) dot.classList.add("active");
1267
+ progress.appendChild(dot);
1268
+ }
1269
+ container.appendChild(progress);
1270
+ if (typeof step.imageUrl === "string" && step.imageUrl && isSafeUrl(step.imageUrl)) {
1271
+ const img = doc.createElement("img");
1272
+ img.className = "veo-guide-image";
1273
+ img.src = step.imageUrl;
1274
+ img.alt = typeof step.title === "string" ? step.title : "";
1275
+ container.appendChild(img);
1276
+ }
1277
+ if (typeof step.title === "string" && step.title) {
1278
+ const heading = doc.createElement("h2");
1279
+ heading.className = "veo-guide-title";
1280
+ heading.textContent = step.title;
1281
+ container.appendChild(heading);
1282
+ }
1283
+ if (typeof step.content === "string" && step.content) {
1284
+ const paragraph = doc.createElement("p");
1285
+ paragraph.className = "veo-guide-text";
1286
+ paragraph.textContent = step.content;
1287
+ container.appendChild(paragraph);
1288
+ }
1289
+ const actions = doc.createElement("div");
1290
+ actions.className = "veo-walkthrough-actions";
1291
+ const skipBtn = doc.createElement("button");
1292
+ skipBtn.type = "button";
1293
+ skipBtn.className = "veo-walkthrough-skip";
1294
+ skipBtn.textContent = "Omitir";
1295
+ skipBtn.addEventListener("click", () => callbacks.onSkip());
1296
+ actions.appendChild(skipBtn);
1297
+ const rightGroup = doc.createElement("div");
1298
+ rightGroup.className = "veo-walkthrough-actions-right";
1299
+ if (stepIndex > 0) {
1300
+ const backBtn = doc.createElement("button");
1301
+ backBtn.type = "button";
1302
+ backBtn.className = "veo-walkthrough-btn-secondary";
1303
+ backBtn.textContent = "Atr\xE1s";
1304
+ backBtn.addEventListener("click", () => callbacks.onBack());
1305
+ rightGroup.appendChild(backBtn);
1306
+ }
1307
+ const isLastStep = stepIndex === totalSteps - 1;
1308
+ const primaryBtn = doc.createElement("button");
1309
+ primaryBtn.type = "button";
1310
+ primaryBtn.className = "veo-guide-cta";
1311
+ const defaultLabel = isLastStep ? "Finalizar" : "Siguiente";
1312
+ primaryBtn.textContent = typeof step.ctaText === "string" && step.ctaText ? step.ctaText : defaultLabel;
1313
+ primaryBtn.addEventListener("click", () => {
1314
+ if (isLastStep) callbacks.onComplete();
1315
+ else callbacks.onNext();
1316
+ });
1317
+ rightGroup.appendChild(primaryBtn);
1318
+ actions.appendChild(rightGroup);
1319
+ container.appendChild(actions);
1320
+ return container;
1321
+ }
1322
+
1323
+ // src/plugins/guides/renderers/walkthrough-renderer.ts
1324
+ var WalkthroughRenderer = class extends BaseRenderer {
1325
+ async render(context) {
1326
+ const step = context.guide.guideSteps[context.currentStepIndex];
1327
+ if (!step) return false;
1328
+ const selector = step.selector ?? context.guide.activationRules.selector;
1329
+ if (typeof selector !== "string" || selector.length === 0) return false;
1330
+ const anchor = await waitForElement(selector, WALKTHROUGH_STEP_SELECTOR_TIMEOUT_MS);
1331
+ if (!anchor) return false;
1332
+ const { root } = this.createHost();
1333
+ this.applyDesign(step.style);
1334
+ const ownerDocument = root.ownerDocument ?? document;
1335
+ const tooltip = ownerDocument.createElement("div");
1336
+ tooltip.className = "veo-walkthrough";
1337
+ const content = buildWalkthroughStepContent(
1338
+ step,
1339
+ context.currentStepIndex,
1340
+ context.guide.guideSteps.length,
1341
+ ownerDocument,
1342
+ context.callbacks
1343
+ );
1344
+ tooltip.appendChild(content);
1345
+ const arrowEl = ownerDocument.createElement("div");
1346
+ arrowEl.className = "veo-walkthrough-arrow";
1347
+ tooltip.appendChild(arrowEl);
1348
+ root.appendChild(tooltip);
1349
+ const updatePosition = async () => {
1350
+ const { x, y, placement, middlewareData } = await computePosition(anchor, tooltip, {
1351
+ placement: "bottom",
1352
+ middleware: [offset(10), flip(), shift({ padding: 8 }), arrow({ element: arrowEl })]
1353
+ });
1354
+ tooltip.style.left = `${x}px`;
1355
+ tooltip.style.top = `${y}px`;
1356
+ positionArrow(arrowEl, placement, middlewareData.arrow);
1357
+ };
1358
+ await updatePosition();
1359
+ const reposition = () => {
1360
+ void updatePosition();
1361
+ };
1362
+ window.addEventListener("scroll", reposition, true);
1363
+ window.addEventListener("resize", reposition);
1364
+ this.registerCleanup(() => {
1365
+ window.removeEventListener("scroll", reposition, true);
1366
+ window.removeEventListener("resize", reposition);
1367
+ });
1368
+ return true;
1369
+ }
1370
+ };
1371
+ var STATIC_SIDE = {
1372
+ top: "bottom",
1373
+ bottom: "top",
1374
+ left: "right",
1375
+ right: "left"
1376
+ };
1377
+ function positionArrow(arrowEl, placement, data) {
1378
+ const side = STATIC_SIDE[placement.split("-")[0] ?? "bottom"] ?? "top";
1379
+ for (const prop of ["top", "bottom", "left", "right"]) {
1380
+ arrowEl.style.setProperty(prop, "");
1381
+ }
1382
+ if (data?.x != null) arrowEl.style.setProperty("left", `${data.x}px`);
1383
+ if (data?.y != null) arrowEl.style.setProperty("top", `${data.y}px`);
1384
+ arrowEl.style.setProperty(side, "-6px");
1385
+ }
1386
+
1387
+ // src/plugins/guides/guide-preview.ts
1388
+ var PREVIEW_GUIDE_ID = "__veo_preview__";
1389
+ var GuidePreviewController = class {
1390
+ constructor() {
1391
+ this.singleStep = null;
1392
+ this.walkthrough = null;
1393
+ this.walkthroughGuide = null;
1394
+ this.walkthroughIndex = 0;
1395
+ /** Input de la preview ACTIVA (para formSubmit/nota de forms). */
1396
+ this.input = null;
1397
+ }
1398
+ preview(input) {
1399
+ this.close();
1400
+ this.input = input;
1401
+ const guide = normalize(input);
1402
+ const start = typeof input.startStepIndex === "number" ? input.startStepIndex : 0;
1403
+ const ready = guide.guideType === "walkthrough" ? this.startWalkthrough(guide, start) : this.renderSingleStep(guide);
1404
+ return { close: () => this.close(), ready };
1405
+ }
1406
+ close() {
1407
+ if (this.singleStep) {
1408
+ safeDestroy(this.singleStep);
1409
+ this.singleStep = null;
1410
+ }
1411
+ if (this.walkthrough) {
1412
+ safeDestroy(this.walkthrough);
1413
+ this.walkthrough = null;
1414
+ }
1415
+ this.walkthroughGuide = null;
1416
+ this.walkthroughIndex = 0;
1417
+ this.input = null;
1418
+ }
1419
+ async renderSingleStep(guide) {
1420
+ const renderer = createSingleStepRenderer(guide.guideType);
1421
+ if (!renderer) return { rendered: false };
1422
+ this.singleStep = renderer;
1423
+ let shown = false;
1424
+ try {
1425
+ await renderer.render({
1426
+ guide,
1427
+ onInteraction: (event) => {
1428
+ if (event.action === "shown") shown = true;
1429
+ },
1430
+ onClose: () => this.close(),
1431
+ onTrack: () => {
1432
+ },
1433
+ // Preview de forms: por defecto simula el envío SIN escribir props;
1434
+ // el caller puede pasar `formSubmit` real (link de preview).
1435
+ onFormSubmit: this.input?.formSubmit ?? (() => Promise.resolve(true)),
1436
+ isPreview: true,
1437
+ ...this.input?.formSubmit && this.input.formSubmitNote !== void 0 ? { previewNote: this.input.formSubmitNote } : {}
1438
+ });
1439
+ } catch {
1440
+ this.close();
1441
+ return { rendered: false };
1442
+ }
1443
+ if (this.singleStep !== renderer) {
1444
+ safeDestroy(renderer);
1445
+ return { rendered: false };
1446
+ }
1447
+ return { rendered: shown };
1448
+ }
1449
+ async startWalkthrough(guide, startIndex = 0) {
1450
+ if (guide.guideSteps.length === 0) return { rendered: false };
1451
+ this.walkthroughGuide = guide;
1452
+ const idx = Math.min(Math.max(0, Math.floor(startIndex)), guide.guideSteps.length - 1);
1453
+ this.walkthroughIndex = idx;
1454
+ const rendered = await this.renderWalkthroughStep(idx);
1455
+ return { rendered };
1456
+ }
1457
+ async renderWalkthroughStep(index) {
1458
+ const guide = this.walkthroughGuide;
1459
+ if (!guide) return false;
1460
+ if (this.walkthrough) {
1461
+ safeDestroy(this.walkthrough);
1462
+ this.walkthrough = null;
1463
+ }
1464
+ const renderer = new WalkthroughRenderer();
1465
+ const callbacks = {
1466
+ onNext: () => {
1467
+ const next = this.walkthroughIndex + 1;
1468
+ if (next >= guide.guideSteps.length) {
1469
+ this.close();
1470
+ return;
1471
+ }
1472
+ this.walkthroughIndex = next;
1473
+ void this.renderWalkthroughStep(next);
1474
+ },
1475
+ onBack: () => {
1476
+ const prev = this.walkthroughIndex - 1;
1477
+ if (prev < 0) return;
1478
+ this.walkthroughIndex = prev;
1479
+ void this.renderWalkthroughStep(prev);
1480
+ },
1481
+ onSkip: () => this.close(),
1482
+ onComplete: () => this.close()
1483
+ };
1484
+ let success = false;
1485
+ try {
1486
+ success = await renderer.render({ guide, currentStepIndex: index, callbacks });
1487
+ } catch {
1488
+ success = false;
1489
+ }
1490
+ if (!success || this.walkthroughGuide !== guide) {
1491
+ safeDestroy(renderer);
1492
+ return false;
1493
+ }
1494
+ this.walkthrough = renderer;
1495
+ return true;
1496
+ }
1497
+ };
1498
+ var activeController = null;
1499
+ function previewGuide(input) {
1500
+ if (!hasDocument()) {
1501
+ return { close: () => {
1502
+ }, ready: Promise.resolve({ rendered: false }) };
1503
+ }
1504
+ if (!activeController) activeController = new GuidePreviewController();
1505
+ return activeController.preview(input);
1506
+ }
1507
+ function closeGuidePreview() {
1508
+ activeController?.close();
1509
+ }
1510
+ function normalize(input) {
1511
+ const selector = input.activationRules?.selector;
1512
+ const activationRules = {
1513
+ url: input.activationRules?.url ?? { type: "prefix", pattern: "/" },
1514
+ trigger: "immediate",
1515
+ ...selector !== void 0 ? { selector } : {}
1516
+ };
1517
+ return {
1518
+ guideId: PREVIEW_GUIDE_ID,
1519
+ guideName: input.guideName ?? "Preview",
1520
+ guideType: input.guideType,
1521
+ guideSteps: input.guideSteps,
1522
+ activationRules,
1523
+ displayFrequency: "always",
1524
+ displayPriority: 0
1525
+ };
1526
+ }
1527
+ function createSingleStepRenderer(type) {
1528
+ switch (type) {
1529
+ case "modal":
1530
+ return new ModalRenderer();
1531
+ case "banner":
1532
+ return new BannerRenderer();
1533
+ case "tooltip":
1534
+ return new TooltipRenderer();
1535
+ case "custom":
1536
+ return new CustomRenderer();
1537
+ case "inline":
1538
+ return new InlineRenderer();
1539
+ case "inline-custom":
1540
+ return new InlineCustomRenderer();
1541
+ case "form":
1542
+ return new FormRenderer();
1543
+ case "inline-form":
1544
+ return new InlineFormRenderer();
1545
+ case "walkthrough":
1546
+ return null;
1547
+ }
1548
+ }
1549
+ function safeDestroy(renderer) {
1550
+ try {
1551
+ renderer.destroy();
1552
+ } catch {
1553
+ }
1554
+ }
1555
+
1556
+ // src/plugins/plugin.ts
1557
+ function definePlugin(plugin) {
1558
+ return plugin;
1559
+ }
1560
+
1561
+ // src/plugins/guides/preview-mode.ts
1562
+ var active = null;
1563
+ async function runPreviewMode(opts) {
1564
+ if (!hasDocument()) return null;
1565
+ active?.teardown();
1566
+ let guide = null;
1567
+ try {
1568
+ const res = await fetch(`${opts.apiUrl}/v1/guides/preview-resolve`, {
1569
+ method: "POST",
1570
+ headers: { "Content-Type": "application/json", "X-Api-Key": opts.apiKey },
1571
+ body: JSON.stringify({ token: opts.token })
1572
+ });
1573
+ if (res.ok) {
1574
+ const data = await res.json();
1575
+ guide = data.guide ?? null;
1576
+ } else if (opts.debug) {
1577
+ console.warn("[veo] preview-resolve respondi\xF3", res.status);
1578
+ }
1579
+ } catch (err) {
1580
+ if (opts.debug) console.warn("[veo] preview-resolve fall\xF3:", err);
1581
+ }
1582
+ const submitToBackend = async (answers) => {
1583
+ if (!guide) return false;
1584
+ const veo = window.veo;
1585
+ const endUserId = veo?.getEndUserId?.() ?? veo?.getAnonymousId?.() ?? null;
1586
+ if (!endUserId) return false;
1587
+ try {
1588
+ const res = await fetch(`${opts.apiUrl}/v1/guides/${guide.guideId}/form-response`, {
1589
+ method: "POST",
1590
+ headers: { "Content-Type": "application/json", "X-Api-Key": opts.apiKey },
1591
+ body: JSON.stringify({ endUserId, answers })
1592
+ });
1593
+ return res.ok;
1594
+ } catch {
1595
+ return false;
1596
+ }
1597
+ };
1598
+ let preview = null;
1599
+ const show = () => {
1600
+ if (!guide) return;
1601
+ preview?.close();
1602
+ preview = previewGuide({
1603
+ guideType: guide.guideType,
1604
+ guideSteps: guide.guideSteps,
1605
+ activationRules: guide.activationRules,
1606
+ guideName: guide.guideName,
1607
+ formSubmit: submitToBackend,
1608
+ formSubmitNote: "Vista previa: guardado en TU usuario para probar el flujo."
1609
+ });
1610
+ };
1611
+ const chip = mountChip({
1612
+ label: guide ? `Vista previa: ${guide.guideName}` : "Link de preview inv\xE1lido o vencido",
1613
+ error: !guide,
1614
+ onReshow: guide ? show : null,
1615
+ onClose: () => handle.teardown()
1616
+ });
1617
+ const handle = {
1618
+ teardown() {
1619
+ preview?.close();
1620
+ preview = null;
1621
+ chip.remove();
1622
+ if (active === handle) active = null;
1623
+ }
1624
+ };
1625
+ active = handle;
1626
+ show();
1627
+ return handle;
1628
+ }
1629
+ function mountChip(opts) {
1630
+ const host = document.createElement("div");
1631
+ host.setAttribute("data-veo-preview-chip", "");
1632
+ const root = host.attachShadow({ mode: "open" });
1633
+ const style = document.createElement("style");
1634
+ style.textContent = `
1635
+ .chip { position: fixed; bottom: 16px; right: 16px; z-index: 2147483647;
1636
+ display: flex; align-items: center; gap: 8px; padding: 8px 12px;
1637
+ background: #111827; color: #f9fafb; border-radius: 9999px;
1638
+ font: 500 12px/1.2 system-ui, sans-serif; box-shadow: 0 4px 12px rgba(0,0,0,.25); }
1639
+ .chip.error { background: #7f1d1d; }
1640
+ .label { max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1641
+ button { all: unset; cursor: pointer; padding: 2px 6px; border-radius: 9999px;
1642
+ font-size: 12px; line-height: 1; }
1643
+ button:hover { background: rgba(255,255,255,.15); }
1644
+ `;
1645
+ root.appendChild(style);
1646
+ const chip = document.createElement("div");
1647
+ chip.className = opts.error ? "chip error" : "chip";
1648
+ const label = document.createElement("span");
1649
+ label.className = "label";
1650
+ label.textContent = opts.label;
1651
+ chip.appendChild(label);
1652
+ if (opts.onReshow) {
1653
+ const reshow = document.createElement("button");
1654
+ reshow.textContent = "\u21BB";
1655
+ reshow.title = "Volver a mostrar la gu\xEDa";
1656
+ reshow.addEventListener("click", opts.onReshow);
1657
+ chip.appendChild(reshow);
1658
+ }
1659
+ const close = document.createElement("button");
1660
+ close.textContent = "\u2715";
1661
+ close.title = "Salir de la vista previa";
1662
+ close.addEventListener("click", opts.onClose);
1663
+ chip.appendChild(close);
1664
+ root.appendChild(chip);
1665
+ document.body.appendChild(host);
1666
+ return { remove: () => host.remove() };
1667
+ }
1668
+
1669
+ // src/plugins/guides/guide-frequency-cache.ts
1670
+ var STATUS_PRIORITY = {
1671
+ shown: 1,
1672
+ dismissed: 2,
1673
+ completed: 2
1674
+ };
1675
+ function namespaceFromApiKey(apiKey) {
1676
+ return apiKey.slice(-16) || "default";
1677
+ }
1678
+ var GuideFrequencyCache = class {
1679
+ constructor(namespace) {
1680
+ this.storageKey = `${FREQUENCY_CACHE_KEY_PREFIX}${namespace}`;
1681
+ this.cache = this.load();
1682
+ this.prune();
1683
+ }
1684
+ /**
1685
+ * Decide si una guía debe filtrarse localmente antes de pintarla.
1686
+ *
1687
+ * - `every_visit` / `always` → nunca filtra (siempre re-pinta).
1688
+ * - `once` → cualquier entry registrada filtra (verla ya cuenta).
1689
+ * - `until_dismissed` → si fue descartada O COMPLETADA (responder un
1690
+ * form / terminar un walkthrough es más fuerte que descartar — sin
1691
+ * esto, una encuesta seguiría preguntando después de respondida).
1692
+ */
1693
+ shouldFilter(guideId, displayFrequency) {
1694
+ if (displayFrequency === "every_visit" || displayFrequency === "always") return false;
1695
+ const entry = this.cache.get(guideId);
1696
+ if (!entry) return false;
1697
+ if (displayFrequency === "once") return true;
1698
+ if (displayFrequency === "until_dismissed") {
1699
+ return entry.status === "dismissed" || entry.status === "completed";
1700
+ }
1701
+ if (displayFrequency === "until_answered") return entry.status === "completed";
1702
+ return false;
1703
+ }
1704
+ /**
1705
+ * Registra/actualiza la entry de una guía. Nunca degrada un estado más
1706
+ * fuerte (no convierte `dismissed` en `shown` por un re-render). Persiste
1707
+ * cada llamada — escribir a localStorage es lo bastante barato.
1708
+ */
1709
+ record(guideId, status) {
1710
+ const existing = this.cache.get(guideId);
1711
+ if (existing && STATUS_PRIORITY[status] < STATUS_PRIORITY[existing.status]) {
1712
+ return;
1713
+ }
1714
+ this.cache.set(guideId, {
1715
+ guideId,
1716
+ status,
1717
+ lastInteractionAt: Date.now()
1718
+ });
1719
+ this.persist();
1720
+ }
1721
+ /** Limpia el cache (botón "reset" del demo, tests). */
1722
+ clear() {
1723
+ this.cache.clear();
1724
+ this.persist();
1725
+ }
1726
+ /** Útil para debugging — devuelve un snapshot. */
1727
+ snapshot() {
1728
+ return Array.from(this.cache.values());
1729
+ }
1730
+ /**
1731
+ * Limpia entries expiradas por TTL y, si aún excede el límite, descarta
1732
+ * las más viejas hasta caber. Se ejecuta una vez en el constructor; no
1733
+ * vale la pena correrlo a cada lectura.
1734
+ */
1735
+ prune() {
1736
+ const now = Date.now();
1737
+ let mutated = false;
1738
+ for (const [id, entry] of this.cache) {
1739
+ if (now - entry.lastInteractionAt > FREQUENCY_CACHE_TTL_MS) {
1740
+ this.cache.delete(id);
1741
+ mutated = true;
1742
+ }
1743
+ }
1744
+ if (this.cache.size > FREQUENCY_CACHE_MAX_ENTRIES) {
1745
+ const sorted = Array.from(this.cache.entries()).sort(
1746
+ (a, b) => a[1].lastInteractionAt - b[1].lastInteractionAt
1747
+ );
1748
+ const excess = this.cache.size - FREQUENCY_CACHE_MAX_ENTRIES;
1749
+ for (let i = 0; i < excess; i++) {
1750
+ const tuple = sorted[i];
1751
+ if (tuple) this.cache.delete(tuple[0]);
1752
+ }
1753
+ mutated = true;
1754
+ }
1755
+ if (mutated) this.persist();
1756
+ }
1757
+ load() {
1758
+ if (typeof localStorage === "undefined") return /* @__PURE__ */ new Map();
1759
+ try {
1760
+ const raw = localStorage.getItem(this.storageKey);
1761
+ if (!raw) return /* @__PURE__ */ new Map();
1762
+ const parsed = JSON.parse(raw);
1763
+ if (!Array.isArray(parsed)) return /* @__PURE__ */ new Map();
1764
+ const entries = [];
1765
+ for (const item of parsed) {
1766
+ if (item && typeof item === "object" && typeof item.guideId === "string" && typeof item.lastInteractionAt === "number" && ["shown", "dismissed", "completed"].includes(item.status)) {
1767
+ const e = item;
1768
+ entries.push([e.guideId, e]);
1769
+ }
1770
+ }
1771
+ return new Map(entries);
1772
+ } catch {
1773
+ return /* @__PURE__ */ new Map();
1774
+ }
1775
+ }
1776
+ persist() {
1777
+ if (typeof localStorage === "undefined") return;
1778
+ try {
1779
+ const serialized = JSON.stringify(Array.from(this.cache.values()));
1780
+ localStorage.setItem(this.storageKey, serialized);
1781
+ } catch {
1782
+ }
1783
+ }
1784
+ };
1785
+
1786
+ // src/plugins/guides/guide-resolver-client.ts
1787
+ var GuideResolverClient = class {
1788
+ constructor(apiUrl, apiKey, debug = false) {
1789
+ this.apiUrl = apiUrl;
1790
+ this.apiKey = apiKey;
1791
+ this.debug = debug;
1792
+ }
1793
+ async resolve(endUserId, pageUrl) {
1794
+ const params = new URLSearchParams({ endUserId, pageUrl });
1795
+ let response;
1796
+ try {
1797
+ response = await fetch(`${this.apiUrl}/v1/guides/resolve?${params.toString()}`, {
1798
+ method: "GET",
1799
+ headers: { "X-Api-Key": this.apiKey },
1800
+ credentials: "omit"
1801
+ });
1802
+ } catch (err) {
1803
+ if (this.debug) console.error("[veo] guides resolve fetch failed:", err);
1804
+ return [];
1805
+ }
1806
+ if (!response.ok) {
1807
+ if (this.debug) console.warn("[veo] guides resolve HTTP", response.status);
1808
+ return [];
1809
+ }
1810
+ try {
1811
+ const data = await response.json();
1812
+ return Array.isArray(data.guides) ? data.guides : [];
1813
+ } catch (err) {
1814
+ if (this.debug) console.error("[veo] guides resolve parse failed:", err);
1815
+ return [];
1816
+ }
1817
+ }
1818
+ /**
1819
+ * Trae una guía específica por ID, sin pasar por la lógica de
1820
+ * activación. La usa el controller para resumir un walkthrough entre
1821
+ * páginas: el `resolve` no devuelve la guía si la URL actual no
1822
+ * matchea, pero el walkthrough activo debe poder consultarla igual.
1823
+ *
1824
+ * El endpoint backend (`GET /v1/guides/:guideId`) devuelve `GuideResponse`,
1825
+ * un superset de `ResolvedGuide` con campos de audit. Acá descartamos los
1826
+ * extra y nos quedamos con el shape que el resto del SDK ya consume.
1827
+ *
1828
+ * Devuelve `null` en cualquier error (404, red, parse) — el caller debe
1829
+ * tratarlo como "guía ya no existe" y cancelar el walkthrough limpiamente.
1830
+ */
1831
+ async fetchById(guideId) {
1832
+ let response;
1833
+ try {
1834
+ response = await fetch(`${this.apiUrl}/v1/guides/${encodeURIComponent(guideId)}`, {
1835
+ method: "GET",
1836
+ headers: { "X-Api-Key": this.apiKey },
1837
+ credentials: "omit"
1838
+ });
1839
+ } catch (err) {
1840
+ if (this.debug) console.error("[veo] guides fetchById network failed:", err);
1841
+ return null;
1842
+ }
1843
+ if (!response.ok) {
1844
+ if (this.debug) console.warn("[veo] guides fetchById HTTP", response.status);
1845
+ return null;
1846
+ }
1847
+ try {
1848
+ const data = await response.json();
1849
+ if (typeof data.guideId !== "string" || typeof data.guideName !== "string" || typeof data.guideType !== "string" || !Array.isArray(data.guideSteps) || !data.activationRules || typeof data.displayFrequency !== "string" || typeof data.displayPriority !== "number") {
1850
+ if (this.debug) console.warn("[veo] guides fetchById: unexpected shape");
1851
+ return null;
1852
+ }
1853
+ return {
1854
+ guideId: data.guideId,
1855
+ guideName: data.guideName,
1856
+ guideType: data.guideType,
1857
+ guideSteps: data.guideSteps,
1858
+ activationRules: data.activationRules,
1859
+ displayFrequency: data.displayFrequency,
1860
+ displayPriority: data.displayPriority
1861
+ };
1862
+ } catch (err) {
1863
+ if (this.debug) console.error("[veo] guides fetchById parse failed:", err);
1864
+ return null;
1865
+ }
1866
+ }
1867
+ };
1868
+
1869
+ // src/plugins/guides/guide-tracker-client.ts
1870
+ function toWire(payload) {
1871
+ const wire = {
1872
+ endUserId: payload.endUserId,
1873
+ interactionType: payload.action
1874
+ };
1875
+ if (payload.sessionId) wire.sessionId = payload.sessionId;
1876
+ if (typeof payload.stepIndex === "number") wire.stepPosition = payload.stepIndex;
1877
+ if (payload.pageUrl) wire.pageUrl = payload.pageUrl;
1878
+ if (payload.pagePath) wire.pagePath = payload.pagePath;
1879
+ return wire;
1880
+ }
1881
+ var GuideTrackerClient = class {
1882
+ constructor(apiUrl, apiKey) {
1883
+ this.apiUrl = apiUrl;
1884
+ this.apiKey = apiKey;
1885
+ }
1886
+ async send(request) {
1887
+ const url = this.buildUrl(request.guideId);
1888
+ const response = await fetch(url, {
1889
+ method: "POST",
1890
+ headers: {
1891
+ "X-Api-Key": this.apiKey,
1892
+ "Content-Type": "application/json"
1893
+ },
1894
+ body: JSON.stringify(toWire(request.payload)),
1895
+ credentials: "omit"
1896
+ });
1897
+ if (!response.ok) {
1898
+ throw new Error(`guide interaction send failed: HTTP ${response.status}`);
1899
+ }
1900
+ }
1901
+ /**
1902
+ * Envío "fire-and-forget" en pagehide. Devuelve `false` si el navegador
1903
+ * no acepta el beacon (cola llena, no soportado) — en ese caso la queue
1904
+ * sabrá que perdió el envío.
1905
+ */
1906
+ sendBeacon(request) {
1907
+ if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") {
1908
+ return false;
1909
+ }
1910
+ const url = `${this.buildUrl(request.guideId)}?key=${encodeURIComponent(this.apiKey)}`;
1911
+ const blob = new Blob([JSON.stringify(toWire(request.payload))], {
1912
+ type: "application/json"
1913
+ });
1914
+ try {
1915
+ return navigator.sendBeacon(url, blob);
1916
+ } catch {
1917
+ return false;
1918
+ }
1919
+ }
1920
+ buildUrl(guideId) {
1921
+ return `${this.apiUrl}/v1/guides/${encodeURIComponent(guideId)}/interactions`;
1922
+ }
1923
+ };
1924
+
1925
+ // src/plugins/guides/guide-tracker-queue.ts
1926
+ var GuideTrackerQueue = class {
1927
+ constructor(config) {
1928
+ this.config = config;
1929
+ this.buffer = [];
1930
+ this.timer = null;
1931
+ this.flushing = false;
1932
+ this.unloadCleanup = null;
1933
+ this.startTimer();
1934
+ this.installUnloadHandler();
1935
+ }
1936
+ /** Tamaño actual del buffer (útil para debug/stats en el demo). */
1937
+ get size() {
1938
+ return this.buffer.length;
1939
+ }
1940
+ /**
1941
+ * Encola una interaction. Si el buffer alcanza `batchSize`, dispara
1942
+ * `flush()` inmediato.
1943
+ */
1944
+ enqueue(request) {
1945
+ this.buffer.push({
1946
+ guideId: request.guideId,
1947
+ payload: request.payload,
1948
+ attempts: 0,
1949
+ enqueuedAt: Date.now()
1950
+ });
1951
+ if (this.buffer.length >= this.config.batchSize) {
1952
+ void this.flush();
1953
+ }
1954
+ }
1955
+ /**
1956
+ * Envía todo el buffer en paralelo. Items que fallan vuelven al buffer
1957
+ * si todavía tienen reintentos disponibles. Idempotente con respecto a
1958
+ * flushes concurrentes (el segundo es un no-op).
1959
+ */
1960
+ async flush() {
1961
+ if (this.flushing || this.buffer.length === 0) return;
1962
+ this.flushing = true;
1963
+ const batch = this.buffer.splice(0, this.buffer.length);
1964
+ const results = await Promise.allSettled(
1965
+ batch.map(
1966
+ (item) => this.config.client.send({ guideId: item.guideId, payload: item.payload })
1967
+ )
1968
+ );
1969
+ for (let i = 0; i < results.length; i++) {
1970
+ const result = results[i];
1971
+ const item = batch[i];
1972
+ if (!result || !item) continue;
1973
+ if (result.status === "fulfilled") continue;
1974
+ item.attempts += 1;
1975
+ if (item.attempts < this.config.maxRetries) {
1976
+ this.buffer.push(item);
1977
+ } else {
1978
+ this.safeOnError(result.reason, item);
1979
+ }
1980
+ }
1981
+ this.flushing = false;
1982
+ }
1983
+ /**
1984
+ * Drena el buffer vía `sendBeacon`. Síncrono, sin retry. Para pagehide.
1985
+ */
1986
+ flushBeacon() {
1987
+ if (this.buffer.length === 0) return;
1988
+ const batch = this.buffer.splice(0, this.buffer.length);
1989
+ for (const item of batch) {
1990
+ this.config.client.sendBeacon({ guideId: item.guideId, payload: item.payload });
1991
+ }
1992
+ }
1993
+ destroy() {
1994
+ if (this.timer) clearInterval(this.timer);
1995
+ this.timer = null;
1996
+ this.unloadCleanup?.();
1997
+ this.unloadCleanup = null;
1998
+ }
1999
+ startTimer() {
2000
+ if (typeof window === "undefined") return;
2001
+ this.timer = setInterval(() => {
2002
+ void this.flush();
2003
+ }, this.config.flushIntervalMs);
2004
+ }
2005
+ installUnloadHandler() {
2006
+ if (typeof window === "undefined") return;
2007
+ const onPagehide = () => this.flushBeacon();
2008
+ const onVisibility = () => {
2009
+ if (document.visibilityState === "hidden") this.flushBeacon();
2010
+ };
2011
+ window.addEventListener("pagehide", onPagehide);
2012
+ window.addEventListener("visibilitychange", onVisibility);
2013
+ this.unloadCleanup = () => {
2014
+ window.removeEventListener("pagehide", onPagehide);
2015
+ window.removeEventListener("visibilitychange", onVisibility);
2016
+ };
2017
+ }
2018
+ safeOnError(reason, item) {
2019
+ if (!this.config.onError) return;
2020
+ const error = reason instanceof Error ? reason : new Error(String(reason));
2021
+ try {
2022
+ this.config.onError(error, item);
2023
+ } catch {
2024
+ }
2025
+ }
2026
+ };
2027
+
2028
+ // src/plugins/guides/walkthrough-state.ts
2029
+ var WalkthroughStateManager = class {
2030
+ constructor(namespace) {
2031
+ this.state = null;
2032
+ this.storageKey = `${WALKTHROUGH_STATE_KEY_PREFIX}${namespace}`;
2033
+ this.state = this.load();
2034
+ if (this.state && this.isAbandoned(this.state)) {
2035
+ this.clear();
2036
+ }
2037
+ }
2038
+ /**
2039
+ * Estado del walkthrough activo. Devuelve `null` si no hay ninguno o si
2040
+ * el último progreso fue hace más del timeout (en ese caso también
2041
+ * limpia el storage como side-effect).
2042
+ */
2043
+ current() {
2044
+ if (!this.state) return null;
2045
+ if (this.isAbandoned(this.state)) {
2046
+ this.clear();
2047
+ return null;
2048
+ }
2049
+ return this.state;
2050
+ }
2051
+ /** Atajo legible para el controller. */
2052
+ hasActive() {
2053
+ return this.current() !== null;
2054
+ }
2055
+ /**
2056
+ * Inicia un nuevo walkthrough en step 0. Sobreescribe cualquier estado
2057
+ * previo — los callers (controller) son responsables de verificar
2058
+ * `hasActive()` antes si necesitan exclusividad.
2059
+ */
2060
+ start(guideId, totalSteps) {
2061
+ const now = Date.now();
2062
+ this.state = {
2063
+ guideId,
2064
+ totalSteps,
2065
+ currentStepIndex: 0,
2066
+ startedAt: now,
2067
+ lastProgressAt: now
2068
+ };
2069
+ this.persist();
2070
+ return this.state;
2071
+ }
2072
+ /**
2073
+ * Avanza al siguiente step. Si el step resultante excede `totalSteps`,
2074
+ * devuelve `null` y NO toca el estado — el controller debe interpretar
2075
+ * eso como "ya estamos en el último, no hay próximo" y disparar
2076
+ * `complete` vía el handler del botón "Finalizar".
2077
+ */
2078
+ advance() {
2079
+ if (!this.state) return null;
2080
+ const next = this.state.currentStepIndex + 1;
2081
+ if (next >= this.state.totalSteps) return null;
2082
+ this.state = {
2083
+ ...this.state,
2084
+ currentStepIndex: next,
2085
+ lastProgressAt: Date.now()
2086
+ };
2087
+ this.persist();
2088
+ return this.state;
2089
+ }
2090
+ /**
2091
+ * Retrocede al step anterior. En step 0 no hace nada y devuelve el
2092
+ * estado tal cual (el botón "Atrás" no debería estar visible en el
2093
+ * primer step, pero por defensa el manager también lo guard-ea).
2094
+ */
2095
+ back() {
2096
+ if (!this.state) return null;
2097
+ if (this.state.currentStepIndex === 0) return this.state;
2098
+ this.state = {
2099
+ ...this.state,
2100
+ currentStepIndex: this.state.currentStepIndex - 1,
2101
+ lastProgressAt: Date.now()
2102
+ };
2103
+ this.persist();
2104
+ return this.state;
2105
+ }
2106
+ /** Limpia el estado activo (skip, complete, abandono, guía borrada). */
2107
+ clear() {
2108
+ this.state = null;
2109
+ if (typeof localStorage === "undefined") return;
2110
+ try {
2111
+ localStorage.removeItem(this.storageKey);
2112
+ } catch {
2113
+ }
2114
+ }
2115
+ isAbandoned(state) {
2116
+ return Date.now() - state.lastProgressAt > WALKTHROUGH_ABANDONMENT_TIMEOUT_MS;
2117
+ }
2118
+ load() {
2119
+ if (typeof localStorage === "undefined") return null;
2120
+ try {
2121
+ const raw = localStorage.getItem(this.storageKey);
2122
+ if (!raw) return null;
2123
+ const parsed = JSON.parse(raw);
2124
+ if (!parsed || typeof parsed !== "object") return null;
2125
+ const candidate = parsed;
2126
+ if (typeof candidate.guideId !== "string" || typeof candidate.totalSteps !== "number" || typeof candidate.currentStepIndex !== "number" || typeof candidate.startedAt !== "number" || typeof candidate.lastProgressAt !== "number") {
2127
+ return null;
2128
+ }
2129
+ return candidate;
2130
+ } catch {
2131
+ return null;
2132
+ }
2133
+ }
2134
+ persist() {
2135
+ if (typeof localStorage === "undefined" || !this.state) return;
2136
+ try {
2137
+ localStorage.setItem(this.storageKey, JSON.stringify(this.state));
2138
+ } catch {
2139
+ }
2140
+ }
2141
+ };
2142
+
2143
+ // src/plugins/guides/guides-controller.ts
2144
+ var STATUS_BY_ACTION = {
2145
+ shown: "shown",
2146
+ dismissed: "dismissed",
2147
+ cta_clicked: null,
2148
+ step_advanced: null,
2149
+ step_back: null,
2150
+ completed: "completed"
2151
+ };
2152
+ var GuidesController = class {
2153
+ constructor(client, config) {
2154
+ this.client = client;
2155
+ this.activeRenderers = /* @__PURE__ */ new Set();
2156
+ /**
2157
+ * Guías single-step actualmente en pantalla, por `guideId`. Evita repintar/
2158
+ * duplicar la misma guía en cada pageview de una SPA (p.ej. una guía inline
2159
+ * anclada a un elemento persistente). Se libera al cerrarse la guía.
2160
+ */
2161
+ this.activeByGuideId = /* @__PURE__ */ new Map();
2162
+ this.dispatched = /* @__PURE__ */ new Set();
2163
+ this.activeWalkthroughRenderer = null;
2164
+ this.activeWalkthroughGuide = null;
2165
+ this.debug = config.debug === true;
2166
+ this.apiUrl = config.apiUrl;
2167
+ this.apiKey = config.apiKey;
2168
+ this.resolver = config.resolver ?? new GuideResolverClient(config.apiUrl, config.apiKey, this.debug);
2169
+ if (config.trackerQueue) {
2170
+ this.trackerQueue = config.trackerQueue;
2171
+ } else {
2172
+ const httpClient = new GuideTrackerClient(config.apiUrl, config.apiKey);
2173
+ this.trackerQueue = new GuideTrackerQueue({
2174
+ client: httpClient,
2175
+ flushIntervalMs: DEFAULT_TRACKER_FLUSH_INTERVAL_MS,
2176
+ batchSize: DEFAULT_TRACKER_BATCH_SIZE,
2177
+ maxRetries: DEFAULT_TRACKER_MAX_RETRIES,
2178
+ onError: (err, item) => {
2179
+ if (this.debug) {
2180
+ console.warn(
2181
+ `[veo] dropped guide interaction after retries: guideId=${item.guideId} action=${item.payload.action}`,
2182
+ err
2183
+ );
2184
+ }
2185
+ }
2186
+ });
2187
+ }
2188
+ const namespace = namespaceFromApiKey(config.apiKey);
2189
+ this.frequencyCache = config.frequencyCache ?? new GuideFrequencyCache(namespace);
2190
+ this.walkthroughState = config.walkthroughState ?? new WalkthroughStateManager(namespace);
2191
+ }
2192
+ /** Cuántas interactions hay pendientes de envío. Útil para debug en demo. */
2193
+ get pendingInteractions() {
2194
+ return this.trackerQueue.size;
2195
+ }
2196
+ /** Limpia el cache local (botón "reset" del demo, tests). */
2197
+ clearFrequencyCache() {
2198
+ this.frequencyCache.clear();
2199
+ }
2200
+ /** Limpia el walkthrough activo (debug del demo). */
2201
+ clearWalkthroughState() {
2202
+ this.tearDownWalkthrough();
2203
+ }
2204
+ /** Punto de entrada principal. Llamado en cada pageview/identify. */
2205
+ async checkGuides(pageUrl, sessionId) {
2206
+ const endUserId = this.client.getEndUserId();
2207
+ if (!endUserId) {
2208
+ if (this.debug) console.log("[veo] guides: skipped, no endUserId yet");
2209
+ return;
2210
+ }
2211
+ if (await this.tryResumeWalkthrough(endUserId, sessionId, pageUrl)) {
2212
+ return;
2213
+ }
2214
+ let guides;
2215
+ try {
2216
+ guides = await this.resolver.resolve(endUserId, pageUrl);
2217
+ } catch (err) {
2218
+ if (this.debug) console.error("[veo] guides resolver threw:", err);
2219
+ return;
2220
+ }
2221
+ if (this.debug) console.log(`[veo] guides: ${guides.length} eligible at ${pageUrl}`);
2222
+ for (const guide of guides) {
2223
+ if (this.frequencyCache.shouldFilter(guide.guideId, guide.displayFrequency)) {
2224
+ if (this.debug) console.log(`[veo] guides: filtered locally guideId=${guide.guideId}`);
2225
+ continue;
2226
+ }
2227
+ if (this.activeByGuideId.has(guide.guideId)) {
2228
+ if (this.debug) console.log(`[veo] guides: already shown guideId=${guide.guideId}`);
2229
+ continue;
2230
+ }
2231
+ if (guide.guideType === "walkthrough" && guide.guideSteps.length > 1) {
2232
+ if (this.walkthroughState.hasActive()) {
2233
+ if (this.debug)
2234
+ console.log(`[veo] guides: walkthrough ${guide.guideId} skipped (another active)`);
2235
+ continue;
2236
+ }
2237
+ await this.startWalkthrough(guide, endUserId, sessionId, pageUrl);
2238
+ continue;
2239
+ }
2240
+ this.runSingleStepRender(guide, sessionId, pageUrl);
2241
+ }
2242
+ }
2243
+ /** Cierra todas las guías activas, limpia cache de dispatch y para timers. */
2244
+ destroy() {
2245
+ for (const renderer of this.activeRenderers) {
2246
+ try {
2247
+ renderer.destroy();
2248
+ } catch (err) {
2249
+ if (this.debug) console.error("[veo] renderer destroy failed:", err);
2250
+ }
2251
+ }
2252
+ this.activeRenderers.clear();
2253
+ this.activeByGuideId.clear();
2254
+ if (this.activeWalkthroughRenderer) {
2255
+ try {
2256
+ this.activeWalkthroughRenderer.destroy();
2257
+ } catch {
2258
+ }
2259
+ this.activeWalkthroughRenderer = null;
2260
+ this.activeWalkthroughGuide = null;
2261
+ }
2262
+ this.dispatched.clear();
2263
+ this.trackerQueue.destroy();
2264
+ }
2265
+ runSingleStepRender(guide, sessionId, pageUrl) {
2266
+ const renderer = this.createSingleStepRenderer(guide.guideType);
2267
+ if (!renderer) return;
2268
+ this.activeByGuideId.set(guide.guideId, renderer);
2269
+ const delayMs = guide.activationRules.delayMs ?? 0;
2270
+ window.setTimeout(
2271
+ () => this.mountSingleStepRenderer(guide, renderer, sessionId, pageUrl),
2272
+ delayMs
2273
+ );
2274
+ }
2275
+ mountSingleStepRenderer(guide, renderer, sessionId, pageUrl) {
2276
+ this.activeRenderers.add(renderer);
2277
+ this.activeByGuideId.set(guide.guideId, renderer);
2278
+ const onClose = () => {
2279
+ renderer.destroy();
2280
+ this.activeRenderers.delete(renderer);
2281
+ if (this.activeByGuideId.get(guide.guideId) === renderer) {
2282
+ this.activeByGuideId.delete(guide.guideId);
2283
+ }
2284
+ };
2285
+ const onInteraction = (event) => {
2286
+ this.handleInteraction(guide, sessionId, pageUrl, event);
2287
+ };
2288
+ const onTrack = (eventName, properties) => {
2289
+ this.client.track(eventName, properties);
2290
+ };
2291
+ const onFormSubmit = (answers) => this.submitFormResponse(guide.guideId, answers);
2292
+ try {
2293
+ void renderer.render({ guide, onInteraction, onClose, onTrack, onFormSubmit });
2294
+ } catch (err) {
2295
+ if (this.debug) console.error("[veo] renderer.render threw:", err);
2296
+ this.activeRenderers.delete(renderer);
2297
+ if (this.activeByGuideId.get(guide.guideId) === renderer) {
2298
+ this.activeByGuideId.delete(guide.guideId);
2299
+ }
2300
+ }
2301
+ }
2302
+ /**
2303
+ * Si hay un walkthrough persistido, intenta pintar el step actual en
2304
+ * la página actual. Retorna `true` si lo intentó (sea éxito o cancel),
2305
+ * `false` si no había walkthrough activo.
2306
+ *
2307
+ * Cancela limpiamente (sin enviar `dismissed` falso) cuando:
2308
+ * - La guía fue eliminada en el backend (404 → `fetchById` devuelve null).
2309
+ * - La guía cambió de tipo y ya no es walkthrough.
2310
+ * - El selector del step actual no aparece dentro del timeout.
2311
+ */
2312
+ async tryResumeWalkthrough(endUserId, sessionId, pageUrl) {
2313
+ const state = this.walkthroughState.current();
2314
+ if (!state) return false;
2315
+ const guide = await this.resolver.fetchById(state.guideId);
2316
+ if (!guide || guide.guideType !== "walkthrough") {
2317
+ if (this.debug)
2318
+ console.log(`[veo] guides: walkthrough ${state.guideId} no longer available, cleaning up`);
2319
+ this.tearDownWalkthrough();
2320
+ return true;
2321
+ }
2322
+ const stepIndex = Math.min(state.currentStepIndex, guide.guideSteps.length - 1);
2323
+ await this.renderWalkthroughStep(guide, stepIndex, endUserId, sessionId, pageUrl);
2324
+ return true;
2325
+ }
2326
+ async startWalkthrough(guide, endUserId, sessionId, pageUrl) {
2327
+ this.walkthroughState.start(guide.guideId, guide.guideSteps.length);
2328
+ const rendered = await this.renderWalkthroughStep(guide, 0, endUserId, sessionId, pageUrl);
2329
+ if (!rendered) return;
2330
+ this.enqueueInteractionOnce(guide.guideId, endUserId, sessionId, pageUrl, "shown", 0);
2331
+ this.frequencyCache.record(guide.guideId, "shown");
2332
+ }
2333
+ /**
2334
+ * Pinta el step indicado. Reusa la guía (no fetcha): el caller debe
2335
+ * traerla. Retorna `true` si el render fue exitoso. En caso contrario
2336
+ * ya disparó el teardown — el caller no debe seguir reportando.
2337
+ */
2338
+ async renderWalkthroughStep(guide, stepIndex, endUserId, sessionId, pageUrl) {
2339
+ if (this.activeWalkthroughRenderer) {
2340
+ this.activeWalkthroughRenderer.destroy();
2341
+ this.activeWalkthroughRenderer = null;
2342
+ }
2343
+ const renderer = new WalkthroughRenderer();
2344
+ this.activeWalkthroughGuide = guide;
2345
+ const callbacks = {
2346
+ onNext: () => this.handleWalkthroughNext(endUserId, sessionId, pageUrl),
2347
+ onBack: () => this.handleWalkthroughBack(endUserId, sessionId, pageUrl),
2348
+ onSkip: () => this.handleWalkthroughSkip(endUserId, sessionId, pageUrl),
2349
+ onComplete: () => this.handleWalkthroughComplete(endUserId, sessionId, pageUrl)
2350
+ };
2351
+ let success = false;
2352
+ try {
2353
+ success = await renderer.render({ guide, currentStepIndex: stepIndex, callbacks });
2354
+ } catch (err) {
2355
+ if (this.debug) console.error("[veo] walkthrough renderer threw:", err);
2356
+ }
2357
+ if (!success) {
2358
+ if (this.debug)
2359
+ console.warn(
2360
+ `[veo] walkthrough ${guide.guideId} step ${stepIndex} could not render, cancelling`
2361
+ );
2362
+ this.tearDownWalkthrough();
2363
+ return false;
2364
+ }
2365
+ this.activeWalkthroughRenderer = renderer;
2366
+ return true;
2367
+ }
2368
+ handleWalkthroughNext(endUserId, sessionId, pageUrl) {
2369
+ const guide = this.activeWalkthroughGuide;
2370
+ if (!guide) return;
2371
+ const newState = this.walkthroughState.advance();
2372
+ if (!newState) {
2373
+ this.handleWalkthroughComplete(endUserId, sessionId, pageUrl);
2374
+ return;
2375
+ }
2376
+ this.enqueueInteraction(
2377
+ guide.guideId,
2378
+ endUserId,
2379
+ sessionId,
2380
+ pageUrl,
2381
+ "step_advanced",
2382
+ newState.currentStepIndex
2383
+ );
2384
+ void this.renderWalkthroughStep(
2385
+ guide,
2386
+ newState.currentStepIndex,
2387
+ endUserId,
2388
+ sessionId,
2389
+ pageUrl
2390
+ );
2391
+ }
2392
+ handleWalkthroughBack(endUserId, sessionId, pageUrl) {
2393
+ const guide = this.activeWalkthroughGuide;
2394
+ if (!guide) return;
2395
+ const state = this.walkthroughState.current();
2396
+ if (!state || state.currentStepIndex === 0) return;
2397
+ const newState = this.walkthroughState.back();
2398
+ if (!newState) return;
2399
+ this.enqueueInteraction(
2400
+ guide.guideId,
2401
+ endUserId,
2402
+ sessionId,
2403
+ pageUrl,
2404
+ "step_back",
2405
+ newState.currentStepIndex
2406
+ );
2407
+ void this.renderWalkthroughStep(
2408
+ guide,
2409
+ newState.currentStepIndex,
2410
+ endUserId,
2411
+ sessionId,
2412
+ pageUrl
2413
+ );
2414
+ }
2415
+ handleWalkthroughSkip(endUserId, sessionId, pageUrl) {
2416
+ const guide = this.activeWalkthroughGuide;
2417
+ const state = this.walkthroughState.current();
2418
+ if (!guide || !state) return;
2419
+ this.enqueueInteraction(
2420
+ guide.guideId,
2421
+ endUserId,
2422
+ sessionId,
2423
+ pageUrl,
2424
+ "dismissed",
2425
+ state.currentStepIndex
2426
+ );
2427
+ this.frequencyCache.record(guide.guideId, "dismissed");
2428
+ this.tearDownWalkthrough();
2429
+ }
2430
+ handleWalkthroughComplete(endUserId, sessionId, pageUrl) {
2431
+ const guide = this.activeWalkthroughGuide;
2432
+ const state = this.walkthroughState.current();
2433
+ if (!guide || !state) return;
2434
+ this.enqueueInteraction(
2435
+ guide.guideId,
2436
+ endUserId,
2437
+ sessionId,
2438
+ pageUrl,
2439
+ "completed",
2440
+ state.currentStepIndex
2441
+ );
2442
+ this.frequencyCache.record(guide.guideId, "completed");
2443
+ this.tearDownWalkthrough();
2444
+ }
2445
+ tearDownWalkthrough() {
2446
+ if (this.activeWalkthroughRenderer) {
2447
+ try {
2448
+ this.activeWalkthroughRenderer.destroy();
2449
+ } catch {
2450
+ }
2451
+ }
2452
+ this.activeWalkthroughRenderer = null;
2453
+ this.activeWalkthroughGuide = null;
2454
+ this.walkthroughState.clear();
2455
+ }
2456
+ /**
2457
+ * Tras cada interaction single-step:
2458
+ * 1. dedupe (no enviar 2× la misma `(guideId, action)` por sesión)
2459
+ * 2. actualizar el cache local si corresponde
2460
+ * 3. encolar el POST
2461
+ *
2462
+ * El endUserId se lee del client EN ESTE MOMENTO (no en el render),
2463
+ * por si el usuario se identificó después de que la guía apareció.
2464
+ */
2465
+ handleInteraction(guide, sessionId, pageUrl, event) {
2466
+ const dedupKey = `${guide.guideId}:${event.action}`;
2467
+ if (this.dispatched.has(dedupKey)) {
2468
+ if (this.debug) console.log(`[veo] guides: dedup hit ${dedupKey}`);
2469
+ return;
2470
+ }
2471
+ this.dispatched.add(dedupKey);
2472
+ const cacheStatus = STATUS_BY_ACTION[event.action];
2473
+ if (cacheStatus) this.frequencyCache.record(guide.guideId, cacheStatus);
2474
+ const endUserId = this.client.getEndUserId();
2475
+ if (!endUserId) {
2476
+ if (this.debug)
2477
+ console.warn("[veo] guides: interaction without endUserId \u2014 skipping enqueue");
2478
+ return;
2479
+ }
2480
+ this.trackerQueue.enqueue({
2481
+ guideId: guide.guideId,
2482
+ payload: {
2483
+ endUserId,
2484
+ sessionId,
2485
+ action: event.action,
2486
+ stepIndex: event.stepIndex,
2487
+ pageUrl,
2488
+ pagePath: extractPath(pageUrl)
2489
+ }
2490
+ });
2491
+ }
2492
+ /**
2493
+ * Encola una interacción de walkthrough SIN pasar por el dedup set.
2494
+ * Acciones como `step_advanced` se repiten naturalmente (cada next es
2495
+ * una navegación distinta) y deduplicarlas perdería datos.
2496
+ */
2497
+ enqueueInteraction(guideId, endUserId, sessionId, pageUrl, action, stepIndex) {
2498
+ this.trackerQueue.enqueue({
2499
+ guideId,
2500
+ payload: {
2501
+ endUserId,
2502
+ sessionId,
2503
+ action,
2504
+ stepIndex,
2505
+ pageUrl,
2506
+ pagePath: extractPath(pageUrl)
2507
+ }
2508
+ });
2509
+ }
2510
+ /**
2511
+ * Encola con dedup por sesión del SDK. Para `shown` del primer step:
2512
+ * no queremos re-emitirlo si el usuario navega entre dos páginas que
2513
+ * matchean el mismo step.
2514
+ */
2515
+ enqueueInteractionOnce(guideId, endUserId, sessionId, pageUrl, action, stepIndex) {
2516
+ const dedupKey = `${guideId}:${action}:${stepIndex}`;
2517
+ if (this.dispatched.has(dedupKey)) return;
2518
+ this.dispatched.add(dedupKey);
2519
+ this.enqueueInteraction(guideId, endUserId, sessionId, pageUrl, action, stepIndex);
2520
+ }
2521
+ createSingleStepRenderer(type) {
2522
+ switch (type) {
2523
+ case "modal":
2524
+ return new ModalRenderer();
2525
+ case "banner":
2526
+ return new BannerRenderer();
2527
+ case "tooltip":
2528
+ return new TooltipRenderer();
2529
+ case "custom":
2530
+ return new CustomRenderer();
2531
+ case "inline":
2532
+ return new InlineRenderer();
2533
+ case "inline-custom":
2534
+ return new InlineCustomRenderer();
2535
+ case "form":
2536
+ return new FormRenderer();
2537
+ case "inline-form":
2538
+ return new InlineFormRenderer();
2539
+ case "walkthrough":
2540
+ return null;
2541
+ }
2542
+ }
2543
+ /**
2544
+ * Envía las respuestas de una guía `form` al backend, que las mergea como
2545
+ * propiedades del usuario. Usa el endUserId actual (o el anónimo si aún no
2546
+ * hubo identify — el backend pre-crea la fila igual que identify).
2547
+ */
2548
+ async submitFormResponse(guideId, answers) {
2549
+ const endUserId = this.client.getEndUserId() ?? this.client.getAnonymousId();
2550
+ try {
2551
+ const res = await fetch(`${this.apiUrl}/v1/guides/${guideId}/form-response`, {
2552
+ method: "POST",
2553
+ headers: { "Content-Type": "application/json", "X-Api-Key": this.apiKey },
2554
+ body: JSON.stringify({ endUserId, answers })
2555
+ });
2556
+ if (!res.ok && this.debug) {
2557
+ console.warn("[veo] form-response respondi\xF3", res.status);
2558
+ }
2559
+ return res.ok;
2560
+ } catch (err) {
2561
+ if (this.debug) console.warn("[veo] form-response fall\xF3:", err);
2562
+ return false;
2563
+ }
2564
+ }
2565
+ };
2566
+ function extractPath(url) {
2567
+ try {
2568
+ return new URL(url).pathname;
2569
+ } catch {
2570
+ return "/";
2571
+ }
2572
+ }
2573
+
2574
+ // src/plugins/guides/index.ts
2575
+ function guidesPlugin(config) {
2576
+ return definePlugin({
2577
+ name: "guides",
2578
+ install(client) {
2579
+ if (!hasWindow()) return;
2580
+ const controller = new GuidesController(client, config);
2581
+ window.__veoGuides = { controller };
2582
+ client.on("identify", ({ sessionId }) => {
2583
+ void controller.checkGuides(window.location.href, sessionId);
2584
+ });
2585
+ client.on("pageview", ({ url, sessionId }) => {
2586
+ void controller.checkGuides(url, sessionId);
2587
+ });
2588
+ void controller.checkGuides(window.location.href, client.getSessionId());
2589
+ }
2590
+ });
2591
+ }
2592
+
2593
+ // src/plugins/autocapture/constants.ts
2594
+ var DEFAULT_AUTOCAPTURE_CONFIG = {
2595
+ enabled: true,
2596
+ captureClicks: true,
2597
+ captureSubmits: true,
2598
+ captureChanges: true,
2599
+ maskAllInputs: true,
2600
+ maskAllText: false,
2601
+ throttleMs: 100,
2602
+ maxTextLength: 200,
2603
+ captureElementMetadata: true,
2604
+ captureAncestors: 5
2605
+ };
2606
+ var MAX_ANCESTORS = 10;
2607
+ var ALWAYS_BLOCK_SELECTORS = [
2608
+ "[data-veo-ignore]",
2609
+ ".veo-ignore",
2610
+ // Inputs sensibles
2611
+ 'input[type="password"]',
2612
+ 'input[type="email"]',
2613
+ 'input[type="tel"]',
2614
+ 'input[type="hidden"]',
2615
+ // Datos de tarjeta
2616
+ '[autocomplete="cc-number"]',
2617
+ '[autocomplete="cc-csc"]',
2618
+ '[autocomplete="cc-exp"]',
2619
+ '[autocomplete="cc-name"]',
2620
+ // Otros campos sensibles típicos
2621
+ '[name*="password" i]',
2622
+ '[name*="ssn" i]',
2623
+ '[name*="credit" i]',
2624
+ '[name*="card" i]',
2625
+ '[id*="password" i]',
2626
+ '[id*="ssn" i]'
2627
+ ];
2628
+ var SENSITIVE_ATTRIBUTES = ["value", "data-credit-card", "data-ssn", "data-password"];
2629
+ var PRIORITY_ATTRIBUTES = [
2630
+ "id",
2631
+ "name",
2632
+ "type",
2633
+ "role",
2634
+ "href",
2635
+ "data-action",
2636
+ "data-veo-tag",
2637
+ "data-testid",
2638
+ "aria-label",
2639
+ "aria-labelledby"
2640
+ ];
2641
+ var HASHED_CLASS_PATTERNS = [
2642
+ /^css-[a-z0-9]{4,}$/i,
2643
+ // emotion, styled-components nuevas
2644
+ /^_[a-z0-9]+_[a-z0-9]{4,}_\d+$/,
2645
+ // CSS Modules con hash
2646
+ /^[a-z]{2,4}-[a-z0-9]{6,}$/i,
2647
+ // Tailwind JIT (a veces)
2648
+ /^[a-f0-9]{6,}$/i,
2649
+ // hashes hex puros
2650
+ /^sc-[a-z0-9]+$/i
2651
+ // styled-components legacy
2652
+ ];
2653
+
2654
+ // src/plugins/autocapture/selector-builder.ts
2655
+ function isHashedClass(className) {
2656
+ return HASHED_CLASS_PATTERNS.some((pattern) => pattern.test(className));
2657
+ }
2658
+ function filterHumanClasses(element) {
2659
+ return Array.from(element.classList).filter((c) => !isHashedClass(c) && c.length < 64);
2660
+ }
2661
+ var STABLE_ATTRIBUTES = [
2662
+ "data-testid",
2663
+ "data-test",
2664
+ "data-cy",
2665
+ "name",
2666
+ "aria-label",
2667
+ "placeholder",
2668
+ "alt",
2669
+ "title",
2670
+ // `href` identifica muy bien a los links (`<a>`), un target frecuente de
2671
+ // autocapture; va después de los atributos semánticos pero antes de los
2672
+ // genéricos. Valores largos (URLs absolutas con query) se descartan por
2673
+ // MAX_ATTR_VALUE_LENGTH.
2674
+ "href",
2675
+ "role",
2676
+ "type"
2677
+ ];
2678
+ var MAX_ATTR_VALUE_LENGTH = 100;
2679
+ function cssEscape(value) {
2680
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
2681
+ return CSS.escape(value);
2682
+ }
2683
+ return value.replace(/[!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, "\\$&");
2684
+ }
2685
+ function escapeAttrValue(value) {
2686
+ return value.replace(/[\\"]/g, "\\$&");
2687
+ }
2688
+ function stableAttributeSelector(element, tag) {
2689
+ for (const attr of STABLE_ATTRIBUTES) {
2690
+ const value = element.getAttribute(attr);
2691
+ if (value && value.length > 0 && value.length <= MAX_ATTR_VALUE_LENGTH) {
2692
+ return `${tag}[${attr}="${escapeAttrValue(value)}"]`;
2693
+ }
2694
+ }
2695
+ return null;
2696
+ }
2697
+ function buildElementSelector(element) {
2698
+ const tag = element.tagName.toLowerCase();
2699
+ const veoTag = element.getAttribute("data-veo-tag");
2700
+ if (veoTag) {
2701
+ return `${tag}[data-veo-tag="${escapeAttrValue(veoTag)}"]`;
2702
+ }
2703
+ if (element.id) {
2704
+ const classes2 = filterHumanClasses(element);
2705
+ const classPart2 = classes2.length > 0 ? `.${classes2.map(cssEscape).join(".")}` : "";
2706
+ return `${tag}#${cssEscape(element.id)}${classPart2}`;
2707
+ }
2708
+ const attrSelector = stableAttributeSelector(element, tag);
2709
+ if (attrSelector) return attrSelector;
2710
+ const classes = filterHumanClasses(element);
2711
+ const classPart = classes.length > 0 ? `.${classes.map(cssEscape).join(".")}` : "";
2712
+ return `${tag}${classPart}`;
2713
+ }
2714
+ function buildSelectorPath(element, maxAncestors = 5) {
2715
+ const parts = [];
2716
+ let current = element;
2717
+ let depth = 0;
2718
+ while (current && current !== document.documentElement && depth <= maxAncestors) {
2719
+ parts.unshift(buildElementSelector(current));
2720
+ current = current.parentElement;
2721
+ depth++;
2722
+ }
2723
+ return parts.join(" > ");
2724
+ }
2725
+
2726
+ export { ALWAYS_BLOCK_SELECTORS, DEFAULT_AUTOCAPTURE_CONFIG, MAX_ANCESTORS, PRIORITY_ATTRIBUTES, SENSITIVE_ATTRIBUTES, buildSelectorPath, closeGuidePreview, definePlugin, filterHumanClasses, guidesPlugin, hasDocument, hasLocalStorage, hasNavigatorSendBeacon, hasWindow, previewGuide, runPreviewMode, uuidv7 };
2727
+ //# sourceMappingURL=chunk-BBAA5BRS.mjs.map
2728
+ //# sourceMappingURL=chunk-BBAA5BRS.mjs.map