causal-inspector 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/CausalInspector.css +124 -0
  2. package/dist/CausalInspector.d.ts +13 -0
  3. package/dist/CausalInspector.js +257 -0
  4. package/dist/components/CopyablePayload.d.ts +4 -0
  5. package/dist/components/CopyablePayload.js +44 -0
  6. package/dist/components/FilterBar.d.ts +1 -0
  7. package/dist/components/FilterBar.js +23 -0
  8. package/dist/components/GlobalScrubber.d.ts +1 -0
  9. package/dist/components/GlobalScrubber.js +148 -0
  10. package/dist/components/JsonSyntax.d.ts +3 -0
  11. package/dist/components/JsonSyntax.js +40 -0
  12. package/dist/context.d.ts +26 -0
  13. package/dist/context.js +28 -0
  14. package/dist/engines/index.d.ts +22 -0
  15. package/dist/engines/index.js +29 -0
  16. package/dist/engines/query.d.ts +14 -0
  17. package/dist/engines/query.js +241 -0
  18. package/dist/engines/scrubber.d.ts +12 -0
  19. package/dist/engines/scrubber.js +69 -0
  20. package/dist/engines/storage.d.ts +15 -0
  21. package/dist/engines/storage.js +16 -0
  22. package/dist/engines/subscription.d.ts +17 -0
  23. package/dist/engines/subscription.js +44 -0
  24. package/dist/engines/url.d.ts +13 -0
  25. package/dist/engines/url.js +64 -0
  26. package/dist/events.d.ts +77 -0
  27. package/dist/events.js +1 -0
  28. package/dist/index.d.ts +30 -0
  29. package/dist/index.js +29 -0
  30. package/dist/machine/core.d.ts +22 -0
  31. package/dist/machine/core.js +34 -0
  32. package/dist/machine/engine.d.ts +17 -0
  33. package/dist/machine/engine.js +21 -0
  34. package/dist/machine/events.d.ts +18 -0
  35. package/dist/machine/events.js +1 -0
  36. package/dist/machine/hooks.d.ts +23 -0
  37. package/dist/machine/hooks.js +52 -0
  38. package/dist/machine/index.d.ts +5 -0
  39. package/dist/machine/index.js +4 -0
  40. package/dist/machine/store.d.ts +27 -0
  41. package/dist/machine/store.js +42 -0
  42. package/dist/panes/AggregateTimelinePane.d.ts +2 -0
  43. package/dist/panes/AggregateTimelinePane.js +224 -0
  44. package/dist/panes/CausalFlowPane.d.ts +7 -0
  45. package/dist/panes/CausalFlowPane.js +596 -0
  46. package/dist/panes/CausalTreePane.d.ts +5 -0
  47. package/dist/panes/CausalTreePane.js +158 -0
  48. package/dist/panes/CorrelationExplorerPane.d.ts +2 -0
  49. package/dist/panes/CorrelationExplorerPane.js +46 -0
  50. package/dist/panes/LogsPane.d.ts +6 -0
  51. package/dist/panes/LogsPane.js +65 -0
  52. package/dist/panes/TimelinePane.d.ts +6 -0
  53. package/dist/panes/TimelinePane.js +121 -0
  54. package/dist/panes/WaterfallPane.d.ts +2 -0
  55. package/dist/panes/WaterfallPane.js +202 -0
  56. package/dist/queries.d.ts +15 -0
  57. package/dist/queries.js +175 -0
  58. package/dist/reducer.d.ts +4 -0
  59. package/dist/reducer.js +177 -0
  60. package/dist/state.d.ts +34 -0
  61. package/dist/state.js +39 -0
  62. package/dist/theme.d.ts +7 -0
  63. package/dist/theme.js +34 -0
  64. package/dist/types.d.ts +140 -0
  65. package/dist/types.js +1 -0
  66. package/dist/utils.d.ts +14 -0
  67. package/dist/utils.js +91 -0
  68. package/package.json +43 -0
