visual-feedback 0.3.3 → 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/AGENTS.md +4 -1
- package/CHANGELOG.md +27 -0
- package/README.md +8 -0
- package/dist/{dialog-W6m1knfu.mjs → dialog-BL7k2Jir.mjs} +80 -27
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/react.d.mts +1 -1
- package/dist/react.mjs +1 -1
- package/dist/{report-CnspEsHf.mjs → report-BJADH9_W.mjs} +24 -2
- package/dist/{widget-Z1KOUKjx.d.mts → widget-CMjTbhGw.d.mts} +2 -2
- package/dist/{widget-IwlLFJCu.mjs → widget-JBN1fbyx.mjs} +31 -10
- package/docs/API.md +3 -3
- package/docs/SPEC.md +40 -22
- package/docs/THEMING.md +8 -8
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -36,7 +36,10 @@ comes before the first report in step 6.
|
|
|
36
36
|
recipe adds. In a React or Next.js app, render `<Feedback endpoint="/api/feedback" />` from
|
|
37
37
|
`visual-feedback/react` instead, once, in the root layout; its props are the same options, and
|
|
38
38
|
a function prop such as `onSubmit` needs a `"use client"` file (`docs/API.md`). Every option is in `docs/API.md`; the look is in
|
|
39
|
-
`docs/THEMING.md`.
|
|
39
|
+
`docs/THEMING.md`. Fit the button to the app's own design: read its layout and pick
|
|
40
|
+
`trigger` and `position`, and `--vf-offset` where the default spot would cover something the app shows, and the
|
|
41
|
+
`--vf-*` colours and corners where the widget does not take them from the page already. Tell the
|
|
42
|
+
developer where it sits and why.
|
|
40
43
|
3. On the server: copy the recipe's route as it is written. Its test runs that code, with only the
|
|
41
44
|
marked parts below filled in, so do not change how it stores a report. If the app is not a Next.js App Router app, keep the logic and
|
|
42
45
|
change only how the route is mounted.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1 (2026-10-14)
|
|
4
|
+
|
|
5
|
+
- The dialog fits the window, 16px from its edges, with nothing to scroll: on a large screen the
|
|
6
|
+
image grows to the height left, and the dialog is as wide as the image needs. On a short
|
|
7
|
+
screen the tool rail has two columns, and a phone in landscape gets the note beside the image
|
|
8
|
+
(SPEC §2).
|
|
9
|
+
- While a send is under way, Cancel, Keep as draft and Esc do nothing. A send that takes over 30
|
|
10
|
+
seconds fails with the new `timeout` text, and `error` fires with a `TimeoutError`; an
|
|
11
|
+
`endpoint` request is aborted.
|
|
12
|
+
- The sent confirmation shows for half a second, or until a click, in place of 3 seconds.
|
|
13
|
+
- Clear asks first (new `clearConfirm` text), and Undo brings the cleared marks back.
|
|
14
|
+
- Delete sits under Select in the tool rail. New parts: `confirm-clear` and `side`.
|
|
15
|
+
|
|
16
|
+
## 0.4.0 (2026-10-13)
|
|
17
|
+
|
|
18
|
+
- Breaking: the trigger is a tab by default, in the corner `position` names, with a flag icon. It
|
|
19
|
+
has no border: the text colour fills it, and on hover the colours swap. `--vf-offset` lifts the
|
|
20
|
+
tab along its side edge, and no longer moves it off that edge. `trigger: "button"` keeps the
|
|
21
|
+
round button, 16px from both edges, filled the same way (SPEC §3).
|
|
22
|
+
- Breaking: the trigger has no `title`. Its tooltip is the widget's own: it shows at once on hover
|
|
23
|
+
or keyboard focus, beside the trigger with an arrow pointing at it, and has its own part,
|
|
24
|
+
`trigger-tooltip`. Screen readers still hear it as the description.
|
|
25
|
+
- The README starts with a prompt to give a coding agent, naming the package by its npm page, and AGENTS.md asks the agent to fit the
|
|
26
|
+
button to the app's design.
|
|
27
|
+
- The demo styles the tooltip as its "Click here" hint, and fades that hint while the tooltip
|
|
28
|
+
shows.
|
|
29
|
+
|
|
3
30
|
## 0.3.3 (2026-10-13)
|
|
4
31
|
|
|
5
32
|
- The trigger stands out more: a swirl icon that fills it, in place of the speech bubble, ringed
|
package/README.md
CHANGED
|
@@ -8,6 +8,14 @@ any design.
|
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
|
11
|
+
Ask your coding agent:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
Install the visual-feedback npm package (https://www.npmjs.com/package/visual-feedback) and add it to this app by following its AGENTS.md. Place its button where it covers nothing, and match its position, colours and corners to our design.
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or the old-school way, by hand:
|
|
18
|
+
|
|
11
19
|
```sh
|
|
12
20
|
npm install visual-feedback
|
|
13
21
|
```
|
|
@@ -450,6 +450,7 @@ function annotate(canvas, onChange, textLabel, kept = []) {
|
|
|
450
450
|
const step = history.pop();
|
|
451
451
|
if (!step) return;
|
|
452
452
|
if (step.kind === "add") marks.pop();
|
|
453
|
+
else if (step.kind === "clear") marks.push(...step.marks);
|
|
453
454
|
else if (step.kind === "move") marks[step.index] = step.mark;
|
|
454
455
|
else marks.splice(step.index, 0, step.mark);
|
|
455
456
|
selected = null;
|
|
@@ -458,8 +459,10 @@ function annotate(canvas, onChange, textLabel, kept = []) {
|
|
|
458
459
|
},
|
|
459
460
|
clear() {
|
|
460
461
|
finishDrag();
|
|
461
|
-
marks.length
|
|
462
|
-
|
|
462
|
+
if (marks.length) history.push({
|
|
463
|
+
kind: "clear",
|
|
464
|
+
marks: marks.splice(0)
|
|
465
|
+
});
|
|
463
466
|
selected = null;
|
|
464
467
|
paint();
|
|
465
468
|
onChange();
|
|
@@ -504,8 +507,8 @@ function icon(d, part) {
|
|
|
504
507
|
}
|
|
505
508
|
//#endregion
|
|
506
509
|
//#region src/dialog.ts
|
|
507
|
-
const LAYOUT = `.backdrop{position:fixed;inset:0;width:100%;height:100%;max-width:none;max-height:none;margin:0;padding:
|
|
508
|
-
const THEME = `.backdrop{background:var(--_bd)}.dialog,.draft{background:var(--_s);color:var(--_t);border:1px solid var(--_b);border-radius:var(--_r);box-shadow:var(--_sh);padding:14px}.draft{padding:8px}.title{margin-left:4px;font-size:1.1em;font-weight:600}.header button,.toolbar button,.thumb-remove{font:inherit;width:2.15em;height:2.15em;padding:0;display:grid;place-items:center;border:0;border-radius:calc(var(--_r) / 2);background:none;color:var(--_m);cursor:pointer}.header button:hover,.toolbar button:hover:not(:disabled){background:color-mix(in srgb,var(--_t) 8%,transparent);color:var(--_t)}.body{padding:12px;border-radius:calc(var(--_r) * .75);background:color-mix(in srgb,var(--_t) 5%,var(--_s))}.toolbar{padding:4px;border:1px solid var(--_b);border-radius:calc(var(--_r) * .75);background:var(--_s)}.toolbar [aria-pressed=true]{background:color-mix(in srgb,var(--_a) 16%,transparent);color:var(--_a)}.toolbar button:disabled{opacity:.35;cursor:default}.divider{width:20px;margin:4px auto;border-top:1px solid var(--_b)}.image{border-radius:calc(var(--_r) / 2);box-shadow:0 0 0 1px var(--_b),0 2px 8px rgb(0 0 0/10%)}.toolbar+.stage>.image{cursor:crosshair}.selection{border:2px dashed var(--_f);border-radius:4px}.text-input{font-family:inherit;font-weight:600;line-height:1.2;color:var(--_mark);background:color-mix(in srgb,var(--_s) 85%,transparent);border:1px dashed var(--_mark);border-radius:4px;padding:0 4px;outline:none}.thumb-pick{border:2px solid transparent;border-radius:calc(var(--_r) / 2.5);background:none;overflow:hidden;cursor:pointer}.thumb-pick[aria-current=true]{border-color:var(--_a)}.thumb-image{border-radius:calc(var(--_r) / 4);box-shadow:0 0 0 1px var(--_b)}.thumb-number{font-size:.75em;font-weight:600;padding:0 4px;border-radius:4px;background:var(--_s);color:var(--_t)}.thumb-remove{width:1.5em;height:1.5em;border:1px solid var(--_b);border-radius:50%;background:var(--_s)}.label{font-size:.9em;font-weight:600;color:var(--_m)}.composer{padding:8px;border:1px solid var(--_b);border-radius:var(--_r);background:var(--_s);transition:border-color .15s,box-shadow .15s}.composer:focus-within{border-color:var(--_f);box-shadow:0 0 0 3px color-mix(in srgb,var(--_f) 22%,transparent)}.note{font:inherit;color:inherit;background:none;border:0;border-radius:calc(var(--_r) / 2);padding:4px 6px}.note:focus-visible{outline:none}.note::placeholder{color:var(--_m)}.category{font:inherit;color:inherit;background:color-mix(in srgb,var(--_t) 5%,var(--_s));border:1px solid var(--_b);border-radius:calc(var(--_r) / 2);padding:6px 8px}.send,.actions button,.add,.draft button{font:inherit;font-weight:600;height:2.25em;padding:0 .9em;border:1px solid var(--_b);border-radius:calc(var(--_r) / 1.6);background:var(--_s);color:inherit;cursor:pointer}.add{border-style:dashed;color:var(--_m)}.send,.draft .capture-view{background:var(--_a);color:var(--_at);border-color:var(--_a)}.split .send{border-radius:calc(var(--_r) / 1.6) 0 0 calc(var(--_r) / 1.6)}.send-more{font:inherit;height:2.25em;padding:0 .55em;border:1px solid var(--_a);border-left-color:color-mix(in srgb,var(--_at) 40%,var(--_a));border-radius:0 calc(var(--_r) / 1.6) calc(var(--_r) / 1.6) 0;background:var(--_a);color:var(--_at);cursor:pointer}.send-more:disabled{opacity:.6;cursor:progress}.send:where([data-ink=dark]),.send-more:where([data-ink=dark]),.draft .capture-view:where([data-ink=dark]){color:#000}.menu{padding:4px;border:1px solid var(--_b);border-radius:calc(var(--_r) / 1.6);background:var(--_s);color:var(--_t);box-shadow:var(--_sh)}.save-pdf{font:inherit;padding:.5em .8em;border:0;border-radius:calc(var(--_r) / 2.5);background:none;color:inherit;cursor:pointer;white-space:nowrap}.save-pdf:hover,.save-pdf:focus-visible{background:color-mix(in srgb,var(--_t) 8%,transparent)}.draft button:disabled{opacity:.5;cursor:default}.draft-label{margin:0 4px;font-size:.9em;font-weight:600}[aria-disabled=true]{opacity:.6;cursor:progress}.note:disabled,.category:disabled{opacity:.6}.hint{margin:0 4px;font-size:.85em;color:var(--_m)}.error{margin:0 4px;font-weight:500}.status{margin:8px 4px}`;
|
|
510
|
+
const LAYOUT = `.backdrop{position:fixed;inset:0;width:100%;height:100%;max-width:none;max-height:none;margin:0;padding:max(16px,env(safe-area-inset-top,0px)) max(16px,env(safe-area-inset-right,0px)) max(16px,env(safe-area-inset-bottom,0px)) max(16px,env(safe-area-inset-left,0px));border:0;box-sizing:border-box;background:none;color:inherit;display:grid;place-items:center;overflow:auto}.backdrop:not([open]){display:none}.backdrop::backdrop{background:none}.dialog{box-sizing:border-box;width:min(560px,100%);display:grid;gap:12px}.dialog.wide{width:fit-content;min-width:min(560px,100%);max-width:100%}.side{display:contents}.header{display:flex;align-items:center;justify-content:space-between;gap:8px}.title{margin:0}.minimise{margin-left:auto}.body{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:start}.body>.stage:only-child{grid-column:1/-1}.stage{position:relative;display:grid;gap:8px;min-width:0}.image{display:block;width:auto;height:var(--_ih,auto);max-width:100%;max-height:var(--_ih,max(4em,min(60vh,100vh - 24.5em)));margin:0 auto;color:var(--_mark)}.toolbar+.stage>.image{touch-action:none}.selection{position:absolute;pointer-events:none;box-sizing:border-box}.text-input{position:absolute;transform:translateY(-50%);min-width:6em;margin:0;box-sizing:content-box}.toolbar{display:flex;flex-direction:column;gap:4px}.dialog svg,.draft svg{width:1.15em;height:1.15em;flex:none;stroke-linecap:round}.divider{display:block}.strip{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:8px}.thumb{position:relative;display:flex}.thumb-pick{position:relative;display:block;margin:0;padding:0;line-height:0}.thumb-image{display:block;height:3em;width:auto}.thumb-number{position:absolute;left:4px;top:4px;line-height:1.2}.thumb-remove{position:absolute;top:-.5em;right:-.5em}.add,.capture-view{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}.composer{display:grid;gap:8px}.note{box-sizing:border-box;width:100%;min-height:64px;resize:vertical}.footer{display:flex;align-items:center;justify-content:space-between;gap:8px}.composer-bar,.actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:8px}.send{display:inline-flex;align-items:center;gap:6px}.split{position:relative;display:flex;margin-left:auto}.send-more{display:inline-flex;align-items:center}.menu{position:absolute;right:0;bottom:calc(100% + 6px);z-index:1;display:grid;min-width:max-content}.menu[hidden]{display:none}.save-pdf{display:inline-flex;align-items:center;gap:8px}.draft{position:fixed;z-index:var(--_z);box-sizing:border-box;max-width:calc(100% - 2 * var(--_off));display:flex;flex-wrap:wrap;align-items:center;gap:8px}.draft-thumbs{display:flex;gap:4px}.draft-thumbs .thumb-image{height:1.75em}.drafting>.trigger{display:none}.live{position:fixed;width:1px;height:1px;margin:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}@media (max-height:760px){.toolbar{display:grid;grid-template-columns:repeat(2,auto);align-content:start}.divider{grid-column:1/-1}}@media (max-height:700px){.dialog{position:relative}.dialog>.confirm{position:absolute;z-index:2;top:0;left:0;right:0}}@media (orientation:landscape) and (max-height:500px){.dialog.wide{width:100%;grid-template-columns:minmax(0,1fr) 12em;grid-template-rows:auto minmax(0,1fr) auto;row-gap:0}.wide>.header{grid-area:1/1/2/-1;margin-bottom:8px}.wide>.body{grid-area:2/1}.wide>.error{grid-area:3/1;margin-top:8px}.wide>.side{grid-area:2/2/4/3;display:flex;flex-direction:column;gap:12px;min-height:0}.wide>.side>.composer{flex:1;grid-template-rows:minmax(0,1fr)}.wide .note{height:100%;resize:none}.wide .footer{flex-wrap:wrap}.wide .hint{flex:1 1 100%}}`;
|
|
511
|
+
const THEME = `.backdrop{background:var(--_bd)}.dialog,.draft{background:var(--_s);color:var(--_t);border:1px solid var(--_b);border-radius:var(--_r);box-shadow:var(--_sh);padding:14px}.draft{padding:8px}.title{margin-left:4px;font-size:1.1em;font-weight:600}.header button,.toolbar button,.thumb-remove{font:inherit;width:2.15em;height:2.15em;padding:0;display:grid;place-items:center;border:0;border-radius:calc(var(--_r) / 2);background:none;color:var(--_m);cursor:pointer}.header button:hover,.toolbar button:hover:not(:disabled){background:color-mix(in srgb,var(--_t) 8%,transparent);color:var(--_t)}.body{padding:12px;border-radius:calc(var(--_r) * .75);background:color-mix(in srgb,var(--_t) 5%,var(--_s))}.toolbar{padding:4px;border:1px solid var(--_b);border-radius:calc(var(--_r) * .75);background:var(--_s)}.toolbar [aria-pressed=true]{background:color-mix(in srgb,var(--_a) 16%,transparent);color:var(--_a)}.toolbar button:disabled{opacity:.35;cursor:default}.divider{width:20px;margin:4px auto;border-top:1px solid var(--_b)}.image{border-radius:calc(var(--_r) / 2);box-shadow:0 0 0 1px var(--_b),0 2px 8px rgb(0 0 0/10%)}.toolbar+.stage>.image{cursor:crosshair}.selection{border:2px dashed var(--_f);border-radius:4px}.text-input{font-family:inherit;font-weight:600;line-height:1.2;color:var(--_mark);background:color-mix(in srgb,var(--_s) 85%,transparent);border:1px dashed var(--_mark);border-radius:4px;padding:0 4px;outline:none}.thumb-pick{border:2px solid transparent;border-radius:calc(var(--_r) / 2.5);background:none;overflow:hidden;cursor:pointer}.thumb-pick[aria-current=true]{border-color:var(--_a)}.thumb-image{border-radius:calc(var(--_r) / 4);box-shadow:0 0 0 1px var(--_b)}.thumb-number{font-size:.75em;font-weight:600;padding:0 4px;border-radius:4px;background:var(--_s);color:var(--_t)}.thumb-remove{width:1.5em;height:1.5em;border:1px solid var(--_b);border-radius:50%;background:var(--_s)}.label{font-size:.9em;font-weight:600;color:var(--_m)}.composer{padding:8px;border:1px solid var(--_b);border-radius:var(--_r);background:var(--_s);transition:border-color .15s,box-shadow .15s}.composer:focus-within{border-color:var(--_f);box-shadow:0 0 0 3px color-mix(in srgb,var(--_f) 22%,transparent)}.note{font:inherit;color:inherit;background:none;border:0;border-radius:calc(var(--_r) / 2);padding:4px 6px}.note:focus-visible{outline:none}.note::placeholder{color:var(--_m)}.category{font:inherit;color:inherit;background:color-mix(in srgb,var(--_t) 5%,var(--_s));border:1px solid var(--_b);border-radius:calc(var(--_r) / 2);padding:6px 8px}.send,.actions button,.add,.draft button{font:inherit;font-weight:600;height:2.25em;padding:0 .9em;border:1px solid var(--_b);border-radius:calc(var(--_r) / 1.6);background:var(--_s);color:inherit;cursor:pointer}.add{border-style:dashed;color:var(--_m)}.send,.draft .capture-view{background:var(--_a);color:var(--_at);border-color:var(--_a)}.split .send{border-radius:calc(var(--_r) / 1.6) 0 0 calc(var(--_r) / 1.6)}.send-more{font:inherit;height:2.25em;padding:0 .55em;border:1px solid var(--_a);border-left-color:color-mix(in srgb,var(--_at) 40%,var(--_a));border-radius:0 calc(var(--_r) / 1.6) calc(var(--_r) / 1.6) 0;background:var(--_a);color:var(--_at);cursor:pointer}.send-more:disabled{opacity:.6;cursor:progress}.send:where([data-ink=dark]),.send-more:where([data-ink=dark]),.draft .capture-view:where([data-ink=dark]){color:#000}.menu{padding:4px;border:1px solid var(--_b);border-radius:calc(var(--_r) / 1.6);background:var(--_s);color:var(--_t);box-shadow:var(--_sh)}.save-pdf{font:inherit;padding:.5em .8em;border:0;border-radius:calc(var(--_r) / 2.5);background:none;color:inherit;cursor:pointer;white-space:nowrap}.save-pdf:hover,.save-pdf:focus-visible{background:color-mix(in srgb,var(--_t) 8%,transparent)}.draft button:disabled{opacity:.5;cursor:default}.draft-label{margin:0 4px;font-size:.9em;font-weight:600}[aria-disabled=true]{opacity:.6;cursor:progress}.note:disabled,.category:disabled{opacity:.6}.hint{margin:0 4px;font-size:.85em;color:var(--_m)}.error{margin:0 4px;font-weight:500}.status{margin:8px 4px}@media (max-height:700px){.dialog>.confirm{min-height:2.15em;padding:14px;border-bottom:1px solid var(--_b);border-radius:calc(var(--_r) - 1px) calc(var(--_r) - 1px) 0 0;background:var(--_s);box-shadow:var(--_sh)}}`;
|
|
509
512
|
const ICONS = {
|
|
510
513
|
pen: "M4 20h4L19 9a2.8 2.8 0 0 0-4-4L4 16v4zm9.5-13.5 4 4",
|
|
511
514
|
box: "M6.5 5h11A2.5 2.5 0 0 1 20 7.5v9a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 4 16.5v-9A2.5 2.5 0 0 1 6.5 5z",
|
|
@@ -540,11 +543,13 @@ const ENGLISH = {
|
|
|
540
543
|
remove: "Delete",
|
|
541
544
|
undo: "Undo",
|
|
542
545
|
clear: "Clear",
|
|
546
|
+
clearConfirm: "Clear all marks on this screenshot?",
|
|
543
547
|
cancel: "Cancel",
|
|
544
548
|
send: "Send",
|
|
545
549
|
sending: "Sending…",
|
|
546
550
|
sent: "Thanks, it's sent.",
|
|
547
551
|
error: "It didn't send. Your note is kept.",
|
|
552
|
+
timeout: "It didn't send in 30 seconds. Check your internet connection, then try again.",
|
|
548
553
|
retry: "Try again",
|
|
549
554
|
escHint: "Esc keeps it as a draft",
|
|
550
555
|
minimise: "Keep as draft",
|
|
@@ -695,7 +700,7 @@ function mount(parent, handlers, settings = {}) {
|
|
|
695
700
|
const title = element("h2", "title", { id: "vf-title" });
|
|
696
701
|
title.textContent = text.title;
|
|
697
702
|
const header = element("div", "header");
|
|
698
|
-
const cancel = iconButton("cancel", text.cancel, ICONS.cancel, () => lose() ?
|
|
703
|
+
const cancel = iconButton("cancel", text.cancel, ICONS.cancel, () => lose() ? askDiscard() : handlers.close());
|
|
699
704
|
const keep = iconButton("minimise", text.minimise, ICONS.minimise, () => minimise());
|
|
700
705
|
header.append(title, cancel);
|
|
701
706
|
backdrop.append(panel);
|
|
@@ -729,26 +734,25 @@ function mount(parent, handlers, settings = {}) {
|
|
|
729
734
|
let reviving = false;
|
|
730
735
|
let picker;
|
|
731
736
|
let asking;
|
|
737
|
+
const busy = () => !over && primary?.getAttribute("aria-disabled") === "true";
|
|
732
738
|
function lose() {
|
|
733
|
-
if (over ||
|
|
739
|
+
if (over || busy()) return false;
|
|
734
740
|
return Boolean(note?.value.trim() || picker?.selectedIndex || taken.length > 1 || taken.some((shot) => shot.annotator?.marks.length));
|
|
735
741
|
}
|
|
736
|
-
function ask() {
|
|
742
|
+
function ask(prompt, choices, from) {
|
|
737
743
|
asking?.remove();
|
|
738
744
|
const back = () => {
|
|
739
745
|
asking?.remove();
|
|
740
|
-
|
|
746
|
+
from.focus();
|
|
741
747
|
};
|
|
742
748
|
const question = element("span", "confirm-text");
|
|
743
|
-
question.textContent =
|
|
749
|
+
question.textContent = prompt;
|
|
744
750
|
const stay = button("keep-editing", text.keepEditing, back);
|
|
745
751
|
asking = element("div", "actions confirm", {
|
|
746
752
|
role: "group",
|
|
747
|
-
"aria-label":
|
|
753
|
+
"aria-label": prompt
|
|
748
754
|
});
|
|
749
|
-
asking.append(question,
|
|
750
|
-
if (taken.length && !draft) asking.append(button("keep-draft", text.minimise, () => minimise()));
|
|
751
|
-
asking.append(stay);
|
|
755
|
+
asking.append(question, ...choices, stay);
|
|
752
756
|
asking.addEventListener("keydown", (event) => {
|
|
753
757
|
if (event.key !== "Escape") return;
|
|
754
758
|
event.preventDefault();
|
|
@@ -758,17 +762,25 @@ function mount(parent, handlers, settings = {}) {
|
|
|
758
762
|
header.after(asking);
|
|
759
763
|
stay.focus();
|
|
760
764
|
}
|
|
765
|
+
function askDiscard() {
|
|
766
|
+
const discard = button("discard", text.discard, handlers.close);
|
|
767
|
+
const fold = taken.length && !draft ? [button("keep-draft", text.minimise, () => minimise())] : [];
|
|
768
|
+
ask(text.discardConfirm, [discard, ...fold], cancel);
|
|
769
|
+
}
|
|
761
770
|
function toolbar() {
|
|
762
771
|
if (!drawing) return [];
|
|
763
772
|
const toolbar = element("div", "toolbar");
|
|
764
773
|
const undo = iconButton("undo", text.undo, ICONS.undo, () => annotator?.undo());
|
|
765
|
-
const clear = iconButton("clear", text.clear, ICONS.clear, () =>
|
|
774
|
+
const clear = iconButton("clear", text.clear, ICONS.clear, () => {
|
|
775
|
+
const asked = annotator;
|
|
776
|
+
const yes = button("confirm-clear", text.clear, () => {
|
|
777
|
+
asking?.remove();
|
|
778
|
+
asked?.clear();
|
|
779
|
+
(undo.disabled ? buttons.find((b) => b.getAttribute("aria-pressed") === "true") : undo)?.focus();
|
|
780
|
+
});
|
|
781
|
+
ask(text.clearConfirm, [yes], clear);
|
|
782
|
+
});
|
|
766
783
|
const remove = iconButton("remove", text.remove, ICONS.remove, () => annotator?.remove());
|
|
767
|
-
const actions = choices.includes("select") ? [
|
|
768
|
-
remove,
|
|
769
|
-
undo,
|
|
770
|
-
clear
|
|
771
|
-
] : [undo, clear];
|
|
772
784
|
refresh = () => {
|
|
773
785
|
const off = /* @__PURE__ */ new Map([
|
|
774
786
|
[undo, !annotator?.canUndo],
|
|
@@ -789,8 +801,10 @@ function mount(parent, handlers, settings = {}) {
|
|
|
789
801
|
};
|
|
790
802
|
const buttons = choices.map((choice) => iconButton("tool", text[choice], ICONS[choice], () => pick(choice)));
|
|
791
803
|
pick(choices.find((choice) => choice !== "select"));
|
|
792
|
-
|
|
793
|
-
|
|
804
|
+
const tools = buttons.flatMap((choice, i) => choices[i] === "select" ? [choice, remove] : [choice]);
|
|
805
|
+
const divider = element("span", "divider", { "aria-hidden": "true" });
|
|
806
|
+
toolbar.append(...tools, divider, undo, clear);
|
|
807
|
+
locked.push(...tools, undo, clear);
|
|
794
808
|
return [toolbar];
|
|
795
809
|
}
|
|
796
810
|
let thumb;
|
|
@@ -832,6 +846,7 @@ function mount(parent, handlers, settings = {}) {
|
|
|
832
846
|
stage?.prepend(shot.image);
|
|
833
847
|
renderStrip();
|
|
834
848
|
refresh();
|
|
849
|
+
measure();
|
|
835
850
|
if (focused) strip?.querySelector("[aria-current=true]")?.focus();
|
|
836
851
|
}
|
|
837
852
|
function drop(index) {
|
|
@@ -983,6 +998,7 @@ function mount(parent, handlers, settings = {}) {
|
|
|
983
998
|
lockPage(true);
|
|
984
999
|
renderStrip();
|
|
985
1000
|
reink();
|
|
1001
|
+
measure();
|
|
986
1002
|
note?.focus();
|
|
987
1003
|
}
|
|
988
1004
|
function reink() {
|
|
@@ -996,7 +1012,7 @@ function mount(parent, handlers, settings = {}) {
|
|
|
996
1012
|
const options = handlers.options();
|
|
997
1013
|
let module;
|
|
998
1014
|
try {
|
|
999
|
-
module = await import("./report-
|
|
1015
|
+
module = await import("./report-BJADH9_W.mjs");
|
|
1000
1016
|
if (!report) {
|
|
1001
1017
|
const capture = await import("./capture-CufRosj3.mjs");
|
|
1002
1018
|
const shots = [];
|
|
@@ -1016,7 +1032,7 @@ function mount(parent, handlers, settings = {}) {
|
|
|
1016
1032
|
if (mounted) sent();
|
|
1017
1033
|
} catch (error) {
|
|
1018
1034
|
handlers.emit("error", error);
|
|
1019
|
-
if (mounted) failed(module?.shown(error));
|
|
1035
|
+
if (mounted) failed(module?.late(error) ? text.timeout : module?.shown(error));
|
|
1020
1036
|
}
|
|
1021
1037
|
}
|
|
1022
1038
|
let saving = false;
|
|
@@ -1132,6 +1148,7 @@ function mount(parent, handlers, settings = {}) {
|
|
|
1132
1148
|
body = element("div", "body");
|
|
1133
1149
|
if (image) {
|
|
1134
1150
|
stage = element("div", "stage");
|
|
1151
|
+
fit.observe(stage);
|
|
1135
1152
|
strip = element("div", "strip");
|
|
1136
1153
|
stage.append(strip);
|
|
1137
1154
|
body.append(...toolbar(), stage);
|
|
@@ -1223,7 +1240,9 @@ function mount(parent, handlers, settings = {}) {
|
|
|
1223
1240
|
locked.push(more);
|
|
1224
1241
|
footer.append(split);
|
|
1225
1242
|
panel.classList.toggle("wide", Boolean(image));
|
|
1226
|
-
|
|
1243
|
+
const side = element("div", "side");
|
|
1244
|
+
side.append(label, box, footer);
|
|
1245
|
+
panel.replaceChildren(header, ...image ? [body] : [], side);
|
|
1227
1246
|
if (image) add(image);
|
|
1228
1247
|
ink(send);
|
|
1229
1248
|
ink(more);
|
|
@@ -1236,6 +1255,7 @@ function mount(parent, handlers, settings = {}) {
|
|
|
1236
1255
|
asking?.remove();
|
|
1237
1256
|
for (const control of locked) control.disabled = true;
|
|
1238
1257
|
for (const item of inert) item.inert = true;
|
|
1258
|
+
cancel.disabled = true;
|
|
1239
1259
|
failure?.remove();
|
|
1240
1260
|
primary.setAttribute("aria-disabled", "true");
|
|
1241
1261
|
primary.textContent = text.sending;
|
|
@@ -1250,25 +1270,33 @@ function mount(parent, handlers, settings = {}) {
|
|
|
1250
1270
|
status.textContent = text.sent;
|
|
1251
1271
|
panel.classList.remove("wide");
|
|
1252
1272
|
header.replaceChildren(title, cancel);
|
|
1273
|
+
cancel.disabled = false;
|
|
1253
1274
|
panel.replaceChildren(header, status);
|
|
1254
1275
|
status.focus();
|
|
1255
|
-
done = setTimeout(handlers.close,
|
|
1276
|
+
done = setTimeout(handlers.close, 500);
|
|
1277
|
+
const shown = performance.now();
|
|
1278
|
+
backdrop.addEventListener("click", (event) => {
|
|
1279
|
+
if (event.timeStamp >= shown) handlers.close();
|
|
1280
|
+
});
|
|
1256
1281
|
}
|
|
1257
1282
|
function failed(message = text.error) {
|
|
1258
1283
|
if (!primary || !composer) return;
|
|
1259
1284
|
failure = element("p", "error", { role: "alert" });
|
|
1260
1285
|
failure.textContent = message;
|
|
1261
1286
|
composer.after(failure);
|
|
1287
|
+
cancel.disabled = false;
|
|
1262
1288
|
primary.removeAttribute("aria-disabled");
|
|
1263
1289
|
primary.textContent = text.retry;
|
|
1264
1290
|
primary.setAttribute("part", "retry");
|
|
1265
1291
|
primary.focus();
|
|
1266
1292
|
}
|
|
1267
1293
|
const onEscape = () => {
|
|
1268
|
-
if (
|
|
1294
|
+
if (busy()) {
|
|
1295
|
+
if (!backdrop.open) backdrop.showModal();
|
|
1296
|
+
} else if (taken.length && !draft) minimise();
|
|
1269
1297
|
else if (lose()) {
|
|
1270
1298
|
if (!backdrop.open) backdrop.showModal();
|
|
1271
|
-
|
|
1299
|
+
askDiscard();
|
|
1272
1300
|
} else handlers.close();
|
|
1273
1301
|
};
|
|
1274
1302
|
backdrop.addEventListener("cancel", (event) => {
|
|
@@ -1294,6 +1322,29 @@ function mount(parent, handlers, settings = {}) {
|
|
|
1294
1322
|
parent.classList.add("drafting");
|
|
1295
1323
|
if (keeping()) import("./keep-BG5WxZPU.mjs").then(({ sweep }) => sweep()).catch(() => {});
|
|
1296
1324
|
let done;
|
|
1325
|
+
let fitting = 0;
|
|
1326
|
+
const fit = new ResizeObserver(() => {
|
|
1327
|
+
cancelAnimationFrame(fitting);
|
|
1328
|
+
fitting = requestAnimationFrame(measure);
|
|
1329
|
+
});
|
|
1330
|
+
function measure() {
|
|
1331
|
+
const image = stage?.querySelector(":scope > .image");
|
|
1332
|
+
const shown = image?.getBoundingClientRect();
|
|
1333
|
+
const outer = panel.getBoundingClientRect();
|
|
1334
|
+
if (!image || !shown || !outer.width || !stage || !panel.classList.contains("wide")) return;
|
|
1335
|
+
const rail = panel.querySelector(".toolbar")?.getBoundingClientRect().height ?? 0;
|
|
1336
|
+
const column = stage.getBoundingClientRect();
|
|
1337
|
+
const below = Math.max(0, rail - column.height);
|
|
1338
|
+
const room = getComputedStyle(backdrop);
|
|
1339
|
+
const across = backdrop.clientWidth - Number.parseFloat(room.paddingLeft) - Number.parseFloat(room.paddingRight);
|
|
1340
|
+
const high = backdrop.clientHeight - Number.parseFloat(room.paddingTop) - Number.parseFloat(room.paddingBottom) - (outer.height - shown.height - below);
|
|
1341
|
+
const wide = across - (outer.width - column.width);
|
|
1342
|
+
const floor = 4 * Number.parseFloat(getComputedStyle(image).fontSize);
|
|
1343
|
+
const height = Math.min(wide * image.height / image.width, Math.max(floor, high));
|
|
1344
|
+
panel.style.setProperty("--_ih", `${Math.floor(height)}px`);
|
|
1345
|
+
}
|
|
1346
|
+
fit.observe(panel);
|
|
1347
|
+
fit.observe(backdrop);
|
|
1297
1348
|
let scrollLocked = false;
|
|
1298
1349
|
function lockPage(on) {
|
|
1299
1350
|
if (on !== scrollLocked) (on ? lockScroll : unlockScroll)();
|
|
@@ -1366,6 +1417,8 @@ function mount(parent, handlers, settings = {}) {
|
|
|
1366
1417
|
const stays = reviving || !!keep && !submitted && store();
|
|
1367
1418
|
mounted = false;
|
|
1368
1419
|
clearTimeout(done);
|
|
1420
|
+
fit.disconnect();
|
|
1421
|
+
cancelAnimationFrame(fitting);
|
|
1369
1422
|
lockPage(false);
|
|
1370
1423
|
backdrop.removeEventListener("close", closed);
|
|
1371
1424
|
if (backdrop.open) backdrop.close();
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
|
|
2
|
-
import { a as FeedbackWidget, c as CaptureOptions, i as FeedbackStrings, n as FeedbackEvents, o as createFeedback, r as FeedbackOptions, s as discardDraft, t as FeedbackEvent } from "./widget-
|
|
2
|
+
import { a as FeedbackWidget, c as CaptureOptions, i as FeedbackStrings, n as FeedbackEvents, o as createFeedback, r as FeedbackOptions, s as discardDraft, t as FeedbackEvent } from "./widget-CMjTbhGw.mjs";
|
|
3
3
|
import { n as Mark, r as Screenshot, t as FeedbackReport } from "./report-CaJegYyg.mjs";
|
|
4
4
|
export { type CaptureOptions, type FeedbackEvent, type FeedbackEvents, type FeedbackOptions, type FeedbackReport, type FeedbackStrings, type FeedbackWidget, type Mark, type Screenshot, createFeedback, discardDraft };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
|
|
2
|
-
export { t as createFeedback, n as discardDraft } from "./widget-
|
|
2
|
+
export { t as createFeedback, n as discardDraft } from "./widget-JBN1fbyx.mjs";
|
package/dist/react.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import { r as FeedbackOptions } from "./widget-
|
|
2
|
+
import { r as FeedbackOptions } from "./widget-CMjTbhGw.mjs";
|
|
3
3
|
//#region src/react/index.d.ts
|
|
4
4
|
export type FeedbackProps = FeedbackOptions;
|
|
5
5
|
export declare function Feedback(props: FeedbackProps): null;
|
package/dist/react.mjs
CHANGED
|
@@ -66,8 +66,29 @@ async function build(draft, shots, seen, settings) {
|
|
|
66
66
|
screenshots
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
|
+
/** How long a send may take before it fails as a TimeoutError. */
|
|
70
|
+
const SEND_TIMEOUT = 3e4;
|
|
71
|
+
const limits = /* @__PURE__ */ new WeakSet();
|
|
72
|
+
/** Whether the send failed on the widget's own 30 second limit. */
|
|
73
|
+
function late(error) {
|
|
74
|
+
return typeof error === "object" && error !== null && limits.has(error);
|
|
75
|
+
}
|
|
69
76
|
/** Sends with onSubmit, or else POSTs what parseFeedbackRequest reads to the endpoint. */
|
|
70
77
|
async function deliver(report, settings) {
|
|
78
|
+
const stop = new AbortController();
|
|
79
|
+
const timer = setTimeout(() => {
|
|
80
|
+
const error = new DOMException("The send took over 30 seconds.", "TimeoutError");
|
|
81
|
+
limits.add(error);
|
|
82
|
+
stop.abort(error);
|
|
83
|
+
}, SEND_TIMEOUT);
|
|
84
|
+
const late = new Promise((_, reject) => stop.signal.addEventListener("abort", () => reject(stop.signal.reason), { once: true }));
|
|
85
|
+
try {
|
|
86
|
+
return await Promise.race([send(report, settings, stop.signal), late]);
|
|
87
|
+
} finally {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function send(report, settings, signal) {
|
|
71
92
|
const { onSubmit, endpoint } = settings;
|
|
72
93
|
if (onSubmit) return host(() => onSubmit(report));
|
|
73
94
|
if (!endpoint) throw new Error("Neither onSubmit nor endpoint is set.");
|
|
@@ -87,9 +108,10 @@ async function deliver(report, settings) {
|
|
|
87
108
|
method: "POST",
|
|
88
109
|
body,
|
|
89
110
|
headers,
|
|
90
|
-
redirect: "manual"
|
|
111
|
+
redirect: "manual",
|
|
112
|
+
signal
|
|
91
113
|
});
|
|
92
114
|
if (!response.ok) throw new Error(`The endpoint answered ${response.status}.`);
|
|
93
115
|
}
|
|
94
116
|
//#endregion
|
|
95
|
-
export { build, deliver, shown };
|
|
117
|
+
export { build, deliver, late, shown };
|
|
@@ -23,7 +23,7 @@ type FeedbackEvents = {
|
|
|
23
23
|
};
|
|
24
24
|
type FeedbackEvent = keyof FeedbackEvents;
|
|
25
25
|
/** The texts the widget shows, by SPEC §12 key. */
|
|
26
|
-
type FeedbackStrings = Record<"trigger" | "title" | "capturing" | "captureError" | "sendWithout" | "noteLabel" | "notePlaceholder" | "noteRequired" | "categoryLabel" | "pen" | "box" | "arrow" | "text" | "select" | "remove" | "undo" | "clear" | "cancel" | "send" | "sending" | "sent" | "error" | "retry" | "triggerText" | "escHint" | "minimise" | "screenshot" | "addScreenshot" | "removeScreenshot" | "draft" | "captureView" | "reopen" | "discard" | "discardConfirm" | "keepEditing" | "moreSend" | "savePdf" | "pdfError", string>;
|
|
26
|
+
type FeedbackStrings = Record<"trigger" | "title" | "capturing" | "captureError" | "sendWithout" | "noteLabel" | "notePlaceholder" | "noteRequired" | "categoryLabel" | "pen" | "box" | "arrow" | "text" | "select" | "remove" | "undo" | "clear" | "clearConfirm" | "cancel" | "send" | "sending" | "sent" | "error" | "timeout" | "retry" | "triggerText" | "escHint" | "minimise" | "screenshot" | "addScreenshot" | "removeScreenshot" | "draft" | "captureView" | "reopen" | "discard" | "discardConfirm" | "keepEditing" | "moreSend" | "savePdf" | "pdfError", string>;
|
|
27
27
|
type FeedbackOptions = {
|
|
28
28
|
onSubmit?: (report: FeedbackReport) => Promise<void>;
|
|
29
29
|
endpoint?: string;
|
|
@@ -37,7 +37,7 @@ type FeedbackOptions = {
|
|
|
37
37
|
requireNote?: boolean;
|
|
38
38
|
redactUrl?: boolean;
|
|
39
39
|
context?: () => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
40
|
-
trigger?: "button" | "none";
|
|
40
|
+
trigger?: "tab" | "button" | "none";
|
|
41
41
|
position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
|
|
42
42
|
shortcut?: string | false;
|
|
43
43
|
strings?: Partial<FeedbackStrings>;
|
|
@@ -162,23 +162,24 @@ function sheet(text) {
|
|
|
162
162
|
return result;
|
|
163
163
|
}
|
|
164
164
|
function variables() {
|
|
165
|
-
return `.vf{--_r:var(--vf-radius,var(--_pr,16px));--_s:var(--vf-surface,var(--_ps,Canvas));--_t:var(--vf-text,var(--_pt,CanvasText));--_m:var(--vf-muted,var(--_pm,color-mix(in srgb,var(--_t) 60%,transparent)));--_b:var(--vf-border,color-mix(in srgb,var(--_t) 15%,transparent));--_a:var(--vf-accent,var(--_pa,${CSS.supports("color", "AccentColor") ? "AccentColor" : "#0b57d0"}));--_at:var(--vf-accent-text,#fff);--_f:var(--vf-focus,var(--_a));--_mark:var(--vf-mark,#e5243b);--_bd:var(--vf-backdrop,rgb(0 0 0/45%));--_sh:var(--vf-shadow,0 12px 32px -12px rgb(0 0 0/35%));--_size:var(--vf-trigger-size,2.5em);--_off:var(--vf-offset,16px);--_z:var(--vf-z,2147483000)}`;
|
|
165
|
+
return `.vf{--_r:var(--vf-radius,var(--_pr,16px));--_s:var(--vf-surface,var(--_ps,Canvas));--_t:var(--vf-text,var(--_pt,CanvasText));--_m:var(--vf-muted,var(--_pm,color-mix(in srgb,var(--_t) 60%,transparent)));--_b:var(--vf-border,color-mix(in srgb,var(--_t) 15%,transparent));--_a:var(--vf-accent,var(--_pa,${CSS.supports("color", "AccentColor") ? "AccentColor" : "#0b57d0"}));--_at:var(--vf-accent-text,#fff);--_f:var(--vf-focus,var(--_a));--_mark:var(--vf-mark,#e5243b);--_bd:var(--vf-backdrop,rgb(0 0 0/45%));--_sh:var(--vf-shadow,0 12px 32px -12px rgb(0 0 0/35%));--_size:var(--vf-trigger-size,2.5em);--_off:var(--vf-offset,16px);--_toff:var(--vf-offset,0px);--_z:var(--vf-z,2147483000)}`;
|
|
166
166
|
}
|
|
167
|
-
const LAYOUT = `.vf{display:contents;font-family:var(--vf-font,inherit);color-scheme:light dark}.trigger{font:inherit;position:fixed;z-index:var(--_z);box-sizing:border-box;height:var(--_size);margin:0;padding:0 14px 0 12px;display:flex;align-items:center;gap:8px;white-space:nowrap}.trigger[data-icon]{width:var(--_size);padding:0;justify-content:center}.trigger[data-icon]
|
|
168
|
-
const THEME = `.trigger{font-weight:600;border:
|
|
169
|
-
function icon() {
|
|
167
|
+
const LAYOUT = `.vf{display:contents;font-family:var(--vf-font,inherit);color-scheme:light dark}.trigger{font:inherit;position:fixed;z-index:var(--_z);box-sizing:border-box;height:var(--_size);margin:0;padding:0 14px 0 12px;display:flex;align-items:center;gap:8px;white-space:nowrap}.trigger[data-icon]{width:var(--_size);padding:0;justify-content:center}.trigger[data-icon]>svg{width:62.5%;height:62.5%}[data-y=bottom]{bottom:calc(var(--_off) + env(safe-area-inset-bottom,0px))}[data-y=top]{top:calc(var(--_off) + env(safe-area-inset-top,0px))}[data-x=right]{right:calc(var(--_off) + env(safe-area-inset-right,0px))}[data-x=left]{left:calc(var(--_off) + env(safe-area-inset-left,0px))}.trigger:where([data-tab][data-y=bottom]){bottom:calc(var(--_toff) + env(safe-area-inset-bottom,0px))}.trigger:where([data-tab][data-y=top]){top:calc(var(--_toff) + env(safe-area-inset-top,0px))}.trigger:where([data-tab][data-x=right]){right:env(safe-area-inset-right,0px)}.trigger:where([data-tab][data-x=left]){left:env(safe-area-inset-left,0px)}.trigger>svg{width:1.15em;height:1.15em}.tip{position:absolute;top:50%;translate:0 -50%;direction:ltr;box-sizing:border-box;width:max-content;max-width:calc(100vw - 100% - 2 * var(--_off) - 10px);white-space:normal;display:flex;align-items:center;gap:4px;visibility:hidden;opacity:0;pointer-events:none;transition:opacity .15s,visibility .15s}[data-x=right]>.tip{right:calc(100% + 10px)}[data-x=left]>.tip{left:calc(100% + 10px);flex-direction:row-reverse}[data-x=left]>.tip svg{rotate:180deg}.tip svg{width:1.15em;height:1.15em;flex:none}.trigger:focus-visible>.tip{visibility:inherit;opacity:1}@media (hover:hover){.trigger:hover>.tip{visibility:inherit;opacity:1}}.capturing,.capturing .backdrop{visibility:hidden}.capturing .tip{transition:none}.still .trigger{transition:none!important}:focus-visible{outline:2px solid var(--_f);outline-offset:2px}@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}`;
|
|
168
|
+
const THEME = `.trigger{font-weight:600;border:0;border-radius:999px;background:var(--_t);color:var(--_s);box-shadow:var(--_sh);cursor:pointer;transition:background-color .15s,color .15s}@media (hover:hover){.trigger:hover{background:var(--_s);color:var(--_t)}}.tip{padding:6px 8px 6px 14px;border-radius:999px;background:var(--_t);color:var(--_s);font-weight:700;box-shadow:var(--_sh)}[data-x=left]>.tip{padding:6px 14px 6px 8px}.trigger:where([data-tab]){--_tr:clamp(0px,var(--_toff) * 1000,var(--_r));border-radius:var(--_r)}.trigger:where([data-tab][data-y=bottom]){border-bottom-left-radius:var(--_tr);border-bottom-right-radius:var(--_tr)}.trigger:where([data-tab][data-y=top]){border-top-left-radius:var(--_tr);border-top-right-radius:var(--_tr)}.trigger:where([data-tab][data-x=right]){border-top-right-radius:0;border-bottom-right-radius:0}.trigger:where([data-tab][data-x=left]){border-top-left-radius:0;border-bottom-left-radius:0}`;
|
|
169
|
+
function icon(d, attributes) {
|
|
170
170
|
const svg = document.createElementNS(SVG, "svg");
|
|
171
171
|
const path = document.createElementNS(SVG, "path");
|
|
172
172
|
for (const [name, value] of Object.entries({
|
|
173
173
|
viewBox: "0 0 24 24",
|
|
174
174
|
"aria-hidden": "true",
|
|
175
|
-
|
|
176
|
-
part: "trigger-icon"
|
|
175
|
+
...attributes
|
|
177
176
|
})) svg.setAttribute(name, value);
|
|
178
|
-
path.setAttribute("d",
|
|
177
|
+
path.setAttribute("d", d);
|
|
179
178
|
svg.append(path);
|
|
180
179
|
return svg;
|
|
181
180
|
}
|
|
181
|
+
const FLAG = "M4 3a1 1 0 0 1 2 0v18a1 1 0 0 1-2 0zM7 4c2.5-1.5 5-1.5 7 0s4 1.5 6 0v10c-2 1.5-4 1.5-6 0s-4.5-1.5-7 0z";
|
|
182
|
+
const ARROW = "M5 12h14m-6-6 6 6-6 6";
|
|
182
183
|
function field(element) {
|
|
183
184
|
return element.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(element.tagName);
|
|
184
185
|
}
|
|
@@ -289,6 +290,9 @@ function createFeedback(options = {}) {
|
|
|
289
290
|
let pending;
|
|
290
291
|
let trigger;
|
|
291
292
|
let label;
|
|
293
|
+
const tip = document.createElement("span");
|
|
294
|
+
tip.dir = "auto";
|
|
295
|
+
tip.setAttribute("part", "trigger-tooltip-text");
|
|
292
296
|
let unlisten;
|
|
293
297
|
function listen(shortcut) {
|
|
294
298
|
const parsed = parse(shortcut);
|
|
@@ -340,18 +344,35 @@ function createFeedback(options = {}) {
|
|
|
340
344
|
trigger.setAttribute("part", "trigger");
|
|
341
345
|
label = document.createElement("span");
|
|
342
346
|
label.setAttribute("part", "trigger-text");
|
|
343
|
-
|
|
347
|
+
const tooltip = document.createElement("span");
|
|
348
|
+
tooltip.className = "tip";
|
|
349
|
+
tooltip.id = "vf-tip";
|
|
350
|
+
tooltip.setAttribute("part", "trigger-tooltip");
|
|
351
|
+
tooltip.setAttribute("aria-hidden", "true");
|
|
352
|
+
tooltip.append(tip, icon(ARROW, {
|
|
353
|
+
fill: "none",
|
|
354
|
+
stroke: "currentColor",
|
|
355
|
+
"stroke-width": "2.4",
|
|
356
|
+
"stroke-linecap": "round",
|
|
357
|
+
part: "icon"
|
|
358
|
+
}));
|
|
359
|
+
trigger.setAttribute("aria-describedby", tooltip.id);
|
|
360
|
+
trigger.append(icon(FLAG, {
|
|
361
|
+
fill: "currentColor",
|
|
362
|
+
part: "trigger-icon"
|
|
363
|
+
}), label, tooltip);
|
|
344
364
|
trigger.addEventListener("click", start);
|
|
345
365
|
wrapper.prepend(trigger);
|
|
346
366
|
}
|
|
347
367
|
const [y, x] = (options.position ?? "bottom-right").split("-");
|
|
348
368
|
trigger.dataset.y = y;
|
|
349
369
|
trigger.dataset.x = x;
|
|
370
|
+
trigger.toggleAttribute("data-tab", options.trigger !== "button");
|
|
350
371
|
const { trigger: name, triggerText } = options.strings ?? {};
|
|
351
372
|
const accessible = name || triggerText || "Send feedback";
|
|
352
373
|
trigger.setAttribute("aria-label", accessible);
|
|
353
374
|
const keys = options.shortcut ? spoken(options.shortcut) : void 0;
|
|
354
|
-
|
|
375
|
+
tip.textContent = keys ? `${accessible} (${keys})` : accessible;
|
|
355
376
|
trigger.toggleAttribute("data-icon", !triggerText);
|
|
356
377
|
if (label) {
|
|
357
378
|
label.textContent = triggerText ?? "";
|
|
@@ -469,7 +490,7 @@ function createFeedback(options = {}) {
|
|
|
469
490
|
returnFocus = deepActive();
|
|
470
491
|
const stream = kept ? void 0 : requestDisplay();
|
|
471
492
|
try {
|
|
472
|
-
dialogs ??= import("./dialog-
|
|
493
|
+
dialogs ??= import("./dialog-BL7k2Jir.mjs").catch((error) => {
|
|
473
494
|
dialogs = void 0;
|
|
474
495
|
throw error;
|
|
475
496
|
});
|
package/docs/API.md
CHANGED
|
@@ -34,10 +34,10 @@ const feedback = createFeedback({
|
|
|
34
34
|
### Options
|
|
35
35
|
| option | type | default | meaning |
|
|
36
36
|
|---|---|---|---|
|
|
37
|
-
| `onSubmit` | `(report) => Promise<void>` | – | Sends the report. A throw shows the send error state, using the error's message. Give this or `endpoint`; with both, `onSubmit` is used. |
|
|
38
|
-
| `endpoint` | `string` | – | POSTs `multipart/form-data` with a `report` part (JSON, each screenshot without its image) and one part per screenshot, `image0`, `image1` and on, named `<id>-<n>.webp` or `<id>-<n>.png`. An answer that is not 2xx, a redirect included, shows the send error state with the default `error` text. It is a same-origin `fetch`, so it sends cookies only to the page's own origin; another origin needs CORS on its server and a `connect-src` that allows it. |
|
|
37
|
+
| `onSubmit` | `(report) => Promise<void>` | – | Sends the report. A throw shows the send error state, using the error's message. A promise still pending after 30 seconds shows it with the `timeout` text, and `error` fires with a `TimeoutError`; the widget stops waiting, but cannot stop the host's work. Give this or `endpoint`; with both, `onSubmit` is used. |
|
|
38
|
+
| `endpoint` | `string` | – | POSTs `multipart/form-data` with a `report` part (JSON, each screenshot without its image) and one part per screenshot, `image0`, `image1` and on, named `<id>-<n>.webp` or `<id>-<n>.png`. An answer that is not 2xx, a redirect included, shows the send error state with the default `error` text. A request still under way after 30 seconds is aborted and shows the `timeout` text. It is a same-origin `fetch`, so it sends cookies only to the page's own origin; another origin needs CORS on its server and a `connect-src` that allows it. |
|
|
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
|
-
| `trigger` | `"button" \| "none"` | `"
|
|
40
|
+
| `trigger` | `"tab" \| "button" \| "none"` | `"tab"` | `"tab"` touches the side edge, `"button"` is round, in the corner (SPEC §3). `"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
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. |
|
package/docs/SPEC.md
CHANGED
|
@@ -8,15 +8,15 @@ package sends nothing by itself, and stores nothing but an unsent draft on the u
|
|
|
8
8
|
|
|
9
9
|
## 2. Flow
|
|
10
10
|
```
|
|
11
|
-
idle --open--> capturing --image ready--> annotating --Send--> sending --ok--> sent --
|
|
11
|
+
idle --open--> capturing --image ready--> annotating --Send--> sending --ok--> sent --0.5 s or a click--> idle
|
|
12
12
|
| | ^ |
|
|
13
13
|
+--fails--> capture error | | +--fails--> send error
|
|
14
14
|
| +--Open, or Capture this view (capturing first)
|
|
15
15
|
+--Esc / Keep as draft / Add screenshot--> draft
|
|
16
16
|
annotating, draft --Cancel (it may ask first, below) / Discard--> idle
|
|
17
17
|
```
|
|
18
|
-
- **idle:** the trigger
|
|
19
|
-
where the host calls `open()`).
|
|
18
|
+
- **idle:** the trigger shows, a tab (`trigger: "tab"`) or a round button (`trigger: "button"`), or
|
|
19
|
+
nothing shows (`trigger: "none"`, where the host calls `open()`).
|
|
20
20
|
- **capturing:** the trigger and any open panel are hidden, so they are not in the image. The
|
|
21
21
|
screen is captured (SPEC §5), and a screen reader is told the `capturing` text.
|
|
22
22
|
- **capture error:** the `captureError` text, with "Try again" (back to capturing), "Send without
|
|
@@ -24,6 +24,12 @@ annotating, draft --Cancel (it may ask first, below) / Discard--> idle
|
|
|
24
24
|
(idle). This is the first screenshot only; a later one that fails is the draft's (below).
|
|
25
25
|
- **annotating:** a full-viewport overlay shows the image with a tool rail beside it, a strip of
|
|
26
26
|
the report's screenshots under it, and the note field, with Send in a footer row under that.
|
|
27
|
+
The dialog fits the window, 16 px from its edges (or the safe area's), with nothing to scroll:
|
|
28
|
+
the image takes the height the rest leaves, growing past the screenshot's own size on a large
|
|
29
|
+
screen, and the dialog is as wide as the image needs. Below
|
|
30
|
+
760 px of height the tool rail has two columns, and below 700 px a question lies over the
|
|
31
|
+
header in place of adding a row; on a landscape screen under 500 px high, the note and Send go
|
|
32
|
+
beside the image.
|
|
27
33
|
Esc and the header's Keep as draft fold it into the draft; the header's close button (Cancel)
|
|
28
34
|
discards it. When that would throw away a note, a mark, a second screenshot or a chosen category,
|
|
29
35
|
Cancel first asks `discardConfirm`, with "Discard", `minimise` (before Send, with a screenshot) and
|
|
@@ -41,26 +47,35 @@ annotating, draft --Cancel (it may ask first, below) / Discard--> idle
|
|
|
41
47
|
it.
|
|
42
48
|
- **sending:** the note, the category and the tools are disabled, and the Send button shows
|
|
43
49
|
progress. The Esc hint goes, as it is absent with no screenshot: from Send on, Esc keeps no
|
|
44
|
-
draft,
|
|
45
|
-
|
|
46
|
-
- **
|
|
50
|
+
draft. Until the send ends, Cancel, Keep as draft and Esc do nothing. A send that has not ended
|
|
51
|
+
after 30 seconds fails, with the `timeout` text.
|
|
52
|
+
- **sent:** the `sent` confirmation for half a second, or until a click anywhere on the overlay,
|
|
53
|
+
then idle.
|
|
54
|
+
- **send error:** the message is the `timeout` text after 30 seconds, else the host's error, or
|
|
55
|
+
else the `error` text. The note and
|
|
47
56
|
the marks are kept, and "Try again" sends the same report. Cancel and Esc, after
|
|
48
57
|
the same question when it would throw work away, close the dialog, and the draft goes with it.
|
|
49
58
|
|
|
50
59
|
## 3. The trigger
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
- By default it is a small tab, 2.5em square (40 px on a 16 px site), in the corner `position`
|
|
61
|
+
names, square on both edges there, with a flag icon and the `trigger` accessible name.
|
|
62
|
+
`--vf-offset` lifts it along its side edge, and then its end side is rounded too.
|
|
63
|
+
`trigger: "button"` makes it a round button in that corner instead, 16 px from both edges.
|
|
64
|
+
Either has no border: the text colour fills it, and the icon and label take the background
|
|
65
|
+
colour. On hover, on a device that can hover, the two swap back.
|
|
66
|
+
A host that gives `triggerText` gets that short label beside a small icon, in a wider tab, or a
|
|
67
|
+
pill as a button; with only `triggerText` given, that text is the name too. A host that sets
|
|
68
|
+
both keeps `triggerText` inside `trigger`, so speech control can say what it sees. It has no
|
|
69
|
+
badge, and it does not move or pulse.
|
|
70
|
+
- Its tooltip is its own, not the browser's `title`: a pill beside it, on the page's side, with an
|
|
71
|
+
arrow pointing at it. It fades in at once on keyboard focus, or on hover where the device can
|
|
72
|
+
hover, and out at the same speed.
|
|
73
|
+
It reads the name. With a `shortcut` that can match, it is the name and the keys, each modifier
|
|
74
|
+
once, joined by ` + `, as the platform's keyboards name them: Option, Command, Control and Shift
|
|
75
|
+
on a Mac, iPhone or iPad (`navigator.platform`), as in `Send feedback (Option + Shift + F)`;
|
|
76
|
+
Alt, Win, Ctrl and Shift on Windows; Alt, Super, Ctrl and Shift elsewhere. A screen reader reads
|
|
77
|
+
the tooltip as the description.
|
|
78
|
+
- It respects `env(safe-area-inset-*)`.
|
|
64
79
|
- It is hidden while there is a draft, whether the dialog is open or kept in the draft bar.
|
|
65
80
|
- It does not move to keep clear of the page's content. The host moves it off its own content
|
|
66
81
|
with `position` or `--vf-offset`.
|
|
@@ -71,8 +86,9 @@ annotating, draft --Cancel (it may ask first, below) / Discard--> idle
|
|
|
71
86
|
is not kept; a label holds up to 280 characters. Labels are one size, in the mark colour with a white outline. Select picks a mark by
|
|
72
87
|
its line or a label by its text, and shows a dashed outline that never reaches the sent image: a
|
|
73
88
|
drag moves it, kept whole inside the image; the arrow keys move it (Shift for ten); Delete,
|
|
74
|
-
Backspace or the Delete button removes it; Escape lets go of
|
|
75
|
-
|
|
89
|
+
Backspace or the Delete button, under Select in the tool rail, removes it; Escape lets go of
|
|
90
|
+
it. Clear asks `clearConfirm` first, with Clear and `keepEditing`, then removes every mark on the
|
|
91
|
+
shown screenshot. Undo takes back the last add, move, removal or Clear, one at a time.
|
|
76
92
|
- **One colour** for the marks, `--vf-mark`, which defaults to a red that contrasts with most pages.
|
|
77
93
|
- Pointer events, so mouse, pen and touch all work. While drawing, touch never scrolls the page.
|
|
78
94
|
- **Note:** a textarea of 2000 characters at most, with a visible label (`noteLabel`). It is
|
|
@@ -147,7 +163,7 @@ Defined in `docs/API.md` (`FeedbackReport`). It holds:
|
|
|
147
163
|
mark (§4), or on Cancel's question (§2),
|
|
148
164
|
keeps an unsent draft that has a screenshot in the draft bar, and moves the focus to it ("Capture
|
|
149
165
|
this view", or "Open" at 10 screenshots). With no screenshot, or after a send error, it cancels,
|
|
150
|
-
asking first as Cancel does; while sending, and once sent
|
|
166
|
+
asking first as Cancel does; while sending it does nothing, and once sent it closes at once.
|
|
151
167
|
While the dialog is open, the page does not scroll. On close, focus goes back to the trigger,
|
|
152
168
|
or to where it was before.
|
|
153
169
|
- Every control has an accessible name and a visible focus ring (`--vf-focus`), `unstyled`
|
|
@@ -209,10 +225,12 @@ These are the default texts. Each one can be replaced through `strings` (`docs/A
|
|
|
209
225
|
| pen / box / arrow / text / select | Draw / Box / Arrow / Text / Select |
|
|
210
226
|
| remove | Delete |
|
|
211
227
|
| undo / clear | Undo / Clear |
|
|
228
|
+
| clearConfirm | Clear all marks on this screenshot? |
|
|
212
229
|
| cancel / send | Cancel / Send |
|
|
213
230
|
| sending | Sending… |
|
|
214
231
|
| sent | Thanks, it's sent. |
|
|
215
232
|
| error | It didn't send. Your note is kept. |
|
|
233
|
+
| timeout | It didn't send in 30 seconds. Check your internet connection, then try again. |
|
|
216
234
|
| retry | Try again |
|
|
217
235
|
| triggerText | none: the icon alone |
|
|
218
236
|
| escHint | Esc keeps it as a draft |
|
package/docs/THEMING.md
CHANGED
|
@@ -39,8 +39,8 @@ Set these on `visual-feedback` (or on any ancestor). Custom properties cross the
|
|
|
39
39
|
|---|---|---|
|
|
40
40
|
| `--vf-font` | `inherit` | font family of all text, such as `Inter, sans-serif` |
|
|
41
41
|
| `--vf-radius` | from the page's button corners, so the widget's buttons match them up to 15px (this is 1.6 times theirs, up to `24px`), else `16px` | panel and composer, and a share of it for buttons and the image |
|
|
42
|
-
| `--vf-surface` | with `theme: "auto"` the page's background, else `Canvas` | panel, tool rail and composer background |
|
|
43
|
-
| `--vf-text` | with `theme: "auto"` the page's text colour, else `CanvasText` | text,
|
|
42
|
+
| `--vf-surface` | with `theme: "auto"` the page's background, else `Canvas` | panel, tool rail and composer background, the trigger's icon and label, its fill on hover, and the text of its tooltip |
|
|
43
|
+
| `--vf-text` | with `theme: "auto"` the page's text colour, else `CanvasText` | text, the trigger's fill, its icon and label on hover, and its tooltip's background |
|
|
44
44
|
| `--vf-muted` | text at 60%, more on a page whose text would make that too faint | hints, counters |
|
|
45
45
|
| `--vf-border` | text at 15% | hairlines |
|
|
46
46
|
| `--vf-accent` | the page's accent, else `AccentColor`, or `#0b57d0` where the browser has no `AccentColor` | Send button and its menu button, "Capture this view", active tool, chosen screenshot |
|
|
@@ -48,9 +48,9 @@ Set these on `visual-feedback` (or on any ancestor). Custom properties cross the
|
|
|
48
48
|
| `--vf-focus` | `--vf-accent` | focus ring |
|
|
49
49
|
| `--vf-mark` | `#e5243b` | marks on the screenshot |
|
|
50
50
|
| `--vf-backdrop` | `rgb(0 0 0 / 45%)` | overlay behind the image |
|
|
51
|
-
| `--vf-shadow` | `0 12px 32px -12px rgb(0 0 0 / 35%)` | trigger, panel and draft bar |
|
|
51
|
+
| `--vf-shadow` | `0 12px 32px -12px rgb(0 0 0 / 35%)` | trigger, its tooltip, panel and draft bar |
|
|
52
52
|
| `--vf-trigger-size` | `2.5em`, 40px on a 16px site | trigger height, and its width when it shows the icon alone |
|
|
53
|
-
| `--vf-offset` | `16px`
|
|
53
|
+
| `--vf-offset` | `16px`, and `0px` for the tab; a length, not a percentage | draft bar and round trigger distance from the edges; the tab keeps to its side edge, and this lifts it along it |
|
|
54
54
|
| `--vf-z` | `2147483000` | stacking |
|
|
55
55
|
|
|
56
56
|
```css
|
|
@@ -62,14 +62,14 @@ visual-feedback {
|
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
## 3. `::part()`
|
|
65
|
-
Every visible element has a part name: `trigger`, `trigger-icon`, `trigger-text`, `
|
|
66
|
-
`dialog`, `header`, `title`, `minimise`, `body`, `stage`, `image`, `toolbar`, `tool`, `tool-active`,
|
|
65
|
+
Every visible element has a part name: `trigger`, `trigger-icon`, `trigger-text`, `trigger-tooltip`,
|
|
66
|
+
`trigger-tooltip-text`, `backdrop`, `dialog`, `header`, `title`, `minimise`, `body`, `stage`, `image`, `toolbar`, `tool`, `tool-active`,
|
|
67
67
|
`divider`, `undo`, `clear`, `remove`, `icon`, `text-input`, `selection`, `strip`, `thumb`,
|
|
68
68
|
`thumb-pick`, `thumb-image`, `thumb-number`, `thumb-remove`, `add`, `label`, `composer`, `note`,
|
|
69
69
|
`composer-bar`, `category`, `footer`, `split`, `send-more`, `menu`, `save-pdf`, `actions`, `cancel`,
|
|
70
70
|
`send`, `hint`, `status`, `error`, `retry`, `send-without`, `draft`, `draft-label`, `draft-thumbs`,
|
|
71
|
-
`capture-view`, `reopen`, `discard`, `confirm`, `confirm-text`, `keep-draft`, `keep-editing`. Cancel's
|
|
72
|
-
question is `confirm`, also an `actions` part, with its own `discard` button. After a send error, the Send button's part is `retry` until it
|
|
71
|
+
`capture-view`, `reopen`, `discard`, `confirm`, `confirm-text`, `confirm-clear`, `keep-draft`, `keep-editing`, `side`. Cancel's
|
|
72
|
+
question is `confirm`, also an `actions` part, with its own `discard` button; Clear's is `confirm` too, with `confirm-clear`. `side` holds the label, the composer and the footer; it lays nothing out except beside the image on a short landscape screen. After a send error, the Send button's part is `retry` until it
|
|
73
73
|
sends again, the same part as the capture error's "Try again".
|
|
74
74
|
|
|
75
75
|
```css
|
package/package.json
CHANGED