@offscript/sdk 0.1.1 → 0.1.2
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 +3 -3
- package/dist/adapters/nuxt.cjs +3 -3
- package/dist/adapters/nuxt.js +2 -2
- package/dist/adapters/vanilla.cjs +5 -5
- package/dist/adapters/vanilla.js +3 -3
- package/dist/adapters/vue.cjs +3 -3
- package/dist/adapters/vue.js +2 -2
- package/dist/{chunk-MBYNQSKA.js → chunk-HECPMGDC.js} +3 -3
- package/dist/{chunk-MBYNQSKA.js.map → chunk-HECPMGDC.js.map} +1 -1
- package/dist/{chunk-3NX2AGXM.cjs → chunk-I6JPZAQA.cjs} +738 -31
- package/dist/chunk-I6JPZAQA.cjs.map +1 -0
- package/dist/{chunk-3OSCH7J2.js → chunk-IBEPWI7S.js} +729 -22
- package/dist/chunk-IBEPWI7S.js.map +1 -0
- package/dist/{chunk-B6GO5FE5.cjs → chunk-KQVK5EJR.cjs} +4 -3
- package/dist/chunk-KQVK5EJR.cjs.map +1 -0
- package/dist/{chunk-HAPNSHFG.cjs → chunk-TBPD2LMI.cjs} +4 -4
- package/dist/{chunk-HAPNSHFG.cjs.map → chunk-TBPD2LMI.cjs.map} +1 -1
- package/dist/{chunk-LFXHB4PN.js → chunk-WUFW7TEL.js} +4 -3
- package/dist/chunk-WUFW7TEL.js.map +1 -0
- package/dist/core/OffscriptSDK.d.ts +19 -0
- package/dist/core/SessionManager.d.ts +1 -0
- package/dist/index.cjs +25 -25
- package/dist/index.js +3 -3
- package/dist/playwright.cjs +13 -13
- package/dist/playwright.js +1 -1
- package/dist/privacy/DataSanitizer.d.ts +1 -0
- package/dist/recorders/ContextRecorder.d.ts +1 -1
- package/dist/types/config.d.ts +11 -0
- package/dist/types/events.d.ts +2 -0
- package/dist/types/session.d.ts +2 -2
- package/dist/ui/RecordingControls.d.ts +60 -0
- package/docs/api-contract.md +1 -1
- package/package.json +1 -1
- package/dist/chunk-3NX2AGXM.cjs.map +0 -1
- package/dist/chunk-3OSCH7J2.js.map +0 -1
- package/dist/chunk-B6GO5FE5.cjs.map +0 -1
- package/dist/chunk-LFXHB4PN.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkKQVK5EJR_cjs = require('./chunk-KQVK5EJR.cjs');
|
|
4
4
|
|
|
5
5
|
// src/types/config.ts
|
|
6
6
|
var DEFAULT_RECORDING = {
|
|
@@ -20,7 +20,7 @@ var DEFAULT_RECORDING = {
|
|
|
20
20
|
stableAttributes: ["name", "data-id", "aria-label"]
|
|
21
21
|
};
|
|
22
22
|
var DEFAULT_PRIVACY = {
|
|
23
|
-
maskSensitiveInputs:
|
|
23
|
+
maskSensitiveInputs: false,
|
|
24
24
|
maskAllText: false,
|
|
25
25
|
captureRequestBodies: false,
|
|
26
26
|
captureResponseBodies: false,
|
|
@@ -32,6 +32,7 @@ var DEFAULT_PRIVACY = {
|
|
|
32
32
|
ignoreUrls: [],
|
|
33
33
|
redactHeaders: [],
|
|
34
34
|
redactKeys: [],
|
|
35
|
+
captureSensitiveValues: false,
|
|
35
36
|
maxBodyBytes: 16 * 1024,
|
|
36
37
|
textContentTypes: [/^text\//i, /json/i, /xml/i, /x-www-form-urlencoded/i, /javascript/i],
|
|
37
38
|
localRetentionMs: 24 * 36e5
|
|
@@ -161,13 +162,15 @@ var DataSanitizer = class {
|
|
|
161
162
|
this.cfg = cfg;
|
|
162
163
|
this.headerSet = /* @__PURE__ */ new Set([...BASE_SENSITIVE_HEADERS, ...cfg.redactHeaders.map((h) => h.toLowerCase())]);
|
|
163
164
|
this.extraKeys = new Set(cfg.redactKeys.map((k) => k.toLowerCase()));
|
|
165
|
+
this.open = cfg.captureSensitiveValues === true;
|
|
164
166
|
}
|
|
165
167
|
isSensitiveKey(key) {
|
|
168
|
+
if (this.open) return false;
|
|
166
169
|
return BASE_SENSITIVE_KEY_PATTERN.test(key) || this.extraKeys.has(key.toLowerCase());
|
|
167
170
|
}
|
|
168
171
|
/** Mascara padrões sensíveis em texto livre (JWT, Bearer, CPF, CNPJ, cartões). */
|
|
169
172
|
scrubString(input) {
|
|
170
|
-
if (!input) return input;
|
|
173
|
+
if (this.open || !input) return input;
|
|
171
174
|
let s = input.replace(JWT, REDACTED).replace(BEARER, (_m, scheme) => `${scheme} ${REDACTED}`);
|
|
172
175
|
s = s.replace(CNPJ, REDACTED).replace(CPF, REDACTED);
|
|
173
176
|
s = s.replace(CARD, (m) => {
|
|
@@ -177,6 +180,7 @@ var DataSanitizer = class {
|
|
|
177
180
|
return s;
|
|
178
181
|
}
|
|
179
182
|
sanitizeHeaders(headers) {
|
|
183
|
+
if (this.open) return { ...headers };
|
|
180
184
|
const out = {};
|
|
181
185
|
for (const [k, v] of Object.entries(headers)) {
|
|
182
186
|
out[k] = this.headerSet.has(k.toLowerCase()) || this.isSensitiveKey(k) ? REDACTED : this.scrubString(v);
|
|
@@ -185,6 +189,7 @@ var DataSanitizer = class {
|
|
|
185
189
|
}
|
|
186
190
|
/** Redige query params sensíveis e credenciais embutidas (user:pass@). */
|
|
187
191
|
sanitizeUrl(raw, base) {
|
|
192
|
+
if (this.open) return raw;
|
|
188
193
|
try {
|
|
189
194
|
const u = new URL(raw, base ?? (typeof location !== "undefined" ? location.href : void 0));
|
|
190
195
|
if (u.username || u.password) {
|
|
@@ -203,7 +208,7 @@ var DataSanitizer = class {
|
|
|
203
208
|
}
|
|
204
209
|
/** Redige query/fragmento de um valor de atributo de URL preservando a forma original (relativa ou absoluta). */
|
|
205
210
|
redactUrlAttribute(value) {
|
|
206
|
-
if (typeof value !== "string" || !/[?#@]/.test(value)) return value;
|
|
211
|
+
if (this.open || typeof value !== "string" || !/[?#@]/.test(value)) return value;
|
|
207
212
|
const hashAt = value.indexOf("#");
|
|
208
213
|
const hash = hashAt >= 0 ? value.slice(hashAt) : "";
|
|
209
214
|
const noHash = hashAt >= 0 ? value.slice(0, hashAt) : value;
|
|
@@ -397,7 +402,7 @@ var ApiClient = class {
|
|
|
397
402
|
const headers = {
|
|
398
403
|
Accept: "application/json",
|
|
399
404
|
Authorization: `Bearer ${this.getToken()}`,
|
|
400
|
-
"X-Offscript-Sdk":
|
|
405
|
+
"X-Offscript-Sdk": chunkKQVK5EJR_cjs.SDK_VERSION,
|
|
401
406
|
...extraHeaders
|
|
402
407
|
};
|
|
403
408
|
let payload;
|
|
@@ -700,7 +705,7 @@ var EventTransport = class extends Emitter {
|
|
|
700
705
|
const first = events[0].sequence;
|
|
701
706
|
const last = events[events.length - 1].sequence;
|
|
702
707
|
return {
|
|
703
|
-
schemaVersion:
|
|
708
|
+
schemaVersion: chunkKQVK5EJR_cjs.SCHEMA_VERSION,
|
|
704
709
|
batchId: `${id.sessionId}.${id.tabId}.${first}-${last}.${events.length}`,
|
|
705
710
|
sessionId: id.sessionId,
|
|
706
711
|
executionId: id.executionId,
|
|
@@ -708,7 +713,7 @@ var EventTransport = class extends Emitter {
|
|
|
708
713
|
sequenceStart: first,
|
|
709
714
|
sequenceEnd: last,
|
|
710
715
|
createdAt: Date.now(),
|
|
711
|
-
sdkVersion:
|
|
716
|
+
sdkVersion: chunkKQVK5EJR_cjs.SDK_VERSION,
|
|
712
717
|
events
|
|
713
718
|
};
|
|
714
719
|
}
|
|
@@ -960,6 +965,7 @@ function guard(sink, name, fn) {
|
|
|
960
965
|
|
|
961
966
|
// src/recorders/ActionRecorder.ts
|
|
962
967
|
var KEYS = /* @__PURE__ */ new Set(["Enter", "Escape", "Tab", "Backspace", "Delete", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"]);
|
|
968
|
+
var MODIFIERS = /* @__PURE__ */ new Set(["Control", "Shift", "Alt", "Meta"]);
|
|
963
969
|
var INTERACTIVE = "a,button,input,select,textarea,summary,[role=button],[role=link],[role=menuitem],[role=tab],[role=option],[data-testid],[data-test],[data-cy]";
|
|
964
970
|
var INPUT_IDLE_MS = 700;
|
|
965
971
|
var DRAG_MIN_PX = 8;
|
|
@@ -973,7 +979,7 @@ var ActionRecorder = class {
|
|
|
973
979
|
this.scrollState = /* @__PURE__ */ new Map();
|
|
974
980
|
this.move = [];
|
|
975
981
|
const r = sink.config.recording;
|
|
976
|
-
this.gen = new
|
|
982
|
+
this.gen = new chunkKQVK5EJR_cjs.SelectorGenerator({ testIdAttributes: r.testIdAttributes, stableAttributes: r.stableAttributes });
|
|
977
983
|
}
|
|
978
984
|
start() {
|
|
979
985
|
const d = this.d;
|
|
@@ -1037,7 +1043,7 @@ var ActionRecorder = class {
|
|
|
1037
1043
|
emit(draft, el, extra) {
|
|
1038
1044
|
const s = this.sink.sanitizer;
|
|
1039
1045
|
const data = {
|
|
1040
|
-
schemaVersion:
|
|
1046
|
+
schemaVersion: chunkKQVK5EJR_cjs.ACTION_SCHEMA_VERSION,
|
|
1041
1047
|
type: draft.type,
|
|
1042
1048
|
url: s.sanitizeUrl(location.href),
|
|
1043
1049
|
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
@@ -1085,7 +1091,7 @@ var ActionRecorder = class {
|
|
|
1085
1091
|
onInput(e) {
|
|
1086
1092
|
const el = this.target(e);
|
|
1087
1093
|
if (!el || !(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) return;
|
|
1088
|
-
if (el instanceof HTMLInputElement && ["checkbox", "radio", "file"
|
|
1094
|
+
if (el instanceof HTMLInputElement && ["checkbox", "radio", "file"].includes(el.type)) return;
|
|
1089
1095
|
if (this.pendingInput && this.pendingInput.el !== el) this.flushInput();
|
|
1090
1096
|
this.pendingInput ?? (this.pendingInput = { el, inputType: e.inputType ?? "input", startedAt: Date.now() });
|
|
1091
1097
|
if (this.inputTimer) clearTimeout(this.inputTimer);
|
|
@@ -1103,8 +1109,9 @@ var ActionRecorder = class {
|
|
|
1103
1109
|
const policy = this.sink.config.privacy;
|
|
1104
1110
|
const allowed = policy.allowedInputSelectors.join(",");
|
|
1105
1111
|
const isPassword = el instanceof HTMLInputElement && el.type === "password";
|
|
1106
|
-
|
|
1107
|
-
|
|
1112
|
+
const listed = Boolean(allowed) && safe(() => el.matches(allowed));
|
|
1113
|
+
if (policy.captureSensitiveValues || !isPassword && (!policy.maskSensitiveInputs || listed)) {
|
|
1114
|
+
payload.value = policy.captureSensitiveValues ? value : this.sink.sanitizer.scrubString(value);
|
|
1108
1115
|
payload.valueCaptured = true;
|
|
1109
1116
|
} else {
|
|
1110
1117
|
payload.valueCaptured = false;
|
|
@@ -1132,13 +1139,15 @@ var ActionRecorder = class {
|
|
|
1132
1139
|
}
|
|
1133
1140
|
}
|
|
1134
1141
|
onKey(e) {
|
|
1135
|
-
const el = this.target(e);
|
|
1142
|
+
const el = this.target(e) ?? document.body;
|
|
1136
1143
|
if (!el) return;
|
|
1137
1144
|
const modified = e.ctrlKey || e.metaKey || e.altKey;
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1145
|
+
const isModifier = MODIFIERS.has(e.key);
|
|
1146
|
+
const relevant = KEYS.has(e.key) || isModifier || /^F\d{1,2}$/.test(e.key) || modified && e.key.length === 1;
|
|
1147
|
+
if (!relevant || isModifier && e.repeat) return;
|
|
1140
1148
|
if (e.key === "Enter" || e.key === "Tab") this.flushInput();
|
|
1141
|
-
|
|
1149
|
+
const modifiers = mods(e).filter((name) => name !== e.key);
|
|
1150
|
+
this.emit({ type: "keydown", payload: { key: e.key, code: e.code, modifiers, repeat: e.repeat } }, el);
|
|
1142
1151
|
}
|
|
1143
1152
|
onFocus(type, e) {
|
|
1144
1153
|
const el = this.target(e);
|
|
@@ -1271,7 +1280,7 @@ function parseBrowser(ua) {
|
|
|
1271
1280
|
}
|
|
1272
1281
|
return {};
|
|
1273
1282
|
}
|
|
1274
|
-
function collectContext(id, reason, appVersion) {
|
|
1283
|
+
function collectContext(id, reason, appVersion, captureSensitiveValues = false) {
|
|
1275
1284
|
const nav = navigator;
|
|
1276
1285
|
const ua = nav.userAgent ?? "";
|
|
1277
1286
|
let browser = parseBrowser(ua);
|
|
@@ -1298,8 +1307,9 @@ function collectContext(id, reason, appVersion) {
|
|
|
1298
1307
|
platformId: id.platformId,
|
|
1299
1308
|
testCaseId: id.testCaseId,
|
|
1300
1309
|
executionId: id.executionId,
|
|
1301
|
-
sdkVersion:
|
|
1302
|
-
appVersion
|
|
1310
|
+
sdkVersion: chunkKQVK5EJR_cjs.SDK_VERSION,
|
|
1311
|
+
appVersion,
|
|
1312
|
+
captureSensitiveValues
|
|
1303
1313
|
};
|
|
1304
1314
|
}
|
|
1305
1315
|
var ContextRecorder = class {
|
|
@@ -1318,7 +1328,7 @@ var ContextRecorder = class {
|
|
|
1318
1328
|
}
|
|
1319
1329
|
record(reason) {
|
|
1320
1330
|
guard(this.sink, this.name, () => {
|
|
1321
|
-
const data = collectContext(this.identity(), reason, this.sink.config.appVersion);
|
|
1331
|
+
const data = collectContext(this.identity(), reason, this.sink.config.appVersion, this.sink.config.privacy.captureSensitiveValues);
|
|
1322
1332
|
data.url = this.sink.sanitizer.sanitizeUrl(data.url);
|
|
1323
1333
|
if (data.referrer) data.referrer = this.sink.sanitizer.sanitizeUrl(data.referrer);
|
|
1324
1334
|
this.sink.emit("context", data, { priority: "normal" });
|
|
@@ -1872,9 +1882,10 @@ var RRWebRecorder = class {
|
|
|
1872
1882
|
emit: (event) => this.onEvent(event),
|
|
1873
1883
|
blockSelector: block || void 0,
|
|
1874
1884
|
maskTextSelector: maskText || void 0,
|
|
1875
|
-
maskAllInputs: privacy.maskSensitiveInputs,
|
|
1876
|
-
maskInputOptions: privacy.maskSensitiveInputs ? { password: true, email: true, tel: true, text: true, textarea: true, search: true, url: true, number: true, select: true, date: true, "datetime-local": true, month: true, time: true, week: true } : { password: true },
|
|
1885
|
+
maskAllInputs: privacy.maskSensitiveInputs && !privacy.captureSensitiveValues,
|
|
1886
|
+
maskInputOptions: privacy.captureSensitiveValues ? { password: false } : privacy.maskSensitiveInputs ? { password: true, email: true, tel: true, text: true, textarea: true, search: true, url: true, number: true, select: true, date: true, "datetime-local": true, month: true, time: true, week: true } : { password: true },
|
|
1877
1887
|
maskInputFn: (text, el) => {
|
|
1888
|
+
if (privacy.captureSensitiveValues) return text;
|
|
1878
1889
|
if (el?.type === "password") return "*".repeat(text.length);
|
|
1879
1890
|
if (allowed && safe(() => el.matches(allowed))) return text;
|
|
1880
1891
|
return "*".repeat(text.length);
|
|
@@ -2038,6 +2049,9 @@ var SessionManager = class {
|
|
|
2038
2049
|
get currentToken() {
|
|
2039
2050
|
return this.token;
|
|
2040
2051
|
}
|
|
2052
|
+
hasTokenAvailable() {
|
|
2053
|
+
return Boolean(this.token || readQueryToken());
|
|
2054
|
+
}
|
|
2041
2055
|
// ---- persistência -------------------------------------------------------
|
|
2042
2056
|
loadPersisted() {
|
|
2043
2057
|
const raw = safe(() => sessionStorage.getItem(this.storageKey));
|
|
@@ -2106,16 +2120,18 @@ var SessionManager = class {
|
|
|
2106
2120
|
// ---- ciclo de vida ------------------------------------------------------
|
|
2107
2121
|
/** Cria (ou obtém, de forma idempotente) a sessão no backend. */
|
|
2108
2122
|
async create(opts, context) {
|
|
2109
|
-
|
|
2123
|
+
const queryToken = consumeQueryToken();
|
|
2124
|
+
const sessionToken = opts?.sessionToken || this.token || queryToken;
|
|
2125
|
+
if (!sessionToken || !opts.executionId || !opts.testCaseId)
|
|
2110
2126
|
throw new AuthError("sessionToken, executionId e testCaseId s\xE3o obrigat\xF3rios");
|
|
2111
|
-
this.token =
|
|
2127
|
+
this.token = sessionToken;
|
|
2112
2128
|
const tabId = await resolveTabId(this.cfg.storagePrefix, this.loadPersisted()?.tabId);
|
|
2113
2129
|
const res = await this.api.createSession({
|
|
2114
2130
|
platformId: this.cfg.platformId,
|
|
2115
2131
|
testCaseId: opts.testCaseId,
|
|
2116
2132
|
executionId: opts.executionId,
|
|
2117
2133
|
tabId,
|
|
2118
|
-
sdkVersion:
|
|
2134
|
+
sdkVersion: chunkKQVK5EJR_cjs.SDK_VERSION,
|
|
2119
2135
|
appVersion: this.cfg.appVersion,
|
|
2120
2136
|
context
|
|
2121
2137
|
});
|
|
@@ -2206,6 +2222,27 @@ var SessionManager = class {
|
|
|
2206
2222
|
this.seq = 0;
|
|
2207
2223
|
}
|
|
2208
2224
|
};
|
|
2225
|
+
function consumeQueryToken() {
|
|
2226
|
+
if (typeof window === "undefined") return "";
|
|
2227
|
+
try {
|
|
2228
|
+
const token = readQueryToken();
|
|
2229
|
+
if (!token) return "";
|
|
2230
|
+
const url = new URL(window.location.href);
|
|
2231
|
+
url.searchParams.delete("offscript_token");
|
|
2232
|
+
window.history.replaceState(window.history.state, typeof document === "undefined" ? "" : document.title, `${url.pathname}${url.search}${url.hash}`);
|
|
2233
|
+
return token;
|
|
2234
|
+
} catch {
|
|
2235
|
+
return "";
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
function readQueryToken() {
|
|
2239
|
+
if (typeof window === "undefined") return "";
|
|
2240
|
+
try {
|
|
2241
|
+
return new URL(window.location.href).searchParams.get("offscript_token")?.trim() ?? "";
|
|
2242
|
+
} catch {
|
|
2243
|
+
return "";
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2209
2246
|
var heldLocks = [];
|
|
2210
2247
|
async function resolveTabId(prefix, previous) {
|
|
2211
2248
|
const candidate = previous ?? uid("tab_");
|
|
@@ -2264,10 +2301,628 @@ function resolveConfig(input) {
|
|
|
2264
2301
|
autoResume: input.autoResume ?? true,
|
|
2265
2302
|
heartbeatIntervalMs: input.heartbeatIntervalMs ?? 3e4,
|
|
2266
2303
|
storagePrefix: input.storagePrefix ?? "offscript",
|
|
2267
|
-
debug: input.debug ?? false
|
|
2304
|
+
debug: input.debug ?? false,
|
|
2305
|
+
controls: input.controls ?? true
|
|
2268
2306
|
};
|
|
2269
2307
|
}
|
|
2270
2308
|
|
|
2309
|
+
// src/ui/RecordingControls.ts
|
|
2310
|
+
var HOST_ATTR = "data-offscript-console";
|
|
2311
|
+
var EYE_SEQUENCE = [
|
|
2312
|
+
{ pose: "open", duration: 2600 },
|
|
2313
|
+
{ pose: "blink", duration: 160 },
|
|
2314
|
+
{ pose: "open", duration: 1800 },
|
|
2315
|
+
{ pose: "left", duration: 700 },
|
|
2316
|
+
{ pose: "open", duration: 1500 },
|
|
2317
|
+
{ pose: "wink", duration: 480 },
|
|
2318
|
+
{ pose: "open", duration: 1900 },
|
|
2319
|
+
{ pose: "right", duration: 700 },
|
|
2320
|
+
{ pose: "open", duration: 1600 },
|
|
2321
|
+
{ pose: "up", duration: 640 },
|
|
2322
|
+
{ pose: "happy", duration: 720 }
|
|
2323
|
+
];
|
|
2324
|
+
var EYE_PATHS = {
|
|
2325
|
+
open: ["M59.5 79.5v15", "M93 79.5v15"],
|
|
2326
|
+
blink: ["M52 89q7.5 5 15 0", "M85.5 89q7.5 5 15 0"],
|
|
2327
|
+
wink: ["M52 89q7.5 5 15 0", "M93 79.5v15"],
|
|
2328
|
+
happy: ["M52 91q7.5-12 15 0", "M85.5 91q7.5-12 15 0"]
|
|
2329
|
+
};
|
|
2330
|
+
var RecordingControls = class {
|
|
2331
|
+
constructor(sdk) {
|
|
2332
|
+
this.sdk = sdk;
|
|
2333
|
+
this.eyeTimer = 0;
|
|
2334
|
+
this.placeFrame = 0;
|
|
2335
|
+
this.eyeIndex = 0;
|
|
2336
|
+
this.busy = false;
|
|
2337
|
+
this.expanded = true;
|
|
2338
|
+
this.privacyOpen = true;
|
|
2339
|
+
this.failure = "";
|
|
2340
|
+
this.onVisibility = () => {
|
|
2341
|
+
window.clearTimeout(this.eyeTimer);
|
|
2342
|
+
if (!document.hidden) this.scheduleEye();
|
|
2343
|
+
};
|
|
2344
|
+
this.onViewport = () => {
|
|
2345
|
+
this.placeConfirm();
|
|
2346
|
+
this.schedulePlacePrivacy();
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
mount() {
|
|
2350
|
+
if (this.host || typeof document === "undefined") return;
|
|
2351
|
+
const host = document.createElement("div");
|
|
2352
|
+
host.setAttribute(HOST_ATTR, "");
|
|
2353
|
+
host.setAttribute("data-offscript-ignore", "");
|
|
2354
|
+
host.className = "offscript-ignore";
|
|
2355
|
+
host.style.cssText = "position:fixed;left:0;top:0;width:0;height:0;margin:0;padding:0;border:0;overflow:visible;z-index:2147483646;pointer-events:none;";
|
|
2356
|
+
const shadow = host.attachShadow({ mode: "closed" });
|
|
2357
|
+
shadow.innerHTML = TEMPLATE;
|
|
2358
|
+
this.bind(shadow);
|
|
2359
|
+
this.host = host;
|
|
2360
|
+
this.shadow = shadow;
|
|
2361
|
+
this.note = shadow.querySelector("[data-note]") ?? void 0;
|
|
2362
|
+
this.place();
|
|
2363
|
+
this.watchDom();
|
|
2364
|
+
this.unsubscribe = this.sdk.on("statusChange", () => this.render());
|
|
2365
|
+
window.addEventListener("resize", this.onViewport);
|
|
2366
|
+
window.visualViewport?.addEventListener("resize", this.onViewport);
|
|
2367
|
+
document.fonts?.ready.then(() => {
|
|
2368
|
+
if (this.host) this.onViewport();
|
|
2369
|
+
});
|
|
2370
|
+
this.watchDock();
|
|
2371
|
+
this.render();
|
|
2372
|
+
this.startEyes();
|
|
2373
|
+
}
|
|
2374
|
+
unmount() {
|
|
2375
|
+
this.unsubscribe?.();
|
|
2376
|
+
this.unsubscribe = void 0;
|
|
2377
|
+
this.observer?.disconnect();
|
|
2378
|
+
this.observer = void 0;
|
|
2379
|
+
this.dockWatch?.disconnect();
|
|
2380
|
+
this.dockWatch = void 0;
|
|
2381
|
+
window.cancelAnimationFrame(this.placeFrame);
|
|
2382
|
+
this.placeFrame = 0;
|
|
2383
|
+
window.clearTimeout(this.eyeTimer);
|
|
2384
|
+
window.removeEventListener("resize", this.onViewport);
|
|
2385
|
+
window.visualViewport?.removeEventListener("resize", this.onViewport);
|
|
2386
|
+
document.removeEventListener("visibilitychange", this.onVisibility);
|
|
2387
|
+
this.host?.remove();
|
|
2388
|
+
this.host = void 0;
|
|
2389
|
+
this.shadow = void 0;
|
|
2390
|
+
}
|
|
2391
|
+
place() {
|
|
2392
|
+
const host = this.host;
|
|
2393
|
+
const parent = document.body ?? document.documentElement;
|
|
2394
|
+
if (!host || !parent || host.parentNode === parent) return;
|
|
2395
|
+
parent.appendChild(host);
|
|
2396
|
+
}
|
|
2397
|
+
watchDom() {
|
|
2398
|
+
if (typeof MutationObserver === "undefined") return;
|
|
2399
|
+
this.observer?.disconnect();
|
|
2400
|
+
this.observer = new MutationObserver(() => {
|
|
2401
|
+
this.place();
|
|
2402
|
+
if (document.body) this.observer?.observe(document.body, { childList: true });
|
|
2403
|
+
});
|
|
2404
|
+
this.observer.observe(document.documentElement, { childList: true });
|
|
2405
|
+
if (document.body) this.observer.observe(document.body, { childList: true });
|
|
2406
|
+
}
|
|
2407
|
+
watchDock() {
|
|
2408
|
+
const dock = this.shadow?.querySelector(".dock");
|
|
2409
|
+
if (!(dock instanceof HTMLElement) || typeof ResizeObserver === "undefined") return;
|
|
2410
|
+
this.dockWatch?.disconnect();
|
|
2411
|
+
this.dockWatch = new ResizeObserver(() => this.placePrivacy());
|
|
2412
|
+
this.dockWatch.observe(dock);
|
|
2413
|
+
}
|
|
2414
|
+
bind(shadow) {
|
|
2415
|
+
const stop = (event) => event.stopPropagation();
|
|
2416
|
+
shadow.addEventListener("click", (event) => {
|
|
2417
|
+
const actionNode = event.composedPath().find((node) => node instanceof Element && node.hasAttribute("data-action"));
|
|
2418
|
+
const action = actionNode instanceof Element ? actionNode.getAttribute("data-action") : null;
|
|
2419
|
+
if (action === "open") {
|
|
2420
|
+
this.expanded = true;
|
|
2421
|
+
this.render();
|
|
2422
|
+
} else if (action === "close") {
|
|
2423
|
+
this.expanded = false;
|
|
2424
|
+
this.render();
|
|
2425
|
+
} else if (action === "record") void this.run(() => this.sdk.startArmedRecording());
|
|
2426
|
+
else if (action === "pause") void this.run(() => this.sdk.pauseRecording());
|
|
2427
|
+
else if (action === "resume") void this.run(() => this.sdk.resumeRecording());
|
|
2428
|
+
else if (action === "stop") void this.run(() => this.sdk.stopRecording());
|
|
2429
|
+
else if (action === "new") this.openConfirm();
|
|
2430
|
+
else if (action === "cancel") this.closeConfirm();
|
|
2431
|
+
else if (action === "confirm") {
|
|
2432
|
+
this.closeConfirm();
|
|
2433
|
+
void this.run(() => this.sdk.startNewSession());
|
|
2434
|
+
} else if (action === "privacy-show") {
|
|
2435
|
+
this.privacyOpen = true;
|
|
2436
|
+
this.render();
|
|
2437
|
+
} else if (action === "privacy-hide") {
|
|
2438
|
+
this.privacyOpen = false;
|
|
2439
|
+
this.render();
|
|
2440
|
+
}
|
|
2441
|
+
}, true);
|
|
2442
|
+
for (const type of ["click", "dblclick", "auxclick", "contextmenu", "pointerdown", "pointerup", "mousedown", "mouseup", "keydown", "keyup", "keypress", "input", "change", "mouseover", "mouseout"]) {
|
|
2443
|
+
shadow.addEventListener(type, stop);
|
|
2444
|
+
}
|
|
2445
|
+
shadow.querySelector("[data-confirm]")?.addEventListener("keydown", (event) => {
|
|
2446
|
+
if (event instanceof KeyboardEvent && event.key === "Escape") this.closeConfirm();
|
|
2447
|
+
});
|
|
2448
|
+
shadow.querySelector("[data-privacy]")?.addEventListener("keydown", (event) => {
|
|
2449
|
+
if (event instanceof KeyboardEvent && event.key === "Escape") {
|
|
2450
|
+
this.privacyOpen = false;
|
|
2451
|
+
this.render();
|
|
2452
|
+
}
|
|
2453
|
+
});
|
|
2454
|
+
}
|
|
2455
|
+
openConfirm() {
|
|
2456
|
+
const bubble = this.shadow?.querySelector("[data-confirm]");
|
|
2457
|
+
if (!(bubble instanceof HTMLElement) || this.busy || !bubble.hidden) return;
|
|
2458
|
+
bubble.hidden = false;
|
|
2459
|
+
this.placeConfirm();
|
|
2460
|
+
this.paintPrivacy();
|
|
2461
|
+
const cancel = bubble.querySelector("[data-action='cancel']");
|
|
2462
|
+
if (cancel instanceof HTMLElement) cancel.focus();
|
|
2463
|
+
}
|
|
2464
|
+
placeConfirm() {
|
|
2465
|
+
const bubble = this.shadow?.querySelector("[data-confirm]");
|
|
2466
|
+
const card = bubble?.querySelector(".bubble");
|
|
2467
|
+
const button = this.shadow?.querySelector("[data-action='new']");
|
|
2468
|
+
if (!(bubble instanceof HTMLElement) || !(card instanceof HTMLElement) || !(button instanceof HTMLElement)) return;
|
|
2469
|
+
const buttonBox = button.getBoundingClientRect();
|
|
2470
|
+
const buttonCenter = buttonBox.left + buttonBox.width / 2;
|
|
2471
|
+
const tailHalf = 4.5;
|
|
2472
|
+
const margin = 12;
|
|
2473
|
+
const viewport = document.documentElement.clientWidth;
|
|
2474
|
+
let rightEdge = buttonCenter + 22 + tailHalf;
|
|
2475
|
+
rightEdge = Math.min(viewport - margin, Math.max(margin + card.offsetWidth, rightEdge));
|
|
2476
|
+
bubble.style.right = `${Math.round(viewport - rightEdge)}px`;
|
|
2477
|
+
const tail = Math.min(Math.max(12, rightEdge - buttonCenter - tailHalf), Math.max(12, card.offsetWidth - 18));
|
|
2478
|
+
card.style.setProperty("--tail", `${Math.round(tail)}px`);
|
|
2479
|
+
}
|
|
2480
|
+
closeConfirm() {
|
|
2481
|
+
const bubble = this.shadow?.querySelector("[data-confirm]");
|
|
2482
|
+
if (bubble instanceof HTMLElement) bubble.hidden = true;
|
|
2483
|
+
this.paintPrivacy();
|
|
2484
|
+
}
|
|
2485
|
+
async run(task) {
|
|
2486
|
+
if (this.busy) return;
|
|
2487
|
+
this.busy = true;
|
|
2488
|
+
this.failure = "";
|
|
2489
|
+
this.render();
|
|
2490
|
+
try {
|
|
2491
|
+
await task();
|
|
2492
|
+
} catch {
|
|
2493
|
+
this.failure = "N\xE3o foi poss\xEDvel concluir";
|
|
2494
|
+
} finally {
|
|
2495
|
+
this.busy = false;
|
|
2496
|
+
this.render();
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
render() {
|
|
2500
|
+
this.place();
|
|
2501
|
+
const status = this.sdk.getState().status;
|
|
2502
|
+
const shell = this.shadow?.querySelector("[data-shell]");
|
|
2503
|
+
shell?.setAttribute("data-open", this.expanded ? "true" : "false");
|
|
2504
|
+
shell?.setAttribute("data-state", status);
|
|
2505
|
+
if (this.note) this.note.textContent = this.failure;
|
|
2506
|
+
const live = status === "recording" || status === "paused";
|
|
2507
|
+
if (!live) this.closeConfirm();
|
|
2508
|
+
this.paintPrivacy();
|
|
2509
|
+
this.setHidden("record", live || status === "initializing" || status === "stopping");
|
|
2510
|
+
this.setHidden("pause", status !== "recording");
|
|
2511
|
+
this.setHidden("resume", status !== "paused");
|
|
2512
|
+
this.setHidden("new", !live);
|
|
2513
|
+
this.setHidden("stop", !live);
|
|
2514
|
+
this.setEnabled("record", !this.busy && !live && status !== "initializing" && status !== "stopping");
|
|
2515
|
+
this.setEnabled("pause", !this.busy && status === "recording");
|
|
2516
|
+
this.setEnabled("resume", !this.busy && status === "paused");
|
|
2517
|
+
this.setEnabled("stop", !this.busy && live);
|
|
2518
|
+
this.setEnabled("new", !this.busy && live);
|
|
2519
|
+
}
|
|
2520
|
+
paintPrivacy() {
|
|
2521
|
+
const flag = this.sdk.capturesSensitiveValues();
|
|
2522
|
+
const confirm = this.shadow?.querySelector("[data-confirm]");
|
|
2523
|
+
const confirmOpen = confirm instanceof HTMLElement && !confirm.hidden;
|
|
2524
|
+
const bubble = this.shadow?.querySelector("[data-privacy]");
|
|
2525
|
+
const show = flag && this.privacyOpen && this.expanded && !confirmOpen;
|
|
2526
|
+
if (bubble instanceof HTMLElement) {
|
|
2527
|
+
bubble.hidden = !show;
|
|
2528
|
+
if (!show) this.clearPrivacyPlace(bubble);
|
|
2529
|
+
}
|
|
2530
|
+
if (show) this.schedulePlacePrivacy();
|
|
2531
|
+
this.setHidden("privacy-show", !flag || this.privacyOpen || !this.expanded);
|
|
2532
|
+
}
|
|
2533
|
+
schedulePlacePrivacy() {
|
|
2534
|
+
this.placePrivacy();
|
|
2535
|
+
window.cancelAnimationFrame(this.placeFrame);
|
|
2536
|
+
this.placeFrame = window.requestAnimationFrame(() => {
|
|
2537
|
+
this.placePrivacy();
|
|
2538
|
+
this.placeFrame = window.requestAnimationFrame(() => {
|
|
2539
|
+
this.placeFrame = 0;
|
|
2540
|
+
this.placePrivacy();
|
|
2541
|
+
});
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2544
|
+
clearPrivacyPlace(wrap) {
|
|
2545
|
+
wrap.style.position = "";
|
|
2546
|
+
wrap.style.transform = "";
|
|
2547
|
+
wrap.style.width = "";
|
|
2548
|
+
wrap.style.maxWidth = "";
|
|
2549
|
+
wrap.style.left = "";
|
|
2550
|
+
wrap.style.right = "";
|
|
2551
|
+
wrap.style.top = "";
|
|
2552
|
+
wrap.style.bottom = "";
|
|
2553
|
+
}
|
|
2554
|
+
placePrivacy() {
|
|
2555
|
+
const wrap = this.shadow?.querySelector("[data-privacy]");
|
|
2556
|
+
const card = wrap?.querySelector(".bubble");
|
|
2557
|
+
const face = this.shadow?.querySelector(".face");
|
|
2558
|
+
if (!(wrap instanceof HTMLElement) || !(card instanceof HTMLElement) || !(face instanceof HTMLElement) || wrap.hidden) return;
|
|
2559
|
+
const box = face.getBoundingClientRect();
|
|
2560
|
+
if (box.width < 8 || box.height < 8 || box.top < 1 && box.left < 1) return;
|
|
2561
|
+
const margin = 12;
|
|
2562
|
+
const viewport = document.documentElement.clientWidth;
|
|
2563
|
+
const maxWidth = Math.min(264, viewport - margin * 2);
|
|
2564
|
+
wrap.style.position = "fixed";
|
|
2565
|
+
wrap.style.transform = "none";
|
|
2566
|
+
wrap.style.width = `${maxWidth}px`;
|
|
2567
|
+
wrap.style.maxWidth = `${maxWidth}px`;
|
|
2568
|
+
wrap.style.left = "auto";
|
|
2569
|
+
wrap.style.bottom = "auto";
|
|
2570
|
+
const faceCenter = box.left + box.width / 2;
|
|
2571
|
+
const tailHalf = 5;
|
|
2572
|
+
let rightEdge = faceCenter + 12 + tailHalf;
|
|
2573
|
+
rightEdge = Math.min(viewport - margin, Math.max(margin + maxWidth, rightEdge));
|
|
2574
|
+
wrap.style.right = `${Math.round(viewport - rightEdge)}px`;
|
|
2575
|
+
wrap.style.top = `${Math.round(Math.max(margin, box.top - 10 - wrap.offsetHeight))}px`;
|
|
2576
|
+
const tail = Math.min(Math.max(12, rightEdge - faceCenter - tailHalf), Math.max(12, maxWidth - 18));
|
|
2577
|
+
card.style.setProperty("--tail", `${Math.round(tail)}px`);
|
|
2578
|
+
}
|
|
2579
|
+
setEnabled(action, enabled) {
|
|
2580
|
+
const button = this.shadow?.querySelector(`[data-action='${action}']`);
|
|
2581
|
+
if (button instanceof HTMLButtonElement) button.disabled = !enabled;
|
|
2582
|
+
}
|
|
2583
|
+
setHidden(action, hidden) {
|
|
2584
|
+
const button = this.shadow?.querySelector(`[data-action='${action}']`);
|
|
2585
|
+
if (button instanceof HTMLButtonElement) button.hidden = hidden;
|
|
2586
|
+
}
|
|
2587
|
+
startEyes() {
|
|
2588
|
+
const media = window.matchMedia?.("(prefers-reduced-motion: reduce)");
|
|
2589
|
+
if (!media || media.matches) return;
|
|
2590
|
+
document.addEventListener("visibilitychange", this.onVisibility);
|
|
2591
|
+
this.scheduleEye();
|
|
2592
|
+
}
|
|
2593
|
+
scheduleEye() {
|
|
2594
|
+
const beat = EYE_SEQUENCE[this.eyeIndex % EYE_SEQUENCE.length];
|
|
2595
|
+
this.applyPose(beat.pose);
|
|
2596
|
+
this.eyeIndex = (this.eyeIndex + 1) % EYE_SEQUENCE.length;
|
|
2597
|
+
this.eyeTimer = window.setTimeout(() => this.scheduleEye(), beat.duration);
|
|
2598
|
+
}
|
|
2599
|
+
applyPose(pose) {
|
|
2600
|
+
const [left, right] = EYE_PATHS[pose] ?? EYE_PATHS.open;
|
|
2601
|
+
this.shadow?.querySelectorAll("[data-eye='left']").forEach((node) => node.setAttribute("d", left));
|
|
2602
|
+
this.shadow?.querySelectorAll("[data-eye='right']").forEach((node) => node.setAttribute("d", right));
|
|
2603
|
+
this.shadow?.querySelectorAll("[data-eyes]").forEach((node) => node.setAttribute("data-pose", pose));
|
|
2604
|
+
}
|
|
2605
|
+
};
|
|
2606
|
+
var ICON = {
|
|
2607
|
+
pause: `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3.5v9M11 3.5v9"/></svg>`,
|
|
2608
|
+
play: `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6 3.8 12.2 8 6 12.2z" fill="currentColor" stroke="none"/></svg>`,
|
|
2609
|
+
close: `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 4l8 8M12 4 4 12"/></svg>`,
|
|
2610
|
+
speak: `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3.5 3.5h9a1.5 1.5 0 0 1 1.5 1.5v5a1.5 1.5 0 0 1-1.5 1.5H7.2L4.5 13.5v-2H3.5A1.5 1.5 0 0 1 2 10V5a1.5 1.5 0 0 1 1.5-1.5Z"/></svg>`
|
|
2611
|
+
};
|
|
2612
|
+
function mark(gradientId) {
|
|
2613
|
+
return `<svg class="mark" viewBox="14 9 138 126" fill="none" aria-hidden="true">
|
|
2614
|
+
<defs>
|
|
2615
|
+
<linearGradient id="${gradientId}" x1="112" y1="41" x2="143" y2="17" gradientUnits="userSpaceOnUse">
|
|
2616
|
+
<stop offset="0" stop-color="#753eff"></stop>
|
|
2617
|
+
<stop offset="1" stop-color="#1772ff"></stop>
|
|
2618
|
+
</linearGradient>
|
|
2619
|
+
</defs>
|
|
2620
|
+
<path d="M72 23C88 23 101 26 109 32L112 40C122 50 128 64 128 80C128 109 104 131 72 131C41 131 16 108 16 80C16 46 40 23 72 23Z" fill="#eeeef7"></path>
|
|
2621
|
+
<path d="M112 41C119 37 123 27 131 22C135 19 139 18 143 17" stroke="url(#${gradientId})" stroke-width="8" stroke-linecap="round"></path>
|
|
2622
|
+
<g class="eyes" data-eyes data-pose="open" stroke="#10182e" stroke-width="7" stroke-linecap="round">
|
|
2623
|
+
<path data-eye="left" d="M59.5 79.5v15"></path>
|
|
2624
|
+
<path data-eye="right" d="M93 79.5v15"></path>
|
|
2625
|
+
</g>
|
|
2626
|
+
</svg>`;
|
|
2627
|
+
}
|
|
2628
|
+
var TEMPLATE = `
|
|
2629
|
+
<style>
|
|
2630
|
+
:host { color-scheme: dark; }
|
|
2631
|
+
button {
|
|
2632
|
+
display: grid;
|
|
2633
|
+
place-items: center;
|
|
2634
|
+
width: 32px;
|
|
2635
|
+
height: 32px;
|
|
2636
|
+
margin: 0;
|
|
2637
|
+
padding: 0;
|
|
2638
|
+
border: 1px solid #3a4458;
|
|
2639
|
+
border-radius: 999px;
|
|
2640
|
+
background: transparent;
|
|
2641
|
+
color: #f4f3fc;
|
|
2642
|
+
cursor: pointer;
|
|
2643
|
+
}
|
|
2644
|
+
button svg {
|
|
2645
|
+
width: 15px;
|
|
2646
|
+
height: 15px;
|
|
2647
|
+
fill: none;
|
|
2648
|
+
stroke: currentColor;
|
|
2649
|
+
stroke-width: 1.7;
|
|
2650
|
+
stroke-linecap: round;
|
|
2651
|
+
stroke-linejoin: round;
|
|
2652
|
+
}
|
|
2653
|
+
button:hover:not(:disabled) { border-color: #a596ff; color: #d0c8ff; }
|
|
2654
|
+
button:focus-visible { outline: 2px solid #a596ff; outline-offset: 2px; }
|
|
2655
|
+
button:disabled { cursor: not-allowed; opacity: 0.4; }
|
|
2656
|
+
button[hidden] { display: none; }
|
|
2657
|
+
button.text {
|
|
2658
|
+
width: auto;
|
|
2659
|
+
padding: 0 12px;
|
|
2660
|
+
font: 600 12px/1 "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2661
|
+
}
|
|
2662
|
+
button.record {
|
|
2663
|
+
border: 0;
|
|
2664
|
+
background: linear-gradient(100deg, #6847ff, #1673ff);
|
|
2665
|
+
color: #fff;
|
|
2666
|
+
}
|
|
2667
|
+
button.finish {
|
|
2668
|
+
border: 0;
|
|
2669
|
+
background: #e5485f;
|
|
2670
|
+
color: #fff;
|
|
2671
|
+
}
|
|
2672
|
+
button.record:hover:not(:disabled),
|
|
2673
|
+
button.finish:hover:not(:disabled) { color: #fff; filter: brightness(1.08); }
|
|
2674
|
+
button.primary {
|
|
2675
|
+
border: 0;
|
|
2676
|
+
background: linear-gradient(100deg, #6847ff, #1673ff);
|
|
2677
|
+
color: #fff;
|
|
2678
|
+
}
|
|
2679
|
+
button.primary:hover:not(:disabled) { color: #fff; filter: brightness(1.08); }
|
|
2680
|
+
.launcher, .dock {
|
|
2681
|
+
position: fixed;
|
|
2682
|
+
right: 16px;
|
|
2683
|
+
bottom: 16px;
|
|
2684
|
+
z-index: 1;
|
|
2685
|
+
border: 1px solid #232b3b;
|
|
2686
|
+
background: #151b27;
|
|
2687
|
+
box-shadow: 0 18px 40px rgba(3, 5, 12, 0.5);
|
|
2688
|
+
pointer-events: auto;
|
|
2689
|
+
font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2690
|
+
}
|
|
2691
|
+
.launcher {
|
|
2692
|
+
width: 56px;
|
|
2693
|
+
height: 56px;
|
|
2694
|
+
padding: 8px;
|
|
2695
|
+
border-radius: 999px;
|
|
2696
|
+
cursor: pointer;
|
|
2697
|
+
}
|
|
2698
|
+
.launcher .mark { width: 100%; height: 100%; }
|
|
2699
|
+
.launcher::after {
|
|
2700
|
+
content: "";
|
|
2701
|
+
position: absolute;
|
|
2702
|
+
right: 1px;
|
|
2703
|
+
bottom: 1px;
|
|
2704
|
+
width: 10px;
|
|
2705
|
+
height: 10px;
|
|
2706
|
+
border-radius: 999px;
|
|
2707
|
+
background: #929bb1;
|
|
2708
|
+
box-shadow: 0 0 0 2px #151b27;
|
|
2709
|
+
}
|
|
2710
|
+
[data-state="recording"] .launcher::after { background: #1673ff; }
|
|
2711
|
+
[data-state="paused"] .launcher::after { background: #d0c8ff; }
|
|
2712
|
+
[data-state="error"] .launcher::after { background: #e5485f; }
|
|
2713
|
+
.dock {
|
|
2714
|
+
display: flex;
|
|
2715
|
+
align-items: center;
|
|
2716
|
+
gap: 10px;
|
|
2717
|
+
max-width: calc(100vw - 32px);
|
|
2718
|
+
padding: 6px 6px 6px 12px;
|
|
2719
|
+
border-radius: 999px;
|
|
2720
|
+
color: #f4f3fc;
|
|
2721
|
+
overflow: visible;
|
|
2722
|
+
}
|
|
2723
|
+
[data-open="false"] .dock, [data-open="true"] .launcher { display: none; }
|
|
2724
|
+
.brand { display: flex; align-items: center; gap: 8px; min-width: 0; overflow: visible; }
|
|
2725
|
+
.face { position: relative; flex: none; width: 28px; height: 28px; overflow: visible; }
|
|
2726
|
+
.dock .mark { width: 28px; height: 28px; }
|
|
2727
|
+
.word {
|
|
2728
|
+
color: #f4f3fc;
|
|
2729
|
+
font-size: 14px;
|
|
2730
|
+
font-weight: 600;
|
|
2731
|
+
letter-spacing: -0.03em;
|
|
2732
|
+
line-height: 1;
|
|
2733
|
+
}
|
|
2734
|
+
.actions { display: flex; align-items: center; gap: 6px; }
|
|
2735
|
+
.note {
|
|
2736
|
+
position: fixed;
|
|
2737
|
+
right: 16px;
|
|
2738
|
+
bottom: 78px;
|
|
2739
|
+
z-index: 1;
|
|
2740
|
+
margin: 0;
|
|
2741
|
+
padding: 4px 10px;
|
|
2742
|
+
border-radius: 999px;
|
|
2743
|
+
background: #151b27;
|
|
2744
|
+
color: #ff9aa8;
|
|
2745
|
+
font: 600 11px/1.3 "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2746
|
+
pointer-events: none;
|
|
2747
|
+
}
|
|
2748
|
+
.note:empty { display: none; }
|
|
2749
|
+
[data-shell]:has(.note:not(:empty)) .privacy { bottom: calc(100% + 48px); }
|
|
2750
|
+
.eyes {
|
|
2751
|
+
transform-box: view-box;
|
|
2752
|
+
transform-origin: 0 0;
|
|
2753
|
+
transition: transform 280ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
2754
|
+
}
|
|
2755
|
+
.eyes[data-pose="left"] { transform: translate(-4.5%, 0.8%); }
|
|
2756
|
+
.eyes[data-pose="right"] { transform: translate(4.5%, 0.8%); }
|
|
2757
|
+
.eyes[data-pose="up"] { transform: translate(0, -3.5%); }
|
|
2758
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2759
|
+
.eyes { transition: none; }
|
|
2760
|
+
.eyes[data-pose] { transform: none; }
|
|
2761
|
+
.speak:not([hidden]) .bubble { animation: none; }
|
|
2762
|
+
}
|
|
2763
|
+
.speak {
|
|
2764
|
+
position: fixed;
|
|
2765
|
+
right: 16px;
|
|
2766
|
+
bottom: 76px;
|
|
2767
|
+
z-index: 2;
|
|
2768
|
+
width: max-content;
|
|
2769
|
+
max-width: min(16rem, calc(100vw - 32px));
|
|
2770
|
+
pointer-events: auto;
|
|
2771
|
+
font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2772
|
+
}
|
|
2773
|
+
.speak[hidden] { display: none; }
|
|
2774
|
+
.bubble {
|
|
2775
|
+
position: relative;
|
|
2776
|
+
padding: 0.6rem 0.7rem 0.55rem;
|
|
2777
|
+
border: 1px solid #3d3590;
|
|
2778
|
+
border-radius: 1rem;
|
|
2779
|
+
background: #1b1a3f;
|
|
2780
|
+
color: #c7cadc;
|
|
2781
|
+
box-shadow: 0 16px 40px rgba(3, 5, 12, 0.45);
|
|
2782
|
+
animation: bubble-in 320ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
2783
|
+
}
|
|
2784
|
+
.bubble::after {
|
|
2785
|
+
content: "";
|
|
2786
|
+
position: absolute;
|
|
2787
|
+
right: var(--tail, 22px);
|
|
2788
|
+
bottom: -5px;
|
|
2789
|
+
width: 9px;
|
|
2790
|
+
height: 9px;
|
|
2791
|
+
border-right: 1px solid #3d3590;
|
|
2792
|
+
border-bottom: 1px solid #3d3590;
|
|
2793
|
+
background: #1b1a3f;
|
|
2794
|
+
transform: rotate(45deg);
|
|
2795
|
+
}
|
|
2796
|
+
.bubble .mark {
|
|
2797
|
+
display: block;
|
|
2798
|
+
width: 22px;
|
|
2799
|
+
height: 22px;
|
|
2800
|
+
margin: 0 0 0.35rem;
|
|
2801
|
+
}
|
|
2802
|
+
@keyframes bubble-in {
|
|
2803
|
+
from { opacity: 0; transform: translateY(8px) scale(0.96); }
|
|
2804
|
+
to { opacity: 1; transform: none; }
|
|
2805
|
+
}
|
|
2806
|
+
.bubble p {
|
|
2807
|
+
margin: 0;
|
|
2808
|
+
color: #c7cadc;
|
|
2809
|
+
font-size: 0.8125rem;
|
|
2810
|
+
font-weight: 400;
|
|
2811
|
+
line-height: 1.45;
|
|
2812
|
+
}
|
|
2813
|
+
.privacy {
|
|
2814
|
+
position: absolute;
|
|
2815
|
+
left: auto;
|
|
2816
|
+
right: 50%;
|
|
2817
|
+
bottom: calc(100% + 18px);
|
|
2818
|
+
z-index: 3;
|
|
2819
|
+
width: min(16.5rem, calc(100vw - 24px));
|
|
2820
|
+
max-width: min(16.5rem, calc(100vw - 24px));
|
|
2821
|
+
pointer-events: auto;
|
|
2822
|
+
font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2823
|
+
transform: translateX(20px);
|
|
2824
|
+
}
|
|
2825
|
+
.privacy[hidden] { display: none; }
|
|
2826
|
+
.privacy .bubble {
|
|
2827
|
+
padding: 0.75rem 2.15rem 0.75rem 0.9rem;
|
|
2828
|
+
border-radius: 16px;
|
|
2829
|
+
}
|
|
2830
|
+
.privacy .bubble::after {
|
|
2831
|
+
right: var(--tail, 12px);
|
|
2832
|
+
bottom: -5px;
|
|
2833
|
+
width: 10px;
|
|
2834
|
+
height: 10px;
|
|
2835
|
+
}
|
|
2836
|
+
@media (max-width: 560px) {
|
|
2837
|
+
.word { display: none; }
|
|
2838
|
+
.privacy {
|
|
2839
|
+
position: fixed;
|
|
2840
|
+
left: 12px;
|
|
2841
|
+
right: 12px;
|
|
2842
|
+
bottom: 76px;
|
|
2843
|
+
width: auto;
|
|
2844
|
+
max-width: none;
|
|
2845
|
+
transform: none;
|
|
2846
|
+
}
|
|
2847
|
+
.privacy .bubble {
|
|
2848
|
+
padding: 0.65rem 1.85rem 0.6rem 0.75rem;
|
|
2849
|
+
}
|
|
2850
|
+
.privacy .bubble p { font-size: 0.75rem; }
|
|
2851
|
+
}
|
|
2852
|
+
.privacy button.hide {
|
|
2853
|
+
position: absolute;
|
|
2854
|
+
top: 0.4rem;
|
|
2855
|
+
right: 0.4rem;
|
|
2856
|
+
width: 24px;
|
|
2857
|
+
height: 24px;
|
|
2858
|
+
border: 0;
|
|
2859
|
+
background: transparent;
|
|
2860
|
+
color: #c7cadc;
|
|
2861
|
+
}
|
|
2862
|
+
.privacy button.hide:hover:not(:disabled) { color: #f4f3fc; }
|
|
2863
|
+
.choices { display: flex; gap: 6px; margin-top: 0.7rem; }
|
|
2864
|
+
.speak button.choice {
|
|
2865
|
+
width: auto;
|
|
2866
|
+
height: auto;
|
|
2867
|
+
min-height: 1.75rem;
|
|
2868
|
+
padding: 0 0.7rem;
|
|
2869
|
+
border-radius: 999px;
|
|
2870
|
+
font: 600 0.75rem/1 "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2871
|
+
}
|
|
2872
|
+
.speak button.secondary {
|
|
2873
|
+
border: 1px solid #3a4458;
|
|
2874
|
+
background: transparent;
|
|
2875
|
+
color: #f4f3fc;
|
|
2876
|
+
}
|
|
2877
|
+
.speak button.secondary:hover:not(:disabled) { border-color: #a596ff; color: #d0c8ff; }
|
|
2878
|
+
.speak button.danger {
|
|
2879
|
+
border: 0;
|
|
2880
|
+
background: #e5485f;
|
|
2881
|
+
color: #fff;
|
|
2882
|
+
}
|
|
2883
|
+
.speak button.danger:hover:not(:disabled) { color: #fff; background: color-mix(in srgb, #e5485f 88%, #fff); }
|
|
2884
|
+
</style>
|
|
2885
|
+
<div data-shell data-open="true" data-state="recording">
|
|
2886
|
+
<button type="button" class="launcher" data-action="open" aria-label="Abrir controles da grava\xE7\xE3o">
|
|
2887
|
+
${mark("offscript-trail-launcher")}
|
|
2888
|
+
</button>
|
|
2889
|
+
<div class="dock" role="toolbar" aria-label="Controles da grava\xE7\xE3o">
|
|
2890
|
+
<div class="brand">
|
|
2891
|
+
<span class="face">
|
|
2892
|
+
${mark("offscript-trail-dock")}
|
|
2893
|
+
<div class="privacy" data-privacy hidden>
|
|
2894
|
+
<div class="bubble" role="status">
|
|
2895
|
+
<button type="button" class="hide" data-action="privacy-hide" aria-label="Fechar aviso">${ICON.close}</button>
|
|
2896
|
+
<p>Tudo vis\xEDvel na tela ser\xE1 gravado, inclusive o que for digitado \u2014 senha e dado sens\xEDvel tamb\xE9m.</p>
|
|
2897
|
+
</div>
|
|
2898
|
+
</div>
|
|
2899
|
+
</span>
|
|
2900
|
+
<span class="word">Offscript</span>
|
|
2901
|
+
</div>
|
|
2902
|
+
<div class="actions">
|
|
2903
|
+
<button type="button" data-action="privacy-show" aria-label="Abrir aviso" title="Tudo vis\xEDvel na tela ser\xE1 gravado, inclusive o que for digitado" hidden>${ICON.speak}</button>
|
|
2904
|
+
<button type="button" class="text record" data-action="record">Iniciar</button>
|
|
2905
|
+
<button type="button" data-action="pause" aria-label="Pausar" title="Pausar">${ICON.pause}</button>
|
|
2906
|
+
<button type="button" data-action="resume" aria-label="Retomar" title="Retomar">${ICON.play}</button>
|
|
2907
|
+
<button type="button" class="text" data-action="new" title="Descarta o que foi gravado e volta ao in\xEDcio">Nova sess\xE3o</button>
|
|
2908
|
+
<button type="button" class="text finish" data-action="stop" title="Encerra a coleta e envia o que foi gravado">Finalizar</button>
|
|
2909
|
+
<button type="button" data-action="close" aria-label="Fechar" title="Fechar">${ICON.close}</button>
|
|
2910
|
+
</div>
|
|
2911
|
+
</div>
|
|
2912
|
+
<div class="speak" data-confirm hidden>
|
|
2913
|
+
<div class="bubble" role="dialog" aria-labelledby="offscript-confirm-title">
|
|
2914
|
+
${mark("offscript-trail-speak")}
|
|
2915
|
+
<p id="offscript-confirm-title">Descartar esta sess\xE3o? O que foi gravado ser\xE1 ignorado.</p>
|
|
2916
|
+
<div class="choices">
|
|
2917
|
+
<button type="button" class="choice secondary" data-action="cancel">Cancelar</button>
|
|
2918
|
+
<button type="button" class="choice danger" data-action="confirm">Descartar</button>
|
|
2919
|
+
</div>
|
|
2920
|
+
</div>
|
|
2921
|
+
</div>
|
|
2922
|
+
<p class="note" data-note></p>
|
|
2923
|
+
</div>
|
|
2924
|
+
`;
|
|
2925
|
+
|
|
2271
2926
|
// src/core/OffscriptSDK.ts
|
|
2272
2927
|
var MAX_LOG = 5e3;
|
|
2273
2928
|
var Offscript = class {
|
|
@@ -2290,6 +2945,9 @@ var Offscript = class {
|
|
|
2290
2945
|
getState() {
|
|
2291
2946
|
return { status: this.status, session: this.sessions.session, pending: this.transport?.pending ?? 0, lastError: this.lastError };
|
|
2292
2947
|
}
|
|
2948
|
+
capturesSensitiveValues() {
|
|
2949
|
+
return this.config.privacy.captureSensitiveValues === true;
|
|
2950
|
+
}
|
|
2293
2951
|
getMetrics() {
|
|
2294
2952
|
return { emitted: this.metrics.emitted, dropped: this.metrics.dropped, avgEmitMs: this.metrics.emitted ? this.metrics.emitMs / this.metrics.emitted : 0, bufferKind: this.transport?.bufferKind };
|
|
2295
2953
|
}
|
|
@@ -2301,6 +2959,7 @@ var Offscript = class {
|
|
|
2301
2959
|
}
|
|
2302
2960
|
async doInit() {
|
|
2303
2961
|
if (typeof window === "undefined") throw new OffscriptError("NOT_SUPPORTED", "O SDK requer um ambiente de navegador");
|
|
2962
|
+
this.ensureControls();
|
|
2304
2963
|
this.setStatus("initializing");
|
|
2305
2964
|
try {
|
|
2306
2965
|
const buffer = await openBuffer(`${this.config.storagePrefix}-${this.config.platformId}`, this.config.transport.persist);
|
|
@@ -2333,9 +2992,10 @@ var Offscript = class {
|
|
|
2333
2992
|
throw new InvalidStateError(`J\xE1 existe grava\xE7\xE3o ativa para a execu\xE7\xE3o ${cur.executionId}. Encerre-a antes.`);
|
|
2334
2993
|
}
|
|
2335
2994
|
if (this.status === "stopping") throw new InvalidStateError("Grava\xE7\xE3o em encerramento");
|
|
2995
|
+
this.lastStart = { testCaseId: opts.testCaseId, executionId: opts.executionId, sessionToken: opts.sessionToken };
|
|
2336
2996
|
this.setStatus("initializing");
|
|
2337
2997
|
try {
|
|
2338
|
-
const ctx = collectContext({ platformId: this.config.platformId, testCaseId: opts.testCaseId, executionId: opts.executionId }, "initial", this.config.appVersion);
|
|
2998
|
+
const ctx = collectContext({ platformId: this.config.platformId, testCaseId: opts.testCaseId, executionId: opts.executionId }, "initial", this.config.appVersion, this.config.privacy.captureSensitiveValues);
|
|
2339
2999
|
await this.sessions.create(opts, { ...ctx, url: this.sanitizer.sanitizeUrl(ctx.url), referrer: void 0, title: void 0 });
|
|
2340
3000
|
const s = this.sessions.session;
|
|
2341
3001
|
await this.transport.start({ sessionId: s.sessionId, executionId: s.executionId, tabId: s.tabId });
|
|
@@ -2370,6 +3030,11 @@ var Offscript = class {
|
|
|
2370
3030
|
if (state.status === "paused") this.recorders.forEach((r) => r.pause?.());
|
|
2371
3031
|
this.actions?.recordPageLoad(safe(() => performance.getEntriesByType("navigation")[0]?.type), document.referrer);
|
|
2372
3032
|
this.sessions.startHeartbeat((r) => this.onHeartbeat(r.status), (e) => void this.onFatal(e), () => this.transport?.pending ?? 0, () => this.status);
|
|
3033
|
+
const token = this.sessions.currentToken;
|
|
3034
|
+
if (token) {
|
|
3035
|
+
this.lastStart = { testCaseId: s.testCaseId, executionId: s.executionId, sessionToken: token };
|
|
3036
|
+
this.armed ?? (this.armed = { testCaseId: s.testCaseId, sessionToken: token });
|
|
3037
|
+
}
|
|
2373
3038
|
this.bus.emit("sessionStart", s);
|
|
2374
3039
|
} catch (e) {
|
|
2375
3040
|
await this.teardown();
|
|
@@ -2379,6 +3044,38 @@ var Offscript = class {
|
|
|
2379
3044
|
if (!(e instanceof SessionExpiredError || e instanceof SessionDisabledError || e instanceof AuthError)) throw e;
|
|
2380
3045
|
}
|
|
2381
3046
|
}
|
|
3047
|
+
/** Guarda caso de teste e token para o botão de gravar, sem abrir a sessão. */
|
|
3048
|
+
armRecording(opts) {
|
|
3049
|
+
this.armed = { testCaseId: opts.testCaseId, sessionToken: opts.sessionToken };
|
|
3050
|
+
}
|
|
3051
|
+
/** Abre a coleta com os dados armados, ou com os da última sessão. */
|
|
3052
|
+
startArmedRecording() {
|
|
3053
|
+
if ((this.status === "recording" || this.status === "paused") && this.sessions.session) return Promise.resolve(this.sessions.session);
|
|
3054
|
+
return this.startRecording(this.freshStart());
|
|
3055
|
+
}
|
|
3056
|
+
/** Descarta a coleta atual e volta ao estado inicial, sem abrir outra. */
|
|
3057
|
+
async startNewSession() {
|
|
3058
|
+
if (this.status !== "recording" && this.status !== "paused") return;
|
|
3059
|
+
await this.discardRecording();
|
|
3060
|
+
}
|
|
3061
|
+
freshStart() {
|
|
3062
|
+
const base = this.armed ?? (this.lastStart ? { testCaseId: this.lastStart.testCaseId, sessionToken: this.lastStart.sessionToken } : void 0);
|
|
3063
|
+
if (!base?.testCaseId || !base.sessionToken) throw new InvalidStateError("N\xE3o h\xE1 dados para iniciar a grava\xE7\xE3o");
|
|
3064
|
+
return { testCaseId: base.testCaseId, sessionToken: base.sessionToken, executionId: uid("exec_") };
|
|
3065
|
+
}
|
|
3066
|
+
async discardRecording() {
|
|
3067
|
+
const sessionId = this.sessions.session?.sessionId ?? "";
|
|
3068
|
+
this.accepting = false;
|
|
3069
|
+
this.stopRecorders();
|
|
3070
|
+
this.sessions.stopHeartbeat();
|
|
3071
|
+
this.transport?.stop();
|
|
3072
|
+
if (sessionId) await this.transport?.clearSession(sessionId);
|
|
3073
|
+
this.lifecycle?.uninstall();
|
|
3074
|
+
this.sessions.clearPersisted();
|
|
3075
|
+
this.sessions.reset();
|
|
3076
|
+
this.setStatus("idle");
|
|
3077
|
+
if (sessionId) this.bus.emit("sessionDiscard", { sessionId });
|
|
3078
|
+
}
|
|
2382
3079
|
async pauseRecording() {
|
|
2383
3080
|
if (this.status === "paused") return;
|
|
2384
3081
|
if (this.status !== "recording") throw new InvalidStateError(`N\xE3o \xE9 poss\xEDvel pausar no estado ${this.status}`);
|
|
@@ -2455,6 +3152,8 @@ var Offscript = class {
|
|
|
2455
3152
|
}
|
|
2456
3153
|
this.destroyed = true;
|
|
2457
3154
|
await this.teardown();
|
|
3155
|
+
this.controls?.unmount();
|
|
3156
|
+
this.controls = void 0;
|
|
2458
3157
|
this.sessions.reset();
|
|
2459
3158
|
this.transport?.close();
|
|
2460
3159
|
this.bus.clear();
|
|
@@ -2463,7 +3162,7 @@ var Offscript = class {
|
|
|
2463
3162
|
exportPlaywrightData() {
|
|
2464
3163
|
const s = this.sessions.session;
|
|
2465
3164
|
if (!s) throw new InvalidStateError("Sem sess\xE3o ativa");
|
|
2466
|
-
return
|
|
3165
|
+
return chunkKQVK5EJR_cjs.buildPlaywrightContract({
|
|
2467
3166
|
session: { sessionId: s.sessionId, executionId: s.executionId, testCaseId: s.testCaseId, platformId: s.platformId },
|
|
2468
3167
|
actions: this.actionLog,
|
|
2469
3168
|
contexts: this.contextLog.map((e) => e.data)
|
|
@@ -2580,6 +3279,7 @@ var Offscript = class {
|
|
|
2580
3279
|
this.transport?.setOnline(typeof navigator === "undefined" ? true : navigator.onLine !== false);
|
|
2581
3280
|
}
|
|
2582
3281
|
async startRecorders() {
|
|
3282
|
+
this.ensureControls();
|
|
2583
3283
|
const r = this.config.recording;
|
|
2584
3284
|
const list = [];
|
|
2585
3285
|
if (r.rrweb) list.push(this.rrweb = new RRWebRecorder(this));
|
|
@@ -2602,6 +3302,13 @@ var Offscript = class {
|
|
|
2602
3302
|
for (const r of this.recorders.splice(0).reverse()) safe(() => r.stop());
|
|
2603
3303
|
this.rrweb = this.actions = this.context = void 0;
|
|
2604
3304
|
}
|
|
3305
|
+
ensureControls() {
|
|
3306
|
+
if (!this.config.controls || typeof document === "undefined" || !this.sessions.hasTokenAvailable()) return;
|
|
3307
|
+
safe(() => {
|
|
3308
|
+
this.controls ?? (this.controls = new RecordingControls(this));
|
|
3309
|
+
this.controls.mount();
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
2605
3312
|
async teardown() {
|
|
2606
3313
|
this.accepting = false;
|
|
2607
3314
|
this.stopRecorders();
|
|
@@ -2653,7 +3360,7 @@ var Offscript = class {
|
|
|
2653
3360
|
if (this.config.debug) console.debug("[offscript]", ...args);
|
|
2654
3361
|
}
|
|
2655
3362
|
};
|
|
2656
|
-
Offscript.version =
|
|
3363
|
+
Offscript.version = chunkKQVK5EJR_cjs.SDK_VERSION;
|
|
2657
3364
|
|
|
2658
3365
|
exports.AuthError = AuthError;
|
|
2659
3366
|
exports.ConfigError = ConfigError;
|
|
@@ -2667,5 +3374,5 @@ exports.OffscriptError = OffscriptError;
|
|
|
2667
3374
|
exports.SessionDisabledError = SessionDisabledError;
|
|
2668
3375
|
exports.SessionExpiredError = SessionExpiredError;
|
|
2669
3376
|
exports.TransportError = TransportError;
|
|
2670
|
-
//# sourceMappingURL=chunk-
|
|
2671
|
-
//# sourceMappingURL=chunk-
|
|
3377
|
+
//# sourceMappingURL=chunk-I6JPZAQA.cjs.map
|
|
3378
|
+
//# sourceMappingURL=chunk-I6JPZAQA.cjs.map
|