visual-feedback 0.4.0 → 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/CHANGELOG.md +13 -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-Cxcm3h1Z.d.mts → widget-CMjTbhGw.d.mts} +1 -1
- package/dist/{widget-BoohIxqp.mjs → widget-JBN1fbyx.mjs} +1 -1
- package/docs/API.md +2 -2
- package/docs/SPEC.md +19 -7
- package/docs/THEMING.md +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
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
|
+
|
|
3
16
|
## 0.4.0 (2026-10-13)
|
|
4
17
|
|
|
5
18
|
- Breaking: the trigger is a tab by default, in the corner `position` names, with a flag icon. It
|
|
@@ -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;
|
|
@@ -490,7 +490,7 @@ function createFeedback(options = {}) {
|
|
|
490
490
|
returnFocus = deepActive();
|
|
491
491
|
const stream = kept ? void 0 : requestDisplay();
|
|
492
492
|
try {
|
|
493
|
-
dialogs ??= import("./dialog-
|
|
493
|
+
dialogs ??= import("./dialog-BL7k2Jir.mjs").catch((error) => {
|
|
494
494
|
dialogs = void 0;
|
|
495
495
|
throw error;
|
|
496
496
|
});
|
package/docs/API.md
CHANGED
|
@@ -34,8 +34,8 @@ 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
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. |
|
package/docs/SPEC.md
CHANGED
|
@@ -8,7 +8,7 @@ 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)
|
|
@@ -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,9 +47,12 @@ 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
|
|
|
@@ -77,8 +86,9 @@ annotating, draft --Cancel (it may ask first, below) / Discard--> idle
|
|
|
77
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
|
|
78
87
|
its line or a label by its text, and shows a dashed outline that never reaches the sent image: a
|
|
79
88
|
drag moves it, kept whole inside the image; the arrow keys move it (Shift for ten); Delete,
|
|
80
|
-
Backspace or the Delete button removes it; Escape lets go of
|
|
81
|
-
|
|
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.
|
|
82
92
|
- **One colour** for the marks, `--vf-mark`, which defaults to a red that contrasts with most pages.
|
|
83
93
|
- Pointer events, so mouse, pen and touch all work. While drawing, touch never scrolls the page.
|
|
84
94
|
- **Note:** a textarea of 2000 characters at most, with a visible label (`noteLabel`). It is
|
|
@@ -153,7 +163,7 @@ Defined in `docs/API.md` (`FeedbackReport`). It holds:
|
|
|
153
163
|
mark (§4), or on Cancel's question (§2),
|
|
154
164
|
keeps an unsent draft that has a screenshot in the draft bar, and moves the focus to it ("Capture
|
|
155
165
|
this view", or "Open" at 10 screenshots). With no screenshot, or after a send error, it cancels,
|
|
156
|
-
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.
|
|
157
167
|
While the dialog is open, the page does not scroll. On close, focus goes back to the trigger,
|
|
158
168
|
or to where it was before.
|
|
159
169
|
- Every control has an accessible name and a visible focus ring (`--vf-focus`), `unstyled`
|
|
@@ -215,10 +225,12 @@ These are the default texts. Each one can be replaced through `strings` (`docs/A
|
|
|
215
225
|
| pen / box / arrow / text / select | Draw / Box / Arrow / Text / Select |
|
|
216
226
|
| remove | Delete |
|
|
217
227
|
| undo / clear | Undo / Clear |
|
|
228
|
+
| clearConfirm | Clear all marks on this screenshot? |
|
|
218
229
|
| cancel / send | Cancel / Send |
|
|
219
230
|
| sending | Sending… |
|
|
220
231
|
| sent | Thanks, it's sent. |
|
|
221
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. |
|
|
222
234
|
| retry | Try again |
|
|
223
235
|
| triggerText | none: the icon alone |
|
|
224
236
|
| escHint | Esc keeps it as a draft |
|
package/docs/THEMING.md
CHANGED
|
@@ -68,8 +68,8 @@ Every visible element has a part name: `trigger`, `trigger-icon`, `trigger-text`
|
|
|
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