@@ -0,0 +1,124 @@
1
+ /* ── CausalInspector scoped styles ───────────────────────────── */
2
+
3
+ .causal-inspector {
4
+ height: 100%;
5
+ width: 100%;
6
+ background: #0a0a0f;
7
+ color: #e0e0f0;
8
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
9
+ -webkit-font-smoothing: antialiased;
10
+ -moz-osx-font-smoothing: grayscale;
11
+ }
12
+
13
+ /* ── Scrollbar ────────────────────────────────────────────── */
14
+ .causal-inspector ::-webkit-scrollbar {
15
+ width: 6px;
16
+ height: 6px;
17
+ }
18
+ .causal-inspector ::-webkit-scrollbar-track {
19
+ background: transparent;
20
+ }
21
+ .causal-inspector ::-webkit-scrollbar-thumb {
22
+ background: rgba(255, 255, 255, 0.08);
23
+ border-radius: 3px;
24
+ }
25
+ .causal-inspector ::-webkit-scrollbar-thumb:hover {
26
+ background: rgba(255, 255, 255, 0.14);
27
+ }
28
+
29
+ /* ── Flexlayout overrides ─────────────────────────────────── */
30
+ .causal-inspector .flexlayout__tabset-selected,
31
+ .causal-inspector .flexlayout__tabset-maximized {
32
+ background-image: none;
33
+ }
34
+ .causal-inspector .flexlayout__tabset_tabbar_outer {
35
+ background: rgba(15, 15, 20, 0.8) !important;
36
+ backdrop-filter: blur(12px) !important;
37
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06) !important;
38
+ }
39
+ .causal-inspector .flexlayout__tab_button--selected {
40
+ background: rgba(99, 102, 241, 0.12) !important;
41
+ color: #c7c7ff !important;
42
+ }
43
+ .causal-inspector .flexlayout__tab_button {
44
+ color: #70708a !important;
45
+ font-size: 11px !important;
46
+ letter-spacing: 0.02em !important;
47
+ transition: color 150ms, background 150ms !important;
48
+ }
49
+ .causal-inspector .flexlayout__tab_button:hover {
50
+ color: #b0b0c0 !important;
51
+ background: rgba(255, 255, 255, 0.04) !important;
52
+ }
53
+ .causal-inspector .flexlayout__splitter {
54
+ background: rgba(255, 255, 255, 0.03) !important;
55
+ }
56
+ .causal-inspector .flexlayout__splitter:hover {
57
+ background: rgba(99, 102, 241, 0.2) !important;
58
+ }
59
+ .causal-inspector .flexlayout__splitter_drag {
60
+ background: rgba(99, 102, 241, 0.3) !important;
61
+ }
62
+
63
+ /* ── Flow diagram ─────────────────────────────────────────── */
64
+ .causal-inspector .react-flow .react-flow__edges {
65
+ z-index: 0 !important;
66
+ }
67
+ .causal-inspector .react-flow .react-flow__nodes {
68
+ position: relative;
69
+ z-index: 1 !important;
70
+ }
71
+ .causal-inspector .react-flow__controls {
72
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important;
73
+ border: 1px solid rgba(255, 255, 255, 0.08) !important;
74
+ border-radius: 8px !important;
75
+ overflow: hidden !important;
76
+ }
77
+ .causal-inspector .react-flow__controls-button {
78
+ background: rgba(15, 15, 20, 0.9) !important;
79
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06) !important;
80
+ fill: #70708a !important;
81
+ }
82
+ .causal-inspector .react-flow__controls-button:hover {
83
+ background: rgba(99, 102, 241, 0.15) !important;
84
+ fill: #c7c7ff !important;
85
+ }
86
+
87
+ /* ── Selection ring ───────────────────────────────────────── */
88
+ .causal-inspector *:focus-visible {
89
+ outline: none;
90
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.4);
91
+ border-radius: 4px;
92
+ }
93
+
94
+ /* ── Range input ──────────────────────────────────────────── */
95
+ .causal-inspector input[type="range"] {
96
+ -webkit-appearance: none;
97
+ appearance: none;
98
+ height: 3px;
99
+ background: rgba(255, 255, 255, 0.08);
100
+ border-radius: 2px;
101
+ }
102
+ .causal-inspector input[type="range"]::-webkit-slider-thumb {
103
+ -webkit-appearance: none;
104
+ width: 12px;
105
+ height: 12px;
106
+ border-radius: 50%;
107
+ background: #6366f1;
108
+ cursor: pointer;
109
+ box-shadow: 0 0 6px rgba(99, 102, 241, 0.4);
110
+ transition: transform 100ms;
111
+ }
112
+ .causal-inspector input[type="range"]::-webkit-slider-thumb:hover {
113
+ transform: scale(1.2);
114
+ }
115
+
116
+ /* ── Pulse animation for running reactors ─────────────────── */
117
+ @keyframes causal-inspector-pulse {
118
+ 0%, 100% { opacity: 1; }
119
+ 50% { opacity: 0.7; }
120
+ }
121
+ @keyframes causal-inspector-glow {
122
+ 0%, 100% { box-shadow: 0 0 4px rgba(99, 102, 241, 0.2); }
123
+ 50% { box-shadow: 0 0 12px rgba(99, 102, 241, 0.4); }
124
+ }
@@ -0,0 +1,13 @@
1
+ import "./CausalInspector.css";
2
+ export type CausalInspectorProps = {
3
+ /** GraphQL endpoint URL (relative or absolute). Queries POST here, WS connects to {endpoint}/ws */
4
+ endpoint: string;
5
+ /** Extra fetch options. Defaults to { credentials: "include" } */
6
+ fetchOptions?: {
7
+ credentials?: RequestCredentials;
8
+ headers?: Record<string, string>;
9
+ };
10
+ /** CSS class for the container */
11
+ className?: string;
12
+ };
13
+ export declare function CausalInspector({ endpoint, fetchOptions, className, }: CausalInspectorProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,257 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useRef, useCallback, useEffect } from "react";
3
+ import { Layout, Model, Actions, DockLocation, } from "flexlayout-react";
4
+ import { createClient } from "graphql-ws";
5
+ import { Plus } from "lucide-react";
6
+ import { CausalInspectorProvider } from "./context";
7
+ import { createInspectorEngine } from "./engines";
8
+ import { useSelector, useDispatch } from "./machine";
9
+ import { TimelinePane } from "./panes/TimelinePane";
10
+ import { CausalTreePane } from "./panes/CausalTreePane";
11
+ import { CausalFlowPane } from "./panes/CausalFlowPane";
12
+ import { LogsPane } from "./panes/LogsPane";
13
+ import { AggregateTimelinePane } from "./panes/AggregateTimelinePane";
14
+ import { WaterfallPane } from "./panes/WaterfallPane";
15
+ import { CorrelationExplorerPane } from "./panes/CorrelationExplorerPane";
16
+ import "./CausalInspector.css";
17
+ // ── Transport ─────────────────────────────────────────────────
18
+ function createTransport(endpoint, fetchOptions) {
19
+ const url = new URL(endpoint, window.location.origin);
20
+ const httpUrl = url.toString();
21
+ const wsUrl = httpUrl.replace(/^http/, "ws") + "/ws";
22
+ const wsClient = createClient({ url: wsUrl });
23
+ return {
24
+ async query(query, variables) {
25
+ const res = await fetch(httpUrl, {
26
+ method: "POST",
27
+ headers: {
28
+ "Content-Type": "application/json",
29
+ ...fetchOptions?.headers,
30
+ },
31
+ credentials: fetchOptions?.credentials ?? "include",
32
+ body: JSON.stringify({ query, variables }),
33
+ });
34
+ const json = await res.json();
35
+ if (json.errors) {
36
+ throw new Error(json.errors
37
+ .map((e) => e.message)
38
+ .join(", "));
39
+ }
40
+ return json.data;
41
+ },
42
+ subscribe(query, variables, onData, onError) {
43
+ let disposed = false;
44
+ const unsubscribe = wsClient.subscribe({ query, variables }, {
45
+ next(value) {
46
+ if (!disposed && value.data)
47
+ onData(value.data);
48
+ },
49
+ error(err) {
50
+ if (!disposed)
51
+ onError?.(err);
52
+ },
53
+ complete() { },
54
+ });
55
+ return () => {
56
+ disposed = true;
57
+ unsubscribe();
58
+ };
59
+ },
60
+ };
61
+ }
62
+ // ── Layout persistence ────────────────────────────────────────
63
+ const STORAGE_KEY = "causal-inspector-layout";
64
+ function loadSavedLayout() {
65
+ try {
66
+ const raw = localStorage.getItem(STORAGE_KEY);
67
+ if (!raw)
68
+ return null;
69
+ const json = JSON.parse(raw);
70
+ Model.fromJson(json); // validate
71
+ return json;
72
+ }
73
+ catch {
74
+ localStorage.removeItem(STORAGE_KEY);
75
+ return null;
76
+ }
77
+ }
78
+ const storage = {
79
+ saveLayout: (layout) => {
80
+ try {
81
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(layout));
82
+ }
83
+ catch { }
84
+ },
85
+ };
86
+ // ── Default layout ────────────────────────────────────────────
87
+ const DEFAULT_LAYOUT = {
88
+ global: {
89
+ tabEnableClose: true,
90
+ tabSetEnableMaximize: true,
91
+ tabSetEnableTabStrip: true,
92
+ splitterSize: 6,
93
+ splitterExtra: 4,
94
+ enableEdgeDock: false,
95
+ },
96
+ layout: {
97
+ type: "row",
98
+ children: [
99
+ {
100
+ type: "tabset",
101
+ weight: 55,
102
+ children: [
103
+ { type: "tab", name: "Timeline", component: "timeline" },
104
+ ],
105
+ },
106
+ {
107
+ type: "tabset",
108
+ weight: 45,
109
+ children: [
110
+ { type: "tab", name: "Causal Tree", component: "causal-tree" },
111
+ ],
112
+ },
113
+ ],
114
+ },
115
+ };
116
+ // ── Pane registry ─────────────────────────────────────────────
117
+ const PANE_REGISTRY = [
118
+ { name: "Timeline", component: "timeline", render: () => _jsx(TimelinePane, {}) },
119
+ {
120
+ name: "Causal Tree",
121
+ component: "causal-tree",
122
+ render: () => _jsx(CausalTreePane, {}),
123
+ },
124
+ {
125
+ name: "Flow",
126
+ component: "causal-flow",
127
+ render: () => _jsx(CausalFlowPane, {}),
128
+ },
129
+ { name: "Logs", component: "logs", render: () => _jsx(LogsPane, {}) },
130
+ {
131
+ name: "State Timeline",
132
+ component: "state-timeline",
133
+ render: () => _jsx(AggregateTimelinePane, {}),
134
+ },
135
+ {
136
+ name: "Waterfall",
137
+ component: "waterfall",
138
+ render: () => _jsx(WaterfallPane, {}),
139
+ },
140
+ {
141
+ name: "Correlations",
142
+ component: "correlations",
143
+ render: () => _jsx(CorrelationExplorerPane, {}),
144
+ },
145
+ ];
146
+ // ── Helpers ───────────────────────────────────────────────────
147
+ function findTab(model, component) {
148
+ let found = null;
149
+ model.visitNodes((node) => {
150
+ if (node.getType() === "tab" &&
151
+ node.getComponent() === component) {
152
+ found = node;
153
+ }
154
+ });
155
+ return found;
156
+ }
157
+ // ── InspectorLayout (inner component) ─────────────────────────
158
+ function InspectorLayout() {
159
+ const paneLayout = useSelector((s) => s.paneLayout);
160
+ const flowCorrelationId = useSelector((s) => s.flowCorrelationId);
161
+ const selectedSeq = useSelector((s) => s.selectedSeq);
162
+ const dispatch = useDispatch();
163
+ const modelRef = useRef(null);
164
+ if (!modelRef.current) {
165
+ const json = paneLayout ?? DEFAULT_LAYOUT;
166
+ try {
167
+ modelRef.current = Model.fromJson(json);
168
+ }
169
+ catch {
170
+ modelRef.current = Model.fromJson(DEFAULT_LAYOUT);
171
+ }
172
+ }
173
+ const layoutRef = useRef(null);
174
+ const addTab = useCallback((component, name) => {
175
+ const model = modelRef.current;
176
+ const existing = findTab(model, component);
177
+ if (existing) {
178
+ model.doAction(Actions.selectTab(existing.getId()));
179
+ return;
180
+ }
181
+ const target = model.getActiveTabset()?.getId() ??
182
+ model.getRoot().getChildren()[0]?.getId() ??
183
+ "";
184
+ model.doAction(Actions.addNode({ type: "tab", component, name }, target, DockLocation.CENTER, -1));
185
+ }, []);
186
+ useEffect(() => {
187
+ if (selectedSeq != null)
188
+ addTab("causal-tree", "Causal Tree");
189
+ }, [selectedSeq, addTab]);
190
+ useEffect(() => {
191
+ if (flowCorrelationId)
192
+ addTab("causal-flow", "Flow");
193
+ }, [flowCorrelationId, addTab]);
194
+ const factory = useCallback((node) => {
195
+ const component = node.getComponent();
196
+ const pane = PANE_REGISTRY.find((p) => p.component === component);
197
+ if (!pane)
198
+ return (_jsxs("div", { style: { padding: 16, fontSize: 12, color: "#9090a0" }, children: ["Unknown pane: ", component] }));
199
+ return (_jsx("div", { style: { height: "100%", overflow: "hidden" }, children: pane.render() }));
200
+ }, []);
201
+ const onModelChange = useCallback((model, action) => {
202
+ dispatch({
203
+ type: "ui/layout_changed",
204
+ payload: model.toJson(),
205
+ });
206
+ if (action.type === Actions.DELETE_TAB) {
207
+ if (!findTab(model, "causal-flow")) {
208
+ dispatch({ type: "ui/flow_closed" });
209
+ }
210
+ }
211
+ }, [dispatch]);
212
+ const onRenderTabSet = useCallback((_node, renderValues) => {
213
+ renderValues.stickyButtons.push(_jsx("button", { className: "flexlayout__tab_toolbar_button", title: "Add pane", onClick: (e) => {
214
+ const btn = e.currentTarget;
215
+ const rect = btn.getBoundingClientRect();
216
+ const menu = document.createElement("div");
217
+ menu.style.cssText = `position:fixed;z-index:9999;background:rgba(17,17,22,0.95);backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,0.06);border-radius:10px;padding:4px;box-shadow:0 8px 32px rgba(0,0,0,0.5);top:${rect.bottom + 6}px;right:${window.innerWidth - rect.right}px;`;
218
+ for (const pane of PANE_REGISTRY) {
219
+ const item = document.createElement("button");
220
+ item.textContent = pane.name;
221
+ item.style.cssText =
222
+ "display:block;width:100%;text-align:left;padding:7px 14px;font-size:11px;color:rgba(240,240,245,0.7);background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all 100ms;letter-spacing:0.02em;";
223
+ item.onmouseenter = () => {
224
+ item.style.background = "rgba(255,255,255,0.04)";
225
+ item.style.color = "rgba(240,240,245,0.95)";
226
+ };
227
+ item.onmouseleave = () => {
228
+ item.style.background = "transparent";
229
+ item.style.color = "rgba(240,240,245,0.7)";
230
+ };
231
+ item.onclick = () => {
232
+ addTab(pane.component, pane.name);
233
+ menu.remove();
234
+ };
235
+ menu.appendChild(item);
236
+ }
237
+ document.body.appendChild(menu);
238
+ const close = (ev) => {
239
+ if (!menu.contains(ev.target)) {
240
+ menu.remove();
241
+ document.removeEventListener("mousedown", close);
242
+ }
243
+ };
244
+ setTimeout(() => document.addEventListener("mousedown", close), 0);
245
+ }, children: _jsx(Plus, { size: 14 }) }, "add-pane"));
246
+ }, [addTab]);
247
+ return (_jsx("div", { style: { height: "100%", width: "100%" }, children: _jsx(Layout, { ref: layoutRef, model: modelRef.current, factory: factory, onModelChange: onModelChange, onRenderTabSet: onRenderTabSet }) }));
248
+ }
249
+ // ── CausalInspector (public API) ──────────────────────────────
250
+ const savedLayout = loadSavedLayout();
251
+ export function CausalInspector({ endpoint, fetchOptions, className, }) {
252
+ const transport = useMemo(() => createTransport(endpoint, fetchOptions),
253
+ // eslint-disable-next-line react-hooks/exhaustive-deps
254
+ [endpoint]);
255
+ const createEngine = useMemo(() => createInspectorEngine(transport, storage), [transport]);
256
+ return (_jsx("div", { className: `causal-inspector${className ? ` ${className}` : ""}`, children: _jsx(CausalInspectorProvider, { createEngine: createEngine, initialState: savedLayout ? { paneLayout: savedLayout } : undefined, children: _jsx(InspectorLayout, {}) }) }));
257
+ }
@@ -0,0 +1,4 @@
1
+ export declare function CopyablePayload({ payload, className, }: {
2
+ payload: string;
3
+ className?: string;
4
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { createPortal } from "react-dom";
4
+ import { JsonSyntax } from "./JsonSyntax";
5
+ import { copyToClipboard } from "../utils";
6
+ import { Copy, Check, Maximize2, X } from "lucide-react";
7
+ function formatPayload(raw) {
8
+ try {
9
+ return JSON.stringify(JSON.parse(raw), null, 2);
10
+ }
11
+ catch {
12
+ return raw;
13
+ }
14
+ }
15
+ function PayloadModal({ formatted, onClose, }) {
16
+ const [copied, setCopied] = useState(false);
17
+ useEffect(() => {
18
+ const handleKey = (e) => {
19
+ if (e.key === "Escape")
20
+ onClose();
21
+ };
22
+ document.addEventListener("keydown", handleKey);
23
+ return () => document.removeEventListener("keydown", handleKey);
24
+ }, [onClose]);
25
+ return createPortal(_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center", style: { background: "rgba(0, 0, 0, 0.6)", backdropFilter: "blur(4px)" }, onClick: onClose, children: _jsxs("div", { className: "relative w-[90vw] max-h-[90vh] overflow-auto rounded-xl border border-border p-5", style: { background: "rgba(15, 15, 20, 0.95)", boxShadow: "0 24px 64px rgba(0, 0, 0, 0.5)" }, onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { className: "absolute top-3 right-3 flex gap-1", children: [_jsx("button", { onClick: async () => {
26
+ await copyToClipboard(formatted);
27
+ setCopied(true);
28
+ setTimeout(() => setCopied(false), 1500);
29
+ }, className: "p-1.5 rounded-md hover:bg-white/[0.05] transition-colors text-xs text-muted-foreground/50 hover:text-foreground", title: "Copy payload", children: copied ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }) }), _jsx("button", { onClick: onClose, className: "p-1.5 rounded-md hover:bg-white/[0.05] transition-colors text-xs text-muted-foreground/50 hover:text-foreground", title: "Close", children: _jsx(X, { size: 14 }) })] }), _jsx("pre", { className: "text-xs whitespace-pre-wrap", children: _jsx(JsonSyntax, { json: formatted }) })] }) }), document.body);
30
+ }
31
+ export function CopyablePayload({ payload, className = "", }) {
32
+ const [copied, setCopied] = useState(false);
33
+ const [modalOpen, setModalOpen] = useState(false);
34
+ const formatted = formatPayload(payload);
35
+ return (_jsxs("div", { className: `relative ${className}`, children: [_jsx("pre", { className: "p-2.5 text-[10px] rounded-md border border-border overflow-auto whitespace-pre-wrap resize-y min-h-24 max-h-[80vh]", style: { background: "rgba(255, 255, 255, 0.015)" }, children: _jsx(JsonSyntax, { json: formatted }) }), _jsxs("div", { className: "absolute top-2 right-2 z-10 flex gap-1", children: [_jsx("button", { onClick: (e) => {
36
+ e.stopPropagation();
37
+ setModalOpen(true);
38
+ }, className: "p-1 rounded-md border border-border hover:bg-white/[0.05] transition-all text-[10px] text-muted-foreground/40 hover:text-muted-foreground", style: { background: "rgba(10, 10, 15, 0.8)", backdropFilter: "blur(4px)" }, title: "Expand", children: _jsx(Maximize2, { size: 12 }) }), _jsx("button", { onClick: async (e) => {
39
+ e.stopPropagation();
40
+ await copyToClipboard(formatted);
41
+ setCopied(true);
42
+ setTimeout(() => setCopied(false), 1500);
43
+ }, className: "p-1 rounded-md border border-border hover:bg-white/[0.05] transition-all text-[10px] text-muted-foreground/40 hover:text-muted-foreground", style: { background: "rgba(10, 10, 15, 0.8)", backdropFilter: "blur(4px)" }, title: "Copy", children: copied ? _jsx(Check, { size: 12 }) : _jsx(Copy, { size: 12 }) })] }), modalOpen && (_jsx(PayloadModal, { formatted: formatted, onClose: () => setModalOpen(false) }))] }));
44
+ }
@@ -0,0 +1 @@
1
+ export declare function FilterBar(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useSelector, useDispatch } from "../machine";
3
+ import { Search, X } from "lucide-react";
4
+ export function FilterBar() {
5
+ const filters = useSelector((s) => s.filters);
6
+ const dispatch = useDispatch();
7
+ return (_jsxs("div", { className: "flex flex-wrap items-center gap-2 px-3 py-2 border-b border-border", style: { background: "rgba(15, 15, 20, 0.6)", backdropFilter: "blur(8px)" }, children: [_jsxs("div", { className: "relative flex items-center", children: [_jsx(Search, { size: 12, className: "absolute left-2.5 text-muted-foreground pointer-events-none" }), _jsx("input", { type: "text", placeholder: "search events...", value: filters.search, onChange: (e) => dispatch({
8
+ type: "ui/filter_changed",
9
+ payload: { search: e.target.value },
10
+ }), className: "pl-7 pr-2 py-1.5 text-xs rounded-md bg-background/50 border border-border text-foreground placeholder:text-muted-foreground w-64 focus:outline-none focus:ring-1 focus:ring-indigo-500/40 focus:border-indigo-500/30 transition-all" })] }), _jsx("span", { className: "w-px h-4 bg-border" }), _jsx("label", { className: "text-[10px] text-muted-foreground uppercase tracking-wider", children: "From" }), _jsx("input", { type: "date", value: filters.from ?? "", onChange: (e) => dispatch({
11
+ type: "ui/filter_changed",
12
+ payload: { from: e.target.value || null },
13
+ }), className: "px-2 py-1.5 text-xs rounded-md bg-background/50 border border-border text-foreground w-32 focus:outline-none focus:ring-1 focus:ring-indigo-500/40 focus:border-indigo-500/30 transition-all" }), _jsx("label", { className: "text-[10px] text-muted-foreground uppercase tracking-wider", children: "To" }), _jsx("input", { type: "date", value: filters.to ?? "", onChange: (e) => dispatch({
14
+ type: "ui/filter_changed",
15
+ payload: { to: e.target.value || null },
16
+ }), className: "px-2 py-1.5 text-xs rounded-md bg-background/50 border border-border text-foreground w-32 focus:outline-none focus:ring-1 focus:ring-indigo-500/40 focus:border-indigo-500/30 transition-all" }), filters.correlationId && (_jsxs(_Fragment, { children: [_jsx("span", { className: "w-px h-4 bg-border" }), _jsxs("span", { className: "inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-purple-500/10 border border-purple-500/20 text-purple-400 text-[10px] font-mono", children: [filters.correlationId.slice(0, 8), _jsx("button", { onClick: () => dispatch({
17
+ type: "ui/filter_changed",
18
+ payload: { correlationId: null },
19
+ }), className: "hover:text-foreground transition-colors", children: _jsx(X, { size: 10 }) })] })] })), filters.aggregateKey && (_jsxs(_Fragment, { children: [_jsx("span", { className: "w-px h-4 bg-border" }), _jsxs("span", { className: "inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-teal-500/10 border border-teal-500/20 text-teal-400 text-[10px] font-mono", children: [filters.aggregateKey.split(":")[0], ":", filters.aggregateKey.split(":").slice(1).join(":").slice(0, 8), _jsx("button", { onClick: () => dispatch({
20
+ type: "ui/filter_changed",
21
+ payload: { aggregateKey: null },
22
+ }), className: "hover:text-foreground transition-colors", children: _jsx(X, { size: 10 }) })] })] }))] }));
23
+ }
@@ -0,0 +1 @@
1
+ export declare function GlobalScrubber(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,148 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useMemo, useRef } from "react";
3
+ import { useSelector, useDispatch } from "../machine";
4
+ import { getScrubberSequence } from "../utils";
5
+ import { Play, Pause, SkipBack, SkipForward, ChevronsRight, RotateCcw } from "lucide-react";
6
+ // ---------------------------------------------------------------------------
7
+ // Dual-handle range slider
8
+ // ---------------------------------------------------------------------------
9
+ function RangeSlider({ seqs, start, end, onStartChange, onEndChange, }) {
10
+ const trackRef = useRef(null);
11
+ const min = seqs[0] ?? 0;
12
+ const max = seqs[seqs.length - 1] ?? 0;
13
+ const range = max - min || 1;
14
+ const startVal = start ?? min;
15
+ const endVal = end ?? max;
16
+ const leftPct = ((startVal - min) / range) * 100;
17
+ const rightPct = ((endVal - min) / range) * 100;
18
+ function snapToNearest(value) {
19
+ return seqs.reduce((best, s) => Math.abs(s - value) < Math.abs(best - value) ? s : best, seqs[0]);
20
+ }
21
+ function valueFromPointer(clientX) {
22
+ const track = trackRef.current;
23
+ if (!track)
24
+ return min;
25
+ const rect = track.getBoundingClientRect();
26
+ const pct = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
27
+ return min + pct * range;
28
+ }
29
+ function handlePointerDown(which) {
30
+ return (e) => {
31
+ e.preventDefault();
32
+ const el = e.currentTarget;
33
+ el.setPointerCapture(e.pointerId);
34
+ const onMove = (ev) => {
35
+ const raw = valueFromPointer(ev.clientX);
36
+ const snapped = snapToNearest(raw);
37
+ if (which === "start") {
38
+ // Clamp: start cannot exceed end
39
+ const clampedEnd = end ?? max;
40
+ onStartChange(snapped >= clampedEnd ? clampedEnd : snapped <= min ? null : snapped);
41
+ }
42
+ else {
43
+ // Clamp: end cannot go below start
44
+ const clampedStart = start ?? min;
45
+ onEndChange(snapped <= clampedStart ? clampedStart : snapped >= max ? null : snapped);
46
+ }
47
+ };
48
+ const onUp = () => {
49
+ el.removeEventListener("pointermove", onMove);
50
+ el.removeEventListener("pointerup", onUp);
51
+ };
52
+ el.addEventListener("pointermove", onMove);
53
+ el.addEventListener("pointerup", onUp);
54
+ };
55
+ }
56
+ return (_jsxs("div", { ref: trackRef, className: "relative flex-1 h-5 flex items-center cursor-pointer group/slider", children: [_jsx("div", { className: "absolute inset-x-0 h-1 rounded-full bg-white/[0.06]" }), _jsx("div", { className: "absolute h-1 rounded-full bg-indigo-500/40 transition-none", style: { left: `${leftPct}%`, width: `${rightPct - leftPct}%` } }), _jsx("div", { onPointerDown: handlePointerDown("start"), className: "absolute w-3 h-3 rounded-full bg-white/60 hover:bg-white border border-white/20 hover:border-indigo-400/50 shadow-sm transition-colors duration-100 -translate-x-1/2 cursor-grab active:cursor-grabbing z-10", style: { left: `${leftPct}%` }, title: "Range start" }), _jsx("div", { onPointerDown: handlePointerDown("end"), className: "absolute w-3 h-3 rounded-full bg-indigo-400 hover:bg-indigo-300 border border-indigo-400/50 hover:border-indigo-300 shadow-sm transition-colors duration-100 -translate-x-1/2 cursor-grab active:cursor-grabbing z-10", style: { left: `${rightPct}%` }, title: "Range end" })] }));
57
+ }
58
+ // ---------------------------------------------------------------------------
59
+ // Context label
60
+ // ---------------------------------------------------------------------------
61
+ function getContextLabel(flowCorrelationId, flowSelection) {
62
+ if (!flowCorrelationId)
63
+ return "All events";
64
+ if (flowSelection) {
65
+ if (flowSelection.kind === "event-type")
66
+ return `${flowSelection.name} events`;
67
+ return `Reactor ${flowSelection.reactorId}`;
68
+ }
69
+ return `Flow ${flowCorrelationId.slice(0, 8)}`;
70
+ }
71
+ // ---------------------------------------------------------------------------
72
+ // GlobalScrubber
73
+ // ---------------------------------------------------------------------------
74
+ export function GlobalScrubber() {
75
+ const scrubberStart = useSelector((s) => s.scrubberStart);
76
+ const scrubberEnd = useSelector((s) => s.scrubberEnd);
77
+ const playing = useSelector((s) => s.scrubberPlaying);
78
+ const speed = useSelector((s) => s.scrubberSpeed);
79
+ const flowCorrelationId = useSelector((s) => s.flowCorrelationId);
80
+ const flowSelection = useSelector((s) => s.flowSelection);
81
+ const flowData = useSelector((s) => s.flowData);
82
+ const events = useSelector((s) => s.events);
83
+ const dispatch = useDispatch();
84
+ const seqs = useMemo(() => getScrubberSequence({ flowCorrelationId, flowData, flowSelection, events }), [flowCorrelationId, flowData, flowSelection, events]);
85
+ const endVal = scrubberEnd ?? (seqs[seqs.length - 1] ?? 0);
86
+ const isAtEnd = scrubberEnd == null || (seqs.length > 0 && scrubberEnd >= seqs[seqs.length - 1]);
87
+ // Count events in range
88
+ const startIndex = scrubberStart != null
89
+ ? seqs.filter((s) => s < scrubberStart).length + 1
90
+ : 1;
91
+ const endIndex = scrubberEnd != null
92
+ ? seqs.filter((s) => s <= scrubberEnd).length
93
+ : seqs.length;
94
+ const hasRange = scrubberStart != null;
95
+ const stepBack = useCallback(() => {
96
+ const idx = seqs.findIndex((s) => s >= endVal);
97
+ const prev = seqs[Math.max(0, idx - 1)];
98
+ if (prev != null)
99
+ dispatch({ type: "ui/scrubber_end_changed", payload: { end: prev } });
100
+ }, [seqs, endVal, dispatch]);
101
+ const stepForward = useCallback(() => {
102
+ const next = seqs.find((s) => s > endVal);
103
+ if (next != null) {
104
+ dispatch({ type: "ui/scrubber_end_changed", payload: { end: next } });
105
+ }
106
+ else {
107
+ dispatch({ type: "ui/scrubber_end_changed", payload: { end: null } });
108
+ }
109
+ }, [seqs, endVal, dispatch]);
110
+ const reset = useCallback(() => {
111
+ if (playing)
112
+ dispatch({ type: "ui/scrubber_play_toggled" });
113
+ dispatch({ type: "ui/scrubber_start_changed", payload: { start: null } });
114
+ dispatch({ type: "ui/scrubber_end_changed", payload: { end: seqs[0] ?? null } });
115
+ }, [seqs, playing, dispatch]);
116
+ const jumpToEnd = useCallback(() => {
117
+ if (playing)
118
+ dispatch({ type: "ui/scrubber_play_toggled" });
119
+ dispatch({ type: "ui/scrubber_end_changed", payload: { end: null } });
120
+ }, [playing, dispatch]);
121
+ const togglePlay = useCallback(() => {
122
+ if (!playing && isAtEnd && seqs.length > 0) {
123
+ dispatch({ type: "ui/scrubber_end_changed", payload: { end: seqs[0] } });
124
+ }
125
+ dispatch({ type: "ui/scrubber_play_toggled" });
126
+ }, [playing, isAtEnd, seqs, dispatch]);
127
+ const cycleSpeed = useCallback(() => {
128
+ const speeds = [500, 300, 150, 50];
129
+ const idx = speeds.indexOf(speed);
130
+ const next = speeds[(idx + 1) % speeds.length];
131
+ dispatch({ type: "ui/scrubber_speed_changed", payload: { speed: next } });
132
+ }, [speed, dispatch]);
133
+ const handleStartChange = useCallback((start) => {
134
+ dispatch({ type: "ui/scrubber_start_changed", payload: { start } });
135
+ }, [dispatch]);
136
+ const handleEndChange = useCallback((end) => {
137
+ dispatch({ type: "ui/scrubber_end_changed", payload: { end } });
138
+ }, [dispatch]);
139
+ const disabled = seqs.length < 2;
140
+ const speedLabel = speed <= 50 ? "4x" : speed <= 150 ? "2x" : speed <= 300 ? "1x" : "0.5x";
141
+ const btnClass = `p-1.5 rounded-md transition-all duration-150 ${disabled ? "text-muted-foreground/20 cursor-default" : "text-muted-foreground/60 hover:text-foreground hover:bg-white/[0.05]"}`;
142
+ const contextLabel = getContextLabel(flowCorrelationId, flowSelection);
143
+ return (_jsxs("div", { className: "flex items-center gap-2 px-3 py-2 border-t border-border shrink-0", style: { background: "rgba(15, 15, 20, 0.6)", backdropFilter: "blur(8px)" }, children: [_jsx("span", { className: "text-[9px] font-medium text-muted-foreground/40 uppercase tracking-wider shrink-0 max-w-[120px] truncate", title: contextLabel, children: contextLabel }), _jsx("button", { onClick: disabled ? undefined : reset, className: btnClass, title: "Reset to start", children: _jsx(RotateCcw, { size: 12 }) }), _jsx("button", { onClick: disabled ? undefined : stepBack, className: btnClass, title: "Step back", children: _jsx(SkipBack, { size: 12 }) }), _jsx("button", { onClick: disabled ? undefined : togglePlay, className: `p-1.5 rounded-md transition-all duration-150 ${disabled
144
+ ? "text-muted-foreground/20 cursor-default"
145
+ : playing
146
+ ? "text-indigo-400 bg-indigo-500/10"
147
+ : "text-muted-foreground/60 hover:text-foreground hover:bg-white/[0.05]"}`, title: playing ? "Pause" : "Play", children: playing ? _jsx(Pause, { size: 14 }) : _jsx(Play, { size: 14 }) }), _jsx("button", { onClick: disabled ? undefined : stepForward, className: btnClass, title: "Step forward", children: _jsx(SkipForward, { size: 12 }) }), _jsx("button", { onClick: disabled ? undefined : jumpToEnd, className: btnClass, title: "Jump to end", children: _jsx(ChevronsRight, { size: 12 }) }), disabled ? (_jsx("div", { className: "flex-1 h-5 flex items-center", children: _jsx("div", { className: "w-full h-1 rounded-full bg-white/[0.04]" }) })) : (_jsx(RangeSlider, { seqs: seqs, start: scrubberStart, end: scrubberEnd, onStartChange: handleStartChange, onEndChange: handleEndChange })), _jsx("span", { className: `text-[10px] tabular-nums min-w-[4rem] text-right shrink-0 ${disabled ? "text-muted-foreground/20" : "text-muted-foreground/60"}`, children: disabled ? `${seqs.length}` : `${hasRange ? `${startIndex}-${endIndex}` : endIndex}/${seqs.length}` }), _jsx("button", { onClick: disabled ? undefined : cycleSpeed, className: `text-[10px] px-2 py-1 rounded-md border tabular-nums min-w-[2.5rem] text-center transition-all duration-150 shrink-0 ${disabled ? "text-muted-foreground/20 border-border/50 cursor-default" : "text-muted-foreground/60 hover:text-foreground border-border hover:border-indigo-500/30"}`, title: "Playback speed", children: speedLabel })] }));
148
+ }
@@ -0,0 +1,3 @@
1
+ export declare function JsonSyntax({ json }: {
2
+ json: string;
3
+ }): import("react/jsx-runtime").JSX.Element;