reflet-sdk 0.4.0 → 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-BomgCsXm.d.ts → client-w4Rkl10y.d.ts} +2 -2
- package/dist/{client-BomgCsXm.d.ts.map → client-w4Rkl10y.d.ts.map} +1 -1
- package/dist/feedback.d.ts +4 -2
- package/dist/feedback.d.ts.map +1 -1
- package/dist/feedback.js +517 -436
- package/dist/feedback.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/{types-Cak4ZkSy.d.ts → types-DnaDEN9Y.d.ts} +2 -2
- package/dist/{types-Cak4ZkSy.d.ts.map → types-DnaDEN9Y.d.ts.map} +1 -1
- package/package.json +1 -1
package/dist/feedback.js
CHANGED
|
@@ -810,7 +810,7 @@ function collectPageContext(options) {
|
|
|
810
810
|
}
|
|
811
811
|
//#endregion
|
|
812
812
|
//#region src/feedback/types.ts
|
|
813
|
-
const SDK_VERSION = "0.4.
|
|
813
|
+
const SDK_VERSION = "0.4.1";
|
|
814
814
|
const DEFAULT_WIDGET_LABELS = {
|
|
815
815
|
annotateHint: "Draw on the screenshot to point at the problem.",
|
|
816
816
|
attachElement: "Attach this element",
|
|
@@ -821,7 +821,6 @@ const DEFAULT_WIDGET_LABELS = {
|
|
|
821
821
|
captureFailed: "Screenshot unavailable. Try again or send without it.",
|
|
822
822
|
capturing: "Taking screenshot…",
|
|
823
823
|
clearAnnotations: "Clear drawing",
|
|
824
|
-
clearSelection: "Remove selected element",
|
|
825
824
|
descriptionPlaceholder: "What would you like to share?",
|
|
826
825
|
dismissForDays: "Hide for {days} days",
|
|
827
826
|
done: "Done",
|
|
@@ -830,6 +829,8 @@ const DEFAULT_WIDGET_LABELS = {
|
|
|
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",
|
|
@@ -844,6 +845,7 @@ const DEFAULT_WIDGET_LABELS = {
|
|
|
844
845
|
retakeHint: "Screenshot of this page",
|
|
845
846
|
retryAttachments: "Retry attachments",
|
|
846
847
|
screenshot: "Screenshot",
|
|
848
|
+
sendWithoutEmail: "Send without email",
|
|
847
849
|
submit: "Send feedback",
|
|
848
850
|
successMessage: "We read every report. Thanks for taking the time.",
|
|
849
851
|
successTitle: "Feedback sent",
|
|
@@ -1013,9 +1015,6 @@ function CheckIcon({ size = 28 }) {
|
|
|
1013
1015
|
function GripIcon() {
|
|
1014
1016
|
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M9 5h.01M15 5h.01M9 12h.01M15 12h.01M9 19h.01M15 19h.01" }) });
|
|
1015
1017
|
}
|
|
1016
|
-
function MinusIcon() {
|
|
1017
|
-
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M5 12h14" }) });
|
|
1018
|
-
}
|
|
1019
1018
|
function MoreIcon() {
|
|
1020
1019
|
return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M5 12h.01M12 12h.01M19 12h.01" }) });
|
|
1021
1020
|
}
|
|
@@ -1736,7 +1735,7 @@ function toImagePoint(canvas, clientX, clientY) {
|
|
|
1736
1735
|
y: Math.max(0, Math.min(canvas.height, (clientY - bounds.top) / bounds.height * canvas.height))
|
|
1737
1736
|
};
|
|
1738
1737
|
}
|
|
1739
|
-
function Annotator({ capture, labels, editor }) {
|
|
1738
|
+
function Annotator({ capture, labels, editor, note }) {
|
|
1740
1739
|
const { annotations, onChange, onDone, onRetake } = editor;
|
|
1741
1740
|
const morph = useImageMorph(editor.trigger, onDone);
|
|
1742
1741
|
const dialogRef = useRef(null);
|
|
@@ -1856,6 +1855,16 @@ function Annotator({ capture, labels, editor }) {
|
|
|
1856
1855
|
onRetake: onRetake ? () => morph.close(onRetake) : void 0
|
|
1857
1856
|
},
|
|
1858
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
|
+
})]
|
|
1859
1868
|
})
|
|
1860
1869
|
]
|
|
1861
1870
|
});
|
|
@@ -1877,6 +1886,156 @@ function Launcher({ isOpen, label, onClick, buttonRef }) {
|
|
|
1877
1886
|
});
|
|
1878
1887
|
}
|
|
1879
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
|
|
1880
2039
|
//#region src/feedback/ui/annotation/screenshot-preview.tsx
|
|
1881
2040
|
function ScreenshotPreview({ annotations, capture }) {
|
|
1882
2041
|
const { canvasRef } = useAnnotationCanvas(capture, annotations);
|
|
@@ -1895,103 +2054,66 @@ function ScreenshotPreview({ annotations, capture }) {
|
|
|
1895
2054
|
//#endregion
|
|
1896
2055
|
//#region src/feedback/ui/composer/screenshot-card.tsx
|
|
1897
2056
|
function ScreenshotCard({ item, labels, state }) {
|
|
1898
|
-
const { draft, number
|
|
2057
|
+
const { draft, number } = item;
|
|
2058
|
+
const previewRef = useRef(null);
|
|
1899
2059
|
const isCapturing = state.pendingCapture?.id === draft.id;
|
|
1900
2060
|
const disabled = state.isEditingDisabled || state.isCapturing;
|
|
2061
|
+
const drawings = draft.annotations.filter((annotation) => !isSelectionAnnotation(annotation));
|
|
1901
2062
|
return /* @__PURE__ */ jsxs("fieldset", {
|
|
1902
2063
|
"aria-label": `${labels.screenshot} ${number}`,
|
|
1903
|
-
className: "screenshot-attachment
|
|
1904
|
-
children: [
|
|
1905
|
-
"
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
className: "
|
|
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
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
})
|
|
1951
|
-
|
|
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
|
+
]
|
|
1952
2113
|
});
|
|
1953
2114
|
}
|
|
1954
2115
|
//#endregion
|
|
1955
2116
|
//#region src/feedback/ui/composer/attachments.tsx
|
|
1956
|
-
function SelectedElement({ labels, state }) {
|
|
1957
|
-
if (!state.selection) return null;
|
|
1958
|
-
const { comment, componentStack, label, selector, sourceLocation } = state.selection;
|
|
1959
|
-
const component = componentStack[0];
|
|
1960
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1961
|
-
className: "selection-chip glass",
|
|
1962
|
-
children: [/* @__PURE__ */ jsxs("button", {
|
|
1963
|
-
"aria-label": labels.pickElement,
|
|
1964
|
-
className: "selection-label",
|
|
1965
|
-
disabled: state.isEditingDisabled || state.isCapturing,
|
|
1966
|
-
onClick: () => state.setStep("picking"),
|
|
1967
|
-
title: comment ?? sourceLocation ?? selector,
|
|
1968
|
-
type: "button",
|
|
1969
|
-
children: [
|
|
1970
|
-
state.elementCapture ? /* @__PURE__ */ jsx("img", {
|
|
1971
|
-
alt: "",
|
|
1972
|
-
height: 32,
|
|
1973
|
-
src: state.elementCapture.objectUrl,
|
|
1974
|
-
width: 32
|
|
1975
|
-
}) : /* @__PURE__ */ jsx(TargetIcon, {}),
|
|
1976
|
-
/* @__PURE__ */ jsx("span", {
|
|
1977
|
-
className: "selection-name",
|
|
1978
|
-
children: component ? `<${component}>` : label
|
|
1979
|
-
}),
|
|
1980
|
-
comment && /* @__PURE__ */ jsx("span", {
|
|
1981
|
-
className: "truncate",
|
|
1982
|
-
children: comment
|
|
1983
|
-
})
|
|
1984
|
-
]
|
|
1985
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
1986
|
-
"aria-label": labels.clearSelection,
|
|
1987
|
-
className: "icon-btn",
|
|
1988
|
-
disabled: state.isEditingDisabled || state.isCapturing,
|
|
1989
|
-
onClick: state.clearSelection,
|
|
1990
|
-
type: "button",
|
|
1991
|
-
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
1992
|
-
})]
|
|
1993
|
-
});
|
|
1994
|
-
}
|
|
1995
2117
|
function Attachments({ labels, state }) {
|
|
1996
2118
|
const stripRef = useRef(null);
|
|
1997
2119
|
const count = state.screenshots.length;
|
|
@@ -2000,41 +2122,102 @@ function Attachments({ labels, state }) {
|
|
|
2000
2122
|
if (strip && count > 0) strip.scrollTo?.({ left: strip.scrollWidth });
|
|
2001
2123
|
}, [count]);
|
|
2002
2124
|
const isAdding = state.pendingCapture && !state.screenshots.some(({ id }) => id === state.pendingCapture?.id);
|
|
2003
|
-
return /* @__PURE__ */
|
|
2125
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2004
2126
|
className: "attachments",
|
|
2005
|
-
children:
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
"
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
children:
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
"
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
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
|
+
})]
|
|
2036
2160
|
})]
|
|
2037
|
-
})
|
|
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
|
+
]
|
|
2038
2221
|
});
|
|
2039
2222
|
}
|
|
2040
2223
|
//#endregion
|
|
@@ -2110,64 +2293,37 @@ function ComposerOptions({ labels, state }) {
|
|
|
2110
2293
|
});
|
|
2111
2294
|
}
|
|
2112
2295
|
//#endregion
|
|
2113
|
-
//#region src/feedback/ui/composer/controls.tsx
|
|
2114
|
-
function ComposerControls({ floatingControls, labels, state }) {
|
|
2115
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
2116
|
-
className: "composer-toolbar",
|
|
2117
|
-
children: [floatingControls, /* @__PURE__ */ jsxs("div", {
|
|
2118
|
-
className: "composer-controls glass",
|
|
2119
|
-
children: [
|
|
2120
|
-
/* @__PURE__ */ jsx("button", {
|
|
2121
|
-
"aria-label": labels.attachScreenshot,
|
|
2122
|
-
className: "icon-btn",
|
|
2123
|
-
disabled: state.isCapturing || state.isEditingDisabled,
|
|
2124
|
-
onClick: state.takeCapture,
|
|
2125
|
-
title: labels.attachScreenshot,
|
|
2126
|
-
type: "button",
|
|
2127
|
-
children: /* @__PURE__ */ jsx(CameraIcon, {})
|
|
2128
|
-
}),
|
|
2129
|
-
/* @__PURE__ */ jsx("button", {
|
|
2130
|
-
"aria-label": labels.pickElement,
|
|
2131
|
-
className: "icon-btn",
|
|
2132
|
-
disabled: state.isEditingDisabled || state.isCapturing,
|
|
2133
|
-
onClick: () => state.setStep("picking"),
|
|
2134
|
-
title: labels.pickElement,
|
|
2135
|
-
type: "button",
|
|
2136
|
-
children: /* @__PURE__ */ jsx(TargetIcon, {})
|
|
2137
|
-
}),
|
|
2138
|
-
/* @__PURE__ */ jsx(ComposerOptions, {
|
|
2139
|
-
labels,
|
|
2140
|
-
state
|
|
2141
|
-
})
|
|
2142
|
-
]
|
|
2143
|
-
})]
|
|
2144
|
-
});
|
|
2145
|
-
}
|
|
2146
|
-
//#endregion
|
|
2147
2296
|
//#region src/feedback/ui/composer/message-input.tsx
|
|
2148
|
-
function MessageInput({ labels, state }) {
|
|
2149
|
-
const [expanded, setExpanded] = useState(false);
|
|
2297
|
+
function MessageInput({ floatingControls, labels, state }) {
|
|
2150
2298
|
const submitLabel = state.hasPendingAttachments ? labels.retryAttachments : labels.submit;
|
|
2151
2299
|
return /* @__PURE__ */ jsxs("div", {
|
|
2152
2300
|
className: "message-row glass",
|
|
2153
|
-
"data-expanded": expanded,
|
|
2154
2301
|
children: [/* @__PURE__ */ jsx("textarea", {
|
|
2155
2302
|
"aria-label": labels.descriptionPlaceholder,
|
|
2156
2303
|
disabled: state.isEditingDisabled,
|
|
2157
2304
|
maxLength: 4e3,
|
|
2158
2305
|
onChange: (event) => state.setMessage(event.target.value),
|
|
2159
|
-
onFocus: () => setExpanded(true),
|
|
2160
2306
|
placeholder: labels.descriptionPlaceholder,
|
|
2161
|
-
rows:
|
|
2307
|
+
rows: 3,
|
|
2162
2308
|
value: state.message
|
|
2163
|
-
}), /* @__PURE__ */
|
|
2164
|
-
"
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2309
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2310
|
+
className: "message-actions",
|
|
2311
|
+
children: [
|
|
2312
|
+
floatingControls,
|
|
2313
|
+
/* @__PURE__ */ jsx(ComposerOptions, {
|
|
2314
|
+
labels,
|
|
2315
|
+
state
|
|
2316
|
+
}),
|
|
2317
|
+
/* @__PURE__ */ jsx("button", {
|
|
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, {})
|
|
2325
|
+
})
|
|
2326
|
+
]
|
|
2171
2327
|
})]
|
|
2172
2328
|
});
|
|
2173
2329
|
}
|
|
@@ -2193,6 +2349,26 @@ function FeedbackPanel({ floatingControls, labels, state }) {
|
|
|
2193
2349
|
children: floatingControls
|
|
2194
2350
|
})]
|
|
2195
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
|
+
});
|
|
2196
2372
|
return /* @__PURE__ */ jsxs("section", {
|
|
2197
2373
|
"aria-label": labels.title,
|
|
2198
2374
|
className: "panel",
|
|
@@ -2205,14 +2381,10 @@ function FeedbackPanel({ floatingControls, labels, state }) {
|
|
|
2205
2381
|
className: "composer",
|
|
2206
2382
|
onSubmit: (event) => {
|
|
2207
2383
|
event.preventDefault();
|
|
2208
|
-
state.
|
|
2384
|
+
state.requestSubmit();
|
|
2209
2385
|
},
|
|
2210
2386
|
children: [
|
|
2211
2387
|
/* @__PURE__ */ jsx(MessageInput, {
|
|
2212
|
-
labels,
|
|
2213
|
-
state
|
|
2214
|
-
}),
|
|
2215
|
-
/* @__PURE__ */ jsx(ComposerControls, {
|
|
2216
2388
|
floatingControls,
|
|
2217
2389
|
labels,
|
|
2218
2390
|
state
|
|
@@ -2343,13 +2515,18 @@ function clampPosition(point, bounds, viewport = visibleViewport()) {
|
|
|
2343
2515
|
y: Math.max(viewport.top + EDGE_GAP$1, Math.min(point.y, viewport.top + viewport.height - bounds.height - EDGE_GAP$1))
|
|
2344
2516
|
};
|
|
2345
2517
|
}
|
|
2346
|
-
function useFloatingPosition(corner = "bottom-right") {
|
|
2518
|
+
function useFloatingPosition(corner = "bottom-right", isPanelOpen = false) {
|
|
2347
2519
|
const rootRef = useRef(null);
|
|
2348
2520
|
const gesture = useRef(null);
|
|
2349
2521
|
const [position, setPosition] = useState(null);
|
|
2522
|
+
const [positionedPanelOpen, setPositionedPanelOpen] = useState(isPanelOpen);
|
|
2350
2523
|
const [viewport, setViewport] = useState(visibleViewport);
|
|
2351
2524
|
const horizontalEdge = corner.endsWith("right") ? "right" : "left";
|
|
2352
2525
|
const verticalEdge = corner.startsWith("bottom") ? "bottom" : "top";
|
|
2526
|
+
if (positionedPanelOpen !== isPanelOpen) {
|
|
2527
|
+
setPositionedPanelOpen(isPanelOpen);
|
|
2528
|
+
setPosition(null);
|
|
2529
|
+
}
|
|
2353
2530
|
const moveTo = useCallback((point, bounds) => {
|
|
2354
2531
|
const fitted = clampPosition(point, bounds);
|
|
2355
2532
|
const visible = visibleViewport();
|
|
@@ -2388,9 +2565,11 @@ function useFloatingPosition(corner = "bottom-right") {
|
|
|
2388
2565
|
};
|
|
2389
2566
|
const onPointerMove = (event) => {
|
|
2390
2567
|
const bounds = rootRef.current?.getBoundingClientRect();
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
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
|
|
2394
2573
|
}, bounds);
|
|
2395
2574
|
};
|
|
2396
2575
|
const onKeyDown = (event) => {
|
|
@@ -2408,6 +2587,9 @@ function useFloatingPosition(corner = "bottom-right") {
|
|
|
2408
2587
|
return {
|
|
2409
2588
|
handleProps: {
|
|
2410
2589
|
onKeyDown,
|
|
2590
|
+
onLostPointerCapture: () => {
|
|
2591
|
+
gesture.current = null;
|
|
2592
|
+
},
|
|
2411
2593
|
onPointerCancel: () => {
|
|
2412
2594
|
gesture.current = null;
|
|
2413
2595
|
},
|
|
@@ -2427,14 +2609,14 @@ function useFloatingPosition(corner = "bottom-right") {
|
|
|
2427
2609
|
function FloatingWidget({ labels, position, state }) {
|
|
2428
2610
|
const [minimized, setMinimized] = useState(false);
|
|
2429
2611
|
const launcherRef = useRef(null);
|
|
2430
|
-
const floating = useFloatingPosition(position);
|
|
2431
2612
|
const showPanel = state.isOpen && !minimized;
|
|
2613
|
+
const floating = useFloatingPosition(position, showPanel);
|
|
2432
2614
|
const minimize = () => {
|
|
2433
2615
|
setMinimized(true);
|
|
2434
2616
|
requestAnimationFrame(() => launcherRef.current?.focus());
|
|
2435
2617
|
};
|
|
2436
2618
|
useEffect(() => {
|
|
2437
|
-
if (state.step === "compose") state.annotationTrigger?.focus({ preventScroll: true });
|
|
2619
|
+
if (state.step === "compose") state.annotationTrigger?.button.focus({ preventScroll: true });
|
|
2438
2620
|
}, [state.step, state.annotationTrigger]);
|
|
2439
2621
|
useEffect(() => {
|
|
2440
2622
|
if (!showPanel || state.step !== "compose") return;
|
|
@@ -2455,10 +2637,10 @@ function FloatingWidget({ labels, position, state }) {
|
|
|
2455
2637
|
ref: floating.rootRef,
|
|
2456
2638
|
children: [
|
|
2457
2639
|
/* @__PURE__ */ jsx("style", { children: floating.styles }),
|
|
2458
|
-
showPanel && state.step === "compose" && /* @__PURE__ */ jsx(SelectionOutline, { node:
|
|
2640
|
+
showPanel && state.step === "compose" && state.screenshots.map(({ id, selectedNode }) => selectedNode ? /* @__PURE__ */ jsx(SelectionOutline, { node: selectedNode }, id) : null),
|
|
2459
2641
|
showPanel ? /* @__PURE__ */ jsx(FeedbackPanel, {
|
|
2460
2642
|
floatingControls: /* @__PURE__ */ jsxs("div", {
|
|
2461
|
-
className: "floating-controls
|
|
2643
|
+
className: "floating-controls",
|
|
2462
2644
|
children: [/* @__PURE__ */ jsx("button", {
|
|
2463
2645
|
"aria-label": labels.moveFeedback,
|
|
2464
2646
|
className: "icon-btn drag-handle",
|
|
@@ -2472,7 +2654,7 @@ function FloatingWidget({ labels, position, state }) {
|
|
|
2472
2654
|
onClick: minimize,
|
|
2473
2655
|
title: labels.minimize,
|
|
2474
2656
|
type: "button",
|
|
2475
|
-
children: /* @__PURE__ */ jsx(
|
|
2657
|
+
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
2476
2658
|
})]
|
|
2477
2659
|
}),
|
|
2478
2660
|
labels,
|
|
@@ -2807,9 +2989,15 @@ const ANNOTATOR_STYLES = `
|
|
|
2807
2989
|
.overlay[data-opening="true"] .editor { pointer-events: none; }
|
|
2808
2990
|
.overlay[data-closing="true"] { pointer-events: none; }
|
|
2809
2991
|
.overlay[data-closing="true"]::before { opacity: 0; }
|
|
2810
|
-
.overlay[data-closing="true"] .toolbar { opacity: 0; transform: translateY(8px); }
|
|
2992
|
+
.overlay[data-closing="true"] .toolbar, .overlay[data-closing="true"] .element-note { opacity: 0; transform: translateY(8px); }
|
|
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; }
|
|
2811
2999
|
|
|
2812
|
-
.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)); }
|
|
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)); }
|
|
2813
3001
|
.toolbar .group { display: flex; align-items: center; gap: 2px; padding: 4px; border-radius: var(--rf-pill); }
|
|
2814
3002
|
.tool[aria-pressed="true"], .tool[data-selected="true"] { background: var(--rf-primary); color: var(--rf-primary-text); }
|
|
2815
3003
|
.color-picker { position: relative; }
|
|
@@ -2864,22 +3052,21 @@ const COMPOSER_STYLES = `
|
|
|
2864
3052
|
.panel { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 8px; outline: none; animation: rf-in 240ms var(--rf-ease); }
|
|
2865
3053
|
.success-panel { border-radius: var(--rf-radius); }
|
|
2866
3054
|
.composer { position: relative; display: flex; flex-direction: column; gap: 8px; }
|
|
2867
|
-
.message-row { display: flex;
|
|
3055
|
+
.message-row { display: flex; flex-direction: column; gap: 2px; padding: 6px; border-radius: 26px; }
|
|
2868
3056
|
.message-row textarea {
|
|
2869
|
-
|
|
2870
|
-
margin: 0; padding:
|
|
3057
|
+
width: 100%; height: 76px; max-height: min(140px, 30dvh);
|
|
3058
|
+
margin: 0; padding: 6px 6px 0 8px; border: 0; resize: none;
|
|
2871
3059
|
background: transparent; line-height: 20px;
|
|
2872
3060
|
}
|
|
2873
|
-
.
|
|
3061
|
+
.message-actions { display: flex; align-items: center; gap: 2px; }
|
|
2874
3062
|
.message-row textarea::placeholder { color: var(--rf-text-muted); }
|
|
2875
3063
|
.message-row textarea:focus { outline: none; }
|
|
2876
3064
|
.message-row:has(textarea:focus-visible) { border-color: color-mix(in srgb, var(--rf-accent) 40%, var(--rf-glass-stroke)); }
|
|
2877
|
-
.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); }
|
|
2878
3066
|
.submit svg { transform: rotate(-45deg); }
|
|
2879
3067
|
.submit:hover:enabled { transform: scale(1.05); }
|
|
2880
3068
|
.submit:disabled { background: var(--rf-bg-hover); color: var(--rf-text-muted); opacity: .65; }
|
|
2881
3069
|
.composer-toolbar { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex-wrap: wrap; }
|
|
2882
|
-
.composer-controls { display: flex; align-items: center; gap: 2px; padding: 3px; border-radius: var(--rf-pill); }
|
|
2883
3070
|
.composer-options { position: relative; }
|
|
2884
3071
|
summary { cursor: pointer; list-style: none; }
|
|
2885
3072
|
summary::-webkit-details-marker { display: none; }
|
|
@@ -2892,47 +3079,67 @@ summary::-webkit-details-marker { display: none; }
|
|
|
2892
3079
|
.dismiss-btn:hover { background: var(--rf-bg-hover); }
|
|
2893
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; }
|
|
2894
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; }
|
|
2895
3091
|
.attachments { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }
|
|
2896
|
-
.
|
|
2897
|
-
.screenshot-
|
|
2898
|
-
.screenshot-strip
|
|
2899
|
-
.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))); }
|
|
2900
3097
|
.screenshot-strip-content { display: flex; align-items: flex-end; gap: 8px; }
|
|
2901
3098
|
.screenshot-scroll-area:has(.screenshot-strip-content:empty) { display: none; }
|
|
2902
|
-
.
|
|
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); }
|
|
2903
3104
|
.screenshot-scrollbar { height: 4px; margin: 0 8px 2px; opacity: 0; transition: opacity 150ms var(--rf-ease); }
|
|
2904
3105
|
.screenshot-scrollbar[data-hovering], .screenshot-scrollbar[data-scrolling] { opacity: 1; }
|
|
2905
3106
|
.screenshot-scroll-thumb { border-radius: var(--rf-pill); background: var(--rf-border); }
|
|
2906
|
-
.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; }
|
|
2907
3108
|
.attachments:empty { display: none; }
|
|
2908
|
-
.screenshot-attachment, .attachment-skeleton { position: relative; flex: none; margin: 0; min-width: 0; width: 128px; height:
|
|
2909
|
-
.screenshot-
|
|
2910
|
-
.screenshot-preview
|
|
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; }
|
|
3112
|
+
.screenshot-preview img, .screenshot-preview canvas { width: 100%; height: 100%; display: block; object-fit: cover; object-position: top center; }
|
|
2911
3113
|
.screenshot-preview canvas { position: absolute; inset: 0; pointer-events: none; }
|
|
2912
|
-
.
|
|
2913
|
-
.attachment-
|
|
2914
|
-
.
|
|
2915
|
-
.attachment-actions {
|
|
2916
|
-
.screenshot-attachment:hover .attachment-actions, .screenshot-attachment:focus-within .attachment-actions { opacity: 1; pointer-events: auto; transform: none; }
|
|
2917
|
-
.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; }
|
|
2918
3118
|
.attachment-skeleton { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: var(--rf-text-muted); font-size: 11px; }
|
|
2919
3119
|
.attachment-skeleton::after { content: ""; position: absolute; inset: 0; border-radius: inherit; background: var(--rf-bg-subtle); animation: rf-fade 800ms alternate infinite; }
|
|
2920
|
-
.selection-
|
|
2921
|
-
.selection-
|
|
2922
|
-
.selection-name { flex: none; font-weight: 600; color: var(--rf-accent); }
|
|
2923
|
-
.selection-label img { object-fit: contain; border-radius: var(--rf-pill); outline: 1px solid var(--rf-hairline); outline-offset: -1px; }
|
|
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; }
|
|
2924
3122
|
@media (pointer: coarse) {
|
|
2925
3123
|
.message-row { border-radius: 28px; }
|
|
2926
|
-
.
|
|
3124
|
+
.capture-slot { width: 64px; }
|
|
3125
|
+
.message-row textarea { font-size: 16px; }
|
|
2927
3126
|
.submit { width: 44px; height: 44px; }
|
|
2928
|
-
|
|
2929
|
-
.attachment-
|
|
2930
|
-
.attachment-
|
|
2931
|
-
.
|
|
2932
|
-
.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; }
|
|
2933
3131
|
.email-field input { font-size: 16px; }
|
|
2934
|
-
.dismiss-btn
|
|
3132
|
+
.dismiss-btn { min-height: 44px; }
|
|
2935
3133
|
}
|
|
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; }
|
|
3141
|
+
}
|
|
3142
|
+
@keyframes rf-drawer-in { from { transform: translateY(100%); } }
|
|
2936
3143
|
`;
|
|
2937
3144
|
//#endregion
|
|
2938
3145
|
//#region src/feedback/ui/floating/picker-styles.ts
|
|
@@ -2997,7 +3204,7 @@ const PICKER_STYLES = `
|
|
|
2997
3204
|
color: var(--rf-text);
|
|
2998
3205
|
background: var(--rf-bg);
|
|
2999
3206
|
border-radius: var(--rf-pill);
|
|
3000
|
-
box-shadow: var(--rf-shadow);
|
|
3207
|
+
box-shadow: var(--rf-ring), var(--rf-shadow);
|
|
3001
3208
|
animation: rf-fade 240ms var(--rf-ease);
|
|
3002
3209
|
}
|
|
3003
3210
|
.picker-instruction { display: flex; align-items: center; gap: 6px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -3093,14 +3300,16 @@ const WIDGET_STYLES = `
|
|
|
3093
3300
|
--rf-spectrum: conic-gradient(#ff5757, #ffde59, #6bdb78, #4ac9fa, #8b5cf6, #ef73dc, #ff5757);
|
|
3094
3301
|
--rf-radius: 20px;
|
|
3095
3302
|
--rf-pill: 999px;
|
|
3303
|
+
--rf-attachment-height: 78px;
|
|
3096
3304
|
--rf-width: 360px;
|
|
3097
3305
|
--rf-glass: rgb(255 255 255 / 72%);
|
|
3098
3306
|
--rf-glass-highlight: rgb(255 255 255 / 90%);
|
|
3099
3307
|
--rf-glass-stroke: rgb(255 255 255 / 70%);
|
|
3100
3308
|
--rf-radius-sm: 8px;
|
|
3101
3309
|
--rf-shadow-inset: inset 0 0 0 1px var(--rf-border);
|
|
3102
|
-
--rf-
|
|
3103
|
-
--rf-shadow: 0
|
|
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%);
|
|
3104
3313
|
--rf-shadow-float: 0 4px 12px -3px rgb(15 15 15 / 35%);
|
|
3105
3314
|
--rf-ease: cubic-bezier(0.16, 1, 0.3, 1);
|
|
3106
3315
|
color-scheme: light;
|
|
@@ -3124,8 +3333,9 @@ const WIDGET_STYLES = `
|
|
|
3124
3333
|
--rf-accent: #529cca;
|
|
3125
3334
|
--rf-danger: #eb5757;
|
|
3126
3335
|
--rf-success: #4dab74;
|
|
3127
|
-
--rf-
|
|
3128
|
-
--rf-shadow: 0
|
|
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%);
|
|
3129
3339
|
--rf-shadow-float: 0 4px 14px -3px rgb(0 0 0 / 55%);
|
|
3130
3340
|
color-scheme: dark;
|
|
3131
3341
|
}
|
|
@@ -3173,8 +3383,8 @@ svg { flex-shrink: 0; }
|
|
|
3173
3383
|
.launcher-icon { display: flex; width: 15px; height: 15px; }
|
|
3174
3384
|
.launcher-icon .on-close { display: none; }
|
|
3175
3385
|
.floating-controls {
|
|
3176
|
-
display: flex; align-items: center;
|
|
3177
|
-
|
|
3386
|
+
display: flex; align-items: center; flex: none;
|
|
3387
|
+
margin-right: auto; padding-left: 3px;
|
|
3178
3388
|
animation: rf-in 220ms var(--rf-ease);
|
|
3179
3389
|
}
|
|
3180
3390
|
.drag-handle { touch-action: none; cursor: grab; }
|
|
@@ -3358,140 +3568,18 @@ function startConsoleRecorder(options) {
|
|
|
3358
3568
|
};
|
|
3359
3569
|
}
|
|
3360
3570
|
//#endregion
|
|
3361
|
-
//#region src/feedback/core/element-capture.ts
|
|
3362
|
-
const MIN_CONTEXT_PADDING = 24;
|
|
3363
|
-
const MAX_CONTEXT_WIDTH = 2e3;
|
|
3364
|
-
const CLOSE_UP_MAX_WIDTH = 1200;
|
|
3365
|
-
const SELECTION_COLOR = "#4f46e5";
|
|
3366
|
-
const OUTLINE_COLOR = "#4f46e5";
|
|
3367
|
-
const OUTLINE_GLOW = "#ffffff";
|
|
3368
|
-
const OUTLINE_WIDTH = 3;
|
|
3369
|
-
const GLOW_WIDTH = 6;
|
|
3370
|
-
const DIM_OVERLAY = "rgba(15, 23, 42, 0.45)";
|
|
3371
|
-
/**
|
|
3372
|
-
* Tightest ancestor that keeps some breathing room around the pick — the zoom
|
|
3373
|
-
* stays zoomed instead of growing to a giant section. Only when nothing up
|
|
3374
|
-
* the tree clears the minimum does the widest candidate below the cap win.
|
|
3375
|
-
*/
|
|
3376
|
-
function contextRootFor(element) {
|
|
3377
|
-
const target = element.getBoundingClientRect();
|
|
3378
|
-
const coversAtLeast = (box, margin) => box.left <= target.left - margin && box.right >= target.right + margin && box.top <= target.top - margin && box.bottom >= target.bottom + margin;
|
|
3379
|
-
let tightest = null;
|
|
3380
|
-
let current = element.parentElement;
|
|
3381
|
-
while (current && current !== document.documentElement) {
|
|
3382
|
-
const box = current.getBoundingClientRect();
|
|
3383
|
-
if (coversAtLeast(box, MIN_CONTEXT_PADDING)) return current;
|
|
3384
|
-
tightest ??= box.width < MAX_CONTEXT_WIDTH ? current : null;
|
|
3385
|
-
current = current.parentElement;
|
|
3386
|
-
}
|
|
3387
|
-
return tightest ?? element;
|
|
3388
|
-
}
|
|
3389
|
-
/** Where the picked element sits inside the context shot, in image pixels. */
|
|
3390
|
-
function closeUpGeometry(params) {
|
|
3391
|
-
const scale = params.rootRect.width > 0 ? params.capture.width / params.rootRect.width : 1;
|
|
3392
|
-
return {
|
|
3393
|
-
height: Math.round(params.elementRect.height * scale),
|
|
3394
|
-
scale,
|
|
3395
|
-
width: Math.round(params.elementRect.width * scale),
|
|
3396
|
-
x: Math.round((params.elementRect.left - params.rootRect.left) * scale),
|
|
3397
|
-
y: Math.round((params.elementRect.top - params.rootRect.top) * scale)
|
|
3398
|
-
};
|
|
3399
|
-
}
|
|
3400
|
-
/** Rectangle annotation marking the picked element on the viewport shot. */
|
|
3401
|
-
function highlightFor(selection, capture) {
|
|
3402
|
-
const scale = capture.width / window.innerWidth;
|
|
3403
|
-
const { rect } = selection;
|
|
3404
|
-
return {
|
|
3405
|
-
color: SELECTION_COLOR,
|
|
3406
|
-
end: {
|
|
3407
|
-
x: (rect.x + rect.width) * scale,
|
|
3408
|
-
y: (rect.y + rect.height) * scale
|
|
3409
|
-
},
|
|
3410
|
-
id: `selection-${rect.x}-${rect.y}`,
|
|
3411
|
-
start: {
|
|
3412
|
-
x: rect.x * scale,
|
|
3413
|
-
y: rect.y * scale
|
|
3414
|
-
},
|
|
3415
|
-
tool: "rectangle"
|
|
3416
|
-
};
|
|
3417
|
-
}
|
|
3418
|
-
function drawZoneOverlay(context, zone) {
|
|
3419
|
-
context.save();
|
|
3420
|
-
context.fillStyle = DIM_OVERLAY;
|
|
3421
|
-
context.beginPath();
|
|
3422
|
-
context.rect(0, 0, context.canvas.width, context.canvas.height);
|
|
3423
|
-
context.rect(zone.x, zone.y, zone.width, zone.height);
|
|
3424
|
-
context.fill("evenodd");
|
|
3425
|
-
for (const [color, width] of [[OUTLINE_GLOW, GLOW_WIDTH], [OUTLINE_COLOR, OUTLINE_WIDTH]]) {
|
|
3426
|
-
context.strokeStyle = color;
|
|
3427
|
-
context.lineWidth = width * zone.scale;
|
|
3428
|
-
context.strokeRect(zone.x, zone.y, zone.width, zone.height);
|
|
3429
|
-
}
|
|
3430
|
-
context.restore();
|
|
3431
|
-
}
|
|
3432
|
-
function drawCloseUp(capture, zone) {
|
|
3433
|
-
return new Promise((resolve) => {
|
|
3434
|
-
const canvas = document.createElement("canvas");
|
|
3435
|
-
canvas.width = capture.width;
|
|
3436
|
-
canvas.height = capture.height;
|
|
3437
|
-
const context = canvas.getContext("2d");
|
|
3438
|
-
const image = new Image();
|
|
3439
|
-
image.onload = () => {
|
|
3440
|
-
if (!context) {
|
|
3441
|
-
resolve(null);
|
|
3442
|
-
return;
|
|
3443
|
-
}
|
|
3444
|
-
context.drawImage(image, 0, 0, capture.width, capture.height);
|
|
3445
|
-
drawZoneOverlay(context, zone);
|
|
3446
|
-
resolve(capturedFromCanvas(canvas, CLOSE_UP_MAX_WIDTH));
|
|
3447
|
-
};
|
|
3448
|
-
image.onerror = () => resolve(null);
|
|
3449
|
-
image.src = capture.objectUrl;
|
|
3450
|
-
});
|
|
3451
|
-
}
|
|
3452
|
-
/**
|
|
3453
|
-
* Zoomed view of the picked zone: the surrounding area captured for context,
|
|
3454
|
-
* everything outside the selection dimmed and the zone framed with a glowing
|
|
3455
|
-
* outline — unambiguous to eyes and vision models alike.
|
|
3456
|
-
* Falls back to the plain context shot when the overlay cannot be drawn.
|
|
3457
|
-
*/
|
|
3458
|
-
async function captureElementCloseUp(element) {
|
|
3459
|
-
try {
|
|
3460
|
-
const root = contextRootFor(element);
|
|
3461
|
-
const rootRect = root.getBoundingClientRect();
|
|
3462
|
-
const elementRect = element.getBoundingClientRect();
|
|
3463
|
-
const capture = await captureViewport({
|
|
3464
|
-
element: root,
|
|
3465
|
-
maxWidth: CLOSE_UP_MAX_WIDTH
|
|
3466
|
-
});
|
|
3467
|
-
if (!capture || root === element) return capture;
|
|
3468
|
-
const outlined = await drawCloseUp(capture, closeUpGeometry({
|
|
3469
|
-
capture,
|
|
3470
|
-
elementRect,
|
|
3471
|
-
rootRect
|
|
3472
|
-
}));
|
|
3473
|
-
if (outlined) {
|
|
3474
|
-
releaseCapture(capture);
|
|
3475
|
-
return outlined;
|
|
3476
|
-
}
|
|
3477
|
-
return capture;
|
|
3478
|
-
} catch {
|
|
3479
|
-
return null;
|
|
3480
|
-
}
|
|
3481
|
-
}
|
|
3482
|
-
//#endregion
|
|
3483
3571
|
//#region src/feedback/core/submission/upload-screenshots.ts
|
|
3484
3572
|
async function uploadImage(transport, image) {
|
|
3485
3573
|
const { uploadUrl } = await transport.getScreenshotUploadUrl();
|
|
3486
3574
|
return transport.uploadImage(uploadUrl, image);
|
|
3487
3575
|
}
|
|
3488
3576
|
async function prepareScreenshot(transport, screenshot, index) {
|
|
3489
|
-
const { image, annotations, context } = screenshot;
|
|
3577
|
+
const { closeUp, image, annotations, context } = screenshot;
|
|
3490
3578
|
const annotated = annotations.length > 0 ? await renderAnnotatedImage(image, annotations) : null;
|
|
3491
3579
|
if (annotations.length > 0 && !annotated) throw new Error("Could not render your annotations. Please try again.");
|
|
3492
3580
|
try {
|
|
3493
3581
|
const storageId = await uploadImage(transport, image);
|
|
3494
|
-
|
|
3582
|
+
const prepared = [{
|
|
3495
3583
|
annotatedStorageId: annotated ? await uploadImage(transport, annotated) : void 0,
|
|
3496
3584
|
annotations: annotations.length > 0 ? toWireAnnotations(annotations) : void 0,
|
|
3497
3585
|
captureSource: "widget",
|
|
@@ -3502,28 +3590,27 @@ async function prepareScreenshot(transport, screenshot, index) {
|
|
|
3502
3590
|
size: image.blob.size,
|
|
3503
3591
|
storageId,
|
|
3504
3592
|
width: image.width
|
|
3505
|
-
};
|
|
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;
|
|
3506
3605
|
} finally {
|
|
3507
3606
|
releaseCapture(annotated);
|
|
3508
3607
|
}
|
|
3509
3608
|
}
|
|
3510
3609
|
async function uploadScreenshots(transport, submission) {
|
|
3511
|
-
|
|
3610
|
+
return (await Promise.allSettled(submission.screenshots.map((screenshot, index) => prepareScreenshot(transport, screenshot, index)))).flatMap((result) => {
|
|
3512
3611
|
if (result.status === "rejected") throw result.reason;
|
|
3513
3612
|
return result.value;
|
|
3514
3613
|
});
|
|
3515
|
-
const element = submission.element;
|
|
3516
|
-
if (element) prepared.push({
|
|
3517
|
-
captureSource: "element",
|
|
3518
|
-
filename: "element.png",
|
|
3519
|
-
height: element.height,
|
|
3520
|
-
mimeType: element.mimeType,
|
|
3521
|
-
pageUrl: submission.context.url,
|
|
3522
|
-
size: element.blob.size,
|
|
3523
|
-
storageId: await uploadImage(transport, element),
|
|
3524
|
-
width: element.width
|
|
3525
|
-
});
|
|
3526
|
-
return prepared;
|
|
3527
3614
|
}
|
|
3528
3615
|
//#endregion
|
|
3529
3616
|
//#region src/feedback/core/submit.ts
|
|
@@ -3643,36 +3730,28 @@ function useFeedbackSubmission(options) {
|
|
|
3643
3730
|
};
|
|
3644
3731
|
}
|
|
3645
3732
|
//#endregion
|
|
3646
|
-
//#region src/feedback/ui/state/use-owned-capture.ts
|
|
3647
|
-
function useOwnedCapture() {
|
|
3648
|
-
const [image, setImage] = useState(null);
|
|
3649
|
-
const ownedImage = useRef(null);
|
|
3650
|
-
useEffect(() => () => releaseCapture(ownedImage.current), []);
|
|
3651
|
-
return [image, useCallback((nextImage) => {
|
|
3652
|
-
releaseCapture(ownedImage.current);
|
|
3653
|
-
ownedImage.current = nextImage;
|
|
3654
|
-
setImage(nextImage);
|
|
3655
|
-
}, [])];
|
|
3656
|
-
}
|
|
3657
|
-
//#endregion
|
|
3658
3733
|
//#region src/feedback/ui/state/use-screenshot-drafts.ts
|
|
3734
|
+
function releaseDraft(draft) {
|
|
3735
|
+
releaseCapture(draft?.image);
|
|
3736
|
+
releaseCapture(draft?.closeUp);
|
|
3737
|
+
}
|
|
3659
3738
|
function useScreenshotDrafts(captureFailed, onError) {
|
|
3660
3739
|
const [screenshots, setScreenshots] = useState([]);
|
|
3661
3740
|
const [activeId, setActiveId] = useState(null);
|
|
3662
3741
|
const [pendingCapture, setPendingCapture] = useState(null);
|
|
3663
|
-
const
|
|
3742
|
+
const ownedDrafts = useRef(/* @__PURE__ */ new Map());
|
|
3664
3743
|
const captureRequest = useRef(null);
|
|
3665
3744
|
useEffect(() => {
|
|
3666
|
-
const
|
|
3745
|
+
const drafts = ownedDrafts.current;
|
|
3667
3746
|
return () => {
|
|
3668
3747
|
captureRequest.current = null;
|
|
3669
|
-
for (const
|
|
3670
|
-
|
|
3748
|
+
for (const draft of drafts.values()) releaseDraft(draft);
|
|
3749
|
+
drafts.clear();
|
|
3671
3750
|
};
|
|
3672
3751
|
}, []);
|
|
3673
3752
|
const storeScreenshot = useCallback((screenshot) => {
|
|
3674
|
-
|
|
3675
|
-
|
|
3753
|
+
releaseDraft(ownedDrafts.current.get(screenshot.id));
|
|
3754
|
+
ownedDrafts.current.set(screenshot.id, screenshot);
|
|
3676
3755
|
setScreenshots((previous) => {
|
|
3677
3756
|
return previous.some(({ id }) => id === screenshot.id) ? previous.map((item) => item.id === screenshot.id ? screenshot : item) : [...previous, screenshot];
|
|
3678
3757
|
});
|
|
@@ -3717,27 +3796,27 @@ function useScreenshotDrafts(captureFailed, onError) {
|
|
|
3717
3796
|
}, [takeScreenshot]);
|
|
3718
3797
|
const refreshAutomatic = useCallback(() => {
|
|
3719
3798
|
takeScreenshot({
|
|
3720
|
-
id:
|
|
3799
|
+
id: ownedDrafts.current.keys().next().value ?? captureRequest.current?.id ?? crypto.randomUUID(),
|
|
3721
3800
|
source: "automatic"
|
|
3722
3801
|
});
|
|
3723
3802
|
}, [takeScreenshot]);
|
|
3724
3803
|
const retakeCapture = useCallback((id) => {
|
|
3725
|
-
if (!captureRequest.current &&
|
|
3804
|
+
if (!captureRequest.current && ownedDrafts.current.has(id)) takeScreenshot({
|
|
3726
3805
|
id,
|
|
3727
3806
|
source: "manual"
|
|
3728
3807
|
});
|
|
3729
3808
|
}, [takeScreenshot]);
|
|
3730
3809
|
const removeCapture = useCallback((id) => {
|
|
3731
3810
|
if (captureRequest.current?.id === id) cancelPendingCapture();
|
|
3732
|
-
|
|
3733
|
-
|
|
3811
|
+
releaseDraft(ownedDrafts.current.get(id));
|
|
3812
|
+
ownedDrafts.current.delete(id);
|
|
3734
3813
|
setScreenshots((previous) => previous.filter((item) => item.id !== id));
|
|
3735
3814
|
setActiveId((current) => current === id ? null : current);
|
|
3736
3815
|
}, [cancelPendingCapture]);
|
|
3737
3816
|
const resetScreenshots = useCallback(() => {
|
|
3738
3817
|
cancelPendingCapture();
|
|
3739
|
-
for (const
|
|
3740
|
-
|
|
3818
|
+
for (const draft of ownedDrafts.current.values()) releaseDraft(draft);
|
|
3819
|
+
ownedDrafts.current.clear();
|
|
3741
3820
|
setScreenshots([]);
|
|
3742
3821
|
setActiveId(null);
|
|
3743
3822
|
}, [cancelPendingCapture]);
|
|
@@ -3747,16 +3826,18 @@ function useScreenshotDrafts(captureFailed, onError) {
|
|
|
3747
3826
|
annotations
|
|
3748
3827
|
} : item));
|
|
3749
3828
|
}, [activeId]);
|
|
3750
|
-
const
|
|
3751
|
-
setScreenshots((previous) => previous.map((item) =>
|
|
3829
|
+
const setSelectionComment = useCallback((id, comment) => {
|
|
3830
|
+
setScreenshots((previous) => previous.map((item) => item.id === id && item.selection ? {
|
|
3752
3831
|
...item,
|
|
3753
|
-
|
|
3754
|
-
|
|
3832
|
+
selection: {
|
|
3833
|
+
...item.selection,
|
|
3834
|
+
comment
|
|
3835
|
+
}
|
|
3836
|
+
} : item));
|
|
3755
3837
|
}, []);
|
|
3756
3838
|
return {
|
|
3757
3839
|
activeScreenshot: screenshots.find(({ id }) => id === activeId) ?? screenshots.at(-1) ?? null,
|
|
3758
3840
|
cancelPendingCapture,
|
|
3759
|
-
clearSelectionAnnotations,
|
|
3760
3841
|
pendingCapture,
|
|
3761
3842
|
refreshAutomatic,
|
|
3762
3843
|
removeCapture,
|
|
@@ -3765,6 +3846,7 @@ function useScreenshotDrafts(captureFailed, onError) {
|
|
|
3765
3846
|
screenshots,
|
|
3766
3847
|
selectScreenshot: setActiveId,
|
|
3767
3848
|
setAnnotations,
|
|
3849
|
+
setSelectionComment,
|
|
3768
3850
|
storeScreenshot,
|
|
3769
3851
|
takeCapture
|
|
3770
3852
|
};
|
|
@@ -3885,9 +3967,6 @@ function useWidgetState(props) {
|
|
|
3885
3967
|
const [message, setMessage] = useState("");
|
|
3886
3968
|
const [email, setEmail] = useState("");
|
|
3887
3969
|
const [honeypot, setHoneypot] = useState("");
|
|
3888
|
-
const [elementCapture, replaceElementCapture] = useOwnedCapture();
|
|
3889
|
-
const [selection, setSelection] = useState(null);
|
|
3890
|
-
const [selectedNode, setSelectedNode] = useState(null);
|
|
3891
3970
|
const [isElementCapturing, setIsElementCapturing] = useState(false);
|
|
3892
3971
|
const successTimer = useRef(null);
|
|
3893
3972
|
const [error, setError] = useState(null);
|
|
@@ -3912,7 +3991,7 @@ function useWidgetState(props) {
|
|
|
3912
3991
|
}, [props.captureConsole]);
|
|
3913
3992
|
const captureFailed = props.labels?.captureFailed ?? DEFAULT_WIDGET_LABELS.captureFailed;
|
|
3914
3993
|
const gallery = useScreenshotDrafts(captureFailed, setError);
|
|
3915
|
-
const { activeScreenshot, cancelPendingCapture,
|
|
3994
|
+
const { activeScreenshot, cancelPendingCapture, refreshAutomatic, resetScreenshots, storeScreenshot } = gallery;
|
|
3916
3995
|
const capture = activeScreenshot?.image ?? null;
|
|
3917
3996
|
const annotations = activeScreenshot?.annotations ?? [];
|
|
3918
3997
|
const isCapturing = gallery.pendingCapture !== null || isElementCapturing;
|
|
@@ -3929,7 +4008,7 @@ function useWidgetState(props) {
|
|
|
3929
4008
|
const { resetSubmission } = submission;
|
|
3930
4009
|
const firstCapture = gallery.pendingCapture ?? gallery.screenshots[0];
|
|
3931
4010
|
const canRefreshAutomatically = gallery.screenshots.length <= 1 && firstCapture?.source === "automatic";
|
|
3932
|
-
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);
|
|
3933
4012
|
const reset = useCallback(() => {
|
|
3934
4013
|
if (successTimer.current) clearTimeout(successTimer.current);
|
|
3935
4014
|
setAnnotationTrigger(null);
|
|
@@ -3937,19 +4016,12 @@ function useWidgetState(props) {
|
|
|
3937
4016
|
pickRef.current++;
|
|
3938
4017
|
setIsElementCapturing(false);
|
|
3939
4018
|
resetScreenshots();
|
|
3940
|
-
replaceElementCapture(null);
|
|
3941
|
-
setSelection(null);
|
|
3942
|
-
setSelectedNode(null);
|
|
3943
4019
|
setMessage("");
|
|
3944
4020
|
setEmail("");
|
|
3945
4021
|
setHoneypot("");
|
|
3946
4022
|
setError(null);
|
|
3947
4023
|
setStep("compose");
|
|
3948
|
-
}, [
|
|
3949
|
-
resetSubmission,
|
|
3950
|
-
resetScreenshots,
|
|
3951
|
-
replaceElementCapture
|
|
3952
|
-
]);
|
|
4024
|
+
}, [resetSubmission, resetScreenshots]);
|
|
3953
4025
|
const close = useCallback(() => {
|
|
3954
4026
|
setIsOpen(false);
|
|
3955
4027
|
reset();
|
|
@@ -3986,8 +4058,6 @@ function useWidgetState(props) {
|
|
|
3986
4058
|
...buildElementSelection(element),
|
|
3987
4059
|
comment: note.slice(0, MAX_SELECTION_COMMENT_LENGTH)
|
|
3988
4060
|
} : buildElementSelection(element);
|
|
3989
|
-
setSelection(picked);
|
|
3990
|
-
setSelectedNode(element);
|
|
3991
4061
|
setStep("compose");
|
|
3992
4062
|
const pick = ++pickRef.current;
|
|
3993
4063
|
const context = collectPageContext({ sdkVersion: SDK_VERSION });
|
|
@@ -3998,49 +4068,55 @@ function useWidgetState(props) {
|
|
|
3998
4068
|
return;
|
|
3999
4069
|
}
|
|
4000
4070
|
setIsElementCapturing(false);
|
|
4001
|
-
if (fresh)
|
|
4071
|
+
if (!fresh) {
|
|
4072
|
+
releaseCapture(closeUp);
|
|
4073
|
+
setError(captureFailed);
|
|
4074
|
+
return;
|
|
4075
|
+
}
|
|
4076
|
+
storeScreenshot({
|
|
4002
4077
|
annotations: [highlightFor(picked, fresh)],
|
|
4078
|
+
closeUp,
|
|
4003
4079
|
context,
|
|
4004
4080
|
id: crypto.randomUUID(),
|
|
4005
4081
|
image: fresh,
|
|
4082
|
+
selectedNode: element,
|
|
4083
|
+
selection: picked,
|
|
4006
4084
|
source: "manual"
|
|
4007
4085
|
});
|
|
4008
|
-
else setError(captureFailed);
|
|
4009
|
-
replaceElementCapture(closeUp);
|
|
4010
4086
|
}, [
|
|
4011
4087
|
cancelPendingCapture,
|
|
4012
4088
|
captureFailed,
|
|
4013
|
-
storeScreenshot
|
|
4014
|
-
replaceElementCapture
|
|
4089
|
+
storeScreenshot
|
|
4015
4090
|
]);
|
|
4016
|
-
const
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
setSelection(null);
|
|
4020
|
-
setSelectedNode(null);
|
|
4021
|
-
setIsElementCapturing(false);
|
|
4022
|
-
clearSelectionAnnotations();
|
|
4023
|
-
}, [clearSelectionAnnotations, replaceElementCapture]);
|
|
4024
|
-
const reportedMessage = message.trim() || selection?.comment || "";
|
|
4091
|
+
const selections = gallery.screenshots.flatMap(({ selection }) => selection ? [selection] : []);
|
|
4092
|
+
const firstElementNote = selections.find(({ comment }) => comment)?.comment;
|
|
4093
|
+
const reportedMessage = message.trim() || firstElementNote || "";
|
|
4025
4094
|
const submit = () => {
|
|
4026
4095
|
if (honeypot) {
|
|
4027
4096
|
setStep("success");
|
|
4028
4097
|
return;
|
|
4029
4098
|
}
|
|
4099
|
+
setStep("compose");
|
|
4030
4100
|
return submission.submit({
|
|
4031
4101
|
context: {
|
|
4032
|
-
...activeScreenshot?.context ?? collectPageContext({ sdkVersion: "0.4.
|
|
4102
|
+
...activeScreenshot?.context ?? collectPageContext({ sdkVersion: "0.4.1" }),
|
|
4033
4103
|
consoleEvents: consoleRef.current?.events(),
|
|
4034
4104
|
metadata: props.metadata,
|
|
4035
|
-
|
|
4105
|
+
selections
|
|
4036
4106
|
},
|
|
4037
|
-
element: elementCapture,
|
|
4038
4107
|
email,
|
|
4039
4108
|
isAnonymous,
|
|
4040
4109
|
message: reportedMessage,
|
|
4041
4110
|
screenshots: gallery.screenshots
|
|
4042
4111
|
});
|
|
4043
4112
|
};
|
|
4113
|
+
const requestSubmit = () => {
|
|
4114
|
+
if (isAnonymous && email.length === 0 && !submission.hasPendingAttachments && !honeypot) {
|
|
4115
|
+
setStep("email");
|
|
4116
|
+
return;
|
|
4117
|
+
}
|
|
4118
|
+
return submit();
|
|
4119
|
+
};
|
|
4044
4120
|
return {
|
|
4045
4121
|
activeScreenshot,
|
|
4046
4122
|
annotateScreenshot: (id, trigger) => {
|
|
@@ -4053,11 +4129,9 @@ function useWidgetState(props) {
|
|
|
4053
4129
|
canDismiss: dismissForDays !== null,
|
|
4054
4130
|
canSubmit: reportedMessage.length > 0,
|
|
4055
4131
|
capture,
|
|
4056
|
-
clearSelection,
|
|
4057
4132
|
close,
|
|
4058
4133
|
dismiss,
|
|
4059
4134
|
dismissForDays,
|
|
4060
|
-
elementCapture,
|
|
4061
4135
|
email,
|
|
4062
4136
|
error,
|
|
4063
4137
|
hasPendingAttachments: submission.hasPendingAttachments,
|
|
@@ -4072,15 +4146,16 @@ function useWidgetState(props) {
|
|
|
4072
4146
|
open,
|
|
4073
4147
|
pendingCapture: gallery.pendingCapture,
|
|
4074
4148
|
removeCapture: gallery.removeCapture,
|
|
4149
|
+
requestSubmit,
|
|
4075
4150
|
retakeCapture: gallery.retakeCapture,
|
|
4076
4151
|
screenshots: gallery.screenshots,
|
|
4077
4152
|
selectElement,
|
|
4078
|
-
|
|
4079
|
-
selection,
|
|
4153
|
+
selections,
|
|
4080
4154
|
setAnnotations: gallery.setAnnotations,
|
|
4081
4155
|
setEmail,
|
|
4082
4156
|
setHoneypot,
|
|
4083
4157
|
setMessage,
|
|
4158
|
+
setSelectionComment: gallery.setSelectionComment,
|
|
4084
4159
|
setStep,
|
|
4085
4160
|
step,
|
|
4086
4161
|
submit,
|
|
@@ -4137,13 +4212,19 @@ function RefletFeedback(props) {
|
|
|
4137
4212
|
annotations: state.annotations,
|
|
4138
4213
|
onChange: state.setAnnotations,
|
|
4139
4214
|
onDone: () => state.setStep("compose"),
|
|
4140
|
-
onRetake: () => {
|
|
4215
|
+
onRetake: state.activeScreenshot.selection ? void 0 : () => {
|
|
4141
4216
|
state.setStep("compose");
|
|
4142
4217
|
if (state.activeScreenshot) state.retakeCapture(state.activeScreenshot.id);
|
|
4143
4218
|
},
|
|
4144
|
-
trigger: state.annotationTrigger
|
|
4219
|
+
trigger: state.annotationTrigger?.thumbnail
|
|
4145
4220
|
},
|
|
4146
|
-
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
|
+
}
|
|
4147
4228
|
}),
|
|
4148
4229
|
isOpen && state.step === "picking" && /* @__PURE__ */ jsx(ElementPicker, {
|
|
4149
4230
|
labels,
|