@zargaryanvh/react-component-inspector 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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
- * Highlight overlay that shows the boundary of the hovered component
7
- * Only visible when CTRL is held and a component is hovered
8
- */
9
- export const InspectionHighlight = () => {
10
- const { isInspectionActive, hoveredElement } = useInspection();
11
- const [highlightStyle, setHighlightStyle] = useState(null);
12
- useEffect(() => {
13
- // Show highlight for any element when CTRL is held, even without metadata
14
- if (!isInspectionActive) {
15
- setHighlightStyle(null);
16
- return;
17
- }
18
- // If no hoveredElement but inspection is active, don't show highlight
19
- if (!hoveredElement) {
20
- setHighlightStyle(null);
21
- return;
22
- }
23
- const updateHighlight = () => {
24
- // Check if element is still in the DOM
25
- if (!document.body.contains(hoveredElement)) {
26
- setHighlightStyle(null);
27
- return;
28
- }
29
- try {
30
- const rect = hoveredElement.getBoundingClientRect();
31
- setHighlightStyle({
32
- position: "fixed",
33
- left: `${rect.left + window.scrollX}px`,
34
- top: `${rect.top + window.scrollY}px`,
35
- width: `${rect.width}px`,
36
- height: `${rect.height}px`,
37
- pointerEvents: "none",
38
- zIndex: 999998,
39
- border: "2px solid #2196f3",
40
- backgroundColor: "rgba(33, 150, 243, 0.1)",
41
- boxShadow: "0 0 0 1px rgba(33, 150, 243, 0.3), 0 0 8px rgba(33, 150, 243, 0.2)",
42
- borderRadius: "2px",
43
- transition: "all 0.1s ease-out",
44
- });
45
- }
46
- catch (error) {
47
- // Element might have been removed, clear highlight
48
- setHighlightStyle(null);
49
- }
50
- };
51
- updateHighlight();
52
- // Update on scroll/resize
53
- const handleUpdate = () => updateHighlight();
54
- window.addEventListener("scroll", handleUpdate, true);
55
- window.addEventListener("resize", handleUpdate);
56
- return () => {
57
- window.removeEventListener("scroll", handleUpdate, true);
58
- window.removeEventListener("resize", handleUpdate);
59
- };
60
- }, [isInspectionActive, hoveredElement]);
61
- if (!isInspectionActive || !highlightStyle) {
62
- return null;
63
- }
64
- return _jsx(Box, { sx: highlightStyle });
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;