@treelocator/runtime 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,287 @@
1
+ import { For, Show } from "solid-js";
2
+ import type { DejitterFinding, DejitterSummary } from "../dejitter/recorder";
3
+
4
+ export type InteractionEvent = {
5
+ t: number;
6
+ type: string;
7
+ target: string;
8
+ x: number;
9
+ y: number;
10
+ };
11
+
12
+ type RecordingResultsProps = {
13
+ findings: DejitterFinding[];
14
+ summary: DejitterSummary | null;
15
+ data: any;
16
+ elementPath: string;
17
+ interactions: InteractionEvent[];
18
+ onDismiss: () => void;
19
+ onReplay?: () => void;
20
+ replaying?: boolean;
21
+ onToast?: (msg: string) => void;
22
+ hasPrevious?: boolean;
23
+ onLoadPrevious?: () => void;
24
+ hasNext?: boolean;
25
+ onLoadNext?: () => void;
26
+ };
27
+
28
+ const SEVERITY_COLORS: Record<string, string> = {
29
+ high: "#ef4444",
30
+ medium: "#f59e0b",
31
+ low: "#eab308",
32
+ info: "#9ca3af",
33
+ };
34
+
35
+ function formatDataForClipboard(
36
+ data: any,
37
+ elementPath: string,
38
+ summary: DejitterSummary | null,
39
+ findings: DejitterFinding[],
40
+ interactions: InteractionEvent[]
41
+ ): string {
42
+ const lines: string[] = [];
43
+
44
+ // Element ancestry path from treelocator
45
+ if (elementPath) {
46
+ lines.push(`Element: ${elementPath}`);
47
+ }
48
+
49
+ // Header
50
+ lines.push(`Recording: ${Math.round(summary?.duration ?? 0)}ms, ${summary?.rawFrameCount ?? 0} frames`);
51
+ lines.push('');
52
+
53
+ // Property changes
54
+ if (data?.propStats?.props && data.propStats.props.length > 0) {
55
+ lines.push('Changed properties:');
56
+ for (const p of data.propStats.props) {
57
+ if (p.raw === 0) continue;
58
+ lines.push(` ${p.prop}: ${p.raw} changes (${p.mode})`);
59
+ }
60
+ lines.push('');
61
+ }
62
+
63
+ // Samples with actual values — replace dejitter element IDs with the ancestry path
64
+ if (data?.samples && data.samples.length > 0) {
65
+ lines.push('Timeline:');
66
+ for (const frame of data.samples) {
67
+ for (const change of frame.changes) {
68
+ const { id, ...props } = change;
69
+ const propEntries = Object.entries(props);
70
+ if (propEntries.length > 0) {
71
+ const vals = propEntries.map(([k, v]) => `${k}=${v}`).join(', ');
72
+ lines.push(` ${frame.t}ms: ${vals}`);
73
+ }
74
+ }
75
+ }
76
+ lines.push('');
77
+ }
78
+
79
+ // Findings — replace dejitter element IDs in descriptions
80
+ if (findings.length > 0) {
81
+ lines.push('Anomalies:');
82
+ for (const f of findings) {
83
+ lines.push(` [${f.severity}] ${f.type}: ${f.description}`);
84
+ }
85
+ lines.push('');
86
+ }
87
+
88
+ // Interactions
89
+ if (interactions.length > 0) {
90
+ lines.push('User interactions:');
91
+ for (const evt of interactions) {
92
+ lines.push(` ${evt.t}ms ${evt.type} ${evt.target} (${evt.x},${evt.y})`);
93
+ }
94
+ }
95
+
96
+ return lines.join('\n');
97
+ }
98
+
99
+ const buttonStyle = (active?: boolean) => ({
100
+ cursor: "pointer",
101
+ padding: "4px 10px",
102
+ "border-radius": "4px",
103
+ background: active ? "rgba(59, 130, 246, 0.2)" : "rgba(255, 255, 255, 0.08)",
104
+ color: active ? "#60a5fa" : "#9ca3af",
105
+ "font-size": "11px",
106
+ "font-weight": "600",
107
+ "line-height": "1.4",
108
+ transition: "background 0.15s, color 0.15s",
109
+ });
110
+
111
+ export function RecordingResults(props: RecordingResultsProps) {
112
+ const duration = () => props.summary?.duration ?? 0;
113
+ const frameCount = () => props.summary?.rawFrameCount ?? 0;
114
+
115
+ function handleCopy() {
116
+ const text = formatDataForClipboard(props.data, props.elementPath, props.summary, props.findings, props.interactions);
117
+ navigator.clipboard.writeText(text).then(() => {
118
+ props.onToast?.("Copied to clipboard");
119
+ });
120
+ }
121
+
122
+ return (
123
+ <div
124
+ style={{
125
+ position: "fixed",
126
+ bottom: "84px",
127
+ right: "20px",
128
+ "z-index": "2147483646",
129
+ width: "340px",
130
+ "max-height": "400px",
131
+ "overflow-y": "auto",
132
+ background: "rgba(15, 15, 15, 0.92)",
133
+ "backdrop-filter": "blur(12px)",
134
+ "border-radius": "12px",
135
+ border: "1px solid rgba(255, 255, 255, 0.1)",
136
+ "box-shadow": "0 8px 32px rgba(0, 0, 0, 0.4)",
137
+ color: "#e5e5e5",
138
+ "font-family": "system-ui, -apple-system, sans-serif",
139
+ "font-size": "12px",
140
+ "pointer-events": "auto",
141
+ }}
142
+ >
143
+ {/* Header */}
144
+ <div
145
+ style={{
146
+ display: "flex",
147
+ "align-items": "center",
148
+ "justify-content": "space-between",
149
+ padding: "12px 14px 8px",
150
+ "border-bottom": "1px solid rgba(255, 255, 255, 0.08)",
151
+ }}
152
+ >
153
+ <div>
154
+ <div style={{ "font-weight": "600", "font-size": "13px", color: "#fff" }}>
155
+ Recording Results
156
+ </div>
157
+ <div style={{ color: "#9ca3af", "margin-top": "2px" }}>
158
+ {Math.round(duration())}ms &middot; {frameCount()} frames
159
+ </div>
160
+ </div>
161
+ <div style={{ display: "flex", "align-items": "center", gap: "4px" }}>
162
+ <div style={buttonStyle()} onClick={handleCopy}>
163
+ Copy
164
+ </div>
165
+ {props.onReplay && (
166
+ <div style={buttonStyle(props.replaying)} onClick={props.onReplay}>
167
+ {props.replaying ? "Replaying..." : "Replay"}
168
+ </div>
169
+ )}
170
+ {props.hasPrevious && props.onLoadPrevious && (
171
+ <div style={buttonStyle()} onClick={props.onLoadPrevious}>
172
+ Prev
173
+ </div>
174
+ )}
175
+ {props.hasNext && props.onLoadNext && (
176
+ <div style={buttonStyle()} onClick={props.onLoadNext}>
177
+ Next
178
+ </div>
179
+ )}
180
+ <div
181
+ style={{
182
+ cursor: "pointer",
183
+ padding: "4px 8px",
184
+ "border-radius": "4px",
185
+ color: "#9ca3af",
186
+ "font-size": "16px",
187
+ "line-height": "1",
188
+ }}
189
+ onClick={props.onDismiss}
190
+ >
191
+ &times;
192
+ </div>
193
+ </div>
194
+ </div>
195
+
196
+ {/* Findings */}
197
+ <div style={{ padding: "8px 14px" }}>
198
+ <Show
199
+ when={props.findings.length > 0}
200
+ fallback={
201
+ <div
202
+ style={{
203
+ display: "flex",
204
+ "align-items": "center",
205
+ gap: "6px",
206
+ padding: "8px 0",
207
+ color: "#4ade80",
208
+ }}
209
+ >
210
+ <span style={{ "font-size": "14px" }}>&#10003;</span>
211
+ No anomalies detected
212
+ </div>
213
+ }
214
+ >
215
+ <div style={{ "margin-bottom": "4px", color: "#9ca3af", "font-size": "11px", "text-transform": "uppercase", "letter-spacing": "0.5px" }}>
216
+ Findings ({props.findings.length})
217
+ </div>
218
+ <For each={props.findings}>
219
+ {(finding) => (
220
+ <div
221
+ style={{
222
+ display: "flex",
223
+ "align-items": "flex-start",
224
+ gap: "8px",
225
+ padding: "6px 0",
226
+ "border-bottom": "1px solid rgba(255, 255, 255, 0.05)",
227
+ }}
228
+ >
229
+ <div
230
+ style={{
231
+ width: "8px",
232
+ height: "8px",
233
+ "border-radius": "50%",
234
+ background: SEVERITY_COLORS[finding.severity] || "#9ca3af",
235
+ "flex-shrink": "0",
236
+ "margin-top": "3px",
237
+ }}
238
+ />
239
+ <div style={{ "min-width": "0" }}>
240
+ <div style={{ display: "flex", gap: "6px", "align-items": "center" }}>
241
+ <span style={{ "font-weight": "600", color: "#fff" }}>{finding.type}</span>
242
+ <span style={{ color: SEVERITY_COLORS[finding.severity], "font-size": "11px" }}>
243
+ {finding.severity}
244
+ </span>
245
+ </div>
246
+ <div
247
+ style={{
248
+ color: "#a1a1aa",
249
+ "margin-top": "2px",
250
+ "line-height": "1.4",
251
+ "word-break": "break-word",
252
+ }}
253
+ >
254
+ {finding.description}
255
+ </div>
256
+ </div>
257
+ </div>
258
+ )}
259
+ </For>
260
+ </Show>
261
+ </div>
262
+
263
+ {/* Interactions */}
264
+ <Show when={props.interactions.length > 0}>
265
+ <div
266
+ style={{
267
+ padding: "8px 14px 12px",
268
+ "border-top": "1px solid rgba(255, 255, 255, 0.08)",
269
+ }}
270
+ >
271
+ <div style={{ "margin-bottom": "4px", color: "#9ca3af", "font-size": "11px", "text-transform": "uppercase", "letter-spacing": "0.5px" }}>
272
+ Interactions ({props.interactions.length})
273
+ </div>
274
+ <For each={props.interactions}>
275
+ {(evt) => (
276
+ <div style={{ padding: "3px 0", color: "#a1a1aa", "font-family": "monospace", "font-size": "11px" }}>
277
+ <span style={{ color: "#9ca3af" }}>{evt.t}ms</span>{" "}
278
+ <span style={{ color: "#60a5fa" }}>{evt.type}</span>{" "}
279
+ <span>{evt.target}</span>
280
+ </div>
281
+ )}
282
+ </For>
283
+ </div>
284
+ </Show>
285
+ </div>
286
+ );
287
+ }