@diegotsi/flint-react 0.6.0 → 1.0.1

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 CHANGED
@@ -1,14 +1,13 @@
1
- // src/FlintWidget.tsx
2
- import { useState as useState3, useEffect as useEffect3, useRef as useRef3, useCallback as useCallback2 } from "react";
3
- import { I18nextProvider, useTranslation as useTranslation2 } from "react-i18next";
4
-
5
1
  // src/FlintModal.tsx
6
- import { useRef as useRef2, useState as useState2, useEffect as useEffect2, useCallback } from "react";
2
+ import { useCallback, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
7
3
  import { useTranslation } from "react-i18next";
8
4
 
5
+ // src/api.ts
6
+ import { submitReplay, submitReport } from "@flint/core";
7
+
9
8
  // src/ScreenAnnotator.tsx
10
- import { useEffect, useRef, useState } from "react";
11
9
  import { domToCanvas } from "modern-screenshot";
10
+ import { useEffect, useRef, useState } from "react";
12
11
  import { jsx, jsxs } from "react/jsx-runtime";
13
12
  function normalizeRect(startX, startY, endX, endY) {
14
13
  return {
@@ -53,9 +52,7 @@ function ScreenAnnotator({ zIndex, onCapture, onCancel }) {
53
52
  const dpr = window.devicePixelRatio ?? 1;
54
53
  const vw = window.innerWidth;
55
54
  const vh = window.innerHeight;
56
- await new Promise(
57
- (resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
58
- );
55
+ await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
59
56
  try {
60
57
  const fullCanvas = await domToCanvas(document.documentElement, {
61
58
  scale: dpr,
@@ -70,17 +67,7 @@ function ScreenAnnotator({ zIndex, onCapture, onCancel }) {
70
67
  canvas.width = vw * dpr;
71
68
  canvas.height = vh * dpr;
72
69
  const ctx = canvas.getContext("2d");
73
- ctx.drawImage(
74
- fullCanvas,
75
- 0,
76
- 0,
77
- vw * dpr,
78
- vh * dpr,
79
- 0,
80
- 0,
81
- vw * dpr,
82
- vh * dpr
83
- );
70
+ ctx.drawImage(fullCanvas, 0, 0, vw * dpr, vh * dpr, 0, 0, vw * dpr, vh * dpr);
84
71
  ctx.fillStyle = "rgba(255,200,0,0.25)";
85
72
  ctx.fillRect(finalRect.x * dpr, finalRect.y * dpr, finalRect.w * dpr, finalRect.h * dpr);
86
73
  ctx.strokeStyle = "#f97316";
@@ -118,123 +105,50 @@ function ScreenAnnotator({ zIndex, onCapture, onCancel }) {
118
105
  pointerEvents: isCapturing ? "none" : "auto"
119
106
  },
120
107
  children: [
121
- !isCapturing && /* @__PURE__ */ jsx("div", { style: {
122
- position: "absolute",
123
- top: 16,
124
- left: "50%",
125
- transform: "translateX(-50%)",
126
- background: "rgba(0,0,0,0.75)",
127
- color: "#fff",
128
- padding: "8px 18px",
129
- borderRadius: 8,
130
- fontSize: 14,
131
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
132
- pointerEvents: "none",
133
- whiteSpace: "nowrap",
134
- backdropFilter: "blur(4px)"
135
- }, children: "Drag to highlight the problem area \xA0\xB7\xA0 Esc to cancel" }),
136
- rect && phase === "selecting" && /* @__PURE__ */ jsx("div", { style: {
137
- position: "absolute",
138
- left: rect.x,
139
- top: rect.y,
140
- width: rect.w,
141
- height: rect.h,
142
- background: "rgba(255,200,0,0.2)",
143
- border: "2px dashed #f97316",
144
- boxSizing: "border-box",
145
- pointerEvents: "none"
146
- } })
108
+ !isCapturing && /* @__PURE__ */ jsx(
109
+ "div",
110
+ {
111
+ style: {
112
+ position: "absolute",
113
+ top: 16,
114
+ left: "50%",
115
+ transform: "translateX(-50%)",
116
+ background: "rgba(0,0,0,0.75)",
117
+ color: "#fff",
118
+ padding: "8px 18px",
119
+ borderRadius: 8,
120
+ fontSize: 14,
121
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
122
+ pointerEvents: "none",
123
+ whiteSpace: "nowrap",
124
+ backdropFilter: "blur(4px)"
125
+ },
126
+ children: "Drag to highlight the problem area \xA0\xB7\xA0 Esc to cancel"
127
+ }
128
+ ),
129
+ rect && phase === "selecting" && /* @__PURE__ */ jsx(
130
+ "div",
131
+ {
132
+ style: {
133
+ position: "absolute",
134
+ left: rect.x,
135
+ top: rect.y,
136
+ width: rect.w,
137
+ height: rect.h,
138
+ background: "rgba(255,200,0,0.2)",
139
+ border: "2px dashed #f97316",
140
+ boxSizing: "border-box",
141
+ pointerEvents: "none"
142
+ }
143
+ }
144
+ )
147
145
  ]
148
146
  }
149
147
  );
150
148
  }
151
149
 
152
- // src/api.ts
153
- import { gzipSync } from "fflate";
154
- async function submitReport(serverUrl, projectKey, payload, screenshot) {
155
- const url = `${serverUrl.replace(/\/$/, "")}/api/v1/bug-reports`;
156
- let body;
157
- let headers = {
158
- "x-project-key": projectKey
159
- };
160
- if (screenshot) {
161
- const form = new FormData();
162
- form.append("reporterId", payload.reporterId);
163
- form.append("reporterName", payload.reporterName);
164
- form.append("description", payload.description);
165
- if (payload.expectedBehavior) form.append("expectedBehavior", payload.expectedBehavior);
166
- if (payload.stepsToReproduce) form.append("stepsToReproduce", JSON.stringify(payload.stepsToReproduce));
167
- if (payload.externalReplayUrl) form.append("externalReplayUrl", payload.externalReplayUrl);
168
- if (payload.additionalContext) form.append("additionalContext", payload.additionalContext);
169
- form.append("severity", payload.severity);
170
- if (payload.url) form.append("url", payload.url);
171
- if (payload.meta) form.append("meta", JSON.stringify(payload.meta));
172
- form.append("screenshot", screenshot);
173
- body = form;
174
- } else {
175
- body = JSON.stringify(payload);
176
- headers["Content-Type"] = "application/json";
177
- }
178
- const res = await fetch(url, { method: "POST", headers, body });
179
- if (!res.ok) {
180
- const err = await res.json().catch(() => ({ error: "Unknown error" }));
181
- throw new Error(err.error ?? `HTTP ${res.status}`);
182
- }
183
- return res.json();
184
- }
185
- async function submitReplay(serverUrl, projectKey, reportId, events) {
186
- const json = JSON.stringify(events);
187
- const encoded = new TextEncoder().encode(json);
188
- const compressed = gzipSync(encoded);
189
- const url = `${serverUrl.replace(/\/$/, "")}/api/v1/bug-reports/${reportId}/replay`;
190
- await fetch(url, {
191
- method: "POST",
192
- headers: {
193
- "x-project-key": projectKey,
194
- "Content-Type": "application/octet-stream"
195
- },
196
- body: compressed.buffer
197
- });
198
- }
199
-
200
150
  // src/theme.ts
201
- var light = {
202
- background: "rgba(255,255,255,0.90)",
203
- backgroundSecondary: "rgba(249,250,251,0.75)",
204
- accent: "#2563eb",
205
- accentHover: "#1d4ed8",
206
- text: "#111827",
207
- textMuted: "#6b7280",
208
- border: "rgba(255,255,255,0.9)",
209
- 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)",
210
- buttonText: "#ffffff",
211
- backdropFilter: "blur(32px) saturate(1.8)"
212
- };
213
- var dark = {
214
- background: "rgba(15,20,35,0.88)",
215
- backgroundSecondary: "rgba(5,8,18,0.65)",
216
- accent: "#f97316",
217
- accentHover: "#ea6c0a",
218
- text: "#dde3ef",
219
- textMuted: "#6b7a93",
220
- border: "rgba(255,255,255,0.08)",
221
- shadow: "0 24px 60px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04)",
222
- buttonText: "#ffffff",
223
- backdropFilter: "blur(32px) saturate(1.6)"
224
- };
225
- function resolveTheme(theme) {
226
- if (theme === "dark") return dark;
227
- if (theme === "light") return light;
228
- const override = theme;
229
- return {
230
- ...light,
231
- background: override.background ?? light.background,
232
- accent: override.accent ?? light.accent,
233
- accentHover: override.accent ?? light.accentHover,
234
- text: override.text ?? light.text,
235
- border: override.border ?? light.border
236
- };
237
- }
151
+ import { resolveTheme } from "@flint/core";
238
152
 
239
153
  // src/FlintModal.tsx
240
154
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -294,7 +208,12 @@ function FlintModal({
294
208
  getNetworkErrors,
295
209
  getReplayEvents,
296
210
  getExternalReplayUrl,
297
- initialSelection = ""
211
+ initialSelection = "",
212
+ enableScreenshot = true,
213
+ statusPageUrl,
214
+ onBeforeSubmit,
215
+ onSuccess,
216
+ onError
298
217
  }) {
299
218
  const { t } = useTranslation();
300
219
  const colors = resolveTheme(theme);
@@ -354,33 +273,40 @@ function FlintModal({
354
273
  lang: textLang
355
274
  };
356
275
  }
276
+ const payload = {
277
+ reporterId: user?.id ?? "anonymous",
278
+ reporterName: user?.name ?? "Anonymous",
279
+ reporterEmail: user?.email,
280
+ description: isText ? `[Text issue] "${textOriginal.trim()}" \u2192 "${textSuggested.trim()}"` : description.trim(),
281
+ expectedBehavior: !isText ? expectedBehavior.trim() || void 0 : void 0,
282
+ externalReplayUrl: getExternalReplayUrl() || void 0,
283
+ severity: isText ? "P3" : severity,
284
+ url: window.location.href,
285
+ meta: collectedMeta,
286
+ label: isText ? "TEXT" : void 0
287
+ };
288
+ if (onBeforeSubmit) {
289
+ const proceed = await onBeforeSubmit(payload);
290
+ if (!proceed) {
291
+ setStatus("idle");
292
+ return;
293
+ }
294
+ }
357
295
  try {
358
- const res = await submitReport(
359
- serverUrl,
360
- projectKey,
361
- {
362
- reporterId: user?.id ?? "anonymous",
363
- reporterName: user?.name ?? "Anonymous",
364
- description: isText ? `[Text issue] "${textOriginal.trim()}" \u2192 "${textSuggested.trim()}"` : description.trim(),
365
- expectedBehavior: !isText ? expectedBehavior.trim() || void 0 : void 0,
366
- externalReplayUrl: getExternalReplayUrl() || void 0,
367
- severity: isText ? "P3" : severity,
368
- url: window.location.href,
369
- meta: collectedMeta,
370
- label: isText ? "TEXT" : void 0
371
- },
372
- !isText ? screenshot ?? void 0 : void 0
373
- );
296
+ const res = await submitReport(serverUrl, projectKey, payload, !isText ? screenshot ?? void 0 : void 0);
374
297
  setResult(res);
375
298
  setStatus("success");
299
+ onSuccess?.(res);
376
300
  const events = getReplayEvents();
377
301
  if (events.length > 0) {
378
302
  submitReplay(serverUrl, projectKey, res.id, events).catch(() => {
379
303
  });
380
304
  }
381
305
  } catch (err) {
382
- setErrorMsg(err instanceof Error ? err.message : t("errorLabel"));
306
+ const error = err instanceof Error ? err : new Error(t("errorLabel"));
307
+ setErrorMsg(error.message);
383
308
  setStatus("error");
309
+ onError?.(error);
384
310
  }
385
311
  };
386
312
  const inputBorder = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
@@ -430,157 +356,249 @@ function FlintModal({
430
356
  const isSuccess = status === "success";
431
357
  const ringBorder = isSuccess ? "3px solid #22c55e" : `3px solid ${isDark ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.08)"}`;
432
358
  const ringTopColor = isSuccess ? "#22c55e" : colors.accent;
433
- return /* @__PURE__ */ jsx2("div", { style: overlayStyle, children: /* @__PURE__ */ jsxs2("div", { style: modalStyle, role: "dialog", "aria-modal": "true", "aria-label": isSuccess ? t("successTitle") : t("sending"), children: [
434
- /* @__PURE__ */ jsx2(ModalHeader, { colors, inputBorder, showClose: false, onClose }),
435
- /* @__PURE__ */ jsxs2("div", { style: { padding: "40px 32px 48px", textAlign: "center", display: "flex", flexDirection: "column", alignItems: "center" }, children: [
436
- /* @__PURE__ */ jsxs2("div", { style: { position: "relative", width: 80, height: 80, marginBottom: 28 }, children: [
437
- /* @__PURE__ */ jsx2("div", { style: {
438
- position: "absolute",
439
- inset: -10,
440
- borderRadius: "50%",
441
- border: `1.5px solid ${colors.accent}`,
442
- animation: "_flint_ripple 1.8s ease-out infinite",
443
- opacity: isSuccess ? 0 : 1,
444
- transition: "opacity 0.3s ease"
445
- } }),
446
- /* @__PURE__ */ jsx2("div", { style: {
447
- position: "absolute",
448
- inset: -10,
449
- borderRadius: "50%",
450
- border: `1.5px solid ${colors.accent}`,
451
- animation: "_flint_ripple 1.8s ease-out infinite 0.6s",
452
- opacity: isSuccess ? 0 : 1,
453
- transition: "opacity 0.3s ease"
454
- } }),
455
- /* @__PURE__ */ jsx2("div", { style: {
456
- position: "absolute",
457
- inset: 0,
458
- borderRadius: "50%",
459
- border: ringBorder,
460
- borderTopColor: ringTopColor,
461
- animation: isSuccess ? "none" : "_flint_spin 0.85s linear infinite",
462
- transition: "border-color 0.45s ease, border-top-color 0.45s ease"
463
- } }),
464
- /* @__PURE__ */ jsxs2("div", { style: { position: "absolute", inset: 14, borderRadius: "50%" }, children: [
465
- /* @__PURE__ */ jsx2("div", { style: {
466
- position: "absolute",
467
- inset: 0,
468
- borderRadius: "50%",
469
- background: `linear-gradient(135deg, ${colors.accent}30, ${colors.accentHover}50)`,
470
- display: "flex",
471
- alignItems: "center",
472
- justifyContent: "center",
473
- animation: isSuccess ? "none" : "_flint_pulse 2s ease-in-out infinite",
474
- opacity: isSuccess ? 0 : 1,
475
- transition: "opacity 0.3s ease"
476
- }, children: /* @__PURE__ */ jsx2(SparkIcon, { color: colors.accent, size: 20 }) }),
477
- /* @__PURE__ */ jsx2("div", { style: {
478
- position: "absolute",
479
- inset: 0,
480
- borderRadius: "50%",
481
- background: "rgba(34,197,94,0.15)",
482
- display: "flex",
483
- alignItems: "center",
484
- justifyContent: "center",
485
- opacity: isSuccess ? 1 : 0,
486
- transform: isSuccess ? "scale(1)" : "scale(0.65)",
487
- transition: "opacity 0.35s ease 0.2s, transform 0.4s cubic-bezier(0.16,1,0.3,1) 0.2s"
488
- }, children: /* @__PURE__ */ jsx2(CheckIcon, { size: 20 }) })
489
- ] })
490
- ] }),
491
- /* @__PURE__ */ jsxs2("div", { style: { position: "relative", height: 26, width: "100%", marginBottom: 10 }, children: [
492
- /* @__PURE__ */ jsx2("div", { style: {
493
- position: "absolute",
494
- inset: 0,
495
- display: "flex",
496
- alignItems: "center",
497
- justifyContent: "center",
498
- fontSize: 19,
499
- fontWeight: 700,
500
- color: colors.text,
501
- letterSpacing: "-0.02em",
502
- opacity: isSuccess ? 0 : 1,
503
- transition: "opacity 0.25s ease",
504
- pointerEvents: "none"
505
- }, children: t("sending") }),
506
- /* @__PURE__ */ jsx2("div", { style: {
507
- position: "absolute",
508
- inset: 0,
509
- display: "flex",
510
- alignItems: "center",
511
- justifyContent: "center",
512
- fontSize: 19,
513
- fontWeight: 700,
514
- color: colors.text,
515
- letterSpacing: "-0.02em",
516
- opacity: isSuccess ? 1 : 0,
517
- transform: isSuccess ? "translateY(0)" : "translateY(6px)",
518
- transition: "opacity 0.35s ease 0.25s, transform 0.35s ease 0.25s",
519
- pointerEvents: isSuccess ? "auto" : "none"
520
- }, children: t("successTitle") })
521
- ] }),
522
- /* @__PURE__ */ jsxs2("div", { style: { position: "relative", minHeight: 76, width: "100%" }, children: [
523
- /* @__PURE__ */ jsxs2("div", { style: {
524
- position: "absolute",
525
- inset: 0,
526
- display: "flex",
527
- alignItems: "center",
528
- justifyContent: "center",
529
- gap: 6,
530
- color: colors.textMuted,
531
- fontSize: 15,
532
- opacity: isSuccess ? 0 : 1,
533
- transition: "opacity 0.2s ease",
534
- pointerEvents: "none"
535
- }, children: [
536
- /* @__PURE__ */ jsx2("span", { children: t("capturingContext") }),
537
- /* @__PURE__ */ jsx2(SendingDots, { color: colors.accent })
538
- ] }),
539
- /* @__PURE__ */ jsxs2("div", { style: {
540
- position: "absolute",
541
- inset: 0,
542
- display: "flex",
543
- flexDirection: "column",
544
- alignItems: "center",
545
- gap: 10,
546
- opacity: isSuccess ? 1 : 0,
547
- transform: isSuccess ? "translateY(0)" : "translateY(8px)",
548
- transition: "opacity 0.35s ease 0.35s, transform 0.35s ease 0.35s",
549
- pointerEvents: isSuccess ? "auto" : "none"
550
- }, children: [
551
- /* @__PURE__ */ jsx2("p", { style: { fontSize: 15, color: colors.textMuted, margin: 0 }, children: result ? `ID: ${result.id}` : "" }),
552
- /* @__PURE__ */ jsx2(
553
- "button",
554
- {
555
- onClick: onClose,
556
- style: {
557
- width: "100%",
558
- padding: "13px 20px",
559
- borderRadius: 12,
560
- border: "none",
561
- background: `linear-gradient(135deg, ${colors.accent}, ${colors.accentHover})`,
562
- color: colors.buttonText,
563
- fontSize: 17,
564
- fontWeight: 700,
565
- cursor: "pointer",
566
- letterSpacing: "-0.01em",
567
- boxShadow: accentGlow,
568
- fontFamily: "inherit",
569
- display: "flex",
570
- alignItems: "center",
571
- justifyContent: "center",
572
- gap: 8
573
- },
574
- children: t("close")
575
- }
576
- )
577
- ] })
578
- ] })
579
- ] })
580
- ] }) });
359
+ return /* @__PURE__ */ jsx2("div", { style: overlayStyle, children: /* @__PURE__ */ jsxs2(
360
+ "div",
361
+ {
362
+ style: modalStyle,
363
+ role: "dialog",
364
+ "aria-modal": "true",
365
+ "aria-label": isSuccess ? t("successTitle") : t("sending"),
366
+ children: [
367
+ /* @__PURE__ */ jsx2(ModalHeader, { colors, inputBorder, showClose: false, onClose }),
368
+ /* @__PURE__ */ jsxs2(
369
+ "div",
370
+ {
371
+ style: {
372
+ padding: "40px 32px 48px",
373
+ textAlign: "center",
374
+ display: "flex",
375
+ flexDirection: "column",
376
+ alignItems: "center"
377
+ },
378
+ children: [
379
+ /* @__PURE__ */ jsxs2("div", { style: { position: "relative", width: 80, height: 80, marginBottom: 28 }, children: [
380
+ /* @__PURE__ */ jsx2(
381
+ "div",
382
+ {
383
+ style: {
384
+ position: "absolute",
385
+ inset: -10,
386
+ borderRadius: "50%",
387
+ border: `1.5px solid ${colors.accent}`,
388
+ animation: "_flint_ripple 1.8s ease-out infinite",
389
+ opacity: isSuccess ? 0 : 1,
390
+ transition: "opacity 0.3s ease"
391
+ }
392
+ }
393
+ ),
394
+ /* @__PURE__ */ jsx2(
395
+ "div",
396
+ {
397
+ style: {
398
+ position: "absolute",
399
+ inset: -10,
400
+ borderRadius: "50%",
401
+ border: `1.5px solid ${colors.accent}`,
402
+ animation: "_flint_ripple 1.8s ease-out infinite 0.6s",
403
+ opacity: isSuccess ? 0 : 1,
404
+ transition: "opacity 0.3s ease"
405
+ }
406
+ }
407
+ ),
408
+ /* @__PURE__ */ jsx2(
409
+ "div",
410
+ {
411
+ style: {
412
+ position: "absolute",
413
+ inset: 0,
414
+ borderRadius: "50%",
415
+ border: ringBorder,
416
+ borderTopColor: ringTopColor,
417
+ animation: isSuccess ? "none" : "_flint_spin 0.85s linear infinite",
418
+ transition: "border-color 0.45s ease, border-top-color 0.45s ease"
419
+ }
420
+ }
421
+ ),
422
+ /* @__PURE__ */ jsxs2("div", { style: { position: "absolute", inset: 14, borderRadius: "50%" }, children: [
423
+ /* @__PURE__ */ jsx2(
424
+ "div",
425
+ {
426
+ style: {
427
+ position: "absolute",
428
+ inset: 0,
429
+ borderRadius: "50%",
430
+ background: `linear-gradient(135deg, ${colors.accent}30, ${colors.accentHover}50)`,
431
+ display: "flex",
432
+ alignItems: "center",
433
+ justifyContent: "center",
434
+ animation: isSuccess ? "none" : "_flint_pulse 2s ease-in-out infinite",
435
+ opacity: isSuccess ? 0 : 1,
436
+ transition: "opacity 0.3s ease"
437
+ },
438
+ children: /* @__PURE__ */ jsx2(SparkIcon, { color: colors.accent, size: 20 })
439
+ }
440
+ ),
441
+ /* @__PURE__ */ jsx2(
442
+ "div",
443
+ {
444
+ style: {
445
+ position: "absolute",
446
+ inset: 0,
447
+ borderRadius: "50%",
448
+ background: "rgba(34,197,94,0.15)",
449
+ display: "flex",
450
+ alignItems: "center",
451
+ justifyContent: "center",
452
+ opacity: isSuccess ? 1 : 0,
453
+ transform: isSuccess ? "scale(1)" : "scale(0.65)",
454
+ transition: "opacity 0.35s ease 0.2s, transform 0.4s cubic-bezier(0.16,1,0.3,1) 0.2s"
455
+ },
456
+ children: /* @__PURE__ */ jsx2(CheckIcon, { size: 20 })
457
+ }
458
+ )
459
+ ] })
460
+ ] }),
461
+ /* @__PURE__ */ jsxs2("div", { style: { position: "relative", height: 26, width: "100%", marginBottom: 10 }, children: [
462
+ /* @__PURE__ */ jsx2(
463
+ "div",
464
+ {
465
+ style: {
466
+ position: "absolute",
467
+ inset: 0,
468
+ display: "flex",
469
+ alignItems: "center",
470
+ justifyContent: "center",
471
+ fontSize: 19,
472
+ fontWeight: 700,
473
+ color: colors.text,
474
+ letterSpacing: "-0.02em",
475
+ opacity: isSuccess ? 0 : 1,
476
+ transition: "opacity 0.25s ease",
477
+ pointerEvents: "none"
478
+ },
479
+ children: t("sending")
480
+ }
481
+ ),
482
+ /* @__PURE__ */ jsx2(
483
+ "div",
484
+ {
485
+ style: {
486
+ position: "absolute",
487
+ inset: 0,
488
+ display: "flex",
489
+ alignItems: "center",
490
+ justifyContent: "center",
491
+ fontSize: 19,
492
+ fontWeight: 700,
493
+ color: colors.text,
494
+ letterSpacing: "-0.02em",
495
+ opacity: isSuccess ? 1 : 0,
496
+ transform: isSuccess ? "translateY(0)" : "translateY(6px)",
497
+ transition: "opacity 0.35s ease 0.25s, transform 0.35s ease 0.25s",
498
+ pointerEvents: isSuccess ? "auto" : "none"
499
+ },
500
+ children: t("successTitle")
501
+ }
502
+ )
503
+ ] }),
504
+ /* @__PURE__ */ jsxs2("div", { style: { position: "relative", minHeight: 76, width: "100%" }, children: [
505
+ /* @__PURE__ */ jsxs2(
506
+ "div",
507
+ {
508
+ style: {
509
+ position: "absolute",
510
+ inset: 0,
511
+ display: "flex",
512
+ alignItems: "center",
513
+ justifyContent: "center",
514
+ gap: 6,
515
+ color: colors.textMuted,
516
+ fontSize: 15,
517
+ opacity: isSuccess ? 0 : 1,
518
+ transition: "opacity 0.2s ease",
519
+ pointerEvents: "none"
520
+ },
521
+ children: [
522
+ /* @__PURE__ */ jsx2("span", { children: t("capturingContext") }),
523
+ /* @__PURE__ */ jsx2(SendingDots, { color: colors.accent })
524
+ ]
525
+ }
526
+ ),
527
+ /* @__PURE__ */ jsxs2(
528
+ "div",
529
+ {
530
+ style: {
531
+ position: "absolute",
532
+ inset: 0,
533
+ display: "flex",
534
+ flexDirection: "column",
535
+ alignItems: "center",
536
+ gap: 10,
537
+ opacity: isSuccess ? 1 : 0,
538
+ transform: isSuccess ? "translateY(0)" : "translateY(8px)",
539
+ transition: "opacity 0.35s ease 0.35s, transform 0.35s ease 0.35s",
540
+ pointerEvents: isSuccess ? "auto" : "none"
541
+ },
542
+ children: [
543
+ /* @__PURE__ */ jsx2("p", { style: { fontSize: 15, color: colors.textMuted, margin: 0 }, children: result ? `ID: ${result.id}` : "" }),
544
+ statusPageUrl && user?.id && /* @__PURE__ */ jsxs2(
545
+ "a",
546
+ {
547
+ href: `${statusPageUrl}/status?project_key=${encodeURIComponent(projectKey)}&reporter_id=${encodeURIComponent(user.id)}&server_url=${encodeURIComponent(serverUrl)}`,
548
+ target: "_blank",
549
+ rel: "noreferrer",
550
+ style: {
551
+ fontSize: 14,
552
+ color: colors.accent,
553
+ textDecoration: "none",
554
+ fontWeight: 600,
555
+ animation: "_flint_success_up 0.35s ease 0.4s both"
556
+ },
557
+ children: [
558
+ "\u{1F4CB}",
559
+ " Track your bugs ",
560
+ "\u2192"
561
+ ]
562
+ }
563
+ ),
564
+ /* @__PURE__ */ jsx2(
565
+ "button",
566
+ {
567
+ onClick: onClose,
568
+ style: {
569
+ width: "100%",
570
+ padding: "13px 20px",
571
+ borderRadius: 12,
572
+ border: "none",
573
+ background: `linear-gradient(135deg, ${colors.accent}, ${colors.accentHover})`,
574
+ color: colors.buttonText,
575
+ fontSize: 17,
576
+ fontWeight: 700,
577
+ cursor: "pointer",
578
+ letterSpacing: "-0.01em",
579
+ boxShadow: accentGlow,
580
+ fontFamily: "inherit",
581
+ display: "flex",
582
+ alignItems: "center",
583
+ justifyContent: "center",
584
+ gap: 8
585
+ },
586
+ children: t("close")
587
+ }
588
+ )
589
+ ]
590
+ }
591
+ )
592
+ ] })
593
+ ]
594
+ }
595
+ )
596
+ ]
597
+ }
598
+ ) });
581
599
  }
582
600
  return /* @__PURE__ */ jsxs2(Fragment, { children: [
583
- annotating && /* @__PURE__ */ jsx2(
601
+ enableScreenshot && annotating && /* @__PURE__ */ jsx2(
584
602
  ScreenAnnotator,
585
603
  {
586
604
  zIndex: zIndex + 1,
@@ -604,37 +622,43 @@ function FlintModal({
604
622
  }
605
623
  ),
606
624
  /* @__PURE__ */ jsxs2("form", { onSubmit: handleSubmit, style: { padding: "20px 24px 24px" }, children: [
607
- /* @__PURE__ */ jsx2("div", { style: {
608
- display: "flex",
609
- gap: 4,
610
- marginBottom: 20,
611
- background: colors.backgroundSecondary,
612
- borderRadius: 12,
613
- padding: 4,
614
- border: `1px solid ${inputBorder}`
615
- }, children: ["bug", "text"].map((m) => /* @__PURE__ */ jsx2(
616
- "button",
625
+ /* @__PURE__ */ jsx2(
626
+ "div",
617
627
  {
618
- type: "button",
619
- onClick: () => setMode(m),
620
628
  style: {
621
- flex: 1,
622
- padding: "8px 10px",
623
- borderRadius: 9,
624
- border: "none",
625
- cursor: "pointer",
626
- fontSize: 13,
627
- fontWeight: mode === m ? 700 : 500,
628
- fontFamily: "inherit",
629
- transition: "background 0.15s, color 0.15s",
630
- background: mode === m ? `linear-gradient(135deg, ${colors.accent}, ${colors.accentHover})` : "transparent",
631
- color: mode === m ? colors.buttonText : colors.textMuted,
632
- boxShadow: mode === m ? `0 2px 8px ${colors.accent}30` : "none"
629
+ display: "flex",
630
+ gap: 4,
631
+ marginBottom: 20,
632
+ background: colors.backgroundSecondary,
633
+ borderRadius: 12,
634
+ padding: 4,
635
+ border: `1px solid ${inputBorder}`
633
636
  },
634
- children: m === "bug" ? "\u{1F41B} Bug" : "\u{1F524} Text / Translation"
635
- },
636
- m
637
- )) }),
637
+ children: ["bug", "text"].map((m) => /* @__PURE__ */ jsx2(
638
+ "button",
639
+ {
640
+ type: "button",
641
+ onClick: () => setMode(m),
642
+ style: {
643
+ flex: 1,
644
+ padding: "8px 10px",
645
+ borderRadius: 9,
646
+ border: "none",
647
+ cursor: "pointer",
648
+ fontSize: 13,
649
+ fontWeight: mode === m ? 700 : 500,
650
+ fontFamily: "inherit",
651
+ transition: "background 0.15s, color 0.15s",
652
+ background: mode === m ? `linear-gradient(135deg, ${colors.accent}, ${colors.accentHover})` : "transparent",
653
+ color: mode === m ? colors.buttonText : colors.textMuted,
654
+ boxShadow: mode === m ? `0 2px 8px ${colors.accent}30` : "none"
655
+ },
656
+ children: m === "bug" ? "\u{1F41B} Bug" : "\u{1F524} Text / Translation"
657
+ },
658
+ m
659
+ ))
660
+ }
661
+ ),
638
662
  mode === "text" && /* @__PURE__ */ jsxs2(Fragment, { children: [
639
663
  /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 14 }, children: [
640
664
  /* @__PURE__ */ jsx2(FieldLabel, { colors, htmlFor: "flint-text-original", children: "Original text" }),
@@ -731,7 +755,7 @@ function FlintModal({
731
755
  }
732
756
  )
733
757
  ] }),
734
- /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 20, display: mode === "text" ? "none" : void 0 }, children: [
758
+ /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 20, display: mode === "text" || !enableScreenshot ? "none" : void 0 }, children: [
735
759
  /* @__PURE__ */ jsx2(FieldLabel, { colors, children: t("screenshotLabel") }),
736
760
  screenshot ? /* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", gap: 10 }, children: [
737
761
  /* @__PURE__ */ jsx2(
@@ -742,7 +766,20 @@ function FlintModal({
742
766
  style: { height: 60, borderRadius: 8, objectFit: "cover", border: `1px solid ${inputBorder}` }
743
767
  }
744
768
  ),
745
- /* @__PURE__ */ jsx2("span", { style: { fontSize: 13, color: colors.textMuted, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: screenshot.name }),
769
+ /* @__PURE__ */ jsx2(
770
+ "span",
771
+ {
772
+ style: {
773
+ fontSize: 13,
774
+ color: colors.textMuted,
775
+ flex: 1,
776
+ overflow: "hidden",
777
+ textOverflow: "ellipsis",
778
+ whiteSpace: "nowrap"
779
+ },
780
+ children: screenshot.name
781
+ }
782
+ ),
746
783
  /* @__PURE__ */ jsx2(
747
784
  "button",
748
785
  {
@@ -829,31 +866,43 @@ function FlintModal({
829
866
  }
830
867
  )
831
868
  ] }),
832
- /* @__PURE__ */ jsxs2("div", { style: {
833
- display: "flex",
834
- alignItems: "center",
835
- gap: 8,
836
- padding: "9px 12px",
837
- borderRadius: 10,
838
- background: isDark ? "rgba(255,255,255,0.04)" : "rgba(0,77,240,0.04)",
839
- border: `1px solid ${isDark ? "rgba(255,255,255,0.08)" : "rgba(0,77,240,0.1)"}`,
840
- marginBottom: 16
841
- }, children: [
842
- /* @__PURE__ */ jsx2("span", { style: { fontSize: 16 }, children: "\u{1F3A5}" }),
843
- /* @__PURE__ */ jsx2("span", { style: { fontSize: 14, color: colors.textMuted, lineHeight: 1.4 }, children: t("replayInfo") })
844
- ] }),
845
- status === "error" && /* @__PURE__ */ jsxs2("div", { style: {
846
- padding: "10px 13px",
847
- borderRadius: 10,
848
- background: "rgba(239,68,68,0.08)",
849
- border: "1px solid rgba(239,68,68,0.2)",
850
- color: "#f87171",
851
- fontSize: 14,
852
- marginBottom: 16
853
- }, children: [
854
- "\u26A0\uFE0F ",
855
- errorMsg || t("errorLabel")
856
- ] }),
869
+ /* @__PURE__ */ jsxs2(
870
+ "div",
871
+ {
872
+ style: {
873
+ display: "flex",
874
+ alignItems: "center",
875
+ gap: 8,
876
+ padding: "9px 12px",
877
+ borderRadius: 10,
878
+ background: isDark ? "rgba(255,255,255,0.04)" : "rgba(0,77,240,0.04)",
879
+ border: `1px solid ${isDark ? "rgba(255,255,255,0.08)" : "rgba(0,77,240,0.1)"}`,
880
+ marginBottom: 16
881
+ },
882
+ children: [
883
+ /* @__PURE__ */ jsx2("span", { style: { fontSize: 16 }, children: "\u{1F3A5}" }),
884
+ /* @__PURE__ */ jsx2("span", { style: { fontSize: 14, color: colors.textMuted, lineHeight: 1.4 }, children: t("replayInfo") })
885
+ ]
886
+ }
887
+ ),
888
+ status === "error" && /* @__PURE__ */ jsxs2(
889
+ "div",
890
+ {
891
+ style: {
892
+ padding: "10px 13px",
893
+ borderRadius: 10,
894
+ background: "rgba(239,68,68,0.08)",
895
+ border: "1px solid rgba(239,68,68,0.2)",
896
+ color: "#f87171",
897
+ fontSize: 14,
898
+ marginBottom: 16
899
+ },
900
+ children: [
901
+ "\u26A0\uFE0F ",
902
+ errorMsg || t("errorLabel")
903
+ ]
904
+ }
905
+ ),
857
906
  /* @__PURE__ */ jsxs2(
858
907
  "button",
859
908
  {
@@ -914,49 +963,68 @@ function ModalHeader({
914
963
  titleId,
915
964
  title
916
965
  }) {
917
- return /* @__PURE__ */ jsxs2("div", { style: {
918
- display: "flex",
919
- alignItems: "center",
920
- gap: 10,
921
- padding: "16px 20px 14px",
922
- borderBottom: `1px solid ${inputBorder}`
923
- }, children: [
924
- /* @__PURE__ */ jsx2("div", { style: {
925
- width: 28,
926
- height: 28,
927
- borderRadius: 8,
928
- background: `linear-gradient(135deg, ${colors.accent}20, ${colors.accentHover}35)`,
929
- border: `1px solid ${colors.accent}30`,
930
- display: "flex",
931
- alignItems: "center",
932
- justifyContent: "center",
933
- flexShrink: 0
934
- }, children: /* @__PURE__ */ jsx2(SparkIcon, { color: colors.accent, size: 13 }) }),
935
- titleId && title ? /* @__PURE__ */ jsx2("h2", { id: titleId, style: { margin: 0, fontSize: 16, fontWeight: 600, color: colors.text, letterSpacing: "-0.01em", flex: 1 }, children: title }) : /* @__PURE__ */ jsx2("span", { style: { flex: 1, fontSize: 15, fontWeight: 600, color: colors.textMuted }, children: "Flint" }),
936
- showClose && /* @__PURE__ */ jsx2(
937
- "button",
938
- {
939
- onClick: onClose,
940
- "aria-label": "Close",
941
- style: {
942
- background: "none",
943
- border: "none",
944
- cursor: "pointer",
945
- padding: 4,
946
- color: colors.textMuted,
947
- fontSize: 22,
948
- lineHeight: 1,
949
- borderRadius: 6,
950
- display: "flex",
951
- alignItems: "center",
952
- justifyContent: "center",
953
- opacity: 0.6,
954
- fontFamily: "inherit"
955
- },
956
- children: "\xD7"
957
- }
958
- )
959
- ] });
966
+ return /* @__PURE__ */ jsxs2(
967
+ "div",
968
+ {
969
+ style: {
970
+ display: "flex",
971
+ alignItems: "center",
972
+ gap: 10,
973
+ padding: "16px 20px 14px",
974
+ borderBottom: `1px solid ${inputBorder}`
975
+ },
976
+ children: [
977
+ /* @__PURE__ */ jsx2(
978
+ "div",
979
+ {
980
+ style: {
981
+ width: 28,
982
+ height: 28,
983
+ borderRadius: 8,
984
+ background: `linear-gradient(135deg, ${colors.accent}20, ${colors.accentHover}35)`,
985
+ border: `1px solid ${colors.accent}30`,
986
+ display: "flex",
987
+ alignItems: "center",
988
+ justifyContent: "center",
989
+ flexShrink: 0
990
+ },
991
+ children: /* @__PURE__ */ jsx2(SparkIcon, { color: colors.accent, size: 13 })
992
+ }
993
+ ),
994
+ titleId && title ? /* @__PURE__ */ jsx2(
995
+ "h2",
996
+ {
997
+ id: titleId,
998
+ style: { margin: 0, fontSize: 16, fontWeight: 600, color: colors.text, letterSpacing: "-0.01em", flex: 1 },
999
+ children: title
1000
+ }
1001
+ ) : /* @__PURE__ */ jsx2("span", { style: { flex: 1, fontSize: 15, fontWeight: 600, color: colors.textMuted }, children: "Flint" }),
1002
+ showClose && /* @__PURE__ */ jsx2(
1003
+ "button",
1004
+ {
1005
+ onClick: onClose,
1006
+ "aria-label": "Close",
1007
+ style: {
1008
+ background: "none",
1009
+ border: "none",
1010
+ cursor: "pointer",
1011
+ padding: 4,
1012
+ color: colors.textMuted,
1013
+ fontSize: 22,
1014
+ lineHeight: 1,
1015
+ borderRadius: 6,
1016
+ display: "flex",
1017
+ alignItems: "center",
1018
+ justifyContent: "center",
1019
+ opacity: 0.6,
1020
+ fontFamily: "inherit"
1021
+ },
1022
+ children: "\xD7"
1023
+ }
1024
+ )
1025
+ ]
1026
+ }
1027
+ );
960
1028
  }
961
1029
  function FieldLabel({
962
1030
  children,
@@ -1018,19 +1086,74 @@ function SeverityButton({ sev, label, selected, hint, color, accent, border, bg,
1018
1086
  },
1019
1087
  children: [
1020
1088
  /* @__PURE__ */ jsx2("span", { style: { width: 8, height: 8, borderRadius: "50%", background: color, display: "block" } }),
1021
- /* @__PURE__ */ jsx2("span", { style: { fontSize: 13, fontWeight: selected ? 700 : 500, color: selected ? accent : text, letterSpacing: "0.02em" }, children: sev }),
1089
+ /* @__PURE__ */ jsx2(
1090
+ "span",
1091
+ {
1092
+ style: {
1093
+ fontSize: 13,
1094
+ fontWeight: selected ? 700 : 500,
1095
+ color: selected ? accent : text,
1096
+ letterSpacing: "0.02em"
1097
+ },
1098
+ children: sev
1099
+ }
1100
+ ),
1022
1101
  /* @__PURE__ */ jsx2("span", { style: { fontSize: 11, color: selected ? accent : text, opacity: 0.6, letterSpacing: "0.02em" }, children: label })
1023
1102
  ]
1024
1103
  }
1025
1104
  );
1026
1105
  }
1027
1106
  function SparkIcon({ color = "currentColor", size = 14 }) {
1028
- return /* @__PURE__ */ jsx2("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__ */ jsx2("path", { d: "M13 2L3 14h9l-1 8 10-12h-9l1-8z" }) });
1107
+ return /* @__PURE__ */ jsx2(
1108
+ "svg",
1109
+ {
1110
+ width: size,
1111
+ height: size,
1112
+ viewBox: "0 0 24 24",
1113
+ fill: "none",
1114
+ stroke: color,
1115
+ strokeWidth: "2.2",
1116
+ strokeLinecap: "round",
1117
+ strokeLinejoin: "round",
1118
+ "aria-hidden": "true",
1119
+ children: /* @__PURE__ */ jsx2("path", { d: "M13 2L3 14h9l-1 8 10-12h-9l1-8z" })
1120
+ }
1121
+ );
1029
1122
  }
1030
1123
  function CheckIcon({ size = 20 }) {
1031
- return /* @__PURE__ */ jsx2("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__ */ jsx2("polyline", { points: "20 6 9 17 4 12" }) });
1124
+ return /* @__PURE__ */ jsx2(
1125
+ "svg",
1126
+ {
1127
+ width: size,
1128
+ height: size,
1129
+ viewBox: "0 0 24 24",
1130
+ fill: "none",
1131
+ stroke: "#22c55e",
1132
+ strokeWidth: "2.5",
1133
+ strokeLinecap: "round",
1134
+ strokeLinejoin: "round",
1135
+ "aria-hidden": "true",
1136
+ children: /* @__PURE__ */ jsx2("polyline", { points: "20 6 9 17 4 12" })
1137
+ }
1138
+ );
1032
1139
  }
1033
1140
 
1141
+ // src/FlintWidget.tsx
1142
+ import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
1143
+ import { I18nextProvider, useTranslation as useTranslation2 } from "react-i18next";
1144
+
1145
+ // src/collectors/console.ts
1146
+ import { createConsoleCollector } from "@flint/core";
1147
+
1148
+ // src/collectors/environment.ts
1149
+ import { collectEnvironment } from "@flint/core";
1150
+
1151
+ // src/collectors/frustration.ts
1152
+ import { createFrustrationCollector } from "@flint/core";
1153
+
1154
+ // src/collectors/network.ts
1155
+ import { createNetworkCollector } from "@flint/core";
1156
+
1034
1157
  // src/i18n/index.ts
1035
1158
  import { createInstance } from "i18next";
1036
1159
  import { initReactI18next } from "react-i18next";
@@ -1080,251 +1203,15 @@ widgetI18n.use(initReactI18next).init({
1080
1203
  });
1081
1204
  var i18n_default = widgetI18n;
1082
1205
 
1083
- // src/collectors/environment.ts
1084
- function collectEnvironment() {
1085
- const ua = navigator.userAgent;
1086
- let browser = "Unknown";
1087
- const chromeM = ua.match(/Chrome\/(\d+)/);
1088
- const firefoxM = ua.match(/Firefox\/(\d+)/);
1089
- const edgeM = ua.match(/Edg\/(\d+)/);
1090
- const safariM = ua.match(/Version\/(\d+)/);
1091
- const operaM = ua.match(/OPR\/(\d+)/);
1092
- if (operaM) {
1093
- browser = `Opera ${operaM[1]}`;
1094
- } else if (edgeM) {
1095
- browser = `Edge ${edgeM[1]}`;
1096
- } else if (chromeM && !/Edg|OPR/.test(ua)) {
1097
- browser = `Chrome ${chromeM[1]}`;
1098
- } else if (firefoxM) {
1099
- browser = `Firefox ${firefoxM[1]}`;
1100
- } else if (safariM && /Safari\//.test(ua)) {
1101
- browser = `Safari ${safariM[1]}`;
1102
- }
1103
- let os = "Unknown";
1104
- const macM = ua.match(/Mac OS X (\d+[._]\d+)/);
1105
- const winM = ua.match(/Windows NT (\d+\.\d+)/);
1106
- const androidM = ua.match(/Android (\d+)/);
1107
- const iosM = ua.match(/iPhone OS (\d+[._]\d+)/);
1108
- if (macM) {
1109
- os = `macOS ${macM[1].replace("_", ".")}`;
1110
- } else if (winM) {
1111
- const winMap = {
1112
- "10.0": "10/11",
1113
- "6.3": "8.1",
1114
- "6.2": "8",
1115
- "6.1": "7"
1116
- };
1117
- os = `Windows ${winMap[winM[1]] ?? winM[1]}`;
1118
- } else if (androidM) {
1119
- os = `Android ${androidM[1]}`;
1120
- } else if (iosM) {
1121
- os = `iOS ${iosM[1].replace(/_/g, ".")}`;
1122
- } else if (/Linux/.test(ua)) {
1123
- os = "Linux";
1124
- }
1125
- return {
1126
- browser,
1127
- os,
1128
- viewport: `${window.innerWidth}x${window.innerHeight}`,
1129
- screen: `${screen.width}x${screen.height}`,
1130
- language: navigator.language,
1131
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
1132
- online: navigator.onLine
1133
- };
1134
- }
1135
-
1136
- // src/collectors/console.ts
1137
- var MAX_ENTRIES = 50;
1138
- function createConsoleCollector() {
1139
- const entries = [];
1140
- let active = false;
1141
- const originals = {
1142
- log: console.log.bind(console),
1143
- warn: console.warn.bind(console),
1144
- error: console.error.bind(console)
1145
- };
1146
- let origOnerror = null;
1147
- let origUnhandled = null;
1148
- function push(level, args) {
1149
- let str;
1150
- try {
1151
- str = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
1152
- } catch {
1153
- str = String(args[0]);
1154
- }
1155
- if (str.length > 500) str = str.slice(0, 500) + "\u2026";
1156
- entries.push({ level, args: str, timestamp: Date.now() });
1157
- if (entries.length > MAX_ENTRIES) entries.shift();
1158
- }
1159
- return {
1160
- start() {
1161
- if (active) return;
1162
- active = true;
1163
- console.log = (...args) => {
1164
- push("log", args);
1165
- originals.log(...args);
1166
- };
1167
- console.warn = (...args) => {
1168
- push("warn", args);
1169
- originals.warn(...args);
1170
- };
1171
- console.error = (...args) => {
1172
- push("error", args);
1173
- originals.error(...args);
1174
- };
1175
- origOnerror = window.onerror;
1176
- window.onerror = (msg, src, line, col, err) => {
1177
- push("error", [err?.message ?? String(msg), `${src}:${line}:${col}`]);
1178
- if (typeof origOnerror === "function")
1179
- return origOnerror(msg, src, line, col, err);
1180
- return false;
1181
- };
1182
- origUnhandled = window.onunhandledrejection;
1183
- window.onunhandledrejection = (event) => {
1184
- const reason = event.reason instanceof Error ? event.reason.message : JSON.stringify(event.reason);
1185
- push("error", ["UnhandledRejection:", reason]);
1186
- if (typeof origUnhandled === "function")
1187
- origUnhandled.call(window, event);
1188
- };
1189
- },
1190
- stop() {
1191
- if (!active) return;
1192
- active = false;
1193
- console.log = originals.log;
1194
- console.warn = originals.warn;
1195
- console.error = originals.error;
1196
- window.onerror = origOnerror;
1197
- window.onunhandledrejection = origUnhandled;
1198
- },
1199
- getEntries() {
1200
- return [...entries];
1201
- }
1202
- };
1203
- }
1204
-
1205
- // src/collectors/network.ts
1206
- var MAX_ENTRIES2 = 50;
1207
- var BLOCKED_HOSTS = /* @__PURE__ */ new Set([
1208
- "browser-intake-datadoghq.com",
1209
- "rum.browser-intake-datadoghq.com",
1210
- "logs.browser-intake-datadoghq.com",
1211
- "session-replay.browser-intake-datadoghq.com"
1212
- ]);
1213
- function isBlockedUrl(url, extra) {
1214
- try {
1215
- const host = new URL(url, location.href).hostname;
1216
- const all = [...BLOCKED_HOSTS, ...extra];
1217
- return all.some((b) => host === b || host.endsWith("." + b));
1218
- } catch {
1219
- return false;
1220
- }
1221
- }
1222
- function truncateUrl(url) {
1223
- try {
1224
- const u = new URL(url, location.href);
1225
- const base = `${u.origin}${u.pathname}`;
1226
- return base.length > 200 ? base.slice(0, 200) + "\u2026" : base;
1227
- } catch {
1228
- return url.length > 200 ? url.slice(0, 200) + "\u2026" : url;
1229
- }
1230
- }
1231
- function createNetworkCollector(extraBlockedHosts = []) {
1232
- const entries = [];
1233
- const blocked = new Set(extraBlockedHosts);
1234
- let origFetch = null;
1235
- let origXHROpen = null;
1236
- let active = false;
1237
- function push(entry) {
1238
- entries.push(entry);
1239
- if (entries.length > MAX_ENTRIES2) entries.shift();
1240
- }
1241
- return {
1242
- start() {
1243
- if (active) return;
1244
- active = true;
1245
- origFetch = window.fetch;
1246
- window.fetch = async (input, init) => {
1247
- const method = (init?.method ?? "GET").toUpperCase();
1248
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1249
- const startTime = Date.now();
1250
- const res = await origFetch.call(window, input, init);
1251
- if (res.status >= 400 && !isBlockedUrl(url, blocked)) {
1252
- push({
1253
- method,
1254
- url: truncateUrl(url),
1255
- status: res.status,
1256
- duration: Date.now() - startTime,
1257
- timestamp: startTime
1258
- });
1259
- }
1260
- return res;
1261
- };
1262
- origXHROpen = XMLHttpRequest.prototype.open;
1263
- XMLHttpRequest.prototype.open = function(method, url, async, username, password) {
1264
- const startTime = Date.now();
1265
- const urlStr = typeof url === "string" ? url : url.href;
1266
- this.addEventListener("load", () => {
1267
- if (this.status >= 400 && !isBlockedUrl(urlStr, blocked)) {
1268
- push({
1269
- method: method.toUpperCase(),
1270
- url: truncateUrl(urlStr),
1271
- status: this.status,
1272
- duration: Date.now() - startTime,
1273
- timestamp: startTime
1274
- });
1275
- }
1276
- });
1277
- return origXHROpen.apply(this, [
1278
- method,
1279
- url,
1280
- async ?? true,
1281
- username,
1282
- password
1283
- ]);
1284
- };
1285
- },
1286
- stop() {
1287
- if (!active) return;
1288
- active = false;
1289
- if (origFetch) window.fetch = origFetch;
1290
- if (origXHROpen) XMLHttpRequest.prototype.open = origXHROpen;
1291
- },
1292
- getEntries() {
1293
- return [...entries];
1294
- }
1295
- };
1296
- }
1297
-
1298
1206
  // src/store.ts
1207
+ import { getSnapshot, subscribe } from "@flint/core";
1299
1208
  import { useSyncExternalStore } from "react";
1300
- var state = { user: void 0, sessionReplay: void 0 };
1301
- var listeners = /* @__PURE__ */ new Set();
1302
- function emit() {
1303
- listeners.forEach((fn) => fn());
1304
- }
1305
- function subscribeStore(fn) {
1306
- listeners.add(fn);
1307
- return () => listeners.delete(fn);
1308
- }
1309
- function getSnapshot() {
1310
- return state;
1311
- }
1209
+ import { flint as flint2 } from "@flint/core";
1312
1210
  function useFlintStore() {
1313
- return useSyncExternalStore(subscribeStore, getSnapshot);
1211
+ return useSyncExternalStore(subscribe, getSnapshot);
1314
1212
  }
1315
- var flint = {
1316
- setUser(user) {
1317
- state = { ...state, user: user ?? void 0 };
1318
- emit();
1319
- },
1320
- setSessionReplay(url) {
1321
- state = { ...state, sessionReplay: url };
1322
- emit();
1323
- }
1324
- };
1325
1213
 
1326
1214
  // src/FlintWidget.tsx
1327
- import { record } from "rrweb";
1328
1215
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1329
1216
  var REPLAY_WINDOW_MS = 6e4;
1330
1217
  function FlintWidget(props) {
@@ -1343,7 +1230,19 @@ function WidgetContent({
1343
1230
  buttonLabel,
1344
1231
  theme = "dark",
1345
1232
  zIndex = 9999,
1346
- datadogSite
1233
+ statusPageUrl,
1234
+ datadogSite,
1235
+ enableReplay = true,
1236
+ enableScreenshot = true,
1237
+ enableConsole = true,
1238
+ enableNetwork = true,
1239
+ enableFrustration = false,
1240
+ autoReportFrustration = false,
1241
+ onBeforeSubmit,
1242
+ onSuccess,
1243
+ onError,
1244
+ onOpen,
1245
+ onClose
1347
1246
  }) {
1348
1247
  const globalState = useFlintStore();
1349
1248
  const resolvedUser = user ?? globalState.user;
@@ -1357,9 +1256,9 @@ function WidgetContent({
1357
1256
  const ddRum = window.DD_RUM;
1358
1257
  const ctx = ddRum?.getInternalContext?.();
1359
1258
  if (ctx?.session_id) {
1360
- const now = Date.now();
1361
- const fromTs = now - 3e4;
1362
- const toTs = now + 5e3;
1259
+ const now = Math.floor(Date.now() / 1e3);
1260
+ const fromTs = now - 30;
1261
+ const toTs = now + 5;
1363
1262
  return `https://${datadogSite}/rum/replay/sessions/${ctx.session_id}?from_ts=${fromTs}&to_ts=${toTs}&tab=replay&live=false`;
1364
1263
  }
1365
1264
  } catch {
@@ -1412,16 +1311,17 @@ function WidgetContent({
1412
1311
  pendingSelection.current = text;
1413
1312
  setSelectionTooltip(null);
1414
1313
  setOpen(true);
1314
+ onOpen?.();
1415
1315
  };
1416
1316
  const consoleCollector = useRef3(null);
1417
1317
  const networkCollector = useRef3(null);
1418
1318
  const replayEvents = useRef3([]);
1419
1319
  const stopReplay = useRef3(null);
1420
- if (!consoleCollector.current) {
1320
+ if (enableConsole && !consoleCollector.current) {
1421
1321
  consoleCollector.current = createConsoleCollector();
1422
1322
  consoleCollector.current.start();
1423
1323
  }
1424
- if (!networkCollector.current) {
1324
+ if (enableNetwork && !networkCollector.current) {
1425
1325
  const flintHost = (() => {
1426
1326
  try {
1427
1327
  return new URL(serverUrl).hostname;
@@ -1432,23 +1332,58 @@ function WidgetContent({
1432
1332
  networkCollector.current = createNetworkCollector(flintHost ? [flintHost] : []);
1433
1333
  networkCollector.current.start();
1434
1334
  }
1335
+ const frustrationCollector = useRef3(null);
1336
+ if (enableFrustration && !frustrationCollector.current) {
1337
+ frustrationCollector.current = createFrustrationCollector();
1338
+ frustrationCollector.current.start();
1339
+ }
1435
1340
  useEffect3(() => {
1436
- const stopFn = record({
1437
- emit(event) {
1438
- replayEvents.current.push(event);
1439
- const cutoff = Date.now() - REPLAY_WINDOW_MS;
1440
- while (replayEvents.current.length > 0 && replayEvents.current[0].timestamp < cutoff) {
1441
- replayEvents.current.shift();
1442
- }
1443
- }
1444
- });
1445
- stopReplay.current = stopFn ?? null;
1341
+ let cancelled = false;
1342
+ if (enableReplay) {
1343
+ import("rrweb").then(({ record }) => {
1344
+ if (cancelled) return;
1345
+ const stopFn = record({
1346
+ emit(event) {
1347
+ replayEvents.current.push(event);
1348
+ const cutoff = Date.now() - REPLAY_WINDOW_MS;
1349
+ while (replayEvents.current.length > 0 && replayEvents.current[0].timestamp < cutoff) {
1350
+ replayEvents.current.shift();
1351
+ }
1352
+ }
1353
+ });
1354
+ stopReplay.current = stopFn ?? null;
1355
+ });
1356
+ }
1446
1357
  return () => {
1358
+ cancelled = true;
1447
1359
  consoleCollector.current?.stop();
1448
1360
  networkCollector.current?.stop();
1361
+ frustrationCollector.current?.stop();
1449
1362
  stopReplay.current?.();
1450
1363
  };
1451
- }, []);
1364
+ }, [enableReplay]);
1365
+ useEffect3(() => {
1366
+ if (!enableFrustration || !autoReportFrustration || !frustrationCollector.current) return;
1367
+ const unsubscribe = frustrationCollector.current.onFrustration(async (event) => {
1368
+ const user2 = resolvedUser;
1369
+ await submitReport(serverUrl, projectKey, {
1370
+ reporterId: user2?.id ?? "anonymous",
1371
+ reporterName: user2?.name ?? "Anonymous",
1372
+ reporterEmail: user2?.email,
1373
+ description: `[Auto-detected] ${event.type.replace(/_/g, " ")}: ${event.details}`,
1374
+ severity: event.type === "error_loop" ? "P1" : event.type === "rage_click" ? "P2" : "P3",
1375
+ url: event.url,
1376
+ meta: {
1377
+ environment: collectEnvironment(),
1378
+ consoleLogs: consoleCollector.current?.getEntries() ?? [],
1379
+ networkErrors: networkCollector.current?.getEntries() ?? [],
1380
+ frustrationEvent: event
1381
+ }
1382
+ }).catch(() => {
1383
+ });
1384
+ });
1385
+ return unsubscribe;
1386
+ }, [enableFrustration, autoReportFrustration]);
1452
1387
  const label = buttonLabel ?? t("buttonLabel");
1453
1388
  return /* @__PURE__ */ jsxs3(Fragment2, { children: [
1454
1389
  /* @__PURE__ */ jsxs3(
@@ -1458,7 +1393,10 @@ function WidgetContent({
1458
1393
  onMouseDown: () => {
1459
1394
  pendingSelection.current = window.getSelection()?.toString().trim() ?? "";
1460
1395
  },
1461
- onClick: () => setOpen(true),
1396
+ onClick: () => {
1397
+ setOpen(true);
1398
+ onOpen?.();
1399
+ },
1462
1400
  onMouseEnter: () => setHovered(true),
1463
1401
  onMouseLeave: () => setHovered(false),
1464
1402
  "aria-label": label,
@@ -1539,6 +1477,7 @@ function WidgetContent({
1539
1477
  zIndex,
1540
1478
  onClose: () => {
1541
1479
  setOpen(false);
1480
+ onClose?.();
1542
1481
  pendingSelection.current = "";
1543
1482
  },
1544
1483
  getEnvironment: collectEnvironment,
@@ -1546,18 +1485,37 @@ function WidgetContent({
1546
1485
  getNetworkErrors: () => networkCollector.current?.getEntries() ?? [],
1547
1486
  getReplayEvents: () => [...replayEvents.current],
1548
1487
  getExternalReplayUrl,
1549
- initialSelection: pendingSelection.current
1488
+ initialSelection: pendingSelection.current,
1489
+ enableScreenshot,
1490
+ statusPageUrl,
1491
+ onBeforeSubmit,
1492
+ onSuccess,
1493
+ onError
1550
1494
  }
1551
1495
  )
1552
1496
  ] });
1553
1497
  }
1554
1498
  function TextIcon() {
1555
- return /* @__PURE__ */ jsxs3("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
1556
- /* @__PURE__ */ jsx3("path", { d: "M17 10H3" }),
1557
- /* @__PURE__ */ jsx3("path", { d: "M21 6H3" }),
1558
- /* @__PURE__ */ jsx3("path", { d: "M21 14H3" }),
1559
- /* @__PURE__ */ jsx3("path", { d: "M17 18H3" })
1560
- ] });
1499
+ return /* @__PURE__ */ jsxs3(
1500
+ "svg",
1501
+ {
1502
+ width: "12",
1503
+ height: "12",
1504
+ viewBox: "0 0 24 24",
1505
+ fill: "none",
1506
+ stroke: "currentColor",
1507
+ strokeWidth: "2.2",
1508
+ strokeLinecap: "round",
1509
+ strokeLinejoin: "round",
1510
+ "aria-hidden": "true",
1511
+ children: [
1512
+ /* @__PURE__ */ jsx3("path", { d: "M17 10H3" }),
1513
+ /* @__PURE__ */ jsx3("path", { d: "M21 6H3" }),
1514
+ /* @__PURE__ */ jsx3("path", { d: "M21 14H3" }),
1515
+ /* @__PURE__ */ jsx3("path", { d: "M17 18H3" })
1516
+ ]
1517
+ }
1518
+ );
1561
1519
  }
1562
1520
  function SparkIcon2() {
1563
1521
  return /* @__PURE__ */ jsx3(
@@ -1579,6 +1537,6 @@ function SparkIcon2() {
1579
1537
  export {
1580
1538
  FlintModal,
1581
1539
  FlintWidget,
1582
- flint
1540
+ flint2 as flint
1583
1541
  };
1584
1542
  //# sourceMappingURL=index.js.map