reflet-sdk 0.6.3 → 0.6.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 +21 -0
- package/dist/{devtools-layer-DtO89RlJ.js → devtools-layer-CEjzYGBB.js} +18 -12
- package/dist/devtools-layer-CEjzYGBB.js.map +1 -0
- package/dist/devtools-next.js +1 -1
- package/dist/devtools-vite.js +1 -1
- package/dist/feedback.d.ts +1 -0
- package/dist/feedback.d.ts.map +1 -1
- package/dist/feedback.js +772 -779
- package/dist/feedback.js.map +1 -1
- package/dist/{handler-C9tdKcl0.js → handler-BSNmdHXv.js} +11 -11
- package/dist/handler-BSNmdHXv.js.map +1 -0
- package/dist/{z-index-CIb4-3Fy.js → react-context-Caq1fodf.js} +13 -13
- package/dist/react-context-Caq1fodf.js.map +1 -0
- package/dist/react.js +1 -1
- package/dist/{picker-Rh-hsxyX.js → submit-MvVjNdYc.js} +162 -109
- package/dist/submit-MvVjNdYc.js.map +1 -0
- package/package.json +1 -1
- package/dist/devtools-layer-DtO89RlJ.js.map +0 -1
- package/dist/handler-C9tdKcl0.js.map +0 -1
- package/dist/picker-Rh-hsxyX.js.map +0 -1
- package/dist/z-index-CIb4-3Fy.js.map +0 -1
package/dist/feedback.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { n as Reflet } from "./client-DAdWdIFY.js";
|
|
3
|
-
import { A as
|
|
4
|
-
import {
|
|
3
|
+
import { A as SquareIcon, B as collectPageContext, C as ChevronDownIcon, D as MoreIcon, E as HighlightIcon, F as UndoIcon, H as buildElementSelection, I as isDegenerate, J as captureViewport, L as normalizeRect, M as TargetIcon, N as TextIcon, O as PencilIcon, P as TrashIcon, R as DEFAULT_WIDGET_LABELS, S as CheckIcon, T as GripIcon, V as MAX_SELECTION_COMMENT_LENGTH, X as capturedViewportSize, Z as releaseCapture, _ as matchesHotkey, b as CameraIcon, c as useFloatingPosition, d as SelectionOutline, f as drawAnnotations, g as isSubmitEnter, h as stopClicksAtHost, i as submitWidgetFeedback, k as SpotlightIcon, m as listenToKeydown, n as createFeedbackTransport, o as ElementPicker, p as listenOnPageAndWidget, q as WIDGET_MARKER, t as attachPreparedScreenshots, v as ArrowIcon, w as CloseIcon, x as ChatIcon, y as BlurIcon, z as SDK_VERSION } from "./submit-MvVjNdYc.js";
|
|
4
|
+
import { a as REFLET_Z_INDEX, t as RefletContext } from "./react-context-Caq1fodf.js";
|
|
5
5
|
import { Suspense, lazy, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
6
6
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
7
|
import { Slider } from "@base-ui/react/slider";
|
|
@@ -48,12 +48,13 @@ function TextAnnotationEditor({ draft, onCommit, onCancel }) {
|
|
|
48
48
|
},
|
|
49
49
|
onChange: (event) => setText(event.target.value),
|
|
50
50
|
onKeyDown: (event) => {
|
|
51
|
+
if (event.nativeEvent.isComposing) return;
|
|
51
52
|
if (event.key === "Escape") {
|
|
52
53
|
event.preventDefault();
|
|
53
54
|
event.stopPropagation();
|
|
54
55
|
onCancel();
|
|
55
56
|
}
|
|
56
|
-
if (event.
|
|
57
|
+
if (isSubmitEnter(event.nativeEvent)) {
|
|
57
58
|
event.preventDefault();
|
|
58
59
|
onCommit(text);
|
|
59
60
|
}
|
|
@@ -749,7 +750,7 @@ function isSelectionAnnotation({ id }) {
|
|
|
749
750
|
return id.startsWith(SELECTION_ANNOTATION_PREFIX);
|
|
750
751
|
}
|
|
751
752
|
function highlightFor(selection, capture) {
|
|
752
|
-
const scale = capture.width /
|
|
753
|
+
const scale = capture.width / capturedViewportSize().width;
|
|
753
754
|
const { rect } = selection;
|
|
754
755
|
return {
|
|
755
756
|
color: SELECTION_COLOR,
|
|
@@ -789,13 +790,23 @@ function ScreenshotCard({ item, labels, state }) {
|
|
|
789
790
|
const isCapturing = state.pendingCapture?.id === draft.id;
|
|
790
791
|
const disabled = state.isEditingDisabled || state.isCapturing;
|
|
791
792
|
const drawings = draft.annotations.filter((annotation) => !isSelectionAnnotation(annotation));
|
|
793
|
+
const openScreenshot = (button) => {
|
|
794
|
+
if (previewRef.current) state.annotateScreenshot(draft.id, {
|
|
795
|
+
button,
|
|
796
|
+
thumbnail: previewRef.current
|
|
797
|
+
});
|
|
798
|
+
};
|
|
792
799
|
return /* @__PURE__ */ jsxs("fieldset", {
|
|
793
800
|
"aria-label": `${labels.screenshot} ${number}`,
|
|
794
801
|
className: "screenshot-attachment",
|
|
795
802
|
children: [
|
|
796
|
-
/* @__PURE__ */ jsx("
|
|
803
|
+
/* @__PURE__ */ jsx("button", {
|
|
804
|
+
"aria-label": `${labels.openScreenshot} ${number}`,
|
|
797
805
|
className: "screenshot-preview glass",
|
|
806
|
+
disabled,
|
|
807
|
+
onClick: (event) => openScreenshot(event.currentTarget),
|
|
798
808
|
ref: previewRef,
|
|
809
|
+
type: "button",
|
|
799
810
|
children: /* @__PURE__ */ jsx(ScreenshotPreview, {
|
|
800
811
|
annotations: draft.annotations,
|
|
801
812
|
capture: draft.image
|
|
@@ -820,12 +831,7 @@ function ScreenshotCard({ item, labels, state }) {
|
|
|
820
831
|
"aria-label": labels.annotateHint,
|
|
821
832
|
className: "icon-btn annotate-action",
|
|
822
833
|
disabled,
|
|
823
|
-
onClick: (event) =>
|
|
824
|
-
if (previewRef.current) state.annotateScreenshot(draft.id, {
|
|
825
|
-
button: event.currentTarget,
|
|
826
|
-
thumbnail: previewRef.current
|
|
827
|
-
});
|
|
828
|
-
},
|
|
834
|
+
onClick: (event) => openScreenshot(event.currentTarget),
|
|
829
835
|
title: labels.annotateHint,
|
|
830
836
|
type: "button",
|
|
831
837
|
children: /* @__PURE__ */ jsx(PencilIcon, {})
|
|
@@ -1140,703 +1146,138 @@ function FeedbackPanel({ floatingControls, labels, state }) {
|
|
|
1140
1146
|
});
|
|
1141
1147
|
}
|
|
1142
1148
|
//#endregion
|
|
1143
|
-
//#region src/feedback/
|
|
1144
|
-
|
|
1145
|
-
const
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
return
|
|
1153
|
-
|
|
1154
|
-
return "[unserializable]";
|
|
1155
|
-
}
|
|
1156
|
-
if (typeof value === "function") return "[function]";
|
|
1157
|
-
return String(value);
|
|
1158
|
-
}
|
|
1159
|
-
function formatMessage(args) {
|
|
1160
|
-
const message = args.map(serializeArgument).join(" ");
|
|
1161
|
-
return message.length > MAX_MESSAGE_LENGTH ? `${message.slice(0, 999)}…` : message;
|
|
1162
|
-
}
|
|
1163
|
-
/**
|
|
1164
|
-
* Buffers the console noise a user saw right before reporting a bug.
|
|
1165
|
-
* Wrappers delegate to the previous implementation, so nothing is swallowed.
|
|
1166
|
-
*/
|
|
1167
|
-
function startConsoleRecorder(options) {
|
|
1168
|
-
const limit = options?.limit ?? DEFAULT_LIMIT;
|
|
1169
|
-
const buffer = [];
|
|
1170
|
-
let active = true;
|
|
1171
|
-
const push = (level, message) => {
|
|
1172
|
-
if (!(active && message)) return;
|
|
1173
|
-
buffer.push({
|
|
1174
|
-
level,
|
|
1175
|
-
message,
|
|
1176
|
-
timestamp: Date.now()
|
|
1149
|
+
//#region src/feedback/ui/floating/use-floating-widget-keyboard.ts
|
|
1150
|
+
function useFloatingWidgetKeyboard(options) {
|
|
1151
|
+
const { anchorRef, hotkey, launcher, showPanel, state } = options;
|
|
1152
|
+
useEffect(() => {
|
|
1153
|
+
if (state.step === "compose") state.annotationTrigger?.button.focus({ preventScroll: true });
|
|
1154
|
+
}, [state.step, state.annotationTrigger]);
|
|
1155
|
+
const minimize = launcher.minimize;
|
|
1156
|
+
useEffect(() => {
|
|
1157
|
+
if (!showPanel || state.step !== "compose") return;
|
|
1158
|
+
return listenToKeydown(anchorRef.current, (event) => {
|
|
1159
|
+
if (event.key === "Escape" && !event.defaultPrevented) minimize();
|
|
1177
1160
|
});
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
return {
|
|
1202
|
-
events: () => buffer.slice(),
|
|
1203
|
-
stop: () => {
|
|
1204
|
-
if (!active) return;
|
|
1205
|
-
active = false;
|
|
1206
|
-
buffer.length = 0;
|
|
1207
|
-
for (const method of RECORDED_METHODS) {
|
|
1208
|
-
const original = originals.get(method);
|
|
1209
|
-
if (original && console[method] === wrappers.get(method)) console[method] = original;
|
|
1161
|
+
}, [
|
|
1162
|
+
anchorRef,
|
|
1163
|
+
minimize,
|
|
1164
|
+
showPanel,
|
|
1165
|
+
state.step
|
|
1166
|
+
]);
|
|
1167
|
+
useWidgetHotkey({
|
|
1168
|
+
anchorRef,
|
|
1169
|
+
hotkey,
|
|
1170
|
+
launcher,
|
|
1171
|
+
state
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
function useWidgetHotkey({ anchorRef, hotkey, launcher, state }) {
|
|
1175
|
+
const { close, isOpen } = state;
|
|
1176
|
+
const open = launcher.triggerProps.onClick;
|
|
1177
|
+
useEffect(() => {
|
|
1178
|
+
if (!hotkey) return;
|
|
1179
|
+
return listenToKeydown(anchorRef.current, (event) => {
|
|
1180
|
+
if (matchesHotkey(event, hotkey)) {
|
|
1181
|
+
event.preventDefault();
|
|
1182
|
+
if (isOpen) close();
|
|
1183
|
+
else open();
|
|
1210
1184
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1185
|
+
}, true);
|
|
1186
|
+
}, [
|
|
1187
|
+
anchorRef,
|
|
1188
|
+
close,
|
|
1189
|
+
hotkey,
|
|
1190
|
+
isOpen,
|
|
1191
|
+
open
|
|
1192
|
+
]);
|
|
1215
1193
|
}
|
|
1216
1194
|
//#endregion
|
|
1217
|
-
//#region src/feedback/ui/
|
|
1218
|
-
function
|
|
1219
|
-
const
|
|
1220
|
-
const
|
|
1221
|
-
const
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
options.onError("Reflet is missing a publicKey. Pass one to RefletFeedback or RefletProvider.");
|
|
1243
|
-
return;
|
|
1244
|
-
}
|
|
1245
|
-
const request = {};
|
|
1246
|
-
requestRef.current = request;
|
|
1247
|
-
setActiveRequest(request);
|
|
1248
|
-
options.onError(null);
|
|
1249
|
-
try {
|
|
1250
|
-
const transport = createFeedbackTransport(options.client);
|
|
1251
|
-
const result = pendingReport ? await attachPreparedScreenshots(transport, pendingReport) : await submitWidgetFeedback(transport, draft);
|
|
1252
|
-
if (requestRef.current === request) finishSubmission(result);
|
|
1253
|
-
} catch (error) {
|
|
1254
|
-
if (requestRef.current === request) options.onError(error instanceof Error ? error.message : "Something went wrong. Please try again.");
|
|
1255
|
-
} finally {
|
|
1256
|
-
if (requestRef.current === request) {
|
|
1257
|
-
requestRef.current = null;
|
|
1258
|
-
setActiveRequest(null);
|
|
1195
|
+
//#region src/feedback/ui/floating/floating-widget.tsx
|
|
1196
|
+
function FloatingWidget({ config, launcher, state }) {
|
|
1197
|
+
const { hotkey, position } = config;
|
|
1198
|
+
const showPanel = state.isOpen && !launcher.minimized;
|
|
1199
|
+
const floating = useFloatingPosition(position, showPanel);
|
|
1200
|
+
useFloatingWidgetKeyboard({
|
|
1201
|
+
anchorRef: floating.rootRef,
|
|
1202
|
+
hotkey,
|
|
1203
|
+
launcher,
|
|
1204
|
+
showPanel,
|
|
1205
|
+
state
|
|
1206
|
+
});
|
|
1207
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1208
|
+
className: "root",
|
|
1209
|
+
"data-editing": state.isOpen && (state.step === "picking" || state.step === "annotate"),
|
|
1210
|
+
"data-moved": Boolean(floating.position),
|
|
1211
|
+
"data-position": position,
|
|
1212
|
+
ref: floating.rootRef,
|
|
1213
|
+
children: [/* @__PURE__ */ jsx("style", { children: floating.styles }), /* @__PURE__ */ jsx(FloatingComposer, {
|
|
1214
|
+
config,
|
|
1215
|
+
launcher,
|
|
1216
|
+
panel: {
|
|
1217
|
+
floating,
|
|
1218
|
+
showPanel,
|
|
1219
|
+
state
|
|
1259
1220
|
}
|
|
1260
|
-
}
|
|
1261
|
-
};
|
|
1221
|
+
})]
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
function FloatingComposer({ config, launcher, panel }) {
|
|
1225
|
+
const { labels } = config;
|
|
1226
|
+
const { floating, showPanel, state } = panel;
|
|
1227
|
+
if (!showPanel) return launcher.custom ? null : /* @__PURE__ */ jsx(Launcher, {
|
|
1228
|
+
buttonRef: launcher.triggerProps.ref,
|
|
1229
|
+
isOpen: false,
|
|
1230
|
+
label: state.isOpen ? labels.resume : labels.trigger,
|
|
1231
|
+
onClick: launcher.triggerProps.onClick
|
|
1232
|
+
});
|
|
1233
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [state.step === "compose" && state.screenshots.map(({ id, selectedNode }) => selectedNode ? /* @__PURE__ */ jsx(SelectionOutline, { node: selectedNode }, id) : null), /* @__PURE__ */ jsx(FeedbackPanel, {
|
|
1234
|
+
floatingControls: /* @__PURE__ */ jsx(FloatingControls, {
|
|
1235
|
+
floating,
|
|
1236
|
+
labels,
|
|
1237
|
+
minimize: launcher.minimize
|
|
1238
|
+
}),
|
|
1239
|
+
labels,
|
|
1240
|
+
state
|
|
1241
|
+
})] });
|
|
1242
|
+
}
|
|
1243
|
+
function FloatingControls({ labels, floating, minimize }) {
|
|
1244
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1245
|
+
className: "floating-controls",
|
|
1246
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
1247
|
+
"aria-label": labels.moveFeedback,
|
|
1248
|
+
className: "icon-btn drag-handle",
|
|
1249
|
+
title: labels.moveFeedback,
|
|
1250
|
+
type: "button",
|
|
1251
|
+
...floating.handleProps,
|
|
1252
|
+
children: /* @__PURE__ */ jsx(GripIcon, {})
|
|
1253
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1254
|
+
"aria-label": labels.minimize,
|
|
1255
|
+
className: "icon-btn",
|
|
1256
|
+
onClick: minimize,
|
|
1257
|
+
title: labels.minimize,
|
|
1258
|
+
type: "button",
|
|
1259
|
+
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
1260
|
+
})]
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
//#endregion
|
|
1264
|
+
//#region src/feedback/ui/floating/use-widget-launcher.ts
|
|
1265
|
+
function useWidgetLauncher(state) {
|
|
1266
|
+
const [minimized, setMinimized] = useState(false);
|
|
1267
|
+
const triggerRef = useRef(null);
|
|
1262
1268
|
return {
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
}
|
|
1275
|
-
function useScreenshotDrafts(captureFailed, onError) {
|
|
1276
|
-
const [screenshots, setScreenshots] = useState([]);
|
|
1277
|
-
const [activeId, setActiveId] = useState(null);
|
|
1278
|
-
const [pendingCapture, setPendingCapture] = useState(null);
|
|
1279
|
-
const ownedDrafts = useRef(/* @__PURE__ */ new Map());
|
|
1280
|
-
const captureRequest = useRef(null);
|
|
1281
|
-
useEffect(() => {
|
|
1282
|
-
const drafts = ownedDrafts.current;
|
|
1283
|
-
return () => {
|
|
1284
|
-
captureRequest.current = null;
|
|
1285
|
-
for (const draft of drafts.values()) releaseDraft(draft);
|
|
1286
|
-
drafts.clear();
|
|
1287
|
-
};
|
|
1288
|
-
}, []);
|
|
1289
|
-
const storeScreenshot = useCallback((screenshot) => {
|
|
1290
|
-
releaseDraft(ownedDrafts.current.get(screenshot.id));
|
|
1291
|
-
ownedDrafts.current.set(screenshot.id, screenshot);
|
|
1292
|
-
setScreenshots((previous) => {
|
|
1293
|
-
return previous.some(({ id }) => id === screenshot.id) ? previous.map((item) => item.id === screenshot.id ? screenshot : item) : [...previous, screenshot];
|
|
1294
|
-
});
|
|
1295
|
-
setActiveId(screenshot.id);
|
|
1296
|
-
}, []);
|
|
1297
|
-
const cancelPendingCapture = useCallback(() => {
|
|
1298
|
-
captureRequest.current = null;
|
|
1299
|
-
setPendingCapture(null);
|
|
1300
|
-
}, []);
|
|
1301
|
-
const takeScreenshot = useCallback(async (request) => {
|
|
1302
|
-
captureRequest.current = request;
|
|
1303
|
-
setPendingCapture(request);
|
|
1304
|
-
onError(null);
|
|
1305
|
-
const context = collectPageContext({ sdkVersion: SDK_VERSION });
|
|
1306
|
-
const image = await captureViewport().catch(() => null);
|
|
1307
|
-
if (captureRequest.current !== request) {
|
|
1308
|
-
releaseCapture(image);
|
|
1309
|
-
return;
|
|
1310
|
-
}
|
|
1311
|
-
cancelPendingCapture();
|
|
1312
|
-
if (!image) {
|
|
1313
|
-
onError(captureFailed);
|
|
1314
|
-
return;
|
|
1315
|
-
}
|
|
1316
|
-
storeScreenshot({
|
|
1317
|
-
...request,
|
|
1318
|
-
annotations: [],
|
|
1319
|
-
context,
|
|
1320
|
-
image
|
|
1321
|
-
});
|
|
1322
|
-
}, [
|
|
1323
|
-
cancelPendingCapture,
|
|
1324
|
-
captureFailed,
|
|
1325
|
-
onError,
|
|
1326
|
-
storeScreenshot
|
|
1327
|
-
]);
|
|
1328
|
-
const takeCapture = useCallback(() => {
|
|
1329
|
-
if (!captureRequest.current) takeScreenshot({
|
|
1330
|
-
id: crypto.randomUUID(),
|
|
1331
|
-
source: "manual"
|
|
1332
|
-
});
|
|
1333
|
-
}, [takeScreenshot]);
|
|
1334
|
-
const refreshAutomatic = useCallback(() => {
|
|
1335
|
-
takeScreenshot({
|
|
1336
|
-
id: ownedDrafts.current.keys().next().value ?? captureRequest.current?.id ?? crypto.randomUUID(),
|
|
1337
|
-
source: "automatic"
|
|
1338
|
-
});
|
|
1339
|
-
}, [takeScreenshot]);
|
|
1340
|
-
const retakeCapture = useCallback((id) => {
|
|
1341
|
-
if (!captureRequest.current && ownedDrafts.current.has(id)) takeScreenshot({
|
|
1342
|
-
id,
|
|
1343
|
-
source: "manual"
|
|
1344
|
-
});
|
|
1345
|
-
}, [takeScreenshot]);
|
|
1346
|
-
const removeCapture = useCallback((id) => {
|
|
1347
|
-
if (captureRequest.current?.id === id) cancelPendingCapture();
|
|
1348
|
-
releaseDraft(ownedDrafts.current.get(id));
|
|
1349
|
-
ownedDrafts.current.delete(id);
|
|
1350
|
-
setScreenshots((previous) => previous.filter((item) => item.id !== id));
|
|
1351
|
-
setActiveId((current) => current === id ? null : current);
|
|
1352
|
-
}, [cancelPendingCapture]);
|
|
1353
|
-
const resetScreenshots = useCallback(() => {
|
|
1354
|
-
cancelPendingCapture();
|
|
1355
|
-
for (const draft of ownedDrafts.current.values()) releaseDraft(draft);
|
|
1356
|
-
ownedDrafts.current.clear();
|
|
1357
|
-
setScreenshots([]);
|
|
1358
|
-
setActiveId(null);
|
|
1359
|
-
}, [cancelPendingCapture]);
|
|
1360
|
-
const setAnnotations = useCallback((annotations) => {
|
|
1361
|
-
setScreenshots((previous) => previous.map((item) => item.id === activeId ? {
|
|
1362
|
-
...item,
|
|
1363
|
-
annotations
|
|
1364
|
-
} : item));
|
|
1365
|
-
}, [activeId]);
|
|
1366
|
-
const setSelectionComment = useCallback((id, comment) => {
|
|
1367
|
-
setScreenshots((previous) => previous.map((item) => item.id === id && item.selection ? {
|
|
1368
|
-
...item,
|
|
1369
|
-
selection: {
|
|
1370
|
-
...item.selection,
|
|
1371
|
-
comment
|
|
1372
|
-
}
|
|
1373
|
-
} : item));
|
|
1374
|
-
}, []);
|
|
1375
|
-
const activeScreenshot = screenshots.find(({ id }) => id === activeId) ?? screenshots.at(-1) ?? null;
|
|
1376
|
-
const firstCapture = pendingCapture ?? screenshots[0];
|
|
1377
|
-
return {
|
|
1378
|
-
activeScreenshot,
|
|
1379
|
-
cancelPendingCapture,
|
|
1380
|
-
canRefreshAutomatically: screenshots.length <= 1 && firstCapture?.source === "automatic" && (activeScreenshot?.annotations.length ?? 0) === 0 && !screenshots.some((draft) => draft.selection),
|
|
1381
|
-
pendingCapture,
|
|
1382
|
-
refreshAutomatic,
|
|
1383
|
-
removeCapture,
|
|
1384
|
-
resetScreenshots,
|
|
1385
|
-
retakeCapture,
|
|
1386
|
-
screenshots,
|
|
1387
|
-
selectScreenshot: setActiveId,
|
|
1388
|
-
setAnnotations,
|
|
1389
|
-
setSelectionComment,
|
|
1390
|
-
storeScreenshot,
|
|
1391
|
-
takeCapture
|
|
1392
|
-
};
|
|
1393
|
-
}
|
|
1394
|
-
//#endregion
|
|
1395
|
-
//#region src/feedback/ui/state/use-widget-config.ts
|
|
1396
|
-
function dismissalDuration(days) {
|
|
1397
|
-
return days !== void 0 && Number.isFinite(days) && days > 0 ? days : null;
|
|
1398
|
-
}
|
|
1399
|
-
function useWidgetConfig(props) {
|
|
1400
|
-
const context = useContext(RefletContext);
|
|
1401
|
-
const publicKey = props.publicKey ?? context?.publicKey;
|
|
1402
|
-
const baseUrl = props.baseUrl ?? context?.baseUrl;
|
|
1403
|
-
const user = props.user ?? context?.user;
|
|
1404
|
-
const userToken = props.userToken ?? context?.userToken;
|
|
1405
|
-
return {
|
|
1406
|
-
client: useMemo(() => publicKey ? new Reflet({
|
|
1407
|
-
baseUrl,
|
|
1408
|
-
publicKey,
|
|
1409
|
-
user,
|
|
1410
|
-
userToken
|
|
1411
|
-
}) : null, [
|
|
1412
|
-
baseUrl,
|
|
1413
|
-
publicKey,
|
|
1414
|
-
user,
|
|
1415
|
-
userToken
|
|
1416
|
-
]),
|
|
1417
|
-
dismissalKey: `reflet-feedback-dismissed:${publicKey ?? "default"}:${user?.id ?? "anonymous"}`,
|
|
1418
|
-
dismissForDays: dismissalDuration(props.dismissForDays),
|
|
1419
|
-
isAnonymous: !(user || userToken),
|
|
1420
|
-
publicKey: publicKey ?? null
|
|
1421
|
-
};
|
|
1422
|
-
}
|
|
1423
|
-
//#endregion
|
|
1424
|
-
//#region src/feedback/ui/use-capture-sync.ts
|
|
1425
|
-
const RESIZE_DEBOUNCE = 400;
|
|
1426
|
-
/**
|
|
1427
|
-
* Calls `refresh` whenever the page the reporter is looking at changes shape:
|
|
1428
|
-
* SPA routes (which never fire popstate on push/replace — history methods must
|
|
1429
|
-
* be patched) and window resizes, debounced. Keeps the frozen screenshot and
|
|
1430
|
-
* page context describing what the reporter currently sees.
|
|
1431
|
-
*/
|
|
1432
|
-
function useCaptureSync(enabled, refresh) {
|
|
1433
|
-
const refreshRef = useRef(refresh);
|
|
1434
|
-
useEffect(() => {
|
|
1435
|
-
refreshRef.current = refresh;
|
|
1436
|
-
});
|
|
1437
|
-
useEffect(() => {
|
|
1438
|
-
if (!enabled) return;
|
|
1439
|
-
const runRefresh = () => refreshRef.current();
|
|
1440
|
-
const history = window.history;
|
|
1441
|
-
const originalPush = history.pushState;
|
|
1442
|
-
const originalReplace = history.replaceState;
|
|
1443
|
-
const hadOwnPush = Object.hasOwn(history, "pushState");
|
|
1444
|
-
const hadOwnReplace = Object.hasOwn(history, "replaceState");
|
|
1445
|
-
history.pushState = function pushState(data, unused, url) {
|
|
1446
|
-
const result = originalPush.call(history, data, unused, url);
|
|
1447
|
-
runRefresh();
|
|
1448
|
-
return result;
|
|
1449
|
-
};
|
|
1450
|
-
history.replaceState = function replaceState(data, unused, url) {
|
|
1451
|
-
const result = originalReplace.call(history, data, unused, url);
|
|
1452
|
-
runRefresh();
|
|
1453
|
-
return result;
|
|
1454
|
-
};
|
|
1455
|
-
window.addEventListener("popstate", runRefresh);
|
|
1456
|
-
let resizeTimer;
|
|
1457
|
-
const onResize = () => {
|
|
1458
|
-
window.clearTimeout(resizeTimer);
|
|
1459
|
-
resizeTimer = window.setTimeout(runRefresh, RESIZE_DEBOUNCE);
|
|
1460
|
-
};
|
|
1461
|
-
window.addEventListener("resize", onResize);
|
|
1462
|
-
return () => {
|
|
1463
|
-
if (hadOwnPush) history.pushState = originalPush;
|
|
1464
|
-
else Reflect.deleteProperty(history, "pushState");
|
|
1465
|
-
if (hadOwnReplace) history.replaceState = originalReplace;
|
|
1466
|
-
else Reflect.deleteProperty(history, "replaceState");
|
|
1467
|
-
window.removeEventListener("popstate", runRefresh);
|
|
1468
|
-
window.clearTimeout(resizeTimer);
|
|
1469
|
-
window.removeEventListener("resize", onResize);
|
|
1470
|
-
};
|
|
1471
|
-
}, [enabled]);
|
|
1472
|
-
}
|
|
1473
|
-
//#endregion
|
|
1474
|
-
//#region src/feedback/ui/use-widget-state.ts
|
|
1475
|
-
const SUCCESS_CLOSE_DELAY = 2400;
|
|
1476
|
-
const IS_APPLE = /Mac|iPhone|iPad/;
|
|
1477
|
-
const MILLISECONDS_PER_DAY = 864e5;
|
|
1478
|
-
function readDismissedUntil(key) {
|
|
1479
|
-
if (typeof window === "undefined") return 0;
|
|
1480
|
-
try {
|
|
1481
|
-
const value = Number.parseInt(window.localStorage.getItem(key) ?? "", 10);
|
|
1482
|
-
if (Number.isFinite(value) && value > Date.now()) return value;
|
|
1483
|
-
window.localStorage.removeItem(key);
|
|
1484
|
-
} catch {
|
|
1485
|
-
return 0;
|
|
1486
|
-
}
|
|
1487
|
-
return 0;
|
|
1488
|
-
}
|
|
1489
|
-
function writeDismissedUntil(key, until) {
|
|
1490
|
-
try {
|
|
1491
|
-
window.localStorage.setItem(key, String(until));
|
|
1492
|
-
} catch {}
|
|
1493
|
-
}
|
|
1494
|
-
function matchesHotkey(event, hotkey, platform = typeof navigator === "undefined" ? "" : navigator.platform) {
|
|
1495
|
-
const parts = hotkey.toLowerCase().split("+").map((part) => part.trim());
|
|
1496
|
-
const key = parts.at(-1);
|
|
1497
|
-
if (!key || event.key.toLowerCase() !== key) return false;
|
|
1498
|
-
const isApple = IS_APPLE.test(platform);
|
|
1499
|
-
const wantsMod = parts.includes("mod");
|
|
1500
|
-
const expected = {
|
|
1501
|
-
alt: parts.includes("alt") || parts.includes("option"),
|
|
1502
|
-
ctrl: parts.includes("ctrl") || wantsMod && !isApple,
|
|
1503
|
-
meta: parts.includes("meta") || parts.includes("cmd") || wantsMod && isApple,
|
|
1504
|
-
shift: parts.includes("shift")
|
|
1505
|
-
};
|
|
1506
|
-
return event.altKey === expected.alt && event.ctrlKey === expected.ctrl && event.metaKey === expected.meta && event.shiftKey === expected.shift;
|
|
1507
|
-
}
|
|
1508
|
-
function useWidgetState(props) {
|
|
1509
|
-
const { client, dismissalKey, dismissForDays, isAnonymous, publicKey } = useWidgetConfig(props);
|
|
1510
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
1511
|
-
const [annotationTrigger, setAnnotationTrigger] = useState(null);
|
|
1512
|
-
const [step, setStep] = useState("compose");
|
|
1513
|
-
const [message, setMessage] = useState("");
|
|
1514
|
-
const [email, setEmail] = useState("");
|
|
1515
|
-
const [honeypot, setHoneypot] = useState("");
|
|
1516
|
-
const [isElementCapturing, setIsElementCapturing] = useState(false);
|
|
1517
|
-
const successTimer = useRef(null);
|
|
1518
|
-
const [error, setError] = useState(null);
|
|
1519
|
-
const [dismissedUntil, setDismissedUntil] = useState(() => readDismissedUntil(dismissalKey));
|
|
1520
|
-
const pickRef = useRef(0);
|
|
1521
|
-
useEffect(() => () => {
|
|
1522
|
-
pickRef.current++;
|
|
1523
|
-
if (successTimer.current) clearTimeout(successTimer.current);
|
|
1524
|
-
}, []);
|
|
1525
|
-
const consoleRef = useRef(null);
|
|
1526
|
-
useEffect(() => {
|
|
1527
|
-
setDismissedUntil(readDismissedUntil(dismissalKey));
|
|
1528
|
-
}, [dismissalKey]);
|
|
1529
|
-
useEffect(() => {
|
|
1530
|
-
if (props.captureConsole === false) return;
|
|
1531
|
-
const recorder = startConsoleRecorder();
|
|
1532
|
-
consoleRef.current = recorder;
|
|
1533
|
-
return () => {
|
|
1534
|
-
consoleRef.current = null;
|
|
1535
|
-
recorder.stop();
|
|
1536
|
-
};
|
|
1537
|
-
}, [props.captureConsole]);
|
|
1538
|
-
const captureFailed = props.labels?.captureFailed ?? DEFAULT_WIDGET_LABELS.captureFailed;
|
|
1539
|
-
const gallery = useScreenshotDrafts(captureFailed, setError);
|
|
1540
|
-
const { activeScreenshot, cancelPendingCapture, refreshAutomatic, resetScreenshots, storeScreenshot } = gallery;
|
|
1541
|
-
const capture = activeScreenshot?.image ?? null;
|
|
1542
|
-
const annotations = activeScreenshot?.annotations ?? [];
|
|
1543
|
-
const isCapturing = gallery.pendingCapture !== null || isElementCapturing;
|
|
1544
|
-
const submission = useFeedbackSubmission({
|
|
1545
|
-
client,
|
|
1546
|
-
onError: setError,
|
|
1547
|
-
onSubmitted: (feedbackId) => {
|
|
1548
|
-
setStep("success");
|
|
1549
|
-
props.onSubmit?.({ feedbackId });
|
|
1550
|
-
successTimer.current = setTimeout(close, SUCCESS_CLOSE_DELAY);
|
|
1551
|
-
},
|
|
1552
|
-
partialUploadMessage: props.labels?.attachmentUploadFailed ?? DEFAULT_WIDGET_LABELS.attachmentUploadFailed
|
|
1553
|
-
});
|
|
1554
|
-
const { resetSubmission } = submission;
|
|
1555
|
-
useCaptureSync(isOpen && !submission.isEditingDisabled && step === "compose" && gallery.canRefreshAutomatically && props.captureOnOpen !== false, refreshAutomatic);
|
|
1556
|
-
const reset = useCallback(() => {
|
|
1557
|
-
if (successTimer.current) clearTimeout(successTimer.current);
|
|
1558
|
-
setAnnotationTrigger(null);
|
|
1559
|
-
resetSubmission();
|
|
1560
|
-
pickRef.current++;
|
|
1561
|
-
setIsElementCapturing(false);
|
|
1562
|
-
resetScreenshots();
|
|
1563
|
-
setMessage("");
|
|
1564
|
-
setEmail("");
|
|
1565
|
-
setHoneypot("");
|
|
1566
|
-
setError(null);
|
|
1567
|
-
setStep("compose");
|
|
1568
|
-
}, [resetSubmission, resetScreenshots]);
|
|
1569
|
-
const close = useCallback(() => {
|
|
1570
|
-
setIsOpen(false);
|
|
1571
|
-
reset();
|
|
1572
|
-
props.onClose?.();
|
|
1573
|
-
}, [reset, props.onClose]);
|
|
1574
|
-
const dismiss = useCallback(() => {
|
|
1575
|
-
if (dismissForDays === null) return;
|
|
1576
|
-
const until = Date.now() + dismissForDays * MILLISECONDS_PER_DAY;
|
|
1577
|
-
writeDismissedUntil(dismissalKey, until);
|
|
1578
|
-
setIsOpen(false);
|
|
1579
|
-
reset();
|
|
1580
|
-
setDismissedUntil(until);
|
|
1581
|
-
props.onDismiss?.({ until });
|
|
1582
|
-
}, [
|
|
1583
|
-
dismissForDays,
|
|
1584
|
-
dismissalKey,
|
|
1585
|
-
props.onDismiss,
|
|
1586
|
-
reset
|
|
1587
|
-
]);
|
|
1588
|
-
const open = useCallback(() => {
|
|
1589
|
-
setIsOpen(true);
|
|
1590
|
-
setStep("compose");
|
|
1591
|
-
props.onOpen?.();
|
|
1592
|
-
if (props.captureOnOpen !== false) refreshAutomatic();
|
|
1593
|
-
}, [
|
|
1594
|
-
props.captureOnOpen,
|
|
1595
|
-
props.onOpen,
|
|
1596
|
-
refreshAutomatic
|
|
1597
|
-
]);
|
|
1598
|
-
const selectElement = useCallback(async (element, note = "") => {
|
|
1599
|
-
cancelPendingCapture();
|
|
1600
|
-
setIsElementCapturing(true);
|
|
1601
|
-
const picked = note ? {
|
|
1602
|
-
...buildElementSelection(element),
|
|
1603
|
-
comment: note.slice(0, MAX_SELECTION_COMMENT_LENGTH)
|
|
1604
|
-
} : buildElementSelection(element);
|
|
1605
|
-
setStep("compose");
|
|
1606
|
-
const pick = ++pickRef.current;
|
|
1607
|
-
const context = collectPageContext({ sdkVersion: SDK_VERSION });
|
|
1608
|
-
const fresh = await captureViewport().catch(() => null);
|
|
1609
|
-
if (pickRef.current !== pick) {
|
|
1610
|
-
releaseCapture(fresh);
|
|
1611
|
-
return;
|
|
1612
|
-
}
|
|
1613
|
-
setIsElementCapturing(false);
|
|
1614
|
-
if (!fresh) {
|
|
1615
|
-
setError(captureFailed);
|
|
1616
|
-
return;
|
|
1617
|
-
}
|
|
1618
|
-
storeScreenshot({
|
|
1619
|
-
annotations: [highlightFor(picked, fresh)],
|
|
1620
|
-
context,
|
|
1621
|
-
id: crypto.randomUUID(),
|
|
1622
|
-
image: fresh,
|
|
1623
|
-
selectedNode: element,
|
|
1624
|
-
selection: picked,
|
|
1625
|
-
source: "manual"
|
|
1626
|
-
});
|
|
1627
|
-
}, [
|
|
1628
|
-
cancelPendingCapture,
|
|
1629
|
-
captureFailed,
|
|
1630
|
-
storeScreenshot
|
|
1631
|
-
]);
|
|
1632
|
-
const selections = gallery.screenshots.flatMap(({ selection }) => selection ? [selection] : []);
|
|
1633
|
-
const firstElementNote = selections.find(({ comment }) => comment)?.comment;
|
|
1634
|
-
const reportedMessage = message.trim() || firstElementNote || "";
|
|
1635
|
-
const submit = () => {
|
|
1636
|
-
if (honeypot) {
|
|
1637
|
-
setStep("success");
|
|
1638
|
-
return;
|
|
1639
|
-
}
|
|
1640
|
-
setStep("compose");
|
|
1641
|
-
return submission.submit({
|
|
1642
|
-
context: {
|
|
1643
|
-
...activeScreenshot?.context ?? collectPageContext({ sdkVersion: "0.6.3" }),
|
|
1644
|
-
consoleEvents: consoleRef.current?.events(),
|
|
1645
|
-
metadata: props.metadata,
|
|
1646
|
-
selections
|
|
1647
|
-
},
|
|
1648
|
-
email,
|
|
1649
|
-
isAnonymous,
|
|
1650
|
-
message: reportedMessage,
|
|
1651
|
-
screenshots: gallery.screenshots
|
|
1652
|
-
});
|
|
1653
|
-
};
|
|
1654
|
-
const requestSubmit = () => {
|
|
1655
|
-
if (isAnonymous && email.length === 0 && !submission.hasPendingAttachments && !honeypot) {
|
|
1656
|
-
setStep("email");
|
|
1657
|
-
return;
|
|
1658
|
-
}
|
|
1659
|
-
return submit();
|
|
1660
|
-
};
|
|
1661
|
-
return {
|
|
1662
|
-
activeScreenshot,
|
|
1663
|
-
annotateScreenshot: (id, trigger) => {
|
|
1664
|
-
setAnnotationTrigger(trigger);
|
|
1665
|
-
gallery.selectScreenshot(id);
|
|
1666
|
-
setStep("annotate");
|
|
1667
|
-
},
|
|
1668
|
-
annotations,
|
|
1669
|
-
annotationTrigger,
|
|
1670
|
-
canDismiss: dismissForDays !== null,
|
|
1671
|
-
canSubmit: reportedMessage.length > 0,
|
|
1672
|
-
capture,
|
|
1673
|
-
close,
|
|
1674
|
-
dismiss,
|
|
1675
|
-
dismissForDays,
|
|
1676
|
-
email,
|
|
1677
|
-
error,
|
|
1678
|
-
hasPendingAttachments: submission.hasPendingAttachments,
|
|
1679
|
-
honeypot,
|
|
1680
|
-
isAnonymous,
|
|
1681
|
-
isCapturing,
|
|
1682
|
-
isDismissed: dismissedUntil > Date.now(),
|
|
1683
|
-
isEditingDisabled: submission.isEditingDisabled,
|
|
1684
|
-
isOpen,
|
|
1685
|
-
isSubmitting: submission.isSubmitting,
|
|
1686
|
-
message,
|
|
1687
|
-
open,
|
|
1688
|
-
pendingCapture: gallery.pendingCapture,
|
|
1689
|
-
publicKey,
|
|
1690
|
-
removeCapture: gallery.removeCapture,
|
|
1691
|
-
requestSubmit,
|
|
1692
|
-
retakeCapture: gallery.retakeCapture,
|
|
1693
|
-
screenshots: gallery.screenshots,
|
|
1694
|
-
selectElement,
|
|
1695
|
-
selections,
|
|
1696
|
-
setAnnotations: gallery.setAnnotations,
|
|
1697
|
-
setEmail,
|
|
1698
|
-
setHoneypot,
|
|
1699
|
-
setMessage,
|
|
1700
|
-
setSelectionComment: gallery.setSelectionComment,
|
|
1701
|
-
setStep,
|
|
1702
|
-
step,
|
|
1703
|
-
submit,
|
|
1704
|
-
takeCapture: gallery.takeCapture
|
|
1705
|
-
};
|
|
1706
|
-
}
|
|
1707
|
-
//#endregion
|
|
1708
|
-
//#region src/feedback/ui/floating/use-floating-widget-keyboard.ts
|
|
1709
|
-
function useFloatingWidgetKeyboard(options) {
|
|
1710
|
-
const { anchorRef, hotkey, launcher, showPanel, state } = options;
|
|
1711
|
-
useEffect(() => {
|
|
1712
|
-
if (state.step === "compose") state.annotationTrigger?.button.focus({ preventScroll: true });
|
|
1713
|
-
}, [state.step, state.annotationTrigger]);
|
|
1714
|
-
const minimize = launcher.minimize;
|
|
1715
|
-
useEffect(() => {
|
|
1716
|
-
if (!showPanel || state.step !== "compose") return;
|
|
1717
|
-
return listenToKeydown(anchorRef.current, (event) => {
|
|
1718
|
-
if (event.key === "Escape" && !event.defaultPrevented) minimize();
|
|
1719
|
-
});
|
|
1720
|
-
}, [
|
|
1721
|
-
anchorRef,
|
|
1722
|
-
minimize,
|
|
1723
|
-
showPanel,
|
|
1724
|
-
state.step
|
|
1725
|
-
]);
|
|
1726
|
-
useWidgetHotkey({
|
|
1727
|
-
anchorRef,
|
|
1728
|
-
hotkey,
|
|
1729
|
-
launcher,
|
|
1730
|
-
state
|
|
1731
|
-
});
|
|
1732
|
-
}
|
|
1733
|
-
function useWidgetHotkey({ anchorRef, hotkey, launcher, state }) {
|
|
1734
|
-
const { close, isOpen } = state;
|
|
1735
|
-
const open = launcher.triggerProps.onClick;
|
|
1736
|
-
useEffect(() => {
|
|
1737
|
-
if (!hotkey) return;
|
|
1738
|
-
return listenToKeydown(anchorRef.current, (event) => {
|
|
1739
|
-
if (matchesHotkey(event, hotkey)) {
|
|
1740
|
-
event.preventDefault();
|
|
1741
|
-
if (isOpen) close();
|
|
1742
|
-
else open();
|
|
1743
|
-
}
|
|
1744
|
-
}, true);
|
|
1745
|
-
}, [
|
|
1746
|
-
anchorRef,
|
|
1747
|
-
close,
|
|
1748
|
-
hotkey,
|
|
1749
|
-
isOpen,
|
|
1750
|
-
open
|
|
1751
|
-
]);
|
|
1752
|
-
}
|
|
1753
|
-
//#endregion
|
|
1754
|
-
//#region src/feedback/ui/floating/floating-widget.tsx
|
|
1755
|
-
function FloatingWidget({ config, launcher, state }) {
|
|
1756
|
-
const { hotkey, position } = config;
|
|
1757
|
-
const showPanel = state.isOpen && !launcher.minimized;
|
|
1758
|
-
const floating = useFloatingPosition(position, showPanel);
|
|
1759
|
-
useFloatingWidgetKeyboard({
|
|
1760
|
-
anchorRef: floating.rootRef,
|
|
1761
|
-
hotkey,
|
|
1762
|
-
launcher,
|
|
1763
|
-
showPanel,
|
|
1764
|
-
state
|
|
1765
|
-
});
|
|
1766
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1767
|
-
className: "root",
|
|
1768
|
-
"data-editing": state.isOpen && (state.step === "picking" || state.step === "annotate"),
|
|
1769
|
-
"data-moved": Boolean(floating.position),
|
|
1770
|
-
"data-position": position,
|
|
1771
|
-
ref: floating.rootRef,
|
|
1772
|
-
children: [/* @__PURE__ */ jsx("style", { children: floating.styles }), /* @__PURE__ */ jsx(FloatingComposer, {
|
|
1773
|
-
config,
|
|
1774
|
-
launcher,
|
|
1775
|
-
panel: {
|
|
1776
|
-
floating,
|
|
1777
|
-
showPanel,
|
|
1778
|
-
state
|
|
1779
|
-
}
|
|
1780
|
-
})]
|
|
1781
|
-
});
|
|
1782
|
-
}
|
|
1783
|
-
function FloatingComposer({ config, launcher, panel }) {
|
|
1784
|
-
const { labels } = config;
|
|
1785
|
-
const { floating, showPanel, state } = panel;
|
|
1786
|
-
if (!showPanel) return launcher.custom ? null : /* @__PURE__ */ jsx(Launcher, {
|
|
1787
|
-
buttonRef: launcher.triggerProps.ref,
|
|
1788
|
-
isOpen: false,
|
|
1789
|
-
label: state.isOpen ? labels.resume : labels.trigger,
|
|
1790
|
-
onClick: launcher.triggerProps.onClick
|
|
1791
|
-
});
|
|
1792
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [state.step === "compose" && state.screenshots.map(({ id, selectedNode }) => selectedNode ? /* @__PURE__ */ jsx(SelectionOutline, { node: selectedNode }, id) : null), /* @__PURE__ */ jsx(FeedbackPanel, {
|
|
1793
|
-
floatingControls: /* @__PURE__ */ jsx(FloatingControls, {
|
|
1794
|
-
floating,
|
|
1795
|
-
labels,
|
|
1796
|
-
minimize: launcher.minimize
|
|
1797
|
-
}),
|
|
1798
|
-
labels,
|
|
1799
|
-
state
|
|
1800
|
-
})] });
|
|
1801
|
-
}
|
|
1802
|
-
function FloatingControls({ labels, floating, minimize }) {
|
|
1803
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1804
|
-
className: "floating-controls",
|
|
1805
|
-
children: [/* @__PURE__ */ jsx("button", {
|
|
1806
|
-
"aria-label": labels.moveFeedback,
|
|
1807
|
-
className: "icon-btn drag-handle",
|
|
1808
|
-
title: labels.moveFeedback,
|
|
1809
|
-
type: "button",
|
|
1810
|
-
...floating.handleProps,
|
|
1811
|
-
children: /* @__PURE__ */ jsx(GripIcon, {})
|
|
1812
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
1813
|
-
"aria-label": labels.minimize,
|
|
1814
|
-
className: "icon-btn",
|
|
1815
|
-
onClick: minimize,
|
|
1816
|
-
title: labels.minimize,
|
|
1817
|
-
type: "button",
|
|
1818
|
-
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
1819
|
-
})]
|
|
1820
|
-
});
|
|
1821
|
-
}
|
|
1822
|
-
//#endregion
|
|
1823
|
-
//#region src/feedback/ui/floating/use-widget-launcher.ts
|
|
1824
|
-
function useWidgetLauncher(state) {
|
|
1825
|
-
const [minimized, setMinimized] = useState(false);
|
|
1826
|
-
const triggerRef = useRef(null);
|
|
1827
|
-
return {
|
|
1828
|
-
minimize: () => {
|
|
1829
|
-
setMinimized(true);
|
|
1830
|
-
requestAnimationFrame(() => triggerRef.current?.focus());
|
|
1831
|
-
},
|
|
1832
|
-
minimized,
|
|
1833
|
-
triggerProps: {
|
|
1834
|
-
onClick: () => {
|
|
1835
|
-
setMinimized(false);
|
|
1836
|
-
if (!state.isOpen) state.open();
|
|
1837
|
-
},
|
|
1838
|
-
ref: triggerRef
|
|
1839
|
-
}
|
|
1269
|
+
minimize: () => {
|
|
1270
|
+
setMinimized(true);
|
|
1271
|
+
requestAnimationFrame(() => triggerRef.current?.focus());
|
|
1272
|
+
},
|
|
1273
|
+
minimized,
|
|
1274
|
+
triggerProps: {
|
|
1275
|
+
onClick: () => {
|
|
1276
|
+
setMinimized(false);
|
|
1277
|
+
if (!state.isOpen) state.open();
|
|
1278
|
+
},
|
|
1279
|
+
ref: triggerRef
|
|
1280
|
+
}
|
|
1840
1281
|
};
|
|
1841
1282
|
}
|
|
1842
1283
|
//#endregion
|
|
@@ -1974,6 +1415,8 @@ summary::-webkit-details-marker { display: none; }
|
|
|
1974
1415
|
.screenshot-attachment, .attachment-skeleton { position: relative; flex: none; margin: 0; min-width: 0; width: 128px; height: var(--rf-attachment-height); padding: 0; border-radius: 16px; animation: rf-in 260ms var(--rf-ease); }
|
|
1975
1416
|
.screenshot-attachment { border: 0; }
|
|
1976
1417
|
.screenshot-preview { display: block; position: relative; width: 100%; height: 100%; padding: 0; border-radius: inherit; overflow: hidden; }
|
|
1418
|
+
.screenshot-preview:disabled { opacity: 1; cursor: default; }
|
|
1419
|
+
.screenshot-preview:focus-visible { outline-offset: -2px; }
|
|
1977
1420
|
.screenshot-preview img, .screenshot-preview canvas { width: 100%; height: 100%; display: block; object-fit: cover; object-position: top center; }
|
|
1978
1421
|
.screenshot-preview canvas { position: absolute; inset: 0; pointer-events: none; }
|
|
1979
1422
|
.annotation-count { position: absolute; left: 5px; bottom: 5px; min-width: 20px; padding: 2px 6px; border-radius: var(--rf-pill); font-size: 10px; font-weight: 600; font-variant-numeric: tabular-nums; text-align: center; color: var(--rf-text); }
|
|
@@ -2274,97 +1717,647 @@ svg { flex-shrink: 0; }
|
|
|
2274
1717
|
.icon-btn, .tool { width: 44px; height: 44px; }
|
|
2275
1718
|
.launcher, .ghost-btn { min-height: 44px; }
|
|
2276
1719
|
}
|
|
2277
|
-
.glass {
|
|
2278
|
-
position: relative;
|
|
2279
|
-
isolation: isolate;
|
|
2280
|
-
border: 1px solid var(--rf-glass-stroke);
|
|
2281
|
-
box-shadow: inset 0 1px 0 var(--rf-glass-highlight), var(--rf-shadow);
|
|
1720
|
+
.glass {
|
|
1721
|
+
position: relative;
|
|
1722
|
+
isolation: isolate;
|
|
1723
|
+
border: 1px solid var(--rf-glass-stroke);
|
|
1724
|
+
box-shadow: inset 0 1px 0 var(--rf-glass-highlight), var(--rf-shadow);
|
|
1725
|
+
}
|
|
1726
|
+
.glass::before {
|
|
1727
|
+
content: "";
|
|
1728
|
+
position: absolute;
|
|
1729
|
+
inset: 0;
|
|
1730
|
+
z-index: -1;
|
|
1731
|
+
border-radius: inherit;
|
|
1732
|
+
background: var(--rf-bg);
|
|
1733
|
+
pointer-events: none;
|
|
1734
|
+
}
|
|
1735
|
+
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
|
|
1736
|
+
.glass::before {
|
|
1737
|
+
background: var(--rf-glass);
|
|
1738
|
+
-webkit-backdrop-filter: blur(20px) saturate(1.6);
|
|
1739
|
+
backdrop-filter: blur(20px) saturate(1.6);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
|
|
1743
|
+
.glass::before { background: var(--rf-bg); backdrop-filter: none; -webkit-backdrop-filter: none; }
|
|
1744
|
+
}
|
|
1745
|
+
.capture-halo { position: fixed; inset: 0; z-index: ${REFLET_Z_INDEX.captureHalo}; pointer-events: none; opacity: 0; transition: opacity 500ms var(--rf-ease); }
|
|
1746
|
+
.capture-halo[data-active="true"] { opacity: 1; transition-duration: 140ms; }
|
|
1747
|
+
.capture-halo::before { content: ""; position: absolute; inset: 0; 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); }
|
|
1748
|
+
.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)); }
|
|
1749
|
+
${COMPOSER_STYLES}
|
|
1750
|
+
${ANNOTATOR_STYLES}
|
|
1751
|
+
${PICKER_STYLES}
|
|
1752
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1753
|
+
*, *::before, *::after { animation: none !important; transition: none !important; }
|
|
1754
|
+
button:active:not(:disabled) { scale: 1; }
|
|
1755
|
+
}
|
|
1756
|
+
`;
|
|
1757
|
+
//#endregion
|
|
1758
|
+
//#region src/feedback/ui/shadow-portal.tsx
|
|
1759
|
+
const DARK_QUERY = "(prefers-color-scheme: dark)";
|
|
1760
|
+
function useResolvedTheme(theme) {
|
|
1761
|
+
const [systemDark, setSystemDark] = useState(false);
|
|
1762
|
+
useEffect(() => {
|
|
1763
|
+
if (theme !== "auto" || typeof window.matchMedia !== "function") return;
|
|
1764
|
+
const query = window.matchMedia(DARK_QUERY);
|
|
1765
|
+
setSystemDark(query.matches);
|
|
1766
|
+
const onChange = (event) => setSystemDark(event.matches);
|
|
1767
|
+
query.addEventListener("change", onChange);
|
|
1768
|
+
return () => query.removeEventListener("change", onChange);
|
|
1769
|
+
}, [theme]);
|
|
1770
|
+
if (theme === "auto") return systemDark ? "dark" : "light";
|
|
1771
|
+
return theme;
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Hosts the widget in a shadow root: the app's CSS cannot reach the panel and
|
|
1775
|
+
* the panel's CSS cannot reach the app. Direct DOM work is unavoidable here —
|
|
1776
|
+
* React has no declarative way to create a shadow root.
|
|
1777
|
+
*/
|
|
1778
|
+
function ShadowPortal({ children, offset, primaryColor, theme }) {
|
|
1779
|
+
const [shadowRoot, setShadowRoot] = useState(null);
|
|
1780
|
+
const resolvedTheme = useResolvedTheme(theme);
|
|
1781
|
+
useEffect(() => {
|
|
1782
|
+
const host = document.createElement("div");
|
|
1783
|
+
host.setAttribute(WIDGET_MARKER, "");
|
|
1784
|
+
stopClicksAtHost(host);
|
|
1785
|
+
const root = host.attachShadow({ mode: "open" });
|
|
1786
|
+
const style = document.createElement("style");
|
|
1787
|
+
style.textContent = WIDGET_STYLES;
|
|
1788
|
+
root.append(style);
|
|
1789
|
+
document.body.append(host);
|
|
1790
|
+
setShadowRoot(root);
|
|
1791
|
+
return () => host.remove();
|
|
1792
|
+
}, []);
|
|
1793
|
+
useEffect(() => {
|
|
1794
|
+
const host = shadowRoot?.host;
|
|
1795
|
+
if (!(host instanceof HTMLElement)) return;
|
|
1796
|
+
host.setAttribute("data-theme", resolvedTheme);
|
|
1797
|
+
host.style.setProperty("--rf-offset", `${offset}px`);
|
|
1798
|
+
if (primaryColor) host.style.setProperty("--rf-primary", primaryColor);
|
|
1799
|
+
}, [
|
|
1800
|
+
shadowRoot,
|
|
1801
|
+
resolvedTheme,
|
|
1802
|
+
primaryColor,
|
|
1803
|
+
offset
|
|
1804
|
+
]);
|
|
1805
|
+
if (!shadowRoot) return null;
|
|
1806
|
+
return createPortal(children, shadowRoot);
|
|
1807
|
+
}
|
|
1808
|
+
//#endregion
|
|
1809
|
+
//#region src/feedback/core/console-recorder.ts
|
|
1810
|
+
const DEFAULT_LIMIT = 30;
|
|
1811
|
+
const MAX_MESSAGE_LENGTH = 1e3;
|
|
1812
|
+
const RECORDED_METHODS = ["error", "warn"];
|
|
1813
|
+
function serializeArgument(value) {
|
|
1814
|
+
if (typeof value === "string") return value;
|
|
1815
|
+
if (value instanceof Error) return `${value.name}: ${value.message}`;
|
|
1816
|
+
if (value === null || value === void 0) return String(value);
|
|
1817
|
+
if (typeof value === "object") try {
|
|
1818
|
+
return JSON.stringify(value) ?? "[unserializable]";
|
|
1819
|
+
} catch {
|
|
1820
|
+
return "[unserializable]";
|
|
1821
|
+
}
|
|
1822
|
+
if (typeof value === "function") return "[function]";
|
|
1823
|
+
return String(value);
|
|
1824
|
+
}
|
|
1825
|
+
function formatMessage(args) {
|
|
1826
|
+
const message = args.map(serializeArgument).join(" ");
|
|
1827
|
+
return message.length > MAX_MESSAGE_LENGTH ? `${message.slice(0, 999)}…` : message;
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Buffers the console noise a user saw right before reporting a bug.
|
|
1831
|
+
* Wrappers delegate to the previous implementation, so nothing is swallowed.
|
|
1832
|
+
*/
|
|
1833
|
+
function startConsoleRecorder(options) {
|
|
1834
|
+
const limit = options?.limit ?? DEFAULT_LIMIT;
|
|
1835
|
+
const buffer = [];
|
|
1836
|
+
let active = true;
|
|
1837
|
+
const push = (level, message) => {
|
|
1838
|
+
if (!(active && message)) return;
|
|
1839
|
+
buffer.push({
|
|
1840
|
+
level,
|
|
1841
|
+
message,
|
|
1842
|
+
timestamp: Date.now()
|
|
1843
|
+
});
|
|
1844
|
+
if (buffer.length > limit) buffer.splice(0, buffer.length - limit);
|
|
1845
|
+
};
|
|
1846
|
+
const originals = /* @__PURE__ */ new Map();
|
|
1847
|
+
const wrappers = /* @__PURE__ */ new Map();
|
|
1848
|
+
for (const method of RECORDED_METHODS) {
|
|
1849
|
+
const original = console[method];
|
|
1850
|
+
const wrapper = (...args) => {
|
|
1851
|
+
push(method, formatMessage(args));
|
|
1852
|
+
Reflect.apply(original, console, args);
|
|
1853
|
+
};
|
|
1854
|
+
originals.set(method, original);
|
|
1855
|
+
wrappers.set(method, wrapper);
|
|
1856
|
+
console[method] = wrapper;
|
|
1857
|
+
}
|
|
1858
|
+
const onError = (event) => {
|
|
1859
|
+
const where = event.filename ? ` (${event.filename}:${event.lineno}:${event.colno})` : "";
|
|
1860
|
+
push("error", `${event.message}${where}`);
|
|
1861
|
+
};
|
|
1862
|
+
const onRejection = (event) => {
|
|
1863
|
+
push("error", `Unhandled rejection: ${serializeArgument(event.reason)}`);
|
|
1864
|
+
};
|
|
1865
|
+
window.addEventListener("error", onError);
|
|
1866
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
1867
|
+
return {
|
|
1868
|
+
events: () => buffer.slice(),
|
|
1869
|
+
stop: () => {
|
|
1870
|
+
if (!active) return;
|
|
1871
|
+
active = false;
|
|
1872
|
+
buffer.length = 0;
|
|
1873
|
+
for (const method of RECORDED_METHODS) {
|
|
1874
|
+
const original = originals.get(method);
|
|
1875
|
+
if (original && console[method] === wrappers.get(method)) console[method] = original;
|
|
1876
|
+
}
|
|
1877
|
+
window.removeEventListener("error", onError);
|
|
1878
|
+
window.removeEventListener("unhandledrejection", onRejection);
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
//#endregion
|
|
1883
|
+
//#region src/feedback/ui/state/use-feedback-submission.ts
|
|
1884
|
+
function useFeedbackSubmission(options) {
|
|
1885
|
+
const [activeRequest, setActiveRequest] = useState(null);
|
|
1886
|
+
const [pendingReport, setPendingReport] = useState(null);
|
|
1887
|
+
const requestRef = useRef(null);
|
|
1888
|
+
useEffect(() => () => {
|
|
1889
|
+
requestRef.current = null;
|
|
1890
|
+
}, []);
|
|
1891
|
+
const resetSubmission = useCallback(() => {
|
|
1892
|
+
requestRef.current = null;
|
|
1893
|
+
setPendingReport(null);
|
|
1894
|
+
setActiveRequest(null);
|
|
1895
|
+
}, []);
|
|
1896
|
+
const finishSubmission = (result) => {
|
|
1897
|
+
if (result.pendingScreenshots.length > 0) {
|
|
1898
|
+
setPendingReport(result);
|
|
1899
|
+
options.onError(options.partialUploadMessage);
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
setPendingReport(null);
|
|
1903
|
+
options.onSubmitted(result.feedbackId);
|
|
1904
|
+
};
|
|
1905
|
+
const submit = async (draft) => {
|
|
1906
|
+
if (requestRef.current) return;
|
|
1907
|
+
if (!options.client) {
|
|
1908
|
+
options.onError("Reflet is missing a publicKey. Pass one to RefletFeedback or RefletProvider.");
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
const request = {};
|
|
1912
|
+
requestRef.current = request;
|
|
1913
|
+
setActiveRequest(request);
|
|
1914
|
+
options.onError(null);
|
|
1915
|
+
try {
|
|
1916
|
+
const transport = createFeedbackTransport(options.client);
|
|
1917
|
+
const result = pendingReport ? await attachPreparedScreenshots(transport, pendingReport) : await submitWidgetFeedback(transport, draft);
|
|
1918
|
+
if (requestRef.current === request) finishSubmission(result);
|
|
1919
|
+
} catch (error) {
|
|
1920
|
+
if (requestRef.current === request) options.onError(error instanceof Error ? error.message : "Something went wrong. Please try again.");
|
|
1921
|
+
} finally {
|
|
1922
|
+
if (requestRef.current === request) {
|
|
1923
|
+
requestRef.current = null;
|
|
1924
|
+
setActiveRequest(null);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
};
|
|
1928
|
+
return {
|
|
1929
|
+
hasPendingAttachments: pendingReport !== null,
|
|
1930
|
+
isEditingDisabled: activeRequest !== null || pendingReport !== null,
|
|
1931
|
+
isSubmitting: activeRequest !== null,
|
|
1932
|
+
resetSubmission,
|
|
1933
|
+
submit
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
//#endregion
|
|
1937
|
+
//#region src/feedback/ui/state/use-screenshot-drafts.ts
|
|
1938
|
+
function releaseDraft(draft) {
|
|
1939
|
+
releaseCapture(draft?.image);
|
|
2282
1940
|
}
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
1941
|
+
function useScreenshotDrafts(captureFailed, onError) {
|
|
1942
|
+
const [screenshots, setScreenshots] = useState([]);
|
|
1943
|
+
const [activeId, setActiveId] = useState(null);
|
|
1944
|
+
const [pendingCapture, setPendingCapture] = useState(null);
|
|
1945
|
+
const ownedDrafts = useRef(/* @__PURE__ */ new Map());
|
|
1946
|
+
const captureRequest = useRef(null);
|
|
1947
|
+
useEffect(() => {
|
|
1948
|
+
const drafts = ownedDrafts.current;
|
|
1949
|
+
return () => {
|
|
1950
|
+
captureRequest.current = null;
|
|
1951
|
+
for (const draft of drafts.values()) releaseDraft(draft);
|
|
1952
|
+
drafts.clear();
|
|
1953
|
+
};
|
|
1954
|
+
}, []);
|
|
1955
|
+
const storeScreenshot = useCallback((screenshot) => {
|
|
1956
|
+
releaseDraft(ownedDrafts.current.get(screenshot.id));
|
|
1957
|
+
ownedDrafts.current.set(screenshot.id, screenshot);
|
|
1958
|
+
setScreenshots((previous) => {
|
|
1959
|
+
return previous.some(({ id }) => id === screenshot.id) ? previous.map((item) => item.id === screenshot.id ? screenshot : item) : [...previous, screenshot];
|
|
1960
|
+
});
|
|
1961
|
+
setActiveId(screenshot.id);
|
|
1962
|
+
}, []);
|
|
1963
|
+
const cancelPendingCapture = useCallback(() => {
|
|
1964
|
+
captureRequest.current = null;
|
|
1965
|
+
setPendingCapture(null);
|
|
1966
|
+
}, []);
|
|
1967
|
+
const takeScreenshot = useCallback(async (request) => {
|
|
1968
|
+
captureRequest.current = request;
|
|
1969
|
+
setPendingCapture(request);
|
|
1970
|
+
onError(null);
|
|
1971
|
+
const context = collectPageContext({ sdkVersion: SDK_VERSION });
|
|
1972
|
+
const image = await captureViewport().catch(() => null);
|
|
1973
|
+
if (captureRequest.current !== request) {
|
|
1974
|
+
releaseCapture(image);
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
cancelPendingCapture();
|
|
1978
|
+
if (!image) {
|
|
1979
|
+
onError(captureFailed);
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
storeScreenshot({
|
|
1983
|
+
...request,
|
|
1984
|
+
annotations: [],
|
|
1985
|
+
context,
|
|
1986
|
+
image
|
|
1987
|
+
});
|
|
1988
|
+
}, [
|
|
1989
|
+
cancelPendingCapture,
|
|
1990
|
+
captureFailed,
|
|
1991
|
+
onError,
|
|
1992
|
+
storeScreenshot
|
|
1993
|
+
]);
|
|
1994
|
+
const takeCapture = useCallback(() => {
|
|
1995
|
+
if (!captureRequest.current) takeScreenshot({
|
|
1996
|
+
id: crypto.randomUUID(),
|
|
1997
|
+
source: "manual"
|
|
1998
|
+
});
|
|
1999
|
+
}, [takeScreenshot]);
|
|
2000
|
+
const refreshAutomatic = useCallback(() => {
|
|
2001
|
+
takeScreenshot({
|
|
2002
|
+
id: ownedDrafts.current.keys().next().value ?? captureRequest.current?.id ?? crypto.randomUUID(),
|
|
2003
|
+
source: "automatic"
|
|
2004
|
+
});
|
|
2005
|
+
}, [takeScreenshot]);
|
|
2006
|
+
const retakeCapture = useCallback((id) => {
|
|
2007
|
+
if (!captureRequest.current && ownedDrafts.current.has(id)) takeScreenshot({
|
|
2008
|
+
id,
|
|
2009
|
+
source: "manual"
|
|
2010
|
+
});
|
|
2011
|
+
}, [takeScreenshot]);
|
|
2012
|
+
const removeCapture = useCallback((id) => {
|
|
2013
|
+
if (captureRequest.current?.id === id) cancelPendingCapture();
|
|
2014
|
+
releaseDraft(ownedDrafts.current.get(id));
|
|
2015
|
+
ownedDrafts.current.delete(id);
|
|
2016
|
+
setScreenshots((previous) => previous.filter((item) => item.id !== id));
|
|
2017
|
+
setActiveId((current) => current === id ? null : current);
|
|
2018
|
+
}, [cancelPendingCapture]);
|
|
2019
|
+
const resetScreenshots = useCallback(() => {
|
|
2020
|
+
cancelPendingCapture();
|
|
2021
|
+
for (const draft of ownedDrafts.current.values()) releaseDraft(draft);
|
|
2022
|
+
ownedDrafts.current.clear();
|
|
2023
|
+
setScreenshots([]);
|
|
2024
|
+
setActiveId(null);
|
|
2025
|
+
}, [cancelPendingCapture]);
|
|
2026
|
+
const setAnnotations = useCallback((annotations) => {
|
|
2027
|
+
setScreenshots((previous) => previous.map((item) => item.id === activeId ? {
|
|
2028
|
+
...item,
|
|
2029
|
+
annotations
|
|
2030
|
+
} : item));
|
|
2031
|
+
}, [activeId]);
|
|
2032
|
+
const setSelectionComment = useCallback((id, comment) => {
|
|
2033
|
+
setScreenshots((previous) => previous.map((item) => item.id === id && item.selection ? {
|
|
2034
|
+
...item,
|
|
2035
|
+
selection: {
|
|
2036
|
+
...item.selection,
|
|
2037
|
+
comment
|
|
2038
|
+
}
|
|
2039
|
+
} : item));
|
|
2040
|
+
}, []);
|
|
2041
|
+
const activeScreenshot = screenshots.find(({ id }) => id === activeId) ?? screenshots.at(-1) ?? null;
|
|
2042
|
+
const firstCapture = pendingCapture ?? screenshots[0];
|
|
2043
|
+
return {
|
|
2044
|
+
activeScreenshot,
|
|
2045
|
+
cancelPendingCapture,
|
|
2046
|
+
canRefreshAutomatically: screenshots.length <= 1 && firstCapture?.source === "automatic" && (activeScreenshot?.annotations.length ?? 0) === 0 && !screenshots.some((draft) => draft.selection),
|
|
2047
|
+
pendingCapture,
|
|
2048
|
+
refreshAutomatic,
|
|
2049
|
+
removeCapture,
|
|
2050
|
+
resetScreenshots,
|
|
2051
|
+
retakeCapture,
|
|
2052
|
+
screenshots,
|
|
2053
|
+
selectScreenshot: setActiveId,
|
|
2054
|
+
setAnnotations,
|
|
2055
|
+
setSelectionComment,
|
|
2056
|
+
storeScreenshot,
|
|
2057
|
+
takeCapture
|
|
2058
|
+
};
|
|
2291
2059
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
backdrop-filter: blur(20px) saturate(1.6);
|
|
2297
|
-
}
|
|
2060
|
+
//#endregion
|
|
2061
|
+
//#region src/feedback/ui/state/use-widget-config.ts
|
|
2062
|
+
function dismissalDuration(days) {
|
|
2063
|
+
return days !== void 0 && Number.isFinite(days) && days > 0 ? days : null;
|
|
2298
2064
|
}
|
|
2299
|
-
|
|
2300
|
-
|
|
2065
|
+
function useWidgetConfig(props) {
|
|
2066
|
+
const context = useContext(RefletContext);
|
|
2067
|
+
const publicKey = props.publicKey ?? context?.publicKey;
|
|
2068
|
+
const baseUrl = props.baseUrl ?? context?.baseUrl;
|
|
2069
|
+
const user = props.user ?? context?.user;
|
|
2070
|
+
const userToken = props.userToken ?? context?.userToken;
|
|
2071
|
+
return {
|
|
2072
|
+
client: useMemo(() => publicKey ? new Reflet({
|
|
2073
|
+
baseUrl,
|
|
2074
|
+
publicKey,
|
|
2075
|
+
user,
|
|
2076
|
+
userToken
|
|
2077
|
+
}) : null, [
|
|
2078
|
+
baseUrl,
|
|
2079
|
+
publicKey,
|
|
2080
|
+
user,
|
|
2081
|
+
userToken
|
|
2082
|
+
]),
|
|
2083
|
+
dismissalKey: `reflet-feedback-dismissed:${publicKey ?? "default"}:${user?.id ?? "anonymous"}`,
|
|
2084
|
+
dismissForDays: dismissalDuration(props.dismissForDays),
|
|
2085
|
+
isAnonymous: !(user || userToken),
|
|
2086
|
+
publicKey: publicKey ?? null
|
|
2087
|
+
};
|
|
2301
2088
|
}
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2089
|
+
//#endregion
|
|
2090
|
+
//#region src/feedback/ui/use-capture-sync.ts
|
|
2091
|
+
const RESIZE_DEBOUNCE = 400;
|
|
2092
|
+
/**
|
|
2093
|
+
* Calls `refresh` whenever the page the reporter is looking at changes shape:
|
|
2094
|
+
* SPA routes (which never fire popstate on push/replace — history methods must
|
|
2095
|
+
* be patched) and window resizes, debounced. Keeps the frozen screenshot and
|
|
2096
|
+
* page context describing what the reporter currently sees.
|
|
2097
|
+
*/
|
|
2098
|
+
function useCaptureSync(enabled, refresh) {
|
|
2099
|
+
const refreshRef = useRef(refresh);
|
|
2100
|
+
useEffect(() => {
|
|
2101
|
+
refreshRef.current = refresh;
|
|
2102
|
+
});
|
|
2103
|
+
useEffect(() => {
|
|
2104
|
+
if (!enabled) return;
|
|
2105
|
+
const runRefresh = () => refreshRef.current();
|
|
2106
|
+
const history = window.history;
|
|
2107
|
+
const originalPush = history.pushState;
|
|
2108
|
+
const originalReplace = history.replaceState;
|
|
2109
|
+
const hadOwnPush = Object.hasOwn(history, "pushState");
|
|
2110
|
+
const hadOwnReplace = Object.hasOwn(history, "replaceState");
|
|
2111
|
+
history.pushState = function pushState(data, unused, url) {
|
|
2112
|
+
const result = originalPush.call(history, data, unused, url);
|
|
2113
|
+
runRefresh();
|
|
2114
|
+
return result;
|
|
2115
|
+
};
|
|
2116
|
+
history.replaceState = function replaceState(data, unused, url) {
|
|
2117
|
+
const result = originalReplace.call(history, data, unused, url);
|
|
2118
|
+
runRefresh();
|
|
2119
|
+
return result;
|
|
2120
|
+
};
|
|
2121
|
+
window.addEventListener("popstate", runRefresh);
|
|
2122
|
+
let resizeTimer;
|
|
2123
|
+
const onResize = () => {
|
|
2124
|
+
window.clearTimeout(resizeTimer);
|
|
2125
|
+
resizeTimer = window.setTimeout(runRefresh, RESIZE_DEBOUNCE);
|
|
2126
|
+
};
|
|
2127
|
+
window.addEventListener("resize", onResize);
|
|
2128
|
+
return () => {
|
|
2129
|
+
if (hadOwnPush) history.pushState = originalPush;
|
|
2130
|
+
else Reflect.deleteProperty(history, "pushState");
|
|
2131
|
+
if (hadOwnReplace) history.replaceState = originalReplace;
|
|
2132
|
+
else Reflect.deleteProperty(history, "replaceState");
|
|
2133
|
+
window.removeEventListener("popstate", runRefresh);
|
|
2134
|
+
window.clearTimeout(resizeTimer);
|
|
2135
|
+
window.removeEventListener("resize", onResize);
|
|
2136
|
+
};
|
|
2137
|
+
}, [enabled]);
|
|
2312
2138
|
}
|
|
2313
|
-
`;
|
|
2314
2139
|
//#endregion
|
|
2315
|
-
//#region src/feedback/ui/
|
|
2316
|
-
const
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
const
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
return
|
|
2326
|
-
}
|
|
2327
|
-
|
|
2328
|
-
return theme;
|
|
2140
|
+
//#region src/feedback/ui/use-widget-state.ts
|
|
2141
|
+
const SUCCESS_CLOSE_DELAY = 2400;
|
|
2142
|
+
const MILLISECONDS_PER_DAY = 864e5;
|
|
2143
|
+
function readDismissedUntil(key) {
|
|
2144
|
+
if (typeof window === "undefined") return 0;
|
|
2145
|
+
try {
|
|
2146
|
+
const value = Number.parseInt(window.localStorage.getItem(key) ?? "", 10);
|
|
2147
|
+
if (Number.isFinite(value) && value > Date.now()) return value;
|
|
2148
|
+
window.localStorage.removeItem(key);
|
|
2149
|
+
} catch {
|
|
2150
|
+
return 0;
|
|
2151
|
+
}
|
|
2152
|
+
return 0;
|
|
2329
2153
|
}
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
function
|
|
2336
|
-
const
|
|
2337
|
-
const
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2154
|
+
function writeDismissedUntil(key, until) {
|
|
2155
|
+
try {
|
|
2156
|
+
window.localStorage.setItem(key, String(until));
|
|
2157
|
+
} catch {}
|
|
2158
|
+
}
|
|
2159
|
+
function useWidgetState(props) {
|
|
2160
|
+
const { client, dismissalKey, dismissForDays, isAnonymous, publicKey } = useWidgetConfig(props);
|
|
2161
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
2162
|
+
const [annotationTrigger, setAnnotationTrigger] = useState(null);
|
|
2163
|
+
const [step, setStep] = useState("compose");
|
|
2164
|
+
const [message, setMessage] = useState("");
|
|
2165
|
+
const [email, setEmail] = useState("");
|
|
2166
|
+
const [honeypot, setHoneypot] = useState("");
|
|
2167
|
+
const [isElementCapturing, setIsElementCapturing] = useState(false);
|
|
2168
|
+
const successTimer = useRef(null);
|
|
2169
|
+
const [error, setError] = useState(null);
|
|
2170
|
+
const [dismissedUntil, setDismissedUntil] = useState(() => readDismissedUntil(dismissalKey));
|
|
2171
|
+
const pickRef = useRef(0);
|
|
2172
|
+
useEffect(() => () => {
|
|
2173
|
+
pickRef.current++;
|
|
2174
|
+
if (successTimer.current) clearTimeout(successTimer.current);
|
|
2349
2175
|
}, []);
|
|
2176
|
+
const consoleRef = useRef(null);
|
|
2350
2177
|
useEffect(() => {
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2178
|
+
setDismissedUntil(readDismissedUntil(dismissalKey));
|
|
2179
|
+
}, [dismissalKey]);
|
|
2180
|
+
useEffect(() => {
|
|
2181
|
+
if (props.captureConsole === false) return;
|
|
2182
|
+
const recorder = startConsoleRecorder();
|
|
2183
|
+
consoleRef.current = recorder;
|
|
2184
|
+
return () => {
|
|
2185
|
+
consoleRef.current = null;
|
|
2186
|
+
recorder.stop();
|
|
2187
|
+
};
|
|
2188
|
+
}, [props.captureConsole]);
|
|
2189
|
+
const captureFailed = props.labels?.captureFailed ?? DEFAULT_WIDGET_LABELS.captureFailed;
|
|
2190
|
+
const gallery = useScreenshotDrafts(captureFailed, setError);
|
|
2191
|
+
const { activeScreenshot, cancelPendingCapture, refreshAutomatic, resetScreenshots, storeScreenshot } = gallery;
|
|
2192
|
+
const capture = activeScreenshot?.image ?? null;
|
|
2193
|
+
const annotations = activeScreenshot?.annotations ?? [];
|
|
2194
|
+
const isCapturing = gallery.pendingCapture !== null || isElementCapturing;
|
|
2195
|
+
const submission = useFeedbackSubmission({
|
|
2196
|
+
client,
|
|
2197
|
+
onError: setError,
|
|
2198
|
+
onSubmitted: (feedbackId) => {
|
|
2199
|
+
setStep("success");
|
|
2200
|
+
props.onSubmit?.({ feedbackId });
|
|
2201
|
+
successTimer.current = setTimeout(close, SUCCESS_CLOSE_DELAY);
|
|
2202
|
+
},
|
|
2203
|
+
partialUploadMessage: props.labels?.attachmentUploadFailed ?? DEFAULT_WIDGET_LABELS.attachmentUploadFailed
|
|
2204
|
+
});
|
|
2205
|
+
const { resetSubmission } = submission;
|
|
2206
|
+
useCaptureSync(isOpen && !submission.isEditingDisabled && step === "compose" && gallery.canRefreshAutomatically && props.captureOnOpen !== false, refreshAutomatic);
|
|
2207
|
+
const reset = useCallback(() => {
|
|
2208
|
+
if (successTimer.current) clearTimeout(successTimer.current);
|
|
2209
|
+
setAnnotationTrigger(null);
|
|
2210
|
+
resetSubmission();
|
|
2211
|
+
pickRef.current++;
|
|
2212
|
+
setIsElementCapturing(false);
|
|
2213
|
+
resetScreenshots();
|
|
2214
|
+
setMessage("");
|
|
2215
|
+
setEmail("");
|
|
2216
|
+
setHoneypot("");
|
|
2217
|
+
setError(null);
|
|
2218
|
+
setStep("compose");
|
|
2219
|
+
}, [resetSubmission, resetScreenshots]);
|
|
2220
|
+
const close = useCallback(() => {
|
|
2221
|
+
setIsOpen(false);
|
|
2222
|
+
reset();
|
|
2223
|
+
props.onClose?.();
|
|
2224
|
+
}, [reset, props.onClose]);
|
|
2225
|
+
const dismiss = useCallback(() => {
|
|
2226
|
+
if (dismissForDays === null) return;
|
|
2227
|
+
const until = Date.now() + dismissForDays * MILLISECONDS_PER_DAY;
|
|
2228
|
+
writeDismissedUntil(dismissalKey, until);
|
|
2229
|
+
setIsOpen(false);
|
|
2230
|
+
reset();
|
|
2231
|
+
setDismissedUntil(until);
|
|
2232
|
+
props.onDismiss?.({ until });
|
|
2356
2233
|
}, [
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2234
|
+
dismissForDays,
|
|
2235
|
+
dismissalKey,
|
|
2236
|
+
props.onDismiss,
|
|
2237
|
+
reset
|
|
2361
2238
|
]);
|
|
2362
|
-
|
|
2363
|
-
|
|
2239
|
+
const open = useCallback(() => {
|
|
2240
|
+
setIsOpen(true);
|
|
2241
|
+
setStep("compose");
|
|
2242
|
+
props.onOpen?.();
|
|
2243
|
+
if (props.captureOnOpen !== false) refreshAutomatic();
|
|
2244
|
+
}, [
|
|
2245
|
+
props.captureOnOpen,
|
|
2246
|
+
props.onOpen,
|
|
2247
|
+
refreshAutomatic
|
|
2248
|
+
]);
|
|
2249
|
+
const selectElement = useCallback(async (element, note = "") => {
|
|
2250
|
+
cancelPendingCapture();
|
|
2251
|
+
setIsElementCapturing(true);
|
|
2252
|
+
const picked = note ? {
|
|
2253
|
+
...buildElementSelection(element),
|
|
2254
|
+
comment: note.slice(0, MAX_SELECTION_COMMENT_LENGTH)
|
|
2255
|
+
} : buildElementSelection(element);
|
|
2256
|
+
setStep("compose");
|
|
2257
|
+
const pick = ++pickRef.current;
|
|
2258
|
+
const context = collectPageContext({ sdkVersion: SDK_VERSION });
|
|
2259
|
+
const fresh = await captureViewport().catch(() => null);
|
|
2260
|
+
if (pickRef.current !== pick) {
|
|
2261
|
+
releaseCapture(fresh);
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
setIsElementCapturing(false);
|
|
2265
|
+
if (!fresh) {
|
|
2266
|
+
setError(captureFailed);
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
storeScreenshot({
|
|
2270
|
+
annotations: [highlightFor(picked, fresh)],
|
|
2271
|
+
context,
|
|
2272
|
+
id: crypto.randomUUID(),
|
|
2273
|
+
image: fresh,
|
|
2274
|
+
selectedNode: element,
|
|
2275
|
+
selection: picked,
|
|
2276
|
+
source: "manual"
|
|
2277
|
+
});
|
|
2278
|
+
}, [
|
|
2279
|
+
cancelPendingCapture,
|
|
2280
|
+
captureFailed,
|
|
2281
|
+
storeScreenshot
|
|
2282
|
+
]);
|
|
2283
|
+
const selections = gallery.screenshots.flatMap(({ selection }) => selection ? [selection] : []);
|
|
2284
|
+
const firstElementNote = selections.find(({ comment }) => comment)?.comment;
|
|
2285
|
+
const reportedMessage = message.trim() || firstElementNote || "";
|
|
2286
|
+
const submit = () => {
|
|
2287
|
+
if (honeypot) {
|
|
2288
|
+
setStep("success");
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
setStep("compose");
|
|
2292
|
+
return submission.submit({
|
|
2293
|
+
context: {
|
|
2294
|
+
...activeScreenshot?.context ?? collectPageContext({ sdkVersion: "0.6.5" }),
|
|
2295
|
+
consoleEvents: consoleRef.current?.events(),
|
|
2296
|
+
metadata: props.metadata,
|
|
2297
|
+
selections
|
|
2298
|
+
},
|
|
2299
|
+
email,
|
|
2300
|
+
isAnonymous,
|
|
2301
|
+
message: reportedMessage,
|
|
2302
|
+
screenshots: gallery.screenshots
|
|
2303
|
+
});
|
|
2304
|
+
};
|
|
2305
|
+
const requestSubmit = () => {
|
|
2306
|
+
if (isAnonymous && email.length === 0 && !submission.hasPendingAttachments && !honeypot) {
|
|
2307
|
+
setStep("email");
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
return submit();
|
|
2311
|
+
};
|
|
2312
|
+
return {
|
|
2313
|
+
activeScreenshot,
|
|
2314
|
+
annotateScreenshot: (id, trigger) => {
|
|
2315
|
+
setAnnotationTrigger(trigger);
|
|
2316
|
+
gallery.selectScreenshot(id);
|
|
2317
|
+
setStep("annotate");
|
|
2318
|
+
},
|
|
2319
|
+
annotations,
|
|
2320
|
+
annotationTrigger,
|
|
2321
|
+
canDismiss: dismissForDays !== null,
|
|
2322
|
+
canSubmit: reportedMessage.length > 0,
|
|
2323
|
+
capture,
|
|
2324
|
+
close,
|
|
2325
|
+
dismiss,
|
|
2326
|
+
dismissForDays,
|
|
2327
|
+
email,
|
|
2328
|
+
error,
|
|
2329
|
+
hasPendingAttachments: submission.hasPendingAttachments,
|
|
2330
|
+
honeypot,
|
|
2331
|
+
isAnonymous,
|
|
2332
|
+
isCapturing,
|
|
2333
|
+
isDismissed: dismissedUntil > Date.now(),
|
|
2334
|
+
isEditingDisabled: submission.isEditingDisabled,
|
|
2335
|
+
isOpen,
|
|
2336
|
+
isSubmitting: submission.isSubmitting,
|
|
2337
|
+
message,
|
|
2338
|
+
open,
|
|
2339
|
+
pendingCapture: gallery.pendingCapture,
|
|
2340
|
+
publicKey,
|
|
2341
|
+
removeCapture: gallery.removeCapture,
|
|
2342
|
+
requestSubmit,
|
|
2343
|
+
retakeCapture: gallery.retakeCapture,
|
|
2344
|
+
screenshots: gallery.screenshots,
|
|
2345
|
+
selectElement,
|
|
2346
|
+
selections,
|
|
2347
|
+
setAnnotations: gallery.setAnnotations,
|
|
2348
|
+
setEmail,
|
|
2349
|
+
setHoneypot,
|
|
2350
|
+
setMessage,
|
|
2351
|
+
setSelectionComment: gallery.setSelectionComment,
|
|
2352
|
+
setStep,
|
|
2353
|
+
step,
|
|
2354
|
+
submit,
|
|
2355
|
+
takeCapture: gallery.takeCapture
|
|
2356
|
+
};
|
|
2364
2357
|
}
|
|
2365
2358
|
//#endregion
|
|
2366
2359
|
//#region src/feedback/widget.tsx
|
|
2367
|
-
const DevtoolsLayer = process.env.NODE_ENV === "development" ? lazy(() => import("./devtools-layer-
|
|
2360
|
+
const DevtoolsLayer = process.env.NODE_ENV === "development" ? lazy(() => import("./devtools-layer-CEjzYGBB.js").then((module) => ({ default: module.DevtoolsLayer }))) : null;
|
|
2368
2361
|
function RefletFeedback(props) {
|
|
2369
2362
|
const state = useWidgetState(props);
|
|
2370
2363
|
const launcher = useWidgetLauncher(state);
|