@mikuexe/annotator-react 0.1.0 → 0.2.1

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,209 @@ 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]);
397
605
  const addAnnotation = (0, import_react.useCallback)(async () => {
398
606
  const current = selectedRef.current;
399
- if (!current || current.loading) {
607
+ if (!current || current.targets.some((targetEntry) => targetEntry.loading)) {
400
608
  return;
401
609
  }
402
610
  const trimmedNote = note.trim();
@@ -404,14 +612,67 @@ function SourceAnnotator({
404
612
  setStatus("Add a note before saving this annotation.");
405
613
  return;
406
614
  }
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) }]);
615
+ const currentTargets = await Promise.all(
616
+ current.targets.map(async (targetEntry) => {
617
+ if (targetEntry.target) {
618
+ return targetEntry;
619
+ }
620
+ const annotation = await captureElementAnnotation(targetEntry.element, trimmedNote);
621
+ return { ...targetEntry, target: annotation.targets[0], loading: false };
622
+ })
623
+ );
624
+ const storedAnnotation = {
625
+ id: current.editingId ?? createAnnotationId(),
626
+ note: trimmedNote,
627
+ targets: currentTargets.map((targetEntry) => ({
628
+ targetElement: targetEntry.element,
629
+ rect: getRect(targetEntry.element, targetEntry.frameElement),
630
+ frameElement: targetEntry.frameElement,
631
+ data: targetEntry.target
632
+ }))
633
+ };
634
+ setAnnotations((existing) => {
635
+ if (!current.editingId) {
636
+ return [...existing, storedAnnotation];
637
+ }
638
+ return existing.map((item) => item.id === current.editingId ? storedAnnotation : item);
639
+ });
409
640
  setSelected(null);
410
641
  setNote("");
411
- setStatus("Annotation saved.");
642
+ setPreviewedAnnotation(null);
643
+ setStatus(current.editingId ? "Annotation updated." : "Annotation saved.");
412
644
  }, [note]);
645
+ const editAnnotation = (0, import_react.useCallback)((annotation) => {
646
+ setLinkingAnnotationId(null);
647
+ setSelected({
648
+ editingId: annotation.id,
649
+ targets: annotation.targets.map((targetEntry) => ({
650
+ element: targetEntry.targetElement,
651
+ rect: getRect(targetEntry.targetElement, targetEntry.frameElement),
652
+ frameElement: targetEntry.frameElement,
653
+ target: targetEntry.data,
654
+ loading: false
655
+ }))
656
+ });
657
+ setNote(annotation.note);
658
+ setPreviewedAnnotation(null);
659
+ setStatus("Editing annotation.");
660
+ }, []);
661
+ const startLinkingAnnotation = (0, import_react.useCallback)((annotationId) => {
662
+ setSelected(null);
663
+ setPreviewedAnnotation(null);
664
+ setLinkingAnnotationId(annotationId);
665
+ setStatus("Click another element to link it to this annotation.");
666
+ }, []);
667
+ const deleteAnnotation = (0, import_react.useCallback)((annotationId) => {
668
+ setAnnotations((existing) => existing.filter((annotation) => annotation.id !== annotationId));
669
+ setSelected((current) => current?.editingId === annotationId ? null : current);
670
+ setPreviewedAnnotation((current) => current?.id === annotationId ? null : current);
671
+ setLinkingAnnotationId((current) => current === annotationId ? null : current);
672
+ setStatus("Annotation deleted.");
673
+ }, []);
413
674
  const collect = (0, import_react.useCallback)(async () => {
414
- const payload = createAnnotationCollection(annotations.map(({ rect: _rect, targetElement: _targetElement, ...annotation }) => annotation));
675
+ const payload = createAnnotationCollection(stripStoredAnnotations(annotations), getPageContext(resolvedTarget.document));
415
676
  const text = formatAnnotationCollection(payload, output);
416
677
  try {
417
678
  await copyTextToClipboard(text);
@@ -419,14 +680,17 @@ function SourceAnnotator({
419
680
  setIsAnnotating(false);
420
681
  setSelected(null);
421
682
  setHoverRect(null);
683
+ setAnnotations([]);
684
+ setPreviewedAnnotation(null);
422
685
  setNote("");
686
+ setStatus(null);
687
+ setLinkingAnnotationId(null);
423
688
  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
689
  } catch (error) {
426
690
  import_sonner.toast.error("Copy failed", { description: error instanceof Error ? error.message : "Clipboard copy failed." });
427
691
  setStatus(error instanceof Error ? error.message : "Clipboard copy failed.");
428
692
  }
429
- }, [annotations, onCollect, output]);
693
+ }, [annotations, onCollect, output, resolvedTarget.document]);
430
694
  if (!enabled) {
431
695
  return null;
432
696
  }
