reflet-sdk 0.3.5 → 0.4.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 +21 -0
- package/dist/client-BYuSKJw5.js.map +1 -1
- package/dist/{client-BpEYxx4d.d.ts → client-w4Rkl10y.d.ts} +2 -2
- package/dist/{client-BpEYxx4d.d.ts.map → client-w4Rkl10y.d.ts.map} +1 -1
- package/dist/feedback.d.ts +9 -11
- package/dist/feedback.d.ts.map +1 -1
- package/dist/feedback.js +1241 -819
- package/dist/feedback.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/{react-context-DZw_HHjj.js → react-context-3_IbhIFB.js} +11 -2
- package/dist/react-context-3_IbhIFB.js.map +1 -0
- package/dist/react.d.ts +6 -17
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +206 -241
- package/dist/react.js.map +1 -1
- package/dist/{types-wANRMsGI.d.ts → types-DnaDEN9Y.d.ts} +4 -2
- package/dist/types-DnaDEN9Y.d.ts.map +1 -0
- package/package.json +1 -1
- package/dist/react-context-DZw_HHjj.js.map +0 -1
- package/dist/types-wANRMsGI.d.ts.map +0 -1
package/dist/feedback.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { a as REFLET_Z_INDEX, t as RefletContext } from "./react-context-3_IbhIFB.js";
|
|
2
3
|
import { t as Reflet } from "./client-BYuSKJw5.js";
|
|
3
|
-
import { t as RefletContext } from "./react-context-DZw_HHjj.js";
|
|
4
4
|
import { useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import { Slider } from "@base-ui/react/slider";
|
|
@@ -533,6 +533,7 @@ const MAX_DEPTH = 12;
|
|
|
533
533
|
const MIN_PATH_SEGMENTS = 2;
|
|
534
534
|
const MAX_SELECTOR_LENGTH = 600;
|
|
535
535
|
const MAX_SOURCE_LOCATION_LENGTH = 400;
|
|
536
|
+
const MAX_SELECTION_COMMENT_LENGTH = 2e3;
|
|
536
537
|
const TEST_ATTRIBUTES = [
|
|
537
538
|
"data-testid",
|
|
538
539
|
"data-test-id",
|
|
@@ -809,40 +810,42 @@ function collectPageContext(options) {
|
|
|
809
810
|
}
|
|
810
811
|
//#endregion
|
|
811
812
|
//#region src/feedback/types.ts
|
|
812
|
-
const SDK_VERSION = "0.
|
|
813
|
+
const SDK_VERSION = "0.4.1";
|
|
813
814
|
const DEFAULT_WIDGET_LABELS = {
|
|
814
815
|
annotateHint: "Draw on the screenshot to point at the problem.",
|
|
816
|
+
attachElement: "Attach this element",
|
|
815
817
|
attachmentUploadFailed: "Your feedback was saved, but some screenshots are still pending. Retry to attach them.",
|
|
816
818
|
attachScreenshot: "Attach a screenshot",
|
|
817
819
|
back: "Back",
|
|
818
820
|
cancel: "Cancel",
|
|
819
821
|
captureFailed: "Screenshot unavailable. Try again or send without it.",
|
|
820
822
|
capturing: "Taking screenshot…",
|
|
821
|
-
categoryBug: "Bug",
|
|
822
|
-
categoryIdea: "Idea",
|
|
823
|
-
categoryLabel: "Feedback type",
|
|
824
|
-
categoryQuestion: "Question",
|
|
825
823
|
clearAnnotations: "Clear drawing",
|
|
826
|
-
clearSelection: "Remove selected element",
|
|
827
824
|
descriptionPlaceholder: "What would you like to share?",
|
|
828
825
|
dismissForDays: "Hide for {days} days",
|
|
829
826
|
done: "Done",
|
|
827
|
+
elementNote: "Comment on the picked element",
|
|
828
|
+
elementNotePlaceholder: "What is wrong with this?",
|
|
830
829
|
emailInvalid: "Enter a valid email address.",
|
|
831
830
|
emailLabel: "Email",
|
|
832
831
|
emailPlaceholder: "you@company.com",
|
|
832
|
+
emailPromptBody: "We only use it to follow up on this report.",
|
|
833
|
+
emailPromptTitle: "Add your email?",
|
|
833
834
|
errorGeneric: "Something went wrong. Please try again.",
|
|
834
835
|
errorTitleRequired: "Tell us a bit about it first.",
|
|
835
836
|
minimize: "Minimize feedback",
|
|
836
837
|
moreOptions: "More options",
|
|
837
838
|
moveFeedback: "Move feedback",
|
|
839
|
+
pickAnother: "Pick another",
|
|
838
840
|
pickElement: "Point at an element",
|
|
839
|
-
pickElementHint: "
|
|
841
|
+
pickElementHint: "Pick the element you are talking about",
|
|
840
842
|
recapture: "Retake",
|
|
841
843
|
removeScreenshot: "Remove",
|
|
842
844
|
resume: "Resume feedback",
|
|
843
845
|
retakeHint: "Screenshot of this page",
|
|
844
846
|
retryAttachments: "Retry attachments",
|
|
845
847
|
screenshot: "Screenshot",
|
|
848
|
+
sendWithoutEmail: "Send without email",
|
|
846
849
|
submit: "Send feedback",
|
|
847
850
|
successMessage: "We read every report. Thanks for taking the time.",
|
|
848
851
|
successTitle: "Feedback sent",
|
|
@@ -861,7 +864,6 @@ const BOX_TOOLS = [
|
|
|
861
864
|
"blur"
|
|
862
865
|
];
|
|
863
866
|
const MIN_GESTURE_LENGTH = 8;
|
|
864
|
-
const ARROW_HEAD_ANGLE = Math.PI / 7;
|
|
865
867
|
const DEFAULT_PATH_TOLERANCE = 2.5;
|
|
866
868
|
function normalizeRect(start, end) {
|
|
867
869
|
return {
|
|
@@ -871,17 +873,6 @@ function normalizeRect(start, end) {
|
|
|
871
873
|
y: Math.min(start.y, end.y)
|
|
872
874
|
};
|
|
873
875
|
}
|
|
874
|
-
/** The two barbs closing the arrow tip, both `size` px away from `end`. */
|
|
875
|
-
function arrowHead(start, end, size) {
|
|
876
|
-
const angle = Math.atan2(end.y - start.y, end.x - start.x);
|
|
877
|
-
if (Math.hypot(end.x - start.x, end.y - start.y) === 0) return [end, end];
|
|
878
|
-
const barb = (offset) => ({
|
|
879
|
-
x: end.x - size * Math.cos(angle + offset),
|
|
880
|
-
y: end.y - size * Math.sin(angle + offset)
|
|
881
|
-
});
|
|
882
|
-
return [barb(-ARROW_HEAD_ANGLE), barb(ARROW_HEAD_ANGLE)];
|
|
883
|
-
}
|
|
884
|
-
/** Drops points a user cannot perceive, keeping the payload small. */
|
|
885
876
|
function simplifyPath(points, tolerance = DEFAULT_PATH_TOLERANCE) {
|
|
886
877
|
if (points.length < 3) return points;
|
|
887
878
|
const [first, ...rest] = points;
|
|
@@ -932,7 +923,6 @@ function toWire(annotation) {
|
|
|
932
923
|
y: start.y
|
|
933
924
|
};
|
|
934
925
|
}
|
|
935
|
-
/** Serializes to the shape the Reflet API stores next to the screenshot. */
|
|
936
926
|
function toWireAnnotations(annotations) {
|
|
937
927
|
return annotations.filter((item) => !isDegenerate(item)).map(toWire);
|
|
938
928
|
}
|
|
@@ -1025,9 +1015,6 @@ function CheckIcon({ size = 28 }) {
|
|
|
1025
1015
|
function GripIcon() {
|
|
1026
1016
|
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M9 5h.01M15 5h.01M9 12h.01M15 12h.01M9 19h.01M15 19h.01" }) });
|
|
1027
1017
|
}
|
|
1028
|
-
function MinusIcon() {
|
|
1029
|
-
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M5 12h14" }) });
|
|
1030
|
-
}
|
|
1031
1018
|
function MoreIcon() {
|
|
1032
1019
|
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M5 12h.01M12 12h.01M19 12h.01" }) });
|
|
1033
1020
|
}
|
|
@@ -1457,9 +1444,41 @@ function AnnotationToolbar({ editor, labels, drawing }) {
|
|
|
1457
1444
|
});
|
|
1458
1445
|
}
|
|
1459
1446
|
//#endregion
|
|
1447
|
+
//#region src/feedback/core/arrow-geometry.ts
|
|
1448
|
+
function arrowOutline(start, end) {
|
|
1449
|
+
const dx = end.x - start.x;
|
|
1450
|
+
const dy = end.y - start.y;
|
|
1451
|
+
const length = Math.hypot(dx, dy);
|
|
1452
|
+
if (length === 0) return {
|
|
1453
|
+
points: [],
|
|
1454
|
+
strokeWidth: 0
|
|
1455
|
+
};
|
|
1456
|
+
const shaftWidth = Math.min(20, Math.max(6, length * .025), length * .2);
|
|
1457
|
+
const headLength = Math.min(length * .45, shaftWidth * 3);
|
|
1458
|
+
const headHalfWidth = headLength * .55;
|
|
1459
|
+
const shaftLength = length - headLength;
|
|
1460
|
+
const pointOnArrow = (along, across) => ({
|
|
1461
|
+
x: start.x + (dx * along - dy * across) / length,
|
|
1462
|
+
y: start.y + (dy * along + dx * across) / length
|
|
1463
|
+
});
|
|
1464
|
+
return {
|
|
1465
|
+
points: [
|
|
1466
|
+
pointOnArrow(0, 0),
|
|
1467
|
+
pointOnArrow(shaftLength, -shaftWidth / 2),
|
|
1468
|
+
pointOnArrow(shaftLength, -headHalfWidth),
|
|
1469
|
+
{
|
|
1470
|
+
x: end.x,
|
|
1471
|
+
y: end.y
|
|
1472
|
+
},
|
|
1473
|
+
pointOnArrow(shaftLength, headHalfWidth),
|
|
1474
|
+
pointOnArrow(shaftLength, shaftWidth / 2)
|
|
1475
|
+
],
|
|
1476
|
+
strokeWidth: Math.min(4, length * .02)
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
//#endregion
|
|
1460
1480
|
//#region src/feedback/core/annotation-renderer.ts
|
|
1461
1481
|
const STROKE_WIDTH = 3;
|
|
1462
|
-
const ARROW_HEAD_SIZE = 16;
|
|
1463
1482
|
const HIGHLIGHT_ALPHA = .3;
|
|
1464
1483
|
const REDACTION_FILL = "#111827";
|
|
1465
1484
|
const PIXEL_BLOCK = 10;
|
|
@@ -1480,10 +1499,6 @@ function scaleRect(rect, scale) {
|
|
|
1480
1499
|
function isCanvasElement(value) {
|
|
1481
1500
|
return typeof HTMLCanvasElement !== "undefined" && value instanceof HTMLCanvasElement;
|
|
1482
1501
|
}
|
|
1483
|
-
/**
|
|
1484
|
-
* Downsamples the region and paints it back enlarged, so redacted pixels are
|
|
1485
|
-
* unrecoverable. Falls back to an opaque block wherever 2d canvases are absent.
|
|
1486
|
-
*/
|
|
1487
1502
|
function redact(context, rect) {
|
|
1488
1503
|
const source = context.canvas;
|
|
1489
1504
|
const width = Math.max(1, Math.round(rect.width / PIXEL_BLOCK));
|
|
@@ -1503,18 +1518,17 @@ function redact(context, rect) {
|
|
|
1503
1518
|
context.fillStyle = REDACTION_FILL;
|
|
1504
1519
|
context.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
1505
1520
|
}
|
|
1506
|
-
function drawArrow(context,
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
context.stroke();
|
|
1511
|
-
const [left, right] = arrowHead(start, end, ARROW_HEAD_SIZE * scale);
|
|
1521
|
+
function drawArrow(context, annotation, scale) {
|
|
1522
|
+
const { points, strokeWidth } = arrowOutline(annotation.start, annotation.end);
|
|
1523
|
+
const [first, ...rest] = points.map((point) => scalePoint(point, scale));
|
|
1524
|
+
if (!first) return;
|
|
1512
1525
|
context.beginPath();
|
|
1513
|
-
context.moveTo(
|
|
1514
|
-
context.lineTo(
|
|
1515
|
-
context.lineTo(right.x, right.y);
|
|
1526
|
+
context.moveTo(first.x, first.y);
|
|
1527
|
+
for (const point of rest) context.lineTo(point.x, point.y);
|
|
1516
1528
|
context.closePath();
|
|
1529
|
+
context.lineWidth = strokeWidth * scale;
|
|
1517
1530
|
context.fill();
|
|
1531
|
+
context.stroke();
|
|
1518
1532
|
}
|
|
1519
1533
|
function drawPen(context, points, scale) {
|
|
1520
1534
|
const [first, ...rest] = points;
|
|
@@ -1562,7 +1576,7 @@ function drawAnnotation(context, annotation, scale) {
|
|
|
1562
1576
|
drawPen(context, annotation.points ?? [], scale);
|
|
1563
1577
|
break;
|
|
1564
1578
|
case "arrow":
|
|
1565
|
-
drawArrow(context,
|
|
1579
|
+
drawArrow(context, annotation, scale);
|
|
1566
1580
|
break;
|
|
1567
1581
|
case "spotlight":
|
|
1568
1582
|
drawSpotlight(context, rect);
|
|
@@ -1590,10 +1604,6 @@ function loadImage(url) {
|
|
|
1590
1604
|
image.src = url;
|
|
1591
1605
|
});
|
|
1592
1606
|
}
|
|
1593
|
-
/**
|
|
1594
|
-
* Burns the annotations into a copy of the capture so reviewers and agents see
|
|
1595
|
-
* the drawing without needing to replay the vector data.
|
|
1596
|
-
*/
|
|
1597
1607
|
async function renderAnnotatedImage(source, annotations) {
|
|
1598
1608
|
if (annotations.length === 0) return null;
|
|
1599
1609
|
const canvas = document.createElement("canvas");
|
|
@@ -1725,7 +1735,7 @@ function toImagePoint(canvas, clientX, clientY) {
|
|
|
1725
1735
|
y: Math.max(0, Math.min(canvas.height, (clientY - bounds.top) / bounds.height * canvas.height))
|
|
1726
1736
|
};
|
|
1727
1737
|
}
|
|
1728
|
-
function Annotator({ capture, labels, editor }) {
|
|
1738
|
+
function Annotator({ capture, labels, editor, note }) {
|
|
1729
1739
|
const { annotations, onChange, onDone, onRetake } = editor;
|
|
1730
1740
|
const morph = useImageMorph(editor.trigger, onDone);
|
|
1731
1741
|
const dialogRef = useRef(null);
|
|
@@ -1845,6 +1855,16 @@ function Annotator({ capture, labels, editor }) {
|
|
|
1845
1855
|
onRetake: onRetake ? () => morph.close(onRetake) : void 0
|
|
1846
1856
|
},
|
|
1847
1857
|
labels
|
|
1858
|
+
}),
|
|
1859
|
+
note && /* @__PURE__ */ jsxs("div", {
|
|
1860
|
+
className: "element-note glass",
|
|
1861
|
+
children: [/* @__PURE__ */ jsx(TargetIcon, {}), /* @__PURE__ */ jsx("textarea", {
|
|
1862
|
+
"aria-label": labels.elementNote,
|
|
1863
|
+
maxLength: 2e3,
|
|
1864
|
+
onChange: (event) => note.onChange(event.target.value),
|
|
1865
|
+
placeholder: labels.elementNotePlaceholder,
|
|
1866
|
+
value: note.comment
|
|
1867
|
+
})]
|
|
1848
1868
|
})
|
|
1849
1869
|
]
|
|
1850
1870
|
});
|
|
@@ -1866,6 +1886,156 @@ function Launcher({ isOpen, label, onClick, buttonRef }) {
|
|
|
1866
1886
|
});
|
|
1867
1887
|
}
|
|
1868
1888
|
//#endregion
|
|
1889
|
+
//#region src/feedback/ui/composer/capture-slot.tsx
|
|
1890
|
+
function CaptureSlot({ labels, state }) {
|
|
1891
|
+
const disabled = state.isCapturing || state.isEditingDisabled;
|
|
1892
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1893
|
+
className: "capture-slot glass",
|
|
1894
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
1895
|
+
"aria-label": labels.attachScreenshot,
|
|
1896
|
+
className: "capture-action",
|
|
1897
|
+
disabled,
|
|
1898
|
+
onClick: state.takeCapture,
|
|
1899
|
+
title: labels.attachScreenshot,
|
|
1900
|
+
type: "button",
|
|
1901
|
+
children: /* @__PURE__ */ jsx(CameraIcon, {})
|
|
1902
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1903
|
+
"aria-label": labels.pickElement,
|
|
1904
|
+
className: "capture-action",
|
|
1905
|
+
disabled,
|
|
1906
|
+
onClick: () => state.setStep("picking"),
|
|
1907
|
+
title: labels.pickElement,
|
|
1908
|
+
type: "button",
|
|
1909
|
+
children: /* @__PURE__ */ jsx(TargetIcon, {})
|
|
1910
|
+
})]
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
//#endregion
|
|
1914
|
+
//#region src/feedback/core/element-capture.ts
|
|
1915
|
+
const MIN_CONTEXT_PADDING = 24;
|
|
1916
|
+
const MAX_CONTEXT_WIDTH = 2e3;
|
|
1917
|
+
const CLOSE_UP_MAX_WIDTH = 1200;
|
|
1918
|
+
const SELECTION_COLOR = "#4f46e5";
|
|
1919
|
+
const SELECTION_ANNOTATION_PREFIX = "selection-";
|
|
1920
|
+
const OUTLINE_COLOR = "#4f46e5";
|
|
1921
|
+
const OUTLINE_GLOW = "#ffffff";
|
|
1922
|
+
const OUTLINE_WIDTH = 3;
|
|
1923
|
+
const GLOW_WIDTH = 6;
|
|
1924
|
+
const DIM_OVERLAY = "rgba(15, 23, 42, 0.45)";
|
|
1925
|
+
/**
|
|
1926
|
+
* Tightest ancestor that keeps some breathing room around the pick — the zoom
|
|
1927
|
+
* stays zoomed instead of growing to a giant section. Only when nothing up
|
|
1928
|
+
* the tree clears the minimum does the widest candidate below the cap win.
|
|
1929
|
+
*/
|
|
1930
|
+
function contextRootFor(element) {
|
|
1931
|
+
const target = element.getBoundingClientRect();
|
|
1932
|
+
const coversAtLeast = (box, margin) => box.left <= target.left - margin && box.right >= target.right + margin && box.top <= target.top - margin && box.bottom >= target.bottom + margin;
|
|
1933
|
+
let tightest = null;
|
|
1934
|
+
let current = element.parentElement;
|
|
1935
|
+
while (current && current !== document.documentElement) {
|
|
1936
|
+
const box = current.getBoundingClientRect();
|
|
1937
|
+
if (coversAtLeast(box, MIN_CONTEXT_PADDING)) return current;
|
|
1938
|
+
tightest ??= box.width < MAX_CONTEXT_WIDTH ? current : null;
|
|
1939
|
+
current = current.parentElement;
|
|
1940
|
+
}
|
|
1941
|
+
return tightest ?? element;
|
|
1942
|
+
}
|
|
1943
|
+
/** Where the picked element sits inside the context shot, in image pixels. */
|
|
1944
|
+
function closeUpGeometry(params) {
|
|
1945
|
+
const scale = params.rootRect.width > 0 ? params.capture.width / params.rootRect.width : 1;
|
|
1946
|
+
return {
|
|
1947
|
+
height: Math.round(params.elementRect.height * scale),
|
|
1948
|
+
scale,
|
|
1949
|
+
width: Math.round(params.elementRect.width * scale),
|
|
1950
|
+
x: Math.round((params.elementRect.left - params.rootRect.left) * scale),
|
|
1951
|
+
y: Math.round((params.elementRect.top - params.rootRect.top) * scale)
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
function isSelectionAnnotation({ id }) {
|
|
1955
|
+
return id.startsWith(SELECTION_ANNOTATION_PREFIX);
|
|
1956
|
+
}
|
|
1957
|
+
function highlightFor(selection, capture) {
|
|
1958
|
+
const scale = capture.width / window.innerWidth;
|
|
1959
|
+
const { rect } = selection;
|
|
1960
|
+
return {
|
|
1961
|
+
color: SELECTION_COLOR,
|
|
1962
|
+
end: {
|
|
1963
|
+
x: (rect.x + rect.width) * scale,
|
|
1964
|
+
y: (rect.y + rect.height) * scale
|
|
1965
|
+
},
|
|
1966
|
+
id: `${SELECTION_ANNOTATION_PREFIX}${rect.x}-${rect.y}`,
|
|
1967
|
+
start: {
|
|
1968
|
+
x: rect.x * scale,
|
|
1969
|
+
y: rect.y * scale
|
|
1970
|
+
},
|
|
1971
|
+
tool: "rectangle"
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
function drawZoneOverlay(context, zone) {
|
|
1975
|
+
context.save();
|
|
1976
|
+
context.fillStyle = DIM_OVERLAY;
|
|
1977
|
+
context.beginPath();
|
|
1978
|
+
context.rect(0, 0, context.canvas.width, context.canvas.height);
|
|
1979
|
+
context.rect(zone.x, zone.y, zone.width, zone.height);
|
|
1980
|
+
context.fill("evenodd");
|
|
1981
|
+
for (const [color, width] of [[OUTLINE_GLOW, GLOW_WIDTH], [OUTLINE_COLOR, OUTLINE_WIDTH]]) {
|
|
1982
|
+
context.strokeStyle = color;
|
|
1983
|
+
context.lineWidth = width * zone.scale;
|
|
1984
|
+
context.strokeRect(zone.x, zone.y, zone.width, zone.height);
|
|
1985
|
+
}
|
|
1986
|
+
context.restore();
|
|
1987
|
+
}
|
|
1988
|
+
function drawCloseUp(capture, zone) {
|
|
1989
|
+
return new Promise((resolve) => {
|
|
1990
|
+
const canvas = document.createElement("canvas");
|
|
1991
|
+
canvas.width = capture.width;
|
|
1992
|
+
canvas.height = capture.height;
|
|
1993
|
+
const context = canvas.getContext("2d");
|
|
1994
|
+
const image = new Image();
|
|
1995
|
+
image.onload = () => {
|
|
1996
|
+
if (!context) {
|
|
1997
|
+
resolve(null);
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
context.drawImage(image, 0, 0, capture.width, capture.height);
|
|
2001
|
+
drawZoneOverlay(context, zone);
|
|
2002
|
+
resolve(capturedFromCanvas(canvas, CLOSE_UP_MAX_WIDTH));
|
|
2003
|
+
};
|
|
2004
|
+
image.onerror = () => resolve(null);
|
|
2005
|
+
image.src = capture.objectUrl;
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* Zoomed view of the picked zone: the surrounding area captured for context,
|
|
2010
|
+
* everything outside the selection dimmed and the zone framed with a glowing
|
|
2011
|
+
* outline — unambiguous to eyes and vision models alike.
|
|
2012
|
+
* Falls back to the plain context shot when the overlay cannot be drawn.
|
|
2013
|
+
*/
|
|
2014
|
+
async function captureElementCloseUp(element) {
|
|
2015
|
+
try {
|
|
2016
|
+
const root = contextRootFor(element);
|
|
2017
|
+
const rootRect = root.getBoundingClientRect();
|
|
2018
|
+
const elementRect = element.getBoundingClientRect();
|
|
2019
|
+
const capture = await captureViewport({
|
|
2020
|
+
element: root,
|
|
2021
|
+
maxWidth: CLOSE_UP_MAX_WIDTH
|
|
2022
|
+
});
|
|
2023
|
+
if (!capture || root === element) return capture;
|
|
2024
|
+
const outlined = await drawCloseUp(capture, closeUpGeometry({
|
|
2025
|
+
capture,
|
|
2026
|
+
elementRect,
|
|
2027
|
+
rootRect
|
|
2028
|
+
}));
|
|
2029
|
+
if (outlined) {
|
|
2030
|
+
releaseCapture(capture);
|
|
2031
|
+
return outlined;
|
|
2032
|
+
}
|
|
2033
|
+
return capture;
|
|
2034
|
+
} catch {
|
|
2035
|
+
return null;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
//#endregion
|
|
1869
2039
|
//#region src/feedback/ui/annotation/screenshot-preview.tsx
|
|
1870
2040
|
function ScreenshotPreview({ annotations, capture }) {
|
|
1871
2041
|
const { canvasRef } = useAnnotationCanvas(capture, annotations);
|
|
@@ -1884,95 +2054,66 @@ function ScreenshotPreview({ annotations, capture }) {
|
|
|
1884
2054
|
//#endregion
|
|
1885
2055
|
//#region src/feedback/ui/composer/screenshot-card.tsx
|
|
1886
2056
|
function ScreenshotCard({ item, labels, state }) {
|
|
1887
|
-
const { draft, number
|
|
2057
|
+
const { draft, number } = item;
|
|
2058
|
+
const previewRef = useRef(null);
|
|
1888
2059
|
const isCapturing = state.pendingCapture?.id === draft.id;
|
|
1889
2060
|
const disabled = state.isEditingDisabled || state.isCapturing;
|
|
2061
|
+
const drawings = draft.annotations.filter((annotation) => !isSelectionAnnotation(annotation));
|
|
1890
2062
|
return /* @__PURE__ */ jsxs("fieldset", {
|
|
1891
2063
|
"aria-label": `${labels.screenshot} ${number}`,
|
|
1892
|
-
className: "screenshot-attachment
|
|
1893
|
-
children: [
|
|
1894
|
-
"
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
className: "
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
})
|
|
1940
|
-
|
|
2064
|
+
className: "screenshot-attachment",
|
|
2065
|
+
children: [
|
|
2066
|
+
/* @__PURE__ */ jsx("div", {
|
|
2067
|
+
className: "screenshot-preview glass",
|
|
2068
|
+
ref: previewRef,
|
|
2069
|
+
children: /* @__PURE__ */ jsx(ScreenshotPreview, {
|
|
2070
|
+
annotations: draft.annotations,
|
|
2071
|
+
capture: draft.image
|
|
2072
|
+
})
|
|
2073
|
+
}),
|
|
2074
|
+
draft.selection && /* @__PURE__ */ jsx("span", {
|
|
2075
|
+
className: "selection-badge glass",
|
|
2076
|
+
title: draft.selection.comment ?? draft.selection.label,
|
|
2077
|
+
children: /* @__PURE__ */ jsx(TargetIcon, {})
|
|
2078
|
+
}),
|
|
2079
|
+
drawings.length > 0 && /* @__PURE__ */ jsx("span", {
|
|
2080
|
+
className: "annotation-count glass",
|
|
2081
|
+
children: drawings.length
|
|
2082
|
+
}),
|
|
2083
|
+
isCapturing ? /* @__PURE__ */ jsxs("div", {
|
|
2084
|
+
className: "capture-progress glass",
|
|
2085
|
+
role: "status",
|
|
2086
|
+
children: [/* @__PURE__ */ jsx("span", { className: "spinner" }), labels.capturing]
|
|
2087
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
2088
|
+
className: "attachment-actions glass",
|
|
2089
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
2090
|
+
"aria-label": labels.annotateHint,
|
|
2091
|
+
className: "icon-btn annotate-action",
|
|
2092
|
+
disabled,
|
|
2093
|
+
onClick: (event) => {
|
|
2094
|
+
if (previewRef.current) state.annotateScreenshot(draft.id, {
|
|
2095
|
+
button: event.currentTarget,
|
|
2096
|
+
thumbnail: previewRef.current
|
|
2097
|
+
});
|
|
2098
|
+
},
|
|
2099
|
+
title: labels.annotateHint,
|
|
2100
|
+
type: "button",
|
|
2101
|
+
children: /* @__PURE__ */ jsx(PencilIcon, {})
|
|
2102
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
2103
|
+
"aria-label": labels.removeScreenshot,
|
|
2104
|
+
className: "icon-btn",
|
|
2105
|
+
disabled,
|
|
2106
|
+
onClick: () => state.removeCapture(draft.id),
|
|
2107
|
+
title: labels.removeScreenshot,
|
|
2108
|
+
type: "button",
|
|
2109
|
+
children: /* @__PURE__ */ jsx(TrashIcon, {})
|
|
2110
|
+
})]
|
|
2111
|
+
})
|
|
2112
|
+
]
|
|
1941
2113
|
});
|
|
1942
2114
|
}
|
|
1943
2115
|
//#endregion
|
|
1944
2116
|
//#region src/feedback/ui/composer/attachments.tsx
|
|
1945
|
-
function SelectedElement({ labels, state }) {
|
|
1946
|
-
if (!state.selection) return null;
|
|
1947
|
-
const component = state.selection.componentStack[0];
|
|
1948
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1949
|
-
className: "selection-chip glass",
|
|
1950
|
-
children: [/* @__PURE__ */ jsxs("button", {
|
|
1951
|
-
"aria-label": labels.pickElement,
|
|
1952
|
-
className: "selection-label",
|
|
1953
|
-
disabled: state.isEditingDisabled || state.isCapturing,
|
|
1954
|
-
onClick: () => state.setStep("picking"),
|
|
1955
|
-
title: state.selection.sourceLocation ?? state.selection.selector,
|
|
1956
|
-
type: "button",
|
|
1957
|
-
children: [state.elementCapture ? /* @__PURE__ */ jsx("img", {
|
|
1958
|
-
alt: "",
|
|
1959
|
-
height: 32,
|
|
1960
|
-
src: state.elementCapture.objectUrl,
|
|
1961
|
-
width: 32
|
|
1962
|
-
}) : /* @__PURE__ */ jsx(TargetIcon, {}), /* @__PURE__ */ jsx("span", {
|
|
1963
|
-
className: "truncate",
|
|
1964
|
-
children: component ? `<${component}>` : state.selection.label
|
|
1965
|
-
})]
|
|
1966
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
1967
|
-
"aria-label": labels.clearSelection,
|
|
1968
|
-
className: "icon-btn",
|
|
1969
|
-
disabled: state.isEditingDisabled || state.isCapturing,
|
|
1970
|
-
onClick: state.clearSelection,
|
|
1971
|
-
type: "button",
|
|
1972
|
-
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
1973
|
-
})]
|
|
1974
|
-
});
|
|
1975
|
-
}
|
|
1976
2117
|
function Attachments({ labels, state }) {
|
|
1977
2118
|
const stripRef = useRef(null);
|
|
1978
2119
|
const count = state.screenshots.length;
|
|
@@ -1981,41 +2122,102 @@ function Attachments({ labels, state }) {
|
|
|
1981
2122
|
if (strip && count > 0) strip.scrollTo?.({ left: strip.scrollWidth });
|
|
1982
2123
|
}, [count]);
|
|
1983
2124
|
const isAdding = state.pendingCapture && !state.screenshots.some(({ id }) => id === state.pendingCapture?.id);
|
|
1984
|
-
return /* @__PURE__ */
|
|
2125
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1985
2126
|
className: "attachments",
|
|
1986
|
-
children:
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
"
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
children:
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
"
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2127
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
2128
|
+
className: "attachment-row",
|
|
2129
|
+
children: [/* @__PURE__ */ jsx(CaptureSlot, {
|
|
2130
|
+
labels,
|
|
2131
|
+
state
|
|
2132
|
+
}), /* @__PURE__ */ jsxs(ScrollArea.Root, {
|
|
2133
|
+
className: "screenshot-scroll-area",
|
|
2134
|
+
children: [/* @__PURE__ */ jsx(ScrollArea.Viewport, {
|
|
2135
|
+
"aria-label": labels.screenshot,
|
|
2136
|
+
className: "screenshot-strip",
|
|
2137
|
+
ref: stripRef,
|
|
2138
|
+
role: "region",
|
|
2139
|
+
children: /* @__PURE__ */ jsxs(ScrollArea.Content, {
|
|
2140
|
+
className: "screenshot-strip-content",
|
|
2141
|
+
children: [state.screenshots.map((draft, index) => /* @__PURE__ */ jsx(ScreenshotCard, {
|
|
2142
|
+
item: {
|
|
2143
|
+
draft,
|
|
2144
|
+
number: index + 1
|
|
2145
|
+
},
|
|
2146
|
+
labels,
|
|
2147
|
+
state
|
|
2148
|
+
}, draft.id)), isAdding && /* @__PURE__ */ jsxs("div", {
|
|
2149
|
+
"aria-label": labels.capturing,
|
|
2150
|
+
className: "attachment-skeleton glass",
|
|
2151
|
+
role: "status",
|
|
2152
|
+
children: [/* @__PURE__ */ jsx(CameraIcon, {}), /* @__PURE__ */ jsx("span", { children: labels.capturing })]
|
|
2153
|
+
})]
|
|
2154
|
+
})
|
|
2155
|
+
}), /* @__PURE__ */ jsx(ScrollArea.Scrollbar, {
|
|
2156
|
+
className: "screenshot-scrollbar",
|
|
2157
|
+
orientation: "horizontal",
|
|
2158
|
+
children: /* @__PURE__ */ jsx(ScrollArea.Thumb, { className: "screenshot-scroll-thumb" })
|
|
2159
|
+
})]
|
|
2017
2160
|
})]
|
|
2018
|
-
})
|
|
2161
|
+
})
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
//#endregion
|
|
2165
|
+
//#region src/feedback/ui/composer/email-prompt.tsx
|
|
2166
|
+
function EmailPrompt({ labels, state }) {
|
|
2167
|
+
const inputRef = useRef(null);
|
|
2168
|
+
const [invalid, setInvalid] = useState(false);
|
|
2169
|
+
useEffect(() => {
|
|
2170
|
+
inputRef.current?.focus();
|
|
2171
|
+
}, []);
|
|
2172
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
2173
|
+
className: "email-prompt glass",
|
|
2174
|
+
onSubmit: (event) => {
|
|
2175
|
+
event.preventDefault();
|
|
2176
|
+
state.submit();
|
|
2177
|
+
},
|
|
2178
|
+
children: [
|
|
2179
|
+
/* @__PURE__ */ jsx("h2", { children: labels.emailPromptTitle }),
|
|
2180
|
+
/* @__PURE__ */ jsx("p", { children: labels.emailPromptBody }),
|
|
2181
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2182
|
+
className: "email-field",
|
|
2183
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
2184
|
+
"aria-invalid": invalid,
|
|
2185
|
+
"aria-label": labels.emailLabel,
|
|
2186
|
+
autoComplete: "email",
|
|
2187
|
+
onChange: (event) => {
|
|
2188
|
+
setInvalid(false);
|
|
2189
|
+
state.setEmail(event.target.value);
|
|
2190
|
+
},
|
|
2191
|
+
onInvalid: (event) => {
|
|
2192
|
+
event.preventDefault();
|
|
2193
|
+
setInvalid(true);
|
|
2194
|
+
},
|
|
2195
|
+
placeholder: labels.emailPlaceholder,
|
|
2196
|
+
ref: inputRef,
|
|
2197
|
+
type: "email",
|
|
2198
|
+
value: state.email
|
|
2199
|
+
}), invalid && /* @__PURE__ */ jsx("span", {
|
|
2200
|
+
role: "alert",
|
|
2201
|
+
children: labels.emailInvalid
|
|
2202
|
+
})]
|
|
2203
|
+
}),
|
|
2204
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2205
|
+
className: "email-prompt-actions",
|
|
2206
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
2207
|
+
className: "ghost-btn",
|
|
2208
|
+
onClick: () => {
|
|
2209
|
+
state.setEmail("");
|
|
2210
|
+
state.submit();
|
|
2211
|
+
},
|
|
2212
|
+
type: "button",
|
|
2213
|
+
children: labels.sendWithoutEmail
|
|
2214
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
2215
|
+
className: "primary-btn",
|
|
2216
|
+
type: "submit",
|
|
2217
|
+
children: labels.submit
|
|
2218
|
+
})]
|
|
2219
|
+
})
|
|
2220
|
+
]
|
|
2019
2221
|
});
|
|
2020
2222
|
}
|
|
2021
2223
|
//#endregion
|
|
@@ -2091,92 +2293,43 @@ function ComposerOptions({ labels, state }) {
|
|
|
2091
2293
|
});
|
|
2092
2294
|
}
|
|
2093
2295
|
//#endregion
|
|
2094
|
-
//#region src/feedback/ui/composer/
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
idea: "categoryIdea",
|
|
2098
|
-
question: "categoryQuestion"
|
|
2099
|
-
};
|
|
2100
|
-
function ComposerControls({ options, labels, state }) {
|
|
2296
|
+
//#region src/feedback/ui/composer/message-input.tsx
|
|
2297
|
+
function MessageInput({ floatingControls, labels, state }) {
|
|
2298
|
+
const submitLabel = state.hasPendingAttachments ? labels.retryAttachments : labels.submit;
|
|
2101
2299
|
return /* @__PURE__ */ jsxs("div", {
|
|
2102
|
-
className: "
|
|
2103
|
-
children: [
|
|
2104
|
-
|
|
2300
|
+
className: "message-row glass",
|
|
2301
|
+
children: [/* @__PURE__ */ jsx("textarea", {
|
|
2302
|
+
"aria-label": labels.descriptionPlaceholder,
|
|
2303
|
+
disabled: state.isEditingDisabled,
|
|
2304
|
+
maxLength: 4e3,
|
|
2305
|
+
onChange: (event) => state.setMessage(event.target.value),
|
|
2306
|
+
placeholder: labels.descriptionPlaceholder,
|
|
2307
|
+
rows: 3,
|
|
2308
|
+
value: state.message
|
|
2309
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2310
|
+
className: "message-actions",
|
|
2105
2311
|
children: [
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
className: "category-select",
|
|
2111
|
-
disabled: state.isEditingDisabled,
|
|
2112
|
-
onChange: (event) => {
|
|
2113
|
-
const category = options.categories.find((value) => value === event.target.value);
|
|
2114
|
-
if (category) state.setCategory(category);
|
|
2115
|
-
},
|
|
2116
|
-
value: state.category,
|
|
2117
|
-
children: options.categories.map((value) => /* @__PURE__ */ jsx("option", {
|
|
2118
|
-
value,
|
|
2119
|
-
children: labels[CATEGORY_LABELS[value]]
|
|
2120
|
-
}, value))
|
|
2121
|
-
}), /* @__PURE__ */ jsx(ChevronDownIcon, {})]
|
|
2122
|
-
}),
|
|
2123
|
-
/* @__PURE__ */ jsx("button", {
|
|
2124
|
-
"aria-label": labels.attachScreenshot,
|
|
2125
|
-
className: "icon-btn",
|
|
2126
|
-
disabled: state.isCapturing || state.isEditingDisabled,
|
|
2127
|
-
onClick: state.takeCapture,
|
|
2128
|
-
title: labels.attachScreenshot,
|
|
2129
|
-
type: "button",
|
|
2130
|
-
children: /* @__PURE__ */ jsx(CameraIcon, {})
|
|
2312
|
+
floatingControls,
|
|
2313
|
+
/* @__PURE__ */ jsx(ComposerOptions, {
|
|
2314
|
+
labels,
|
|
2315
|
+
state
|
|
2131
2316
|
}),
|
|
2132
2317
|
/* @__PURE__ */ jsx("button", {
|
|
2133
|
-
"aria-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
title:
|
|
2138
|
-
type: "
|
|
2139
|
-
children: /* @__PURE__ */ jsx(
|
|
2140
|
-
}),
|
|
2141
|
-
/* @__PURE__ */ jsx(ComposerOptions, {
|
|
2142
|
-
labels,
|
|
2143
|
-
state
|
|
2318
|
+
"aria-busy": state.isSubmitting,
|
|
2319
|
+
"aria-label": submitLabel,
|
|
2320
|
+
className: "submit",
|
|
2321
|
+
disabled: state.isSubmitting || state.isCapturing || !state.canSubmit,
|
|
2322
|
+
title: submitLabel,
|
|
2323
|
+
type: "submit",
|
|
2324
|
+
children: state.isSubmitting ? /* @__PURE__ */ jsx("span", { className: "spinner" }) : /* @__PURE__ */ jsx(ArrowIcon, {})
|
|
2144
2325
|
})
|
|
2145
2326
|
]
|
|
2146
2327
|
})]
|
|
2147
2328
|
});
|
|
2148
2329
|
}
|
|
2149
2330
|
//#endregion
|
|
2150
|
-
//#region src/feedback/ui/composer/message-input.tsx
|
|
2151
|
-
function MessageInput({ labels, state }) {
|
|
2152
|
-
const [expanded, setExpanded] = useState(false);
|
|
2153
|
-
const submitLabel = state.hasPendingAttachments ? labels.retryAttachments : labels.submit;
|
|
2154
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
2155
|
-
className: "message-row glass",
|
|
2156
|
-
"data-expanded": expanded,
|
|
2157
|
-
children: [/* @__PURE__ */ jsx("textarea", {
|
|
2158
|
-
"aria-label": labels.descriptionPlaceholder,
|
|
2159
|
-
disabled: state.isEditingDisabled,
|
|
2160
|
-
maxLength: 4e3,
|
|
2161
|
-
onChange: (event) => state.setMessage(event.target.value),
|
|
2162
|
-
onFocus: () => setExpanded(true),
|
|
2163
|
-
placeholder: labels.descriptionPlaceholder,
|
|
2164
|
-
rows: expanded ? 3 : 1,
|
|
2165
|
-
value: state.message
|
|
2166
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
2167
|
-
"aria-busy": state.isSubmitting,
|
|
2168
|
-
"aria-label": submitLabel,
|
|
2169
|
-
className: "submit",
|
|
2170
|
-
disabled: state.isSubmitting || state.isCapturing || !state.message.trim(),
|
|
2171
|
-
title: submitLabel,
|
|
2172
|
-
type: "submit",
|
|
2173
|
-
children: state.isSubmitting ? /* @__PURE__ */ jsx("span", { className: "spinner" }) : /* @__PURE__ */ jsx(ArrowIcon, {})
|
|
2174
|
-
})]
|
|
2175
|
-
});
|
|
2176
|
-
}
|
|
2177
|
-
//#endregion
|
|
2178
2331
|
//#region src/feedback/ui/panel.tsx
|
|
2179
|
-
function FeedbackPanel({
|
|
2332
|
+
function FeedbackPanel({ floatingControls, labels, state }) {
|
|
2180
2333
|
const panelRef = useRef(null);
|
|
2181
2334
|
useEffect(() => {
|
|
2182
2335
|
panelRef.current?.focus();
|
|
@@ -2193,9 +2346,29 @@ function FeedbackPanel({ options, labels, state }) {
|
|
|
2193
2346
|
]
|
|
2194
2347
|
}), /* @__PURE__ */ jsx("div", {
|
|
2195
2348
|
className: "composer-toolbar",
|
|
2196
|
-
children:
|
|
2349
|
+
children: floatingControls
|
|
2197
2350
|
})]
|
|
2198
2351
|
});
|
|
2352
|
+
if (state.step === "email") return /* @__PURE__ */ jsxs("section", {
|
|
2353
|
+
"aria-label": labels.emailPromptTitle,
|
|
2354
|
+
className: "panel",
|
|
2355
|
+
children: [
|
|
2356
|
+
/* @__PURE__ */ jsx("button", {
|
|
2357
|
+
"aria-label": labels.back,
|
|
2358
|
+
className: "email-scrim",
|
|
2359
|
+
onClick: () => state.setStep("compose"),
|
|
2360
|
+
type: "button"
|
|
2361
|
+
}),
|
|
2362
|
+
/* @__PURE__ */ jsx(EmailPrompt, {
|
|
2363
|
+
labels,
|
|
2364
|
+
state
|
|
2365
|
+
}),
|
|
2366
|
+
/* @__PURE__ */ jsx("div", {
|
|
2367
|
+
className: "composer-toolbar",
|
|
2368
|
+
children: floatingControls
|
|
2369
|
+
})
|
|
2370
|
+
]
|
|
2371
|
+
});
|
|
2199
2372
|
return /* @__PURE__ */ jsxs("section", {
|
|
2200
2373
|
"aria-label": labels.title,
|
|
2201
2374
|
className: "panel",
|
|
@@ -2208,18 +2381,14 @@ function FeedbackPanel({ options, labels, state }) {
|
|
|
2208
2381
|
className: "composer",
|
|
2209
2382
|
onSubmit: (event) => {
|
|
2210
2383
|
event.preventDefault();
|
|
2211
|
-
state.
|
|
2384
|
+
state.requestSubmit();
|
|
2212
2385
|
},
|
|
2213
2386
|
children: [
|
|
2214
2387
|
/* @__PURE__ */ jsx(MessageInput, {
|
|
2388
|
+
floatingControls,
|
|
2215
2389
|
labels,
|
|
2216
2390
|
state
|
|
2217
2391
|
}),
|
|
2218
|
-
/* @__PURE__ */ jsx(ComposerControls, {
|
|
2219
|
-
labels,
|
|
2220
|
-
options,
|
|
2221
|
-
state
|
|
2222
|
-
}),
|
|
2223
2392
|
/* @__PURE__ */ jsx("div", {
|
|
2224
2393
|
"aria-hidden": "true",
|
|
2225
2394
|
className: "hp",
|
|
@@ -2241,8 +2410,86 @@ function FeedbackPanel({ options, labels, state }) {
|
|
|
2241
2410
|
});
|
|
2242
2411
|
}
|
|
2243
2412
|
//#endregion
|
|
2413
|
+
//#region src/feedback/ui/selection-outline.tsx
|
|
2414
|
+
/**
|
|
2415
|
+
* Keeps the picked element ringed while the reporter writes about it, so the
|
|
2416
|
+
* composer parked next to it is visibly about that element.
|
|
2417
|
+
*/
|
|
2418
|
+
function SelectionOutline({ node }) {
|
|
2419
|
+
const boxRef = useRef(null);
|
|
2420
|
+
useEffect(() => {
|
|
2421
|
+
const box = boxRef.current;
|
|
2422
|
+
if (!(box && node)) return;
|
|
2423
|
+
let frame = 0;
|
|
2424
|
+
const draw = () => {
|
|
2425
|
+
frame = 0;
|
|
2426
|
+
const rect = node.getBoundingClientRect();
|
|
2427
|
+
const hidden = rect.width === 0 || rect.height === 0 || rect.bottom < 0 || rect.top > window.innerHeight;
|
|
2428
|
+
box.style.opacity = hidden ? "0" : "1";
|
|
2429
|
+
box.style.width = `${rect.width}px`;
|
|
2430
|
+
box.style.height = `${rect.height}px`;
|
|
2431
|
+
box.style.transform = `translate(${rect.left}px, ${rect.top}px)`;
|
|
2432
|
+
};
|
|
2433
|
+
const schedule = () => {
|
|
2434
|
+
frame ||= requestAnimationFrame(draw);
|
|
2435
|
+
};
|
|
2436
|
+
draw();
|
|
2437
|
+
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(draw);
|
|
2438
|
+
observer?.observe(node);
|
|
2439
|
+
window.addEventListener("scroll", schedule, {
|
|
2440
|
+
capture: true,
|
|
2441
|
+
passive: true
|
|
2442
|
+
});
|
|
2443
|
+
window.addEventListener("resize", schedule);
|
|
2444
|
+
return () => {
|
|
2445
|
+
cancelAnimationFrame(frame);
|
|
2446
|
+
observer?.disconnect();
|
|
2447
|
+
window.removeEventListener("scroll", schedule, true);
|
|
2448
|
+
window.removeEventListener("resize", schedule);
|
|
2449
|
+
};
|
|
2450
|
+
}, [node]);
|
|
2451
|
+
if (!node) return null;
|
|
2452
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2453
|
+
"aria-hidden": "true",
|
|
2454
|
+
className: "selection-outline",
|
|
2455
|
+
ref: boxRef
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
//#endregion
|
|
2459
|
+
//#region src/feedback/ui/visible-viewport.ts
|
|
2460
|
+
/**
|
|
2461
|
+
* The part of the page the reporter can actually see, in fixed-position
|
|
2462
|
+
* coordinates. On mobile the keyboard shrinks this without touching
|
|
2463
|
+
* `window.innerHeight`, so anything that must stay reachable measures here.
|
|
2464
|
+
*/
|
|
2465
|
+
function visibleViewport() {
|
|
2466
|
+
const viewport = window.visualViewport;
|
|
2467
|
+
const left = viewport?.offsetLeft ?? 0;
|
|
2468
|
+
const top = viewport?.offsetTop ?? 0;
|
|
2469
|
+
const width = viewport?.width ?? window.innerWidth;
|
|
2470
|
+
const height = viewport?.height ?? window.innerHeight;
|
|
2471
|
+
return {
|
|
2472
|
+
bottom: window.innerHeight - top - height,
|
|
2473
|
+
height,
|
|
2474
|
+
left,
|
|
2475
|
+
right: window.innerWidth - left - width,
|
|
2476
|
+
top,
|
|
2477
|
+
width
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
function onViewportChange(update) {
|
|
2481
|
+
window.addEventListener("resize", update);
|
|
2482
|
+
window.visualViewport?.addEventListener("resize", update);
|
|
2483
|
+
window.visualViewport?.addEventListener("scroll", update);
|
|
2484
|
+
return () => {
|
|
2485
|
+
window.removeEventListener("resize", update);
|
|
2486
|
+
window.visualViewport?.removeEventListener("resize", update);
|
|
2487
|
+
window.visualViewport?.removeEventListener("scroll", update);
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
//#endregion
|
|
2244
2491
|
//#region src/feedback/ui/floating/use-floating-position.ts
|
|
2245
|
-
const EDGE_GAP = 12;
|
|
2492
|
+
const EDGE_GAP$1 = 12;
|
|
2246
2493
|
const KEYBOARD_STEP = 24;
|
|
2247
2494
|
const KEY_DIRECTIONS = {
|
|
2248
2495
|
ArrowDown: {
|
|
@@ -2262,34 +2509,24 @@ const KEY_DIRECTIONS = {
|
|
|
2262
2509
|
y: -24
|
|
2263
2510
|
}
|
|
2264
2511
|
};
|
|
2265
|
-
function visibleViewport() {
|
|
2266
|
-
const viewport = window.visualViewport;
|
|
2267
|
-
const left = viewport?.offsetLeft ?? 0;
|
|
2268
|
-
const top = viewport?.offsetTop ?? 0;
|
|
2269
|
-
const width = viewport?.width ?? window.innerWidth;
|
|
2270
|
-
const height = viewport?.height ?? window.innerHeight;
|
|
2271
|
-
return {
|
|
2272
|
-
bottom: window.innerHeight - top - height,
|
|
2273
|
-
height,
|
|
2274
|
-
left,
|
|
2275
|
-
right: window.innerWidth - left - width,
|
|
2276
|
-
top,
|
|
2277
|
-
width
|
|
2278
|
-
};
|
|
2279
|
-
}
|
|
2280
2512
|
function clampPosition(point, bounds, viewport = visibleViewport()) {
|
|
2281
2513
|
return {
|
|
2282
|
-
x: Math.max(viewport.left + EDGE_GAP, Math.min(point.x, viewport.left + viewport.width - bounds.width - EDGE_GAP)),
|
|
2283
|
-
y: Math.max(viewport.top + EDGE_GAP, Math.min(point.y, viewport.top + viewport.height - bounds.height - EDGE_GAP))
|
|
2514
|
+
x: Math.max(viewport.left + EDGE_GAP$1, Math.min(point.x, viewport.left + viewport.width - bounds.width - EDGE_GAP$1)),
|
|
2515
|
+
y: Math.max(viewport.top + EDGE_GAP$1, Math.min(point.y, viewport.top + viewport.height - bounds.height - EDGE_GAP$1))
|
|
2284
2516
|
};
|
|
2285
2517
|
}
|
|
2286
|
-
function useFloatingPosition(corner = "bottom-right") {
|
|
2518
|
+
function useFloatingPosition(corner = "bottom-right", isPanelOpen = false) {
|
|
2287
2519
|
const rootRef = useRef(null);
|
|
2288
2520
|
const gesture = useRef(null);
|
|
2289
2521
|
const [position, setPosition] = useState(null);
|
|
2522
|
+
const [positionedPanelOpen, setPositionedPanelOpen] = useState(isPanelOpen);
|
|
2290
2523
|
const [viewport, setViewport] = useState(visibleViewport);
|
|
2291
2524
|
const horizontalEdge = corner.endsWith("right") ? "right" : "left";
|
|
2292
2525
|
const verticalEdge = corner.startsWith("bottom") ? "bottom" : "top";
|
|
2526
|
+
if (positionedPanelOpen !== isPanelOpen) {
|
|
2527
|
+
setPositionedPanelOpen(isPanelOpen);
|
|
2528
|
+
setPosition(null);
|
|
2529
|
+
}
|
|
2293
2530
|
const moveTo = useCallback((point, bounds) => {
|
|
2294
2531
|
const fitted = clampPosition(point, bounds);
|
|
2295
2532
|
const visible = visibleViewport();
|
|
@@ -2309,17 +2546,12 @@ function useFloatingPosition(corner = "bottom-right") {
|
|
|
2309
2546
|
keepInViewport();
|
|
2310
2547
|
}, [keepInViewport]);
|
|
2311
2548
|
useEffect(() => {
|
|
2312
|
-
const resizeViewport = () => setViewport(visibleViewport());
|
|
2313
2549
|
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(keepInViewport);
|
|
2314
2550
|
if (rootRef.current) observer?.observe(rootRef.current);
|
|
2315
|
-
|
|
2316
|
-
window.visualViewport?.addEventListener("resize", resizeViewport);
|
|
2317
|
-
window.visualViewport?.addEventListener("scroll", resizeViewport);
|
|
2551
|
+
const stopListening = onViewportChange(() => setViewport(visibleViewport()));
|
|
2318
2552
|
return () => {
|
|
2319
2553
|
observer?.disconnect();
|
|
2320
|
-
|
|
2321
|
-
window.visualViewport?.removeEventListener("resize", resizeViewport);
|
|
2322
|
-
window.visualViewport?.removeEventListener("scroll", resizeViewport);
|
|
2554
|
+
stopListening();
|
|
2323
2555
|
};
|
|
2324
2556
|
}, [keepInViewport]);
|
|
2325
2557
|
const onPointerDown = (event) => {
|
|
@@ -2333,9 +2565,11 @@ function useFloatingPosition(corner = "bottom-right") {
|
|
|
2333
2565
|
};
|
|
2334
2566
|
const onPointerMove = (event) => {
|
|
2335
2567
|
const bounds = rootRef.current?.getBoundingClientRect();
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2568
|
+
const grab = gesture.current;
|
|
2569
|
+
if (!(bounds && grab && event.currentTarget.hasPointerCapture(event.pointerId))) return;
|
|
2570
|
+
moveTo({
|
|
2571
|
+
x: event.clientX - grab.x,
|
|
2572
|
+
y: event.clientY - grab.y
|
|
2339
2573
|
}, bounds);
|
|
2340
2574
|
};
|
|
2341
2575
|
const onKeyDown = (event) => {
|
|
@@ -2353,6 +2587,9 @@ function useFloatingPosition(corner = "bottom-right") {
|
|
|
2353
2587
|
return {
|
|
2354
2588
|
handleProps: {
|
|
2355
2589
|
onKeyDown,
|
|
2590
|
+
onLostPointerCapture: () => {
|
|
2591
|
+
gesture.current = null;
|
|
2592
|
+
},
|
|
2356
2593
|
onPointerCancel: () => {
|
|
2357
2594
|
gesture.current = null;
|
|
2358
2595
|
},
|
|
@@ -2369,17 +2606,17 @@ function useFloatingPosition(corner = "bottom-right") {
|
|
|
2369
2606
|
}
|
|
2370
2607
|
//#endregion
|
|
2371
2608
|
//#region src/feedback/ui/floating/floating-widget.tsx
|
|
2372
|
-
function FloatingWidget({ labels,
|
|
2609
|
+
function FloatingWidget({ labels, position, state }) {
|
|
2373
2610
|
const [minimized, setMinimized] = useState(false);
|
|
2374
2611
|
const launcherRef = useRef(null);
|
|
2375
|
-
const floating = useFloatingPosition(options.position);
|
|
2376
2612
|
const showPanel = state.isOpen && !minimized;
|
|
2613
|
+
const floating = useFloatingPosition(position, showPanel);
|
|
2377
2614
|
const minimize = () => {
|
|
2378
2615
|
setMinimized(true);
|
|
2379
2616
|
requestAnimationFrame(() => launcherRef.current?.focus());
|
|
2380
2617
|
};
|
|
2381
2618
|
useEffect(() => {
|
|
2382
|
-
if (state.step === "compose") state.annotationTrigger?.focus({ preventScroll: true });
|
|
2619
|
+
if (state.step === "compose") state.annotationTrigger?.button.focus({ preventScroll: true });
|
|
2383
2620
|
}, [state.step, state.annotationTrigger]);
|
|
2384
2621
|
useEffect(() => {
|
|
2385
2622
|
if (!showPanel || state.step !== "compose") return;
|
|
@@ -2396,14 +2633,14 @@ function FloatingWidget({ labels, options, state }) {
|
|
|
2396
2633
|
className: "root",
|
|
2397
2634
|
"data-editing": state.isOpen && (state.step === "picking" || state.step === "annotate"),
|
|
2398
2635
|
"data-moved": Boolean(floating.position),
|
|
2399
|
-
"data-position":
|
|
2636
|
+
"data-position": position,
|
|
2400
2637
|
ref: floating.rootRef,
|
|
2401
|
-
children: [
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2638
|
+
children: [
|
|
2639
|
+
/* @__PURE__ */ jsx("style", { children: floating.styles }),
|
|
2640
|
+
showPanel && state.step === "compose" && state.screenshots.map(({ id, selectedNode }) => selectedNode ? /* @__PURE__ */ jsx(SelectionOutline, { node: selectedNode }, id) : null),
|
|
2641
|
+
showPanel ? /* @__PURE__ */ jsx(FeedbackPanel, {
|
|
2405
2642
|
floatingControls: /* @__PURE__ */ jsxs("div", {
|
|
2406
|
-
className: "floating-controls
|
|
2643
|
+
className: "floating-controls",
|
|
2407
2644
|
children: [/* @__PURE__ */ jsx("button", {
|
|
2408
2645
|
"aria-label": labels.moveFeedback,
|
|
2409
2646
|
className: "icon-btn drag-handle",
|
|
@@ -2417,28 +2654,87 @@ function FloatingWidget({ labels, options, state }) {
|
|
|
2417
2654
|
onClick: minimize,
|
|
2418
2655
|
title: labels.minimize,
|
|
2419
2656
|
type: "button",
|
|
2420
|
-
children: /* @__PURE__ */ jsx(
|
|
2657
|
+
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
2421
2658
|
})]
|
|
2422
|
-
})
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2659
|
+
}),
|
|
2660
|
+
labels,
|
|
2661
|
+
state
|
|
2662
|
+
}) : /* @__PURE__ */ jsx(Launcher, {
|
|
2663
|
+
buttonRef: launcherRef,
|
|
2664
|
+
isOpen: false,
|
|
2665
|
+
label: state.isOpen ? labels.resume : labels.trigger,
|
|
2666
|
+
onClick: () => {
|
|
2667
|
+
setMinimized(false);
|
|
2668
|
+
if (!state.isOpen) state.open();
|
|
2669
|
+
}
|
|
2670
|
+
})
|
|
2671
|
+
]
|
|
2434
2672
|
});
|
|
2435
2673
|
}
|
|
2436
2674
|
//#endregion
|
|
2675
|
+
//#region src/feedback/ui/floating/element-walk.ts
|
|
2676
|
+
const ARROW_SIBLING = {
|
|
2677
|
+
ArrowLeft: "previousElementSibling",
|
|
2678
|
+
ArrowRight: "nextElementSibling"
|
|
2679
|
+
};
|
|
2680
|
+
const AIM_KEYS = [
|
|
2681
|
+
"Tab",
|
|
2682
|
+
"ArrowUp",
|
|
2683
|
+
"ArrowDown",
|
|
2684
|
+
"ArrowLeft",
|
|
2685
|
+
"ArrowRight"
|
|
2686
|
+
];
|
|
2687
|
+
function isWidgetOwned(element) {
|
|
2688
|
+
return element.closest(`[${WIDGET_MARKER}]`) !== null;
|
|
2689
|
+
}
|
|
2690
|
+
function isPickable(element) {
|
|
2691
|
+
if (!element || element === document.documentElement || isWidgetOwned(element)) return false;
|
|
2692
|
+
const rect = element.getBoundingClientRect();
|
|
2693
|
+
return rect.width > 0 && rect.height > 0;
|
|
2694
|
+
}
|
|
2695
|
+
function pickableElements() {
|
|
2696
|
+
return Array.from(document.body.querySelectorAll("*")).filter(isPickable);
|
|
2697
|
+
}
|
|
2698
|
+
function documentStep(from, backwards) {
|
|
2699
|
+
const all = pickableElements();
|
|
2700
|
+
const index = all.indexOf(from);
|
|
2701
|
+
if (all.length === 0) return null;
|
|
2702
|
+
if (index === -1) return all[0] ?? null;
|
|
2703
|
+
return all[(index + (backwards ? -1 : 1) + all.length) % all.length] ?? null;
|
|
2704
|
+
}
|
|
2705
|
+
function climb(from) {
|
|
2706
|
+
let candidate = from.parentElement;
|
|
2707
|
+
while (candidate && !isPickable(candidate)) candidate = candidate.parentElement;
|
|
2708
|
+
return candidate;
|
|
2709
|
+
}
|
|
2710
|
+
function descend(from) {
|
|
2711
|
+
let candidate = from.firstElementChild;
|
|
2712
|
+
while (candidate && !isPickable(candidate)) candidate = candidate.nextElementSibling;
|
|
2713
|
+
return candidate;
|
|
2714
|
+
}
|
|
2715
|
+
function sibling(from, direction) {
|
|
2716
|
+
let candidate = from[direction];
|
|
2717
|
+
while (candidate && !isPickable(candidate)) candidate = candidate[direction];
|
|
2718
|
+
return candidate;
|
|
2719
|
+
}
|
|
2720
|
+
function stepAim(from, key, backwards) {
|
|
2721
|
+
if (!(from && isPickable(from))) return pickableElements()[0] ?? null;
|
|
2722
|
+
if (key === "Tab") return documentStep(from, backwards);
|
|
2723
|
+
if (key === "ArrowUp") return climb(from);
|
|
2724
|
+
if (key === "ArrowDown") return descend(from);
|
|
2725
|
+
const direction = ARROW_SIBLING[key];
|
|
2726
|
+
return direction ? sibling(from, direction) : null;
|
|
2727
|
+
}
|
|
2728
|
+
//#endregion
|
|
2437
2729
|
//#region src/feedback/ui/picker.tsx
|
|
2438
2730
|
const LABEL_HEIGHT = 24;
|
|
2439
2731
|
const LABEL_GAP = 6;
|
|
2440
|
-
|
|
2441
|
-
|
|
2732
|
+
const NOTE_WIDTH = 300;
|
|
2733
|
+
const NOTE_GAP = 10;
|
|
2734
|
+
const EDGE_GAP = 12;
|
|
2735
|
+
/** Events raised by the picker's own note card must reach it, never be swallowed. */
|
|
2736
|
+
function targetsWidget(event) {
|
|
2737
|
+
return event.composedPath().some((node) => node instanceof Element && isWidgetOwned(node));
|
|
2442
2738
|
}
|
|
2443
2739
|
function elementUnder(x, y) {
|
|
2444
2740
|
const element = document.elementFromPoint(x, y);
|
|
@@ -2455,257 +2751,322 @@ function describeTarget(element) {
|
|
|
2455
2751
|
};
|
|
2456
2752
|
}
|
|
2457
2753
|
/**
|
|
2754
|
+
* Pins the note card to the element: below it, flipped above when the visible
|
|
2755
|
+
* viewport — what is left of the screen once a mobile keyboard is up — has no
|
|
2756
|
+
* room there. Measures against the part of the element actually on screen, so
|
|
2757
|
+
* picking a full-height section still leaves the card in view.
|
|
2758
|
+
*/
|
|
2759
|
+
function noteStyle(rect, view) {
|
|
2760
|
+
const left = Math.min(Math.max(view.left + EDGE_GAP, rect.left), Math.max(view.left + EDGE_GAP, view.left + view.width - NOTE_WIDTH - EDGE_GAP));
|
|
2761
|
+
const visibleTop = Math.max(rect.top, view.top + EDGE_GAP);
|
|
2762
|
+
const visibleBottom = Math.min(rect.bottom, view.top + view.height - EDGE_GAP);
|
|
2763
|
+
if (view.top + view.height - visibleBottom > visibleTop - view.top) return {
|
|
2764
|
+
left,
|
|
2765
|
+
top: visibleBottom + NOTE_GAP
|
|
2766
|
+
};
|
|
2767
|
+
return {
|
|
2768
|
+
bottom: window.innerHeight - visibleTop + NOTE_GAP,
|
|
2769
|
+
left
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
/**
|
|
2458
2773
|
* Points at any element in the page the way a coding agent needs it: the React
|
|
2459
|
-
* component that owns it plus the page region it lives in.
|
|
2774
|
+
* component that owns it plus the page region it lives in. Picking never
|
|
2775
|
+
* commits on its own — it pins a note card to the element, so the reporter
|
|
2776
|
+
* writes about that element right there and confirms. Aiming stays live until
|
|
2777
|
+
* then, which is what makes the flow usable without hover, on touch.
|
|
2460
2778
|
*/
|
|
2461
|
-
function ElementPicker({
|
|
2779
|
+
function ElementPicker({ labels, onCancel, onPick }) {
|
|
2462
2780
|
const [target, setTarget] = useState(null);
|
|
2781
|
+
const [pinned, setPinned] = useState(null);
|
|
2782
|
+
const [note, setNote] = useState("");
|
|
2783
|
+
const noteRef = useRef(null);
|
|
2784
|
+
const pinnedRef = useRef(false);
|
|
2785
|
+
const [view, setView] = useState(visibleViewport);
|
|
2786
|
+
const aimRef = useRef(null);
|
|
2787
|
+
useEffect(() => onViewportChange(() => setView(visibleViewport())), []);
|
|
2788
|
+
useEffect(() => {
|
|
2789
|
+
pinnedRef.current = pinned !== null;
|
|
2790
|
+
if (pinned) noteRef.current?.focus({ preventScroll: true });
|
|
2791
|
+
}, [pinned]);
|
|
2792
|
+
useEffect(() => {
|
|
2793
|
+
aimRef.current = target?.element ?? null;
|
|
2794
|
+
}, [target]);
|
|
2463
2795
|
useEffect(() => {
|
|
2464
2796
|
const swallow = (event) => {
|
|
2797
|
+
if (targetsWidget(event)) return;
|
|
2465
2798
|
event.preventDefault();
|
|
2466
2799
|
event.stopPropagation();
|
|
2467
2800
|
};
|
|
2468
|
-
const
|
|
2469
|
-
const element = elementUnder(
|
|
2470
|
-
if (!element)
|
|
2471
|
-
setTarget(null);
|
|
2472
|
-
return;
|
|
2473
|
-
}
|
|
2801
|
+
const aimAt = (x, y) => {
|
|
2802
|
+
const element = elementUnder(x, y);
|
|
2803
|
+
if (!element) return;
|
|
2474
2804
|
setTarget((current) => current?.element === element ? {
|
|
2475
2805
|
...current,
|
|
2476
2806
|
rect: element.getBoundingClientRect()
|
|
2477
2807
|
} : describeTarget(element));
|
|
2478
2808
|
};
|
|
2479
|
-
const
|
|
2809
|
+
const onMove = (event) => {
|
|
2810
|
+
if (pinnedRef.current || targetsWidget(event)) return;
|
|
2811
|
+
aimAt(event.clientX, event.clientY);
|
|
2812
|
+
};
|
|
2813
|
+
const onPointerDown = (event) => {
|
|
2814
|
+
if (targetsWidget(event)) return;
|
|
2815
|
+
swallow(event);
|
|
2816
|
+
if (!pinnedRef.current) aimAt(event.clientX, event.clientY);
|
|
2817
|
+
};
|
|
2818
|
+
const onPointerUp = (event) => {
|
|
2819
|
+
if (targetsWidget(event)) return;
|
|
2480
2820
|
swallow(event);
|
|
2821
|
+
if (pinnedRef.current) return;
|
|
2481
2822
|
const element = elementUnder(event.clientX, event.clientY);
|
|
2482
|
-
if (element)
|
|
2823
|
+
if (element) setPinned(describeTarget(element));
|
|
2824
|
+
};
|
|
2825
|
+
const moveAim = (key, backwards) => {
|
|
2826
|
+
const next = stepAim(aimRef.current, key, backwards);
|
|
2827
|
+
if (!next) return;
|
|
2828
|
+
next.scrollIntoView({
|
|
2829
|
+
block: "nearest",
|
|
2830
|
+
inline: "nearest"
|
|
2831
|
+
});
|
|
2832
|
+
aimRef.current = next;
|
|
2833
|
+
setTarget(describeTarget(next));
|
|
2834
|
+
};
|
|
2835
|
+
const onEscape = () => {
|
|
2836
|
+
if (pinnedRef.current) {
|
|
2837
|
+
setPinned(null);
|
|
2838
|
+
setNote("");
|
|
2839
|
+
return;
|
|
2840
|
+
}
|
|
2841
|
+
onCancel();
|
|
2483
2842
|
};
|
|
2484
2843
|
const onKeyDown = (event) => {
|
|
2485
2844
|
if (event.key === "Escape") {
|
|
2486
2845
|
swallow(event);
|
|
2487
|
-
|
|
2846
|
+
onEscape();
|
|
2847
|
+
return;
|
|
2488
2848
|
}
|
|
2849
|
+
if (pinnedRef.current || targetsWidget(event)) return;
|
|
2850
|
+
if (event.key === "Enter") {
|
|
2851
|
+
swallow(event);
|
|
2852
|
+
if (aimRef.current) setPinned(describeTarget(aimRef.current));
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
2855
|
+
if (!AIM_KEYS.includes(event.key)) return;
|
|
2856
|
+
swallow(event);
|
|
2857
|
+
moveAim(event.key, event.shiftKey);
|
|
2858
|
+
};
|
|
2859
|
+
const onScroll = () => {
|
|
2860
|
+
setTarget((current) => current ? {
|
|
2861
|
+
...current,
|
|
2862
|
+
rect: current.element.getBoundingClientRect()
|
|
2863
|
+
} : current);
|
|
2864
|
+
setPinned((current) => current ? {
|
|
2865
|
+
...current,
|
|
2866
|
+
rect: current.element.getBoundingClientRect()
|
|
2867
|
+
} : current);
|
|
2489
2868
|
};
|
|
2490
2869
|
document.addEventListener("pointermove", onMove, true);
|
|
2491
|
-
document.addEventListener("pointerdown",
|
|
2870
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
2871
|
+
document.addEventListener("pointerup", onPointerUp, true);
|
|
2492
2872
|
document.addEventListener("mousedown", swallow, true);
|
|
2493
2873
|
document.addEventListener("mouseup", swallow, true);
|
|
2494
|
-
document.addEventListener("click",
|
|
2874
|
+
document.addEventListener("click", swallow, true);
|
|
2495
2875
|
document.addEventListener("keydown", onKeyDown, true);
|
|
2876
|
+
window.addEventListener("scroll", onScroll, {
|
|
2877
|
+
capture: true,
|
|
2878
|
+
passive: true
|
|
2879
|
+
});
|
|
2496
2880
|
const previousCursor = document.body.style.cursor;
|
|
2497
2881
|
document.body.style.cursor = "crosshair";
|
|
2498
2882
|
return () => {
|
|
2499
2883
|
document.removeEventListener("pointermove", onMove, true);
|
|
2500
|
-
document.removeEventListener("pointerdown",
|
|
2884
|
+
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
2885
|
+
document.removeEventListener("pointerup", onPointerUp, true);
|
|
2501
2886
|
document.removeEventListener("mousedown", swallow, true);
|
|
2502
2887
|
document.removeEventListener("mouseup", swallow, true);
|
|
2503
|
-
document.removeEventListener("click",
|
|
2888
|
+
document.removeEventListener("click", swallow, true);
|
|
2504
2889
|
document.removeEventListener("keydown", onKeyDown, true);
|
|
2890
|
+
window.removeEventListener("scroll", onScroll, true);
|
|
2505
2891
|
document.body.style.cursor = previousCursor;
|
|
2506
2892
|
};
|
|
2507
|
-
}, [onCancel
|
|
2508
|
-
const rect = target?.rect;
|
|
2893
|
+
}, [onCancel]);
|
|
2894
|
+
const rect = (pinned ?? target)?.rect;
|
|
2509
2895
|
const labelAbove = rect ? rect.top > 30 : true;
|
|
2510
2896
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
2511
2897
|
rect && /* @__PURE__ */ jsx("div", {
|
|
2512
2898
|
className: "picker-box",
|
|
2899
|
+
"data-pinned": pinned !== null,
|
|
2513
2900
|
style: {
|
|
2514
2901
|
height: rect.height,
|
|
2515
|
-
|
|
2516
|
-
top: rect.top,
|
|
2902
|
+
translate: `${rect.left}px ${rect.top}px`,
|
|
2517
2903
|
width: rect.width
|
|
2518
2904
|
}
|
|
2519
2905
|
}),
|
|
2520
|
-
rect && target && /* @__PURE__ */ jsxs("div", {
|
|
2906
|
+
rect && target && !pinned && /* @__PURE__ */ jsxs("div", {
|
|
2521
2907
|
className: "picker-label",
|
|
2522
|
-
style: {
|
|
2523
|
-
left: Math.max(4, rect.left),
|
|
2524
|
-
top: labelAbove ? rect.top - LABEL_HEIGHT - LABEL_GAP : rect.bottom + LABEL_GAP
|
|
2525
|
-
},
|
|
2908
|
+
style: { translate: `${Math.max(4, rect.left)}px ${labelAbove ? rect.top - LABEL_HEIGHT - LABEL_GAP : rect.bottom + LABEL_GAP}px` },
|
|
2526
2909
|
children: [/* @__PURE__ */ jsx("strong", { children: target.componentName ? `<${target.componentName}>` : target.label }), target.region && /* @__PURE__ */ jsx("span", { children: target.region })]
|
|
2527
2910
|
}),
|
|
2528
|
-
/* @__PURE__ */ jsxs("
|
|
2529
|
-
className: "picker-
|
|
2911
|
+
pinned ? /* @__PURE__ */ jsxs("form", {
|
|
2912
|
+
className: "picker-note glass",
|
|
2913
|
+
onSubmit: (event) => {
|
|
2914
|
+
event.preventDefault();
|
|
2915
|
+
onPick(pinned.element, note.trim());
|
|
2916
|
+
},
|
|
2917
|
+
style: noteStyle(pinned.rect, view),
|
|
2530
2918
|
children: [
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2919
|
+
/* @__PURE__ */ jsxs("p", {
|
|
2920
|
+
className: "picker-note-target",
|
|
2921
|
+
children: [/* @__PURE__ */ jsx("strong", { children: pinned.componentName ? `<${pinned.componentName}>` : pinned.label }), pinned.region && /* @__PURE__ */ jsx("span", { children: pinned.region })]
|
|
2922
|
+
}),
|
|
2923
|
+
/* @__PURE__ */ jsx("textarea", {
|
|
2924
|
+
"aria-label": labels.elementNote,
|
|
2925
|
+
maxLength: MAX_SELECTION_COMMENT_LENGTH,
|
|
2926
|
+
onChange: (event) => setNote(event.target.value),
|
|
2927
|
+
onKeyDown: (event) => {
|
|
2928
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
2929
|
+
event.preventDefault();
|
|
2930
|
+
onPick(pinned.element, note.trim());
|
|
2931
|
+
}
|
|
2932
|
+
},
|
|
2933
|
+
placeholder: labels.elementNotePlaceholder,
|
|
2934
|
+
ref: noteRef,
|
|
2935
|
+
rows: 2,
|
|
2936
|
+
value: note
|
|
2937
|
+
}),
|
|
2938
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2939
|
+
className: "picker-note-actions",
|
|
2940
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
2941
|
+
className: "picker-cancel",
|
|
2942
|
+
onClick: () => {
|
|
2943
|
+
setPinned(null);
|
|
2944
|
+
setNote("");
|
|
2945
|
+
},
|
|
2946
|
+
type: "button",
|
|
2947
|
+
children: labels.pickAnother
|
|
2948
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
2949
|
+
"aria-label": labels.attachElement,
|
|
2950
|
+
className: "submit",
|
|
2951
|
+
title: labels.attachElement,
|
|
2952
|
+
type: "submit",
|
|
2953
|
+
children: /* @__PURE__ */ jsx(ArrowIcon, {})
|
|
2954
|
+
})]
|
|
2955
|
+
})
|
|
2534
2956
|
]
|
|
2957
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
2958
|
+
className: "picker-hint",
|
|
2959
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
2960
|
+
className: "picker-instruction",
|
|
2961
|
+
children: [
|
|
2962
|
+
labels.pickElementHint,
|
|
2963
|
+
" ",
|
|
2964
|
+
/* @__PURE__ */ jsx("kbd", { children: "Tab" }),
|
|
2965
|
+
/* @__PURE__ */ jsx("kbd", { children: "Enter" }),
|
|
2966
|
+
/* @__PURE__ */ jsx("kbd", { children: "Esc" })
|
|
2967
|
+
]
|
|
2968
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
2969
|
+
className: "picker-cancel",
|
|
2970
|
+
onClick: onCancel,
|
|
2971
|
+
type: "button",
|
|
2972
|
+
children: labels.cancel
|
|
2973
|
+
})]
|
|
2535
2974
|
})
|
|
2536
2975
|
] });
|
|
2537
2976
|
}
|
|
2538
2977
|
//#endregion
|
|
2539
|
-
//#region src/feedback/ui/styles.ts
|
|
2540
|
-
const
|
|
2541
|
-
:
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
--rf-radius: 20px;
|
|
2555
|
-
--rf-pill: 999px;
|
|
2556
|
-
--rf-width: 360px;
|
|
2557
|
-
--rf-glass: rgb(255 255 255 / 72%);
|
|
2558
|
-
--rf-glass-highlight: rgb(255 255 255 / 90%);
|
|
2559
|
-
--rf-glass-stroke: rgb(255 255 255 / 70%);
|
|
2560
|
-
--rf-radius-sm: 8px;
|
|
2561
|
-
--rf-shadow-inset: inset 0 0 0 1px var(--rf-border);
|
|
2562
|
-
--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%);
|
|
2563
|
-
--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%);
|
|
2564
|
-
--rf-ease: cubic-bezier(0.16, 1, 0.3, 1);
|
|
2565
|
-
color-scheme: light;
|
|
2566
|
-
all: initial;
|
|
2567
|
-
font-family: var(--rf-font, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
|
|
2568
|
-
}
|
|
2569
|
-
|
|
2570
|
-
:host([data-theme="dark"]) {
|
|
2571
|
-
--rf-bg: #202020;
|
|
2572
|
-
--rf-bg-subtle: rgb(255 255 255 / 5%);
|
|
2573
|
-
--rf-bg-hover: rgb(255 255 255 / 9%);
|
|
2574
|
-
--rf-border: rgb(255 255 255 / 9%);
|
|
2575
|
-
--rf-text: #ebeae8;
|
|
2576
|
-
--rf-text-muted: rgb(255 255 255 / 68%);
|
|
2577
|
-
--rf-glass: rgb(32 32 32 / 78%);
|
|
2578
|
-
--rf-glass-highlight: rgb(255 255 255 / 14%);
|
|
2579
|
-
--rf-glass-stroke: rgb(255 255 255 / 12%);
|
|
2580
|
-
--rf-primary: #f1efec;
|
|
2581
|
-
--rf-primary-text: #1c1c1b;
|
|
2582
|
-
--rf-accent: #529cca;
|
|
2583
|
-
--rf-danger: #eb5757;
|
|
2584
|
-
--rf-success: #4dab74;
|
|
2585
|
-
--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%);
|
|
2586
|
-
--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%);
|
|
2587
|
-
color-scheme: dark;
|
|
2588
|
-
}
|
|
2589
|
-
|
|
2590
|
-
*, *::before, *::after { box-sizing: border-box; }
|
|
2978
|
+
//#region src/feedback/ui/annotation/styles.ts
|
|
2979
|
+
const ANNOTATOR_STYLES = `
|
|
2980
|
+
.overlay { margin: 0; border: 0; padding: 0; width: 100vw; max-width: none; height: 100dvh; max-height: none; position: fixed; inset: 0; z-index: ${REFLET_Z_INDEX.annotatorOverlay}; display: flex; flex-direction: column; background: transparent; color: var(--rf-text); font: 13px/1.45 ui-sans-serif, system-ui, sans-serif; }
|
|
2981
|
+
.overlay::before { content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none; background: color-mix(in srgb, var(--rf-bg) 94%, transparent); animation: rf-editor-in 260ms var(--rf-ease); transition: opacity 260ms var(--rf-ease); }
|
|
2982
|
+
.overlay::backdrop { background: transparent; }
|
|
2983
|
+
@keyframes rf-editor-in { from { opacity: 0; } }
|
|
2984
|
+
.editor { flex: 1; position: relative; display: flex; align-items: center; justify-content: center; padding: 20px; min-height: 0; container-type: size; }
|
|
2985
|
+
.editor-image { width: min(100cqw, calc(100cqh * var(--rf-capture-ratio))); aspect-ratio: var(--rf-capture-ratio); flex: none; position: relative; }
|
|
2986
|
+
.editor-visual { position: absolute; inset: 0; border-radius: var(--rf-radius); box-shadow: var(--rf-shadow); transform-origin: top left; overflow: hidden; }
|
|
2987
|
+
.editor-visual img, .editor-visual canvas { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: inherit; object-fit: cover; object-position: top center; outline: 1px solid var(--rf-hairline); outline-offset: -1px; }
|
|
2988
|
+
.editor-visual canvas { cursor: crosshair; touch-action: none; }
|
|
2989
|
+
.overlay[data-opening="true"] .editor { pointer-events: none; }
|
|
2990
|
+
.overlay[data-closing="true"] { pointer-events: none; }
|
|
2991
|
+
.overlay[data-closing="true"]::before { opacity: 0; }
|
|
2992
|
+
.overlay[data-closing="true"] .toolbar, .overlay[data-closing="true"] .element-note { opacity: 0; transform: translateY(8px); }
|
|
2591
2993
|
|
|
2994
|
+
.element-note { order: 1; display: flex; align-items: flex-start; gap: 10px; width: min(560px, calc(100vw - 32px)); margin: 0 auto 10px; padding: 10px 14px; border-radius: var(--rf-radius); transition: opacity 180ms var(--rf-ease), transform 180ms var(--rf-ease); }
|
|
2995
|
+
.element-note svg { flex: none; margin-top: 4px; opacity: 0.7; }
|
|
2996
|
+
.element-note textarea { flex: 1; min-width: 0; height: 42px; max-height: 96px; padding: 0; border: 0; background: transparent; color: inherit; font: inherit; resize: none; }
|
|
2997
|
+
.element-note textarea:focus { outline: none; }
|
|
2998
|
+
.element-note:focus-within { outline: 2px solid var(--rf-accent); outline-offset: 2px; }
|
|
2592
2999
|
|
|
2593
|
-
.
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
}
|
|
2602
|
-
.
|
|
2603
|
-
.
|
|
2604
|
-
.
|
|
2605
|
-
.
|
|
2606
|
-
.
|
|
2607
|
-
.
|
|
2608
|
-
.
|
|
2609
|
-
.
|
|
2610
|
-
.
|
|
2611
|
-
button
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
.
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
}
|
|
2621
|
-
.
|
|
2622
|
-
.
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
transition: transform 200ms var(--rf-ease), box-shadow 200ms var(--rf-ease);
|
|
2627
|
-
}
|
|
2628
|
-
.launcher:hover { transform: translateY(-2px); box-shadow: var(--rf-shadow); }
|
|
2629
|
-
.launcher-icon { display: flex; width: 15px; height: 15px; }
|
|
2630
|
-
.launcher-icon .on-close { display: none; }
|
|
2631
|
-
.floating-controls {
|
|
2632
|
-
display: flex; align-items: center; padding: 3px; flex: none;
|
|
2633
|
-
border-radius: var(--rf-pill);
|
|
2634
|
-
animation: rf-in 220ms var(--rf-ease);
|
|
2635
|
-
}
|
|
2636
|
-
.drag-handle { touch-action: none; cursor: grab; }
|
|
2637
|
-
.drag-handle:active { cursor: grabbing; }
|
|
2638
|
-
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2639
|
-
.ghost-btn { display: flex; align-items: center; gap: 8px; min-height: 32px; padding: 0 8px; border-radius: var(--rf-radius-sm); }
|
|
2640
|
-
.ghost-btn:hover { background: var(--rf-bg-hover); }
|
|
2641
|
-
.error { margin: 0; padding: 10px 14px; border-radius: var(--rf-radius); font-size: 12px; color: var(--rf-danger); }
|
|
2642
|
-
.done { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 24px; text-align: center; }
|
|
2643
|
-
.done svg { color: var(--rf-success); }
|
|
2644
|
-
.done h2 { margin: 0; font-size: 14px; font-weight: 600; }
|
|
2645
|
-
.done p { margin: 0; font-size: 12px; color: var(--rf-text-muted); }
|
|
2646
|
-
.spinner { width: 14px; height: 14px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: rf-spin 700ms linear infinite; }
|
|
2647
|
-
.hp { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
|
2648
|
-
@keyframes rf-spin { to { transform: rotate(360deg); } }
|
|
2649
|
-
@keyframes rf-in { from { opacity: 0; transform: translateY(5px) scale(.98); } }
|
|
2650
|
-
@keyframes rf-fade { from { opacity: 0; } }
|
|
3000
|
+
.toolbar { order: 2; display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; transition: opacity 180ms var(--rf-ease), transform 180ms var(--rf-ease); padding: 4px 12px max(16px, env(safe-area-inset-bottom)); }
|
|
3001
|
+
.toolbar .group { display: flex; align-items: center; gap: 2px; padding: 4px; border-radius: var(--rf-pill); }
|
|
3002
|
+
.tool[aria-pressed="true"], .tool[data-selected="true"] { background: var(--rf-primary); color: var(--rf-primary-text); }
|
|
3003
|
+
.color-picker { position: relative; }
|
|
3004
|
+
.color-picker summary { display: flex; justify-content: center; align-items: center; }
|
|
3005
|
+
.color-preview { width: 18px; height: 18px; border-radius: 50%; background: var(--annotation-color); box-shadow: inset 0 0 0 1px var(--rf-border); }
|
|
3006
|
+
.color-popover { --rf-color-width: 200px; position: absolute; bottom: calc(100% + 14px); left: clamp(calc(12px - var(--rf-color-anchor)), 0px, calc(100vw - var(--rf-color-anchor) - var(--rf-color-width) - 12px)); display: flex; align-items: center; gap: 6px; width: var(--rf-color-width); padding: 4px 4px 4px 14px; border-radius: var(--rf-pill); animation: rf-in 180ms var(--rf-ease); }
|
|
3007
|
+
.color-picker[data-placement="below"] .color-popover { top: calc(100% + 14px); bottom: auto; }
|
|
3008
|
+
.color-slider { flex: 1; min-width: 0; }
|
|
3009
|
+
.color-slider-control { display: flex; align-items: center; height: 40px; width: 100%; touch-action: none; user-select: none; cursor: pointer; }
|
|
3010
|
+
.color-slider-track { height: 12px; width: 100%; border-radius: var(--rf-pill); background: linear-gradient(to right, var(--rf-swatch-base), var(--annotation-base-color)); box-shadow: inset 0 0 0 1px var(--rf-border); }
|
|
3011
|
+
.color-slider-thumb { width: 16px; height: 16px; border: 2px solid var(--rf-swatch-base); border-radius: 50%; background: var(--annotation-color); box-shadow: var(--rf-shadow-low); transition: scale 160ms var(--rf-ease); }
|
|
3012
|
+
.color-slider-thumb[data-dragging] { scale: 1.12; }
|
|
3013
|
+
.color-slider-thumb:has(:focus-visible) { outline: 2px solid var(--rf-accent); outline-offset: 3px; }
|
|
3014
|
+
.color-spectrum { position: relative; display: flex; align-items: center; justify-content: center; flex: none; width: 40px; height: 40px; border-radius: 50%; cursor: pointer; }
|
|
3015
|
+
.color-spectrum::before { content: ""; width: 16px; height: 16px; border-radius: 50%; background: var(--rf-spectrum); box-shadow: inset 0 0 0 1px var(--rf-glass-highlight); }
|
|
3016
|
+
.color-spectrum:hover { background: var(--rf-bg-hover); }
|
|
3017
|
+
.color-spectrum button { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: inherit; }
|
|
3018
|
+
.color-spectrum button:focus-visible { outline: 2px solid var(--rf-accent); outline-offset: 3px; }
|
|
3019
|
+
.color-spectrum input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; pointer-events: none; }
|
|
3020
|
+
.done-btn { display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; padding: 0; border-radius: var(--rf-pill); color: var(--rf-primary-text); background: var(--rf-primary); }
|
|
3021
|
+
.drawing-menu { position: relative; }
|
|
3022
|
+
.drawing-popover { position: absolute; z-index: 2; right: 0; bottom: calc(100% + 12px); width: 200px; max-height: calc(50dvh - 68px); overflow-y: auto; padding: 6px; border-radius: var(--rf-radius); }
|
|
3023
|
+
.drawing-menu[data-placement="below"] .drawing-popover { top: calc(100% + 12px); bottom: auto; }
|
|
3024
|
+
.drawing-option { display: flex; align-items: center; width: 100%; min-height: 40px; gap: 10px; padding: 0 10px; border-radius: 12px; text-align: left; }
|
|
3025
|
+
.drawing-option:hover, .drawing-option[aria-pressed="true"] { background: var(--rf-bg-hover); }
|
|
3026
|
+
.drawing-divider { height: 1px; margin: 5px; background: var(--rf-border); }
|
|
3027
|
+
.editor canvas[data-tool="text"] { cursor: text; }
|
|
3028
|
+
.text-annotation-editor { position: fixed; z-index: 3; width: 240px; padding: 8px; border-radius: var(--rf-radius); }
|
|
3029
|
+
.text-annotation-editor:focus-within { outline: 2px solid var(--rf-accent); outline-offset: 2px; }
|
|
3030
|
+
.text-annotation-editor textarea { display: block; width: 100%; min-height: 64px; max-height: 112px; padding: 6px; border: 0; background: transparent; resize: vertical; font-size: 16px; line-height: 24px; }
|
|
3031
|
+
.text-annotation-editor textarea:focus { outline: none; }
|
|
3032
|
+
.text-annotation-actions { display: flex; align-items: center; justify-content: flex-end; gap: 4px; }
|
|
2651
3033
|
@media (pointer: coarse) {
|
|
2652
|
-
.
|
|
2653
|
-
.
|
|
2654
|
-
}
|
|
2655
|
-
.
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
}
|
|
2661
|
-
.glass::before {
|
|
2662
|
-
content: "";
|
|
2663
|
-
position: absolute;
|
|
2664
|
-
inset: 0;
|
|
2665
|
-
z-index: -1;
|
|
2666
|
-
border-radius: inherit;
|
|
2667
|
-
background: var(--rf-bg);
|
|
2668
|
-
pointer-events: none;
|
|
2669
|
-
}
|
|
2670
|
-
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
|
|
2671
|
-
.glass::before {
|
|
2672
|
-
background: var(--rf-glass);
|
|
2673
|
-
-webkit-backdrop-filter: blur(20px) saturate(1.6);
|
|
2674
|
-
backdrop-filter: blur(20px) saturate(1.6);
|
|
2675
|
-
}
|
|
3034
|
+
.drawing-option { min-height: 44px; }
|
|
3035
|
+
.done-btn { width: 44px; height: 44px; }
|
|
3036
|
+
.color-popover { --rf-color-width: 232px; padding-left: 16px; }
|
|
3037
|
+
.color-slider-control, .color-spectrum { height: 44px; }
|
|
3038
|
+
.color-spectrum { width: 44px; }
|
|
3039
|
+
.color-slider-thumb, .color-spectrum::before { width: 18px; height: 18px; }
|
|
3040
|
+
.editor { padding: 8px; }
|
|
3041
|
+
.toolbar { gap: 6px; }
|
|
3042
|
+
.toolbar .group { padding: 3px; gap: 0; }
|
|
2676
3043
|
}
|
|
2677
|
-
@media (
|
|
2678
|
-
.
|
|
3044
|
+
@media (pointer: coarse) and (max-width: 360px) {
|
|
3045
|
+
.toolbar { gap: 4px; padding-inline: 2px; }
|
|
3046
|
+
.toolbar .group { padding: 0; }
|
|
2679
3047
|
}
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
.
|
|
2683
|
-
|
|
2684
|
-
|
|
3048
|
+
`;
|
|
3049
|
+
//#endregion
|
|
3050
|
+
//#region src/feedback/ui/composer/styles.ts
|
|
3051
|
+
const COMPOSER_STYLES = `
|
|
2685
3052
|
.panel { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 8px; outline: none; animation: rf-in 240ms var(--rf-ease); }
|
|
2686
3053
|
.success-panel { border-radius: var(--rf-radius); }
|
|
2687
3054
|
.composer { position: relative; display: flex; flex-direction: column; gap: 8px; }
|
|
2688
|
-
.message-row { display: flex;
|
|
3055
|
+
.message-row { display: flex; flex-direction: column; gap: 2px; padding: 6px; border-radius: 26px; }
|
|
2689
3056
|
.message-row textarea {
|
|
2690
|
-
|
|
2691
|
-
margin: 0; padding:
|
|
2692
|
-
background: transparent; line-height: 20px;
|
|
3057
|
+
width: 100%; height: 76px; max-height: min(140px, 30dvh);
|
|
3058
|
+
margin: 0; padding: 6px 6px 0 8px; border: 0; resize: none;
|
|
3059
|
+
background: transparent; line-height: 20px;
|
|
2693
3060
|
}
|
|
2694
|
-
.
|
|
3061
|
+
.message-actions { display: flex; align-items: center; gap: 2px; }
|
|
2695
3062
|
.message-row textarea::placeholder { color: var(--rf-text-muted); }
|
|
2696
3063
|
.message-row textarea:focus { outline: none; }
|
|
2697
3064
|
.message-row:has(textarea:focus-visible) { border-color: color-mix(in srgb, var(--rf-accent) 40%, var(--rf-glass-stroke)); }
|
|
2698
|
-
.submit { display: flex; align-items: center; justify-content: center; width:
|
|
3065
|
+
.submit { display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; flex: none; border-radius: var(--rf-pill); background: var(--rf-primary); color: var(--rf-primary-text); transition: opacity 160ms var(--rf-ease), transform 160ms var(--rf-ease); }
|
|
2699
3066
|
.submit svg { transform: rotate(-45deg); }
|
|
2700
3067
|
.submit:hover:enabled { transform: scale(1.05); }
|
|
2701
3068
|
.submit:disabled { background: var(--rf-bg-hover); color: var(--rf-text-muted); opacity: .65; }
|
|
2702
3069
|
.composer-toolbar { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex-wrap: wrap; }
|
|
2703
|
-
.composer-controls { display: flex; align-items: center; gap: 2px; padding: 3px; border-radius: var(--rf-pill); }
|
|
2704
|
-
.select-control { position: relative; display: flex; align-items: center; min-width: 0; }
|
|
2705
|
-
.select-control > svg { position: absolute; right: 9px; width: 12px; height: 12px; pointer-events: none; color: var(--rf-text-muted); }
|
|
2706
|
-
.category-select { appearance: none; min-width: 0; max-width: 124px; height: 32px; padding: 0 26px 0 12px; border: 0; border-radius: var(--rf-pill); color: var(--rf-text); background: transparent; cursor: pointer; text-overflow: ellipsis; }
|
|
2707
|
-
.category-select:hover:hover { background: var(--rf-bg-hover); }
|
|
2708
|
-
option { background: var(--rf-bg); color: var(--rf-text); }
|
|
2709
3070
|
.composer-options { position: relative; }
|
|
2710
3071
|
summary { cursor: pointer; list-style: none; }
|
|
2711
3072
|
summary::-webkit-details-marker { display: none; }
|
|
@@ -2714,124 +3075,91 @@ summary::-webkit-details-marker { display: none; }
|
|
|
2714
3075
|
.email-field { display: flex; flex-direction: column; gap: 6px; color: var(--rf-text-muted); font-size: 12px; }
|
|
2715
3076
|
.email-field [role="alert"] { color: var(--rf-danger); }
|
|
2716
3077
|
.email-field input { width: 100%; padding: 9px; border: 1px solid var(--rf-border); border-radius: var(--rf-radius-sm); background: var(--rf-bg-subtle); color: var(--rf-text); }
|
|
2717
|
-
.dismiss-btn { min-height:
|
|
3078
|
+
.dismiss-btn { min-height: 40px; text-align: left; padding: 0 8px; border-radius: var(--rf-radius-sm); color: var(--rf-text-muted); }
|
|
2718
3079
|
.dismiss-btn:hover { background: var(--rf-bg-hover); }
|
|
2719
3080
|
.version-info { font-size: 10px; color: var(--rf-text-muted); padding: 4px 8px 0; border-top: 1px solid var(--rf-border); text-decoration: none; }
|
|
2720
3081
|
.version-info:hover { color: var(--rf-text); }
|
|
3082
|
+
.email-scrim { display: none; }
|
|
3083
|
+
.email-prompt { display: flex; flex-direction: column; gap: 12px; padding: 16px; border-radius: 22px; animation: rf-in 240ms var(--rf-ease); }
|
|
3084
|
+
.email-prompt h2 { margin: 0; font-size: 14px; font-weight: 600; line-height: 1.3; }
|
|
3085
|
+
.email-prompt p { margin: 0; font-size: 12px; line-height: 1.45; color: var(--rf-text-muted); }
|
|
3086
|
+
.email-prompt .email-field input { min-height: 40px; border-radius: 14px; padding: 0 12px; }
|
|
3087
|
+
.email-prompt-actions { display: flex; align-items: center; justify-content: flex-end; gap: 4px; }
|
|
3088
|
+
.email-prompt-actions .ghost-btn { padding: 0 12px; color: var(--rf-text-muted); }
|
|
3089
|
+
.primary-btn { display: flex; align-items: center; justify-content: center; min-height: 40px; padding: 0 16px; border-radius: var(--rf-pill); background: var(--rf-primary); color: var(--rf-primary-text); font-weight: 500; }
|
|
3090
|
+
.primary-btn:hover { opacity: .9; }
|
|
2721
3091
|
.attachments { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }
|
|
2722
|
-
.
|
|
2723
|
-
.screenshot-
|
|
2724
|
-
.screenshot-strip
|
|
2725
|
-
.screenshot-strip[data-overflow-x-
|
|
3092
|
+
.attachment-row { display: flex; align-items: flex-end; gap: 8px; min-width: 0; max-width: 100%; }
|
|
3093
|
+
.screenshot-scroll-area { position: relative; min-width: 0; margin: -14px -10px -20px; }
|
|
3094
|
+
.screenshot-strip { width: 100%; padding: 14px 10px 20px; overscroll-behavior-x: contain; --rf-fade-start: 0px; --rf-fade-end: 0px; mask-image: linear-gradient(to right, transparent, black var(--rf-fade-start), black calc(100% - var(--rf-fade-end)), transparent); mask-repeat: no-repeat; }
|
|
3095
|
+
.screenshot-strip[data-overflow-x-start] { --rf-fade-start: calc(10px + min(24px, var(--scroll-area-overflow-x-start))); }
|
|
3096
|
+
.screenshot-strip[data-overflow-x-end] { --rf-fade-end: calc(10px + min(24px, var(--scroll-area-overflow-x-end))); }
|
|
2726
3097
|
.screenshot-strip-content { display: flex; align-items: flex-end; gap: 8px; }
|
|
2727
3098
|
.screenshot-scroll-area:has(.screenshot-strip-content:empty) { display: none; }
|
|
2728
|
-
.
|
|
3099
|
+
.capture-slot { display: flex; flex-direction: column; gap: 2px; flex: none; width: 56px; height: var(--rf-attachment-height); padding: 3px; border: 1px dashed var(--rf-border); border-radius: var(--rf-radius); box-shadow: none; }
|
|
3100
|
+
.capture-action { display: flex; flex: 1; min-height: 0; align-items: center; justify-content: center; border-radius: 5px; color: var(--rf-text-muted); transition: background 160ms var(--rf-ease), color 160ms var(--rf-ease); }
|
|
3101
|
+
.capture-action:first-child { border-radius: 16px 16px 5px 5px; }
|
|
3102
|
+
.capture-action:last-child { border-radius: 5px 5px 16px 16px; }
|
|
3103
|
+
.capture-action:hover:enabled { background: var(--rf-bg-hover); color: var(--rf-text); }
|
|
2729
3104
|
.screenshot-scrollbar { height: 4px; margin: 0 8px 2px; opacity: 0; transition: opacity 150ms var(--rf-ease); }
|
|
2730
3105
|
.screenshot-scrollbar[data-hovering], .screenshot-scrollbar[data-scrolling] { opacity: 1; }
|
|
2731
3106
|
.screenshot-scroll-thumb { border-radius: var(--rf-pill); background: var(--rf-border); }
|
|
2732
|
-
.capture-progress { position: absolute; inset:
|
|
3107
|
+
.capture-progress { position: absolute; inset: 0; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 6px; border-radius: inherit; color: var(--rf-text-muted); font-size: 10px; }
|
|
2733
3108
|
.attachments:empty { display: none; }
|
|
2734
|
-
.screenshot-attachment, .attachment-skeleton { position: relative; flex: none; margin: 0; min-width: 0; width: 128px; height:
|
|
2735
|
-
.screenshot-
|
|
3109
|
+
.screenshot-attachment, .attachment-skeleton { position: relative; flex: none; margin: 0; min-width: 0; width: 128px; height: var(--rf-attachment-height); padding: 0; border-radius: 16px; animation: rf-in 260ms var(--rf-ease); }
|
|
3110
|
+
.screenshot-attachment { border: 0; }
|
|
3111
|
+
.screenshot-preview { display: block; position: relative; width: 100%; height: 100%; padding: 0; border-radius: inherit; overflow: hidden; }
|
|
2736
3112
|
.screenshot-preview img, .screenshot-preview canvas { width: 100%; height: 100%; display: block; object-fit: cover; object-position: top center; }
|
|
2737
3113
|
.screenshot-preview canvas { position: absolute; inset: 0; pointer-events: none; }
|
|
2738
|
-
.
|
|
2739
|
-
.attachment-
|
|
2740
|
-
.
|
|
2741
|
-
.attachment-actions {
|
|
2742
|
-
.screenshot-attachment:hover .attachment-actions, .screenshot-attachment:focus-within .attachment-actions { opacity: 1; pointer-events: auto; transform: none; }
|
|
2743
|
-
.screenshot-attachment:hover .attachment-caption, .screenshot-attachment:focus-within .attachment-caption { opacity: 0; }
|
|
3114
|
+
.annotation-count { position: absolute; left: 5px; bottom: 5px; min-width: 20px; padding: 2px 6px; border-radius: var(--rf-pill); font-size: 10px; font-weight: 600; font-variant-numeric: tabular-nums; text-align: center; color: var(--rf-text); }
|
|
3115
|
+
.attachment-actions { position: absolute; right: 4px; bottom: 4px; display: flex; gap: 1px; padding: 1px; border-radius: var(--rf-pill); }
|
|
3116
|
+
.attachment-actions .icon-btn { width: 28px; height: 28px; }
|
|
3117
|
+
.attachment-actions svg { width: 14px; height: 14px; }
|
|
2744
3118
|
.attachment-skeleton { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: var(--rf-text-muted); font-size: 11px; }
|
|
2745
3119
|
.attachment-skeleton::after { content: ""; position: absolute; inset: 0; border-radius: inherit; background: var(--rf-bg-subtle); animation: rf-fade 800ms alternate infinite; }
|
|
2746
|
-
.selection-
|
|
2747
|
-
.selection-
|
|
2748
|
-
.selection-label img { object-fit: contain; border-radius: var(--rf-pill); }
|
|
3120
|
+
.selection-badge { position: absolute; left: 5px; top: 5px; display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: var(--rf-pill); color: var(--rf-accent); }
|
|
3121
|
+
.selection-badge svg { width: 12px; height: 12px; }
|
|
2749
3122
|
@media (pointer: coarse) {
|
|
2750
|
-
.category-select { height: 44px; max-width: 96px; }
|
|
2751
3123
|
.message-row { border-radius: 28px; }
|
|
2752
|
-
.
|
|
3124
|
+
.capture-slot { width: 64px; }
|
|
3125
|
+
.message-row textarea { font-size: 16px; }
|
|
2753
3126
|
.submit { width: 44px; height: 44px; }
|
|
2754
|
-
|
|
2755
|
-
.attachment-
|
|
2756
|
-
.attachment-
|
|
2757
|
-
.
|
|
2758
|
-
.screenshot-attachment, .attachment-skeleton { height: 96px; }
|
|
3127
|
+
:host { --rf-attachment-height: 96px; }
|
|
3128
|
+
.attachment-actions { right: 5px; bottom: 5px; }
|
|
3129
|
+
.attachment-actions .icon-btn { width: 34px; height: 34px; }
|
|
3130
|
+
.selection-badge { width: 24px; height: 24px; }
|
|
2759
3131
|
.email-field input { font-size: 16px; }
|
|
2760
|
-
.dismiss-btn
|
|
3132
|
+
.dismiss-btn { min-height: 44px; }
|
|
2761
3133
|
}
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
.
|
|
2765
|
-
.
|
|
2766
|
-
|
|
2767
|
-
.
|
|
2768
|
-
.
|
|
2769
|
-
.editor-visual { position: absolute; inset: 0; border-radius: var(--rf-radius); box-shadow: var(--rf-shadow); transform-origin: top left; overflow: hidden; }
|
|
2770
|
-
.editor-visual img, .editor-visual canvas { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: inherit; object-fit: cover; object-position: top center; }
|
|
2771
|
-
.editor-visual canvas { cursor: crosshair; touch-action: none; }
|
|
2772
|
-
.overlay[data-opening="true"] .editor { pointer-events: none; }
|
|
2773
|
-
.overlay[data-closing="true"] { pointer-events: none; background: transparent; }
|
|
2774
|
-
.overlay[data-closing="true"] .toolbar { opacity: 0; transform: translateY(8px); }
|
|
2775
|
-
|
|
2776
|
-
.toolbar { display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; transition: opacity 180ms var(--rf-ease), transform 180ms var(--rf-ease); padding: 4px 12px max(16px, env(safe-area-inset-bottom)); }
|
|
2777
|
-
.toolbar .group { display: flex; align-items: center; gap: 2px; padding: 4px; border-radius: var(--rf-pill); }
|
|
2778
|
-
.tool[aria-pressed="true"], .tool[data-selected="true"] { background: var(--rf-primary); color: var(--rf-primary-text); }
|
|
2779
|
-
.color-picker { position: relative; }
|
|
2780
|
-
.color-picker summary { display: flex; justify-content: center; align-items: center; }
|
|
2781
|
-
.color-preview { width: 18px; height: 18px; border-radius: 50%; background: var(--annotation-color); box-shadow: inset 0 0 0 1px var(--rf-border); }
|
|
2782
|
-
.color-popover { --rf-color-width: 200px; position: absolute; bottom: calc(100% + 14px); left: clamp(calc(12px - var(--rf-color-anchor)), 0px, calc(100vw - var(--rf-color-anchor) - var(--rf-color-width) - 12px)); display: flex; align-items: center; gap: 6px; width: var(--rf-color-width); padding: 4px 4px 4px 14px; border-radius: var(--rf-pill); animation: rf-in 180ms var(--rf-ease); }
|
|
2783
|
-
.color-picker[data-placement="below"] .color-popover { top: calc(100% + 14px); bottom: auto; }
|
|
2784
|
-
.color-slider { flex: 1; min-width: 0; }
|
|
2785
|
-
.color-slider-control { display: flex; align-items: center; height: 32px; width: 100%; touch-action: none; user-select: none; cursor: pointer; }
|
|
2786
|
-
.color-slider-track { height: 12px; width: 100%; border-radius: var(--rf-pill); background: linear-gradient(to right, white, var(--annotation-base-color)); box-shadow: inset 0 0 0 1px var(--rf-border); }
|
|
2787
|
-
.color-slider-thumb { width: 16px; height: 16px; border: 2px solid white; border-radius: 50%; background: var(--annotation-color); box-shadow: var(--rf-shadow-low); transition: scale 160ms var(--rf-ease); }
|
|
2788
|
-
.color-slider-thumb[data-dragging] { scale: 1.12; }
|
|
2789
|
-
.color-slider-thumb:has(:focus-visible), .color-spectrum:has(:focus-visible) { outline: 2px solid var(--rf-accent); outline-offset: 3px; }
|
|
2790
|
-
.color-spectrum { position: relative; display: flex; align-items: center; justify-content: center; flex: none; width: 32px; height: 32px; border-radius: 50%; cursor: pointer; }
|
|
2791
|
-
.color-spectrum::before { content: ""; width: 16px; height: 16px; border-radius: 50%; background: conic-gradient(#ff5757, #ffde59, #6bdb78, #4ac9fa, #8b5cf6, #ef73dc, #ff5757); box-shadow: inset 0 0 0 1px var(--rf-glass-highlight); }
|
|
2792
|
-
.color-spectrum:hover { background: var(--rf-bg-hover); }
|
|
2793
|
-
.color-spectrum button { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: inherit; }
|
|
2794
|
-
.color-spectrum button:focus-visible { outline: none; }
|
|
2795
|
-
.color-spectrum input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; pointer-events: none; }
|
|
2796
|
-
.done-btn { display: flex; align-items: center; justify-content: center; width: 34px; height: 34px; padding: 0; border-radius: var(--rf-pill); color: var(--rf-primary-text); background: var(--rf-primary); }
|
|
2797
|
-
.drawing-menu { position: relative; }
|
|
2798
|
-
.drawing-popover { position: absolute; z-index: 2; right: 0; bottom: calc(100% + 12px); width: 200px; max-height: calc(50dvh - 68px); overflow-y: auto; padding: 6px; border-radius: var(--rf-radius); }
|
|
2799
|
-
.drawing-menu[data-placement="below"] .drawing-popover { top: calc(100% + 12px); bottom: auto; }
|
|
2800
|
-
.drawing-option { display: flex; align-items: center; width: 100%; min-height: 36px; gap: 10px; padding: 0 10px; border-radius: 12px; text-align: left; }
|
|
2801
|
-
.drawing-option:hover, .drawing-option[aria-pressed="true"] { background: var(--rf-bg-hover); }
|
|
2802
|
-
.drawing-divider { height: 1px; margin: 5px; background: var(--rf-border); }
|
|
2803
|
-
.editor canvas[data-tool="text"] { cursor: text; }
|
|
2804
|
-
.text-annotation-editor { position: fixed; z-index: 3; width: 240px; padding: 8px; border-radius: var(--rf-radius); }
|
|
2805
|
-
.text-annotation-editor textarea { display: block; width: 100%; min-height: 64px; max-height: 112px; padding: 6px; border: 0; background: transparent; resize: vertical; font-size: 16px; line-height: 24px; }
|
|
2806
|
-
.text-annotation-editor textarea:focus { outline: none; }
|
|
2807
|
-
.text-annotation-actions { display: flex; align-items: center; justify-content: flex-end; gap: 4px; }
|
|
2808
|
-
@media (pointer: coarse) {
|
|
2809
|
-
.drawing-option { min-height: 44px; }
|
|
2810
|
-
.done-btn { width: 44px; height: 44px; }
|
|
2811
|
-
.color-popover { --rf-color-width: 232px; padding-left: 16px; }
|
|
2812
|
-
.color-slider-control, .color-spectrum { height: 44px; }
|
|
2813
|
-
.color-spectrum { width: 44px; }
|
|
2814
|
-
.color-slider-thumb, .color-spectrum::before { width: 18px; height: 18px; }
|
|
2815
|
-
.editor { padding: 8px; }
|
|
2816
|
-
.toolbar { gap: 6px; }
|
|
2817
|
-
.toolbar .group { padding: 3px; gap: 0; }
|
|
3134
|
+
@media (max-width: 480px) {
|
|
3135
|
+
.email-scrim { display: block; position: fixed; inset: 0; background: rgb(0 0 0 / 40%); animation: rf-fade 200ms var(--rf-ease); }
|
|
3136
|
+
.email-prompt { position: fixed; left: 0; right: 0; bottom: 0; gap: 14px; padding: 20px 16px calc(20px + env(safe-area-inset-bottom)); border-radius: 26px 26px 0 0; animation: rf-drawer-in 280ms var(--rf-ease); }
|
|
3137
|
+
.email-prompt h2 { font-size: 17px; }
|
|
3138
|
+
.email-prompt p { font-size: 13px; }
|
|
3139
|
+
.email-prompt-actions { flex-direction: column-reverse; align-items: stretch; gap: 8px; }
|
|
3140
|
+
.email-prompt-actions .ghost-btn, .email-prompt-actions .primary-btn { justify-content: center; min-height: 48px; }
|
|
2818
3141
|
}
|
|
2819
|
-
@
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
3142
|
+
@keyframes rf-drawer-in { from { transform: translateY(100%); } }
|
|
3143
|
+
`;
|
|
3144
|
+
//#endregion
|
|
3145
|
+
//#region src/feedback/ui/floating/picker-styles.ts
|
|
3146
|
+
const PICKER_STYLES = `
|
|
2825
3147
|
.picker-box {
|
|
2826
3148
|
position: fixed;
|
|
3149
|
+
top: 0;
|
|
3150
|
+
left: 0;
|
|
2827
3151
|
pointer-events: none;
|
|
2828
3152
|
border-radius: 5px;
|
|
2829
3153
|
background: color-mix(in srgb, var(--rf-accent) 10%, transparent);
|
|
2830
3154
|
box-shadow: 0 0 0 2px var(--rf-accent), 0 0 0 5px color-mix(in srgb, var(--rf-accent) 18%, transparent);
|
|
2831
|
-
|
|
3155
|
+
will-change: translate;
|
|
3156
|
+
transition: translate 130ms var(--rf-ease);
|
|
2832
3157
|
}
|
|
3158
|
+
.picker-box[data-pinned="true"] { transition: none; }
|
|
2833
3159
|
.picker-label {
|
|
2834
3160
|
position: fixed;
|
|
3161
|
+
top: 0;
|
|
3162
|
+
left: 0;
|
|
2835
3163
|
pointer-events: none;
|
|
2836
3164
|
display: inline-flex;
|
|
2837
3165
|
align-items: center;
|
|
@@ -2841,45 +3169,278 @@ summary::-webkit-details-marker { display: none; }
|
|
|
2841
3169
|
padding: 0 8px;
|
|
2842
3170
|
font-size: 11.5px;
|
|
2843
3171
|
font-weight: 500;
|
|
2844
|
-
color:
|
|
3172
|
+
color: var(--rf-accent-text);
|
|
2845
3173
|
background: var(--rf-accent);
|
|
2846
3174
|
border-radius: 6px;
|
|
2847
|
-
box-shadow:
|
|
3175
|
+
box-shadow: var(--rf-shadow-float);
|
|
2848
3176
|
white-space: nowrap;
|
|
2849
3177
|
overflow: hidden;
|
|
2850
|
-
|
|
3178
|
+
will-change: translate;
|
|
3179
|
+
transition: translate 130ms var(--rf-ease);
|
|
3180
|
+
}
|
|
3181
|
+
.picker-label strong { flex: none; font-weight: 600; }
|
|
3182
|
+
.picker-label span { min-width: 0; overflow: hidden; text-overflow: ellipsis; opacity: 0.75; }
|
|
3183
|
+
.selection-outline {
|
|
3184
|
+
position: fixed;
|
|
3185
|
+
top: 0;
|
|
3186
|
+
left: 0;
|
|
3187
|
+
pointer-events: none;
|
|
3188
|
+
border-radius: 5px;
|
|
3189
|
+
background: color-mix(in srgb, var(--rf-accent) 7%, transparent);
|
|
3190
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--rf-accent) 70%, transparent), 0 0 0 5px color-mix(in srgb, var(--rf-accent) 14%, transparent);
|
|
3191
|
+
transition: opacity 160ms var(--rf-ease);
|
|
3192
|
+
}
|
|
3193
|
+
.picker-hint {
|
|
3194
|
+
position: fixed;
|
|
3195
|
+
left: 50%;
|
|
3196
|
+
bottom: calc(24px + env(safe-area-inset-bottom));
|
|
3197
|
+
transform: translateX(-50%);
|
|
3198
|
+
display: flex;
|
|
3199
|
+
align-items: center;
|
|
3200
|
+
gap: 6px;
|
|
3201
|
+
max-width: calc(100vw - 24px);
|
|
3202
|
+
padding: 5px 6px 5px 12px;
|
|
3203
|
+
font-size: 12.5px;
|
|
3204
|
+
color: var(--rf-text);
|
|
3205
|
+
background: var(--rf-bg);
|
|
3206
|
+
border-radius: var(--rf-pill);
|
|
3207
|
+
box-shadow: var(--rf-ring), var(--rf-shadow);
|
|
3208
|
+
animation: rf-fade 240ms var(--rf-ease);
|
|
3209
|
+
}
|
|
3210
|
+
.picker-instruction { display: flex; align-items: center; gap: 6px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
3211
|
+
.picker-hint kbd {
|
|
3212
|
+
padding: 1px 5px;
|
|
3213
|
+
font: inherit;
|
|
3214
|
+
font-size: 11px;
|
|
3215
|
+
color: var(--rf-text-muted);
|
|
3216
|
+
border-radius: 4px;
|
|
3217
|
+
background: var(--rf-bg-subtle);
|
|
3218
|
+
box-shadow: var(--rf-shadow-inset);
|
|
3219
|
+
}
|
|
3220
|
+
.picker-cancel {
|
|
3221
|
+
display: inline-flex;
|
|
3222
|
+
align-items: center;
|
|
3223
|
+
height: 40px;
|
|
3224
|
+
padding: 0 14px;
|
|
3225
|
+
border-radius: var(--rf-pill);
|
|
3226
|
+
font-size: 12.5px;
|
|
3227
|
+
font-weight: 500;
|
|
3228
|
+
white-space: nowrap;
|
|
3229
|
+
color: var(--rf-text-muted);
|
|
3230
|
+
}
|
|
3231
|
+
.picker-cancel:hover { background: var(--rf-bg-hover); color: var(--rf-text); }
|
|
3232
|
+
.picker-note {
|
|
3233
|
+
position: fixed;
|
|
3234
|
+
z-index: ${REFLET_Z_INDEX.annotatorOverlay};
|
|
3235
|
+
display: flex;
|
|
3236
|
+
flex-direction: column;
|
|
3237
|
+
gap: 6px;
|
|
3238
|
+
width: 300px;
|
|
3239
|
+
max-width: calc(100vw - 24px);
|
|
3240
|
+
padding: 6px 6px 6px 8px;
|
|
3241
|
+
border-radius: 18px;
|
|
3242
|
+
animation: rf-in 180ms var(--rf-ease);
|
|
3243
|
+
}
|
|
3244
|
+
.picker-note:focus-within { outline: 2px solid var(--rf-accent); outline-offset: 2px; }
|
|
3245
|
+
.picker-note-target {
|
|
3246
|
+
display: flex;
|
|
3247
|
+
align-items: center;
|
|
3248
|
+
gap: 6px;
|
|
3249
|
+
margin: 0;
|
|
3250
|
+
padding: 2px 4px 0;
|
|
3251
|
+
font-size: 11px;
|
|
3252
|
+
color: var(--rf-text-muted);
|
|
3253
|
+
white-space: nowrap;
|
|
3254
|
+
}
|
|
3255
|
+
.picker-note-target strong { flex: none; font-weight: 600; color: var(--rf-accent); }
|
|
3256
|
+
.picker-note-target span { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
3257
|
+
.picker-note textarea {
|
|
3258
|
+
width: 100%;
|
|
3259
|
+
min-height: 46px;
|
|
3260
|
+
max-height: min(160px, 30dvh);
|
|
3261
|
+
margin: 0;
|
|
3262
|
+
padding: 7px 6px 0;
|
|
3263
|
+
border: 0;
|
|
3264
|
+
resize: none;
|
|
3265
|
+
background: transparent;
|
|
3266
|
+
color: var(--rf-text);
|
|
3267
|
+
font-size: 13px;
|
|
3268
|
+
line-height: 19px;
|
|
3269
|
+
}
|
|
3270
|
+
.picker-note textarea:focus { outline: none; }
|
|
3271
|
+
.picker-note textarea::placeholder { color: var(--rf-text-muted); }
|
|
3272
|
+
.picker-note-actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
3273
|
+
@media (pointer: coarse) {
|
|
3274
|
+
.picker-hint { padding: 6px 8px 6px 14px; }
|
|
3275
|
+
.picker-hint kbd { display: none; }
|
|
3276
|
+
.picker-cancel { height: 44px; padding: 0 18px; font-size: 14px; }
|
|
3277
|
+
.picker-note textarea { font-size: 16px; min-height: 56px; }
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
`;
|
|
3281
|
+
//#endregion
|
|
3282
|
+
//#region src/feedback/ui/styles.ts
|
|
3283
|
+
const WIDGET_STYLES = `
|
|
3284
|
+
:host {
|
|
3285
|
+
--rf-offset: 20px;
|
|
3286
|
+
--rf-bg: #ffffff;
|
|
3287
|
+
--rf-bg-subtle: rgb(55 53 47 / 4%);
|
|
3288
|
+
--rf-bg-hover: rgb(55 53 47 / 7%);
|
|
3289
|
+
--rf-border: rgb(55 53 47 / 9%);
|
|
3290
|
+
--rf-hairline: rgb(15 15 15 / 10%);
|
|
3291
|
+
--rf-text: #37352f;
|
|
3292
|
+
--rf-text-muted: rgb(55 53 47 / 68%);
|
|
3293
|
+
--rf-primary: #37352f;
|
|
3294
|
+
--rf-primary-text: #ffffff;
|
|
3295
|
+
--rf-accent: #2383e2;
|
|
3296
|
+
--rf-accent-text: #ffffff;
|
|
3297
|
+
--rf-danger: #d44c47;
|
|
3298
|
+
--rf-success: #448361;
|
|
3299
|
+
--rf-swatch-base: #ffffff;
|
|
3300
|
+
--rf-spectrum: conic-gradient(#ff5757, #ffde59, #6bdb78, #4ac9fa, #8b5cf6, #ef73dc, #ff5757);
|
|
3301
|
+
--rf-radius: 20px;
|
|
3302
|
+
--rf-pill: 999px;
|
|
3303
|
+
--rf-attachment-height: 78px;
|
|
3304
|
+
--rf-width: 360px;
|
|
3305
|
+
--rf-glass: rgb(255 255 255 / 72%);
|
|
3306
|
+
--rf-glass-highlight: rgb(255 255 255 / 90%);
|
|
3307
|
+
--rf-glass-stroke: rgb(255 255 255 / 70%);
|
|
3308
|
+
--rf-radius-sm: 8px;
|
|
3309
|
+
--rf-shadow-inset: inset 0 0 0 1px var(--rf-border);
|
|
3310
|
+
--rf-ring: 0 0 0 1px rgb(15 15 15 / 4%);
|
|
3311
|
+
--rf-shadow-low: 0 1px 2px rgb(15 15 15 / 6%), 0 4px 10px -4px rgb(15 15 15 / 10%);
|
|
3312
|
+
--rf-shadow: 0 2px 5px -1px rgb(15 15 15 / 7%), 0 12px 32px -8px rgb(15 15 15 / 16%);
|
|
3313
|
+
--rf-shadow-float: 0 4px 12px -3px rgb(15 15 15 / 35%);
|
|
3314
|
+
--rf-ease: cubic-bezier(0.16, 1, 0.3, 1);
|
|
3315
|
+
color-scheme: light;
|
|
3316
|
+
all: initial;
|
|
3317
|
+
font-family: var(--rf-font, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
:host([data-theme="dark"]) {
|
|
3321
|
+
--rf-bg: #202020;
|
|
3322
|
+
--rf-bg-subtle: rgb(255 255 255 / 5%);
|
|
3323
|
+
--rf-bg-hover: rgb(255 255 255 / 9%);
|
|
3324
|
+
--rf-border: rgb(255 255 255 / 9%);
|
|
3325
|
+
--rf-hairline: rgb(255 255 255 / 12%);
|
|
3326
|
+
--rf-text: #ebeae8;
|
|
3327
|
+
--rf-text-muted: rgb(255 255 255 / 68%);
|
|
3328
|
+
--rf-glass: rgb(32 32 32 / 78%);
|
|
3329
|
+
--rf-glass-highlight: rgb(255 255 255 / 14%);
|
|
3330
|
+
--rf-glass-stroke: rgb(255 255 255 / 12%);
|
|
3331
|
+
--rf-primary: #f1efec;
|
|
3332
|
+
--rf-primary-text: #1c1c1b;
|
|
3333
|
+
--rf-accent: #529cca;
|
|
3334
|
+
--rf-danger: #eb5757;
|
|
3335
|
+
--rf-success: #4dab74;
|
|
3336
|
+
--rf-ring: 0 0 0 1px rgb(255 255 255 / 7%);
|
|
3337
|
+
--rf-shadow-low: 0 1px 2px rgb(0 0 0 / 30%), 0 4px 10px -4px rgb(0 0 0 / 40%);
|
|
3338
|
+
--rf-shadow: 0 2px 5px -1px rgb(0 0 0 / 35%), 0 14px 36px -10px rgb(0 0 0 / 55%);
|
|
3339
|
+
--rf-shadow-float: 0 4px 14px -3px rgb(0 0 0 / 55%);
|
|
3340
|
+
color-scheme: dark;
|
|
2851
3341
|
}
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
3342
|
+
|
|
3343
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
3344
|
+
|
|
3345
|
+
|
|
3346
|
+
.root {
|
|
2855
3347
|
position: fixed;
|
|
2856
|
-
|
|
2857
|
-
bottom: 24px;
|
|
2858
|
-
transform: translateX(-50%);
|
|
3348
|
+
z-index: ${REFLET_Z_INDEX.widgetRoot};
|
|
2859
3349
|
display: flex;
|
|
2860
|
-
align-items:
|
|
2861
|
-
gap:
|
|
2862
|
-
|
|
2863
|
-
font-size: 12.5px;
|
|
3350
|
+
align-items: flex-end;
|
|
3351
|
+
gap: 8px;
|
|
3352
|
+
font: 13px/1.45 var(--rf-font, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif);
|
|
2864
3353
|
color: var(--rf-text);
|
|
3354
|
+
}
|
|
3355
|
+
.root:has(.panel) { width: min(var(--rf-width), calc(var(--rf-viewport-width, 100vw) - 24px)); }
|
|
3356
|
+
.root[data-position$="right"] { right: calc(var(--rf-viewport-right, 0px) + max(12px, min(var(--rf-offset), calc((var(--rf-viewport-width, 100vw) - var(--rf-width)) / 2)))); }
|
|
3357
|
+
.root[data-position$="left"] { left: calc(var(--rf-viewport-left, 0px) + max(12px, min(var(--rf-offset), calc((var(--rf-viewport-width, 100vw) - var(--rf-width)) / 2)))); }
|
|
3358
|
+
.root[data-position^="bottom"] { bottom: calc(var(--rf-viewport-bottom, 0px) + max(var(--rf-offset), env(safe-area-inset-bottom))); }
|
|
3359
|
+
.root[data-position^="top"] { top: calc(var(--rf-viewport-top, 0px) + max(var(--rf-offset), env(safe-area-inset-top))); align-items: flex-start; }
|
|
3360
|
+
.root[data-position="bottom-right"] .panel { transform-origin: bottom right; }
|
|
3361
|
+
.root[data-position="bottom-left"] .panel { transform-origin: bottom left; }
|
|
3362
|
+
.root[data-position^="top"] .panel { transform-origin: top center; }
|
|
3363
|
+
.root[data-editing="true"] { visibility: hidden; pointer-events: none; }
|
|
3364
|
+
button, select, input, textarea { font: inherit; color: inherit; }
|
|
3365
|
+
button { margin: 0; cursor: pointer; border: 0; background: none; transition: scale 120ms var(--rf-ease); }
|
|
3366
|
+
button:disabled { cursor: not-allowed; opacity: .4; }
|
|
3367
|
+
button:active:not(:disabled) { scale: .97; }
|
|
3368
|
+
svg { flex-shrink: 0; }
|
|
3369
|
+
:focus-visible { outline: 2px solid var(--rf-accent); outline-offset: 3px; }
|
|
3370
|
+
.icon-btn, .tool {
|
|
3371
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
3372
|
+
width: 40px; height: 40px; flex: none; border-radius: var(--rf-pill); color: var(--rf-text-muted);
|
|
3373
|
+
transition: background 160ms var(--rf-ease), color 160ms var(--rf-ease), scale 120ms var(--rf-ease);
|
|
3374
|
+
}
|
|
3375
|
+
.icon-btn:hover, .tool:hover { background: var(--rf-bg-hover); color: var(--rf-text); }
|
|
3376
|
+
.launcher {
|
|
3377
|
+
display: inline-flex; align-items: center; gap: 7px; height: 40px; padding: 0 15px;
|
|
3378
|
+
border-radius: var(--rf-pill); color: var(--rf-text);
|
|
3379
|
+
font-weight: 500;
|
|
3380
|
+
transition: transform 200ms var(--rf-ease), box-shadow 200ms var(--rf-ease), scale 120ms var(--rf-ease);
|
|
3381
|
+
}
|
|
3382
|
+
.launcher:hover { transform: translateY(-2px); box-shadow: var(--rf-shadow); }
|
|
3383
|
+
.launcher-icon { display: flex; width: 15px; height: 15px; }
|
|
3384
|
+
.launcher-icon .on-close { display: none; }
|
|
3385
|
+
.floating-controls {
|
|
3386
|
+
display: flex; align-items: center; flex: none;
|
|
3387
|
+
margin-right: auto; padding-left: 3px;
|
|
3388
|
+
animation: rf-in 220ms var(--rf-ease);
|
|
3389
|
+
}
|
|
3390
|
+
.drag-handle { touch-action: none; cursor: grab; }
|
|
3391
|
+
.drag-handle:active { cursor: grabbing; }
|
|
3392
|
+
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
3393
|
+
.ghost-btn { display: flex; align-items: center; gap: 8px; min-height: 40px; padding: 0 8px; border-radius: var(--rf-radius-sm); }
|
|
3394
|
+
.ghost-btn:hover { background: var(--rf-bg-hover); }
|
|
3395
|
+
.error { margin: 0; padding: 10px 14px; border-radius: var(--rf-radius); font-size: 12px; color: var(--rf-danger); }
|
|
3396
|
+
.done { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 24px; text-align: center; }
|
|
3397
|
+
.done svg { color: var(--rf-success); }
|
|
3398
|
+
.done h2 { margin: 0; font-size: 14px; font-weight: 600; }
|
|
3399
|
+
.done p { margin: 0; font-size: 12px; color: var(--rf-text-muted); }
|
|
3400
|
+
.spinner { width: 14px; height: 14px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: rf-spin 700ms linear infinite; }
|
|
3401
|
+
.hp { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
|
3402
|
+
@keyframes rf-spin { to { transform: rotate(360deg); } }
|
|
3403
|
+
@keyframes rf-in { from { opacity: 0; transform: translateY(5px) scale(.98); } }
|
|
3404
|
+
@keyframes rf-fade { from { opacity: 0; } }
|
|
3405
|
+
@media (pointer: coarse) {
|
|
3406
|
+
.icon-btn, .tool { width: 44px; height: 44px; }
|
|
3407
|
+
.launcher, .ghost-btn { min-height: 44px; }
|
|
3408
|
+
}
|
|
3409
|
+
.glass {
|
|
3410
|
+
position: relative;
|
|
3411
|
+
isolation: isolate;
|
|
3412
|
+
border: 1px solid var(--rf-glass-stroke);
|
|
3413
|
+
box-shadow: inset 0 1px 0 var(--rf-glass-highlight), var(--rf-shadow);
|
|
3414
|
+
}
|
|
3415
|
+
.glass::before {
|
|
3416
|
+
content: "";
|
|
3417
|
+
position: absolute;
|
|
3418
|
+
inset: 0;
|
|
3419
|
+
z-index: -1;
|
|
3420
|
+
border-radius: inherit;
|
|
2865
3421
|
background: var(--rf-bg);
|
|
2866
|
-
|
|
2867
|
-
box-shadow: var(--rf-shadow);
|
|
2868
|
-
animation: rf-fade 240ms var(--rf-ease);
|
|
3422
|
+
pointer-events: none;
|
|
2869
3423
|
}
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
background: var(--rf-bg-subtle);
|
|
2877
|
-
box-shadow: var(--rf-shadow-inset);
|
|
3424
|
+
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
|
|
3425
|
+
.glass::before {
|
|
3426
|
+
background: var(--rf-glass);
|
|
3427
|
+
-webkit-backdrop-filter: blur(20px) saturate(1.6);
|
|
3428
|
+
backdrop-filter: blur(20px) saturate(1.6);
|
|
3429
|
+
}
|
|
2878
3430
|
}
|
|
2879
|
-
|
|
2880
|
-
|
|
3431
|
+
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
|
|
3432
|
+
.glass::before { background: var(--rf-bg); backdrop-filter: none; -webkit-backdrop-filter: none; }
|
|
3433
|
+
}
|
|
3434
|
+
.capture-halo { position: fixed; inset: 0; z-index: ${REFLET_Z_INDEX.captureHalo}; pointer-events: none; opacity: 0; transition: opacity 500ms var(--rf-ease); }
|
|
3435
|
+
.capture-halo[data-active="true"] { opacity: 1; transition-duration: 140ms; }
|
|
3436
|
+
.capture-halo::before { content: ""; position: absolute; inset: 0; border-radius: 20px; box-shadow: inset 14px 0 36px -16px var(--rf-accent), inset -14px 0 36px -16px var(--rf-accent), inset 0 0 4px 1px color-mix(in srgb, var(--rf-accent) 35%, transparent); }
|
|
3437
|
+
.capture-halo::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, color-mix(in srgb, var(--rf-accent) 20%, transparent), transparent 5%, transparent 95%, color-mix(in srgb, var(--rf-accent) 20%, transparent)); }
|
|
3438
|
+
${COMPOSER_STYLES}
|
|
3439
|
+
${ANNOTATOR_STYLES}
|
|
3440
|
+
${PICKER_STYLES}
|
|
2881
3441
|
@media (prefers-reduced-motion: reduce) {
|
|
2882
3442
|
*, *::before, *::after { animation: none !important; transition: none !important; }
|
|
3443
|
+
button:active:not(:disabled) { scale: 1; }
|
|
2883
3444
|
}
|
|
2884
3445
|
`;
|
|
2885
3446
|
//#endregion
|
|
@@ -3007,140 +3568,18 @@ function startConsoleRecorder(options) {
|
|
|
3007
3568
|
};
|
|
3008
3569
|
}
|
|
3009
3570
|
//#endregion
|
|
3010
|
-
//#region src/feedback/core/element-capture.ts
|
|
3011
|
-
const MIN_CONTEXT_PADDING = 24;
|
|
3012
|
-
const MAX_CONTEXT_WIDTH = 2e3;
|
|
3013
|
-
const CLOSE_UP_MAX_WIDTH = 1200;
|
|
3014
|
-
const SELECTION_COLOR = "#4f46e5";
|
|
3015
|
-
const OUTLINE_COLOR = "#4f46e5";
|
|
3016
|
-
const OUTLINE_GLOW = "#ffffff";
|
|
3017
|
-
const OUTLINE_WIDTH = 3;
|
|
3018
|
-
const GLOW_WIDTH = 6;
|
|
3019
|
-
const DIM_OVERLAY = "rgba(15, 23, 42, 0.45)";
|
|
3020
|
-
/**
|
|
3021
|
-
* Tightest ancestor that keeps some breathing room around the pick — the zoom
|
|
3022
|
-
* stays zoomed instead of growing to a giant section. Only when nothing up
|
|
3023
|
-
* the tree clears the minimum does the widest candidate below the cap win.
|
|
3024
|
-
*/
|
|
3025
|
-
function contextRootFor(element) {
|
|
3026
|
-
const target = element.getBoundingClientRect();
|
|
3027
|
-
const coversAtLeast = (box, margin) => box.left <= target.left - margin && box.right >= target.right + margin && box.top <= target.top - margin && box.bottom >= target.bottom + margin;
|
|
3028
|
-
let tightest = null;
|
|
3029
|
-
let current = element.parentElement;
|
|
3030
|
-
while (current && current !== document.documentElement) {
|
|
3031
|
-
const box = current.getBoundingClientRect();
|
|
3032
|
-
if (coversAtLeast(box, MIN_CONTEXT_PADDING)) return current;
|
|
3033
|
-
tightest ??= box.width < MAX_CONTEXT_WIDTH ? current : null;
|
|
3034
|
-
current = current.parentElement;
|
|
3035
|
-
}
|
|
3036
|
-
return tightest ?? element;
|
|
3037
|
-
}
|
|
3038
|
-
/** Where the picked element sits inside the context shot, in image pixels. */
|
|
3039
|
-
function closeUpGeometry(params) {
|
|
3040
|
-
const scale = params.rootRect.width > 0 ? params.capture.width / params.rootRect.width : 1;
|
|
3041
|
-
return {
|
|
3042
|
-
height: Math.round(params.elementRect.height * scale),
|
|
3043
|
-
scale,
|
|
3044
|
-
width: Math.round(params.elementRect.width * scale),
|
|
3045
|
-
x: Math.round((params.elementRect.left - params.rootRect.left) * scale),
|
|
3046
|
-
y: Math.round((params.elementRect.top - params.rootRect.top) * scale)
|
|
3047
|
-
};
|
|
3048
|
-
}
|
|
3049
|
-
/** Rectangle annotation marking the picked element on the viewport shot. */
|
|
3050
|
-
function highlightFor(selection, capture) {
|
|
3051
|
-
const scale = capture.width / window.innerWidth;
|
|
3052
|
-
const { rect } = selection;
|
|
3053
|
-
return {
|
|
3054
|
-
color: SELECTION_COLOR,
|
|
3055
|
-
end: {
|
|
3056
|
-
x: (rect.x + rect.width) * scale,
|
|
3057
|
-
y: (rect.y + rect.height) * scale
|
|
3058
|
-
},
|
|
3059
|
-
id: `selection-${rect.x}-${rect.y}`,
|
|
3060
|
-
start: {
|
|
3061
|
-
x: rect.x * scale,
|
|
3062
|
-
y: rect.y * scale
|
|
3063
|
-
},
|
|
3064
|
-
tool: "rectangle"
|
|
3065
|
-
};
|
|
3066
|
-
}
|
|
3067
|
-
function drawZoneOverlay(context, zone) {
|
|
3068
|
-
context.save();
|
|
3069
|
-
context.fillStyle = DIM_OVERLAY;
|
|
3070
|
-
context.beginPath();
|
|
3071
|
-
context.rect(0, 0, context.canvas.width, context.canvas.height);
|
|
3072
|
-
context.rect(zone.x, zone.y, zone.width, zone.height);
|
|
3073
|
-
context.fill("evenodd");
|
|
3074
|
-
for (const [color, width] of [[OUTLINE_GLOW, GLOW_WIDTH], [OUTLINE_COLOR, OUTLINE_WIDTH]]) {
|
|
3075
|
-
context.strokeStyle = color;
|
|
3076
|
-
context.lineWidth = width * zone.scale;
|
|
3077
|
-
context.strokeRect(zone.x, zone.y, zone.width, zone.height);
|
|
3078
|
-
}
|
|
3079
|
-
context.restore();
|
|
3080
|
-
}
|
|
3081
|
-
function drawCloseUp(capture, zone) {
|
|
3082
|
-
return new Promise((resolve) => {
|
|
3083
|
-
const canvas = document.createElement("canvas");
|
|
3084
|
-
canvas.width = capture.width;
|
|
3085
|
-
canvas.height = capture.height;
|
|
3086
|
-
const context = canvas.getContext("2d");
|
|
3087
|
-
const image = new Image();
|
|
3088
|
-
image.onload = () => {
|
|
3089
|
-
if (!context) {
|
|
3090
|
-
resolve(null);
|
|
3091
|
-
return;
|
|
3092
|
-
}
|
|
3093
|
-
context.drawImage(image, 0, 0, capture.width, capture.height);
|
|
3094
|
-
drawZoneOverlay(context, zone);
|
|
3095
|
-
resolve(capturedFromCanvas(canvas, CLOSE_UP_MAX_WIDTH));
|
|
3096
|
-
};
|
|
3097
|
-
image.onerror = () => resolve(null);
|
|
3098
|
-
image.src = capture.objectUrl;
|
|
3099
|
-
});
|
|
3100
|
-
}
|
|
3101
|
-
/**
|
|
3102
|
-
* Zoomed view of the picked zone: the surrounding area captured for context,
|
|
3103
|
-
* everything outside the selection dimmed and the zone framed with a glowing
|
|
3104
|
-
* outline — unambiguous to eyes and vision models alike.
|
|
3105
|
-
* Falls back to the plain context shot when the overlay cannot be drawn.
|
|
3106
|
-
*/
|
|
3107
|
-
async function captureElementCloseUp(element) {
|
|
3108
|
-
try {
|
|
3109
|
-
const root = contextRootFor(element);
|
|
3110
|
-
const rootRect = root.getBoundingClientRect();
|
|
3111
|
-
const elementRect = element.getBoundingClientRect();
|
|
3112
|
-
const capture = await captureViewport({
|
|
3113
|
-
element: root,
|
|
3114
|
-
maxWidth: CLOSE_UP_MAX_WIDTH
|
|
3115
|
-
});
|
|
3116
|
-
if (!capture || root === element) return capture;
|
|
3117
|
-
const outlined = await drawCloseUp(capture, closeUpGeometry({
|
|
3118
|
-
capture,
|
|
3119
|
-
elementRect,
|
|
3120
|
-
rootRect
|
|
3121
|
-
}));
|
|
3122
|
-
if (outlined) {
|
|
3123
|
-
releaseCapture(capture);
|
|
3124
|
-
return outlined;
|
|
3125
|
-
}
|
|
3126
|
-
return capture;
|
|
3127
|
-
} catch {
|
|
3128
|
-
return null;
|
|
3129
|
-
}
|
|
3130
|
-
}
|
|
3131
|
-
//#endregion
|
|
3132
3571
|
//#region src/feedback/core/submission/upload-screenshots.ts
|
|
3133
3572
|
async function uploadImage(transport, image) {
|
|
3134
3573
|
const { uploadUrl } = await transport.getScreenshotUploadUrl();
|
|
3135
3574
|
return transport.uploadImage(uploadUrl, image);
|
|
3136
3575
|
}
|
|
3137
3576
|
async function prepareScreenshot(transport, screenshot, index) {
|
|
3138
|
-
const { image, annotations, context } = screenshot;
|
|
3577
|
+
const { closeUp, image, annotations, context } = screenshot;
|
|
3139
3578
|
const annotated = annotations.length > 0 ? await renderAnnotatedImage(image, annotations) : null;
|
|
3140
3579
|
if (annotations.length > 0 && !annotated) throw new Error("Could not render your annotations. Please try again.");
|
|
3141
3580
|
try {
|
|
3142
3581
|
const storageId = await uploadImage(transport, image);
|
|
3143
|
-
|
|
3582
|
+
const prepared = [{
|
|
3144
3583
|
annotatedStorageId: annotated ? await uploadImage(transport, annotated) : void 0,
|
|
3145
3584
|
annotations: annotations.length > 0 ? toWireAnnotations(annotations) : void 0,
|
|
3146
3585
|
captureSource: "widget",
|
|
@@ -3151,28 +3590,27 @@ async function prepareScreenshot(transport, screenshot, index) {
|
|
|
3151
3590
|
size: image.blob.size,
|
|
3152
3591
|
storageId,
|
|
3153
3592
|
width: image.width
|
|
3154
|
-
};
|
|
3593
|
+
}];
|
|
3594
|
+
if (closeUp) prepared.push({
|
|
3595
|
+
captureSource: "element",
|
|
3596
|
+
filename: `element-${index + 1}.png`,
|
|
3597
|
+
height: closeUp.height,
|
|
3598
|
+
mimeType: closeUp.mimeType,
|
|
3599
|
+
pageUrl: context.url,
|
|
3600
|
+
size: closeUp.blob.size,
|
|
3601
|
+
storageId: await uploadImage(transport, closeUp),
|
|
3602
|
+
width: closeUp.width
|
|
3603
|
+
});
|
|
3604
|
+
return prepared;
|
|
3155
3605
|
} finally {
|
|
3156
3606
|
releaseCapture(annotated);
|
|
3157
3607
|
}
|
|
3158
3608
|
}
|
|
3159
3609
|
async function uploadScreenshots(transport, submission) {
|
|
3160
|
-
|
|
3610
|
+
return (await Promise.allSettled(submission.screenshots.map((screenshot, index) => prepareScreenshot(transport, screenshot, index)))).flatMap((result) => {
|
|
3161
3611
|
if (result.status === "rejected") throw result.reason;
|
|
3162
3612
|
return result.value;
|
|
3163
3613
|
});
|
|
3164
|
-
const element = submission.element;
|
|
3165
|
-
if (element) prepared.push({
|
|
3166
|
-
captureSource: "element",
|
|
3167
|
-
filename: "element.png",
|
|
3168
|
-
height: element.height,
|
|
3169
|
-
mimeType: element.mimeType,
|
|
3170
|
-
pageUrl: submission.context.url,
|
|
3171
|
-
size: element.blob.size,
|
|
3172
|
-
storageId: await uploadImage(transport, element),
|
|
3173
|
-
width: element.width
|
|
3174
|
-
});
|
|
3175
|
-
return prepared;
|
|
3176
3614
|
}
|
|
3177
3615
|
//#endregion
|
|
3178
3616
|
//#region src/feedback/core/submit.ts
|
|
@@ -3186,10 +3624,9 @@ function deriveTitle(message) {
|
|
|
3186
3624
|
return `${(lastSpace > 0 ? clipped.slice(0, lastSpace) : clipped).trimEnd()}…`;
|
|
3187
3625
|
}
|
|
3188
3626
|
function buildDescription(params) {
|
|
3189
|
-
const {
|
|
3190
|
-
const label = category.charAt(0).toUpperCase() + category.slice(1);
|
|
3627
|
+
const { email, isAnonymous, message } = params;
|
|
3191
3628
|
const contact = isAnonymous && email ? `\n\n---\nContact: ${email}` : "";
|
|
3192
|
-
return
|
|
3629
|
+
return `${message.trim()}${contact}`;
|
|
3193
3630
|
}
|
|
3194
3631
|
async function attachPreparedScreenshots(transport, result) {
|
|
3195
3632
|
const results = await Promise.allSettled(result.pendingScreenshots.map((screenshot) => transport.saveScreenshot({
|
|
@@ -3293,36 +3730,28 @@ function useFeedbackSubmission(options) {
|
|
|
3293
3730
|
};
|
|
3294
3731
|
}
|
|
3295
3732
|
//#endregion
|
|
3296
|
-
//#region src/feedback/ui/state/use-owned-capture.ts
|
|
3297
|
-
function useOwnedCapture() {
|
|
3298
|
-
const [image, setImage] = useState(null);
|
|
3299
|
-
const ownedImage = useRef(null);
|
|
3300
|
-
useEffect(() => () => releaseCapture(ownedImage.current), []);
|
|
3301
|
-
return [image, useCallback((nextImage) => {
|
|
3302
|
-
releaseCapture(ownedImage.current);
|
|
3303
|
-
ownedImage.current = nextImage;
|
|
3304
|
-
setImage(nextImage);
|
|
3305
|
-
}, [])];
|
|
3306
|
-
}
|
|
3307
|
-
//#endregion
|
|
3308
3733
|
//#region src/feedback/ui/state/use-screenshot-drafts.ts
|
|
3734
|
+
function releaseDraft(draft) {
|
|
3735
|
+
releaseCapture(draft?.image);
|
|
3736
|
+
releaseCapture(draft?.closeUp);
|
|
3737
|
+
}
|
|
3309
3738
|
function useScreenshotDrafts(captureFailed, onError) {
|
|
3310
3739
|
const [screenshots, setScreenshots] = useState([]);
|
|
3311
3740
|
const [activeId, setActiveId] = useState(null);
|
|
3312
3741
|
const [pendingCapture, setPendingCapture] = useState(null);
|
|
3313
|
-
const
|
|
3742
|
+
const ownedDrafts = useRef(/* @__PURE__ */ new Map());
|
|
3314
3743
|
const captureRequest = useRef(null);
|
|
3315
3744
|
useEffect(() => {
|
|
3316
|
-
const
|
|
3745
|
+
const drafts = ownedDrafts.current;
|
|
3317
3746
|
return () => {
|
|
3318
3747
|
captureRequest.current = null;
|
|
3319
|
-
for (const
|
|
3320
|
-
|
|
3748
|
+
for (const draft of drafts.values()) releaseDraft(draft);
|
|
3749
|
+
drafts.clear();
|
|
3321
3750
|
};
|
|
3322
3751
|
}, []);
|
|
3323
3752
|
const storeScreenshot = useCallback((screenshot) => {
|
|
3324
|
-
|
|
3325
|
-
|
|
3753
|
+
releaseDraft(ownedDrafts.current.get(screenshot.id));
|
|
3754
|
+
ownedDrafts.current.set(screenshot.id, screenshot);
|
|
3326
3755
|
setScreenshots((previous) => {
|
|
3327
3756
|
return previous.some(({ id }) => id === screenshot.id) ? previous.map((item) => item.id === screenshot.id ? screenshot : item) : [...previous, screenshot];
|
|
3328
3757
|
});
|
|
@@ -3367,27 +3796,27 @@ function useScreenshotDrafts(captureFailed, onError) {
|
|
|
3367
3796
|
}, [takeScreenshot]);
|
|
3368
3797
|
const refreshAutomatic = useCallback(() => {
|
|
3369
3798
|
takeScreenshot({
|
|
3370
|
-
id:
|
|
3799
|
+
id: ownedDrafts.current.keys().next().value ?? captureRequest.current?.id ?? crypto.randomUUID(),
|
|
3371
3800
|
source: "automatic"
|
|
3372
3801
|
});
|
|
3373
3802
|
}, [takeScreenshot]);
|
|
3374
3803
|
const retakeCapture = useCallback((id) => {
|
|
3375
|
-
if (!captureRequest.current &&
|
|
3804
|
+
if (!captureRequest.current && ownedDrafts.current.has(id)) takeScreenshot({
|
|
3376
3805
|
id,
|
|
3377
3806
|
source: "manual"
|
|
3378
3807
|
});
|
|
3379
3808
|
}, [takeScreenshot]);
|
|
3380
3809
|
const removeCapture = useCallback((id) => {
|
|
3381
3810
|
if (captureRequest.current?.id === id) cancelPendingCapture();
|
|
3382
|
-
|
|
3383
|
-
|
|
3811
|
+
releaseDraft(ownedDrafts.current.get(id));
|
|
3812
|
+
ownedDrafts.current.delete(id);
|
|
3384
3813
|
setScreenshots((previous) => previous.filter((item) => item.id !== id));
|
|
3385
3814
|
setActiveId((current) => current === id ? null : current);
|
|
3386
3815
|
}, [cancelPendingCapture]);
|
|
3387
3816
|
const resetScreenshots = useCallback(() => {
|
|
3388
3817
|
cancelPendingCapture();
|
|
3389
|
-
for (const
|
|
3390
|
-
|
|
3818
|
+
for (const draft of ownedDrafts.current.values()) releaseDraft(draft);
|
|
3819
|
+
ownedDrafts.current.clear();
|
|
3391
3820
|
setScreenshots([]);
|
|
3392
3821
|
setActiveId(null);
|
|
3393
3822
|
}, [cancelPendingCapture]);
|
|
@@ -3397,16 +3826,18 @@ function useScreenshotDrafts(captureFailed, onError) {
|
|
|
3397
3826
|
annotations
|
|
3398
3827
|
} : item));
|
|
3399
3828
|
}, [activeId]);
|
|
3400
|
-
const
|
|
3401
|
-
setScreenshots((previous) => previous.map((item) =>
|
|
3829
|
+
const setSelectionComment = useCallback((id, comment) => {
|
|
3830
|
+
setScreenshots((previous) => previous.map((item) => item.id === id && item.selection ? {
|
|
3402
3831
|
...item,
|
|
3403
|
-
|
|
3404
|
-
|
|
3832
|
+
selection: {
|
|
3833
|
+
...item.selection,
|
|
3834
|
+
comment
|
|
3835
|
+
}
|
|
3836
|
+
} : item));
|
|
3405
3837
|
}, []);
|
|
3406
3838
|
return {
|
|
3407
3839
|
activeScreenshot: screenshots.find(({ id }) => id === activeId) ?? screenshots.at(-1) ?? null,
|
|
3408
3840
|
cancelPendingCapture,
|
|
3409
|
-
clearSelectionAnnotations,
|
|
3410
3841
|
pendingCapture,
|
|
3411
3842
|
refreshAutomatic,
|
|
3412
3843
|
removeCapture,
|
|
@@ -3415,16 +3846,13 @@ function useScreenshotDrafts(captureFailed, onError) {
|
|
|
3415
3846
|
screenshots,
|
|
3416
3847
|
selectScreenshot: setActiveId,
|
|
3417
3848
|
setAnnotations,
|
|
3849
|
+
setSelectionComment,
|
|
3418
3850
|
storeScreenshot,
|
|
3419
3851
|
takeCapture
|
|
3420
3852
|
};
|
|
3421
3853
|
}
|
|
3422
3854
|
//#endregion
|
|
3423
3855
|
//#region src/feedback/ui/state/use-widget-config.ts
|
|
3424
|
-
function defaultCategory(props) {
|
|
3425
|
-
const preferred = props.defaultCategory ?? "bug";
|
|
3426
|
-
return props.categories?.find((category) => category === preferred) ?? props.categories?.[0] ?? preferred;
|
|
3427
|
-
}
|
|
3428
3856
|
function dismissalDuration(days) {
|
|
3429
3857
|
return days !== void 0 && Number.isFinite(days) && days > 0 ? days : null;
|
|
3430
3858
|
}
|
|
@@ -3446,7 +3874,6 @@ function useWidgetConfig(props) {
|
|
|
3446
3874
|
user,
|
|
3447
3875
|
userToken
|
|
3448
3876
|
]),
|
|
3449
|
-
defaultCategory: defaultCategory(props),
|
|
3450
3877
|
dismissalKey: `reflet-feedback-dismissed:${publicKey ?? "default"}:${user?.id ?? "anonymous"}`,
|
|
3451
3878
|
dismissForDays: dismissalDuration(props.dismissForDays),
|
|
3452
3879
|
isAnonymous: !(user || userToken)
|
|
@@ -3533,16 +3960,13 @@ function matchesHotkey(event, hotkey, platform = typeof navigator === "undefined
|
|
|
3533
3960
|
return event.altKey === expected.alt && event.ctrlKey === expected.ctrl && event.metaKey === expected.meta && event.shiftKey === expected.shift;
|
|
3534
3961
|
}
|
|
3535
3962
|
function useWidgetState(props) {
|
|
3536
|
-
const { client,
|
|
3963
|
+
const { client, dismissalKey, dismissForDays, isAnonymous } = useWidgetConfig(props);
|
|
3537
3964
|
const [isOpen, setIsOpen] = useState(false);
|
|
3538
3965
|
const [annotationTrigger, setAnnotationTrigger] = useState(null);
|
|
3539
3966
|
const [step, setStep] = useState("compose");
|
|
3540
|
-
const [category, setCategory] = useState(defaultCategory);
|
|
3541
3967
|
const [message, setMessage] = useState("");
|
|
3542
3968
|
const [email, setEmail] = useState("");
|
|
3543
3969
|
const [honeypot, setHoneypot] = useState("");
|
|
3544
|
-
const [elementCapture, replaceElementCapture] = useOwnedCapture();
|
|
3545
|
-
const [selection, setSelection] = useState(null);
|
|
3546
3970
|
const [isElementCapturing, setIsElementCapturing] = useState(false);
|
|
3547
3971
|
const successTimer = useRef(null);
|
|
3548
3972
|
const [error, setError] = useState(null);
|
|
@@ -3567,7 +3991,7 @@ function useWidgetState(props) {
|
|
|
3567
3991
|
}, [props.captureConsole]);
|
|
3568
3992
|
const captureFailed = props.labels?.captureFailed ?? DEFAULT_WIDGET_LABELS.captureFailed;
|
|
3569
3993
|
const gallery = useScreenshotDrafts(captureFailed, setError);
|
|
3570
|
-
const { activeScreenshot, cancelPendingCapture,
|
|
3994
|
+
const { activeScreenshot, cancelPendingCapture, refreshAutomatic, resetScreenshots, storeScreenshot } = gallery;
|
|
3571
3995
|
const capture = activeScreenshot?.image ?? null;
|
|
3572
3996
|
const annotations = activeScreenshot?.annotations ?? [];
|
|
3573
3997
|
const isCapturing = gallery.pendingCapture !== null || isElementCapturing;
|
|
@@ -3584,7 +4008,7 @@ function useWidgetState(props) {
|
|
|
3584
4008
|
const { resetSubmission } = submission;
|
|
3585
4009
|
const firstCapture = gallery.pendingCapture ?? gallery.screenshots[0];
|
|
3586
4010
|
const canRefreshAutomatically = gallery.screenshots.length <= 1 && firstCapture?.source === "automatic";
|
|
3587
|
-
useCaptureSync(isOpen && !submission.isEditingDisabled && step === "compose" && canRefreshAutomatically && annotations.length === 0 && !selection && props.captureOnOpen !== false, refreshAutomatic);
|
|
4011
|
+
useCaptureSync(isOpen && !submission.isEditingDisabled && step === "compose" && canRefreshAutomatically && annotations.length === 0 && !gallery.screenshots.some((draft) => draft.selection) && props.captureOnOpen !== false, refreshAutomatic);
|
|
3588
4012
|
const reset = useCallback(() => {
|
|
3589
4013
|
if (successTimer.current) clearTimeout(successTimer.current);
|
|
3590
4014
|
setAnnotationTrigger(null);
|
|
@@ -3592,20 +4016,12 @@ function useWidgetState(props) {
|
|
|
3592
4016
|
pickRef.current++;
|
|
3593
4017
|
setIsElementCapturing(false);
|
|
3594
4018
|
resetScreenshots();
|
|
3595
|
-
replaceElementCapture(null);
|
|
3596
|
-
setSelection(null);
|
|
3597
4019
|
setMessage("");
|
|
3598
4020
|
setEmail("");
|
|
3599
4021
|
setHoneypot("");
|
|
3600
4022
|
setError(null);
|
|
3601
4023
|
setStep("compose");
|
|
3602
|
-
|
|
3603
|
-
}, [
|
|
3604
|
-
defaultCategory,
|
|
3605
|
-
resetSubmission,
|
|
3606
|
-
resetScreenshots,
|
|
3607
|
-
replaceElementCapture
|
|
3608
|
-
]);
|
|
4024
|
+
}, [resetSubmission, resetScreenshots]);
|
|
3609
4025
|
const close = useCallback(() => {
|
|
3610
4026
|
setIsOpen(false);
|
|
3611
4027
|
reset();
|
|
@@ -3635,11 +4051,13 @@ function useWidgetState(props) {
|
|
|
3635
4051
|
props.onOpen,
|
|
3636
4052
|
refreshAutomatic
|
|
3637
4053
|
]);
|
|
3638
|
-
const selectElement = useCallback(async (element) => {
|
|
4054
|
+
const selectElement = useCallback(async (element, note = "") => {
|
|
3639
4055
|
cancelPendingCapture();
|
|
3640
4056
|
setIsElementCapturing(true);
|
|
3641
|
-
const picked =
|
|
3642
|
-
|
|
4057
|
+
const picked = note ? {
|
|
4058
|
+
...buildElementSelection(element),
|
|
4059
|
+
comment: note.slice(0, MAX_SELECTION_COMMENT_LENGTH)
|
|
4060
|
+
} : buildElementSelection(element);
|
|
3643
4061
|
setStep("compose");
|
|
3644
4062
|
const pick = ++pickRef.current;
|
|
3645
4063
|
const context = collectPageContext({ sdkVersion: SDK_VERSION });
|
|
@@ -3650,48 +4068,55 @@ function useWidgetState(props) {
|
|
|
3650
4068
|
return;
|
|
3651
4069
|
}
|
|
3652
4070
|
setIsElementCapturing(false);
|
|
3653
|
-
if (fresh)
|
|
4071
|
+
if (!fresh) {
|
|
4072
|
+
releaseCapture(closeUp);
|
|
4073
|
+
setError(captureFailed);
|
|
4074
|
+
return;
|
|
4075
|
+
}
|
|
4076
|
+
storeScreenshot({
|
|
3654
4077
|
annotations: [highlightFor(picked, fresh)],
|
|
4078
|
+
closeUp,
|
|
3655
4079
|
context,
|
|
3656
4080
|
id: crypto.randomUUID(),
|
|
3657
4081
|
image: fresh,
|
|
4082
|
+
selectedNode: element,
|
|
4083
|
+
selection: picked,
|
|
3658
4084
|
source: "manual"
|
|
3659
4085
|
});
|
|
3660
|
-
else setError(captureFailed);
|
|
3661
|
-
replaceElementCapture(closeUp);
|
|
3662
4086
|
}, [
|
|
3663
4087
|
cancelPendingCapture,
|
|
3664
4088
|
captureFailed,
|
|
3665
|
-
storeScreenshot
|
|
3666
|
-
replaceElementCapture
|
|
4089
|
+
storeScreenshot
|
|
3667
4090
|
]);
|
|
3668
|
-
const
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
setSelection(null);
|
|
3672
|
-
setIsElementCapturing(false);
|
|
3673
|
-
clearSelectionAnnotations();
|
|
3674
|
-
}, [clearSelectionAnnotations, replaceElementCapture]);
|
|
4091
|
+
const selections = gallery.screenshots.flatMap(({ selection }) => selection ? [selection] : []);
|
|
4092
|
+
const firstElementNote = selections.find(({ comment }) => comment)?.comment;
|
|
4093
|
+
const reportedMessage = message.trim() || firstElementNote || "";
|
|
3675
4094
|
const submit = () => {
|
|
3676
4095
|
if (honeypot) {
|
|
3677
4096
|
setStep("success");
|
|
3678
4097
|
return;
|
|
3679
4098
|
}
|
|
4099
|
+
setStep("compose");
|
|
3680
4100
|
return submission.submit({
|
|
3681
|
-
category,
|
|
3682
4101
|
context: {
|
|
3683
|
-
...activeScreenshot?.context ?? collectPageContext({ sdkVersion: "0.
|
|
4102
|
+
...activeScreenshot?.context ?? collectPageContext({ sdkVersion: "0.4.1" }),
|
|
3684
4103
|
consoleEvents: consoleRef.current?.events(),
|
|
3685
4104
|
metadata: props.metadata,
|
|
3686
|
-
|
|
4105
|
+
selections
|
|
3687
4106
|
},
|
|
3688
|
-
element: elementCapture,
|
|
3689
4107
|
email,
|
|
3690
4108
|
isAnonymous,
|
|
3691
|
-
message,
|
|
4109
|
+
message: reportedMessage,
|
|
3692
4110
|
screenshots: gallery.screenshots
|
|
3693
4111
|
});
|
|
3694
4112
|
};
|
|
4113
|
+
const requestSubmit = () => {
|
|
4114
|
+
if (isAnonymous && email.length === 0 && !submission.hasPendingAttachments && !honeypot) {
|
|
4115
|
+
setStep("email");
|
|
4116
|
+
return;
|
|
4117
|
+
}
|
|
4118
|
+
return submit();
|
|
4119
|
+
};
|
|
3695
4120
|
return {
|
|
3696
4121
|
activeScreenshot,
|
|
3697
4122
|
annotateScreenshot: (id, trigger) => {
|
|
@@ -3702,13 +4127,11 @@ function useWidgetState(props) {
|
|
|
3702
4127
|
annotations,
|
|
3703
4128
|
annotationTrigger,
|
|
3704
4129
|
canDismiss: dismissForDays !== null,
|
|
4130
|
+
canSubmit: reportedMessage.length > 0,
|
|
3705
4131
|
capture,
|
|
3706
|
-
category,
|
|
3707
|
-
clearSelection,
|
|
3708
4132
|
close,
|
|
3709
4133
|
dismiss,
|
|
3710
4134
|
dismissForDays,
|
|
3711
|
-
elementCapture,
|
|
3712
4135
|
email,
|
|
3713
4136
|
error,
|
|
3714
4137
|
hasPendingAttachments: submission.hasPendingAttachments,
|
|
@@ -3723,15 +4146,16 @@ function useWidgetState(props) {
|
|
|
3723
4146
|
open,
|
|
3724
4147
|
pendingCapture: gallery.pendingCapture,
|
|
3725
4148
|
removeCapture: gallery.removeCapture,
|
|
4149
|
+
requestSubmit,
|
|
3726
4150
|
retakeCapture: gallery.retakeCapture,
|
|
3727
4151
|
screenshots: gallery.screenshots,
|
|
3728
4152
|
selectElement,
|
|
3729
|
-
|
|
4153
|
+
selections,
|
|
3730
4154
|
setAnnotations: gallery.setAnnotations,
|
|
3731
|
-
setCategory,
|
|
3732
4155
|
setEmail,
|
|
3733
4156
|
setHoneypot,
|
|
3734
4157
|
setMessage,
|
|
4158
|
+
setSelectionComment: gallery.setSelectionComment,
|
|
3735
4159
|
setStep,
|
|
3736
4160
|
step,
|
|
3737
4161
|
submit,
|
|
@@ -3740,13 +4164,8 @@ function useWidgetState(props) {
|
|
|
3740
4164
|
}
|
|
3741
4165
|
//#endregion
|
|
3742
4166
|
//#region src/feedback/widget.tsx
|
|
3743
|
-
const DEFAULT_CATEGORIES = [
|
|
3744
|
-
"bug",
|
|
3745
|
-
"idea",
|
|
3746
|
-
"question"
|
|
3747
|
-
];
|
|
3748
4167
|
function RefletFeedback(props) {
|
|
3749
|
-
const {
|
|
4168
|
+
const { enabled = true, hotkey = null, offset = 20, position = "bottom-right", theme = "auto" } = props;
|
|
3750
4169
|
const labels = {
|
|
3751
4170
|
...DEFAULT_WIDGET_LABELS,
|
|
3752
4171
|
...props.labels
|
|
@@ -3784,10 +4203,7 @@ function RefletFeedback(props) {
|
|
|
3784
4203
|
}),
|
|
3785
4204
|
/* @__PURE__ */ jsx(FloatingWidget, {
|
|
3786
4205
|
labels,
|
|
3787
|
-
|
|
3788
|
-
categories,
|
|
3789
|
-
position
|
|
3790
|
-
},
|
|
4206
|
+
position,
|
|
3791
4207
|
state
|
|
3792
4208
|
}),
|
|
3793
4209
|
isOpen && state.step === "annotate" && state.activeScreenshot && /* @__PURE__ */ jsx(Annotator, {
|
|
@@ -3796,16 +4212,22 @@ function RefletFeedback(props) {
|
|
|
3796
4212
|
annotations: state.annotations,
|
|
3797
4213
|
onChange: state.setAnnotations,
|
|
3798
4214
|
onDone: () => state.setStep("compose"),
|
|
3799
|
-
onRetake: () => {
|
|
4215
|
+
onRetake: state.activeScreenshot.selection ? void 0 : () => {
|
|
3800
4216
|
state.setStep("compose");
|
|
3801
4217
|
if (state.activeScreenshot) state.retakeCapture(state.activeScreenshot.id);
|
|
3802
4218
|
},
|
|
3803
|
-
trigger: state.annotationTrigger
|
|
4219
|
+
trigger: state.annotationTrigger?.thumbnail
|
|
3804
4220
|
},
|
|
3805
|
-
labels
|
|
4221
|
+
labels,
|
|
4222
|
+
note: state.activeScreenshot.selection && {
|
|
4223
|
+
comment: state.activeScreenshot.selection.comment ?? "",
|
|
4224
|
+
onChange: (comment) => {
|
|
4225
|
+
if (state.activeScreenshot) state.setSelectionComment(state.activeScreenshot.id, comment);
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
3806
4228
|
}),
|
|
3807
4229
|
isOpen && state.step === "picking" && /* @__PURE__ */ jsx(ElementPicker, {
|
|
3808
|
-
|
|
4230
|
+
labels,
|
|
3809
4231
|
onCancel: () => state.setStep("compose"),
|
|
3810
4232
|
onPick: state.selectElement
|
|
3811
4233
|
})
|