@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.cjs CHANGED
@@ -321,7 +321,8 @@ function FlintModal({
321
321
  getConsoleLogs,
322
322
  getNetworkErrors,
323
323
  getReplayEvents,
324
- getExternalReplayUrl
324
+ getExternalReplayUrl,
325
+ initialSelection = ""
325
326
  }) {
326
327
  const { t } = (0, import_react_i18next.useTranslation)();
327
328
  const colors = resolveTheme(theme);
@@ -334,6 +335,12 @@ function FlintModal({
334
335
  const [status, setStatus] = (0, import_react2.useState)("idle");
335
336
  const [result, setResult] = (0, import_react2.useState)(null);
336
337
  const [errorMsg, setErrorMsg] = (0, import_react2.useState)("");
338
+ const [mode, setMode] = (0, import_react2.useState)(initialSelection ? "text" : "bug");
339
+ const [textOriginal, setTextOriginal] = (0, import_react2.useState)(initialSelection);
340
+ const [textSuggested, setTextSuggested] = (0, import_react2.useState)("");
341
+ const [textLang, setTextLang] = (0, import_react2.useState)(
342
+ typeof document !== "undefined" ? document.documentElement.lang?.split("-")[0] || "en" : "en"
343
+ );
337
344
  const fileRef = (0, import_react2.useRef)(null);
338
345
  const overlayRef = (0, import_react2.useRef)(null);
339
346
  (0, import_react2.useEffect)(() => {
@@ -354,7 +361,12 @@ function FlintModal({
354
361
  );
355
362
  const handleSubmit = async (e) => {
356
363
  e.preventDefault();
357
- if (!description.trim()) return;
364
+ const isText = mode === "text";
365
+ if (isText) {
366
+ if (!textOriginal.trim() || !textSuggested.trim()) return;
367
+ } else {
368
+ if (!description.trim()) return;
369
+ }
358
370
  setStatus("submitting");
359
371
  setErrorMsg("");
360
372
  const collectedMeta = {
@@ -363,6 +375,13 @@ function FlintModal({
363
375
  consoleLogs: getConsoleLogs(),
364
376
  networkErrors: getNetworkErrors()
365
377
  };
378
+ if (isText) {
379
+ collectedMeta.textIssue = {
380
+ original: textOriginal.trim(),
381
+ suggested: textSuggested.trim(),
382
+ lang: textLang
383
+ };
384
+ }
366
385
  try {
367
386
  const res = await submitReport(
368
387
  serverUrl,
@@ -370,14 +389,15 @@ function FlintModal({
370
389
  {
371
390
  reporterId: user?.id ?? "anonymous",
372
391
  reporterName: user?.name ?? "Anonymous",
373
- description: description.trim(),
374
- expectedBehavior: expectedBehavior.trim() || void 0,
392
+ description: isText ? `[Text issue] "${textOriginal.trim()}" \u2192 "${textSuggested.trim()}"` : description.trim(),
393
+ expectedBehavior: !isText ? expectedBehavior.trim() || void 0 : void 0,
375
394
  externalReplayUrl: getExternalReplayUrl() || void 0,
376
- severity,
395
+ severity: isText ? "P3" : severity,
377
396
  url: window.location.href,
378
- meta: collectedMeta
397
+ meta: collectedMeta,
398
+ label: isText ? "TEXT" : void 0
379
399
  },
380
- screenshot ?? void 0
400
+ !isText ? screenshot ?? void 0 : void 0
381
401
  );
382
402
  setResult(res);
383
403
  setStatus("success");
@@ -612,7 +632,88 @@ function FlintModal({
612
632
  }
613
633
  ),
614
634
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("form", { onSubmit: handleSubmit, style: { padding: "20px 24px 24px" }, children: [
615
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginBottom: 18 }, children: [
635
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: {
636
+ display: "flex",
637
+ gap: 4,
638
+ marginBottom: 20,
639
+ background: colors.backgroundSecondary,
640
+ borderRadius: 12,
641
+ padding: 4,
642
+ border: `1px solid ${inputBorder}`
643
+ }, children: ["bug", "text"].map((m) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
644
+ "button",
645
+ {
646
+ type: "button",
647
+ onClick: () => setMode(m),
648
+ style: {
649
+ flex: 1,
650
+ padding: "8px 10px",
651
+ borderRadius: 9,
652
+ border: "none",
653
+ cursor: "pointer",
654
+ fontSize: 13,
655
+ fontWeight: mode === m ? 700 : 500,
656
+ fontFamily: "inherit",
657
+ transition: "background 0.15s, color 0.15s",
658
+ background: mode === m ? `linear-gradient(135deg, ${colors.accent}, ${colors.accentHover})` : "transparent",
659
+ color: mode === m ? colors.buttonText : colors.textMuted,
660
+ boxShadow: mode === m ? `0 2px 8px ${colors.accent}30` : "none"
661
+ },
662
+ children: m === "bug" ? "\u{1F41B} Bug" : "\u{1F524} Text / Translation"
663
+ },
664
+ m
665
+ )) }),
666
+ mode === "text" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
667
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginBottom: 14 }, children: [
668
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FieldLabel, { colors, htmlFor: "flint-text-original", children: "Original text" }),
669
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
670
+ "textarea",
671
+ {
672
+ id: "flint-text-original",
673
+ style: { ...inputStyle, resize: "vertical", minHeight: 60 },
674
+ value: textOriginal,
675
+ onChange: (e) => setTextOriginal(e.target.value),
676
+ placeholder: "Text that is wrong on screen\u2026",
677
+ required: true
678
+ }
679
+ )
680
+ ] }),
681
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginBottom: 14 }, children: [
682
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FieldLabel, { colors, htmlFor: "flint-text-suggested", children: "Suggested correction" }),
683
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
684
+ "textarea",
685
+ {
686
+ id: "flint-text-suggested",
687
+ style: { ...inputStyle, resize: "vertical", minHeight: 60 },
688
+ value: textSuggested,
689
+ onChange: (e) => setTextSuggested(e.target.value),
690
+ placeholder: "How it should read\u2026",
691
+ required: true
692
+ }
693
+ )
694
+ ] }),
695
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginBottom: 20 }, children: [
696
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FieldLabel, { colors, htmlFor: "flint-text-lang", children: "Language" }),
697
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
698
+ "select",
699
+ {
700
+ id: "flint-text-lang",
701
+ value: textLang,
702
+ onChange: (e) => setTextLang(e.target.value),
703
+ style: {
704
+ ...inputStyle,
705
+ appearance: "none",
706
+ cursor: "pointer"
707
+ },
708
+ children: [
709
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: "en", children: "English (en)" }),
710
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: "he", children: "\u05E2\u05D1\u05E8\u05D9\u05EA (he)" })
711
+ ]
712
+ }
713
+ )
714
+ ] })
715
+ ] }),
716
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginBottom: 18, display: mode === "text" ? "none" : void 0 }, children: [
616
717
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FieldLabel, { colors, children: t("severityLabel") }),
617
718
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { display: "grid", gridTemplateColumns: "repeat(4,1fr)", gap: 8 }, children: SEVERITIES.map((sev) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
618
719
  SeverityButton,
@@ -631,7 +732,7 @@ function FlintModal({
631
732
  sev
632
733
  )) })
