afterbefore 0.2.2 → 0.2.4

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.
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  // src/overlay/index.tsx
4
- import { useState as useState5, useCallback as useCallback6, useRef as useRef5 } from "react";
4
+ import { useState as useState6, useCallback as useCallback7, useRef as useRef6, useEffect as useEffect6 } from "react";
5
5
 
6
6
  // src/overlay/state.ts
7
7
  import { useState, useCallback } from "react";
@@ -33,32 +33,99 @@ function useOverlayState() {
33
33
  }
34
34
 
35
35
  // src/overlay/capture.ts
36
- import { toPng } from "html-to-image";
37
- async function capture(options) {
38
- const { mode, area } = options;
39
- if (mode === "viewport") {
40
- return captureViewport();
36
+ var cachedStream = null;
37
+ async function getTabStream() {
38
+ if (cachedStream && cachedStream.active) {
39
+ return cachedStream;
40
+ }
41
+ const stream = await navigator.mediaDevices.getDisplayMedia({
42
+ video: { displaySurface: "browser" },
43
+ preferCurrentTab: true
44
+ });
45
+ cachedStream = stream;
46
+ stream.getVideoTracks()[0].addEventListener("ended", () => {
47
+ cachedStream = null;
48
+ });
49
+ return stream;
50
+ }
51
+ async function hideOverlay() {
52
+ const elements = document.querySelectorAll(
53
+ "[data-afterbefore]"
54
+ );
55
+ const saved = /* @__PURE__ */ new Map();
56
+ elements.forEach((el) => {
57
+ saved.set(el, el.style.visibility);
58
+ el.style.visibility = "hidden";
59
+ });
60
+ await new Promise((resolve) => {
61
+ requestAnimationFrame(() => {
62
+ requestAnimationFrame(() => {
63
+ setTimeout(resolve, 50);
64
+ });
65
+ });
66
+ });
67
+ return () => {
68
+ saved.forEach((v, el) => {
69
+ el.style.visibility = v;
70
+ });
71
+ };
72
+ }
73
+ async function grabFrame() {
74
+ const stream = await getTabStream();
75
+ const track = stream.getVideoTracks()[0];
76
+ const imageCapture = new window.ImageCapture(track);
77
+ return imageCapture.grabFrame();
78
+ }
79
+ function bitmapToDataUrl(bitmap, crop) {
80
+ const canvas = document.createElement("canvas");
81
+ const ctx = canvas.getContext("2d");
82
+ if (crop) {
83
+ const scaleX = bitmap.width / window.innerWidth;
84
+ const scaleY = bitmap.height / window.innerHeight;
85
+ const sx = crop.x * scaleX;
86
+ const sy = crop.y * scaleY;
87
+ const sw = crop.width * scaleX;
88
+ const sh = crop.height * scaleY;
89
+ canvas.width = sw;
90
+ canvas.height = sh;
91
+ ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, sw, sh);
92
+ } else {
93
+ canvas.width = bitmap.width;
94
+ canvas.height = bitmap.height;
95
+ ctx.drawImage(bitmap, 0, 0);
41
96
  }
97
+ return canvas.toDataURL("image/png");
98
+ }
99
+ async function capture(options) {
100
+ const { mode, area, element } = options;
42
101
  if (mode === "fullpage") {
43
102
  return captureFullPage();
44
103
  }
45
- if (mode === "area" && area) {
46
- return captureArea(area);
104
+ const restoreOverlay = await hideOverlay();
105
+ try {
106
+ const bitmap = await grabFrame();
107
+ if (mode === "viewport") {
108
+ return bitmapToDataUrl(bitmap);
109
+ }
110
+ if (mode === "area" && area) {
111
+ return bitmapToDataUrl(bitmap, area);
112
+ }
113
+ if (mode === "component" && element) {
114
+ const rect = element.getBoundingClientRect();
115
+ return bitmapToDataUrl(bitmap, {
116
+ x: rect.x,
117
+ y: rect.y,
118
+ width: rect.width,
119
+ height: rect.height
120
+ });
121
+ }
122
+ throw new Error(`Invalid capture mode: ${mode}`);
123
+ } finally {
124
+ restoreOverlay();
47
125
  }
48
- throw new Error(`Invalid capture mode: ${mode}`);
49
- }
50
- async function captureViewport() {
51
- const dataUrl = await toPng(document.documentElement, {
52
- width: window.innerWidth,
53
- height: window.innerHeight,
54
- style: {
55
- overflow: "hidden"
56
- },
57
- filter: filterOverlay
58
- });
59
- return dataUrl;
60
126
  }
61
127
  async function captureFullPage() {
128
+ const { toPng } = await import("html-to-image");
62
129
  const scrollY = window.scrollY;
63
130
  const body = document.body;
64
131
  const html = document.documentElement;
@@ -76,43 +143,11 @@ async function captureFullPage() {
76
143
  overflow: "visible",
77
144
  height: `${fullHeight}px`
78
145
  },
79
- filter: filterOverlay
146
+ filter: (node) => !node.dataset?.afterbefore
80
147
  });
81
148
  window.scrollTo(0, scrollY);
82
149
  return dataUrl;
83
150
  }
84
- async function captureArea(area) {
85
- const fullDataUrl = await captureViewport();
86
- const img = await loadImage(fullDataUrl);
87
- const dpr = window.devicePixelRatio || 1;
88
- const canvas = document.createElement("canvas");
89
- canvas.width = area.width * dpr;
90
- canvas.height = area.height * dpr;
91
- const ctx = canvas.getContext("2d");
92
- ctx.drawImage(
93
- img,
94
- area.x * dpr,
95
- area.y * dpr,
96
- area.width * dpr,
97
- area.height * dpr,
98
- 0,
99
- 0,
100
- area.width * dpr,
101
- area.height * dpr
102
- );
103
- return canvas.toDataURL("image/png");
104
- }
105
- function loadImage(src) {
106
- return new Promise((resolve, reject) => {
107
- const img = new Image();
108
- img.onload = () => resolve(img);
109
- img.onerror = reject;
110
- img.src = src;
111
- });
112
- }
113
- function filterOverlay(node) {
114
- return !node.dataset?.afterbefore;
115
- }
116
151
 
117
152
  // src/overlay/ui/icon.tsx
118
153
  import { useRef, useCallback as useCallback2, useEffect, useState as useState2 } from "react";
