@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.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,209 @@ 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]);
365
571
  const addAnnotation = useCallback(async () => {
366
572
  const current = selectedRef.current;
367
- if (!current || current.loading) {
573
+ if (!current || current.targets.some((targetEntry) => targetEntry.loading)) {
368
574
  return;
369
575
  }
370
576
  const trimmedNote = note.trim();
@@ -372,14 +578,67 @@ function SourceAnnotator({
372
578
  setStatus("Add a note before saving this annotation.");
373
579
  return;
374
580
  }
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) }]);
581
+ const currentTargets = await Promise.all(
582
+ current.targets.map(async (targetEntry) => {
583
+ if (targetEntry.target) {
584
+ return targetEntry;
585
+ }
586
+ const annotation = await captureElementAnnotation(targetEntry.element, trimmedNote);
587
+ return { ...targetEntry, target: annotation.targets[0], loading: false };
588
+ })
589
+ );
590
+ const storedAnnotation = {
591
+ id: current.editingId ?? createAnnotationId(),
592
+ note: trimmedNote,
593
+ targets: currentTargets.map((targetEntry) => ({
594
+ targetElement: targetEntry.element,
595
+ rect: getRect(targetEntry.element, targetEntry.frameElement),
596
+ frameElement: targetEntry.frameElement,
597
+ data: targetEntry.target
598
+ }))
599
+ };
600
+ setAnnotations((existing) => {
601
+ if (!current.editingId) {
602
+ return [...existing, storedAnnotation];
603
+ }
604
+ return existing.map((item) => item.id === current.editingId ? storedAnnotation : item);
605
+ });
377
606
  setSelected(null);
378
607
  setNote("");
379
- setStatus("Annotation saved.");
608
+ setPreviewedAnnotation(null);
609
+ setStatus(current.editingId ? "Annotation updated." : "Annotation saved.");
380
610
  }, [note]);
611
+ const editAnnotation = useCallback((annotation) => {
612
+ setLinkingAnnotationId(null);
613
+ setSelected({
614
+ editingId: annotation.id,
615
+ targets: annotation.targets.map((targetEntry) => ({
616
+ element: targetEntry.targetElement,
617
+ rect: getRect(targetEntry.targetElement, targetEntry.frameElement),
618
+ frameElement: targetEntry.frameElement,
619
+ target: targetEntry.data,
620
+ loading: false
621
+ }))
622
+ });
623
+ setNote(annotation.note);
624
+ setPreviewedAnnotation(null);
625
+ setStatus("Editing annotation.");
626
+ }, []);
627
+ const startLinkingAnnotation = useCallback((annotationId) => {
628
+ setSelected(null);
629
+ setPreviewedAnnotation(null);
630
+ setLinkingAnnotationId(annotationId);
631
+ setStatus("Click another element to link it to this annotation.");
632
+ }, []);
633
+ const deleteAnnotation = useCallback((annotationId) => {
634
+ setAnnotations((existing) => existing.filter((annotation) => annotation.id !== annotationId));
635
+ setSelected((current) => current?.editingId === annotationId ? null : current);
636
+ setPreviewedAnnotation((current) => current?.id === annotationId ? null : current);
637
+ setLinkingAnnotationId((current) => current === annotationId ? null : current);
638
+ setStatus("Annotation deleted.");
639
+ }, []);
381
640
  const collect = useCallback(async () => {
382
- const payload = createAnnotationCollection(annotations.map(({ rect: _rect, targetElement: _targetElement, ...annotation }) => annotation));
641
+ const payload = createAnnotationCollection(stripStoredAnnotations(annotations), getPageContext(resolvedTarget.document));
383
642
  const text = formatAnnotationCollection(payload, output);
384
643
  try {
385
644
  await copyTextToClipboard(text);
@@ -387,14 +646,17 @@ function SourceAnnotator({
387
646
  setIsAnnotating(false);
388
647
  setSelected(null);
389
648
  setHoverRect(null);
649
+ setAnnotations([]);
650
+ setPreviewedAnnotation(null);
390
651
  setNote("");
652
+ setStatus(null);
653
+ setLinkingAnnotationId(null);
391
654
  toast.success("Annotations copied", { description: `${payload.annotations.length} copied to clipboard.` });
392
- setStatus(`Copied ${payload.annotations.length} annotation${payload.annotations.length === 1 ? "" : "s"}.`);
393
655
  } catch (error) {
394
656
  toast.error("Copy failed", { description: error instanceof Error ? error.message : "Clipboard copy failed." });
395
657
  setStatus(error instanceof Error ? error.message : "Clipboard copy failed.");
396
658
  }
397
- }, [annotations, onCollect, output]);
659
+ }, [annotations, onCollect, output, resolvedTarget.document]);
398
660
  if (!enabled) {
399
661
  return null;
400
662
  }
