reflet-sdk 0.3.5 → 0.4.1

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