veo-sdk 0.6.1 → 0.6.3

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.
Files changed (35) hide show
  1. package/dist/builder-PPLIAUCM.mjs +5 -0
  2. package/dist/{builder-BKPMANUB.mjs.map → builder-PPLIAUCM.mjs.map} +1 -1
  3. package/dist/builder.cjs +641 -105
  4. package/dist/builder.cjs.map +1 -1
  5. package/dist/builder.d.cts +78 -8
  6. package/dist/builder.d.ts +78 -8
  7. package/dist/builder.mjs +4 -4
  8. package/dist/{chunk-GVPAJFNW.mjs → chunk-3JEAPZ6A.mjs} +3 -3
  9. package/dist/{chunk-GVPAJFNW.mjs.map → chunk-3JEAPZ6A.mjs.map} +1 -1
  10. package/dist/{chunk-VWF4UHUU.mjs → chunk-DU6I4ZU2.mjs} +284 -15
  11. package/dist/chunk-DU6I4ZU2.mjs.map +1 -0
  12. package/dist/{chunk-NGP7IJVE.mjs → chunk-SZCIXA5X.mjs} +357 -82
  13. package/dist/chunk-SZCIXA5X.mjs.map +1 -0
  14. package/dist/detect-theme-X4K3TLUL.mjs +446 -0
  15. package/dist/detect-theme-X4K3TLUL.mjs.map +1 -0
  16. package/dist/{guide-preview-DylsEhrg.d.cts → guide-preview-DEFhfA4o.d.cts} +115 -0
  17. package/dist/{guide-preview-DylsEhrg.d.ts → guide-preview-DEFhfA4o.d.ts} +115 -0
  18. package/dist/index.cjs +1515 -56
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +18 -1
  21. package/dist/index.d.ts +18 -1
  22. package/dist/index.mjs +442 -17
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/veo-builder.js +132 -25
  25. package/dist/veo-builder.js.map +1 -1
  26. package/dist/veo-guides.js +84 -12
  27. package/dist/veo-guides.js.map +1 -1
  28. package/dist/veo-theme.js +2 -0
  29. package/dist/veo-theme.js.map +1 -0
  30. package/dist/veo.js +2 -2
  31. package/dist/veo.js.map +1 -1
  32. package/package.json +2 -2
  33. package/dist/builder-BKPMANUB.mjs +0 -5
  34. package/dist/chunk-NGP7IJVE.mjs.map +0 -1
  35. package/dist/chunk-VWF4UHUU.mjs.map +0 -1
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- export { P as PreviewGuideInput, a as PreviewHandle, b as PreviewResult, c as closeGuidePreview, p as previewGuide } from './guide-preview-DylsEhrg.cjs';
1
+ export { P as PreviewGuideInput, a as PreviewHandle, b as PreviewResult, c as closeGuidePreview, p as previewGuide } from './guide-preview-DEFhfA4o.cjs';
2
2
 
3
3
  /**
4
4
  * Configuración de la autocaptura de interacciones del DOM.
@@ -26,6 +26,11 @@ interface VeoAutocaptureConfig {
26
26
  captureElementMetadata?: boolean;
27
27
  /** Niveles de ancestros a capturar. Default: 5. Max: 10. */
28
28
  captureAncestors?: number;
29
+ /**
30
+ * Calidad de los clics: emite `$dead_click` (clic sin reacción), `$rage_click`
31
+ * (ráfaga de clics) y `$click_error` (clic seguido de un error). Default: true.
32
+ */
33
+ captureClickQuality?: boolean;
29
34
  }
30
35
  /**
31
36
  * Configuración del cliente Veo.
@@ -49,6 +54,18 @@ interface VeoConfig {
49
54
  * tooltips. Default: `false`.
50
55
  */
51
56
  guides?: boolean;