@@ -338,7 +373,7 @@ var modes = [
338
373
  {
339
374
  mode: "viewport",
340
375
  label: "Viewport",
341
- icon: /* @__PURE__ */ jsxs2("svg", { width: "16", height: "16", viewBox: "0 0 16 16", children: [
376
+ icon: /* @__PURE__ */ jsxs2("svg", { width: "18", height: "18", viewBox: "0 0 16 16", children: [
342
377
  /* @__PURE__ */ jsx2(
343
378
  "rect",
344
379
  {
@@ -369,7 +404,7 @@ var modes = [
369
404
  {
370
405
  mode: "fullpage",
371
406
  label: "Full Page",
372
- icon: /* @__PURE__ */ jsxs2("svg", { width: "16", height: "16", viewBox: "0 0 16 16", children: [
407
+ icon: /* @__PURE__ */ jsxs2("svg", { width: "18", height: "18", viewBox: "0 0 16 16", children: [
373
408
  /* @__PURE__ */ jsx2(
374
409
  "rect",
375
410
  {
@@ -424,7 +459,7 @@ var modes = [
424
459
  {
425
460
  mode: "area",
426
461
  label: "Select Area",
427
- icon: /* @__PURE__ */ jsxs2("svg", { width: "16", height: "16", viewBox: "0 0 16 16", children: [
462
+ icon: /* @__PURE__ */ jsxs2("svg", { width: "18", height: "18", viewBox: "0 0 16 16", children: [
428
463
  /* @__PURE__ */ jsx2(
429
464
  "path",
430
465
  {
@@ -466,9 +501,41 @@ var modes = [
466
501
  }
467
502
  )
468
503
  ] })
504
+ },
505
+ {
506
+ mode: "component",
507
+ label: "Component",
508
+ icon: /* @__PURE__ */ jsxs2("svg", { width: "18", height: "18", viewBox: "0 0 16 16", children: [
509
+ /* @__PURE__ */ jsx2(
510
+ "path",
511
+ {
512
+ d: "M3 2l2.5 10 2-3.5L11 11 3 2z",
513
+ fill: "none",
514
+ stroke: "currentColor",
515
+ strokeWidth: "1.3",
516
+ strokeLinejoin: "round",
517
+ strokeLinecap: "round"
518
+ }
519
+ ),
520
+ /* @__PURE__ */ jsx2(
521
+ "rect",
522
+ {
523
+ x: "8",
524
+ y: "5",
525
+ width: "6.5",
526
+ height: "6.5",
527
+ rx: "1",
528
+ fill: "none",
529
+ stroke: "currentColor",
530
+ strokeWidth: "1.2",
531
+ strokeDasharray: "2 1.5"
532
+ }
533
+ )
534
+ ] })
469
535
  }
470
536
  ];
471
- var MENU_WIDTH = 160;
537
+ var PILL_WIDTH = 270;
538
+ var BTN_SIZE = 40;
472
539
  function Menu({ onSelect, onClose, position }) {
473
540
  const menuRef = useRef2(null);
474
541
  const handleClickOutside = useCallback3(
@@ -497,10 +564,10 @@ function Menu({ onSelect, onClose, position }) {
497
564
  }, [handleClickOutside, handleKeyDown]);
498
565
  const menuLeft = Math.max(
499
566
  8,
500
- Math.min(position.x - MENU_WIDTH / 2 + 20, window.innerWidth - MENU_WIDTH - 8)
567
+ Math.min(position.x - PILL_WIDTH / 2 + 20, window.innerWidth - PILL_WIDTH - 8)
501
568
  );
502
569
  const menuBottom = window.innerHeight - position.y + 8;
503
- return /* @__PURE__ */ jsx2(
570
+ return /* @__PURE__ */ jsxs2(
504
571
  "div",
505
572
  {
506
573
  ref: menuRef,
@@ -509,128 +576,354 @@ function Menu({ onSelect, onClose, position }) {
509
576
  position: "fixed",
510
577
  left: menuLeft,
511
578
  bottom: menuBottom,
512
- width: MENU_WIDTH,
579
+ display: "flex",
580
+ alignItems: "center",
581
+ gap: 4,
513
582
  background: "rgba(24, 24, 27, 0.95)",
514
- borderRadius: 10,
515
- padding: 4,
583
+ borderRadius: 22,
584
+ padding: "5px 6px",
516
585
  boxShadow: "0 4px 20px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.08)",
517
586
  zIndex: 2147483647,
518
587
  fontFamily: "system-ui, -apple-system, sans-serif",
519
588
  backdropFilter: "blur(12px)"
520
589
  },
521
- children: modes.map(({ mode, label, icon }) => /* @__PURE__ */ jsxs2(
522
- "button",
523
- {
524
- onClick: () => onSelect(mode),
525
- style: {
526
- display: "flex",
527
- alignItems: "center",
528
- gap: 8,
529
- width: "100%",
530
- padding: "8px 10px",
531
- border: "none",
532
- background: "transparent",
533
- color: "rgba(255,255,255,0.9)",
534
- fontSize: 13,
535
- borderRadius: 6,
536
- cursor: "pointer",
537
- textAlign: "left",
538
- transition: "background 0.1s"
539
- },
540
- onMouseEnter: (e) => {
541
- e.currentTarget.style.background = "rgba(255,255,255,0.1)";
542
- },
543
- onMouseLeave: (e) => {
544
- e.currentTarget.style.background = "transparent";
590
+ children: [
591
+ modes.map(({ mode, label, icon }) => /* @__PURE__ */ jsx2(
592
+ "button",
593
+ {
594
+ title: label,
595
+ onClick: () => onSelect(mode),
596
+ style: {
597
+ display: "flex",
598
+ alignItems: "center",
599
+ justifyContent: "center",
600
+ width: BTN_SIZE,
601
+ height: BTN_SIZE,
602
+ border: "none",
603
+ background: "transparent",
604
+ color: "rgba(255,255,255,0.7)",
605
+ borderRadius: "50%",
606
+ cursor: "pointer",
607
+ padding: 0,
608
+ transition: "background 0.1s, color 0.1s",
609
+ flexShrink: 0
610
+ },
611
+ onMouseEnter: (e) => {
612
+ const btn = e.currentTarget;
613
+ btn.style.background = "rgba(255,255,255,0.12)";
614
+ btn.style.color = "rgba(255,255,255,1)";
615
+ },
616
+ onMouseLeave: (e) => {
617
+ const btn = e.currentTarget;
618
+ btn.style.background = "transparent";
619
+ btn.style.color = "rgba(255,255,255,0.7)";
620
+ },
621
+ children: icon
545
622
  },
546
- children: [
547
- /* @__PURE__ */ jsx2(
548
- "span",
623
+ mode
624
+ )),
625
+ /* @__PURE__ */ jsx2(
626
+ "div",
627
+ {
628
+ style: {
629
+ width: 1,
630
+ height: 24,
631
+ background: "rgba(255,255,255,0.12)",
632
+ flexShrink: 0,
633
+ margin: "0 2px"
634
+ }
635
+ }
636
+ ),
637
+ /* @__PURE__ */ jsx2(
638
+ "button",
639
+ {
640
+ title: "Close",
641
+ onClick: onClose,
642
+ style: {
643
+ display: "flex",
644
+ alignItems: "center",
645
+ justifyContent: "center",
646
+ width: BTN_SIZE,
647
+ height: BTN_SIZE,
648
+ border: "none",
649
+ background: "transparent",
650
+ color: "rgba(255,255,255,0.45)",
651
+ borderRadius: "50%",
652
+ cursor: "pointer",
653
+ padding: 0,
654
+ transition: "background 0.1s, color 0.1s",
655
+ flexShrink: 0
656
+ },
657
+ onMouseEnter: (e) => {
658
+ const btn = e.currentTarget;
659
+ btn.style.background = "rgba(255,255,255,0.12)";
660
+ btn.style.color = "rgba(255,255,255,0.9)";
661
+ },
662
+ onMouseLeave: (e) => {
663
+ const btn = e.currentTarget;
664
+ btn.style.background = "transparent";
665
+ btn.style.color = "rgba(255,255,255,0.45)";
666
+ },
667
+ children: /* @__PURE__ */ jsx2("svg", { width: "16", height: "16", viewBox: "0 0 14 14", children: /* @__PURE__ */ jsx2(
668
+ "path",
549
669
  {
550
- style: {
551
- display: "flex",
552
- alignItems: "center",
553
- color: "rgba(255,255,255,0.6)"
554
- },
555
- children: icon
670
+ d: "M3 3l8 8M11 3l-8 8",
671
+ stroke: "currentColor",
672
+ strokeWidth: "1.5",
673
+ strokeLinecap: "round"
556
674
  }
557
- ),
558
- label
559
- ]
560
- },
561
- mode
562
- ))
675
+ ) })
676
+ }
677
+ )
678
+ ]
563
679
  }
564
680
  );
565
681
  }
566
682
 
567
683
  // src/overlay/ui/selector.tsx
568
- import { useState as useState3, useRef as useRef3, useCallback as useCallback4, useEffect as useEffect3 } from "react";
684
+ import React3, { useState as useState3, useRef as useRef3, useCallback as useCallback4, useEffect as useEffect3 } from "react";
569
685
  import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
570
- var MIN_SIZE = 10;
686
+ var HANDLE_R = 6;
687
+ var HANDLE_HIT = 16;
688
+ var MIN_SIZE = 20;
689
+ var PANEL_HEIGHT_EST = 140;
690
+ var ASPECT_RATIOS = [
691
+ { label: "Free", value: 0 },
692
+ { label: "16:9", value: 16 / 9 },
693
+ { label: "4:3", value: 4 / 3 },
694
+ { label: "1:1", value: 1 },
695
+ { label: "3:2", value: 3 / 2 },
696
+ { label: "21:9", value: 21 / 9 }
697
+ ];
571
698
  function Selector({ onSelect, onCancel }) {
572
- const [selection, setSelection] = useState3(null);
573
- const dragging = useRef3(false);
574
- const handleKeyDown = useCallback4(
575
- (e) => {
699
+ const [rect, setRect] = useState3(null);
700
+ const [placed, setPlaced] = useState3(false);
701
+ const [aspect, setAspect] = useState3("Free");
702
+ const [aspectOpen, setAspectOpen] = useState3(false);
703
+ const [savedOpen, setSavedOpen] = useState3(false);
704
+ const [presets, setPresets] = useState3(
705
+ () => {
706
+ try {
707
+ const s = localStorage.getItem("ab-area-presets");
708
+ return s ? JSON.parse(s) : [];
709
+ } catch {
710
+ return [];
711
+ }
712
+ }
713
+ );
714
+ const [cursor, setCursor] = useState3("crosshair");
715
+ const mode = useRef3("none");
716
+ const start = useRef3({ x: 0, y: 0 });
717
+ const snap = useRef3({ x: 0, y: 0, w: 0, h: 0 });
718
+ const corner = useRef3("br");
719
+ const panelRef = useRef3(null);
720
+ const ratio = ASPECT_RATIOS.find((a) => a.label === aspect)?.value ?? 0;
721
+ useEffect3(() => {
722
+ const onKey = (e) => {
723
+ if (e.target?.tagName === "INPUT") {
724
+ if (e.key === "Escape") e.target.blur();
725
+ return;
726
+ }
576
727
  if (e.key === "Escape") {
577
- onCancel();
728
+ if (placed) {
729
+ setPlaced(false);
730
+ setRect(null);
731
+ } else {
732
+ onCancel();
733
+ }
734
+ } else if (e.key === "Enter" && placed && rect) {
735
+ onSelect({
736
+ x: Math.round(rect.x),
737
+ y: Math.round(rect.y),
738
+ width: Math.round(rect.w),
739
+ height: Math.round(rect.h)
740
+ });
578
741
  }
742
+ };
743
+ document.addEventListener("keydown", onKey);
744
+ return () => document.removeEventListener("keydown", onKey);
745
+ }, [placed, rect, onSelect, onCancel]);
746
+ const hitCorner = useCallback4(
747
+ (mx, my, r) => {
748
+ const cs = [
749
+ ["tl", r.x, r.y],
750
+ ["tr", r.x + r.w, r.y],
751
+ ["bl", r.x, r.y + r.h],
752
+ ["br", r.x + r.w, r.y + r.h]
753
+ ];
754
+ for (const [c, cx, cy] of cs) {
755
+ if (Math.abs(mx - cx) <= HANDLE_HIT && Math.abs(my - cy) <= HANDLE_HIT)
756
+ return c;
757
+ }
758
+ return null;
579
759
  },
580
- [onCancel]
760
+ []
581
761
  );
582
- useEffect3(() => {
583
- document.addEventListener("keydown", handleKeyDown);
584
- return () => document.removeEventListener("keydown", handleKeyDown);
585
- }, [handleKeyDown]);
586
- const handleMouseDown = useCallback4((e) => {
587
- e.preventDefault();
588
- dragging.current = true;
589
- setSelection({
590
- startX: e.clientX,
591
- startY: e.clientY,
592
- endX: e.clientX,
593
- endY: e.clientY
594
- });
595
- }, []);
596
- const handleMouseMove = useCallback4((e) => {
597
- if (!dragging.current) return;
598
- setSelection((prev) => {
599
- if (!prev) return prev;
600
- return { ...prev, endX: e.clientX, endY: e.clientY };
601
- });
602
- }, []);
603
- const handleMouseUp = useCallback4(() => {
604
- if (!dragging.current || !selection) return;
605
- dragging.current = false;
606
- const x = Math.min(selection.startX, selection.endX);
607
- const y = Math.min(selection.startY, selection.endY);
608
- const width = Math.abs(selection.endX - selection.startX);
609
- const height = Math.abs(selection.endY - selection.startY);
610
- if (width >= MIN_SIZE && height >= MIN_SIZE) {
611
- onSelect({ x, y, width, height });
612
- } else {
613
- setSelection(null);
762
+ const hitInside = useCallback4(
763
+ (mx, my, r) => mx >= r.x && mx <= r.x + r.w && my >= r.y && my <= r.y + r.h,
764
+ []
765
+ );
766
+ const onDown = useCallback4(
767
+ (e) => {
768
+ if (panelRef.current?.contains(e.target)) return;
769
+ e.preventDefault();
770
+ setAspectOpen(false);
771
+ setSavedOpen(false);
772
+ const mx = e.clientX, my = e.clientY;
773
+ if (placed && rect) {
774
+ const c = hitCorner(mx, my, rect);
775
+ if (c) {
776
+ mode.current = "resizing";
777
+ corner.current = c;
778
+ start.current = { x: mx, y: my };
779
+ snap.current = { ...rect };
780
+ return;
781
+ }
782
+ if (hitInside(mx, my, rect)) {
783
+ mode.current = "moving";
784
+ start.current = { x: mx, y: my };
785
+ snap.current = { ...rect };
786
+ return;
787
+ }
788
+ setPlaced(false);
789
+ }
790
+ mode.current = "drawing";
791
+ start.current = { x: mx, y: my };
792
+ setRect({ x: mx, y: my, w: 0, h: 0 });
793
+ },
794
+ [placed, rect, hitCorner, hitInside]
795
+ );
796
+ const onMove = useCallback4(
797
+ (e) => {
798
+ const mx = e.clientX, my = e.clientY;
799
+ if (mode.current === "drawing") {
800
+ const sx = start.current.x, sy = start.current.y;
801
+ let x = Math.min(sx, mx), y = Math.min(sy, my);
802
+ let w = Math.abs(mx - sx), h = Math.abs(my - sy);
803
+ if (ratio > 0) {
804
+ h = w / ratio;
805
+ if (my < sy) y = sy - h;
806
+ }
807
+ setRect({ x, y, w, h });
808
+ } else if (mode.current === "moving") {
809
+ const dx = mx - start.current.x, dy = my - start.current.y;
810
+ setRect({
811
+ ...snap.current,
812
+ x: snap.current.x + dx,
813
+ y: snap.current.y + dy
814
+ });
815
+ } else if (mode.current === "resizing") {
816
+ const o = snap.current;
817
+ const c = corner.current;
818
+ const nr = { ...o };
819
+ if (c === "br") {
820
+ nr.w = Math.max(MIN_SIZE, mx - o.x);
821
+ nr.h = ratio > 0 ? nr.w / ratio : Math.max(MIN_SIZE, my - o.y);
822
+ } else if (c === "bl") {
823
+ nr.w = Math.max(MIN_SIZE, o.x + o.w - mx);
824
+ nr.x = o.x + o.w - nr.w;
825
+ nr.h = ratio > 0 ? nr.w / ratio : Math.max(MIN_SIZE, my - o.y);
826
+ } else if (c === "tr") {
827
+ nr.w = Math.max(MIN_SIZE, mx - o.x);
828
+ nr.h = ratio > 0 ? nr.w / ratio : Math.max(MIN_SIZE, o.y + o.h - my);
829
+ nr.y = o.y + o.h - nr.h;
830
+ } else {
831
+ nr.w = Math.max(MIN_SIZE, o.x + o.w - mx);
832
+ nr.h = ratio > 0 ? nr.w / ratio : Math.max(MIN_SIZE, o.y + o.h - my);
833
+ nr.x = o.x + o.w - nr.w;
834
+ nr.y = o.y + o.h - nr.h;
835
+ }
836
+ setRect(nr);
837
+ } else if (placed && rect) {
838
+ const c = hitCorner(mx, my, rect);
839
+ if (c === "tl" || c === "br") setCursor("nwse-resize");
840
+ else if (c === "tr" || c === "bl") setCursor("nesw-resize");
841
+ else if (hitInside(mx, my, rect)) setCursor("move");
842
+ else setCursor("crosshair");
843
+ }
844
+ },
845
+ [ratio, placed, rect, hitCorner, hitInside]
846
+ );
847
+ const onUp = useCallback4(
848
+ (e) => {
849
+ const prevMode = mode.current;
850
+ if (prevMode === "drawing" && rect) {
851
+ if (rect.w >= MIN_SIZE && rect.h >= MIN_SIZE) {
852
+ setPlaced(true);
853
+ } else {
854
+ setRect(null);
855
+ }
856
+ }
857
+ if (prevMode === "moving" && placed && rect) {
858
+ const dx = Math.abs(e.clientX - start.current.x);
859
+ const dy = Math.abs(e.clientY - start.current.y);
860
+ if (dx < 3 && dy < 3) {
861
+ onSelect({
862
+ x: Math.round(rect.x),
863
+ y: Math.round(rect.y),
864
+ width: Math.round(rect.w),
865
+ height: Math.round(rect.h)
866
+ });
867
+ }
868
+ }
869
+ mode.current = "none";
870
+ },
871
+ [rect, placed, onSelect]
872
+ );
873
+ const setSize = useCallback4(
874
+ (field, v) => {
875
+ const n = parseInt(v, 10);
876
+ if (isNaN(n) || n < MIN_SIZE || !rect) return;
877
+ if (field === "w") {
878
+ setRect({ ...rect, w: n, h: ratio > 0 ? n / ratio : rect.h });
879
+ } else {
880
+ setRect({ ...rect, h: n, w: ratio > 0 ? n * ratio : rect.w });
881
+ }
882
+ },
883
+ [rect, ratio]
884
+ );
885
+ const setPos = useCallback4(
886
+ (field, v) => {
887
+ const n = parseInt(v, 10);
888
+ if (isNaN(n) || !rect) return;
889
+ setRect({ ...rect, [field]: n });
890
+ },
891
+ [rect]
892
+ );
893
+ const savePreset = useCallback4(() => {
894
+ if (!rect) return;
895
+ const label = `${Math.round(rect.w)}\xD7${Math.round(rect.h)}`;
896
+ const next = [
897
+ ...presets.filter((p) => p.label !== label),
898
+ { label, rect: { ...rect } }
899
+ ];
900
+ setPresets(next);
901
+ try {
902
+ localStorage.setItem("ab-area-presets", JSON.stringify(next));
903
+ } catch {
614
904
  }
615
- }, [selection, onSelect]);
616
- const rect = selection ? {
617
- x: Math.min(selection.startX, selection.endX),
618
- y: Math.min(selection.startY, selection.endY),
619
- w: Math.abs(selection.endX - selection.startX),
620
- h: Math.abs(selection.endY - selection.startY)
621
- } : null;
905
+ setSavedOpen(false);
906
+ }, [rect, presets]);
907
+ const loadPreset = useCallback4((p) => {
908
+ setRect({ ...p.rect });
909
+ setPlaced(true);
910
+ setSavedOpen(false);
911
+ }, []);
912
+ const activeCursor = mode.current === "moving" ? "move" : mode.current === "resizing" ? "nwse-resize" : mode.current === "drawing" ? "crosshair" : cursor;
913
+ const panelAbove = rect && rect.y + rect.h + PANEL_HEIGHT_EST > window.innerHeight;
914
+ const hasRect = rect && rect.w > 0 && rect.h > 0;
622
915
  return /* @__PURE__ */ jsxs3(
623
916
  "div",
624
917
  {
625
918
  "data-afterbefore": "true",
626
- onMouseDown: handleMouseDown,
627
- onMouseMove: handleMouseMove,
628
- onMouseUp: handleMouseUp,
919
+ onMouseDown: onDown,
920
+ onMouseMove: onMove,
921
+ onMouseUp: onUp,
629
922
  style: {
630
923
  position: "fixed",
631
924
  inset: 0,
632
925
  zIndex: 2147483647,
633
- cursor: "crosshair"
926
+ cursor: activeCursor
634
927
  },
635
928
  children: [
636
929
  /* @__PURE__ */ jsx3(
@@ -639,9 +932,9 @@ function Selector({ onSelect, onCancel }) {
639
932
  style: {
640
933
  position: "absolute",
641
934
  inset: 0,
642
- background: "rgba(0, 0, 0, 0.4)",
935
+ background: "rgba(0, 0, 0, 0.5)",
643
936
  pointerEvents: "none",
644
- ...rect && rect.w > 0 && rect.h > 0 ? {
937
+ ...hasRect ? {
645
938
  clipPath: `polygon(
646
939
  0% 0%, 0% 100%, 100% 100%, 100% 0%, 0% 0%,
647
940
  ${rect.x}px ${rect.y}px,
@@ -654,7 +947,7 @@ function Selector({ onSelect, onCancel }) {
654
947
  }
655
948
  }
656
949
  ),
657
- rect && rect.w > 0 && rect.h > 0 && /* @__PURE__ */ jsxs3(Fragment, { children: [
950
+ hasRect && /* @__PURE__ */ jsxs3(Fragment, { children: [
658
951
  /* @__PURE__ */ jsx3(
659
952
  "div",
660
953
  {
@@ -664,14 +957,245 @@ function Selector({ onSelect, onCancel }) {
664
957
  top: rect.y,
665
958
  width: rect.w,
666
959
  height: rect.h,
667
- border: "2px solid rgba(59, 130, 246, 0.8)",
668
- borderRadius: 2,
669
- pointerEvents: "none",
670
- boxShadow: "0 0 0 1px rgba(0,0,0,0.3)"
960
+ border: "1.5px dashed rgba(255, 255, 255, 0.45)",
961
+ pointerEvents: "none"
671
962
  }
672
963
  }
673
964
  ),
674
- /* @__PURE__ */ jsxs3(
965
+ placed && [1, 2].map((i) => /* @__PURE__ */ jsxs3(React3.Fragment, { children: [
966
+ /* @__PURE__ */ jsx3(
967
+ "div",
968
+ {
969
+ style: {
970
+ position: "absolute",
971
+ left: rect.x + rect.w * i / 3,
972
+ top: rect.y,
973
+ width: 0,
974
+ height: rect.h,
975
+ borderLeft: "1px dashed rgba(255, 255, 255, 0.18)",
976
+ pointerEvents: "none"
977
+ }
978
+ }
979
+ ),
980
+ /* @__PURE__ */ jsx3(
981
+ "div",
982
+ {
983
+ style: {
984
+ position: "absolute",
985
+ left: rect.x,
986
+ top: rect.y + rect.h * i / 3,
987
+ width: rect.w,
988
+ height: 0,
989
+ borderTop: "1px dashed rgba(255, 255, 255, 0.18)",
990
+ pointerEvents: "none"
991
+ }
992
+ }
993
+ )
994
+ ] }, i)),
995
+ placed && [
996
+ [rect.x, rect.y],
997
+ [rect.x + rect.w, rect.y],
998
+ [rect.x, rect.y + rect.h],
999
+ [rect.x + rect.w, rect.y + rect.h]
1000
+ ].map(([cx, cy], i) => /* @__PURE__ */ jsx3(
1001
+ "div",
1002
+ {
1003
+ style: {
1004
+ position: "absolute",
1005
+ left: cx - HANDLE_R,
1006
+ top: cy - HANDLE_R,
1007
+ width: HANDLE_R * 2,
1008
+ height: HANDLE_R * 2,
1009
+ borderRadius: "50%",
1010
+ border: "2px solid rgba(255, 255, 255, 0.8)",
1011
+ background: "rgba(0, 0, 0, 0.25)",
1012
+ pointerEvents: "none"
1013
+ }
1014
+ },
1015
+ i
1016
+ )),
1017
+ placed && /* @__PURE__ */ jsxs3(
1018
+ "div",
1019
+ {
1020
+ ref: panelRef,
1021
+ onMouseDown: (e) => e.stopPropagation(),
1022
+ style: {
1023
+ position: "absolute",
1024
+ left: rect.x + rect.w / 2,
1025
+ ...panelAbove ? { bottom: window.innerHeight - rect.y + 16 } : { top: rect.y + rect.h + 16 },
1026
+ transform: "translateX(-50%)",
1027
+ background: "rgba(32, 32, 36, 0.92)",
1028
+ backdropFilter: "blur(20px)",
1029
+ WebkitBackdropFilter: "blur(20px)",
1030
+ border: "1px solid rgba(255, 255, 255, 0.1)",
1031
+ borderRadius: 10,
1032
+ padding: "10px 14px",
1033
+ display: "flex",
1034
+ flexDirection: "column",
1035
+ gap: 6,
1036
+ minWidth: 0,
1037
+ fontFamily: "system-ui, -apple-system, sans-serif",
1038
+ color: "rgba(255, 255, 255, 0.9)",
1039
+ boxShadow: "0 8px 32px rgba(0, 0, 0, 0.4)",
1040
+ zIndex: 1
1041
+ },
1042
+ children: [
1043
+ /* @__PURE__ */ jsxs3(Row, { label: "Size", children: [
1044
+ /* @__PURE__ */ jsx3(
1045
+ NumInput,
1046
+ {
1047
+ value: Math.round(rect.w),
1048
+ onChange: (v) => setSize("w", v)
1049
+ }
1050
+ ),
1051
+ /* @__PURE__ */ jsx3(Sep, { children: "\xD7" }),
1052
+ /* @__PURE__ */ jsx3(
1053
+ NumInput,
1054
+ {
1055
+ value: Math.round(rect.h),
1056
+ onChange: (v) => setSize("h", v)
1057
+ }
1058
+ ),
1059
+ /* @__PURE__ */ jsx3(Unit, { children: "px" })
1060
+ ] }),
1061
+ /* @__PURE__ */ jsxs3(Row, { label: "Position", children: [
1062
+ /* @__PURE__ */ jsx3(
1063
+ NumInput,
1064
+ {
1065
+ value: Math.round(rect.x),
1066
+ onChange: (v) => setPos("x", v)
1067
+ }
1068
+ ),
1069
+ /* @__PURE__ */ jsx3(Sep, {}),
1070
+ /* @__PURE__ */ jsx3(
1071
+ NumInput,
1072
+ {
1073
+ value: Math.round(rect.y),
1074
+ onChange: (v) => setPos("y", v)
1075
+ }
1076
+ ),
1077
+ /* @__PURE__ */ jsx3(Unit, { children: "px" })
1078
+ ] }),
1079
+ /* @__PURE__ */ jsx3(
1080
+ "div",
1081
+ {
1082
+ style: {
1083
+ height: 1,
1084
+ background: "rgba(255, 255, 255, 0.08)",
1085
+ margin: "1px 0"
1086
+ }
1087
+ }
1088
+ ),
1089
+ /* @__PURE__ */ jsxs3(
1090
+ "div",
1091
+ {
1092
+ style: { display: "flex", alignItems: "center", gap: 12 },
1093
+ children: [
1094
+ /* @__PURE__ */ jsxs3("div", { style: { position: "relative" }, children: [
1095
+ /* @__PURE__ */ jsxs3(
1096
+ DropBtn,
1097
+ {
1098
+ active: aspect !== "Free",
1099
+ onClick: () => {
1100
+ setAspectOpen(!aspectOpen);
1101
+ setSavedOpen(false);
1102
+ },
1103
+ children: [
1104
+ /* @__PURE__ */ jsx3(AspectIcon, {}),
1105
+ aspect === "Free" ? "Free" : aspect,
1106
+ /* @__PURE__ */ jsx3(Chevron, {})
1107
+ ]
1108
+ }
1109
+ ),
1110
+ aspectOpen && /* @__PURE__ */ jsx3(DropMenu, { children: ASPECT_RATIOS.map((ar) => /* @__PURE__ */ jsx3(
1111
+ DropItem,
1112
+ {
1113
+ active: ar.label === aspect,
1114
+ onClick: () => {
1115
+ setAspect(ar.label);
1116
+ setAspectOpen(false);
1117
+ if (ar.value > 0 && rect) {
1118
+ setRect({ ...rect, h: rect.w / ar.value });
1119
+ }
1120
+ },
1121
+ children: ar.label
1122
+ },
1123
+ ar.label
1124
+ )) })
1125
+ ] }),
1126
+ /* @__PURE__ */ jsxs3("div", { style: { position: "relative" }, children: [
1127
+ /* @__PURE__ */ jsxs3(
1128
+ DropBtn,
1129
+ {
1130
+ onClick: () => {
1131
+ setSavedOpen(!savedOpen);
1132
+ setAspectOpen(false);
1133
+ },
1134
+ children: [
1135
+ /* @__PURE__ */ jsx3(SavedIcon, {}),
1136
+ "Saved",
1137
+ /* @__PURE__ */ jsx3(Chevron, {})
1138
+ ]
1139
+ }
1140
+ ),
1141
+ savedOpen && /* @__PURE__ */ jsxs3(DropMenu, { children: [
1142
+ /* @__PURE__ */ jsx3(
1143
+ DropItem,
1144
+ {
1145
+ accent: true,
1146
+ onClick: savePreset,
1147
+ children: "Save current"
1148
+ }
1149
+ ),
1150
+ presets.length > 0 && /* @__PURE__ */ jsx3(
1151
+ "div",
1152
+ {
1153
+ style: {
1154
+ height: 1,
1155
+ background: "rgba(255,255,255,0.08)",
1156
+ margin: "4px 0"
1157
+ }
1158
+ }
1159
+ ),
1160
+ presets.map((p) => /* @__PURE__ */ jsx3(
1161
+ DropItem,
1162
+ {
1163
+ onClick: () => loadPreset(p),
1164
+ children: p.label
1165
+ },
1166
+ p.label
1167
+ )),
1168
+ presets.length === 0 && /* @__PURE__ */ jsx3(
1169
+ "div",
1170
+ {
1171
+ style: {
1172
+ padding: "6px 12px",
1173
+ color: "rgba(255,255,255,0.3)",
1174
+ fontSize: 12
1175
+ },
1176
+ children: "No saved areas"
1177
+ }
1178
+ )
1179
+ ] })
1180
+ ] })
1181
+ ]
1182
+ }
1183
+ ),
1184
+ /* @__PURE__ */ jsx3(
1185
+ "div",
1186
+ {
1187
+ style: {
1188
+ fontSize: 10,
1189
+ color: "rgba(255, 255, 255, 0.25)",
1190
+ textAlign: "center"
1191
+ },
1192
+ children: "Click area or Enter to capture \xB7 Esc to cancel"
1193
+ }
1194
+ )
1195
+ ]
1196
+ }
1197
+ ),
1198
+ !placed && /* @__PURE__ */ jsxs3(
675
1199
  "div",
676
1200
  {
677
1201
  style: {
@@ -680,7 +1204,7 @@ function Selector({ onSelect, onCancel }) {
680
1204
  top: rect.y + rect.h + 8,
681
1205
  transform: "translateX(-50%)",
682
1206
  background: "rgba(24, 24, 27, 0.9)",
683
- color: "rgba(255,255,255,0.9)",
1207
+ color: "rgba(255, 255, 255, 0.9)",
684
1208
  fontSize: 11,
685
1209
  fontFamily: "system-ui, -apple-system, monospace",
686
1210
  padding: "2px 8px",
@@ -696,7 +1220,7 @@ function Selector({ onSelect, onCancel }) {
696
1220
  }
697
1221
  )
698
1222
  ] }),
699
- !selection && /* @__PURE__ */ jsx3(
1223
+ !rect && /* @__PURE__ */ jsx3(
700
1224
  "div",
701
1225
  {
702
1226
  style: {
@@ -704,11 +1228,11 @@ function Selector({ onSelect, onCancel }) {
704
1228
  top: "50%",
705
1229
  left: "50%",
706
1230
  transform: "translate(-50%, -50%)",
707
- color: "rgba(255,255,255,0.7)",
1231
+ color: "rgba(255, 255, 255, 0.7)",
708
1232
  fontSize: 14,
709
1233
  fontFamily: "system-ui, -apple-system, sans-serif",
710
1234
  pointerEvents: "none",
711
- textShadow: "0 1px 4px rgba(0,0,0,0.5)"
1235
+ textShadow: "0 1px 4px rgba(0, 0, 0, 0.5)"
712
1236
  },
713
1237
  children: "Drag to select an area \xB7 Esc to cancel"
714
1238
  }
@@ -717,20 +1241,386 @@ function Selector({ onSelect, onCancel }) {
717
1241
  }
718
1242
  );
719
1243
  }
1244
+ function Row({
1245
+ label,
1246
+ children
1247
+ }) {
1248
+ return /* @__PURE__ */ jsxs3("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
1249
+ /* @__PURE__ */ jsx3(
1250
+ "span",
1251
+ {
1252
+ style: {
1253
+ width: 36,
1254
+ fontSize: 11,
1255
+ color: "rgba(255, 255, 255, 0.4)",
1256
+ fontWeight: 400,
1257
+ flexShrink: 0
1258
+ },
1259
+ children: label
1260
+ }
1261
+ ),
1262
+ children
1263
+ ] });
1264
+ }
1265
+ function Sep({ children }) {
1266
+ return /* @__PURE__ */ jsx3(
1267
+ "span",
1268
+ {
1269
+ style: {
1270
+ fontSize: 11,
1271
+ color: "rgba(255, 255, 255, 0.35)",
1272
+ width: 12,
1273
+ textAlign: "center",
1274
+ flexShrink: 0,
1275
+ visibility: children ? "visible" : "hidden"
1276
+ },
1277
+ children: children || "\xD7"
1278
+ }
1279
+ );
1280
+ }
1281
+ function Unit({ children }) {
1282
+ return /* @__PURE__ */ jsx3(
1283
+ "span",
1284
+ {
1285
+ style: {
1286
+ fontSize: 10,
1287
+ color: "rgba(255, 255, 255, 0.3)",
1288
+ flexShrink: 0
1289
+ },
1290
+ children
1291
+ }
1292
+ );
1293
+ }
1294
+ function NumInput({
1295
+ value,
1296
+ onChange
1297
+ }) {
1298
+ const [editing, setEditing] = useState3(false);
1299
+ const [text, setText] = useState3(String(value));
1300
+ useEffect3(() => {
1301
+ if (!editing) setText(String(value));
1302
+ }, [value, editing]);
1303
+ return /* @__PURE__ */ jsx3(
1304
+ "input",
1305
+ {
1306
+ type: "text",
1307
+ value: editing ? text : String(value),
1308
+ onFocus: () => {
1309
+ setEditing(true);
1310
+ setText(String(value));
1311
+ },
1312
+ onBlur: () => {
1313
+ setEditing(false);
1314
+ onChange(text);
1315
+ },
1316
+ onChange: (e) => setText(e.target.value),
1317
+ onKeyDown: (e) => {
1318
+ if (e.key === "Enter") e.target.blur();
1319
+ },
1320
+ style: {
1321
+ width: 52,
1322
+ padding: "3px 6px",
1323
+ background: "rgba(255, 255, 255, 0.07)",
1324
+ border: "1px solid rgba(255, 255, 255, 0.1)",
1325
+ borderRadius: 4,
1326
+ color: "rgba(255, 255, 255, 0.9)",
1327
+ fontSize: 12,
1328
+ fontFamily: "system-ui, -apple-system, sans-serif",
1329
+ textAlign: "center",
1330
+ outline: "none"
1331
+ }
1332
+ }
1333
+ );
1334
+ }
1335
+ function DropBtn({
1336
+ children,
1337
+ onClick,
1338
+ active
1339
+ }) {
1340
+ return /* @__PURE__ */ jsx3(
1341
+ "button",
1342
+ {
1343
+ onClick,
1344
+ style: {
1345
+ display: "flex",
1346
+ alignItems: "center",
1347
+ gap: 4,
1348
+ background: "none",
1349
+ border: "none",
1350
+ color: active ? "rgba(147, 130, 220, 0.9)" : "rgba(255, 255, 255, 0.5)",
1351
+ cursor: "pointer",
1352
+ fontSize: 11,
1353
+ fontFamily: "inherit",
1354
+ padding: "2px 0"
1355
+ },
1356
+ children
1357
+ }
1358
+ );
1359
+ }
1360
+ function DropMenu({ children }) {
1361
+ return /* @__PURE__ */ jsx3(
1362
+ "div",
1363
+ {
1364
+ style: {
1365
+ position: "absolute",
1366
+ bottom: "100%",
1367
+ left: 0,
1368
+ marginBottom: 4,
1369
+ background: "rgba(32, 32, 36, 0.95)",
1370
+ border: "1px solid rgba(255, 255, 255, 0.1)",
1371
+ borderRadius: 8,
1372
+ padding: "4px 0",
1373
+ minWidth: 110,
1374
+ boxShadow: "0 4px 16px rgba(0, 0, 0, 0.3)",
1375
+ backdropFilter: "blur(20px)",
1376
+ WebkitBackdropFilter: "blur(20px)"
1377
+ },
1378
+ children
1379
+ }
1380
+ );
1381
+ }
1382
+ function DropItem({
1383
+ children,
1384
+ onClick,
1385
+ active,
1386
+ accent
1387
+ }) {
1388
+ return /* @__PURE__ */ jsx3(
1389
+ "button",
1390
+ {
1391
+ onClick,
1392
+ style: {
1393
+ display: "block",
1394
+ width: "100%",
1395
+ padding: "6px 12px",
1396
+ background: active ? "rgba(255, 255, 255, 0.08)" : "none",
1397
+ border: "none",
1398
+ color: accent ? "rgba(147, 130, 220, 0.9)" : "rgba(255, 255, 255, 0.8)",
1399
+ textAlign: "left",
1400
+ cursor: "pointer",
1401
+ fontSize: 13,
1402
+ fontFamily: "inherit"
1403
+ },
1404
+ children
1405
+ }
1406
+ );
1407
+ }
1408
+ function Chevron() {
1409
+ return /* @__PURE__ */ jsx3("svg", { width: "10", height: "10", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ jsx3(
1410
+ "path",
1411
+ {
1412
+ d: "M3 4l2 2.5L7 4",
1413
+ stroke: "currentColor",
1414
+ strokeWidth: "1.2"
1415
+ }
1416
+ ) });
1417
+ }
1418
+ function AspectIcon() {
1419
+ return /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", children: [
1420
+ /* @__PURE__ */ jsx3(
1421
+ "rect",
1422
+ {
1423
+ x: "2",
1424
+ y: "4",
1425
+ width: "12",
1426
+ height: "8",
1427
+ rx: "1.5",
1428
+ stroke: "currentColor",
1429
+ strokeWidth: "1.2",
1430
+ strokeDasharray: "2 1.5"
1431
+ }
1432
+ ),
1433
+ /* @__PURE__ */ jsx3(
1434
+ "line",
1435
+ {
1436
+ x1: "6.33",
1437
+ y1: "4",
1438
+ x2: "6.33",
1439
+ y2: "12",
1440
+ stroke: "currentColor",
1441
+ strokeWidth: "0.8",
1442
+ strokeDasharray: "1.5 1"
1443
+ }
1444
+ ),
1445
+ /* @__PURE__ */ jsx3(
1446
+ "line",
1447
+ {
1448
+ x1: "9.67",
1449
+ y1: "4",
1450
+ x2: "9.67",
1451
+ y2: "12",
1452
+ stroke: "currentColor",
1453
+ strokeWidth: "0.8",
1454
+ strokeDasharray: "1.5 1"
1455
+ }
1456
+ )
1457
+ ] });
1458
+ }
1459
+ function SavedIcon() {
1460
+ return /* @__PURE__ */ jsx3("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", children: /* @__PURE__ */ jsx3(
1461
+ "rect",
1462
+ {
1463
+ x: "2",
1464
+ y: "3",
1465
+ width: "12",
1466
+ height: "10",
1467
+ rx: "1.5",
1468
+ stroke: "currentColor",
1469
+ strokeWidth: "1.2",
1470
+ strokeDasharray: "2 1.5"
1471
+ }
1472
+ ) });
1473
+ }
1474
+
1475
+ // src/overlay/ui/inspector.tsx
1476
+ import { useEffect as useEffect4, useRef as useRef4, useCallback as useCallback5, useState as useState4 } from "react";
1477
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1478
+ function Inspector({ onSelect, onCancel }) {
1479
+ const [highlight, setHighlight] = useState4(null);
1480
+ const hoveredEl = useRef4(null);
1481
+ const styleEl = useRef4(null);
1482
+ useEffect4(() => {
1483
+ const style = document.createElement("style");
1484
+ style.setAttribute("data-afterbefore", "true");
1485
+ style.textContent = "*, *::before, *::after { cursor: crosshair !important; }";
1486
+ document.head.appendChild(style);
1487
+ styleEl.current = style;
1488
+ return () => {
1489
+ style.remove();
1490
+ };
1491
+ }, []);
1492
+ const isOverlayElement = useCallback5((el) => {
1493
+ let node = el;
1494
+ while (node) {
1495
+ if (node instanceof HTMLElement && node.dataset.afterbefore) return true;
1496
+ node = node.parentElement;
1497
+ }
1498
+ return false;
1499
+ }, []);
1500
+ const handleMouseMove = useCallback5(
1501
+ (e) => {
1502
+ const el = document.elementFromPoint(e.clientX, e.clientY);
1503
+ if (!el || !(el instanceof HTMLElement) || isOverlayElement(el)) {
1504
+ setHighlight(null);
1505
+ hoveredEl.current = null;
1506
+ return;
1507
+ }
1508
+ hoveredEl.current = el;
1509
+ const rect = el.getBoundingClientRect();
1510
+ setHighlight({
1511
+ x: rect.x,
1512
+ y: rect.y,
1513
+ width: rect.width,
1514
+ height: rect.height,
1515
+ tag: el.tagName.toLowerCase() + (el.className && typeof el.className === "string" ? `.${el.className.split(" ")[0]}` : "")
1516
+ });
1517
+ },
1518
+ [isOverlayElement]
1519
+ );
1520
+ const handleClick = useCallback5(
1521
+ (e) => {
1522
+ e.preventDefault();
1523
+ e.stopPropagation();
1524
+ e.stopImmediatePropagation();
1525
+ if (hoveredEl.current) {
1526
+ onSelect(hoveredEl.current);
1527
+ }
1528
+ },
1529
+ [onSelect]
1530
+ );
1531
+ const handleKeyDown = useCallback5(
1532
+ (e) => {
1533
+ if (e.key === "Escape") {
1534
+ onCancel();
1535
+ }
1536
+ },
1537
+ [onCancel]
1538
+ );
1539
+ useEffect4(() => {
1540
+ document.addEventListener("mousemove", handleMouseMove, true);
1541
+ document.addEventListener("click", handleClick, true);
1542
+ document.addEventListener("keydown", handleKeyDown);
1543
+ return () => {
1544
+ document.removeEventListener("mousemove", handleMouseMove, true);
1545
+ document.removeEventListener("click", handleClick, true);
1546
+ document.removeEventListener("keydown", handleKeyDown);
1547
+ };
1548
+ }, [handleMouseMove, handleClick, handleKeyDown]);
1549
+ return /* @__PURE__ */ jsxs4("div", { "data-afterbefore": "true", style: { position: "fixed", inset: 0, zIndex: 2147483646, pointerEvents: "none" }, children: [
1550
+ highlight && /* @__PURE__ */ jsxs4(Fragment2, { children: [
1551
+ /* @__PURE__ */ jsx4(
1552
+ "div",
1553
+ {
1554
+ style: {
1555
+ position: "fixed",
1556
+ left: highlight.x,
1557
+ top: highlight.y,
1558
+ width: highlight.width,
1559
+ height: highlight.height,
1560
+ background: "rgba(59, 130, 246, 0.15)",
1561
+ border: "2px solid rgba(59, 130, 246, 0.7)",
1562
+ borderRadius: 2,
1563
+ pointerEvents: "none"
1564
+ }
1565
+ }
1566
+ ),
1567
+ /* @__PURE__ */ jsx4(
1568
+ "div",
1569
+ {
1570
+ style: {
1571
+ position: "fixed",
1572
+ left: highlight.x,
1573
+ top: Math.max(0, highlight.y - 24),
1574
+ background: "rgba(59, 130, 246, 0.9)",
1575
+ color: "#fff",
1576
+ fontSize: 11,
1577
+ fontFamily: "system-ui, -apple-system, monospace",
1578
+ padding: "2px 6px",
1579
+ borderRadius: 3,
1580
+ pointerEvents: "none",
1581
+ whiteSpace: "nowrap",
1582
+ lineHeight: "18px"
1583
+ },
1584
+ children: highlight.tag
1585
+ }
1586
+ )
1587
+ ] }),
1588
+ !highlight && /* @__PURE__ */ jsx4(
1589
+ "div",
1590
+ {
1591
+ style: {
1592
+ position: "fixed",
1593
+ top: "50%",
1594
+ left: "50%",
1595
+ transform: "translate(-50%, -50%)",
1596
+ color: "rgba(255, 255, 255, 0.7)",
1597
+ fontSize: 14,
1598
+ fontFamily: "system-ui, -apple-system, sans-serif",
1599
+ pointerEvents: "none",
1600
+ textShadow: "0 1px 4px rgba(0, 0, 0, 0.5)",
1601
+ background: "rgba(0, 0, 0, 0.5)",
1602
+ padding: "8px 16px",
1603
+ borderRadius: 8
1604
+ },
1605
+ children: "Hover to inspect \xB7 Click to capture \xB7 Esc to cancel"
1606
+ }
1607
+ )
1608
+ ] });
1609
+ }
720
1610
 
721
1611
  // src/overlay/ui/status.tsx
722
- import { useState as useState4, useRef as useRef4, useEffect as useEffect4, useCallback as useCallback5 } from "react";
723
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1612
+ import { useState as useState5, useRef as useRef5, useEffect as useEffect5, useCallback as useCallback6 } from "react";
1613
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
724
1614
  var PANEL_WIDTH = 220;
725
1615
  function Status({ onReset, position, onClose }) {
726
- const panelRef = useRef4(null);
727
- const [toast, setToast] = useState4(null);
728
- const [pushing, setPushing] = useState4(false);
729
- const showToast = useCallback5((message, type) => {
1616
+ const panelRef = useRef5(null);
1617
+ const [toast, setToast] = useState5(null);
1618
+ const [pushing, setPushing] = useState5(false);
1619
+ const showToast = useCallback6((message, type) => {
730
1620
  setToast({ message, type });
731
1621
  setTimeout(() => setToast(null), 3e3);
732
1622
  }, []);
733
- useEffect4(() => {
1623
+ useEffect5(() => {
734
1624
  const handler = (e) => {
735
1625
  if (panelRef.current && !panelRef.current.contains(e.target)) {
736
1626
  onClose();
@@ -739,7 +1629,7 @@ function Status({ onReset, position, onClose }) {
739
1629
  document.addEventListener("mousedown", handler);
740
1630
  return () => document.removeEventListener("mousedown", handler);
741
1631
  }, [onClose]);
742
- useEffect4(() => {
1632
+ useEffect5(() => {
743
1633
  const handler = (e) => {
744
1634
  if (e.key === "Escape") onClose();
745
1635
  };
@@ -815,7 +1705,7 @@ function Status({ onReset, position, onClose }) {
815
1705
  const onLeave = (e) => {
816
1706
  e.currentTarget.style.background = "transparent";
817
1707
  };
818
- return /* @__PURE__ */ jsxs4(
1708
+ return /* @__PURE__ */ jsxs5(
819
1709
  "div",
820
1710
  {
821
1711
  ref: panelRef,
@@ -834,7 +1724,7 @@ function Status({ onReset, position, onClose }) {
834
1724
  overflow: "hidden"
835
1725
  },
836
1726
  children: [
837
- /* @__PURE__ */ jsx4(
1727
+ /* @__PURE__ */ jsx5(
838
1728
  "div",
839
1729
  {
840
1730
  style: {
@@ -847,8 +1737,8 @@ function Status({ onReset, position, onClose }) {
847
1737
  children: "Before & After captured"
848
1738
  }
849
1739
  ),
850
- /* @__PURE__ */ jsxs4("div", { style: { padding: "0 4px 4px" }, children: [
851
- /* @__PURE__ */ jsxs4(
1740
+ /* @__PURE__ */ jsxs5("div", { style: { padding: "0 4px 4px" }, children: [
1741
+ /* @__PURE__ */ jsxs5(
852
1742
  "button",
853
1743
  {
854
1744
  style: buttonStyle,
@@ -856,12 +1746,12 @@ function Status({ onReset, position, onClose }) {
856
1746
  onMouseEnter: onEnter,
857
1747
  onMouseLeave: onLeave,
858
1748
  children: [
859
- /* @__PURE__ */ jsx4(FolderIcon, {}),
1749
+ /* @__PURE__ */ jsx5(FolderIcon, {}),
860
1750
  "Open Folder"
861
1751
  ]
862
1752
  }
863
1753
  ),
864
- /* @__PURE__ */ jsxs4(
1754
+ /* @__PURE__ */ jsxs5(
865
1755
  "button",
866
1756
  {
867
1757
  style: buttonStyle,
@@ -869,12 +1759,12 @@ function Status({ onReset, position, onClose }) {
869
1759
  onMouseEnter: onEnter,
870
1760
  onMouseLeave: onLeave,
871
1761
  children: [
872
- /* @__PURE__ */ jsx4(CopyIcon, {}),
1762
+ /* @__PURE__ */ jsx5(CopyIcon, {}),
873
1763
  "Copy Markdown"
874
1764
  ]
875
1765
  }
876
1766
  ),
877
- /* @__PURE__ */ jsxs4(
1767
+ /* @__PURE__ */ jsxs5(
878
1768
  "button",
879
1769
  {
880
1770
  style: buttonStyle,
@@ -883,12 +1773,12 @@ function Status({ onReset, position, onClose }) {
883
1773
  onMouseEnter: onEnter,
884
1774
  onMouseLeave: onLeave,
885
1775
  children: [
886
- /* @__PURE__ */ jsx4(PushIcon, {}),
1776
+ /* @__PURE__ */ jsx5(PushIcon, {}),
887
1777
  pushing ? "Pushing..." : "Push to PR"
888
1778
  ]
889
1779
  }
890
1780
  ),
891
- /* @__PURE__ */ jsx4(
1781
+ /* @__PURE__ */ jsx5(
892
1782
  "div",
893
1783
  {
894
1784
  style: {
@@ -898,7 +1788,7 @@ function Status({ onReset, position, onClose }) {
898
1788
  }
899
1789
  }
900
1790
  ),
901
- /* @__PURE__ */ jsxs4(
1791
+ /* @__PURE__ */ jsxs5(
902
1792
  "button",
903
1793
  {
904
1794
  style: { ...buttonStyle, color: "rgba(255,255,255,0.5)" },
@@ -906,13 +1796,13 @@ function Status({ onReset, position, onClose }) {
906
1796
  onMouseEnter: onEnter,
907
1797
  onMouseLeave: onLeave,
908
1798
  children: [
909
- /* @__PURE__ */ jsx4(ResetIcon, {}),
1799
+ /* @__PURE__ */ jsx5(ResetIcon, {}),
910
1800
  "Reset"
911
1801
  ]
912
1802
  }
913
1803
  )
914
1804
  ] }),
915
- toast && /* @__PURE__ */ jsx4(
1805
+ toast && /* @__PURE__ */ jsx5(
916
1806
  "div",
917
1807
  {
918
1808
  style: {
@@ -938,14 +1828,14 @@ function Status({ onReset, position, onClose }) {
938
1828
  );
939
1829
  }
940
1830
  function FolderIcon() {
941
- return /* @__PURE__ */ jsx4(
1831
+ return /* @__PURE__ */ jsx5(
942
1832
  "svg",
943
1833
  {
944
1834
  width: "14",
945
1835
  height: "14",
946
1836
  viewBox: "0 0 14 14",
947
1837
  style: { color: "rgba(255,255,255,0.5)" },
948
- children: /* @__PURE__ */ jsx4(
1838
+ children: /* @__PURE__ */ jsx5(
949
1839
  "path",
950
1840
  {
951
1841
  d: "M1.5 3A1.5 1.5 0 013 1.5h2.38a1 1 0 01.72.3L7 2.72a1 1 0 00.72.3H11A1.5 1.5 0 0112.5 4.5v6A1.5 1.5 0 0111 12H3A1.5 1.5 0 011.5 10.5V3z",
@@ -958,7 +1848,7 @@ function FolderIcon() {
958
1848
  );
959
1849
  }
960
1850
  function CopyIcon() {
961
- return /* @__PURE__ */ jsxs4(
1851
+ return /* @__PURE__ */ jsxs5(
962
1852
  "svg",
963
1853
  {
964
1854
  width: "14",
@@ -966,7 +1856,7 @@ function CopyIcon() {
966
1856
  viewBox: "0 0 14 14",
967
1857
  style: { color: "rgba(255,255,255,0.5)" },
968
1858
  children: [
969
- /* @__PURE__ */ jsx4(
1859
+ /* @__PURE__ */ jsx5(
970
1860
  "rect",
971
1861
  {
972
1862
  x: "4",
@@ -979,7 +1869,7 @@ function CopyIcon() {
979
1869
  strokeWidth: "1.3"
980
1870
  }
981
1871
  ),
982
- /* @__PURE__ */ jsx4(
1872
+ /* @__PURE__ */ jsx5(
983
1873
  "path",
984
1874
  {
985
1875
  d: "M10 4V2.5A1.5 1.5 0 008.5 1h-6A1.5 1.5 0 001 2.5v6A1.5 1.5 0 002.5 10H4",
@@ -993,14 +1883,14 @@ function CopyIcon() {
993
1883
  );
994
1884
  }
995
1885
  function PushIcon() {
996
- return /* @__PURE__ */ jsx4(
1886
+ return /* @__PURE__ */ jsx5(
997
1887
  "svg",
998
1888
  {
999
1889
  width: "14",
1000
1890
  height: "14",
1001
1891
  viewBox: "0 0 14 14",
1002
1892
  style: { color: "rgba(255,255,255,0.5)" },
1003
- children: /* @__PURE__ */ jsx4(
1893
+ children: /* @__PURE__ */ jsx5(
1004
1894
  "path",
1005
1895
  {
1006
1896
  d: "M7 11V3m0 0L4 6m3-3l3 3",
@@ -1015,7 +1905,7 @@ function PushIcon() {
1015
1905
  );
1016
1906
  }
1017
1907
  function ResetIcon() {
1018
- return /* @__PURE__ */ jsxs4(
1908
+ return /* @__PURE__ */ jsxs5(
1019
1909
  "svg",
1020
1910
  {
1021
1911
  width: "14",
@@ -1023,7 +1913,7 @@ function ResetIcon() {
1023
1913
  viewBox: "0 0 14 14",
1024
1914
  style: { color: "rgba(255,255,255,0.4)" },
1025
1915
  children: [
1026
- /* @__PURE__ */ jsx4(
1916
+ /* @__PURE__ */ jsx5(
1027
1917
  "path",
1028
1918
  {
1029
1919
  d: "M2.5 7a4.5 4.5 0 118 2.5",
@@ -1033,7 +1923,7 @@ function ResetIcon() {
1033
1923
  strokeLinecap: "round"
1034
1924
  }
1035
1925
  ),
1036
- /* @__PURE__ */ jsx4(
1926
+ /* @__PURE__ */ jsx5(
1037
1927
  "path",
1038
1928
  {
1039
1929
  d: "M2.5 3v4h4",
@@ -1050,7 +1940,7 @@ function ResetIcon() {
1050
1940
  }
1051
1941
 
1052
1942
  // src/overlay/index.tsx
1053
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1943
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1054
1944
  async function saveCapture(type, mode, dataUrl) {
1055
1945
  try {
1056
1946
  const res = await fetch("/__afterbefore/save", {
@@ -1068,18 +1958,29 @@ async function saveCapture(type, mode, dataUrl) {
1068
1958
  }
1069
1959
  function AfterBefore() {
1070
1960
  const { state, captureComplete, reset } = useOverlayState();
1071
- const [menuOpen, setMenuOpen] = useState5(false);
1072
- const [statusOpen, setStatusOpen] = useState5(false);
1073
- const [selectorActive, setSelectorActive] = useState5(false);
1074
- const [loading, setLoading] = useState5(false);
1075
- const iconPos = useRef5({ x: 24, y: 0 });
1076
- const handlePositionChange = useCallback6(
1961
+ const [menuOpen, setMenuOpen] = useState6(false);
1962
+ const [statusOpen, setStatusOpen] = useState6(false);
1963
+ const [selectorActive, setSelectorActive] = useState6(false);
1964
+ const [inspectorActive, setInspectorActive] = useState6(false);
1965
+ const [loading, setLoading] = useState6(false);
1966
+ const iconPos = useRef6({ x: 24, y: 0 });
1967
+ useEffect6(() => {
1968
+ if (state.phase === "ready") {
1969
+ const timer = setTimeout(() => {
1970
+ reset();
1971
+ setStatusOpen(false);
1972
+ setMenuOpen(false);
1973
+ }, 1500);
1974
+ return () => clearTimeout(timer);
1975
+ }
1976
+ }, [state.phase, reset]);
1977
+ const handlePositionChange = useCallback7(
1077
1978
  (pos) => {
1078
1979
  iconPos.current = pos;
1079
1980
  },
1080
1981
  []
1081
1982
  );
1082
- const handleIconClick = useCallback6(() => {
1983
+ const handleIconClick = useCallback7(() => {
1083
1984
  if (loading) return;
1084
1985
  if (state.phase === "ready") {
1085
1986
  setStatusOpen((prev) => !prev);
@@ -1089,11 +1990,11 @@ function AfterBefore() {
1089
1990
  setStatusOpen(false);
1090
1991
  }
1091
1992
  }, [state.phase, loading]);
1092
- const performCapture = useCallback6(
1093
- async (mode, area) => {
1993
+ const performCapture = useCallback7(
1994
+ async (mode, area, element) => {
1094
1995
  setLoading(true);
1095
1996
  try {
1096
- const dataUrl = await capture({ mode, area });
1997
+ const dataUrl = await capture({ mode, area, element });
1097
1998
  const type = state.phase === "idle" ? "before" : "after";
1098
1999
  await saveCapture(type, mode, dataUrl);
1099
2000
  captureComplete({
@@ -1109,40 +2010,52 @@ function AfterBefore() {
1109
2010
  },
1110
2011
  [state.phase, captureComplete]
1111
2012
  );
1112
- const handleModeSelect = useCallback6(
2013
+ const handleModeSelect = useCallback7(
1113
2014
  (mode) => {
1114
2015
  setMenuOpen(false);
1115
2016
  if (mode === "area") {
1116
2017
  setSelectorActive(true);
2018
+ } else if (mode === "component") {
2019
+ setInspectorActive(true);
1117
2020
  } else {
1118
2021
  performCapture(mode);
1119
2022
  }
1120
2023
  },
1121
2024
  [performCapture]
1122
2025
  );
1123
- const handleAreaSelect = useCallback6(
2026
+ const handleComponentSelect = useCallback7(
2027
+ (element) => {
2028
+ setInspectorActive(false);
2029
+ performCapture("component", void 0, element);
2030
+ },
2031
+ [performCapture]
2032
+ );
2033
+ const handleComponentCancel = useCallback7(() => {
2034
+ setInspectorActive(false);
2035
+ }, []);
2036
+ const handleAreaSelect = useCallback7(
1124
2037
  (area) => {
1125
2038
  setSelectorActive(false);
1126
2039
  performCapture("area", area);
1127
2040
  },
1128
2041
  [performCapture]
1129
2042
  );
1130
- const handleAreaCancel = useCallback6(() => {
2043
+ const handleAreaCancel = useCallback7(() => {
1131
2044
  setSelectorActive(false);
1132
2045
  }, []);
1133
- const handleReset = useCallback6(() => {
2046
+ const handleReset = useCallback7(() => {
1134
2047
  reset();
1135
2048
  setStatusOpen(false);
1136
2049
  setMenuOpen(false);
1137
2050
  }, [reset]);
1138
- const handleMenuClose = useCallback6(() => {
2051
+ const handleMenuClose = useCallback7(() => {
1139
2052
  setMenuOpen(false);
1140
2053
  }, []);
1141
- const handleStatusClose = useCallback6(() => {
2054
+ const handleStatusClose = useCallback7(() => {
1142
2055
  setStatusOpen(false);
1143
2056
  }, []);
1144
- return /* @__PURE__ */ jsxs5("div", { "data-afterbefore": "true", children: [
1145
- /* @__PURE__ */ jsx5(
2057
+ return /* @__PURE__ */ jsxs6("div", { "data-afterbefore": "true", children: [
2058
+ /* @__PURE__ */ jsx6(
1146
2059
  Icon,
1147
2060
  {
1148
2061
  phase: state.phase,
@@ -1151,7 +2064,7 @@ function AfterBefore() {
1151
2064
  onPositionChange: handlePositionChange
1152
2065
  }
1153
2066
  ),
1154
- menuOpen && (state.phase === "idle" || state.phase === "captured-before") && /* @__PURE__ */ jsx5(
2067
+ menuOpen && (state.phase === "idle" || state.phase === "captured-before") && /* @__PURE__ */ jsx6(
1155
2068
  Menu,
1156
2069
  {
1157
2070
  onSelect: handleModeSelect,
@@ -1159,8 +2072,9 @@ function AfterBefore() {
1159
2072
  position: iconPos.current
1160
2073
  }
1161
2074
  ),
1162
- selectorActive && /* @__PURE__ */ jsx5(Selector, { onSelect: handleAreaSelect, onCancel: handleAreaCancel }),
1163
- statusOpen && state.phase === "ready" && /* @__PURE__ */ jsx5(
2075
+ selectorActive && /* @__PURE__ */ jsx6(Selector, { onSelect: handleAreaSelect, onCancel: handleAreaCancel }),
2076
+ inspectorActive && /* @__PURE__ */ jsx6(Inspector, { onSelect: handleComponentSelect, onCancel: handleComponentCancel }),
2077
+ statusOpen && state.phase === "ready" && /* @__PURE__ */ jsx6(
1164
2078
  Status,
1165
2079
  {
1166
2080
  onReset: handleReset,