@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,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a unique component ID
|
|
3
|
+
*/
|
|
4
|
+
export const generateComponentId = (componentName, instanceIndex) => {
|
|
5
|
+
return `${componentName.toLowerCase().replace(/\s+/g, "-")}-${instanceIndex}`;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Format props signature for display
|
|
9
|
+
*/
|
|
10
|
+
export const formatPropsSignature = (props) => {
|
|
11
|
+
const keyProps = [];
|
|
12
|
+
// Include props that affect behavior/rendering
|
|
13
|
+
const importantProps = ["variant", "role", "type", "mode", "status", "disabled", "selected", "active"];
|
|
14
|
+
for (const key of importantProps) {
|
|
15
|
+
if (key in props && props[key] !== undefined && props[key] !== null) {
|
|
16
|
+
const value = props[key];
|
|
17
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
18
|
+
keyProps.push(`${key}=${String(value)}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return keyProps.length > 0 ? keyProps.join(", ") : "default";
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Get component name from component type
|
|
26
|
+
*/
|
|
27
|
+
export const getComponentName = (component) => {
|
|
28
|
+
if (typeof component === "string") {
|
|
29
|
+
return component;
|
|
30
|
+
}
|
|
31
|
+
return component.displayName || component.name || "Unknown";
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Build usage path from component hierarchy
|
|
35
|
+
*/
|
|
36
|
+
export const buildUsagePath = (hierarchy) => {
|
|
37
|
+
return hierarchy.join(" > ");
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Format metadata for clipboard with full element details
|
|
41
|
+
*/
|
|
42
|
+
export const formatMetadataForClipboard = (metadata, element) => {
|
|
43
|
+
const lines = [];
|
|
44
|
+
// Element identification (most important for AI understanding)
|
|
45
|
+
lines.push(`=== ELEMENT IDENTIFICATION ===`);
|
|
46
|
+
lines.push(`Element Type: ${element.tagName.toLowerCase()}`);
|
|
47
|
+
// Get element text content (label/button text)
|
|
48
|
+
const textContent = element.textContent?.trim() || "";
|
|
49
|
+
const visibleText = textContent.substring(0, 100).replace(/\s+/g, " ");
|
|
50
|
+
if (visibleText) {
|
|
51
|
+
lines.push(`Element Text/Label: "${visibleText}"`);
|
|
52
|
+
}
|
|
53
|
+
// Element ID (if exists)
|
|
54
|
+
if (element.id) {
|
|
55
|
+
lines.push(`Element ID: ${element.id}`);
|
|
56
|
+
}
|
|
57
|
+
// Element classes (first few meaningful ones)
|
|
58
|
+
if (element.className) {
|
|
59
|
+
// Convert className to string - it can be a DOMTokenList or string
|
|
60
|
+
const classNameStr = typeof element.className === "string"
|
|
61
|
+
? element.className
|
|
62
|
+
: String(element.className);
|
|
63
|
+
const classes = classNameStr.split(/\s+/).filter(c => c && !c.startsWith("Mui")).slice(0, 5);
|
|
64
|
+
if (classes.length > 0) {
|
|
65
|
+
lines.push(`Element Classes: ${classes.join(", ")}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Data attributes that might help identify the element
|
|
69
|
+
const dataAttrs = [];
|
|
70
|
+
Array.from(element.attributes).forEach(attr => {
|
|
71
|
+
if (attr.name.startsWith("data-") && !attr.name.startsWith("data-inspection-")) {
|
|
72
|
+
dataAttrs.push(`${attr.name}="${attr.value}"`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
if (dataAttrs.length > 0) {
|
|
76
|
+
lines.push(`Data Attributes: ${dataAttrs.slice(0, 3).join(", ")}`);
|
|
77
|
+
}
|
|
78
|
+
// Role/aria-label for accessibility
|
|
79
|
+
const role = element.getAttribute("role") || metadata.role;
|
|
80
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
81
|
+
if (role) {
|
|
82
|
+
lines.push(`Role: ${role}`);
|
|
83
|
+
}
|
|
84
|
+
if (ariaLabel) {
|
|
85
|
+
lines.push(`Aria Label: "${ariaLabel}"`);
|
|
86
|
+
}
|
|
87
|
+
// Component metadata
|
|
88
|
+
lines.push(``);
|
|
89
|
+
lines.push(`=== COMPONENT METADATA ===`);
|
|
90
|
+
lines.push(`Component Name: ${metadata.componentName}`);
|
|
91
|
+
lines.push(`Component ID: ${metadata.componentId}`);
|
|
92
|
+
if (metadata.variant) {
|
|
93
|
+
lines.push(`Variant: ${metadata.variant}`);
|
|
94
|
+
}
|
|
95
|
+
lines.push(`Usage Path: ${metadata.usagePath}`);
|
|
96
|
+
lines.push(`Instance: ${metadata.instanceIndex}`);
|
|
97
|
+
lines.push(`Props: ${metadata.propsSignature}`);
|
|
98
|
+
lines.push(`Source File: ${metadata.sourceFile}`);
|
|
99
|
+
// CSS selector for direct targeting
|
|
100
|
+
lines.push(``);
|
|
101
|
+
lines.push(`=== CSS SELECTOR ===`);
|
|
102
|
+
let selector = element.tagName.toLowerCase();
|
|
103
|
+
if (element.id) {
|
|
104
|
+
selector = `#${element.id}`;
|
|
105
|
+
}
|
|
106
|
+
else if (element.className) {
|
|
107
|
+
// Convert className to string - it can be a DOMTokenList or string
|
|
108
|
+
const classNameStr = typeof element.className === "string"
|
|
109
|
+
? element.className
|
|
110
|
+
: String(element.className);
|
|
111
|
+
const firstClass = classNameStr.split(/\s+/).find(c => c && !c.startsWith("Mui"));
|
|
112
|
+
if (firstClass) {
|
|
113
|
+
selector = `${element.tagName.toLowerCase()}.${firstClass}`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
lines.push(`Selector: ${selector}`);
|
|
117
|
+
// Visual description
|
|
118
|
+
const rect = element.getBoundingClientRect();
|
|
119
|
+
lines.push(`Position: (${Math.round(rect.left)}, ${Math.round(rect.top)})`);
|
|
120
|
+
lines.push(`Size: ${Math.round(rect.width)}x${Math.round(rect.height)}px`);
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Track component instances for instance indexing
|
|
125
|
+
*/
|
|
126
|
+
const componentInstanceCounts = new Map();
|
|
127
|
+
/**
|
|
128
|
+
* Get next instance index for a component
|
|
129
|
+
*/
|
|
130
|
+
export const getNextInstanceIndex = (componentName) => {
|
|
131
|
+
const current = componentInstanceCounts.get(componentName) || 0;
|
|
132
|
+
componentInstanceCounts.set(componentName, current + 1);
|
|
133
|
+
return current;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Reset instance counts (useful for testing or remounting)
|
|
137
|
+
*/
|
|
138
|
+
export const resetInstanceCounts = () => {
|
|
139
|
+
componentInstanceCounts.clear();
|
|
140
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interceptors to block API and Firebase requests when CTRL is pressed
|
|
3
|
+
* Only active in development
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Set inspection active state (called from InspectionContext)
|
|
7
|
+
*/
|
|
8
|
+
export declare const setInspectionActive: (active: boolean) => void;
|
|
9
|
+
/**
|
|
10
|
+
* Check if inspection is active and requests should be blocked
|
|
11
|
+
*/
|
|
12
|
+
export declare const shouldBlockRequest: () => boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Intercept fetch requests (for API calls)
|
|
15
|
+
*/
|
|
16
|
+
export declare const setupFetchInterceptor: () => void;
|
|
17
|
+
/**
|
|
18
|
+
* Intercept Firebase Firestore operations
|
|
19
|
+
* Note: Firestore operations are harder to intercept at the SDK level
|
|
20
|
+
* We'll rely on blocking at the fetch level (which Firestore uses internally)
|
|
21
|
+
* and also provide a wrapper utility for direct Firestore calls
|
|
22
|
+
*/
|
|
23
|
+
export declare const setupFirestoreInterceptor: () => void;
|
|
24
|
+
/**
|
|
25
|
+
* Setup all interceptors
|
|
26
|
+
*/
|
|
27
|
+
export declare const setupInterceptors: () => void;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interceptors to block API and Firebase requests when CTRL is pressed
|
|
3
|
+
* Only active in development
|
|
4
|
+
*/
|
|
5
|
+
let isInspectionActive = false;
|
|
6
|
+
/**
|
|
7
|
+
* Set inspection active state (called from InspectionContext)
|
|
8
|
+
*/
|
|
9
|
+
export const setInspectionActive = (active) => {
|
|
10
|
+
if (process.env.NODE_ENV !== "development") {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
isInspectionActive = active;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Check if inspection is active and requests should be blocked
|
|
17
|
+
*/
|
|
18
|
+
export const shouldBlockRequest = () => {
|
|
19
|
+
if (process.env.NODE_ENV !== "development") {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return isInspectionActive;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Intercept fetch requests (for API calls)
|
|
26
|
+
*/
|
|
27
|
+
export const setupFetchInterceptor = () => {
|
|
28
|
+
if (process.env.NODE_ENV !== "development") {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const originalFetch = window.fetch;
|
|
32
|
+
window.fetch = async (...args) => {
|
|
33
|
+
if (shouldBlockRequest()) {
|
|
34
|
+
console.warn("[Inspection] Blocked API request (CTRL held):", args[0]);
|
|
35
|
+
// Return a rejected promise to prevent the request
|
|
36
|
+
return Promise.reject(new Error("Request blocked: Inspection mode active (CTRL held)"));
|
|
37
|
+
}
|
|
38
|
+
return originalFetch(...args);
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Intercept Firebase Firestore operations
|
|
43
|
+
* Note: Firestore operations are harder to intercept at the SDK level
|
|
44
|
+
* We'll rely on blocking at the fetch level (which Firestore uses internally)
|
|
45
|
+
* and also provide a wrapper utility for direct Firestore calls
|
|
46
|
+
*/
|
|
47
|
+
export const setupFirestoreInterceptor = () => {
|
|
48
|
+
if (process.env.NODE_ENV !== "development") {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Firestore uses fetch internally, so the fetch interceptor will catch most operations
|
|
52
|
+
// For direct Firestore SDK calls, we'll need to wrap them manually
|
|
53
|
+
// This is a limitation of the Firestore SDK architecture
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Setup all interceptors
|
|
57
|
+
*/
|
|
58
|
+
export const setupInterceptors = () => {
|
|
59
|
+
if (process.env.NODE_ENV !== "development") {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
setupFetchInterceptor();
|
|
63
|
+
setupFirestoreInterceptor();
|
|
64
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Hook to add inspection metadata to a component
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* ```tsx
|
|
7
|
+
* const MyComponent = (props) => {
|
|
8
|
+
* const inspectionProps = useInspectionMetadata({
|
|
9
|
+
* componentName: "MyComponent",
|
|
10
|
+
* variant: props.variant,
|
|
11
|
+
* usagePath: "ActivityPage > TransactionList",
|
|
12
|
+
* props,
|
|
13
|
+
* sourceFile: "src/components/MyComponent.tsx",
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* return <Box {...inspectionProps}>...</Box>;
|
|
17
|
+
* };
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare const useInspectionMetadata: (config: {
|
|
21
|
+
componentName: string;
|
|
22
|
+
variant?: string;
|
|
23
|
+
role?: string;
|
|
24
|
+
usagePath: string;
|
|
25
|
+
props: Record<string, any>;
|
|
26
|
+
sourceFile: string;
|
|
27
|
+
}) => {
|
|
28
|
+
onMouseEnter: (e: React.MouseEvent) => void;
|
|
29
|
+
onMouseLeave: () => void;
|
|
30
|
+
"data-inspection-id": string;
|
|
31
|
+
"data-inspection-name": string;
|
|
32
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useInspection } from "./InspectionContext";
|
|
3
|
+
import { generateComponentId, formatPropsSignature, getNextInstanceIndex, } from "./inspection";
|
|
4
|
+
/**
|
|
5
|
+
* Hook to add inspection metadata to a component
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```tsx
|
|
9
|
+
* const MyComponent = (props) => {
|
|
10
|
+
* const inspectionProps = useInspectionMetadata({
|
|
11
|
+
* componentName: "MyComponent",
|
|
12
|
+
* variant: props.variant,
|
|
13
|
+
* usagePath: "ActivityPage > TransactionList",
|
|
14
|
+
* props,
|
|
15
|
+
* sourceFile: "src/components/MyComponent.tsx",
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* return <Box {...inspectionProps}>...</Box>;
|
|
19
|
+
* };
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export const useInspectionMetadata = (config) => {
|
|
23
|
+
const { isInspectionActive, setHoveredComponent } = useInspection();
|
|
24
|
+
const instanceIndex = useMemo(() => getNextInstanceIndex(config.componentName), [config.componentName]);
|
|
25
|
+
const metadata = useMemo(() => ({
|
|
26
|
+
componentName: config.componentName,
|
|
27
|
+
componentId: generateComponentId(config.componentName, instanceIndex),
|
|
28
|
+
variant: config.variant,
|
|
29
|
+
role: config.role,
|
|
30
|
+
usagePath: config.usagePath,
|
|
31
|
+
instanceIndex,
|
|
32
|
+
propsSignature: formatPropsSignature(config.props),
|
|
33
|
+
sourceFile: config.sourceFile,
|
|
34
|
+
}), [config, instanceIndex]);
|
|
35
|
+
const handleMouseEnter = (e) => {
|
|
36
|
+
if (!isInspectionActive)
|
|
37
|
+
return;
|
|
38
|
+
const target = e.currentTarget;
|
|
39
|
+
setHoveredComponent(metadata, target);
|
|
40
|
+
};
|
|
41
|
+
const handleMouseLeave = () => {
|
|
42
|
+
if (!isInspectionActive)
|
|
43
|
+
return;
|
|
44
|
+
setHoveredComponent(null, null);
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
onMouseEnter: handleMouseEnter,
|
|
48
|
+
onMouseLeave: handleMouseLeave,
|
|
49
|
+
"data-inspection-id": metadata.componentId,
|
|
50
|
+
"data-inspection-name": config.componentName,
|
|
51
|
+
};
|
|
52
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zargaryanvh/react-component-inspector",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A development tool for inspecting React components with AI-friendly metadata extraction",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"require": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/**/*",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"package.json"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"prepublishOnly": "npm run build",
|
|
23
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"react",
|
|
27
|
+
"component",
|
|
28
|
+
"inspection",
|
|
29
|
+
"development",
|
|
30
|
+
"debugging",
|
|
31
|
+
"ai",
|
|
32
|
+
"metadata",
|
|
33
|
+
"tool",
|
|
34
|
+
"cursor",
|
|
35
|
+
"cursor-ai"
|
|
36
|
+
],
|
|
37
|
+
"author": "zargaryanvh",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"react": ">=16.8.0",
|
|
41
|
+
"react-dom": ">=16.8.0",
|
|
42
|
+
"@mui/material": ">=5.0.0",
|
|
43
|
+
"@mui/icons-material": ">=5.0.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/react": "^18.0.0",
|
|
47
|
+
"@types/react-dom": "^18.0.0",
|
|
48
|
+
"typescript": "^5.0.0"
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+https://github.com/zargaryanvh/react-component-inspector.git"
|
|
53
|
+
}
|
|
54
|
+
}
|