@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.
Files changed (166) hide show
  1. package/.eslintignore +1 -0
  2. package/dist/_generated_styles.d.ts +1 -1
  3. package/dist/_generated_styles.js +20 -0
  4. package/dist/_generated_tree_icon.d.ts +1 -1
  5. package/dist/adapters/HtmlElementTreeNode.d.ts +2 -2
  6. package/dist/adapters/HtmlElementTreeNode.js +4 -6
  7. package/dist/adapters/createTreeNode.js +17 -44
  8. package/dist/adapters/detectFramework.d.ts +8 -0
  9. package/dist/adapters/detectFramework.js +25 -0
  10. package/dist/adapters/detectFramework.test.d.ts +1 -0
  11. package/dist/adapters/detectFramework.test.js +60 -0
  12. package/dist/adapters/jsx/jsxAdapter.js +54 -89
  13. package/dist/adapters/jsx/jsxAdapter.test.d.ts +1 -0
  14. package/dist/adapters/jsx/jsxAdapter.test.js +273 -0
  15. package/dist/adapters/nextjs/parseNextjsDataAttributes.js +1 -1
  16. package/dist/adapters/nextjs/parseNextjsDataAttributes.test.d.ts +1 -0
  17. package/dist/adapters/nextjs/parseNextjsDataAttributes.test.js +158 -0
  18. package/dist/adapters/react/findFiberByHtmlElement.d.ts +1 -1
  19. package/dist/adapters/react/findFiberByHtmlElement.js +1 -1
  20. package/dist/adapters/react/getAllParentsElementsAndRootComponent.js +4 -0
  21. package/dist/adapters/resolveAdapter.d.ts +1 -1
  22. package/dist/adapters/resolveAdapter.js +4 -8
  23. package/dist/adapters/svelte/svelteAdapter.test.d.ts +1 -0
  24. package/dist/adapters/svelte/svelteAdapter.test.js +280 -0
  25. package/dist/adapters/vue/vueAdapter.test.d.ts +1 -0
  26. package/dist/adapters/vue/vueAdapter.test.js +222 -0
  27. package/dist/browserApi.d.ts +148 -0
  28. package/dist/browserApi.js +146 -5
  29. package/dist/browserApi.test.d.ts +1 -0
  30. package/dist/browserApi.test.js +287 -0
  31. package/dist/components/RecordingPillButton.d.ts +11 -0
  32. package/dist/components/RecordingPillButton.js +202 -0
  33. package/dist/components/RecordingResults.d.ts +2 -0
  34. package/dist/components/RecordingResults.js +213 -78
  35. package/dist/components/Runtime.js +161 -554
  36. package/dist/components/SettingsPanel.d.ts +5 -0
  37. package/dist/components/SettingsPanel.js +312 -0
  38. package/dist/consoleCapture.d.ts +9 -0
  39. package/dist/consoleCapture.js +95 -0
  40. package/dist/dejitter/recorder.d.ts +7 -1
  41. package/dist/dejitter/recorder.js +64 -1
  42. package/dist/functions/cssRuleInspector.d.ts +83 -0
  43. package/dist/functions/cssRuleInspector.js +608 -0
  44. package/dist/functions/cssRuleInspector.test.d.ts +1 -0
  45. package/dist/functions/cssRuleInspector.test.js +439 -0
  46. package/dist/functions/deduplicateLabels.test.d.ts +1 -0
  47. package/dist/functions/deduplicateLabels.test.js +178 -0
  48. package/dist/functions/enrichAncestrySourceMaps.js +0 -1
  49. package/dist/functions/extractComputedStyles.d.ts +51 -0
  50. package/dist/functions/extractComputedStyles.js +447 -0
  51. package/dist/functions/extractComputedStyles.test.d.ts +1 -0
  52. package/dist/functions/extractComputedStyles.test.js +549 -0
  53. package/dist/functions/formatAncestryChain.d.ts +8 -0
  54. package/dist/functions/formatAncestryChain.js +21 -1
  55. package/dist/functions/formatAncestryChain.test.js +18 -0
  56. package/dist/functions/getUsableName.test.d.ts +1 -0
  57. package/dist/functions/getUsableName.test.js +219 -0
  58. package/dist/functions/isCombinationModifiersPressed.test.d.ts +1 -0
  59. package/dist/functions/isCombinationModifiersPressed.test.js +192 -0
  60. package/dist/functions/mergeRects.test.js +210 -1
  61. package/dist/functions/namedSnapshots.d.ts +52 -0
  62. package/dist/functions/namedSnapshots.js +161 -0
  63. package/dist/functions/namedSnapshots.test.d.ts +1 -0
  64. package/dist/functions/namedSnapshots.test.js +85 -0
  65. package/dist/functions/normalizeFilePath.test.d.ts +1 -0
  66. package/dist/functions/normalizeFilePath.test.js +66 -0
  67. package/dist/functions/parseDataId.test.d.ts +1 -0
  68. package/dist/functions/parseDataId.test.js +101 -0
  69. package/dist/hooks/getStorage.d.ts +3 -0
  70. package/dist/hooks/getStorage.js +17 -0
  71. package/dist/hooks/useEventListeners.d.ts +15 -0
  72. package/dist/hooks/useEventListeners.js +56 -0
  73. package/dist/hooks/useLocatorStorage.d.ts +18 -0
  74. package/dist/hooks/useLocatorStorage.js +41 -0
  75. package/dist/hooks/useLocatorStorage.test.d.ts +1 -0
  76. package/dist/hooks/useLocatorStorage.test.js +124 -0
  77. package/dist/hooks/useRecordingState.d.ts +43 -0
  78. package/dist/hooks/useRecordingState.js +387 -0
  79. package/dist/hooks/useSettings.d.ts +13 -0
  80. package/dist/hooks/useSettings.js +66 -0
  81. package/dist/index.d.ts +5 -2
  82. package/dist/index.js +4 -2
  83. package/dist/initRuntime.d.ts +3 -1
  84. package/dist/initRuntime.js +4 -1
  85. package/dist/mcpBridge.d.ts +61 -0
  86. package/dist/mcpBridge.js +534 -0
  87. package/dist/mcpBridge.test.d.ts +1 -0
  88. package/dist/mcpBridge.test.js +248 -0
  89. package/dist/output.css +20 -0
  90. package/dist/visualDiff/diff.d.ts +9 -0
  91. package/dist/visualDiff/diff.js +209 -0
  92. package/dist/visualDiff/diff.test.d.ts +1 -0
  93. package/dist/visualDiff/diff.test.js +253 -0
  94. package/dist/visualDiff/settle.d.ts +3 -0
  95. package/dist/visualDiff/settle.js +50 -0
  96. package/dist/visualDiff/settle.test.d.ts +1 -0
  97. package/dist/visualDiff/settle.test.js +65 -0
  98. package/dist/visualDiff/snapshot.d.ts +4 -0
  99. package/dist/visualDiff/snapshot.js +84 -0
  100. package/dist/visualDiff/snapshot.test.d.ts +1 -0
  101. package/dist/visualDiff/snapshot.test.js +245 -0
  102. package/dist/visualDiff/types.d.ts +37 -0
  103. package/dist/visualDiff/types.js +1 -0
  104. package/package.json +2 -2
  105. package/scripts/wrapCSS.js +1 -1
  106. package/scripts/wrapImage.js +1 -1
  107. package/src/_generated_styles.ts +21 -1
  108. package/src/_generated_tree_icon.ts +1 -1
  109. package/src/adapters/HtmlElementTreeNode.ts +10 -7
  110. package/src/adapters/createTreeNode.ts +12 -51
  111. package/src/adapters/detectFramework.test.ts +73 -0
  112. package/src/adapters/detectFramework.ts +28 -0
  113. package/src/adapters/jsx/jsxAdapter.test.ts +240 -0
  114. package/src/adapters/jsx/jsxAdapter.ts +53 -106
  115. package/src/adapters/nextjs/parseNextjsDataAttributes.test.ts +212 -0
  116. package/src/adapters/nextjs/parseNextjsDataAttributes.ts +1 -1
  117. package/src/adapters/react/findDebugSource.ts +5 -6
  118. package/src/adapters/react/findFiberByHtmlElement.ts +3 -3
  119. package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +3 -0
  120. package/src/adapters/react/reactAdapter.ts +1 -2
  121. package/src/adapters/resolveAdapter.ts +4 -14
  122. package/src/adapters/svelte/svelteAdapter.test.ts +334 -0
  123. package/src/adapters/vue/vueAdapter.test.ts +259 -0
  124. package/src/browserApi.test.ts +329 -0
  125. package/src/browserApi.ts +351 -4
  126. package/src/components/RecordingPillButton.tsx +301 -0
  127. package/src/components/RecordingResults.tsx +114 -13
  128. package/src/components/Runtime.tsx +176 -621
  129. package/src/components/SettingsPanel.tsx +339 -0
  130. package/src/consoleCapture.ts +113 -0
  131. package/src/dejitter/recorder.ts +67 -3
  132. package/src/functions/cssRuleInspector.test.ts +517 -0
  133. package/src/functions/cssRuleInspector.ts +708 -0
  134. package/src/functions/deduplicateLabels.test.ts +115 -0
  135. package/src/functions/enrichAncestrySourceMaps.ts +6 -3
  136. package/src/functions/extractComputedStyles.test.ts +681 -0
  137. package/src/functions/extractComputedStyles.ts +768 -0
  138. package/src/functions/formatAncestryChain.test.ts +23 -1
  139. package/src/functions/formatAncestryChain.ts +22 -1
  140. package/src/functions/getUsableName.test.ts +242 -0
  141. package/src/functions/isCombinationModifiersPressed.test.ts +156 -0
  142. package/src/functions/mergeRects.test.ts +111 -1
  143. package/src/functions/namedSnapshots.test.ts +106 -0
  144. package/src/functions/namedSnapshots.ts +232 -0
  145. package/src/functions/normalizeFilePath.test.ts +80 -0
  146. package/src/functions/parseDataId.test.ts +125 -0
  147. package/src/hooks/getStorage.ts +26 -0
  148. package/src/hooks/useEventListeners.ts +97 -0
  149. package/src/hooks/useLocatorStorage.test.ts +127 -0
  150. package/src/hooks/useLocatorStorage.ts +60 -0
  151. package/src/hooks/useRecordingState.ts +516 -0
  152. package/src/hooks/useSettings.ts +83 -0
  153. package/src/index.ts +10 -5
  154. package/src/initRuntime.ts +5 -0
  155. package/src/mcpBridge.test.ts +260 -0
  156. package/src/mcpBridge.ts +677 -0
  157. package/src/visualDiff/diff.test.ts +167 -0
  158. package/src/visualDiff/diff.ts +242 -0
  159. package/src/visualDiff/settle.test.ts +77 -0
  160. package/src/visualDiff/settle.ts +62 -0
  161. package/src/visualDiff/snapshot.test.ts +200 -0
  162. package/src/visualDiff/snapshot.ts +119 -0
  163. package/src/visualDiff/types.ts +40 -0
  164. package/tsconfig.json +3 -1
  165. package/vitest.config.ts +18 -0
  166. package/jest.config.ts +0 -195
