browsertrack 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/.github/workflows/docs.yml +54 -0
  2. package/AGENTS.md +133 -0
  3. package/README.md +143 -0
  4. package/dist/chunk-6VA7GBAO.js +246 -0
  5. package/dist/chunk-6VA7GBAO.js.map +1 -0
  6. package/dist/chunk-G2Y3CXCY.js +659 -0
  7. package/dist/chunk-G2Y3CXCY.js.map +1 -0
  8. package/dist/chunk-ILRYKMME.js +1228 -0
  9. package/dist/chunk-ILRYKMME.js.map +1 -0
  10. package/dist/chunk-SKCMT2DE.js +2641 -0
  11. package/dist/chunk-SKCMT2DE.js.map +1 -0
  12. package/dist/chunk-SPCIROIU.js +616 -0
  13. package/dist/chunk-SPCIROIU.js.map +1 -0
  14. package/dist/cli/index.js +2942 -0
  15. package/dist/cli/index.js.map +1 -0
  16. package/dist/client/index.cjs +2780 -0
  17. package/dist/client/index.d.ts +211 -0
  18. package/dist/client/index.js +20 -0
  19. package/dist/client/index.js.map +1 -0
  20. package/dist/client.iife.js +630 -0
  21. package/dist/core/index.d.ts +86 -0
  22. package/dist/core/index.js +37 -0
  23. package/dist/core/index.js.map +1 -0
  24. package/dist/daemon/index.d.ts +58 -0
  25. package/dist/daemon/index.js +32 -0
  26. package/dist/daemon/index.js.map +1 -0
  27. package/dist/engine-CeT9URuN.d.ts +161 -0
  28. package/dist/index.d.ts +11 -0
  29. package/dist/index.js +56 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/mcp/index.d.ts +11 -0
  32. package/dist/mcp/index.js +12 -0
  33. package/dist/mcp/index.js.map +1 -0
  34. package/dist/notes-CBvN91Wf.d.ts +260 -0
  35. package/dist/projects-CY8ungMt.d.ts +99 -0
  36. package/dist/server-Dd8NX2Mk.d.ts +55 -0
  37. package/docboot.config.js +16 -0
  38. package/docs/cli.md +51 -0
  39. package/docs/closed-loop-verification.md +57 -0
  40. package/docs/getting-started.md +122 -0
  41. package/docs/incidents-diagnostics.md +61 -0
  42. package/docs/index.md +46 -0
  43. package/docs/mcp-reference.md +128 -0
  44. package/docs/security-privacy.md +32 -0
  45. package/docs/visual-notes.md +67 -0
  46. package/examples/test-app/index.html +299 -0
  47. package/package.json +56 -7
  48. package/packages/cli/src/index.ts +413 -0
  49. package/packages/client/package.json +28 -0
  50. package/packages/client/src/breadcrumbs.ts +30 -0
  51. package/packages/client/src/client.ts +278 -0
  52. package/packages/client/src/commands/handler.ts +301 -0
  53. package/packages/client/src/config.ts +37 -0
  54. package/packages/client/src/index.ts +50 -0
  55. package/packages/client/src/interceptors/console.ts +68 -0
  56. package/packages/client/src/interceptors/interaction.ts +119 -0
  57. package/packages/client/src/interceptors/navigation.ts +93 -0
  58. package/packages/client/src/interceptors/network.ts +165 -0
  59. package/packages/client/src/interceptors/runtime.ts +65 -0
  60. package/packages/client/src/notes/inspector.ts +1553 -0
  61. package/packages/client/src/screenshot/browser-script-driver.ts +192 -0
  62. package/packages/client/src/screenshot/driver.ts +14 -0
  63. package/packages/client/src/transport/websocket.ts +194 -0
  64. package/packages/client/tsconfig.json +8 -0
  65. package/packages/core/package.json +26 -0
  66. package/packages/core/src/fingerprint.ts +120 -0
  67. package/packages/core/src/index.ts +9 -0
  68. package/packages/core/src/redaction.ts +174 -0
  69. package/packages/core/src/selector.ts +77 -0
  70. package/packages/core/src/types/commands.ts +101 -0
  71. package/packages/core/src/types/events.ts +108 -0
  72. package/packages/core/src/types/incidents.ts +54 -0
  73. package/packages/core/src/types/notes.ts +106 -0
  74. package/packages/core/src/types/probes.ts +44 -0
  75. package/packages/core/src/types/projects.ts +20 -0
  76. package/packages/core/tsconfig.json +8 -0
  77. package/packages/daemon/src/config.ts +29 -0
  78. package/packages/daemon/src/incidents/engine.ts +211 -0
  79. package/packages/daemon/src/index.ts +18 -0
  80. package/packages/daemon/src/notes/engine.ts +92 -0
  81. package/packages/daemon/src/notes/verification.ts +191 -0
  82. package/packages/daemon/src/server/daemon.ts +112 -0
  83. package/packages/daemon/src/server/http.ts +165 -0
  84. package/packages/daemon/src/server/ws.ts +246 -0
  85. package/packages/daemon/src/session/manager.ts +147 -0
  86. package/packages/daemon/src/storage/db.ts +742 -0
  87. package/packages/daemon/src/storage/screenshot-store.ts +49 -0
  88. package/packages/daemon/src/verification/engine.ts +264 -0
  89. package/packages/mcp/src/handlers.ts +398 -0
  90. package/packages/mcp/src/index.ts +3 -0
  91. package/packages/mcp/src/server.ts +86 -0
  92. package/packages/mcp/src/tools.ts +236 -0
  93. package/src/index.ts +4 -0
  94. package/test/client/interceptors.test.ts +130 -0
  95. package/test/core/fingerprint.test.ts +53 -0
  96. package/test/core/notes.test.ts +66 -0
  97. package/test/core/redaction.test.ts +60 -0
  98. package/test/daemon/incident-engine.test.ts +98 -0
  99. package/test/daemon/notes-storage.test.ts +130 -0
  100. package/test/daemon/notes-verification.test.ts +135 -0
  101. package/test/daemon/storage.test.ts +123 -0
  102. package/test/daemon/verification-engine.test.ts +88 -0
  103. package/test/e2e/daemon-mcp-e2e.test.ts +153 -0
  104. package/test/e2e/visual-notes-e2e.test.ts +205 -0
  105. package/test/mcp/handlers.test.ts +116 -0
  106. package/test/mcp/notes.test.ts +107 -0
  107. package/tsconfig.base.json +17 -0
  108. package/tsconfig.json +26 -0
  109. package/tsup.config.ts +54 -0
  110. package/vitest.config.ts +9 -0
