@atolljs/devtools 0.1.7 → 0.1.8

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 (48) hide show
  1. package/README.md +408 -71
  2. package/app/api.js +68 -0
  3. package/app/index.html +161 -2
  4. package/app/main.js +877 -127
  5. package/app/panels/actions.js +324 -0
  6. package/app/panels/audit-rules.js +728 -0
  7. package/app/panels/audits.js +438 -0
  8. package/app/panels/inspect-island.js +351 -0
  9. package/app/panels/memory-values.js +287 -0
  10. package/app/panels/performance.js +304 -0
  11. package/app/panels/reactivity.js +271 -0
  12. package/app/panels/recorder.js +344 -0
  13. package/app/panels/recording-format.js +166 -0
  14. package/app/panels/route.d.ts +44 -0
  15. package/app/panels/route.js +264 -0
  16. package/app/panels/shell.js +789 -0
  17. package/app/panels/toast.js +38 -0
  18. package/dist/bin.js +704 -5
  19. package/dist/chunk-57NGHQUL.js +344 -0
  20. package/dist/{chunk-4N3NBFL3.js → chunk-AGT6VNZI.js} +179 -217
  21. package/dist/chunk-WIL2P7HG.js +661 -0
  22. package/dist/client.d.ts +7 -0
  23. package/dist/control.d.ts +4 -0
  24. package/dist/index.js +2 -1
  25. package/dist/node.js +2 -1
  26. package/dist/nodeWs.d.ts +1 -0
  27. package/dist/otel.d.ts +35 -0
  28. package/dist/otel.js +87 -0
  29. package/dist/otelMap.d.ts +232 -0
  30. package/dist/otelTransport.d.ts +69 -0
  31. package/dist/overlay.d.ts +16 -2
  32. package/dist/probes.d.ts +7 -0
  33. package/dist/protocol.d.ts +46 -4
  34. package/dist/server.d.ts +19 -0
  35. package/dist/server.js +61 -3
  36. package/package.json +6 -2
  37. package/src/bin.ts +27 -2
  38. package/src/broadcast.ts +6 -0
  39. package/src/client.ts +26 -2
  40. package/src/control.ts +51 -0
  41. package/src/nodeWs.ts +7 -1
  42. package/src/otel.ts +140 -0
  43. package/src/otelMap.ts +661 -0
  44. package/src/otelTransport.ts +286 -0
  45. package/src/overlay.ts +153 -27
  46. package/src/probes.ts +155 -0
  47. package/src/protocol.ts +52 -4
  48. package/src/server.ts +83 -3