@@ -412,11 +674,25 @@ function SourceAnnotator({
412
674
  }
413
675
  ),
414
676
  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: [
677
+ selected?.targets.map((targetEntry, index) => /* @__PURE__ */ jsx(Box, { rect: targetEntry.rect, kind: "selected" }, index)),
678
+ isAnnotating ? annotations.map(
679
+ (annotation, index) => annotation.targets.map((targetEntry, targetIndex) => /* @__PURE__ */ jsx(
680
+ Pin,
681
+ {
682
+ annotation,
683
+ rect: targetEntry.rect,
684
+ index,
685
+ onEdit: editAnnotation,
686
+ onPreview: setPreviewedAnnotation,
687
+ onPreviewEnd: () => setPreviewedAnnotation(null)
688
+ },
689
+ `${annotation.id}:${targetIndex}`
690
+ ))
691
+ ) : null,
692
+ isAnnotating && previewedAnnotation ? /* @__PURE__ */ jsx(AnnotationPreview, { annotation: previewedAnnotation }) : null,
693
+ selected?.targets.length ? /* @__PURE__ */ jsxs("div", { style: getPopoverStyle(selected.targets[selected.targets.length - 1].rect), role: "dialog", "aria-label": "Add source annotation", children: [
418
694
  /* @__PURE__ */ jsx("div", { style: styles.popoverTitle, children: "Annotation" }),
419
- /* @__PURE__ */ jsx("div", { style: styles.metaText, children: selected.loading ? "Resolving source\u2026" : formatSelectedSource(selected.annotation) }),
695
+ /* @__PURE__ */ jsx("div", { style: styles.metaText, children: formatSelectedTargets(selected.targets) }),
420
696
  /* @__PURE__ */ jsx(
421
697
  "textarea",
422
698
  {
@@ -430,17 +706,26 @@ function SourceAnnotator({
430
706
  ),
431
707
  /* @__PURE__ */ jsxs("div", { style: styles.popoverActions, children: [
432
708
  /* @__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" })
709
+ /* @__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
710
  ] })
435
711
  ] }) : null,
436
712
  isAnnotating ? /* @__PURE__ */ jsxs("section", { style: styles.panel, "aria-label": "Collected annotations", children: [
437
713
  /* @__PURE__ */ jsxs("div", { style: styles.panelHeader, children: [
438
714
  /* @__PURE__ */ jsx("strong", { children: "Annotations" }),
439
- /* @__PURE__ */ jsx("span", { style: styles.badge, children: collection.annotations.length })
715
+ /* @__PURE__ */ jsx("span", { style: styles.badge, children: annotations.length })
440
716
  ] }),
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) })
717
+ annotations.length ? /* @__PURE__ */ jsx("ol", { style: styles.annotationList, children: annotations.map((annotation, index) => /* @__PURE__ */ jsxs("li", { style: styles.annotationItem, children: [
718
+ /* @__PURE__ */ jsxs("div", { style: styles.annotationContent, children: [
719
+ /* @__PURE__ */ jsx("div", { style: styles.noteText, children: annotation.note }),
720
+ /* @__PURE__ */ jsx("div", { style: styles.metaText, children: formatStoredAnnotationSummary(annotation) })
721
+ ] }),
722
+ /* @__PURE__ */ jsxs("div", { style: styles.annotationActions, children: [
723
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => startLinkingAnnotation(annotation.id), style: styles.linkButton, children: "Link element" }),
724
+ /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => deleteAnnotation(annotation.id), style: styles.deleteButton, children: [
725
+ "Delete annotation ",
726
+ index + 1
727
+ ] })
728
+ ] })
444
729
  ] }, annotation.id)) }) : /* @__PURE__ */ jsx("p", { style: styles.emptyText, children: "Hover an element, click it, then add a note." }),
445
730
  /* @__PURE__ */ jsx("button", { type: "button", onClick: collect, style: styles.collectButton, disabled: !annotations.length, children: "Collect" }),
446
731
  status ? /* @__PURE__ */ jsx("div", { style: styles.status, children: status }) : null
@@ -451,6 +736,7 @@ function Box({ rect, kind }) {
451
736
  return /* @__PURE__ */ jsx(
452
737
  "div",
453
738
  {
739
+ "data-mikuexe-annotator-box": kind,
454
740
  style: {
455
741
  ...styles.box,
456
742
  ...kind === "selected" ? styles.selectedBox : styles.hoverBox,
@@ -462,37 +748,115 @@ function Box({ rect, kind }) {
462
748
  }
463
749
  );
464
750
  }
465
- function Pin({ annotation, index }) {
751
+ function Pin({
752
+ annotation,
753
+ rect,
754
+ index,
755
+ onEdit,
756
+ onPreview,
757
+ onPreviewEnd
758
+ }) {
466
759
  return /* @__PURE__ */ jsx(
467
- "div",
760
+ "button",
468
761
  {
469
- style: { ...styles.pin, top: Math.max(8, annotation.rect.top - 10), left: Math.max(8, annotation.rect.left - 10) },
762
+ type: "button",
763
+ style: { ...styles.pin, top: Math.max(8, rect.top - 10), left: Math.max(8, rect.left - 10) },
470
764
  title: annotation.note,
765
+ "aria-label": `Edit annotation ${index + 1}`,
766
+ onClick: () => onEdit(annotation),
767
+ onMouseOver: () => onPreview(annotation),
768
+ onMouseOut: onPreviewEnd,
769
+ onFocus: () => onPreview(annotation),
770
+ onBlur: onPreviewEnd,
471
771
  children: index + 1
472
772
  }
473
773
  );
474
774
  }
475
- function getAnnotatableTarget(target) {
476
- if (!(target instanceof Element)) {
775
+ function AnnotationPreview({ annotation }) {
776
+ return /* @__PURE__ */ jsxs("div", { role: "tooltip", style: getPreviewStyle(annotation.targets[0]?.rect ?? { top: 8, left: 8, width: 0, height: 0 }), children: [
777
+ /* @__PURE__ */ jsx("div", { style: styles.noteText, children: annotation.note }),
778
+ /* @__PURE__ */ jsx("div", { style: styles.metaText, children: formatStoredAnnotationSummary(annotation) })
779
+ ] });
780
+ }
781
+ function stripStoredAnnotations(annotations) {
782
+ return annotations.map((annotation) => ({
783
+ id: annotation.id,
784
+ note: annotation.note,
785
+ targets: annotation.targets.map((targetEntry) => targetEntry.data)
786
+ }));
787
+ }
788
+ function getAnnotatableTarget(target, ownerDocument) {
789
+ if (!isElement(target, ownerDocument)) {
477
790
  return null;
478
791
  }
479
792
  if (target.closest(`[${ROOT_ATTR}]`)) {
480
793
  return null;
481
794
  }
482
- if (target === document.body || target === document.documentElement) {
795
+ if (target === ownerDocument.body || target === ownerDocument.documentElement) {
483
796
  return null;
484
797
  }
485
798
  return target;
486
799
  }
487
- function getRect(element) {
800
+ function getRect(element, frameElement) {
488
801
  const rect = element.getBoundingClientRect();
802
+ const frameRect = frameElement?.getBoundingClientRect();
489
803
  return {
490
- top: rect.top,
491
- left: rect.left,
804
+ top: rect.top + (frameRect?.top ?? 0),
805
+ left: rect.left + (frameRect?.left ?? 0),
492
806
  width: rect.width,
493
807
  height: rect.height
494
808
  };
495
809
  }
810
+ function useResolvedTarget(target) {
811
+ const [navigationVersion, setNavigationVersion] = useState(0);
812
+ const resolvedTarget = useMemo(() => resolveTarget(target), [target, navigationVersion]);
813
+ const currentDocumentRef = useRef(resolvedTarget.document);
814
+ currentDocumentRef.current = resolvedTarget.document;
815
+ useEffect(() => {
816
+ if (typeof HTMLIFrameElement !== "undefined" && target instanceof HTMLIFrameElement) {
817
+ const updateTarget = () => {
818
+ const nextTarget = resolveTarget(target);
819
+ if (nextTarget.document !== currentDocumentRef.current) {
820
+ setNavigationVersion((version) => version + 1);
821
+ }
822
+ };
823
+ target.addEventListener("load", updateTarget);
824
+ return () => target.removeEventListener("load", updateTarget);
825
+ }
826
+ }, [target]);
827
+ return resolvedTarget;
828
+ }
829
+ function resolveTarget(target) {
830
+ const hostDocument = typeof document === "undefined" ? null : document;
831
+ if (typeof HTMLIFrameElement !== "undefined" && target instanceof HTMLIFrameElement) {
832
+ const frameDocument = target.contentDocument;
833
+ if (!frameDocument) {
834
+ console.warn("@mikuexe/annotator-react: SourceAnnotator target iframe must be same-origin; iframe contentDocument is not accessible.");
835
+ return {
836
+ document: null,
837
+ frameElement: target
838
+ };
839
+ }
840
+ return {
841
+ document: frameDocument,
842
+ frameElement: target
843
+ };
844
+ }
845
+ if (typeof Document !== "undefined" && target instanceof Document) {
846
+ return {
847
+ document: target,
848
+ frameElement: null
849
+ };
850
+ }
851
+ return {
852
+ document: hostDocument,
853
+ frameElement: null
854
+ };
855
+ }
856
+ function isElement(target, ownerDocument) {
857
+ const elementConstructor = ownerDocument.defaultView?.Element ?? Element;
858
+ return target instanceof elementConstructor;
859
+ }
496
860
  function getPopoverStyle(rect) {
497
861
  const top = Math.min(window.innerHeight - 260, rect.top + rect.height + 8);
498
862
  const left = Math.min(window.innerWidth - 340, Math.max(8, rect.left));
@@ -502,17 +866,45 @@ function getPopoverStyle(rect) {
502
866
  left
503
867
  };
504
868
  }
505
- function formatSelectedSource(annotation) {
506
- const componentPath = annotation?.componentPath.length ? annotation.componentPath.join(" \u203A ") : null;
507
- if (!annotation) {
869
+ function getPreviewStyle(rect) {
870
+ const top = Math.min(window.innerHeight - 120, rect.top + rect.height + 8);
871
+ const left = Math.min(window.innerWidth - 260, Math.max(8, rect.left));
872
+ return {
873
+ ...styles.preview,
874
+ top: Math.max(8, top),
875
+ left
876
+ };
877
+ }
878
+ function formatSelectedTargets(targets) {
879
+ if (!targets.length) {
880
+ return "Source unavailable";
881
+ }
882
+ if (targets.length === 1) {
883
+ return formatTargetSource(targets[0].target);
884
+ }
885
+ const resolvedCount = targets.filter((targetEntry) => targetEntry.target).length;
886
+ return `${targets.length} elements selected \xB7 ${resolvedCount}/${targets.length} resolved`;
887
+ }
888
+ function formatStoredAnnotationSummary(annotation) {
889
+ const firstTarget = annotation.targets[0]?.data;
890
+ const targetSummary = formatTargetSource(firstTarget);
891
+ const linkedSummary = annotation.targets.length > 1 ? ` \xB7 ${annotation.targets.length} linked elements` : "";
892
+ return `${targetSummary}${linkedSummary}`;
893
+ }
894
+ function formatTargetSource(target) {
895
+ const componentPath = target?.componentPath.length ? target.componentPath.join(" \u203A ") : null;
896
+ if (!target) {
508
897
  return "Source unavailable";
509
898
  }
510
- if (!annotation.source?.filePath) {
511
- return `${annotation.element.selector} \xB7 source unavailable`;
899
+ if (!target.source?.filePath) {
900
+ return `${target.element.selector} \xB7 source unavailable`;
512
901
  }
513
- const line = annotation.source.lineNumber ? `:${annotation.source.lineNumber}` : "";
902
+ const line = target.source.lineNumber ? `:${target.source.lineNumber}` : "";
514
903
  const component = componentPath ? ` \xB7 ${componentPath}` : "";
515
- return `${annotation.source.filePath}${line}${component}`;
904
+ return `${target.source.filePath}${line}${component}`;
905
+ }
906
+ function shouldExtendSelection(event) {
907
+ return event.metaKey || event.ctrlKey;
516
908
  }
517
909
  function matchesHotkey(event, hotkey) {
518
910
  const parts = hotkey.toLowerCase().split("+").map((part) => part.trim()).filter(Boolean);
@@ -574,6 +966,7 @@ var styles = {
574
966
  },
575
967
  popover: {
576
968
  position: "fixed",
969
+ zIndex: 2,
577
970
  width: 320,
578
971
  pointerEvents: "auto",
579
972
  background: "#ffffff",
@@ -626,6 +1019,7 @@ var styles = {
626
1019
  },
627
1020
  panel: {
628
1021
  position: "fixed",
1022
+ zIndex: 1,
629
1023
  right: 16,
630
1024
  bottom: 68,
631
1025
  width: 300,
@@ -662,11 +1056,23 @@ var styles = {
662
1056
  overflow: "auto"
663
1057
  },
664
1058
  annotationItem: {
1059
+ display: "grid",
1060
+ gap: 8,
1061
+ alignItems: "start",
665
1062
  marginBottom: 8
666
1063
  },
1064
+ annotationContent: {
1065
+ minWidth: 0
1066
+ },
1067
+ annotationActions: {
1068
+ display: "flex",
1069
+ gap: 8,
1070
+ flexWrap: "wrap"
1071
+ },
667
1072
  noteText: {
668
1073
  color: "#0f172a",
669
- fontWeight: 650
1074
+ fontWeight: 650,
1075
+ overflowWrap: "anywhere"
670
1076
  },
671
1077
  emptyText: {
672
1078
  color: "#64748b",
@@ -687,30 +1093,65 @@ var styles = {
687
1093
  color: "#475569",
688
1094
  fontSize: 12
689
1095
  },
1096
+ linkButton: {
1097
+ border: "1px solid #bfdbfe",
1098
+ borderRadius: 999,
1099
+ background: "#eff6ff",
1100
+ color: "#1d4ed8",
1101
+ padding: "4px 7px",
1102
+ fontSize: 11,
1103
+ fontWeight: 750,
1104
+ cursor: "pointer"
1105
+ },
1106
+ deleteButton: {
1107
+ border: "1px solid #fecaca",
1108
+ borderRadius: 999,
1109
+ background: "#fff1f2",
1110
+ color: "#be123c",
1111
+ padding: "4px 7px",
1112
+ fontSize: 11,
1113
+ fontWeight: 750,
1114
+ cursor: "pointer"
1115
+ },
1116
+ preview: {
1117
+ position: "fixed",
1118
+ maxWidth: 240,
1119
+ pointerEvents: "none",
1120
+ background: "#ffffff",
1121
+ border: "1px solid #cbd5e1",
1122
+ borderRadius: 10,
1123
+ padding: 10,
1124
+ boxShadow: "0 14px 35px rgba(15, 23, 42, 0.18)"
1125
+ },
690
1126
  pin: {
691
1127
  position: "fixed",
1128
+ border: 0,
692
1129
  width: 20,
693
1130
  height: 20,
694
1131
  borderRadius: 999,
695
1132
  display: "inline-flex",
696
1133
  alignItems: "center",
697
1134
  justifyContent: "center",
698
- pointerEvents: "none",
1135
+ pointerEvents: "auto",
699
1136
  background: "#f97316",
700
1137
  color: "#ffffff",
701
1138
  fontSize: 12,
702
1139
  fontWeight: 800,
703
- boxShadow: "0 8px 18px rgba(15, 23, 42, 0.2)"
1140
+ boxShadow: "0 8px 18px rgba(15, 23, 42, 0.2)",
1141
+ cursor: "pointer",
1142
+ padding: 0
704
1143
  }
705
1144
  };
706
1145
  export {
707
1146
  SourceAnnotator,
1147
+ captureAnnotationTarget,
708
1148
  captureElementAnnotation,
709
1149
  copyTextToClipboard,
710
1150
  createAnnotationCollection,
711
1151
  formatAnnotationCollection,
712
1152
  formatMarkdown,
713
1153
  getElementSelector,
1154
+ getPageContext,
714
1155
  trimText
715
1156
  };
716
1157
  //# sourceMappingURL=index.js.map