afterbefore 0.1.18 → 0.2.0

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.
@@ -0,0 +1,1176 @@
1
+ "use client";
2
+
3
+ // src/overlay/index.tsx
4
+ import { useState as useState5, useCallback as useCallback6, useRef as useRef5 } from "react";
5
+
6
+ // src/overlay/state.ts
7
+ import { useState, useCallback } from "react";
8
+ var initialState = {
9
+ phase: "idle",
10
+ before: null,
11
+ after: null
12
+ };
13
+ function useOverlayState() {
14
+ const [state, setState] = useState(initialState);
15
+ const captureComplete = useCallback(
16
+ (result) => {
17
+ setState((prev) => {
18
+ if (prev.phase === "idle") {
19
+ return { ...prev, phase: "captured-before", before: result };
20
+ }
21
+ if (prev.phase === "captured-before") {
22
+ return { ...prev, phase: "ready", after: result };
23
+ }
24
+ return prev;
25
+ });
26
+ },
27
+ []
28
+ );
29
+ const reset = useCallback(() => {
30
+ setState(initialState);
31
+ }, []);
32
+ return { state, captureComplete, reset };
33
+ }
34
+
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();
41
+ }
42
+ if (mode === "fullpage") {
43
+ return captureFullPage();
44
+ }
45
+ if (mode === "area" && area) {
46
+ return captureArea(area);
47
+ }
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
+ }
61
+ async function captureFullPage() {
62
+ const scrollY = window.scrollY;
63
+ const body = document.body;
64
+ const html = document.documentElement;
65
+ const fullHeight = Math.max(
66
+ body.scrollHeight,
67
+ body.offsetHeight,
68
+ html.clientHeight,
69
+ html.scrollHeight,
70
+ html.offsetHeight
71
+ );
72
+ const dataUrl = await toPng(document.documentElement, {
73
+ width: window.innerWidth,
74
+ height: fullHeight,
75
+ style: {
76
+ overflow: "visible",
77
+ height: `${fullHeight}px`
78
+ },
79
+ filter: filterOverlay
80
+ });
81
+ window.scrollTo(0, scrollY);
82
+ return dataUrl;
83
+ }
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
+
117
+ // src/overlay/ui/icon.tsx
118
+ import { useRef, useCallback as useCallback2, useEffect, useState as useState2 } from "react";
119
+ import { jsx, jsxs } from "react/jsx-runtime";
120
+ var ICON_SIZE = 40;
121
+ var EDGE_MARGIN = 24;
122
+ function Icon({ phase, onClick, loading, onPositionChange }) {
123
+ const ref = useRef(null);
124
+ const [pos, setPos] = useState2({ x: EDGE_MARGIN, y: -1 });
125
+ const dragState = useRef(null);
126
+ useEffect(() => {
127
+ setPos((prev) => {
128
+ if (prev.y === -1) {
129
+ const y = window.innerHeight - ICON_SIZE - EDGE_MARGIN;
130
+ return { x: prev.x, y };
131
+ }
132
+ return prev;
133
+ });
134
+ }, []);
135
+ useEffect(() => {
136
+ if (pos.y !== -1) {
137
+ onPositionChange?.({ x: pos.x, y: pos.y });
138
+ }
139
+ }, [pos, onPositionChange]);
140
+ const handleMouseDown = useCallback2(
141
+ (e) => {
142
+ e.preventDefault();
143
+ dragState.current = {
144
+ dragging: true,
145
+ startX: e.clientX,
146
+ startY: e.clientY,
147
+ origX: pos.x,
148
+ origY: pos.y,
149
+ distance: 0
150
+ };
151
+ },
152
+ [pos]
153
+ );
154
+ useEffect(() => {
155
+ const handleMouseMove = (e) => {
156
+ const ds = dragState.current;
157
+ if (!ds || !ds.dragging) return;
158
+ const dx = e.clientX - ds.startX;
159
+ const dy = e.clientY - ds.startY;
160
+ ds.distance = Math.sqrt(dx * dx + dy * dy);
161
+ const newX = Math.max(
162
+ 0,
163
+ Math.min(window.innerWidth - ICON_SIZE, ds.origX + dx)
164
+ );
165
+ const newY = Math.max(
166
+ 0,
167
+ Math.min(window.innerHeight - ICON_SIZE, ds.origY + dy)
168
+ );
169
+ setPos({ x: newX, y: newY });
170
+ };
171
+ const handleMouseUp = () => {
172
+ const ds = dragState.current;
173
+ if (!ds) return;
174
+ if (ds.distance < 5) {
175
+ onClick();
176
+ }
177
+ dragState.current = null;
178
+ };
179
+ window.addEventListener("mousemove", handleMouseMove);
180
+ window.addEventListener("mouseup", handleMouseUp);
181
+ return () => {
182
+ window.removeEventListener("mousemove", handleMouseMove);
183
+ window.removeEventListener("mouseup", handleMouseUp);
184
+ };
185
+ }, [onClick]);
186
+ if (pos.y === -1) return null;
187
+ return /* @__PURE__ */ jsxs(
188
+ "div",
189
+ {
190
+ ref,
191
+ "data-afterbefore": "true",
192
+ onMouseDown: handleMouseDown,
193
+ style: {
194
+ position: "fixed",
195
+ left: pos.x,
196
+ top: pos.y,
197
+ width: ICON_SIZE,
198
+ height: ICON_SIZE,
199
+ borderRadius: "50%",
200
+ background: "rgba(30, 30, 30, 0.85)",
201
+ display: "flex",
202
+ alignItems: "center",
203
+ justifyContent: "center",
204
+ cursor: "grab",
205
+ zIndex: 2147483647,
206
+ boxShadow: "0 2px 8px rgba(0,0,0,0.3)",
207
+ transition: "background 0.15s",
208
+ userSelect: "none"
209
+ },
210
+ onMouseEnter: (e) => {
211
+ e.currentTarget.style.background = "rgba(30, 30, 30, 0.95)";
212
+ },
213
+ onMouseLeave: (e) => {
214
+ e.currentTarget.style.background = "rgba(30, 30, 30, 0.85)";
215
+ },
216
+ children: [
217
+ /* @__PURE__ */ jsx(
218
+ "style",
219
+ {
220
+ dangerouslySetInnerHTML: {
221
+ __html: `
222
+ @keyframes ab-pulse {
223
+ 0%, 100% { transform: scale(1); opacity: 1; }
224
+ 50% { transform: scale(1.08); opacity: 0.85; }
225
+ }
226
+ @keyframes ab-spin {
227
+ 0% { transform: rotate(0deg); }
228
+ 100% { transform: rotate(360deg); }
229
+ }`
230
+ }
231
+ }
232
+ ),
233
+ loading ? /* @__PURE__ */ jsx(
234
+ "svg",
235
+ {
236
+ width: "20",
237
+ height: "20",
238
+ viewBox: "0 0 20 20",
239
+ style: { animation: "ab-spin 0.8s linear infinite" },
240
+ children: /* @__PURE__ */ jsx(
241
+ "circle",
242
+ {
243
+ cx: "10",
244
+ cy: "10",
245
+ r: "8",
246
+ fill: "none",
247
+ stroke: "white",
248
+ strokeWidth: "2",
249
+ strokeDasharray: "40",
250
+ strokeDashoffset: "10",
251
+ strokeLinecap: "round"
252
+ }
253
+ )
254
+ }
255
+ ) : phase === "ready" ? /* @__PURE__ */ jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx(
256
+ "path",
257
+ {
258
+ d: "M4 10l4 4 8-8",
259
+ fill: "none",
260
+ stroke: "#4ade80",
261
+ strokeWidth: "2.5",
262
+ strokeLinecap: "round",
263
+ strokeLinejoin: "round"
264
+ }
265
+ ) }) : /* @__PURE__ */ jsxs(
266
+ "div",
267
+ {
268
+ style: {
269
+ position: "relative",
270
+ display: "flex",
271
+ alignItems: "center",
272
+ justifyContent: "center",
273
+ animation: phase === "captured-before" ? "ab-pulse 2s ease-in-out infinite" : "none"
274
+ },
275
+ children: [
276
+ /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 20 20", children: [
277
+ /* @__PURE__ */ jsx(
278
+ "rect",
279
+ {
280
+ x: "2",
281
+ y: "5",
282
+ width: "16",
283
+ height: "12",
284
+ rx: "2",
285
+ fill: "none",
286
+ stroke: "white",
287
+ strokeWidth: "1.5"
288
+ }
289
+ ),
290
+ /* @__PURE__ */ jsx(
291
+ "circle",
292
+ {
293
+ cx: "10",
294
+ cy: "11",
295
+ r: "3",
296
+ fill: "none",
297
+ stroke: "white",
298
+ strokeWidth: "1.5"
299
+ }
300
+ ),
301
+ /* @__PURE__ */ jsx("path", { d: "M7 5l1-2h4l1 2", fill: "none", stroke: "white", strokeWidth: "1.5" })
302
+ ] }),
303
+ phase === "captured-before" && /* @__PURE__ */ jsx(
304
+ "div",
305
+ {
306
+ style: {
307
+ position: "absolute",
308
+ top: -6,
309
+ right: -8,
310
+ width: 14,
311
+ height: 14,
312
+ borderRadius: "50%",
313
+ background: "#3b82f6",
314
+ color: "white",
315
+ fontSize: "9px",
316
+ fontWeight: 700,
317
+ display: "flex",
318
+ alignItems: "center",
319
+ justifyContent: "center",
320
+ lineHeight: 1,
321
+ fontFamily: "system-ui, sans-serif"
322
+ },
323
+ children: "1"
324
+ }
325
+ )
326
+ ]
327
+ }
328
+ )
329
+ ]
330
+ }
331
+ );
332
+ }
333
+
334
+ // src/overlay/ui/menu.tsx
335
+ import { useEffect as useEffect2, useRef as useRef2, useCallback as useCallback3 } from "react";
336
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
337
+ var modes = [
338
+ {
339
+ mode: "viewport",
340
+ label: "Viewport",
341
+ icon: /* @__PURE__ */ jsxs2("svg", { width: "16", height: "16", viewBox: "0 0 16 16", children: [
342
+ /* @__PURE__ */ jsx2(
343
+ "rect",
344
+ {
345
+ x: "1",
346
+ y: "2",
347
+ width: "14",
348
+ height: "11",
349
+ rx: "1.5",
350
+ fill: "none",
351
+ stroke: "currentColor",
352
+ strokeWidth: "1.5"
353
+ }
354
+ ),
355
+ /* @__PURE__ */ jsx2(
356
+ "line",
357
+ {
358
+ x1: "1",
359
+ y1: "14",
360
+ x2: "15",
361
+ y2: "14",
362
+ stroke: "currentColor",
363
+ strokeWidth: "1.5",
364
+ strokeLinecap: "round"
365
+ }
366
+ )
367
+ ] })
368
+ },
369
+ {
370
+ mode: "fullpage",
371
+ label: "Full Page",
372
+ icon: /* @__PURE__ */ jsxs2("svg", { width: "16", height: "16", viewBox: "0 0 16 16", children: [
373
+ /* @__PURE__ */ jsx2(
374
+ "rect",
375
+ {
376
+ x: "3",
377
+ y: "1",
378
+ width: "10",
379
+ height: "14",
380
+ rx: "1.5",
381
+ fill: "none",
382
+ stroke: "currentColor",
383
+ strokeWidth: "1.5"
384
+ }
385
+ ),
386
+ /* @__PURE__ */ jsx2(
387
+ "line",
388
+ {
389
+ x1: "5.5",
390
+ y1: "4",
391
+ x2: "10.5",
392
+ y2: "4",
393
+ stroke: "currentColor",
394
+ strokeWidth: "1",
395
+ strokeLinecap: "round"
396
+ }
397
+ ),
398
+ /* @__PURE__ */ jsx2(
399
+ "line",
400
+ {
401
+ x1: "5.5",
402
+ y1: "6.5",
403
+ x2: "10.5",
404
+ y2: "6.5",
405
+ stroke: "currentColor",
406
+ strokeWidth: "1",
407
+ strokeLinecap: "round"
408
+ }
409
+ ),
410
+ /* @__PURE__ */ jsx2(
411
+ "line",
412
+ {
413
+ x1: "5.5",
414
+ y1: "9",
415
+ x2: "10.5",
416
+ y2: "9",
417
+ stroke: "currentColor",
418
+ strokeWidth: "1",
419
+ strokeLinecap: "round"
420
+ }
421
+ )
422
+ ] })
423
+ },
424
+ {
425
+ mode: "area",
426
+ label: "Select Area",
427
+ icon: /* @__PURE__ */ jsxs2("svg", { width: "16", height: "16", viewBox: "0 0 16 16", children: [
428
+ /* @__PURE__ */ jsx2(
429
+ "path",
430
+ {
431
+ d: "M1 5V2.5A1.5 1.5 0 012.5 1H5",
432
+ fill: "none",
433
+ stroke: "currentColor",
434
+ strokeWidth: "1.5",
435
+ strokeLinecap: "round"
436
+ }
437
+ ),
438
+ /* @__PURE__ */ jsx2(
439
+ "path",
440
+ {
441
+ d: "M11 1h2.5A1.5 1.5 0 0115 2.5V5",
442
+ fill: "none",
443
+ stroke: "currentColor",
444
+ strokeWidth: "1.5",
445
+ strokeLinecap: "round"
446
+ }
447
+ ),
448
+ /* @__PURE__ */ jsx2(
449
+ "path",
450
+ {
451
+ d: "M15 11v2.5a1.5 1.5 0 01-1.5 1.5H11",
452
+ fill: "none",
453
+ stroke: "currentColor",
454
+ strokeWidth: "1.5",
455
+ strokeLinecap: "round"
456
+ }
457
+ ),
458
+ /* @__PURE__ */ jsx2(
459
+ "path",
460
+ {
461
+ d: "M5 15H2.5A1.5 1.5 0 011 13.5V11",
462
+ fill: "none",
463
+ stroke: "currentColor",
464
+ strokeWidth: "1.5",
465
+ strokeLinecap: "round"
466
+ }
467
+ )
468
+ ] })
469
+ }
470
+ ];
471
+ var MENU_WIDTH = 160;
472
+ function Menu({ onSelect, onClose, position }) {
473
+ const menuRef = useRef2(null);
474
+ const handleClickOutside = useCallback3(
475
+ (e) => {
476
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
477
+ onClose();
478
+ }
479
+ },
480
+ [onClose]
481
+ );
482
+ const handleKeyDown = useCallback3(
483
+ (e) => {
484
+ if (e.key === "Escape") {
485
+ onClose();
486
+ }
487
+ },
488
+ [onClose]
489
+ );
490
+ useEffect2(() => {
491
+ document.addEventListener("mousedown", handleClickOutside);
492
+ document.addEventListener("keydown", handleKeyDown);
493
+ return () => {
494
+ document.removeEventListener("mousedown", handleClickOutside);
495
+ document.removeEventListener("keydown", handleKeyDown);
496
+ };
497
+ }, [handleClickOutside, handleKeyDown]);
498
+ const menuLeft = Math.max(
499
+ 8,
500
+ Math.min(position.x - MENU_WIDTH / 2 + 20, window.innerWidth - MENU_WIDTH - 8)
501
+ );
502
+ const menuBottom = window.innerHeight - position.y + 8;
503
+ return /* @__PURE__ */ jsx2(
504
+ "div",
505
+ {
506
+ ref: menuRef,
507
+ "data-afterbefore": "true",
508
+ style: {
509
+ position: "fixed",
510
+ left: menuLeft,
511
+ bottom: menuBottom,
512
+ width: MENU_WIDTH,
513
+ background: "rgba(24, 24, 27, 0.95)",
514
+ borderRadius: 10,
515
+ padding: 4,
516
+ boxShadow: "0 4px 20px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.08)",
517
+ zIndex: 2147483647,
518
+ fontFamily: "system-ui, -apple-system, sans-serif",
519
+ backdropFilter: "blur(12px)"
520
+ },
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";
545
+ },
546
+ children: [
547
+ /* @__PURE__ */ jsx2(
548
+ "span",
549
+ {
550
+ style: {
551
+ display: "flex",
552
+ alignItems: "center",
553
+ color: "rgba(255,255,255,0.6)"
554
+ },
555
+ children: icon
556
+ }
557
+ ),
558
+ label
559
+ ]
560
+ },
561
+ mode
562
+ ))
563
+ }
564
+ );
565
+ }
566
+
567
+ // src/overlay/ui/selector.tsx
568
+ import { useState as useState3, useRef as useRef3, useCallback as useCallback4, useEffect as useEffect3 } from "react";
569
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
570
+ var MIN_SIZE = 10;
571
+ function Selector({ onSelect, onCancel }) {
572
+ const [selection, setSelection] = useState3(null);
573
+ const dragging = useRef3(false);
574
+ const handleKeyDown = useCallback4(
575
+ (e) => {
576
+ if (e.key === "Escape") {
577
+ onCancel();
578
+ }
579
+ },
580
+ [onCancel]
581
+ );
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);
614
+ }
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;
622
+ return /* @__PURE__ */ jsxs3(
623
+ "div",
624
+ {
625
+ "data-afterbefore": "true",
626
+ onMouseDown: handleMouseDown,
627
+ onMouseMove: handleMouseMove,
628
+ onMouseUp: handleMouseUp,
629
+ style: {
630
+ position: "fixed",
631
+ inset: 0,
632
+ zIndex: 2147483647,
633
+ cursor: "crosshair"
634
+ },
635
+ children: [
636
+ /* @__PURE__ */ jsx3(
637
+ "div",
638
+ {
639
+ style: {
640
+ position: "absolute",
641
+ inset: 0,
642
+ background: "rgba(0, 0, 0, 0.4)",
643
+ pointerEvents: "none",
644
+ ...rect && rect.w > 0 && rect.h > 0 ? {
645
+ clipPath: `polygon(
646
+ 0% 0%, 0% 100%, 100% 100%, 100% 0%, 0% 0%,
647
+ ${rect.x}px ${rect.y}px,
648
+ ${rect.x}px ${rect.y + rect.h}px,
649
+ ${rect.x + rect.w}px ${rect.y + rect.h}px,
650
+ ${rect.x + rect.w}px ${rect.y}px,
651
+ ${rect.x}px ${rect.y}px
652
+ )`
653
+ } : {}
654
+ }
655
+ }
656
+ ),
657
+ rect && rect.w > 0 && rect.h > 0 && /* @__PURE__ */ jsxs3(Fragment, { children: [
658
+ /* @__PURE__ */ jsx3(
659
+ "div",
660
+ {
661
+ style: {
662
+ position: "absolute",
663
+ left: rect.x,
664
+ top: rect.y,
665
+ width: rect.w,
666
+ 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)"
671
+ }
672
+ }
673
+ ),
674
+ /* @__PURE__ */ jsxs3(
675
+ "div",
676
+ {
677
+ style: {
678
+ position: "absolute",
679
+ left: rect.x + rect.w / 2,
680
+ top: rect.y + rect.h + 8,
681
+ transform: "translateX(-50%)",
682
+ background: "rgba(24, 24, 27, 0.9)",
683
+ color: "rgba(255,255,255,0.9)",
684
+ fontSize: 11,
685
+ fontFamily: "system-ui, -apple-system, monospace",
686
+ padding: "2px 8px",
687
+ borderRadius: 4,
688
+ whiteSpace: "nowrap",
689
+ pointerEvents: "none"
690
+ },
691
+ children: [
692
+ Math.round(rect.w),
693
+ " \xD7 ",
694
+ Math.round(rect.h)
695
+ ]
696
+ }
697
+ )
698
+ ] }),
699
+ !selection && /* @__PURE__ */ jsx3(
700
+ "div",
701
+ {
702
+ style: {
703
+ position: "absolute",
704
+ top: "50%",
705
+ left: "50%",
706
+ transform: "translate(-50%, -50%)",
707
+ color: "rgba(255,255,255,0.7)",
708
+ fontSize: 14,
709
+ fontFamily: "system-ui, -apple-system, sans-serif",
710
+ pointerEvents: "none",
711
+ textShadow: "0 1px 4px rgba(0,0,0,0.5)"
712
+ },
713
+ children: "Drag to select an area \xB7 Esc to cancel"
714
+ }
715
+ )
716
+ ]
717
+ }
718
+ );
719
+ }
720
+
721
+ // 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";
724
+ var PANEL_WIDTH = 220;
725
+ 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) => {
730
+ setToast({ message, type });
731
+ setTimeout(() => setToast(null), 3e3);
732
+ }, []);
733
+ useEffect4(() => {
734
+ const handler = (e) => {
735
+ if (panelRef.current && !panelRef.current.contains(e.target)) {
736
+ onClose();
737
+ }
738
+ };
739
+ document.addEventListener("mousedown", handler);
740
+ return () => document.removeEventListener("mousedown", handler);
741
+ }, [onClose]);
742
+ useEffect4(() => {
743
+ const handler = (e) => {
744
+ if (e.key === "Escape") onClose();
745
+ };
746
+ document.addEventListener("keydown", handler);
747
+ return () => document.removeEventListener("keydown", handler);
748
+ }, [onClose]);
749
+ const handleOpenFolder = async () => {
750
+ try {
751
+ const res = await fetch("/__afterbefore/open", { method: "POST" });
752
+ if (!res.ok) throw new Error();
753
+ showToast("Opened folder", "success");
754
+ } catch {
755
+ showToast("Could not open folder", "error");
756
+ }
757
+ };
758
+ const handleCopyMarkdown = async () => {
759
+ try {
760
+ const res = await fetch("/__afterbefore/markdown");
761
+ if (!res.ok) throw new Error();
762
+ const { markdown } = await res.json();
763
+ await navigator.clipboard.writeText(markdown);
764
+ showToast("Copied!", "success");
765
+ } catch {
766
+ showToast("Copy failed", "error");
767
+ }
768
+ };
769
+ const handlePush = async () => {
770
+ setPushing(true);
771
+ try {
772
+ const res = await fetch("/__afterbefore/push", { method: "POST" });
773
+ const data = await res.json();
774
+ if (!res.ok) {
775
+ showToast(data.error || "Push failed", "error");
776
+ } else if (data.pr) {
777
+ showToast(`Posted to PR #${data.pr}`, "success");
778
+ } else {
779
+ showToast("No PR found", "error");
780
+ }
781
+ } catch {
782
+ showToast("Push failed", "error");
783
+ } finally {
784
+ setPushing(false);
785
+ }
786
+ };
787
+ const handleReset = () => {
788
+ onReset();
789
+ onClose();
790
+ };
791
+ const panelLeft = Math.max(
792
+ 8,
793
+ Math.min(position.x - PANEL_WIDTH / 2 + 20, window.innerWidth - PANEL_WIDTH - 8)
794
+ );
795
+ const panelBottom = window.innerHeight - position.y + 8;
796
+ const buttonStyle = {
797
+ display: "flex",
798
+ alignItems: "center",
799
+ gap: 6,
800
+ width: "100%",
801
+ padding: "7px 10px",
802
+ border: "none",
803
+ background: "transparent",
804
+ color: "rgba(255,255,255,0.9)",
805
+ fontSize: 13,
806
+ borderRadius: 6,
807
+ cursor: "pointer",
808
+ textAlign: "left",
809
+ fontFamily: "system-ui, -apple-system, sans-serif",
810
+ transition: "background 0.1s"
811
+ };
812
+ const onEnter = (e) => {
813
+ e.currentTarget.style.background = "rgba(255,255,255,0.1)";
814
+ };
815
+ const onLeave = (e) => {
816
+ e.currentTarget.style.background = "transparent";
817
+ };
818
+ return /* @__PURE__ */ jsxs4(
819
+ "div",
820
+ {
821
+ ref: panelRef,
822
+ "data-afterbefore": "true",
823
+ style: {
824
+ position: "fixed",
825
+ left: panelLeft,
826
+ bottom: panelBottom,
827
+ width: PANEL_WIDTH,
828
+ background: "rgba(24, 24, 27, 0.95)",
829
+ borderRadius: 10,
830
+ boxShadow: "0 4px 20px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.08)",
831
+ zIndex: 2147483647,
832
+ fontFamily: "system-ui, -apple-system, sans-serif",
833
+ backdropFilter: "blur(12px)",
834
+ overflow: "hidden"
835
+ },
836
+ children: [
837
+ /* @__PURE__ */ jsx4(
838
+ "div",
839
+ {
840
+ style: {
841
+ padding: "10px 12px 6px",
842
+ fontSize: 12,
843
+ fontWeight: 600,
844
+ color: "rgba(255,255,255,0.5)",
845
+ letterSpacing: "0.02em"
846
+ },
847
+ children: "Before & After captured"
848
+ }
849
+ ),
850
+ /* @__PURE__ */ jsxs4("div", { style: { padding: "0 4px 4px" }, children: [
851
+ /* @__PURE__ */ jsxs4(
852
+ "button",
853
+ {
854
+ style: buttonStyle,
855
+ onClick: handleOpenFolder,
856
+ onMouseEnter: onEnter,
857
+ onMouseLeave: onLeave,
858
+ children: [
859
+ /* @__PURE__ */ jsx4(FolderIcon, {}),
860
+ "Open Folder"
861
+ ]
862
+ }
863
+ ),
864
+ /* @__PURE__ */ jsxs4(
865
+ "button",
866
+ {
867
+ style: buttonStyle,
868
+ onClick: handleCopyMarkdown,
869
+ onMouseEnter: onEnter,
870
+ onMouseLeave: onLeave,
871
+ children: [
872
+ /* @__PURE__ */ jsx4(CopyIcon, {}),
873
+ "Copy Markdown"
874
+ ]
875
+ }
876
+ ),
877
+ /* @__PURE__ */ jsxs4(
878
+ "button",
879
+ {
880
+ style: buttonStyle,
881
+ onClick: handlePush,
882
+ disabled: pushing,
883
+ onMouseEnter: onEnter,
884
+ onMouseLeave: onLeave,
885
+ children: [
886
+ /* @__PURE__ */ jsx4(PushIcon, {}),
887
+ pushing ? "Pushing..." : "Push to PR"
888
+ ]
889
+ }
890
+ ),
891
+ /* @__PURE__ */ jsx4(
892
+ "div",
893
+ {
894
+ style: {
895
+ height: 1,
896
+ background: "rgba(255,255,255,0.08)",
897
+ margin: "4px 6px"
898
+ }
899
+ }
900
+ ),
901
+ /* @__PURE__ */ jsxs4(
902
+ "button",
903
+ {
904
+ style: { ...buttonStyle, color: "rgba(255,255,255,0.5)" },
905
+ onClick: handleReset,
906
+ onMouseEnter: onEnter,
907
+ onMouseLeave: onLeave,
908
+ children: [
909
+ /* @__PURE__ */ jsx4(ResetIcon, {}),
910
+ "Reset"
911
+ ]
912
+ }
913
+ )
914
+ ] }),
915
+ toast && /* @__PURE__ */ jsx4(
916
+ "div",
917
+ {
918
+ style: {
919
+ position: "absolute",
920
+ bottom: "100%",
921
+ left: "50%",
922
+ transform: "translateX(-50%)",
923
+ marginBottom: 8,
924
+ padding: "6px 12px",
925
+ borderRadius: 6,
926
+ fontSize: 12,
927
+ fontWeight: 500,
928
+ whiteSpace: "nowrap",
929
+ color: "white",
930
+ background: toast.type === "success" ? "rgba(34, 197, 94, 0.9)" : "rgba(239, 68, 68, 0.9)",
931
+ boxShadow: "0 2px 8px rgba(0,0,0,0.3)"
932
+ },
933
+ children: toast.message
934
+ }
935
+ )
936
+ ]
937
+ }
938
+ );
939
+ }
940
+ function FolderIcon() {
941
+ return /* @__PURE__ */ jsx4(
942
+ "svg",
943
+ {
944
+ width: "14",
945
+ height: "14",
946
+ viewBox: "0 0 14 14",
947
+ style: { color: "rgba(255,255,255,0.5)" },
948
+ children: /* @__PURE__ */ jsx4(
949
+ "path",
950
+ {
951
+ 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",
952
+ fill: "none",
953
+ stroke: "currentColor",
954
+ strokeWidth: "1.3"
955
+ }
956
+ )
957
+ }
958
+ );
959
+ }
960
+ function CopyIcon() {
961
+ return /* @__PURE__ */ jsxs4(
962
+ "svg",
963
+ {
964
+ width: "14",
965
+ height: "14",
966
+ viewBox: "0 0 14 14",
967
+ style: { color: "rgba(255,255,255,0.5)" },
968
+ children: [
969
+ /* @__PURE__ */ jsx4(
970
+ "rect",
971
+ {
972
+ x: "4",
973
+ y: "4",
974
+ width: "8.5",
975
+ height: "8.5",
976
+ rx: "1.5",
977
+ fill: "none",
978
+ stroke: "currentColor",
979
+ strokeWidth: "1.3"
980
+ }
981
+ ),
982
+ /* @__PURE__ */ jsx4(
983
+ "path",
984
+ {
985
+ 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",
986
+ fill: "none",
987
+ stroke: "currentColor",
988
+ strokeWidth: "1.3"
989
+ }
990
+ )
991
+ ]
992
+ }
993
+ );
994
+ }
995
+ function PushIcon() {
996
+ return /* @__PURE__ */ jsx4(
997
+ "svg",
998
+ {
999
+ width: "14",
1000
+ height: "14",
1001
+ viewBox: "0 0 14 14",
1002
+ style: { color: "rgba(255,255,255,0.5)" },
1003
+ children: /* @__PURE__ */ jsx4(
1004
+ "path",
1005
+ {
1006
+ d: "M7 11V3m0 0L4 6m3-3l3 3",
1007
+ fill: "none",
1008
+ stroke: "currentColor",
1009
+ strokeWidth: "1.3",
1010
+ strokeLinecap: "round",
1011
+ strokeLinejoin: "round"
1012
+ }
1013
+ )
1014
+ }
1015
+ );
1016
+ }
1017
+ function ResetIcon() {
1018
+ return /* @__PURE__ */ jsxs4(
1019
+ "svg",
1020
+ {
1021
+ width: "14",
1022
+ height: "14",
1023
+ viewBox: "0 0 14 14",
1024
+ style: { color: "rgba(255,255,255,0.4)" },
1025
+ children: [
1026
+ /* @__PURE__ */ jsx4(
1027
+ "path",
1028
+ {
1029
+ d: "M2.5 7a4.5 4.5 0 118 2.5",
1030
+ fill: "none",
1031
+ stroke: "currentColor",
1032
+ strokeWidth: "1.3",
1033
+ strokeLinecap: "round"
1034
+ }
1035
+ ),
1036
+ /* @__PURE__ */ jsx4(
1037
+ "path",
1038
+ {
1039
+ d: "M2.5 3v4h4",
1040
+ fill: "none",
1041
+ stroke: "currentColor",
1042
+ strokeWidth: "1.3",
1043
+ strokeLinecap: "round",
1044
+ strokeLinejoin: "round"
1045
+ }
1046
+ )
1047
+ ]
1048
+ }
1049
+ );
1050
+ }
1051
+
1052
+ // src/overlay/index.tsx
1053
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1054
+ async function saveCapture(type, mode, dataUrl) {
1055
+ try {
1056
+ const res = await fetch("/__afterbefore/save", {
1057
+ method: "POST",
1058
+ headers: { "Content-Type": "application/json" },
1059
+ body: JSON.stringify({ type, mode, image: dataUrl })
1060
+ });
1061
+ if (!res.ok) throw new Error("Save failed");
1062
+ } catch {
1063
+ const link = document.createElement("a");
1064
+ link.download = `${type}.png`;
1065
+ link.href = dataUrl;
1066
+ link.click();
1067
+ }
1068
+ }
1069
+ function AfterBefore() {
1070
+ 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(
1077
+ (pos) => {
1078
+ iconPos.current = pos;
1079
+ },
1080
+ []
1081
+ );
1082
+ const handleIconClick = useCallback6(() => {
1083
+ if (loading) return;
1084
+ if (state.phase === "ready") {
1085
+ setStatusOpen((prev) => !prev);
1086
+ setMenuOpen(false);
1087
+ } else {
1088
+ setMenuOpen((prev) => !prev);
1089
+ setStatusOpen(false);
1090
+ }
1091
+ }, [state.phase, loading]);
1092
+ const performCapture = useCallback6(
1093
+ async (mode, area) => {
1094
+ setLoading(true);
1095
+ try {
1096
+ const dataUrl = await capture({ mode, area });
1097
+ const type = state.phase === "idle" ? "before" : "after";
1098
+ await saveCapture(type, mode, dataUrl);
1099
+ captureComplete({
1100
+ dataUrl,
1101
+ mode,
1102
+ timestamp: Date.now()
1103
+ });
1104
+ } catch (err) {
1105
+ console.error("[afterbefore] Capture failed:", err);
1106
+ } finally {
1107
+ setLoading(false);
1108
+ }
1109
+ },
1110
+ [state.phase, captureComplete]
1111
+ );
1112
+ const handleModeSelect = useCallback6(
1113
+ (mode) => {
1114
+ setMenuOpen(false);
1115
+ if (mode === "area") {
1116
+ setSelectorActive(true);
1117
+ } else {
1118
+ performCapture(mode);
1119
+ }
1120
+ },
1121
+ [performCapture]
1122
+ );
1123
+ const handleAreaSelect = useCallback6(
1124
+ (area) => {
1125
+ setSelectorActive(false);
1126
+ performCapture("area", area);
1127
+ },
1128
+ [performCapture]
1129
+ );
1130
+ const handleAreaCancel = useCallback6(() => {
1131
+ setSelectorActive(false);
1132
+ }, []);
1133
+ const handleReset = useCallback6(() => {
1134
+ reset();
1135
+ setStatusOpen(false);
1136
+ setMenuOpen(false);
1137
+ }, [reset]);
1138
+ const handleMenuClose = useCallback6(() => {
1139
+ setMenuOpen(false);
1140
+ }, []);
1141
+ const handleStatusClose = useCallback6(() => {
1142
+ setStatusOpen(false);
1143
+ }, []);
1144
+ return /* @__PURE__ */ jsxs5("div", { "data-afterbefore": "true", children: [
1145
+ /* @__PURE__ */ jsx5(
1146
+ Icon,
1147
+ {
1148
+ phase: state.phase,
1149
+ onClick: handleIconClick,
1150
+ loading,
1151
+ onPositionChange: handlePositionChange
1152
+ }
1153
+ ),
1154
+ menuOpen && (state.phase === "idle" || state.phase === "captured-before") && /* @__PURE__ */ jsx5(
1155
+ Menu,
1156
+ {
1157
+ onSelect: handleModeSelect,
1158
+ onClose: handleMenuClose,
1159
+ position: iconPos.current
1160
+ }
1161
+ ),
1162
+ selectorActive && /* @__PURE__ */ jsx5(Selector, { onSelect: handleAreaSelect, onCancel: handleAreaCancel }),
1163
+ statusOpen && state.phase === "ready" && /* @__PURE__ */ jsx5(
1164
+ Status,
1165
+ {
1166
+ onReset: handleReset,
1167
+ position: iconPos.current,
1168
+ onClose: handleStatusClose
1169
+ }
1170
+ )
1171
+ ] });
1172
+ }
1173
+ export {
1174
+ AfterBefore
1175
+ };
1176
+ //# sourceMappingURL=index.js.map