@@ -0,0 +1,124 @@
1
+ import { describe, expect, test, beforeAll, afterAll, beforeEach, vi } from "vitest";
2
+ import { STORAGE_KEY, loadFromStorage, saveToStorage, clearStorage } from "./useLocatorStorage";
3
+ const mockStorage = new Map();
4
+ const mockLocalStorage = {
5
+ getItem(key) {
6
+ return mockStorage.has(key) ? mockStorage.get(key) : null;
7
+ },
8
+ setItem(key, value) {
9
+ mockStorage.set(key, String(value));
10
+ },
11
+ removeItem(key) {
12
+ mockStorage.delete(key);
13
+ },
14
+ clear() {
15
+ mockStorage.clear();
16
+ }
17
+ };
18
+ describe("useLocatorStorage", () => {
19
+ beforeAll(() => {
20
+ vi.stubGlobal("localStorage", mockLocalStorage);
21
+ });
22
+ afterAll(() => {
23
+ vi.unstubAllGlobals();
24
+ });
25
+ beforeEach(() => {
26
+ mockLocalStorage.clear();
27
+ });
28
+ describe("loadFromStorage", () => {
29
+ test("returns defaults when storage is empty", () => {
30
+ const result = loadFromStorage();
31
+ expect(result).toEqual({
32
+ last: null,
33
+ previous: null
34
+ });
35
+ });
36
+ test("returns parsed data from localStorage", () => {
37
+ const data = {
38
+ last: {
39
+ findings: [],
40
+ summary: null,
41
+ data: null,
42
+ elementPath: "div > button",
43
+ interactions: []
44
+ },
45
+ previous: null
46
+ };
47
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
48
+ const result = loadFromStorage();
49
+ expect(result).toEqual(data);
50
+ });
51
+ test("returns defaults on corrupt JSON", () => {
52
+ localStorage.setItem(STORAGE_KEY, "not-valid-json{{{");
53
+ const result = loadFromStorage();
54
+ expect(result).toEqual({
55
+ last: null,
56
+ previous: null
57
+ });
58
+ });
59
+ });
60
+ describe("saveToStorage", () => {
61
+ test("saves recording and shifts previous", () => {
62
+ const first = {
63
+ findings: [],
64
+ summary: null,
65
+ data: "first",
66
+ elementPath: "path1",
67
+ interactions: []
68
+ };
69
+ const second = {
70
+ findings: [],
71
+ summary: null,
72
+ data: "second",
73
+ elementPath: "path2",
74
+ interactions: []
75
+ };
76
+ saveToStorage(first);
77
+ let stored = JSON.parse(localStorage.getItem(STORAGE_KEY));
78
+ expect(stored.last.data).toBe("first");
79
+ expect(stored.previous).toBeNull();
80
+ saveToStorage(second);
81
+ stored = JSON.parse(localStorage.getItem(STORAGE_KEY));
82
+ expect(stored.last.data).toBe("second");
83
+ expect(stored.previous.data).toBe("first");
84
+ });
85
+ });
86
+ describe("clearStorage", () => {
87
+ test("removes the storage key", () => {
88
+ localStorage.setItem(STORAGE_KEY, "something");
89
+ clearStorage();
90
+ expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
91
+ });
92
+ test("does not throw when key does not exist", () => {
93
+ expect(() => clearStorage()).not.toThrow();
94
+ });
95
+ });
96
+ describe("roundtrip", () => {
97
+ test("save then load returns correct data", () => {
98
+ const recording = {
99
+ findings: [{
100
+ type: "jitter",
101
+ severity: "low"
102
+ }],
103
+ summary: {
104
+ totalFindings: 1
105
+ },
106
+ data: {
107
+ frames: 100
108
+ },
109
+ elementPath: "App > Header > Button",
110
+ interactions: [{
111
+ t: 0,
112
+ type: "click",
113
+ target: "button",
114
+ x: 100,
115
+ y: 200
116
+ }]
117
+ };
118
+ saveToStorage(recording);
119
+ const loaded = loadFromStorage();
120
+ expect(loaded.last).toEqual(recording);
121
+ expect(loaded.previous).toBeNull();
122
+ });
123
+ });
124
+ });
@@ -0,0 +1,43 @@
1
+ import { type Accessor } from "solid-js";
2
+ import { AdapterId } from "../consts";
3
+ import { type DejitterFinding, type DejitterSummary } from "../dejitter/recorder";
4
+ import type { InteractionEvent } from "../components/RecordingResults";
5
+ import type { DeltaReport } from "../visualDiff/types";
6
+ export type RecordingState = "idle" | "selecting" | "recording" | "results";
7
+ export interface RecordingStateAPI {
8
+ recordingState: Accessor<RecordingState>;
9
+ recordedElement: Accessor<HTMLElement | null>;
10
+ recordingFindings: Accessor<DejitterFinding[]>;
11
+ recordingSummary: Accessor<DejitterSummary | null>;
12
+ recordingData: Accessor<any>;
13
+ recordingElementPath: Accessor<string>;
14
+ interactionLog: Accessor<InteractionEvent[]>;
15
+ visualDiff: Accessor<DeltaReport | null>;
16
+ replayBox: Accessor<{
17
+ x: number;
18
+ y: number;
19
+ w: number;
20
+ h: number;
21
+ } | null>;
22
+ replaying: Accessor<boolean>;
23
+ viewingPrevious: Accessor<boolean>;
24
+ handleRecordClick: () => void;
25
+ startRecording: (element: HTMLElement) => void;
26
+ stopRecording: () => Promise<void>;
27
+ replayRecording: () => void;
28
+ stopReplay: () => void;
29
+ replayWithRecord: (elementOrSelector: HTMLElement | string) => Promise<{
30
+ path: string;
31
+ findings: DejitterFinding[];
32
+ summary: DejitterSummary | null;
33
+ data: any;
34
+ interactions: InteractionEvent[];
35
+ visualDiff: DeltaReport | null;
36
+ } | null>;
37
+ dismissResults: () => void;
38
+ hasPreviousRecording: () => boolean;
39
+ loadPreviousRecording: () => void;
40
+ loadLatestRecording: () => void;
41
+ cleanup: () => void;
42
+ }
43
+ export declare function useRecordingState(adapterId?: AdapterId): RecordingStateAPI;
@@ -0,0 +1,387 @@
1
+ import { createSignal } from "solid-js";
2
+ import { createDejitterRecorder } from "../dejitter/recorder";
3
+ import { isLocatorsOwnElement } from "../functions/isLocatorsOwnElement";
4
+ import { collectAncestry, formatAncestryChain } from "../functions/formatAncestryChain";
5
+ import { createTreeNode } from "../adapters/createTreeNode";
6
+ import { loadFromStorage, saveToStorage, clearStorage } from "./useLocatorStorage";
7
+ import { settings } from "./useSettings";
8
+ import { takeSnapshot } from "../visualDiff/snapshot";
9
+ import { computeDiff, formatReport } from "../visualDiff/diff";
10
+ import { waitForSettle } from "../visualDiff/settle";
11
+ function buildDejitterConfig() {
12
+ const s = settings();
13
+ return {
14
+ selector: "[data-treelocator-recording]",
15
+ props: ["opacity", "transform", "boundingRect", "width", "height"],
16
+ sampleRate: s.sampleRate,
17
+ maxDuration: s.maxDurationMs,
18
+ idleTimeout: 0,
19
+ mutations: true,
20
+ thresholds: {
21
+ jump: {
22
+ minAbsolute: s.jumpMinAbsolute
23
+ },
24
+ lag: {
25
+ minDelay: s.lagMinDelay
26
+ }
27
+ }
28
+ };
29
+ }
30
+ export function useRecordingState(adapterId) {
31
+ // Restore last results on mount
32
+ const restored = loadFromStorage();
33
+ const restoredLast = restored.last;
34
+ const [recordingState, setRecordingState] = createSignal(restoredLast ? "results" : "idle");
35
+ const [recordedElement, setRecordedElement] = createSignal(null);
36
+ const [recordingFindings, setRecordingFindings] = createSignal(restoredLast?.findings ?? []);
37
+ const [recordingSummary, setRecordingSummary] = createSignal(restoredLast?.summary ?? null);
38
+ const [interactionLog, setInteractionLog] = createSignal(restoredLast?.interactions ?? []);
39
+ const [recordingData, setRecordingData] = createSignal(restoredLast?.data ?? null);
40
+ const [recordingElementPath, setRecordingElementPath] = createSignal(restoredLast?.elementPath ?? "");
41
+ const [visualDiff, setVisualDiff] = createSignal(restoredLast?.visualDiff ?? null);
42
+ const [replayBox, setReplayBox] = createSignal(null);
43
+ const [replaying, setReplaying] = createSignal(false);
44
+ const [viewingPrevious, setViewingPrevious] = createSignal(false);
45
+ let dejitterInstance = null;
46
+ let interactionClickHandler = null;
47
+ let recordingStartTime = 0;
48
+ let recordingStartPerf = 0;
49
+ let replayTimerId = null;
50
+ let visualDiffContext = null;
51
+ async function finalizeVisualDiff() {
52
+ if (!visualDiffContext) return null;
53
+ const {
54
+ before,
55
+ root
56
+ } = visualDiffContext;
57
+ visualDiffContext = null;
58
+ const settle = await waitForSettle(1000, root);
59
+ const after = takeSnapshot(root);
60
+ const report = computeDiff(before, after);
61
+ report.elapsedMs = performance.now() - recordingStartPerf;
62
+ report.settle = settle;
63
+ report.text = formatReport(report.entries, {
64
+ elapsedMs: report.elapsedMs,
65
+ settle
66
+ });
67
+ return report;
68
+ }
69
+ function collectElementPath(el) {
70
+ const treeNode = createTreeNode(el, adapterId);
71
+ if (treeNode) {
72
+ const ancestry = collectAncestry(treeNode);
73
+ return formatAncestryChain(ancestry);
74
+ }
75
+ return "";
76
+ }
77
+
78
+ // --- Recording lifecycle ---
79
+
80
+ function handleRecordClick() {
81
+ switch (recordingState()) {
82
+ case "idle":
83
+ setRecordingState("selecting");
84
+ break;
85
+ case "selecting":
86
+ setRecordingState("idle");
87
+ break;
88
+ case "recording":
89
+ stopRecording();
90
+ break;
91
+ case "results":
92
+ dismissResults();
93
+ setRecordingState("selecting");
94
+ break;
95
+ }
96
+ }
97
+ function startRecording(element) {
98
+ element.setAttribute("data-treelocator-recording", "true");
99
+ setRecordedElement(element);
100
+ recordingStartPerf = performance.now();
101
+ visualDiffContext = settings().visualDiff ? {
102
+ before: takeSnapshot(element),
103
+ root: element
104
+ } : null;
105
+ dejitterInstance = createDejitterRecorder();
106
+ dejitterInstance.configure(buildDejitterConfig());
107
+ dejitterInstance.start();
108
+ startInteractionTracker();
109
+ setRecordingState("recording");
110
+ }
111
+ async function stopRecording() {
112
+ const instance = dejitterInstance;
113
+ if (!instance) return;
114
+ dejitterInstance = null;
115
+ instance.stop();
116
+ const anomalyEnabled = settings().anomalyTracking;
117
+ const findings = anomalyEnabled ? instance.findings(true) : [];
118
+ const summary = instance.summary(true);
119
+ const data = anomalyEnabled ? instance.getData() : null;
120
+ const el = recordedElement();
121
+ const elementPath = el ? collectElementPath(el) : "";
122
+ stopInteractionTracker();
123
+ const diffReport = await finalizeVisualDiff();
124
+ setRecordingFindings(findings);
125
+ setRecordingSummary(summary);
126
+ setRecordingData(data);
127
+ setRecordingElementPath(elementPath);
128
+ setVisualDiff(diffReport);
129
+ saveToStorage({
130
+ findings,
131
+ summary,
132
+ data,
133
+ elementPath,
134
+ interactions: interactionLog(),
135
+ visualDiff: diffReport
136
+ });
137
+ el?.removeAttribute("data-treelocator-recording");
138
+ setRecordingState("results");
139
+ }
140
+ function replayRecording() {
141
+ const events = interactionLog();
142
+ if (events.length === 0) return;
143
+ stopReplay();
144
+ setReplaying(true);
145
+ let eventIdx = 0;
146
+ function scheduleNext() {
147
+ if (eventIdx >= events.length) {
148
+ stopReplay();
149
+ return;
150
+ }
151
+ const evt = events[eventIdx];
152
+ const delay = eventIdx === 0 ? 100 : evt.t - events[eventIdx - 1].t;
153
+ replayTimerId = window.setTimeout(() => {
154
+ setReplayBox({
155
+ x: evt.x - 12,
156
+ y: evt.y - 12,
157
+ w: 24,
158
+ h: 24
159
+ });
160
+ const target = document.elementFromPoint(evt.x, evt.y);
161
+ if (target) {
162
+ target.dispatchEvent(new MouseEvent("click", {
163
+ bubbles: true,
164
+ cancelable: true,
165
+ clientX: evt.x,
166
+ clientY: evt.y,
167
+ view: window
168
+ }));
169
+ }
170
+ window.setTimeout(() => setReplayBox(null), 200);
171
+ eventIdx++;
172
+ scheduleNext();
173
+ }, Math.max(delay, 50));
174
+ }
175
+ scheduleNext();
176
+ }
177
+ function stopReplay() {
178
+ if (replayTimerId) {
179
+ clearTimeout(replayTimerId);
180
+ replayTimerId = null;
181
+ }
182
+ setReplaying(false);
183
+ setReplayBox(null);
184
+ }
185
+ function replayWithRecord(elementOrSelector) {
186
+ let element;
187
+ if (typeof elementOrSelector === "string") {
188
+ const found = document.querySelector(elementOrSelector);
189
+ element = found instanceof HTMLElement ? found : null;
190
+ } else {
191
+ element = elementOrSelector;
192
+ }
193
+ if (!element) return Promise.resolve(null);
194
+ const stored = loadFromStorage();
195
+ const events = stored.last?.interactions ?? interactionLog();
196
+ if (events.length === 0) return Promise.resolve(null);
197
+ return new Promise(resolve => {
198
+ element.setAttribute("data-treelocator-recording", "true");
199
+ setRecordedElement(element);
200
+ recordingStartPerf = performance.now();
201
+ visualDiffContext = settings().visualDiff ? {
202
+ before: takeSnapshot(element),
203
+ root: element
204
+ } : null;
205
+ dejitterInstance = createDejitterRecorder();
206
+ dejitterInstance.configure(buildDejitterConfig());
207
+ dejitterInstance.start();
208
+ setRecordingState("recording");
209
+ setReplaying(true);
210
+ let eventIdx = 0;
211
+ async function finishRecording() {
212
+ setReplaying(false);
213
+ setReplayBox(null);
214
+ const instance = dejitterInstance;
215
+ if (!instance) {
216
+ resolve(null);
217
+ return;
218
+ }
219
+ dejitterInstance = null;
220
+ instance.stop();
221
+ const anomalyEnabled = settings().anomalyTracking;
222
+ const findings = anomalyEnabled ? instance.findings(true) : [];
223
+ const summary = instance.summary(true);
224
+ const data = anomalyEnabled ? instance.getData() : null;
225
+ const el = recordedElement();
226
+ const elementPath = el ? collectElementPath(el) : "";
227
+ const diffReport = await finalizeVisualDiff();
228
+ setRecordingFindings(findings);
229
+ setRecordingSummary(summary);
230
+ setRecordingData(data);
231
+ setRecordingElementPath(elementPath);
232
+ setInteractionLog(events);
233
+ setVisualDiff(diffReport);
234
+ saveToStorage({
235
+ findings,
236
+ summary,
237
+ data,
238
+ elementPath,
239
+ interactions: events,
240
+ visualDiff: diffReport
241
+ });
242
+ el?.removeAttribute("data-treelocator-recording");
243
+ setRecordingState("results");
244
+ resolve({
245
+ path: elementPath,
246
+ findings,
247
+ summary,
248
+ data,
249
+ interactions: events,
250
+ visualDiff: diffReport
251
+ });
252
+ }
253
+ function scheduleNext() {
254
+ if (eventIdx >= events.length) {
255
+ replayTimerId = window.setTimeout(finishRecording, 500);
256
+ return;
257
+ }
258
+ const evt = events[eventIdx];
259
+ const delay = eventIdx === 0 ? 100 : evt.t - events[eventIdx - 1].t;
260
+ replayTimerId = window.setTimeout(() => {
261
+ setReplayBox({
262
+ x: evt.x - 12,
263
+ y: evt.y - 12,
264
+ w: 24,
265
+ h: 24
266
+ });
267
+ const target = document.elementFromPoint(evt.x, evt.y);
268
+ if (target) {
269
+ target.dispatchEvent(new MouseEvent("click", {
270
+ bubbles: true,
271
+ cancelable: true,
272
+ clientX: evt.x,
273
+ clientY: evt.y,
274
+ view: window
275
+ }));
276
+ }
277
+ window.setTimeout(() => setReplayBox(null), 200);
278
+ eventIdx++;
279
+ scheduleNext();
280
+ }, Math.max(delay, 50));
281
+ }
282
+ scheduleNext();
283
+ });
284
+ }
285
+ function dismissResults() {
286
+ stopReplay();
287
+ setRecordingFindings([]);
288
+ setRecordingSummary(null);
289
+ setRecordingData(null);
290
+ setRecordingElementPath("");
291
+ setInteractionLog([]);
292
+ setVisualDiff(null);
293
+ setRecordedElement(null);
294
+ setViewingPrevious(false);
295
+ setRecordingState("idle");
296
+ clearStorage();
297
+ }
298
+ function hasPreviousRecording() {
299
+ return loadFromStorage().previous !== null;
300
+ }
301
+ function loadPreviousRecording() {
302
+ const stored = loadFromStorage();
303
+ if (!stored.previous) return;
304
+ const prev = stored.previous;
305
+ setRecordingFindings(prev.findings);
306
+ setRecordingSummary(prev.summary);
307
+ setRecordingData(prev.data);
308
+ setRecordingElementPath(prev.elementPath);
309
+ setInteractionLog(prev.interactions);
310
+ setVisualDiff(prev.visualDiff ?? null);
311
+ setViewingPrevious(true);
312
+ setRecordingState("results");
313
+ }
314
+ function loadLatestRecording() {
315
+ const stored = loadFromStorage();
316
+ if (!stored.last) return;
317
+ const last = stored.last;
318
+ setRecordingFindings(last.findings);
319
+ setRecordingSummary(last.summary);
320
+ setRecordingData(last.data);
321
+ setRecordingElementPath(last.elementPath);
322
+ setInteractionLog(last.interactions);
323
+ setVisualDiff(last.visualDiff ?? null);
324
+ setViewingPrevious(false);
325
+ setRecordingState("results");
326
+ }
327
+
328
+ // --- Interaction tracking ---
329
+
330
+ function startInteractionTracker() {
331
+ recordingStartTime = performance.now();
332
+ setInteractionLog([]);
333
+ interactionClickHandler = e => {
334
+ if (isLocatorsOwnElement(e.target)) return;
335
+ const el = e.target;
336
+ const tag = el.tagName?.toLowerCase() || "unknown";
337
+ const id = el.id ? "#" + el.id : "";
338
+ const cls = el.className && typeof el.className === "string" ? "." + el.className.split(" ")[0] : "";
339
+ setInteractionLog(prev => [...prev, {
340
+ t: Math.round(performance.now() - recordingStartTime),
341
+ type: "click",
342
+ target: `${tag}${id}${cls}`,
343
+ x: e.clientX,
344
+ y: e.clientY
345
+ }]);
346
+ };
347
+ document.addEventListener("click", interactionClickHandler, {
348
+ capture: true
349
+ });
350
+ }
351
+ function stopInteractionTracker() {
352
+ if (interactionClickHandler) {
353
+ document.removeEventListener("click", interactionClickHandler, {
354
+ capture: true
355
+ });
356
+ interactionClickHandler = null;
357
+ }
358
+ }
359
+ function cleanup() {
360
+ stopReplay();
361
+ stopInteractionTracker();
362
+ }
363
+ return {
364
+ recordingState,
365
+ recordedElement,
366
+ recordingFindings,
367
+ recordingSummary,
368
+ recordingData,
369
+ recordingElementPath,
370
+ interactionLog,
371
+ visualDiff,
372
+ replayBox,
373
+ replaying,
374
+ viewingPrevious,
375
+ handleRecordClick,
376
+ startRecording,
377
+ stopRecording,
378
+ replayRecording,
379
+ stopReplay,
380
+ replayWithRecord,
381
+ dismissResults,
382
+ hasPreviousRecording,
383
+ loadPreviousRecording,
384
+ loadLatestRecording,
385
+ cleanup
386
+ };
387
+ }
@@ -0,0 +1,13 @@
1
+ import { type Accessor } from "solid-js";
2
+ export type TreelocatorSettings = {
3
+ anomalyTracking: boolean;
4
+ visualDiff: boolean;
5
+ computedStyles: boolean;
6
+ computedStylesIncludeDefaults: boolean;
7
+ sampleRate: number;
8
+ maxDurationMs: number;
9
+ jumpMinAbsolute: number;
10
+ lagMinDelay: number;
11
+ };
12
+ export declare const DEFAULT_SETTINGS: TreelocatorSettings;
13
+ export declare const settings: Accessor<TreelocatorSettings>, setSetting: <K extends keyof TreelocatorSettings>(key: K, value: TreelocatorSettings[K]) => void, resetSettings: () => void;
@@ -0,0 +1,66 @@
1
+ import { createRoot, createSignal } from "solid-js";
2
+ import { getStorage } from "./getStorage";
3
+ export const DEFAULT_SETTINGS = {
4
+ anomalyTracking: true,
5
+ visualDiff: true,
6
+ computedStyles: true,
7
+ computedStylesIncludeDefaults: false,
8
+ sampleRate: 15,
9
+ maxDurationMs: 30000,
10
+ jumpMinAbsolute: 50,
11
+ lagMinDelay: 50
12
+ };
13
+ const STORAGE_KEY = "__treelocator_settings__";
14
+ function loadSettings() {
15
+ const storage = getStorage();
16
+ if (!storage) return {
17
+ ...DEFAULT_SETTINGS
18
+ };
19
+ try {
20
+ const raw = storage.getItem(STORAGE_KEY);
21
+ if (raw) return {
22
+ ...DEFAULT_SETTINGS,
23
+ ...JSON.parse(raw)
24
+ };
25
+ } catch {
26
+ // Ignore localStorage errors (SSR, permissions, quota)
27
+ }
28
+ return {
29
+ ...DEFAULT_SETTINGS
30
+ };
31
+ }
32
+ function persistSettings(next) {
33
+ const storage = getStorage();
34
+ if (!storage) return;
35
+ try {
36
+ storage.setItem(STORAGE_KEY, JSON.stringify(next));
37
+ } catch {
38
+ // Ignore localStorage errors (SSR, permissions, quota)
39
+ }
40
+ }
41
+ export const {
42
+ settings,
43
+ setSetting,
44
+ resetSettings
45
+ } = createRoot(() => {
46
+ const [settings, setSettingsSignal] = createSignal(loadSettings());
47
+ function setSetting(key, value) {
48
+ const next = {
49
+ ...settings(),
50
+ [key]: value
51
+ };
52
+ setSettingsSignal(next);
53
+ persistSettings(next);
54
+ }
55
+ function resetSettings() {
56
+ setSettingsSignal({
57
+ ...DEFAULT_SETTINGS
58
+ });
59
+ persistSettings(DEFAULT_SETTINGS);
60
+ }
61
+ return {
62
+ settings,
63
+ setSetting,
64
+ resetSettings
65
+ };
66
+ });
package/dist/index.d.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  import { Target } from "@locator/shared";
2
2
  import { AdapterId } from "./consts";
