feedbackkit-web 1.0.47 → 1.0.49

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.
@@ -1672,7 +1672,11 @@ class Widget {
1672
1672
  button2.dataset.position = options.position ?? "bottom-right";
1673
1673
  button2.dataset.compact = String(!!options.compact);
1674
1674
  const label = options.label ?? "Feedback";
1675
- button2.setAttribute("aria-label", label);
1675
+ const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);
1676
+ const shortcutLabel = isMac ? "⌘⇧F" : "Ctrl+Shift+F";
1677
+ button2.title = `${label} (${shortcutLabel})`;
1678
+ button2.setAttribute("aria-label", `${label} (${shortcutLabel})`);
1679
+ button2.setAttribute("aria-keyshortcuts", isMac ? "Meta+Shift+F" : "Control+Shift+F");
1676
1680
  button2.innerHTML = `${icons.feedback}<span class="fk-trigger-label"></span>`;
1677
1681
  button2.querySelector(".fk-trigger-label").textContent = label;
1678
1682
  button2.addEventListener("click", onClick);
@@ -1725,16 +1729,38 @@ class Widget {
1725
1729
  if (secondary) host.style.setProperty("--fk-secondary", secondary);
1726
1730
  else host.style.removeProperty("--fk-secondary");
1727
1731
  }