57
+ /**
58
+ * Detectar el tema visual de la app (colores base en claro/oscuro y fuentes)
59
+ * para que el design system de OnUser arranque con la marca del cliente.
60
+ * Solo viajan colores y nombres de fuentes, como mucho una vez al día por
61
+ * navegador. Default: `true`; `false` lo deshabilita.
62
+ */
63
+ detectTheme?: boolean;
64
+ /**
65
+ * URL del bundle del detector de tema (`veo-theme.js`). Solo aplica al build
66
+ * IIFE; si se omite, se deriva del `<script>` que cargó `veo.js`.
67
+ */
68
+ themeUrl?: string;
52
69
  /**
53
70
  * URL del bundle del builder (`veo-builder.js`). Solo se carga cuando la
54
71
  * página trae `?veoBuilder=<token>` (modo edición desde el dashboard). Si
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { P as PreviewGuideInput, a as PreviewHandle, b as PreviewResult, c as closeGuidePreview, p as previewGuide } from './guide-preview-DylsEhrg.js';
1
+ export { P as PreviewGuideInput, a as PreviewHandle, b as PreviewResult, c as closeGuidePreview, p as previewGuide } from './guide-preview-DEFhfA4o.js';
2
2
 
3
3
  /**
4
4
  * Configuración de la autocaptura de interacciones del DOM.
@@ -26,6 +26,11 @@ interface VeoAutocaptureConfig {
26
26
  captureElementMetadata?: boolean;
27
27
  /** Niveles de ancestros a capturar. Default: 5. Max: 10. */
28
28
  captureAncestors?: number;
29
+ /**
30
+ * Calidad de los clics: emite `$dead_click` (clic sin reacción), `$rage_click`
31
+ * (ráfaga de clics) y `$click_error` (clic seguido de un error). Default: true.
32
+ */
33
+ captureClickQuality?: boolean;
29
34
  }
30
35
  /**
31
36
  * Configuración del cliente Veo.
@@ -49,6 +54,18 @@ interface VeoConfig {
49
54
  * tooltips. Default: `false`.
50
55
  */
51
56
  guides?: boolean;
