@zargaryanvh/react-component-inspector 1.0.3 → 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.
package/README.md CHANGED
@@ -23,13 +23,19 @@ When working with AI assistants to fix or modify frontend code, you often need t
23
23
 
24
24
  ### The Solution
25
25
  React Component Inspector provides:
26
- - **One-click component identification** - Just hold CTRL and hover
26
+ - **One-click component identification** - Hold CTRL and hover over any element
27
+ - **Margin, padding & gap inspection** - Hold CTRL+ALT to see spacing (margin, padding, flex/grid gap) and copy it for Cursor
27
28
  - **Rich metadata extraction** - Component name, props, file path, usage context
28
- - **AI-optimized format** - Copy-paste ready metadata for AI assistants
29
+ - **AI-optimized format** - Copy-paste ready metadata for AI assistants (Component, Margin, Padding, Gap)
30
+ - **How to find in code** - Tooltip shows DOM path, target element, and step-by-step how to find and modify in Cursor
29
31
  - **Zero production overhead** - Completely disabled in production builds
30
32
 
31
33
  ## 📊 What Data Does It Provide?
32
34
 
35
+ When you hold **CTRL** and hover over an element, a tooltip appears showing component metadata, copy buttons (Component / Margin / Padding / Gap), and the "How to find in code" section:
36
+
37
+ ![Inspector tooltip showing component data, copy buttons, and how to find in code](docs/screenshots/tooltip-preview.png)
38
+
33
39
  When you inspect a component, you get:
34
40
 
35
41
  ### Element Identification
@@ -50,27 +56,44 @@ When you inspect a component, you get:
50
56
  - Props signature (key props affecting behavior)
51
57
  - Source file path
52
58
 
53
- ### Example Output
59
+ ### Example Output (Copy Component)
54
60
  ```
61
+ === TYPE: COMPONENT ===
62
+
55
63
  === ELEMENT IDENTIFICATION ===
56
64
  Element Type: button
57
65
  Element Text/Label: "Save Transaction"
58
- Element ID: save-button
59
- Element Classes: MuiButton-root, primary-button
60
- CSS Selector: button#save-button
61
- Position: (450, 320)
62
- Size: 120x36px
66
+ ...
67
+ DOM Path: body > div#root > div.MuiBox-root > button.MuiButton-root
68
+ Parent: div.MuiBox-root
69
+ Role in tree: leaf element; parent: div.MuiBox-root
70
+
71
+ === TARGET (use this to instruct Cursor) ===
72
+ TARGET: The SaveButton with class MuiButton-root - position (450, 320), size 120x36px. It is the element in the DOM path above, NOT a child.
73
+
74
+ === HOW TO FIND AND MODIFY THIS COMPONENT IN CODE ===
75
+ 1. Use the DOM Path to locate the correct element...
76
+ 2. Open src/components/buttons/SaveButton.tsx and find the component...
77
+ 3. Modify the component's props, sx, or styles as needed.
63
78
 
64
79
  === COMPONENT METADATA ===
65
- Component Name: SaveButton
66
- Component ID: save-button-0
67
- Variant: primary
68
- Usage Path: ActivityPage > EditTransactionModal > TransactionForm
69
- Instance: 0
70
- Props: variant=primary, disabled=false
71
- Source File: src/components/buttons/SaveButton.tsx
80
+ ...
72
81
  ```
73
82
 
83
+ When you copy **Margin**, **Padding**, or **Gap**, you get the same structure with current values and instructions for changing that spacing in code (e.g. `sx={{ m: 1 }}`, `sx={{ p: 0.5 }}`, `sx={{ gap: 1 }}`).
84
+
85
+ ## 🔎 How to Find and Modify in Cursor
86
+
87
+ The tooltip shows a **“How to find in code”** section (same on desktop and mobile):
88
+
89
+ - **DOM Path** – Full path from `body` to the element (e.g. `body > div#root > div.MuiBox-root > main.MuiBox-root > …`)
90
+ - **Parent** – Direct parent selector
91
+ - **Role in tree** – e.g. “has 3 child element(s); parent: div.MuiStack-root”
92
+ - **TARGET** – One-line description for Cursor: “The Card with class MuiPaper-root – the element with margin 0px 0px 16px 0px. It is the PARENT in the DOM path above, NOT a child.”
93
+ - **Steps** – Numbered steps: use DOM path, open source file, then how to change (margin, padding, gap, or component)
94
+
95
+ When you click **Copy Component**, **Copy Margin**, **Copy Padding**, or **Copy Gap**, the clipboard gets the full block (element identification, DOM path, TARGET, and “How to find and modify in code”). Paste that into Cursor and ask it to change the component, margin, padding, or gap; the text is written so Cursor can locate the right element and apply the change.
96
+
74
97
  ## 🚀 How to Use This Data for AI-Powered Frontend Optimization
75
98
 
76
99
  ### 1. **Precise Component Targeting**