1732
+ shouldCaptureScreenshot(options, captureOptions) {
1733
+ if (options.captureScreenshot !== void 0) return options.captureScreenshot;
1734
+ if (options.includeScreenshot !== void 0) return options.includeScreenshot;
1735
+ if (captureOptions.enabled !== void 0) return captureOptions.enabled;
1736
+ if (captureOptions.includeScreenshot !== void 0) return captureOptions.includeScreenshot;
1737
+ if (captureOptions.mode === "off") return false;
1738
+ if (this.deps.includeScreenshot && !this.deps.includeScreenshot()) return false;
1739
+ const config = this.deps.configuration();
1740
+ if (config) {
1741
+ if (typeof config.captureScreenshot === "boolean") return config.captureScreenshot;
1742
+ if (typeof config.captureScreenshot === "object" && config.captureScreenshot !== null) {
1743
+ if (config.captureScreenshot.enabled !== void 0) return config.captureScreenshot.enabled;
1744
+ if (config.captureScreenshot.includeScreenshot !== void 0) return config.captureScreenshot.includeScreenshot;
1745
+ if (config.captureScreenshot.mode === "off") return false;
1746
+ }
1747
+ if (config.includeScreenshot !== void 0) return config.includeScreenshot;
1748
+ }
1749
+ return true;
1750
+ }
1728
1751
  async run(options) {
1729
1752
  const shadow = this.mount();
1730
1753
  this.applyTheme();
1731
- this.trigger?.setAttribute("aria-busy", "true");
1732
1754
  const captureOptions = this.deps.captureOptions();
1755
+ const shouldCapture = this.shouldCaptureScreenshot(options, captureOptions);
1756
+ if (shouldCapture) {
1757
+ this.trigger?.setAttribute("aria-busy", "true");
1758
+ }
1733
1759
  const [shot, environment] = await Promise.all([
1734
- captureViewport({ ...captureOptions, exclude: this.isOwnNode }).catch((error) => {
1760
+ shouldCapture ? captureViewport({ ...captureOptions, exclude: this.isOwnNode }).catch((error) => {
1735
1761
  console.warn("[FeedbackKit] Screenshot capture failed; continuing without one.", error);
1736
1762
  return null;
1737
- }),
1763
+ }) : Promise.resolve(null),
1738
1764
  collectEnvironment({
1739
1765
  screenName: this.deps.currentScreen(),
1740
1766
  appVersion: this.deps.configuration()?.appVersion,
@@ -1744,21 +1770,31 @@ class Widget {
1744
1770
  const logs = this.deps.logs();
1745
1771
  this.trigger?.removeAttribute("aria-busy");
1746
1772
  return new Promise((resolve) => {
1747
- const dialog = new Dialog(shadow, shot, environment, logs, this.deps, options, (result) => {
1748
- resolve(result);
1749
- });
1773
+ const dialog = new Dialog(
1774
+ shadow,
1775
+ shot,
1776
+ environment,
1777
+ logs,
1778
+ this.deps,
1779
+ options,
1780
+ !shouldCapture,
1781
+ (result) => {
1782
+ resolve(result);
1783
+ }
1784
+ );
1750
1785
  dialog.show();
1751
1786
  });
1752
1787
  }
1753
1788
  }
1754
1789
  class Dialog {
1755
- constructor(shadow, shot, environment, logs, deps, options, done) {
1790
+ constructor(shadow, shot, environment, logs, deps, options, screenshotDisabled, done) {
1756
1791
  this.shadow = shadow;
1757
1792
  this.shot = shot;
1758
1793
  this.environment = environment;
1759
1794
  this.logs = logs;
1760
1795
  this.deps = deps;
1761
1796
  this.options = options;
1797
+ this.screenshotDisabled = screenshotDisabled;
1762
1798
  this.done = done;
1763
1799
  this.editor = null;
1764
1800
  this.includeLogs = true;
@@ -1779,7 +1815,13 @@ class Dialog {
1779
1815
  this.toolButtons = /* @__PURE__ */ new Map();
1780
1816
  this.swatches = [];
1781
1817
  this.onKeyDown = (e) => this.handleKey(e);
1782
- this.includeScreenshot = shot !== null;
1818
+ if (this.screenshotDisabled || !shot) {
1819
+ this.includeScreenshot = false;
1820
+ } else if (options.includeScreenshot !== void 0) {
1821
+ this.includeScreenshot = options.includeScreenshot;
1822
+ } else {
1823
+ this.includeScreenshot = true;
1824
+ }
1783
1825
  }
1784
1826
  show() {
1785
1827
  this.previousFocus = document.activeElement;
@@ -1853,7 +1895,7 @@ class Dialog {
1853
1895
  this.resizeObserver.observe(wrap);
1854
1896
  }
1855
1897
  const note = el("div", "fk-off-note");
1856
- note.textContent = this.shot ? "Screenshot won't be included" : "Screenshot unavailable on this page";
1898
+ note.textContent = this.shot || this.screenshotDisabled ? "Screenshot won't be included" : "Screenshot unavailable on this page";
1857
1899
  wrap.append(note);
1858
1900
  stage.append(toolbar, wrap);
1859
1901
  return stage;
@@ -1883,6 +1925,16 @@ class Dialog {
1883
1925
  const close = iconButton("fk-close", icons.close, "Close");
1884
1926
  close.addEventListener("click", () => this.cancel());
1885
1927
  header.append(title, close);
1928
+ let renderAttachment = () => {
1929
+ };
1930
+ const attachFile = (file) => {
1931
+ if (file.size > MAX_ATTACHMENT_BYTES) {
1932
+ this.showError(`"${file.name}" is larger than 10 MB.`);
1933
+ return;
1934
+ }
1935
+ this.attachment = { filename: file.name, mimeType: file.type || "application/octet-stream", data: file };
1936
+ renderAttachment();
1937
+ };
1886
1938
  const textLabel = el("label", "fk-label");
1887
1939
  textLabel.textContent = this.options.copy?.label ?? "What's the problem?";
1888
1940
  textLabel.setAttribute("for", "fk-text");
@@ -1891,6 +1943,27 @@ class Dialog {
1891
1943
  this.textarea.className = "fk-textarea";
1892
1944
  this.textarea.placeholder = this.options.copy?.placeholder ?? "Describe what happened and what you expected instead…";
1893
1945
  this.textarea.addEventListener("input", () => this.refreshState());
1946
+ this.textarea.addEventListener("paste", (e) => {
1947
+ const items = e.clipboardData?.items;
1948
+ if (!items || items.length === 0) return;
1949
+ for (let i = 0; i < items.length; i++) {
1950
+ const item = items[i];
1951
+ if (item.kind === "file") {
1952
+ const file = item.getAsFile();
1953
+ if (file) {
1954
+ let filename = file.name;
1955
+ if (!filename || filename === "image.png") {
1956
+ const ext = file.type.startsWith("image/") ? file.type.split("/")[1] || "png" : "bin";
1957
+ filename = `clipboard-${Date.now()}.${ext}`;
1958
+ }
1959
+ const attached = new File([file], filename, { type: file.type || "application/octet-stream" });
1960
+ attachFile(attached);
1961
+ e.preventDefault();
1962
+ break;
1963
+ }
1964
+ }
1965
+ }
1966
+ });
1894
1967
  const textGroup = el("div");
1895
1968
  textGroup.append(textLabel, this.textarea);
1896
1969
  const productsGroup = el("div");
@@ -1909,12 +1982,7 @@ class Dialog {
1909
1982
  const file = input.files?.[0];
1910
1983
  input.value = "";
1911
1984
  if (!file) return;
1912
- if (file.size > MAX_ATTACHMENT_BYTES) {
1913
- this.showError(`"${file.name}" is larger than 10 MB.`);
1914
- return;
1915
- }
1916
- this.attachment = { filename: file.name, mimeType: file.type || "application/octet-stream", data: file };
1917
- renderAttachment();
1985
+ attachFile(file);
1918
1986
  };
1919
1987
  fileInput.addEventListener("change", () => onFile(fileInput));
1920
1988
  cameraInput.addEventListener("change", () => onFile(cameraInput));
@@ -1926,7 +1994,7 @@ class Dialog {
1926
1994
  const summary = el("span", "fk-options-summary");
1927
1995
  const renderSummary = () => {
1928
1996
  const parts = [];
1929
- if (this.shot && !this.includeScreenshot) parts.push("No screenshot");
1997
+ if (!this.includeScreenshot) parts.push("No screenshot");
1930
1998
  if (this.notifyReporter) parts.push("Notify me");
1931
1999
  summary.textContent = parts.join(" · ");
1932
2000
  };
@@ -1973,7 +2041,7 @@ class Dialog {
1973
2041
  menu.querySelector(".fk-menu-item")?.focus();
1974
2042
  };
1975
2043
  const attachGroup = el("div", "fk-attach-group");
1976
- const renderAttachment = () => {
2044
+ renderAttachment = () => {
1977
2045
  attachGroup.replaceChildren();
1978
2046
  if (!this.attachment) return;
1979
2047
  const chip = el("span", "fk-file");
@@ -2281,6 +2349,7 @@ const widget = new Widget({
2281
2349
  defaultProductKey: () => configuration?.defaultProductKey,
2282
2350
  logs: () => logCapture?.buffer.snapshot() ?? [],
2283
2351
  captureOptions: () => FeedbackKit.captureOptions,
2352
+ includeScreenshot: () => FeedbackKit.includeScreenshot,
2284
2353
  currentScreen: () => FeedbackKit.currentScreen,
2285
2354
  identity: () => ({ reporterId: reporterId(), user: getUser() })
2286
2355
  });
@@ -2369,6 +2438,8 @@ const FeedbackKit = {
2369
2438
  theme: null,
2370
2439
  /** Screenshot capture settings. See `CaptureOptions`. */
2371
2440
  captureOptions: {},
2441
+ /** Whether screenshots should be captured and included in reports by default. Default `true`. */
2442
+ includeScreenshot: true,
2372
2443
  /** The hosted dashboard's ingestion endpoint, used when `configure` gets no `endpoint`. */
2373
2444
  DEFAULT_ENDPOINT,
2374
2445
  /**
@@ -2401,7 +2472,12 @@ const FeedbackKit = {
2401
2472
  * `null` if the user cancels. Nothing is sent anywhere; delivery is yours.
2402
2473
  */
2403
2474
  present(options = {}) {
2404
- return widget.open({ submit: false, onReport: options.onReport });
2475
+ return widget.open({
2476
+ submit: false,
2477
+ onReport: options.onReport,
2478
+ captureScreenshot: options.captureScreenshot,
2479
+ includeScreenshot: options.includeScreenshot
2480
+ });
2405
2481
  },
2406
2482
  /**
2407
2483
  * Opens the dialog and, on Send, delivers the report to the configured
@@ -2412,7 +2488,13 @@ const FeedbackKit = {
2412
2488
  if (!configuration) {
2413
2489
  return Promise.reject(new Error("FeedbackKit.configure({ projectKey }) must be called before presentAndSubmit()."));
2414
2490
  }
2415
- return widget.open({ submit: true, onReport: options.onSubmitted, onError: options.onError });
2491
+ return widget.open({
2492
+ submit: true,
2493
+ onReport: options.onSubmitted,
2494
+ onError: options.onError,
2495
+ captureScreenshot: options.captureScreenshot,
2496
+ includeScreenshot: options.includeScreenshot
2497
+ });
2416
2498
  },
2417
2499
  /** Sends an already-built report (e.g. one from `present`) to the configured endpoint. */
2418
2500
  submit(report) {
@@ -2429,10 +2511,17 @@ const FeedbackKit = {
2429
2511
  * configured, otherwise `present` and logs the report to the console.
2430
2512
  */
2431
2513
  showFloatingTriggerButton(options = {}) {
2514
+ if (options.keyboardShortcut !== false && options.enableKeyboardShortcut !== false && !shortcutHandler) {
2515
+ FeedbackKit.enableKeyboardShortcut(typeof options.keyboardShortcut === "object" ? options.keyboardShortcut : {});
2516
+ }
2432
2517
  whenBodyReady(
2433
2518
  () => widget.showTrigger(options, () => {
2434
- if (configuration) void FeedbackKit.presentAndSubmit();
2435
- else void FeedbackKit.present({ onReport: (r) => console.info("[FeedbackKit] report", r) });
2519
+ const openOptions = {
2520
+ captureScreenshot: options.captureScreenshot,
2521
+ includeScreenshot: options.includeScreenshot
2522
+ };
2523
+ if (configuration) void FeedbackKit.presentAndSubmit(openOptions);
2524
+ else void FeedbackKit.present({ ...openOptions, onReport: (r) => console.info("[FeedbackKit] report", r) });
2436
2525
  })
2437
2526
  );
2438
2527
  },
@@ -2447,7 +2536,8 @@ const FeedbackKit = {
2447
2536
  FeedbackKit.disableKeyboardShortcut();
2448
2537
  const key = (options.key ?? "F").toLowerCase();
2449
2538
  shortcutHandler = (e) => {
2450
- if (!(e.metaKey || e.ctrlKey) || !e.shiftKey || e.altKey || e.key.toLowerCase() !== key) return;
2539
+ const matchesKey = e.key.toLowerCase() === key || e.code.toLowerCase() === `key${key}`;
2540
+ if (!(e.metaKey || e.ctrlKey) || !e.shiftKey || e.altKey || !matchesKey) return;
2451
2541
  e.preventDefault();
2452
2542
  if (configuration) void FeedbackKit.presentAndSubmit();
2453
2543
  else void FeedbackKit.present({ onReport: (r) => console.info("[FeedbackKit] report", r) });