veo-sdk 0.1.0 → 0.2.0

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