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/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.0";
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, multiple } = item;
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 glass",
1904
- children: [/* @__PURE__ */ jsxs("button", {
1905
- "aria-label": labels.annotateHint,
1906
- className: "screenshot-preview",
1907
- disabled,
1908
- onClick: (event) => state.annotateScreenshot(draft.id, event.currentTarget),
1909
- type: "button",
1910
- children: [/* @__PURE__ */ jsx(ScreenshotPreview, {
1911
- annotations: draft.annotations,
1912
- capture: draft.image
1913
- }), /* @__PURE__ */ jsxs("span", {
1914
- className: "attachment-caption glass",
1915
- children: [
1916
- /* @__PURE__ */ jsx(PencilIcon, {}),
1917
- /* @__PURE__ */ jsx("span", {
1918
- className: "attachment-caption-label",
1919
- children: labels.screenshot
1920
- }),
1921
- multiple && /* @__PURE__ */ jsx("span", { children: number }),
1922
- draft.annotations.length > 0 && /* @__PURE__ */ jsx("span", {
1923
- className: "annotation-count",
1924
- children: draft.annotations.length
1925
- })
1926
- ]
1927
- })]
1928
- }), isCapturing ? /* @__PURE__ */ jsxs("div", {
1929
- className: "capture-progress glass",
1930
- role: "status",
1931
- children: [/* @__PURE__ */ jsx("span", { className: "spinner" }), labels.capturing]
1932
- }) : /* @__PURE__ */ jsxs("div", {
1933
- className: "attachment-actions glass",
1934
- children: [/* @__PURE__ */ jsx("button", {
1935
- "aria-label": labels.recapture,
1936
- className: "icon-btn",
1937
- disabled,
1938
- onClick: () => state.retakeCapture(draft.id),
1939
- title: labels.recapture,
1940
- type: "button",
1941
- children: /* @__PURE__ */ jsx(CameraIcon, {})
1942
- }), /* @__PURE__ */ jsx("button", {
1943
- "aria-label": labels.removeScreenshot,
1944
- className: "icon-btn",
1945
- disabled,
1946
- onClick: () => state.removeCapture(draft.id),
1947
- title: labels.removeScreenshot,
1948
- type: "button",
1949
- children: /* @__PURE__ */ jsx(TrashIcon, {})
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__ */ jsxs("div", {
2125
+ return /* @__PURE__ */ jsx("div", {
2004
2126
  className: "attachments",
2005
- children: [/* @__PURE__ */ jsx(SelectedElement, {
2006
- labels,
2007
- state
2008
- }), /* @__PURE__ */ jsxs(ScrollArea.Root, {
2009
- className: "screenshot-scroll-area",
2010
- children: [/* @__PURE__ */ jsx(ScrollArea.Viewport, {
2011
- "aria-label": labels.screenshot,
2012
- className: "screenshot-strip",
2013
- ref: stripRef,
2014
- role: "region",
2015
- children: /* @__PURE__ */ jsxs(ScrollArea.Content, {
2016
- className: "screenshot-strip-content",
2017
- children: [state.screenshots.map((draft, index) => /* @__PURE__ */ jsx(ScreenshotCard, {
2018
- item: {
2019
- draft,
2020
- multiple: count > 1,
2021
- number: index + 1
2022
- },
2023
- labels,
2024
- state
2025
- }, draft.id)), isAdding && /* @__PURE__ */ jsxs("div", {
2026
- "aria-label": labels.capturing,
2027
- className: "attachment-skeleton glass",
2028
- role: "status",
2029
- children: [/* @__PURE__ */ jsx(CameraIcon, {}), /* @__PURE__ */ jsx("span", { children: labels.capturing })]
2030
- })]
2031
- })
2032
- }), /* @__PURE__ */ jsx(ScrollArea.Scrollbar, {
2033
- className: "screenshot-scrollbar",
2034
- orientation: "horizontal",
2035
- children: /* @__PURE__ */ jsx(ScrollArea.Thumb, { className: "screenshot-scroll-thumb" })
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: expanded ? 3 : 1,
2307
+ rows: 3,
2162
2308
  value: state.message
2163
- }), /* @__PURE__ */ jsx("button", {
2164
- "aria-busy": state.isSubmitting,
2165
- "aria-label": submitLabel,
2166
- className: "submit",
2167
- disabled: state.isSubmitting || state.isCapturing || !state.canSubmit,
2168
- title: submitLabel,
2169
- type: "submit",
2170
- children: state.isSubmitting ? /* @__PURE__ */ jsx("span", { className: "spinner" }) : /* @__PURE__ */ jsx(ArrowIcon, {})
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.submit();
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
- if (bounds && gesture.current) moveTo({
2392
- x: event.clientX - gesture.current.x,
2393
- y: event.clientY - gesture.current.y
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: state.selectedNode }),
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 glass",
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(MinusIcon, {})
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; align-items: flex-end; gap: 4px; padding: 4px; border-radius: 26px; }
3055
+ .message-row { display: flex; flex-direction: column; gap: 2px; padding: 6px; border-radius: 26px; }
2868
3056
  .message-row textarea {
2869
- flex: 1; width: 0; min-height: 36px; height: 36px; max-height: min(140px, 30dvh);
2870
- margin: 0; padding: 8px 10px 8px 12px; border: 0; border-radius: 20px; resize: none;
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
- .panel:focus-within .message-row[data-expanded="true"] textarea { height: 76px; }
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: 36px; height: 36px; 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); }
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
- .screenshot-scroll-area { position: relative; min-width: 0; max-width: calc(100% + 8px); margin: -4px -4px -8px; }
2897
- .screenshot-strip { width: 100%; padding: 4px 4px 8px; 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; }
2898
- .screenshot-strip[data-overflow-x-start] { --rf-fade-start: min(24px, var(--scroll-area-overflow-x-start)); }
2899
- .screenshot-strip[data-overflow-x-end] { --rf-fade-end: min(24px, var(--scroll-area-overflow-x-end)); }
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
- .attachments:has(.screenshot-strip-content:empty):not(:has(.selection-chip)) { display: none; }
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: 3px; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 6px; border-radius: 16px; color: var(--rf-text-muted); font-size: 10px; }
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: 78px; padding: 3px; border-radius: var(--rf-radius); animation: rf-in 260ms var(--rf-ease); }
2909
- .screenshot-preview { display: block; position: relative; width: 100%; height: 100%; padding: 0; overflow: hidden; border-radius: 16px; }
2910
- .screenshot-preview img, .screenshot-preview canvas { width: 100%; height: 100%; display: block; object-fit: cover; object-position: top center; outline: 1px solid var(--rf-hairline); outline-offset: -1px; }
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
- .attachment-caption { position: absolute; left: 5px; bottom: 5px; display: flex; align-items: center; gap: 4px; padding: 3px 6px; font-size: 10px; font-variant-numeric: tabular-nums; border-radius: var(--rf-pill); color: var(--rf-text); }
2913
- .attachment-caption svg { width: 12px; height: 12px; }
2914
- .annotation-count { color: var(--rf-text); font-weight: 600; }
2915
- .attachment-actions { position: absolute; right: 4px; bottom: -4px; display: flex; gap: 1px; padding: 1px; border-radius: var(--rf-pill); opacity: 0; pointer-events: none; transform: translateY(3px); transition: opacity 150ms var(--rf-ease), transform 150ms var(--rf-ease); }
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-chip { display: flex; align-items: center; max-width: 100%; padding: 3px; border-radius: var(--rf-pill); }
2921
- .selection-label { display: flex; align-items: center; min-width: 0; gap: 6px; padding: 0 6px; font-size: 11px; }
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
- .message-row textarea { font-size: 16px; min-height: 44px; height: 44px; padding-top: 12px; }
3124
+ .capture-slot { width: 64px; }
3125
+ .message-row textarea { font-size: 16px; }
2927
3126
  .submit { width: 44px; height: 44px; }
2928
- .attachment-actions { opacity: 1; pointer-events: auto; transform: none; }
2929
- .attachment-caption { top: 4px; bottom: auto; right: 4px; left: auto; }
2930
- .attachment-caption-label { display: none; }
2931
- .screenshot-attachment:hover .attachment-caption, .screenshot-attachment:focus-within .attachment-caption { opacity: 1; }
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, .selection-label { min-height: 44px; }
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-shadow-low: 0 0 0 1px rgb(15 15 15 / 4%), 0 1px 2px rgb(15 15 15 / 6%), 0 4px 10px -4px rgb(15 15 15 / 10%);
3103
- --rf-shadow: 0 0 0 1px rgb(15 15 15 / 4%), 0 2px 5px -1px rgb(15 15 15 / 7%), 0 12px 32px -8px rgb(15 15 15 / 16%);
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-shadow-low: 0 0 0 1px rgb(255 255 255 / 6%), 0 1px 2px rgb(0 0 0 / 30%), 0 4px 10px -4px rgb(0 0 0 / 40%);
3128
- --rf-shadow: 0 0 0 1px rgb(255 255 255 / 7%), 0 2px 5px -1px rgb(0 0 0 / 35%), 0 14px 36px -10px rgb(0 0 0 / 55%);
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; padding: 3px; flex: none;
3177
- border-radius: var(--rf-pill);
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
- return {
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
- const prepared = (await Promise.allSettled(submission.screenshots.map((screenshot, index) => prepareScreenshot(transport, screenshot, index)))).map((result) => {
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 ownedImages = useRef(/* @__PURE__ */ new Map());
3742
+ const ownedDrafts = useRef(/* @__PURE__ */ new Map());
3664
3743
  const captureRequest = useRef(null);
3665
3744
  useEffect(() => {
3666
- const images = ownedImages.current;
3745
+ const drafts = ownedDrafts.current;
3667
3746
  return () => {
3668
3747
  captureRequest.current = null;
3669
- for (const image of images.values()) releaseCapture(image);
3670
- images.clear();
3748
+ for (const draft of drafts.values()) releaseDraft(draft);
3749
+ drafts.clear();
3671
3750
  };
3672
3751
  }, []);
3673
3752
  const storeScreenshot = useCallback((screenshot) => {
3674
- releaseCapture(ownedImages.current.get(screenshot.id));
3675
- ownedImages.current.set(screenshot.id, screenshot.image);
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: ownedImages.current.keys().next().value ?? captureRequest.current?.id ?? crypto.randomUUID(),
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 && ownedImages.current.has(id)) takeScreenshot({
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
- releaseCapture(ownedImages.current.get(id));
3733
- ownedImages.current.delete(id);
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 image of ownedImages.current.values()) releaseCapture(image);
3740
- ownedImages.current.clear();
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 clearSelectionAnnotations = useCallback(() => {
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
- annotations: item.annotations.filter(({ id }) => !id.startsWith("selection-"))
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, clearSelectionAnnotations, refreshAutomatic, resetScreenshots, storeScreenshot } = gallery;
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) storeScreenshot({
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 clearSelection = useCallback(() => {
4017
- pickRef.current++;
4018
- replaceElementCapture(null);
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.0" }),
4102
+ ...activeScreenshot?.context ?? collectPageContext({ sdkVersion: "0.4.1" }),
4033
4103
  consoleEvents: consoleRef.current?.events(),
4034
4104
  metadata: props.metadata,
4035
- selection: selection ?? void 0
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
- selectedNode,
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,