reflet-sdk 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/feedback.d.ts.map +1 -1
- package/dist/feedback.js +1692 -1640
- package/dist/feedback.js.map +1 -1
- package/dist/react.js +1 -1
- package/dist/{react-context-3_IbhIFB.js → z-index-_Q9PWbfY.js} +11 -11
- package/dist/z-index-_Q9PWbfY.js.map +1 -0
- package/package.json +1 -1
- package/dist/react-context-3_IbhIFB.js.map +0 -1
package/dist/feedback.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { a as REFLET_Z_INDEX, t as RefletContext } from "./react-context-3_IbhIFB.js";
|
|
3
2
|
import { t as Reflet } from "./client-BYuSKJw5.js";
|
|
3
|
+
import { n as RefletContext, t as REFLET_Z_INDEX } from "./z-index-_Q9PWbfY.js";
|
|
4
4
|
import { useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import { Slider } from "@base-ui/react/slider";
|
|
@@ -810,7 +810,7 @@ function collectPageContext(options) {
|
|
|
810
810
|
}
|
|
811
811
|
//#endregion
|
|
812
812
|
//#region src/feedback/types.ts
|
|
813
|
-
const SDK_VERSION = "0.4.
|
|
813
|
+
const SDK_VERSION = "0.4.3";
|
|
814
814
|
const DEFAULT_WIDGET_LABELS = {
|
|
815
815
|
annotateHint: "Draw on the screenshot to point at the problem.",
|
|
816
816
|
attachElement: "Attach this element",
|
|
@@ -1169,6 +1169,54 @@ function useTextAnnotation(options) {
|
|
|
1169
1169
|
};
|
|
1170
1170
|
}
|
|
1171
1171
|
//#endregion
|
|
1172
|
+
//#region src/feedback/ui/widget-keys.ts
|
|
1173
|
+
const KEY_EVENTS = [
|
|
1174
|
+
"keydown",
|
|
1175
|
+
"keyup",
|
|
1176
|
+
"keypress"
|
|
1177
|
+
];
|
|
1178
|
+
function isWidgetNode(node) {
|
|
1179
|
+
if (node instanceof ShadowRoot) return node.host.hasAttribute(WIDGET_MARKER);
|
|
1180
|
+
return node instanceof Element && node.hasAttribute("data-reflet-widget");
|
|
1181
|
+
}
|
|
1182
|
+
function startsInWidget(event) {
|
|
1183
|
+
return event.composedPath().some(isWidgetNode);
|
|
1184
|
+
}
|
|
1185
|
+
function relayInsideWidget(event) {
|
|
1186
|
+
const target = event.composedPath()[0];
|
|
1187
|
+
if (!(event instanceof KeyboardEvent && target && startsInWidget(event))) return;
|
|
1188
|
+
event.stopImmediatePropagation();
|
|
1189
|
+
const relay = new KeyboardEvent(event.type, {
|
|
1190
|
+
altKey: event.altKey,
|
|
1191
|
+
bubbles: true,
|
|
1192
|
+
cancelable: true,
|
|
1193
|
+
charCode: event.charCode,
|
|
1194
|
+
code: event.code,
|
|
1195
|
+
ctrlKey: event.ctrlKey,
|
|
1196
|
+
isComposing: event.isComposing,
|
|
1197
|
+
key: event.key,
|
|
1198
|
+
keyCode: event.keyCode,
|
|
1199
|
+
location: event.location,
|
|
1200
|
+
metaKey: event.metaKey,
|
|
1201
|
+
repeat: event.repeat,
|
|
1202
|
+
shiftKey: event.shiftKey
|
|
1203
|
+
});
|
|
1204
|
+
target.dispatchEvent(relay);
|
|
1205
|
+
if (relay.defaultPrevented) event.preventDefault();
|
|
1206
|
+
}
|
|
1207
|
+
if (typeof window !== "undefined") for (const type of KEY_EVENTS) window.addEventListener(type, relayInsideWidget, true);
|
|
1208
|
+
function listenToKeydown(anchor, onKeyDown, capture = false) {
|
|
1209
|
+
const widgetRoot = anchor?.getRootNode();
|
|
1210
|
+
const targets = widgetRoot instanceof ShadowRoot ? [document, widgetRoot] : [document];
|
|
1211
|
+
const listener = (event) => {
|
|
1212
|
+
if (event instanceof KeyboardEvent) onKeyDown(event);
|
|
1213
|
+
};
|
|
1214
|
+
for (const target of targets) target.addEventListener("keydown", listener, capture);
|
|
1215
|
+
return () => {
|
|
1216
|
+
for (const target of targets) target.removeEventListener("keydown", listener, capture);
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
//#endregion
|
|
1172
1220
|
//#region src/feedback/ui/floating/use-details-popover.ts
|
|
1173
1221
|
function useDetailsPopover() {
|
|
1174
1222
|
const [open, setOpen] = useState(false);
|
|
@@ -1196,11 +1244,11 @@ function useDetailsPopover() {
|
|
|
1196
1244
|
summaryRef.current?.focus();
|
|
1197
1245
|
};
|
|
1198
1246
|
document.addEventListener("pointerdown", dismissOutside);
|
|
1199
|
-
|
|
1247
|
+
const stopListeningToKeys = listenToKeydown(detailsRef.current, dismissWithEscape, true);
|
|
1200
1248
|
window.addEventListener("resize", reposition);
|
|
1201
1249
|
return () => {
|
|
1202
1250
|
document.removeEventListener("pointerdown", dismissOutside);
|
|
1203
|
-
|
|
1251
|
+
stopListeningToKeys();
|
|
1204
1252
|
window.removeEventListener("resize", reposition);
|
|
1205
1253
|
};
|
|
1206
1254
|
}, []);
|
|
@@ -2456,1711 +2504,1732 @@ function SelectionOutline({ node }) {
|
|
|
2456
2504
|
});
|
|
2457
2505
|
}
|
|
2458
2506
|
//#endregion
|
|
2459
|
-
//#region src/feedback/
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
left,
|
|
2475
|
-
right: window.innerWidth - left - width,
|
|
2476
|
-
top,
|
|
2477
|
-
width
|
|
2478
|
-
};
|
|
2507
|
+
//#region src/feedback/core/console-recorder.ts
|
|
2508
|
+
const DEFAULT_LIMIT = 30;
|
|
2509
|
+
const MAX_MESSAGE_LENGTH = 1e3;
|
|
2510
|
+
const RECORDED_METHODS = ["error", "warn"];
|
|
2511
|
+
function serializeArgument(value) {
|
|
2512
|
+
if (typeof value === "string") return value;
|
|
2513
|
+
if (value instanceof Error) return `${value.name}: ${value.message}`;
|
|
2514
|
+
if (value === null || value === void 0) return String(value);
|
|
2515
|
+
if (typeof value === "object") try {
|
|
2516
|
+
return JSON.stringify(value) ?? "[unserializable]";
|
|
2517
|
+
} catch {
|
|
2518
|
+
return "[unserializable]";
|
|
2519
|
+
}
|
|
2520
|
+
if (typeof value === "function") return "[function]";
|
|
2521
|
+
return String(value);
|
|
2479
2522
|
}
|
|
2480
|
-
function
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
window.visualViewport?.addEventListener("scroll", update);
|
|
2484
|
-
return () => {
|
|
2485
|
-
window.removeEventListener("resize", update);
|
|
2486
|
-
window.visualViewport?.removeEventListener("resize", update);
|
|
2487
|
-
window.visualViewport?.removeEventListener("scroll", update);
|
|
2488
|
-
};
|
|
2523
|
+
function formatMessage(args) {
|
|
2524
|
+
const message = args.map(serializeArgument).join(" ");
|
|
2525
|
+
return message.length > MAX_MESSAGE_LENGTH ? `${message.slice(0, 999)}…` : message;
|
|
2489
2526
|
}
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
},
|
|
2507
|
-
ArrowUp: {
|
|
2508
|
-
x: 0,
|
|
2509
|
-
y: -24
|
|
2510
|
-
}
|
|
2511
|
-
};
|
|
2512
|
-
function clampPosition(point, bounds, viewport = visibleViewport()) {
|
|
2513
|
-
return {
|
|
2514
|
-
x: Math.max(viewport.left + EDGE_GAP$1, Math.min(point.x, viewport.left + viewport.width - bounds.width - EDGE_GAP$1)),
|
|
2515
|
-
y: Math.max(viewport.top + EDGE_GAP$1, Math.min(point.y, viewport.top + viewport.height - bounds.height - EDGE_GAP$1))
|
|
2527
|
+
/**
|
|
2528
|
+
* Buffers the console noise a user saw right before reporting a bug.
|
|
2529
|
+
* Wrappers delegate to the previous implementation, so nothing is swallowed.
|
|
2530
|
+
*/
|
|
2531
|
+
function startConsoleRecorder(options) {
|
|
2532
|
+
const limit = options?.limit ?? DEFAULT_LIMIT;
|
|
2533
|
+
const buffer = [];
|
|
2534
|
+
let active = true;
|
|
2535
|
+
const push = (level, message) => {
|
|
2536
|
+
if (!(active && message)) return;
|
|
2537
|
+
buffer.push({
|
|
2538
|
+
level,
|
|
2539
|
+
message,
|
|
2540
|
+
timestamp: Date.now()
|
|
2541
|
+
});
|
|
2542
|
+
if (buffer.length > limit) buffer.splice(0, buffer.length - limit);
|
|
2516
2543
|
};
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
const
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
const horizontalEdge = corner.endsWith("right") ? "right" : "left";
|
|
2525
|
-
const verticalEdge = corner.startsWith("bottom") ? "bottom" : "top";
|
|
2526
|
-
if (positionedPanelOpen !== isPanelOpen) {
|
|
2527
|
-
setPositionedPanelOpen(isPanelOpen);
|
|
2528
|
-
setPosition(null);
|
|
2529
|
-
}
|
|
2530
|
-
const moveTo = useCallback((point, bounds) => {
|
|
2531
|
-
const fitted = clampPosition(point, bounds);
|
|
2532
|
-
const visible = visibleViewport();
|
|
2533
|
-
const anchor = {
|
|
2534
|
-
x: horizontalEdge === "right" ? visible.left + visible.width - fitted.x - bounds.width : fitted.x - visible.left,
|
|
2535
|
-
y: verticalEdge === "bottom" ? visible.top + visible.height - fitted.y - bounds.height : fitted.y - visible.top
|
|
2536
|
-
};
|
|
2537
|
-
setPosition((current) => current?.x === anchor.x && current.y === anchor.y ? current : anchor);
|
|
2538
|
-
}, [horizontalEdge, verticalEdge]);
|
|
2539
|
-
const keepInViewport = useCallback(() => {
|
|
2540
|
-
const bounds = rootRef.current?.getBoundingClientRect();
|
|
2541
|
-
if (!bounds) return;
|
|
2542
|
-
const fitted = clampPosition(bounds, bounds, viewport);
|
|
2543
|
-
if (Math.abs(fitted.x - bounds.x) > .5 || Math.abs(fitted.y - bounds.y) > .5) moveTo(fitted, bounds);
|
|
2544
|
-
}, [moveTo, viewport]);
|
|
2545
|
-
useLayoutEffect(() => {
|
|
2546
|
-
keepInViewport();
|
|
2547
|
-
}, [keepInViewport]);
|
|
2548
|
-
useEffect(() => {
|
|
2549
|
-
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(keepInViewport);
|
|
2550
|
-
if (rootRef.current) observer?.observe(rootRef.current);
|
|
2551
|
-
const stopListening = onViewportChange(() => setViewport(visibleViewport()));
|
|
2552
|
-
return () => {
|
|
2553
|
-
observer?.disconnect();
|
|
2554
|
-
stopListening();
|
|
2555
|
-
};
|
|
2556
|
-
}, [keepInViewport]);
|
|
2557
|
-
const onPointerDown = (event) => {
|
|
2558
|
-
const bounds = rootRef.current?.getBoundingClientRect();
|
|
2559
|
-
if (!bounds || event.button !== 0) return;
|
|
2560
|
-
event.currentTarget.setPointerCapture(event.pointerId);
|
|
2561
|
-
gesture.current = {
|
|
2562
|
-
x: event.clientX - bounds.x,
|
|
2563
|
-
y: event.clientY - bounds.y
|
|
2544
|
+
const originals = /* @__PURE__ */ new Map();
|
|
2545
|
+
const wrappers = /* @__PURE__ */ new Map();
|
|
2546
|
+
for (const method of RECORDED_METHODS) {
|
|
2547
|
+
const original = console[method];
|
|
2548
|
+
const wrapper = (...args) => {
|
|
2549
|
+
push(method, formatMessage(args));
|
|
2550
|
+
Reflect.apply(original, console, args);
|
|
2564
2551
|
};
|
|
2552
|
+
originals.set(method, original);
|
|
2553
|
+
wrappers.set(method, wrapper);
|
|
2554
|
+
console[method] = wrapper;
|
|
2555
|
+
}
|
|
2556
|
+
const onError = (event) => {
|
|
2557
|
+
const where = event.filename ? ` (${event.filename}:${event.lineno}:${event.colno})` : "";
|
|
2558
|
+
push("error", `${event.message}${where}`);
|
|
2565
2559
|
};
|
|
2566
|
-
const
|
|
2567
|
-
|
|
2568
|
-
const grab = gesture.current;
|
|
2569
|
-
if (!(bounds && grab && event.currentTarget.hasPointerCapture(event.pointerId))) return;
|
|
2570
|
-
moveTo({
|
|
2571
|
-
x: event.clientX - grab.x,
|
|
2572
|
-
y: event.clientY - grab.y
|
|
2573
|
-
}, bounds);
|
|
2574
|
-
};
|
|
2575
|
-
const onKeyDown = (event) => {
|
|
2576
|
-
const direction = KEY_DIRECTIONS[event.key];
|
|
2577
|
-
const bounds = rootRef.current?.getBoundingClientRect();
|
|
2578
|
-
if (!(bounds && direction)) return;
|
|
2579
|
-
event.preventDefault();
|
|
2580
|
-
moveTo({
|
|
2581
|
-
x: bounds.x + direction.x,
|
|
2582
|
-
y: bounds.y + direction.y
|
|
2583
|
-
}, bounds);
|
|
2560
|
+
const onRejection = (event) => {
|
|
2561
|
+
push("error", `Unhandled rejection: ${serializeArgument(event.reason)}`);
|
|
2584
2562
|
};
|
|
2585
|
-
|
|
2586
|
-
|
|
2563
|
+
window.addEventListener("error", onError);
|
|
2564
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
2587
2565
|
return {
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
onPointerDown,
|
|
2597
|
-
onPointerMove,
|
|
2598
|
-
onPointerUp: () => {
|
|
2599
|
-
gesture.current = null;
|
|
2566
|
+
events: () => buffer.slice(),
|
|
2567
|
+
stop: () => {
|
|
2568
|
+
if (!active) return;
|
|
2569
|
+
active = false;
|
|
2570
|
+
buffer.length = 0;
|
|
2571
|
+
for (const method of RECORDED_METHODS) {
|
|
2572
|
+
const original = originals.get(method);
|
|
2573
|
+
if (original && console[method] === wrappers.get(method)) console[method] = original;
|
|
2600
2574
|
}
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
styles: viewportStyles + anchorStyles
|
|
2575
|
+
window.removeEventListener("error", onError);
|
|
2576
|
+
window.removeEventListener("unhandledrejection", onRejection);
|
|
2577
|
+
}
|
|
2605
2578
|
};
|
|
2606
2579
|
}
|
|
2607
2580
|
//#endregion
|
|
2608
|
-
//#region src/feedback/
|
|
2609
|
-
function
|
|
2610
|
-
const
|
|
2611
|
-
|
|
2612
|
-
const showPanel = state.isOpen && !minimized;
|
|
2613
|
-
const floating = useFloatingPosition(position, showPanel);
|
|
2614
|
-
const minimize = () => {
|
|
2615
|
-
setMinimized(true);
|
|
2616
|
-
requestAnimationFrame(() => launcherRef.current?.focus());
|
|
2617
|
-
};
|
|
2618
|
-
useEffect(() => {
|
|
2619
|
-
if (state.step === "compose") state.annotationTrigger?.button.focus({ preventScroll: true });
|
|
2620
|
-
}, [state.step, state.annotationTrigger]);
|
|
2621
|
-
useEffect(() => {
|
|
2622
|
-
if (!showPanel || state.step !== "compose") return;
|
|
2623
|
-
const onKeyDown = (event) => {
|
|
2624
|
-
if (event.key === "Escape" && !event.defaultPrevented) {
|
|
2625
|
-
setMinimized(true);
|
|
2626
|
-
requestAnimationFrame(() => launcherRef.current?.focus());
|
|
2627
|
-
}
|
|
2628
|
-
};
|
|
2629
|
-
document.addEventListener("keydown", onKeyDown);
|
|
2630
|
-
return () => document.removeEventListener("keydown", onKeyDown);
|
|
2631
|
-
}, [showPanel, state.step]);
|
|
2632
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
2633
|
-
className: "root",
|
|
2634
|
-
"data-editing": state.isOpen && (state.step === "picking" || state.step === "annotate"),
|
|
2635
|
-
"data-moved": Boolean(floating.position),
|
|
2636
|
-
"data-position": position,
|
|
2637
|
-
ref: floating.rootRef,
|
|
2638
|
-
children: [
|
|
2639
|
-
/* @__PURE__ */ jsx("style", { children: floating.styles }),
|
|
2640
|
-
showPanel && state.step === "compose" && state.screenshots.map(({ id, selectedNode }) => selectedNode ? /* @__PURE__ */ jsx(SelectionOutline, { node: selectedNode }, id) : null),
|
|
2641
|
-
showPanel ? /* @__PURE__ */ jsx(FeedbackPanel, {
|
|
2642
|
-
floatingControls: /* @__PURE__ */ jsxs("div", {
|
|
2643
|
-
className: "floating-controls",
|
|
2644
|
-
children: [/* @__PURE__ */ jsx("button", {
|
|
2645
|
-
"aria-label": labels.moveFeedback,
|
|
2646
|
-
className: "icon-btn drag-handle",
|
|
2647
|
-
title: labels.moveFeedback,
|
|
2648
|
-
type: "button",
|
|
2649
|
-
...floating.handleProps,
|
|
2650
|
-
children: /* @__PURE__ */ jsx(GripIcon, {})
|
|
2651
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
2652
|
-
"aria-label": labels.minimize,
|
|
2653
|
-
className: "icon-btn",
|
|
2654
|
-
onClick: minimize,
|
|
2655
|
-
title: labels.minimize,
|
|
2656
|
-
type: "button",
|
|
2657
|
-
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
2658
|
-
})]
|
|
2659
|
-
}),
|
|
2660
|
-
labels,
|
|
2661
|
-
state
|
|
2662
|
-
}) : /* @__PURE__ */ jsx(Launcher, {
|
|
2663
|
-
buttonRef: launcherRef,
|
|
2664
|
-
isOpen: false,
|
|
2665
|
-
label: state.isOpen ? labels.resume : labels.trigger,
|
|
2666
|
-
onClick: () => {
|
|
2667
|
-
setMinimized(false);
|
|
2668
|
-
if (!state.isOpen) state.open();
|
|
2669
|
-
}
|
|
2670
|
-
})
|
|
2671
|
-
]
|
|
2672
|
-
});
|
|
2673
|
-
}
|
|
2674
|
-
//#endregion
|
|
2675
|
-
//#region src/feedback/ui/floating/element-walk.ts
|
|
2676
|
-
const ARROW_SIBLING = {
|
|
2677
|
-
ArrowLeft: "previousElementSibling",
|
|
2678
|
-
ArrowRight: "nextElementSibling"
|
|
2679
|
-
};
|
|
2680
|
-
const AIM_KEYS = [
|
|
2681
|
-
"Tab",
|
|
2682
|
-
"ArrowUp",
|
|
2683
|
-
"ArrowDown",
|
|
2684
|
-
"ArrowLeft",
|
|
2685
|
-
"ArrowRight"
|
|
2686
|
-
];
|
|
2687
|
-
function isWidgetOwned(element) {
|
|
2688
|
-
return element.closest(`[${WIDGET_MARKER}]`) !== null;
|
|
2689
|
-
}
|
|
2690
|
-
function isPickable(element) {
|
|
2691
|
-
if (!element || element === document.documentElement || isWidgetOwned(element)) return false;
|
|
2692
|
-
const rect = element.getBoundingClientRect();
|
|
2693
|
-
return rect.width > 0 && rect.height > 0;
|
|
2581
|
+
//#region src/feedback/core/submission/upload-screenshots.ts
|
|
2582
|
+
async function uploadImage(transport, image) {
|
|
2583
|
+
const { uploadUrl } = await transport.getScreenshotUploadUrl();
|
|
2584
|
+
return transport.uploadImage(uploadUrl, image);
|
|
2694
2585
|
}
|
|
2695
|
-
function
|
|
2696
|
-
|
|
2586
|
+
async function prepareScreenshot(transport, screenshot, index) {
|
|
2587
|
+
const { closeUp, image, annotations, context } = screenshot;
|
|
2588
|
+
const annotated = annotations.length > 0 ? await renderAnnotatedImage(image, annotations) : null;
|
|
2589
|
+
if (annotations.length > 0 && !annotated) throw new Error("Could not render your annotations. Please try again.");
|
|
2590
|
+
try {
|
|
2591
|
+
const storageId = await uploadImage(transport, image);
|
|
2592
|
+
const prepared = [{
|
|
2593
|
+
annotatedStorageId: annotated ? await uploadImage(transport, annotated) : void 0,
|
|
2594
|
+
annotations: annotations.length > 0 ? toWireAnnotations(annotations) : void 0,
|
|
2595
|
+
captureSource: "widget",
|
|
2596
|
+
filename: `screenshot-${index + 1}.png`,
|
|
2597
|
+
height: image.height,
|
|
2598
|
+
mimeType: image.mimeType,
|
|
2599
|
+
pageUrl: context.url,
|
|
2600
|
+
size: image.blob.size,
|
|
2601
|
+
storageId,
|
|
2602
|
+
width: image.width
|
|
2603
|
+
}];
|
|
2604
|
+
if (closeUp) prepared.push({
|
|
2605
|
+
captureSource: "element",
|
|
2606
|
+
filename: `element-${index + 1}.png`,
|
|
2607
|
+
height: closeUp.height,
|
|
2608
|
+
mimeType: closeUp.mimeType,
|
|
2609
|
+
pageUrl: context.url,
|
|
2610
|
+
size: closeUp.blob.size,
|
|
2611
|
+
storageId: await uploadImage(transport, closeUp),
|
|
2612
|
+
width: closeUp.width
|
|
2613
|
+
});
|
|
2614
|
+
return prepared;
|
|
2615
|
+
} finally {
|
|
2616
|
+
releaseCapture(annotated);
|
|
2617
|
+
}
|
|
2697
2618
|
}
|
|
2698
|
-
function
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
return all[(index + (backwards ? -1 : 1) + all.length) % all.length] ?? null;
|
|
2619
|
+
async function uploadScreenshots(transport, submission) {
|
|
2620
|
+
return (await Promise.allSettled(submission.screenshots.map((screenshot, index) => prepareScreenshot(transport, screenshot, index)))).flatMap((result) => {
|
|
2621
|
+
if (result.status === "rejected") throw result.reason;
|
|
2622
|
+
return result.value;
|
|
2623
|
+
});
|
|
2704
2624
|
}
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2625
|
+
//#endregion
|
|
2626
|
+
//#region src/feedback/core/submit.ts
|
|
2627
|
+
const MAX_TITLE_LENGTH = 100;
|
|
2628
|
+
const NEWLINE = /\r?\n/;
|
|
2629
|
+
function deriveTitle(message) {
|
|
2630
|
+
const firstLine = message.split(NEWLINE).map((line) => line.trim()).find(Boolean) ?? "";
|
|
2631
|
+
if (firstLine.length <= MAX_TITLE_LENGTH) return firstLine;
|
|
2632
|
+
const clipped = firstLine.slice(0, 99);
|
|
2633
|
+
const lastSpace = clipped.lastIndexOf(" ");
|
|
2634
|
+
return `${(lastSpace > 0 ? clipped.slice(0, lastSpace) : clipped).trimEnd()}…`;
|
|
2709
2635
|
}
|
|
2710
|
-
function
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
return
|
|
2636
|
+
function buildDescription(params) {
|
|
2637
|
+
const { email, isAnonymous, message } = params;
|
|
2638
|
+
const contact = isAnonymous && email ? `\n\n---\nContact: ${email}` : "";
|
|
2639
|
+
return `${message.trim()}${contact}`;
|
|
2714
2640
|
}
|
|
2715
|
-
function
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2641
|
+
async function attachPreparedScreenshots(transport, result) {
|
|
2642
|
+
const results = await Promise.allSettled(result.pendingScreenshots.map((screenshot) => transport.saveScreenshot({
|
|
2643
|
+
...screenshot,
|
|
2644
|
+
feedbackId: result.feedbackId
|
|
2645
|
+
})));
|
|
2646
|
+
const pendingScreenshots = result.pendingScreenshots.filter((_, index) => results[index]?.status === "rejected");
|
|
2647
|
+
return {
|
|
2648
|
+
feedbackId: result.feedbackId,
|
|
2649
|
+
pendingScreenshots
|
|
2650
|
+
};
|
|
2719
2651
|
}
|
|
2720
|
-
function
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
return
|
|
2652
|
+
async function createReportWithScreenshots(transport, submission, pendingScreenshots) {
|
|
2653
|
+
const { feedbackId } = await transport.create({
|
|
2654
|
+
context: submission.context,
|
|
2655
|
+
description: buildDescription(submission),
|
|
2656
|
+
title: deriveTitle(submission.message)
|
|
2657
|
+
});
|
|
2658
|
+
return attachPreparedScreenshots(transport, {
|
|
2659
|
+
feedbackId,
|
|
2660
|
+
pendingScreenshots
|
|
2661
|
+
});
|
|
2727
2662
|
}
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
const LABEL_GAP = 6;
|
|
2732
|
-
const NOTE_WIDTH = 300;
|
|
2733
|
-
const NOTE_GAP = 10;
|
|
2734
|
-
const EDGE_GAP = 12;
|
|
2735
|
-
/** Events raised by the picker's own note card must reach it, never be swallowed. */
|
|
2736
|
-
function targetsWidget(event) {
|
|
2737
|
-
return event.composedPath().some((node) => node instanceof Element && isWidgetOwned(node));
|
|
2663
|
+
async function submitWidgetFeedback(transport, submission) {
|
|
2664
|
+
if (!deriveTitle(submission.message)) throw new Error("Please describe your feedback before sending it.");
|
|
2665
|
+
return createReportWithScreenshots(transport, submission, await uploadScreenshots(transport, submission));
|
|
2738
2666
|
}
|
|
2739
|
-
function
|
|
2740
|
-
|
|
2741
|
-
if (!element || isWidgetOwned(element)) return null;
|
|
2742
|
-
return element;
|
|
2667
|
+
function isStorageResponse(value) {
|
|
2668
|
+
return typeof value === "object" && value !== null && typeof Reflect.get(value, "storageId") === "string";
|
|
2743
2669
|
}
|
|
2744
|
-
function
|
|
2670
|
+
function createFeedbackTransport(client) {
|
|
2745
2671
|
return {
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2672
|
+
create: (params) => client.create(params),
|
|
2673
|
+
getScreenshotUploadUrl: () => client.getScreenshotUploadUrl(),
|
|
2674
|
+
saveScreenshot: (params) => client.saveScreenshot(params),
|
|
2675
|
+
uploadImage: async (uploadUrl, image) => {
|
|
2676
|
+
const response = await fetch(uploadUrl, {
|
|
2677
|
+
body: image.blob,
|
|
2678
|
+
headers: { "Content-Type": image.mimeType },
|
|
2679
|
+
method: "POST"
|
|
2680
|
+
});
|
|
2681
|
+
if (!response.ok) throw new Error(`Screenshot upload failed (${response.status})`);
|
|
2682
|
+
const payload = await response.json();
|
|
2683
|
+
if (!isStorageResponse(payload)) throw new Error("Screenshot upload returned no storage id");
|
|
2684
|
+
return payload.storageId;
|
|
2685
|
+
}
|
|
2751
2686
|
};
|
|
2752
2687
|
}
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
const
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2688
|
+
//#endregion
|
|
2689
|
+
//#region src/feedback/ui/state/use-feedback-submission.ts
|
|
2690
|
+
function useFeedbackSubmission(options) {
|
|
2691
|
+
const [activeRequest, setActiveRequest] = useState(null);
|
|
2692
|
+
const [pendingReport, setPendingReport] = useState(null);
|
|
2693
|
+
const requestRef = useRef(null);
|
|
2694
|
+
useEffect(() => () => {
|
|
2695
|
+
requestRef.current = null;
|
|
2696
|
+
}, []);
|
|
2697
|
+
const resetSubmission = useCallback(() => {
|
|
2698
|
+
requestRef.current = null;
|
|
2699
|
+
setPendingReport(null);
|
|
2700
|
+
setActiveRequest(null);
|
|
2701
|
+
}, []);
|
|
2702
|
+
const finishSubmission = (result) => {
|
|
2703
|
+
if (result.pendingScreenshots.length > 0) {
|
|
2704
|
+
setPendingReport(result);
|
|
2705
|
+
options.onError(options.partialUploadMessage);
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
setPendingReport(null);
|
|
2709
|
+
options.onSubmitted(result.feedbackId);
|
|
2710
|
+
};
|
|
2711
|
+
const submit = async (draft) => {
|
|
2712
|
+
if (requestRef.current) return;
|
|
2713
|
+
if (!options.client) {
|
|
2714
|
+
options.onError("Reflet is missing a publicKey. Pass one to RefletFeedback or RefletProvider.");
|
|
2715
|
+
return;
|
|
2716
|
+
}
|
|
2717
|
+
const request = {};
|
|
2718
|
+
requestRef.current = request;
|
|
2719
|
+
setActiveRequest(request);
|
|
2720
|
+
options.onError(null);
|
|
2721
|
+
try {
|
|
2722
|
+
const transport = createFeedbackTransport(options.client);
|
|
2723
|
+
const result = pendingReport ? await attachPreparedScreenshots(transport, pendingReport) : await submitWidgetFeedback(transport, draft);
|
|
2724
|
+
if (requestRef.current === request) finishSubmission(result);
|
|
2725
|
+
} catch (error) {
|
|
2726
|
+
if (requestRef.current === request) options.onError(error instanceof Error ? error.message : "Something went wrong. Please try again.");
|
|
2727
|
+
} finally {
|
|
2728
|
+
if (requestRef.current === request) {
|
|
2729
|
+
requestRef.current = null;
|
|
2730
|
+
setActiveRequest(null);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2766
2733
|
};
|
|
2767
2734
|
return {
|
|
2768
|
-
|
|
2769
|
-
|
|
2735
|
+
hasPendingAttachments: pendingReport !== null,
|
|
2736
|
+
isEditingDisabled: activeRequest !== null || pendingReport !== null,
|
|
2737
|
+
isSubmitting: activeRequest !== null,
|
|
2738
|
+
resetSubmission,
|
|
2739
|
+
submit
|
|
2770
2740
|
};
|
|
2771
2741
|
}
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
const [
|
|
2781
|
-
const [
|
|
2782
|
-
const
|
|
2783
|
-
const
|
|
2784
|
-
const pinnedRef = useRef(false);
|
|
2785
|
-
const [view, setView] = useState(visibleViewport);
|
|
2786
|
-
const aimRef = useRef(null);
|
|
2787
|
-
useEffect(() => onViewportChange(() => setView(visibleViewport())), []);
|
|
2788
|
-
useEffect(() => {
|
|
2789
|
-
pinnedRef.current = pinned !== null;
|
|
2790
|
-
if (pinned) noteRef.current?.focus({ preventScroll: true });
|
|
2791
|
-
}, [pinned]);
|
|
2792
|
-
useEffect(() => {
|
|
2793
|
-
aimRef.current = target?.element ?? null;
|
|
2794
|
-
}, [target]);
|
|
2742
|
+
//#endregion
|
|
2743
|
+
//#region src/feedback/ui/state/use-screenshot-drafts.ts
|
|
2744
|
+
function releaseDraft(draft) {
|
|
2745
|
+
releaseCapture(draft?.image);
|
|
2746
|
+
releaseCapture(draft?.closeUp);
|
|
2747
|
+
}
|
|
2748
|
+
function useScreenshotDrafts(captureFailed, onError) {
|
|
2749
|
+
const [screenshots, setScreenshots] = useState([]);
|
|
2750
|
+
const [activeId, setActiveId] = useState(null);
|
|
2751
|
+
const [pendingCapture, setPendingCapture] = useState(null);
|
|
2752
|
+
const ownedDrafts = useRef(/* @__PURE__ */ new Map());
|
|
2753
|
+
const captureRequest = useRef(null);
|
|
2795
2754
|
useEffect(() => {
|
|
2796
|
-
const
|
|
2797
|
-
if (targetsWidget(event)) return;
|
|
2798
|
-
event.preventDefault();
|
|
2799
|
-
event.stopPropagation();
|
|
2800
|
-
};
|
|
2801
|
-
const aimAt = (x, y) => {
|
|
2802
|
-
const element = elementUnder(x, y);
|
|
2803
|
-
if (!element) return;
|
|
2804
|
-
setTarget((current) => current?.element === element ? {
|
|
2805
|
-
...current,
|
|
2806
|
-
rect: element.getBoundingClientRect()
|
|
2807
|
-
} : describeTarget(element));
|
|
2808
|
-
};
|
|
2809
|
-
const onMove = (event) => {
|
|
2810
|
-
if (pinnedRef.current || targetsWidget(event)) return;
|
|
2811
|
-
aimAt(event.clientX, event.clientY);
|
|
2812
|
-
};
|
|
2813
|
-
const onPointerDown = (event) => {
|
|
2814
|
-
if (targetsWidget(event)) return;
|
|
2815
|
-
swallow(event);
|
|
2816
|
-
if (!pinnedRef.current) aimAt(event.clientX, event.clientY);
|
|
2817
|
-
};
|
|
2818
|
-
const onPointerUp = (event) => {
|
|
2819
|
-
if (targetsWidget(event)) return;
|
|
2820
|
-
swallow(event);
|
|
2821
|
-
if (pinnedRef.current) return;
|
|
2822
|
-
const element = elementUnder(event.clientX, event.clientY);
|
|
2823
|
-
if (element) setPinned(describeTarget(element));
|
|
2824
|
-
};
|
|
2825
|
-
const moveAim = (key, backwards) => {
|
|
2826
|
-
const next = stepAim(aimRef.current, key, backwards);
|
|
2827
|
-
if (!next) return;
|
|
2828
|
-
next.scrollIntoView({
|
|
2829
|
-
block: "nearest",
|
|
2830
|
-
inline: "nearest"
|
|
2831
|
-
});
|
|
2832
|
-
aimRef.current = next;
|
|
2833
|
-
setTarget(describeTarget(next));
|
|
2834
|
-
};
|
|
2835
|
-
const onEscape = () => {
|
|
2836
|
-
if (pinnedRef.current) {
|
|
2837
|
-
setPinned(null);
|
|
2838
|
-
setNote("");
|
|
2839
|
-
return;
|
|
2840
|
-
}
|
|
2841
|
-
onCancel();
|
|
2842
|
-
};
|
|
2843
|
-
const onKeyDown = (event) => {
|
|
2844
|
-
if (event.key === "Escape") {
|
|
2845
|
-
swallow(event);
|
|
2846
|
-
onEscape();
|
|
2847
|
-
return;
|
|
2848
|
-
}
|
|
2849
|
-
if (pinnedRef.current || targetsWidget(event)) return;
|
|
2850
|
-
if (event.key === "Enter") {
|
|
2851
|
-
swallow(event);
|
|
2852
|
-
if (aimRef.current) setPinned(describeTarget(aimRef.current));
|
|
2853
|
-
return;
|
|
2854
|
-
}
|
|
2855
|
-
if (!AIM_KEYS.includes(event.key)) return;
|
|
2856
|
-
swallow(event);
|
|
2857
|
-
moveAim(event.key, event.shiftKey);
|
|
2858
|
-
};
|
|
2859
|
-
const onScroll = () => {
|
|
2860
|
-
setTarget((current) => current ? {
|
|
2861
|
-
...current,
|
|
2862
|
-
rect: current.element.getBoundingClientRect()
|
|
2863
|
-
} : current);
|
|
2864
|
-
setPinned((current) => current ? {
|
|
2865
|
-
...current,
|
|
2866
|
-
rect: current.element.getBoundingClientRect()
|
|
2867
|
-
} : current);
|
|
2868
|
-
};
|
|
2869
|
-
document.addEventListener("pointermove", onMove, true);
|
|
2870
|
-
document.addEventListener("pointerdown", onPointerDown, true);
|
|
2871
|
-
document.addEventListener("pointerup", onPointerUp, true);
|
|
2872
|
-
document.addEventListener("mousedown", swallow, true);
|
|
2873
|
-
document.addEventListener("mouseup", swallow, true);
|
|
2874
|
-
document.addEventListener("click", swallow, true);
|
|
2875
|
-
document.addEventListener("keydown", onKeyDown, true);
|
|
2876
|
-
window.addEventListener("scroll", onScroll, {
|
|
2877
|
-
capture: true,
|
|
2878
|
-
passive: true
|
|
2879
|
-
});
|
|
2880
|
-
const previousCursor = document.body.style.cursor;
|
|
2881
|
-
document.body.style.cursor = "crosshair";
|
|
2755
|
+
const drafts = ownedDrafts.current;
|
|
2882
2756
|
return () => {
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
document.removeEventListener("mousedown", swallow, true);
|
|
2887
|
-
document.removeEventListener("mouseup", swallow, true);
|
|
2888
|
-
document.removeEventListener("click", swallow, true);
|
|
2889
|
-
document.removeEventListener("keydown", onKeyDown, true);
|
|
2890
|
-
window.removeEventListener("scroll", onScroll, true);
|
|
2891
|
-
document.body.style.cursor = previousCursor;
|
|
2757
|
+
captureRequest.current = null;
|
|
2758
|
+
for (const draft of drafts.values()) releaseDraft(draft);
|
|
2759
|
+
drafts.clear();
|
|
2892
2760
|
};
|
|
2893
|
-
}, [
|
|
2894
|
-
const
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2761
|
+
}, []);
|
|
2762
|
+
const storeScreenshot = useCallback((screenshot) => {
|
|
2763
|
+
releaseDraft(ownedDrafts.current.get(screenshot.id));
|
|
2764
|
+
ownedDrafts.current.set(screenshot.id, screenshot);
|
|
2765
|
+
setScreenshots((previous) => {
|
|
2766
|
+
return previous.some(({ id }) => id === screenshot.id) ? previous.map((item) => item.id === screenshot.id ? screenshot : item) : [...previous, screenshot];
|
|
2767
|
+
});
|
|
2768
|
+
setActiveId(screenshot.id);
|
|
2769
|
+
}, []);
|
|
2770
|
+
const cancelPendingCapture = useCallback(() => {
|
|
2771
|
+
captureRequest.current = null;
|
|
2772
|
+
setPendingCapture(null);
|
|
2773
|
+
}, []);
|
|
2774
|
+
const takeScreenshot = useCallback(async (request) => {
|
|
2775
|
+
captureRequest.current = request;
|
|
2776
|
+
setPendingCapture(request);
|
|
2777
|
+
onError(null);
|
|
2778
|
+
const context = collectPageContext({ sdkVersion: SDK_VERSION });
|
|
2779
|
+
const image = await captureViewport().catch(() => null);
|
|
2780
|
+
if (captureRequest.current !== request) {
|
|
2781
|
+
releaseCapture(image);
|
|
2782
|
+
return;
|
|
2783
|
+
}
|
|
2784
|
+
cancelPendingCapture();
|
|
2785
|
+
if (!image) {
|
|
2786
|
+
onError(captureFailed);
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
storeScreenshot({
|
|
2790
|
+
...request,
|
|
2791
|
+
annotations: [],
|
|
2792
|
+
context,
|
|
2793
|
+
image
|
|
2794
|
+
});
|
|
2795
|
+
}, [
|
|
2796
|
+
cancelPendingCapture,
|
|
2797
|
+
captureFailed,
|
|
2798
|
+
onError,
|
|
2799
|
+
storeScreenshot
|
|
2800
|
+
]);
|
|
2801
|
+
const takeCapture = useCallback(() => {
|
|
2802
|
+
if (!captureRequest.current) takeScreenshot({
|
|
2803
|
+
id: crypto.randomUUID(),
|
|
2804
|
+
source: "manual"
|
|
2805
|
+
});
|
|
2806
|
+
}, [takeScreenshot]);
|
|
2807
|
+
const refreshAutomatic = useCallback(() => {
|
|
2808
|
+
takeScreenshot({
|
|
2809
|
+
id: ownedDrafts.current.keys().next().value ?? captureRequest.current?.id ?? crypto.randomUUID(),
|
|
2810
|
+
source: "automatic"
|
|
2811
|
+
});
|
|
2812
|
+
}, [takeScreenshot]);
|
|
2813
|
+
const retakeCapture = useCallback((id) => {
|
|
2814
|
+
if (!captureRequest.current && ownedDrafts.current.has(id)) takeScreenshot({
|
|
2815
|
+
id,
|
|
2816
|
+
source: "manual"
|
|
2817
|
+
});
|
|
2818
|
+
}, [takeScreenshot]);
|
|
2819
|
+
const removeCapture = useCallback((id) => {
|
|
2820
|
+
if (captureRequest.current?.id === id) cancelPendingCapture();
|
|
2821
|
+
releaseDraft(ownedDrafts.current.get(id));
|
|
2822
|
+
ownedDrafts.current.delete(id);
|
|
2823
|
+
setScreenshots((previous) => previous.filter((item) => item.id !== id));
|
|
2824
|
+
setActiveId((current) => current === id ? null : current);
|
|
2825
|
+
}, [cancelPendingCapture]);
|
|
2826
|
+
const resetScreenshots = useCallback(() => {
|
|
2827
|
+
cancelPendingCapture();
|
|
2828
|
+
for (const draft of ownedDrafts.current.values()) releaseDraft(draft);
|
|
2829
|
+
ownedDrafts.current.clear();
|
|
2830
|
+
setScreenshots([]);
|
|
2831
|
+
setActiveId(null);
|
|
2832
|
+
}, [cancelPendingCapture]);
|
|
2833
|
+
const setAnnotations = useCallback((annotations) => {
|
|
2834
|
+
setScreenshots((previous) => previous.map((item) => item.id === activeId ? {
|
|
2835
|
+
...item,
|
|
2836
|
+
annotations
|
|
2837
|
+
} : item));
|
|
2838
|
+
}, [activeId]);
|
|
2839
|
+
const setSelectionComment = useCallback((id, comment) => {
|
|
2840
|
+
setScreenshots((previous) => previous.map((item) => item.id === id && item.selection ? {
|
|
2841
|
+
...item,
|
|
2842
|
+
selection: {
|
|
2843
|
+
...item.selection,
|
|
2844
|
+
comment
|
|
2904
2845
|
}
|
|
2905
|
-
})
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
}),
|
|
2923
|
-
/* @__PURE__ */ jsx("textarea", {
|
|
2924
|
-
"aria-label": labels.elementNote,
|
|
2925
|
-
maxLength: MAX_SELECTION_COMMENT_LENGTH,
|
|
2926
|
-
onChange: (event) => setNote(event.target.value),
|
|
2927
|
-
onKeyDown: (event) => {
|
|
2928
|
-
if (event.key === "Enter" && !event.shiftKey) {
|
|
2929
|
-
event.preventDefault();
|
|
2930
|
-
onPick(pinned.element, note.trim());
|
|
2931
|
-
}
|
|
2932
|
-
},
|
|
2933
|
-
placeholder: labels.elementNotePlaceholder,
|
|
2934
|
-
ref: noteRef,
|
|
2935
|
-
rows: 2,
|
|
2936
|
-
value: note
|
|
2937
|
-
}),
|
|
2938
|
-
/* @__PURE__ */ jsxs("div", {
|
|
2939
|
-
className: "picker-note-actions",
|
|
2940
|
-
children: [/* @__PURE__ */ jsx("button", {
|
|
2941
|
-
className: "picker-cancel",
|
|
2942
|
-
onClick: () => {
|
|
2943
|
-
setPinned(null);
|
|
2944
|
-
setNote("");
|
|
2945
|
-
},
|
|
2946
|
-
type: "button",
|
|
2947
|
-
children: labels.pickAnother
|
|
2948
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
2949
|
-
"aria-label": labels.attachElement,
|
|
2950
|
-
className: "submit",
|
|
2951
|
-
title: labels.attachElement,
|
|
2952
|
-
type: "submit",
|
|
2953
|
-
children: /* @__PURE__ */ jsx(ArrowIcon, {})
|
|
2954
|
-
})]
|
|
2955
|
-
})
|
|
2956
|
-
]
|
|
2957
|
-
}) : /* @__PURE__ */ jsxs("div", {
|
|
2958
|
-
className: "picker-hint",
|
|
2959
|
-
children: [/* @__PURE__ */ jsxs("span", {
|
|
2960
|
-
className: "picker-instruction",
|
|
2961
|
-
children: [
|
|
2962
|
-
labels.pickElementHint,
|
|
2963
|
-
" ",
|
|
2964
|
-
/* @__PURE__ */ jsx("kbd", { children: "Tab" }),
|
|
2965
|
-
/* @__PURE__ */ jsx("kbd", { children: "Enter" }),
|
|
2966
|
-
/* @__PURE__ */ jsx("kbd", { children: "Esc" })
|
|
2967
|
-
]
|
|
2968
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
2969
|
-
className: "picker-cancel",
|
|
2970
|
-
onClick: onCancel,
|
|
2971
|
-
type: "button",
|
|
2972
|
-
children: labels.cancel
|
|
2973
|
-
})]
|
|
2974
|
-
})
|
|
2975
|
-
] });
|
|
2846
|
+
} : item));
|
|
2847
|
+
}, []);
|
|
2848
|
+
return {
|
|
2849
|
+
activeScreenshot: screenshots.find(({ id }) => id === activeId) ?? screenshots.at(-1) ?? null,
|
|
2850
|
+
cancelPendingCapture,
|
|
2851
|
+
pendingCapture,
|
|
2852
|
+
refreshAutomatic,
|
|
2853
|
+
removeCapture,
|
|
2854
|
+
resetScreenshots,
|
|
2855
|
+
retakeCapture,
|
|
2856
|
+
screenshots,
|
|
2857
|
+
selectScreenshot: setActiveId,
|
|
2858
|
+
setAnnotations,
|
|
2859
|
+
setSelectionComment,
|
|
2860
|
+
storeScreenshot,
|
|
2861
|
+
takeCapture
|
|
2862
|
+
};
|
|
2976
2863
|
}
|
|
2977
2864
|
//#endregion
|
|
2978
|
-
//#region src/feedback/ui/
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
.overlay::before { content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none; background: color-mix(in srgb, var(--rf-bg) 94%, transparent); animation: rf-editor-in 260ms var(--rf-ease); transition: opacity 260ms var(--rf-ease); }
|
|
2982
|
-
.overlay::backdrop { background: transparent; }
|
|
2983
|
-
@keyframes rf-editor-in { from { opacity: 0; } }
|
|
2984
|
-
.editor { flex: 1; position: relative; display: flex; align-items: center; justify-content: center; padding: 20px; min-height: 0; container-type: size; }
|
|
2985
|
-
.editor-image { width: min(100cqw, calc(100cqh * var(--rf-capture-ratio))); aspect-ratio: var(--rf-capture-ratio); flex: none; position: relative; }
|
|
2986
|
-
.editor-visual { position: absolute; inset: 0; border-radius: var(--rf-radius); box-shadow: var(--rf-shadow); transform-origin: top left; overflow: hidden; }
|
|
2987
|
-
.editor-visual img, .editor-visual canvas { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: inherit; object-fit: cover; object-position: top center; outline: 1px solid var(--rf-hairline); outline-offset: -1px; }
|
|
2988
|
-
.editor-visual canvas { cursor: crosshair; touch-action: none; }
|
|
2989
|
-
.overlay[data-opening="true"] .editor { pointer-events: none; }
|
|
2990
|
-
.overlay[data-closing="true"] { pointer-events: none; }
|
|
2991
|
-
.overlay[data-closing="true"]::before { opacity: 0; }
|
|
2992
|
-
.overlay[data-closing="true"] .toolbar, .overlay[data-closing="true"] .element-note { opacity: 0; transform: translateY(8px); }
|
|
2993
|
-
|
|
2994
|
-
.element-note { order: 1; display: flex; align-items: flex-start; gap: 10px; width: min(560px, calc(100vw - 32px)); margin: 0 auto 10px; padding: 10px 14px; border-radius: var(--rf-radius); transition: opacity 180ms var(--rf-ease), transform 180ms var(--rf-ease); }
|
|
2995
|
-
.element-note svg { flex: none; margin-top: 4px; opacity: 0.7; }
|
|
2996
|
-
.element-note textarea { flex: 1; min-width: 0; height: 42px; max-height: 96px; padding: 0; border: 0; background: transparent; color: inherit; font: inherit; resize: none; }
|
|
2997
|
-
.element-note textarea:focus { outline: none; }
|
|
2998
|
-
.element-note:focus-within { outline: 2px solid var(--rf-accent); outline-offset: 2px; }
|
|
2999
|
-
|
|
3000
|
-
.toolbar { order: 2; display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; transition: opacity 180ms var(--rf-ease), transform 180ms var(--rf-ease); padding: 4px 12px max(16px, env(safe-area-inset-bottom)); }
|
|
3001
|
-
.toolbar .group { display: flex; align-items: center; gap: 2px; padding: 4px; border-radius: var(--rf-pill); }
|
|
3002
|
-
.tool[aria-pressed="true"], .tool[data-selected="true"] { background: var(--rf-primary); color: var(--rf-primary-text); }
|
|
3003
|
-
.color-picker { position: relative; }
|
|
3004
|
-
.color-picker summary { display: flex; justify-content: center; align-items: center; }
|
|
3005
|
-
.color-preview { width: 18px; height: 18px; border-radius: 50%; background: var(--annotation-color); box-shadow: inset 0 0 0 1px var(--rf-border); }
|
|
3006
|
-
.color-popover { --rf-color-width: 200px; position: absolute; bottom: calc(100% + 14px); left: clamp(calc(12px - var(--rf-color-anchor)), 0px, calc(100vw - var(--rf-color-anchor) - var(--rf-color-width) - 12px)); display: flex; align-items: center; gap: 6px; width: var(--rf-color-width); padding: 4px 4px 4px 14px; border-radius: var(--rf-pill); animation: rf-in 180ms var(--rf-ease); }
|
|
3007
|
-
.color-picker[data-placement="below"] .color-popover { top: calc(100% + 14px); bottom: auto; }
|
|
3008
|
-
.color-slider { flex: 1; min-width: 0; }
|
|
3009
|
-
.color-slider-control { display: flex; align-items: center; height: 40px; width: 100%; touch-action: none; user-select: none; cursor: pointer; }
|
|
3010
|
-
.color-slider-track { height: 12px; width: 100%; border-radius: var(--rf-pill); background: linear-gradient(to right, var(--rf-swatch-base), var(--annotation-base-color)); box-shadow: inset 0 0 0 1px var(--rf-border); }
|
|
3011
|
-
.color-slider-thumb { width: 16px; height: 16px; border: 2px solid var(--rf-swatch-base); border-radius: 50%; background: var(--annotation-color); box-shadow: var(--rf-shadow-low); transition: scale 160ms var(--rf-ease); }
|
|
3012
|
-
.color-slider-thumb[data-dragging] { scale: 1.12; }
|
|
3013
|
-
.color-slider-thumb:has(:focus-visible) { outline: 2px solid var(--rf-accent); outline-offset: 3px; }
|
|
3014
|
-
.color-spectrum { position: relative; display: flex; align-items: center; justify-content: center; flex: none; width: 40px; height: 40px; border-radius: 50%; cursor: pointer; }
|
|
3015
|
-
.color-spectrum::before { content: ""; width: 16px; height: 16px; border-radius: 50%; background: var(--rf-spectrum); box-shadow: inset 0 0 0 1px var(--rf-glass-highlight); }
|
|
3016
|
-
.color-spectrum:hover { background: var(--rf-bg-hover); }
|
|
3017
|
-
.color-spectrum button { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: inherit; }
|
|
3018
|
-
.color-spectrum button:focus-visible { outline: 2px solid var(--rf-accent); outline-offset: 3px; }
|
|
3019
|
-
.color-spectrum input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; pointer-events: none; }
|
|
3020
|
-
.done-btn { display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; padding: 0; border-radius: var(--rf-pill); color: var(--rf-primary-text); background: var(--rf-primary); }
|
|
3021
|
-
.drawing-menu { position: relative; }
|
|
3022
|
-
.drawing-popover { position: absolute; z-index: 2; right: 0; bottom: calc(100% + 12px); width: 200px; max-height: calc(50dvh - 68px); overflow-y: auto; padding: 6px; border-radius: var(--rf-radius); }
|
|
3023
|
-
.drawing-menu[data-placement="below"] .drawing-popover { top: calc(100% + 12px); bottom: auto; }
|
|
3024
|
-
.drawing-option { display: flex; align-items: center; width: 100%; min-height: 40px; gap: 10px; padding: 0 10px; border-radius: 12px; text-align: left; }
|
|
3025
|
-
.drawing-option:hover, .drawing-option[aria-pressed="true"] { background: var(--rf-bg-hover); }
|
|
3026
|
-
.drawing-divider { height: 1px; margin: 5px; background: var(--rf-border); }
|
|
3027
|
-
.editor canvas[data-tool="text"] { cursor: text; }
|
|
3028
|
-
.text-annotation-editor { position: fixed; z-index: 3; width: 240px; padding: 8px; border-radius: var(--rf-radius); }
|
|
3029
|
-
.text-annotation-editor:focus-within { outline: 2px solid var(--rf-accent); outline-offset: 2px; }
|
|
3030
|
-
.text-annotation-editor textarea { display: block; width: 100%; min-height: 64px; max-height: 112px; padding: 6px; border: 0; background: transparent; resize: vertical; font-size: 16px; line-height: 24px; }
|
|
3031
|
-
.text-annotation-editor textarea:focus { outline: none; }
|
|
3032
|
-
.text-annotation-actions { display: flex; align-items: center; justify-content: flex-end; gap: 4px; }
|
|
3033
|
-
@media (pointer: coarse) {
|
|
3034
|
-
.drawing-option { min-height: 44px; }
|
|
3035
|
-
.done-btn { width: 44px; height: 44px; }
|
|
3036
|
-
.color-popover { --rf-color-width: 232px; padding-left: 16px; }
|
|
3037
|
-
.color-slider-control, .color-spectrum { height: 44px; }
|
|
3038
|
-
.color-spectrum { width: 44px; }
|
|
3039
|
-
.color-slider-thumb, .color-spectrum::before { width: 18px; height: 18px; }
|
|
3040
|
-
.editor { padding: 8px; }
|
|
3041
|
-
.toolbar { gap: 6px; }
|
|
3042
|
-
.toolbar .group { padding: 3px; gap: 0; }
|
|
2865
|
+
//#region src/feedback/ui/state/use-widget-config.ts
|
|
2866
|
+
function dismissalDuration(days) {
|
|
2867
|
+
return days !== void 0 && Number.isFinite(days) && days > 0 ? days : null;
|
|
3043
2868
|
}
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
2869
|
+
function useWidgetConfig(props) {
|
|
2870
|
+
const context = useContext(RefletContext);
|
|
2871
|
+
const publicKey = props.publicKey ?? context?.publicKey;
|
|
2872
|
+
const baseUrl = props.baseUrl ?? context?.baseUrl;
|
|
2873
|
+
const user = props.user ?? context?.user;
|
|
2874
|
+
const userToken = props.userToken ?? context?.userToken;
|
|
2875
|
+
return {
|
|
2876
|
+
client: useMemo(() => publicKey ? new Reflet({
|
|
2877
|
+
baseUrl,
|
|
2878
|
+
publicKey,
|
|
2879
|
+
user,
|
|
2880
|
+
userToken
|
|
2881
|
+
}) : null, [
|
|
2882
|
+
baseUrl,
|
|
2883
|
+
publicKey,
|
|
2884
|
+
user,
|
|
2885
|
+
userToken
|
|
2886
|
+
]),
|
|
2887
|
+
dismissalKey: `reflet-feedback-dismissed:${publicKey ?? "default"}:${user?.id ?? "anonymous"}`,
|
|
2888
|
+
dismissForDays: dismissalDuration(props.dismissForDays),
|
|
2889
|
+
isAnonymous: !(user || userToken)
|
|
2890
|
+
};
|
|
3047
2891
|
}
|
|
3048
|
-
`;
|
|
3049
2892
|
//#endregion
|
|
3050
|
-
//#region src/feedback/ui/
|
|
3051
|
-
const
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
2893
|
+
//#region src/feedback/ui/use-capture-sync.ts
|
|
2894
|
+
const RESIZE_DEBOUNCE = 400;
|
|
2895
|
+
/**
|
|
2896
|
+
* Calls `refresh` whenever the page the reporter is looking at changes shape:
|
|
2897
|
+
* SPA routes (which never fire popstate on push/replace — history methods must
|
|
2898
|
+
* be patched) and window resizes, debounced. Keeps the frozen screenshot and
|
|
2899
|
+
* page context describing what the reporter currently sees.
|
|
2900
|
+
*/
|
|
2901
|
+
function useCaptureSync(enabled, refresh) {
|
|
2902
|
+
useEffect(() => {
|
|
2903
|
+
if (!enabled) return;
|
|
2904
|
+
const history = window.history;
|
|
2905
|
+
const originalPush = history.pushState;
|
|
2906
|
+
const originalReplace = history.replaceState;
|
|
2907
|
+
const hadOwnPush = Object.hasOwn(history, "pushState");
|
|
2908
|
+
const hadOwnReplace = Object.hasOwn(history, "replaceState");
|
|
2909
|
+
history.pushState = function pushState(data, unused, url) {
|
|
2910
|
+
const result = originalPush.call(history, data, unused, url);
|
|
2911
|
+
refresh();
|
|
2912
|
+
return result;
|
|
2913
|
+
};
|
|
2914
|
+
history.replaceState = function replaceState(data, unused, url) {
|
|
2915
|
+
const result = originalReplace.call(history, data, unused, url);
|
|
2916
|
+
refresh();
|
|
2917
|
+
return result;
|
|
2918
|
+
};
|
|
2919
|
+
window.addEventListener("popstate", refresh);
|
|
2920
|
+
let resizeTimer;
|
|
2921
|
+
const onResize = () => {
|
|
2922
|
+
window.clearTimeout(resizeTimer);
|
|
2923
|
+
resizeTimer = window.setTimeout(refresh, RESIZE_DEBOUNCE);
|
|
2924
|
+
};
|
|
2925
|
+
window.addEventListener("resize", onResize);
|
|
2926
|
+
return () => {
|
|
2927
|
+
if (hadOwnPush) history.pushState = originalPush;
|
|
2928
|
+
else Reflect.deleteProperty(history, "pushState");
|
|
2929
|
+
if (hadOwnReplace) history.replaceState = originalReplace;
|
|
2930
|
+
else Reflect.deleteProperty(history, "replaceState");
|
|
2931
|
+
window.removeEventListener("popstate", refresh);
|
|
2932
|
+
window.clearTimeout(resizeTimer);
|
|
2933
|
+
window.removeEventListener("resize", onResize);
|
|
2934
|
+
};
|
|
2935
|
+
}, [enabled, refresh]);
|
|
3060
2936
|
}
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
.
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
.email-field [role="alert"] { color: var(--rf-danger); }
|
|
3077
|
-
.email-field input { width: 100%; padding: 9px; border: 1px solid var(--rf-border); border-radius: var(--rf-radius-sm); background: var(--rf-bg-subtle); color: var(--rf-text); }
|
|
3078
|
-
.dismiss-btn { min-height: 40px; text-align: left; padding: 0 8px; border-radius: var(--rf-radius-sm); color: var(--rf-text-muted); }
|
|
3079
|
-
.dismiss-btn:hover { background: var(--rf-bg-hover); }
|
|
3080
|
-
.version-info { font-size: 10px; color: var(--rf-text-muted); padding: 4px 8px 0; border-top: 1px solid var(--rf-border); text-decoration: none; }
|
|
3081
|
-
.version-info:hover { color: var(--rf-text); }
|
|
3082
|
-
.email-scrim { display: none; }
|
|
3083
|
-
.email-prompt { display: flex; flex-direction: column; gap: 12px; padding: 16px; border-radius: 22px; animation: rf-in 240ms var(--rf-ease); }
|
|
3084
|
-
.email-prompt h2 { margin: 0; font-size: 14px; font-weight: 600; line-height: 1.3; }
|
|
3085
|
-
.email-prompt p { margin: 0; font-size: 12px; line-height: 1.45; color: var(--rf-text-muted); }
|
|
3086
|
-
.email-prompt .email-field input { min-height: 40px; border-radius: 14px; padding: 0 12px; }
|
|
3087
|
-
.email-prompt-actions { display: flex; align-items: center; justify-content: flex-end; gap: 4px; }
|
|
3088
|
-
.email-prompt-actions .ghost-btn { padding: 0 12px; color: var(--rf-text-muted); }
|
|
3089
|
-
.primary-btn { display: flex; align-items: center; justify-content: center; min-height: 40px; padding: 0 16px; border-radius: var(--rf-pill); background: var(--rf-primary); color: var(--rf-primary-text); font-weight: 500; }
|
|
3090
|
-
.primary-btn:hover { opacity: .9; }
|
|
3091
|
-
.attachments { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }
|
|
3092
|
-
.attachment-row { display: flex; align-items: flex-end; gap: 8px; min-width: 0; max-width: 100%; }
|
|
3093
|
-
.screenshot-scroll-area { position: relative; min-width: 0; margin: -14px -10px -20px; }
|
|
3094
|
-
.screenshot-strip { width: 100%; padding: 14px 10px 20px; overscroll-behavior-x: contain; --rf-fade-start: 0px; --rf-fade-end: 0px; mask-image: linear-gradient(to right, transparent, black var(--rf-fade-start), black calc(100% - var(--rf-fade-end)), transparent); mask-repeat: no-repeat; }
|
|
3095
|
-
.screenshot-strip[data-overflow-x-start] { --rf-fade-start: calc(10px + min(24px, var(--scroll-area-overflow-x-start))); }
|
|
3096
|
-
.screenshot-strip[data-overflow-x-end] { --rf-fade-end: calc(10px + min(24px, var(--scroll-area-overflow-x-end))); }
|
|
3097
|
-
.screenshot-strip-content { display: flex; align-items: flex-end; gap: 8px; }
|
|
3098
|
-
.screenshot-scroll-area:has(.screenshot-strip-content:empty) { display: none; }
|
|
3099
|
-
.capture-slot { display: flex; flex-direction: column; gap: 2px; flex: none; width: 56px; height: var(--rf-attachment-height); padding: 3px; border: 1px dashed var(--rf-border); border-radius: var(--rf-radius); box-shadow: none; }
|
|
3100
|
-
.capture-action { display: flex; flex: 1; min-height: 0; align-items: center; justify-content: center; border-radius: 5px; color: var(--rf-text-muted); transition: background 160ms var(--rf-ease), color 160ms var(--rf-ease); }
|
|
3101
|
-
.capture-action:first-child { border-radius: 16px 16px 5px 5px; }
|
|
3102
|
-
.capture-action:last-child { border-radius: 5px 5px 16px 16px; }
|
|
3103
|
-
.capture-action:hover:enabled { background: var(--rf-bg-hover); color: var(--rf-text); }
|
|
3104
|
-
.screenshot-scrollbar { height: 4px; margin: 0 8px 2px; opacity: 0; transition: opacity 150ms var(--rf-ease); }
|
|
3105
|
-
.screenshot-scrollbar[data-hovering], .screenshot-scrollbar[data-scrolling] { opacity: 1; }
|
|
3106
|
-
.screenshot-scroll-thumb { border-radius: var(--rf-pill); background: var(--rf-border); }
|
|
3107
|
-
.capture-progress { position: absolute; inset: 0; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 6px; border-radius: inherit; color: var(--rf-text-muted); font-size: 10px; }
|
|
3108
|
-
.attachments:empty { display: none; }
|
|
3109
|
-
.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); }
|
|
3110
|
-
.screenshot-attachment { border: 0; }
|
|
3111
|
-
.screenshot-preview { display: block; position: relative; width: 100%; height: 100%; padding: 0; border-radius: inherit; overflow: hidden; }
|
|
3112
|
-
.screenshot-preview img, .screenshot-preview canvas { width: 100%; height: 100%; display: block; object-fit: cover; object-position: top center; }
|
|
3113
|
-
.screenshot-preview canvas { position: absolute; inset: 0; pointer-events: none; }
|
|
3114
|
-
.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); }
|
|
3115
|
-
.attachment-actions { position: absolute; right: 4px; bottom: 4px; display: flex; gap: 1px; padding: 1px; border-radius: var(--rf-pill); }
|
|
3116
|
-
.attachment-actions .icon-btn { width: 28px; height: 28px; }
|
|
3117
|
-
.attachment-actions svg { width: 14px; height: 14px; }
|
|
3118
|
-
.attachment-skeleton { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: var(--rf-text-muted); font-size: 11px; }
|
|
3119
|
-
.attachment-skeleton::after { content: ""; position: absolute; inset: 0; border-radius: inherit; background: var(--rf-bg-subtle); animation: rf-fade 800ms alternate infinite; }
|
|
3120
|
-
.selection-badge { position: absolute; left: 5px; top: 5px; display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: var(--rf-pill); color: var(--rf-accent); }
|
|
3121
|
-
.selection-badge svg { width: 12px; height: 12px; }
|
|
3122
|
-
@media (pointer: coarse) {
|
|
3123
|
-
.message-row { border-radius: 28px; }
|
|
3124
|
-
.capture-slot { width: 64px; }
|
|
3125
|
-
.message-row textarea { font-size: 16px; }
|
|
3126
|
-
.submit { width: 44px; height: 44px; }
|
|
3127
|
-
:host { --rf-attachment-height: 96px; }
|
|
3128
|
-
.attachment-actions { right: 5px; bottom: 5px; }
|
|
3129
|
-
.attachment-actions .icon-btn { width: 34px; height: 34px; }
|
|
3130
|
-
.selection-badge { width: 24px; height: 24px; }
|
|
3131
|
-
.email-field input { font-size: 16px; }
|
|
3132
|
-
.dismiss-btn { min-height: 44px; }
|
|
2937
|
+
//#endregion
|
|
2938
|
+
//#region src/feedback/ui/use-widget-state.ts
|
|
2939
|
+
const SUCCESS_CLOSE_DELAY = 2400;
|
|
2940
|
+
const IS_APPLE = /Mac|iPhone|iPad/;
|
|
2941
|
+
const MILLISECONDS_PER_DAY = 864e5;
|
|
2942
|
+
function readDismissedUntil(key) {
|
|
2943
|
+
if (typeof window === "undefined") return 0;
|
|
2944
|
+
try {
|
|
2945
|
+
const value = Number.parseInt(window.localStorage.getItem(key) ?? "", 10);
|
|
2946
|
+
if (Number.isFinite(value) && value > Date.now()) return value;
|
|
2947
|
+
window.localStorage.removeItem(key);
|
|
2948
|
+
} catch {
|
|
2949
|
+
return 0;
|
|
2950
|
+
}
|
|
2951
|
+
return 0;
|
|
3133
2952
|
}
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
.email-prompt p { font-size: 13px; }
|
|
3139
|
-
.email-prompt-actions { flex-direction: column-reverse; align-items: stretch; gap: 8px; }
|
|
3140
|
-
.email-prompt-actions .ghost-btn, .email-prompt-actions .primary-btn { justify-content: center; min-height: 48px; }
|
|
2953
|
+
function writeDismissedUntil(key, until) {
|
|
2954
|
+
try {
|
|
2955
|
+
window.localStorage.setItem(key, String(until));
|
|
2956
|
+
} catch {}
|
|
3141
2957
|
}
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
const
|
|
3147
|
-
.
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
will-change: translate;
|
|
3156
|
-
transition: translate 130ms var(--rf-ease);
|
|
2958
|
+
function matchesHotkey(event, hotkey, platform = typeof navigator === "undefined" ? "" : navigator.platform) {
|
|
2959
|
+
const parts = hotkey.toLowerCase().split("+").map((part) => part.trim());
|
|
2960
|
+
const key = parts.at(-1);
|
|
2961
|
+
if (!key || event.key.toLowerCase() !== key) return false;
|
|
2962
|
+
const isApple = IS_APPLE.test(platform);
|
|
2963
|
+
const wantsMod = parts.includes("mod");
|
|
2964
|
+
const expected = {
|
|
2965
|
+
alt: parts.includes("alt") || parts.includes("option"),
|
|
2966
|
+
ctrl: parts.includes("ctrl") || wantsMod && !isApple,
|
|
2967
|
+
meta: parts.includes("meta") || parts.includes("cmd") || wantsMod && isApple,
|
|
2968
|
+
shift: parts.includes("shift")
|
|
2969
|
+
};
|
|
2970
|
+
return event.altKey === expected.alt && event.ctrlKey === expected.ctrl && event.metaKey === expected.meta && event.shiftKey === expected.shift;
|
|
3157
2971
|
}
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
.
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
.
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
.
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
2972
|
+
function useWidgetState(props) {
|
|
2973
|
+
const { client, dismissalKey, dismissForDays, isAnonymous } = useWidgetConfig(props);
|
|
2974
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
2975
|
+
const [annotationTrigger, setAnnotationTrigger] = useState(null);
|
|
2976
|
+
const [step, setStep] = useState("compose");
|
|
2977
|
+
const [message, setMessage] = useState("");
|
|
2978
|
+
const [email, setEmail] = useState("");
|
|
2979
|
+
const [honeypot, setHoneypot] = useState("");
|
|
2980
|
+
const [isElementCapturing, setIsElementCapturing] = useState(false);
|
|
2981
|
+
const successTimer = useRef(null);
|
|
2982
|
+
const [error, setError] = useState(null);
|
|
2983
|
+
const [dismissedUntil, setDismissedUntil] = useState(() => readDismissedUntil(dismissalKey));
|
|
2984
|
+
const pickRef = useRef(0);
|
|
2985
|
+
useEffect(() => () => {
|
|
2986
|
+
pickRef.current++;
|
|
2987
|
+
if (successTimer.current) clearTimeout(successTimer.current);
|
|
2988
|
+
}, []);
|
|
2989
|
+
const consoleRef = useRef(null);
|
|
2990
|
+
useEffect(() => {
|
|
2991
|
+
setDismissedUntil(readDismissedUntil(dismissalKey));
|
|
2992
|
+
}, [dismissalKey]);
|
|
2993
|
+
useEffect(() => {
|
|
2994
|
+
if (props.captureConsole === false) return;
|
|
2995
|
+
const recorder = startConsoleRecorder();
|
|
2996
|
+
consoleRef.current = recorder;
|
|
2997
|
+
return () => {
|
|
2998
|
+
consoleRef.current = null;
|
|
2999
|
+
recorder.stop();
|
|
3000
|
+
};
|
|
3001
|
+
}, [props.captureConsole]);
|
|
3002
|
+
const captureFailed = props.labels?.captureFailed ?? DEFAULT_WIDGET_LABELS.captureFailed;
|
|
3003
|
+
const gallery = useScreenshotDrafts(captureFailed, setError);
|
|
3004
|
+
const { activeScreenshot, cancelPendingCapture, refreshAutomatic, resetScreenshots, storeScreenshot } = gallery;
|
|
3005
|
+
const capture = activeScreenshot?.image ?? null;
|
|
3006
|
+
const annotations = activeScreenshot?.annotations ?? [];
|
|
3007
|
+
const isCapturing = gallery.pendingCapture !== null || isElementCapturing;
|
|
3008
|
+
const submission = useFeedbackSubmission({
|
|
3009
|
+
client,
|
|
3010
|
+
onError: setError,
|
|
3011
|
+
onSubmitted: (feedbackId) => {
|
|
3012
|
+
setStep("success");
|
|
3013
|
+
props.onSubmit?.({ feedbackId });
|
|
3014
|
+
successTimer.current = setTimeout(close, SUCCESS_CLOSE_DELAY);
|
|
3015
|
+
},
|
|
3016
|
+
partialUploadMessage: props.labels?.attachmentUploadFailed ?? DEFAULT_WIDGET_LABELS.attachmentUploadFailed
|
|
3017
|
+
});
|
|
3018
|
+
const { resetSubmission } = submission;
|
|
3019
|
+
const firstCapture = gallery.pendingCapture ?? gallery.screenshots[0];
|
|
3020
|
+
const canRefreshAutomatically = gallery.screenshots.length <= 1 && firstCapture?.source === "automatic";
|
|
3021
|
+
useCaptureSync(isOpen && !submission.isEditingDisabled && step === "compose" && canRefreshAutomatically && annotations.length === 0 && !gallery.screenshots.some((draft) => draft.selection) && props.captureOnOpen !== false, refreshAutomatic);
|
|
3022
|
+
const reset = useCallback(() => {
|
|
3023
|
+
if (successTimer.current) clearTimeout(successTimer.current);
|
|
3024
|
+
setAnnotationTrigger(null);
|
|
3025
|
+
resetSubmission();
|
|
3026
|
+
pickRef.current++;
|
|
3027
|
+
setIsElementCapturing(false);
|
|
3028
|
+
resetScreenshots();
|
|
3029
|
+
setMessage("");
|
|
3030
|
+
setEmail("");
|
|
3031
|
+
setHoneypot("");
|
|
3032
|
+
setError(null);
|
|
3033
|
+
setStep("compose");
|
|
3034
|
+
}, [resetSubmission, resetScreenshots]);
|
|
3035
|
+
const close = useCallback(() => {
|
|
3036
|
+
setIsOpen(false);
|
|
3037
|
+
reset();
|
|
3038
|
+
props.onClose?.();
|
|
3039
|
+
}, [reset, props.onClose]);
|
|
3040
|
+
const dismiss = useCallback(() => {
|
|
3041
|
+
if (dismissForDays === null) return;
|
|
3042
|
+
const until = Date.now() + dismissForDays * MILLISECONDS_PER_DAY;
|
|
3043
|
+
writeDismissedUntil(dismissalKey, until);
|
|
3044
|
+
setIsOpen(false);
|
|
3045
|
+
reset();
|
|
3046
|
+
setDismissedUntil(until);
|
|
3047
|
+
props.onDismiss?.({ until });
|
|
3048
|
+
}, [
|
|
3049
|
+
dismissForDays,
|
|
3050
|
+
dismissalKey,
|
|
3051
|
+
props.onDismiss,
|
|
3052
|
+
reset
|
|
3053
|
+
]);
|
|
3054
|
+
const open = useCallback(() => {
|
|
3055
|
+
setIsOpen(true);
|
|
3056
|
+
setStep("compose");
|
|
3057
|
+
props.onOpen?.();
|
|
3058
|
+
if (props.captureOnOpen !== false) refreshAutomatic();
|
|
3059
|
+
}, [
|
|
3060
|
+
props.captureOnOpen,
|
|
3061
|
+
props.onOpen,
|
|
3062
|
+
refreshAutomatic
|
|
3063
|
+
]);
|
|
3064
|
+
const selectElement = useCallback(async (element, note = "") => {
|
|
3065
|
+
cancelPendingCapture();
|
|
3066
|
+
setIsElementCapturing(true);
|
|
3067
|
+
const picked = note ? {
|
|
3068
|
+
...buildElementSelection(element),
|
|
3069
|
+
comment: note.slice(0, MAX_SELECTION_COMMENT_LENGTH)
|
|
3070
|
+
} : buildElementSelection(element);
|
|
3071
|
+
setStep("compose");
|
|
3072
|
+
const pick = ++pickRef.current;
|
|
3073
|
+
const context = collectPageContext({ sdkVersion: SDK_VERSION });
|
|
3074
|
+
const [fresh, closeUp] = await Promise.all([captureViewport().catch(() => null), captureElementCloseUp(element).catch(() => null)]);
|
|
3075
|
+
if (pickRef.current !== pick) {
|
|
3076
|
+
releaseCapture(fresh);
|
|
3077
|
+
releaseCapture(closeUp);
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3080
|
+
setIsElementCapturing(false);
|
|
3081
|
+
if (!fresh) {
|
|
3082
|
+
releaseCapture(closeUp);
|
|
3083
|
+
setError(captureFailed);
|
|
3084
|
+
return;
|
|
3085
|
+
}
|
|
3086
|
+
storeScreenshot({
|
|
3087
|
+
annotations: [highlightFor(picked, fresh)],
|
|
3088
|
+
closeUp,
|
|
3089
|
+
context,
|
|
3090
|
+
id: crypto.randomUUID(),
|
|
3091
|
+
image: fresh,
|
|
3092
|
+
selectedNode: element,
|
|
3093
|
+
selection: picked,
|
|
3094
|
+
source: "manual"
|
|
3095
|
+
});
|
|
3096
|
+
}, [
|
|
3097
|
+
cancelPendingCapture,
|
|
3098
|
+
captureFailed,
|
|
3099
|
+
storeScreenshot
|
|
3100
|
+
]);
|
|
3101
|
+
const selections = gallery.screenshots.flatMap(({ selection }) => selection ? [selection] : []);
|
|
3102
|
+
const firstElementNote = selections.find(({ comment }) => comment)?.comment;
|
|
3103
|
+
const reportedMessage = message.trim() || firstElementNote || "";
|
|
3104
|
+
const submit = () => {
|
|
3105
|
+
if (honeypot) {
|
|
3106
|
+
setStep("success");
|
|
3107
|
+
return;
|
|
3108
|
+
}
|
|
3109
|
+
setStep("compose");
|
|
3110
|
+
return submission.submit({
|
|
3111
|
+
context: {
|
|
3112
|
+
...activeScreenshot?.context ?? collectPageContext({ sdkVersion: "0.4.3" }),
|
|
3113
|
+
consoleEvents: consoleRef.current?.events(),
|
|
3114
|
+
metadata: props.metadata,
|
|
3115
|
+
selections
|
|
3116
|
+
},
|
|
3117
|
+
email,
|
|
3118
|
+
isAnonymous,
|
|
3119
|
+
message: reportedMessage,
|
|
3120
|
+
screenshots: gallery.screenshots
|
|
3121
|
+
});
|
|
3122
|
+
};
|
|
3123
|
+
const requestSubmit = () => {
|
|
3124
|
+
if (isAnonymous && email.length === 0 && !submission.hasPendingAttachments && !honeypot) {
|
|
3125
|
+
setStep("email");
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
3128
|
+
return submit();
|
|
3129
|
+
};
|
|
3130
|
+
return {
|
|
3131
|
+
activeScreenshot,
|
|
3132
|
+
annotateScreenshot: (id, trigger) => {
|
|
3133
|
+
setAnnotationTrigger(trigger);
|
|
3134
|
+
gallery.selectScreenshot(id);
|
|
3135
|
+
setStep("annotate");
|
|
3136
|
+
},
|
|
3137
|
+
annotations,
|
|
3138
|
+
annotationTrigger,
|
|
3139
|
+
canDismiss: dismissForDays !== null,
|
|
3140
|
+
canSubmit: reportedMessage.length > 0,
|
|
3141
|
+
capture,
|
|
3142
|
+
close,
|
|
3143
|
+
dismiss,
|
|
3144
|
+
dismissForDays,
|
|
3145
|
+
email,
|
|
3146
|
+
error,
|
|
3147
|
+
hasPendingAttachments: submission.hasPendingAttachments,
|
|
3148
|
+
honeypot,
|
|
3149
|
+
isAnonymous,
|
|
3150
|
+
isCapturing,
|
|
3151
|
+
isDismissed: dismissedUntil > Date.now(),
|
|
3152
|
+
isEditingDisabled: submission.isEditingDisabled,
|
|
3153
|
+
isOpen,
|
|
3154
|
+
isSubmitting: submission.isSubmitting,
|
|
3155
|
+
message,
|
|
3156
|
+
open,
|
|
3157
|
+
pendingCapture: gallery.pendingCapture,
|
|
3158
|
+
removeCapture: gallery.removeCapture,
|
|
3159
|
+
requestSubmit,
|
|
3160
|
+
retakeCapture: gallery.retakeCapture,
|
|
3161
|
+
screenshots: gallery.screenshots,
|
|
3162
|
+
selectElement,
|
|
3163
|
+
selections,
|
|
3164
|
+
setAnnotations: gallery.setAnnotations,
|
|
3165
|
+
setEmail,
|
|
3166
|
+
setHoneypot,
|
|
3167
|
+
setMessage,
|
|
3168
|
+
setSelectionComment: gallery.setSelectionComment,
|
|
3169
|
+
setStep,
|
|
3170
|
+
step,
|
|
3171
|
+
submit,
|
|
3172
|
+
takeCapture: gallery.takeCapture
|
|
3173
|
+
};
|
|
3278
3174
|
}
|
|
3279
|
-
|
|
3280
|
-
`;
|
|
3281
3175
|
//#endregion
|
|
3282
|
-
//#region src/feedback/ui/
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
--rf-pill: 999px;
|
|
3303
|
-
--rf-attachment-height: 78px;
|
|
3304
|
-
--rf-width: 360px;
|
|
3305
|
-
--rf-glass: rgb(255 255 255 / 72%);
|
|
3306
|
-
--rf-glass-highlight: rgb(255 255 255 / 90%);
|
|
3307
|
-
--rf-glass-stroke: rgb(255 255 255 / 70%);
|
|
3308
|
-
--rf-radius-sm: 8px;
|
|
3309
|
-
--rf-shadow-inset: inset 0 0 0 1px var(--rf-border);
|
|
3310
|
-
--rf-ring: 0 0 0 1px rgb(15 15 15 / 4%);
|
|
3311
|
-
--rf-shadow-low: 0 1px 2px rgb(15 15 15 / 6%), 0 4px 10px -4px rgb(15 15 15 / 10%);
|
|
3312
|
-
--rf-shadow: 0 2px 5px -1px rgb(15 15 15 / 7%), 0 12px 32px -8px rgb(15 15 15 / 16%);
|
|
3313
|
-
--rf-shadow-float: 0 4px 12px -3px rgb(15 15 15 / 35%);
|
|
3314
|
-
--rf-ease: cubic-bezier(0.16, 1, 0.3, 1);
|
|
3315
|
-
color-scheme: light;
|
|
3316
|
-
all: initial;
|
|
3317
|
-
font-family: var(--rf-font, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
|
|
3318
|
-
}
|
|
3319
|
-
|
|
3320
|
-
:host([data-theme="dark"]) {
|
|
3321
|
-
--rf-bg: #202020;
|
|
3322
|
-
--rf-bg-subtle: rgb(255 255 255 / 5%);
|
|
3323
|
-
--rf-bg-hover: rgb(255 255 255 / 9%);
|
|
3324
|
-
--rf-border: rgb(255 255 255 / 9%);
|
|
3325
|
-
--rf-hairline: rgb(255 255 255 / 12%);
|
|
3326
|
-
--rf-text: #ebeae8;
|
|
3327
|
-
--rf-text-muted: rgb(255 255 255 / 68%);
|
|
3328
|
-
--rf-glass: rgb(32 32 32 / 78%);
|
|
3329
|
-
--rf-glass-highlight: rgb(255 255 255 / 14%);
|
|
3330
|
-
--rf-glass-stroke: rgb(255 255 255 / 12%);
|
|
3331
|
-
--rf-primary: #f1efec;
|
|
3332
|
-
--rf-primary-text: #1c1c1b;
|
|
3333
|
-
--rf-accent: #529cca;
|
|
3334
|
-
--rf-danger: #eb5757;
|
|
3335
|
-
--rf-success: #4dab74;
|
|
3336
|
-
--rf-ring: 0 0 0 1px rgb(255 255 255 / 7%);
|
|
3337
|
-
--rf-shadow-low: 0 1px 2px rgb(0 0 0 / 30%), 0 4px 10px -4px rgb(0 0 0 / 40%);
|
|
3338
|
-
--rf-shadow: 0 2px 5px -1px rgb(0 0 0 / 35%), 0 14px 36px -10px rgb(0 0 0 / 55%);
|
|
3339
|
-
--rf-shadow-float: 0 4px 14px -3px rgb(0 0 0 / 55%);
|
|
3340
|
-
color-scheme: dark;
|
|
3341
|
-
}
|
|
3342
|
-
|
|
3343
|
-
*, *::before, *::after { box-sizing: border-box; }
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
.root {
|
|
3347
|
-
position: fixed;
|
|
3348
|
-
z-index: ${REFLET_Z_INDEX.widgetRoot};
|
|
3349
|
-
display: flex;
|
|
3350
|
-
align-items: flex-end;
|
|
3351
|
-
gap: 8px;
|
|
3352
|
-
font: 13px/1.45 var(--rf-font, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif);
|
|
3353
|
-
color: var(--rf-text);
|
|
3176
|
+
//#region src/feedback/ui/visible-viewport.ts
|
|
3177
|
+
/**
|
|
3178
|
+
* The part of the page the reporter can actually see, in fixed-position
|
|
3179
|
+
* coordinates. On mobile the keyboard shrinks this without touching
|
|
3180
|
+
* `window.innerHeight`, so anything that must stay reachable measures here.
|
|
3181
|
+
*/
|
|
3182
|
+
function visibleViewport() {
|
|
3183
|
+
const viewport = window.visualViewport;
|
|
3184
|
+
const left = viewport?.offsetLeft ?? 0;
|
|
3185
|
+
const top = viewport?.offsetTop ?? 0;
|
|
3186
|
+
const width = viewport?.width ?? window.innerWidth;
|
|
3187
|
+
const height = viewport?.height ?? window.innerHeight;
|
|
3188
|
+
return {
|
|
3189
|
+
bottom: window.innerHeight - top - height,
|
|
3190
|
+
height,
|
|
3191
|
+
left,
|
|
3192
|
+
right: window.innerWidth - left - width,
|
|
3193
|
+
top,
|
|
3194
|
+
width
|
|
3195
|
+
};
|
|
3354
3196
|
}
|
|
3355
|
-
|
|
3356
|
-
.
|
|
3357
|
-
.
|
|
3358
|
-
.
|
|
3359
|
-
|
|
3360
|
-
.
|
|
3361
|
-
.
|
|
3362
|
-
.
|
|
3363
|
-
|
|
3364
|
-
button, select, input, textarea { font: inherit; color: inherit; }
|
|
3365
|
-
button { margin: 0; cursor: pointer; border: 0; background: none; transition: scale 120ms var(--rf-ease); }
|
|
3366
|
-
button:disabled { cursor: not-allowed; opacity: .4; }
|
|
3367
|
-
button:active:not(:disabled) { scale: .97; }
|
|
3368
|
-
svg { flex-shrink: 0; }
|
|
3369
|
-
:focus-visible { outline: 2px solid var(--rf-accent); outline-offset: 3px; }
|
|
3370
|
-
.icon-btn, .tool {
|
|
3371
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
3372
|
-
width: 40px; height: 40px; flex: none; border-radius: var(--rf-pill); color: var(--rf-text-muted);
|
|
3373
|
-
transition: background 160ms var(--rf-ease), color 160ms var(--rf-ease), scale 120ms var(--rf-ease);
|
|
3197
|
+
function onViewportChange(update) {
|
|
3198
|
+
window.addEventListener("resize", update);
|
|
3199
|
+
window.visualViewport?.addEventListener("resize", update);
|
|
3200
|
+
window.visualViewport?.addEventListener("scroll", update);
|
|
3201
|
+
return () => {
|
|
3202
|
+
window.removeEventListener("resize", update);
|
|
3203
|
+
window.visualViewport?.removeEventListener("resize", update);
|
|
3204
|
+
window.visualViewport?.removeEventListener("scroll", update);
|
|
3205
|
+
};
|
|
3374
3206
|
}
|
|
3375
|
-
|
|
3376
|
-
.
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3207
|
+
//#endregion
|
|
3208
|
+
//#region src/feedback/ui/floating/use-floating-position.ts
|
|
3209
|
+
const EDGE_GAP$1 = 12;
|
|
3210
|
+
const KEYBOARD_STEP = 24;
|
|
3211
|
+
const KEY_DIRECTIONS = {
|
|
3212
|
+
ArrowDown: {
|
|
3213
|
+
x: 0,
|
|
3214
|
+
y: KEYBOARD_STEP
|
|
3215
|
+
},
|
|
3216
|
+
ArrowLeft: {
|
|
3217
|
+
x: -24,
|
|
3218
|
+
y: 0
|
|
3219
|
+
},
|
|
3220
|
+
ArrowRight: {
|
|
3221
|
+
x: KEYBOARD_STEP,
|
|
3222
|
+
y: 0
|
|
3223
|
+
},
|
|
3224
|
+
ArrowUp: {
|
|
3225
|
+
x: 0,
|
|
3226
|
+
y: -24
|
|
3227
|
+
}
|
|
3228
|
+
};
|
|
3229
|
+
function clampPosition(point, bounds, viewport = visibleViewport()) {
|
|
3230
|
+
return {
|
|
3231
|
+
x: Math.max(viewport.left + EDGE_GAP$1, Math.min(point.x, viewport.left + viewport.width - bounds.width - EDGE_GAP$1)),
|
|
3232
|
+
y: Math.max(viewport.top + EDGE_GAP$1, Math.min(point.y, viewport.top + viewport.height - bounds.height - EDGE_GAP$1))
|
|
3233
|
+
};
|
|
3381
3234
|
}
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3235
|
+
function useFloatingPosition(corner = "bottom-right", isPanelOpen = false) {
|
|
3236
|
+
const rootRef = useRef(null);
|
|
3237
|
+
const gesture = useRef(null);
|
|
3238
|
+
const [position, setPosition] = useState(null);
|
|
3239
|
+
const [positionedPanelOpen, setPositionedPanelOpen] = useState(isPanelOpen);
|
|
3240
|
+
const [viewport, setViewport] = useState(visibleViewport);
|
|
3241
|
+
const horizontalEdge = corner.endsWith("right") ? "right" : "left";
|
|
3242
|
+
const verticalEdge = corner.startsWith("bottom") ? "bottom" : "top";
|
|
3243
|
+
if (positionedPanelOpen !== isPanelOpen) {
|
|
3244
|
+
setPositionedPanelOpen(isPanelOpen);
|
|
3245
|
+
setPosition(null);
|
|
3246
|
+
}
|
|
3247
|
+
const moveTo = useCallback((point, bounds) => {
|
|
3248
|
+
const fitted = clampPosition(point, bounds);
|
|
3249
|
+
const visible = visibleViewport();
|
|
3250
|
+
const anchor = {
|
|
3251
|
+
x: horizontalEdge === "right" ? visible.left + visible.width - fitted.x - bounds.width : fitted.x - visible.left,
|
|
3252
|
+
y: verticalEdge === "bottom" ? visible.top + visible.height - fitted.y - bounds.height : fitted.y - visible.top
|
|
3253
|
+
};
|
|
3254
|
+
setPosition((current) => current?.x === anchor.x && current.y === anchor.y ? current : anchor);
|
|
3255
|
+
}, [horizontalEdge, verticalEdge]);
|
|
3256
|
+
const keepInViewport = useCallback(() => {
|
|
3257
|
+
const bounds = rootRef.current?.getBoundingClientRect();
|
|
3258
|
+
if (!bounds) return;
|
|
3259
|
+
const fitted = clampPosition(bounds, bounds, viewport);
|
|
3260
|
+
if (Math.abs(fitted.x - bounds.x) > .5 || Math.abs(fitted.y - bounds.y) > .5) moveTo(fitted, bounds);
|
|
3261
|
+
}, [moveTo, viewport]);
|
|
3262
|
+
useLayoutEffect(() => {
|
|
3263
|
+
keepInViewport();
|
|
3264
|
+
}, [keepInViewport]);
|
|
3265
|
+
useEffect(() => {
|
|
3266
|
+
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(keepInViewport);
|
|
3267
|
+
if (rootRef.current) observer?.observe(rootRef.current);
|
|
3268
|
+
const stopListening = onViewportChange(() => setViewport(visibleViewport()));
|
|
3269
|
+
return () => {
|
|
3270
|
+
observer?.disconnect();
|
|
3271
|
+
stopListening();
|
|
3272
|
+
};
|
|
3273
|
+
}, [keepInViewport]);
|
|
3274
|
+
const onPointerDown = (event) => {
|
|
3275
|
+
const bounds = rootRef.current?.getBoundingClientRect();
|
|
3276
|
+
if (!bounds || event.button !== 0) return;
|
|
3277
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
3278
|
+
gesture.current = {
|
|
3279
|
+
x: event.clientX - bounds.x,
|
|
3280
|
+
y: event.clientY - bounds.y
|
|
3281
|
+
};
|
|
3282
|
+
};
|
|
3283
|
+
const onPointerMove = (event) => {
|
|
3284
|
+
const bounds = rootRef.current?.getBoundingClientRect();
|
|
3285
|
+
const grab = gesture.current;
|
|
3286
|
+
if (!(bounds && grab && event.currentTarget.hasPointerCapture(event.pointerId))) return;
|
|
3287
|
+
moveTo({
|
|
3288
|
+
x: event.clientX - grab.x,
|
|
3289
|
+
y: event.clientY - grab.y
|
|
3290
|
+
}, bounds);
|
|
3291
|
+
};
|
|
3292
|
+
const onKeyDown = (event) => {
|
|
3293
|
+
const direction = KEY_DIRECTIONS[event.key];
|
|
3294
|
+
const bounds = rootRef.current?.getBoundingClientRect();
|
|
3295
|
+
if (!(bounds && direction)) return;
|
|
3296
|
+
event.preventDefault();
|
|
3297
|
+
moveTo({
|
|
3298
|
+
x: bounds.x + direction.x,
|
|
3299
|
+
y: bounds.y + direction.y
|
|
3300
|
+
}, bounds);
|
|
3301
|
+
};
|
|
3302
|
+
const viewportStyles = `.root { --rf-viewport-width: ${viewport.width}px; --rf-viewport-top: ${viewport.top}px; --rf-viewport-bottom: ${viewport.bottom}px; --rf-viewport-left: ${viewport.left}px; --rf-viewport-right: ${viewport.right}px; }`;
|
|
3303
|
+
const anchorStyles = position ? `.root[data-moved="true"] { left: auto; right: auto; top: auto; bottom: auto; ${horizontalEdge}: ${position.x + viewport[horizontalEdge]}px; ${verticalEdge}: ${position.y + viewport[verticalEdge]}px; }` : "";
|
|
3304
|
+
return {
|
|
3305
|
+
handleProps: {
|
|
3306
|
+
onKeyDown,
|
|
3307
|
+
onLostPointerCapture: () => {
|
|
3308
|
+
gesture.current = null;
|
|
3309
|
+
},
|
|
3310
|
+
onPointerCancel: () => {
|
|
3311
|
+
gesture.current = null;
|
|
3312
|
+
},
|
|
3313
|
+
onPointerDown,
|
|
3314
|
+
onPointerMove,
|
|
3315
|
+
onPointerUp: () => {
|
|
3316
|
+
gesture.current = null;
|
|
3317
|
+
}
|
|
3318
|
+
},
|
|
3319
|
+
position,
|
|
3320
|
+
rootRef,
|
|
3321
|
+
styles: viewportStyles + anchorStyles
|
|
3322
|
+
};
|
|
3389
3323
|
}
|
|
3390
|
-
|
|
3391
|
-
.
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3324
|
+
//#endregion
|
|
3325
|
+
//#region src/feedback/ui/floating/floating-widget.tsx
|
|
3326
|
+
function FloatingWidget({ hotkey, labels, position, state }) {
|
|
3327
|
+
const [minimized, setMinimized] = useState(false);
|
|
3328
|
+
const launcherRef = useRef(null);
|
|
3329
|
+
const showPanel = state.isOpen && !minimized;
|
|
3330
|
+
const floating = useFloatingPosition(position, showPanel);
|
|
3331
|
+
const minimize = () => {
|
|
3332
|
+
setMinimized(true);
|
|
3333
|
+
requestAnimationFrame(() => launcherRef.current?.focus());
|
|
3334
|
+
};
|
|
3335
|
+
useEffect(() => {
|
|
3336
|
+
if (state.step === "compose") state.annotationTrigger?.button.focus({ preventScroll: true });
|
|
3337
|
+
}, [state.step, state.annotationTrigger]);
|
|
3338
|
+
useEffect(() => {
|
|
3339
|
+
if (!showPanel || state.step !== "compose") return;
|
|
3340
|
+
const onKeyDown = (event) => {
|
|
3341
|
+
if (event.key === "Escape" && !event.defaultPrevented) {
|
|
3342
|
+
setMinimized(true);
|
|
3343
|
+
requestAnimationFrame(() => launcherRef.current?.focus());
|
|
3344
|
+
}
|
|
3345
|
+
};
|
|
3346
|
+
return listenToKeydown(floating.rootRef.current, onKeyDown);
|
|
3347
|
+
}, [
|
|
3348
|
+
floating.rootRef,
|
|
3349
|
+
showPanel,
|
|
3350
|
+
state.step
|
|
3351
|
+
]);
|
|
3352
|
+
const { close, isOpen, open } = state;
|
|
3353
|
+
useEffect(() => {
|
|
3354
|
+
if (!hotkey) return;
|
|
3355
|
+
const toggleOnHotkey = (event) => {
|
|
3356
|
+
if (matchesHotkey(event, hotkey)) {
|
|
3357
|
+
event.preventDefault();
|
|
3358
|
+
if (isOpen) close();
|
|
3359
|
+
else open();
|
|
3360
|
+
}
|
|
3361
|
+
};
|
|
3362
|
+
return listenToKeydown(floating.rootRef.current, toggleOnHotkey, true);
|
|
3363
|
+
}, [
|
|
3364
|
+
close,
|
|
3365
|
+
floating.rootRef,
|
|
3366
|
+
hotkey,
|
|
3367
|
+
isOpen,
|
|
3368
|
+
open
|
|
3369
|
+
]);
|
|
3370
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3371
|
+
className: "root",
|
|
3372
|
+
"data-editing": state.isOpen && (state.step === "picking" || state.step === "annotate"),
|
|
3373
|
+
"data-moved": Boolean(floating.position),
|
|
3374
|
+
"data-position": position,
|
|
3375
|
+
ref: floating.rootRef,
|
|
3376
|
+
children: [
|
|
3377
|
+
/* @__PURE__ */ jsx("style", { children: floating.styles }),
|
|
3378
|
+
showPanel && state.step === "compose" && state.screenshots.map(({ id, selectedNode }) => selectedNode ? /* @__PURE__ */ jsx(SelectionOutline, { node: selectedNode }, id) : null),
|
|
3379
|
+
showPanel ? /* @__PURE__ */ jsx(FeedbackPanel, {
|
|
3380
|
+
floatingControls: /* @__PURE__ */ jsxs("div", {
|
|
3381
|
+
className: "floating-controls",
|
|
3382
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
3383
|
+
"aria-label": labels.moveFeedback,
|
|
3384
|
+
className: "icon-btn drag-handle",
|
|
3385
|
+
title: labels.moveFeedback,
|
|
3386
|
+
type: "button",
|
|
3387
|
+
...floating.handleProps,
|
|
3388
|
+
children: /* @__PURE__ */ jsx(GripIcon, {})
|
|
3389
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
3390
|
+
"aria-label": labels.minimize,
|
|
3391
|
+
className: "icon-btn",
|
|
3392
|
+
onClick: minimize,
|
|
3393
|
+
title: labels.minimize,
|
|
3394
|
+
type: "button",
|
|
3395
|
+
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
3396
|
+
})]
|
|
3397
|
+
}),
|
|
3398
|
+
labels,
|
|
3399
|
+
state
|
|
3400
|
+
}) : /* @__PURE__ */ jsx(Launcher, {
|
|
3401
|
+
buttonRef: launcherRef,
|
|
3402
|
+
isOpen: false,
|
|
3403
|
+
label: state.isOpen ? labels.resume : labels.trigger,
|
|
3404
|
+
onClick: () => {
|
|
3405
|
+
setMinimized(false);
|
|
3406
|
+
if (!state.isOpen) state.open();
|
|
3407
|
+
}
|
|
3408
|
+
})
|
|
3409
|
+
]
|
|
3410
|
+
});
|
|
3408
3411
|
}
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3412
|
+
//#endregion
|
|
3413
|
+
//#region src/feedback/ui/floating/element-walk.ts
|
|
3414
|
+
const ARROW_SIBLING = {
|
|
3415
|
+
ArrowLeft: "previousElementSibling",
|
|
3416
|
+
ArrowRight: "nextElementSibling"
|
|
3417
|
+
};
|
|
3418
|
+
const AIM_KEYS = [
|
|
3419
|
+
"Tab",
|
|
3420
|
+
"ArrowUp",
|
|
3421
|
+
"ArrowDown",
|
|
3422
|
+
"ArrowLeft",
|
|
3423
|
+
"ArrowRight"
|
|
3424
|
+
];
|
|
3425
|
+
function isWidgetOwned(element) {
|
|
3426
|
+
return element.closest(`[${WIDGET_MARKER}]`) !== null;
|
|
3414
3427
|
}
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
z-index: -1;
|
|
3420
|
-
border-radius: inherit;
|
|
3421
|
-
background: var(--rf-bg);
|
|
3422
|
-
pointer-events: none;
|
|
3428
|
+
function isPickable(element) {
|
|
3429
|
+
if (!element || element === document.documentElement || isWidgetOwned(element)) return false;
|
|
3430
|
+
const rect = element.getBoundingClientRect();
|
|
3431
|
+
return rect.width > 0 && rect.height > 0;
|
|
3423
3432
|
}
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
background: var(--rf-glass);
|
|
3427
|
-
-webkit-backdrop-filter: blur(20px) saturate(1.6);
|
|
3428
|
-
backdrop-filter: blur(20px) saturate(1.6);
|
|
3429
|
-
}
|
|
3433
|
+
function pickableElements() {
|
|
3434
|
+
return Array.from(document.body.querySelectorAll("*")).filter(isPickable);
|
|
3430
3435
|
}
|
|
3431
|
-
|
|
3432
|
-
|
|
3436
|
+
function documentStep(from, backwards) {
|
|
3437
|
+
const all = pickableElements();
|
|
3438
|
+
const index = all.indexOf(from);
|
|
3439
|
+
if (all.length === 0) return null;
|
|
3440
|
+
if (index === -1) return all[0] ?? null;
|
|
3441
|
+
return all[(index + (backwards ? -1 : 1) + all.length) % all.length] ?? null;
|
|
3433
3442
|
}
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
${COMPOSER_STYLES}
|
|
3439
|
-
${ANNOTATOR_STYLES}
|
|
3440
|
-
${PICKER_STYLES}
|
|
3441
|
-
@media (prefers-reduced-motion: reduce) {
|
|
3442
|
-
*, *::before, *::after { animation: none !important; transition: none !important; }
|
|
3443
|
-
button:active:not(:disabled) { scale: 1; }
|
|
3443
|
+
function climb(from) {
|
|
3444
|
+
let candidate = from.parentElement;
|
|
3445
|
+
while (candidate && !isPickable(candidate)) candidate = candidate.parentElement;
|
|
3446
|
+
return candidate;
|
|
3444
3447
|
}
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
function useResolvedTheme(theme) {
|
|
3450
|
-
const [systemDark, setSystemDark] = useState(false);
|
|
3451
|
-
useEffect(() => {
|
|
3452
|
-
if (theme !== "auto" || typeof window.matchMedia !== "function") return;
|
|
3453
|
-
const query = window.matchMedia(DARK_QUERY);
|
|
3454
|
-
setSystemDark(query.matches);
|
|
3455
|
-
const onChange = (event) => setSystemDark(event.matches);
|
|
3456
|
-
query.addEventListener("change", onChange);
|
|
3457
|
-
return () => query.removeEventListener("change", onChange);
|
|
3458
|
-
}, [theme]);
|
|
3459
|
-
if (theme === "auto") return systemDark ? "dark" : "light";
|
|
3460
|
-
return theme;
|
|
3448
|
+
function descend(from) {
|
|
3449
|
+
let candidate = from.firstElementChild;
|
|
3450
|
+
while (candidate && !isPickable(candidate)) candidate = candidate.nextElementSibling;
|
|
3451
|
+
return candidate;
|
|
3461
3452
|
}
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
function
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
const style = document.createElement("style");
|
|
3475
|
-
style.textContent = WIDGET_STYLES;
|
|
3476
|
-
root.append(style);
|
|
3477
|
-
document.body.append(host);
|
|
3478
|
-
setShadowRoot(root);
|
|
3479
|
-
return () => host.remove();
|
|
3480
|
-
}, []);
|
|
3481
|
-
useEffect(() => {
|
|
3482
|
-
const host = shadowRoot?.host;
|
|
3483
|
-
if (!(host instanceof HTMLElement)) return;
|
|
3484
|
-
host.setAttribute("data-theme", resolvedTheme);
|
|
3485
|
-
host.style.setProperty("--rf-offset", `${offset}px`);
|
|
3486
|
-
if (primaryColor) host.style.setProperty("--rf-primary", primaryColor);
|
|
3487
|
-
}, [
|
|
3488
|
-
shadowRoot,
|
|
3489
|
-
resolvedTheme,
|
|
3490
|
-
primaryColor,
|
|
3491
|
-
offset
|
|
3492
|
-
]);
|
|
3493
|
-
if (!shadowRoot) return null;
|
|
3494
|
-
return createPortal(children, shadowRoot);
|
|
3453
|
+
function sibling(from, direction) {
|
|
3454
|
+
let candidate = from[direction];
|
|
3455
|
+
while (candidate && !isPickable(candidate)) candidate = candidate[direction];
|
|
3456
|
+
return candidate;
|
|
3457
|
+
}
|
|
3458
|
+
function stepAim(from, key, backwards) {
|
|
3459
|
+
if (!(from && isPickable(from))) return pickableElements()[0] ?? null;
|
|
3460
|
+
if (key === "Tab") return documentStep(from, backwards);
|
|
3461
|
+
if (key === "ArrowUp") return climb(from);
|
|
3462
|
+
if (key === "ArrowDown") return descend(from);
|
|
3463
|
+
const direction = ARROW_SIBLING[key];
|
|
3464
|
+
return direction ? sibling(from, direction) : null;
|
|
3495
3465
|
}
|
|
3496
3466
|
//#endregion
|
|
3497
|
-
//#region src/feedback/
|
|
3498
|
-
const
|
|
3499
|
-
const
|
|
3500
|
-
const
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
if (
|
|
3506
|
-
|
|
3507
|
-
} catch {
|
|
3508
|
-
return "[unserializable]";
|
|
3509
|
-
}
|
|
3510
|
-
if (typeof value === "function") return "[function]";
|
|
3511
|
-
return String(value);
|
|
3467
|
+
//#region src/feedback/ui/picker.tsx
|
|
3468
|
+
const LABEL_HEIGHT = 24;
|
|
3469
|
+
const LABEL_GAP = 6;
|
|
3470
|
+
const NOTE_WIDTH = 300;
|
|
3471
|
+
const NOTE_GAP = 10;
|
|
3472
|
+
const EDGE_GAP = 12;
|
|
3473
|
+
function elementUnder(x, y) {
|
|
3474
|
+
const element = document.elementFromPoint(x, y);
|
|
3475
|
+
if (!element || isWidgetOwned(element)) return null;
|
|
3476
|
+
return element;
|
|
3512
3477
|
}
|
|
3513
|
-
function
|
|
3514
|
-
|
|
3515
|
-
|
|
3478
|
+
function describeTarget(element) {
|
|
3479
|
+
return {
|
|
3480
|
+
componentName: resolveComponentStack(getFiberFromNode(element))[0],
|
|
3481
|
+
element,
|
|
3482
|
+
label: describeElement(element),
|
|
3483
|
+
rect: element.getBoundingClientRect(),
|
|
3484
|
+
region: describeRegion(element)
|
|
3485
|
+
};
|
|
3516
3486
|
}
|
|
3517
3487
|
/**
|
|
3518
|
-
*
|
|
3519
|
-
*
|
|
3488
|
+
* Pins the note card to the element: below it, flipped above when the visible
|
|
3489
|
+
* viewport — what is left of the screen once a mobile keyboard is up — has no
|
|
3490
|
+
* room there. Measures against the part of the element actually on screen, so
|
|
3491
|
+
* picking a full-height section still leaves the card in view.
|
|
3520
3492
|
*/
|
|
3521
|
-
function
|
|
3522
|
-
const
|
|
3523
|
-
const
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
level,
|
|
3529
|
-
message,
|
|
3530
|
-
timestamp: Date.now()
|
|
3531
|
-
});
|
|
3532
|
-
if (buffer.length > limit) buffer.splice(0, buffer.length - limit);
|
|
3533
|
-
};
|
|
3534
|
-
const originals = /* @__PURE__ */ new Map();
|
|
3535
|
-
const wrappers = /* @__PURE__ */ new Map();
|
|
3536
|
-
for (const method of RECORDED_METHODS) {
|
|
3537
|
-
const original = console[method];
|
|
3538
|
-
const wrapper = (...args) => {
|
|
3539
|
-
push(method, formatMessage(args));
|
|
3540
|
-
Reflect.apply(original, console, args);
|
|
3541
|
-
};
|
|
3542
|
-
originals.set(method, original);
|
|
3543
|
-
wrappers.set(method, wrapper);
|
|
3544
|
-
console[method] = wrapper;
|
|
3545
|
-
}
|
|
3546
|
-
const onError = (event) => {
|
|
3547
|
-
const where = event.filename ? ` (${event.filename}:${event.lineno}:${event.colno})` : "";
|
|
3548
|
-
push("error", `${event.message}${where}`);
|
|
3549
|
-
};
|
|
3550
|
-
const onRejection = (event) => {
|
|
3551
|
-
push("error", `Unhandled rejection: ${serializeArgument(event.reason)}`);
|
|
3493
|
+
function noteStyle(rect, view) {
|
|
3494
|
+
const left = Math.min(Math.max(view.left + EDGE_GAP, rect.left), Math.max(view.left + EDGE_GAP, view.left + view.width - NOTE_WIDTH - EDGE_GAP));
|
|
3495
|
+
const visibleTop = Math.max(rect.top, view.top + EDGE_GAP);
|
|
3496
|
+
const visibleBottom = Math.min(rect.bottom, view.top + view.height - EDGE_GAP);
|
|
3497
|
+
if (view.top + view.height - visibleBottom > visibleTop - view.top) return {
|
|
3498
|
+
left,
|
|
3499
|
+
top: visibleBottom + NOTE_GAP
|
|
3552
3500
|
};
|
|
3553
|
-
window.addEventListener("error", onError);
|
|
3554
|
-
window.addEventListener("unhandledrejection", onRejection);
|
|
3555
3501
|
return {
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
if (!active) return;
|
|
3559
|
-
active = false;
|
|
3560
|
-
buffer.length = 0;
|
|
3561
|
-
for (const method of RECORDED_METHODS) {
|
|
3562
|
-
const original = originals.get(method);
|
|
3563
|
-
if (original && console[method] === wrappers.get(method)) console[method] = original;
|
|
3564
|
-
}
|
|
3565
|
-
window.removeEventListener("error", onError);
|
|
3566
|
-
window.removeEventListener("unhandledrejection", onRejection);
|
|
3567
|
-
}
|
|
3502
|
+
bottom: window.innerHeight - visibleTop + NOTE_GAP,
|
|
3503
|
+
left
|
|
3568
3504
|
};
|
|
3569
3505
|
}
|
|
3506
|
+
/**
|
|
3507
|
+
* Points at any element in the page the way a coding agent needs it: the React
|
|
3508
|
+
* component that owns it plus the page region it lives in. Picking never
|
|
3509
|
+
* commits on its own — it pins a note card to the element, so the reporter
|
|
3510
|
+
* writes about that element right there and confirms. Aiming stays live until
|
|
3511
|
+
* then, which is what makes the flow usable without hover, on touch.
|
|
3512
|
+
*/
|
|
3513
|
+
function ElementPicker({ labels, onCancel, onPick }) {
|
|
3514
|
+
const [target, setTarget] = useState(null);
|
|
3515
|
+
const [pinned, setPinned] = useState(null);
|
|
3516
|
+
const [note, setNote] = useState("");
|
|
3517
|
+
const pickerRef = useRef(null);
|
|
3518
|
+
const noteRef = useRef(null);
|
|
3519
|
+
const pinnedRef = useRef(false);
|
|
3520
|
+
const [view, setView] = useState(visibleViewport);
|
|
3521
|
+
const aimRef = useRef(null);
|
|
3522
|
+
useEffect(() => onViewportChange(() => setView(visibleViewport())), []);
|
|
3523
|
+
useEffect(() => {
|
|
3524
|
+
pinnedRef.current = pinned !== null;
|
|
3525
|
+
if (pinned) noteRef.current?.focus({ preventScroll: true });
|
|
3526
|
+
}, [pinned]);
|
|
3527
|
+
useEffect(() => {
|
|
3528
|
+
aimRef.current = target?.element ?? null;
|
|
3529
|
+
}, [target]);
|
|
3530
|
+
useEffect(() => {
|
|
3531
|
+
const swallow = (event) => {
|
|
3532
|
+
if (startsInWidget(event)) return;
|
|
3533
|
+
event.preventDefault();
|
|
3534
|
+
event.stopPropagation();
|
|
3535
|
+
};
|
|
3536
|
+
const aimAt = (x, y) => {
|
|
3537
|
+
const element = elementUnder(x, y);
|
|
3538
|
+
if (!element) return;
|
|
3539
|
+
setTarget((current) => current?.element === element ? {
|
|
3540
|
+
...current,
|
|
3541
|
+
rect: element.getBoundingClientRect()
|
|
3542
|
+
} : describeTarget(element));
|
|
3543
|
+
};
|
|
3544
|
+
const onMove = (event) => {
|
|
3545
|
+
if (pinnedRef.current || startsInWidget(event)) return;
|
|
3546
|
+
aimAt(event.clientX, event.clientY);
|
|
3547
|
+
};
|
|
3548
|
+
const onPointerDown = (event) => {
|
|
3549
|
+
if (startsInWidget(event)) return;
|
|
3550
|
+
swallow(event);
|
|
3551
|
+
if (!pinnedRef.current) aimAt(event.clientX, event.clientY);
|
|
3552
|
+
};
|
|
3553
|
+
const onPointerUp = (event) => {
|
|
3554
|
+
if (startsInWidget(event)) return;
|
|
3555
|
+
swallow(event);
|
|
3556
|
+
if (pinnedRef.current) return;
|
|
3557
|
+
const element = elementUnder(event.clientX, event.clientY);
|
|
3558
|
+
if (element) setPinned(describeTarget(element));
|
|
3559
|
+
};
|
|
3560
|
+
const moveAim = (key, backwards) => {
|
|
3561
|
+
const next = stepAim(aimRef.current, key, backwards);
|
|
3562
|
+
if (!next) return;
|
|
3563
|
+
next.scrollIntoView({
|
|
3564
|
+
block: "nearest",
|
|
3565
|
+
inline: "nearest"
|
|
3566
|
+
});
|
|
3567
|
+
aimRef.current = next;
|
|
3568
|
+
setTarget(describeTarget(next));
|
|
3569
|
+
};
|
|
3570
|
+
const onEscape = () => {
|
|
3571
|
+
if (pinnedRef.current) {
|
|
3572
|
+
setPinned(null);
|
|
3573
|
+
setNote("");
|
|
3574
|
+
return;
|
|
3575
|
+
}
|
|
3576
|
+
onCancel();
|
|
3577
|
+
};
|
|
3578
|
+
const onKeyDown = (event) => {
|
|
3579
|
+
if (event.key === "Escape") {
|
|
3580
|
+
swallow(event);
|
|
3581
|
+
onEscape();
|
|
3582
|
+
return;
|
|
3583
|
+
}
|
|
3584
|
+
if (pinnedRef.current || startsInWidget(event)) return;
|
|
3585
|
+
if (event.key === "Enter") {
|
|
3586
|
+
swallow(event);
|
|
3587
|
+
if (aimRef.current) setPinned(describeTarget(aimRef.current));
|
|
3588
|
+
return;
|
|
3589
|
+
}
|
|
3590
|
+
if (!AIM_KEYS.includes(event.key)) return;
|
|
3591
|
+
swallow(event);
|
|
3592
|
+
moveAim(event.key, event.shiftKey);
|
|
3593
|
+
};
|
|
3594
|
+
const onScroll = () => {
|
|
3595
|
+
setTarget((current) => current ? {
|
|
3596
|
+
...current,
|
|
3597
|
+
rect: current.element.getBoundingClientRect()
|
|
3598
|
+
} : current);
|
|
3599
|
+
setPinned((current) => current ? {
|
|
3600
|
+
...current,
|
|
3601
|
+
rect: current.element.getBoundingClientRect()
|
|
3602
|
+
} : current);
|
|
3603
|
+
};
|
|
3604
|
+
document.addEventListener("pointermove", onMove, true);
|
|
3605
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
3606
|
+
document.addEventListener("pointerup", onPointerUp, true);
|
|
3607
|
+
document.addEventListener("mousedown", swallow, true);
|
|
3608
|
+
document.addEventListener("mouseup", swallow, true);
|
|
3609
|
+
document.addEventListener("click", swallow, true);
|
|
3610
|
+
const stopListeningToKeys = listenToKeydown(pickerRef.current, onKeyDown, true);
|
|
3611
|
+
window.addEventListener("scroll", onScroll, {
|
|
3612
|
+
capture: true,
|
|
3613
|
+
passive: true
|
|
3614
|
+
});
|
|
3615
|
+
const previousCursor = document.body.style.cursor;
|
|
3616
|
+
document.body.style.cursor = "crosshair";
|
|
3617
|
+
return () => {
|
|
3618
|
+
document.removeEventListener("pointermove", onMove, true);
|
|
3619
|
+
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
3620
|
+
document.removeEventListener("pointerup", onPointerUp, true);
|
|
3621
|
+
document.removeEventListener("mousedown", swallow, true);
|
|
3622
|
+
document.removeEventListener("mouseup", swallow, true);
|
|
3623
|
+
document.removeEventListener("click", swallow, true);
|
|
3624
|
+
stopListeningToKeys();
|
|
3625
|
+
window.removeEventListener("scroll", onScroll, true);
|
|
3626
|
+
document.body.style.cursor = previousCursor;
|
|
3627
|
+
};
|
|
3628
|
+
}, [onCancel]);
|
|
3629
|
+
const rect = (pinned ?? target)?.rect;
|
|
3630
|
+
const labelAbove = rect ? rect.top > 30 : true;
|
|
3631
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3632
|
+
ref: pickerRef,
|
|
3633
|
+
children: [
|
|
3634
|
+
rect && /* @__PURE__ */ jsx("div", {
|
|
3635
|
+
className: "picker-box",
|
|
3636
|
+
"data-pinned": pinned !== null,
|
|
3637
|
+
style: {
|
|
3638
|
+
height: rect.height,
|
|
3639
|
+
translate: `${rect.left}px ${rect.top}px`,
|
|
3640
|
+
width: rect.width
|
|
3641
|
+
}
|
|
3642
|
+
}),
|
|
3643
|
+
rect && target && !pinned && /* @__PURE__ */ jsxs("div", {
|
|
3644
|
+
className: "picker-label",
|
|
3645
|
+
style: { translate: `${Math.max(4, rect.left)}px ${labelAbove ? rect.top - LABEL_HEIGHT - LABEL_GAP : rect.bottom + LABEL_GAP}px` },
|
|
3646
|
+
children: [/* @__PURE__ */ jsx("strong", { children: target.componentName ? `<${target.componentName}>` : target.label }), target.region && /* @__PURE__ */ jsx("span", { children: target.region })]
|
|
3647
|
+
}),
|
|
3648
|
+
pinned ? /* @__PURE__ */ jsxs("form", {
|
|
3649
|
+
className: "picker-note glass",
|
|
3650
|
+
onSubmit: (event) => {
|
|
3651
|
+
event.preventDefault();
|
|
3652
|
+
onPick(pinned.element, note.trim());
|
|
3653
|
+
},
|
|
3654
|
+
style: noteStyle(pinned.rect, view),
|
|
3655
|
+
children: [
|
|
3656
|
+
/* @__PURE__ */ jsxs("p", {
|
|
3657
|
+
className: "picker-note-target",
|
|
3658
|
+
children: [/* @__PURE__ */ jsx("strong", { children: pinned.componentName ? `<${pinned.componentName}>` : pinned.label }), pinned.region && /* @__PURE__ */ jsx("span", { children: pinned.region })]
|
|
3659
|
+
}),
|
|
3660
|
+
/* @__PURE__ */ jsx("textarea", {
|
|
3661
|
+
"aria-label": labels.elementNote,
|
|
3662
|
+
maxLength: MAX_SELECTION_COMMENT_LENGTH,
|
|
3663
|
+
onChange: (event) => setNote(event.target.value),
|
|
3664
|
+
onKeyDown: (event) => {
|
|
3665
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
3666
|
+
event.preventDefault();
|
|
3667
|
+
onPick(pinned.element, note.trim());
|
|
3668
|
+
}
|
|
3669
|
+
},
|
|
3670
|
+
placeholder: labels.elementNotePlaceholder,
|
|
3671
|
+
ref: noteRef,
|
|
3672
|
+
rows: 2,
|
|
3673
|
+
value: note
|
|
3674
|
+
}),
|
|
3675
|
+
/* @__PURE__ */ jsxs("div", {
|
|
3676
|
+
className: "picker-note-actions",
|
|
3677
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
3678
|
+
className: "picker-cancel",
|
|
3679
|
+
onClick: () => {
|
|
3680
|
+
setPinned(null);
|
|
3681
|
+
setNote("");
|
|
3682
|
+
},
|
|
3683
|
+
type: "button",
|
|
3684
|
+
children: labels.pickAnother
|
|
3685
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
3686
|
+
"aria-label": labels.attachElement,
|
|
3687
|
+
className: "submit",
|
|
3688
|
+
title: labels.attachElement,
|
|
3689
|
+
type: "submit",
|
|
3690
|
+
children: /* @__PURE__ */ jsx(ArrowIcon, {})
|
|
3691
|
+
})]
|
|
3692
|
+
})
|
|
3693
|
+
]
|
|
3694
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
3695
|
+
className: "picker-hint",
|
|
3696
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
3697
|
+
className: "picker-instruction",
|
|
3698
|
+
children: [
|
|
3699
|
+
labels.pickElementHint,
|
|
3700
|
+
" ",
|
|
3701
|
+
/* @__PURE__ */ jsx("kbd", { children: "Tab" }),
|
|
3702
|
+
/* @__PURE__ */ jsx("kbd", { children: "Enter" }),
|
|
3703
|
+
/* @__PURE__ */ jsx("kbd", { children: "Esc" })
|
|
3704
|
+
]
|
|
3705
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
3706
|
+
className: "picker-cancel",
|
|
3707
|
+
onClick: onCancel,
|
|
3708
|
+
type: "button",
|
|
3709
|
+
children: labels.cancel
|
|
3710
|
+
})]
|
|
3711
|
+
})
|
|
3712
|
+
]
|
|
3713
|
+
});
|
|
3714
|
+
}
|
|
3715
|
+
//#endregion
|
|
3716
|
+
//#region src/feedback/ui/annotation/styles.ts
|
|
3717
|
+
const ANNOTATOR_STYLES = `
|
|
3718
|
+
.overlay { margin: 0; border: 0; padding: 0; width: 100vw; max-width: none; height: 100dvh; max-height: none; position: fixed; inset: 0; z-index: ${REFLET_Z_INDEX.annotatorOverlay}; display: flex; flex-direction: column; background: transparent; color: var(--rf-text); font: 13px/1.45 ui-sans-serif, system-ui, sans-serif; }
|
|
3719
|
+
.overlay::before { content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none; background: color-mix(in srgb, var(--rf-bg) 94%, transparent); animation: rf-editor-in 260ms var(--rf-ease); transition: opacity 260ms var(--rf-ease); }
|
|
3720
|
+
.overlay::backdrop { background: transparent; }
|
|
3721
|
+
@keyframes rf-editor-in { from { opacity: 0; } }
|
|
3722
|
+
.editor { flex: 1; position: relative; display: flex; align-items: center; justify-content: center; padding: 20px; min-height: 0; container-type: size; }
|
|
3723
|
+
.editor-image { width: min(100cqw, calc(100cqh * var(--rf-capture-ratio))); aspect-ratio: var(--rf-capture-ratio); flex: none; position: relative; }
|
|
3724
|
+
.editor-visual { position: absolute; inset: 0; border-radius: var(--rf-radius); box-shadow: var(--rf-shadow); transform-origin: top left; overflow: hidden; }
|
|
3725
|
+
.editor-visual img, .editor-visual canvas { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: inherit; object-fit: cover; object-position: top center; outline: 1px solid var(--rf-hairline); outline-offset: -1px; }
|
|
3726
|
+
.editor-visual canvas { cursor: crosshair; touch-action: none; }
|
|
3727
|
+
.overlay[data-opening="true"] .editor { pointer-events: none; }
|
|
3728
|
+
.overlay[data-closing="true"] { pointer-events: none; }
|
|
3729
|
+
.overlay[data-closing="true"]::before { opacity: 0; }
|
|
3730
|
+
.overlay[data-closing="true"] .toolbar, .overlay[data-closing="true"] .element-note { opacity: 0; transform: translateY(8px); }
|
|
3731
|
+
|
|
3732
|
+
.element-note { order: 1; display: flex; align-items: flex-start; gap: 10px; width: min(560px, calc(100vw - 32px)); margin: 0 auto 10px; padding: 10px 14px; border-radius: var(--rf-radius); transition: opacity 180ms var(--rf-ease), transform 180ms var(--rf-ease); }
|
|
3733
|
+
.element-note svg { flex: none; margin-top: 4px; opacity: 0.7; }
|
|
3734
|
+
.element-note textarea { flex: 1; min-width: 0; height: 42px; max-height: 96px; padding: 0; border: 0; background: transparent; color: inherit; font: inherit; resize: none; }
|
|
3735
|
+
.element-note textarea:focus { outline: none; }
|
|
3736
|
+
.element-note:focus-within { outline: 2px solid var(--rf-accent); outline-offset: 2px; }
|
|
3737
|
+
|
|
3738
|
+
.toolbar { order: 2; display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; transition: opacity 180ms var(--rf-ease), transform 180ms var(--rf-ease); padding: 4px 12px max(16px, env(safe-area-inset-bottom)); }
|
|
3739
|
+
.toolbar .group { display: flex; align-items: center; gap: 2px; padding: 4px; border-radius: var(--rf-pill); }
|
|
3740
|
+
.tool[aria-pressed="true"], .tool[data-selected="true"] { background: var(--rf-primary); color: var(--rf-primary-text); }
|
|
3741
|
+
.color-picker { position: relative; }
|
|
3742
|
+
.color-picker summary { display: flex; justify-content: center; align-items: center; }
|
|
3743
|
+
.color-preview { width: 18px; height: 18px; border-radius: 50%; background: var(--annotation-color); box-shadow: inset 0 0 0 1px var(--rf-border); }
|
|
3744
|
+
.color-popover { --rf-color-width: 200px; position: absolute; bottom: calc(100% + 14px); left: clamp(calc(12px - var(--rf-color-anchor)), 0px, calc(100vw - var(--rf-color-anchor) - var(--rf-color-width) - 12px)); display: flex; align-items: center; gap: 6px; width: var(--rf-color-width); padding: 4px 4px 4px 14px; border-radius: var(--rf-pill); animation: rf-in 180ms var(--rf-ease); }
|
|
3745
|
+
.color-picker[data-placement="below"] .color-popover { top: calc(100% + 14px); bottom: auto; }
|
|
3746
|
+
.color-slider { flex: 1; min-width: 0; }
|
|
3747
|
+
.color-slider-control { display: flex; align-items: center; height: 40px; width: 100%; touch-action: none; user-select: none; cursor: pointer; }
|
|
3748
|
+
.color-slider-track { height: 12px; width: 100%; border-radius: var(--rf-pill); background: linear-gradient(to right, var(--rf-swatch-base), var(--annotation-base-color)); box-shadow: inset 0 0 0 1px var(--rf-border); }
|
|
3749
|
+
.color-slider-thumb { width: 16px; height: 16px; border: 2px solid var(--rf-swatch-base); border-radius: 50%; background: var(--annotation-color); box-shadow: var(--rf-shadow-low); transition: scale 160ms var(--rf-ease); }
|
|
3750
|
+
.color-slider-thumb[data-dragging] { scale: 1.12; }
|
|
3751
|
+
.color-slider-thumb:has(:focus-visible) { outline: 2px solid var(--rf-accent); outline-offset: 3px; }
|
|
3752
|
+
.color-spectrum { position: relative; display: flex; align-items: center; justify-content: center; flex: none; width: 40px; height: 40px; border-radius: 50%; cursor: pointer; }
|
|
3753
|
+
.color-spectrum::before { content: ""; width: 16px; height: 16px; border-radius: 50%; background: var(--rf-spectrum); box-shadow: inset 0 0 0 1px var(--rf-glass-highlight); }
|
|
3754
|
+
.color-spectrum:hover { background: var(--rf-bg-hover); }
|
|
3755
|
+
.color-spectrum button { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: inherit; }
|
|
3756
|
+
.color-spectrum button:focus-visible { outline: 2px solid var(--rf-accent); outline-offset: 3px; }
|
|
3757
|
+
.color-spectrum input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; pointer-events: none; }
|
|
3758
|
+
.done-btn { display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; padding: 0; border-radius: var(--rf-pill); color: var(--rf-primary-text); background: var(--rf-primary); }
|
|
3759
|
+
.drawing-menu { position: relative; }
|
|
3760
|
+
.drawing-popover { position: absolute; z-index: 2; right: 0; bottom: calc(100% + 12px); width: 200px; max-height: calc(50dvh - 68px); overflow-y: auto; padding: 6px; border-radius: var(--rf-radius); }
|
|
3761
|
+
.drawing-menu[data-placement="below"] .drawing-popover { top: calc(100% + 12px); bottom: auto; }
|
|
3762
|
+
.drawing-option { display: flex; align-items: center; width: 100%; min-height: 40px; gap: 10px; padding: 0 10px; border-radius: 12px; text-align: left; }
|
|
3763
|
+
.drawing-option:hover, .drawing-option[aria-pressed="true"] { background: var(--rf-bg-hover); }
|
|
3764
|
+
.drawing-divider { height: 1px; margin: 5px; background: var(--rf-border); }
|
|
3765
|
+
.editor canvas[data-tool="text"] { cursor: text; }
|
|
3766
|
+
.text-annotation-editor { position: fixed; z-index: 3; width: 240px; padding: 8px; border-radius: var(--rf-radius); }
|
|
3767
|
+
.text-annotation-editor:focus-within { outline: 2px solid var(--rf-accent); outline-offset: 2px; }
|
|
3768
|
+
.text-annotation-editor textarea { display: block; width: 100%; min-height: 64px; max-height: 112px; padding: 6px; border: 0; background: transparent; resize: vertical; font-size: 16px; line-height: 24px; }
|
|
3769
|
+
.text-annotation-editor textarea:focus { outline: none; }
|
|
3770
|
+
.text-annotation-actions { display: flex; align-items: center; justify-content: flex-end; gap: 4px; }
|
|
3771
|
+
@media (pointer: coarse) {
|
|
3772
|
+
.drawing-option { min-height: 44px; }
|
|
3773
|
+
.done-btn { width: 44px; height: 44px; }
|
|
3774
|
+
.color-popover { --rf-color-width: 232px; padding-left: 16px; }
|
|
3775
|
+
.color-slider-control, .color-spectrum { height: 44px; }
|
|
3776
|
+
.color-spectrum { width: 44px; }
|
|
3777
|
+
.color-slider-thumb, .color-spectrum::before { width: 18px; height: 18px; }
|
|
3778
|
+
.editor { padding: 8px; }
|
|
3779
|
+
.toolbar { gap: 6px; }
|
|
3780
|
+
.toolbar .group { padding: 3px; gap: 0; }
|
|
3781
|
+
}
|
|
3782
|
+
@media (pointer: coarse) and (max-width: 360px) {
|
|
3783
|
+
.toolbar { gap: 4px; padding-inline: 2px; }
|
|
3784
|
+
.toolbar .group { padding: 0; }
|
|
3785
|
+
}
|
|
3786
|
+
`;
|
|
3570
3787
|
//#endregion
|
|
3571
|
-
//#region src/feedback/
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3788
|
+
//#region src/feedback/ui/composer/styles.ts
|
|
3789
|
+
const COMPOSER_STYLES = `
|
|
3790
|
+
.panel { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 8px; outline: none; animation: rf-in 240ms var(--rf-ease); }
|
|
3791
|
+
.success-panel { border-radius: var(--rf-radius); }
|
|
3792
|
+
.composer { position: relative; display: flex; flex-direction: column; gap: 8px; }
|
|
3793
|
+
.message-row { display: flex; flex-direction: column; gap: 2px; padding: 6px; border-radius: 26px; }
|
|
3794
|
+
.message-row textarea {
|
|
3795
|
+
width: 100%; height: 76px; max-height: min(140px, 30dvh);
|
|
3796
|
+
margin: 0; padding: 6px 6px 0 8px; border: 0; resize: none;
|
|
3797
|
+
background: transparent; line-height: 20px;
|
|
3575
3798
|
}
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3799
|
+
.message-actions { display: flex; align-items: center; gap: 2px; }
|
|
3800
|
+
.message-row textarea::placeholder { color: var(--rf-text-muted); }
|
|
3801
|
+
.message-row textarea:focus { outline: none; }
|
|
3802
|
+
.message-row:has(textarea:focus-visible) { border-color: color-mix(in srgb, var(--rf-accent) 40%, var(--rf-glass-stroke)); }
|
|
3803
|
+
.submit { display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; flex: none; border-radius: var(--rf-pill); background: var(--rf-primary); color: var(--rf-primary-text); transition: opacity 160ms var(--rf-ease), transform 160ms var(--rf-ease); }
|
|
3804
|
+
.submit svg { transform: rotate(-45deg); }
|
|
3805
|
+
.submit:hover:enabled { transform: scale(1.05); }
|
|
3806
|
+
.submit:disabled { background: var(--rf-bg-hover); color: var(--rf-text-muted); opacity: .65; }
|
|
3807
|
+
.composer-toolbar { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex-wrap: wrap; }
|
|
3808
|
+
.composer-options { position: relative; }
|
|
3809
|
+
summary { cursor: pointer; list-style: none; }
|
|
3810
|
+
summary::-webkit-details-marker { display: none; }
|
|
3811
|
+
.options-popover { position: absolute; z-index: 2; right: 0; bottom: calc(100% + 12px); width: min(240px, calc(100vw - 40px)); max-height: calc(50dvh - 68px); overflow-y: auto; display: flex; flex-direction: column; gap: 8px; padding: 12px; border-radius: var(--rf-radius); animation: rf-in 160ms var(--rf-ease); }
|
|
3812
|
+
.composer-options[data-placement="below"] .options-popover { top: calc(100% + 12px); bottom: auto; }
|
|
3813
|
+
.email-field { display: flex; flex-direction: column; gap: 6px; color: var(--rf-text-muted); font-size: 12px; }
|
|
3814
|
+
.email-field [role="alert"] { color: var(--rf-danger); }
|
|
3815
|
+
.email-field input { width: 100%; padding: 9px; border: 1px solid var(--rf-border); border-radius: var(--rf-radius-sm); background: var(--rf-bg-subtle); color: var(--rf-text); }
|
|
3816
|
+
.dismiss-btn { min-height: 40px; text-align: left; padding: 0 8px; border-radius: var(--rf-radius-sm); color: var(--rf-text-muted); }
|
|
3817
|
+
.dismiss-btn:hover { background: var(--rf-bg-hover); }
|
|
3818
|
+
.version-info { font-size: 10px; color: var(--rf-text-muted); padding: 4px 8px 0; border-top: 1px solid var(--rf-border); text-decoration: none; }
|
|
3819
|
+
.version-info:hover { color: var(--rf-text); }
|
|
3820
|
+
.email-scrim { display: none; }
|
|
3821
|
+
.email-prompt { display: flex; flex-direction: column; gap: 12px; padding: 16px; border-radius: 22px; animation: rf-in 240ms var(--rf-ease); }
|
|
3822
|
+
.email-prompt h2 { margin: 0; font-size: 14px; font-weight: 600; line-height: 1.3; }
|
|
3823
|
+
.email-prompt p { margin: 0; font-size: 12px; line-height: 1.45; color: var(--rf-text-muted); }
|
|
3824
|
+
.email-prompt .email-field input { min-height: 40px; border-radius: 14px; padding: 0 12px; }
|
|
3825
|
+
.email-prompt-actions { display: flex; align-items: center; justify-content: flex-end; gap: 4px; }
|
|
3826
|
+
.email-prompt-actions .ghost-btn { padding: 0 12px; color: var(--rf-text-muted); }
|
|
3827
|
+
.primary-btn { display: flex; align-items: center; justify-content: center; min-height: 40px; padding: 0 16px; border-radius: var(--rf-pill); background: var(--rf-primary); color: var(--rf-primary-text); font-weight: 500; }
|
|
3828
|
+
.primary-btn:hover { opacity: .9; }
|
|
3829
|
+
.attachments { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }
|
|
3830
|
+
.attachment-row { display: flex; align-items: flex-end; gap: 8px; min-width: 0; max-width: 100%; }
|
|
3831
|
+
.screenshot-scroll-area { position: relative; min-width: 0; margin: -14px -10px -20px; }
|
|
3832
|
+
.screenshot-strip { width: 100%; padding: 14px 10px 20px; overscroll-behavior-x: contain; --rf-fade-start: 0px; --rf-fade-end: 0px; mask-image: linear-gradient(to right, transparent, black var(--rf-fade-start), black calc(100% - var(--rf-fade-end)), transparent); mask-repeat: no-repeat; }
|
|
3833
|
+
.screenshot-strip[data-overflow-x-start] { --rf-fade-start: calc(10px + min(24px, var(--scroll-area-overflow-x-start))); }
|
|
3834
|
+
.screenshot-strip[data-overflow-x-end] { --rf-fade-end: calc(10px + min(24px, var(--scroll-area-overflow-x-end))); }
|
|
3835
|
+
.screenshot-strip-content { display: flex; align-items: flex-end; gap: 8px; }
|
|
3836
|
+
.screenshot-scroll-area:has(.screenshot-strip-content:empty) { display: none; }
|
|
3837
|
+
.capture-slot { display: flex; flex-direction: column; gap: 2px; flex: none; width: 56px; height: var(--rf-attachment-height); padding: 3px; border: 1px dashed var(--rf-border); border-radius: var(--rf-radius); box-shadow: none; }
|
|
3838
|
+
.capture-action { display: flex; flex: 1; min-height: 0; align-items: center; justify-content: center; border-radius: 5px; color: var(--rf-text-muted); transition: background 160ms var(--rf-ease), color 160ms var(--rf-ease); }
|
|
3839
|
+
.capture-action:first-child { border-radius: 16px 16px 5px 5px; }
|
|
3840
|
+
.capture-action:last-child { border-radius: 5px 5px 16px 16px; }
|
|
3841
|
+
.capture-action:hover:enabled { background: var(--rf-bg-hover); color: var(--rf-text); }
|
|
3842
|
+
.screenshot-scrollbar { height: 4px; margin: 0 8px 2px; opacity: 0; transition: opacity 150ms var(--rf-ease); }
|
|
3843
|
+
.screenshot-scrollbar[data-hovering], .screenshot-scrollbar[data-scrolling] { opacity: 1; }
|
|
3844
|
+
.screenshot-scroll-thumb { border-radius: var(--rf-pill); background: var(--rf-border); }
|
|
3845
|
+
.capture-progress { position: absolute; inset: 0; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 6px; border-radius: inherit; color: var(--rf-text-muted); font-size: 10px; }
|
|
3846
|
+
.attachments:empty { display: none; }
|
|
3847
|
+
.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); }
|
|
3848
|
+
.screenshot-attachment { border: 0; }
|
|
3849
|
+
.screenshot-preview { display: block; position: relative; width: 100%; height: 100%; padding: 0; border-radius: inherit; overflow: hidden; }
|
|
3850
|
+
.screenshot-preview img, .screenshot-preview canvas { width: 100%; height: 100%; display: block; object-fit: cover; object-position: top center; }
|
|
3851
|
+
.screenshot-preview canvas { position: absolute; inset: 0; pointer-events: none; }
|
|
3852
|
+
.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); }
|
|
3853
|
+
.attachment-actions { position: absolute; right: 4px; bottom: 4px; display: flex; gap: 1px; padding: 1px; border-radius: var(--rf-pill); }
|
|
3854
|
+
.attachment-actions .icon-btn { width: 28px; height: 28px; }
|
|
3855
|
+
.attachment-actions svg { width: 14px; height: 14px; }
|
|
3856
|
+
.attachment-skeleton { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: var(--rf-text-muted); font-size: 11px; }
|
|
3857
|
+
.attachment-skeleton::after { content: ""; position: absolute; inset: 0; border-radius: inherit; background: var(--rf-bg-subtle); animation: rf-fade 800ms alternate infinite; }
|
|
3858
|
+
.selection-badge { position: absolute; left: 5px; top: 5px; display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: var(--rf-pill); color: var(--rf-accent); }
|
|
3859
|
+
.selection-badge svg { width: 12px; height: 12px; }
|
|
3860
|
+
@media (pointer: coarse) {
|
|
3861
|
+
.message-row { border-radius: 28px; }
|
|
3862
|
+
.capture-slot { width: 64px; }
|
|
3863
|
+
.message-row textarea { font-size: 16px; }
|
|
3864
|
+
.submit { width: 44px; height: 44px; }
|
|
3865
|
+
:host { --rf-attachment-height: 96px; }
|
|
3866
|
+
.attachment-actions { right: 5px; bottom: 5px; }
|
|
3867
|
+
.attachment-actions .icon-btn { width: 34px; height: 34px; }
|
|
3868
|
+
.selection-badge { width: 24px; height: 24px; }
|
|
3869
|
+
.email-field input { font-size: 16px; }
|
|
3870
|
+
.dismiss-btn { min-height: 44px; }
|
|
3608
3871
|
}
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3872
|
+
@media (max-width: 480px) {
|
|
3873
|
+
.email-scrim { display: block; position: fixed; inset: 0; background: rgb(0 0 0 / 40%); animation: rf-fade 200ms var(--rf-ease); }
|
|
3874
|
+
.email-prompt { position: fixed; left: 0; right: 0; bottom: 0; gap: 14px; padding: 20px 16px calc(20px + env(safe-area-inset-bottom)); border-radius: 26px 26px 0 0; animation: rf-drawer-in 280ms var(--rf-ease); }
|
|
3875
|
+
.email-prompt h2 { font-size: 17px; }
|
|
3876
|
+
.email-prompt p { font-size: 13px; }
|
|
3877
|
+
.email-prompt-actions { flex-direction: column-reverse; align-items: stretch; gap: 8px; }
|
|
3878
|
+
.email-prompt-actions .ghost-btn, .email-prompt-actions .primary-btn { justify-content: center; min-height: 48px; }
|
|
3614
3879
|
}
|
|
3880
|
+
@keyframes rf-drawer-in { from { transform: translateY(100%); } }
|
|
3881
|
+
`;
|
|
3615
3882
|
//#endregion
|
|
3616
|
-
//#region src/feedback/
|
|
3617
|
-
const
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3883
|
+
//#region src/feedback/ui/floating/picker-styles.ts
|
|
3884
|
+
const PICKER_STYLES = `
|
|
3885
|
+
.picker-box {
|
|
3886
|
+
position: fixed;
|
|
3887
|
+
top: 0;
|
|
3888
|
+
left: 0;
|
|
3889
|
+
pointer-events: none;
|
|
3890
|
+
border-radius: 5px;
|
|
3891
|
+
background: color-mix(in srgb, var(--rf-accent) 10%, transparent);
|
|
3892
|
+
box-shadow: 0 0 0 2px var(--rf-accent), 0 0 0 5px color-mix(in srgb, var(--rf-accent) 18%, transparent);
|
|
3893
|
+
will-change: translate;
|
|
3894
|
+
transition: translate 130ms var(--rf-ease);
|
|
3625
3895
|
}
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3896
|
+
.picker-box[data-pinned="true"] { transition: none; }
|
|
3897
|
+
.picker-label {
|
|
3898
|
+
position: fixed;
|
|
3899
|
+
top: 0;
|
|
3900
|
+
left: 0;
|
|
3901
|
+
pointer-events: none;
|
|
3902
|
+
display: inline-flex;
|
|
3903
|
+
align-items: center;
|
|
3904
|
+
gap: 6px;
|
|
3905
|
+
max-width: min(420px, 90vw);
|
|
3906
|
+
height: 24px;
|
|
3907
|
+
padding: 0 8px;
|
|
3908
|
+
font-size: 11.5px;
|
|
3909
|
+
font-weight: 500;
|
|
3910
|
+
color: var(--rf-accent-text);
|
|
3911
|
+
background: var(--rf-accent);
|
|
3912
|
+
border-radius: 6px;
|
|
3913
|
+
box-shadow: var(--rf-shadow-float);
|
|
3914
|
+
white-space: nowrap;
|
|
3915
|
+
overflow: hidden;
|
|
3916
|
+
will-change: translate;
|
|
3917
|
+
transition: translate 130ms var(--rf-ease);
|
|
3630
3918
|
}
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3919
|
+
.picker-label strong { flex: none; font-weight: 600; }
|
|
3920
|
+
.picker-label span { min-width: 0; overflow: hidden; text-overflow: ellipsis; opacity: 0.75; }
|
|
3921
|
+
.selection-outline {
|
|
3922
|
+
position: fixed;
|
|
3923
|
+
top: 0;
|
|
3924
|
+
left: 0;
|
|
3925
|
+
pointer-events: none;
|
|
3926
|
+
border-radius: 5px;
|
|
3927
|
+
background: color-mix(in srgb, var(--rf-accent) 7%, transparent);
|
|
3928
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--rf-accent) 70%, transparent), 0 0 0 5px color-mix(in srgb, var(--rf-accent) 14%, transparent);
|
|
3929
|
+
transition: opacity 160ms var(--rf-ease);
|
|
3641
3930
|
}
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3931
|
+
.picker-hint {
|
|
3932
|
+
position: fixed;
|
|
3933
|
+
left: 50%;
|
|
3934
|
+
bottom: calc(24px + env(safe-area-inset-bottom));
|
|
3935
|
+
transform: translateX(-50%);
|
|
3936
|
+
display: flex;
|
|
3937
|
+
align-items: center;
|
|
3938
|
+
gap: 6px;
|
|
3939
|
+
max-width: calc(100vw - 24px);
|
|
3940
|
+
padding: 5px 6px 5px 12px;
|
|
3941
|
+
font-size: 12.5px;
|
|
3942
|
+
color: var(--rf-text);
|
|
3943
|
+
background: var(--rf-bg);
|
|
3944
|
+
border-radius: var(--rf-pill);
|
|
3945
|
+
box-shadow: var(--rf-ring), var(--rf-shadow);
|
|
3946
|
+
animation: rf-fade 240ms var(--rf-ease);
|
|
3947
|
+
}
|
|
3948
|
+
.picker-instruction { display: flex; align-items: center; gap: 6px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
3949
|
+
.picker-hint kbd {
|
|
3950
|
+
padding: 1px 5px;
|
|
3951
|
+
font: inherit;
|
|
3952
|
+
font-size: 11px;
|
|
3953
|
+
color: var(--rf-text-muted);
|
|
3954
|
+
border-radius: 4px;
|
|
3955
|
+
background: var(--rf-bg-subtle);
|
|
3956
|
+
box-shadow: var(--rf-shadow-inset);
|
|
3652
3957
|
}
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3958
|
+
.picker-cancel {
|
|
3959
|
+
display: inline-flex;
|
|
3960
|
+
align-items: center;
|
|
3961
|
+
height: 40px;
|
|
3962
|
+
padding: 0 14px;
|
|
3963
|
+
border-radius: var(--rf-pill);
|
|
3964
|
+
font-size: 12.5px;
|
|
3965
|
+
font-weight: 500;
|
|
3966
|
+
white-space: nowrap;
|
|
3967
|
+
color: var(--rf-text-muted);
|
|
3656
3968
|
}
|
|
3657
|
-
|
|
3658
|
-
|
|
3969
|
+
.picker-cancel:hover { background: var(--rf-bg-hover); color: var(--rf-text); }
|
|
3970
|
+
.picker-note {
|
|
3971
|
+
position: fixed;
|
|
3972
|
+
z-index: ${REFLET_Z_INDEX.annotatorOverlay};
|
|
3973
|
+
display: flex;
|
|
3974
|
+
flex-direction: column;
|
|
3975
|
+
gap: 6px;
|
|
3976
|
+
width: 300px;
|
|
3977
|
+
max-width: calc(100vw - 24px);
|
|
3978
|
+
padding: 6px 6px 6px 8px;
|
|
3979
|
+
border-radius: 18px;
|
|
3980
|
+
animation: rf-in 180ms var(--rf-ease);
|
|
3659
3981
|
}
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
});
|
|
3671
|
-
if (!response.ok) throw new Error(`Screenshot upload failed (${response.status})`);
|
|
3672
|
-
const payload = await response.json();
|
|
3673
|
-
if (!isStorageResponse(payload)) throw new Error("Screenshot upload returned no storage id");
|
|
3674
|
-
return payload.storageId;
|
|
3675
|
-
}
|
|
3676
|
-
};
|
|
3982
|
+
.picker-note:focus-within { outline: 2px solid var(--rf-accent); outline-offset: 2px; }
|
|
3983
|
+
.picker-note-target {
|
|
3984
|
+
display: flex;
|
|
3985
|
+
align-items: center;
|
|
3986
|
+
gap: 6px;
|
|
3987
|
+
margin: 0;
|
|
3988
|
+
padding: 2px 4px 0;
|
|
3989
|
+
font-size: 11px;
|
|
3990
|
+
color: var(--rf-text-muted);
|
|
3991
|
+
white-space: nowrap;
|
|
3677
3992
|
}
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
const finishSubmission = (result) => {
|
|
3693
|
-
if (result.pendingScreenshots.length > 0) {
|
|
3694
|
-
setPendingReport(result);
|
|
3695
|
-
options.onError(options.partialUploadMessage);
|
|
3696
|
-
return;
|
|
3697
|
-
}
|
|
3698
|
-
setPendingReport(null);
|
|
3699
|
-
options.onSubmitted(result.feedbackId);
|
|
3700
|
-
};
|
|
3701
|
-
const submit = async (draft) => {
|
|
3702
|
-
if (requestRef.current) return;
|
|
3703
|
-
if (!options.client) {
|
|
3704
|
-
options.onError("Reflet is missing a publicKey. Pass one to RefletFeedback or RefletProvider.");
|
|
3705
|
-
return;
|
|
3706
|
-
}
|
|
3707
|
-
const request = {};
|
|
3708
|
-
requestRef.current = request;
|
|
3709
|
-
setActiveRequest(request);
|
|
3710
|
-
options.onError(null);
|
|
3711
|
-
try {
|
|
3712
|
-
const transport = createFeedbackTransport(options.client);
|
|
3713
|
-
const result = pendingReport ? await attachPreparedScreenshots(transport, pendingReport) : await submitWidgetFeedback(transport, draft);
|
|
3714
|
-
if (requestRef.current === request) finishSubmission(result);
|
|
3715
|
-
} catch (error) {
|
|
3716
|
-
if (requestRef.current === request) options.onError(error instanceof Error ? error.message : "Something went wrong. Please try again.");
|
|
3717
|
-
} finally {
|
|
3718
|
-
if (requestRef.current === request) {
|
|
3719
|
-
requestRef.current = null;
|
|
3720
|
-
setActiveRequest(null);
|
|
3721
|
-
}
|
|
3722
|
-
}
|
|
3723
|
-
};
|
|
3724
|
-
return {
|
|
3725
|
-
hasPendingAttachments: pendingReport !== null,
|
|
3726
|
-
isEditingDisabled: activeRequest !== null || pendingReport !== null,
|
|
3727
|
-
isSubmitting: activeRequest !== null,
|
|
3728
|
-
resetSubmission,
|
|
3729
|
-
submit
|
|
3730
|
-
};
|
|
3993
|
+
.picker-note-target strong { flex: none; font-weight: 600; color: var(--rf-accent); }
|
|
3994
|
+
.picker-note-target span { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
3995
|
+
.picker-note textarea {
|
|
3996
|
+
width: 100%;
|
|
3997
|
+
min-height: 46px;
|
|
3998
|
+
max-height: min(160px, 30dvh);
|
|
3999
|
+
margin: 0;
|
|
4000
|
+
padding: 7px 6px 0;
|
|
4001
|
+
border: 0;
|
|
4002
|
+
resize: none;
|
|
4003
|
+
background: transparent;
|
|
4004
|
+
color: var(--rf-text);
|
|
4005
|
+
font-size: 13px;
|
|
4006
|
+
line-height: 19px;
|
|
3731
4007
|
}
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
4008
|
+
.picker-note textarea:focus { outline: none; }
|
|
4009
|
+
.picker-note textarea::placeholder { color: var(--rf-text-muted); }
|
|
4010
|
+
.picker-note-actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
4011
|
+
@media (pointer: coarse) {
|
|
4012
|
+
.picker-hint { padding: 6px 8px 6px 14px; }
|
|
4013
|
+
.picker-hint kbd { display: none; }
|
|
4014
|
+
.picker-cancel { height: 44px; padding: 0 18px; font-size: 14px; }
|
|
4015
|
+
.picker-note textarea { font-size: 16px; min-height: 56px; }
|
|
3737
4016
|
}
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
return;
|
|
3778
|
-
}
|
|
3779
|
-
storeScreenshot({
|
|
3780
|
-
...request,
|
|
3781
|
-
annotations: [],
|
|
3782
|
-
context,
|
|
3783
|
-
image
|
|
3784
|
-
});
|
|
3785
|
-
}, [
|
|
3786
|
-
cancelPendingCapture,
|
|
3787
|
-
captureFailed,
|
|
3788
|
-
onError,
|
|
3789
|
-
storeScreenshot
|
|
3790
|
-
]);
|
|
3791
|
-
const takeCapture = useCallback(() => {
|
|
3792
|
-
if (!captureRequest.current) takeScreenshot({
|
|
3793
|
-
id: crypto.randomUUID(),
|
|
3794
|
-
source: "manual"
|
|
3795
|
-
});
|
|
3796
|
-
}, [takeScreenshot]);
|
|
3797
|
-
const refreshAutomatic = useCallback(() => {
|
|
3798
|
-
takeScreenshot({
|
|
3799
|
-
id: ownedDrafts.current.keys().next().value ?? captureRequest.current?.id ?? crypto.randomUUID(),
|
|
3800
|
-
source: "automatic"
|
|
3801
|
-
});
|
|
3802
|
-
}, [takeScreenshot]);
|
|
3803
|
-
const retakeCapture = useCallback((id) => {
|
|
3804
|
-
if (!captureRequest.current && ownedDrafts.current.has(id)) takeScreenshot({
|
|
3805
|
-
id,
|
|
3806
|
-
source: "manual"
|
|
3807
|
-
});
|
|
3808
|
-
}, [takeScreenshot]);
|
|
3809
|
-
const removeCapture = useCallback((id) => {
|
|
3810
|
-
if (captureRequest.current?.id === id) cancelPendingCapture();
|
|
3811
|
-
releaseDraft(ownedDrafts.current.get(id));
|
|
3812
|
-
ownedDrafts.current.delete(id);
|
|
3813
|
-
setScreenshots((previous) => previous.filter((item) => item.id !== id));
|
|
3814
|
-
setActiveId((current) => current === id ? null : current);
|
|
3815
|
-
}, [cancelPendingCapture]);
|
|
3816
|
-
const resetScreenshots = useCallback(() => {
|
|
3817
|
-
cancelPendingCapture();
|
|
3818
|
-
for (const draft of ownedDrafts.current.values()) releaseDraft(draft);
|
|
3819
|
-
ownedDrafts.current.clear();
|
|
3820
|
-
setScreenshots([]);
|
|
3821
|
-
setActiveId(null);
|
|
3822
|
-
}, [cancelPendingCapture]);
|
|
3823
|
-
const setAnnotations = useCallback((annotations) => {
|
|
3824
|
-
setScreenshots((previous) => previous.map((item) => item.id === activeId ? {
|
|
3825
|
-
...item,
|
|
3826
|
-
annotations
|
|
3827
|
-
} : item));
|
|
3828
|
-
}, [activeId]);
|
|
3829
|
-
const setSelectionComment = useCallback((id, comment) => {
|
|
3830
|
-
setScreenshots((previous) => previous.map((item) => item.id === id && item.selection ? {
|
|
3831
|
-
...item,
|
|
3832
|
-
selection: {
|
|
3833
|
-
...item.selection,
|
|
3834
|
-
comment
|
|
3835
|
-
}
|
|
3836
|
-
} : item));
|
|
3837
|
-
}, []);
|
|
3838
|
-
return {
|
|
3839
|
-
activeScreenshot: screenshots.find(({ id }) => id === activeId) ?? screenshots.at(-1) ?? null,
|
|
3840
|
-
cancelPendingCapture,
|
|
3841
|
-
pendingCapture,
|
|
3842
|
-
refreshAutomatic,
|
|
3843
|
-
removeCapture,
|
|
3844
|
-
resetScreenshots,
|
|
3845
|
-
retakeCapture,
|
|
3846
|
-
screenshots,
|
|
3847
|
-
selectScreenshot: setActiveId,
|
|
3848
|
-
setAnnotations,
|
|
3849
|
-
setSelectionComment,
|
|
3850
|
-
storeScreenshot,
|
|
3851
|
-
takeCapture
|
|
3852
|
-
};
|
|
4017
|
+
|
|
4018
|
+
`;
|
|
4019
|
+
//#endregion
|
|
4020
|
+
//#region src/feedback/ui/styles.ts
|
|
4021
|
+
const WIDGET_STYLES = `
|
|
4022
|
+
:host {
|
|
4023
|
+
--rf-offset: 20px;
|
|
4024
|
+
--rf-bg: #ffffff;
|
|
4025
|
+
--rf-bg-subtle: rgb(55 53 47 / 4%);
|
|
4026
|
+
--rf-bg-hover: rgb(55 53 47 / 7%);
|
|
4027
|
+
--rf-border: rgb(55 53 47 / 9%);
|
|
4028
|
+
--rf-hairline: rgb(15 15 15 / 10%);
|
|
4029
|
+
--rf-text: #37352f;
|
|
4030
|
+
--rf-text-muted: rgb(55 53 47 / 68%);
|
|
4031
|
+
--rf-primary: #37352f;
|
|
4032
|
+
--rf-primary-text: #ffffff;
|
|
4033
|
+
--rf-accent: #2383e2;
|
|
4034
|
+
--rf-accent-text: #ffffff;
|
|
4035
|
+
--rf-danger: #d44c47;
|
|
4036
|
+
--rf-success: #448361;
|
|
4037
|
+
--rf-swatch-base: #ffffff;
|
|
4038
|
+
--rf-spectrum: conic-gradient(#ff5757, #ffde59, #6bdb78, #4ac9fa, #8b5cf6, #ef73dc, #ff5757);
|
|
4039
|
+
--rf-radius: 20px;
|
|
4040
|
+
--rf-pill: 999px;
|
|
4041
|
+
--rf-attachment-height: 78px;
|
|
4042
|
+
--rf-width: 360px;
|
|
4043
|
+
--rf-glass: rgb(255 255 255 / 72%);
|
|
4044
|
+
--rf-glass-highlight: rgb(255 255 255 / 90%);
|
|
4045
|
+
--rf-glass-stroke: rgb(255 255 255 / 70%);
|
|
4046
|
+
--rf-radius-sm: 8px;
|
|
4047
|
+
--rf-shadow-inset: inset 0 0 0 1px var(--rf-border);
|
|
4048
|
+
--rf-ring: 0 0 0 1px rgb(15 15 15 / 4%);
|
|
4049
|
+
--rf-shadow-low: 0 1px 2px rgb(15 15 15 / 6%), 0 4px 10px -4px rgb(15 15 15 / 10%);
|
|
4050
|
+
--rf-shadow: 0 2px 5px -1px rgb(15 15 15 / 7%), 0 12px 32px -8px rgb(15 15 15 / 16%);
|
|
4051
|
+
--rf-shadow-float: 0 4px 12px -3px rgb(15 15 15 / 35%);
|
|
4052
|
+
--rf-ease: cubic-bezier(0.16, 1, 0.3, 1);
|
|
4053
|
+
color-scheme: light;
|
|
4054
|
+
all: initial;
|
|
4055
|
+
font-family: var(--rf-font, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
|
|
3853
4056
|
}
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
4057
|
+
|
|
4058
|
+
:host([data-theme="dark"]) {
|
|
4059
|
+
--rf-bg: #202020;
|
|
4060
|
+
--rf-bg-subtle: rgb(255 255 255 / 5%);
|
|
4061
|
+
--rf-bg-hover: rgb(255 255 255 / 9%);
|
|
4062
|
+
--rf-border: rgb(255 255 255 / 9%);
|
|
4063
|
+
--rf-hairline: rgb(255 255 255 / 12%);
|
|
4064
|
+
--rf-text: #ebeae8;
|
|
4065
|
+
--rf-text-muted: rgb(255 255 255 / 68%);
|
|
4066
|
+
--rf-glass: rgb(32 32 32 / 78%);
|
|
4067
|
+
--rf-glass-highlight: rgb(255 255 255 / 14%);
|
|
4068
|
+
--rf-glass-stroke: rgb(255 255 255 / 12%);
|
|
4069
|
+
--rf-primary: #f1efec;
|
|
4070
|
+
--rf-primary-text: #1c1c1b;
|
|
4071
|
+
--rf-accent: #529cca;
|
|
4072
|
+
--rf-danger: #eb5757;
|
|
4073
|
+
--rf-success: #4dab74;
|
|
4074
|
+
--rf-ring: 0 0 0 1px rgb(255 255 255 / 7%);
|
|
4075
|
+
--rf-shadow-low: 0 1px 2px rgb(0 0 0 / 30%), 0 4px 10px -4px rgb(0 0 0 / 40%);
|
|
4076
|
+
--rf-shadow: 0 2px 5px -1px rgb(0 0 0 / 35%), 0 14px 36px -10px rgb(0 0 0 / 55%);
|
|
4077
|
+
--rf-shadow-float: 0 4px 14px -3px rgb(0 0 0 / 55%);
|
|
4078
|
+
color-scheme: dark;
|
|
3858
4079
|
}
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
}) : null, [
|
|
3872
|
-
baseUrl,
|
|
3873
|
-
publicKey,
|
|
3874
|
-
user,
|
|
3875
|
-
userToken
|
|
3876
|
-
]),
|
|
3877
|
-
dismissalKey: `reflet-feedback-dismissed:${publicKey ?? "default"}:${user?.id ?? "anonymous"}`,
|
|
3878
|
-
dismissForDays: dismissalDuration(props.dismissForDays),
|
|
3879
|
-
isAnonymous: !(user || userToken)
|
|
3880
|
-
};
|
|
4080
|
+
|
|
4081
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
4082
|
+
|
|
4083
|
+
|
|
4084
|
+
.root {
|
|
4085
|
+
position: fixed;
|
|
4086
|
+
z-index: ${REFLET_Z_INDEX.widgetRoot};
|
|
4087
|
+
display: flex;
|
|
4088
|
+
align-items: flex-end;
|
|
4089
|
+
gap: 8px;
|
|
4090
|
+
font: 13px/1.45 var(--rf-font, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif);
|
|
4091
|
+
color: var(--rf-text);
|
|
3881
4092
|
}
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
refresh();
|
|
3902
|
-
return result;
|
|
3903
|
-
};
|
|
3904
|
-
history.replaceState = function replaceState(data, unused, url) {
|
|
3905
|
-
const result = originalReplace.call(history, data, unused, url);
|
|
3906
|
-
refresh();
|
|
3907
|
-
return result;
|
|
3908
|
-
};
|
|
3909
|
-
window.addEventListener("popstate", refresh);
|
|
3910
|
-
let resizeTimer;
|
|
3911
|
-
const onResize = () => {
|
|
3912
|
-
window.clearTimeout(resizeTimer);
|
|
3913
|
-
resizeTimer = window.setTimeout(refresh, RESIZE_DEBOUNCE);
|
|
3914
|
-
};
|
|
3915
|
-
window.addEventListener("resize", onResize);
|
|
3916
|
-
return () => {
|
|
3917
|
-
if (hadOwnPush) history.pushState = originalPush;
|
|
3918
|
-
else Reflect.deleteProperty(history, "pushState");
|
|
3919
|
-
if (hadOwnReplace) history.replaceState = originalReplace;
|
|
3920
|
-
else Reflect.deleteProperty(history, "replaceState");
|
|
3921
|
-
window.removeEventListener("popstate", refresh);
|
|
3922
|
-
window.clearTimeout(resizeTimer);
|
|
3923
|
-
window.removeEventListener("resize", onResize);
|
|
3924
|
-
};
|
|
3925
|
-
}, [enabled, refresh]);
|
|
4093
|
+
.root:has(.panel) { width: min(var(--rf-width), calc(var(--rf-viewport-width, 100vw) - 24px)); }
|
|
4094
|
+
.root[data-position$="right"] { right: calc(var(--rf-viewport-right, 0px) + max(12px, min(var(--rf-offset), calc((var(--rf-viewport-width, 100vw) - var(--rf-width)) / 2)))); }
|
|
4095
|
+
.root[data-position$="left"] { left: calc(var(--rf-viewport-left, 0px) + max(12px, min(var(--rf-offset), calc((var(--rf-viewport-width, 100vw) - var(--rf-width)) / 2)))); }
|
|
4096
|
+
.root[data-position^="bottom"] { bottom: calc(var(--rf-viewport-bottom, 0px) + max(var(--rf-offset), env(safe-area-inset-bottom))); }
|
|
4097
|
+
.root[data-position^="top"] { top: calc(var(--rf-viewport-top, 0px) + max(var(--rf-offset), env(safe-area-inset-top))); align-items: flex-start; }
|
|
4098
|
+
.root[data-position="bottom-right"] .panel { transform-origin: bottom right; }
|
|
4099
|
+
.root[data-position="bottom-left"] .panel { transform-origin: bottom left; }
|
|
4100
|
+
.root[data-position^="top"] .panel { transform-origin: top center; }
|
|
4101
|
+
.root[data-editing="true"] { visibility: hidden; pointer-events: none; }
|
|
4102
|
+
button, select, input, textarea { font: inherit; color: inherit; }
|
|
4103
|
+
button { margin: 0; cursor: pointer; border: 0; background: none; transition: scale 120ms var(--rf-ease); }
|
|
4104
|
+
button:disabled { cursor: not-allowed; opacity: .4; }
|
|
4105
|
+
button:active:not(:disabled) { scale: .97; }
|
|
4106
|
+
svg { flex-shrink: 0; }
|
|
4107
|
+
:focus-visible { outline: 2px solid var(--rf-accent); outline-offset: 3px; }
|
|
4108
|
+
.icon-btn, .tool {
|
|
4109
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
4110
|
+
width: 40px; height: 40px; flex: none; border-radius: var(--rf-pill); color: var(--rf-text-muted);
|
|
4111
|
+
transition: background 160ms var(--rf-ease), color 160ms var(--rf-ease), scale 120ms var(--rf-ease);
|
|
3926
4112
|
}
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
if (typeof window === "undefined") return 0;
|
|
3934
|
-
try {
|
|
3935
|
-
const value = Number.parseInt(window.localStorage.getItem(key) ?? "", 10);
|
|
3936
|
-
if (Number.isFinite(value) && value > Date.now()) return value;
|
|
3937
|
-
window.localStorage.removeItem(key);
|
|
3938
|
-
} catch {
|
|
3939
|
-
return 0;
|
|
3940
|
-
}
|
|
3941
|
-
return 0;
|
|
4113
|
+
.icon-btn:hover, .tool:hover { background: var(--rf-bg-hover); color: var(--rf-text); }
|
|
4114
|
+
.launcher {
|
|
4115
|
+
display: inline-flex; align-items: center; gap: 7px; height: 40px; padding: 0 15px;
|
|
4116
|
+
border-radius: var(--rf-pill); color: var(--rf-text);
|
|
4117
|
+
font-weight: 500;
|
|
4118
|
+
transition: transform 200ms var(--rf-ease), box-shadow 200ms var(--rf-ease), scale 120ms var(--rf-ease);
|
|
3942
4119
|
}
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
4120
|
+
.launcher:hover { transform: translateY(-2px); box-shadow: var(--rf-shadow); }
|
|
4121
|
+
.launcher-icon { display: flex; width: 15px; height: 15px; }
|
|
4122
|
+
.launcher-icon .on-close { display: none; }
|
|
4123
|
+
.floating-controls {
|
|
4124
|
+
display: flex; align-items: center; flex: none;
|
|
4125
|
+
margin-right: auto; padding-left: 3px;
|
|
4126
|
+
animation: rf-in 220ms var(--rf-ease);
|
|
3947
4127
|
}
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
4128
|
+
.drag-handle { touch-action: none; cursor: grab; }
|
|
4129
|
+
.drag-handle:active { cursor: grabbing; }
|
|
4130
|
+
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
4131
|
+
.ghost-btn { display: flex; align-items: center; gap: 8px; min-height: 40px; padding: 0 8px; border-radius: var(--rf-radius-sm); }
|
|
4132
|
+
.ghost-btn:hover { background: var(--rf-bg-hover); }
|
|
4133
|
+
.error { margin: 0; padding: 10px 14px; border-radius: var(--rf-radius); font-size: 12px; color: var(--rf-danger); }
|
|
4134
|
+
.done { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 24px; text-align: center; }
|
|
4135
|
+
.done svg { color: var(--rf-success); }
|
|
4136
|
+
.done h2 { margin: 0; font-size: 14px; font-weight: 600; }
|
|
4137
|
+
.done p { margin: 0; font-size: 12px; color: var(--rf-text-muted); }
|
|
4138
|
+
.spinner { width: 14px; height: 14px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: rf-spin 700ms linear infinite; }
|
|
4139
|
+
.hp { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
|
4140
|
+
@keyframes rf-spin { to { transform: rotate(360deg); } }
|
|
4141
|
+
@keyframes rf-in { from { opacity: 0; transform: translateY(5px) scale(.98); } }
|
|
4142
|
+
@keyframes rf-fade { from { opacity: 0; } }
|
|
4143
|
+
@media (pointer: coarse) {
|
|
4144
|
+
.icon-btn, .tool { width: 44px; height: 44px; }
|
|
4145
|
+
.launcher, .ghost-btn { min-height: 44px; }
|
|
4146
|
+
}
|
|
4147
|
+
.glass {
|
|
4148
|
+
position: relative;
|
|
4149
|
+
isolation: isolate;
|
|
4150
|
+
border: 1px solid var(--rf-glass-stroke);
|
|
4151
|
+
box-shadow: inset 0 1px 0 var(--rf-glass-highlight), var(--rf-shadow);
|
|
4152
|
+
}
|
|
4153
|
+
.glass::before {
|
|
4154
|
+
content: "";
|
|
4155
|
+
position: absolute;
|
|
4156
|
+
inset: 0;
|
|
4157
|
+
z-index: -1;
|
|
4158
|
+
border-radius: inherit;
|
|
4159
|
+
background: var(--rf-bg);
|
|
4160
|
+
pointer-events: none;
|
|
4161
|
+
}
|
|
4162
|
+
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
|
|
4163
|
+
.glass::before {
|
|
4164
|
+
background: var(--rf-glass);
|
|
4165
|
+
-webkit-backdrop-filter: blur(20px) saturate(1.6);
|
|
4166
|
+
backdrop-filter: blur(20px) saturate(1.6);
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
|
|
4170
|
+
.glass::before { background: var(--rf-bg); backdrop-filter: none; -webkit-backdrop-filter: none; }
|
|
4171
|
+
}
|
|
4172
|
+
.capture-halo { position: fixed; inset: 0; z-index: ${REFLET_Z_INDEX.captureHalo}; pointer-events: none; opacity: 0; transition: opacity 500ms var(--rf-ease); }
|
|
4173
|
+
.capture-halo[data-active="true"] { opacity: 1; transition-duration: 140ms; }
|
|
4174
|
+
.capture-halo::before { content: ""; position: absolute; inset: 0; border-radius: 20px; box-shadow: inset 14px 0 36px -16px var(--rf-accent), inset -14px 0 36px -16px var(--rf-accent), inset 0 0 4px 1px color-mix(in srgb, var(--rf-accent) 35%, transparent); }
|
|
4175
|
+
.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)); }
|
|
4176
|
+
${COMPOSER_STYLES}
|
|
4177
|
+
${ANNOTATOR_STYLES}
|
|
4178
|
+
${PICKER_STYLES}
|
|
4179
|
+
@media (prefers-reduced-motion: reduce) {
|
|
4180
|
+
*, *::before, *::after { animation: none !important; transition: none !important; }
|
|
4181
|
+
button:active:not(:disabled) { scale: 1; }
|
|
4182
|
+
}
|
|
4183
|
+
`;
|
|
4184
|
+
//#endregion
|
|
4185
|
+
//#region src/feedback/ui/shadow-portal.tsx
|
|
4186
|
+
const DARK_QUERY = "(prefers-color-scheme: dark)";
|
|
4187
|
+
function useResolvedTheme(theme) {
|
|
4188
|
+
const [systemDark, setSystemDark] = useState(false);
|
|
4189
|
+
useEffect(() => {
|
|
4190
|
+
if (theme !== "auto" || typeof window.matchMedia !== "function") return;
|
|
4191
|
+
const query = window.matchMedia(DARK_QUERY);
|
|
4192
|
+
setSystemDark(query.matches);
|
|
4193
|
+
const onChange = (event) => setSystemDark(event.matches);
|
|
4194
|
+
query.addEventListener("change", onChange);
|
|
4195
|
+
return () => query.removeEventListener("change", onChange);
|
|
4196
|
+
}, [theme]);
|
|
4197
|
+
if (theme === "auto") return systemDark ? "dark" : "light";
|
|
4198
|
+
return theme;
|
|
3961
4199
|
}
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
const [
|
|
3969
|
-
const
|
|
3970
|
-
const [isElementCapturing, setIsElementCapturing] = useState(false);
|
|
3971
|
-
const successTimer = useRef(null);
|
|
3972
|
-
const [error, setError] = useState(null);
|
|
3973
|
-
const [dismissedUntil, setDismissedUntil] = useState(() => readDismissedUntil(dismissalKey));
|
|
3974
|
-
const pickRef = useRef(0);
|
|
3975
|
-
useEffect(() => () => {
|
|
3976
|
-
pickRef.current++;
|
|
3977
|
-
if (successTimer.current) clearTimeout(successTimer.current);
|
|
3978
|
-
}, []);
|
|
3979
|
-
const consoleRef = useRef(null);
|
|
4200
|
+
/**
|
|
4201
|
+
* Hosts the widget in a shadow root: the app's CSS cannot reach the panel and
|
|
4202
|
+
* the panel's CSS cannot reach the app. Direct DOM work is unavoidable here —
|
|
4203
|
+
* React has no declarative way to create a shadow root.
|
|
4204
|
+
*/
|
|
4205
|
+
function ShadowPortal({ children, offset, primaryColor, theme }) {
|
|
4206
|
+
const [shadowRoot, setShadowRoot] = useState(null);
|
|
4207
|
+
const resolvedTheme = useResolvedTheme(theme);
|
|
3980
4208
|
useEffect(() => {
|
|
3981
|
-
|
|
3982
|
-
|
|
4209
|
+
const host = document.createElement("div");
|
|
4210
|
+
host.setAttribute(WIDGET_MARKER, "");
|
|
4211
|
+
const root = host.attachShadow({ mode: "open" });
|
|
4212
|
+
const style = document.createElement("style");
|
|
4213
|
+
style.textContent = WIDGET_STYLES;
|
|
4214
|
+
root.append(style);
|
|
4215
|
+
document.body.append(host);
|
|
4216
|
+
setShadowRoot(root);
|
|
4217
|
+
return () => host.remove();
|
|
4218
|
+
}, []);
|
|
3983
4219
|
useEffect(() => {
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
recorder.stop();
|
|
3990
|
-
};
|
|
3991
|
-
}, [props.captureConsole]);
|
|
3992
|
-
const captureFailed = props.labels?.captureFailed ?? DEFAULT_WIDGET_LABELS.captureFailed;
|
|
3993
|
-
const gallery = useScreenshotDrafts(captureFailed, setError);
|
|
3994
|
-
const { activeScreenshot, cancelPendingCapture, refreshAutomatic, resetScreenshots, storeScreenshot } = gallery;
|
|
3995
|
-
const capture = activeScreenshot?.image ?? null;
|
|
3996
|
-
const annotations = activeScreenshot?.annotations ?? [];
|
|
3997
|
-
const isCapturing = gallery.pendingCapture !== null || isElementCapturing;
|
|
3998
|
-
const submission = useFeedbackSubmission({
|
|
3999
|
-
client,
|
|
4000
|
-
onError: setError,
|
|
4001
|
-
onSubmitted: (feedbackId) => {
|
|
4002
|
-
setStep("success");
|
|
4003
|
-
props.onSubmit?.({ feedbackId });
|
|
4004
|
-
successTimer.current = setTimeout(close, SUCCESS_CLOSE_DELAY);
|
|
4005
|
-
},
|
|
4006
|
-
partialUploadMessage: props.labels?.attachmentUploadFailed ?? DEFAULT_WIDGET_LABELS.attachmentUploadFailed
|
|
4007
|
-
});
|
|
4008
|
-
const { resetSubmission } = submission;
|
|
4009
|
-
const firstCapture = gallery.pendingCapture ?? gallery.screenshots[0];
|
|
4010
|
-
const canRefreshAutomatically = gallery.screenshots.length <= 1 && firstCapture?.source === "automatic";
|
|
4011
|
-
useCaptureSync(isOpen && !submission.isEditingDisabled && step === "compose" && canRefreshAutomatically && annotations.length === 0 && !gallery.screenshots.some((draft) => draft.selection) && props.captureOnOpen !== false, refreshAutomatic);
|
|
4012
|
-
const reset = useCallback(() => {
|
|
4013
|
-
if (successTimer.current) clearTimeout(successTimer.current);
|
|
4014
|
-
setAnnotationTrigger(null);
|
|
4015
|
-
resetSubmission();
|
|
4016
|
-
pickRef.current++;
|
|
4017
|
-
setIsElementCapturing(false);
|
|
4018
|
-
resetScreenshots();
|
|
4019
|
-
setMessage("");
|
|
4020
|
-
setEmail("");
|
|
4021
|
-
setHoneypot("");
|
|
4022
|
-
setError(null);
|
|
4023
|
-
setStep("compose");
|
|
4024
|
-
}, [resetSubmission, resetScreenshots]);
|
|
4025
|
-
const close = useCallback(() => {
|
|
4026
|
-
setIsOpen(false);
|
|
4027
|
-
reset();
|
|
4028
|
-
props.onClose?.();
|
|
4029
|
-
}, [reset, props.onClose]);
|
|
4030
|
-
const dismiss = useCallback(() => {
|
|
4031
|
-
if (dismissForDays === null) return;
|
|
4032
|
-
const until = Date.now() + dismissForDays * MILLISECONDS_PER_DAY;
|
|
4033
|
-
writeDismissedUntil(dismissalKey, until);
|
|
4034
|
-
setIsOpen(false);
|
|
4035
|
-
reset();
|
|
4036
|
-
setDismissedUntil(until);
|
|
4037
|
-
props.onDismiss?.({ until });
|
|
4038
|
-
}, [
|
|
4039
|
-
dismissForDays,
|
|
4040
|
-
dismissalKey,
|
|
4041
|
-
props.onDismiss,
|
|
4042
|
-
reset
|
|
4043
|
-
]);
|
|
4044
|
-
const open = useCallback(() => {
|
|
4045
|
-
setIsOpen(true);
|
|
4046
|
-
setStep("compose");
|
|
4047
|
-
props.onOpen?.();
|
|
4048
|
-
if (props.captureOnOpen !== false) refreshAutomatic();
|
|
4049
|
-
}, [
|
|
4050
|
-
props.captureOnOpen,
|
|
4051
|
-
props.onOpen,
|
|
4052
|
-
refreshAutomatic
|
|
4053
|
-
]);
|
|
4054
|
-
const selectElement = useCallback(async (element, note = "") => {
|
|
4055
|
-
cancelPendingCapture();
|
|
4056
|
-
setIsElementCapturing(true);
|
|
4057
|
-
const picked = note ? {
|
|
4058
|
-
...buildElementSelection(element),
|
|
4059
|
-
comment: note.slice(0, MAX_SELECTION_COMMENT_LENGTH)
|
|
4060
|
-
} : buildElementSelection(element);
|
|
4061
|
-
setStep("compose");
|
|
4062
|
-
const pick = ++pickRef.current;
|
|
4063
|
-
const context = collectPageContext({ sdkVersion: SDK_VERSION });
|
|
4064
|
-
const [fresh, closeUp] = await Promise.all([captureViewport().catch(() => null), captureElementCloseUp(element).catch(() => null)]);
|
|
4065
|
-
if (pickRef.current !== pick) {
|
|
4066
|
-
releaseCapture(fresh);
|
|
4067
|
-
releaseCapture(closeUp);
|
|
4068
|
-
return;
|
|
4069
|
-
}
|
|
4070
|
-
setIsElementCapturing(false);
|
|
4071
|
-
if (!fresh) {
|
|
4072
|
-
releaseCapture(closeUp);
|
|
4073
|
-
setError(captureFailed);
|
|
4074
|
-
return;
|
|
4075
|
-
}
|
|
4076
|
-
storeScreenshot({
|
|
4077
|
-
annotations: [highlightFor(picked, fresh)],
|
|
4078
|
-
closeUp,
|
|
4079
|
-
context,
|
|
4080
|
-
id: crypto.randomUUID(),
|
|
4081
|
-
image: fresh,
|
|
4082
|
-
selectedNode: element,
|
|
4083
|
-
selection: picked,
|
|
4084
|
-
source: "manual"
|
|
4085
|
-
});
|
|
4220
|
+
const host = shadowRoot?.host;
|
|
4221
|
+
if (!(host instanceof HTMLElement)) return;
|
|
4222
|
+
host.setAttribute("data-theme", resolvedTheme);
|
|
4223
|
+
host.style.setProperty("--rf-offset", `${offset}px`);
|
|
4224
|
+
if (primaryColor) host.style.setProperty("--rf-primary", primaryColor);
|
|
4086
4225
|
}, [
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4226
|
+
shadowRoot,
|
|
4227
|
+
resolvedTheme,
|
|
4228
|
+
primaryColor,
|
|
4229
|
+
offset
|
|
4090
4230
|
]);
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
const reportedMessage = message.trim() || firstElementNote || "";
|
|
4094
|
-
const submit = () => {
|
|
4095
|
-
if (honeypot) {
|
|
4096
|
-
setStep("success");
|
|
4097
|
-
return;
|
|
4098
|
-
}
|
|
4099
|
-
setStep("compose");
|
|
4100
|
-
return submission.submit({
|
|
4101
|
-
context: {
|
|
4102
|
-
...activeScreenshot?.context ?? collectPageContext({ sdkVersion: "0.4.1" }),
|
|
4103
|
-
consoleEvents: consoleRef.current?.events(),
|
|
4104
|
-
metadata: props.metadata,
|
|
4105
|
-
selections
|
|
4106
|
-
},
|
|
4107
|
-
email,
|
|
4108
|
-
isAnonymous,
|
|
4109
|
-
message: reportedMessage,
|
|
4110
|
-
screenshots: gallery.screenshots
|
|
4111
|
-
});
|
|
4112
|
-
};
|
|
4113
|
-
const requestSubmit = () => {
|
|
4114
|
-
if (isAnonymous && email.length === 0 && !submission.hasPendingAttachments && !honeypot) {
|
|
4115
|
-
setStep("email");
|
|
4116
|
-
return;
|
|
4117
|
-
}
|
|
4118
|
-
return submit();
|
|
4119
|
-
};
|
|
4120
|
-
return {
|
|
4121
|
-
activeScreenshot,
|
|
4122
|
-
annotateScreenshot: (id, trigger) => {
|
|
4123
|
-
setAnnotationTrigger(trigger);
|
|
4124
|
-
gallery.selectScreenshot(id);
|
|
4125
|
-
setStep("annotate");
|
|
4126
|
-
},
|
|
4127
|
-
annotations,
|
|
4128
|
-
annotationTrigger,
|
|
4129
|
-
canDismiss: dismissForDays !== null,
|
|
4130
|
-
canSubmit: reportedMessage.length > 0,
|
|
4131
|
-
capture,
|
|
4132
|
-
close,
|
|
4133
|
-
dismiss,
|
|
4134
|
-
dismissForDays,
|
|
4135
|
-
email,
|
|
4136
|
-
error,
|
|
4137
|
-
hasPendingAttachments: submission.hasPendingAttachments,
|
|
4138
|
-
honeypot,
|
|
4139
|
-
isAnonymous,
|
|
4140
|
-
isCapturing,
|
|
4141
|
-
isDismissed: dismissedUntil > Date.now(),
|
|
4142
|
-
isEditingDisabled: submission.isEditingDisabled,
|
|
4143
|
-
isOpen,
|
|
4144
|
-
isSubmitting: submission.isSubmitting,
|
|
4145
|
-
message,
|
|
4146
|
-
open,
|
|
4147
|
-
pendingCapture: gallery.pendingCapture,
|
|
4148
|
-
removeCapture: gallery.removeCapture,
|
|
4149
|
-
requestSubmit,
|
|
4150
|
-
retakeCapture: gallery.retakeCapture,
|
|
4151
|
-
screenshots: gallery.screenshots,
|
|
4152
|
-
selectElement,
|
|
4153
|
-
selections,
|
|
4154
|
-
setAnnotations: gallery.setAnnotations,
|
|
4155
|
-
setEmail,
|
|
4156
|
-
setHoneypot,
|
|
4157
|
-
setMessage,
|
|
4158
|
-
setSelectionComment: gallery.setSelectionComment,
|
|
4159
|
-
setStep,
|
|
4160
|
-
step,
|
|
4161
|
-
submit,
|
|
4162
|
-
takeCapture: gallery.takeCapture
|
|
4163
|
-
};
|
|
4231
|
+
if (!shadowRoot) return null;
|
|
4232
|
+
return createPortal(children, shadowRoot);
|
|
4164
4233
|
}
|
|
4165
4234
|
//#endregion
|
|
4166
4235
|
//#region src/feedback/widget.tsx
|
|
@@ -4171,25 +4240,7 @@ function RefletFeedback(props) {
|
|
|
4171
4240
|
...props.labels
|
|
4172
4241
|
};
|
|
4173
4242
|
const state = useWidgetState(props);
|
|
4174
|
-
const {
|
|
4175
|
-
useEffect(() => {
|
|
4176
|
-
if (!(hotkey && enabled)) return;
|
|
4177
|
-
const onKeyDown = (event) => {
|
|
4178
|
-
if (matchesHotkey(event, hotkey)) {
|
|
4179
|
-
event.preventDefault();
|
|
4180
|
-
if (isOpen) close();
|
|
4181
|
-
else open();
|
|
4182
|
-
}
|
|
4183
|
-
};
|
|
4184
|
-
document.addEventListener("keydown", onKeyDown);
|
|
4185
|
-
return () => document.removeEventListener("keydown", onKeyDown);
|
|
4186
|
-
}, [
|
|
4187
|
-
close,
|
|
4188
|
-
enabled,
|
|
4189
|
-
hotkey,
|
|
4190
|
-
isOpen,
|
|
4191
|
-
open
|
|
4192
|
-
]);
|
|
4243
|
+
const { isOpen } = state;
|
|
4193
4244
|
if (!enabled || state.isDismissed) return null;
|
|
4194
4245
|
return /* @__PURE__ */ jsxs(ShadowPortal, {
|
|
4195
4246
|
offset,
|
|
@@ -4202,6 +4253,7 @@ function RefletFeedback(props) {
|
|
|
4202
4253
|
"data-active": state.isCapturing
|
|
4203
4254
|
}),
|
|
4204
4255
|
/* @__PURE__ */ jsx(FloatingWidget, {
|
|
4256
|
+
hotkey,
|
|
4205
4257
|
labels,
|
|
4206
4258
|
position,
|
|
4207
4259
|
state
|