3
+ import type { MCPBridgeConfig } from "./mcpBridge";
3
4
  export * from "./adapters/jsx/runtimeStore";
4
5
  export declare const MAX_ZINDEX = 2147483647;
5
- export declare function setup({ adapter, targets, }?: {
6
+ export interface SetupOptions {
6
7
  adapter?: AdapterId;
7
8
  targets?: {
8
9
  [k: string]: Target | string;
9
10
  };
10
- }): void;
11
+ mcp?: MCPBridgeConfig;
12
+ }
13
+ export declare function setup({ adapter, targets, mcp, }?: SetupOptions): void;
11
14
  export default setup;
package/dist/index.js CHANGED
@@ -3,11 +3,13 @@ export * from "./adapters/jsx/runtimeStore";
3
3
  export const MAX_ZINDEX = 2147483647;
4
4
  export function setup({
5
5
  adapter,
6
- targets
6
+ targets,
7
+ mcp
7
8
  } = {}) {
8
9
  setTimeout(() => initRuntime({
9
10
  adapter,
10
- targets
11
+ targets,
12
+ mcp
11
13
  }), 0);
12
14
  }
13
15
  export default setup;
@@ -1,8 +1,10 @@
1
1
  import { Target } from "@locator/shared";
2
2
  import { AdapterId } from "./consts";
