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/README.md +15 -11
- package/dist/builder.cjs +2072 -0
- package/dist/builder.cjs.map +1 -0
- package/dist/builder.d.cts +162 -0
- package/dist/builder.d.ts +162 -0
- package/dist/builder.mjs +449 -0
- package/dist/builder.mjs.map +1 -0
- package/dist/chunk-BBAA5BRS.mjs +2728 -0
- package/dist/chunk-BBAA5BRS.mjs.map +1 -0
- package/dist/guide-preview-CO5ShtNf.d.cts +137 -0
- package/dist/guide-preview-CO5ShtNf.d.ts +137 -0
- package/dist/index.cjs +1952 -904
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -22
- package/dist/index.d.ts +41 -22
- package/dist/index.mjs +351 -2027
- package/dist/index.mjs.map +1 -1
- package/dist/veo-builder.js +289 -0
- package/dist/veo-builder.js.map +1 -0
- package/dist/veo-guides.js +259 -0
- package/dist/veo-guides.js.map +1 -0
- package/dist/veo.js +1 -169
- package/dist/veo.js.map +1 -1
- package/package.json +7 -2
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/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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/
|
|
128
|
-
var
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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/
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
270
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
|
|
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/
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
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.
|
|
350
|
-
this.
|
|
351
|
-
this.
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
this.
|
|
359
|
-
this.
|
|
360
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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.
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
this.
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
454
|
-
this.
|
|
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
|
-
|
|
461
|
-
return this.
|
|
575
|
+
get sessionId() {
|
|
576
|
+
return this._sessionId;
|
|
462
577
|
}
|
|
463
|
-
|
|
464
|
-
this.
|
|
465
|
-
plugin.install(this);
|
|
466
|
-
return this;
|
|
578
|
+
get endUserId() {
|
|
579
|
+
return this._endUserId;
|
|
467
580
|
}
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
472
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
return plugin;
|
|
656
|
+
function readCookies() {
|
|
657
|
+
const raw = document.cookie;
|
|
658
|
+
return raw === "" ? [] : raw.split("; ");
|
|
536
659
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
668
|
+
return null;
|
|
611
669
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
-
|
|
677
|
-
|
|
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
|
-
|
|
682
|
-
|
|
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
|
-
|
|
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/
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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
|
-
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
-
|
|
738
|
-
|
|
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
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
this.
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
this.cleanups = [];
|
|
906
|
+
getEndUserId() {
|
|
907
|
+
return this.session.endUserId;
|
|
799
908
|
}
|
|
800
|
-
|
|
801
|
-
|
|
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
|
-
|
|
812
|
-
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
-
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
-
/**
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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/
|
|
859
|
-
function
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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` →
|
|
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")
|
|
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
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
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
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
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
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
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(
|
|
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
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
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
|
-
|
|
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
|