@ekairos/toolbar 1.22.4-beta.development.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.
@@ -0,0 +1,898 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
3
+ import { createPortal } from "react-dom";
4
+ import { ToolbarPopup } from "./popup";
5
+ import { generateToolbarOutput } from "./output";
6
+ import { closestCrossingShadow, deepElementFromPoint, getElementClasses, getElementPath, getNearbyText, getStableSelector, identifyElementName, isElementFixed, } from "./selectors";
7
+ const DEFAULT_ACCENT = "#2f7bf6";
8
+ const MULTI_ACCENT = "#2fbf71";
9
+ const ROOT_ATTR = "[data-ekairos-toolbar-root]";
10
+ const MARKER_ATTR = "[data-ekairos-toolbar-marker]";
11
+ const POPUP_ATTR = "[data-ekairos-toolbar-popup]";
12
+ function intersects(rect, area) {
13
+ return (rect.left < area.left + area.width &&
14
+ rect.right > area.left &&
15
+ rect.top < area.top + area.height &&
16
+ rect.bottom > area.top);
17
+ }
18
+ function getViewportY(annotation, scrollY) {
19
+ return annotation.isFixed ? annotation.y : annotation.y - scrollY;
20
+ }
21
+ function getPopupPosition(xPercent, y, isFixed, scrollY) {
22
+ const markerY = isFixed ? y : y - scrollY;
23
+ const xPx = (xPercent / 100) * window.innerWidth;
24
+ const clampedX = Math.max(180, Math.min(window.innerWidth - 180, xPx));
25
+ const nearBottom = markerY > window.innerHeight - 280;
26
+ return {
27
+ left: clampedX,
28
+ ...(nearBottom
29
+ ? { bottom: window.innerHeight - markerY + 18 }
30
+ : { top: markerY + 18 }),
31
+ };
32
+ }
33
+ function detectElementsInDragArea(rect) {
34
+ const candidates = Array.from(document.querySelectorAll([
35
+ "button",
36
+ "a",
37
+ "input",
38
+ "textarea",
39
+ "select",
40
+ "img",
41
+ "p",
42
+ "h1",
43
+ "h2",
44
+ "h3",
45
+ "h4",
46
+ "h5",
47
+ "h6",
48
+ "li",
49
+ "label",
50
+ "[role='button']",
51
+ "[data-testid]",
52
+ ].join(","))).filter((element) => !closestCrossingShadow(element, ROOT_ATTR));
53
+ const selected = candidates
54
+ .map((element) => ({ element, rect: element.getBoundingClientRect() }))
55
+ .filter(({ rect: box }) => box.width >= 10 && box.height >= 10)
56
+ .filter(({ rect: box }) => intersects(box, rect));
57
+ return selected
58
+ .filter(({ element }) => !selected.some(({ element: other }) => other !== element && element.contains(other)))
59
+ .map(({ element }) => element);
60
+ }
61
+ export function EkairosToolbar({ onAnnotationAdd, onAnnotationDelete, onAnnotationUpdate, onAnnotationsClear, onCopy, onSubmit, copyToClipboard = true, blockInteractions = true, initialActive = false, outputDetail = "standard", storageKey, } = {}) {
62
+ const [mounted, setMounted] = useState(false);
63
+ const [active, setActive] = useState(initialActive);
64
+ const [annotations, setAnnotations] = useState([]);
65
+ const [pending, setPending] = useState(null);
66
+ const [editing, setEditing] = useState(null);
67
+ const [hoverRect, setHoverRect] = useState(null);
68
+ const [hoverLabel, setHoverLabel] = useState("");
69
+ const [hoverPosition, setHoverPosition] = useState({ x: 0, y: 0 });
70
+ const [hoveredMarkerId, setHoveredMarkerId] = useState(null);
71
+ const [scrollY, setScrollY] = useState(0);
72
+ const [copied, setCopied] = useState(false);
73
+ const [showMarkers, setShowMarkers] = useState(true);
74
+ const [pendingMulti, setPendingMulti] = useState([]);
75
+ const [dragRect, setDragRect] = useState(null);
76
+ const [dragTargets, setDragTargets] = useState([]);
77
+ const popupRef = useRef(null);
78
+ const editPopupRef = useRef(null);
79
+ const pointerRef = useRef({
80
+ start: null,
81
+ });
82
+ const dragStartRef = useRef(null);
83
+ const modifierRef = useRef({ metaOrCtrl: false, shift: false });
84
+ const pagePath = typeof window !== "undefined"
85
+ ? `${window.location.pathname}${window.location.search}${window.location.hash}`
86
+ : "/";
87
+ const resolvedOutputDetail = useMemo(() => {
88
+ return outputDetail;
89
+ }, [outputDetail]);
90
+ const resolvedStorageKey = useMemo(() => {
91
+ if (storageKey)
92
+ return storageKey;
93
+ if (typeof window === "undefined")
94
+ return "ekairos-toolbar:/";
95
+ return `ekairos-toolbar:${window.location.pathname}`;
96
+ }, [storageKey]);
97
+ const clearTransientState = useCallback(() => {
98
+ setPending(null);
99
+ setEditing(null);
100
+ setHoverRect(null);
101
+ setHoverLabel("");
102
+ setPendingMulti([]);
103
+ setDragRect(null);
104
+ setDragTargets([]);
105
+ }, []);
106
+ const createSnapshotForElement = useCallback((element, clientX, clientY, selectedText) => {
107
+ const rect = element.getBoundingClientRect();
108
+ const fixed = isElementFixed(element);
109
+ return {
110
+ x: (clientX / window.innerWidth) * 100,
111
+ y: fixed ? clientY : clientY + window.scrollY,
112
+ clientY,
113
+ element: identifyElementName(element),
114
+ elementPath: getElementPath(element),
115
+ stableSelector: getStableSelector(element),
116
+ selectedText,
117
+ boundingBox: {
118
+ x: rect.left,
119
+ y: fixed ? rect.top : rect.top + window.scrollY,
120
+ width: rect.width,
121
+ height: rect.height,
122
+ },
123
+ cssClasses: getElementClasses(element),
124
+ nearbyText: getNearbyText(element),
125
+ isFixed: fixed,
126
+ targetElement: element,
127
+ };
128
+ }, []);
129
+ const createSnapshotFromModifierMulti = useCallback(() => {
130
+ if (pendingMulti.length === 0)
131
+ return;
132
+ if (pendingMulti.length === 1) {
133
+ const item = pendingMulti[0];
134
+ const centerX = item.rect.left + item.rect.width / 2;
135
+ const centerY = item.rect.top + item.rect.height / 2;
136
+ setPending(createSnapshotForElement(item.element, centerX, centerY));
137
+ setPendingMulti([]);
138
+ return;
139
+ }
140
+ const freshRects = pendingMulti.map((item) => item.element.getBoundingClientRect());
141
+ const lastRect = freshRects[freshRects.length - 1];
142
+ const centerX = lastRect.left + lastRect.width / 2;
143
+ const centerY = lastRect.top + lastRect.height / 2;
144
+ const bounds = freshRects.reduce((acc, rect) => ({
145
+ left: Math.min(acc.left, rect.left),
146
+ top: Math.min(acc.top, rect.top),
147
+ right: Math.max(acc.right, rect.right),
148
+ bottom: Math.max(acc.bottom, rect.bottom),
149
+ }), { left: Infinity, top: Infinity, right: -Infinity, bottom: -Infinity });
150
+ const names = pendingMulti
151
+ .slice(0, 4)
152
+ .map((item) => item.name)
153
+ .join(", ");
154
+ const selectors = pendingMulti
155
+ .slice(0, 6)
156
+ .map((item) => item.stableSelector)
157
+ .join(", ");
158
+ setPending({
159
+ x: (centerX / window.innerWidth) * 100,
160
+ y: centerY + window.scrollY,
161
+ clientY: centerY,
162
+ element: `${pendingMulti.length} elements: ${names}${pendingMulti.length > 4 ? "..." : ""}`,
163
+ elementPath: "multi-select",
164
+ stableSelector: selectors,
165
+ boundingBox: {
166
+ x: bounds.left,
167
+ y: bounds.top + window.scrollY,
168
+ width: bounds.right - bounds.left,
169
+ height: bounds.bottom - bounds.top,
170
+ },
171
+ elementBoundingBoxes: freshRects.map((rect) => ({
172
+ x: rect.left,
173
+ y: rect.top + window.scrollY,
174
+ width: rect.width,
175
+ height: rect.height,
176
+ })),
177
+ cssClasses: "",
178
+ nearbyText: "",
179
+ isMultiSelect: true,
180
+ targetElements: pendingMulti.map((item) => item.element),
181
+ });
182
+ setPendingMulti([]);
183
+ setHoverRect(null);
184
+ }, [createSnapshotForElement, pendingMulti]);
185
+ const createSnapshotFromDrag = useCallback((elements, area, releaseX, releaseY) => {
186
+ if (elements.length === 0) {
187
+ if (area.width > 20 && area.height > 20) {
188
+ setPending({
189
+ x: (releaseX / window.innerWidth) * 100,
190
+ y: releaseY + window.scrollY,
191
+ clientY: releaseY,
192
+ element: "Area selection",
193
+ elementPath: `region (${Math.round(area.left)}, ${Math.round(area.top)})`,
194
+ boundingBox: {
195
+ x: area.left,
196
+ y: area.top + window.scrollY,
197
+ width: area.width,
198
+ height: area.height,
199
+ },
200
+ isMultiSelect: true,
201
+ });
202
+ }
203
+ return;
204
+ }
205
+ if (elements.length === 1) {
206
+ const rect = elements[0].getBoundingClientRect();
207
+ setPending(createSnapshotForElement(elements[0], rect.left + rect.width / 2, rect.top + rect.height / 2));
208
+ return;
209
+ }
210
+ const rects = elements.map((element) => element.getBoundingClientRect());
211
+ const bounds = rects.reduce((acc, rect) => ({
212
+ left: Math.min(acc.left, rect.left),
213
+ top: Math.min(acc.top, rect.top),
214
+ right: Math.max(acc.right, rect.right),
215
+ bottom: Math.max(acc.bottom, rect.bottom),
216
+ }), { left: Infinity, top: Infinity, right: -Infinity, bottom: -Infinity });
217
+ const names = elements
218
+ .slice(0, 4)
219
+ .map((element) => identifyElementName(element))
220
+ .join(", ");
221
+ setPending({
222
+ x: (releaseX / window.innerWidth) * 100,
223
+ y: releaseY + window.scrollY,
224
+ clientY: releaseY,
225
+ element: `${elements.length} elements: ${names}${elements.length > 4 ? "..." : ""}`,
226
+ elementPath: "multi-select",
227
+ stableSelector: elements
228
+ .slice(0, 6)
229
+ .map((element) => getStableSelector(element))
230
+ .join(", "),
231
+ boundingBox: {
232
+ x: bounds.left,
233
+ y: bounds.top + window.scrollY,
234
+ width: bounds.right - bounds.left,
235
+ height: bounds.bottom - bounds.top,
236
+ },
237
+ elementBoundingBoxes: rects.map((rect) => ({
238
+ x: rect.left,
239
+ y: rect.top + window.scrollY,
240
+ width: rect.width,
241
+ height: rect.height,
242
+ })),
243
+ isMultiSelect: true,
244
+ targetElements: elements,
245
+ });
246
+ }, [createSnapshotForElement]);
247
+ const addAnnotation = useCallback((comment) => {
248
+ if (!pending)
249
+ return;
250
+ const annotation = {
251
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
252
+ timestamp: Date.now(),
253
+ x: pending.x,
254
+ y: pending.y,
255
+ comment,
256
+ element: pending.element,
257
+ elementPath: pending.elementPath,
258
+ stableSelector: pending.stableSelector,
259
+ selectedText: pending.selectedText,
260
+ boundingBox: pending.boundingBox,
261
+ elementBoundingBoxes: pending.elementBoundingBoxes,
262
+ cssClasses: pending.cssClasses,
263
+ nearbyText: pending.nearbyText,
264
+ isMultiSelect: pending.isMultiSelect,
265
+ isFixed: pending.isFixed,
266
+ };
267
+ setAnnotations((prev) => [...prev, annotation]);
268
+ setPending(null);
269
+ onAnnotationAdd?.(annotation);
270
+ window.getSelection()?.removeAllRanges();
271
+ }, [onAnnotationAdd, pending]);
272
+ const updateAnnotation = useCallback((comment) => {
273
+ if (!editing)
274
+ return;
275
+ const updated = { ...editing, comment };
276
+ setAnnotations((prev) => prev.map((item) => (item.id === editing.id ? updated : item)));
277
+ setEditing(null);
278
+ onAnnotationUpdate?.(updated);
279
+ }, [editing, onAnnotationUpdate]);
280
+ const deleteAnnotation = useCallback((id) => {
281
+ const target = annotations.find((annotation) => annotation.id === id);
282
+ setAnnotations((prev) => prev.filter((annotation) => annotation.id !== id));
283
+ setEditing((prev) => (prev?.id === id ? null : prev));
284
+ if (target)
285
+ onAnnotationDelete?.(target);
286
+ }, [annotations, onAnnotationDelete]);
287
+ const clearAll = useCallback(() => {
288
+ if (annotations.length === 0)
289
+ return;
290
+ onAnnotationsClear?.(annotations);
291
+ setAnnotations([]);
292
+ setHoveredMarkerId(null);
293
+ }, [annotations, onAnnotationsClear]);
294
+ const copyOutput = useCallback(async () => {
295
+ const output = generateToolbarOutput(annotations, pagePath, resolvedOutputDetail);
296
+ if (!output)
297
+ return;
298
+ if (copyToClipboard) {
299
+ try {
300
+ await navigator.clipboard.writeText(output);
301
+ }
302
+ catch {
303
+ // Ignore clipboard failures; callback still receives output.
304
+ }
305
+ }
306
+ onCopy?.(output);
307
+ setCopied(true);
308
+ setTimeout(() => setCopied(false), 1200);
309
+ }, [annotations, copyToClipboard, onCopy, pagePath, resolvedOutputDetail]);
310
+ const sendOutput = useCallback(() => {
311
+ const output = generateToolbarOutput(annotations, pagePath, resolvedOutputDetail);
312
+ if (!output)
313
+ return;
314
+ onSubmit?.(output, annotations);
315
+ }, [annotations, onSubmit, pagePath, resolvedOutputDetail]);
316
+ useEffect(() => {
317
+ setMounted(true);
318
+ }, []);
319
+ useEffect(() => {
320
+ if (!mounted || typeof window === "undefined")
321
+ return;
322
+ try {
323
+ const raw = localStorage.getItem(resolvedStorageKey);
324
+ if (!raw)
325
+ return;
326
+ const parsed = JSON.parse(raw);
327
+ if (Array.isArray(parsed)) {
328
+ setAnnotations(parsed);
329
+ }
330
+ }
331
+ catch {
332
+ // Ignore invalid cached state.
333
+ }
334
+ }, [mounted, resolvedStorageKey]);
335
+ useEffect(() => {
336
+ if (!mounted || typeof window === "undefined")
337
+ return;
338
+ try {
339
+ localStorage.setItem(resolvedStorageKey, JSON.stringify(annotations));
340
+ }
341
+ catch {
342
+ // Ignore storage failures.
343
+ }
344
+ }, [annotations, mounted, resolvedStorageKey]);
345
+ useEffect(() => {
346
+ if (!mounted)
347
+ return;
348
+ const onScroll = () => setScrollY(window.scrollY);
349
+ onScroll();
350
+ window.addEventListener("scroll", onScroll, { passive: true });
351
+ return () => window.removeEventListener("scroll", onScroll);
352
+ }, [mounted]);
353
+ useEffect(() => {
354
+ if (!active) {
355
+ clearTransientState();
356
+ }
357
+ }, [active, clearTransientState]);
358
+ useEffect(() => {
359
+ const onKeyDown = (event) => {
360
+ const target = event.target;
361
+ const typing = !!target &&
362
+ (target.tagName === "INPUT" ||
363
+ target.tagName === "TEXTAREA" ||
364
+ target.isContentEditable);
365
+ if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key.toLowerCase() === "f") {
366
+ event.preventDefault();
367
+ setActive((prev) => !prev);
368
+ return;
369
+ }
370
+ if (!active)
371
+ return;
372
+ if (event.key === "Escape") {
373
+ if (pendingMulti.length > 0) {
374
+ setPendingMulti([]);
375
+ return;
376
+ }
377
+ if (pending) {
378
+ setPending(null);
379
+ return;
380
+ }
381
+ if (editing) {
382
+ setEditing(null);
383
+ return;
384
+ }
385
+ setActive(false);
386
+ return;
387
+ }
388
+ if (typing || event.metaKey || event.ctrlKey || event.altKey)
389
+ return;
390
+ if (event.key.toLowerCase() === "c" && annotations.length > 0) {
391
+ event.preventDefault();
392
+ void copyOutput();
393
+ }
394
+ if (event.key.toLowerCase() === "s" && annotations.length > 0) {
395
+ event.preventDefault();
396
+ sendOutput();
397
+ }
398
+ if (event.key.toLowerCase() === "x" && annotations.length > 0) {
399
+ event.preventDefault();
400
+ clearAll();
401
+ }
402
+ if (event.key.toLowerCase() === "h") {
403
+ event.preventDefault();
404
+ setShowMarkers((prev) => !prev);
405
+ }
406
+ };
407
+ document.addEventListener("keydown", onKeyDown);
408
+ return () => document.removeEventListener("keydown", onKeyDown);
409
+ }, [
410
+ active,
411
+ annotations.length,
412
+ clearAll,
413
+ copyOutput,
414
+ editing,
415
+ pending,
416
+ pendingMulti.length,
417
+ sendOutput,
418
+ ]);
419
+ useEffect(() => {
420
+ if (!active)
421
+ return;
422
+ const onKeyDown = (event) => {
423
+ if (event.key === "Meta" || event.key === "Control") {
424
+ modifierRef.current.metaOrCtrl = true;
425
+ }
426
+ if (event.key === "Shift")
427
+ modifierRef.current.shift = true;
428
+ };
429
+ const onKeyUp = (event) => {
430
+ const hadBoth = modifierRef.current.metaOrCtrl && modifierRef.current.shift;
431
+ if (event.key === "Meta" || event.key === "Control") {
432
+ modifierRef.current.metaOrCtrl = false;
433
+ }
434
+ if (event.key === "Shift")
435
+ modifierRef.current.shift = false;
436
+ const hasBoth = modifierRef.current.metaOrCtrl && modifierRef.current.shift;
437
+ if (hadBoth && !hasBoth && pendingMulti.length > 0) {
438
+ createSnapshotFromModifierMulti();
439
+ }
440
+ };
441
+ const onBlur = () => {
442
+ modifierRef.current = { metaOrCtrl: false, shift: false };
443
+ setPendingMulti([]);
444
+ };
445
+ document.addEventListener("keydown", onKeyDown);
446
+ document.addEventListener("keyup", onKeyUp);
447
+ window.addEventListener("blur", onBlur);
448
+ return () => {
449
+ document.removeEventListener("keydown", onKeyDown);
450
+ document.removeEventListener("keyup", onKeyUp);
451
+ window.removeEventListener("blur", onBlur);
452
+ };
453
+ }, [active, createSnapshotFromModifierMulti, pendingMulti.length]);
454
+ useEffect(() => {
455
+ if (!active || pending || editing || dragRect)
456
+ return;
457
+ const onMouseMove = (event) => {
458
+ const element = deepElementFromPoint(event.clientX, event.clientY);
459
+ if (!element || closestCrossingShadow(element, ROOT_ATTR)) {
460
+ setHoverRect(null);
461
+ setHoverLabel("");
462
+ return;
463
+ }
464
+ const rect = element.getBoundingClientRect();
465
+ setHoverRect(rect);
466
+ setHoverLabel(identifyElementName(element));
467
+ setHoverPosition({ x: event.clientX, y: event.clientY });
468
+ };
469
+ document.addEventListener("mousemove", onMouseMove);
470
+ return () => document.removeEventListener("mousemove", onMouseMove);
471
+ }, [active, dragRect, editing, pending]);
472
+ useEffect(() => {
473
+ if (!active)
474
+ return;
475
+ const onClick = (event) => {
476
+ const target = (event.composedPath()[0] || event.target);
477
+ if (!target)
478
+ return;
479
+ if (closestCrossingShadow(target, ROOT_ATTR))
480
+ return;
481
+ if (closestCrossingShadow(target, MARKER_ATTR))
482
+ return;
483
+ if (closestCrossingShadow(target, POPUP_ATTR))
484
+ return;
485
+ if (pending) {
486
+ event.preventDefault();
487
+ popupRef.current?.shake();
488
+ return;
489
+ }
490
+ if (editing) {
491
+ event.preventDefault();
492
+ editPopupRef.current?.shake();
493
+ return;
494
+ }
495
+ const modifierMulti = (event.metaKey || event.ctrlKey) && event.shiftKey;
496
+ const element = deepElementFromPoint(event.clientX, event.clientY);
497
+ if (!element)
498
+ return;
499
+ if (modifierMulti) {
500
+ event.preventDefault();
501
+ event.stopPropagation();
502
+ const existing = pendingMulti.findIndex((item) => item.element === element);
503
+ if (existing >= 0) {
504
+ setPendingMulti((prev) => prev.filter((_, index) => index !== existing));
505
+ }
506
+ else {
507
+ const rect = element.getBoundingClientRect();
508
+ setPendingMulti((prev) => [
509
+ ...prev,
510
+ {
511
+ element,
512
+ rect,
513
+ name: identifyElementName(element),
514
+ path: getElementPath(element),
515
+ stableSelector: getStableSelector(element),
516
+ },
517
+ ]);
518
+ }
519
+ return;
520
+ }
521
+ const interactive = closestCrossingShadow(target, "button, a, input, select, textarea, [role='button'], [onclick]");
522
+ if (blockInteractions && interactive) {
523
+ event.preventDefault();
524
+ event.stopPropagation();
525
+ }
526
+ else {
527
+ event.preventDefault();
528
+ }
529
+ const selectedText = window.getSelection()?.toString().trim();
530
+ setPending(createSnapshotForElement(element, event.clientX, event.clientY, selectedText?.slice(0, 500)));
531
+ setHoverRect(null);
532
+ setPendingMulti([]);
533
+ };
534
+ document.addEventListener("click", onClick, true);
535
+ return () => document.removeEventListener("click", onClick, true);
536
+ }, [
537
+ active,
538
+ blockInteractions,
539
+ createSnapshotForElement,
540
+ editing,
541
+ pending,
542
+ pendingMulti,
543
+ ]);
544
+ useEffect(() => {
545
+ if (!active || pending || editing)
546
+ return;
547
+ const onMouseDown = (event) => {
548
+ const target = (event.composedPath()[0] || event.target);
549
+ if (!target)
550
+ return;
551
+ if (closestCrossingShadow(target, ROOT_ATTR))
552
+ return;
553
+ if (closestCrossingShadow(target, POPUP_ATTR))
554
+ return;
555
+ if (closestCrossingShadow(target, MARKER_ATTR))
556
+ return;
557
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) {
558
+ return;
559
+ }
560
+ pointerRef.current.start = { x: event.clientX, y: event.clientY };
561
+ };
562
+ document.addEventListener("mousedown", onMouseDown);
563
+ return () => document.removeEventListener("mousedown", onMouseDown);
564
+ }, [active, editing, pending]);
565
+ useEffect(() => {
566
+ if (!active || pending || editing)
567
+ return;
568
+ const onMouseMove = (event) => {
569
+ const start = pointerRef.current.start;
570
+ if (!start)
571
+ return;
572
+ const dx = event.clientX - start.x;
573
+ const dy = event.clientY - start.y;
574
+ const distance = dx * dx + dy * dy;
575
+ if (!dragStartRef.current && distance < 64)
576
+ return;
577
+ if (!dragStartRef.current) {
578
+ dragStartRef.current = start;
579
+ }
580
+ const left = Math.min(dragStartRef.current.x, event.clientX);
581
+ const top = Math.min(dragStartRef.current.y, event.clientY);
582
+ const width = Math.abs(event.clientX - dragStartRef.current.x);
583
+ const height = Math.abs(event.clientY - dragStartRef.current.y);
584
+ const rect = { left, top, width, height };
585
+ setDragRect(rect);
586
+ setDragTargets(detectElementsInDragArea(rect));
587
+ };
588
+ document.addEventListener("mousemove", onMouseMove, { passive: true });
589
+ return () => document.removeEventListener("mousemove", onMouseMove);
590
+ }, [active, editing, pending]);
591
+ useEffect(() => {
592
+ if (!active)
593
+ return;
594
+ const onMouseUp = (event) => {
595
+ const started = dragStartRef.current;
596
+ const currentDrag = dragRect;
597
+ pointerRef.current.start = null;
598
+ dragStartRef.current = null;
599
+ if (!started || !currentDrag) {
600
+ setDragRect(null);
601
+ setDragTargets([]);
602
+ return;
603
+ }
604
+ createSnapshotFromDrag(dragTargets, currentDrag, event.clientX, event.clientY);
605
+ setDragRect(null);
606
+ setDragTargets([]);
607
+ setHoverRect(null);
608
+ };
609
+ document.addEventListener("mouseup", onMouseUp);
610
+ return () => document.removeEventListener("mouseup", onMouseUp);
611
+ }, [active, createSnapshotFromDrag, dragRect, dragTargets]);
612
+ if (!mounted || typeof document === "undefined")
613
+ return null;
614
+ return createPortal(_jsxs(_Fragment, { children: [_jsxs("div", { "data-ekairos-toolbar-root": true, style: {
615
+ position: "fixed",
616
+ right: 18,
617
+ bottom: 18,
618
+ zIndex: 100010,
619
+ display: "flex",
620
+ alignItems: "center",
621
+ gap: 8,
622
+ padding: active ? "8px 10px" : 0,
623
+ borderRadius: 999,
624
+ background: active ? "rgba(24,26,32,0.96)" : "transparent",
625
+ border: active ? "1px solid rgba(255,255,255,0.08)" : "none",
626
+ boxShadow: active ? "0 8px 22px rgba(0,0,0,0.35)" : "none",
627
+ color: "#fff",
628
+ fontFamily: "ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
629
+ }, children: [_jsx("button", { type: "button", onClick: () => setActive((prev) => !prev), style: {
630
+ width: 38,
631
+ height: 38,
632
+ border: "none",
633
+ borderRadius: 999,
634
+ cursor: "pointer",
635
+ background: active ? DEFAULT_ACCENT : "rgba(24,26,32,0.96)",
636
+ color: "#fff",
637
+ fontSize: 12,
638
+ fontWeight: 700,
639
+ letterSpacing: "0.02em",
640
+ }, children: "FB" }), active ? (_jsxs(_Fragment, { children: [_jsx("span", { style: { fontSize: 12, color: "rgba(255,255,255,0.8)" }, children: annotations.length }), _jsx("button", { type: "button", onClick: () => setShowMarkers((prev) => !prev), style: toolbarButtonStyle, children: showMarkers ? "Hide" : "Show" }), _jsx("button", { type: "button", onClick: () => void copyOutput(), style: toolbarButtonStyle, children: copied ? "Copied" : "Copy" }), _jsx("button", { type: "button", onClick: sendOutput, style: toolbarButtonStyle, children: "Send" }), _jsx("button", { type: "button", onClick: clearAll, style: toolbarButtonStyle, children: "Clear" })] })) : null] }), active ? (_jsxs("div", { "data-ekairos-toolbar-root": true, style: {
641
+ position: "fixed",
642
+ inset: 0,
643
+ zIndex: 100000,
644
+ pointerEvents: "none",
645
+ }, children: [hoverRect && !pending && !editing && !dragRect ? (_jsx("div", { style: {
646
+ position: "fixed",
647
+ left: hoverRect.left,
648
+ top: hoverRect.top,
649
+ width: hoverRect.width,
650
+ height: hoverRect.height,
651
+ border: "2px solid rgba(47,123,246,0.55)",
652
+ background: "rgba(47,123,246,0.08)",
653
+ borderRadius: 4,
654
+ boxSizing: "border-box",
655
+ } })) : null, hoverRect && hoverLabel && !pending && !editing && !dragRect ? (_jsx("div", { style: {
656
+ position: "fixed",
657
+ left: Math.max(8, Math.min(window.innerWidth - 220, hoverPosition.x + 10)),
658
+ top: Math.max(8, hoverPosition.y - 24),
659
+ maxWidth: 210,
660
+ padding: "4px 7px",
661
+ borderRadius: 6,
662
+ background: "rgba(0,0,0,0.82)",
663
+ color: "#fff",
664
+ fontSize: 11,
665
+ whiteSpace: "nowrap",
666
+ overflow: "hidden",
667
+ textOverflow: "ellipsis",
668
+ }, children: hoverLabel })) : null, pendingMulti.map((item, index) => {
669
+ const rect = item.element.getBoundingClientRect();
670
+ return (_jsx("div", { style: {
671
+ position: "fixed",
672
+ left: rect.left,
673
+ top: rect.top,
674
+ width: rect.width,
675
+ height: rect.height,
676
+ borderRadius: 4,
677
+ border: `2px dashed ${pendingMulti.length > 1 ? MULTI_ACCENT : DEFAULT_ACCENT}`,
678
+ background: pendingMulti.length > 1
679
+ ? "rgba(47,191,113,0.08)"
680
+ : "rgba(47,123,246,0.08)",
681
+ } }, `${item.path}-${index}`));
682
+ }), dragRect ? (_jsx("div", { style: {
683
+ position: "fixed",
684
+ left: dragRect.left,
685
+ top: dragRect.top,
686
+ width: dragRect.width,
687
+ height: dragRect.height,
688
+ borderRadius: 4,
689
+ border: "2px solid rgba(47,191,113,0.65)",
690
+ background: "rgba(47,191,113,0.1)",
691
+ } })) : null, dragRect
692
+ ? dragTargets.map((element, index) => {
693
+ const rect = element.getBoundingClientRect();
694
+ return (_jsx("div", { style: {
695
+ position: "fixed",
696
+ left: rect.left,
697
+ top: rect.top,
698
+ width: rect.width,
699
+ height: rect.height,
700
+ borderRadius: 4,
701
+ border: "2px solid rgba(47,191,113,0.45)",
702
+ background: "rgba(47,191,113,0.05)",
703
+ } }, `${index}-${rect.left}-${rect.top}`));
704
+ })
705
+ : null, showMarkers
706
+ ? annotations.map((annotation, index) => {
707
+ const y = getViewportY(annotation, scrollY);
708
+ if (!annotation.isFixed && (y < -20 || y > window.innerHeight + 20)) {
709
+ return null;
710
+ }
711
+ const isMulti = !!annotation.isMultiSelect;
712
+ const hovered = hoveredMarkerId === annotation.id;
713
+ return (_jsx("div", { "data-ekairos-toolbar-marker": true, onMouseEnter: (event) => {
714
+ event.stopPropagation();
715
+ setHoveredMarkerId(annotation.id);
716
+ }, onMouseLeave: (event) => {
717
+ event.stopPropagation();
718
+ setHoveredMarkerId((prev) => (prev === annotation.id ? null : prev));
719
+ }, onClick: (event) => {
720
+ event.stopPropagation();
721
+ setEditing(annotation);
722
+ setPending(null);
723
+ }, onContextMenu: (event) => {
724
+ event.preventDefault();
725
+ event.stopPropagation();
726
+ setEditing(annotation);
727
+ setPending(null);
728
+ }, style: {
729
+ position: "fixed",
730
+ left: `${annotation.x}%`,
731
+ top: y,
732
+ transform: "translate(-50%, -50%)",
733
+ width: isMulti ? 26 : 22,
734
+ height: isMulti ? 26 : 22,
735
+ borderRadius: isMulti ? 6 : 999,
736
+ border: "none",
737
+ background: isMulti ? MULTI_ACCENT : DEFAULT_ACCENT,
738
+ color: "#fff",
739
+ fontSize: 12,
740
+ fontWeight: 700,
741
+ cursor: "pointer",
742
+ display: "flex",
743
+ alignItems: "center",
744
+ justifyContent: "center",
745
+ boxShadow: "0 3px 8px rgba(0,0,0,0.28)",
746
+ pointerEvents: "auto",
747
+ }, children: hovered ? "E" : index + 1 }, annotation.id));
748
+ })
749
+ : null, hoveredMarkerId && !pending && !editing
750
+ ? (() => {
751
+ const current = annotations.find((annotation) => annotation.id === hoveredMarkerId);
752
+ if (!current)
753
+ return null;
754
+ if (current.elementBoundingBoxes?.length) {
755
+ return current.elementBoundingBoxes.map((box, index) => (_jsx("div", { style: {
756
+ position: "fixed",
757
+ left: box.x,
758
+ top: box.y - scrollY,
759
+ width: box.width,
760
+ height: box.height,
761
+ border: "2px dashed rgba(47,191,113,0.7)",
762
+ background: "rgba(47,191,113,0.08)",
763
+ borderRadius: 4,
764
+ } }, `hover-outline-${index}`)));
765
+ }
766
+ if (!current.boundingBox)
767
+ return null;
768
+ return (_jsx("div", { style: {
769
+ position: "fixed",
770
+ left: current.boundingBox.x,
771
+ top: current.isFixed
772
+ ? current.boundingBox.y
773
+ : current.boundingBox.y - scrollY,
774
+ width: current.boundingBox.width,
775
+ height: current.boundingBox.height,
776
+ border: "2px solid rgba(47,123,246,0.7)",
777
+ background: "rgba(47,123,246,0.08)",
778
+ borderRadius: 4,
779
+ } }));
780
+ })()
781
+ : null, pending
782
+ ? (() => {
783
+ const color = pending.isMultiSelect ? MULTI_ACCENT : DEFAULT_ACCENT;
784
+ const markerY = pending.isFixed ? pending.y : pending.y - scrollY;
785
+ const markerX = pending.x;
786
+ const popupPosition = getPopupPosition(markerX, pending.y, pending.isFixed, scrollY);
787
+ return (_jsxs(_Fragment, { children: [pending.targetElements?.length
788
+ ? pending.targetElements
789
+ .filter((element) => document.contains(element))
790
+ .map((element, index) => {
791
+ const rect = element.getBoundingClientRect();
792
+ return (_jsx("div", { style: {
793
+ position: "fixed",
794
+ left: rect.left,
795
+ top: rect.top,
796
+ width: rect.width,
797
+ height: rect.height,
798
+ borderRadius: 4,
799
+ border: `2px dashed ${MULTI_ACCENT}`,
800
+ background: "rgba(47,191,113,0.08)",
801
+ } }, `pending-el-${index}`));
802
+ })
803
+ : pending.targetElement && document.contains(pending.targetElement)
804
+ ? (() => {
805
+ const rect = pending.targetElement.getBoundingClientRect();
806
+ return (_jsx("div", { style: {
807
+ position: "fixed",
808
+ left: rect.left,
809
+ top: rect.top,
810
+ width: rect.width,
811
+ height: rect.height,
812
+ borderRadius: 4,
813
+ border: `2px solid ${color}`,
814
+ background: "rgba(47,123,246,0.08)",
815
+ } }));
816
+ })()
817
+ : pending.boundingBox
818
+ ? (() => {
819
+ const box = pending.boundingBox;
820
+ return (_jsx("div", { style: {
821
+ position: "fixed",
822
+ left: box.x,
823
+ top: box.y - scrollY,
824
+ width: box.width,
825
+ height: box.height,
826
+ borderRadius: 4,
827
+ border: `2px ${pending.isMultiSelect ? "dashed" : "solid"} ${color}`,
828
+ background: pending.isMultiSelect
829
+ ? "rgba(47,191,113,0.08)"
830
+ : "rgba(47,123,246,0.08)",
831
+ } }));
832
+ })()
833
+ : null, _jsx("div", { style: {
834
+ position: "fixed",
835
+ left: `${markerX}%`,
836
+ top: markerY,
837
+ transform: "translate(-50%, -50%)",
838
+ width: pending.isMultiSelect ? 26 : 22,
839
+ height: pending.isMultiSelect ? 26 : 22,
840
+ borderRadius: pending.isMultiSelect ? 6 : 999,
841
+ background: color,
842
+ color: "#fff",
843
+ display: "flex",
844
+ alignItems: "center",
845
+ justifyContent: "center",
846
+ fontWeight: 700,
847
+ fontSize: 14,
848
+ boxShadow: "0 3px 8px rgba(0,0,0,0.3)",
849
+ }, children: "+" }), _jsx(ToolbarPopup, { ref: popupRef, element: pending.element, selectedText: pending.selectedText, placeholder: pending.element === "Area selection"
850
+ ? "What should change in this area?"
851
+ : pending.isMultiSelect
852
+ ? "Feedback for this set of elements..."
853
+ : "What should change?", onSubmit: addAnnotation, onCancel: () => setPending(null), accentColor: color, style: popupPosition })] }));
854
+ })()
855
+ : null, editing
856
+ ? (() => {
857
+ const popupPosition = getPopupPosition(editing.x, editing.y, editing.isFixed, scrollY);
858
+ return (_jsxs(_Fragment, { children: [editing.elementBoundingBoxes?.length
859
+ ? editing.elementBoundingBoxes.map((box, index) => (_jsx("div", { style: {
860
+ position: "fixed",
861
+ left: box.x,
862
+ top: box.y - scrollY,
863
+ width: box.width,
864
+ height: box.height,
865
+ borderRadius: 4,
866
+ border: "2px dashed rgba(47,191,113,0.7)",
867
+ background: "rgba(47,191,113,0.08)",
868
+ } }, `edit-box-${index}`)))
869
+ : editing.boundingBox
870
+ ? (() => {
871
+ const box = editing.boundingBox;
872
+ return (_jsx("div", { style: {
873
+ position: "fixed",
874
+ left: box.x,
875
+ top: editing.isFixed ? box.y : box.y - scrollY,
876
+ width: box.width,
877
+ height: box.height,
878
+ borderRadius: 4,
879
+ border: `2px ${editing.isMultiSelect ? "dashed" : "solid"} ${editing.isMultiSelect ? MULTI_ACCENT : DEFAULT_ACCENT}`,
880
+ background: editing.isMultiSelect
881
+ ? "rgba(47,191,113,0.08)"
882
+ : "rgba(47,123,246,0.08)",
883
+ } }));
884
+ })()
885
+ : null, _jsx(ToolbarPopup, { ref: editPopupRef, element: editing.element, selectedText: editing.selectedText, initialValue: editing.comment, submitLabel: "Save", onSubmit: updateAnnotation, onCancel: () => setEditing(null), onDelete: () => deleteAnnotation(editing.id), accentColor: editing.isMultiSelect ? MULTI_ACCENT : DEFAULT_ACCENT, style: popupPosition })] }));
886
+ })()
887
+ : null] })) : null] }), document.body);
888
+ }
889
+ const toolbarButtonStyle = {
890
+ border: "1px solid rgba(255,255,255,0.14)",
891
+ borderRadius: 999,
892
+ background: "transparent",
893
+ color: "rgba(255,255,255,0.92)",
894
+ fontSize: 12,
895
+ lineHeight: 1,
896
+ cursor: "pointer",
897
+ padding: "6px 10px",
898
+ };