@np-dev/ui-ai-anotation 0.1.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/README.md +245 -0
- package/dist/cjs/index.cjs +1550 -0
- package/dist/cjs/index.cjs.map +7 -0
- package/dist/cjs/index.native.cjs +1004 -0
- package/dist/cjs/index.native.cjs.map +7 -0
- package/dist/cjs/index.web.cjs +83 -0
- package/dist/cjs/index.web.cjs.map +7 -0
- package/dist/esm/index.js +1524 -0
- package/dist/esm/index.js.map +7 -0
- package/dist/esm/index.native.js +1012 -0
- package/dist/esm/index.native.js.map +7 -0
- package/dist/esm/index.web.js +62 -0
- package/dist/esm/index.web.js.map +7 -0
- package/dist/types/components/AnnotationInput.d.ts +8 -0
- package/dist/types/components/AnnotationList.d.ts +1 -0
- package/dist/types/components/Draggable.d.ts +10 -0
- package/dist/types/components/Highlighter.d.ts +1 -0
- package/dist/types/components/Toolbar.d.ts +1 -0
- package/dist/types/index.d.ts +20 -0
- package/dist/types/index.web.d.ts +69 -0
- package/dist/types/store.d.ts +66 -0
- package/dist/types/utils/fiber.d.ts +51 -0
- package/dist/types/utils/platform.d.ts +8 -0
- package/dist/types/utils/screenshot.d.ts +28 -0
- package/package.json +115 -0
- package/src/components/AnnotationInput.tsx +269 -0
- package/src/components/AnnotationList.tsx +248 -0
- package/src/components/Draggable.tsx +73 -0
- package/src/components/Highlighter.tsx +497 -0
- package/src/components/Toolbar.tsx +213 -0
- package/src/components/native/AnnotationInput.tsx +227 -0
- package/src/components/native/AnnotationList.tsx +157 -0
- package/src/components/native/Draggable.tsx +65 -0
- package/src/components/native/Highlighter.tsx +239 -0
- package/src/components/native/Toolbar.tsx +192 -0
- package/src/components/native/index.ts +6 -0
- package/src/components/web/AnnotationInput.tsx +150 -0
- package/src/components/web/AnnotationList.tsx +117 -0
- package/src/components/web/Draggable.tsx +74 -0
- package/src/components/web/Highlighter.tsx +329 -0
- package/src/components/web/Toolbar.tsx +198 -0
- package/src/components/web/index.ts +6 -0
- package/src/extension.tsx +15 -0
- package/src/index.native.tsx +50 -0
- package/src/index.tsx +41 -0
- package/src/index.web.tsx +124 -0
- package/src/store.tsx +120 -0
- package/src/utils/fiber.native.ts +90 -0
- package/src/utils/fiber.ts +255 -0
- package/src/utils/platform.ts +33 -0
- package/src/utils/screenshot.native.ts +139 -0
- package/src/utils/screenshot.ts +162 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useAiAnnotation } from "../store";
|
|
3
|
+
import { AnnotationInput } from "./AnnotationInput";
|
|
4
|
+
import { captureScreenshot } from "../utils/screenshot";
|
|
5
|
+
import { getComponentDetails } from "../utils/fiber";
|
|
6
|
+
|
|
7
|
+
export function Highlighter() {
|
|
8
|
+
const { state, dispatch } = useAiAnnotation();
|
|
9
|
+
const { hoveredElement, mode, hoveredComponentInfo } = state;
|
|
10
|
+
|
|
11
|
+
const [rect, setRect] = useState<DOMRect | null>(null);
|
|
12
|
+
const [showInput, setShowInput] = useState(false);
|
|
13
|
+
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
|
|
14
|
+
const [isLocked, setIsLocked] = useState(false);
|
|
15
|
+
const [lockedPos, setLockedPos] = useState({ x: 0, y: 0 });
|
|
16
|
+
const [isCapturing, setIsCapturing] = useState(false);
|
|
17
|
+
const [captureSuccess, setCaptureSuccess] = useState(false);
|
|
18
|
+
const [showDetails, setShowDetails] = useState(false);
|
|
19
|
+
|
|
20
|
+
const componentName = hoveredComponentInfo?.name || "";
|
|
21
|
+
const componentDetails = hoveredComponentInfo?.details;
|
|
22
|
+
|
|
23
|
+
// Detect hovered element when in inspecting mode
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (mode !== 'inspecting') return;
|
|
26
|
+
|
|
27
|
+
const handleMouseOver = (e: MouseEvent) => {
|
|
28
|
+
// Don't detect if locked (user is interacting with tooltip)
|
|
29
|
+
if (isLocked) return;
|
|
30
|
+
|
|
31
|
+
const target = e.target as HTMLElement;
|
|
32
|
+
|
|
33
|
+
// Ignore annotation UI elements
|
|
34
|
+
if (target.closest('[data-ai-annotation-ui]') || target.closest('[data-annotation-tooltip]')) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Get React component info from fiber with enhanced details
|
|
39
|
+
const details = getComponentDetails(target, {
|
|
40
|
+
includeProps: false,
|
|
41
|
+
maxParentDepth: 10,
|
|
42
|
+
maxChildDepth: 5,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
dispatch({
|
|
46
|
+
type: 'SET_HOVERED',
|
|
47
|
+
payload: { element: target, name: details.name, details }
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
document.addEventListener('mouseover', handleMouseOver);
|
|
52
|
+
|
|
53
|
+
return () => {
|
|
54
|
+
document.removeEventListener('mouseover', handleMouseOver);
|
|
55
|
+
};
|
|
56
|
+
}, [mode, isLocked, dispatch]);
|
|
57
|
+
|
|
58
|
+
// Global ESC key handler for inspecting mode - handles ESC when no element is selected
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (mode !== 'inspecting') return;
|
|
61
|
+
|
|
62
|
+
const handleGlobalEsc = (e: KeyboardEvent) => {
|
|
63
|
+
if (e.key === 'Escape' && !isLocked) {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
e.stopPropagation();
|
|
66
|
+
// No highlight block selected, disable mode
|
|
67
|
+
dispatch({ type: 'SET_MODE', payload: 'disabled' });
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
document.addEventListener('keydown', handleGlobalEsc);
|
|
72
|
+
return () => document.removeEventListener('keydown', handleGlobalEsc);
|
|
73
|
+
}, [mode, isLocked, dispatch]);
|
|
74
|
+
|
|
75
|
+
// Reset all state when mode changes to disabled
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (mode === 'disabled') {
|
|
78
|
+
setRect(null);
|
|
79
|
+
setShowInput(false);
|
|
80
|
+
setIsLocked(false);
|
|
81
|
+
setMousePos({ x: 0, y: 0 });
|
|
82
|
+
setLockedPos({ x: 0, y: 0 });
|
|
83
|
+
setIsCapturing(false);
|
|
84
|
+
setCaptureSuccess(false);
|
|
85
|
+
setShowDetails(false);
|
|
86
|
+
|
|
87
|
+
// Clear hovered element in store
|
|
88
|
+
dispatch({ type: 'RESET_HOVER' });
|
|
89
|
+
}
|
|
90
|
+
}, [mode, dispatch]);
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!hoveredElement) {
|
|
94
|
+
setRect(null);
|
|
95
|
+
setShowInput(false);
|
|
96
|
+
setIsLocked(false);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const updateRect = () => {
|
|
101
|
+
const newRect = hoveredElement.getBoundingClientRect();
|
|
102
|
+
setRect(newRect);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
updateRect();
|
|
106
|
+
|
|
107
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
108
|
+
// Only update mouse position if not locked
|
|
109
|
+
if (!isLocked) {
|
|
110
|
+
setMousePos({ x: e.clientX, y: e.clientY });
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Click handler to lock/unlock tooltip
|
|
115
|
+
const handleClick = (e: MouseEvent) => {
|
|
116
|
+
const target = e.target as HTMLElement;
|
|
117
|
+
|
|
118
|
+
// Ignore clicks on the tooltip buttons themselves
|
|
119
|
+
if (target.closest('[data-annotation-tooltip]')) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Ignore clicks on annotation input
|
|
124
|
+
if (target.closest('[data-ai-annotation-ui]')) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!isLocked) {
|
|
129
|
+
// Lock the tooltip at current position
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
e.stopPropagation();
|
|
132
|
+
setIsLocked(true);
|
|
133
|
+
setLockedPos({ x: e.clientX, y: e.clientY });
|
|
134
|
+
} else {
|
|
135
|
+
// Unlock and go back to following mode
|
|
136
|
+
setIsLocked(false);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// ESC key handler to unlock tooltip when locked
|
|
141
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
142
|
+
if (e.key === 'Escape' && isLocked) {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
e.stopPropagation();
|
|
145
|
+
setIsLocked(false);
|
|
146
|
+
setShowInput(false);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
window.addEventListener("scroll", updateRect, true);
|
|
151
|
+
window.addEventListener("resize", updateRect);
|
|
152
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
153
|
+
document.addEventListener("click", handleClick, true);
|
|
154
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
155
|
+
|
|
156
|
+
return () => {
|
|
157
|
+
window.removeEventListener("scroll", updateRect, true);
|
|
158
|
+
window.removeEventListener("resize", updateRect);
|
|
159
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
160
|
+
document.removeEventListener("click", handleClick, true);
|
|
161
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
162
|
+
};
|
|
163
|
+
}, [hoveredElement, isLocked]);
|
|
164
|
+
|
|
165
|
+
const handleScreenshot = async (e: React.MouseEvent) => {
|
|
166
|
+
e.stopPropagation();
|
|
167
|
+
if (!hoveredElement || isCapturing) return;
|
|
168
|
+
|
|
169
|
+
setIsCapturing(true);
|
|
170
|
+
try {
|
|
171
|
+
const result = await captureScreenshot(hoveredElement, {
|
|
172
|
+
copyToClipboard: true,
|
|
173
|
+
scale: 2,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (result.success) {
|
|
177
|
+
setCaptureSuccess(true);
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
setCaptureSuccess(false);
|
|
180
|
+
setIsLocked(false); // Unlock after successful capture
|
|
181
|
+
}, 1500);
|
|
182
|
+
}
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error("Screenshot failed:", error);
|
|
185
|
+
} finally {
|
|
186
|
+
setIsCapturing(false);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const handleAddClick = (e: React.MouseEvent) => {
|
|
191
|
+
e.stopPropagation();
|
|
192
|
+
setShowInput(true);
|
|
193
|
+
setShowDetails(false);
|
|
194
|
+
setIsLocked(false); // Unlock when showing input
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const handleDetailsClick = (e: React.MouseEvent) => {
|
|
198
|
+
e.stopPropagation();
|
|
199
|
+
setShowDetails(!showDetails);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const handleCloseInput = () => {
|
|
203
|
+
setShowInput(false);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
if (mode !== "inspecting" || !rect) return null;
|
|
207
|
+
|
|
208
|
+
// Use locked position or current mouse position
|
|
209
|
+
const tooltipX = isLocked ? lockedPos.x : mousePos.x;
|
|
210
|
+
const tooltipY = isLocked ? lockedPos.y : mousePos.y;
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<>
|
|
214
|
+
{/* Highlight overlay */}
|
|
215
|
+
<div
|
|
216
|
+
data-annotation-ui="true"
|
|
217
|
+
style={{
|
|
218
|
+
position: "fixed",
|
|
219
|
+
left: rect.left - 2,
|
|
220
|
+
top: rect.top - 2,
|
|
221
|
+
width: rect.width + 4,
|
|
222
|
+
height: rect.height + 4,
|
|
223
|
+
border: "2px solid #3b82f6",
|
|
224
|
+
borderRadius: 4,
|
|
225
|
+
pointerEvents: "none",
|
|
226
|
+
zIndex: 99998,
|
|
227
|
+
backgroundColor: "rgba(59, 130, 246, 0.1)",
|
|
228
|
+
}}
|
|
229
|
+
/>
|
|
230
|
+
|
|
231
|
+
{/* Component name label */}
|
|
232
|
+
{componentName && (
|
|
233
|
+
<div
|
|
234
|
+
data-annotation-ui="true"
|
|
235
|
+
style={{
|
|
236
|
+
position: "fixed",
|
|
237
|
+
left: rect.left,
|
|
238
|
+
top: rect.top - 24,
|
|
239
|
+
backgroundColor: "#3b82f6",
|
|
240
|
+
color: "white",
|
|
241
|
+
padding: "2px 8px",
|
|
242
|
+
borderRadius: 4,
|
|
243
|
+
fontSize: 12,
|
|
244
|
+
fontFamily: "monospace",
|
|
245
|
+
pointerEvents: "none",
|
|
246
|
+
zIndex: 99999,
|
|
247
|
+
}}
|
|
248
|
+
>
|
|
249
|
+
{componentName}
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
|
|
253
|
+
{/* Floating tooltip following mouse */}
|
|
254
|
+
{!showInput && (
|
|
255
|
+
<div
|
|
256
|
+
data-annotation-ui="true"
|
|
257
|
+
data-annotation-tooltip="true"
|
|
258
|
+
style={{
|
|
259
|
+
position: "fixed",
|
|
260
|
+
left: tooltipX + 16,
|
|
261
|
+
top: tooltipY + 16,
|
|
262
|
+
display: "flex",
|
|
263
|
+
flexDirection: "column",
|
|
264
|
+
gap: 6,
|
|
265
|
+
padding: 6,
|
|
266
|
+
backgroundColor: isLocked ? "rgba(0, 0, 0, 0.95)" : "rgba(0, 0, 0, 0.85)",
|
|
267
|
+
borderRadius: 8,
|
|
268
|
+
boxShadow: isLocked
|
|
269
|
+
? "0 4px 12px rgba(0, 0, 0, 0.4), 0 0 0 2px rgba(59, 130, 246, 0.5)"
|
|
270
|
+
: "0 4px 12px rgba(0, 0, 0, 0.3)",
|
|
271
|
+
zIndex: 100000,
|
|
272
|
+
pointerEvents: isLocked ? "auto" : "none",
|
|
273
|
+
backdropFilter: "blur(8px)",
|
|
274
|
+
transition: isLocked ? "none" : "left 0.05s ease-out, top 0.05s ease-out",
|
|
275
|
+
maxWidth: showDetails ? 400 : "auto",
|
|
276
|
+
}}
|
|
277
|
+
>
|
|
278
|
+
{/* Button row */}
|
|
279
|
+
<div style={{ display: "flex", gap: 6 }}>
|
|
280
|
+
{/* Screenshot button */}
|
|
281
|
+
<button
|
|
282
|
+
onClick={handleScreenshot}
|
|
283
|
+
disabled={isCapturing || !isLocked}
|
|
284
|
+
style={{
|
|
285
|
+
width: 32,
|
|
286
|
+
height: 32,
|
|
287
|
+
borderRadius: 6,
|
|
288
|
+
border: "none",
|
|
289
|
+
backgroundColor: captureSuccess
|
|
290
|
+
? "#22c55e"
|
|
291
|
+
: isCapturing
|
|
292
|
+
? "#6b7280"
|
|
293
|
+
: "#8b5cf6",
|
|
294
|
+
color: "white",
|
|
295
|
+
cursor: isLocked ? "pointer" : "default",
|
|
296
|
+
display: "flex",
|
|
297
|
+
alignItems: "center",
|
|
298
|
+
justifyContent: "center",
|
|
299
|
+
fontSize: 16,
|
|
300
|
+
transition: "all 0.15s ease",
|
|
301
|
+
opacity: isLocked ? 1 : 0.7,
|
|
302
|
+
}}
|
|
303
|
+
title="Capture screenshot"
|
|
304
|
+
>
|
|
305
|
+
{captureSuccess ? "✓" : isCapturing ? "..." : "📷"}
|
|
306
|
+
</button>
|
|
307
|
+
|
|
308
|
+
{/* Add annotation button */}
|
|
309
|
+
<button
|
|
310
|
+
onClick={handleAddClick}
|
|
311
|
+
disabled={!isLocked}
|
|
312
|
+
style={{
|
|
313
|
+
width: 32,
|
|
314
|
+
height: 32,
|
|
315
|
+
borderRadius: 6,
|
|
316
|
+
border: "none",
|
|
317
|
+
backgroundColor: "#3b82f6",
|
|
318
|
+
color: "white",
|
|
319
|
+
cursor: isLocked ? "pointer" : "default",
|
|
320
|
+
display: "flex",
|
|
321
|
+
alignItems: "center",
|
|
322
|
+
justifyContent: "center",
|
|
323
|
+
fontSize: 18,
|
|
324
|
+
fontWeight: "bold",
|
|
325
|
+
transition: "all 0.15s ease",
|
|
326
|
+
opacity: isLocked ? 1 : 0.7,
|
|
327
|
+
}}
|
|
328
|
+
title="Add annotation"
|
|
329
|
+
>
|
|
330
|
+
+
|
|
331
|
+
</button>
|
|
332
|
+
|
|
333
|
+
{/* Details toggle button */}
|
|
334
|
+
<button
|
|
335
|
+
onClick={handleDetailsClick}
|
|
336
|
+
disabled={!isLocked}
|
|
337
|
+
style={{
|
|
338
|
+
width: 32,
|
|
339
|
+
height: 32,
|
|
340
|
+
borderRadius: 6,
|
|
341
|
+
border: showDetails ? "2px solid #10b981" : "none",
|
|
342
|
+
backgroundColor: showDetails ? "#10b981" : "#6b7280",
|
|
343
|
+
color: "white",
|
|
344
|
+
cursor: isLocked ? "pointer" : "default",
|
|
345
|
+
display: "flex",
|
|
346
|
+
alignItems: "center",
|
|
347
|
+
justifyContent: "center",
|
|
348
|
+
fontSize: 14,
|
|
349
|
+
transition: "all 0.15s ease",
|
|
350
|
+
opacity: isLocked ? 1 : 0.7,
|
|
351
|
+
}}
|
|
352
|
+
title="Show component details"
|
|
353
|
+
>
|
|
354
|
+
ⓘ
|
|
355
|
+
</button>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
{/* Lock hint */}
|
|
359
|
+
{!isLocked && (
|
|
360
|
+
<div
|
|
361
|
+
style={{
|
|
362
|
+
position: "absolute",
|
|
363
|
+
bottom: -20,
|
|
364
|
+
left: "50%",
|
|
365
|
+
transform: "translateX(-50%)",
|
|
366
|
+
fontSize: 10,
|
|
367
|
+
color: "rgba(255, 255, 255, 0.7)",
|
|
368
|
+
whiteSpace: "nowrap",
|
|
369
|
+
pointerEvents: "none",
|
|
370
|
+
}}
|
|
371
|
+
>
|
|
372
|
+
Click to lock
|
|
373
|
+
</div>
|
|
374
|
+
)}
|
|
375
|
+
|
|
376
|
+
{/* Component details panel */}
|
|
377
|
+
{showDetails && componentDetails && (
|
|
378
|
+
<div
|
|
379
|
+
style={{
|
|
380
|
+
padding: "8px 4px 4px 4px",
|
|
381
|
+
borderTop: "1px solid rgba(255, 255, 255, 0.2)",
|
|
382
|
+
fontSize: 11,
|
|
383
|
+
color: "rgba(255, 255, 255, 0.9)",
|
|
384
|
+
maxHeight: 300,
|
|
385
|
+
overflowY: "auto",
|
|
386
|
+
}}
|
|
387
|
+
>
|
|
388
|
+
{/* Element info */}
|
|
389
|
+
<div style={{ marginBottom: 8 }}>
|
|
390
|
+
<div style={{ color: "#60a5fa", fontWeight: "bold", marginBottom: 4 }}>
|
|
391
|
+
Element
|
|
392
|
+
</div>
|
|
393
|
+
<div style={{ fontFamily: "monospace", color: "#f59e0b" }}>
|
|
394
|
+
<{componentDetails.elementInfo.tagName}
|
|
395
|
+
{componentDetails.elementInfo.id && ` id="${componentDetails.elementInfo.id}"`}
|
|
396
|
+
{componentDetails.elementInfo.className && ` class="${componentDetails.elementInfo.className}"`}
|
|
397
|
+
>
|
|
398
|
+
</div>
|
|
399
|
+
{componentDetails.elementInfo.textContent && (
|
|
400
|
+
<div style={{ color: "rgba(255, 255, 255, 0.6)", marginTop: 2, fontStyle: "italic" }}>
|
|
401
|
+
"{componentDetails.elementInfo.textContent}"
|
|
402
|
+
</div>
|
|
403
|
+
)}
|
|
404
|
+
<div style={{ color: "rgba(255, 255, 255, 0.5)", marginTop: 2 }}>
|
|
405
|
+
{componentDetails.elementInfo.childElementCount} child element(s)
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
|
|
409
|
+
{/* Parent hierarchy */}
|
|
410
|
+
{componentDetails.parentHierarchy.length > 0 && (
|
|
411
|
+
<div style={{ marginBottom: 8 }}>
|
|
412
|
+
<div style={{ color: "#60a5fa", fontWeight: "bold", marginBottom: 4 }}>
|
|
413
|
+
Parent Components
|
|
414
|
+
</div>
|
|
415
|
+
<div style={{ fontFamily: "monospace" }}>
|
|
416
|
+
{componentDetails.parentHierarchy.slice(0, 5).map((parent, i) => (
|
|
417
|
+
<div key={i} style={{ color: "#a78bfa", paddingLeft: i * 8 }}>
|
|
418
|
+
{"← "}{parent}
|
|
419
|
+
</div>
|
|
420
|
+
))}
|
|
421
|
+
{componentDetails.parentHierarchy.length > 5 && (
|
|
422
|
+
<div style={{ color: "rgba(255, 255, 255, 0.5)" }}>
|
|
423
|
+
... and {componentDetails.parentHierarchy.length - 5} more
|
|
424
|
+
</div>
|
|
425
|
+
)}
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
)}
|
|
429
|
+
|
|
430
|
+
{/* Child components */}
|
|
431
|
+
{componentDetails.childComponents.length > 0 && (
|
|
432
|
+
<div style={{ marginBottom: 8 }}>
|
|
433
|
+
<div style={{ color: "#60a5fa", fontWeight: "bold", marginBottom: 4 }}>
|
|
434
|
+
Child Components ({componentDetails.childComponents.length})
|
|
435
|
+
</div>
|
|
436
|
+
<div style={{ fontFamily: "monospace" }}>
|
|
437
|
+
{componentDetails.childComponents.slice(0, 10).map((child, i) => (
|
|
438
|
+
<div
|
|
439
|
+
key={i}
|
|
440
|
+
style={{
|
|
441
|
+
display: "flex",
|
|
442
|
+
justifyContent: "space-between",
|
|
443
|
+
gap: 8,
|
|
444
|
+
color: "#34d399",
|
|
445
|
+
}}
|
|
446
|
+
>
|
|
447
|
+
<span>
|
|
448
|
+
{"→ "}{child.name}
|
|
449
|
+
{child.hasChildren && " ▾"}
|
|
450
|
+
</span>
|
|
451
|
+
{child.count > 1 && (
|
|
452
|
+
<span style={{ color: "rgba(255, 255, 255, 0.5)" }}>
|
|
453
|
+
×{child.count}
|
|
454
|
+
</span>
|
|
455
|
+
)}
|
|
456
|
+
</div>
|
|
457
|
+
))}
|
|
458
|
+
{componentDetails.childComponents.length > 10 && (
|
|
459
|
+
<div style={{ color: "rgba(255, 255, 255, 0.5)" }}>
|
|
460
|
+
... and {componentDetails.childComponents.length - 10} more
|
|
461
|
+
</div>
|
|
462
|
+
)}
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
)}
|
|
466
|
+
|
|
467
|
+
{/* Important attributes */}
|
|
468
|
+
{Object.keys(componentDetails.elementInfo.attributes).length > 0 && (
|
|
469
|
+
<div>
|
|
470
|
+
<div style={{ color: "#60a5fa", fontWeight: "bold", marginBottom: 4 }}>
|
|
471
|
+
Attributes
|
|
472
|
+
</div>
|
|
473
|
+
<div style={{ fontFamily: "monospace" }}>
|
|
474
|
+
{Object.entries(componentDetails.elementInfo.attributes).map(([key, value]) => (
|
|
475
|
+
<div key={key} style={{ color: "#fbbf24" }}>
|
|
476
|
+
{key}="{value}"
|
|
477
|
+
</div>
|
|
478
|
+
))}
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
)}
|
|
482
|
+
</div>
|
|
483
|
+
)}
|
|
484
|
+
</div>
|
|
485
|
+
)}
|
|
486
|
+
|
|
487
|
+
{/* Annotation input */}
|
|
488
|
+
{showInput && (
|
|
489
|
+
<AnnotationInput
|
|
490
|
+
onClose={handleCloseInput}
|
|
491
|
+
componentName={componentName || "Unknown"}
|
|
492
|
+
componentDetails={componentDetails}
|
|
493
|
+
/>
|
|
494
|
+
)}
|
|
495
|
+
</>
|
|
496
|
+
);
|
|
497
|
+
}
|