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