@zargaryanvh/react-component-inspector 1.0.0
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/LICENSE +21 -0
- package/README.md +274 -0
- package/dist/InspectionContext.d.ts +35 -0
- package/dist/InspectionContext.js +122 -0
- package/dist/InspectionHighlight.d.ts +6 -0
- package/dist/InspectionHighlight.js +65 -0
- package/dist/InspectionTooltip.d.ts +6 -0
- package/dist/InspectionTooltip.js +232 -0
- package/dist/InspectionWrapper.d.ts +28 -0
- package/dist/InspectionWrapper.js +68 -0
- package/dist/autoInspection.d.ts +14 -0
- package/dist/autoInspection.js +258 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/inspection.d.ts +29 -0
- package/dist/inspection.js +140 -0
- package/dist/inspectionInterceptors.d.ts +27 -0
- package/dist/inspectionInterceptors.js +64 -0
- package/dist/useInspectionMetadata.d.ts +32 -0
- package/dist/useInspectionMetadata.js +52 -0
- package/package.json +54 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useRef, useMemo } from "react";
|
|
3
|
+
import { Box, Paper, Typography, IconButton, Tooltip as MuiTooltip, Divider } from "@mui/material";
|
|
4
|
+
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
|
5
|
+
import { useInspection } from "./InspectionContext";
|
|
6
|
+
import { formatMetadataForClipboard } from "./inspection";
|
|
7
|
+
/**
|
|
8
|
+
* Helper: Get element text content (first 100 chars)
|
|
9
|
+
*/
|
|
10
|
+
const getElementText = (element) => {
|
|
11
|
+
if (!element)
|
|
12
|
+
return null;
|
|
13
|
+
const textContent = element.textContent?.trim() || "";
|
|
14
|
+
const visibleText = textContent.substring(0, 100).replace(/\s+/g, " ");
|
|
15
|
+
return visibleText || null;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Helper: Get element classes (prioritize MUI classes)
|
|
19
|
+
*/
|
|
20
|
+
const getElementClasses = (element) => {
|
|
21
|
+
if (!element?.className)
|
|
22
|
+
return [];
|
|
23
|
+
const classNameStr = typeof element.className === "string"
|
|
24
|
+
? element.className
|
|
25
|
+
: String(element.className);
|
|
26
|
+
const classes = classNameStr.split(/\s+/).filter(c => c);
|
|
27
|
+
// Prioritize MUI classes, then others
|
|
28
|
+
const muiClasses = classes.filter(c => c.includes("Mui")).slice(0, 2);
|
|
29
|
+
const otherClasses = classes.filter(c => !c.includes("Mui")).slice(0, 2);
|
|
30
|
+
return [...muiClasses, ...otherClasses].slice(0, 3);
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Helper: Generate CSS selector for element
|
|
34
|
+
*/
|
|
35
|
+
const getElementSelector = (element) => {
|
|
36
|
+
if (!element)
|
|
37
|
+
return null;
|
|
38
|
+
if (element.id) {
|
|
39
|
+
return `#${element.id}`;
|
|
40
|
+
}
|
|
41
|
+
if (element.className) {
|
|
42
|
+
const classNameStr = typeof element.className === "string"
|
|
43
|
+
? element.className
|
|
44
|
+
: String(element.className);
|
|
45
|
+
const firstClass = classNameStr.split(/\s+/).find(c => c);
|
|
46
|
+
if (firstClass) {
|
|
47
|
+
return `${element.tagName.toLowerCase()}.${firstClass}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return element.tagName.toLowerCase();
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Helper: Get element position and size
|
|
54
|
+
*/
|
|
55
|
+
const getElementBounds = (element) => {
|
|
56
|
+
if (!element)
|
|
57
|
+
return null;
|
|
58
|
+
const rect = element.getBoundingClientRect();
|
|
59
|
+
return {
|
|
60
|
+
position: `(${Math.round(rect.left)}, ${Math.round(rect.top)})`,
|
|
61
|
+
size: `${Math.round(rect.width)}x${Math.round(rect.height)}px`,
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Section: Element Identification
|
|
66
|
+
*/
|
|
67
|
+
const ElementIdentificationSection = ({ element, role }) => {
|
|
68
|
+
const elementText = getElementText(element);
|
|
69
|
+
const elementClasses = getElementClasses(element);
|
|
70
|
+
const selector = getElementSelector(element);
|
|
71
|
+
const bounds = getElementBounds(element);
|
|
72
|
+
return (_jsxs(Box, { sx: { display: "flex", flexDirection: "column", gap: 0.5 }, children: [_jsx(Typography, { variant: "caption", sx: { color: "rgba(255, 255, 255, 0.9)", fontSize: "0.7rem", fontWeight: 600 }, children: "=== ELEMENT IDENTIFICATION ===" }), _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.7rem" }, children: [_jsx("strong", { children: "Element Type:" }), " ", element?.tagName.toLowerCase() || "unknown"] }), elementText && (_jsxs(Typography, { variant: "caption", sx: { color: "rgba(255, 255, 255, 0.7)", fontSize: "0.7rem" }, children: [_jsx("strong", { children: "Element Text/Label:" }), " \"", elementText, "\""] })), element?.id && (_jsxs(Typography, { variant: "caption", sx: { color: "rgba(255, 255, 255, 0.7)", fontSize: "0.7rem" }, children: [_jsx("strong", { children: "Element ID:" }), " ", element.id] })), elementClasses.length > 0 && (_jsxs(Typography, { variant: "caption", sx: { color: "rgba(255, 255, 255, 0.7)", fontSize: "0.7rem" }, children: [_jsx("strong", { children: "Element Classes:" }), " ", elementClasses.join(", ")] })), role && (_jsxs(Typography, { variant: "caption", sx: { color: "rgba(255, 255, 255, 0.7)", fontSize: "0.7rem" }, children: [_jsx("strong", { children: "Role:" }), " ", role] })), selector && (_jsxs(Typography, { variant: "caption", sx: { color: "rgba(255, 255, 255, 0.7)", fontSize: "0.7rem" }, children: [_jsx("strong", { children: "CSS Selector:" }), " ", selector] })), bounds && (_jsxs(_Fragment, { children: [_jsxs(Typography, { variant: "caption", sx: { color: "rgba(255, 255, 255, 0.7)", fontSize: "0.7rem" }, children: [_jsx("strong", { children: "Position:" }), " ", bounds.position] }), _jsxs(Typography, { variant: "caption", sx: { color: "rgba(255, 255, 255, 0.7)", fontSize: "0.7rem" }, children: [_jsx("strong", { children: "Size:" }), " ", bounds.size] })] }))] })] }));
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Section: Component Metadata
|
|
76
|
+
*/
|
|
77
|
+
const ComponentMetadataSection = ({ metadata }) => {
|
|
78
|
+
return (_jsxs(Box, { sx: { display: "flex", flexDirection: "column", gap: 0.5 }, children: [_jsx(Typography, { variant: "caption", sx: { color: "rgba(255, 255, 255, 0.9)", fontSize: "0.7rem", fontWeight: 600 }, children: "=== COMPONENT METADATA ===" }), _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.7rem" }, children: [_jsx("strong", { children: "Component Name:" }), " ", metadata.componentName] }), _jsxs(Typography, { variant: "caption", sx: { color: "rgba(255, 255, 255, 0.7)", fontSize: "0.7rem" }, children: [_jsx("strong", { children: "Component ID:" }), " ", metadata.componentId] }), metadata.variant && (_jsxs(Typography, { variant: "caption", sx: { color: "rgba(255, 255, 255, 0.7)", fontSize: "0.7rem" }, children: [_jsx("strong", { children: "Variant:" }), " ", metadata.variant] })), _jsxs(Typography, { variant: "caption", sx: { color: "rgba(255, 255, 255, 0.7)", fontSize: "0.7rem" }, children: [_jsx("strong", { children: "Usage Path:" }), " ", metadata.usagePath] }), _jsxs(Typography, { variant: "caption", sx: { color: "rgba(255, 255, 255, 0.7)", fontSize: "0.7rem" }, children: [_jsx("strong", { children: "Instance:" }), " ", metadata.instanceIndex] }), _jsxs(Typography, { variant: "caption", sx: { color: "rgba(255, 255, 255, 0.7)", fontSize: "0.7rem" }, children: [_jsx("strong", { children: "Props:" }), " ", metadata.propsSignature] }), _jsxs(Typography, { variant: "caption", sx: { color: "rgba(255, 255, 255, 0.7)", fontSize: "0.7rem" }, children: [_jsx("strong", { children: "Source File:" }), " ", metadata.sourceFile] })] })] }));
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Inspection tooltip that shows component metadata
|
|
82
|
+
* Only visible when CTRL is held and a component is hovered
|
|
83
|
+
*/
|
|
84
|
+
export const InspectionTooltip = () => {
|
|
85
|
+
const { isInspectionActive, isLocked, hoveredComponent, hoveredElement } = useInspection();
|
|
86
|
+
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
87
|
+
const [stablePosition, setStablePosition] = useState(null);
|
|
88
|
+
const [copied, setCopied] = useState(false);
|
|
89
|
+
const tooltipRef = useRef(null);
|
|
90
|
+
// Update position based on cursor, but keep it stable when locked or mouse is near tooltip
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!isInspectionActive || !hoveredElement) {
|
|
93
|
+
setStablePosition(null);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// If locked, don't update position at all - keep it completely fixed
|
|
97
|
+
if (isLocked) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const updatePosition = (e) => {
|
|
101
|
+
// If we have a stable position, check if mouse is near tooltip
|
|
102
|
+
if (stablePosition && tooltipRef.current) {
|
|
103
|
+
const tooltipRect = tooltipRef.current.getBoundingClientRect();
|
|
104
|
+
const mouseX = e.clientX;
|
|
105
|
+
const mouseY = e.clientY;
|
|
106
|
+
// Create a "dead zone" around tooltip (80px padding for easier clicking)
|
|
107
|
+
const deadZone = {
|
|
108
|
+
left: tooltipRect.left - 80,
|
|
109
|
+
right: tooltipRect.right + 80,
|
|
110
|
+
top: tooltipRect.top - 80,
|
|
111
|
+
bottom: tooltipRect.bottom + 80,
|
|
112
|
+
};
|
|
113
|
+
// If mouse is in dead zone, keep position stable (don't update)
|
|
114
|
+
if (mouseX >= deadZone.left &&
|
|
115
|
+
mouseX <= deadZone.right &&
|
|
116
|
+
mouseY >= deadZone.top &&
|
|
117
|
+
mouseY <= deadZone.bottom) {
|
|
118
|
+
return; // Don't update position - keep it stable
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Mouse moved far from tooltip - update position
|
|
122
|
+
setPosition({ x: e.clientX, y: e.clientY });
|
|
123
|
+
// Clear stable position so it recalculates
|
|
124
|
+
setStablePosition(null);
|
|
125
|
+
};
|
|
126
|
+
window.addEventListener("mousemove", updatePosition);
|
|
127
|
+
return () => window.removeEventListener("mousemove", updatePosition);
|
|
128
|
+
}, [isInspectionActive, isLocked, hoveredElement, stablePosition]);
|
|
129
|
+
// If no component metadata but we have an element, create basic metadata
|
|
130
|
+
const displayComponent = hoveredComponent || (hoveredElement ? {
|
|
131
|
+
componentName: hoveredElement.tagName.toLowerCase(),
|
|
132
|
+
componentId: hoveredElement.id || "no-id",
|
|
133
|
+
usagePath: "DOM Element",
|
|
134
|
+
instanceIndex: 0,
|
|
135
|
+
propsSignature: "default",
|
|
136
|
+
sourceFile: "DOM",
|
|
137
|
+
} : null);
|
|
138
|
+
// Calculate adjusted position to avoid going off-screen
|
|
139
|
+
// Use stable position if available, otherwise calculate from cursor position
|
|
140
|
+
const adjustedPosition = useMemo(() => {
|
|
141
|
+
if (!displayComponent) {
|
|
142
|
+
return { x: position.x + 15, y: position.y + 15 };
|
|
143
|
+
}
|
|
144
|
+
// If we have a stable position, use it (don't recalculate)
|
|
145
|
+
if (stablePosition) {
|
|
146
|
+
return stablePosition;
|
|
147
|
+
}
|
|
148
|
+
// Calculate new position from cursor (only when stablePosition is null)
|
|
149
|
+
const padding = 10;
|
|
150
|
+
let x = position.x + 15;
|
|
151
|
+
let y = position.y + 15;
|
|
152
|
+
// Adjust if tooltip would go off right edge (estimate width)
|
|
153
|
+
const estimatedWidth = 400; // Approximate tooltip width
|
|
154
|
+
if (x + estimatedWidth > window.innerWidth - padding) {
|
|
155
|
+
x = position.x - estimatedWidth - 15;
|
|
156
|
+
}
|
|
157
|
+
// Adjust if tooltip would go off bottom edge (estimate height)
|
|
158
|
+
const estimatedHeight = 200; // Approximate tooltip height
|
|
159
|
+
if (y + estimatedHeight > window.innerHeight - padding) {
|
|
160
|
+
y = position.y - estimatedHeight - 15;
|
|
161
|
+
}
|
|
162
|
+
// Adjust if tooltip would go off left edge
|
|
163
|
+
if (x < padding) {
|
|
164
|
+
x = padding;
|
|
165
|
+
}
|
|
166
|
+
// Adjust if tooltip would go off top edge
|
|
167
|
+
if (y < padding) {
|
|
168
|
+
y = padding;
|
|
169
|
+
}
|
|
170
|
+
return { x, y };
|
|
171
|
+
}, [position, displayComponent, stablePosition]);
|
|
172
|
+
// Set stable position once when tooltip first appears for a new element, or when locked
|
|
173
|
+
const lastComponentIdRef = useRef(null);
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
const currentComponentId = displayComponent?.componentId || null;
|
|
176
|
+
// When locked, ensure we have a stable position
|
|
177
|
+
if (isLocked && !stablePosition && displayComponent && adjustedPosition.x > 0 && adjustedPosition.y > 0) {
|
|
178
|
+
setStablePosition(adjustedPosition);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
// Only set stable position when component changes or when we don't have one yet
|
|
182
|
+
if (currentComponentId !== lastComponentIdRef.current && !stablePosition && displayComponent && adjustedPosition.x > 0 && adjustedPosition.y > 0) {
|
|
183
|
+
lastComponentIdRef.current = currentComponentId;
|
|
184
|
+
// Set stable position after a small delay to ensure tooltip is rendered
|
|
185
|
+
const timer = setTimeout(() => {
|
|
186
|
+
setStablePosition(adjustedPosition);
|
|
187
|
+
}, 50);
|
|
188
|
+
return () => clearTimeout(timer);
|
|
189
|
+
}
|
|
190
|
+
}, [displayComponent?.componentId, adjustedPosition, stablePosition, displayComponent, isLocked]);
|
|
191
|
+
const handleCopy = async () => {
|
|
192
|
+
if (!displayComponent || !hoveredElement)
|
|
193
|
+
return;
|
|
194
|
+
const text = formatMetadataForClipboard(displayComponent, hoveredElement);
|
|
195
|
+
try {
|
|
196
|
+
await navigator.clipboard.writeText(text);
|
|
197
|
+
setCopied(true);
|
|
198
|
+
setTimeout(() => setCopied(false), 2000);
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
console.error("Failed to copy:", err);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
// Reset stable position when element changes (but not when locked)
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (!isLocked) {
|
|
207
|
+
setStablePosition(null);
|
|
208
|
+
}
|
|
209
|
+
}, [hoveredElement, isLocked]);
|
|
210
|
+
// Show tooltip for any hovered element when CTRL is held
|
|
211
|
+
if (!isInspectionActive || !displayComponent) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
return (_jsxs(Paper, { ref: tooltipRef, elevation: 8, sx: {
|
|
215
|
+
position: "fixed",
|
|
216
|
+
left: stablePosition?.x ?? adjustedPosition.x,
|
|
217
|
+
top: stablePosition?.y ?? adjustedPosition.y,
|
|
218
|
+
zIndex: 999999,
|
|
219
|
+
minWidth: 300,
|
|
220
|
+
maxWidth: 500,
|
|
221
|
+
p: 1.5,
|
|
222
|
+
pointerEvents: "auto",
|
|
223
|
+
backgroundColor: "rgba(18, 18, 18, 0.95)",
|
|
224
|
+
backdropFilter: "blur(8px)",
|
|
225
|
+
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
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 CTRL to unlock)" }))] }), _jsx(MuiTooltip, { title: copied ? "Copied!" : "Copy metadata", children: _jsx(IconButton, { size: "small", onClick: handleCopy, sx: {
|
|
228
|
+
color: copied ? "#4caf50" : "#fff",
|
|
229
|
+
"&:hover": { backgroundColor: "rgba(255, 255, 255, 0.1)" },
|
|
230
|
+
p: 0.5,
|
|
231
|
+
}, children: _jsx(ContentCopyIcon, { fontSize: "small" }) }) })] }), _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 })] })] }));
|
|
232
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React, { ReactElement, ComponentType } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Props for InspectionWrapper
|
|
4
|
+
*/
|
|
5
|
+
interface InspectionWrapperProps {
|
|
6
|
+
componentName: string;
|
|
7
|
+
variant?: string;
|
|
8
|
+
role?: string;
|
|
9
|
+
usagePath: string;
|
|
10
|
+
props: Record<string, any>;
|
|
11
|
+
sourceFile: string;
|
|
12
|
+
children: ReactElement;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Wrapper component that adds inspection metadata to a component
|
|
16
|
+
*/
|
|
17
|
+
export declare const InspectionWrapper: React.FC<InspectionWrapperProps>;
|
|
18
|
+
/**
|
|
19
|
+
* HOC to wrap a component with inspection capabilities
|
|
20
|
+
*/
|
|
21
|
+
export declare function withInspection<P extends object>(Component: ComponentType<P>, inspectionConfig: {
|
|
22
|
+
componentName: string;
|
|
23
|
+
variant?: string;
|
|
24
|
+
role?: string;
|
|
25
|
+
getUsagePath: (props: P) => string;
|
|
26
|
+
getSourceFile: () => string;
|
|
27
|
+
}): ComponentType<P>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { useInspection } from "./InspectionContext";
|
|
4
|
+
import { generateComponentId, formatPropsSignature, getComponentName, getNextInstanceIndex, } from "./inspection";
|
|
5
|
+
/**
|
|
6
|
+
* Wrapper component that adds inspection metadata to a component
|
|
7
|
+
*/
|
|
8
|
+
export const InspectionWrapper = ({ componentName, variant, role, usagePath, props, sourceFile, children, }) => {
|
|
9
|
+
const { isInspectionActive, setHoveredComponent } = useInspection();
|
|
10
|
+
const instanceIndex = React.useMemo(() => getNextInstanceIndex(componentName), [componentName]);
|
|
11
|
+
const handleMouseEnter = (e) => {
|
|
12
|
+
if (!isInspectionActive)
|
|
13
|
+
return;
|
|
14
|
+
const target = e.currentTarget;
|
|
15
|
+
const metadata = {
|
|
16
|
+
componentName,
|
|
17
|
+
componentId: generateComponentId(componentName, instanceIndex),
|
|
18
|
+
variant,
|
|
19
|
+
role,
|
|
20
|
+
usagePath,
|
|
21
|
+
instanceIndex,
|
|
22
|
+
propsSignature: formatPropsSignature(props),
|
|
23
|
+
sourceFile,
|
|
24
|
+
};
|
|
25
|
+
setHoveredComponent(metadata, target);
|
|
26
|
+
};
|
|
27
|
+
const handleMouseLeave = () => {
|
|
28
|
+
if (!isInspectionActive)
|
|
29
|
+
return;
|
|
30
|
+
setHoveredComponent(null, null);
|
|
31
|
+
};
|
|
32
|
+
// Clone the child element and add inspection handlers
|
|
33
|
+
if (!React.isValidElement(children)) {
|
|
34
|
+
return _jsx(_Fragment, { children: children });
|
|
35
|
+
}
|
|
36
|
+
const existingProps = (children.props || {});
|
|
37
|
+
const existingOnMouseEnter = existingProps.onMouseEnter;
|
|
38
|
+
const existingOnMouseLeave = existingProps.onMouseLeave;
|
|
39
|
+
const childWithProps = React.cloneElement(children, {
|
|
40
|
+
onMouseEnter: (e) => {
|
|
41
|
+
handleMouseEnter(e);
|
|
42
|
+
if (existingOnMouseEnter) {
|
|
43
|
+
existingOnMouseEnter(e);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
onMouseLeave: (e) => {
|
|
47
|
+
handleMouseLeave();
|
|
48
|
+
if (existingOnMouseLeave) {
|
|
49
|
+
existingOnMouseLeave(e);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"data-inspection-id": generateComponentId(componentName, instanceIndex),
|
|
53
|
+
"data-inspection-name": componentName,
|
|
54
|
+
});
|
|
55
|
+
return _jsx(_Fragment, { children: childWithProps });
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* HOC to wrap a component with inspection capabilities
|
|
59
|
+
*/
|
|
60
|
+
export function withInspection(Component, inspectionConfig) {
|
|
61
|
+
const WrappedComponent = (props) => {
|
|
62
|
+
const usagePath = inspectionConfig.getUsagePath(props);
|
|
63
|
+
const sourceFile = inspectionConfig.getSourceFile();
|
|
64
|
+
return (_jsx(InspectionWrapper, { componentName: inspectionConfig.componentName, variant: inspectionConfig.variant, role: inspectionConfig.role, usagePath: usagePath, props: props, sourceFile: sourceFile, children: _jsx(Component, { ...props }) }));
|
|
65
|
+
};
|
|
66
|
+
WrappedComponent.displayName = `withInspection(${getComponentName(Component)})`;
|
|
67
|
+
return WrappedComponent;
|
|
68
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic inspection detection via data attributes
|
|
3
|
+
* Components can add data-inspection-* attributes and the system will detect them automatically
|
|
4
|
+
*/
|
|
5
|
+
import { ComponentMetadata } from "./InspectionContext";
|
|
6
|
+
/**
|
|
7
|
+
* Parse component metadata from data attributes or infer from element
|
|
8
|
+
*/
|
|
9
|
+
export declare const parseInspectionMetadata: (element: HTMLElement) => ComponentMetadata | null;
|
|
10
|
+
/**
|
|
11
|
+
* Setup global mouse event listener for automatic inspection detection
|
|
12
|
+
* This works with components that have data-inspection-* attributes
|
|
13
|
+
*/
|
|
14
|
+
export declare const setupAutoInspection: (setHoveredComponent: (metadata: ComponentMetadata | null, element: HTMLElement | null) => void, isInspectionActive: boolean, isLocked: boolean) => (() => void);
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic inspection detection via data attributes
|
|
3
|
+
* Components can add data-inspection-* attributes and the system will detect them automatically
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Extract basic element info for inspection
|
|
7
|
+
*/
|
|
8
|
+
const extractElementInfo = (element) => {
|
|
9
|
+
const tagName = element.tagName.toLowerCase();
|
|
10
|
+
// Convert className to string - it can be a DOMTokenList or string
|
|
11
|
+
let className = "";
|
|
12
|
+
if (typeof element.className === "string") {
|
|
13
|
+
className = element.className;
|
|
14
|
+
}
|
|
15
|
+
else if (element.className) {
|
|
16
|
+
// DOMTokenList - convert to string
|
|
17
|
+
className = String(element.className);
|
|
18
|
+
}
|
|
19
|
+
const id = element.id || "";
|
|
20
|
+
// Extract text preview (first 50 chars)
|
|
21
|
+
const textContent = element.textContent || "";
|
|
22
|
+
const textPreview = textContent.trim().substring(0, 50).replace(/\s+/g, " ");
|
|
23
|
+
// Try to detect MUI components from class names
|
|
24
|
+
let muiComponent;
|
|
25
|
+
if (className && typeof className === "string" && className.includes("Mui")) {
|
|
26
|
+
const muiMatch = className.match(/Mui(\w+)/);
|
|
27
|
+
if (muiMatch) {
|
|
28
|
+
muiComponent = muiMatch[1];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { tagName, className, id, textPreview, muiComponent };
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Parse component metadata from data attributes or infer from element
|
|
35
|
+
*/
|
|
36
|
+
export const parseInspectionMetadata = (element) => {
|
|
37
|
+
if (process.env.NODE_ENV !== "development") {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
// First, try to find explicit inspection metadata
|
|
41
|
+
const componentName = element.getAttribute("data-inspection-name");
|
|
42
|
+
const componentId = element.getAttribute("data-inspection-id");
|
|
43
|
+
if (componentName && componentId) {
|
|
44
|
+
// Has explicit metadata - use it
|
|
45
|
+
const variant = element.getAttribute("data-inspection-variant") || undefined;
|
|
46
|
+
const role = element.getAttribute("data-inspection-role") || undefined;
|
|
47
|
+
const usagePath = element.getAttribute("data-inspection-usage-path") || "Unknown";
|
|
48
|
+
const instanceIndex = parseInt(element.getAttribute("data-inspection-instance") || "0", 10);
|
|
49
|
+
const propsSignature = element.getAttribute("data-inspection-props") || "default";
|
|
50
|
+
const sourceFile = element.getAttribute("data-inspection-file") || "Unknown";
|
|
51
|
+
return {
|
|
52
|
+
componentName,
|
|
53
|
+
componentId,
|
|
54
|
+
variant,
|
|
55
|
+
role,
|
|
56
|
+
usagePath,
|
|
57
|
+
instanceIndex,
|
|
58
|
+
propsSignature,
|
|
59
|
+
sourceFile,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// No explicit metadata - infer from element
|
|
63
|
+
const info = extractElementInfo(element);
|
|
64
|
+
// Generate component name
|
|
65
|
+
let inferredName = info.muiComponent || info.tagName.toUpperCase();
|
|
66
|
+
if (info.id) {
|
|
67
|
+
inferredName = `${inferredName} (${info.id})`;
|
|
68
|
+
}
|
|
69
|
+
else if (info.className) {
|
|
70
|
+
// Try to extract meaningful class name
|
|
71
|
+
const classes = info.className.split(/\s+/).filter(c => c && !c.startsWith("Mui") && c.length > 2);
|
|
72
|
+
if (classes.length > 0) {
|
|
73
|
+
inferredName = `${inferredName} .${classes[0]}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Generate component ID - use deterministic hash based on element's position in DOM tree
|
|
77
|
+
// This ensures the same element always gets the same ID, even without explicit metadata
|
|
78
|
+
const generateDeterministicId = (el, tagName) => {
|
|
79
|
+
if (info.id) {
|
|
80
|
+
return info.id;
|
|
81
|
+
}
|
|
82
|
+
// Create a stable identifier based on element's path in DOM tree
|
|
83
|
+
const path = [];
|
|
84
|
+
let current = el;
|
|
85
|
+
// Walk up to body, collecting tag names and indices
|
|
86
|
+
while (current && current !== document.body && current !== document.documentElement) {
|
|
87
|
+
const currentTagName = current.tagName.toLowerCase();
|
|
88
|
+
const parent = current.parentElement;
|
|
89
|
+
if (parent) {
|
|
90
|
+
// Get index among siblings with same tag name
|
|
91
|
+
const siblings = Array.from(parent.children).filter((child) => child.tagName.toLowerCase() === currentTagName);
|
|
92
|
+
const index = siblings.indexOf(current);
|
|
93
|
+
path.unshift(`${currentTagName}[${index}]`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
path.unshift(currentTagName);
|
|
97
|
+
}
|
|
98
|
+
current = parent;
|
|
99
|
+
}
|
|
100
|
+
// Create hash from path (simple hash function)
|
|
101
|
+
const pathString = path.join('>');
|
|
102
|
+
let hash = 0;
|
|
103
|
+
for (let i = 0; i < pathString.length; i++) {
|
|
104
|
+
const char = pathString.charCodeAt(i);
|
|
105
|
+
hash = ((hash << 5) - hash) + char;
|
|
106
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
107
|
+
}
|
|
108
|
+
// Convert to positive hex string (6 chars)
|
|
109
|
+
const hashStr = Math.abs(hash).toString(36).substr(0, 6);
|
|
110
|
+
return `${tagName}-${hashStr}`;
|
|
111
|
+
};
|
|
112
|
+
const inferredId = generateDeterministicId(element, info.tagName);
|
|
113
|
+
// Build props signature from attributes
|
|
114
|
+
const props = [];
|
|
115
|
+
if (info.id)
|
|
116
|
+
props.push(`id=${info.id}`);
|
|
117
|
+
if (info.className) {
|
|
118
|
+
const classCount = info.className.split(/\s+/).length;
|
|
119
|
+
props.push(`classes=${classCount}`);
|
|
120
|
+
}
|
|
121
|
+
const propsSignature = props.length > 0 ? props.join(", ") : "default";
|
|
122
|
+
return {
|
|
123
|
+
componentName: inferredName,
|
|
124
|
+
componentId: inferredId,
|
|
125
|
+
variant: info.muiComponent ? info.muiComponent.toLowerCase() : undefined,
|
|
126
|
+
role: element.getAttribute("role") || element.getAttribute("aria-label") || undefined,
|
|
127
|
+
usagePath: "DOM Element",
|
|
128
|
+
instanceIndex: 0,
|
|
129
|
+
propsSignature,
|
|
130
|
+
sourceFile: "DOM",
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Setup global mouse event listener for automatic inspection detection
|
|
135
|
+
* This works with components that have data-inspection-* attributes
|
|
136
|
+
*/
|
|
137
|
+
export const setupAutoInspection = (setHoveredComponent, isInspectionActive, isLocked) => {
|
|
138
|
+
if (process.env.NODE_ENV !== "development") {
|
|
139
|
+
return () => { }; // No-op in production
|
|
140
|
+
}
|
|
141
|
+
const handleMouseMove = (e) => {
|
|
142
|
+
if (!isInspectionActive) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// If locked, don't update on mouse move - keep current tooltip fixed
|
|
146
|
+
if (isLocked) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const target = e.target;
|
|
150
|
+
if (!target || !document.body.contains(target)) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
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
|
+
}
|
|
196
|
+
}
|
|
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);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
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
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
const handleMouseLeave = () => {
|
|
248
|
+
if (isInspectionActive && !isLocked) {
|
|
249
|
+
setHoveredComponent(null, null);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
253
|
+
document.addEventListener("mouseleave", handleMouseLeave);
|
|
254
|
+
return () => {
|
|
255
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
256
|
+
document.removeEventListener("mouseleave", handleMouseLeave);
|
|
257
|
+
};
|
|
258
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { InspectionProvider, useInspection, ComponentMetadata } from './InspectionContext';
|
|
2
|
+
export { InspectionTooltip } from './InspectionTooltip';
|
|
3
|
+
export { InspectionHighlight } from './InspectionHighlight';
|
|
4
|
+
export { InspectionWrapper, withInspection } from './InspectionWrapper';
|
|
5
|
+
export { useInspectionMetadata } from './useInspectionMetadata';
|
|
6
|
+
export { setupInterceptors, setInspectionActive, shouldBlockRequest } from './inspectionInterceptors';
|
|
7
|
+
export { generateComponentId, formatPropsSignature, formatMetadataForClipboard, getComponentName, getNextInstanceIndex } from './inspection';
|
|
8
|
+
export { setupAutoInspection, parseInspectionMetadata } from './autoInspection';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Main exports
|
|
2
|
+
export { InspectionProvider, useInspection } from './InspectionContext';
|
|
3
|
+
export { InspectionTooltip } from './InspectionTooltip';
|
|
4
|
+
export { InspectionHighlight } from './InspectionHighlight';
|
|
5
|
+
export { InspectionWrapper, withInspection } from './InspectionWrapper';
|
|
6
|
+
export { useInspectionMetadata } from './useInspectionMetadata';
|
|
7
|
+
export { setupInterceptors, setInspectionActive, shouldBlockRequest } from './inspectionInterceptors';
|
|
8
|
+
export { generateComponentId, formatPropsSignature, formatMetadataForClipboard, getComponentName, getNextInstanceIndex } from './inspection';
|
|
9
|
+
export { setupAutoInspection, parseInspectionMetadata } from './autoInspection';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ComponentMetadata } from "./InspectionContext";
|
|
2
|
+
/**
|
|
3
|
+
* Generate a unique component ID
|
|
4
|
+
*/
|
|
5
|
+
export declare const generateComponentId: (componentName: string, instanceIndex: number) => string;
|
|
6
|
+
/**
|
|
7
|
+
* Format props signature for display
|
|
8
|
+
*/
|
|
9
|
+
export declare const formatPropsSignature: (props: Record<string, any>) => string;
|
|
10
|
+
/**
|
|
11
|
+
* Get component name from component type
|
|
12
|
+
*/
|
|
13
|
+
export declare const getComponentName: (component: React.ComponentType<any> | string) => string;
|
|
14
|
+
/**
|
|
15
|
+
* Build usage path from component hierarchy
|
|
16
|
+
*/
|
|
17
|
+
export declare const buildUsagePath: (hierarchy: string[]) => string;
|
|
18
|
+
/**
|
|
19
|
+
* Format metadata for clipboard with full element details
|
|
20
|
+
*/
|
|
21
|
+
export declare const formatMetadataForClipboard: (metadata: ComponentMetadata, element: HTMLElement) => string;
|
|
22
|
+
/**
|
|
23
|
+
* Get next instance index for a component
|
|
24
|
+
*/
|
|
25
|
+
export declare const getNextInstanceIndex: (componentName: string) => number;
|
|
26
|
+
/**
|
|
27
|
+
* Reset instance counts (useful for testing or remounting)
|
|
28
|
+
*/
|
|
29
|
+
export declare const resetInstanceCounts: () => void;
|