@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,74 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
interface DraggableProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
initialPos?: { x: number; y: number };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Draggable({ children, initialPos = { x: 20, y: 20 } }: DraggableProps) {
|
|
9
|
+
const [pos, setPos] = useState(initialPos);
|
|
10
|
+
const [dragging, setDragging] = useState(false);
|
|
11
|
+
const [rel, setRel] = useState({ x: 0, y: 0 });
|
|
12
|
+
const nodeRef = useRef<HTMLDivElement>(null);
|
|
13
|
+
|
|
14
|
+
const onMouseDown = (e: React.MouseEvent) => {
|
|
15
|
+
if (e.button !== 0) return;
|
|
16
|
+
const node = nodeRef.current;
|
|
17
|
+
if (!node) return;
|
|
18
|
+
|
|
19
|
+
// Only drag if clicking the handle or the container itself, but usually we want a specific handle.
|
|
20
|
+
// For now, let's assume the whole container is draggable unless propagation stopped.
|
|
21
|
+
|
|
22
|
+
const rect = node.getBoundingClientRect();
|
|
23
|
+
setDragging(true);
|
|
24
|
+
setRel({
|
|
25
|
+
x: e.pageX - rect.left - window.scrollX,
|
|
26
|
+
y: e.pageY - rect.top - window.scrollY,
|
|
27
|
+
});
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const onMouseMove = (e: MouseEvent) => {
|
|
32
|
+
if (!dragging) return;
|
|
33
|
+
setPos({
|
|
34
|
+
x: e.pageX - rel.x,
|
|
35
|
+
y: e.pageY - rel.y,
|
|
36
|
+
});
|
|
37
|
+
e.preventDefault();
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const onMouseUp = () => {
|
|
41
|
+
setDragging(false);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (dragging) {
|
|
46
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
47
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
48
|
+
} else {
|
|
49
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
50
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
51
|
+
}
|
|
52
|
+
return () => {
|
|
53
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
54
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
55
|
+
};
|
|
56
|
+
}, [dragging]);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
ref={nodeRef}
|
|
61
|
+
style={{
|
|
62
|
+
position: 'fixed',
|
|
63
|
+
left: pos.x,
|
|
64
|
+
top: pos.y,
|
|
65
|
+
zIndex: 9999,
|
|
66
|
+
cursor: dragging ? 'grabbing' : 'grab',
|
|
67
|
+
pointerEvents: 'auto',
|
|
68
|
+
}}
|
|
69
|
+
onMouseDown={onMouseDown}
|
|
70
|
+
>
|
|
71
|
+
{children}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useAiAnnotation } from "../../store";
|
|
3
|
+
import { AnnotationInput } from "./AnnotationInput";
|
|
4
|
+
import { captureScreenshot } from "../../utils/screenshot";
|
|
5
|
+
import { getReactFiber, getComponentDisplayName } 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
|
+
|
|
19
|
+
const componentName = hoveredComponentInfo?.name || "";
|
|
20
|
+
|
|
21
|
+
// Detect hovered element when in inspecting mode
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (mode !== 'inspecting') return;
|
|
24
|
+
|
|
25
|
+
const handleMouseOver = (e: MouseEvent) => {
|
|
26
|
+
// Don't detect if locked (user is interacting with tooltip)
|
|
27
|
+
if (isLocked) return;
|
|
28
|
+
|
|
29
|
+
const target = e.target as HTMLElement;
|
|
30
|
+
|
|
31
|
+
// Ignore annotation UI elements
|
|
32
|
+
if (target.closest('[data-ai-annotation-ui]') || target.closest('[data-annotation-tooltip]')) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Get React component info from fiber
|
|
37
|
+
const fiber = getReactFiber(target);
|
|
38
|
+
const name = fiber ? getComponentDisplayName(fiber) : null;
|
|
39
|
+
|
|
40
|
+
dispatch({
|
|
41
|
+
type: 'SET_HOVERED',
|
|
42
|
+
payload: { element: target, name }
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
document.addEventListener('mouseover', handleMouseOver);
|
|
47
|
+
|
|
48
|
+
return () => {
|
|
49
|
+
document.removeEventListener('mouseover', handleMouseOver);
|
|
50
|
+
};
|
|
51
|
+
}, [mode, isLocked, dispatch]);
|
|
52
|
+
|
|
53
|
+
// Reset all state when mode changes to disabled
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (mode === 'disabled') {
|
|
56
|
+
setRect(null);
|
|
57
|
+
setShowInput(false);
|
|
58
|
+
setIsLocked(false);
|
|
59
|
+
setMousePos({ x: 0, y: 0 });
|
|
60
|
+
setLockedPos({ x: 0, y: 0 });
|
|
61
|
+
setIsCapturing(false);
|
|
62
|
+
setCaptureSuccess(false);
|
|
63
|
+
|
|
64
|
+
// Clear hovered element in store
|
|
65
|
+
dispatch({ type: 'RESET_HOVER' });
|
|
66
|
+
}
|
|
67
|
+
}, [mode, dispatch]);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!hoveredElement) {
|
|
71
|
+
setRect(null);
|
|
72
|
+
setShowInput(false);
|
|
73
|
+
setIsLocked(false);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const updateRect = () => {
|
|
78
|
+
const newRect = hoveredElement.getBoundingClientRect();
|
|
79
|
+
setRect(newRect);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
updateRect();
|
|
83
|
+
|
|
84
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
85
|
+
// Only update mouse position if not locked
|
|
86
|
+
if (!isLocked) {
|
|
87
|
+
setMousePos({ x: e.clientX, y: e.clientY });
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Click handler to lock/unlock tooltip
|
|
92
|
+
const handleClick = (e: MouseEvent) => {
|
|
93
|
+
const target = e.target as HTMLElement;
|
|
94
|
+
|
|
95
|
+
// Ignore clicks on the tooltip buttons themselves
|
|
96
|
+
if (target.closest('[data-annotation-tooltip]')) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Ignore clicks on annotation input
|
|
101
|
+
if (target.closest('[data-ai-annotation-ui]')) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!isLocked) {
|
|
106
|
+
// Lock the tooltip at current position
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
e.stopPropagation();
|
|
109
|
+
setIsLocked(true);
|
|
110
|
+
setLockedPos({ x: e.clientX, y: e.clientY });
|
|
111
|
+
} else {
|
|
112
|
+
// Unlock and go back to following mode
|
|
113
|
+
setIsLocked(false);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// ESC key handler to unlock tooltip
|
|
118
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
119
|
+
if (e.key === 'Escape' && isLocked) {
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
e.stopPropagation();
|
|
122
|
+
setIsLocked(false);
|
|
123
|
+
setShowInput(false);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
window.addEventListener("scroll", updateRect, true);
|
|
128
|
+
window.addEventListener("resize", updateRect);
|
|
129
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
130
|
+
document.addEventListener("click", handleClick, true);
|
|
131
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
132
|
+
|
|
133
|
+
return () => {
|
|
134
|
+
window.removeEventListener("scroll", updateRect, true);
|
|
135
|
+
window.removeEventListener("resize", updateRect);
|
|
136
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
137
|
+
document.removeEventListener("click", handleClick, true);
|
|
138
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
139
|
+
};
|
|
140
|
+
}, [hoveredElement, isLocked]);
|
|
141
|
+
|
|
142
|
+
const handleScreenshot = async (e: React.MouseEvent) => {
|
|
143
|
+
e.stopPropagation();
|
|
144
|
+
if (!hoveredElement || isCapturing) return;
|
|
145
|
+
|
|
146
|
+
setIsCapturing(true);
|
|
147
|
+
try {
|
|
148
|
+
const result = await captureScreenshot(hoveredElement, {
|
|
149
|
+
copyToClipboard: true,
|
|
150
|
+
scale: 2,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (result.success) {
|
|
154
|
+
setCaptureSuccess(true);
|
|
155
|
+
setTimeout(() => {
|
|
156
|
+
setCaptureSuccess(false);
|
|
157
|
+
setIsLocked(false); // Unlock after successful capture
|
|
158
|
+
}, 1500);
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error("Screenshot failed:", error);
|
|
162
|
+
} finally {
|
|
163
|
+
setIsCapturing(false);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const handleAddClick = (e: React.MouseEvent) => {
|
|
168
|
+
e.stopPropagation();
|
|
169
|
+
setShowInput(true);
|
|
170
|
+
setIsLocked(false); // Unlock when showing input
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const handleCloseInput = () => {
|
|
174
|
+
setShowInput(false);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if (mode !== "inspecting" || !rect) return null;
|
|
178
|
+
|
|
179
|
+
// Use locked position or current mouse position
|
|
180
|
+
const tooltipX = isLocked ? lockedPos.x : mousePos.x;
|
|
181
|
+
const tooltipY = isLocked ? lockedPos.y : mousePos.y;
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<>
|
|
185
|
+
{/* Highlight overlay */}
|
|
186
|
+
<div
|
|
187
|
+
data-annotation-ui="true"
|
|
188
|
+
style={{
|
|
189
|
+
position: "fixed",
|
|
190
|
+
left: rect.left - 2,
|
|
191
|
+
top: rect.top - 2,
|
|
192
|
+
width: rect.width + 4,
|
|
193
|
+
height: rect.height + 4,
|
|
194
|
+
border: "2px solid #3b82f6",
|
|
195
|
+
borderRadius: 4,
|
|
196
|
+
pointerEvents: "none",
|
|
197
|
+
zIndex: 99998,
|
|
198
|
+
backgroundColor: "rgba(59, 130, 246, 0.1)",
|
|
199
|
+
}}
|
|
200
|
+
/>
|
|
201
|
+
|
|
202
|
+
{/* Component name label */}
|
|
203
|
+
{componentName && (
|
|
204
|
+
<div
|
|
205
|
+
data-annotation-ui="true"
|
|
206
|
+
style={{
|
|
207
|
+
position: "fixed",
|
|
208
|
+
left: rect.left,
|
|
209
|
+
top: rect.top - 24,
|
|
210
|
+
backgroundColor: "#3b82f6",
|
|
211
|
+
color: "white",
|
|
212
|
+
padding: "2px 8px",
|
|
213
|
+
borderRadius: 4,
|
|
214
|
+
fontSize: 12,
|
|
215
|
+
fontFamily: "monospace",
|
|
216
|
+
pointerEvents: "none",
|
|
217
|
+
zIndex: 99999,
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
{componentName}
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{/* Floating tooltip following mouse */}
|
|
225
|
+
{!showInput && (
|
|
226
|
+
<div
|
|
227
|
+
data-annotation-ui="true"
|
|
228
|
+
data-annotation-tooltip="true"
|
|
229
|
+
style={{
|
|
230
|
+
position: "fixed",
|
|
231
|
+
left: tooltipX + 16,
|
|
232
|
+
top: tooltipY + 16,
|
|
233
|
+
display: "flex",
|
|
234
|
+
gap: 6,
|
|
235
|
+
padding: 6,
|
|
236
|
+
backgroundColor: isLocked ? "rgba(0, 0, 0, 0.95)" : "rgba(0, 0, 0, 0.85)",
|
|
237
|
+
borderRadius: 8,
|
|
238
|
+
boxShadow: isLocked
|
|
239
|
+
? "0 4px 12px rgba(0, 0, 0, 0.4), 0 0 0 2px rgba(59, 130, 246, 0.5)"
|
|
240
|
+
: "0 4px 12px rgba(0, 0, 0, 0.3)",
|
|
241
|
+
zIndex: 100000,
|
|
242
|
+
pointerEvents: isLocked ? "auto" : "none",
|
|
243
|
+
backdropFilter: "blur(8px)",
|
|
244
|
+
transition: isLocked ? "none" : "left 0.05s ease-out, top 0.05s ease-out",
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
247
|
+
{/* Screenshot button */}
|
|
248
|
+
<button
|
|
249
|
+
onClick={handleScreenshot}
|
|
250
|
+
disabled={isCapturing || !isLocked}
|
|
251
|
+
style={{
|
|
252
|
+
width: 32,
|
|
253
|
+
height: 32,
|
|
254
|
+
borderRadius: 6,
|
|
255
|
+
border: "none",
|
|
256
|
+
backgroundColor: captureSuccess
|
|
257
|
+
? "#22c55e"
|
|
258
|
+
: isCapturing
|
|
259
|
+
? "#6b7280"
|
|
260
|
+
: "#8b5cf6",
|
|
261
|
+
color: "white",
|
|
262
|
+
cursor: isLocked ? "pointer" : "default",
|
|
263
|
+
display: "flex",
|
|
264
|
+
alignItems: "center",
|
|
265
|
+
justifyContent: "center",
|
|
266
|
+
fontSize: 16,
|
|
267
|
+
transition: "all 0.15s ease",
|
|
268
|
+
opacity: isLocked ? 1 : 0.7,
|
|
269
|
+
}}
|
|
270
|
+
title="Capture screenshot"
|
|
271
|
+
>
|
|
272
|
+
{captureSuccess ? "✓" : isCapturing ? "..." : "📷"}
|
|
273
|
+
</button>
|
|
274
|
+
|
|
275
|
+
{/* Add annotation button */}
|
|
276
|
+
<button
|
|
277
|
+
onClick={handleAddClick}
|
|
278
|
+
disabled={!isLocked}
|
|
279
|
+
style={{
|
|
280
|
+
width: 32,
|
|
281
|
+
height: 32,
|
|
282
|
+
borderRadius: 6,
|
|
283
|
+
border: "none",
|
|
284
|
+
backgroundColor: "#3b82f6",
|
|
285
|
+
color: "white",
|
|
286
|
+
cursor: isLocked ? "pointer" : "default",
|
|
287
|
+
display: "flex",
|
|
288
|
+
alignItems: "center",
|
|
289
|
+
justifyContent: "center",
|
|
290
|
+
fontSize: 18,
|
|
291
|
+
fontWeight: "bold",
|
|
292
|
+
transition: "all 0.15s ease",
|
|
293
|
+
opacity: isLocked ? 1 : 0.7,
|
|
294
|
+
}}
|
|
295
|
+
title="Add annotation"
|
|
296
|
+
>
|
|
297
|
+
+
|
|
298
|
+
</button>
|
|
299
|
+
|
|
300
|
+
{/* Lock hint */}
|
|
301
|
+
{!isLocked && (
|
|
302
|
+
<div
|
|
303
|
+
style={{
|
|
304
|
+
position: "absolute",
|
|
305
|
+
bottom: -20,
|
|
306
|
+
left: "50%",
|
|
307
|
+
transform: "translateX(-50%)",
|
|
308
|
+
fontSize: 10,
|
|
309
|
+
color: "rgba(255, 255, 255, 0.7)",
|
|
310
|
+
whiteSpace: "nowrap",
|
|
311
|
+
pointerEvents: "none",
|
|
312
|
+
}}
|
|
313
|
+
>
|
|
314
|
+
Click to lock
|
|
315
|
+
</div>
|
|
316
|
+
)}
|
|
317
|
+
</div>
|
|
318
|
+
)}
|
|
319
|
+
|
|
320
|
+
{/* Annotation input */}
|
|
321
|
+
{showInput && (
|
|
322
|
+
<AnnotationInput
|
|
323
|
+
onClose={handleCloseInput}
|
|
324
|
+
componentName={componentName || "Unknown"}
|
|
325
|
+
/>
|
|
326
|
+
)}
|
|
327
|
+
</>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { useAiAnnotation } from '../../store';
|
|
3
|
+
import { Draggable } from './Draggable';
|
|
4
|
+
import { GripVertical, MousePointer2, List, Copy, Minus, Maximize2, Ban, Check } from 'lucide-react';
|
|
5
|
+
import { Highlighter } from './Highlighter';
|
|
6
|
+
import { AnnotationList } from './AnnotationList';
|
|
7
|
+
|
|
8
|
+
export function Toolbar() {
|
|
9
|
+
const { state, dispatch } = useAiAnnotation();
|
|
10
|
+
const [showCopied, setShowCopied] = useState(false);
|
|
11
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
12
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
13
|
+
const [contentWidth, setContentWidth] = useState<number | null>(null);
|
|
14
|
+
|
|
15
|
+
// Measure content width for smooth animation
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (contentRef.current && !state.isMinimized) {
|
|
18
|
+
// Use requestAnimationFrame to ensure DOM is updated before measuring
|
|
19
|
+
requestAnimationFrame(() => {
|
|
20
|
+
if (contentRef.current) {
|
|
21
|
+
setContentWidth(contentRef.current.scrollWidth);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}, [state.isMinimized, showCopied, state.annotations.length]);
|
|
26
|
+
|
|
27
|
+
// Handle minimize toggle with animation
|
|
28
|
+
const handleToggleMinimized = () => {
|
|
29
|
+
setIsAnimating(true);
|
|
30
|
+
dispatch({ type: 'TOGGLE_MINIMIZED' });
|
|
31
|
+
setTimeout(() => setIsAnimating(false), 300);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const handleCopy = () => {
|
|
35
|
+
const data = state.annotations.map((a) => ({
|
|
36
|
+
component: a.componentName,
|
|
37
|
+
instruction: a.note,
|
|
38
|
+
}));
|
|
39
|
+
const text = JSON.stringify(data, null, 2);
|
|
40
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
41
|
+
setShowCopied(true);
|
|
42
|
+
setTimeout(() => setShowCopied(false), 2000);
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const toggleMode = () => {
|
|
47
|
+
dispatch({
|
|
48
|
+
type: 'SET_MODE',
|
|
49
|
+
payload: state.mode === 'disabled' ? 'inspecting' : 'disabled',
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<>
|
|
55
|
+
<Highlighter />
|
|
56
|
+
<AnnotationList />
|
|
57
|
+
<Draggable>
|
|
58
|
+
<div
|
|
59
|
+
style={{
|
|
60
|
+
backgroundColor: '#1e1e1e',
|
|
61
|
+
border: '1px solid #333',
|
|
62
|
+
borderRadius: '8px',
|
|
63
|
+
padding: '8px',
|
|
64
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
|
|
65
|
+
display: 'flex',
|
|
66
|
+
alignItems: 'center',
|
|
67
|
+
gap: '8px',
|
|
68
|
+
color: '#e5e7eb',
|
|
69
|
+
transition: 'width 0.2s',
|
|
70
|
+
}}
|
|
71
|
+
data-ai-annotation-ui
|
|
72
|
+
>
|
|
73
|
+
{/* Drag Handle */}
|
|
74
|
+
<div style={{ cursor: 'grab', color: '#666', display: 'flex' }} title="Drag">
|
|
75
|
+
<GripVertical size={20} />
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div
|
|
79
|
+
ref={contentRef}
|
|
80
|
+
style={{
|
|
81
|
+
display: 'flex',
|
|
82
|
+
alignItems: 'center',
|
|
83
|
+
gap: '8px',
|
|
84
|
+
overflow: 'hidden',
|
|
85
|
+
transition: 'max-width 0.3s ease, opacity 0.2s ease',
|
|
86
|
+
maxWidth: state.isMinimized ? 0 : (contentWidth || 300),
|
|
87
|
+
opacity: state.isMinimized ? 0 : 1,
|
|
88
|
+
paddingTop: '4px',
|
|
89
|
+
paddingBottom: '4px',
|
|
90
|
+
marginTop: '-4px',
|
|
91
|
+
marginBottom: '-4px',
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<>
|
|
95
|
+
<div style={{ width: '1px', height: '24px', backgroundColor: '#333' }} />
|
|
96
|
+
|
|
97
|
+
<button
|
|
98
|
+
onClick={toggleMode}
|
|
99
|
+
title={state.mode === 'inspecting' ? "Disable Inspection" : "Enable Inspection"}
|
|
100
|
+
style={{
|
|
101
|
+
background: state.mode === 'inspecting' ? '#3b82f6' : 'transparent',
|
|
102
|
+
border: 'none',
|
|
103
|
+
borderRadius: '4px',
|
|
104
|
+
padding: '6px',
|
|
105
|
+
color: state.mode === 'inspecting' ? 'white' : 'inherit',
|
|
106
|
+
cursor: 'pointer',
|
|
107
|
+
display: 'flex'
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
{state.mode === 'inspecting' ? <MousePointer2 size={18} /> : <Ban size={18} />}
|
|
111
|
+
</button>
|
|
112
|
+
|
|
113
|
+
<button
|
|
114
|
+
onClick={() => dispatch({ type: 'TOGGLE_LIST' })}
|
|
115
|
+
title="List Annotations"
|
|
116
|
+
style={{
|
|
117
|
+
background: 'transparent',
|
|
118
|
+
border: 'none',
|
|
119
|
+
borderRadius: '4px',
|
|
120
|
+
padding: '6px',
|
|
121
|
+
color: 'inherit',
|
|
122
|
+
cursor: 'pointer',
|
|
123
|
+
display: 'flex',
|
|
124
|
+
position: 'relative'
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
<List size={18} />
|
|
128
|
+
{state.annotations.length > 0 && (
|
|
129
|
+
<span style={{
|
|
130
|
+
position: 'absolute',
|
|
131
|
+
top: -2,
|
|
132
|
+
right: -2,
|
|
133
|
+
background: '#ef4444',
|
|
134
|
+
color: 'white',
|
|
135
|
+
fontSize: '9px',
|
|
136
|
+
width: '14px',
|
|
137
|
+
height: '14px',
|
|
138
|
+
borderRadius: '50%',
|
|
139
|
+
display: 'flex',
|
|
140
|
+
alignItems: 'center',
|
|
141
|
+
justifyContent: 'center'
|
|
142
|
+
}}>
|
|
143
|
+
{state.annotations.length}
|
|
144
|
+
</span>
|
|
145
|
+
)}
|
|
146
|
+
</button>
|
|
147
|
+
|
|
148
|
+
<button
|
|
149
|
+
onClick={handleCopy}
|
|
150
|
+
title="Copy Annotations"
|
|
151
|
+
style={{
|
|
152
|
+
background: showCopied ? '#22c55e' : 'transparent',
|
|
153
|
+
border: 'none',
|
|
154
|
+
borderRadius: '4px',
|
|
155
|
+
padding: '6px',
|
|
156
|
+
color: showCopied ? 'white' : 'inherit',
|
|
157
|
+
cursor: 'pointer',
|
|
158
|
+
display: 'flex',
|
|
159
|
+
alignItems: 'center',
|
|
160
|
+
gap: '4px',
|
|
161
|
+
transition: 'background-color 0.2s, color 0.2s',
|
|
162
|
+
minWidth: showCopied ? '75px' : 'auto',
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
{showCopied ? (
|
|
166
|
+
<>
|
|
167
|
+
<Check size={18} />
|
|
168
|
+
<span style={{ fontSize: '12px', whiteSpace: 'nowrap' }}>Copied!</span>
|
|
169
|
+
</>
|
|
170
|
+
) : (
|
|
171
|
+
<Copy size={18} />
|
|
172
|
+
)}
|
|
173
|
+
</button>
|
|
174
|
+
</>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div style={{ width: '1px', height: '24px', backgroundColor: '#333', marginLeft: state.isMinimized ? 0 : 'auto' }} />
|
|
178
|
+
|
|
179
|
+
<button
|
|
180
|
+
onClick={handleToggleMinimized}
|
|
181
|
+
title={state.isMinimized ? "Expand" : "Minimize"}
|
|
182
|
+
style={{
|
|
183
|
+
background: 'transparent',
|
|
184
|
+
border: 'none',
|
|
185
|
+
borderRadius: '4px',
|
|
186
|
+
padding: '6px',
|
|
187
|
+
color: 'inherit',
|
|
188
|
+
cursor: 'pointer',
|
|
189
|
+
display: 'flex'
|
|
190
|
+
}}
|
|
191
|
+
>
|
|
192
|
+
{state.isMinimized ? <Maximize2 size={18} /> : <Minus size={18} />}
|
|
193
|
+
</button>
|
|
194
|
+
</div>
|
|
195
|
+
</Draggable>
|
|
196
|
+
</>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Web components export (re-export from parent for backwards compatibility)
|
|
2
|
+
export { Toolbar } from '../Toolbar';
|
|
3
|
+
export { Highlighter } from '../Highlighter';
|
|
4
|
+
export { AnnotationInput } from '../AnnotationInput';
|
|
5
|
+
export { AnnotationList } from '../AnnotationList';
|
|
6
|
+
export { Draggable } from '../Draggable';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension entry point
|
|
3
|
+
* Self-contained - no cross-imports with react-native code
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Re-export web components directly
|
|
7
|
+
export { AiAnnotationProvider, useAiAnnotation } from './index.web';
|
|
8
|
+
export type { Annotation, Mode, HoveredElement, HoveredComponentInfo } from './index.web';
|
|
9
|
+
|
|
10
|
+
// Export web components
|
|
11
|
+
export { Toolbar } from './components/web/Toolbar';
|
|
12
|
+
export { Highlighter } from './components/web/Highlighter';
|
|
13
|
+
export { AnnotationInput } from './components/web/AnnotationInput';
|
|
14
|
+
export { AnnotationList } from './components/web/AnnotationList';
|
|
15
|
+
export { Draggable } from './components/web/Draggable';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { AiAnnotationProvider as Provider } from './store';
|
|
4
|
+
import { Toolbar } from './components/native/Toolbar';
|
|
5
|
+
|
|
6
|
+
export interface AiAnnotationProviderProps {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* React Native AI Annotation Provider
|
|
12
|
+
* Wraps your app to provide annotation functionality
|
|
13
|
+
*/
|
|
14
|
+
export function AiAnnotationProvider({ children }: AiAnnotationProviderProps) {
|
|
15
|
+
return (
|
|
16
|
+
<Provider>
|
|
17
|
+
<View style={{ flex: 1 }}>
|
|
18
|
+
{children}
|
|
19
|
+
<Toolbar />
|
|
20
|
+
</View>
|
|
21
|
+
</Provider>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Re-export store
|
|
26
|
+
export * from './store';
|
|
27
|
+
|
|
28
|
+
// Export native components
|
|
29
|
+
export { Toolbar } from './components/native/Toolbar';
|
|
30
|
+
export { Highlighter } from './components/native/Highlighter';
|
|
31
|
+
export { AnnotationInput } from './components/native/AnnotationInput';
|
|
32
|
+
export { AnnotationList } from './components/native/AnnotationList';
|
|
33
|
+
export { Draggable } from './components/native/Draggable';
|
|
34
|
+
|
|
35
|
+
// Export native screenshot utility
|
|
36
|
+
export { captureScreenshot } from './utils/screenshot.native';
|
|
37
|
+
export type { ScreenshotOptions, ScreenshotResult } from './utils/screenshot.native';
|
|
38
|
+
|
|
39
|
+
// Export native fiber utilities
|
|
40
|
+
export {
|
|
41
|
+
getReactFiber,
|
|
42
|
+
getComponentDisplayName,
|
|
43
|
+
getElementFromFiber,
|
|
44
|
+
inspectComponent,
|
|
45
|
+
} from './utils/fiber.native';
|
|
46
|
+
export type { ComponentInfo } from './utils/fiber.native';
|
|
47
|
+
|
|
48
|
+
// Export platform utilities
|
|
49
|
+
export { isWeb, isNative, getPlatform, isTauriEnv } from './utils/platform';
|
|
50
|
+
export type { PlatformType } from './utils/platform';
|