@mhosaic/feedback 0.5.4 → 0.6.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.
- package/dist/chunk-LGGKJPFH.mjs +1680 -0
- package/dist/chunk-LGGKJPFH.mjs.map +1 -0
- package/dist/embed.min.js +230 -5
- package/dist/embed.min.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/react.mjs +1 -1
- package/package.json +1 -1
- package/dist/chunk-PFD5VBA3.mjs +0 -892
- package/dist/chunk-PFD5VBA3.mjs.map +0 -1
package/dist/chunk-PFD5VBA3.mjs
DELETED
|
@@ -1,892 +0,0 @@
|
|
|
1
|
-
// src/api/client.ts
|
|
2
|
-
var SCALAR_FIELDS = [
|
|
3
|
-
"description",
|
|
4
|
-
"feedback_type",
|
|
5
|
-
"severity",
|
|
6
|
-
"env",
|
|
7
|
-
"page_url",
|
|
8
|
-
"user_agent",
|
|
9
|
-
"capture_method"
|
|
10
|
-
];
|
|
11
|
-
function createApiClient(options) {
|
|
12
|
-
const endpoint = (options.endpoint ?? "").replace(/\/+$/, "");
|
|
13
|
-
if (!endpoint) {
|
|
14
|
-
throw new Error(
|
|
15
|
-
'[mhosaic-feedback] `endpoint` is required (e.g. "https://feedback.example.com").'
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
const fetcher = options.fetch ?? globalThis.fetch;
|
|
19
|
-
async function submitReport(input) {
|
|
20
|
-
let payload = input;
|
|
21
|
-
if (options.beforeSend) payload = await options.beforeSend(input);
|
|
22
|
-
if (payload === false) throw new Error("Submission cancelled by beforeSend");
|
|
23
|
-
const form = new FormData();
|
|
24
|
-
for (const field of SCALAR_FIELDS) {
|
|
25
|
-
form.append(field, String(payload[field]));
|
|
26
|
-
}
|
|
27
|
-
form.append("technical_context", JSON.stringify(payload.technical_context));
|
|
28
|
-
if (payload.screenshot) form.append("screenshot", payload.screenshot, "screenshot.png");
|
|
29
|
-
if (payload.synthetic) form.append("synthetic", "true");
|
|
30
|
-
const response = await fetcher(`${endpoint}/api/feedback/v1/reports/`, {
|
|
31
|
-
method: "POST",
|
|
32
|
-
headers: { Authorization: `Bearer ${options.apiKey}` },
|
|
33
|
-
body: form
|
|
34
|
-
});
|
|
35
|
-
if (!response.ok) {
|
|
36
|
-
const text = await response.text().catch(() => "");
|
|
37
|
-
throw new Error(`Feedback submit failed: ${response.status} ${text}`);
|
|
38
|
-
}
|
|
39
|
-
return response.json();
|
|
40
|
-
}
|
|
41
|
-
return { submitReport };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// src/capture/urlSanitizer.ts
|
|
45
|
-
var SENSITIVE = /token|key|password|secret|auth|session|sig/i;
|
|
46
|
-
function sanitizeUrl(url) {
|
|
47
|
-
try {
|
|
48
|
-
const isAbsolute = /^https?:\/\//i.test(url);
|
|
49
|
-
const isRootRelative = url.startsWith("/");
|
|
50
|
-
if (!isAbsolute && !isRootRelative) return url;
|
|
51
|
-
const base = typeof window !== "undefined" ? window.location.origin : "http://localhost";
|
|
52
|
-
const u = new URL(url, base);
|
|
53
|
-
const clean = new URLSearchParams();
|
|
54
|
-
u.searchParams.forEach((value, name) => {
|
|
55
|
-
clean.set(name, SENSITIVE.test(name) ? "[redacted]" : value);
|
|
56
|
-
});
|
|
57
|
-
u.search = clean.toString();
|
|
58
|
-
return u.toString();
|
|
59
|
-
} catch {
|
|
60
|
-
return url;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// src/capture/device.ts
|
|
65
|
-
function collectDevice() {
|
|
66
|
-
const nav = navigator;
|
|
67
|
-
const connection = nav.connection?.effectiveType;
|
|
68
|
-
const deviceMemory = nav.deviceMemory;
|
|
69
|
-
const referrer = document.referrer || void 0;
|
|
70
|
-
return {
|
|
71
|
-
viewport: { w: window.innerWidth, h: window.innerHeight, dpr: window.devicePixelRatio || 1 },
|
|
72
|
-
screen: { w: window.screen.width, h: window.screen.height },
|
|
73
|
-
platform: nav.platform,
|
|
74
|
-
language: nav.language,
|
|
75
|
-
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
76
|
-
timezoneOffset: (/* @__PURE__ */ new Date()).getTimezoneOffset(),
|
|
77
|
-
...connection !== void 0 && { connection },
|
|
78
|
-
online: nav.onLine,
|
|
79
|
-
...deviceMemory !== void 0 && { deviceMemory },
|
|
80
|
-
hardwareConcurrency: nav.hardwareConcurrency,
|
|
81
|
-
...referrer !== void 0 && { referrer },
|
|
82
|
-
title: document.title,
|
|
83
|
-
pathname: window.location.pathname
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// src/capture/console.ts
|
|
88
|
-
function safeStringify(arg) {
|
|
89
|
-
if (arg == null) return String(arg);
|
|
90
|
-
if (typeof arg === "string") return arg;
|
|
91
|
-
if (typeof arg === "number" || typeof arg === "boolean") return String(arg);
|
|
92
|
-
if (arg instanceof Error) return `${arg.name}: ${arg.message}`;
|
|
93
|
-
if (arg instanceof Element) return `<${arg.tagName.toLowerCase()}>`;
|
|
94
|
-
try {
|
|
95
|
-
return JSON.stringify(arg, (_k, v) => typeof v === "bigint" ? v.toString() : v);
|
|
96
|
-
} catch {
|
|
97
|
-
try {
|
|
98
|
-
return String(arg);
|
|
99
|
-
} catch {
|
|
100
|
-
return "[unserializable]";
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
function installConsolePatch(buffer) {
|
|
105
|
-
const levels = ["log", "info", "warn", "error", "debug"];
|
|
106
|
-
const originals = {};
|
|
107
|
-
for (const level of levels) {
|
|
108
|
-
const original = console[level];
|
|
109
|
-
if (typeof original !== "function") continue;
|
|
110
|
-
originals[level] = original;
|
|
111
|
-
console[level] = function patched(...args) {
|
|
112
|
-
try {
|
|
113
|
-
const message = args.map(safeStringify).join(" ").slice(0, 2e3);
|
|
114
|
-
const entry = { level, message, ts: Date.now() };
|
|
115
|
-
if (level === "error") {
|
|
116
|
-
const stack = new Error().stack;
|
|
117
|
-
if (stack) entry.stack = stack.split("\n").slice(2, 8).join("\n");
|
|
118
|
-
}
|
|
119
|
-
buffer.push(entry);
|
|
120
|
-
} catch {
|
|
121
|
-
}
|
|
122
|
-
original.apply(console, args);
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
return () => {
|
|
126
|
-
for (const [level, fn] of Object.entries(originals)) {
|
|
127
|
-
console[level] = fn;
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// src/capture/network.ts
|
|
133
|
-
function installFetchPatch(buffer, sanitize) {
|
|
134
|
-
if (typeof window === "undefined" || typeof window.fetch !== "function") return () => {
|
|
135
|
-
};
|
|
136
|
-
const original = window.fetch.bind(window);
|
|
137
|
-
window.fetch = async function patched(input, init) {
|
|
138
|
-
const start = performance.now();
|
|
139
|
-
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
140
|
-
const method = (init?.method || (input instanceof Request ? input.method : "GET")).toUpperCase();
|
|
141
|
-
try {
|
|
142
|
-
const response = await original(input, init);
|
|
143
|
-
buffer.push({ url: sanitize(url), method, status: response.status, durationMs: Math.round(performance.now() - start), ts: Date.now() });
|
|
144
|
-
return response;
|
|
145
|
-
} catch (err) {
|
|
146
|
-
buffer.push({
|
|
147
|
-
url: sanitize(url),
|
|
148
|
-
method,
|
|
149
|
-
status: 0,
|
|
150
|
-
durationMs: Math.round(performance.now() - start),
|
|
151
|
-
ts: Date.now(),
|
|
152
|
-
error: err instanceof Error ? err.message : String(err)
|
|
153
|
-
});
|
|
154
|
-
throw err;
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
return () => {
|
|
158
|
-
window.fetch = original;
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
function installXhrPatch(buffer, sanitize) {
|
|
162
|
-
if (typeof window === "undefined" || typeof window.XMLHttpRequest !== "function") return () => {
|
|
163
|
-
};
|
|
164
|
-
const Original = window.XMLHttpRequest;
|
|
165
|
-
const originalOpen = Original.prototype.open;
|
|
166
|
-
const originalSend = Original.prototype.send;
|
|
167
|
-
Original.prototype.open = function patchedOpen(method, url) {
|
|
168
|
-
this.__mfb = { method: method.toUpperCase(), url: typeof url === "string" ? url : url.toString(), start: performance.now() };
|
|
169
|
-
return originalOpen.apply(this, arguments);
|
|
170
|
-
};
|
|
171
|
-
Original.prototype.send = function patchedSend(body) {
|
|
172
|
-
this.addEventListener("loadend", () => {
|
|
173
|
-
try {
|
|
174
|
-
const ctx = this.__mfb;
|
|
175
|
-
if (!ctx) return;
|
|
176
|
-
buffer.push({
|
|
177
|
-
url: sanitize(ctx.url),
|
|
178
|
-
method: ctx.method,
|
|
179
|
-
status: this.status,
|
|
180
|
-
durationMs: Math.round(performance.now() - ctx.start),
|
|
181
|
-
ts: Date.now()
|
|
182
|
-
});
|
|
183
|
-
} catch {
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
return originalSend.call(this, body ?? null);
|
|
187
|
-
};
|
|
188
|
-
return () => {
|
|
189
|
-
Original.prototype.open = originalOpen;
|
|
190
|
-
Original.prototype.send = originalSend;
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// src/capture/errors.ts
|
|
195
|
-
function installErrorHandlers(buffer) {
|
|
196
|
-
if (typeof window === "undefined") return () => {
|
|
197
|
-
};
|
|
198
|
-
const onError = (e) => {
|
|
199
|
-
const stack = e.error instanceof Error ? e.error.stack : void 0;
|
|
200
|
-
buffer.push({
|
|
201
|
-
message: e.message || "Unknown error",
|
|
202
|
-
...stack !== void 0 && { stack },
|
|
203
|
-
ts: Date.now(),
|
|
204
|
-
source: "window.error"
|
|
205
|
-
});
|
|
206
|
-
};
|
|
207
|
-
const onRejection = (e) => {
|
|
208
|
-
const reason = e.reason;
|
|
209
|
-
const message = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : (() => {
|
|
210
|
-
try {
|
|
211
|
-
return JSON.stringify(reason);
|
|
212
|
-
} catch {
|
|
213
|
-
return String(reason);
|
|
214
|
-
}
|
|
215
|
-
})();
|
|
216
|
-
const stack = reason instanceof Error ? reason.stack : void 0;
|
|
217
|
-
buffer.push({
|
|
218
|
-
message,
|
|
219
|
-
...stack !== void 0 && { stack },
|
|
220
|
-
ts: Date.now(),
|
|
221
|
-
source: "unhandledrejection"
|
|
222
|
-
});
|
|
223
|
-
};
|
|
224
|
-
window.addEventListener("error", onError);
|
|
225
|
-
window.addEventListener("unhandledrejection", onRejection);
|
|
226
|
-
return () => {
|
|
227
|
-
window.removeEventListener("error", onError);
|
|
228
|
-
window.removeEventListener("unhandledrejection", onRejection);
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// src/capture/performance.ts
|
|
233
|
-
function createPerformanceCollector(slowResourceMs = 1e3) {
|
|
234
|
-
const longTasks = [];
|
|
235
|
-
const slowResources = [];
|
|
236
|
-
let observer = null;
|
|
237
|
-
if (typeof PerformanceObserver !== "undefined") {
|
|
238
|
-
try {
|
|
239
|
-
observer = new PerformanceObserver((list) => {
|
|
240
|
-
for (const entry of list.getEntries()) {
|
|
241
|
-
if (entry.entryType === "longtask") {
|
|
242
|
-
longTasks.push({ duration: entry.duration, startTime: entry.startTime });
|
|
243
|
-
while (longTasks.length > 20) longTasks.shift();
|
|
244
|
-
} else if (entry.entryType === "resource") {
|
|
245
|
-
const e = entry;
|
|
246
|
-
if (e.duration > slowResourceMs) {
|
|
247
|
-
slowResources.push({ name: e.name, duration: e.duration, initiatorType: e.initiatorType });
|
|
248
|
-
while (slowResources.length > 20) slowResources.shift();
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
observer.observe({ entryTypes: ["longtask", "resource"] });
|
|
254
|
-
} catch {
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return {
|
|
258
|
-
snapshot() {
|
|
259
|
-
const nav = typeof performance !== "undefined" ? performance.getEntriesByType("navigation")[0] : void 0;
|
|
260
|
-
const navigation = nav ? { type: nav.type, duration: nav.duration } : void 0;
|
|
261
|
-
return {
|
|
262
|
-
...navigation !== void 0 && { navigation },
|
|
263
|
-
longTasks: longTasks.slice(),
|
|
264
|
-
slowResources: slowResources.slice()
|
|
265
|
-
};
|
|
266
|
-
},
|
|
267
|
-
dispose() {
|
|
268
|
-
observer?.disconnect();
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// src/capture/ringBuffer.ts
|
|
274
|
-
var RingBuffer = class {
|
|
275
|
-
constructor(max) {
|
|
276
|
-
this.max = max;
|
|
277
|
-
}
|
|
278
|
-
max;
|
|
279
|
-
items = [];
|
|
280
|
-
push(item) {
|
|
281
|
-
this.items.push(item);
|
|
282
|
-
while (this.items.length > this.max) this.items.shift();
|
|
283
|
-
}
|
|
284
|
-
snapshot() {
|
|
285
|
-
return this.items.slice();
|
|
286
|
-
}
|
|
287
|
-
clear() {
|
|
288
|
-
this.items.length = 0;
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
// src/capture/index.ts
|
|
293
|
-
function installCapture(options = {}) {
|
|
294
|
-
const { maxConsole = 50, maxNetwork = 50, maxErrors = 20 } = options;
|
|
295
|
-
const sanitize = options.sanitizeUrl ?? sanitizeUrl;
|
|
296
|
-
const consoleBuf = new RingBuffer(maxConsole);
|
|
297
|
-
const networkBuf = new RingBuffer(maxNetwork);
|
|
298
|
-
const errorBuf = new RingBuffer(maxErrors);
|
|
299
|
-
const uninstallConsole = installConsolePatch(consoleBuf);
|
|
300
|
-
const uninstallFetch = installFetchPatch(networkBuf, sanitize);
|
|
301
|
-
const uninstallXhr = installXhrPatch(networkBuf, sanitize);
|
|
302
|
-
const uninstallErrors = installErrorHandlers(errorBuf);
|
|
303
|
-
const perf = createPerformanceCollector();
|
|
304
|
-
return {
|
|
305
|
-
snapshot() {
|
|
306
|
-
return {
|
|
307
|
-
consoleLogs: consoleBuf.snapshot(),
|
|
308
|
-
networkRequests: networkBuf.snapshot(),
|
|
309
|
-
errors: errorBuf.snapshot(),
|
|
310
|
-
device: collectDevice(),
|
|
311
|
-
capturedAt: Date.now()
|
|
312
|
-
};
|
|
313
|
-
},
|
|
314
|
-
clear() {
|
|
315
|
-
consoleBuf.clear();
|
|
316
|
-
networkBuf.clear();
|
|
317
|
-
errorBuf.clear();
|
|
318
|
-
},
|
|
319
|
-
dispose() {
|
|
320
|
-
uninstallConsole();
|
|
321
|
-
uninstallFetch();
|
|
322
|
-
uninstallXhr();
|
|
323
|
-
uninstallErrors();
|
|
324
|
-
perf.dispose();
|
|
325
|
-
}
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// src/screenshot/index.ts
|
|
330
|
-
function isMaskedElement(el) {
|
|
331
|
-
return el.hasAttribute("data-mfb-mask") || el.classList.contains("mfb-mask");
|
|
332
|
-
}
|
|
333
|
-
function prepareMaskMatcher(selectors) {
|
|
334
|
-
const joined = selectors.join(",").trim();
|
|
335
|
-
if (!joined) return isMaskedElement;
|
|
336
|
-
return (el) => isMaskedElement(el) || el.matches(joined);
|
|
337
|
-
}
|
|
338
|
-
async function takeScreenshot(target, options = {}) {
|
|
339
|
-
if (typeof document === "undefined") return null;
|
|
340
|
-
try {
|
|
341
|
-
const html2canvas = (await import("html2canvas-pro")).default;
|
|
342
|
-
const matcher = prepareMaskMatcher(options.mask ?? []);
|
|
343
|
-
const canvas = await html2canvas(target, {
|
|
344
|
-
ignoreElements: (el) => matcher(el),
|
|
345
|
-
useCORS: true,
|
|
346
|
-
logging: false,
|
|
347
|
-
backgroundColor: null
|
|
348
|
-
});
|
|
349
|
-
return await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
|
|
350
|
-
} catch {
|
|
351
|
-
return null;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// src/widget/i18n.ts
|
|
356
|
-
var DEFAULT_STRINGS = {
|
|
357
|
-
"fab.label": "Send feedback",
|
|
358
|
-
"form.title": "Send feedback",
|
|
359
|
-
"form.description.label": "What happened?",
|
|
360
|
-
"form.description.placeholder": "Describe the issue or idea in one or two sentences.",
|
|
361
|
-
"form.type.label": "Type",
|
|
362
|
-
"form.severity.label": "Severity",
|
|
363
|
-
"form.submit": "Send",
|
|
364
|
-
"form.cancel": "Cancel",
|
|
365
|
-
"form.close": "Close",
|
|
366
|
-
"form.submitting": "Sending\u2026",
|
|
367
|
-
"form.success": "Thanks \u2014 your feedback was sent.",
|
|
368
|
-
"form.error": "Could not send. Please try again.",
|
|
369
|
-
"type.bug": "Bug",
|
|
370
|
-
"type.feature": "Feature request",
|
|
371
|
-
"type.question": "Question",
|
|
372
|
-
"type.praise": "Praise",
|
|
373
|
-
"type.typo": "Typo",
|
|
374
|
-
"severity.blocker": "Blocker",
|
|
375
|
-
"severity.high": "High",
|
|
376
|
-
"severity.medium": "Medium",
|
|
377
|
-
"severity.low": "Low"
|
|
378
|
-
};
|
|
379
|
-
var FRENCH_STRINGS = {
|
|
380
|
-
"fab.label": "Envoyer un commentaire",
|
|
381
|
-
"form.title": "Envoyer un commentaire",
|
|
382
|
-
"form.description.label": "Qu\u2019est-il arriv\xE9 ?",
|
|
383
|
-
"form.description.placeholder": "D\xE9crivez le probl\xE8me ou l\u2019id\xE9e en une ou deux phrases.",
|
|
384
|
-
"form.type.label": "Type",
|
|
385
|
-
"form.severity.label": "S\xE9v\xE9rit\xE9",
|
|
386
|
-
"form.submit": "Envoyer",
|
|
387
|
-
"form.cancel": "Annuler",
|
|
388
|
-
"form.close": "Fermer",
|
|
389
|
-
"form.submitting": "Envoi\u2026",
|
|
390
|
-
"form.success": "Merci \u2014 votre commentaire a \xE9t\xE9 envoy\xE9.",
|
|
391
|
-
"form.error": "\xC9chec d\u2019envoi. Veuillez r\xE9essayer.",
|
|
392
|
-
"type.bug": "Bogue",
|
|
393
|
-
"type.feature": "Suggestion",
|
|
394
|
-
"type.question": "Question",
|
|
395
|
-
"type.praise": "Compliment",
|
|
396
|
-
"type.typo": "Coquille",
|
|
397
|
-
"severity.blocker": "Bloquant",
|
|
398
|
-
"severity.high": "\xC9lev\xE9e",
|
|
399
|
-
"severity.medium": "Moyenne",
|
|
400
|
-
"severity.low": "Faible"
|
|
401
|
-
};
|
|
402
|
-
var LOCALE_PACKS = {
|
|
403
|
-
fr: FRENCH_STRINGS
|
|
404
|
-
};
|
|
405
|
-
function packFor(locale) {
|
|
406
|
-
if (!locale) return null;
|
|
407
|
-
const tag = locale.toLowerCase().split(/[-_]/)[0];
|
|
408
|
-
return LOCALE_PACKS[tag] ?? null;
|
|
409
|
-
}
|
|
410
|
-
function resolveStrings(overrides, options = {}) {
|
|
411
|
-
const localePack = packFor(options.locale) ?? {};
|
|
412
|
-
return {
|
|
413
|
-
...DEFAULT_STRINGS,
|
|
414
|
-
...localePack,
|
|
415
|
-
...overrides
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// src/widget/mount.tsx
|
|
420
|
-
import { h, render } from "preact";
|
|
421
|
-
import { useCallback } from "preact/hooks";
|
|
422
|
-
|
|
423
|
-
// src/widget/Fab.tsx
|
|
424
|
-
import { jsx } from "preact/jsx-runtime";
|
|
425
|
-
function Fab({ label, onClick }) {
|
|
426
|
-
return /* @__PURE__ */ jsx("button", { type: "button", class: "fab", "aria-label": label, onClick, children: "\u{1F4AC}" });
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// src/widget/Form.tsx
|
|
430
|
-
import { useState } from "preact/hooks";
|
|
431
|
-
import { jsx as jsx2, jsxs } from "preact/jsx-runtime";
|
|
432
|
-
var TYPES = ["bug", "feature", "question", "praise", "typo"];
|
|
433
|
-
var SEVERITIES = ["blocker", "high", "medium", "low"];
|
|
434
|
-
function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
|
|
435
|
-
const [description, setDescription] = useState("");
|
|
436
|
-
const [feedbackType, setFeedbackType] = useState("bug");
|
|
437
|
-
const [severity, setSeverity] = useState("medium");
|
|
438
|
-
const [localError, setLocalError] = useState("");
|
|
439
|
-
const submitting = status === "submitting";
|
|
440
|
-
const submitLabel = submitting ? strings["form.submitting"] : strings["form.submit"];
|
|
441
|
-
const handleSubmit = (e) => {
|
|
442
|
-
e.preventDefault();
|
|
443
|
-
if (!description.trim()) {
|
|
444
|
-
setLocalError(strings["form.description.placeholder"]);
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
setLocalError("");
|
|
448
|
-
onSubmit({ description: description.trim(), feedback_type: feedbackType, severity });
|
|
449
|
-
};
|
|
450
|
-
return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, children: [
|
|
451
|
-
/* @__PURE__ */ jsx2("h2", { children: strings["form.title"] }),
|
|
452
|
-
/* @__PURE__ */ jsxs("div", { class: "field", children: [
|
|
453
|
-
/* @__PURE__ */ jsx2("label", { for: "mfb-desc", children: strings["form.description.label"] }),
|
|
454
|
-
/* @__PURE__ */ jsx2(
|
|
455
|
-
"textarea",
|
|
456
|
-
{
|
|
457
|
-
id: "mfb-desc",
|
|
458
|
-
value: description,
|
|
459
|
-
placeholder: strings["form.description.placeholder"],
|
|
460
|
-
onInput: (e) => setDescription(e.target.value)
|
|
461
|
-
}
|
|
462
|
-
)
|
|
463
|
-
] }),
|
|
464
|
-
/* @__PURE__ */ jsxs("div", { class: "row", children: [
|
|
465
|
-
/* @__PURE__ */ jsxs("div", { class: "field", children: [
|
|
466
|
-
/* @__PURE__ */ jsx2("label", { for: "mfb-type", children: strings["form.type.label"] }),
|
|
467
|
-
/* @__PURE__ */ jsx2(
|
|
468
|
-
"select",
|
|
469
|
-
{
|
|
470
|
-
id: "mfb-type",
|
|
471
|
-
value: feedbackType,
|
|
472
|
-
onChange: (e) => setFeedbackType(e.target.value),
|
|
473
|
-
children: TYPES.map((t) => /* @__PURE__ */ jsx2("option", { value: t, children: strings[`type.${t}`] }))
|
|
474
|
-
}
|
|
475
|
-
)
|
|
476
|
-
] }),
|
|
477
|
-
/* @__PURE__ */ jsxs("div", { class: "field", children: [
|
|
478
|
-
/* @__PURE__ */ jsx2("label", { for: "mfb-sev", children: strings["form.severity.label"] }),
|
|
479
|
-
/* @__PURE__ */ jsx2(
|
|
480
|
-
"select",
|
|
481
|
-
{
|
|
482
|
-
id: "mfb-sev",
|
|
483
|
-
value: severity,
|
|
484
|
-
onChange: (e) => setSeverity(e.target.value),
|
|
485
|
-
children: SEVERITIES.map((s) => /* @__PURE__ */ jsx2("option", { value: s, children: strings[`severity.${s}`] }))
|
|
486
|
-
}
|
|
487
|
-
)
|
|
488
|
-
] })
|
|
489
|
-
] }),
|
|
490
|
-
localError && /* @__PURE__ */ jsx2("div", { class: "error", children: localError }),
|
|
491
|
-
status === "error" && errorMessage && /* @__PURE__ */ jsx2("div", { class: "error", children: errorMessage }),
|
|
492
|
-
status === "success" && /* @__PURE__ */ jsx2("div", { class: "success", children: strings["form.success"] }),
|
|
493
|
-
/* @__PURE__ */ jsxs("div", { class: "actions", children: [
|
|
494
|
-
/* @__PURE__ */ jsx2("button", { type: "button", class: "btn", onClick: onCancel, disabled: submitting, children: strings["form.cancel"] }),
|
|
495
|
-
/* @__PURE__ */ jsx2("button", { type: "submit", class: "btn btn--primary", disabled: submitting, children: submitLabel })
|
|
496
|
-
] })
|
|
497
|
-
] });
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// src/widget/Modal.tsx
|
|
501
|
-
import { useEffect, useRef } from "preact/hooks";
|
|
502
|
-
import { jsx as jsx3, jsxs as jsxs2 } from "preact/jsx-runtime";
|
|
503
|
-
function Modal({ onDismiss, children, closeLabel = "Close" }) {
|
|
504
|
-
const modalRef = useRef(null);
|
|
505
|
-
const previouslyFocused = useRef(null);
|
|
506
|
-
useEffect(() => {
|
|
507
|
-
previouslyFocused.current = document.activeElement;
|
|
508
|
-
const onKey = (e) => {
|
|
509
|
-
if (e.key === "Escape") {
|
|
510
|
-
e.stopPropagation();
|
|
511
|
-
onDismiss();
|
|
512
|
-
}
|
|
513
|
-
};
|
|
514
|
-
window.addEventListener("keydown", onKey);
|
|
515
|
-
const first = modalRef.current?.querySelector(
|
|
516
|
-
"textarea, input, select, button"
|
|
517
|
-
);
|
|
518
|
-
first?.focus();
|
|
519
|
-
return () => {
|
|
520
|
-
window.removeEventListener("keydown", onKey);
|
|
521
|
-
const prev = previouslyFocused.current;
|
|
522
|
-
if (prev && typeof prev.focus === "function") prev.focus();
|
|
523
|
-
};
|
|
524
|
-
}, [onDismiss]);
|
|
525
|
-
return /* @__PURE__ */ jsx3(
|
|
526
|
-
"div",
|
|
527
|
-
{
|
|
528
|
-
class: "backdrop",
|
|
529
|
-
role: "presentation",
|
|
530
|
-
onClick: (e) => {
|
|
531
|
-
if (e.target === e.currentTarget) onDismiss();
|
|
532
|
-
},
|
|
533
|
-
children: /* @__PURE__ */ jsxs2("div", { ref: modalRef, class: "modal", role: "dialog", "aria-modal": "true", children: [
|
|
534
|
-
/* @__PURE__ */ jsx3(
|
|
535
|
-
"button",
|
|
536
|
-
{
|
|
537
|
-
type: "button",
|
|
538
|
-
class: "modal-close",
|
|
539
|
-
"aria-label": closeLabel,
|
|
540
|
-
onClick: onDismiss,
|
|
541
|
-
children: "\xD7"
|
|
542
|
-
}
|
|
543
|
-
),
|
|
544
|
-
children
|
|
545
|
-
] })
|
|
546
|
-
}
|
|
547
|
-
);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// src/widget/styles.ts
|
|
551
|
-
var WIDGET_STYLES = `
|
|
552
|
-
:host {
|
|
553
|
-
--mfb-accent: #3b82f6;
|
|
554
|
-
--mfb-accent-contrast: #ffffff;
|
|
555
|
-
--mfb-bg: #ffffff;
|
|
556
|
-
--mfb-surface: #f9fafb;
|
|
557
|
-
--mfb-text: #0a0a0a;
|
|
558
|
-
--mfb-text-muted: #6b7280;
|
|
559
|
-
--mfb-border: #e5e7eb;
|
|
560
|
-
--mfb-radius: 8px;
|
|
561
|
-
--mfb-font: system-ui, -apple-system, sans-serif;
|
|
562
|
-
--mfb-z-index: 2147483640;
|
|
563
|
-
|
|
564
|
-
all: initial;
|
|
565
|
-
font-family: var(--mfb-font);
|
|
566
|
-
color: var(--mfb-text);
|
|
567
|
-
position: fixed;
|
|
568
|
-
z-index: var(--mfb-z-index);
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
@media (prefers-color-scheme: dark) {
|
|
572
|
-
:host {
|
|
573
|
-
--mfb-bg: #111827;
|
|
574
|
-
--mfb-surface: #1f2937;
|
|
575
|
-
--mfb-text: #f9fafb;
|
|
576
|
-
--mfb-text-muted: #9ca3af;
|
|
577
|
-
--mfb-border: #374151;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
.fab {
|
|
582
|
-
position: fixed;
|
|
583
|
-
bottom: 24px;
|
|
584
|
-
right: 24px;
|
|
585
|
-
width: 52px;
|
|
586
|
-
height: 52px;
|
|
587
|
-
border-radius: 999px;
|
|
588
|
-
background: var(--mfb-accent);
|
|
589
|
-
color: var(--mfb-accent-contrast);
|
|
590
|
-
border: none;
|
|
591
|
-
cursor: pointer;
|
|
592
|
-
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
|
|
593
|
-
font-size: 22px;
|
|
594
|
-
display: grid;
|
|
595
|
-
place-items: center;
|
|
596
|
-
transition: transform 120ms ease, box-shadow 120ms ease;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
.fab:hover { transform: translateY(-1px); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.24); }
|
|
600
|
-
.fab:active { transform: translateY(0) scale(0.96); box-shadow: 0 3px 10px rgba(0, 0, 0, 0.22); }
|
|
601
|
-
.fab:focus-visible { outline: 2px solid #fff; outline-offset: 3px; box-shadow: 0 0 0 4px var(--mfb-accent), 0 4px 14px rgba(0, 0, 0, 0.18); }
|
|
602
|
-
@media (prefers-reduced-motion: reduce) { .fab { transition: none; } .fab:hover, .fab:active { transform: none; } }
|
|
603
|
-
|
|
604
|
-
.backdrop {
|
|
605
|
-
position: fixed;
|
|
606
|
-
inset: 0;
|
|
607
|
-
background: rgba(0, 0, 0, 0.45);
|
|
608
|
-
display: grid;
|
|
609
|
-
place-items: center;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
.modal {
|
|
613
|
-
background: var(--mfb-bg);
|
|
614
|
-
border-radius: calc(var(--mfb-radius) * 1.5);
|
|
615
|
-
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.25);
|
|
616
|
-
width: min(420px, 92vw);
|
|
617
|
-
padding: 20px;
|
|
618
|
-
display: flex;
|
|
619
|
-
flex-direction: column;
|
|
620
|
-
gap: 12px;
|
|
621
|
-
position: relative;
|
|
622
|
-
/* Cap modal height on short viewports (mobile landscape, tiny laptops)
|
|
623
|
-
so the form scrolls inside the modal rather than overflowing the
|
|
624
|
-
viewport with no way to reach the actions row. */
|
|
625
|
-
max-height: calc(100vh - 32px);
|
|
626
|
-
overflow-y: auto;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
.modal h2 { margin: 0 0 4px; font-size: 18px; font-weight: 600; padding-right: 28px; }
|
|
630
|
-
|
|
631
|
-
.modal-close {
|
|
632
|
-
position: absolute;
|
|
633
|
-
top: 8px;
|
|
634
|
-
right: 8px;
|
|
635
|
-
width: 32px;
|
|
636
|
-
height: 32px;
|
|
637
|
-
display: grid;
|
|
638
|
-
place-items: center;
|
|
639
|
-
background: transparent;
|
|
640
|
-
border: none;
|
|
641
|
-
border-radius: var(--mfb-radius);
|
|
642
|
-
color: var(--mfb-text-muted);
|
|
643
|
-
font: inherit;
|
|
644
|
-
font-size: 22px;
|
|
645
|
-
line-height: 1;
|
|
646
|
-
cursor: pointer;
|
|
647
|
-
}
|
|
648
|
-
.modal-close:hover { background: var(--mfb-surface); color: var(--mfb-text); }
|
|
649
|
-
.modal-close:focus-visible { outline: 2px solid var(--mfb-accent); outline-offset: 2px; }
|
|
650
|
-
|
|
651
|
-
.field { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }
|
|
652
|
-
|
|
653
|
-
.field label { color: var(--mfb-text-muted); }
|
|
654
|
-
|
|
655
|
-
.field input, .field select, .field textarea {
|
|
656
|
-
font-family: inherit;
|
|
657
|
-
font-size: 14px;
|
|
658
|
-
color: inherit;
|
|
659
|
-
padding: 8px 10px;
|
|
660
|
-
border: 1px solid var(--mfb-border);
|
|
661
|
-
border-radius: var(--mfb-radius);
|
|
662
|
-
background: var(--mfb-surface);
|
|
663
|
-
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
.field input:hover, .field select:hover, .field textarea:hover { border-color: var(--mfb-text-muted); }
|
|
667
|
-
.field input:focus, .field select:focus, .field textarea:focus {
|
|
668
|
-
outline: none;
|
|
669
|
-
border-color: var(--mfb-accent);
|
|
670
|
-
box-shadow: 0 0 0 3px color-mix(in srgb, var(--mfb-accent) 22%, transparent);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
.field select {
|
|
674
|
-
-webkit-appearance: none;
|
|
675
|
-
-moz-appearance: none;
|
|
676
|
-
appearance: none;
|
|
677
|
-
padding-right: 28px;
|
|
678
|
-
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='none' stroke='%236b7280' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M1 1l4 4 4-4'/></svg>");
|
|
679
|
-
background-repeat: no-repeat;
|
|
680
|
-
background-position: right 10px center;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
.field textarea { min-height: 88px; resize: vertical; }
|
|
684
|
-
|
|
685
|
-
.row { display: flex; gap: 8px; }
|
|
686
|
-
.row > * { flex: 1; }
|
|
687
|
-
|
|
688
|
-
.actions { display: flex; gap: 8px; justify-content: flex-end; padding-top: 8px; }
|
|
689
|
-
|
|
690
|
-
.btn {
|
|
691
|
-
padding: 8px 14px;
|
|
692
|
-
border-radius: var(--mfb-radius);
|
|
693
|
-
border: 1px solid var(--mfb-border);
|
|
694
|
-
background: var(--mfb-bg);
|
|
695
|
-
color: var(--mfb-text);
|
|
696
|
-
font: inherit;
|
|
697
|
-
cursor: pointer;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
.btn--primary {
|
|
701
|
-
background: var(--mfb-accent);
|
|
702
|
-
color: var(--mfb-accent-contrast);
|
|
703
|
-
border-color: var(--mfb-accent);
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
.btn[disabled] { opacity: 0.6; cursor: not-allowed; }
|
|
707
|
-
|
|
708
|
-
.error { color: #dc2626; font-size: 13px; }
|
|
709
|
-
.success { color: #059669; font-size: 13px; }
|
|
710
|
-
`;
|
|
711
|
-
|
|
712
|
-
// src/widget/mount.tsx
|
|
713
|
-
import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "preact/jsx-runtime";
|
|
714
|
-
function mountWidget(options) {
|
|
715
|
-
const shadow = options.host.attachShadow({ mode: "open" });
|
|
716
|
-
const style = document.createElement("style");
|
|
717
|
-
style.textContent = WIDGET_STYLES;
|
|
718
|
-
shadow.appendChild(style);
|
|
719
|
-
const mountPoint = document.createElement("div");
|
|
720
|
-
shadow.appendChild(mountPoint);
|
|
721
|
-
let currentState = { open: false, status: "idle" };
|
|
722
|
-
function rerender(state) {
|
|
723
|
-
currentState = state;
|
|
724
|
-
render(h(Root, { state }), mountPoint);
|
|
725
|
-
}
|
|
726
|
-
function Root({ state }) {
|
|
727
|
-
const handleSubmit = useCallback(async (values) => {
|
|
728
|
-
rerender({ open: true, status: "submitting" });
|
|
729
|
-
try {
|
|
730
|
-
await options.onSubmit(values);
|
|
731
|
-
rerender({ open: true, status: "success" });
|
|
732
|
-
setTimeout(() => rerender({ open: false, status: "idle" }), 1200);
|
|
733
|
-
} catch (err) {
|
|
734
|
-
rerender({ open: true, status: "error", error: err instanceof Error ? err.message : String(err) });
|
|
735
|
-
}
|
|
736
|
-
}, []);
|
|
737
|
-
return /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
738
|
-
options.showFAB && /* @__PURE__ */ jsx4(
|
|
739
|
-
Fab,
|
|
740
|
-
{
|
|
741
|
-
label: options.strings["fab.label"],
|
|
742
|
-
onClick: () => rerender({ ...currentState, open: true })
|
|
743
|
-
}
|
|
744
|
-
),
|
|
745
|
-
state.open && /* @__PURE__ */ jsx4(
|
|
746
|
-
Modal,
|
|
747
|
-
{
|
|
748
|
-
onDismiss: () => rerender({ open: false, status: "idle" }),
|
|
749
|
-
closeLabel: options.strings["form.close"],
|
|
750
|
-
children: /* @__PURE__ */ jsx4(
|
|
751
|
-
Form,
|
|
752
|
-
{
|
|
753
|
-
strings: options.strings,
|
|
754
|
-
onSubmit: handleSubmit,
|
|
755
|
-
onCancel: () => rerender({ open: false, status: "idle" }),
|
|
756
|
-
status: state.status,
|
|
757
|
-
...state.error !== void 0 && { errorMessage: state.error }
|
|
758
|
-
}
|
|
759
|
-
)
|
|
760
|
-
}
|
|
761
|
-
)
|
|
762
|
-
] });
|
|
763
|
-
}
|
|
764
|
-
rerender(currentState);
|
|
765
|
-
return {
|
|
766
|
-
open() {
|
|
767
|
-
rerender({ ...currentState, open: true });
|
|
768
|
-
},
|
|
769
|
-
close() {
|
|
770
|
-
rerender({ ...currentState, open: false, status: "idle" });
|
|
771
|
-
},
|
|
772
|
-
dispose() {
|
|
773
|
-
render(null, mountPoint);
|
|
774
|
-
options.host.innerHTML = "";
|
|
775
|
-
}
|
|
776
|
-
};
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
// src/core.ts
|
|
780
|
-
function createFeedback(config) {
|
|
781
|
-
const env = config.env ?? "prod";
|
|
782
|
-
const locale = config.locale ?? (typeof navigator !== "undefined" ? navigator.language : void 0);
|
|
783
|
-
const strings = resolveStrings(
|
|
784
|
-
config.translations ?? {},
|
|
785
|
-
locale !== void 0 ? { locale } : {}
|
|
786
|
-
);
|
|
787
|
-
const capture = installCapture({
|
|
788
|
-
...config.sanitizeUrl !== void 0 && { sanitizeUrl: config.sanitizeUrl }
|
|
789
|
-
});
|
|
790
|
-
const api = createApiClient({
|
|
791
|
-
apiKey: config.apiKey,
|
|
792
|
-
endpoint: config.endpoint,
|
|
793
|
-
...config.fetchImpl !== void 0 && { fetch: config.fetchImpl },
|
|
794
|
-
...config.beforeSend !== void 0 && { beforeSend: config.beforeSend }
|
|
795
|
-
});
|
|
796
|
-
let user = config.user;
|
|
797
|
-
let metadata = config.metadata ?? {};
|
|
798
|
-
const transformers = [];
|
|
799
|
-
const host = document.createElement("div");
|
|
800
|
-
host.className = "mhosaic-feedback";
|
|
801
|
-
if (config.attachTo) {
|
|
802
|
-
const attach = typeof config.attachTo === "string" ? document.querySelector(config.attachTo) : config.attachTo;
|
|
803
|
-
attach?.appendChild(host);
|
|
804
|
-
} else {
|
|
805
|
-
document.body.appendChild(host);
|
|
806
|
-
}
|
|
807
|
-
async function buildAndSubmit(values) {
|
|
808
|
-
const screenshot = values.synthetic ? void 0 : await takeScreenshot(document.body, { mask: [".mhosaic-feedback", "[data-mfb-mask]"] });
|
|
809
|
-
const technical_context = capture.snapshot();
|
|
810
|
-
if (user) technical_context.user = user;
|
|
811
|
-
if (metadata && Object.keys(metadata).length > 0) {
|
|
812
|
-
technical_context.metadata = { ...metadata };
|
|
813
|
-
}
|
|
814
|
-
const payload = {
|
|
815
|
-
description: values.description,
|
|
816
|
-
feedback_type: values.feedback_type ?? "bug",
|
|
817
|
-
severity: values.severity ?? "medium",
|
|
818
|
-
env,
|
|
819
|
-
page_url: window.location.href,
|
|
820
|
-
user_agent: navigator.userAgent,
|
|
821
|
-
capture_method: screenshot ? "html2canvas" : "none",
|
|
822
|
-
technical_context
|
|
823
|
-
};
|
|
824
|
-
if (screenshot) payload.screenshot = screenshot;
|
|
825
|
-
if (values.synthetic) payload.synthetic = true;
|
|
826
|
-
let finalPayload = payload;
|
|
827
|
-
for (const t of transformers) finalPayload = await t(finalPayload);
|
|
828
|
-
try {
|
|
829
|
-
const result = await api.submitReport(finalPayload);
|
|
830
|
-
config.onSubmitSuccess?.(result);
|
|
831
|
-
capture.clear();
|
|
832
|
-
return result;
|
|
833
|
-
} catch (err) {
|
|
834
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
835
|
-
config.onError?.(error);
|
|
836
|
-
throw error;
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
const handle = mountWidget({
|
|
840
|
-
host,
|
|
841
|
-
strings,
|
|
842
|
-
showFAB: config.showFAB ?? true,
|
|
843
|
-
onSubmit: async (values) => {
|
|
844
|
-
await buildAndSubmit(values);
|
|
845
|
-
}
|
|
846
|
-
});
|
|
847
|
-
const instance = {
|
|
848
|
-
show() {
|
|
849
|
-
handle.open();
|
|
850
|
-
},
|
|
851
|
-
hide() {
|
|
852
|
-
handle.close();
|
|
853
|
-
},
|
|
854
|
-
open(opts) {
|
|
855
|
-
handle.open();
|
|
856
|
-
void opts;
|
|
857
|
-
},
|
|
858
|
-
async submit(partial) {
|
|
859
|
-
return buildAndSubmit({
|
|
860
|
-
description: partial.description,
|
|
861
|
-
...partial.feedback_type !== void 0 && { feedback_type: partial.feedback_type },
|
|
862
|
-
...partial.severity !== void 0 && { severity: partial.severity },
|
|
863
|
-
...partial.synthetic !== void 0 && { synthetic: partial.synthetic }
|
|
864
|
-
});
|
|
865
|
-
},
|
|
866
|
-
identify(u) {
|
|
867
|
-
user = u;
|
|
868
|
-
},
|
|
869
|
-
setMetadata(kv) {
|
|
870
|
-
metadata = { ...metadata, ...kv };
|
|
871
|
-
},
|
|
872
|
-
shutdown() {
|
|
873
|
-
handle.dispose();
|
|
874
|
-
capture.dispose();
|
|
875
|
-
host.remove();
|
|
876
|
-
const w = window;
|
|
877
|
-
if (w.mhosaicFeedback === instance) {
|
|
878
|
-
delete w.mhosaicFeedback;
|
|
879
|
-
}
|
|
880
|
-
},
|
|
881
|
-
_registerTransformer(fn) {
|
|
882
|
-
transformers.push(fn);
|
|
883
|
-
}
|
|
884
|
-
};
|
|
885
|
-
window.mhosaicFeedback = instance;
|
|
886
|
-
return instance;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
export {
|
|
890
|
-
createFeedback
|
|
891
|
-
};
|
|
892
|
-
//# sourceMappingURL=chunk-PFD5VBA3.mjs.map
|