@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Targets } from "@locator/shared";
|
|
2
|
-
import { createEffect, createSignal
|
|
2
|
+
import { createEffect, createSignal } from "solid-js";
|
|
3
3
|
import { render } from "solid-js/web";
|
|
4
4
|
import { AdapterId } from "../consts";
|
|
5
5
|
import { isCombinationModifiersPressed } from "../functions/isCombinationModifiersPressed";
|
|
@@ -7,22 +7,17 @@ import { Targets as SetupTargets } from "../types/types";
|
|
|
7
7
|
import { MaybeOutline } from "./MaybeOutline";
|
|
8
8
|
import { isLocatorsOwnElement } from "../functions/isLocatorsOwnElement";
|
|
9
9
|
import { Toast } from "./Toast";
|
|
10
|
-
import { collectAncestry, formatAncestryChain, truncateAtFirstFile } from "../functions/formatAncestryChain";
|
|
10
|
+
import { collectAncestry, formatAncestryChain, truncateAtFirstFile, getElementLabel } from "../functions/formatAncestryChain";
|
|
11
11
|
import { enrichAncestryWithSourceMaps } from "../functions/enrichAncestrySourceMaps";
|
|
12
|
+
import { extractComputedStyles } from "../functions/extractComputedStyles";
|
|
12
13
|
import { createTreeNode } from "../adapters/createTreeNode";
|
|
13
|
-
import treeIconUrl from "../_generated_tree_icon";
|
|
14
|
-
import { createDejitterRecorder, DejitterAPI, DejitterFinding, DejitterSummary } from "../dejitter/recorder";
|
|
15
14
|
import { RecordingOutline } from "./RecordingOutline";
|
|
16
|
-
import { RecordingResults
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
maxDuration: 30000,
|
|
23
|
-
idleTimeout: 0,
|
|
24
|
-
mutations: true,
|
|
25
|
-
};
|
|
15
|
+
import { RecordingResults } from "./RecordingResults";
|
|
16
|
+
import { RecordingPillButton } from "./RecordingPillButton";
|
|
17
|
+
import { SettingsPanel } from "./SettingsPanel";
|
|
18
|
+
import { useRecordingState } from "../hooks/useRecordingState";
|
|
19
|
+
import { useEventListeners } from "../hooks/useEventListeners";
|
|
20
|
+
import { settings } from "../hooks/useSettings";
|
|
26
21
|
|
|
27
22
|
type RuntimeProps = {
|
|
28
23
|
adapterId?: AdapterId;
|
|
@@ -37,59 +32,15 @@ function Runtime(props: RuntimeProps) {
|
|
|
37
32
|
);
|
|
38
33
|
const [toastMessage, setToastMessage] = createSignal<string | null>(null);
|
|
39
34
|
const [locatorActive, setLocatorActive] = createSignal<boolean>(false);
|
|
35
|
+
const [settingsOpen, setSettingsOpen] = createSignal<boolean>(false);
|
|
40
36
|
|
|
41
|
-
|
|
42
|
-
type RecordingState = 'idle' | 'selecting' | 'recording' | 'results';
|
|
43
|
-
|
|
44
|
-
// --- localStorage persistence ---
|
|
45
|
-
const STORAGE_KEY = '__treelocator_recording__';
|
|
46
|
-
|
|
47
|
-
type SavedRecording = {
|
|
48
|
-
findings: DejitterFinding[];
|
|
49
|
-
summary: DejitterSummary | null;
|
|
50
|
-
data: any;
|
|
51
|
-
elementPath: string;
|
|
52
|
-
interactions: InteractionEvent[];
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
function loadFromStorage(): { last: SavedRecording | null; previous: SavedRecording | null } {
|
|
56
|
-
try {
|
|
57
|
-
const raw = localStorage.getItem(STORAGE_KEY);
|
|
58
|
-
if (raw) return JSON.parse(raw);
|
|
59
|
-
} catch {}
|
|
60
|
-
return { last: null, previous: null };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function saveToStorage(current: SavedRecording) {
|
|
64
|
-
try {
|
|
65
|
-
const stored = loadFromStorage();
|
|
66
|
-
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
|
67
|
-
last: current,
|
|
68
|
-
previous: stored.last,
|
|
69
|
-
}));
|
|
70
|
-
} catch {}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Restore last results on mount
|
|
74
|
-
const restored = loadFromStorage();
|
|
75
|
-
const restoredLast = restored.last;
|
|
76
|
-
|
|
77
|
-
const [recordingState, setRecordingState] = createSignal<RecordingState>(restoredLast ? 'results' : 'idle');
|
|
78
|
-
const [recordedElement, setRecordedElement] = createSignal<HTMLElement | null>(null);
|
|
79
|
-
const [recordingFindings, setRecordingFindings] = createSignal<DejitterFinding[]>(restoredLast?.findings ?? []);
|
|
80
|
-
const [recordingSummary, setRecordingSummary] = createSignal<DejitterSummary | null>(restoredLast?.summary ?? null);
|
|
81
|
-
const [interactionLog, setInteractionLog] = createSignal<InteractionEvent[]>(restoredLast?.interactions ?? []);
|
|
82
|
-
const [recordingData, setRecordingData] = createSignal<any>(restoredLast?.data ?? null);
|
|
83
|
-
const [recordingElementPath, setRecordingElementPath] = createSignal<string>(restoredLast?.elementPath ?? "");
|
|
84
|
-
const [replayBox, setReplayBox] = createSignal<{ x: number; y: number; w: number; h: number } | null>(null);
|
|
85
|
-
const [replaying, setReplaying] = createSignal(false);
|
|
86
|
-
const [viewingPrevious, setViewingPrevious] = createSignal(false);
|
|
87
|
-
let dejitterInstance: DejitterAPI | null = null;
|
|
88
|
-
let interactionClickHandler: ((e: MouseEvent) => void) | null = null;
|
|
89
|
-
let recordingStartTime = 0;
|
|
90
|
-
let replayTimerId: number | null = null;
|
|
37
|
+
const recording = useRecordingState(props.adapterId);
|
|
91
38
|
|
|
92
|
-
const isActive = () =>
|
|
39
|
+
const isActive = () =>
|
|
40
|
+
(holdingModKey() ||
|
|
41
|
+
locatorActive() ||
|
|
42
|
+
recording.recordingState() === "selecting") &&
|
|
43
|
+
currentElement();
|
|
93
44
|
|
|
94
45
|
createEffect(() => {
|
|
95
46
|
if (isActive()) {
|
|
@@ -101,25 +52,24 @@ function Runtime(props: RuntimeProps) {
|
|
|
101
52
|
|
|
102
53
|
// Expose replay functions on the browser API
|
|
103
54
|
if (typeof window !== "undefined" && (window as any).__treelocator__) {
|
|
104
|
-
(window as any).__treelocator__.replay = () => replayRecording();
|
|
105
|
-
(window as any).__treelocator__.replayWithRecord = (
|
|
106
|
-
|
|
55
|
+
(window as any).__treelocator__.replay = () => recording.replayRecording();
|
|
56
|
+
(window as any).__treelocator__.replayWithRecord = (
|
|
57
|
+
elementOrSelector: HTMLElement | string
|
|
58
|
+
) => recording.replayWithRecord(elementOrSelector);
|
|
107
59
|
}
|
|
108
60
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
setHoldingModKey(e.altKey);
|
|
122
|
-
setHoldingShift(e.shiftKey);
|
|
61
|
+
// --- Event handlers ---
|
|
62
|
+
|
|
63
|
+
function eventPathHasAttribute(e: Event, attr: string): boolean {
|
|
64
|
+
const path =
|
|
65
|
+
typeof e.composedPath === "function"
|
|
66
|
+
? e.composedPath()
|
|
67
|
+
: e.target
|
|
68
|
+
? [e.target]
|
|
69
|
+
: [];
|
|
70
|
+
return path.some(
|
|
71
|
+
(node) => node instanceof Element && node.hasAttribute(attr)
|
|
72
|
+
);
|
|
123
73
|
}
|
|
124
74
|
|
|
125
75
|
function findElementAtPoint(e: MouseEvent): HTMLElement | null {
|
|
@@ -127,356 +77,52 @@ function Runtime(props: RuntimeProps) {
|
|
|
127
77
|
for (const el of elementsAtPoint) {
|
|
128
78
|
if (isLocatorsOwnElement(el as HTMLElement)) continue;
|
|
129
79
|
if (el instanceof HTMLElement || el instanceof SVGElement) {
|
|
130
|
-
const withLocator = el.closest(
|
|
80
|
+
const withLocator = el.closest(
|
|
81
|
+
"[data-locatorjs-id], [data-locatorjs]"
|
|
82
|
+
);
|
|
131
83
|
if (withLocator) return withLocator as HTMLElement;
|
|
132
84
|
}
|
|
133
85
|
}
|
|
134
|
-
// Fallback to e.target
|
|
135
86
|
const target = e.target;
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
87
|
+
if (
|
|
88
|
+
target &&
|
|
89
|
+
(target instanceof HTMLElement || target instanceof SVGElement)
|
|
90
|
+
) {
|
|
91
|
+
const el =
|
|
92
|
+
target instanceof SVGElement
|
|
93
|
+
? ((target.closest(
|
|
94
|
+
"[data-locatorjs-id], [data-locatorjs]"
|
|
95
|
+
) as HTMLElement | null) ??
|
|
96
|
+
(target.closest("svg") as HTMLElement | null) ??
|
|
97
|
+
(target as unknown as HTMLElement))
|
|
98
|
+
: target;
|
|
142
99
|
if (el && !isLocatorsOwnElement(el)) return el;
|
|
143
100
|
}
|
|
144
101
|
return null;
|
|
145
102
|
}
|
|
146
103
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
case 'selecting':
|
|
155
|
-
setRecordingState('idle');
|
|
156
|
-
break;
|
|
157
|
-
case 'recording':
|
|
158
|
-
stopRecording();
|
|
159
|
-
break;
|
|
160
|
-
case 'results':
|
|
161
|
-
dismissResults();
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function startRecording(element: HTMLElement) {
|
|
167
|
-
element.setAttribute('data-treelocator-recording', 'true');
|
|
168
|
-
setRecordedElement(element);
|
|
169
|
-
|
|
170
|
-
dejitterInstance = createDejitterRecorder();
|
|
171
|
-
dejitterInstance.configure(DEJITTER_CONFIG);
|
|
172
|
-
dejitterInstance.start();
|
|
173
|
-
startInteractionTracker();
|
|
174
|
-
setRecordingState('recording');
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function stopRecording() {
|
|
178
|
-
if (!dejitterInstance) return;
|
|
179
|
-
dejitterInstance.stop();
|
|
180
|
-
const findings = dejitterInstance.findings(true) as DejitterFinding[];
|
|
181
|
-
const summary = dejitterInstance.summary(true) as DejitterSummary;
|
|
182
|
-
const data = dejitterInstance.getData();
|
|
183
|
-
|
|
184
|
-
// Collect ancestry path from treelocator before clearing
|
|
185
|
-
const el = recordedElement();
|
|
186
|
-
let elementPath = "";
|
|
187
|
-
if (el) {
|
|
188
|
-
const treeNode = createTreeNode(el, props.adapterId);
|
|
189
|
-
if (treeNode) {
|
|
190
|
-
const ancestry = collectAncestry(treeNode);
|
|
191
|
-
elementPath = formatAncestryChain(ancestry);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
setRecordingFindings(findings);
|
|
196
|
-
setRecordingSummary(summary);
|
|
197
|
-
setRecordingData(data);
|
|
198
|
-
setRecordingElementPath(elementPath);
|
|
199
|
-
stopInteractionTracker();
|
|
200
|
-
|
|
201
|
-
// Persist to localStorage (moves previous "last" to "previous")
|
|
202
|
-
saveToStorage({
|
|
203
|
-
findings,
|
|
204
|
-
summary,
|
|
205
|
-
data,
|
|
206
|
-
elementPath,
|
|
207
|
-
interactions: interactionLog(),
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
el?.removeAttribute('data-treelocator-recording');
|
|
211
|
-
setRecordingState('results');
|
|
212
|
-
dejitterInstance = null;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function replayRecording() {
|
|
216
|
-
const events = interactionLog();
|
|
217
|
-
if (events.length === 0) return;
|
|
218
|
-
|
|
219
|
-
stopReplay();
|
|
220
|
-
setReplaying(true);
|
|
221
|
-
|
|
222
|
-
let eventIdx = 0;
|
|
223
|
-
|
|
224
|
-
function scheduleNext() {
|
|
225
|
-
if (eventIdx >= events.length) {
|
|
226
|
-
stopReplay();
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const evt = events[eventIdx]!;
|
|
231
|
-
const delay = eventIdx === 0 ? 100 : evt.t - events[eventIdx - 1]!.t;
|
|
232
|
-
|
|
233
|
-
replayTimerId = window.setTimeout(() => {
|
|
234
|
-
// Show click indicator
|
|
235
|
-
setReplayBox({ x: evt.x - 12, y: evt.y - 12, w: 24, h: 24 });
|
|
236
|
-
|
|
237
|
-
// Dispatch a real click at the recorded position
|
|
238
|
-
const target = document.elementFromPoint(evt.x, evt.y);
|
|
239
|
-
if (target) {
|
|
240
|
-
target.dispatchEvent(new MouseEvent('click', {
|
|
241
|
-
bubbles: true,
|
|
242
|
-
cancelable: true,
|
|
243
|
-
clientX: evt.x,
|
|
244
|
-
clientY: evt.y,
|
|
245
|
-
view: window,
|
|
246
|
-
}));
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Clear click indicator after a short flash
|
|
250
|
-
window.setTimeout(() => setReplayBox(null), 200);
|
|
251
|
-
|
|
252
|
-
eventIdx++;
|
|
253
|
-
scheduleNext();
|
|
254
|
-
}, Math.max(delay, 50));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
scheduleNext();
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function stopReplay() {
|
|
261
|
-
if (replayTimerId) {
|
|
262
|
-
clearTimeout(replayTimerId);
|
|
263
|
-
replayTimerId = null;
|
|
264
|
-
}
|
|
265
|
-
setReplaying(false);
|
|
266
|
-
setReplayBox(null);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function replayWithRecord(elementOrSelector: HTMLElement | string): Promise<{
|
|
270
|
-
path: string;
|
|
271
|
-
findings: DejitterFinding[];
|
|
272
|
-
summary: DejitterSummary | null;
|
|
273
|
-
data: any;
|
|
274
|
-
interactions: InteractionEvent[];
|
|
275
|
-
} | null> {
|
|
276
|
-
// Resolve element
|
|
277
|
-
let element: HTMLElement | null;
|
|
278
|
-
if (typeof elementOrSelector === 'string') {
|
|
279
|
-
const found = document.querySelector(elementOrSelector);
|
|
280
|
-
element = found instanceof HTMLElement ? found : null;
|
|
281
|
-
} else {
|
|
282
|
-
element = elementOrSelector;
|
|
283
|
-
}
|
|
284
|
-
if (!element) return Promise.resolve(null);
|
|
285
|
-
|
|
286
|
-
// Get stored interactions to replay
|
|
287
|
-
const stored = loadFromStorage();
|
|
288
|
-
const events = stored.last?.interactions ?? interactionLog();
|
|
289
|
-
if (events.length === 0) return Promise.resolve(null);
|
|
290
|
-
|
|
291
|
-
return new Promise((resolve) => {
|
|
292
|
-
// Start recording on the element
|
|
293
|
-
element!.setAttribute('data-treelocator-recording', 'true');
|
|
294
|
-
setRecordedElement(element);
|
|
295
|
-
|
|
296
|
-
dejitterInstance = createDejitterRecorder();
|
|
297
|
-
dejitterInstance.configure(DEJITTER_CONFIG);
|
|
298
|
-
dejitterInstance.start();
|
|
299
|
-
setRecordingState('recording');
|
|
300
|
-
setReplaying(true);
|
|
301
|
-
|
|
302
|
-
let eventIdx = 0;
|
|
303
|
-
|
|
304
|
-
function finishRecording() {
|
|
305
|
-
setReplaying(false);
|
|
306
|
-
setReplayBox(null);
|
|
307
|
-
|
|
308
|
-
if (!dejitterInstance) { resolve(null); return; }
|
|
309
|
-
dejitterInstance.stop();
|
|
310
|
-
const findings = dejitterInstance.findings(true) as DejitterFinding[];
|
|
311
|
-
const summary = dejitterInstance.summary(true) as DejitterSummary;
|
|
312
|
-
const data = dejitterInstance.getData();
|
|
313
|
-
|
|
314
|
-
const el = recordedElement();
|
|
315
|
-
let elementPath = "";
|
|
316
|
-
if (el) {
|
|
317
|
-
const treeNode = createTreeNode(el, props.adapterId);
|
|
318
|
-
if (treeNode) {
|
|
319
|
-
const ancestry = collectAncestry(treeNode);
|
|
320
|
-
elementPath = formatAncestryChain(ancestry);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
setRecordingFindings(findings);
|
|
325
|
-
setRecordingSummary(summary);
|
|
326
|
-
setRecordingData(data);
|
|
327
|
-
setRecordingElementPath(elementPath);
|
|
328
|
-
setInteractionLog(events);
|
|
329
|
-
|
|
330
|
-
saveToStorage({ findings, summary, data, elementPath, interactions: events });
|
|
331
|
-
|
|
332
|
-
el?.removeAttribute('data-treelocator-recording');
|
|
333
|
-
setRecordingState('results');
|
|
334
|
-
dejitterInstance = null;
|
|
335
|
-
|
|
336
|
-
resolve({ path: elementPath, findings, summary, data, interactions: events });
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function scheduleNext() {
|
|
340
|
-
if (eventIdx >= events.length) {
|
|
341
|
-
// Wait for CSS transitions to settle before stopping recording
|
|
342
|
-
replayTimerId = window.setTimeout(finishRecording, 500);
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const evt = events[eventIdx]!;
|
|
347
|
-
const delay = eventIdx === 0 ? 100 : evt.t - events[eventIdx - 1]!.t;
|
|
348
|
-
|
|
349
|
-
replayTimerId = window.setTimeout(() => {
|
|
350
|
-
setReplayBox({ x: evt.x - 12, y: evt.y - 12, w: 24, h: 24 });
|
|
351
|
-
|
|
352
|
-
const target = document.elementFromPoint(evt.x, evt.y);
|
|
353
|
-
if (target) {
|
|
354
|
-
target.dispatchEvent(new MouseEvent('click', {
|
|
355
|
-
bubbles: true,
|
|
356
|
-
cancelable: true,
|
|
357
|
-
clientX: evt.x,
|
|
358
|
-
clientY: evt.y,
|
|
359
|
-
view: window,
|
|
360
|
-
}));
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
window.setTimeout(() => setReplayBox(null), 200);
|
|
364
|
-
|
|
365
|
-
eventIdx++;
|
|
366
|
-
scheduleNext();
|
|
367
|
-
}, Math.max(delay, 50));
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
scheduleNext();
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function dismissResults() {
|
|
375
|
-
stopReplay();
|
|
376
|
-
setRecordingFindings([]);
|
|
377
|
-
setRecordingSummary(null);
|
|
378
|
-
setRecordingData(null);
|
|
379
|
-
setRecordingElementPath("");
|
|
380
|
-
setInteractionLog([]);
|
|
381
|
-
setRecordedElement(null);
|
|
382
|
-
setViewingPrevious(false);
|
|
383
|
-
setRecordingState('idle');
|
|
384
|
-
try { localStorage.removeItem(STORAGE_KEY); } catch {}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function hasPreviousRecording(): boolean {
|
|
388
|
-
return loadFromStorage().previous !== null;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function loadPreviousRecording() {
|
|
392
|
-
const stored = loadFromStorage();
|
|
393
|
-
if (!stored.previous) return;
|
|
394
|
-
const prev = stored.previous;
|
|
395
|
-
setRecordingFindings(prev.findings);
|
|
396
|
-
setRecordingSummary(prev.summary);
|
|
397
|
-
setRecordingData(prev.data);
|
|
398
|
-
setRecordingElementPath(prev.elementPath);
|
|
399
|
-
setInteractionLog(prev.interactions);
|
|
400
|
-
setViewingPrevious(true);
|
|
401
|
-
setRecordingState('results');
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function loadLatestRecording() {
|
|
405
|
-
const stored = loadFromStorage();
|
|
406
|
-
if (!stored.last) return;
|
|
407
|
-
const last = stored.last;
|
|
408
|
-
setRecordingFindings(last.findings);
|
|
409
|
-
setRecordingSummary(last.summary);
|
|
410
|
-
setRecordingData(last.data);
|
|
411
|
-
setRecordingElementPath(last.elementPath);
|
|
412
|
-
setInteractionLog(last.interactions);
|
|
413
|
-
setViewingPrevious(false);
|
|
414
|
-
setRecordingState('results');
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function startInteractionTracker() {
|
|
418
|
-
recordingStartTime = performance.now();
|
|
419
|
-
setInteractionLog([]);
|
|
420
|
-
interactionClickHandler = (e: MouseEvent) => {
|
|
421
|
-
if (isLocatorsOwnElement(e.target as HTMLElement)) return;
|
|
422
|
-
const el = e.target as HTMLElement;
|
|
423
|
-
const tag = el.tagName?.toLowerCase() || 'unknown';
|
|
424
|
-
const id = el.id ? '#' + el.id : '';
|
|
425
|
-
const cls = el.className && typeof el.className === 'string' ? '.' + el.className.split(' ')[0] : '';
|
|
426
|
-
setInteractionLog((prev) => [...prev, {
|
|
427
|
-
t: Math.round(performance.now() - recordingStartTime),
|
|
428
|
-
type: 'click',
|
|
429
|
-
target: `${tag}${id}${cls}`,
|
|
430
|
-
x: e.clientX,
|
|
431
|
-
y: e.clientY,
|
|
432
|
-
}]);
|
|
433
|
-
};
|
|
434
|
-
document.addEventListener('click', interactionClickHandler, { capture: true });
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function stopInteractionTracker() {
|
|
438
|
-
if (interactionClickHandler) {
|
|
439
|
-
document.removeEventListener('click', interactionClickHandler, { capture: true });
|
|
440
|
-
interactionClickHandler = null;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function mouseOverListener(e: MouseEvent) {
|
|
445
|
-
setHoldingModKey(e.altKey);
|
|
446
|
-
setHoldingShift(e.shiftKey);
|
|
447
|
-
|
|
448
|
-
// Don't update hovered element while recording -- highlight is sticky
|
|
449
|
-
if (recordingState() === 'recording') return;
|
|
450
|
-
|
|
451
|
-
const element = findElementAtPoint(e);
|
|
452
|
-
if (element) {
|
|
453
|
-
setCurrentElement(element);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function mouseDownUpListener(e: MouseEvent) {
|
|
458
|
-
setHoldingModKey(e.altKey);
|
|
459
|
-
|
|
460
|
-
if (e.altKey || locatorActive() || recordingState() === 'selecting') {
|
|
461
|
-
e.preventDefault();
|
|
462
|
-
e.stopPropagation();
|
|
104
|
+
function clickListener(e: MouseEvent) {
|
|
105
|
+
if (
|
|
106
|
+
settingsOpen() &&
|
|
107
|
+
!eventPathHasAttribute(e, "data-treelocator-settings-panel") &&
|
|
108
|
+
!eventPathHasAttribute(e, "data-treelocator-settings-toggle")
|
|
109
|
+
) {
|
|
110
|
+
setSettingsOpen(false);
|
|
463
111
|
}
|
|
464
|
-
}
|
|
465
112
|
|
|
466
|
-
function clickListener(e: MouseEvent) {
|
|
467
113
|
// Handle recording element selection
|
|
468
|
-
if (recordingState() ===
|
|
114
|
+
if (recording.recordingState() === "selecting") {
|
|
469
115
|
e.preventDefault();
|
|
470
116
|
e.stopPropagation();
|
|
471
117
|
const element = findElementAtPoint(e);
|
|
472
118
|
if (element && !isLocatorsOwnElement(element)) {
|
|
473
|
-
startRecording(element);
|
|
119
|
+
recording.startRecording(element);
|
|
474
120
|
}
|
|
475
121
|
return;
|
|
476
122
|
}
|
|
477
123
|
|
|
478
124
|
// During recording, let clicks pass through (tracked by interaction logger)
|
|
479
|
-
if (recordingState() ===
|
|
125
|
+
if (recording.recordingState() === "recording") return;
|
|
480
126
|
|
|
481
127
|
if (!e.altKey && !isCombinationModifiersPressed(e) && !locatorActive()) {
|
|
482
128
|
return;
|
|
@@ -484,22 +130,14 @@ function Runtime(props: RuntimeProps) {
|
|
|
484
130
|
|
|
485
131
|
const element = findElementAtPoint(e);
|
|
486
132
|
|
|
487
|
-
if (!element)
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
if (element instanceof HTMLElement && element.shadowRoot) {
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
if (isLocatorsOwnElement(element as HTMLElement)) {
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
133
|
+
if (!element) return;
|
|
134
|
+
if (element instanceof HTMLElement && element.shadowRoot) return;
|
|
135
|
+
if (isLocatorsOwnElement(element as HTMLElement)) return;
|
|
498
136
|
|
|
499
137
|
e.preventDefault();
|
|
500
138
|
e.stopPropagation();
|
|
501
139
|
|
|
502
|
-
// Copy ancestry to clipboard on alt+click
|
|
140
|
+
// Copy ancestry + computed styles to clipboard on alt+click
|
|
503
141
|
const treeNode = createTreeNode(element as HTMLElement, props.adapterId);
|
|
504
142
|
if (treeNode) {
|
|
505
143
|
let ancestry = collectAncestry(treeNode);
|
|
@@ -509,18 +147,45 @@ function Runtime(props: RuntimeProps) {
|
|
|
509
147
|
ancestry = truncateAtFirstFile(ancestry);
|
|
510
148
|
}
|
|
511
149
|
|
|
150
|
+
// Extract computed styles for the clicked element (if enabled)
|
|
151
|
+
const stylesEnabled = settings().computedStyles;
|
|
152
|
+
const elementLabel = getElementLabel(ancestry);
|
|
153
|
+
const stylesResult = stylesEnabled
|
|
154
|
+
? extractComputedStyles(element as Element, elementLabel, {
|
|
155
|
+
includeDefaults: settings().computedStylesIncludeDefaults,
|
|
156
|
+
})
|
|
157
|
+
: null;
|
|
158
|
+
|
|
512
159
|
// Write immediately with component names (preserves user gesture for clipboard API)
|
|
513
160
|
const formatted = formatAncestryChain(ancestry);
|
|
514
|
-
|
|
161
|
+
const fullOutput = stylesResult
|
|
162
|
+
? formatted + "\n\n" + stylesResult.formatted
|
|
163
|
+
: formatted;
|
|
164
|
+
navigator.clipboard.writeText(fullOutput).then(() => {
|
|
515
165
|
setToastMessage("Copied to clipboard");
|
|
516
166
|
});
|
|
517
167
|
|
|
518
|
-
// For React 19+: try to enrich with source map file paths and re-copy
|
|
168
|
+
// For React 19+: try to enrich with source map file paths and re-copy.
|
|
169
|
+
// If the enriched label differs, re-extract with forceFull:true so the
|
|
170
|
+
// diff-mode fast path doesn't collapse the second extraction (for the
|
|
171
|
+
// same element within the diff window) into "No changes detected".
|
|
519
172
|
enrichAncestryWithSourceMaps(ancestry, element as HTMLElement).then(
|
|
520
173
|
(enriched) => {
|
|
521
174
|
const enrichedFormatted = formatAncestryChain(enriched);
|
|
522
175
|
if (enrichedFormatted !== formatted) {
|
|
523
|
-
|
|
176
|
+
let enrichedFull = enrichedFormatted;
|
|
177
|
+
if (stylesResult) {
|
|
178
|
+
const enrichedLabel = getElementLabel(enriched);
|
|
179
|
+
const enrichedStyles =
|
|
180
|
+
enrichedLabel !== elementLabel
|
|
181
|
+
? extractComputedStyles(element as Element, enrichedLabel, {
|
|
182
|
+
forceFull: true,
|
|
183
|
+
includeDefaults: settings().computedStylesIncludeDefaults,
|
|
184
|
+
}).formatted
|
|
185
|
+
: stylesResult.formatted;
|
|
186
|
+
enrichedFull = enrichedFormatted + "\n\n" + enrichedStyles;
|
|
187
|
+
}
|
|
188
|
+
navigator.clipboard.writeText(enrichedFull).then(() => {
|
|
524
189
|
setToastMessage("Copied to clipboard");
|
|
525
190
|
});
|
|
526
191
|
}
|
|
@@ -534,67 +199,60 @@ function Runtime(props: RuntimeProps) {
|
|
|
534
199
|
}
|
|
535
200
|
}
|
|
536
201
|
|
|
537
|
-
function
|
|
538
|
-
|
|
539
|
-
|
|
202
|
+
function mouseOverListener(e: MouseEvent) {
|
|
203
|
+
setHoldingModKey(e.altKey);
|
|
204
|
+
setHoldingShift(e.shiftKey);
|
|
540
205
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
if (node.id === "locatorjs-wrapper") {
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
if (node.shadowRoot) {
|
|
547
|
-
roots.push(node.shadowRoot);
|
|
548
|
-
}
|
|
549
|
-
});
|
|
206
|
+
// Don't update hovered element while recording — highlight is sticky
|
|
207
|
+
if (recording.recordingState() === "recording") return;
|
|
550
208
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
root.addEventListener("mousemove", mouseMoveListener as EventListener, {
|
|
556
|
-
capture: true,
|
|
557
|
-
});
|
|
558
|
-
root.addEventListener("keydown", keyDownListener as EventListener);
|
|
559
|
-
root.addEventListener("keyup", keyUpListener as EventListener);
|
|
560
|
-
root.addEventListener("click", clickListener as EventListener, {
|
|
561
|
-
capture: true,
|
|
562
|
-
});
|
|
563
|
-
root.addEventListener("mousedown", mouseDownUpListener as EventListener, {
|
|
564
|
-
capture: true,
|
|
565
|
-
});
|
|
566
|
-
root.addEventListener("mouseup", mouseDownUpListener as EventListener, {
|
|
567
|
-
capture: true,
|
|
568
|
-
});
|
|
569
|
-
root.addEventListener("scroll", scrollListener);
|
|
209
|
+
const element = findElementAtPoint(e);
|
|
210
|
+
if (element) {
|
|
211
|
+
setCurrentElement(element);
|
|
212
|
+
}
|
|
570
213
|
}
|
|
571
214
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
);
|
|
596
|
-
|
|
597
|
-
|
|
215
|
+
useEventListeners({
|
|
216
|
+
mouseOverListener,
|
|
217
|
+
mouseMoveListener(e: MouseEvent) {
|
|
218
|
+
setHoldingModKey(e.altKey);
|
|
219
|
+
setHoldingShift(e.shiftKey);
|
|
220
|
+
},
|
|
221
|
+
keyDownListener(e: KeyboardEvent) {
|
|
222
|
+
setHoldingModKey(isCombinationModifiersPressed(e, true));
|
|
223
|
+
setHoldingShift(e.shiftKey);
|
|
224
|
+
if (e.key === "Escape") {
|
|
225
|
+
const wasSelecting = recording.recordingState() === "selecting";
|
|
226
|
+
const wasLocatorActive = locatorActive();
|
|
227
|
+
if (wasSelecting || wasLocatorActive) {
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
e.stopPropagation();
|
|
230
|
+
if (wasLocatorActive) setLocatorActive(false);
|
|
231
|
+
if (wasSelecting) recording.handleRecordClick();
|
|
232
|
+
setCurrentElement(null);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
keyUpListener(e: KeyboardEvent) {
|
|
237
|
+
setHoldingModKey(isCombinationModifiersPressed(e));
|
|
238
|
+
setHoldingShift(e.shiftKey);
|
|
239
|
+
},
|
|
240
|
+
clickListener,
|
|
241
|
+
mouseDownUpListener(e: MouseEvent) {
|
|
242
|
+
setHoldingModKey(e.altKey);
|
|
243
|
+
if (
|
|
244
|
+
e.altKey ||
|
|
245
|
+
locatorActive() ||
|
|
246
|
+
recording.recordingState() === "selecting"
|
|
247
|
+
) {
|
|
248
|
+
e.preventDefault();
|
|
249
|
+
e.stopPropagation();
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
scrollListener() {
|
|
253
|
+
setCurrentElement(null);
|
|
254
|
+
},
|
|
255
|
+
onCleanup: recording.cleanup,
|
|
598
256
|
});
|
|
599
257
|
|
|
600
258
|
return (
|
|
@@ -607,18 +265,19 @@ function Runtime(props: RuntimeProps) {
|
|
|
607
265
|
dashed={holdingShift()}
|
|
608
266
|
/>
|
|
609
267
|
) : null}
|
|
610
|
-
{recordingState() ===
|
|
611
|
-
|
|
268
|
+
{recording.recordingState() === "recording" &&
|
|
269
|
+
recording.recordedElement() ? (
|
|
270
|
+
<RecordingOutline element={recording.recordedElement()!} />
|
|
612
271
|
) : null}
|
|
613
|
-
{replayBox() ? (
|
|
272
|
+
{recording.replayBox() ? (
|
|
614
273
|
<div
|
|
615
274
|
style={{
|
|
616
275
|
position: "fixed",
|
|
617
276
|
"z-index": "2147483645",
|
|
618
|
-
left: replayBox()!.x + "px",
|
|
619
|
-
top: replayBox()!.y + "px",
|
|
620
|
-
width: replayBox()!.w + "px",
|
|
621
|
-
height: replayBox()!.h + "px",
|
|
277
|
+
left: recording.replayBox()!.x + "px",
|
|
278
|
+
top: recording.replayBox()!.y + "px",
|
|
279
|
+
width: recording.replayBox()!.w + "px",
|
|
280
|
+
height: recording.replayBox()!.h + "px",
|
|
622
281
|
"border-radius": "50%",
|
|
623
282
|
"pointer-events": "none",
|
|
624
283
|
background: "rgba(59, 130, 246, 0.4)",
|
|
@@ -627,147 +286,43 @@ function Runtime(props: RuntimeProps) {
|
|
|
627
286
|
}}
|
|
628
287
|
/>
|
|
629
288
|
) : null}
|
|
630
|
-
{recordingState() ===
|
|
289
|
+
{recording.recordingState() === "results" ? (
|
|
631
290
|
<RecordingResults
|
|
632
|
-
findings={recordingFindings()}
|
|
633
|
-
summary={recordingSummary()}
|
|
634
|
-
data={recordingData()}
|
|
635
|
-
elementPath={recordingElementPath()}
|
|
636
|
-
interactions={interactionLog()}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
291
|
+
findings={recording.recordingFindings()}
|
|
292
|
+
summary={recording.recordingSummary()}
|
|
293
|
+
data={recording.recordingData()}
|
|
294
|
+
elementPath={recording.recordingElementPath()}
|
|
295
|
+
interactions={recording.interactionLog()}
|
|
296
|
+
visualDiff={recording.visualDiff()}
|
|
297
|
+
onDismiss={recording.dismissResults}
|
|
298
|
+
onReplay={recording.replayRecording}
|
|
299
|
+
replaying={recording.replaying()}
|
|
640
300
|
onToast={setToastMessage}
|
|
641
|
-
hasPrevious={
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
301
|
+
hasPrevious={
|
|
302
|
+
!recording.viewingPrevious() && recording.hasPreviousRecording()
|
|
303
|
+
}
|
|
304
|
+
onLoadPrevious={recording.loadPreviousRecording}
|
|
305
|
+
hasNext={recording.viewingPrevious()}
|
|
306
|
+
onLoadNext={recording.loadLatestRecording}
|
|
645
307
|
/>
|
|
646
308
|
) : null}
|
|
309
|
+
{settingsOpen() ? (
|
|
310
|
+
<SettingsPanel onDismiss={() => setSettingsOpen(false)} />
|
|
311
|
+
) : null}
|
|
647
312
|
{toastMessage() && (
|
|
648
313
|
<Toast
|
|
649
314
|
message={toastMessage()!}
|
|
650
315
|
onClose={() => setToastMessage(null)}
|
|
651
316
|
/>
|
|
652
317
|
)}
|
|
653
|
-
<
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
@keyframes treelocator-rec-pulse {
|
|
662
|
-
0%, 100% { opacity: 1; }
|
|
663
|
-
50% { opacity: 0.3; }
|
|
664
|
-
}
|
|
665
|
-
`}</style>
|
|
666
|
-
{/* Combined pill button: tree (left) | record (right) */}
|
|
667
|
-
<div
|
|
668
|
-
style={{
|
|
669
|
-
display: "flex",
|
|
670
|
-
"align-items": "stretch",
|
|
671
|
-
"border-radius": "27px",
|
|
672
|
-
overflow: "hidden",
|
|
673
|
-
"box-shadow":
|
|
674
|
-
locatorActive()
|
|
675
|
-
? "0 0 0 3px #3b82f6, 0 4px 14px rgba(0, 0, 0, 0.25)"
|
|
676
|
-
: recordingState() === 'selecting'
|
|
677
|
-
? "0 0 0 3px #3b82f6, 0 4px 14px rgba(0, 0, 0, 0.25)"
|
|
678
|
-
: recordingState() === 'recording'
|
|
679
|
-
? "0 0 0 3px #ef4444, 0 4px 14px rgba(0, 0, 0, 0.25)"
|
|
680
|
-
: "0 4px 14px rgba(0, 0, 0, 0.25)",
|
|
681
|
-
transition: "box-shadow 0.2s ease-in-out",
|
|
682
|
-
}}
|
|
683
|
-
>
|
|
684
|
-
{/* Left half: Tree icon */}
|
|
685
|
-
<div
|
|
686
|
-
style={{
|
|
687
|
-
width: "54px",
|
|
688
|
-
height: "54px",
|
|
689
|
-
background: "#ffffff",
|
|
690
|
-
display: "flex",
|
|
691
|
-
"align-items": "center",
|
|
692
|
-
"justify-content": "center",
|
|
693
|
-
cursor: "pointer",
|
|
694
|
-
overflow: "hidden",
|
|
695
|
-
"border-right": "1px solid rgba(0, 0, 0, 0.1)",
|
|
696
|
-
transition: "background 0.15s ease-in-out",
|
|
697
|
-
}}
|
|
698
|
-
onMouseEnter={(e) => e.currentTarget.style.background = "#f0f0f0"}
|
|
699
|
-
onMouseLeave={(e) => e.currentTarget.style.background = "#ffffff"}
|
|
700
|
-
onClick={() => setLocatorActive(!locatorActive())}
|
|
701
|
-
aria-label="TreeLocatorJS: Get component paths using window.__treelocator__.getPath(selector)"
|
|
702
|
-
role="button"
|
|
703
|
-
>
|
|
704
|
-
<img
|
|
705
|
-
src={treeIconUrl}
|
|
706
|
-
alt="TreeLocatorJS"
|
|
707
|
-
width={44}
|
|
708
|
-
height={44}
|
|
709
|
-
/>
|
|
710
|
-
</div>
|
|
711
|
-
{/* Right half: Record button */}
|
|
712
|
-
<div
|
|
713
|
-
style={{
|
|
714
|
-
width: "54px",
|
|
715
|
-
height: "54px",
|
|
716
|
-
background: recordingState() === 'recording' ? "#ef4444" : "#ffffff",
|
|
717
|
-
display: "flex",
|
|
718
|
-
"align-items": "center",
|
|
719
|
-
"justify-content": "center",
|
|
720
|
-
cursor: "pointer",
|
|
721
|
-
transition: "background 0.15s ease-in-out",
|
|
722
|
-
}}
|
|
723
|
-
onMouseEnter={(e) => {
|
|
724
|
-
if (recordingState() !== 'recording') e.currentTarget.style.background = "#f0f0f0";
|
|
725
|
-
}}
|
|
726
|
-
onMouseLeave={(e) => {
|
|
727
|
-
if (recordingState() !== 'recording') e.currentTarget.style.background = "#ffffff";
|
|
728
|
-
}}
|
|
729
|
-
onClick={handleRecordClick}
|
|
730
|
-
aria-label={
|
|
731
|
-
recordingState() === 'idle' ? "Record element changes. API: window.__treelocator__.replayWithRecord(selector)" :
|
|
732
|
-
recordingState() === 'selecting' ? "Cancel recording selection" :
|
|
733
|
-
recordingState() === 'recording' ? "Stop recording" :
|
|
734
|
-
"Dismiss results"
|
|
735
|
-
}
|
|
736
|
-
role="button"
|
|
737
|
-
>
|
|
738
|
-
{recordingState() === 'recording' ? (
|
|
739
|
-
<div style={{ width: "18px", height: "18px", background: "#fff", "border-radius": "3px" }} />
|
|
740
|
-
) : (
|
|
741
|
-
<div
|
|
742
|
-
style={{
|
|
743
|
-
width: "18px",
|
|
744
|
-
height: "18px",
|
|
745
|
-
background: "#ef4444",
|
|
746
|
-
"border-radius": "50%",
|
|
747
|
-
animation: recordingState() === 'selecting' ? "treelocator-rec-pulse 1s ease-in-out infinite" : "none",
|
|
748
|
-
}}
|
|
749
|
-
/>
|
|
750
|
-
)}
|
|
751
|
-
</div>
|
|
752
|
-
</div>
|
|
753
|
-
<div
|
|
754
|
-
role="note"
|
|
755
|
-
aria-label="TreeLocatorJS Developer Tool API — Run: window.__treelocator__.help() for full usage instructions. Quick start: window.__treelocator__.getPath('button.submit') returns component ancestry. window.__treelocator__.replayWithRecord('.my-element') replays recorded interactions while tracking visual changes."
|
|
756
|
-
style={{
|
|
757
|
-
position: "absolute",
|
|
758
|
-
width: "1px",
|
|
759
|
-
height: "1px",
|
|
760
|
-
padding: "0",
|
|
761
|
-
margin: "-1px",
|
|
762
|
-
overflow: "hidden",
|
|
763
|
-
clip: "rect(0,0,0,0)",
|
|
764
|
-
"white-space": "nowrap",
|
|
765
|
-
border: "0",
|
|
766
|
-
}}
|
|
767
|
-
>
|
|
768
|
-
TreeLocatorJS: Run window.__treelocator__.help() for API docs
|
|
769
|
-
</div>
|
|
770
|
-
</div>
|
|
318
|
+
<RecordingPillButton
|
|
319
|
+
locatorActive={locatorActive()}
|
|
320
|
+
recordingState={recording.recordingState()}
|
|
321
|
+
settingsOpen={settingsOpen()}
|
|
322
|
+
onLocatorToggle={() => setLocatorActive(!locatorActive())}
|
|
323
|
+
onRecordClick={recording.handleRecordClick}
|
|
324
|
+
onSettingsClick={() => setSettingsOpen(!settingsOpen())}
|
|
325
|
+
/>
|
|
771
326
|
</>
|
|
772
327
|
);
|
|
773
328
|
}
|