reflet-sdk 0.2.0 → 0.3.1
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 +46 -0
- package/dist/client-BYuSKJw5.js.map +1 -1
- package/dist/{client-Bi-GlSii.d.ts → client-DR_mRfak.d.ts} +2 -2
- package/dist/{client-Bi-GlSii.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 +814 -312
- package/dist/feedback.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/{types-xyOadyfX.d.ts → types-Dp37I5ES.d.ts} +13 -2
- package/dist/types-Dp37I5ES.d.ts.map +1 -0
- package/package.json +2 -2
- package/dist/types-xyOadyfX.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
|
}
|
|
@@ -94,6 +98,9 @@ const FORWARD_REF_TYPE = Symbol.for("react.forward_ref");
|
|
|
94
98
|
const MAX_STACK_DEPTH = 6;
|
|
95
99
|
const MAX_FIBER_WALK = 60;
|
|
96
100
|
const MAX_OWNER_WALK = 8;
|
|
101
|
+
const MIN_NAME_LENGTH = 4;
|
|
102
|
+
const CAPITALIZED = /^[A-Z]/;
|
|
103
|
+
const WORD_INSIDE = /[a-z]{2}/;
|
|
97
104
|
const ORIGIN_PREFIX = /^[a-z][\w+.-]*:\/\/[^/]*/i;
|
|
98
105
|
const WEBPACK_PREFIX = /^webpack-internal:\/{2,}(\.\/)?/;
|
|
99
106
|
const STACK_LOCATION = /([^\s()]+):(\d+):(\d+)\)?\s*$/;
|
|
@@ -122,6 +129,14 @@ function readName(value) {
|
|
|
122
129
|
const name = Reflect.get(value, "name");
|
|
123
130
|
return typeof name === "string" && name ? name : null;
|
|
124
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Minified bundles hand out names like `Ur` or `Sn`. Reporting those as the
|
|
134
|
+
* owning component actively misleads an agent, so only real words get through.
|
|
135
|
+
*/
|
|
136
|
+
function isMeaningfulName(name) {
|
|
137
|
+
if (name.includes("/") || name.includes("_")) return true;
|
|
138
|
+
return name.length >= MIN_NAME_LENGTH && CAPITALIZED.test(name) && WORD_INSIDE.test(name);
|
|
139
|
+
}
|
|
125
140
|
function componentName(type, depth = 0) {
|
|
126
141
|
if (typeof type === "string" || type === null || depth > 3) return null;
|
|
127
142
|
if (isRecord(type)) {
|
|
@@ -130,8 +145,7 @@ function componentName(type, depth = 0) {
|
|
|
130
145
|
if (marker === FORWARD_REF_TYPE) return componentName(Reflect.get(type, "render"), depth + 1);
|
|
131
146
|
}
|
|
132
147
|
const name = readName(type);
|
|
133
|
-
|
|
134
|
-
return name[0] === name[0]?.toUpperCase() || name.includes("/") ? name : null;
|
|
148
|
+
return name && isMeaningfulName(name) ? name : null;
|
|
135
149
|
}
|
|
136
150
|
/** Component names owning the fiber, innermost first. */
|
|
137
151
|
function resolveComponentStack(fiber) {
|
|
@@ -205,14 +219,206 @@ function inspectReactElement(element) {
|
|
|
205
219
|
};
|
|
206
220
|
}
|
|
207
221
|
//#endregion
|
|
222
|
+
//#region src/feedback/core/redact.ts
|
|
223
|
+
/**
|
|
224
|
+
* The picked element's markup leaves the reporter's browser and ends up in an
|
|
225
|
+
* agent prompt, so it is scrubbed here: typed-in values, credentials and
|
|
226
|
+
* anything marked `data-reflet-redact` never make the trip.
|
|
227
|
+
*/
|
|
228
|
+
const EMAIL = /[\w.%+-]+@[\w.-]+\.[a-z]{2,}/gi;
|
|
229
|
+
const LONG_TOKEN = /\b[\w-]{28,}\b/g;
|
|
230
|
+
const OPAQUE_URI = /^(?:data|blob|javascript):/i;
|
|
231
|
+
const WHITESPACE_RUN = /\s+/g;
|
|
232
|
+
const URL_QUERY = /[?#]/;
|
|
233
|
+
const REDACTED = "[redacted]";
|
|
234
|
+
const ELLIPSIS = "…";
|
|
235
|
+
const MAX_ATTRIBUTE_LENGTH = 120;
|
|
236
|
+
const MAX_TEXT_LENGTH$1 = 200;
|
|
237
|
+
const MAX_CHILD_DEPTH = 3;
|
|
238
|
+
const MAX_CLONED_NODES = 400;
|
|
239
|
+
const DROPPED_TAGS = /* @__PURE__ */ new Set([
|
|
240
|
+
"script",
|
|
241
|
+
"style",
|
|
242
|
+
"noscript",
|
|
243
|
+
"template"
|
|
244
|
+
]);
|
|
245
|
+
const VALUE_HOLDERS = /* @__PURE__ */ new Set([
|
|
246
|
+
"input",
|
|
247
|
+
"textarea",
|
|
248
|
+
"select",
|
|
249
|
+
"option"
|
|
250
|
+
]);
|
|
251
|
+
const URL_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
252
|
+
"src",
|
|
253
|
+
"srcset",
|
|
254
|
+
"href",
|
|
255
|
+
"poster",
|
|
256
|
+
"action",
|
|
257
|
+
"formaction"
|
|
258
|
+
]);
|
|
259
|
+
function truncate$1(value, max) {
|
|
260
|
+
return value.length <= max ? value : `${value.slice(0, max - 1)}${ELLIPSIS}`;
|
|
261
|
+
}
|
|
262
|
+
/** Emails and token-shaped strings, wherever they appear in reported text. */
|
|
263
|
+
function maskSecrets(value) {
|
|
264
|
+
return value.replace(EMAIL, REDACTED).replace(LONG_TOKEN, REDACTED);
|
|
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
|
+
}
|
|
296
|
+
function sanitizeUrl(value) {
|
|
297
|
+
if (OPAQUE_URI.test(value)) return `${value.slice(0, value.indexOf(":"))}:${ELLIPSIS}`;
|
|
298
|
+
return value.split(URL_QUERY)[0] ?? value;
|
|
299
|
+
}
|
|
300
|
+
function sanitizeAttributes(element) {
|
|
301
|
+
const holdsTypedValue = VALUE_HOLDERS.has(element.localName);
|
|
302
|
+
for (const { name, value } of Array.from(element.attributes)) {
|
|
303
|
+
if (holdsTypedValue && name === "value") {
|
|
304
|
+
element.removeAttribute(name);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const cleaned = URL_ATTRIBUTES.has(name) ? sanitizeUrl(value) : maskSecrets(value);
|
|
308
|
+
element.setAttribute(name, truncate$1(cleaned, MAX_ATTRIBUTE_LENGTH));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function collapse(element) {
|
|
312
|
+
element.replaceChildren(element.ownerDocument.createTextNode(ELLIPSIS));
|
|
313
|
+
}
|
|
314
|
+
function sanitizeTree(element, depth) {
|
|
315
|
+
sanitizeAttributes(element);
|
|
316
|
+
if (element.hasAttribute("data-reflet-redact")) {
|
|
317
|
+
element.replaceChildren(element.ownerDocument.createTextNode(REDACTED));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (element.localName === "svg") {
|
|
321
|
+
element.replaceChildren();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
for (const child of Array.from(element.childNodes)) {
|
|
325
|
+
if (child.nodeType === Node.COMMENT_NODE) {
|
|
326
|
+
child.remove();
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
330
|
+
child.textContent = truncate$1(maskSecrets(child.textContent ?? ""), MAX_TEXT_LENGTH$1);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (!(child instanceof Element)) continue;
|
|
334
|
+
if (DROPPED_TAGS.has(child.localName)) {
|
|
335
|
+
child.remove();
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (depth >= MAX_CHILD_DEPTH) {
|
|
339
|
+
sanitizeAttributes(child);
|
|
340
|
+
collapse(child);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
sanitizeTree(child, depth + 1);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/** Redacted `outerHTML`, shallow-cloned when the subtree is too large to be useful. */
|
|
347
|
+
function sanitizeMarkup(element, maxLength) {
|
|
348
|
+
const isSmallEnough = element.querySelectorAll("*").length <= MAX_CLONED_NODES;
|
|
349
|
+
const clone = element.cloneNode(isSmallEnough);
|
|
350
|
+
if (!(clone instanceof Element)) return "";
|
|
351
|
+
sanitizeTree(clone, 0);
|
|
352
|
+
if (!isSmallEnough) collapse(clone);
|
|
353
|
+
return truncate$1(clone.outerHTML.replace(WHITESPACE_RUN, " "), maxLength);
|
|
354
|
+
}
|
|
355
|
+
//#endregion
|
|
208
356
|
//#region src/feedback/core/element-selector.ts
|
|
209
357
|
const SAFE_ID = /^[A-Za-z][\w-]*$/;
|
|
210
358
|
const WHITESPACE = /\s+/g;
|
|
211
359
|
const MAX_LABEL_LENGTH = 80;
|
|
212
360
|
const MAX_HTML_LENGTH = 600;
|
|
361
|
+
const MAX_TEXT_LENGTH = 300;
|
|
362
|
+
const MAX_REGION_LENGTH = 160;
|
|
363
|
+
const MAX_HANDLE_VALUE_LENGTH = 80;
|
|
213
364
|
const MAX_DEPTH = 12;
|
|
365
|
+
const MIN_PATH_SEGMENTS = 2;
|
|
214
366
|
const MAX_SELECTOR_LENGTH = 600;
|
|
215
367
|
const MAX_SOURCE_LOCATION_LENGTH = 400;
|
|
368
|
+
const TEST_ATTRIBUTES = [
|
|
369
|
+
"data-testid",
|
|
370
|
+
"data-test-id",
|
|
371
|
+
"data-test",
|
|
372
|
+
"data-cy",
|
|
373
|
+
"data-qa"
|
|
374
|
+
];
|
|
375
|
+
const NAMING_ATTRIBUTES = ["name", "aria-label"];
|
|
376
|
+
const LANDMARK_SELECTOR = "dialog, [role=\"dialog\"], [role=\"alertdialog\"], main, nav, aside, header, footer, form, section, article";
|
|
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
|
+
}
|
|
216
422
|
function isUnique(selector) {
|
|
217
423
|
try {
|
|
218
424
|
return document.querySelectorAll(selector).length === 1;
|
|
@@ -223,13 +429,23 @@ function isUnique(selector) {
|
|
|
223
429
|
function escapeAttributeValue(value) {
|
|
224
430
|
return value.replace(/["\\]/g, "\\$&");
|
|
225
431
|
}
|
|
432
|
+
function attributeHandle(element, attribute, qualifier) {
|
|
433
|
+
const value = element.getAttribute(attribute);
|
|
434
|
+
if (!value || value.length > MAX_HANDLE_VALUE_LENGTH) return null;
|
|
435
|
+
const selector = `${qualifier}[${attribute}="${escapeAttributeValue(value)}"]`;
|
|
436
|
+
return isUnique(selector) ? selector : null;
|
|
437
|
+
}
|
|
438
|
+
/** Something an engineer would have written on purpose, unlike a positional path. */
|
|
226
439
|
function uniqueHandle(element) {
|
|
227
440
|
const id = element.getAttribute("id");
|
|
228
441
|
if (id && SAFE_ID.test(id) && isUnique(`#${id}`)) return `#${id}`;
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
442
|
+
for (const attribute of TEST_ATTRIBUTES) {
|
|
443
|
+
const handle = attributeHandle(element, attribute, "");
|
|
444
|
+
if (handle) return handle;
|
|
445
|
+
}
|
|
446
|
+
for (const attribute of NAMING_ATTRIBUTES) {
|
|
447
|
+
const handle = attributeHandle(element, attribute, element.localName);
|
|
448
|
+
if (handle) return handle;
|
|
233
449
|
}
|
|
234
450
|
return null;
|
|
235
451
|
}
|
|
@@ -243,6 +459,9 @@ function positionalSegment(element) {
|
|
|
243
459
|
}
|
|
244
460
|
/**
|
|
245
461
|
* Shortest selector that still resolves back to `element` from the document.
|
|
462
|
+
* Grows the path one ancestor at a time and stops as soon as it is unique, so
|
|
463
|
+
* a stable handle anywhere up the tree replaces the whole positional chain.
|
|
464
|
+
* A bare tag is never enough: positional paths keep a parent for context.
|
|
246
465
|
*/
|
|
247
466
|
function buildSelector(element) {
|
|
248
467
|
if (element === document.documentElement) return "html";
|
|
@@ -251,43 +470,62 @@ function buildSelector(element) {
|
|
|
251
470
|
if (handle) return handle;
|
|
252
471
|
const segments = [];
|
|
253
472
|
let current = element;
|
|
254
|
-
let depth = 0;
|
|
255
|
-
while (current && depth < MAX_DEPTH) {
|
|
256
|
-
if (current === document.body) {
|
|
257
|
-
segments.unshift("body");
|
|
258
|
-
break;
|
|
259
|
-
}
|
|
260
|
-
if (current === document.documentElement) {
|
|
261
|
-
segments.unshift("html");
|
|
262
|
-
break;
|
|
263
|
-
}
|
|
473
|
+
for (let depth = 0; current && depth < MAX_DEPTH; depth++) {
|
|
264
474
|
const ancestorHandle = current === element ? null : uniqueHandle(current);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
segments.unshift(positionalSegment(current));
|
|
475
|
+
segments.unshift(ancestorHandle ?? positionalSegment(current));
|
|
476
|
+
const candidate = segments.join(" > ");
|
|
477
|
+
const isAnchored = segments.length >= MIN_PATH_SEGMENTS;
|
|
478
|
+
if (ancestorHandle || current === document.body || isAnchored && isUnique(candidate)) return candidate;
|
|
270
479
|
current = current.parentElement;
|
|
271
|
-
depth++;
|
|
272
480
|
}
|
|
273
481
|
return segments.join(" > ");
|
|
274
482
|
}
|
|
275
483
|
function truncate(value, max) {
|
|
276
484
|
return value.length <= max ? value : `${value.slice(0, max - 1)}…`;
|
|
277
485
|
}
|
|
486
|
+
function collapseText(value) {
|
|
487
|
+
return value.replace(WHITESPACE, " ").trim();
|
|
488
|
+
}
|
|
278
489
|
function accessibleLabel(element) {
|
|
279
490
|
const explicit = element.getAttribute("aria-label") ?? element.getAttribute("title") ?? element.getAttribute("alt") ?? element.getAttribute("placeholder");
|
|
280
491
|
if (explicit?.trim()) return explicit.trim();
|
|
281
|
-
return (element.textContent ?? "")
|
|
492
|
+
return collapseText(element.textContent ?? "");
|
|
282
493
|
}
|
|
283
494
|
/** Short human label such as `button "Sign in"` or `div.card`. */
|
|
284
495
|
function describeElement(element) {
|
|
285
496
|
const tag = element.localName;
|
|
286
|
-
const label = accessibleLabel(element);
|
|
497
|
+
const label = maskSecrets(accessibleLabel(element));
|
|
287
498
|
if (label) return truncate(`${tag} "${label}"`, MAX_LABEL_LENGTH);
|
|
288
499
|
const firstClass = element.classList.item(0);
|
|
289
500
|
return truncate(firstClass ? `${tag}.${firstClass}` : tag, MAX_LABEL_LENGTH);
|
|
290
501
|
}
|
|
502
|
+
function describeLandmark(element) {
|
|
503
|
+
const landmark = element.parentElement?.closest(LANDMARK_SELECTOR);
|
|
504
|
+
if (!landmark) return null;
|
|
505
|
+
const name = landmark.getAttribute("aria-label") ?? landmark.getAttribute("title");
|
|
506
|
+
return name?.trim() ? `${landmark.localName} "${collapseText(name)}"` : landmark.localName;
|
|
507
|
+
}
|
|
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);
|
|
513
|
+
let closest = null;
|
|
514
|
+
for (const heading of scope.querySelectorAll(HEADING_SELECTOR)) if (probe.comparePoint(heading, 0) < 0) closest = heading;
|
|
515
|
+
return collapseText(closest?.textContent ?? "") || null;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Where the element sits from a reader's point of view — `dialog "Members" ›
|
|
519
|
+
* Danger zone` says more to an agent than any DOM path can.
|
|
520
|
+
*/
|
|
521
|
+
function describeRegion(element) {
|
|
522
|
+
const landmark = describeLandmark(element);
|
|
523
|
+
const heading = headingAbove(element);
|
|
524
|
+
const parts = [landmark];
|
|
525
|
+
if (heading && !landmark?.includes(`"${heading}"`)) parts.push(heading);
|
|
526
|
+
const region = parts.filter((part) => part !== null).join(" › ");
|
|
527
|
+
return region ? truncate(maskSecrets(region), MAX_REGION_LENGTH) : void 0;
|
|
528
|
+
}
|
|
291
529
|
function getElementRect(element) {
|
|
292
530
|
const rect = element.getBoundingClientRect();
|
|
293
531
|
return {
|
|
@@ -300,13 +538,16 @@ function getElementRect(element) {
|
|
|
300
538
|
/** Everything an agent needs to find the picked element back in the codebase. */
|
|
301
539
|
function buildElementSelection(element) {
|
|
302
540
|
const { componentStack, sourceLocation } = inspectReactElement(element);
|
|
541
|
+
const text = truncate(visibleText(element), MAX_TEXT_LENGTH);
|
|
303
542
|
return {
|
|
304
543
|
componentStack,
|
|
305
|
-
html:
|
|
544
|
+
html: sanitizeMarkup(element, MAX_HTML_LENGTH),
|
|
306
545
|
label: describeElement(element),
|
|
307
546
|
rect: getElementRect(element),
|
|
547
|
+
region: describeRegion(element),
|
|
308
548
|
selector: truncate(buildSelector(element), MAX_SELECTOR_LENGTH),
|
|
309
|
-
sourceLocation: sourceLocation ? truncate(sourceLocation, MAX_SOURCE_LOCATION_LENGTH) : sourceLocation
|
|
549
|
+
sourceLocation: sourceLocation ? truncate(sourceLocation, MAX_SOURCE_LOCATION_LENGTH) : sourceLocation,
|
|
550
|
+
text: text || void 0
|
|
310
551
|
};
|
|
311
552
|
}
|
|
312
553
|
//#endregion
|
|
@@ -383,9 +624,13 @@ function collectPageContext(options) {
|
|
|
383
624
|
height: window.screen.height,
|
|
384
625
|
width: window.screen.width
|
|
385
626
|
},
|
|
627
|
+
scroll: {
|
|
628
|
+
x: window.scrollX,
|
|
629
|
+
y: window.scrollY
|
|
630
|
+
},
|
|
386
631
|
sdkVersion: options?.sdkVersion,
|
|
387
632
|
timezone: resolveTimezone(),
|
|
388
|
-
url:
|
|
633
|
+
url: redactUrl(window.location.href),
|
|
389
634
|
userAgent: clip(userAgent, MAX_USER_AGENT_LENGTH),
|
|
390
635
|
viewport: {
|
|
391
636
|
devicePixelRatio: window.devicePixelRatio,
|
|
@@ -396,8 +641,8 @@ function collectPageContext(options) {
|
|
|
396
641
|
}
|
|
397
642
|
//#endregion
|
|
398
643
|
//#region src/feedback/types.ts
|
|
399
|
-
/** Reported with every submission.
|
|
400
|
-
const SDK_VERSION = "0.
|
|
644
|
+
/** Reported with every submission. */
|
|
645
|
+
const SDK_VERSION = "0.3.1";
|
|
401
646
|
const DEFAULT_WIDGET_LABELS = {
|
|
402
647
|
annotateHint: "Draw on the screenshot to point at the problem.",
|
|
403
648
|
attachScreenshot: "Attach a screenshot",
|
|
@@ -657,9 +902,10 @@ async function renderAnnotatedImage(source, annotations) {
|
|
|
657
902
|
}
|
|
658
903
|
//#endregion
|
|
659
904
|
//#region src/feedback/ui/icons.tsx
|
|
660
|
-
function Svg({ children, size = 16 }) {
|
|
905
|
+
function Svg({ children, className, size = 16 }) {
|
|
661
906
|
return /* @__PURE__ */ jsx("svg", {
|
|
662
907
|
"aria-hidden": "true",
|
|
908
|
+
className,
|
|
663
909
|
fill: "none",
|
|
664
910
|
height: size,
|
|
665
911
|
stroke: "currentColor",
|
|
@@ -671,14 +917,18 @@ function Svg({ children, size = 16 }) {
|
|
|
671
917
|
children
|
|
672
918
|
});
|
|
673
919
|
}
|
|
674
|
-
function ChatIcon() {
|
|
920
|
+
function ChatIcon({ className }) {
|
|
675
921
|
return /* @__PURE__ */ jsx(Svg, {
|
|
676
|
-
|
|
922
|
+
className,
|
|
923
|
+
size: 15,
|
|
677
924
|
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" })
|
|
678
925
|
});
|
|
679
926
|
}
|
|
680
|
-
function CloseIcon() {
|
|
681
|
-
return /* @__PURE__ */ jsx(Svg, {
|
|
927
|
+
function CloseIcon({ className }) {
|
|
928
|
+
return /* @__PURE__ */ jsx(Svg, {
|
|
929
|
+
className,
|
|
930
|
+
children: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" })
|
|
931
|
+
});
|
|
682
932
|
}
|
|
683
933
|
function BugIcon() {
|
|
684
934
|
return /* @__PURE__ */ jsx(Svg, {
|
|
@@ -941,15 +1191,21 @@ function Annotator({ annotations, capture, labels, onChange, onDone }) {
|
|
|
941
1191
|
}
|
|
942
1192
|
//#endregion
|
|
943
1193
|
//#region src/feedback/ui/launcher.tsx
|
|
944
|
-
|
|
1194
|
+
/**
|
|
1195
|
+
* Never changes size between states — the panel opening must not move the
|
|
1196
|
+
* button out from under the pointer.
|
|
1197
|
+
*/
|
|
1198
|
+
function Launcher({ isOpen, label, onClick }) {
|
|
945
1199
|
return /* @__PURE__ */ jsxs("button", {
|
|
946
1200
|
"aria-expanded": isOpen,
|
|
947
|
-
"aria-label": label,
|
|
948
1201
|
className: "launcher",
|
|
949
|
-
"data-
|
|
1202
|
+
"data-open": isOpen,
|
|
950
1203
|
onClick,
|
|
951
1204
|
type: "button",
|
|
952
|
-
children: [
|
|
1205
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
1206
|
+
className: "launcher-icon",
|
|
1207
|
+
children: [/* @__PURE__ */ jsx(ChatIcon, { className: "on-open" }), /* @__PURE__ */ jsx(CloseIcon, { className: "on-close" })]
|
|
1208
|
+
}), label]
|
|
953
1209
|
});
|
|
954
1210
|
}
|
|
955
1211
|
//#endregion
|
|
@@ -970,7 +1226,7 @@ const CATEGORY_META = {
|
|
|
970
1226
|
};
|
|
971
1227
|
function Attachment({ labels, state }) {
|
|
972
1228
|
if (state.isCapturing) return /* @__PURE__ */ jsxs("div", {
|
|
973
|
-
className: "
|
|
1229
|
+
className: "card",
|
|
974
1230
|
children: [/* @__PURE__ */ jsx("div", { className: "thumb" }), /* @__PURE__ */ jsxs("div", {
|
|
975
1231
|
className: "meta",
|
|
976
1232
|
children: [/* @__PURE__ */ jsx("strong", { children: labels.retakeHint }), /* @__PURE__ */ jsx("span", { children: "…" })]
|
|
@@ -984,7 +1240,7 @@ function Attachment({ labels, state }) {
|
|
|
984
1240
|
});
|
|
985
1241
|
const annotationCount = state.annotations.length;
|
|
986
1242
|
return /* @__PURE__ */ jsxs("div", {
|
|
987
|
-
className: "
|
|
1243
|
+
className: "card",
|
|
988
1244
|
children: [
|
|
989
1245
|
/* @__PURE__ */ jsx("div", {
|
|
990
1246
|
className: "thumb",
|
|
@@ -997,12 +1253,18 @@ function Attachment({ labels, state }) {
|
|
|
997
1253
|
}),
|
|
998
1254
|
/* @__PURE__ */ jsxs("div", {
|
|
999
1255
|
className: "meta",
|
|
1000
|
-
children: [/* @__PURE__ */ jsx("strong", {
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1256
|
+
children: [/* @__PURE__ */ jsx("strong", {
|
|
1257
|
+
className: "truncate",
|
|
1258
|
+
children: labels.retakeHint
|
|
1259
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
1260
|
+
className: "truncate",
|
|
1261
|
+
children: [
|
|
1262
|
+
state.capture.width,
|
|
1263
|
+
"×",
|
|
1264
|
+
state.capture.height,
|
|
1265
|
+
annotationCount > 0 ? ` · ${annotationCount} marks` : ""
|
|
1266
|
+
]
|
|
1267
|
+
})]
|
|
1006
1268
|
}),
|
|
1007
1269
|
/* @__PURE__ */ jsxs("div", {
|
|
1008
1270
|
className: "actions",
|
|
@@ -1036,8 +1298,9 @@ function Attachment({ labels, state }) {
|
|
|
1036
1298
|
]
|
|
1037
1299
|
});
|
|
1038
1300
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1301
|
+
/** Shows the reporter exactly what will be sent about the element they picked. */
|
|
1302
|
+
function Selection({ labels, state }) {
|
|
1303
|
+
const { elementCapture, selection } = state;
|
|
1041
1304
|
if (!selection) return /* @__PURE__ */ jsxs("button", {
|
|
1042
1305
|
className: "ghost-btn",
|
|
1043
1306
|
onClick: () => state.setStep("picking"),
|
|
@@ -1045,24 +1308,45 @@ function SelectionChip({ labels, state }) {
|
|
|
1045
1308
|
children: [/* @__PURE__ */ jsx(TargetIcon, {}), labels.pickElement]
|
|
1046
1309
|
});
|
|
1047
1310
|
const component = selection.componentStack[0];
|
|
1048
|
-
return /* @__PURE__ */ jsxs("
|
|
1049
|
-
className: "
|
|
1311
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1312
|
+
className: "card",
|
|
1050
1313
|
children: [
|
|
1051
|
-
/* @__PURE__ */ jsx(
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1314
|
+
/* @__PURE__ */ jsx("div", {
|
|
1315
|
+
className: "thumb contain",
|
|
1316
|
+
children: elementCapture && /* @__PURE__ */ jsx("img", {
|
|
1317
|
+
alt: "",
|
|
1318
|
+
height: elementCapture.height,
|
|
1319
|
+
src: elementCapture.objectUrl,
|
|
1320
|
+
width: elementCapture.width
|
|
1321
|
+
})
|
|
1322
|
+
}),
|
|
1323
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1324
|
+
className: "meta",
|
|
1325
|
+
children: [/* @__PURE__ */ jsx("strong", {
|
|
1326
|
+
className: "truncate",
|
|
1327
|
+
children: component ? `<${component}>` : selection.label
|
|
1328
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1329
|
+
className: "truncate",
|
|
1330
|
+
children: selection.region ?? selection.sourceLocation ?? selection.selector
|
|
1331
|
+
})]
|
|
1059
1332
|
}),
|
|
1060
|
-
/* @__PURE__ */
|
|
1061
|
-
"
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1333
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1334
|
+
className: "actions",
|
|
1335
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
1336
|
+
"aria-label": labels.pickElement,
|
|
1337
|
+
className: "icon-btn",
|
|
1338
|
+
onClick: () => state.setStep("picking"),
|
|
1339
|
+
title: labels.pickElement,
|
|
1340
|
+
type: "button",
|
|
1341
|
+
children: /* @__PURE__ */ jsx(TargetIcon, {})
|
|
1342
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1343
|
+
"aria-label": labels.cancel,
|
|
1344
|
+
className: "icon-btn",
|
|
1345
|
+
onClick: state.clearSelection,
|
|
1346
|
+
title: labels.cancel,
|
|
1347
|
+
type: "button",
|
|
1348
|
+
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
1349
|
+
})]
|
|
1066
1350
|
})
|
|
1067
1351
|
]
|
|
1068
1352
|
});
|
|
@@ -1129,13 +1413,15 @@ function FeedbackPanel({ categories, labels, state }) {
|
|
|
1129
1413
|
ref: messageRef,
|
|
1130
1414
|
value: state.message
|
|
1131
1415
|
}),
|
|
1132
|
-
/* @__PURE__ */
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1416
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1417
|
+
className: "attachments",
|
|
1418
|
+
children: [/* @__PURE__ */ jsx(Attachment, {
|
|
1419
|
+
labels,
|
|
1420
|
+
state
|
|
1421
|
+
}), /* @__PURE__ */ jsx(Selection, {
|
|
1422
|
+
labels,
|
|
1423
|
+
state
|
|
1424
|
+
})]
|
|
1139
1425
|
}),
|
|
1140
1426
|
state.isAnonymous && /* @__PURE__ */ jsx("input", {
|
|
1141
1427
|
"aria-label": labels.emailLabel,
|
|
@@ -1192,7 +1478,7 @@ function FeedbackPanel({ categories, labels, state }) {
|
|
|
1192
1478
|
}
|
|
1193
1479
|
//#endregion
|
|
1194
1480
|
//#region src/feedback/ui/picker.tsx
|
|
1195
|
-
const LABEL_HEIGHT =
|
|
1481
|
+
const LABEL_HEIGHT = 24;
|
|
1196
1482
|
const LABEL_GAP = 6;
|
|
1197
1483
|
function isWidgetOwned(element) {
|
|
1198
1484
|
return element.closest(`[${WIDGET_MARKER}]`) !== null;
|
|
@@ -1205,14 +1491,15 @@ function elementUnder(x, y) {
|
|
|
1205
1491
|
function describeTarget(element) {
|
|
1206
1492
|
return {
|
|
1207
1493
|
componentName: resolveComponentStack(getFiberFromNode(element))[0],
|
|
1494
|
+
element,
|
|
1208
1495
|
label: describeElement(element),
|
|
1209
1496
|
rect: element.getBoundingClientRect(),
|
|
1210
|
-
|
|
1497
|
+
region: describeRegion(element)
|
|
1211
1498
|
};
|
|
1212
1499
|
}
|
|
1213
1500
|
/**
|
|
1214
1501
|
* Points at any element in the page the way a coding agent needs it: the React
|
|
1215
|
-
* component that owns it plus
|
|
1502
|
+
* component that owns it plus the page region it lives in.
|
|
1216
1503
|
*/
|
|
1217
1504
|
function ElementPicker({ hint, onCancel, onPick }) {
|
|
1218
1505
|
const [target, setTarget] = useState(null);
|
|
@@ -1223,7 +1510,14 @@ function ElementPicker({ hint, onCancel, onPick }) {
|
|
|
1223
1510
|
};
|
|
1224
1511
|
const onMove = (event) => {
|
|
1225
1512
|
const element = elementUnder(event.clientX, event.clientY);
|
|
1226
|
-
|
|
1513
|
+
if (!element) {
|
|
1514
|
+
setTarget(null);
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
setTarget((current) => current?.element === element ? {
|
|
1518
|
+
...current,
|
|
1519
|
+
rect: element.getBoundingClientRect()
|
|
1520
|
+
} : describeTarget(element));
|
|
1227
1521
|
};
|
|
1228
1522
|
const onClick = (event) => {
|
|
1229
1523
|
swallow(event);
|
|
@@ -1255,7 +1549,7 @@ function ElementPicker({ hint, onCancel, onPick }) {
|
|
|
1255
1549
|
};
|
|
1256
1550
|
}, [onCancel, onPick]);
|
|
1257
1551
|
const rect = target?.rect;
|
|
1258
|
-
const labelAbove = rect ? rect.top >
|
|
1552
|
+
const labelAbove = rect ? rect.top > 30 : true;
|
|
1259
1553
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1260
1554
|
rect && /* @__PURE__ */ jsx("div", {
|
|
1261
1555
|
className: "picker-box",
|
|
@@ -1272,7 +1566,7 @@ function ElementPicker({ hint, onCancel, onPick }) {
|
|
|
1272
1566
|
left: Math.max(4, rect.left),
|
|
1273
1567
|
top: labelAbove ? rect.top - LABEL_HEIGHT - LABEL_GAP : rect.bottom + LABEL_GAP
|
|
1274
1568
|
},
|
|
1275
|
-
children: [
|
|
1569
|
+
children: [/* @__PURE__ */ jsx("strong", { children: target.componentName ? `<${target.componentName}>` : target.label }), target.region && /* @__PURE__ */ jsx("span", { children: target.region })]
|
|
1276
1570
|
}),
|
|
1277
1571
|
/* @__PURE__ */ jsxs("div", {
|
|
1278
1572
|
className: "picker-hint",
|
|
@@ -1293,30 +1587,42 @@ function ElementPicker({ hint, onCancel, onPick }) {
|
|
|
1293
1587
|
const WIDGET_STYLES = `
|
|
1294
1588
|
:host {
|
|
1295
1589
|
--rf-bg: #ffffff;
|
|
1296
|
-
--rf-bg-subtle:
|
|
1297
|
-
--rf-
|
|
1298
|
-
--rf-
|
|
1299
|
-
--rf-text
|
|
1300
|
-
--rf-
|
|
1590
|
+
--rf-bg-subtle: rgb(55 53 47 / 4%);
|
|
1591
|
+
--rf-bg-hover: rgb(55 53 47 / 7%);
|
|
1592
|
+
--rf-border: rgb(55 53 47 / 9%);
|
|
1593
|
+
--rf-text: #37352f;
|
|
1594
|
+
--rf-text-muted: rgb(55 53 47 / 55%);
|
|
1595
|
+
--rf-primary: #37352f;
|
|
1301
1596
|
--rf-primary-text: #ffffff;
|
|
1302
|
-
--rf-
|
|
1303
|
-
--rf-
|
|
1304
|
-
--rf-
|
|
1305
|
-
--rf-
|
|
1306
|
-
--rf-
|
|
1307
|
-
--rf-
|
|
1597
|
+
--rf-accent: #2383e2;
|
|
1598
|
+
--rf-danger: #d44c47;
|
|
1599
|
+
--rf-success: #448361;
|
|
1600
|
+
--rf-radius: 12px;
|
|
1601
|
+
--rf-radius-sm: 8px;
|
|
1602
|
+
--rf-shadow-inset: inset 0 0 0 1px var(--rf-border);
|
|
1603
|
+
--rf-shadow-low: 0 0 0 1px rgb(15 15 15 / 4%), 0 1px 2px rgb(15 15 15 / 6%), 0 4px 10px -4px rgb(15 15 15 / 10%);
|
|
1604
|
+
--rf-shadow: 0 0 0 1px rgb(15 15 15 / 4%), 0 2px 5px -1px rgb(15 15 15 / 7%), 0 12px 32px -8px rgb(15 15 15 / 16%);
|
|
1605
|
+
--rf-ring: 0 0 0 3px color-mix(in srgb, var(--rf-accent) 20%, transparent);
|
|
1606
|
+
--rf-ease: cubic-bezier(0.16, 1, 0.3, 1);
|
|
1308
1607
|
color-scheme: light;
|
|
1309
1608
|
all: initial;
|
|
1310
1609
|
font-family: var(--rf-font, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
|
|
1311
1610
|
}
|
|
1312
1611
|
|
|
1313
1612
|
:host([data-theme="dark"]) {
|
|
1314
|
-
--rf-bg: #
|
|
1315
|
-
--rf-bg-subtle:
|
|
1316
|
-
--rf-
|
|
1317
|
-
--rf-
|
|
1318
|
-
--rf-text
|
|
1319
|
-
--rf-
|
|
1613
|
+
--rf-bg: #202020;
|
|
1614
|
+
--rf-bg-subtle: rgb(255 255 255 / 5%);
|
|
1615
|
+
--rf-bg-hover: rgb(255 255 255 / 9%);
|
|
1616
|
+
--rf-border: rgb(255 255 255 / 9%);
|
|
1617
|
+
--rf-text: #ebeae8;
|
|
1618
|
+
--rf-text-muted: rgb(255 255 255 / 46%);
|
|
1619
|
+
--rf-primary: #f1efec;
|
|
1620
|
+
--rf-primary-text: #1c1c1b;
|
|
1621
|
+
--rf-accent: #529cca;
|
|
1622
|
+
--rf-danger: #eb5757;
|
|
1623
|
+
--rf-success: #4dab74;
|
|
1624
|
+
--rf-shadow-low: 0 0 0 1px rgb(255 255 255 / 6%), 0 1px 2px rgb(0 0 0 / 30%), 0 4px 10px -4px rgb(0 0 0 / 40%);
|
|
1625
|
+
--rf-shadow: 0 0 0 1px rgb(255 255 255 / 7%), 0 2px 5px -1px rgb(0 0 0 / 35%), 0 14px 36px -10px rgb(0 0 0 / 55%);
|
|
1320
1626
|
color-scheme: dark;
|
|
1321
1627
|
}
|
|
1322
1628
|
|
|
@@ -1325,19 +1631,19 @@ const WIDGET_STYLES = `
|
|
|
1325
1631
|
.root {
|
|
1326
1632
|
position: fixed;
|
|
1327
1633
|
z-index: 2147483000;
|
|
1634
|
+
display: flex;
|
|
1635
|
+
flex-direction: column;
|
|
1636
|
+
gap: 10px;
|
|
1328
1637
|
font-family: inherit;
|
|
1329
|
-
font-size:
|
|
1638
|
+
font-size: 13.5px;
|
|
1330
1639
|
line-height: 1.45;
|
|
1331
1640
|
color: var(--rf-text);
|
|
1332
1641
|
}
|
|
1333
1642
|
|
|
1334
|
-
.root[data-position
|
|
1335
|
-
.root[data-position
|
|
1336
|
-
.root[data-position
|
|
1337
|
-
.root[data-position
|
|
1338
|
-
|
|
1339
|
-
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
1340
|
-
.root[data-position^="top"] .stack { flex-direction: column-reverse; }
|
|
1643
|
+
.root[data-position$="right"] { right: var(--rf-offset); align-items: flex-end; }
|
|
1644
|
+
.root[data-position$="left"] { left: var(--rf-offset); align-items: flex-start; }
|
|
1645
|
+
.root[data-position^="bottom"] { bottom: var(--rf-offset); }
|
|
1646
|
+
.root[data-position^="top"] { top: var(--rf-offset); flex-direction: column-reverse; }
|
|
1341
1647
|
|
|
1342
1648
|
button {
|
|
1343
1649
|
font: inherit;
|
|
@@ -1353,64 +1659,76 @@ button {
|
|
|
1353
1659
|
.launcher {
|
|
1354
1660
|
display: inline-flex;
|
|
1355
1661
|
align-items: center;
|
|
1356
|
-
gap:
|
|
1357
|
-
height:
|
|
1358
|
-
padding: 0
|
|
1359
|
-
border-radius:
|
|
1662
|
+
gap: 6px;
|
|
1663
|
+
height: 32px;
|
|
1664
|
+
padding: 0 11px;
|
|
1665
|
+
border-radius: var(--rf-radius-sm);
|
|
1360
1666
|
background: var(--rf-primary);
|
|
1361
1667
|
color: var(--rf-primary-text);
|
|
1362
|
-
font-
|
|
1363
|
-
|
|
1364
|
-
|
|
1668
|
+
font-size: 13px;
|
|
1669
|
+
font-weight: 500;
|
|
1670
|
+
letter-spacing: -0.005em;
|
|
1671
|
+
box-shadow: var(--rf-shadow-low);
|
|
1672
|
+
transition: transform 240ms var(--rf-ease), box-shadow 240ms var(--rf-ease), opacity 160ms var(--rf-ease);
|
|
1365
1673
|
}
|
|
1366
|
-
.launcher:hover { transform: translateY(-1px);
|
|
1367
|
-
.launcher:active { transform: translateY(0) scale(0.
|
|
1368
|
-
|
|
1674
|
+
.launcher:hover { transform: translateY(-1px); box-shadow: var(--rf-shadow); }
|
|
1675
|
+
.launcher:active { transform: translateY(0) scale(0.98); }
|
|
1676
|
+
|
|
1677
|
+
.launcher-icon { position: relative; width: 15px; height: 15px; flex: none; }
|
|
1678
|
+
.launcher-icon svg { position: absolute; inset: 0; width: 100%; height: 100%; transition: opacity 200ms var(--rf-ease), transform 320ms var(--rf-ease); }
|
|
1679
|
+
.launcher-icon .on-close { opacity: 0; transform: rotate(-90deg) scale(0.6); }
|
|
1680
|
+
.launcher[data-open="true"] .on-open { opacity: 0; transform: rotate(90deg) scale(0.6); }
|
|
1681
|
+
.launcher[data-open="true"] .on-close { opacity: 1; transform: none; }
|
|
1369
1682
|
|
|
1370
1683
|
.panel {
|
|
1371
|
-
width: min(
|
|
1372
|
-
max-height: min(
|
|
1684
|
+
width: min(352px, calc(100vw - 24px));
|
|
1685
|
+
max-height: min(600px, calc(100vh - 96px));
|
|
1373
1686
|
display: flex;
|
|
1374
1687
|
flex-direction: column;
|
|
1375
1688
|
overflow: hidden;
|
|
1376
1689
|
background: var(--rf-bg);
|
|
1377
|
-
border: 1px solid var(--rf-border);
|
|
1378
1690
|
border-radius: var(--rf-radius);
|
|
1379
1691
|
box-shadow: var(--rf-shadow);
|
|
1380
1692
|
animation: rf-in 220ms var(--rf-ease);
|
|
1381
1693
|
}
|
|
1694
|
+
.root[data-position^="bottom"] .panel { transform-origin: bottom center; }
|
|
1695
|
+
.root[data-position^="top"] .panel { transform-origin: top center; animation-name: rf-in-top; }
|
|
1382
1696
|
|
|
1383
1697
|
@keyframes rf-in {
|
|
1384
|
-
from { opacity: 0; transform: translateY(
|
|
1385
|
-
|
|
1698
|
+
from { opacity: 0; transform: translateY(6px) scale(0.985); }
|
|
1699
|
+
}
|
|
1700
|
+
@keyframes rf-in-top {
|
|
1701
|
+
from { opacity: 0; transform: translateY(-6px) scale(0.985); }
|
|
1386
1702
|
}
|
|
1703
|
+
@keyframes rf-fade { from { opacity: 0; } }
|
|
1387
1704
|
|
|
1388
1705
|
@media (prefers-reduced-motion: reduce) {
|
|
1389
|
-
|
|
1706
|
+
*, *::before, *::after { animation: none !important; transition: none !important; }
|
|
1390
1707
|
}
|
|
1391
1708
|
|
|
1392
1709
|
.head {
|
|
1393
1710
|
display: flex;
|
|
1394
1711
|
align-items: center;
|
|
1395
1712
|
gap: 8px;
|
|
1396
|
-
padding:
|
|
1713
|
+
padding: 12px 10px 4px 14px;
|
|
1397
1714
|
}
|
|
1398
|
-
.head h2 { margin: 0; font-size:
|
|
1715
|
+
.head h2 { margin: 0; font-size: 13.5px; font-weight: 600; letter-spacing: -0.01em; }
|
|
1399
1716
|
.head .spacer { flex: 1; }
|
|
1400
1717
|
|
|
1401
1718
|
.icon-btn {
|
|
1402
1719
|
display: inline-flex;
|
|
1403
1720
|
align-items: center;
|
|
1404
1721
|
justify-content: center;
|
|
1405
|
-
width:
|
|
1406
|
-
height:
|
|
1407
|
-
|
|
1722
|
+
width: 26px;
|
|
1723
|
+
height: 26px;
|
|
1724
|
+
flex: none;
|
|
1725
|
+
border-radius: 6px;
|
|
1408
1726
|
color: var(--rf-text-muted);
|
|
1409
1727
|
transition: background 120ms var(--rf-ease), color 120ms var(--rf-ease);
|
|
1410
1728
|
}
|
|
1411
|
-
.icon-btn:hover { background: var(--rf-bg-
|
|
1729
|
+
.icon-btn:hover { background: var(--rf-bg-hover); color: var(--rf-text); }
|
|
1412
1730
|
|
|
1413
|
-
.body { display: flex; flex-direction: column; gap:
|
|
1731
|
+
.body { display: flex; flex-direction: column; gap: 8px; padding: 6px 14px 14px; overflow-y: auto; }
|
|
1414
1732
|
|
|
1415
1733
|
.segmented {
|
|
1416
1734
|
border: 0;
|
|
@@ -1419,94 +1737,88 @@ button {
|
|
|
1419
1737
|
display: grid;
|
|
1420
1738
|
grid-auto-flow: column;
|
|
1421
1739
|
grid-auto-columns: 1fr;
|
|
1422
|
-
gap:
|
|
1423
|
-
padding:
|
|
1740
|
+
gap: 2px;
|
|
1741
|
+
padding: 2px;
|
|
1424
1742
|
background: var(--rf-bg-subtle);
|
|
1425
|
-
border-radius:
|
|
1743
|
+
border-radius: var(--rf-radius-sm);
|
|
1426
1744
|
}
|
|
1427
1745
|
.segmented button {
|
|
1428
1746
|
display: flex;
|
|
1429
1747
|
align-items: center;
|
|
1430
1748
|
justify-content: center;
|
|
1431
|
-
gap:
|
|
1432
|
-
height:
|
|
1433
|
-
border-radius:
|
|
1434
|
-
font-size:
|
|
1749
|
+
gap: 5px;
|
|
1750
|
+
height: 26px;
|
|
1751
|
+
border-radius: 6px;
|
|
1752
|
+
font-size: 12.5px;
|
|
1435
1753
|
font-weight: 500;
|
|
1436
1754
|
color: var(--rf-text-muted);
|
|
1437
|
-
transition: background
|
|
1755
|
+
transition: background 160ms var(--rf-ease), color 160ms var(--rf-ease);
|
|
1438
1756
|
}
|
|
1757
|
+
.segmented button:hover { color: var(--rf-text); }
|
|
1439
1758
|
.segmented button[aria-pressed="true"] {
|
|
1440
1759
|
background: var(--rf-bg);
|
|
1441
1760
|
color: var(--rf-text);
|
|
1442
|
-
box-shadow:
|
|
1761
|
+
box-shadow: var(--rf-shadow-low);
|
|
1443
1762
|
}
|
|
1444
1763
|
|
|
1445
1764
|
textarea, input[type="email"] {
|
|
1446
1765
|
width: 100%;
|
|
1447
|
-
padding: 10px
|
|
1766
|
+
padding: 8px 10px;
|
|
1448
1767
|
font: inherit;
|
|
1449
1768
|
color: var(--rf-text);
|
|
1450
1769
|
background: var(--rf-bg);
|
|
1451
1770
|
border: 1px solid var(--rf-border);
|
|
1452
|
-
border-radius:
|
|
1453
|
-
transition: border-color
|
|
1771
|
+
border-radius: var(--rf-radius-sm);
|
|
1772
|
+
transition: border-color 160ms var(--rf-ease), box-shadow 160ms var(--rf-ease);
|
|
1454
1773
|
}
|
|
1455
|
-
textarea { min-height:
|
|
1774
|
+
textarea { min-height: 84px; max-height: 220px; resize: none; }
|
|
1456
1775
|
textarea::placeholder, input::placeholder { color: var(--rf-text-muted); }
|
|
1457
|
-
textarea:focus, input:focus { outline: none; border-color: var(--rf-
|
|
1776
|
+
textarea:focus, input:focus { outline: none; border-color: color-mix(in srgb, var(--rf-accent) 55%, transparent); box-shadow: var(--rf-ring); }
|
|
1777
|
+
|
|
1778
|
+
.attachments { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
1458
1779
|
|
|
1459
|
-
.
|
|
1780
|
+
.card {
|
|
1781
|
+
flex: 1 0 100%;
|
|
1460
1782
|
display: flex;
|
|
1783
|
+
align-items: center;
|
|
1461
1784
|
gap: 10px;
|
|
1462
|
-
|
|
1463
|
-
|
|
1785
|
+
min-width: 0;
|
|
1786
|
+
padding: 6px;
|
|
1464
1787
|
border-radius: 10px;
|
|
1465
1788
|
background: var(--rf-bg-subtle);
|
|
1789
|
+
animation: rf-fade 200ms var(--rf-ease);
|
|
1466
1790
|
}
|
|
1467
1791
|
.thumb {
|
|
1468
1792
|
position: relative;
|
|
1469
|
-
flex: 0 0
|
|
1470
|
-
height:
|
|
1793
|
+
flex: 0 0 60px;
|
|
1794
|
+
height: 42px;
|
|
1471
1795
|
overflow: hidden;
|
|
1472
1796
|
border-radius: 6px;
|
|
1473
|
-
border: 1px solid var(--rf-border);
|
|
1474
1797
|
background: var(--rf-bg);
|
|
1798
|
+
box-shadow: var(--rf-shadow-inset);
|
|
1475
1799
|
}
|
|
1476
1800
|
.thumb img { width: 100%; height: 100%; object-fit: cover; object-position: top center; display: block; }
|
|
1477
|
-
.
|
|
1478
|
-
.
|
|
1479
|
-
.
|
|
1480
|
-
.
|
|
1481
|
-
|
|
1482
|
-
.
|
|
1483
|
-
|
|
1484
|
-
align-items: center;
|
|
1485
|
-
gap: 6px;
|
|
1486
|
-
max-width: 100%;
|
|
1487
|
-
padding: 5px 8px;
|
|
1488
|
-
font-size: 12px;
|
|
1489
|
-
border: 1px solid var(--rf-border);
|
|
1490
|
-
border-radius: 8px;
|
|
1491
|
-
background: var(--rf-bg-subtle);
|
|
1492
|
-
color: var(--rf-text-muted);
|
|
1493
|
-
}
|
|
1494
|
-
.chip code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; color: var(--rf-text); }
|
|
1495
|
-
.chip .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1801
|
+
.thumb.contain img { object-fit: contain; object-position: center; }
|
|
1802
|
+
.card .meta { display: flex; flex-direction: column; justify-content: center; gap: 1px; min-width: 0; }
|
|
1803
|
+
.card .meta strong { font-size: 12.5px; font-weight: 550; }
|
|
1804
|
+
.card .meta span { font-size: 11.5px; color: var(--rf-text-muted); }
|
|
1805
|
+
.card .actions { display: flex; gap: 2px; margin-left: auto; align-items: center; }
|
|
1806
|
+
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1807
|
+
.card code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; }
|
|
1496
1808
|
|
|
1497
|
-
.row { display: flex; gap: 8px; align-items: center; }
|
|
1498
1809
|
.ghost-btn {
|
|
1810
|
+
flex: 0 0 auto;
|
|
1499
1811
|
display: inline-flex;
|
|
1500
1812
|
align-items: center;
|
|
1501
1813
|
gap: 6px;
|
|
1502
|
-
height:
|
|
1814
|
+
height: 30px;
|
|
1503
1815
|
padding: 0 10px;
|
|
1504
|
-
font-size:
|
|
1816
|
+
font-size: 12.5px;
|
|
1505
1817
|
font-weight: 500;
|
|
1506
1818
|
color: var(--rf-text-muted);
|
|
1507
|
-
border:
|
|
1508
|
-
|
|
1509
|
-
transition: background
|
|
1819
|
+
border-radius: var(--rf-radius-sm);
|
|
1820
|
+
box-shadow: var(--rf-shadow-inset);
|
|
1821
|
+
transition: background 160ms var(--rf-ease), color 160ms var(--rf-ease);
|
|
1510
1822
|
}
|
|
1511
1823
|
.ghost-btn:hover { background: var(--rf-bg-subtle); color: var(--rf-text); }
|
|
1512
1824
|
|
|
@@ -1516,53 +1828,55 @@ textarea:focus, input:focus { outline: none; border-color: var(--rf-primary); bo
|
|
|
1516
1828
|
justify-content: center;
|
|
1517
1829
|
gap: 8px;
|
|
1518
1830
|
width: 100%;
|
|
1519
|
-
height:
|
|
1831
|
+
height: 32px;
|
|
1832
|
+
margin-top: 2px;
|
|
1833
|
+
font-size: 13px;
|
|
1520
1834
|
font-weight: 550;
|
|
1521
1835
|
color: var(--rf-primary-text);
|
|
1522
1836
|
background: var(--rf-primary);
|
|
1523
|
-
border-radius:
|
|
1524
|
-
transition:
|
|
1837
|
+
border-radius: var(--rf-radius-sm);
|
|
1838
|
+
transition: opacity 160ms var(--rf-ease), transform 160ms var(--rf-ease);
|
|
1525
1839
|
}
|
|
1526
|
-
.submit:hover:not(:disabled) {
|
|
1527
|
-
.submit:disabled {
|
|
1840
|
+
.submit:hover:not(:disabled) { opacity: 0.88; }
|
|
1841
|
+
.submit:active:not(:disabled) { transform: scale(0.99); }
|
|
1842
|
+
.submit:disabled { opacity: 0.35; cursor: not-allowed; }
|
|
1528
1843
|
|
|
1529
|
-
.error { margin: 0; font-size:
|
|
1530
|
-
.hint { margin: 0; font-size: 12px; color: var(--rf-text-muted); }
|
|
1844
|
+
.error { margin: 0; font-size: 12px; color: var(--rf-danger); }
|
|
1531
1845
|
|
|
1532
1846
|
.footer {
|
|
1533
|
-
min-height:
|
|
1847
|
+
min-height: 34px;
|
|
1534
1848
|
padding: 0 14px;
|
|
1535
1849
|
border-top: 1px solid var(--rf-border);
|
|
1536
1850
|
display: flex;
|
|
1537
1851
|
align-items: center;
|
|
1538
1852
|
justify-content: center;
|
|
1539
1853
|
gap: 12px;
|
|
1540
|
-
font-size:
|
|
1854
|
+
font-size: 11px;
|
|
1541
1855
|
color: var(--rf-text-muted);
|
|
1542
1856
|
}
|
|
1543
1857
|
.footer[data-dismissible="true"] { justify-content: space-between; }
|
|
1544
|
-
.footer a { color: inherit; text-decoration: none; font-weight: 550; }
|
|
1858
|
+
.footer a { color: inherit; text-decoration: none; font-weight: 550; transition: color 120ms var(--rf-ease); }
|
|
1545
1859
|
.footer a:hover { color: var(--rf-text); }
|
|
1546
1860
|
.dismiss-btn {
|
|
1547
1861
|
align-self: stretch;
|
|
1548
1862
|
color: inherit;
|
|
1549
|
-
font-size:
|
|
1550
|
-
|
|
1551
|
-
text-underline-offset: 3px;
|
|
1863
|
+
font-size: 11px;
|
|
1864
|
+
transition: color 120ms var(--rf-ease);
|
|
1552
1865
|
}
|
|
1553
1866
|
.dismiss-btn:hover { color: var(--rf-text); }
|
|
1554
1867
|
|
|
1555
|
-
.done { display: flex; flex-direction: column; align-items: center; gap:
|
|
1868
|
+
.done { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 30px 20px 34px; text-align: center; }
|
|
1556
1869
|
.done svg { color: var(--rf-success); }
|
|
1557
|
-
.done h2 { margin: 0; font-size:
|
|
1558
|
-
.done p { margin: 0; font-size:
|
|
1870
|
+
.done h2 { margin: 0; font-size: 14px; font-weight: 600; }
|
|
1871
|
+
.done p { margin: 0; font-size: 12.5px; color: var(--rf-text-muted); }
|
|
1559
1872
|
|
|
1560
1873
|
.spinner {
|
|
1561
|
-
width:
|
|
1562
|
-
height:
|
|
1874
|
+
width: 14px;
|
|
1875
|
+
height: 14px;
|
|
1563
1876
|
border: 2px solid currentColor;
|
|
1564
1877
|
border-right-color: transparent;
|
|
1565
1878
|
border-radius: 50%;
|
|
1879
|
+
opacity: 0.6;
|
|
1566
1880
|
animation: rf-spin 700ms linear infinite;
|
|
1567
1881
|
}
|
|
1568
1882
|
@keyframes rf-spin { to { transform: rotate(360deg); } }
|
|
@@ -1575,16 +1889,17 @@ textarea:focus, input:focus { outline: none; border-color: var(--rf-primary); bo
|
|
|
1575
1889
|
z-index: 2147483001;
|
|
1576
1890
|
display: flex;
|
|
1577
1891
|
flex-direction: column;
|
|
1578
|
-
background: rgb(
|
|
1579
|
-
backdrop-filter: blur(
|
|
1892
|
+
background: rgb(15 15 15 / 76%);
|
|
1893
|
+
backdrop-filter: blur(3px);
|
|
1894
|
+
animation: rf-fade 180ms var(--rf-ease);
|
|
1580
1895
|
}
|
|
1581
1896
|
|
|
1582
1897
|
.editor { flex: 1; display: flex; align-items: center; justify-content: center; padding: 24px 24px 0; min-height: 0; }
|
|
1583
1898
|
.editor canvas {
|
|
1584
1899
|
max-width: 100%;
|
|
1585
1900
|
max-height: 100%;
|
|
1586
|
-
border-radius:
|
|
1587
|
-
box-shadow: 0
|
|
1901
|
+
border-radius: 10px;
|
|
1902
|
+
box-shadow: 0 20px 60px -16px rgb(0 0 0 / 70%);
|
|
1588
1903
|
cursor: crosshair;
|
|
1589
1904
|
touch-action: none;
|
|
1590
1905
|
}
|
|
@@ -1600,97 +1915,106 @@ textarea:focus, input:focus { outline: none; border-color: var(--rf-primary); bo
|
|
|
1600
1915
|
.toolbar .group {
|
|
1601
1916
|
display: flex;
|
|
1602
1917
|
align-items: center;
|
|
1603
|
-
gap:
|
|
1604
|
-
padding:
|
|
1918
|
+
gap: 2px;
|
|
1919
|
+
padding: 4px;
|
|
1605
1920
|
background: var(--rf-bg);
|
|
1606
|
-
border:
|
|
1607
|
-
border-radius: 12px;
|
|
1921
|
+
border-radius: 10px;
|
|
1608
1922
|
box-shadow: var(--rf-shadow);
|
|
1609
1923
|
}
|
|
1610
1924
|
.tool {
|
|
1611
1925
|
display: inline-flex;
|
|
1612
1926
|
align-items: center;
|
|
1613
1927
|
justify-content: center;
|
|
1614
|
-
width:
|
|
1615
|
-
height:
|
|
1616
|
-
border-radius:
|
|
1928
|
+
width: 30px;
|
|
1929
|
+
height: 30px;
|
|
1930
|
+
border-radius: 7px;
|
|
1617
1931
|
color: var(--rf-text-muted);
|
|
1618
|
-
transition: background
|
|
1932
|
+
transition: background 160ms var(--rf-ease), color 160ms var(--rf-ease);
|
|
1619
1933
|
}
|
|
1620
|
-
.tool:hover { background: var(--rf-bg-
|
|
1621
|
-
.tool[aria-pressed="true"] { background: var(--rf-
|
|
1934
|
+
.tool:hover { background: var(--rf-bg-hover); color: var(--rf-text); }
|
|
1935
|
+
.tool[aria-pressed="true"] { background: var(--rf-bg-hover); color: var(--rf-text); }
|
|
1622
1936
|
.swatch {
|
|
1623
|
-
width:
|
|
1624
|
-
height:
|
|
1937
|
+
width: 20px;
|
|
1938
|
+
height: 20px;
|
|
1625
1939
|
border-radius: 50%;
|
|
1626
1940
|
border: 2px solid transparent;
|
|
1627
|
-
transition: transform
|
|
1941
|
+
transition: transform 200ms var(--rf-ease), border-color 160ms var(--rf-ease);
|
|
1628
1942
|
}
|
|
1629
|
-
.swatch[aria-pressed="true"] { border-color: var(--rf-text); transform: scale(1.
|
|
1943
|
+
.swatch[aria-pressed="true"] { border-color: var(--rf-text); transform: scale(1.15); }
|
|
1630
1944
|
.toolbar .done-btn {
|
|
1631
|
-
height:
|
|
1632
|
-
padding: 0
|
|
1945
|
+
height: 30px;
|
|
1946
|
+
padding: 0 13px;
|
|
1947
|
+
font-size: 13px;
|
|
1633
1948
|
font-weight: 550;
|
|
1634
|
-
border-radius:
|
|
1949
|
+
border-radius: 7px;
|
|
1635
1950
|
color: var(--rf-primary-text);
|
|
1636
1951
|
background: var(--rf-primary);
|
|
1952
|
+
transition: opacity 160ms var(--rf-ease);
|
|
1637
1953
|
}
|
|
1954
|
+
.toolbar .done-btn:hover { opacity: 0.88; }
|
|
1638
1955
|
|
|
1639
|
-
.picker { position: fixed; inset: 0; z-index: 2147483002; cursor: crosshair; }
|
|
1640
1956
|
.picker-box {
|
|
1641
1957
|
position: fixed;
|
|
1642
1958
|
pointer-events: none;
|
|
1643
|
-
border:
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
transition:
|
|
1959
|
+
border-radius: 5px;
|
|
1960
|
+
background: color-mix(in srgb, var(--rf-accent) 10%, transparent);
|
|
1961
|
+
box-shadow: 0 0 0 2px var(--rf-accent), 0 0 0 5px color-mix(in srgb, var(--rf-accent) 18%, transparent);
|
|
1962
|
+
transition: top 130ms var(--rf-ease), left 130ms var(--rf-ease), width 130ms var(--rf-ease), height 130ms var(--rf-ease);
|
|
1647
1963
|
}
|
|
1648
1964
|
.picker-label {
|
|
1649
1965
|
position: fixed;
|
|
1650
1966
|
pointer-events: none;
|
|
1651
|
-
display: flex;
|
|
1967
|
+
display: inline-flex;
|
|
1652
1968
|
align-items: center;
|
|
1653
1969
|
gap: 6px;
|
|
1654
1970
|
max-width: min(420px, 90vw);
|
|
1655
|
-
|
|
1656
|
-
|
|
1971
|
+
height: 24px;
|
|
1972
|
+
padding: 0 8px;
|
|
1973
|
+
font-size: 11.5px;
|
|
1657
1974
|
font-weight: 500;
|
|
1658
|
-
color:
|
|
1659
|
-
background: var(--rf-
|
|
1660
|
-
border-radius:
|
|
1975
|
+
color: #ffffff;
|
|
1976
|
+
background: var(--rf-accent);
|
|
1977
|
+
border-radius: 6px;
|
|
1978
|
+
box-shadow: 0 4px 12px -3px rgb(15 15 15 / 35%);
|
|
1661
1979
|
white-space: nowrap;
|
|
1662
1980
|
overflow: hidden;
|
|
1663
|
-
|
|
1981
|
+
transition: top 130ms var(--rf-ease), left 130ms var(--rf-ease);
|
|
1664
1982
|
}
|
|
1665
|
-
.picker-label
|
|
1983
|
+
.picker-label strong { flex: none; font-weight: 600; }
|
|
1984
|
+
.picker-label span { min-width: 0; overflow: hidden; text-overflow: ellipsis; opacity: 0.75; }
|
|
1666
1985
|
.picker-hint {
|
|
1667
1986
|
position: fixed;
|
|
1668
1987
|
left: 50%;
|
|
1669
1988
|
bottom: 24px;
|
|
1670
1989
|
transform: translateX(-50%);
|
|
1671
|
-
|
|
1672
|
-
|
|
1990
|
+
display: flex;
|
|
1991
|
+
align-items: center;
|
|
1992
|
+
gap: 6px;
|
|
1993
|
+
padding: 7px 12px;
|
|
1994
|
+
font-size: 12.5px;
|
|
1673
1995
|
color: var(--rf-text);
|
|
1674
1996
|
background: var(--rf-bg);
|
|
1675
|
-
border: 1px solid var(--rf-border);
|
|
1676
1997
|
border-radius: 10px;
|
|
1677
1998
|
box-shadow: var(--rf-shadow);
|
|
1999
|
+
animation: rf-fade 240ms var(--rf-ease);
|
|
1678
2000
|
}
|
|
1679
2001
|
.picker-hint kbd {
|
|
1680
2002
|
padding: 1px 5px;
|
|
1681
2003
|
font: inherit;
|
|
1682
|
-
font-size:
|
|
1683
|
-
|
|
2004
|
+
font-size: 11px;
|
|
2005
|
+
color: var(--rf-text-muted);
|
|
1684
2006
|
border-radius: 4px;
|
|
1685
2007
|
background: var(--rf-bg-subtle);
|
|
2008
|
+
box-shadow: var(--rf-shadow-inset);
|
|
1686
2009
|
}
|
|
2010
|
+
|
|
1687
2011
|
@media (pointer: coarse) {
|
|
1688
2012
|
.icon-btn, .tool, .swatch {
|
|
1689
2013
|
width: 44px;
|
|
1690
2014
|
height: 44px;
|
|
1691
2015
|
}
|
|
1692
2016
|
|
|
1693
|
-
.segmented button, .ghost-btn, .submit, .toolbar .done-btn {
|
|
2017
|
+
.launcher, .segmented button, .ghost-btn, .submit, .toolbar .done-btn {
|
|
1694
2018
|
min-height: 44px;
|
|
1695
2019
|
}
|
|
1696
2020
|
|
|
@@ -1705,7 +2029,6 @@ textarea:focus, input:focus { outline: none; border-color: var(--rf-primary); bo
|
|
|
1705
2029
|
justify-content: center;
|
|
1706
2030
|
}
|
|
1707
2031
|
}
|
|
1708
|
-
|
|
1709
2032
|
`;
|
|
1710
2033
|
//#endregion
|
|
1711
2034
|
//#region src/feedback/ui/shadow-portal.tsx
|
|
@@ -1832,6 +2155,128 @@ function startConsoleRecorder(options) {
|
|
|
1832
2155
|
};
|
|
1833
2156
|
}
|
|
1834
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
|
|
1835
2280
|
//#region src/feedback/core/submit.ts
|
|
1836
2281
|
const MAX_TITLE_LENGTH = 100;
|
|
1837
2282
|
const NEWLINE = /\r?\n/;
|
|
@@ -1849,20 +2294,20 @@ function buildDescription(params) {
|
|
|
1849
2294
|
const contact = isAnonymous && email ? `\n\n---\nContact: ${email}` : "";
|
|
1850
2295
|
return `[${label}] ${message.trim()}${contact}`;
|
|
1851
2296
|
}
|
|
2297
|
+
async function upload(transport, image) {
|
|
2298
|
+
const { uploadUrl } = await transport.getScreenshotUploadUrl();
|
|
2299
|
+
return await transport.uploadImage(uploadUrl, image);
|
|
2300
|
+
}
|
|
1852
2301
|
async function attachScreenshot(transport, feedbackId, submission) {
|
|
1853
2302
|
const { annotated, annotations, context, screenshot } = submission;
|
|
1854
2303
|
if (!screenshot) return false;
|
|
1855
|
-
const
|
|
1856
|
-
const
|
|
1857
|
-
let annotatedStorageId;
|
|
1858
|
-
if (annotated) {
|
|
1859
|
-
const target = await transport.getScreenshotUploadUrl();
|
|
1860
|
-
annotatedStorageId = await transport.uploadImage(target.uploadUrl, annotated);
|
|
1861
|
-
}
|
|
2304
|
+
const storageId = await upload(transport, screenshot);
|
|
2305
|
+
const annotatedStorageId = annotated ? await upload(transport, annotated) : void 0;
|
|
1862
2306
|
const wireAnnotations = toWireAnnotations(annotations);
|
|
1863
2307
|
await transport.saveScreenshot({
|
|
1864
2308
|
annotatedStorageId,
|
|
1865
2309
|
annotations: wireAnnotations.length > 0 ? wireAnnotations : void 0,
|
|
2310
|
+
captureSource: "widget",
|
|
1866
2311
|
feedbackId,
|
|
1867
2312
|
filename: "screenshot.png",
|
|
1868
2313
|
height: screenshot.height,
|
|
@@ -1874,6 +2319,21 @@ async function attachScreenshot(transport, feedbackId, submission) {
|
|
|
1874
2319
|
});
|
|
1875
2320
|
return true;
|
|
1876
2321
|
}
|
|
2322
|
+
async function attachElement(transport, feedbackId, submission) {
|
|
2323
|
+
const { context, element } = submission;
|
|
2324
|
+
if (!element) return;
|
|
2325
|
+
await transport.saveScreenshot({
|
|
2326
|
+
captureSource: "element",
|
|
2327
|
+
feedbackId,
|
|
2328
|
+
filename: "element.png",
|
|
2329
|
+
height: element.height,
|
|
2330
|
+
mimeType: element.mimeType,
|
|
2331
|
+
pageUrl: context.url,
|
|
2332
|
+
size: element.blob.size,
|
|
2333
|
+
storageId: await upload(transport, element),
|
|
2334
|
+
width: element.width
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
1877
2337
|
/**
|
|
1878
2338
|
* Creates the feedback first, then attaches the capture. A failed upload is
|
|
1879
2339
|
* reported back but never discards what the user wrote.
|
|
@@ -1886,12 +2346,8 @@ async function submitWidgetFeedback(transport, submission) {
|
|
|
1886
2346
|
description: buildDescription(submission),
|
|
1887
2347
|
title
|
|
1888
2348
|
});
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
screenshotSaved = await attachScreenshot(transport, feedbackId, submission);
|
|
1892
|
-
} catch {
|
|
1893
|
-
screenshotSaved = false;
|
|
1894
|
-
}
|
|
2349
|
+
const screenshotSaved = await attachScreenshot(transport, feedbackId, submission).catch(() => false);
|
|
2350
|
+
await attachElement(transport, feedbackId, submission).catch(() => void 0);
|
|
1895
2351
|
return {
|
|
1896
2352
|
feedbackId,
|
|
1897
2353
|
screenshotSaved
|
|
@@ -1919,6 +2375,38 @@ function createFeedbackTransport(client) {
|
|
|
1919
2375
|
};
|
|
1920
2376
|
}
|
|
1921
2377
|
//#endregion
|
|
2378
|
+
//#region src/feedback/ui/use-route-sync.ts
|
|
2379
|
+
/**
|
|
2380
|
+
* Calls `refresh` whenever the app navigates client-side while it is active.
|
|
2381
|
+
*
|
|
2382
|
+
* SPA routes never fire popstate on push/replace — patching history methods is
|
|
2383
|
+
* the only way to keep screenshot and URL describing the same page while the
|
|
2384
|
+
* reporter is composing.
|
|
2385
|
+
*/
|
|
2386
|
+
function useRouteSync(enabled, refresh) {
|
|
2387
|
+
useEffect(() => {
|
|
2388
|
+
if (!enabled) return;
|
|
2389
|
+
const originalPush = window.history.pushState.bind(window.history);
|
|
2390
|
+
const originalReplace = window.history.replaceState.bind(window.history);
|
|
2391
|
+
window.history.pushState = function pushState(data, unused, url) {
|
|
2392
|
+
const result = originalPush(data, unused, url);
|
|
2393
|
+
refresh();
|
|
2394
|
+
return result;
|
|
2395
|
+
};
|
|
2396
|
+
window.history.replaceState = function replaceState(data, unused, url) {
|
|
2397
|
+
const result = originalReplace(data, unused, url);
|
|
2398
|
+
refresh();
|
|
2399
|
+
return result;
|
|
2400
|
+
};
|
|
2401
|
+
window.addEventListener("popstate", refresh);
|
|
2402
|
+
return () => {
|
|
2403
|
+
window.history.pushState = originalPush;
|
|
2404
|
+
window.history.replaceState = originalReplace;
|
|
2405
|
+
window.removeEventListener("popstate", refresh);
|
|
2406
|
+
};
|
|
2407
|
+
}, [enabled, refresh]);
|
|
2408
|
+
}
|
|
2409
|
+
//#endregion
|
|
1922
2410
|
//#region src/feedback/ui/use-widget-state.ts
|
|
1923
2411
|
const SUCCESS_CLOSE_DELAY = 2400;
|
|
1924
2412
|
const IS_APPLE = /Mac|iPhone|iPad/;
|
|
@@ -1958,22 +2446,21 @@ function matchesHotkey(event, hotkey, platform = typeof navigator === "undefined
|
|
|
1958
2446
|
};
|
|
1959
2447
|
return event.altKey === expected.alt && event.ctrlKey === expected.ctrl && event.metaKey === expected.meta && event.shiftKey === expected.shift;
|
|
1960
2448
|
}
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
};
|
|
2449
|
+
/**
|
|
2450
|
+
* A capture whose object URL is revoked the moment it is replaced or dropped —
|
|
2451
|
+
* holding one without this leaks the blob for the lifetime of the page.
|
|
2452
|
+
*/
|
|
2453
|
+
function useOwnedCapture() {
|
|
2454
|
+
const [image, setImage] = useState(null);
|
|
2455
|
+
const ref = useRef(null);
|
|
2456
|
+
useEffect(() => {
|
|
2457
|
+
ref.current = image;
|
|
2458
|
+
}, [image]);
|
|
2459
|
+
useEffect(() => () => releaseCapture(ref.current), []);
|
|
2460
|
+
return [image, useCallback((next) => {
|
|
2461
|
+
releaseCapture(ref.current);
|
|
2462
|
+
setImage(next);
|
|
2463
|
+
}, [])];
|
|
1977
2464
|
}
|
|
1978
2465
|
function useWidgetState(props) {
|
|
1979
2466
|
const context = useContext(RefletContext);
|
|
@@ -1990,18 +2477,17 @@ function useWidgetState(props) {
|
|
|
1990
2477
|
const [message, setMessage] = useState("");
|
|
1991
2478
|
const [email, setEmail] = useState("");
|
|
1992
2479
|
const [honeypot, setHoneypot] = useState("");
|
|
1993
|
-
const [capture,
|
|
2480
|
+
const [capture, replaceCapture] = useOwnedCapture();
|
|
2481
|
+
const [elementCapture, replaceElementCapture] = useOwnedCapture();
|
|
1994
2482
|
const [annotations, setAnnotations] = useState([]);
|
|
1995
2483
|
const [selection, setSelection] = useState(null);
|
|
1996
2484
|
const [isCapturing, setIsCapturing] = useState(false);
|
|
1997
2485
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
1998
2486
|
const [error, setError] = useState(null);
|
|
1999
2487
|
const [dismissedUntil, setDismissedUntil] = useState(() => readDismissedUntil(dismissalKey));
|
|
2000
|
-
const
|
|
2488
|
+
const pickRef = useRef(0);
|
|
2001
2489
|
const consoleRef = useRef(null);
|
|
2002
|
-
|
|
2003
|
-
captureRef.current = capture;
|
|
2004
|
-
}, [capture]);
|
|
2490
|
+
const frozenContext = useRef(null);
|
|
2005
2491
|
useEffect(() => {
|
|
2006
2492
|
setDismissedUntil(readDismissedUntil(dismissalKey));
|
|
2007
2493
|
}, [dismissalKey]);
|
|
@@ -2014,23 +2500,22 @@ function useWidgetState(props) {
|
|
|
2014
2500
|
recorder.stop();
|
|
2015
2501
|
};
|
|
2016
2502
|
}, [props.captureConsole]);
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
const replaceCapture = useCallback((next) => {
|
|
2021
|
-
releaseCapture(captureRef.current);
|
|
2022
|
-
setCapture(next);
|
|
2023
|
-
}, []);
|
|
2024
|
-
const takeCapture = useCallback((element) => {
|
|
2503
|
+
/** Frozen at the same instant as the screenshot, so shot and URL always match. */
|
|
2504
|
+
const takeCapture = useCallback(() => {
|
|
2505
|
+
const context = collectPageContext({ sdkVersion: SDK_VERSION });
|
|
2025
2506
|
setIsCapturing(true);
|
|
2026
|
-
captureViewport(
|
|
2507
|
+
captureViewport().catch(() => null).then((image) => {
|
|
2027
2508
|
setIsCapturing(false);
|
|
2028
2509
|
replaceCapture(image);
|
|
2510
|
+
frozenContext.current = image ? context : null;
|
|
2029
2511
|
});
|
|
2030
2512
|
}, [replaceCapture]);
|
|
2513
|
+
useRouteSync(isOpen && step === "compose" && annotations.length === 0 && !selection && props.captureOnOpen !== false, takeCapture);
|
|
2031
2514
|
const reset = useCallback(() => {
|
|
2032
|
-
|
|
2033
|
-
|
|
2515
|
+
pickRef.current++;
|
|
2516
|
+
frozenContext.current = null;
|
|
2517
|
+
replaceCapture(null);
|
|
2518
|
+
replaceElementCapture(null);
|
|
2034
2519
|
setAnnotations([]);
|
|
2035
2520
|
setSelection(null);
|
|
2036
2521
|
setMessage("");
|
|
@@ -2039,7 +2524,11 @@ function useWidgetState(props) {
|
|
|
2039
2524
|
setError(null);
|
|
2040
2525
|
setStep("compose");
|
|
2041
2526
|
setCategory(props.defaultCategory ?? "bug");
|
|
2042
|
-
}, [
|
|
2527
|
+
}, [
|
|
2528
|
+
props.defaultCategory,
|
|
2529
|
+
replaceCapture,
|
|
2530
|
+
replaceElementCapture
|
|
2531
|
+
]);
|
|
2043
2532
|
const close = useCallback(() => {
|
|
2044
2533
|
setIsOpen(false);
|
|
2045
2534
|
reset();
|
|
@@ -2069,18 +2558,32 @@ function useWidgetState(props) {
|
|
|
2069
2558
|
props.onOpen,
|
|
2070
2559
|
takeCapture
|
|
2071
2560
|
]);
|
|
2072
|
-
|
|
2561
|
+
/** Fresh pixels so the highlight maps onto the scroll position the element had. */
|
|
2562
|
+
const selectElement = useCallback(async (element) => {
|
|
2073
2563
|
const picked = buildElementSelection(element);
|
|
2074
2564
|
setSelection(picked);
|
|
2075
2565
|
setStep("compose");
|
|
2076
|
-
const
|
|
2077
|
-
|
|
2078
|
-
|
|
2566
|
+
const pick = ++pickRef.current;
|
|
2567
|
+
const context = collectPageContext({ sdkVersion: SDK_VERSION });
|
|
2568
|
+
const [fresh, closeUp] = await Promise.all([captureViewport().catch(() => null), captureElementCloseUp(element).catch(() => null)]);
|
|
2569
|
+
if (pickRef.current !== pick) {
|
|
2570
|
+
releaseCapture(fresh);
|
|
2571
|
+
releaseCapture(closeUp);
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
replaceCapture(fresh);
|
|
2575
|
+
replaceElementCapture(closeUp);
|
|
2576
|
+
frozenContext.current = fresh ? context : null;
|
|
2577
|
+
setAnnotations(fresh ? [highlightFor(picked, fresh)] : []);
|
|
2578
|
+
}, [replaceCapture, replaceElementCapture]);
|
|
2079
2579
|
const clearSelection = useCallback(() => {
|
|
2580
|
+
pickRef.current++;
|
|
2581
|
+
replaceElementCapture(null);
|
|
2080
2582
|
setSelection(null);
|
|
2081
2583
|
setAnnotations((previous) => previous.filter((item) => !item.id.startsWith("selection-")));
|
|
2082
|
-
}, []);
|
|
2584
|
+
}, [replaceElementCapture]);
|
|
2083
2585
|
const removeCapture = useCallback(() => {
|
|
2586
|
+
frozenContext.current = null;
|
|
2084
2587
|
replaceCapture(null);
|
|
2085
2588
|
setAnnotations([]);
|
|
2086
2589
|
}, [replaceCapture]);
|
|
@@ -2114,11 +2617,12 @@ function useWidgetState(props) {
|
|
|
2114
2617
|
annotations,
|
|
2115
2618
|
category,
|
|
2116
2619
|
context: {
|
|
2117
|
-
...collectPageContext({ sdkVersion:
|
|
2620
|
+
...frozenContext.current ?? collectPageContext({ sdkVersion: "0.3.1" }),
|
|
2118
2621
|
consoleEvents: consoleRef.current?.events(),
|
|
2119
2622
|
metadata: props.metadata,
|
|
2120
2623
|
selection: selection ?? void 0
|
|
2121
2624
|
},
|
|
2625
|
+
element: elementCapture,
|
|
2122
2626
|
email,
|
|
2123
2627
|
isAnonymous,
|
|
2124
2628
|
message,
|
|
@@ -2139,6 +2643,7 @@ function useWidgetState(props) {
|
|
|
2139
2643
|
category,
|
|
2140
2644
|
client,
|
|
2141
2645
|
close,
|
|
2646
|
+
elementCapture,
|
|
2142
2647
|
email,
|
|
2143
2648
|
honeypot,
|
|
2144
2649
|
isAnonymous,
|
|
@@ -2156,6 +2661,7 @@ function useWidgetState(props) {
|
|
|
2156
2661
|
close,
|
|
2157
2662
|
dismiss,
|
|
2158
2663
|
dismissForDays,
|
|
2664
|
+
elementCapture,
|
|
2159
2665
|
email,
|
|
2160
2666
|
error,
|
|
2161
2667
|
honeypot,
|
|
@@ -2238,22 +2744,18 @@ function RefletFeedback(props) {
|
|
|
2238
2744
|
primaryColor: props.primaryColor,
|
|
2239
2745
|
theme,
|
|
2240
2746
|
children: [
|
|
2241
|
-
/* @__PURE__ */
|
|
2747
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2242
2748
|
className: "root",
|
|
2243
2749
|
"data-position": position,
|
|
2244
|
-
children: /* @__PURE__ */
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
label: labels.trigger,
|
|
2254
|
-
onClick: isOpen ? close : open
|
|
2255
|
-
})]
|
|
2256
|
-
})
|
|
2750
|
+
children: [showPanel && /* @__PURE__ */ jsx(FeedbackPanel, {
|
|
2751
|
+
categories,
|
|
2752
|
+
labels,
|
|
2753
|
+
state
|
|
2754
|
+
}), /* @__PURE__ */ jsx(Launcher, {
|
|
2755
|
+
isOpen,
|
|
2756
|
+
label: labels.trigger,
|
|
2757
|
+
onClick: isOpen ? close : open
|
|
2758
|
+
})]
|
|
2257
2759
|
}),
|
|
2258
2760
|
isOpen && state.step === "annotate" && state.capture && /* @__PURE__ */ jsx(Annotator, {
|
|
2259
2761
|
annotations: state.annotations,
|