57
+ /**
58
+ * Detectar el tema visual de la app (colores base en claro/oscuro y fuentes)
59
+ * para que el design system de OnUser arranque con la marca del cliente.
60
+ * Solo viajan colores y nombres de fuentes, como mucho una vez al día por
61
+ * navegador. Default: `true`; `false` lo deshabilita.
62
+ */
63
+ detectTheme?: boolean;
64
+ /**
65
+ * URL del bundle del detector de tema (`veo-theme.js`). Solo aplica al build
66
+ * IIFE; si se omite, se deriva del `<script>` que cargó `veo.js`.
67
+ */
68
+ themeUrl?: string;
52
69
  /**
53
70
  * URL del bundle del builder (`veo-builder.js`). Solo se carga cuando la
54
71
  * página trae `?veoBuilder=<token>` (modo edición desde el dashboard). Si
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { definePlugin, guidesPlugin, runPreviewMode } from './chunk-GVPAJFNW.mjs';
2
- export { definePlugin, guidesPlugin } from './chunk-GVPAJFNW.mjs';
3
- import { hasWindow, hasDocument, uuidv7, DEFAULT_AUTOCAPTURE_CONFIG, MAX_ANCESTORS, hasLocalStorage, hasNavigatorSendBeacon, ALWAYS_BLOCK_SELECTORS, buildSelectorPath, resolveBuilderToken, filterHumanClasses, PRIORITY_ATTRIBUTES, SENSITIVE_ATTRIBUTES, getPreviewToken, isBuilderMode, previewGuide, closeGuidePreview } from './chunk-NGP7IJVE.mjs';
4
- export { closeGuidePreview, previewGuide } from './chunk-NGP7IJVE.mjs';
1
+ import { definePlugin, guidesPlugin, runPreviewMode } from './chunk-3JEAPZ6A.mjs';
2
+ export { definePlugin, guidesPlugin } from './chunk-3JEAPZ6A.mjs';
3
+ import { hasWindow, hasDocument, uuidv7, DEFAULT_AUTOCAPTURE_CONFIG, MAX_ANCESTORS, hasLocalStorage, hasNavigatorSendBeacon, isBuilderMode, buildSelectorPath, ALWAYS_BLOCK_SELECTORS, resolveBuilderToken, filterHumanClasses, PRIORITY_ATTRIBUTES, SENSITIVE_ATTRIBUTES, getPreviewToken, previewGuide, closeGuidePreview } from './chunk-SZCIXA5X.mjs';
4
+ export { closeGuidePreview, previewGuide } from './chunk-SZCIXA5X.mjs';
5
5
  import './chunk-PTG3BTJE.mjs';
6
6
 
7
7
  // src/contract/v1.ts
@@ -62,6 +62,294 @@ function deriveBuilderUrl() {
62
62
  return null;
63
63
  }
64
64
 
65
+ // src/plugins/autocapture/click-quality.ts
66
+ var NO_DEAD_TAGS = /* @__PURE__ */ new Set(["input", "textarea", "select", "option", "label"]);
67
+ var MAX_ERROR_MESSAGE = 200;
68
+ var HISTORY_MS = 3e3;
69
+ var DEFAULTS = {
70
+ reactionWindowMs: 1e3,
71
+ errorWindowMs: 2e3,
72
+ rageClicks: 3,
73
+ rageWindowMs: 1e3,
74
+ rageClicksSlow: 5,
75
+ rageWindowSlowMs: 3e3,
76
+ maxPerElement: 50
77
+ };
78
+ function describeError(value) {
79
+ let text;
80
+ if (value instanceof Error) text = `${value.name}: ${value.message}`;
81
+ else if (typeof value === "string") text = value;
82
+ else {
83
+ try {
84
+ text = JSON.stringify(value) ?? String(value);
85
+ } catch {
86
+ text = String(value);
87
+ }
88
+ }
89
+ return text.slice(0, MAX_ERROR_MESSAGE);
90
+ }
91
+ var ClickQualityTracker = class {
92
+ constructor(emit, options = {}) {
93
+ this.emit = emit;
94
+ this.installed = false;
95
+ this.cleanups = [];
96
+ // Sensores globales (contadores: cada observación compara su foto inicial).
97
+ this.netCount = 0;
98
+ this.navCount = 0;
99
+ this.focusCount = 0;
100
+ this.unloading = false;
101
+ this.errors = [];
102
+ this.history = /* @__PURE__ */ new Map();
103
+ this.bursts = /* @__PURE__ */ new Map();
104
+ this.emitted = /* @__PURE__ */ new Map();
105
+ this.opts = { ...DEFAULTS, ...options };
106
+ }
107
+ install() {
108
+ if (this.installed || !hasWindow()) return;
109
+ this.installed = true;
110
+ const on = (type, handler, capture = true) => {
111
+ window.addEventListener(type, handler, capture);
112
+ this.cleanups.push(() => window.removeEventListener(type, handler, capture));
113
+ };
114
+ on("error", (e) => {
115
+ const err = e;
116
+ this.pushError("error", err.error ?? err.message ?? "error");
117
+ });
118
+ on("unhandledrejection", (e) => {
119
+ this.pushError("unhandledrejection", e.reason ?? "rejection");
120
+ });
121
+ on("popstate", () => this.navCount++);
122
+ on("hashchange", () => this.navCount++);
123
+ on("focusin", () => this.focusCount++);
124
+ on("pagehide", () => {
125
+ this.unloading = true;
126
+ });
127
+ on("beforeunload", () => {
128
+ this.unloading = true;
129
+ });
130
+ this.patchHistory();
131
+ this.patchConsole();
132
+ this.patchFetch();
133
+ this.patchXhr();
134
+ }
135
+ destroy() {
136
+ for (const c of this.cleanups) c();
137
+ this.cleanups = [];
138
+ for (const b of this.bursts.values()) window.clearTimeout(b.timer);
139
+ this.bursts.clear();
140
+ this.history.clear();
141
+ this.installed = false;
142
+ }
143
+ /** Llamar por cada clic real sobre un elemento interactivo, ANTES del throttle. */
144
+ onClick(element, event) {
145
+ if (!this.installed) return;
146
+ const now = Date.now();
147
+ const obs = { at: now, reacted: false };
148
+ this.remember(element, obs);
149
+ this.observeReaction(element, obs, event);
150
+ this.observeErrors(element, now);
151
+ this.trackRage(element, now);
152
+ }
153
+ // ---------------------------------------------------------------- dead ----
154
+ observeReaction(element, obs, event) {
155
+ const netAtClick = this.netCount;
156
+ const navAtClick = this.navCount;
157
+ const focusAtClick = this.focusCount;
158
+ const hrefAtClick = window.location.href;
159
+ const skipDead = this.isDeadExempt(element, event);
160
+ let observer = null;
161
+ if (typeof MutationObserver !== "undefined") {
162
+ observer = new MutationObserver(() => {
163
+ obs.reacted = true;
164
+ observer?.disconnect();
165
+ observer = null;
166
+ });
167
+ observer.observe(document.documentElement, {
168
+ childList: true,
169
+ attributes: true,
170
+ characterData: true,
171
+ subtree: true
172
+ });
173
+ }
174
+ window.setTimeout(() => {
175
+ observer?.disconnect();
176
+ const reacted = obs.reacted || this.unloading || this.netCount !== netAtClick || this.navCount !== navAtClick || this.focusCount !== focusAtClick || window.location.href !== hrefAtClick;
177
+ obs.reacted = reacted;
178
+ if (!reacted && !skipDead) this.report("$dead_click", element, {});
179
+ }, this.opts.reactionWindowMs);
180
+ }
181
+ isDeadExempt(element, event) {
182
+ if (event && (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey)) return true;
183
+ if (event && typeof event.button === "number" && event.button !== 0) return true;
184
+ const tag = element.tagName.toLowerCase();
185
+ if (NO_DEAD_TAGS.has(tag)) return true;
186
+ if (element.closest('[contenteditable=""],[contenteditable="true"]')) return true;
187
+ const anchor = element.closest("a");
188
+ if (anchor) {
189
+ if (anchor.target === "_blank" || anchor.hasAttribute("download")) return true;
190
+ const href = anchor.getAttribute("href") ?? "";
191
+ if (/^(mailto|tel|sms):/i.test(href)) return true;
192
+ }
193
+ return false;
194
+ }
195
+ // --------------------------------------------------------------- error ----
196
+ observeErrors(element, clickedAt) {
197
+ const deadline = clickedAt + this.opts.errorWindowMs;
198
+ const check = () => {
199
+ const err = this.errors.find((e) => e.at >= clickedAt && e.at <= deadline);
200
+ if (err) {
201
+ this.report("$click_error", element, {
202
+ $error_source: err.source,
203
+ $error_message: err.message
204
+ });
205
+ return;
206
+ }
207
+ if (Date.now() < deadline) window.setTimeout(check, 250);
208
+ };
209
+ window.setTimeout(check, 250);
210
+ }
211
+ pushError(source, value) {
212
+ const message = describeError(value);
213
+ if (message.startsWith("[veo]")) return;
214
+ this.errors.push({ at: Date.now(), source, message });
215
+ const cutoff = Date.now() - this.opts.errorWindowMs - 1e3;
216
+ while (this.errors.length > 0 && this.errors[0].at < cutoff) {
217
+ this.errors.shift();
218
+ }
219
+ if (this.errors.length > 50) this.errors.splice(0, this.errors.length - 50);
220
+ }
221
+ // ---------------------------------------------------------------- rage ----
222
+ remember(element, obs) {
223
+ const list = this.history.get(element) ?? [];
224
+ const cutoff = obs.at - HISTORY_MS;
225
+ const kept = list.filter((o) => o.at >= cutoff);
226
+ kept.push(obs);
227
+ this.history.set(element, kept);
228
+ if (this.history.size > 100) {
229
+ for (const [el, obsList] of this.history) {
230
+ if (obsList.every((o) => o.at < cutoff)) this.history.delete(el);
231
+ }
232
+ }
233
+ }
234
+ trackRage(element, now) {
235
+ const list = this.history.get(element) ?? [];
236
+ const fast = list.filter((o) => o.at >= now - this.opts.rageWindowMs).length;
237
+ const slow = list.filter((o) => o.at >= now - this.opts.rageWindowSlowMs).length;
238
+ const existing = this.bursts.get(element);
239
+ const isRage = fast >= this.opts.rageClicks || slow >= this.opts.rageClicksSlow;
240
+ if (!existing && !isRage) return;
241
+ const burst = existing ?? {
242
+ startedAt: (list.find((o) => o.at >= now - this.opts.rageWindowSlowMs) ?? { at: now }).at,
243
+ count: 0,
244
+ timer: 0
245
+ };
246
+ burst.count = list.filter((o) => o.at >= burst.startedAt).length;
247
+ window.clearTimeout(burst.timer);
248
+ burst.timer = window.setTimeout(() => {
249
+ this.bursts.delete(element);
250
+ const clicks = (this.history.get(element) ?? []).filter((o) => o.at >= burst.startedAt);
251
+ this.report("$rage_click", element, {
252
+ $click_count: Math.max(clicks.length, burst.count),
253
+ $reacted_clicks: clicks.filter((o) => o.reacted).length
254
+ });
255
+ }, this.opts.reactionWindowMs + 50);
256
+ this.bursts.set(element, burst);
257
+ }
258
+ // ---------------------------------------------------------------- emit ----
259
+ report(kind, element, extra) {
260
+ const key = buildSelectorPath(element, 5);
261
+ const counts = this.emitted.get(key) ?? { $dead_click: 0, $rage_click: 0, $click_error: 0 };
262
+ if (counts[kind] >= this.opts.maxPerElement) return;
263
+ counts[kind]++;
264
+ this.emitted.set(key, counts);
265
+ try {
266
+ this.emit(kind, element, extra);
267
+ } catch {
268
+ }
269
+ }
270
+ // ------------------------------------------------------------- patches ----
271
+ patchHistory() {
272
+ const h = window.history;
273
+ if (!h) return;
274
+ const origPush = h.pushState;
275
+ const origReplace = h.replaceState;
276
+ const self = this;
277
+ h.pushState = function(...args) {
278
+ self.navCount++;
279
+ return origPush.apply(this, args);
280
+ };
281
+ h.replaceState = function(...args) {
282
+ self.navCount++;
283
+ return origReplace.apply(this, args);
284
+ };
285
+ this.cleanups.push(() => {
286
+ h.pushState = origPush;
287
+ h.replaceState = origReplace;
288
+ });
289
+ }
290
+ patchConsole() {
291
+ const orig = console.error;
292
+ const self = this;
293
+ console.error = function(...args) {
294
+ try {
295
+ self.pushError("console", args.map(describeError).join(" "));
296
+ } catch {
297
+ }
298
+ return orig.apply(this, args);
299
+ };
300
+ this.cleanups.push(() => {
301
+ console.error = orig;
302
+ });
303
+ }
304
+ /**
305
+ * `fetch`: cuenta la petición (reacción) y registra respuestas ≥500 o
306
+ * rechazos como error. Se observa con `.then(ok, err)` sobre la promesa
307
+ * original: eso marca el rechazo como manejado para el navegador, así que
308
+ * un fetch fallido sin catch de la app no dispara `unhandledrejection` —
309
+ * lo registramos nosotros como error de red.
310
+ */
311
+ patchFetch() {
312
+ if (typeof window.fetch !== "function") return;
313
+ const orig = window.fetch;
314
+ const self = this;
315
+ window.fetch = function(...args) {
316
+ self.netCount++;
317
+ const p = orig.apply(this, args);
318
+ if (p && typeof p.then === "function") {
319
+ p.then(
320
+ (res) => {
321
+ if (res && res.status >= 500) self.pushError("fetch", `${res.status} ${res.url}`);
322
+ },
323
+ (err) => self.pushError("fetch", err)
324
+ );
325
+ }
326
+ return p;
327
+ };
328
+ this.cleanups.push(() => {
329
+ window.fetch = orig;
330
+ });
331
+ }
332
+ patchXhr() {
333
+ const proto = typeof XMLHttpRequest !== "undefined" ? XMLHttpRequest.prototype : null;
334
+ if (!proto) return;
335
+ const origSend = proto.send;
336
+ const self = this;
337
+ proto.send = function(...args) {
338
+ self.netCount++;
339
+ try {
340
+ this.addEventListener("loadend", () => {
341
+ if (this.status >= 500) self.pushError("xhr", `${this.status} ${this.responseURL}`);
342
+ });
343
+ } catch {
344
+ }
345
+ return origSend.apply(this, args);
346
+ };
347
+ this.cleanups.push(() => {
348
+ proto.send = origSend;
349
+ });
350
+ }
351
+ };
352
+
65
353
  // src/plugins/autocapture/pii-guard.ts
