reflet-sdk 0.3.0 → 0.3.2
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 +25 -0
- package/dist/client-BYuSKJw5.js.map +1 -1
- package/dist/{client-CSyPxFPK.d.ts → client-DR_mRfak.d.ts} +2 -2
- package/dist/{client-CSyPxFPK.d.ts.map → client-DR_mRfak.d.ts.map} +1 -1
- package/dist/feedback.d.ts +1 -1
- package/dist/feedback.d.ts.map +1 -1
- package/dist/feedback.js +303 -59
- package/dist/feedback.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/{types-UKPPJGrA.d.ts → types-Dp37I5ES.d.ts} +8 -1
- package/dist/types-Dp37I5ES.d.ts.map +1 -0
- package/package.json +1 -1
- package/dist/types-UKPPJGrA.d.ts.map +0 -1
package/dist/feedback.js
CHANGED
|
@@ -17,7 +17,8 @@ function buildSnapdomOptions(options) {
|
|
|
17
17
|
dpr: Math.min(Math.max(ratio || 1, 1), MAX_PIXEL_RATIO),
|
|
18
18
|
embedFonts: true,
|
|
19
19
|
exclude: [`[${WIDGET_MARKER}]`, ...options.excludeSelectors ?? []],
|
|
20
|
-
fast: true
|
|
20
|
+
fast: true,
|
|
21
|
+
reconcile: true
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
function fitWithin(size, maxWidth) {
|
|
@@ -48,6 +49,23 @@ function downscale(canvas, target) {
|
|
|
48
49
|
context.drawImage(canvas, 0, 0, target.width, target.height);
|
|
49
50
|
return resized;
|
|
50
51
|
}
|
|
52
|
+
/** Wraps a rendered canvas as an owned capture, downscaled to `maxWidth`. */
|
|
53
|
+
async function capturedFromCanvas(source, maxWidth) {
|
|
54
|
+
const fitted = fitWithin({
|
|
55
|
+
height: source.height,
|
|
56
|
+
width: source.width
|
|
57
|
+
}, maxWidth);
|
|
58
|
+
const output = fitted.scale === 1 ? source : downscale(source, fitted);
|
|
59
|
+
const blob = await toBlob$1(output);
|
|
60
|
+
if (!blob) return null;
|
|
61
|
+
return {
|
|
62
|
+
blob,
|
|
63
|
+
height: output.height,
|
|
64
|
+
mimeType: "image/png",
|
|
65
|
+
objectUrl: URL.createObjectURL(blob),
|
|
66
|
+
width: output.width
|
|
67
|
+
};
|
|
68
|
+
}
|
|
51
69
|
/**
|
|
52
70
|
* Renders what the user is looking at right now.
|
|
53
71
|
*
|
|
@@ -60,25 +78,11 @@ async function captureViewport(options = {}) {
|
|
|
60
78
|
try {
|
|
61
79
|
const { snapdom } = await import("@zumer/snapdom");
|
|
62
80
|
const target = options.element ?? document.documentElement;
|
|
63
|
-
|
|
81
|
+
return await capturedFromCanvas(await snapdom.toCanvas(target, buildSnapdomOptions({
|
|
64
82
|
devicePixelRatio: window.devicePixelRatio,
|
|
65
83
|
element: options.element,
|
|
66
84
|
excludeSelectors: options.excludeSelectors
|
|
67
|
-
}));
|
|
68
|
-
const fitted = fitWithin({
|
|
69
|
-
height: canvas.height,
|
|
70
|
-
width: canvas.width
|
|
71
|
-
}, options.maxWidth ?? DEFAULT_MAX_WIDTH);
|
|
72
|
-
const output = fitted.scale === 1 ? canvas : downscale(canvas, fitted);
|
|
73
|
-
const blob = await toBlob$1(output);
|
|
74
|
-
if (!blob) return null;
|
|
75
|
-
return {
|
|
76
|
-
blob,
|
|
77
|
-
height: output.height,
|
|
78
|
-
mimeType: "image/png",
|
|
79
|
-
objectUrl: URL.createObjectURL(blob),
|
|
80
|
-
width: output.width
|
|
81
|
-
};
|
|
85
|
+
})), options.maxWidth ?? DEFAULT_MAX_WIDTH);
|
|
82
86
|
} catch {
|
|
83
87
|
return null;
|
|
84
88
|
}
|
|
@@ -229,7 +233,7 @@ const URL_QUERY = /[?#]/;
|
|
|
229
233
|
const REDACTED = "[redacted]";
|
|
230
234
|
const ELLIPSIS = "…";
|
|
231
235
|
const MAX_ATTRIBUTE_LENGTH = 120;
|
|
232
|
-
const MAX_TEXT_LENGTH = 200;
|
|
236
|
+
const MAX_TEXT_LENGTH$1 = 200;
|
|
233
237
|
const MAX_CHILD_DEPTH = 3;
|
|
234
238
|
const MAX_CLONED_NODES = 400;
|
|
235
239
|
const DROPPED_TAGS = /* @__PURE__ */ new Set([
|
|
@@ -259,6 +263,36 @@ function truncate$1(value, max) {
|
|
|
259
263
|
function maskSecrets(value) {
|
|
260
264
|
return value.replace(EMAIL, REDACTED).replace(LONG_TOKEN, REDACTED);
|
|
261
265
|
}
|
|
266
|
+
const SENSITIVE_PARAM = /token|secret|pass(?:word|wd)?|pwd|auth|jwt|credential|session|sid|key|email|ssn|otp|code|signature|nonce/i;
|
|
267
|
+
const LEADING_QUESTION_MARK = /^\?/;
|
|
268
|
+
function decoded(value) {
|
|
269
|
+
try {
|
|
270
|
+
return decodeURIComponent(value);
|
|
271
|
+
} catch {
|
|
272
|
+
return value;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Query strings carry signed-in state (`?token=…`, `?userEmail=…`) straight
|
|
277
|
+
* into stored reports and agent prompts. Values of anything credential- or
|
|
278
|
+
* identity-shaped are masked; every other byte of the address stays intact.
|
|
279
|
+
*/
|
|
280
|
+
function redactUrl(href) {
|
|
281
|
+
let url;
|
|
282
|
+
try {
|
|
283
|
+
url = new URL(href);
|
|
284
|
+
} catch {
|
|
285
|
+
return maskSecrets(href);
|
|
286
|
+
}
|
|
287
|
+
const pairs = url.search.replace(LEADING_QUESTION_MARK, "").split("&").filter(Boolean);
|
|
288
|
+
if (!pairs.some((pair) => SENSITIVE_PARAM.test(decoded(pair.split("=")[0] ?? "")))) return href;
|
|
289
|
+
const query = pairs.map((pair) => {
|
|
290
|
+
const equals = pair.indexOf("=");
|
|
291
|
+
if (equals === -1 || !SENSITIVE_PARAM.test(decoded(pair.slice(0, equals)))) return pair;
|
|
292
|
+
return `${pair.slice(0, equals)}=${REDACTED}`;
|
|
293
|
+
}).join("&");
|
|
294
|
+
return `${url.origin}${url.pathname}${query ? `?${query}` : ""}${url.hash}`;
|
|
295
|
+
}
|
|
262
296
|
function sanitizeUrl(value) {
|
|
263
297
|
if (OPAQUE_URI.test(value)) return `${value.slice(0, value.indexOf(":"))}:${ELLIPSIS}`;
|
|
264
298
|
return value.split(URL_QUERY)[0] ?? value;
|
|
@@ -293,7 +327,7 @@ function sanitizeTree(element, depth) {
|
|
|
293
327
|
continue;
|
|
294
328
|
}
|
|
295
329
|
if (child.nodeType === Node.TEXT_NODE) {
|
|
296
|
-
child.textContent = truncate$1(maskSecrets(child.textContent ?? ""), MAX_TEXT_LENGTH);
|
|
330
|
+
child.textContent = truncate$1(maskSecrets(child.textContent ?? ""), MAX_TEXT_LENGTH$1);
|
|
297
331
|
continue;
|
|
298
332
|
}
|
|
299
333
|
if (!(child instanceof Element)) continue;
|
|
@@ -324,6 +358,7 @@ const SAFE_ID = /^[A-Za-z][\w-]*$/;
|
|
|
324
358
|
const WHITESPACE = /\s+/g;
|
|
325
359
|
const MAX_LABEL_LENGTH = 80;
|
|
326
360
|
const MAX_HTML_LENGTH = 600;
|
|
361
|
+
const MAX_TEXT_LENGTH = 300;
|
|
327
362
|
const MAX_REGION_LENGTH = 160;
|
|
328
363
|
const MAX_HANDLE_VALUE_LENGTH = 80;
|
|
329
364
|
const MAX_DEPTH = 12;
|
|
@@ -340,6 +375,50 @@ const TEST_ATTRIBUTES = [
|
|
|
340
375
|
const NAMING_ATTRIBUTES = ["name", "aria-label"];
|
|
341
376
|
const LANDMARK_SELECTOR = "dialog, [role=\"dialog\"], [role=\"alertdialog\"], main, nav, aside, header, footer, form, section, article";
|
|
342
377
|
const HEADING_SELECTOR = "h1, h2, h3, h4, h5, h6, [role=\"heading\"]";
|
|
378
|
+
/** Elements that start a new visual block — their text gets separated by a space. */
|
|
379
|
+
const BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
380
|
+
"address",
|
|
381
|
+
"article",
|
|
382
|
+
"aside",
|
|
383
|
+
"blockquote",
|
|
384
|
+
"dd",
|
|
385
|
+
"details",
|
|
386
|
+
"div",
|
|
387
|
+
"dl",
|
|
388
|
+
"dt",
|
|
389
|
+
"fieldset",
|
|
390
|
+
"figcaption",
|
|
391
|
+
"figure",
|
|
392
|
+
"footer",
|
|
393
|
+
"form",
|
|
394
|
+
"h1",
|
|
395
|
+
"h2",
|
|
396
|
+
"h3",
|
|
397
|
+
"h4",
|
|
398
|
+
"h5",
|
|
399
|
+
"h6",
|
|
400
|
+
"header",
|
|
401
|
+
"hr",
|
|
402
|
+
"li",
|
|
403
|
+
"main",
|
|
404
|
+
"nav",
|
|
405
|
+
"ol",
|
|
406
|
+
"p",
|
|
407
|
+
"pre",
|
|
408
|
+
"section",
|
|
409
|
+
"summary",
|
|
410
|
+
"table",
|
|
411
|
+
"td",
|
|
412
|
+
"th",
|
|
413
|
+
"tr",
|
|
414
|
+
"ul"
|
|
415
|
+
]);
|
|
416
|
+
function visibleText(element) {
|
|
417
|
+
const clone = element.cloneNode(true);
|
|
418
|
+
if (!(clone instanceof Element)) return "";
|
|
419
|
+
for (const node of clone.querySelectorAll("*")) if (BLOCK_TAGS.has(node.localName)) node.before(clone.ownerDocument.createTextNode(" "));
|
|
420
|
+
return collapseText(maskSecrets(clone.textContent ?? ""));
|
|
421
|
+
}
|
|
343
422
|
function isUnique(selector) {
|
|
344
423
|
try {
|
|
345
424
|
return document.querySelectorAll(selector).length === 1;
|
|
@@ -427,8 +506,12 @@ function describeLandmark(element) {
|
|
|
427
506
|
return name?.trim() ? `${landmark.localName} "${collapseText(name)}"` : landmark.localName;
|
|
428
507
|
}
|
|
429
508
|
function headingAbove(element) {
|
|
509
|
+
const scope = element.closest(LANDMARK_SELECTOR) ?? document.body;
|
|
510
|
+
const probe = document.createRange();
|
|
511
|
+
probe.selectNodeContents(element);
|
|
512
|
+
probe.collapse(true);
|
|
430
513
|
let closest = null;
|
|
431
|
-
for (const heading of
|
|
514
|
+
for (const heading of scope.querySelectorAll(HEADING_SELECTOR)) if (probe.comparePoint(heading, 0) < 0) closest = heading;
|
|
432
515
|
return collapseText(closest?.textContent ?? "") || null;
|
|
433
516
|
}
|
|
434
517
|
/**
|
|
@@ -455,6 +538,7 @@ function getElementRect(element) {
|
|
|
455
538
|
/** Everything an agent needs to find the picked element back in the codebase. */
|
|
456
539
|
function buildElementSelection(element) {
|
|
457
540
|
const { componentStack, sourceLocation } = inspectReactElement(element);
|
|
541
|
+
const text = truncate(visibleText(element), MAX_TEXT_LENGTH);
|
|
458
542
|
return {
|
|
459
543
|
componentStack,
|
|
460
544
|
html: sanitizeMarkup(element, MAX_HTML_LENGTH),
|
|
@@ -462,7 +546,8 @@ function buildElementSelection(element) {
|
|
|
462
546
|
rect: getElementRect(element),
|
|
463
547
|
region: describeRegion(element),
|
|
464
548
|
selector: truncate(buildSelector(element), MAX_SELECTOR_LENGTH),
|
|
465
|
-
sourceLocation: sourceLocation ? truncate(sourceLocation, MAX_SOURCE_LOCATION_LENGTH) : sourceLocation
|
|
549
|
+
sourceLocation: sourceLocation ? truncate(sourceLocation, MAX_SOURCE_LOCATION_LENGTH) : sourceLocation,
|
|
550
|
+
text: text || void 0
|
|
466
551
|
};
|
|
467
552
|
}
|
|
468
553
|
//#endregion
|
|
@@ -539,9 +624,13 @@ function collectPageContext(options) {
|
|
|
539
624
|
height: window.screen.height,
|
|
540
625
|
width: window.screen.width
|
|
541
626
|
},
|
|
627
|
+
scroll: {
|
|
628
|
+
x: window.scrollX,
|
|
629
|
+
y: window.scrollY
|
|
630
|
+
},
|
|
542
631
|
sdkVersion: options?.sdkVersion,
|
|
543
632
|
timezone: resolveTimezone(),
|
|
544
|
-
url:
|
|
633
|
+
url: redactUrl(window.location.href),
|
|
545
634
|
userAgent: clip(userAgent, MAX_USER_AGENT_LENGTH),
|
|
546
635
|
viewport: {
|
|
547
636
|
devicePixelRatio: window.devicePixelRatio,
|
|
@@ -553,7 +642,7 @@ function collectPageContext(options) {
|
|
|
553
642
|
//#endregion
|
|
554
643
|
//#region src/feedback/types.ts
|
|
555
644
|
/** Reported with every submission. */
|
|
556
|
-
const SDK_VERSION = "0.3.
|
|
645
|
+
const SDK_VERSION = "0.3.2";
|
|
557
646
|
const DEFAULT_WIDGET_LABELS = {
|
|
558
647
|
annotateHint: "Draw on the screenshot to point at the problem.",
|
|
559
648
|
attachScreenshot: "Attach a screenshot",
|
|
@@ -2066,6 +2155,128 @@ function startConsoleRecorder(options) {
|
|
|
2066
2155
|
};
|
|
2067
2156
|
}
|
|
2068
2157
|
//#endregion
|
|
2158
|
+
//#region src/feedback/core/element-capture.ts
|
|
2159
|
+
const MIN_CONTEXT_PADDING = 24;
|
|
2160
|
+
const MAX_CONTEXT_WIDTH = 2e3;
|
|
2161
|
+
const CLOSE_UP_MAX_WIDTH = 1200;
|
|
2162
|
+
const SELECTION_COLOR = "#4f46e5";
|
|
2163
|
+
const OUTLINE_COLOR = "#4f46e5";
|
|
2164
|
+
const OUTLINE_GLOW = "#ffffff";
|
|
2165
|
+
const OUTLINE_WIDTH = 3;
|
|
2166
|
+
const GLOW_WIDTH = 6;
|
|
2167
|
+
const DIM_OVERLAY = "rgba(15, 23, 42, 0.45)";
|
|
2168
|
+
/**
|
|
2169
|
+
* Tightest ancestor that keeps some breathing room around the pick — the zoom
|
|
2170
|
+
* stays zoomed instead of growing to a giant section. Only when nothing up
|
|
2171
|
+
* the tree clears the minimum does the widest candidate below the cap win.
|
|
2172
|
+
*/
|
|
2173
|
+
function contextRootFor(element) {
|
|
2174
|
+
const target = element.getBoundingClientRect();
|
|
2175
|
+
const coversAtLeast = (box, margin) => box.left <= target.left - margin && box.right >= target.right + margin && box.top <= target.top - margin && box.bottom >= target.bottom + margin;
|
|
2176
|
+
let tightest = null;
|
|
2177
|
+
let current = element.parentElement;
|
|
2178
|
+
while (current && current !== document.documentElement) {
|
|
2179
|
+
const box = current.getBoundingClientRect();
|
|
2180
|
+
if (coversAtLeast(box, MIN_CONTEXT_PADDING)) return current;
|
|
2181
|
+
tightest ??= box.width < MAX_CONTEXT_WIDTH ? current : null;
|
|
2182
|
+
current = current.parentElement;
|
|
2183
|
+
}
|
|
2184
|
+
return tightest ?? element;
|
|
2185
|
+
}
|
|
2186
|
+
/** Where the picked element sits inside the context shot, in image pixels. */
|
|
2187
|
+
function closeUpGeometry(params) {
|
|
2188
|
+
const scale = params.rootRect.width > 0 ? params.capture.width / params.rootRect.width : 1;
|
|
2189
|
+
return {
|
|
2190
|
+
height: Math.round(params.elementRect.height * scale),
|
|
2191
|
+
scale,
|
|
2192
|
+
width: Math.round(params.elementRect.width * scale),
|
|
2193
|
+
x: Math.round((params.elementRect.left - params.rootRect.left) * scale),
|
|
2194
|
+
y: Math.round((params.elementRect.top - params.rootRect.top) * scale)
|
|
2195
|
+
};
|
|
2196
|
+
}
|
|
2197
|
+
/** Rectangle annotation marking the picked element on the viewport shot. */
|
|
2198
|
+
function highlightFor(selection, capture) {
|
|
2199
|
+
const scale = capture.width / window.innerWidth;
|
|
2200
|
+
const { rect } = selection;
|
|
2201
|
+
return {
|
|
2202
|
+
color: SELECTION_COLOR,
|
|
2203
|
+
end: {
|
|
2204
|
+
x: (rect.x + rect.width) * scale,
|
|
2205
|
+
y: (rect.y + rect.height) * scale
|
|
2206
|
+
},
|
|
2207
|
+
id: `selection-${rect.x}-${rect.y}`,
|
|
2208
|
+
start: {
|
|
2209
|
+
x: rect.x * scale,
|
|
2210
|
+
y: rect.y * scale
|
|
2211
|
+
},
|
|
2212
|
+
tool: "rectangle"
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
function drawZoneOverlay(context, zone) {
|
|
2216
|
+
context.save();
|
|
2217
|
+
context.fillStyle = DIM_OVERLAY;
|
|
2218
|
+
context.beginPath();
|
|
2219
|
+
context.rect(0, 0, context.canvas.width, context.canvas.height);
|
|
2220
|
+
context.rect(zone.x, zone.y, zone.width, zone.height);
|
|
2221
|
+
context.fill("evenodd");
|
|
2222
|
+
for (const [color, width] of [[OUTLINE_GLOW, GLOW_WIDTH], [OUTLINE_COLOR, OUTLINE_WIDTH]]) {
|
|
2223
|
+
context.strokeStyle = color;
|
|
2224
|
+
context.lineWidth = width * zone.scale;
|
|
2225
|
+
context.strokeRect(zone.x, zone.y, zone.width, zone.height);
|
|
2226
|
+
}
|
|
2227
|
+
context.restore();
|
|
2228
|
+
}
|
|
2229
|
+
function drawCloseUp(capture, zone) {
|
|
2230
|
+
return new Promise((resolve) => {
|
|
2231
|
+
const canvas = document.createElement("canvas");
|
|
2232
|
+
canvas.width = capture.width;
|
|
2233
|
+
canvas.height = capture.height;
|
|
2234
|
+
const context = canvas.getContext("2d");
|
|
2235
|
+
const image = new Image();
|
|
2236
|
+
image.onload = () => {
|
|
2237
|
+
if (!context) {
|
|
2238
|
+
resolve(null);
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
context.drawImage(image, 0, 0, capture.width, capture.height);
|
|
2242
|
+
drawZoneOverlay(context, zone);
|
|
2243
|
+
resolve(capturedFromCanvas(canvas, CLOSE_UP_MAX_WIDTH));
|
|
2244
|
+
};
|
|
2245
|
+
image.onerror = () => resolve(null);
|
|
2246
|
+
image.src = capture.objectUrl;
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* Zoomed view of the picked zone: the surrounding area captured for context,
|
|
2251
|
+
* everything outside the selection dimmed and the zone framed with a glowing
|
|
2252
|
+
* outline — unambiguous to eyes and vision models alike.
|
|
2253
|
+
* Falls back to the plain context shot when the overlay cannot be drawn.
|
|
2254
|
+
*/
|
|
2255
|
+
async function captureElementCloseUp(element) {
|
|
2256
|
+
try {
|
|
2257
|
+
const root = contextRootFor(element);
|
|
2258
|
+
const rootRect = root.getBoundingClientRect();
|
|
2259
|
+
const elementRect = element.getBoundingClientRect();
|
|
2260
|
+
const capture = await captureViewport({
|
|
2261
|
+
element: root,
|
|
2262
|
+
maxWidth: CLOSE_UP_MAX_WIDTH
|
|
2263
|
+
});
|
|
2264
|
+
if (!capture || root === element) return capture;
|
|
2265
|
+
const outlined = await drawCloseUp(capture, closeUpGeometry({
|
|
2266
|
+
capture,
|
|
2267
|
+
elementRect,
|
|
2268
|
+
rootRect
|
|
2269
|
+
}));
|
|
2270
|
+
if (outlined) {
|
|
2271
|
+
releaseCapture(capture);
|
|
2272
|
+
return outlined;
|
|
2273
|
+
}
|
|
2274
|
+
return capture;
|
|
2275
|
+
} catch {
|
|
2276
|
+
return null;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
//#endregion
|
|
2069
2280
|
//#region src/feedback/core/submit.ts
|
|
2070
2281
|
const MAX_TITLE_LENGTH = 100;
|
|
2071
2282
|
const NEWLINE = /\r?\n/;
|
|
@@ -2164,9 +2375,53 @@ function createFeedbackTransport(client) {
|
|
|
2164
2375
|
};
|
|
2165
2376
|
}
|
|
2166
2377
|
//#endregion
|
|
2378
|
+
//#region src/feedback/ui/use-capture-sync.ts
|
|
2379
|
+
const RESIZE_DEBOUNCE = 400;
|
|
2380
|
+
/**
|
|
2381
|
+
* Calls `refresh` whenever the page the reporter is looking at changes shape:
|
|
2382
|
+
* SPA routes (which never fire popstate on push/replace — history methods must
|
|
2383
|
+
* be patched) and window resizes, debounced. Keeps the frozen screenshot and
|
|
2384
|
+
* page context describing what the reporter currently sees.
|
|
2385
|
+
*/
|
|
2386
|
+
function useCaptureSync(enabled, refresh) {
|
|
2387
|
+
useEffect(() => {
|
|
2388
|
+
if (!enabled) return;
|
|
2389
|
+
const history = window.history;
|
|
2390
|
+
const originalPush = history.pushState;
|
|
2391
|
+
const originalReplace = history.replaceState;
|
|
2392
|
+
const hadOwnPush = Object.hasOwn(history, "pushState");
|
|
2393
|
+
const hadOwnReplace = Object.hasOwn(history, "replaceState");
|
|
2394
|
+
history.pushState = function pushState(data, unused, url) {
|
|
2395
|
+
const result = originalPush.call(history, data, unused, url);
|
|
2396
|
+
refresh();
|
|
2397
|
+
return result;
|
|
2398
|
+
};
|
|
2399
|
+
history.replaceState = function replaceState(data, unused, url) {
|
|
2400
|
+
const result = originalReplace.call(history, data, unused, url);
|
|
2401
|
+
refresh();
|
|
2402
|
+
return result;
|
|
2403
|
+
};
|
|
2404
|
+
window.addEventListener("popstate", refresh);
|
|
2405
|
+
let resizeTimer;
|
|
2406
|
+
const onResize = () => {
|
|
2407
|
+
window.clearTimeout(resizeTimer);
|
|
2408
|
+
resizeTimer = window.setTimeout(refresh, RESIZE_DEBOUNCE);
|
|
2409
|
+
};
|
|
2410
|
+
window.addEventListener("resize", onResize);
|
|
2411
|
+
return () => {
|
|
2412
|
+
if (hadOwnPush) history.pushState = originalPush;
|
|
2413
|
+
else Reflect.deleteProperty(history, "pushState");
|
|
2414
|
+
if (hadOwnReplace) history.replaceState = originalReplace;
|
|
2415
|
+
else Reflect.deleteProperty(history, "replaceState");
|
|
2416
|
+
window.removeEventListener("popstate", refresh);
|
|
2417
|
+
window.clearTimeout(resizeTimer);
|
|
2418
|
+
window.removeEventListener("resize", onResize);
|
|
2419
|
+
};
|
|
2420
|
+
}, [enabled, refresh]);
|
|
2421
|
+
}
|
|
2422
|
+
//#endregion
|
|
2167
2423
|
//#region src/feedback/ui/use-widget-state.ts
|
|
2168
2424
|
const SUCCESS_CLOSE_DELAY = 2400;
|
|
2169
|
-
const ELEMENT_CAPTURE_MAX_WIDTH = 1200;
|
|
2170
2425
|
const IS_APPLE = /Mac|iPhone|iPad/;
|
|
2171
2426
|
const MILLISECONDS_PER_DAY = 864e5;
|
|
2172
2427
|
const DISMISSAL_STORAGE_PREFIX = "reflet-feedback-dismissed";
|
|
@@ -2204,23 +2459,6 @@ function matchesHotkey(event, hotkey, platform = typeof navigator === "undefined
|
|
|
2204
2459
|
};
|
|
2205
2460
|
return event.altKey === expected.alt && event.ctrlKey === expected.ctrl && event.metaKey === expected.meta && event.shiftKey === expected.shift;
|
|
2206
2461
|
}
|
|
2207
|
-
function highlightFor(selection, capture) {
|
|
2208
|
-
const scale = capture.width / window.innerWidth;
|
|
2209
|
-
const { rect } = selection;
|
|
2210
|
-
return {
|
|
2211
|
-
color: "#4f46e5",
|
|
2212
|
-
end: {
|
|
2213
|
-
x: (rect.x + rect.width) * scale,
|
|
2214
|
-
y: (rect.y + rect.height) * scale
|
|
2215
|
-
},
|
|
2216
|
-
id: `selection-${rect.x}-${rect.y}`,
|
|
2217
|
-
start: {
|
|
2218
|
-
x: rect.x * scale,
|
|
2219
|
-
y: rect.y * scale
|
|
2220
|
-
},
|
|
2221
|
-
tool: "rectangle"
|
|
2222
|
-
};
|
|
2223
|
-
}
|
|
2224
2462
|
/**
|
|
2225
2463
|
* A capture whose object URL is revoked the moment it is replaced or dropped —
|
|
2226
2464
|
* holding one without this leaks the blob for the lifetime of the page.
|
|
@@ -2260,12 +2498,9 @@ function useWidgetState(props) {
|
|
|
2260
2498
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
2261
2499
|
const [error, setError] = useState(null);
|
|
2262
2500
|
const [dismissedUntil, setDismissedUntil] = useState(() => readDismissedUntil(dismissalKey));
|
|
2263
|
-
const captureRef = useRef(null);
|
|
2264
2501
|
const pickRef = useRef(0);
|
|
2265
2502
|
const consoleRef = useRef(null);
|
|
2266
|
-
|
|
2267
|
-
captureRef.current = capture;
|
|
2268
|
-
}, [capture]);
|
|
2503
|
+
const frozenContext = useRef(null);
|
|
2269
2504
|
useEffect(() => {
|
|
2270
2505
|
setDismissedUntil(readDismissedUntil(dismissalKey));
|
|
2271
2506
|
}, [dismissalKey]);
|
|
@@ -2278,15 +2513,20 @@ function useWidgetState(props) {
|
|
|
2278
2513
|
recorder.stop();
|
|
2279
2514
|
};
|
|
2280
2515
|
}, [props.captureConsole]);
|
|
2281
|
-
|
|
2516
|
+
/** Frozen at the same instant as the screenshot, so shot and URL always match. */
|
|
2517
|
+
const takeCapture = useCallback(() => {
|
|
2518
|
+
const context = collectPageContext({ sdkVersion: SDK_VERSION });
|
|
2282
2519
|
setIsCapturing(true);
|
|
2283
|
-
captureViewport(
|
|
2520
|
+
captureViewport().catch(() => null).then((image) => {
|
|
2284
2521
|
setIsCapturing(false);
|
|
2285
2522
|
replaceCapture(image);
|
|
2523
|
+
frozenContext.current = image ? context : null;
|
|
2286
2524
|
});
|
|
2287
2525
|
}, [replaceCapture]);
|
|
2526
|
+
useCaptureSync(isOpen && step === "compose" && annotations.length === 0 && !selection && props.captureOnOpen !== false, takeCapture);
|
|
2288
2527
|
const reset = useCallback(() => {
|
|
2289
2528
|
pickRef.current++;
|
|
2529
|
+
frozenContext.current = null;
|
|
2290
2530
|
replaceCapture(null);
|
|
2291
2531
|
replaceElementCapture(null);
|
|
2292
2532
|
setAnnotations([]);
|
|
@@ -2331,21 +2571,24 @@ function useWidgetState(props) {
|
|
|
2331
2571
|
props.onOpen,
|
|
2332
2572
|
takeCapture
|
|
2333
2573
|
]);
|
|
2334
|
-
|
|
2574
|
+
/** Fresh pixels so the highlight maps onto the scroll position the element had. */
|
|
2575
|
+
const selectElement = useCallback(async (element) => {
|
|
2335
2576
|
const picked = buildElementSelection(element);
|
|
2336
2577
|
setSelection(picked);
|
|
2337
2578
|
setStep("compose");
|
|
2338
|
-
const current = captureRef.current;
|
|
2339
|
-
if (current) setAnnotations((previous) => [...previous.filter((item) => !item.id.startsWith("selection-")), highlightFor(picked, current)]);
|
|
2340
2579
|
const pick = ++pickRef.current;
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
}
|
|
2348
|
-
|
|
2580
|
+
const context = collectPageContext({ sdkVersion: SDK_VERSION });
|
|
2581
|
+
const [fresh, closeUp] = await Promise.all([captureViewport().catch(() => null), captureElementCloseUp(element).catch(() => null)]);
|
|
2582
|
+
if (pickRef.current !== pick) {
|
|
2583
|
+
releaseCapture(fresh);
|
|
2584
|
+
releaseCapture(closeUp);
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
replaceCapture(fresh);
|
|
2588
|
+
replaceElementCapture(closeUp);
|
|
2589
|
+
frozenContext.current = fresh ? context : null;
|
|
2590
|
+
setAnnotations(fresh ? [highlightFor(picked, fresh)] : []);
|
|
2591
|
+
}, [replaceCapture, replaceElementCapture]);
|
|
2349
2592
|
const clearSelection = useCallback(() => {
|
|
2350
2593
|
pickRef.current++;
|
|
2351
2594
|
replaceElementCapture(null);
|
|
@@ -2353,6 +2596,7 @@ function useWidgetState(props) {
|
|
|
2353
2596
|
setAnnotations((previous) => previous.filter((item) => !item.id.startsWith("selection-")));
|
|
2354
2597
|
}, [replaceElementCapture]);
|
|
2355
2598
|
const removeCapture = useCallback(() => {
|
|
2599
|
+
frozenContext.current = null;
|
|
2356
2600
|
replaceCapture(null);
|
|
2357
2601
|
setAnnotations([]);
|
|
2358
2602
|
}, [replaceCapture]);
|
|
@@ -2386,7 +2630,7 @@ function useWidgetState(props) {
|
|
|
2386
2630
|
annotations,
|
|
2387
2631
|
category,
|
|
2388
2632
|
context: {
|
|
2389
|
-
...collectPageContext({ sdkVersion:
|
|
2633
|
+
...frozenContext.current ?? collectPageContext({ sdkVersion: "0.3.2" }),
|
|
2390
2634
|
consoleEvents: consoleRef.current?.events(),
|
|
2391
2635
|
metadata: props.metadata,
|
|
2392
2636
|
selection: selection ?? void 0
|