visual-feedback 0.3.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.1 (2026-10-12)
4
+
5
+ - The trigger shows its name as a tooltip. With a `shortcut` set, the tooltip names it too, the way
6
+ the platform's keyboards do, such as `Send feedback (Option + Shift + F)` on a Mac and
7
+ `(Alt + Shift + F)` elsewhere (SPEC §3).
8
+ - A `shortcut` with no key, such as `"Alt+"`, now matches nothing: before, a key press whose code
9
+ was empty, as some virtual keyboards send, could open the widget.
10
+
3
11
  ## 0.3.0 (2026-10-11)
4
12
 
5
13
  - Breaking: with no `--vf-*` variables set, the widget now matches the site by itself. It takes
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
1
 
2
- export { t as createFeedback, n as discardDraft } from "./widget-DvGTo0B4.mjs";
2
+ export { t as createFeedback, n as discardDraft } from "./widget-Ryu07XUU.mjs";
package/dist/react.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { t as createFeedback } from "./widget-DvGTo0B4.mjs";
2
+ import { t as createFeedback } from "./widget-Ryu07XUU.mjs";
3
3
  import { useEffect, useRef } from "react";
4
4
  //#region src/react/index.ts
5
5
  function Feedback(props) {
@@ -223,6 +223,41 @@ const noop = {
223
223
  update() {},
224
224
  on: () => () => {}
225
225
  };
226
+ const MODIFIERS = [
227
+ "alt",
228
+ "shift",
229
+ "control",
230
+ "meta"
231
+ ];
232
+ /** A shortcut's modifiers in lower case and its key as written; undefined when it can match nothing. */
233
+ function parse(shortcut) {
234
+ const parts = shortcut.split("+");
235
+ const key = parts.pop() ?? "";
236
+ const mods = parts.map((part) => part.toLowerCase());
237
+ return key && mods.every((mod) => MODIFIERS.includes(mod)) ? {
238
+ mods,
239
+ key
240
+ } : void 0;
241
+ }
242
+ /** The shortcut as this platform's keyboards name it, such as "Option + Shift + F" on a Mac. */
243
+ function spoken(shortcut) {
244
+ const parsed = parse(shortcut);
245
+ if (!parsed) return void 0;
246
+ const platform = navigator.platform;
247
+ const names = /mac|iphone|ipad|ipod/i.test(platform) ? {
248
+ alt: "Option",
249
+ shift: "Shift",
250
+ control: "Control",
251
+ meta: "Command"
252
+ } : {
253
+ alt: "Alt",
254
+ shift: "Shift",
255
+ control: "Ctrl",
256
+ meta: /^win/i.test(platform) ? "Win" : "Super"
257
+ };
258
+ const key = parsed.key.charAt(0).toUpperCase() + parsed.key.slice(1);
259
+ return [...new Set(parsed.mods)].map((mod) => names[mod]).concat(key).join(" + ");
260
+ }
226
261
  function createFeedback(options = {}) {
227
262
  if (!supported()) return noop;
228
263
  const tag = options.tagName ?? "visual-feedback";
@@ -259,18 +294,12 @@ function createFeedback(options = {}) {
259
294
  let label;
260
295
  let unlisten;
261
296
  function listen(shortcut) {
262
- const parts = shortcut.toLowerCase().split("+");
263
- const key = parts.pop();
264
- const names = [
265
- "alt",
266
- "shift",
267
- "control",
268
- "meta"
269
- ];
270
- const mods = names.map((name) => parts.includes(name)).join();
271
- const typing = !/control|meta/.test(shortcut.toLowerCase());
297
+ const parsed = parse(shortcut);
298
+ const key = parsed?.key.toLowerCase();
299
+ const mods = MODIFIERS.map((name) => parsed?.mods.includes(name)).join();
300
+ const typing = !parsed?.mods.some((mod) => mod === "control" || mod === "meta");
272
301
  const on = (event) => {
273
- if (event instanceof KeyboardEvent && state === "idle" && !event.repeat && !event.isComposing && !event.getModifierState("AltGraph") && parts.every((part) => names.includes(part)) && [
302
+ if (event instanceof KeyboardEvent && state === "idle" && !event.repeat && !event.isComposing && !event.getModifierState("AltGraph") && parsed && [
274
303
  event.altKey,
275
304
  event.shiftKey,
276
305
  event.ctrlKey,
@@ -322,7 +351,10 @@ function createFeedback(options = {}) {
322
351
  trigger.dataset.y = y;
323
352
  trigger.dataset.x = x;
324
353
  const { trigger: name, triggerText } = options.strings ?? {};
325
- trigger.setAttribute("aria-label", name || triggerText || "Send feedback");
354
+ const accessible = name || triggerText || "Send feedback";
355
+ trigger.setAttribute("aria-label", accessible);
356
+ const keys = options.shortcut ? spoken(options.shortcut) : void 0;
357
+ trigger.title = keys ? `${accessible} (${keys})` : accessible;
326
358
  trigger.toggleAttribute("data-icon", !triggerText);
327
359
  if (label) {
328
360
  label.textContent = triggerText ?? "";
@@ -507,7 +539,10 @@ function createFeedback(options = {}) {
507
539
  const mine = session;
508
540
  return pending.then(() => mine === session ? widget.open() : void 0);
509
541
  }
510
- if (state !== "idle") return dialog?.reopen(), Promise.resolve();
542
+ if (state !== "idle") {
543
+ dialog?.reopen();
544
+ return Promise.resolve();
545
+ }
511
546
  if (restore()) return widget.open();
512
547
  state = "opening";
513
548
  pending = show(++session);
package/docs/API.md CHANGED
@@ -39,7 +39,7 @@ const feedback = createFeedback({
39
39
  | `headers` | `Record<string, string>` or `() => Record<string, string>` | – | Extra headers for `endpoint`. A function is called at Send; a throw shows the send error state, using the error's message. |
40
40
  | `trigger` | `"button" \| "none"` | `"button"` | `"none"` draws no trigger; the host calls `open()`. A kept draft's bar still shows. |
41
41
  | `position` | `"bottom-right" \| "bottom-left" \| "top-right" \| "top-left"` | `"bottom-right"` | Corner of the trigger. |
42
- | `shortcut` | `string \| false` | `false` | For example `"Alt+Shift+F"`: any of `Alt`, `Shift`, `Control` and `Meta`, then one key, a letter, a digit or a key name such as `F2`, joined by `+`. The modifiers must match exactly, AltGr never matches, and any other modifier name makes it match nothing. Without `Control` or `Meta` it does not fire while focus is in a form field (`input`, `textarea`, `select`) or editable content. It does nothing while the dialog is open or a draft is kept in the draft bar. Opt-in; it is the widget's only listener on `window` or `document`, one `keydown` on `window` (SPEC §9 names what else the widget watches, and what the capture library keeps). |
42
+ | `shortcut` | `string \| false` | `false` | For example `"Alt+Shift+F"`: any of `Alt`, `Shift`, `Control` and `Meta`, then one key, a letter, a digit or a key name such as `F2`, joined by `+`. The modifiers must match exactly, AltGr never matches, and any other modifier name, or no key, makes it match nothing. Without `Control` or `Meta` it does not fire while focus is in a form field (`input`, `textarea`, `select`) or editable content. It does nothing while the dialog is open or a draft is kept in the draft bar. The trigger's tooltip names it (SPEC §3). Opt-in; it is the widget's only listener on `window` or `document`, one `keydown` on `window` (SPEC §9 names what else the widget watches, and what the capture library keeps). |
43
43
  | `tools` | `("pen" \| "box" \| "arrow" \| "text" \| "select")[]` | all five | Order is the toolbar order, and the first drawing tool is the one selected at open. Repeats and unknown names are ignored; an empty list, or Select alone, shows no toolbar. The Delete button is there only with Select. |
44
44
  | `categories` | `{ id: string; label: string }[]` | none | Shows a category picker. |
45
45
  | `requireNote` | `boolean` | `true` | |
package/docs/SPEC.md CHANGED
@@ -53,6 +53,12 @@ annotating, draft --Cancel (it may ask first, below) / Discard--> idle
53
53
  gets that short label beside the icon, as a pill; with only `triggerText` given, that text is
54
54
  the name too. A host that sets both keeps `triggerText` inside `trigger`, so
55
55
  speech control can say what it sees. It has no badge, and it does not move or pulse.
56
+ - Its tooltip (`title`) is its name, as for the dialog's icon buttons. With a `shortcut` that can
57
+ match, it is the name and the keys, each modifier once, joined by ` + `, as the platform's
58
+ keyboards name them: Option, Command, Control and Shift on a Mac, iPhone or iPad
59
+ (`navigator.platform`), as in `Send feedback (Option + Shift + F)`; Alt, Win, Ctrl and Shift on
60
+ Windows; Alt, Super, Ctrl and Shift elsewhere. A screen reader then reads the keys as the
61
+ description.
56
62
  - It sits 16 px from the edges by default, and it respects `env(safe-area-inset-*)`.
57
63
  - It is hidden while there is a draft, whether the dialog is open or kept in the draft bar.
58
64
  - It does not move to keep clear of the page's content. The host moves it off its own content
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "visual-feedback",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "A small button lets your users screenshot the page, mark it up and write a note. You receive one report and store it where you want.",
5
5
  "keywords": [
6
6
  "feedback",