@zargaryanvh/react-component-inspector 1.0.0 → 1.0.2
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,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import React, { createContext, useContext, useState, useCallback, useEffect } from "react";
|
|
2
|
+
import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from "react";
|
|
3
3
|
import { setInspectionActive } from "./inspectionInterceptors";
|
|
4
4
|
import { setupAutoInspection } from "./autoInspection";
|
|
5
5
|
const InspectionContext = createContext(undefined);
|
|
@@ -11,34 +11,136 @@ export const InspectionProvider = ({ children }) => {
|
|
|
11
11
|
const [isLocked, setIsLocked] = useState(false);
|
|
12
12
|
const [hoveredComponent, setHoveredComponentState] = useState(null);
|
|
13
13
|
const [hoveredElement, setHoveredElement] = useState(null);
|
|
14
|
-
//
|
|
14
|
+
// Use refs to always access latest state values in event handlers
|
|
15
|
+
const isInspectionActiveRef = useRef(isInspectionActive);
|
|
16
|
+
const isLockedRef = useRef(isLocked);
|
|
17
|
+
const hoveredComponentRef = useRef(hoveredComponent);
|
|
18
|
+
const hKeyPressedRef = useRef(false); // Track if H key is currently being held
|
|
19
|
+
// Mobile touch support
|
|
20
|
+
const longPressTimerRef = useRef(null);
|
|
21
|
+
const touchStartTimeRef = useRef(0);
|
|
22
|
+
const lastTapRef = useRef(0);
|
|
23
|
+
const isMobileRef = useRef(false);
|
|
24
|
+
// Detect mobile device
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const checkMobile = () => {
|
|
27
|
+
if (typeof window === 'undefined')
|
|
28
|
+
return false;
|
|
29
|
+
return 'ontouchstart' in window ||
|
|
30
|
+
navigator.maxTouchPoints > 0 ||
|
|
31
|
+
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
32
|
+
};
|
|
33
|
+
isMobileRef.current = checkMobile();
|
|
34
|
+
}, []);
|
|
35
|
+
// Keep refs in sync with state
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
isInspectionActiveRef.current = isInspectionActive;
|
|
38
|
+
}, [isInspectionActive]);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
isLockedRef.current = isLocked;
|
|
41
|
+
}, [isLocked]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
hoveredComponentRef.current = hoveredComponent;
|
|
44
|
+
}, [hoveredComponent]);
|
|
45
|
+
// Mobile touch handlers for activation and locking
|
|
15
46
|
React.useEffect(() => {
|
|
16
47
|
if (process.env.NODE_ENV !== "development") {
|
|
17
48
|
return; // Only in development
|
|
18
49
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
50
|
+
// Long-press to activate inspection mode (mobile)
|
|
51
|
+
const handleTouchStart = (e) => {
|
|
52
|
+
if (!isMobileRef.current)
|
|
53
|
+
return;
|
|
54
|
+
// Only activate if touching with 3 fingers (to avoid accidental activation)
|
|
55
|
+
if (e.touches.length === 3) {
|
|
56
|
+
touchStartTimeRef.current = Date.now();
|
|
57
|
+
longPressTimerRef.current = setTimeout(() => {
|
|
58
|
+
setIsInspectionActive(true);
|
|
59
|
+
setInspectionActive(true);
|
|
60
|
+
if (process.env.NODE_ENV === "development") {
|
|
61
|
+
console.log("[Inspection] Activated (mobile) - Long-press with 3 fingers. Double-tap tooltip to lock.");
|
|
62
|
+
}
|
|
63
|
+
}, 800); // 800ms long-press
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const handleTouchEnd = (e) => {
|
|
67
|
+
if (!isMobileRef.current)
|
|
68
|
+
return;
|
|
69
|
+
// Clear long-press timer
|
|
70
|
+
if (longPressTimerRef.current) {
|
|
71
|
+
clearTimeout(longPressTimerRef.current);
|
|
72
|
+
longPressTimerRef.current = null;
|
|
73
|
+
}
|
|
74
|
+
// Double-tap detection for locking (on tooltip or component)
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const timeSinceLastTap = now - lastTapRef.current;
|
|
77
|
+
if (timeSinceLastTap < 300 && timeSinceLastTap > 0) {
|
|
78
|
+
// Double-tap detected
|
|
79
|
+
if (isInspectionActiveRef.current && hoveredComponentRef.current && !isLockedRef.current) {
|
|
25
80
|
setIsLocked(true);
|
|
26
81
|
if (process.env.NODE_ENV === "development") {
|
|
27
|
-
console.log("[Inspection] Tooltip
|
|
82
|
+
console.log("[Inspection] Tooltip LOCKED (mobile) - Double-tap again to unlock.");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (isLockedRef.current) {
|
|
86
|
+
// Unlock on double-tap when locked
|
|
87
|
+
setIsLocked(false);
|
|
88
|
+
if (process.env.NODE_ENV === "development") {
|
|
89
|
+
console.log("[Inspection] Tooltip UNLOCKED (mobile) - inspection continues.");
|
|
28
90
|
}
|
|
29
91
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
92
|
+
lastTapRef.current = 0; // Reset
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
lastTapRef.current = now;
|
|
96
|
+
}
|
|
97
|
+
// Deactivate if 3-finger touch ends and inspection was active
|
|
98
|
+
if (e.touches.length === 0 && isInspectionActiveRef.current) {
|
|
99
|
+
const touchDuration = Date.now() - touchStartTimeRef.current;
|
|
100
|
+
// Only deactivate if it was a quick tap (not a long-press that activated)
|
|
101
|
+
if (touchDuration < 800) {
|
|
102
|
+
setIsInspectionActive(false);
|
|
33
103
|
setIsLocked(false);
|
|
104
|
+
hKeyPressedRef.current = false;
|
|
105
|
+
setInspectionActive(false);
|
|
106
|
+
setHoveredComponentState(null);
|
|
107
|
+
setHoveredElement(null);
|
|
34
108
|
if (process.env.NODE_ENV === "development") {
|
|
35
|
-
console.log("[Inspection]
|
|
109
|
+
console.log("[Inspection] Deactivated (mobile)");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const handleTouchCancel = () => {
|
|
115
|
+
if (!isMobileRef.current)
|
|
116
|
+
return;
|
|
117
|
+
if (longPressTimerRef.current) {
|
|
118
|
+
clearTimeout(longPressTimerRef.current);
|
|
119
|
+
longPressTimerRef.current = null;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
// Track CTRL key state and CTRL+H for locking (desktop)
|
|
123
|
+
const handleKeyDown = (e) => {
|
|
124
|
+
// H key pressed while CTRL is held - lock tooltip position
|
|
125
|
+
if (e.key.toLowerCase() === "h" && e.ctrlKey) {
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
e.stopPropagation();
|
|
128
|
+
// Ignore repeated keydown events (when key is held down)
|
|
129
|
+
if (e.repeat) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Only lock if inspection is active, we have a hovered component, and H is not already being held
|
|
133
|
+
if (isInspectionActiveRef.current && hoveredComponentRef.current && !hKeyPressedRef.current) {
|
|
134
|
+
hKeyPressedRef.current = true;
|
|
135
|
+
setIsLocked(true);
|
|
136
|
+
if (process.env.NODE_ENV === "development") {
|
|
137
|
+
console.log("[Inspection] Tooltip LOCKED - Hold H to keep locked. Release H to unlock.");
|
|
36
138
|
}
|
|
37
139
|
}
|
|
38
140
|
return;
|
|
39
141
|
}
|
|
40
142
|
// CTRL key pressed
|
|
41
|
-
if (e.key === "Control"
|
|
143
|
+
if (e.key === "Control" && !e.repeat) {
|
|
42
144
|
setIsInspectionActive(true);
|
|
43
145
|
setInspectionActive(true);
|
|
44
146
|
if (process.env.NODE_ENV === "development") {
|
|
@@ -47,10 +149,29 @@ export const InspectionProvider = ({ children }) => {
|
|
|
47
149
|
}
|
|
48
150
|
};
|
|
49
151
|
const handleKeyUp = (e) => {
|
|
152
|
+
// H key released - unlock tooltip but keep inspection active if CTRL is still held
|
|
153
|
+
if (e.key.toLowerCase() === "h") {
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
e.stopPropagation();
|
|
156
|
+
// Only process if H was actually being held
|
|
157
|
+
if (hKeyPressedRef.current) {
|
|
158
|
+
const wasLocked = isLockedRef.current;
|
|
159
|
+
hKeyPressedRef.current = false;
|
|
160
|
+
// Only unlock if we were locked and CTRL is still held
|
|
161
|
+
if (wasLocked && e.ctrlKey && isInspectionActiveRef.current) {
|
|
162
|
+
setIsLocked(false);
|
|
163
|
+
if (process.env.NODE_ENV === "development") {
|
|
164
|
+
console.log("[Inspection] Tooltip UNLOCKED - inspection continues while CTRL is held.");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
50
170
|
// CTRL key released - unlock and clear
|
|
51
|
-
if (e.key === "Control"
|
|
171
|
+
if (e.key === "Control") {
|
|
52
172
|
setIsInspectionActive(false);
|
|
53
173
|
setIsLocked(false);
|
|
174
|
+
hKeyPressedRef.current = false;
|
|
54
175
|
setInspectionActive(false);
|
|
55
176
|
setHoveredComponentState(null);
|
|
56
177
|
setHoveredElement(null);
|
|
@@ -58,15 +179,25 @@ export const InspectionProvider = ({ children }) => {
|
|
|
58
179
|
console.log("[Inspection] Deactivated");
|
|
59
180
|
}
|
|
60
181
|
}
|
|
61
|
-
// Note: H key release doesn't unlock anymore - use H key press to toggle lock state
|
|
62
182
|
};
|
|
63
183
|
window.addEventListener("keydown", handleKeyDown);
|
|
64
184
|
window.addEventListener("keyup", handleKeyUp);
|
|
185
|
+
// Mobile touch events
|
|
186
|
+
window.addEventListener("touchstart", handleTouchStart, { passive: true });
|
|
187
|
+
window.addEventListener("touchend", handleTouchEnd, { passive: true });
|
|
188
|
+
window.addEventListener("touchcancel", handleTouchCancel, { passive: true });
|
|
65
189
|
return () => {
|
|
66
190
|
window.removeEventListener("keydown", handleKeyDown);
|
|
67
191
|
window.removeEventListener("keyup", handleKeyUp);
|
|
192
|
+
window.removeEventListener("touchstart", handleTouchStart);
|
|
193
|
+
window.removeEventListener("touchend", handleTouchEnd);
|
|
194
|
+
window.removeEventListener("touchcancel", handleTouchCancel);
|
|
195
|
+
// Cleanup timers
|
|
196
|
+
if (longPressTimerRef.current) {
|
|
197
|
+
clearTimeout(longPressTimerRef.current);
|
|
198
|
+
}
|
|
68
199
|
};
|
|
69
|
-
}, [
|
|
200
|
+
}, []); // Empty deps - refs handle state access
|
|
70
201
|
const setHoveredComponent = useCallback((component, element) => {
|
|
71
202
|
if (process.env.NODE_ENV !== "development") {
|
|
72
203
|
return; // Only in development
|
|
@@ -224,7 +224,7 @@ export const InspectionTooltip = () => {
|
|
|
224
224
|
backdropFilter: "blur(8px)",
|
|
225
225
|
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
226
226
|
transition: stablePosition ? "none" : "left 0.1s ease-out, top 0.1s ease-out",
|
|
227
|
-
}, children: [_jsxs(Box, { sx: { display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 1 }, children: [_jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 1 }, children: [_jsx(Typography, { variant: "subtitle2", sx: { color: "#fff", fontWeight: 600, fontSize: "0.875rem" }, children: "Component Inspector" }), isLocked && (_jsx(Typography, { variant: "caption", sx: { color: "#4caf50", fontSize: "0.7rem", fontStyle: "italic" }, children: "(Locked - Release
|
|
227
|
+
}, children: [_jsxs(Box, { sx: { display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 1 }, children: [_jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 1 }, children: [_jsx(Typography, { variant: "subtitle2", sx: { color: "#fff", fontWeight: 600, fontSize: "0.875rem" }, children: "Component Inspector" }), isLocked && (_jsx(Typography, { variant: "caption", sx: { color: "#4caf50", fontSize: "0.7rem", fontStyle: "italic" }, children: "(Locked - Release H to unlock)" }))] }), _jsx(MuiTooltip, { title: copied ? "Copied!" : "Copy metadata", children: _jsx(IconButton, { size: "small", onClick: handleCopy, sx: {
|
|
228
228
|
color: copied ? "#4caf50" : "#fff",
|
|
229
229
|
"&:hover": { backgroundColor: "rgba(255, 255, 255, 0.1)" },
|
|
230
230
|
p: 0.5,
|
|
@@ -29,6 +29,26 @@ export const InspectionWrapper = ({ componentName, variant, role, usagePath, pro
|
|
|
29
29
|
return;
|
|
30
30
|
setHoveredComponent(null, null);
|
|
31
31
|
};
|
|
32
|
+
// Touch handlers for mobile
|
|
33
|
+
const handleTouchStart = (e) => {
|
|
34
|
+
if (!isInspectionActive)
|
|
35
|
+
return;
|
|
36
|
+
const target = e.currentTarget;
|
|
37
|
+
const metadata = {
|
|
38
|
+
componentName,
|
|
39
|
+
componentId: generateComponentId(componentName, instanceIndex),
|
|
40
|
+
variant,
|
|
41
|
+
role,
|
|
42
|
+
usagePath,
|
|
43
|
+
instanceIndex,
|
|
44
|
+
propsSignature: formatPropsSignature(props),
|
|
45
|
+
sourceFile,
|
|
46
|
+
};
|
|
47
|
+
setHoveredComponent(metadata, target);
|
|
48
|
+
};
|
|
49
|
+
const handleTouchEnd = () => {
|
|
50
|
+
// Don't clear on touch end - let autoInspection handle it
|
|
51
|
+
};
|
|
32
52
|
// Clone the child element and add inspection handlers
|
|
33
53
|
if (!React.isValidElement(children)) {
|
|
34
54
|
return _jsx(_Fragment, { children: children });
|
|
@@ -36,6 +56,8 @@ export const InspectionWrapper = ({ componentName, variant, role, usagePath, pro
|
|
|
36
56
|
const existingProps = (children.props || {});
|
|
37
57
|
const existingOnMouseEnter = existingProps.onMouseEnter;
|
|
38
58
|
const existingOnMouseLeave = existingProps.onMouseLeave;
|
|
59
|
+
const existingOnTouchStart = existingProps.onTouchStart;
|
|
60
|
+
const existingOnTouchEnd = existingProps.onTouchEnd;
|
|
39
61
|
const childWithProps = React.cloneElement(children, {
|
|
40
62
|
onMouseEnter: (e) => {
|
|
41
63
|
handleMouseEnter(e);
|
|
@@ -49,6 +71,18 @@ export const InspectionWrapper = ({ componentName, variant, role, usagePath, pro
|
|
|
49
71
|
existingOnMouseLeave(e);
|
|
50
72
|
}
|
|
51
73
|
},
|
|
74
|
+
onTouchStart: (e) => {
|
|
75
|
+
handleTouchStart(e);
|
|
76
|
+
if (existingOnTouchStart) {
|
|
77
|
+
existingOnTouchStart(e);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
onTouchEnd: (e) => {
|
|
81
|
+
handleTouchEnd();
|
|
82
|
+
if (existingOnTouchEnd) {
|
|
83
|
+
existingOnTouchEnd(e);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
52
86
|
"data-inspection-id": generateComponentId(componentName, instanceIndex),
|
|
53
87
|
"data-inspection-name": componentName,
|
|
54
88
|
});
|
package/dist/autoInspection.js
CHANGED
|
@@ -130,6 +130,114 @@ export const parseInspectionMetadata = (element) => {
|
|
|
130
130
|
sourceFile: "DOM",
|
|
131
131
|
};
|
|
132
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
|
+
};
|
|
133
241
|
/**
|
|
134
242
|
* Setup global mouse event listener for automatic inspection detection
|
|
135
243
|
* This works with components that have data-inspection-* attributes
|
|
@@ -138,6 +246,10 @@ export const setupAutoInspection = (setHoveredComponent, isInspectionActive, isL
|
|
|
138
246
|
if (process.env.NODE_ENV !== "development") {
|
|
139
247
|
return () => { }; // No-op in production
|
|
140
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);
|
|
141
253
|
const handleMouseMove = (e) => {
|
|
142
254
|
if (!isInspectionActive) {
|
|
143
255
|
return;
|
|
@@ -147,101 +259,33 @@ export const setupAutoInspection = (setHoveredComponent, isInspectionActive, isL
|
|
|
147
259
|
return;
|
|
148
260
|
}
|
|
149
261
|
const target = e.target;
|
|
150
|
-
|
|
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) {
|
|
151
268
|
return;
|
|
152
269
|
}
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
let elementWithExplicitMetadata = null;
|
|
159
|
-
let metadataWithExplicitId = null;
|
|
160
|
-
// Skip text nodes and very small elements
|
|
161
|
-
const isMeaningfulElement = (el) => {
|
|
162
|
-
const rect = el.getBoundingClientRect();
|
|
163
|
-
// Skip elements that are too small (likely text nodes or empty spans)
|
|
164
|
-
if (rect.width < 5 && rect.height < 5) {
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
// Prefer elements with explicit metadata, IDs, or meaningful classes
|
|
168
|
-
return !!(el.getAttribute("data-inspection-name") ||
|
|
169
|
-
el.id ||
|
|
170
|
-
el.className ||
|
|
171
|
-
el.tagName !== "SPAN" && el.tagName !== "DIV");
|
|
172
|
-
};
|
|
173
|
-
// First pass: Look for elements with explicit inspection metadata
|
|
174
|
-
// This ensures we always find the same component regardless of which child is hovered
|
|
175
|
-
while (current) {
|
|
176
|
-
// Ensure element is still in the DOM
|
|
177
|
-
if (!document.body.contains(current)) {
|
|
178
|
-
break;
|
|
179
|
-
}
|
|
180
|
-
// Check if this element has explicit inspection metadata
|
|
181
|
-
const hasExplicitMetadata = current.getAttribute("data-inspection-name") &&
|
|
182
|
-
current.getAttribute("data-inspection-id");
|
|
183
|
-
if (hasExplicitMetadata) {
|
|
184
|
-
const metadata = parseInspectionMetadata(current);
|
|
185
|
-
if (metadata) {
|
|
186
|
-
elementWithExplicitMetadata = current;
|
|
187
|
-
metadataWithExplicitId = metadata;
|
|
188
|
-
break; // Found explicit metadata, use it
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
current = current.parentElement;
|
|
192
|
-
// Stop at body to avoid inspecting the entire page
|
|
193
|
-
if (current && (current.tagName === "BODY" || current.tagName === "HTML")) {
|
|
194
|
-
break;
|
|
195
|
-
}
|
|
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);
|
|
196
275
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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) {
|
|
203
282
|
return;
|
|
204
283
|
}
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (!document.body.contains(current)) {
|
|
211
|
-
break;
|
|
212
|
-
}
|
|
213
|
-
// Check if this is a meaningful element
|
|
214
|
-
if (isMeaningfulElement(current)) {
|
|
215
|
-
const metadata = parseInspectionMetadata(current);
|
|
216
|
-
if (metadata) {
|
|
217
|
-
// Keep the first meaningful element (closest to target)
|
|
218
|
-
if (!bestElement) {
|
|
219
|
-
bestElement = current;
|
|
220
|
-
bestMetadata = metadata;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
current = current.parentElement;
|
|
225
|
-
// Stop at body to avoid inspecting the entire page
|
|
226
|
-
if (current && (current.tagName === "BODY" || current.tagName === "HTML")) {
|
|
227
|
-
break;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
if (bestElement && bestMetadata) {
|
|
231
|
-
if (process.env.NODE_ENV === "development") {
|
|
232
|
-
console.log("[Inspection] Found element:", bestMetadata.componentName);
|
|
233
|
-
}
|
|
234
|
-
setHoveredComponent(bestMetadata, bestElement);
|
|
235
|
-
}
|
|
236
|
-
else {
|
|
237
|
-
// Fallback: show info for the target element itself
|
|
238
|
-
const fallbackMetadata = parseInspectionMetadata(target);
|
|
239
|
-
if (fallbackMetadata) {
|
|
240
|
-
setHoveredComponent(fallbackMetadata, target);
|
|
241
|
-
}
|
|
242
|
-
else {
|
|
243
|
-
setHoveredComponent(null, null);
|
|
244
|
-
}
|
|
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);
|
|
245
289
|
}
|
|
246
290
|
};
|
|
247
291
|
const handleMouseLeave = () => {
|
|
@@ -249,10 +293,28 @@ export const setupAutoInspection = (setHoveredComponent, isInspectionActive, isL
|
|
|
249
293
|
setHoveredComponent(null, null);
|
|
250
294
|
}
|
|
251
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
|
|
252
303
|
window.addEventListener("mousemove", handleMouseMove);
|
|
253
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
|
+
}
|
|
254
311
|
return () => {
|
|
255
312
|
window.removeEventListener("mousemove", handleMouseMove);
|
|
256
313
|
document.removeEventListener("mouseleave", handleMouseLeave);
|
|
314
|
+
if (isTouchDevice) {
|
|
315
|
+
window.removeEventListener("touchmove", handleTouchMove);
|
|
316
|
+
window.removeEventListener("touchstart", handleTouchStart);
|
|
317
|
+
window.removeEventListener("touchend", handleTouchEnd);
|
|
318
|
+
}
|
|
257
319
|
};
|
|
258
320
|
};
|
|
@@ -27,6 +27,8 @@ export declare const useInspectionMetadata: (config: {
|
|
|
27
27
|
}) => {
|
|
28
28
|
onMouseEnter: (e: React.MouseEvent) => void;
|
|
29
29
|
onMouseLeave: () => void;
|
|
30
|
+
onTouchStart: (e: React.TouchEvent) => void;
|
|
31
|
+
onTouchEnd: () => void;
|
|
30
32
|
"data-inspection-id": string;
|
|
31
33
|
"data-inspection-name": string;
|
|
32
34
|
};
|
|
@@ -43,9 +43,24 @@ export const useInspectionMetadata = (config) => {
|
|
|
43
43
|
return;
|
|
44
44
|
setHoveredComponent(null, null);
|
|
45
45
|
};
|
|
46
|
+
// Touch handlers for mobile
|
|
47
|
+
const handleTouchStart = (e) => {
|
|
48
|
+
if (!isInspectionActive)
|
|
49
|
+
return;
|
|
50
|
+
const target = e.currentTarget;
|
|
51
|
+
setHoveredComponent(metadata, target);
|
|
52
|
+
};
|
|
53
|
+
const handleTouchEnd = () => {
|
|
54
|
+
if (!isInspectionActive)
|
|
55
|
+
return;
|
|
56
|
+
// Don't clear on touch end - let autoInspection handle it
|
|
57
|
+
// This allows touch move to continue inspecting
|
|
58
|
+
};
|
|
46
59
|
return {
|
|
47
60
|
onMouseEnter: handleMouseEnter,
|
|
48
61
|
onMouseLeave: handleMouseLeave,
|
|
62
|
+
onTouchStart: handleTouchStart,
|
|
63
|
+
onTouchEnd: handleTouchEnd,
|
|
49
64
|
"data-inspection-id": metadata.componentId,
|
|
50
65
|
"data-inspection-name": config.componentName,
|
|
51
66
|
};
|
package/package.json
CHANGED