@diegotsi/flint-react 0.5.0 → 0.5.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,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(
@@ -1253,7 +1354,49 @@ function WidgetContent({
1253
1354
  const { t } = useTranslation2();
1254
1355
  const [open, setOpen] = useState3(false);
1255
1356
  const [hovered, setHovered] = useState3(false);
1357
+ const pendingSelection = useRef3("");
1256
1358
  const colors = resolveTheme(theme);
1359
+ const [selectionTooltip, setSelectionTooltip] = useState3(null);
1360
+ const tooltipRef = useRef3(null);
1361
+ const triggerRef = useRef3(null);
1362
+ const handleMouseUp = useCallback2((e) => {
1363
+ const target = e.target;
1364
+ if (tooltipRef.current?.contains(target)) return;
1365
+ if (triggerRef.current?.contains(target)) return;
1366
+ requestAnimationFrame(() => {
1367
+ const sel = window.getSelection();
1368
+ const text = sel?.toString().trim() ?? "";
1369
+ if (text.length < 2) {
1370
+ setSelectionTooltip(null);
1371
+ return;
1372
+ }
1373
+ const range = sel?.getRangeAt(0);
1374
+ if (!range) return;
1375
+ const rect = range.getBoundingClientRect();
1376
+ setSelectionTooltip({
1377
+ text,
1378
+ x: rect.left + rect.width / 2,
1379
+ y: rect.top - 8
1380
+ });
1381
+ });
1382
+ }, []);
1383
+ const handleSelectionChange = useCallback2(() => {
1384
+ const text = window.getSelection()?.toString().trim() ?? "";
1385
+ if (text.length < 2) setSelectionTooltip(null);
1386
+ }, []);
1387
+ useEffect3(() => {
1388
+ document.addEventListener("mouseup", handleMouseUp);
1389
+ document.addEventListener("selectionchange", handleSelectionChange);
1390
+ return () => {
1391
+ document.removeEventListener("mouseup", handleMouseUp);
1392
+ document.removeEventListener("selectionchange", handleSelectionChange);
1393
+ };
1394
+ }, [handleMouseUp, handleSelectionChange]);
1395
+ const openWithSelection = (text) => {
1396
+ pendingSelection.current = text;
1397
+ setSelectionTooltip(null);
1398
+ setOpen(true);
1399
+ };
1257
1400
  const consoleCollector = useRef3(null);
1258
1401
  const networkCollector = useRef3(null);
1259
1402
  const replayEvents = useRef3([]);
@@ -1295,6 +1438,10 @@ function WidgetContent({
1295
1438
  /* @__PURE__ */ jsxs3(
1296
1439
  "button",
1297
1440
  {
1441
+ ref: triggerRef,
1442
+ onMouseDown: () => {
1443
+ pendingSelection.current = window.getSelection()?.toString().trim() ?? "";
1444
+ },
1298
1445
  onClick: () => setOpen(true),
1299
1446
  onMouseEnter: () => setHovered(true),
1300
1447
  onMouseLeave: () => setHovered(false),
@@ -1327,6 +1474,44 @@ function WidgetContent({
1327
1474
  ]
1328
1475
  }
1329
1476
  ),
1477
+ selectionTooltip && !open && /* @__PURE__ */ jsxs3(
1478
+ "button",
1479
+ {
1480
+ ref: tooltipRef,
1481
+ onClick: () => openWithSelection(selectionTooltip.text),
1482
+ style: {
1483
+ position: "fixed",
1484
+ left: Math.max(8, Math.min(selectionTooltip.x - 70, window.innerWidth - 148)),
1485
+ top: Math.max(8, selectionTooltip.y - 36),
1486
+ zIndex: zIndex + 1,
1487
+ display: "flex",
1488
+ alignItems: "center",
1489
+ gap: 6,
1490
+ padding: "6px 12px",
1491
+ borderRadius: 10,
1492
+ border: "none",
1493
+ background: `linear-gradient(135deg, ${colors.accent}, ${colors.accentHover})`,
1494
+ color: colors.buttonText,
1495
+ fontSize: 12,
1496
+ fontWeight: 600,
1497
+ cursor: "pointer",
1498
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1499
+ boxShadow: `0 4px 20px rgba(0,0,0,0.25), 0 0 12px ${colors.accent}40`,
1500
+ animation: "flint-tooltip-in 0.15s ease-out",
1501
+ whiteSpace: "nowrap"
1502
+ },
1503
+ children: [
1504
+ /* @__PURE__ */ jsx3(TextIcon, {}),
1505
+ "Report text issue"
1506
+ ]
1507
+ }
1508
+ ),
1509
+ /* @__PURE__ */ jsx3("style", { children: `
1510
+ @keyframes flint-tooltip-in {
1511
+ from { opacity: 0; transform: translateY(4px); }
1512
+ to { opacity: 1; transform: translateY(0); }
1513
+ }
1514
+ ` }),
1330
1515
  open && /* @__PURE__ */ jsx3(
1331
1516
  FlintModal,
1332
1517
  {
@@ -1336,16 +1521,28 @@ function WidgetContent({
1336
1521
  meta,
1337
1522
  theme,
1338
1523
  zIndex,
1339
- onClose: () => setOpen(false),
1524
+ onClose: () => {
1525
+ setOpen(false);
1526
+ pendingSelection.current = "";
1527
+ },
1340
1528
  getEnvironment: collectEnvironment,
1341
1529
  getConsoleLogs: () => consoleCollector.current?.getEntries() ?? [],
1342
1530
  getNetworkErrors: () => networkCollector.current?.getEntries() ?? [],
1343
1531
  getReplayEvents: () => [...replayEvents.current],
1344
- getExternalReplayUrl
1532
+ getExternalReplayUrl,
1533
+ initialSelection: pendingSelection.current
1345
1534
  }
1346
1535
  )
1347
1536
  ] });
1348
1537
  }
1538
+ function TextIcon() {
1539
+ 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: [
1540
+ /* @__PURE__ */ jsx3("path", { d: "M17 10H3" }),
1541
+ /* @__PURE__ */ jsx3("path", { d: "M21 6H3" }),
1542
+ /* @__PURE__ */ jsx3("path", { d: "M21 14H3" }),
1543
+ /* @__PURE__ */ jsx3("path", { d: "M17 18H3" })
1544
+ ] });
1545
+ }
1349
1546
  function SparkIcon2() {
1350
1547
  return /* @__PURE__ */ jsx3(
1351
1548
  "svg",