@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.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)(
@@ -1281,7 +1382,49 @@ function WidgetContent({
1281
1382
  const { t } = (0, import_react_i18next3.useTranslation)();
1282
1383
  const [open, setOpen] = (0, import_react4.useState)(false);
1283
1384
  const [hovered, setHovered] = (0, import_react4.useState)(false);
1385
+ const pendingSelection = (0, import_react4.useRef)("");
1284
1386
  const colors = resolveTheme(theme);
1387
+ const [selectionTooltip, setSelectionTooltip] = (0, import_react4.useState)(null);
1388
+ const tooltipRef = (0, import_react4.useRef)(null);
1389
+ const triggerRef = (0, import_react4.useRef)(null);
1390
+ const handleMouseUp = (0, import_react4.useCallback)((e) => {
1391
+ const target = e.target;
1392
+ if (tooltipRef.current?.contains(target)) return;
1393
+ if (triggerRef.current?.contains(target)) return;
1394
+ requestAnimationFrame(() => {
1395
+ const sel = window.getSelection();
1396
+ const text = sel?.toString().trim() ?? "";
1397
+ if (text.length < 2) {
1398
+ setSelectionTooltip(null);
1399
+ return;
1400
+ }
1401
+ const range = sel?.getRangeAt(0);
1402
+ if (!range) return;
1403
+ const rect = range.getBoundingClientRect();
1404
+ setSelectionTooltip({
1405
+ text,
1406
+ x: rect.left + rect.width / 2,
1407
+ y: rect.top - 8
1408
+ });
1409
+ });
1410
+ }, []);
1411
+ const handleSelectionChange = (0, import_react4.useCallback)(() => {
1412
+ const text = window.getSelection()?.toString().trim() ?? "";
1413
+ if (text.length < 2) setSelectionTooltip(null);
1414
+ }, []);
1415
+ (0, import_react4.useEffect)(() => {
1416
+ document.addEventListener("mouseup", handleMouseUp);
1417
+ document.addEventListener("selectionchange", handleSelectionChange);
1418
+ return () => {
1419
+ document.removeEventListener("mouseup", handleMouseUp);
1420
+ document.removeEventListener("selectionchange", handleSelectionChange);
1421
+ };
1422
+ }, [handleMouseUp, handleSelectionChange]);
1423
+ const openWithSelection = (text) => {
1424
+ pendingSelection.current = text;
1425
+ setSelectionTooltip(null);
1426
+ setOpen(true);
1427
+ };
1285
1428
  const consoleCollector = (0, import_react4.useRef)(null);
1286
1429
  const networkCollector = (0, import_react4.useRef)(null);
1287
1430
  const replayEvents = (0, import_react4.useRef)([]);
@@ -1323,6 +1466,10 @@ function WidgetContent({
1323
1466
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1324
1467
  "button",
1325
1468
  {
1469
+ ref: triggerRef,
1470
+ onMouseDown: () => {
1471
+ pendingSelection.current = window.getSelection()?.toString().trim() ?? "";
1472
+ },
1326
1473
  onClick: () => setOpen(true),
1327
1474
  onMouseEnter: () => setHovered(true),
1328
1475
  onMouseLeave: () => setHovered(false),
@@ -1355,6 +1502,44 @@ function WidgetContent({
1355
1502
  ]
1356
1503
  }
1357
1504
  ),
1505
+ selectionTooltip && !open && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1506
+ "button",
1507
+ {
1508
+ ref: tooltipRef,
1509
+ onClick: () => openWithSelection(selectionTooltip.text),
1510
+ style: {
1511
+ position: "fixed",
1512
+ left: Math.max(8, Math.min(selectionTooltip.x - 70, window.innerWidth - 148)),
1513
+ top: Math.max(8, selectionTooltip.y - 36),
1514
+ zIndex: zIndex + 1,
1515
+ display: "flex",
1516
+ alignItems: "center",
1517
+ gap: 6,
1518
+ padding: "6px 12px",
1519
+ borderRadius: 10,
1520
+ border: "none",
1521
+ background: `linear-gradient(135deg, ${colors.accent}, ${colors.accentHover})`,
1522
+ color: colors.buttonText,
1523
+ fontSize: 12,
1524
+ fontWeight: 600,
1525
+ cursor: "pointer",
1526
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1527
+ boxShadow: `0 4px 20px rgba(0,0,0,0.25), 0 0 12px ${colors.accent}40`,
1528
+ animation: "flint-tooltip-in 0.15s ease-out",
1529
+ whiteSpace: "nowrap"
1530
+ },
1531
+ children: [
1532
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TextIcon, {}),
1533
+ "Report text issue"
1534
+ ]
1535
+ }
1536
+ ),
1537
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("style", { children: `
1538
+ @keyframes flint-tooltip-in {
1539
+ from { opacity: 0; transform: translateY(4px); }
1540
+ to { opacity: 1; transform: translateY(0); }
1541
+ }
1542
+ ` }),
1358
1543
  open && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1359
1544
  FlintModal,
1360
1545
  {
@@ -1364,16 +1549,28 @@ function WidgetContent({
1364
1549
  meta,
1365
1550
  theme,
1366
1551
  zIndex,
1367
- onClose: () => setOpen(false),
1552
+ onClose: () => {
1553
+ setOpen(false);
1554
+ pendingSelection.current = "";
1555
+ },
1368
1556
  getEnvironment: collectEnvironment,
1369
1557
  getConsoleLogs: () => consoleCollector.current?.getEntries() ?? [],
1370
1558
  getNetworkErrors: () => networkCollector.current?.getEntries() ?? [],
1371
1559
  getReplayEvents: () => [...replayEvents.current],
1372
- getExternalReplayUrl
1560
+ getExternalReplayUrl,
1561
+ initialSelection: pendingSelection.current
1373
1562
  }
1374
1563
  )
1375
1564
  ] });
1376
1565
  }
1566
+ function TextIcon() {
1567
+ 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: [
1568
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M17 10H3" }),
1569
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M21 6H3" }),
1570
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M21 14H3" }),
1571
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M17 18H3" })
1572
+ ] });
1573
+ }
1377
1574
  function SparkIcon2() {
1378
1575
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1379
1576
  "svg",