reflet-sdk 0.6.3 → 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-Rh-hsxyX.js";
4
- import { n as RefletContext, t as REFLET_Z_INDEX } from "./z-index-CIb4-3Fy.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.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
- .glass::before {
2284
- content: "";
2285
- position: absolute;
2286
- inset: 0;
2287
- z-index: -1;
2288
- border-radius: inherit;
2289
- background: var(--rf-bg);
2290
- 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
+ };
2291
2059
  }
2292
- @supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
2293
- .glass::before {
2294
- background: var(--rf-glass);
2295
- -webkit-backdrop-filter: blur(20px) saturate(1.6);
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
- @media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
2300
- .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
+ };
2301
2088
  }
2302
- .capture-halo { position: fixed; inset: 0; z-index: ${REFLET_Z_INDEX.captureHalo}; pointer-events: none; opacity: 0; transition: opacity 500ms var(--rf-ease); }
2303
- .capture-halo[data-active="true"] { opacity: 1; transition-duration: 140ms; }
2304
- .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); }
2305
- .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)); }
2306
- ${COMPOSER_STYLES}
2307
- ${ANNOTATOR_STYLES}
2308
- ${PICKER_STYLES}
2309
- @media (prefers-reduced-motion: reduce) {
2310
- *, *::before, *::after { animation: none !important; transition: none !important; }
2311
- 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]);
2312
2138
  }
2313
- `;
2314
2139
  //#endregion
2315
- //#region src/feedback/ui/shadow-portal.tsx
2316
- const DARK_QUERY = "(prefers-color-scheme: dark)";
2317
- function useResolvedTheme(theme) {
2318
- const [systemDark, setSystemDark] = useState(false);
2319
- useEffect(() => {
2320
- if (theme !== "auto" || typeof window.matchMedia !== "function") return;
2321
- const query = window.matchMedia(DARK_QUERY);
2322
- setSystemDark(query.matches);
2323
- const onChange = (event) => setSystemDark(event.matches);
2324
- query.addEventListener("change", onChange);
2325
- return () => query.removeEventListener("change", onChange);
2326
- }, [theme]);
2327
- if (theme === "auto") return systemDark ? "dark" : "light";
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
- * Hosts the widget in a shadow root: the app's CSS cannot reach the panel and
2332
- * the panel's CSS cannot reach the app. Direct DOM work is unavoidable here —
2333
- * React has no declarative way to create a shadow root.
2334
- */
2335
- function ShadowPortal({ children, offset, primaryColor, theme }) {
2336
- const [shadowRoot, setShadowRoot] = useState(null);
2337
- const resolvedTheme = useResolvedTheme(theme);
2338
- useEffect(() => {
2339
- const host = document.createElement("div");
2340
- host.setAttribute(WIDGET_MARKER, "");
2341
- stopClicksAtHost(host);
2342
- const root = host.attachShadow({ mode: "open" });
2343
- const style = document.createElement("style");
2344
- style.textContent = WIDGET_STYLES;
2345
- root.append(style);
2346
- document.body.append(host);
2347
- setShadowRoot(root);
2348
- 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);
2349
2175
  }, []);
2176
+ const consoleRef = useRef(null);
2350
2177
  useEffect(() => {
2351
- const host = shadowRoot?.host;
2352
- if (!(host instanceof HTMLElement)) return;
2353
- host.setAttribute("data-theme", resolvedTheme);
2354
- host.style.setProperty("--rf-offset", `${offset}px`);
2355
- 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 });
2356
2233
  }, [
2357
- shadowRoot,
2358
- resolvedTheme,
2359
- primaryColor,
2360
- offset
2234
+ dismissForDays,
2235
+ dismissalKey,
2236
+ props.onDismiss,
2237
+ reset
2361
2238
  ]);
2362
- if (!shadowRoot) return null;
2363
- 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
+ };
2364
2357
  }
2365
2358
  //#endregion
2366
2359
  //#region src/feedback/widget.tsx
2367
- const DevtoolsLayer = process.env.NODE_ENV === "development" ? lazy(() => import("./devtools-layer-DtO89RlJ.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;
2368
2361
  function RefletFeedback(props) {
2369
2362
  const state = useWidgetState(props);
2370
2363
  const launcher = useWidgetLauncher(state);