@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.
Files changed (52) hide show
  1. package/README.md +245 -0
  2. package/dist/cjs/index.cjs +1550 -0
  3. package/dist/cjs/index.cjs.map +7 -0
  4. package/dist/cjs/index.native.cjs +1004 -0
  5. package/dist/cjs/index.native.cjs.map +7 -0
  6. package/dist/cjs/index.web.cjs +83 -0
  7. package/dist/cjs/index.web.cjs.map +7 -0
  8. package/dist/esm/index.js +1524 -0
  9. package/dist/esm/index.js.map +7 -0
  10. package/dist/esm/index.native.js +1012 -0
  11. package/dist/esm/index.native.js.map +7 -0
  12. package/dist/esm/index.web.js +62 -0
  13. package/dist/esm/index.web.js.map +7 -0
  14. package/dist/types/components/AnnotationInput.d.ts +8 -0
  15. package/dist/types/components/AnnotationList.d.ts +1 -0
  16. package/dist/types/components/Draggable.d.ts +10 -0
  17. package/dist/types/components/Highlighter.d.ts +1 -0
  18. package/dist/types/components/Toolbar.d.ts +1 -0
  19. package/dist/types/index.d.ts +20 -0
  20. package/dist/types/index.web.d.ts +69 -0
  21. package/dist/types/store.d.ts +66 -0
  22. package/dist/types/utils/fiber.d.ts +51 -0
  23. package/dist/types/utils/platform.d.ts +8 -0
  24. package/dist/types/utils/screenshot.d.ts +28 -0
  25. package/package.json +115 -0
  26. package/src/components/AnnotationInput.tsx +269 -0
  27. package/src/components/AnnotationList.tsx +248 -0
  28. package/src/components/Draggable.tsx +73 -0
  29. package/src/components/Highlighter.tsx +497 -0
  30. package/src/components/Toolbar.tsx +213 -0
  31. package/src/components/native/AnnotationInput.tsx +227 -0
  32. package/src/components/native/AnnotationList.tsx +157 -0
  33. package/src/components/native/Draggable.tsx +65 -0
  34. package/src/components/native/Highlighter.tsx +239 -0
  35. package/src/components/native/Toolbar.tsx +192 -0
  36. package/src/components/native/index.ts +6 -0
  37. package/src/components/web/AnnotationInput.tsx +150 -0
  38. package/src/components/web/AnnotationList.tsx +117 -0
  39. package/src/components/web/Draggable.tsx +74 -0
  40. package/src/components/web/Highlighter.tsx +329 -0
  41. package/src/components/web/Toolbar.tsx +198 -0
  42. package/src/components/web/index.ts +6 -0
  43. package/src/extension.tsx +15 -0
  44. package/src/index.native.tsx +50 -0
  45. package/src/index.tsx +41 -0
  46. package/src/index.web.tsx +124 -0
  47. package/src/store.tsx +120 -0
  48. package/src/utils/fiber.native.ts +90 -0
  49. package/src/utils/fiber.ts +255 -0
  50. package/src/utils/platform.ts +33 -0
  51. package/src/utils/screenshot.native.ts +139 -0
  52. 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
+ &lt;{componentDetails.elementInfo.tagName}
395
+ {componentDetails.elementInfo.id && ` id="${componentDetails.elementInfo.id}"`}
396
+ {componentDetails.elementInfo.className && ` class="${componentDetails.elementInfo.className}"`}
397
+ &gt;
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
+ }