@diegotsi/flint-react 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/FlintWidget.tsx
2
- import { useState as useState3, useEffect as useEffect3, useRef as useRef3 } from "react";
2
+ import { useState as useState3, useEffect as useEffect3, useRef as useRef3, useCallback as useCallback2 } from "react";
3
3
  import { I18nextProvider, useTranslation as useTranslation2 } from "react-i18next";
4
4
 
5
5
  // src/FlintModal.tsx
@@ -293,7 +293,8 @@ function FlintModal({
293
293
  getConsoleLogs,
294
294
  getNetworkErrors,
295
295
  getReplayEvents,
296
- getExternalReplayUrl
296
+ getExternalReplayUrl,
297
+ initialSelection = ""
297
298
  }) {
298
299
  const { t } = useTranslation();
299
300
  const colors = resolveTheme(theme);
@@ -306,6 +307,12 @@ function FlintModal({
306
307
  const [status, setStatus] = useState2("idle");
307
308
  const [result, setResult] = useState2(null);
308
309
  const [errorMsg, setErrorMsg] = useState2("");
310
+ const [mode, setMode] = useState2(initialSelection ? "text" : "bug");
311
+ const [textOriginal, setTextOriginal] = useState2(initialSelection);
312
+ const [textSuggested, setTextSuggested] = useState2("");
313
+ const [textLang, setTextLang] = useState2(
314
+ typeof document !== "undefined" ? document.documentElement.lang?.split("-")[0] || "en" : "en"
315
+ );
309
316
  const fileRef = useRef2(null);
310
317
  const overlayRef = useRef2(null);
311
318
  useEffect2(() => {
@@ -326,7 +333,12 @@ function FlintModal({
326
333
  );
327
334
  const handleSubmit = async (e) => {
328
335
  e.preventDefault();
329
- if (!description.trim()) return;
336
+ const isText = mode === "text";
337
+ if (isText) {
338
+ if (!textOriginal.trim() || !textSuggested.trim()) return;
339
+ } else {
340
+ if (!description.trim()) return;
341
+ }
330
342
  setStatus("submitting");
331
343
  setErrorMsg("");
332
344
  const collectedMeta = {
@@ -335,6 +347,13 @@ function FlintModal({
335
347
  consoleLogs: getConsoleLogs(),
336
348
  networkErrors: getNetworkErrors()
337
349
  };
350
+ if (isText) {
351
+ collectedMeta.textIssue = {
352
+ original: textOriginal.trim(),
353
+ suggested: textSuggested.trim(),
354
+ lang: textLang
355
+ };
356
+ }
338
357
  try {
339
358
  const res = await submitReport(
340
359
  serverUrl,
@@ -342,14 +361,15 @@ function FlintModal({
342
361
  {
343
362
  reporterId: user?.id ?? "anonymous",
344
363
  reporterName: user?.name ?? "Anonymous",
345
- description: description.trim(),
346
- expectedBehavior: expectedBehavior.trim() || void 0,
364
+ description: isText ? `[Text issue] "${textOriginal.trim()}" \u2192 "${textSuggested.trim()}"` : description.trim(),
365
+ expectedBehavior: !isText ? expectedBehavior.trim() || void 0 : void 0,
347
366
  externalReplayUrl: getExternalReplayUrl() || void 0,
348
- severity,
367
+ severity: isText ? "P3" : severity,
349
368
  url: window.location.href,
350
- meta: collectedMeta
369
+ meta: collectedMeta,
370
+ label: isText ? "TEXT" : void 0
351
371
  },
352
- screenshot ?? void 0
372
+ !isText ? screenshot ?? void 0 : void 0
353
373
  );
354
374
  setResult(res);
355
375
  setStatus("success");
@@ -584,7 +604,88 @@ function FlintModal({
584
604
  }
585
605
  ),
586
606
  /* @__PURE__ */ jsxs2("form", { onSubmit: handleSubmit, style: { padding: "20px 24px 24px" }, children: [
587
- /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 18 }, 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",
617
+ {
618
+ type: "button",
619
+ onClick: () => setMode(m),
620
+ 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"
633
+ },
634
+ children: m === "bug" ? "\u{1F41B} Bug" : "\u{1F524} Text / Translation"
635
+ },
636
+ m
637
+ )) }),
638
+ mode === "text" && /* @__PURE__ */ jsxs2(Fragment, { children: [
639
+ /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 14 }, children: [
640
+ /* @__PURE__ */ jsx2(FieldLabel, { colors, htmlFor: "flint-text-original", children: "Original text" }),
641
+ /* @__PURE__ */ jsx2(
642
+ "textarea",
643
+ {
644
+ id: "flint-text-original",
645
+ style: { ...inputStyle, resize: "vertical", minHeight: 60 },
646
+ value: textOriginal,
647
+ onChange: (e) => setTextOriginal(e.target.value),
648
+ placeholder: "Text that is wrong on screen\u2026",
649
+ required: true
650
+ }
651
+ )
652
+ ] }),
653
+ /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 14 }, children: [
654
+ /* @__PURE__ */ jsx2(FieldLabel, { colors, htmlFor: "flint-text-suggested", children: "Suggested correction" }),
655
+ /* @__PURE__ */ jsx2(
656
+ "textarea",
657
+ {
658
+ id: "flint-text-suggested",
659
+ style: { ...inputStyle, resize: "vertical", minHeight: 60 },
660
+ value: textSuggested,
661
+ onChange: (e) => setTextSuggested(e.target.value),
662
+ placeholder: "How it should read\u2026",
663
+ required: true
664
+ }
665
+ )
666
+ ] }),
667
+ /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 20 }, children: [
668
+ /* @__PURE__ */ jsx2(FieldLabel, { colors, htmlFor: "flint-text-lang", children: "Language" }),
669
+ /* @__PURE__ */ jsxs2(
670
+ "select",
671
+ {
672
+ id: "flint-text-lang",
673
+ value: textLang,
674
+ onChange: (e) => setTextLang(e.target.value),
675
+ style: {
676
+ ...inputStyle,
677
+ appearance: "none",
678
+ cursor: "pointer"
679
+ },
680
+ children: [
681
+ /* @__PURE__ */ jsx2("option", { value: "en", children: "English (en)" }),
682
+ /* @__PURE__ */ jsx2("option", { value: "he", children: "\u05E2\u05D1\u05E8\u05D9\u05EA (he)" })
683
+ ]
684
+ }
685
+ )
686
+ ] })
687
+ ] }),
688
+ /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 18, display: mode === "text" ? "none" : void 0 }, children: [
588
689
  /* @__PURE__ */ jsx2(FieldLabel, { colors, children: t("severityLabel") }),
589
690
  /* @__PURE__ */ jsx2("div", { style: { display: "grid", gridTemplateColumns: "repeat(4,1fr)", gap: 8 }, children: SEVERITIES.map((sev) => /* @__PURE__ */ jsx2(
590
691
  SeverityButton,
@@ -603,7 +704,7 @@ function FlintModal({
603
704
  sev
604
705
  )) })
605
706
  ] }),
