reflet-sdk 0.4.3 → 0.5.0
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 +15 -0
- package/README.md +64 -0
- package/dist/{client-BYuSKJw5.js → client-BWEBWk1k.js} +3 -3
- package/dist/client-BWEBWk1k.js.map +1 -0
- package/dist/client-w4Rkl10y.d.ts.map +1 -1
- package/dist/devtools-layer-BYGuFmhg.js +3935 -0
- package/dist/devtools-layer-BYGuFmhg.js.map +1 -0
- package/dist/devtools-next.d.ts +19 -0
- package/dist/devtools-next.d.ts.map +1 -0
- package/dist/devtools-next.js +25 -0
- package/dist/devtools-next.js.map +1 -0
- package/dist/devtools-vite.d.ts +8 -0
- package/dist/devtools-vite.d.ts.map +1 -0
- package/dist/devtools-vite.js +75 -0
- package/dist/devtools-vite.js.map +1 -0
- package/dist/feedback.d.ts +5 -0
- package/dist/feedback.d.ts.map +1 -1
- package/dist/feedback.js +54 -1868
- package/dist/feedback.js.map +1 -1
- package/dist/handler-CHP0m7KF.js +713 -0
- package/dist/handler-CHP0m7KF.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/options-7KtQZJj8.d.ts +20 -0
- package/dist/options-7KtQZJj8.d.ts.map +1 -0
- package/dist/picker-DDDpVcfr.js +1918 -0
- package/dist/picker-DDDpVcfr.js.map +1 -0
- package/dist/protocol-Bn6Bg_0r.js +32 -0
- package/dist/protocol-Bn6Bg_0r.js.map +1 -0
- package/dist/react.js +2 -2
- package/dist/{z-index-_Q9PWbfY.js → z-index-CKxBgZTG.js} +3 -2
- package/dist/{z-index-_Q9PWbfY.js.map → z-index-CKxBgZTG.js.map} +1 -1
- package/package.json +15 -2
- package/dist/client-BYuSKJw5.js.map +0 -1
package/dist/feedback.js
CHANGED
|
@@ -1,932 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { n as Reflet } from "./client-BWEBWk1k.js";
|
|
3
|
+
import { A as TrashIcon, C as MoreIcon, E as SquareIcon, F as SDK_VERSION, G as capturedFromCanvas, I as collectPageContext, K as releaseCapture, L as MAX_SELECTION_COMMENT_LENGTH, M as isDegenerate, N as normalizeRect, O as TargetIcon, P as DEFAULT_WIDGET_LABELS, R as buildElementSelection, S as HighlightIcon, T as SpotlightIcon, U as WIDGET_MARKER, W as captureViewport, _ as ChatIcon, b as CloseIcon, d as SelectionOutline, f as drawAnnotations, g as CameraIcon, h as BlurIcon, j as UndoIcon, k as TextIcon, l as submitWidgetFeedback, m as ArrowIcon, o as attachPreparedScreenshots, p as listenToKeydown, r as useFloatingPosition, s as createFeedbackTransport, t as ElementPicker, v as CheckIcon, w as PencilIcon, x as GripIcon, y as ChevronDownIcon } from "./picker-DDDpVcfr.js";
|
|
4
|
+
import { n as RefletContext, t as REFLET_Z_INDEX } from "./z-index-CKxBgZTG.js";
|
|
5
|
+
import { Suspense, lazy, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
5
6
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
7
|
import { Slider } from "@base-ui/react/slider";
|
|
7
8
|
import { ScrollArea } from "@base-ui/react/scroll-area";
|
|
8
9
|
import { createPortal } from "react-dom";
|
|
9
|
-
//#region src/feedback/core/capture.ts
|
|
10
|
-
/** Marks every node the widget owns, so captures never include the panel. */
|
|
11
|
-
const WIDGET_MARKER = "data-reflet-widget";
|
|
12
|
-
const MAX_PIXEL_RATIO = 2;
|
|
13
|
-
const DEFAULT_MAX_WIDTH = 2400;
|
|
14
|
-
const MAX_INLINE_BYTES = 3e6;
|
|
15
|
-
const TRANSPARENT_PIXEL = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
|
16
|
-
const EXTERNAL_URL = /^(https?:)?\/\//i;
|
|
17
|
-
const CSS_URL = /url\((['"]?)([^)'"]+)\1\)/g;
|
|
18
|
-
function isExternalImageUrl(url) {
|
|
19
|
-
if (!url || url.startsWith("data:") || url.startsWith("blob:")) return false;
|
|
20
|
-
if (!EXTERNAL_URL.test(url)) return false;
|
|
21
|
-
try {
|
|
22
|
-
return new URL(url, window.location.href).origin !== window.location.origin;
|
|
23
|
-
} catch {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
function toDataUrl(url) {
|
|
28
|
-
return new Promise((resolve) => {
|
|
29
|
-
fetch(url, { mode: "cors" }).then(async (response) => {
|
|
30
|
-
if (!response.ok) {
|
|
31
|
-
resolve(null);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
const blob = await response.blob();
|
|
35
|
-
if (blob.size === 0 || blob.size > MAX_INLINE_BYTES) {
|
|
36
|
-
resolve(null);
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
const reader = new FileReader();
|
|
40
|
-
reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : null);
|
|
41
|
-
reader.onerror = () => resolve(null);
|
|
42
|
-
reader.readAsDataURL(blob);
|
|
43
|
-
}).catch(() => resolve(null));
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
const PNG_NORMALIZE_TIMEOUT = 750;
|
|
47
|
-
const CAPTURE_TIMEOUT = 1e4;
|
|
48
|
-
const ANIMATION_SETTLE_TIMEOUT = 1e3;
|
|
49
|
-
/**
|
|
50
|
-
* Tiles and sections routinely animate in or reflow (grid drags, entrance
|
|
51
|
-
* transitions); a capture taken mid-flight freezes them displaced. Waits for
|
|
52
|
-
* animations touching the page to finish, capped so an infinite spinner can
|
|
53
|
-
* never stall the report.
|
|
54
|
-
*/
|
|
55
|
-
async function waitForPageAnimations() {
|
|
56
|
-
if (typeof document.getAnimations !== "function") return;
|
|
57
|
-
const start = performance.now();
|
|
58
|
-
while (performance.now() - start < ANIMATION_SETTLE_TIMEOUT) {
|
|
59
|
-
const running = document.getAnimations().filter((animation) => animation.playState === "running");
|
|
60
|
-
if (running.length === 0) return;
|
|
61
|
-
await Promise.race([Promise.allSettled(running.map((animation) => animation.finished)), new Promise((resolve) => setTimeout(resolve, 150))]);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* snapdom awaits `decode()` on images created with `decoding="sync"` — a
|
|
66
|
-
* promise Chrome 151 sometimes never settles (observed for cross-origin
|
|
67
|
-
* webp, cached or inlined). Re-encoding through a canvas yields a PNG data
|
|
68
|
-
* url, which decodes reliably; the load itself goes through `onload`, never
|
|
69
|
-
* `decode()`.
|
|
70
|
-
*/
|
|
71
|
-
function normalizeToPng(dataUrl) {
|
|
72
|
-
return new Promise((resolve) => {
|
|
73
|
-
const image = new Image();
|
|
74
|
-
const timeout = setTimeout(() => resolve(dataUrl), PNG_NORMALIZE_TIMEOUT);
|
|
75
|
-
image.onload = () => {
|
|
76
|
-
clearTimeout(timeout);
|
|
77
|
-
try {
|
|
78
|
-
if (image.naturalWidth === 0 || image.naturalHeight === 0) {
|
|
79
|
-
resolve(null);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
const canvas = document.createElement("canvas");
|
|
83
|
-
canvas.width = image.naturalWidth;
|
|
84
|
-
canvas.height = image.naturalHeight;
|
|
85
|
-
const context = canvas.getContext("2d");
|
|
86
|
-
if (!context) {
|
|
87
|
-
resolve(dataUrl);
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
context.drawImage(image, 0, 0);
|
|
91
|
-
resolve(canvas.toDataURL("image/png"));
|
|
92
|
-
} catch {
|
|
93
|
-
resolve(null);
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
image.onerror = () => {
|
|
97
|
-
clearTimeout(timeout);
|
|
98
|
-
resolve(null);
|
|
99
|
-
};
|
|
100
|
-
image.src = dataUrl;
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
async function toInlineUrl(url) {
|
|
104
|
-
const dataUrl = await toDataUrl(url);
|
|
105
|
-
if (!dataUrl) return null;
|
|
106
|
-
return normalizeToPng(dataUrl);
|
|
107
|
-
}
|
|
108
|
-
function remember(swaps, element, name) {
|
|
109
|
-
swaps.push({
|
|
110
|
-
element,
|
|
111
|
-
name,
|
|
112
|
-
value: element.getAttribute(name)
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
async function inlineImgSource(img, swaps) {
|
|
116
|
-
const src = img.getAttribute("src");
|
|
117
|
-
if (!(src && isExternalImageUrl(src))) return;
|
|
118
|
-
const dataUrl = await toInlineUrl(src);
|
|
119
|
-
if (dataUrl) {
|
|
120
|
-
if (img.hasAttribute("srcset")) {
|
|
121
|
-
remember(swaps, img, "srcset");
|
|
122
|
-
img.removeAttribute("srcset");
|
|
123
|
-
}
|
|
124
|
-
if (img.hasAttribute("sizes")) {
|
|
125
|
-
remember(swaps, img, "sizes");
|
|
126
|
-
img.removeAttribute("sizes");
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
remember(swaps, img, "src");
|
|
130
|
-
img.setAttribute("src", dataUrl ?? TRANSPARENT_PIXEL);
|
|
131
|
-
}
|
|
132
|
-
async function inlineBackgroundImages(element, swaps) {
|
|
133
|
-
const style = element.getAttribute("style");
|
|
134
|
-
if (!style) return;
|
|
135
|
-
const urls = [...style.matchAll(CSS_URL)].map((match) => match[2] ?? "");
|
|
136
|
-
if (!urls.some(isExternalImageUrl)) return;
|
|
137
|
-
let next = style;
|
|
138
|
-
for (const url of urls) {
|
|
139
|
-
if (!isExternalImageUrl(url)) continue;
|
|
140
|
-
const dataUrl = await toInlineUrl(url);
|
|
141
|
-
next = next.split(url).join(dataUrl ?? TRANSPARENT_PIXEL);
|
|
142
|
-
}
|
|
143
|
-
if (next !== style) {
|
|
144
|
-
remember(swaps, element, "style");
|
|
145
|
-
element.setAttribute("style", next);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Swaps every cross-origin image under `root` for a data URL so the capture
|
|
150
|
-
* cannot hit snapdom's decode-failure hang. Returns the undo list — always
|
|
151
|
-
* run it, even after a failed capture.
|
|
152
|
-
*/
|
|
153
|
-
async function inlineExternalImages(root) {
|
|
154
|
-
const swaps = [];
|
|
155
|
-
for (const img of [...root.querySelectorAll("img")]) await inlineImgSource(img, swaps);
|
|
156
|
-
for (const el of [...root.querySelectorAll("[style*='url(']")]) await inlineBackgroundImages(el, swaps);
|
|
157
|
-
return swaps;
|
|
158
|
-
}
|
|
159
|
-
function restoreInlinedImages(swaps) {
|
|
160
|
-
for (const swap of swaps) if (swap.value === null) swap.element.removeAttribute(swap.name);
|
|
161
|
-
else swap.element.setAttribute(swap.name, swap.value);
|
|
162
|
-
}
|
|
163
|
-
function buildSnapdomOptions(options) {
|
|
164
|
-
const ratio = options.devicePixelRatio ?? 1;
|
|
165
|
-
return {
|
|
166
|
-
cache: "disabled",
|
|
167
|
-
clip: options.element ? void 0 : "viewport",
|
|
168
|
-
dpr: Math.min(Math.max(ratio || 1, 1), MAX_PIXEL_RATIO),
|
|
169
|
-
embedFonts: true,
|
|
170
|
-
exclude: [`[${WIDGET_MARKER}]`, ...options.excludeSelectors ?? []],
|
|
171
|
-
fast: true,
|
|
172
|
-
filter: options.filter,
|
|
173
|
-
reconcile: true
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
function fitWithin(size, maxWidth) {
|
|
177
|
-
if (size.width <= maxWidth) return {
|
|
178
|
-
height: size.height,
|
|
179
|
-
scale: 1,
|
|
180
|
-
width: size.width
|
|
181
|
-
};
|
|
182
|
-
const scale = maxWidth / size.width;
|
|
183
|
-
return {
|
|
184
|
-
height: Math.max(1, Math.round(size.height * scale)),
|
|
185
|
-
scale,
|
|
186
|
-
width: maxWidth
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
function toBlob(canvas) {
|
|
190
|
-
return new Promise((resolve) => {
|
|
191
|
-
canvas.toBlob(resolve, "image/png");
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
function downscale(canvas, target) {
|
|
195
|
-
const resized = document.createElement("canvas");
|
|
196
|
-
resized.width = target.width;
|
|
197
|
-
resized.height = target.height;
|
|
198
|
-
const context = resized.getContext("2d");
|
|
199
|
-
if (!context) return canvas;
|
|
200
|
-
context.imageSmoothingQuality = "high";
|
|
201
|
-
context.drawImage(canvas, 0, 0, target.width, target.height);
|
|
202
|
-
return resized;
|
|
203
|
-
}
|
|
204
|
-
/** Wraps a rendered canvas as an owned capture, downscaled to `maxWidth`. */
|
|
205
|
-
async function capturedFromCanvas(source, maxWidth) {
|
|
206
|
-
const fitted = fitWithin({
|
|
207
|
-
height: source.height,
|
|
208
|
-
width: source.width
|
|
209
|
-
}, maxWidth);
|
|
210
|
-
const output = fitted.scale === 1 ? source : downscale(source, fitted);
|
|
211
|
-
const blob = await toBlob(output);
|
|
212
|
-
if (!blob) return null;
|
|
213
|
-
return {
|
|
214
|
-
blob,
|
|
215
|
-
height: output.height,
|
|
216
|
-
mimeType: "image/png",
|
|
217
|
-
objectUrl: URL.createObjectURL(blob),
|
|
218
|
-
width: output.width
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Renders what the user is looking at right now.
|
|
223
|
-
*
|
|
224
|
-
* Runs off the DOM rather than `getDisplayMedia` on purpose: a screen-share
|
|
225
|
-
* permission prompt on every report would cost more reports than it captures.
|
|
226
|
-
* Returns null instead of throwing — a screenshot is never worth losing the
|
|
227
|
-
* written feedback.
|
|
228
|
-
*/
|
|
229
|
-
async function captureViewport(options = {}) {
|
|
230
|
-
try {
|
|
231
|
-
const { snapdom } = await import("@zumer/snapdom");
|
|
232
|
-
const target = options.element ?? document.documentElement;
|
|
233
|
-
await waitForPageAnimations();
|
|
234
|
-
const swaps = await inlineExternalImages(target);
|
|
235
|
-
const shoot = (withoutImages = false) => {
|
|
236
|
-
const attempt = snapdom.toCanvas(target, buildSnapdomOptions({
|
|
237
|
-
devicePixelRatio: window.devicePixelRatio,
|
|
238
|
-
element: options.element,
|
|
239
|
-
excludeSelectors: options.excludeSelectors,
|
|
240
|
-
filter: withoutImages ? (element) => element.tagName !== "IMG" : void 0
|
|
241
|
-
}));
|
|
242
|
-
attempt.catch(() => void 0);
|
|
243
|
-
return Promise.race([attempt, new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("capture timed out")), options.timeout ?? CAPTURE_TIMEOUT))]);
|
|
244
|
-
};
|
|
245
|
-
let canvas;
|
|
246
|
-
try {
|
|
247
|
-
canvas = await shoot();
|
|
248
|
-
} catch {
|
|
249
|
-
canvas = await shoot(true);
|
|
250
|
-
} finally {
|
|
251
|
-
restoreInlinedImages(swaps);
|
|
252
|
-
}
|
|
253
|
-
return await capturedFromCanvas(canvas, options.maxWidth ?? DEFAULT_MAX_WIDTH);
|
|
254
|
-
} catch {
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
function releaseCapture(image) {
|
|
259
|
-
if (image) URL.revokeObjectURL(image.objectUrl);
|
|
260
|
-
}
|
|
261
|
-
//#endregion
|
|
262
|
-
//#region src/feedback/core/react-source.ts
|
|
263
|
-
const FIBER_KEY_PREFIXES = ["__reactFiber$", "__reactInternalInstance$"];
|
|
264
|
-
const MEMO_TYPE = Symbol.for("react.memo");
|
|
265
|
-
const FORWARD_REF_TYPE = Symbol.for("react.forward_ref");
|
|
266
|
-
const MAX_STACK_DEPTH = 6;
|
|
267
|
-
const MAX_FIBER_WALK = 60;
|
|
268
|
-
const MAX_OWNER_WALK = 8;
|
|
269
|
-
const MIN_NAME_LENGTH = 4;
|
|
270
|
-
const CAPITALIZED = /^[A-Z]/;
|
|
271
|
-
const WORD_INSIDE = /[a-z]{2}/;
|
|
272
|
-
const ORIGIN_PREFIX = /^[a-z][\w+.-]*:\/\/[^/]*/i;
|
|
273
|
-
const WEBPACK_PREFIX = /^webpack-internal:\/{2,}(\.\/)?/;
|
|
274
|
-
const STACK_LOCATION = /([^\s()]+):(\d+):(\d+)\)?\s*$/;
|
|
275
|
-
const NON_SOURCE_FRAME = /node_modules|\/_next\/|\/chunks?\/|\/\.vite\/|\/dist\/|\/build\/|hot-update/;
|
|
276
|
-
const SOURCE_EXTENSION = /\.(?:[cm]?[jt]sx?)$/;
|
|
277
|
-
const SOURCE_ROOT_MARKERS = [
|
|
278
|
-
"src",
|
|
279
|
-
"app",
|
|
280
|
-
"pages",
|
|
281
|
-
"components",
|
|
282
|
-
"features",
|
|
283
|
-
"lib"
|
|
284
|
-
];
|
|
285
|
-
function isRecord(value) {
|
|
286
|
-
return typeof value === "object" && value !== null;
|
|
287
|
-
}
|
|
288
|
-
/** React attaches the fiber under a randomly suffixed key on the host node. */
|
|
289
|
-
function getFiberFromNode(node) {
|
|
290
|
-
for (const key of Object.getOwnPropertyNames(node)) for (const prefix of FIBER_KEY_PREFIXES) if (key.startsWith(prefix)) return Reflect.get(node, key) ?? null;
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
function readName(value) {
|
|
294
|
-
if (typeof value !== "function" && !isRecord(value)) return null;
|
|
295
|
-
const displayName = Reflect.get(value, "displayName");
|
|
296
|
-
if (typeof displayName === "string" && displayName) return displayName;
|
|
297
|
-
const name = Reflect.get(value, "name");
|
|
298
|
-
return typeof name === "string" && name ? name : null;
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Minified bundles hand out names like `Ur` or `Sn`. Reporting those as the
|
|
302
|
-
* owning component actively misleads an agent, so only real words get through.
|
|
303
|
-
*/
|
|
304
|
-
function isMeaningfulName(name) {
|
|
305
|
-
if (name.includes("/") || name.includes("_")) return true;
|
|
306
|
-
return name.length >= MIN_NAME_LENGTH && CAPITALIZED.test(name) && WORD_INSIDE.test(name);
|
|
307
|
-
}
|
|
308
|
-
function componentName(type, depth = 0) {
|
|
309
|
-
if (typeof type === "string" || type === null || depth > 3) return null;
|
|
310
|
-
if (isRecord(type)) {
|
|
311
|
-
const marker = Reflect.get(type, "$$typeof");
|
|
312
|
-
if (marker === MEMO_TYPE) return componentName(Reflect.get(type, "type"), depth + 1);
|
|
313
|
-
if (marker === FORWARD_REF_TYPE) return componentName(Reflect.get(type, "render"), depth + 1);
|
|
314
|
-
}
|
|
315
|
-
const name = readName(type);
|
|
316
|
-
return name && isMeaningfulName(name) ? name : null;
|
|
317
|
-
}
|
|
318
|
-
/** Component names owning the fiber, innermost first. */
|
|
319
|
-
function resolveComponentStack(fiber) {
|
|
320
|
-
const stack = [];
|
|
321
|
-
let current = fiber;
|
|
322
|
-
let steps = 0;
|
|
323
|
-
while (isRecord(current) && steps < MAX_FIBER_WALK) {
|
|
324
|
-
const name = componentName(Reflect.get(current, "type")) ?? componentName(Reflect.get(current, "elementType"));
|
|
325
|
-
if (name && stack.at(-1) !== name) {
|
|
326
|
-
stack.push(name);
|
|
327
|
-
if (stack.length >= MAX_STACK_DEPTH) break;
|
|
328
|
-
}
|
|
329
|
-
current = Reflect.get(current, "return");
|
|
330
|
-
steps++;
|
|
331
|
-
}
|
|
332
|
-
return stack;
|
|
333
|
-
}
|
|
334
|
-
function shortenSourcePath(raw) {
|
|
335
|
-
const path = raw.replace(WEBPACK_PREFIX, "").replace(ORIGIN_PREFIX, "").split("?")[0];
|
|
336
|
-
if (!path) return raw;
|
|
337
|
-
for (const marker of SOURCE_ROOT_MARKERS) {
|
|
338
|
-
if (path.startsWith(`${marker}/`)) return path;
|
|
339
|
-
const index = path.lastIndexOf(`/${marker}/`);
|
|
340
|
-
if (index !== -1) return path.slice(index + 1);
|
|
341
|
-
}
|
|
342
|
-
return path.split("/").filter(Boolean).slice(-3).join("/");
|
|
343
|
-
}
|
|
344
|
-
function formatLocation(fileName, line, column) {
|
|
345
|
-
const base = `${shortenSourcePath(fileName)}:${line}`;
|
|
346
|
-
return column === void 0 ? base : `${base}:${column}`;
|
|
347
|
-
}
|
|
348
|
-
function fromDebugSource(source) {
|
|
349
|
-
if (!isRecord(source)) return;
|
|
350
|
-
const fileName = Reflect.get(source, "fileName");
|
|
351
|
-
const lineNumber = Reflect.get(source, "lineNumber");
|
|
352
|
-
if (typeof fileName !== "string" || typeof lineNumber !== "number") return;
|
|
353
|
-
const columnNumber = Reflect.get(source, "columnNumber");
|
|
354
|
-
return formatLocation(fileName, lineNumber, typeof columnNumber === "number" ? columnNumber : void 0);
|
|
355
|
-
}
|
|
356
|
-
function fromDebugStack(stack) {
|
|
357
|
-
const raw = isRecord(stack) ? Reflect.get(stack, "stack") : stack;
|
|
358
|
-
if (typeof raw !== "string") return;
|
|
359
|
-
for (const line of raw.split("\n")) {
|
|
360
|
-
const match = STACK_LOCATION.exec(line);
|
|
361
|
-
const file = match?.[1];
|
|
362
|
-
if (!(match && file) || NON_SOURCE_FRAME.test(file)) continue;
|
|
363
|
-
const path = file.replace(ORIGIN_PREFIX, "").split("?")[0] ?? "";
|
|
364
|
-
if (!SOURCE_EXTENSION.test(path)) continue;
|
|
365
|
-
return formatLocation(path, Number(match[2]), Number(match[3]));
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
/**
|
|
369
|
-
* `src/components/login-form.tsx:46:19` when the React build exposes it.
|
|
370
|
-
* Production bundles strip debug info — callers must treat this as optional.
|
|
371
|
-
*/
|
|
372
|
-
function resolveSourceLocation(fiber) {
|
|
373
|
-
let current = fiber;
|
|
374
|
-
let steps = 0;
|
|
375
|
-
while (isRecord(current) && steps < MAX_OWNER_WALK) {
|
|
376
|
-
const located = fromDebugSource(Reflect.get(current, "_debugSource")) ?? fromDebugStack(Reflect.get(current, "_debugStack"));
|
|
377
|
-
if (located) return located;
|
|
378
|
-
current = Reflect.get(current, "_debugOwner");
|
|
379
|
-
steps++;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
function inspectReactElement(element) {
|
|
383
|
-
const fiber = getFiberFromNode(element);
|
|
384
|
-
return {
|
|
385
|
-
componentStack: resolveComponentStack(fiber),
|
|
386
|
-
sourceLocation: resolveSourceLocation(fiber)
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
//#endregion
|
|
390
|
-
//#region src/feedback/core/redact.ts
|
|
391
|
-
/**
|
|
392
|
-
* The picked element's markup leaves the reporter's browser and ends up in an
|
|
393
|
-
* agent prompt, so it is scrubbed here: typed-in values, credentials and
|
|
394
|
-
* anything marked `data-reflet-redact` never make the trip.
|
|
395
|
-
*/
|
|
396
|
-
const EMAIL = /[\w.%+-]+@[\w.-]+\.[a-z]{2,}/gi;
|
|
397
|
-
const LONG_TOKEN = /\b[\w-]{28,}\b/g;
|
|
398
|
-
const OPAQUE_URI = /^(?:data|blob|javascript):/i;
|
|
399
|
-
const WHITESPACE_RUN = /\s+/g;
|
|
400
|
-
const URL_QUERY = /[?#]/;
|
|
401
|
-
const REDACTED = "[redacted]";
|
|
402
|
-
const ELLIPSIS = "…";
|
|
403
|
-
const MAX_ATTRIBUTE_LENGTH = 120;
|
|
404
|
-
const MAX_TEXT_LENGTH$1 = 200;
|
|
405
|
-
const MAX_CHILD_DEPTH = 3;
|
|
406
|
-
const MAX_CLONED_NODES = 400;
|
|
407
|
-
const DROPPED_TAGS = /* @__PURE__ */ new Set([
|
|
408
|
-
"script",
|
|
409
|
-
"style",
|
|
410
|
-
"noscript",
|
|
411
|
-
"template"
|
|
412
|
-
]);
|
|
413
|
-
const VALUE_HOLDERS = /* @__PURE__ */ new Set([
|
|
414
|
-
"input",
|
|
415
|
-
"textarea",
|
|
416
|
-
"select",
|
|
417
|
-
"option"
|
|
418
|
-
]);
|
|
419
|
-
const URL_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
420
|
-
"src",
|
|
421
|
-
"srcset",
|
|
422
|
-
"href",
|
|
423
|
-
"poster",
|
|
424
|
-
"action",
|
|
425
|
-
"formaction"
|
|
426
|
-
]);
|
|
427
|
-
function truncate$1(value, max) {
|
|
428
|
-
return value.length <= max ? value : `${value.slice(0, max - 1)}${ELLIPSIS}`;
|
|
429
|
-
}
|
|
430
|
-
/** Emails and token-shaped strings, wherever they appear in reported text. */
|
|
431
|
-
function maskSecrets(value) {
|
|
432
|
-
return value.replace(EMAIL, REDACTED).replace(LONG_TOKEN, REDACTED);
|
|
433
|
-
}
|
|
434
|
-
const SENSITIVE_PARAM = /token|secret|pass(?:word|wd)?|pwd|auth|jwt|credential|session|sid|key|email|ssn|otp|code|signature|nonce/i;
|
|
435
|
-
const LEADING_QUESTION_MARK = /^\?/;
|
|
436
|
-
function decoded(value) {
|
|
437
|
-
try {
|
|
438
|
-
return decodeURIComponent(value);
|
|
439
|
-
} catch {
|
|
440
|
-
return value;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
/**
|
|
444
|
-
* Query strings carry signed-in state (`?token=…`, `?userEmail=…`) straight
|
|
445
|
-
* into stored reports and agent prompts. Values of anything credential- or
|
|
446
|
-
* identity-shaped are masked; every other byte of the address stays intact.
|
|
447
|
-
*/
|
|
448
|
-
function redactUrl(href) {
|
|
449
|
-
let url;
|
|
450
|
-
try {
|
|
451
|
-
url = new URL(href);
|
|
452
|
-
} catch {
|
|
453
|
-
return maskSecrets(href);
|
|
454
|
-
}
|
|
455
|
-
const pairs = url.search.replace(LEADING_QUESTION_MARK, "").split("&").filter(Boolean);
|
|
456
|
-
if (!pairs.some((pair) => SENSITIVE_PARAM.test(decoded(pair.split("=")[0] ?? "")))) return href;
|
|
457
|
-
const query = pairs.map((pair) => {
|
|
458
|
-
const equals = pair.indexOf("=");
|
|
459
|
-
if (equals === -1 || !SENSITIVE_PARAM.test(decoded(pair.slice(0, equals)))) return pair;
|
|
460
|
-
return `${pair.slice(0, equals)}=${REDACTED}`;
|
|
461
|
-
}).join("&");
|
|
462
|
-
return `${url.origin}${url.pathname}${query ? `?${query}` : ""}${url.hash}`;
|
|
463
|
-
}
|
|
464
|
-
function sanitizeUrl(value) {
|
|
465
|
-
if (OPAQUE_URI.test(value)) return `${value.slice(0, value.indexOf(":"))}:${ELLIPSIS}`;
|
|
466
|
-
return value.split(URL_QUERY)[0] ?? value;
|
|
467
|
-
}
|
|
468
|
-
function sanitizeAttributes(element) {
|
|
469
|
-
const holdsTypedValue = VALUE_HOLDERS.has(element.localName);
|
|
470
|
-
for (const { name, value } of Array.from(element.attributes)) {
|
|
471
|
-
if (holdsTypedValue && name === "value") {
|
|
472
|
-
element.removeAttribute(name);
|
|
473
|
-
continue;
|
|
474
|
-
}
|
|
475
|
-
const cleaned = URL_ATTRIBUTES.has(name) ? sanitizeUrl(value) : maskSecrets(value);
|
|
476
|
-
element.setAttribute(name, truncate$1(cleaned, MAX_ATTRIBUTE_LENGTH));
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
function collapse(element) {
|
|
480
|
-
element.replaceChildren(element.ownerDocument.createTextNode(ELLIPSIS));
|
|
481
|
-
}
|
|
482
|
-
function sanitizeTree(element, depth) {
|
|
483
|
-
sanitizeAttributes(element);
|
|
484
|
-
if (element.hasAttribute("data-reflet-redact")) {
|
|
485
|
-
element.replaceChildren(element.ownerDocument.createTextNode(REDACTED));
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
if (element.localName === "svg") {
|
|
489
|
-
element.replaceChildren();
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
for (const child of Array.from(element.childNodes)) {
|
|
493
|
-
if (child.nodeType === Node.COMMENT_NODE) {
|
|
494
|
-
child.remove();
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
if (child.nodeType === Node.TEXT_NODE) {
|
|
498
|
-
child.textContent = truncate$1(maskSecrets(child.textContent ?? ""), MAX_TEXT_LENGTH$1);
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
if (!(child instanceof Element)) continue;
|
|
502
|
-
if (DROPPED_TAGS.has(child.localName)) {
|
|
503
|
-
child.remove();
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
506
|
-
if (depth >= MAX_CHILD_DEPTH) {
|
|
507
|
-
sanitizeAttributes(child);
|
|
508
|
-
collapse(child);
|
|
509
|
-
continue;
|
|
510
|
-
}
|
|
511
|
-
sanitizeTree(child, depth + 1);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
/** Redacted `outerHTML`, shallow-cloned when the subtree is too large to be useful. */
|
|
515
|
-
function sanitizeMarkup(element, maxLength) {
|
|
516
|
-
const isSmallEnough = element.querySelectorAll("*").length <= MAX_CLONED_NODES;
|
|
517
|
-
const clone = element.cloneNode(isSmallEnough);
|
|
518
|
-
if (!(clone instanceof Element)) return "";
|
|
519
|
-
sanitizeTree(clone, 0);
|
|
520
|
-
if (!isSmallEnough) collapse(clone);
|
|
521
|
-
return truncate$1(clone.outerHTML.replace(WHITESPACE_RUN, " "), maxLength);
|
|
522
|
-
}
|
|
523
|
-
//#endregion
|
|
524
|
-
//#region src/feedback/core/element-selector.ts
|
|
525
|
-
const SAFE_ID = /^[A-Za-z][\w-]*$/;
|
|
526
|
-
const WHITESPACE = /\s+/g;
|
|
527
|
-
const MAX_LABEL_LENGTH = 80;
|
|
528
|
-
const MAX_HTML_LENGTH = 600;
|
|
529
|
-
const MAX_TEXT_LENGTH = 300;
|
|
530
|
-
const MAX_REGION_LENGTH = 160;
|
|
531
|
-
const MAX_HANDLE_VALUE_LENGTH = 80;
|
|
532
|
-
const MAX_DEPTH = 12;
|
|
533
|
-
const MIN_PATH_SEGMENTS = 2;
|
|
534
|
-
const MAX_SELECTOR_LENGTH = 600;
|
|
535
|
-
const MAX_SOURCE_LOCATION_LENGTH = 400;
|
|
536
|
-
const MAX_SELECTION_COMMENT_LENGTH = 2e3;
|
|
537
|
-
const TEST_ATTRIBUTES = [
|
|
538
|
-
"data-testid",
|
|
539
|
-
"data-test-id",
|
|
540
|
-
"data-test",
|
|
541
|
-
"data-cy",
|
|
542
|
-
"data-qa"
|
|
543
|
-
];
|
|
544
|
-
const NAMING_ATTRIBUTES = ["name", "aria-label"];
|
|
545
|
-
const LANDMARK_SELECTOR = "dialog, [role=\"dialog\"], [role=\"alertdialog\"], main, nav, aside, header, footer, form, section, article";
|
|
546
|
-
const HEADING_SELECTOR = "h1, h2, h3, h4, h5, h6, [role=\"heading\"]";
|
|
547
|
-
/** Elements that start a new visual block — their text gets separated by a space. */
|
|
548
|
-
const BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
549
|
-
"address",
|
|
550
|
-
"article",
|
|
551
|
-
"aside",
|
|
552
|
-
"blockquote",
|
|
553
|
-
"dd",
|
|
554
|
-
"details",
|
|
555
|
-
"div",
|
|
556
|
-
"dl",
|
|
557
|
-
"dt",
|
|
558
|
-
"fieldset",
|
|
559
|
-
"figcaption",
|
|
560
|
-
"figure",
|
|
561
|
-
"footer",
|
|
562
|
-
"form",
|
|
563
|
-
"h1",
|
|
564
|
-
"h2",
|
|
565
|
-
"h3",
|
|
566
|
-
"h4",
|
|
567
|
-
"h5",
|
|
568
|
-
"h6",
|
|
569
|
-
"header",
|
|
570
|
-
"hr",
|
|
571
|
-
"li",
|
|
572
|
-
"main",
|
|
573
|
-
"nav",
|
|
574
|
-
"ol",
|
|
575
|
-
"p",
|
|
576
|
-
"pre",
|
|
577
|
-
"section",
|
|
578
|
-
"summary",
|
|
579
|
-
"table",
|
|
580
|
-
"td",
|
|
581
|
-
"th",
|
|
582
|
-
"tr",
|
|
583
|
-
"ul"
|
|
584
|
-
]);
|
|
585
|
-
function visibleText(element) {
|
|
586
|
-
const clone = element.cloneNode(true);
|
|
587
|
-
if (!(clone instanceof Element)) return "";
|
|
588
|
-
for (const node of clone.querySelectorAll("*")) if (BLOCK_TAGS.has(node.localName)) node.before(clone.ownerDocument.createTextNode(" "));
|
|
589
|
-
return collapseText(maskSecrets(clone.textContent ?? ""));
|
|
590
|
-
}
|
|
591
|
-
function isUnique(selector) {
|
|
592
|
-
try {
|
|
593
|
-
return document.querySelectorAll(selector).length === 1;
|
|
594
|
-
} catch {
|
|
595
|
-
return false;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
function escapeAttributeValue(value) {
|
|
599
|
-
return value.replace(/["\\]/g, "\\$&");
|
|
600
|
-
}
|
|
601
|
-
function attributeHandle(element, attribute, qualifier) {
|
|
602
|
-
const value = element.getAttribute(attribute);
|
|
603
|
-
if (!value || value.length > MAX_HANDLE_VALUE_LENGTH) return null;
|
|
604
|
-
const selector = `${qualifier}[${attribute}="${escapeAttributeValue(value)}"]`;
|
|
605
|
-
return isUnique(selector) ? selector : null;
|
|
606
|
-
}
|
|
607
|
-
/** Something an engineer would have written on purpose, unlike a positional path. */
|
|
608
|
-
function uniqueHandle(element) {
|
|
609
|
-
const id = element.getAttribute("id");
|
|
610
|
-
if (id && SAFE_ID.test(id) && isUnique(`#${id}`)) return `#${id}`;
|
|
611
|
-
for (const attribute of TEST_ATTRIBUTES) {
|
|
612
|
-
const handle = attributeHandle(element, attribute, "");
|
|
613
|
-
if (handle) return handle;
|
|
614
|
-
}
|
|
615
|
-
for (const attribute of NAMING_ATTRIBUTES) {
|
|
616
|
-
const handle = attributeHandle(element, attribute, element.localName);
|
|
617
|
-
if (handle) return handle;
|
|
618
|
-
}
|
|
619
|
-
return null;
|
|
620
|
-
}
|
|
621
|
-
function positionalSegment(element) {
|
|
622
|
-
const tag = element.localName;
|
|
623
|
-
const parent = element.parentElement;
|
|
624
|
-
if (!parent) return tag;
|
|
625
|
-
const sameTag = Array.from(parent.children).filter((child) => child.localName === tag);
|
|
626
|
-
if (sameTag.length <= 1) return tag;
|
|
627
|
-
return `${tag}:nth-of-type(${sameTag.indexOf(element) + 1})`;
|
|
628
|
-
}
|
|
629
|
-
/**
|
|
630
|
-
* Shortest selector that still resolves back to `element` from the document.
|
|
631
|
-
* Grows the path one ancestor at a time and stops as soon as it is unique, so
|
|
632
|
-
* a stable handle anywhere up the tree replaces the whole positional chain.
|
|
633
|
-
* A bare tag is never enough: positional paths keep a parent for context.
|
|
634
|
-
*/
|
|
635
|
-
function buildSelector(element) {
|
|
636
|
-
if (element === document.documentElement) return "html";
|
|
637
|
-
if (element === document.body) return "body";
|
|
638
|
-
const handle = uniqueHandle(element);
|
|
639
|
-
if (handle) return handle;
|
|
640
|
-
const segments = [];
|
|
641
|
-
let current = element;
|
|
642
|
-
for (let depth = 0; current && depth < MAX_DEPTH; depth++) {
|
|
643
|
-
const ancestorHandle = current === element ? null : uniqueHandle(current);
|
|
644
|
-
segments.unshift(ancestorHandle ?? positionalSegment(current));
|
|
645
|
-
const candidate = segments.join(" > ");
|
|
646
|
-
const isAnchored = segments.length >= MIN_PATH_SEGMENTS;
|
|
647
|
-
if (ancestorHandle || current === document.body || isAnchored && isUnique(candidate)) return candidate;
|
|
648
|
-
current = current.parentElement;
|
|
649
|
-
}
|
|
650
|
-
return segments.join(" > ");
|
|
651
|
-
}
|
|
652
|
-
function truncate(value, max) {
|
|
653
|
-
return value.length <= max ? value : `${value.slice(0, max - 1)}…`;
|
|
654
|
-
}
|
|
655
|
-
function collapseText(value) {
|
|
656
|
-
return value.replace(WHITESPACE, " ").trim();
|
|
657
|
-
}
|
|
658
|
-
function accessibleLabel(element) {
|
|
659
|
-
const explicit = element.getAttribute("aria-label") ?? element.getAttribute("title") ?? element.getAttribute("alt") ?? element.getAttribute("placeholder");
|
|
660
|
-
if (explicit?.trim()) return explicit.trim();
|
|
661
|
-
return collapseText(element.textContent ?? "");
|
|
662
|
-
}
|
|
663
|
-
/** Short human label such as `button "Sign in"` or `div.card`. */
|
|
664
|
-
function describeElement(element) {
|
|
665
|
-
const tag = element.localName;
|
|
666
|
-
const label = maskSecrets(accessibleLabel(element));
|
|
667
|
-
if (label) return truncate(`${tag} "${label}"`, MAX_LABEL_LENGTH);
|
|
668
|
-
const firstClass = element.classList.item(0);
|
|
669
|
-
return truncate(firstClass ? `${tag}.${firstClass}` : tag, MAX_LABEL_LENGTH);
|
|
670
|
-
}
|
|
671
|
-
function describeLandmark(element) {
|
|
672
|
-
const landmark = element.parentElement?.closest(LANDMARK_SELECTOR);
|
|
673
|
-
if (!landmark) return null;
|
|
674
|
-
const name = landmark.getAttribute("aria-label") ?? landmark.getAttribute("title");
|
|
675
|
-
return name?.trim() ? `${landmark.localName} "${collapseText(name)}"` : landmark.localName;
|
|
676
|
-
}
|
|
677
|
-
function headingAbove(element) {
|
|
678
|
-
const scope = element.closest(LANDMARK_SELECTOR) ?? document.body;
|
|
679
|
-
const probe = document.createRange();
|
|
680
|
-
probe.selectNodeContents(element);
|
|
681
|
-
probe.collapse(true);
|
|
682
|
-
let closest = null;
|
|
683
|
-
for (const heading of scope.querySelectorAll(HEADING_SELECTOR)) if (probe.comparePoint(heading, 0) < 0) closest = heading;
|
|
684
|
-
return collapseText(closest?.textContent ?? "") || null;
|
|
685
|
-
}
|
|
686
|
-
/**
|
|
687
|
-
* Where the element sits from a reader's point of view — `dialog "Members" ›
|
|
688
|
-
* Danger zone` says more to an agent than any DOM path can.
|
|
689
|
-
*/
|
|
690
|
-
function describeRegion(element) {
|
|
691
|
-
const landmark = describeLandmark(element);
|
|
692
|
-
const heading = headingAbove(element);
|
|
693
|
-
const parts = [landmark];
|
|
694
|
-
if (heading && !landmark?.includes(`"${heading}"`)) parts.push(heading);
|
|
695
|
-
const region = parts.filter((part) => part !== null).join(" › ");
|
|
696
|
-
return region ? truncate(maskSecrets(region), MAX_REGION_LENGTH) : void 0;
|
|
697
|
-
}
|
|
698
|
-
function getElementRect(element) {
|
|
699
|
-
const rect = element.getBoundingClientRect();
|
|
700
|
-
return {
|
|
701
|
-
height: Math.round(rect.height),
|
|
702
|
-
width: Math.round(rect.width),
|
|
703
|
-
x: Math.round(rect.x),
|
|
704
|
-
y: Math.round(rect.y)
|
|
705
|
-
};
|
|
706
|
-
}
|
|
707
|
-
/** Everything an agent needs to find the picked element back in the codebase. */
|
|
708
|
-
function buildElementSelection(element) {
|
|
709
|
-
const { componentStack, sourceLocation } = inspectReactElement(element);
|
|
710
|
-
const text = truncate(visibleText(element), MAX_TEXT_LENGTH);
|
|
711
|
-
return {
|
|
712
|
-
componentStack,
|
|
713
|
-
html: sanitizeMarkup(element, MAX_HTML_LENGTH),
|
|
714
|
-
label: describeElement(element),
|
|
715
|
-
rect: getElementRect(element),
|
|
716
|
-
region: describeRegion(element),
|
|
717
|
-
selector: truncate(buildSelector(element), MAX_SELECTOR_LENGTH),
|
|
718
|
-
sourceLocation: sourceLocation ? truncate(sourceLocation, MAX_SOURCE_LOCATION_LENGTH) : sourceLocation,
|
|
719
|
-
text: text || void 0
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
//#endregion
|
|
723
|
-
//#region src/feedback/core/page-context.ts
|
|
724
|
-
const BROWSER_PATTERNS = [
|
|
725
|
-
["Edge", /Edg(?:e|A|iOS)?\/([\d.]+)/],
|
|
726
|
-
["Opera", /OPR\/([\d.]+)/],
|
|
727
|
-
["Samsung Internet", /SamsungBrowser\/([\d.]+)/],
|
|
728
|
-
["Firefox", /(?:Firefox|FxiOS)\/([\d.]+)/],
|
|
729
|
-
["Chrome", /(?:Chrome|CriOS)\/([\d.]+)/],
|
|
730
|
-
["Safari", /Version\/([\d.]+).*Safari/]
|
|
731
|
-
];
|
|
732
|
-
const OS_PATTERNS = [
|
|
733
|
-
["iOS", /iPhone|iPad|iPod/],
|
|
734
|
-
["Android", /Android/],
|
|
735
|
-
["Windows", /Windows NT/],
|
|
736
|
-
["macOS", /Mac OS X|Macintosh/],
|
|
737
|
-
["ChromeOS", /CrOS/],
|
|
738
|
-
["Linux", /Linux|X11/]
|
|
739
|
-
];
|
|
740
|
-
const MOBILE_HINT = /Mobile|iPhone|iPod/;
|
|
741
|
-
const TABLET_HINT = /iPad|Tablet/;
|
|
742
|
-
const TRAILING_ZERO = /\.0$/;
|
|
743
|
-
const ANDROID = /Android/;
|
|
744
|
-
function shortenVersion(version) {
|
|
745
|
-
return version.split(".").slice(0, 2).join(".").replace(TRAILING_ZERO, "");
|
|
746
|
-
}
|
|
747
|
-
function detectDevice(userAgent) {
|
|
748
|
-
if (TABLET_HINT.test(userAgent)) return "tablet";
|
|
749
|
-
if (MOBILE_HINT.test(userAgent)) return "mobile";
|
|
750
|
-
return ANDROID.test(userAgent) ? "tablet" : "desktop";
|
|
751
|
-
}
|
|
752
|
-
function parseUserAgent(userAgent) {
|
|
753
|
-
const info = { device: detectDevice(userAgent) };
|
|
754
|
-
for (const [name, pattern] of BROWSER_PATTERNS) {
|
|
755
|
-
const version = pattern.exec(userAgent)?.[1];
|
|
756
|
-
if (version) {
|
|
757
|
-
info.browser = `${name} ${shortenVersion(version)}`;
|
|
758
|
-
break;
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
for (const [name, pattern] of OS_PATTERNS) if (pattern.test(userAgent)) {
|
|
762
|
-
info.os = name;
|
|
763
|
-
break;
|
|
764
|
-
}
|
|
765
|
-
return info;
|
|
766
|
-
}
|
|
767
|
-
const MAX_PAGE_TITLE_LENGTH = 300;
|
|
768
|
-
const MAX_URL_LENGTH = 2e3;
|
|
769
|
-
const MAX_USER_AGENT_LENGTH = 600;
|
|
770
|
-
function clip(value, max) {
|
|
771
|
-
return value.length <= max ? value : value.slice(0, max);
|
|
772
|
-
}
|
|
773
|
-
function optionalText(value, max) {
|
|
774
|
-
return value?.trim() ? clip(value, max) : void 0;
|
|
775
|
-
}
|
|
776
|
-
function resolveTimezone() {
|
|
777
|
-
try {
|
|
778
|
-
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
779
|
-
} catch {}
|
|
780
|
-
}
|
|
781
|
-
/** Everything about the page that a bug report needs and the DOM already knows. */
|
|
782
|
-
function collectPageContext(options) {
|
|
783
|
-
const userAgent = navigator.userAgent;
|
|
784
|
-
const agent = parseUserAgent(userAgent);
|
|
785
|
-
return {
|
|
786
|
-
browser: agent.browser,
|
|
787
|
-
device: agent.device,
|
|
788
|
-
language: navigator.language,
|
|
789
|
-
os: agent.os,
|
|
790
|
-
pageTitle: optionalText(document.title, MAX_PAGE_TITLE_LENGTH),
|
|
791
|
-
referrer: optionalText(document.referrer, MAX_URL_LENGTH),
|
|
792
|
-
screen: {
|
|
793
|
-
height: window.screen.height,
|
|
794
|
-
width: window.screen.width
|
|
795
|
-
},
|
|
796
|
-
scroll: {
|
|
797
|
-
x: window.scrollX,
|
|
798
|
-
y: window.scrollY
|
|
799
|
-
},
|
|
800
|
-
sdkVersion: options?.sdkVersion,
|
|
801
|
-
timezone: resolveTimezone(),
|
|
802
|
-
url: redactUrl(window.location.href),
|
|
803
|
-
userAgent: clip(userAgent, MAX_USER_AGENT_LENGTH),
|
|
804
|
-
viewport: {
|
|
805
|
-
devicePixelRatio: window.devicePixelRatio,
|
|
806
|
-
height: window.innerHeight,
|
|
807
|
-
width: window.innerWidth
|
|
808
|
-
}
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
//#endregion
|
|
812
|
-
//#region src/feedback/types.ts
|
|
813
|
-
const SDK_VERSION = "0.4.3";
|
|
814
|
-
const DEFAULT_WIDGET_LABELS = {
|
|
815
|
-
annotateHint: "Draw on the screenshot to point at the problem.",
|
|
816
|
-
attachElement: "Attach this element",
|
|
817
|
-
attachmentUploadFailed: "Your feedback was saved, but some screenshots are still pending. Retry to attach them.",
|
|
818
|
-
attachScreenshot: "Attach a screenshot",
|
|
819
|
-
back: "Back",
|
|
820
|
-
cancel: "Cancel",
|
|
821
|
-
captureFailed: "Screenshot unavailable. Try again or send without it.",
|
|
822
|
-
capturing: "Taking screenshot…",
|
|
823
|
-
clearAnnotations: "Clear drawing",
|
|
824
|
-
descriptionPlaceholder: "What would you like to share?",
|
|
825
|
-
dismissForDays: "Hide for {days} days",
|
|
826
|
-
done: "Done",
|
|
827
|
-
elementNote: "Comment on the picked element",
|
|
828
|
-
elementNotePlaceholder: "What is wrong with this?",
|
|
829
|
-
emailInvalid: "Enter a valid email address.",
|
|
830
|
-
emailLabel: "Email",
|
|
831
|
-
emailPlaceholder: "you@company.com",
|
|
832
|
-
emailPromptBody: "We only use it to follow up on this report.",
|
|
833
|
-
emailPromptTitle: "Add your email?",
|
|
834
|
-
errorGeneric: "Something went wrong. Please try again.",
|
|
835
|
-
errorTitleRequired: "Tell us a bit about it first.",
|
|
836
|
-
minimize: "Minimize feedback",
|
|
837
|
-
moreOptions: "More options",
|
|
838
|
-
moveFeedback: "Move feedback",
|
|
839
|
-
pickAnother: "Pick another",
|
|
840
|
-
pickElement: "Point at an element",
|
|
841
|
-
pickElementHint: "Pick the element you are talking about",
|
|
842
|
-
recapture: "Retake",
|
|
843
|
-
removeScreenshot: "Remove",
|
|
844
|
-
resume: "Resume feedback",
|
|
845
|
-
retakeHint: "Screenshot of this page",
|
|
846
|
-
retryAttachments: "Retry attachments",
|
|
847
|
-
screenshot: "Screenshot",
|
|
848
|
-
sendWithoutEmail: "Send without email",
|
|
849
|
-
submit: "Send feedback",
|
|
850
|
-
successMessage: "We read every report. Thanks for taking the time.",
|
|
851
|
-
successTitle: "Feedback sent",
|
|
852
|
-
title: "Send feedback",
|
|
853
|
-
titlePlaceholder: "Short summary",
|
|
854
|
-
trigger: "Feedback",
|
|
855
|
-
undo: "Undo"
|
|
856
|
-
};
|
|
857
|
-
//#endregion
|
|
858
|
-
//#region src/feedback/core/annotations.ts
|
|
859
|
-
const BOX_TOOLS = [
|
|
860
|
-
"text",
|
|
861
|
-
"rectangle",
|
|
862
|
-
"spotlight",
|
|
863
|
-
"highlight",
|
|
864
|
-
"blur"
|
|
865
|
-
];
|
|
866
|
-
const MIN_GESTURE_LENGTH = 8;
|
|
867
|
-
const DEFAULT_PATH_TOLERANCE = 2.5;
|
|
868
|
-
function normalizeRect(start, end) {
|
|
869
|
-
return {
|
|
870
|
-
height: Math.abs(end.y - start.y),
|
|
871
|
-
width: Math.abs(end.x - start.x),
|
|
872
|
-
x: Math.min(start.x, end.x),
|
|
873
|
-
y: Math.min(start.y, end.y)
|
|
874
|
-
};
|
|
875
|
-
}
|
|
876
|
-
function simplifyPath(points, tolerance = DEFAULT_PATH_TOLERANCE) {
|
|
877
|
-
if (points.length < 3) return points;
|
|
878
|
-
const [first, ...rest] = points;
|
|
879
|
-
if (!first) return points;
|
|
880
|
-
const kept = [first];
|
|
881
|
-
let anchor = first;
|
|
882
|
-
for (const point of rest.slice(0, -1)) if (Math.hypot(point.x - anchor.x, point.y - anchor.y) >= tolerance) {
|
|
883
|
-
kept.push(point);
|
|
884
|
-
anchor = point;
|
|
885
|
-
}
|
|
886
|
-
const last = points.at(-1);
|
|
887
|
-
if (last) kept.push(last);
|
|
888
|
-
return kept;
|
|
889
|
-
}
|
|
890
|
-
function isDegenerate(annotation) {
|
|
891
|
-
if (annotation.tool === "text") return !annotation.text?.trim();
|
|
892
|
-
if (annotation.tool === "pen") return (annotation.points?.length ?? 0) < 2;
|
|
893
|
-
const { start, end } = annotation;
|
|
894
|
-
return Math.hypot(end.x - start.x, end.y - start.y) < MIN_GESTURE_LENGTH;
|
|
895
|
-
}
|
|
896
|
-
function toWire(annotation) {
|
|
897
|
-
const { color, start, end, tool } = annotation;
|
|
898
|
-
if (tool === "pen") return {
|
|
899
|
-
color,
|
|
900
|
-
points: simplifyPath(annotation.points ?? []),
|
|
901
|
-
type: tool,
|
|
902
|
-
x: start.x,
|
|
903
|
-
y: start.y
|
|
904
|
-
};
|
|
905
|
-
if (BOX_TOOLS.includes(tool)) {
|
|
906
|
-
const rect = normalizeRect(start, end);
|
|
907
|
-
return {
|
|
908
|
-
color,
|
|
909
|
-
height: rect.height,
|
|
910
|
-
...tool === "text" ? { text: annotation.text } : {},
|
|
911
|
-
type: tool,
|
|
912
|
-
width: rect.width,
|
|
913
|
-
x: rect.x,
|
|
914
|
-
y: rect.y
|
|
915
|
-
};
|
|
916
|
-
}
|
|
917
|
-
return {
|
|
918
|
-
color,
|
|
919
|
-
endX: end.x,
|
|
920
|
-
endY: end.y,
|
|
921
|
-
type: tool,
|
|
922
|
-
x: start.x,
|
|
923
|
-
y: start.y
|
|
924
|
-
};
|
|
925
|
-
}
|
|
926
|
-
function toWireAnnotations(annotations) {
|
|
927
|
-
return annotations.filter((item) => !isDegenerate(item)).map(toWire);
|
|
928
|
-
}
|
|
929
|
-
//#endregion
|
|
930
10
|
//#region src/feedback/ui/annotation/color-value.ts
|
|
931
11
|
const DEFAULT_ANNOTATION_COLOR = {
|
|
932
12
|
baseColor: "#ef4444",
|
|
@@ -943,118 +23,6 @@ function annotationColorHex({ baseColor, strengthPercent }) {
|
|
|
943
23
|
}).join("")}`;
|
|
944
24
|
}
|
|
945
25
|
//#endregion
|
|
946
|
-
//#region src/feedback/ui/icons.tsx
|
|
947
|
-
function Svg({ children, className, size = 16 }) {
|
|
948
|
-
return /* @__PURE__ */ jsx("svg", {
|
|
949
|
-
"aria-hidden": "true",
|
|
950
|
-
className,
|
|
951
|
-
fill: "none",
|
|
952
|
-
height: size,
|
|
953
|
-
stroke: "currentColor",
|
|
954
|
-
strokeLinecap: "round",
|
|
955
|
-
strokeLinejoin: "round",
|
|
956
|
-
strokeWidth: "1.8",
|
|
957
|
-
viewBox: "0 0 24 24",
|
|
958
|
-
width: size,
|
|
959
|
-
children
|
|
960
|
-
});
|
|
961
|
-
}
|
|
962
|
-
function ChatIcon({ className }) {
|
|
963
|
-
return /* @__PURE__ */ jsx(Svg, {
|
|
964
|
-
className,
|
|
965
|
-
size: 15,
|
|
966
|
-
children: /* @__PURE__ */ jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" })
|
|
967
|
-
});
|
|
968
|
-
}
|
|
969
|
-
function CloseIcon({ className }) {
|
|
970
|
-
return /* @__PURE__ */ jsx(Svg, {
|
|
971
|
-
className,
|
|
972
|
-
children: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" })
|
|
973
|
-
});
|
|
974
|
-
}
|
|
975
|
-
function CameraIcon() {
|
|
976
|
-
return /* @__PURE__ */ jsxs(Svg, { children: [/* @__PURE__ */ jsx("path", { d: "M3 8.5A1.5 1.5 0 0 1 4.5 7h2.2l1.2-2h8.2l1.2 2h2.2A1.5 1.5 0 0 1 21 8.5v9A1.5 1.5 0 0 1 19.5 19h-15A1.5 1.5 0 0 1 3 17.5z" }), /* @__PURE__ */ jsx("circle", {
|
|
977
|
-
cx: "12",
|
|
978
|
-
cy: "13",
|
|
979
|
-
r: "3.2"
|
|
980
|
-
})] });
|
|
981
|
-
}
|
|
982
|
-
function PencilIcon() {
|
|
983
|
-
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M16.5 4.5a2.1 2.1 0 0 1 3 3L8 19l-4 1 1-4z" }) });
|
|
984
|
-
}
|
|
985
|
-
function TargetIcon() {
|
|
986
|
-
return /* @__PURE__ */ jsxs(Svg, { children: [/* @__PURE__ */ jsx("path", { d: "M12 3v3M12 18v3M3 12h3M18 12h3" }), /* @__PURE__ */ jsx("circle", {
|
|
987
|
-
cx: "12",
|
|
988
|
-
cy: "12",
|
|
989
|
-
r: "5"
|
|
990
|
-
})] });
|
|
991
|
-
}
|
|
992
|
-
function TrashIcon() {
|
|
993
|
-
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M4 7h16M9 7V5h6v2M6 7l1 13h10l1-13M10 11v5M14 11v5" }) });
|
|
994
|
-
}
|
|
995
|
-
function ArrowIcon() {
|
|
996
|
-
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M5 19 19 5M11 5h8v8" }) });
|
|
997
|
-
}
|
|
998
|
-
function UndoIcon() {
|
|
999
|
-
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M4 9h11a5 5 0 0 1 0 10h-5M4 9l4-4M4 9l4 4" }) });
|
|
1000
|
-
}
|
|
1001
|
-
function CheckIcon({ size = 28 }) {
|
|
1002
|
-
return /* @__PURE__ */ jsx("svg", {
|
|
1003
|
-
"aria-hidden": "true",
|
|
1004
|
-
fill: "none",
|
|
1005
|
-
height: size,
|
|
1006
|
-
stroke: "currentColor",
|
|
1007
|
-
strokeLinecap: "round",
|
|
1008
|
-
strokeLinejoin: "round",
|
|
1009
|
-
strokeWidth: "2.4",
|
|
1010
|
-
viewBox: "0 0 24 24",
|
|
1011
|
-
width: size,
|
|
1012
|
-
children: /* @__PURE__ */ jsx("path", { d: "m5 13 4 4L19 7" })
|
|
1013
|
-
});
|
|
1014
|
-
}
|
|
1015
|
-
function GripIcon() {
|
|
1016
|
-
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M9 5h.01M15 5h.01M9 12h.01M15 12h.01M9 19h.01M15 19h.01" }) });
|
|
1017
|
-
}
|
|
1018
|
-
function MoreIcon() {
|
|
1019
|
-
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M5 12h.01M12 12h.01M19 12h.01" }) });
|
|
1020
|
-
}
|
|
1021
|
-
function ChevronDownIcon() {
|
|
1022
|
-
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "m7 10 5 5 5-5" }) });
|
|
1023
|
-
}
|
|
1024
|
-
function SquareIcon() {
|
|
1025
|
-
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("rect", {
|
|
1026
|
-
height: "14",
|
|
1027
|
-
rx: "2",
|
|
1028
|
-
width: "14",
|
|
1029
|
-
x: "5",
|
|
1030
|
-
y: "5"
|
|
1031
|
-
}) });
|
|
1032
|
-
}
|
|
1033
|
-
function TextIcon() {
|
|
1034
|
-
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M5 6V4h14v2M12 4v16M8 20h8" }) });
|
|
1035
|
-
}
|
|
1036
|
-
function SpotlightIcon() {
|
|
1037
|
-
return /* @__PURE__ */ jsxs(Svg, { children: [/* @__PURE__ */ jsx("path", { d: "M8 4H4v4M16 4h4v4M4 16v4h4M20 16v4h-4" }), /* @__PURE__ */ jsx("rect", {
|
|
1038
|
-
height: "8",
|
|
1039
|
-
rx: "1",
|
|
1040
|
-
width: "8",
|
|
1041
|
-
x: "8",
|
|
1042
|
-
y: "8"
|
|
1043
|
-
})] });
|
|
1044
|
-
}
|
|
1045
|
-
function HighlightIcon() {
|
|
1046
|
-
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M4 19h16M8 15l8-11 4 3-8 11-4-3ZM7 15l-2 3" }) });
|
|
1047
|
-
}
|
|
1048
|
-
function BlurIcon() {
|
|
1049
|
-
return /* @__PURE__ */ jsxs(Svg, { children: [/* @__PURE__ */ jsx("rect", {
|
|
1050
|
-
height: "14",
|
|
1051
|
-
rx: "2",
|
|
1052
|
-
width: "14",
|
|
1053
|
-
x: "5",
|
|
1054
|
-
y: "5"
|
|
1055
|
-
}), /* @__PURE__ */ jsx("path", { d: "M8 8h8v8H8z" })] });
|
|
1056
|
-
}
|
|
1057
|
-
//#endregion
|
|
1058
26
|
//#region src/feedback/ui/annotation/text/text-editor.tsx
|
|
1059
27
|
function TextAnnotationEditor({ draft, onCommit, onCancel }) {
|
|
1060
28
|
const [text, setText] = useState(draft.annotation.text ?? "");
|
|
@@ -1169,54 +137,6 @@ function useTextAnnotation(options) {
|
|
|
1169
137
|
};
|
|
1170
138
|
}
|
|
1171
139
|
//#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
|
|
1220
140
|
//#region src/feedback/ui/floating/use-details-popover.ts
|
|
1221
141
|
function useDetailsPopover() {
|
|
1222
142
|
const [open, setOpen] = useState(false);
|
|
@@ -1492,179 +412,6 @@ function AnnotationToolbar({ editor, labels, drawing }) {
|
|
|
1492
412
|
});
|
|
1493
413
|
}
|
|
1494
414
|
//#endregion
|
|
1495
|
-
//#region src/feedback/core/arrow-geometry.ts
|
|
1496
|
-
function arrowOutline(start, end) {
|
|
1497
|
-
const dx = end.x - start.x;
|
|
1498
|
-
const dy = end.y - start.y;
|
|
1499
|
-
const length = Math.hypot(dx, dy);
|
|
1500
|
-
if (length === 0) return {
|
|
1501
|
-
points: [],
|
|
1502
|
-
strokeWidth: 0
|
|
1503
|
-
};
|
|
1504
|
-
const shaftWidth = Math.min(20, Math.max(6, length * .025), length * .2);
|
|
1505
|
-
const headLength = Math.min(length * .45, shaftWidth * 3);
|
|
1506
|
-
const headHalfWidth = headLength * .55;
|
|
1507
|
-
const shaftLength = length - headLength;
|
|
1508
|
-
const pointOnArrow = (along, across) => ({
|
|
1509
|
-
x: start.x + (dx * along - dy * across) / length,
|
|
1510
|
-
y: start.y + (dy * along + dx * across) / length
|
|
1511
|
-
});
|
|
1512
|
-
return {
|
|
1513
|
-
points: [
|
|
1514
|
-
pointOnArrow(0, 0),
|
|
1515
|
-
pointOnArrow(shaftLength, -shaftWidth / 2),
|
|
1516
|
-
pointOnArrow(shaftLength, -headHalfWidth),
|
|
1517
|
-
{
|
|
1518
|
-
x: end.x,
|
|
1519
|
-
y: end.y
|
|
1520
|
-
},
|
|
1521
|
-
pointOnArrow(shaftLength, headHalfWidth),
|
|
1522
|
-
pointOnArrow(shaftLength, shaftWidth / 2)
|
|
1523
|
-
],
|
|
1524
|
-
strokeWidth: Math.min(4, length * .02)
|
|
1525
|
-
};
|
|
1526
|
-
}
|
|
1527
|
-
//#endregion
|
|
1528
|
-
//#region src/feedback/core/annotation-renderer.ts
|
|
1529
|
-
const STROKE_WIDTH = 3;
|
|
1530
|
-
const HIGHLIGHT_ALPHA = .3;
|
|
1531
|
-
const REDACTION_FILL = "#111827";
|
|
1532
|
-
const PIXEL_BLOCK = 10;
|
|
1533
|
-
function scalePoint(point, scale) {
|
|
1534
|
-
return {
|
|
1535
|
-
x: point.x * scale,
|
|
1536
|
-
y: point.y * scale
|
|
1537
|
-
};
|
|
1538
|
-
}
|
|
1539
|
-
function scaleRect(rect, scale) {
|
|
1540
|
-
return {
|
|
1541
|
-
height: rect.height * scale,
|
|
1542
|
-
width: rect.width * scale,
|
|
1543
|
-
x: rect.x * scale,
|
|
1544
|
-
y: rect.y * scale
|
|
1545
|
-
};
|
|
1546
|
-
}
|
|
1547
|
-
function isCanvasElement(value) {
|
|
1548
|
-
return typeof HTMLCanvasElement !== "undefined" && value instanceof HTMLCanvasElement;
|
|
1549
|
-
}
|
|
1550
|
-
function redact(context, rect) {
|
|
1551
|
-
const source = context.canvas;
|
|
1552
|
-
const width = Math.max(1, Math.round(rect.width / PIXEL_BLOCK));
|
|
1553
|
-
const height = Math.max(1, Math.round(rect.height / PIXEL_BLOCK));
|
|
1554
|
-
if (isCanvasElement(source)) {
|
|
1555
|
-
const offscreen = document.createElement("canvas");
|
|
1556
|
-
offscreen.width = width;
|
|
1557
|
-
offscreen.height = height;
|
|
1558
|
-
const offscreenContext = offscreen.getContext("2d");
|
|
1559
|
-
if (offscreenContext) {
|
|
1560
|
-
offscreenContext.imageSmoothingEnabled = true;
|
|
1561
|
-
offscreenContext.drawImage(source, rect.x, rect.y, rect.width, rect.height, 0, 0, width, height);
|
|
1562
|
-
context.drawImage(offscreen, rect.x, rect.y, rect.width, rect.height);
|
|
1563
|
-
return;
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
context.fillStyle = REDACTION_FILL;
|
|
1567
|
-
context.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
1568
|
-
}
|
|
1569
|
-
function drawArrow(context, annotation, scale) {
|
|
1570
|
-
const { points, strokeWidth } = arrowOutline(annotation.start, annotation.end);
|
|
1571
|
-
const [first, ...rest] = points.map((point) => scalePoint(point, scale));
|
|
1572
|
-
if (!first) return;
|
|
1573
|
-
context.beginPath();
|
|
1574
|
-
context.moveTo(first.x, first.y);
|
|
1575
|
-
for (const point of rest) context.lineTo(point.x, point.y);
|
|
1576
|
-
context.closePath();
|
|
1577
|
-
context.lineWidth = strokeWidth * scale;
|
|
1578
|
-
context.fill();
|
|
1579
|
-
context.stroke();
|
|
1580
|
-
}
|
|
1581
|
-
function drawPen(context, points, scale) {
|
|
1582
|
-
const [first, ...rest] = points;
|
|
1583
|
-
if (!first) return;
|
|
1584
|
-
const head = scalePoint(first, scale);
|
|
1585
|
-
context.beginPath();
|
|
1586
|
-
context.moveTo(head.x, head.y);
|
|
1587
|
-
for (const point of rest) {
|
|
1588
|
-
const scaled = scalePoint(point, scale);
|
|
1589
|
-
context.lineTo(scaled.x, scaled.y);
|
|
1590
|
-
}
|
|
1591
|
-
context.stroke();
|
|
1592
|
-
}
|
|
1593
|
-
function drawSpotlight(context, rect) {
|
|
1594
|
-
const { width, height } = context.canvas;
|
|
1595
|
-
context.save();
|
|
1596
|
-
context.fillStyle = REDACTION_FILL;
|
|
1597
|
-
context.globalAlpha = .55;
|
|
1598
|
-
context.fillRect(0, 0, width, rect.y);
|
|
1599
|
-
context.fillRect(0, rect.y + rect.height, width, height - rect.y - rect.height);
|
|
1600
|
-
context.fillRect(0, rect.y, rect.x, rect.height);
|
|
1601
|
-
context.fillRect(rect.x + rect.width, rect.y, width - rect.x - rect.width, rect.height);
|
|
1602
|
-
context.restore();
|
|
1603
|
-
context.strokeRect(rect.x, rect.y, rect.width, rect.height);
|
|
1604
|
-
}
|
|
1605
|
-
function drawAnnotation(context, annotation, scale) {
|
|
1606
|
-
const { color, tool } = annotation;
|
|
1607
|
-
const rect = scaleRect(normalizeRect(annotation.start, annotation.end), scale);
|
|
1608
|
-
context.save();
|
|
1609
|
-
context.lineWidth = STROKE_WIDTH * scale;
|
|
1610
|
-
context.lineCap = "round";
|
|
1611
|
-
context.lineJoin = "round";
|
|
1612
|
-
context.strokeStyle = color;
|
|
1613
|
-
context.fillStyle = color;
|
|
1614
|
-
switch (tool) {
|
|
1615
|
-
case "text": {
|
|
1616
|
-
const lines = (annotation.text ?? "").split("\n");
|
|
1617
|
-
const lineHeight = rect.height / lines.length;
|
|
1618
|
-
context.font = `${lineHeight / 1.4}px ui-sans-serif, system-ui, sans-serif`;
|
|
1619
|
-
context.textBaseline = "top";
|
|
1620
|
-
for (const [index, line] of lines.entries()) context.fillText(line, rect.x, rect.y + index * lineHeight);
|
|
1621
|
-
break;
|
|
1622
|
-
}
|
|
1623
|
-
case "pen":
|
|
1624
|
-
drawPen(context, annotation.points ?? [], scale);
|
|
1625
|
-
break;
|
|
1626
|
-
case "arrow":
|
|
1627
|
-
drawArrow(context, annotation, scale);
|
|
1628
|
-
break;
|
|
1629
|
-
case "spotlight":
|
|
1630
|
-
drawSpotlight(context, rect);
|
|
1631
|
-
break;
|
|
1632
|
-
case "rectangle":
|
|
1633
|
-
context.strokeRect(rect.x, rect.y, rect.width, rect.height);
|
|
1634
|
-
break;
|
|
1635
|
-
case "highlight":
|
|
1636
|
-
context.globalAlpha = HIGHLIGHT_ALPHA;
|
|
1637
|
-
context.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
1638
|
-
break;
|
|
1639
|
-
default: redact(context, rect);
|
|
1640
|
-
}
|
|
1641
|
-
context.restore();
|
|
1642
|
-
}
|
|
1643
|
-
function drawAnnotations(context, annotations, options) {
|
|
1644
|
-
const scale = options?.scale ?? 1;
|
|
1645
|
-
for (const annotation of annotations) if (!isDegenerate(annotation)) drawAnnotation(context, annotation, scale);
|
|
1646
|
-
}
|
|
1647
|
-
function loadImage(url) {
|
|
1648
|
-
return new Promise((resolve, reject) => {
|
|
1649
|
-
const image = new Image();
|
|
1650
|
-
image.onload = () => resolve(image);
|
|
1651
|
-
image.onerror = () => reject(/* @__PURE__ */ new Error("Screenshot could not be decoded"));
|
|
1652
|
-
image.src = url;
|
|
1653
|
-
});
|
|
1654
|
-
}
|
|
1655
|
-
async function renderAnnotatedImage(source, annotations) {
|
|
1656
|
-
if (annotations.length === 0) return null;
|
|
1657
|
-
const canvas = document.createElement("canvas");
|
|
1658
|
-
canvas.width = source.width;
|
|
1659
|
-
canvas.height = source.height;
|
|
1660
|
-
const context = canvas.getContext("2d");
|
|
1661
|
-
if (!context) return null;
|
|
1662
|
-
const image = await loadImage(source.objectUrl);
|
|
1663
|
-
context.drawImage(image, 0, 0, source.width, source.height);
|
|
1664
|
-
drawAnnotations(context, annotations);
|
|
1665
|
-
return await capturedFromCanvas(canvas, source.width);
|
|
1666
|
-
}
|
|
1667
|
-
//#endregion
|
|
1668
415
|
//#region src/feedback/ui/annotation/use-annotation-canvas.ts
|
|
1669
416
|
function useAnnotationCanvas(capture, annotations) {
|
|
1670
417
|
const canvasRef = useRef(null);
|
|
@@ -1918,6 +665,42 @@ function Annotator({ capture, labels, editor, note }) {
|
|
|
1918
665
|
});
|
|
1919
666
|
}
|
|
1920
667
|
//#endregion
|
|
668
|
+
//#region src/feedback/ui/devtools-owner.ts
|
|
669
|
+
const claims = /* @__PURE__ */ new Map();
|
|
670
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
671
|
+
function notify() {
|
|
672
|
+
for (const listener of listeners) listener();
|
|
673
|
+
}
|
|
674
|
+
function subscribe(listener) {
|
|
675
|
+
listeners.add(listener);
|
|
676
|
+
return () => listeners.delete(listener);
|
|
677
|
+
}
|
|
678
|
+
function currentOwner() {
|
|
679
|
+
for (const [token, claim] of claims) if (claim.hasWidget) return token;
|
|
680
|
+
return claims.keys().next().value;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* One devtools bar per page, however many RefletFeedback instances mount:
|
|
684
|
+
* the one with a visible widget wins, so its bar can step aside for its panel.
|
|
685
|
+
*/
|
|
686
|
+
function useOwnsDevtools(wantsDevtools, hasWidget) {
|
|
687
|
+
const [token] = useState(() => Symbol("reflet-devtools"));
|
|
688
|
+
useEffect(() => {
|
|
689
|
+
if (!wantsDevtools) return;
|
|
690
|
+
claims.set(token, { hasWidget });
|
|
691
|
+
notify();
|
|
692
|
+
return () => {
|
|
693
|
+
claims.delete(token);
|
|
694
|
+
notify();
|
|
695
|
+
};
|
|
696
|
+
}, [
|
|
697
|
+
hasWidget,
|
|
698
|
+
token,
|
|
699
|
+
wantsDevtools
|
|
700
|
+
]);
|
|
701
|
+
return useSyncExternalStore(subscribe, () => currentOwner() === token, () => false);
|
|
702
|
+
}
|
|
703
|
+
//#endregion
|
|
1921
704
|
//#region src/feedback/ui/launcher.tsx
|
|
1922
705
|
function Launcher({ isOpen, label, onClick, buttonRef }) {
|
|
1923
706
|
return /* @__PURE__ */ jsxs("button", {
|
|
@@ -2458,52 +1241,6 @@ function FeedbackPanel({ floatingControls, labels, state }) {
|
|
|
2458
1241
|
});
|
|
2459
1242
|
}
|
|
2460
1243
|
//#endregion
|
|
2461
|
-
//#region src/feedback/ui/selection-outline.tsx
|
|
2462
|
-
/**
|
|
2463
|
-
* Keeps the picked element ringed while the reporter writes about it, so the
|
|
2464
|
-
* composer parked next to it is visibly about that element.
|
|
2465
|
-
*/
|
|
2466
|
-
function SelectionOutline({ node }) {
|
|
2467
|
-
const boxRef = useRef(null);
|
|
2468
|
-
useEffect(() => {
|
|
2469
|
-
const box = boxRef.current;
|
|
2470
|
-
if (!(box && node)) return;
|
|
2471
|
-
let frame = 0;
|
|
2472
|
-
const draw = () => {
|
|
2473
|
-
frame = 0;
|
|
2474
|
-
const rect = node.getBoundingClientRect();
|
|
2475
|
-
const hidden = rect.width === 0 || rect.height === 0 || rect.bottom < 0 || rect.top > window.innerHeight;
|
|
2476
|
-
box.style.opacity = hidden ? "0" : "1";
|
|
2477
|
-
box.style.width = `${rect.width}px`;
|
|
2478
|
-
box.style.height = `${rect.height}px`;
|
|
2479
|
-
box.style.transform = `translate(${rect.left}px, ${rect.top}px)`;
|
|
2480
|
-
};
|
|
2481
|
-
const schedule = () => {
|
|
2482
|
-
frame ||= requestAnimationFrame(draw);
|
|
2483
|
-
};
|
|
2484
|
-
draw();
|
|
2485
|
-
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(draw);
|
|
2486
|
-
observer?.observe(node);
|
|
2487
|
-
window.addEventListener("scroll", schedule, {
|
|
2488
|
-
capture: true,
|
|
2489
|
-
passive: true
|
|
2490
|
-
});
|
|
2491
|
-
window.addEventListener("resize", schedule);
|
|
2492
|
-
return () => {
|
|
2493
|
-
cancelAnimationFrame(frame);
|
|
2494
|
-
observer?.disconnect();
|
|
2495
|
-
window.removeEventListener("scroll", schedule, true);
|
|
2496
|
-
window.removeEventListener("resize", schedule);
|
|
2497
|
-
};
|
|
2498
|
-
}, [node]);
|
|
2499
|
-
if (!node) return null;
|
|
2500
|
-
return /* @__PURE__ */ jsx("div", {
|
|
2501
|
-
"aria-hidden": "true",
|
|
2502
|
-
className: "selection-outline",
|
|
2503
|
-
ref: boxRef
|
|
2504
|
-
});
|
|
2505
|
-
}
|
|
2506
|
-
//#endregion
|
|
2507
1244
|
//#region src/feedback/core/console-recorder.ts
|
|
2508
1245
|
const DEFAULT_LIMIT = 30;
|
|
2509
1246
|
const MAX_MESSAGE_LENGTH = 1e3;
|
|
@@ -2578,114 +1315,6 @@ function startConsoleRecorder(options) {
|
|
|
2578
1315
|
};
|
|
2579
1316
|
}
|
|
2580
1317
|
//#endregion
|
|
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);
|
|
2585
|
-
}
|
|
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
|
-
}
|
|
2618
|
-
}
|
|
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
|
-
});
|
|
2624
|
-
}
|
|
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()}…`;
|
|
2635
|
-
}
|
|
2636
|
-
function buildDescription(params) {
|
|
2637
|
-
const { email, isAnonymous, message } = params;
|
|
2638
|
-
const contact = isAnonymous && email ? `\n\n---\nContact: ${email}` : "";
|
|
2639
|
-
return `${message.trim()}${contact}`;
|
|
2640
|
-
}
|
|
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
|
-
};
|
|
2651
|
-
}
|
|
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
|
-
});
|
|
2662
|
-
}
|
|
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));
|
|
2666
|
-
}
|
|
2667
|
-
function isStorageResponse(value) {
|
|
2668
|
-
return typeof value === "object" && value !== null && typeof Reflect.get(value, "storageId") === "string";
|
|
2669
|
-
}
|
|
2670
|
-
function createFeedbackTransport(client) {
|
|
2671
|
-
return {
|
|
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
|
-
}
|
|
2686
|
-
};
|
|
2687
|
-
}
|
|
2688
|
-
//#endregion
|
|
2689
1318
|
//#region src/feedback/ui/state/use-feedback-submission.ts
|
|
2690
1319
|
function useFeedbackSubmission(options) {
|
|
2691
1320
|
const [activeRequest, setActiveRequest] = useState(null);
|
|
@@ -3109,7 +1738,7 @@ function useWidgetState(props) {
|
|
|
3109
1738
|
setStep("compose");
|
|
3110
1739
|
return submission.submit({
|
|
3111
1740
|
context: {
|
|
3112
|
-
...activeScreenshot?.context ?? collectPageContext({ sdkVersion: "0.
|
|
1741
|
+
...activeScreenshot?.context ?? collectPageContext({ sdkVersion: "0.5.0" }),
|
|
3113
1742
|
consoleEvents: consoleRef.current?.events(),
|
|
3114
1743
|
metadata: props.metadata,
|
|
3115
1744
|
selections
|
|
@@ -3173,155 +1802,6 @@ function useWidgetState(props) {
|
|
|
3173
1802
|
};
|
|
3174
1803
|
}
|
|
3175
1804
|
//#endregion
|
|
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
|
-
};
|
|
3196
|
-
}
|
|
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
|
-
};
|
|
3206
|
-
}
|
|
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
|
-
};
|
|
3234
|
-
}
|
|
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
|
-
};
|
|
3323
|
-
}
|
|
3324
|
-
//#endregion
|
|
3325
1805
|
//#region src/feedback/ui/floating/floating-widget.tsx
|
|
3326
1806
|
function FloatingWidget({ hotkey, labels, position, state }) {
|
|
3327
1807
|
const [minimized, setMinimized] = useState(false);
|
|
@@ -3410,309 +1890,6 @@ function FloatingWidget({ hotkey, labels, position, state }) {
|
|
|
3410
1890
|
});
|
|
3411
1891
|
}
|
|
3412
1892
|
//#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;
|
|
3427
|
-
}
|
|
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;
|
|
3432
|
-
}
|
|
3433
|
-
function pickableElements() {
|
|
3434
|
-
return Array.from(document.body.querySelectorAll("*")).filter(isPickable);
|
|
3435
|
-
}
|
|
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;
|
|
3442
|
-
}
|
|
3443
|
-
function climb(from) {
|
|
3444
|
-
let candidate = from.parentElement;
|
|
3445
|
-
while (candidate && !isPickable(candidate)) candidate = candidate.parentElement;
|
|
3446
|
-
return candidate;
|
|
3447
|
-
}
|
|
3448
|
-
function descend(from) {
|
|
3449
|
-
let candidate = from.firstElementChild;
|
|
3450
|
-
while (candidate && !isPickable(candidate)) candidate = candidate.nextElementSibling;
|
|
3451
|
-
return candidate;
|
|
3452
|
-
}
|
|
3453
|
-
function sibling(from, direction) {
|
|
3454
|
-
let candidate = from[direction];
|
|
3455
|
-
while (candidate && !isPickable(candidate)) candidate = candidate[direction];
|
|
3456
|
-
return candidate;
|
|
3457
|
-
}
|
|
3458
|
-
function stepAim(from, key, backwards) {
|
|
3459
|
-
if (!(from && isPickable(from))) return pickableElements()[0] ?? null;
|
|
3460
|
-
if (key === "Tab") return documentStep(from, backwards);
|
|
3461
|
-
if (key === "ArrowUp") return climb(from);
|
|
3462
|
-
if (key === "ArrowDown") return descend(from);
|
|
3463
|
-
const direction = ARROW_SIBLING[key];
|
|
3464
|
-
return direction ? sibling(from, direction) : null;
|
|
3465
|
-
}
|
|
3466
|
-
//#endregion
|
|
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;
|
|
3477
|
-
}
|
|
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
|
-
};
|
|
3486
|
-
}
|
|
3487
|
-
/**
|
|
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.
|
|
3492
|
-
*/
|
|
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
|
|
3500
|
-
};
|
|
3501
|
-
return {
|
|
3502
|
-
bottom: window.innerHeight - visibleTop + NOTE_GAP,
|
|
3503
|
-
left
|
|
3504
|
-
};
|
|
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
1893
|
//#region src/feedback/ui/annotation/styles.ts
|
|
3717
1894
|
const ANNOTATOR_STYLES = `
|
|
3718
1895
|
.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; }
|
|
@@ -4233,20 +2410,23 @@ function ShadowPortal({ children, offset, primaryColor, theme }) {
|
|
|
4233
2410
|
}
|
|
4234
2411
|
//#endregion
|
|
4235
2412
|
//#region src/feedback/widget.tsx
|
|
2413
|
+
const DevtoolsLayer = process.env.NODE_ENV === "development" ? lazy(() => import("./devtools-layer-BYGuFmhg.js").then((module) => ({ default: module.DevtoolsLayer }))) : null;
|
|
4236
2414
|
function RefletFeedback(props) {
|
|
4237
|
-
const { enabled = true, hotkey = null, offset = 20, position = "bottom-right", theme = "auto" } = props;
|
|
2415
|
+
const { devtools = true, enabled = true, hotkey = null, offset = 20, position = "bottom-right", theme = "auto" } = props;
|
|
4238
2416
|
const labels = {
|
|
4239
2417
|
...DEFAULT_WIDGET_LABELS,
|
|
4240
2418
|
...props.labels
|
|
4241
2419
|
};
|
|
4242
2420
|
const state = useWidgetState(props);
|
|
4243
2421
|
const { isOpen } = state;
|
|
4244
|
-
|
|
2422
|
+
const showsWidget = enabled && !state.isDismissed;
|
|
2423
|
+
const showsDevtools = useOwnsDevtools(devtools && DevtoolsLayer !== null, showsWidget) && DevtoolsLayer !== null;
|
|
2424
|
+
if (!(showsWidget || showsDevtools)) return null;
|
|
4245
2425
|
return /* @__PURE__ */ jsxs(ShadowPortal, {
|
|
4246
2426
|
offset,
|
|
4247
2427
|
primaryColor: props.primaryColor,
|
|
4248
2428
|
theme,
|
|
4249
|
-
children: [
|
|
2429
|
+
children: [showsWidget && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4250
2430
|
/* @__PURE__ */ jsx("div", {
|
|
4251
2431
|
"aria-hidden": "true",
|
|
4252
2432
|
className: "capture-halo",
|
|
@@ -4283,7 +2463,13 @@ function RefletFeedback(props) {
|
|
|
4283
2463
|
onCancel: () => state.setStep("compose"),
|
|
4284
2464
|
onPick: state.selectElement
|
|
4285
2465
|
})
|
|
4286
|
-
]
|
|
2466
|
+
] }), showsDevtools && /* @__PURE__ */ jsx(Suspense, {
|
|
2467
|
+
fallback: null,
|
|
2468
|
+
children: /* @__PURE__ */ jsx(DevtoolsLayer, {
|
|
2469
|
+
isWidgetOpen: showsWidget && isOpen,
|
|
2470
|
+
position
|
|
2471
|
+
})
|
|
2472
|
+
})]
|
|
4287
2473
|
});
|
|
4288
2474
|
}
|
|
4289
2475
|
//#endregion
|