feedbackkit-web 1.0.42 → 1.0.45

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.
@@ -440,7 +440,8 @@ async function encodePayload(report, projectKey, identity = {}) {
440
440
  created_at: report.createdAt.toISOString(),
441
441
  text: report.text,
442
442
  annotations: report.annotations,
443
- environment: report.environment
443
+ environment: report.environment,
444
+ notify_reporter: report.notifyReporter ?? false
444
445
  };
445
446
  if (report.screenshotRaw && report.screenshotAnnotated) {
446
447
  const [raw, annotated] = await Promise.all([
@@ -495,7 +496,7 @@ async function submitReport(report, configuration2, identity = {}) {
495
496
  } catch {
496
497
  }
497
498
  const detail = code ? ` (${code})` : "";
498
- const message = code === "origin_not_allowed" ? `This site (${location.origin}) isn't in the project's allowed origins.` : response.status === 401 || response.status === 403 ? `Server rejected the project key or origin (HTTP ${response.status})${detail}.` : response.status === 429 ? "Too many reports right now — please try again in a minute." : `Server returned HTTP ${response.status}${detail}.`;
499
+ const message = code === "origin_not_allowed" ? `This site (${location.origin}) isn't in the project's allowed origins.` : response.status === 401 || response.status === 403 ? `Server rejected the project key or origin (HTTP ${response.status})${detail}.` : response.status === 429 ? "Too many reports right now — please try again in a minute." : response.status === 402 ? "Feedback for this site is paused this month. Please try again later." : `Server returned HTTP ${response.status}${detail}.`;
499
500
  throw new FeedbackSubmissionError(message, "server", response.status);
500
501
  }
501
502
  }
@@ -641,9 +642,7 @@ const icons = {
641
642
  undo: svg('<path d="M9 14L4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>'),
642
643
  trash: svg('<path d="M4 7h16"/><path d="M10 11v6M14 11v6"/><path d="M6 7l1 13h10l1-13"/><path d="M9 7V4h6v3"/>'),
643
644
  close: svg('<path d="M6 6l12 12M18 6L6 18"/>'),
644
- paperclip: svg(
645
- '<path d="M21 11.5l-8.6 8.6a5.5 5.5 0 0 1-7.8-7.8l8.6-8.6a3.7 3.7 0 0 1 5.2 5.2l-8.6 8.6a1.8 1.8 0 0 1-2.6-2.6l7.9-7.9"/>'
646
- ),
645
+ plus: svg('<path d="M12 5v14M5 12h14"/>'),
647
646
  send: svg('<path d="M4 12l16-8-6 16-3-7-7-1Z"/>'),
648
647
  check: svg('<path d="M5 12.5l4.5 4.5L19 7.5"/>')
649
648
  };
@@ -1527,6 +1526,16 @@ button:disabled { cursor: not-allowed; opacity: 0.45; }
1527
1526
  color: var(--fk-secondary);
1528
1527
  font-size: 13px;
1529
1528
  }
1529
+ .fk-options-left { display: flex; align-items: center; gap: 8px; min-width: 0; }
1530
+ .fk-menu-anchor { position: relative; display: inline-flex; }
1531
+ .fk-plus { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: 1px dashed var(--fk-secondary); border-radius: 8px; background: transparent; color: var(--fk-secondary); cursor: pointer; }
1532
+ .fk-options-summary { font-size: 12px; color: var(--fk-muted); white-space: nowrap; }
1533
+ .fk-menu { position: absolute; bottom: calc(100% + 6px); left: 0; z-index: 5; min-width: 230px; padding: 4px; border: 1px solid var(--fk-border); border-radius: 10px; background: var(--fk-bg); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18); }
1534
+ .fk-menu-item { display: flex; align-items: center; gap: 6px; width: 100%; padding: 7px 10px 7px 6px; border: 0; border-radius: 6px; background: transparent; color: var(--fk-text); font-size: 13px; text-align: left; cursor: pointer; }
1535
+ .fk-menu-item:hover, .fk-menu-item:focus-visible { background: var(--fk-surface); outline: none; }
1536
+ .fk-menu-check { display: inline-flex; width: 16px; height: 16px; color: var(--fk-primary); }
1537
+ .fk-menu-check svg { width: 16px; height: 16px; }
1538
+ .fk-menu-sep { margin: 4px 0; border: 0; border-top: 1px solid var(--fk-border); }
1530
1539
  .fk-file { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 4px 6px 4px 10px; border-radius: 8px; background: var(--fk-surface); font-size: 12px; }
1531
1540
  .fk-file span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1532
1541
  .fk-file button { display: inline-flex; border: 0; background: transparent; color: var(--fk-muted); padding: 2px; }
