@diegotsi/flint-react 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.
package/dist/index.js ADDED
@@ -0,0 +1,1088 @@
1
+ // src/FlintWidget.tsx
2
+ import { useState as useState2, useEffect as useEffect2, useRef as useRef2 } from "react";
3
+ import { I18nextProvider, useTranslation as useTranslation2 } from "react-i18next";
4
+
5
+ // src/FlintModal.tsx
6
+ import { useRef, useState, useEffect, useCallback } from "react";
7
+ import { useTranslation } from "react-i18next";
8
+
9
+ // src/api.ts
10
+ import { gzipSync } from "fflate";
11
+ async function submitReport(serverUrl, projectKey, payload, screenshot) {
12
+ const url = `${serverUrl.replace(/\/$/, "")}/api/v1/bug-reports`;
13
+ let body;
14
+ let headers = {
15
+ "x-project-key": projectKey
16
+ };
17
+ if (screenshot) {
18
+ const form = new FormData();
19
+ form.append("reporterId", payload.reporterId);
20
+ form.append("reporterName", payload.reporterName);
21
+ form.append("description", payload.description);
22
+ if (payload.expectedBehavior) form.append("expectedBehavior", payload.expectedBehavior);
23
+ if (payload.stepsToReproduce) form.append("stepsToReproduce", JSON.stringify(payload.stepsToReproduce));
24
+ if (payload.externalReplayUrl) form.append("externalReplayUrl", payload.externalReplayUrl);
25
+ if (payload.additionalContext) form.append("additionalContext", payload.additionalContext);
26
+ form.append("severity", payload.severity);
27
+ if (payload.url) form.append("url", payload.url);
28
+ if (payload.meta) form.append("meta", JSON.stringify(payload.meta));
29
+ form.append("screenshot", screenshot);
30
+ body = form;
31
+ } else {
32
+ body = JSON.stringify(payload);
33
+ headers["Content-Type"] = "application/json";
34
+ }
35
+ const res = await fetch(url, { method: "POST", headers, body });
36
+ if (!res.ok) {
37
+ const err = await res.json().catch(() => ({ error: "Unknown error" }));
38
+ throw new Error(err.error ?? `HTTP ${res.status}`);
39
+ }
40
+ return res.json();
41
+ }
42
+ async function submitReplay(serverUrl, projectKey, reportId, events) {
43
+ const json = JSON.stringify(events);
44
+ const encoded = new TextEncoder().encode(json);
45
+ const compressed = gzipSync(encoded);
46
+ const url = `${serverUrl.replace(/\/$/, "")}/api/v1/bug-reports/${reportId}/replay`;
47
+ await fetch(url, {
48
+ method: "POST",
49
+ headers: {
50
+ "x-project-key": projectKey,
51
+ "Content-Type": "application/octet-stream"
52
+ },
53
+ body: compressed.buffer
54
+ });
55
+ }
56
+
57
+ // src/theme.ts
58
+ var light = {
59
+ background: "rgba(255,255,255,0.90)",
60
+ backgroundSecondary: "rgba(249,250,251,0.75)",
61
+ accent: "#2563eb",
62
+ accentHover: "#1d4ed8",
63
+ text: "#111827",
64
+ textMuted: "#6b7280",
65
+ border: "rgba(255,255,255,0.9)",
66
+ shadow: "0 32px 80px rgba(0,0,0,0.18), 0 8px 32px rgba(0,0,0,0.1), 0 0 0 1px rgba(0,0,0,0.04)",
67
+ buttonText: "#ffffff",
68
+ backdropFilter: "blur(32px) saturate(1.8)"
69
+ };
70
+ var dark = {
71
+ background: "rgba(15,20,35,0.88)",
72
+ backgroundSecondary: "rgba(5,8,18,0.65)",
73
+ accent: "#f97316",
74
+ accentHover: "#ea6c0a",
75
+ text: "#dde3ef",
76
+ textMuted: "#6b7a93",
77
+ border: "rgba(255,255,255,0.08)",
78
+ shadow: "0 24px 60px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04)",
79
+ buttonText: "#ffffff",
80
+ backdropFilter: "blur(32px) saturate(1.6)"
81
+ };
82
+ function resolveTheme(theme) {
83
+ if (theme === "dark") return dark;
84
+ if (theme === "light") return light;
85
+ const override = theme;
86
+ return {
87
+ ...light,
88
+ background: override.background ?? light.background,
89
+ accent: override.accent ?? light.accent,
90
+ accentHover: override.accent ?? light.accentHover,
91
+ text: override.text ?? light.text,
92
+ border: override.border ?? light.border
93
+ };
94
+ }
95
+
96
+ // src/FlintModal.tsx
97
+ import { jsx, jsxs } from "react/jsx-runtime";
98
+ var SEVERITIES = ["P1", "P2", "P3", "P4"];
99
+ var SEV_COLOR = {
100
+ P1: "#ef4444",
101
+ P2: "#f97316",
102
+ P3: "#eab308",
103
+ P4: "#22c55e"
104
+ };
105
+ function injectKeyframes() {
106
+ if (typeof document === "undefined") return;
107
+ if (document.getElementById("_flint_kf")) return;
108
+ const s = document.createElement("style");
109
+ s.id = "_flint_kf";
110
+ s.textContent = `
111
+ @keyframes _flint_in {
112
+ from { opacity: 0; transform: scale(0.93) translateY(10px); }
113
+ to { opacity: 1; transform: scale(1) translateY(0); }
114
+ }
115
+ @keyframes _flint_overlay_in {
116
+ from { opacity: 0; }
117
+ to { opacity: 1; }
118
+ }
119
+ @keyframes _flint_spin {
120
+ to { transform: rotate(360deg); }
121
+ }
122
+ @keyframes _flint_pulse {
123
+ 0%, 100% { opacity: 0.5; transform: scale(0.88); }
124
+ 50% { opacity: 1; transform: scale(1.08); }
125
+ }
126
+ @keyframes _flint_ripple {
127
+ 0% { opacity: 0.5; transform: scale(0.75); }
128
+ 100% { opacity: 0; transform: scale(1.55); }
129
+ }
130
+ @keyframes _flint_sending_dot {
131
+ 0%, 80%, 100% { opacity: 0.2; transform: scale(0.8); }
132
+ 40% { opacity: 1; transform: scale(1); }
133
+ }
134
+ @keyframes _flint_success_up {
135
+ from { opacity: 0; transform: translateY(8px); }
136
+ to { opacity: 1; transform: translateY(0); }
137
+ }
138
+ `;
139
+ document.head.appendChild(s);
140
+ }
141
+ function FlintModal({
142
+ projectKey,
143
+ serverUrl,
144
+ user,
145
+ meta,
146
+ theme,
147
+ zIndex,
148
+ onClose,
149
+ getEnvironment,
150
+ getConsoleLogs,
151
+ getNetworkErrors,
152
+ getReplayEvents,
153
+ externalReplayUrl
154
+ }) {
155
+ const { t } = useTranslation();
156
+ const colors = resolveTheme(theme);
157
+ const isDark = theme === "dark";
158
+ const [severity, setSeverity] = useState("P2");
159
+ const [description, setDescription] = useState("");
160
+ const [expectedBehavior, setExpectedBehavior] = useState("");
161
+ const [screenshot, setScreenshot] = useState(null);
162
+ const [status, setStatus] = useState("idle");
163
+ const [result, setResult] = useState(null);
164
+ const [errorMsg, setErrorMsg] = useState("");
165
+ const fileRef = useRef(null);
166
+ const overlayRef = useRef(null);
167
+ useEffect(() => {
168
+ injectKeyframes();
169
+ }, []);
170
+ useEffect(() => {
171
+ const handler = (e) => {
172
+ if (e.key === "Escape" && status !== "submitting") onClose();
173
+ };
174
+ window.addEventListener("keydown", handler);
175
+ return () => window.removeEventListener("keydown", handler);
176
+ }, [onClose, status]);
177
+ const handleOverlayClick = useCallback(
178
+ (e) => {
179
+ if (e.target === overlayRef.current && status !== "submitting") onClose();
180
+ },
181
+ [onClose, status]
182
+ );
183
+ const handleSubmit = async (e) => {
184
+ e.preventDefault();
185
+ if (!description.trim()) return;
186
+ setStatus("submitting");
187
+ setErrorMsg("");
188
+ const collectedMeta = {
189
+ ...meta,
190
+ environment: getEnvironment(),
191
+ consoleLogs: getConsoleLogs(),
192
+ networkErrors: getNetworkErrors()
193
+ };
194
+ try {
195
+ const res = await submitReport(
196
+ serverUrl,
197
+ projectKey,
198
+ {
199
+ reporterId: user?.id ?? "anonymous",
200
+ reporterName: user?.name ?? "Anonymous",
201
+ description: description.trim(),
202
+ expectedBehavior: expectedBehavior.trim() || void 0,
203
+ externalReplayUrl: externalReplayUrl || void 0,
204
+ severity,
205
+ url: window.location.href,
206
+ meta: collectedMeta
207
+ },
208
+ screenshot ?? void 0
209
+ );
210
+ setResult(res);
211
+ setStatus("success");
212
+ const events = getReplayEvents();
213
+ if (events.length > 0) {
214
+ submitReplay(serverUrl, projectKey, res.id, events).catch(() => {
215
+ });
216
+ }
217
+ } catch (err) {
218
+ setErrorMsg(err instanceof Error ? err.message : t("errorLabel"));
219
+ setStatus("error");
220
+ }
221
+ };
222
+ const inputBorder = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
223
+ const accentGlow = `0 0 20px ${colors.accent}40, 0 4px 16px rgba(0,0,0,0.2)`;
224
+ const overlayStyle = {
225
+ position: "fixed",
226
+ inset: 0,
227
+ background: "rgba(0,0,0,0.5)",
228
+ display: "flex",
229
+ alignItems: "center",
230
+ justifyContent: "center",
231
+ zIndex,
232
+ padding: "16px",
233
+ backdropFilter: "blur(10px)",
234
+ WebkitBackdropFilter: "blur(10px)",
235
+ animation: "_flint_overlay_in 0.2s ease"
236
+ };
237
+ const modalStyle = {
238
+ background: colors.background,
239
+ backdropFilter: colors.backdropFilter,
240
+ WebkitBackdropFilter: colors.backdropFilter,
241
+ borderRadius: "20px",
242
+ boxShadow: colors.shadow,
243
+ border: `1px solid ${colors.border}`,
244
+ width: "100%",
245
+ maxWidth: "480px",
246
+ maxHeight: "92vh",
247
+ overflowY: "auto",
248
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
249
+ color: colors.text,
250
+ animation: "_flint_in 0.28s cubic-bezier(0.16, 1, 0.3, 1)"
251
+ };
252
+ const inputStyle = {
253
+ width: "100%",
254
+ padding: "11px 13px",
255
+ borderRadius: "10px",
256
+ border: `1px solid ${inputBorder}`,
257
+ background: colors.backgroundSecondary,
258
+ color: colors.text,
259
+ fontSize: "14px",
260
+ outline: "none",
261
+ boxSizing: "border-box",
262
+ fontFamily: "inherit",
263
+ transition: "border-color 0.15s"
264
+ };
265
+ if (status === "submitting" || status === "success") {
266
+ const isSuccess = status === "success";
267
+ const ringBorder = isSuccess ? "3px solid #22c55e" : `3px solid ${isDark ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.08)"}`;
268
+ const ringTopColor = isSuccess ? "#22c55e" : colors.accent;
269
+ return /* @__PURE__ */ jsx("div", { style: overlayStyle, children: /* @__PURE__ */ jsxs("div", { style: modalStyle, role: "dialog", "aria-modal": "true", "aria-label": isSuccess ? t("successTitle") : t("sending"), children: [
270
+ /* @__PURE__ */ jsx(ModalHeader, { colors, inputBorder, showClose: false, onClose }),
271
+ /* @__PURE__ */ jsxs("div", { style: { padding: "40px 32px 48px", textAlign: "center", display: "flex", flexDirection: "column", alignItems: "center" }, children: [
272
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative", width: 80, height: 80, marginBottom: 28 }, children: [
273
+ /* @__PURE__ */ jsx("div", { style: {
274
+ position: "absolute",
275
+ inset: -10,
276
+ borderRadius: "50%",
277
+ border: `1.5px solid ${colors.accent}`,
278
+ animation: "_flint_ripple 1.8s ease-out infinite",
279
+ opacity: isSuccess ? 0 : 1,
280
+ transition: "opacity 0.3s ease"
281
+ } }),
282
+ /* @__PURE__ */ jsx("div", { style: {
283
+ position: "absolute",
284
+ inset: -10,
285
+ borderRadius: "50%",
286
+ border: `1.5px solid ${colors.accent}`,
287
+ animation: "_flint_ripple 1.8s ease-out infinite 0.6s",
288
+ opacity: isSuccess ? 0 : 1,
289
+ transition: "opacity 0.3s ease"
290
+ } }),
291
+ /* @__PURE__ */ jsx("div", { style: {
292
+ position: "absolute",
293
+ inset: 0,
294
+ borderRadius: "50%",
295
+ border: ringBorder,
296
+ borderTopColor: ringTopColor,
297
+ animation: isSuccess ? "none" : "_flint_spin 0.85s linear infinite",
298
+ transition: "border-color 0.45s ease, border-top-color 0.45s ease"
299
+ } }),
300
+ /* @__PURE__ */ jsxs("div", { style: { position: "absolute", inset: 14, borderRadius: "50%" }, children: [
301
+ /* @__PURE__ */ jsx("div", { style: {
302
+ position: "absolute",
303
+ inset: 0,
304
+ borderRadius: "50%",
305
+ background: `linear-gradient(135deg, ${colors.accent}30, ${colors.accentHover}50)`,
306
+ display: "flex",
307
+ alignItems: "center",
308
+ justifyContent: "center",
309
+ animation: isSuccess ? "none" : "_flint_pulse 2s ease-in-out infinite",
310
+ opacity: isSuccess ? 0 : 1,
311
+ transition: "opacity 0.3s ease"
312
+ }, children: /* @__PURE__ */ jsx(SparkIcon, { color: colors.accent, size: 20 }) }),
313
+ /* @__PURE__ */ jsx("div", { style: {
314
+ position: "absolute",
315
+ inset: 0,
316
+ borderRadius: "50%",
317
+ background: "rgba(34,197,94,0.15)",
318
+ display: "flex",
319
+ alignItems: "center",
320
+ justifyContent: "center",
321
+ opacity: isSuccess ? 1 : 0,
322
+ transform: isSuccess ? "scale(1)" : "scale(0.65)",
323
+ transition: "opacity 0.35s ease 0.2s, transform 0.4s cubic-bezier(0.16,1,0.3,1) 0.2s"
324
+ }, children: /* @__PURE__ */ jsx(CheckIcon, { size: 20 }) })
325
+ ] })
326
+ ] }),
327
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative", height: 26, width: "100%", marginBottom: 10 }, children: [
328
+ /* @__PURE__ */ jsx("div", { style: {
329
+ position: "absolute",
330
+ inset: 0,
331
+ display: "flex",
332
+ alignItems: "center",
333
+ justifyContent: "center",
334
+ fontSize: 17,
335
+ fontWeight: 700,
336
+ color: colors.text,
337
+ letterSpacing: "-0.02em",
338
+ opacity: isSuccess ? 0 : 1,
339
+ transition: "opacity 0.25s ease",
340
+ pointerEvents: "none"
341
+ }, children: t("sending") }),
342
+ /* @__PURE__ */ jsx("div", { style: {
343
+ position: "absolute",
344
+ inset: 0,
345
+ display: "flex",
346
+ alignItems: "center",
347
+ justifyContent: "center",
348
+ fontSize: 17,
349
+ fontWeight: 700,
350
+ color: colors.text,
351
+ letterSpacing: "-0.02em",
352
+ opacity: isSuccess ? 1 : 0,
353
+ transform: isSuccess ? "translateY(0)" : "translateY(6px)",
354
+ transition: "opacity 0.35s ease 0.25s, transform 0.35s ease 0.25s",
355
+ pointerEvents: isSuccess ? "auto" : "none"
356
+ }, children: t("successTitle") })
357
+ ] }),
358
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative", minHeight: 76, width: "100%" }, children: [
359
+ /* @__PURE__ */ jsxs("div", { style: {
360
+ position: "absolute",
361
+ inset: 0,
362
+ display: "flex",
363
+ alignItems: "center",
364
+ justifyContent: "center",
365
+ gap: 6,
366
+ color: colors.textMuted,
367
+ fontSize: 13,
368
+ opacity: isSuccess ? 0 : 1,
369
+ transition: "opacity 0.2s ease",
370
+ pointerEvents: "none"
371
+ }, children: [
372
+ /* @__PURE__ */ jsx("span", { children: t("capturingContext") }),
373
+ /* @__PURE__ */ jsx(SendingDots, { color: colors.accent })
374
+ ] }),
375
+ /* @__PURE__ */ jsxs("div", { style: {
376
+ position: "absolute",
377
+ inset: 0,
378
+ display: "flex",
379
+ flexDirection: "column",
380
+ alignItems: "center",
381
+ gap: 10,
382
+ opacity: isSuccess ? 1 : 0,
383
+ transform: isSuccess ? "translateY(0)" : "translateY(8px)",
384
+ transition: "opacity 0.35s ease 0.35s, transform 0.35s ease 0.35s",
385
+ pointerEvents: isSuccess ? "auto" : "none"
386
+ }, children: [
387
+ /* @__PURE__ */ jsx("p", { style: { fontSize: 13, color: colors.textMuted, margin: 0 }, children: result?.isDuplicate ? t("successDuplicate") : result ? `ID: ${result.id}` : "" }),
388
+ result?.githubIssueUrl && /* @__PURE__ */ jsxs(
389
+ "a",
390
+ {
391
+ href: result.githubIssueUrl,
392
+ target: "_blank",
393
+ rel: "noreferrer",
394
+ style: {
395
+ display: "inline-flex",
396
+ alignItems: "center",
397
+ gap: 6,
398
+ padding: "9px 18px",
399
+ borderRadius: "10px",
400
+ background: `linear-gradient(135deg, ${colors.accent}, ${colors.accentHover})`,
401
+ color: colors.buttonText,
402
+ textDecoration: "none",
403
+ fontSize: "13px",
404
+ fontWeight: 600,
405
+ boxShadow: accentGlow
406
+ },
407
+ children: [
408
+ t("successGitHub"),
409
+ " \u2197"
410
+ ]
411
+ }
412
+ ),
413
+ /* @__PURE__ */ jsx(
414
+ "button",
415
+ {
416
+ onClick: onClose,
417
+ style: {
418
+ background: "none",
419
+ border: "none",
420
+ cursor: "pointer",
421
+ fontSize: 13,
422
+ color: colors.textMuted,
423
+ padding: "4px 8px",
424
+ fontFamily: "inherit"
425
+ },
426
+ children: t("cancel")
427
+ }
428
+ )
429
+ ] })
430
+ ] })
431
+ ] })
432
+ ] }) });
433
+ }
434
+ return /* @__PURE__ */ jsx("div", { ref: overlayRef, style: overlayStyle, onClick: handleOverlayClick, children: /* @__PURE__ */ jsxs("div", { style: modalStyle, role: "dialog", "aria-modal": "true", "aria-labelledby": "flint-modal-title", children: [
435
+ /* @__PURE__ */ jsx(
436
+ ModalHeader,
437
+ {
438
+ colors,
439
+ inputBorder,
440
+ showClose: true,
441
+ onClose,
442
+ titleId: "flint-modal-title",
443
+ title: t("modalTitle")
444
+ }
445
+ ),
446
+ /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, style: { padding: "20px 24px 24px" }, children: [
447
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: 18 }, children: [
448
+ /* @__PURE__ */ jsx(FieldLabel, { colors, children: t("severityLabel") }),
449
+ /* @__PURE__ */ jsx("div", { style: { display: "grid", gridTemplateColumns: "repeat(4,1fr)", gap: 8 }, children: SEVERITIES.map((sev) => /* @__PURE__ */ jsx(
450
+ SeverityButton,
451
+ {
452
+ sev,
453
+ label: t(`severity_${sev}_label`),
454
+ selected: severity === sev,
455
+ hint: t(`severity_${sev}_hint`),
456
+ color: SEV_COLOR[sev],
457
+ accent: colors.accent,
458
+ border: inputBorder,
459
+ bg: colors.backgroundSecondary,
460
+ text: colors.text,
461
+ onClick: () => setSeverity(sev)
462
+ },
463
+ sev
464
+ )) })
465
+ ] }),
466
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: 14 }, children: [
467
+ /* @__PURE__ */ jsx(FieldLabel, { colors, htmlFor: "flint-description", children: t("whatIsBrokenLabel") }),
468
+ /* @__PURE__ */ jsx(
469
+ "textarea",
470
+ {
471
+ id: "flint-description",
472
+ style: { ...inputStyle, resize: "vertical", minHeight: 80 },
473
+ value: description,
474
+ onChange: (e) => setDescription(e.target.value),
475
+ placeholder: t("whatIsBrokenPlaceholder"),
476
+ required: true
477
+ }
478
+ )
479
+ ] }),
480
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: 14 }, children: [
481
+ /* @__PURE__ */ jsx(FieldLabel, { colors, htmlFor: "flint-expected", children: t("expectedBehaviorLabel") }),
482
+ /* @__PURE__ */ jsx(
483
+ "textarea",
484
+ {
485
+ id: "flint-expected",
486
+ style: { ...inputStyle, resize: "vertical", minHeight: 72 },
487
+ value: expectedBehavior,
488
+ onChange: (e) => setExpectedBehavior(e.target.value),
489
+ placeholder: t("expectedBehaviorPlaceholder")
490
+ }
491
+ )
492
+ ] }),
493
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: 20 }, children: [
494
+ /* @__PURE__ */ jsx(FieldLabel, { colors, children: t("screenshotLabel") }),
495
+ /* @__PURE__ */ jsxs(
496
+ "label",
497
+ {
498
+ htmlFor: "flint-screenshot",
499
+ style: {
500
+ display: "flex",
501
+ alignItems: "center",
502
+ gap: 8,
503
+ padding: "10px 13px",
504
+ borderRadius: 10,
505
+ border: `1px dashed ${inputBorder}`,
506
+ cursor: "pointer",
507
+ fontSize: 13,
508
+ color: colors.textMuted,
509
+ background: colors.backgroundSecondary
510
+ },
511
+ children: [
512
+ "\u{1F4CE} ",
513
+ screenshot ? screenshot.name : t("screenshotPlaceholder")
514
+ ]
515
+ }
516
+ ),
517
+ /* @__PURE__ */ jsx(
518
+ "input",
519
+ {
520
+ id: "flint-screenshot",
521
+ ref: fileRef,
522
+ type: "file",
523
+ accept: "image/*",
524
+ style: { display: "none" },
525
+ onChange: (e) => setScreenshot(e.target.files?.[0] ?? null)
526
+ }
527
+ )
528
+ ] }),
529
+ status === "error" && /* @__PURE__ */ jsxs("div", { style: {
530
+ padding: "10px 13px",
531
+ borderRadius: 10,
532
+ background: "rgba(239,68,68,0.08)",
533
+ border: "1px solid rgba(239,68,68,0.2)",
534
+ color: "#f87171",
535
+ fontSize: 12,
536
+ marginBottom: 16
537
+ }, children: [
538
+ "\u26A0\uFE0F ",
539
+ errorMsg || t("errorLabel")
540
+ ] }),
541
+ /* @__PURE__ */ jsxs(
542
+ "button",
543
+ {
544
+ type: "submit",
545
+ style: {
546
+ width: "100%",
547
+ padding: "13px 20px",
548
+ borderRadius: 12,
549
+ border: "none",
550
+ background: `linear-gradient(135deg, ${colors.accent}, ${colors.accentHover})`,
551
+ color: colors.buttonText,
552
+ fontSize: 15,
553
+ fontWeight: 700,
554
+ cursor: "pointer",
555
+ letterSpacing: "-0.01em",
556
+ boxShadow: accentGlow,
557
+ fontFamily: "inherit",
558
+ display: "flex",
559
+ alignItems: "center",
560
+ justifyContent: "center",
561
+ gap: 8
562
+ },
563
+ children: [
564
+ /* @__PURE__ */ jsx(SparkIcon, { color: colors.buttonText, size: 15 }),
565
+ t("submitLabel")
566
+ ]
567
+ }
568
+ ),
569
+ /* @__PURE__ */ jsx(
570
+ "button",
571
+ {
572
+ type: "button",
573
+ onClick: onClose,
574
+ style: {
575
+ width: "100%",
576
+ padding: "10px",
577
+ marginTop: 8,
578
+ background: "none",
579
+ border: "none",
580
+ cursor: "pointer",
581
+ fontSize: 13,
582
+ color: colors.textMuted,
583
+ fontFamily: "inherit",
584
+ borderRadius: 8
585
+ },
586
+ children: t("cancel")
587
+ }
588
+ )
589
+ ] })
590
+ ] }) });
591
+ }
592
+ function ModalHeader({
593
+ colors,
594
+ inputBorder,
595
+ showClose,
596
+ onClose,
597
+ titleId,
598
+ title
599
+ }) {
600
+ return /* @__PURE__ */ jsxs("div", { style: {
601
+ display: "flex",
602
+ alignItems: "center",
603
+ gap: 10,
604
+ padding: "16px 20px 14px",
605
+ borderBottom: `1px solid ${inputBorder}`
606
+ }, children: [
607
+ /* @__PURE__ */ jsx("div", { style: {
608
+ width: 28,
609
+ height: 28,
610
+ borderRadius: 8,
611
+ background: `linear-gradient(135deg, ${colors.accent}20, ${colors.accentHover}35)`,
612
+ border: `1px solid ${colors.accent}30`,
613
+ display: "flex",
614
+ alignItems: "center",
615
+ justifyContent: "center",
616
+ flexShrink: 0
617
+ }, children: /* @__PURE__ */ jsx(SparkIcon, { color: colors.accent, size: 13 }) }),
618
+ titleId && title ? /* @__PURE__ */ jsx("h2", { id: titleId, style: { margin: 0, fontSize: 14, fontWeight: 600, color: colors.text, letterSpacing: "-0.01em", flex: 1 }, children: title }) : /* @__PURE__ */ jsx("span", { style: { flex: 1, fontSize: 13, fontWeight: 600, color: colors.textMuted }, children: "Flint" }),
619
+ showClose && /* @__PURE__ */ jsx(
620
+ "button",
621
+ {
622
+ onClick: onClose,
623
+ "aria-label": "Close",
624
+ style: {
625
+ background: "none",
626
+ border: "none",
627
+ cursor: "pointer",
628
+ padding: 4,
629
+ color: colors.textMuted,
630
+ fontSize: 20,
631
+ lineHeight: 1,
632
+ borderRadius: 6,
633
+ display: "flex",
634
+ alignItems: "center",
635
+ justifyContent: "center",
636
+ opacity: 0.6,
637
+ fontFamily: "inherit"
638
+ },
639
+ children: "\xD7"
640
+ }
641
+ )
642
+ ] });
643
+ }
644
+ function FieldLabel({
645
+ children,
646
+ colors,
647
+ htmlFor
648
+ }) {
649
+ return /* @__PURE__ */ jsx(
650
+ "label",
651
+ {
652
+ htmlFor,
653
+ style: {
654
+ display: "block",
655
+ fontSize: "10px",
656
+ fontWeight: 700,
657
+ color: colors.textMuted,
658
+ marginBottom: 6,
659
+ textTransform: "uppercase",
660
+ letterSpacing: "0.07em"
661
+ },
662
+ children
663
+ }
664
+ );
665
+ }
666
+ function SendingDots({ color }) {
667
+ return /* @__PURE__ */ jsx("span", { style: { display: "inline-flex", gap: 3, alignItems: "center" }, children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx(
668
+ "span",
669
+ {
670
+ style: {
671
+ width: 4,
672
+ height: 4,
673
+ borderRadius: "50%",
674
+ background: color,
675
+ display: "inline-block",
676
+ animation: `_flint_sending_dot 1.4s ease-in-out infinite ${i * 0.2}s`
677
+ }
678
+ },
679
+ i
680
+ )) });
681
+ }
682
+ function SeverityButton({ sev, label, selected, hint, color, accent, border, bg, text, onClick }) {
683
+ return /* @__PURE__ */ jsxs(
684
+ "button",
685
+ {
686
+ type: "button",
687
+ title: hint,
688
+ onClick,
689
+ style: {
690
+ padding: "9px 6px 8px",
691
+ borderRadius: 10,
692
+ border: selected ? `2px solid ${accent}` : `1.5px solid ${border}`,
693
+ background: selected ? `${accent}15` : bg,
694
+ cursor: "pointer",
695
+ display: "flex",
696
+ flexDirection: "column",
697
+ alignItems: "center",
698
+ gap: 5,
699
+ transition: "border-color 0.12s, background 0.12s",
700
+ fontFamily: "inherit"
701
+ },
702
+ children: [
703
+ /* @__PURE__ */ jsx("span", { style: { width: 8, height: 8, borderRadius: "50%", background: color, display: "block" } }),
704
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 11, fontWeight: selected ? 700 : 500, color: selected ? accent : text, letterSpacing: "0.02em" }, children: sev }),
705
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 9, color: selected ? accent : text, opacity: 0.6, letterSpacing: "0.02em" }, children: label })
706
+ ]
707
+ }
708
+ );
709
+ }
710
+ function SparkIcon({ color = "currentColor", size = 14 }) {
711
+ return /* @__PURE__ */ jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M13 2L3 14h9l-1 8 10-12h-9l1-8z" }) });
712
+ }
713
+ function CheckIcon({ size = 20 }) {
714
+ return /* @__PURE__ */ jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "#22c55e", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" }) });
715
+ }
716
+
717
+ // src/i18n/index.ts
718
+ import { createInstance } from "i18next";
719
+ import { initReactI18next } from "react-i18next";
720
+
721
+ // src/i18n/locales/en.json
722
+ var en_default = {
723
+ buttonLabel: "Report bug",
724
+ modalTitle: "Report an issue",
725
+ severityLabel: "Severity",
726
+ severity_P1_hint: "Critical \u2014 system down",
727
+ severity_P2_hint: "High \u2014 core feature broken",
728
+ severity_P3_hint: "Medium \u2014 noticeable but workable",
729
+ severity_P4_hint: "Low \u2014 cosmetic or improvement",
730
+ severity_P1_label: "Critical",
731
+ severity_P2_label: "High",
732
+ severity_P3_label: "Medium",
733
+ severity_P4_label: "Low",
734
+ whatIsBrokenLabel: "What Is Broken",
735
+ whatIsBrokenPlaceholder: "1\u20132 sentences: what is currently happening that should NOT happen.",
736
+ expectedBehaviorLabel: "Expected Behavior (optional)",
737
+ expectedBehaviorPlaceholder: "Describe exactly what the user should see or receive after the fix.",
738
+ screenshotLabel: "Screenshot (optional)",
739
+ screenshotPlaceholder: "Click to attach...",
740
+ submitLabel: "Submit",
741
+ successTitle: "Bug reported!",
742
+ successDuplicate: "Looks like a duplicate of an existing bug.",
743
+ successGitHub: "View GitHub issue",
744
+ errorLabel: "Failed to submit. Please try again.",
745
+ cancel: "Cancel",
746
+ sending: "Sending report",
747
+ capturingContext: "Capturing context"
748
+ };
749
+
750
+ // src/i18n/index.ts
751
+ var widgetI18n = createInstance();
752
+ widgetI18n.use(initReactI18next).init({
753
+ lng: "en-US",
754
+ fallbackLng: "en-US",
755
+ resources: { "en-US": { translation: en_default } },
756
+ interpolation: { escapeValue: false },
757
+ initImmediate: false
758
+ });
759
+ var i18n_default = widgetI18n;
760
+
761
+ // src/collectors/environment.ts
762
+ function collectEnvironment() {
763
+ const ua = navigator.userAgent;
764
+ let browser = "Unknown";
765
+ const chromeM = ua.match(/Chrome\/(\d+)/);
766
+ const firefoxM = ua.match(/Firefox\/(\d+)/);
767
+ const edgeM = ua.match(/Edg\/(\d+)/);
768
+ const safariM = ua.match(/Version\/(\d+)/);
769
+ const operaM = ua.match(/OPR\/(\d+)/);
770
+ if (operaM) {
771
+ browser = `Opera ${operaM[1]}`;
772
+ } else if (edgeM) {
773
+ browser = `Edge ${edgeM[1]}`;
774
+ } else if (chromeM && !/Edg|OPR/.test(ua)) {
775
+ browser = `Chrome ${chromeM[1]}`;
776
+ } else if (firefoxM) {
777
+ browser = `Firefox ${firefoxM[1]}`;
778
+ } else if (safariM && /Safari\//.test(ua)) {
779
+ browser = `Safari ${safariM[1]}`;
780
+ }
781
+ let os = "Unknown";
782
+ const macM = ua.match(/Mac OS X (\d+[._]\d+)/);
783
+ const winM = ua.match(/Windows NT (\d+\.\d+)/);
784
+ const androidM = ua.match(/Android (\d+)/);
785
+ const iosM = ua.match(/iPhone OS (\d+[._]\d+)/);
786
+ if (macM) {
787
+ os = `macOS ${macM[1].replace("_", ".")}`;
788
+ } else if (winM) {
789
+ const winMap = {
790
+ "10.0": "10/11",
791
+ "6.3": "8.1",
792
+ "6.2": "8",
793
+ "6.1": "7"
794
+ };
795
+ os = `Windows ${winMap[winM[1]] ?? winM[1]}`;
796
+ } else if (androidM) {
797
+ os = `Android ${androidM[1]}`;
798
+ } else if (iosM) {
799
+ os = `iOS ${iosM[1].replace(/_/g, ".")}`;
800
+ } else if (/Linux/.test(ua)) {
801
+ os = "Linux";
802
+ }
803
+ return {
804
+ browser,
805
+ os,
806
+ viewport: `${window.innerWidth}x${window.innerHeight}`,
807
+ screen: `${screen.width}x${screen.height}`,
808
+ language: navigator.language,
809
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
810
+ online: navigator.onLine
811
+ };
812
+ }
813
+
814
+ // src/collectors/console.ts
815
+ var MAX_ENTRIES = 50;
816
+ function createConsoleCollector() {
817
+ const entries = [];
818
+ let active = false;
819
+ const originals = {
820
+ log: console.log.bind(console),
821
+ warn: console.warn.bind(console),
822
+ error: console.error.bind(console)
823
+ };
824
+ let origOnerror = null;
825
+ let origUnhandled = null;
826
+ function push(level, args) {
827
+ let str;
828
+ try {
829
+ str = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
830
+ } catch {
831
+ str = String(args[0]);
832
+ }
833
+ if (str.length > 500) str = str.slice(0, 500) + "\u2026";
834
+ entries.push({ level, args: str, timestamp: Date.now() });
835
+ if (entries.length > MAX_ENTRIES) entries.shift();
836
+ }
837
+ return {
838
+ start() {
839
+ if (active) return;
840
+ active = true;
841
+ console.log = (...args) => {
842
+ push("log", args);
843
+ originals.log(...args);
844
+ };
845
+ console.warn = (...args) => {
846
+ push("warn", args);
847
+ originals.warn(...args);
848
+ };
849
+ console.error = (...args) => {
850
+ push("error", args);
851
+ originals.error(...args);
852
+ };
853
+ origOnerror = window.onerror;
854
+ window.onerror = (msg, src, line, col, err) => {
855
+ push("error", [err?.message ?? String(msg), `${src}:${line}:${col}`]);
856
+ if (typeof origOnerror === "function")
857
+ return origOnerror(msg, src, line, col, err);
858
+ return false;
859
+ };
860
+ origUnhandled = window.onunhandledrejection;
861
+ window.onunhandledrejection = (event) => {
862
+ const reason = event.reason instanceof Error ? event.reason.message : JSON.stringify(event.reason);
863
+ push("error", ["UnhandledRejection:", reason]);
864
+ if (typeof origUnhandled === "function")
865
+ origUnhandled.call(window, event);
866
+ };
867
+ },
868
+ stop() {
869
+ if (!active) return;
870
+ active = false;
871
+ console.log = originals.log;
872
+ console.warn = originals.warn;
873
+ console.error = originals.error;
874
+ window.onerror = origOnerror;
875
+ window.onunhandledrejection = origUnhandled;
876
+ },
877
+ getEntries() {
878
+ return [...entries];
879
+ }
880
+ };
881
+ }
882
+
883
+ // src/collectors/network.ts
884
+ var MAX_ENTRIES2 = 20;
885
+ function truncateUrl(url) {
886
+ try {
887
+ const u = new URL(url, location.href);
888
+ const base = `${u.origin}${u.pathname}`;
889
+ return base.length > 200 ? base.slice(0, 200) + "\u2026" : base;
890
+ } catch {
891
+ return url.length > 200 ? url.slice(0, 200) + "\u2026" : url;
892
+ }
893
+ }
894
+ function createNetworkCollector() {
895
+ const entries = [];
896
+ let origFetch = null;
897
+ let origXHROpen = null;
898
+ let active = false;
899
+ function push(entry) {
900
+ entries.push(entry);
901
+ if (entries.length > MAX_ENTRIES2) entries.shift();
902
+ }
903
+ return {
904
+ start() {
905
+ if (active) return;
906
+ active = true;
907
+ origFetch = window.fetch;
908
+ window.fetch = async (input, init) => {
909
+ const method = (init?.method ?? "GET").toUpperCase();
910
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
911
+ const startTime = Date.now();
912
+ const res = await origFetch.call(window, input, init);
913
+ push({
914
+ method,
915
+ url: truncateUrl(url),
916
+ status: res.status,
917
+ duration: Date.now() - startTime,
918
+ timestamp: startTime
919
+ });
920
+ return res;
921
+ };
922
+ origXHROpen = XMLHttpRequest.prototype.open;
923
+ XMLHttpRequest.prototype.open = function(method, url, async, username, password) {
924
+ const startTime = Date.now();
925
+ const urlStr = typeof url === "string" ? url : url.href;
926
+ this.addEventListener("load", () => {
927
+ push({
928
+ method: method.toUpperCase(),
929
+ url: truncateUrl(urlStr),
930
+ status: this.status,
931
+ duration: Date.now() - startTime,
932
+ timestamp: startTime
933
+ });
934
+ });
935
+ return origXHROpen.apply(this, [
936
+ method,
937
+ url,
938
+ async ?? true,
939
+ username,
940
+ password
941
+ ]);
942
+ };
943
+ },
944
+ stop() {
945
+ if (!active) return;
946
+ active = false;
947
+ if (origFetch) window.fetch = origFetch;
948
+ if (origXHROpen) XMLHttpRequest.prototype.open = origXHROpen;
949
+ },
950
+ getEntries() {
951
+ return [...entries];
952
+ }
953
+ };
954
+ }
955
+
956
+ // src/FlintWidget.tsx
957
+ import { record } from "rrweb";
958
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
959
+ var REPLAY_WINDOW_MS = 6e4;
960
+ function FlintWidget(props) {
961
+ const { locale = "en-US" } = props;
962
+ useEffect2(() => {
963
+ i18n_default.changeLanguage(locale);
964
+ }, [locale]);
965
+ return /* @__PURE__ */ jsx2(I18nextProvider, { i18n: i18n_default, children: /* @__PURE__ */ jsx2(WidgetContent, { ...props }) });
966
+ }
967
+ function WidgetContent({
968
+ projectKey,
969
+ serverUrl,
970
+ user,
971
+ meta,
972
+ extraFields,
973
+ buttonLabel,
974
+ theme = "dark",
975
+ zIndex = 9999
976
+ }) {
977
+ const externalReplayUrl = extraFields?.sessionReplay;
978
+ const { t } = useTranslation2();
979
+ const [open, setOpen] = useState2(false);
980
+ const [hovered, setHovered] = useState2(false);
981
+ const colors = resolveTheme(theme);
982
+ const consoleCollector = useRef2(null);
983
+ const networkCollector = useRef2(null);
984
+ const replayEvents = useRef2([]);
985
+ const stopReplay = useRef2(null);
986
+ if (!consoleCollector.current) {
987
+ consoleCollector.current = createConsoleCollector();
988
+ consoleCollector.current.start();
989
+ }
990
+ if (!networkCollector.current) {
991
+ networkCollector.current = createNetworkCollector();
992
+ networkCollector.current.start();
993
+ }
994
+ useEffect2(() => {
995
+ const stopFn = record({
996
+ emit(event) {
997
+ replayEvents.current.push(event);
998
+ const cutoff = Date.now() - REPLAY_WINDOW_MS;
999
+ while (replayEvents.current.length > 0 && replayEvents.current[0].timestamp < cutoff) {
1000
+ replayEvents.current.shift();
1001
+ }
1002
+ }
1003
+ });
1004
+ stopReplay.current = stopFn ?? null;
1005
+ return () => {
1006
+ consoleCollector.current?.stop();
1007
+ networkCollector.current?.stop();
1008
+ stopReplay.current?.();
1009
+ };
1010
+ }, []);
1011
+ const label = buttonLabel ?? t("buttonLabel");
1012
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
1013
+ /* @__PURE__ */ jsxs2(
1014
+ "button",
1015
+ {
1016
+ onClick: () => setOpen(true),
1017
+ onMouseEnter: () => setHovered(true),
1018
+ onMouseLeave: () => setHovered(false),
1019
+ "aria-label": label,
1020
+ style: {
1021
+ position: "fixed",
1022
+ bottom: "20px",
1023
+ right: "20px",
1024
+ zIndex: zIndex - 1,
1025
+ display: "flex",
1026
+ alignItems: "center",
1027
+ gap: "8px",
1028
+ padding: "10px 18px",
1029
+ borderRadius: "24px",
1030
+ border: "none",
1031
+ background: `linear-gradient(135deg, ${colors.accent}, ${colors.accentHover})`,
1032
+ color: colors.buttonText,
1033
+ fontSize: "13px",
1034
+ fontWeight: 600,
1035
+ cursor: "pointer",
1036
+ boxShadow: hovered ? `0 0 28px ${colors.accent}55, 0 8px 24px rgba(0,0,0,0.3)` : `0 0 16px ${colors.accent}33, 0 4px 16px rgba(0,0,0,0.2)`,
1037
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1038
+ transform: hovered ? "translateY(-2px)" : "translateY(0)",
1039
+ transition: "transform 0.15s ease, box-shadow 0.15s ease",
1040
+ letterSpacing: "0.01em"
1041
+ },
1042
+ children: [
1043
+ /* @__PURE__ */ jsx2(SparkIcon2, {}),
1044
+ label
1045
+ ]
1046
+ }
1047
+ ),
1048
+ open && /* @__PURE__ */ jsx2(
1049
+ FlintModal,
1050
+ {
1051
+ projectKey,
1052
+ serverUrl,
1053
+ user,
1054
+ meta,
1055
+ theme,
1056
+ zIndex,
1057
+ onClose: () => setOpen(false),
1058
+ getEnvironment: collectEnvironment,
1059
+ getConsoleLogs: () => consoleCollector.current?.getEntries() ?? [],
1060
+ getNetworkErrors: () => networkCollector.current?.getEntries() ?? [],
1061
+ getReplayEvents: () => [...replayEvents.current],
1062
+ externalReplayUrl
1063
+ }
1064
+ )
1065
+ ] });
1066
+ }
1067
+ function SparkIcon2() {
1068
+ return /* @__PURE__ */ jsx2(
1069
+ "svg",
1070
+ {
1071
+ width: "14",
1072
+ height: "14",
1073
+ viewBox: "0 0 24 24",
1074
+ fill: "none",
1075
+ stroke: "currentColor",
1076
+ strokeWidth: "2.2",
1077
+ strokeLinecap: "round",
1078
+ strokeLinejoin: "round",
1079
+ "aria-hidden": "true",
1080
+ children: /* @__PURE__ */ jsx2("path", { d: "M13 2L3 14h9l-1 8 10-12h-9l1-8z" })
1081
+ }
1082
+ );
1083
+ }
1084
+ export {
1085
+ FlintModal,
1086
+ FlintWidget
1087
+ };
1088
+ //# sourceMappingURL=index.js.map