snoop-ui 0.0.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.
@@ -0,0 +1,751 @@
1
+ // ../core/src/styles/selector.ts
2
+ /** Split a selector list on top-level commas, ignoring commas in functions. */
3
+ function splitSelectorList(selector) {
4
+ const parts = [];
5
+ let current = "";
6
+ let quote;
7
+ let brackets = 0;
8
+ let parentheses = 0;
9
+ for (let index = 0; index < selector.length; index += 1) {
10
+ const character = selector[index];
11
+ if (quote) {
12
+ current += character;
13
+ if (character === "\\") {
14
+ const next = selector[index + 1];
15
+ if (next !== void 0) {
16
+ current += next;
17
+ index += 1;
18
+ }
19
+ } else if (character === quote) quote = void 0;
20
+ continue;
21
+ }
22
+ if (character === "\"" || character === "'") {
23
+ quote = character;
24
+ current += character;
25
+ continue;
26
+ }
27
+ if (character === "[") brackets += 1;
28
+ else if (character === "]") brackets = Math.max(0, brackets - 1);
29
+ else if (character === "(") parentheses += 1;
30
+ else if (character === ")") parentheses = Math.max(0, parentheses - 1);
31
+ if (character === "," && brackets === 0 && parentheses === 0) {
32
+ parts.push(current);
33
+ current = "";
34
+ continue;
35
+ }
36
+ current += character;
37
+ }
38
+ parts.push(current);
39
+ return parts;
40
+ }
41
+ // ../core/src/runtime/measure.ts
42
+ /**
43
+ * Computed style properties captured for the selected element. The list is
44
+ * deliberately small: the agent sends full style detail for one element at a
45
+ * time, and the style resolver (M4) adds provenance on top of these values.
46
+ */
47
+ var STYLE_PROPERTIES = [
48
+ "display",
49
+ "position",
50
+ "top",
51
+ "right",
52
+ "bottom",
53
+ "left",
54
+ "width",
55
+ "height",
56
+ "min-width",
57
+ "min-height",
58
+ "max-width",
59
+ "max-height",
60
+ "margin-top",
61
+ "margin-right",
62
+ "margin-bottom",
63
+ "margin-left",
64
+ "padding-top",
65
+ "padding-right",
66
+ "padding-bottom",
67
+ "padding-left",
68
+ "border-top-width",
69
+ "border-right-width",
70
+ "border-bottom-width",
71
+ "border-left-width",
72
+ "border-radius",
73
+ "border-top-left-radius",
74
+ "border-top-right-radius",
75
+ "border-bottom-right-radius",
76
+ "border-bottom-left-radius",
77
+ "box-sizing",
78
+ "overflow",
79
+ "z-index",
80
+ "opacity",
81
+ "color",
82
+ "background-color",
83
+ "box-shadow",
84
+ "font-family",
85
+ "font-size",
86
+ "font-weight",
87
+ "line-height",
88
+ "letter-spacing",
89
+ "text-align",
90
+ "text-transform",
91
+ "white-space",
92
+ "flex-direction",
93
+ "flex-wrap",
94
+ "justify-content",
95
+ "align-items",
96
+ "gap",
97
+ "grid-template-columns",
98
+ "grid-template-rows",
99
+ "transform"
100
+ ];
101
+ /**
102
+ * Collect the geometry of every tagged element that is currently rendered.
103
+ *
104
+ * Only visible elements are measured: the agent runs inside the host app and
105
+ * must stay cheap.
106
+ */
107
+ function collectMeasuredElements(doc, view) {
108
+ const elements = doc.querySelectorAll("[data-snoop-src]");
109
+ const measured = [];
110
+ for (const element of elements) {
111
+ const src = element.getAttribute("data-snoop-src");
112
+ if (!src || !isVisible(element, view)) continue;
113
+ measured.push({
114
+ src,
115
+ tag: element.tagName.toLowerCase(),
116
+ rect: toRect(element.getBoundingClientRect())
117
+ });
118
+ }
119
+ return measured;
120
+ }
121
+ /** Capture the full detail of a single element. */
122
+ function describeElement(element, view) {
123
+ const src = element.getAttribute("data-snoop-src");
124
+ if (!src) return;
125
+ const computed = view.getComputedStyle(element);
126
+ const style = {};
127
+ for (const property of STYLE_PROPERTIES) {
128
+ const value = computed.getPropertyValue(property);
129
+ if (value) style[property] = value;
130
+ }
131
+ return {
132
+ src,
133
+ tag: element.tagName.toLowerCase(),
134
+ rect: toRect(element.getBoundingClientRect()),
135
+ style,
136
+ classes: classNames(element),
137
+ inlineStyle: collectInlineStyle(element),
138
+ matchedRules: collectMatchedRules(element, documentOf(element, view))
139
+ };
140
+ }
141
+ /**
142
+ * Read every stylesheet rule that matches the element. The browser CSSOM is the
143
+ * source of truth; the static index only labels where the winning rule lives.
144
+ */
145
+ function collectMatchedRules(element, doc) {
146
+ if (!doc?.styleSheets) return [];
147
+ const matched = [];
148
+ let order = 0;
149
+ const walk = (rules, atRules) => {
150
+ if (!rules) return;
151
+ for (const rule of Array.from(rules)) {
152
+ if (matched.length >= 250) return;
153
+ const styleRule = rule;
154
+ if (typeof styleRule.selectorText === "string" && styleRule.style) {
155
+ const currentOrder = order;
156
+ order += 1;
157
+ if (!matchesAny(element, styleRule.selectorText)) continue;
158
+ const declarations = matchedDeclarations(styleRule.style);
159
+ if (declarations.length > 0) matched.push({
160
+ selector: styleRule.selectorText,
161
+ declarations,
162
+ order: currentOrder,
163
+ atRules
164
+ });
165
+ continue;
166
+ }
167
+ const grouping = rule;
168
+ if (grouping.cssRules) {
169
+ const label = groupingLabel(rule);
170
+ walk(grouping.cssRules, label ? [...atRules, label] : atRules);
171
+ }
172
+ }
173
+ };
174
+ for (const sheet of Array.from(doc.styleSheets)) {
175
+ if (sheet.disabled) continue;
176
+ let rules;
177
+ try {
178
+ rules = sheet.cssRules;
179
+ } catch {
180
+ continue;
181
+ }
182
+ walk(rules, []);
183
+ }
184
+ return matched;
185
+ }
186
+ function matchedDeclarations(style) {
187
+ const declarations = [];
188
+ for (const property of STYLE_PROPERTIES) {
189
+ const value = style.getPropertyValue(property);
190
+ if (!value) continue;
191
+ declarations.push({
192
+ property,
193
+ value,
194
+ important: style.getPropertyPriority(property) === "important"
195
+ });
196
+ }
197
+ return declarations;
198
+ }
199
+ function collectInlineStyle(element) {
200
+ const style = element.style;
201
+ if (!style || typeof style.getPropertyValue !== "function") return {};
202
+ const declarations = {};
203
+ for (const property of STYLE_PROPERTIES) {
204
+ const value = style.getPropertyValue(property);
205
+ if (value) declarations[property] = value;
206
+ }
207
+ return declarations;
208
+ }
209
+ function matchesAny(element, selector) {
210
+ for (const part of splitSelectorList(selector)) {
211
+ const candidate = part.trim();
212
+ if (!candidate) continue;
213
+ try {
214
+ if (element.matches(candidate)) return true;
215
+ } catch {}
216
+ }
217
+ return false;
218
+ }
219
+ function groupingLabel(rule) {
220
+ const media = rule.media;
221
+ if (media && typeof media.mediaText === "string") return `@media ${media.mediaText}`;
222
+ const condition = rule.conditionText;
223
+ if (typeof condition === "string") return (rule.constructor?.name ?? "").includes("Container") ? `@container ${condition}` : `@supports ${condition}`;
224
+ const layer = rule.name;
225
+ if (typeof layer === "string" && layer.length > 0) return `@layer ${layer}`;
226
+ }
227
+ function classNames(element) {
228
+ const value = element.getAttribute("class");
229
+ return value ? value.split(/\s+/).filter(Boolean) : [];
230
+ }
231
+ function documentOf(element, view) {
232
+ return element.ownerDocument ?? view.document;
233
+ }
234
+ /** An element counts as rendered when it has a box and is not hidden. */
235
+ function isVisible(element, view) {
236
+ const rect = element.getBoundingClientRect();
237
+ if (rect.width <= 0 || rect.height <= 0) return false;
238
+ const computed = view.getComputedStyle(element);
239
+ if (computed.getPropertyValue("display") === "none") return false;
240
+ const visibility = computed.getPropertyValue("visibility");
241
+ if (visibility === "hidden" || visibility === "collapse") return false;
242
+ if (computed.getPropertyValue("opacity") === "0") return false;
243
+ return true;
244
+ }
245
+ /** Walk up from an event target to the closest tagged element. */
246
+ function findSourceElement(target) {
247
+ let current = target;
248
+ while (current && typeof current.getAttribute === "function") {
249
+ if (current.getAttribute("data-snoop-src")) return current;
250
+ current = current.parentElement ?? null;
251
+ }
252
+ }
253
+ function toRect(rect) {
254
+ return {
255
+ x: round$1(rect.x),
256
+ y: round$1(rect.y),
257
+ width: round$1(rect.width),
258
+ height: round$1(rect.height),
259
+ top: round$1(rect.top),
260
+ left: round$1(rect.left),
261
+ right: round$1(rect.right),
262
+ bottom: round$1(rect.bottom)
263
+ };
264
+ }
265
+ function round$1(value) {
266
+ return Math.round(value * 100) / 100;
267
+ }
268
+ // ../core/src/runtime/overlay.ts
269
+ var HOVER_COLOR = "#6366f1";
270
+ var SELECTED_COLOR = "#16a34a";
271
+ var FOCUS_DIM = "rgba(17, 17, 41, 0.55)";
272
+ var ISOLATE_BACKDROP = "#f4f2fb";
273
+ var SVG_NS = "http://www.w3.org/2000/svg";
274
+ var DIM_MASK_ID = "snoop-dim-mask";
275
+ /**
276
+ * A layer over the whole viewport with the selected rectangles cut out. A mask
277
+ * is used instead of per-box shadows because overlapping or nested selections
278
+ * must stay clear.
279
+ */
280
+ function createDim(doc) {
281
+ const svg = doc.createElementNS(SVG_NS, "svg");
282
+ svg.setAttribute("aria-hidden", "true");
283
+ svg.style.cssText = [
284
+ "position:fixed",
285
+ "top:0",
286
+ "left:0",
287
+ "width:100vw",
288
+ "height:100vh",
289
+ "display:none",
290
+ "pointer-events:none"
291
+ ].join(";");
292
+ const defs = doc.createElementNS(SVG_NS, "defs");
293
+ const mask = doc.createElementNS(SVG_NS, "mask");
294
+ mask.setAttribute("id", DIM_MASK_ID);
295
+ mask.setAttribute("maskUnits", "userSpaceOnUse");
296
+ mask.setAttribute("x", "0");
297
+ mask.setAttribute("y", "0");
298
+ mask.setAttribute("width", "100%");
299
+ mask.setAttribute("height", "100%");
300
+ const visible = doc.createElementNS(SVG_NS, "rect");
301
+ visible.setAttribute("width", "100%");
302
+ visible.setAttribute("height", "100%");
303
+ visible.setAttribute("fill", "#fff");
304
+ const holes = doc.createElementNS(SVG_NS, "g");
305
+ holes.setAttribute("fill", "#000");
306
+ mask.append(visible, holes);
307
+ defs.append(mask);
308
+ const layer = doc.createElementNS(SVG_NS, "rect");
309
+ layer.setAttribute("width", "100%");
310
+ layer.setAttribute("height", "100%");
311
+ layer.setAttribute("mask", `url(#${DIM_MASK_ID})`);
312
+ svg.append(defs, layer);
313
+ return {
314
+ svg,
315
+ update(rects, fill) {
316
+ holes.replaceChildren();
317
+ for (const rect of rects) {
318
+ const hole = doc.createElementNS(SVG_NS, "rect");
319
+ hole.setAttribute("x", String(rect.left));
320
+ hole.setAttribute("y", String(rect.top));
321
+ hole.setAttribute("width", String(rect.width));
322
+ hole.setAttribute("height", String(rect.height));
323
+ holes.append(hole);
324
+ }
325
+ layer.setAttribute("fill", fill);
326
+ svg.style.display = rects.length > 0 ? "block" : "none";
327
+ }
328
+ };
329
+ }
330
+ /**
331
+ * Draw the inspection overlay inside the host app document.
332
+ *
333
+ * The overlay never receives pointer events, so the app stays usable while it
334
+ * is shown. Clicks are intercepted separately by the agent when inspect mode
335
+ * is on.
336
+ */
337
+ function createOverlay(doc) {
338
+ const root = doc.createElement("div");
339
+ root.setAttribute("data-snoop-overlay", "");
340
+ root.setAttribute("aria-hidden", "true");
341
+ root.style.cssText = [
342
+ "position:fixed",
343
+ "top:0",
344
+ "left:0",
345
+ "width:0",
346
+ "height:0",
347
+ "z-index:2147483647",
348
+ "pointer-events:none",
349
+ "contain:layout style size"
350
+ ].join(";");
351
+ const dim = createDim(doc);
352
+ const hover = createMarker(doc, HOVER_COLOR);
353
+ const selected = [];
354
+ let items = [];
355
+ let isolated = false;
356
+ root.append(dim.svg, hover.box, hover.label);
357
+ doc.body?.append(root);
358
+ function place(marker, rect, label) {
359
+ marker.box.style.top = `${rect.top}px`;
360
+ marker.box.style.left = `${rect.left}px`;
361
+ marker.box.style.width = `${rect.width}px`;
362
+ marker.box.style.height = `${rect.height}px`;
363
+ marker.box.style.display = "block";
364
+ marker.label.textContent = label;
365
+ marker.label.style.display = "block";
366
+ const labelTop = rect.top - 20 < 0 ? rect.top + 2 : rect.top - 20;
367
+ marker.label.style.top = `${labelTop}px`;
368
+ marker.label.style.left = `${rect.left}px`;
369
+ }
370
+ function hide(marker) {
371
+ marker.box.style.display = "none";
372
+ marker.label.style.display = "none";
373
+ }
374
+ function removeMarkers() {
375
+ for (const marker of selected) {
376
+ hide(marker);
377
+ marker.box.remove();
378
+ marker.label.remove();
379
+ }
380
+ selected.length = 0;
381
+ }
382
+ function render() {
383
+ removeMarkers();
384
+ if (!isolated) for (const item of items) {
385
+ const marker = createMarker(doc, SELECTED_COLOR);
386
+ root.append(marker.box, marker.label);
387
+ selected.push(marker);
388
+ place(marker, item.rect, item.label);
389
+ }
390
+ dim.update(items.map((item) => item.rect), isolated ? ISOLATE_BACKDROP : FOCUS_DIM);
391
+ }
392
+ function clearSelected() {
393
+ items = [];
394
+ render();
395
+ }
396
+ return {
397
+ showHover(item) {
398
+ if (!item) {
399
+ hide(hover);
400
+ return;
401
+ }
402
+ place(hover, item.rect, item.label);
403
+ },
404
+ showSelected(next) {
405
+ items = next;
406
+ render();
407
+ },
408
+ setIsolated(next) {
409
+ isolated = next;
410
+ render();
411
+ },
412
+ clearHover() {
413
+ hide(hover);
414
+ },
415
+ clearSelected,
416
+ destroy() {
417
+ removeMarkers();
418
+ hover.box.remove();
419
+ hover.label.remove();
420
+ dim.svg.remove();
421
+ root.remove();
422
+ }
423
+ };
424
+ }
425
+ function createMarker(doc, color) {
426
+ const box = doc.createElement("div");
427
+ box.style.cssText = [
428
+ "position:fixed",
429
+ "display:none",
430
+ "box-sizing:border-box",
431
+ "pointer-events:none",
432
+ `border:2px solid ${color}`,
433
+ `background:${color}1f`,
434
+ "border-radius:2px"
435
+ ].join(";");
436
+ const label = doc.createElement("div");
437
+ label.style.cssText = [
438
+ "position:fixed",
439
+ "display:none",
440
+ "pointer-events:none",
441
+ "white-space:nowrap",
442
+ "font:11px/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",
443
+ `background:${color}`,
444
+ "color:#ffffff",
445
+ "padding:2px 5px",
446
+ "border-radius:4px",
447
+ "max-width:60vw",
448
+ "overflow:hidden",
449
+ "text-overflow:ellipsis"
450
+ ].join(";");
451
+ return {
452
+ box,
453
+ label
454
+ };
455
+ }
456
+ // ../core/src/runtime/protocol.ts
457
+ function isDashboardMessage(value) {
458
+ if (!isRecord(value) || typeof value.kind !== "string") return false;
459
+ switch (value.kind) {
460
+ case "measure":
461
+ case "inspect":
462
+ case "isolate":
463
+ case "select":
464
+ case "sync": return true;
465
+ default: return false;
466
+ }
467
+ }
468
+ function isRecord(value) {
469
+ return typeof value === "object" && value !== null;
470
+ }
471
+ // src/agent.ts
472
+ var SESSION_ENDPOINT = "/__snoop/session";
473
+ var WS_ENDPOINT = "/__snoop/ws";
474
+ var MEASURE_DEBOUNCE = 120;
475
+ var RECONNECT_DELAY = 2e3;
476
+ /**
477
+ * Start the runtime agent inside the host app.
478
+ *
479
+ * The agent is injected by the Vite plugin in dev mode only. It stays idle
480
+ * until the dashboard asks for measurements, so a dev server without an open
481
+ * dashboard costs one WebSocket and nothing else.
482
+ */
483
+ function startAgent(options = {}) {
484
+ if (typeof window === "undefined" || typeof document === "undefined") return;
485
+ run(options);
486
+ }
487
+ async function run(options) {
488
+ const sessionEndpoint = options.sessionEndpoint ?? SESSION_ENDPOINT;
489
+ const websocketEndpoint = options.websocketEndpoint ?? WS_ENDPOINT;
490
+ const token = await fetchToken(sessionEndpoint);
491
+ if (!token) return;
492
+ const sessionToken = token;
493
+ const state = {
494
+ socket: null,
495
+ overlay: null,
496
+ measureEnabled: false,
497
+ inspectEnabled: false,
498
+ selectedSrcs: [],
499
+ hoverSrc: null,
500
+ elements: [],
501
+ reconnectTimer: void 0,
502
+ measureTimer: void 0,
503
+ observer: void 0,
504
+ listening: false,
505
+ isolateEnabled: false
506
+ };
507
+ function send(message) {
508
+ const socket = state.socket;
509
+ if (!socket || socket.readyState !== WebSocket.OPEN) return;
510
+ socket.send(JSON.stringify({
511
+ type: "relay",
512
+ payload: message
513
+ }));
514
+ }
515
+ function sendElements() {
516
+ if (!state.measureEnabled) return;
517
+ const elements = collectMeasuredElements(document, window);
518
+ state.elements = elements;
519
+ send({
520
+ kind: "elements",
521
+ elements
522
+ });
523
+ }
524
+ function sendDetail(src) {
525
+ const element = findElementBySource(src);
526
+ if (!element) return;
527
+ const detail = describeElement(element, window);
528
+ if (detail) send({
529
+ kind: "detail",
530
+ detail
531
+ });
532
+ }
533
+ function ensureOverlay() {
534
+ if (!state.overlay) state.overlay = createOverlay(document);
535
+ return state.overlay;
536
+ }
537
+ function showSelected(srcs) {
538
+ const items = [];
539
+ for (const src of srcs) for (const element of findElementsBySource(src)) {
540
+ const rect = toOverlayRect(element);
541
+ if (rect.width > 0 && rect.height > 0) items.push({
542
+ rect,
543
+ label: src
544
+ });
545
+ }
546
+ ensureOverlay().showSelected(items);
547
+ }
548
+ function showHover(src) {
549
+ const overlay = ensureOverlay();
550
+ if (!src) {
551
+ overlay.showHover(void 0);
552
+ return;
553
+ }
554
+ const element = findElementBySource(src);
555
+ if (!element) {
556
+ overlay.showHover(void 0);
557
+ return;
558
+ }
559
+ overlay.showHover({
560
+ rect: toOverlayRect(element),
561
+ label: src
562
+ });
563
+ }
564
+ function reveal(src) {
565
+ const element = findElementBySource(src);
566
+ if (!element) return;
567
+ const reduceMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
568
+ element.scrollIntoView({
569
+ block: "center",
570
+ inline: "center",
571
+ behavior: reduceMotion ? "auto" : "smooth"
572
+ });
573
+ }
574
+ let refreshFrame = 0;
575
+ function refreshSelected() {
576
+ if (refreshFrame !== 0 || state.selectedSrcs.length === 0) return;
577
+ refreshFrame = window.requestAnimationFrame(() => {
578
+ refreshFrame = 0;
579
+ showSelected(state.selectedSrcs);
580
+ });
581
+ }
582
+ window.addEventListener("scroll", refreshSelected, true);
583
+ window.addEventListener("resize", refreshSelected);
584
+ function scheduleMeasure() {
585
+ if (state.measureTimer !== void 0) window.clearTimeout(state.measureTimer);
586
+ state.measureTimer = window.setTimeout(() => {
587
+ state.measureTimer = void 0;
588
+ sendElements();
589
+ }, MEASURE_DEBOUNCE);
590
+ }
591
+ function startMeasureListeners() {
592
+ if (state.listening) return;
593
+ state.listening = true;
594
+ window.addEventListener("scroll", scheduleMeasure, true);
595
+ window.addEventListener("resize", scheduleMeasure);
596
+ state.observer = new MutationObserver(scheduleMeasure);
597
+ state.observer.observe(document.documentElement, {
598
+ childList: true,
599
+ subtree: true
600
+ });
601
+ }
602
+ function stopMeasureListeners() {
603
+ if (!state.listening) return;
604
+ state.listening = false;
605
+ window.removeEventListener("scroll", scheduleMeasure, true);
606
+ window.removeEventListener("resize", scheduleMeasure);
607
+ state.observer?.disconnect();
608
+ state.observer = void 0;
609
+ }
610
+ function onClick(event) {
611
+ const src = findSourceElement(event.target)?.getAttribute("data-snoop-src");
612
+ if (!src) return;
613
+ event.preventDefault();
614
+ event.stopPropagation();
615
+ state.selectedSrcs = [src];
616
+ showSelected(state.selectedSrcs);
617
+ send({
618
+ kind: "click",
619
+ src
620
+ });
621
+ }
622
+ let hoverFrame = 0;
623
+ function onMouseMove(event) {
624
+ if (hoverFrame) return;
625
+ const target = event.target;
626
+ hoverFrame = window.requestAnimationFrame(() => {
627
+ hoverFrame = 0;
628
+ const src = findSourceElement(target)?.getAttribute("data-snoop-src") ?? null;
629
+ if (src === state.hoverSrc) return;
630
+ state.hoverSrc = src;
631
+ showHover(src);
632
+ send({
633
+ kind: "hover",
634
+ src
635
+ });
636
+ });
637
+ }
638
+ function handleDashboardMessage(message) {
639
+ switch (message.kind) {
640
+ case "measure":
641
+ state.measureEnabled = message.enabled;
642
+ if (message.enabled) {
643
+ startMeasureListeners();
644
+ sendElements();
645
+ } else stopMeasureListeners();
646
+ break;
647
+ case "inspect":
648
+ state.inspectEnabled = message.enabled;
649
+ if (message.enabled) {
650
+ document.addEventListener("click", onClick, true);
651
+ document.addEventListener("mousemove", onMouseMove, true);
652
+ } else {
653
+ document.removeEventListener("click", onClick, true);
654
+ document.removeEventListener("mousemove", onMouseMove, true);
655
+ state.hoverSrc = null;
656
+ showHover(null);
657
+ }
658
+ break;
659
+ case "select": {
660
+ state.selectedSrcs = message.srcs;
661
+ const first = message.srcs[0];
662
+ if (first) {
663
+ showSelected(message.srcs);
664
+ sendDetail(first);
665
+ if (message.reveal) reveal(first);
666
+ } else ensureOverlay().showSelected([]);
667
+ break;
668
+ }
669
+ case "isolate": {
670
+ state.isolateEnabled = message.enabled;
671
+ ensureOverlay().setIsolated(message.enabled);
672
+ showSelected(state.selectedSrcs);
673
+ const first = state.selectedSrcs[0];
674
+ if (message.enabled && first) reveal(first);
675
+ break;
676
+ }
677
+ case "sync": {
678
+ send({ kind: "ready" });
679
+ sendElements();
680
+ const first = state.selectedSrcs[0];
681
+ if (first) sendDetail(first);
682
+ break;
683
+ }
684
+ }
685
+ }
686
+ function connect() {
687
+ const url = new URL(websocketEndpoint, window.location.href);
688
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
689
+ url.searchParams.set("token", sessionToken);
690
+ url.searchParams.set("role", "agent");
691
+ const socket = new WebSocket(url);
692
+ state.socket = socket;
693
+ socket.addEventListener("open", () => {
694
+ send({ kind: "ready" });
695
+ sendElements();
696
+ });
697
+ socket.addEventListener("message", (event) => {
698
+ let payload;
699
+ try {
700
+ payload = JSON.parse(String(event.data));
701
+ } catch {
702
+ return;
703
+ }
704
+ if (typeof payload === "object" && payload !== null && payload.type === "relay" && isDashboardMessage(payload.payload)) handleDashboardMessage(payload.payload);
705
+ });
706
+ socket.addEventListener("close", () => {
707
+ state.socket = null;
708
+ state.reconnectTimer = window.setTimeout(connect, RECONNECT_DELAY);
709
+ });
710
+ }
711
+ connect();
712
+ }
713
+ async function fetchToken(endpoint) {
714
+ try {
715
+ const response = await fetch(endpoint, {
716
+ headers: { Accept: "application/json" },
717
+ cache: "no-store"
718
+ });
719
+ if (!response.ok) return;
720
+ const session = await response.json();
721
+ return typeof session.token === "string" ? session.token : void 0;
722
+ } catch {
723
+ return;
724
+ }
725
+ }
726
+ function findElementBySource(src) {
727
+ const elements = document.querySelectorAll("[data-snoop-src]");
728
+ for (const element of elements) if (element.getAttribute("data-snoop-src") === src) return element;
729
+ }
730
+ function findElementsBySource(src) {
731
+ return Array.from(document.querySelectorAll("[data-snoop-src]")).filter((element) => element.getAttribute("data-snoop-src") === src);
732
+ }
733
+ function toOverlayRect(element) {
734
+ const rect = element.getBoundingClientRect();
735
+ return {
736
+ x: round(rect.x),
737
+ y: round(rect.y),
738
+ width: round(rect.width),
739
+ height: round(rect.height),
740
+ top: round(rect.top),
741
+ left: round(rect.left),
742
+ right: round(rect.right),
743
+ bottom: round(rect.bottom)
744
+ };
745
+ }
746
+ function round(value) {
747
+ return Math.round(value * 100) / 100;
748
+ }
749
+ export { startAgent };
750
+
751
+ //# sourceMappingURL=agent.js.map