@@ -444,11 +708,25 @@ function SourceAnnotator({
444
708
  }
445
709
  ),
446
710
  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: [
711
+ selected?.targets.map((targetEntry, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, { rect: targetEntry.rect, kind: "selected" }, index)),
712
+ isAnnotating ? annotations.map(
713
+ (annotation, index) => annotation.targets.map((targetEntry, targetIndex) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
714
+ Pin,
715
+ {
716
+ annotation,
717
+ rect: targetEntry.rect,
718
+ index,
719
+ onEdit: editAnnotation,
720
+ onPreview: setPreviewedAnnotation,
721
+ onPreviewEnd: () => setPreviewedAnnotation(null)
722
+ },
723
+ `${annotation.id}:${targetIndex}`
724
+ ))
725
+ ) : null,
726
+ isAnnotating && previewedAnnotation ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AnnotationPreview, { annotation: previewedAnnotation }) : null,
727
+ 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
728
  /* @__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) }),
729
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.metaText, children: formatSelectedTargets(selected.targets) }),
452
730
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
453
731
  "textarea",
454
732
  {
@@ -462,17 +740,26 @@ function SourceAnnotator({
462
740
  ),
463
741
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.popoverActions, children: [
464
742
  /* @__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" })
743
+ /* @__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
744
  ] })
467
745
  ] }) : null,
468
746
  isAnnotating ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { style: styles.panel, "aria-label": "Collected annotations", children: [
469
747
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.panelHeader, children: [
470
748
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: "Annotations" }),
471
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.badge, children: collection.annotations.length })
749
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.badge, children: annotations.length })
472
750
  ] }),
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) })
751
+ 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: [
752
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.annotationContent, children: [
753
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.noteText, children: annotation.note }),
754
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.metaText, children: formatStoredAnnotationSummary(annotation) })
755
+ ] }),
756
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.annotationActions, children: [
757
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: () => startLinkingAnnotation(annotation.id), style: styles.linkButton, children: "Link element" }),
758
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", onClick: () => deleteAnnotation(annotation.id), style: styles.deleteButton, children: [
759
+ "Delete annotation ",
760
+ index + 1
761
+ ] })
762
+ ] })
476
763
  ] }, annotation.id)) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.emptyText, children: "Hover an element, click it, then add a note." }),
477
764
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: collect, style: styles.collectButton, disabled: !annotations.length, children: "Collect" }),
478
765
  status ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.status, children: status }) : null
@@ -483,6 +770,7 @@ function Box({ rect, kind }) {
483
770
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
484
771
  "div",
485
772
  {
773
+ "data-mikuexe-annotator-box": kind,
486
774
  style: {
487
775
  ...styles.box,
488
776
  ...kind === "selected" ? styles.selectedBox : styles.hoverBox,
@@ -494,37 +782,115 @@ function Box({ rect, kind }) {
494
782
  }
495
783
  );
496
784
  }
497
- function Pin({ annotation, index }) {
785
+ function Pin({
786
+ annotation,
787
+ rect,
788
+ index,
789
+ onEdit,
790
+ onPreview,
791
+ onPreviewEnd
792
+ }) {
498
793
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
499
- "div",
794
+ "button",
500
795
  {
501
- 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) },
502
798
  title: annotation.note,
799
+ "aria-label": `Edit annotation ${index + 1}`,
800
+ onClick: () => onEdit(annotation),
801
+ onMouseOver: () => onPreview(annotation),
802
+ onMouseOut: onPreviewEnd,
803
+ onFocus: () => onPreview(annotation),
804
+ onBlur: onPreviewEnd,
503
805
  children: index + 1
504
806
  }
505
807
  );
506
808
  }
507
- function getAnnotatableTarget(target) {
508
- if (!(target instanceof Element)) {
809
+ function AnnotationPreview({ annotation }) {
810
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { role: "tooltip", style: getPreviewStyle(annotation.targets[0]?.rect ?? { top: 8, left: 8, width: 0, height: 0 }), children: [
811
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.noteText, children: annotation.note }),
812
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.metaText, children: formatStoredAnnotationSummary(annotation) })
813
+ ] });
814
+ }
815
+ function stripStoredAnnotations(annotations) {
816
+ return annotations.map((annotation) => ({
817
+ id: annotation.id,
818
+ note: annotation.note,
819
+ targets: annotation.targets.map((targetEntry) => targetEntry.data)
820
+ }));
821
+ }
822
+ function getAnnotatableTarget(target, ownerDocument) {
823
+ if (!isElement(target, ownerDocument)) {
509
824
  return null;
510
825
  }
511
826
  if (target.closest(`[${ROOT_ATTR}]`)) {
512
827
  return null;
513
828
  }
514
- if (target === document.body || target === document.documentElement) {
829
+ if (target === ownerDocument.body || target === ownerDocument.documentElement) {
515
830
  return null;
516
831
  }
517
832
  return target;
518
833
  }
519
- function getRect(element) {
834
+ function getRect(element, frameElement) {
520
835
  const rect = element.getBoundingClientRect();
836
+ const frameRect = frameElement?.getBoundingClientRect();
521
837
  return {
522
- top: rect.top,
523
- left: rect.left,
838
+ top: rect.top + (frameRect?.top ?? 0),
839
+ left: rect.left + (frameRect?.left ?? 0),
524
840
  width: rect.width,
525
841
  height: rect.height
526
842
  };
527
843
  }
844
+ function useResolvedTarget(target) {
845
+ const [navigationVersion, setNavigationVersion] = (0, import_react.useState)(0);
846
+ const resolvedTarget = (0, import_react.useMemo)(() => resolveTarget(target), [target, navigationVersion]);
847
+ const currentDocumentRef = (0, import_react.useRef)(resolvedTarget.document);
848
+ currentDocumentRef.current = resolvedTarget.document;
849
+ (0, import_react.useEffect)(() => {
850
+ if (typeof HTMLIFrameElement !== "undefined" && target instanceof HTMLIFrameElement) {
851
+ const updateTarget = () => {
852
+ const nextTarget = resolveTarget(target);
853
+ if (nextTarget.document !== currentDocumentRef.current) {
854
+ setNavigationVersion((version) => version + 1);
855
+ }
856
+ };
857
+ target.addEventListener("load", updateTarget);
858
+ return () => target.removeEventListener("load", updateTarget);
859
+ }
860
+ }, [target]);
861
+ return resolvedTarget;
862
+ }
863
+ function resolveTarget(target) {
864
+ const hostDocument = typeof document === "undefined" ? null : document;
865
+ if (typeof HTMLIFrameElement !== "undefined" && target instanceof HTMLIFrameElement) {
866
+ const frameDocument = target.contentDocument;
867
+ if (!frameDocument) {
868
+ console.warn("@mikuexe/annotator-react: SourceAnnotator target iframe must be same-origin; iframe contentDocument is not accessible.");
869
+ return {
870
+ document: null,
871
+ frameElement: target
872
+ };
873
+ }
874
+ return {
875
+ document: frameDocument,
876
+ frameElement: target
877
+ };
878
+ }
879
+ if (typeof Document !== "undefined" && target instanceof Document) {
880
+ return {
881
+ document: target,
882
+ frameElement: null
883
+ };
884
+ }
885
+ return {
886
+ document: hostDocument,
887
+ frameElement: null
888
+ };
889
+ }
890
+ function isElement(target, ownerDocument) {
891
+ const elementConstructor = ownerDocument.defaultView?.Element ?? Element;
892
+ return target instanceof elementConstructor;
893
+ }
528
894
  function getPopoverStyle(rect) {
529
895
  const top = Math.min(window.innerHeight - 260, rect.top + rect.height + 8);
530
896
  const left = Math.min(window.innerWidth - 340, Math.max(8, rect.left));
@@ -534,17 +900,45 @@ function getPopoverStyle(rect) {
534
900
  left
535
901
  };
536
902
  }
537
- function formatSelectedSource(annotation) {
538
- const componentPath = annotation?.componentPath.length ? annotation.componentPath.join(" \u203A ") : null;
539
- if (!annotation) {
903
+ function getPreviewStyle(rect) {
904
+ const top = Math.min(window.innerHeight - 120, rect.top + rect.height + 8);
905
+ const left = Math.min(window.innerWidth - 260, Math.max(8, rect.left));
906
+ return {
907
+ ...styles.preview,
908
+ top: Math.max(8, top),
909
+ left
910
+ };
911
+ }
912
+ function formatSelectedTargets(targets) {
913
+ if (!targets.length) {
914
+ return "Source unavailable";
915
+ }
916
+ if (targets.length === 1) {
917
+ return formatTargetSource(targets[0].target);
918
+ }
919
+ const resolvedCount = targets.filter((targetEntry) => targetEntry.target).length;
920
+ return `${targets.length} elements selected \xB7 ${resolvedCount}/${targets.length} resolved`;
921
+ }
922
+ function formatStoredAnnotationSummary(annotation) {
923
+ const firstTarget = annotation.targets[0]?.data;
924
+ const targetSummary = formatTargetSource(firstTarget);
925
+ const linkedSummary = annotation.targets.length > 1 ? ` \xB7 ${annotation.targets.length} linked elements` : "";
926
+ return `${targetSummary}${linkedSummary}`;
927
+ }
928
+ function formatTargetSource(target) {
929
+ const componentPath = target?.componentPath.length ? target.componentPath.join(" \u203A ") : null;
930
+ if (!target) {
540
931
  return "Source unavailable";
541
932
  }
542
- if (!annotation.source?.filePath) {
543
- return `${annotation.element.selector} \xB7 source unavailable`;
933
+ if (!target.source?.filePath) {
934
+ return `${target.element.selector} \xB7 source unavailable`;
544
935
  }
545
- const line = annotation.source.lineNumber ? `:${annotation.source.lineNumber}` : "";
936
+ const line = target.source.lineNumber ? `:${target.source.lineNumber}` : "";
546
937
  const component = componentPath ? ` \xB7 ${componentPath}` : "";
547
- return `${annotation.source.filePath}${line}${component}`;
938
+ return `${target.source.filePath}${line}${component}`;
939
+ }
940
+ function shouldExtendSelection(event) {
941
+ return event.metaKey || event.ctrlKey;
548
942
  }
549
943
  function matchesHotkey(event, hotkey) {
550
944
  const parts = hotkey.toLowerCase().split("+").map((part) => part.trim()).filter(Boolean);
@@ -606,6 +1000,7 @@ var styles = {
606
1000
  },
607
1001
  popover: {
608
1002
  position: "fixed",
1003
+ zIndex: 2,
609
1004
  width: 320,
610
1005
  pointerEvents: "auto",
611
1006
  background: "#ffffff",
@@ -658,6 +1053,7 @@ var styles = {
658
1053
  },
659
1054
  panel: {
660
1055
  position: "fixed",
1056
+ zIndex: 1,
661
1057
  right: 16,
662
1058
  bottom: 68,
663
1059
  width: 300,
@@ -694,11 +1090,23 @@ var styles = {
694
1090
  overflow: "auto"
695
1091
  },
696
1092
  annotationItem: {
1093
+ display: "grid",
1094
+ gap: 8,
1095
+ alignItems: "start",
697
1096
  marginBottom: 8
698
1097
  },
1098
+ annotationContent: {
1099
+ minWidth: 0
1100
+ },
1101
+ annotationActions: {
1102
+ display: "flex",
1103
+ gap: 8,
1104
+ flexWrap: "wrap"
1105
+ },
699
1106
  noteText: {
700
1107
  color: "#0f172a",
701
- fontWeight: 650
1108
+ fontWeight: 650,
1109
+ overflowWrap: "anywhere"
702
1110
  },
703
1111
  emptyText: {
704
1112
  color: "#64748b",
@@ -719,31 +1127,66 @@ var styles = {
719
1127
  color: "#475569",
720
1128
  fontSize: 12
721
1129
  },
1130
+ linkButton: {
1131
+ border: "1px solid #bfdbfe",
1132
+ borderRadius: 999,
1133
+ background: "#eff6ff",
1134
+ color: "#1d4ed8",
1135
+ padding: "4px 7px",
1136
+ fontSize: 11,
1137
+ fontWeight: 750,
1138
+ cursor: "pointer"
1139
+ },
1140
+ deleteButton: {
1141
+ border: "1px solid #fecaca",
1142
+ borderRadius: 999,
1143
+ background: "#fff1f2",
1144
+ color: "#be123c",
1145
+ padding: "4px 7px",
1146
+ fontSize: 11,
1147
+ fontWeight: 750,
1148
+ cursor: "pointer"
1149
+ },
1150
+ preview: {
1151
+ position: "fixed",
1152
+ maxWidth: 240,
1153
+ pointerEvents: "none",
1154
+ background: "#ffffff",
1155
+ border: "1px solid #cbd5e1",
1156
+ borderRadius: 10,
1157
+ padding: 10,
1158
+ boxShadow: "0 14px 35px rgba(15, 23, 42, 0.18)"
1159
+ },
722
1160
  pin: {
723
1161
  position: "fixed",
1162
+ border: 0,
724
1163
  width: 20,
725
1164
  height: 20,
726
1165
  borderRadius: 999,
727
1166
  display: "inline-flex",
728
1167
  alignItems: "center",
729
1168
  justifyContent: "center",
730
- pointerEvents: "none",
1169
+ pointerEvents: "auto",
731
1170
  background: "#f97316",
732
1171
  color: "#ffffff",
733
1172
  fontSize: 12,
734
1173
  fontWeight: 800,
735
- boxShadow: "0 8px 18px rgba(15, 23, 42, 0.2)"
1174
+ boxShadow: "0 8px 18px rgba(15, 23, 42, 0.2)",
1175
+ cursor: "pointer",
1176
+ padding: 0
736
1177
  }
737
1178
  };
738
1179
  // Annotate the CommonJS export names for ESM import in node:
739
1180
  0 && (module.exports = {
740
1181
  SourceAnnotator,
1182
+ captureAnnotationTarget,
741
1183
  captureElementAnnotation,
742
1184
  copyTextToClipboard,
743
1185
  createAnnotationCollection,
744
1186
  formatAnnotationCollection,
745
1187
  formatMarkdown,
746
1188
  getElementSelector,
1189
+ getPageContext,
747
1190
  trimText
748
1191
  });
749
1192
  //# sourceMappingURL=index.cjs.map