visual-feedback 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,485 @@
1
+
2
+ import { n as shots, t as look } from "./seen-BpIWldOP.mjs";
3
+ //#region src/annotate.ts
4
+ function context(canvas) {
5
+ const result = canvas.getContext("2d");
6
+ if (!result) throw new Error("no 2d context");
7
+ return result;
8
+ }
9
+ function arrowHead(target, [x1, y1], [x2, y2]) {
10
+ const angle = Math.atan2(y2 - y1, x2 - x1);
11
+ const size = target.lineWidth * 4;
12
+ for (const side of [-1, 1]) {
13
+ target.moveTo(x2, y2);
14
+ target.lineTo(x2 - size * Math.cos(angle + side * Math.PI / 6), y2 - size * Math.sin(angle + side * Math.PI / 6));
15
+ }
16
+ }
17
+ function stroke(target, mark) {
18
+ target.beginPath();
19
+ if (mark.tool === "pen") for (const [i, [x, y]] of mark.points.entries()) if (i === 0) target.moveTo(x, y);
20
+ else target.lineTo(x, y);
21
+ else if (mark.tool === "box") target.rect(mark.x, mark.y, mark.width, mark.height);
22
+ else {
23
+ target.moveTo(...mark.from);
24
+ target.lineTo(...mark.to);
25
+ arrowHead(target, mark.from, mark.to);
26
+ }
27
+ target.stroke();
28
+ }
29
+ function drawn(mark) {
30
+ if (mark.tool === "pen") return mark.points.length > 1;
31
+ if (mark.tool === "box") return mark.width > 0 || mark.height > 0;
32
+ return mark.from[0] !== mark.to[0] || mark.from[1] !== mark.to[1];
33
+ }
34
+ /** Draws on `canvas` in image pixels, and repaints it from a clean copy on every change. */
35
+ function annotate(canvas, onChange) {
36
+ const clean = document.createElement("canvas");
37
+ clean.width = canvas.width;
38
+ clean.height = canvas.height;
39
+ context(clean).drawImage(canvas, 0, 0);
40
+ const target = context(canvas);
41
+ const marks = [];
42
+ let current = null;
43
+ function paint() {
44
+ target.setTransform(1, 0, 0, 1, 0, 0);
45
+ target.drawImage(clean, 0, 0);
46
+ target.strokeStyle = getComputedStyle(canvas).color;
47
+ target.lineWidth = Math.max(4, Math.round(canvas.width / 240));
48
+ target.lineCap = "round";
49
+ target.lineJoin = "round";
50
+ for (const mark of current ? [...marks, current.mark] : marks) stroke(target, mark);
51
+ }
52
+ function point(event) {
53
+ const box = canvas.getBoundingClientRect();
54
+ const x = (event.clientX - box.left - canvas.clientLeft) * canvas.width / canvas.clientWidth;
55
+ const y = (event.clientY - box.top - canvas.clientTop) * canvas.height / canvas.clientHeight;
56
+ return [Math.round(Math.min(canvas.width, Math.max(0, x))), Math.round(Math.min(canvas.height, Math.max(0, y)))];
57
+ }
58
+ function shape(tool, start, end) {
59
+ if (tool === "pen") return {
60
+ tool,
61
+ points: [start]
62
+ };
63
+ if (tool === "arrow") return {
64
+ tool,
65
+ from: start,
66
+ to: end
67
+ };
68
+ return {
69
+ tool,
70
+ x: Math.min(start[0], end[0]),
71
+ y: Math.min(start[1], end[1]),
72
+ width: Math.abs(end[0] - start[0]),
73
+ height: Math.abs(end[1] - start[1])
74
+ };
75
+ }
76
+ canvas.addEventListener("pointerdown", (event) => {
77
+ if (current || !event.isPrimary || event.button !== 0) return;
78
+ event.preventDefault();
79
+ canvas.setPointerCapture(event.pointerId);
80
+ const start = point(event);
81
+ current = {
82
+ id: event.pointerId,
83
+ start,
84
+ mark: shape(annotator.tool, start, start)
85
+ };
86
+ });
87
+ function extend(event) {
88
+ if (!current) return;
89
+ const end = point(event);
90
+ const last = current.mark.tool === "pen" ? current.mark.points.at(-1) : void 0;
91
+ if (current.mark.tool === "pen") {
92
+ if (last?.[0] !== end[0] || last?.[1] !== end[1]) current.mark.points.push(end);
93
+ } else current.mark = shape(current.mark.tool, current.start, end);
94
+ }
95
+ function end() {
96
+ if (!current) return;
97
+ if (drawn(current.mark)) marks.push(current.mark);
98
+ current = null;
99
+ paint();
100
+ onChange();
101
+ }
102
+ canvas.addEventListener("pointermove", (event) => {
103
+ if (current?.id !== event.pointerId) return;
104
+ if (event.buttons === 0) return end();
105
+ extend(event);
106
+ paint();
107
+ });
108
+ canvas.addEventListener("pointerup", (event) => {
109
+ if (current?.id !== event.pointerId) return;
110
+ extend(event);
111
+ end();
112
+ });
113
+ for (const type of ["pointercancel", "lostpointercapture"]) canvas.addEventListener(type, (event) => {
114
+ if (current?.id !== event.pointerId) return;
115
+ current = null;
116
+ paint();
117
+ });
118
+ const annotator = {
119
+ tool: "pen",
120
+ marks,
121
+ end,
122
+ undo() {
123
+ marks.pop();
124
+ paint();
125
+ onChange();
126
+ },
127
+ clear() {
128
+ marks.length = 0;
129
+ paint();
130
+ onChange();
131
+ }
132
+ };
133
+ return annotator;
134
+ }
135
+ //#endregion
136
+ //#region src/dialog.ts
137
+ const LAYOUT = `.backdrop{position:fixed;inset:0;width:100%;height:100%;max-width:none;max-height:none;margin:0;padding:0;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% - 32px);display:grid;gap:8px}.dialog.wide{width:min(1100px,100% - 32px)}.image{display:block;max-width:100%;max-height:60vh;margin:0 auto;color:var(--_mark)}.toolbar+.image{touch-action:none}.toolbar{display:flex;flex-wrap:wrap;gap:8px}.note{box-sizing:border-box;width:100%;min-height:96px;resize:vertical}.actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px}.live{position:fixed;width:1px;height:1px;margin:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}`;
138
+ const THEME = `.backdrop{background:var(--_bd)}.dialog{background:var(--_s);color:var(--_t);border-radius:var(--_r);box-shadow:var(--_sh);padding:16px}.title{margin:0 0 4px;font-size:1.15em}.label{font-weight:600}.image{border:1px solid var(--_b);border-radius:calc(var(--_r) / 2)}.error{margin:0}.note,.category,.actions button,.toolbar button{font:inherit;color:inherit;background:var(--_s);border:1px solid var(--_b);border-radius:calc(var(--_r) / 2)}.note,.category{padding:8px}.note::placeholder{color:var(--_m)}.actions button,.toolbar button{padding:6px 14px;cursor:pointer}.toolbar button:disabled{opacity:.5;cursor:default}.toolbar [aria-pressed=true]{border-color:var(--_a);box-shadow:inset 0 0 0 1px var(--_a)}.toolbar+.image{cursor:crosshair}.actions .send{font-weight:600;background:var(--_a);color:var(--_at);border-color:var(--_a)}.actions .send:where([data-ink=dark]){color:#000}.actions [aria-disabled=true]{opacity:.6;cursor:progress}.note:disabled,.category:disabled{opacity:.6}.status{margin:0}`;
139
+ const ENGLISH = {
140
+ title: "Send feedback",
141
+ capturing: "Taking a screenshot…",
142
+ captureError: "Couldn't take a screenshot.",
143
+ sendWithout: "Send without screenshot",
144
+ noteLabel: "Your note",
145
+ notePlaceholder: "What happened, or what would you change?",
146
+ noteRequired: "Write a few words first.",
147
+ categoryLabel: "Type",
148
+ pen: "Draw",
149
+ box: "Box",
150
+ arrow: "Arrow",
151
+ undo: "Undo",
152
+ clear: "Clear",
153
+ cancel: "Cancel",
154
+ send: "Send",
155
+ sending: "Sending…",
156
+ sent: "Thanks, it's sent.",
157
+ error: "It didn't send. Your note is kept.",
158
+ retry: "Try again"
159
+ };
160
+ const texts = (strings = {}) => Object.assign({}, ENGLISH, ...Object.entries(strings).map(([key, text]) => text === void 0 ? {} : { [key]: text }));
161
+ /** Tells a screen reader the screenshot is being taken; returns what removes it. */
162
+ function announce(root, strings) {
163
+ const live = element("p", "live", { role: "status" });
164
+ root.append(live);
165
+ const frame = requestAnimationFrame(() => {
166
+ live.textContent = texts(strings).capturing;
167
+ });
168
+ return () => {
169
+ cancelAnimationFrame(frame);
170
+ live.remove();
171
+ };
172
+ }
173
+ function css(unstyled) {
174
+ return unstyled ? [LAYOUT] : [LAYOUT, THEME];
175
+ }
176
+ function element(tag, name, attributes = {}) {
177
+ const result = document.createElement(tag);
178
+ result.className = name;
179
+ result.setAttribute("part", name);
180
+ for (const [key, value] of Object.entries(attributes)) result.setAttribute(key, value);
181
+ return result;
182
+ }
183
+ function button(name, text, onClick) {
184
+ const result = element("button", name, { type: "button" });
185
+ result.textContent = text;
186
+ result.addEventListener("click", onClick);
187
+ return result;
188
+ }
189
+ function ink(button) {
190
+ const style = getComputedStyle(button);
191
+ if (style.getPropertyValue("--vf-accent-text").trim()) return;
192
+ let [r = 0, g = 0, b = 0] = /^rgba?\(([\d.]+), ([\d.]+), ([\d.]+)/.exec(style.backgroundColor)?.slice(1).map(Number) ?? [];
193
+ if (!/^rgb/.test(style.backgroundColor)) {
194
+ const pixel = document.createElement("canvas").getContext("2d");
195
+ if (!pixel) return;
196
+ pixel.fillStyle = style.backgroundColor;
197
+ pixel.fillRect(0, 0, 1, 1);
198
+ [r = 0, g = 0, b = 0] = pixel.getImageData(0, 0, 1, 1).data;
199
+ }
200
+ const linear = (c) => c / 255 <= .04045 ? c / 255 / 12.92 : ((c / 255 + .055) / 1.055) ** 2.4;
201
+ if ((.2126 * linear(r) + .7152 * linear(g) + .0722 * linear(b) + .05) ** 2 > .0525) button.dataset.ink = "dark";
202
+ }
203
+ const LOCK = Symbol.for("visual-feedback.scroll-lock");
204
+ function lock() {
205
+ const html = document.documentElement;
206
+ html[LOCK] ??= {
207
+ count: 0,
208
+ saved: [],
209
+ attribute: false
210
+ };
211
+ return html[LOCK];
212
+ }
213
+ function lockScroll() {
214
+ const state = lock();
215
+ if (state.count++ > 0) return;
216
+ const style = document.documentElement.style;
217
+ state.attribute = document.documentElement.hasAttribute("style");
218
+ state.saved = ["overflow-x", "overflow-y"].map((name) => [
219
+ name,
220
+ style.getPropertyValue(name),
221
+ style.getPropertyPriority(name)
222
+ ]);
223
+ style.setProperty("overflow", "hidden", "important");
224
+ }
225
+ function unlockScroll() {
226
+ const state = lock();
227
+ if (--state.count > 0) return;
228
+ const style = document.documentElement.style;
229
+ for (const [name, value, priority] of state.saved) if (value) style.setProperty(name, value, priority);
230
+ else style.removeProperty(name);
231
+ state.saved = [];
232
+ if (!state.attribute && !style.length) document.documentElement.removeAttribute("style");
233
+ }
234
+ const FOCUSABLE = "button,textarea,input,select,a[href],[tabindex]:not([tabindex='-1'])";
235
+ const NOTE_LIMIT = 2e3;
236
+ function mount(parent, handlers, settings = {}) {
237
+ const tools = settings.tools ?? [
238
+ "pen",
239
+ "box",
240
+ "arrow"
241
+ ];
242
+ const text = texts(settings.strings);
243
+ const backdrop = element("dialog", "backdrop", {
244
+ "aria-modal": "true",
245
+ "aria-labelledby": "vf-title"
246
+ });
247
+ const panel = element("div", "dialog");
248
+ const title = element("h2", "title", { id: "vf-title" });
249
+ title.textContent = text.title;
250
+ backdrop.append(panel);
251
+ let marks = () => [];
252
+ let locked = [];
253
+ let inert = [];
254
+ let primary;
255
+ let draft;
256
+ let actions;
257
+ let failure;
258
+ function toolbar(image) {
259
+ const choices = [...new Set(tools)].filter((tool) => [
260
+ "pen",
261
+ "box",
262
+ "arrow"
263
+ ].includes(tool));
264
+ if (choices.length === 0) return [];
265
+ const bar = element("div", "toolbar");
266
+ const undo = button("undo", text.undo, () => annotator.undo());
267
+ const clear = button("clear", text.clear, () => annotator.clear());
268
+ const update = () => {
269
+ const none = annotator.marks.length === 0;
270
+ const root = parent.getRootNode();
271
+ if (none && (root.activeElement === undo || root.activeElement === clear)) buttons.find((choice) => choice.getAttribute("aria-pressed") === "true")?.focus();
272
+ undo.disabled = clear.disabled = none;
273
+ };
274
+ const annotator = annotate(image, update);
275
+ const pick = (tool) => {
276
+ annotator.tool = tool;
277
+ for (const [i, choice] of buttons.entries()) {
278
+ const active = choices[i] === tool;
279
+ choice.setAttribute("aria-pressed", String(active));
280
+ choice.setAttribute("part", active ? "tool tool-active" : "tool");
281
+ }
282
+ };
283
+ const buttons = choices.map((tool) => button("tool", text[tool], () => pick(tool)));
284
+ pick(choices[0]);
285
+ update();
286
+ marks = () => {
287
+ annotator.end();
288
+ return [...annotator.marks];
289
+ };
290
+ bar.append(...buttons, undo, clear);
291
+ locked.push(...buttons, undo, clear);
292
+ return [bar];
293
+ }
294
+ let current = null;
295
+ let seen;
296
+ let report;
297
+ let mounted = true;
298
+ async function deliver(draft) {
299
+ sending();
300
+ const options = handlers.options();
301
+ let module;
302
+ try {
303
+ module = await import("./report-BRZX6sWL.mjs");
304
+ if (!report) {
305
+ const capture = await import("./capture-CufRosj3.mjs");
306
+ const image = current ? await capture.encode(current, options.capture) : null;
307
+ const built = await module.build(draft, image, seen, options);
308
+ if (mounted) report = built;
309
+ }
310
+ if (!mounted || !report) return;
311
+ handlers.emit("submit", report);
312
+ await module.deliver(report, options);
313
+ handlers.emit("sent", report);
314
+ if (mounted) sent();
315
+ } catch (error) {
316
+ handlers.emit("error", error);
317
+ if (mounted) failed(module?.shown(error));
318
+ }
319
+ }
320
+ function show(view) {
321
+ current = view.error ? null : view.image;
322
+ marks = () => [];
323
+ locked = [];
324
+ inert = [];
325
+ primary = void 0;
326
+ draft = void 0;
327
+ failure = void 0;
328
+ actions = element("div", "actions");
329
+ const cancel = button("cancel", text.cancel, handlers.close);
330
+ if (view.error) {
331
+ const message = element("p", "error", { role: "alert" });
332
+ message.textContent = text.captureError;
333
+ const retry = button("retry", text.retry, handlers.retry);
334
+ actions.append(cancel, button("send-without", text.sendWithout, () => show({ image: null })), retry);
335
+ panel.classList.remove("wide");
336
+ panel.replaceChildren(title, message, actions);
337
+ retry.focus();
338
+ return;
339
+ }
340
+ const label = element("label", "label", { for: "vf-note" });
341
+ label.textContent = text.noteLabel;
342
+ const note = element("textarea", "note", {
343
+ id: "vf-note",
344
+ placeholder: text.notePlaceholder,
345
+ maxlength: String(NOTE_LIMIT)
346
+ });
347
+ const required = element("p", "error", {
348
+ id: "vf-note-error",
349
+ role: "alert"
350
+ });
351
+ required.textContent = text.noteRequired;
352
+ note.addEventListener("input", () => {
353
+ if (!note.value.trim()) return;
354
+ note.removeAttribute("aria-invalid");
355
+ note.removeAttribute("aria-describedby");
356
+ required.remove();
357
+ });
358
+ const fields = [label, note];
359
+ let category;
360
+ if (settings.categories?.length) {
361
+ const name = element("label", "label", { for: "vf-category" });
362
+ name.textContent = text.categoryLabel;
363
+ category = element("select", "category", { id: "vf-category" });
364
+ for (const choice of settings.categories) {
365
+ const option = document.createElement("option");
366
+ option.value = choice.id;
367
+ option.textContent = choice.label;
368
+ category.append(option);
369
+ }
370
+ fields.push(name, category);
371
+ }
372
+ const image = view.image;
373
+ if (image) {
374
+ image.className = "image";
375
+ image.setAttribute("part", "image");
376
+ image.removeAttribute("style");
377
+ inert.push(image);
378
+ }
379
+ const drawing = image ? [...toolbar(image), image] : [];
380
+ const send = button("send", text.send, () => {
381
+ if (draft) {
382
+ if (send.getAttribute("aria-disabled") !== "true") deliver(draft);
383
+ return;
384
+ }
385
+ if (settings.requireNote !== false && !note.value.trim()) {
386
+ note.setAttribute("aria-invalid", "true");
387
+ note.setAttribute("aria-describedby", required.id);
388
+ note.after(required);
389
+ note.focus();
390
+ return;
391
+ }
392
+ draft = {
393
+ note: note.value,
394
+ category: category?.value,
395
+ marks: marks()
396
+ };
397
+ deliver(draft);
398
+ });
399
+ primary = send;
400
+ locked.push(note, ...category ? [category] : []);
401
+ actions.append(cancel, send);
402
+ panel.classList.toggle("wide", Boolean(image));
403
+ panel.replaceChildren(title, ...drawing, ...fields, actions);
404
+ ink(send);
405
+ note.focus();
406
+ }
407
+ function sending() {
408
+ if (!primary) return;
409
+ for (const control of locked) control.disabled = true;
410
+ for (const item of inert) item.inert = true;
411
+ failure?.remove();
412
+ primary.setAttribute("aria-disabled", "true");
413
+ primary.textContent = text.sending;
414
+ primary.setAttribute("part", "send");
415
+ primary.focus();
416
+ }
417
+ function sent() {
418
+ const status = element("p", "status", {
419
+ role: "status",
420
+ tabindex: "-1"
421
+ });
422
+ status.textContent = text.sent;
423
+ panel.classList.remove("wide");
424
+ panel.replaceChildren(title, status);
425
+ status.focus();
426
+ done = setTimeout(handlers.close, 3e3);
427
+ }
428
+ function failed(message = text.error) {
429
+ if (!primary || !actions) return;
430
+ failure = element("p", "error", { role: "alert" });
431
+ failure.textContent = message;
432
+ actions.before(failure);
433
+ primary.removeAttribute("aria-disabled");
434
+ primary.textContent = text.retry;
435
+ primary.setAttribute("part", "retry");
436
+ primary.focus();
437
+ }
438
+ backdrop.addEventListener("cancel", (event) => {
439
+ event.preventDefault();
440
+ handlers.close();
441
+ });
442
+ backdrop.addEventListener("close", handlers.close);
443
+ backdrop.addEventListener("keydown", (event) => {
444
+ if (event.key !== "Tab") return;
445
+ event.preventDefault();
446
+ const items = [...panel.querySelectorAll(FOCUSABLE)].filter((item) => !item.matches(":disabled") && item.getClientRects().length > 0);
447
+ const active = parent.getRootNode().activeElement;
448
+ const index = active ? items.indexOf(active) : -1;
449
+ const step = event.shiftKey ? -1 : 1;
450
+ const next = index < 0 ? event.shiftKey ? items.length - 1 : 0 : index + step;
451
+ items.at(next % items.length)?.focus();
452
+ });
453
+ parent.append(backdrop);
454
+ try {
455
+ backdrop.showModal();
456
+ } catch (error) {
457
+ backdrop.remove();
458
+ throw error;
459
+ }
460
+ let done;
461
+ let scrollLocked = false;
462
+ const lock = (on) => {
463
+ if (on !== scrollLocked) (on ? lockScroll : unlockScroll)();
464
+ scrollLocked = on;
465
+ };
466
+ return {
467
+ show,
468
+ capture(image) {
469
+ seen = (image && shots.get(image)) ?? look();
470
+ show(image ? { image } : { error: true });
471
+ lock(true);
472
+ },
473
+ lock,
474
+ unmount() {
475
+ mounted = false;
476
+ clearTimeout(done);
477
+ lock(false);
478
+ backdrop.removeEventListener("close", handlers.close);
479
+ if (backdrop.open) backdrop.close();
480
+ backdrop.remove();
481
+ }
482
+ };
483
+ }
484
+ //#endregion
485
+ export { announce, css, mount };
@@ -0,0 +1,4 @@
1
+
2
+ import { a as FeedbackWidget, i as FeedbackStrings, n as FeedbackEvents, o as createFeedback, r as FeedbackOptions, s as CaptureOptions, t as FeedbackEvent } from "./widget-Ca1heF8n.mjs";
3
+ import { n as Mark, t as FeedbackReport } from "./report-CIgy7uzP.mjs";
4
+ export { type CaptureOptions, type FeedbackEvent, type FeedbackEvents, type FeedbackOptions, type FeedbackReport, type FeedbackStrings, type FeedbackWidget, type Mark, createFeedback };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { t as createFeedback } from "./widget-CVnusNlY.mjs";
@@ -0,0 +1,6 @@
1
+
2
+ import { r as FeedbackOptions } from "./widget-Ca1heF8n.mjs";
3
+ //#region src/react/index.d.ts
4
+ export type FeedbackProps = FeedbackOptions;
5
+ export declare function Feedback(props: FeedbackProps): null;
6
+ //#endregion
package/dist/react.mjs ADDED
@@ -0,0 +1,29 @@
1
+ "use client";
2
+ import { t as createFeedback } from "./widget-CVnusNlY.mjs";
3
+ import { useEffect, useRef } from "react";
4
+ //#region src/react/index.ts
5
+ function Feedback(props) {
6
+ const widget = useRef(null);
7
+ const shown = useRef(props);
8
+ useEffect(() => {
9
+ const created = createFeedback(shown.current);
10
+ widget.current = created;
11
+ return () => {
12
+ created.destroy();
13
+ widget.current = null;
14
+ };
15
+ }, []);
16
+ useEffect(() => {
17
+ const before = shown.current;
18
+ if (!widget.current || before === props) return;
19
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(props)]);
20
+ if ([...keys].every((key) => Object.is(before[key], props[key]))) return;
21
+ const next = { ...props };
22
+ for (const key of keys) if (!(key in props)) next[key] = void 0;
23
+ widget.current.update(next);
24
+ shown.current = props;
25
+ });
26
+ return null;
27
+ }
28
+ //#endregion
29
+ export { Feedback };
@@ -0,0 +1,83 @@
1
+
2
+ import { t as checkReport } from "./report-BxonlIdW.mjs";
3
+ //#region src/report.ts
4
+ function uuid() {
5
+ const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
6
+ bytes[6] = (bytes[6] ?? 0) & 15 | 64;
7
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
8
+ const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
9
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
10
+ }
11
+ function pageUrl(href, redact) {
12
+ if (!redact) return href;
13
+ const url = new URL(href);
14
+ url.search = "";
15
+ url.hash = "";
16
+ return url.href;
17
+ }
18
+ let fromHost;
19
+ async function host(call) {
20
+ try {
21
+ return await call();
22
+ } catch (error) {
23
+ fromHost = error;
24
+ throw error;
25
+ }
26
+ }
27
+ /** The host's own error message, the only one the user is shown. */
28
+ function shown(error) {
29
+ if (error !== fromHost) return void 0;
30
+ const message = typeof error === "string" ? error : error?.message;
31
+ return typeof message === "string" && message ? message : void 0;
32
+ }
33
+ /** Builds the report and checks it with the same shape check the server uses. */
34
+ async function build(draft, image, seen, settings) {
35
+ const context = await host(() => settings.context?.()) ?? {};
36
+ const report = {
37
+ id: uuid(),
38
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
39
+ note: draft.note,
40
+ ...draft.category === void 0 ? {} : { category: draft.category },
41
+ marks: image ? draft.marks : [],
42
+ page: {
43
+ url: pageUrl(seen.url, Boolean(settings.redactUrl)),
44
+ title: seen.title
45
+ },
46
+ viewport: seen.viewport,
47
+ scroll: seen.scroll,
48
+ userAgent: navigator.userAgent,
49
+ language: navigator.language,
50
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
51
+ context
52
+ };
53
+ return {
54
+ ...checkReport(report),
55
+ image
56
+ };
57
+ }
58
+ /** Sends with onSubmit, or else POSTs what parseFeedbackRequest reads to the endpoint. */
59
+ async function deliver(report, settings) {
60
+ const { onSubmit, endpoint } = settings;
61
+ if (onSubmit) return host(() => onSubmit(report));
62
+ if (!endpoint) throw new Error("Neither onSubmit nor endpoint is set.");
63
+ const body = new FormData();
64
+ body.append("report", JSON.stringify({
65
+ ...report,
66
+ image: void 0
67
+ }));
68
+ if (report.image) {
69
+ const extension = report.image.type === "image/png" ? "png" : "webp";
70
+ body.append("image", report.image, `${report.id}.${extension}`);
71
+ }
72
+ const { headers: given } = settings;
73
+ const headers = typeof given === "function" ? await host(given) : given ?? {};
74
+ const response = await fetch(endpoint, {
75
+ method: "POST",
76
+ body,
77
+ headers,
78
+ redirect: "manual"
79
+ });
80
+ if (!response.ok) throw new Error(`The endpoint answered ${response.status}.`);
81
+ }
82
+ //#endregion
83
+ export { build, deliver, shown };