606
- /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 14 }, children: [
707
+ mode === "bug" && /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 14 }, children: [
607
708
  /* @__PURE__ */ jsx2(FieldLabel, { colors, htmlFor: "flint-description", children: t("whatIsBrokenLabel") }),
608
709
  /* @__PURE__ */ jsx2(
609
710
  "textarea",
@@ -617,7 +718,7 @@ function FlintModal({
617
718
  }
618
719
  )
619
720
  ] }),
620
- /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 14 }, children: [
721
+ mode === "bug" && /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 14 }, children: [
621
722
  /* @__PURE__ */ jsx2(FieldLabel, { colors, htmlFor: "flint-expected", children: t("expectedBehaviorLabel") }),
622
723
  /* @__PURE__ */ jsx2(
623
724
  "textarea",
@@ -630,7 +731,7 @@ function FlintModal({
630
731
  }
631
732
  )
632
733
  ] }),
633
- /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 20 }, children: [
734
+ /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 20, display: mode === "text" ? "none" : void 0 }, children: [
634
735
  /* @__PURE__ */ jsx2(FieldLabel, { colors, children: t("screenshotLabel") }),
635
736
  screenshot ? /* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", gap: 10 }, children: [
636
737
  /* @__PURE__ */ jsx2(
@@ -1102,7 +1203,7 @@ function createConsoleCollector() {
1102
1203
  }
1103
1204
 
1104
1205
  // src/collectors/network.ts
1105
- var MAX_ENTRIES2 = 20;
1206
+ var MAX_ENTRIES2 = 50;
1106
1207
  var BLOCKED_HOSTS = /* @__PURE__ */ new Set([
1107
1208
  "browser-intake-datadoghq.com",
1108
1209
  "rum.browser-intake-datadoghq.com",
@@ -1147,7 +1248,7 @@ function createNetworkCollector(extraBlockedHosts = []) {
1147
1248
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1148
1249
  const startTime = Date.now();
1149
1250
  const res = await origFetch.call(window, input, init);
1150
- if (!isBlockedUrl(url, blocked)) {
1251
+ if (res.status >= 400 && !isBlockedUrl(url, blocked)) {
1151
1252
  push({
1152
1253
  method,
1153
1254
  url: truncateUrl(url),
@@ -1163,7 +1264,7 @@ function createNetworkCollector(extraBlockedHosts = []) {
1163
1264
  const startTime = Date.now();
1164
1265
  const urlStr = typeof url === "string" ? url : url.href;
1165
1266
  this.addEventListener("load", () => {
1166
- if (!isBlockedUrl(urlStr, blocked)) {
1267
+ if (this.status >= 400 && !isBlockedUrl(urlStr, blocked)) {
1167
1268
  push({
1168
1269
  method: method.toUpperCase(),
1169
1270
  url: truncateUrl(urlStr),
@@ -1241,19 +1342,77 @@ function WidgetContent({
1241
1342
  extraFields,
1242
1343
  buttonLabel,
1243
1344
  theme = "dark",
1244
- zIndex = 9999
1345
+ zIndex = 9999,
1346
+ datadogSite
1245
1347
  }) {
1246
1348
  const globalState = useFlintStore();
1247
1349
  const resolvedUser = user ?? globalState.user;
1248
1350
  const resolvedSessionReplay = extraFields?.sessionReplay ?? globalState.sessionReplay;
1249
1351
  const getExternalReplayUrl = () => {
1250
1352
  const src = resolvedSessionReplay;
1251
- return typeof src === "function" ? src() : src;
1353
+ const explicit = typeof src === "function" ? src() : src;
1354
+ if (explicit) return explicit;
1355
+ if (datadogSite) {
1356
+ try {
1357
+ const ddRum = window.DD_RUM;
1358
+ const ctx = ddRum?.getInternalContext?.();
1359
+ if (ctx?.session_id) {
1360
+ const now = Date.now();
1361
+ const fromTs = now - 3e4;
1362
+ const toTs = now + 5e3;
1363
+ return `https://${datadogSite}/rum/replay/sessions/${ctx.session_id}?from_ts=${fromTs}&to_ts=${toTs}&tab=replay&live=false`;
1364
+ }
1365
+ } catch {
1366
+ }
1367
+ }
1368
+ return void 0;
1252
1369
  };
1253
1370
  const { t } = useTranslation2();
1254
1371
  const [open, setOpen] = useState3(false);
1255
1372
  const [hovered, setHovered] = useState3(false);
1373
+ const pendingSelection = useRef3("");
1256
1374
  const colors = resolveTheme(theme);
1375
+ const [selectionTooltip, setSelectionTooltip] = useState3(null);
1376
+ const tooltipRef = useRef3(null);
1377
+ const triggerRef = useRef3(null);
1378
+ const handleMouseUp = useCallback2((e) => {
1379
+ const target = e.target;
1380
+ if (tooltipRef.current?.contains(target)) return;
1381
+ if (triggerRef.current?.contains(target)) return;
1382
+ requestAnimationFrame(() => {
1383
+ const sel = window.getSelection();
1384
+ const text = sel?.toString().trim() ?? "";
1385
+ if (text.length < 2) {
1386
+ setSelectionTooltip(null);
1387
+ return;
1388
+ }
1389
+ const range = sel?.getRangeAt(0);
1390
+ if (!range) return;
1391
+ const rect = range.getBoundingClientRect();
1392
+ setSelectionTooltip({
1393
+ text,
1394
+ x: rect.left + rect.width / 2,
1395
+ y: rect.top - 8
1396
+ });
1397
+ });
1398
+ }, []);
1399
+ const handleSelectionChange = useCallback2(() => {
1400
+ const text = window.getSelection()?.toString().trim() ?? "";
1401
+ if (text.length < 2) setSelectionTooltip(null);
1402
+ }, []);
1403
+ useEffect3(() => {
1404
+ document.addEventListener("mouseup", handleMouseUp);
1405
+ document.addEventListener("selectionchange", handleSelectionChange);
1406
+ return () => {
1407
+ document.removeEventListener("mouseup", handleMouseUp);
1408
+ document.removeEventListener("selectionchange", handleSelectionChange);
1409
+ };
1410
+ }, [handleMouseUp, handleSelectionChange]);
1411
+ const openWithSelection = (text) => {
1412
+ pendingSelection.current = text;
1413
+ setSelectionTooltip(null);
1414
+ setOpen(true);
1415
+ };
1257
1416
  const consoleCollector = useRef3(null);
1258
1417
  const networkCollector = useRef3(null);
1259
1418
  const replayEvents = useRef3([]);
@@ -1295,6 +1454,10 @@ function WidgetContent({
1295
1454
  /* @__PURE__ */ jsxs3(
1296
1455
  "button",
1297
1456
  {
1457
+ ref: triggerRef,
1458
+ onMouseDown: () => {
1459
+ pendingSelection.current = window.getSelection()?.toString().trim() ?? "";
1460
+ },
1298
1461
  onClick: () => setOpen(true),
1299
1462
  onMouseEnter: () => setHovered(true),
1300
1463
  onMouseLeave: () => setHovered(false),
@@ -1327,6 +1490,44 @@ function WidgetContent({
1327
1490
  ]
1328
1491
  }
1329
1492
  ),
1493
+ selectionTooltip && !open && /* @__PURE__ */ jsxs3(
1494
+ "button",
1495
+ {
1496
+ ref: tooltipRef,
1497
+ onClick: () => openWithSelection(selectionTooltip.text),
1498
+ style: {
1499
+ position: "fixed",
1500
+ left: Math.max(8, Math.min(selectionTooltip.x - 70, window.innerWidth - 148)),
1501
+ top: Math.max(8, selectionTooltip.y - 36),
1502
+ zIndex: zIndex + 1,
1503
+ display: "flex",
1504
+ alignItems: "center",
1505
+ gap: 6,
1506
+ padding: "6px 12px",
1507
+ borderRadius: 10,
1508
+ border: "none",
1509
+ background: `linear-gradient(135deg, ${colors.accent}, ${colors.accentHover})`,
1510
+ color: colors.buttonText,
1511
+ fontSize: 12,
1512
+ fontWeight: 600,
1513
+ cursor: "pointer",
1514
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1515
+ boxShadow: `0 4px 20px rgba(0,0,0,0.25), 0 0 12px ${colors.accent}40`,
1516
+ animation: "flint-tooltip-in 0.15s ease-out",
1517
+ whiteSpace: "nowrap"
1518
+ },
1519
+ children: [
1520
+ /* @__PURE__ */ jsx3(TextIcon, {}),
1521
+ "Report text issue"
1522
+ ]
1523
+ }
1524
+ ),
1525
+ /* @__PURE__ */ jsx3("style", { children: `
1526
+ @keyframes flint-tooltip-in {
1527
+ from { opacity: 0; transform: translateY(4px); }
1528
+ to { opacity: 1; transform: translateY(0); }
1529
+ }
1530
+ ` }),
1330
1531
  open && /* @__PURE__ */ jsx3(
1331
1532
  FlintModal,
1332
1533
  {
@@ -1336,16 +1537,28 @@ function WidgetContent({
1336
1537
  meta,
1337
1538
  theme,
1338
1539
  zIndex,
1339
- onClose: () => setOpen(false),
1540
+ onClose: () => {
1541
+ setOpen(false);
1542
+ pendingSelection.current = "";
1543
+ },
1340
1544
  getEnvironment: collectEnvironment,
1341
1545
  getConsoleLogs: () => consoleCollector.current?.getEntries() ?? [],
1342
1546
  getNetworkErrors: () => networkCollector.current?.getEntries() ?? [],
1343
1547
  getReplayEvents: () => [...replayEvents.current],
1344
- getExternalReplayUrl
1548
+ getExternalReplayUrl,
1549
+ initialSelection: pendingSelection.current
1345
1550
  }
1346
1551
  )
1347
1552
  ] });
1348
1553
  }
1554
+ 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
+ ] });
1561
+ }
1349
1562
  function SparkIcon2() {
1350
1563
  return /* @__PURE__ */ jsx3(
1351
1564
  "svg",