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 +3 -3
- package/lib/src/JevDevtools.js +2 -2
- package/lib/src/JevDrawer.d.ts +1 -1
- package/lib/src/JevDrawer.js +21 -3
- package/lib/src/TraceDiagram.js +21 -28
- package/lib/src/diagram-camera.d.ts +16 -0
- package/lib/src/diagram-camera.js +20 -0
- package/lib/src/inspector-styles.d.ts +2 -0
- package/lib/src/inspector-styles.js +1 -0
- package/lib/src/inspector.css +56 -7
- package/lib/src/useDiagramViewport.d.ts +9 -0
- package/lib/src/useDiagramViewport.js +172 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Jev DevTools
|
|
2
2
|
|
|
3
|
-
[](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.
|
|
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
|
|
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
|
|
package/lib/src/JevDevtools.js
CHANGED
|
@@ -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"
|
|
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."] })] })) })] }));
|
package/lib/src/JevDrawer.d.ts
CHANGED
package/lib/src/JevDrawer.js
CHANGED
|
@@ -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
|
-
|
|
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 })] })] }),
|
|
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
|
}
|
package/lib/src/TraceDiagram.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
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(""), [
|
|
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",
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 @@
|
|
|
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";
|
package/lib/src/inspector.css
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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:
|
|
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.
|
|
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",
|