@@ -1,199 +1,43 @@
1
- // ../../src/devtools.ts
2
- var THREAD = typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope ? "worker" : "main";
3
- var sink = null;
4
- var setDevtoolsSink = (custom) => {
5
- sink = custom;
6
- };
7
- var emitDevtools = (event) => {
8
- if (sink) sink({ ...event, at: performance.now(), thread: THREAD });
9
- };
10
- var PROBED = /* @__PURE__ */ Symbol.for("atoll.devtools.fetch");
11
- var BODY_PREVIEW = 8 * 1024;
12
- var TEXTISH = /text|json|javascript|xml|html|css|svg|csv|urlencoded|graphql/;
13
- var headerList = (h) => h ? [...h.entries()].slice(0, 64) : void 0;
14
- var bodyPreview = (body) => {
15
- if (body === null || body === void 0) return void 0;
16
- if (typeof body === "string") return body.slice(0, BODY_PREVIEW);
17
- if (body instanceof URLSearchParams) return String(body).slice(0, BODY_PREVIEW);
18
- if (body instanceof Blob) return `[${body.type || "blob"} ${body.size}B]`;
19
- if (body instanceof ArrayBuffer) return `[ArrayBuffer ${body.byteLength}B]`;
20
- if (ArrayBuffer.isView(body)) return `[${body.constructor.name} ${body.byteLength}B]`;
21
- if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
22
- return "[stream]";
23
- };
24
- var readPreview = async (res, cap) => {
25
- const reader = res.body?.getReader();
26
- if (!reader) return void 0;
27
- const chunks = [];
28
- let n = 0;
1
+ import {
2
+ installFetchProbe,
3
+ installJankProbe,
4
+ installMemoryProbe,
5
+ runDevtoolsCommand,
6
+ setDevtoolsSink
7
+ } from "./chunk-57NGHQUL.js";
8
+
9
+ // src/control.ts
10
+ var jsonSafe = (v) => {
11
+ if (v === void 0) return void 0;
29
12
  try {
30
- while (n < cap) {
31
- const { done, value } = await reader.read();
32
- if (done || !value) break;
33
- chunks.push(value);
34
- n += value.byteLength;
35
- }
13
+ return JSON.parse(JSON.stringify(v));
36
14
  } catch {
15
+ return String(v);
37
16
  }
38
- if (!n) return void 0;
39
- const all = new Uint8Array(n);
40
- let off = 0;
41
- for (const c of chunks) {
42
- all.set(c, off);
43
- off += c.byteLength;
44
- }
45
- return new TextDecoder().decode(all) + (n >= cap ? "\u2026" : "");
46
17
  };
47
- var findTiming = async (absUrl, t0) => {
48
- const perf = globalThis.performance;
49
- if (!perf?.getEntriesByType) return void 0;
50
- for (let tries = 0; tries < 12; tries++) {
51
- let best;
52
- for (const e of perf.getEntriesByType("resource")) {
53
- if (e.name !== absUrl || e.startTime < t0 - 5) continue;
54
- if (!best || e.startTime < best.startTime) best = e;
55
- }
56
- if (best) {
57
- const seg = (a, b) => b > a && a > 0 ? b - a : 0;
58
- return {
59
- queue: seg(best.fetchStart, best.domainLookupStart),
60
- dns: seg(best.domainLookupStart, best.domainLookupEnd),
61
- tcp: best.secureConnectionStart > 0 ? seg(best.connectStart, best.secureConnectionStart) : seg(best.connectStart, best.connectEnd),
62
- tls: best.secureConnectionStart > 0 ? seg(best.secureConnectionStart, best.connectEnd) : 0,
63
- wait: seg(best.requestStart, best.responseStart),
64
- download: seg(best.responseStart, best.responseEnd),
65
- transferSize: best.transferSize,
66
- encodedSize: best.encodedBodySize,
67
- decodedSize: best.decodedBodySize
68
- };
69
- }
70
- await new Promise((r) => setTimeout(r, 250));
18
+ var answerControl = async (req, reply) => {
19
+ try {
20
+ const result = await runDevtoolsCommand(req.cmd, req.args ?? {});
21
+ reply({ type: "control-result", sessionId: req.sessionId, id: req.id, ok: true, result: jsonSafe(result) });
22
+ } catch (err) {
23
+ reply({
24
+ type: "control-result",
25
+ sessionId: req.sessionId,
26
+ id: req.id,
27
+ ok: false,
28
+ error: err instanceof Error ? err.message : String(err)
29
+ });
71
30
  }
72
- return void 0;
73
31
  };
74
- var installFetchProbe = (scope) => {
75
- const original = scope.fetch;
76
- if (!original || original[PROBED]) return;
77
- let seq = 0;
78
- const wrapped = async (input, init) => {
79
- const t0 = performance.now();
80
- const id = ++seq;
81
- const isReq = typeof Request !== "undefined" && input instanceof Request;
82
- const url = isReq ? input.url : String(input);
83
- const abs = (() => {
84
- try {
85
- return new URL(url, self.location?.href).href;
86
- } catch {
87
- return url;
88
- }
89
- })();
90
- const method = (init?.method ?? (isReq ? input.method : "GET")).toUpperCase();
91
- const reqClone = isReq && !init?.body ? input.clone() : null;
92
- const reqHeaders = headerList(
93
- init?.headers ? new Headers(init.headers) : isReq ? input.headers : void 0
94
- );
95
- let reqBody = bodyPreview(init?.body);
96
- try {
97
- const res = await original.call(scope, input, init);
98
- const len = res.headers.get("content-length");
99
- emitDevtools({
100
- type: "net:fetch",
101
- id,
102
- url: url.slice(0, 300),
103
- method,
104
- status: res.status,
105
- ms: performance.now() - t0,
106
- bytes: len === null ? void 0 : Number(len),
107
- reqHeaders,
108
- reqBody
109
- });
110
- const resClone = res.clone();
111
- void (async () => {
112
- reqBody ??= reqClone ? await reqClone.text().then((t2) => t2.slice(0, BODY_PREVIEW)).catch(() => void 0) : void 0;
113
- const ct = res.headers.get("content-type") ?? "";
114
- const resBody = TEXTISH.test(ct) ? await readPreview(resClone, BODY_PREVIEW).catch(() => void 0) : void 0;
115
- const t = await findTiming(abs, t0);
116
- emitDevtools({
117
- type: "net:fetch-detail",
118
- id,
119
- reqBody,
120
- resHeaders: headerList(res.headers),
121
- resBody,
122
- timing: t,
123
- transferSize: t?.transferSize,
124
- encodedSize: t?.encodedSize,
125
- decodedSize: t?.decodedSize
126
- });
127
- })();
128
- return res;
129
- } catch (err) {
130
- emitDevtools({
131
- type: "net:fetch",
132
- id,
133
- url: url.slice(0, 300),
134
- method,
135
- ms: performance.now() - t0,
136
- error: err instanceof Error ? err.message : String(err),
137
- reqHeaders,
138
- reqBody
139
- });
140
- throw err;
141
- }
32
+ var captureEnv = () => {
33
+ const g = globalThis;
34
+ return {
35
+ crossOriginIsolated: g.crossOriginIsolated,
36
+ sharedArrayBuffer: typeof SharedArrayBuffer !== "undefined",
37
+ hardwareConcurrency: g.navigator?.hardwareConcurrency,
38
+ userAgent: g.navigator?.userAgent,
39
+ node: g.navigator ? void 0 : g.process?.version
142
40
  };
143
- wrapped[PROBED] = true;
144
- scope.fetch = wrapped;
145
- };
146
- var memProbed = /* @__PURE__ */ new WeakSet();
147
- var installMemoryProbe = (scope, intervalMs = 2500) => {
148
- if (memProbed.has(scope)) return;
149
- const perf = scope.performance;
150
- if (!perf) return;
151
- const mem = perf.memory;
152
- const inWorker = typeof WorkerGlobalScope !== "undefined" && globalThis.self instanceof WorkerGlobalScope;
153
- const measure = perf.measureUserAgentSpecificMemory?.bind(perf);
154
- const procMem = globalThis.process?.memoryUsage;
155
- if (!mem && (!measure || inWorker) && !procMem) return;
156
- memProbed.add(scope);
157
- if (measure && !inWorker) {
158
- const timer2 = setInterval(() => {
159
- measure().then((res) => {
160
- const contexts = (res.breakdown ?? []).map((b) => ({
161
- bytes: b.bytes,
162
- scope: b.attribution?.[0]?.scope,
163
- url: b.attribution?.[0]?.url
164
- })).sort((a, b) => b.bytes - a.bytes).slice(0, 24);
165
- emitDevtools({
166
- type: "runtime:memory",
167
- heapBytes: res.bytes,
168
- heapLimitBytes: mem?.jsHeapSizeLimit,
169
- contexts
170
- });
171
- }).catch(() => {
172
- if (mem) {
173
- emitDevtools({
174
- type: "runtime:memory",
175
- heapBytes: mem.usedJSHeapSize,
176
- heapLimitBytes: mem.jsHeapSizeLimit
177
- });
178
- }
179
- });
180
- }, Math.max(intervalMs, 4e3));
181
- timer2.unref?.();
182
- return;
183
- }
184
- const timer = setInterval(() => {
185
- if (mem) {
186
- emitDevtools({
187
- type: "runtime:memory",
188
- heapBytes: mem.usedJSHeapSize,
189
- heapLimitBytes: mem.jsHeapSizeLimit
190
- });
191
- } else if (procMem) {
192
- const u = procMem();
193
- emitDevtools({ type: "runtime:memory", heapBytes: u.heapUsed, rssBytes: u.rss });
194
- }
195
- }, intervalMs);
196
- timer.unref?.();
197
41
  };
198
42
 
199
43
  // src/protocol.ts
@@ -225,6 +69,8 @@ function connectBroadcast(opts, session) {
225
69
  if (m.data?.type === "view") {
226
70
  hello();
227
71
  for (const events of tail) post({ type: "batch", session, events });
72
+ } else if (m.data?.type === "control" && m.data.sessionId === session.id) {
73
+ void answerControl(m.data, (res) => post(res));
228
74
  }
229
75
  };
230
76
  const timer = setInterval(flush, opts.flushMs ?? 100);
@@ -237,6 +83,8 @@ function connectBroadcast(opts, session) {
237
83
  if (typeof addEventListener === "function") addEventListener("pagehide", onHide);
238
84
  if (opts.network !== false) installFetchProbe(globalThis);
239
85
  if (opts.memory !== false) installMemoryProbe(globalThis);
86
+ const releaseJank = opts.jank !== false ? installJankProbe() : () => {
87
+ };
240
88
  setDevtoolsSink((event) => {
241
89
  if (closed) return;
242
90
  pending.push(event);
@@ -255,6 +103,7 @@ function connectBroadcast(opts, session) {
255
103
  bye();
256
104
  setDevtoolsSink(null);
257
105
  clearInterval(timer);
106
+ releaseJank();
258
107
  if (typeof removeEventListener === "function") removeEventListener("pagehide", onHide);
259
108
  bc.close();
260
109
  }
@@ -269,8 +118,10 @@ var makeSession = (opts) => {
269
118
  return {
270
119
  id: `s-${Date.now().toString(36)}-${++sessionSeq}`,
271
120
  name: opts.session?.name,
121
+ framework: opts.session?.framework,
272
122
  runtime,
273
- hint
123
+ hint,
124
+ env: captureEnv()
274
125
  };
275
126
  };
276
127
  var autoBroadcast = () => typeof BroadcastChannel !== "undefined" && typeof window !== "undefined" && typeof window.document !== "undefined";
@@ -298,6 +149,15 @@ function connectWebSocket(opts, session) {
298
149
  send({ type: "hello", session });
299
150
  flush();
300
151
  };
152
+ const onMessage = (text) => {
153
+ let msg;
154
+ try {
155
+ msg = JSON.parse(text);
156
+ } catch {
157
+ return;
158
+ }
159
+ if (msg.type === "control") void answerControl(msg, (res) => send(res));
160
+ };
301
161
  const isNode = typeof window === "undefined" || typeof window.document === "undefined";
302
162
  if (typeof WebSocket !== "undefined") {
303
163
  const ws = new WebSocket(url);
@@ -309,6 +169,7 @@ function connectWebSocket(opts, session) {
309
169
  close: () => ws.close()
310
170
  };
311
171
  ws.onopen = onOpen;
172
+ ws.onmessage = (m) => onMessage(String(m.data));
312
173
  ws.onclose = () => clearInterval(timer);
313
174
  ws.onerror = () => ws.close();
314
175
  } else if (isNode) {
@@ -321,6 +182,7 @@ function connectWebSocket(opts, session) {
321
182
  const ws = m.connectNodeWebSocket(url);
322
183
  sock = ws;
323
184
  ws.onOpen = onOpen;
185
+ ws.onMessage = onMessage;
324
186
  ws.onClose = () => clearInterval(timer);
325
187
  }).catch(() => {
326
188
  });
@@ -329,6 +191,8 @@ function connectWebSocket(opts, session) {
329
191
  timer.unref?.();
330
192
  if (opts.network !== false) installFetchProbe(globalThis);
331
193
  if (opts.memory !== false) installMemoryProbe(globalThis);
194
+ const releaseJank = opts.jank !== false ? installJankProbe() : () => {
195
+ };
332
196
  setDevtoolsSink((event) => {
333
197
  if (closed) return;
334
198
  pending.push(event);
@@ -346,6 +210,7 @@ function connectWebSocket(opts, session) {
346
210
  flush();
347
211
  sock?.close();
348
212
  clearInterval(timer);
213
+ releaseJank();
349
214
  }
350
215
  };
351
216
  }
@@ -361,41 +226,120 @@ var ANCHOR = {
361
226
  bottomcenter: (m) => `left:50%;bottom:${m}px;transform:translateX(-50%)`,
362
227
  bottomright: (m) => `right:${m}px;bottom:${m}px`
363
228
  };
229
+ var MIN_W = 340;
230
+ var MIN_H = 220;
231
+ var seq = 0;
232
+ function parseHotkey(spec) {
233
+ const parts = spec.split("+").map((p) => p.trim().toLowerCase()).filter(Boolean);
234
+ const key = parts.pop() ?? "";
235
+ const mods = new Set(parts.map((m) => m === "option" ? "alt" : m === "cmd" || m === "meta" ? "meta" : m === "control" ? "ctrl" : m));
236
+ const code = /^[a-z]$/.test(key) ? `Key${key.toUpperCase()}` : /^[0-9]$/.test(key) ? `Digit${key}` : null;
237
+ return (e) => e.altKey === mods.has("alt") && e.shiftKey === mods.has("shift") && e.ctrlKey === mods.has("ctrl") && e.metaKey === mods.has("meta") && (code ? e.code === code : e.key.toLowerCase() === key);
238
+ }
364
239
  function mountDevtoolsOverlay(opts = {}) {
365
240
  const src = opts.src ?? "/__atoll/?mini=1";
366
- const W = opts.width ?? 760;
367
- const H = opts.height ?? 580;
241
+ const storeKey = opts.persist === false ? null : typeof opts.persist === "string" ? opts.persist : "atoll-devtools:overlay";
242
+ const load = () => {
243
+ if (!storeKey) return {};
244
+ try {
245
+ return JSON.parse(localStorage.getItem(storeKey) ?? "{}");
246
+ } catch {
247
+ return {};
248
+ }
249
+ };
250
+ const saved = load();
251
+ const W = Math.max(MIN_W, saved.width ?? opts.width ?? 760);
252
+ const H = Math.max(MIN_H, saved.height ?? opts.height ?? 580);
368
253
  const anchor = ANCHOR[opts.position ?? "bottomright"];
254
+ const hotkey = opts.hotkey === false ? null : opts.hotkey ?? "Alt+Shift+D";
255
+ const isHotkey = hotkey ? parseHotkey(hotkey) : null;
256
+ const hint = hotkey ? ` (${hotkey})` : "";
257
+ const id = `atoll-devtools-overlay-${++seq}`;
369
258
  let open = false;
370
259
  const btn = document.createElement("button");
260
+ btn.type = "button";
371
261
  btn.textContent = "atoll devtools";
262
+ btn.title = `Open atoll devtools${hint}`;
263
+ btn.setAttribute("aria-controls", id);
264
+ btn.setAttribute("aria-expanded", "false");
372
265
  btn.style.cssText = `${BTN_STYLE};${anchor(12)}`;
373
266
  const panel = document.createElement("div");
267
+ panel.id = id;
268
+ panel.setAttribute("role", "dialog");
269
+ panel.setAttribute("aria-label", "atoll devtools");
374
270
  panel.style.cssText = `${PANEL_STYLE};width:${W}px;height:${H}px;display:none;${anchor(16)}`;
375
271
  const head = document.createElement("div");
376
272
  head.style.cssText = "display:flex;align-items:center;gap:8px;padding:5px 10px;flex:none;border-bottom:1px solid #21262d;font:11px ui-monospace,monospace;color:#8b949e;background:#161b22;cursor:move;user-select:none";
377
- head.textContent = "atoll devtools";
273
+ const title = document.createElement("span");
274
+ title.textContent = "atoll devtools";
275
+ const base = src.split(/[?#]/)[0];
276
+ const frame = document.createElement("iframe");
277
+ let route = "";
278
+ const currentRoute = () => {
279
+ try {
280
+ const h = frame.contentWindow?.location.hash;
281
+ if (typeof h === "string" && h.startsWith("#/")) return h;
282
+ } catch {
283
+ }
284
+ return route;
285
+ };
378
286
  const pop = document.createElement("a");
379
- pop.href = "/__atoll/";
287
+ pop.href = base;
380
288
  pop.target = "_blank";
381
289
  pop.rel = "noopener";
382
290
  pop.textContent = "full page \u2197";
291
+ pop.title = "Open this view in the full-page dashboard";
383
292
  pop.style.cssText = "color:#58a6ff;text-decoration:none;margin-left:auto";
293
+ const syncPop = () => {
294
+ pop.href = base + currentRoute();
295
+ };
296
+ pop.addEventListener("pointerenter", syncPop);
297
+ pop.addEventListener("focus", syncPop);
298
+ pop.addEventListener("click", syncPop);
384
299
  const x = document.createElement("button");
385
- x.textContent = "\u2014";
386
- x.title = "collapse";
387
- x.style.cssText = "color:#8b949e;background:none;border:none;cursor:pointer;font:inherit;padding:0 2px";
388
- head.append(pop, x);
389
- const frame = document.createElement("iframe");
300
+ x.type = "button";
301
+ x.textContent = "\xD7";
302
+ x.title = `Close${hint}`;
303
+ x.setAttribute("aria-label", "Close atoll devtools");
304
+ x.style.cssText = "color:#8b949e;background:none;border:none;cursor:pointer;font:14px/1 ui-monospace,monospace;padding:0 2px";
305
+ head.append(title, pop, x);
390
306
  frame.src = src;
307
+ frame.title = "atoll devtools dashboard";
391
308
  frame.style.cssText = "flex:1;border:0;background:#0d1117;min-height:0";
392
309
  const grip = document.createElement("div");
310
+ grip.setAttribute("aria-hidden", "true");
393
311
  grip.style.cssText = "position:absolute;right:0;bottom:0;width:16px;height:16px;cursor:nwse-resize;background:linear-gradient(135deg,transparent 50%,#30363d 50%);border-radius:0 0 7px";
394
312
  panel.append(head, frame, grip);
395
313
  document.body.append(panel, btn);
396
- const drag = (e, move) => {
314
+ const placeAt = (left, top) => {
315
+ panel.style.left = `${Math.min(Math.max(0, left), Math.max(0, innerWidth - 60))}px`;
316
+ panel.style.top = `${Math.min(Math.max(0, top), Math.max(0, innerHeight - 40))}px`;
317
+ panel.style.right = "auto";
318
+ panel.style.bottom = "auto";
319
+ panel.style.transform = "none";
320
+ };
321
+ if (typeof saved.left === "number" && typeof saved.top === "number") placeAt(saved.left, saved.top);
322
+ const mem = { ...saved };
323
+ const save = (what) => {
324
+ if (!storeKey) return;
325
+ if (what === "open") mem.open = open;
326
+ if (what === "size") {
327
+ mem.width = parseFloat(panel.style.width);
328
+ mem.height = parseFloat(panel.style.height);
329
+ }
330
+ if (what === "move") {
331
+ mem.left = parseFloat(panel.style.left);
332
+ mem.top = parseFloat(panel.style.top);
333
+ }
334
+ try {
335
+ localStorage.setItem(storeKey, JSON.stringify(mem));
336
+ } catch {
337
+ }
338
+ };
339
+ const drag = (e, what, move) => {
397
340
  e.preventDefault();
398
341
  let lx = e.clientX, ly = e.clientY;
342
+ frame.style.pointerEvents = "none";
399
343
  const mm = (ev) => {
400
344
  move(ev.clientX - lx, ev.clientY - ly);
401
345
  lx = ev.clientX;
@@ -404,44 +348,62 @@ function mountDevtoolsOverlay(opts = {}) {
404
348
  const up = () => {
405
349
  removeEventListener("pointermove", mm);
406
350
  removeEventListener("pointerup", up);
351
+ frame.style.pointerEvents = "";
352
+ save(what);
407
353
  };
408
354
  addEventListener("pointermove", mm);
409
355
  addEventListener("pointerup", up);
410
356
  };
411
357
  head.onpointerdown = (e) => {
412
358
  if (e.target.closest("a,button")) return;
413
- drag(e, (dx, dy) => {
359
+ drag(e, "move", (dx, dy) => {
414
360
  const r = panel.getBoundingClientRect();
415
- const left = Math.min(Math.max(0, r.left + dx), innerWidth - 60);
416
- const top = Math.min(Math.max(0, r.top + dy), innerHeight - 40);
417
- panel.style.left = `${left}px`;
418
- panel.style.top = `${top}px`;
419
- panel.style.right = "auto";
420
- panel.style.bottom = "auto";
421
- panel.style.transform = "none";
361
+ placeAt(r.left + dx, r.top + dy);
422
362
  });
423
363
  };
424
364
  grip.onpointerdown = (e) => {
425
365
  e.stopPropagation();
426
- drag(e, (dx, dy) => {
366
+ drag(e, "size", (dx, dy) => {
427
367
  const r = panel.getBoundingClientRect();
428
- panel.style.width = `${Math.max(340, r.width + dx)}px`;
429
- panel.style.height = `${Math.max(220, r.height + dy)}px`;
368
+ panel.style.width = `${Math.max(MIN_W, r.width + dx)}px`;
369
+ panel.style.height = `${Math.max(MIN_H, r.height + dy)}px`;
430
370
  });
431
371
  };
432
- const set = (v) => {
372
+ const set = (v, focus = false, persist = true) => {
373
+ const hadFocus = panel.contains(document.activeElement);
433
374
  open = v;
434
375
  panel.style.display = v ? "flex" : "none";
435
376
  btn.style.display = v ? "none" : "block";
377
+ btn.setAttribute("aria-expanded", String(v));
378
+ if (v && focus) frame.focus();
379
+ if (!v && hadFocus) btn.focus();
380
+ if (persist) save("open");
436
381
  };
437
- btn.onclick = () => set(true);
382
+ btn.onclick = () => set(true, true);
438
383
  x.onclick = () => set(false);
439
- set(opts.startOpen === true);
384
+ const onKey = (e) => {
385
+ if (!isHotkey?.(e)) return;
386
+ e.preventDefault();
387
+ set(!open, true);
388
+ };
389
+ const onMessage = (e) => {
390
+ if (e.source !== frame.contentWindow || !e.data || typeof e.data !== "object") return;
391
+ const d = e.data;
392
+ if (d.type === "atoll-devtools:toggle") set(!open);
393
+ else if (d.type === "atoll-devtools:route" && typeof d.hash === "string" && d.hash.startsWith("#/")) {
394
+ route = d.hash;
395
+ }
396
+ };
397
+ if (isHotkey) addEventListener("keydown", onKey);
398
+ addEventListener("message", onMessage);
399
+ set(opts.startOpen ?? saved.open === true, false, false);
440
400
  return {
441
401
  open: () => set(true),
442
402
  close: () => set(false),
443
403
  toggle: () => set(!open),
444
404
  unmount: () => {
405
+ removeEventListener("keydown", onKey);
406
+ removeEventListener("message", onMessage);
445
407
  panel.remove();
446
408
  btn.remove();
447
409
  }
@@ -450,7 +412,7 @@ function mountDevtoolsOverlay(opts = {}) {
450
412
 
451
413
  // src/init.ts
452
414
  var DEVTOOLS_PARAM = "__atoll_devtools";
453
- var devtoolsEnabled2 = (enabled) => {
415
+ var devtoolsEnabled = (enabled) => {
454
416
  if (enabled !== void 0) return enabled;
455
417
  if (typeof location !== "undefined" && typeof location.search === "string") {
456
418
  return new URLSearchParams(location.search).has(DEVTOOLS_PARAM);
@@ -458,7 +420,7 @@ var devtoolsEnabled2 = (enabled) => {
458
420
  return !!globalThis.process?.env?.ATOLL_DEVTOOLS;
459
421
  };
460
422
  function initDevtools(opts = {}) {
461
- if (!devtoolsEnabled2(opts.enabled)) return null;
423
+ if (!devtoolsEnabled(opts.enabled)) return null;
462
424
  const conn = connectDevtools(opts);
463
425
  const inWindow = typeof window !== "undefined" && typeof window.document !== "undefined";
464
426
  const overlay = inWindow && opts.overlay !== false ? mountDevtoolsOverlay(typeof opts.overlay === "object" ? opts.overlay : {}) : null;
@@ -469,6 +431,6 @@ export {
469
431
  connectDevtools,
470
432
  mountDevtoolsOverlay,
471
433
  DEVTOOLS_PARAM,
472
- devtoolsEnabled2 as devtoolsEnabled,
434
+ devtoolsEnabled,
473
435
  initDevtools
474
436
  };