@atolljs/devtools 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,7 +24,7 @@ current and future viewers. Live sessions can't be dismissed.
24
24
  |---|---|
25
25
  | `@atolljs/devtools` | `connectDevtools({ transport?, url?, session?, flushMs?, bufferCap?, replayBatches?, network?, memory?, jank? })`: installs the core event sink and batches events to dashboards. Default `transport: 'auto'`: **BroadcastChannel** in a browser window (pure client, no backend; the dashboard is served on the app's own origin at `/__atoll/` by the vite plugin in dev, and emitted into `vite build` output whenever the app bundles devtools), WebSocket to the aggregate server everywhere else (`url` or `transport: 'websocket'` opts in; Node uses this path). `session` is `{ name?, hint?, framework? }`. `network` (default on) wraps `fetch` to emit `net:fetch` per request; `memory` (default on) samples JS heap into `runtime:memory`; `jank` (default on) installs the main-thread jank probe (`runtime:longframe`, `runtime:frames`). |
26
26
  | `@atolljs/devtools` | `initDevtools({ enabled?, overlay?, ...connectOptions })`: the URL-gated one-liner (`?__atoll_devtools`), connects and mounts the overlay. |
27
- | `@atolljs/devtools` | `mountDevtoolsOverlay({ src?, startOpen?, width?, height?, position?, persist?, hotkey? })`: the floating flyout (draggable, resizable). `persist` (default `true`, or a localStorage key, default `'atoll-devtools:overlay'`) remembers position, size and open state; `hotkey` (default `'Alt+Shift+D'`, `false` disables) toggles it. |
27
+ | `@atolljs/devtools` | `mountDevtoolsOverlay({ src?, startOpen?, width?, height?, position?, persist?, hotkey? })`: the floating flyout (drag the header to move it, any edge or corner to resize; it stays inside the visible viewport). `persist` (default `true`, or a localStorage key, default `'atoll-devtools:overlay'`) remembers position, size and open state; `hotkey` (default `'Alt+Shift+D'`, `false` disables) toggles it. |
28
28
  | `@atolljs/devtools/node` | Node entry: `initDevtools`/`connectDevtools` without the browser-only pieces (WebSocket-only, env-gated, no overlay). Use this import in Node apps. |
29
29
  | `@atolljs/devtools/server` | `createDevtoolsServer({ port?, host?, appDir?, replayBatches?, closedTtlMs?, sweepMs? })`: Node http + dependency-free WebSocket server on `127.0.0.1` (default port 4780). |
30
30
  | `@atolljs/devtools/otel` | `exportOtel({ endpoint?, headers?, serviceName?, resource?, signals?, flushIntervalMs?, maxBatch?, maxQueue?, maxSeries?, onError?, network?, memory?, jank? })`: dependency-free OTLP/HTTP JSON exporter (traces, metrics, logs) to any OpenTelemetry backend; returns `{ flush(), close() }`. Install before pools spawn. See [OpenTelemetry export](#opentelemetry-export). |
@@ -0,0 +1,14 @@
1
+ /** Framework logos from Simple Icons 16.33.0 (CC0, https://simpleicons.org):
2
+ * 24x24 path data + official brand hex, keyed by the ramework tag. */
3
+ export const FW_LOGOS = {
4
+ react: { title: "React", hex: '#61DAFB', d: "M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z" },
5
+ vue: { title: "Vue.js", hex: '#4FC08D', d: "M24,1.61H14.06L12,5.16,9.94,1.61H0L12,22.39ZM12,14.08,5.16,2.23H9.59L12,6.41l2.41-4.18h4.43Z" },
6
+ svelte: { title: "Svelte", hex: '#FF3E00', d: "M10.354 21.125a4.44 4.44 0 0 1-4.765-1.767 4.109 4.109 0 0 1-.703-3.107 3.898 3.898 0 0 1 .134-.522l.105-.321.287.21a7.21 7.21 0 0 0 2.186 1.092l.208.063-.02.208a1.253 1.253 0 0 0 .226.83 1.337 1.337 0 0 0 1.435.533 1.231 1.231 0 0 0 .343-.15l5.59-3.562a1.164 1.164 0 0 0 .524-.778 1.242 1.242 0 0 0-.211-.937 1.338 1.338 0 0 0-1.435-.533 1.23 1.23 0 0 0-.343.15l-2.133 1.36a4.078 4.078 0 0 1-1.135.499 4.44 4.44 0 0 1-4.765-1.766 4.108 4.108 0 0 1-.702-3.108 3.855 3.855 0 0 1 1.742-2.582l5.589-3.563a4.072 4.072 0 0 1 1.135-.499 4.44 4.44 0 0 1 4.765 1.767 4.109 4.109 0 0 1 .703 3.107 3.943 3.943 0 0 1-.134.522l-.105.321-.286-.21a7.204 7.204 0 0 0-2.187-1.093l-.208-.063.02-.207a1.255 1.255 0 0 0-.226-.831 1.337 1.337 0 0 0-1.435-.532 1.231 1.231 0 0 0-.343.15L8.62 9.368a1.162 1.162 0 0 0-.524.778 1.24 1.24 0 0 0 .211.937 1.338 1.338 0 0 0 1.435.533 1.235 1.235 0 0 0 .344-.151l2.132-1.36a4.067 4.067 0 0 1 1.135-.498 4.44 4.44 0 0 1 4.765 1.766 4.108 4.108 0 0 1 .702 3.108 3.857 3.857 0 0 1-1.742 2.583l-5.589 3.562a4.072 4.072 0 0 1-1.135.499m10.358-17.95C18.484-.015 14.082-.96 10.9 1.068L5.31 4.63a6.412 6.412 0 0 0-2.896 4.295 6.753 6.753 0 0 0 .666 4.336 6.43 6.43 0 0 0-.96 2.396 6.833 6.833 0 0 0 1.168 5.167c2.229 3.19 6.63 4.135 9.812 2.108l5.59-3.562a6.41 6.41 0 0 0 2.896-4.295 6.756 6.756 0 0 0-.665-4.336 6.429 6.429 0 0 0 .958-2.396 6.831 6.831 0 0 0-1.167-5.168Z" },
7
+ solid: { title: "Solid", hex: '#2C4F7C', d: "M11.558.788A9.082 9.082 0 0 0 9.776.99l-.453.15c-.906.303-1.656.755-2.1 1.348l-.301.452-2.035 3.528c.426-.387.974-.698 1.643-.894h.001l.613-.154h.001a8.82 8.82 0 0 1 1.777-.206c2.916-.053 6.033 1.148 8.423 2.36 2.317 1.175 3.888 2.32 3.987 2.39L24 5.518c-.082-.06-1.66-1.21-3.991-2.386-2.393-1.206-5.521-2.396-8.45-2.343zM8.924 5.366a8.634 8.634 0 0 0-1.745.203l-.606.151c-1.278.376-2.095 1.16-2.43 2.108-.334.948-.188 2.065.487 3.116.33.43.747.813 1.216 1.147L12.328 10h.001a6.943 6.943 0 0 1 6.013 1.013l2.844-.963c-.17-.124-1.663-1.2-3.91-2.34-2.379-1.206-5.479-2.396-8.352-2.344zm5.435 4.497a6.791 6.791 0 0 0-1.984.283L2.94 13.189 0 18.334l9.276-2.992a6.945 6.945 0 0 1 7.408 2.314v.001c.695.903.89 1.906.66 2.808l2.572-4.63c.595-1.041.45-2.225-.302-3.429a6.792 6.792 0 0 0-5.255-2.543zm-3.031 5.341a6.787 6.787 0 0 0-2.006.283L.008 18.492c.175.131 2.02 1.498 4.687 2.768 2.797 1.332 6.37 2.467 9.468 1.712l.454-.152h.002c1.278-.376 2.134-1.162 2.487-2.09.353-.93.207-2.004-.541-2.978a6.791 6.791 0 0 0-5.237-2.548z" },
8
+ angular: { title: "Angular", hex: '#0F0F11', d: "M16.712 17.711H7.288l-1.204 2.916L12 24l5.916-3.373-1.204-2.916ZM14.692 0l7.832 16.855.814-12.856L14.692 0ZM9.308 0 .662 3.999l.814 12.856L9.308 0Zm-.405 13.93h6.198L12 6.396 8.903 13.93Z" },
9
+ vanilla: { title: "JavaScript", hex: '#F7DF1E', d: "M0 0h24v24H0V0zm22.034 18.276c-.175-1.095-.888-2.015-3.003-2.873-.736-.345-1.554-.585-1.797-1.14-.091-.33-.105-.51-.046-.705.15-.646.915-.84 1.515-.66.39.12.75.42.976.9 1.034-.676 1.034-.676 1.755-1.125-.27-.42-.404-.601-.586-.78-.63-.705-1.469-1.065-2.834-1.034l-.705.089c-.676.165-1.32.525-1.71 1.005-1.14 1.291-.811 3.541.569 4.471 1.365 1.02 3.361 1.244 3.616 2.205.24 1.17-.87 1.545-1.966 1.41-.811-.18-1.26-.586-1.755-1.336l-1.83 1.051c.21.48.45.689.81 1.109 1.74 1.756 6.09 1.666 6.871-1.004.029-.09.24-.705.074-1.65l.046.067zm-8.983-7.245h-2.248c0 1.938-.009 3.864-.009 5.805 0 1.232.063 2.363-.138 2.711-.33.689-1.18.601-1.566.48-.396-.196-.597-.466-.83-.855-.063-.105-.11-.196-.127-.196l-1.825 1.125c.305.63.75 1.172 1.324 1.517.855.51 2.004.675 3.207.405.783-.226 1.458-.691 1.811-1.411.51-.93.402-2.07.397-3.346.012-2.054 0-4.109 0-6.179l.004-.056z" },
10
+ leaflet: { title: "Leaflet", hex: '#199900', d: "M17.69 0c-.355.574-8.432 4.74-10.856 8.649-2.424 3.91-3.116 6.988-2.237 9.882.879 2.893 2.559 2.763 3.516 3.717.958.954 2.257 2.113 4.332 1.645 2.717-.613 5.335-2.426 6.638-7.508 1.302-5.082.448-9.533-.103-11.99A35.395 35.395 0 0 0 17.69 0zm-.138.858l-9.22 21.585-.574-.577Z" },
11
+ nextjs: { title: "Next.js", hex: '#000000', d: "M18.665 21.978C16.758 23.255 14.465 24 12 24 5.377 24 0 18.623 0 12S5.377 0 12 0s12 5.377 12 12c0 3.583-1.574 6.801-4.067 9.001L9.219 7.2H7.2v9.596h1.615V9.251l9.85 12.727Zm-3.332-8.533 1.6 2.061V7.2h-1.6v6.245Z" },
12
+ };
13
+ FW_LOGOS.next = FW_LOGOS.nextjs;
14
+ FW_LOGOS.js = FW_LOGOS.vanilla;
package/app/main.js CHANGED
@@ -9,6 +9,7 @@
9
9
  // Feature panels live in ./panels/*.js and plug in through ./api.js (see
10
10
  // PANELS at the bottom): this file owns the reducer, core views, transport.
11
11
  import { api } from './api.js';
12
+ import { FW_LOGOS } from './fw-logos.js';
12
13
 
13
14
  const $ = (id) => document.getElementById(id);
14
15
  const esc = (s) => String(s ?? '').replace(/[&<>]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));
@@ -1394,17 +1395,6 @@ function fitTopo(canvas, world) {
1394
1395
  topo.oy = (H - world.height * topo.scale) / 2;
1395
1396
  }
1396
1397
 
1397
- /** framework → [chip color, short tag] — HTML chips + fallback marks. */
1398
- const FW_BADGES = {
1399
- react: ['#61dafb', '⚛'],
1400
- vue: ['#42b883', 'V'],
1401
- svelte: ['#ff3e00', 'S'],
1402
- angular: ['#dd0031', 'A'],
1403
- solid: ['#4f88c6', 'So'],
1404
- vanilla: ['#f0db4f', 'JS'],
1405
- leaflet: ['#19993b', 'L'],
1406
- };
1407
-
1408
1398
  /** Structural (non-island) nodes — a lettered disc, so pools and plain
1409
1399
  * workers never read as a framework island. */
1410
1400
  const ROLE_MARKS = {
@@ -1426,96 +1416,31 @@ function drawRoleMark(ctx, m, x, y, s, dead) {
1426
1416
  ctx.textBaseline = 'top';
1427
1417
  }
1428
1418
 
1429
- /** Real framework marks, drawn at icon size (s ≈ node radius). */
1419
+ /** Framework logos (FW_LOGOS paths in their brand colours) on a disc, drawn
1420
+ * at icon size (s ≈ node radius). Logos too dark for the dashboard sit on
1421
+ * a light disc. Unknown tags fall back to a lettered disc. */
1422
+ const fwPaths = {};
1430
1423
  function drawFwMark(ctx, fw, x, y, s, dead) {
1431
1424
  if (ROLE_MARKS[fw]) return drawRoleMark(ctx, ROLE_MARKS[fw], x, y, s, dead);
1425
+ const logo = FW_LOGOS[fw];
1432
1426
  ctx.save();
1433
1427
  if (dead) ctx.globalAlpha = 0.35;
1434
- switch (fw) {
1435
- case 'react': {
1436
- // the atom — nucleus + three orbital ellipses
1437
- ctx.beginPath(); ctx.arc(x, y, s, 0, 7);
1438
- ctx.fillStyle = '#22272e'; ctx.fill();
1439
- ctx.strokeStyle = '#61dafb'; ctx.lineWidth = s * 0.13;
1440
- for (const deg of [0, 60, 120]) {
1441
- ctx.beginPath();
1442
- ctx.ellipse(x, y, s * 0.95, s * 0.38, (deg * Math.PI) / 180, 0, 7);
1443
- ctx.stroke();
1444
- }
1445
- ctx.beginPath(); ctx.arc(x, y, s * 0.15, 0, 7);
1446
- ctx.fillStyle = '#61dafb'; ctx.fill();
1447
- break;
1448
- }
1449
- case 'vue': {
1450
- // nested V — green outer chevron, dark inner chevron
1451
- const h = s * 0.92;
1452
- ctx.beginPath();
1453
- ctx.moveTo(x - s, y - h); ctx.lineTo(x + s, y - h); ctx.lineTo(x, y + h);
1454
- ctx.closePath(); ctx.fillStyle = '#42b883'; ctx.fill();
1455
- ctx.beginPath();
1456
- ctx.moveTo(x - s * 0.58, y - h); ctx.lineTo(x + s * 0.58, y - h); ctx.lineTo(x, y + h * 0.14);
1457
- ctx.closePath(); ctx.fillStyle = '#35495e'; ctx.fill();
1458
- break;
1459
- }
1460
- case 'svelte': {
1461
- ctx.beginPath(); ctx.arc(x, y, s, 0, 7);
1462
- ctx.fillStyle = '#ff3e00'; ctx.fill();
1463
- ctx.font = `bold ${s * 1.3}px ui-monospace, monospace`;
1464
- ctx.fillStyle = '#fff'; ctx.textBaseline = 'middle';
1465
- ctx.fillText('S', x, y + s * 0.06);
1466
- break;
1467
- }
1468
- case 'angular': {
1469
- // shield + A
1470
- ctx.beginPath();
1471
- ctx.moveTo(x, y - s); ctx.lineTo(x + s * 0.95, y - s * 0.62);
1472
- ctx.lineTo(x + s * 0.78, y + s * 0.72); ctx.lineTo(x, y + s);
1473
- ctx.lineTo(x - s * 0.78, y + s * 0.72); ctx.lineTo(x - s * 0.95, y - s * 0.62);
1474
- ctx.closePath(); ctx.fillStyle = '#dd0031'; ctx.fill();
1475
- ctx.font = `bold ${s * 1.15}px ui-monospace, monospace`;
1476
- ctx.fillStyle = '#fff'; ctx.textBaseline = 'middle';
1477
- ctx.fillText('A', x, y + s * 0.1);
1478
- break;
1479
- }
1480
- case 'solid': {
1481
- ctx.beginPath(); ctx.arc(x, y, s, 0, 7);
1482
- ctx.fillStyle = '#4f88c6'; ctx.fill();
1483
- ctx.font = `bold ${s * 1.15}px ui-monospace, monospace`;
1484
- ctx.fillStyle = '#fff'; ctx.textBaseline = 'middle';
1485
- ctx.fillText('S', x, y + s * 0.06);
1486
- break;
1487
- }
1488
- case 'vanilla': {
1489
- // the JS badge — yellow square, black JS
1490
- ctx.beginPath();
1491
- ctx.roundRect(x - s, y - s, s * 2, s * 2, s * 0.14);
1492
- ctx.fillStyle = '#f0db4f'; ctx.fill();
1493
- ctx.font = `bold ${s * 0.85}px ui-monospace, monospace`;
1494
- ctx.fillStyle = '#0d1117'; ctx.textBaseline = 'middle';
1495
- ctx.fillText('JS', x, y + s * 0.18);
1496
- break;
1497
- }
1498
- case 'leaflet': {
1499
- // green rounded square + white leaf
1500
- ctx.beginPath();
1501
- ctx.roundRect(x - s, y - s, s * 2, s * 2, s * 0.25);
1502
- ctx.fillStyle = '#19993b'; ctx.fill();
1503
- ctx.beginPath();
1504
- ctx.ellipse(x, y - s * 0.08, s * 0.3, s * 0.58, Math.PI / 4, 0, 7);
1505
- ctx.fillStyle = '#fff'; ctx.fill();
1506
- ctx.beginPath();
1507
- ctx.moveTo(x - s * 0.45, y + s * 0.42); ctx.lineTo(x + s * 0.45, y - s * 0.48);
1508
- ctx.strokeStyle = '#19993b'; ctx.lineWidth = s * 0.07; ctx.stroke();
1509
- break;
1510
- }
1511
- default: {
1512
- // tagged but unknown framework — green disc + first letter
1513
- ctx.beginPath(); ctx.arc(x, y, s * 0.85, 0, 7);
1514
- ctx.fillStyle = dead ? '#484f58' : '#3fb950'; ctx.fill();
1515
- ctx.font = `bold ${s * 0.9}px ui-monospace, monospace`;
1516
- ctx.fillStyle = '#0d1117'; ctx.textBaseline = 'middle';
1517
- ctx.fillText(fw[0].toUpperCase(), x, y);
1518
- }
1428
+ if (logo && typeof Path2D === 'function') {
1429
+ const n = parseInt(logo.hex.slice(1), 16);
1430
+ const lum = (0.299 * (n >> 16) + 0.587 * ((n >> 8) & 255) + 0.114 * (n & 255)) / 255;
1431
+ ctx.beginPath(); ctx.arc(x, y, s, 0, 7);
1432
+ ctx.fillStyle = lum < 0.35 ? '#f0f3f6' : '#22272e'; ctx.fill();
1433
+ const k = (s * 1.3) / 24;
1434
+ ctx.translate(x - 12 * k, y - 12 * k);
1435
+ ctx.scale(k, k);
1436
+ ctx.fillStyle = logo.hex;
1437
+ ctx.fill(fwPaths[fw] ??= new Path2D(logo.d));
1438
+ } else {
1439
+ ctx.beginPath(); ctx.arc(x, y, s * 0.85, 0, 7);
1440
+ ctx.fillStyle = dead ? '#484f58' : '#3fb950'; ctx.fill();
1441
+ ctx.font = `bold ${s * 0.9}px ui-monospace, monospace`;
1442
+ ctx.fillStyle = '#0d1117'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
1443
+ ctx.fillText(fw[0].toUpperCase(), x, y);
1519
1444
  }
1520
1445
  ctx.restore();
1521
1446
  ctx.textBaseline = 'top';
@@ -217,7 +217,17 @@ function connectWebSocket(opts, session) {
217
217
 
218
218
  // src/overlay.ts
219
219
  var BTN_STYLE = "position:fixed;z-index:2147483646;font:11px ui-monospace,monospace;color:#c9d1d9;background:#161b22;border:1px solid #30363d;border-radius:6px;padding:5px 9px;cursor:pointer";
220
- var PANEL_STYLE = "position:fixed;z-index:2147483646;display:flex;flex-direction:column;background:#0d1117;border:1px solid #30363d;border-radius:8px;box-shadow:0 12px 40px #000c;overflow:hidden;min-width:340px;min-height:220px";
220
+ var PANEL_STYLE = "position:fixed;z-index:2147483646;display:flex;flex-direction:column;background:#0d1117;border:1px solid #30363d;border-radius:8px;box-shadow:0 12px 40px #000c;overflow:hidden;min-width:340px;min-height:220px;max-width:100vw;max-height:100vh;box-sizing:border-box";
221
+ var EDGE_CSS = {
222
+ n: "top:0;left:10px;right:10px;height:4px;cursor:ns-resize",
223
+ s: "bottom:0;left:10px;right:16px;height:5px;cursor:ns-resize",
224
+ e: "right:0;top:10px;bottom:16px;width:5px;cursor:ew-resize",
225
+ w: "left:0;top:10px;bottom:10px;width:5px;cursor:ew-resize",
226
+ ne: "right:0;top:0;width:10px;height:10px;cursor:nesw-resize",
227
+ nw: "left:0;top:0;width:10px;height:10px;cursor:nwse-resize",
228
+ sw: "left:0;bottom:0;width:10px;height:10px;cursor:nesw-resize",
229
+ se: "right:0;bottom:0;width:16px;height:16px;cursor:nwse-resize;background:linear-gradient(135deg,transparent 50%,#30363d 50%);border-radius:0 0 7px"
230
+ };
221
231
  var ANCHOR = {
222
232
  topleft: (m) => `left:${m}px;top:${m}px`,
223
233
  topcenter: (m) => `left:50%;top:${m}px;transform:translateX(-50%)`,
@@ -306,19 +316,44 @@ function mountDevtoolsOverlay(opts = {}) {
306
316
  frame.src = src;
307
317
  frame.title = "atoll devtools dashboard";
308
318
  frame.style.cssText = "flex:1;border:0;background:#0d1117;min-height:0";
309
- const grip = document.createElement("div");
310
- grip.setAttribute("aria-hidden", "true");
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";
312
- panel.append(head, frame, grip);
319
+ const handles = ["n", "s", "e", "w", "ne", "nw", "sw", "se"].map((edge) => {
320
+ const h = document.createElement("div");
321
+ h.setAttribute("aria-hidden", "true");
322
+ h.dataset.edge = edge;
323
+ h.style.cssText = `position:absolute;z-index:1;${EDGE_CSS[edge]}`;
324
+ return h;
325
+ });
326
+ panel.append(head, frame, ...handles);
313
327
  document.body.append(panel, btn);
328
+ const px = (v) => parseFloat(v) || 0;
329
+ const vw = () => document.documentElement.clientWidth || innerWidth;
330
+ const vh = () => document.documentElement.clientHeight || innerHeight;
331
+ const fitW = (w) => Math.max(MIN_W, Math.min(w, vw()));
332
+ const fitH = (h) => Math.max(MIN_H, Math.min(h, vh()));
314
333
  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`;
334
+ const w = px(panel.style.width), h = px(panel.style.height);
335
+ panel.style.left = `${Math.min(Math.max(0, left), Math.max(0, vw() - w))}px`;
336
+ panel.style.top = `${Math.min(Math.max(0, top), Math.max(0, vh() - h))}px`;
317
337
  panel.style.right = "auto";
318
338
  panel.style.bottom = "auto";
319
339
  panel.style.transform = "none";
320
340
  };
321
- if (typeof saved.left === "number" && typeof saved.top === "number") placeAt(saved.left, saved.top);
341
+ const pinned = () => panel.style.right === "auto";
342
+ const pin = () => {
343
+ if (pinned()) return;
344
+ const r = panel.getBoundingClientRect();
345
+ placeAt(r.left, r.top);
346
+ };
347
+ const refit = () => {
348
+ panel.style.width = `${fitW(px(panel.style.width))}px`;
349
+ panel.style.height = `${fitH(px(panel.style.height))}px`;
350
+ if (pinned()) placeAt(px(panel.style.left), px(panel.style.top));
351
+ };
352
+ if (typeof saved.left === "number" && typeof saved.top === "number") {
353
+ panel.style.width = `${fitW(W)}px`;
354
+ panel.style.height = `${fitH(H)}px`;
355
+ placeAt(saved.left, saved.top);
356
+ }
322
357
  const mem = { ...saved };
323
358
  const save = (what) => {
324
359
  if (!storeKey) return;
@@ -327,7 +362,7 @@ function mountDevtoolsOverlay(opts = {}) {
327
362
  mem.width = parseFloat(panel.style.width);
328
363
  mem.height = parseFloat(panel.style.height);
329
364
  }
330
- if (what === "move") {
365
+ if (what === "move" || what === "size") {
331
366
  mem.left = parseFloat(panel.style.left);
332
367
  mem.top = parseFloat(panel.style.top);
333
368
  }
@@ -338,13 +373,9 @@ function mountDevtoolsOverlay(opts = {}) {
338
373
  };
339
374
  const drag = (e, what, move) => {
340
375
  e.preventDefault();
341
- let lx = e.clientX, ly = e.clientY;
376
+ const sx = e.clientX, sy = e.clientY;
342
377
  frame.style.pointerEvents = "none";
343
- const mm = (ev) => {
344
- move(ev.clientX - lx, ev.clientY - ly);
345
- lx = ev.clientX;
346
- ly = ev.clientY;
347
- };
378
+ const mm = (ev) => move(ev.clientX - sx, ev.clientY - sy);
348
379
  const up = () => {
349
380
  removeEventListener("pointermove", mm);
350
381
  removeEventListener("pointerup", up);
@@ -356,23 +387,43 @@ function mountDevtoolsOverlay(opts = {}) {
356
387
  };
357
388
  head.onpointerdown = (e) => {
358
389
  if (e.target.closest("a,button")) return;
359
- drag(e, "move", (dx, dy) => {
360
- const r = panel.getBoundingClientRect();
361
- placeAt(r.left + dx, r.top + dy);
362
- });
363
- };
364
- grip.onpointerdown = (e) => {
365
- e.stopPropagation();
366
- drag(e, "size", (dx, dy) => {
367
- const r = panel.getBoundingClientRect();
368
- panel.style.width = `${Math.max(MIN_W, r.width + dx)}px`;
369
- panel.style.height = `${Math.max(MIN_H, r.height + dy)}px`;
370
- });
390
+ pin();
391
+ refit();
392
+ const l0 = px(panel.style.left), t0 = px(panel.style.top);
393
+ drag(e, "move", (dx, dy) => placeAt(l0 + dx, t0 + dy));
371
394
  };
395
+ for (const h of handles) {
396
+ const edge = h.dataset.edge;
397
+ h.onpointerdown = (e) => {
398
+ e.stopPropagation();
399
+ pin();
400
+ refit();
401
+ const l0 = px(panel.style.left), t0 = px(panel.style.top);
402
+ const w0 = px(panel.style.width), h0 = px(panel.style.height);
403
+ drag(e, "size", (dx, dy) => {
404
+ let left = l0, top = t0, w = w0, hh = h0;
405
+ if (edge.includes("e")) w = Math.max(MIN_W, Math.min(w0 + dx, vw() - l0));
406
+ if (edge.includes("s")) hh = Math.max(MIN_H, Math.min(h0 + dy, vh() - t0));
407
+ if (edge.includes("w")) {
408
+ left = Math.max(0, Math.min(l0 + dx, l0 + w0 - MIN_W));
409
+ w = l0 + w0 - left;
410
+ }
411
+ if (edge.includes("n")) {
412
+ top = Math.max(0, Math.min(t0 + dy, t0 + h0 - MIN_H));
413
+ hh = t0 + h0 - top;
414
+ }
415
+ panel.style.width = `${w}px`;
416
+ panel.style.height = `${hh}px`;
417
+ panel.style.left = `${left}px`;
418
+ panel.style.top = `${top}px`;
419
+ });
420
+ };
421
+ }
372
422
  const set = (v, focus = false, persist = true) => {
373
423
  const hadFocus = panel.contains(document.activeElement);
374
424
  open = v;
375
425
  panel.style.display = v ? "flex" : "none";
426
+ if (v) refit();
376
427
  btn.style.display = v ? "none" : "block";
377
428
  btn.setAttribute("aria-expanded", String(v));
378
429
  if (v && focus) frame.focus();
@@ -394,8 +445,14 @@ function mountDevtoolsOverlay(opts = {}) {
394
445
  route = d.hash;
395
446
  }
396
447
  };
448
+ const onResize = () => {
449
+ if (open) refit();
450
+ };
397
451
  if (isHotkey) addEventListener("keydown", onKey);
398
452
  addEventListener("message", onMessage);
453
+ addEventListener("resize", onResize);
454
+ const ro = typeof ResizeObserver === "function" ? new ResizeObserver(onResize) : void 0;
455
+ ro?.observe(document.documentElement);
399
456
  set(opts.startOpen ?? saved.open === true, false, false);
400
457
  return {
401
458
  open: () => set(true),
@@ -404,6 +461,8 @@ function mountDevtoolsOverlay(opts = {}) {
404
461
  unmount: () => {
405
462
  removeEventListener("keydown", onKey);
406
463
  removeEventListener("message", onMessage);
464
+ removeEventListener("resize", onResize);
465
+ ro?.disconnect();
407
466
  panel.remove();
408
467
  btn.remove();
409
468
  }
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  devtoolsEnabled,
5
5
  initDevtools,
6
6
  mountDevtoolsOverlay
7
- } from "./chunk-AGT6VNZI.js";
7
+ } from "./chunk-6E54FXKS.js";
8
8
  import "./chunk-57NGHQUL.js";
9
9
  export {
10
10
  DEVTOOLS_PARAM,
package/dist/node.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  connectDevtools,
4
4
  devtoolsEnabled,
5
5
  initDevtools
6
- } from "./chunk-AGT6VNZI.js";
6
+ } from "./chunk-6E54FXKS.js";
7
7
  import "./chunk-57NGHQUL.js";
8
8
  export {
9
9
  DEVTOOLS_PARAM,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolljs/devtools",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Devtools transport + local dashboard server for AtollJS instrumentation events",
5
5
  "homepage": "https://jwhenry3.github.io/atolljs/consumer/quickstart/",
6
6
  "type": "module",
@@ -39,7 +39,7 @@
39
39
  "registry": "https://registry.npmjs.org"
40
40
  },
41
41
  "peerDependencies": {
42
- "@atolljs/core": "0.1.8"
42
+ "@atolljs/core": "0.1.10"
43
43
  },
44
44
  "repository": {
45
45
  "type": "git",
package/src/overlay.ts CHANGED
@@ -66,7 +66,21 @@ const BTN_STYLE =
66
66
  const PANEL_STYLE =
67
67
  'position:fixed;z-index:2147483646;display:flex;flex-direction:column;' +
68
68
  'background:#0d1117;border:1px solid #30363d;border-radius:8px;' +
69
- 'box-shadow:0 12px 40px #000c;overflow:hidden;min-width:340px;min-height:220px';
69
+ 'box-shadow:0 12px 40px #000c;overflow:hidden;min-width:340px;min-height:220px;' +
70
+ 'max-width:100vw;max-height:100vh;box-sizing:border-box';
71
+
72
+ type Edge = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
73
+ const EDGE_CSS: Record<Edge, string> = {
74
+ n: 'top:0;left:10px;right:10px;height:4px;cursor:ns-resize',
75
+ s: 'bottom:0;left:10px;right:16px;height:5px;cursor:ns-resize',
76
+ e: 'right:0;top:10px;bottom:16px;width:5px;cursor:ew-resize',
77
+ w: 'left:0;top:10px;bottom:10px;width:5px;cursor:ew-resize',
78
+ ne: 'right:0;top:0;width:10px;height:10px;cursor:nesw-resize',
79
+ nw: 'left:0;top:0;width:10px;height:10px;cursor:nwse-resize',
80
+ sw: 'left:0;bottom:0;width:10px;height:10px;cursor:nesw-resize',
81
+ se: 'right:0;bottom:0;width:16px;height:16px;cursor:nwse-resize;' +
82
+ 'background:linear-gradient(135deg,transparent 50%,#30363d 50%);border-radius:0 0 7px',
83
+ };
70
84
 
71
85
  const ANCHOR: Record<DevtoolsOverlayPosition, (m: number) => string> = {
72
86
  topleft: (m) => `left:${m}px;top:${m}px`,
@@ -173,24 +187,51 @@ export function mountDevtoolsOverlay(opts: DevtoolsOverlayOptions = {}): Devtool
173
187
  frame.title = 'atoll devtools dashboard';
174
188
  frame.style.cssText = 'flex:1;border:0;background:#0d1117;min-height:0';
175
189
 
176
- // manual resize grip — CSS `resize` doesn't work over an iframe
177
- const grip = document.createElement('div');
178
- grip.setAttribute('aria-hidden', 'true');
179
- grip.style.cssText =
180
- 'position:absolute;right:0;bottom:0;width:16px;height:16px;cursor:nwse-resize;' +
181
- 'background:linear-gradient(135deg,transparent 50%,#30363d 50%);border-radius:0 0 7px';
182
- panel.append(head, frame, grip);
190
+ // Manual resize handles on every edge and corner (CSS `resize` doesn't work
191
+ // over an iframe). The visible se grip stays the panel's last child.
192
+ const handles = (['n', 's', 'e', 'w', 'ne', 'nw', 'sw', 'se'] as const).map((edge) => {
193
+ const h = document.createElement('div');
194
+ h.setAttribute('aria-hidden', 'true');
195
+ h.dataset.edge = edge;
196
+ h.style.cssText = `position:absolute;z-index:1;${EDGE_CSS[edge]}`;
197
+ return h;
198
+ });
199
+ panel.append(head, frame, ...handles);
183
200
  document.body.append(panel, btn);
184
201
 
202
+ const px = (v: string) => parseFloat(v) || 0;
203
+ const vw = () => document.documentElement.clientWidth || innerWidth;
204
+ const vh = () => document.documentElement.clientHeight || innerHeight;
205
+ const fitW = (w: number) => Math.max(MIN_W, Math.min(w, vw()));
206
+ const fitH = (h: number) => Math.max(MIN_H, Math.min(h, vh()));
207
+
208
+ /** Explicit left/top, clamped so the whole panel (close button included) stays in the viewport. */
185
209
  const placeAt = (left: number, top: number) => {
186
- // keep at least 40px of the title bar on screen
187
- panel.style.left = `${Math.min(Math.max(0, left), Math.max(0, innerWidth - 60))}px`;
188
- panel.style.top = `${Math.min(Math.max(0, top), Math.max(0, innerHeight - 40))}px`;
210
+ const w = px(panel.style.width), h = px(panel.style.height);
211
+ panel.style.left = `${Math.min(Math.max(0, left), Math.max(0, vw() - w))}px`;
212
+ panel.style.top = `${Math.min(Math.max(0, top), Math.max(0, vh() - h))}px`;
189
213
  panel.style.right = 'auto';
190
214
  panel.style.bottom = 'auto';
191
215
  panel.style.transform = 'none'; // drop the center anchor's translateX
192
216
  };
193
- if (typeof saved.left === 'number' && typeof saved.top === 'number') placeAt(saved.left, saved.top);
217
+ const pinned = () => panel.style.right === 'auto';
218
+ /** Swap a corner/center anchor for explicit left/top at the panel's current spot. */
219
+ const pin = () => {
220
+ if (pinned()) return;
221
+ const r = panel.getBoundingClientRect();
222
+ placeAt(r.left, r.top);
223
+ };
224
+ /** Shrink to the viewport and pull back on screen (window resized, or reopened smaller). */
225
+ const refit = () => {
226
+ panel.style.width = `${fitW(px(panel.style.width))}px`;
227
+ panel.style.height = `${fitH(px(panel.style.height))}px`;
228
+ if (pinned()) placeAt(px(panel.style.left), px(panel.style.top));
229
+ };
230
+ if (typeof saved.left === 'number' && typeof saved.top === 'number') {
231
+ panel.style.width = `${fitW(W)}px`;
232
+ panel.style.height = `${fitH(H)}px`;
233
+ placeAt(saved.left, saved.top);
234
+ }
194
235
 
195
236
  // Only what the user changed is remembered: an untouched size/position
196
237
  // keeps following the options.
@@ -202,23 +243,21 @@ export function mountDevtoolsOverlay(opts: DevtoolsOverlayOptions = {}): Devtool
202
243
  mem.width = parseFloat(panel.style.width);
203
244
  mem.height = parseFloat(panel.style.height);
204
245
  }
205
- if (what === 'move') {
246
+ // resizing pins the panel and a w/n edge moves it, so size saves position too
247
+ if (what === 'move' || what === 'size') {
206
248
  mem.left = parseFloat(panel.style.left);
207
249
  mem.top = parseFloat(panel.style.top);
208
250
  }
209
251
  try { localStorage.setItem(storeKey, JSON.stringify(mem)); } catch { /* storage off */ }
210
252
  };
211
253
 
212
- /** Pointer-drag helper: run `move` on pointermove until pointerup. */
254
+ /** Pointer-drag helper: run `move` with the total offset from the press until pointerup. */
213
255
  const drag = (e: PointerEvent, what: 'move' | 'size', move: (dx: number, dy: number) => void) => {
214
256
  e.preventDefault();
215
- let lx = e.clientX, ly = e.clientY;
257
+ const sx = e.clientX, sy = e.clientY;
216
258
  // the iframe would swallow pointermove while the cursor is over it
217
259
  frame.style.pointerEvents = 'none';
218
- const mm = (ev: PointerEvent) => {
219
- move(ev.clientX - lx, ev.clientY - ly);
220
- lx = ev.clientX; ly = ev.clientY;
221
- };
260
+ const mm = (ev: PointerEvent) => move(ev.clientX - sx, ev.clientY - sy);
222
261
  const up = () => {
223
262
  removeEventListener('pointermove', mm);
224
263
  removeEventListener('pointerup', up);
@@ -231,25 +270,45 @@ export function mountDevtoolsOverlay(opts: DevtoolsOverlayOptions = {}): Devtool
231
270
 
232
271
  head.onpointerdown = (e) => {
233
272
  if ((e.target as HTMLElement).closest('a,button')) return;
234
- drag(e, 'move', (dx, dy) => {
235
- const r = panel.getBoundingClientRect();
236
- placeAt(r.left + dx, r.top + dy);
237
- });
273
+ pin();
274
+ refit();
275
+ const l0 = px(panel.style.left), t0 = px(panel.style.top);
276
+ drag(e, 'move', (dx, dy) => placeAt(l0 + dx, t0 + dy));
238
277
  };
239
278
 
240
- grip.onpointerdown = (e) => {
241
- e.stopPropagation();
242
- drag(e, 'size', (dx, dy) => {
243
- const r = panel.getBoundingClientRect();
244
- panel.style.width = `${Math.max(MIN_W, r.width + dx)}px`;
245
- panel.style.height = `${Math.max(MIN_H, r.height + dy)}px`;
246
- });
247
- };
279
+ for (const h of handles) {
280
+ const edge = h.dataset.edge as Edge;
281
+ h.onpointerdown = (e) => {
282
+ e.stopPropagation();
283
+ pin();
284
+ refit();
285
+ const l0 = px(panel.style.left), t0 = px(panel.style.top);
286
+ const w0 = px(panel.style.width), h0 = px(panel.style.height);
287
+ drag(e, 'size', (dx, dy) => {
288
+ let left = l0, top = t0, w = w0, hh = h0;
289
+ if (edge.includes('e')) w = Math.max(MIN_W, Math.min(w0 + dx, vw() - l0));
290
+ if (edge.includes('s')) hh = Math.max(MIN_H, Math.min(h0 + dy, vh() - t0));
291
+ if (edge.includes('w')) {
292
+ left = Math.max(0, Math.min(l0 + dx, l0 + w0 - MIN_W));
293
+ w = l0 + w0 - left;
294
+ }
295
+ if (edge.includes('n')) {
296
+ top = Math.max(0, Math.min(t0 + dy, t0 + h0 - MIN_H));
297
+ hh = t0 + h0 - top;
298
+ }
299
+ panel.style.width = `${w}px`;
300
+ panel.style.height = `${hh}px`;
301
+ panel.style.left = `${left}px`;
302
+ panel.style.top = `${top}px`;
303
+ });
304
+ };
305
+ }
248
306
 
249
307
  const set = (v: boolean, focus = false, persist = true) => {
250
308
  const hadFocus = panel.contains(document.activeElement);
251
309
  open = v;
252
310
  panel.style.display = v ? 'flex' : 'none';
311
+ if (v) refit();
253
312
  btn.style.display = v ? 'none' : 'block';
254
313
  btn.setAttribute('aria-expanded', String(v));
255
314
  if (v && focus) frame.focus();
@@ -272,8 +331,13 @@ export function mountDevtoolsOverlay(opts: DevtoolsOverlayOptions = {}): Devtool
272
331
  route = d.hash;
273
332
  }
274
333
  };
334
+ const onResize = () => { if (open) refit(); };
275
335
  if (isHotkey) addEventListener('keydown', onKey);
276
336
  addEventListener('message', onMessage);
337
+ addEventListener('resize', onResize);
338
+ // a page scrollbar appearing shrinks the visible width without a window resize event
339
+ const ro = typeof ResizeObserver === 'function' ? new ResizeObserver(onResize) : undefined;
340
+ ro?.observe(document.documentElement);
277
341
 
278
342
  set(opts.startOpen ?? saved.open === true, false, false);
279
343
 
@@ -284,6 +348,8 @@ export function mountDevtoolsOverlay(opts: DevtoolsOverlayOptions = {}): Devtool
284
348
  unmount: () => {
285
349
  removeEventListener('keydown', onKey);
286
350
  removeEventListener('message', onMessage);
351
+ removeEventListener('resize', onResize);
352
+ ro?.disconnect();
287
353
  panel.remove();
288
354
  btn.remove();
289
355
  },