633
734
  ] }),
634
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginBottom: 14 }, children: [
735
+ mode === "bug" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginBottom: 14 }, children: [
635
736
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FieldLabel, { colors, htmlFor: "flint-description", children: t("whatIsBrokenLabel") }),
636
737
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
637
738
  "textarea",
@@ -645,7 +746,7 @@ function FlintModal({
645
746
  }
646
747
  )
647
748
  ] }),
648
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginBottom: 14 }, children: [
749
+ mode === "bug" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginBottom: 14 }, children: [
649
750
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FieldLabel, { colors, htmlFor: "flint-expected", children: t("expectedBehaviorLabel") }),
650
751
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
651
752
  "textarea",
@@ -658,7 +759,7 @@ function FlintModal({
658
759
  }
659
760
  )
660
761
  ] }),
661
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginBottom: 20 }, children: [
762
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginBottom: 20, display: mode === "text" ? "none" : void 0 }, children: [
662
763
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FieldLabel, { colors, children: t("screenshotLabel") }),
663
764
  screenshot ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 10 }, children: [
664
765
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
@@ -1130,7 +1231,7 @@ function createConsoleCollector() {
1130
1231
  }
1131
1232
 
1132
1233
  // src/collectors/network.ts
1133
- var MAX_ENTRIES2 = 20;
1234
+ var MAX_ENTRIES2 = 50;
1134
1235
  var BLOCKED_HOSTS = /* @__PURE__ */ new Set([
1135
1236
  "browser-intake-datadoghq.com",
1136
1237
  "rum.browser-intake-datadoghq.com",
@@ -1175,7 +1276,7 @@ function createNetworkCollector(extraBlockedHosts = []) {
1175
1276
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1176
1277
  const startTime = Date.now();
1177
1278
  const res = await origFetch.call(window, input, init);
1178
- if (!isBlockedUrl(url, blocked)) {
1279
+ if (res.status >= 400 && !isBlockedUrl(url, blocked)) {
1179
1280
  push({
1180
1281
  method,
1181
1282
  url: truncateUrl(url),
@@ -1191,7 +1292,7 @@ function createNetworkCollector(extraBlockedHosts = []) {
1191
1292
  const startTime = Date.now();
1192
1293
  const urlStr = typeof url === "string" ? url : url.href;
1193
1294
  this.addEventListener("load", () => {
1194
- if (!isBlockedUrl(urlStr, blocked)) {
1295
+ if (this.status >= 400 && !isBlockedUrl(urlStr, blocked)) {
1195
1296
  push({
1196
1297
  method: method.toUpperCase(),
1197
1298
  url: truncateUrl(urlStr),
@@ -1269,19 +1370,77 @@ function WidgetContent({
1269
1370
  extraFields,
1270
1371
  buttonLabel,
1271
1372
  theme = "dark",
1272
- zIndex = 9999
1373
+ zIndex = 9999,
1374
+ datadogSite
1273
1375
  }) {
1274
1376
  const globalState = useFlintStore();
1275
1377
  const resolvedUser = user ?? globalState.user;
1276
1378
  const resolvedSessionReplay = extraFields?.sessionReplay ?? globalState.sessionReplay;
1277
1379
  const getExternalReplayUrl = () => {
1278
1380
  const src = resolvedSessionReplay;
1279
- return typeof src === "function" ? src() : src;
1381
+ const explicit = typeof src === "function" ? src() : src;
1382
+ if (explicit) return explicit;
1383
+ if (datadogSite) {
1384
+ try {
1385
+ const ddRum = window.DD_RUM;
1386
+ const ctx = ddRum?.getInternalContext?.();
1387
+ if (ctx?.session_id) {
1388
+ const now = Date.now();
1389
+ const fromTs = now - 3e4;
1390
+ const toTs = now + 5e3;
1391
+ return `https://${datadogSite}/rum/replay/sessions/${ctx.session_id}?from_ts=${fromTs}&to_ts=${toTs}&tab=replay&live=false`;
1392
+ }
1393
+ } catch {
1394
+ }
1395
+ }
1396
+ return void 0;
1280
1397
  };
1281
1398
  const { t } = (0, import_react_i18next3.useTranslation)();
1282
1399
  const [open, setOpen] = (0, import_react4.useState)(false);
1283
1400
  const [hovered, setHovered] = (0, import_react4.useState)(false);
1401
+ const pendingSelection = (0, import_react4.useRef)("");
1284
1402
  const colors = resolveTheme(theme);
1403
+ const [selectionTooltip, setSelectionTooltip] = (0, import_react4.useState)(null);
1404
+ const tooltipRef = (0, import_react4.useRef)(null);
1405
+ const triggerRef = (0, import_react4.useRef)(null);
1406
+ const handleMouseUp = (0, import_react4.useCallback)((e) => {
1407
+ const target = e.target;
1408
+ if (tooltipRef.current?.contains(target)) return;
1409
+ if (triggerRef.current?.contains(target)) return;
1410
+ requestAnimationFrame(() => {
1411
+ const sel = window.getSelection();
1412
+ const text = sel?.toString().trim() ?? "";
1413
+ if (text.length < 2) {
1414
+ setSelectionTooltip(null);
1415
+ return;
1416
+ }
1417
+ const range = sel?.getRangeAt(0);
1418
+ if (!range) return;
1419
+ const rect = range.getBoundingClientRect();
1420
+ setSelectionTooltip({
1421
+ text,
1422
+ x: rect.left + rect.width / 2,
1423
+ y: rect.top - 8
1424
+ });
1425
+ });
1426
+ }, []);
1427
+ const handleSelectionChange = (0, import_react4.useCallback)(() => {
1428
+ const text = window.getSelection()?.toString().trim() ?? "";
1429
+ if (text.length < 2) setSelectionTooltip(null);
1430
+ }, []);
1431
+ (0, import_react4.useEffect)(() => {
1432
+ document.addEventListener("mouseup", handleMouseUp);
1433
+ document.addEventListener("selectionchange", handleSelectionChange);
1434
+ return () => {
1435
+ document.removeEventListener("mouseup", handleMouseUp);
1436
+ document.removeEventListener("selectionchange", handleSelectionChange);
1437
+ };
1438
+ }, [handleMouseUp, handleSelectionChange]);
1439
+ const openWithSelection = (text) => {
1440
+ pendingSelection.current = text;
1441
+ setSelectionTooltip(null);
1442
+ setOpen(true);
1443
+ };
1285
1444
  const consoleCollector = (0, import_react4.useRef)(null);
1286
1445
  const networkCollector = (0, import_react4.useRef)(null);
1287
1446
  const replayEvents = (0, import_react4.useRef)([]);
@@ -1323,6 +1482,10 @@ function WidgetContent({
1323
1482
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1324
1483
  "button",
1325
1484
  {
1485
+ ref: triggerRef,
1486
+ onMouseDown: () => {
1487
+ pendingSelection.current = window.getSelection()?.toString().trim() ?? "";
1488
+ },
1326
1489
  onClick: () => setOpen(true),
1327
1490
  onMouseEnter: () => setHovered(true),
1328
1491
  onMouseLeave: () => setHovered(false),
@@ -1355,6 +1518,44 @@ function WidgetContent({
1355
1518
  ]
1356
1519
  }
1357
1520
  ),
1521
+ selectionTooltip && !open && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1522
+ "button",
1523
+ {
1524
+ ref: tooltipRef,
1525
+ onClick: () => openWithSelection(selectionTooltip.text),
1526
+ style: {
1527
+ position: "fixed",
1528
+ left: Math.max(8, Math.min(selectionTooltip.x - 70, window.innerWidth - 148)),
1529
+ top: Math.max(8, selectionTooltip.y - 36),
1530
+ zIndex: zIndex + 1,
1531
+ display: "flex",
1532
+ alignItems: "center",
1533
+ gap: 6,
1534
+ padding: "6px 12px",
1535
+ borderRadius: 10,
1536
+ border: "none",
1537
+ background: `linear-gradient(135deg, ${colors.accent}, ${colors.accentHover})`,
1538
+ color: colors.buttonText,
1539
+ fontSize: 12,
1540
+ fontWeight: 600,
1541
+ cursor: "pointer",
1542
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1543
+ boxShadow: `0 4px 20px rgba(0,0,0,0.25), 0 0 12px ${colors.accent}40`,
1544
+ animation: "flint-tooltip-in 0.15s ease-out",
1545
+ whiteSpace: "nowrap"
1546
+ },
1547
+ children: [
1548
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TextIcon, {}),
1549
+ "Report text issue"
1550
+ ]
1551
+ }
1552
+ ),
1553
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("style", { children: `
1554
+ @keyframes flint-tooltip-in {
1555
+ from { opacity: 0; transform: translateY(4px); }
1556
+ to { opacity: 1; transform: translateY(0); }
1557
+ }
1558
+ ` }),
1358
1559
  open && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1359
1560
  FlintModal,
1360
1561
  {
@@ -1364,16 +1565,28 @@ function WidgetContent({
1364
1565
  meta,
1365
1566
  theme,
1366
1567
  zIndex,
1367
- onClose: () => setOpen(false),
1568
+ onClose: () => {
1569
+ setOpen(false);
1570
+ pendingSelection.current = "";
1571
+ },
1368
1572
  getEnvironment: collectEnvironment,
1369
1573
  getConsoleLogs: () => consoleCollector.current?.getEntries() ?? [],
1370
1574
  getNetworkErrors: () => networkCollector.current?.getEntries() ?? [],
1371
1575
  getReplayEvents: () => [...replayEvents.current],
1372
- getExternalReplayUrl
1576
+ getExternalReplayUrl,
1577
+ initialSelection: pendingSelection.current
1373
1578
  }
1374
1579
  )
1375
1580
  ] });
1376
1581
  }
1582
+ function TextIcon() {
1583
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("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: [
1584
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M17 10H3" }),
1585
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M21 6H3" }),
1586
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M21 14H3" }),
1587
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M17 18H3" })
1588
+ ] });
1589
+ }
1377
1590
  function SparkIcon2() {
1378
1591
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1379
1592
  "svg",