@rafaelbarross/feedback-widget 0.1.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,1072 @@
1
+ import { __spreadValues, __spreadProps } from './chunk-FJBZBVPE.mjs';
2
+ import { useState, useEffect, useCallback, useRef } from 'react';
3
+ import ReactDOM from 'react-dom';
4
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
5
+
6
+ var MARGIN = 20;
7
+ function snapStyles(pos) {
8
+ const m = MARGIN;
9
+ const map = {
10
+ "top-left": { top: m, left: m },
11
+ "top-center": { top: m, left: "50%", transform: "translateX(-50%)" },
12
+ "top-right": { top: m, right: m },
13
+ "middle-left": { top: "50%", left: m, transform: "translateY(-50%)" },
14
+ "middle-right": { top: "50%", right: m, transform: "translateY(-50%)" },
15
+ "bottom-left": { bottom: m, left: m },
16
+ "bottom-center": { bottom: m, left: "50%", transform: "translateX(-50%)" },
17
+ "bottom-right": { bottom: m, right: m }
18
+ };
19
+ return map[pos];
20
+ }
21
+ function resolveSnap(x, y) {
22
+ const rx = x / window.innerWidth;
23
+ const ry = y / window.innerHeight;
24
+ const h = rx < 0.33 ? "left" : rx > 0.67 ? "right" : "center";
25
+ const v = ry < 0.25 ? "top" : ry > 0.75 ? "bottom" : "middle";
26
+ if (v === "middle" && h === "center") return rx < 0.5 ? "middle-left" : "middle-right";
27
+ if (v === "middle") return `middle-${h}`;
28
+ return `${v}-${h}`;
29
+ }
30
+ function FeedbackButton({ position, label, onClick }) {
31
+ const [snap, setSnap] = useState(position);
32
+ const [hovered, setHovered] = useState(false);
33
+ const [dragging, setDragging] = useState(false);
34
+ const [dragXY, setDragXY] = useState(null);
35
+ const dragStart = useRef(null);
36
+ const didDrag = useRef(false);
37
+ const btnRef = useRef(null);
38
+ useEffect(() => {
39
+ if (!dragging) return;
40
+ const onMove = (e) => {
41
+ if (!dragStart.current) return;
42
+ const dx = e.clientX - dragStart.current.mouseX;
43
+ const dy = e.clientY - dragStart.current.mouseY;
44
+ if (Math.abs(dx) > 4 || Math.abs(dy) > 4) didDrag.current = true;
45
+ setDragXY({ x: dragStart.current.btnX + dx, y: dragStart.current.btnY + dy });
46
+ };
47
+ const onUp = (e) => {
48
+ setDragging(false);
49
+ if (didDrag.current) {
50
+ setSnap(resolveSnap(e.clientX, e.clientY));
51
+ setDragXY(null);
52
+ } else {
53
+ setDragXY(null);
54
+ onClick();
55
+ }
56
+ didDrag.current = false;
57
+ dragStart.current = null;
58
+ };
59
+ window.addEventListener("mousemove", onMove);
60
+ window.addEventListener("mouseup", onUp);
61
+ return () => {
62
+ window.removeEventListener("mousemove", onMove);
63
+ window.removeEventListener("mouseup", onUp);
64
+ };
65
+ }, [dragging, onClick]);
66
+ const onMouseDown = (e) => {
67
+ e.preventDefault();
68
+ const rect = btnRef.current.getBoundingClientRect();
69
+ dragStart.current = { mouseX: e.clientX, mouseY: e.clientY, btnX: rect.left, btnY: rect.top };
70
+ didDrag.current = false;
71
+ setDragging(true);
72
+ };
73
+ const baseStyle = {
74
+ position: "fixed",
75
+ zIndex: 999998,
76
+ display: "flex",
77
+ alignItems: "center",
78
+ gap: 8,
79
+ padding: "10px 16px",
80
+ background: hovered && !dragging ? "#4338CA" : "#4F46E5",
81
+ color: "#fff",
82
+ border: "none",
83
+ borderRadius: 9999,
84
+ cursor: dragging ? "grabbing" : "grab",
85
+ fontSize: 14,
86
+ fontFamily: "system-ui, -apple-system, sans-serif",
87
+ fontWeight: 600,
88
+ boxShadow: dragging ? "0 8px 24px rgba(79,70,229,0.55)" : "0 4px 14px rgba(79,70,229,0.45)",
89
+ userSelect: "none",
90
+ transition: dragging ? "none" : "background 0.15s, box-shadow 0.15s"
91
+ };
92
+ const positionStyle = dragging && dragXY ? { left: dragXY.x, top: dragXY.y } : __spreadProps(__spreadValues({}, snapStyles(snap)), { transition: dragging ? "none" : "all 0.2s cubic-bezier(.34,1.56,.64,1)" });
93
+ return /* @__PURE__ */ jsxs(
94
+ "button",
95
+ {
96
+ ref: btnRef,
97
+ onMouseDown,
98
+ onMouseEnter: () => setHovered(true),
99
+ onMouseLeave: () => setHovered(false),
100
+ style: __spreadValues(__spreadValues({}, baseStyle), positionStyle),
101
+ "aria-label": label,
102
+ children: [
103
+ /* @__PURE__ */ jsx(BugIcon, {}),
104
+ label
105
+ ]
106
+ }
107
+ );
108
+ }
109
+ function BugIcon() {
110
+ return /* @__PURE__ */ jsx("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "M8 2l1.5 1.5M14.5 3.5L16 2M9 7.5C9 6.1 10.1 5 11.5 5h1C13.9 5 15 6.1 15 7.5M6.5 9H4a1 1 0 0 0-1 1v1a4 4 0 0 0 4 4M17.5 9H20a1 1 0 0 1 1 1v1a4 4 0 0 1-4 4M8 15a4 4 0 0 0 8 0V9a4 4 0 0 0-8 0v6zM9 19.5V22M15 19.5V22M3 15h2M19 15h2" }) });
111
+ }
112
+ var INIT_DRAG = { active: false, startX: 0, startY: 0, endX: 0, endY: 0 };
113
+ function CaptureOverlay({ fullPageCanvas, onCapture, onCancel }) {
114
+ const canvasRef = useRef(null);
115
+ const drag = useRef(INIT_DRAG);
116
+ const imgRef = useRef(null);
117
+ const [selecting, setSelecting] = useState(false);
118
+ const draw = useCallback(() => {
119
+ const canvas = canvasRef.current;
120
+ const img = imgRef.current;
121
+ if (!canvas || !img) return;
122
+ const ctx = canvas.getContext("2d");
123
+ const { startX, startY, endX, endY, active } = drag.current;
124
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
125
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
126
+ ctx.fillStyle = "rgba(0,0,0,0.52)";
127
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
128
+ if (active) {
129
+ const x = Math.min(startX, endX);
130
+ const y = Math.min(startY, endY);
131
+ const w = Math.abs(endX - startX);
132
+ const h = Math.abs(endY - startY);
133
+ ctx.clearRect(x, y, w, h);
134
+ ctx.drawImage(img, x, y, w, h, x, y, w, h);
135
+ ctx.strokeStyle = "#4F46E5";
136
+ ctx.lineWidth = 2;
137
+ ctx.strokeRect(x, y, w, h);
138
+ if (w > 60 && h > 20) {
139
+ ctx.fillStyle = "rgba(79,70,229,0.9)";
140
+ ctx.fillRect(x + 4, y + h - 20, 76, 18);
141
+ ctx.fillStyle = "#fff";
142
+ ctx.font = "bold 11px system-ui, -apple-system, sans-serif";
143
+ ctx.fillText(`${Math.round(w)} \xD7 ${Math.round(h)}`, x + 8, y + h - 6);
144
+ }
145
+ }
146
+ }, []);
147
+ useEffect(() => {
148
+ const canvas = canvasRef.current;
149
+ if (!canvas) return;
150
+ canvas.width = window.innerWidth;
151
+ canvas.height = window.innerHeight;
152
+ const img = new Image();
153
+ img.onload = () => {
154
+ imgRef.current = img;
155
+ draw();
156
+ };
157
+ img.src = fullPageCanvas.toDataURL();
158
+ }, [fullPageCanvas, draw]);
159
+ useEffect(() => {
160
+ const onKeyDown = (e) => {
161
+ if (e.key === "Escape") onCancel();
162
+ };
163
+ window.addEventListener("keydown", onKeyDown);
164
+ return () => window.removeEventListener("keydown", onKeyDown);
165
+ }, [onCancel]);
166
+ const onMouseDown = (e) => {
167
+ drag.current = { active: true, startX: e.clientX, startY: e.clientY, endX: e.clientX, endY: e.clientY };
168
+ setSelecting(true);
169
+ };
170
+ const onMouseMove = (e) => {
171
+ if (!drag.current.active) return;
172
+ drag.current.endX = e.clientX;
173
+ drag.current.endY = e.clientY;
174
+ draw();
175
+ };
176
+ const onMouseUp = () => {
177
+ if (!drag.current.active) return;
178
+ drag.current.active = false;
179
+ setSelecting(false);
180
+ const { startX, startY, endX, endY } = drag.current;
181
+ const w = Math.abs(endX - startX);
182
+ const h = Math.abs(endY - startY);
183
+ if (w < 20 || h < 20) {
184
+ drag.current = INIT_DRAG;
185
+ draw();
186
+ return;
187
+ }
188
+ onCapture({
189
+ startX: Math.min(startX, endX),
190
+ startY: Math.min(startY, endY),
191
+ endX: Math.max(startX, endX),
192
+ endY: Math.max(startY, endY)
193
+ });
194
+ };
195
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
196
+ /* @__PURE__ */ jsx(
197
+ "canvas",
198
+ {
199
+ ref: canvasRef,
200
+ onMouseDown,
201
+ onMouseMove,
202
+ onMouseUp,
203
+ style: { position: "fixed", top: 0, left: 0, zIndex: 999999, cursor: "crosshair", display: "block" }
204
+ }
205
+ ),
206
+ /* @__PURE__ */ jsxs(
207
+ "div",
208
+ {
209
+ style: {
210
+ position: "fixed",
211
+ top: 16,
212
+ left: "50%",
213
+ transform: "translateX(-50%)",
214
+ zIndex: 1e6,
215
+ display: "flex",
216
+ alignItems: "center",
217
+ gap: 8,
218
+ background: "rgba(15,15,15,0.88)",
219
+ borderRadius: 8,
220
+ padding: "6px 8px 6px 16px",
221
+ fontFamily: "system-ui, -apple-system, sans-serif"
222
+ },
223
+ children: [
224
+ /* @__PURE__ */ jsxs("span", { style: { color: "#fff", fontSize: 13, fontWeight: 500, whiteSpace: "nowrap" }, children: [
225
+ "Click and drag to select \xA0\xB7\xA0 ",
226
+ /* @__PURE__ */ jsx("span", { style: { opacity: 0.6 }, children: "Esc to cancel" })
227
+ ] }),
228
+ /* @__PURE__ */ jsx(
229
+ "button",
230
+ {
231
+ onClick: () => onCapture({ startX: 0, startY: 0, endX: window.innerWidth, endY: window.innerHeight }),
232
+ style: {
233
+ background: "#4F46E5",
234
+ color: "#fff",
235
+ border: "none",
236
+ borderRadius: 6,
237
+ padding: "5px 12px",
238
+ cursor: "pointer",
239
+ fontSize: 12,
240
+ fontWeight: 600,
241
+ whiteSpace: "nowrap",
242
+ marginLeft: 4
243
+ },
244
+ children: "Full screen"
245
+ }
246
+ ),
247
+ /* @__PURE__ */ jsx(
248
+ "button",
249
+ {
250
+ onClick: onCancel,
251
+ style: {
252
+ background: "rgba(255,255,255,0.1)",
253
+ color: "#fff",
254
+ border: "1px solid rgba(255,255,255,0.15)",
255
+ borderRadius: 6,
256
+ padding: "5px 12px",
257
+ cursor: "pointer",
258
+ fontSize: 12
259
+ },
260
+ children: "Cancel"
261
+ }
262
+ )
263
+ ]
264
+ }
265
+ )
266
+ ] });
267
+ }
268
+ var COLORS = ["#EF4444", "#F97316", "#EAB308", "#22C55E", "#3B82F6", "#8B5CF6", "#000000", "#ffffff"];
269
+ var SIZES = [2, 5, 10];
270
+ var TOOL_ICONS = {
271
+ pen: /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" }) }),
272
+ rect: /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }) }),
273
+ ellipse: /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("ellipse", { cx: "12", cy: "12", rx: "10", ry: "6" }) }),
274
+ line: /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "20", y2: "4" }) }),
275
+ arrow: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
276
+ /* @__PURE__ */ jsx("line", { x1: "5", y1: "19", x2: "19", y2: "5" }),
277
+ /* @__PURE__ */ jsx("polyline", { points: "9 5 19 5 19 15" })
278
+ ] })
279
+ };
280
+ function applyStrokeStyle(ctx, color, size) {
281
+ ctx.strokeStyle = color;
282
+ ctx.fillStyle = color;
283
+ ctx.lineWidth = size;
284
+ ctx.lineCap = "round";
285
+ ctx.lineJoin = "round";
286
+ }
287
+ function drawShape(ctx, tool, x1, y1, x2, y2) {
288
+ ctx.beginPath();
289
+ if (tool === "rect") {
290
+ ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
291
+ } else if (tool === "ellipse") {
292
+ const cx = (x1 + x2) / 2;
293
+ const cy = (y1 + y2) / 2;
294
+ ctx.ellipse(cx, cy, Math.abs(x2 - x1) / 2, Math.abs(y2 - y1) / 2, 0, 0, Math.PI * 2);
295
+ ctx.stroke();
296
+ } else if (tool === "line") {
297
+ ctx.moveTo(x1, y1);
298
+ ctx.lineTo(x2, y2);
299
+ ctx.stroke();
300
+ } else if (tool === "arrow") {
301
+ const angle = Math.atan2(y2 - y1, x2 - x1);
302
+ const head = Math.max(12, ctx.lineWidth * 4);
303
+ ctx.moveTo(x1, y1);
304
+ ctx.lineTo(x2, y2);
305
+ ctx.stroke();
306
+ ctx.beginPath();
307
+ ctx.moveTo(x2, y2);
308
+ ctx.lineTo(x2 - head * Math.cos(angle - Math.PI / 6), y2 - head * Math.sin(angle - Math.PI / 6));
309
+ ctx.lineTo(x2 - head * Math.cos(angle + Math.PI / 6), y2 - head * Math.sin(angle + Math.PI / 6));
310
+ ctx.closePath();
311
+ ctx.fill();
312
+ }
313
+ }
314
+ function AnnotationCanvas({ screenshotDataUrl, onDone, onBack }) {
315
+ const canvasRef = useRef(null);
316
+ const isDrawing = useRef(false);
317
+ const startPoint = useRef(null);
318
+ const lastPoint = useRef(null);
319
+ const baseSnapshot = useRef(null);
320
+ const [tool, setTool] = useState("pen");
321
+ const [color, setColor] = useState("#EF4444");
322
+ const [size, setSize] = useState(3);
323
+ const [naturalSize, setNaturalSize] = useState({ w: 0, h: 0 });
324
+ const toolRef = useRef(tool);
325
+ const colorRef = useRef(color);
326
+ const sizeRef = useRef(size);
327
+ useEffect(() => {
328
+ toolRef.current = tool;
329
+ }, [tool]);
330
+ useEffect(() => {
331
+ colorRef.current = color;
332
+ }, [color]);
333
+ useEffect(() => {
334
+ sizeRef.current = size;
335
+ }, [size]);
336
+ useEffect(() => {
337
+ const img = new Image();
338
+ img.onload = () => {
339
+ const maxW = window.innerWidth * 0.88;
340
+ const maxH = window.innerHeight * 0.72;
341
+ const ratio = img.width / img.height;
342
+ let dw = img.width, dh = img.height;
343
+ if (dw > maxW) {
344
+ dw = maxW;
345
+ dh = dw / ratio;
346
+ }
347
+ if (dh > maxH) {
348
+ dh = maxH;
349
+ dw = dh * ratio;
350
+ }
351
+ setNaturalSize({ w: img.width, h: img.height });
352
+ const canvas = canvasRef.current;
353
+ canvas.width = img.width;
354
+ canvas.height = img.height;
355
+ canvas.style.width = `${Math.round(dw)}px`;
356
+ canvas.style.height = `${Math.round(dh)}px`;
357
+ const ctx = canvas.getContext("2d");
358
+ ctx.drawImage(img, 0, 0);
359
+ baseSnapshot.current = ctx.getImageData(0, 0, canvas.width, canvas.height);
360
+ };
361
+ img.src = screenshotDataUrl;
362
+ }, [screenshotDataUrl]);
363
+ function getPoint(e) {
364
+ const canvas = canvasRef.current;
365
+ const rect = canvas.getBoundingClientRect();
366
+ return {
367
+ x: (e.clientX - rect.left) * (naturalSize.w / rect.width),
368
+ y: (e.clientY - rect.top) * (naturalSize.h / rect.height)
369
+ };
370
+ }
371
+ function onMouseDown(e) {
372
+ isDrawing.current = true;
373
+ const pt = getPoint(e);
374
+ startPoint.current = pt;
375
+ lastPoint.current = pt;
376
+ const canvas = canvasRef.current;
377
+ const ctx = canvas.getContext("2d");
378
+ baseSnapshot.current = ctx.getImageData(0, 0, canvas.width, canvas.height);
379
+ }
380
+ function onMouseMove(e) {
381
+ if (!isDrawing.current || !startPoint.current) return;
382
+ const canvas = canvasRef.current;
383
+ const ctx = canvas.getContext("2d");
384
+ const pt = getPoint(e);
385
+ applyStrokeStyle(ctx, colorRef.current, sizeRef.current);
386
+ if (toolRef.current === "pen") {
387
+ ctx.beginPath();
388
+ ctx.moveTo(lastPoint.current.x, lastPoint.current.y);
389
+ ctx.lineTo(pt.x, pt.y);
390
+ ctx.stroke();
391
+ lastPoint.current = pt;
392
+ } else {
393
+ ctx.putImageData(baseSnapshot.current, 0, 0);
394
+ applyStrokeStyle(ctx, colorRef.current, sizeRef.current);
395
+ drawShape(ctx, toolRef.current, startPoint.current.x, startPoint.current.y, pt.x, pt.y);
396
+ }
397
+ }
398
+ function onMouseUp(e) {
399
+ if (!isDrawing.current || !startPoint.current) return;
400
+ isDrawing.current = false;
401
+ const canvas = canvasRef.current;
402
+ const ctx = canvas.getContext("2d");
403
+ const pt = getPoint(e);
404
+ if (toolRef.current !== "pen") {
405
+ ctx.putImageData(baseSnapshot.current, 0, 0);
406
+ applyStrokeStyle(ctx, colorRef.current, sizeRef.current);
407
+ drawShape(ctx, toolRef.current, startPoint.current.x, startPoint.current.y, pt.x, pt.y);
408
+ }
409
+ baseSnapshot.current = ctx.getImageData(0, 0, canvas.width, canvas.height);
410
+ startPoint.current = null;
411
+ lastPoint.current = null;
412
+ }
413
+ function clearAnnotations() {
414
+ const canvas = canvasRef.current;
415
+ const ctx = canvas.getContext("2d");
416
+ const img = new Image();
417
+ img.onload = () => {
418
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
419
+ ctx.drawImage(img, 0, 0);
420
+ baseSnapshot.current = ctx.getImageData(0, 0, canvas.width, canvas.height);
421
+ };
422
+ img.src = screenshotDataUrl;
423
+ }
424
+ const toolBtn = (t) => ({
425
+ background: tool === t ? "#4F46E5" : "rgba(255,255,255,0.07)",
426
+ color: "#fff",
427
+ border: "none",
428
+ borderRadius: 6,
429
+ width: 30,
430
+ height: 30,
431
+ cursor: "pointer",
432
+ display: "flex",
433
+ alignItems: "center",
434
+ justifyContent: "center",
435
+ flexShrink: 0
436
+ });
437
+ return /* @__PURE__ */ jsxs("div", { style: {
438
+ position: "fixed",
439
+ inset: 0,
440
+ zIndex: 999999,
441
+ background: "rgba(0,0,0,0.82)",
442
+ display: "flex",
443
+ flexDirection: "column",
444
+ alignItems: "center",
445
+ justifyContent: "center",
446
+ gap: 16,
447
+ fontFamily: "system-ui, -apple-system, sans-serif"
448
+ }, children: [
449
+ /* @__PURE__ */ jsxs("div", { style: {
450
+ display: "flex",
451
+ alignItems: "center",
452
+ gap: 10,
453
+ background: "#1a1a1a",
454
+ border: "1px solid rgba(255,255,255,0.1)",
455
+ padding: "8px 14px",
456
+ borderRadius: 10,
457
+ flexWrap: "wrap"
458
+ }, children: [
459
+ /* @__PURE__ */ jsx("span", { style: { color: "#999", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em" }, children: "Tool" }),
460
+ Object.keys(TOOL_ICONS).map((t) => /* @__PURE__ */ jsx("button", { onClick: () => setTool(t), style: toolBtn(t), title: t, children: TOOL_ICONS[t] }, t)),
461
+ /* @__PURE__ */ jsx("div", { style: { width: 1, height: 20, background: "rgba(255,255,255,0.12)" } }),
462
+ /* @__PURE__ */ jsx("span", { style: { color: "#999", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em" }, children: "Color" }),
463
+ COLORS.map((c) => /* @__PURE__ */ jsx("button", { onClick: () => setColor(c), style: {
464
+ width: 20,
465
+ height: 20,
466
+ borderRadius: "50%",
467
+ background: c,
468
+ border: "none",
469
+ cursor: "pointer",
470
+ padding: 0,
471
+ flexShrink: 0,
472
+ outline: color === c ? "2px solid #4F46E5" : "none",
473
+ outlineOffset: 2,
474
+ boxShadow: color === c ? "0 0 0 1px rgba(79,70,229,0.4)" : "none"
475
+ } }, c)),
476
+ /* @__PURE__ */ jsx("div", { style: { width: 1, height: 20, background: "rgba(255,255,255,0.12)" } }),
477
+ /* @__PURE__ */ jsx("span", { style: { color: "#999", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em" }, children: "Size" }),
478
+ SIZES.map((s) => /* @__PURE__ */ jsx("button", { onClick: () => setSize(s), style: {
479
+ width: 28,
480
+ height: 28,
481
+ borderRadius: 6,
482
+ border: "none",
483
+ cursor: "pointer",
484
+ background: size === s ? "#4F46E5" : "rgba(255,255,255,0.07)",
485
+ display: "flex",
486
+ alignItems: "center",
487
+ justifyContent: "center",
488
+ flexShrink: 0
489
+ }, children: /* @__PURE__ */ jsx("div", { style: { width: s * 2.5, height: s * 2.5, borderRadius: "50%", background: "#fff" } }) }, s)),
490
+ /* @__PURE__ */ jsx("div", { style: { width: 1, height: 20, background: "rgba(255,255,255,0.12)" } }),
491
+ /* @__PURE__ */ jsx("button", { onClick: clearAnnotations, style: {
492
+ background: "rgba(255,255,255,0.07)",
493
+ color: "#ccc",
494
+ border: "none",
495
+ borderRadius: 6,
496
+ padding: "4px 10px",
497
+ cursor: "pointer",
498
+ fontSize: 12,
499
+ fontWeight: 500
500
+ }, children: "Clear" })
501
+ ] }),
502
+ /* @__PURE__ */ jsx(
503
+ "canvas",
504
+ {
505
+ ref: canvasRef,
506
+ onMouseDown,
507
+ onMouseMove,
508
+ onMouseUp,
509
+ onMouseLeave: onMouseUp,
510
+ style: {
511
+ cursor: tool === "pen" ? "crosshair" : "crosshair",
512
+ borderRadius: 4,
513
+ boxShadow: "0 8px 32px rgba(0,0,0,0.6)",
514
+ display: "block",
515
+ maxWidth: "88vw",
516
+ maxHeight: "72vh"
517
+ }
518
+ }
519
+ ),
520
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 10 }, children: [
521
+ /* @__PURE__ */ jsx("button", { onClick: onBack, style: {
522
+ background: "rgba(255,255,255,0.08)",
523
+ color: "#ccc",
524
+ border: "1px solid rgba(255,255,255,0.12)",
525
+ borderRadius: 8,
526
+ padding: "9px 20px",
527
+ cursor: "pointer",
528
+ fontSize: 14,
529
+ fontWeight: 500
530
+ }, children: "\u2190 Back" }),
531
+ /* @__PURE__ */ jsx("button", { onClick: () => canvasRef.current && onDone(canvasRef.current), style: {
532
+ background: "#4F46E5",
533
+ color: "#fff",
534
+ border: "none",
535
+ borderRadius: 8,
536
+ padding: "9px 22px",
537
+ cursor: "pointer",
538
+ fontSize: 14,
539
+ fontWeight: 600
540
+ }, children: "Add comment \u2192" })
541
+ ] })
542
+ ] });
543
+ }
544
+ function FeedbackForm({
545
+ screenshotDataUrl,
546
+ isSubmitting,
547
+ error,
548
+ onSubmit,
549
+ onBack
550
+ }) {
551
+ const [summary, setSummary] = useState("");
552
+ const [description, setDescription] = useState("");
553
+ function handleSubmit(e) {
554
+ e.preventDefault();
555
+ if (!summary.trim()) return;
556
+ onSubmit(summary.trim(), description.trim());
557
+ }
558
+ return /* @__PURE__ */ jsx(
559
+ "div",
560
+ {
561
+ style: {
562
+ position: "fixed",
563
+ inset: 0,
564
+ zIndex: 999999,
565
+ background: "rgba(0,0,0,0.7)",
566
+ display: "flex",
567
+ alignItems: "center",
568
+ justifyContent: "center",
569
+ fontFamily: "system-ui, -apple-system, sans-serif",
570
+ padding: 16
571
+ },
572
+ children: /* @__PURE__ */ jsxs(
573
+ "div",
574
+ {
575
+ style: {
576
+ background: "#fff",
577
+ borderRadius: 12,
578
+ width: "100%",
579
+ maxWidth: 480,
580
+ boxShadow: "0 24px 64px rgba(0,0,0,0.4)",
581
+ overflow: "hidden"
582
+ },
583
+ children: [
584
+ /* @__PURE__ */ jsxs(
585
+ "div",
586
+ {
587
+ style: {
588
+ background: "#4F46E5",
589
+ padding: "16px 20px",
590
+ color: "#fff"
591
+ },
592
+ children: [
593
+ /* @__PURE__ */ jsx("h2", { style: { margin: 0, fontSize: 16, fontWeight: 700 }, children: "Send Feedback" }),
594
+ /* @__PURE__ */ jsx("p", { style: { margin: "4px 0 0", fontSize: 13, opacity: 0.8 }, children: "Describe what went wrong or what you'd like to report." })
595
+ ]
596
+ }
597
+ ),
598
+ /* @__PURE__ */ jsxs("div", { style: { padding: "12px 20px 0" }, children: [
599
+ /* @__PURE__ */ jsx("p", { style: { margin: "0 0 6px", fontSize: 12, fontWeight: 600, color: "#6B7280" }, children: "SCREENSHOT" }),
600
+ /* @__PURE__ */ jsx(
601
+ "img",
602
+ {
603
+ src: screenshotDataUrl,
604
+ alt: "Captured screenshot",
605
+ style: {
606
+ width: "100%",
607
+ borderRadius: 6,
608
+ border: "1px solid #E5E7EB",
609
+ display: "block",
610
+ maxHeight: 140,
611
+ objectFit: "cover",
612
+ objectPosition: "top"
613
+ }
614
+ }
615
+ )
616
+ ] }),
617
+ /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, style: { padding: "16px 20px 20px" }, children: [
618
+ /* @__PURE__ */ jsxs("label", { style: { display: "block", marginBottom: 12 }, children: [
619
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 13, fontWeight: 600, color: "#374151", display: "block", marginBottom: 4 }, children: [
620
+ "Summary ",
621
+ /* @__PURE__ */ jsx("span", { style: { color: "#EF4444" }, children: "*" })
622
+ ] }),
623
+ /* @__PURE__ */ jsx(
624
+ "input",
625
+ {
626
+ type: "text",
627
+ value: summary,
628
+ onChange: (e) => setSummary(e.target.value),
629
+ placeholder: "Short description of the issue",
630
+ disabled: isSubmitting,
631
+ maxLength: 120,
632
+ required: true,
633
+ style: {
634
+ width: "100%",
635
+ padding: "8px 12px",
636
+ border: "1px solid #D1D5DB",
637
+ borderRadius: 7,
638
+ fontSize: 14,
639
+ outline: "none",
640
+ boxSizing: "border-box",
641
+ transition: "border-color 0.15s",
642
+ color: "#111827"
643
+ }
644
+ }
645
+ )
646
+ ] }),
647
+ /* @__PURE__ */ jsxs("label", { style: { display: "block", marginBottom: 16 }, children: [
648
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 13, fontWeight: 600, color: "#374151", display: "block", marginBottom: 4 }, children: "Additional details" }),
649
+ /* @__PURE__ */ jsx(
650
+ "textarea",
651
+ {
652
+ value: description,
653
+ onChange: (e) => setDescription(e.target.value),
654
+ placeholder: "Steps to reproduce, expected behavior, etc.",
655
+ disabled: isSubmitting,
656
+ rows: 4,
657
+ style: {
658
+ width: "100%",
659
+ padding: "8px 12px",
660
+ border: "1px solid #D1D5DB",
661
+ borderRadius: 7,
662
+ fontSize: 14,
663
+ outline: "none",
664
+ resize: "vertical",
665
+ boxSizing: "border-box",
666
+ fontFamily: "inherit",
667
+ color: "#111827"
668
+ }
669
+ }
670
+ )
671
+ ] }),
672
+ error && /* @__PURE__ */ jsx(
673
+ "div",
674
+ {
675
+ style: {
676
+ background: "#FEF2F2",
677
+ border: "1px solid #FECACA",
678
+ color: "#DC2626",
679
+ borderRadius: 7,
680
+ padding: "8px 12px",
681
+ fontSize: 13,
682
+ marginBottom: 12
683
+ },
684
+ children: error
685
+ }
686
+ ),
687
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end" }, children: [
688
+ /* @__PURE__ */ jsx(
689
+ "button",
690
+ {
691
+ type: "button",
692
+ onClick: onBack,
693
+ disabled: isSubmitting,
694
+ style: {
695
+ background: "transparent",
696
+ color: "#6B7280",
697
+ border: "1px solid #D1D5DB",
698
+ borderRadius: 7,
699
+ padding: "8px 16px",
700
+ cursor: "pointer",
701
+ fontSize: 14,
702
+ fontWeight: 500
703
+ },
704
+ children: "\u2190 Back"
705
+ }
706
+ ),
707
+ /* @__PURE__ */ jsx(
708
+ "button",
709
+ {
710
+ type: "submit",
711
+ disabled: isSubmitting || !summary.trim(),
712
+ style: {
713
+ background: isSubmitting || !summary.trim() ? "#A5B4FC" : "#4F46E5",
714
+ color: "#fff",
715
+ border: "none",
716
+ borderRadius: 7,
717
+ padding: "8px 20px",
718
+ cursor: isSubmitting || !summary.trim() ? "not-allowed" : "pointer",
719
+ fontSize: 14,
720
+ fontWeight: 600,
721
+ minWidth: 110,
722
+ transition: "background 0.15s"
723
+ },
724
+ children: isSubmitting ? "Sending\u2026" : "Send to Jira"
725
+ }
726
+ )
727
+ ] })
728
+ ] })
729
+ ]
730
+ }
731
+ )
732
+ }
733
+ );
734
+ }
735
+ function makeAuthHeader(email, token) {
736
+ return "Basic " + btoa(`${email}:${token}`);
737
+ }
738
+ function makeAdfDescription(description, pageUrl, pageTitle) {
739
+ return {
740
+ type: "doc",
741
+ version: 1,
742
+ content: [
743
+ {
744
+ type: "paragraph",
745
+ content: [{ type: "text", text: description }]
746
+ },
747
+ {
748
+ type: "paragraph",
749
+ content: [
750
+ { type: "text", text: "Page: ", marks: [{ type: "strong" }] },
751
+ {
752
+ type: "text",
753
+ text: pageTitle || pageUrl,
754
+ marks: [
755
+ {
756
+ type: "link",
757
+ attrs: { href: pageUrl }
758
+ }
759
+ ]
760
+ }
761
+ ]
762
+ }
763
+ ]
764
+ };
765
+ }
766
+ async function createJiraIssue(params) {
767
+ const {
768
+ host,
769
+ projectKey,
770
+ token,
771
+ userEmail,
772
+ issueType,
773
+ labels,
774
+ summary,
775
+ description,
776
+ screenshotBlob,
777
+ pageUrl,
778
+ pageTitle
779
+ } = params;
780
+ const baseUrl = `https://${host}`;
781
+ const auth = makeAuthHeader(userEmail, token);
782
+ const issueBody = {
783
+ fields: __spreadValues({
784
+ project: { key: projectKey },
785
+ summary: `[Feedback] ${summary}`,
786
+ description: makeAdfDescription(description, pageUrl, pageTitle),
787
+ issuetype: { name: issueType }
788
+ }, labels.length > 0 && { labels })
789
+ };
790
+ const createRes = await fetch(`${baseUrl}/rest/api/3/issue`, {
791
+ method: "POST",
792
+ headers: {
793
+ Authorization: auth,
794
+ "Content-Type": "application/json",
795
+ Accept: "application/json"
796
+ },
797
+ body: JSON.stringify(issueBody)
798
+ });
799
+ if (!createRes.ok) {
800
+ const err = await createRes.text();
801
+ throw new Error(`Jira issue creation failed (${createRes.status}): ${err}`);
802
+ }
803
+ const { key: issueKey } = await createRes.json();
804
+ const formData = new FormData();
805
+ formData.append("file", screenshotBlob, "feedback-screenshot.png");
806
+ const attachRes = await fetch(`${baseUrl}/rest/api/3/issue/${issueKey}/attachments`, {
807
+ method: "POST",
808
+ headers: {
809
+ Authorization: auth,
810
+ "X-Atlassian-Token": "no-check"
811
+ },
812
+ body: formData
813
+ });
814
+ if (!attachRes.ok) {
815
+ console.warn(`[FeedbackWidget] Screenshot attachment failed (${attachRes.status})`);
816
+ }
817
+ return {
818
+ issueKey,
819
+ issueUrl: `${baseUrl}/browse/${issueKey}`
820
+ };
821
+ }
822
+ function useJira() {
823
+ const [isLoading, setIsLoading] = useState(false);
824
+ const [error, setError] = useState(null);
825
+ async function submit(params) {
826
+ setIsLoading(true);
827
+ setError(null);
828
+ try {
829
+ const result = await createJiraIssue(params);
830
+ return result;
831
+ } catch (err) {
832
+ const e = err instanceof Error ? err : new Error(String(err));
833
+ setError(e);
834
+ throw e;
835
+ } finally {
836
+ setIsLoading(false);
837
+ }
838
+ }
839
+ return { submit, isLoading, error };
840
+ }
841
+ function FeedbackWidget({
842
+ jiraHost,
843
+ jiraProjectKey,
844
+ jiraToken,
845
+ jiraUserEmail,
846
+ jiraIssueType = "Bug",
847
+ jiraLabels = [],
848
+ onSubmit: onSubmitProp,
849
+ apiRoute,
850
+ position = "bottom-right",
851
+ buttonLabel = "Feedback",
852
+ onSuccess,
853
+ onError
854
+ }) {
855
+ const [step, setStep] = useState("idle");
856
+ const [fullPageCanvas, setFullPageCanvas] = useState(null);
857
+ const [screenshotDataUrl, setScreenshotDataUrl] = useState("");
858
+ const [annotatedCanvas, setAnnotatedCanvas] = useState(null);
859
+ const [submitError, setSubmitError] = useState(null);
860
+ const [portalRoot, setPortalRoot] = useState(null);
861
+ const { submit: submitToJira } = useJira();
862
+ useEffect(() => {
863
+ const hasJiraConfig = jiraHost && jiraProjectKey && jiraToken && jiraUserEmail;
864
+ if (!hasJiraConfig && !onSubmitProp && !apiRoute) {
865
+ console.error("[FeedbackWidget] Provide apiRoute, onSubmit, or (jiraHost + jiraProjectKey + jiraToken + jiraUserEmail).");
866
+ }
867
+ }, [jiraHost, jiraProjectKey, jiraToken, jiraUserEmail, onSubmitProp, apiRoute]);
868
+ useEffect(() => {
869
+ const el = document.createElement("div");
870
+ el.setAttribute("data-feedback-widget", "");
871
+ document.body.appendChild(el);
872
+ setPortalRoot(el);
873
+ return () => {
874
+ document.body.removeChild(el);
875
+ };
876
+ }, []);
877
+ const handleButtonClick = useCallback(async () => {
878
+ setStep("capturing");
879
+ await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
880
+ try {
881
+ const stream = await navigator.mediaDevices.getDisplayMedia({
882
+ preferCurrentTab: true,
883
+ video: true,
884
+ audio: false
885
+ });
886
+ const video = document.createElement("video");
887
+ video.srcObject = stream;
888
+ video.muted = true;
889
+ await new Promise((resolve) => {
890
+ video.onloadedmetadata = () => resolve();
891
+ });
892
+ await video.play();
893
+ await new Promise((resolve) => requestAnimationFrame(resolve));
894
+ const canvas = document.createElement("canvas");
895
+ canvas.width = video.videoWidth;
896
+ canvas.height = video.videoHeight;
897
+ canvas.getContext("2d").drawImage(video, 0, 0);
898
+ stream.getTracks().forEach((t) => t.stop());
899
+ setFullPageCanvas(canvas);
900
+ setStep("selecting");
901
+ } catch (e) {
902
+ setStep("idle");
903
+ }
904
+ }, []);
905
+ const handleCapture = useCallback((rect) => {
906
+ if (!fullPageCanvas) return;
907
+ const scaleX = fullPageCanvas.width / window.innerWidth;
908
+ const scaleY = fullPageCanvas.height / window.innerHeight;
909
+ const cropX = Math.round(rect.startX * scaleX);
910
+ const cropY = Math.round(rect.startY * scaleY);
911
+ const cropW = Math.round((rect.endX - rect.startX) * scaleX);
912
+ const cropH = Math.round((rect.endY - rect.startY) * scaleY);
913
+ const cropped = document.createElement("canvas");
914
+ cropped.width = cropW;
915
+ cropped.height = cropH;
916
+ cropped.getContext("2d").drawImage(fullPageCanvas, cropX, cropY, cropW, cropH, 0, 0, cropW, cropH);
917
+ setScreenshotDataUrl(cropped.toDataURL("image/png"));
918
+ setStep("annotating");
919
+ }, [fullPageCanvas]);
920
+ const handleAnnotationDone = useCallback((canvas) => {
921
+ setAnnotatedCanvas(canvas);
922
+ setScreenshotDataUrl(canvas.toDataURL("image/png"));
923
+ setStep("form");
924
+ }, []);
925
+ const handleFormSubmit = useCallback(
926
+ async (summary, description) => {
927
+ var _a;
928
+ setSubmitError(null);
929
+ setStep("submitting");
930
+ const screenshotBlob = await new Promise((resolve, reject) => {
931
+ annotatedCanvas.toBlob((b) => b ? resolve(b) : reject(new Error("toBlob failed")), "image/png");
932
+ });
933
+ const payload = {
934
+ screenshot: screenshotBlob,
935
+ summary,
936
+ description,
937
+ pageUrl: window.location.href,
938
+ pageTitle: document.title
939
+ };
940
+ try {
941
+ let result = {};
942
+ if (onSubmitProp) {
943
+ result = (_a = await onSubmitProp(payload)) != null ? _a : {};
944
+ } else if (apiRoute) {
945
+ const fd = new FormData();
946
+ fd.append("screenshot", screenshotBlob);
947
+ fd.append("summary", summary);
948
+ fd.append("description", description);
949
+ fd.append("pageUrl", payload.pageUrl);
950
+ fd.append("pageTitle", payload.pageTitle);
951
+ const res = await fetch(apiRoute, { method: "POST", body: fd });
952
+ if (!res.ok) throw new Error(await res.text());
953
+ result = await res.json();
954
+ } else {
955
+ result = await submitToJira({
956
+ host: jiraHost,
957
+ projectKey: jiraProjectKey,
958
+ token: jiraToken,
959
+ userEmail: jiraUserEmail,
960
+ issueType: jiraIssueType,
961
+ labels: jiraLabels,
962
+ summary,
963
+ description,
964
+ screenshotBlob,
965
+ pageUrl: payload.pageUrl,
966
+ pageTitle: payload.pageTitle
967
+ });
968
+ }
969
+ setStep("success");
970
+ onSuccess == null ? void 0 : onSuccess(result);
971
+ setTimeout(() => {
972
+ setStep("idle");
973
+ setFullPageCanvas(null);
974
+ setScreenshotDataUrl("");
975
+ setAnnotatedCanvas(null);
976
+ }, 3e3);
977
+ } catch (err) {
978
+ const e = err instanceof Error ? err : new Error(String(err));
979
+ setSubmitError(e.message);
980
+ setStep("form");
981
+ onError == null ? void 0 : onError(e);
982
+ }
983
+ },
984
+ [
985
+ annotatedCanvas,
986
+ onSubmitProp,
987
+ submitToJira,
988
+ jiraHost,
989
+ jiraProjectKey,
990
+ jiraToken,
991
+ jiraUserEmail,
992
+ jiraIssueType,
993
+ jiraLabels,
994
+ onSuccess,
995
+ onError
996
+ ]
997
+ );
998
+ const reset = useCallback(() => {
999
+ setStep("idle");
1000
+ setFullPageCanvas(null);
1001
+ setScreenshotDataUrl("");
1002
+ setAnnotatedCanvas(null);
1003
+ setSubmitError(null);
1004
+ }, []);
1005
+ if (!portalRoot) return null;
1006
+ return ReactDOM.createPortal(
1007
+ /* @__PURE__ */ jsxs(Fragment, { children: [
1008
+ step === "idle" && /* @__PURE__ */ jsx(
1009
+ FeedbackButton,
1010
+ {
1011
+ position,
1012
+ label: buttonLabel,
1013
+ onClick: handleButtonClick
1014
+ }
1015
+ ),
1016
+ step === "selecting" && fullPageCanvas && /* @__PURE__ */ jsx(
1017
+ CaptureOverlay,
1018
+ {
1019
+ fullPageCanvas,
1020
+ onCapture: handleCapture,
1021
+ onCancel: reset
1022
+ }
1023
+ ),
1024
+ step === "annotating" && screenshotDataUrl && /* @__PURE__ */ jsx(
1025
+ AnnotationCanvas,
1026
+ {
1027
+ screenshotDataUrl,
1028
+ onDone: handleAnnotationDone,
1029
+ onBack: () => setStep("selecting")
1030
+ }
1031
+ ),
1032
+ (step === "form" || step === "submitting") && screenshotDataUrl && /* @__PURE__ */ jsx(
1033
+ FeedbackForm,
1034
+ {
1035
+ screenshotDataUrl,
1036
+ isSubmitting: step === "submitting",
1037
+ error: submitError,
1038
+ onSubmit: handleFormSubmit,
1039
+ onBack: () => setStep("annotating")
1040
+ }
1041
+ ),
1042
+ step === "success" && /* @__PURE__ */ jsx(SuccessToast, { position })
1043
+ ] }),
1044
+ portalRoot
1045
+ );
1046
+ }
1047
+ function SuccessToast({ position }) {
1048
+ const posStyle = position === "bottom-right" || position === "bottom-left" ? { bottom: 24 } : { top: 24 };
1049
+ const sideStyle = position === "bottom-right" || position === "top-right" ? { right: 24 } : { left: 24 };
1050
+ return /* @__PURE__ */ jsxs("div", { style: __spreadValues(__spreadValues({
1051
+ position: "fixed",
1052
+ zIndex: 999999,
1053
+ background: "#059669",
1054
+ color: "#fff",
1055
+ padding: "12px 20px",
1056
+ borderRadius: 10,
1057
+ fontFamily: "system-ui, -apple-system, sans-serif",
1058
+ fontSize: 14,
1059
+ fontWeight: 600,
1060
+ display: "flex",
1061
+ alignItems: "center",
1062
+ gap: 8,
1063
+ boxShadow: "0 4px 16px rgba(5,150,105,0.4)"
1064
+ }, posStyle), sideStyle), children: [
1065
+ /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" }) }),
1066
+ "Feedback sent to Jira!"
1067
+ ] });
1068
+ }
1069
+
1070
+ export { FeedbackWidget };
1071
+ //# sourceMappingURL=index.mjs.map
1072
+ //# sourceMappingURL=index.mjs.map