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