reflet-sdk 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -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 +537 -441
- 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.2";
|
|
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,26 +2609,35 @@ 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;
|
|
2441
2623
|
const onKeyDown = (event) => {
|
|
2442
|
-
if (event.key === "Escape" && !event.defaultPrevented) {
|
|
2624
|
+
if (event instanceof KeyboardEvent && event.key === "Escape" && !event.defaultPrevented) {
|
|
2443
2625
|
setMinimized(true);
|
|
2444
2626
|
requestAnimationFrame(() => launcherRef.current?.focus());
|
|
2445
2627
|
}
|
|
2446
2628
|
};
|
|
2629
|
+
const widgetRoot = floating.rootRef.current?.getRootNode();
|
|
2447
2630
|
document.addEventListener("keydown", onKeyDown);
|
|
2448
|
-
|
|
2449
|
-
|
|
2631
|
+
widgetRoot?.addEventListener("keydown", onKeyDown);
|
|
2632
|
+
return () => {
|
|
2633
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
2634
|
+
widgetRoot?.removeEventListener("keydown", onKeyDown);
|
|
2635
|
+
};
|
|
2636
|
+
}, [
|
|
2637
|
+
floating.rootRef,
|
|
2638
|
+
showPanel,
|
|
2639
|
+
state.step
|
|
2640
|
+
]);
|
|
2450
2641
|
return /* @__PURE__ */ jsxs("div", {
|
|
2451
2642
|
className: "root",
|
|
2452
2643
|
"data-editing": state.isOpen && (state.step === "picking" || state.step === "annotate"),
|
|
@@ -2455,10 +2646,10 @@ function FloatingWidget({ labels, position, state }) {
|
|
|
2455
2646
|
ref: floating.rootRef,
|
|
2456
2647
|
children: [
|
|
2457
2648
|
/* @__PURE__ */ jsx("style", { children: floating.styles }),
|
|
2458
|
-
showPanel && state.step === "compose" && /* @__PURE__ */ jsx(SelectionOutline, { node:
|
|
2649
|
+
showPanel && state.step === "compose" && state.screenshots.map(({ id, selectedNode }) => selectedNode ? /* @__PURE__ */ jsx(SelectionOutline, { node: selectedNode }, id) : null),
|
|
2459
2650
|
showPanel ? /* @__PURE__ */ jsx(FeedbackPanel, {
|
|
2460
2651
|
floatingControls: /* @__PURE__ */ jsxs("div", {
|
|
2461
|
-
className: "floating-controls
|
|
2652
|
+
className: "floating-controls",
|
|
2462
2653
|
children: [/* @__PURE__ */ jsx("button", {
|
|
2463
2654
|
"aria-label": labels.moveFeedback,
|
|
2464
2655
|
className: "icon-btn drag-handle",
|
|
@@ -2472,7 +2663,7 @@ function FloatingWidget({ labels, position, state }) {
|
|
|
2472
2663
|
onClick: minimize,
|
|
2473
2664
|
title: labels.minimize,
|
|
2474
2665
|
type: "button",
|
|
2475
|
-
children: /* @__PURE__ */ jsx(
|
|
2666
|
+
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
2476
2667
|
})]
|
|
2477
2668
|
}),
|
|
2478
2669
|
labels,
|
|
@@ -2807,9 +2998,15 @@ const ANNOTATOR_STYLES = `
|
|
|
2807
2998
|
.overlay[data-opening="true"] .editor { pointer-events: none; }
|
|
2808
2999
|
.overlay[data-closing="true"] { pointer-events: none; }
|
|
2809
3000
|
.overlay[data-closing="true"]::before { opacity: 0; }
|
|
2810
|
-
.overlay[data-closing="true"] .toolbar { opacity: 0; transform: translateY(8px); }
|
|
3001
|
+
.overlay[data-closing="true"] .toolbar, .overlay[data-closing="true"] .element-note { opacity: 0; transform: translateY(8px); }
|
|
3002
|
+
|
|
3003
|
+
.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); }
|
|
3004
|
+
.element-note svg { flex: none; margin-top: 4px; opacity: 0.7; }
|
|
3005
|
+
.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; }
|
|
3006
|
+
.element-note textarea:focus { outline: none; }
|
|
3007
|
+
.element-note:focus-within { outline: 2px solid var(--rf-accent); outline-offset: 2px; }
|
|
2811
3008
|
|
|
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)); }
|
|
3009
|
+
.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
3010
|
.toolbar .group { display: flex; align-items: center; gap: 2px; padding: 4px; border-radius: var(--rf-pill); }
|
|
2814
3011
|
.tool[aria-pressed="true"], .tool[data-selected="true"] { background: var(--rf-primary); color: var(--rf-primary-text); }
|
|
2815
3012
|
.color-picker { position: relative; }
|
|
@@ -2864,22 +3061,21 @@ const COMPOSER_STYLES = `
|
|
|
2864
3061
|
.panel { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 8px; outline: none; animation: rf-in 240ms var(--rf-ease); }
|
|
2865
3062
|
.success-panel { border-radius: var(--rf-radius); }
|
|
2866
3063
|
.composer { position: relative; display: flex; flex-direction: column; gap: 8px; }
|
|
2867
|
-
.message-row { display: flex;
|
|
3064
|
+
.message-row { display: flex; flex-direction: column; gap: 2px; padding: 6px; border-radius: 26px; }
|
|
2868
3065
|
.message-row textarea {
|
|
2869
|
-
|
|
2870
|
-
margin: 0; padding:
|
|
3066
|
+
width: 100%; height: 76px; max-height: min(140px, 30dvh);
|
|
3067
|
+
margin: 0; padding: 6px 6px 0 8px; border: 0; resize: none;
|
|
2871
3068
|
background: transparent; line-height: 20px;
|
|
2872
3069
|
}
|
|
2873
|
-
.
|
|
3070
|
+
.message-actions { display: flex; align-items: center; gap: 2px; }
|
|
2874
3071
|
.message-row textarea::placeholder { color: var(--rf-text-muted); }
|
|
2875
3072
|
.message-row textarea:focus { outline: none; }
|
|
2876
3073
|
.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:
|
|
3074
|
+
.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
3075
|
.submit svg { transform: rotate(-45deg); }
|
|
2879
3076
|
.submit:hover:enabled { transform: scale(1.05); }
|
|
2880
3077
|
.submit:disabled { background: var(--rf-bg-hover); color: var(--rf-text-muted); opacity: .65; }
|
|
2881
3078
|
.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
3079
|
.composer-options { position: relative; }
|
|
2884
3080
|
summary { cursor: pointer; list-style: none; }
|
|
2885
3081
|
summary::-webkit-details-marker { display: none; }
|
|
@@ -2892,47 +3088,67 @@ summary::-webkit-details-marker { display: none; }
|
|
|
2892
3088
|
.dismiss-btn:hover { background: var(--rf-bg-hover); }
|
|
2893
3089
|
.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
3090
|
.version-info:hover { color: var(--rf-text); }
|
|
3091
|
+
.email-scrim { display: none; }
|
|
3092
|
+
.email-prompt { display: flex; flex-direction: column; gap: 12px; padding: 16px; border-radius: 22px; animation: rf-in 240ms var(--rf-ease); }
|
|
3093
|
+
.email-prompt h2 { margin: 0; font-size: 14px; font-weight: 600; line-height: 1.3; }
|
|
3094
|
+
.email-prompt p { margin: 0; font-size: 12px; line-height: 1.45; color: var(--rf-text-muted); }
|
|
3095
|
+
.email-prompt .email-field input { min-height: 40px; border-radius: 14px; padding: 0 12px; }
|
|
3096
|
+
.email-prompt-actions { display: flex; align-items: center; justify-content: flex-end; gap: 4px; }
|
|
3097
|
+
.email-prompt-actions .ghost-btn { padding: 0 12px; color: var(--rf-text-muted); }
|
|
3098
|
+
.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; }
|
|
3099
|
+
.primary-btn:hover { opacity: .9; }
|
|
2895
3100
|
.attachments { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }
|
|
2896
|
-
.
|
|
2897
|
-
.screenshot-
|
|
2898
|
-
.screenshot-strip
|
|
2899
|
-
.screenshot-strip[data-overflow-x-
|
|
3101
|
+
.attachment-row { display: flex; align-items: flex-end; gap: 8px; min-width: 0; max-width: 100%; }
|
|
3102
|
+
.screenshot-scroll-area { position: relative; min-width: 0; margin: -14px -10px -20px; }
|
|
3103
|
+
.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; }
|
|
3104
|
+
.screenshot-strip[data-overflow-x-start] { --rf-fade-start: calc(10px + min(24px, var(--scroll-area-overflow-x-start))); }
|
|
3105
|
+
.screenshot-strip[data-overflow-x-end] { --rf-fade-end: calc(10px + min(24px, var(--scroll-area-overflow-x-end))); }
|
|
2900
3106
|
.screenshot-strip-content { display: flex; align-items: flex-end; gap: 8px; }
|
|
2901
3107
|
.screenshot-scroll-area:has(.screenshot-strip-content:empty) { display: none; }
|
|
2902
|
-
.
|
|
3108
|
+
.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; }
|
|
3109
|
+
.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); }
|
|
3110
|
+
.capture-action:first-child { border-radius: 16px 16px 5px 5px; }
|
|
3111
|
+
.capture-action:last-child { border-radius: 5px 5px 16px 16px; }
|
|
3112
|
+
.capture-action:hover:enabled { background: var(--rf-bg-hover); color: var(--rf-text); }
|
|
2903
3113
|
.screenshot-scrollbar { height: 4px; margin: 0 8px 2px; opacity: 0; transition: opacity 150ms var(--rf-ease); }
|
|
2904
3114
|
.screenshot-scrollbar[data-hovering], .screenshot-scrollbar[data-scrolling] { opacity: 1; }
|
|
2905
3115
|
.screenshot-scroll-thumb { border-radius: var(--rf-pill); background: var(--rf-border); }
|
|
2906
|
-
.capture-progress { position: absolute; inset:
|
|
3116
|
+
.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
3117
|
.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
|
|
3118
|
+
.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); }
|
|
3119
|
+
.screenshot-attachment { border: 0; }
|
|
3120
|
+
.screenshot-preview { display: block; position: relative; width: 100%; height: 100%; padding: 0; border-radius: inherit; overflow: hidden; }
|
|
3121
|
+
.screenshot-preview img, .screenshot-preview canvas { width: 100%; height: 100%; display: block; object-fit: cover; object-position: top center; }
|
|
2911
3122
|
.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; }
|
|
3123
|
+
.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); }
|
|
3124
|
+
.attachment-actions { position: absolute; right: 4px; bottom: 4px; display: flex; gap: 1px; padding: 1px; border-radius: var(--rf-pill); }
|
|
3125
|
+
.attachment-actions .icon-btn { width: 28px; height: 28px; }
|
|
3126
|
+
.attachment-actions svg { width: 14px; height: 14px; }
|
|
2918
3127
|
.attachment-skeleton { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: var(--rf-text-muted); font-size: 11px; }
|
|
2919
3128
|
.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; }
|
|
3129
|
+
.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); }
|
|
3130
|
+
.selection-badge svg { width: 12px; height: 12px; }
|
|
2924
3131
|
@media (pointer: coarse) {
|
|
2925
3132
|
.message-row { border-radius: 28px; }
|
|
2926
|
-
.
|
|
3133
|
+
.capture-slot { width: 64px; }
|
|
3134
|
+
.message-row textarea { font-size: 16px; }
|
|
2927
3135
|
.submit { width: 44px; height: 44px; }
|
|
2928
|
-
|
|
2929
|
-
.attachment-
|
|
2930
|
-
.attachment-
|
|
2931
|
-
.
|
|
2932
|
-
.screenshot-attachment, .attachment-skeleton { height: 96px; }
|
|
3136
|
+
:host { --rf-attachment-height: 96px; }
|
|
3137
|
+
.attachment-actions { right: 5px; bottom: 5px; }
|
|
3138
|
+
.attachment-actions .icon-btn { width: 34px; height: 34px; }
|
|
3139
|
+
.selection-badge { width: 24px; height: 24px; }
|
|
2933
3140
|
.email-field input { font-size: 16px; }
|
|
2934
|
-
.dismiss-btn
|
|
3141
|
+
.dismiss-btn { min-height: 44px; }
|
|
2935
3142
|
}
|
|
3143
|
+
@media (max-width: 480px) {
|
|
3144
|
+
.email-scrim { display: block; position: fixed; inset: 0; background: rgb(0 0 0 / 40%); animation: rf-fade 200ms var(--rf-ease); }
|
|
3145
|
+
.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); }
|
|
3146
|
+
.email-prompt h2 { font-size: 17px; }
|
|
3147
|
+
.email-prompt p { font-size: 13px; }
|
|
3148
|
+
.email-prompt-actions { flex-direction: column-reverse; align-items: stretch; gap: 8px; }
|
|
3149
|
+
.email-prompt-actions .ghost-btn, .email-prompt-actions .primary-btn { justify-content: center; min-height: 48px; }
|
|
3150
|
+
}
|
|
3151
|
+
@keyframes rf-drawer-in { from { transform: translateY(100%); } }
|
|
2936
3152
|
`;
|
|
2937
3153
|
//#endregion
|
|
2938
3154
|
//#region src/feedback/ui/floating/picker-styles.ts
|
|
@@ -2997,7 +3213,7 @@ const PICKER_STYLES = `
|
|
|
2997
3213
|
color: var(--rf-text);
|
|
2998
3214
|
background: var(--rf-bg);
|
|
2999
3215
|
border-radius: var(--rf-pill);
|
|
3000
|
-
box-shadow: var(--rf-shadow);
|
|
3216
|
+
box-shadow: var(--rf-ring), var(--rf-shadow);
|
|
3001
3217
|
animation: rf-fade 240ms var(--rf-ease);
|
|
3002
3218
|
}
|
|
3003
3219
|
.picker-instruction { display: flex; align-items: center; gap: 6px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -3093,14 +3309,16 @@ const WIDGET_STYLES = `
|
|
|
3093
3309
|
--rf-spectrum: conic-gradient(#ff5757, #ffde59, #6bdb78, #4ac9fa, #8b5cf6, #ef73dc, #ff5757);
|
|
3094
3310
|
--rf-radius: 20px;
|
|
3095
3311
|
--rf-pill: 999px;
|
|
3312
|
+
--rf-attachment-height: 78px;
|
|
3096
3313
|
--rf-width: 360px;
|
|
3097
3314
|
--rf-glass: rgb(255 255 255 / 72%);
|
|
3098
3315
|
--rf-glass-highlight: rgb(255 255 255 / 90%);
|
|
3099
3316
|
--rf-glass-stroke: rgb(255 255 255 / 70%);
|
|
3100
3317
|
--rf-radius-sm: 8px;
|
|
3101
3318
|
--rf-shadow-inset: inset 0 0 0 1px var(--rf-border);
|
|
3102
|
-
--rf-
|
|
3103
|
-
--rf-shadow: 0
|
|
3319
|
+
--rf-ring: 0 0 0 1px rgb(15 15 15 / 4%);
|
|
3320
|
+
--rf-shadow-low: 0 1px 2px rgb(15 15 15 / 6%), 0 4px 10px -4px rgb(15 15 15 / 10%);
|
|
3321
|
+
--rf-shadow: 0 2px 5px -1px rgb(15 15 15 / 7%), 0 12px 32px -8px rgb(15 15 15 / 16%);
|
|
3104
3322
|
--rf-shadow-float: 0 4px 12px -3px rgb(15 15 15 / 35%);
|
|
3105
3323
|
--rf-ease: cubic-bezier(0.16, 1, 0.3, 1);
|
|
3106
3324
|
color-scheme: light;
|
|
@@ -3124,8 +3342,9 @@ const WIDGET_STYLES = `
|
|
|
3124
3342
|
--rf-accent: #529cca;
|
|
3125
3343
|
--rf-danger: #eb5757;
|
|
3126
3344
|
--rf-success: #4dab74;
|
|
3127
|
-
--rf-
|
|
3128
|
-
--rf-shadow: 0
|
|
3345
|
+
--rf-ring: 0 0 0 1px rgb(255 255 255 / 7%);
|
|
3346
|
+
--rf-shadow-low: 0 1px 2px rgb(0 0 0 / 30%), 0 4px 10px -4px rgb(0 0 0 / 40%);
|
|
3347
|
+
--rf-shadow: 0 2px 5px -1px rgb(0 0 0 / 35%), 0 14px 36px -10px rgb(0 0 0 / 55%);
|
|
3129
3348
|
--rf-shadow-float: 0 4px 14px -3px rgb(0 0 0 / 55%);
|
|
3130
3349
|
color-scheme: dark;
|
|
3131
3350
|
}
|
|
@@ -3173,8 +3392,8 @@ svg { flex-shrink: 0; }
|
|
|
3173
3392
|
.launcher-icon { display: flex; width: 15px; height: 15px; }
|
|
3174
3393
|
.launcher-icon .on-close { display: none; }
|
|
3175
3394
|
.floating-controls {
|
|
3176
|
-
display: flex; align-items: center;
|
|
3177
|
-
|
|
3395
|
+
display: flex; align-items: center; flex: none;
|
|
3396
|
+
margin-right: auto; padding-left: 3px;
|
|
3178
3397
|
animation: rf-in 220ms var(--rf-ease);
|
|
3179
3398
|
}
|
|
3180
3399
|
.drag-handle { touch-action: none; cursor: grab; }
|
|
@@ -3236,6 +3455,11 @@ ${PICKER_STYLES}
|
|
|
3236
3455
|
//#endregion
|
|
3237
3456
|
//#region src/feedback/ui/shadow-portal.tsx
|
|
3238
3457
|
const DARK_QUERY = "(prefers-color-scheme: dark)";
|
|
3458
|
+
const KEY_EVENTS = [
|
|
3459
|
+
"keydown",
|
|
3460
|
+
"keyup",
|
|
3461
|
+
"keypress"
|
|
3462
|
+
];
|
|
3239
3463
|
function useResolvedTheme(theme) {
|
|
3240
3464
|
const [systemDark, setSystemDark] = useState(false);
|
|
3241
3465
|
useEffect(() => {
|
|
@@ -3261,6 +3485,7 @@ function ShadowPortal({ children, offset, primaryColor, theme }) {
|
|
|
3261
3485
|
const host = document.createElement("div");
|
|
3262
3486
|
host.setAttribute(WIDGET_MARKER, "");
|
|
3263
3487
|
const root = host.attachShadow({ mode: "open" });
|
|
3488
|
+
for (const type of KEY_EVENTS) root.addEventListener(type, (event) => event.stopPropagation());
|
|
3264
3489
|
const style = document.createElement("style");
|
|
3265
3490
|
style.textContent = WIDGET_STYLES;
|
|
3266
3491
|
root.append(style);
|
|
@@ -3358,140 +3583,18 @@ function startConsoleRecorder(options) {
|
|
|
3358
3583
|
};
|
|
3359
3584
|
}
|
|
3360
3585
|
//#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
3586
|
//#region src/feedback/core/submission/upload-screenshots.ts
|
|
3484
3587
|
async function uploadImage(transport, image) {
|
|
3485
3588
|
const { uploadUrl } = await transport.getScreenshotUploadUrl();
|
|
3486
3589
|
return transport.uploadImage(uploadUrl, image);
|
|
3487
3590
|
}
|
|
3488
3591
|
async function prepareScreenshot(transport, screenshot, index) {
|
|
3489
|
-
const { image, annotations, context } = screenshot;
|
|
3592
|
+
const { closeUp, image, annotations, context } = screenshot;
|
|
3490
3593
|
const annotated = annotations.length > 0 ? await renderAnnotatedImage(image, annotations) : null;
|
|
3491
3594
|
if (annotations.length > 0 && !annotated) throw new Error("Could not render your annotations. Please try again.");
|
|
3492
3595
|
try {
|
|
3493
3596
|
const storageId = await uploadImage(transport, image);
|
|
3494
|
-
|
|
3597
|
+
const prepared = [{
|
|
3495
3598
|
annotatedStorageId: annotated ? await uploadImage(transport, annotated) : void 0,
|
|
3496
3599
|
annotations: annotations.length > 0 ? toWireAnnotations(annotations) : void 0,
|
|
3497
3600
|
captureSource: "widget",
|
|
@@ -3502,28 +3605,27 @@ async function prepareScreenshot(transport, screenshot, index) {
|
|
|
3502
3605
|
size: image.blob.size,
|
|
3503
3606
|
storageId,
|
|
3504
3607
|
width: image.width
|
|
3505
|
-
};
|
|
3608
|
+
}];
|
|
3609
|
+
if (closeUp) prepared.push({
|
|
3610
|
+
captureSource: "element",
|
|
3611
|
+
filename: `element-${index + 1}.png`,
|
|
3612
|
+
height: closeUp.height,
|
|
3613
|
+
mimeType: closeUp.mimeType,
|
|
3614
|
+
pageUrl: context.url,
|
|
3615
|
+
size: closeUp.blob.size,
|
|
3616
|
+
storageId: await uploadImage(transport, closeUp),
|
|
3617
|
+
width: closeUp.width
|
|
3618
|
+
});
|
|
3619
|
+
return prepared;
|
|
3506
3620
|
} finally {
|
|
3507
3621
|
releaseCapture(annotated);
|
|
3508
3622
|
}
|
|
3509
3623
|
}
|
|
3510
3624
|
async function uploadScreenshots(transport, submission) {
|
|
3511
|
-
|
|
3625
|
+
return (await Promise.allSettled(submission.screenshots.map((screenshot, index) => prepareScreenshot(transport, screenshot, index)))).flatMap((result) => {
|
|
3512
3626
|
if (result.status === "rejected") throw result.reason;
|
|
3513
3627
|
return result.value;
|
|
3514
3628
|
});
|
|
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
3629
|
}
|
|
3528
3630
|
//#endregion
|
|
3529
3631
|
//#region src/feedback/core/submit.ts
|
|
@@ -3643,36 +3745,28 @@ function useFeedbackSubmission(options) {
|
|
|
3643
3745
|
};
|
|
3644
3746
|
}
|
|
3645
3747
|
//#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
3748
|
//#region src/feedback/ui/state/use-screenshot-drafts.ts
|
|
3749
|
+
function releaseDraft(draft) {
|
|
3750
|
+
releaseCapture(draft?.image);
|
|
3751
|
+
releaseCapture(draft?.closeUp);
|
|
3752
|
+
}
|
|
3659
3753
|
function useScreenshotDrafts(captureFailed, onError) {
|
|
3660
3754
|
const [screenshots, setScreenshots] = useState([]);
|
|
3661
3755
|
const [activeId, setActiveId] = useState(null);
|
|
3662
3756
|
const [pendingCapture, setPendingCapture] = useState(null);
|
|
3663
|
-
const
|
|
3757
|
+
const ownedDrafts = useRef(/* @__PURE__ */ new Map());
|
|
3664
3758
|
const captureRequest = useRef(null);
|
|
3665
3759
|
useEffect(() => {
|
|
3666
|
-
const
|
|
3760
|
+
const drafts = ownedDrafts.current;
|
|
3667
3761
|
return () => {
|
|
3668
3762
|
captureRequest.current = null;
|
|
3669
|
-
for (const
|
|
3670
|
-
|
|
3763
|
+
for (const draft of drafts.values()) releaseDraft(draft);
|
|
3764
|
+
drafts.clear();
|
|
3671
3765
|
};
|
|
3672
3766
|
}, []);
|
|
3673
3767
|
const storeScreenshot = useCallback((screenshot) => {
|
|
3674
|
-
|
|
3675
|
-
|
|
3768
|
+
releaseDraft(ownedDrafts.current.get(screenshot.id));
|
|
3769
|
+
ownedDrafts.current.set(screenshot.id, screenshot);
|
|
3676
3770
|
setScreenshots((previous) => {
|
|
3677
3771
|
return previous.some(({ id }) => id === screenshot.id) ? previous.map((item) => item.id === screenshot.id ? screenshot : item) : [...previous, screenshot];
|
|
3678
3772
|
});
|
|
@@ -3717,27 +3811,27 @@ function useScreenshotDrafts(captureFailed, onError) {
|
|
|
3717
3811
|
}, [takeScreenshot]);
|
|
3718
3812
|
const refreshAutomatic = useCallback(() => {
|
|
3719
3813
|
takeScreenshot({
|
|
3720
|
-
id:
|
|
3814
|
+
id: ownedDrafts.current.keys().next().value ?? captureRequest.current?.id ?? crypto.randomUUID(),
|
|
3721
3815
|
source: "automatic"
|
|
3722
3816
|
});
|
|
3723
3817
|
}, [takeScreenshot]);
|
|
3724
3818
|
const retakeCapture = useCallback((id) => {
|
|
3725
|
-
if (!captureRequest.current &&
|
|
3819
|
+
if (!captureRequest.current && ownedDrafts.current.has(id)) takeScreenshot({
|
|
3726
3820
|
id,
|
|
3727
3821
|
source: "manual"
|
|
3728
3822
|
});
|
|
3729
3823
|
}, [takeScreenshot]);
|
|
3730
3824
|
const removeCapture = useCallback((id) => {
|
|
3731
3825
|
if (captureRequest.current?.id === id) cancelPendingCapture();
|
|
3732
|
-
|
|
3733
|
-
|
|
3826
|
+
releaseDraft(ownedDrafts.current.get(id));
|
|
3827
|
+
ownedDrafts.current.delete(id);
|
|
3734
3828
|
setScreenshots((previous) => previous.filter((item) => item.id !== id));
|
|
3735
3829
|
setActiveId((current) => current === id ? null : current);
|
|
3736
3830
|
}, [cancelPendingCapture]);
|
|
3737
3831
|
const resetScreenshots = useCallback(() => {
|
|
3738
3832
|
cancelPendingCapture();
|
|
3739
|
-
for (const
|
|
3740
|
-
|
|
3833
|
+
for (const draft of ownedDrafts.current.values()) releaseDraft(draft);
|
|
3834
|
+
ownedDrafts.current.clear();
|
|
3741
3835
|
setScreenshots([]);
|
|
3742
3836
|
setActiveId(null);
|
|
3743
3837
|
}, [cancelPendingCapture]);
|
|
@@ -3747,16 +3841,18 @@ function useScreenshotDrafts(captureFailed, onError) {
|
|
|
3747
3841
|
annotations
|
|
3748
3842
|
} : item));
|
|
3749
3843
|
}, [activeId]);
|
|
3750
|
-
const
|
|
3751
|
-
setScreenshots((previous) => previous.map((item) =>
|
|
3844
|
+
const setSelectionComment = useCallback((id, comment) => {
|
|
3845
|
+
setScreenshots((previous) => previous.map((item) => item.id === id && item.selection ? {
|
|
3752
3846
|
...item,
|
|
3753
|
-
|
|
3754
|
-
|
|
3847
|
+
selection: {
|
|
3848
|
+
...item.selection,
|
|
3849
|
+
comment
|
|
3850
|
+
}
|
|
3851
|
+
} : item));
|
|
3755
3852
|
}, []);
|
|
3756
3853
|
return {
|
|
3757
3854
|
activeScreenshot: screenshots.find(({ id }) => id === activeId) ?? screenshots.at(-1) ?? null,
|
|
3758
3855
|
cancelPendingCapture,
|
|
3759
|
-
clearSelectionAnnotations,
|
|
3760
3856
|
pendingCapture,
|
|
3761
3857
|
refreshAutomatic,
|
|
3762
3858
|
removeCapture,
|
|
@@ -3765,6 +3861,7 @@ function useScreenshotDrafts(captureFailed, onError) {
|
|
|
3765
3861
|
screenshots,
|
|
3766
3862
|
selectScreenshot: setActiveId,
|
|
3767
3863
|
setAnnotations,
|
|
3864
|
+
setSelectionComment,
|
|
3768
3865
|
storeScreenshot,
|
|
3769
3866
|
takeCapture
|
|
3770
3867
|
};
|
|
@@ -3885,9 +3982,6 @@ function useWidgetState(props) {
|
|
|
3885
3982
|
const [message, setMessage] = useState("");
|
|
3886
3983
|
const [email, setEmail] = useState("");
|
|
3887
3984
|
const [honeypot, setHoneypot] = useState("");
|
|
3888
|
-
const [elementCapture, replaceElementCapture] = useOwnedCapture();
|
|
3889
|
-
const [selection, setSelection] = useState(null);
|
|
3890
|
-
const [selectedNode, setSelectedNode] = useState(null);
|
|
3891
3985
|
const [isElementCapturing, setIsElementCapturing] = useState(false);
|
|
3892
3986
|
const successTimer = useRef(null);
|
|
3893
3987
|
const [error, setError] = useState(null);
|
|
@@ -3912,7 +4006,7 @@ function useWidgetState(props) {
|
|
|
3912
4006
|
}, [props.captureConsole]);
|
|
3913
4007
|
const captureFailed = props.labels?.captureFailed ?? DEFAULT_WIDGET_LABELS.captureFailed;
|
|
3914
4008
|
const gallery = useScreenshotDrafts(captureFailed, setError);
|
|
3915
|
-
const { activeScreenshot, cancelPendingCapture,
|
|
4009
|
+
const { activeScreenshot, cancelPendingCapture, refreshAutomatic, resetScreenshots, storeScreenshot } = gallery;
|
|
3916
4010
|
const capture = activeScreenshot?.image ?? null;
|
|
3917
4011
|
const annotations = activeScreenshot?.annotations ?? [];
|
|
3918
4012
|
const isCapturing = gallery.pendingCapture !== null || isElementCapturing;
|
|
@@ -3929,7 +4023,7 @@ function useWidgetState(props) {
|
|
|
3929
4023
|
const { resetSubmission } = submission;
|
|
3930
4024
|
const firstCapture = gallery.pendingCapture ?? gallery.screenshots[0];
|
|
3931
4025
|
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);
|
|
4026
|
+
useCaptureSync(isOpen && !submission.isEditingDisabled && step === "compose" && canRefreshAutomatically && annotations.length === 0 && !gallery.screenshots.some((draft) => draft.selection) && props.captureOnOpen !== false, refreshAutomatic);
|
|
3933
4027
|
const reset = useCallback(() => {
|
|
3934
4028
|
if (successTimer.current) clearTimeout(successTimer.current);
|
|
3935
4029
|
setAnnotationTrigger(null);
|
|
@@ -3937,19 +4031,12 @@ function useWidgetState(props) {
|
|
|
3937
4031
|
pickRef.current++;
|
|
3938
4032
|
setIsElementCapturing(false);
|
|
3939
4033
|
resetScreenshots();
|
|
3940
|
-
replaceElementCapture(null);
|
|
3941
|
-
setSelection(null);
|
|
3942
|
-
setSelectedNode(null);
|
|
3943
4034
|
setMessage("");
|
|
3944
4035
|
setEmail("");
|
|
3945
4036
|
setHoneypot("");
|
|
3946
4037
|
setError(null);
|
|
3947
4038
|
setStep("compose");
|
|
3948
|
-
}, [
|
|
3949
|
-
resetSubmission,
|
|
3950
|
-
resetScreenshots,
|
|
3951
|
-
replaceElementCapture
|
|
3952
|
-
]);
|
|
4039
|
+
}, [resetSubmission, resetScreenshots]);
|
|
3953
4040
|
const close = useCallback(() => {
|
|
3954
4041
|
setIsOpen(false);
|
|
3955
4042
|
reset();
|
|
@@ -3986,8 +4073,6 @@ function useWidgetState(props) {
|
|
|
3986
4073
|
...buildElementSelection(element),
|
|
3987
4074
|
comment: note.slice(0, MAX_SELECTION_COMMENT_LENGTH)
|
|
3988
4075
|
} : buildElementSelection(element);
|
|
3989
|
-
setSelection(picked);
|
|
3990
|
-
setSelectedNode(element);
|
|
3991
4076
|
setStep("compose");
|
|
3992
4077
|
const pick = ++pickRef.current;
|
|
3993
4078
|
const context = collectPageContext({ sdkVersion: SDK_VERSION });
|
|
@@ -3998,49 +4083,55 @@ function useWidgetState(props) {
|
|
|
3998
4083
|
return;
|
|
3999
4084
|
}
|
|
4000
4085
|
setIsElementCapturing(false);
|
|
4001
|
-
if (fresh)
|
|
4086
|
+
if (!fresh) {
|
|
4087
|
+
releaseCapture(closeUp);
|
|
4088
|
+
setError(captureFailed);
|
|
4089
|
+
return;
|
|
4090
|
+
}
|
|
4091
|
+
storeScreenshot({
|
|
4002
4092
|
annotations: [highlightFor(picked, fresh)],
|
|
4093
|
+
closeUp,
|
|
4003
4094
|
context,
|
|
4004
4095
|
id: crypto.randomUUID(),
|
|
4005
4096
|
image: fresh,
|
|
4097
|
+
selectedNode: element,
|
|
4098
|
+
selection: picked,
|
|
4006
4099
|
source: "manual"
|
|
4007
4100
|
});
|
|
4008
|
-
else setError(captureFailed);
|
|
4009
|
-
replaceElementCapture(closeUp);
|
|
4010
4101
|
}, [
|
|
4011
4102
|
cancelPendingCapture,
|
|
4012
4103
|
captureFailed,
|
|
4013
|
-
storeScreenshot
|
|
4014
|
-
replaceElementCapture
|
|
4104
|
+
storeScreenshot
|
|
4015
4105
|
]);
|
|
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 || "";
|
|
4106
|
+
const selections = gallery.screenshots.flatMap(({ selection }) => selection ? [selection] : []);
|
|
4107
|
+
const firstElementNote = selections.find(({ comment }) => comment)?.comment;
|
|
4108
|
+
const reportedMessage = message.trim() || firstElementNote || "";
|
|
4025
4109
|
const submit = () => {
|
|
4026
4110
|
if (honeypot) {
|
|
4027
4111
|
setStep("success");
|
|
4028
4112
|
return;
|
|
4029
4113
|
}
|
|
4114
|
+
setStep("compose");
|
|
4030
4115
|
return submission.submit({
|
|
4031
4116
|
context: {
|
|
4032
|
-
...activeScreenshot?.context ?? collectPageContext({ sdkVersion: "0.4.
|
|
4117
|
+
...activeScreenshot?.context ?? collectPageContext({ sdkVersion: "0.4.2" }),
|
|
4033
4118
|
consoleEvents: consoleRef.current?.events(),
|
|
4034
4119
|
metadata: props.metadata,
|
|
4035
|
-
|
|
4120
|
+
selections
|
|
4036
4121
|
},
|
|
4037
|
-
element: elementCapture,
|
|
4038
4122
|
email,
|
|
4039
4123
|
isAnonymous,
|
|
4040
4124
|
message: reportedMessage,
|
|
4041
4125
|
screenshots: gallery.screenshots
|
|
4042
4126
|
});
|
|
4043
4127
|
};
|
|
4128
|
+
const requestSubmit = () => {
|
|
4129
|
+
if (isAnonymous && email.length === 0 && !submission.hasPendingAttachments && !honeypot) {
|
|
4130
|
+
setStep("email");
|
|
4131
|
+
return;
|
|
4132
|
+
}
|
|
4133
|
+
return submit();
|
|
4134
|
+
};
|
|
4044
4135
|
return {
|
|
4045
4136
|
activeScreenshot,
|
|
4046
4137
|
annotateScreenshot: (id, trigger) => {
|
|
@@ -4053,11 +4144,9 @@ function useWidgetState(props) {
|
|
|
4053
4144
|
canDismiss: dismissForDays !== null,
|
|
4054
4145
|
canSubmit: reportedMessage.length > 0,
|
|
4055
4146
|
capture,
|
|
4056
|
-
clearSelection,
|
|
4057
4147
|
close,
|
|
4058
4148
|
dismiss,
|
|
4059
4149
|
dismissForDays,
|
|
4060
|
-
elementCapture,
|
|
4061
4150
|
email,
|
|
4062
4151
|
error,
|
|
4063
4152
|
hasPendingAttachments: submission.hasPendingAttachments,
|
|
@@ -4072,15 +4161,16 @@ function useWidgetState(props) {
|
|
|
4072
4161
|
open,
|
|
4073
4162
|
pendingCapture: gallery.pendingCapture,
|
|
4074
4163
|
removeCapture: gallery.removeCapture,
|
|
4164
|
+
requestSubmit,
|
|
4075
4165
|
retakeCapture: gallery.retakeCapture,
|
|
4076
4166
|
screenshots: gallery.screenshots,
|
|
4077
4167
|
selectElement,
|
|
4078
|
-
|
|
4079
|
-
selection,
|
|
4168
|
+
selections,
|
|
4080
4169
|
setAnnotations: gallery.setAnnotations,
|
|
4081
4170
|
setEmail,
|
|
4082
4171
|
setHoneypot,
|
|
4083
4172
|
setMessage,
|
|
4173
|
+
setSelectionComment: gallery.setSelectionComment,
|
|
4084
4174
|
setStep,
|
|
4085
4175
|
step,
|
|
4086
4176
|
submit,
|
|
@@ -4106,8 +4196,8 @@ function RefletFeedback(props) {
|
|
|
4106
4196
|
else open();
|
|
4107
4197
|
}
|
|
4108
4198
|
};
|
|
4109
|
-
document.addEventListener("keydown", onKeyDown);
|
|
4110
|
-
return () => document.removeEventListener("keydown", onKeyDown);
|
|
4199
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
4200
|
+
return () => document.removeEventListener("keydown", onKeyDown, true);
|
|
4111
4201
|
}, [
|
|
4112
4202
|
close,
|
|
4113
4203
|
enabled,
|
|
@@ -4137,13 +4227,19 @@ function RefletFeedback(props) {
|
|
|
4137
4227
|
annotations: state.annotations,
|
|
4138
4228
|
onChange: state.setAnnotations,
|
|
4139
4229
|
onDone: () => state.setStep("compose"),
|
|
4140
|
-
onRetake: () => {
|
|
4230
|
+
onRetake: state.activeScreenshot.selection ? void 0 : () => {
|
|
4141
4231
|
state.setStep("compose");
|
|
4142
4232
|
if (state.activeScreenshot) state.retakeCapture(state.activeScreenshot.id);
|
|
4143
4233
|
},
|
|
4144
|
-
trigger: state.annotationTrigger
|
|
4234
|
+
trigger: state.annotationTrigger?.thumbnail
|
|
4145
4235
|
},
|
|
4146
|
-
labels
|
|
4236
|
+
labels,
|
|
4237
|
+
note: state.activeScreenshot.selection && {
|
|
4238
|
+
comment: state.activeScreenshot.selection.comment ?? "",
|
|
4239
|
+
onChange: (comment) => {
|
|
4240
|
+
if (state.activeScreenshot) state.setSelectionComment(state.activeScreenshot.id, comment);
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4147
4243
|
}),
|
|
4148
4244
|
isOpen && state.step === "picking" && /* @__PURE__ */ jsx(ElementPicker, {
|
|
4149
4245
|
labels,
|