@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.
- package/dist/browserApi.d.ts +45 -0
- package/dist/browserApi.js +23 -1
- package/dist/components/RecordingOutline.d.ts +5 -0
- package/dist/components/RecordingOutline.js +53 -0
- package/dist/components/RecordingResults.d.ts +25 -0
- package/dist/components/RecordingResults.js +272 -0
- package/dist/components/Runtime.js +505 -70
- package/dist/dejitter/recorder.d.ts +91 -0
- package/dist/dejitter/recorder.js +908 -0
- package/dist/functions/enrichAncestrySourceMaps.js +9 -2
- package/dist/functions/formatAncestryChain.js +27 -7
- package/dist/functions/formatAncestryChain.test.js +114 -0
- package/dist/functions/getUsableName.js +24 -2
- package/dist/output.css +13 -0
- package/package.json +2 -2
- package/src/browserApi.ts +74 -1
- package/src/components/RecordingOutline.tsx +66 -0
- package/src/components/RecordingResults.tsx +287 -0
- package/src/components/Runtime.tsx +534 -80
- package/src/dejitter/recorder.ts +938 -0
- package/src/functions/enrichAncestrySourceMaps.ts +9 -2
- package/src/functions/formatAncestryChain.test.ts +123 -0
- package/src/functions/formatAncestryChain.ts +28 -7
- package/src/functions/getUsableName.ts +24 -2
- package/.turbo/turbo-build.log +0 -32
- package/.turbo/turbo-dev.log +0 -32
- package/.turbo/turbo-test.log +0 -14
- package/.turbo/turbo-ts.log +0 -4
- package/LICENSE +0 -22
|
@@ -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 · {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
|
+
×
|
|
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" }}>✓</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
|
+
}
|