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.
package/dist/index.cjs CHANGED
@@ -6,6 +6,16 @@ var dom = require('@floating-ui/dom');
6
6
  var SDK_VERSION = "0.0.1";
7
7
  var DEFAULT_FLUSH_INTERVAL_MS = 5e3;
8
8
  var DEFAULT_FLUSH_BATCH_SIZE = 20;
9
+ var DEFAULT_API_URL = "https://veo-backend-production-8c78.up.railway.app";
10
+
11
+ // src/core/api-url.ts
12
+ function resolveApiUrl(config) {
13
+ const fromConfig = config?.apiUrl;
14
+ if (typeof fromConfig === "string" && fromConfig.trim()) return fromConfig;
15
+ const fromGlobal = globalThis.__VEO_API_URL__;
16
+ if (typeof fromGlobal === "string" && fromGlobal.trim()) return fromGlobal;
17
+ return DEFAULT_API_URL;
18
+ }
9
19
 
10
20
  // src/utils/safe-env.ts
11
21
  function hasWindow() {
@@ -29,507 +39,70 @@ function hasNavigatorSendBeacon() {
29
39
  return typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function";
30
40
  }
31
41
 
32
- // src/utils/uuid.ts
33
- function uuidv7() {
34
- const timestamp = Date.now();
35
- const timeHex = timestamp.toString(16).padStart(12, "0");
36
- const random = new Uint8Array(10);
37
- if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
38
- crypto.getRandomValues(random);
39
- } else {
40
- for (let i = 0; i < random.length; i++) {
41
- random[i] = Math.floor(Math.random() * 256);
42
- }
42
+ // src/core/builder-loader.ts
43
+ var BUILDER_TOKEN_PARAM = "veoBuilder";
44
+ function isBuilderMode() {
45
+ if (!hasWindow()) return false;
46
+ try {
47
+ return new URLSearchParams(window.location.search).has(BUILDER_TOKEN_PARAM);
48
+ } catch {
49
+ return false;
43
50
  }
44
- random[0] = (random[0] ?? 0) & 15 | 112;
45
- random[2] = (random[2] ?? 0) & 63 | 128;
46
- const hex = Array.from(random, (b) => b.toString(16).padStart(2, "0")).join("");
47
- return [
48
- timeHex.slice(0, 8),
49
- timeHex.slice(8, 12),
50
- hex.slice(0, 4),
51
- hex.slice(4, 8),
52
- hex.slice(8, 20)
53
- ].join("-");
54
51
  }
55
-
56
- // src/core/queue.ts
57
- var RETRY_MAX_DELAY_MS = 3e4;
58
- var Queue = class {
59
- constructor(config) {
60
- this.config = config;
61
- this.buffer = [];
62
- this.timer = null;
63
- this.flushing = false;
64
- this.retryCount = 0;
65
- this.retryTimer = null;
66
- this.startTimer();
67
- }
68
- /** Encola un evento; dispara flush si se alcanza `batchSize`. */
69
- add(item) {
70
- this.buffer.push(item);
71
- if (this.buffer.length >= this.config.batchSize) {
72
- void this.flush();
73
- }
74
- }
75
- /** Flush manual vía `fetch`. Resuelve cuando termina (los retries son diferidos). */
76
- async flush() {
77
- if (this.flushing || this.buffer.length === 0) return;
78
- this.flushing = true;
79
- const batch = this.buffer.splice(0, this.buffer.length);
80
- try {
81
- await this.config.transport.send("/v1/events", { events: batch });
82
- this.retryCount = 0;
83
- } catch (error) {
84
- this.buffer.unshift(...batch);
85
- this.scheduleRetry();
86
- this.config.onError?.(error);
87
- } finally {
88
- this.flushing = false;
89
- }
90
- }
91
- /** Flush síncrono vía `sendBeacon`, para `pagehide`/`visibilitychange`. */
92
- flushBeacon() {
93
- if (this.buffer.length === 0) return;
94
- const batch = this.buffer.splice(0, this.buffer.length);
95
- const success = this.config.transport.sendBeacon("/v1/events", { events: batch });
96
- if (!success) {
97
- this.buffer.unshift(...batch);
98
- }
99
- }
100
- /** Detiene timers internos. Llamar al destruir el cliente. */
101
- destroy() {
102
- if (this.timer) clearInterval(this.timer);
103
- if (this.retryTimer) clearTimeout(this.retryTimer);
104
- this.timer = null;
105
- this.retryTimer = null;
106
- }
107
- startTimer() {
108
- this.timer = setInterval(() => {
109
- void this.flush();
110
- }, this.config.flushIntervalMs);
111
- }
112
- scheduleRetry() {
113
- const maxRetries = this.config.maxRetries ?? 3;
114
- if (this.retryCount >= maxRetries) {
115
- this.retryCount = 0;
116
- return;
117
- }
118
- this.retryCount++;
119
- const delayMs = Math.min(1e3 * 2 ** (this.retryCount - 1), RETRY_MAX_DELAY_MS);
120
- if (this.retryTimer) clearTimeout(this.retryTimer);
121
- this.retryTimer = setTimeout(() => {
122
- void this.flush();
123
- }, delayMs);
124
- }
125
- };
126
-
127
- // src/core/session.ts
128
- var ANONYMOUS_ID_KEY = "anonymous_user_id";
129
- var END_USER_ID_KEY = "end_user_id";
130
- var SESSION_ID_KEY = "session_id";
131
- var SESSION_LAST_ACTIVE_KEY = "session_last_active";
132
- var SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
133
- var Session = class {
134
- constructor(storage) {
135
- this.storage = storage;
136
- this._endUserId = null;
137
- this._anonymousId = this.loadOrCreateAnonymousId();
138
- this._sessionId = this.loadOrCreateSession();
139
- this._endUserId = this.storage.get(END_USER_ID_KEY);
140
- }
141
- get anonymousId() {
142
- return this._anonymousId;
143
- }
144
- get sessionId() {
145
- return this._sessionId;
146
- }
147
- get endUserId() {
148
- return this._endUserId;
149
- }
150
- /** ID a usar como visitor en eventos (end_user_id si está identificado, si no anonymous). */
151
- get effectiveUserId() {
152
- return this._endUserId ?? this._anonymousId;
153
- }
154
- setEndUserId(id) {
155
- this._endUserId = id;
156
- this.storage.set(END_USER_ID_KEY, id);
157
- }
158
- /** Renueva el timestamp de actividad. Si pasaron > 30 min, genera nueva sesión. */
159
- touch() {
160
- const lastActive = this.storage.get(SESSION_LAST_ACTIVE_KEY);
161
- const now = Date.now();
162
- if (lastActive && now - Number.parseInt(lastActive, 10) > SESSION_TIMEOUT_MS) {
163
- this._sessionId = uuidv7();
164
- this.storage.set(SESSION_ID_KEY, this._sessionId);
165
- }
166
- this.storage.set(SESSION_LAST_ACTIVE_KEY, String(now));
167
- }
168
- /** Limpia la identidad (reset) pero conserva el anonymous_id. */
169
- reset() {
170
- this._endUserId = null;
171
- this.storage.remove(END_USER_ID_KEY);
172
- this._sessionId = uuidv7();
173
- this.storage.set(SESSION_ID_KEY, this._sessionId);
174
- this.storage.set(SESSION_LAST_ACTIVE_KEY, String(Date.now()));
175
- }
176
- loadOrCreateAnonymousId() {
177
- let id = this.storage.get(ANONYMOUS_ID_KEY);
178
- if (!id) {
179
- id = uuidv7();
180
- this.storage.set(ANONYMOUS_ID_KEY, id);
181
- }
182
- return id;
183
- }
184
- loadOrCreateSession() {
185
- const existing = this.storage.get(SESSION_ID_KEY);
186
- const lastActive = this.storage.get(SESSION_LAST_ACTIVE_KEY);
187
- if (existing && lastActive) {
188
- const elapsed = Date.now() - Number.parseInt(lastActive, 10);
189
- if (elapsed < SESSION_TIMEOUT_MS) {
190
- return existing;
191
- }
192
- }
193
- const newId = uuidv7();
194
- this.storage.set(SESSION_ID_KEY, newId);
195
- this.storage.set(SESSION_LAST_ACTIVE_KEY, String(Date.now()));
196
- return newId;
52
+ function maybeLoadBuilder(opts) {
53
+ if (!hasWindow() || !hasDocument()) return;
54
+ let params;
55
+ try {
56
+ params = new URLSearchParams(window.location.search);
57
+ } catch {
58
+ return;
197
59
  }
198
- };
199
-
200
- // src/core/storage.ts
201
- var STORAGE_PREFIX = "veo:";
202
- var ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1e3;
203
- var LocalStorageAdapter = class {
204
- get(key) {
205
- return localStorage.getItem(STORAGE_PREFIX + key);
60
+ if (!params.has(BUILDER_TOKEN_PARAM)) return;
61
+ if (opts.apiKey && opts.apiUrl) {
62
+ window.__veoBuilder = {
63
+ apiKey: opts.apiKey,
64
+ apiUrl: opts.apiUrl
65
+ };
206
66
  }
207
- set(key, value) {
208
- try {
209
- localStorage.setItem(STORAGE_PREFIX + key, value);
210
- } catch {
67
+ if (window.veoBuilder) return;
68
+ const url = opts.builderUrl ?? deriveBuilderUrl();
69
+ if (!url) {
70
+ if (opts.debug) {
71
+ console.warn("[veo] builder mode pedido pero no pude resolver la URL de veo-builder.js");
211
72
  }
73
+ return;
212
74
  }
213
- remove(key) {
214
- localStorage.removeItem(STORAGE_PREFIX + key);
75
+ if (document.querySelector(`script[src="${url}"]`)) return;
76
+ const script = document.createElement("script");
77
+ script.src = url;
78
+ script.async = true;
79
+ if (opts.debug) {
80
+ script.addEventListener("error", () => console.error("[veo] no pude cargar", url));
215
81
  }
216
- clear() {
217
- const toRemove = [];
218
- for (let i = 0; i < localStorage.length; i++) {
219
- const key = localStorage.key(i);
220
- if (key?.startsWith(STORAGE_PREFIX)) toRemove.push(key);
82
+ document.head.appendChild(script);
83
+ }
84
+ function deriveBuilderUrl() {
85
+ const scripts = document.querySelectorAll("script[src]");
86
+ for (const script of Array.from(scripts)) {
87
+ if (/\bveo(\.min)?\.js(\?.*)?$/.test(script.src)) {
88
+ return script.src.replace(/\bveo(\.min)?\.js/, "veo-builder$1.js");
221
89
  }
222
- for (const key of toRemove) localStorage.removeItem(key);
223
90
  }
224
- };
225
- function readCookies() {
226
- const raw = document.cookie;
227
- return raw === "" ? [] : raw.split("; ");
91
+ return null;
228
92
  }
229
- var CookieAdapter = class {
230
- get(key) {
231
- const name = `${STORAGE_PREFIX}${key}=`;
232
- for (const cookie of readCookies()) {
233
- if (cookie.startsWith(name)) {
234
- return decodeURIComponent(cookie.slice(name.length));
235
- }
236
- }
93
+
94
+ // src/core/preview-loader.ts
95
+ var PREVIEW_TOKEN_PARAM = "veoPreview";
96
+ function getPreviewToken() {
97
+ if (!hasWindow()) return null;
98
+ try {
99
+ const value = new URLSearchParams(window.location.search).get(PREVIEW_TOKEN_PARAM);
100
+ return value ? value : null;
101
+ } catch {
237
102
  return null;
238
103
  }
239
- set(key, value, options) {
240
- const maxAgeSec = Math.floor((options?.ttlMs ?? ONE_YEAR_MS) / 1e3);
241
- const secure = typeof location !== "undefined" && location.protocol === "https:" ? "; Secure" : "";
242
- document.cookie = `${STORAGE_PREFIX}${key}=${encodeURIComponent(
243
- value
244
- )}; Path=/; Max-Age=${maxAgeSec}; SameSite=Lax${secure}`;
245
- }
246
- remove(key) {
247
- document.cookie = `${STORAGE_PREFIX}${key}=; Path=/; Max-Age=0; SameSite=Lax`;
248
- }
249
- clear() {
250
- for (const cookie of readCookies()) {
251
- const eq = cookie.indexOf("=");
252
- const name = eq === -1 ? cookie : cookie.slice(0, eq);
253
- if (name.startsWith(STORAGE_PREFIX)) {
254
- this.remove(name.slice(STORAGE_PREFIX.length));
255
- }
256
- }
257
- }
258
- };
259
- var MemoryAdapter = class {
260
- constructor() {
261
- this.store = /* @__PURE__ */ new Map();
262
- }
263
- get(key) {
264
- return this.store.get(key) ?? null;
265
- }
266
- set(key, value) {
267
- this.store.set(key, value);
268
- }
269
- remove(key) {
270
- this.store.delete(key);
271
- }
272
- clear() {
273
- this.store.clear();
274
- }
275
- };
276
- function createStorage(preferred = "localStorage") {
277
- if (preferred === "localStorage" && hasLocalStorage()) return new LocalStorageAdapter();
278
- if (preferred === "cookie" && hasDocument()) return new CookieAdapter();
279
- if (preferred === "memory") return new MemoryAdapter();
280
- if (hasLocalStorage()) return new LocalStorageAdapter();
281
- if (hasDocument()) return new CookieAdapter();
282
- return new MemoryAdapter();
283
104
  }
284
105
 
285
- // src/core/transport.ts
286
- var TransportError = class extends Error {
287
- constructor(message, statusCode) {
288
- super(message);
289
- this.statusCode = statusCode;
290
- this.name = "TransportError";
291
- }
292
- };
293
- var Transport = class {
294
- constructor(config) {
295
- this.config = config;
296
- }
297
- /** Envío normal con `fetch`. Lanza `TransportError` si la respuesta no es OK. */
298
- async send(path, body) {
299
- const url = `${this.config.apiUrl}${path}`;
300
- const controller = new AbortController();
301
- const timeoutId = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 1e4);
302
- try {
303
- const response = await fetch(url, {
304
- method: "POST",
305
- headers: {
306
- "Content-Type": "application/json",
307
- "X-Api-Key": this.config.apiKey,
308
- "X-Sdk-Version": this.config.sdkVersion
309
- },
310
- body: JSON.stringify(body),
311
- keepalive: true,
312
- signal: controller.signal
313
- });
314
- if (!response.ok) {
315
- throw new TransportError(`HTTP ${response.status}`, response.status);
316
- }
317
- return await response.json();
318
- } finally {
319
- clearTimeout(timeoutId);
320
- }
321
- }
322
- /**
323
- * Envío con `navigator.sendBeacon`. Fire-and-forget, pensado para `pagehide`.
324
- * Como sendBeacon no admite headers, la API key viaja por query string.
325
- */
326
- sendBeacon(path, body) {
327
- if (!hasNavigatorSendBeacon()) return false;
328
- const url = new URL(`${this.config.apiUrl}${path}`);
329
- url.searchParams.set("key", this.config.apiKey);
330
- url.searchParams.set("v", this.config.sdkVersion);
331
- const blob = new Blob([JSON.stringify(body)], { type: "application/json" });
332
- return navigator.sendBeacon(url.toString(), blob);
333
- }
334
- };
335
-
336
- // src/core/client.ts
337
- var ORGANIZATION_ID_KEY = "organization_id";
338
- var END_USER_ID_KEY2 = "end_user_id";
339
- var IDENTIFY_SIG_KEY = "identify_signature";
340
- function stableStringify(value) {
341
- if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
342
- if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
343
- const entries = Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`);
344
- return `{${entries.join(",")}}`;
345
- }
346
- var Client = class {
347
- constructor(config) {
348
- this.config = config;
349
- this.plugins = [];
350
- this.currentOrganizationId = null;
351
- this.listeners = {
352
- pageview: /* @__PURE__ */ new Set(),
353
- identify: /* @__PURE__ */ new Set()
354
- };
355
- if (!config.apiKey) throw new Error("apiKey is required");
356
- if (!config.apiUrl) throw new Error("apiUrl is required");
357
- this.storage = createStorage(config.storage ?? "localStorage");
358
- this.session = new Session(this.storage);
359
- this.transport = new Transport({
360
- apiKey: config.apiKey,
361
- apiUrl: config.apiUrl.replace(/\/$/, ""),
362
- sdkVersion: SDK_VERSION
363
- });
364
- this.queue = new Queue({
365
- transport: this.transport,
366
- flushIntervalMs: config.flushInterval ?? DEFAULT_FLUSH_INTERVAL_MS,
367
- batchSize: config.flushBatchSize ?? DEFAULT_FLUSH_BATCH_SIZE,
368
- onError: (err) => {
369
- if (config.debug) console.error("[veo] flush error:", err);
370
- }
371
- });
372
- const persistedUserId = this.storage.get(END_USER_ID_KEY2);
373
- if (persistedUserId) this.session.setEndUserId(persistedUserId);
374
- const persistedOrgId = this.storage.get(ORGANIZATION_ID_KEY);
375
- if (persistedOrgId) this.currentOrganizationId = persistedOrgId;
376
- this.setupUnloadHandlers();
377
- if (config.debug) {
378
- console.log("[veo] initialized", {
379
- anonymousId: this.session.anonymousId,
380
- sessionId: this.session.sessionId,
381
- endUserId: this.session.endUserId
382
- });
383
- }
384
- }
385
- identify(payload) {
386
- const { visitor, organization } = payload;
387
- const signature = stableStringify({
388
- id: visitor.id,
389
- traits: visitor.traits ?? {},
390
- org: organization ? { id: organization.id, attributes: organization.attributes ?? {} } : null
391
- });
392
- if (this.session.endUserId === visitor.id && this.storage.get(IDENTIFY_SIG_KEY) === signature) {
393
- if (this.config.debug) console.debug("[veo] identify: sin cambios, omitido");
394
- return;
395
- }
396
- const previousAnonymousId = this.session.endUserId === null ? this.session.anonymousId : void 0;
397
- this.session.setEndUserId(visitor.id);
398
- if (organization) {
399
- this.currentOrganizationId = organization.id;
400
- this.storage.set(ORGANIZATION_ID_KEY, organization.id);
401
- }
402
- const endUser = {
403
- id: visitor.id,
404
- traits: visitor.traits ?? {},
405
- ...previousAnonymousId !== void 0 ? { anonymousId: previousAnonymousId } : {}
406
- };
407
- const body = {
408
- endUser,
409
- ...organization ? { organization: { id: organization.id, attributes: organization.attributes ?? {} } } : {}
410
- };
411
- void this.transport.send("/v1/identify", body).catch((err) => {
412
- if (this.config.debug) console.error("[veo] identify failed:", err);
413
- });
414
- this.enqueue({
415
- actionType: "identify",
416
- actionProperties: {
417
- traits: visitor.traits,
418
- organizationAttributes: organization?.attributes
419
- }
420
- });
421
- this.storage.set(IDENTIFY_SIG_KEY, signature);
422
- this.emit("identify", { endUserId: visitor.id, sessionId: this.session.sessionId });
423
- }
424
- track(name, properties) {
425
- if (!name || typeof name !== "string") {
426
- if (this.config.debug) console.error("[veo] track: name is required");
427
- return;
428
- }
429
- this.enqueue({
430
- actionType: "track",
431
- actionName: name,
432
- ...properties !== void 0 ? { actionProperties: properties } : {}
433
- });
434
- }
435
- pageview(path) {
436
- if (!hasDocument()) return;
437
- const url = window.location.href;
438
- const pagePath = path ?? window.location.pathname;
439
- this.enqueue({
440
- actionType: "pageview",
441
- pageUrl: url,
442
- pagePath,
443
- pageTitle: document.title,
444
- pageReferrer: document.referrer
445
- });
446
- this.emit("pageview", {
447
- url,
448
- path: pagePath,
449
- sessionId: this.session.sessionId,
450
- endUserId: this.session.endUserId
451
- });
452
- }
453
- reset() {
454
- this.session.reset();
455
- this.currentOrganizationId = null;
456
- this.storage.remove(ORGANIZATION_ID_KEY);
457
- this.storage.remove(END_USER_ID_KEY2);
458
- this.storage.remove(IDENTIFY_SIG_KEY);
459
- }
460
- flush() {
461
- return this.queue.flush();
462
- }
463
- use(plugin) {
464
- this.plugins.push(plugin);
465
- plugin.install(this);
466
- return this;
467
- }
468
- getEndUserId() {
469
- return this.session.endUserId;
470
- }
471
- getSessionId() {
472
- return this.session.sessionId;
473
- }
474
- /**
475
- * Suscribe `listener` a `event`. Devuelve una función para desuscribir.
476
- * Los errores en listeners se logean en debug mode pero NO se propagan
477
- * (un plugin no debe poder romper a otro).
478
- */
479
- on(event, listener) {
480
- this.listeners[event].add(listener);
481
- return () => {
482
- this.listeners[event].delete(listener);
483
- };
484
- }
485
- emit(event, payload) {
486
- for (const listener of this.listeners[event]) {
487
- try {
488
- listener(payload);
489
- } catch (err) {
490
- if (this.config.debug) console.error(`[veo] listener for '${event}' failed:`, err);
491
- }
492
- }
493
- }
494
- /**
495
- * Construye el evento completo y lo agrega a la queue. Omite los campos
496
- * opcionales no definidos (exigido por `exactOptionalPropertyTypes`).
497
- * @internal
498
- */
499
- enqueue(input) {
500
- this.session.touch();
501
- const organizationId = this.currentOrganizationId ?? void 0;
502
- const pageUrl = input.pageUrl ?? (hasWindow() ? window.location.href : void 0);
503
- const pagePath = input.pagePath ?? (hasWindow() ? window.location.pathname : void 0);
504
- const pageTitle = input.pageTitle ?? (hasDocument() ? document.title : void 0);
505
- const pageReferrer = input.pageReferrer ?? (hasDocument() ? document.referrer : void 0);
506
- const event = {
507
- actionId: uuidv7(),
508
- endUserId: this.session.effectiveUserId,
509
- sessionId: this.session.sessionId,
510
- actionType: input.actionType,
511
- occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
512
- ...organizationId !== void 0 ? { organizationId } : {},
513
- ...input.actionName !== void 0 ? { actionName: input.actionName } : {},
514
- ...pageUrl !== void 0 ? { pageUrl } : {},
515
- ...pagePath !== void 0 ? { pagePath } : {},
516
- ...pageTitle !== void 0 ? { pageTitle } : {},
517
- ...pageReferrer !== void 0 ? { pageReferrer } : {},
518
- ...input.actionProperties !== void 0 ? { actionProperties: input.actionProperties } : {}
519
- };
520
- this.queue.add(event);
521
- }
522
- setupUnloadHandlers() {
523
- if (!hasWindow()) return;
524
- window.addEventListener("pagehide", () => this.queue.flushBeacon());
525
- window.addEventListener("visibilitychange", () => {
526
- if (document.visibilityState === "hidden") {
527
- this.queue.flushBeacon();
528
- }
529
- });
530
- }
531
- };
532
-
533
106
  // src/plugins/plugin.ts
534
107
  function definePlugin(plugin) {
535
108
  return plugin;
@@ -756,119 +329,690 @@ var ElementThrottle = class {
756
329
  this.windowMs = windowMs;
757
330
  this.lastFired = /* @__PURE__ */ new WeakMap();
758
331
  }
759
- /** Devuelve true si el elemento puede emitir ahora (y registra el disparo). */
760
- shouldFire(element) {
761
- const now = Date.now();
762
- const last = this.lastFired.get(element) ?? 0;
763
- if (now - last < this.windowMs) return false;
764
- this.lastFired.set(element, now);
765
- return true;
332
+ /** Devuelve true si el elemento puede emitir ahora (y registra el disparo). */
333
+ shouldFire(element) {
334
+ const now = Date.now();
335
+ const last = this.lastFired.get(element) ?? 0;
336
+ if (now - last < this.windowMs) return false;
337
+ this.lastFired.set(element, now);
338
+ return true;
339
+ }
340
+ };
341
+
342
+ // src/plugins/autocapture/dom-listener.ts
343
+ var DomListener = class {
344
+ constructor(client, config) {
345
+ this.client = client;
346
+ this.cleanups = [];
347
+ const d = DEFAULT_AUTOCAPTURE_CONFIG;
348
+ this.config = {
349
+ enabled: config.enabled ?? d.enabled,
350
+ captureClicks: config.captureClicks ?? d.captureClicks,
351
+ captureSubmits: config.captureSubmits ?? d.captureSubmits,
352
+ captureChanges: config.captureChanges ?? d.captureChanges,
353
+ maskAllInputs: config.maskAllInputs ?? d.maskAllInputs,
354
+ maskAllText: config.maskAllText ?? d.maskAllText,
355
+ blockSelectors: config.blockSelectors ?? [],
356
+ throttleMs: config.throttleMs ?? d.throttleMs,
357
+ maxTextLength: config.maxTextLength ?? d.maxTextLength,
358
+ captureElementMetadata: config.captureElementMetadata ?? d.captureElementMetadata,
359
+ captureAncestors: Math.min(config.captureAncestors ?? d.captureAncestors, MAX_ANCESTORS)
360
+ };
361
+ this.throttle = new ElementThrottle(this.config.throttleMs);
362
+ }
363
+ install() {
364
+ if (!hasDocument() || !this.config.enabled) return;
365
+ if (this.config.captureClicks) this.attach("click", (e) => this.handleClick(e));
366
+ if (this.config.captureSubmits) this.attach("submit", (e) => this.handleSubmit(e), true);
367
+ if (this.config.captureChanges) this.attach("change", (e) => this.handleChange(e), true);
368
+ }
369
+ destroy() {
370
+ for (const cleanup of this.cleanups) cleanup();
371
+ this.cleanups = [];
372
+ }
373
+ attach(eventName, handler, useCapture = false) {
374
+ const listener = (e) => {
375
+ if (!e.isTrusted) return;
376
+ try {
377
+ handler(e);
378
+ } catch {
379
+ }
380
+ };
381
+ document.addEventListener(eventName, listener, useCapture);
382
+ this.cleanups.push(() => document.removeEventListener(eventName, listener, useCapture));
383
+ }
384
+ handleClick(event) {
385
+ const target = event.target;
386
+ if (!(target instanceof Element)) return;
387
+ const meaningful = this.findMeaningfulAncestor(target);
388
+ if (!meaningful) return;
389
+ if (isElementBlocked(meaningful, this.config.blockSelectors)) return;
390
+ if (!this.throttle.shouldFire(meaningful)) return;
391
+ this.emit("click", meaningful);
392
+ }
393
+ handleSubmit(event) {
394
+ const target = event.target;
395
+ if (!(target instanceof Element) || target.tagName.toLowerCase() !== "form") return;
396
+ if (isElementBlocked(target, this.config.blockSelectors)) return;
397
+ if (!this.throttle.shouldFire(target)) return;
398
+ this.emit("submit", target);
399
+ }
400
+ handleChange(event) {
401
+ const target = event.target;
402
+ if (!(target instanceof Element)) return;
403
+ const tag = target.tagName.toLowerCase();
404
+ if (tag !== "select" && tag !== "input" && tag !== "textarea") return;
405
+ if (target instanceof HTMLInputElement && target.type !== "checkbox" && target.type !== "radio") {
406
+ return;
407
+ }
408
+ if (isElementBlocked(target, this.config.blockSelectors)) return;
409
+ if (!this.throttle.shouldFire(target)) return;
410
+ this.emit("change", target);
411
+ }
412
+ /** Busca hacia arriba un ancestro "interactivo". Devuelve null si no hay ninguno. */
413
+ findMeaningfulAncestor(element) {
414
+ let current = element;
415
+ let depth = 0;
416
+ while (current && current !== document.documentElement && depth < 5) {
417
+ const tag = current.tagName.toLowerCase();
418
+ const isInteractive = tag === "button" || tag === "a" || tag === "input" || tag === "select" || tag === "textarea" || current.hasAttribute("data-veo-tag") || current.hasAttribute("role") || current.hasAttribute("onclick") || current instanceof HTMLElement && current.style.cursor === "pointer";
419
+ if (isInteractive) return current;
420
+ current = current.parentElement;
421
+ depth++;
422
+ }
423
+ return null;
424
+ }
425
+ emit(eventType, element) {
426
+ const payload = buildAutocapturePayload(eventType, element, this.config);
427
+ this.client.track("$autocapture", payload);
428
+ }
429
+ };
430
+
431
+ // src/plugins/autocapture/index.ts
432
+ function autocapturePlugin(config = {}) {
433
+ let listener = null;
434
+ return definePlugin({
435
+ name: "autocapture",
436
+ install(client) {
437
+ listener = new DomListener(client, config);
438
+ listener.install();
439
+ },
440
+ destroy() {
441
+ listener?.destroy();
442
+ listener = null;
443
+ }
444
+ });
445
+ }
446
+
447
+ // src/plugins/spa-router.ts
448
+ function spaRouterPlugin() {
449
+ return definePlugin({
450
+ name: "spa-router",
451
+ install(client) {
452
+ if (!hasWindow()) return;
453
+ client.pageview();
454
+ const fire = () => {
455
+ queueMicrotask(() => client.pageview());
456
+ };
457
+ const origPushState = window.history.pushState;
458
+ const origReplaceState = window.history.replaceState;
459
+ window.history.pushState = function(...args) {
460
+ origPushState.apply(this, args);
461
+ fire();
462
+ };
463
+ window.history.replaceState = function(...args) {
464
+ origReplaceState.apply(this, args);
465
+ fire();
466
+ };
467
+ window.addEventListener("popstate", fire);
468
+ window.addEventListener("hashchange", fire);
469
+ }
470
+ });
471
+ }
472
+
473
+ // src/utils/uuid.ts
474
+ function uuidv7() {
475
+ const timestamp = Date.now();
476
+ const timeHex = timestamp.toString(16).padStart(12, "0");
477
+ const random = new Uint8Array(10);
478
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
479
+ crypto.getRandomValues(random);
480
+ } else {
481
+ for (let i = 0; i < random.length; i++) {
482
+ random[i] = Math.floor(Math.random() * 256);
483
+ }
484
+ }
485
+ random[0] = (random[0] ?? 0) & 15 | 112;
486
+ random[2] = (random[2] ?? 0) & 63 | 128;
487
+ const hex = Array.from(random, (b) => b.toString(16).padStart(2, "0")).join("");
488
+ return [
489
+ timeHex.slice(0, 8),
490
+ timeHex.slice(8, 12),
491
+ hex.slice(0, 4),
492
+ hex.slice(4, 8),
493
+ hex.slice(8, 20)
494
+ ].join("-");
495
+ }
496
+
497
+ // src/core/queue.ts
498
+ var RETRY_MAX_DELAY_MS = 3e4;
499
+ var Queue = class {
500
+ constructor(config) {
501
+ this.config = config;
502
+ this.buffer = [];
503
+ this.timer = null;
504
+ this.flushing = false;
505
+ this.retryCount = 0;
506
+ this.retryTimer = null;
507
+ this.startTimer();
508
+ }
509
+ /** Encola un evento; dispara flush si se alcanza `batchSize`. */
510
+ add(item) {
511
+ this.buffer.push(item);
512
+ if (this.buffer.length >= this.config.batchSize) {
513
+ void this.flush();
514
+ }
515
+ }
516
+ /** Flush manual vía `fetch`. Resuelve cuando termina (los retries son diferidos). */
517
+ async flush() {
518
+ if (this.flushing || this.buffer.length === 0) return;
519
+ this.flushing = true;
520
+ const batch = this.buffer.splice(0, this.buffer.length);
521
+ try {
522
+ await this.config.transport.send("/v1/events", { events: batch });
523
+ this.retryCount = 0;
524
+ } catch (error) {
525
+ this.buffer.unshift(...batch);
526
+ this.scheduleRetry();
527
+ this.config.onError?.(error);
528
+ } finally {
529
+ this.flushing = false;
530
+ }
531
+ }
532
+ /** Flush síncrono vía `sendBeacon`, para `pagehide`/`visibilitychange`. */
533
+ flushBeacon() {
534
+ if (this.buffer.length === 0) return;
535
+ const batch = this.buffer.splice(0, this.buffer.length);
536
+ const success = this.config.transport.sendBeacon("/v1/events", { events: batch });
537
+ if (!success) {
538
+ this.buffer.unshift(...batch);
539
+ }
540
+ }
541
+ /** Detiene timers internos. Llamar al destruir el cliente. */
542
+ destroy() {
543
+ if (this.timer) clearInterval(this.timer);
544
+ if (this.retryTimer) clearTimeout(this.retryTimer);
545
+ this.timer = null;
546
+ this.retryTimer = null;
547
+ }
548
+ startTimer() {
549
+ this.timer = setInterval(() => {
550
+ void this.flush();
551
+ }, this.config.flushIntervalMs);
552
+ }
553
+ scheduleRetry() {
554
+ const maxRetries = this.config.maxRetries ?? 3;
555
+ if (this.retryCount >= maxRetries) {
556
+ this.retryCount = 0;
557
+ return;
558
+ }
559
+ this.retryCount++;
560
+ const delayMs = Math.min(1e3 * 2 ** (this.retryCount - 1), RETRY_MAX_DELAY_MS);
561
+ if (this.retryTimer) clearTimeout(this.retryTimer);
562
+ this.retryTimer = setTimeout(() => {
563
+ void this.flush();
564
+ }, delayMs);
565
+ }
566
+ };
567
+
568
+ // src/core/session.ts
569
+ var ANONYMOUS_ID_KEY = "anonymous_user_id";
570
+ var END_USER_ID_KEY = "end_user_id";
571
+ var SESSION_ID_KEY = "session_id";
572
+ var SESSION_LAST_ACTIVE_KEY = "session_last_active";
573
+ var SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
574
+ var Session = class {
575
+ constructor(storage) {
576
+ this.storage = storage;
577
+ this._endUserId = null;
578
+ this._anonymousId = this.loadOrCreateAnonymousId();
579
+ this._sessionId = this.loadOrCreateSession();
580
+ this._endUserId = this.storage.get(END_USER_ID_KEY);
581
+ }
582
+ get anonymousId() {
583
+ return this._anonymousId;
584
+ }
585
+ get sessionId() {
586
+ return this._sessionId;
587
+ }
588
+ get endUserId() {
589
+ return this._endUserId;
590
+ }
591
+ /** ID a usar como visitor en eventos (end_user_id si está identificado, si no anonymous). */
592
+ get effectiveUserId() {
593
+ return this._endUserId ?? this._anonymousId;
594
+ }
595
+ setEndUserId(id) {
596
+ this._endUserId = id;
597
+ this.storage.set(END_USER_ID_KEY, id);
598
+ }
599
+ /** Renueva el timestamp de actividad. Si pasaron > 30 min, genera nueva sesión. */
600
+ touch() {
601
+ const lastActive = this.storage.get(SESSION_LAST_ACTIVE_KEY);
602
+ const now = Date.now();
603
+ if (lastActive && now - Number.parseInt(lastActive, 10) > SESSION_TIMEOUT_MS) {
604
+ this._sessionId = uuidv7();
605
+ this.storage.set(SESSION_ID_KEY, this._sessionId);
606
+ }
607
+ this.storage.set(SESSION_LAST_ACTIVE_KEY, String(now));
608
+ }
609
+ /** Limpia la identidad (reset) pero conserva el anonymous_id. */
610
+ reset() {
611
+ this._endUserId = null;
612
+ this.storage.remove(END_USER_ID_KEY);
613
+ this._sessionId = uuidv7();
614
+ this.storage.set(SESSION_ID_KEY, this._sessionId);
615
+ this.storage.set(SESSION_LAST_ACTIVE_KEY, String(Date.now()));
616
+ }
617
+ loadOrCreateAnonymousId() {
618
+ let id = this.storage.get(ANONYMOUS_ID_KEY);
619
+ if (!id) {
620
+ id = uuidv7();
621
+ this.storage.set(ANONYMOUS_ID_KEY, id);
622
+ }
623
+ return id;
624
+ }
625
+ loadOrCreateSession() {
626
+ const existing = this.storage.get(SESSION_ID_KEY);
627
+ const lastActive = this.storage.get(SESSION_LAST_ACTIVE_KEY);
628
+ if (existing && lastActive) {
629
+ const elapsed = Date.now() - Number.parseInt(lastActive, 10);
630
+ if (elapsed < SESSION_TIMEOUT_MS) {
631
+ return existing;
632
+ }
633
+ }
634
+ const newId = uuidv7();
635
+ this.storage.set(SESSION_ID_KEY, newId);
636
+ this.storage.set(SESSION_LAST_ACTIVE_KEY, String(Date.now()));
637
+ return newId;
638
+ }
639
+ };
640
+
641
+ // src/core/storage.ts
642
+ var STORAGE_PREFIX = "veo:";
643
+ var ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1e3;
644
+ var LocalStorageAdapter = class {
645
+ get(key) {
646
+ return localStorage.getItem(STORAGE_PREFIX + key);
647
+ }
648
+ set(key, value) {
649
+ try {
650
+ localStorage.setItem(STORAGE_PREFIX + key, value);
651
+ } catch {
652
+ }
653
+ }
654
+ remove(key) {
655
+ localStorage.removeItem(STORAGE_PREFIX + key);
656
+ }
657
+ clear() {
658
+ const toRemove = [];
659
+ for (let i = 0; i < localStorage.length; i++) {
660
+ const key = localStorage.key(i);
661
+ if (key?.startsWith(STORAGE_PREFIX)) toRemove.push(key);
662
+ }
663
+ for (const key of toRemove) localStorage.removeItem(key);
664
+ }
665
+ };
666
+ function readCookies() {
667
+ const raw = document.cookie;
668
+ return raw === "" ? [] : raw.split("; ");
669
+ }
670
+ var CookieAdapter = class {
671
+ get(key) {
672
+ const name = `${STORAGE_PREFIX}${key}=`;
673
+ for (const cookie of readCookies()) {
674
+ if (cookie.startsWith(name)) {
675
+ return decodeURIComponent(cookie.slice(name.length));
676
+ }
677
+ }
678
+ return null;
679
+ }
680
+ set(key, value, options) {
681
+ const maxAgeSec = Math.floor((options?.ttlMs ?? ONE_YEAR_MS) / 1e3);
682
+ const secure = typeof location !== "undefined" && location.protocol === "https:" ? "; Secure" : "";
683
+ document.cookie = `${STORAGE_PREFIX}${key}=${encodeURIComponent(
684
+ value
685
+ )}; Path=/; Max-Age=${maxAgeSec}; SameSite=Lax${secure}`;
686
+ }
687
+ remove(key) {
688
+ document.cookie = `${STORAGE_PREFIX}${key}=; Path=/; Max-Age=0; SameSite=Lax`;
689
+ }
690
+ clear() {
691
+ for (const cookie of readCookies()) {
692
+ const eq = cookie.indexOf("=");
693
+ const name = eq === -1 ? cookie : cookie.slice(0, eq);
694
+ if (name.startsWith(STORAGE_PREFIX)) {
695
+ this.remove(name.slice(STORAGE_PREFIX.length));
696
+ }
697
+ }
698
+ }
699
+ };
700
+ var MemoryAdapter = class {
701
+ constructor() {
702
+ this.store = /* @__PURE__ */ new Map();
703
+ }
704
+ get(key) {
705
+ return this.store.get(key) ?? null;
706
+ }
707
+ set(key, value) {
708
+ this.store.set(key, value);
709
+ }
710
+ remove(key) {
711
+ this.store.delete(key);
712
+ }
713
+ clear() {
714
+ this.store.clear();
715
+ }
716
+ };
717
+ function createStorage(preferred = "localStorage") {
718
+ if (preferred === "localStorage" && hasLocalStorage()) return new LocalStorageAdapter();
719
+ if (preferred === "cookie" && hasDocument()) return new CookieAdapter();
720
+ if (preferred === "memory") return new MemoryAdapter();
721
+ if (hasLocalStorage()) return new LocalStorageAdapter();
722
+ if (hasDocument()) return new CookieAdapter();
723
+ return new MemoryAdapter();
724
+ }
725
+
726
+ // src/core/transport.ts
727
+ var TransportError = class extends Error {
728
+ constructor(message, statusCode) {
729
+ super(message);
730
+ this.statusCode = statusCode;
731
+ this.name = "TransportError";
732
+ }
733
+ };
734
+ var Transport = class {
735
+ constructor(config) {
736
+ this.config = config;
737
+ }
738
+ /** Envío normal con `fetch`. Lanza `TransportError` si la respuesta no es OK. */
739
+ async send(path, body) {
740
+ const url = `${this.config.apiUrl}${path}`;
741
+ const controller = new AbortController();
742
+ const timeoutMs = this.config.timeoutMs ?? 1e4;
743
+ const timeoutId = setTimeout(
744
+ () => controller.abort(new DOMException(`Veo request timeout (${timeoutMs}ms)`, "TimeoutError")),
745
+ timeoutMs
746
+ );
747
+ try {
748
+ const response = await fetch(url, {
749
+ method: "POST",
750
+ headers: {
751
+ "Content-Type": "application/json",
752
+ "X-Api-Key": this.config.apiKey,
753
+ "X-Sdk-Version": this.config.sdkVersion
754
+ },
755
+ body: JSON.stringify(body),
756
+ // NO usar `keepalive: true` en los flushes normales: tiene límites
757
+ // estrictos del navegador (cuota de requests/64KB) y produce abortos
758
+ // espurios cuando hay varios envíos seguidos. El envío fiable al cerrar
759
+ // la página ya lo cubre `sendBeacon()` (ver Queue.flushBeacon).
760
+ signal: controller.signal
761
+ });
762
+ if (!response.ok) {
763
+ throw new TransportError(`HTTP ${response.status}`, response.status);
764
+ }
765
+ return await response.json();
766
+ } finally {
767
+ clearTimeout(timeoutId);
768
+ }
769
+ }
770
+ /**
771
+ * Envío con `navigator.sendBeacon`. Fire-and-forget, pensado para `pagehide`.
772
+ * Como sendBeacon no admite headers, la API key viaja por query string.
773
+ */
774
+ sendBeacon(path, body) {
775
+ if (!hasNavigatorSendBeacon()) return false;
776
+ const url = new URL(`${this.config.apiUrl}${path}`);
777
+ url.searchParams.set("key", this.config.apiKey);
778
+ url.searchParams.set("v", this.config.sdkVersion);
779
+ const blob = new Blob([JSON.stringify(body)], { type: "application/json" });
780
+ return navigator.sendBeacon(url.toString(), blob);
781
+ }
782
+ };
783
+
784
+ // src/core/client.ts
785
+ var ORGANIZATION_ID_KEY = "organization_id";
786
+ var END_USER_ID_KEY2 = "end_user_id";
787
+ var IDENTIFY_SIG_KEY = "identify_signature";
788
+ function stableStringify(value) {
789
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
790
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
791
+ const entries = Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`);
792
+ return `{${entries.join(",")}}`;
793
+ }
794
+ var Client = class {
795
+ constructor(config) {
796
+ this.config = config;
797
+ this.plugins = [];
798
+ this.currentOrganizationId = null;
799
+ this.listeners = {
800
+ pageview: /* @__PURE__ */ new Set(),
801
+ identify: /* @__PURE__ */ new Set()
802
+ };
803
+ if (!config.apiKey) throw new Error("apiKey is required");
804
+ this.storage = createStorage(config.storage ?? "localStorage");
805
+ this.session = new Session(this.storage);
806
+ this.transport = new Transport({
807
+ apiKey: config.apiKey,
808
+ apiUrl: resolveApiUrl(config).replace(/\/$/, ""),
809
+ sdkVersion: SDK_VERSION
810
+ });
811
+ this.queue = new Queue({
812
+ transport: this.transport,
813
+ flushIntervalMs: config.flushInterval ?? DEFAULT_FLUSH_INTERVAL_MS,
814
+ batchSize: config.flushBatchSize ?? DEFAULT_FLUSH_BATCH_SIZE,
815
+ onError: (err) => {
816
+ if (config.debug) console.error("[veo] flush error:", err);
817
+ }
818
+ });
819
+ const persistedUserId = this.storage.get(END_USER_ID_KEY2);
820
+ if (persistedUserId) this.session.setEndUserId(persistedUserId);
821
+ const persistedOrgId = this.storage.get(ORGANIZATION_ID_KEY);
822
+ if (persistedOrgId) this.currentOrganizationId = persistedOrgId;
823
+ this.setupUnloadHandlers();
824
+ if (config.debug) {
825
+ console.log("[veo] initialized", {
826
+ anonymousId: this.session.anonymousId,
827
+ sessionId: this.session.sessionId,
828
+ endUserId: this.session.endUserId
829
+ });
830
+ }
831
+ }
832
+ identify(payload) {
833
+ const { visitor, organization } = payload;
834
+ const signature = stableStringify({
835
+ id: visitor.id,
836
+ traits: visitor.traits ?? {},
837
+ org: organization ? { id: organization.id, attributes: organization.attributes ?? {} } : null
838
+ });
839
+ if (this.session.endUserId === visitor.id && this.storage.get(IDENTIFY_SIG_KEY) === signature) {
840
+ if (this.config.debug) console.debug("[veo] identify: sin cambios, omitido");
841
+ return;
842
+ }
843
+ const previousAnonymousId = this.session.endUserId === null ? this.session.anonymousId : void 0;
844
+ this.session.setEndUserId(visitor.id);
845
+ if (organization) {
846
+ this.currentOrganizationId = organization.id;
847
+ this.storage.set(ORGANIZATION_ID_KEY, organization.id);
848
+ }
849
+ const endUser = {
850
+ id: visitor.id,
851
+ traits: visitor.traits ?? {},
852
+ ...previousAnonymousId !== void 0 ? { anonymousId: previousAnonymousId } : {}
853
+ };
854
+ const body = {
855
+ endUser,
856
+ ...organization ? { organization: { id: organization.id, attributes: organization.attributes ?? {} } } : {}
857
+ };
858
+ void this.transport.send("/v1/identify", body).catch((err) => {
859
+ if (this.config.debug) console.error("[veo] identify failed:", err);
860
+ });
861
+ this.enqueue({
862
+ actionType: "identify",
863
+ actionProperties: {
864
+ traits: visitor.traits,
865
+ organizationAttributes: organization?.attributes
866
+ }
867
+ });
868
+ this.storage.set(IDENTIFY_SIG_KEY, signature);
869
+ this.emit("identify", { endUserId: visitor.id, sessionId: this.session.sessionId });
870
+ }
871
+ track(name, properties) {
872
+ if (!name || typeof name !== "string") {
873
+ if (this.config.debug) console.error("[veo] track: name is required");
874
+ return;
875
+ }
876
+ this.enqueue({
877
+ actionType: "track",
878
+ actionName: name,
879
+ ...properties !== void 0 ? { actionProperties: properties } : {}
880
+ });
881
+ }
882
+ pageview(path) {
883
+ if (!hasDocument()) return;
884
+ const url = window.location.href;
885
+ const pagePath = path ?? window.location.pathname;
886
+ this.enqueue({
887
+ actionType: "pageview",
888
+ pageUrl: url,
889
+ pagePath,
890
+ pageTitle: document.title,
891
+ pageReferrer: document.referrer
892
+ });
893
+ this.emit("pageview", {
894
+ url,
895
+ path: pagePath,
896
+ sessionId: this.session.sessionId,
897
+ endUserId: this.session.endUserId
898
+ });
899
+ }
900
+ reset() {
901
+ this.session.reset();
902
+ this.currentOrganizationId = null;
903
+ this.storage.remove(ORGANIZATION_ID_KEY);
904
+ this.storage.remove(END_USER_ID_KEY2);
905
+ this.storage.remove(IDENTIFY_SIG_KEY);
766
906
  }
767
- };
768
-
769
- // src/plugins/autocapture/dom-listener.ts
770
- var DomListener = class {
771
- constructor(client, config) {
772
- this.client = client;
773
- this.cleanups = [];
774
- const d = DEFAULT_AUTOCAPTURE_CONFIG;
775
- this.config = {
776
- enabled: config.enabled ?? d.enabled,
777
- captureClicks: config.captureClicks ?? d.captureClicks,
778
- captureSubmits: config.captureSubmits ?? d.captureSubmits,
779
- captureChanges: config.captureChanges ?? d.captureChanges,
780
- maskAllInputs: config.maskAllInputs ?? d.maskAllInputs,
781
- maskAllText: config.maskAllText ?? d.maskAllText,
782
- blockSelectors: config.blockSelectors ?? [],
783
- throttleMs: config.throttleMs ?? d.throttleMs,
784
- maxTextLength: config.maxTextLength ?? d.maxTextLength,
785
- captureElementMetadata: config.captureElementMetadata ?? d.captureElementMetadata,
786
- captureAncestors: Math.min(config.captureAncestors ?? d.captureAncestors, MAX_ANCESTORS)
787
- };
788
- this.throttle = new ElementThrottle(this.config.throttleMs);
907
+ flush() {
908
+ return this.queue.flush();
789
909
  }
790
- install() {
791
- if (!hasDocument() || !this.config.enabled) return;
792
- if (this.config.captureClicks) this.attach("click", (e) => this.handleClick(e));
793
- if (this.config.captureSubmits) this.attach("submit", (e) => this.handleSubmit(e), true);
794
- if (this.config.captureChanges) this.attach("change", (e) => this.handleChange(e), true);
910
+ use(plugin) {
911
+ this.plugins.push(plugin);
912
+ plugin.install(this);
913
+ return this;
795
914
  }
796
- destroy() {
797
- for (const cleanup of this.cleanups) cleanup();
798
- this.cleanups = [];
915
+ getEndUserId() {
916
+ return this.session.endUserId;
799
917
  }
800
- attach(eventName, handler, useCapture = false) {
801
- const listener = (e) => {
802
- if (!e.isTrusted) return;
803
- try {
804
- handler(e);
805
- } catch {
806
- }
807
- };
808
- document.addEventListener(eventName, listener, useCapture);
809
- this.cleanups.push(() => document.removeEventListener(eventName, listener, useCapture));
918
+ getAnonymousId() {
919
+ return this.session.anonymousId;
810
920
  }
811
- handleClick(event) {
812
- const target = event.target;
813
- if (!(target instanceof Element)) return;
814
- const meaningful = this.findMeaningfulAncestor(target);
815
- if (!meaningful) return;
816
- if (isElementBlocked(meaningful, this.config.blockSelectors)) return;
817
- if (!this.throttle.shouldFire(meaningful)) return;
818
- this.emit("click", meaningful);
921
+ getSessionId() {
922
+ return this.session.sessionId;
819
923
  }
820
- handleSubmit(event) {
821
- const target = event.target;
822
- if (!(target instanceof Element) || target.tagName.toLowerCase() !== "form") return;
823
- if (isElementBlocked(target, this.config.blockSelectors)) return;
824
- if (!this.throttle.shouldFire(target)) return;
825
- this.emit("submit", target);
924
+ /**
925
+ * Suscribe `listener` a `event`. Devuelve una función para desuscribir.
926
+ * Los errores en listeners se logean en debug mode pero NO se propagan
927
+ * (un plugin no debe poder romper a otro).
928
+ */
929
+ on(event, listener) {
930
+ this.listeners[event].add(listener);
931
+ return () => {
932
+ this.listeners[event].delete(listener);
933
+ };
826
934
  }
827
- handleChange(event) {
828
- const target = event.target;
829
- if (!(target instanceof Element)) return;
830
- const tag = target.tagName.toLowerCase();
831
- if (tag !== "select" && tag !== "input" && tag !== "textarea") return;
832
- if (target instanceof HTMLInputElement && target.type !== "checkbox" && target.type !== "radio") {
833
- return;
935
+ emit(event, payload) {
936
+ for (const listener of this.listeners[event]) {
937
+ try {
938
+ listener(payload);
939
+ } catch (err) {
940
+ if (this.config.debug) console.error(`[veo] listener for '${event}' failed:`, err);
941
+ }
834
942
  }
835
- if (isElementBlocked(target, this.config.blockSelectors)) return;
836
- if (!this.throttle.shouldFire(target)) return;
837
- this.emit("change", target);
838
943
  }
839
- /** Busca hacia arriba un ancestro "interactivo". Devuelve null si no hay ninguno. */
840
- findMeaningfulAncestor(element) {
841
- let current = element;
842
- let depth = 0;
843
- while (current && current !== document.documentElement && depth < 5) {
844
- const tag = current.tagName.toLowerCase();
845
- const isInteractive = tag === "button" || tag === "a" || tag === "input" || tag === "select" || tag === "textarea" || current.hasAttribute("data-veo-tag") || current.hasAttribute("role") || current.hasAttribute("onclick") || current instanceof HTMLElement && current.style.cursor === "pointer";
846
- if (isInteractive) return current;
847
- current = current.parentElement;
848
- depth++;
849
- }
850
- return null;
944
+ /**
945
+ * Construye el evento completo y lo agrega a la queue. Omite los campos
946
+ * opcionales no definidos (exigido por `exactOptionalPropertyTypes`).
947
+ * @internal
948
+ */
949
+ enqueue(input) {
950
+ this.session.touch();
951
+ const organizationId = this.currentOrganizationId ?? void 0;
952
+ const pageUrl = input.pageUrl ?? (hasWindow() ? window.location.href : void 0);
953
+ const pagePath = input.pagePath ?? (hasWindow() ? window.location.pathname : void 0);
954
+ const pageTitle = input.pageTitle ?? (hasDocument() ? document.title : void 0);
955
+ const pageReferrer = input.pageReferrer ?? (hasDocument() ? document.referrer : void 0);
956
+ const event = {
957
+ actionId: uuidv7(),
958
+ endUserId: this.session.effectiveUserId,
959
+ sessionId: this.session.sessionId,
960
+ actionType: input.actionType,
961
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
962
+ ...organizationId !== void 0 ? { organizationId } : {},
963
+ ...input.actionName !== void 0 ? { actionName: input.actionName } : {},
964
+ ...pageUrl !== void 0 ? { pageUrl } : {},
965
+ ...pagePath !== void 0 ? { pagePath } : {},
966
+ ...pageTitle !== void 0 ? { pageTitle } : {},
967
+ ...pageReferrer !== void 0 ? { pageReferrer } : {},
968
+ ...input.actionProperties !== void 0 ? { actionProperties: input.actionProperties } : {}
969
+ };
970
+ this.queue.add(event);
851
971
  }
852
- emit(eventType, element) {
853
- const payload = buildAutocapturePayload(eventType, element, this.config);
854
- this.client.track("$autocapture", payload);
972
+ setupUnloadHandlers() {
973
+ if (!hasWindow()) return;
974
+ window.addEventListener("pagehide", () => this.queue.flushBeacon());
975
+ window.addEventListener("visibilitychange", () => {
976
+ if (document.visibilityState === "hidden") {
977
+ this.queue.flushBeacon();
978
+ }
979
+ });
855
980
  }
856
981
  };
857
982
 
858
- // src/plugins/autocapture/index.ts
859
- function autocapturePlugin(config = {}) {
860
- let listener = null;
861
- return definePlugin({
862
- name: "autocapture",
863
- install(client) {
864
- listener = new DomListener(client, config);
865
- listener.install();
866
- },
867
- destroy() {
868
- listener?.destroy();
869
- listener = null;
983
+ // src/core/setup.ts
984
+ function setupClient(config, onGuides) {
985
+ const client = new Client(config);
986
+ client.use(spaRouterPlugin());
987
+ if (config.autocapture !== false) {
988
+ const autocaptureConfig = typeof config.autocapture === "object" ? config.autocapture : {};
989
+ client.use(autocapturePlugin(autocaptureConfig));
990
+ }
991
+ onGuides(client);
992
+ if (typeof window !== "undefined") {
993
+ const stub = window.veo;
994
+ const pending = stub?._q ?? [];
995
+ window.veo = client;
996
+ for (const entry of pending) {
997
+ const [method, args] = entry;
998
+ if (method === "init") continue;
999
+ const fn = client[method];
1000
+ if (typeof fn === "function") {
1001
+ try {
1002
+ fn.apply(client, Array.from(args));
1003
+ } catch (err) {
1004
+ if (config.debug) console.error("[veo] drain error:", err);
1005
+ }
1006
+ }
870
1007
  }
871
- });
1008
+ maybeLoadBuilder({
1009
+ ...config.builderUrl !== void 0 ? { builderUrl: config.builderUrl } : {},
1010
+ ...config.debug !== void 0 ? { debug: config.debug } : {},
1011
+ ...config.apiKey !== void 0 ? { apiKey: config.apiKey } : {},
1012
+ apiUrl: resolveApiUrl(config)
1013
+ });
1014
+ }
1015
+ return client;
872
1016
  }
873
1017
 
874
1018
  // src/plugins/guides/constants.ts
@@ -905,15 +1049,20 @@ var GuideFrequencyCache = class {
905
1049
  * Decide si una guía debe filtrarse localmente antes de pintarla.
906
1050
  *
907
1051
  * - `every_visit` / `always` → nunca filtra (siempre re-pinta).
908
- * - `once` → cualquier entry registrada filtra.
909
- * - `until_dismissed` → solo si `status === 'dismissed'`.
1052
+ * - `once` → cualquier entry registrada filtra (verla ya cuenta).
1053
+ * - `until_dismissed` → si fue descartada O COMPLETADA (responder un
1054
+ * form / terminar un walkthrough es más fuerte que descartar — sin
1055
+ * esto, una encuesta seguiría preguntando después de respondida).
910
1056
  */
911
1057
  shouldFilter(guideId, displayFrequency) {
912
1058
  if (displayFrequency === "every_visit" || displayFrequency === "always") return false;
913
1059
  const entry = this.cache.get(guideId);
914
1060
  if (!entry) return false;
915
1061
  if (displayFrequency === "once") return true;
916
- if (displayFrequency === "until_dismissed") return entry.status === "dismissed";
1062
+ if (displayFrequency === "until_dismissed") {
1063
+ return entry.status === "dismissed" || entry.status === "completed";
1064
+ }
1065
+ if (displayFrequency === "until_answered") return entry.status === "completed";
917
1066
  return false;
918
1067
  }
919
1068
  /**
@@ -1303,6 +1452,88 @@ function readCtaUrl(step) {
1303
1452
  return isSafeUrl(candidate) ? candidate : void 0;
1304
1453
  }
1305
1454
 
1455
+ // src/plugins/guides/guide-design.ts
1456
+ var DARK_THEME = {
1457
+ "--veo-bg": "#1f2937",
1458
+ "--veo-text": "#f9fafb",
1459
+ "--veo-text-secondary": "#d1d5db",
1460
+ "--veo-shadow": "0 20px 60px rgba(0, 0, 0, 0.55)"
1461
+ };
1462
+ var ALIGN_JUSTIFY = {
1463
+ left: "flex-start",
1464
+ center: "center",
1465
+ right: "flex-end"
1466
+ };
1467
+ var SHADOW = {
1468
+ none: "none",
1469
+ soft: "0 4px 14px rgba(0, 0, 0, 0.10)",
1470
+ medium: "0 8px 30px rgba(0, 0, 0, 0.18)",
1471
+ strong: "0 24px 60px rgba(0, 0, 0, 0.32)"
1472
+ };
1473
+ var PRESET_POS = {
1474
+ center: [0.5, 0.5],
1475
+ top: [0.5, 0],
1476
+ bottom: [0.5, 1],
1477
+ left: [0, 0.5],
1478
+ right: [1, 0.5],
1479
+ "top-left": [0, 0],
1480
+ "top-right": [1, 0],
1481
+ "bottom-left": [0, 1],
1482
+ "bottom-right": [1, 1]
1483
+ };
1484
+ var COLOR_RE = /^#[0-9a-f]{3,8}$|^(rgb|rgba|hsl|hsla)\([\d.,%/\sdeg]+\)$|^[a-z]{3,20}$/i;
1485
+ function clamp(n, min, max) {
1486
+ return Math.min(max, Math.max(min, n));
1487
+ }
1488
+ function applyDesignVars(host, style) {
1489
+ if (!style || typeof style !== "object") return;
1490
+ const s = style;
1491
+ if (typeof s.accentColor === "string" && COLOR_RE.test(s.accentColor.trim())) {
1492
+ host.style.setProperty("--veo-primary", s.accentColor.trim());
1493
+ }
1494
+ if (s.theme === "dark") {
1495
+ for (const [key, value] of Object.entries(DARK_THEME)) {
1496
+ host.style.setProperty(key, value);
1497
+ }
1498
+ }
1499
+ if (typeof s.radius === "number" && Number.isFinite(s.radius)) {
1500
+ host.style.setProperty("--veo-radius", `${clamp(s.radius, 0, 48)}px`);
1501
+ }
1502
+ if (typeof s.width === "number" && Number.isFinite(s.width)) {
1503
+ host.style.setProperty("--veo-width", `${clamp(s.width, 220, 720)}px`);
1504
+ }
1505
+ if (s.align === "left" || s.align === "center" || s.align === "right") {
1506
+ host.style.setProperty("--veo-actions-justify", ALIGN_JUSTIFY[s.align]);
1507
+ }
1508
+ if (typeof s.padding === "number" && Number.isFinite(s.padding)) {
1509
+ host.style.setProperty("--veo-pad", `${clamp(s.padding, 0, 64)}px`);
1510
+ }
1511
+ if (typeof s.margin === "number" && Number.isFinite(s.margin)) {
1512
+ host.style.setProperty("--veo-margin", `${clamp(s.margin, 0, 64)}px`);
1513
+ }
1514
+ if (typeof s.borderWidth === "number" && Number.isFinite(s.borderWidth)) {
1515
+ host.style.setProperty("--veo-border-width", `${clamp(s.borderWidth, 0, 16)}px`);
1516
+ }
1517
+ if (typeof s.borderColor === "string" && COLOR_RE.test(s.borderColor.trim())) {
1518
+ host.style.setProperty("--veo-border-color", s.borderColor.trim());
1519
+ }
1520
+ if (typeof s.shadow === "string" && SHADOW[s.shadow]) {
1521
+ host.style.setProperty("--veo-shadow", SHADOW[s.shadow]);
1522
+ }
1523
+ let px;
1524
+ let py;
1525
+ if (typeof s.posX === "number" && typeof s.posY === "number") {
1526
+ px = clamp(s.posX, 0, 1);
1527
+ py = clamp(s.posY, 0, 1);
1528
+ } else if (typeof s.overlayPosition === "string" && PRESET_POS[s.overlayPosition]) {
1529
+ [px, py] = PRESET_POS[s.overlayPosition];
1530
+ }
1531
+ if (px !== void 0 && py !== void 0) {
1532
+ host.style.setProperty("--veo-pos-x", `${px * 100}%`);
1533
+ host.style.setProperty("--veo-pos-y", `${py * 100}%`);
1534
+ }
1535
+ }
1536
+
1306
1537
  // src/plugins/guides/styles.ts
1307
1538
  var GUIDE_STYLES = `
1308
1539
  :host {
@@ -1312,6 +1543,8 @@ var GUIDE_STYLES = `
1312
1543
  --veo-bg: #ffffff;
1313
1544
  --veo-radius: 12px;
1314
1545
  --veo-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
1546
+ --veo-width: 420px;
1547
+ --veo-actions-justify: flex-end;
1315
1548
  all: initial;
1316
1549
  }
1317
1550
  * { box-sizing: border-box; font-family: -apple-system, system-ui, "Segoe UI", Roboto, sans-serif; }
@@ -1319,26 +1552,36 @@ var GUIDE_STYLES = `
1319
1552
  .veo-modal-overlay {
1320
1553
  position: fixed; inset: 0;
1321
1554
  background: rgba(15, 15, 30, 0.45);
1322
- display: flex; align-items: center; justify-content: center;
1323
1555
  z-index: ${GUIDE_Z_INDEX};
1324
1556
  animation: veo-fade-in 180ms ease-out;
1325
1557
  }
1558
+ /*
1559
+ * Posici\xF3n libre/preset: --veo-pos-x/y son porcentajes (default 50% = centro).
1560
+ * El truco translate(-pos) alinea la MISMA fracci\xF3n de la tarjeta con esa
1561
+ * fracci\xF3n del overlay, as\xED la tarjeta nunca se sale (0% = pegada izquierda,
1562
+ * 100% = pegada derecha, 50% = centrada) sea cual sea su ancho.
1563
+ */
1326
1564
  .veo-modal-card {
1565
+ position: absolute;
1566
+ left: var(--veo-pos-x, 50%);
1567
+ top: var(--veo-pos-y, 50%);
1568
+ transform: translate(calc(var(--veo-pos-x, 50%) * -1), calc(var(--veo-pos-y, 50%) * -1));
1327
1569
  background: var(--veo-bg);
1328
1570
  border-radius: var(--veo-radius);
1329
- padding: 24px 28px;
1330
- max-width: 420px; width: 90%;
1331
- position: relative;
1571
+ border: var(--veo-border-width, 0) solid var(--veo-border-color, transparent);
1572
+ padding: var(--veo-pad, 24px);
1573
+ max-width: var(--veo-width); width: 90%;
1332
1574
  box-shadow: var(--veo-shadow);
1333
- animation: veo-slide-up 220ms cubic-bezier(0.16, 1, 0.3, 1);
1575
+ animation: veo-fade-in 180ms ease-out;
1334
1576
  }
1335
1577
 
1336
1578
  .veo-banner {
1337
1579
  position: fixed; left: 0; right: 0;
1338
1580
  background: var(--veo-bg);
1339
- padding: 14px 20px;
1581
+ border: var(--veo-border-width, 0) solid var(--veo-border-color, transparent);
1582
+ padding: var(--veo-pad, 16px);
1340
1583
  z-index: ${GUIDE_Z_INDEX};
1341
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
1584
+ box-shadow: var(--veo-shadow, 0 2px 12px rgba(0, 0, 0, 0.12));
1342
1585
  animation: veo-slide-down 200ms ease-out;
1343
1586
  }
1344
1587
  .veo-banner-top { top: 0; }
@@ -1348,13 +1591,26 @@ var GUIDE_STYLES = `
1348
1591
  position: absolute;
1349
1592
  background: var(--veo-bg);
1350
1593
  border-radius: var(--veo-radius);
1351
- padding: 14px 16px;
1594
+ border: var(--veo-border-width, 0) solid var(--veo-border-color, transparent);
1595
+ padding: var(--veo-pad, 15px);
1352
1596
  max-width: 300px;
1353
1597
  z-index: ${GUIDE_Z_INDEX};
1354
- box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
1598
+ box-shadow: var(--veo-shadow, 0 8px 30px rgba(0, 0, 0, 0.18));
1355
1599
  animation: veo-fade-in 150ms ease-out;
1356
1600
  }
1357
1601
 
1602
+ .veo-inline {
1603
+ background: var(--veo-bg);
1604
+ border-radius: var(--veo-radius);
1605
+ border: var(--veo-border-width, 0) solid var(--veo-border-color, transparent);
1606
+ padding: var(--veo-pad, 20px);
1607
+ max-width: var(--veo-width);
1608
+ margin: var(--veo-margin, 12px) 0;
1609
+ position: relative;
1610
+ box-shadow: var(--veo-shadow);
1611
+ animation: veo-fade-in 160ms ease-out;
1612
+ }
1613
+
1358
1614
  .veo-guide-content {
1359
1615
  position: relative;
1360
1616
  display: flex; flex-direction: column;
@@ -1373,7 +1629,7 @@ var GUIDE_STYLES = `
1373
1629
  margin: 0 0 16px; color: var(--veo-text-secondary);
1374
1630
  }
1375
1631
  .veo-guide-actions {
1376
- display: flex; gap: 8px; justify-content: flex-end;
1632
+ display: flex; gap: 8px; justify-content: var(--veo-actions-justify);
1377
1633
  }
1378
1634
  .veo-guide-cta {
1379
1635
  background: var(--veo-primary); color: #fff; border: none;
@@ -1392,16 +1648,66 @@ var GUIDE_STYLES = `
1392
1648
  }
1393
1649
  .veo-guide-close:hover { color: var(--veo-text); }
1394
1650
 
1651
+ .veo-form { display: flex; flex-direction: column; gap: 14px; margin-bottom: 16px; }
1652
+ .veo-form-field { display: flex; flex-direction: column; gap: 6px; }
1653
+ .veo-form-label { font-size: 13px; font-weight: 500; color: var(--veo-text); }
1654
+ .veo-form-input {
1655
+ font-size: 14px; color: var(--veo-text);
1656
+ background: var(--veo-bg);
1657
+ border: 1px solid #d1d5db; border-radius: 8px;
1658
+ padding: 8px 10px; width: 100%;
1659
+ }
1660
+ .veo-form-input:focus { outline: 2px solid var(--veo-primary); outline-offset: -1px; border-color: transparent; }
1661
+ .veo-form-options { display: flex; flex-direction: column; gap: 6px; }
1662
+ .veo-form-option {
1663
+ display: flex; align-items: center; gap: 8px;
1664
+ font-size: 14px; color: var(--veo-text); cursor: pointer;
1665
+ }
1666
+ .veo-form-option input { accent-color: var(--veo-primary); margin: 0; cursor: pointer; }
1667
+ .veo-form-scale { display: flex; gap: 4px; flex-wrap: wrap; }
1668
+ .veo-nps-btn {
1669
+ min-width: 30px; height: 30px; padding: 0 4px;
1670
+ font-size: 13px; font-weight: 500; color: var(--veo-text);
1671
+ background: var(--veo-bg); border: 1px solid #d1d5db; border-radius: 6px;
1672
+ cursor: pointer; transition: all 100ms ease;
1673
+ }
1674
+ .veo-nps-btn:hover { border-color: var(--veo-primary); }
1675
+ .veo-rating-btn {
1676
+ background: none; border: none; padding: 0 2px;
1677
+ font-size: 26px; line-height: 1; color: #d1d5db;
1678
+ cursor: pointer; transition: color 100ms ease;
1679
+ }
1680
+ .veo-scale-active.veo-nps-btn { background: var(--veo-primary); color: #fff; border-color: var(--veo-primary); }
1681
+ .veo-scale-active.veo-rating-btn { color: #f59e0b; }
1682
+ .veo-form-error { font-size: 13px; color: #dc2626; margin: 0; }
1683
+ .veo-form-thanks {
1684
+ text-align: center; padding: 24px 8px;
1685
+ font-size: 16px; font-weight: 600; color: var(--veo-text);
1686
+ }
1687
+ .veo-form-preview-note {
1688
+ text-align: center; margin: 0 0 12px;
1689
+ font-size: 12px; color: #b45309;
1690
+ }
1691
+
1395
1692
  .veo-walkthrough {
1396
1693
  position: absolute;
1397
1694
  background: var(--veo-bg);
1398
1695
  border-radius: var(--veo-radius);
1399
- padding: 16px;
1696
+ border: var(--veo-border-width, 0) solid var(--veo-border-color, transparent);
1697
+ padding: var(--veo-pad, 16px);
1400
1698
  max-width: 340px;
1401
1699
  z-index: ${GUIDE_Z_INDEX};
1402
- box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
1700
+ box-shadow: var(--veo-shadow, 0 8px 30px rgba(0, 0, 0, 0.2));
1403
1701
  animation: veo-fade-in 150ms ease-out;
1404
1702
  }
1703
+ .veo-walkthrough-arrow {
1704
+ position: absolute;
1705
+ width: 12px;
1706
+ height: 12px;
1707
+ background: var(--veo-bg);
1708
+ transform: rotate(45deg);
1709
+ pointer-events: none;
1710
+ }
1405
1711
  .veo-walkthrough-counter {
1406
1712
  font-size: 11px; color: #9ca3af;
1407
1713
  margin-bottom: 8px; letter-spacing: 0.02em;
@@ -1455,6 +1761,10 @@ var GUIDE_STYLES = `
1455
1761
  max-width: calc(100vw - 16px);
1456
1762
  animation: veo-fade-in 160ms ease;
1457
1763
  }
1764
+ .veo-custom-inline {
1765
+ display: block;
1766
+ margin: 12px 0;
1767
+ }
1458
1768
  .veo-custom-frame {
1459
1769
  display: block;
1460
1770
  border: 0;
@@ -1500,6 +1810,14 @@ var BaseRenderer = class {
1500
1810
  this.shadow = shadow;
1501
1811
  return { host, shadow, root };
1502
1812
  }
1813
+ /**
1814
+ * Aplica la tematización por datos del step (`step.style`) como variables
1815
+ * CSS sobre el host. Llamar tras `createHost()`. No-op si no hay host o
1816
+ * style. Ver `guide-design.ts`.
1817
+ */
1818
+ applyDesign(style) {
1819
+ if (this.host) applyDesignVars(this.host, style);
1820
+ }
1503
1821
  /**
1504
1822
  * Registra una función a ejecutar en `destroy()`. Útil para limpiar
1505
1823
  * listeners globales (scroll, resize) o intervalos.
@@ -1530,6 +1848,7 @@ var BannerRenderer = class extends BaseRenderer {
1530
1848
  const step = context.guide.guideSteps[0];
1531
1849
  if (!step) return;
1532
1850
  const { root } = this.createHost();
1851
+ this.applyDesign(step.style);
1533
1852
  const ownerDocument = root.ownerDocument ?? document;
1534
1853
  const position = step.style?.position === "bottom" ? "bottom" : "top";
1535
1854
  const banner = ownerDocument.createElement("div");
@@ -1591,6 +1910,77 @@ function waitForElement(selector, timeoutMs = DEFAULT_ANCHOR_WAIT_MS) {
1591
1910
  });
1592
1911
  }
1593
1912
 
1913
+ // src/plugins/guides/renderers/custom-frame.ts
1914
+ function mountCustomFrame(doc, step, context, opts) {
1915
+ const iframe = doc.createElement("iframe");
1916
+ iframe.className = "veo-custom-frame";
1917
+ iframe.setAttribute("sandbox", "allow-scripts");
1918
+ iframe.setAttribute("title", context.guide.guideName || "Veo");
1919
+ iframe.style.width = opts.width;
1920
+ const hasFixedHeight = opts.fixedHeight !== void 0 && opts.fixedHeight !== null;
1921
+ if (hasFixedHeight) iframe.style.height = toCssSize(opts.fixedHeight);
1922
+ const token = uuidv7();
1923
+ iframe.srcdoc = buildSrcdoc(step.html ?? "", step.css ?? "", step.js ?? "", token);
1924
+ const close = () => context.onClose();
1925
+ const onMessage = (event) => {
1926
+ if (event.source !== iframe.contentWindow) return;
1927
+ const data = event.data;
1928
+ if (!data || typeof data !== "object" || data.__veo !== token) return;
1929
+ switch (data.type) {
1930
+ case "dismiss":
1931
+ context.onInteraction({
1932
+ guideId: context.guide.guideId,
1933
+ stepIndex: 0,
1934
+ action: "dismissed"
1935
+ });
1936
+ close();
1937
+ break;
1938
+ case "cta":
1939
+ context.onInteraction({
1940
+ guideId: context.guide.guideId,
1941
+ stepIndex: 0,
1942
+ action: "cta_clicked"
1943
+ });
1944
+ if (typeof data.url === "string" && isSafeUrl(data.url)) {
1945
+ window.open(data.url, "_blank", "noopener,noreferrer");
1946
+ }
1947
+ if (data.close !== false) close();
1948
+ break;
1949
+ case "navigate":
1950
+ if (typeof data.url === "string" && isSafeUrl(data.url)) {
1951
+ window.location.assign(data.url);
1952
+ }
1953
+ break;
1954
+ case "track":
1955
+ if (typeof data.name === "string" && context.onTrack) {
1956
+ context.onTrack(data.name, isRecord(data.props) ? data.props : void 0);
1957
+ }
1958
+ break;
1959
+ case "resize":
1960
+ if (!hasFixedHeight && typeof data.height === "number" && data.height > 0) {
1961
+ iframe.style.height = `${Math.ceil(data.height)}px`;
1962
+ }
1963
+ break;
1964
+ }
1965
+ };
1966
+ window.addEventListener("message", onMessage);
1967
+ return { iframe, cleanup: () => window.removeEventListener("message", onMessage) };
1968
+ }
1969
+ function toCssSize(value) {
1970
+ return typeof value === "number" ? `${value}px` : value;
1971
+ }
1972
+ function isRecord(value) {
1973
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1974
+ }
1975
+ function buildSrcdoc(html, css, js, token) {
1976
+ 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){}})();`;
1977
+ 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>";
1978
+ }
1979
+ function escapeClosing(code, tag) {
1980
+ const rx = tag === "script" ? /<\/script/gi : /<\/style/gi;
1981
+ return code.replace(rx, `<\\/${tag}`);
1982
+ }
1983
+
1594
1984
  // src/plugins/guides/renderers/custom-renderer.ts
1595
1985
  var DEFAULT_PLACEMENT = { mode: "floating", position: "bottom-right" };
1596
1986
  var DEFAULT_OFFSET = 16;
@@ -1611,17 +2001,11 @@ var CustomRenderer = class extends BaseRenderer {
1611
2001
  const doc = root.ownerDocument ?? document;
1612
2002
  const wrapper = doc.createElement("div");
1613
2003
  wrapper.className = placement.mode === "anchored" ? "veo-custom-anchored" : "veo-custom-floating";
1614
- const iframe = doc.createElement("iframe");
1615
- iframe.className = "veo-custom-frame";
1616
- iframe.setAttribute("sandbox", "allow-scripts");
1617
- iframe.setAttribute("title", context.guide.guideName || "Veo");
1618
- iframe.style.width = toCssSize(placement.width ?? DEFAULT_WIDTH);
1619
- const hasFixedHeight = placement.height !== void 0 && placement.height !== null;
1620
- if (hasFixedHeight) {
1621
- iframe.style.height = toCssSize(placement.height);
1622
- }
1623
- const token = uuidv7();
1624
- iframe.srcdoc = buildSrcdoc(step.html, step.css ?? "", step.js ?? "", token);
2004
+ const { iframe, cleanup } = mountCustomFrame(doc, step, context, {
2005
+ width: toCssSize(placement.width ?? DEFAULT_WIDTH),
2006
+ fixedHeight: placement.height ?? null
2007
+ });
2008
+ this.registerCleanup(cleanup);
1625
2009
  wrapper.appendChild(iframe);
1626
2010
  root.appendChild(wrapper);
1627
2011
  if (placement.mode === "floating") {
@@ -1629,50 +2013,6 @@ var CustomRenderer = class extends BaseRenderer {
1629
2013
  } else if (anchor) {
1630
2014
  this.registerCleanup(await this.setupAnchoredPosition(anchor, wrapper, placement));
1631
2015
  }
1632
- const close = () => context.onClose();
1633
- const onMessage = (event) => {
1634
- if (event.source !== iframe.contentWindow) return;
1635
- const data = event.data;
1636
- if (!data || typeof data !== "object" || data.__veo !== token) return;
1637
- switch (data.type) {
1638
- case "dismiss":
1639
- context.onInteraction({
1640
- guideId: context.guide.guideId,
1641
- stepIndex: 0,
1642
- action: "dismissed"
1643
- });
1644
- close();
1645
- break;
1646
- case "cta":
1647
- context.onInteraction({
1648
- guideId: context.guide.guideId,
1649
- stepIndex: 0,
1650
- action: "cta_clicked"
1651
- });
1652
- if (typeof data.url === "string" && isSafeUrl(data.url)) {
1653
- window.open(data.url, "_blank", "noopener,noreferrer");
1654
- }
1655
- if (data.close !== false) close();
1656
- break;
1657
- case "navigate":
1658
- if (typeof data.url === "string" && isSafeUrl(data.url)) {
1659
- window.location.assign(data.url);
1660
- }
1661
- break;
1662
- case "track":
1663
- if (typeof data.name === "string" && context.onTrack) {
1664
- context.onTrack(data.name, isRecord(data.props) ? data.props : void 0);
1665
- }
1666
- break;
1667
- case "resize":
1668
- if (!hasFixedHeight && typeof data.height === "number" && data.height > 0) {
1669
- iframe.style.height = `${Math.ceil(data.height)}px`;
1670
- }
1671
- break;
1672
- }
1673
- };
1674
- window.addEventListener("message", onMessage);
1675
- this.registerCleanup(() => window.removeEventListener("message", onMessage));
1676
2016
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
1677
2017
  }
1678
2018
  async setupAnchoredPosition(anchor, wrapper, placement) {
@@ -1698,12 +2038,6 @@ var CustomRenderer = class extends BaseRenderer {
1698
2038
  };
1699
2039
  }
1700
2040
  };
1701
- function toCssSize(value) {
1702
- return typeof value === "number" ? `${value}px` : value;
1703
- }
1704
- function isRecord(value) {
1705
- return typeof value === "object" && value !== null && !Array.isArray(value);
1706
- }
1707
2041
  function applyFloatingPosition(wrapper, placement) {
1708
2042
  const position = placement.position ?? "bottom-right";
1709
2043
  const ox = `${placement.offsetX ?? DEFAULT_OFFSET}px`;
@@ -1727,21 +2061,428 @@ function applyFloatingPosition(wrapper, placement) {
1727
2061
  s.transform = "translateX(-50%)";
1728
2062
  }
1729
2063
  }
1730
- function buildSrcdoc(html, css, js, token) {
1731
- 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});}};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){}})();`;
1732
- 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>";
2064
+
2065
+ // src/plugins/guides/form-content.ts
2066
+ function mountFormContent(card, context, doc) {
2067
+ const step = context.guide.guideSteps[0];
2068
+ if (!step) return;
2069
+ const fields = step.fields ?? [];
2070
+ const dismiss = () => {
2071
+ context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "dismissed" });
2072
+ context.onClose();
2073
+ };
2074
+ const content = doc.createElement("div");
2075
+ content.className = "veo-guide-content";
2076
+ if (step.title) {
2077
+ const heading = doc.createElement("h2");
2078
+ heading.className = "veo-guide-title";
2079
+ heading.textContent = step.title;
2080
+ content.appendChild(heading);
2081
+ }
2082
+ if (step.content) {
2083
+ const paragraph = doc.createElement("p");
2084
+ paragraph.className = "veo-guide-text";
2085
+ paragraph.textContent = step.content;
2086
+ content.appendChild(paragraph);
2087
+ }
2088
+ const readers = [];
2089
+ const form = doc.createElement("form");
2090
+ form.className = "veo-form";
2091
+ form.noValidate = true;
2092
+ for (const field of fields) {
2093
+ const { wrapper, read } = buildField(field, doc);
2094
+ readers.push({ field, read });
2095
+ form.appendChild(wrapper);
2096
+ }
2097
+ const error = doc.createElement("p");
2098
+ error.className = "veo-form-error";
2099
+ error.style.display = "none";
2100
+ form.appendChild(error);
2101
+ const actions = doc.createElement("div");
2102
+ actions.className = "veo-guide-actions";
2103
+ const submit = doc.createElement("button");
2104
+ submit.type = "submit";
2105
+ submit.className = "veo-guide-cta";
2106
+ submit.textContent = step.ctaText || "Enviar";
2107
+ actions.appendChild(submit);
2108
+ form.appendChild(actions);
2109
+ form.addEventListener("submit", (e) => {
2110
+ e.preventDefault();
2111
+ error.style.display = "none";
2112
+ const answers = {};
2113
+ for (const { field, read } of readers) {
2114
+ const value = read();
2115
+ const empty = value === void 0 || value === null || value === "";
2116
+ if (empty) {
2117
+ if (field.required) {
2118
+ error.textContent = `Complet\xE1 "${field.label}"`;
2119
+ error.style.display = "block";
2120
+ return;
2121
+ }
2122
+ continue;
2123
+ }
2124
+ answers[field.key] = value;
2125
+ }
2126
+ if (Object.keys(answers).length === 0) {
2127
+ error.textContent = "Respond\xE9 al menos un campo";
2128
+ error.style.display = "block";
2129
+ return;
2130
+ }
2131
+ submit.disabled = true;
2132
+ const submitFn = context.onFormSubmit ?? (() => Promise.resolve(true));
2133
+ void submitFn(answers).then((ok) => {
2134
+ if (!ok) {
2135
+ submit.disabled = false;
2136
+ error.textContent = "No se pudo enviar. Prob\xE1 de nuevo.";
2137
+ error.style.display = "block";
2138
+ return;
2139
+ }
2140
+ context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "completed" });
2141
+ const note = context.isPreview ? context.previewNote ?? "Vista previa: la respuesta NO se guard\xF3." : null;
2142
+ showThanks(card, doc, note);
2143
+ window.setTimeout(() => context.onClose(), note ? 2200 : 1400);
2144
+ });
2145
+ });
2146
+ content.appendChild(form);
2147
+ const closeBtn = doc.createElement("button");
2148
+ closeBtn.type = "button";
2149
+ closeBtn.className = "veo-guide-close";
2150
+ closeBtn.setAttribute("aria-label", "Cerrar");
2151
+ closeBtn.textContent = "\xD7";
2152
+ closeBtn.addEventListener("click", dismiss);
2153
+ content.appendChild(closeBtn);
2154
+ card.appendChild(content);
2155
+ }
2156
+ function showThanks(card, doc, note) {
2157
+ while (card.firstChild) card.removeChild(card.firstChild);
2158
+ const thanks = doc.createElement("div");
2159
+ thanks.className = "veo-form-thanks";
2160
+ thanks.textContent = "\u2713 \xA1Gracias por tu respuesta!";
2161
+ card.appendChild(thanks);
2162
+ if (note) {
2163
+ const noteEl = doc.createElement("p");
2164
+ noteEl.className = "veo-form-preview-note";
2165
+ noteEl.textContent = note;
2166
+ card.appendChild(noteEl);
2167
+ }
2168
+ }
2169
+ function buildField(field, doc) {
2170
+ const wrapper = doc.createElement("div");
2171
+ wrapper.className = "veo-form-field";
2172
+ const label = doc.createElement("label");
2173
+ label.className = "veo-form-label";
2174
+ label.textContent = field.required ? `${field.label} *` : field.label;
2175
+ wrapper.appendChild(label);
2176
+ switch (field.type) {
2177
+ case "textarea": {
2178
+ const input = doc.createElement("textarea");
2179
+ input.className = "veo-form-input";
2180
+ input.rows = 3;
2181
+ if (field.placeholder) input.placeholder = field.placeholder;
2182
+ wrapper.appendChild(input);
2183
+ return { wrapper, read: () => input.value.trim() };
2184
+ }
2185
+ case "number": {
2186
+ const input = doc.createElement("input");
2187
+ input.type = "number";
2188
+ input.className = "veo-form-input";
2189
+ if (field.placeholder) input.placeholder = field.placeholder;
2190
+ wrapper.appendChild(input);
2191
+ return {
2192
+ wrapper,
2193
+ read: () => input.value.trim() === "" ? void 0 : Number(input.value)
2194
+ };
2195
+ }
2196
+ case "select": {
2197
+ const select = doc.createElement("select");
2198
+ select.className = "veo-form-input";
2199
+ const blank = doc.createElement("option");
2200
+ blank.value = "";
2201
+ blank.textContent = field.placeholder || "Eleg\xED una opci\xF3n\u2026";
2202
+ select.appendChild(blank);
2203
+ for (const opt of field.options ?? []) {
2204
+ const option = doc.createElement("option");
2205
+ option.value = opt;
2206
+ option.textContent = opt;
2207
+ select.appendChild(option);
2208
+ }
2209
+ wrapper.appendChild(select);
2210
+ return { wrapper, read: () => select.value || void 0 };
2211
+ }
2212
+ case "radio": {
2213
+ const group = doc.createElement("div");
2214
+ group.className = "veo-form-options";
2215
+ const name = `veo-${field.key}`;
2216
+ const inputs = [];
2217
+ for (const opt of field.options ?? []) {
2218
+ const optionLabel = doc.createElement("label");
2219
+ optionLabel.className = "veo-form-option";
2220
+ const radio = doc.createElement("input");
2221
+ radio.type = "radio";
2222
+ radio.name = name;
2223
+ radio.value = opt;
2224
+ inputs.push(radio);
2225
+ const text = doc.createElement("span");
2226
+ text.textContent = opt;
2227
+ optionLabel.appendChild(radio);
2228
+ optionLabel.appendChild(text);
2229
+ group.appendChild(optionLabel);
2230
+ }
2231
+ wrapper.appendChild(group);
2232
+ return { wrapper, read: () => inputs.find((i) => i.checked)?.value };
2233
+ }
2234
+ case "multiselect": {
2235
+ const group = doc.createElement("div");
2236
+ group.className = "veo-form-options";
2237
+ const inputs = [];
2238
+ for (const opt of field.options ?? []) {
2239
+ const optionLabel = doc.createElement("label");
2240
+ optionLabel.className = "veo-form-option";
2241
+ const checkbox = doc.createElement("input");
2242
+ checkbox.type = "checkbox";
2243
+ checkbox.value = opt;
2244
+ inputs.push(checkbox);
2245
+ const text = doc.createElement("span");
2246
+ text.textContent = opt;
2247
+ optionLabel.appendChild(checkbox);
2248
+ optionLabel.appendChild(text);
2249
+ group.appendChild(optionLabel);
2250
+ }
2251
+ wrapper.appendChild(group);
2252
+ return {
2253
+ wrapper,
2254
+ read: () => {
2255
+ const values = inputs.filter((i) => i.checked).map((i) => i.value);
2256
+ return values.length > 0 ? values : void 0;
2257
+ }
2258
+ };
2259
+ }
2260
+ case "checkbox": {
2261
+ const optionLabel = doc.createElement("label");
2262
+ optionLabel.className = "veo-form-option";
2263
+ const checkbox = doc.createElement("input");
2264
+ checkbox.type = "checkbox";
2265
+ const text = doc.createElement("span");
2266
+ text.textContent = field.placeholder || "S\xED";
2267
+ optionLabel.appendChild(checkbox);
2268
+ optionLabel.appendChild(text);
2269
+ wrapper.appendChild(optionLabel);
2270
+ return { wrapper, read: () => checkbox.checked ? true : field.required ? "" : false };
2271
+ }
2272
+ case "yesno": {
2273
+ const group = doc.createElement("div");
2274
+ group.className = "veo-form-options";
2275
+ const name = `veo-${field.key}`;
2276
+ const [yesLabel, noLabel] = field.options?.length === 2 ? field.options : ["S\xED", "No"];
2277
+ const inputs = [];
2278
+ for (const { label: optText, value } of [
2279
+ { label: yesLabel, value: true },
2280
+ { label: noLabel, value: false }
2281
+ ]) {
2282
+ const optionLabel = doc.createElement("label");
2283
+ optionLabel.className = "veo-form-option";
2284
+ const radio = doc.createElement("input");
2285
+ radio.type = "radio";
2286
+ radio.name = name;
2287
+ inputs.push({ input: radio, value });
2288
+ const text = doc.createElement("span");
2289
+ text.textContent = optText;
2290
+ optionLabel.appendChild(radio);
2291
+ optionLabel.appendChild(text);
2292
+ group.appendChild(optionLabel);
2293
+ }
2294
+ wrapper.appendChild(group);
2295
+ return { wrapper, read: () => inputs.find((i) => i.input.checked)?.value };
2296
+ }
2297
+ case "nps":
2298
+ return buildScale(wrapper, doc, 0, 10, "veo-nps-btn");
2299
+ case "rating":
2300
+ return buildScale(wrapper, doc, 1, 5, "veo-rating-btn", "\u2605");
2301
+ default: {
2302
+ const input = doc.createElement("input");
2303
+ input.type = "text";
2304
+ input.className = "veo-form-input";
2305
+ if (field.placeholder) input.placeholder = field.placeholder;
2306
+ wrapper.appendChild(input);
2307
+ return { wrapper, read: () => input.value.trim() };
2308
+ }
2309
+ }
2310
+ }
2311
+ function buildScale(wrapper, doc, min, max, btnClass, symbol) {
2312
+ const row = doc.createElement("div");
2313
+ row.className = "veo-form-scale";
2314
+ let selected;
2315
+ const buttons = [];
2316
+ for (let n = min; n <= max; n++) {
2317
+ const btn = doc.createElement("button");
2318
+ btn.type = "button";
2319
+ btn.className = btnClass;
2320
+ btn.textContent = symbol ?? String(n);
2321
+ btn.setAttribute("aria-label", String(n));
2322
+ btn.addEventListener("click", () => {
2323
+ selected = n;
2324
+ buttons.forEach((b, i) => {
2325
+ const active2 = symbol ? i + min <= n : i + min === n;
2326
+ b.classList.toggle("veo-scale-active", active2);
2327
+ });
2328
+ });
2329
+ buttons.push(btn);
2330
+ row.appendChild(btn);
2331
+ }
2332
+ wrapper.appendChild(row);
2333
+ return { wrapper, read: () => selected };
1733
2334
  }
1734
- function escapeClosing(code, tag) {
1735
- const rx = tag === "script" ? /<\/script/gi : /<\/style/gi;
1736
- return code.replace(rx, `<\\/${tag}`);
2335
+
2336
+ // src/plugins/guides/renderers/form-renderer.ts
2337
+ var FormRenderer = class extends BaseRenderer {
2338
+ render(context) {
2339
+ const step = context.guide.guideSteps[0];
2340
+ if (!step || (step.fields ?? []).length === 0) return;
2341
+ const { root } = this.createHost();
2342
+ this.applyDesign(step.style);
2343
+ const doc = root.ownerDocument ?? document;
2344
+ const overlay = doc.createElement("div");
2345
+ overlay.className = "veo-modal-overlay";
2346
+ const card = doc.createElement("div");
2347
+ card.className = "veo-modal-card";
2348
+ mountFormContent(card, context, doc);
2349
+ overlay.appendChild(card);
2350
+ root.appendChild(overlay);
2351
+ const dismiss = () => {
2352
+ context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "dismissed" });
2353
+ context.onClose();
2354
+ };
2355
+ overlay.addEventListener("click", (e) => {
2356
+ if (e.target === overlay) dismiss();
2357
+ });
2358
+ const onKey = (e) => {
2359
+ if (e.key === "Escape") dismiss();
2360
+ };
2361
+ document.addEventListener("keydown", onKey);
2362
+ this.registerCleanup(() => document.removeEventListener("keydown", onKey));
2363
+ context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
2364
+ }
2365
+ };
2366
+
2367
+ // src/plugins/guides/inline-host.ts
2368
+ function readInlinePosition(style) {
2369
+ const p = style?.inlinePosition;
2370
+ return p === "before" || p === "prepend" || p === "append" ? p : "after";
2371
+ }
2372
+ function insertHost(anchor, host, position) {
2373
+ switch (position) {
2374
+ case "before":
2375
+ anchor.before(host);
2376
+ break;
2377
+ case "after":
2378
+ anchor.after(host);
2379
+ break;
2380
+ case "prepend":
2381
+ anchor.prepend(host);
2382
+ break;
2383
+ case "append":
2384
+ anchor.append(host);
2385
+ break;
2386
+ }
2387
+ }
2388
+ function keepHostAttached(host, selector, position) {
2389
+ const id = window.setInterval(() => {
2390
+ if (host.isConnected) return;
2391
+ const anchor = document.querySelector(selector);
2392
+ if (anchor) insertHost(anchor, host, position);
2393
+ }, 1e3);
2394
+ return () => window.clearInterval(id);
1737
2395
  }
1738
2396
 
2397
+ // src/plugins/guides/renderers/inline-custom-renderer.ts
2398
+ var InlineCustomRenderer = class extends BaseRenderer {
2399
+ async render(context) {
2400
+ const step = context.guide.guideSteps[0];
2401
+ if (!step || typeof step.html !== "string" || step.html.length === 0) return;
2402
+ const selector = step.selector ?? context.guide.activationRules.selector;
2403
+ if (typeof selector !== "string" || selector.length === 0) return;
2404
+ const anchor = await waitForElement(selector);
2405
+ if (!anchor) return;
2406
+ const { host, root } = this.createHost();
2407
+ host.style.display = "block";
2408
+ const position = readInlinePosition(step.style);
2409
+ insertHost(anchor, host, position);
2410
+ this.registerCleanup(keepHostAttached(host, selector, position));
2411
+ const doc = root.ownerDocument ?? document;
2412
+ const wrapper = doc.createElement("div");
2413
+ wrapper.className = "veo-custom-inline";
2414
+ const { iframe, cleanup } = mountCustomFrame(doc, step, context, { width: "100%" });
2415
+ this.registerCleanup(cleanup);
2416
+ wrapper.appendChild(iframe);
2417
+ root.appendChild(wrapper);
2418
+ context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
2419
+ }
2420
+ };
2421
+
2422
+ // src/plugins/guides/renderers/inline-form-renderer.ts
2423
+ var InlineFormRenderer = class extends BaseRenderer {
2424
+ async render(context) {
2425
+ const step = context.guide.guideSteps[0];
2426
+ if (!step || (step.fields ?? []).length === 0) return;
2427
+ const selector = step.selector ?? context.guide.activationRules.selector;
2428
+ if (typeof selector !== "string" || selector.length === 0) return;
2429
+ const anchor = await waitForElement(selector);
2430
+ if (!anchor) return;
2431
+ const { host, root } = this.createHost();
2432
+ host.style.display = "block";
2433
+ const position = readInlinePosition(step.style);
2434
+ insertHost(anchor, host, position);
2435
+ this.registerCleanup(keepHostAttached(host, selector, position));
2436
+ this.applyDesign(step.style);
2437
+ const doc = root.ownerDocument ?? document;
2438
+ const card = doc.createElement("div");
2439
+ card.className = "veo-inline";
2440
+ mountFormContent(card, context, doc);
2441
+ root.appendChild(card);
2442
+ context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
2443
+ }
2444
+ };
2445
+
2446
+ // src/plugins/guides/renderers/inline-renderer.ts
2447
+ var InlineRenderer = class extends BaseRenderer {
2448
+ async render(context) {
2449
+ const step = context.guide.guideSteps[0];
2450
+ if (!step) return;
2451
+ const selector = step.selector ?? context.guide.activationRules.selector;
2452
+ if (typeof selector !== "string" || selector.length === 0) return;
2453
+ const anchor = await waitForElement(selector);
2454
+ if (!anchor) return;
2455
+ const { host, root } = this.createHost();
2456
+ host.style.display = "block";
2457
+ const position = readInlinePosition(step.style);
2458
+ insertHost(anchor, host, position);
2459
+ this.registerCleanup(keepHostAttached(host, selector, position));
2460
+ this.applyDesign(step.style);
2461
+ const ownerDocument = root.ownerDocument ?? document;
2462
+ const card = ownerDocument.createElement("div");
2463
+ card.className = "veo-inline";
2464
+ const dismiss = (action, ctaUrl) => {
2465
+ context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action });
2466
+ if (ctaUrl) window.open(ctaUrl, "_blank", "noopener,noreferrer");
2467
+ context.onClose();
2468
+ };
2469
+ const content = buildStepContent(step, ownerDocument, {
2470
+ onCtaClick: (action, url) => dismiss("cta_clicked", action === "url" ? url : void 0),
2471
+ onDismiss: () => dismiss("dismissed")
2472
+ });
2473
+ card.appendChild(content);
2474
+ root.appendChild(card);
2475
+ context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
2476
+ }
2477
+ };
2478
+
1739
2479
  // src/plugins/guides/renderers/modal-renderer.ts
1740
2480
  var ModalRenderer = class extends BaseRenderer {
1741
2481
  render(context) {
1742
2482
  const step = context.guide.guideSteps[0];
1743
2483
  if (!step) return;
1744
2484
  const { root } = this.createHost();
2485
+ this.applyDesign(step.style);
1745
2486
  const ownerDocument = root.ownerDocument ?? document;
1746
2487
  const overlay = ownerDocument.createElement("div");
1747
2488
  overlay.className = "veo-modal-overlay";
@@ -1789,6 +2530,7 @@ var TooltipRenderer = class extends BaseRenderer {
1789
2530
  const anchor = await waitForElement(selector);
1790
2531
  if (!anchor) return;
1791
2532
  const { root } = this.createHost();
2533
+ this.applyDesign(step.style);
1792
2534
  const ownerDocument = root.ownerDocument ?? document;
1793
2535
  const tooltip = ownerDocument.createElement("div");
1794
2536
  tooltip.className = "veo-tooltip";
@@ -1916,6 +2658,7 @@ var WalkthroughRenderer = class extends BaseRenderer {
1916
2658
  const anchor = await waitForElement(selector, WALKTHROUGH_STEP_SELECTOR_TIMEOUT_MS);
1917
2659
  if (!anchor) return false;
1918
2660
  const { root } = this.createHost();
2661
+ this.applyDesign(step.style);
1919
2662
  const ownerDocument = root.ownerDocument ?? document;
1920
2663
  const tooltip = ownerDocument.createElement("div");
1921
2664
  tooltip.className = "veo-walkthrough";
@@ -1927,14 +2670,18 @@ var WalkthroughRenderer = class extends BaseRenderer {
1927
2670
  context.callbacks
1928
2671
  );
1929
2672
  tooltip.appendChild(content);
2673
+ const arrowEl = ownerDocument.createElement("div");
2674
+ arrowEl.className = "veo-walkthrough-arrow";
2675
+ tooltip.appendChild(arrowEl);
1930
2676
  root.appendChild(tooltip);
1931
2677
  const updatePosition = async () => {
1932
- const { x, y } = await dom.computePosition(anchor, tooltip, {
2678
+ const { x, y, placement, middlewareData } = await dom.computePosition(anchor, tooltip, {
1933
2679
  placement: "bottom",
1934
- middleware: [dom.offset(8), dom.flip(), dom.shift({ padding: 8 })]
2680
+ middleware: [dom.offset(10), dom.flip(), dom.shift({ padding: 8 }), dom.arrow({ element: arrowEl })]
1935
2681
  });
1936
2682
  tooltip.style.left = `${x}px`;
1937
2683
  tooltip.style.top = `${y}px`;
2684
+ positionArrow(arrowEl, placement, middlewareData.arrow);
1938
2685
  };
1939
2686
  await updatePosition();
1940
2687
  const reposition = () => {
@@ -1949,6 +2696,21 @@ var WalkthroughRenderer = class extends BaseRenderer {
1949
2696
  return true;
1950
2697
  }
1951
2698
  };
2699
+ var STATIC_SIDE = {
2700
+ top: "bottom",
2701
+ bottom: "top",
2702
+ left: "right",
2703
+ right: "left"
2704
+ };
2705
+ function positionArrow(arrowEl, placement, data) {
2706
+ const side = STATIC_SIDE[placement.split("-")[0] ?? "bottom"] ?? "top";
2707
+ for (const prop of ["top", "bottom", "left", "right"]) {
2708
+ arrowEl.style.setProperty(prop, "");
2709
+ }
2710
+ if (data?.x != null) arrowEl.style.setProperty("left", `${data.x}px`);
2711
+ if (data?.y != null) arrowEl.style.setProperty("top", `${data.y}px`);
2712
+ arrowEl.style.setProperty(side, "-6px");
2713
+ }
1952
2714
 
1953
2715
  // src/plugins/guides/walkthrough-state.ts
1954
2716
  var WalkthroughStateManager = class {
@@ -2078,10 +2840,18 @@ var GuidesController = class {
2078
2840
  constructor(client, config) {
2079
2841
  this.client = client;
2080
2842
  this.activeRenderers = /* @__PURE__ */ new Set();
2843
+ /**
2844
+ * Guías single-step actualmente en pantalla, por `guideId`. Evita repintar/
2845
+ * duplicar la misma guía en cada pageview de una SPA (p.ej. una guía inline
2846
+ * anclada a un elemento persistente). Se libera al cerrarse la guía.
2847
+ */
2848
+ this.activeByGuideId = /* @__PURE__ */ new Map();
2081
2849
  this.dispatched = /* @__PURE__ */ new Set();
2082
2850
  this.activeWalkthroughRenderer = null;
2083
2851
  this.activeWalkthroughGuide = null;
2084
2852
  this.debug = config.debug === true;
2853
+ this.apiUrl = config.apiUrl;
2854
+ this.apiKey = config.apiKey;
2085
2855
  this.resolver = config.resolver ?? new GuideResolverClient(config.apiUrl, config.apiKey, this.debug);
2086
2856
  if (config.trackerQueue) {
2087
2857
  this.trackerQueue = config.trackerQueue;
@@ -2141,6 +2911,10 @@ var GuidesController = class {
2141
2911
  if (this.debug) console.log(`[veo] guides: filtered locally guideId=${guide.guideId}`);
2142
2912
  continue;
2143
2913
  }
2914
+ if (this.activeByGuideId.has(guide.guideId)) {
2915
+ if (this.debug) console.log(`[veo] guides: already shown guideId=${guide.guideId}`);
2916
+ continue;
2917
+ }
2144
2918
  if (guide.guideType === "walkthrough" && guide.guideSteps.length > 1) {
2145
2919
  if (this.walkthroughState.hasActive()) {
2146
2920
  if (this.debug)
@@ -2163,6 +2937,7 @@ var GuidesController = class {
2163
2937
  }
2164
2938
  }
2165
2939
  this.activeRenderers.clear();
2940
+ this.activeByGuideId.clear();
2166
2941
  if (this.activeWalkthroughRenderer) {
2167
2942
  try {
2168
2943
  this.activeWalkthroughRenderer.destroy();
@@ -2177,6 +2952,7 @@ var GuidesController = class {
2177
2952
  runSingleStepRender(guide, sessionId, pageUrl) {
2178
2953
  const renderer = this.createSingleStepRenderer(guide.guideType);
2179
2954
  if (!renderer) return;
2955
+ this.activeByGuideId.set(guide.guideId, renderer);
2180
2956
  const delayMs = guide.activationRules.delayMs ?? 0;
2181
2957
  window.setTimeout(
2182
2958
  () => this.mountSingleStepRenderer(guide, renderer, sessionId, pageUrl),
@@ -2185,9 +2961,13 @@ var GuidesController = class {
2185
2961
  }
2186
2962
  mountSingleStepRenderer(guide, renderer, sessionId, pageUrl) {
2187
2963
  this.activeRenderers.add(renderer);
2964
+ this.activeByGuideId.set(guide.guideId, renderer);
2188
2965
  const onClose = () => {
2189
2966
  renderer.destroy();
2190
2967
  this.activeRenderers.delete(renderer);
2968
+ if (this.activeByGuideId.get(guide.guideId) === renderer) {
2969
+ this.activeByGuideId.delete(guide.guideId);
2970
+ }
2191
2971
  };
2192
2972
  const onInteraction = (event) => {
2193
2973
  this.handleInteraction(guide, sessionId, pageUrl, event);
@@ -2195,11 +2975,15 @@ var GuidesController = class {
2195
2975
  const onTrack = (eventName, properties) => {
2196
2976
  this.client.track(eventName, properties);
2197
2977
  };
2978
+ const onFormSubmit = (answers) => this.submitFormResponse(guide.guideId, answers);
2198
2979
  try {
2199
- void renderer.render({ guide, onInteraction, onClose, onTrack });
2980
+ void renderer.render({ guide, onInteraction, onClose, onTrack, onFormSubmit });
2200
2981
  } catch (err) {
2201
2982
  if (this.debug) console.error("[veo] renderer.render threw:", err);
2202
2983
  this.activeRenderers.delete(renderer);
2984
+ if (this.activeByGuideId.get(guide.guideId) === renderer) {
2985
+ this.activeByGuideId.delete(guide.guideId);
2986
+ }
2203
2987
  }
2204
2988
  }
2205
2989
  /**
@@ -2431,10 +3215,40 @@ var GuidesController = class {
2431
3215
  return new TooltipRenderer();
2432
3216
  case "custom":
2433
3217
  return new CustomRenderer();
3218
+ case "inline":
3219
+ return new InlineRenderer();
3220
+ case "inline-custom":
3221
+ return new InlineCustomRenderer();
3222
+ case "form":
3223
+ return new FormRenderer();
3224
+ case "inline-form":
3225
+ return new InlineFormRenderer();
2434
3226
  case "walkthrough":
2435
3227
  return null;
2436
3228
  }
2437
3229
  }
3230
+ /**
3231
+ * Envía las respuestas de una guía `form` al backend, que las mergea como
3232
+ * propiedades del usuario. Usa el endUserId actual (o el anónimo si aún no
3233
+ * hubo identify — el backend pre-crea la fila igual que identify).
3234
+ */
3235
+ async submitFormResponse(guideId, answers) {
3236
+ const endUserId = this.client.getEndUserId() ?? this.client.getAnonymousId();
3237
+ try {
3238
+ const res = await fetch(`${this.apiUrl}/v1/guides/${guideId}/form-response`, {
3239
+ method: "POST",
3240
+ headers: { "Content-Type": "application/json", "X-Api-Key": this.apiKey },
3241
+ body: JSON.stringify({ endUserId, answers })
3242
+ });
3243
+ if (!res.ok && this.debug) {
3244
+ console.warn("[veo] form-response respondi\xF3", res.status);
3245
+ }
3246
+ return res.ok;
3247
+ } catch (err) {
3248
+ if (this.debug) console.warn("[veo] form-response fall\xF3:", err);
3249
+ return false;
3250
+ }
3251
+ }
2438
3252
  };
2439
3253
  function extractPath(url) {
2440
3254
  try {
@@ -2444,6 +3258,283 @@ function extractPath(url) {
2444
3258
  }
2445
3259
  }
2446
3260
 
3261
+ // src/plugins/guides/guide-preview.ts
3262
+ var PREVIEW_GUIDE_ID = "__veo_preview__";
3263
+ var GuidePreviewController = class {
3264
+ constructor() {
3265
+ this.singleStep = null;
3266
+ this.walkthrough = null;
3267
+ this.walkthroughGuide = null;
3268
+ this.walkthroughIndex = 0;
3269
+ /** Input de la preview ACTIVA (para formSubmit/nota de forms). */
3270
+ this.input = null;
3271
+ }
3272
+ preview(input) {
3273
+ this.close();
3274
+ this.input = input;
3275
+ const guide = normalize(input);
3276
+ const start = typeof input.startStepIndex === "number" ? input.startStepIndex : 0;
3277
+ const ready = guide.guideType === "walkthrough" ? this.startWalkthrough(guide, start) : this.renderSingleStep(guide);
3278
+ return { close: () => this.close(), ready };
3279
+ }
3280
+ close() {
3281
+ if (this.singleStep) {
3282
+ safeDestroy(this.singleStep);
3283
+ this.singleStep = null;
3284
+ }
3285
+ if (this.walkthrough) {
3286
+ safeDestroy(this.walkthrough);
3287
+ this.walkthrough = null;
3288
+ }
3289
+ this.walkthroughGuide = null;
3290
+ this.walkthroughIndex = 0;
3291
+ this.input = null;
3292
+ }
3293
+ async renderSingleStep(guide) {
3294
+ const renderer = createSingleStepRenderer(guide.guideType);
3295
+ if (!renderer) return { rendered: false };
3296
+ this.singleStep = renderer;
3297
+ let shown = false;
3298
+ try {
3299
+ await renderer.render({
3300
+ guide,
3301
+ onInteraction: (event) => {
3302
+ if (event.action === "shown") shown = true;
3303
+ },
3304
+ onClose: () => this.close(),
3305
+ onTrack: () => {
3306
+ },
3307
+ // Preview de forms: por defecto simula el envío SIN escribir props;
3308
+ // el caller puede pasar `formSubmit` real (link de preview).
3309
+ onFormSubmit: this.input?.formSubmit ?? (() => Promise.resolve(true)),
3310
+ isPreview: true,
3311
+ ...this.input?.formSubmit && this.input.formSubmitNote !== void 0 ? { previewNote: this.input.formSubmitNote } : {}
3312
+ });
3313
+ } catch {
3314
+ this.close();
3315
+ return { rendered: false };
3316
+ }
3317
+ if (this.singleStep !== renderer) {
3318
+ safeDestroy(renderer);
3319
+ return { rendered: false };
3320
+ }
3321
+ return { rendered: shown };
3322
+ }
3323
+ async startWalkthrough(guide, startIndex = 0) {
3324
+ if (guide.guideSteps.length === 0) return { rendered: false };
3325
+ this.walkthroughGuide = guide;
3326
+ const idx = Math.min(Math.max(0, Math.floor(startIndex)), guide.guideSteps.length - 1);
3327
+ this.walkthroughIndex = idx;
3328
+ const rendered = await this.renderWalkthroughStep(idx);
3329
+ return { rendered };
3330
+ }
3331
+ async renderWalkthroughStep(index) {
3332
+ const guide = this.walkthroughGuide;
3333
+ if (!guide) return false;
3334
+ if (this.walkthrough) {
3335
+ safeDestroy(this.walkthrough);
3336
+ this.walkthrough = null;
3337
+ }
3338
+ const renderer = new WalkthroughRenderer();
3339
+ const callbacks = {
3340
+ onNext: () => {
3341
+ const next = this.walkthroughIndex + 1;
3342
+ if (next >= guide.guideSteps.length) {
3343
+ this.close();
3344
+ return;
3345
+ }
3346
+ this.walkthroughIndex = next;
3347
+ void this.renderWalkthroughStep(next);
3348
+ },
3349
+ onBack: () => {
3350
+ const prev = this.walkthroughIndex - 1;
3351
+ if (prev < 0) return;
3352
+ this.walkthroughIndex = prev;
3353
+ void this.renderWalkthroughStep(prev);
3354
+ },
3355
+ onSkip: () => this.close(),
3356
+ onComplete: () => this.close()
3357
+ };
3358
+ let success = false;
3359
+ try {
3360
+ success = await renderer.render({ guide, currentStepIndex: index, callbacks });
3361
+ } catch {
3362
+ success = false;
3363
+ }
3364
+ if (!success || this.walkthroughGuide !== guide) {
3365
+ safeDestroy(renderer);
3366
+ return false;
3367
+ }
3368
+ this.walkthrough = renderer;
3369
+ return true;
3370
+ }
3371
+ };
3372
+ var activeController = null;
3373
+ function previewGuide(input) {
3374
+ if (!hasDocument()) {
3375
+ return { close: () => {
3376
+ }, ready: Promise.resolve({ rendered: false }) };
3377
+ }
3378
+ if (!activeController) activeController = new GuidePreviewController();
3379
+ return activeController.preview(input);
3380
+ }
3381
+ function closeGuidePreview() {
3382
+ activeController?.close();
3383
+ }
3384
+ function normalize(input) {
3385
+ const selector = input.activationRules?.selector;
3386
+ const activationRules = {
3387
+ url: input.activationRules?.url ?? { type: "prefix", pattern: "/" },
3388
+ trigger: "immediate",
3389
+ ...selector !== void 0 ? { selector } : {}
3390
+ };
3391
+ return {
3392
+ guideId: PREVIEW_GUIDE_ID,
3393
+ guideName: input.guideName ?? "Preview",
3394
+ guideType: input.guideType,
3395
+ guideSteps: input.guideSteps,
3396
+ activationRules,
3397
+ displayFrequency: "always",
3398
+ displayPriority: 0
3399
+ };
3400
+ }
3401
+ function createSingleStepRenderer(type) {
3402
+ switch (type) {
3403
+ case "modal":
3404
+ return new ModalRenderer();
3405
+ case "banner":
3406
+ return new BannerRenderer();
3407
+ case "tooltip":
3408
+ return new TooltipRenderer();
3409
+ case "custom":
3410
+ return new CustomRenderer();
3411
+ case "inline":
3412
+ return new InlineRenderer();
3413
+ case "inline-custom":
3414
+ return new InlineCustomRenderer();
3415
+ case "form":
3416
+ return new FormRenderer();
3417
+ case "inline-form":
3418
+ return new InlineFormRenderer();
3419
+ case "walkthrough":
3420
+ return null;
3421
+ }
3422
+ }
3423
+ function safeDestroy(renderer) {
3424
+ try {
3425
+ renderer.destroy();
3426
+ } catch {
3427
+ }
3428
+ }
3429
+
3430
+ // src/plugins/guides/preview-mode.ts
3431
+ var active = null;
3432
+ async function runPreviewMode(opts) {
3433
+ if (!hasDocument()) return null;
3434
+ active?.teardown();
3435
+ let guide = null;
3436
+ try {
3437
+ const res = await fetch(`${opts.apiUrl}/v1/guides/preview-resolve`, {
3438
+ method: "POST",
3439
+ headers: { "Content-Type": "application/json", "X-Api-Key": opts.apiKey },
3440
+ body: JSON.stringify({ token: opts.token })
3441
+ });
3442
+ if (res.ok) {
3443
+ const data = await res.json();
3444
+ guide = data.guide ?? null;
3445
+ } else if (opts.debug) {
3446
+ console.warn("[veo] preview-resolve respondi\xF3", res.status);
3447
+ }
3448
+ } catch (err) {
3449
+ if (opts.debug) console.warn("[veo] preview-resolve fall\xF3:", err);
3450
+ }
3451
+ const submitToBackend = async (answers) => {
3452
+ if (!guide) return false;
3453
+ const veo = window.veo;
3454
+ const endUserId = veo?.getEndUserId?.() ?? veo?.getAnonymousId?.() ?? null;
3455
+ if (!endUserId) return false;
3456
+ try {
3457
+ const res = await fetch(`${opts.apiUrl}/v1/guides/${guide.guideId}/form-response`, {
3458
+ method: "POST",
3459
+ headers: { "Content-Type": "application/json", "X-Api-Key": opts.apiKey },
3460
+ body: JSON.stringify({ endUserId, answers })
3461
+ });
3462
+ return res.ok;
3463
+ } catch {
3464
+ return false;
3465
+ }
3466
+ };
3467
+ let preview = null;
3468
+ const show = () => {
3469
+ if (!guide) return;
3470
+ preview?.close();
3471
+ preview = previewGuide({
3472
+ guideType: guide.guideType,
3473
+ guideSteps: guide.guideSteps,
3474
+ activationRules: guide.activationRules,
3475
+ guideName: guide.guideName,
3476
+ formSubmit: submitToBackend,
3477
+ formSubmitNote: "Vista previa: guardado en TU usuario para probar el flujo."
3478
+ });
3479
+ };
3480
+ const chip = mountChip({
3481
+ label: guide ? `Vista previa: ${guide.guideName}` : "Link de preview inv\xE1lido o vencido",
3482
+ error: !guide,
3483
+ onReshow: guide ? show : null,
3484
+ onClose: () => handle.teardown()
3485
+ });
3486
+ const handle = {
3487
+ teardown() {
3488
+ preview?.close();
3489
+ preview = null;
3490
+ chip.remove();
3491
+ if (active === handle) active = null;
3492
+ }
3493
+ };
3494
+ active = handle;
3495
+ show();
3496
+ return handle;
3497
+ }
3498
+ function mountChip(opts) {
3499
+ const host = document.createElement("div");
3500
+ host.setAttribute("data-veo-preview-chip", "");
3501
+ const root = host.attachShadow({ mode: "open" });
3502
+ const style = document.createElement("style");
3503
+ style.textContent = `
3504
+ .chip { position: fixed; bottom: 16px; right: 16px; z-index: 2147483647;
3505
+ display: flex; align-items: center; gap: 8px; padding: 8px 12px;
3506
+ background: #111827; color: #f9fafb; border-radius: 9999px;
3507
+ font: 500 12px/1.2 system-ui, sans-serif; box-shadow: 0 4px 12px rgba(0,0,0,.25); }
3508
+ .chip.error { background: #7f1d1d; }
3509
+ .label { max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3510
+ button { all: unset; cursor: pointer; padding: 2px 6px; border-radius: 9999px;
3511
+ font-size: 12px; line-height: 1; }
3512
+ button:hover { background: rgba(255,255,255,.15); }
3513
+ `;
3514
+ root.appendChild(style);
3515
+ const chip = document.createElement("div");
3516
+ chip.className = opts.error ? "chip error" : "chip";
3517
+ const label = document.createElement("span");
3518
+ label.className = "label";
3519
+ label.textContent = opts.label;
3520
+ chip.appendChild(label);
3521
+ if (opts.onReshow) {
3522
+ const reshow = document.createElement("button");
3523
+ reshow.textContent = "\u21BB";
3524
+ reshow.title = "Volver a mostrar la gu\xEDa";
3525
+ reshow.addEventListener("click", opts.onReshow);
3526
+ chip.appendChild(reshow);
3527
+ }
3528
+ const close = document.createElement("button");
3529
+ close.textContent = "\u2715";
3530
+ close.title = "Salir de la vista previa";
3531
+ close.addEventListener("click", opts.onClose);
3532
+ chip.appendChild(close);
3533
+ root.appendChild(chip);
3534
+ document.body.appendChild(host);
3535
+ return { remove: () => host.remove() };
3536
+ }
3537
+
2447
3538
  // src/plugins/guides/index.ts
2448
3539
  function guidesPlugin(config) {
2449
3540
  return definePlugin({
@@ -2458,79 +3549,46 @@ function guidesPlugin(config) {
2458
3549
  client.on("pageview", ({ url, sessionId }) => {
2459
3550
  void controller.checkGuides(url, sessionId);
2460
3551
  });
2461
- }
2462
- });
2463
- }
2464
-
2465
- // src/plugins/spa-router.ts
2466
- function spaRouterPlugin() {
2467
- return definePlugin({
2468
- name: "spa-router",
2469
- install(client) {
2470
- if (!hasWindow()) return;
2471
- client.pageview();
2472
- const fire = () => {
2473
- queueMicrotask(() => client.pageview());
2474
- };
2475
- const origPushState = window.history.pushState;
2476
- const origReplaceState = window.history.replaceState;
2477
- window.history.pushState = function(...args) {
2478
- origPushState.apply(this, args);
2479
- fire();
2480
- };
2481
- window.history.replaceState = function(...args) {
2482
- origReplaceState.apply(this, args);
2483
- fire();
2484
- };
2485
- window.addEventListener("popstate", fire);
2486
- window.addEventListener("hashchange", fire);
3552
+ void controller.checkGuides(window.location.href, client.getSessionId());
2487
3553
  }
2488
3554
  });
2489
3555
  }
2490
3556
 
2491
3557
  // src/index.ts
2492
3558
  function init(config) {
2493
- const client = new Client(config);
2494
- client.use(spaRouterPlugin());
2495
- if (config.autocapture !== false) {
2496
- const autocaptureConfig = typeof config.autocapture === "object" ? config.autocapture : {};
2497
- client.use(autocapturePlugin(autocaptureConfig));
2498
- }
2499
- if (config.guides === true) {
2500
- client.use(
2501
- guidesPlugin({
2502
- apiUrl: config.apiUrl,
3559
+ const apiUrl = resolveApiUrl(config);
3560
+ return setupClient(config, (client) => {
3561
+ const previewToken = getPreviewToken();
3562
+ if (config.guides === true && !isBuilderMode() && previewToken === null) {
3563
+ client.use(
3564
+ guidesPlugin({
3565
+ apiUrl,
3566
+ apiKey: config.apiKey,
3567
+ ...config.debug !== void 0 ? { debug: config.debug } : {}
3568
+ })
3569
+ );
3570
+ }
3571
+ if (previewToken !== null) {
3572
+ void runPreviewMode({
2503
3573
  apiKey: config.apiKey,
3574
+ apiUrl,
3575
+ token: previewToken,
2504
3576
  ...config.debug !== void 0 ? { debug: config.debug } : {}
2505
- })
2506
- );
2507
- }
2508
- if (typeof window !== "undefined") {
2509
- const stub = window.veo;
2510
- const pending = stub?._q ?? [];
2511
- window.veo = client;
2512
- for (const entry of pending) {
2513
- const [method, args] = entry;
2514
- if (method === "init") continue;
2515
- const fn = client[method];
2516
- if (typeof fn === "function") {
2517
- try {
2518
- fn.apply(client, Array.from(args));
2519
- } catch (err) {
2520
- if (config.debug) console.error("[veo] drain error:", err);
2521
- }
2522
- }
3577
+ });
2523
3578
  }
2524
- }
2525
- return client;
3579
+ client.preview = previewGuide;
3580
+ client.closePreview = closeGuidePreview;
3581
+ });
2526
3582
  }
2527
3583
 
2528
3584
  exports.Client = Client;
2529
3585
  exports.SDK_VERSION = SDK_VERSION;
2530
3586
  exports.autocapturePlugin = autocapturePlugin;
3587
+ exports.closeGuidePreview = closeGuidePreview;
2531
3588
  exports.definePlugin = definePlugin;
2532
3589
  exports.guidesPlugin = guidesPlugin;
2533
3590
  exports.init = init;
3591
+ exports.previewGuide = previewGuide;
2534
3592
  exports.spaRouterPlugin = spaRouterPlugin;
2535
3593
  //# sourceMappingURL=index.cjs.map
2536
3594
  //# sourceMappingURL=index.cjs.map