@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,269 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useAiAnnotation } from '../store';
3
+ import type { ComponentDetails } from '../store';
4
+ import { X } from 'lucide-react';
5
+
6
+ interface AnnotationInputProps {
7
+ onClose: () => void;
8
+ componentName: string;
9
+ componentDetails?: ComponentDetails;
10
+ }
11
+
12
+ /**
13
+ * Format component details into a readable text block for the annotation
14
+ */
15
+ function formatComponentDetails(details: ComponentDetails): string {
16
+ const lines: string[] = [];
17
+
18
+ // Element info
19
+ lines.push('## Element');
20
+ let elementStr = `<${details.elementInfo.tagName}`;
21
+ if (details.elementInfo.id) elementStr += ` id="${details.elementInfo.id}"`;
22
+ if (details.elementInfo.className) elementStr += ` class="${details.elementInfo.className}"`;
23
+ elementStr += '>';
24
+ lines.push(elementStr);
25
+
26
+ if (details.elementInfo.textContent) {
27
+ lines.push(`Text: "${details.elementInfo.textContent}"`);
28
+ }
29
+ lines.push(`Child elements: ${details.elementInfo.childElementCount}`);
30
+
31
+ // Attributes
32
+ if (Object.keys(details.elementInfo.attributes).length > 0) {
33
+ lines.push('');
34
+ lines.push('## Attributes');
35
+ for (const [key, value] of Object.entries(details.elementInfo.attributes)) {
36
+ lines.push(`${key}="${value}"`);
37
+ }
38
+ }
39
+
40
+ // Parent hierarchy
41
+ if (details.parentHierarchy.length > 0) {
42
+ lines.push('');
43
+ lines.push('## Parent Components');
44
+ lines.push(details.parentHierarchy.join(' → '));
45
+ }
46
+
47
+ // Child components
48
+ if (details.childComponents.length > 0) {
49
+ lines.push('');
50
+ lines.push(`## Child Components (${details.childComponents.length})`);
51
+ const childLines = details.childComponents.slice(0, 15).map(child => {
52
+ let str = child.name;
53
+ if (child.count > 1) str += ` ×${child.count}`;
54
+ if (child.hasChildren) str += ' (has children)';
55
+ return str;
56
+ });
57
+ lines.push(childLines.join(', '));
58
+ if (details.childComponents.length > 15) {
59
+ lines.push(`... and ${details.childComponents.length - 15} more`);
60
+ }
61
+ }
62
+
63
+ return lines.join('\n');
64
+ }
65
+
66
+ export function AnnotationInput({ onClose, componentName, componentDetails }: AnnotationInputProps) {
67
+ const { dispatch } = useAiAnnotation();
68
+ const [note, setNote] = useState('');
69
+ const [includeDetails, setIncludeDetails] = useState(true);
70
+ const [isVisible, setIsVisible] = useState(false);
71
+ const [isClosing, setIsClosing] = useState(false);
72
+
73
+ // Trigger entrance animation on mount
74
+ useEffect(() => {
75
+ requestAnimationFrame(() => {
76
+ setIsVisible(true);
77
+ });
78
+ }, []);
79
+
80
+ // ESC key to close
81
+ useEffect(() => {
82
+ const handleKeyDown = (e: KeyboardEvent) => {
83
+ if (e.key === 'Escape') {
84
+ e.preventDefault();
85
+ e.stopPropagation();
86
+ handleClose();
87
+ }
88
+ };
89
+ document.addEventListener('keydown', handleKeyDown);
90
+ return () => document.removeEventListener('keydown', handleKeyDown);
91
+ }, []);
92
+
93
+ // Handle close with exit animation
94
+ const handleClose = () => {
95
+ setIsClosing(true);
96
+ setTimeout(() => {
97
+ onClose();
98
+ }, 200);
99
+ };
100
+
101
+ const handleSubmit = (e: React.FormEvent) => {
102
+ e.preventDefault();
103
+ if (!note.trim()) return;
104
+
105
+ // Build the final annotation text with component details if enabled
106
+ let finalNote = note.trim();
107
+ if (includeDetails && componentDetails) {
108
+ const detailsText = formatComponentDetails(componentDetails);
109
+ finalNote = `${note.trim()}\n\n---\n${detailsText}`;
110
+ }
111
+
112
+ dispatch({
113
+ type: 'ADD_ANNOTATION',
114
+ payload: {
115
+ id: Date.now().toString(),
116
+ componentName,
117
+ note: finalNote,
118
+ timestamp: Date.now(),
119
+ details: componentDetails,
120
+ },
121
+ });
122
+ handleClose();
123
+ };
124
+
125
+ // Preview of what will be included
126
+ const detailsPreview = componentDetails ? (
127
+ <div style={{
128
+ fontSize: '0.75rem',
129
+ color: 'rgba(255, 255, 255, 0.6)',
130
+ marginTop: '4px',
131
+ }}>
132
+ {componentDetails.parentHierarchy.length > 0 && (
133
+ <div>Parents: {componentDetails.parentHierarchy.slice(0, 3).join(' → ')}{componentDetails.parentHierarchy.length > 3 ? '...' : ''}</div>
134
+ )}
135
+ {componentDetails.childComponents.length > 0 && (
136
+ <div>Children: {componentDetails.childComponents.slice(0, 3).map(c => c.name).join(', ')}{componentDetails.childComponents.length > 3 ? '...' : ''}</div>
137
+ )}
138
+ <div>Element: &lt;{componentDetails.elementInfo.tagName}&gt; ({componentDetails.elementInfo.childElementCount} children)</div>
139
+ </div>
140
+ ) : null;
141
+
142
+ return (
143
+ <div
144
+ style={{
145
+ position: 'fixed',
146
+ top: 0,
147
+ left: 0,
148
+ right: 0,
149
+ bottom: 0,
150
+ backgroundColor: isVisible && !isClosing ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0)',
151
+ display: 'flex',
152
+ alignItems: 'center',
153
+ justifyContent: 'center',
154
+ zIndex: 10000,
155
+ transition: 'background-color 0.2s ease',
156
+ }}
157
+ data-ai-annotation-ui
158
+ onClick={handleClose}
159
+ >
160
+ <div
161
+ style={{
162
+ backgroundColor: '#1e1e1e',
163
+ color: '#e5e7eb',
164
+ padding: '20px',
165
+ borderRadius: '8px',
166
+ width: '500px',
167
+ maxWidth: '90%',
168
+ maxHeight: '80vh',
169
+ overflow: 'auto',
170
+ boxShadow: '0 4px 6px rgba(0,0,0,0.3)',
171
+ transform: isVisible && !isClosing ? 'scale(1) translateY(0)' : 'scale(0.95) translateY(-10px)',
172
+ opacity: isVisible && !isClosing ? 1 : 0,
173
+ transition: 'transform 0.2s ease, opacity 0.2s ease',
174
+ }}
175
+ onClick={(e) => e.stopPropagation()}
176
+ >
177
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '16px' }}>
178
+ <h3 style={{ margin: 0, fontSize: '1.1rem' }}>Add Annotation</h3>
179
+ <button onClick={handleClose} style={{ background: 'none', border: 'none', color: 'inherit', cursor: 'pointer' }}>
180
+ <X size={20} />
181
+ </button>
182
+ </div>
183
+
184
+ <div style={{ marginBottom: '12px', fontSize: '0.9rem', opacity: 0.8 }}>
185
+ Component: <strong>{componentName}</strong>
186
+ </div>
187
+
188
+ <form onSubmit={handleSubmit}>
189
+ <textarea
190
+ value={note}
191
+ onChange={(e) => setNote(e.target.value)}
192
+ placeholder="Describe what this component does or what changes you need..."
193
+ style={{
194
+ width: '100%',
195
+ height: '120px',
196
+ padding: '8px',
197
+ borderRadius: '4px',
198
+ backgroundColor: '#2d2d2d',
199
+ border: '1px solid #404040',
200
+ color: 'white',
201
+ marginBottom: '12px',
202
+ resize: 'vertical',
203
+ fontFamily: 'inherit',
204
+ }}
205
+ autoFocus
206
+ />
207
+
208
+ {/* Include details checkbox */}
209
+ {componentDetails && (
210
+ <div style={{ marginBottom: '16px' }}>
211
+ <label
212
+ style={{
213
+ display: 'flex',
214
+ alignItems: 'flex-start',
215
+ gap: '8px',
216
+ cursor: 'pointer',
217
+ fontSize: '0.85rem',
218
+ }}
219
+ >
220
+ <input
221
+ type="checkbox"
222
+ checked={includeDetails}
223
+ onChange={(e) => setIncludeDetails(e.target.checked)}
224
+ style={{ marginTop: '2px' }}
225
+ />
226
+ <div>
227
+ <div style={{ color: '#10b981', fontWeight: 500 }}>
228
+ Include component details in annotation
229
+ </div>
230
+ {includeDetails && detailsPreview}
231
+ </div>
232
+ </label>
233
+ </div>
234
+ )}
235
+
236
+ <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px' }}>
237
+ <button
238
+ type="button"
239
+ onClick={handleClose}
240
+ style={{
241
+ padding: '6px 12px',
242
+ borderRadius: '4px',
243
+ backgroundColor: 'transparent',
244
+ border: '1px solid #404040',
245
+ color: 'white',
246
+ cursor: 'pointer',
247
+ }}
248
+ >
249
+ Cancel
250
+ </button>
251
+ <button
252
+ type="submit"
253
+ style={{
254
+ padding: '6px 12px',
255
+ borderRadius: '4px',
256
+ backgroundColor: '#3b82f6',
257
+ border: 'none',
258
+ color: 'white',
259
+ cursor: 'pointer',
260
+ }}
261
+ >
262
+ Save Annotation
263
+ </button>
264
+ </div>
265
+ </form>
266
+ </div>
267
+ </div>
268
+ );
269
+ }
@@ -0,0 +1,248 @@
1
+ import React, { useState } from 'react';
2
+ import { useAiAnnotation } from '../store';
3
+ import { X, Trash2, Trash, ChevronDown, ChevronRight } from 'lucide-react';
4
+
5
+ export function AnnotationList() {
6
+ const { state, dispatch } = useAiAnnotation();
7
+ const [expandedId, setExpandedId] = useState<string | null>(null);
8
+
9
+ if (!state.showList) return null;
10
+
11
+ const toggleExpand = (id: string) => {
12
+ setExpandedId(expandedId === id ? null : id);
13
+ };
14
+
15
+ return (
16
+ <div
17
+ style={{
18
+ position: 'fixed',
19
+ top: 0,
20
+ left: 0,
21
+ right: 0,
22
+ bottom: 0,
23
+ backgroundColor: 'rgba(0,0,0,0.5)',
24
+ display: 'flex',
25
+ alignItems: 'center',
26
+ justifyContent: 'center',
27
+ zIndex: 10000,
28
+ }}
29
+ data-ai-annotation-ui
30
+ onClick={() => dispatch({ type: 'TOGGLE_LIST' })}
31
+ >
32
+ <div
33
+ style={{
34
+ backgroundColor: '#1e1e1e',
35
+ color: '#e5e7eb',
36
+ padding: '20px',
37
+ borderRadius: '8px',
38
+ width: '600px',
39
+ maxWidth: '90%',
40
+ maxHeight: '80vh',
41
+ display: 'flex',
42
+ flexDirection: 'column',
43
+ boxShadow: '0 4px 6px rgba(0,0,0,0.3)',
44
+ }}
45
+ onClick={(e) => e.stopPropagation()}
46
+ >
47
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '16px', alignItems: 'center' }}>
48
+ <h3 style={{ margin: 0 }}>Annotations ({state.annotations.length})</h3>
49
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
50
+ {state.annotations.length > 0 && (
51
+ <button
52
+ onClick={() => dispatch({ type: 'CLEAR_ALL_ANNOTATIONS' })}
53
+ style={{
54
+ background: '#ef4444',
55
+ border: 'none',
56
+ color: 'white',
57
+ cursor: 'pointer',
58
+ padding: '6px 12px',
59
+ borderRadius: '4px',
60
+ fontSize: '12px',
61
+ display: 'flex',
62
+ alignItems: 'center',
63
+ gap: '4px',
64
+ }}
65
+ title="Clear all annotations"
66
+ >
67
+ <Trash size={14} />
68
+ Clear All
69
+ </button>
70
+ )}
71
+ <button
72
+ onClick={() => dispatch({ type: 'TOGGLE_LIST' })}
73
+ style={{ background: 'none', border: 'none', color: 'inherit', cursor: 'pointer' }}
74
+ >
75
+ <X size={20} />
76
+ </button>
77
+ </div>
78
+ </div>
79
+
80
+ <div style={{ overflowY: 'auto', flex: 1, display: 'flex', flexDirection: 'column', gap: '8px' }}>
81
+ {state.annotations.length === 0 ? (
82
+ <p style={{ textAlign: 'center', opacity: 0.6, padding: '20px' }}>No annotations yet.</p>
83
+ ) : (
84
+ state.annotations.map((ann) => {
85
+ const isExpanded = expandedId === ann.id;
86
+ const hasDetails = !!ann.details;
87
+
88
+ return (
89
+ <div
90
+ key={ann.id}
91
+ style={{
92
+ backgroundColor: '#2d2d2d',
93
+ borderRadius: '4px',
94
+ overflow: 'hidden',
95
+ }}
96
+ >
97
+ {/* Main annotation row */}
98
+ <div
99
+ style={{
100
+ padding: '12px',
101
+ display: 'flex',
102
+ justifyContent: 'space-between',
103
+ alignItems: 'flex-start',
104
+ gap: '12px',
105
+ }}
106
+ >
107
+ <div style={{ flex: 1 }}>
108
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
109
+ {hasDetails && (
110
+ <button
111
+ onClick={() => toggleExpand(ann.id)}
112
+ style={{
113
+ background: 'none',
114
+ border: 'none',
115
+ color: '#60a5fa',
116
+ cursor: 'pointer',
117
+ padding: 0,
118
+ display: 'flex',
119
+ alignItems: 'center',
120
+ }}
121
+ >
122
+ {isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
123
+ </button>
124
+ )}
125
+ <div style={{ fontWeight: 'bold', fontSize: '0.9rem', color: '#60a5fa' }}>
126
+ {ann.componentName}
127
+ </div>
128
+ </div>
129
+ <div style={{ fontSize: '0.9rem', whiteSpace: 'pre-wrap', marginLeft: hasDetails ? 24 : 0 }}>
130
+ {ann.note}
131
+ </div>
132
+ </div>
133
+ <button
134
+ onClick={() => dispatch({ type: 'REMOVE_ANNOTATION', payload: ann.id })}
135
+ style={{
136
+ background: 'none',
137
+ border: 'none',
138
+ color: '#ef4444',
139
+ cursor: 'pointer',
140
+ padding: '4px',
141
+ flexShrink: 0,
142
+ }}
143
+ title="Remove annotation"
144
+ >
145
+ <Trash2 size={16} />
146
+ </button>
147
+ </div>
148
+
149
+ {/* Expanded details */}
150
+ {isExpanded && ann.details && (
151
+ <div
152
+ style={{
153
+ padding: '12px',
154
+ paddingTop: 0,
155
+ borderTop: '1px solid #404040',
156
+ marginTop: '8px',
157
+ fontSize: '0.8rem',
158
+ }}
159
+ >
160
+ {/* Element info */}
161
+ <div style={{ marginBottom: '12px' }}>
162
+ <div style={{ color: '#10b981', fontWeight: 'bold', marginBottom: '4px' }}>
163
+ Element
164
+ </div>
165
+ <div style={{ fontFamily: 'monospace', color: '#f59e0b', fontSize: '0.75rem' }}>
166
+ &lt;{ann.details.elementInfo.tagName}
167
+ {ann.details.elementInfo.id && ` id="${ann.details.elementInfo.id}"`}
168
+ {ann.details.elementInfo.className && ` class="${ann.details.elementInfo.className}"`}
169
+ &gt;
170
+ </div>
171
+ {ann.details.elementInfo.textContent && (
172
+ <div style={{ color: 'rgba(255, 255, 255, 0.6)', marginTop: '4px', fontStyle: 'italic' }}>
173
+ Text: "{ann.details.elementInfo.textContent}"
174
+ </div>
175
+ )}
176
+ </div>
177
+
178
+ {/* Parent hierarchy */}
179
+ {ann.details.parentHierarchy.length > 0 && (
180
+ <div style={{ marginBottom: '12px' }}>
181
+ <div style={{ color: '#10b981', fontWeight: 'bold', marginBottom: '4px' }}>
182
+ Parent Components
183
+ </div>
184
+ <div style={{ fontFamily: 'monospace', fontSize: '0.75rem' }}>
185
+ {ann.details.parentHierarchy.map((parent, i) => (
186
+ <span key={i} style={{ color: '#a78bfa' }}>
187
+ {i > 0 && ' → '}
188
+ {parent}
189
+ </span>
190
+ ))}
191
+ </div>
192
+ </div>
193
+ )}
194
+
195
+ {/* Child components */}
196
+ {ann.details.childComponents.length > 0 && (
197
+ <div style={{ marginBottom: '12px' }}>
198
+ <div style={{ color: '#10b981', fontWeight: 'bold', marginBottom: '4px' }}>
199
+ Child Components ({ann.details.childComponents.length})
200
+ </div>
201
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
202
+ {ann.details.childComponents.map((child, i) => (
203
+ <span
204
+ key={i}
205
+ style={{
206
+ backgroundColor: '#374151',
207
+ padding: '2px 8px',
208
+ borderRadius: '4px',
209
+ fontFamily: 'monospace',
210
+ fontSize: '0.75rem',
211
+ color: '#34d399',
212
+ }}
213
+ >
214
+ {child.name}
215
+ {child.count > 1 && ` ×${child.count}`}
216
+ {child.hasChildren && ' ▾'}
217
+ </span>
218
+ ))}
219
+ </div>
220
+ </div>
221
+ )}
222
+
223
+ {/* Attributes */}
224
+ {Object.keys(ann.details.elementInfo.attributes).length > 0 && (
225
+ <div>
226
+ <div style={{ color: '#10b981', fontWeight: 'bold', marginBottom: '4px' }}>
227
+ Attributes
228
+ </div>
229
+ <div style={{ fontFamily: 'monospace', fontSize: '0.75rem' }}>
230
+ {Object.entries(ann.details.elementInfo.attributes).map(([key, value]) => (
231
+ <div key={key} style={{ color: '#fbbf24' }}>
232
+ {key}="{value}"
233
+ </div>
234
+ ))}
235
+ </div>
236
+ </div>
237
+ )}
238
+ </div>
239
+ )}
240
+ </div>
241
+ );
242
+ })
243
+ )}
244
+ </div>
245
+ </div>
246
+ </div>
247
+ );
248
+ }
@@ -0,0 +1,73 @@
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
+ }}
68
+ onMouseDown={onMouseDown}
69
+ >
70
+ {children}
71
+ </div>
72
+ );
73
+ }