@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.
- package/README.md +72 -25
- package/dist/InspectionContext.d.ts +37 -35
- package/dist/InspectionContext.js +211 -253
- package/dist/InspectionHighlight.d.ts +2 -6
- package/dist/InspectionHighlight.js +258 -65
- package/dist/InspectionOverlays.d.ts +7 -0
- package/dist/InspectionOverlays.js +32 -0
- package/dist/InspectionTooltip.d.ts +6 -6
- package/dist/InspectionTooltip.js +406 -232
- package/dist/InspectionWrapper.d.ts +28 -28
- package/dist/InspectionWrapper.js +102 -102
- package/dist/autoInspection.d.ts +18 -14
- package/dist/autoInspection.js +329 -320
- package/dist/index.d.ts +10 -8
- package/dist/index.js +10 -9
- package/dist/inspection.d.ts +78 -29
- package/dist/inspection.js +493 -140
- package/dist/inspectionInterceptors.d.ts +27 -27
- package/dist/inspectionInterceptors.js +68 -64
- package/dist/useInspectionMetadata.d.ts +34 -34
- package/dist/useInspectionMetadata.js +67 -67
- package/package.json +1 -1
|
@@ -1,65 +1,258 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState } from "react";
|
|
3
|
-
import { Box } from "@mui/material";
|
|
4
|
-
import { useInspection } from "./InspectionContext";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Box } from "@mui/material";
|
|
4
|
+
import { useInspection } from "./InspectionContext";
|
|
5
|
+
import { getParentWithGap, getAncestorsWithMargin } from "./inspection";
|
|
6
|
+
import { parseInspectionMetadata } from "./autoInspection";
|
|
7
|
+
/**
|
|
8
|
+
* Parse CSS length (e.g. "8px", "1em") to pixels
|
|
9
|
+
*/
|
|
10
|
+
const parsePx = (value) => {
|
|
11
|
+
if (!value || value === "0")
|
|
12
|
+
return 0;
|
|
13
|
+
const num = parseFloat(value);
|
|
14
|
+
if (value.endsWith("px"))
|
|
15
|
+
return num;
|
|
16
|
+
if (value.endsWith("em") || value.endsWith("rem"))
|
|
17
|
+
return num * 16; // approximate
|
|
18
|
+
return num;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Highlight overlay that shows the boundary of the hovered component
|
|
22
|
+
* When hold CTRL+ALT: orange = margin, green = padding, purple = gap (hold-to-use, release to exit)
|
|
23
|
+
* Otherwise: blue outline for component
|
|
24
|
+
*/
|
|
25
|
+
const stripStyle = (left, top, width, height, color, bg) => ({
|
|
26
|
+
position: "fixed",
|
|
27
|
+
left: `${left}px`,
|
|
28
|
+
top: `${top}px`,
|
|
29
|
+
width: `${Math.max(0, width)}px`,
|
|
30
|
+
height: `${Math.max(0, height)}px`,
|
|
31
|
+
pointerEvents: "none",
|
|
32
|
+
border: `2px solid ${color}`,
|
|
33
|
+
backgroundColor: bg,
|
|
34
|
+
boxSizing: "border-box",
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* Clickable ancestor overlay - dashed outline, pointer-events for click-to-switch
|
|
38
|
+
*/
|
|
39
|
+
const ancestorOutlineStyle = (left, top, width, height, color, bg) => ({
|
|
40
|
+
position: "fixed",
|
|
41
|
+
left: `${left}px`,
|
|
42
|
+
top: `${top}px`,
|
|
43
|
+
width: `${Math.max(0, width)}px`,
|
|
44
|
+
height: `${Math.max(0, height)}px`,
|
|
45
|
+
pointerEvents: "auto",
|
|
46
|
+
cursor: "pointer",
|
|
47
|
+
border: `2px dashed ${color}`,
|
|
48
|
+
backgroundColor: bg,
|
|
49
|
+
boxSizing: "border-box",
|
|
50
|
+
zIndex: 999996,
|
|
51
|
+
});
|
|
52
|
+
export const InspectionHighlight = () => {
|
|
53
|
+
const { isInspectionActive, isMarginPaddingMode, hoveredElement, setHoveredComponent } = useInspection();
|
|
54
|
+
const [highlightStyle, setHighlightStyle] = useState(null);
|
|
55
|
+
const [marginStrips, setMarginStrips] = useState([]);
|
|
56
|
+
const [paddingStrips, setPaddingStrips] = useState([]);
|
|
57
|
+
const [elementOutlineStyle, setElementOutlineStyle] = useState(null);
|
|
58
|
+
const [gapOutlineStyle, setGapOutlineStyle] = useState(null);
|
|
59
|
+
const [ancestorOutlines, setAncestorOutlines] = useState([]);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
// Show when CTRL is held (inspection active)
|
|
62
|
+
const shouldShow = isInspectionActive;
|
|
63
|
+
if (!shouldShow) {
|
|
64
|
+
setHighlightStyle(null);
|
|
65
|
+
setMarginStrips([]);
|
|
66
|
+
setPaddingStrips([]);
|
|
67
|
+
setElementOutlineStyle(null);
|
|
68
|
+
setGapOutlineStyle(null);
|
|
69
|
+
setAncestorOutlines([]);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!hoveredElement) {
|
|
73
|
+
setHighlightStyle(null);
|
|
74
|
+
setMarginStrips([]);
|
|
75
|
+
setPaddingStrips([]);
|
|
76
|
+
setElementOutlineStyle(null);
|
|
77
|
+
setGapOutlineStyle(null);
|
|
78
|
+
setAncestorOutlines([]);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const updateHighlight = () => {
|
|
82
|
+
if (!document.body.contains(hoveredElement)) {
|
|
83
|
+
setHighlightStyle(null);
|
|
84
|
+
setMarginStrips([]);
|
|
85
|
+
setPaddingStrips([]);
|
|
86
|
+
setElementOutlineStyle(null);
|
|
87
|
+
setGapOutlineStyle(null);
|
|
88
|
+
setAncestorOutlines([]);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const rect = hoveredElement.getBoundingClientRect();
|
|
93
|
+
// position:fixed uses viewport coords - getBoundingClientRect already returns viewport coords
|
|
94
|
+
const left = rect.left;
|
|
95
|
+
const top = rect.top;
|
|
96
|
+
// Default blue component outline
|
|
97
|
+
if (!isMarginPaddingMode) {
|
|
98
|
+
setHighlightStyle({
|
|
99
|
+
position: "fixed",
|
|
100
|
+
left: `${left}px`,
|
|
101
|
+
top: `${top}px`,
|
|
102
|
+
width: `${rect.width}px`,
|
|
103
|
+
height: `${rect.height}px`,
|
|
104
|
+
pointerEvents: "none",
|
|
105
|
+
zIndex: 999998,
|
|
106
|
+
border: "2px solid #2196f3",
|
|
107
|
+
backgroundColor: "rgba(33, 150, 243, 0.1)",
|
|
108
|
+
boxShadow: "0 0 0 1px rgba(33, 150, 243, 0.3), 0 0 8px rgba(33, 150, 243, 0.2)",
|
|
109
|
+
borderRadius: "2px",
|
|
110
|
+
transition: "all 0.1s ease-out",
|
|
111
|
+
});
|
|
112
|
+
setMarginStrips([]);
|
|
113
|
+
setPaddingStrips([]);
|
|
114
|
+
setElementOutlineStyle(null);
|
|
115
|
+
setGapOutlineStyle(null);
|
|
116
|
+
setAncestorOutlines([]);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// Margin/padding mode: draw margin as 4 strips (outside), element outline, padding as 4 strips (inside)
|
|
120
|
+
setHighlightStyle(null);
|
|
121
|
+
const cs = window.getComputedStyle(hoveredElement);
|
|
122
|
+
const mt = parsePx(cs.marginTop);
|
|
123
|
+
const mr = parsePx(cs.marginRight);
|
|
124
|
+
const mb = parsePx(cs.marginBottom);
|
|
125
|
+
const ml = parsePx(cs.marginLeft);
|
|
126
|
+
const pt = parsePx(cs.paddingTop);
|
|
127
|
+
const pr = parsePx(cs.paddingRight);
|
|
128
|
+
const pb = parsePx(cs.paddingBottom);
|
|
129
|
+
const pl = parsePx(cs.paddingLeft);
|
|
130
|
+
const bt = parsePx(cs.borderTopWidth);
|
|
131
|
+
const br = parsePx(cs.borderRightWidth);
|
|
132
|
+
const bb = parsePx(cs.borderBottomWidth);
|
|
133
|
+
const bl = parsePx(cs.borderLeftWidth);
|
|
134
|
+
const w = rect.width;
|
|
135
|
+
const h = rect.height;
|
|
136
|
+
const M_ORANGE = "#e65100";
|
|
137
|
+
const M_BG = "rgba(255, 152, 0, 0.4)";
|
|
138
|
+
const P_GREEN = "#2e7d32";
|
|
139
|
+
const P_BG = "rgba(76, 175, 80, 0.4)";
|
|
140
|
+
// Element border box outline so you see the full outside size of the element
|
|
141
|
+
setElementOutlineStyle({
|
|
142
|
+
position: "fixed",
|
|
143
|
+
left: `${left}px`,
|
|
144
|
+
top: `${top}px`,
|
|
145
|
+
width: `${w}px`,
|
|
146
|
+
height: `${h}px`,
|
|
147
|
+
pointerEvents: "none",
|
|
148
|
+
zIndex: 999998,
|
|
149
|
+
border: "2px solid rgba(255,255,255,0.6)",
|
|
150
|
+
boxSizing: "border-box",
|
|
151
|
+
});
|
|
152
|
+
// Margin: 4 strips only (the actual margin space outside the element), so the outside size is visible
|
|
153
|
+
const marginStripsList = [];
|
|
154
|
+
if (mt > 0)
|
|
155
|
+
marginStripsList.push(stripStyle(left - ml, top - mt, w + ml + mr, mt, M_ORANGE, M_BG));
|
|
156
|
+
if (mb > 0)
|
|
157
|
+
marginStripsList.push(stripStyle(left - ml, top + h, w + ml + mr, mb, M_ORANGE, M_BG));
|
|
158
|
+
if (ml > 0)
|
|
159
|
+
marginStripsList.push(stripStyle(left - ml, top, ml, h, M_ORANGE, M_BG));
|
|
160
|
+
if (mr > 0)
|
|
161
|
+
marginStripsList.push(stripStyle(left + w, top, mr, h, M_ORANGE, M_BG));
|
|
162
|
+
setMarginStrips(marginStripsList);
|
|
163
|
+
// Padding: 4 strips only (the actual padding space inside the border)
|
|
164
|
+
const paddingStripsList = [];
|
|
165
|
+
const innerLeft = left + bl;
|
|
166
|
+
const innerTop = top + bt;
|
|
167
|
+
const innerW = w - bl - br;
|
|
168
|
+
const innerH = h - bt - bb;
|
|
169
|
+
if (pt > 0)
|
|
170
|
+
paddingStripsList.push(stripStyle(innerLeft, innerTop, innerW, pt, P_GREEN, P_BG));
|
|
171
|
+
if (pb > 0)
|
|
172
|
+
paddingStripsList.push(stripStyle(innerLeft, innerTop + innerH - pb, innerW, pb, P_GREEN, P_BG));
|
|
173
|
+
if (pl > 0)
|
|
174
|
+
paddingStripsList.push(stripStyle(innerLeft, innerTop, pl, innerH, P_GREEN, P_BG));
|
|
175
|
+
if (pr > 0)
|
|
176
|
+
paddingStripsList.push(stripStyle(innerLeft + innerW - pr, innerTop, pr, innerH, P_GREEN, P_BG));
|
|
177
|
+
setPaddingStrips(paddingStripsList);
|
|
178
|
+
// Gap overlay: parent with flex/grid + non-zero gap
|
|
179
|
+
const parentWithGap = getParentWithGap(hoveredElement);
|
|
180
|
+
if (parentWithGap && document.body.contains(parentWithGap)) {
|
|
181
|
+
const pr = parentWithGap.getBoundingClientRect();
|
|
182
|
+
const GAP_PURPLE = "#7b1fa2";
|
|
183
|
+
const GAP_BG = "rgba(156, 39, 176, 0.2)";
|
|
184
|
+
setGapOutlineStyle({
|
|
185
|
+
position: "fixed",
|
|
186
|
+
left: `${pr.left}px`,
|
|
187
|
+
top: `${pr.top}px`,
|
|
188
|
+
width: `${pr.width}px`,
|
|
189
|
+
height: `${pr.height}px`,
|
|
190
|
+
pointerEvents: "none",
|
|
191
|
+
zIndex: 999995,
|
|
192
|
+
border: "2px dashed #7b1fa2",
|
|
193
|
+
backgroundColor: GAP_BG,
|
|
194
|
+
boxSizing: "border-box",
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
setGapOutlineStyle(null);
|
|
199
|
+
}
|
|
200
|
+
// Ancestor margin overlays: when current has zero margin, show ancestors with margin (clickable)
|
|
201
|
+
const hasCurrentMargin = mt > 1 || mr > 1 || mb > 1 || ml > 1;
|
|
202
|
+
if (!hasCurrentMargin) {
|
|
203
|
+
const ancestors = getAncestorsWithMargin(hoveredElement, 2);
|
|
204
|
+
const outlines = ancestors
|
|
205
|
+
.filter((a) => document.body.contains(a.element))
|
|
206
|
+
.map((a) => {
|
|
207
|
+
const ar = a.element.getBoundingClientRect();
|
|
208
|
+
return {
|
|
209
|
+
style: ancestorOutlineStyle(ar.left, ar.top, ar.width, ar.height, "#e65100", "rgba(255, 152, 0, 0.2)"),
|
|
210
|
+
element: a.element,
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
setAncestorOutlines(outlines);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
setAncestorOutlines([]);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
setHighlightStyle(null);
|
|
222
|
+
setMarginStrips([]);
|
|
223
|
+
setPaddingStrips([]);
|
|
224
|
+
setElementOutlineStyle(null);
|
|
225
|
+
setGapOutlineStyle(null);
|
|
226
|
+
setAncestorOutlines([]);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
updateHighlight();
|
|
230
|
+
const handleUpdate = () => updateHighlight();
|
|
231
|
+
window.addEventListener("scroll", handleUpdate, true);
|
|
232
|
+
window.addEventListener("resize", handleUpdate);
|
|
233
|
+
return () => {
|
|
234
|
+
window.removeEventListener("scroll", handleUpdate, true);
|
|
235
|
+
window.removeEventListener("resize", handleUpdate);
|
|
236
|
+
};
|
|
237
|
+
}, [isInspectionActive, isMarginPaddingMode, hoveredElement]);
|
|
238
|
+
const showContent = isInspectionActive &&
|
|
239
|
+
(highlightStyle ||
|
|
240
|
+
marginStrips.length > 0 ||
|
|
241
|
+
paddingStrips.length > 0 ||
|
|
242
|
+
elementOutlineStyle ||
|
|
243
|
+
gapOutlineStyle ||
|
|
244
|
+
ancestorOutlines.length > 0);
|
|
245
|
+
if (!showContent)
|
|
246
|
+
return null;
|
|
247
|
+
const handleAncestorClick = (element) => {
|
|
248
|
+
const metadata = parseInspectionMetadata(element);
|
|
249
|
+
if (metadata) {
|
|
250
|
+
setHoveredComponent(metadata, element);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
return (_jsxs(_Fragment, { children: [gapOutlineStyle && _jsx(Box, { sx: gapOutlineStyle }), ancestorOutlines.map((o, i) => (_jsx(Box, { sx: o.style, onClick: (e) => {
|
|
254
|
+
e.stopPropagation();
|
|
255
|
+
e.preventDefault();
|
|
256
|
+
handleAncestorClick(o.element);
|
|
257
|
+
}, onPointerDown: (e) => e.stopPropagation(), title: "Click to inspect this ancestor (has margin)", "aria-label": "Ancestor with margin - click to inspect" }, `ancestor-${i}`))), marginStrips.map((s, i) => (_jsx(Box, { sx: { ...s, zIndex: 999997 } }, `m-${i}`))), paddingStrips.map((s, i) => (_jsx(Box, { sx: { ...s, zIndex: 999998 } }, `p-${i}`))), elementOutlineStyle && _jsx(Box, { sx: elementOutlineStyle }), highlightStyle && _jsx(Box, { sx: highlightStyle })] }));
|
|
258
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Renders InspectionTooltip and InspectionHighlight in a dedicated portal container.
|
|
4
|
+
* This avoids "removeChild" DOM errors that can occur when inspector DOM lives
|
|
5
|
+
* in the same tree as the app (e.g. with MUI portals or rapid mount/unmount).
|
|
6
|
+
*/
|
|
7
|
+
export declare const InspectionOverlays: React.FC;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useRef } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import { InspectionTooltip } from "./InspectionTooltip";
|
|
5
|
+
import { InspectionHighlight } from "./InspectionHighlight";
|
|
6
|
+
const INSPECTOR_PORTAL_ID = "react-component-inspector-portal";
|
|
7
|
+
/**
|
|
8
|
+
* Renders InspectionTooltip and InspectionHighlight in a dedicated portal container.
|
|
9
|
+
* This avoids "removeChild" DOM errors that can occur when inspector DOM lives
|
|
10
|
+
* in the same tree as the app (e.g. with MUI portals or rapid mount/unmount).
|
|
11
|
+
*/
|
|
12
|
+
export const InspectionOverlays = () => {
|
|
13
|
+
const [container, setContainer] = useState(null);
|
|
14
|
+
const createdRef = useRef(false);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (typeof document === "undefined" || createdRef.current)
|
|
17
|
+
return;
|
|
18
|
+
let el = document.getElementById(INSPECTOR_PORTAL_ID);
|
|
19
|
+
if (!el) {
|
|
20
|
+
el = document.createElement("div");
|
|
21
|
+
el.id = INSPECTOR_PORTAL_ID;
|
|
22
|
+
el.setAttribute("data-inspector-portal", "true");
|
|
23
|
+
document.body.appendChild(el);
|
|
24
|
+
createdRef.current = true;
|
|
25
|
+
}
|
|
26
|
+
setContainer(el);
|
|
27
|
+
// Intentionally never remove the container to avoid React removeChild conflicts
|
|
28
|
+
}, []);
|
|
29
|
+
if (!container)
|
|
30
|
+
return null;
|
|
31
|
+
return createPortal(_jsxs(_Fragment, { children: [_jsx(InspectionTooltip, {}), _jsx(InspectionHighlight, {})] }), container);
|
|
32
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
/**
|
|
3
|
-
* Inspection tooltip that shows component metadata
|
|
4
|
-
* Only visible when CTRL is held and a component is hovered
|
|
5
|
-
*/
|
|
6
|
-
export declare const InspectionTooltip: React.FC;
|
|
1
|
+
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Inspection tooltip that shows component metadata
|
|
4
|
+
* Only visible when CTRL is held and a component is hovered
|
|
5
|
+
*/
|
|
6
|
+
export declare const InspectionTooltip: React.FC;
|