@@ -1753,6 +1762,12 @@ class Dialog {
1753
1762
  this.done = done;
1754
1763
  this.editor = null;
1755
1764
  this.includeLogs = true;
1765
+ this.notifyReporter = false;
1766
+ this.optionsMenu = null;
1767
+ this.onPointerDownOutsideMenu = (e) => {
1768
+ const anchor = this.optionsMenu?.parentElement;
1769
+ if (anchor && !e.composedPath().includes(anchor)) this.closeOptionsMenu();
1770
+ };
1756
1771
  this.attachment = null;
1757
1772
  this.selectedProducts = /* @__PURE__ */ new Set();
1758
1773
  this.products = [];
@@ -1882,24 +1897,17 @@ class Dialog {
1882
1897
  productsGroup.dataset.role = "products";
1883
1898
  productsGroup.hidden = true;
1884
1899
  const optionsRow = el("div", "fk-row");
1885
- const toggle = switchControl("Screenshot", this.includeScreenshot, (on) => {
1886
- this.includeScreenshot = on;
1887
- this.stage.dataset.screenshot = on ? "on" : "off";
1888
- this.refreshState();
1889
- });
1890
- if (!this.shot) toggle.querySelector("input").disabled = true;
1891
- const attachGroup = el("div");
1892
1900
  const fileInput = document.createElement("input");
1893
1901
  fileInput.type = "file";
1894
1902
  fileInput.hidden = true;
1895
- const attachButton = document.createElement("button");
1896
- attachButton.type = "button";
1897
- attachButton.className = "fk-attach";
1898
- attachButton.innerHTML = `${icons.paperclip}<span>Attach file</span>`;
1899
- attachButton.addEventListener("click", () => fileInput.click());
1900
- fileInput.addEventListener("change", () => {
1901
- const file = fileInput.files?.[0];
1902
- fileInput.value = "";
1903
+ const cameraInput = document.createElement("input");
1904
+ cameraInput.type = "file";
1905
+ cameraInput.accept = "image/*";
1906
+ cameraInput.setAttribute("capture", "environment");
1907
+ cameraInput.hidden = true;
1908
+ const onFile = (input) => {
1909
+ const file = input.files?.[0];
1910
+ input.value = "";
1903
1911
  if (!file) return;
1904
1912
  if (file.size > MAX_ATTACHMENT_BYTES) {
1905
1913
  this.showError(`"${file.name}" is larger than 10 MB.`);
@@ -1907,26 +1915,84 @@ class Dialog {
1907
1915
  }
1908
1916
  this.attachment = { filename: file.name, mimeType: file.type || "application/octet-stream", data: file };
1909
1917
  renderAttachment();
1910
- });
1911
- const renderAttachment = () => {
1912
- attachGroup.replaceChildren(fileInput);
1913
- if (this.attachment) {
1914
- const chip = el("span", "fk-file");
1915
- const name = el("span");
1916
- name.textContent = this.attachment.filename;
1917
- const remove = iconButton("", icons.close, `Remove ${this.attachment.filename}`);
1918
- remove.addEventListener("click", () => {
1919
- this.attachment = null;
1920
- renderAttachment();
1918
+ };
1919
+ fileInput.addEventListener("change", () => onFile(fileInput));
1920
+ cameraInput.addEventListener("change", () => onFile(cameraInput));
1921
+ const menuAnchor = el("div", "fk-menu-anchor");
1922
+ const menuButton = iconButton("fk-plus", icons.plus, "Options and attachments");
1923
+ menuButton.setAttribute("aria-haspopup", "menu");
1924
+ menuButton.setAttribute("aria-expanded", "false");
1925
+ menuButton.addEventListener("click", () => this.optionsMenu ? this.closeOptionsMenu() : openMenu());
1926
+ const summary = el("span", "fk-options-summary");
1927
+ const renderSummary = () => {
1928
+ const parts = [];
1929
+ if (this.shot && !this.includeScreenshot) parts.push("No screenshot");
1930
+ if (this.notifyReporter) parts.push("Notify me");
1931
+ summary.textContent = parts.join(" · ");
1932
+ };
1933
+ const openMenu = () => {
1934
+ const menu = el("div", "fk-menu");
1935
+ menu.setAttribute("role", "menu");
1936
+ const item = (label, onSelect, checked) => {
1937
+ const b = document.createElement("button");
1938
+ b.type = "button";
1939
+ b.className = "fk-menu-item";
1940
+ b.setAttribute("role", checked === void 0 ? "menuitem" : "menuitemcheckbox");
1941
+ if (checked !== void 0) b.setAttribute("aria-checked", String(checked));
1942
+ b.innerHTML = `<span class="fk-menu-check">${checked ? icons.check : ""}</span><span></span>`;
1943
+ b.lastElementChild.textContent = label;
1944
+ b.addEventListener("click", () => {
1945
+ this.closeOptionsMenu();
1946
+ onSelect();
1921
1947
  });
1922
- chip.append(name, remove);
1923
- attachGroup.append(chip);
1924
- } else {
1925
- attachGroup.append(attachButton);
1948
+ return b;
1949
+ };
1950
+ menu.append(item("Attach file…", () => fileInput.click()));
1951
+ if (window.matchMedia?.("(pointer: coarse)").matches) menu.append(item("Take photo", () => cameraInput.click()));
1952
+ menu.append(el("hr", "fk-menu-sep"));
1953
+ if (this.shot) {
1954
+ menu.append(
1955
+ item("Include screenshot", () => {
1956
+ this.includeScreenshot = !this.includeScreenshot;
1957
+ this.stage.dataset.screenshot = this.includeScreenshot ? "on" : "off";
1958
+ renderSummary();
1959
+ this.refreshState();
1960
+ }, this.includeScreenshot)
1961
+ );
1926
1962
  }
1963
+ menu.append(
1964
+ item("Notify me when it's fixed", () => {
1965
+ this.notifyReporter = !this.notifyReporter;
1966
+ renderSummary();
1967
+ }, this.notifyReporter)
1968
+ );
1969
+ menuAnchor.append(menu);
1970
+ menuButton.setAttribute("aria-expanded", "true");
1971
+ this.optionsMenu = menu;
1972
+ this.overlay.addEventListener("pointerdown", this.onPointerDownOutsideMenu, true);
1973
+ menu.querySelector(".fk-menu-item")?.focus();
1974
+ };
1975
+ const attachGroup = el("div", "fk-attach-group");
1976
+ const renderAttachment = () => {
1977
+ attachGroup.replaceChildren();
1978
+ if (!this.attachment) return;
1979
+ const chip = el("span", "fk-file");
1980
+ const name = el("span");
1981
+ name.textContent = this.attachment.filename;
1982
+ const remove = iconButton("", icons.close, `Remove ${this.attachment.filename}`);
1983
+ remove.addEventListener("click", () => {
1984
+ this.attachment = null;
1985
+ renderAttachment();
1986
+ });
1987
+ chip.append(name, remove);
1988
+ attachGroup.append(chip);
1927
1989
  };
1928
1990
  renderAttachment();
1929
- optionsRow.append(attachGroup, toggle);
1991
+ renderSummary();
1992
+ menuAnchor.append(menuButton, fileInput, cameraInput);
1993
+ const left = el("div", "fk-options-left");
1994
+ left.append(menuAnchor, attachGroup);
1995
+ optionsRow.append(left, summary);
1930
1996
  const meta = this.buildDiagnostics();
1931
1997
  this.errorBox = el("p", "fk-error");
1932
1998
  this.errorBox.setAttribute("role", "alert");
@@ -2018,6 +2084,12 @@ class Dialog {
2018
2084
  if (this.undoButton) this.undoButton.disabled = !this.editor?.canUndo;
2019
2085
  if (this.clearButton) this.clearButton.disabled = !(this.editor && this.editor.annotations.length > 0);
2020
2086
  }
2087
+ closeOptionsMenu() {
2088
+ this.optionsMenu?.remove();
2089
+ this.optionsMenu = null;
2090
+ this.overlay?.removeEventListener("pointerdown", this.onPointerDownOutsideMenu, true);
2091
+ this.overlay?.querySelector(".fk-plus")?.setAttribute("aria-expanded", "false");
2092
+ }
2021
2093
  // ---------------------------------------------------------------- keyboard
2022
2094
  handleKey(e) {
2023
2095
  if (this.finished) return;
@@ -2025,7 +2097,11 @@ class Dialog {
2025
2097
  const active = this.shadow.activeElement;
2026
2098
  const typing = active instanceof HTMLTextAreaElement || active instanceof HTMLInputElement;
2027
2099
  if (active instanceof HTMLInputElement && active.classList.contains("fk-text-input")) return;
2028
- if (e.key === "Escape") {
2100
+ if (e.key === "Escape" && this.optionsMenu) {
2101
+ e.preventDefault();
2102
+ e.stopPropagation();
2103
+ this.closeOptionsMenu();
2104
+ } else if (e.key === "Escape") {
2029
2105
  e.preventDefault();
2030
2106
  e.stopPropagation();
2031
2107
  this.cancel();
@@ -2089,7 +2165,8 @@ class Dialog {
2089
2165
  environment: this.environment,
2090
2166
  attachment: this.attachment,
2091
2167
  products: this.products.filter((p) => this.selectedProducts.has(p.key)),
2092
- logs: this.includeLogs ? this.logs : []
2168
+ logs: this.includeLogs ? this.logs : [],
2169
+ notifyReporter: this.notifyReporter
2093
2170
  };
2094
2171
  }
2095
2172
  async send() {