reflet-sdk 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 TargetIcon, B as buildElementSelection, C as GripIcon, D as SpotlightIcon, E as PencilIcon, F as normalizeRect, G as WIDGET_MARKER, I as DEFAULT_WIDGET_LABELS, J as releaseCapture, K as captureViewport, L as SDK_VERSION, M as TrashIcon, N as UndoIcon, O as SquareIcon, P as isDegenerate, R as collectPageContext, S as CloseIcon, T as MoreIcon, _ as BlurIcon, b as CheckIcon, c as useFloatingPosition, d as SelectionOutline, f as drawAnnotations, g as ArrowIcon, h as stopClicksAtHost, i as createFeedbackTransport, j as TextIcon, m as listenToKeydown, o as submitWidgetFeedback, p as listenOnPageAndWidget, r as attachPreparedScreenshots, t as ElementPicker, v as CameraIcon, w as HighlightIcon, x as ChevronDownIcon, y as ChatIcon, z as MAX_SELECTION_COMMENT_LENGTH } from "./picker-DCoAFX5y.js";
4
- import { n as RefletContext, t as REFLET_Z_INDEX } from "./z-index-BL0SGZEP.js";
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-Cd-g1Dtg.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.key === "Enter" && !event.shiftKey) {
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 / window.innerWidth;
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("div", {
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/core/console-recorder.ts
1144
- const DEFAULT_LIMIT = 30;
1145
- const MAX_MESSAGE_LENGTH = 1e3;
1146
- const RECORDED_METHODS = ["error", "warn"];
1147
- function serializeArgument(value) {
1148
- if (typeof value === "string") return value;
1149
- if (value instanceof Error) return `${value.name}: ${value.message}`;
1150
- if (value === null || value === void 0) return String(value);
1151
- if (typeof value === "object") try {
1152
- return JSON.stringify(value) ?? "[unserializable]";
1153
- } catch {
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
- if (buffer.length > limit) buffer.splice(0, buffer.length - limit);
1179
- };
1180
- const originals = /* @__PURE__ */ new Map();
1181
- const wrappers = /* @__PURE__ */ new Map();
1182
- for (const method of RECORDED_METHODS) {
1183
- const original = console[method];
1184
- const wrapper = (...args) => {
1185
- push(method, formatMessage(args));
1186
- Reflect.apply(original, console, args);
1187
- };
1188
- originals.set(method, original);
1189
- wrappers.set(method, wrapper);
1190
- console[method] = wrapper;
1191
- }
1192
- const onError = (event) => {
1193
- const where = event.filename ? ` (${event.filename}:${event.lineno}:${event.colno})` : "";
1194
- push("error", `${event.message}${where}`);
1195
- };
1196
- const onRejection = (event) => {
1197
- push("error", `Unhandled rejection: ${serializeArgument(event.reason)}`);
1198
- };
1199
- window.addEventListener("error", onError);
1200
- window.addEventListener("unhandledrejection", onRejection);
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
- window.removeEventListener("error", onError);
1212
- window.removeEventListener("unhandledrejection", onRejection);
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/state/use-feedback-submission.ts
1218
- function useFeedbackSubmission(options) {
1219
- const [activeRequest, setActiveRequest] = useState(null);
1220
- const [pendingReport, setPendingReport] = useState(null);
1221
- const requestRef = useRef(null);
1222
- useEffect(() => () => {
1223
- requestRef.current = null;
1224
- }, []);
1225
- const resetSubmission = useCallback(() => {
1226
- requestRef.current = null;
1227
- setPendingReport(null);
1228
- setActiveRequest(null);
1229
- }, []);
1230
- const finishSubmission = (result) => {
1231
- if (result.pendingScreenshots.length > 0) {
1232
- setPendingReport(result);
1233
- options.onError(options.partialUploadMessage);
1234
- return;
1235
- }
1236
- setPendingReport(null);
1237
- options.onSubmitted(result.feedbackId);
1238
- };
1239
- const submit = async (draft) => {
1240
- if (requestRef.current) return;
1241
- if (!options.client) {
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
- hasPendingAttachments: pendingReport !== null,
1264
- isEditingDisabled: activeRequest !== null || pendingReport !== null,
1265
- isSubmitting: activeRequest !== null,
1266
- resetSubmission,
1267
- submit
1268
- };
1269
- }
1270
- //#endregion
1271
- //#region src/feedback/ui/state/use-screenshot-drafts.ts
1272
- function releaseDraft(draft) {
1273
- releaseCapture(draft?.image);
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.2" }),
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); }
@@ -2009,6 +1452,8 @@ summary::-webkit-details-marker { display: none; }
2009
1452
  //#endregion
2010
1453
  //#region src/feedback/ui/floating/picker-styles.ts
2011
1454
  const PICKER_STYLES = `
1455
+ .picker { position: fixed; inset: 0; z-index: ${REFLET_Z_INDEX.elementPicker}; pointer-events: none; }
1456
+ .picker-hint, .picker-note { pointer-events: auto; }
2012
1457
  .picker-box {
2013
1458
  position: fixed;
2014
1459
  top: 0;
@@ -2096,17 +1541,15 @@ const PICKER_STYLES = `
2096
1541
  .picker-cancel:hover { background: var(--rf-bg-hover); color: var(--rf-text); }
2097
1542
  .picker-note {
2098
1543
  position: fixed;
2099
- z-index: ${REFLET_Z_INDEX.annotatorOverlay};
2100
1544
  display: flex;
2101
1545
  flex-direction: column;
2102
1546
  gap: 6px;
2103
- width: 300px;
1547
+ width: 320px;
2104
1548
  max-width: calc(100vw - 24px);
2105
1549
  padding: 6px 6px 6px 8px;
2106
1550
  border-radius: 18px;
2107
1551
  animation: rf-in 180ms var(--rf-ease);
2108
1552
  }
2109
- .picker-note:focus-within { outline: 2px solid var(--rf-accent); outline-offset: 2px; }
2110
1553
  .picker-note-target {
2111
1554
  display: flex;
2112
1555
  align-items: center;
@@ -2134,7 +1577,9 @@ const PICKER_STYLES = `
2134
1577
  }
2135
1578
  .picker-note textarea:focus { outline: none; }
2136
1579
  .picker-note textarea::placeholder { color: var(--rf-text-muted); }
2137
- .picker-note-actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
1580
+ .picker-note-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; }
1581
+ .picker-note .picker-cancel { padding: 0 10px; }
1582
+ .picker-note .submit { margin-left: auto; }
2138
1583
  @media (pointer: coarse) {
2139
1584
  .picker-hint { padding: 6px 8px 6px 14px; }
2140
1585
  .picker-hint kbd { display: none; }
@@ -2272,97 +1717,647 @@ svg { flex-shrink: 0; }
2272
1717
  .icon-btn, .tool { width: 44px; height: 44px; }
2273
1718
  .launcher, .ghost-btn { min-height: 44px; }
2274
1719
  }
2275
- .glass {
2276
- position: relative;
2277
- isolation: isolate;
2278
- border: 1px solid var(--rf-glass-stroke);
2279
- 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);
2280
1940
  }
2281
- .glass::before {
2282
- content: "";
2283
- position: absolute;
2284
- inset: 0;
2285
- z-index: -1;
2286
- border-radius: inherit;
2287
- background: var(--rf-bg);
2288
- pointer-events: none;
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
+ };
2289
2059
  }
2290
- @supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
2291
- .glass::before {
2292
- background: var(--rf-glass);
2293
- -webkit-backdrop-filter: blur(20px) saturate(1.6);
2294
- backdrop-filter: blur(20px) saturate(1.6);
2295
- }
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;
2296
2064
  }
2297
- @media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
2298
- .glass::before { background: var(--rf-bg); backdrop-filter: none; -webkit-backdrop-filter: none; }
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
+ };
2299
2088
  }
2300
- .capture-halo { position: fixed; inset: 0; z-index: ${REFLET_Z_INDEX.captureHalo}; pointer-events: none; opacity: 0; transition: opacity 500ms var(--rf-ease); }
2301
- .capture-halo[data-active="true"] { opacity: 1; transition-duration: 140ms; }
2302
- .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); }
2303
- .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)); }
2304
- ${COMPOSER_STYLES}
2305
- ${ANNOTATOR_STYLES}
2306
- ${PICKER_STYLES}
2307
- @media (prefers-reduced-motion: reduce) {
2308
- *, *::before, *::after { animation: none !important; transition: none !important; }
2309
- button:active:not(:disabled) { scale: 1; }
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]);
2310
2138
  }
2311
- `;
2312
2139
  //#endregion
2313
- //#region src/feedback/ui/shadow-portal.tsx
2314
- const DARK_QUERY = "(prefers-color-scheme: dark)";
2315
- function useResolvedTheme(theme) {
2316
- const [systemDark, setSystemDark] = useState(false);
2317
- useEffect(() => {
2318
- if (theme !== "auto" || typeof window.matchMedia !== "function") return;
2319
- const query = window.matchMedia(DARK_QUERY);
2320
- setSystemDark(query.matches);
2321
- const onChange = (event) => setSystemDark(event.matches);
2322
- query.addEventListener("change", onChange);
2323
- return () => query.removeEventListener("change", onChange);
2324
- }, [theme]);
2325
- if (theme === "auto") return systemDark ? "dark" : "light";
2326
- 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;
2327
2153
  }
2328
- /**
2329
- * Hosts the widget in a shadow root: the app's CSS cannot reach the panel and
2330
- * the panel's CSS cannot reach the app. Direct DOM work is unavoidable here —
2331
- * React has no declarative way to create a shadow root.
2332
- */
2333
- function ShadowPortal({ children, offset, primaryColor, theme }) {
2334
- const [shadowRoot, setShadowRoot] = useState(null);
2335
- const resolvedTheme = useResolvedTheme(theme);
2336
- useEffect(() => {
2337
- const host = document.createElement("div");
2338
- host.setAttribute(WIDGET_MARKER, "");
2339
- stopClicksAtHost(host);
2340
- const root = host.attachShadow({ mode: "open" });
2341
- const style = document.createElement("style");
2342
- style.textContent = WIDGET_STYLES;
2343
- root.append(style);
2344
- document.body.append(host);
2345
- setShadowRoot(root);
2346
- return () => host.remove();
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);
2347
2175
  }, []);
2176
+ const consoleRef = useRef(null);
2348
2177
  useEffect(() => {
2349
- const host = shadowRoot?.host;
2350
- if (!(host instanceof HTMLElement)) return;
2351
- host.setAttribute("data-theme", resolvedTheme);
2352
- host.style.setProperty("--rf-offset", `${offset}px`);
2353
- if (primaryColor) host.style.setProperty("--rf-primary", primaryColor);
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 });
2354
2233
  }, [
2355
- shadowRoot,
2356
- resolvedTheme,
2357
- primaryColor,
2358
- offset
2234
+ dismissForDays,
2235
+ dismissalKey,
2236
+ props.onDismiss,
2237
+ reset
2359
2238
  ]);
2360
- if (!shadowRoot) return null;
2361
- return createPortal(children, shadowRoot);
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.4" }),
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
+ };
2362
2357
  }
2363
2358
  //#endregion
2364
2359
  //#region src/feedback/widget.tsx
2365
- const DevtoolsLayer = process.env.NODE_ENV === "development" ? lazy(() => import("./devtools-layer-DOECMj0S.js").then((module) => ({ default: module.DevtoolsLayer }))) : null;
2360
+ const DevtoolsLayer = process.env.NODE_ENV === "development" ? lazy(() => import("./devtools-layer-DgmM3RhW.js").then((module) => ({ default: module.DevtoolsLayer }))) : null;
2366
2361
  function RefletFeedback(props) {
2367
2362
  const state = useWidgetState(props);
2368
2363
  const launcher = useWidgetLauncher(state);