jev-devtools 0.1.2 → 0.1.3

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
@@ -1,6 +1,6 @@
1
1
  # Jev DevTools
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/jev-devtools/latest?color=E551BA&labelColor=0D0A0F&logo=npm)](https://www.npmjs.com/package/jev-devtools)
3
+ [![npm version 0.1.3](https://img.shields.io/badge/npm-v0.1.3-E551BA?labelColor=0D0A0F&logo=npm)](https://www.npmjs.com/package/jev-devtools/v/0.1.3)
4
4
 
5
5
  A React drawer and Node adapter for inspecting TypeSafe Jev requests with animated diagrams. Includes request history, context, questions, typed answers, transport attempts, and raw snapshots.
6
6
 
@@ -42,7 +42,7 @@ To try unpublished local changes instead:
42
42
  npm ci
43
43
  npm run pack:devtools
44
44
  # From the consuming app:
45
- npm install /absolute/path/to/jev-devtools-0.1.2.tgz
45
+ npm install /absolute/path/to/jev-devtools-0.1.3.tgz
46
46
  ```
47
47
 
48
48
  Mount once:
@@ -69,7 +69,7 @@ app.use(inspector.middleware); // Mount at root, before fallback handlers.
69
69
  // Call inspector.dispose() on shutdown.
70
70
  ```
71
71
 
72
- Only calls through that client are captured. The React component subscribes via same-origin SSE. Both sides default to disabled. API keys remain server-side. Browser code is SSR-safe and styles are scoped; no Tailwind or shadcn installation is required.
72
+ Only calls through that client are captured. The React component subscribes via same-origin SSE. Both sides default to disabled. API keys remain server-side. Browser code is SSR-safe. The drawer uses a Shadow DOM boundary and its own stylesheet so host page selectors cannot alter its internal layout. Existing stylesheet imports remain supported; no Tailwind or shadcn installation is required. Drag the Flow canvas to pan, scroll/pinch to zoom, or use Fit.
73
73
 
74
74
  History is bounded to 50 calls in memory by default. API routes and inspector endpoints must share an adapter instance; distributed processes and edge runtimes are not supported. Non-loopback use requires explicit authorization. Captures include bounded/redacted context and answers, not hidden model reasoning.
75
75
 
@@ -46,12 +46,12 @@ export function JevPanel({ traces, connection, onClear, }) {
46
46
  const trace = filtered.find((t) => t.id === selected) || filtered[0];
47
47
  return (_jsxs("div", { className: "jev-inspector", children: [_jsxs("aside", { className: "request-list", children: [_jsxs("header", { children: [_jsxs("h2", { children: ["Jev requests ", _jsx("span", { children: traces.length })] }), _jsx("button", { onClick: () => void onClear().catch((e) => setError(e.message)), disabled: !traces.length, children: "Clear" })] }), _jsxs("div", { className: "list-filters", children: [_jsx("input", { "aria-label": "Search requests", placeholder: "Search requests\u2026", value: search, onChange: (e) => setSearch(e.target.value) }), _jsx("select", { "aria-label": "Filter request status", value: status, onChange: (e) => setStatus(e.target.value), children: ["all", "pending", "success", "error", "cancelled"].map((s) => (_jsx("option", { children: s }, s))) })] }), _jsxs("div", { className: "request-items", children: [filtered.map((t) => (_jsxs("button", { className: `request-row ${trace?.id === t.id ? "selected" : ""}`, onClick: () => setSelected(t.id), children: [_jsx("span", { className: `status-dot ${t.status}` }), _jsxs("span", { children: [_jsx("strong", { children: t.label }), _jsxs("small", { children: [new Date(t.startedAt).toLocaleTimeString(), " \u00B7 ", t.source, " \u00B7", " ", ms(t.durationMs)] })] }), _jsx("span", { className: "row-status", children: t.status })] }, t.id))), !filtered.length && (_jsx("p", { className: "empty", children: traces.length
48
48
  ? "No matching requests."
49
- : "Run a scenario to capture your first request." }))] }), (connection !== "connected" || error) && (_jsxs("footer", { children: [connection !== "connected" && _jsx("span", { role: "status", children: connection }), error && _jsx("p", { role: "alert", children: error })] }))] }), _jsx("section", { className: "request-detail", children: trace ? (_jsxs(_Fragment, { children: [_jsxs("header", { className: "detail-header", children: [_jsxs("div", { children: [_jsxs("span", { className: "eyebrow", children: [trace.source, " response \u00B7 ", trace.status] }), _jsx("h2", { children: trace.label })] }), _jsxs("div", { className: "metrics", children: [_jsxs("span", { children: [ms(trace.durationMs), _jsx("small", { children: "Total duration" })] }), _jsxs("span", { children: [trace.attempts.length, _jsx("small", { children: "Attempts" })] }), _jsxs("span", { children: [trace.result?.usage
49
+ : "Run a scenario to capture your first request." }))] }), (connection !== "connected" || error) && (_jsxs("footer", { children: [connection !== "connected" && (_jsx("span", { role: "status", children: connection })), error && _jsx("p", { role: "alert", children: error })] }))] }), _jsx("section", { className: "request-detail", children: trace ? (_jsxs(_Fragment, { children: [_jsxs("header", { className: "detail-header", children: [_jsxs("div", { children: [_jsxs("span", { className: "eyebrow", children: [trace.source, " response \u00B7 ", trace.status] }), _jsx("h2", { children: trace.label })] }), _jsxs("div", { className: "metrics", children: [_jsxs("span", { children: [ms(trace.durationMs), _jsx("small", { children: "Total duration" })] }), _jsxs("span", { children: [trace.attempts.length, _jsx("small", { children: "Attempts" })] }), _jsxs("span", { children: [trace.result?.usage
50
50
  ? `${trace.result.usage.input_tokens} / ${trace.result.usage.output_tokens}`
51
51
  : "—", _jsx("small", { children: "Tokens in / out" })] })] })] }), _jsxs("div", { className: "detail-meta", children: [_jsx("code", { children: trace.result?.model || trace.request.model }), _jsx("code", { children: trace.requestId ||
52
52
  (trace.status === "pending"
53
53
  ? "Request ID pending"
54
- : "No request ID") })] }), _jsx("nav", { className: "detail-tabs", "aria-label": "Request views", children: ["Flow", "Questions", "Context", "Transport", "Raw"].map((name) => (_jsx("button", { "aria-pressed": tab === name, onClick: () => setTab(name), children: name }, name))) }), _jsxs("div", { className: "detail-body", children: [trace.error && (_jsxs("div", { role: "alert", className: "error-banner", children: [_jsx("strong", { children: trace.error.name }), " ", trace.error.message] })), trace.captureWarnings.map((w) => (_jsx("p", { className: "warning", children: w }, w))), tab === "Flow" && _jsx(TraceDiagram, { trace: trace }, trace.id), tab === "Questions" &&
54
+ : "No request ID") })] }), _jsx("nav", { className: "detail-tabs", "aria-label": "Request views", children: ["Flow", "Questions", "Context", "Transport", "Raw"].map((name) => (_jsx("button", { "aria-pressed": tab === name, onClick: () => setTab(name), children: name }, name))) }), _jsxs("div", { className: `detail-body ${tab === "Flow" ? "detail-body-flow" : ""}`, children: [trace.error && (_jsxs("div", { role: "alert", className: "error-banner", children: [_jsx("strong", { children: trace.error.name }), " ", trace.error.message] })), trace.captureWarnings.map((w) => (_jsx("p", { className: "warning", children: w }, w))), tab === "Flow" && _jsx(TraceDiagram, { trace: trace }, trace.id), tab === "Questions" &&
55
55
  Object.entries(trace.request.questions ?? {})
56
56
  .filter(([, q]) => q && typeof q === "object" && typeof q.type === "string")
57
57
  .map(([key, q]) => (_jsxs("article", { className: "question-node standalone", children: [_jsxs("header", { children: [_jsx("h3", { children: key }), _jsx("span", { className: "type", children: q.type })] }), _jsx(JsonBlock, { value: q }), _jsx(Answer, { answer: trace.result?.answers[key], image: findImage(trace.request.state) })] }, key))), tab === "Context" && (_jsxs(_Fragment, { children: [_jsx("p", { className: "muted", children: "Captured before dispatch. Known credential fields and email are redacted; captures are size limited." }), findImage(trace.request.state) && (_jsx("img", { className: "context-image", src: findImage(trace.request.state), alt: "Captured context" })), _jsx(JsonBlock, { value: trace.request.state }), trace.request.assets && (_jsx(JsonBlock, { value: trace.request.assets }))] })), tab === "Transport" && (_jsxs(_Fragment, { children: [_jsx("h3", { children: "HTTP attempts" }), _jsx("p", { className: "muted", children: "Attempt duration ends at response headers. Total duration includes SDK processing and retry waits." }), _jsxs("table", { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Attempt" }), _jsx("th", { children: "Status" }), _jsx("th", { children: "Duration" })] }) }), _jsx("tbody", { children: trace.attempts.map((a) => (_jsxs("tr", { children: [_jsx("td", { children: a.number }), _jsx("td", { children: a.error || a.status || "Pending" }), _jsx("td", { children: ms(a.durationMs) })] }, a.number))) })] }), !trace.attempts.length && _jsx("p", { children: "No HTTP attempt recorded." })] })), tab === "Raw" && _jsx(JsonBlock, { value: trace })] })] })) : (_jsxs("div", { className: "empty-detail", children: [_jsx("span", { className: "empty-symbol", children: "\u21B3" }), _jsx("h2", { children: "Follow a Jev request" }), _jsxs("p", { children: ["Context, parallel questions, typed answers.", _jsx("br", {}), "Run a sample above to see the complete flow."] })] })) })] }));
@@ -3,4 +3,4 @@ import { type ReactNode } from "react";
3
3
  export declare function JevDrawer({ children, defaultOpen, }: {
4
4
  children: ReactNode;
5
5
  defaultOpen?: boolean;
6
- }): import("react").ReactPortal;
6
+ }): import("react").ReactPortal | null;
@@ -2,6 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import { useEffect, useId, useRef, useState } from "react";
3
3
  import { TypeSafeMark } from "./TypeSafeMark.js";
4
4
  import { createPortal } from "react-dom";
5
+ import { inspectorStyles } from "./inspector-styles.js";
5
6
  const heightKey = "jev-inspector-drawer-height";
6
7
  const viewportHeight = () => window.visualViewport?.height ?? window.innerHeight;
7
8
  const limits = (viewport) => ({
@@ -26,6 +27,7 @@ function initialHeight() {
26
27
  /** Non-modal drawer: the app remains usable and children stay mounted when closed. */
27
28
  export function JevDrawer({ children, defaultOpen = true, }) {
28
29
  const id = useId();
30
+ const [root, setRoot] = useState(null);
29
31
  const [open, setOpen] = useState(defaultOpen);
30
32
  const [expanded, setExpanded] = useState(false);
31
33
  const [viewport, setViewport] = useState(viewportHeight);
@@ -37,6 +39,20 @@ export function JevDrawer({ children, defaultOpen = true, }) {
37
39
  const drag = useRef(null);
38
40
  const { min, max } = limits(viewport);
39
41
  const visibleHeight = expanded ? max : clamp(height, viewport);
42
+ useEffect(() => {
43
+ const host = document.createElement("jev-devtools-root");
44
+ // The host is the only element exposed to page selectors. The overlay itself
45
+ // lives in the shadow tree, with its own typography and component styles.
46
+ host.style.cssText =
47
+ "all: initial !important; position: fixed !important; inset: 0 !important; pointer-events: none !important; z-index: 2147483000 !important;";
48
+ const shadow = host.attachShadow({ mode: "open" });
49
+ const style = document.createElement("style");
50
+ style.textContent = inspectorStyles;
51
+ shadow.append(style);
52
+ document.body.append(host);
53
+ setRoot(shadow);
54
+ return () => host.remove();
55
+ }, []);
40
56
  useEffect(() => {
41
57
  const resize = () => setViewport(viewportHeight());
42
58
  window.addEventListener("resize", resize);
@@ -57,7 +73,7 @@ export function JevDrawer({ children, defaultOpen = true, }) {
57
73
  useEffect(() => {
58
74
  if (interacted.current)
59
75
  (open ? closeButton.current : trigger.current)?.focus();
60
- }, [open]);
76
+ }, [open, root]);
61
77
  const toggle = (value) => {
62
78
  interacted.current = true;
63
79
  setOpen(value);
@@ -66,7 +82,9 @@ export function JevDrawer({ children, defaultOpen = true, }) {
66
82
  setExpanded(false);
67
83
  setHeight(clamp(value, viewport));
68
84
  };
69
- return createPortal(_jsxs(_Fragment, { children: [_jsxs("button", { ref: trigger, className: "jev-devtools jev-drawer-trigger", hidden: open, "aria-label": "Open Jev DevTools", "aria-expanded": open, "aria-controls": id, onClick: () => toggle(true), children: [_jsx(TypeSafeMark, {}), " Jev DevTools"] }), _jsxs("section", { ref: panel, id: id, className: "jev-devtools jev-drawer", hidden: !open, "aria-label": "Jev DevTools", style: { height: visibleHeight }, onKeyDown: (event) => {
85
+ if (!root)
86
+ return null;
87
+ return createPortal(_jsxs(_Fragment, { children: [_jsxs("button", { ref: trigger, className: "jev-devtools jev-drawer-trigger", hidden: open, "aria-label": "Open Jev DevTools", "aria-expanded": open, "aria-controls": id, onClick: () => toggle(true), children: [_jsx(TypeSafeMark, {}), " Jev DevTools"] }), _jsxs("div", { ref: panel, id: id, className: "jev-devtools jev-drawer", hidden: !open, role: "region", "aria-label": "Jev DevTools", style: { height: visibleHeight }, onKeyDown: (event) => {
70
88
  if (event.key === "Escape" && !event.defaultPrevented) {
71
89
  event.stopPropagation();
72
90
  toggle(false);
@@ -107,5 +125,5 @@ export function JevDrawer({ children, defaultOpen = true, }) {
107
125
  event.preventDefault();
108
126
  resizeTo(next);
109
127
  }
110
- }, children: _jsx("span", {}) }), _jsxs("header", { className: "jev-drawer-header", children: [_jsxs("strong", { children: [_jsx(TypeSafeMark, {}), "Jev DevTools"] }), _jsxs("div", { children: [_jsx("button", { "aria-label": expanded ? "Restore Jev drawer" : "Expand Jev drawer", "aria-pressed": expanded, onClick: () => setExpanded(!expanded), children: expanded ? "Restore" : "Expand" }), _jsx("button", { ref: closeButton, className: "jev-drawer-close", "aria-label": "Close Jev DevTools", title: "Close (Esc)", onClick: () => toggle(false), children: "\u00D7" })] })] }), _jsx("div", { className: "jev-drawer-content", children: children })] })] }), document.body);
128
+ }, children: _jsx("span", {}) }), _jsxs("header", { className: "jev-drawer-header", children: [_jsxs("strong", { children: [_jsx(TypeSafeMark, {}), "Jev DevTools"] }), _jsxs("div", { children: [_jsx("button", { "aria-label": expanded ? "Restore Jev drawer" : "Expand Jev drawer", "aria-pressed": expanded, onClick: () => setExpanded(!expanded), children: expanded ? "Restore" : "Expand" }), _jsx("button", { ref: closeButton, className: "jev-drawer-close", "aria-label": "Close Jev DevTools", title: "Close (Esc)", onClick: () => toggle(false), children: "\u00D7" })] })] }), _jsx("div", { className: "jev-drawer-content", children: children })] })] }), root);
111
129
  }
@@ -1,5 +1,6 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from "react";
2
+ import { useState } from "react";
3
+ import { useDiagramViewport } from "./useDiagramViewport.js";
3
4
  import { findImage, flowAnswer, stateText } from "./flow-model.js";
4
5
  const pct = (v) => `${Math.round(v * 100)}%`;
5
6
  function Result({ answer, image }) {
@@ -23,8 +24,7 @@ function Result({ answer, image }) {
23
24
  } }))] }), _jsxs("p", { className: "lab-small lab-coordinates", children: ["left ", answer.box.left.toFixed(2), " \u00B7 top ", answer.box.top.toFixed(2), _jsx("br", {}), "right ", answer.box.right.toFixed(2), " \u00B7 bottom", " ", answer.box.bottom.toFixed(2)] })] })), answer.confidence !== undefined && (_jsxs("p", { className: "lab-small", children: ["Confidence ", pct(answer.confidence)] }))] }));
24
25
  }
25
26
  export function TraceDiagram({ trace }) {
26
- const [selected, setSelected] = useState(""), [zoom, setZoom] = useState(0.8), [flow, setFlow] = useState(true);
27
- const viewport = useRef(null);
27
+ const [selected, setSelected] = useState(""), [flow, setFlow] = useState(true);
28
28
  const entries = Object.entries(trace.request.questions ?? {}).filter(([, q]) => q && typeof q === "object" && typeof q.type === "string"), [key, question] = entries.find(([key]) => key === selected) ??
29
29
  entries[0] ?? ["", undefined];
30
30
  const answer = trace.result?.answers[key], image = findImage(trace.request.state) ||
@@ -33,14 +33,7 @@ export function TraceDiagram({ trace }) {
33
33
  Object.keys(answer?.probabilities ?? {}).length * 44 +
34
34
  (answer?.score !== undefined ? 50 : 0), answer?.box ? 390 : 0);
35
35
  const height = Math.max(520, resultHeight + 90, outputs.length * 136 + 60), ry = Math.max(40, (height - resultHeight) / 2), mid = ry + resultHeight / 2;
36
- const fit = () => {
37
- if (viewport.current)
38
- setZoom(Math.min(1, (viewport.current.clientWidth - 24) / 1120));
39
- };
40
- useEffect(() => {
41
- if (viewport.current)
42
- setZoom(Math.min(1, Math.max(0.75, (viewport.current.clientWidth - 24) / 1120)));
43
- }, []);
36
+ const { viewport, canvas, zoom, zoomBy, fit } = useDiagramViewport(1120, height, key);
44
37
  if (!question)
45
38
  return _jsx("p", { className: "muted", children: "No questions available in this capture." });
46
39
  const sourcePath = (y) => `M260 ${y} C325 ${y},325 ${mid},390 ${mid}`;
@@ -53,21 +46,21 @@ export function TraceDiagram({ trace }) {
53
46
  active: output.active,
54
47
  })),
55
48
  ];
56
- return (_jsxs("div", { className: "lab-flow", children: [_jsxs("div", { className: "lab-flow-toolbar", children: [_jsxs("label", { children: ["Question", _jsx("select", { "aria-label": "Diagram question", value: key, onChange: (event) => setSelected(event.target.value), children: entries.map(([name, q]) => (_jsxs("option", { value: name, children: [name, " \u00B7 ", q.type] }, name))) })] }), _jsxs("div", { children: [_jsx("button", { "aria-label": "Zoom diagram out", onClick: () => setZoom((v) => Math.max(0.4, v - 0.1)), children: "\u2212" }), _jsxs("output", { "aria-label": "Diagram zoom", children: [Math.round(zoom * 100), "%"] }), _jsx("button", { "aria-label": "Zoom diagram in", onClick: () => setZoom((v) => Math.min(1.5, v + 0.1)), children: "+" }), _jsx("button", { onClick: fit, children: "Fit" }), _jsx("button", { "aria-pressed": flow, onClick: () => setFlow(!flow), children: flow ? "Pause flow" : "Play flow" })] })] }), _jsx("div", { className: "lab-flow-viewport", ref: viewport, tabIndex: 0, "aria-label": "Jev diagram, scroll to explore", children: _jsx("div", { style: { width: 1120 * zoom, height: height * zoom }, children: _jsxs("div", { className: "lab-flow-canvas", style: { width: 1120, height, transform: `scale(${zoom})` }, children: [_jsxs("svg", { className: "lab-connections", width: "1120", height: height, "aria-hidden": "true", children: [paths.map((path) => (_jsxs("g", { children: [_jsx("path", { d: path.d, className: path.active ? "active" : "" }), flow &&
57
- path.active &&
58
- !["error", "cancelled"].includes(trace.status) && (_jsx("circle", { r: "3", className: "lab-flow-dot", children: _jsx("animateMotion", { dur: "3.2s", repeatCount: "indefinite", path: path.d }) }))] }, path.id))), _jsx("text", { x: "305", y: mid - 65, children: "state" }), _jsx("text", { x: "300", y: mid + 65, children: "question" })] }), _jsxs("article", { className: "lab-card", style: { left: 20, top: 32, width: 240, height: 180 }, children: [_jsx("span", { className: "lab-eyebrow", children: image ? "IMAGE STATE" : "STATE" }), _jsx("h3", { children: "Shared context" }), image ? (_jsx("img", { className: "lab-source-image", src: image, alt: "Captured image state" })) : (_jsx("p", { className: "lab-copy", title: stateText(trace), children: stateText(trace) }))] }), _jsxs("article", { className: "lab-card", style: { left: 20, top: 288, width: 240, height: 180 }, children: [_jsx("span", { className: "lab-eyebrow", children: "INSTRUCTIONS" }), _jsxs("h3", { children: ["Question \u00B7 ", question.type] }), _jsx("p", { className: "lab-copy", title: typeof question.instructions === "string"
59
- ? question.instructions
60
- : JSON.stringify(question.instructions), children: typeof question.instructions === "string"
61
- ? question.instructions
62
- : JSON.stringify(question.instructions) })] }), _jsxs("article", { className: "lab-card lab-answer-card", style: {
63
- left: 390,
64
- top: ry,
65
- width: 320,
66
- minHeight: resultHeight,
67
- }, children: [_jsxs("span", { className: "lab-eyebrow", children: ["JEV \u00B7 ", question.type.toUpperCase()] }), _jsx("h3", { title: key, children: key }), _jsx(Result, { answer: answer, image: image })] }), outputs.map((output, i) => (_jsxs("article", { className: `lab-card lab-output ${output.active ? "" : "lab-idle"}`, style: {
68
- left: 870,
69
- top: 40 + i * 136,
70
- width: 230,
71
- minHeight: 104,
72
- }, children: [_jsx("span", { className: "lab-eyebrow", children: output.eyebrow }), _jsx("h3", { title: output.label, children: output.label }), _jsx("p", { children: output.detail })] }, i)))] }) }) }), _jsxs("p", { className: "muted", children: [entries.length, " parallel question", entries.length === 1 ? "" : "s", " in one request. Connections show data flow, not model timing. Application routes and thresholds are not captured. Full inputs and criteria are in Questions and Context."] })] }));
49
+ return (_jsxs("div", { className: "lab-flow", children: [_jsxs("div", { className: "lab-flow-toolbar", children: [_jsxs("label", { children: ["Question", _jsx("select", { "aria-label": "Diagram question", value: key, onChange: (event) => setSelected(event.target.value), children: entries.map(([name, q]) => (_jsxs("option", { value: name, children: [name, " \u00B7 ", q.type] }, name))) })] }), _jsxs("div", { children: [_jsx("button", { "aria-label": "Zoom diagram out", disabled: zoom <= 10, onClick: () => zoomBy(1 / 1.2), children: "\u2212" }), _jsxs("output", { "aria-label": "Diagram zoom", children: [zoom, "%"] }), _jsx("button", { "aria-label": "Zoom diagram in", disabled: zoom >= 400, onClick: () => zoomBy(1.2), children: "+" }), _jsx("button", { onClick: fit, children: "Fit" }), _jsx("button", { "aria-pressed": flow, onClick: () => setFlow(!flow), children: flow ? "Pause flow" : "Play flow" })] })] }), _jsx("div", { className: "lab-flow-viewport", ref: viewport, tabIndex: 0, role: "group", "aria-label": "Jev diagram. Drag to pan, scroll or pinch to zoom. Arrow keys pan, plus and minus zoom, zero fits.", children: _jsxs("div", { ref: canvas, className: "lab-flow-canvas", style: { width: 1120, height }, children: [_jsxs("svg", { className: "lab-connections", width: "1120", height: height, "aria-hidden": "true", children: [paths.map((path) => (_jsxs("g", { children: [_jsx("path", { d: path.d, className: path.active ? "active" : "" }), flow &&
50
+ path.active &&
51
+ !["error", "cancelled"].includes(trace.status) && (_jsx("circle", { r: "3", className: "lab-flow-dot", children: _jsx("animateMotion", { dur: "3.2s", repeatCount: "indefinite", path: path.d }) }))] }, path.id))), _jsx("text", { x: "305", y: mid - 65, children: "state" }), _jsx("text", { x: "300", y: mid + 65, children: "question" })] }), _jsxs("article", { className: "lab-card", style: { left: 20, top: 32, width: 240, height: 180 }, children: [_jsx("span", { className: "lab-eyebrow", children: image ? "IMAGE STATE" : "STATE" }), _jsx("h3", { children: "Shared context" }), image ? (_jsx("img", { className: "lab-source-image", src: image, alt: "Captured image state" })) : (_jsx("p", { className: "lab-copy", title: stateText(trace), children: stateText(trace) }))] }), _jsxs("article", { className: "lab-card", style: { left: 20, top: 288, width: 240, height: 180 }, children: [_jsx("span", { className: "lab-eyebrow", children: "INSTRUCTIONS" }), _jsxs("h3", { children: ["Question \u00B7 ", question.type] }), _jsx("p", { className: "lab-copy", title: typeof question.instructions === "string"
52
+ ? question.instructions
53
+ : JSON.stringify(question.instructions), children: typeof question.instructions === "string"
54
+ ? question.instructions
55
+ : JSON.stringify(question.instructions) })] }), _jsxs("article", { className: "lab-card lab-answer-card", style: {
56
+ left: 390,
57
+ top: ry,
58
+ width: 320,
59
+ minHeight: resultHeight,
60
+ }, children: [_jsxs("span", { className: "lab-eyebrow", children: ["JEV \u00B7 ", question.type.toUpperCase()] }), _jsx("h3", { title: key, children: key }), _jsx(Result, { answer: answer, image: image })] }), outputs.map((output, i) => (_jsxs("article", { className: `lab-card lab-output ${output.active ? "" : "lab-idle"}`, style: {
61
+ left: 870,
62
+ top: 40 + i * 136,
63
+ width: 230,
64
+ minHeight: 104,
65
+ }, children: [_jsx("span", { className: "lab-eyebrow", children: output.eyebrow }), _jsx("h3", { title: output.label, children: output.label }), _jsx("p", { children: output.detail })] }, i)))] }) }), _jsxs("p", { className: "muted lab-flow-caption", children: [_jsx("span", { className: "lab-gesture-hint", children: "Drag to pan \u00B7 Scroll or pinch to zoom \u00B7 0 to fit" }), entries.length, " parallel question", entries.length === 1 ? "" : "s", " in one request. Connections show data flow, not model timing. Application routes and thresholds are not captured. Full inputs and criteria are in Questions and Context."] })] }));
73
66
  }
@@ -0,0 +1,16 @@
1
+ export type Point = {
2
+ x: number;
3
+ y: number;
4
+ };
5
+ export type Camera = Point & {
6
+ scale: number;
7
+ };
8
+ export type Size = {
9
+ width: number;
10
+ height: number;
11
+ };
12
+ export declare const minScale = 0.1;
13
+ export declare const maxScale = 4;
14
+ export declare function fitCamera(view: Size, content: Size): Camera;
15
+ /** Keep the content under `from` under `to`, including while a pinch moves. */
16
+ export declare function zoomCamera(camera: Camera, factor: number, from: Point, to?: Point): Camera;
@@ -0,0 +1,20 @@
1
+ export const minScale = 0.1;
2
+ export const maxScale = 4;
3
+ const limit = (scale) => Math.max(minScale, Math.min(maxScale, scale));
4
+ export function fitCamera(view, content) {
5
+ const scale = limit(Math.min((view.width - 48) / content.width, (view.height - 48) / content.height, 1));
6
+ return {
7
+ x: (view.width - content.width * scale) / 2,
8
+ y: (view.height - content.height * scale) / 2,
9
+ scale,
10
+ };
11
+ }
12
+ /** Keep the content under `from` under `to`, including while a pinch moves. */
13
+ export function zoomCamera(camera, factor, from, to = from) {
14
+ const scale = limit(camera.scale * factor);
15
+ return {
16
+ x: to.x - ((from.x - camera.x) * scale) / camera.scale,
17
+ y: to.y - ((from.y - camera.y) * scale) / camera.scale,
18
+ scale,
19
+ };
20
+ }
@@ -0,0 +1,2 @@
1
+ /** Generated from inspector.css during the package build. */
2
+ export declare const inspectorStyles: string;
@@ -0,0 +1 @@
1
+ export const inspectorStyles = ".jev-devtools {\n all: initial;\n display: block;\n direction: ltr;\n unicode-bidi: isolate;\n pointer-events: auto;\n color-scheme: dark;\n font:\n 12px system-ui,\n sans-serif;\n color: #eee8ed;\n line-height: 1.5;\n}\n.jev-devtools,\n.jev-devtools * {\n box-sizing: border-box;\n}\n.jev-devtools button,\n.jev-devtools input,\n.jev-devtools select,\n.jev-devtools textarea {\n font: inherit;\n color: inherit;\n border: 1px solid #3b303d;\n border-radius: 5px;\n background: #17121b;\n}\n.jev-devtools button {\n padding: 7px 11px;\n cursor: pointer;\n}\n.jev-devtools button:disabled {\n opacity: 0.45;\n cursor: default;\n}\n.jev-devtools input,\n.jev-devtools select {\n padding: 8px;\n width: 100%;\n}\n.jev-devtools button:hover:not(:disabled) {\n border-color: #e551ba;\n}\n.jev-devtools :is(button, input, select, textarea, summary):focus-visible {\n outline: 2px solid #e551ba;\n outline-offset: 3px;\n}\n.jev-devtools .eyebrow {\n font-size: 10px;\n letter-spacing: 0.12em;\n color: #bc9db5;\n text-transform: uppercase;\n}\n.jev-devtools .jev-inspector {\n height: 100%;\n display: flex;\n color: #eee8ed;\n background: #0d0a0f;\n font:\n 12px \"DM Sans\",\n sans-serif;\n min-height: 0;\n overflow: hidden;\n}\n.jev-devtools .jev-inspector h2 {\n font-size: 15px;\n margin: 0;\n font-weight: 600;\n}\n.jev-devtools .jev-inspector h3 {\n font-size: 13px;\n margin: 0 0 8px;\n}\n.jev-devtools .jev-inspector code,\n.jev-devtools .jev-inspector pre {\n font:\n 11px \"JetBrains Mono\",\n monospace;\n}\n.jev-devtools .jev-inspector pre {\n line-height: 1.7;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n max-height: 460px;\n overflow: auto;\n background: #100d13;\n padding: 16px;\n border: 1px solid #2e2533;\n border-radius: 5px;\n}\n.jev-devtools .jev-inspector .muted {\n color: #a89bac;\n font-size: 11px;\n line-height: 1.6;\n}\n.jev-devtools .request-list {\n flex: 0 0 300px;\n border-right: 1px solid #302634;\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n.jev-devtools .request-list > header {\n padding: 17px 14px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n.jev-devtools .request-list h2 span {\n font-size: 11px;\n color: #b796ad;\n margin-left: 5px;\n}\n.jev-devtools .request-list button {\n font-size: 11px;\n}\n.jev-devtools .list-filters {\n display: flex;\n gap: 7px;\n padding: 0 12px 12px;\n}\n.jev-devtools .list-filters input {\n min-width: 0;\n}\n.jev-devtools .list-filters select {\n width: 86px;\n font-size: 11px;\n}\n.jev-devtools .request-items {\n overflow: auto;\n flex: 1;\n}\n.jev-devtools .request-row {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n text-align: left;\n border: 0;\n border-top: 1px solid #251d2a;\n border-radius: 0;\n background: transparent;\n padding: 16px 12px;\n}\n.jev-devtools .request-row.selected {\n background: #251423;\n box-shadow: inset 2px 0 #e551ba;\n}\n.jev-devtools .request-row strong {\n display: block;\n font-size: 12px;\n font-weight: 500;\n}\n.jev-devtools .request-row small {\n display: block;\n font-size: 10px;\n color: #a99aac;\n margin-top: 6px;\n}\n.jev-devtools .row-status {\n font-size: 9px;\n color: #ad9bb0;\n margin-left: auto;\n}\n.jev-devtools .status-dot {\n width: 6px;\n height: 6px;\n display: inline-block;\n border-radius: 50%;\n background: #b09ab7;\n flex-shrink: 0;\n}\n.jev-devtools .status-dot.success {\n background: #b3c79c;\n}\n.jev-devtools .status-dot.pending {\n background: #e551ba;\n}\n.jev-devtools .status-dot.error {\n background: #ee8e85;\n}\n.jev-devtools .request-list footer {\n padding: 12px;\n color: #9e8ea5;\n border-top: 1px solid #302634;\n font-size: 10px;\n display: flex;\n gap: 7px;\n}\n.jev-devtools .request-detail {\n flex: 1;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.jev-devtools .detail-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 20px;\n padding: 18px 24px 10px;\n}\n.jev-devtools .detail-header h2 {\n margin-top: 6px;\n}\n.jev-devtools .metrics {\n display: flex;\n gap: 24px;\n font:\n 12px \"JetBrains Mono\",\n monospace;\n white-space: nowrap;\n}\n.jev-devtools .metrics small {\n display: block;\n font:\n 9px \"DM Sans\",\n sans-serif;\n color: #97879f;\n margin-top: 6px;\n}\n.jev-devtools .detail-meta {\n display: flex;\n gap: 20px;\n flex-wrap: wrap;\n padding: 0 24px 14px;\n color: #a08aa5;\n}\n.jev-devtools .detail-meta code {\n font-size: 10px;\n}\n.jev-devtools .detail-tabs {\n display: flex;\n gap: 20px;\n border-bottom: 1px solid #302634;\n padding: 0 24px;\n}\n.jev-devtools .detail-tabs button {\n border: 0;\n border-radius: 0;\n background: none;\n padding: 10px 0;\n color: #a89bac;\n font-size: 11px;\n}\n.jev-devtools .detail-tabs button[aria-pressed=\"true\"] {\n box-shadow: inset 0 -2px #e551ba;\n color: #f5c1e6;\n}\n.jev-devtools .detail-body {\n overflow: auto;\n flex: 1;\n min-height: 0;\n padding: 16px 24px 28px;\n background-image: radial-gradient(#342337 0.6px, transparent 0.6px);\n background-size: 16px 16px;\n}\n.jev-devtools .request-flow {\n display: flex;\n align-items: center;\n gap: 40px;\n padding: 14px 0 20px;\n}\n.jev-devtools .state-node {\n position: relative;\n flex: 0 0 160px;\n border: 1px solid #e551ba;\n padding: 20px 15px;\n background: #1f1320;\n border-radius: 6px;\n}\n.jev-devtools .state-node h3 {\n margin-top: 12px;\n}\n.jev-devtools .state-node p {\n color: #b69db3;\n line-height: 1.5;\n}\n.jev-devtools .state-node code {\n font-size: 10px;\n}\n.jev-devtools .state-node:after {\n content: \"\";\n position: absolute;\n left: 100%;\n top: 50%;\n width: 20px;\n border-top: 1px solid #7f416d;\n}\n.jev-devtools .question-branches {\n border-left: 1px solid #7f416d;\n padding-left: 20px;\n display: grid;\n grid-template-columns: repeat(3, minmax(160px, 1fr));\n gap: 12px;\n flex: 1;\n position: relative;\n}\n.jev-devtools .question-node {\n position: relative;\n border: 1px solid #75516c;\n border-radius: 6px;\n padding: 15px;\n background: #110d14;\n min-width: 0;\n}\n.jev-devtools .question-node:before {\n content: \"\";\n position: absolute;\n right: 100%;\n top: 50%;\n width: 21px;\n border-top: 1px solid #7f416d;\n}\n.jev-devtools .question-branches .question-node:not(:first-child):before {\n display: none;\n}\n.jev-devtools .question-node header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n.jev-devtools .question-node header h3 {\n margin: 0;\n overflow-wrap: anywhere;\n}\n.jev-devtools .type {\n font:\n 9px \"JetBrains Mono\",\n monospace;\n color: #e59acc;\n border: 1px solid #5e3654;\n border-radius: 3px;\n padding: 3px 5px;\n}\n.jev-devtools .instructions {\n font-size: 11px;\n color: #b8a6bc;\n line-height: 1.5;\n}\n.jev-devtools .answer-value {\n display: block;\n font-size: 20px;\n font-weight: 500;\n margin: 15px 0;\n color: #f0c7e5;\n}\n.jev-devtools .answer-value small {\n font-size: 10px;\n color: #b5a1ba;\n}\n.jev-devtools .probability {\n margin: 10px 0;\n font-size: 10px;\n}\n.jev-devtools .probability > div:first-child {\n display: flex;\n justify-content: space-between;\n gap: 5px;\n margin-bottom: 5px;\n}\n.jev-devtools .probability b {\n font-weight: 400;\n color: #d8abc9;\n}\n.jev-devtools .bar {\n height: 4px;\n background: #302033;\n border-radius: 2px;\n overflow: hidden;\n}\n.jev-devtools .bar i {\n display: block;\n height: 100%;\n background: #e551ba;\n}\n.jev-devtools .confidence {\n font-size: 10px;\n color: #a68cac;\n border-top: 1px solid #302634;\n padding-top: 10px;\n display: flex;\n justify-content: space-between;\n}\n.jev-devtools .confidence b {\n color: #d1bad2;\n font-weight: 400;\n}\n.jev-devtools details {\n margin-top: 12px;\n font-size: 10px;\n color: #b7a0bd;\n}\n.jev-devtools summary {\n cursor: pointer;\n}\n.jev-devtools .standalone {\n margin-bottom: 14px;\n max-width: 750px;\n}\n.jev-devtools .standalone:before {\n display: none;\n}\n.jev-devtools .standalone .answer {\n max-width: 450px;\n}\n.jev-devtools .box-preview {\n position: relative;\n margin-top: 14px;\n min-height: 80px;\n background: #19121c;\n}\n.jev-devtools .box-preview img {\n width: 100%;\n display: block;\n}\n.jev-devtools .bounding-box {\n position: absolute;\n border: 1.5px solid #e551ba;\n background: #e551ba15;\n}\n.jev-devtools .coordinates {\n display: block;\n font-size: 9px !important;\n line-height: 1.5;\n margin-top: 8px;\n color: #ac93b0;\n}\n.jev-devtools .context-image {\n max-width: 400px;\n width: 100%;\n border: 1px solid #75516c;\n border-radius: 6px;\n}\n.jev-devtools .empty {\n padding: 20px;\n color: #a294a8;\n line-height: 1.7;\n}\n.jev-devtools .empty-detail {\n margin: auto;\n padding: 40px;\n text-align: center;\n color: #b2a0ba;\n}\n.jev-devtools .empty-symbol {\n font-size: 36px;\n color: #e551ba;\n display: block;\n margin-bottom: 20px;\n}\n.jev-devtools .empty-detail p {\n font-size: 12px;\n line-height: 1.8;\n}\n.jev-devtools .error-banner {\n border: 1px solid #874b50;\n background: #31191e;\n padding: 12px;\n line-height: 1.5;\n margin-bottom: 16px;\n}\n.jev-devtools .warning {\n color: #e2b97d;\n}\n.jev-devtools .jev-inspector table {\n border-collapse: collapse;\n width: 100%;\n text-align: left;\n}\n.jev-devtools .jev-inspector th,\n.jev-devtools .jev-inspector td {\n border-bottom: 1px solid #302634;\n padding: 14px;\n}\n.jev-devtools .flow-dot {\n display: none;\n}\n.jev-devtools .pending .flow-dot {\n display: block;\n position: absolute;\n right: -23px;\n top: calc(50% - 3px);\n width: 6px;\n height: 6px;\n background: #e551ba;\n border-radius: 50%;\n animation: jev-inspector-pulse 1s infinite;\n}\n@keyframes jev-inspector-pulse {\n 50% {\n opacity: 0.2;\n transform: scale(0.7);\n }\n}\n@media (prefers-reduced-motion: reduce) {\n .jev-devtools .pending .flow-dot {\n animation: none;\n }\n}\n@media (max-width: 1100px) {\n .jev-devtools .question-branches {\n grid-template-columns: 1fr;\n }\n .jev-devtools .question-branches .question-node:not(:first-child):before {\n display: block;\n }\n .jev-devtools .request-flow {\n align-items: flex-start;\n }\n .jev-devtools .state-node {\n margin-top: 45px;\n }\n .jev-devtools .metrics {\n gap: 12px;\n }\n .jev-devtools .request-list {\n flex-basis: 250px;\n }\n}\n@media (max-width: 700px) {\n .jev-devtools .jev-inspector {\n flex-direction: column;\n }\n .jev-devtools .request-list {\n flex-basis: 140px;\n flex-shrink: 0;\n border-right: 0;\n border-bottom: 1px solid #302634;\n }\n .jev-devtools .request-list > header {\n padding: 6px 12px;\n }\n .jev-devtools .request-list footer {\n display: none;\n }\n .jev-devtools .list-filters {\n padding-bottom: 6px;\n }\n .jev-devtools .request-items {\n display: flex;\n }\n .jev-devtools .request-row {\n min-width: 240px;\n padding: 8px 12px;\n }\n .jev-devtools .request-detail {\n min-height: 0;\n }\n .jev-devtools .detail-header {\n padding: 12px;\n }\n .jev-devtools .detail-meta,\n .jev-devtools .detail-tabs {\n padding-left: 12px;\n padding-right: 12px;\n }\n .jev-devtools .detail-body {\n padding: 12px;\n }\n .jev-devtools .metrics {\n display: none;\n }\n .jev-devtools .state-node {\n flex-basis: 115px;\n padding: 12px;\n }\n .jev-devtools .request-flow {\n gap: 25px;\n }\n .jev-devtools .state-node:after {\n width: 12px;\n }\n .jev-devtools .question-branches {\n padding-left: 12px;\n }\n .jev-devtools .question-node:before {\n width: 13px;\n }\n .jev-devtools .question-node {\n padding: 12px;\n }\n}\n\n.jev-devtools .request-flow {\n flex-direction: column;\n align-items: stretch;\n gap: 24px;\n}\n.jev-devtools .state-node {\n flex: auto;\n width: fit-content;\n align-self: center;\n padding: 12px 24px;\n text-align: center;\n margin: 0;\n}\n.jev-devtools .state-node h3 {\n display: inline-block;\n margin: 0 12px;\n}\n.jev-devtools .state-node p {\n display: inline-block;\n margin: 0 12px 0 0;\n font-size: 11px;\n}\n.jev-devtools .state-node .eyebrow {\n display: none;\n}\n.jev-devtools .state-node:after {\n left: 50%;\n top: 100%;\n width: 0;\n height: 24px;\n border-top: 0;\n border-left: 1px solid #7f416d;\n}\n.jev-devtools .question-branches {\n border-left: 0;\n border-top: 1px solid #7f416d;\n padding: 20px 0 0;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n}\n.jev-devtools .question-node:before,\n.jev-devtools .question-branches .question-node:not(:first-child):before {\n display: block;\n right: auto;\n left: 50%;\n top: -21px;\n width: 0;\n height: 20px;\n border-top: 0;\n border-left: 1px solid #7f416d;\n}\n.jev-devtools .standalone:before {\n display: none;\n}\n.jev-devtools .flow-caption {\n margin-top: 0;\n}\n.jev-devtools .pending .flow-dot {\n right: auto;\n left: calc(50% - 3px);\n top: calc(100% + 10px);\n}\n@media (max-width: 900px) {\n .jev-devtools .question-branches {\n grid-template-columns: 1fr;\n }\n .jev-devtools .question-node:before,\n .jev-devtools .question-branches .question-node:not(:first-child):before {\n height: 12px;\n top: -13px;\n }\n .jev-devtools .question-branches .question-node:first-child:before {\n height: 20px;\n top: -21px;\n }\n .jev-devtools .state-node {\n padding: 10px;\n }\n .jev-devtools .state-node p {\n display: block;\n margin: 6px 0;\n }\n .jev-devtools .state-node h3 {\n font-size: 12px;\n }\n}\n\n.jev-devtools.jev-drawer {\n margin: 0;\n padding: 0;\n position: fixed;\n z-index: 1000;\n inset: auto 0 0;\n display: flex;\n flex-direction: column;\n background: #0d0a0f;\n color: #eee8ed;\n border-top: 1px solid #604058;\n box-shadow: 0 -12px 48px #0005;\n font:\n 12px \"DM Sans\",\n sans-serif;\n}\n.jev-devtools.jev-drawer[hidden],\n.jev-devtools.jev-drawer-trigger[hidden] {\n display: none;\n}\n.jev-devtools .jev-drawer-resize {\n position: absolute;\n z-index: 1;\n top: -6px;\n width: 100%;\n height: 12px;\n cursor: ns-resize;\n touch-action: none;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n.jev-devtools .jev-drawer-resize span {\n width: 42px;\n height: 3px;\n border-radius: 3px;\n background: #79506f;\n}\n.jev-devtools .jev-drawer-resize:hover span,\n.jev-devtools .jev-drawer-resize:focus-visible span {\n background: #e551ba;\n}\n.jev-devtools .jev-drawer-resize:focus-visible {\n outline: 2px solid #e551ba;\n outline-offset: -2px;\n}\n.jev-devtools .jev-drawer-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n flex: 0 0 46px;\n padding: 0 16px;\n border-bottom: 1px solid #302634;\n}\n.jev-devtools .jev-drawer-header strong {\n display: flex;\n align-items: center;\n gap: 9px;\n font-weight: 600;\n}\n.jev-devtools .jev-mark {\n color: #e551ba;\n font-size: 21px;\n}\n.jev-devtools .jev-drawer-header > div {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.jev-devtools .jev-drawer-header button {\n background: transparent;\n border-color: transparent;\n font-size: 11px;\n color: #c4afc8;\n}\n.jev-devtools .jev-drawer-header .jev-drawer-close {\n font-size: 22px;\n line-height: 1;\n padding: 4px 8px;\n}\n.jev-devtools .jev-drawer-content {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n.jev-devtools.jev-drawer-trigger {\n position: fixed;\n bottom: 18px;\n right: 18px;\n z-index: 1000;\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 10px 16px;\n border: 1px solid #a24487;\n border-radius: 8px;\n cursor: pointer;\n background: #211321;\n color: #f1cbe6;\n font:\n 600 13px \"DM Sans\",\n sans-serif;\n box-shadow: 0 4px 20px #0005;\n}\n.jev-devtools.jev-drawer-trigger span {\n color: #e551ba;\n font-size: 20px;\n}\n/* Baseline cards and authored left-to-right composition from the lab's Jev presets. */\n.jev-devtools .detail-body.detail-body-flow {\n display: flex;\n padding: 0;\n overflow: hidden;\n}\n.jev-devtools .lab-flow {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-width: 0;\n min-height: 0;\n}\n.jev-devtools .lab-flow-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-shrink: 0;\n padding: 12px 16px;\n flex-wrap: wrap;\n}\n.jev-devtools .lab-flow-toolbar label {\n display: flex;\n gap: 10px;\n align-items: center;\n font-size: 11px;\n min-width: 0;\n}\n.jev-devtools .lab-flow-toolbar select {\n width: auto;\n max-width: 240px;\n font-size: 11px;\n}\n.jev-devtools .lab-flow-toolbar > div {\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.jev-devtools .lab-flow-toolbar button {\n font-size: 11px;\n}\n.jev-devtools .lab-flow-toolbar output {\n font-size: 11px;\n min-width: 35px;\n text-align: center;\n}\n.jev-devtools .lab-flow-viewport {\n position: relative;\n flex: 1;\n min-height: 100px;\n overflow: hidden;\n background-color: #0d0a0f;\n background-image: radial-gradient(#463449 0.8px, transparent 0.8px);\n background-size: 20px 20px;\n border-block: 1px solid #302634;\n cursor: grab;\n touch-action: none;\n user-select: none;\n overscroll-behavior: contain;\n}\n.jev-devtools .lab-flow-viewport[data-panning] {\n cursor: grabbing;\n}\n.jev-devtools .lab-flow-viewport:focus-visible {\n outline: 2px solid #e551ba;\n outline-offset: -2px;\n}\n.jev-devtools .lab-flow-caption {\n flex-shrink: 0;\n padding: 8px 16px;\n margin: 0;\n}\n.jev-devtools .lab-gesture-hint {\n display: block;\n color: #c6a7bd;\n}\n.jev-devtools .lab-flow-canvas {\n position: absolute;\n inset: 0 auto auto 0;\n transform-origin: top left;\n will-change: transform;\n font-family: system-ui, sans-serif;\n color: #f5f3f6;\n}\n.jev-devtools .lab-connections {\n position: absolute;\n inset: 0;\n overflow: visible;\n}\n.jev-devtools .lab-connections path {\n fill: none;\n stroke: #645a68;\n stroke-width: 1;\n}\n.jev-devtools .lab-connections path.active {\n stroke: #968592;\n}\n.jev-devtools .lab-connections text {\n fill: #b7b1bc;\n font:\n 12px ui-monospace,\n monospace;\n}\n.jev-devtools .lab-flow-dot {\n fill: #e551ba;\n}\n.jev-devtools .lab-card {\n position: absolute;\n border: 1.3px solid #f5f3f6;\n border-radius: 14px;\n padding: 20px;\n background: #0d0a0f;\n overflow: hidden;\n}\n.jev-devtools .lab-eyebrow {\n display: block;\n color: #e551ba;\n font:\n 500 10px ui-monospace,\n monospace;\n letter-spacing: 1px;\n margin: 0 0 10px;\n}\n.jev-devtools .lab-card h3 {\n font-size: 18px;\n font-weight: 500;\n line-height: 1.25;\n margin: 0 0 16px;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n.jev-devtools .lab-card p {\n font-size: 15px;\n line-height: 1.4;\n margin: 10px 0;\n}\n.jev-devtools .lab-copy {\n display: -webkit-box;\n -webkit-line-clamp: 5;\n -webkit-box-orient: vertical;\n overflow: hidden;\n overflow-wrap: anywhere;\n}\n.jev-devtools .lab-probability {\n margin: 17px 0;\n}\n.jev-devtools .lab-probability > div:first-child {\n display: flex;\n justify-content: space-between;\n gap: 10px;\n margin-bottom: 9px;\n font-size: 15px;\n}\n.jev-devtools .lab-probability span {\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n.jev-devtools .lab-probability code {\n font:\n 13px ui-monospace,\n monospace;\n}\n.jev-devtools .lab-track {\n height: 7px;\n border-radius: 4px;\n background: #f5f3f61a;\n overflow: hidden;\n}\n.jev-devtools .lab-track i {\n display: block;\n height: 100%;\n border-radius: 4px;\n background: #e551ba;\n}\n.jev-devtools .lab-track i.lab-alternative {\n opacity: 0.35;\n}\n.jev-devtools .lab-card .lab-result {\n font-size: 14px;\n margin-top: 24px;\n}\n.jev-devtools .lab-card .lab-small {\n font-size: 12px;\n color: #b7b1bc;\n margin-top: 20px;\n}\n.jev-devtools .lab-idle {\n opacity: 0.65;\n}\n.jev-devtools .lab-scale {\n position: relative;\n margin-top: 18px;\n border-top: 1px solid #b7b1bc;\n display: flex;\n justify-content: space-between;\n padding-top: 9px;\n}\n.jev-devtools .lab-scale > span {\n position: absolute;\n top: -5px;\n width: 9px;\n height: 9px;\n border-radius: 50%;\n background: #e551ba;\n transform: translateX(-50%);\n}\n.jev-devtools .lab-big {\n display: block;\n font-size: 36px;\n font-weight: 500;\n margin-top: 26px;\n}\n.jev-devtools .lab-endpoints {\n display: flex;\n justify-content: space-between;\n font-size: 12px;\n color: #b7b1bc;\n margin-top: 10px;\n}\n.jev-devtools .lab-image {\n position: relative;\n}\n.jev-devtools .lab-image img {\n display: block;\n width: 100%;\n}\n.jev-devtools .lab-box {\n position: absolute;\n border: 1.5px solid #e551ba;\n background: #e551ba15;\n}\n.jev-devtools .lab-source-image {\n width: 100%;\n max-height: 105px;\n object-fit: contain;\n}\n.jev-devtools .lab-coordinates {\n font-family: ui-monospace, monospace;\n}\n@media (prefers-reduced-motion: reduce) {\n .jev-devtools .lab-flow-dot {\n display: none;\n }\n}\n/* The public TypeSafe mark travels with the package, independent of host assets. */\n.jev-devtools .typesafe-mark {\n display: inline-block;\n flex: 0 0 23px;\n width: 23px;\n height: 32px;\n overflow: hidden;\n line-height: 0;\n}\n.jev-devtools .typesafe-mark img {\n display: block;\n height: 32px;\n width: auto;\n max-width: none;\n}\n";
@@ -1,9 +1,15 @@
1
1
  .jev-devtools {
2
+ all: initial;
3
+ display: block;
4
+ direction: ltr;
5
+ unicode-bidi: isolate;
6
+ pointer-events: auto;
2
7
  color-scheme: dark;
3
8
  font:
4
9
  12px system-ui,
5
10
  sans-serif;
6
11
  color: #eee8ed;
12
+ line-height: 1.5;
7
13
  }
8
14
  .jev-devtools,
9
15
  .jev-devtools * {
@@ -184,6 +190,7 @@
184
190
  .jev-devtools .request-detail {
185
191
  flex: 1;
186
192
  min-width: 0;
193
+ min-height: 0;
187
194
  display: flex;
188
195
  flex-direction: column;
189
196
  }
@@ -664,6 +671,8 @@
664
671
  }
665
672
 
666
673
  .jev-devtools.jev-drawer {
674
+ margin: 0;
675
+ padding: 0;
667
676
  position: fixed;
668
677
  z-index: 1000;
669
678
  inset: auto 0 0;
@@ -756,6 +765,8 @@
756
765
  gap: 9px;
757
766
  padding: 10px 16px;
758
767
  border: 1px solid #a24487;
768
+ border-radius: 8px;
769
+ cursor: pointer;
759
770
  background: #211321;
760
771
  color: #f1cbe6;
761
772
  font:
@@ -768,12 +779,25 @@
768
779
  font-size: 20px;
769
780
  }
770
781
  /* Baseline cards and authored left-to-right composition from the lab's Jev presets. */
782
+ .jev-devtools .detail-body.detail-body-flow {
783
+ display: flex;
784
+ padding: 0;
785
+ overflow: hidden;
786
+ }
787
+ .jev-devtools .lab-flow {
788
+ display: flex;
789
+ flex-direction: column;
790
+ flex: 1;
791
+ min-width: 0;
792
+ min-height: 0;
793
+ }
771
794
  .jev-devtools .lab-flow-toolbar {
772
795
  display: flex;
773
796
  align-items: center;
774
797
  justify-content: space-between;
775
798
  gap: 12px;
776
- margin-bottom: 14px;
799
+ flex-shrink: 0;
800
+ padding: 12px 16px;
777
801
  flex-wrap: wrap;
778
802
  }
779
803
  .jev-devtools .lab-flow-toolbar label {
@@ -802,15 +826,40 @@
802
826
  text-align: center;
803
827
  }
804
828
  .jev-devtools .lab-flow-viewport {
805
- overflow: auto;
806
- max-height: 65vh;
807
- background: #0d0a0f;
808
- border: 1px solid #302634;
809
- border-radius: 8px;
829
+ position: relative;
830
+ flex: 1;
831
+ min-height: 100px;
832
+ overflow: hidden;
833
+ background-color: #0d0a0f;
834
+ background-image: radial-gradient(#463449 0.8px, transparent 0.8px);
835
+ background-size: 20px 20px;
836
+ border-block: 1px solid #302634;
837
+ cursor: grab;
838
+ touch-action: none;
839
+ user-select: none;
840
+ overscroll-behavior: contain;
841
+ }
842
+ .jev-devtools .lab-flow-viewport[data-panning] {
843
+ cursor: grabbing;
844
+ }
845
+ .jev-devtools .lab-flow-viewport:focus-visible {
846
+ outline: 2px solid #e551ba;
847
+ outline-offset: -2px;
848
+ }
849
+ .jev-devtools .lab-flow-caption {
850
+ flex-shrink: 0;
851
+ padding: 8px 16px;
852
+ margin: 0;
853
+ }
854
+ .jev-devtools .lab-gesture-hint {
855
+ display: block;
856
+ color: #c6a7bd;
810
857
  }
811
858
  .jev-devtools .lab-flow-canvas {
812
- position: relative;
859
+ position: absolute;
860
+ inset: 0 auto auto 0;
813
861
  transform-origin: top left;
862
+ will-change: transform;
814
863
  font-family: system-ui, sans-serif;
815
864
  color: #f5f3f6;
816
865
  }
@@ -0,0 +1,9 @@
1
+ import { type Point } from "./diagram-camera.js";
2
+ /** Lab-style camera. Pointer frames update the transform without rerendering cards. */
3
+ export declare function useDiagramViewport(width: number, height: number, scene: string): {
4
+ viewport: import("react").RefObject<HTMLDivElement | null>;
5
+ canvas: import("react").RefObject<HTMLDivElement | null>;
6
+ zoom: number;
7
+ zoomBy: (factor: number, point?: Point) => void;
8
+ fit: () => void;
9
+ };
@@ -0,0 +1,172 @@
1
+ import { useCallback, useEffect, 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 frame = useRef(null);
11
+ const [zoom, setZoom] = useState(100);
12
+ const paint = useCallback(() => {
13
+ if (frame.current !== null)
14
+ return;
15
+ frame.current = requestAnimationFrame(() => {
16
+ frame.current = null;
17
+ const { x, y, scale } = camera.current;
18
+ if (canvas.current)
19
+ canvas.current.style.transform = `translate(${x}px, ${y}px) scale(${scale})`;
20
+ if (viewport.current) {
21
+ viewport.current.style.backgroundPosition = `${x}px ${y}px`;
22
+ viewport.current.style.backgroundSize = `${20 * scale}px ${20 * scale}px`;
23
+ }
24
+ setZoom(Math.round(scale * 100));
25
+ });
26
+ }, []);
27
+ const fit = useCallback(() => {
28
+ fitted.current = true;
29
+ if (size.current.width <= 0 || size.current.height <= 0)
30
+ return;
31
+ camera.current = fitCamera(size.current, { width, height });
32
+ paint();
33
+ }, [width, height, paint]);
34
+ const zoomBy = useCallback((factor, point) => {
35
+ fitted.current = false;
36
+ camera.current = zoomCamera(camera.current, factor, point ?? { x: size.current.width / 2, y: size.current.height / 2 });
37
+ paint();
38
+ }, [paint]);
39
+ useEffect(() => {
40
+ const el = viewport.current;
41
+ if (!el)
42
+ return;
43
+ const pointers = new Map();
44
+ const local = (event) => {
45
+ const rect = el.getBoundingClientRect();
46
+ return {
47
+ x: event.clientX - rect.left - el.clientLeft,
48
+ y: event.clientY - rect.top - el.clientTop,
49
+ };
50
+ };
51
+ const measure = () => {
52
+ const next = { width: el.clientWidth, height: el.clientHeight };
53
+ if (next.width <= 0 || next.height <= 0)
54
+ return; // Closed drawer.
55
+ const previous = size.current;
56
+ size.current = next;
57
+ if (fitted.current)
58
+ fit();
59
+ else {
60
+ camera.current.x += (next.width - previous.width) / 2;
61
+ camera.current.y += (next.height - previous.height) / 2;
62
+ paint();
63
+ }
64
+ };
65
+ const down = (event) => {
66
+ if (event.button !== 0 ||
67
+ pointers.size >= 2 ||
68
+ event.target.closest("button, input, select, textarea, a, summary"))
69
+ return;
70
+ event.preventDefault();
71
+ el.focus({ preventScroll: true });
72
+ el.setPointerCapture(event.pointerId);
73
+ pointers.set(event.pointerId, local(event));
74
+ el.dataset.panning = "true";
75
+ };
76
+ const move = (event) => {
77
+ if (!pointers.has(event.pointerId))
78
+ return;
79
+ const before = [...pointers.values()];
80
+ pointers.set(event.pointerId, local(event));
81
+ const after = [...pointers.values()];
82
+ fitted.current = false;
83
+ if (before.length === 1) {
84
+ camera.current.x += after[0].x - before[0].x;
85
+ camera.current.y += after[0].y - before[0].y;
86
+ }
87
+ else {
88
+ const center = (p) => ({
89
+ x: (p[0].x + p[1].x) / 2,
90
+ y: (p[0].y + p[1].y) / 2,
91
+ });
92
+ const distance = (p) => Math.hypot(p[0].x - p[1].x, p[0].y - p[1].y);
93
+ camera.current = zoomCamera(camera.current, distance(after) / Math.max(1, distance(before)), center(before), center(after));
94
+ }
95
+ paint();
96
+ };
97
+ const release = (event) => {
98
+ pointers.delete(event.pointerId);
99
+ if (el.hasPointerCapture(event.pointerId))
100
+ el.releasePointerCapture(event.pointerId);
101
+ if (!pointers.size)
102
+ delete el.dataset.panning;
103
+ };
104
+ const wheel = (event) => {
105
+ event.preventDefault();
106
+ const units = event.deltaMode === 1
107
+ ? 16
108
+ : event.deltaMode === 2
109
+ ? el.clientHeight
110
+ : 1;
111
+ // Match the lab viewer's anchored wheel/pinch zoom, including trackpads.
112
+ zoomBy(Math.exp(-Math.max(-100, Math.min(100, event.deltaY * units)) *
113
+ (event.ctrlKey ? 0.01 : 0.003)), local(event));
114
+ };
115
+ const keydown = (event) => {
116
+ if (event.target !== el)
117
+ return;
118
+ const step = event.shiftKey ? 100 : 40;
119
+ if (event.key === "+" || event.key === "=")
120
+ zoomBy(1.2);
121
+ else if (event.key === "-")
122
+ zoomBy(1 / 1.2);
123
+ else if (event.key === "0" || event.key === "Home")
124
+ fit();
125
+ else if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) {
126
+ fitted.current = false;
127
+ if (event.key === "ArrowLeft")
128
+ camera.current.x += step;
129
+ if (event.key === "ArrowRight")
130
+ camera.current.x -= step;
131
+ if (event.key === "ArrowUp")
132
+ camera.current.y += step;
133
+ if (event.key === "ArrowDown")
134
+ camera.current.y -= step;
135
+ paint();
136
+ }
137
+ else
138
+ return;
139
+ event.preventDefault();
140
+ };
141
+ fitted.current = true;
142
+ measure();
143
+ const observer = new ResizeObserver(measure);
144
+ observer.observe(el);
145
+ el.addEventListener("pointerdown", down);
146
+ el.addEventListener("pointermove", move);
147
+ el.addEventListener("pointerup", release);
148
+ el.addEventListener("pointercancel", release);
149
+ el.addEventListener("lostpointercapture", release);
150
+ el.addEventListener("wheel", wheel, { passive: false });
151
+ el.addEventListener("keydown", keydown);
152
+ return () => {
153
+ observer.disconnect();
154
+ el.removeEventListener("pointerdown", down);
155
+ el.removeEventListener("pointermove", move);
156
+ el.removeEventListener("pointerup", release);
157
+ el.removeEventListener("pointercancel", release);
158
+ el.removeEventListener("lostpointercapture", release);
159
+ el.removeEventListener("wheel", wheel);
160
+ el.removeEventListener("keydown", keydown);
161
+ for (const pointer of pointers.keys())
162
+ if (el.hasPointerCapture(pointer))
163
+ el.releasePointerCapture(pointer);
164
+ pointers.clear();
165
+ delete el.dataset.panning;
166
+ if (frame.current !== null)
167
+ cancelAnimationFrame(frame.current);
168
+ frame.current = null;
169
+ };
170
+ }, [fit, zoomBy, paint, scene]);
171
+ return { viewport, canvas, zoom, zoomBy, fit };
172
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jev-devtools",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.12"
@@ -46,7 +46,7 @@
46
46
  "check": "tsc --noEmit",
47
47
  "test": "node --import tsx --test tests/*.test.ts",
48
48
  "build": "tsc -p tsconfig.lib.json && node scripts/copy-styles.mjs",
49
- "prepack": "npm run build"
49
+ "prepack": "node ../../scripts/sync-readme-version.mjs --check && npm run build"
50
50
  },
51
51
  "publishConfig": {
52
52
  "access": "public",