@zargaryanvh/react-component-inspector 1.0.2 → 1.0.4

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.
@@ -1,320 +1,329 @@
1
- /**
2
- * Automatic inspection detection via data attributes
3
- * Components can add data-inspection-* attributes and the system will detect them automatically
4
- */
5
- /**
6
- * Extract basic element info for inspection
7
- */
8
- const extractElementInfo = (element) => {
9
- const tagName = element.tagName.toLowerCase();
10
- // Convert className to string - it can be a DOMTokenList or string
11
- let className = "";
12
- if (typeof element.className === "string") {
13
- className = element.className;
14
- }
15
- else if (element.className) {
16
- // DOMTokenList - convert to string
17
- className = String(element.className);
18
- }
19
- const id = element.id || "";
20
- // Extract text preview (first 50 chars)
21
- const textContent = element.textContent || "";
22
- const textPreview = textContent.trim().substring(0, 50).replace(/\s+/g, " ");
23
- // Try to detect MUI components from class names
24
- let muiComponent;
25
- if (className && typeof className === "string" && className.includes("Mui")) {
26
- const muiMatch = className.match(/Mui(\w+)/);
27
- if (muiMatch) {
28
- muiComponent = muiMatch[1];
29
- }
30
- }
31
- return { tagName, className, id, textPreview, muiComponent };
32
- };
33
- /**
34
- * Parse component metadata from data attributes or infer from element
35
- */
36
- export const parseInspectionMetadata = (element) => {
37
- if (process.env.NODE_ENV !== "development") {
38
- return null;
39
- }
40
- // First, try to find explicit inspection metadata
41
- const componentName = element.getAttribute("data-inspection-name");
42
- const componentId = element.getAttribute("data-inspection-id");
43
- if (componentName && componentId) {
44
- // Has explicit metadata - use it
45
- const variant = element.getAttribute("data-inspection-variant") || undefined;
46
- const role = element.getAttribute("data-inspection-role") || undefined;
47
- const usagePath = element.getAttribute("data-inspection-usage-path") || "Unknown";
48
- const instanceIndex = parseInt(element.getAttribute("data-inspection-instance") || "0", 10);
49
- const propsSignature = element.getAttribute("data-inspection-props") || "default";
50
- const sourceFile = element.getAttribute("data-inspection-file") || "Unknown";
51
- return {
52
- componentName,
53
- componentId,
54
- variant,
55
- role,
56
- usagePath,
57
- instanceIndex,
58
- propsSignature,
59
- sourceFile,
60
- };
61
- }
62
- // No explicit metadata - infer from element
63
- const info = extractElementInfo(element);
64
- // Generate component name
65
- let inferredName = info.muiComponent || info.tagName.toUpperCase();
66
- if (info.id) {
67
- inferredName = `${inferredName} (${info.id})`;
68
- }
69
- else if (info.className) {
70
- // Try to extract meaningful class name
71
- const classes = info.className.split(/\s+/).filter(c => c && !c.startsWith("Mui") && c.length > 2);
72
- if (classes.length > 0) {
73
- inferredName = `${inferredName} .${classes[0]}`;
74
- }
75
- }
76
- // Generate component ID - use deterministic hash based on element's position in DOM tree
77
- // This ensures the same element always gets the same ID, even without explicit metadata
78
- const generateDeterministicId = (el, tagName) => {
79
- if (info.id) {
80
- return info.id;
81
- }
82
- // Create a stable identifier based on element's path in DOM tree
83
- const path = [];
84
- let current = el;
85
- // Walk up to body, collecting tag names and indices
86
- while (current && current !== document.body && current !== document.documentElement) {
87
- const currentTagName = current.tagName.toLowerCase();
88
- const parent = current.parentElement;
89
- if (parent) {
90
- // Get index among siblings with same tag name
91
- const siblings = Array.from(parent.children).filter((child) => child.tagName.toLowerCase() === currentTagName);
92
- const index = siblings.indexOf(current);
93
- path.unshift(`${currentTagName}[${index}]`);
94
- }
95
- else {
96
- path.unshift(currentTagName);
97
- }
98
- current = parent;
99
- }
100
- // Create hash from path (simple hash function)
101
- const pathString = path.join('>');
102
- let hash = 0;
103
- for (let i = 0; i < pathString.length; i++) {
104
- const char = pathString.charCodeAt(i);
105
- hash = ((hash << 5) - hash) + char;
106
- hash = hash & hash; // Convert to 32-bit integer
107
- }
108
- // Convert to positive hex string (6 chars)
109
- const hashStr = Math.abs(hash).toString(36).substr(0, 6);
110
- return `${tagName}-${hashStr}`;
111
- };
112
- const inferredId = generateDeterministicId(element, info.tagName);
113
- // Build props signature from attributes
114
- const props = [];
115
- if (info.id)
116
- props.push(`id=${info.id}`);
117
- if (info.className) {
118
- const classCount = info.className.split(/\s+/).length;
119
- props.push(`classes=${classCount}`);
120
- }
121
- const propsSignature = props.length > 0 ? props.join(", ") : "default";
122
- return {
123
- componentName: inferredName,
124
- componentId: inferredId,
125
- variant: info.muiComponent ? info.muiComponent.toLowerCase() : undefined,
126
- role: element.getAttribute("role") || element.getAttribute("aria-label") || undefined,
127
- usagePath: "DOM Element",
128
- instanceIndex: 0,
129
- propsSignature,
130
- sourceFile: "DOM",
131
- };
132
- };
133
- /**
134
- * Helper function to inspect an element (used by both mouse and touch events)
135
- */
136
- const inspectElement = (target, isInspectionActive, isLocked, setHoveredComponent) => {
137
- if (!isInspectionActive) {
138
- return;
139
- }
140
- // If locked, don't update - keep current tooltip fixed
141
- if (isLocked) {
142
- return;
143
- }
144
- if (!target || !document.body.contains(target)) {
145
- return;
146
- }
147
- // Always show inspection for any element (not just ones with metadata)
148
- // Walk up the DOM tree to find a meaningful element to inspect
149
- let current = target;
150
- let bestElement = null;
151
- let bestMetadata = null;
152
- let elementWithExplicitMetadata = null;
153
- let metadataWithExplicitId = null;
154
- // Skip text nodes and very small elements
155
- const isMeaningfulElement = (el) => {
156
- const rect = el.getBoundingClientRect();
157
- // Skip elements that are too small (likely text nodes or empty spans)
158
- if (rect.width < 5 && rect.height < 5) {
159
- return false;
160
- }
161
- // Prefer elements with explicit metadata, IDs, or meaningful classes
162
- return !!(el.getAttribute("data-inspection-name") ||
163
- el.id ||
164
- el.className ||
165
- el.tagName !== "SPAN" && el.tagName !== "DIV");
166
- };
167
- // First pass: Look for elements with explicit inspection metadata
168
- // This ensures we always find the same component regardless of which child is hovered
169
- while (current) {
170
- // Ensure element is still in the DOM
171
- if (!document.body.contains(current)) {
172
- break;
173
- }
174
- // Check if this element has explicit inspection metadata
175
- const hasExplicitMetadata = current.getAttribute("data-inspection-name") &&
176
- current.getAttribute("data-inspection-id");
177
- if (hasExplicitMetadata) {
178
- const metadata = parseInspectionMetadata(current);
179
- if (metadata) {
180
- elementWithExplicitMetadata = current;
181
- metadataWithExplicitId = metadata;
182
- break; // Found explicit metadata, use it
183
- }
184
- }
185
- current = current.parentElement;
186
- // Stop at body to avoid inspecting the entire page
187
- if (current && (current.tagName === "BODY" || current.tagName === "HTML")) {
188
- break;
189
- }
190
- }
191
- // If we found explicit metadata, use it (this ensures consistent IDs)
192
- if (elementWithExplicitMetadata && metadataWithExplicitId) {
193
- if (process.env.NODE_ENV === "development") {
194
- console.log("[Inspection] Found element with explicit metadata:", metadataWithExplicitId.componentName);
195
- }
196
- setHoveredComponent(metadataWithExplicitId, elementWithExplicitMetadata);
197
- return;
198
- }
199
- // Second pass: If no explicit metadata found, look for meaningful elements
200
- // Reset current to target for second pass
201
- current = target;
202
- while (current) {
203
- // Ensure element is still in the DOM
204
- if (!document.body.contains(current)) {
205
- break;
206
- }
207
- // Check if this is a meaningful element
208
- if (isMeaningfulElement(current)) {
209
- const metadata = parseInspectionMetadata(current);
210
- if (metadata) {
211
- // Keep the first meaningful element (closest to target)
212
- if (!bestElement) {
213
- bestElement = current;
214
- bestMetadata = metadata;
215
- }
216
- }
217
- }
218
- current = current.parentElement;
219
- // Stop at body to avoid inspecting the entire page
220
- if (current && (current.tagName === "BODY" || current.tagName === "HTML")) {
221
- break;
222
- }
223
- }
224
- if (bestElement && bestMetadata) {
225
- if (process.env.NODE_ENV === "development") {
226
- console.log("[Inspection] Found element:", bestMetadata.componentName);
227
- }
228
- setHoveredComponent(bestMetadata, bestElement);
229
- }
230
- else {
231
- // Fallback: show info for the target element itself
232
- const fallbackMetadata = parseInspectionMetadata(target);
233
- if (fallbackMetadata) {
234
- setHoveredComponent(fallbackMetadata, target);
235
- }
236
- else {
237
- setHoveredComponent(null, null);
238
- }
239
- }
240
- };
241
- /**
242
- * Setup global mouse event listener for automatic inspection detection
243
- * This works with components that have data-inspection-* attributes
244
- */
245
- export const setupAutoInspection = (setHoveredComponent, isInspectionActive, isLocked) => {
246
- if (process.env.NODE_ENV !== "development") {
247
- return () => { }; // No-op in production
248
- }
249
- // Detect if device supports touch
250
- const isTouchDevice = 'ontouchstart' in window ||
251
- navigator.maxTouchPoints > 0 ||
252
- /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
253
- const handleMouseMove = (e) => {
254
- if (!isInspectionActive) {
255
- return;
256
- }
257
- // If locked, don't update on mouse move - keep current tooltip fixed
258
- if (isLocked) {
259
- return;
260
- }
261
- const target = e.target;
262
- inspectElement(target, isInspectionActive, isLocked, setHoveredComponent);
263
- };
264
- // Touch move handler for mobile devices
265
- const handleTouchMove = (e) => {
266
- // Only process if inspection is active and we have touches
267
- if (!isInspectionActive || e.touches.length === 0) {
268
- return;
269
- }
270
- // Get the element under the first touch point
271
- const touch = e.touches[0];
272
- const target = document.elementFromPoint(touch.clientX, touch.clientY);
273
- if (target) {
274
- inspectElement(target, isInspectionActive, isLocked, setHoveredComponent);
275
- }
276
- };
277
- // Touch start handler for mobile - inspect on touch
278
- const handleTouchStart = (e) => {
279
- // Only process if inspection is active and we have touches
280
- // Skip if it's a 3-finger touch (used for activation)
281
- if (!isInspectionActive || e.touches.length === 3) {
282
- return;
283
- }
284
- // Get the element under the first touch point
285
- const touch = e.touches[0];
286
- const target = document.elementFromPoint(touch.clientX, touch.clientY);
287
- if (target) {
288
- inspectElement(target, isInspectionActive, isLocked, setHoveredComponent);
289
- }
290
- };
291
- const handleMouseLeave = () => {
292
- if (isInspectionActive && !isLocked) {
293
- setHoveredComponent(null, null);
294
- }
295
- };
296
- const handleTouchEnd = () => {
297
- // Clear inspection on touch end (unless locked)
298
- if (isInspectionActive && !isLocked) {
299
- setHoveredComponent(null, null);
300
- }
301
- };
302
- // Add mouse event listeners
303
- window.addEventListener("mousemove", handleMouseMove);
304
- document.addEventListener("mouseleave", handleMouseLeave);
305
- // Add touch event listeners for mobile devices
306
- if (isTouchDevice) {
307
- window.addEventListener("touchmove", handleTouchMove, { passive: true });
308
- window.addEventListener("touchstart", handleTouchStart, { passive: true });
309
- window.addEventListener("touchend", handleTouchEnd, { passive: true });
310
- }
311
- return () => {
312
- window.removeEventListener("mousemove", handleMouseMove);
313
- document.removeEventListener("mouseleave", handleMouseLeave);
314
- if (isTouchDevice) {
315
- window.removeEventListener("touchmove", handleTouchMove);
316
- window.removeEventListener("touchstart", handleTouchStart);
317
- window.removeEventListener("touchend", handleTouchEnd);
318
- }
319
- };
320
- };
1
+ /**
2
+ * Automatic inspection detection via data attributes
3
+ * Components can add data-inspection-* attributes and the system will detect them automatically
4
+ */
5
+ /**
6
+ * Extract basic element info for inspection
7
+ */
8
+ const extractElementInfo = (element) => {
9
+ const tagName = element.tagName.toLowerCase();
10
+ // Convert className to string - it can be a DOMTokenList or string
11
+ let className = "";
12
+ if (typeof element.className === "string") {
13
+ className = element.className;
14
+ }
15
+ else if (element.className) {
16
+ // DOMTokenList - convert to string
17
+ className = String(element.className);
18
+ }
19
+ const id = element.id || "";
20
+ // Extract text preview (first 50 chars)
21
+ const textContent = element.textContent || "";
22
+ const textPreview = textContent.trim().substring(0, 50).replace(/\s+/g, " ");
23
+ // Try to detect MUI components from class names
24
+ let muiComponent;
25
+ if (className && typeof className === "string" && className.includes("Mui")) {
26
+ const muiMatch = className.match(/Mui(\w+)/);
27
+ if (muiMatch) {
28
+ muiComponent = muiMatch[1];
29
+ }
30
+ }
31
+ return { tagName, className, id, textPreview, muiComponent };
32
+ };
33
+ /**
34
+ * Parse component metadata from data attributes or infer from element
35
+ */
36
+ export const parseInspectionMetadata = (element) => {
37
+ if (process.env.NODE_ENV !== "development") {
38
+ return null;
39
+ }
40
+ // First, try to find explicit inspection metadata
41
+ const componentName = element.getAttribute("data-inspection-name");
42
+ const componentId = element.getAttribute("data-inspection-id");
43
+ if (componentName && componentId) {
44
+ // Has explicit metadata - use it
45
+ const variant = element.getAttribute("data-inspection-variant") || undefined;
46
+ const role = element.getAttribute("data-inspection-role") || undefined;
47
+ const usagePath = element.getAttribute("data-inspection-usage-path") || "Unknown";
48
+ const instanceIndex = parseInt(element.getAttribute("data-inspection-instance") || "0", 10);
49
+ const propsSignature = element.getAttribute("data-inspection-props") || "default";
50
+ const sourceFile = element.getAttribute("data-inspection-file") || "Unknown";
51
+ return {
52
+ componentName,
53
+ componentId,
54
+ variant,
55
+ role,
56
+ usagePath,
57
+ instanceIndex,
58
+ propsSignature,
59
+ sourceFile,
60
+ };
61
+ }
62
+ // No explicit metadata - infer from element
63
+ const info = extractElementInfo(element);
64
+ // Generate component name
65
+ let inferredName = info.muiComponent || info.tagName.toUpperCase();
66
+ if (info.id) {
67
+ inferredName = `${inferredName} (${info.id})`;
68
+ }
69
+ else if (info.className) {
70
+ // Try to extract meaningful class name
71
+ const classes = info.className.split(/\s+/).filter(c => c && !c.startsWith("Mui") && c.length > 2);
72
+ if (classes.length > 0) {
73
+ inferredName = `${inferredName} .${classes[0]}`;
74
+ }
75
+ }
76
+ // Generate component ID - use deterministic hash based on element's position in DOM tree
77
+ // This ensures the same element always gets the same ID, even without explicit metadata
78
+ const generateDeterministicId = (el, tagName) => {
79
+ if (info.id) {
80
+ return info.id;
81
+ }
82
+ // Create a stable identifier based on element's path in DOM tree
83
+ const path = [];
84
+ let current = el;
85
+ // Walk up to body, collecting tag names and indices
86
+ while (current && current !== document.body && current !== document.documentElement) {
87
+ const currentTagName = current.tagName.toLowerCase();
88
+ const parent = current.parentElement;
89
+ if (parent) {
90
+ // Get index among siblings with same tag name
91
+ const siblings = Array.from(parent.children).filter((child) => child.tagName.toLowerCase() === currentTagName);
92
+ const index = siblings.indexOf(current);
93
+ path.unshift(`${currentTagName}[${index}]`);
94
+ }
95
+ else {
96
+ path.unshift(currentTagName);
97
+ }
98
+ current = parent;
99
+ }
100
+ // Create hash from path (simple hash function)
101
+ const pathString = path.join('>');
102
+ let hash = 0;
103
+ for (let i = 0; i < pathString.length; i++) {
104
+ const char = pathString.charCodeAt(i);
105
+ hash = ((hash << 5) - hash) + char;
106
+ hash = hash & hash; // Convert to 32-bit integer
107
+ }
108
+ // Convert to positive hex string (6 chars)
109
+ const hashStr = Math.abs(hash).toString(36).substr(0, 6);
110
+ return `${tagName}-${hashStr}`;
111
+ };
112
+ const inferredId = generateDeterministicId(element, info.tagName);
113
+ // Build props signature from attributes
114
+ const props = [];
115
+ if (info.id)
116
+ props.push(`id=${info.id}`);
117
+ if (info.className) {
118
+ const classCount = info.className.split(/\s+/).length;
119
+ props.push(`classes=${classCount}`);
120
+ }
121
+ const propsSignature = props.length > 0 ? props.join(", ") : "default";
122
+ return {
123
+ componentName: inferredName,
124
+ componentId: inferredId,
125
+ variant: info.muiComponent ? info.muiComponent.toLowerCase() : undefined,
126
+ role: element.getAttribute("role") || element.getAttribute("aria-label") || undefined,
127
+ usagePath: "DOM Element",
128
+ instanceIndex: 0,
129
+ propsSignature,
130
+ sourceFile: "DOM",
131
+ };
132
+ };
133
+ /**
134
+ * Inspect element at screen coordinates - used to start inspection immediately on mobile activation
135
+ */
136
+ export const inspectAtPoint = (clientX, clientY, setHoveredComponent) => {
137
+ const target = document.elementFromPoint(clientX, clientY);
138
+ if (target && document.body.contains(target)) {
139
+ inspectElement(target, true, false, setHoveredComponent);
140
+ }
141
+ };
142
+ /**
143
+ * Helper function to inspect an element (used by both mouse and touch events)
144
+ */
145
+ const inspectElement = (target, isInspectionActive, isLocked, setHoveredComponent) => {
146
+ if (!isInspectionActive) {
147
+ return;
148
+ }
149
+ // If locked, don't update - keep current tooltip fixed
150
+ if (isLocked) {
151
+ return;
152
+ }
153
+ if (!target || !document.body.contains(target)) {
154
+ return;
155
+ }
156
+ // Always show inspection for any element (not just ones with metadata)
157
+ // Walk up the DOM tree to find a meaningful element to inspect
158
+ let current = target;
159
+ let bestElement = null;
160
+ let bestMetadata = null;
161
+ let elementWithExplicitMetadata = null;
162
+ let metadataWithExplicitId = null;
163
+ // Skip text nodes and very small elements
164
+ const isMeaningfulElement = (el) => {
165
+ const rect = el.getBoundingClientRect();
166
+ // Skip elements that are too small (likely text nodes or empty spans)
167
+ if (rect.width < 5 && rect.height < 5) {
168
+ return false;
169
+ }
170
+ // Prefer elements with explicit metadata, IDs, or meaningful classes
171
+ return !!(el.getAttribute("data-inspection-name") ||
172
+ el.id ||
173
+ el.className ||
174
+ el.tagName !== "SPAN" && el.tagName !== "DIV");
175
+ };
176
+ // First pass: Look for elements with explicit inspection metadata
177
+ // This ensures we always find the same component regardless of which child is hovered
178
+ while (current) {
179
+ // Ensure element is still in the DOM
180
+ if (!document.body.contains(current)) {
181
+ break;
182
+ }
183
+ // Check if this element has explicit inspection metadata
184
+ const hasExplicitMetadata = current.getAttribute("data-inspection-name") &&
185
+ current.getAttribute("data-inspection-id");
186
+ if (hasExplicitMetadata) {
187
+ const metadata = parseInspectionMetadata(current);
188
+ if (metadata) {
189
+ elementWithExplicitMetadata = current;
190
+ metadataWithExplicitId = metadata;
191
+ break; // Found explicit metadata, use it
192
+ }
193
+ }
194
+ current = current.parentElement;
195
+ // Stop at body to avoid inspecting the entire page
196
+ if (current && (current.tagName === "BODY" || current.tagName === "HTML")) {
197
+ break;
198
+ }
199
+ }
200
+ // If we found explicit metadata, use it (this ensures consistent IDs)
201
+ if (elementWithExplicitMetadata && metadataWithExplicitId) {
202
+ if (process.env.NODE_ENV === "development") {
203
+ console.log("[Inspection] Found element with explicit metadata:", metadataWithExplicitId.componentName);
204
+ }
205
+ setHoveredComponent(metadataWithExplicitId, elementWithExplicitMetadata);
206
+ return;
207
+ }
208
+ // Second pass: If no explicit metadata found, look for meaningful elements
209
+ // Reset current to target for second pass
210
+ current = target;
211
+ while (current) {
212
+ // Ensure element is still in the DOM
213
+ if (!document.body.contains(current)) {
214
+ break;
215
+ }
216
+ // Check if this is a meaningful element
217
+ if (isMeaningfulElement(current)) {
218
+ const metadata = parseInspectionMetadata(current);
219
+ if (metadata) {
220
+ // Keep the first meaningful element (closest to target)
221
+ if (!bestElement) {
222
+ bestElement = current;
223
+ bestMetadata = metadata;
224
+ }
225
+ }
226
+ }
227
+ current = current.parentElement;
228
+ // Stop at body to avoid inspecting the entire page
229
+ if (current && (current.tagName === "BODY" || current.tagName === "HTML")) {
230
+ break;
231
+ }
232
+ }
233
+ if (bestElement && bestMetadata) {
234
+ if (process.env.NODE_ENV === "development") {
235
+ console.log("[Inspection] Found element:", bestMetadata.componentName);
236
+ }
237
+ setHoveredComponent(bestMetadata, bestElement);
238
+ }
239
+ else {
240
+ // Fallback: show info for the target element itself
241
+ const fallbackMetadata = parseInspectionMetadata(target);
242
+ if (fallbackMetadata) {
243
+ setHoveredComponent(fallbackMetadata, target);
244
+ }
245
+ else {
246
+ setHoveredComponent(null, null);
247
+ }
248
+ }
249
+ };
250
+ /**
251
+ * Setup global mouse event listener for automatic inspection detection
252
+ * This works with components that have data-inspection-* attributes
253
+ */
254
+ export const setupAutoInspection = (setHoveredComponent, isInspectionActive, isLocked) => {
255
+ if (process.env.NODE_ENV !== "development") {
256
+ return () => { }; // No-op in production
257
+ }
258
+ // Detect if device supports touch
259
+ const isTouchDevice = 'ontouchstart' in window ||
260
+ navigator.maxTouchPoints > 0 ||
261
+ /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
262
+ const handleMouseMove = (e) => {
263
+ if (!isInspectionActive) {
264
+ return;
265
+ }
266
+ // If locked, don't update on mouse move - keep current tooltip fixed
267
+ if (isLocked) {
268
+ return;
269
+ }
270
+ const target = e.target;
271
+ inspectElement(target, isInspectionActive, isLocked, setHoveredComponent);
272
+ };
273
+ // Touch move handler for mobile devices
274
+ const handleTouchMove = (e) => {
275
+ // Only process if inspection is active and we have touches
276
+ if (!isInspectionActive || e.touches.length === 0) {
277
+ return;
278
+ }
279
+ // Get the element under the first touch point
280
+ const touch = e.touches[0];
281
+ const target = document.elementFromPoint(touch.clientX, touch.clientY);
282
+ if (target) {
283
+ inspectElement(target, isInspectionActive, isLocked, setHoveredComponent);
284
+ }
285
+ };
286
+ // Touch start handler for mobile - inspect on touch
287
+ const handleTouchStart = (e) => {
288
+ // Only process if inspection is active and we have touches
289
+ // Skip if it's a 3-finger touch (used for activation)
290
+ if (!isInspectionActive || e.touches.length === 3) {
291
+ return;
292
+ }
293
+ // Get the element under the first touch point
294
+ const touch = e.touches[0];
295
+ const target = document.elementFromPoint(touch.clientX, touch.clientY);
296
+ if (target) {
297
+ inspectElement(target, isInspectionActive, isLocked, setHoveredComponent);
298
+ }
299
+ };
300
+ const handleMouseLeave = () => {
301
+ if (isInspectionActive && !isLocked) {
302
+ setHoveredComponent(null, null);
303
+ }
304
+ };
305
+ const handleTouchEnd = () => {
306
+ // Clear inspection on touch end (unless locked)
307
+ if (isInspectionActive && !isLocked) {
308
+ setHoveredComponent(null, null);
309
+ }
310
+ };
311
+ // Add mouse event listeners
312
+ window.addEventListener("mousemove", handleMouseMove);
313
+ document.addEventListener("mouseleave", handleMouseLeave);
314
+ // Add touch event listeners for mobile devices
315
+ if (isTouchDevice) {
316
+ window.addEventListener("touchmove", handleTouchMove, { passive: true });
317
+ window.addEventListener("touchstart", handleTouchStart, { passive: true });
318
+ window.addEventListener("touchend", handleTouchEnd, { passive: true });
319
+ }
320
+ return () => {
321
+ window.removeEventListener("mousemove", handleMouseMove);
322
+ document.removeEventListener("mouseleave", handleMouseLeave);
323
+ if (isTouchDevice) {
324
+ window.removeEventListener("touchmove", handleTouchMove);
325
+ window.removeEventListener("touchstart", handleTouchStart);
326
+ window.removeEventListener("touchend", handleTouchEnd);
327
+ }
328
+ };
329
+ };