@@ -0,0 +1,2641 @@
1
+ import {
2
+ getSemanticSelector,
3
+ redactUrl,
4
+ truncate
5
+ } from "./chunk-6VA7GBAO.js";
6
+
7
+ // packages/client/src/breadcrumbs.ts
8
+ var BreadcrumbBuffer = class {
9
+ capacity;
10
+ buffer = [];
11
+ constructor(capacity = 50) {
12
+ this.capacity = capacity;
13
+ }
14
+ add(breadcrumb) {
15
+ const item = {
16
+ ...breadcrumb,
17
+ timestamp: breadcrumb.timestamp || Date.now()
18
+ };
19
+ this.buffer.push(item);
20
+ if (this.buffer.length > this.capacity) {
21
+ this.buffer.shift();
22
+ }
23
+ }
24
+ getRecent() {
25
+ return [...this.buffer];
26
+ }
27
+ clear() {
28
+ this.buffer = [];
29
+ }
30
+ };
31
+
32
+ // packages/client/src/interceptors/interaction.ts
33
+ function extractElementSummary(el) {
34
+ const selector = getSemanticSelector(el);
35
+ const tag = el.tagName.toLowerCase();
36
+ const id = el.id || void 0;
37
+ const classes = Array.from(el.classList || []);
38
+ let boundingRect = void 0;
39
+ let visible = true;
40
+ try {
41
+ const rect = el.getBoundingClientRect();
42
+ boundingRect = {
43
+ x: Math.round(rect.x),
44
+ y: Math.round(rect.y),
45
+ width: Math.round(rect.width),
46
+ height: Math.round(rect.height),
47
+ top: Math.round(rect.top),
48
+ left: Math.round(rect.left),
49
+ bottom: Math.round(rect.bottom),
50
+ right: Math.round(rect.right)
51
+ };
52
+ const style = window.getComputedStyle(el);
53
+ visible = style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0" && rect.width > 0 && rect.height > 0;
54
+ } catch {
55
+ }
56
+ let innerText;
57
+ if (tag !== "input" && tag !== "textarea" && tag !== "select") {
58
+ innerText = truncate(el.textContent?.trim(), 80);
59
+ }
60
+ const outerHTML = truncate(el.outerHTML?.trim(), 200);
61
+ return {
62
+ selector,
63
+ tag,
64
+ id,
65
+ classes,
66
+ boundingRect,
67
+ visible,
68
+ innerText,
69
+ outerHTML
70
+ };
71
+ }
72
+ function setupInteractionInterceptors(onInteraction) {
73
+ if (typeof document === "undefined") return () => {
74
+ };
75
+ const onClick = (event) => {
76
+ try {
77
+ const target = event.target;
78
+ if (!target || !(target instanceof Element)) return;
79
+ const summary = extractElementSummary(target);
80
+ const message = `click ${summary.selector}${summary.innerText ? ` ("${summary.innerText}")` : ""}`;
81
+ onInteraction(
82
+ {
83
+ type: "click",
84
+ category: "ui",
85
+ message,
86
+ timestamp: Date.now(),
87
+ element: summary
88
+ },
89
+ summary
90
+ );
91
+ } catch {
92
+ }
93
+ };
94
+ const onSubmit = (event) => {
95
+ try {
96
+ const target = event.target;
97
+ if (!target || !(target instanceof Element)) return;
98
+ const summary = extractElementSummary(target);
99
+ const formAction = target.getAttribute("action") || "";
100
+ const message = `submit form ${summary.selector}${formAction ? ` (action: ${formAction})` : ""}`;
101
+ onInteraction(
102
+ {
103
+ type: "submit",
104
+ category: "ui",
105
+ message,
106
+ timestamp: Date.now(),
107
+ element: summary
108
+ },
109
+ summary
110
+ );
111
+ } catch {
112
+ }
113
+ };
114
+ document.addEventListener("click", onClick, { capture: true, passive: true });
115
+ document.addEventListener("submit", onSubmit, { capture: true, passive: true });
116
+ return () => {
117
+ document.removeEventListener("click", onClick, { capture: true });
118
+ document.removeEventListener("submit", onSubmit, { capture: true });
119
+ };
120
+ }
121
+
122
+ // packages/client/src/commands/handler.ts
123
+ var ClientCommandHandler = class {
124
+ screenshotDriver;
125
+ constructor(screenshotDriver) {
126
+ this.screenshotDriver = screenshotDriver;
127
+ }
128
+ async executeCommand(command) {
129
+ try {
130
+ switch (command.type) {
131
+ case "reload":
132
+ return this.handleReload(command);
133
+ case "navigate":
134
+ return this.handleNavigate(command);
135
+ case "get_page_state":
136
+ return this.handleGetPageState(command);
137
+ case "query_element":
138
+ return this.handleQueryElement(command);
139
+ case "capture_element":
140
+ return await this.handleCaptureElement(command);
141
+ case "check_overflow":
142
+ return this.handleCheckOverflow(command);
143
+ case "get_element_rect":
144
+ return this.handleGetElementRect(command);
145
+ case "get_element_style":
146
+ return this.handleGetElementStyle(command);
147
+ default:
148
+ return {
149
+ id: command.id,
150
+ ok: false,
151
+ error: `Unsupported command type: ${command.type}`
152
+ };
153
+ }
154
+ } catch (err) {
155
+ return {
156
+ id: command.id,
157
+ ok: false,
158
+ error: err?.message || "Command execution failed"
159
+ };
160
+ }
161
+ }
162
+ handleReload(command) {
163
+ if (typeof window !== "undefined" && window.location) {
164
+ setTimeout(() => {
165
+ window.location.reload();
166
+ }, 50);
167
+ return { id: command.id, ok: true, result: { message: "Reload initiated" } };
168
+ }
169
+ return { id: command.id, ok: false, error: "No browser window available" };
170
+ }
171
+ handleNavigate(command) {
172
+ const targetUrl = command.params?.url;
173
+ if (!targetUrl) {
174
+ return { id: command.id, ok: false, error: "Missing target url parameter" };
175
+ }
176
+ if (typeof window !== "undefined" && window.location) {
177
+ setTimeout(() => {
178
+ window.location.href = targetUrl;
179
+ }, 50);
180
+ return { id: command.id, ok: true, result: { message: `Navigating to ${targetUrl}` } };
181
+ }
182
+ return { id: command.id, ok: false, error: "No browser window available" };
183
+ }
184
+ handleGetPageState(command) {
185
+ if (typeof window === "undefined" || typeof document === "undefined") {
186
+ return { id: command.id, ok: false, error: "No browser document available" };
187
+ }
188
+ const activeEl = document.activeElement && document.activeElement !== document.body ? extractElementSummary(document.activeElement) : void 0;
189
+ return {
190
+ id: command.id,
191
+ ok: true,
192
+ result: {
193
+ url: window.location.href,
194
+ route: window.location.pathname + window.location.search + window.location.hash,
195
+ title: document.title,
196
+ readyState: document.readyState,
197
+ activeElement: activeEl
198
+ }
199
+ };
200
+ }
201
+ handleQueryElement(command) {
202
+ if (typeof document === "undefined") {
203
+ return { id: command.id, ok: false, error: "No browser document available" };
204
+ }
205
+ const selector = command.params?.selector;
206
+ if (!selector) {
207
+ return { id: command.id, ok: false, error: "Missing selector parameter" };
208
+ }
209
+ const element = document.querySelector(selector);
210
+ if (!element) {
211
+ return {
212
+ id: command.id,
213
+ ok: true,
214
+ result: { exists: false }
215
+ };
216
+ }
217
+ const summary = extractElementSummary(element);
218
+ return {
219
+ id: command.id,
220
+ ok: true,
221
+ result: {
222
+ exists: true,
223
+ visible: summary.visible,
224
+ tag: summary.tag,
225
+ id: summary.id,
226
+ classes: summary.classes,
227
+ boundingRect: summary.boundingRect,
228
+ innerText: summary.innerText,
229
+ outerHTML: summary.outerHTML
230
+ }
231
+ };
232
+ }
233
+ async handleCaptureElement(command) {
234
+ const selector = command.params?.selector;
235
+ let targetEl = null;
236
+ if (selector) {
237
+ targetEl = document.querySelector(selector);
238
+ if (!targetEl) {
239
+ return {
240
+ id: command.id,
241
+ ok: false,
242
+ reason: `ELEMENT_NOT_FOUND: ${selector}`,
243
+ error: `Element not found for selector: ${selector}`
244
+ };
245
+ }
246
+ } else {
247
+ targetEl = document.body || document.documentElement;
248
+ }
249
+ const captureRes = await this.screenshotDriver.captureElement(targetEl);
250
+ if (!captureRes.ok) {
251
+ return {
252
+ id: command.id,
253
+ ok: false,
254
+ reason: captureRes.reason || "CAPTURE_FAILED",
255
+ error: captureRes.reason || "Failed to capture element screenshot"
256
+ };
257
+ }
258
+ return {
259
+ id: command.id,
260
+ ok: true,
261
+ result: {
262
+ dataUrl: captureRes.dataUrl,
263
+ format: captureRes.format,
264
+ width: captureRes.width,
265
+ height: captureRes.height
266
+ }
267
+ };
268
+ }
269
+ handleCheckOverflow(command) {
270
+ if (typeof window === "undefined" || typeof document === "undefined") {
271
+ return { id: command.id, ok: false, error: "No browser window available" };
272
+ }
273
+ const selector = command.params?.selector;
274
+ if (!selector) {
275
+ return { id: command.id, ok: false, error: "Missing selector parameter" };
276
+ }
277
+ const el = document.querySelector(selector);
278
+ if (!el) {
279
+ return { id: command.id, ok: false, error: `Element not found: ${selector}` };
280
+ }
281
+ const rect = el.getBoundingClientRect();
282
+ const vw = window.innerWidth;
283
+ const vh = window.innerHeight;
284
+ const overflowRightPx = Math.max(0, Math.round(rect.right - vw));
285
+ const overflowBottomPx = Math.max(0, Math.round(rect.bottom - vh));
286
+ const isViewportOverflow = overflowRightPx > 0 || rect.left < 0;
287
+ let parentOverflow = false;
288
+ if (el.parentElement) {
289
+ parentOverflow = el.parentElement.scrollWidth > el.parentElement.clientWidth;
290
+ }
291
+ return {
292
+ id: command.id,
293
+ ok: true,
294
+ result: {
295
+ selector,
296
+ overflow: isViewportOverflow || parentOverflow,
297
+ viewportWidth: vw,
298
+ viewportHeight: vh,
299
+ rect: {
300
+ x: Math.round(rect.x),
301
+ y: Math.round(rect.y),
302
+ width: Math.round(rect.width),
303
+ height: Math.round(rect.height),
304
+ top: Math.round(rect.top),
305
+ left: Math.round(rect.left),
306
+ bottom: Math.round(rect.bottom),
307
+ right: Math.round(rect.right)
308
+ },
309
+ overflowRightPx,
310
+ overflowBottomPx,
311
+ parentOverflow
312
+ }
313
+ };
314
+ }
315
+ handleGetElementRect(command) {
316
+ if (typeof document === "undefined") {
317
+ return { id: command.id, ok: false, error: "No browser document available" };
318
+ }
319
+ const selector = command.params?.selector;
320
+ if (!selector) {
321
+ return { id: command.id, ok: false, error: "Missing selector parameter" };
322
+ }
323
+ const el = document.querySelector(selector);
324
+ if (!el) {
325
+ return { id: command.id, ok: false, error: `Element not found: ${selector}` };
326
+ }
327
+ const rect = el.getBoundingClientRect();
328
+ return {
329
+ id: command.id,
330
+ ok: true,
331
+ result: {
332
+ x: Math.round(rect.x),
333
+ y: Math.round(rect.y),
334
+ width: Math.round(rect.width),
335
+ height: Math.round(rect.height),
336
+ top: Math.round(rect.top),
337
+ left: Math.round(rect.left),
338
+ bottom: Math.round(rect.bottom),
339
+ right: Math.round(rect.right)
340
+ }
341
+ };
342
+ }
343
+ handleGetElementStyle(command) {
344
+ if (typeof window === "undefined" || typeof document === "undefined") {
345
+ return { id: command.id, ok: false, error: "No browser window available" };
346
+ }
347
+ const selector = command.params?.selector;
348
+ if (!selector) {
349
+ return { id: command.id, ok: false, error: "Missing selector parameter" };
350
+ }
351
+ const el = document.querySelector(selector);
352
+ if (!el) {
353
+ return { id: command.id, ok: false, error: `Element not found: ${selector}` };
354
+ }
355
+ const computed = window.getComputedStyle(el);
356
+ const properties = command.params?.properties || ["overflow", "overflowX", "overflowY", "width", "maxWidth", "display", "position"];
357
+ const styles = {};
358
+ for (const prop of properties) {
359
+ styles[prop] = computed.getPropertyValue(prop) || computed[prop] || "";
360
+ }
361
+ return {
362
+ id: command.id,
363
+ ok: true,
364
+ result: {
365
+ selector,
366
+ styles
367
+ }
368
+ };
369
+ }
370
+ };
371
+
372
+ // packages/client/src/config.ts
373
+ var DEFAULT_OPTIONS = {
374
+ daemonUrl: "ws://127.0.0.1:7331",
375
+ projectId: "",
376
+ maxBreadcrumbs: 50,
377
+ captureErrors: true,
378
+ captureConsole: true,
379
+ captureNetwork: true,
380
+ captureNavigation: true,
381
+ captureInteractions: true,
382
+ onErrorScreenshot: true,
383
+ notes: {
384
+ enabled: true,
385
+ shortcut: "Alt+Click",
386
+ showBadges: true,
387
+ maskSelectors: ['input[type="password"]', "[data-sensitive]"]
388
+ },
389
+ debug: false
390
+ };
391
+
392
+ // packages/client/src/interceptors/console.ts
393
+ function setupConsoleInterceptors(onConsole) {
394
+ if (typeof console === "undefined") return () => {
395
+ };
396
+ const originalError = console.error;
397
+ const originalWarn = console.warn;
398
+ function formatArgs(args) {
399
+ return args.map((arg) => {
400
+ if (arg instanceof Error) {
401
+ return arg.stack || `${arg.name}: ${arg.message}`;
402
+ }
403
+ if (typeof arg === "object" && arg !== null) {
404
+ try {
405
+ return JSON.stringify(arg);
406
+ } catch {
407
+ return String(arg);
408
+ }
409
+ }
410
+ return String(arg);
411
+ }).join(" ");
412
+ }
413
+ console.error = function(...args) {
414
+ try {
415
+ const message = formatArgs(args);
416
+ const stack = new Error().stack;
417
+ onConsole({
418
+ level: "error",
419
+ message,
420
+ stack,
421
+ timestamp: Date.now()
422
+ });
423
+ } catch {
424
+ }
425
+ return originalError.apply(console, args);
426
+ };
427
+ console.warn = function(...args) {
428
+ try {
429
+ const message = formatArgs(args);
430
+ const stack = new Error().stack;
431
+ onConsole({
432
+ level: "warn",
433
+ message,
434
+ stack,
435
+ timestamp: Date.now()
436
+ });
437
+ } catch {
438
+ }
439
+ return originalWarn.apply(console, args);
440
+ };
441
+ return () => {
442
+ console.error = originalError;
443
+ console.warn = originalWarn;
444
+ };
445
+ }
446
+
447
+ // packages/client/src/interceptors/navigation.ts
448
+ function setupNavigationInterceptors(onNavigation) {
449
+ if (typeof window === "undefined" || typeof history === "undefined") return () => {
450
+ };
451
+ let currentUrl = redactUrl(window.location.href);
452
+ onNavigation({
453
+ to: currentUrl,
454
+ type: "initial",
455
+ timestamp: Date.now()
456
+ });
457
+ const originalPushState = history.pushState;
458
+ const originalReplaceState = history.replaceState;
459
+ history.pushState = function(data, unused, url) {
460
+ const from = currentUrl;
461
+ const result = originalPushState.apply(history, [data, unused, url]);
462
+ try {
463
+ const to = redactUrl(window.location.href);
464
+ currentUrl = to;
465
+ onNavigation({
466
+ from,
467
+ to,
468
+ type: "pushState",
469
+ timestamp: Date.now()
470
+ });
471
+ } catch {
472
+ }
473
+ return result;
474
+ };
475
+ history.replaceState = function(data, unused, url) {
476
+ const from = currentUrl;
477
+ const result = originalReplaceState.apply(history, [data, unused, url]);
478
+ try {
479
+ const to = redactUrl(window.location.href);
480
+ currentUrl = to;
481
+ onNavigation({
482
+ from,
483
+ to,
484
+ type: "replaceState",
485
+ timestamp: Date.now()
486
+ });
487
+ } catch {
488
+ }
489
+ return result;
490
+ };
491
+ const onPopState = () => {
492
+ const from = currentUrl;
493
+ const to = redactUrl(window.location.href);
494
+ currentUrl = to;
495
+ onNavigation({
496
+ from,
497
+ to,
498
+ type: "popstate",
499
+ timestamp: Date.now()
500
+ });
501
+ };
502
+ const onHashChange = () => {
503
+ const from = currentUrl;
504
+ const to = redactUrl(window.location.href);
505
+ currentUrl = to;
506
+ onNavigation({
507
+ from,
508
+ to,
509
+ type: "hashchange",
510
+ timestamp: Date.now()
511
+ });
512
+ };
513
+ window.addEventListener("popstate", onPopState);
514
+ window.addEventListener("hashchange", onHashChange);
515
+ return () => {
516
+ history.pushState = originalPushState;
517
+ history.replaceState = originalReplaceState;
518
+ window.removeEventListener("popstate", onPopState);
519
+ window.removeEventListener("hashchange", onHashChange);
520
+ };
521
+ }
522
+
523
+ // packages/client/src/interceptors/network.ts
524
+ function setupNetworkInterceptors(onNetwork) {
525
+ const cleanups = [];
526
+ if (typeof window !== "undefined" && typeof window.fetch === "function") {
527
+ const originalFetch = window.fetch;
528
+ window.fetch = async function(input, init2) {
529
+ const startTime = Date.now();
530
+ let urlStr = "";
531
+ let method = "GET";
532
+ try {
533
+ if (typeof input === "string") {
534
+ urlStr = input;
535
+ } else if (input instanceof URL) {
536
+ urlStr = input.toString();
537
+ } else if (input && typeof input === "object" && "url" in input) {
538
+ urlStr = input.url;
539
+ method = input.method || "GET";
540
+ }
541
+ if (init2 && init2.method) {
542
+ method = init2.method.toUpperCase();
543
+ }
544
+ } catch {
545
+ urlStr = "unknown_url";
546
+ }
547
+ const safeUrl = redactUrl(urlStr);
548
+ try {
549
+ const response = await originalFetch.apply(window, [input, init2]);
550
+ const durationMs = Date.now() - startTime;
551
+ onNetwork({
552
+ url: safeUrl,
553
+ method,
554
+ status: response.status,
555
+ statusText: response.statusText,
556
+ durationMs,
557
+ timestamp: startTime
558
+ });
559
+ return response;
560
+ } catch (err) {
561
+ const durationMs = Date.now() - startTime;
562
+ const isAbort = err?.name === "AbortError";
563
+ onNetwork({
564
+ url: safeUrl,
565
+ method,
566
+ durationMs,
567
+ error: err?.message || "Network request failed",
568
+ aborted: isAbort,
569
+ timestamp: startTime
570
+ });
571
+ throw err;
572
+ }
573
+ };
574
+ cleanups.push(() => {
575
+ window.fetch = originalFetch;
576
+ });
577
+ }
578
+ if (typeof window !== "undefined" && typeof window.XMLHttpRequest === "function") {
579
+ const originalOpen = XMLHttpRequest.prototype.open;
580
+ const originalSend = XMLHttpRequest.prototype.send;
581
+ const originalAbort = XMLHttpRequest.prototype.abort;
582
+ const xhrMap = /* @__PURE__ */ new WeakMap();
583
+ XMLHttpRequest.prototype.open = function(...args) {
584
+ try {
585
+ const method = String(args[0] || "GET").toUpperCase();
586
+ const rawUrl = String(args[1] || "");
587
+ xhrMap.set(this, {
588
+ url: redactUrl(rawUrl),
589
+ method,
590
+ startTime: Date.now()
591
+ });
592
+ } catch {
593
+ }
594
+ return originalOpen.apply(this, args);
595
+ };
596
+ XMLHttpRequest.prototype.abort = function() {
597
+ const meta = xhrMap.get(this);
598
+ if (meta) {
599
+ meta.aborted = true;
600
+ }
601
+ return originalAbort.apply(this);
602
+ };
603
+ XMLHttpRequest.prototype.send = function(body) {
604
+ const meta = xhrMap.get(this) || { url: "unknown_url", method: "GET", startTime: Date.now() };
605
+ const onComplete = () => {
606
+ try {
607
+ const durationMs = Date.now() - meta.startTime;
608
+ onNetwork({
609
+ url: meta.url,
610
+ method: meta.method,
611
+ status: this.status,
612
+ statusText: this.statusText,
613
+ durationMs,
614
+ aborted: meta.aborted,
615
+ timestamp: meta.startTime
616
+ });
617
+ } catch {
618
+ }
619
+ };
620
+ const onError = () => {
621
+ try {
622
+ const durationMs = Date.now() - meta.startTime;
623
+ onNetwork({
624
+ url: meta.url,
625
+ method: meta.method,
626
+ status: this.status || 0,
627
+ durationMs,
628
+ error: "XHR Network Error",
629
+ aborted: meta.aborted,
630
+ timestamp: meta.startTime
631
+ });
632
+ } catch {
633
+ }
634
+ };
635
+ this.addEventListener("load", onComplete, { once: true });
636
+ this.addEventListener("error", onError, { once: true });
637
+ this.addEventListener("timeout", onError, { once: true });
638
+ return originalSend.apply(this, [body]);
639
+ };
640
+ cleanups.push(() => {
641
+ XMLHttpRequest.prototype.open = originalOpen;
642
+ XMLHttpRequest.prototype.send = originalSend;
643
+ XMLHttpRequest.prototype.abort = originalAbort;
644
+ });
645
+ }
646
+ return () => {
647
+ for (const cleanup of cleanups) {
648
+ try {
649
+ cleanup();
650
+ } catch {
651
+ }
652
+ }
653
+ };
654
+ }
655
+
656
+ // packages/client/src/interceptors/runtime.ts
657
+ function setupRuntimeInterceptors(onRuntimeError) {
658
+ if (typeof window === "undefined") return () => {
659
+ };
660
+ const errorHandler = (event) => {
661
+ try {
662
+ const errorEvent = {
663
+ message: event.message || event.error && event.error.message || "Script error",
664
+ stack: event.error && event.error.stack || void 0,
665
+ filename: event.filename || void 0,
666
+ lineno: event.lineno || void 0,
667
+ colno: event.colno || void 0,
668
+ errorType: event.error && event.error.name || "Error",
669
+ timestamp: Date.now()
670
+ };
671
+ onRuntimeError(errorEvent, event.error);
672
+ } catch {
673
+ }
674
+ };
675
+ const unhandledRejectionHandler = (event) => {
676
+ try {
677
+ const reason = event.reason;
678
+ let message = "Unhandled Promise Rejection";
679
+ let stack;
680
+ let errorType = "UnhandledRejection";
681
+ if (reason instanceof Error) {
682
+ message = reason.message;
683
+ stack = reason.stack;
684
+ errorType = reason.name || "UnhandledRejection";
685
+ } else if (typeof reason === "string") {
686
+ message = reason;
687
+ } else if (reason && typeof reason === "object") {
688
+ try {
689
+ message = JSON.stringify(reason);
690
+ } catch {
691
+ message = String(reason);
692
+ }
693
+ }
694
+ const errorEvent = {
695
+ message,
696
+ stack,
697
+ errorType,
698
+ timestamp: Date.now()
699
+ };
700
+ onRuntimeError(errorEvent, reason);
701
+ } catch {
702
+ }
703
+ };
704
+ window.addEventListener("error", errorHandler);
705
+ window.addEventListener("unhandledrejection", unhandledRejectionHandler);
706
+ return () => {
707
+ window.removeEventListener("error", errorHandler);
708
+ window.removeEventListener("unhandledrejection", unhandledRejectionHandler);
709
+ };
710
+ }
711
+
712
+ // packages/client/src/screenshot/browser-script-driver.ts
713
+ import { snapdom } from "@zumer/snapdom";
714
+ var BrowserScriptScreenshotDriver = class {
715
+ name = "BrowserScriptScreenshotDriver";
716
+ async captureSelector(selector) {
717
+ try {
718
+ if (typeof document === "undefined") {
719
+ return { ok: false, reason: "NO_DOCUMENT_ENVIRONMENT" };
720
+ }
721
+ const el = document.querySelector(selector);
722
+ if (!el) {
723
+ return { ok: false, reason: `ELEMENT_NOT_FOUND: ${selector}` };
724
+ }
725
+ return await this.captureElement(el);
726
+ } catch (err) {
727
+ return { ok: false, reason: err?.message || "UNKNOWN_CAPTURE_ERROR" };
728
+ }
729
+ }
730
+ async captureElement(element) {
731
+ try {
732
+ if (typeof window === "undefined" || typeof document === "undefined") {
733
+ return { ok: false, reason: "NO_BROWSER_ENVIRONMENT" };
734
+ }
735
+ const rect = element.getBoundingClientRect();
736
+ const width = Math.max(Math.round(rect.width), 10);
737
+ const height = Math.max(Math.round(rect.height), 10);
738
+ const capturePromise = (async () => {
739
+ try {
740
+ let imgElement = void 0;
741
+ let format = "webp";
742
+ try {
743
+ if (typeof snapdom === "function") {
744
+ const captureRes = await snapdom(element, { scale: 1, embedFonts: false });
745
+ if (captureRes && typeof captureRes.toWebp === "function") {
746
+ imgElement = await captureRes.toWebp();
747
+ } else if (captureRes && typeof captureRes.toPng === "function") {
748
+ imgElement = await captureRes.toPng();
749
+ format = "png";
750
+ } else if (captureRes?.url) {
751
+ return {
752
+ ok: true,
753
+ dataUrl: captureRes.url,
754
+ format: "webp",
755
+ width,
756
+ height
757
+ };
758
+ }
759
+ } else if (snapdom?.toWebp) {
760
+ imgElement = await snapdom.toWebp(element, {
761
+ scale: 1,
762
+ embedFonts: false
763
+ });
764
+ } else if (snapdom?.toPng) {
765
+ imgElement = await snapdom.toPng(element, {
766
+ scale: 1,
767
+ embedFonts: false
768
+ });
769
+ format = "png";
770
+ }
771
+ } catch {
772
+ }
773
+ if (imgElement && imgElement.src) {
774
+ return {
775
+ ok: true,
776
+ dataUrl: imgElement.src,
777
+ format: imgElement.src.startsWith("data:image/webp") ? "webp" : format,
778
+ width: imgElement.naturalWidth || width,
779
+ height: imgElement.naturalHeight || height
780
+ };
781
+ }
782
+ } catch (snapErr) {
783
+ const errMsg = String(snapErr?.message || snapErr);
784
+ if (errMsg.includes("CORS") || errMsg.includes("SecurityError") || errMsg.includes("tainted")) {
785
+ return { ok: false, reason: "CROSS_ORIGIN_RESOURCE" };
786
+ }
787
+ }
788
+ return await this.captureWithSvgFallback(element, width, height);
789
+ })();
790
+ const timeoutPromise = new Promise((resolve) => {
791
+ setTimeout(() => resolve({ ok: false, reason: "CAPTURE_TIMEOUT" }), 3e3);
792
+ });
793
+ return await Promise.race([capturePromise, timeoutPromise]);
794
+ } catch (err) {
795
+ return { ok: false, reason: err?.message || "CAPTURE_EXCEPTION" };
796
+ }
797
+ }
798
+ async captureWithSvgFallback(element, width, height) {
799
+ try {
800
+ const clone = element.cloneNode(true);
801
+ const computed = window.getComputedStyle(element);
802
+ let cssText = "";
803
+ for (let i = 0; i < computed.length; i++) {
804
+ const prop = computed[i];
805
+ cssText += `${prop}:${computed.getPropertyValue(prop)};`;
806
+ }
807
+ clone.style.cssText = cssText;
808
+ clone.style.margin = "0";
809
+ clone.style.position = "static";
810
+ const svg = `
811
+ <svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
812
+ <foreignObject width="100%" height="100%">
813
+ <div xmlns="http://www.w3.org/1999/xhtml" style="width:${width}px;height:${height}px;">
814
+ ${clone.outerHTML}
815
+ </div>
816
+ </foreignObject>
817
+ </svg>
818
+ `.trim();
819
+ const svgBlob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
820
+ const url = URL.createObjectURL(svgBlob);
821
+ return await new Promise((resolve) => {
822
+ const img = new Image();
823
+ img.onload = () => {
824
+ try {
825
+ const canvas = document.createElement("canvas");
826
+ canvas.width = width;
827
+ canvas.height = height;
828
+ const ctx = canvas.getContext("2d");
829
+ if (!ctx) {
830
+ URL.revokeObjectURL(url);
831
+ resolve({ ok: false, reason: "CANVAS_CONTEXT_UNAVAILABLE" });
832
+ return;
833
+ }
834
+ ctx.drawImage(img, 0, 0);
835
+ URL.revokeObjectURL(url);
836
+ let dataUrl;
837
+ try {
838
+ dataUrl = canvas.toDataURL("image/webp", 0.85);
839
+ } catch {
840
+ dataUrl = canvas.toDataURL("image/png");
841
+ }
842
+ resolve({
843
+ ok: true,
844
+ dataUrl,
845
+ format: dataUrl.startsWith("data:image/webp") ? "webp" : "png",
846
+ width,
847
+ height
848
+ });
849
+ } catch (e) {
850
+ URL.revokeObjectURL(url);
851
+ if (e?.name === "SecurityError" || String(e).includes("tainted")) {
852
+ resolve({ ok: false, reason: "CROSS_ORIGIN_RESOURCE" });
853
+ } else {
854
+ resolve({ ok: false, reason: e?.message || "CANVAS_EXPORT_FAILED" });
855
+ }
856
+ }
857
+ };
858
+ img.onerror = () => {
859
+ URL.revokeObjectURL(url);
860
+ resolve({ ok: false, reason: "IMAGE_LOAD_FAILED" });
861
+ };
862
+ img.src = url;
863
+ });
864
+ } catch (e) {
865
+ return { ok: false, reason: e?.message || "FALLBACK_CAPTURE_FAILED" };
866
+ }
867
+ }
868
+ };
869
+
870
+ // packages/client/src/transport/websocket.ts
871
+ var WebSocketTransport = class {
872
+ wsUrl;
873
+ projectId;
874
+ debug;
875
+ ws = null;
876
+ sessionId = null;
877
+ queue = [];
878
+ maxQueueSize = 50;
879
+ reconnectAttempts = 0;
880
+ maxReconnectDelay = 1e4;
881
+ reconnectTimer = null;
882
+ commandHandler;
883
+ messageListeners = [];
884
+ isDestroyed = false;
885
+ constructor(options) {
886
+ this.wsUrl = options.url || "ws://127.0.0.1:7331";
887
+ this.projectId = options.projectId;
888
+ this.debug = !!options.debug;
889
+ }
890
+ setCommandHandler(handler) {
891
+ this.commandHandler = handler;
892
+ }
893
+ onMessage(listener) {
894
+ this.messageListeners.push(listener);
895
+ return () => {
896
+ this.messageListeners = this.messageListeners.filter((l) => l !== listener);
897
+ };
898
+ }
899
+ getSessionId() {
900
+ return this.sessionId;
901
+ }
902
+ isConnected() {
903
+ return this.ws !== null && this.ws.readyState === (typeof WebSocket !== "undefined" ? WebSocket.OPEN : 1);
904
+ }
905
+ connect() {
906
+ if (this.isDestroyed || typeof WebSocket === "undefined") return;
907
+ if (this.ws && (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN)) {
908
+ return;
909
+ }
910
+ try {
911
+ this.ws = new WebSocket(this.wsUrl);
912
+ this.ws.onopen = () => {
913
+ this.reconnectAttempts = 0;
914
+ this.sendHello();
915
+ this.flushQueue();
916
+ };
917
+ this.ws.onmessage = async (event) => {
918
+ try {
919
+ const raw = typeof event.data === "string" ? event.data : "";
920
+ if (!raw) return;
921
+ const msg = JSON.parse(raw);
922
+ for (const listener of this.messageListeners) {
923
+ try {
924
+ listener(msg);
925
+ } catch {
926
+ }
927
+ }
928
+ if (msg.type === "hello_ack") {
929
+ this.sessionId = msg.sessionId;
930
+ if (this.debug) {
931
+ console.log("[BrowserTrack] Connected, session ID:", this.sessionId);
932
+ }
933
+ return;
934
+ }
935
+ if (msg.type === "command" && this.commandHandler) {
936
+ const command = msg.command;
937
+ const res = await this.commandHandler.executeCommand(command);
938
+ this.send({
939
+ type: "command_response",
940
+ sessionId: this.sessionId,
941
+ response: res
942
+ });
943
+ }
944
+ } catch {
945
+ }
946
+ };
947
+ this.ws.onerror = () => {
948
+ };
949
+ this.ws.onclose = () => {
950
+ this.ws = null;
951
+ this.scheduleReconnect();
952
+ };
953
+ } catch {
954
+ this.scheduleReconnect();
955
+ }
956
+ }
957
+ send(payload) {
958
+ const raw = JSON.stringify(payload);
959
+ if (this.isConnected()) {
960
+ try {
961
+ this.ws.send(raw);
962
+ } catch {
963
+ this.enqueue(raw);
964
+ }
965
+ } else {
966
+ this.enqueue(raw);
967
+ }
968
+ }
969
+ enqueue(raw) {
970
+ if (this.queue.length >= this.maxQueueSize) {
971
+ this.queue.shift();
972
+ }
973
+ this.queue.push(raw);
974
+ }
975
+ flushQueue() {
976
+ if (!this.isConnected()) return;
977
+ while (this.queue.length > 0) {
978
+ const item = this.queue.shift();
979
+ if (item) {
980
+ try {
981
+ this.ws.send(item);
982
+ } catch {
983
+ this.queue.unshift(item);
984
+ break;
985
+ }
986
+ }
987
+ }
988
+ }
989
+ sendHello() {
990
+ if (typeof window === "undefined") return;
991
+ const hello = {
992
+ type: "hello",
993
+ origin: window.location.origin,
994
+ url: window.location.href,
995
+ title: document.title,
996
+ userAgent: navigator.userAgent,
997
+ timestamp: Date.now(),
998
+ projectId: this.projectId
999
+ };
1000
+ try {
1001
+ this.ws.send(JSON.stringify(hello));
1002
+ } catch {
1003
+ }
1004
+ }
1005
+ scheduleReconnect() {
1006
+ if (this.isDestroyed || this.reconnectTimer) return;
1007
+ this.reconnectAttempts++;
1008
+ const delay = Math.min(1e3 * Math.pow(1.5, this.reconnectAttempts), this.maxReconnectDelay);
1009
+ this.reconnectTimer = setTimeout(() => {
1010
+ this.reconnectTimer = null;
1011
+ this.connect();
1012
+ }, delay);
1013
+ }
1014
+ destroy() {
1015
+ this.isDestroyed = true;
1016
+ if (this.reconnectTimer) {
1017
+ clearTimeout(this.reconnectTimer);
1018
+ this.reconnectTimer = null;
1019
+ }
1020
+ if (this.ws) {
1021
+ try {
1022
+ this.ws.close();
1023
+ } catch {
1024
+ }
1025
+ this.ws = null;
1026
+ }
1027
+ this.queue = [];
1028
+ }
1029
+ };
1030
+
1031
+ // packages/client/src/notes/inspector.ts
1032
+ var NoteInspector = class {
1033
+ transport;
1034
+ screenshotDriver;
1035
+ options;
1036
+ container = null;
1037
+ shadowRoot = null;
1038
+ highlightOverlay = null;
1039
+ regionOverlay = null;
1040
+ regionBox = null;
1041
+ regionBanner = null;
1042
+ markersContainer = null;
1043
+ toolbarElement = null;
1044
+ modalOverlay = null;
1045
+ cardOverlay = null;
1046
+ activeMode = "idle";
1047
+ hoveredElement = null;
1048
+ selectedElement = null;
1049
+ selectedRegion = null;
1050
+ isDraggingRegion = false;
1051
+ dragStartX = 0;
1052
+ dragStartY = 0;
1053
+ savedNotes = [];
1054
+ showMarkers = true;
1055
+ cleanups = [];
1056
+ constructor(transport, screenshotDriver, options = {}) {
1057
+ this.transport = transport;
1058
+ this.screenshotDriver = screenshotDriver;
1059
+ this.options = {
1060
+ shortcut: "Alt+Click",
1061
+ maskSelectors: ['input[type="password"]', "[data-sensitive]"],
1062
+ showToolbar: true,
1063
+ ...options
1064
+ };
1065
+ }
1066
+ init() {
1067
+ if (typeof window === "undefined" || typeof document === "undefined") return;
1068
+ if (document.readyState === "loading") {
1069
+ document.addEventListener("DOMContentLoaded", () => {
1070
+ this.ensureContainer();
1071
+ });
1072
+ } else {
1073
+ this.ensureContainer();
1074
+ }
1075
+ const unsubscribe = this.transport.onMessage((msg) => {
1076
+ if (msg && msg.type === "notes_sync" && Array.isArray(msg.notes)) {
1077
+ this.syncNotes(msg.notes);
1078
+ }
1079
+ });
1080
+ this.cleanups.push(unsubscribe);
1081
+ this.setupListeners();
1082
+ }
1083
+ syncNotes(notes) {
1084
+ this.savedNotes = notes;
1085
+ this.updateToolbarCount();
1086
+ this.renderMarkers();
1087
+ }
1088
+ setMode(mode) {
1089
+ this.activeMode = mode;
1090
+ this.updateToolbarState();
1091
+ if (mode === "element") {
1092
+ this.hideRegionOverlay();
1093
+ } else if (mode === "region") {
1094
+ this.hideHighlight();
1095
+ this.showRegionOverlay();
1096
+ } else if (mode === "page") {
1097
+ this.hideRegionOverlay();
1098
+ this.hideHighlight();
1099
+ this.openNoteEditor(document.body, "page");
1100
+ } else {
1101
+ this.hideRegionOverlay();
1102
+ this.hideHighlight();
1103
+ }
1104
+ }
1105
+ ensureContainer() {
1106
+ if (typeof document === "undefined") return null;
1107
+ if (!this.container) {
1108
+ this.container = document.createElement("div");
1109
+ this.container.id = "browsertrack-inspector-host";
1110
+ this.container.style.cssText = "all: initial; position: fixed; top: 0; left: 0; width: 0; height: 0; z-index: 2147483647; pointer-events: none;";
1111
+ this.shadowRoot = this.container.attachShadow({ mode: "open" });
1112
+ const style = document.createElement("style");
1113
+ style.textContent = `
1114
+ :host {
1115
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
1116
+ color-scheme: dark;
1117
+ -webkit-font-smoothing: antialiased;
1118
+ -moz-osx-font-smoothing: grayscale;
1119
+ }
1120
+
1121
+ /* 1. Element Highlight Overlay */
1122
+ .bt-highlight {
1123
+ position: fixed;
1124
+ border: 2px solid #3b82f6;
1125
+ background: rgba(59, 130, 246, 0.16);
1126
+ border-radius: 4px;
1127
+ pointer-events: none;
1128
+ transition: all 0.05s ease-out;
1129
+ z-index: 2147483640;
1130
+ box-sizing: border-box;
1131
+ }
1132
+
1133
+ .bt-badge {
1134
+ position: absolute;
1135
+ top: -26px;
1136
+ left: 0;
1137
+ background: #0f172a;
1138
+ color: #38bdf8;
1139
+ border: 1px solid #3b82f6;
1140
+ font-size: 11px;
1141
+ font-weight: 600;
1142
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1143
+ padding: 2px 7px;
1144
+ border-radius: 4px;
1145
+ white-space: nowrap;
1146
+ pointer-events: none;
1147
+ box-shadow: 0 4px 6px -1px rgba(0,0,0,0.5);
1148
+ }
1149
+
1150
+ /* 2. Region Selection Overlay */
1151
+ .bt-region-overlay {
1152
+ position: fixed;
1153
+ inset: 0;
1154
+ width: 100vw;
1155
+ height: 100vh;
1156
+ cursor: crosshair;
1157
+ z-index: 2147483642;
1158
+ pointer-events: auto;
1159
+ background: rgba(15, 23, 42, 0.25);
1160
+ user-select: none;
1161
+ }
1162
+
1163
+ .bt-region-box {
1164
+ position: fixed;
1165
+ border: 2px dashed #38bdf8;
1166
+ background: rgba(56, 189, 248, 0.2);
1167
+ box-sizing: border-box;
1168
+ pointer-events: none;
1169
+ z-index: 2147483643;
1170
+ }
1171
+
1172
+ .bt-region-badge {
1173
+ position: absolute;
1174
+ top: -26px;
1175
+ left: 0;
1176
+ background: #0f172a;
1177
+ color: #38bdf8;
1178
+ border: 1px solid #0284c7;
1179
+ font-size: 11px;
1180
+ font-weight: 600;
1181
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1182
+ padding: 2px 7px;
1183
+ border-radius: 4px;
1184
+ white-space: nowrap;
1185
+ pointer-events: none;
1186
+ box-shadow: 0 4px 6px -1px rgba(0,0,0,0.5);
1187
+ }
1188
+
1189
+ .bt-region-banner {
1190
+ position: fixed;
1191
+ top: 20px;
1192
+ left: 50%;
1193
+ transform: translateX(-50%);
1194
+ background: #0f172a;
1195
+ color: #f8fafc;
1196
+ border: 1px solid #0284c7;
1197
+ border-radius: 30px;
1198
+ padding: 8px 16px;
1199
+ display: flex;
1200
+ align-items: center;
1201
+ gap: 12px;
1202
+ box-shadow: 0 10px 25px -5px rgba(0,0,0,0.6);
1203
+ font-size: 12.5px;
1204
+ font-weight: 500;
1205
+ pointer-events: auto;
1206
+ z-index: 2147483645;
1207
+ user-select: none;
1208
+ }
1209
+
1210
+ .bt-region-cancel-btn {
1211
+ background: rgba(239, 68, 68, 0.15);
1212
+ color: #fca5a5;
1213
+ border: 1px solid rgba(239, 68, 68, 0.4);
1214
+ padding: 3px 10px;
1215
+ border-radius: 20px;
1216
+ font-size: 11px;
1217
+ font-weight: 600;
1218
+ cursor: pointer;
1219
+ transition: all 0.15s ease;
1220
+ display: inline-flex;
1221
+ align-items: center;
1222
+ gap: 4px;
1223
+ font-family: inherit;
1224
+ }
1225
+
1226
+ .bt-region-cancel-btn:hover {
1227
+ background: rgba(239, 68, 68, 0.3);
1228
+ color: #ffffff;
1229
+ }
1230
+
1231
+ /* 3. Note Markers on Page */
1232
+ .bt-markers-layer {
1233
+ position: fixed;
1234
+ top: 0;
1235
+ left: 0;
1236
+ width: 0;
1237
+ height: 0;
1238
+ pointer-events: none;
1239
+ z-index: 2147483638;
1240
+ }
1241
+
1242
+ .bt-note-marker {
1243
+ position: fixed;
1244
+ pointer-events: auto;
1245
+ background: linear-gradient(135deg, #2563eb, #1d4ed8);
1246
+ color: #ffffff;
1247
+ border: 2px solid #ffffff;
1248
+ box-shadow: 0 4px 12px rgba(0,0,0,0.5), 0 0 0 1px rgba(37,99,235,0.4);
1249
+ border-radius: 999px;
1250
+ height: 26px;
1251
+ min-width: 26px;
1252
+ padding: 0 7px;
1253
+ display: inline-flex;
1254
+ align-items: center;
1255
+ justify-content: center;
1256
+ gap: 4px;
1257
+ cursor: pointer;
1258
+ font-size: 11px;
1259
+ font-weight: 700;
1260
+ user-select: none;
1261
+ transition: transform 0.15s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.15s ease;
1262
+ animation: bt-pop-in 0.2s ease-out;
1263
+ box-sizing: border-box;
1264
+ z-index: 2147483641;
1265
+ }
1266
+
1267
+ @keyframes bt-pop-in {
1268
+ 0% { transform: scale(0.6); opacity: 0; }
1269
+ 100% { transform: scale(1); opacity: 1; }
1270
+ }
1271
+
1272
+ .bt-note-marker:hover {
1273
+ transform: scale(1.15) translateY(-2px);
1274
+ box-shadow: 0 8px 20px rgba(37,99,235,0.6), 0 0 0 2px #60a5fa;
1275
+ }
1276
+
1277
+ .bt-marker-resolved {
1278
+ background: linear-gradient(135deg, #475569, #334155);
1279
+ border-color: #94a3b8;
1280
+ opacity: 0.75;
1281
+ }
1282
+
1283
+ .bt-region-marker-box {
1284
+ position: fixed;
1285
+ border: 2px dashed #0284c7;
1286
+ background: rgba(14, 165, 233, 0.08);
1287
+ pointer-events: none;
1288
+ box-sizing: border-box;
1289
+ border-radius: 6px;
1290
+ z-index: 2147483639;
1291
+ }
1292
+
1293
+ .bt-page-notes-dock {
1294
+ position: fixed;
1295
+ top: 16px;
1296
+ right: 16px;
1297
+ display: flex;
1298
+ flex-direction: column;
1299
+ gap: 6px;
1300
+ pointer-events: none;
1301
+ z-index: 2147483641;
1302
+ }
1303
+
1304
+ .bt-page-note-pill {
1305
+ background: #0f172a;
1306
+ color: #c084fc;
1307
+ border: 1px solid #7e22ce;
1308
+ border-radius: 20px;
1309
+ padding: 5px 12px;
1310
+ font-size: 11.5px;
1311
+ font-weight: 600;
1312
+ display: inline-flex;
1313
+ align-items: center;
1314
+ gap: 6px;
1315
+ box-shadow: 0 4px 12px rgba(0,0,0,0.5);
1316
+ cursor: pointer;
1317
+ pointer-events: auto;
1318
+ transition: all 0.15s ease;
1319
+ }
1320
+
1321
+ .bt-page-note-pill:hover {
1322
+ background: #1e1b4b;
1323
+ transform: translateY(-1px);
1324
+ }
1325
+
1326
+ /* 4. Floating Quick Toolbar */
1327
+ .bt-toolbar {
1328
+ position: fixed;
1329
+ bottom: 20px;
1330
+ right: 20px;
1331
+ background: #0f172a;
1332
+ border: 1px solid #334155;
1333
+ border-radius: 30px;
1334
+ padding: 4px 6px;
1335
+ display: flex;
1336
+ align-items: center;
1337
+ gap: 4px;
1338
+ box-shadow: 0 10px 25px -3px rgba(0,0,0,0.6), 0 4px 6px -4px rgba(0,0,0,0.4);
1339
+ pointer-events: auto;
1340
+ z-index: 2147483646;
1341
+ user-select: none;
1342
+ transition: all 0.2s ease;
1343
+ }
1344
+
1345
+ .bt-toolbar-btn {
1346
+ background: transparent;
1347
+ border: none;
1348
+ color: #94a3b8;
1349
+ font-size: 12px;
1350
+ font-weight: 500;
1351
+ padding: 6px 12px;
1352
+ border-radius: 20px;
1353
+ cursor: pointer;
1354
+ display: flex;
1355
+ align-items: center;
1356
+ gap: 6px;
1357
+ transition: all 0.15s ease;
1358
+ font-family: inherit;
1359
+ }
1360
+
1361
+ .bt-toolbar-btn:hover {
1362
+ background: #1e293b;
1363
+ color: #f8fafc;
1364
+ }
1365
+
1366
+ .bt-toolbar-btn.active {
1367
+ background: #2563eb;
1368
+ color: #ffffff;
1369
+ font-weight: 600;
1370
+ box-shadow: 0 2px 6px rgba(37, 99, 235, 0.35);
1371
+ }
1372
+
1373
+ .bt-count-pill {
1374
+ background: rgba(255, 255, 255, 0.2);
1375
+ color: #ffffff;
1376
+ font-size: 10px;
1377
+ font-weight: 700;
1378
+ padding: 1px 6px;
1379
+ border-radius: 10px;
1380
+ line-height: 1.2;
1381
+ }
1382
+
1383
+ .bt-toolbar-divider {
1384
+ width: 1px;
1385
+ height: 16px;
1386
+ background: #334155;
1387
+ margin: 0 2px;
1388
+ }
1389
+
1390
+ /* 5. Modals & Popover Card */
1391
+ .bt-modal-backdrop {
1392
+ position: fixed;
1393
+ top: 0;
1394
+ left: 0;
1395
+ width: 100vw;
1396
+ height: 100vh;
1397
+ background: rgba(15, 23, 42, 0.75);
1398
+ backdrop-filter: blur(6px);
1399
+ display: flex;
1400
+ align-items: center;
1401
+ justify-content: center;
1402
+ pointer-events: auto;
1403
+ z-index: 2147483647;
1404
+ opacity: 1;
1405
+ box-sizing: border-box;
1406
+ animation: bt-fade-in 0.15s ease-out;
1407
+ }
1408
+
1409
+ @keyframes bt-fade-in {
1410
+ from { opacity: 0; transform: scale(0.98); }
1411
+ to { opacity: 1; transform: scale(1); }
1412
+ }
1413
+
1414
+ .bt-modal {
1415
+ background: #0f172a;
1416
+ color: #f8fafc;
1417
+ border: 1px solid #334155;
1418
+ border-radius: 14px;
1419
+ padding: 20px;
1420
+ width: 450px;
1421
+ max-width: 92vw;
1422
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(255, 255, 255, 0.05);
1423
+ display: flex;
1424
+ flex-direction: column;
1425
+ gap: 14px;
1426
+ box-sizing: border-box;
1427
+ }
1428
+
1429
+ .bt-modal-header {
1430
+ display: flex;
1431
+ justify-content: space-between;
1432
+ align-items: center;
1433
+ }
1434
+
1435
+ .bt-modal-title {
1436
+ font-size: 14.5px;
1437
+ font-weight: 600;
1438
+ letter-spacing: -0.01em;
1439
+ display: flex;
1440
+ align-items: center;
1441
+ gap: 8px;
1442
+ color: #f8fafc;
1443
+ }
1444
+
1445
+ .bt-mode-badge {
1446
+ font-size: 10px;
1447
+ font-weight: 700;
1448
+ letter-spacing: 0.05em;
1449
+ text-transform: uppercase;
1450
+ padding: 2px 7px;
1451
+ border-radius: 6px;
1452
+ }
1453
+
1454
+ .bt-badge-element {
1455
+ background: rgba(59, 130, 246, 0.15);
1456
+ color: #60a5fa;
1457
+ border: 1px solid rgba(59, 130, 246, 0.4);
1458
+ }
1459
+
1460
+ .bt-badge-region {
1461
+ background: rgba(14, 165, 233, 0.15);
1462
+ color: #38bdf8;
1463
+ border: 1px solid rgba(14, 165, 233, 0.4);
1464
+ }
1465
+
1466
+ .bt-badge-page {
1467
+ background: rgba(168, 85, 247, 0.15);
1468
+ color: #c084fc;
1469
+ border: 1px solid rgba(168, 85, 247, 0.4);
1470
+ }
1471
+
1472
+ .bt-badge-status-open {
1473
+ background: rgba(16, 185, 129, 0.15);
1474
+ color: #34d399;
1475
+ border: 1px solid rgba(16, 185, 129, 0.4);
1476
+ }
1477
+
1478
+ .bt-badge-status-resolved {
1479
+ background: rgba(100, 116, 139, 0.2);
1480
+ color: #94a3b8;
1481
+ border: 1px solid rgba(100, 116, 139, 0.4);
1482
+ }
1483
+
1484
+ .bt-close-btn {
1485
+ background: transparent;
1486
+ border: none;
1487
+ color: #94a3b8;
1488
+ font-size: 15px;
1489
+ cursor: pointer;
1490
+ padding: 3px 7px;
1491
+ border-radius: 6px;
1492
+ transition: all 0.12s ease;
1493
+ display: flex;
1494
+ align-items: center;
1495
+ justify-content: center;
1496
+ }
1497
+ .bt-close-btn:hover {
1498
+ background: #1e293b;
1499
+ color: #ffffff;
1500
+ }
1501
+
1502
+ .bt-target-pill {
1503
+ background: #090d16;
1504
+ color: #94a3b8;
1505
+ border: 1px solid #1e293b;
1506
+ border-radius: 8px;
1507
+ padding: 8px 12px;
1508
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1509
+ font-size: 11.5px;
1510
+ display: flex;
1511
+ align-items: center;
1512
+ gap: 8px;
1513
+ overflow: hidden;
1514
+ }
1515
+
1516
+ .bt-pill-content {
1517
+ overflow: hidden;
1518
+ text-overflow: ellipsis;
1519
+ white-space: nowrap;
1520
+ color: #cbd5e1;
1521
+ }
1522
+
1523
+ .bt-note-message-box {
1524
+ background: #090d16;
1525
+ border: 1px solid #1e293b;
1526
+ border-radius: 8px;
1527
+ padding: 12px 14px;
1528
+ font-size: 13.5px;
1529
+ line-height: 1.5;
1530
+ color: #f1f5f9;
1531
+ white-space: pre-wrap;
1532
+ word-break: break-word;
1533
+ max-height: 180px;
1534
+ overflow-y: auto;
1535
+ }
1536
+
1537
+ .bt-textarea {
1538
+ background: #090d16;
1539
+ color: #f8fafc;
1540
+ border: 1px solid #334155;
1541
+ border-radius: 8px;
1542
+ padding: 12px;
1543
+ font-size: 13.5px;
1544
+ line-height: 1.5;
1545
+ resize: vertical;
1546
+ min-height: 95px;
1547
+ outline: none;
1548
+ box-sizing: border-box;
1549
+ width: 100%;
1550
+ font-family: inherit;
1551
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
1552
+ }
1553
+
1554
+ .bt-textarea::placeholder {
1555
+ color: #64748b;
1556
+ }
1557
+
1558
+ .bt-textarea:focus {
1559
+ border-color: #3b82f6;
1560
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);
1561
+ }
1562
+
1563
+ .bt-modal-footer {
1564
+ display: flex;
1565
+ justify-content: space-between;
1566
+ align-items: center;
1567
+ gap: 12px;
1568
+ margin-top: 2px;
1569
+ }
1570
+
1571
+ .bt-kbd-hint {
1572
+ font-size: 11px;
1573
+ color: #64748b;
1574
+ display: flex;
1575
+ align-items: center;
1576
+ gap: 4px;
1577
+ user-select: none;
1578
+ }
1579
+
1580
+ .bt-kbd-hint kbd {
1581
+ background: #1e293b;
1582
+ color: #94a3b8;
1583
+ border: 1px solid #334155;
1584
+ border-radius: 4px;
1585
+ padding: 1px 5px;
1586
+ font-family: inherit;
1587
+ font-size: 10px;
1588
+ font-weight: 600;
1589
+ }
1590
+
1591
+ .bt-modal-actions {
1592
+ display: flex;
1593
+ justify-content: flex-end;
1594
+ align-items: center;
1595
+ gap: 8px;
1596
+ }
1597
+
1598
+ .bt-btn {
1599
+ padding: 8px 16px;
1600
+ border-radius: 7px;
1601
+ font-size: 12.5px;
1602
+ font-weight: 600;
1603
+ cursor: pointer;
1604
+ border: 1px solid transparent;
1605
+ transition: all 0.15s ease;
1606
+ font-family: inherit;
1607
+ display: inline-flex;
1608
+ align-items: center;
1609
+ justify-content: center;
1610
+ gap: 6px;
1611
+ }
1612
+
1613
+ .bt-btn-cancel {
1614
+ background: transparent;
1615
+ color: #94a3b8;
1616
+ }
1617
+ .bt-btn-cancel:hover {
1618
+ background: #1e293b;
1619
+ color: #f8fafc;
1620
+ }
1621
+
1622
+ .bt-btn-save {
1623
+ background: #2563eb;
1624
+ color: #ffffff;
1625
+ box-shadow: 0 2px 6px rgba(37, 99, 235, 0.35);
1626
+ }
1627
+ .bt-btn-save:hover {
1628
+ background: #1d4ed8;
1629
+ box-shadow: 0 4px 10px rgba(37, 99, 235, 0.45);
1630
+ }
1631
+
1632
+ .bt-btn-resolve {
1633
+ background: rgba(16, 185, 129, 0.15);
1634
+ color: #6ee7b7;
1635
+ border: 1px solid rgba(16, 185, 129, 0.35);
1636
+ }
1637
+ .bt-btn-resolve:hover {
1638
+ background: rgba(16, 185, 129, 0.3);
1639
+ color: #ffffff;
1640
+ }
1641
+
1642
+ .bt-btn-delete {
1643
+ background: rgba(239, 68, 68, 0.15);
1644
+ color: #fca5a5;
1645
+ border: 1px solid rgba(239, 68, 68, 0.35);
1646
+ }
1647
+ .bt-btn-delete:hover {
1648
+ background: rgba(239, 68, 68, 0.3);
1649
+ color: #ffffff;
1650
+ }
1651
+ `;
1652
+ this.shadowRoot.appendChild(style);
1653
+ this.highlightOverlay = document.createElement("div");
1654
+ this.highlightOverlay.className = "bt-highlight";
1655
+ this.highlightOverlay.style.display = "none";
1656
+ this.shadowRoot.appendChild(this.highlightOverlay);
1657
+ this.markersContainer = document.createElement("div");
1658
+ this.markersContainer.className = "bt-markers-layer";
1659
+ this.shadowRoot.appendChild(this.markersContainer);
1660
+ if (this.options.showToolbar) {
1661
+ this.createToolbar();
1662
+ }
1663
+ }
1664
+ if (this.container && !this.container.isConnected) {
1665
+ const mountParent = document.body || document.documentElement;
1666
+ if (mountParent) {
1667
+ mountParent.appendChild(this.container);
1668
+ }
1669
+ }
1670
+ return this.shadowRoot;
1671
+ }
1672
+ createToolbar() {
1673
+ if (!this.shadowRoot || this.toolbarElement) return;
1674
+ this.toolbarElement = document.createElement("div");
1675
+ this.toolbarElement.className = "bt-toolbar";
1676
+ this.toolbarElement.innerHTML = `
1677
+ <button class="bt-toolbar-btn" id="bt-mode-element" title="Inspect element (or hold Alt+Click)">
1678
+ <span>\u{1F3AF}</span> Element
1679
+ </button>
1680
+ <button class="bt-toolbar-btn" id="bt-mode-region" title="Drag to select screen region">
1681
+ <span>\u{1F4D0}</span> Region
1682
+ </button>
1683
+ <button class="bt-toolbar-btn" id="bt-mode-page" title="Leave full-page note">
1684
+ <span>\u{1F4C4}</span> Page
1685
+ </button>
1686
+ <div class="bt-toolbar-divider"></div>
1687
+ <button class="bt-toolbar-btn active" id="bt-toggle-notes" title="Toggle visible note markers on screen">
1688
+ <span>\u{1F4CC}</span> Notes <span class="bt-count-pill" id="bt-notes-count">0</span>
1689
+ </button>
1690
+ `;
1691
+ const btnElement = this.toolbarElement.querySelector("#bt-mode-element");
1692
+ const btnRegion = this.toolbarElement.querySelector("#bt-mode-region");
1693
+ const btnPage = this.toolbarElement.querySelector("#bt-mode-page");
1694
+ const btnToggleNotes = this.toolbarElement.querySelector("#bt-toggle-notes");
1695
+ btnElement.onclick = (e) => {
1696
+ e.stopPropagation();
1697
+ this.setMode(this.activeMode === "element" ? "idle" : "element");
1698
+ };
1699
+ btnRegion.onclick = (e) => {
1700
+ e.stopPropagation();
1701
+ this.setMode(this.activeMode === "region" ? "idle" : "region");
1702
+ };
1703
+ btnPage.onclick = (e) => {
1704
+ e.stopPropagation();
1705
+ this.setMode("page");
1706
+ };
1707
+ btnToggleNotes.onclick = (e) => {
1708
+ e.stopPropagation();
1709
+ this.showMarkers = !this.showMarkers;
1710
+ btnToggleNotes.classList.toggle("active", this.showMarkers);
1711
+ this.renderMarkers();
1712
+ };
1713
+ this.shadowRoot.appendChild(this.toolbarElement);
1714
+ }
1715
+ updateToolbarState() {
1716
+ if (!this.toolbarElement) return;
1717
+ const btnElement = this.toolbarElement.querySelector("#bt-mode-element");
1718
+ const btnRegion = this.toolbarElement.querySelector("#bt-mode-region");
1719
+ const btnPage = this.toolbarElement.querySelector("#bt-mode-page");
1720
+ btnElement?.classList.toggle("active", this.activeMode === "element");
1721
+ btnRegion?.classList.toggle("active", this.activeMode === "region");
1722
+ btnPage?.classList.toggle("active", this.activeMode === "page");
1723
+ }
1724
+ updateToolbarCount() {
1725
+ if (!this.toolbarElement) return;
1726
+ const countEl = this.toolbarElement.querySelector("#bt-notes-count");
1727
+ if (countEl) {
1728
+ const openCount = this.savedNotes.filter((n) => n.status === "OPEN").length;
1729
+ countEl.textContent = String(openCount);
1730
+ }
1731
+ }
1732
+ renderMarkers() {
1733
+ const root = this.ensureContainer();
1734
+ if (!root || !this.markersContainer) return;
1735
+ this.markersContainer.innerHTML = "";
1736
+ if (!this.showMarkers) return;
1737
+ const currentPath = window.location.pathname;
1738
+ const activeNotes = this.savedNotes.filter(
1739
+ (n) => n.status === "OPEN" && (n.route === currentPath || !n.route || n.route === "/" || window.location.href.includes(n.route))
1740
+ );
1741
+ const pageNotes = [];
1742
+ activeNotes.forEach((note, index) => {
1743
+ if (note.type === "page") {
1744
+ pageNotes.push(note);
1745
+ return;
1746
+ }
1747
+ if (note.type === "region" && note.region) {
1748
+ const regBox = document.createElement("div");
1749
+ regBox.className = "bt-region-marker-box";
1750
+ regBox.style.left = `${note.region.x}px`;
1751
+ regBox.style.top = `${note.region.y}px`;
1752
+ regBox.style.width = `${note.region.width}px`;
1753
+ regBox.style.height = `${note.region.height}px`;
1754
+ this.markersContainer.appendChild(regBox);
1755
+ const pin = document.createElement("div");
1756
+ pin.className = "bt-note-marker";
1757
+ pin.style.left = `${Math.max(4, note.region.x - 12)}px`;
1758
+ pin.style.top = `${Math.max(4, note.region.y - 12)}px`;
1759
+ pin.title = note.message;
1760
+ pin.innerHTML = `<span>\u{1F4D0}</span> <span>#${index + 1}</span>`;
1761
+ pin.onclick = (e) => {
1762
+ e.stopPropagation();
1763
+ this.openNoteCard(note);
1764
+ };
1765
+ this.markersContainer.appendChild(pin);
1766
+ return;
1767
+ }
1768
+ let targetEl = null;
1769
+ if (note.target?.selector) {
1770
+ try {
1771
+ targetEl = document.querySelector(note.target.selector);
1772
+ } catch {
1773
+ }
1774
+ }
1775
+ const rect = targetEl ? targetEl.getBoundingClientRect() : note.target?.boundingRect;
1776
+ if (rect) {
1777
+ const pin = document.createElement("div");
1778
+ pin.className = "bt-note-marker";
1779
+ pin.setAttribute("data-note-id", note.id);
1780
+ pin.title = note.message;
1781
+ pin.style.left = `${Math.max(4, rect.left - 10)}px`;
1782
+ pin.style.top = `${Math.max(4, rect.top - 12)}px`;
1783
+ pin.innerHTML = `<span>\u{1F4DD}</span> <span>#${index + 1}</span>`;
1784
+ pin.onclick = (e) => {
1785
+ e.stopPropagation();
1786
+ this.openNoteCard(note);
1787
+ };
1788
+ if (targetEl) {
1789
+ pin.onmouseenter = () => this.updateHighlight(targetEl);
1790
+ pin.onmouseleave = () => this.hideHighlight();
1791
+ }
1792
+ this.markersContainer.appendChild(pin);
1793
+ }
1794
+ });
1795
+ if (pageNotes.length > 0) {
1796
+ const pageDock = document.createElement("div");
1797
+ pageDock.className = "bt-page-notes-dock";
1798
+ pageNotes.forEach((pNote, idx) => {
1799
+ const pill = document.createElement("div");
1800
+ pill.className = "bt-page-note-pill";
1801
+ pill.innerHTML = `<span>\u{1F4C4}</span> <span>Page Note (${truncate(pNote.message, 25)})</span>`;
1802
+ pill.onclick = (e) => {
1803
+ e.stopPropagation();
1804
+ this.openNoteCard(pNote);
1805
+ };
1806
+ pageDock.appendChild(pill);
1807
+ });
1808
+ this.markersContainer.appendChild(pageDock);
1809
+ }
1810
+ }
1811
+ updateMarkerPositions() {
1812
+ if (!this.showMarkers || !this.markersContainer) return;
1813
+ this.renderMarkers();
1814
+ }
1815
+ openNoteCard(note) {
1816
+ const root = this.ensureContainer();
1817
+ if (!root) return;
1818
+ if (this.cardOverlay && this.cardOverlay.parentElement) {
1819
+ this.cardOverlay.parentElement.removeChild(this.cardOverlay);
1820
+ this.cardOverlay = null;
1821
+ }
1822
+ const backdrop = document.createElement("div");
1823
+ backdrop.className = "bt-modal-backdrop";
1824
+ this.cardOverlay = backdrop;
1825
+ const badgeClass = `bt-badge-${note.type.toLowerCase()}`;
1826
+ const statusClass = note.status === "OPEN" ? "bt-badge-status-open" : "bt-badge-status-resolved";
1827
+ const icon = note.type === "region" ? "\u{1F4D0}" : note.type === "page" ? "\u{1F4C4}" : "\u{1F3AF}";
1828
+ const contextText = note.type === "region" && note.region ? `Region: ${note.region.width} \xD7 ${note.region.height} px \xB7 Route: ${note.route}` : note.type === "page" ? `Page Note \xB7 Route: ${note.route}` : `${note.target?.selector || "Element"} (${note.target?.boundingRect?.width || 0}\xD7${note.target?.boundingRect?.height || 0}px) \xB7 ${note.route}`;
1829
+ const formattedDate = new Date(note.createdAt).toLocaleString();
1830
+ backdrop.innerHTML = `
1831
+ <div class="bt-modal">
1832
+ <div class="bt-modal-header">
1833
+ <div class="bt-modal-title">
1834
+ <span>${icon} Visual Note Details</span>
1835
+ <span class="bt-mode-badge ${badgeClass}">${note.type.toUpperCase()}</span>
1836
+ <span class="bt-mode-badge ${statusClass}">${note.status}</span>
1837
+ </div>
1838
+ <button class="bt-close-btn" id="btn-card-close" title="Close (Esc)">\u2715</button>
1839
+ </div>
1840
+
1841
+ <div class="bt-target-pill" title="${contextText}">
1842
+ <span>\u{1F3F7}\uFE0F</span>
1843
+ <span class="bt-pill-content">${contextText}</span>
1844
+ </div>
1845
+
1846
+ <div class="bt-note-message-box">${note.message}</div>
1847
+
1848
+ <div style="font-size: 11px; color: #64748b; display: flex; justify-content: space-between; padding: 0 2px;">
1849
+ <span>Created: ${formattedDate}</span>
1850
+ <span>ID: <code style="font-family: monospace; color: #94a3b8;">${note.id}</code></span>
1851
+ </div>
1852
+
1853
+ <div class="bt-modal-footer">
1854
+ <button class="bt-btn bt-btn-delete" id="btn-card-delete" title="Delete this note">
1855
+ <span>\u{1F5D1}\uFE0F</span> Delete
1856
+ </button>
1857
+ <div class="bt-modal-actions">
1858
+ <button class="bt-btn bt-btn-cancel" id="btn-card-dismiss">Close</button>
1859
+ <button class="bt-btn ${note.status === "OPEN" ? "bt-btn-resolve" : "bt-btn-save"}" id="btn-card-resolve">
1860
+ ${note.status === "OPEN" ? "<span>\u2705</span> Resolve Note" : "<span>\u21BA</span> Reopen"}
1861
+ </button>
1862
+ </div>
1863
+ </div>
1864
+ </div>
1865
+ `;
1866
+ const btnClose = backdrop.querySelector("#btn-card-close");
1867
+ const btnDismiss = backdrop.querySelector("#btn-card-dismiss");
1868
+ const btnResolve = backdrop.querySelector("#btn-card-resolve");
1869
+ const btnDelete = backdrop.querySelector("#btn-card-delete");
1870
+ const closeCard = () => {
1871
+ if (this.cardOverlay) {
1872
+ root.removeChild(this.cardOverlay);
1873
+ this.cardOverlay = null;
1874
+ this.hideHighlight();
1875
+ }
1876
+ };
1877
+ btnClose.onclick = closeCard;
1878
+ btnDismiss.onclick = closeCard;
1879
+ backdrop.onclick = (e) => {
1880
+ if (e.target === backdrop) closeCard();
1881
+ };
1882
+ btnResolve.onclick = () => {
1883
+ if (note.status === "OPEN") {
1884
+ this.transport.send({ type: "resolve_note", noteId: note.id });
1885
+ } else {
1886
+ this.transport.send({ type: "reopen_note", noteId: note.id });
1887
+ }
1888
+ closeCard();
1889
+ };
1890
+ btnDelete.onclick = () => {
1891
+ this.transport.send({ type: "delete_note", noteId: note.id });
1892
+ closeCard();
1893
+ };
1894
+ root.appendChild(backdrop);
1895
+ }
1896
+ setupListeners() {
1897
+ const onMouseMove = (e) => {
1898
+ if (this.modalOverlay || this.cardOverlay || this.isDraggingRegion || this.activeMode === "region") return;
1899
+ const path = e.composedPath ? e.composedPath() : [];
1900
+ if (this.container && path.includes(this.container)) {
1901
+ this.hideHighlight();
1902
+ return;
1903
+ }
1904
+ if (e.altKey || this.activeMode === "element") {
1905
+ const target = document.elementFromPoint(e.clientX, e.clientY);
1906
+ if (target && target !== this.container && !this.container?.contains(target)) {
1907
+ this.hoveredElement = target;
1908
+ this.updateHighlight(target);
1909
+ return;
1910
+ }
1911
+ }
1912
+ if (!e.altKey && this.activeMode !== "element") {
1913
+ this.hideHighlight();
1914
+ }
1915
+ };
1916
+ const onClick = (e) => {
1917
+ if (this.modalOverlay || this.cardOverlay) return;
1918
+ const path = e.composedPath ? e.composedPath() : [];
1919
+ if (this.container && path.includes(this.container)) {
1920
+ return;
1921
+ }
1922
+ if (this.activeMode === "region") return;
1923
+ if (e.altKey || this.activeMode === "element") {
1924
+ e.preventDefault();
1925
+ e.stopPropagation();
1926
+ const target = this.hoveredElement || document.elementFromPoint(e.clientX, e.clientY);
1927
+ if (target && target !== this.container && !this.container?.contains(target)) {
1928
+ this.selectedElement = target;
1929
+ this.openNoteEditor(target, "element");
1930
+ if (this.activeMode === "element") {
1931
+ this.setMode("idle");
1932
+ }
1933
+ }
1934
+ }
1935
+ };
1936
+ const onKeyDown = (e) => {
1937
+ if (e.key === "Escape") {
1938
+ if (this.cardOverlay && this.cardOverlay.parentElement && this.shadowRoot) {
1939
+ this.shadowRoot.removeChild(this.cardOverlay);
1940
+ this.cardOverlay = null;
1941
+ } else if (this.activeMode === "region" || this.activeMode === "element") {
1942
+ this.setMode("idle");
1943
+ }
1944
+ }
1945
+ };
1946
+ const onKeyUp = (e) => {
1947
+ if (e.key === "Alt" && !this.modalOverlay && !this.cardOverlay && this.activeMode !== "element") {
1948
+ this.hideHighlight();
1949
+ }
1950
+ };
1951
+ const onScrollOrResize = () => {
1952
+ requestAnimationFrame(() => {
1953
+ this.updateMarkerPositions();
1954
+ });
1955
+ };
1956
+ window.addEventListener("mousemove", onMouseMove, { capture: true, passive: true });
1957
+ window.addEventListener("click", onClick, { capture: true });
1958
+ window.addEventListener("keydown", onKeyDown, { capture: true });
1959
+ window.addEventListener("keyup", onKeyUp, { capture: true });
1960
+ window.addEventListener("scroll", onScrollOrResize, { capture: true, passive: true });
1961
+ window.addEventListener("resize", onScrollOrResize, { passive: true });
1962
+ this.cleanups.push(() => {
1963
+ window.removeEventListener("mousemove", onMouseMove, { capture: true });
1964
+ window.removeEventListener("click", onClick, { capture: true });
1965
+ window.removeEventListener("keydown", onKeyDown, { capture: true });
1966
+ window.removeEventListener("keyup", onKeyUp, { capture: true });
1967
+ window.removeEventListener("scroll", onScrollOrResize, { capture: true });
1968
+ window.removeEventListener("resize", onScrollOrResize);
1969
+ });
1970
+ }
1971
+ showRegionOverlay() {
1972
+ const root = this.ensureContainer();
1973
+ if (!root) return;
1974
+ if (!this.regionOverlay) {
1975
+ this.regionOverlay = document.createElement("div");
1976
+ this.regionOverlay.className = "bt-region-overlay";
1977
+ this.regionBox = document.createElement("div");
1978
+ this.regionBox.className = "bt-region-box";
1979
+ this.regionBox.style.display = "none";
1980
+ this.regionOverlay.appendChild(this.regionBox);
1981
+ this.regionBanner = document.createElement("div");
1982
+ this.regionBanner.className = "bt-region-banner";
1983
+ this.regionBanner.innerHTML = `
1984
+ <span>\u{1F4D0} Drag rectangle on screen to select region</span>
1985
+ <button class="bt-region-cancel-btn" id="bt-region-cancel">\u2715 Cancel (Esc)</button>
1986
+ `;
1987
+ const btnCancel = this.regionBanner.querySelector("#bt-region-cancel");
1988
+ btnCancel.onclick = (e) => {
1989
+ e.stopPropagation();
1990
+ this.setMode("idle");
1991
+ };
1992
+ this.regionOverlay.appendChild(this.regionBanner);
1993
+ this.regionOverlay.oncontextmenu = (e) => {
1994
+ e.preventDefault();
1995
+ this.setMode("idle");
1996
+ };
1997
+ this.regionOverlay.onmousedown = (e) => {
1998
+ const path = e.composedPath ? e.composedPath() : [];
1999
+ if (this.regionBanner && path.includes(this.regionBanner)) return;
2000
+ if (this.toolbarElement && path.includes(this.toolbarElement)) return;
2001
+ this.isDraggingRegion = true;
2002
+ this.dragStartX = e.clientX;
2003
+ this.dragStartY = e.clientY;
2004
+ if (this.regionBox) {
2005
+ this.regionBox.style.display = "block";
2006
+ this.regionBox.style.left = `${e.clientX}px`;
2007
+ this.regionBox.style.top = `${e.clientY}px`;
2008
+ this.regionBox.style.width = "0px";
2009
+ this.regionBox.style.height = "0px";
2010
+ }
2011
+ };
2012
+ this.regionOverlay.onmousemove = (e) => {
2013
+ if (!this.isDraggingRegion || !this.regionBox) return;
2014
+ const currentX = e.clientX;
2015
+ const currentY = e.clientY;
2016
+ const left = Math.min(this.dragStartX, currentX);
2017
+ const top = Math.min(this.dragStartY, currentY);
2018
+ const width = Math.abs(currentX - this.dragStartX);
2019
+ const height = Math.abs(currentY - this.dragStartY);
2020
+ this.regionBox.style.left = `${left}px`;
2021
+ this.regionBox.style.top = `${top}px`;
2022
+ this.regionBox.style.width = `${width}px`;
2023
+ this.regionBox.style.height = `${height}px`;
2024
+ let badge = this.regionBox.querySelector(".bt-region-badge");
2025
+ if (!badge) {
2026
+ badge = document.createElement("div");
2027
+ badge.className = "bt-region-badge";
2028
+ this.regionBox.appendChild(badge);
2029
+ }
2030
+ badge.textContent = `Region: ${Math.round(width)} \xD7 ${Math.round(height)} px`;
2031
+ };
2032
+ this.regionOverlay.onmouseup = (e) => {
2033
+ if (!this.isDraggingRegion) return;
2034
+ this.isDraggingRegion = false;
2035
+ const currentX = e.clientX;
2036
+ const currentY = e.clientY;
2037
+ const left = Math.min(this.dragStartX, currentX);
2038
+ const top = Math.min(this.dragStartY, currentY);
2039
+ const width = Math.abs(currentX - this.dragStartX);
2040
+ const height = Math.abs(currentY - this.dragStartY);
2041
+ if (width > 15 && height > 15) {
2042
+ this.selectedRegion = {
2043
+ x: Math.round(left),
2044
+ y: Math.round(top),
2045
+ width: Math.round(width),
2046
+ height: Math.round(height)
2047
+ };
2048
+ this.hideRegionOverlay();
2049
+ this.setMode("idle");
2050
+ this.openRegionNoteEditor(this.selectedRegion);
2051
+ } else {
2052
+ this.hideRegionOverlay();
2053
+ this.setMode("idle");
2054
+ }
2055
+ };
2056
+ }
2057
+ if (this.toolbarElement && this.toolbarElement.parentNode === root) {
2058
+ root.insertBefore(this.regionOverlay, this.toolbarElement);
2059
+ } else {
2060
+ root.appendChild(this.regionOverlay);
2061
+ }
2062
+ }
2063
+ hideRegionOverlay() {
2064
+ this.isDraggingRegion = false;
2065
+ if (this.regionOverlay && this.regionOverlay.parentNode) {
2066
+ this.regionOverlay.parentNode.removeChild(this.regionOverlay);
2067
+ }
2068
+ if (this.regionBox) {
2069
+ this.regionBox.style.display = "none";
2070
+ }
2071
+ }
2072
+ updateHighlight(el) {
2073
+ const root = this.ensureContainer();
2074
+ if (!root || !this.highlightOverlay) return;
2075
+ const rect = el.getBoundingClientRect();
2076
+ this.highlightOverlay.style.display = "block";
2077
+ this.highlightOverlay.style.top = `${rect.top}px`;
2078
+ this.highlightOverlay.style.left = `${rect.left}px`;
2079
+ this.highlightOverlay.style.width = `${rect.width}px`;
2080
+ this.highlightOverlay.style.height = `${rect.height}px`;
2081
+ const selector = getSemanticSelector(el);
2082
+ const badgeText = `${selector} \xB7 ${Math.round(rect.width)}\xD7${Math.round(rect.height)}`;
2083
+ let badge = this.highlightOverlay.querySelector(".bt-badge");
2084
+ if (!badge) {
2085
+ badge = document.createElement("div");
2086
+ badge.className = "bt-badge";
2087
+ this.highlightOverlay.appendChild(badge);
2088
+ }
2089
+ badge.textContent = badgeText;
2090
+ }
2091
+ hideHighlight() {
2092
+ if (this.highlightOverlay) {
2093
+ this.highlightOverlay.style.display = "none";
2094
+ }
2095
+ }
2096
+ openNoteEditor(el, noteType = "element") {
2097
+ const root = this.ensureContainer();
2098
+ if (!root) return;
2099
+ const targetEl = el || document.body;
2100
+ this.selectedElement = targetEl;
2101
+ if (noteType === "element") {
2102
+ this.updateHighlight(targetEl);
2103
+ }
2104
+ this.renderNoteModal({
2105
+ title: "Add Visual Note",
2106
+ modeBadge: noteType.toUpperCase(),
2107
+ pillText: noteType === "page" ? `Page Viewport: ${window.innerWidth} \xD7 ${window.innerHeight} px` : `${getSemanticSelector(targetEl)} (${Math.round(targetEl.getBoundingClientRect().width)}\xD7${Math.round(targetEl.getBoundingClientRect().height)}) \xB7 Viewport: ${window.innerWidth}\xD7${window.innerHeight}`,
2108
+ onSave: async (message) => {
2109
+ await this.saveVisualNote(targetEl, message, noteType);
2110
+ }
2111
+ });
2112
+ }
2113
+ openRegionNoteEditor(region) {
2114
+ this.renderNoteModal({
2115
+ title: "Add Region Note",
2116
+ modeBadge: "REGION",
2117
+ pillText: `Selected Area: x:${region.x}, y:${region.y} (${region.width} \xD7 ${region.height} px)`,
2118
+ onSave: async (message) => {
2119
+ await this.saveRegionVisualNote(region, message);
2120
+ }
2121
+ });
2122
+ }
2123
+ renderNoteModal(options) {
2124
+ const root = this.ensureContainer();
2125
+ if (!root) return;
2126
+ if (this.modalOverlay && this.modalOverlay.parentElement) {
2127
+ this.modalOverlay.parentElement.removeChild(this.modalOverlay);
2128
+ this.modalOverlay = null;
2129
+ }
2130
+ const backdrop = document.createElement("div");
2131
+ backdrop.className = "bt-modal-backdrop";
2132
+ this.modalOverlay = backdrop;
2133
+ const badgeClass = `bt-badge-${options.modeBadge.toLowerCase()}`;
2134
+ const icon = options.modeBadge === "REGION" ? "\u{1F4D0}" : options.modeBadge === "PAGE" ? "\u{1F4C4}" : "\u{1F3AF}";
2135
+ const isMac = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
2136
+ backdrop.innerHTML = `
2137
+ <div class="bt-modal">
2138
+ <div class="bt-modal-header">
2139
+ <div class="bt-modal-title">
2140
+ <span>\u{1F4DD} ${options.title}</span>
2141
+ <span class="bt-mode-badge ${badgeClass}">${options.modeBadge}</span>
2142
+ </div>
2143
+ <button class="bt-close-btn" id="btn-close" title="Close (Esc)">\u2715</button>
2144
+ </div>
2145
+ <div class="bt-target-pill" title="${options.pillText}">
2146
+ <span>${icon}</span>
2147
+ <span class="bt-pill-content">${options.pillText}</span>
2148
+ </div>
2149
+ <textarea class="bt-textarea" placeholder="Describe the layout issue, styling bug, or note for AI agent..." autofocus></textarea>
2150
+ <div class="bt-modal-footer">
2151
+ <div class="bt-kbd-hint">
2152
+ <kbd>${isMac ? "\u2318" : "Ctrl"}+Enter</kbd> save \xB7 <kbd>Esc</kbd> cancel
2153
+ </div>
2154
+ <div class="bt-modal-actions">
2155
+ <button class="bt-btn bt-btn-cancel" id="btn-cancel">Cancel</button>
2156
+ <button class="bt-btn bt-btn-save" id="btn-save">Save Note</button>
2157
+ </div>
2158
+ </div>
2159
+ </div>
2160
+ `;
2161
+ const textarea = backdrop.querySelector("textarea");
2162
+ const btnClose = backdrop.querySelector("#btn-close");
2163
+ const btnCancel = backdrop.querySelector("#btn-cancel");
2164
+ const btnSave = backdrop.querySelector("#btn-save");
2165
+ const closeModal = () => {
2166
+ if (this.modalOverlay) {
2167
+ root.removeChild(this.modalOverlay);
2168
+ this.modalOverlay = null;
2169
+ this.hideHighlight();
2170
+ this.hideRegionOverlay();
2171
+ }
2172
+ };
2173
+ btnClose.onclick = closeModal;
2174
+ btnCancel.onclick = closeModal;
2175
+ backdrop.onclick = (e) => {
2176
+ if (e.target === backdrop) {
2177
+ closeModal();
2178
+ }
2179
+ };
2180
+ const submitNote = async () => {
2181
+ const message = textarea.value.trim();
2182
+ if (!message) return;
2183
+ btnSave.textContent = "Saving...";
2184
+ btnSave.disabled = true;
2185
+ try {
2186
+ await options.onSave(message);
2187
+ } finally {
2188
+ closeModal();
2189
+ }
2190
+ };
2191
+ btnSave.onclick = submitNote;
2192
+ textarea.onkeydown = (e) => {
2193
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey || !e.shiftKey)) {
2194
+ e.preventDefault();
2195
+ submitNote();
2196
+ }
2197
+ if (e.key === "Escape") {
2198
+ closeModal();
2199
+ }
2200
+ };
2201
+ root.appendChild(backdrop);
2202
+ setTimeout(() => textarea?.focus(), 50);
2203
+ }
2204
+ async saveVisualNote(targetEl, message, noteType = "element") {
2205
+ const rect = targetEl.getBoundingClientRect();
2206
+ const selector = noteType === "page" ? "body" : getSemanticSelector(targetEl);
2207
+ let screenshotDataUrl;
2208
+ try {
2209
+ const snap = await this.screenshotDriver.captureElement(targetEl);
2210
+ if (snap.ok) {
2211
+ screenshotDataUrl = snap.dataUrl;
2212
+ }
2213
+ } catch {
2214
+ }
2215
+ const elementContext = this.extractElementContext(targetEl);
2216
+ const target = {
2217
+ selector,
2218
+ boundingRect: {
2219
+ x: Math.round(rect.x),
2220
+ y: Math.round(rect.y),
2221
+ width: Math.round(rect.width),
2222
+ height: Math.round(rect.height),
2223
+ top: Math.round(rect.top),
2224
+ left: Math.round(rect.left),
2225
+ bottom: Math.round(rect.bottom),
2226
+ right: Math.round(rect.right)
2227
+ },
2228
+ visible: rect.width > 0 && rect.height > 0,
2229
+ confidence: selector.startsWith("[data-test") ? "high" : selector.startsWith("#") ? "medium" : "low"
2230
+ };
2231
+ const notePayload = {
2232
+ type: "create_note",
2233
+ sessionId: this.transport.getSessionId() || "",
2234
+ noteType,
2235
+ message,
2236
+ route: window.location.pathname + window.location.search,
2237
+ url: window.location.href,
2238
+ viewport: {
2239
+ width: window.innerWidth,
2240
+ height: window.innerHeight,
2241
+ devicePixelRatio: window.devicePixelRatio || 1
2242
+ },
2243
+ scroll: {
2244
+ scrollX: window.scrollX,
2245
+ scrollY: window.scrollY
2246
+ },
2247
+ target,
2248
+ elementContext,
2249
+ screenshot: screenshotDataUrl,
2250
+ timestamp: Date.now()
2251
+ };
2252
+ this.transport.send(notePayload);
2253
+ if (this.options.onNoteCreated) {
2254
+ this.options.onNoteCreated(notePayload);
2255
+ }
2256
+ }
2257
+ async saveRegionVisualNote(region, message) {
2258
+ let screenshotDataUrl;
2259
+ try {
2260
+ const snap = await this.screenshotDriver.captureElement(document.body || document.documentElement);
2261
+ if (snap.ok && snap.dataUrl) {
2262
+ screenshotDataUrl = await this.cropDataUrl(snap.dataUrl, region);
2263
+ }
2264
+ } catch {
2265
+ }
2266
+ const notePayload = {
2267
+ type: "create_note",
2268
+ sessionId: this.transport.getSessionId() || "",
2269
+ noteType: "region",
2270
+ message,
2271
+ route: window.location.pathname + window.location.search,
2272
+ url: window.location.href,
2273
+ viewport: {
2274
+ width: window.innerWidth,
2275
+ height: window.innerHeight,
2276
+ devicePixelRatio: window.devicePixelRatio || 1
2277
+ },
2278
+ scroll: {
2279
+ scrollX: window.scrollX,
2280
+ scrollY: window.scrollY
2281
+ },
2282
+ region,
2283
+ screenshot: screenshotDataUrl,
2284
+ timestamp: Date.now()
2285
+ };
2286
+ this.transport.send(notePayload);
2287
+ if (this.options.onNoteCreated) {
2288
+ this.options.onNoteCreated(notePayload);
2289
+ }
2290
+ }
2291
+ async cropDataUrl(dataUrl, region) {
2292
+ return new Promise((resolve) => {
2293
+ const img = new Image();
2294
+ img.onload = () => {
2295
+ try {
2296
+ const canvas = document.createElement("canvas");
2297
+ canvas.width = region.width;
2298
+ canvas.height = region.height;
2299
+ const ctx = canvas.getContext("2d");
2300
+ if (!ctx) {
2301
+ resolve(dataUrl);
2302
+ return;
2303
+ }
2304
+ const dpr = window.devicePixelRatio || 1;
2305
+ ctx.drawImage(
2306
+ img,
2307
+ region.x * dpr,
2308
+ region.y * dpr,
2309
+ region.width * dpr,
2310
+ region.height * dpr,
2311
+ 0,
2312
+ 0,
2313
+ region.width,
2314
+ region.height
2315
+ );
2316
+ resolve(canvas.toDataURL("image/webp", 0.9));
2317
+ } catch {
2318
+ resolve(dataUrl);
2319
+ }
2320
+ };
2321
+ img.onerror = () => resolve(dataUrl);
2322
+ img.src = dataUrl;
2323
+ });
2324
+ }
2325
+ extractElementContext(el) {
2326
+ const selector = getSemanticSelector(el);
2327
+ const tag = el.tagName.toLowerCase();
2328
+ const attributes = {};
2329
+ for (let i = 0; i < el.attributes.length; i++) {
2330
+ const attr = el.attributes[i];
2331
+ if (attr.name === "value" && el.type === "password") {
2332
+ attributes[attr.name] = "[REDACTED]";
2333
+ } else {
2334
+ attributes[attr.name] = attr.value;
2335
+ }
2336
+ }
2337
+ let outerHTML = "";
2338
+ try {
2339
+ const clone = el.cloneNode(true);
2340
+ for (const passInput of Array.from(clone.querySelectorAll('input[type="password"]'))) {
2341
+ passInput.setAttribute("value", "[REDACTED]");
2342
+ }
2343
+ outerHTML = truncate(clone.outerHTML, 10240);
2344
+ } catch {
2345
+ outerHTML = truncate(el.outerHTML, 10240);
2346
+ }
2347
+ let parent;
2348
+ if (el.parentElement && el.parentElement !== document.body) {
2349
+ parent = {
2350
+ selector: getSemanticSelector(el.parentElement),
2351
+ tag: el.parentElement.tagName.toLowerCase()
2352
+ };
2353
+ }
2354
+ return {
2355
+ selector,
2356
+ tag,
2357
+ attributes,
2358
+ outerHTML,
2359
+ innerText: truncate(el.textContent?.trim(), 200),
2360
+ parent
2361
+ };
2362
+ }
2363
+ destroy() {
2364
+ for (const cleanup of this.cleanups) {
2365
+ try {
2366
+ cleanup();
2367
+ } catch {
2368
+ }
2369
+ }
2370
+ this.cleanups = [];
2371
+ if (this.container && this.container.parentElement) {
2372
+ this.container.parentElement.removeChild(this.container);
2373
+ }
2374
+ this.container = null;
2375
+ this.shadowRoot = null;
2376
+ this.toolbarElement = null;
2377
+ this.regionOverlay = null;
2378
+ this.regionBox = null;
2379
+ this.regionBanner = null;
2380
+ this.markersContainer = null;
2381
+ this.modalOverlay = null;
2382
+ this.cardOverlay = null;
2383
+ }
2384
+ };
2385
+
2386
+ // packages/client/src/client.ts
2387
+ var BrowserTrackClient = class {
2388
+ options;
2389
+ breadcrumbs;
2390
+ transport;
2391
+ screenshotDriver;
2392
+ commandHandler;
2393
+ inspector;
2394
+ lastElement;
2395
+ currentRoute = "/";
2396
+ cleanups = [];
2397
+ isInitialized = false;
2398
+ constructor(options = {}) {
2399
+ this.options = {
2400
+ ...DEFAULT_OPTIONS,
2401
+ ...options,
2402
+ notes: {
2403
+ ...DEFAULT_OPTIONS.notes,
2404
+ ...options.notes || {}
2405
+ }
2406
+ };
2407
+ this.breadcrumbs = new BreadcrumbBuffer(this.options.maxBreadcrumbs);
2408
+ this.transport = new WebSocketTransport({
2409
+ url: this.options.daemonUrl,
2410
+ projectId: this.options.projectId,
2411
+ debug: this.options.debug
2412
+ });
2413
+ this.screenshotDriver = new BrowserScriptScreenshotDriver();
2414
+ this.commandHandler = new ClientCommandHandler(this.screenshotDriver);
2415
+ this.transport.setCommandHandler(this.commandHandler);
2416
+ if (this.options.notes.enabled) {
2417
+ this.inspector = new NoteInspector(this.transport, this.screenshotDriver, {
2418
+ shortcut: this.options.notes.shortcut,
2419
+ maskSelectors: this.options.notes.maskSelectors
2420
+ });
2421
+ }
2422
+ }
2423
+ init() {
2424
+ if (this.isInitialized || typeof window === "undefined") return this;
2425
+ this.isInitialized = true;
2426
+ try {
2427
+ this.currentRoute = window.location.pathname + window.location.search;
2428
+ } catch {
2429
+ this.currentRoute = "/";
2430
+ }
2431
+ this.transport.connect();
2432
+ if (this.options.captureErrors) {
2433
+ const cleanupRuntime = setupRuntimeInterceptors(async (errEvent) => {
2434
+ await this.handleRuntimeError(errEvent);
2435
+ });
2436
+ this.cleanups.push(cleanupRuntime);
2437
+ }
2438
+ if (this.options.captureConsole) {
2439
+ const cleanupConsole = setupConsoleInterceptors(async (consoleEvent) => {
2440
+ await this.handleConsole(consoleEvent);
2441
+ });
2442
+ this.cleanups.push(cleanupConsole);
2443
+ }
2444
+ if (this.options.captureNetwork) {
2445
+ const cleanupNetwork = setupNetworkInterceptors((netEvent) => {
2446
+ this.handleNetwork(netEvent);
2447
+ });
2448
+ this.cleanups.push(cleanupNetwork);
2449
+ }
2450
+ if (this.options.captureNavigation) {
2451
+ const cleanupNav = setupNavigationInterceptors((navEvent) => {
2452
+ this.handleNavigation(navEvent);
2453
+ });
2454
+ this.cleanups.push(cleanupNav);
2455
+ }
2456
+ if (this.options.captureInteractions) {
2457
+ const cleanupInteraction = setupInteractionInterceptors((breadcrumb, elSummary) => {
2458
+ if (elSummary) {
2459
+ this.lastElement = elSummary;
2460
+ }
2461
+ this.breadcrumbs.add(breadcrumb);
2462
+ });
2463
+ this.cleanups.push(cleanupInteraction);
2464
+ }
2465
+ if (this.inspector) {
2466
+ this.inspector.init();
2467
+ }
2468
+ return this;
2469
+ }
2470
+ openNoteEditor(el, noteType = "element") {
2471
+ if (this.inspector) {
2472
+ this.inspector.openNoteEditor(el, noteType);
2473
+ }
2474
+ }
2475
+ startRegionSelection() {
2476
+ if (this.inspector) {
2477
+ this.inspector.setMode("region");
2478
+ }
2479
+ }
2480
+ startElementSelection() {
2481
+ if (this.inspector) {
2482
+ this.inspector.setMode("element");
2483
+ }
2484
+ }
2485
+ setInspectMode(mode) {
2486
+ if (this.inspector) {
2487
+ this.inspector.setMode(mode);
2488
+ }
2489
+ }
2490
+ async createNote(targetEl, message, noteType = "element") {
2491
+ if (this.inspector) {
2492
+ await this.inspector.saveVisualNote(targetEl, message, noteType);
2493
+ }
2494
+ }
2495
+ async handleRuntimeError(errorEvent) {
2496
+ this.breadcrumbs.add({
2497
+ type: "error",
2498
+ category: "runtime",
2499
+ message: `${errorEvent.errorType}: ${errorEvent.message}`,
2500
+ timestamp: errorEvent.timestamp,
2501
+ level: "error",
2502
+ data: {
2503
+ filename: errorEvent.filename,
2504
+ lineno: errorEvent.lineno,
2505
+ colno: errorEvent.colno
2506
+ }
2507
+ });
2508
+ let screenshotDataUrl;
2509
+ if (this.options.onErrorScreenshot && this.lastElement?.selector) {
2510
+ try {
2511
+ const snap = await this.screenshotDriver.captureSelector(this.lastElement.selector);
2512
+ if (snap.ok) {
2513
+ screenshotDataUrl = snap.dataUrl;
2514
+ }
2515
+ } catch {
2516
+ }
2517
+ }
2518
+ this.sendEvent("runtime_error", errorEvent, screenshotDataUrl);
2519
+ }
2520
+ async handleConsole(consoleEvent) {
2521
+ this.breadcrumbs.add({
2522
+ type: "console",
2523
+ category: "console",
2524
+ message: `console.${consoleEvent.level}: ${consoleEvent.message}`,
2525
+ timestamp: consoleEvent.timestamp,
2526
+ level: consoleEvent.level === "error" ? "error" : consoleEvent.level === "warn" ? "warn" : "info"
2527
+ });
2528
+ if (consoleEvent.level === "error" || consoleEvent.level === "warn") {
2529
+ this.sendEvent("console", consoleEvent);
2530
+ }
2531
+ }
2532
+ handleNetwork(netEvent) {
2533
+ const isError = netEvent.status && netEvent.status >= 400 || !!netEvent.error;
2534
+ const msg = `${netEvent.method} ${netEvent.url} -> ${netEvent.status || "ERR"} (${netEvent.durationMs}ms)`;
2535
+ this.breadcrumbs.add({
2536
+ type: netEvent.id?.startsWith("xhr") ? "xhr" : "fetch",
2537
+ category: "network",
2538
+ message: msg,
2539
+ timestamp: netEvent.timestamp,
2540
+ level: isError ? "error" : "info",
2541
+ data: {
2542
+ status: netEvent.status,
2543
+ durationMs: netEvent.durationMs,
2544
+ error: netEvent.error,
2545
+ aborted: netEvent.aborted
2546
+ }
2547
+ });
2548
+ this.sendEvent("fetch", netEvent);
2549
+ }
2550
+ handleNavigation(navEvent) {
2551
+ this.currentRoute = navEvent.to;
2552
+ this.breadcrumbs.add({
2553
+ type: "navigation",
2554
+ category: "navigation",
2555
+ message: `navigate ${navEvent.to} (${navEvent.type})`,
2556
+ timestamp: navEvent.timestamp,
2557
+ level: "info",
2558
+ data: { from: navEvent.from, to: navEvent.to, type: navEvent.type }
2559
+ });
2560
+ this.sendEvent("navigation", navEvent);
2561
+ }
2562
+ sendEvent(eventType, payload, screenshotDataUrl) {
2563
+ if (typeof window === "undefined") return;
2564
+ const message = {
2565
+ type: "event",
2566
+ sessionId: this.transport.getSessionId() || "",
2567
+ eventType,
2568
+ payload,
2569
+ breadcrumbs: this.breadcrumbs.getRecent(),
2570
+ lastElement: this.lastElement,
2571
+ route: this.currentRoute,
2572
+ url: window.location.href,
2573
+ title: document.title,
2574
+ timestamp: Date.now(),
2575
+ screenshot: screenshotDataUrl
2576
+ };
2577
+ this.transport.send(message);
2578
+ }
2579
+ getBreadcrumbs() {
2580
+ return this.breadcrumbs.getRecent();
2581
+ }
2582
+ getLastElement() {
2583
+ return this.lastElement;
2584
+ }
2585
+ destroy() {
2586
+ for (const cleanup of this.cleanups) {
2587
+ try {
2588
+ cleanup();
2589
+ } catch {
2590
+ }
2591
+ }
2592
+ this.cleanups = [];
2593
+ this.transport.destroy();
2594
+ if (this.inspector) {
2595
+ this.inspector.destroy();
2596
+ }
2597
+ this.breadcrumbs.clear();
2598
+ this.isInitialized = false;
2599
+ }
2600
+ };
2601
+
2602
+ // packages/client/src/index.ts
2603
+ var defaultClient = null;
2604
+ function init(options = {}) {
2605
+ if (defaultClient) {
2606
+ return defaultClient;
2607
+ }
2608
+ defaultClient = new BrowserTrackClient(options);
2609
+ defaultClient.init();
2610
+ return defaultClient;
2611
+ }
2612
+ function getClient() {
2613
+ return defaultClient;
2614
+ }
2615
+ if (typeof window !== "undefined") {
2616
+ window.__BROWSERTRACK__ = {
2617
+ init,
2618
+ getClient
2619
+ };
2620
+ try {
2621
+ const currentScript = document.currentScript;
2622
+ const autoInit = currentScript?.getAttribute("data-auto-init") !== "false";
2623
+ const daemonUrl = currentScript?.getAttribute("data-daemon-url") || void 0;
2624
+ const projectId = currentScript?.getAttribute("data-project-id") || void 0;
2625
+ if (autoInit) {
2626
+ init({ daemonUrl, projectId });
2627
+ }
2628
+ } catch {
2629
+ }
2630
+ }
2631
+
2632
+ export {
2633
+ BreadcrumbBuffer,
2634
+ DEFAULT_OPTIONS,
2635
+ BrowserScriptScreenshotDriver,
2636
+ NoteInspector,
2637
+ BrowserTrackClient,
2638
+ init,
2639
+ getClient
2640
+ };
2641
+ //# sourceMappingURL=chunk-SKCMT2DE.js.map