@treelocator/runtime 0.4.7 → 0.6.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/.eslintignore +1 -0
- package/dist/_generated_styles.d.ts +1 -1
- package/dist/_generated_styles.js +20 -0
- package/dist/_generated_tree_icon.d.ts +1 -1
- package/dist/adapters/HtmlElementTreeNode.d.ts +2 -2
- package/dist/adapters/HtmlElementTreeNode.js +4 -6
- package/dist/adapters/createTreeNode.js +17 -44
- package/dist/adapters/detectFramework.d.ts +8 -0
- package/dist/adapters/detectFramework.js +25 -0
- package/dist/adapters/detectFramework.test.d.ts +1 -0
- package/dist/adapters/detectFramework.test.js +60 -0
- package/dist/adapters/jsx/jsxAdapter.js +54 -89
- package/dist/adapters/jsx/jsxAdapter.test.d.ts +1 -0
- package/dist/adapters/jsx/jsxAdapter.test.js +273 -0
- package/dist/adapters/nextjs/parseNextjsDataAttributes.js +1 -1
- package/dist/adapters/nextjs/parseNextjsDataAttributes.test.d.ts +1 -0
- package/dist/adapters/nextjs/parseNextjsDataAttributes.test.js +158 -0
- package/dist/adapters/react/findFiberByHtmlElement.d.ts +1 -1
- package/dist/adapters/react/findFiberByHtmlElement.js +1 -1
- package/dist/adapters/react/getAllParentsElementsAndRootComponent.js +4 -0
- package/dist/adapters/resolveAdapter.d.ts +1 -1
- package/dist/adapters/resolveAdapter.js +4 -8
- package/dist/adapters/svelte/svelteAdapter.test.d.ts +1 -0
- package/dist/adapters/svelte/svelteAdapter.test.js +280 -0
- package/dist/adapters/vue/vueAdapter.test.d.ts +1 -0
- package/dist/adapters/vue/vueAdapter.test.js +222 -0
- package/dist/browserApi.d.ts +148 -0
- package/dist/browserApi.js +146 -5
- package/dist/browserApi.test.d.ts +1 -0
- package/dist/browserApi.test.js +287 -0
- package/dist/components/RecordingPillButton.d.ts +11 -0
- package/dist/components/RecordingPillButton.js +202 -0
- package/dist/components/RecordingResults.d.ts +2 -0
- package/dist/components/RecordingResults.js +213 -78
- package/dist/components/Runtime.js +161 -554
- package/dist/components/SettingsPanel.d.ts +5 -0
- package/dist/components/SettingsPanel.js +312 -0
- package/dist/consoleCapture.d.ts +9 -0
- package/dist/consoleCapture.js +95 -0
- package/dist/dejitter/recorder.d.ts +7 -1
- package/dist/dejitter/recorder.js +64 -1
- package/dist/functions/cssRuleInspector.d.ts +83 -0
- package/dist/functions/cssRuleInspector.js +608 -0
- package/dist/functions/cssRuleInspector.test.d.ts +1 -0
- package/dist/functions/cssRuleInspector.test.js +439 -0
- package/dist/functions/deduplicateLabels.test.d.ts +1 -0
- package/dist/functions/deduplicateLabels.test.js +178 -0
- package/dist/functions/enrichAncestrySourceMaps.js +0 -1
- package/dist/functions/extractComputedStyles.d.ts +51 -0
- package/dist/functions/extractComputedStyles.js +447 -0
- package/dist/functions/extractComputedStyles.test.d.ts +1 -0
- package/dist/functions/extractComputedStyles.test.js +549 -0
- package/dist/functions/formatAncestryChain.d.ts +8 -0
- package/dist/functions/formatAncestryChain.js +21 -1
- package/dist/functions/formatAncestryChain.test.js +18 -0
- package/dist/functions/getUsableName.test.d.ts +1 -0
- package/dist/functions/getUsableName.test.js +219 -0
- package/dist/functions/isCombinationModifiersPressed.test.d.ts +1 -0
- package/dist/functions/isCombinationModifiersPressed.test.js +192 -0
- package/dist/functions/mergeRects.test.js +210 -1
- package/dist/functions/namedSnapshots.d.ts +52 -0
- package/dist/functions/namedSnapshots.js +161 -0
- package/dist/functions/namedSnapshots.test.d.ts +1 -0
- package/dist/functions/namedSnapshots.test.js +85 -0
- package/dist/functions/normalizeFilePath.test.d.ts +1 -0
- package/dist/functions/normalizeFilePath.test.js +66 -0
- package/dist/functions/parseDataId.test.d.ts +1 -0
- package/dist/functions/parseDataId.test.js +101 -0
- package/dist/hooks/getStorage.d.ts +3 -0
- package/dist/hooks/getStorage.js +17 -0
- package/dist/hooks/useEventListeners.d.ts +15 -0
- package/dist/hooks/useEventListeners.js +56 -0
- package/dist/hooks/useLocatorStorage.d.ts +18 -0
- package/dist/hooks/useLocatorStorage.js +41 -0
- package/dist/hooks/useLocatorStorage.test.d.ts +1 -0
- package/dist/hooks/useLocatorStorage.test.js +124 -0
- package/dist/hooks/useRecordingState.d.ts +43 -0
- package/dist/hooks/useRecordingState.js +387 -0
- package/dist/hooks/useSettings.d.ts +13 -0
- package/dist/hooks/useSettings.js +66 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -2
- package/dist/initRuntime.d.ts +3 -1
- package/dist/initRuntime.js +4 -1
- package/dist/mcpBridge.d.ts +61 -0
- package/dist/mcpBridge.js +534 -0
- package/dist/mcpBridge.test.d.ts +1 -0
- package/dist/mcpBridge.test.js +248 -0
- package/dist/output.css +20 -0
- package/dist/visualDiff/diff.d.ts +9 -0
- package/dist/visualDiff/diff.js +209 -0
- package/dist/visualDiff/diff.test.d.ts +1 -0
- package/dist/visualDiff/diff.test.js +253 -0
- package/dist/visualDiff/settle.d.ts +3 -0
- package/dist/visualDiff/settle.js +50 -0
- package/dist/visualDiff/settle.test.d.ts +1 -0
- package/dist/visualDiff/settle.test.js +65 -0
- package/dist/visualDiff/snapshot.d.ts +4 -0
- package/dist/visualDiff/snapshot.js +84 -0
- package/dist/visualDiff/snapshot.test.d.ts +1 -0
- package/dist/visualDiff/snapshot.test.js +245 -0
- package/dist/visualDiff/types.d.ts +37 -0
- package/dist/visualDiff/types.js +1 -0
- package/package.json +2 -2
- package/scripts/wrapCSS.js +1 -1
- package/scripts/wrapImage.js +1 -1
- package/src/_generated_styles.ts +21 -1
- package/src/_generated_tree_icon.ts +1 -1
- package/src/adapters/HtmlElementTreeNode.ts +10 -7
- package/src/adapters/createTreeNode.ts +12 -51
- package/src/adapters/detectFramework.test.ts +73 -0
- package/src/adapters/detectFramework.ts +28 -0
- package/src/adapters/jsx/jsxAdapter.test.ts +240 -0
- package/src/adapters/jsx/jsxAdapter.ts +53 -106
- package/src/adapters/nextjs/parseNextjsDataAttributes.test.ts +212 -0
- package/src/adapters/nextjs/parseNextjsDataAttributes.ts +1 -1
- package/src/adapters/react/findDebugSource.ts +5 -6
- package/src/adapters/react/findFiberByHtmlElement.ts +3 -3
- package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +3 -0
- package/src/adapters/react/reactAdapter.ts +1 -2
- package/src/adapters/resolveAdapter.ts +4 -14
- package/src/adapters/svelte/svelteAdapter.test.ts +334 -0
- package/src/adapters/vue/vueAdapter.test.ts +259 -0
- package/src/browserApi.test.ts +329 -0
- package/src/browserApi.ts +351 -4
- package/src/components/RecordingPillButton.tsx +301 -0
- package/src/components/RecordingResults.tsx +114 -13
- package/src/components/Runtime.tsx +176 -621
- package/src/components/SettingsPanel.tsx +339 -0
- package/src/consoleCapture.ts +113 -0
- package/src/dejitter/recorder.ts +67 -3
- package/src/functions/cssRuleInspector.test.ts +517 -0
- package/src/functions/cssRuleInspector.ts +708 -0
- package/src/functions/deduplicateLabels.test.ts +115 -0
- package/src/functions/enrichAncestrySourceMaps.ts +6 -3
- package/src/functions/extractComputedStyles.test.ts +681 -0
- package/src/functions/extractComputedStyles.ts +768 -0
- package/src/functions/formatAncestryChain.test.ts +23 -1
- package/src/functions/formatAncestryChain.ts +22 -1
- package/src/functions/getUsableName.test.ts +242 -0
- package/src/functions/isCombinationModifiersPressed.test.ts +156 -0
- package/src/functions/mergeRects.test.ts +111 -1
- package/src/functions/namedSnapshots.test.ts +106 -0
- package/src/functions/namedSnapshots.ts +232 -0
- package/src/functions/normalizeFilePath.test.ts +80 -0
- package/src/functions/parseDataId.test.ts +125 -0
- package/src/hooks/getStorage.ts +26 -0
- package/src/hooks/useEventListeners.ts +97 -0
- package/src/hooks/useLocatorStorage.test.ts +127 -0
- package/src/hooks/useLocatorStorage.ts +60 -0
- package/src/hooks/useRecordingState.ts +516 -0
- package/src/hooks/useSettings.ts +83 -0
- package/src/index.ts +10 -5
- package/src/initRuntime.ts +5 -0
- package/src/mcpBridge.test.ts +260 -0
- package/src/mcpBridge.ts +677 -0
- package/src/visualDiff/diff.test.ts +167 -0
- package/src/visualDiff/diff.ts +242 -0
- package/src/visualDiff/settle.test.ts +77 -0
- package/src/visualDiff/settle.ts +62 -0
- package/src/visualDiff/snapshot.test.ts +200 -0
- package/src/visualDiff/snapshot.ts +119 -0
- package/src/visualDiff/types.ts +40 -0
- package/tsconfig.json +3 -1
- package/vitest.config.ts +18 -0
- package/jest.config.ts +0 -195
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { For } from "solid-js";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_SETTINGS,
|
|
4
|
+
resetSettings,
|
|
5
|
+
setSetting,
|
|
6
|
+
settings,
|
|
7
|
+
type TreelocatorSettings,
|
|
8
|
+
} from "../hooks/useSettings";
|
|
9
|
+
|
|
10
|
+
type SettingsPanelProps = {
|
|
11
|
+
onDismiss: () => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type ToggleKey =
|
|
15
|
+
| "anomalyTracking"
|
|
16
|
+
| "visualDiff"
|
|
17
|
+
| "computedStyles"
|
|
18
|
+
| "computedStylesIncludeDefaults";
|
|
19
|
+
type NumberKey = "sampleRate" | "maxDurationMs" | "jumpMinAbsolute" | "lagMinDelay";
|
|
20
|
+
|
|
21
|
+
const TOGGLES: { key: ToggleKey; label: string; hint: string }[] = [
|
|
22
|
+
{
|
|
23
|
+
key: "anomalyTracking",
|
|
24
|
+
label: "Anomaly tracking",
|
|
25
|
+
hint: "Detect jitter, jumps, stutter, stuck frames",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: "visualDiff",
|
|
29
|
+
label: "Visual diff",
|
|
30
|
+
hint: "Snapshot before/after element tree",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
key: "computedStyles",
|
|
34
|
+
label: "Computed styles",
|
|
35
|
+
hint: "Include styles in alt+click clipboard output",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: "computedStylesIncludeDefaults",
|
|
39
|
+
label: "Include default styles",
|
|
40
|
+
hint: "Show browser-default values like display:block and font-weight:bold",
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const NUMBERS: {
|
|
45
|
+
key: NumberKey;
|
|
46
|
+
label: string;
|
|
47
|
+
hint: string;
|
|
48
|
+
unit: string;
|
|
49
|
+
min: number;
|
|
50
|
+
max: number;
|
|
51
|
+
step: number;
|
|
52
|
+
}[] = [
|
|
53
|
+
{
|
|
54
|
+
key: "sampleRate",
|
|
55
|
+
label: "Sample rate",
|
|
56
|
+
hint: "Max frames per second to capture",
|
|
57
|
+
unit: "Hz",
|
|
58
|
+
min: 1,
|
|
59
|
+
max: 60,
|
|
60
|
+
step: 1,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
key: "maxDurationMs",
|
|
64
|
+
label: "Max duration",
|
|
65
|
+
hint: "Auto-stop recording after this long",
|
|
66
|
+
unit: "ms",
|
|
67
|
+
min: 1000,
|
|
68
|
+
max: 120000,
|
|
69
|
+
step: 1000,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: "jumpMinAbsolute",
|
|
73
|
+
label: "Jump threshold",
|
|
74
|
+
hint: "Minimum px delta to flag a jump",
|
|
75
|
+
unit: "px",
|
|
76
|
+
min: 1,
|
|
77
|
+
max: 1000,
|
|
78
|
+
step: 1,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
key: "lagMinDelay",
|
|
82
|
+
label: "Lag threshold",
|
|
83
|
+
hint: "Minimum rAF delay to flag lag",
|
|
84
|
+
unit: "ms",
|
|
85
|
+
min: 1,
|
|
86
|
+
max: 1000,
|
|
87
|
+
step: 1,
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const rowStyle = {
|
|
92
|
+
display: "flex",
|
|
93
|
+
"align-items": "center",
|
|
94
|
+
"justify-content": "space-between",
|
|
95
|
+
gap: "12px",
|
|
96
|
+
padding: "8px 0",
|
|
97
|
+
"border-bottom": "1px solid rgba(255, 255, 255, 0.05)",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const labelStyle = { "min-width": "0", flex: "1 1 auto" };
|
|
101
|
+
const labelTitle = { "font-weight": "600", color: "#fff" };
|
|
102
|
+
const labelHint = {
|
|
103
|
+
color: "#9ca3af",
|
|
104
|
+
"font-size": "11px",
|
|
105
|
+
"margin-top": "2px",
|
|
106
|
+
"line-height": "1.3",
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const sectionTitleStyle = {
|
|
110
|
+
"margin-bottom": "4px",
|
|
111
|
+
color: "#9ca3af",
|
|
112
|
+
"font-size": "11px",
|
|
113
|
+
"text-transform": "uppercase",
|
|
114
|
+
"letter-spacing": "0.5px",
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
function Toggle(props: {
|
|
118
|
+
checked: boolean;
|
|
119
|
+
onChange: (v: boolean) => void;
|
|
120
|
+
}) {
|
|
121
|
+
return (
|
|
122
|
+
<div
|
|
123
|
+
role="switch"
|
|
124
|
+
aria-checked={props.checked}
|
|
125
|
+
onClick={() => props.onChange(!props.checked)}
|
|
126
|
+
style={{
|
|
127
|
+
cursor: "pointer",
|
|
128
|
+
width: "32px",
|
|
129
|
+
height: "18px",
|
|
130
|
+
"border-radius": "9px",
|
|
131
|
+
background: props.checked
|
|
132
|
+
? "rgba(59, 130, 246, 0.6)"
|
|
133
|
+
: "rgba(255, 255, 255, 0.12)",
|
|
134
|
+
position: "relative",
|
|
135
|
+
transition: "background 0.15s",
|
|
136
|
+
"flex-shrink": "0",
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
<div
|
|
140
|
+
style={{
|
|
141
|
+
position: "absolute",
|
|
142
|
+
top: "2px",
|
|
143
|
+
left: props.checked ? "16px" : "2px",
|
|
144
|
+
width: "14px",
|
|
145
|
+
height: "14px",
|
|
146
|
+
"border-radius": "50%",
|
|
147
|
+
background: "#fff",
|
|
148
|
+
transition: "left 0.15s",
|
|
149
|
+
}}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function NumberInput(props: {
|
|
156
|
+
value: number;
|
|
157
|
+
min: number;
|
|
158
|
+
max: number;
|
|
159
|
+
step: number;
|
|
160
|
+
unit: string;
|
|
161
|
+
onChange: (v: number) => void;
|
|
162
|
+
}) {
|
|
163
|
+
return (
|
|
164
|
+
<div
|
|
165
|
+
style={{
|
|
166
|
+
display: "flex",
|
|
167
|
+
"align-items": "center",
|
|
168
|
+
gap: "4px",
|
|
169
|
+
"flex-shrink": "0",
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
<input
|
|
173
|
+
type="number"
|
|
174
|
+
value={props.value}
|
|
175
|
+
min={props.min}
|
|
176
|
+
max={props.max}
|
|
177
|
+
step={props.step}
|
|
178
|
+
onInput={(e) => {
|
|
179
|
+
const n = Number(e.currentTarget.value);
|
|
180
|
+
if (!Number.isFinite(n)) return;
|
|
181
|
+
const clamped = Math.min(props.max, Math.max(props.min, n));
|
|
182
|
+
props.onChange(clamped);
|
|
183
|
+
}}
|
|
184
|
+
style={{
|
|
185
|
+
width: "72px",
|
|
186
|
+
padding: "4px 6px",
|
|
187
|
+
"border-radius": "4px",
|
|
188
|
+
background: "rgba(255, 255, 255, 0.08)",
|
|
189
|
+
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
190
|
+
color: "#fff",
|
|
191
|
+
"font-family": "inherit",
|
|
192
|
+
"font-size": "11px",
|
|
193
|
+
"text-align": "right",
|
|
194
|
+
outline: "none",
|
|
195
|
+
}}
|
|
196
|
+
/>
|
|
197
|
+
<span style={{ color: "#9ca3af", "font-size": "11px", "min-width": "18px" }}>
|
|
198
|
+
{props.unit}
|
|
199
|
+
</span>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function SettingsPanel(props: SettingsPanelProps) {
|
|
205
|
+
const current = () => settings();
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<div
|
|
209
|
+
data-treelocator-settings-panel
|
|
210
|
+
style={{
|
|
211
|
+
position: "fixed",
|
|
212
|
+
bottom: "180px",
|
|
213
|
+
right: "20px",
|
|
214
|
+
"z-index": "2147483646",
|
|
215
|
+
width: "340px",
|
|
216
|
+
"max-height": "400px",
|
|
217
|
+
"overflow-y": "auto",
|
|
218
|
+
background: "rgba(15, 15, 15, 0.92)",
|
|
219
|
+
"backdrop-filter": "blur(12px)",
|
|
220
|
+
"border-radius": "12px",
|
|
221
|
+
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
222
|
+
"box-shadow": "0 8px 32px rgba(0, 0, 0, 0.4)",
|
|
223
|
+
color: "#e5e5e5",
|
|
224
|
+
"font-family": "system-ui, -apple-system, sans-serif",
|
|
225
|
+
"font-size": "12px",
|
|
226
|
+
"pointer-events": "auto",
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
{/* Header */}
|
|
230
|
+
<div
|
|
231
|
+
style={{
|
|
232
|
+
display: "flex",
|
|
233
|
+
"align-items": "center",
|
|
234
|
+
"justify-content": "space-between",
|
|
235
|
+
padding: "12px 14px 8px",
|
|
236
|
+
"border-bottom": "1px solid rgba(255, 255, 255, 0.08)",
|
|
237
|
+
}}
|
|
238
|
+
>
|
|
239
|
+
<div>
|
|
240
|
+
<div style={{ "font-weight": "600", "font-size": "13px", color: "#fff" }}>
|
|
241
|
+
Settings
|
|
242
|
+
</div>
|
|
243
|
+
<div style={{ color: "#9ca3af", "margin-top": "2px" }}>
|
|
244
|
+
Persisted to localStorage
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
<div style={{ display: "flex", "align-items": "center", gap: "4px" }}>
|
|
248
|
+
<div
|
|
249
|
+
style={{
|
|
250
|
+
cursor: "pointer",
|
|
251
|
+
padding: "4px 10px",
|
|
252
|
+
"border-radius": "4px",
|
|
253
|
+
background: "rgba(255, 255, 255, 0.08)",
|
|
254
|
+
color: "#9ca3af",
|
|
255
|
+
"font-size": "11px",
|
|
256
|
+
"font-weight": "600",
|
|
257
|
+
}}
|
|
258
|
+
onClick={resetSettings}
|
|
259
|
+
>
|
|
260
|
+
Reset
|
|
261
|
+
</div>
|
|
262
|
+
<div
|
|
263
|
+
style={{
|
|
264
|
+
cursor: "pointer",
|
|
265
|
+
padding: "4px 8px",
|
|
266
|
+
"border-radius": "4px",
|
|
267
|
+
color: "#9ca3af",
|
|
268
|
+
"font-size": "16px",
|
|
269
|
+
"line-height": "1",
|
|
270
|
+
}}
|
|
271
|
+
onClick={props.onDismiss}
|
|
272
|
+
>
|
|
273
|
+
×
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
{/* Feature toggles */}
|
|
279
|
+
<div style={{ padding: "8px 14px" }}>
|
|
280
|
+
<div style={sectionTitleStyle}>Features</div>
|
|
281
|
+
<For each={TOGGLES}>
|
|
282
|
+
{(item) => (
|
|
283
|
+
<div style={rowStyle}>
|
|
284
|
+
<div style={labelStyle}>
|
|
285
|
+
<div style={labelTitle}>{item.label}</div>
|
|
286
|
+
<div style={labelHint}>{item.hint}</div>
|
|
287
|
+
</div>
|
|
288
|
+
<Toggle
|
|
289
|
+
checked={current()[item.key]}
|
|
290
|
+
onChange={(v) => setSetting(item.key, v)}
|
|
291
|
+
/>
|
|
292
|
+
</div>
|
|
293
|
+
)}
|
|
294
|
+
</For>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
{/* Thresholds */}
|
|
298
|
+
<div
|
|
299
|
+
style={{
|
|
300
|
+
padding: "8px 14px 12px",
|
|
301
|
+
"border-top": "1px solid rgba(255, 255, 255, 0.08)",
|
|
302
|
+
}}
|
|
303
|
+
>
|
|
304
|
+
<div style={sectionTitleStyle}>Thresholds</div>
|
|
305
|
+
<For each={NUMBERS}>
|
|
306
|
+
{(item) => (
|
|
307
|
+
<div style={rowStyle}>
|
|
308
|
+
<div style={labelStyle}>
|
|
309
|
+
<div style={labelTitle}>{item.label}</div>
|
|
310
|
+
<div style={labelHint}>{item.hint}</div>
|
|
311
|
+
</div>
|
|
312
|
+
<NumberInput
|
|
313
|
+
value={current()[item.key]}
|
|
314
|
+
min={item.min}
|
|
315
|
+
max={item.max}
|
|
316
|
+
step={item.step}
|
|
317
|
+
unit={item.unit}
|
|
318
|
+
onChange={(v) =>
|
|
319
|
+
setSetting(item.key as keyof TreelocatorSettings, v as never)
|
|
320
|
+
}
|
|
321
|
+
/>
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
</For>
|
|
325
|
+
<div
|
|
326
|
+
style={{
|
|
327
|
+
color: "#6b7280",
|
|
328
|
+
"font-size": "11px",
|
|
329
|
+
"margin-top": "8px",
|
|
330
|
+
}}
|
|
331
|
+
>
|
|
332
|
+
Defaults: sample {DEFAULT_SETTINGS.sampleRate}Hz, max{" "}
|
|
333
|
+
{DEFAULT_SETTINGS.maxDurationMs}ms, jump {DEFAULT_SETTINGS.jumpMinAbsolute}px,
|
|
334
|
+
lag {DEFAULT_SETTINGS.lagMinDelay}ms
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export type ConsoleLevel = "log" | "info" | "warn" | "error" | "debug";
|
|
2
|
+
|
|
3
|
+
export interface ConsoleEntry {
|
|
4
|
+
level: ConsoleLevel;
|
|
5
|
+
timestamp: number;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const MAX_ENTRIES = 500;
|
|
10
|
+
const LEVELS: ConsoleLevel[] = ["log", "info", "warn", "error", "debug"];
|
|
11
|
+
|
|
12
|
+
const CAPTURE_FLAG = "__treelocator_console_captured__";
|
|
13
|
+
const entries: ConsoleEntry[] = [];
|
|
14
|
+
|
|
15
|
+
function formatArg(value: unknown, seen: WeakSet<object>): string {
|
|
16
|
+
if (value === null) return "null";
|
|
17
|
+
if (value === undefined) return "undefined";
|
|
18
|
+
const t = typeof value;
|
|
19
|
+
if (t === "string") return value as string;
|
|
20
|
+
if (t === "number" || t === "boolean" || t === "bigint") return String(value);
|
|
21
|
+
if (t === "function") {
|
|
22
|
+
const name = (value as { name?: string }).name || "anonymous";
|
|
23
|
+
return `[Function: ${name}]`;
|
|
24
|
+
}
|
|
25
|
+
if (value instanceof Error) {
|
|
26
|
+
return `${value.name}: ${value.message}`;
|
|
27
|
+
}
|
|
28
|
+
if (typeof Element !== "undefined" && value instanceof Element) {
|
|
29
|
+
const el = value as Element;
|
|
30
|
+
const id = el.id ? `#${el.id}` : "";
|
|
31
|
+
const cls = el.className && typeof el.className === "string"
|
|
32
|
+
? `.${el.className.trim().split(/\s+/).join(".")}`
|
|
33
|
+
: "";
|
|
34
|
+
return `<${el.tagName.toLowerCase()}${id}${cls}>`;
|
|
35
|
+
}
|
|
36
|
+
if (t === "object") {
|
|
37
|
+
if (seen.has(value as object)) return "[Circular]";
|
|
38
|
+
seen.add(value as object);
|
|
39
|
+
try {
|
|
40
|
+
const localSeen = new WeakSet<object>();
|
|
41
|
+
return JSON.stringify(value, (_key, v) => {
|
|
42
|
+
if (v === null || v === undefined) return v;
|
|
43
|
+
if (typeof v === "bigint") return v.toString();
|
|
44
|
+
if (typeof v === "function") return `[Function: ${v.name || "anonymous"}]`;
|
|
45
|
+
if (typeof v === "object") {
|
|
46
|
+
if (localSeen.has(v)) return "[Circular]";
|
|
47
|
+
localSeen.add(v);
|
|
48
|
+
}
|
|
49
|
+
return v;
|
|
50
|
+
});
|
|
51
|
+
} catch {
|
|
52
|
+
try {
|
|
53
|
+
return String(value);
|
|
54
|
+
} catch {
|
|
55
|
+
return "[Unserializable]";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return String(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function formatArgs(args: unknown[]): string {
|
|
63
|
+
const seen = new WeakSet<object>();
|
|
64
|
+
return args.map((arg) => formatArg(arg, seen)).join(" ");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function pushEntry(level: ConsoleLevel, args: unknown[]): void {
|
|
68
|
+
entries.push({
|
|
69
|
+
level,
|
|
70
|
+
timestamp: Date.now(),
|
|
71
|
+
message: formatArgs(args),
|
|
72
|
+
});
|
|
73
|
+
if (entries.length > MAX_ENTRIES) {
|
|
74
|
+
entries.splice(0, entries.length - MAX_ENTRIES);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function installConsoleCapture(): void {
|
|
79
|
+
if (typeof console === "undefined") return;
|
|
80
|
+
const target = console as unknown as Record<string, unknown>;
|
|
81
|
+
if (target[CAPTURE_FLAG]) return;
|
|
82
|
+
target[CAPTURE_FLAG] = true;
|
|
83
|
+
|
|
84
|
+
for (const level of LEVELS) {
|
|
85
|
+
const original = target[level];
|
|
86
|
+
if (typeof original !== "function") continue;
|
|
87
|
+
const wrapped = function (this: unknown, ...args: unknown[]) {
|
|
88
|
+
try {
|
|
89
|
+
pushEntry(level, args);
|
|
90
|
+
} catch {
|
|
91
|
+
// Swallow — capture must never break logging.
|
|
92
|
+
}
|
|
93
|
+
return (original as (...a: unknown[]) => unknown).apply(this, args);
|
|
94
|
+
};
|
|
95
|
+
try {
|
|
96
|
+
target[level] = wrapped;
|
|
97
|
+
} catch {
|
|
98
|
+
// Some environments freeze console — ignore.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getConsoleEntries(last?: number): ConsoleEntry[] {
|
|
104
|
+
if (typeof last !== "number" || !Number.isFinite(last) || last <= 0) {
|
|
105
|
+
return entries.slice();
|
|
106
|
+
}
|
|
107
|
+
const n = Math.min(Math.floor(last), entries.length);
|
|
108
|
+
return entries.slice(entries.length - n);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function clearConsoleEntries(): void {
|
|
112
|
+
entries.length = 0;
|
|
113
|
+
}
|
package/src/dejitter/recorder.ts
CHANGED
|
@@ -20,11 +20,12 @@ export interface DejitterConfig {
|
|
|
20
20
|
stutter: { velocityRatio: number; maxFrames: number; minVelocity: number };
|
|
21
21
|
stuck: { minStillFrames: number; maxDelta: number; minSurroundingVelocity: number; highDuration: number; medDuration: number };
|
|
22
22
|
outlier: { ratioThreshold: number };
|
|
23
|
+
lag: { minDelay: number; highDelay: number; medDelay: number };
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export interface DejitterFinding {
|
|
27
|
-
type: 'jitter' | 'flicker' | 'shiver' | 'jump' | 'stutter' | 'stuck' | 'outlier';
|
|
28
|
+
type: 'jitter' | 'flicker' | 'shiver' | 'jump' | 'stutter' | 'stuck' | 'outlier' | 'lag';
|
|
28
29
|
severity: 'high' | 'medium' | 'low' | 'info';
|
|
29
30
|
elem: string;
|
|
30
31
|
elemLabel: { tag: string; cls: string; text: string };
|
|
@@ -50,7 +51,7 @@ export interface DejitterAPI {
|
|
|
50
51
|
getData(): any;
|
|
51
52
|
summary(json?: boolean): string | DejitterSummary;
|
|
52
53
|
findings(json?: boolean): string | DejitterFinding[];
|
|
53
|
-
getRaw(): { rawFrames: any[]; mutations: any[] };
|
|
54
|
+
getRaw(): { rawFrames: any[]; mutations: any[]; interactions: any[] };
|
|
54
55
|
toJSON(): string;
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -70,6 +71,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
70
71
|
stutter: { velocityRatio: 0.3, maxFrames: 3, minVelocity: 0.5 },
|
|
71
72
|
stuck: { minStillFrames: 3, maxDelta: 0.5, minSurroundingVelocity: 1, highDuration: 500, medDuration: 200 },
|
|
72
73
|
outlier: { ratioThreshold: 3 },
|
|
74
|
+
lag: { minDelay: 50, highDelay: 200, medDelay: 100 },
|
|
73
75
|
},
|
|
74
76
|
};
|
|
75
77
|
|
|
@@ -79,6 +81,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
79
81
|
let rafId: number | null = null;
|
|
80
82
|
let stopTimer: ReturnType<typeof setTimeout> | null = null;
|
|
81
83
|
let mutationObserver: MutationObserver | null = null;
|
|
84
|
+
let interactionAbort: AbortController | null = null;
|
|
82
85
|
let onStopCallbacks: Array<() => void> = [];
|
|
83
86
|
|
|
84
87
|
let lastSeen = new Map<string, Record<string, any>>();
|
|
@@ -86,6 +89,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
86
89
|
let lastChangeTime = 0;
|
|
87
90
|
let hasSeenChange = false;
|
|
88
91
|
let mutations: any[] = [];
|
|
92
|
+
let interactions: Array<{ t: number; type: string }> = [];
|
|
89
93
|
let nextElemId = 0;
|
|
90
94
|
|
|
91
95
|
function elemId(el: any): string {
|
|
@@ -212,6 +216,19 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
212
216
|
});
|
|
213
217
|
}
|
|
214
218
|
|
|
219
|
+
function startInteractionListeners(): void {
|
|
220
|
+
interactionAbort = new AbortController();
|
|
221
|
+
const opts: AddEventListenerOptions & { signal: AbortSignal } = { capture: true, signal: interactionAbort.signal };
|
|
222
|
+
const handler = (e: Event): void => {
|
|
223
|
+
if (!recording) return;
|
|
224
|
+
const t = Math.round(performance.now() - startTime);
|
|
225
|
+
interactions.push({ t, type: e.type });
|
|
226
|
+
};
|
|
227
|
+
for (const evt of ['click', 'pointerdown', 'keydown'] as const) {
|
|
228
|
+
document.addEventListener(evt, handler, opts);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
215
232
|
// --- Downsampling ---
|
|
216
233
|
|
|
217
234
|
function downsample(): any[] {
|
|
@@ -778,6 +795,49 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
778
795
|
return findings;
|
|
779
796
|
}
|
|
780
797
|
|
|
798
|
+
function detectLagFindings(elements: Record<string, any>): DejitterFinding[] {
|
|
799
|
+
const findings: DejitterFinding[] = [];
|
|
800
|
+
if (interactions.length === 0 || rawFrames.length === 0) return findings;
|
|
801
|
+
|
|
802
|
+
const lt = config.thresholds.lag;
|
|
803
|
+
|
|
804
|
+
for (const interaction of interactions) {
|
|
805
|
+
let firstFrame: { t: number; changes: any[] } | null = null;
|
|
806
|
+
for (const frame of rawFrames) {
|
|
807
|
+
if (frame.t > interaction.t) {
|
|
808
|
+
firstFrame = frame;
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (!firstFrame) continue;
|
|
814
|
+
|
|
815
|
+
const delay = firstFrame.t - interaction.t;
|
|
816
|
+
if (delay < lt.minDelay) continue;
|
|
817
|
+
|
|
818
|
+
const severity = delay >= lt.highDelay ? 'high' : delay >= lt.medDelay ? 'medium' : 'low';
|
|
819
|
+
|
|
820
|
+
const firstChange = firstFrame.changes[0];
|
|
821
|
+
const label = (firstChange && elements[firstChange.id]) || { tag: '?', cls: '', text: '' };
|
|
822
|
+
|
|
823
|
+
findings.push(makeFinding(
|
|
824
|
+
'lag', severity,
|
|
825
|
+
firstChange?.id || '?', label, interaction.type,
|
|
826
|
+
`${delay}ms between ${interaction.type} at t=${interaction.t}ms and first visual change at t=${firstFrame.t}ms`,
|
|
827
|
+
{
|
|
828
|
+
lag: {
|
|
829
|
+
interactionType: interaction.type,
|
|
830
|
+
interactionT: interaction.t,
|
|
831
|
+
firstChangeT: firstFrame.t,
|
|
832
|
+
delay,
|
|
833
|
+
},
|
|
834
|
+
}
|
|
835
|
+
));
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return findings;
|
|
839
|
+
}
|
|
840
|
+
|
|
781
841
|
function deduplicateShivers(findings: DejitterFinding[]): DejitterFinding[] {
|
|
782
842
|
const shiverFindings = findings.filter((f) => f.type === 'shiver');
|
|
783
843
|
const otherFindings = findings.filter((f) => f.type !== 'shiver');
|
|
@@ -814,6 +874,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
814
874
|
findings = findings.concat(detectJumpFindings(propStats, elements));
|
|
815
875
|
findings = findings.concat(detectStutterFindings(propStats, elements));
|
|
816
876
|
findings = findings.concat(detectStuckFindings(propStats, elements));
|
|
877
|
+
findings = findings.concat(detectLagFindings(elements));
|
|
817
878
|
findings = deduplicateShivers(findings);
|
|
818
879
|
|
|
819
880
|
const sevOrder: Record<string, number> = { high: 0, medium: 1, low: 2, info: 3 };
|
|
@@ -842,6 +903,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
842
903
|
start() {
|
|
843
904
|
rawFrames = [];
|
|
844
905
|
mutations = [];
|
|
906
|
+
interactions = [];
|
|
845
907
|
lastSeen = new Map();
|
|
846
908
|
nextElemId = 0;
|
|
847
909
|
recording = true;
|
|
@@ -851,6 +913,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
851
913
|
onStopCallbacks = [];
|
|
852
914
|
|
|
853
915
|
startMutationObserver();
|
|
916
|
+
startInteractionListeners();
|
|
854
917
|
rafId = requestAnimationFrame(loop);
|
|
855
918
|
|
|
856
919
|
if (config.maxDuration > 0) {
|
|
@@ -865,6 +928,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
865
928
|
if (rafId) cancelAnimationFrame(rafId);
|
|
866
929
|
if (stopTimer) clearTimeout(stopTimer);
|
|
867
930
|
mutationObserver?.disconnect();
|
|
931
|
+
interactionAbort?.abort();
|
|
868
932
|
|
|
869
933
|
const msg = `Stopped. ${rawFrames.length} raw frames, ${mutations.length} mutation events.`;
|
|
870
934
|
|
|
@@ -921,7 +985,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
921
985
|
},
|
|
922
986
|
|
|
923
987
|
getRaw() {
|
|
924
|
-
return { rawFrames, mutations };
|
|
988
|
+
return { rawFrames, mutations, interactions };
|
|
925
989
|
},
|
|
926
990
|
|
|
927
991
|
toJSON() {
|