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/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.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, 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,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
- return () => document.removeEventListener("keydown", onKeyDown);
2449
- }, [showPanel, state.step]);
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: state.selectedNode }),
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 glass",
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(MinusIcon, {})
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; align-items: flex-end; gap: 4px; padding: 4px; border-radius: 26px; }
3064
+ .message-row { display: flex; flex-direction: column; gap: 2px; padding: 6px; border-radius: 26px; }
2868
3065
  .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;
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
- .panel:focus-within .message-row[data-expanded="true"] textarea { height: 76px; }
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: 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); }
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
- .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)); }
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
- .attachments:has(.screenshot-strip-content:empty):not(:has(.selection-chip)) { display: none; }
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: 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; }
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: 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; }
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
- .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; }
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-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; }
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
- .message-row textarea { font-size: 16px; min-height: 44px; height: 44px; padding-top: 12px; }
3133
+ .capture-slot { width: 64px; }
3134
+ .message-row textarea { font-size: 16px; }
2927
3135
  .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; }
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, .selection-label { min-height: 44px; }
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-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%);
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-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%);
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; padding: 3px; flex: none;
3177
- border-radius: var(--rf-pill);
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
- return {
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
- const prepared = (await Promise.allSettled(submission.screenshots.map((screenshot, index) => prepareScreenshot(transport, screenshot, index)))).map((result) => {
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 ownedImages = useRef(/* @__PURE__ */ new Map());
3757
+ const ownedDrafts = useRef(/* @__PURE__ */ new Map());
3664
3758
  const captureRequest = useRef(null);
3665
3759
  useEffect(() => {
3666
- const images = ownedImages.current;
3760
+ const drafts = ownedDrafts.current;
3667
3761
  return () => {
3668
3762
  captureRequest.current = null;
3669
- for (const image of images.values()) releaseCapture(image);
3670
- images.clear();
3763
+ for (const draft of drafts.values()) releaseDraft(draft);
3764
+ drafts.clear();
3671
3765
  };
3672
3766
  }, []);
3673
3767
  const storeScreenshot = useCallback((screenshot) => {
3674
- releaseCapture(ownedImages.current.get(screenshot.id));
3675
- ownedImages.current.set(screenshot.id, screenshot.image);
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: ownedImages.current.keys().next().value ?? captureRequest.current?.id ?? crypto.randomUUID(),
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 && ownedImages.current.has(id)) takeScreenshot({
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
- releaseCapture(ownedImages.current.get(id));
3733
- ownedImages.current.delete(id);
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 image of ownedImages.current.values()) releaseCapture(image);
3740
- ownedImages.current.clear();
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 clearSelectionAnnotations = useCallback(() => {
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
- annotations: item.annotations.filter(({ id }) => !id.startsWith("selection-"))
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, clearSelectionAnnotations, refreshAutomatic, resetScreenshots, storeScreenshot } = gallery;
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) storeScreenshot({
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 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 || "";
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.0" }),
4117
+ ...activeScreenshot?.context ?? collectPageContext({ sdkVersion: "0.4.2" }),
4033
4118
  consoleEvents: consoleRef.current?.events(),
4034
4119
  metadata: props.metadata,
4035
- selection: selection ?? void 0
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
- selectedNode,
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,