jev-devtools 0.1.2 → 0.2.0

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,183 @@
1
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState, } from "react";
2
+ import { fitCamera, zoomCamera, } from "./diagram-camera.js";
3
+ /** Lab-style camera. Pointer frames update the transform without rerendering cards. */
4
+ export function useDiagramViewport(width, height, scene) {
5
+ const viewport = useRef(null);
6
+ const canvas = useRef(null);
7
+ const camera = useRef({ x: 0, y: 0, scale: 1 });
8
+ const size = useRef({ width: 0, height: 0 });
9
+ const fitted = useRef(true);
10
+ const bounds = useRef({ width, height });
11
+ useLayoutEffect(() => {
12
+ bounds.current = { width, height };
13
+ }, [width, height]);
14
+ const frame = useRef(null);
15
+ const [zoom, setZoom] = useState(100);
16
+ const paint = useCallback(() => {
17
+ if (frame.current !== null)
18
+ return;
19
+ frame.current = requestAnimationFrame(() => {
20
+ frame.current = null;
21
+ const { x, y, scale } = camera.current;
22
+ if (canvas.current)
23
+ canvas.current.style.transform = `translate(${x}px, ${y}px) scale(${scale})`;
24
+ if (viewport.current) {
25
+ viewport.current.style.backgroundPosition = `${x}px ${y}px`;
26
+ viewport.current.style.backgroundSize = `${20 * scale}px ${20 * scale}px`;
27
+ }
28
+ setZoom(Math.round(scale * 100));
29
+ });
30
+ }, []);
31
+ const fit = useCallback(() => {
32
+ fitted.current = true;
33
+ if (size.current.width <= 0 || size.current.height <= 0)
34
+ return;
35
+ camera.current = fitCamera(size.current, bounds.current);
36
+ paint();
37
+ }, [paint]);
38
+ const preserveCamera = useCallback(() => {
39
+ fitted.current = false;
40
+ }, []);
41
+ useEffect(() => {
42
+ if (fitted.current)
43
+ fit();
44
+ }, [width, height, fit]);
45
+ const zoomBy = useCallback((factor, point) => {
46
+ fitted.current = false;
47
+ camera.current = zoomCamera(camera.current, factor, point ?? { x: size.current.width / 2, y: size.current.height / 2 });
48
+ paint();
49
+ }, [paint]);
50
+ useEffect(() => {
51
+ const el = viewport.current;
52
+ if (!el)
53
+ return;
54
+ const pointers = new Map();
55
+ const local = (event) => {
56
+ const rect = el.getBoundingClientRect();
57
+ return {
58
+ x: event.clientX - rect.left - el.clientLeft,
59
+ y: event.clientY - rect.top - el.clientTop,
60
+ };
61
+ };
62
+ const measure = () => {
63
+ const next = { width: el.clientWidth, height: el.clientHeight };
64
+ if (next.width <= 0 || next.height <= 0)
65
+ return; // Closed drawer.
66
+ const previous = size.current;
67
+ size.current = next;
68
+ if (fitted.current)
69
+ fit();
70
+ else {
71
+ camera.current.x += (next.width - previous.width) / 2;
72
+ camera.current.y += (next.height - previous.height) / 2;
73
+ paint();
74
+ }
75
+ };
76
+ const down = (event) => {
77
+ if (event.button !== 0 ||
78
+ pointers.size >= 2 ||
79
+ event.target.closest("button, input, select, textarea, a, summary"))
80
+ return;
81
+ event.preventDefault();
82
+ el.focus({ preventScroll: true });
83
+ el.setPointerCapture(event.pointerId);
84
+ pointers.set(event.pointerId, local(event));
85
+ el.dataset.panning = "true";
86
+ };
87
+ const move = (event) => {
88
+ if (!pointers.has(event.pointerId))
89
+ return;
90
+ const before = [...pointers.values()];
91
+ pointers.set(event.pointerId, local(event));
92
+ const after = [...pointers.values()];
93
+ fitted.current = false;
94
+ if (before.length === 1) {
95
+ camera.current.x += after[0].x - before[0].x;
96
+ camera.current.y += after[0].y - before[0].y;
97
+ }
98
+ else {
99
+ const center = (p) => ({
100
+ x: (p[0].x + p[1].x) / 2,
101
+ y: (p[0].y + p[1].y) / 2,
102
+ });
103
+ const distance = (p) => Math.hypot(p[0].x - p[1].x, p[0].y - p[1].y);
104
+ camera.current = zoomCamera(camera.current, distance(after) / Math.max(1, distance(before)), center(before), center(after));
105
+ }
106
+ paint();
107
+ };
108
+ const release = (event) => {
109
+ pointers.delete(event.pointerId);
110
+ if (el.hasPointerCapture(event.pointerId))
111
+ el.releasePointerCapture(event.pointerId);
112
+ if (!pointers.size)
113
+ delete el.dataset.panning;
114
+ };
115
+ const wheel = (event) => {
116
+ event.preventDefault();
117
+ const units = event.deltaMode === 1
118
+ ? 16
119
+ : event.deltaMode === 2
120
+ ? el.clientHeight
121
+ : 1;
122
+ // Match the lab viewer's anchored wheel/pinch zoom, including trackpads.
123
+ zoomBy(Math.exp(-Math.max(-100, Math.min(100, event.deltaY * units)) *
124
+ (event.ctrlKey ? 0.01 : 0.003)), local(event));
125
+ };
126
+ const keydown = (event) => {
127
+ if (event.target !== el)
128
+ return;
129
+ const step = event.shiftKey ? 100 : 40;
130
+ if (event.key === "+" || event.key === "=")
131
+ zoomBy(1.2);
132
+ else if (event.key === "-")
133
+ zoomBy(1 / 1.2);
134
+ else if (event.key === "0" || event.key === "Home")
135
+ fit();
136
+ else if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) {
137
+ fitted.current = false;
138
+ if (event.key === "ArrowLeft")
139
+ camera.current.x += step;
140
+ if (event.key === "ArrowRight")
141
+ camera.current.x -= step;
142
+ if (event.key === "ArrowUp")
143
+ camera.current.y += step;
144
+ if (event.key === "ArrowDown")
145
+ camera.current.y -= step;
146
+ paint();
147
+ }
148
+ else
149
+ return;
150
+ event.preventDefault();
151
+ };
152
+ fitted.current = true;
153
+ measure();
154
+ const observer = new ResizeObserver(measure);
155
+ observer.observe(el);
156
+ el.addEventListener("pointerdown", down);
157
+ el.addEventListener("pointermove", move);
158
+ el.addEventListener("pointerup", release);
159
+ el.addEventListener("pointercancel", release);
160
+ el.addEventListener("lostpointercapture", release);
161
+ el.addEventListener("wheel", wheel, { passive: false });
162
+ el.addEventListener("keydown", keydown);
163
+ return () => {
164
+ observer.disconnect();
165
+ el.removeEventListener("pointerdown", down);
166
+ el.removeEventListener("pointermove", move);
167
+ el.removeEventListener("pointerup", release);
168
+ el.removeEventListener("pointercancel", release);
169
+ el.removeEventListener("lostpointercapture", release);
170
+ el.removeEventListener("wheel", wheel);
171
+ el.removeEventListener("keydown", keydown);
172
+ for (const pointer of pointers.keys())
173
+ if (el.hasPointerCapture(pointer))
174
+ el.releasePointerCapture(pointer);
175
+ pointers.clear();
176
+ delete el.dataset.panning;
177
+ if (frame.current !== null)
178
+ cancelAnimationFrame(frame.current);
179
+ frame.current = null;
180
+ };
181
+ }, [fit, zoomBy, paint, scene]);
182
+ return { viewport, canvas, zoom, zoomBy, fit, preserveCamera };
183
+ }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "jev-devtools",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.12"
7
7
  },
