reflet-sdk 0.3.3 → 0.3.5
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 +20 -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 +21 -13
- package/dist/feedback.d.ts.map +1 -1
- package/dist/feedback.js +1940 -1098
- package/dist/feedback.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/react.d.ts +21 -21
- package/dist/react.d.ts.map +1 -1
- package/dist/server.d.ts +5 -5
- package/dist/server.d.ts.map +1 -1
- 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 +44 -43
package/dist/feedback.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
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. */
|
|
@@ -184,7 +186,7 @@ function fitWithin(size, maxWidth) {
|
|
|
184
186
|
width: maxWidth
|
|
185
187
|
};
|
|
186
188
|
}
|
|
187
|
-
function toBlob
|
|
189
|
+
function toBlob(canvas) {
|
|
188
190
|
return new Promise((resolve) => {
|
|
189
191
|
canvas.toBlob(resolve, "image/png");
|
|
190
192
|
});
|
|
@@ -206,7 +208,7 @@ async function capturedFromCanvas(source, maxWidth) {
|
|
|
206
208
|
width: source.width
|
|
207
209
|
}, maxWidth);
|
|
208
210
|
const output = fitted.scale === 1 ? source : downscale(source, fitted);
|
|
209
|
-
const blob = await toBlob
|
|
211
|
+
const blob = await toBlob(output);
|
|
210
212
|
if (!blob) return null;
|
|
211
213
|
return {
|
|
212
214
|
blob,
|
|
@@ -807,29 +809,40 @@ function collectPageContext(options) {
|
|
|
807
809
|
}
|
|
808
810
|
//#endregion
|
|
809
811
|
//#region src/feedback/types.ts
|
|
810
|
-
|
|
811
|
-
const SDK_VERSION = "0.3.3";
|
|
812
|
+
const SDK_VERSION = "0.3.5";
|
|
812
813
|
const DEFAULT_WIDGET_LABELS = {
|
|
813
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.",
|
|
814
816
|
attachScreenshot: "Attach a screenshot",
|
|
815
817
|
back: "Back",
|
|
816
818
|
cancel: "Cancel",
|
|
819
|
+
captureFailed: "Screenshot unavailable. Try again or send without it.",
|
|
820
|
+
capturing: "Taking screenshot…",
|
|
817
821
|
categoryBug: "Bug",
|
|
818
822
|
categoryIdea: "Idea",
|
|
823
|
+
categoryLabel: "Feedback type",
|
|
819
824
|
categoryQuestion: "Question",
|
|
820
825
|
clearAnnotations: "Clear drawing",
|
|
821
|
-
|
|
826
|
+
clearSelection: "Remove selected element",
|
|
827
|
+
descriptionPlaceholder: "What would you like to share?",
|
|
822
828
|
dismissForDays: "Hide for {days} days",
|
|
823
829
|
done: "Done",
|
|
830
|
+
emailInvalid: "Enter a valid email address.",
|
|
824
831
|
emailLabel: "Email",
|
|
825
832
|
emailPlaceholder: "you@company.com",
|
|
826
833
|
errorGeneric: "Something went wrong. Please try again.",
|
|
827
834
|
errorTitleRequired: "Tell us a bit about it first.",
|
|
835
|
+
minimize: "Minimize feedback",
|
|
836
|
+
moreOptions: "More options",
|
|
837
|
+
moveFeedback: "Move feedback",
|
|
828
838
|
pickElement: "Point at an element",
|
|
829
839
|
pickElementHint: "Click the element you are talking about — cancel with",
|
|
830
840
|
recapture: "Retake",
|
|
831
841
|
removeScreenshot: "Remove",
|
|
842
|
+
resume: "Resume feedback",
|
|
832
843
|
retakeHint: "Screenshot of this page",
|
|
844
|
+
retryAttachments: "Retry attachments",
|
|
845
|
+
screenshot: "Screenshot",
|
|
833
846
|
submit: "Send feedback",
|
|
834
847
|
successMessage: "We read every report. Thanks for taking the time.",
|
|
835
848
|
successTitle: "Feedback sent",
|
|
@@ -840,15 +853,10 @@ const DEFAULT_WIDGET_LABELS = {
|
|
|
840
853
|
};
|
|
841
854
|
//#endregion
|
|
842
855
|
//#region src/feedback/core/annotations.ts
|
|
843
|
-
const ANNOTATION_COLORS = [
|
|
844
|
-
"#ef4444",
|
|
845
|
-
"#f59e0b",
|
|
846
|
-
"#22c55e",
|
|
847
|
-
"#3b82f6",
|
|
848
|
-
"#111827"
|
|
849
|
-
];
|
|
850
856
|
const BOX_TOOLS = [
|
|
857
|
+
"text",
|
|
851
858
|
"rectangle",
|
|
859
|
+
"spotlight",
|
|
852
860
|
"highlight",
|
|
853
861
|
"blur"
|
|
854
862
|
];
|
|
@@ -889,6 +897,7 @@ function simplifyPath(points, tolerance = DEFAULT_PATH_TOLERANCE) {
|
|
|
889
897
|
return kept;
|
|
890
898
|
}
|
|
891
899
|
function isDegenerate(annotation) {
|
|
900
|
+
if (annotation.tool === "text") return !annotation.text?.trim();
|
|
892
901
|
if (annotation.tool === "pen") return (annotation.points?.length ?? 0) < 2;
|
|
893
902
|
const { start, end } = annotation;
|
|
894
903
|
return Math.hypot(end.x - start.x, end.y - start.y) < MIN_GESTURE_LENGTH;
|
|
@@ -907,6 +916,7 @@ function toWire(annotation) {
|
|
|
907
916
|
return {
|
|
908
917
|
color,
|
|
909
918
|
height: rect.height,
|
|
919
|
+
...tool === "text" ? { text: annotation.text } : {},
|
|
910
920
|
type: tool,
|
|
911
921
|
width: rect.width,
|
|
912
922
|
x: rect.x,
|
|
@@ -927,6 +937,526 @@ function toWireAnnotations(annotations) {
|
|
|
927
937
|
return annotations.filter((item) => !isDegenerate(item)).map(toWire);
|
|
928
938
|
}
|
|
929
939
|
//#endregion
|
|
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("")}`;
|
|
954
|
+
}
|
|
955
|
+
//#endregion
|
|
956
|
+
//#region src/feedback/ui/icons.tsx
|
|
957
|
+
function Svg({ children, className, size = 16 }) {
|
|
958
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
959
|
+
"aria-hidden": "true",
|
|
960
|
+
className,
|
|
961
|
+
fill: "none",
|
|
962
|
+
height: size,
|
|
963
|
+
stroke: "currentColor",
|
|
964
|
+
strokeLinecap: "round",
|
|
965
|
+
strokeLinejoin: "round",
|
|
966
|
+
strokeWidth: "1.8",
|
|
967
|
+
viewBox: "0 0 24 24",
|
|
968
|
+
width: size,
|
|
969
|
+
children
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
function ChatIcon({ className }) {
|
|
973
|
+
return /* @__PURE__ */ jsx(Svg, {
|
|
974
|
+
className,
|
|
975
|
+
size: 15,
|
|
976
|
+
children: /* @__PURE__ */ jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" })
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
function CloseIcon({ className }) {
|
|
980
|
+
return /* @__PURE__ */ jsx(Svg, {
|
|
981
|
+
className,
|
|
982
|
+
children: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" })
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
function CameraIcon() {
|
|
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", {
|
|
987
|
+
cx: "12",
|
|
988
|
+
cy: "13",
|
|
989
|
+
r: "3.2"
|
|
990
|
+
})] });
|
|
991
|
+
}
|
|
992
|
+
function PencilIcon() {
|
|
993
|
+
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M16.5 4.5a2.1 2.1 0 0 1 3 3L8 19l-4 1 1-4z" }) });
|
|
994
|
+
}
|
|
995
|
+
function TargetIcon() {
|
|
996
|
+
return /* @__PURE__ */ jsxs(Svg, { children: [/* @__PURE__ */ jsx("path", { d: "M12 3v3M12 18v3M3 12h3M18 12h3" }), /* @__PURE__ */ jsx("circle", {
|
|
997
|
+
cx: "12",
|
|
998
|
+
cy: "12",
|
|
999
|
+
r: "5"
|
|
1000
|
+
})] });
|
|
1001
|
+
}
|
|
1002
|
+
function TrashIcon() {
|
|
1003
|
+
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M4 7h16M9 7V5h6v2M6 7l1 13h10l1-13M10 11v5M14 11v5" }) });
|
|
1004
|
+
}
|
|
1005
|
+
function ArrowIcon() {
|
|
1006
|
+
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M5 19 19 5M11 5h8v8" }) });
|
|
1007
|
+
}
|
|
1008
|
+
function UndoIcon() {
|
|
1009
|
+
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M4 9h11a5 5 0 0 1 0 10h-5M4 9l4-4M4 9l4 4" }) });
|
|
1010
|
+
}
|
|
1011
|
+
function CheckIcon({ size = 28 }) {
|
|
1012
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
1013
|
+
"aria-hidden": "true",
|
|
1014
|
+
fill: "none",
|
|
1015
|
+
height: size,
|
|
1016
|
+
stroke: "currentColor",
|
|
1017
|
+
strokeLinecap: "round",
|
|
1018
|
+
strokeLinejoin: "round",
|
|
1019
|
+
strokeWidth: "2.4",
|
|
1020
|
+
viewBox: "0 0 24 24",
|
|
1021
|
+
width: size,
|
|
1022
|
+
children: /* @__PURE__ */ jsx("path", { d: "m5 13 4 4L19 7" })
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
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" }) });
|
|
1027
|
+
}
|
|
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
|
|
930
1460
|
//#region src/feedback/core/annotation-renderer.ts
|
|
931
1461
|
const STROKE_WIDTH = 3;
|
|
932
1462
|
const ARROW_HEAD_SIZE = 16;
|
|
@@ -998,6 +1528,18 @@ function drawPen(context, points, scale) {
|
|
|
998
1528
|
}
|
|
999
1529
|
context.stroke();
|
|
1000
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
|
+
}
|
|
1001
1543
|
function drawAnnotation(context, annotation, scale) {
|
|
1002
1544
|
const { color, tool } = annotation;
|
|
1003
1545
|
const rect = scaleRect(normalizeRect(annotation.start, annotation.end), scale);
|
|
@@ -1008,12 +1550,23 @@ function drawAnnotation(context, annotation, scale) {
|
|
|
1008
1550
|
context.strokeStyle = color;
|
|
1009
1551
|
context.fillStyle = color;
|
|
1010
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
|
+
}
|
|
1011
1561
|
case "pen":
|
|
1012
1562
|
drawPen(context, annotation.points ?? [], scale);
|
|
1013
1563
|
break;
|
|
1014
1564
|
case "arrow":
|
|
1015
1565
|
drawArrow(context, scalePoint(annotation.start, scale), scalePoint(annotation.end, scale), scale);
|
|
1016
1566
|
break;
|
|
1567
|
+
case "spotlight":
|
|
1568
|
+
drawSpotlight(context, rect);
|
|
1569
|
+
break;
|
|
1017
1570
|
case "rectangle":
|
|
1018
1571
|
context.strokeRect(rect.x, rect.y, rect.width, rect.height);
|
|
1019
1572
|
break;
|
|
@@ -1037,11 +1590,6 @@ function loadImage(url) {
|
|
|
1037
1590
|
image.src = url;
|
|
1038
1591
|
});
|
|
1039
1592
|
}
|
|
1040
|
-
function toBlob(canvas) {
|
|
1041
|
-
return new Promise((resolve) => {
|
|
1042
|
-
canvas.toBlob(resolve, "image/png");
|
|
1043
|
-
});
|
|
1044
|
-
}
|
|
1045
1593
|
/**
|
|
1046
1594
|
* Burns the annotations into a copy of the capture so reviewers and agents see
|
|
1047
1595
|
* the drawing without needing to replay the vector data.
|
|
@@ -1056,211 +1604,160 @@ async function renderAnnotatedImage(source, annotations) {
|
|
|
1056
1604
|
const image = await loadImage(source.objectUrl);
|
|
1057
1605
|
context.drawImage(image, 0, 0, source.width, source.height);
|
|
1058
1606
|
drawAnnotations(context, annotations);
|
|
1059
|
-
|
|
1060
|
-
|
|
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;
|
|
1626
|
+
useEffect(() => {
|
|
1627
|
+
const canvas = canvasRef.current;
|
|
1628
|
+
const context = canvas?.getContext("2d");
|
|
1629
|
+
if (!(canvas && context && bitmap && isDecoded)) return;
|
|
1630
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
1631
|
+
context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
|
1632
|
+
drawAnnotations(context, annotations);
|
|
1633
|
+
}, [
|
|
1634
|
+
annotations,
|
|
1635
|
+
bitmap,
|
|
1636
|
+
isDecoded
|
|
1637
|
+
]);
|
|
1061
1638
|
return {
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
objectUrl: URL.createObjectURL(blob),
|
|
1066
|
-
width: source.width
|
|
1639
|
+
canvasRef,
|
|
1640
|
+
hasError: failedUrl === capture.objectUrl,
|
|
1641
|
+
isDecoded
|
|
1067
1642
|
};
|
|
1068
1643
|
}
|
|
1069
1644
|
//#endregion
|
|
1070
|
-
//#region src/feedback/ui/
|
|
1071
|
-
function
|
|
1072
|
-
return
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
children
|
|
1084
|
-
});
|
|
1085
|
-
}
|
|
1086
|
-
function ChatIcon({ className }) {
|
|
1087
|
-
return /* @__PURE__ */ jsx(Svg, {
|
|
1088
|
-
className,
|
|
1089
|
-
size: 15,
|
|
1090
|
-
children: /* @__PURE__ */ jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" })
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1093
|
-
function CloseIcon({ className }) {
|
|
1094
|
-
return /* @__PURE__ */ jsx(Svg, {
|
|
1095
|
-
className,
|
|
1096
|
-
children: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" })
|
|
1097
|
-
});
|
|
1098
|
-
}
|
|
1099
|
-
function BugIcon() {
|
|
1100
|
-
return /* @__PURE__ */ jsx(Svg, {
|
|
1101
|
-
size: 14,
|
|
1102
|
-
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" })
|
|
1103
|
-
});
|
|
1104
|
-
}
|
|
1105
|
-
function IdeaIcon() {
|
|
1106
|
-
return /* @__PURE__ */ jsx(Svg, {
|
|
1107
|
-
size: 14,
|
|
1108
|
-
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" })
|
|
1109
|
-
});
|
|
1110
|
-
}
|
|
1111
|
-
function QuestionIcon() {
|
|
1112
|
-
return /* @__PURE__ */ jsxs(Svg, {
|
|
1113
|
-
size: 14,
|
|
1114
|
-
children: [/* @__PURE__ */ jsx("circle", {
|
|
1115
|
-
cx: "12",
|
|
1116
|
-
cy: "12",
|
|
1117
|
-
r: "9"
|
|
1118
|
-
}), /* @__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" })]
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
function CameraIcon() {
|
|
1122
|
-
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", {
|
|
1123
|
-
cx: "12",
|
|
1124
|
-
cy: "13",
|
|
1125
|
-
r: "3.2"
|
|
1126
|
-
})] });
|
|
1127
|
-
}
|
|
1128
|
-
function PencilIcon() {
|
|
1129
|
-
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M16.5 4.5a2.1 2.1 0 0 1 3 3L8 19l-4 1 1-4z" }) });
|
|
1130
|
-
}
|
|
1131
|
-
function TargetIcon() {
|
|
1132
|
-
return /* @__PURE__ */ jsxs(Svg, { children: [/* @__PURE__ */ jsx("path", { d: "M12 3v3M12 18v3M3 12h3M18 12h3" }), /* @__PURE__ */ jsx("circle", {
|
|
1133
|
-
cx: "12",
|
|
1134
|
-
cy: "12",
|
|
1135
|
-
r: "5"
|
|
1136
|
-
})] });
|
|
1137
|
-
}
|
|
1138
|
-
function TrashIcon() {
|
|
1139
|
-
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M4 7h16M9 7V5h6v2M6 7l1 13h10l1-13M10 11v5M14 11v5" }) });
|
|
1140
|
-
}
|
|
1141
|
-
function ArrowIcon() {
|
|
1142
|
-
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M5 19 19 5M11 5h8v8" }) });
|
|
1143
|
-
}
|
|
1144
|
-
function SquareIcon() {
|
|
1145
|
-
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("rect", {
|
|
1146
|
-
height: "13",
|
|
1147
|
-
rx: "1.5",
|
|
1148
|
-
width: "16",
|
|
1149
|
-
x: "4",
|
|
1150
|
-
y: "5.5"
|
|
1151
|
-
}) });
|
|
1152
|
-
}
|
|
1153
|
-
function HighlightIcon() {
|
|
1154
|
-
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M4 15h16M6 5h12v7H6z" }) });
|
|
1155
|
-
}
|
|
1156
|
-
function BlurIcon() {
|
|
1157
|
-
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" })] });
|
|
1158
|
-
}
|
|
1159
|
-
function UndoIcon() {
|
|
1160
|
-
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M4 9h11a5 5 0 0 1 0 10h-5M4 9l4-4M4 9l4 4" }) });
|
|
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
|
+
};
|
|
1161
1658
|
}
|
|
1162
|
-
function
|
|
1163
|
-
return
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
stroke: "currentColor",
|
|
1168
|
-
strokeLinecap: "round",
|
|
1169
|
-
strokeLinejoin: "round",
|
|
1170
|
-
strokeWidth: "2.4",
|
|
1171
|
-
viewBox: "0 0 24 24",
|
|
1172
|
-
width: size,
|
|
1173
|
-
children: /* @__PURE__ */ jsx("path", { d: "m5 13 4 4L19 7" })
|
|
1174
|
-
});
|
|
1659
|
+
function expandedFrame(visual) {
|
|
1660
|
+
return {
|
|
1661
|
+
clipPath: `inset(0px 0px 0px 0px round ${getComputedStyle(visual).borderRadius})`,
|
|
1662
|
+
transform: "none"
|
|
1663
|
+
};
|
|
1175
1664
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
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
|
+
};
|
|
1186
1718
|
}
|
|
1187
1719
|
//#endregion
|
|
1188
1720
|
//#region src/feedback/ui/annotator.tsx
|
|
1189
|
-
const TOOLS = [
|
|
1190
|
-
{
|
|
1191
|
-
icon: PencilIcon,
|
|
1192
|
-
id: "pen",
|
|
1193
|
-
label: "Draw"
|
|
1194
|
-
},
|
|
1195
|
-
{
|
|
1196
|
-
icon: ArrowIcon,
|
|
1197
|
-
id: "arrow",
|
|
1198
|
-
label: "Arrow"
|
|
1199
|
-
},
|
|
1200
|
-
{
|
|
1201
|
-
icon: SquareIcon,
|
|
1202
|
-
id: "rectangle",
|
|
1203
|
-
label: "Box"
|
|
1204
|
-
},
|
|
1205
|
-
{
|
|
1206
|
-
icon: HighlightIcon,
|
|
1207
|
-
id: "highlight",
|
|
1208
|
-
label: "Highlight"
|
|
1209
|
-
},
|
|
1210
|
-
{
|
|
1211
|
-
icon: BlurIcon,
|
|
1212
|
-
id: "blur",
|
|
1213
|
-
label: "Hide"
|
|
1214
|
-
}
|
|
1215
|
-
];
|
|
1216
1721
|
function toImagePoint(canvas, clientX, clientY) {
|
|
1217
1722
|
const bounds = canvas.getBoundingClientRect();
|
|
1218
1723
|
return {
|
|
1219
|
-
x: (clientX - bounds.left) / bounds.width * canvas.width,
|
|
1220
|
-
y: (clientY - bounds.top) / bounds.height * canvas.height
|
|
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))
|
|
1221
1726
|
};
|
|
1222
1727
|
}
|
|
1223
|
-
function Annotator({
|
|
1224
|
-
const
|
|
1225
|
-
const
|
|
1226
|
-
const
|
|
1227
|
-
const [
|
|
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);
|
|
1228
1735
|
const [draft, setDraft] = useState(null);
|
|
1229
|
-
const [
|
|
1230
|
-
|
|
1231
|
-
let active = true;
|
|
1232
|
-
const image = new Image();
|
|
1233
|
-
image.onload = () => {
|
|
1234
|
-
if (active) setBase(image);
|
|
1235
|
-
};
|
|
1236
|
-
image.src = capture.objectUrl;
|
|
1237
|
-
return () => {
|
|
1238
|
-
active = false;
|
|
1239
|
-
setBase(null);
|
|
1240
|
-
};
|
|
1241
|
-
}, [capture.objectUrl]);
|
|
1242
|
-
useEffect(() => {
|
|
1243
|
-
const canvas = canvasRef.current;
|
|
1244
|
-
const context = canvas?.getContext("2d");
|
|
1245
|
-
if (!(canvas && context)) return;
|
|
1246
|
-
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
1247
|
-
if (base) context.drawImage(base, 0, 0, canvas.width, canvas.height);
|
|
1248
|
-
drawAnnotations(context, draft ? [...annotations, draft] : annotations);
|
|
1249
|
-
}, [
|
|
1736
|
+
const { canvasRef, hasError, isDecoded } = useAnnotationCanvas(capture, draft ? [...annotations, draft] : annotations);
|
|
1737
|
+
const text = useTextAnnotation({
|
|
1250
1738
|
annotations,
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1739
|
+
color,
|
|
1740
|
+
onChange
|
|
1741
|
+
});
|
|
1742
|
+
useEffect(() => {
|
|
1743
|
+
const dialog = dialogRef.current;
|
|
1744
|
+
dialog?.showModal();
|
|
1745
|
+
return () => dialog?.close();
|
|
1746
|
+
}, []);
|
|
1254
1747
|
const onPointerDown = (event) => {
|
|
1255
1748
|
const canvas = canvasRef.current;
|
|
1256
|
-
if (!canvas) return;
|
|
1257
|
-
canvas.setPointerCapture(event.pointerId);
|
|
1749
|
+
if (!(canvas && isDecoded) || event.button !== 0) return;
|
|
1258
1750
|
const point = toImagePoint(canvas, event.clientX, event.clientY);
|
|
1259
|
-
|
|
1751
|
+
if (tool === "text") {
|
|
1752
|
+
event.preventDefault();
|
|
1753
|
+
text.beginText(canvas, point);
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
canvas.setPointerCapture(event.pointerId);
|
|
1260
1757
|
setDraft({
|
|
1261
1758
|
color,
|
|
1262
1759
|
end: point,
|
|
1263
|
-
id:
|
|
1760
|
+
id: crypto.randomUUID(),
|
|
1264
1761
|
points: tool === "pen" ? [point] : void 0,
|
|
1265
1762
|
start: point,
|
|
1266
1763
|
tool
|
|
@@ -1284,100 +1781,83 @@ function Annotator({ annotations, capture, labels, onChange, onDone }) {
|
|
|
1284
1781
|
if (!isDegenerate(draft)) onChange([...annotations, draft]);
|
|
1285
1782
|
setDraft(null);
|
|
1286
1783
|
};
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
if (event.key === "Escape") {
|
|
1290
|
-
event.preventDefault();
|
|
1291
|
-
onDone();
|
|
1292
|
-
}
|
|
1293
|
-
};
|
|
1294
|
-
document.addEventListener("keydown", onKeyDown);
|
|
1295
|
-
return () => document.removeEventListener("keydown", onKeyDown);
|
|
1296
|
-
}, [onDone]);
|
|
1297
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1784
|
+
return /* @__PURE__ */ jsxs("dialog", {
|
|
1785
|
+
"aria-label": labels.annotateHint,
|
|
1298
1786
|
className: "overlay",
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
children:
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
className: "group",
|
|
1338
|
-
children: [
|
|
1339
|
-
/* @__PURE__ */ jsx("button", {
|
|
1340
|
-
"aria-label": labels.undo,
|
|
1341
|
-
className: "tool",
|
|
1342
|
-
disabled: annotations.length === 0,
|
|
1343
|
-
onClick: () => onChange(annotations.slice(0, -1)),
|
|
1344
|
-
title: labels.undo,
|
|
1345
|
-
type: "button",
|
|
1346
|
-
children: /* @__PURE__ */ jsx(UndoIcon, {})
|
|
1347
|
-
}),
|
|
1348
|
-
/* @__PURE__ */ jsx("button", {
|
|
1349
|
-
"aria-label": labels.clearAnnotations,
|
|
1350
|
-
className: "tool",
|
|
1351
|
-
disabled: annotations.length === 0,
|
|
1352
|
-
onClick: () => onChange([]),
|
|
1353
|
-
title: labels.clearAnnotations,
|
|
1354
|
-
type: "button",
|
|
1355
|
-
children: /* @__PURE__ */ jsx(TrashIcon, {})
|
|
1356
|
-
}),
|
|
1357
|
-
/* @__PURE__ */ jsx("button", {
|
|
1358
|
-
className: "done-btn",
|
|
1359
|
-
onClick: onDone,
|
|
1360
|
-
type: "button",
|
|
1361
|
-
children: labels.done
|
|
1362
|
-
})
|
|
1363
|
-
]
|
|
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
|
+
})
|
|
1364
1825
|
})
|
|
1365
|
-
|
|
1366
|
-
|
|
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
|
+
]
|
|
1367
1850
|
});
|
|
1368
1851
|
}
|
|
1369
1852
|
//#endregion
|
|
1370
1853
|
//#region src/feedback/ui/launcher.tsx
|
|
1371
|
-
|
|
1372
|
-
* Never changes size between states — the panel opening must not move the
|
|
1373
|
-
* button out from under the pointer.
|
|
1374
|
-
*/
|
|
1375
|
-
function Launcher({ isOpen, label, onClick }) {
|
|
1854
|
+
function Launcher({ isOpen, label, onClick, buttonRef }) {
|
|
1376
1855
|
return /* @__PURE__ */ jsxs("button", {
|
|
1377
1856
|
"aria-expanded": isOpen,
|
|
1378
|
-
className: "launcher",
|
|
1857
|
+
className: "launcher glass",
|
|
1379
1858
|
"data-open": isOpen,
|
|
1380
1859
|
onClick,
|
|
1860
|
+
ref: buttonRef,
|
|
1381
1861
|
type: "button",
|
|
1382
1862
|
children: [/* @__PURE__ */ jsxs("span", {
|
|
1383
1863
|
className: "launcher-icon",
|
|
@@ -1386,277 +1866,571 @@ function Launcher({ isOpen, label, onClick }) {
|
|
|
1386
1866
|
});
|
|
1387
1867
|
}
|
|
1388
1868
|
//#endregion
|
|
1389
|
-
//#region src/feedback/ui/
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
},
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
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",
|
|
1904
|
+
children: [
|
|
1905
|
+
/* @__PURE__ */ jsx(PencilIcon, {}),
|
|
1906
|
+
/* @__PURE__ */ jsx("span", {
|
|
1907
|
+
className: "attachment-caption-label",
|
|
1908
|
+
children: labels.screenshot
|
|
1909
|
+
}),
|
|
1910
|
+
multiple && /* @__PURE__ */ jsx("span", { children: number }),
|
|
1911
|
+
draft.annotations.length > 0 && /* @__PURE__ */ jsx("span", {
|
|
1912
|
+
className: "annotation-count",
|
|
1913
|
+
children: draft.annotations.length
|
|
1914
|
+
})
|
|
1915
|
+
]
|
|
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
|
+
})]
|
|
1410
1940
|
})]
|
|
1411
1941
|
});
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
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
|
+
})]
|
|
1417
1974
|
});
|
|
1418
|
-
|
|
1975
|
+
}
|
|
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);
|
|
1419
1984
|
return /* @__PURE__ */ jsxs("div", {
|
|
1420
|
-
className: "
|
|
1421
|
-
children: [
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
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
|
+
})]
|
|
1429
2012
|
})
|
|
1430
|
-
}),
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
children:
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
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",
|
|
1438
2040
|
children: [
|
|
1439
|
-
|
|
1440
|
-
"
|
|
1441
|
-
|
|
1442
|
-
|
|
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
|
+
})
|
|
1443
2066
|
]
|
|
1444
|
-
})
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
onClick: state.removeCapture,
|
|
1469
|
-
title: labels.removeScreenshot,
|
|
1470
|
-
type: "button",
|
|
1471
|
-
children: /* @__PURE__ */ jsx(TrashIcon, {})
|
|
1472
|
-
})
|
|
1473
|
-
]
|
|
1474
|
-
})
|
|
1475
|
-
]
|
|
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
|
+
})]
|
|
1476
2091
|
});
|
|
1477
2092
|
}
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
});
|
|
1487
|
-
const component = selection.componentStack[0];
|
|
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 }) {
|
|
1488
2101
|
return /* @__PURE__ */ jsxs("div", {
|
|
1489
|
-
className: "
|
|
1490
|
-
children: [
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
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", {
|
|
1513
2133
|
"aria-label": labels.pickElement,
|
|
1514
2134
|
className: "icon-btn",
|
|
2135
|
+
disabled: state.isEditingDisabled || state.isCapturing,
|
|
1515
2136
|
onClick: () => state.setStep("picking"),
|
|
1516
2137
|
title: labels.pickElement,
|
|
1517
2138
|
type: "button",
|
|
1518
2139
|
children: /* @__PURE__ */ jsx(TargetIcon, {})
|
|
1519
|
-
}),
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
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
|
+
})]
|
|
1529
2175
|
});
|
|
1530
2176
|
}
|
|
1531
|
-
|
|
1532
|
-
|
|
2177
|
+
//#endregion
|
|
2178
|
+
//#region src/feedback/ui/panel.tsx
|
|
2179
|
+
function FeedbackPanel({ options, labels, state }) {
|
|
2180
|
+
const panelRef = useRef(null);
|
|
1533
2181
|
useEffect(() => {
|
|
1534
|
-
|
|
2182
|
+
panelRef.current?.focus();
|
|
1535
2183
|
}, []);
|
|
1536
|
-
if (state.step === "success") return /* @__PURE__ */
|
|
2184
|
+
if (state.step === "success") return /* @__PURE__ */ jsxs("section", {
|
|
1537
2185
|
"aria-live": "polite",
|
|
1538
2186
|
className: "panel",
|
|
1539
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
1540
|
-
className: "done",
|
|
2187
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2188
|
+
className: "done success-panel glass",
|
|
1541
2189
|
children: [
|
|
1542
2190
|
/* @__PURE__ */ jsx(CheckIcon, {}),
|
|
1543
2191
|
/* @__PURE__ */ jsx("h2", { children: labels.successTitle }),
|
|
1544
2192
|
/* @__PURE__ */ jsx("p", { children: labels.successMessage })
|
|
1545
2193
|
]
|
|
1546
|
-
})
|
|
2194
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2195
|
+
className: "composer-toolbar",
|
|
2196
|
+
children: options.floatingControls
|
|
2197
|
+
})]
|
|
1547
2198
|
});
|
|
1548
2199
|
return /* @__PURE__ */ jsxs("section", {
|
|
1549
2200
|
"aria-label": labels.title,
|
|
1550
2201
|
className: "panel",
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
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,
|
|
1559
2416
|
className: "icon-btn",
|
|
1560
|
-
onClick:
|
|
2417
|
+
onClick: minimize,
|
|
2418
|
+
title: labels.minimize,
|
|
1561
2419
|
type: "button",
|
|
1562
|
-
children: /* @__PURE__ */ jsx(
|
|
1563
|
-
})
|
|
1564
|
-
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
const { icon: Icon, labelKey } = CATEGORY_META[value];
|
|
1577
|
-
return /* @__PURE__ */ jsxs("button", {
|
|
1578
|
-
"aria-pressed": state.category === value,
|
|
1579
|
-
onClick: () => state.setCategory(value),
|
|
1580
|
-
type: "button",
|
|
1581
|
-
children: [/* @__PURE__ */ jsx(Icon, {}), labels[labelKey]]
|
|
1582
|
-
}, value);
|
|
1583
|
-
})
|
|
1584
|
-
}),
|
|
1585
|
-
/* @__PURE__ */ jsx("textarea", {
|
|
1586
|
-
"aria-label": labels.descriptionPlaceholder,
|
|
1587
|
-
maxLength: 4e3,
|
|
1588
|
-
onChange: (event) => state.setMessage(event.target.value),
|
|
1589
|
-
placeholder: labels.descriptionPlaceholder,
|
|
1590
|
-
ref: messageRef,
|
|
1591
|
-
value: state.message
|
|
1592
|
-
}),
|
|
1593
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1594
|
-
className: "attachments",
|
|
1595
|
-
children: [/* @__PURE__ */ jsx(Attachment, {
|
|
1596
|
-
labels,
|
|
1597
|
-
state
|
|
1598
|
-
}), /* @__PURE__ */ jsx(Selection, {
|
|
1599
|
-
labels,
|
|
1600
|
-
state
|
|
1601
|
-
})]
|
|
1602
|
-
}),
|
|
1603
|
-
state.isAnonymous && /* @__PURE__ */ jsx("input", {
|
|
1604
|
-
"aria-label": labels.emailLabel,
|
|
1605
|
-
autoComplete: "email",
|
|
1606
|
-
onChange: (event) => state.setEmail(event.target.value),
|
|
1607
|
-
placeholder: labels.emailPlaceholder,
|
|
1608
|
-
type: "email",
|
|
1609
|
-
value: state.email
|
|
1610
|
-
}),
|
|
1611
|
-
/* @__PURE__ */ jsx("div", {
|
|
1612
|
-
"aria-hidden": "true",
|
|
1613
|
-
className: "hp",
|
|
1614
|
-
children: /* @__PURE__ */ jsx("input", {
|
|
1615
|
-
autoComplete: "off",
|
|
1616
|
-
name: "company-website",
|
|
1617
|
-
onChange: (event) => state.setHoneypot(event.target.value),
|
|
1618
|
-
tabIndex: -1,
|
|
1619
|
-
value: state.honeypot
|
|
1620
|
-
})
|
|
1621
|
-
}),
|
|
1622
|
-
state.error && /* @__PURE__ */ jsx("p", {
|
|
1623
|
-
className: "error",
|
|
1624
|
-
children: state.error
|
|
1625
|
-
}),
|
|
1626
|
-
/* @__PURE__ */ jsx("button", {
|
|
1627
|
-
className: "submit",
|
|
1628
|
-
disabled: state.isSubmitting || !state.message.trim(),
|
|
1629
|
-
type: "submit",
|
|
1630
|
-
children: state.isSubmitting ? /* @__PURE__ */ jsx("span", { className: "spinner" }) : labels.submit
|
|
1631
|
-
})
|
|
1632
|
-
]
|
|
1633
|
-
}),
|
|
1634
|
-
/* @__PURE__ */ jsxs("footer", {
|
|
1635
|
-
className: "footer",
|
|
1636
|
-
"data-dismissible": state.canDismiss,
|
|
1637
|
-
children: [state.canDismiss && /* @__PURE__ */ jsx("button", {
|
|
1638
|
-
className: "dismiss-btn",
|
|
1639
|
-
onClick: state.dismiss,
|
|
1640
|
-
type: "button",
|
|
1641
|
-
children: labels.dismissForDays.replace("{days}", String(state.dismissForDays))
|
|
1642
|
-
}), /* @__PURE__ */ jsxs("span", { children: [
|
|
1643
|
-
"Powered by",
|
|
1644
|
-
" ",
|
|
1645
|
-
/* @__PURE__ */ jsx("a", {
|
|
1646
|
-
href: "https://reflet.app",
|
|
1647
|
-
rel: "noopener noreferrer",
|
|
1648
|
-
target: "_blank",
|
|
1649
|
-
children: "Reflet"
|
|
1650
|
-
}),
|
|
1651
|
-
/* @__PURE__ */ jsx("span", {
|
|
1652
|
-
className: "version-info",
|
|
1653
|
-
role: "note",
|
|
1654
|
-
title: `Reflet SDK ${SDK_VERSION}`,
|
|
1655
|
-
children: /* @__PURE__ */ jsx(InfoIcon, {})
|
|
1656
|
-
})
|
|
1657
|
-
] })]
|
|
1658
|
-
})
|
|
1659
|
-
]
|
|
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
|
+
})]
|
|
1660
2434
|
});
|
|
1661
2435
|
}
|
|
1662
2436
|
//#endregion
|
|
@@ -1763,29 +2537,30 @@ function ElementPicker({ hint, onCancel, onPick }) {
|
|
|
1763
2537
|
}
|
|
1764
2538
|
//#endregion
|
|
1765
2539
|
//#region src/feedback/ui/styles.ts
|
|
1766
|
-
/**
|
|
1767
|
-
* Styles for the feedback widget. Injected into a shadow root, so nothing here
|
|
1768
|
-
* leaks into the host app and nothing from the host app leaks in.
|
|
1769
|
-
*/
|
|
1770
2540
|
const WIDGET_STYLES = `
|
|
1771
2541
|
:host {
|
|
2542
|
+
--rf-offset: 20px;
|
|
1772
2543
|
--rf-bg: #ffffff;
|
|
1773
2544
|
--rf-bg-subtle: rgb(55 53 47 / 4%);
|
|
1774
2545
|
--rf-bg-hover: rgb(55 53 47 / 7%);
|
|
1775
2546
|
--rf-border: rgb(55 53 47 / 9%);
|
|
1776
2547
|
--rf-text: #37352f;
|
|
1777
|
-
--rf-text-muted: rgb(55 53 47 /
|
|
2548
|
+
--rf-text-muted: rgb(55 53 47 / 68%);
|
|
1778
2549
|
--rf-primary: #37352f;
|
|
1779
2550
|
--rf-primary-text: #ffffff;
|
|
1780
2551
|
--rf-accent: #2383e2;
|
|
1781
2552
|
--rf-danger: #d44c47;
|
|
1782
2553
|
--rf-success: #448361;
|
|
1783
|
-
--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%);
|
|
1784
2560
|
--rf-radius-sm: 8px;
|
|
1785
2561
|
--rf-shadow-inset: inset 0 0 0 1px var(--rf-border);
|
|
1786
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%);
|
|
1787
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%);
|
|
1788
|
-
--rf-ring: 0 0 0 3px color-mix(in srgb, var(--rf-accent) 20%, transparent);
|
|
1789
2564
|
--rf-ease: cubic-bezier(0.16, 1, 0.3, 1);
|
|
1790
2565
|
color-scheme: light;
|
|
1791
2566
|
all: initial;
|
|
@@ -1794,349 +2569,258 @@ const WIDGET_STYLES = `
|
|
|
1794
2569
|
|
|
1795
2570
|
:host([data-theme="dark"]) {
|
|
1796
2571
|
--rf-bg: #202020;
|
|
1797
|
-
--rf-bg-subtle: rgb(255 255 255 / 5%);
|
|
1798
|
-
--rf-bg-hover: rgb(255 255 255 / 9%);
|
|
1799
|
-
--rf-border: rgb(255 255 255 / 9%);
|
|
1800
|
-
--rf-text: #ebeae8;
|
|
1801
|
-
--rf-text-muted: rgb(255 255 255 /
|
|
1802
|
-
--rf-
|
|
1803
|
-
--rf-
|
|
1804
|
-
--rf-
|
|
1805
|
-
--rf-
|
|
1806
|
-
--rf-
|
|
1807
|
-
--rf-
|
|
1808
|
-
--rf-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
.root {
|
|
1815
|
-
position: fixed;
|
|
1816
|
-
z-index: 2147483000;
|
|
1817
|
-
display: flex;
|
|
1818
|
-
flex-direction: column;
|
|
1819
|
-
gap: 10px;
|
|
1820
|
-
font-family: inherit;
|
|
1821
|
-
font-size: 13.5px;
|
|
1822
|
-
line-height: 1.45;
|
|
1823
|
-
color: var(--rf-text);
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
.root[data-position$="right"] { right: var(--rf-offset); align-items: flex-end; }
|
|
1827
|
-
.root[data-position$="left"] { left: var(--rf-offset); align-items: flex-start; }
|
|
1828
|
-
.root[data-position^="bottom"] { bottom: var(--rf-offset); }
|
|
1829
|
-
.root[data-position^="top"] { top: var(--rf-offset); flex-direction: column-reverse; }
|
|
1830
|
-
|
|
1831
|
-
button {
|
|
1832
|
-
font: inherit;
|
|
1833
|
-
color: inherit;
|
|
1834
|
-
margin: 0;
|
|
1835
|
-
cursor: pointer;
|
|
1836
|
-
border: 0;
|
|
1837
|
-
background: none;
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
:focus-visible { outline: none; box-shadow: var(--rf-ring); }
|
|
1841
|
-
|
|
1842
|
-
.launcher {
|
|
1843
|
-
display: inline-flex;
|
|
1844
|
-
align-items: center;
|
|
1845
|
-
gap: 6px;
|
|
1846
|
-
height: 32px;
|
|
1847
|
-
padding: 0 11px;
|
|
1848
|
-
border-radius: var(--rf-radius-sm);
|
|
1849
|
-
background: var(--rf-primary);
|
|
1850
|
-
color: var(--rf-primary-text);
|
|
1851
|
-
font-size: 13px;
|
|
1852
|
-
font-weight: 500;
|
|
1853
|
-
letter-spacing: -0.005em;
|
|
1854
|
-
box-shadow: var(--rf-shadow-low);
|
|
1855
|
-
transition: transform 240ms var(--rf-ease), box-shadow 240ms var(--rf-ease), opacity 160ms var(--rf-ease);
|
|
1856
|
-
}
|
|
1857
|
-
.launcher:hover { transform: translateY(-1px); box-shadow: var(--rf-shadow); }
|
|
1858
|
-
.launcher:active { transform: translateY(0) scale(0.98); }
|
|
1859
|
-
|
|
1860
|
-
.launcher-icon { position: relative; width: 15px; height: 15px; flex: none; }
|
|
1861
|
-
.launcher-icon svg { position: absolute; inset: 0; width: 100%; height: 100%; transition: opacity 200ms var(--rf-ease), transform 320ms var(--rf-ease); }
|
|
1862
|
-
.launcher-icon .on-close { opacity: 0; transform: rotate(-90deg) scale(0.6); }
|
|
1863
|
-
.launcher[data-open="true"] .on-open { opacity: 0; transform: rotate(90deg) scale(0.6); }
|
|
1864
|
-
.launcher[data-open="true"] .on-close { opacity: 1; transform: none; }
|
|
1865
|
-
|
|
1866
|
-
.panel {
|
|
1867
|
-
width: min(352px, calc(100vw - 24px));
|
|
1868
|
-
max-height: min(600px, calc(100vh - 96px));
|
|
1869
|
-
display: flex;
|
|
1870
|
-
flex-direction: column;
|
|
1871
|
-
overflow: hidden;
|
|
1872
|
-
background: var(--rf-bg);
|
|
1873
|
-
border-radius: var(--rf-radius);
|
|
1874
|
-
box-shadow: var(--rf-shadow);
|
|
1875
|
-
animation: rf-in 220ms var(--rf-ease);
|
|
1876
|
-
}
|
|
1877
|
-
.root[data-position^="bottom"] .panel { transform-origin: bottom center; }
|
|
1878
|
-
.root[data-position^="top"] .panel { transform-origin: top center; animation-name: rf-in-top; }
|
|
1879
|
-
|
|
1880
|
-
@keyframes rf-in {
|
|
1881
|
-
from { opacity: 0; transform: translateY(6px) scale(0.985); }
|
|
1882
|
-
}
|
|
1883
|
-
@keyframes rf-in-top {
|
|
1884
|
-
from { opacity: 0; transform: translateY(-6px) scale(0.985); }
|
|
1885
|
-
}
|
|
1886
|
-
@keyframes rf-fade { from { opacity: 0; } }
|
|
1887
|
-
|
|
1888
|
-
@media (prefers-reduced-motion: reduce) {
|
|
1889
|
-
*, *::before, *::after { animation: none !important; transition: none !important; }
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
.head {
|
|
1893
|
-
display: flex;
|
|
1894
|
-
align-items: center;
|
|
1895
|
-
gap: 8px;
|
|
1896
|
-
padding: 12px 10px 4px 14px;
|
|
1897
|
-
}
|
|
1898
|
-
.head h2 { margin: 0; font-size: 13.5px; font-weight: 600; letter-spacing: -0.01em; }
|
|
1899
|
-
.head .spacer { flex: 1; }
|
|
1900
|
-
|
|
1901
|
-
.icon-btn {
|
|
1902
|
-
display: inline-flex;
|
|
1903
|
-
align-items: center;
|
|
1904
|
-
justify-content: center;
|
|
1905
|
-
width: 26px;
|
|
1906
|
-
height: 26px;
|
|
1907
|
-
flex: none;
|
|
1908
|
-
border-radius: 6px;
|
|
1909
|
-
color: var(--rf-text-muted);
|
|
1910
|
-
transition: background 120ms var(--rf-ease), color 120ms var(--rf-ease);
|
|
1911
|
-
}
|
|
1912
|
-
.icon-btn:hover { background: var(--rf-bg-hover); color: var(--rf-text); }
|
|
1913
|
-
|
|
1914
|
-
.body { display: flex; flex-direction: column; gap: 8px; padding: 6px 14px 14px; overflow-y: auto; }
|
|
1915
|
-
|
|
1916
|
-
.segmented {
|
|
1917
|
-
border: 0;
|
|
1918
|
-
margin: 0;
|
|
1919
|
-
min-inline-size: 0;
|
|
1920
|
-
display: grid;
|
|
1921
|
-
grid-auto-flow: column;
|
|
1922
|
-
grid-auto-columns: 1fr;
|
|
1923
|
-
gap: 2px;
|
|
1924
|
-
padding: 2px;
|
|
1925
|
-
background: var(--rf-bg-subtle);
|
|
1926
|
-
border-radius: var(--rf-radius-sm);
|
|
1927
|
-
}
|
|
1928
|
-
.segmented button {
|
|
1929
|
-
display: flex;
|
|
1930
|
-
align-items: center;
|
|
1931
|
-
justify-content: center;
|
|
1932
|
-
gap: 5px;
|
|
1933
|
-
height: 26px;
|
|
1934
|
-
border-radius: 6px;
|
|
1935
|
-
font-size: 12.5px;
|
|
1936
|
-
font-weight: 500;
|
|
1937
|
-
color: var(--rf-text-muted);
|
|
1938
|
-
transition: background 160ms var(--rf-ease), color 160ms var(--rf-ease);
|
|
1939
|
-
}
|
|
1940
|
-
.segmented button:hover { color: var(--rf-text); }
|
|
1941
|
-
.segmented button[aria-pressed="true"] {
|
|
1942
|
-
background: var(--rf-bg);
|
|
1943
|
-
color: var(--rf-text);
|
|
1944
|
-
box-shadow: var(--rf-shadow-low);
|
|
1945
|
-
}
|
|
1946
|
-
|
|
1947
|
-
textarea, input[type="email"] {
|
|
1948
|
-
width: 100%;
|
|
1949
|
-
padding: 8px 10px;
|
|
1950
|
-
font: inherit;
|
|
1951
|
-
color: var(--rf-text);
|
|
1952
|
-
background: var(--rf-bg);
|
|
1953
|
-
border: 1px solid var(--rf-border);
|
|
1954
|
-
border-radius: var(--rf-radius-sm);
|
|
1955
|
-
transition: border-color 160ms var(--rf-ease), box-shadow 160ms var(--rf-ease);
|
|
1956
|
-
}
|
|
1957
|
-
textarea { min-height: 84px; max-height: 220px; resize: none; }
|
|
1958
|
-
textarea::placeholder, input::placeholder { color: var(--rf-text-muted); }
|
|
1959
|
-
textarea:focus, input:focus { outline: none; border-color: color-mix(in srgb, var(--rf-accent) 55%, transparent); box-shadow: var(--rf-ring); }
|
|
1960
|
-
|
|
1961
|
-
.attachments { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
1962
|
-
|
|
1963
|
-
.card {
|
|
1964
|
-
flex: 1 0 100%;
|
|
1965
|
-
display: flex;
|
|
1966
|
-
align-items: center;
|
|
1967
|
-
gap: 10px;
|
|
1968
|
-
min-width: 0;
|
|
1969
|
-
padding: 6px;
|
|
1970
|
-
border-radius: 10px;
|
|
1971
|
-
background: var(--rf-bg-subtle);
|
|
1972
|
-
animation: rf-fade 200ms var(--rf-ease);
|
|
1973
|
-
}
|
|
1974
|
-
.thumb {
|
|
1975
|
-
position: relative;
|
|
1976
|
-
flex: 0 0 60px;
|
|
1977
|
-
height: 42px;
|
|
1978
|
-
overflow: hidden;
|
|
1979
|
-
border-radius: 6px;
|
|
1980
|
-
background: var(--rf-bg);
|
|
1981
|
-
box-shadow: var(--rf-shadow-inset);
|
|
1982
|
-
}
|
|
1983
|
-
.thumb img { width: 100%; height: 100%; object-fit: cover; object-position: top center; display: block; }
|
|
1984
|
-
.thumb.contain img { object-fit: contain; object-position: center; }
|
|
1985
|
-
.card .meta { display: flex; flex-direction: column; justify-content: center; gap: 1px; min-width: 0; }
|
|
1986
|
-
.card .meta strong { font-size: 12.5px; font-weight: 550; }
|
|
1987
|
-
.card .meta span { font-size: 11.5px; color: var(--rf-text-muted); }
|
|
1988
|
-
.card .actions { display: flex; gap: 2px; margin-left: auto; align-items: center; }
|
|
1989
|
-
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1990
|
-
.card code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; }
|
|
1991
|
-
|
|
1992
|
-
.ghost-btn {
|
|
1993
|
-
flex: 0 0 auto;
|
|
1994
|
-
display: inline-flex;
|
|
1995
|
-
align-items: center;
|
|
1996
|
-
gap: 6px;
|
|
1997
|
-
height: 30px;
|
|
1998
|
-
padding: 0 10px;
|
|
1999
|
-
font-size: 12.5px;
|
|
2000
|
-
font-weight: 500;
|
|
2001
|
-
color: var(--rf-text-muted);
|
|
2002
|
-
border-radius: var(--rf-radius-sm);
|
|
2003
|
-
box-shadow: var(--rf-shadow-inset);
|
|
2004
|
-
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;
|
|
2005
2588
|
}
|
|
2006
|
-
.ghost-btn:hover { background: var(--rf-bg-subtle); color: var(--rf-text); }
|
|
2007
2589
|
|
|
2008
|
-
|
|
2009
|
-
display: inline-flex;
|
|
2010
|
-
align-items: center;
|
|
2011
|
-
justify-content: center;
|
|
2012
|
-
gap: 8px;
|
|
2013
|
-
width: 100%;
|
|
2014
|
-
height: 32px;
|
|
2015
|
-
margin-top: 2px;
|
|
2016
|
-
font-size: 13px;
|
|
2017
|
-
font-weight: 550;
|
|
2018
|
-
color: var(--rf-primary-text);
|
|
2019
|
-
background: var(--rf-primary);
|
|
2020
|
-
border-radius: var(--rf-radius-sm);
|
|
2021
|
-
transition: opacity 160ms var(--rf-ease), transform 160ms var(--rf-ease);
|
|
2022
|
-
}
|
|
2023
|
-
.submit:hover:not(:disabled) { opacity: 0.88; }
|
|
2024
|
-
.submit:active:not(:disabled) { transform: scale(0.99); }
|
|
2025
|
-
.submit:disabled { opacity: 0.35; cursor: not-allowed; }
|
|
2590
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
2026
2591
|
|
|
2027
|
-
.error { margin: 0; font-size: 12px; color: var(--rf-danger); }
|
|
2028
2592
|
|
|
2029
|
-
.
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
border-top: 1px solid var(--rf-border);
|
|
2593
|
+
.root {
|
|
2594
|
+
position: fixed;
|
|
2595
|
+
z-index: 2147483000;
|
|
2033
2596
|
display: flex;
|
|
2034
|
-
align-items:
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
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);
|
|
2039
2601
|
}
|
|
2040
|
-
.
|
|
2041
|
-
.
|
|
2042
|
-
.
|
|
2043
|
-
.
|
|
2044
|
-
.
|
|
2045
|
-
.
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
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);
|
|
2050
2620
|
}
|
|
2051
|
-
.
|
|
2052
|
-
|
|
2053
|
-
|
|
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; }
|
|
2054
2643
|
.done svg { color: var(--rf-success); }
|
|
2055
2644
|
.done h2 { margin: 0; font-size: 14px; font-weight: 600; }
|
|
2056
|
-
.done p { margin: 0; font-size:
|
|
2057
|
-
|
|
2058
|
-
.spinner {
|
|
2059
|
-
width: 14px;
|
|
2060
|
-
height: 14px;
|
|
2061
|
-
border: 2px solid currentColor;
|
|
2062
|
-
border-right-color: transparent;
|
|
2063
|
-
border-radius: 50%;
|
|
2064
|
-
opacity: 0.6;
|
|
2065
|
-
animation: rf-spin 700ms linear infinite;
|
|
2066
|
-
}
|
|
2067
|
-
@keyframes rf-spin { to { transform: rotate(360deg); } }
|
|
2068
|
-
|
|
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; }
|
|
2069
2647
|
.hp { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
|
2070
|
-
|
|
2071
|
-
.
|
|
2072
|
-
|
|
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;
|
|
2073
2664
|
inset: 0;
|
|
2074
|
-
z-index:
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
backdrop-filter: blur(3px);
|
|
2079
|
-
animation: rf-fade 180ms var(--rf-ease);
|
|
2665
|
+
z-index: -1;
|
|
2666
|
+
border-radius: inherit;
|
|
2667
|
+
background: var(--rf-bg);
|
|
2668
|
+
pointer-events: none;
|
|
2080
2669
|
}
|
|
2081
|
-
|
|
2082
|
-
.
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
box-shadow: 0 20px 60px -16px rgb(0 0 0 / 70%);
|
|
2088
|
-
cursor: crosshair;
|
|
2089
|
-
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
|
+
}
|
|
2090
2676
|
}
|
|
2091
|
-
|
|
2092
|
-
.
|
|
2093
|
-
display: flex;
|
|
2094
|
-
align-items: center;
|
|
2095
|
-
justify-content: center;
|
|
2096
|
-
gap: 6px;
|
|
2097
|
-
flex-wrap: wrap;
|
|
2098
|
-
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; }
|
|
2099
2679
|
}
|
|
2100
|
-
.
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
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; }
|
|
2108
2761
|
}
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
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; }
|
|
2118
2822
|
}
|
|
2119
|
-
|
|
2120
|
-
.tool[aria-pressed="true"] { background: var(--rf-bg-hover); color: var(--rf-text); }
|
|
2121
|
-
.swatch {
|
|
2122
|
-
width: 20px;
|
|
2123
|
-
height: 20px;
|
|
2124
|
-
border-radius: 50%;
|
|
2125
|
-
border: 2px solid transparent;
|
|
2126
|
-
transition: transform 200ms var(--rf-ease), border-color 160ms var(--rf-ease);
|
|
2127
|
-
}
|
|
2128
|
-
.swatch[aria-pressed="true"] { border-color: var(--rf-text); transform: scale(1.15); }
|
|
2129
|
-
.toolbar .done-btn {
|
|
2130
|
-
height: 30px;
|
|
2131
|
-
padding: 0 13px;
|
|
2132
|
-
font-size: 13px;
|
|
2133
|
-
font-weight: 550;
|
|
2134
|
-
border-radius: 7px;
|
|
2135
|
-
color: var(--rf-primary-text);
|
|
2136
|
-
background: var(--rf-primary);
|
|
2137
|
-
transition: opacity 160ms var(--rf-ease);
|
|
2138
|
-
}
|
|
2139
|
-
.toolbar .done-btn:hover { opacity: 0.88; }
|
|
2823
|
+
|
|
2140
2824
|
|
|
2141
2825
|
.picker-box {
|
|
2142
2826
|
position: fixed;
|
|
@@ -2193,26 +2877,9 @@ textarea:focus, input:focus { outline: none; border-color: color-mix(in srgb, va
|
|
|
2193
2877
|
box-shadow: var(--rf-shadow-inset);
|
|
2194
2878
|
}
|
|
2195
2879
|
|
|
2196
|
-
@media (pointer: coarse) {
|
|
2197
|
-
.icon-btn, .tool, .swatch {
|
|
2198
|
-
width: 44px;
|
|
2199
|
-
height: 44px;
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
.launcher, .segmented button, .ghost-btn, .submit, .toolbar .done-btn {
|
|
2203
|
-
min-height: 44px;
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
.dismiss-btn, .footer a {
|
|
2207
|
-
display: inline-flex;
|
|
2208
|
-
min-height: 44px;
|
|
2209
|
-
align-items: center;
|
|
2210
|
-
}
|
|
2211
2880
|
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
justify-content: center;
|
|
2215
|
-
}
|
|
2881
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2882
|
+
*, *::before, *::after { animation: none !important; transition: none !important; }
|
|
2216
2883
|
}
|
|
2217
2884
|
`;
|
|
2218
2885
|
//#endregion
|
|
@@ -2462,10 +3129,55 @@ async function captureElementCloseUp(element) {
|
|
|
2462
3129
|
}
|
|
2463
3130
|
}
|
|
2464
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
|
|
2465
3178
|
//#region src/feedback/core/submit.ts
|
|
2466
3179
|
const MAX_TITLE_LENGTH = 100;
|
|
2467
3180
|
const NEWLINE = /\r?\n/;
|
|
2468
|
-
/** First line of the message, cut on a word boundary so titles stay readable. */
|
|
2469
3181
|
function deriveTitle(message) {
|
|
2470
3182
|
const firstLine = message.split(NEWLINE).map((line) => line.trim()).find(Boolean) ?? "";
|
|
2471
3183
|
if (firstLine.length <= MAX_TITLE_LENGTH) return firstLine;
|
|
@@ -2479,64 +3191,31 @@ function buildDescription(params) {
|
|
|
2479
3191
|
const contact = isAnonymous && email ? `\n\n---\nContact: ${email}` : "";
|
|
2480
3192
|
return `[${label}] ${message.trim()}${contact}`;
|
|
2481
3193
|
}
|
|
2482
|
-
async function
|
|
2483
|
-
const
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
const
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
await transport.saveScreenshot({
|
|
2493
|
-
annotatedStorageId,
|
|
2494
|
-
annotations: wireAnnotations.length > 0 ? wireAnnotations : void 0,
|
|
2495
|
-
captureSource: "widget",
|
|
2496
|
-
feedbackId,
|
|
2497
|
-
filename: "screenshot.png",
|
|
2498
|
-
height: screenshot.height,
|
|
2499
|
-
mimeType: screenshot.mimeType,
|
|
2500
|
-
pageUrl: context.url,
|
|
2501
|
-
size: screenshot.blob.size,
|
|
2502
|
-
storageId,
|
|
2503
|
-
width: screenshot.width
|
|
2504
|
-
});
|
|
2505
|
-
return true;
|
|
2506
|
-
}
|
|
2507
|
-
async function attachElement(transport, feedbackId, submission) {
|
|
2508
|
-
const { context, element } = submission;
|
|
2509
|
-
if (!element) return;
|
|
2510
|
-
await transport.saveScreenshot({
|
|
2511
|
-
captureSource: "element",
|
|
2512
|
-
feedbackId,
|
|
2513
|
-
filename: "element.png",
|
|
2514
|
-
height: element.height,
|
|
2515
|
-
mimeType: element.mimeType,
|
|
2516
|
-
pageUrl: context.url,
|
|
2517
|
-
size: element.blob.size,
|
|
2518
|
-
storageId: await upload(transport, element),
|
|
2519
|
-
width: element.width
|
|
2520
|
-
});
|
|
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
|
+
};
|
|
2521
3204
|
}
|
|
2522
|
-
|
|
2523
|
-
* Creates the feedback first, then attaches the capture. A failed upload is
|
|
2524
|
-
* reported back but never discards what the user wrote.
|
|
2525
|
-
*/
|
|
2526
|
-
async function submitWidgetFeedback(transport, submission) {
|
|
2527
|
-
const title = deriveTitle(submission.message);
|
|
2528
|
-
if (!title) throw new Error("Please describe your feedback before sending it.");
|
|
3205
|
+
async function createReportWithScreenshots(transport, submission, pendingScreenshots) {
|
|
2529
3206
|
const { feedbackId } = await transport.create({
|
|
2530
3207
|
context: submission.context,
|
|
2531
3208
|
description: buildDescription(submission),
|
|
2532
|
-
title
|
|
3209
|
+
title: deriveTitle(submission.message)
|
|
2533
3210
|
});
|
|
2534
|
-
|
|
2535
|
-
await attachElement(transport, feedbackId, submission).catch(() => void 0);
|
|
2536
|
-
return {
|
|
3211
|
+
return attachPreparedScreenshots(transport, {
|
|
2537
3212
|
feedbackId,
|
|
2538
|
-
|
|
2539
|
-
};
|
|
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));
|
|
2540
3219
|
}
|
|
2541
3220
|
function isStorageResponse(value) {
|
|
2542
3221
|
return typeof value === "object" && value !== null && typeof Reflect.get(value, "storageId") === "string";
|
|
@@ -2560,6 +3239,220 @@ function createFeedbackTransport(client) {
|
|
|
2560
3239
|
};
|
|
2561
3240
|
}
|
|
2562
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
|
|
2563
3456
|
//#region src/feedback/ui/use-capture-sync.ts
|
|
2564
3457
|
const RESIZE_DEBOUNCE = 400;
|
|
2565
3458
|
/**
|
|
@@ -2609,7 +3502,6 @@ function useCaptureSync(enabled, refresh) {
|
|
|
2609
3502
|
const SUCCESS_CLOSE_DELAY = 2400;
|
|
2610
3503
|
const IS_APPLE = /Mac|iPhone|iPad/;
|
|
2611
3504
|
const MILLISECONDS_PER_DAY = 864e5;
|
|
2612
|
-
const DISMISSAL_STORAGE_PREFIX = "reflet-feedback-dismissed";
|
|
2613
3505
|
function readDismissedUntil(key) {
|
|
2614
3506
|
if (typeof window === "undefined") return 0;
|
|
2615
3507
|
try {
|
|
@@ -2626,10 +3518,6 @@ function writeDismissedUntil(key, until) {
|
|
|
2626
3518
|
window.localStorage.setItem(key, String(until));
|
|
2627
3519
|
} catch {}
|
|
2628
3520
|
}
|
|
2629
|
-
/**
|
|
2630
|
-
* Matches shortcuts written as `alt+f` or `mod+shift+k`, where `mod` is the
|
|
2631
|
-
* platform's command key.
|
|
2632
|
-
*/
|
|
2633
3521
|
function matchesHotkey(event, hotkey, platform = typeof navigator === "undefined" ? "" : navigator.platform) {
|
|
2634
3522
|
const parts = hotkey.toLowerCase().split("+").map((part) => part.trim());
|
|
2635
3523
|
const key = parts.at(-1);
|
|
@@ -2644,48 +3532,27 @@ function matchesHotkey(event, hotkey, platform = typeof navigator === "undefined
|
|
|
2644
3532
|
};
|
|
2645
3533
|
return event.altKey === expected.alt && event.ctrlKey === expected.ctrl && event.metaKey === expected.meta && event.shiftKey === expected.shift;
|
|
2646
3534
|
}
|
|
2647
|
-
/**
|
|
2648
|
-
* A capture whose object URL is revoked the moment it is replaced or dropped —
|
|
2649
|
-
* holding one without this leaks the blob for the lifetime of the page.
|
|
2650
|
-
*/
|
|
2651
|
-
function useOwnedCapture() {
|
|
2652
|
-
const [image, setImage] = useState(null);
|
|
2653
|
-
const ref = useRef(null);
|
|
2654
|
-
useEffect(() => {
|
|
2655
|
-
ref.current = image;
|
|
2656
|
-
}, [image]);
|
|
2657
|
-
useEffect(() => () => releaseCapture(ref.current), []);
|
|
2658
|
-
return [image, useCallback((next) => {
|
|
2659
|
-
releaseCapture(ref.current);
|
|
2660
|
-
setImage(next);
|
|
2661
|
-
}, [])];
|
|
2662
|
-
}
|
|
2663
3535
|
function useWidgetState(props) {
|
|
2664
|
-
const
|
|
2665
|
-
const publicKey = props.publicKey ?? context?.publicKey;
|
|
2666
|
-
const baseUrl = props.baseUrl ?? context?.baseUrl;
|
|
2667
|
-
const user = props.user ?? context?.user;
|
|
2668
|
-
const userToken = props.userToken ?? context?.userToken;
|
|
2669
|
-
const isAnonymous = !(user || userToken);
|
|
2670
|
-
const dismissForDays = props.dismissForDays !== void 0 && Number.isFinite(props.dismissForDays) && props.dismissForDays > 0 ? props.dismissForDays : null;
|
|
2671
|
-
const dismissalKey = `${DISMISSAL_STORAGE_PREFIX}:${publicKey ?? "default"}:${user?.id ?? "anonymous"}`;
|
|
3536
|
+
const { client, defaultCategory, dismissalKey, dismissForDays, isAnonymous } = useWidgetConfig(props);
|
|
2672
3537
|
const [isOpen, setIsOpen] = useState(false);
|
|
3538
|
+
const [annotationTrigger, setAnnotationTrigger] = useState(null);
|
|
2673
3539
|
const [step, setStep] = useState("compose");
|
|
2674
|
-
const [category, setCategory] = useState(
|
|
3540
|
+
const [category, setCategory] = useState(defaultCategory);
|
|
2675
3541
|
const [message, setMessage] = useState("");
|
|
2676
3542
|
const [email, setEmail] = useState("");
|
|
2677
3543
|
const [honeypot, setHoneypot] = useState("");
|
|
2678
|
-
const [capture, replaceCapture] = useOwnedCapture();
|
|
2679
3544
|
const [elementCapture, replaceElementCapture] = useOwnedCapture();
|
|
2680
|
-
const [annotations, setAnnotations] = useState([]);
|
|
2681
3545
|
const [selection, setSelection] = useState(null);
|
|
2682
|
-
const [
|
|
2683
|
-
const
|
|
3546
|
+
const [isElementCapturing, setIsElementCapturing] = useState(false);
|
|
3547
|
+
const successTimer = useRef(null);
|
|
2684
3548
|
const [error, setError] = useState(null);
|
|
2685
3549
|
const [dismissedUntil, setDismissedUntil] = useState(() => readDismissedUntil(dismissalKey));
|
|
2686
3550
|
const pickRef = useRef(0);
|
|
3551
|
+
useEffect(() => () => {
|
|
3552
|
+
pickRef.current++;
|
|
3553
|
+
if (successTimer.current) clearTimeout(successTimer.current);
|
|
3554
|
+
}, []);
|
|
2687
3555
|
const consoleRef = useRef(null);
|
|
2688
|
-
const frozenContext = useRef(null);
|
|
2689
3556
|
useEffect(() => {
|
|
2690
3557
|
setDismissedUntil(readDismissedUntil(dismissalKey));
|
|
2691
3558
|
}, [dismissalKey]);
|
|
@@ -2698,33 +3565,45 @@ function useWidgetState(props) {
|
|
|
2698
3565
|
recorder.stop();
|
|
2699
3566
|
};
|
|
2700
3567
|
}, [props.captureConsole]);
|
|
2701
|
-
|
|
2702
|
-
const
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
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);
|
|
2712
3588
|
const reset = useCallback(() => {
|
|
3589
|
+
if (successTimer.current) clearTimeout(successTimer.current);
|
|
3590
|
+
setAnnotationTrigger(null);
|
|
3591
|
+
resetSubmission();
|
|
2713
3592
|
pickRef.current++;
|
|
2714
|
-
|
|
2715
|
-
|
|
3593
|
+
setIsElementCapturing(false);
|
|
3594
|
+
resetScreenshots();
|
|
2716
3595
|
replaceElementCapture(null);
|
|
2717
|
-
setAnnotations([]);
|
|
2718
3596
|
setSelection(null);
|
|
2719
3597
|
setMessage("");
|
|
2720
3598
|
setEmail("");
|
|
2721
3599
|
setHoneypot("");
|
|
2722
3600
|
setError(null);
|
|
2723
3601
|
setStep("compose");
|
|
2724
|
-
setCategory(
|
|
3602
|
+
setCategory(defaultCategory);
|
|
2725
3603
|
}, [
|
|
2726
|
-
|
|
2727
|
-
|
|
3604
|
+
defaultCategory,
|
|
3605
|
+
resetSubmission,
|
|
3606
|
+
resetScreenshots,
|
|
2728
3607
|
replaceElementCapture
|
|
2729
3608
|
]);
|
|
2730
3609
|
const close = useCallback(() => {
|
|
@@ -2750,14 +3629,15 @@ function useWidgetState(props) {
|
|
|
2750
3629
|
setIsOpen(true);
|
|
2751
3630
|
setStep("compose");
|
|
2752
3631
|
props.onOpen?.();
|
|
2753
|
-
if (props.captureOnOpen !== false)
|
|
3632
|
+
if (props.captureOnOpen !== false) refreshAutomatic();
|
|
2754
3633
|
}, [
|
|
2755
3634
|
props.captureOnOpen,
|
|
2756
3635
|
props.onOpen,
|
|
2757
|
-
|
|
3636
|
+
refreshAutomatic
|
|
2758
3637
|
]);
|
|
2759
|
-
/** Fresh pixels so the highlight maps onto the scroll position the element had. */
|
|
2760
3638
|
const selectElement = useCallback(async (element) => {
|
|
3639
|
+
cancelPendingCapture();
|
|
3640
|
+
setIsElementCapturing(true);
|
|
2761
3641
|
const picked = buildElementSelection(element);
|
|
2762
3642
|
setSelection(picked);
|
|
2763
3643
|
setStep("compose");
|
|
@@ -2769,89 +3649,58 @@ function useWidgetState(props) {
|
|
|
2769
3649
|
releaseCapture(closeUp);
|
|
2770
3650
|
return;
|
|
2771
3651
|
}
|
|
2772
|
-
|
|
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);
|
|
2773
3661
|
replaceElementCapture(closeUp);
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
3662
|
+
}, [
|
|
3663
|
+
cancelPendingCapture,
|
|
3664
|
+
captureFailed,
|
|
3665
|
+
storeScreenshot,
|
|
3666
|
+
replaceElementCapture
|
|
3667
|
+
]);
|
|
2777
3668
|
const clearSelection = useCallback(() => {
|
|
2778
3669
|
pickRef.current++;
|
|
2779
3670
|
replaceElementCapture(null);
|
|
2780
3671
|
setSelection(null);
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
replaceCapture(null);
|
|
2786
|
-
setAnnotations([]);
|
|
2787
|
-
}, [replaceCapture]);
|
|
2788
|
-
const client = useMemo(() => publicKey ? new Reflet({
|
|
2789
|
-
baseUrl,
|
|
2790
|
-
publicKey,
|
|
2791
|
-
user,
|
|
2792
|
-
userToken
|
|
2793
|
-
}) : null, [
|
|
2794
|
-
baseUrl,
|
|
2795
|
-
publicKey,
|
|
2796
|
-
user,
|
|
2797
|
-
userToken
|
|
2798
|
-
]);
|
|
2799
|
-
const submit = useCallback(async () => {
|
|
3672
|
+
setIsElementCapturing(false);
|
|
3673
|
+
clearSelectionAnnotations();
|
|
3674
|
+
}, [clearSelectionAnnotations, replaceElementCapture]);
|
|
3675
|
+
const submit = () => {
|
|
2800
3676
|
if (honeypot) {
|
|
2801
3677
|
setStep("success");
|
|
2802
3678
|
return;
|
|
2803
3679
|
}
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
consoleEvents: consoleRef.current?.events(),
|
|
2820
|
-
metadata: props.metadata,
|
|
2821
|
-
selection: selection ?? void 0
|
|
2822
|
-
},
|
|
2823
|
-
element: elementCapture,
|
|
2824
|
-
email,
|
|
2825
|
-
isAnonymous,
|
|
2826
|
-
message,
|
|
2827
|
-
screenshot: capture
|
|
2828
|
-
});
|
|
2829
|
-
setStep("success");
|
|
2830
|
-
props.onSubmit?.({ feedbackId: result.feedbackId });
|
|
2831
|
-
setTimeout(close, SUCCESS_CLOSE_DELAY);
|
|
2832
|
-
} catch (submitError) {
|
|
2833
|
-
setError(submitError instanceof Error ? submitError.message : "Something went wrong. Please try again.");
|
|
2834
|
-
} finally {
|
|
2835
|
-
releaseCapture(annotated);
|
|
2836
|
-
setIsSubmitting(false);
|
|
2837
|
-
}
|
|
2838
|
-
}, [
|
|
2839
|
-
annotations,
|
|
2840
|
-
capture,
|
|
2841
|
-
category,
|
|
2842
|
-
client,
|
|
2843
|
-
close,
|
|
2844
|
-
elementCapture,
|
|
2845
|
-
email,
|
|
2846
|
-
honeypot,
|
|
2847
|
-
isAnonymous,
|
|
2848
|
-
message,
|
|
2849
|
-
props.metadata,
|
|
2850
|
-
props.onSubmit,
|
|
2851
|
-
selection
|
|
2852
|
-
]);
|
|
3680
|
+
return submission.submit({
|
|
3681
|
+
category,
|
|
3682
|
+
context: {
|
|
3683
|
+
...activeScreenshot?.context ?? collectPageContext({ sdkVersion: "0.3.5" }),
|
|
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
|
+
};
|
|
2853
3695
|
return {
|
|
3696
|
+
activeScreenshot,
|
|
3697
|
+
annotateScreenshot: (id, trigger) => {
|
|
3698
|
+
setAnnotationTrigger(trigger);
|
|
3699
|
+
gallery.selectScreenshot(id);
|
|
3700
|
+
setStep("annotate");
|
|
3701
|
+
},
|
|
2854
3702
|
annotations,
|
|
3703
|
+
annotationTrigger,
|
|
2855
3704
|
canDismiss: dismissForDays !== null,
|
|
2856
3705
|
capture,
|
|
2857
3706
|
category,
|
|
@@ -2862,18 +3711,23 @@ function useWidgetState(props) {
|
|
|
2862
3711
|
elementCapture,
|
|
2863
3712
|
email,
|
|
2864
3713
|
error,
|
|
3714
|
+
hasPendingAttachments: submission.hasPendingAttachments,
|
|
2865
3715
|
honeypot,
|
|
2866
3716
|
isAnonymous,
|
|
2867
3717
|
isCapturing,
|
|
2868
3718
|
isDismissed: dismissedUntil > Date.now(),
|
|
3719
|
+
isEditingDisabled: submission.isEditingDisabled,
|
|
2869
3720
|
isOpen,
|
|
2870
|
-
isSubmitting,
|
|
3721
|
+
isSubmitting: submission.isSubmitting,
|
|
2871
3722
|
message,
|
|
2872
3723
|
open,
|
|
2873
|
-
|
|
3724
|
+
pendingCapture: gallery.pendingCapture,
|
|
3725
|
+
removeCapture: gallery.removeCapture,
|
|
3726
|
+
retakeCapture: gallery.retakeCapture,
|
|
3727
|
+
screenshots: gallery.screenshots,
|
|
2874
3728
|
selectElement,
|
|
2875
3729
|
selection,
|
|
2876
|
-
setAnnotations,
|
|
3730
|
+
setAnnotations: gallery.setAnnotations,
|
|
2877
3731
|
setCategory,
|
|
2878
3732
|
setEmail,
|
|
2879
3733
|
setHoneypot,
|
|
@@ -2881,7 +3735,7 @@ function useWidgetState(props) {
|
|
|
2881
3735
|
setStep,
|
|
2882
3736
|
step,
|
|
2883
3737
|
submit,
|
|
2884
|
-
takeCapture
|
|
3738
|
+
takeCapture: gallery.takeCapture
|
|
2885
3739
|
};
|
|
2886
3740
|
}
|
|
2887
3741
|
//#endregion
|
|
@@ -2891,14 +3745,8 @@ const DEFAULT_CATEGORIES = [
|
|
|
2891
3745
|
"idea",
|
|
2892
3746
|
"question"
|
|
2893
3747
|
];
|
|
2894
|
-
const DEFAULT_OFFSET = 20;
|
|
2895
|
-
/**
|
|
2896
|
-
* Floating feedback button with a screenshot, annotation and element picker
|
|
2897
|
-
* flow. Mount it once, anywhere below `RefletProvider` or with its own
|
|
2898
|
-
* `publicKey`.
|
|
2899
|
-
*/
|
|
2900
3748
|
function RefletFeedback(props) {
|
|
2901
|
-
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;
|
|
2902
3750
|
const labels = {
|
|
2903
3751
|
...DEFAULT_WIDGET_LABELS,
|
|
2904
3752
|
...props.labels
|
|
@@ -2923,44 +3771,38 @@ function RefletFeedback(props) {
|
|
|
2923
3771
|
isOpen,
|
|
2924
3772
|
open
|
|
2925
3773
|
]);
|
|
2926
|
-
useEffect(() => {
|
|
2927
|
-
if (!isOpen || state.step !== "compose") return;
|
|
2928
|
-
const onKeyDown = (event) => {
|
|
2929
|
-
if (event.key === "Escape") close();
|
|
2930
|
-
};
|
|
2931
|
-
document.addEventListener("keydown", onKeyDown);
|
|
2932
|
-
return () => document.removeEventListener("keydown", onKeyDown);
|
|
2933
|
-
}, [
|
|
2934
|
-
close,
|
|
2935
|
-
isOpen,
|
|
2936
|
-
state.step
|
|
2937
|
-
]);
|
|
2938
3774
|
if (!enabled || state.isDismissed) return null;
|
|
2939
|
-
const showPanel = isOpen && state.step !== "picking" && state.step !== "annotate";
|
|
2940
3775
|
return /* @__PURE__ */ jsxs(ShadowPortal, {
|
|
2941
3776
|
offset,
|
|
2942
3777
|
primaryColor: props.primaryColor,
|
|
2943
3778
|
theme,
|
|
2944
3779
|
children: [
|
|
2945
|
-
/* @__PURE__ */
|
|
2946
|
-
|
|
2947
|
-
"
|
|
2948
|
-
|
|
2949
|
-
categories,
|
|
2950
|
-
labels,
|
|
2951
|
-
state
|
|
2952
|
-
}), /* @__PURE__ */ jsx(Launcher, {
|
|
2953
|
-
isOpen,
|
|
2954
|
-
label: labels.trigger,
|
|
2955
|
-
onClick: isOpen ? close : open
|
|
2956
|
-
})]
|
|
3780
|
+
/* @__PURE__ */ jsx("div", {
|
|
3781
|
+
"aria-hidden": "true",
|
|
3782
|
+
className: "capture-halo",
|
|
3783
|
+
"data-active": state.isCapturing
|
|
2957
3784
|
}),
|
|
2958
|
-
|
|
2959
|
-
annotations: state.annotations,
|
|
2960
|
-
capture: state.capture,
|
|
3785
|
+
/* @__PURE__ */ jsx(FloatingWidget, {
|
|
2961
3786
|
labels,
|
|
2962
|
-
|
|
2963
|
-
|
|
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
|
|
2964
3806
|
}),
|
|
2965
3807
|
isOpen && state.step === "picking" && /* @__PURE__ */ jsx(ElementPicker, {
|
|
2966
3808
|
hint: labels.pickElementHint,
|