@mikuexe/annotator-react 0.1.0 → 0.2.2

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/dist/index.js CHANGED
@@ -10,13 +10,18 @@ var MAX_TEXT_LENGTH = 240;
10
10
  var MAX_HTML_LENGTH = 640;
11
11
  var MAX_SELECTOR_DEPTH = 6;
12
12
  async function captureElementAnnotation(element, note, id = createAnnotationId()) {
13
+ return {
14
+ id,
15
+ note,
16
+ targets: [await captureAnnotationTarget(element)]
17
+ };
18
+ }
19
+ async function captureAnnotationTarget(element) {
13
20
  const elementInfo = await safeResolveElementInfo(element);
14
21
  const source = normalizeSource(elementInfo?.source, elementInfo?.componentName);
15
22
  const sourceStack = normalizeSourceStack(elementInfo?.stack, source);
16
23
  const componentPath = getComponentPath(sourceStack, source);
17
24
  return {
18
- id,
19
- note,
20
25
  source,
21
26
  sourceStack,
22
27
  componentPath,
@@ -170,10 +175,19 @@ async function copyTextToClipboard(text) {
170
175
 
171
176
  // src/format.ts
172
177
  var TASK_FRAMING = "Please update the UI based on these source-linked annotations.";
173
- function createAnnotationCollection(annotations) {
178
+ function createAnnotationCollection(annotations, page = getPageContext()) {
174
179
  return {
175
180
  annotations,
176
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
181
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
182
+ page
183
+ };
184
+ }
185
+ function getPageContext(targetDocument) {
186
+ const activeDocument = targetDocument ?? (typeof document === "undefined" ? null : document);
187
+ const location = activeDocument?.location;
188
+ return {
189
+ domain: location?.hostname ?? "",
190
+ path: location?.pathname ?? ""
177
191
  };
178
192
  }
179
193
  function formatAnnotationCollection(collection, output = "markdown") {
@@ -193,43 +207,28 @@ ${formatJson(collection)}
193
207
  \`\`\``;
194
208
  }
195
209
  function formatMarkdown(collection) {
196
- const lines = [TASK_FRAMING, "", `Collected at: ${collection.createdAt}`, ""];
210
+ const lines = [
211
+ TASK_FRAMING,
212
+ "",
213
+ `Collected at: ${collection.createdAt}`,
214
+ `Domain: ${collection.page.domain}`,
215
+ `Path: ${collection.page.path}`,
216
+ ""
217
+ ];
197
218
  if (collection.annotations.length === 0) {
198
219
  lines.push("No annotations were collected.");
199
220
  return lines.join("\n");
200
221
  }
201
222
  collection.annotations.forEach((annotation, index) => {
202
- const source = formatSource(annotation);
203
- const sourceStack = formatSourceStack(annotation);
204
- const nearestComponent = annotation.source?.componentName;
205
- const ownerPath = annotation.componentPath.join(" \u203A ");
206
223
  lines.push(`## Annotation ${index + 1}`);
207
224
  lines.push("");
208
225
  lines.push(`ID: ${annotation.id}`);
209
226
  lines.push(`Note: ${annotation.note || "(no note provided)"}`);
210
- if (source) {
211
- lines.push(`Source: ${source}`);
212
- }
213
- if (nearestComponent) {
214
- lines.push(`Nearest React component: ${nearestComponent}`);
215
- }
216
- if (ownerPath && ownerPath !== nearestComponent) {
217
- lines.push(`React owner path: ${ownerPath}`);
218
- }
219
- if (sourceStack.length) {
220
- lines.push("React source stack:");
221
- sourceStack.forEach((frame) => lines.push(`- ${frame}`));
222
- }
223
- lines.push(`Element tag: ${annotation.element.tagName}`);
224
- if (annotation.element.html) {
225
- lines.push(`Element HTML: ${annotation.element.html}`);
226
- }
227
- if (annotation.element.text) {
228
- lines.push(`Element text: ${annotation.element.text}`);
229
- }
230
- if (annotation.element.selector) {
231
- lines.push(`Selector: ${annotation.element.selector}`);
232
- }
227
+ annotation.targets.forEach((target, targetIndex) => {
228
+ const isSingleTarget = annotation.targets.length === 1;
229
+ const label = isSingleTarget ? "" : `Target ${targetIndex + 1} `;
230
+ appendTargetMarkdown(lines, target, label);
231
+ });
233
232
  lines.push("");
234
233
  });
235
234
  return lines.join("\n").trimEnd();
@@ -237,8 +236,37 @@ function formatMarkdown(collection) {
237
236
  function formatJson(collection) {
238
237
  return JSON.stringify(collection, null, 2);
239
238
  }
240
- function formatSource(annotation) {
241
- const source = annotation.source;
239
+ function appendTargetMarkdown(lines, target, label) {
240
+ const source = formatSource(target);
241
+ const sourceStack = formatSourceStack(target);
242
+ const nearestComponent = target.source?.componentName;
243
+ const ownerPath = target.componentPath.join(" \u203A ");
244
+ if (source) {
245
+ lines.push(`${label}Source: ${source}`);
246
+ }
247
+ if (nearestComponent) {
248
+ lines.push(`${label}Nearest React component: ${nearestComponent}`);
249
+ }
250
+ if (ownerPath && ownerPath !== nearestComponent) {
251
+ lines.push(`${label}React owner path: ${ownerPath}`);
252
+ }
253
+ if (sourceStack.length) {
254
+ lines.push(`${label}React source stack:`);
255
+ sourceStack.forEach((frame) => lines.push(`- ${frame}`));
256
+ }
257
+ lines.push(`${label}Element tag: ${target.element.tagName}`);
258
+ if (target.element.html) {
259
+ lines.push(`${label}Element HTML: ${target.element.html}`);
260
+ }
261
+ if (target.element.text) {
262
+ lines.push(`${label}Element text: ${target.element.text}`);
263
+ }
264
+ if (target.element.selector) {
265
+ lines.push(`${label}Selector: ${target.element.selector}`);
266
+ }
267
+ }
268
+ function formatSource(target) {
269
+ const source = target.source;
242
270
  if (!source?.filePath) {
243
271
  return "";
244
272
  }
@@ -246,8 +274,8 @@ function formatSource(annotation) {
246
274
  const column = source.columnNumber ? `:${source.columnNumber}` : "";
247
275
  return `${source.filePath}${line}${column}`;
248
276
  }
249
- function formatSourceStack(annotation) {
250
- return annotation.sourceStack.map((frame) => {
277
+ function formatSourceStack(target) {
278
+ return target.sourceStack.map((frame) => {
251
279
  const location = formatSourceFrame(frame);
252
280
  const component = frame.componentName ? ` (${frame.componentName})` : "";
253
281
  return location ? `${location}${component}` : frame.componentName || "";
@@ -267,10 +295,22 @@ import { jsx, jsxs } from "react/jsx-runtime";
267
295
  var ROOT_ATTR = "data-mikuexe-annotator-root";
268
296
  var DEFAULT_HOTKEY = "alt+a";
269
297
  var DEFAULT_OUTPUT = "markdown";
298
+ var BLOCKED_INTERACTION_EVENTS = [
299
+ "pointerdown",
300
+ "pointerup",
301
+ "mousedown",
302
+ "mouseup",
303
+ "dblclick",
304
+ "auxclick",
305
+ "contextmenu",
306
+ "touchstart",
307
+ "touchend"
308
+ ];
270
309
  function SourceAnnotator({
271
310
  enabled = true,
272
311
  hotkey = DEFAULT_HOTKEY,
273
312
  output = DEFAULT_OUTPUT,
313
+ target,
274
314
  onCollect,
275
315
  renderToaster = true
276
316
  }) {
@@ -279,17 +319,41 @@ function SourceAnnotator({
279
319
  const [selected, setSelected] = useState(null);
280
320
  const [note, setNote] = useState("");
281
321
  const [annotations, setAnnotations] = useState([]);
322
+ const [previewedAnnotation, setPreviewedAnnotation] = useState(null);
282
323
  const [status, setStatus] = useState(null);
324
+ const [linkingAnnotationId, setLinkingAnnotationId] = useState(null);
283
325
  const selectedRef = useRef(null);
326
+ const resolvedTarget = useResolvedTarget(target);
284
327
  selectedRef.current = selected;
285
- const collection = useMemo(
286
- () => createAnnotationCollection(annotations.map(({ rect: _rect, targetElement: _targetElement, ...annotation }) => annotation)),
287
- [annotations]
288
- );
328
+ useEffect(() => {
329
+ setHoverRect(null);
330
+ setSelected(null);
331
+ setAnnotations([]);
332
+ setPreviewedAnnotation(null);
333
+ setNote("");
334
+ setStatus(null);
335
+ setLinkingAnnotationId(null);
336
+ }, [resolvedTarget.document, resolvedTarget.frameElement]);
289
337
  const refreshTrackedRects = useCallback(() => {
290
338
  setHoverRect(null);
291
- setSelected((current) => current ? { ...current, rect: getRect(current.element) } : current);
292
- setAnnotations((existing) => existing.map((annotation) => ({ ...annotation, rect: getRect(annotation.targetElement) })));
339
+ setSelected(
340
+ (current) => current ? {
341
+ ...current,
342
+ targets: current.targets.map((targetEntry) => ({
343
+ ...targetEntry,
344
+ rect: getRect(targetEntry.element, targetEntry.frameElement)
345
+ }))
346
+ } : current
347
+ );
348
+ setAnnotations(
349
+ (existing) => existing.map((annotation) => ({
350
+ ...annotation,
351
+ targets: annotation.targets.map((targetEntry) => ({
352
+ ...targetEntry,
353
+ rect: getRect(targetEntry.targetElement, targetEntry.frameElement)
354
+ }))
355
+ }))
356
+ );
293
357
  }, []);
294
358
  useEffect(() => {
295
359
  if (!enabled) {
@@ -304,67 +368,229 @@ function SourceAnnotator({
304
368
  event.preventDefault();
305
369
  setIsAnnotating((current) => enabled && !current);
306
370
  };
371
+ if (typeof document === "undefined") {
372
+ return;
373
+ }
307
374
  document.addEventListener("keydown", onKeyDown);
308
- return () => document.removeEventListener("keydown", onKeyDown);
309
- }, [enabled, hotkey]);
375
+ if (resolvedTarget.document && resolvedTarget.document !== document) {
376
+ resolvedTarget.document.addEventListener("keydown", onKeyDown);
377
+ }
378
+ return () => {
379
+ document.removeEventListener("keydown", onKeyDown);
380
+ if (resolvedTarget.document && resolvedTarget.document !== document) {
381
+ resolvedTarget.document.removeEventListener("keydown", onKeyDown);
382
+ }
383
+ };
384
+ }, [enabled, hotkey, resolvedTarget.document]);
385
+ const handleElementSelection = useCallback(
386
+ async (eventTarget, frameElement, extendSelection) => {
387
+ if (linkingAnnotationId) {
388
+ const rect2 = getRect(eventTarget, frameElement);
389
+ setStatus("Resolving linked element\u2026");
390
+ try {
391
+ const targetData = await captureAnnotationTarget(eventTarget);
392
+ setAnnotations(
393
+ (existing) => existing.map((annotation) => {
394
+ if (annotation.id !== linkingAnnotationId) {
395
+ return annotation;
396
+ }
397
+ if (annotation.targets.some((targetEntry) => targetEntry.targetElement === eventTarget)) {
398
+ return annotation;
399
+ }
400
+ return {
401
+ ...annotation,
402
+ targets: [
403
+ ...annotation.targets,
404
+ {
405
+ targetElement: eventTarget,
406
+ rect: rect2,
407
+ frameElement,
408
+ data: targetData
409
+ }
410
+ ]
411
+ };
412
+ })
413
+ );
414
+ setLinkingAnnotationId(null);
415
+ setPreviewedAnnotation(null);
416
+ setStatus("Element linked to annotation.");
417
+ } catch {
418
+ setStatus("Element linked without source info.");
419
+ setAnnotations(
420
+ (existing) => existing.map((annotation) => {
421
+ if (annotation.id !== linkingAnnotationId) {
422
+ return annotation;
423
+ }
424
+ if (annotation.targets.some((targetEntry) => targetEntry.targetElement === eventTarget)) {
425
+ return annotation;
426
+ }
427
+ return {
428
+ ...annotation,
429
+ targets: [
430
+ ...annotation.targets,
431
+ {
432
+ targetElement: eventTarget,
433
+ rect: rect2,
434
+ frameElement,
435
+ data: {
436
+ source: null,
437
+ sourceStack: [],
438
+ componentPath: [],
439
+ element: {
440
+ tagName: eventTarget.tagName.toLowerCase(),
441
+ text: eventTarget.textContent?.trim() ?? "",
442
+ html: "",
443
+ selector: ""
444
+ }
445
+ }
446
+ }
447
+ ]
448
+ };
449
+ })
450
+ );
451
+ setLinkingAnnotationId(null);
452
+ }
453
+ return;
454
+ }
455
+ const rect = getRect(eventTarget, frameElement);
456
+ const shouldAppend = extendSelection && Boolean(selectedRef.current) && !selectedRef.current?.editingId;
457
+ setSelected((current) => {
458
+ if (shouldAppend && current) {
459
+ if (current.targets.some((targetEntry) => targetEntry.element === eventTarget)) {
460
+ return current;
461
+ }
462
+ return {
463
+ ...current,
464
+ targets: [...current.targets, { element: eventTarget, rect, frameElement, target: null, loading: true }]
465
+ };
466
+ }
467
+ return {
468
+ targets: [{ element: eventTarget, rect, frameElement, target: null, loading: true }]
469
+ };
470
+ });
471
+ if (!shouldAppend) {
472
+ setNote("");
473
+ }
474
+ setStatus("Resolving source\u2026");
475
+ try {
476
+ const targetData = await captureAnnotationTarget(eventTarget);
477
+ setSelected((current) => {
478
+ if (!current) {
479
+ return current;
480
+ }
481
+ return {
482
+ ...current,
483
+ targets: current.targets.map(
484
+ (targetEntry) => targetEntry.element === eventTarget ? { ...targetEntry, target: targetData, loading: false } : targetEntry
485
+ )
486
+ };
487
+ });
488
+ setStatus(targetData.source ? "Source captured." : "Element captured without source info.");
489
+ } catch {
490
+ setSelected((current) => {
491
+ if (!current) {
492
+ return current;
493
+ }
494
+ return {
495
+ ...current,
496
+ targets: current.targets.map(
497
+ (targetEntry) => targetEntry.element === eventTarget ? { ...targetEntry, loading: false } : targetEntry
498
+ )
499
+ };
500
+ });
501
+ setStatus("Element captured without source info.");
502
+ }
503
+ },
504
+ [linkingAnnotationId]
505
+ );
310
506
  useEffect(() => {
311
- if (!enabled || !isAnnotating) {
507
+ if (!enabled || !isAnnotating || !resolvedTarget.document) {
312
508
  setHoverRect(null);
313
509
  return;
314
510
  }
511
+ const activeDocument = resolvedTarget.document;
315
512
  const onPointerOver = (event) => {
316
- const target = getAnnotatableTarget(event.target);
317
- if (!target) {
513
+ const eventTarget = getAnnotatableTarget(event.target, activeDocument);
514
+ if (!eventTarget) {
318
515
  setHoverRect(null);
319
516
  return;
320
517
  }
321
- setHoverRect(getRect(target));
518
+ setHoverRect(getRect(eventTarget, resolvedTarget.frameElement));
519
+ };
520
+ const suppressInteraction = (event) => {
521
+ const eventTarget = getAnnotatableTarget(event.target, activeDocument);
522
+ if (!eventTarget) {
523
+ return;
524
+ }
525
+ event.preventDefault();
526
+ event.stopPropagation();
527
+ event.stopImmediatePropagation();
322
528
  };
323
529
  const onClick = (event) => {
324
- const target = getAnnotatableTarget(event.target);
325
- if (!target) {
530
+ const eventTarget = getAnnotatableTarget(event.target, activeDocument);
531
+ if (!eventTarget) {
326
532
  return;
327
533
  }
328
534
  event.preventDefault();
329
535
  event.stopPropagation();
330
- const rect = getRect(target);
331
- setSelected({ element: target, rect, annotation: null, loading: true });
332
- setNote("");
333
- setStatus("Resolving source\u2026");
334
- captureElementAnnotation(target, "").then((annotation) => {
335
- setSelected((current) => {
336
- if (current?.element !== target) {
337
- return current;
338
- }
339
- return { ...current, annotation, loading: false };
340
- });
341
- setStatus(annotation.source ? "Source captured." : "Element captured without source info.");
342
- }).catch(() => {
343
- setSelected((current) => current?.element === target ? { ...current, loading: false } : current);
344
- setStatus("Element captured without source info.");
345
- });
536
+ event.stopImmediatePropagation();
537
+ void handleElementSelection(eventTarget, resolvedTarget.frameElement, shouldExtendSelection(event));
346
538
  };
347
- document.addEventListener("pointerover", onPointerOver, true);
348
- document.addEventListener("click", onClick, true);
539
+ const onFramePointerLeave = () => {
540
+ setHoverRect(null);
541
+ };
542
+ activeDocument.addEventListener("pointerover", onPointerOver, true);
543
+ activeDocument.addEventListener("click", onClick, true);
544
+ BLOCKED_INTERACTION_EVENTS.forEach((eventName) => activeDocument.addEventListener(eventName, suppressInteraction, true));
545
+ resolvedTarget.frameElement?.addEventListener("pointerleave", onFramePointerLeave);
349
546
  return () => {
350
- document.removeEventListener("pointerover", onPointerOver, true);
351
- document.removeEventListener("click", onClick, true);
547
+ activeDocument.removeEventListener("pointerover", onPointerOver, true);
548
+ activeDocument.removeEventListener("click", onClick, true);
549
+ BLOCKED_INTERACTION_EVENTS.forEach((eventName) => activeDocument.removeEventListener(eventName, suppressInteraction, true));
550
+ resolvedTarget.frameElement?.removeEventListener("pointerleave", onFramePointerLeave);
352
551
  };
353
- }, [enabled, isAnnotating]);
552
+ }, [enabled, handleElementSelection, isAnnotating, resolvedTarget]);
354
553
  useEffect(() => {
355
- if (!enabled || !isAnnotating) {
554
+ if (!enabled || !isAnnotating || !resolvedTarget.document) {
356
555
  return;
357
556
  }
358
- document.addEventListener("scroll", refreshTrackedRects, true);
557
+ const activeDocument = resolvedTarget.document;
558
+ activeDocument.addEventListener("scroll", refreshTrackedRects, true);
559
+ if (typeof document !== "undefined" && activeDocument !== document) {
560
+ document.addEventListener("scroll", refreshTrackedRects, true);
561
+ }
359
562
  window.addEventListener("resize", refreshTrackedRects);
360
563
  return () => {
361
- document.removeEventListener("scroll", refreshTrackedRects, true);
564
+ activeDocument.removeEventListener("scroll", refreshTrackedRects, true);
565
+ if (typeof document !== "undefined" && activeDocument !== document) {
566
+ document.removeEventListener("scroll", refreshTrackedRects, true);
567
+ }
362
568
  window.removeEventListener("resize", refreshTrackedRects);
363
569
  };
364
- }, [enabled, isAnnotating, refreshTrackedRects]);
570
+ }, [enabled, isAnnotating, refreshTrackedRects, resolvedTarget.document]);
571
+ useEffect(() => {
572
+ if (!previewedAnnotation) {
573
+ return;
574
+ }
575
+ const onKeyDown = (event) => {
576
+ if (event.key === "Escape") {
577
+ setPreviewedAnnotation(null);
578
+ }
579
+ };
580
+ document.addEventListener("keydown", onKeyDown);
581
+ if (resolvedTarget.document && resolvedTarget.document !== document) {
582
+ resolvedTarget.document.addEventListener("keydown", onKeyDown);
583
+ }
584
+ return () => {
585
+ document.removeEventListener("keydown", onKeyDown);
586
+ if (resolvedTarget.document && resolvedTarget.document !== document) {
587
+ resolvedTarget.document.removeEventListener("keydown", onKeyDown);
588
+ }
589
+ };
590
+ }, [previewedAnnotation, resolvedTarget.document]);
365
591
  const addAnnotation = useCallback(async () => {
366
592
  const current = selectedRef.current;
367
- if (!current || current.loading) {
593
+ if (!current || current.targets.some((targetEntry) => targetEntry.loading)) {
368
594
  return;
369
595
  }
370
596
  const trimmedNote = note.trim();
@@ -372,14 +598,67 @@ function SourceAnnotator({
372
598
  setStatus("Add a note before saving this annotation.");
373
599
  return;
374
600
  }
375
- const annotation = current.annotation ? { ...current.annotation, note: trimmedNote } : await captureElementAnnotation(current.element, trimmedNote);
376
- setAnnotations((existing) => [...existing, { ...annotation, targetElement: current.element, rect: getRect(current.element) }]);
601
+ const currentTargets = await Promise.all(
602
+ current.targets.map(async (targetEntry) => {
603
+ if (targetEntry.target) {
604
+ return targetEntry;
605
+ }
606
+ const annotation = await captureElementAnnotation(targetEntry.element, trimmedNote);
607
+ return { ...targetEntry, target: annotation.targets[0], loading: false };
608
+ })
609
+ );
610
+ const storedAnnotation = {
611
+ id: current.editingId ?? createAnnotationId(),
612
+ note: trimmedNote,
613
+ targets: currentTargets.map((targetEntry) => ({
614
+ targetElement: targetEntry.element,
615
+ rect: getRect(targetEntry.element, targetEntry.frameElement),
616
+ frameElement: targetEntry.frameElement,
617
+ data: targetEntry.target
618
+ }))
619
+ };
620
+ setAnnotations((existing) => {
621
+ if (!current.editingId) {
622
+ return [...existing, storedAnnotation];
623
+ }
624
+ return existing.map((item) => item.id === current.editingId ? storedAnnotation : item);
625
+ });
377
626
  setSelected(null);
378
627
  setNote("");
379
- setStatus("Annotation saved.");
628
+ setPreviewedAnnotation(null);
629
+ setStatus(current.editingId ? "Annotation updated." : "Annotation saved.");
380
630
  }, [note]);
631
+ const editAnnotation = useCallback((annotation) => {
632
+ setLinkingAnnotationId(null);
633
+ setSelected({
634
+ editingId: annotation.id,
635
+ targets: annotation.targets.map((targetEntry) => ({
636
+ element: targetEntry.targetElement,
637
+ rect: getRect(targetEntry.targetElement, targetEntry.frameElement),
638
+ frameElement: targetEntry.frameElement,
639
+ target: targetEntry.data,
640
+ loading: false
641
+ }))
642
+ });
643
+ setNote(annotation.note);
644
+ setPreviewedAnnotation(null);
645
+ setStatus("Editing annotation.");
646
+ }, []);
647
+ const startLinkingAnnotation = useCallback((annotationId) => {
648
+ setSelected(null);
649
+ setPreviewedAnnotation(null);
650
+ setLinkingAnnotationId(annotationId);
651
+ setStatus("Click another element to link it to this annotation.");
652
+ }, []);
653
+ const deleteAnnotation = useCallback((annotationId) => {
654
+ setAnnotations((existing) => existing.filter((annotation) => annotation.id !== annotationId));
655
+ setSelected((current) => current?.editingId === annotationId ? null : current);
656
+ setPreviewedAnnotation((current) => current?.id === annotationId ? null : current);
657
+ setLinkingAnnotationId((current) => current === annotationId ? null : current);
658
+ setStatus("Annotation deleted.");
659
+ }, []);
381
660
  const collect = useCallback(async () => {
382
- const payload = createAnnotationCollection(annotations.map(({ rect: _rect, targetElement: _targetElement, ...annotation }) => annotation));
661
+ const payload = createAnnotationCollection(stripStoredAnnotations(annotations), getPageContext(resolvedTarget.document));
383
662
  const text = formatAnnotationCollection(payload, output);
384
663
  try {
385
664
  await copyTextToClipboard(text);
@@ -387,14 +666,17 @@ function SourceAnnotator({
387
666
  setIsAnnotating(false);
388
667
  setSelected(null);
389
668
  setHoverRect(null);
669
+ setAnnotations([]);
670
+ setPreviewedAnnotation(null);
390
671
  setNote("");
672
+ setStatus(null);
673
+ setLinkingAnnotationId(null);
391
674
  toast.success("Annotations copied", { description: `${payload.annotations.length} copied to clipboard.` });
392
- setStatus(`Copied ${payload.annotations.length} annotation${payload.annotations.length === 1 ? "" : "s"}.`);
393
675
  } catch (error) {
394
676
  toast.error("Copy failed", { description: error instanceof Error ? error.message : "Clipboard copy failed." });
395
677
  setStatus(error instanceof Error ? error.message : "Clipboard copy failed.");
396
678
  }
397
- }, [annotations, onCollect, output]);
679
+ }, [annotations, onCollect, output, resolvedTarget.document]);
398
680
  if (!enabled) {
399
681
  return null;
400
682
  }
@@ -412,11 +694,33 @@ function SourceAnnotator({
412
694
  }
413
695
  ),
414
696
  isAnnotating && hoverRect ? /* @__PURE__ */ jsx(Box, { rect: hoverRect, kind: "hover" }) : null,
415
- selected ? /* @__PURE__ */ jsx(Box, { rect: selected.rect, kind: "selected" }) : null,
416
- isAnnotating ? annotations.map((annotation, index) => /* @__PURE__ */ jsx(Pin, { annotation, index }, annotation.id)) : null,
417
- selected ? /* @__PURE__ */ jsxs("div", { style: getPopoverStyle(selected.rect), role: "dialog", "aria-label": "Add source annotation", children: [
697
+ selected?.targets.map((targetEntry, index) => /* @__PURE__ */ jsx(Box, { rect: targetEntry.rect, kind: "selected" }, index)),
698
+ isAnnotating ? annotations.map(
699
+ (annotation, index) => annotation.targets.map((targetEntry, targetIndex) => /* @__PURE__ */ jsx(
700
+ Pin,
701
+ {
702
+ annotation,
703
+ rect: targetEntry.rect,
704
+ index,
705
+ isPreviewed: previewedAnnotation?.id === annotation.id,
706
+ onPreview: setPreviewedAnnotation
707
+ },
708
+ `${annotation.id}:${targetIndex}`
709
+ ))
710
+ ) : null,
711
+ isAnnotating && previewedAnnotation ? /* @__PURE__ */ jsx(
712
+ AnnotationPreview,
713
+ {
714
+ annotation: previewedAnnotation,
715
+ index: annotations.findIndex((annotation) => annotation.id === previewedAnnotation.id),
716
+ onEdit: editAnnotation,
717
+ onDelete: deleteAnnotation,
718
+ onClose: () => setPreviewedAnnotation(null)
719
+ }
720
+ ) : null,
721
+ selected?.targets.length ? /* @__PURE__ */ jsxs("div", { style: getPopoverStyle(selected.targets[selected.targets.length - 1].rect), role: "dialog", "aria-label": "Add source annotation", children: [
418
722
  /* @__PURE__ */ jsx("div", { style: styles.popoverTitle, children: "Annotation" }),
419
- /* @__PURE__ */ jsx("div", { style: styles.metaText, children: selected.loading ? "Resolving source\u2026" : formatSelectedSource(selected.annotation) }),
723
+ /* @__PURE__ */ jsx("div", { style: styles.metaText, children: formatSelectedTargets(selected.targets) }),
420
724
  /* @__PURE__ */ jsx(
421
725
  "textarea",
422
726
  {
@@ -430,17 +734,33 @@ function SourceAnnotator({
430
734
  ),
431
735
  /* @__PURE__ */ jsxs("div", { style: styles.popoverActions, children: [
432
736
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setSelected(null), style: styles.secondaryButton, children: "Cancel" }),
433
- /* @__PURE__ */ jsx("button", { type: "button", onClick: addAnnotation, style: styles.primaryButton, disabled: selected.loading, children: "Save note" })
737
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: addAnnotation, style: styles.primaryButton, disabled: selected.targets.some((targetEntry) => targetEntry.loading), children: selected.editingId ? "Update note" : "Save note" })
434
738
  ] })
435
739
  ] }) : null,
436
740
  isAnnotating ? /* @__PURE__ */ jsxs("section", { style: styles.panel, "aria-label": "Collected annotations", children: [
437
741
  /* @__PURE__ */ jsxs("div", { style: styles.panelHeader, children: [
438
742
  /* @__PURE__ */ jsx("strong", { children: "Annotations" }),
439
- /* @__PURE__ */ jsx("span", { style: styles.badge, children: collection.annotations.length })
743
+ /* @__PURE__ */ jsx("span", { style: styles.badge, children: annotations.length })
440
744
  ] }),
441
- annotations.length ? /* @__PURE__ */ jsx("ol", { style: styles.annotationList, children: annotations.map((annotation) => /* @__PURE__ */ jsxs("li", { style: styles.annotationItem, children: [
442
- /* @__PURE__ */ jsx("div", { style: styles.noteText, children: annotation.note }),
443
- /* @__PURE__ */ jsx("div", { style: styles.metaText, children: formatSelectedSource(annotation) })
745
+ annotations.length ? /* @__PURE__ */ jsx("ol", { style: styles.annotationList, children: annotations.map((annotation, index) => /* @__PURE__ */ jsxs("li", { style: styles.annotationItem, children: [
746
+ /* @__PURE__ */ jsxs("div", { style: styles.annotationContent, children: [
747
+ /* @__PURE__ */ jsx("div", { style: styles.noteText, children: annotation.note }),
748
+ /* @__PURE__ */ jsx("div", { style: styles.metaText, children: formatStoredAnnotationSummary(annotation) })
749
+ ] }),
750
+ /* @__PURE__ */ jsxs("div", { style: styles.annotationActions, children: [
751
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => startLinkingAnnotation(annotation.id), style: styles.linkButton, children: "Link element" }),
752
+ /* @__PURE__ */ jsx(
753
+ "button",
754
+ {
755
+ type: "button",
756
+ onClick: () => deleteAnnotation(annotation.id),
757
+ style: { ...styles.deleteButton, ...styles.iconButton },
758
+ "aria-label": `Delete annotation ${index + 1}`,
759
+ title: `Delete annotation ${index + 1}`,
760
+ children: "\u{1F5D1}"
761
+ }
762
+ )
763
+ ] })
444
764
  ] }, annotation.id)) }) : /* @__PURE__ */ jsx("p", { style: styles.emptyText, children: "Hover an element, click it, then add a note." }),
445
765
  /* @__PURE__ */ jsx("button", { type: "button", onClick: collect, style: styles.collectButton, disabled: !annotations.length, children: "Collect" }),
446
766
  status ? /* @__PURE__ */ jsx("div", { style: styles.status, children: status }) : null
@@ -451,6 +771,7 @@ function Box({ rect, kind }) {
451
771
  return /* @__PURE__ */ jsx(
452
772
  "div",
453
773
  {
774
+ "data-mikuexe-annotator-box": kind,
454
775
  style: {
455
776
  ...styles.box,
456
777
  ...kind === "selected" ? styles.selectedBox : styles.hoverBox,
@@ -462,37 +783,170 @@ function Box({ rect, kind }) {
462
783
  }
463
784
  );
464
785
  }
465
- function Pin({ annotation, index }) {
786
+ function Pin({
787
+ annotation,
788
+ rect,
789
+ index,
790
+ isPreviewed,
791
+ onPreview
792
+ }) {
466
793
  return /* @__PURE__ */ jsx(
467
- "div",
794
+ "button",
468
795
  {
469
- style: { ...styles.pin, top: Math.max(8, annotation.rect.top - 10), left: Math.max(8, annotation.rect.left - 10) },
796
+ type: "button",
797
+ style: { ...styles.pin, top: Math.max(8, rect.top - 10), left: Math.max(8, rect.left - 10) },
470
798
  title: annotation.note,
799
+ "aria-label": `Show annotation ${index + 1}`,
800
+ "aria-haspopup": "dialog",
801
+ "aria-expanded": isPreviewed,
802
+ onClick: () => onPreview(annotation),
803
+ onMouseOver: () => onPreview(annotation),
804
+ onFocus: () => onPreview(annotation),
471
805
  children: index + 1
472
806
  }
473
807
  );
474
808
  }
475
- function getAnnotatableTarget(target) {
476
- if (!(target instanceof Element)) {
809
+ function AnnotationPreview({
810
+ annotation,
811
+ index,
812
+ onEdit,
813
+ onDelete,
814
+ onClose
815
+ }) {
816
+ const displayIndex = index >= 0 ? index + 1 : 1;
817
+ return /* @__PURE__ */ jsxs(
818
+ "div",
819
+ {
820
+ role: "dialog",
821
+ "aria-label": `Annotation ${displayIndex}`,
822
+ style: getPreviewStyle(annotation.targets[0]?.rect ?? { top: 8, left: 8, width: 0, height: 0 }),
823
+ children: [
824
+ /* @__PURE__ */ jsxs("div", { style: styles.previewHeader, children: [
825
+ /* @__PURE__ */ jsxs("div", { style: styles.previewTitle, children: [
826
+ "Annotation ",
827
+ displayIndex
828
+ ] }),
829
+ /* @__PURE__ */ jsxs("div", { style: styles.previewActions, children: [
830
+ /* @__PURE__ */ jsx(
831
+ "button",
832
+ {
833
+ type: "button",
834
+ onClick: onClose,
835
+ style: { ...styles.secondaryButton, ...styles.iconButton },
836
+ "aria-label": `Close annotation ${displayIndex}`,
837
+ title: `Close annotation ${displayIndex}`,
838
+ children: "\xD7"
839
+ }
840
+ ),
841
+ /* @__PURE__ */ jsx(
842
+ "button",
843
+ {
844
+ type: "button",
845
+ onClick: () => onEdit(annotation),
846
+ style: { ...styles.secondaryButton, ...styles.iconButton },
847
+ "aria-label": `Edit annotation ${displayIndex}`,
848
+ title: `Edit annotation ${displayIndex}`,
849
+ children: "\u270E"
850
+ }
851
+ ),
852
+ /* @__PURE__ */ jsx(
853
+ "button",
854
+ {
855
+ type: "button",
856
+ onClick: () => onDelete(annotation.id),
857
+ style: { ...styles.deleteButton, ...styles.iconButton },
858
+ "aria-label": `Delete annotation ${displayIndex}`,
859
+ title: `Delete annotation ${displayIndex}`,
860
+ children: "\u{1F5D1}"
861
+ }
862
+ )
863
+ ] })
864
+ ] }),
865
+ /* @__PURE__ */ jsx("div", { style: styles.noteText, children: annotation.note }),
866
+ /* @__PURE__ */ jsx("div", { style: styles.metaText, children: formatStoredAnnotationSummary(annotation) })
867
+ ]
868
+ }
869
+ );
870
+ }
871
+ function stripStoredAnnotations(annotations) {
872
+ return annotations.map((annotation) => ({
873
+ id: annotation.id,
874
+ note: annotation.note,
875
+ targets: annotation.targets.map((targetEntry) => targetEntry.data)
876
+ }));
877
+ }
878
+ function getAnnotatableTarget(target, ownerDocument) {
879
+ if (!isElement(target, ownerDocument)) {
477
880
  return null;
478
881
  }
479
882
  if (target.closest(`[${ROOT_ATTR}]`)) {
480
883
  return null;
481
884
  }
482
- if (target === document.body || target === document.documentElement) {
885
+ if (target === ownerDocument.body || target === ownerDocument.documentElement) {
483
886
  return null;
484
887
  }
485
888
  return target;
486
889
  }
487
- function getRect(element) {
890
+ function getRect(element, frameElement) {
488
891
  const rect = element.getBoundingClientRect();
892
+ const frameRect = frameElement?.getBoundingClientRect();
489
893
  return {
490
- top: rect.top,
491
- left: rect.left,
894
+ top: rect.top + (frameRect?.top ?? 0),
895
+ left: rect.left + (frameRect?.left ?? 0),
492
896
  width: rect.width,
493
897
  height: rect.height
494
898
  };
495
899
  }
900
+ function useResolvedTarget(target) {
901
+ const [navigationVersion, setNavigationVersion] = useState(0);
902
+ const resolvedTarget = useMemo(() => resolveTarget(target), [target, navigationVersion]);
903
+ const currentDocumentRef = useRef(resolvedTarget.document);
904
+ currentDocumentRef.current = resolvedTarget.document;
905
+ useEffect(() => {
906
+ if (typeof HTMLIFrameElement !== "undefined" && target instanceof HTMLIFrameElement) {
907
+ const updateTarget = () => {
908
+ const nextTarget = resolveTarget(target);
909
+ if (nextTarget.document !== currentDocumentRef.current) {
910
+ setNavigationVersion((version) => version + 1);
911
+ }
912
+ };
913
+ target.addEventListener("load", updateTarget);
914
+ return () => target.removeEventListener("load", updateTarget);
915
+ }
916
+ }, [target]);
917
+ return resolvedTarget;
918
+ }
919
+ function resolveTarget(target) {
920
+ const hostDocument = typeof document === "undefined" ? null : document;
921
+ if (typeof HTMLIFrameElement !== "undefined" && target instanceof HTMLIFrameElement) {
922
+ const frameDocument = target.contentDocument;
923
+ if (!frameDocument) {
924
+ console.warn("@mikuexe/annotator-react: SourceAnnotator target iframe must be same-origin; iframe contentDocument is not accessible.");
925
+ return {
926
+ document: null,
927
+ frameElement: target
928
+ };
929
+ }
930
+ return {
931
+ document: frameDocument,
932
+ frameElement: target
933
+ };
934
+ }
935
+ if (typeof Document !== "undefined" && target instanceof Document) {
936
+ return {
937
+ document: target,
938
+ frameElement: null
939
+ };
940
+ }
941
+ return {
942
+ document: hostDocument,
943
+ frameElement: null
944
+ };
945
+ }
946
+ function isElement(target, ownerDocument) {
947
+ const elementConstructor = ownerDocument.defaultView?.Element ?? Element;
948
+ return target instanceof elementConstructor;
949
+ }
496
950
  function getPopoverStyle(rect) {
497
951
  const top = Math.min(window.innerHeight - 260, rect.top + rect.height + 8);
498
952
  const left = Math.min(window.innerWidth - 340, Math.max(8, rect.left));
@@ -502,17 +956,45 @@ function getPopoverStyle(rect) {
502
956
  left
503
957
  };
504
958
  }
505
- function formatSelectedSource(annotation) {
506
- const componentPath = annotation?.componentPath.length ? annotation.componentPath.join(" \u203A ") : null;
507
- if (!annotation) {
959
+ function getPreviewStyle(rect) {
960
+ const top = Math.min(window.innerHeight - 120, rect.top + rect.height + 8);
961
+ const left = Math.min(window.innerWidth - 260, Math.max(8, rect.left));
962
+ return {
963
+ ...styles.preview,
964
+ top: Math.max(8, top),
965
+ left
966
+ };
967
+ }
968
+ function formatSelectedTargets(targets) {
969
+ if (!targets.length) {
970
+ return "Source unavailable";
971
+ }
972
+ if (targets.length === 1) {
973
+ return formatTargetSource(targets[0].target);
974
+ }
975
+ const resolvedCount = targets.filter((targetEntry) => targetEntry.target).length;
976
+ return `${targets.length} elements selected \xB7 ${resolvedCount}/${targets.length} resolved`;
977
+ }
978
+ function formatStoredAnnotationSummary(annotation) {
979
+ const firstTarget = annotation.targets[0]?.data;
980
+ const targetSummary = formatTargetSource(firstTarget);
981
+ const linkedSummary = annotation.targets.length > 1 ? ` \xB7 ${annotation.targets.length} linked elements` : "";
982
+ return `${targetSummary}${linkedSummary}`;
983
+ }
984
+ function formatTargetSource(target) {
985
+ const componentPath = target?.componentPath.length ? target.componentPath.join(" \u203A ") : null;
986
+ if (!target) {
508
987
  return "Source unavailable";
509
988
  }
510
- if (!annotation.source?.filePath) {
511
- return `${annotation.element.selector} \xB7 source unavailable`;
989
+ if (!target.source?.filePath) {
990
+ return `${target.element.selector} \xB7 source unavailable`;
512
991
  }
513
- const line = annotation.source.lineNumber ? `:${annotation.source.lineNumber}` : "";
992
+ const line = target.source.lineNumber ? `:${target.source.lineNumber}` : "";
514
993
  const component = componentPath ? ` \xB7 ${componentPath}` : "";
515
- return `${annotation.source.filePath}${line}${component}`;
994
+ return `${target.source.filePath}${line}${component}`;
995
+ }
996
+ function shouldExtendSelection(event) {
997
+ return event.metaKey || event.ctrlKey;
516
998
  }
517
999
  function matchesHotkey(event, hotkey) {
518
1000
  const parts = hotkey.toLowerCase().split("+").map((part) => part.trim()).filter(Boolean);
@@ -574,6 +1056,7 @@ var styles = {
574
1056
  },
575
1057
  popover: {
576
1058
  position: "fixed",
1059
+ zIndex: 2,
577
1060
  width: 320,
578
1061
  pointerEvents: "auto",
579
1062
  background: "#ffffff",
@@ -626,6 +1109,7 @@ var styles = {
626
1109
  },
627
1110
  panel: {
628
1111
  position: "fixed",
1112
+ zIndex: 1,
629
1113
  right: 16,
630
1114
  bottom: 68,
631
1115
  width: 300,
@@ -662,11 +1146,23 @@ var styles = {
662
1146
  overflow: "auto"
663
1147
  },
664
1148
  annotationItem: {
1149
+ display: "grid",
1150
+ gap: 8,
1151
+ alignItems: "start",
665
1152
  marginBottom: 8
666
1153
  },
1154
+ annotationContent: {
1155
+ minWidth: 0
1156
+ },
1157
+ annotationActions: {
1158
+ display: "flex",
1159
+ gap: 8,
1160
+ flexWrap: "wrap"
1161
+ },
667
1162
  noteText: {
668
1163
  color: "#0f172a",
669
- fontWeight: 650
1164
+ fontWeight: 650,
1165
+ overflowWrap: "anywhere"
670
1166
  },
671
1167
  emptyText: {
672
1168
  color: "#64748b",
@@ -687,30 +1183,90 @@ var styles = {
687
1183
  color: "#475569",
688
1184
  fontSize: 12
689
1185
  },
1186
+ linkButton: {
1187
+ border: "1px solid #bfdbfe",
1188
+ borderRadius: 999,
1189
+ background: "#eff6ff",
1190
+ color: "#1d4ed8",
1191
+ padding: "4px 7px",
1192
+ fontSize: 11,
1193
+ fontWeight: 750,
1194
+ cursor: "pointer"
1195
+ },
1196
+ deleteButton: {
1197
+ border: "1px solid #fecaca",
1198
+ borderRadius: 999,
1199
+ background: "#fff1f2",
1200
+ color: "#be123c",
1201
+ padding: "4px 7px",
1202
+ fontSize: 11,
1203
+ fontWeight: 750,
1204
+ cursor: "pointer"
1205
+ },
1206
+ preview: {
1207
+ position: "fixed",
1208
+ maxWidth: 240,
1209
+ pointerEvents: "auto",
1210
+ background: "#ffffff",
1211
+ border: "1px solid #cbd5e1",
1212
+ borderRadius: 10,
1213
+ padding: 10,
1214
+ boxShadow: "0 14px 35px rgba(15, 23, 42, 0.18)"
1215
+ },
1216
+ previewHeader: {
1217
+ display: "flex",
1218
+ alignItems: "center",
1219
+ justifyContent: "space-between",
1220
+ gap: 8,
1221
+ marginBottom: 6
1222
+ },
1223
+ previewTitle: {
1224
+ color: "#475569",
1225
+ fontSize: 12,
1226
+ fontWeight: 800
1227
+ },
1228
+ previewActions: {
1229
+ display: "inline-flex",
1230
+ gap: 6
1231
+ },
1232
+ iconButton: {
1233
+ width: 26,
1234
+ height: 26,
1235
+ display: "inline-flex",
1236
+ alignItems: "center",
1237
+ justifyContent: "center",
1238
+ padding: 0,
1239
+ lineHeight: 1
1240
+ },
690
1241
  pin: {
691
1242
  position: "fixed",
1243
+ border: 0,
692
1244
  width: 20,
693
1245
  height: 20,
694
1246
  borderRadius: 999,
695
1247
  display: "inline-flex",
696
1248
  alignItems: "center",
697
1249
  justifyContent: "center",
698
- pointerEvents: "none",
1250
+ pointerEvents: "auto",
699
1251
  background: "#f97316",
700
1252
  color: "#ffffff",
701
1253
  fontSize: 12,
702
1254
  fontWeight: 800,
703
- boxShadow: "0 8px 18px rgba(15, 23, 42, 0.2)"
1255
+ boxShadow: "0 8px 18px rgba(15, 23, 42, 0.2)",
1256
+ cursor: "pointer",
1257
+ padding: 0
704
1258
  }
705
1259
  };
706
1260
  export {
707
1261
  SourceAnnotator,
1262
+ captureAnnotationTarget,
708
1263
  captureElementAnnotation,
709
1264
  copyTextToClipboard,
710
1265
  createAnnotationCollection,
711
1266
  formatAnnotationCollection,
712
1267
  formatMarkdown,
713
1268
  getElementSelector,
1269
+ getPageContext,
714
1270
  trimText
715
1271
  };
716
1272
  //# sourceMappingURL=index.js.map