@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.
@@ -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
+ };
@@ -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;