browsertrack 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -6
- package/README.md +2 -0
- package/dist/{chunk-464D4U2U.js → chunk-5NR5K3ER.js} +299 -44
- package/dist/chunk-5NR5K3ER.js.map +1 -0
- package/dist/{chunk-3HOXPTM2.js → chunk-G5CIZSQM.js} +808 -53
- package/dist/chunk-G5CIZSQM.js.map +1 -0
- package/dist/{chunk-6VA7GBAO.js → chunk-ONPW7AYL.js} +144 -12
- package/dist/chunk-ONPW7AYL.js.map +1 -0
- package/dist/{chunk-INXDWPJW.js → chunk-PG4JJDCV.js} +353 -119
- package/dist/chunk-PG4JJDCV.js.map +1 -0
- package/dist/cli/index.js +1058 -546
- package/dist/cli/index.js.map +1 -1
- package/dist/client/index.cjs +449 -128
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +2 -2
- package/dist/client.iife.js +121 -15
- package/dist/core/index.d.ts +32 -2
- package/dist/core/index.js +11 -1
- package/dist/daemon/index.d.ts +2 -2
- package/dist/daemon/index.js +6 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.js +16 -7
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +7 -4
- package/dist/{server-DiVmTrIR.d.ts → server-BRG-RQQP.d.ts} +10 -1
- package/docs/cli.md +4 -1
- package/docs/getting-started.md +60 -6
- package/docs/mcp-reference.md +42 -0
- package/package.json +1 -1
- package/packages/cli/src/index.ts +247 -151
- package/packages/client/src/interceptors/navigation.ts +38 -26
- package/packages/client/src/interceptors/network.ts +22 -17
- package/packages/client/src/notes/inspector.ts +289 -65
- package/packages/client/src/source/resolver.ts +9 -3
- package/packages/client/src/transport/websocket.ts +23 -18
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/safety.ts +86 -0
- package/packages/core/src/selector.ts +110 -15
- package/packages/daemon/src/server/daemon.ts +7 -1
- package/packages/daemon/src/server/http.ts +125 -5
- package/packages/daemon/src/server/ws.ts +33 -29
- package/packages/daemon/src/storage/db.ts +57 -35
- package/packages/mcp/src/handlers.ts +121 -45
- package/packages/mcp/src/server.ts +221 -3
- package/test/core/safety.test.ts +106 -0
- package/test/core/selector.test.ts +133 -0
- package/test/daemon/storage.test.ts +36 -0
- package/test/e2e/daemon-mcp-e2e.test.ts +10 -0
- package/test/mcp/auto-start.test.ts +87 -0
- package/dist/chunk-3HOXPTM2.js.map +0 -1
- package/dist/chunk-464D4U2U.js.map +0 -1
- package/dist/chunk-6VA7GBAO.js.map +0 -1
- package/dist/chunk-7OCOQGDN.js +0 -635
- package/dist/chunk-7OCOQGDN.js.map +0 -1
- package/dist/chunk-INXDWPJW.js.map +0 -1
package/dist/client/index.cjs
CHANGED
|
@@ -133,7 +133,8 @@ function redactUrl(rawUrl) {
|
|
|
133
133
|
function getSemanticSelector(element, options = {}) {
|
|
134
134
|
if (!element || !element.tagName) return "unknown";
|
|
135
135
|
const maxClasses = options.maxClasses ?? 2;
|
|
136
|
-
const
|
|
136
|
+
const maxParentDepth = options.maxParentDepth ?? 3;
|
|
137
|
+
const testId = element.getAttribute("data-testid") || element.getAttribute("data-test") || element.getAttribute("data-cy") || element.getAttribute("data-qa") || element.getAttribute("data-component");
|
|
137
138
|
if (testId) {
|
|
138
139
|
return `[data-testid="${testId}"]`;
|
|
139
140
|
}
|
|
@@ -141,34 +142,122 @@ function getSemanticSelector(element, options = {}) {
|
|
|
141
142
|
if (id && !/^[0-9]+$/.test(id) && !/[0-9a-f]{8}-[0-9a-f]{4}/i.test(id)) {
|
|
142
143
|
return `#${id}`;
|
|
143
144
|
}
|
|
145
|
+
const tag = element.tagName.toLowerCase();
|
|
144
146
|
const ariaLabel = element.getAttribute("aria-label");
|
|
145
|
-
if (ariaLabel && ariaLabel.length <
|
|
146
|
-
return `${
|
|
147
|
+
if (ariaLabel && ariaLabel.length < 40) {
|
|
148
|
+
return `${tag}[aria-label="${ariaLabel}"]`;
|
|
149
|
+
}
|
|
150
|
+
const role = element.getAttribute("role");
|
|
151
|
+
if (role && !["presentation", "none"].includes(role)) {
|
|
152
|
+
const roleSelector = `${tag}[role="${role}"]`;
|
|
153
|
+
if (tag === "button" || tag === "a" || tag === "nav" || tag === "dialog") {
|
|
154
|
+
return roleSelector;
|
|
155
|
+
}
|
|
147
156
|
}
|
|
148
157
|
const nameAttr = element.getAttribute("name");
|
|
149
158
|
if (nameAttr) {
|
|
150
|
-
return `${
|
|
151
|
-
}
|
|
152
|
-
const
|
|
153
|
-
|
|
159
|
+
return `${tag}[name="${nameAttr}"]`;
|
|
160
|
+
}
|
|
161
|
+
const titleAttr = element.getAttribute("title");
|
|
162
|
+
if (titleAttr && titleAttr.length < 30) {
|
|
163
|
+
return `${tag}[title="${titleAttr}"]`;
|
|
164
|
+
}
|
|
165
|
+
const rawClassList = Array.from(element.classList || []);
|
|
166
|
+
const classList = rawClassList.filter(
|
|
167
|
+
(c) => !c.startsWith("css-") && !c.startsWith("_") && !/^[a-z0-9]{5,}$/i.test(c) && // filter dynamic hashed utility classes
|
|
168
|
+
!c.includes(":") && // Tailwind pseudo-class prefixes (hover:, md:, etc.)
|
|
169
|
+
!c.includes("[")
|
|
170
|
+
// Tailwind arbitrary value classes
|
|
171
|
+
).slice(0, maxClasses);
|
|
154
172
|
if (classList.length > 0) {
|
|
155
173
|
const classStr = classList.map((c) => `.${c}`).join("");
|
|
156
174
|
return `${tag}${classStr}`;
|
|
157
175
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
176
|
+
const hasParent = element.parentElement && (typeof document === "undefined" || element.parentElement !== document.body && element.parentElement !== document.documentElement);
|
|
177
|
+
if (hasParent) {
|
|
178
|
+
let suffix = "";
|
|
179
|
+
if (element.parentElement.children && element.parentElement.children.length > 1) {
|
|
180
|
+
const sameTagSiblings = Array.from(element.parentElement.children).filter(
|
|
181
|
+
(child) => child.tagName && child.tagName.toLowerCase() === tag
|
|
182
|
+
);
|
|
183
|
+
if (sameTagSiblings.length > 1) {
|
|
184
|
+
const index = sameTagSiblings.indexOf(element) + 1;
|
|
185
|
+
if (index > 0) {
|
|
186
|
+
suffix = `:nth-of-type(${index})`;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (maxParentDepth > 0) {
|
|
191
|
+
const parentSelector = getSemanticSelector(element.parentElement, {
|
|
192
|
+
maxClasses: 1,
|
|
193
|
+
maxParentDepth: maxParentDepth - 1
|
|
194
|
+
});
|
|
195
|
+
if (parentSelector && parentSelector !== "unknown" && parentSelector !== "body" && parentSelector !== "html") {
|
|
196
|
+
return `${parentSelector} > ${tag}${suffix}`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (suffix) {
|
|
200
|
+
return `${tag}${suffix}`;
|
|
162
201
|
}
|
|
163
202
|
}
|
|
164
203
|
return tag;
|
|
165
204
|
}
|
|
205
|
+
function resolveMeaningfulTarget(element) {
|
|
206
|
+
if (!element || typeof element !== "object") {
|
|
207
|
+
return element;
|
|
208
|
+
}
|
|
209
|
+
const el = element;
|
|
210
|
+
const isSvg = el.tagName.toLowerCase() === "svg" || el.namespaceURI?.includes("svg");
|
|
211
|
+
if (isSvg || el.closest?.("svg")) {
|
|
212
|
+
const interactiveParent = el.closest?.(
|
|
213
|
+
'button, a, [role="button"], [role="link"], [role="tab"], summary, label, [data-testid], [data-component]'
|
|
214
|
+
);
|
|
215
|
+
if (interactiveParent) return interactiveParent;
|
|
216
|
+
const svgEl = isSvg ? el : el.closest?.("svg");
|
|
217
|
+
if (svgEl) return svgEl;
|
|
218
|
+
}
|
|
219
|
+
const leafTags = ["span", "i", "em", "b", "strong", "small", "mark", "code"];
|
|
220
|
+
const tag = el.tagName.toLowerCase();
|
|
221
|
+
if (leafTags.includes(tag) && !el.id && !el.getAttribute("data-testid") && !el.getAttribute("data-component") && !el.getAttribute("aria-label")) {
|
|
222
|
+
const interactiveParent = el.closest?.(
|
|
223
|
+
'button, a, [role="button"], [role="link"], [role="tab"], [role="menuitem"], summary, label, [data-testid], [data-component]'
|
|
224
|
+
);
|
|
225
|
+
if (interactiveParent) return interactiveParent;
|
|
226
|
+
}
|
|
227
|
+
return el;
|
|
228
|
+
}
|
|
166
229
|
function truncate(str, maxLength = 200) {
|
|
167
230
|
if (!str) return "";
|
|
168
231
|
if (str.length <= maxLength) return str;
|
|
169
232
|
return str.slice(0, maxLength) + "...";
|
|
170
233
|
}
|
|
171
234
|
|
|
235
|
+
// packages/core/src/safety.ts
|
|
236
|
+
function safeJsonStringify(val, fallback = "{}") {
|
|
237
|
+
if (val === void 0) return fallback;
|
|
238
|
+
try {
|
|
239
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
240
|
+
return JSON.stringify(val, (key, value) => {
|
|
241
|
+
if (typeof value === "object" && value !== null) {
|
|
242
|
+
if (seen.has(value)) {
|
|
243
|
+
return "[Circular]";
|
|
244
|
+
}
|
|
245
|
+
seen.add(value);
|
|
246
|
+
}
|
|
247
|
+
if (typeof value === "bigint") {
|
|
248
|
+
return value.toString();
|
|
249
|
+
}
|
|
250
|
+
return value;
|
|
251
|
+
});
|
|
252
|
+
} catch {
|
|
253
|
+
try {
|
|
254
|
+
return JSON.stringify(String(val));
|
|
255
|
+
} catch {
|
|
256
|
+
return fallback;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
172
261
|
// packages/client/src/source/resolver.ts
|
|
173
262
|
function resolveComponentSource(el) {
|
|
174
263
|
if (!el || typeof el !== "object") return void 0;
|
|
@@ -218,7 +307,9 @@ function resolveReactComponent(el) {
|
|
|
218
307
|
}
|
|
219
308
|
}
|
|
220
309
|
let curr = hostFiber;
|
|
221
|
-
|
|
310
|
+
let depth = 0;
|
|
311
|
+
while (curr && depth < 50) {
|
|
312
|
+
depth++;
|
|
222
313
|
const type = curr.type;
|
|
223
314
|
const name = getReactComponentName(type);
|
|
224
315
|
if (name && !name.startsWith("html:") && name !== "Fragment") {
|
|
@@ -273,7 +364,9 @@ function resolveVueComponent(el) {
|
|
|
273
364
|
const sourceFile = type.__file;
|
|
274
365
|
const hierarchy = [];
|
|
275
366
|
let curr = vueParent;
|
|
276
|
-
|
|
367
|
+
let depth = 0;
|
|
368
|
+
while (curr && depth < 50) {
|
|
369
|
+
depth++;
|
|
277
370
|
const cType = curr.type || {};
|
|
278
371
|
const cName = cType.name || cType.__name || cType.displayName;
|
|
279
372
|
if (cName && !hierarchy.includes(cName)) {
|
|
@@ -310,7 +403,9 @@ function resolveVueComponent(el) {
|
|
|
310
403
|
function resolveSvelteComponent(el) {
|
|
311
404
|
try {
|
|
312
405
|
let curr = el;
|
|
313
|
-
|
|
406
|
+
let depth = 0;
|
|
407
|
+
while (curr && depth < 50) {
|
|
408
|
+
depth++;
|
|
314
409
|
const meta = curr.__svelte_meta;
|
|
315
410
|
if (meta && meta.loc) {
|
|
316
411
|
return {
|
|
@@ -849,12 +944,16 @@ function setupConsoleInterceptors(onConsole) {
|
|
|
849
944
|
function setupNavigationInterceptors(onNavigation) {
|
|
850
945
|
if (typeof window === "undefined" || typeof history === "undefined") return () => {
|
|
851
946
|
};
|
|
852
|
-
let currentUrl =
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
947
|
+
let currentUrl = "";
|
|
948
|
+
try {
|
|
949
|
+
currentUrl = redactUrl(window.location.href);
|
|
950
|
+
onNavigation({
|
|
951
|
+
to: currentUrl,
|
|
952
|
+
type: "initial",
|
|
953
|
+
timestamp: Date.now()
|
|
954
|
+
});
|
|
955
|
+
} catch {
|
|
956
|
+
}
|
|
858
957
|
const originalPushState = history.pushState;
|
|
859
958
|
const originalReplaceState = history.replaceState;
|
|
860
959
|
history.pushState = function(data, unused, url) {
|
|
@@ -890,26 +989,32 @@ function setupNavigationInterceptors(onNavigation) {
|
|
|
890
989
|
return result;
|
|
891
990
|
};
|
|
892
991
|
const onPopState = () => {
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
992
|
+
try {
|
|
993
|
+
const from = currentUrl;
|
|
994
|
+
const to = redactUrl(window.location.href);
|
|
995
|
+
currentUrl = to;
|
|
996
|
+
onNavigation({
|
|
997
|
+
from,
|
|
998
|
+
to,
|
|
999
|
+
type: "popstate",
|
|
1000
|
+
timestamp: Date.now()
|
|
1001
|
+
});
|
|
1002
|
+
} catch {
|
|
1003
|
+
}
|
|
902
1004
|
};
|
|
903
1005
|
const onHashChange = () => {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1006
|
+
try {
|
|
1007
|
+
const from = currentUrl;
|
|
1008
|
+
const to = redactUrl(window.location.href);
|
|
1009
|
+
currentUrl = to;
|
|
1010
|
+
onNavigation({
|
|
1011
|
+
from,
|
|
1012
|
+
to,
|
|
1013
|
+
type: "hashchange",
|
|
1014
|
+
timestamp: Date.now()
|
|
1015
|
+
});
|
|
1016
|
+
} catch {
|
|
1017
|
+
}
|
|
913
1018
|
};
|
|
914
1019
|
window.addEventListener("popstate", onPopState);
|
|
915
1020
|
window.addEventListener("hashchange", onHashChange);
|
|
@@ -946,31 +1051,38 @@ function setupNetworkInterceptors(onNetwork) {
|
|
|
946
1051
|
urlStr = "unknown_url";
|
|
947
1052
|
}
|
|
948
1053
|
const safeUrl = redactUrl(urlStr);
|
|
1054
|
+
let response;
|
|
949
1055
|
try {
|
|
950
|
-
|
|
951
|
-
const durationMs = Date.now() - startTime;
|
|
952
|
-
onNetwork({
|
|
953
|
-
url: safeUrl,
|
|
954
|
-
method,
|
|
955
|
-
status: response.status,
|
|
956
|
-
statusText: response.statusText,
|
|
957
|
-
durationMs,
|
|
958
|
-
timestamp: startTime
|
|
959
|
-
});
|
|
960
|
-
return response;
|
|
1056
|
+
response = await originalFetch.apply(window, [input, init2]);
|
|
961
1057
|
} catch (err) {
|
|
962
|
-
const
|
|
1058
|
+
const durationMs2 = Date.now() - startTime;
|
|
963
1059
|
const isAbort = err?.name === "AbortError";
|
|
1060
|
+
try {
|
|
1061
|
+
onNetwork({
|
|
1062
|
+
url: safeUrl,
|
|
1063
|
+
method,
|
|
1064
|
+
durationMs: durationMs2,
|
|
1065
|
+
error: err?.message || "Network request failed",
|
|
1066
|
+
aborted: isAbort,
|
|
1067
|
+
timestamp: startTime
|
|
1068
|
+
});
|
|
1069
|
+
} catch {
|
|
1070
|
+
}
|
|
1071
|
+
throw err;
|
|
1072
|
+
}
|
|
1073
|
+
const durationMs = Date.now() - startTime;
|
|
1074
|
+
try {
|
|
964
1075
|
onNetwork({
|
|
965
1076
|
url: safeUrl,
|
|
966
1077
|
method,
|
|
1078
|
+
status: response.status,
|
|
1079
|
+
statusText: response.statusText,
|
|
967
1080
|
durationMs,
|
|
968
|
-
error: err?.message || "Network request failed",
|
|
969
|
-
aborted: isAbort,
|
|
970
1081
|
timestamp: startTime
|
|
971
1082
|
});
|
|
972
|
-
|
|
1083
|
+
} catch {
|
|
973
1084
|
}
|
|
1085
|
+
return response;
|
|
974
1086
|
};
|
|
975
1087
|
cleanups.push(() => {
|
|
976
1088
|
window.fetch = originalFetch;
|
|
@@ -1358,15 +1470,18 @@ var WebSocketTransport = class {
|
|
|
1358
1470
|
}
|
|
1359
1471
|
}
|
|
1360
1472
|
send(payload) {
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1473
|
+
try {
|
|
1474
|
+
const raw = safeJsonStringify(payload);
|
|
1475
|
+
if (this.isConnected()) {
|
|
1476
|
+
try {
|
|
1477
|
+
this.ws.send(raw);
|
|
1478
|
+
} catch {
|
|
1479
|
+
this.enqueue(raw);
|
|
1480
|
+
}
|
|
1481
|
+
} else {
|
|
1366
1482
|
this.enqueue(raw);
|
|
1367
1483
|
}
|
|
1368
|
-
}
|
|
1369
|
-
this.enqueue(raw);
|
|
1484
|
+
} catch {
|
|
1370
1485
|
}
|
|
1371
1486
|
}
|
|
1372
1487
|
enqueue(raw) {
|
|
@@ -1391,17 +1506,17 @@ var WebSocketTransport = class {
|
|
|
1391
1506
|
}
|
|
1392
1507
|
sendHello() {
|
|
1393
1508
|
if (typeof window === "undefined") return;
|
|
1394
|
-
const hello = {
|
|
1395
|
-
type: "hello",
|
|
1396
|
-
origin: window.location.origin,
|
|
1397
|
-
url: window.location.href,
|
|
1398
|
-
title: document.title,
|
|
1399
|
-
userAgent: navigator.userAgent,
|
|
1400
|
-
timestamp: Date.now(),
|
|
1401
|
-
projectId: this.projectId
|
|
1402
|
-
};
|
|
1403
1509
|
try {
|
|
1404
|
-
|
|
1510
|
+
const hello = {
|
|
1511
|
+
type: "hello",
|
|
1512
|
+
origin: window.location.origin,
|
|
1513
|
+
url: window.location.href,
|
|
1514
|
+
title: document.title,
|
|
1515
|
+
userAgent: navigator.userAgent,
|
|
1516
|
+
timestamp: Date.now(),
|
|
1517
|
+
projectId: this.projectId
|
|
1518
|
+
};
|
|
1519
|
+
this.ws.send(safeJsonStringify(hello));
|
|
1405
1520
|
} catch {
|
|
1406
1521
|
}
|
|
1407
1522
|
}
|
|
@@ -1459,12 +1574,19 @@ var NoteInspector = class {
|
|
|
1459
1574
|
__publicField(this, "savedNotes", []);
|
|
1460
1575
|
__publicField(this, "showMarkers", true);
|
|
1461
1576
|
__publicField(this, "isHidden", false);
|
|
1577
|
+
__publicField(this, "isToolbarCollapsed", false);
|
|
1462
1578
|
__publicField(this, "cleanups", []);
|
|
1463
1579
|
this.transport = transport;
|
|
1464
1580
|
this.screenshotDriver = screenshotDriver;
|
|
1465
1581
|
const hiddenByQuery = shouldHideUIFromUrl(options.hideQueryParam);
|
|
1466
1582
|
this.isHidden = options.hidden === true || hiddenByQuery;
|
|
1467
1583
|
this.showMarkers = options.showBadges !== false && !this.isHidden;
|
|
1584
|
+
try {
|
|
1585
|
+
if (typeof window !== "undefined" && window.sessionStorage) {
|
|
1586
|
+
this.isToolbarCollapsed = window.sessionStorage.getItem("bt_toolbar_collapsed") === "true";
|
|
1587
|
+
}
|
|
1588
|
+
} catch {
|
|
1589
|
+
}
|
|
1468
1590
|
this.options = {
|
|
1469
1591
|
shortcut: "Alt+Click",
|
|
1470
1592
|
maskSelectors: ['input[type="password"]', "[data-sensitive]"],
|
|
@@ -1592,11 +1714,28 @@ var NoteInspector = class {
|
|
|
1592
1714
|
this.shadowRoot = this.container.attachShadow({ mode: "open" });
|
|
1593
1715
|
const style = document.createElement("style");
|
|
1594
1716
|
style.textContent = `
|
|
1595
|
-
:
|
|
1596
|
-
|
|
1597
|
-
|
|
1717
|
+
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
|
|
1718
|
+
|
|
1719
|
+
:host, :host *, :host *::before, :host *::after {
|
|
1720
|
+
box-sizing: border-box;
|
|
1598
1721
|
-webkit-font-smoothing: antialiased;
|
|
1599
1722
|
-moz-osx-font-smoothing: grayscale;
|
|
1723
|
+
text-rendering: optimizeLegibility;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
:host {
|
|
1727
|
+
font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
1728
|
+
color-scheme: dark;
|
|
1729
|
+
letter-spacing: -0.012em;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
button, input, textarea, select {
|
|
1733
|
+
font-family: inherit;
|
|
1734
|
+
letter-spacing: inherit;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
code, pre, kbd, .bt-mono, .bt-badge, .bt-region-badge, .bt-target-pill, .bt-component-pill {
|
|
1738
|
+
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
1600
1739
|
}
|
|
1601
1740
|
|
|
1602
1741
|
/* 1. Element Highlight Overlay */
|
|
@@ -1830,7 +1969,7 @@ var NoteInspector = class {
|
|
|
1830
1969
|
pointer-events: auto;
|
|
1831
1970
|
z-index: 2147483646;
|
|
1832
1971
|
user-select: none;
|
|
1833
|
-
transition: all 0.
|
|
1972
|
+
transition: all 0.22s cubic-bezier(0.16, 1, 0.3, 1);
|
|
1834
1973
|
}
|
|
1835
1974
|
|
|
1836
1975
|
.bt-toolbar-btn {
|
|
@@ -1891,6 +2030,86 @@ var NoteInspector = class {
|
|
|
1891
2030
|
margin: 0 2px;
|
|
1892
2031
|
}
|
|
1893
2032
|
|
|
2033
|
+
.bt-toolbar-collapse-btn {
|
|
2034
|
+
background: transparent;
|
|
2035
|
+
border: none;
|
|
2036
|
+
color: #64748b;
|
|
2037
|
+
font-size: 11px;
|
|
2038
|
+
font-weight: 700;
|
|
2039
|
+
padding: 6px 8px;
|
|
2040
|
+
border-radius: 20px;
|
|
2041
|
+
cursor: pointer;
|
|
2042
|
+
display: flex;
|
|
2043
|
+
align-items: center;
|
|
2044
|
+
justify-content: center;
|
|
2045
|
+
transition: all 0.15s ease;
|
|
2046
|
+
font-family: inherit;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
.bt-toolbar-collapse-btn:hover {
|
|
2050
|
+
background: #1e293b;
|
|
2051
|
+
color: #f8fafc;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
/* Collapsed Toolbar State */
|
|
2055
|
+
.bt-toolbar.bt-toolbar-collapsed {
|
|
2056
|
+
padding: 0;
|
|
2057
|
+
background: transparent;
|
|
2058
|
+
border: none;
|
|
2059
|
+
box-shadow: none;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
.bt-toolbar.bt-toolbar-collapsed .bt-toolbar-btn,
|
|
2063
|
+
.bt-toolbar.bt-toolbar-collapsed .bt-toolbar-divider,
|
|
2064
|
+
.bt-toolbar.bt-toolbar-collapsed .bt-toolbar-collapse-btn {
|
|
2065
|
+
display: none !important;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
.bt-toolbar-trigger {
|
|
2069
|
+
display: none;
|
|
2070
|
+
background: #0f172a;
|
|
2071
|
+
border: 1px solid #334155;
|
|
2072
|
+
border-radius: 30px;
|
|
2073
|
+
padding: 6px 14px;
|
|
2074
|
+
color: #38bdf8;
|
|
2075
|
+
font-size: 12px;
|
|
2076
|
+
font-weight: 600;
|
|
2077
|
+
cursor: pointer;
|
|
2078
|
+
align-items: center;
|
|
2079
|
+
gap: 8px;
|
|
2080
|
+
box-shadow: 0 10px 25px -3px rgba(0,0,0,0.6), 0 4px 6px -4px rgba(0,0,0,0.4);
|
|
2081
|
+
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
2082
|
+
font-family: inherit;
|
|
2083
|
+
user-select: none;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
.bt-toolbar.bt-toolbar-collapsed .bt-toolbar-trigger {
|
|
2087
|
+
display: inline-flex;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
.bt-toolbar-trigger:hover {
|
|
2091
|
+
background: #1e293b;
|
|
2092
|
+
color: #ffffff;
|
|
2093
|
+
transform: translateY(-2px);
|
|
2094
|
+
box-shadow: 0 12px 28px -3px rgba(0,0,0,0.7);
|
|
2095
|
+
border-color: #38bdf8;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
.bt-pulse-dot {
|
|
2099
|
+
width: 7px;
|
|
2100
|
+
height: 7px;
|
|
2101
|
+
border-radius: 50%;
|
|
2102
|
+
background: #38bdf8;
|
|
2103
|
+
box-shadow: 0 0 8px #38bdf8;
|
|
2104
|
+
display: inline-block;
|
|
2105
|
+
animation: bt-dot-pulse 2s infinite;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
@keyframes bt-dot-pulse {
|
|
2109
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
2110
|
+
50% { opacity: 0.4; transform: scale(0.85); }
|
|
2111
|
+
}
|
|
2112
|
+
|
|
1894
2113
|
/* 5. Modals & Popover Card */
|
|
1895
2114
|
.bt-modal-backdrop {
|
|
1896
2115
|
position: fixed;
|
|
@@ -2301,8 +2520,13 @@ var NoteInspector = class {
|
|
|
2301
2520
|
createToolbar() {
|
|
2302
2521
|
if (!this.shadowRoot || this.toolbarElement) return;
|
|
2303
2522
|
this.toolbarElement = document.createElement("div");
|
|
2304
|
-
this.toolbarElement.className = "bt-toolbar"
|
|
2523
|
+
this.toolbarElement.className = `bt-toolbar ${this.isToolbarCollapsed ? "bt-toolbar-collapsed" : ""}`;
|
|
2305
2524
|
this.toolbarElement.innerHTML = `
|
|
2525
|
+
<button class="bt-toolbar-trigger" id="bt-toolbar-expand" title="Expand BrowserTrack toolbar (Alt+T)">
|
|
2526
|
+
<span class="bt-pulse-dot"></span>
|
|
2527
|
+
<span>\u26A1 BrowserTrack</span>
|
|
2528
|
+
<span class="bt-count-pill" id="bt-notes-count-collapsed">0</span>
|
|
2529
|
+
</button>
|
|
2306
2530
|
<button class="bt-toolbar-btn" id="bt-mode-element" title="Inspect element (or hold Alt+Click)">
|
|
2307
2531
|
<span>\u{1F3AF}</span> Element
|
|
2308
2532
|
</button>
|
|
@@ -2320,12 +2544,26 @@ var NoteInspector = class {
|
|
|
2320
2544
|
<button class="bt-toolbar-btn active" id="bt-toggle-notes" title="Toggle visible note markers on screen">
|
|
2321
2545
|
<span>\u{1F4CC}</span> Notes <span class="bt-count-pill" id="bt-notes-count">0</span>
|
|
2322
2546
|
</button>
|
|
2547
|
+
<div class="bt-toolbar-divider"></div>
|
|
2548
|
+
<button class="bt-toolbar-collapse-btn" id="bt-toolbar-collapse" title="Collapse toolbar (Alt+T)">
|
|
2549
|
+
<span>\u276F</span>
|
|
2550
|
+
</button>
|
|
2323
2551
|
`;
|
|
2552
|
+
const btnExpand = this.toolbarElement.querySelector("#bt-toolbar-expand");
|
|
2553
|
+
const btnCollapse = this.toolbarElement.querySelector("#bt-toolbar-collapse");
|
|
2324
2554
|
const btnElement = this.toolbarElement.querySelector("#bt-mode-element");
|
|
2325
2555
|
const btnRegion = this.toolbarElement.querySelector("#bt-mode-region");
|
|
2326
2556
|
const btnPage = this.toolbarElement.querySelector("#bt-mode-page");
|
|
2327
2557
|
const btnFlow = this.toolbarElement.querySelector("#bt-mode-flow");
|
|
2328
2558
|
const btnToggleNotes = this.toolbarElement.querySelector("#bt-toggle-notes");
|
|
2559
|
+
btnExpand.onclick = (e) => {
|
|
2560
|
+
e.stopPropagation();
|
|
2561
|
+
this.toggleToolbarCollapse(false);
|
|
2562
|
+
};
|
|
2563
|
+
btnCollapse.onclick = (e) => {
|
|
2564
|
+
e.stopPropagation();
|
|
2565
|
+
this.toggleToolbarCollapse(true);
|
|
2566
|
+
};
|
|
2329
2567
|
btnElement.onclick = (e) => {
|
|
2330
2568
|
e.stopPropagation();
|
|
2331
2569
|
this.setMode(this.activeMode === "element" ? "idle" : "element");
|
|
@@ -2354,6 +2592,17 @@ var NoteInspector = class {
|
|
|
2354
2592
|
};
|
|
2355
2593
|
this.shadowRoot.appendChild(this.toolbarElement);
|
|
2356
2594
|
}
|
|
2595
|
+
toggleToolbarCollapse(force) {
|
|
2596
|
+
if (!this.toolbarElement) return;
|
|
2597
|
+
this.isToolbarCollapsed = typeof force === "boolean" ? force : !this.isToolbarCollapsed;
|
|
2598
|
+
this.toolbarElement.classList.toggle("bt-toolbar-collapsed", this.isToolbarCollapsed);
|
|
2599
|
+
try {
|
|
2600
|
+
if (typeof window !== "undefined" && window.sessionStorage) {
|
|
2601
|
+
window.sessionStorage.setItem("bt_toolbar_collapsed", String(this.isToolbarCollapsed));
|
|
2602
|
+
}
|
|
2603
|
+
} catch {
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2357
2606
|
updateToolbarState() {
|
|
2358
2607
|
if (!this.toolbarElement) return;
|
|
2359
2608
|
const btnElement = this.toolbarElement.querySelector("#bt-mode-element");
|
|
@@ -2377,11 +2626,15 @@ var NoteInspector = class {
|
|
|
2377
2626
|
}
|
|
2378
2627
|
updateToolbarCount() {
|
|
2379
2628
|
if (!this.toolbarElement) return;
|
|
2629
|
+
const openCount = this.savedNotes.filter((n) => n.status === "OPEN").length;
|
|
2380
2630
|
const countEl = this.toolbarElement.querySelector("#bt-notes-count");
|
|
2381
2631
|
if (countEl) {
|
|
2382
|
-
const openCount = this.savedNotes.filter((n) => n.status === "OPEN").length;
|
|
2383
2632
|
countEl.textContent = String(openCount);
|
|
2384
2633
|
}
|
|
2634
|
+
const countCollapsedEl = this.toolbarElement.querySelector("#bt-notes-count-collapsed");
|
|
2635
|
+
if (countCollapsedEl) {
|
|
2636
|
+
countCollapsedEl.textContent = String(openCount);
|
|
2637
|
+
}
|
|
2385
2638
|
}
|
|
2386
2639
|
renderMarkers() {
|
|
2387
2640
|
const root = this.ensureContainer();
|
|
@@ -2389,9 +2642,15 @@ var NoteInspector = class {
|
|
|
2389
2642
|
this.markersContainer.innerHTML = "";
|
|
2390
2643
|
if (!this.showMarkers || this.isHidden) return;
|
|
2391
2644
|
const currentPath = window.location.pathname;
|
|
2392
|
-
const activeNotes = this.savedNotes.filter(
|
|
2393
|
-
(n
|
|
2394
|
-
|
|
2645
|
+
const activeNotes = this.savedNotes.filter((n) => {
|
|
2646
|
+
if (n.status !== "OPEN") return false;
|
|
2647
|
+
if (!n.route) return true;
|
|
2648
|
+
if (n.route === currentPath) return true;
|
|
2649
|
+
if (n.route.includes("#") || n.route.includes("?")) {
|
|
2650
|
+
return window.location.href.includes(n.route);
|
|
2651
|
+
}
|
|
2652
|
+
return false;
|
|
2653
|
+
});
|
|
2395
2654
|
const pageNotes = [];
|
|
2396
2655
|
activeNotes.forEach((note, index) => {
|
|
2397
2656
|
if (note.type === "page") {
|
|
@@ -2431,6 +2690,10 @@ var NoteInspector = class {
|
|
|
2431
2690
|
}
|
|
2432
2691
|
const rect = targetEl ? targetEl.getBoundingClientRect() : note.target?.boundingRect;
|
|
2433
2692
|
if (rect) {
|
|
2693
|
+
const isOffscreen = targetEl && (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth);
|
|
2694
|
+
if (isOffscreen) {
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2434
2697
|
const pin = document.createElement("div");
|
|
2435
2698
|
pin.className = markerClass;
|
|
2436
2699
|
pin.setAttribute("data-note-id", note.id);
|
|
@@ -2616,65 +2879,90 @@ var NoteInspector = class {
|
|
|
2616
2879
|
}
|
|
2617
2880
|
setupListeners() {
|
|
2618
2881
|
const onMouseMove = (e) => {
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
this.
|
|
2623
|
-
|
|
2624
|
-
}
|
|
2625
|
-
if (e.altKey || this.activeMode === "element") {
|
|
2626
|
-
const target = document.elementFromPoint(e.clientX, e.clientY);
|
|
2627
|
-
if (target && target !== this.container && !this.container?.contains(target)) {
|
|
2628
|
-
this.hoveredElement = target;
|
|
2629
|
-
this.updateHighlight(target);
|
|
2882
|
+
try {
|
|
2883
|
+
if (this.isHidden || this.modalOverlay || this.cardOverlay || this.isDraggingRegion || this.activeMode === "region") return;
|
|
2884
|
+
const path = e.composedPath ? e.composedPath() : [];
|
|
2885
|
+
if (this.container && path.includes(this.container)) {
|
|
2886
|
+
this.hideHighlight();
|
|
2630
2887
|
return;
|
|
2631
2888
|
}
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2889
|
+
if (e.altKey || this.activeMode === "element") {
|
|
2890
|
+
const rawTarget = document.elementFromPoint(e.clientX, e.clientY);
|
|
2891
|
+
const target = rawTarget ? resolveMeaningfulTarget(rawTarget) : null;
|
|
2892
|
+
if (target && target !== this.container && !this.container?.contains(target)) {
|
|
2893
|
+
this.hoveredElement = target;
|
|
2894
|
+
this.updateHighlight(target);
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
if (!e.altKey && this.activeMode !== "element") {
|
|
2899
|
+
this.hideHighlight();
|
|
2900
|
+
}
|
|
2901
|
+
} catch {
|
|
2635
2902
|
}
|
|
2636
2903
|
};
|
|
2637
2904
|
const onClick = (e) => {
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
e.
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
this.
|
|
2651
|
-
|
|
2652
|
-
this.
|
|
2905
|
+
try {
|
|
2906
|
+
if (this.isHidden || this.modalOverlay || this.cardOverlay) return;
|
|
2907
|
+
const path = e.composedPath ? e.composedPath() : [];
|
|
2908
|
+
if (this.container && path.includes(this.container)) {
|
|
2909
|
+
return;
|
|
2910
|
+
}
|
|
2911
|
+
if (this.activeMode === "region") return;
|
|
2912
|
+
if (e.altKey || this.activeMode === "element") {
|
|
2913
|
+
e.preventDefault();
|
|
2914
|
+
e.stopPropagation();
|
|
2915
|
+
const rawTarget = this.hoveredElement || document.elementFromPoint(e.clientX, e.clientY);
|
|
2916
|
+
const target = rawTarget ? resolveMeaningfulTarget(rawTarget) : null;
|
|
2917
|
+
if (target && target !== this.container && !this.container?.contains(target)) {
|
|
2918
|
+
this.selectedElement = target;
|
|
2919
|
+
this.openNoteEditor(target, "element");
|
|
2920
|
+
if (this.activeMode === "element" && !this.activeScenario) {
|
|
2921
|
+
this.setMode("idle");
|
|
2922
|
+
}
|
|
2653
2923
|
}
|
|
2654
2924
|
}
|
|
2925
|
+
} catch {
|
|
2655
2926
|
}
|
|
2656
2927
|
};
|
|
2657
2928
|
const onKeyDown = (e) => {
|
|
2658
|
-
|
|
2659
|
-
if (
|
|
2660
|
-
|
|
2661
|
-
this.
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2929
|
+
try {
|
|
2930
|
+
if (e.altKey && (e.key === "t" || e.key === "T")) {
|
|
2931
|
+
e.preventDefault();
|
|
2932
|
+
this.toggleToolbarCollapse();
|
|
2933
|
+
return;
|
|
2934
|
+
}
|
|
2935
|
+
if (e.key === "Escape") {
|
|
2936
|
+
if (this.cardOverlay && this.cardOverlay.parentElement && this.shadowRoot) {
|
|
2937
|
+
this.shadowRoot.removeChild(this.cardOverlay);
|
|
2938
|
+
this.cardOverlay = null;
|
|
2939
|
+
} else if (this.activeMode === "region" || this.activeMode === "element") {
|
|
2940
|
+
if (!this.activeScenario) {
|
|
2941
|
+
this.setMode("idle");
|
|
2942
|
+
}
|
|
2665
2943
|
}
|
|
2666
2944
|
}
|
|
2945
|
+
} catch {
|
|
2667
2946
|
}
|
|
2668
2947
|
};
|
|
2669
2948
|
const onKeyUp = (e) => {
|
|
2670
|
-
|
|
2671
|
-
this.
|
|
2949
|
+
try {
|
|
2950
|
+
if (e.key === "Alt" && !this.modalOverlay && !this.cardOverlay && this.activeMode !== "element") {
|
|
2951
|
+
this.hideHighlight();
|
|
2952
|
+
}
|
|
2953
|
+
} catch {
|
|
2672
2954
|
}
|
|
2673
2955
|
};
|
|
2674
2956
|
const onScrollOrResize = () => {
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2957
|
+
try {
|
|
2958
|
+
requestAnimationFrame(() => {
|
|
2959
|
+
try {
|
|
2960
|
+
this.updateMarkerPositions();
|
|
2961
|
+
} catch {
|
|
2962
|
+
}
|
|
2963
|
+
});
|
|
2964
|
+
} catch {
|
|
2965
|
+
}
|
|
2678
2966
|
};
|
|
2679
2967
|
window.addEventListener("mousemove", onMouseMove, { capture: true, passive: true });
|
|
2680
2968
|
window.addEventListener("click", onClick, { capture: true });
|
|
@@ -2751,6 +3039,11 @@ var NoteInspector = class {
|
|
|
2751
3039
|
this.regionBox.appendChild(badge);
|
|
2752
3040
|
}
|
|
2753
3041
|
badge.textContent = `Region: ${Math.round(width)} \xD7 ${Math.round(height)} px`;
|
|
3042
|
+
if (top < 32) {
|
|
3043
|
+
badge.style.top = "4px";
|
|
3044
|
+
} else {
|
|
3045
|
+
badge.style.top = "-26px";
|
|
3046
|
+
}
|
|
2754
3047
|
};
|
|
2755
3048
|
this.regionOverlay.onmouseup = (e) => {
|
|
2756
3049
|
if (!this.isDraggingRegion) return;
|
|
@@ -2809,6 +3102,18 @@ var NoteInspector = class {
|
|
|
2809
3102
|
const selector = getSemanticSelector(el);
|
|
2810
3103
|
const label = this.activeScenario ? `\u{1F3AC} Step ${this.activeScenario.stepNumber} \xB7 ${selector}` : selector;
|
|
2811
3104
|
badge.textContent = `${label} (${Math.round(rect.width)} \xD7 ${Math.round(rect.height)} px)`;
|
|
3105
|
+
if (rect.top < 32) {
|
|
3106
|
+
badge.style.top = `${rect.height + 4}px`;
|
|
3107
|
+
} else {
|
|
3108
|
+
badge.style.top = "-26px";
|
|
3109
|
+
}
|
|
3110
|
+
if (rect.left + 260 > window.innerWidth) {
|
|
3111
|
+
badge.style.left = "auto";
|
|
3112
|
+
badge.style.right = "0";
|
|
3113
|
+
} else {
|
|
3114
|
+
badge.style.left = "0";
|
|
3115
|
+
badge.style.right = "auto";
|
|
3116
|
+
}
|
|
2812
3117
|
}
|
|
2813
3118
|
hideHighlight() {
|
|
2814
3119
|
if (this.highlightOverlay) {
|
|
@@ -2951,10 +3256,11 @@ var NoteInspector = class {
|
|
|
2951
3256
|
btnNextStep.disabled = true;
|
|
2952
3257
|
if (btnSave) btnSave.disabled = true;
|
|
2953
3258
|
if (btnFinishFlow) btnFinishFlow.disabled = true;
|
|
3259
|
+
closeModal();
|
|
2954
3260
|
try {
|
|
2955
3261
|
await options.onSave(message, action);
|
|
2956
|
-
}
|
|
2957
|
-
|
|
3262
|
+
} catch {
|
|
3263
|
+
this.showToast("Failed to save note", "\u26A0\uFE0F");
|
|
2958
3264
|
}
|
|
2959
3265
|
};
|
|
2960
3266
|
if (btnSave) {
|
|
@@ -2981,8 +3287,12 @@ var NoteInspector = class {
|
|
|
2981
3287
|
const selector = noteType === "page" ? "body" : getSemanticSelector(targetEl);
|
|
2982
3288
|
let screenshotDataUrl;
|
|
2983
3289
|
try {
|
|
2984
|
-
const
|
|
2985
|
-
|
|
3290
|
+
const snapPromise = this.screenshotDriver.captureElement(targetEl);
|
|
3291
|
+
const timeoutPromise = new Promise(
|
|
3292
|
+
(resolve) => setTimeout(() => resolve({ ok: false }), 800)
|
|
3293
|
+
);
|
|
3294
|
+
const snap = await Promise.race([snapPromise, timeoutPromise]);
|
|
3295
|
+
if (snap && snap.ok && snap.dataUrl) {
|
|
2986
3296
|
screenshotDataUrl = snap.dataUrl;
|
|
2987
3297
|
}
|
|
2988
3298
|
} catch {
|
|
@@ -3037,9 +3347,16 @@ var NoteInspector = class {
|
|
|
3037
3347
|
async saveRegionVisualNote(region, message, scenario) {
|
|
3038
3348
|
let screenshotDataUrl;
|
|
3039
3349
|
try {
|
|
3040
|
-
const
|
|
3041
|
-
|
|
3042
|
-
|
|
3350
|
+
const centerX = region.x + region.width / 2;
|
|
3351
|
+
const centerY = region.y + region.height / 2;
|
|
3352
|
+
const targetElement = document.elementFromPoint(centerX, centerY) || document.body;
|
|
3353
|
+
const snapPromise = this.screenshotDriver.captureElement(targetElement);
|
|
3354
|
+
const timeoutPromise = new Promise(
|
|
3355
|
+
(resolve) => setTimeout(() => resolve({ ok: false }), 800)
|
|
3356
|
+
);
|
|
3357
|
+
const snap = await Promise.race([snapPromise, timeoutPromise]);
|
|
3358
|
+
if (snap && snap.ok && snap.dataUrl) {
|
|
3359
|
+
screenshotDataUrl = snap.dataUrl;
|
|
3043
3360
|
}
|
|
3044
3361
|
} catch {
|
|
3045
3362
|
}
|
|
@@ -3136,7 +3453,11 @@ var NoteInspector = class {
|
|
|
3136
3453
|
region.width,
|
|
3137
3454
|
region.height
|
|
3138
3455
|
);
|
|
3139
|
-
|
|
3456
|
+
try {
|
|
3457
|
+
resolve(canvas.toDataURL("image/png"));
|
|
3458
|
+
} catch {
|
|
3459
|
+
resolve(dataUrl);
|
|
3460
|
+
}
|
|
3140
3461
|
};
|
|
3141
3462
|
img.onerror = () => resolve(dataUrl);
|
|
3142
3463
|
img.src = dataUrl;
|