66
354
  function isElementBlocked(element, extraBlockSelectors = []) {
67
355
  const allBlockers = [...ALWAYS_BLOCK_SELECTORS, ...extraBlockSelectors];
@@ -165,6 +453,7 @@ var DomListener = class {
165
453
  constructor(client, config) {
166
454
  this.client = client;
167
455
  this.cleanups = [];
456
+ this.quality = null;
168
457
  const d = DEFAULT_AUTOCAPTURE_CONFIG;
169
458
  this.config = {
170
459
  enabled: config.enabled ?? d.enabled,
@@ -177,12 +466,20 @@ var DomListener = class {
177
466
  throttleMs: config.throttleMs ?? d.throttleMs,
178
467
  maxTextLength: config.maxTextLength ?? d.maxTextLength,
179
468
  captureElementMetadata: config.captureElementMetadata ?? d.captureElementMetadata,
180
- captureAncestors: Math.min(config.captureAncestors ?? d.captureAncestors, MAX_ANCESTORS)
469
+ captureAncestors: Math.min(config.captureAncestors ?? d.captureAncestors, MAX_ANCESTORS),
470
+ captureClickQuality: config.captureClickQuality ?? d.captureClickQuality
181
471
  };
182
472
  this.throttle = new ElementThrottle(this.config.throttleMs);
183
473
  }
184
474
  install() {
185
475
  if (!hasDocument() || !this.config.enabled) return;
476
+ if (this.config.captureClicks && this.config.captureClickQuality) {
477
+ this.quality = new ClickQualityTracker((kind, element, extra) => {
478
+ const payload = buildAutocapturePayload("click", element, this.config);
479
+ this.client.track(kind, { ...payload, ...extra });
480
+ });
481
+ this.quality.install();
482
+ }
186
483
  if (this.config.captureClicks) this.attach("click", (e) => this.handleClick(e));
187
484
  if (this.config.captureSubmits) this.attach("submit", (e) => this.handleSubmit(e), true);
188
485
  if (this.config.captureChanges) this.attach("change", (e) => this.handleChange(e), true);
@@ -190,6 +487,8 @@ var DomListener = class {
190
487
  destroy() {
191
488
  for (const cleanup of this.cleanups) cleanup();
192
489
  this.cleanups = [];
490
+ this.quality?.destroy();
491
+ this.quality = null;
193
492
  }
194
493
  attach(eventName, handler, useCapture = false) {
195
494
  const listener = (e) => {
@@ -208,6 +507,7 @@ var DomListener = class {
208
507
  const meaningful = this.findMeaningfulAncestor(target);
209
508
  if (!meaningful) return;
210
509
  if (isElementBlocked(meaningful, this.config.blockSelectors)) return;
510
+ this.quality?.onClick(meaningful, event);
211
511
  if (!this.throttle.shouldFire(meaningful)) return;
212
512
  this.emit("click", meaningful);
213
513
  }
@@ -291,6 +591,118 @@ function spaRouterPlugin() {
291
591
  });
292
592
  }
293
593
 
594
+ // src/plugins/theme-detect/index.ts
595
+ var STORAGE_PREFIX = "veo_theme_";
596
+ var RESEND_AFTER_MS = 24 * 60 * 60 * 1e3;
597
+ var SETTLE_MS = 4e3;
598
+ var MAX_RUNS_PER_PAGE = 4;
599
+ function readState(key) {
600
+ try {
601
+ const raw = localStorage.getItem(key);
602
+ const parsed = raw ? JSON.parse(raw) : null;
603
+ return parsed && typeof parsed === "object" ? parsed : {};
604
+ } catch {
605
+ return {};
606
+ }
607
+ }
608
+ function writeState(key, state) {
609
+ try {
610
+ localStorage.setItem(key, JSON.stringify(state));
611
+ } catch {
612
+ }
613
+ }
614
+ function guessMode(doc, prefersDark) {
615
+ const hints = [
616
+ doc.documentElement.className,
617
+ doc.documentElement.dataset.theme,
618
+ doc.documentElement.dataset.mode,
619
+ doc.body?.className
620
+ ].join(" ").toLowerCase();
621
+ if (/\bdark\b/.test(hints)) return "dark";
622
+ if (/\blight\b/.test(hints)) return "light";
623
+ return prefersDark ? "dark" : "light";
624
+ }
625
+ function isFresh(state, mode, now) {
626
+ const last = state[mode];
627
+ return last !== void 0 && now - last.at < RESEND_AFTER_MS;
628
+ }
629
+ function shouldSendSnapshot(state, mode, hash, now) {
630
+ const last = state[mode];
631
+ return !last || last.hash !== hash || now - last.at >= RESEND_AFTER_MS;
632
+ }
633
+ function themeDetectPlugin(config) {
634
+ const storageKey = `${STORAGE_PREFIX}${config.apiKey.slice(-16) || "default"}`;
635
+ let observer = null;
636
+ let timer = null;
637
+ let destroyed = false;
638
+ let runs = 0;
639
+ const run = async () => {
640
+ if (destroyed || runs >= MAX_RUNS_PER_PAGE) return;
641
+ const prefersDark = typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches;
642
+ const guess = guessMode(document, prefersDark);
643
+ if (isFresh(readState(storageKey), guess, Date.now())) return;
644
+ runs++;
645
+ try {
646
+ const detector = await config.loadDetector();
647
+ if (!detector || destroyed) return;
648
+ const snapshot = detector.detectTheme();
649
+ if (!snapshot) return;
650
+ const hash = detector.snapshotHash(snapshot);
651
+ const state = readState(storageKey);
652
+ const now = Date.now();
653
+ const mark = () => {
654
+ const record = { hash, at: Date.now() };
655
+ state[snapshot.mode] = record;
656
+ state[guess] = state[guess] && guess !== snapshot.mode ? state[guess] : record;
657
+ writeState(storageKey, state);
658
+ };
659
+ if (!shouldSendSnapshot(state, snapshot.mode, hash, now)) {
660
+ mark();
661
+ return;
662
+ }
663
+ if (config.debug) console.log("[veo] theme snapshot", snapshot);
664
+ const res = await fetch(`${config.apiUrl}/v1/workspaces/theme-snapshot`, {
665
+ method: "POST",
666
+ headers: { "X-Api-Key": config.apiKey, "Content-Type": "application/json" },
667
+ body: JSON.stringify(snapshot),
668
+ credentials: "omit"
669
+ });
670
+ if (res.ok) mark();
671
+ } catch (err) {
672
+ if (config.debug) console.warn("[veo] theme detect failed", err);
673
+ }
674
+ };
675
+ const schedule = (delayMs) => {
676
+ if (timer) clearTimeout(timer);
677
+ timer = setTimeout(() => {
678
+ timer = null;
679
+ const idle = window.requestIdleCallback;
680
+ if (typeof idle === "function") idle(() => void run());
681
+ else void run();
682
+ }, delayMs);
683
+ };
684
+ return {
685
+ name: "theme-detect",
686
+ install() {
687
+ if (!hasWindow() || !hasDocument()) return;
688
+ if (document.readyState === "complete") schedule(SETTLE_MS);
689
+ else window.addEventListener("load", () => schedule(SETTLE_MS), { once: true });
690
+ if (typeof MutationObserver === "function") {
691
+ observer = new MutationObserver(() => schedule(1500));
692
+ observer.observe(document.documentElement, {
693
+ attributes: true,
694
+ attributeFilter: ["class", "data-theme", "data-mode"]
695
+ });
696
+ }
697
+ },
698
+ destroy() {
699
+ destroyed = true;
700
+ observer?.disconnect();
701
+ if (timer) clearTimeout(timer);
702
+ }
703
+ };
704
+ }
705
+
294
706
  // src/core/queue.ts
295
707
  var RETRY_MAX_DELAY_MS = 3e4;
296
708
  var Queue = class {
@@ -437,26 +849,26 @@ var Session = class {
437
849
  };
438
850
 
439
851
  // src/core/storage.ts
440
- var STORAGE_PREFIX = "veo:";
852
+ var STORAGE_PREFIX2 = "veo:";
441
853
  var ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1e3;
442
854
  var LocalStorageAdapter = class {
443
855
  get(key) {
444
- return localStorage.getItem(STORAGE_PREFIX + key);
856
+ return localStorage.getItem(STORAGE_PREFIX2 + key);
445
857
  }
446
858
  set(key, value) {
447
859
  try {
448
- localStorage.setItem(STORAGE_PREFIX + key, value);
860
+ localStorage.setItem(STORAGE_PREFIX2 + key, value);
449
861
  } catch {
450
862
  }
451
863
  }
452
864
  remove(key) {
453
- localStorage.removeItem(STORAGE_PREFIX + key);
865
+ localStorage.removeItem(STORAGE_PREFIX2 + key);
454
866
  }
455
867
  clear() {
456
868
  const toRemove = [];
457
869
  for (let i = 0; i < localStorage.length; i++) {
458
870
  const key = localStorage.key(i);
459
- if (key?.startsWith(STORAGE_PREFIX)) toRemove.push(key);
871
+ if (key?.startsWith(STORAGE_PREFIX2)) toRemove.push(key);
460
872
  }
461
873
  for (const key of toRemove) localStorage.removeItem(key);
462
874
  }
@@ -467,7 +879,7 @@ function readCookies() {
467
879
  }
468
880
  var CookieAdapter = class {
469
881
  get(key) {
470
- const name = `${STORAGE_PREFIX}${key}=`;
882
+ const name = `${STORAGE_PREFIX2}${key}=`;
471
883
  for (const cookie of readCookies()) {
472
884
  if (cookie.startsWith(name)) {
473
885
  return decodeURIComponent(cookie.slice(name.length));
@@ -478,19 +890,19 @@ var CookieAdapter = class {
478
890
  set(key, value, options) {
479
891
  const maxAgeSec = Math.floor((options?.ttlMs ?? ONE_YEAR_MS) / 1e3);
480
892
  const secure = typeof location !== "undefined" && location.protocol === "https:" ? "; Secure" : "";
481
- document.cookie = `${STORAGE_PREFIX}${key}=${encodeURIComponent(
893
+ document.cookie = `${STORAGE_PREFIX2}${key}=${encodeURIComponent(
482
894
  value
483
895
  )}; Path=/; Max-Age=${maxAgeSec}; SameSite=Lax${secure}`;
484
896
  }
485
897
  remove(key) {
486
- document.cookie = `${STORAGE_PREFIX}${key}=; Path=/; Max-Age=0; SameSite=Lax`;
898
+ document.cookie = `${STORAGE_PREFIX2}${key}=; Path=/; Max-Age=0; SameSite=Lax`;
487
899
  }
488
900
  clear() {
489
901
  for (const cookie of readCookies()) {
490
902
  const eq = cookie.indexOf("=");
491
903
  const name = eq === -1 ? cookie : cookie.slice(0, eq);
492
- if (name.startsWith(STORAGE_PREFIX)) {
493
- this.remove(name.slice(STORAGE_PREFIX.length));
904
+ if (name.startsWith(STORAGE_PREFIX2)) {
905
+ this.remove(name.slice(STORAGE_PREFIX2.length));
494
906
  }
495
907
  }
496
908
  }
@@ -779,13 +1191,23 @@ var Client = class {
779
1191
  };
780
1192
 
781
1193
  // src/core/setup.ts
782
- function setupClient(config, onGuides, loadBuilder) {
1194
+ function setupClient(config, onGuides, loadBuilder, loadThemeDetector) {
783
1195
  const client = new Client(config);
784
1196
  client.use(spaRouterPlugin());
785
1197
  if (config.autocapture !== false) {
786
1198
  const autocaptureConfig = typeof config.autocapture === "object" ? config.autocapture : {};
787
1199
  client.use(autocapturePlugin(autocaptureConfig));
788
1200
  }
1201
+ if (config.detectTheme !== false && loadThemeDetector && !isBuilderMode()) {
1202
+ client.use(
1203
+ themeDetectPlugin({
1204
+ apiUrl: resolveApiUrl(config),
1205
+ apiKey: config.apiKey,
1206
+ loadDetector: loadThemeDetector,
1207
+ ...config.debug !== void 0 ? { debug: config.debug } : {}
1208
+ })
1209
+ );
1210
+ }
789
1211
  onGuides(client);
790
1212
  if (typeof window !== "undefined") {
791
1213
  const stub = window.veo;
@@ -844,7 +1266,10 @@ function init(config) {
844
1266
  // Fallback de carga del builder para bundlers: `import()` dinámico → el bundler
845
1267
  // lo code-splitea (solo se descarga en modo builder). Así el consumidor por
846
1268
  // bundler NO necesita copiar `veo-builder.js` ni setear `builderUrl`.
847
- () => import('./builder-BKPMANUB.mjs').then((m) => m.initBuilderMode())
1269
+ () => import('./builder-PPLIAUCM.mjs').then((m) => m.initBuilderMode()),
1270
+ // Detector de tema: también por `import()` dinámico → chunk aparte que el
1271
+ // bundler solo descarga cuando toca detectar (como mucho una vez al día).
1272
+ () => import('./detect-theme-X4K3TLUL.mjs')
848
1273
  );
849
1274
  }
850
1275