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,97 @@
1
+
2
+ //#region src/shared/report.ts
3
+ const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
4
+ function fail(path) {
5
+ throw new TypeError(`report.${path} has the wrong shape`);
6
+ }
7
+ function object(value, path) {
8
+ if (typeof value !== "object" || value === null || Array.isArray(value)) fail(path);
9
+ return value;
10
+ }
11
+ const UNSTORABLE = /\0|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
12
+ const storable = (text) => text.replace(UNSTORABLE, (found) => found === "\0" ? "" : "�");
13
+ function string(value, path) {
14
+ if (typeof value !== "string") fail(path);
15
+ return storable(value);
16
+ }
17
+ function storableDeep(value) {
18
+ if (typeof value === "string") return storable(value);
19
+ if (Array.isArray(value)) return value.map(storableDeep);
20
+ if (value === null || typeof value !== "object") return value;
21
+ const proto = Object.getPrototypeOf(value);
22
+ if (proto !== Object.prototype && proto !== null) return value;
23
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [storable(key), storableDeep(item)]).filter(([key]) => key !== "__proto__"));
24
+ }
25
+ function number(value, path) {
26
+ if (typeof value !== "number" || !Number.isFinite(value)) fail(path);
27
+ return value;
28
+ }
29
+ function point(value, path) {
30
+ if (!Array.isArray(value) || value.length !== 2) fail(path);
31
+ return [number(value[0], path), number(value[1], path)];
32
+ }
33
+ function mark(value, path) {
34
+ const fields = object(value, path);
35
+ if (fields.tool === "pen") {
36
+ if (!Array.isArray(fields.points)) fail(`${path}.points`);
37
+ return {
38
+ tool: "pen",
39
+ points: fields.points.map((item, i) => point(item, `${path}.points[${i}]`))
40
+ };
41
+ }
42
+ if (fields.tool === "box") return {
43
+ tool: "box",
44
+ x: number(fields.x, `${path}.x`),
45
+ y: number(fields.y, `${path}.y`),
46
+ width: number(fields.width, `${path}.width`),
47
+ height: number(fields.height, `${path}.height`)
48
+ };
49
+ if (fields.tool === "arrow") return {
50
+ tool: "arrow",
51
+ from: point(fields.from, `${path}.from`),
52
+ to: point(fields.to, `${path}.to`)
53
+ };
54
+ return fail(`${path}.tool`);
55
+ }
56
+ /**
57
+ * The one shape check for a report without its image, as JSON carries it. Returns a copy that
58
+ * holds only the known fields, or throws a TypeError that names the first wrong field.
59
+ */
60
+ function checkReport(value) {
61
+ const fields = object(value, "");
62
+ if (typeof fields.id !== "string" || !UUID_V4.test(fields.id)) fail("id");
63
+ const createdAt = string(fields.createdAt, "createdAt");
64
+ const time = Date.parse(createdAt);
65
+ if (!(time >= 0 && time < Date.UTC(1e4, 0, 1)) || new Date(time).toISOString() !== createdAt) fail("createdAt");
66
+ if (fields.image !== void 0 && fields.image !== null) fail("image");
67
+ if (!Array.isArray(fields.marks)) fail("marks");
68
+ const page = object(fields.page, "page");
69
+ const viewport = object(fields.viewport, "viewport");
70
+ const scroll = object(fields.scroll, "scroll");
71
+ return {
72
+ id: fields.id,
73
+ createdAt,
74
+ note: string(fields.note, "note"),
75
+ ...fields.category === void 0 ? {} : { category: string(fields.category, "category") },
76
+ marks: fields.marks.map((item, i) => mark(item, `marks[${i}]`)),
77
+ page: {
78
+ url: string(page.url, "page.url"),
79
+ title: string(page.title, "page.title")
80
+ },
81
+ viewport: {
82
+ width: number(viewport.width, "viewport.width"),
83
+ height: number(viewport.height, "viewport.height"),
84
+ devicePixelRatio: number(viewport.devicePixelRatio, "viewport.devicePixelRatio")
85
+ },
86
+ scroll: {
87
+ x: number(scroll.x, "scroll.x"),
88
+ y: number(scroll.y, "scroll.y")
89
+ },
90
+ userAgent: string(fields.userAgent, "userAgent"),
91
+ language: string(fields.language, "language"),
92
+ timeZone: string(fields.timeZone, "timeZone"),
93
+ context: storableDeep(object(fields.context, "context"))
94
+ };
95
+ }
96
+ //#endregion
97
+ export { checkReport as t };
@@ -0,0 +1,43 @@
1
+
2
+ //#region src/shared/report.d.ts
3
+ type Mark = {
4
+ tool: "pen";
5
+ points: [number, number][];
6
+ } | {
7
+ tool: "box";
8
+ x: number;
9
+ y: number;
10
+ width: number;
11
+ height: number;
12
+ } | {
13
+ tool: "arrow";
14
+ from: [number, number];
15
+ to: [number, number];
16
+ };
17
+ type FeedbackReport = {
18
+ id: string;
19
+ createdAt: string;
20
+ note: string;
21
+ category?: string;
22
+ image: Blob | null;
23
+ marks: Mark[];
24
+ page: {
25
+ url: string;
26
+ title: string;
27
+ };
28
+ viewport: {
29
+ width: number;
30
+ height: number;
31
+ devicePixelRatio: number;
32
+ };
33
+ scroll: {
34
+ x: number;
35
+ y: number;
36
+ };
37
+ userAgent: string;
38
+ language: string;
39
+ timeZone: string;
40
+ context: Record<string, unknown>;
41
+ };
42
+ //#endregion
43
+ export { Mark as n, FeedbackReport as t };
@@ -0,0 +1,21 @@
1
+
2
+ //#region src/seen.ts
3
+ /** What the page looked like when each screenshot was taken. */
4
+ const shots = /* @__PURE__ */ new WeakMap();
5
+ function look() {
6
+ return {
7
+ url: location.href,
8
+ title: document.title,
9
+ viewport: {
10
+ width: innerWidth,
11
+ height: innerHeight,
12
+ devicePixelRatio
13
+ },
14
+ scroll: {
15
+ x: scrollX,
16
+ y: scrollY
17
+ }
18
+ };
19
+ }
20
+ //#endregion
21
+ export { shots as n, look as t };
@@ -0,0 +1,15 @@
1
+
2
+ import { n as Mark, t as FeedbackReport } from "./report-CIgy7uzP.mjs";
3
+ //#region src/server/index.d.ts
4
+ export declare class FeedbackRequestError extends Error {
5
+ readonly status: 400 | 413;
6
+ constructor(status: 400 | 413, message: string);
7
+ }
8
+ export declare function parseFeedbackRequest(request: Request, limits?: {
9
+ maxImageBytes?: number;
10
+ }): Promise<{
11
+ report: FeedbackReport;
12
+ image: Blob | null;
13
+ }>;
14
+ //#endregion
15
+ export type { FeedbackReport, Mark };
@@ -0,0 +1,95 @@
1
+
2
+ import { t as checkReport } from "./report-BxonlIdW.mjs";
3
+ //#region src/server/index.ts
4
+ const DEFAULT_MAX_IMAGE_BYTES = 1e7;
5
+ const REPORT_BYTES = 1048576;
6
+ const FRAMING_BYTES = 65536;
7
+ const PNG = [
8
+ 137,
9
+ 80,
10
+ 78,
11
+ 71,
12
+ 13,
13
+ 10,
14
+ 26,
15
+ 10
16
+ ];
17
+ var FeedbackRequestError = class extends Error {
18
+ status;
19
+ constructor(status, message) {
20
+ super(message);
21
+ this.name = "FeedbackRequestError";
22
+ this.status = status;
23
+ }
24
+ };
25
+ async function read(request, limit) {
26
+ if (Number(request.headers.get("content-length")) > limit) throw new FeedbackRequestError(413, "The request is too large.");
27
+ if (!request.body) throw new FeedbackRequestError(400, "The request has no body.");
28
+ const reader = request.body.getReader();
29
+ const chunks = [];
30
+ let size = 0;
31
+ for (;;) {
32
+ let next;
33
+ try {
34
+ next = await reader.read();
35
+ } catch {
36
+ throw new FeedbackRequestError(400, "The request body broke off.");
37
+ }
38
+ const { done, value } = next;
39
+ if (done) return new Blob(chunks);
40
+ size += value.byteLength;
41
+ if (size > limit) {
42
+ await reader.cancel().catch(() => {});
43
+ throw new FeedbackRequestError(413, "The request is too large.");
44
+ }
45
+ chunks.push(value);
46
+ }
47
+ }
48
+ function imageType(bytes) {
49
+ if (PNG.every((byte, i) => bytes[i] === byte)) return "image/png";
50
+ const header = String.fromCharCode(...bytes.subarray(0, 12));
51
+ if (header.startsWith("RIFF") && header.slice(8, 12) === "WEBP") return "image/webp";
52
+ return null;
53
+ }
54
+ async function checkImage(part, maxImageBytes) {
55
+ if (part.size > maxImageBytes) throw new FeedbackRequestError(413, "The image is too large.");
56
+ const type = imageType(new Uint8Array(await part.slice(0, 12).arrayBuffer()));
57
+ if (!type) throw new FeedbackRequestError(400, "The image is not a PNG or WebP.");
58
+ return part.slice(0, part.size, type);
59
+ }
60
+ async function parseFeedbackRequest(request, limits = {}) {
61
+ const maxImageBytes = limits.maxImageBytes ?? DEFAULT_MAX_IMAGE_BYTES;
62
+ if (!(maxImageBytes >= 0)) throw new RangeError("maxImageBytes must be a number of bytes.");
63
+ const contentType = request.headers.get("content-type") ?? "";
64
+ if (!/^multipart\/form-data\s*;/i.test(contentType)) throw new FeedbackRequestError(400, "The request is not multipart/form-data.");
65
+ const body = await read(request, maxImageBytes + REPORT_BYTES + FRAMING_BYTES);
66
+ let form;
67
+ try {
68
+ form = await new Response(body, { headers: { "content-type": contentType } }).formData();
69
+ } catch {
70
+ throw new FeedbackRequestError(400, "The multipart body cannot be read.");
71
+ }
72
+ const reports = form.getAll("report");
73
+ const images = form.getAll("image");
74
+ const [json] = reports;
75
+ const [part] = images;
76
+ if (reports.length !== 1 || typeof json !== "string") throw new FeedbackRequestError(400, "The request needs one report part, as text.");
77
+ if (images.length > 1 || typeof part === "string") throw new FeedbackRequestError(400, "The request has more than one image, or an image as text.");
78
+ if (new Blob([json]).size > REPORT_BYTES) throw new FeedbackRequestError(413, "The report is too large.");
79
+ let fields;
80
+ try {
81
+ fields = checkReport(JSON.parse(json, (key, value) => key === "__proto__" ? void 0 : value));
82
+ } catch (error) {
83
+ throw new FeedbackRequestError(400, error instanceof Error ? error.message : "Bad report.");
84
+ }
85
+ const image = part ? await checkImage(part, maxImageBytes) : null;
86
+ return {
87
+ report: {
88
+ ...fields,
89
+ image
90
+ },
91
+ image
92
+ };
93
+ }
94
+ //#endregion
95
+ export { FeedbackRequestError, parseFeedbackRequest };
@@ -0,0 +1,278 @@
1
+
2
+ //#region src/widget.ts
3
+ const SVG = "http://www.w3.org/2000/svg";
4
+ function supported() {
5
+ return typeof document !== "undefined" && "adoptedStyleSheets" in Document.prototype && "adoptedStyleSheets" in ShadowRoot.prototype && CSS.supports("color", "color-mix(in srgb, red, red)");
6
+ }
7
+ function sheet(text) {
8
+ const result = new CSSStyleSheet();
9
+ result.replaceSync(text);
10
+ return result;
11
+ }
12
+ function variables() {
13
+ return `.vf{--_r:var(--vf-radius,12px);--_s:var(--vf-surface,Canvas);--_t:var(--vf-text,CanvasText);--_m:var(--vf-muted,color-mix(in srgb,var(--_t) 60%,transparent));--_b:var(--vf-border,color-mix(in srgb,var(--_t) 15%,transparent));--_a:var(--vf-accent,${CSS.supports("color", "AccentColor") ? "AccentColor" : "#0b57d0"});--_at:var(--vf-accent-text,#fff);--_f:var(--vf-focus,var(--_a));--_mark:var(--vf-mark,#e5243b);--_bd:var(--vf-backdrop,rgb(0 0 0/45%));--_sh:var(--vf-shadow,0 4px 16px rgb(0 0 0/12%));--_size:var(--vf-trigger-size,36px);--_off:var(--vf-offset,16px);--_z:var(--vf-z,2147483000)}`;
14
+ }
15
+ const LAYOUT = `.vf{display:contents;font-family:var(--vf-font,inherit);color-scheme:light dark}.trigger{position:fixed;z-index:var(--_z);box-sizing:border-box;width:var(--_size);height:var(--_size);margin:0;padding:0;display:grid;place-items:center}.trigger[data-y=bottom]{bottom:calc(var(--_off) + env(safe-area-inset-bottom,0px))}.trigger[data-y=top]{top:calc(var(--_off) + env(safe-area-inset-top,0px))}.trigger[data-x=right]{right:calc(var(--_off) + env(safe-area-inset-right,0px))}.trigger[data-x=left]{left:calc(var(--_off) + env(safe-area-inset-left,0px))}.trigger svg{width:55%;height:55%}.capturing,.capturing .backdrop{visibility:hidden}@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}`;
16
+ const THEME = `.trigger{border:1px solid var(--_b);border-radius:50%;background:var(--_s);color:var(--_t);box-shadow:var(--_sh);cursor:pointer;transition:background-color .15s}.trigger:hover{background:color-mix(in srgb,var(--_s) 90%,var(--_t))}:focus-visible{outline:2px solid var(--_f);outline-offset:2px}`;
17
+ function icon() {
18
+ const svg = document.createElementNS(SVG, "svg");
19
+ const path = document.createElementNS(SVG, "path");
20
+ for (const [name, value] of Object.entries({
21
+ viewBox: "0 0 24 24",
22
+ "aria-hidden": "true",
23
+ fill: "none",
24
+ stroke: "currentColor",
25
+ "stroke-width": "2",
26
+ "stroke-linejoin": "round",
27
+ part: "trigger-icon"
28
+ })) svg.setAttribute(name, value);
29
+ path.setAttribute("d", "M21 12a8 8 0 0 1-11.6 7.1L4 20l1.1-4.4A8 8 0 1 1 21 12z");
30
+ svg.append(path);
31
+ return svg;
32
+ }
33
+ function field(element) {
34
+ return element.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(element.tagName);
35
+ }
36
+ function deepActive() {
37
+ let active = document.activeElement;
38
+ while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
39
+ return active;
40
+ }
41
+ const DISCONNECTED = Symbol.for("visual-feedback.disconnected");
42
+ const noop = {
43
+ open: async () => {},
44
+ close() {},
45
+ destroy() {},
46
+ update() {},
47
+ on: () => () => {}
48
+ };
49
+ function createFeedback(options = {}) {
50
+ if (!supported()) return noop;
51
+ const tag = options.tagName ?? "visual-feedback";
52
+ if (!customElements.get(tag)) customElements.define(tag, class extends HTMLElement {
53
+ disconnectedCallback() {
54
+ this[DISCONNECTED]?.();
55
+ }
56
+ });
57
+ const host = document.createElement(tag);
58
+ const root = host.attachShadow({ mode: "open" });
59
+ const wrapper = document.createElement("div");
60
+ wrapper.className = "vf";
61
+ root.append(wrapper);
62
+ let view;
63
+ const handlers = /* @__PURE__ */ new Map();
64
+ const call = (fns, args) => {
65
+ for (const fn of [...fns]) try {
66
+ fn(...args);
67
+ } catch (error) {
68
+ reportError(error);
69
+ }
70
+ };
71
+ const emit = (event, ...args) => call(handlers.get(event) ?? [], args);
72
+ const start = () => void widget.open().catch(() => {});
73
+ let state = "idle";
74
+ let dialog;
75
+ let session = 0;
76
+ let returnFocus = null;
77
+ let pending;
78
+ let trigger;
79
+ let unlisten;
80
+ function listen(shortcut) {
81
+ const parts = shortcut.toLowerCase().split("+");
82
+ const key = parts.pop();
83
+ const names = [
84
+ "alt",
85
+ "shift",
86
+ "control",
87
+ "meta"
88
+ ];
89
+ const mods = names.map((name) => parts.includes(name)).join();
90
+ const typing = !/control|meta/.test(shortcut.toLowerCase());
91
+ const on = (event) => {
92
+ if (event instanceof KeyboardEvent && state === "idle" && !event.repeat && !event.isComposing && !event.getModifierState("AltGraph") && parts.every((part) => names.includes(part)) && [
93
+ event.altKey,
94
+ event.shiftKey,
95
+ event.ctrlKey,
96
+ event.metaKey
97
+ ].join() === mods && [event.code.replace(/^(Key|Digit)/, ""), event.key].some((name) => name.toLowerCase() === key) && !(typing && field(event.composedPath()[0]))) {
98
+ event.preventDefault();
99
+ start();
100
+ }
101
+ };
102
+ addEventListener("keydown", on);
103
+ return () => removeEventListener("keydown", on);
104
+ }
105
+ let base = [];
106
+ let extra = [];
107
+ function sheets() {
108
+ const dialogCss = view?.css(Boolean(options.unstyled)).map(sheet) ?? [];
109
+ root.adoptedStyleSheets = [
110
+ ...base,
111
+ ...dialogCss,
112
+ ...extra
113
+ ];
114
+ }
115
+ function apply() {
116
+ wrapper.style.colorScheme = options.theme?.replace("auto", "") ?? "";
117
+ base = [sheet(variables() + LAYOUT), ...options.unstyled ? [] : [sheet(THEME)]];
118
+ extra = options.css ? [sheet(options.css)] : [];
119
+ sheets();
120
+ if (options.trigger === "none") {
121
+ trigger?.remove();
122
+ trigger = void 0;
123
+ } else {
124
+ if (!trigger) {
125
+ trigger = document.createElement("button");
126
+ trigger.type = "button";
127
+ trigger.className = "trigger";
128
+ trigger.setAttribute("part", "trigger");
129
+ trigger.append(icon());
130
+ trigger.addEventListener("click", start);
131
+ wrapper.prepend(trigger);
132
+ }
133
+ const [y, x] = (options.position ?? "bottom-right").split("-");
134
+ trigger.dataset.y = y;
135
+ trigger.dataset.x = x;
136
+ trigger.setAttribute("aria-label", options.strings?.trigger ?? "Send feedback");
137
+ }
138
+ unlisten?.();
139
+ unlisten = options.shortcut ? listen(options.shortcut) : void 0;
140
+ }
141
+ apply();
142
+ (options.container ?? document.body ?? document.documentElement).append(host);
143
+ function hide(on) {
144
+ wrapper.classList.toggle("capturing", on);
145
+ }
146
+ function requestDisplay() {
147
+ const media = navigator.mediaDevices;
148
+ if (options.capture?.mode !== "display" || typeof media?.getDisplayMedia !== "function") return;
149
+ hide(true);
150
+ const stream = media.getDisplayMedia({ video: true });
151
+ stream.catch(() => {});
152
+ return stream;
153
+ }
154
+ function stop(stream) {
155
+ stream?.then((media) => {
156
+ for (const track of media.getTracks()) track.stop();
157
+ }, () => {});
158
+ }
159
+ async function grab(module, mine, stream) {
160
+ hide(true);
161
+ const quiet = view?.announce(root, options.strings);
162
+ try {
163
+ return await module.capture(options.capture, stream);
164
+ } catch {
165
+ return null;
166
+ } finally {
167
+ quiet?.();
168
+ if (mine === session) hide(false);
169
+ }
170
+ }
171
+ function cancel() {
172
+ session++;
173
+ hide(false);
174
+ }
175
+ function teardown() {
176
+ dialog?.unmount();
177
+ dialog = void 0;
178
+ const target = returnFocus?.isConnected && returnFocus !== document.body ? returnFocus : trigger;
179
+ returnFocus = null;
180
+ target?.focus?.();
181
+ }
182
+ async function show(mine) {
183
+ returnFocus = deepActive();
184
+ const stream = requestDisplay();
185
+ try {
186
+ const [loaded, capture] = await Promise.all([import("./dialog-BTY6FKLo.mjs"), import("./capture-CufRosj3.mjs")]);
187
+ if (mine !== session) return stop(stream);
188
+ view = loaded;
189
+ sheets();
190
+ const image = await grab(capture, mine, stream);
191
+ if (mine !== session) return;
192
+ if (!host.isConnected) {
193
+ state = "idle";
194
+ return;
195
+ }
196
+ dialog = view.mount(wrapper, {
197
+ close: () => widget.close(),
198
+ retry: () => void retry(capture).catch((error) => {
199
+ widget.close();
200
+ emit("error", error);
201
+ }),
202
+ emit,
203
+ options: () => options
204
+ }, options);
205
+ dialog.capture(image);
206
+ } catch (error) {
207
+ stop(stream);
208
+ if (mine === session) {
209
+ state = "idle";
210
+ hide(false);
211
+ teardown();
212
+ }
213
+ emit("error", error);
214
+ throw error;
215
+ }
216
+ state = "open";
217
+ emit("open");
218
+ }
219
+ async function retry(capture) {
220
+ if (wrapper.classList.contains("capturing")) return;
221
+ const mine = session;
222
+ dialog?.lock(false);
223
+ const image = await grab(capture, mine, requestDisplay());
224
+ if (mine !== session) return;
225
+ dialog?.capture(image);
226
+ }
227
+ const widget = {
228
+ open() {
229
+ if (state === "opening" && pending) return pending;
230
+ if (state !== "idle") return Promise.resolve();
231
+ state = "opening";
232
+ pending = show(++session);
233
+ return pending;
234
+ },
235
+ close() {
236
+ if (state !== "open" && state !== "opening") return;
237
+ const wasOpen = state === "open";
238
+ state = "idle";
239
+ cancel();
240
+ teardown();
241
+ if (wasOpen) emit("close");
242
+ },
243
+ destroy() {
244
+ if (state === "destroyed") return;
245
+ const wasOpen = state === "open";
246
+ state = "destroyed";
247
+ cancel();
248
+ unlisten?.();
249
+ if (wasOpen) teardown();
250
+ const closers = wasOpen ? [...handlers.get("close") ?? []] : [];
251
+ handlers.clear();
252
+ host.remove();
253
+ call(closers, []);
254
+ },
255
+ update(next) {
256
+ if (state === "destroyed") return;
257
+ options = {
258
+ ...options,
259
+ ...next
260
+ };
261
+ apply();
262
+ },
263
+ on(event, fn) {
264
+ if (state === "destroyed") return () => {};
265
+ const set = handlers.get(event) ?? /* @__PURE__ */ new Set();
266
+ handlers.set(event, set);
267
+ const handler = fn;
268
+ set.add(handler);
269
+ return () => set.delete(handler);
270
+ }
271
+ };
272
+ host[DISCONNECTED] = () => queueMicrotask(() => {
273
+ if (!host.isConnected) widget.close();
274
+ });
275
+ return widget;
276
+ }
277
+ //#endregion
278
+ export { createFeedback as t };
@@ -0,0 +1,56 @@
1
+
2
+ import { n as Mark, t as FeedbackReport } from "./report-CIgy7uzP.mjs";
3
+ //#region src/capture.d.ts
4
+ type CaptureOptions = {
5
+ mode?: "dom" | "display";
6
+ maxWidth?: number;
7
+ type?: "image/webp" | "image/png";
8
+ quality?: number;
9
+ };
10
+ //#endregion
11
+ //#region src/widget.d.ts
12
+ /** What each event passes to its handlers. */
13
+ type FeedbackEvents = {
14
+ open: [];
15
+ close: [];
16
+ submit: [report: FeedbackReport];
17
+ sent: [report: FeedbackReport];
18
+ error: [error: unknown];
19
+ };
20
+ type FeedbackEvent = keyof FeedbackEvents;
21
+ /** The texts the widget shows, by SPEC §12 key. */
22
+ type FeedbackStrings = Record<"trigger" | "title" | "capturing" | "captureError" | "sendWithout" | "noteLabel" | "notePlaceholder" | "noteRequired" | "categoryLabel" | "pen" | "box" | "arrow" | "undo" | "clear" | "cancel" | "send" | "sending" | "sent" | "error" | "retry", string>;
23
+ type FeedbackOptions = {
24
+ onSubmit?: (report: FeedbackReport) => Promise<void>;
25
+ endpoint?: string;
26
+ headers?: Record<string, string> | (() => Record<string, string>);
27
+ capture?: CaptureOptions;
28
+ tools?: Mark["tool"][];
29
+ categories?: {
30
+ id: string;
31
+ label: string;
32
+ }[];
33
+ requireNote?: boolean;
34
+ redactUrl?: boolean;
35
+ context?: () => Record<string, unknown> | Promise<Record<string, unknown>>;
36
+ trigger?: "button" | "none";
37
+ position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
38
+ shortcut?: string | false;
39
+ strings?: Partial<FeedbackStrings>;
40
+ theme?: "auto" | "light" | "dark";
41
+ css?: string;
42
+ unstyled?: boolean;
43
+ container?: HTMLElement;
44
+ tagName?: string;
45
+ };
46
+ type FeedbackWidget = {
47
+ open(): Promise<void>;
48
+ close(): void;
49
+ destroy(): void;
50
+ /** A key given as undefined goes back to its default. */
51
+ update(options: { [K in keyof FeedbackOptions]?: FeedbackOptions[K] | undefined; }): void;
52
+ on<E extends FeedbackEvent>(event: E, fn: (...args: FeedbackEvents[E]) => void): () => void;
53
+ };
54
+ declare function createFeedback(options?: FeedbackOptions): FeedbackWidget;
55
+ //#endregion
56
+ export { FeedbackWidget as a, FeedbackStrings as i, FeedbackEvents as n, createFeedback as o, FeedbackOptions as r, CaptureOptions as s, FeedbackEvent as t };