reflet-sdk 0.4.2 → 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 +7 -0
- package/dist/feedback.d.ts.map +1 -1
- package/dist/feedback.js +1690 -1653
- 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,1726 +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 instanceof KeyboardEvent && event.key === "Escape" && !event.defaultPrevented) {
|
|
2625
|
-
setMinimized(true);
|
|
2626
|
-
requestAnimationFrame(() => launcherRef.current?.focus());
|
|
2627
|
-
}
|
|
2628
|
-
};
|
|
2629
|
-
const widgetRoot = floating.rootRef.current?.getRootNode();
|
|
2630
|
-
document.addEventListener("keydown", onKeyDown);
|
|
2631
|
-
widgetRoot?.addEventListener("keydown", onKeyDown);
|
|
2632
|
-
return () => {
|
|
2633
|
-
document.removeEventListener("keydown", onKeyDown);
|
|
2634
|
-
widgetRoot?.removeEventListener("keydown", onKeyDown);
|
|
2635
|
-
};
|
|
2636
|
-
}, [
|
|
2637
|
-
floating.rootRef,
|
|
2638
|
-
showPanel,
|
|
2639
|
-
state.step
|
|
2640
|
-
]);
|
|
2641
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
2642
|
-
className: "root",
|
|
2643
|
-
"data-editing": state.isOpen && (state.step === "picking" || state.step === "annotate"),
|
|
2644
|
-
"data-moved": Boolean(floating.position),
|
|
2645
|
-
"data-position": position,
|
|
2646
|
-
ref: floating.rootRef,
|
|
2647
|
-
children: [
|
|
2648
|
-
/* @__PURE__ */ jsx("style", { children: floating.styles }),
|
|
2649
|
-
showPanel && state.step === "compose" && state.screenshots.map(({ id, selectedNode }) => selectedNode ? /* @__PURE__ */ jsx(SelectionOutline, { node: selectedNode }, id) : null),
|
|
2650
|
-
showPanel ? /* @__PURE__ */ jsx(FeedbackPanel, {
|
|
2651
|
-
floatingControls: /* @__PURE__ */ jsxs("div", {
|
|
2652
|
-
className: "floating-controls",
|
|
2653
|
-
children: [/* @__PURE__ */ jsx("button", {
|
|
2654
|
-
"aria-label": labels.moveFeedback,
|
|
2655
|
-
className: "icon-btn drag-handle",
|
|
2656
|
-
title: labels.moveFeedback,
|
|
2657
|
-
type: "button",
|
|
2658
|
-
...floating.handleProps,
|
|
2659
|
-
children: /* @__PURE__ */ jsx(GripIcon, {})
|
|
2660
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
2661
|
-
"aria-label": labels.minimize,
|
|
2662
|
-
className: "icon-btn",
|
|
2663
|
-
onClick: minimize,
|
|
2664
|
-
title: labels.minimize,
|
|
2665
|
-
type: "button",
|
|
2666
|
-
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
2667
|
-
})]
|
|
2668
|
-
}),
|
|
2669
|
-
labels,
|
|
2670
|
-
state
|
|
2671
|
-
}) : /* @__PURE__ */ jsx(Launcher, {
|
|
2672
|
-
buttonRef: launcherRef,
|
|
2673
|
-
isOpen: false,
|
|
2674
|
-
label: state.isOpen ? labels.resume : labels.trigger,
|
|
2675
|
-
onClick: () => {
|
|
2676
|
-
setMinimized(false);
|
|
2677
|
-
if (!state.isOpen) state.open();
|
|
2678
|
-
}
|
|
2679
|
-
})
|
|
2680
|
-
]
|
|
2681
|
-
});
|
|
2682
|
-
}
|
|
2683
|
-
//#endregion
|
|
2684
|
-
//#region src/feedback/ui/floating/element-walk.ts
|
|
2685
|
-
const ARROW_SIBLING = {
|
|
2686
|
-
ArrowLeft: "previousElementSibling",
|
|
2687
|
-
ArrowRight: "nextElementSibling"
|
|
2688
|
-
};
|
|
2689
|
-
const AIM_KEYS = [
|
|
2690
|
-
"Tab",
|
|
2691
|
-
"ArrowUp",
|
|
2692
|
-
"ArrowDown",
|
|
2693
|
-
"ArrowLeft",
|
|
2694
|
-
"ArrowRight"
|
|
2695
|
-
];
|
|
2696
|
-
function isWidgetOwned(element) {
|
|
2697
|
-
return element.closest(`[${WIDGET_MARKER}]`) !== null;
|
|
2698
|
-
}
|
|
2699
|
-
function isPickable(element) {
|
|
2700
|
-
if (!element || element === document.documentElement || isWidgetOwned(element)) return false;
|
|
2701
|
-
const rect = element.getBoundingClientRect();
|
|
2702
|
-
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);
|
|
2703
2585
|
}
|
|
2704
|
-
function
|
|
2705
|
-
|
|
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
|
+
}
|
|
2706
2618
|
}
|
|
2707
|
-
function
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
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
|
+
});
|
|
2713
2624
|
}
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
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()}…`;
|
|
2718
2635
|
}
|
|
2719
|
-
function
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
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}`;
|
|
2723
2640
|
}
|
|
2724
|
-
function
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
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
|
+
};
|
|
2728
2651
|
}
|
|
2729
|
-
function
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
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
|
+
});
|
|
2736
2662
|
}
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
const LABEL_GAP = 6;
|
|
2741
|
-
const NOTE_WIDTH = 300;
|
|
2742
|
-
const NOTE_GAP = 10;
|
|
2743
|
-
const EDGE_GAP = 12;
|
|
2744
|
-
/** Events raised by the picker's own note card must reach it, never be swallowed. */
|
|
2745
|
-
function targetsWidget(event) {
|
|
2746
|
-
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));
|
|
2747
2666
|
}
|
|
2748
|
-
function
|
|
2749
|
-
|
|
2750
|
-
if (!element || isWidgetOwned(element)) return null;
|
|
2751
|
-
return element;
|
|
2667
|
+
function isStorageResponse(value) {
|
|
2668
|
+
return typeof value === "object" && value !== null && typeof Reflect.get(value, "storageId") === "string";
|
|
2752
2669
|
}
|
|
2753
|
-
function
|
|
2670
|
+
function createFeedbackTransport(client) {
|
|
2754
2671
|
return {
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
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
|
+
}
|
|
2760
2686
|
};
|
|
2761
2687
|
}
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
const
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
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
|
+
}
|
|
2775
2733
|
};
|
|
2776
2734
|
return {
|
|
2777
|
-
|
|
2778
|
-
|
|
2735
|
+
hasPendingAttachments: pendingReport !== null,
|
|
2736
|
+
isEditingDisabled: activeRequest !== null || pendingReport !== null,
|
|
2737
|
+
isSubmitting: activeRequest !== null,
|
|
2738
|
+
resetSubmission,
|
|
2739
|
+
submit
|
|
2779
2740
|
};
|
|
2780
2741
|
}
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
const [
|
|
2790
|
-
const [
|
|
2791
|
-
const
|
|
2792
|
-
const
|
|
2793
|
-
const pinnedRef = useRef(false);
|
|
2794
|
-
const [view, setView] = useState(visibleViewport);
|
|
2795
|
-
const aimRef = useRef(null);
|
|
2796
|
-
useEffect(() => onViewportChange(() => setView(visibleViewport())), []);
|
|
2797
|
-
useEffect(() => {
|
|
2798
|
-
pinnedRef.current = pinned !== null;
|
|
2799
|
-
if (pinned) noteRef.current?.focus({ preventScroll: true });
|
|
2800
|
-
}, [pinned]);
|
|
2801
|
-
useEffect(() => {
|
|
2802
|
-
aimRef.current = target?.element ?? null;
|
|
2803
|
-
}, [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);
|
|
2804
2754
|
useEffect(() => {
|
|
2805
|
-
const
|
|
2806
|
-
if (targetsWidget(event)) return;
|
|
2807
|
-
event.preventDefault();
|
|
2808
|
-
event.stopPropagation();
|
|
2809
|
-
};
|
|
2810
|
-
const aimAt = (x, y) => {
|
|
2811
|
-
const element = elementUnder(x, y);
|
|
2812
|
-
if (!element) return;
|
|
2813
|
-
setTarget((current) => current?.element === element ? {
|
|
2814
|
-
...current,
|
|
2815
|
-
rect: element.getBoundingClientRect()
|
|
2816
|
-
} : describeTarget(element));
|
|
2817
|
-
};
|
|
2818
|
-
const onMove = (event) => {
|
|
2819
|
-
if (pinnedRef.current || targetsWidget(event)) return;
|
|
2820
|
-
aimAt(event.clientX, event.clientY);
|
|
2821
|
-
};
|
|
2822
|
-
const onPointerDown = (event) => {
|
|
2823
|
-
if (targetsWidget(event)) return;
|
|
2824
|
-
swallow(event);
|
|
2825
|
-
if (!pinnedRef.current) aimAt(event.clientX, event.clientY);
|
|
2826
|
-
};
|
|
2827
|
-
const onPointerUp = (event) => {
|
|
2828
|
-
if (targetsWidget(event)) return;
|
|
2829
|
-
swallow(event);
|
|
2830
|
-
if (pinnedRef.current) return;
|
|
2831
|
-
const element = elementUnder(event.clientX, event.clientY);
|
|
2832
|
-
if (element) setPinned(describeTarget(element));
|
|
2833
|
-
};
|
|
2834
|
-
const moveAim = (key, backwards) => {
|
|
2835
|
-
const next = stepAim(aimRef.current, key, backwards);
|
|
2836
|
-
if (!next) return;
|
|
2837
|
-
next.scrollIntoView({
|
|
2838
|
-
block: "nearest",
|
|
2839
|
-
inline: "nearest"
|
|
2840
|
-
});
|
|
2841
|
-
aimRef.current = next;
|
|
2842
|
-
setTarget(describeTarget(next));
|
|
2843
|
-
};
|
|
2844
|
-
const onEscape = () => {
|
|
2845
|
-
if (pinnedRef.current) {
|
|
2846
|
-
setPinned(null);
|
|
2847
|
-
setNote("");
|
|
2848
|
-
return;
|
|
2849
|
-
}
|
|
2850
|
-
onCancel();
|
|
2851
|
-
};
|
|
2852
|
-
const onKeyDown = (event) => {
|
|
2853
|
-
if (event.key === "Escape") {
|
|
2854
|
-
swallow(event);
|
|
2855
|
-
onEscape();
|
|
2856
|
-
return;
|
|
2857
|
-
}
|
|
2858
|
-
if (pinnedRef.current || targetsWidget(event)) return;
|
|
2859
|
-
if (event.key === "Enter") {
|
|
2860
|
-
swallow(event);
|
|
2861
|
-
if (aimRef.current) setPinned(describeTarget(aimRef.current));
|
|
2862
|
-
return;
|
|
2863
|
-
}
|
|
2864
|
-
if (!AIM_KEYS.includes(event.key)) return;
|
|
2865
|
-
swallow(event);
|
|
2866
|
-
moveAim(event.key, event.shiftKey);
|
|
2867
|
-
};
|
|
2868
|
-
const onScroll = () => {
|
|
2869
|
-
setTarget((current) => current ? {
|
|
2870
|
-
...current,
|
|
2871
|
-
rect: current.element.getBoundingClientRect()
|
|
2872
|
-
} : current);
|
|
2873
|
-
setPinned((current) => current ? {
|
|
2874
|
-
...current,
|
|
2875
|
-
rect: current.element.getBoundingClientRect()
|
|
2876
|
-
} : current);
|
|
2877
|
-
};
|
|
2878
|
-
document.addEventListener("pointermove", onMove, true);
|
|
2879
|
-
document.addEventListener("pointerdown", onPointerDown, true);
|
|
2880
|
-
document.addEventListener("pointerup", onPointerUp, true);
|
|
2881
|
-
document.addEventListener("mousedown", swallow, true);
|
|
2882
|
-
document.addEventListener("mouseup", swallow, true);
|
|
2883
|
-
document.addEventListener("click", swallow, true);
|
|
2884
|
-
document.addEventListener("keydown", onKeyDown, true);
|
|
2885
|
-
window.addEventListener("scroll", onScroll, {
|
|
2886
|
-
capture: true,
|
|
2887
|
-
passive: true
|
|
2888
|
-
});
|
|
2889
|
-
const previousCursor = document.body.style.cursor;
|
|
2890
|
-
document.body.style.cursor = "crosshair";
|
|
2755
|
+
const drafts = ownedDrafts.current;
|
|
2891
2756
|
return () => {
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
document.removeEventListener("mousedown", swallow, true);
|
|
2896
|
-
document.removeEventListener("mouseup", swallow, true);
|
|
2897
|
-
document.removeEventListener("click", swallow, true);
|
|
2898
|
-
document.removeEventListener("keydown", onKeyDown, true);
|
|
2899
|
-
window.removeEventListener("scroll", onScroll, true);
|
|
2900
|
-
document.body.style.cursor = previousCursor;
|
|
2757
|
+
captureRequest.current = null;
|
|
2758
|
+
for (const draft of drafts.values()) releaseDraft(draft);
|
|
2759
|
+
drafts.clear();
|
|
2901
2760
|
};
|
|
2902
|
-
}, [
|
|
2903
|
-
const
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
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
|
|
2913
2845
|
}
|
|
2914
|
-
})
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
}),
|
|
2932
|
-
/* @__PURE__ */ jsx("textarea", {
|
|
2933
|
-
"aria-label": labels.elementNote,
|
|
2934
|
-
maxLength: MAX_SELECTION_COMMENT_LENGTH,
|
|
2935
|
-
onChange: (event) => setNote(event.target.value),
|
|
2936
|
-
onKeyDown: (event) => {
|
|
2937
|
-
if (event.key === "Enter" && !event.shiftKey) {
|
|
2938
|
-
event.preventDefault();
|
|
2939
|
-
onPick(pinned.element, note.trim());
|
|
2940
|
-
}
|
|
2941
|
-
},
|
|
2942
|
-
placeholder: labels.elementNotePlaceholder,
|
|
2943
|
-
ref: noteRef,
|
|
2944
|
-
rows: 2,
|
|
2945
|
-
value: note
|
|
2946
|
-
}),
|
|
2947
|
-
/* @__PURE__ */ jsxs("div", {
|
|
2948
|
-
className: "picker-note-actions",
|
|
2949
|
-
children: [/* @__PURE__ */ jsx("button", {
|
|
2950
|
-
className: "picker-cancel",
|
|
2951
|
-
onClick: () => {
|
|
2952
|
-
setPinned(null);
|
|
2953
|
-
setNote("");
|
|
2954
|
-
},
|
|
2955
|
-
type: "button",
|
|
2956
|
-
children: labels.pickAnother
|
|
2957
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
2958
|
-
"aria-label": labels.attachElement,
|
|
2959
|
-
className: "submit",
|
|
2960
|
-
title: labels.attachElement,
|
|
2961
|
-
type: "submit",
|
|
2962
|
-
children: /* @__PURE__ */ jsx(ArrowIcon, {})
|
|
2963
|
-
})]
|
|
2964
|
-
})
|
|
2965
|
-
]
|
|
2966
|
-
}) : /* @__PURE__ */ jsxs("div", {
|
|
2967
|
-
className: "picker-hint",
|
|
2968
|
-
children: [/* @__PURE__ */ jsxs("span", {
|
|
2969
|
-
className: "picker-instruction",
|
|
2970
|
-
children: [
|
|
2971
|
-
labels.pickElementHint,
|
|
2972
|
-
" ",
|
|
2973
|
-
/* @__PURE__ */ jsx("kbd", { children: "Tab" }),
|
|
2974
|
-
/* @__PURE__ */ jsx("kbd", { children: "Enter" }),
|
|
2975
|
-
/* @__PURE__ */ jsx("kbd", { children: "Esc" })
|
|
2976
|
-
]
|
|
2977
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
2978
|
-
className: "picker-cancel",
|
|
2979
|
-
onClick: onCancel,
|
|
2980
|
-
type: "button",
|
|
2981
|
-
children: labels.cancel
|
|
2982
|
-
})]
|
|
2983
|
-
})
|
|
2984
|
-
] });
|
|
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
|
+
};
|
|
2985
2863
|
}
|
|
2986
2864
|
//#endregion
|
|
2987
|
-
//#region src/feedback/ui/
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
.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); }
|
|
2991
|
-
.overlay::backdrop { background: transparent; }
|
|
2992
|
-
@keyframes rf-editor-in { from { opacity: 0; } }
|
|
2993
|
-
.editor { flex: 1; position: relative; display: flex; align-items: center; justify-content: center; padding: 20px; min-height: 0; container-type: size; }
|
|
2994
|
-
.editor-image { width: min(100cqw, calc(100cqh * var(--rf-capture-ratio))); aspect-ratio: var(--rf-capture-ratio); flex: none; position: relative; }
|
|
2995
|
-
.editor-visual { position: absolute; inset: 0; border-radius: var(--rf-radius); box-shadow: var(--rf-shadow); transform-origin: top left; overflow: hidden; }
|
|
2996
|
-
.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; }
|
|
2997
|
-
.editor-visual canvas { cursor: crosshair; touch-action: none; }
|
|
2998
|
-
.overlay[data-opening="true"] .editor { pointer-events: none; }
|
|
2999
|
-
.overlay[data-closing="true"] { pointer-events: none; }
|
|
3000
|
-
.overlay[data-closing="true"]::before { opacity: 0; }
|
|
3001
|
-
.overlay[data-closing="true"] .toolbar, .overlay[data-closing="true"] .element-note { opacity: 0; transform: translateY(8px); }
|
|
3002
|
-
|
|
3003
|
-
.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); }
|
|
3004
|
-
.element-note svg { flex: none; margin-top: 4px; opacity: 0.7; }
|
|
3005
|
-
.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; }
|
|
3006
|
-
.element-note textarea:focus { outline: none; }
|
|
3007
|
-
.element-note:focus-within { outline: 2px solid var(--rf-accent); outline-offset: 2px; }
|
|
3008
|
-
|
|
3009
|
-
.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)); }
|
|
3010
|
-
.toolbar .group { display: flex; align-items: center; gap: 2px; padding: 4px; border-radius: var(--rf-pill); }
|
|
3011
|
-
.tool[aria-pressed="true"], .tool[data-selected="true"] { background: var(--rf-primary); color: var(--rf-primary-text); }
|
|
3012
|
-
.color-picker { position: relative; }
|
|
3013
|
-
.color-picker summary { display: flex; justify-content: center; align-items: center; }
|
|
3014
|
-
.color-preview { width: 18px; height: 18px; border-radius: 50%; background: var(--annotation-color); box-shadow: inset 0 0 0 1px var(--rf-border); }
|
|
3015
|
-
.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); }
|
|
3016
|
-
.color-picker[data-placement="below"] .color-popover { top: calc(100% + 14px); bottom: auto; }
|
|
3017
|
-
.color-slider { flex: 1; min-width: 0; }
|
|
3018
|
-
.color-slider-control { display: flex; align-items: center; height: 40px; width: 100%; touch-action: none; user-select: none; cursor: pointer; }
|
|
3019
|
-
.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); }
|
|
3020
|
-
.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); }
|
|
3021
|
-
.color-slider-thumb[data-dragging] { scale: 1.12; }
|
|
3022
|
-
.color-slider-thumb:has(:focus-visible) { outline: 2px solid var(--rf-accent); outline-offset: 3px; }
|
|
3023
|
-
.color-spectrum { position: relative; display: flex; align-items: center; justify-content: center; flex: none; width: 40px; height: 40px; border-radius: 50%; cursor: pointer; }
|
|
3024
|
-
.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); }
|
|
3025
|
-
.color-spectrum:hover { background: var(--rf-bg-hover); }
|
|
3026
|
-
.color-spectrum button { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: inherit; }
|
|
3027
|
-
.color-spectrum button:focus-visible { outline: 2px solid var(--rf-accent); outline-offset: 3px; }
|
|
3028
|
-
.color-spectrum input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; pointer-events: none; }
|
|
3029
|
-
.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); }
|
|
3030
|
-
.drawing-menu { position: relative; }
|
|
3031
|
-
.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); }
|
|
3032
|
-
.drawing-menu[data-placement="below"] .drawing-popover { top: calc(100% + 12px); bottom: auto; }
|
|
3033
|
-
.drawing-option { display: flex; align-items: center; width: 100%; min-height: 40px; gap: 10px; padding: 0 10px; border-radius: 12px; text-align: left; }
|
|
3034
|
-
.drawing-option:hover, .drawing-option[aria-pressed="true"] { background: var(--rf-bg-hover); }
|
|
3035
|
-
.drawing-divider { height: 1px; margin: 5px; background: var(--rf-border); }
|
|
3036
|
-
.editor canvas[data-tool="text"] { cursor: text; }
|
|
3037
|
-
.text-annotation-editor { position: fixed; z-index: 3; width: 240px; padding: 8px; border-radius: var(--rf-radius); }
|
|
3038
|
-
.text-annotation-editor:focus-within { outline: 2px solid var(--rf-accent); outline-offset: 2px; }
|
|
3039
|
-
.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; }
|
|
3040
|
-
.text-annotation-editor textarea:focus { outline: none; }
|
|
3041
|
-
.text-annotation-actions { display: flex; align-items: center; justify-content: flex-end; gap: 4px; }
|
|
3042
|
-
@media (pointer: coarse) {
|
|
3043
|
-
.drawing-option { min-height: 44px; }
|
|
3044
|
-
.done-btn { width: 44px; height: 44px; }
|
|
3045
|
-
.color-popover { --rf-color-width: 232px; padding-left: 16px; }
|
|
3046
|
-
.color-slider-control, .color-spectrum { height: 44px; }
|
|
3047
|
-
.color-spectrum { width: 44px; }
|
|
3048
|
-
.color-slider-thumb, .color-spectrum::before { width: 18px; height: 18px; }
|
|
3049
|
-
.editor { padding: 8px; }
|
|
3050
|
-
.toolbar { gap: 6px; }
|
|
3051
|
-
.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;
|
|
3052
2868
|
}
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
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
|
+
};
|
|
3056
2891
|
}
|
|
3057
|
-
`;
|
|
3058
2892
|
//#endregion
|
|
3059
|
-
//#region src/feedback/ui/
|
|
3060
|
-
const
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
.
|
|
3076
|
-
.
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
.
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
.
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
.
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
.
|
|
3097
|
-
.
|
|
3098
|
-
.
|
|
3099
|
-
.
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
.screenshot-scroll-area { position: relative; min-width: 0; margin: -14px -10px -20px; }
|
|
3103
|
-
.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; }
|
|
3104
|
-
.screenshot-strip[data-overflow-x-start] { --rf-fade-start: calc(10px + min(24px, var(--scroll-area-overflow-x-start))); }
|
|
3105
|
-
.screenshot-strip[data-overflow-x-end] { --rf-fade-end: calc(10px + min(24px, var(--scroll-area-overflow-x-end))); }
|
|
3106
|
-
.screenshot-strip-content { display: flex; align-items: flex-end; gap: 8px; }
|
|
3107
|
-
.screenshot-scroll-area:has(.screenshot-strip-content:empty) { display: none; }
|
|
3108
|
-
.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; }
|
|
3109
|
-
.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); }
|
|
3110
|
-
.capture-action:first-child { border-radius: 16px 16px 5px 5px; }
|
|
3111
|
-
.capture-action:last-child { border-radius: 5px 5px 16px 16px; }
|
|
3112
|
-
.capture-action:hover:enabled { background: var(--rf-bg-hover); color: var(--rf-text); }
|
|
3113
|
-
.screenshot-scrollbar { height: 4px; margin: 0 8px 2px; opacity: 0; transition: opacity 150ms var(--rf-ease); }
|
|
3114
|
-
.screenshot-scrollbar[data-hovering], .screenshot-scrollbar[data-scrolling] { opacity: 1; }
|
|
3115
|
-
.screenshot-scroll-thumb { border-radius: var(--rf-pill); background: var(--rf-border); }
|
|
3116
|
-
.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; }
|
|
3117
|
-
.attachments:empty { display: none; }
|
|
3118
|
-
.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); }
|
|
3119
|
-
.screenshot-attachment { border: 0; }
|
|
3120
|
-
.screenshot-preview { display: block; position: relative; width: 100%; height: 100%; padding: 0; border-radius: inherit; overflow: hidden; }
|
|
3121
|
-
.screenshot-preview img, .screenshot-preview canvas { width: 100%; height: 100%; display: block; object-fit: cover; object-position: top center; }
|
|
3122
|
-
.screenshot-preview canvas { position: absolute; inset: 0; pointer-events: none; }
|
|
3123
|
-
.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); }
|
|
3124
|
-
.attachment-actions { position: absolute; right: 4px; bottom: 4px; display: flex; gap: 1px; padding: 1px; border-radius: var(--rf-pill); }
|
|
3125
|
-
.attachment-actions .icon-btn { width: 28px; height: 28px; }
|
|
3126
|
-
.attachment-actions svg { width: 14px; height: 14px; }
|
|
3127
|
-
.attachment-skeleton { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: var(--rf-text-muted); font-size: 11px; }
|
|
3128
|
-
.attachment-skeleton::after { content: ""; position: absolute; inset: 0; border-radius: inherit; background: var(--rf-bg-subtle); animation: rf-fade 800ms alternate infinite; }
|
|
3129
|
-
.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); }
|
|
3130
|
-
.selection-badge svg { width: 12px; height: 12px; }
|
|
3131
|
-
@media (pointer: coarse) {
|
|
3132
|
-
.message-row { border-radius: 28px; }
|
|
3133
|
-
.capture-slot { width: 64px; }
|
|
3134
|
-
.message-row textarea { font-size: 16px; }
|
|
3135
|
-
.submit { width: 44px; height: 44px; }
|
|
3136
|
-
:host { --rf-attachment-height: 96px; }
|
|
3137
|
-
.attachment-actions { right: 5px; bottom: 5px; }
|
|
3138
|
-
.attachment-actions .icon-btn { width: 34px; height: 34px; }
|
|
3139
|
-
.selection-badge { width: 24px; height: 24px; }
|
|
3140
|
-
.email-field input { font-size: 16px; }
|
|
3141
|
-
.dismiss-btn { min-height: 44px; }
|
|
3142
|
-
}
|
|
3143
|
-
@media (max-width: 480px) {
|
|
3144
|
-
.email-scrim { display: block; position: fixed; inset: 0; background: rgb(0 0 0 / 40%); animation: rf-fade 200ms var(--rf-ease); }
|
|
3145
|
-
.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); }
|
|
3146
|
-
.email-prompt h2 { font-size: 17px; }
|
|
3147
|
-
.email-prompt p { font-size: 13px; }
|
|
3148
|
-
.email-prompt-actions { flex-direction: column-reverse; align-items: stretch; gap: 8px; }
|
|
3149
|
-
.email-prompt-actions .ghost-btn, .email-prompt-actions .primary-btn { justify-content: center; min-height: 48px; }
|
|
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]);
|
|
3150
2936
|
}
|
|
3151
|
-
@keyframes rf-drawer-in { from { transform: translateY(100%); } }
|
|
3152
|
-
`;
|
|
3153
2937
|
//#endregion
|
|
3154
|
-
//#region src/feedback/ui/
|
|
3155
|
-
const
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
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;
|
|
3166
2952
|
}
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
left: 0;
|
|
3172
|
-
pointer-events: none;
|
|
3173
|
-
display: inline-flex;
|
|
3174
|
-
align-items: center;
|
|
3175
|
-
gap: 6px;
|
|
3176
|
-
max-width: min(420px, 90vw);
|
|
3177
|
-
height: 24px;
|
|
3178
|
-
padding: 0 8px;
|
|
3179
|
-
font-size: 11.5px;
|
|
3180
|
-
font-weight: 500;
|
|
3181
|
-
color: var(--rf-accent-text);
|
|
3182
|
-
background: var(--rf-accent);
|
|
3183
|
-
border-radius: 6px;
|
|
3184
|
-
box-shadow: var(--rf-shadow-float);
|
|
3185
|
-
white-space: nowrap;
|
|
3186
|
-
overflow: hidden;
|
|
3187
|
-
will-change: translate;
|
|
3188
|
-
transition: translate 130ms var(--rf-ease);
|
|
3189
|
-
}
|
|
3190
|
-
.picker-label strong { flex: none; font-weight: 600; }
|
|
3191
|
-
.picker-label span { min-width: 0; overflow: hidden; text-overflow: ellipsis; opacity: 0.75; }
|
|
3192
|
-
.selection-outline {
|
|
3193
|
-
position: fixed;
|
|
3194
|
-
top: 0;
|
|
3195
|
-
left: 0;
|
|
3196
|
-
pointer-events: none;
|
|
3197
|
-
border-radius: 5px;
|
|
3198
|
-
background: color-mix(in srgb, var(--rf-accent) 7%, transparent);
|
|
3199
|
-
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);
|
|
3200
|
-
transition: opacity 160ms var(--rf-ease);
|
|
3201
|
-
}
|
|
3202
|
-
.picker-hint {
|
|
3203
|
-
position: fixed;
|
|
3204
|
-
left: 50%;
|
|
3205
|
-
bottom: calc(24px + env(safe-area-inset-bottom));
|
|
3206
|
-
transform: translateX(-50%);
|
|
3207
|
-
display: flex;
|
|
3208
|
-
align-items: center;
|
|
3209
|
-
gap: 6px;
|
|
3210
|
-
max-width: calc(100vw - 24px);
|
|
3211
|
-
padding: 5px 6px 5px 12px;
|
|
3212
|
-
font-size: 12.5px;
|
|
3213
|
-
color: var(--rf-text);
|
|
3214
|
-
background: var(--rf-bg);
|
|
3215
|
-
border-radius: var(--rf-pill);
|
|
3216
|
-
box-shadow: var(--rf-ring), var(--rf-shadow);
|
|
3217
|
-
animation: rf-fade 240ms var(--rf-ease);
|
|
3218
|
-
}
|
|
3219
|
-
.picker-instruction { display: flex; align-items: center; gap: 6px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
3220
|
-
.picker-hint kbd {
|
|
3221
|
-
padding: 1px 5px;
|
|
3222
|
-
font: inherit;
|
|
3223
|
-
font-size: 11px;
|
|
3224
|
-
color: var(--rf-text-muted);
|
|
3225
|
-
border-radius: 4px;
|
|
3226
|
-
background: var(--rf-bg-subtle);
|
|
3227
|
-
box-shadow: var(--rf-shadow-inset);
|
|
3228
|
-
}
|
|
3229
|
-
.picker-cancel {
|
|
3230
|
-
display: inline-flex;
|
|
3231
|
-
align-items: center;
|
|
3232
|
-
height: 40px;
|
|
3233
|
-
padding: 0 14px;
|
|
3234
|
-
border-radius: var(--rf-pill);
|
|
3235
|
-
font-size: 12.5px;
|
|
3236
|
-
font-weight: 500;
|
|
3237
|
-
white-space: nowrap;
|
|
3238
|
-
color: var(--rf-text-muted);
|
|
3239
|
-
}
|
|
3240
|
-
.picker-cancel:hover { background: var(--rf-bg-hover); color: var(--rf-text); }
|
|
3241
|
-
.picker-note {
|
|
3242
|
-
position: fixed;
|
|
3243
|
-
z-index: ${REFLET_Z_INDEX.annotatorOverlay};
|
|
3244
|
-
display: flex;
|
|
3245
|
-
flex-direction: column;
|
|
3246
|
-
gap: 6px;
|
|
3247
|
-
width: 300px;
|
|
3248
|
-
max-width: calc(100vw - 24px);
|
|
3249
|
-
padding: 6px 6px 6px 8px;
|
|
3250
|
-
border-radius: 18px;
|
|
3251
|
-
animation: rf-in 180ms var(--rf-ease);
|
|
3252
|
-
}
|
|
3253
|
-
.picker-note:focus-within { outline: 2px solid var(--rf-accent); outline-offset: 2px; }
|
|
3254
|
-
.picker-note-target {
|
|
3255
|
-
display: flex;
|
|
3256
|
-
align-items: center;
|
|
3257
|
-
gap: 6px;
|
|
3258
|
-
margin: 0;
|
|
3259
|
-
padding: 2px 4px 0;
|
|
3260
|
-
font-size: 11px;
|
|
3261
|
-
color: var(--rf-text-muted);
|
|
3262
|
-
white-space: nowrap;
|
|
2953
|
+
function writeDismissedUntil(key, until) {
|
|
2954
|
+
try {
|
|
2955
|
+
window.localStorage.setItem(key, String(until));
|
|
2956
|
+
} catch {}
|
|
3263
2957
|
}
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
.
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
line-height: 19px;
|
|
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;
|
|
3278
2971
|
}
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
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
|
+
};
|
|
3287
3174
|
}
|
|
3288
|
-
|
|
3289
|
-
`;
|
|
3290
3175
|
//#endregion
|
|
3291
|
-
//#region src/feedback/ui/
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
--rf-pill: 999px;
|
|
3312
|
-
--rf-attachment-height: 78px;
|
|
3313
|
-
--rf-width: 360px;
|
|
3314
|
-
--rf-glass: rgb(255 255 255 / 72%);
|
|
3315
|
-
--rf-glass-highlight: rgb(255 255 255 / 90%);
|
|
3316
|
-
--rf-glass-stroke: rgb(255 255 255 / 70%);
|
|
3317
|
-
--rf-radius-sm: 8px;
|
|
3318
|
-
--rf-shadow-inset: inset 0 0 0 1px var(--rf-border);
|
|
3319
|
-
--rf-ring: 0 0 0 1px rgb(15 15 15 / 4%);
|
|
3320
|
-
--rf-shadow-low: 0 1px 2px rgb(15 15 15 / 6%), 0 4px 10px -4px rgb(15 15 15 / 10%);
|
|
3321
|
-
--rf-shadow: 0 2px 5px -1px rgb(15 15 15 / 7%), 0 12px 32px -8px rgb(15 15 15 / 16%);
|
|
3322
|
-
--rf-shadow-float: 0 4px 12px -3px rgb(15 15 15 / 35%);
|
|
3323
|
-
--rf-ease: cubic-bezier(0.16, 1, 0.3, 1);
|
|
3324
|
-
color-scheme: light;
|
|
3325
|
-
all: initial;
|
|
3326
|
-
font-family: var(--rf-font, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
|
|
3327
|
-
}
|
|
3328
|
-
|
|
3329
|
-
:host([data-theme="dark"]) {
|
|
3330
|
-
--rf-bg: #202020;
|
|
3331
|
-
--rf-bg-subtle: rgb(255 255 255 / 5%);
|
|
3332
|
-
--rf-bg-hover: rgb(255 255 255 / 9%);
|
|
3333
|
-
--rf-border: rgb(255 255 255 / 9%);
|
|
3334
|
-
--rf-hairline: rgb(255 255 255 / 12%);
|
|
3335
|
-
--rf-text: #ebeae8;
|
|
3336
|
-
--rf-text-muted: rgb(255 255 255 / 68%);
|
|
3337
|
-
--rf-glass: rgb(32 32 32 / 78%);
|
|
3338
|
-
--rf-glass-highlight: rgb(255 255 255 / 14%);
|
|
3339
|
-
--rf-glass-stroke: rgb(255 255 255 / 12%);
|
|
3340
|
-
--rf-primary: #f1efec;
|
|
3341
|
-
--rf-primary-text: #1c1c1b;
|
|
3342
|
-
--rf-accent: #529cca;
|
|
3343
|
-
--rf-danger: #eb5757;
|
|
3344
|
-
--rf-success: #4dab74;
|
|
3345
|
-
--rf-ring: 0 0 0 1px rgb(255 255 255 / 7%);
|
|
3346
|
-
--rf-shadow-low: 0 1px 2px rgb(0 0 0 / 30%), 0 4px 10px -4px rgb(0 0 0 / 40%);
|
|
3347
|
-
--rf-shadow: 0 2px 5px -1px rgb(0 0 0 / 35%), 0 14px 36px -10px rgb(0 0 0 / 55%);
|
|
3348
|
-
--rf-shadow-float: 0 4px 14px -3px rgb(0 0 0 / 55%);
|
|
3349
|
-
color-scheme: dark;
|
|
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
|
+
};
|
|
3350
3196
|
}
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
gap: 8px;
|
|
3361
|
-
font: 13px/1.45 var(--rf-font, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif);
|
|
3362
|
-
color: var(--rf-text);
|
|
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
|
+
};
|
|
3363
3206
|
}
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
:
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
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
|
+
};
|
|
3383
3234
|
}
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
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
|
+
};
|
|
3390
3323
|
}
|
|
3391
|
-
|
|
3392
|
-
.
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
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
|
+
});
|
|
3398
3411
|
}
|
|
3399
|
-
|
|
3400
|
-
.
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
@media (pointer: coarse) {
|
|
3415
|
-
.icon-btn, .tool { width: 44px; height: 44px; }
|
|
3416
|
-
.launcher, .ghost-btn { min-height: 44px; }
|
|
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;
|
|
3417
3427
|
}
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
box-shadow: inset 0 1px 0 var(--rf-glass-highlight), var(--rf-shadow);
|
|
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
|
-
position: absolute;
|
|
3427
|
-
inset: 0;
|
|
3428
|
-
z-index: -1;
|
|
3429
|
-
border-radius: inherit;
|
|
3430
|
-
background: var(--rf-bg);
|
|
3431
|
-
pointer-events: none;
|
|
3433
|
+
function pickableElements() {
|
|
3434
|
+
return Array.from(document.body.querySelectorAll("*")).filter(isPickable);
|
|
3432
3435
|
}
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
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;
|
|
3439
3442
|
}
|
|
3440
|
-
|
|
3441
|
-
|
|
3443
|
+
function climb(from) {
|
|
3444
|
+
let candidate = from.parentElement;
|
|
3445
|
+
while (candidate && !isPickable(candidate)) candidate = candidate.parentElement;
|
|
3446
|
+
return candidate;
|
|
3442
3447
|
}
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
${COMPOSER_STYLES}
|
|
3448
|
-
${ANNOTATOR_STYLES}
|
|
3449
|
-
${PICKER_STYLES}
|
|
3450
|
-
@media (prefers-reduced-motion: reduce) {
|
|
3451
|
-
*, *::before, *::after { animation: none !important; transition: none !important; }
|
|
3452
|
-
button:active:not(:disabled) { scale: 1; }
|
|
3448
|
+
function descend(from) {
|
|
3449
|
+
let candidate = from.firstElementChild;
|
|
3450
|
+
while (candidate && !isPickable(candidate)) candidate = candidate.nextElementSibling;
|
|
3451
|
+
return candidate;
|
|
3453
3452
|
}
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
const KEY_EVENTS = [
|
|
3459
|
-
"keydown",
|
|
3460
|
-
"keyup",
|
|
3461
|
-
"keypress"
|
|
3462
|
-
];
|
|
3463
|
-
function useResolvedTheme(theme) {
|
|
3464
|
-
const [systemDark, setSystemDark] = useState(false);
|
|
3465
|
-
useEffect(() => {
|
|
3466
|
-
if (theme !== "auto" || typeof window.matchMedia !== "function") return;
|
|
3467
|
-
const query = window.matchMedia(DARK_QUERY);
|
|
3468
|
-
setSystemDark(query.matches);
|
|
3469
|
-
const onChange = (event) => setSystemDark(event.matches);
|
|
3470
|
-
query.addEventListener("change", onChange);
|
|
3471
|
-
return () => query.removeEventListener("change", onChange);
|
|
3472
|
-
}, [theme]);
|
|
3473
|
-
if (theme === "auto") return systemDark ? "dark" : "light";
|
|
3474
|
-
return theme;
|
|
3453
|
+
function sibling(from, direction) {
|
|
3454
|
+
let candidate = from[direction];
|
|
3455
|
+
while (candidate && !isPickable(candidate)) candidate = candidate[direction];
|
|
3456
|
+
return candidate;
|
|
3475
3457
|
}
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
const resolvedTheme = useResolvedTheme(theme);
|
|
3484
|
-
useEffect(() => {
|
|
3485
|
-
const host = document.createElement("div");
|
|
3486
|
-
host.setAttribute(WIDGET_MARKER, "");
|
|
3487
|
-
const root = host.attachShadow({ mode: "open" });
|
|
3488
|
-
for (const type of KEY_EVENTS) root.addEventListener(type, (event) => event.stopPropagation());
|
|
3489
|
-
const style = document.createElement("style");
|
|
3490
|
-
style.textContent = WIDGET_STYLES;
|
|
3491
|
-
root.append(style);
|
|
3492
|
-
document.body.append(host);
|
|
3493
|
-
setShadowRoot(root);
|
|
3494
|
-
return () => host.remove();
|
|
3495
|
-
}, []);
|
|
3496
|
-
useEffect(() => {
|
|
3497
|
-
const host = shadowRoot?.host;
|
|
3498
|
-
if (!(host instanceof HTMLElement)) return;
|
|
3499
|
-
host.setAttribute("data-theme", resolvedTheme);
|
|
3500
|
-
host.style.setProperty("--rf-offset", `${offset}px`);
|
|
3501
|
-
if (primaryColor) host.style.setProperty("--rf-primary", primaryColor);
|
|
3502
|
-
}, [
|
|
3503
|
-
shadowRoot,
|
|
3504
|
-
resolvedTheme,
|
|
3505
|
-
primaryColor,
|
|
3506
|
-
offset
|
|
3507
|
-
]);
|
|
3508
|
-
if (!shadowRoot) return null;
|
|
3509
|
-
return createPortal(children, shadowRoot);
|
|
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;
|
|
3510
3465
|
}
|
|
3511
3466
|
//#endregion
|
|
3512
|
-
//#region src/feedback/
|
|
3513
|
-
const
|
|
3514
|
-
const
|
|
3515
|
-
const
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
if (
|
|
3521
|
-
|
|
3522
|
-
} catch {
|
|
3523
|
-
return "[unserializable]";
|
|
3524
|
-
}
|
|
3525
|
-
if (typeof value === "function") return "[function]";
|
|
3526
|
-
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;
|
|
3527
3477
|
}
|
|
3528
|
-
function
|
|
3529
|
-
|
|
3530
|
-
|
|
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
|
+
};
|
|
3531
3486
|
}
|
|
3532
3487
|
/**
|
|
3533
|
-
*
|
|
3534
|
-
*
|
|
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.
|
|
3535
3492
|
*/
|
|
3536
|
-
function
|
|
3537
|
-
const
|
|
3538
|
-
const
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
level,
|
|
3544
|
-
message,
|
|
3545
|
-
timestamp: Date.now()
|
|
3546
|
-
});
|
|
3547
|
-
if (buffer.length > limit) buffer.splice(0, buffer.length - limit);
|
|
3548
|
-
};
|
|
3549
|
-
const originals = /* @__PURE__ */ new Map();
|
|
3550
|
-
const wrappers = /* @__PURE__ */ new Map();
|
|
3551
|
-
for (const method of RECORDED_METHODS) {
|
|
3552
|
-
const original = console[method];
|
|
3553
|
-
const wrapper = (...args) => {
|
|
3554
|
-
push(method, formatMessage(args));
|
|
3555
|
-
Reflect.apply(original, console, args);
|
|
3556
|
-
};
|
|
3557
|
-
originals.set(method, original);
|
|
3558
|
-
wrappers.set(method, wrapper);
|
|
3559
|
-
console[method] = wrapper;
|
|
3560
|
-
}
|
|
3561
|
-
const onError = (event) => {
|
|
3562
|
-
const where = event.filename ? ` (${event.filename}:${event.lineno}:${event.colno})` : "";
|
|
3563
|
-
push("error", `${event.message}${where}`);
|
|
3564
|
-
};
|
|
3565
|
-
const onRejection = (event) => {
|
|
3566
|
-
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
|
|
3567
3500
|
};
|
|
3568
|
-
window.addEventListener("error", onError);
|
|
3569
|
-
window.addEventListener("unhandledrejection", onRejection);
|
|
3570
3501
|
return {
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
if (!active) return;
|
|
3574
|
-
active = false;
|
|
3575
|
-
buffer.length = 0;
|
|
3576
|
-
for (const method of RECORDED_METHODS) {
|
|
3577
|
-
const original = originals.get(method);
|
|
3578
|
-
if (original && console[method] === wrappers.get(method)) console[method] = original;
|
|
3579
|
-
}
|
|
3580
|
-
window.removeEventListener("error", onError);
|
|
3581
|
-
window.removeEventListener("unhandledrejection", onRejection);
|
|
3582
|
-
}
|
|
3502
|
+
bottom: window.innerHeight - visibleTop + NOTE_GAP,
|
|
3503
|
+
left
|
|
3583
3504
|
};
|
|
3584
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
|
+
`;
|
|
3585
3787
|
//#endregion
|
|
3586
|
-
//#region src/feedback/
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
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;
|
|
3590
3798
|
}
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
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; }
|
|
3623
3871
|
}
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
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; }
|
|
3629
3879
|
}
|
|
3880
|
+
@keyframes rf-drawer-in { from { transform: translateY(100%); } }
|
|
3881
|
+
`;
|
|
3630
3882
|
//#endregion
|
|
3631
|
-
//#region src/feedback/
|
|
3632
|
-
const
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
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);
|
|
3640
3895
|
}
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
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);
|
|
3645
3918
|
}
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
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);
|
|
3656
3930
|
}
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
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);
|
|
3667
3957
|
}
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
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);
|
|
3671
3968
|
}
|
|
3672
|
-
|
|
3673
|
-
|
|
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);
|
|
3674
3981
|
}
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
});
|
|
3686
|
-
if (!response.ok) throw new Error(`Screenshot upload failed (${response.status})`);
|
|
3687
|
-
const payload = await response.json();
|
|
3688
|
-
if (!isStorageResponse(payload)) throw new Error("Screenshot upload returned no storage id");
|
|
3689
|
-
return payload.storageId;
|
|
3690
|
-
}
|
|
3691
|
-
};
|
|
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;
|
|
3692
3992
|
}
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
const finishSubmission = (result) => {
|
|
3708
|
-
if (result.pendingScreenshots.length > 0) {
|
|
3709
|
-
setPendingReport(result);
|
|
3710
|
-
options.onError(options.partialUploadMessage);
|
|
3711
|
-
return;
|
|
3712
|
-
}
|
|
3713
|
-
setPendingReport(null);
|
|
3714
|
-
options.onSubmitted(result.feedbackId);
|
|
3715
|
-
};
|
|
3716
|
-
const submit = async (draft) => {
|
|
3717
|
-
if (requestRef.current) return;
|
|
3718
|
-
if (!options.client) {
|
|
3719
|
-
options.onError("Reflet is missing a publicKey. Pass one to RefletFeedback or RefletProvider.");
|
|
3720
|
-
return;
|
|
3721
|
-
}
|
|
3722
|
-
const request = {};
|
|
3723
|
-
requestRef.current = request;
|
|
3724
|
-
setActiveRequest(request);
|
|
3725
|
-
options.onError(null);
|
|
3726
|
-
try {
|
|
3727
|
-
const transport = createFeedbackTransport(options.client);
|
|
3728
|
-
const result = pendingReport ? await attachPreparedScreenshots(transport, pendingReport) : await submitWidgetFeedback(transport, draft);
|
|
3729
|
-
if (requestRef.current === request) finishSubmission(result);
|
|
3730
|
-
} catch (error) {
|
|
3731
|
-
if (requestRef.current === request) options.onError(error instanceof Error ? error.message : "Something went wrong. Please try again.");
|
|
3732
|
-
} finally {
|
|
3733
|
-
if (requestRef.current === request) {
|
|
3734
|
-
requestRef.current = null;
|
|
3735
|
-
setActiveRequest(null);
|
|
3736
|
-
}
|
|
3737
|
-
}
|
|
3738
|
-
};
|
|
3739
|
-
return {
|
|
3740
|
-
hasPendingAttachments: pendingReport !== null,
|
|
3741
|
-
isEditingDisabled: activeRequest !== null || pendingReport !== null,
|
|
3742
|
-
isSubmitting: activeRequest !== null,
|
|
3743
|
-
resetSubmission,
|
|
3744
|
-
submit
|
|
3745
|
-
};
|
|
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;
|
|
3746
4007
|
}
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
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; }
|
|
3752
4016
|
}
|
|
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
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
return;
|
|
3793
|
-
}
|
|
3794
|
-
storeScreenshot({
|
|
3795
|
-
...request,
|
|
3796
|
-
annotations: [],
|
|
3797
|
-
context,
|
|
3798
|
-
image
|
|
3799
|
-
});
|
|
3800
|
-
}, [
|
|
3801
|
-
cancelPendingCapture,
|
|
3802
|
-
captureFailed,
|
|
3803
|
-
onError,
|
|
3804
|
-
storeScreenshot
|
|
3805
|
-
]);
|
|
3806
|
-
const takeCapture = useCallback(() => {
|
|
3807
|
-
if (!captureRequest.current) takeScreenshot({
|
|
3808
|
-
id: crypto.randomUUID(),
|
|
3809
|
-
source: "manual"
|
|
3810
|
-
});
|
|
3811
|
-
}, [takeScreenshot]);
|
|
3812
|
-
const refreshAutomatic = useCallback(() => {
|
|
3813
|
-
takeScreenshot({
|
|
3814
|
-
id: ownedDrafts.current.keys().next().value ?? captureRequest.current?.id ?? crypto.randomUUID(),
|
|
3815
|
-
source: "automatic"
|
|
3816
|
-
});
|
|
3817
|
-
}, [takeScreenshot]);
|
|
3818
|
-
const retakeCapture = useCallback((id) => {
|
|
3819
|
-
if (!captureRequest.current && ownedDrafts.current.has(id)) takeScreenshot({
|
|
3820
|
-
id,
|
|
3821
|
-
source: "manual"
|
|
3822
|
-
});
|
|
3823
|
-
}, [takeScreenshot]);
|
|
3824
|
-
const removeCapture = useCallback((id) => {
|
|
3825
|
-
if (captureRequest.current?.id === id) cancelPendingCapture();
|
|
3826
|
-
releaseDraft(ownedDrafts.current.get(id));
|
|
3827
|
-
ownedDrafts.current.delete(id);
|
|
3828
|
-
setScreenshots((previous) => previous.filter((item) => item.id !== id));
|
|
3829
|
-
setActiveId((current) => current === id ? null : current);
|
|
3830
|
-
}, [cancelPendingCapture]);
|
|
3831
|
-
const resetScreenshots = useCallback(() => {
|
|
3832
|
-
cancelPendingCapture();
|
|
3833
|
-
for (const draft of ownedDrafts.current.values()) releaseDraft(draft);
|
|
3834
|
-
ownedDrafts.current.clear();
|
|
3835
|
-
setScreenshots([]);
|
|
3836
|
-
setActiveId(null);
|
|
3837
|
-
}, [cancelPendingCapture]);
|
|
3838
|
-
const setAnnotations = useCallback((annotations) => {
|
|
3839
|
-
setScreenshots((previous) => previous.map((item) => item.id === activeId ? {
|
|
3840
|
-
...item,
|
|
3841
|
-
annotations
|
|
3842
|
-
} : item));
|
|
3843
|
-
}, [activeId]);
|
|
3844
|
-
const setSelectionComment = useCallback((id, comment) => {
|
|
3845
|
-
setScreenshots((previous) => previous.map((item) => item.id === id && item.selection ? {
|
|
3846
|
-
...item,
|
|
3847
|
-
selection: {
|
|
3848
|
-
...item.selection,
|
|
3849
|
-
comment
|
|
3850
|
-
}
|
|
3851
|
-
} : item));
|
|
3852
|
-
}, []);
|
|
3853
|
-
return {
|
|
3854
|
-
activeScreenshot: screenshots.find(({ id }) => id === activeId) ?? screenshots.at(-1) ?? null,
|
|
3855
|
-
cancelPendingCapture,
|
|
3856
|
-
pendingCapture,
|
|
3857
|
-
refreshAutomatic,
|
|
3858
|
-
removeCapture,
|
|
3859
|
-
resetScreenshots,
|
|
3860
|
-
retakeCapture,
|
|
3861
|
-
screenshots,
|
|
3862
|
-
selectScreenshot: setActiveId,
|
|
3863
|
-
setAnnotations,
|
|
3864
|
-
setSelectionComment,
|
|
3865
|
-
storeScreenshot,
|
|
3866
|
-
takeCapture
|
|
3867
|
-
};
|
|
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);
|
|
3868
4056
|
}
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
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;
|
|
3873
4079
|
}
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
}) : null, [
|
|
3887
|
-
baseUrl,
|
|
3888
|
-
publicKey,
|
|
3889
|
-
user,
|
|
3890
|
-
userToken
|
|
3891
|
-
]),
|
|
3892
|
-
dismissalKey: `reflet-feedback-dismissed:${publicKey ?? "default"}:${user?.id ?? "anonymous"}`,
|
|
3893
|
-
dismissForDays: dismissalDuration(props.dismissForDays),
|
|
3894
|
-
isAnonymous: !(user || userToken)
|
|
3895
|
-
};
|
|
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);
|
|
3896
4092
|
}
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
refresh();
|
|
3917
|
-
return result;
|
|
3918
|
-
};
|
|
3919
|
-
history.replaceState = function replaceState(data, unused, url) {
|
|
3920
|
-
const result = originalReplace.call(history, data, unused, url);
|
|
3921
|
-
refresh();
|
|
3922
|
-
return result;
|
|
3923
|
-
};
|
|
3924
|
-
window.addEventListener("popstate", refresh);
|
|
3925
|
-
let resizeTimer;
|
|
3926
|
-
const onResize = () => {
|
|
3927
|
-
window.clearTimeout(resizeTimer);
|
|
3928
|
-
resizeTimer = window.setTimeout(refresh, RESIZE_DEBOUNCE);
|
|
3929
|
-
};
|
|
3930
|
-
window.addEventListener("resize", onResize);
|
|
3931
|
-
return () => {
|
|
3932
|
-
if (hadOwnPush) history.pushState = originalPush;
|
|
3933
|
-
else Reflect.deleteProperty(history, "pushState");
|
|
3934
|
-
if (hadOwnReplace) history.replaceState = originalReplace;
|
|
3935
|
-
else Reflect.deleteProperty(history, "replaceState");
|
|
3936
|
-
window.removeEventListener("popstate", refresh);
|
|
3937
|
-
window.clearTimeout(resizeTimer);
|
|
3938
|
-
window.removeEventListener("resize", onResize);
|
|
3939
|
-
};
|
|
3940
|
-
}, [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);
|
|
3941
4112
|
}
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
if (typeof window === "undefined") return 0;
|
|
3949
|
-
try {
|
|
3950
|
-
const value = Number.parseInt(window.localStorage.getItem(key) ?? "", 10);
|
|
3951
|
-
if (Number.isFinite(value) && value > Date.now()) return value;
|
|
3952
|
-
window.localStorage.removeItem(key);
|
|
3953
|
-
} catch {
|
|
3954
|
-
return 0;
|
|
3955
|
-
}
|
|
3956
|
-
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);
|
|
3957
4119
|
}
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
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);
|
|
3962
4127
|
}
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
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;
|
|
3976
4199
|
}
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
const [
|
|
3984
|
-
const
|
|
3985
|
-
const [isElementCapturing, setIsElementCapturing] = useState(false);
|
|
3986
|
-
const successTimer = useRef(null);
|
|
3987
|
-
const [error, setError] = useState(null);
|
|
3988
|
-
const [dismissedUntil, setDismissedUntil] = useState(() => readDismissedUntil(dismissalKey));
|
|
3989
|
-
const pickRef = useRef(0);
|
|
3990
|
-
useEffect(() => () => {
|
|
3991
|
-
pickRef.current++;
|
|
3992
|
-
if (successTimer.current) clearTimeout(successTimer.current);
|
|
3993
|
-
}, []);
|
|
3994
|
-
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);
|
|
3995
4208
|
useEffect(() => {
|
|
3996
|
-
|
|
3997
|
-
|
|
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
|
+
}, []);
|
|
3998
4219
|
useEffect(() => {
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
recorder.stop();
|
|
4005
|
-
};
|
|
4006
|
-
}, [props.captureConsole]);
|
|
4007
|
-
const captureFailed = props.labels?.captureFailed ?? DEFAULT_WIDGET_LABELS.captureFailed;
|
|
4008
|
-
const gallery = useScreenshotDrafts(captureFailed, setError);
|
|
4009
|
-
const { activeScreenshot, cancelPendingCapture, refreshAutomatic, resetScreenshots, storeScreenshot } = gallery;
|
|
4010
|
-
const capture = activeScreenshot?.image ?? null;
|
|
4011
|
-
const annotations = activeScreenshot?.annotations ?? [];
|
|
4012
|
-
const isCapturing = gallery.pendingCapture !== null || isElementCapturing;
|
|
4013
|
-
const submission = useFeedbackSubmission({
|
|
4014
|
-
client,
|
|
4015
|
-
onError: setError,
|
|
4016
|
-
onSubmitted: (feedbackId) => {
|
|
4017
|
-
setStep("success");
|
|
4018
|
-
props.onSubmit?.({ feedbackId });
|
|
4019
|
-
successTimer.current = setTimeout(close, SUCCESS_CLOSE_DELAY);
|
|
4020
|
-
},
|
|
4021
|
-
partialUploadMessage: props.labels?.attachmentUploadFailed ?? DEFAULT_WIDGET_LABELS.attachmentUploadFailed
|
|
4022
|
-
});
|
|
4023
|
-
const { resetSubmission } = submission;
|
|
4024
|
-
const firstCapture = gallery.pendingCapture ?? gallery.screenshots[0];
|
|
4025
|
-
const canRefreshAutomatically = gallery.screenshots.length <= 1 && firstCapture?.source === "automatic";
|
|
4026
|
-
useCaptureSync(isOpen && !submission.isEditingDisabled && step === "compose" && canRefreshAutomatically && annotations.length === 0 && !gallery.screenshots.some((draft) => draft.selection) && props.captureOnOpen !== false, refreshAutomatic);
|
|
4027
|
-
const reset = useCallback(() => {
|
|
4028
|
-
if (successTimer.current) clearTimeout(successTimer.current);
|
|
4029
|
-
setAnnotationTrigger(null);
|
|
4030
|
-
resetSubmission();
|
|
4031
|
-
pickRef.current++;
|
|
4032
|
-
setIsElementCapturing(false);
|
|
4033
|
-
resetScreenshots();
|
|
4034
|
-
setMessage("");
|
|
4035
|
-
setEmail("");
|
|
4036
|
-
setHoneypot("");
|
|
4037
|
-
setError(null);
|
|
4038
|
-
setStep("compose");
|
|
4039
|
-
}, [resetSubmission, resetScreenshots]);
|
|
4040
|
-
const close = useCallback(() => {
|
|
4041
|
-
setIsOpen(false);
|
|
4042
|
-
reset();
|
|
4043
|
-
props.onClose?.();
|
|
4044
|
-
}, [reset, props.onClose]);
|
|
4045
|
-
const dismiss = useCallback(() => {
|
|
4046
|
-
if (dismissForDays === null) return;
|
|
4047
|
-
const until = Date.now() + dismissForDays * MILLISECONDS_PER_DAY;
|
|
4048
|
-
writeDismissedUntil(dismissalKey, until);
|
|
4049
|
-
setIsOpen(false);
|
|
4050
|
-
reset();
|
|
4051
|
-
setDismissedUntil(until);
|
|
4052
|
-
props.onDismiss?.({ until });
|
|
4053
|
-
}, [
|
|
4054
|
-
dismissForDays,
|
|
4055
|
-
dismissalKey,
|
|
4056
|
-
props.onDismiss,
|
|
4057
|
-
reset
|
|
4058
|
-
]);
|
|
4059
|
-
const open = useCallback(() => {
|
|
4060
|
-
setIsOpen(true);
|
|
4061
|
-
setStep("compose");
|
|
4062
|
-
props.onOpen?.();
|
|
4063
|
-
if (props.captureOnOpen !== false) refreshAutomatic();
|
|
4064
|
-
}, [
|
|
4065
|
-
props.captureOnOpen,
|
|
4066
|
-
props.onOpen,
|
|
4067
|
-
refreshAutomatic
|
|
4068
|
-
]);
|
|
4069
|
-
const selectElement = useCallback(async (element, note = "") => {
|
|
4070
|
-
cancelPendingCapture();
|
|
4071
|
-
setIsElementCapturing(true);
|
|
4072
|
-
const picked = note ? {
|
|
4073
|
-
...buildElementSelection(element),
|
|
4074
|
-
comment: note.slice(0, MAX_SELECTION_COMMENT_LENGTH)
|
|
4075
|
-
} : buildElementSelection(element);
|
|
4076
|
-
setStep("compose");
|
|
4077
|
-
const pick = ++pickRef.current;
|
|
4078
|
-
const context = collectPageContext({ sdkVersion: SDK_VERSION });
|
|
4079
|
-
const [fresh, closeUp] = await Promise.all([captureViewport().catch(() => null), captureElementCloseUp(element).catch(() => null)]);
|
|
4080
|
-
if (pickRef.current !== pick) {
|
|
4081
|
-
releaseCapture(fresh);
|
|
4082
|
-
releaseCapture(closeUp);
|
|
4083
|
-
return;
|
|
4084
|
-
}
|
|
4085
|
-
setIsElementCapturing(false);
|
|
4086
|
-
if (!fresh) {
|
|
4087
|
-
releaseCapture(closeUp);
|
|
4088
|
-
setError(captureFailed);
|
|
4089
|
-
return;
|
|
4090
|
-
}
|
|
4091
|
-
storeScreenshot({
|
|
4092
|
-
annotations: [highlightFor(picked, fresh)],
|
|
4093
|
-
closeUp,
|
|
4094
|
-
context,
|
|
4095
|
-
id: crypto.randomUUID(),
|
|
4096
|
-
image: fresh,
|
|
4097
|
-
selectedNode: element,
|
|
4098
|
-
selection: picked,
|
|
4099
|
-
source: "manual"
|
|
4100
|
-
});
|
|
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);
|
|
4101
4225
|
}, [
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4226
|
+
shadowRoot,
|
|
4227
|
+
resolvedTheme,
|
|
4228
|
+
primaryColor,
|
|
4229
|
+
offset
|
|
4105
4230
|
]);
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
const reportedMessage = message.trim() || firstElementNote || "";
|
|
4109
|
-
const submit = () => {
|
|
4110
|
-
if (honeypot) {
|
|
4111
|
-
setStep("success");
|
|
4112
|
-
return;
|
|
4113
|
-
}
|
|
4114
|
-
setStep("compose");
|
|
4115
|
-
return submission.submit({
|
|
4116
|
-
context: {
|
|
4117
|
-
...activeScreenshot?.context ?? collectPageContext({ sdkVersion: "0.4.2" }),
|
|
4118
|
-
consoleEvents: consoleRef.current?.events(),
|
|
4119
|
-
metadata: props.metadata,
|
|
4120
|
-
selections
|
|
4121
|
-
},
|
|
4122
|
-
email,
|
|
4123
|
-
isAnonymous,
|
|
4124
|
-
message: reportedMessage,
|
|
4125
|
-
screenshots: gallery.screenshots
|
|
4126
|
-
});
|
|
4127
|
-
};
|
|
4128
|
-
const requestSubmit = () => {
|
|
4129
|
-
if (isAnonymous && email.length === 0 && !submission.hasPendingAttachments && !honeypot) {
|
|
4130
|
-
setStep("email");
|
|
4131
|
-
return;
|
|
4132
|
-
}
|
|
4133
|
-
return submit();
|
|
4134
|
-
};
|
|
4135
|
-
return {
|
|
4136
|
-
activeScreenshot,
|
|
4137
|
-
annotateScreenshot: (id, trigger) => {
|
|
4138
|
-
setAnnotationTrigger(trigger);
|
|
4139
|
-
gallery.selectScreenshot(id);
|
|
4140
|
-
setStep("annotate");
|
|
4141
|
-
},
|
|
4142
|
-
annotations,
|
|
4143
|
-
annotationTrigger,
|
|
4144
|
-
canDismiss: dismissForDays !== null,
|
|
4145
|
-
canSubmit: reportedMessage.length > 0,
|
|
4146
|
-
capture,
|
|
4147
|
-
close,
|
|
4148
|
-
dismiss,
|
|
4149
|
-
dismissForDays,
|
|
4150
|
-
email,
|
|
4151
|
-
error,
|
|
4152
|
-
hasPendingAttachments: submission.hasPendingAttachments,
|
|
4153
|
-
honeypot,
|
|
4154
|
-
isAnonymous,
|
|
4155
|
-
isCapturing,
|
|
4156
|
-
isDismissed: dismissedUntil > Date.now(),
|
|
4157
|
-
isEditingDisabled: submission.isEditingDisabled,
|
|
4158
|
-
isOpen,
|
|
4159
|
-
isSubmitting: submission.isSubmitting,
|
|
4160
|
-
message,
|
|
4161
|
-
open,
|
|
4162
|
-
pendingCapture: gallery.pendingCapture,
|
|
4163
|
-
removeCapture: gallery.removeCapture,
|
|
4164
|
-
requestSubmit,
|
|
4165
|
-
retakeCapture: gallery.retakeCapture,
|
|
4166
|
-
screenshots: gallery.screenshots,
|
|
4167
|
-
selectElement,
|
|
4168
|
-
selections,
|
|
4169
|
-
setAnnotations: gallery.setAnnotations,
|
|
4170
|
-
setEmail,
|
|
4171
|
-
setHoneypot,
|
|
4172
|
-
setMessage,
|
|
4173
|
-
setSelectionComment: gallery.setSelectionComment,
|
|
4174
|
-
setStep,
|
|
4175
|
-
step,
|
|
4176
|
-
submit,
|
|
4177
|
-
takeCapture: gallery.takeCapture
|
|
4178
|
-
};
|
|
4231
|
+
if (!shadowRoot) return null;
|
|
4232
|
+
return createPortal(children, shadowRoot);
|
|
4179
4233
|
}
|
|
4180
4234
|
//#endregion
|
|
4181
4235
|
//#region src/feedback/widget.tsx
|
|
@@ -4186,25 +4240,7 @@ function RefletFeedback(props) {
|
|
|
4186
4240
|
...props.labels
|
|
4187
4241
|
};
|
|
4188
4242
|
const state = useWidgetState(props);
|
|
4189
|
-
const {
|
|
4190
|
-
useEffect(() => {
|
|
4191
|
-
if (!(hotkey && enabled)) return;
|
|
4192
|
-
const onKeyDown = (event) => {
|
|
4193
|
-
if (matchesHotkey(event, hotkey)) {
|
|
4194
|
-
event.preventDefault();
|
|
4195
|
-
if (isOpen) close();
|
|
4196
|
-
else open();
|
|
4197
|
-
}
|
|
4198
|
-
};
|
|
4199
|
-
document.addEventListener("keydown", onKeyDown, true);
|
|
4200
|
-
return () => document.removeEventListener("keydown", onKeyDown, true);
|
|
4201
|
-
}, [
|
|
4202
|
-
close,
|
|
4203
|
-
enabled,
|
|
4204
|
-
hotkey,
|
|
4205
|
-
isOpen,
|
|
4206
|
-
open
|
|
4207
|
-
]);
|
|
4243
|
+
const { isOpen } = state;
|
|
4208
4244
|
if (!enabled || state.isDismissed) return null;
|
|
4209
4245
|
return /* @__PURE__ */ jsxs(ShadowPortal, {
|
|
4210
4246
|
offset,
|
|
@@ -4217,6 +4253,7 @@ function RefletFeedback(props) {
|
|
|
4217
4253
|
"data-active": state.isCapturing
|
|
4218
4254
|
}),
|
|
4219
4255
|
/* @__PURE__ */ jsx(FloatingWidget, {
|
|
4256
|
+
hotkey,
|
|
4220
4257
|
labels,
|
|
4221
4258
|
position,
|
|
4222
4259
|
state
|