@zargaryanvh/react-component-inspector 1.0.1 → 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.
@@ -16,6 +16,22 @@ export const InspectionProvider = ({ children }) => {
16
16
  const isLockedRef = useRef(isLocked);
17
17
  const hoveredComponentRef = useRef(hoveredComponent);
18
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
+ }, []);
19
35
  // Keep refs in sync with state
20
36
  useEffect(() => {
21
37
  isInspectionActiveRef.current = isInspectionActive;
@@ -26,11 +42,84 @@ export const InspectionProvider = ({ children }) => {
26
42
  useEffect(() => {
27
43
  hoveredComponentRef.current = hoveredComponent;
28
44
  }, [hoveredComponent]);
29
- // Track CTRL key state and CTRL+H for locking
45
+ // Mobile touch handlers for activation and locking
30
46
  React.useEffect(() => {
31
47
  if (process.env.NODE_ENV !== "development") {
32
48
  return; // Only in development
33
49
  }
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) {
80
+ setIsLocked(true);
81
+ if (process.env.NODE_ENV === "development") {
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.");
90
+ }
91
+ }
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);
103
+ setIsLocked(false);
104
+ hKeyPressedRef.current = false;
105
+ setInspectionActive(false);
106
+ setHoveredComponentState(null);
107
+ setHoveredElement(null);
108
+ if (process.env.NODE_ENV === "development") {
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)
34
123
  const handleKeyDown = (e) => {
35
124
  // H key pressed while CTRL is held - lock tooltip position
36
125
  if (e.key.toLowerCase() === "h" && e.ctrlKey) {
@@ -93,9 +182,20 @@ export const InspectionProvider = ({ children }) => {
93
182
  };
94
183
  window.addEventListener("keydown", handleKeyDown);
95
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 });
96
189
  return () => {
97
190
  window.removeEventListener("keydown", handleKeyDown);
98
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
+ }
99
199
  };
100
200
  }, []); // Empty deps - refs handle state access
101
201
  const setHoveredComponent = useCallback((component, element) => {
@@ -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
  });
@@ -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
- if (!target || !document.body.contains(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) {
151
268
  return;
152
269
  }
153
- // Always show inspection for any element (not just ones with metadata)
154
- // Walk up the DOM tree to find a meaningful element to inspect
155
- let current = target;
156
- let bestElement = null;
157
- let bestMetadata = null;
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
- // If we found explicit metadata, use it (this ensures consistent IDs)
198
- if (elementWithExplicitMetadata && metadataWithExplicitId) {
199
- if (process.env.NODE_ENV === "development") {
200
- console.log("[Inspection] Found element with explicit metadata:", metadataWithExplicitId.componentName);
201
- }
202
- setHoveredComponent(metadataWithExplicitId, elementWithExplicitMetadata);
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
- // Second pass: If no explicit metadata found, look for meaningful elements
206
- // Reset current to target for second pass
207
- current = target;
208
- while (current) {
209
- // Ensure element is still in the DOM
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zargaryanvh/react-component-inspector",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "A development tool for inspecting React components with AI-friendly metadata extraction",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",