@@ -208,12 +231,33 @@ function MyComponent({ variant, children }) {
208
231
 
209
232
  ## 🎮 Usage
210
233
 
211
- 1. **Activate**: Hold the `CTRL` key (or `Cmd` on Mac)
212
- 2. **Inspect**: Hover over any component with inspection metadata
213
- 3. **View**: A tooltip appears showing component metadata
214
- 4. **Lock**: Press `CTRL+H` to lock the tooltip position
215
- 5. **Copy**: Click the copy icon to copy metadata to clipboard
216
- 6. **Deactivate**: Release `CTRL` to exit inspection mode
234
+ ### Keyboard shortcuts (desktop)
235
+
236
+ | Shortcut | Action |
237
+ |----------|--------|
238
+ | **Hold CTRL** | Enter inspection mode. Hover to inspect component (box/element). |
239
+ | **Release CTRL** | Exit inspection mode and clear tooltip. |
240
+ | **Hold CTRL+ALT** | Enter margin/padding mode. See orange (margin), green (padding), purple (gap). |
241
+ | **CTRL+H** | Lock tooltip position so you can click copy buttons. |
242
+ | **CTRL+Shift+R** | Hard refresh (browser default; not captured by the inspector). |
243
+
244
+ ### Basic flow
245
+
246
+ 1. **Activate**: Hold `CTRL` (or `Cmd` on Mac)
247
+ 2. **Inspect**: Hover over any element — tooltip shows component metadata and **How to find in code**
248
+ 3. **Copy**: Use the copy icon (component) or **Margin** / **Padding** / **Gap** buttons to copy metadata to clipboard
249
+ 4. **Lock** (optional): Press `CTRL+H` to lock the tooltip so it doesn’t follow the cursor
250
+ 5. **Deactivate**: Release `CTRL` to exit inspection mode
251
+
252
+ ### Margin, padding & gap inspection
253
+
254
+ - **Hold CTRL+ALT** (while holding CTRL) to enter margin/padding mode. Overlays show:
255
+ - **Orange** = margin (outside the element)
256
+ - **Green** = padding (inside the element)
257
+ - **Purple dashed** = parent’s flex/grid gap (when the parent has `gap`)
258
+ - If the current element has no margin, **dashed orange** outlines show ancestors that have margin; **click an outline** to inspect that ancestor.
259
+ - Use the **Margin**, **Padding**, or **Gap** copy buttons in the tooltip to copy Cursor-ready text for that spacing.
260
+ - The tooltip always shows **DOM Path**, **Parent**, **Role in tree**, **TARGET**, and **How to find** steps so you (and Cursor) know exactly which element to change.
217
261
 
218
262
  ## 🛡️ Safety Features
219
263
 
@@ -231,15 +275,18 @@ function MyComponent({ variant, children }) {
231
275
 
232
276
  ## 🎨 Features
233
277
 
234
- - ✅ Visual component highlighting
278
+ - ✅ Visual component highlighting (box/element outline)
279
+ - ✅ **Margin, padding & gap inspection** (hold CTRL+ALT) with orange/green/purple overlays
280
+ - ✅ **Ancestor margin detection** – when the element has no margin, dashed outlines show ancestors with margin; click to inspect
281
+ - ✅ **Copy Component / Margin / Padding / Gap** – Cursor-ready text with DOM path, TARGET, and how to find in code
282
+ - ✅ **“How to find in code” in tooltip** – DOM path, parent, role in tree, TARGET, and numbered steps (desktop & mobile)
235
283
  - ✅ Rich metadata extraction
236
- - ✅ Copy-to-clipboard functionality
237
- - ✅ Automatic component detection
284
+ - ✅ Automatic component detection (with or without `data-inspection-*`)
238
285
  - ✅ CSS selector generation
239
286
  - ✅ Usage path tracking
240
287
  - ✅ Instance indexing
241
288
  - ✅ Props signature extraction
242
- - ✅ Request blocking during inspection
289
+ - ✅ Request blocking during inspection (when CTRL is held)
243
290
  - ✅ Production-safe (zero overhead)
244
291
 
245
292
  ## 🤖 Designed by Cursor AI
@@ -8,19 +8,18 @@ const InspectionContext = createContext(undefined);
8
8
  */
9
9
  export const InspectionProvider = ({ children }) => {
10
10
  const [ctrlHeld, setCtrlHeld] = useState(false);
11
- const [isStickyInspection, setIsStickyInspection] = useState(false);
12
- const isInspectionActive = ctrlHeld || isStickyInspection;
13
- const [isLocked, setIsLocked] = useState(false);
14
11
  const [isMarginPaddingMode, setIsMarginPaddingMode] = useState(false);
12
+ const [isLocked, setIsLocked] = useState(false);
13
+ // Inspection active only when CTRL is held (or CTRL+ALT for margin/padding). Release CTRL = stop inspecting.
14
+ const isInspectionActive = ctrlHeld || isMarginPaddingMode;
15
15
  const [hoveredComponent, setHoveredComponentState] = useState(null);
16
16
  const [hoveredElement, setHoveredElement] = useState(null);
17
17
  // Use refs to always access latest state values in event handlers
18
18
  const isInspectionActiveRef = useRef(isInspectionActive);
19
19
  const isLockedRef = useRef(isLocked);
20
- const isStickyInspectionRef = useRef(isStickyInspection);
21
20
  const hoveredComponentRef = useRef(hoveredComponent);
22
21
  const hKeyPressedRef = useRef(false);
23
- // Touch support for locking only (3/4 finger activation removed - use Ctrl+Shift+R on laptop)
22
+ // Touch support for locking only (double-tap to lock tooltip)
24
23
  const lastTapRef = useRef(0);
25
24
  const [isMobile, setIsMobile] = useState(false);
26
25
  useEffect(() => {
@@ -43,10 +42,7 @@ export const InspectionProvider = ({ children }) => {
43
42
  useEffect(() => {
44
43
  hoveredComponentRef.current = hoveredComponent;
45
44
  }, [hoveredComponent]);
46
- useEffect(() => {
47
- isStickyInspectionRef.current = isStickyInspection;
48
- }, [isStickyInspection]);
49
- // Only block API/fetch when CTRL is physically held (not when sticky inspection is on)
45
+ // Only block API/fetch when CTRL is physically held
50
46
  useEffect(() => {
51
47
  setInspectionActive(ctrlHeld);
52
48
  }, [ctrlHeld]);
@@ -74,37 +70,18 @@ export const InspectionProvider = ({ children }) => {
74
70
  lastTapRef.current = now;
75
71
  }
76
72
  };
77
- // Keyboard: CTRL, CTRL+Shift+R (toggle inspection), CTRL+M (margin/padding), CTRL+H (lock)
73
+ // Keyboard: CTRL (hold = inspection), CTRL+Shift+R = hard refresh (do not capture), CTRL+ALT (margin/padding), CTRL+H (lock)
78
74
  const handleKeyDown = (e) => {
79
- // R key with CTRL+Shift - toggle inspection on/off (sticky, for mobile viewport on laptop)
75
+ // Do NOT capture CTRL+Shift+R - let browser do hard refresh
80
76
  if (e.key && e.key.toLowerCase() === "r" && e.ctrlKey && e.shiftKey) {
81
- e.preventDefault();
82
- e.stopPropagation();
83
- if (!e.repeat) {
84
- setIsStickyInspection(prev => {
85
- const next = !prev;
86
- if (!next) {
87
- setHoveredComponentState(null);
88
- setHoveredElement(null);
89
- setIsLocked(false);
90
- }
91
- if (process.env.NODE_ENV === "development") {
92
- console.log("[Inspection] Inspection toggled (Ctrl+Shift+R):", next ? "ON" : "OFF");
93
- }
94
- return next;
95
- });
96
- }
97
77
  return;
98
78
  }
99
- // M key with CTRL (hold) - margin/padding mode while held, inspect on mouse move
100
- if (e.key && e.key.toLowerCase() === "m" && e.ctrlKey && !e.shiftKey && !e.altKey) {
101
- e.preventDefault();
102
- e.stopPropagation();
103
- if (!e.repeat) {
104
- setIsMarginPaddingMode(true);
105
- setCtrlHeld(true);
106
- }
107
- return;
79
+ // CTRL+ALT (hold both) - margin/padding/box mode; desktop and mobile
80
+ if (e.key === "Control" && e.altKey && !e.repeat) {
81
+ setIsMarginPaddingMode(true);
82
+ }
83
+ if (e.key === "Alt" && e.ctrlKey && !e.repeat) {
84
+ setIsMarginPaddingMode(true);
108
85
  }
109
86
  // H key pressed while CTRL is held - lock tooltip position
110
87
  if (e.key && e.key.toLowerCase() === "h" && e.ctrlKey) {
@@ -148,20 +125,18 @@ export const InspectionProvider = ({ children }) => {
148
125
  }
149
126
  return;
150
127
  }
151
- // M key released - turn off margin/padding mode (hold-to-use, no toggle)
152
- if (e.key && e.key.toLowerCase() === "m") {
128
+ // ALT or CTRL released - turn off margin/padding mode (CTRL+ALT only, hold to use)
129
+ if (e.key === "Alt") {
153
130
  setIsMarginPaddingMode(false);
154
131
  }
155
- // CTRL key released - clear only if not in sticky mode
132
+ // CTRL key released - stop inspecting and clear state
156
133
  if (e.key === "Control") {
157
134
  setCtrlHeld(false);
158
135
  hKeyPressedRef.current = false;
159
- if (!isStickyInspectionRef.current) {
160
- setIsMarginPaddingMode(false);
161
- setIsLocked(false);
162
- setHoveredComponentState(null);
163
- setHoveredElement(null);
164
- }
136
+ setIsMarginPaddingMode(false);
137
+ setIsLocked(false);
138
+ setHoveredComponentState(null);
139
+ setHoveredElement(null);
165
140
  }
166
141
  };
167
142
  window.addEventListener("keydown", handleKeyDown);
@@ -19,7 +19,7 @@ const parsePx = (value) => {
19
19
  };
20
20
  /**
21
21
  * Highlight overlay that shows the boundary of the hovered component
22
- * When hold CTRL+M: orange = margin, green = padding, purple = gap (hold-to-use, release to exit)
22
+ * When hold CTRL+ALT: orange = margin, green = padding, purple = gap (hold-to-use, release to exit)
23
23
  * Otherwise: blue outline for component
24
24
  */
25
25
  const stripStyle = (left, top, width, height, color, bg) => ({
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState, useEffect, useRef, useMemo } from "react";
3
- import { Box, Paper, Typography, IconButton, Tooltip as MuiTooltip, Divider, Menu, MenuItem } from "@mui/material";
3
+ import { Box, Paper, Typography, IconButton, Tooltip as MuiTooltip, Divider } from "@mui/material";
4
4
  import ContentCopyIcon from "@mui/icons-material/ContentCopy";
5
5
  import { useInspection } from "./InspectionContext";
6
- import { formatMetadataForClipboard, getParentWithGap } from "./inspection";
6
+ import { formatMetadataForClipboard, getParentWithGap, getTooltipHowToFindInfo } from "./inspection";
7
7
  import { parseInspectionMetadata } from "./autoInspection";
8
8
  /**
9
9
  * Helper: Get element text content (first 100 chars)
@@ -87,7 +87,6 @@ export const InspectionTooltip = () => {
87
87
  const [position, setPosition] = useState({ x: 0, y: 0 });
88
88
  const [stablePosition, setStablePosition] = useState(null);
89
89
  const [copied, setCopied] = useState(null);
90
- const [copyMenuAnchor, setCopyMenuAnchor] = useState(null);
91
90
  const tooltipRef = useRef(null);
92
91
  // Update position based on cursor, but keep it stable when locked or mouse is near tooltip
93
92
  useEffect(() => {
@@ -137,8 +136,29 @@ export const InspectionTooltip = () => {
137
136
  propsSignature: "default",
138
137
  sourceFile: "DOM",
139
138
  } : null);
139
+ // On mobile (or when locking) there is no cursor; position tooltip over/near the inspected element
140
+ const positionFromElement = useMemo(() => {
141
+ if (!hoveredElement || !document.body.contains(hoveredElement))
142
+ return null;
143
+ const padding = 10;
144
+ const estimatedWidth = 400;
145
+ const estimatedHeight = 200;
146
+ const rect = hoveredElement.getBoundingClientRect();
147
+ // Prefer below and to the right of the element, clamped to viewport
148
+ let x = rect.left + 15;
149
+ let y = rect.bottom + 10;
150
+ if (x + estimatedWidth > window.innerWidth - padding)
151
+ x = window.innerWidth - estimatedWidth - padding;
152
+ if (x < padding)
153
+ x = padding;
154
+ if (y + estimatedHeight > window.innerHeight - padding)
155
+ y = rect.top - estimatedHeight - 10;
156
+ if (y < padding)
157
+ y = padding;
158
+ return { x, y };
159
+ }, [hoveredElement]);
140
160
  // Calculate adjusted position to avoid going off-screen
141
- // Use stable position if available, otherwise calculate from cursor position
161
+ // Use stable position if available; on mobile or when locked use element-based position so tooltip appears over inspected area
142
162
  const adjustedPosition = useMemo(() => {
143
163
  if (!displayComponent) {
144
164
  return { x: position.x + 15, y: position.y + 15 };
@@ -147,7 +167,11 @@ export const InspectionTooltip = () => {
147
167
  if (stablePosition) {
148
168
  return stablePosition;
149
169
  }
150
- // Calculate new position from cursor (only when stablePosition is null)
170
+ // Mobile or just locked: place tooltip near the inspected element (not cursor)
171
+ if ((isMobile || isLocked) && positionFromElement) {
172
+ return positionFromElement;
173
+ }
174
+ // Desktop: calculate from cursor position
151
175
  const padding = 10;
152
176
  let x = position.x + 15;
153
177
  let y = position.y + 15;
@@ -170,7 +194,7 @@ export const InspectionTooltip = () => {
170
194
  y = padding;
171
195
  }
172
196
  return { x, y };
173
- }, [position, displayComponent, stablePosition]);
197
+ }, [position, displayComponent, stablePosition, isMobile, isLocked, positionFromElement]);
174
198
  // Set stable position once when tooltip first appears for a new element, or when locked
175
199
  const lastComponentIdRef = useRef(null);
176
200
  useEffect(() => {
@@ -232,7 +256,6 @@ export const InspectionTooltip = () => {
232
256
  const metadata = parseInspectionMetadata(parentWithGap);
233
257
  if (!metadata)
234
258
  return;
235
- setCopyMenuAnchor(null);
236
259
  const text = formatMetadataForClipboard(metadata, parentWithGap, "gap");
237
260
  const showCopied = () => {
238
261
  setCopied("gap");
@@ -260,7 +283,6 @@ export const InspectionTooltip = () => {
260
283
  }
261
284
  if (!displayComponent)
262
285
  return;
263
- setCopyMenuAnchor(null);
264
286
  const text = formatMetadataForClipboard(displayComponent, hoveredElement, type);
265
287
  const showCopied = () => {
266
288
  setCopied(type);
@@ -292,13 +314,6 @@ export const InspectionTooltip = () => {
292
314
  setStablePosition(null);
293
315
  }
294
316
  }, [hoveredElement, isLocked]);
295
- // Close copy menu when tooltip is hidden so MUI Menu never has a detached anchorEl
296
- const tooltipVisible = isInspectionActive && !!displayComponent && !(isMobile && !isLocked);
297
- useEffect(() => {
298
- if (!tooltipVisible) {
299
- setCopyMenuAnchor(null);
300
- }
301
- }, [tooltipVisible]);
302
317
  const parentWithGap = hoveredElement ? getParentWithGap(hoveredElement) : null;
303
318
  // Show tooltip when inspection active; on mobile only when locked (H pressed or double-tap)
304
319
  if (!isInspectionActive || !displayComponent) {
@@ -320,15 +335,21 @@ export const InspectionTooltip = () => {
320
335
  backdropFilter: "blur(8px)",
321
336
  border: "1px solid rgba(255, 255, 255, 0.1)",
322
337
  transition: stablePosition ? "none" : "left 0.1s ease-out, top 0.1s ease-out",
323
- }, 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" }), isMarginPaddingMode && (_jsx(Typography, { variant: "caption", sx: { color: "#ff9800", fontSize: "0.65rem" }, children: "M/P mode" })), isLocked && (_jsx(Typography, { variant: "caption", sx: { color: "#4caf50", fontSize: "0.7rem", fontStyle: "italic" }, children: "(Locked - Release H to unlock)" }))] }), isMobile ? (_jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 0.75 }, children: [_jsx(IconButton, { size: "small", onPointerDown: (e) => { e.preventDefault(); handleCopy("component"); }, sx: { color: copied === "component" ? "#4caf50" : "#fff", p: 0.5, minWidth: 36, minHeight: 36 }, "aria-label": "Copy component", children: _jsx(ContentCopyIcon, { fontSize: "small" }) }), _jsxs(Box, { sx: {
338
+ }, 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" }), isMarginPaddingMode && (_jsx(Typography, { variant: "caption", sx: { color: "#ff9800", fontSize: "0.65rem" }, children: "M/P mode" })), isLocked && (_jsx(Typography, { variant: "caption", sx: { color: "#4caf50", fontSize: "0.7rem", fontStyle: "italic" }, children: "(Locked - Release H to unlock)" }))] }), _jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 0.75 }, children: [_jsx(MuiTooltip, { title: copied === "component" ? "Copied!" : "Copy component", children: _jsx(IconButton, { size: "small", onClick: (e) => { e.preventDefault(); handleCopy("component"); }, onPointerDown: (e) => { e.preventDefault(); handleCopy("component"); }, sx: {
339
+ color: copied === "component" ? "#4caf50" : "#fff",
340
+ "&:hover": { backgroundColor: "rgba(255, 255, 255, 0.1)" },
341
+ p: 0.5,
342
+ minWidth: 36,
343
+ minHeight: 36,
344
+ }, "aria-label": "Copy component", children: _jsx(ContentCopyIcon, { fontSize: "small" }) }) }), _jsxs(Box, { sx: {
324
345
  display: "flex",
325
346
  alignItems: "stretch",
326
347
  border: "1px solid rgba(255,255,255,0.25)",
327
348
  borderRadius: 1,
328
349
  overflow: "hidden",
329
- }, children: [_jsx(Typography, { component: "button", type: "button", variant: "caption", onPointerDown: (e) => { e.preventDefault(); handleCopy("margin"); }, sx: {
350
+ }, children: [_jsx(Typography, { component: "button", type: "button", variant: "caption", onClick: (e) => { e.preventDefault(); handleCopy("margin"); }, onPointerDown: (e) => { e.preventDefault(); handleCopy("margin"); }, sx: {
330
351
  color: copied === "margin" ? "#4caf50" : "rgba(255,255,255,0.9)",
331
- fontSize: "0.65rem",
352
+ fontSize: isMobile ? "0.65rem" : "0.7rem",
332
353
  cursor: "pointer",
333
354
  background: "none",
334
355
  border: "none",
@@ -336,9 +357,10 @@ export const InspectionTooltip = () => {
336
357
  padding: "4px 6px",
337
358
  fontFamily: "inherit",
338
359
  minWidth: 44,
339
- }, children: "Margin" }), _jsx(Typography, { component: "button", type: "button", variant: "caption", onPointerDown: (e) => { e.preventDefault(); handleCopy("padding"); }, sx: {
360
+ "&:hover": { backgroundColor: "rgba(255,255,255,0.08)" },
361
+ }, children: "Margin" }), _jsx(Typography, { component: "button", type: "button", variant: "caption", onClick: (e) => { e.preventDefault(); handleCopy("padding"); }, onPointerDown: (e) => { e.preventDefault(); handleCopy("padding"); }, sx: {
340
362
  color: copied === "padding" ? "#4caf50" : "rgba(255,255,255,0.9)",
341
- fontSize: "0.65rem",
363
+ fontSize: isMobile ? "0.65rem" : "0.7rem",
342
364
  cursor: "pointer",
343
365
  background: "none",
344
366
  border: "none",
@@ -346,20 +368,18 @@ export const InspectionTooltip = () => {
346
368
  padding: "4px 6px",
347
369
  fontFamily: "inherit",
348
370
  minWidth: 44,
349
- }, children: "Padding" }), parentWithGap && (_jsx(Typography, { component: "button", type: "button", variant: "caption", onPointerDown: (e) => { e.preventDefault(); handleCopy("gap"); }, sx: {
371
+ "&:hover": { backgroundColor: "rgba(255,255,255,0.08)" },
372
+ }, children: "Padding" }), parentWithGap && (_jsx(Typography, { component: "button", type: "button", variant: "caption", onClick: (e) => { e.preventDefault(); handleCopy("gap"); }, onPointerDown: (e) => { e.preventDefault(); handleCopy("gap"); }, sx: {
350
373
  color: copied === "gap" ? "#4caf50" : "rgba(156, 39, 176, 0.95)",
351
- fontSize: "0.65rem",
374
+ fontSize: isMobile ? "0.65rem" : "0.7rem",
352
375
  cursor: "pointer",
353
376
  background: "none",
354
377
  border: "none",
355
378
  padding: "4px 6px",
356
379
  fontFamily: "inherit",
357
380
  minWidth: 44,
358
- }, children: "Gap" }))] })] })) : (_jsxs(_Fragment, { children: [_jsx(MuiTooltip, { title: copied ? `Copied ${copied}!` : "Copy: Component / Margin / Padding", children: _jsx(IconButton, { size: "small", onClick: (e) => setCopyMenuAnchor(e.currentTarget), sx: {
359
- color: copied ? "#4caf50" : "#fff",
360
- "&:hover": { backgroundColor: "rgba(255, 255, 255, 0.1)" },
361
- p: 0.5,
362
- }, children: _jsx(ContentCopyIcon, { fontSize: "small" }) }) }), _jsxs(Menu, { anchorEl: copyMenuAnchor && document.body.contains(copyMenuAnchor) ? copyMenuAnchor : null, open: !!copyMenuAnchor && document.body.contains(copyMenuAnchor), onClose: () => setCopyMenuAnchor(null), anchorOrigin: { vertical: "bottom", horizontal: "right" }, transformOrigin: { vertical: "top", horizontal: "right" }, PaperProps: { sx: { backgroundColor: "rgba(18, 18, 18, 0.95)", minWidth: 140 } }, MenuListProps: { dense: true }, children: [_jsx(MenuItem, { onClick: () => handleCopy("component"), sx: { color: "#fff", fontSize: "0.8rem" }, children: "Copy Component" }), _jsx(MenuItem, { onClick: () => handleCopy("margin"), sx: { color: "#fff", fontSize: "0.8rem" }, children: "Copy Margin" }), _jsx(MenuItem, { onClick: () => handleCopy("padding"), sx: { color: "#fff", fontSize: "0.8rem" }, children: "Copy Padding" }), parentWithGap && (_jsx(MenuItem, { onClick: () => handleCopy("gap"), sx: { color: "#9c27b0", fontSize: "0.8rem" }, children: "Copy Gap (parent)" }))] })] }))] }), isMarginPaddingMode && hoveredElement && (() => {
381
+ "&:hover": { backgroundColor: "rgba(255,255,255,0.08)" },
382
+ }, children: "Gap" }))] })] })] }), isMarginPaddingMode && hoveredElement && (() => {
363
383
  try {
364
384
  const cs = window.getComputedStyle(hoveredElement);
365
385
  const mt = cs.marginTop;
@@ -378,5 +398,9 @@ export const InspectionTooltip = () => {
378
398
  catch {
379
399
  return (_jsxs(Box, { sx: { mb: 1, p: 0.75, borderRadius: 1, backgroundColor: "rgba(255,255,255,0.06)" }, children: [_jsx(Typography, { variant: "caption", sx: { color: "#ff9800" }, children: "Orange = Margin (outside)" }), _jsx(Typography, { variant: "caption", sx: { color: "#4caf50", display: "block" }, children: "Green = Padding (inside)" })] }));
380
400
  }
381
- })(), _jsxs(Box, { sx: { display: "flex", flexDirection: "column", gap: 1 }, children: [_jsx(Typography, { variant: "body2", sx: { color: "#fff", fontWeight: 500, mb: 0.5 }, children: displayComponent.componentName }), _jsx(Divider, { sx: { borderColor: "rgba(255, 255, 255, 0.1)", my: 0.5 } }), _jsx(ElementIdentificationSection, { element: hoveredElement, role: displayComponent.role }), _jsx(Divider, { sx: { borderColor: "rgba(255, 255, 255, 0.1)", my: 0.5 } }), _jsx(ComponentMetadataSection, { metadata: displayComponent })] })] }));
401
+ })(), _jsxs(Box, { sx: { display: "flex", flexDirection: "column", gap: 1 }, children: [_jsx(Typography, { variant: "body2", sx: { color: "#fff", fontWeight: 500, mb: 0.5 }, children: displayComponent.componentName }), _jsx(Divider, { sx: { borderColor: "rgba(255, 255, 255, 0.1)", my: 0.5 } }), _jsx(ElementIdentificationSection, { element: hoveredElement, role: displayComponent.role }), _jsx(Divider, { sx: { borderColor: "rgba(255, 255, 255, 0.1)", my: 0.5 } }), hoveredElement && (() => {
402
+ const howToType = isMarginPaddingMode ? "margin" : "component";
403
+ const info = getTooltipHowToFindInfo(displayComponent, hoveredElement, howToType);
404
+ return (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "caption", sx: { color: "rgba(255, 255, 255, 0.9)", fontSize: "0.7rem", fontWeight: 600 }, children: "=== HOW TO FIND IN CODE ===" }), _jsxs(Box, { sx: { display: "flex", flexDirection: "column", gap: 0.25, pl: 0.5 }, children: [_jsxs(Typography, { variant: "caption", sx: { color: "rgba(255, 255, 255, 0.7)", fontSize: "0.65rem", wordBreak: "break-all" }, children: [_jsx("strong", { children: "DOM Path:" }), " ", info.domPath] }), info.parent && (_jsxs(Typography, { variant: "caption", sx: { color: "rgba(255, 255, 255, 0.7)", fontSize: "0.65rem" }, children: [_jsx("strong", { children: "Parent:" }), " ", info.parent] })), _jsxs(Typography, { variant: "caption", sx: { color: "rgba(255, 255, 255, 0.7)", fontSize: "0.65rem" }, children: [_jsx("strong", { children: "Role in tree:" }), " ", info.roleInTree] }), _jsxs(Typography, { variant: "caption", sx: { color: "rgba(255, 255, 255, 0.85)", fontSize: "0.65rem", mt: 0.5 }, children: [_jsx("strong", { children: "TARGET:" }), " ", info.target] }), _jsx(Box, { component: "ol", sx: { m: 0, pl: 1.5, fontSize: "0.65rem", color: "rgba(255, 255, 255, 0.7)" }, children: info.howToFindSteps.map((step, i) => (_jsx(Typography, { component: "li", variant: "caption", sx: { color: "rgba(255, 255, 255, 0.7)", fontSize: "0.65rem", mb: 0.25 }, children: step }, i))) })] }), _jsx(Divider, { sx: { borderColor: "rgba(255, 255, 255, 0.1)", my: 0.5 } })] }));
405
+ })(), _jsx(ComponentMetadataSection, { metadata: displayComponent })] })] }));
382
406
  };
package/dist/index.d.ts CHANGED
@@ -5,6 +5,6 @@ export { InspectionOverlays } from './InspectionOverlays';
5
5
  export { InspectionWrapper, withInspection } from './InspectionWrapper';
6
6
  export { useInspectionMetadata } from './useInspectionMetadata';
7
7
  export { setupInterceptors, setInspectionActive, shouldBlockRequest } from './inspectionInterceptors';
8
- export { generateComponentId, formatPropsSignature, formatMetadataForClipboard, formatMarginForClipboard, formatPaddingForClipboard, formatGapForClipboard, getParentWithGap, getAncestorsWithMargin, getComponentName, getNextInstanceIndex } from './inspection';
8
+ export { generateComponentId, formatPropsSignature, formatMetadataForClipboard, formatMarginForClipboard, formatPaddingForClipboard, formatGapForClipboard, getParentWithGap, getAncestorsWithMargin, getTooltipHowToFindInfo, getComponentName, getNextInstanceIndex } from './inspection';
9
9
  export type { CopyType } from './inspection';
10
10
  export { setupAutoInspection, parseInspectionMetadata } from './autoInspection';
package/dist/index.js CHANGED
@@ -6,5 +6,5 @@ export { InspectionOverlays } from './InspectionOverlays';
6
6
  export { InspectionWrapper, withInspection } from './InspectionWrapper';
7
7
  export { useInspectionMetadata } from './useInspectionMetadata';
8
8
  export { setupInterceptors, setInspectionActive, shouldBlockRequest } from './inspectionInterceptors';
9
- export { generateComponentId, formatPropsSignature, formatMetadataForClipboard, formatMarginForClipboard, formatPaddingForClipboard, formatGapForClipboard, getParentWithGap, getAncestorsWithMargin, getComponentName, getNextInstanceIndex } from './inspection';
9
+ export { generateComponentId, formatPropsSignature, formatMetadataForClipboard, formatMarginForClipboard, formatPaddingForClipboard, formatGapForClipboard, getParentWithGap, getAncestorsWithMargin, getTooltipHowToFindInfo, getComponentName, getNextInstanceIndex } from './inspection';
10
10
  export { setupAutoInspection, parseInspectionMetadata } from './autoInspection';
@@ -30,6 +30,18 @@ export declare const getAncestorsWithMargin: (element: HTMLElement, maxCount?: n
30
30
  mb: number;
31
31
  ml: number;
32
32
  }>;
33
+ /**
34
+ * Build DOM path from body to element (helps Cursor identify exact element)
35
+ */
36
+ export declare const buildDomPath: (element: HTMLElement) => string;
37
+ /**
38
+ * Build parent element description
39
+ */
40
+ export declare const buildParentInfo: (element: HTMLElement) => string | null;
41
+ /**
42
+ * Build role/disambiguation (outer vs inner, position in tree)
43
+ */
44
+ export declare const buildRoleInTree: (element: HTMLElement, type: "margin" | "padding" | "component") => string;
33
45
  /**
34
46
  * Format metadata for clipboard with full element details
35
47
  */
@@ -46,6 +58,16 @@ export declare const formatPaddingForClipboard: (metadata: ComponentMetadata, el
46
58
  * Format gap info for clipboard (alias for formatMetadataForClipboard with type="gap")
47
59
  */
48
60
  export declare const formatGapForClipboard: (metadata: ComponentMetadata, element: HTMLElement) => string;
61
+ /**
62
+ * Get "how to find" display info for tooltip (same logic for desktop and mobile)
63
+ */
64
+ export declare const getTooltipHowToFindInfo: (metadata: ComponentMetadata, element: HTMLElement, type: CopyType) => {
65
+ domPath: string;
66
+ parent: string | null;
67
+ roleInTree: string;
68
+ target: string;
69
+ howToFindSteps: string[];
70
+ };
49
71
  /**
50
72
  * Get next instance index for a component
51
73
  */
@@ -103,7 +103,7 @@ export const getAncestorsWithMargin = (element, maxCount = 2) => {
103
103
  /**
104
104
  * Build DOM path from body to element (helps Cursor identify exact element)
105
105
  */
106
- const buildDomPath = (element) => {
106
+ export const buildDomPath = (element) => {
107
107
  const segments = [];
108
108
  let current = element;
109
109
  while (current && current !== document.body) {
@@ -122,7 +122,7 @@ const buildDomPath = (element) => {
122
122
  /**
123
123
  * Build parent element description
124
124
  */
125
- const buildParentInfo = (element) => {
125
+ export const buildParentInfo = (element) => {
126
126
  const parent = element.parentElement;
127
127
  if (!parent || parent === document.body)
128
128
  return null;
@@ -138,7 +138,7 @@ const buildParentInfo = (element) => {
138
138
  /**
139
139
  * Build role/disambiguation (outer vs inner, position in tree)
140
140
  */
141
- const buildRoleInTree = (element, type) => {
141
+ export const buildRoleInTree = (element, type) => {
142
142
  const parent = element.parentElement;
143
143
  const children = element.children;
144
144
  const childCount = Array.from(children).length;
@@ -400,6 +400,79 @@ export const formatPaddingForClipboard = (metadata, element) => {
400
400
  export const formatGapForClipboard = (metadata, element) => {
401
401
  return formatMetadataForClipboard(metadata, element, "gap");
402
402
  };
403
+ /**
404
+ * Get "how to find" display info for tooltip (same logic for desktop and mobile)
405
+ */
406
+ export const getTooltipHowToFindInfo = (metadata, element, type) => {
407
+ const domPath = buildDomPath(element);
408
+ const parent = buildParentInfo(element);
409
+ if (type === "component") {
410
+ const rect = element.getBoundingClientRect();
411
+ const classNameStr = element.className ? (typeof element.className === "string" ? element.className : String(element.className)) : "";
412
+ const firstClass = classNameStr.split(/\s+/).find((c) => c && (c.startsWith("Mui") || c.startsWith("css-")));
413
+ const desc = firstClass ? `${metadata.componentName} with class ${firstClass}` : metadata.componentName;
414
+ const target = `The ${desc} - position (${Math.round(rect.left)}, ${Math.round(rect.top)}), size ${Math.round(rect.width)}x${Math.round(rect.height)}px. It is the element in the DOM path above, NOT a child.`;
415
+ const steps = [
416
+ "Use the DOM Path to locate the correct element - do NOT change a child if the path shows this element is the parent.",
417
+ metadata.sourceFile !== "DOM"
418
+ ? `Open ${metadata.sourceFile} and find the component that renders this element.`
419
+ : "Search for the parent component that renders this layout. Look for the component in Usage Path or by Element Text/Label.",
420
+ "Modify the component's props, sx, or styles as needed.",
421
+ ];
422
+ return { domPath, parent, roleInTree: buildRoleInTree(element, "component"), target, howToFindSteps: steps };
423
+ }
424
+ if (type === "margin" || type === "padding") {
425
+ const cs = window.getComputedStyle(element);
426
+ const classNameStr = element.className ? (typeof element.className === "string" ? element.className : String(element.className)) : "";
427
+ const firstClass = classNameStr.split(/\s+/).find((c) => c && (c.startsWith("Mui") || c.startsWith("css-")));
428
+ const desc = firstClass ? `${metadata.componentName} with class ${firstClass}` : metadata.componentName;
429
+ if (type === "margin") {
430
+ const mt = getCssValue(cs.marginTop);
431
+ const mr = getCssValue(cs.marginRight);
432
+ const mb = getCssValue(cs.marginBottom);
433
+ const ml = getCssValue(cs.marginLeft);
434
+ const target = `The ${desc} - the element with margin ${mt} ${mr} ${mb} ${ml}. It is the PARENT in the DOM path above, NOT a child.`;
435
+ const steps = [
436
+ "Use the DOM Path to locate the correct element - do NOT change a child (e.g. CardContent, MuiBox) if the path shows this element is the parent.",
437
+ metadata.sourceFile !== "DOM"
438
+ ? `Open ${metadata.sourceFile} and find the component that renders this element.`
439
+ : "Search for the parent component that renders this layout. Look for MUI components (Box, Card, etc.) - the element with these margin values may use sx={{ m: ... }} or style props.",
440
+ "Change margin: sx={{ margin: 0 }} or margin: \"4px\" or mt: 1, mr: 1, mb: 1, ml: 1 (MUI theme spacing).",
441
+ ];
442
+ return { domPath, parent, roleInTree: buildRoleInTree(element, "margin"), target, howToFindSteps: steps };
443
+ }
444
+ else {
445
+ const pt = getCssValue(cs.paddingTop);
446
+ const pr = getCssValue(cs.paddingRight);
447
+ const pb = getCssValue(cs.paddingBottom);
448
+ const pl = getCssValue(cs.paddingLeft);
449
+ const target = `The ${desc} - the element with padding ${pt} ${pr} ${pb} ${pl}. It is the PARENT in the DOM path above, NOT a child.`;
450
+ const steps = [
451
+ "Use the DOM Path to locate the correct element - do NOT change a child (e.g. CardContent, MuiBox) if the path shows this element is the parent.",
452
+ metadata.sourceFile !== "DOM"
453
+ ? `Open ${metadata.sourceFile} and find the component that renders this element.`
454
+ : "Search for the parent component that renders this layout. Look for MUI components (Box, Card, CardContent, etc.) - the element with these exact padding values may use sx={{ p: ... }} or padding props.",
455
+ "Change padding: sx={{ padding: 0 }} or p: \"4px\" or pt: 1, pr: 1, pb: 1, pl: 1 (MUI theme spacing).",
456
+ ];
457
+ return { domPath, parent, roleInTree: buildRoleInTree(element, "padding"), target, howToFindSteps: steps };
458
+ }
459
+ }
460
+ // type === "gap" - element is the parent with gap
461
+ const cs = window.getComputedStyle(element);
462
+ const gap = getCssValue(cs.gap);
463
+ const classNameStr = element.className ? (typeof element.className === "string" ? element.className : String(element.className)) : "";
464
+ const firstClass = classNameStr.split(/\s+/).find((c) => c && (c.startsWith("Mui") || c.startsWith("css-")));
465
+ const desc = firstClass ? `${metadata.componentName} with class ${firstClass}` : metadata.componentName;
466
+ const target = `The ${desc} - the flex/grid container with gap ${gap}. It is the PARENT in the DOM path above.`;
467
+ const steps = [
468
+ "Use the DOM Path to locate the flex/grid container.",
469
+ metadata.sourceFile !== "DOM"
470
+ ? `Open ${metadata.sourceFile} and find the component that renders this element.`
471
+ : "Search for the component that renders this layout (Box, Stack, Grid, etc.).",
472
+ "Change gap: sx={{ gap: 1 }} or gap: \"8px\" or rowGap/columnGap (MUI theme spacing).",
473
+ ];
474
+ return { domPath, parent, roleInTree: "flex/grid container", target, howToFindSteps: steps };
475
+ };
403
476
  /**
404
477
  * Track component instances for instance indexing
405
478
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zargaryanvh/react-component-inspector",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
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",