8
8
  "dependencies": {
9
+ "@tanstack/react-virtual": "3.14.22",
9
10
  "@typesafe-ai/sdk": "0.7.0"
10
11
  },
11
12
  "description": "A React drawer and Node server adapter for inspecting TypeSafe Jev requests.",
@@ -46,7 +47,7 @@
46
47
  "check": "tsc --noEmit",
47
48
  "test": "node --import tsx --test tests/*.test.ts",
48
49
  "build": "tsc -p tsconfig.lib.json && node scripts/copy-styles.mjs",
49
- "prepack": "npm run build"
50
+ "prepack": "node ../../scripts/sync-readme-version.mjs --check && npm run build"
50
51
  },
51
52
  "publishConfig": {
52
53
  "access": "public",
@@ -79,3 +79,5 @@ Report HTTP/capture checks separately from visual browser checks, and sample ver
79
79
  | Production 404 | Expected when disabled; app inference remains independent. |
80
80
 
81
81
  Capture is bounded to 50 calls in memory by default. Built-in redaction has limits; arbitrary secrets in prose are not detected. Diagrams show captured inputs/outputs, not hidden model reasoning or application routing decisions.
82
+
83
+ The current browser requests `/events?protocol=2`: the first event is a full version 2 `snapshot`, followed by `delta` events containing changed traces and the retained ID order. The unqualified `/events` check above intentionally remains compatible with legacy clients. Reconnecting always supplies a fresh snapshot.