reflet-sdk 0.3.2 → 0.3.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.
- package/CHANGELOG.md +25 -0
- package/dist/client-BYuSKJw5.js.map +1 -1
- package/dist/{client-DR_mRfak.d.ts → client-BpEYxx4d.d.ts} +2 -2
- package/dist/{client-DR_mRfak.d.ts.map → client-BpEYxx4d.d.ts.map} +1 -1
- package/dist/feedback.d.ts +17 -7
- package/dist/feedback.d.ts.map +1 -1
- package/dist/feedback.js +2174 -1147
- package/dist/feedback.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/{types-Dp37I5ES.d.ts → types-wANRMsGI.d.ts} +2 -2
- package/dist/{types-Dp37I5ES.d.ts.map → types-wANRMsGI.d.ts.map} +1 -1
- package/package.json +2 -1
package/dist/feedback.js
CHANGED
|
@@ -1,14 +1,165 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { t as Reflet } from "./client-BYuSKJw5.js";
|
|
3
3
|
import { t as RefletContext } from "./react-context-DZw_HHjj.js";
|
|
4
|
-
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { Slider } from "@base-ui/react/slider";
|
|
7
|
+
import { ScrollArea } from "@base-ui/react/scroll-area";
|
|
6
8
|
import { createPortal } from "react-dom";
|
|
7
9
|
//#region src/feedback/core/capture.ts
|
|
8
10
|
/** Marks every node the widget owns, so captures never include the panel. */
|
|
9
11
|
const WIDGET_MARKER = "data-reflet-widget";
|
|
10
12
|
const MAX_PIXEL_RATIO = 2;
|
|
11
13
|
const DEFAULT_MAX_WIDTH = 2400;
|
|
14
|
+
const MAX_INLINE_BYTES = 3e6;
|
|
15
|
+
const TRANSPARENT_PIXEL = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
|
16
|
+
const EXTERNAL_URL = /^(https?:)?\/\//i;
|
|
17
|
+
const CSS_URL = /url\((['"]?)([^)'"]+)\1\)/g;
|
|
18
|
+
function isExternalImageUrl(url) {
|
|
19
|
+
if (!url || url.startsWith("data:") || url.startsWith("blob:")) return false;
|
|
20
|
+
if (!EXTERNAL_URL.test(url)) return false;
|
|
21
|
+
try {
|
|
22
|
+
return new URL(url, window.location.href).origin !== window.location.origin;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function toDataUrl(url) {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
fetch(url, { mode: "cors" }).then(async (response) => {
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
resolve(null);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const blob = await response.blob();
|
|
35
|
+
if (blob.size === 0 || blob.size > MAX_INLINE_BYTES) {
|
|
36
|
+
resolve(null);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const reader = new FileReader();
|
|
40
|
+
reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : null);
|
|
41
|
+
reader.onerror = () => resolve(null);
|
|
42
|
+
reader.readAsDataURL(blob);
|
|
43
|
+
}).catch(() => resolve(null));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const PNG_NORMALIZE_TIMEOUT = 750;
|
|
47
|
+
const CAPTURE_TIMEOUT = 1e4;
|
|
48
|
+
const ANIMATION_SETTLE_TIMEOUT = 1e3;
|
|
49
|
+
/**
|
|
50
|
+
* Tiles and sections routinely animate in or reflow (grid drags, entrance
|
|
51
|
+
* transitions); a capture taken mid-flight freezes them displaced. Waits for
|
|
52
|
+
* animations touching the page to finish, capped so an infinite spinner can
|
|
53
|
+
* never stall the report.
|
|
54
|
+
*/
|
|
55
|
+
async function waitForPageAnimations() {
|
|
56
|
+
if (typeof document.getAnimations !== "function") return;
|
|
57
|
+
const start = performance.now();
|
|
58
|
+
while (performance.now() - start < ANIMATION_SETTLE_TIMEOUT) {
|
|
59
|
+
const running = document.getAnimations().filter((animation) => animation.playState === "running");
|
|
60
|
+
if (running.length === 0) return;
|
|
61
|
+
await Promise.race([Promise.allSettled(running.map((animation) => animation.finished)), new Promise((resolve) => setTimeout(resolve, 150))]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* snapdom awaits `decode()` on images created with `decoding="sync"` — a
|
|
66
|
+
* promise Chrome 151 sometimes never settles (observed for cross-origin
|
|
67
|
+
* webp, cached or inlined). Re-encoding through a canvas yields a PNG data
|
|
68
|
+
* url, which decodes reliably; the load itself goes through `onload`, never
|
|
69
|
+
* `decode()`.
|
|
70
|
+
*/
|
|
71
|
+
function normalizeToPng(dataUrl) {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
const image = new Image();
|
|
74
|
+
const timeout = setTimeout(() => resolve(dataUrl), PNG_NORMALIZE_TIMEOUT);
|
|
75
|
+
image.onload = () => {
|
|
76
|
+
clearTimeout(timeout);
|
|
77
|
+
try {
|
|
78
|
+
if (image.naturalWidth === 0 || image.naturalHeight === 0) {
|
|
79
|
+
resolve(null);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const canvas = document.createElement("canvas");
|
|
83
|
+
canvas.width = image.naturalWidth;
|
|
84
|
+
canvas.height = image.naturalHeight;
|
|
85
|
+
const context = canvas.getContext("2d");
|
|
86
|
+
if (!context) {
|
|
87
|
+
resolve(dataUrl);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
context.drawImage(image, 0, 0);
|
|
91
|
+
resolve(canvas.toDataURL("image/png"));
|
|
92
|
+
} catch {
|
|
93
|
+
resolve(null);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
image.onerror = () => {
|
|
97
|
+
clearTimeout(timeout);
|
|
98
|
+
resolve(null);
|
|
99
|
+
};
|
|
100
|
+
image.src = dataUrl;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async function toInlineUrl(url) {
|
|
104
|
+
const dataUrl = await toDataUrl(url);
|
|
105
|
+
if (!dataUrl) return null;
|
|
106
|
+
return normalizeToPng(dataUrl);
|
|
107
|
+
}
|
|
108
|
+
function remember(swaps, element, name) {
|
|
109
|
+
swaps.push({
|
|
110
|
+
element,
|
|
111
|
+
name,
|
|
112
|
+
value: element.getAttribute(name)
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async function inlineImgSource(img, swaps) {
|
|
116
|
+
const src = img.getAttribute("src");
|
|
117
|
+
if (!(src && isExternalImageUrl(src))) return;
|
|
118
|
+
const dataUrl = await toInlineUrl(src);
|
|
119
|
+
if (dataUrl) {
|
|
120
|
+
if (img.hasAttribute("srcset")) {
|
|
121
|
+
remember(swaps, img, "srcset");
|
|
122
|
+
img.removeAttribute("srcset");
|
|
123
|
+
}
|
|
124
|
+
if (img.hasAttribute("sizes")) {
|
|
125
|
+
remember(swaps, img, "sizes");
|
|
126
|
+
img.removeAttribute("sizes");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
remember(swaps, img, "src");
|
|
130
|
+
img.setAttribute("src", dataUrl ?? TRANSPARENT_PIXEL);
|
|
131
|
+
}
|
|
132
|
+
async function inlineBackgroundImages(element, swaps) {
|
|
133
|
+
const style = element.getAttribute("style");
|
|
134
|
+
if (!style) return;
|
|
135
|
+
const urls = [...style.matchAll(CSS_URL)].map((match) => match[2] ?? "");
|
|
136
|
+
if (!urls.some(isExternalImageUrl)) return;
|
|
137
|
+
let next = style;
|
|
138
|
+
for (const url of urls) {
|
|
139
|
+
if (!isExternalImageUrl(url)) continue;
|
|
140
|
+
const dataUrl = await toInlineUrl(url);
|
|
141
|
+
next = next.split(url).join(dataUrl ?? TRANSPARENT_PIXEL);
|
|
142
|
+
}
|
|
143
|
+
if (next !== style) {
|
|
144
|
+
remember(swaps, element, "style");
|
|
145
|
+
element.setAttribute("style", next);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Swaps every cross-origin image under `root` for a data URL so the capture
|
|
150
|
+
* cannot hit snapdom's decode-failure hang. Returns the undo list — always
|
|
151
|
+
* run it, even after a failed capture.
|
|
152
|
+
*/
|
|
153
|
+
async function inlineExternalImages(root) {
|
|
154
|
+
const swaps = [];
|
|
155
|
+
for (const img of [...root.querySelectorAll("img")]) await inlineImgSource(img, swaps);
|
|
156
|
+
for (const el of [...root.querySelectorAll("[style*='url(']")]) await inlineBackgroundImages(el, swaps);
|
|
157
|
+
return swaps;
|
|
158
|
+
}
|
|
159
|
+
function restoreInlinedImages(swaps) {
|
|
160
|
+
for (const swap of swaps) if (swap.value === null) swap.element.removeAttribute(swap.name);
|
|
161
|
+
else swap.element.setAttribute(swap.name, swap.value);
|
|
162
|
+
}
|
|
12
163
|
function buildSnapdomOptions(options) {
|
|
13
164
|
const ratio = options.devicePixelRatio ?? 1;
|
|
14
165
|
return {
|
|
@@ -18,6 +169,7 @@ function buildSnapdomOptions(options) {
|
|
|
18
169
|
embedFonts: true,
|
|
19
170
|
exclude: [`[${WIDGET_MARKER}]`, ...options.excludeSelectors ?? []],
|
|
20
171
|
fast: true,
|
|
172
|
+
filter: options.filter,
|
|
21
173
|
reconcile: true
|
|
22
174
|
};
|
|
23
175
|
}
|
|
@@ -34,7 +186,7 @@ function fitWithin(size, maxWidth) {
|
|
|
34
186
|
width: maxWidth
|
|
35
187
|
};
|
|
36
188
|
}
|
|
37
|
-
function toBlob
|
|
189
|
+
function toBlob(canvas) {
|
|
38
190
|
return new Promise((resolve) => {
|
|
39
191
|
canvas.toBlob(resolve, "image/png");
|
|
40
192
|
});
|
|
@@ -56,7 +208,7 @@ async function capturedFromCanvas(source, maxWidth) {
|
|
|
56
208
|
width: source.width
|
|
57
209
|
}, maxWidth);
|
|
58
210
|
const output = fitted.scale === 1 ? source : downscale(source, fitted);
|
|
59
|
-
const blob = await toBlob
|
|
211
|
+
const blob = await toBlob(output);
|
|
60
212
|
if (!blob) return null;
|
|
61
213
|
return {
|
|
62
214
|
blob,
|
|
@@ -78,11 +230,27 @@ async function captureViewport(options = {}) {
|
|
|
78
230
|
try {
|
|
79
231
|
const { snapdom } = await import("@zumer/snapdom");
|
|
80
232
|
const target = options.element ?? document.documentElement;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
233
|
+
await waitForPageAnimations();
|
|
234
|
+
const swaps = await inlineExternalImages(target);
|
|
235
|
+
const shoot = (withoutImages = false) => {
|
|
236
|
+
const attempt = snapdom.toCanvas(target, buildSnapdomOptions({
|
|
237
|
+
devicePixelRatio: window.devicePixelRatio,
|
|
238
|
+
element: options.element,
|
|
239
|
+
excludeSelectors: options.excludeSelectors,
|
|
240
|
+
filter: withoutImages ? (element) => element.tagName !== "IMG" : void 0
|
|
241
|
+
}));
|
|
242
|
+
attempt.catch(() => void 0);
|
|
243
|
+
return Promise.race([attempt, new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("capture timed out")), options.timeout ?? CAPTURE_TIMEOUT))]);
|
|
244
|
+
};
|
|
245
|
+
let canvas;
|
|
246
|
+
try {
|
|
247
|
+
canvas = await shoot();
|
|
248
|
+
} catch {
|
|
249
|
+
canvas = await shoot(true);
|
|
250
|
+
} finally {
|
|
251
|
+
restoreInlinedImages(swaps);
|
|
252
|
+
}
|
|
253
|
+
return await capturedFromCanvas(canvas, options.maxWidth ?? DEFAULT_MAX_WIDTH);
|
|
86
254
|
} catch {
|
|
87
255
|
return null;
|
|
88
256
|
}
|
|
@@ -641,29 +809,40 @@ function collectPageContext(options) {
|
|
|
641
809
|
}
|
|
642
810
|
//#endregion
|
|
643
811
|
//#region src/feedback/types.ts
|
|
644
|
-
|
|
645
|
-
const SDK_VERSION = "0.3.2";
|
|
812
|
+
const SDK_VERSION = "0.3.4";
|
|
646
813
|
const DEFAULT_WIDGET_LABELS = {
|
|
647
814
|
annotateHint: "Draw on the screenshot to point at the problem.",
|
|
815
|
+
attachmentUploadFailed: "Your feedback was saved, but some screenshots are still pending. Retry to attach them.",
|
|
648
816
|
attachScreenshot: "Attach a screenshot",
|
|
649
817
|
back: "Back",
|
|
650
818
|
cancel: "Cancel",
|
|
819
|
+
captureFailed: "Screenshot unavailable. Try again or send without it.",
|
|
820
|
+
capturing: "Taking screenshot…",
|
|
651
821
|
categoryBug: "Bug",
|
|
652
822
|
categoryIdea: "Idea",
|
|
823
|
+
categoryLabel: "Feedback type",
|
|
653
824
|
categoryQuestion: "Question",
|
|
654
825
|
clearAnnotations: "Clear drawing",
|
|
655
|
-
|
|
826
|
+
clearSelection: "Remove selected element",
|
|
827
|
+
descriptionPlaceholder: "What would you like to share?",
|
|
656
828
|
dismissForDays: "Hide for {days} days",
|
|
657
829
|
done: "Done",
|
|
830
|
+
emailInvalid: "Enter a valid email address.",
|
|
658
831
|
emailLabel: "Email",
|
|
659
832
|
emailPlaceholder: "you@company.com",
|
|
660
833
|
errorGeneric: "Something went wrong. Please try again.",
|
|
661
834
|
errorTitleRequired: "Tell us a bit about it first.",
|
|
835
|
+
minimize: "Minimize feedback",
|
|
836
|
+
moreOptions: "More options",
|
|
837
|
+
moveFeedback: "Move feedback",
|
|
662
838
|
pickElement: "Point at an element",
|
|
663
839
|
pickElementHint: "Click the element you are talking about — cancel with",
|
|
664
840
|
recapture: "Retake",
|
|
665
841
|
removeScreenshot: "Remove",
|
|
842
|
+
resume: "Resume feedback",
|
|
666
843
|
retakeHint: "Screenshot of this page",
|
|
844
|
+
retryAttachments: "Retry attachments",
|
|
845
|
+
screenshot: "Screenshot",
|
|
667
846
|
submit: "Send feedback",
|
|
668
847
|
successMessage: "We read every report. Thanks for taking the time.",
|
|
669
848
|
successTitle: "Feedback sent",
|
|
@@ -674,15 +853,10 @@ const DEFAULT_WIDGET_LABELS = {
|
|
|
674
853
|
};
|
|
675
854
|
//#endregion
|
|
676
855
|
//#region src/feedback/core/annotations.ts
|
|
677
|
-
const ANNOTATION_COLORS = [
|
|
678
|
-
"#ef4444",
|
|
679
|
-
"#f59e0b",
|
|
680
|
-
"#22c55e",
|
|
681
|
-
"#3b82f6",
|
|
682
|
-
"#111827"
|
|
683
|
-
];
|
|
684
856
|
const BOX_TOOLS = [
|
|
857
|
+
"text",
|
|
685
858
|
"rectangle",
|
|
859
|
+
"spotlight",
|
|
686
860
|
"highlight",
|
|
687
861
|
"blur"
|
|
688
862
|
];
|
|
@@ -723,6 +897,7 @@ function simplifyPath(points, tolerance = DEFAULT_PATH_TOLERANCE) {
|
|
|
723
897
|
return kept;
|
|
724
898
|
}
|
|
725
899
|
function isDegenerate(annotation) {
|
|
900
|
+
if (annotation.tool === "text") return !annotation.text?.trim();
|
|
726
901
|
if (annotation.tool === "pen") return (annotation.points?.length ?? 0) < 2;
|
|
727
902
|
const { start, end } = annotation;
|
|
728
903
|
return Math.hypot(end.x - start.x, end.y - start.y) < MIN_GESTURE_LENGTH;
|
|
@@ -741,6 +916,7 @@ function toWire(annotation) {
|
|
|
741
916
|
return {
|
|
742
917
|
color,
|
|
743
918
|
height: rect.height,
|
|
919
|
+
...tool === "text" ? { text: annotation.text } : {},
|
|
744
920
|
type: tool,
|
|
745
921
|
width: rect.width,
|
|
746
922
|
x: rect.x,
|
|
@@ -761,144 +937,20 @@ function toWireAnnotations(annotations) {
|
|
|
761
937
|
return annotations.filter((item) => !isDegenerate(item)).map(toWire);
|
|
762
938
|
}
|
|
763
939
|
//#endregion
|
|
764
|
-
//#region src/feedback/
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
height: rect.height * scale,
|
|
779
|
-
width: rect.width * scale,
|
|
780
|
-
x: rect.x * scale,
|
|
781
|
-
y: rect.y * scale
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
function isCanvasElement(value) {
|
|
785
|
-
return typeof HTMLCanvasElement !== "undefined" && value instanceof HTMLCanvasElement;
|
|
786
|
-
}
|
|
787
|
-
/**
|
|
788
|
-
* Downsamples the region and paints it back enlarged, so redacted pixels are
|
|
789
|
-
* unrecoverable. Falls back to an opaque block wherever 2d canvases are absent.
|
|
790
|
-
*/
|
|
791
|
-
function redact(context, rect) {
|
|
792
|
-
const source = context.canvas;
|
|
793
|
-
const width = Math.max(1, Math.round(rect.width / PIXEL_BLOCK));
|
|
794
|
-
const height = Math.max(1, Math.round(rect.height / PIXEL_BLOCK));
|
|
795
|
-
if (isCanvasElement(source)) {
|
|
796
|
-
const offscreen = document.createElement("canvas");
|
|
797
|
-
offscreen.width = width;
|
|
798
|
-
offscreen.height = height;
|
|
799
|
-
const offscreenContext = offscreen.getContext("2d");
|
|
800
|
-
if (offscreenContext) {
|
|
801
|
-
offscreenContext.imageSmoothingEnabled = true;
|
|
802
|
-
offscreenContext.drawImage(source, rect.x, rect.y, rect.width, rect.height, 0, 0, width, height);
|
|
803
|
-
context.drawImage(offscreen, rect.x, rect.y, rect.width, rect.height);
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
context.fillStyle = REDACTION_FILL;
|
|
808
|
-
context.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
809
|
-
}
|
|
810
|
-
function drawArrow(context, start, end, scale) {
|
|
811
|
-
context.beginPath();
|
|
812
|
-
context.moveTo(start.x, start.y);
|
|
813
|
-
context.lineTo(end.x, end.y);
|
|
814
|
-
context.stroke();
|
|
815
|
-
const [left, right] = arrowHead(start, end, ARROW_HEAD_SIZE * scale);
|
|
816
|
-
context.beginPath();
|
|
817
|
-
context.moveTo(end.x, end.y);
|
|
818
|
-
context.lineTo(left.x, left.y);
|
|
819
|
-
context.lineTo(right.x, right.y);
|
|
820
|
-
context.closePath();
|
|
821
|
-
context.fill();
|
|
822
|
-
}
|
|
823
|
-
function drawPen(context, points, scale) {
|
|
824
|
-
const [first, ...rest] = points;
|
|
825
|
-
if (!first) return;
|
|
826
|
-
const head = scalePoint(first, scale);
|
|
827
|
-
context.beginPath();
|
|
828
|
-
context.moveTo(head.x, head.y);
|
|
829
|
-
for (const point of rest) {
|
|
830
|
-
const scaled = scalePoint(point, scale);
|
|
831
|
-
context.lineTo(scaled.x, scaled.y);
|
|
832
|
-
}
|
|
833
|
-
context.stroke();
|
|
834
|
-
}
|
|
835
|
-
function drawAnnotation(context, annotation, scale) {
|
|
836
|
-
const { color, tool } = annotation;
|
|
837
|
-
const rect = scaleRect(normalizeRect(annotation.start, annotation.end), scale);
|
|
838
|
-
context.save();
|
|
839
|
-
context.lineWidth = STROKE_WIDTH * scale;
|
|
840
|
-
context.lineCap = "round";
|
|
841
|
-
context.lineJoin = "round";
|
|
842
|
-
context.strokeStyle = color;
|
|
843
|
-
context.fillStyle = color;
|
|
844
|
-
switch (tool) {
|
|
845
|
-
case "pen":
|
|
846
|
-
drawPen(context, annotation.points ?? [], scale);
|
|
847
|
-
break;
|
|
848
|
-
case "arrow":
|
|
849
|
-
drawArrow(context, scalePoint(annotation.start, scale), scalePoint(annotation.end, scale), scale);
|
|
850
|
-
break;
|
|
851
|
-
case "rectangle":
|
|
852
|
-
context.strokeRect(rect.x, rect.y, rect.width, rect.height);
|
|
853
|
-
break;
|
|
854
|
-
case "highlight":
|
|
855
|
-
context.globalAlpha = HIGHLIGHT_ALPHA;
|
|
856
|
-
context.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
857
|
-
break;
|
|
858
|
-
default: redact(context, rect);
|
|
859
|
-
}
|
|
860
|
-
context.restore();
|
|
861
|
-
}
|
|
862
|
-
function drawAnnotations(context, annotations, options) {
|
|
863
|
-
const scale = options?.scale ?? 1;
|
|
864
|
-
for (const annotation of annotations) if (!isDegenerate(annotation)) drawAnnotation(context, annotation, scale);
|
|
865
|
-
}
|
|
866
|
-
function loadImage(url) {
|
|
867
|
-
return new Promise((resolve, reject) => {
|
|
868
|
-
const image = new Image();
|
|
869
|
-
image.onload = () => resolve(image);
|
|
870
|
-
image.onerror = () => reject(/* @__PURE__ */ new Error("Screenshot could not be decoded"));
|
|
871
|
-
image.src = url;
|
|
872
|
-
});
|
|
873
|
-
}
|
|
874
|
-
function toBlob(canvas) {
|
|
875
|
-
return new Promise((resolve) => {
|
|
876
|
-
canvas.toBlob(resolve, "image/png");
|
|
877
|
-
});
|
|
878
|
-
}
|
|
879
|
-
/**
|
|
880
|
-
* Burns the annotations into a copy of the capture so reviewers and agents see
|
|
881
|
-
* the drawing without needing to replay the vector data.
|
|
882
|
-
*/
|
|
883
|
-
async function renderAnnotatedImage(source, annotations) {
|
|
884
|
-
if (annotations.length === 0) return null;
|
|
885
|
-
const canvas = document.createElement("canvas");
|
|
886
|
-
canvas.width = source.width;
|
|
887
|
-
canvas.height = source.height;
|
|
888
|
-
const context = canvas.getContext("2d");
|
|
889
|
-
if (!context) return null;
|
|
890
|
-
const image = await loadImage(source.objectUrl);
|
|
891
|
-
context.drawImage(image, 0, 0, source.width, source.height);
|
|
892
|
-
drawAnnotations(context, annotations);
|
|
893
|
-
const blob = await toBlob(canvas);
|
|
894
|
-
if (!blob) return null;
|
|
895
|
-
return {
|
|
896
|
-
blob,
|
|
897
|
-
height: source.height,
|
|
898
|
-
mimeType: "image/png",
|
|
899
|
-
objectUrl: URL.createObjectURL(blob),
|
|
900
|
-
width: source.width
|
|
901
|
-
};
|
|
940
|
+
//#region src/feedback/ui/annotation/color-value.ts
|
|
941
|
+
const DEFAULT_ANNOTATION_COLOR = {
|
|
942
|
+
baseColor: "#ef4444",
|
|
943
|
+
strengthPercent: 100
|
|
944
|
+
};
|
|
945
|
+
function annotationColorHex({ baseColor, strengthPercent }) {
|
|
946
|
+
return `#${[
|
|
947
|
+
1,
|
|
948
|
+
3,
|
|
949
|
+
5
|
|
950
|
+
].map((offset) => {
|
|
951
|
+
const channel = Number.parseInt(baseColor.slice(offset, offset + 2), 16);
|
|
952
|
+
return Math.round(255 + (channel - 255) * strengthPercent / 100).toString(16).padStart(2, "0");
|
|
953
|
+
}).join("")}`;
|
|
902
954
|
}
|
|
903
955
|
//#endregion
|
|
904
956
|
//#region src/feedback/ui/icons.tsx
|
|
@@ -930,28 +982,6 @@ function CloseIcon({ className }) {
|
|
|
930
982
|
children: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" })
|
|
931
983
|
});
|
|
932
984
|
}
|
|
933
|
-
function BugIcon() {
|
|
934
|
-
return /* @__PURE__ */ jsx(Svg, {
|
|
935
|
-
size: 14,
|
|
936
|
-
children: /* @__PURE__ */ jsx("path", { d: "M8 6a4 4 0 0 1 8 0M6 10h12v4a6 6 0 0 1-12 0zM3 12h3M18 12h3M4.5 6.5 7 8M19.5 6.5 17 8M4.5 17.5 7 16M19.5 17.5 17 16" })
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
function IdeaIcon() {
|
|
940
|
-
return /* @__PURE__ */ jsx(Svg, {
|
|
941
|
-
size: 14,
|
|
942
|
-
children: /* @__PURE__ */ jsx("path", { d: "M9 18h6M10 21h4M12 3a6 6 0 0 0-3.6 10.8c.5.4.8.9.9 1.5l.1.7h5.2l.1-.7c.1-.6.4-1.1.9-1.5A6 6 0 0 0 12 3" })
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
function QuestionIcon() {
|
|
946
|
-
return /* @__PURE__ */ jsxs(Svg, {
|
|
947
|
-
size: 14,
|
|
948
|
-
children: [/* @__PURE__ */ jsx("circle", {
|
|
949
|
-
cx: "12",
|
|
950
|
-
cy: "12",
|
|
951
|
-
r: "9"
|
|
952
|
-
}), /* @__PURE__ */ jsx("path", { d: "M9.6 9.4a2.5 2.5 0 1 1 3.3 2.4c-.6.2-.9.8-.9 1.4v.3M12 17h.01" })]
|
|
953
|
-
});
|
|
954
|
-
}
|
|
955
985
|
function CameraIcon() {
|
|
956
986
|
return /* @__PURE__ */ jsxs(Svg, { children: [/* @__PURE__ */ jsx("path", { d: "M3 8.5A1.5 1.5 0 0 1 4.5 7h2.2l1.2-2h8.2l1.2 2h2.2A1.5 1.5 0 0 1 21 8.5v9A1.5 1.5 0 0 1 19.5 19h-15A1.5 1.5 0 0 1 3 17.5z" }), /* @__PURE__ */ jsx("circle", {
|
|
957
987
|
cx: "12",
|
|
@@ -975,21 +1005,6 @@ function TrashIcon() {
|
|
|
975
1005
|
function ArrowIcon() {
|
|
976
1006
|
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M5 19 19 5M11 5h8v8" }) });
|
|
977
1007
|
}
|
|
978
|
-
function SquareIcon() {
|
|
979
|
-
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("rect", {
|
|
980
|
-
height: "13",
|
|
981
|
-
rx: "1.5",
|
|
982
|
-
width: "16",
|
|
983
|
-
x: "4",
|
|
984
|
-
y: "5.5"
|
|
985
|
-
}) });
|
|
986
|
-
}
|
|
987
|
-
function HighlightIcon() {
|
|
988
|
-
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M4 15h16M6 5h12v7H6z" }) });
|
|
989
|
-
}
|
|
990
|
-
function BlurIcon() {
|
|
991
|
-
return /* @__PURE__ */ jsxs(Svg, { children: [/* @__PURE__ */ jsx("path", { d: "M12 3c3.5 4 5.5 6.6 5.5 9.2A5.5 5.5 0 0 1 6.5 12.2C6.5 9.6 8.5 7 12 3" }), /* @__PURE__ */ jsx("path", { d: "M9 13h.01M12 15h.01M15 12h.01" })] });
|
|
992
|
-
}
|
|
993
1008
|
function UndoIcon() {
|
|
994
1009
|
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M4 9h11a5 5 0 0 1 0 10h-5M4 9l4-4M4 9l4 4" }) });
|
|
995
1010
|
}
|
|
@@ -1007,83 +1022,742 @@ function CheckIcon({ size = 28 }) {
|
|
|
1007
1022
|
children: /* @__PURE__ */ jsx("path", { d: "m5 13 4 4L19 7" })
|
|
1008
1023
|
});
|
|
1009
1024
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
const TOOLS = [
|
|
1013
|
-
{
|
|
1014
|
-
icon: PencilIcon,
|
|
1015
|
-
id: "pen",
|
|
1016
|
-
label: "Draw"
|
|
1017
|
-
},
|
|
1018
|
-
{
|
|
1019
|
-
icon: ArrowIcon,
|
|
1020
|
-
id: "arrow",
|
|
1021
|
-
label: "Arrow"
|
|
1022
|
-
},
|
|
1023
|
-
{
|
|
1024
|
-
icon: SquareIcon,
|
|
1025
|
-
id: "rectangle",
|
|
1026
|
-
label: "Box"
|
|
1027
|
-
},
|
|
1028
|
-
{
|
|
1029
|
-
icon: HighlightIcon,
|
|
1030
|
-
id: "highlight",
|
|
1031
|
-
label: "Highlight"
|
|
1032
|
-
},
|
|
1033
|
-
{
|
|
1034
|
-
icon: BlurIcon,
|
|
1035
|
-
id: "blur",
|
|
1036
|
-
label: "Hide"
|
|
1037
|
-
}
|
|
1038
|
-
];
|
|
1039
|
-
function toImagePoint(canvas, clientX, clientY) {
|
|
1040
|
-
const bounds = canvas.getBoundingClientRect();
|
|
1041
|
-
return {
|
|
1042
|
-
x: (clientX - bounds.left) / bounds.width * canvas.width,
|
|
1043
|
-
y: (clientY - bounds.top) / bounds.height * canvas.height
|
|
1044
|
-
};
|
|
1025
|
+
function GripIcon() {
|
|
1026
|
+
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M9 5h.01M15 5h.01M9 12h.01M15 12h.01M9 19h.01M15 19h.01" }) });
|
|
1045
1027
|
}
|
|
1046
|
-
function
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1028
|
+
function MinusIcon() {
|
|
1029
|
+
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M5 12h14" }) });
|
|
1030
|
+
}
|
|
1031
|
+
function MoreIcon() {
|
|
1032
|
+
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M5 12h.01M12 12h.01M19 12h.01" }) });
|
|
1033
|
+
}
|
|
1034
|
+
function ChevronDownIcon() {
|
|
1035
|
+
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "m7 10 5 5 5-5" }) });
|
|
1036
|
+
}
|
|
1037
|
+
function SquareIcon() {
|
|
1038
|
+
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("rect", {
|
|
1039
|
+
height: "14",
|
|
1040
|
+
rx: "2",
|
|
1041
|
+
width: "14",
|
|
1042
|
+
x: "5",
|
|
1043
|
+
y: "5"
|
|
1044
|
+
}) });
|
|
1045
|
+
}
|
|
1046
|
+
function TextIcon() {
|
|
1047
|
+
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M5 6V4h14v2M12 4v16M8 20h8" }) });
|
|
1048
|
+
}
|
|
1049
|
+
function SpotlightIcon() {
|
|
1050
|
+
return /* @__PURE__ */ jsxs(Svg, { children: [/* @__PURE__ */ jsx("path", { d: "M8 4H4v4M16 4h4v4M4 16v4h4M20 16v4h-4" }), /* @__PURE__ */ jsx("rect", {
|
|
1051
|
+
height: "8",
|
|
1052
|
+
rx: "1",
|
|
1053
|
+
width: "8",
|
|
1054
|
+
x: "8",
|
|
1055
|
+
y: "8"
|
|
1056
|
+
})] });
|
|
1057
|
+
}
|
|
1058
|
+
function HighlightIcon() {
|
|
1059
|
+
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M4 19h16M8 15l8-11 4 3-8 11-4-3ZM7 15l-2 3" }) });
|
|
1060
|
+
}
|
|
1061
|
+
function BlurIcon() {
|
|
1062
|
+
return /* @__PURE__ */ jsxs(Svg, { children: [/* @__PURE__ */ jsx("rect", {
|
|
1063
|
+
height: "14",
|
|
1064
|
+
rx: "2",
|
|
1065
|
+
width: "14",
|
|
1066
|
+
x: "5",
|
|
1067
|
+
y: "5"
|
|
1068
|
+
}), /* @__PURE__ */ jsx("path", { d: "M8 8h8v8H8z" })] });
|
|
1069
|
+
}
|
|
1070
|
+
//#endregion
|
|
1071
|
+
//#region src/feedback/ui/annotation/text/text-editor.tsx
|
|
1072
|
+
function TextAnnotationEditor({ draft, onCommit, onCancel }) {
|
|
1073
|
+
const [text, setText] = useState(draft.annotation.text ?? "");
|
|
1074
|
+
const inputRef = useRef(null);
|
|
1075
|
+
const id = useId().replaceAll(":", "");
|
|
1076
|
+
useEffect(() => {
|
|
1077
|
+
inputRef.current?.focus();
|
|
1078
|
+
}, []);
|
|
1079
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
1080
|
+
className: "glass text-annotation-editor",
|
|
1081
|
+
id,
|
|
1082
|
+
onSubmit: (event) => {
|
|
1083
|
+
event.preventDefault();
|
|
1084
|
+
onCommit(text);
|
|
1085
|
+
},
|
|
1086
|
+
children: [
|
|
1087
|
+
/* @__PURE__ */ jsx("style", { children: `[id="${id}"] { left: clamp(12px, ${draft.position.x}px, calc(100vw - 252px)); top: clamp(12px, ${draft.position.y}px, calc(100dvh - 148px)); }` }),
|
|
1088
|
+
/* @__PURE__ */ jsx("textarea", {
|
|
1089
|
+
"aria-label": "Text annotation",
|
|
1090
|
+
maxLength: 280,
|
|
1091
|
+
onBlur: (event) => {
|
|
1092
|
+
if (!event.currentTarget.form?.contains(event.relatedTarget)) onCommit(text);
|
|
1093
|
+
},
|
|
1094
|
+
onChange: (event) => setText(event.target.value),
|
|
1095
|
+
onKeyDown: (event) => {
|
|
1096
|
+
if (event.key === "Escape") {
|
|
1097
|
+
event.preventDefault();
|
|
1098
|
+
event.stopPropagation();
|
|
1099
|
+
onCancel();
|
|
1100
|
+
}
|
|
1101
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
1102
|
+
event.preventDefault();
|
|
1103
|
+
onCommit(text);
|
|
1104
|
+
}
|
|
1105
|
+
},
|
|
1106
|
+
placeholder: "Type a note…",
|
|
1107
|
+
ref: inputRef,
|
|
1108
|
+
rows: 2,
|
|
1109
|
+
value: text
|
|
1110
|
+
}),
|
|
1111
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1112
|
+
className: "text-annotation-actions",
|
|
1113
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
1114
|
+
"aria-label": "Cancel text",
|
|
1115
|
+
className: "tool",
|
|
1116
|
+
onClick: onCancel,
|
|
1117
|
+
type: "button",
|
|
1118
|
+
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
1119
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1120
|
+
"aria-label": "Apply text",
|
|
1121
|
+
className: "done-btn",
|
|
1122
|
+
type: "submit",
|
|
1123
|
+
children: /* @__PURE__ */ jsx(CheckIcon, { size: 16 })
|
|
1124
|
+
})]
|
|
1125
|
+
})
|
|
1126
|
+
]
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
//#endregion
|
|
1130
|
+
//#region src/feedback/ui/annotation/text/use-text-annotation.ts
|
|
1131
|
+
function containsPoint(annotation, point) {
|
|
1132
|
+
const bounds = normalizeRect(annotation.start, annotation.end);
|
|
1133
|
+
return point.x >= bounds.x && point.x <= bounds.x + bounds.width && point.y >= bounds.y && point.y <= bounds.y + bounds.height;
|
|
1134
|
+
}
|
|
1135
|
+
function useTextAnnotation(options) {
|
|
1136
|
+
const [textDraft, setTextDraft] = useState(null);
|
|
1137
|
+
const beginText = (canvas, point) => {
|
|
1138
|
+
const bounds = canvas.getBoundingClientRect();
|
|
1139
|
+
const existing = [...options.annotations].reverse().find((item) => item.tool === "text" && containsPoint(item, point));
|
|
1140
|
+
const annotation = existing ?? {
|
|
1141
|
+
color: options.color,
|
|
1142
|
+
end: point,
|
|
1143
|
+
id: crypto.randomUUID(),
|
|
1144
|
+
start: point,
|
|
1145
|
+
tool: "text"
|
|
1146
|
+
};
|
|
1147
|
+
const fontSize = existing ? (existing.end.y - existing.start.y) / (existing.text?.split("\n").length ?? 1) / 1.4 : 18 * canvas.width / bounds.width;
|
|
1148
|
+
setTextDraft({
|
|
1149
|
+
annotation,
|
|
1150
|
+
fontSize,
|
|
1151
|
+
position: {
|
|
1152
|
+
x: bounds.x + annotation.start.x / canvas.width * bounds.width,
|
|
1153
|
+
y: bounds.y + annotation.start.y / canvas.height * bounds.height
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
};
|
|
1157
|
+
const commitText = (canvas, text) => {
|
|
1158
|
+
const context = canvas.getContext("2d");
|
|
1159
|
+
if (!(textDraft && context)) return;
|
|
1160
|
+
const value = text.trim();
|
|
1161
|
+
const remaining = options.annotations.filter(({ id }) => id !== textDraft.annotation.id);
|
|
1162
|
+
if (value) {
|
|
1163
|
+
context.font = `${textDraft.fontSize}px ui-sans-serif, system-ui, sans-serif`;
|
|
1164
|
+
const lines = value.split("\n");
|
|
1165
|
+
remaining.push({
|
|
1166
|
+
...textDraft.annotation,
|
|
1167
|
+
end: {
|
|
1168
|
+
x: textDraft.annotation.start.x + Math.max(...lines.map((line) => context.measureText(line).width)),
|
|
1169
|
+
y: textDraft.annotation.start.y + lines.length * textDraft.fontSize * 1.4
|
|
1170
|
+
},
|
|
1171
|
+
text: value
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
options.onChange(remaining);
|
|
1175
|
+
setTextDraft(null);
|
|
1176
|
+
};
|
|
1177
|
+
return {
|
|
1178
|
+
beginText,
|
|
1179
|
+
cancelText: () => setTextDraft(null),
|
|
1180
|
+
commitText,
|
|
1181
|
+
textDraft
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
//#endregion
|
|
1185
|
+
//#region src/feedback/ui/floating/use-details-popover.ts
|
|
1186
|
+
function useDetailsPopover() {
|
|
1187
|
+
const [open, setOpen] = useState(false);
|
|
1188
|
+
const [placement, setPlacement] = useState("above");
|
|
1189
|
+
const [anchorLeft, setAnchorLeft] = useState(0);
|
|
1190
|
+
const detailsRef = useRef(null);
|
|
1191
|
+
const summaryRef = useRef(null);
|
|
1192
|
+
useEffect(() => {
|
|
1193
|
+
const reposition = () => {
|
|
1194
|
+
const bounds = summaryRef.current?.getBoundingClientRect();
|
|
1195
|
+
if (bounds && detailsRef.current?.open) {
|
|
1196
|
+
setAnchorLeft(bounds.left);
|
|
1197
|
+
setPlacement(bounds.bottom < window.innerHeight / 2 ? "below" : "above");
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
const dismissOutside = (event) => {
|
|
1201
|
+
const details = detailsRef.current;
|
|
1202
|
+
if (details?.open && !event.composedPath().includes(details)) setOpen(false);
|
|
1203
|
+
};
|
|
1204
|
+
const dismissWithEscape = (event) => {
|
|
1205
|
+
if (event.key !== "Escape" || !detailsRef.current?.open) return;
|
|
1206
|
+
event.preventDefault();
|
|
1207
|
+
event.stopPropagation();
|
|
1208
|
+
setOpen(false);
|
|
1209
|
+
summaryRef.current?.focus();
|
|
1210
|
+
};
|
|
1211
|
+
document.addEventListener("pointerdown", dismissOutside);
|
|
1212
|
+
document.addEventListener("keydown", dismissWithEscape, true);
|
|
1213
|
+
window.addEventListener("resize", reposition);
|
|
1214
|
+
return () => {
|
|
1215
|
+
document.removeEventListener("pointerdown", dismissOutside);
|
|
1216
|
+
document.removeEventListener("keydown", dismissWithEscape, true);
|
|
1217
|
+
window.removeEventListener("resize", reposition);
|
|
1218
|
+
};
|
|
1219
|
+
}, []);
|
|
1220
|
+
const show = () => {
|
|
1221
|
+
const bounds = summaryRef.current?.getBoundingClientRect();
|
|
1222
|
+
if (bounds) {
|
|
1223
|
+
setAnchorLeft(bounds.left);
|
|
1224
|
+
setPlacement(bounds.bottom < window.innerHeight / 2 ? "below" : "above");
|
|
1225
|
+
}
|
|
1226
|
+
setOpen(true);
|
|
1227
|
+
};
|
|
1228
|
+
return {
|
|
1229
|
+
anchorLeft,
|
|
1230
|
+
detailsProps: {
|
|
1231
|
+
"data-placement": placement,
|
|
1232
|
+
open,
|
|
1233
|
+
ref: detailsRef
|
|
1234
|
+
},
|
|
1235
|
+
hide: () => {
|
|
1236
|
+
setOpen(false);
|
|
1237
|
+
summaryRef.current?.focus();
|
|
1238
|
+
},
|
|
1239
|
+
show,
|
|
1240
|
+
summaryProps: {
|
|
1241
|
+
onClick: (event) => {
|
|
1242
|
+
event.preventDefault();
|
|
1243
|
+
if (open) setOpen(false);
|
|
1244
|
+
else show();
|
|
1245
|
+
},
|
|
1246
|
+
ref: summaryRef
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
//#endregion
|
|
1251
|
+
//#region src/feedback/ui/annotation/color-picker.tsx
|
|
1252
|
+
function ColorSpectrum({ selection, onChange }) {
|
|
1253
|
+
const inputRef = useRef(null);
|
|
1254
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1255
|
+
className: "color-spectrum",
|
|
1256
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
1257
|
+
"aria-label": "Custom color",
|
|
1258
|
+
onClick: () => inputRef.current?.click(),
|
|
1259
|
+
title: "Custom color",
|
|
1260
|
+
type: "button"
|
|
1261
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
1262
|
+
"aria-hidden": "true",
|
|
1263
|
+
onChange: (event) => onChange({
|
|
1264
|
+
baseColor: event.target.value,
|
|
1265
|
+
strengthPercent: 100
|
|
1266
|
+
}),
|
|
1267
|
+
ref: inputRef,
|
|
1268
|
+
tabIndex: -1,
|
|
1269
|
+
type: "color",
|
|
1270
|
+
value: selection.baseColor
|
|
1271
|
+
})]
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
function ColorControls({ selection, onChange }) {
|
|
1275
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1276
|
+
className: "color-popover glass",
|
|
1277
|
+
children: [/* @__PURE__ */ jsx(Slider.Root, {
|
|
1278
|
+
className: "color-slider",
|
|
1279
|
+
onValueChange: (strengthPercent) => onChange({
|
|
1280
|
+
...selection,
|
|
1281
|
+
strengthPercent
|
|
1282
|
+
}),
|
|
1283
|
+
value: selection.strengthPercent,
|
|
1284
|
+
children: /* @__PURE__ */ jsx(Slider.Control, {
|
|
1285
|
+
className: "color-slider-control",
|
|
1286
|
+
children: /* @__PURE__ */ jsx(Slider.Track, {
|
|
1287
|
+
className: "color-slider-track",
|
|
1288
|
+
children: /* @__PURE__ */ jsx(Slider.Thumb, {
|
|
1289
|
+
"aria-label": "Color strength",
|
|
1290
|
+
className: "color-slider-thumb",
|
|
1291
|
+
getAriaValueText: (_, value) => `${value}% color`
|
|
1292
|
+
})
|
|
1293
|
+
})
|
|
1294
|
+
})
|
|
1295
|
+
}), /* @__PURE__ */ jsx(ColorSpectrum, {
|
|
1296
|
+
onChange,
|
|
1297
|
+
selection
|
|
1298
|
+
})]
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
function ColorPicker({ selection, onChange }) {
|
|
1302
|
+
const popover = useDetailsPopover();
|
|
1303
|
+
const id = useId().replaceAll(":", "");
|
|
1304
|
+
return /* @__PURE__ */ jsxs("details", {
|
|
1305
|
+
className: "color-picker",
|
|
1306
|
+
id,
|
|
1307
|
+
...popover.detailsProps,
|
|
1308
|
+
children: [
|
|
1309
|
+
/* @__PURE__ */ jsx("style", { children: `[id="${id}"] { --rf-color-anchor: ${popover.anchorLeft}px; --annotation-color: ${annotationColorHex(selection)}; --annotation-base-color: ${selection.baseColor}; }` }),
|
|
1310
|
+
/* @__PURE__ */ jsx("summary", {
|
|
1311
|
+
"aria-label": "Annotation color",
|
|
1312
|
+
className: "tool",
|
|
1313
|
+
...popover.summaryProps,
|
|
1314
|
+
title: "Annotation color",
|
|
1315
|
+
children: /* @__PURE__ */ jsx("span", { className: "color-preview" })
|
|
1316
|
+
}),
|
|
1317
|
+
/* @__PURE__ */ jsx(ColorControls, {
|
|
1318
|
+
onChange,
|
|
1319
|
+
selection
|
|
1320
|
+
})
|
|
1321
|
+
]
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
//#endregion
|
|
1325
|
+
//#region src/feedback/ui/annotation/tool-menu.tsx
|
|
1326
|
+
const EXTRA_TOOLS = [
|
|
1327
|
+
{
|
|
1328
|
+
Icon: SpotlightIcon,
|
|
1329
|
+
id: "spotlight",
|
|
1330
|
+
label: "Spotlight"
|
|
1331
|
+
},
|
|
1332
|
+
{
|
|
1333
|
+
Icon: PencilIcon,
|
|
1334
|
+
id: "pen",
|
|
1335
|
+
label: "Draw"
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
Icon: HighlightIcon,
|
|
1339
|
+
id: "highlight",
|
|
1340
|
+
label: "Highlight"
|
|
1341
|
+
},
|
|
1342
|
+
{
|
|
1343
|
+
Icon: BlurIcon,
|
|
1344
|
+
id: "blur",
|
|
1345
|
+
label: "Hide"
|
|
1346
|
+
}
|
|
1347
|
+
];
|
|
1348
|
+
function DrawingToolMenu({ drawing, editor, labels }) {
|
|
1349
|
+
const popover = useDetailsPopover();
|
|
1350
|
+
return /* @__PURE__ */ jsxs("details", {
|
|
1351
|
+
className: "drawing-menu",
|
|
1352
|
+
...popover.detailsProps,
|
|
1353
|
+
children: [/* @__PURE__ */ jsx("summary", {
|
|
1354
|
+
"aria-label": "More drawing tools",
|
|
1355
|
+
className: "tool",
|
|
1356
|
+
"data-selected": EXTRA_TOOLS.some(({ id }) => id === drawing.tool),
|
|
1357
|
+
title: "More drawing tools",
|
|
1358
|
+
...popover.summaryProps,
|
|
1359
|
+
children: /* @__PURE__ */ jsx(ChevronDownIcon, {})
|
|
1360
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1361
|
+
className: "drawing-popover glass",
|
|
1362
|
+
children: [
|
|
1363
|
+
EXTRA_TOOLS.map(({ id, label, Icon }) => /* @__PURE__ */ jsxs("button", {
|
|
1364
|
+
"aria-pressed": drawing.tool === id,
|
|
1365
|
+
className: "drawing-option",
|
|
1366
|
+
onClick: () => {
|
|
1367
|
+
drawing.setTool(id);
|
|
1368
|
+
popover.hide();
|
|
1369
|
+
},
|
|
1370
|
+
type: "button",
|
|
1371
|
+
children: [/* @__PURE__ */ jsx(Icon, {}), label]
|
|
1372
|
+
}, id)),
|
|
1373
|
+
/* @__PURE__ */ jsx("div", { className: "drawing-divider" }),
|
|
1374
|
+
/* @__PURE__ */ jsxs("button", {
|
|
1375
|
+
className: "drawing-option",
|
|
1376
|
+
disabled: editor.annotations.length === 0,
|
|
1377
|
+
onClick: () => {
|
|
1378
|
+
editor.onChange([]);
|
|
1379
|
+
popover.hide();
|
|
1380
|
+
},
|
|
1381
|
+
type: "button",
|
|
1382
|
+
children: [/* @__PURE__ */ jsx(TrashIcon, {}), labels.clearAnnotations]
|
|
1383
|
+
}),
|
|
1384
|
+
editor.onRetake && /* @__PURE__ */ jsxs("button", {
|
|
1385
|
+
className: "drawing-option",
|
|
1386
|
+
onClick: editor.onRetake,
|
|
1387
|
+
type: "button",
|
|
1388
|
+
children: [/* @__PURE__ */ jsx(CameraIcon, {}), labels.recapture]
|
|
1389
|
+
})
|
|
1390
|
+
]
|
|
1391
|
+
})]
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
//#endregion
|
|
1395
|
+
//#region src/feedback/ui/annotation/toolbar.tsx
|
|
1396
|
+
const QUICK_TOOLS = [
|
|
1397
|
+
{
|
|
1398
|
+
Icon: SquareIcon,
|
|
1399
|
+
id: "rectangle",
|
|
1400
|
+
label: "Rectangle"
|
|
1401
|
+
},
|
|
1402
|
+
{
|
|
1403
|
+
Icon: ArrowIcon,
|
|
1404
|
+
id: "arrow",
|
|
1405
|
+
label: "Arrow"
|
|
1406
|
+
},
|
|
1407
|
+
{
|
|
1408
|
+
Icon: TextIcon,
|
|
1409
|
+
id: "text",
|
|
1410
|
+
label: "Text"
|
|
1411
|
+
}
|
|
1412
|
+
];
|
|
1413
|
+
function AnnotationToolbar({ editor, labels, drawing }) {
|
|
1414
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1415
|
+
className: "toolbar",
|
|
1416
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1417
|
+
className: "group glass",
|
|
1418
|
+
children: [QUICK_TOOLS.map(({ id, label, Icon }) => /* @__PURE__ */ jsx("button", {
|
|
1419
|
+
"aria-label": label,
|
|
1420
|
+
"aria-pressed": drawing.tool === id,
|
|
1421
|
+
className: "tool",
|
|
1422
|
+
onClick: () => drawing.setTool(id),
|
|
1423
|
+
title: label,
|
|
1424
|
+
type: "button",
|
|
1425
|
+
children: /* @__PURE__ */ jsx(Icon, {})
|
|
1426
|
+
}, id)), /* @__PURE__ */ jsx(DrawingToolMenu, {
|
|
1427
|
+
drawing,
|
|
1428
|
+
editor,
|
|
1429
|
+
labels
|
|
1430
|
+
})]
|
|
1431
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1432
|
+
className: "group glass",
|
|
1433
|
+
children: [
|
|
1434
|
+
/* @__PURE__ */ jsx(ColorPicker, {
|
|
1435
|
+
onChange: drawing.setColor,
|
|
1436
|
+
selection: drawing.color
|
|
1437
|
+
}),
|
|
1438
|
+
/* @__PURE__ */ jsx("button", {
|
|
1439
|
+
"aria-label": labels.undo,
|
|
1440
|
+
className: "tool",
|
|
1441
|
+
disabled: editor.annotations.length === 0,
|
|
1442
|
+
onClick: () => editor.onChange(editor.annotations.slice(0, -1)),
|
|
1443
|
+
title: labels.undo,
|
|
1444
|
+
type: "button",
|
|
1445
|
+
children: /* @__PURE__ */ jsx(UndoIcon, {})
|
|
1446
|
+
}),
|
|
1447
|
+
/* @__PURE__ */ jsx("button", {
|
|
1448
|
+
"aria-label": labels.done,
|
|
1449
|
+
className: "done-btn",
|
|
1450
|
+
onClick: editor.onDone,
|
|
1451
|
+
title: labels.done,
|
|
1452
|
+
type: "button",
|
|
1453
|
+
children: /* @__PURE__ */ jsx(CheckIcon, { size: 18 })
|
|
1454
|
+
})
|
|
1455
|
+
]
|
|
1456
|
+
})]
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
//#endregion
|
|
1460
|
+
//#region src/feedback/core/annotation-renderer.ts
|
|
1461
|
+
const STROKE_WIDTH = 3;
|
|
1462
|
+
const ARROW_HEAD_SIZE = 16;
|
|
1463
|
+
const HIGHLIGHT_ALPHA = .3;
|
|
1464
|
+
const REDACTION_FILL = "#111827";
|
|
1465
|
+
const PIXEL_BLOCK = 10;
|
|
1466
|
+
function scalePoint(point, scale) {
|
|
1467
|
+
return {
|
|
1468
|
+
x: point.x * scale,
|
|
1469
|
+
y: point.y * scale
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
function scaleRect(rect, scale) {
|
|
1473
|
+
return {
|
|
1474
|
+
height: rect.height * scale,
|
|
1475
|
+
width: rect.width * scale,
|
|
1476
|
+
x: rect.x * scale,
|
|
1477
|
+
y: rect.y * scale
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
function isCanvasElement(value) {
|
|
1481
|
+
return typeof HTMLCanvasElement !== "undefined" && value instanceof HTMLCanvasElement;
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Downsamples the region and paints it back enlarged, so redacted pixels are
|
|
1485
|
+
* unrecoverable. Falls back to an opaque block wherever 2d canvases are absent.
|
|
1486
|
+
*/
|
|
1487
|
+
function redact(context, rect) {
|
|
1488
|
+
const source = context.canvas;
|
|
1489
|
+
const width = Math.max(1, Math.round(rect.width / PIXEL_BLOCK));
|
|
1490
|
+
const height = Math.max(1, Math.round(rect.height / PIXEL_BLOCK));
|
|
1491
|
+
if (isCanvasElement(source)) {
|
|
1492
|
+
const offscreen = document.createElement("canvas");
|
|
1493
|
+
offscreen.width = width;
|
|
1494
|
+
offscreen.height = height;
|
|
1495
|
+
const offscreenContext = offscreen.getContext("2d");
|
|
1496
|
+
if (offscreenContext) {
|
|
1497
|
+
offscreenContext.imageSmoothingEnabled = true;
|
|
1498
|
+
offscreenContext.drawImage(source, rect.x, rect.y, rect.width, rect.height, 0, 0, width, height);
|
|
1499
|
+
context.drawImage(offscreen, rect.x, rect.y, rect.width, rect.height);
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
context.fillStyle = REDACTION_FILL;
|
|
1504
|
+
context.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
1505
|
+
}
|
|
1506
|
+
function drawArrow(context, start, end, scale) {
|
|
1507
|
+
context.beginPath();
|
|
1508
|
+
context.moveTo(start.x, start.y);
|
|
1509
|
+
context.lineTo(end.x, end.y);
|
|
1510
|
+
context.stroke();
|
|
1511
|
+
const [left, right] = arrowHead(start, end, ARROW_HEAD_SIZE * scale);
|
|
1512
|
+
context.beginPath();
|
|
1513
|
+
context.moveTo(end.x, end.y);
|
|
1514
|
+
context.lineTo(left.x, left.y);
|
|
1515
|
+
context.lineTo(right.x, right.y);
|
|
1516
|
+
context.closePath();
|
|
1517
|
+
context.fill();
|
|
1518
|
+
}
|
|
1519
|
+
function drawPen(context, points, scale) {
|
|
1520
|
+
const [first, ...rest] = points;
|
|
1521
|
+
if (!first) return;
|
|
1522
|
+
const head = scalePoint(first, scale);
|
|
1523
|
+
context.beginPath();
|
|
1524
|
+
context.moveTo(head.x, head.y);
|
|
1525
|
+
for (const point of rest) {
|
|
1526
|
+
const scaled = scalePoint(point, scale);
|
|
1527
|
+
context.lineTo(scaled.x, scaled.y);
|
|
1528
|
+
}
|
|
1529
|
+
context.stroke();
|
|
1530
|
+
}
|
|
1531
|
+
function drawSpotlight(context, rect) {
|
|
1532
|
+
const { width, height } = context.canvas;
|
|
1533
|
+
context.save();
|
|
1534
|
+
context.fillStyle = REDACTION_FILL;
|
|
1535
|
+
context.globalAlpha = .55;
|
|
1536
|
+
context.fillRect(0, 0, width, rect.y);
|
|
1537
|
+
context.fillRect(0, rect.y + rect.height, width, height - rect.y - rect.height);
|
|
1538
|
+
context.fillRect(0, rect.y, rect.x, rect.height);
|
|
1539
|
+
context.fillRect(rect.x + rect.width, rect.y, width - rect.x - rect.width, rect.height);
|
|
1540
|
+
context.restore();
|
|
1541
|
+
context.strokeRect(rect.x, rect.y, rect.width, rect.height);
|
|
1542
|
+
}
|
|
1543
|
+
function drawAnnotation(context, annotation, scale) {
|
|
1544
|
+
const { color, tool } = annotation;
|
|
1545
|
+
const rect = scaleRect(normalizeRect(annotation.start, annotation.end), scale);
|
|
1546
|
+
context.save();
|
|
1547
|
+
context.lineWidth = STROKE_WIDTH * scale;
|
|
1548
|
+
context.lineCap = "round";
|
|
1549
|
+
context.lineJoin = "round";
|
|
1550
|
+
context.strokeStyle = color;
|
|
1551
|
+
context.fillStyle = color;
|
|
1552
|
+
switch (tool) {
|
|
1553
|
+
case "text": {
|
|
1554
|
+
const lines = (annotation.text ?? "").split("\n");
|
|
1555
|
+
const lineHeight = rect.height / lines.length;
|
|
1556
|
+
context.font = `${lineHeight / 1.4}px ui-sans-serif, system-ui, sans-serif`;
|
|
1557
|
+
context.textBaseline = "top";
|
|
1558
|
+
for (const [index, line] of lines.entries()) context.fillText(line, rect.x, rect.y + index * lineHeight);
|
|
1559
|
+
break;
|
|
1560
|
+
}
|
|
1561
|
+
case "pen":
|
|
1562
|
+
drawPen(context, annotation.points ?? [], scale);
|
|
1563
|
+
break;
|
|
1564
|
+
case "arrow":
|
|
1565
|
+
drawArrow(context, scalePoint(annotation.start, scale), scalePoint(annotation.end, scale), scale);
|
|
1566
|
+
break;
|
|
1567
|
+
case "spotlight":
|
|
1568
|
+
drawSpotlight(context, rect);
|
|
1569
|
+
break;
|
|
1570
|
+
case "rectangle":
|
|
1571
|
+
context.strokeRect(rect.x, rect.y, rect.width, rect.height);
|
|
1572
|
+
break;
|
|
1573
|
+
case "highlight":
|
|
1574
|
+
context.globalAlpha = HIGHLIGHT_ALPHA;
|
|
1575
|
+
context.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
1576
|
+
break;
|
|
1577
|
+
default: redact(context, rect);
|
|
1578
|
+
}
|
|
1579
|
+
context.restore();
|
|
1580
|
+
}
|
|
1581
|
+
function drawAnnotations(context, annotations, options) {
|
|
1582
|
+
const scale = options?.scale ?? 1;
|
|
1583
|
+
for (const annotation of annotations) if (!isDegenerate(annotation)) drawAnnotation(context, annotation, scale);
|
|
1584
|
+
}
|
|
1585
|
+
function loadImage(url) {
|
|
1586
|
+
return new Promise((resolve, reject) => {
|
|
1587
|
+
const image = new Image();
|
|
1588
|
+
image.onload = () => resolve(image);
|
|
1589
|
+
image.onerror = () => reject(/* @__PURE__ */ new Error("Screenshot could not be decoded"));
|
|
1590
|
+
image.src = url;
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Burns the annotations into a copy of the capture so reviewers and agents see
|
|
1595
|
+
* the drawing without needing to replay the vector data.
|
|
1596
|
+
*/
|
|
1597
|
+
async function renderAnnotatedImage(source, annotations) {
|
|
1598
|
+
if (annotations.length === 0) return null;
|
|
1599
|
+
const canvas = document.createElement("canvas");
|
|
1600
|
+
canvas.width = source.width;
|
|
1601
|
+
canvas.height = source.height;
|
|
1602
|
+
const context = canvas.getContext("2d");
|
|
1603
|
+
if (!context) return null;
|
|
1604
|
+
const image = await loadImage(source.objectUrl);
|
|
1605
|
+
context.drawImage(image, 0, 0, source.width, source.height);
|
|
1606
|
+
drawAnnotations(context, annotations);
|
|
1607
|
+
return await capturedFromCanvas(canvas, source.width);
|
|
1608
|
+
}
|
|
1609
|
+
//#endregion
|
|
1610
|
+
//#region src/feedback/ui/annotation/use-annotation-canvas.ts
|
|
1611
|
+
function useAnnotationCanvas(capture, annotations) {
|
|
1612
|
+
const canvasRef = useRef(null);
|
|
1613
|
+
const [bitmap, setBitmap] = useState(null);
|
|
1614
|
+
const [failedUrl, setFailedUrl] = useState(null);
|
|
1615
|
+
useEffect(() => {
|
|
1616
|
+
const image = new Image();
|
|
1617
|
+
image.onload = () => setBitmap(image);
|
|
1618
|
+
image.onerror = () => setFailedUrl(capture.objectUrl);
|
|
1619
|
+
image.src = capture.objectUrl;
|
|
1620
|
+
return () => {
|
|
1621
|
+
image.onload = null;
|
|
1622
|
+
image.onerror = null;
|
|
1623
|
+
};
|
|
1624
|
+
}, [capture.objectUrl]);
|
|
1625
|
+
const isDecoded = bitmap?.src === capture.objectUrl;
|
|
1065
1626
|
useEffect(() => {
|
|
1066
1627
|
const canvas = canvasRef.current;
|
|
1067
1628
|
const context = canvas?.getContext("2d");
|
|
1068
|
-
if (!(canvas && context)) return;
|
|
1629
|
+
if (!(canvas && context && bitmap && isDecoded)) return;
|
|
1069
1630
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
1070
|
-
|
|
1071
|
-
drawAnnotations(context,
|
|
1631
|
+
context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
|
1632
|
+
drawAnnotations(context, annotations);
|
|
1072
1633
|
}, [
|
|
1073
1634
|
annotations,
|
|
1074
|
-
|
|
1075
|
-
|
|
1635
|
+
bitmap,
|
|
1636
|
+
isDecoded
|
|
1076
1637
|
]);
|
|
1638
|
+
return {
|
|
1639
|
+
canvasRef,
|
|
1640
|
+
hasError: failedUrl === capture.objectUrl,
|
|
1641
|
+
isDecoded
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
//#endregion
|
|
1645
|
+
//#region src/feedback/ui/annotation/use-image-morph.ts
|
|
1646
|
+
function prefersReducedMotion() {
|
|
1647
|
+
return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
1648
|
+
}
|
|
1649
|
+
function thumbnailFrame(frame, trigger) {
|
|
1650
|
+
const destination = frame.getBoundingClientRect();
|
|
1651
|
+
const thumbnail = trigger.getBoundingClientRect();
|
|
1652
|
+
const scale = Math.max(thumbnail.width / destination.width, thumbnail.height / destination.height);
|
|
1653
|
+
const insetX = (destination.width - thumbnail.width / scale) / 2;
|
|
1654
|
+
return {
|
|
1655
|
+
clipPath: `inset(0px ${insetX}px ${destination.height - thumbnail.height / scale}px ${insetX}px round ${Number.parseFloat(getComputedStyle(trigger).borderRadius) / scale}px)`,
|
|
1656
|
+
transform: `translate(${thumbnail.x - destination.x - insetX * scale}px, ${thumbnail.y - destination.y}px) scale(${scale})`
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
function expandedFrame(visual) {
|
|
1660
|
+
return {
|
|
1661
|
+
clipPath: `inset(0px 0px 0px 0px round ${getComputedStyle(visual).borderRadius})`,
|
|
1662
|
+
transform: "none"
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
const TIMING = {
|
|
1666
|
+
duration: 340,
|
|
1667
|
+
easing: "cubic-bezier(0.22, 1, 0.36, 1)"
|
|
1668
|
+
};
|
|
1669
|
+
function useImageMorph(trigger, onDone) {
|
|
1670
|
+
const frameRef = useRef(null);
|
|
1671
|
+
const visualRef = useRef(null);
|
|
1672
|
+
const entrance = useRef(null);
|
|
1673
|
+
const exit = useRef(null);
|
|
1674
|
+
const [isOpening, setIsOpening] = useState(false);
|
|
1675
|
+
const [isClosing, setIsClosing] = useState(false);
|
|
1676
|
+
useLayoutEffect(() => {
|
|
1677
|
+
const frame = frameRef.current;
|
|
1678
|
+
const visual = visualRef.current;
|
|
1679
|
+
if (frame && visual?.animate && trigger && !prefersReducedMotion()) {
|
|
1680
|
+
setIsOpening(true);
|
|
1681
|
+
entrance.current = visual.animate([thumbnailFrame(frame, trigger), expandedFrame(visual)], TIMING);
|
|
1682
|
+
entrance.current.finished.then(() => setIsOpening(false), () => void 0);
|
|
1683
|
+
}
|
|
1684
|
+
return () => {
|
|
1685
|
+
entrance.current?.cancel();
|
|
1686
|
+
exit.current?.cancel();
|
|
1687
|
+
};
|
|
1688
|
+
}, [trigger]);
|
|
1689
|
+
const close = (action = onDone) => {
|
|
1690
|
+
if (exit.current) return;
|
|
1691
|
+
const frame = frameRef.current;
|
|
1692
|
+
const visual = visualRef.current;
|
|
1693
|
+
if (!(frame && visual?.animate && trigger) || prefersReducedMotion()) {
|
|
1694
|
+
action();
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
const current = entrance.current?.playState === "running" ? {
|
|
1698
|
+
clipPath: getComputedStyle(visual).clipPath,
|
|
1699
|
+
transform: getComputedStyle(visual).transform
|
|
1700
|
+
} : expandedFrame(visual);
|
|
1701
|
+
entrance.current?.cancel();
|
|
1702
|
+
setIsClosing(true);
|
|
1703
|
+
const animation = visual.animate([current, thumbnailFrame(frame, trigger)], {
|
|
1704
|
+
...TIMING,
|
|
1705
|
+
duration: 260,
|
|
1706
|
+
fill: "forwards"
|
|
1707
|
+
});
|
|
1708
|
+
exit.current = animation;
|
|
1709
|
+
animation.finished.then(action, () => void 0);
|
|
1710
|
+
};
|
|
1711
|
+
return {
|
|
1712
|
+
close,
|
|
1713
|
+
frameRef,
|
|
1714
|
+
isClosing,
|
|
1715
|
+
isOpening,
|
|
1716
|
+
visualRef
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
//#endregion
|
|
1720
|
+
//#region src/feedback/ui/annotator.tsx
|
|
1721
|
+
function toImagePoint(canvas, clientX, clientY) {
|
|
1722
|
+
const bounds = canvas.getBoundingClientRect();
|
|
1723
|
+
return {
|
|
1724
|
+
x: Math.max(0, Math.min(canvas.width, (clientX - bounds.left) / bounds.width * canvas.width)),
|
|
1725
|
+
y: Math.max(0, Math.min(canvas.height, (clientY - bounds.top) / bounds.height * canvas.height))
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
function Annotator({ capture, labels, editor }) {
|
|
1729
|
+
const { annotations, onChange, onDone, onRetake } = editor;
|
|
1730
|
+
const morph = useImageMorph(editor.trigger, onDone);
|
|
1731
|
+
const dialogRef = useRef(null);
|
|
1732
|
+
const [tool, setTool] = useState("rectangle");
|
|
1733
|
+
const [colorSelection, setColorSelection] = useState(DEFAULT_ANNOTATION_COLOR);
|
|
1734
|
+
const color = annotationColorHex(colorSelection);
|
|
1735
|
+
const [draft, setDraft] = useState(null);
|
|
1736
|
+
const { canvasRef, hasError, isDecoded } = useAnnotationCanvas(capture, draft ? [...annotations, draft] : annotations);
|
|
1737
|
+
const text = useTextAnnotation({
|
|
1738
|
+
annotations,
|
|
1739
|
+
color,
|
|
1740
|
+
onChange
|
|
1741
|
+
});
|
|
1742
|
+
useEffect(() => {
|
|
1743
|
+
const dialog = dialogRef.current;
|
|
1744
|
+
dialog?.showModal();
|
|
1745
|
+
return () => dialog?.close();
|
|
1746
|
+
}, []);
|
|
1077
1747
|
const onPointerDown = (event) => {
|
|
1078
1748
|
const canvas = canvasRef.current;
|
|
1079
|
-
if (!canvas) return;
|
|
1080
|
-
canvas.setPointerCapture(event.pointerId);
|
|
1749
|
+
if (!(canvas && isDecoded) || event.button !== 0) return;
|
|
1081
1750
|
const point = toImagePoint(canvas, event.clientX, event.clientY);
|
|
1082
|
-
|
|
1751
|
+
if (tool === "text") {
|
|
1752
|
+
event.preventDefault();
|
|
1753
|
+
text.beginText(canvas, point);
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
canvas.setPointerCapture(event.pointerId);
|
|
1083
1757
|
setDraft({
|
|
1084
1758
|
color,
|
|
1085
1759
|
end: point,
|
|
1086
|
-
id:
|
|
1760
|
+
id: crypto.randomUUID(),
|
|
1087
1761
|
points: tool === "pen" ? [point] : void 0,
|
|
1088
1762
|
start: point,
|
|
1089
1763
|
tool
|
|
@@ -1107,100 +1781,83 @@ function Annotator({ annotations, capture, labels, onChange, onDone }) {
|
|
|
1107
1781
|
if (!isDegenerate(draft)) onChange([...annotations, draft]);
|
|
1108
1782
|
setDraft(null);
|
|
1109
1783
|
};
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
if (event.key === "Escape") {
|
|
1113
|
-
event.preventDefault();
|
|
1114
|
-
onDone();
|
|
1115
|
-
}
|
|
1116
|
-
};
|
|
1117
|
-
document.addEventListener("keydown", onKeyDown);
|
|
1118
|
-
return () => document.removeEventListener("keydown", onKeyDown);
|
|
1119
|
-
}, [onDone]);
|
|
1120
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1784
|
+
return /* @__PURE__ */ jsxs("dialog", {
|
|
1785
|
+
"aria-label": labels.annotateHint,
|
|
1121
1786
|
className: "overlay",
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
children:
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
className: "group",
|
|
1161
|
-
children: [
|
|
1162
|
-
/* @__PURE__ */ jsx("button", {
|
|
1163
|
-
"aria-label": labels.undo,
|
|
1164
|
-
className: "tool",
|
|
1165
|
-
disabled: annotations.length === 0,
|
|
1166
|
-
onClick: () => onChange(annotations.slice(0, -1)),
|
|
1167
|
-
title: labels.undo,
|
|
1168
|
-
type: "button",
|
|
1169
|
-
children: /* @__PURE__ */ jsx(UndoIcon, {})
|
|
1170
|
-
}),
|
|
1171
|
-
/* @__PURE__ */ jsx("button", {
|
|
1172
|
-
"aria-label": labels.clearAnnotations,
|
|
1173
|
-
className: "tool",
|
|
1174
|
-
disabled: annotations.length === 0,
|
|
1175
|
-
onClick: () => onChange([]),
|
|
1176
|
-
title: labels.clearAnnotations,
|
|
1177
|
-
type: "button",
|
|
1178
|
-
children: /* @__PURE__ */ jsx(TrashIcon, {})
|
|
1179
|
-
}),
|
|
1180
|
-
/* @__PURE__ */ jsx("button", {
|
|
1181
|
-
className: "done-btn",
|
|
1182
|
-
onClick: onDone,
|
|
1183
|
-
type: "button",
|
|
1184
|
-
children: labels.done
|
|
1185
|
-
})
|
|
1186
|
-
]
|
|
1787
|
+
"data-closing": morph.isClosing,
|
|
1788
|
+
"data-opening": morph.isOpening,
|
|
1789
|
+
onCancel: (event) => {
|
|
1790
|
+
event.preventDefault();
|
|
1791
|
+
morph.close();
|
|
1792
|
+
},
|
|
1793
|
+
ref: dialogRef,
|
|
1794
|
+
children: [
|
|
1795
|
+
hasError && /* @__PURE__ */ jsx("p", {
|
|
1796
|
+
className: "error",
|
|
1797
|
+
role: "alert",
|
|
1798
|
+
children: labels.captureFailed
|
|
1799
|
+
}),
|
|
1800
|
+
/* @__PURE__ */ jsx("style", { children: `.editor-image { --rf-capture-ratio: ${capture.width / capture.height}; }` }),
|
|
1801
|
+
/* @__PURE__ */ jsx("div", {
|
|
1802
|
+
className: "editor",
|
|
1803
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1804
|
+
className: "editor-image",
|
|
1805
|
+
ref: morph.frameRef,
|
|
1806
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1807
|
+
className: "editor-visual",
|
|
1808
|
+
ref: morph.visualRef,
|
|
1809
|
+
children: [/* @__PURE__ */ jsx("img", {
|
|
1810
|
+
alt: "",
|
|
1811
|
+
height: capture.height,
|
|
1812
|
+
src: capture.objectUrl,
|
|
1813
|
+
width: capture.width
|
|
1814
|
+
}), /* @__PURE__ */ jsx("canvas", {
|
|
1815
|
+
"data-tool": tool,
|
|
1816
|
+
height: capture.height,
|
|
1817
|
+
onPointerCancel: () => setDraft(null),
|
|
1818
|
+
onPointerDown,
|
|
1819
|
+
onPointerMove,
|
|
1820
|
+
onPointerUp,
|
|
1821
|
+
ref: canvasRef,
|
|
1822
|
+
width: capture.width
|
|
1823
|
+
})]
|
|
1824
|
+
})
|
|
1187
1825
|
})
|
|
1188
|
-
|
|
1189
|
-
|
|
1826
|
+
}),
|
|
1827
|
+
text.textDraft && /* @__PURE__ */ jsx(TextAnnotationEditor, {
|
|
1828
|
+
draft: text.textDraft,
|
|
1829
|
+
onCancel: text.cancelText,
|
|
1830
|
+
onCommit: (value) => {
|
|
1831
|
+
if (canvasRef.current) text.commitText(canvasRef.current, value);
|
|
1832
|
+
}
|
|
1833
|
+
}, text.textDraft.annotation.id),
|
|
1834
|
+
/* @__PURE__ */ jsx(AnnotationToolbar, {
|
|
1835
|
+
drawing: {
|
|
1836
|
+
color: colorSelection,
|
|
1837
|
+
setColor: setColorSelection,
|
|
1838
|
+
setTool,
|
|
1839
|
+
tool
|
|
1840
|
+
},
|
|
1841
|
+
editor: {
|
|
1842
|
+
annotations,
|
|
1843
|
+
onChange,
|
|
1844
|
+
onDone: () => morph.close(),
|
|
1845
|
+
onRetake: onRetake ? () => morph.close(onRetake) : void 0
|
|
1846
|
+
},
|
|
1847
|
+
labels
|
|
1848
|
+
})
|
|
1849
|
+
]
|
|
1190
1850
|
});
|
|
1191
1851
|
}
|
|
1192
1852
|
//#endregion
|
|
1193
1853
|
//#region src/feedback/ui/launcher.tsx
|
|
1194
|
-
|
|
1195
|
-
* Never changes size between states — the panel opening must not move the
|
|
1196
|
-
* button out from under the pointer.
|
|
1197
|
-
*/
|
|
1198
|
-
function Launcher({ isOpen, label, onClick }) {
|
|
1854
|
+
function Launcher({ isOpen, label, onClick, buttonRef }) {
|
|
1199
1855
|
return /* @__PURE__ */ jsxs("button", {
|
|
1200
1856
|
"aria-expanded": isOpen,
|
|
1201
|
-
className: "launcher",
|
|
1857
|
+
className: "launcher glass",
|
|
1202
1858
|
"data-open": isOpen,
|
|
1203
1859
|
onClick,
|
|
1860
|
+
ref: buttonRef,
|
|
1204
1861
|
type: "button",
|
|
1205
1862
|
children: [/* @__PURE__ */ jsxs("span", {
|
|
1206
1863
|
className: "launcher-icon",
|
|
@@ -1209,271 +1866,571 @@ function Launcher({ isOpen, label, onClick }) {
|
|
|
1209
1866
|
});
|
|
1210
1867
|
}
|
|
1211
1868
|
//#endregion
|
|
1212
|
-
//#region src/feedback/ui/
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
},
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
/* @__PURE__ */
|
|
1246
|
-
className: "
|
|
1247
|
-
children: /* @__PURE__ */ jsx("img", {
|
|
1248
|
-
alt: "",
|
|
1249
|
-
height: state.capture.height,
|
|
1250
|
-
src: state.capture.objectUrl,
|
|
1251
|
-
width: state.capture.width
|
|
1252
|
-
})
|
|
1253
|
-
}),
|
|
1254
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1255
|
-
className: "meta",
|
|
1256
|
-
children: [/* @__PURE__ */ jsx("strong", {
|
|
1257
|
-
className: "truncate",
|
|
1258
|
-
children: labels.retakeHint
|
|
1259
|
-
}), /* @__PURE__ */ jsxs("span", {
|
|
1260
|
-
className: "truncate",
|
|
1261
|
-
children: [
|
|
1262
|
-
state.capture.width,
|
|
1263
|
-
"×",
|
|
1264
|
-
state.capture.height,
|
|
1265
|
-
annotationCount > 0 ? ` · ${annotationCount} marks` : ""
|
|
1266
|
-
]
|
|
1267
|
-
})]
|
|
1268
|
-
}),
|
|
1269
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1270
|
-
className: "actions",
|
|
1869
|
+
//#region src/feedback/ui/annotation/screenshot-preview.tsx
|
|
1870
|
+
function ScreenshotPreview({ annotations, capture }) {
|
|
1871
|
+
const { canvasRef } = useAnnotationCanvas(capture, annotations);
|
|
1872
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("img", {
|
|
1873
|
+
alt: "",
|
|
1874
|
+
draggable: false,
|
|
1875
|
+
height: capture.height,
|
|
1876
|
+
src: capture.objectUrl,
|
|
1877
|
+
width: capture.width
|
|
1878
|
+
}), annotations.length > 0 && /* @__PURE__ */ jsx("canvas", {
|
|
1879
|
+
height: capture.height,
|
|
1880
|
+
ref: canvasRef,
|
|
1881
|
+
width: capture.width
|
|
1882
|
+
})] });
|
|
1883
|
+
}
|
|
1884
|
+
//#endregion
|
|
1885
|
+
//#region src/feedback/ui/composer/screenshot-card.tsx
|
|
1886
|
+
function ScreenshotCard({ item, labels, state }) {
|
|
1887
|
+
const { draft, number, multiple } = item;
|
|
1888
|
+
const isCapturing = state.pendingCapture?.id === draft.id;
|
|
1889
|
+
const disabled = state.isEditingDisabled || state.isCapturing;
|
|
1890
|
+
return /* @__PURE__ */ jsxs("fieldset", {
|
|
1891
|
+
"aria-label": `${labels.screenshot} ${number}`,
|
|
1892
|
+
className: "screenshot-attachment glass",
|
|
1893
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
1894
|
+
"aria-label": labels.annotateHint,
|
|
1895
|
+
className: "screenshot-preview",
|
|
1896
|
+
disabled,
|
|
1897
|
+
onClick: (event) => state.annotateScreenshot(draft.id, event.currentTarget),
|
|
1898
|
+
type: "button",
|
|
1899
|
+
children: [/* @__PURE__ */ jsx(ScreenshotPreview, {
|
|
1900
|
+
annotations: draft.annotations,
|
|
1901
|
+
capture: draft.image
|
|
1902
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
1903
|
+
className: "attachment-caption glass",
|
|
1271
1904
|
children: [
|
|
1272
|
-
/* @__PURE__ */ jsx(
|
|
1273
|
-
|
|
1274
|
-
className: "
|
|
1275
|
-
|
|
1276
|
-
title: labels.annotateHint,
|
|
1277
|
-
type: "button",
|
|
1278
|
-
children: /* @__PURE__ */ jsx(PencilIcon, {})
|
|
1905
|
+
/* @__PURE__ */ jsx(PencilIcon, {}),
|
|
1906
|
+
/* @__PURE__ */ jsx("span", {
|
|
1907
|
+
className: "attachment-caption-label",
|
|
1908
|
+
children: labels.screenshot
|
|
1279
1909
|
}),
|
|
1280
|
-
/* @__PURE__ */ jsx("
|
|
1281
|
-
|
|
1282
|
-
className: "
|
|
1283
|
-
|
|
1284
|
-
title: labels.recapture,
|
|
1285
|
-
type: "button",
|
|
1286
|
-
children: /* @__PURE__ */ jsx(CameraIcon, {})
|
|
1287
|
-
}),
|
|
1288
|
-
/* @__PURE__ */ jsx("button", {
|
|
1289
|
-
"aria-label": labels.removeScreenshot,
|
|
1290
|
-
className: "icon-btn",
|
|
1291
|
-
onClick: state.removeCapture,
|
|
1292
|
-
title: labels.removeScreenshot,
|
|
1293
|
-
type: "button",
|
|
1294
|
-
children: /* @__PURE__ */ jsx(TrashIcon, {})
|
|
1910
|
+
multiple && /* @__PURE__ */ jsx("span", { children: number }),
|
|
1911
|
+
draft.annotations.length > 0 && /* @__PURE__ */ jsx("span", {
|
|
1912
|
+
className: "annotation-count",
|
|
1913
|
+
children: draft.annotations.length
|
|
1295
1914
|
})
|
|
1296
1915
|
]
|
|
1297
|
-
})
|
|
1298
|
-
|
|
1916
|
+
})]
|
|
1917
|
+
}), isCapturing ? /* @__PURE__ */ jsxs("div", {
|
|
1918
|
+
className: "capture-progress glass",
|
|
1919
|
+
role: "status",
|
|
1920
|
+
children: [/* @__PURE__ */ jsx("span", { className: "spinner" }), labels.capturing]
|
|
1921
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
1922
|
+
className: "attachment-actions glass",
|
|
1923
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
1924
|
+
"aria-label": labels.recapture,
|
|
1925
|
+
className: "icon-btn",
|
|
1926
|
+
disabled,
|
|
1927
|
+
onClick: () => state.retakeCapture(draft.id),
|
|
1928
|
+
title: labels.recapture,
|
|
1929
|
+
type: "button",
|
|
1930
|
+
children: /* @__PURE__ */ jsx(CameraIcon, {})
|
|
1931
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1932
|
+
"aria-label": labels.removeScreenshot,
|
|
1933
|
+
className: "icon-btn",
|
|
1934
|
+
disabled,
|
|
1935
|
+
onClick: () => state.removeCapture(draft.id),
|
|
1936
|
+
title: labels.removeScreenshot,
|
|
1937
|
+
type: "button",
|
|
1938
|
+
children: /* @__PURE__ */ jsx(TrashIcon, {})
|
|
1939
|
+
})]
|
|
1940
|
+
})]
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
//#endregion
|
|
1944
|
+
//#region src/feedback/ui/composer/attachments.tsx
|
|
1945
|
+
function SelectedElement({ labels, state }) {
|
|
1946
|
+
if (!state.selection) return null;
|
|
1947
|
+
const component = state.selection.componentStack[0];
|
|
1948
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1949
|
+
className: "selection-chip glass",
|
|
1950
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
1951
|
+
"aria-label": labels.pickElement,
|
|
1952
|
+
className: "selection-label",
|
|
1953
|
+
disabled: state.isEditingDisabled || state.isCapturing,
|
|
1954
|
+
onClick: () => state.setStep("picking"),
|
|
1955
|
+
title: state.selection.sourceLocation ?? state.selection.selector,
|
|
1956
|
+
type: "button",
|
|
1957
|
+
children: [state.elementCapture ? /* @__PURE__ */ jsx("img", {
|
|
1958
|
+
alt: "",
|
|
1959
|
+
height: 32,
|
|
1960
|
+
src: state.elementCapture.objectUrl,
|
|
1961
|
+
width: 32
|
|
1962
|
+
}) : /* @__PURE__ */ jsx(TargetIcon, {}), /* @__PURE__ */ jsx("span", {
|
|
1963
|
+
className: "truncate",
|
|
1964
|
+
children: component ? `<${component}>` : state.selection.label
|
|
1965
|
+
})]
|
|
1966
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1967
|
+
"aria-label": labels.clearSelection,
|
|
1968
|
+
className: "icon-btn",
|
|
1969
|
+
disabled: state.isEditingDisabled || state.isCapturing,
|
|
1970
|
+
onClick: state.clearSelection,
|
|
1971
|
+
type: "button",
|
|
1972
|
+
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
1973
|
+
})]
|
|
1299
1974
|
});
|
|
1300
1975
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
const
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
});
|
|
1310
|
-
const component = selection.componentStack[0];
|
|
1976
|
+
function Attachments({ labels, state }) {
|
|
1977
|
+
const stripRef = useRef(null);
|
|
1978
|
+
const count = state.screenshots.length;
|
|
1979
|
+
useEffect(() => {
|
|
1980
|
+
const strip = stripRef.current;
|
|
1981
|
+
if (strip && count > 0) strip.scrollTo?.({ left: strip.scrollWidth });
|
|
1982
|
+
}, [count]);
|
|
1983
|
+
const isAdding = state.pendingCapture && !state.screenshots.some(({ id }) => id === state.pendingCapture?.id);
|
|
1311
1984
|
return /* @__PURE__ */ jsxs("div", {
|
|
1312
|
-
className: "
|
|
1313
|
-
children: [
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1985
|
+
className: "attachments",
|
|
1986
|
+
children: [/* @__PURE__ */ jsx(SelectedElement, {
|
|
1987
|
+
labels,
|
|
1988
|
+
state
|
|
1989
|
+
}), /* @__PURE__ */ jsxs(ScrollArea.Root, {
|
|
1990
|
+
className: "screenshot-scroll-area",
|
|
1991
|
+
children: [/* @__PURE__ */ jsx(ScrollArea.Viewport, {
|
|
1992
|
+
"aria-label": labels.screenshot,
|
|
1993
|
+
className: "screenshot-strip",
|
|
1994
|
+
ref: stripRef,
|
|
1995
|
+
role: "region",
|
|
1996
|
+
children: /* @__PURE__ */ jsxs(ScrollArea.Content, {
|
|
1997
|
+
className: "screenshot-strip-content",
|
|
1998
|
+
children: [state.screenshots.map((draft, index) => /* @__PURE__ */ jsx(ScreenshotCard, {
|
|
1999
|
+
item: {
|
|
2000
|
+
draft,
|
|
2001
|
+
multiple: count > 1,
|
|
2002
|
+
number: index + 1
|
|
2003
|
+
},
|
|
2004
|
+
labels,
|
|
2005
|
+
state
|
|
2006
|
+
}, draft.id)), isAdding && /* @__PURE__ */ jsxs("div", {
|
|
2007
|
+
"aria-label": labels.capturing,
|
|
2008
|
+
className: "attachment-skeleton glass",
|
|
2009
|
+
role: "status",
|
|
2010
|
+
children: [/* @__PURE__ */ jsx(CameraIcon, {}), /* @__PURE__ */ jsx("span", { children: labels.capturing })]
|
|
2011
|
+
})]
|
|
1321
2012
|
})
|
|
1322
|
-
}),
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
children:
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
2013
|
+
}), /* @__PURE__ */ jsx(ScrollArea.Scrollbar, {
|
|
2014
|
+
className: "screenshot-scrollbar",
|
|
2015
|
+
orientation: "horizontal",
|
|
2016
|
+
children: /* @__PURE__ */ jsx(ScrollArea.Thumb, { className: "screenshot-scroll-thumb" })
|
|
2017
|
+
})]
|
|
2018
|
+
})]
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
//#endregion
|
|
2022
|
+
//#region src/feedback/ui/composer/options.tsx
|
|
2023
|
+
function ComposerOptions({ labels, state }) {
|
|
2024
|
+
const popover = useDetailsPopover();
|
|
2025
|
+
const [emailInvalid, setEmailInvalid] = useState(false);
|
|
2026
|
+
return /* @__PURE__ */ jsxs("details", {
|
|
2027
|
+
className: "composer-options",
|
|
2028
|
+
...popover.detailsProps,
|
|
2029
|
+
children: [/* @__PURE__ */ jsx("summary", {
|
|
2030
|
+
"aria-label": labels.moreOptions,
|
|
2031
|
+
className: "icon-btn",
|
|
2032
|
+
...popover.summaryProps,
|
|
2033
|
+
title: labels.moreOptions,
|
|
2034
|
+
children: /* @__PURE__ */ jsx(MoreIcon, {})
|
|
2035
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2036
|
+
className: "options-popover glass",
|
|
2037
|
+
children: [
|
|
2038
|
+
state.isAnonymous && /* @__PURE__ */ jsxs("label", {
|
|
2039
|
+
className: "email-field",
|
|
2040
|
+
children: [
|
|
2041
|
+
labels.emailLabel,
|
|
2042
|
+
/* @__PURE__ */ jsx("input", {
|
|
2043
|
+
"aria-invalid": emailInvalid,
|
|
2044
|
+
"aria-label": labels.emailLabel,
|
|
2045
|
+
autoComplete: "email",
|
|
2046
|
+
disabled: state.isEditingDisabled,
|
|
2047
|
+
onChange: (event) => {
|
|
2048
|
+
setEmailInvalid(false);
|
|
2049
|
+
state.setEmail(event.target.value);
|
|
2050
|
+
},
|
|
2051
|
+
onInvalid: (event) => {
|
|
2052
|
+
event.preventDefault();
|
|
2053
|
+
setEmailInvalid(true);
|
|
2054
|
+
popover.show();
|
|
2055
|
+
const input = event.currentTarget;
|
|
2056
|
+
requestAnimationFrame(() => input.focus());
|
|
2057
|
+
},
|
|
2058
|
+
placeholder: labels.emailPlaceholder,
|
|
2059
|
+
type: "email",
|
|
2060
|
+
value: state.email
|
|
2061
|
+
}),
|
|
2062
|
+
emailInvalid && /* @__PURE__ */ jsx("span", {
|
|
2063
|
+
role: "alert",
|
|
2064
|
+
children: labels.emailInvalid
|
|
2065
|
+
})
|
|
2066
|
+
]
|
|
2067
|
+
}),
|
|
2068
|
+
/* @__PURE__ */ jsxs("button", {
|
|
2069
|
+
className: "ghost-btn",
|
|
2070
|
+
disabled: state.isSubmitting,
|
|
2071
|
+
onClick: state.close,
|
|
2072
|
+
type: "button",
|
|
2073
|
+
children: [/* @__PURE__ */ jsx(CloseIcon, {}), labels.cancel]
|
|
2074
|
+
}),
|
|
2075
|
+
state.canDismiss && /* @__PURE__ */ jsx("button", {
|
|
2076
|
+
className: "dismiss-btn",
|
|
2077
|
+
disabled: state.isSubmitting,
|
|
2078
|
+
onClick: state.dismiss,
|
|
2079
|
+
type: "button",
|
|
2080
|
+
children: labels.dismissForDays.replace("{days}", String(state.dismissForDays))
|
|
2081
|
+
}),
|
|
2082
|
+
/* @__PURE__ */ jsxs("a", {
|
|
2083
|
+
className: "version-info",
|
|
2084
|
+
href: "https://reflet.app",
|
|
2085
|
+
rel: "noopener noreferrer",
|
|
2086
|
+
target: "_blank",
|
|
2087
|
+
children: ["Reflet SDK ", SDK_VERSION]
|
|
2088
|
+
})
|
|
2089
|
+
]
|
|
2090
|
+
})]
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
//#endregion
|
|
2094
|
+
//#region src/feedback/ui/composer/controls.tsx
|
|
2095
|
+
const CATEGORY_LABELS = {
|
|
2096
|
+
bug: "categoryBug",
|
|
2097
|
+
idea: "categoryIdea",
|
|
2098
|
+
question: "categoryQuestion"
|
|
2099
|
+
};
|
|
2100
|
+
function ComposerControls({ options, labels, state }) {
|
|
2101
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2102
|
+
className: "composer-toolbar",
|
|
2103
|
+
children: [options.floatingControls, /* @__PURE__ */ jsxs("div", {
|
|
2104
|
+
className: "composer-controls glass",
|
|
2105
|
+
children: [
|
|
2106
|
+
options.categories.length > 1 && /* @__PURE__ */ jsxs("span", {
|
|
2107
|
+
className: "select-control",
|
|
2108
|
+
children: [/* @__PURE__ */ jsx("select", {
|
|
2109
|
+
"aria-label": labels.categoryLabel,
|
|
2110
|
+
className: "category-select",
|
|
2111
|
+
disabled: state.isEditingDisabled,
|
|
2112
|
+
onChange: (event) => {
|
|
2113
|
+
const category = options.categories.find((value) => value === event.target.value);
|
|
2114
|
+
if (category) state.setCategory(category);
|
|
2115
|
+
},
|
|
2116
|
+
value: state.category,
|
|
2117
|
+
children: options.categories.map((value) => /* @__PURE__ */ jsx("option", {
|
|
2118
|
+
value,
|
|
2119
|
+
children: labels[CATEGORY_LABELS[value]]
|
|
2120
|
+
}, value))
|
|
2121
|
+
}), /* @__PURE__ */ jsx(ChevronDownIcon, {})]
|
|
2122
|
+
}),
|
|
2123
|
+
/* @__PURE__ */ jsx("button", {
|
|
2124
|
+
"aria-label": labels.attachScreenshot,
|
|
2125
|
+
className: "icon-btn",
|
|
2126
|
+
disabled: state.isCapturing || state.isEditingDisabled,
|
|
2127
|
+
onClick: state.takeCapture,
|
|
2128
|
+
title: labels.attachScreenshot,
|
|
2129
|
+
type: "button",
|
|
2130
|
+
children: /* @__PURE__ */ jsx(CameraIcon, {})
|
|
2131
|
+
}),
|
|
2132
|
+
/* @__PURE__ */ jsx("button", {
|
|
1336
2133
|
"aria-label": labels.pickElement,
|
|
1337
2134
|
className: "icon-btn",
|
|
2135
|
+
disabled: state.isEditingDisabled || state.isCapturing,
|
|
1338
2136
|
onClick: () => state.setStep("picking"),
|
|
1339
2137
|
title: labels.pickElement,
|
|
1340
2138
|
type: "button",
|
|
1341
2139
|
children: /* @__PURE__ */ jsx(TargetIcon, {})
|
|
1342
|
-
}),
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
2140
|
+
}),
|
|
2141
|
+
/* @__PURE__ */ jsx(ComposerOptions, {
|
|
2142
|
+
labels,
|
|
2143
|
+
state
|
|
2144
|
+
})
|
|
2145
|
+
]
|
|
2146
|
+
})]
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
//#endregion
|
|
2150
|
+
//#region src/feedback/ui/composer/message-input.tsx
|
|
2151
|
+
function MessageInput({ labels, state }) {
|
|
2152
|
+
const [expanded, setExpanded] = useState(false);
|
|
2153
|
+
const submitLabel = state.hasPendingAttachments ? labels.retryAttachments : labels.submit;
|
|
2154
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2155
|
+
className: "message-row glass",
|
|
2156
|
+
"data-expanded": expanded,
|
|
2157
|
+
children: [/* @__PURE__ */ jsx("textarea", {
|
|
2158
|
+
"aria-label": labels.descriptionPlaceholder,
|
|
2159
|
+
disabled: state.isEditingDisabled,
|
|
2160
|
+
maxLength: 4e3,
|
|
2161
|
+
onChange: (event) => state.setMessage(event.target.value),
|
|
2162
|
+
onFocus: () => setExpanded(true),
|
|
2163
|
+
placeholder: labels.descriptionPlaceholder,
|
|
2164
|
+
rows: expanded ? 3 : 1,
|
|
2165
|
+
value: state.message
|
|
2166
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
2167
|
+
"aria-busy": state.isSubmitting,
|
|
2168
|
+
"aria-label": submitLabel,
|
|
2169
|
+
className: "submit",
|
|
2170
|
+
disabled: state.isSubmitting || state.isCapturing || !state.message.trim(),
|
|
2171
|
+
title: submitLabel,
|
|
2172
|
+
type: "submit",
|
|
2173
|
+
children: state.isSubmitting ? /* @__PURE__ */ jsx("span", { className: "spinner" }) : /* @__PURE__ */ jsx(ArrowIcon, {})
|
|
2174
|
+
})]
|
|
1352
2175
|
});
|
|
1353
2176
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
2177
|
+
//#endregion
|
|
2178
|
+
//#region src/feedback/ui/panel.tsx
|
|
2179
|
+
function FeedbackPanel({ options, labels, state }) {
|
|
2180
|
+
const panelRef = useRef(null);
|
|
1356
2181
|
useEffect(() => {
|
|
1357
|
-
|
|
2182
|
+
panelRef.current?.focus();
|
|
1358
2183
|
}, []);
|
|
1359
|
-
if (state.step === "success") return /* @__PURE__ */
|
|
2184
|
+
if (state.step === "success") return /* @__PURE__ */ jsxs("section", {
|
|
1360
2185
|
"aria-live": "polite",
|
|
1361
2186
|
className: "panel",
|
|
1362
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
1363
|
-
className: "done",
|
|
2187
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2188
|
+
className: "done success-panel glass",
|
|
1364
2189
|
children: [
|
|
1365
2190
|
/* @__PURE__ */ jsx(CheckIcon, {}),
|
|
1366
2191
|
/* @__PURE__ */ jsx("h2", { children: labels.successTitle }),
|
|
1367
2192
|
/* @__PURE__ */ jsx("p", { children: labels.successMessage })
|
|
1368
2193
|
]
|
|
1369
|
-
})
|
|
2194
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2195
|
+
className: "composer-toolbar",
|
|
2196
|
+
children: options.floatingControls
|
|
2197
|
+
})]
|
|
1370
2198
|
});
|
|
1371
2199
|
return /* @__PURE__ */ jsxs("section", {
|
|
1372
2200
|
"aria-label": labels.title,
|
|
1373
2201
|
className: "panel",
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
2202
|
+
ref: panelRef,
|
|
2203
|
+
tabIndex: -1,
|
|
2204
|
+
children: [/* @__PURE__ */ jsx(Attachments, {
|
|
2205
|
+
labels,
|
|
2206
|
+
state
|
|
2207
|
+
}), /* @__PURE__ */ jsxs("form", {
|
|
2208
|
+
className: "composer",
|
|
2209
|
+
onSubmit: (event) => {
|
|
2210
|
+
event.preventDefault();
|
|
2211
|
+
state.submit();
|
|
2212
|
+
},
|
|
2213
|
+
children: [
|
|
2214
|
+
/* @__PURE__ */ jsx(MessageInput, {
|
|
2215
|
+
labels,
|
|
2216
|
+
state
|
|
2217
|
+
}),
|
|
2218
|
+
/* @__PURE__ */ jsx(ComposerControls, {
|
|
2219
|
+
labels,
|
|
2220
|
+
options,
|
|
2221
|
+
state
|
|
2222
|
+
}),
|
|
2223
|
+
/* @__PURE__ */ jsx("div", {
|
|
2224
|
+
"aria-hidden": "true",
|
|
2225
|
+
className: "hp",
|
|
2226
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
2227
|
+
autoComplete: "off",
|
|
2228
|
+
name: "company-website",
|
|
2229
|
+
onChange: (event) => state.setHoneypot(event.target.value),
|
|
2230
|
+
tabIndex: -1,
|
|
2231
|
+
value: state.honeypot
|
|
2232
|
+
})
|
|
2233
|
+
}),
|
|
2234
|
+
state.error && /* @__PURE__ */ jsx("p", {
|
|
2235
|
+
className: "error glass",
|
|
2236
|
+
role: "alert",
|
|
2237
|
+
children: state.error
|
|
2238
|
+
})
|
|
2239
|
+
]
|
|
2240
|
+
})]
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
//#endregion
|
|
2244
|
+
//#region src/feedback/ui/floating/use-floating-position.ts
|
|
2245
|
+
const EDGE_GAP = 12;
|
|
2246
|
+
const KEYBOARD_STEP = 24;
|
|
2247
|
+
const KEY_DIRECTIONS = {
|
|
2248
|
+
ArrowDown: {
|
|
2249
|
+
x: 0,
|
|
2250
|
+
y: KEYBOARD_STEP
|
|
2251
|
+
},
|
|
2252
|
+
ArrowLeft: {
|
|
2253
|
+
x: -24,
|
|
2254
|
+
y: 0
|
|
2255
|
+
},
|
|
2256
|
+
ArrowRight: {
|
|
2257
|
+
x: KEYBOARD_STEP,
|
|
2258
|
+
y: 0
|
|
2259
|
+
},
|
|
2260
|
+
ArrowUp: {
|
|
2261
|
+
x: 0,
|
|
2262
|
+
y: -24
|
|
2263
|
+
}
|
|
2264
|
+
};
|
|
2265
|
+
function visibleViewport() {
|
|
2266
|
+
const viewport = window.visualViewport;
|
|
2267
|
+
const left = viewport?.offsetLeft ?? 0;
|
|
2268
|
+
const top = viewport?.offsetTop ?? 0;
|
|
2269
|
+
const width = viewport?.width ?? window.innerWidth;
|
|
2270
|
+
const height = viewport?.height ?? window.innerHeight;
|
|
2271
|
+
return {
|
|
2272
|
+
bottom: window.innerHeight - top - height,
|
|
2273
|
+
height,
|
|
2274
|
+
left,
|
|
2275
|
+
right: window.innerWidth - left - width,
|
|
2276
|
+
top,
|
|
2277
|
+
width
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
function clampPosition(point, bounds, viewport = visibleViewport()) {
|
|
2281
|
+
return {
|
|
2282
|
+
x: Math.max(viewport.left + EDGE_GAP, Math.min(point.x, viewport.left + viewport.width - bounds.width - EDGE_GAP)),
|
|
2283
|
+
y: Math.max(viewport.top + EDGE_GAP, Math.min(point.y, viewport.top + viewport.height - bounds.height - EDGE_GAP))
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
function useFloatingPosition(corner = "bottom-right") {
|
|
2287
|
+
const rootRef = useRef(null);
|
|
2288
|
+
const gesture = useRef(null);
|
|
2289
|
+
const [position, setPosition] = useState(null);
|
|
2290
|
+
const [viewport, setViewport] = useState(visibleViewport);
|
|
2291
|
+
const horizontalEdge = corner.endsWith("right") ? "right" : "left";
|
|
2292
|
+
const verticalEdge = corner.startsWith("bottom") ? "bottom" : "top";
|
|
2293
|
+
const moveTo = useCallback((point, bounds) => {
|
|
2294
|
+
const fitted = clampPosition(point, bounds);
|
|
2295
|
+
const visible = visibleViewport();
|
|
2296
|
+
const anchor = {
|
|
2297
|
+
x: horizontalEdge === "right" ? visible.left + visible.width - fitted.x - bounds.width : fitted.x - visible.left,
|
|
2298
|
+
y: verticalEdge === "bottom" ? visible.top + visible.height - fitted.y - bounds.height : fitted.y - visible.top
|
|
2299
|
+
};
|
|
2300
|
+
setPosition((current) => current?.x === anchor.x && current.y === anchor.y ? current : anchor);
|
|
2301
|
+
}, [horizontalEdge, verticalEdge]);
|
|
2302
|
+
const keepInViewport = useCallback(() => {
|
|
2303
|
+
const bounds = rootRef.current?.getBoundingClientRect();
|
|
2304
|
+
if (!bounds) return;
|
|
2305
|
+
const fitted = clampPosition(bounds, bounds, viewport);
|
|
2306
|
+
if (Math.abs(fitted.x - bounds.x) > .5 || Math.abs(fitted.y - bounds.y) > .5) moveTo(fitted, bounds);
|
|
2307
|
+
}, [moveTo, viewport]);
|
|
2308
|
+
useLayoutEffect(() => {
|
|
2309
|
+
keepInViewport();
|
|
2310
|
+
}, [keepInViewport]);
|
|
2311
|
+
useEffect(() => {
|
|
2312
|
+
const resizeViewport = () => setViewport(visibleViewport());
|
|
2313
|
+
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(keepInViewport);
|
|
2314
|
+
if (rootRef.current) observer?.observe(rootRef.current);
|
|
2315
|
+
window.addEventListener("resize", resizeViewport);
|
|
2316
|
+
window.visualViewport?.addEventListener("resize", resizeViewport);
|
|
2317
|
+
window.visualViewport?.addEventListener("scroll", resizeViewport);
|
|
2318
|
+
return () => {
|
|
2319
|
+
observer?.disconnect();
|
|
2320
|
+
window.removeEventListener("resize", resizeViewport);
|
|
2321
|
+
window.visualViewport?.removeEventListener("resize", resizeViewport);
|
|
2322
|
+
window.visualViewport?.removeEventListener("scroll", resizeViewport);
|
|
2323
|
+
};
|
|
2324
|
+
}, [keepInViewport]);
|
|
2325
|
+
const onPointerDown = (event) => {
|
|
2326
|
+
const bounds = rootRef.current?.getBoundingClientRect();
|
|
2327
|
+
if (!bounds || event.button !== 0) return;
|
|
2328
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
2329
|
+
gesture.current = {
|
|
2330
|
+
x: event.clientX - bounds.x,
|
|
2331
|
+
y: event.clientY - bounds.y
|
|
2332
|
+
};
|
|
2333
|
+
};
|
|
2334
|
+
const onPointerMove = (event) => {
|
|
2335
|
+
const bounds = rootRef.current?.getBoundingClientRect();
|
|
2336
|
+
if (bounds && gesture.current) moveTo({
|
|
2337
|
+
x: event.clientX - gesture.current.x,
|
|
2338
|
+
y: event.clientY - gesture.current.y
|
|
2339
|
+
}, bounds);
|
|
2340
|
+
};
|
|
2341
|
+
const onKeyDown = (event) => {
|
|
2342
|
+
const direction = KEY_DIRECTIONS[event.key];
|
|
2343
|
+
const bounds = rootRef.current?.getBoundingClientRect();
|
|
2344
|
+
if (!(bounds && direction)) return;
|
|
2345
|
+
event.preventDefault();
|
|
2346
|
+
moveTo({
|
|
2347
|
+
x: bounds.x + direction.x,
|
|
2348
|
+
y: bounds.y + direction.y
|
|
2349
|
+
}, bounds);
|
|
2350
|
+
};
|
|
2351
|
+
const viewportStyles = `.root { --rf-viewport-width: ${viewport.width}px; --rf-viewport-top: ${viewport.top}px; --rf-viewport-bottom: ${viewport.bottom}px; --rf-viewport-left: ${viewport.left}px; --rf-viewport-right: ${viewport.right}px; }`;
|
|
2352
|
+
const anchorStyles = position ? `.root[data-moved="true"] { left: auto; right: auto; top: auto; bottom: auto; ${horizontalEdge}: ${position.x + viewport[horizontalEdge]}px; ${verticalEdge}: ${position.y + viewport[verticalEdge]}px; }` : "";
|
|
2353
|
+
return {
|
|
2354
|
+
handleProps: {
|
|
2355
|
+
onKeyDown,
|
|
2356
|
+
onPointerCancel: () => {
|
|
2357
|
+
gesture.current = null;
|
|
2358
|
+
},
|
|
2359
|
+
onPointerDown,
|
|
2360
|
+
onPointerMove,
|
|
2361
|
+
onPointerUp: () => {
|
|
2362
|
+
gesture.current = null;
|
|
2363
|
+
}
|
|
2364
|
+
},
|
|
2365
|
+
position,
|
|
2366
|
+
rootRef,
|
|
2367
|
+
styles: viewportStyles + anchorStyles
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
//#endregion
|
|
2371
|
+
//#region src/feedback/ui/floating/floating-widget.tsx
|
|
2372
|
+
function FloatingWidget({ labels, options, state }) {
|
|
2373
|
+
const [minimized, setMinimized] = useState(false);
|
|
2374
|
+
const launcherRef = useRef(null);
|
|
2375
|
+
const floating = useFloatingPosition(options.position);
|
|
2376
|
+
const showPanel = state.isOpen && !minimized;
|
|
2377
|
+
const minimize = () => {
|
|
2378
|
+
setMinimized(true);
|
|
2379
|
+
requestAnimationFrame(() => launcherRef.current?.focus());
|
|
2380
|
+
};
|
|
2381
|
+
useEffect(() => {
|
|
2382
|
+
if (state.step === "compose") state.annotationTrigger?.focus({ preventScroll: true });
|
|
2383
|
+
}, [state.step, state.annotationTrigger]);
|
|
2384
|
+
useEffect(() => {
|
|
2385
|
+
if (!showPanel || state.step !== "compose") return;
|
|
2386
|
+
const onKeyDown = (event) => {
|
|
2387
|
+
if (event.key === "Escape" && !event.defaultPrevented) {
|
|
2388
|
+
setMinimized(true);
|
|
2389
|
+
requestAnimationFrame(() => launcherRef.current?.focus());
|
|
2390
|
+
}
|
|
2391
|
+
};
|
|
2392
|
+
document.addEventListener("keydown", onKeyDown);
|
|
2393
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
2394
|
+
}, [showPanel, state.step]);
|
|
2395
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2396
|
+
className: "root",
|
|
2397
|
+
"data-editing": state.isOpen && (state.step === "picking" || state.step === "annotate"),
|
|
2398
|
+
"data-moved": Boolean(floating.position),
|
|
2399
|
+
"data-position": options.position,
|
|
2400
|
+
ref: floating.rootRef,
|
|
2401
|
+
children: [/* @__PURE__ */ jsx("style", { children: floating.styles }), showPanel ? /* @__PURE__ */ jsx(FeedbackPanel, {
|
|
2402
|
+
labels,
|
|
2403
|
+
options: {
|
|
2404
|
+
categories: options.categories,
|
|
2405
|
+
floatingControls: /* @__PURE__ */ jsxs("div", {
|
|
2406
|
+
className: "floating-controls glass",
|
|
2407
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
2408
|
+
"aria-label": labels.moveFeedback,
|
|
2409
|
+
className: "icon-btn drag-handle",
|
|
2410
|
+
title: labels.moveFeedback,
|
|
2411
|
+
type: "button",
|
|
2412
|
+
...floating.handleProps,
|
|
2413
|
+
children: /* @__PURE__ */ jsx(GripIcon, {})
|
|
2414
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
2415
|
+
"aria-label": labels.minimize,
|
|
1382
2416
|
className: "icon-btn",
|
|
1383
|
-
onClick:
|
|
2417
|
+
onClick: minimize,
|
|
2418
|
+
title: labels.minimize,
|
|
1384
2419
|
type: "button",
|
|
1385
|
-
children: /* @__PURE__ */ jsx(
|
|
1386
|
-
})
|
|
1387
|
-
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
const { icon: Icon, labelKey } = CATEGORY_META[value];
|
|
1400
|
-
return /* @__PURE__ */ jsxs("button", {
|
|
1401
|
-
"aria-pressed": state.category === value,
|
|
1402
|
-
onClick: () => state.setCategory(value),
|
|
1403
|
-
type: "button",
|
|
1404
|
-
children: [/* @__PURE__ */ jsx(Icon, {}), labels[labelKey]]
|
|
1405
|
-
}, value);
|
|
1406
|
-
})
|
|
1407
|
-
}),
|
|
1408
|
-
/* @__PURE__ */ jsx("textarea", {
|
|
1409
|
-
"aria-label": labels.descriptionPlaceholder,
|
|
1410
|
-
maxLength: 4e3,
|
|
1411
|
-
onChange: (event) => state.setMessage(event.target.value),
|
|
1412
|
-
placeholder: labels.descriptionPlaceholder,
|
|
1413
|
-
ref: messageRef,
|
|
1414
|
-
value: state.message
|
|
1415
|
-
}),
|
|
1416
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1417
|
-
className: "attachments",
|
|
1418
|
-
children: [/* @__PURE__ */ jsx(Attachment, {
|
|
1419
|
-
labels,
|
|
1420
|
-
state
|
|
1421
|
-
}), /* @__PURE__ */ jsx(Selection, {
|
|
1422
|
-
labels,
|
|
1423
|
-
state
|
|
1424
|
-
})]
|
|
1425
|
-
}),
|
|
1426
|
-
state.isAnonymous && /* @__PURE__ */ jsx("input", {
|
|
1427
|
-
"aria-label": labels.emailLabel,
|
|
1428
|
-
autoComplete: "email",
|
|
1429
|
-
onChange: (event) => state.setEmail(event.target.value),
|
|
1430
|
-
placeholder: labels.emailPlaceholder,
|
|
1431
|
-
type: "email",
|
|
1432
|
-
value: state.email
|
|
1433
|
-
}),
|
|
1434
|
-
/* @__PURE__ */ jsx("div", {
|
|
1435
|
-
"aria-hidden": "true",
|
|
1436
|
-
className: "hp",
|
|
1437
|
-
children: /* @__PURE__ */ jsx("input", {
|
|
1438
|
-
autoComplete: "off",
|
|
1439
|
-
name: "company-website",
|
|
1440
|
-
onChange: (event) => state.setHoneypot(event.target.value),
|
|
1441
|
-
tabIndex: -1,
|
|
1442
|
-
value: state.honeypot
|
|
1443
|
-
})
|
|
1444
|
-
}),
|
|
1445
|
-
state.error && /* @__PURE__ */ jsx("p", {
|
|
1446
|
-
className: "error",
|
|
1447
|
-
children: state.error
|
|
1448
|
-
}),
|
|
1449
|
-
/* @__PURE__ */ jsx("button", {
|
|
1450
|
-
className: "submit",
|
|
1451
|
-
disabled: state.isSubmitting || !state.message.trim(),
|
|
1452
|
-
type: "submit",
|
|
1453
|
-
children: state.isSubmitting ? /* @__PURE__ */ jsx("span", { className: "spinner" }) : labels.submit
|
|
1454
|
-
})
|
|
1455
|
-
]
|
|
1456
|
-
}),
|
|
1457
|
-
/* @__PURE__ */ jsxs("footer", {
|
|
1458
|
-
className: "footer",
|
|
1459
|
-
"data-dismissible": state.canDismiss,
|
|
1460
|
-
children: [state.canDismiss && /* @__PURE__ */ jsx("button", {
|
|
1461
|
-
className: "dismiss-btn",
|
|
1462
|
-
onClick: state.dismiss,
|
|
1463
|
-
type: "button",
|
|
1464
|
-
children: labels.dismissForDays.replace("{days}", String(state.dismissForDays))
|
|
1465
|
-
}), /* @__PURE__ */ jsxs("span", { children: [
|
|
1466
|
-
"Powered by",
|
|
1467
|
-
" ",
|
|
1468
|
-
/* @__PURE__ */ jsx("a", {
|
|
1469
|
-
href: "https://reflet.app",
|
|
1470
|
-
rel: "noopener noreferrer",
|
|
1471
|
-
target: "_blank",
|
|
1472
|
-
children: "Reflet"
|
|
1473
|
-
})
|
|
1474
|
-
] })]
|
|
1475
|
-
})
|
|
1476
|
-
]
|
|
2420
|
+
children: /* @__PURE__ */ jsx(MinusIcon, {})
|
|
2421
|
+
})]
|
|
2422
|
+
})
|
|
2423
|
+
},
|
|
2424
|
+
state
|
|
2425
|
+
}) : /* @__PURE__ */ jsx(Launcher, {
|
|
2426
|
+
buttonRef: launcherRef,
|
|
2427
|
+
isOpen: false,
|
|
2428
|
+
label: state.isOpen ? labels.resume : labels.trigger,
|
|
2429
|
+
onClick: () => {
|
|
2430
|
+
setMinimized(false);
|
|
2431
|
+
if (!state.isOpen) state.open();
|
|
2432
|
+
}
|
|
2433
|
+
})]
|
|
1477
2434
|
});
|
|
1478
2435
|
}
|
|
1479
2436
|
//#endregion
|
|
@@ -1580,29 +2537,30 @@ function ElementPicker({ hint, onCancel, onPick }) {
|
|
|
1580
2537
|
}
|
|
1581
2538
|
//#endregion
|
|
1582
2539
|
//#region src/feedback/ui/styles.ts
|
|
1583
|
-
/**
|
|
1584
|
-
* Styles for the feedback widget. Injected into a shadow root, so nothing here
|
|
1585
|
-
* leaks into the host app and nothing from the host app leaks in.
|
|
1586
|
-
*/
|
|
1587
2540
|
const WIDGET_STYLES = `
|
|
1588
2541
|
:host {
|
|
2542
|
+
--rf-offset: 20px;
|
|
1589
2543
|
--rf-bg: #ffffff;
|
|
1590
2544
|
--rf-bg-subtle: rgb(55 53 47 / 4%);
|
|
1591
2545
|
--rf-bg-hover: rgb(55 53 47 / 7%);
|
|
1592
2546
|
--rf-border: rgb(55 53 47 / 9%);
|
|
1593
2547
|
--rf-text: #37352f;
|
|
1594
|
-
--rf-text-muted: rgb(55 53 47 /
|
|
2548
|
+
--rf-text-muted: rgb(55 53 47 / 68%);
|
|
1595
2549
|
--rf-primary: #37352f;
|
|
1596
2550
|
--rf-primary-text: #ffffff;
|
|
1597
2551
|
--rf-accent: #2383e2;
|
|
1598
2552
|
--rf-danger: #d44c47;
|
|
1599
2553
|
--rf-success: #448361;
|
|
1600
|
-
--rf-radius:
|
|
2554
|
+
--rf-radius: 20px;
|
|
2555
|
+
--rf-pill: 999px;
|
|
2556
|
+
--rf-width: 360px;
|
|
2557
|
+
--rf-glass: rgb(255 255 255 / 72%);
|
|
2558
|
+
--rf-glass-highlight: rgb(255 255 255 / 90%);
|
|
2559
|
+
--rf-glass-stroke: rgb(255 255 255 / 70%);
|
|
1601
2560
|
--rf-radius-sm: 8px;
|
|
1602
2561
|
--rf-shadow-inset: inset 0 0 0 1px var(--rf-border);
|
|
1603
2562
|
--rf-shadow-low: 0 0 0 1px rgb(15 15 15 / 4%), 0 1px 2px rgb(15 15 15 / 6%), 0 4px 10px -4px rgb(15 15 15 / 10%);
|
|
1604
2563
|
--rf-shadow: 0 0 0 1px rgb(15 15 15 / 4%), 0 2px 5px -1px rgb(15 15 15 / 7%), 0 12px 32px -8px rgb(15 15 15 / 16%);
|
|
1605
|
-
--rf-ring: 0 0 0 3px color-mix(in srgb, var(--rf-accent) 20%, transparent);
|
|
1606
2564
|
--rf-ease: cubic-bezier(0.16, 1, 0.3, 1);
|
|
1607
2565
|
color-scheme: light;
|
|
1608
2566
|
all: initial;
|
|
@@ -1611,347 +2569,258 @@ const WIDGET_STYLES = `
|
|
|
1611
2569
|
|
|
1612
2570
|
:host([data-theme="dark"]) {
|
|
1613
2571
|
--rf-bg: #202020;
|
|
1614
|
-
--rf-bg-subtle: rgb(255 255 255 / 5%);
|
|
1615
|
-
--rf-bg-hover: rgb(255 255 255 / 9%);
|
|
1616
|
-
--rf-border: rgb(255 255 255 / 9%);
|
|
1617
|
-
--rf-text: #ebeae8;
|
|
1618
|
-
--rf-text-muted: rgb(255 255 255 /
|
|
1619
|
-
--rf-
|
|
1620
|
-
--rf-
|
|
1621
|
-
--rf-
|
|
1622
|
-
--rf-
|
|
1623
|
-
--rf-
|
|
1624
|
-
--rf-
|
|
1625
|
-
--rf-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
.root {
|
|
1632
|
-
position: fixed;
|
|
1633
|
-
z-index: 2147483000;
|
|
1634
|
-
display: flex;
|
|
1635
|
-
flex-direction: column;
|
|
1636
|
-
gap: 10px;
|
|
1637
|
-
font-family: inherit;
|
|
1638
|
-
font-size: 13.5px;
|
|
1639
|
-
line-height: 1.45;
|
|
1640
|
-
color: var(--rf-text);
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
.root[data-position$="right"] { right: var(--rf-offset); align-items: flex-end; }
|
|
1644
|
-
.root[data-position$="left"] { left: var(--rf-offset); align-items: flex-start; }
|
|
1645
|
-
.root[data-position^="bottom"] { bottom: var(--rf-offset); }
|
|
1646
|
-
.root[data-position^="top"] { top: var(--rf-offset); flex-direction: column-reverse; }
|
|
1647
|
-
|
|
1648
|
-
button {
|
|
1649
|
-
font: inherit;
|
|
1650
|
-
color: inherit;
|
|
1651
|
-
margin: 0;
|
|
1652
|
-
cursor: pointer;
|
|
1653
|
-
border: 0;
|
|
1654
|
-
background: none;
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
:focus-visible { outline: none; box-shadow: var(--rf-ring); }
|
|
1658
|
-
|
|
1659
|
-
.launcher {
|
|
1660
|
-
display: inline-flex;
|
|
1661
|
-
align-items: center;
|
|
1662
|
-
gap: 6px;
|
|
1663
|
-
height: 32px;
|
|
1664
|
-
padding: 0 11px;
|
|
1665
|
-
border-radius: var(--rf-radius-sm);
|
|
1666
|
-
background: var(--rf-primary);
|
|
1667
|
-
color: var(--rf-primary-text);
|
|
1668
|
-
font-size: 13px;
|
|
1669
|
-
font-weight: 500;
|
|
1670
|
-
letter-spacing: -0.005em;
|
|
1671
|
-
box-shadow: var(--rf-shadow-low);
|
|
1672
|
-
transition: transform 240ms var(--rf-ease), box-shadow 240ms var(--rf-ease), opacity 160ms var(--rf-ease);
|
|
1673
|
-
}
|
|
1674
|
-
.launcher:hover { transform: translateY(-1px); box-shadow: var(--rf-shadow); }
|
|
1675
|
-
.launcher:active { transform: translateY(0) scale(0.98); }
|
|
1676
|
-
|
|
1677
|
-
.launcher-icon { position: relative; width: 15px; height: 15px; flex: none; }
|
|
1678
|
-
.launcher-icon svg { position: absolute; inset: 0; width: 100%; height: 100%; transition: opacity 200ms var(--rf-ease), transform 320ms var(--rf-ease); }
|
|
1679
|
-
.launcher-icon .on-close { opacity: 0; transform: rotate(-90deg) scale(0.6); }
|
|
1680
|
-
.launcher[data-open="true"] .on-open { opacity: 0; transform: rotate(90deg) scale(0.6); }
|
|
1681
|
-
.launcher[data-open="true"] .on-close { opacity: 1; transform: none; }
|
|
1682
|
-
|
|
1683
|
-
.panel {
|
|
1684
|
-
width: min(352px, calc(100vw - 24px));
|
|
1685
|
-
max-height: min(600px, calc(100vh - 96px));
|
|
1686
|
-
display: flex;
|
|
1687
|
-
flex-direction: column;
|
|
1688
|
-
overflow: hidden;
|
|
1689
|
-
background: var(--rf-bg);
|
|
1690
|
-
border-radius: var(--rf-radius);
|
|
1691
|
-
box-shadow: var(--rf-shadow);
|
|
1692
|
-
animation: rf-in 220ms var(--rf-ease);
|
|
1693
|
-
}
|
|
1694
|
-
.root[data-position^="bottom"] .panel { transform-origin: bottom center; }
|
|
1695
|
-
.root[data-position^="top"] .panel { transform-origin: top center; animation-name: rf-in-top; }
|
|
1696
|
-
|
|
1697
|
-
@keyframes rf-in {
|
|
1698
|
-
from { opacity: 0; transform: translateY(6px) scale(0.985); }
|
|
1699
|
-
}
|
|
1700
|
-
@keyframes rf-in-top {
|
|
1701
|
-
from { opacity: 0; transform: translateY(-6px) scale(0.985); }
|
|
1702
|
-
}
|
|
1703
|
-
@keyframes rf-fade { from { opacity: 0; } }
|
|
1704
|
-
|
|
1705
|
-
@media (prefers-reduced-motion: reduce) {
|
|
1706
|
-
*, *::before, *::after { animation: none !important; transition: none !important; }
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
.head {
|
|
1710
|
-
display: flex;
|
|
1711
|
-
align-items: center;
|
|
1712
|
-
gap: 8px;
|
|
1713
|
-
padding: 12px 10px 4px 14px;
|
|
1714
|
-
}
|
|
1715
|
-
.head h2 { margin: 0; font-size: 13.5px; font-weight: 600; letter-spacing: -0.01em; }
|
|
1716
|
-
.head .spacer { flex: 1; }
|
|
1717
|
-
|
|
1718
|
-
.icon-btn {
|
|
1719
|
-
display: inline-flex;
|
|
1720
|
-
align-items: center;
|
|
1721
|
-
justify-content: center;
|
|
1722
|
-
width: 26px;
|
|
1723
|
-
height: 26px;
|
|
1724
|
-
flex: none;
|
|
1725
|
-
border-radius: 6px;
|
|
1726
|
-
color: var(--rf-text-muted);
|
|
1727
|
-
transition: background 120ms var(--rf-ease), color 120ms var(--rf-ease);
|
|
1728
|
-
}
|
|
1729
|
-
.icon-btn:hover { background: var(--rf-bg-hover); color: var(--rf-text); }
|
|
1730
|
-
|
|
1731
|
-
.body { display: flex; flex-direction: column; gap: 8px; padding: 6px 14px 14px; overflow-y: auto; }
|
|
1732
|
-
|
|
1733
|
-
.segmented {
|
|
1734
|
-
border: 0;
|
|
1735
|
-
margin: 0;
|
|
1736
|
-
min-inline-size: 0;
|
|
1737
|
-
display: grid;
|
|
1738
|
-
grid-auto-flow: column;
|
|
1739
|
-
grid-auto-columns: 1fr;
|
|
1740
|
-
gap: 2px;
|
|
1741
|
-
padding: 2px;
|
|
1742
|
-
background: var(--rf-bg-subtle);
|
|
1743
|
-
border-radius: var(--rf-radius-sm);
|
|
1744
|
-
}
|
|
1745
|
-
.segmented button {
|
|
1746
|
-
display: flex;
|
|
1747
|
-
align-items: center;
|
|
1748
|
-
justify-content: center;
|
|
1749
|
-
gap: 5px;
|
|
1750
|
-
height: 26px;
|
|
1751
|
-
border-radius: 6px;
|
|
1752
|
-
font-size: 12.5px;
|
|
1753
|
-
font-weight: 500;
|
|
1754
|
-
color: var(--rf-text-muted);
|
|
1755
|
-
transition: background 160ms var(--rf-ease), color 160ms var(--rf-ease);
|
|
1756
|
-
}
|
|
1757
|
-
.segmented button:hover { color: var(--rf-text); }
|
|
1758
|
-
.segmented button[aria-pressed="true"] {
|
|
1759
|
-
background: var(--rf-bg);
|
|
1760
|
-
color: var(--rf-text);
|
|
1761
|
-
box-shadow: var(--rf-shadow-low);
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
textarea, input[type="email"] {
|
|
1765
|
-
width: 100%;
|
|
1766
|
-
padding: 8px 10px;
|
|
1767
|
-
font: inherit;
|
|
1768
|
-
color: var(--rf-text);
|
|
1769
|
-
background: var(--rf-bg);
|
|
1770
|
-
border: 1px solid var(--rf-border);
|
|
1771
|
-
border-radius: var(--rf-radius-sm);
|
|
1772
|
-
transition: border-color 160ms var(--rf-ease), box-shadow 160ms var(--rf-ease);
|
|
1773
|
-
}
|
|
1774
|
-
textarea { min-height: 84px; max-height: 220px; resize: none; }
|
|
1775
|
-
textarea::placeholder, input::placeholder { color: var(--rf-text-muted); }
|
|
1776
|
-
textarea:focus, input:focus { outline: none; border-color: color-mix(in srgb, var(--rf-accent) 55%, transparent); box-shadow: var(--rf-ring); }
|
|
1777
|
-
|
|
1778
|
-
.attachments { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
1779
|
-
|
|
1780
|
-
.card {
|
|
1781
|
-
flex: 1 0 100%;
|
|
1782
|
-
display: flex;
|
|
1783
|
-
align-items: center;
|
|
1784
|
-
gap: 10px;
|
|
1785
|
-
min-width: 0;
|
|
1786
|
-
padding: 6px;
|
|
1787
|
-
border-radius: 10px;
|
|
1788
|
-
background: var(--rf-bg-subtle);
|
|
1789
|
-
animation: rf-fade 200ms var(--rf-ease);
|
|
1790
|
-
}
|
|
1791
|
-
.thumb {
|
|
1792
|
-
position: relative;
|
|
1793
|
-
flex: 0 0 60px;
|
|
1794
|
-
height: 42px;
|
|
1795
|
-
overflow: hidden;
|
|
1796
|
-
border-radius: 6px;
|
|
1797
|
-
background: var(--rf-bg);
|
|
1798
|
-
box-shadow: var(--rf-shadow-inset);
|
|
1799
|
-
}
|
|
1800
|
-
.thumb img { width: 100%; height: 100%; object-fit: cover; object-position: top center; display: block; }
|
|
1801
|
-
.thumb.contain img { object-fit: contain; object-position: center; }
|
|
1802
|
-
.card .meta { display: flex; flex-direction: column; justify-content: center; gap: 1px; min-width: 0; }
|
|
1803
|
-
.card .meta strong { font-size: 12.5px; font-weight: 550; }
|
|
1804
|
-
.card .meta span { font-size: 11.5px; color: var(--rf-text-muted); }
|
|
1805
|
-
.card .actions { display: flex; gap: 2px; margin-left: auto; align-items: center; }
|
|
1806
|
-
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1807
|
-
.card code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; }
|
|
1808
|
-
|
|
1809
|
-
.ghost-btn {
|
|
1810
|
-
flex: 0 0 auto;
|
|
1811
|
-
display: inline-flex;
|
|
1812
|
-
align-items: center;
|
|
1813
|
-
gap: 6px;
|
|
1814
|
-
height: 30px;
|
|
1815
|
-
padding: 0 10px;
|
|
1816
|
-
font-size: 12.5px;
|
|
1817
|
-
font-weight: 500;
|
|
1818
|
-
color: var(--rf-text-muted);
|
|
1819
|
-
border-radius: var(--rf-radius-sm);
|
|
1820
|
-
box-shadow: var(--rf-shadow-inset);
|
|
1821
|
-
transition: background 160ms var(--rf-ease), color 160ms var(--rf-ease);
|
|
2572
|
+
--rf-bg-subtle: rgb(255 255 255 / 5%);
|
|
2573
|
+
--rf-bg-hover: rgb(255 255 255 / 9%);
|
|
2574
|
+
--rf-border: rgb(255 255 255 / 9%);
|
|
2575
|
+
--rf-text: #ebeae8;
|
|
2576
|
+
--rf-text-muted: rgb(255 255 255 / 68%);
|
|
2577
|
+
--rf-glass: rgb(32 32 32 / 78%);
|
|
2578
|
+
--rf-glass-highlight: rgb(255 255 255 / 14%);
|
|
2579
|
+
--rf-glass-stroke: rgb(255 255 255 / 12%);
|
|
2580
|
+
--rf-primary: #f1efec;
|
|
2581
|
+
--rf-primary-text: #1c1c1b;
|
|
2582
|
+
--rf-accent: #529cca;
|
|
2583
|
+
--rf-danger: #eb5757;
|
|
2584
|
+
--rf-success: #4dab74;
|
|
2585
|
+
--rf-shadow-low: 0 0 0 1px rgb(255 255 255 / 6%), 0 1px 2px rgb(0 0 0 / 30%), 0 4px 10px -4px rgb(0 0 0 / 40%);
|
|
2586
|
+
--rf-shadow: 0 0 0 1px rgb(255 255 255 / 7%), 0 2px 5px -1px rgb(0 0 0 / 35%), 0 14px 36px -10px rgb(0 0 0 / 55%);
|
|
2587
|
+
color-scheme: dark;
|
|
1822
2588
|
}
|
|
1823
|
-
.ghost-btn:hover { background: var(--rf-bg-subtle); color: var(--rf-text); }
|
|
1824
2589
|
|
|
1825
|
-
|
|
1826
|
-
display: inline-flex;
|
|
1827
|
-
align-items: center;
|
|
1828
|
-
justify-content: center;
|
|
1829
|
-
gap: 8px;
|
|
1830
|
-
width: 100%;
|
|
1831
|
-
height: 32px;
|
|
1832
|
-
margin-top: 2px;
|
|
1833
|
-
font-size: 13px;
|
|
1834
|
-
font-weight: 550;
|
|
1835
|
-
color: var(--rf-primary-text);
|
|
1836
|
-
background: var(--rf-primary);
|
|
1837
|
-
border-radius: var(--rf-radius-sm);
|
|
1838
|
-
transition: opacity 160ms var(--rf-ease), transform 160ms var(--rf-ease);
|
|
1839
|
-
}
|
|
1840
|
-
.submit:hover:not(:disabled) { opacity: 0.88; }
|
|
1841
|
-
.submit:active:not(:disabled) { transform: scale(0.99); }
|
|
1842
|
-
.submit:disabled { opacity: 0.35; cursor: not-allowed; }
|
|
2590
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
1843
2591
|
|
|
1844
|
-
.error { margin: 0; font-size: 12px; color: var(--rf-danger); }
|
|
1845
2592
|
|
|
1846
|
-
.
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
border-top: 1px solid var(--rf-border);
|
|
2593
|
+
.root {
|
|
2594
|
+
position: fixed;
|
|
2595
|
+
z-index: 2147483000;
|
|
1850
2596
|
display: flex;
|
|
1851
|
-
align-items:
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
color: var(--rf-text-muted);
|
|
2597
|
+
align-items: flex-end;
|
|
2598
|
+
gap: 8px;
|
|
2599
|
+
font: 13px/1.45 var(--rf-font, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif);
|
|
2600
|
+
color: var(--rf-text);
|
|
1856
2601
|
}
|
|
1857
|
-
.
|
|
1858
|
-
.
|
|
1859
|
-
.
|
|
1860
|
-
.
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
2602
|
+
.root:has(.panel) { width: min(var(--rf-width), calc(var(--rf-viewport-width, 100vw) - 24px)); }
|
|
2603
|
+
.root[data-position$="right"] { right: calc(var(--rf-viewport-right, 0px) + max(12px, min(var(--rf-offset), calc((var(--rf-viewport-width, 100vw) - var(--rf-width)) / 2)))); }
|
|
2604
|
+
.root[data-position$="left"] { left: calc(var(--rf-viewport-left, 0px) + max(12px, min(var(--rf-offset), calc((var(--rf-viewport-width, 100vw) - var(--rf-width)) / 2)))); }
|
|
2605
|
+
.root[data-position^="bottom"] { bottom: calc(var(--rf-viewport-bottom, 0px) + max(var(--rf-offset), env(safe-area-inset-bottom))); }
|
|
2606
|
+
.root[data-position^="top"] { top: calc(var(--rf-viewport-top, 0px) + max(var(--rf-offset), env(safe-area-inset-top))); align-items: flex-start; }
|
|
2607
|
+
.root[data-position="bottom-right"] .panel { transform-origin: bottom right; }
|
|
2608
|
+
.root[data-position="bottom-left"] .panel { transform-origin: bottom left; }
|
|
2609
|
+
.root[data-position^="top"] .panel { transform-origin: top center; }
|
|
2610
|
+
.root[data-editing="true"] { visibility: hidden; pointer-events: none; }
|
|
2611
|
+
button, select, input, textarea { font: inherit; color: inherit; }
|
|
2612
|
+
button { margin: 0; cursor: pointer; border: 0; background: none; }
|
|
2613
|
+
button:disabled { cursor: not-allowed; opacity: .4; }
|
|
2614
|
+
svg { flex-shrink: 0; }
|
|
2615
|
+
:focus-visible { outline: 2px solid var(--rf-accent); outline-offset: 3px; }
|
|
2616
|
+
.icon-btn, .tool {
|
|
2617
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
2618
|
+
width: 32px; height: 32px; flex: none; border-radius: var(--rf-pill); color: var(--rf-text-muted);
|
|
2619
|
+
transition: background 160ms var(--rf-ease), color 160ms var(--rf-ease);
|
|
1865
2620
|
}
|
|
1866
|
-
.
|
|
1867
|
-
|
|
1868
|
-
|
|
2621
|
+
.icon-btn:hover, .tool:hover { background: var(--rf-bg-hover); color: var(--rf-text); }
|
|
2622
|
+
.launcher {
|
|
2623
|
+
display: inline-flex; align-items: center; gap: 7px; height: 40px; padding: 0 15px;
|
|
2624
|
+
border-radius: var(--rf-pill); color: var(--rf-text);
|
|
2625
|
+
font-weight: 500;
|
|
2626
|
+
transition: transform 200ms var(--rf-ease), box-shadow 200ms var(--rf-ease);
|
|
2627
|
+
}
|
|
2628
|
+
.launcher:hover { transform: translateY(-2px); box-shadow: var(--rf-shadow); }
|
|
2629
|
+
.launcher-icon { display: flex; width: 15px; height: 15px; }
|
|
2630
|
+
.launcher-icon .on-close { display: none; }
|
|
2631
|
+
.floating-controls {
|
|
2632
|
+
display: flex; align-items: center; padding: 3px; flex: none;
|
|
2633
|
+
border-radius: var(--rf-pill);
|
|
2634
|
+
animation: rf-in 220ms var(--rf-ease);
|
|
2635
|
+
}
|
|
2636
|
+
.drag-handle { touch-action: none; cursor: grab; }
|
|
2637
|
+
.drag-handle:active { cursor: grabbing; }
|
|
2638
|
+
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2639
|
+
.ghost-btn { display: flex; align-items: center; gap: 8px; min-height: 32px; padding: 0 8px; border-radius: var(--rf-radius-sm); }
|
|
2640
|
+
.ghost-btn:hover { background: var(--rf-bg-hover); }
|
|
2641
|
+
.error { margin: 0; padding: 10px 14px; border-radius: var(--rf-radius); font-size: 12px; color: var(--rf-danger); }
|
|
2642
|
+
.done { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 24px; text-align: center; }
|
|
1869
2643
|
.done svg { color: var(--rf-success); }
|
|
1870
2644
|
.done h2 { margin: 0; font-size: 14px; font-weight: 600; }
|
|
1871
|
-
.done p { margin: 0; font-size:
|
|
1872
|
-
|
|
1873
|
-
.spinner {
|
|
1874
|
-
width: 14px;
|
|
1875
|
-
height: 14px;
|
|
1876
|
-
border: 2px solid currentColor;
|
|
1877
|
-
border-right-color: transparent;
|
|
1878
|
-
border-radius: 50%;
|
|
1879
|
-
opacity: 0.6;
|
|
1880
|
-
animation: rf-spin 700ms linear infinite;
|
|
1881
|
-
}
|
|
1882
|
-
@keyframes rf-spin { to { transform: rotate(360deg); } }
|
|
1883
|
-
|
|
2645
|
+
.done p { margin: 0; font-size: 12px; color: var(--rf-text-muted); }
|
|
2646
|
+
.spinner { width: 14px; height: 14px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: rf-spin 700ms linear infinite; }
|
|
1884
2647
|
.hp { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
|
1885
|
-
|
|
1886
|
-
.
|
|
1887
|
-
|
|
2648
|
+
@keyframes rf-spin { to { transform: rotate(360deg); } }
|
|
2649
|
+
@keyframes rf-in { from { opacity: 0; transform: translateY(5px) scale(.98); } }
|
|
2650
|
+
@keyframes rf-fade { from { opacity: 0; } }
|
|
2651
|
+
@media (pointer: coarse) {
|
|
2652
|
+
.icon-btn, .tool { width: 44px; height: 44px; }
|
|
2653
|
+
.launcher, .ghost-btn { min-height: 44px; }
|
|
2654
|
+
}
|
|
2655
|
+
.glass {
|
|
2656
|
+
position: relative;
|
|
2657
|
+
isolation: isolate;
|
|
2658
|
+
border: 1px solid var(--rf-glass-stroke);
|
|
2659
|
+
box-shadow: inset 0 1px 0 var(--rf-glass-highlight), var(--rf-shadow);
|
|
2660
|
+
}
|
|
2661
|
+
.glass::before {
|
|
2662
|
+
content: "";
|
|
2663
|
+
position: absolute;
|
|
1888
2664
|
inset: 0;
|
|
1889
|
-
z-index:
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
backdrop-filter: blur(3px);
|
|
1894
|
-
animation: rf-fade 180ms var(--rf-ease);
|
|
2665
|
+
z-index: -1;
|
|
2666
|
+
border-radius: inherit;
|
|
2667
|
+
background: var(--rf-bg);
|
|
2668
|
+
pointer-events: none;
|
|
1895
2669
|
}
|
|
1896
|
-
|
|
1897
|
-
.
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
box-shadow: 0 20px 60px -16px rgb(0 0 0 / 70%);
|
|
1903
|
-
cursor: crosshair;
|
|
1904
|
-
touch-action: none;
|
|
2670
|
+
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
|
|
2671
|
+
.glass::before {
|
|
2672
|
+
background: var(--rf-glass);
|
|
2673
|
+
-webkit-backdrop-filter: blur(20px) saturate(1.6);
|
|
2674
|
+
backdrop-filter: blur(20px) saturate(1.6);
|
|
2675
|
+
}
|
|
1905
2676
|
}
|
|
1906
|
-
|
|
1907
|
-
.
|
|
1908
|
-
display: flex;
|
|
1909
|
-
align-items: center;
|
|
1910
|
-
justify-content: center;
|
|
1911
|
-
gap: 6px;
|
|
1912
|
-
flex-wrap: wrap;
|
|
1913
|
-
padding: 14px 16px calc(14px + env(safe-area-inset-bottom));
|
|
2677
|
+
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
|
|
2678
|
+
.glass::before { background: var(--rf-bg); backdrop-filter: none; -webkit-backdrop-filter: none; }
|
|
1914
2679
|
}
|
|
1915
|
-
.
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
2680
|
+
.capture-halo { position: fixed; inset: 0; z-index: 2147482999; pointer-events: none; opacity: 0; transition: opacity 500ms var(--rf-ease); }
|
|
2681
|
+
.capture-halo[data-active="true"] { opacity: 1; transition-duration: 140ms; }
|
|
2682
|
+
.capture-halo::before { content: ""; position: absolute; inset: 0; border-radius: 20px; box-shadow: inset 14px 0 36px -16px var(--rf-accent), inset -14px 0 36px -16px var(--rf-accent), inset 0 0 4px 1px color-mix(in srgb, var(--rf-accent) 35%, transparent); }
|
|
2683
|
+
.capture-halo::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, color-mix(in srgb, var(--rf-accent) 20%, transparent), transparent 5%, transparent 95%, color-mix(in srgb, var(--rf-accent) 20%, transparent)); }
|
|
2684
|
+
|
|
2685
|
+
.panel { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 8px; outline: none; animation: rf-in 240ms var(--rf-ease); }
|
|
2686
|
+
.success-panel { border-radius: var(--rf-radius); }
|
|
2687
|
+
.composer { position: relative; display: flex; flex-direction: column; gap: 8px; }
|
|
2688
|
+
.message-row { display: flex; align-items: flex-end; gap: 4px; padding: 4px; border-radius: 26px; }
|
|
2689
|
+
.message-row textarea {
|
|
2690
|
+
flex: 1; width: 0; min-height: 36px; height: 36px; max-height: min(140px, 30dvh);
|
|
2691
|
+
margin: 0; padding: 8px 10px 8px 12px; border: 0; border-radius: 20px; resize: none;
|
|
2692
|
+
background: transparent; line-height: 20px; transition: height 220ms var(--rf-ease);
|
|
2693
|
+
}
|
|
2694
|
+
.panel:focus-within .message-row[data-expanded="true"] textarea { height: 76px; }
|
|
2695
|
+
.message-row textarea::placeholder { color: var(--rf-text-muted); }
|
|
2696
|
+
.message-row textarea:focus { outline: none; }
|
|
2697
|
+
.message-row:has(textarea:focus-visible) { border-color: color-mix(in srgb, var(--rf-accent) 40%, var(--rf-glass-stroke)); }
|
|
2698
|
+
.submit { display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; flex: none; border-radius: var(--rf-pill); background: var(--rf-primary); color: var(--rf-primary-text); transition: opacity 160ms var(--rf-ease), transform 160ms var(--rf-ease); }
|
|
2699
|
+
.submit svg { transform: rotate(-45deg); }
|
|
2700
|
+
.submit:hover:enabled { transform: scale(1.05); }
|
|
2701
|
+
.submit:disabled { background: var(--rf-bg-hover); color: var(--rf-text-muted); opacity: .65; }
|
|
2702
|
+
.composer-toolbar { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex-wrap: wrap; }
|
|
2703
|
+
.composer-controls { display: flex; align-items: center; gap: 2px; padding: 3px; border-radius: var(--rf-pill); }
|
|
2704
|
+
.select-control { position: relative; display: flex; align-items: center; min-width: 0; }
|
|
2705
|
+
.select-control > svg { position: absolute; right: 9px; width: 12px; height: 12px; pointer-events: none; color: var(--rf-text-muted); }
|
|
2706
|
+
.category-select { appearance: none; min-width: 0; max-width: 124px; height: 32px; padding: 0 26px 0 12px; border: 0; border-radius: var(--rf-pill); color: var(--rf-text); background: transparent; cursor: pointer; text-overflow: ellipsis; }
|
|
2707
|
+
.category-select:hover:hover { background: var(--rf-bg-hover); }
|
|
2708
|
+
option { background: var(--rf-bg); color: var(--rf-text); }
|
|
2709
|
+
.composer-options { position: relative; }
|
|
2710
|
+
summary { cursor: pointer; list-style: none; }
|
|
2711
|
+
summary::-webkit-details-marker { display: none; }
|
|
2712
|
+
.options-popover { position: absolute; z-index: 2; right: 0; bottom: calc(100% + 12px); width: min(240px, calc(100vw - 40px)); max-height: calc(50dvh - 68px); overflow-y: auto; display: flex; flex-direction: column; gap: 8px; padding: 12px; border-radius: var(--rf-radius); animation: rf-in 160ms var(--rf-ease); }
|
|
2713
|
+
.composer-options[data-placement="below"] .options-popover { top: calc(100% + 12px); bottom: auto; }
|
|
2714
|
+
.email-field { display: flex; flex-direction: column; gap: 6px; color: var(--rf-text-muted); font-size: 12px; }
|
|
2715
|
+
.email-field [role="alert"] { color: var(--rf-danger); }
|
|
2716
|
+
.email-field input { width: 100%; padding: 9px; border: 1px solid var(--rf-border); border-radius: var(--rf-radius-sm); background: var(--rf-bg-subtle); color: var(--rf-text); }
|
|
2717
|
+
.dismiss-btn { min-height: 32px; text-align: left; padding: 0 8px; border-radius: var(--rf-radius-sm); color: var(--rf-text-muted); }
|
|
2718
|
+
.dismiss-btn:hover { background: var(--rf-bg-hover); }
|
|
2719
|
+
.version-info { font-size: 10px; color: var(--rf-text-muted); padding: 4px 8px 0; border-top: 1px solid var(--rf-border); text-decoration: none; }
|
|
2720
|
+
.version-info:hover { color: var(--rf-text); }
|
|
2721
|
+
.attachments { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }
|
|
2722
|
+
.screenshot-scroll-area { position: relative; min-width: 0; max-width: calc(100% + 8px); margin: -4px -4px -8px; }
|
|
2723
|
+
.screenshot-strip { width: 100%; padding: 4px 4px 8px; overscroll-behavior-x: contain; --rf-fade-start: 0px; --rf-fade-end: 0px; mask-image: linear-gradient(to right, transparent, black var(--rf-fade-start), black calc(100% - var(--rf-fade-end)), transparent); mask-repeat: no-repeat; }
|
|
2724
|
+
.screenshot-strip[data-overflow-x-start] { --rf-fade-start: min(24px, var(--scroll-area-overflow-x-start)); }
|
|
2725
|
+
.screenshot-strip[data-overflow-x-end] { --rf-fade-end: min(24px, var(--scroll-area-overflow-x-end)); }
|
|
2726
|
+
.screenshot-strip-content { display: flex; align-items: flex-end; gap: 8px; }
|
|
2727
|
+
.screenshot-scroll-area:has(.screenshot-strip-content:empty) { display: none; }
|
|
2728
|
+
.attachments:has(.screenshot-strip-content:empty):not(:has(.selection-chip)) { display: none; }
|
|
2729
|
+
.screenshot-scrollbar { height: 4px; margin: 0 8px 2px; opacity: 0; transition: opacity 150ms var(--rf-ease); }
|
|
2730
|
+
.screenshot-scrollbar[data-hovering], .screenshot-scrollbar[data-scrolling] { opacity: 1; }
|
|
2731
|
+
.screenshot-scroll-thumb { border-radius: var(--rf-pill); background: var(--rf-border); }
|
|
2732
|
+
.capture-progress { position: absolute; inset: 3px; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 6px; border-radius: 16px; color: var(--rf-text-muted); font-size: 10px; }
|
|
2733
|
+
.attachments:empty { display: none; }
|
|
2734
|
+
.screenshot-attachment, .attachment-skeleton { position: relative; flex: none; margin: 0; min-width: 0; width: 128px; height: 78px; padding: 3px; border-radius: var(--rf-radius); animation: rf-in 260ms var(--rf-ease); }
|
|
2735
|
+
.screenshot-preview { display: block; position: relative; width: 100%; height: 100%; padding: 0; overflow: hidden; border-radius: 16px; }
|
|
2736
|
+
.screenshot-preview img, .screenshot-preview canvas { width: 100%; height: 100%; display: block; object-fit: cover; object-position: top center; }
|
|
2737
|
+
.screenshot-preview canvas { position: absolute; inset: 0; pointer-events: none; }
|
|
2738
|
+
.attachment-caption { position: absolute; left: 5px; bottom: 5px; display: flex; align-items: center; gap: 4px; padding: 3px 6px; font-size: 10px; border-radius: var(--rf-pill); color: var(--rf-text); }
|
|
2739
|
+
.attachment-caption svg { width: 12px; height: 12px; }
|
|
2740
|
+
.annotation-count { color: var(--rf-text); font-weight: 600; }
|
|
2741
|
+
.attachment-actions { position: absolute; right: 4px; bottom: -4px; display: flex; gap: 1px; padding: 1px; border-radius: var(--rf-pill); opacity: 0; pointer-events: none; transform: translateY(3px); transition: opacity 150ms var(--rf-ease), transform 150ms var(--rf-ease); }
|
|
2742
|
+
.screenshot-attachment:hover .attachment-actions, .screenshot-attachment:focus-within .attachment-actions { opacity: 1; pointer-events: auto; transform: none; }
|
|
2743
|
+
.screenshot-attachment:hover .attachment-caption, .screenshot-attachment:focus-within .attachment-caption { opacity: 0; }
|
|
2744
|
+
.attachment-skeleton { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: var(--rf-text-muted); font-size: 11px; }
|
|
2745
|
+
.attachment-skeleton::after { content: ""; position: absolute; inset: 0; border-radius: inherit; background: var(--rf-bg-subtle); animation: rf-fade 800ms alternate infinite; }
|
|
2746
|
+
.selection-chip { display: flex; align-items: center; max-width: 100%; padding: 3px; border-radius: var(--rf-pill); }
|
|
2747
|
+
.selection-label { display: flex; align-items: center; min-width: 0; gap: 6px; padding: 0 6px; font-size: 11px; }
|
|
2748
|
+
.selection-label img { object-fit: contain; border-radius: var(--rf-pill); }
|
|
2749
|
+
@media (pointer: coarse) {
|
|
2750
|
+
.category-select { height: 44px; max-width: 96px; }
|
|
2751
|
+
.message-row { border-radius: 28px; }
|
|
2752
|
+
.message-row textarea { font-size: 16px; min-height: 44px; height: 44px; padding-top: 12px; }
|
|
2753
|
+
.submit { width: 44px; height: 44px; }
|
|
2754
|
+
.attachment-actions { opacity: 1; pointer-events: auto; transform: none; }
|
|
2755
|
+
.attachment-caption { top: 4px; bottom: auto; right: 4px; left: auto; }
|
|
2756
|
+
.attachment-caption-label { display: none; }
|
|
2757
|
+
.screenshot-attachment:hover .attachment-caption, .screenshot-attachment:focus-within .attachment-caption { opacity: 1; }
|
|
2758
|
+
.screenshot-attachment, .attachment-skeleton { height: 96px; }
|
|
2759
|
+
.email-field input { font-size: 16px; }
|
|
2760
|
+
.dismiss-btn, .selection-label { min-height: 44px; }
|
|
1923
2761
|
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
2762
|
+
|
|
2763
|
+
|
|
2764
|
+
.overlay { margin: 0; border: 0; padding: 0; width: 100vw; max-width: none; height: 100dvh; max-height: none; position: fixed; inset: 0; z-index: 2147483001; display: flex; flex-direction: column; background: color-mix(in srgb, var(--rf-bg) 94%, transparent); animation: rf-editor-in 260ms var(--rf-ease); transition: background 260ms var(--rf-ease); color: var(--rf-text); font: 13px/1.45 ui-sans-serif, system-ui, sans-serif; }
|
|
2765
|
+
.overlay::backdrop { background: transparent; }
|
|
2766
|
+
@keyframes rf-editor-in { from { background: transparent; } }
|
|
2767
|
+
.editor { flex: 1; position: relative; display: flex; align-items: center; justify-content: center; padding: 20px; min-height: 0; container-type: size; }
|
|
2768
|
+
.editor-image { width: min(100cqw, calc(100cqh * var(--rf-capture-ratio))); aspect-ratio: var(--rf-capture-ratio); flex: none; position: relative; }
|
|
2769
|
+
.editor-visual { position: absolute; inset: 0; border-radius: var(--rf-radius); box-shadow: var(--rf-shadow); transform-origin: top left; overflow: hidden; }
|
|
2770
|
+
.editor-visual img, .editor-visual canvas { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: inherit; object-fit: cover; object-position: top center; }
|
|
2771
|
+
.editor-visual canvas { cursor: crosshair; touch-action: none; }
|
|
2772
|
+
.overlay[data-opening="true"] .editor { pointer-events: none; }
|
|
2773
|
+
.overlay[data-closing="true"] { pointer-events: none; background: transparent; }
|
|
2774
|
+
.overlay[data-closing="true"] .toolbar { opacity: 0; transform: translateY(8px); }
|
|
2775
|
+
|
|
2776
|
+
.toolbar { display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; transition: opacity 180ms var(--rf-ease), transform 180ms var(--rf-ease); padding: 4px 12px max(16px, env(safe-area-inset-bottom)); }
|
|
2777
|
+
.toolbar .group { display: flex; align-items: center; gap: 2px; padding: 4px; border-radius: var(--rf-pill); }
|
|
2778
|
+
.tool[aria-pressed="true"], .tool[data-selected="true"] { background: var(--rf-primary); color: var(--rf-primary-text); }
|
|
2779
|
+
.color-picker { position: relative; }
|
|
2780
|
+
.color-picker summary { display: flex; justify-content: center; align-items: center; }
|
|
2781
|
+
.color-preview { width: 18px; height: 18px; border-radius: 50%; background: var(--annotation-color); box-shadow: inset 0 0 0 1px var(--rf-border); }
|
|
2782
|
+
.color-popover { --rf-color-width: 200px; position: absolute; bottom: calc(100% + 14px); left: clamp(calc(12px - var(--rf-color-anchor)), 0px, calc(100vw - var(--rf-color-anchor) - var(--rf-color-width) - 12px)); display: flex; align-items: center; gap: 6px; width: var(--rf-color-width); padding: 4px 4px 4px 14px; border-radius: var(--rf-pill); animation: rf-in 180ms var(--rf-ease); }
|
|
2783
|
+
.color-picker[data-placement="below"] .color-popover { top: calc(100% + 14px); bottom: auto; }
|
|
2784
|
+
.color-slider { flex: 1; min-width: 0; }
|
|
2785
|
+
.color-slider-control { display: flex; align-items: center; height: 32px; width: 100%; touch-action: none; user-select: none; cursor: pointer; }
|
|
2786
|
+
.color-slider-track { height: 12px; width: 100%; border-radius: var(--rf-pill); background: linear-gradient(to right, white, var(--annotation-base-color)); box-shadow: inset 0 0 0 1px var(--rf-border); }
|
|
2787
|
+
.color-slider-thumb { width: 16px; height: 16px; border: 2px solid white; border-radius: 50%; background: var(--annotation-color); box-shadow: var(--rf-shadow-low); transition: scale 160ms var(--rf-ease); }
|
|
2788
|
+
.color-slider-thumb[data-dragging] { scale: 1.12; }
|
|
2789
|
+
.color-slider-thumb:has(:focus-visible), .color-spectrum:has(:focus-visible) { outline: 2px solid var(--rf-accent); outline-offset: 3px; }
|
|
2790
|
+
.color-spectrum { position: relative; display: flex; align-items: center; justify-content: center; flex: none; width: 32px; height: 32px; border-radius: 50%; cursor: pointer; }
|
|
2791
|
+
.color-spectrum::before { content: ""; width: 16px; height: 16px; border-radius: 50%; background: conic-gradient(#ff5757, #ffde59, #6bdb78, #4ac9fa, #8b5cf6, #ef73dc, #ff5757); box-shadow: inset 0 0 0 1px var(--rf-glass-highlight); }
|
|
2792
|
+
.color-spectrum:hover { background: var(--rf-bg-hover); }
|
|
2793
|
+
.color-spectrum button { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: inherit; }
|
|
2794
|
+
.color-spectrum button:focus-visible { outline: none; }
|
|
2795
|
+
.color-spectrum input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; pointer-events: none; }
|
|
2796
|
+
.done-btn { display: flex; align-items: center; justify-content: center; width: 34px; height: 34px; padding: 0; border-radius: var(--rf-pill); color: var(--rf-primary-text); background: var(--rf-primary); }
|
|
2797
|
+
.drawing-menu { position: relative; }
|
|
2798
|
+
.drawing-popover { position: absolute; z-index: 2; right: 0; bottom: calc(100% + 12px); width: 200px; max-height: calc(50dvh - 68px); overflow-y: auto; padding: 6px; border-radius: var(--rf-radius); }
|
|
2799
|
+
.drawing-menu[data-placement="below"] .drawing-popover { top: calc(100% + 12px); bottom: auto; }
|
|
2800
|
+
.drawing-option { display: flex; align-items: center; width: 100%; min-height: 36px; gap: 10px; padding: 0 10px; border-radius: 12px; text-align: left; }
|
|
2801
|
+
.drawing-option:hover, .drawing-option[aria-pressed="true"] { background: var(--rf-bg-hover); }
|
|
2802
|
+
.drawing-divider { height: 1px; margin: 5px; background: var(--rf-border); }
|
|
2803
|
+
.editor canvas[data-tool="text"] { cursor: text; }
|
|
2804
|
+
.text-annotation-editor { position: fixed; z-index: 3; width: 240px; padding: 8px; border-radius: var(--rf-radius); }
|
|
2805
|
+
.text-annotation-editor textarea { display: block; width: 100%; min-height: 64px; max-height: 112px; padding: 6px; border: 0; background: transparent; resize: vertical; font-size: 16px; line-height: 24px; }
|
|
2806
|
+
.text-annotation-editor textarea:focus { outline: none; }
|
|
2807
|
+
.text-annotation-actions { display: flex; align-items: center; justify-content: flex-end; gap: 4px; }
|
|
2808
|
+
@media (pointer: coarse) {
|
|
2809
|
+
.drawing-option { min-height: 44px; }
|
|
2810
|
+
.done-btn { width: 44px; height: 44px; }
|
|
2811
|
+
.color-popover { --rf-color-width: 232px; padding-left: 16px; }
|
|
2812
|
+
.color-slider-control, .color-spectrum { height: 44px; }
|
|
2813
|
+
.color-spectrum { width: 44px; }
|
|
2814
|
+
.color-slider-thumb, .color-spectrum::before { width: 18px; height: 18px; }
|
|
2815
|
+
.editor { padding: 8px; }
|
|
2816
|
+
.toolbar { gap: 6px; }
|
|
2817
|
+
.toolbar .group { padding: 3px; gap: 0; }
|
|
2818
|
+
}
|
|
2819
|
+
@media (pointer: coarse) and (max-width: 360px) {
|
|
2820
|
+
.toolbar { gap: 4px; padding-inline: 2px; }
|
|
2821
|
+
.toolbar .group { padding: 0; }
|
|
1933
2822
|
}
|
|
1934
|
-
|
|
1935
|
-
.tool[aria-pressed="true"] { background: var(--rf-bg-hover); color: var(--rf-text); }
|
|
1936
|
-
.swatch {
|
|
1937
|
-
width: 20px;
|
|
1938
|
-
height: 20px;
|
|
1939
|
-
border-radius: 50%;
|
|
1940
|
-
border: 2px solid transparent;
|
|
1941
|
-
transition: transform 200ms var(--rf-ease), border-color 160ms var(--rf-ease);
|
|
1942
|
-
}
|
|
1943
|
-
.swatch[aria-pressed="true"] { border-color: var(--rf-text); transform: scale(1.15); }
|
|
1944
|
-
.toolbar .done-btn {
|
|
1945
|
-
height: 30px;
|
|
1946
|
-
padding: 0 13px;
|
|
1947
|
-
font-size: 13px;
|
|
1948
|
-
font-weight: 550;
|
|
1949
|
-
border-radius: 7px;
|
|
1950
|
-
color: var(--rf-primary-text);
|
|
1951
|
-
background: var(--rf-primary);
|
|
1952
|
-
transition: opacity 160ms var(--rf-ease);
|
|
1953
|
-
}
|
|
1954
|
-
.toolbar .done-btn:hover { opacity: 0.88; }
|
|
2823
|
+
|
|
1955
2824
|
|
|
1956
2825
|
.picker-box {
|
|
1957
2826
|
position: fixed;
|
|
@@ -2008,26 +2877,9 @@ textarea:focus, input:focus { outline: none; border-color: color-mix(in srgb, va
|
|
|
2008
2877
|
box-shadow: var(--rf-shadow-inset);
|
|
2009
2878
|
}
|
|
2010
2879
|
|
|
2011
|
-
@media (pointer: coarse) {
|
|
2012
|
-
.icon-btn, .tool, .swatch {
|
|
2013
|
-
width: 44px;
|
|
2014
|
-
height: 44px;
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
.launcher, .segmented button, .ghost-btn, .submit, .toolbar .done-btn {
|
|
2018
|
-
min-height: 44px;
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
.dismiss-btn, .footer a {
|
|
2022
|
-
display: inline-flex;
|
|
2023
|
-
min-height: 44px;
|
|
2024
|
-
align-items: center;
|
|
2025
|
-
}
|
|
2026
2880
|
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
justify-content: center;
|
|
2030
|
-
}
|
|
2881
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2882
|
+
*, *::before, *::after { animation: none !important; transition: none !important; }
|
|
2031
2883
|
}
|
|
2032
2884
|
`;
|
|
2033
2885
|
//#endregion
|
|
@@ -2277,10 +3129,55 @@ async function captureElementCloseUp(element) {
|
|
|
2277
3129
|
}
|
|
2278
3130
|
}
|
|
2279
3131
|
//#endregion
|
|
3132
|
+
//#region src/feedback/core/submission/upload-screenshots.ts
|
|
3133
|
+
async function uploadImage(transport, image) {
|
|
3134
|
+
const { uploadUrl } = await transport.getScreenshotUploadUrl();
|
|
3135
|
+
return transport.uploadImage(uploadUrl, image);
|
|
3136
|
+
}
|
|
3137
|
+
async function prepareScreenshot(transport, screenshot, index) {
|
|
3138
|
+
const { image, annotations, context } = screenshot;
|
|
3139
|
+
const annotated = annotations.length > 0 ? await renderAnnotatedImage(image, annotations) : null;
|
|
3140
|
+
if (annotations.length > 0 && !annotated) throw new Error("Could not render your annotations. Please try again.");
|
|
3141
|
+
try {
|
|
3142
|
+
const storageId = await uploadImage(transport, image);
|
|
3143
|
+
return {
|
|
3144
|
+
annotatedStorageId: annotated ? await uploadImage(transport, annotated) : void 0,
|
|
3145
|
+
annotations: annotations.length > 0 ? toWireAnnotations(annotations) : void 0,
|
|
3146
|
+
captureSource: "widget",
|
|
3147
|
+
filename: `screenshot-${index + 1}.png`,
|
|
3148
|
+
height: image.height,
|
|
3149
|
+
mimeType: image.mimeType,
|
|
3150
|
+
pageUrl: context.url,
|
|
3151
|
+
size: image.blob.size,
|
|
3152
|
+
storageId,
|
|
3153
|
+
width: image.width
|
|
3154
|
+
};
|
|
3155
|
+
} finally {
|
|
3156
|
+
releaseCapture(annotated);
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
async function uploadScreenshots(transport, submission) {
|
|
3160
|
+
const prepared = (await Promise.allSettled(submission.screenshots.map((screenshot, index) => prepareScreenshot(transport, screenshot, index)))).map((result) => {
|
|
3161
|
+
if (result.status === "rejected") throw result.reason;
|
|
3162
|
+
return result.value;
|
|
3163
|
+
});
|
|
3164
|
+
const element = submission.element;
|
|
3165
|
+
if (element) prepared.push({
|
|
3166
|
+
captureSource: "element",
|
|
3167
|
+
filename: "element.png",
|
|
3168
|
+
height: element.height,
|
|
3169
|
+
mimeType: element.mimeType,
|
|
3170
|
+
pageUrl: submission.context.url,
|
|
3171
|
+
size: element.blob.size,
|
|
3172
|
+
storageId: await uploadImage(transport, element),
|
|
3173
|
+
width: element.width
|
|
3174
|
+
});
|
|
3175
|
+
return prepared;
|
|
3176
|
+
}
|
|
3177
|
+
//#endregion
|
|
2280
3178
|
//#region src/feedback/core/submit.ts
|
|
2281
3179
|
const MAX_TITLE_LENGTH = 100;
|
|
2282
3180
|
const NEWLINE = /\r?\n/;
|
|
2283
|
-
/** First line of the message, cut on a word boundary so titles stay readable. */
|
|
2284
3181
|
function deriveTitle(message) {
|
|
2285
3182
|
const firstLine = message.split(NEWLINE).map((line) => line.trim()).find(Boolean) ?? "";
|
|
2286
3183
|
if (firstLine.length <= MAX_TITLE_LENGTH) return firstLine;
|
|
@@ -2294,64 +3191,31 @@ function buildDescription(params) {
|
|
|
2294
3191
|
const contact = isAnonymous && email ? `\n\n---\nContact: ${email}` : "";
|
|
2295
3192
|
return `[${label}] ${message.trim()}${contact}`;
|
|
2296
3193
|
}
|
|
2297
|
-
async function
|
|
2298
|
-
const
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
const
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
await transport.saveScreenshot({
|
|
2308
|
-
annotatedStorageId,
|
|
2309
|
-
annotations: wireAnnotations.length > 0 ? wireAnnotations : void 0,
|
|
2310
|
-
captureSource: "widget",
|
|
2311
|
-
feedbackId,
|
|
2312
|
-
filename: "screenshot.png",
|
|
2313
|
-
height: screenshot.height,
|
|
2314
|
-
mimeType: screenshot.mimeType,
|
|
2315
|
-
pageUrl: context.url,
|
|
2316
|
-
size: screenshot.blob.size,
|
|
2317
|
-
storageId,
|
|
2318
|
-
width: screenshot.width
|
|
2319
|
-
});
|
|
2320
|
-
return true;
|
|
2321
|
-
}
|
|
2322
|
-
async function attachElement(transport, feedbackId, submission) {
|
|
2323
|
-
const { context, element } = submission;
|
|
2324
|
-
if (!element) return;
|
|
2325
|
-
await transport.saveScreenshot({
|
|
2326
|
-
captureSource: "element",
|
|
2327
|
-
feedbackId,
|
|
2328
|
-
filename: "element.png",
|
|
2329
|
-
height: element.height,
|
|
2330
|
-
mimeType: element.mimeType,
|
|
2331
|
-
pageUrl: context.url,
|
|
2332
|
-
size: element.blob.size,
|
|
2333
|
-
storageId: await upload(transport, element),
|
|
2334
|
-
width: element.width
|
|
2335
|
-
});
|
|
3194
|
+
async function attachPreparedScreenshots(transport, result) {
|
|
3195
|
+
const results = await Promise.allSettled(result.pendingScreenshots.map((screenshot) => transport.saveScreenshot({
|
|
3196
|
+
...screenshot,
|
|
3197
|
+
feedbackId: result.feedbackId
|
|
3198
|
+
})));
|
|
3199
|
+
const pendingScreenshots = result.pendingScreenshots.filter((_, index) => results[index]?.status === "rejected");
|
|
3200
|
+
return {
|
|
3201
|
+
feedbackId: result.feedbackId,
|
|
3202
|
+
pendingScreenshots
|
|
3203
|
+
};
|
|
2336
3204
|
}
|
|
2337
|
-
|
|
2338
|
-
* Creates the feedback first, then attaches the capture. A failed upload is
|
|
2339
|
-
* reported back but never discards what the user wrote.
|
|
2340
|
-
*/
|
|
2341
|
-
async function submitWidgetFeedback(transport, submission) {
|
|
2342
|
-
const title = deriveTitle(submission.message);
|
|
2343
|
-
if (!title) throw new Error("Please describe your feedback before sending it.");
|
|
3205
|
+
async function createReportWithScreenshots(transport, submission, pendingScreenshots) {
|
|
2344
3206
|
const { feedbackId } = await transport.create({
|
|
2345
3207
|
context: submission.context,
|
|
2346
3208
|
description: buildDescription(submission),
|
|
2347
|
-
title
|
|
3209
|
+
title: deriveTitle(submission.message)
|
|
2348
3210
|
});
|
|
2349
|
-
|
|
2350
|
-
await attachElement(transport, feedbackId, submission).catch(() => void 0);
|
|
2351
|
-
return {
|
|
3211
|
+
return attachPreparedScreenshots(transport, {
|
|
2352
3212
|
feedbackId,
|
|
2353
|
-
|
|
2354
|
-
};
|
|
3213
|
+
pendingScreenshots
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
async function submitWidgetFeedback(transport, submission) {
|
|
3217
|
+
if (!deriveTitle(submission.message)) throw new Error("Please describe your feedback before sending it.");
|
|
3218
|
+
return createReportWithScreenshots(transport, submission, await uploadScreenshots(transport, submission));
|
|
2355
3219
|
}
|
|
2356
3220
|
function isStorageResponse(value) {
|
|
2357
3221
|
return typeof value === "object" && value !== null && typeof Reflect.get(value, "storageId") === "string";
|
|
@@ -2375,6 +3239,220 @@ function createFeedbackTransport(client) {
|
|
|
2375
3239
|
};
|
|
2376
3240
|
}
|
|
2377
3241
|
//#endregion
|
|
3242
|
+
//#region src/feedback/ui/state/use-feedback-submission.ts
|
|
3243
|
+
function useFeedbackSubmission(options) {
|
|
3244
|
+
const [activeRequest, setActiveRequest] = useState(null);
|
|
3245
|
+
const [pendingReport, setPendingReport] = useState(null);
|
|
3246
|
+
const requestRef = useRef(null);
|
|
3247
|
+
useEffect(() => () => {
|
|
3248
|
+
requestRef.current = null;
|
|
3249
|
+
}, []);
|
|
3250
|
+
const resetSubmission = useCallback(() => {
|
|
3251
|
+
requestRef.current = null;
|
|
3252
|
+
setPendingReport(null);
|
|
3253
|
+
setActiveRequest(null);
|
|
3254
|
+
}, []);
|
|
3255
|
+
const finishSubmission = (result) => {
|
|
3256
|
+
if (result.pendingScreenshots.length > 0) {
|
|
3257
|
+
setPendingReport(result);
|
|
3258
|
+
options.onError(options.partialUploadMessage);
|
|
3259
|
+
return;
|
|
3260
|
+
}
|
|
3261
|
+
setPendingReport(null);
|
|
3262
|
+
options.onSubmitted(result.feedbackId);
|
|
3263
|
+
};
|
|
3264
|
+
const submit = async (draft) => {
|
|
3265
|
+
if (requestRef.current) return;
|
|
3266
|
+
if (!options.client) {
|
|
3267
|
+
options.onError("Reflet is missing a publicKey. Pass one to RefletFeedback or RefletProvider.");
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3270
|
+
const request = {};
|
|
3271
|
+
requestRef.current = request;
|
|
3272
|
+
setActiveRequest(request);
|
|
3273
|
+
options.onError(null);
|
|
3274
|
+
try {
|
|
3275
|
+
const transport = createFeedbackTransport(options.client);
|
|
3276
|
+
const result = pendingReport ? await attachPreparedScreenshots(transport, pendingReport) : await submitWidgetFeedback(transport, draft);
|
|
3277
|
+
if (requestRef.current === request) finishSubmission(result);
|
|
3278
|
+
} catch (error) {
|
|
3279
|
+
if (requestRef.current === request) options.onError(error instanceof Error ? error.message : "Something went wrong. Please try again.");
|
|
3280
|
+
} finally {
|
|
3281
|
+
if (requestRef.current === request) {
|
|
3282
|
+
requestRef.current = null;
|
|
3283
|
+
setActiveRequest(null);
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
};
|
|
3287
|
+
return {
|
|
3288
|
+
hasPendingAttachments: pendingReport !== null,
|
|
3289
|
+
isEditingDisabled: activeRequest !== null || pendingReport !== null,
|
|
3290
|
+
isSubmitting: activeRequest !== null,
|
|
3291
|
+
resetSubmission,
|
|
3292
|
+
submit
|
|
3293
|
+
};
|
|
3294
|
+
}
|
|
3295
|
+
//#endregion
|
|
3296
|
+
//#region src/feedback/ui/state/use-owned-capture.ts
|
|
3297
|
+
function useOwnedCapture() {
|
|
3298
|
+
const [image, setImage] = useState(null);
|
|
3299
|
+
const ownedImage = useRef(null);
|
|
3300
|
+
useEffect(() => () => releaseCapture(ownedImage.current), []);
|
|
3301
|
+
return [image, useCallback((nextImage) => {
|
|
3302
|
+
releaseCapture(ownedImage.current);
|
|
3303
|
+
ownedImage.current = nextImage;
|
|
3304
|
+
setImage(nextImage);
|
|
3305
|
+
}, [])];
|
|
3306
|
+
}
|
|
3307
|
+
//#endregion
|
|
3308
|
+
//#region src/feedback/ui/state/use-screenshot-drafts.ts
|
|
3309
|
+
function useScreenshotDrafts(captureFailed, onError) {
|
|
3310
|
+
const [screenshots, setScreenshots] = useState([]);
|
|
3311
|
+
const [activeId, setActiveId] = useState(null);
|
|
3312
|
+
const [pendingCapture, setPendingCapture] = useState(null);
|
|
3313
|
+
const ownedImages = useRef(/* @__PURE__ */ new Map());
|
|
3314
|
+
const captureRequest = useRef(null);
|
|
3315
|
+
useEffect(() => {
|
|
3316
|
+
const images = ownedImages.current;
|
|
3317
|
+
return () => {
|
|
3318
|
+
captureRequest.current = null;
|
|
3319
|
+
for (const image of images.values()) releaseCapture(image);
|
|
3320
|
+
images.clear();
|
|
3321
|
+
};
|
|
3322
|
+
}, []);
|
|
3323
|
+
const storeScreenshot = useCallback((screenshot) => {
|
|
3324
|
+
releaseCapture(ownedImages.current.get(screenshot.id));
|
|
3325
|
+
ownedImages.current.set(screenshot.id, screenshot.image);
|
|
3326
|
+
setScreenshots((previous) => {
|
|
3327
|
+
return previous.some(({ id }) => id === screenshot.id) ? previous.map((item) => item.id === screenshot.id ? screenshot : item) : [...previous, screenshot];
|
|
3328
|
+
});
|
|
3329
|
+
setActiveId(screenshot.id);
|
|
3330
|
+
}, []);
|
|
3331
|
+
const cancelPendingCapture = useCallback(() => {
|
|
3332
|
+
captureRequest.current = null;
|
|
3333
|
+
setPendingCapture(null);
|
|
3334
|
+
}, []);
|
|
3335
|
+
const takeScreenshot = useCallback(async (request) => {
|
|
3336
|
+
captureRequest.current = request;
|
|
3337
|
+
setPendingCapture(request);
|
|
3338
|
+
onError(null);
|
|
3339
|
+
const context = collectPageContext({ sdkVersion: SDK_VERSION });
|
|
3340
|
+
const image = await captureViewport().catch(() => null);
|
|
3341
|
+
if (captureRequest.current !== request) {
|
|
3342
|
+
releaseCapture(image);
|
|
3343
|
+
return;
|
|
3344
|
+
}
|
|
3345
|
+
cancelPendingCapture();
|
|
3346
|
+
if (!image) {
|
|
3347
|
+
onError(captureFailed);
|
|
3348
|
+
return;
|
|
3349
|
+
}
|
|
3350
|
+
storeScreenshot({
|
|
3351
|
+
...request,
|
|
3352
|
+
annotations: [],
|
|
3353
|
+
context,
|
|
3354
|
+
image
|
|
3355
|
+
});
|
|
3356
|
+
}, [
|
|
3357
|
+
cancelPendingCapture,
|
|
3358
|
+
captureFailed,
|
|
3359
|
+
onError,
|
|
3360
|
+
storeScreenshot
|
|
3361
|
+
]);
|
|
3362
|
+
const takeCapture = useCallback(() => {
|
|
3363
|
+
if (!captureRequest.current) takeScreenshot({
|
|
3364
|
+
id: crypto.randomUUID(),
|
|
3365
|
+
source: "manual"
|
|
3366
|
+
});
|
|
3367
|
+
}, [takeScreenshot]);
|
|
3368
|
+
const refreshAutomatic = useCallback(() => {
|
|
3369
|
+
takeScreenshot({
|
|
3370
|
+
id: ownedImages.current.keys().next().value ?? captureRequest.current?.id ?? crypto.randomUUID(),
|
|
3371
|
+
source: "automatic"
|
|
3372
|
+
});
|
|
3373
|
+
}, [takeScreenshot]);
|
|
3374
|
+
const retakeCapture = useCallback((id) => {
|
|
3375
|
+
if (!captureRequest.current && ownedImages.current.has(id)) takeScreenshot({
|
|
3376
|
+
id,
|
|
3377
|
+
source: "manual"
|
|
3378
|
+
});
|
|
3379
|
+
}, [takeScreenshot]);
|
|
3380
|
+
const removeCapture = useCallback((id) => {
|
|
3381
|
+
if (captureRequest.current?.id === id) cancelPendingCapture();
|
|
3382
|
+
releaseCapture(ownedImages.current.get(id));
|
|
3383
|
+
ownedImages.current.delete(id);
|
|
3384
|
+
setScreenshots((previous) => previous.filter((item) => item.id !== id));
|
|
3385
|
+
setActiveId((current) => current === id ? null : current);
|
|
3386
|
+
}, [cancelPendingCapture]);
|
|
3387
|
+
const resetScreenshots = useCallback(() => {
|
|
3388
|
+
cancelPendingCapture();
|
|
3389
|
+
for (const image of ownedImages.current.values()) releaseCapture(image);
|
|
3390
|
+
ownedImages.current.clear();
|
|
3391
|
+
setScreenshots([]);
|
|
3392
|
+
setActiveId(null);
|
|
3393
|
+
}, [cancelPendingCapture]);
|
|
3394
|
+
const setAnnotations = useCallback((annotations) => {
|
|
3395
|
+
setScreenshots((previous) => previous.map((item) => item.id === activeId ? {
|
|
3396
|
+
...item,
|
|
3397
|
+
annotations
|
|
3398
|
+
} : item));
|
|
3399
|
+
}, [activeId]);
|
|
3400
|
+
const clearSelectionAnnotations = useCallback(() => {
|
|
3401
|
+
setScreenshots((previous) => previous.map((item) => ({
|
|
3402
|
+
...item,
|
|
3403
|
+
annotations: item.annotations.filter(({ id }) => !id.startsWith("selection-"))
|
|
3404
|
+
})));
|
|
3405
|
+
}, []);
|
|
3406
|
+
return {
|
|
3407
|
+
activeScreenshot: screenshots.find(({ id }) => id === activeId) ?? screenshots.at(-1) ?? null,
|
|
3408
|
+
cancelPendingCapture,
|
|
3409
|
+
clearSelectionAnnotations,
|
|
3410
|
+
pendingCapture,
|
|
3411
|
+
refreshAutomatic,
|
|
3412
|
+
removeCapture,
|
|
3413
|
+
resetScreenshots,
|
|
3414
|
+
retakeCapture,
|
|
3415
|
+
screenshots,
|
|
3416
|
+
selectScreenshot: setActiveId,
|
|
3417
|
+
setAnnotations,
|
|
3418
|
+
storeScreenshot,
|
|
3419
|
+
takeCapture
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
//#endregion
|
|
3423
|
+
//#region src/feedback/ui/state/use-widget-config.ts
|
|
3424
|
+
function defaultCategory(props) {
|
|
3425
|
+
const preferred = props.defaultCategory ?? "bug";
|
|
3426
|
+
return props.categories?.find((category) => category === preferred) ?? props.categories?.[0] ?? preferred;
|
|
3427
|
+
}
|
|
3428
|
+
function dismissalDuration(days) {
|
|
3429
|
+
return days !== void 0 && Number.isFinite(days) && days > 0 ? days : null;
|
|
3430
|
+
}
|
|
3431
|
+
function useWidgetConfig(props) {
|
|
3432
|
+
const context = useContext(RefletContext);
|
|
3433
|
+
const publicKey = props.publicKey ?? context?.publicKey;
|
|
3434
|
+
const baseUrl = props.baseUrl ?? context?.baseUrl;
|
|
3435
|
+
const user = props.user ?? context?.user;
|
|
3436
|
+
const userToken = props.userToken ?? context?.userToken;
|
|
3437
|
+
return {
|
|
3438
|
+
client: useMemo(() => publicKey ? new Reflet({
|
|
3439
|
+
baseUrl,
|
|
3440
|
+
publicKey,
|
|
3441
|
+
user,
|
|
3442
|
+
userToken
|
|
3443
|
+
}) : null, [
|
|
3444
|
+
baseUrl,
|
|
3445
|
+
publicKey,
|
|
3446
|
+
user,
|
|
3447
|
+
userToken
|
|
3448
|
+
]),
|
|
3449
|
+
defaultCategory: defaultCategory(props),
|
|
3450
|
+
dismissalKey: `reflet-feedback-dismissed:${publicKey ?? "default"}:${user?.id ?? "anonymous"}`,
|
|
3451
|
+
dismissForDays: dismissalDuration(props.dismissForDays),
|
|
3452
|
+
isAnonymous: !(user || userToken)
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
//#endregion
|
|
2378
3456
|
//#region src/feedback/ui/use-capture-sync.ts
|
|
2379
3457
|
const RESIZE_DEBOUNCE = 400;
|
|
2380
3458
|
/**
|
|
@@ -2424,7 +3502,6 @@ function useCaptureSync(enabled, refresh) {
|
|
|
2424
3502
|
const SUCCESS_CLOSE_DELAY = 2400;
|
|
2425
3503
|
const IS_APPLE = /Mac|iPhone|iPad/;
|
|
2426
3504
|
const MILLISECONDS_PER_DAY = 864e5;
|
|
2427
|
-
const DISMISSAL_STORAGE_PREFIX = "reflet-feedback-dismissed";
|
|
2428
3505
|
function readDismissedUntil(key) {
|
|
2429
3506
|
if (typeof window === "undefined") return 0;
|
|
2430
3507
|
try {
|
|
@@ -2441,10 +3518,6 @@ function writeDismissedUntil(key, until) {
|
|
|
2441
3518
|
window.localStorage.setItem(key, String(until));
|
|
2442
3519
|
} catch {}
|
|
2443
3520
|
}
|
|
2444
|
-
/**
|
|
2445
|
-
* Matches shortcuts written as `alt+f` or `mod+shift+k`, where `mod` is the
|
|
2446
|
-
* platform's command key.
|
|
2447
|
-
*/
|
|
2448
3521
|
function matchesHotkey(event, hotkey, platform = typeof navigator === "undefined" ? "" : navigator.platform) {
|
|
2449
3522
|
const parts = hotkey.toLowerCase().split("+").map((part) => part.trim());
|
|
2450
3523
|
const key = parts.at(-1);
|
|
@@ -2459,48 +3532,27 @@ function matchesHotkey(event, hotkey, platform = typeof navigator === "undefined
|
|
|
2459
3532
|
};
|
|
2460
3533
|
return event.altKey === expected.alt && event.ctrlKey === expected.ctrl && event.metaKey === expected.meta && event.shiftKey === expected.shift;
|
|
2461
3534
|
}
|
|
2462
|
-
/**
|
|
2463
|
-
* A capture whose object URL is revoked the moment it is replaced or dropped —
|
|
2464
|
-
* holding one without this leaks the blob for the lifetime of the page.
|
|
2465
|
-
*/
|
|
2466
|
-
function useOwnedCapture() {
|
|
2467
|
-
const [image, setImage] = useState(null);
|
|
2468
|
-
const ref = useRef(null);
|
|
2469
|
-
useEffect(() => {
|
|
2470
|
-
ref.current = image;
|
|
2471
|
-
}, [image]);
|
|
2472
|
-
useEffect(() => () => releaseCapture(ref.current), []);
|
|
2473
|
-
return [image, useCallback((next) => {
|
|
2474
|
-
releaseCapture(ref.current);
|
|
2475
|
-
setImage(next);
|
|
2476
|
-
}, [])];
|
|
2477
|
-
}
|
|
2478
3535
|
function useWidgetState(props) {
|
|
2479
|
-
const
|
|
2480
|
-
const publicKey = props.publicKey ?? context?.publicKey;
|
|
2481
|
-
const baseUrl = props.baseUrl ?? context?.baseUrl;
|
|
2482
|
-
const user = props.user ?? context?.user;
|
|
2483
|
-
const userToken = props.userToken ?? context?.userToken;
|
|
2484
|
-
const isAnonymous = !(user || userToken);
|
|
2485
|
-
const dismissForDays = props.dismissForDays !== void 0 && Number.isFinite(props.dismissForDays) && props.dismissForDays > 0 ? props.dismissForDays : null;
|
|
2486
|
-
const dismissalKey = `${DISMISSAL_STORAGE_PREFIX}:${publicKey ?? "default"}:${user?.id ?? "anonymous"}`;
|
|
3536
|
+
const { client, defaultCategory, dismissalKey, dismissForDays, isAnonymous } = useWidgetConfig(props);
|
|
2487
3537
|
const [isOpen, setIsOpen] = useState(false);
|
|
3538
|
+
const [annotationTrigger, setAnnotationTrigger] = useState(null);
|
|
2488
3539
|
const [step, setStep] = useState("compose");
|
|
2489
|
-
const [category, setCategory] = useState(
|
|
3540
|
+
const [category, setCategory] = useState(defaultCategory);
|
|
2490
3541
|
const [message, setMessage] = useState("");
|
|
2491
3542
|
const [email, setEmail] = useState("");
|
|
2492
3543
|
const [honeypot, setHoneypot] = useState("");
|
|
2493
|
-
const [capture, replaceCapture] = useOwnedCapture();
|
|
2494
3544
|
const [elementCapture, replaceElementCapture] = useOwnedCapture();
|
|
2495
|
-
const [annotations, setAnnotations] = useState([]);
|
|
2496
3545
|
const [selection, setSelection] = useState(null);
|
|
2497
|
-
const [
|
|
2498
|
-
const
|
|
3546
|
+
const [isElementCapturing, setIsElementCapturing] = useState(false);
|
|
3547
|
+
const successTimer = useRef(null);
|
|
2499
3548
|
const [error, setError] = useState(null);
|
|
2500
3549
|
const [dismissedUntil, setDismissedUntil] = useState(() => readDismissedUntil(dismissalKey));
|
|
2501
3550
|
const pickRef = useRef(0);
|
|
3551
|
+
useEffect(() => () => {
|
|
3552
|
+
pickRef.current++;
|
|
3553
|
+
if (successTimer.current) clearTimeout(successTimer.current);
|
|
3554
|
+
}, []);
|
|
2502
3555
|
const consoleRef = useRef(null);
|
|
2503
|
-
const frozenContext = useRef(null);
|
|
2504
3556
|
useEffect(() => {
|
|
2505
3557
|
setDismissedUntil(readDismissedUntil(dismissalKey));
|
|
2506
3558
|
}, [dismissalKey]);
|
|
@@ -2513,33 +3565,45 @@ function useWidgetState(props) {
|
|
|
2513
3565
|
recorder.stop();
|
|
2514
3566
|
};
|
|
2515
3567
|
}, [props.captureConsole]);
|
|
2516
|
-
|
|
2517
|
-
const
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
3568
|
+
const captureFailed = props.labels?.captureFailed ?? DEFAULT_WIDGET_LABELS.captureFailed;
|
|
3569
|
+
const gallery = useScreenshotDrafts(captureFailed, setError);
|
|
3570
|
+
const { activeScreenshot, cancelPendingCapture, clearSelectionAnnotations, refreshAutomatic, resetScreenshots, storeScreenshot } = gallery;
|
|
3571
|
+
const capture = activeScreenshot?.image ?? null;
|
|
3572
|
+
const annotations = activeScreenshot?.annotations ?? [];
|
|
3573
|
+
const isCapturing = gallery.pendingCapture !== null || isElementCapturing;
|
|
3574
|
+
const submission = useFeedbackSubmission({
|
|
3575
|
+
client,
|
|
3576
|
+
onError: setError,
|
|
3577
|
+
onSubmitted: (feedbackId) => {
|
|
3578
|
+
setStep("success");
|
|
3579
|
+
props.onSubmit?.({ feedbackId });
|
|
3580
|
+
successTimer.current = setTimeout(close, SUCCESS_CLOSE_DELAY);
|
|
3581
|
+
},
|
|
3582
|
+
partialUploadMessage: props.labels?.attachmentUploadFailed ?? DEFAULT_WIDGET_LABELS.attachmentUploadFailed
|
|
3583
|
+
});
|
|
3584
|
+
const { resetSubmission } = submission;
|
|
3585
|
+
const firstCapture = gallery.pendingCapture ?? gallery.screenshots[0];
|
|
3586
|
+
const canRefreshAutomatically = gallery.screenshots.length <= 1 && firstCapture?.source === "automatic";
|
|
3587
|
+
useCaptureSync(isOpen && !submission.isEditingDisabled && step === "compose" && canRefreshAutomatically && annotations.length === 0 && !selection && props.captureOnOpen !== false, refreshAutomatic);
|
|
2527
3588
|
const reset = useCallback(() => {
|
|
3589
|
+
if (successTimer.current) clearTimeout(successTimer.current);
|
|
3590
|
+
setAnnotationTrigger(null);
|
|
3591
|
+
resetSubmission();
|
|
2528
3592
|
pickRef.current++;
|
|
2529
|
-
|
|
2530
|
-
|
|
3593
|
+
setIsElementCapturing(false);
|
|
3594
|
+
resetScreenshots();
|
|
2531
3595
|
replaceElementCapture(null);
|
|
2532
|
-
setAnnotations([]);
|
|
2533
3596
|
setSelection(null);
|
|
2534
3597
|
setMessage("");
|
|
2535
3598
|
setEmail("");
|
|
2536
3599
|
setHoneypot("");
|
|
2537
3600
|
setError(null);
|
|
2538
3601
|
setStep("compose");
|
|
2539
|
-
setCategory(
|
|
3602
|
+
setCategory(defaultCategory);
|
|
2540
3603
|
}, [
|
|
2541
|
-
|
|
2542
|
-
|
|
3604
|
+
defaultCategory,
|
|
3605
|
+
resetSubmission,
|
|
3606
|
+
resetScreenshots,
|
|
2543
3607
|
replaceElementCapture
|
|
2544
3608
|
]);
|
|
2545
3609
|
const close = useCallback(() => {
|
|
@@ -2565,14 +3629,15 @@ function useWidgetState(props) {
|
|
|
2565
3629
|
setIsOpen(true);
|
|
2566
3630
|
setStep("compose");
|
|
2567
3631
|
props.onOpen?.();
|
|
2568
|
-
if (props.captureOnOpen !== false)
|
|
3632
|
+
if (props.captureOnOpen !== false) refreshAutomatic();
|
|
2569
3633
|
}, [
|
|
2570
3634
|
props.captureOnOpen,
|
|
2571
3635
|
props.onOpen,
|
|
2572
|
-
|
|
3636
|
+
refreshAutomatic
|
|
2573
3637
|
]);
|
|
2574
|
-
/** Fresh pixels so the highlight maps onto the scroll position the element had. */
|
|
2575
3638
|
const selectElement = useCallback(async (element) => {
|
|
3639
|
+
cancelPendingCapture();
|
|
3640
|
+
setIsElementCapturing(true);
|
|
2576
3641
|
const picked = buildElementSelection(element);
|
|
2577
3642
|
setSelection(picked);
|
|
2578
3643
|
setStep("compose");
|
|
@@ -2584,89 +3649,58 @@ function useWidgetState(props) {
|
|
|
2584
3649
|
releaseCapture(closeUp);
|
|
2585
3650
|
return;
|
|
2586
3651
|
}
|
|
2587
|
-
|
|
3652
|
+
setIsElementCapturing(false);
|
|
3653
|
+
if (fresh) storeScreenshot({
|
|
3654
|
+
annotations: [highlightFor(picked, fresh)],
|
|
3655
|
+
context,
|
|
3656
|
+
id: crypto.randomUUID(),
|
|
3657
|
+
image: fresh,
|
|
3658
|
+
source: "manual"
|
|
3659
|
+
});
|
|
3660
|
+
else setError(captureFailed);
|
|
2588
3661
|
replaceElementCapture(closeUp);
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
3662
|
+
}, [
|
|
3663
|
+
cancelPendingCapture,
|
|
3664
|
+
captureFailed,
|
|
3665
|
+
storeScreenshot,
|
|
3666
|
+
replaceElementCapture
|
|
3667
|
+
]);
|
|
2592
3668
|
const clearSelection = useCallback(() => {
|
|
2593
3669
|
pickRef.current++;
|
|
2594
3670
|
replaceElementCapture(null);
|
|
2595
3671
|
setSelection(null);
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
replaceCapture(null);
|
|
2601
|
-
setAnnotations([]);
|
|
2602
|
-
}, [replaceCapture]);
|
|
2603
|
-
const client = useMemo(() => publicKey ? new Reflet({
|
|
2604
|
-
baseUrl,
|
|
2605
|
-
publicKey,
|
|
2606
|
-
user,
|
|
2607
|
-
userToken
|
|
2608
|
-
}) : null, [
|
|
2609
|
-
baseUrl,
|
|
2610
|
-
publicKey,
|
|
2611
|
-
user,
|
|
2612
|
-
userToken
|
|
2613
|
-
]);
|
|
2614
|
-
const submit = useCallback(async () => {
|
|
3672
|
+
setIsElementCapturing(false);
|
|
3673
|
+
clearSelectionAnnotations();
|
|
3674
|
+
}, [clearSelectionAnnotations, replaceElementCapture]);
|
|
3675
|
+
const submit = () => {
|
|
2615
3676
|
if (honeypot) {
|
|
2616
3677
|
setStep("success");
|
|
2617
3678
|
return;
|
|
2618
3679
|
}
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
consoleEvents: consoleRef.current?.events(),
|
|
2635
|
-
metadata: props.metadata,
|
|
2636
|
-
selection: selection ?? void 0
|
|
2637
|
-
},
|
|
2638
|
-
element: elementCapture,
|
|
2639
|
-
email,
|
|
2640
|
-
isAnonymous,
|
|
2641
|
-
message,
|
|
2642
|
-
screenshot: capture
|
|
2643
|
-
});
|
|
2644
|
-
setStep("success");
|
|
2645
|
-
props.onSubmit?.({ feedbackId: result.feedbackId });
|
|
2646
|
-
setTimeout(close, SUCCESS_CLOSE_DELAY);
|
|
2647
|
-
} catch (submitError) {
|
|
2648
|
-
setError(submitError instanceof Error ? submitError.message : "Something went wrong. Please try again.");
|
|
2649
|
-
} finally {
|
|
2650
|
-
releaseCapture(annotated);
|
|
2651
|
-
setIsSubmitting(false);
|
|
2652
|
-
}
|
|
2653
|
-
}, [
|
|
2654
|
-
annotations,
|
|
2655
|
-
capture,
|
|
2656
|
-
category,
|
|
2657
|
-
client,
|
|
2658
|
-
close,
|
|
2659
|
-
elementCapture,
|
|
2660
|
-
email,
|
|
2661
|
-
honeypot,
|
|
2662
|
-
isAnonymous,
|
|
2663
|
-
message,
|
|
2664
|
-
props.metadata,
|
|
2665
|
-
props.onSubmit,
|
|
2666
|
-
selection
|
|
2667
|
-
]);
|
|
3680
|
+
return submission.submit({
|
|
3681
|
+
category,
|
|
3682
|
+
context: {
|
|
3683
|
+
...activeScreenshot?.context ?? collectPageContext({ sdkVersion: "0.3.4" }),
|
|
3684
|
+
consoleEvents: consoleRef.current?.events(),
|
|
3685
|
+
metadata: props.metadata,
|
|
3686
|
+
selection: selection ?? void 0
|
|
3687
|
+
},
|
|
3688
|
+
element: elementCapture,
|
|
3689
|
+
email,
|
|
3690
|
+
isAnonymous,
|
|
3691
|
+
message,
|
|
3692
|
+
screenshots: gallery.screenshots
|
|
3693
|
+
});
|
|
3694
|
+
};
|
|
2668
3695
|
return {
|
|
3696
|
+
activeScreenshot,
|
|
3697
|
+
annotateScreenshot: (id, trigger) => {
|
|
3698
|
+
setAnnotationTrigger(trigger);
|
|
3699
|
+
gallery.selectScreenshot(id);
|
|
3700
|
+
setStep("annotate");
|
|
3701
|
+
},
|
|
2669
3702
|
annotations,
|
|
3703
|
+
annotationTrigger,
|
|
2670
3704
|
canDismiss: dismissForDays !== null,
|
|
2671
3705
|
capture,
|
|
2672
3706
|
category,
|
|
@@ -2677,18 +3711,23 @@ function useWidgetState(props) {
|
|
|
2677
3711
|
elementCapture,
|
|
2678
3712
|
email,
|
|
2679
3713
|
error,
|
|
3714
|
+
hasPendingAttachments: submission.hasPendingAttachments,
|
|
2680
3715
|
honeypot,
|
|
2681
3716
|
isAnonymous,
|
|
2682
3717
|
isCapturing,
|
|
2683
3718
|
isDismissed: dismissedUntil > Date.now(),
|
|
3719
|
+
isEditingDisabled: submission.isEditingDisabled,
|
|
2684
3720
|
isOpen,
|
|
2685
|
-
isSubmitting,
|
|
3721
|
+
isSubmitting: submission.isSubmitting,
|
|
2686
3722
|
message,
|
|
2687
3723
|
open,
|
|
2688
|
-
|
|
3724
|
+
pendingCapture: gallery.pendingCapture,
|
|
3725
|
+
removeCapture: gallery.removeCapture,
|
|
3726
|
+
retakeCapture: gallery.retakeCapture,
|
|
3727
|
+
screenshots: gallery.screenshots,
|
|
2689
3728
|
selectElement,
|
|
2690
3729
|
selection,
|
|
2691
|
-
setAnnotations,
|
|
3730
|
+
setAnnotations: gallery.setAnnotations,
|
|
2692
3731
|
setCategory,
|
|
2693
3732
|
setEmail,
|
|
2694
3733
|
setHoneypot,
|
|
@@ -2696,7 +3735,7 @@ function useWidgetState(props) {
|
|
|
2696
3735
|
setStep,
|
|
2697
3736
|
step,
|
|
2698
3737
|
submit,
|
|
2699
|
-
takeCapture
|
|
3738
|
+
takeCapture: gallery.takeCapture
|
|
2700
3739
|
};
|
|
2701
3740
|
}
|
|
2702
3741
|
//#endregion
|
|
@@ -2706,14 +3745,8 @@ const DEFAULT_CATEGORIES = [
|
|
|
2706
3745
|
"idea",
|
|
2707
3746
|
"question"
|
|
2708
3747
|
];
|
|
2709
|
-
const DEFAULT_OFFSET = 20;
|
|
2710
|
-
/**
|
|
2711
|
-
* Floating feedback button with a screenshot, annotation and element picker
|
|
2712
|
-
* flow. Mount it once, anywhere below `RefletProvider` or with its own
|
|
2713
|
-
* `publicKey`.
|
|
2714
|
-
*/
|
|
2715
3748
|
function RefletFeedback(props) {
|
|
2716
|
-
const { categories = DEFAULT_CATEGORIES, enabled = true, hotkey = null, offset =
|
|
3749
|
+
const { categories = DEFAULT_CATEGORIES, enabled = true, hotkey = null, offset = 20, position = "bottom-right", theme = "auto" } = props;
|
|
2717
3750
|
const labels = {
|
|
2718
3751
|
...DEFAULT_WIDGET_LABELS,
|
|
2719
3752
|
...props.labels
|
|
@@ -2738,44 +3771,38 @@ function RefletFeedback(props) {
|
|
|
2738
3771
|
isOpen,
|
|
2739
3772
|
open
|
|
2740
3773
|
]);
|
|
2741
|
-
useEffect(() => {
|
|
2742
|
-
if (!isOpen || state.step !== "compose") return;
|
|
2743
|
-
const onKeyDown = (event) => {
|
|
2744
|
-
if (event.key === "Escape") close();
|
|
2745
|
-
};
|
|
2746
|
-
document.addEventListener("keydown", onKeyDown);
|
|
2747
|
-
return () => document.removeEventListener("keydown", onKeyDown);
|
|
2748
|
-
}, [
|
|
2749
|
-
close,
|
|
2750
|
-
isOpen,
|
|
2751
|
-
state.step
|
|
2752
|
-
]);
|
|
2753
3774
|
if (!enabled || state.isDismissed) return null;
|
|
2754
|
-
const showPanel = isOpen && state.step !== "picking" && state.step !== "annotate";
|
|
2755
3775
|
return /* @__PURE__ */ jsxs(ShadowPortal, {
|
|
2756
3776
|
offset,
|
|
2757
3777
|
primaryColor: props.primaryColor,
|
|
2758
3778
|
theme,
|
|
2759
3779
|
children: [
|
|
2760
|
-
/* @__PURE__ */
|
|
2761
|
-
|
|
2762
|
-
"
|
|
2763
|
-
|
|
2764
|
-
categories,
|
|
2765
|
-
labels,
|
|
2766
|
-
state
|
|
2767
|
-
}), /* @__PURE__ */ jsx(Launcher, {
|
|
2768
|
-
isOpen,
|
|
2769
|
-
label: labels.trigger,
|
|
2770
|
-
onClick: isOpen ? close : open
|
|
2771
|
-
})]
|
|
3780
|
+
/* @__PURE__ */ jsx("div", {
|
|
3781
|
+
"aria-hidden": "true",
|
|
3782
|
+
className: "capture-halo",
|
|
3783
|
+
"data-active": state.isCapturing
|
|
2772
3784
|
}),
|
|
2773
|
-
|
|
2774
|
-
annotations: state.annotations,
|
|
2775
|
-
capture: state.capture,
|
|
3785
|
+
/* @__PURE__ */ jsx(FloatingWidget, {
|
|
2776
3786
|
labels,
|
|
2777
|
-
|
|
2778
|
-
|
|
3787
|
+
options: {
|
|
3788
|
+
categories,
|
|
3789
|
+
position
|
|
3790
|
+
},
|
|
3791
|
+
state
|
|
3792
|
+
}),
|
|
3793
|
+
isOpen && state.step === "annotate" && state.activeScreenshot && /* @__PURE__ */ jsx(Annotator, {
|
|
3794
|
+
capture: state.activeScreenshot.image,
|
|
3795
|
+
editor: {
|
|
3796
|
+
annotations: state.annotations,
|
|
3797
|
+
onChange: state.setAnnotations,
|
|
3798
|
+
onDone: () => state.setStep("compose"),
|
|
3799
|
+
onRetake: () => {
|
|
3800
|
+
state.setStep("compose");
|
|
3801
|
+
if (state.activeScreenshot) state.retakeCapture(state.activeScreenshot.id);
|
|
3802
|
+
},
|
|
3803
|
+
trigger: state.annotationTrigger
|
|
3804
|
+
},
|
|
3805
|
+
labels
|
|
2779
3806
|
}),
|
|
2780
3807
|
isOpen && state.step === "picking" && /* @__PURE__ */ jsx(ElementPicker, {
|
|
2781
3808
|
hint: labels.pickElementHint,
|