3
- export declare function initRuntime({ adapter, targets, }?: {
3
+ import type { MCPBridgeConfig } from "./mcpBridge";
4
+ export declare function initRuntime({ adapter, targets, mcp, }?: {
4
5
  adapter?: AdapterId;
5
6
  targets?: {
6
7
  [k: string]: Target | string;
7
8
  };
9
+ mcp?: MCPBridgeConfig;
8
10
  }): void;
@@ -3,9 +3,11 @@ import { fontFamily } from "./consts";
3
3
  import generatedStyles from "./_generated_styles";
4
4
  import { MAX_ZINDEX } from "./index";
5
5
  import { installBrowserAPI } from "./browserApi";
6
+ import { startMCPBridge } from "./mcpBridge";
6
7
  export function initRuntime({
7
8
  adapter,
8
- targets
9
+ targets,
10
+ mcp
9
11
  } = {}) {
10
12
  if (typeof window === "undefined" || typeof document === "undefined") {
11
13
  return;
@@ -17,6 +19,7 @@ export function initRuntime({
17
19
 
18
20
  // Install browser API on window.__treelocator__
19
21
  installBrowserAPI(adapter);
22
+ startMCPBridge(mcp);
20
23
 
21
24
  // add style tag to head
22
25
  const style = document.createElement("style");