visual-feedback 0.1.0 → 0.2.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,1374 @@
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 distance([px, py], [ax, ay], [bx, by]) {
30
+ const dx = bx - ax;
31
+ const dy = by - ay;
32
+ const t = dx || dy ? ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy) : 0;
33
+ const s = Math.max(0, Math.min(1, t));
34
+ return Math.hypot(px - (ax + s * dx), py - (ay + s * dy));
35
+ }
36
+ function moved(mark, dx, dy) {
37
+ const by = ([x, y]) => [x + dx, y + dy];
38
+ if (mark.tool === "pen") return {
39
+ ...mark,
40
+ points: mark.points.map(by)
41
+ };
42
+ if (mark.tool === "arrow") return {
43
+ ...mark,
44
+ from: by(mark.from),
45
+ to: by(mark.to)
46
+ };
47
+ return {
48
+ ...mark,
49
+ x: mark.x + dx,
50
+ y: mark.y + dy
51
+ };
52
+ }
53
+ function drawn(mark) {
54
+ if (mark.tool === "text") return mark.text.trim() !== "";
55
+ if (mark.tool === "pen") return mark.points.length > 1;
56
+ if (mark.tool === "box") return mark.width > 0 || mark.height > 0;
57
+ return mark.from[0] !== mark.to[0] || mark.from[1] !== mark.to[1];
58
+ }
59
+ /**
60
+ * Douglas-Peucker: a slow drag records a point for each pixel moved. A point within a pixel of the
61
+ * line its neighbours draw changes nothing a 4 px stroke shows, and only grows the report.
62
+ */
63
+ function simplify(points, tolerance = 1) {
64
+ if (points.length < 3) return points;
65
+ const keep = new Uint8Array(points.length);
66
+ keep[0] = 1;
67
+ keep[points.length - 1] = 1;
68
+ const spans = [[0, points.length - 1]];
69
+ for (let span = spans.pop(); span; span = spans.pop()) {
70
+ const [from, to] = span;
71
+ const a = points[from];
72
+ const b = points[to];
73
+ let far = -1;
74
+ let most = tolerance;
75
+ for (let i = from + 1; i < to; i++) {
76
+ const off = distance(points[i], a, b);
77
+ if (off > most) {
78
+ most = off;
79
+ far = i;
80
+ }
81
+ }
82
+ if (far === -1) continue;
83
+ keep[far] = 1;
84
+ spans.push([from, far], [far, to]);
85
+ }
86
+ return points.filter((_, i) => keep[i]);
87
+ }
88
+ /** Draws on `canvas` in image pixels, and repaints it from a clean copy on every change. */
89
+ function annotate(canvas, onChange, textLabel, kept = []) {
90
+ const clean = document.createElement("canvas");
91
+ clean.width = canvas.width;
92
+ clean.height = canvas.height;
93
+ context(clean).drawImage(canvas, 0, 0);
94
+ const target = context(canvas);
95
+ const marks = [...kept];
96
+ const history = [];
97
+ let tool = "pen";
98
+ let selected = null;
99
+ let dragging = null;
100
+ let hovered = null;
101
+ let frame = null;
102
+ let current = null;
103
+ let editing = null;
104
+ const size = Math.max(16, Math.round(canvas.width / 60));
105
+ const outline = Math.max(3, size / 5);
106
+ function paint() {
107
+ target.setTransform(1, 0, 0, 1, 0, 0);
108
+ target.drawImage(clean, 0, 0);
109
+ target.strokeStyle = getComputedStyle(canvas).color;
110
+ target.lineWidth = Math.max(4, Math.round(canvas.width / 240));
111
+ target.lineCap = "round";
112
+ target.lineJoin = "round";
113
+ outlineSelection();
114
+ hover();
115
+ for (const mark of current ? [...marks, current.mark] : marks) {
116
+ if (mark.tool !== "text") {
117
+ stroke(target, mark);
118
+ continue;
119
+ }
120
+ target.save();
121
+ target.font = font();
122
+ target.textBaseline = "middle";
123
+ target.lineWidth = outline;
124
+ target.strokeStyle = "#fff";
125
+ const room = canvas.width - mark.x;
126
+ target.strokeText(mark.text, mark.x, mark.y, room);
127
+ target.fillStyle = getComputedStyle(canvas).color;
128
+ target.fillText(mark.text, mark.x, mark.y, room);
129
+ target.restore();
130
+ }
131
+ }
132
+ /** The box a mark covers in image pixels, its line and outline included. */
133
+ function bounds(mark) {
134
+ if (mark.tool === "box") {
135
+ const pad = target.lineWidth / 2;
136
+ return {
137
+ x: mark.x - pad,
138
+ y: mark.y - pad,
139
+ width: mark.width + 2 * pad,
140
+ height: mark.height + 2 * pad
141
+ };
142
+ }
143
+ if (mark.tool === "text") {
144
+ target.save();
145
+ target.font = font();
146
+ const width = target.measureText(mark.text).width;
147
+ target.restore();
148
+ const half = size / 2 + outline;
149
+ return {
150
+ x: mark.x - outline,
151
+ y: mark.y - half,
152
+ width: width + 2 * outline,
153
+ height: 2 * half
154
+ };
155
+ }
156
+ const points = mark.tool === "pen" ? mark.points : [mark.from, mark.to];
157
+ const pad = target.lineWidth * (mark.tool === "arrow" ? 4 : .5);
158
+ const xs = points.map(([x]) => x);
159
+ const ys = points.map(([, y]) => y);
160
+ const x = Math.min(...xs) - pad;
161
+ const y = Math.min(...ys) - pad;
162
+ return {
163
+ x,
164
+ y,
165
+ width: Math.max(...xs) + pad - x,
166
+ height: Math.max(...ys) + pad - y
167
+ };
168
+ }
169
+ /** The newest mark under a point, near its line, or inside a label. */
170
+ function hit(at) {
171
+ const near = Math.max(target.lineWidth * 2, 10 * canvas.width / canvas.clientWidth);
172
+ for (let index = marks.length - 1; index >= 0; index--) {
173
+ const mark = marks[index];
174
+ if (mark.tool === "text") {
175
+ const box = bounds(mark);
176
+ if (at[0] >= box.x && at[0] <= box.x + box.width && at[1] >= box.y && at[1] <= box.y + box.height) return index;
177
+ continue;
178
+ }
179
+ const corners = mark.tool === "box" ? [
180
+ [mark.x, mark.y],
181
+ [mark.x + mark.width, mark.y],
182
+ [mark.x + mark.width, mark.y + mark.height],
183
+ [mark.x, mark.y + mark.height],
184
+ [mark.x, mark.y]
185
+ ] : mark.tool === "arrow" ? [mark.from, mark.to] : mark.points;
186
+ if ((corners.length > 1 ? corners.slice(1) : corners).some((to, i) => distance(at, corners[i], to) <= near)) return index;
187
+ }
188
+ return null;
189
+ }
190
+ /** The mark moved by up to dx and dy; it never goes further out, and is never pushed in. */
191
+ function within(mark, dx, dy) {
192
+ const box = bounds(mark);
193
+ const limit = (by, start, size, room) => Math.max(Math.ceil(Math.min(0, -start)), Math.min(Math.round(by), Math.floor(Math.max(0, room - start - size))));
194
+ const x = limit(dx, box.x, box.width, canvas.width);
195
+ const y = limit(dy, box.y, box.height, canvas.height);
196
+ return x || y ? moved(mark, x, y) : mark;
197
+ }
198
+ function outlineSelection() {
199
+ const mark = selected === null ? void 0 : marks[selected];
200
+ if (!mark) {
201
+ frame?.remove();
202
+ frame = null;
203
+ return;
204
+ }
205
+ if (!frame) {
206
+ frame = document.createElement("div");
207
+ frame.className = "selection";
208
+ frame.setAttribute("part", "selection");
209
+ canvas.after(frame);
210
+ }
211
+ const box = bounds(mark);
212
+ const scale = canvas.clientWidth / canvas.width;
213
+ const gap = 4;
214
+ frame.style.left = `${canvas.offsetLeft + canvas.clientLeft + box.x * scale - gap}px`;
215
+ frame.style.top = `${canvas.offsetTop + canvas.clientTop + box.y * scale - gap}px`;
216
+ frame.style.width = `${box.width * scale + 8}px`;
217
+ frame.style.height = `${box.height * scale + 8}px`;
218
+ }
219
+ function select(index) {
220
+ if (index === selected) return;
221
+ finishDrag();
222
+ selected = index;
223
+ paint();
224
+ onChange();
225
+ }
226
+ function hover() {
227
+ if (tool !== "select" || dragging) return;
228
+ canvas.style.cursor = hovered !== null && hit(hovered) !== null ? "grab" : "default";
229
+ }
230
+ function finishDrag() {
231
+ const before = dragging?.mark;
232
+ dragging = null;
233
+ if (before) hover();
234
+ if (!before || selected === null) return;
235
+ if (marks[selected] !== before) history.push({
236
+ kind: "move",
237
+ index: selected,
238
+ mark: before
239
+ });
240
+ onChange();
241
+ }
242
+ function nudge(dx, dy) {
243
+ if (selected === null) return;
244
+ finishDrag();
245
+ const before = marks[selected];
246
+ const after = within(before, dx, dy);
247
+ if (after === before) return;
248
+ marks[selected] = after;
249
+ history.push({
250
+ kind: "move",
251
+ index: selected,
252
+ mark: before
253
+ });
254
+ paint();
255
+ onChange();
256
+ }
257
+ function font() {
258
+ return `600 ${size}px ${getComputedStyle(canvas).fontFamily}`;
259
+ }
260
+ function write(keep) {
261
+ if (!editing) return;
262
+ const { input, at } = editing;
263
+ editing = null;
264
+ input.remove();
265
+ target.font = font();
266
+ const room = canvas.width - target.measureText(input.value).width - outline;
267
+ const x = Math.round(Math.max(outline, Math.min(at[0], room)));
268
+ const half = size / 2 + outline;
269
+ const mark = {
270
+ tool: "text",
271
+ x,
272
+ y: Math.round(Math.max(half, Math.min(at[1], canvas.height - half))),
273
+ text: input.value
274
+ };
275
+ if (!keep || !drawn(mark)) return;
276
+ marks.push(mark);
277
+ history.push({ kind: "add" });
278
+ paint();
279
+ onChange();
280
+ }
281
+ function place(at) {
282
+ const input = document.createElement("input");
283
+ input.className = "text-input";
284
+ input.setAttribute("part", "text-input");
285
+ input.setAttribute("aria-label", textLabel);
286
+ input.maxLength = 280;
287
+ const scale = canvas.clientWidth / canvas.width;
288
+ input.style.top = `${canvas.offsetTop + canvas.clientTop + at[1] * scale}px`;
289
+ input.style.fontSize = `${size * scale}px`;
290
+ input.addEventListener("keydown", (event) => {
291
+ if (event.key !== "Enter" && event.key !== "Escape") return;
292
+ if (event.isComposing || event.keyCode === 229) return;
293
+ event.preventDefault();
294
+ event.stopPropagation();
295
+ write(event.key === "Enter");
296
+ });
297
+ input.addEventListener("blur", () => write(true));
298
+ canvas.after(input);
299
+ const inset = input.clientLeft + Number.parseFloat(getComputedStyle(input).paddingLeft);
300
+ input.style.left = `${canvas.offsetLeft + canvas.clientLeft + at[0] * scale - inset}px`;
301
+ editing = {
302
+ input,
303
+ at
304
+ };
305
+ input.focus();
306
+ }
307
+ function point(event) {
308
+ const box = canvas.getBoundingClientRect();
309
+ const x = (event.clientX - box.left - canvas.clientLeft) * canvas.width / canvas.clientWidth;
310
+ const y = (event.clientY - box.top - canvas.clientTop) * canvas.height / canvas.clientHeight;
311
+ return [Math.round(Math.min(canvas.width, Math.max(0, x))), Math.round(Math.min(canvas.height, Math.max(0, y)))];
312
+ }
313
+ function shape(tool, start, end) {
314
+ if (tool === "pen") return {
315
+ tool,
316
+ points: [start]
317
+ };
318
+ if (tool === "arrow") return {
319
+ tool,
320
+ from: start,
321
+ to: end
322
+ };
323
+ return {
324
+ tool,
325
+ x: Math.min(start[0], end[0]),
326
+ y: Math.min(start[1], end[1]),
327
+ width: Math.abs(end[0] - start[0]),
328
+ height: Math.abs(end[1] - start[1])
329
+ };
330
+ }
331
+ canvas.addEventListener("pointerdown", (event) => {
332
+ if (current || !event.isPrimary || event.button !== 0) return;
333
+ event.preventDefault();
334
+ const start = point(event);
335
+ if (tool === "select") {
336
+ canvas.focus({ preventScroll: true });
337
+ const index = hit(start);
338
+ select(index);
339
+ if (index === null) return;
340
+ canvas.setPointerCapture(event.pointerId);
341
+ dragging = {
342
+ id: event.pointerId,
343
+ start,
344
+ mark: marks[index]
345
+ };
346
+ canvas.style.cursor = "grabbing";
347
+ return;
348
+ }
349
+ if (tool === "text") {
350
+ write(true);
351
+ place(start);
352
+ return;
353
+ }
354
+ canvas.setPointerCapture(event.pointerId);
355
+ current = {
356
+ id: event.pointerId,
357
+ start,
358
+ mark: shape(tool, start, start)
359
+ };
360
+ });
361
+ function extend(event) {
362
+ if (!current) return;
363
+ const end = point(event);
364
+ const last = current.mark.tool === "pen" ? current.mark.points.at(-1) : void 0;
365
+ if (current.mark.tool === "pen") {
366
+ if (last?.[0] !== end[0] || last?.[1] !== end[1]) current.mark.points.push(end);
367
+ } else current.mark = shape(current.mark.tool, current.start, end);
368
+ }
369
+ function end() {
370
+ write(true);
371
+ finishDrag();
372
+ select(null);
373
+ if (!current) return;
374
+ if (current.mark.tool === "pen") current.mark.points = simplify(current.mark.points);
375
+ if (drawn(current.mark)) {
376
+ marks.push(current.mark);
377
+ history.push({ kind: "add" });
378
+ }
379
+ current = null;
380
+ paint();
381
+ onChange();
382
+ }
383
+ canvas.addEventListener("pointermove", (event) => {
384
+ hovered = point(event);
385
+ hover();
386
+ if (dragging?.id === event.pointerId && selected !== null) {
387
+ if (event.buttons === 0) return finishDrag();
388
+ const [x, y] = point(event);
389
+ marks[selected] = within(dragging.mark, x - dragging.start[0], y - dragging.start[1]);
390
+ paint();
391
+ return;
392
+ }
393
+ if (current?.id !== event.pointerId) return;
394
+ if (event.buttons === 0) return end();
395
+ extend(event);
396
+ paint();
397
+ });
398
+ canvas.addEventListener("pointerup", (event) => {
399
+ if (dragging?.id === event.pointerId) return finishDrag();
400
+ if (current?.id !== event.pointerId) return;
401
+ extend(event);
402
+ end();
403
+ });
404
+ canvas.addEventListener("pointerleave", () => {
405
+ hovered = null;
406
+ });
407
+ for (const type of ["pointercancel", "lostpointercapture"]) canvas.addEventListener(type, (event) => {
408
+ if (dragging?.id === event.pointerId) finishDrag();
409
+ else if (current?.id === event.pointerId) end();
410
+ });
411
+ canvas.addEventListener("keydown", (event) => {
412
+ if (selected === null) return;
413
+ const step = Math.max(1, Math.round(canvas.width / canvas.clientWidth)) * (event.shiftKey ? 10 : 1);
414
+ const by = {
415
+ ArrowLeft: [-step, 0],
416
+ ArrowRight: [step, 0],
417
+ ArrowUp: [0, -step],
418
+ ArrowDown: [0, step]
419
+ }[event.key];
420
+ if (by) nudge(...by);
421
+ else if (event.key === "Delete" || event.key === "Backspace") annotator.remove();
422
+ else if (event.key === "Escape") select(null);
423
+ else return;
424
+ event.preventDefault();
425
+ event.stopPropagation();
426
+ });
427
+ const annotator = {
428
+ clean,
429
+ get tool() {
430
+ return tool;
431
+ },
432
+ set tool(next) {
433
+ tool = next;
434
+ if (next === "select") canvas.tabIndex = 0;
435
+ else canvas.removeAttribute("tabindex");
436
+ select(null);
437
+ if (next === "select") hover();
438
+ else canvas.style.cursor = "";
439
+ },
440
+ marks,
441
+ get canUndo() {
442
+ return history.length > 0;
443
+ },
444
+ get selected() {
445
+ return selected !== null;
446
+ },
447
+ end,
448
+ undo() {
449
+ finishDrag();
450
+ const step = history.pop();
451
+ if (!step) return;
452
+ if (step.kind === "add") marks.pop();
453
+ else if (step.kind === "move") marks[step.index] = step.mark;
454
+ else marks.splice(step.index, 0, step.mark);
455
+ selected = null;
456
+ paint();
457
+ onChange();
458
+ },
459
+ clear() {
460
+ finishDrag();
461
+ marks.length = 0;
462
+ history.length = 0;
463
+ selected = null;
464
+ paint();
465
+ onChange();
466
+ },
467
+ remove() {
468
+ finishDrag();
469
+ if (selected === null) return;
470
+ const [mark] = marks.splice(selected, 1);
471
+ if (mark) history.push({
472
+ kind: "remove",
473
+ index: selected,
474
+ mark
475
+ });
476
+ selected = null;
477
+ paint();
478
+ onChange();
479
+ }
480
+ };
481
+ if (marks.length) paint();
482
+ return annotator;
483
+ }
484
+ //#endregion
485
+ //#region src/icon.ts
486
+ const SVG = "http://www.w3.org/2000/svg";
487
+ /** A line icon in a 24 by 24 box, hidden from screen readers. */
488
+ function icon(d, part) {
489
+ const svg = document.createElementNS(SVG, "svg");
490
+ const path = document.createElementNS(SVG, "path");
491
+ const attributes = {
492
+ viewBox: "0 0 24 24",
493
+ "aria-hidden": "true",
494
+ fill: "none",
495
+ stroke: "currentColor",
496
+ "stroke-width": "2",
497
+ "stroke-linejoin": "round",
498
+ part
499
+ };
500
+ for (const [name, value] of Object.entries(attributes)) svg.setAttribute(name, value);
501
+ path.setAttribute("d", d);
502
+ svg.append(path);
503
+ return svg;
504
+ }
505
+ //#endregion
506
+ //#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: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:12px}.dialog.wide{width:min(1100px,100% - 32px)}.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;max-width:100%;max-height:min(60vh,100vh - 23em);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}.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}`;
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}`;
509
+ const ICONS = {
510
+ pen: "M4 20h4L19 9a2.8 2.8 0 0 0-4-4L4 16v4zm9.5-13.5 4 4",
511
+ 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",
512
+ arrow: "M5 19 19 5M9 5h10v10",
513
+ text: "M5 7V5h14v2M12 5v14m-3 0h6",
514
+ select: "M6 3l12 7-5.5 1.5L10 17z",
515
+ remove: "M5 5h14v14H5zm4 4 6 6m0-6-6 6",
516
+ undo: "M9 14 4 9l5-5M4 9h10.5a5.5 5.5 0 0 1 0 11H11",
517
+ clear: "M4 7h16m-10 4v6m4-6v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3",
518
+ cancel: "M6 6l12 12M18 6 6 18",
519
+ send: "M5 12h13m-6-7 7 7-7 7",
520
+ minimise: "M5 17h14",
521
+ add: "M12 5v14M5 12h14",
522
+ camera: "M4 8h3l2-3h6l2 3h3v11H4zm8 8.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z",
523
+ more: "m7 10 5 5 5-5",
524
+ pdf: "M7 3h7l5 5v13H7zM14 3v5h5M10 13h6m-6 4h6"
525
+ };
526
+ const ENGLISH = {
527
+ title: "Send feedback",
528
+ capturing: "Taking a screenshot…",
529
+ captureError: "Couldn't take a screenshot.",
530
+ sendWithout: "Send without screenshot",
531
+ noteLabel: "Your note",
532
+ notePlaceholder: "What happened, or what would you change?",
533
+ noteRequired: "Write a few words first.",
534
+ categoryLabel: "Type",
535
+ pen: "Draw",
536
+ box: "Box",
537
+ arrow: "Arrow",
538
+ text: "Text",
539
+ select: "Select",
540
+ remove: "Delete",
541
+ undo: "Undo",
542
+ clear: "Clear",
543
+ cancel: "Cancel",
544
+ send: "Send",
545
+ sending: "Sending…",
546
+ sent: "Thanks, it's sent.",
547
+ error: "It didn't send. Your note is kept.",
548
+ retry: "Try again",
549
+ escHint: "Esc keeps it as a draft",
550
+ minimise: "Keep as draft",
551
+ screenshot: "Screenshot",
552
+ addScreenshot: "Add screenshot",
553
+ removeScreenshot: "Remove screenshot",
554
+ draft: "Feedback draft",
555
+ captureView: "Capture this view",
556
+ reopen: "Open",
557
+ discard: "Discard",
558
+ discardConfirm: "Discard this feedback?",
559
+ keepEditing: "Keep editing",
560
+ moreSend: "More send options",
561
+ savePdf: "Save as PDF",
562
+ pdfError: "Couldn't save the PDF."
563
+ };
564
+ const texts = (strings = {}) => Object.assign({}, ENGLISH, ...Object.entries(strings).map(([key, text]) => text === void 0 ? {} : { [key]: text }));
565
+ /** Tells a screen reader the screenshot is being taken; returns what removes it. */
566
+ function announce(root, strings) {
567
+ const live = element("p", "live", { role: "status" });
568
+ root.append(live);
569
+ const frame = requestAnimationFrame(() => {
570
+ live.textContent = texts(strings).capturing;
571
+ });
572
+ return () => {
573
+ cancelAnimationFrame(frame);
574
+ live.remove();
575
+ };
576
+ }
577
+ function css(unstyled) {
578
+ return unstyled ? [LAYOUT] : [LAYOUT, THEME];
579
+ }
580
+ function element(tag, name, attributes = {}) {
581
+ const result = document.createElement(tag);
582
+ result.className = name;
583
+ result.setAttribute("part", name);
584
+ for (const [key, value] of Object.entries(attributes)) result.setAttribute(key, value);
585
+ return result;
586
+ }
587
+ function button(name, text, onClick) {
588
+ const result = element("button", name, { type: "button" });
589
+ result.textContent = text;
590
+ result.addEventListener("click", onClick);
591
+ return result;
592
+ }
593
+ /** A button that shows only its icon; the text is its accessible name and its tooltip. */
594
+ function iconButton(name, text, d, onClick) {
595
+ const result = button(name, "", onClick);
596
+ result.setAttribute("aria-label", text);
597
+ result.title = text;
598
+ result.append(icon(d, "icon"));
599
+ return result;
600
+ }
601
+ function copy(small, image) {
602
+ let source = image;
603
+ while (source.width > small.width * 2) {
604
+ const half = document.createElement("canvas");
605
+ half.width = Math.ceil(source.width / 2);
606
+ half.height = Math.ceil(source.height / 2);
607
+ half.getContext("2d")?.drawImage(source, 0, 0, half.width, half.height);
608
+ source = half;
609
+ }
610
+ small.getContext("2d")?.drawImage(source, 0, 0, small.width, small.height);
611
+ }
612
+ /** A small copy of a screenshot, its marks included, for the strip and the draft bar. */
613
+ function thumbnail(image) {
614
+ const small = element("canvas", "thumb-image", { "aria-hidden": "true" });
615
+ small.height = 96;
616
+ small.width = Math.max(1, Math.round(96 * image.width / Math.max(1, image.height)));
617
+ copy(small, image);
618
+ return small;
619
+ }
620
+ function ink(button) {
621
+ const style = getComputedStyle(button);
622
+ if (style.getPropertyValue("--vf-accent-text").trim()) return;
623
+ let [r = 0, g = 0, b = 0] = /^rgba?\(([\d.]+), ([\d.]+), ([\d.]+)/.exec(style.backgroundColor)?.slice(1).map(Number) ?? [];
624
+ if (!/^rgb/.test(style.backgroundColor)) {
625
+ const pixel = document.createElement("canvas").getContext("2d");
626
+ if (!pixel) return;
627
+ pixel.fillStyle = style.backgroundColor;
628
+ pixel.fillRect(0, 0, 1, 1);
629
+ [r = 0, g = 0, b = 0] = pixel.getImageData(0, 0, 1, 1).data;
630
+ }
631
+ const linear = (c) => c / 255 <= .04045 ? c / 255 / 12.92 : ((c / 255 + .055) / 1.055) ** 2.4;
632
+ if ((.2126 * linear(r) + .7152 * linear(g) + .0722 * linear(b) + .05) ** 2 > .0525) button.dataset.ink = "dark";
633
+ }
634
+ const LOCK = Symbol.for("visual-feedback.scroll-lock");
635
+ function lock() {
636
+ const html = document.documentElement;
637
+ html[LOCK] ??= {
638
+ count: 0,
639
+ saved: [],
640
+ attribute: false
641
+ };
642
+ return html[LOCK];
643
+ }
644
+ function lockScroll() {
645
+ const state = lock();
646
+ if (state.count++ > 0) return;
647
+ const style = document.documentElement.style;
648
+ state.attribute = document.documentElement.hasAttribute("style");
649
+ state.saved = ["overflow-x", "overflow-y"].map((name) => [
650
+ name,
651
+ style.getPropertyValue(name),
652
+ style.getPropertyPriority(name)
653
+ ]);
654
+ style.setProperty("overflow", "hidden", "important");
655
+ }
656
+ function unlockScroll() {
657
+ const state = lock();
658
+ if (--state.count > 0) return;
659
+ const style = document.documentElement.style;
660
+ for (const [name, value, priority] of state.saved) if (value) style.setProperty(name, value, priority);
661
+ else style.removeProperty(name);
662
+ state.saved = [];
663
+ if (!state.attribute && !style.length) document.documentElement.removeAttribute("style");
664
+ }
665
+ const FOCUSABLE = "button,textarea,input,select,a[href],[tabindex]:not([tabindex='-1'])";
666
+ const NOTE_LIMIT = 2e3;
667
+ let writing = Promise.resolve();
668
+ /** Settles once every kept-draft write queued so far has landed or failed. */
669
+ function settled() {
670
+ return writing;
671
+ }
672
+ function mount(parent, handlers, settings = {}) {
673
+ const tools = settings.tools ?? [
674
+ "pen",
675
+ "box",
676
+ "arrow",
677
+ "text",
678
+ "select"
679
+ ];
680
+ const choices = [...new Set(tools)].filter((tool) => [
681
+ "pen",
682
+ "box",
683
+ "arrow",
684
+ "text",
685
+ "select"
686
+ ].includes(tool));
687
+ const drawing = choices.some((choice) => choice !== "select");
688
+ const text = texts(settings.strings);
689
+ const backdrop = element("dialog", "backdrop", {
690
+ "aria-modal": "true",
691
+ "aria-labelledby": "vf-title"
692
+ });
693
+ const panel = element("div", "dialog");
694
+ const title = element("h2", "title", { id: "vf-title" });
695
+ title.textContent = text.title;
696
+ const header = element("div", "header");
697
+ const cancel = iconButton("cancel", text.cancel, ICONS.cancel, () => lose() ? ask() : handlers.close());
698
+ const keep = iconButton("minimise", text.minimise, ICONS.minimise, () => minimise());
699
+ header.append(title, cancel);
700
+ backdrop.append(panel);
701
+ const taken = [];
702
+ let at = 0;
703
+ let tool;
704
+ let annotator;
705
+ let refresh = () => {};
706
+ let body;
707
+ let stage;
708
+ let strip;
709
+ let note;
710
+ let notice;
711
+ let bar;
712
+ let minimised = false;
713
+ let folding = false;
714
+ let locked = [];
715
+ let inert = [];
716
+ let primary;
717
+ let draft;
718
+ let composer;
719
+ let failure;
720
+ const root = () => parent.getRootNode();
721
+ const key = `visual-feedback:${root().host.localName}`;
722
+ const keeping = handlers.keeps;
723
+ const restored = /* @__PURE__ */ new WeakMap();
724
+ let kept;
725
+ let ended = 0;
726
+ let over = false;
727
+ let submitted = false;
728
+ let reviving = false;
729
+ let picker;
730
+ let asking;
731
+ function lose() {
732
+ if (over || primary?.getAttribute("aria-disabled") === "true") return false;
733
+ return Boolean(note?.value.trim() || picker?.selectedIndex || taken.length > 1 || taken.some((shot) => shot.annotator?.marks.length));
734
+ }
735
+ function ask() {
736
+ asking?.remove();
737
+ const back = () => {
738
+ asking?.remove();
739
+ cancel.focus();
740
+ };
741
+ const question = element("span", "confirm-text");
742
+ question.textContent = text.discardConfirm;
743
+ const stay = button("keep-editing", text.keepEditing, back);
744
+ asking = element("div", "actions confirm", {
745
+ role: "group",
746
+ "aria-label": text.discardConfirm
747
+ });
748
+ asking.append(question, button("discard", text.discard, handlers.close));
749
+ if (taken.length && !draft) asking.append(button("keep-draft", text.minimise, () => minimise()));
750
+ asking.append(stay);
751
+ asking.addEventListener("keydown", (event) => {
752
+ if (event.key !== "Escape") return;
753
+ event.preventDefault();
754
+ event.stopPropagation();
755
+ back();
756
+ });
757
+ header.after(asking);
758
+ stay.focus();
759
+ }
760
+ function toolbar() {
761
+ if (!drawing) return [];
762
+ const toolbar = element("div", "toolbar");
763
+ const undo = iconButton("undo", text.undo, ICONS.undo, () => annotator?.undo());
764
+ const clear = iconButton("clear", text.clear, ICONS.clear, () => annotator?.clear());
765
+ const remove = iconButton("remove", text.remove, ICONS.remove, () => annotator?.remove());
766
+ const actions = choices.includes("select") ? [
767
+ remove,
768
+ undo,
769
+ clear
770
+ ] : [undo, clear];
771
+ refresh = () => {
772
+ const off = /* @__PURE__ */ new Map([
773
+ [undo, !annotator?.canUndo],
774
+ [clear, !annotator?.marks.length],
775
+ [remove, !annotator?.selected]
776
+ ]);
777
+ if (off.get(root().activeElement)) buttons.find((choice) => choice.getAttribute("aria-pressed") === "true")?.focus();
778
+ for (const [control, disabled] of off) control.disabled = disabled;
779
+ };
780
+ const pick = (next) => {
781
+ tool = next;
782
+ if (annotator) annotator.tool = next;
783
+ for (const [i, choice] of buttons.entries()) {
784
+ const active = choices[i] === next;
785
+ choice.setAttribute("aria-pressed", String(active));
786
+ choice.setAttribute("part", active ? "tool tool-active" : "tool");
787
+ }
788
+ };
789
+ const buttons = choices.map((choice) => iconButton("tool", text[choice], ICONS[choice], () => pick(choice)));
790
+ pick(choices.find((choice) => choice !== "select"));
791
+ toolbar.append(...buttons, element("span", "divider", { "aria-hidden": "true" }), ...actions);
792
+ locked.push(...buttons, ...actions);
793
+ return [toolbar];
794
+ }
795
+ let thumb;
796
+ function changed() {
797
+ refresh();
798
+ const shown = taken[at]?.image;
799
+ if (thumb && shown) copy(thumb, shown);
800
+ }
801
+ function add(image) {
802
+ image.className = "image";
803
+ image.setAttribute("part", "image");
804
+ image.removeAttribute("style");
805
+ const shot = {
806
+ image,
807
+ annotator: void 0,
808
+ seen: shots.get(image) ?? look(),
809
+ blob: void 0
810
+ };
811
+ taken.push(shot);
812
+ showShot(taken.length - 1);
813
+ if (drawing) {
814
+ shot.annotator = annotate(image, changed, text.text, restored.get(image));
815
+ annotator = shot.annotator;
816
+ if (tool) annotator.tool = tool;
817
+ changed();
818
+ }
819
+ const clean = shot.annotator?.clean ?? image;
820
+ shot.blob = keeping() ? import("./capture-CufRosj3.mjs").then((capture) => capture.encode(clean, { type: "image/png" })) : void 0;
821
+ shot.blob?.catch(() => {});
822
+ }
823
+ function showShot(index) {
824
+ const focused = strip?.contains(root().activeElement) ?? false;
825
+ annotator?.end();
826
+ stage?.querySelector(":scope > .image")?.remove();
827
+ at = index;
828
+ const shot = taken[at];
829
+ annotator = shot.annotator;
830
+ if (annotator && tool) annotator.tool = tool;
831
+ stage?.prepend(shot.image);
832
+ renderStrip();
833
+ refresh();
834
+ if (focused) strip?.querySelector("[aria-current=true]")?.focus();
835
+ }
836
+ function drop(index) {
837
+ if (taken.length < 2) return;
838
+ const focused = strip?.contains(root().activeElement) ?? false;
839
+ const [gone] = taken.splice(index, 1);
840
+ if (index === at) {
841
+ gone?.annotator?.end();
842
+ annotator = void 0;
843
+ showShot(Math.min(index, taken.length - 1));
844
+ } else {
845
+ if (index < at) at--;
846
+ renderStrip();
847
+ }
848
+ if (focused) strip?.querySelector("[aria-current=true]")?.focus();
849
+ }
850
+ function renderStrip() {
851
+ if (!strip) return;
852
+ const items = taken.map((shot, i) => {
853
+ const item = element("div", "thumb");
854
+ const name = `${text.screenshot} ${i + 1}`;
855
+ const pick = element("button", "thumb-pick", {
856
+ type: "button",
857
+ "aria-label": name,
858
+ title: name,
859
+ "aria-current": String(i === at)
860
+ });
861
+ const number = element("span", "thumb-number", { "aria-hidden": "true" });
862
+ number.textContent = String(i + 1);
863
+ const small = thumbnail(shot.image);
864
+ if (i === at) thumb = small;
865
+ pick.append(small, number);
866
+ pick.addEventListener("click", () => showShot(i));
867
+ item.append(pick);
868
+ if (taken.length > 1) {
869
+ const label = `${text.removeScreenshot} ${i + 1}`;
870
+ item.append(iconButton("thumb-remove", label, ICONS.cancel, () => drop(i)));
871
+ }
872
+ return item;
873
+ });
874
+ const more = button("add", text.addScreenshot, () => minimise());
875
+ more.prepend(icon(ICONS.add, "icon"));
876
+ strip.replaceChildren(...items, ...taken.length < 10 ? [more] : []);
877
+ }
878
+ function minimise(focus = true) {
879
+ if (minimised || !taken.length || draft) return;
880
+ annotator?.end();
881
+ asking?.remove();
882
+ minimised = true;
883
+ if (backdrop.open) {
884
+ folding = true;
885
+ backdrop.close();
886
+ }
887
+ backdrop.remove();
888
+ lockPage(false);
889
+ bar = draftBar();
890
+ parent.append(bar);
891
+ for (const accent of bar.querySelectorAll(".capture-view")) ink(accent);
892
+ if (focus) bar.querySelector(".capture-view:enabled, .reopen")?.focus();
893
+ store();
894
+ }
895
+ function discard() {
896
+ over = true;
897
+ ended++;
898
+ unkeep();
899
+ }
900
+ function unkeep() {
901
+ const id = kept;
902
+ kept = void 0;
903
+ try {
904
+ if (id && id === sessionStorage.getItem(key)) sessionStorage.removeItem(key);
905
+ } catch {}
906
+ if (id) writing = writing.then(() => import("./keep-BG5WxZPU.mjs").then(({ forget }) => forget(id).then(() => {}), () => {})).catch(() => {});
907
+ }
908
+ function store() {
909
+ if (!keeping() || over || !taken.length || taken.some((shot) => !shot.blob)) return false;
910
+ const turn = ended;
911
+ try {
912
+ const held = sessionStorage.getItem(key);
913
+ if (held && held !== kept) return false;
914
+ kept ??= [...crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(2))].join("-");
915
+ sessionStorage.setItem(key, kept);
916
+ } catch {
917
+ return false;
918
+ }
919
+ const id = kept;
920
+ const shots = taken.map((shot) => ({
921
+ blob: shot.blob,
922
+ marks: [...shot.annotator?.marks ?? []],
923
+ seen: shot.seen
924
+ }));
925
+ const draft = {
926
+ note: note?.value ?? "",
927
+ category: picker?.value
928
+ };
929
+ writing = writing.then(async () => {
930
+ try {
931
+ const images = [];
932
+ for (const { blob, ...shot } of shots) {
933
+ if (!blob) throw new Error("not kept");
934
+ images.push({
935
+ ...shot,
936
+ image: await blob
937
+ });
938
+ }
939
+ const { keep, forget } = await import("./keep-BG5WxZPU.mjs");
940
+ if (turn !== ended) return;
941
+ await keep(id, {
942
+ saved: Date.now(),
943
+ ...draft,
944
+ shots: images
945
+ });
946
+ if (turn !== ended) await forget(id);
947
+ } catch {
948
+ if (turn === ended) {
949
+ ended++;
950
+ unkeep();
951
+ }
952
+ }
953
+ });
954
+ return true;
955
+ }
956
+ function draftBar() {
957
+ const result = element("div", "draft", {
958
+ role: "region",
959
+ "aria-label": text.draft
960
+ });
961
+ const [y, x] = (handlers.options().position ?? "bottom-right").split("-");
962
+ result.dataset.y = y;
963
+ result.dataset.x = x;
964
+ const label = element("span", "draft-label");
965
+ label.textContent = `${text.draft} · ${taken.length}`;
966
+ const thumbs = element("span", "draft-thumbs", { "aria-hidden": "true" });
967
+ thumbs.append(...taken.map((shot) => thumbnail(shot.image)));
968
+ const again = button("capture-view", text.captureView, handlers.retry);
969
+ again.prepend(icon(ICONS.camera, "icon"));
970
+ again.disabled = taken.length >= 10;
971
+ result.append(label, thumbs, again, button("reopen", text.reopen, reopen), button("discard", text.discard, handlers.close));
972
+ return result;
973
+ }
974
+ function reopen() {
975
+ if (!minimised) return;
976
+ minimised = false;
977
+ bar?.remove();
978
+ bar = void 0;
979
+ parent.append(backdrop);
980
+ backdrop.showModal();
981
+ lockPage(true);
982
+ renderStrip();
983
+ note?.focus();
984
+ }
985
+ let seen;
986
+ let report;
987
+ let mounted = true;
988
+ async function deliver(draft) {
989
+ sending();
990
+ const options = handlers.options();
991
+ let module;
992
+ try {
993
+ module = await import("./report-CnspEsHf.mjs");
994
+ if (!report) {
995
+ const capture = await import("./capture-CufRosj3.mjs");
996
+ const shots = [];
997
+ for (const shot of draft.shots) shots.push({
998
+ ...shot,
999
+ image: await capture.encode(shot.image, options.capture)
1000
+ });
1001
+ const built = await module.build(draft, shots, draft.shots[0]?.seen ?? seen, options);
1002
+ if (mounted) report = built;
1003
+ }
1004
+ if (!mounted || !report) return;
1005
+ submitted = true;
1006
+ handlers.emit("submit", report);
1007
+ await module.deliver(report, options);
1008
+ discard();
1009
+ handlers.emit("sent", report);
1010
+ if (mounted) sent();
1011
+ } catch (error) {
1012
+ handlers.emit("error", error);
1013
+ if (mounted) failed(module?.shown(error));
1014
+ }
1015
+ }
1016
+ let saving = false;
1017
+ let pdfError;
1018
+ async function savePdf(note, category) {
1019
+ if (saving) return;
1020
+ saving = true;
1021
+ pdfError?.remove();
1022
+ annotator?.end();
1023
+ const font = getComputedStyle(panel).fontFamily;
1024
+ try {
1025
+ const pages = taken.map(({ image }) => {
1026
+ const page = document.createElement("canvas");
1027
+ page.width = image.width;
1028
+ page.height = image.height;
1029
+ const context = page.getContext("2d");
1030
+ if (!context) throw new Error("no 2d context");
1031
+ context.drawImage(image, 0, 0);
1032
+ return page;
1033
+ });
1034
+ const capture = await import("./capture-CufRosj3.mjs");
1035
+ const writer = await import("./pdf-Gj-ZO9W5.mjs");
1036
+ const [first, ...rest] = pages;
1037
+ const front = writer.cover(note, category, first, font);
1038
+ if (first) first.width = 0;
1039
+ const file = await writer.pdf([front, ...rest], async (page, options) => {
1040
+ try {
1041
+ return await capture.encode(page, options);
1042
+ } finally {
1043
+ page.width = 0;
1044
+ }
1045
+ });
1046
+ if (!mounted) return;
1047
+ const link = document.createElement("a");
1048
+ link.href = URL.createObjectURL(file);
1049
+ const now = /* @__PURE__ */ new Date();
1050
+ link.download = `feedback-${[
1051
+ now.getFullYear(),
1052
+ now.getMonth() + 1,
1053
+ now.getDate()
1054
+ ].map((part) => String(part).padStart(2, "0")).join("-")}.pdf`;
1055
+ link.hidden = true;
1056
+ panel.append(link);
1057
+ link.click();
1058
+ link.remove();
1059
+ setTimeout(() => URL.revokeObjectURL(link.href), 1e4);
1060
+ handlers.emit("saved");
1061
+ } catch {
1062
+ if (!mounted || !composer) return;
1063
+ pdfError = element("p", "error", { role: "alert" });
1064
+ pdfError.textContent = text.pdfError;
1065
+ composer.after(pdfError);
1066
+ } finally {
1067
+ saving = false;
1068
+ }
1069
+ }
1070
+ function show(view) {
1071
+ locked = [];
1072
+ inert = [];
1073
+ primary = void 0;
1074
+ draft = void 0;
1075
+ failure = void 0;
1076
+ composer = void 0;
1077
+ header.replaceChildren(title, cancel);
1078
+ if (view.error) {
1079
+ const message = element("p", "error", { role: "alert" });
1080
+ message.textContent = text.captureError;
1081
+ const retry = button("retry", text.retry, handlers.retry);
1082
+ const actions = element("div", "actions");
1083
+ actions.append(button("send-without", text.sendWithout, () => show({ image: null })), retry);
1084
+ panel.classList.remove("wide");
1085
+ panel.replaceChildren(header, message, actions);
1086
+ retry.focus();
1087
+ return;
1088
+ }
1089
+ const label = element("label", "label", { for: "vf-note" });
1090
+ label.textContent = text.noteLabel;
1091
+ const field = element("textarea", "note", {
1092
+ id: "vf-note",
1093
+ placeholder: text.notePlaceholder,
1094
+ maxlength: String(NOTE_LIMIT)
1095
+ });
1096
+ note = field;
1097
+ const required = element("p", "error", {
1098
+ id: "vf-note-error",
1099
+ role: "alert"
1100
+ });
1101
+ required.textContent = text.noteRequired;
1102
+ field.addEventListener("input", () => {
1103
+ if (!field.value.trim()) return;
1104
+ field.removeAttribute("aria-invalid");
1105
+ field.removeAttribute("aria-describedby");
1106
+ required.remove();
1107
+ });
1108
+ const fields = [];
1109
+ let category;
1110
+ picker = void 0;
1111
+ if (settings.categories?.length) {
1112
+ const name = element("label", "label", { for: "vf-category" });
1113
+ name.textContent = text.categoryLabel;
1114
+ category = element("select", "category", { id: "vf-category" });
1115
+ picker = category;
1116
+ for (const choice of settings.categories) {
1117
+ const option = document.createElement("option");
1118
+ option.value = choice.id;
1119
+ option.textContent = choice.label;
1120
+ category.append(option);
1121
+ }
1122
+ fields.push(name, category);
1123
+ }
1124
+ const image = view.image;
1125
+ body = element("div", "body");
1126
+ if (image) {
1127
+ stage = element("div", "stage");
1128
+ strip = element("div", "strip");
1129
+ stage.append(strip);
1130
+ body.append(...toolbar(), stage);
1131
+ header.replaceChildren(title, keep, cancel);
1132
+ locked.push(keep);
1133
+ inert.push(stage);
1134
+ }
1135
+ const send = button("send", text.send, () => {
1136
+ menu(false);
1137
+ if (draft) {
1138
+ if (send.getAttribute("aria-disabled") !== "true") deliver(draft);
1139
+ return;
1140
+ }
1141
+ if (settings.requireNote !== false && !field.value.trim()) {
1142
+ field.setAttribute("aria-invalid", "true");
1143
+ field.setAttribute("aria-describedby", required.id);
1144
+ box.after(required);
1145
+ field.focus();
1146
+ return;
1147
+ }
1148
+ annotator?.end();
1149
+ draft = {
1150
+ note: field.value,
1151
+ category: category?.value,
1152
+ shots: taken.map((shot) => ({
1153
+ image: shot.image,
1154
+ marks: [...shot.annotator?.marks ?? []],
1155
+ seen: shot.seen
1156
+ }))
1157
+ };
1158
+ deliver(draft);
1159
+ });
1160
+ send.append(icon(ICONS.send, "icon"));
1161
+ primary = send;
1162
+ locked.push(field, ...category ? [category] : []);
1163
+ const box = element("div", "composer");
1164
+ box.append(field);
1165
+ if (fields.length) {
1166
+ const bar = element("div", "composer-bar");
1167
+ bar.append(...fields);
1168
+ box.append(bar);
1169
+ }
1170
+ composer = box;
1171
+ const footer = element("div", "footer");
1172
+ if (image) {
1173
+ const hint = element("p", "hint");
1174
+ hint.textContent = text.escHint;
1175
+ footer.append(hint);
1176
+ }
1177
+ const more = iconButton("send-more", text.moreSend, ICONS.more, () => menu(list.hidden === true));
1178
+ more.setAttribute("aria-haspopup", "menu");
1179
+ more.setAttribute("aria-expanded", "false");
1180
+ const list = element("div", "menu", {
1181
+ role: "menu",
1182
+ "aria-label": text.moreSend
1183
+ });
1184
+ list.hidden = true;
1185
+ const save = button("save-pdf", text.savePdf, () => {
1186
+ menu(false);
1187
+ more.focus();
1188
+ savePdf(field.value, category?.selectedOptions[0]?.textContent ?? void 0);
1189
+ });
1190
+ save.setAttribute("role", "menuitem");
1191
+ save.prepend(icon(ICONS.pdf, "icon"));
1192
+ list.append(save);
1193
+ function menu(open) {
1194
+ list.hidden = !open;
1195
+ more.setAttribute("aria-expanded", String(open));
1196
+ if (open) save.focus();
1197
+ }
1198
+ list.addEventListener("keydown", (event) => {
1199
+ if (event.key !== "Tab") return;
1200
+ menu(false);
1201
+ more.focus();
1202
+ });
1203
+ list.addEventListener("mousedown", (event) => event.preventDefault());
1204
+ const split = element("div", "split");
1205
+ split.addEventListener("keydown", (event) => {
1206
+ if (event.key !== "Escape" || list.hidden) return;
1207
+ event.preventDefault();
1208
+ event.stopPropagation();
1209
+ menu(false);
1210
+ more.focus();
1211
+ });
1212
+ split.addEventListener("focusout", (event) => {
1213
+ if (!split.contains(event.relatedTarget)) menu(false);
1214
+ });
1215
+ split.append(send, more, list);
1216
+ locked.push(more);
1217
+ footer.append(split);
1218
+ panel.classList.toggle("wide", Boolean(image));
1219
+ panel.replaceChildren(header, ...image ? [body] : [], label, box, footer);
1220
+ if (image) add(image);
1221
+ ink(send);
1222
+ ink(more);
1223
+ field.focus();
1224
+ }
1225
+ function sending() {
1226
+ if (!primary) return;
1227
+ panel.querySelector(".hint")?.remove();
1228
+ pdfError?.remove();
1229
+ asking?.remove();
1230
+ for (const control of locked) control.disabled = true;
1231
+ for (const item of inert) item.inert = true;
1232
+ failure?.remove();
1233
+ primary.setAttribute("aria-disabled", "true");
1234
+ primary.textContent = text.sending;
1235
+ primary.setAttribute("part", "send");
1236
+ primary.focus();
1237
+ }
1238
+ function sent() {
1239
+ const status = element("p", "status", {
1240
+ role: "status",
1241
+ tabindex: "-1"
1242
+ });
1243
+ status.textContent = text.sent;
1244
+ panel.classList.remove("wide");
1245
+ header.replaceChildren(title, cancel);
1246
+ panel.replaceChildren(header, status);
1247
+ status.focus();
1248
+ done = setTimeout(handlers.close, 3e3);
1249
+ }
1250
+ function failed(message = text.error) {
1251
+ if (!primary || !composer) return;
1252
+ failure = element("p", "error", { role: "alert" });
1253
+ failure.textContent = message;
1254
+ composer.after(failure);
1255
+ primary.removeAttribute("aria-disabled");
1256
+ primary.textContent = text.retry;
1257
+ primary.setAttribute("part", "retry");
1258
+ primary.focus();
1259
+ }
1260
+ const onEscape = () => {
1261
+ if (taken.length && !draft) minimise();
1262
+ else if (lose()) {
1263
+ if (!backdrop.open) backdrop.showModal();
1264
+ ask();
1265
+ } else handlers.close();
1266
+ };
1267
+ backdrop.addEventListener("cancel", (event) => {
1268
+ event.preventDefault();
1269
+ onEscape();
1270
+ });
1271
+ const closed = () => {
1272
+ if (folding) folding = false;
1273
+ else onEscape();
1274
+ };
1275
+ backdrop.addEventListener("close", closed);
1276
+ backdrop.addEventListener("keydown", (event) => {
1277
+ if (event.key !== "Tab") return;
1278
+ event.preventDefault();
1279
+ const items = [...panel.querySelectorAll(FOCUSABLE)].filter((item) => !item.matches(":disabled") && !item.closest("[inert]") && item.getClientRects().length > 0);
1280
+ const active = root().activeElement;
1281
+ const index = active ? items.indexOf(active) : -1;
1282
+ const step = event.shiftKey ? -1 : 1;
1283
+ const next = index < 0 ? event.shiftKey ? items.length - 1 : 0 : index + step;
1284
+ items.at(next % items.length)?.focus();
1285
+ });
1286
+ parent.append(backdrop);
1287
+ parent.classList.add("drafting");
1288
+ if (keeping()) import("./keep-BG5WxZPU.mjs").then(({ sweep }) => sweep()).catch(() => {});
1289
+ let done;
1290
+ let scrollLocked = false;
1291
+ function lockPage(on) {
1292
+ if (on !== scrollLocked) (on ? lockScroll : unlockScroll)();
1293
+ scrollLocked = on;
1294
+ }
1295
+ return {
1296
+ show,
1297
+ capture(image) {
1298
+ if (taken.length) {
1299
+ reopen();
1300
+ notice?.remove();
1301
+ if (image) add(image);
1302
+ else {
1303
+ notice = element("p", "error", { role: "alert" });
1304
+ notice.textContent = text.captureError;
1305
+ body?.after(notice);
1306
+ }
1307
+ return;
1308
+ }
1309
+ backdrop.showModal();
1310
+ seen = (image && shots.get(image)) ?? look();
1311
+ show(image ? { image } : { error: true });
1312
+ lockPage(true);
1313
+ },
1314
+ reopen,
1315
+ async revive() {
1316
+ reviving = true;
1317
+ try {
1318
+ await writing;
1319
+ const id = sessionStorage.getItem(key);
1320
+ const { read, forget, KEPT_FOR } = await import("./keep-BG5WxZPU.mjs");
1321
+ const draft = id ? await read(id) : void 0;
1322
+ if (!id || !draft?.shots.length || draft.saved < Date.now() - KEPT_FOR) {
1323
+ sessionStorage.removeItem(key);
1324
+ if (id) await forget(id);
1325
+ return false;
1326
+ }
1327
+ const images = [];
1328
+ for (const shot of draft.shots) {
1329
+ const bitmap = await createImageBitmap(shot.image);
1330
+ const image = document.createElement("canvas");
1331
+ image.width = bitmap.width;
1332
+ image.height = bitmap.height;
1333
+ image.getContext("2d")?.drawImage(bitmap, 0, 0);
1334
+ shots.set(image, shot.seen);
1335
+ restored.set(image, shot.marks);
1336
+ images.push(image);
1337
+ }
1338
+ if (!mounted) return false;
1339
+ kept = id;
1340
+ seen = draft.shots[0]?.seen ?? look();
1341
+ show({ image: images[0] ?? null });
1342
+ for (const image of images.slice(1)) add(image);
1343
+ showShot(0);
1344
+ if (note) note.value = draft.note;
1345
+ if ([...picker?.options ?? []].some((option) => option.value === draft.category)) picker.value = draft.category;
1346
+ minimise(false);
1347
+ return true;
1348
+ } catch {
1349
+ return false;
1350
+ } finally {
1351
+ reviving = false;
1352
+ }
1353
+ },
1354
+ lock: lockPage,
1355
+ unmount(keep) {
1356
+ if (!mounted) return false;
1357
+ if (keep) annotator?.end();
1358
+ const stays = reviving || !!keep && !submitted && store();
1359
+ mounted = false;
1360
+ clearTimeout(done);
1361
+ lockPage(false);
1362
+ backdrop.removeEventListener("close", closed);
1363
+ if (backdrop.open) backdrop.close();
1364
+ backdrop.remove();
1365
+ bar?.remove();
1366
+ parent.classList.remove("drafting");
1367
+ if (reviving) ended++;
1368
+ else if (!stays) discard();
1369
+ return stays;
1370
+ }
1371
+ };
1372
+ }
1373
+ //#endregion
1374
+ export { announce, css, mount, settled };