@customerhero/react 1.1.0 → 2.1.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.
Files changed (3) hide show
  1. package/dist/index.cjs +442 -85
  2. package/dist/index.js +445 -87
  3. package/package.json +2 -2
package/dist/index.cjs CHANGED
@@ -57,9 +57,6 @@ function useCustomerHeroClient() {
57
57
  return client;
58
58
  }
59
59
 
60
- // src/components/chat-bubble.tsx
61
- var import_react4 = require("react");
62
-
63
60
  // src/use-chat.ts
64
61
  var import_react2 = require("react");
65
62
  function useChat() {
@@ -125,6 +122,9 @@ function useChat() {
125
122
  };
126
123
  }
127
124
 
125
+ // src/components/chat-bubble.tsx
126
+ var import_react4 = require("react");
127
+
128
128
  // src/use-reduced-motion.ts
129
129
  var import_react3 = require("react");
130
130
  function useReducedMotion() {
@@ -1477,6 +1477,16 @@ var import_react8 = require("react");
1477
1477
  var import_js2 = require("@customerhero/js");
1478
1478
  var import_jsx_runtime8 = require("react/jsx-runtime");
1479
1479
  var MAX_ATTACHMENTS = 3;
1480
+ var ALLOWED_MIME_TYPES = [
1481
+ "image/png",
1482
+ "image/jpeg",
1483
+ "image/webp",
1484
+ "application/pdf"
1485
+ ];
1486
+ var ACCEPT_ATTR = ALLOWED_MIME_TYPES.join(",");
1487
+ function isImageMime(mime) {
1488
+ return mime.startsWith("image/");
1489
+ }
1480
1490
  function ChatInput() {
1481
1491
  const {
1482
1492
  sendMessage,
@@ -1491,6 +1501,13 @@ function ChatInput() {
1491
1501
  const [value, setValue] = (0, import_react8.useState)("");
1492
1502
  const [attachments, setAttachments] = (0, import_react8.useState)([]);
1493
1503
  const [captureSupported, setCaptureSupported] = (0, import_react8.useState)(false);
1504
+ const [menuOpen, setMenuOpen] = (0, import_react8.useState)(false);
1505
+ const [dragActive, setDragActive] = (0, import_react8.useState)(false);
1506
+ const [transientError, setTransientError] = (0, import_react8.useState)(null);
1507
+ const fileInputRef = (0, import_react8.useRef)(null);
1508
+ const menuRef = (0, import_react8.useRef)(null);
1509
+ const menuButtonRef = (0, import_react8.useRef)(null);
1510
+ const dragCounterRef = (0, import_react8.useRef)(0);
1494
1511
  (0, import_react8.useEffect)(() => {
1495
1512
  setCaptureSupported((0, import_js2.canCaptureScreenshot)());
1496
1513
  }, []);
@@ -1504,6 +1521,30 @@ function ChatInput() {
1504
1521
  for (const a of attachments) URL.revokeObjectURL(a.previewUrl);
1505
1522
  };
1506
1523
  }, []);
1524
+ (0, import_react8.useEffect)(() => {
1525
+ if (!menuOpen) return;
1526
+ const onClick = (e) => {
1527
+ const target = e.target;
1528
+ if (menuRef.current?.contains(target) || menuButtonRef.current?.contains(target)) {
1529
+ return;
1530
+ }
1531
+ setMenuOpen(false);
1532
+ };
1533
+ const onKey = (e) => {
1534
+ if (e.key === "Escape") setMenuOpen(false);
1535
+ };
1536
+ document.addEventListener("mousedown", onClick);
1537
+ document.addEventListener("keydown", onKey);
1538
+ return () => {
1539
+ document.removeEventListener("mousedown", onClick);
1540
+ document.removeEventListener("keydown", onKey);
1541
+ };
1542
+ }, [menuOpen]);
1543
+ (0, import_react8.useEffect)(() => {
1544
+ if (!transientError) return;
1545
+ const id = window.setTimeout(() => setTransientError(null), 4e3);
1546
+ return () => window.clearTimeout(id);
1547
+ }, [transientError]);
1507
1548
  const updateAttachment = (id, patch) => {
1508
1549
  setAttachments(
1509
1550
  (current) => current.map(
@@ -1511,16 +1552,16 @@ function ChatInput() {
1511
1552
  )
1512
1553
  );
1513
1554
  };
1514
- const startUpload = async (blob) => {
1555
+ const startUpload = async (blob, filename) => {
1515
1556
  if (attachments.length >= MAX_ATTACHMENTS) return;
1516
1557
  const id = `att_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1517
1558
  const previewUrl = URL.createObjectURL(blob);
1518
1559
  setAttachments((current) => [
1519
1560
  ...current,
1520
- { id, status: "uploading", previewUrl, blob }
1561
+ { id, status: "uploading", previewUrl, blob, filename }
1521
1562
  ]);
1522
1563
  try {
1523
- const { attachmentToken } = await uploadAttachment(blob);
1564
+ const { attachmentToken } = await uploadAttachment(blob, { filename });
1524
1565
  updateAttachment(id, {
1525
1566
  status: "ready",
1526
1567
  token: attachmentToken
@@ -1529,7 +1570,31 @@ function ChatInput() {
1529
1570
  updateAttachment(id, { status: "error" });
1530
1571
  }
1531
1572
  };
1573
+ const ingestFiles = (files) => {
1574
+ const remainingSlots = Math.max(0, MAX_ATTACHMENTS - attachments.length);
1575
+ if (remainingSlots === 0) return 0;
1576
+ let queued = 0;
1577
+ let rejectedAny = false;
1578
+ for (const f of Array.from(files)) {
1579
+ if (queued >= remainingSlots) break;
1580
+ const type = f.type;
1581
+ if (!ALLOWED_MIME_TYPES.includes(
1582
+ type
1583
+ )) {
1584
+ rejectedAny = true;
1585
+ continue;
1586
+ }
1587
+ const filename = typeof f.name === "string" && f.name.length > 0 ? f.name : void 0;
1588
+ void startUpload(f, filename);
1589
+ queued += 1;
1590
+ }
1591
+ if (rejectedAny) {
1592
+ setTransientError(t("attachment_unsupported_type"));
1593
+ }
1594
+ return queued;
1595
+ };
1532
1596
  const handleCapture = async () => {
1597
+ setMenuOpen(false);
1533
1598
  try {
1534
1599
  const blob = await (0, import_js2.captureScreenshot)();
1535
1600
  await startUpload(blob);
@@ -1537,6 +1602,17 @@ function ChatInput() {
1537
1602
  if (e instanceof import_js2.ScreenshotCancelled) return;
1538
1603
  }
1539
1604
  };
1605
+ const handlePickFile = () => {
1606
+ setMenuOpen(false);
1607
+ fileInputRef.current?.click();
1608
+ };
1609
+ const handleFileInputChange = (e) => {
1610
+ const files = e.target.files;
1611
+ if (files && files.length > 0) {
1612
+ ingestFiles(files);
1613
+ }
1614
+ e.target.value = "";
1615
+ };
1540
1616
  const handleRemove = (id) => {
1541
1617
  setAttachments((current) => {
1542
1618
  const target = current.find((a) => a.id === id);
@@ -1544,6 +1620,45 @@ function ChatInput() {
1544
1620
  return current.filter((a) => a.id !== id);
1545
1621
  });
1546
1622
  };
1623
+ const handlePaste = (e) => {
1624
+ const items = e.clipboardData?.items;
1625
+ if (!items || items.length === 0) return;
1626
+ const blobs = [];
1627
+ for (const item of Array.from(items)) {
1628
+ if (item.kind !== "file") continue;
1629
+ const blob = item.getAsFile();
1630
+ if (blob) blobs.push(blob);
1631
+ }
1632
+ if (blobs.length === 0) return;
1633
+ e.preventDefault();
1634
+ ingestFiles(blobs);
1635
+ };
1636
+ const handleDragEnter = (e) => {
1637
+ if (!hasFiles(e)) return;
1638
+ e.preventDefault();
1639
+ dragCounterRef.current += 1;
1640
+ setDragActive(true);
1641
+ };
1642
+ const handleDragOver = (e) => {
1643
+ if (!hasFiles(e)) return;
1644
+ e.preventDefault();
1645
+ };
1646
+ const handleDragLeave = (e) => {
1647
+ if (!hasFiles(e)) return;
1648
+ e.preventDefault();
1649
+ dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);
1650
+ if (dragCounterRef.current === 0) setDragActive(false);
1651
+ };
1652
+ const handleDrop = (e) => {
1653
+ if (!hasFiles(e)) return;
1654
+ e.preventDefault();
1655
+ dragCounterRef.current = 0;
1656
+ setDragActive(false);
1657
+ const files = e.dataTransfer?.files;
1658
+ if (files && files.length > 0) {
1659
+ ingestFiles(files);
1660
+ }
1661
+ };
1547
1662
  const readyTokens = attachments.filter(
1548
1663
  (a) => a.status === "ready"
1549
1664
  ).map((a) => a.token);
@@ -1564,6 +1679,7 @@ function ChatInput() {
1564
1679
  }
1565
1680
  };
1566
1681
  const containerStyle = {
1682
+ position: "relative",
1567
1683
  padding: "12px 16px",
1568
1684
  borderTop: "1px solid #eee",
1569
1685
  display: "flex",
@@ -1616,99 +1732,236 @@ function ChatInput() {
1616
1732
  flexShrink: 0,
1617
1733
  padding: 0
1618
1734
  });
1619
- const captureDisabled = attachments.length >= MAX_ATTACHMENTS || isLoading;
1620
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: containerStyle, children: [
1621
- attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1622
- "div",
1623
- {
1624
- style: { display: "flex", gap: 8, flexWrap: "wrap" },
1625
- "aria-label": "Attachments",
1626
- children: attachments.map((a) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1627
- Thumbnail,
1735
+ const menuStyle = {
1736
+ position: "absolute",
1737
+ bottom: "calc(100% + 4px)",
1738
+ left: 0,
1739
+ background: "white",
1740
+ border: "1px solid #e0e0e0",
1741
+ borderRadius: 8,
1742
+ boxShadow: "0 4px 16px rgba(0,0,0,0.12)",
1743
+ padding: 4,
1744
+ minWidth: 180,
1745
+ zIndex: 10,
1746
+ display: "flex",
1747
+ flexDirection: "column"
1748
+ };
1749
+ const menuItemStyle = {
1750
+ display: "flex",
1751
+ alignItems: "center",
1752
+ gap: 10,
1753
+ padding: "8px 12px",
1754
+ background: "transparent",
1755
+ border: "none",
1756
+ borderRadius: 4,
1757
+ cursor: "pointer",
1758
+ fontSize: 14,
1759
+ color: "#333",
1760
+ textAlign: "left",
1761
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
1762
+ };
1763
+ const dropOverlayStyle = {
1764
+ position: "absolute",
1765
+ inset: 0,
1766
+ background: "rgba(255,255,255,0.92)",
1767
+ border: `2px dashed ${config.primaryColor}`,
1768
+ borderRadius: 4,
1769
+ display: "flex",
1770
+ alignItems: "center",
1771
+ justifyContent: "center",
1772
+ color: config.primaryColor,
1773
+ fontSize: 14,
1774
+ fontWeight: 500,
1775
+ pointerEvents: "none",
1776
+ zIndex: 11
1777
+ };
1778
+ const errorPillStyle = {
1779
+ alignSelf: "flex-start",
1780
+ background: "#fef2f2",
1781
+ color: "#b91c1c",
1782
+ border: "1px solid #fecaca",
1783
+ borderRadius: 12,
1784
+ padding: "4px 10px",
1785
+ fontSize: 12
1786
+ };
1787
+ const attachDisabled = attachments.length >= MAX_ATTACHMENTS || isLoading;
1788
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1789
+ "div",
1790
+ {
1791
+ style: containerStyle,
1792
+ onDragEnter: handleDragEnter,
1793
+ onDragOver: handleDragOver,
1794
+ onDragLeave: handleDragLeave,
1795
+ onDrop: handleDrop,
1796
+ children: [
1797
+ dragActive && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: dropOverlayStyle, "aria-hidden": "true", children: t("drop_files_here") }),
1798
+ transientError && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { role: "alert", style: errorPillStyle, children: transientError }),
1799
+ attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1800
+ "div",
1628
1801
  {
1629
- attachment: a,
1630
- onRemove: () => handleRemove(a.id),
1631
- t
1632
- },
1633
- a.id
1634
- ))
1635
- }
1636
- ),
1637
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: rowStyle, children: [
1638
- captureSupported && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1639
- "button",
1640
- {
1641
- type: "button",
1642
- onClick: handleCapture,
1643
- disabled: captureDisabled,
1644
- style: iconButtonStyle(captureDisabled),
1645
- "aria-label": t("screenshot_capture"),
1646
- title: t("screenshot_capture"),
1647
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CameraIcon, {})
1648
- }
1649
- ),
1650
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1651
- "input",
1652
- {
1653
- type: "text",
1654
- value,
1655
- onChange: (e) => setValue(e.target.value),
1656
- onKeyDown: handleKeyDown,
1657
- placeholder: config.placeholderText,
1658
- style: inputStyle,
1659
- disabled: isLoading
1660
- }
1661
- ),
1662
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1663
- "button",
1664
- {
1665
- onClick: handleSend,
1666
- disabled: isLoading || !value.trim(),
1667
- style: sendButtonStyle,
1668
- "aria-label": t("send_message"),
1669
- onMouseEnter: (e) => {
1670
- if (!reduced && !isLoading)
1671
- e.currentTarget.style.transform = "scale(1.1)";
1672
- },
1673
- onMouseLeave: (e) => {
1674
- if (!reduced) e.currentTarget.style.transform = "scale(1)";
1675
- },
1676
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1677
- "svg",
1802
+ style: { display: "flex", gap: 8, flexWrap: "wrap" },
1803
+ "aria-label": "Attachments",
1804
+ children: attachments.map((a) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1805
+ Thumbnail,
1806
+ {
1807
+ attachment: a,
1808
+ onRemove: () => handleRemove(a.id),
1809
+ t
1810
+ },
1811
+ a.id
1812
+ ))
1813
+ }
1814
+ ),
1815
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: rowStyle, children: [
1816
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { position: "relative" }, children: [
1817
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1818
+ "button",
1819
+ {
1820
+ ref: menuButtonRef,
1821
+ type: "button",
1822
+ onClick: () => setMenuOpen((o) => !o),
1823
+ disabled: attachDisabled,
1824
+ style: iconButtonStyle(attachDisabled),
1825
+ "aria-label": t("attach_menu_open"),
1826
+ "aria-haspopup": "menu",
1827
+ "aria-expanded": menuOpen,
1828
+ title: t("attach_menu_open"),
1829
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PaperclipIcon, {})
1830
+ }
1831
+ ),
1832
+ menuOpen && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { ref: menuRef, role: "menu", style: menuStyle, children: [
1833
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1834
+ "button",
1835
+ {
1836
+ type: "button",
1837
+ role: "menuitem",
1838
+ onClick: handlePickFile,
1839
+ style: menuItemStyle,
1840
+ onMouseEnter: (e) => {
1841
+ e.currentTarget.style.background = "#f5f5f5";
1842
+ },
1843
+ onMouseLeave: (e) => {
1844
+ e.currentTarget.style.background = "transparent";
1845
+ },
1846
+ children: [
1847
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ImageIcon, {}),
1848
+ t("attach_photo")
1849
+ ]
1850
+ }
1851
+ ),
1852
+ captureSupported && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1853
+ "button",
1854
+ {
1855
+ type: "button",
1856
+ role: "menuitem",
1857
+ onClick: handleCapture,
1858
+ style: menuItemStyle,
1859
+ onMouseEnter: (e) => {
1860
+ e.currentTarget.style.background = "#f5f5f5";
1861
+ },
1862
+ onMouseLeave: (e) => {
1863
+ e.currentTarget.style.background = "transparent";
1864
+ },
1865
+ children: [
1866
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CameraIcon, {}),
1867
+ t("screenshot_capture")
1868
+ ]
1869
+ }
1870
+ )
1871
+ ] })
1872
+ ] }),
1873
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1874
+ "input",
1678
1875
  {
1679
- width: "16",
1680
- height: "16",
1681
- viewBox: "0 0 24 24",
1682
- fill: "none",
1683
- stroke: "currentColor",
1684
- strokeWidth: "2",
1685
- strokeLinecap: "round",
1686
- strokeLinejoin: "round",
1687
- children: [
1688
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
1689
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
1690
- ]
1876
+ ref: fileInputRef,
1877
+ type: "file",
1878
+ accept: ACCEPT_ATTR,
1879
+ multiple: true,
1880
+ onChange: handleFileInputChange,
1881
+ style: { display: "none" },
1882
+ "aria-hidden": "true",
1883
+ tabIndex: -1
1884
+ }
1885
+ ),
1886
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1887
+ "input",
1888
+ {
1889
+ type: "text",
1890
+ value,
1891
+ onChange: (e) => setValue(e.target.value),
1892
+ onKeyDown: handleKeyDown,
1893
+ onPaste: handlePaste,
1894
+ placeholder: config.placeholderText,
1895
+ style: inputStyle,
1896
+ disabled: isLoading
1897
+ }
1898
+ ),
1899
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1900
+ "button",
1901
+ {
1902
+ onClick: handleSend,
1903
+ disabled: isLoading || !value.trim(),
1904
+ style: sendButtonStyle,
1905
+ "aria-label": t("send_message"),
1906
+ onMouseEnter: (e) => {
1907
+ if (!reduced && !isLoading)
1908
+ e.currentTarget.style.transform = "scale(1.1)";
1909
+ },
1910
+ onMouseLeave: (e) => {
1911
+ if (!reduced) e.currentTarget.style.transform = "scale(1)";
1912
+ },
1913
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1914
+ "svg",
1915
+ {
1916
+ width: "16",
1917
+ height: "16",
1918
+ viewBox: "0 0 24 24",
1919
+ fill: "none",
1920
+ stroke: "currentColor",
1921
+ strokeWidth: "2",
1922
+ strokeLinecap: "round",
1923
+ strokeLinejoin: "round",
1924
+ children: [
1925
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
1926
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
1927
+ ]
1928
+ }
1929
+ )
1691
1930
  }
1692
1931
  )
1693
- }
1694
- )
1695
- ] })
1696
- ] });
1932
+ ] })
1933
+ ]
1934
+ }
1935
+ );
1936
+ }
1937
+ function hasFiles(e) {
1938
+ const types = e.dataTransfer?.types;
1939
+ if (!types) return false;
1940
+ for (let i = 0; i < types.length; i++) {
1941
+ if (types[i] === "Files") return true;
1942
+ }
1943
+ return false;
1697
1944
  }
1698
1945
  function Thumbnail({
1699
1946
  attachment,
1700
1947
  onRemove,
1701
1948
  t
1702
1949
  }) {
1703
- const { status, previewUrl } = attachment;
1950
+ const { status, previewUrl, blob, filename } = attachment;
1951
+ const isImage = isImageMime(blob.type);
1704
1952
  const wrap = {
1705
1953
  position: "relative",
1706
- width: 56,
1954
+ width: isImage ? 56 : 160,
1707
1955
  height: 56,
1708
1956
  borderRadius: 8,
1709
1957
  overflow: "hidden",
1710
1958
  border: status === "error" ? "2px solid #b91c1c" : "1px solid #e0e0e0",
1711
- background: "#f5f5f5"
1959
+ background: "#f5f5f5",
1960
+ display: "flex",
1961
+ alignItems: "center",
1962
+ justifyContent: isImage ? "stretch" : "flex-start",
1963
+ gap: isImage ? 0 : 8,
1964
+ padding: isImage ? 0 : "0 8px"
1712
1965
  };
1713
1966
  const img = {
1714
1967
  width: "100%",
@@ -1739,8 +1992,42 @@ function Thumbnail({
1739
1992
  alignItems: "center",
1740
1993
  justifyContent: "center"
1741
1994
  };
1995
+ const docName = {
1996
+ fontSize: 12,
1997
+ fontWeight: 500,
1998
+ color: "#333",
1999
+ overflow: "hidden",
2000
+ textOverflow: "ellipsis",
2001
+ whiteSpace: "nowrap",
2002
+ maxWidth: 110,
2003
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
2004
+ };
2005
+ const docMeta = {
2006
+ fontSize: 11,
2007
+ color: "#888",
2008
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
2009
+ };
2010
+ const displayName = filename ?? (isImage ? "" : "Document");
2011
+ const sizeLabel = formatBytes(blob.size);
1742
2012
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: wrap, children: [
1743
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("img", { src: previewUrl, alt: "", style: img }),
2013
+ isImage ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("img", { src: previewUrl, alt: "", style: img }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2014
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(DocIcon, {}),
2015
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2016
+ "div",
2017
+ {
2018
+ style: {
2019
+ display: "flex",
2020
+ flexDirection: "column",
2021
+ minWidth: 0,
2022
+ flex: 1
2023
+ },
2024
+ children: [
2025
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: docName, title: displayName, children: displayName }),
2026
+ sizeLabel && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: docMeta, children: sizeLabel })
2027
+ ]
2028
+ }
2029
+ )
2030
+ ] }),
1744
2031
  status === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: overlay, "aria-label": "Uploading", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Spinner2, {}) }),
1745
2032
  status === "error" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1746
2033
  "div",
@@ -1763,7 +2050,45 @@ function Thumbnail({
1763
2050
  )
1764
2051
  ] });
1765
2052
  }
1766
- function CameraIcon() {
2053
+ function PaperclipIcon() {
2054
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2055
+ "svg",
2056
+ {
2057
+ width: "20",
2058
+ height: "20",
2059
+ viewBox: "0 0 24 24",
2060
+ fill: "none",
2061
+ stroke: "currentColor",
2062
+ strokeWidth: "2",
2063
+ strokeLinecap: "round",
2064
+ strokeLinejoin: "round",
2065
+ "aria-hidden": "true",
2066
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" })
2067
+ }
2068
+ );
2069
+ }
2070
+ function ImageIcon() {
2071
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2072
+ "svg",
2073
+ {
2074
+ width: "18",
2075
+ height: "18",
2076
+ viewBox: "0 0 24 24",
2077
+ fill: "none",
2078
+ stroke: "currentColor",
2079
+ strokeWidth: "2",
2080
+ strokeLinecap: "round",
2081
+ strokeLinejoin: "round",
2082
+ "aria-hidden": "true",
2083
+ children: [
2084
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
2085
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
2086
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("polyline", { points: "21 15 16 10 5 21" })
2087
+ ]
2088
+ }
2089
+ );
2090
+ }
2091
+ function DocIcon() {
1767
2092
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1768
2093
  "svg",
1769
2094
  {
@@ -1776,6 +2101,36 @@ function CameraIcon() {
1776
2101
  strokeLinecap: "round",
1777
2102
  strokeLinejoin: "round",
1778
2103
  "aria-hidden": "true",
2104
+ style: { color: "#666", flexShrink: 0 },
2105
+ children: [
2106
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
2107
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("polyline", { points: "14 2 14 8 20 8" }),
2108
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
2109
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("line", { x1: "16", y1: "17", x2: "8", y2: "17" }),
2110
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("polyline", { points: "10 9 9 9 8 9" })
2111
+ ]
2112
+ }
2113
+ );
2114
+ }
2115
+ function formatBytes(bytes) {
2116
+ if (!Number.isFinite(bytes) || bytes <= 0) return "";
2117
+ if (bytes < 1024) return `${bytes} B`;
2118
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
2119
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2120
+ }
2121
+ function CameraIcon() {
2122
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2123
+ "svg",
2124
+ {
2125
+ width: "18",
2126
+ height: "18",
2127
+ viewBox: "0 0 24 24",
2128
+ fill: "none",
2129
+ stroke: "currentColor",
2130
+ strokeWidth: "2",
2131
+ strokeLinecap: "round",
2132
+ strokeLinejoin: "round",
2133
+ "aria-hidden": "true",
1779
2134
  children: [
1780
2135
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" }),
1781
2136
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: "12", cy: "13", r: "4" })
@@ -2177,6 +2532,7 @@ function ChatWindow() {
2177
2532
  var import_jsx_runtime10 = require("react/jsx-runtime");
2178
2533
  function ChatWidgetInner({ identity }) {
2179
2534
  const client = useCustomerHeroClient();
2535
+ const { configLoaded, configError } = useChat();
2180
2536
  const prevIdentityRef = (0, import_react10.useRef)(void 0);
2181
2537
  (0, import_react10.useEffect)(() => {
2182
2538
  const key = identity ? JSON.stringify(identity) : void 0;
@@ -2187,6 +2543,7 @@ function ChatWidgetInner({ identity }) {
2187
2543
  }
2188
2544
  }
2189
2545
  }, [identity, client]);
2546
+ if (!configLoaded || configError) return null;
2190
2547
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
2191
2548
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ChatBubble, {}),
2192
2549
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ChatWindow, {})
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/components/chat-widget.tsx
2
- import { useEffect as useEffect8, useRef as useRef4 } from "react";
2
+ import { useEffect as useEffect8, useRef as useRef5 } from "react";
3
3
 
4
4
  // src/context.tsx
5
5
  import {
@@ -36,9 +36,6 @@ function useCustomerHeroClient() {
36
36
  return client;
37
37
  }
38
38
 
39
- // src/components/chat-bubble.tsx
40
- import { useEffect as useEffect3, useState as useState2 } from "react";
41
-
42
39
  // src/use-chat.ts
43
40
  import { useCallback, useSyncExternalStore } from "react";
44
41
  function useChat() {
@@ -104,6 +101,9 @@ function useChat() {
104
101
  };
105
102
  }
106
103
 
104
+ // src/components/chat-bubble.tsx
105
+ import { useEffect as useEffect3, useState as useState2 } from "react";
106
+
107
107
  // src/use-reduced-motion.ts
108
108
  import { useEffect as useEffect2, useState } from "react";
109
109
  function useReducedMotion() {
@@ -1454,6 +1454,7 @@ function ChatSuggestions() {
1454
1454
  // src/components/chat-input.tsx
1455
1455
  import {
1456
1456
  useEffect as useEffect6,
1457
+ useRef as useRef4,
1457
1458
  useState as useState6
1458
1459
  } from "react";
1459
1460
  import {
@@ -1463,6 +1464,16 @@ import {
1463
1464
  } from "@customerhero/js";
1464
1465
  import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1465
1466
  var MAX_ATTACHMENTS = 3;
1467
+ var ALLOWED_MIME_TYPES = [
1468
+ "image/png",
1469
+ "image/jpeg",
1470
+ "image/webp",
1471
+ "application/pdf"
1472
+ ];
1473
+ var ACCEPT_ATTR = ALLOWED_MIME_TYPES.join(",");
1474
+ function isImageMime(mime) {
1475
+ return mime.startsWith("image/");
1476
+ }
1466
1477
  function ChatInput() {
1467
1478
  const {
1468
1479
  sendMessage,
@@ -1477,6 +1488,13 @@ function ChatInput() {
1477
1488
  const [value, setValue] = useState6("");
1478
1489
  const [attachments, setAttachments] = useState6([]);
1479
1490
  const [captureSupported, setCaptureSupported] = useState6(false);
1491
+ const [menuOpen, setMenuOpen] = useState6(false);
1492
+ const [dragActive, setDragActive] = useState6(false);
1493
+ const [transientError, setTransientError] = useState6(null);
1494
+ const fileInputRef = useRef4(null);
1495
+ const menuRef = useRef4(null);
1496
+ const menuButtonRef = useRef4(null);
1497
+ const dragCounterRef = useRef4(0);
1480
1498
  useEffect6(() => {
1481
1499
  setCaptureSupported(canCaptureScreenshot());
1482
1500
  }, []);
@@ -1490,6 +1508,30 @@ function ChatInput() {
1490
1508
  for (const a of attachments) URL.revokeObjectURL(a.previewUrl);
1491
1509
  };
1492
1510
  }, []);
1511
+ useEffect6(() => {
1512
+ if (!menuOpen) return;
1513
+ const onClick = (e) => {
1514
+ const target = e.target;
1515
+ if (menuRef.current?.contains(target) || menuButtonRef.current?.contains(target)) {
1516
+ return;
1517
+ }
1518
+ setMenuOpen(false);
1519
+ };
1520
+ const onKey = (e) => {
1521
+ if (e.key === "Escape") setMenuOpen(false);
1522
+ };
1523
+ document.addEventListener("mousedown", onClick);
1524
+ document.addEventListener("keydown", onKey);
1525
+ return () => {
1526
+ document.removeEventListener("mousedown", onClick);
1527
+ document.removeEventListener("keydown", onKey);
1528
+ };
1529
+ }, [menuOpen]);
1530
+ useEffect6(() => {
1531
+ if (!transientError) return;
1532
+ const id = window.setTimeout(() => setTransientError(null), 4e3);
1533
+ return () => window.clearTimeout(id);
1534
+ }, [transientError]);
1493
1535
  const updateAttachment = (id, patch) => {
1494
1536
  setAttachments(
1495
1537
  (current) => current.map(
@@ -1497,16 +1539,16 @@ function ChatInput() {
1497
1539
  )
1498
1540
  );
1499
1541
  };
1500
- const startUpload = async (blob) => {
1542
+ const startUpload = async (blob, filename) => {
1501
1543
  if (attachments.length >= MAX_ATTACHMENTS) return;
1502
1544
  const id = `att_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1503
1545
  const previewUrl = URL.createObjectURL(blob);
1504
1546
  setAttachments((current) => [
1505
1547
  ...current,
1506
- { id, status: "uploading", previewUrl, blob }
1548
+ { id, status: "uploading", previewUrl, blob, filename }
1507
1549
  ]);
1508
1550
  try {
1509
- const { attachmentToken } = await uploadAttachment(blob);
1551
+ const { attachmentToken } = await uploadAttachment(blob, { filename });
1510
1552
  updateAttachment(id, {
1511
1553
  status: "ready",
1512
1554
  token: attachmentToken
@@ -1515,7 +1557,31 @@ function ChatInput() {
1515
1557
  updateAttachment(id, { status: "error" });
1516
1558
  }
1517
1559
  };
1560
+ const ingestFiles = (files) => {
1561
+ const remainingSlots = Math.max(0, MAX_ATTACHMENTS - attachments.length);
1562
+ if (remainingSlots === 0) return 0;
1563
+ let queued = 0;
1564
+ let rejectedAny = false;
1565
+ for (const f of Array.from(files)) {
1566
+ if (queued >= remainingSlots) break;
1567
+ const type = f.type;
1568
+ if (!ALLOWED_MIME_TYPES.includes(
1569
+ type
1570
+ )) {
1571
+ rejectedAny = true;
1572
+ continue;
1573
+ }
1574
+ const filename = typeof f.name === "string" && f.name.length > 0 ? f.name : void 0;
1575
+ void startUpload(f, filename);
1576
+ queued += 1;
1577
+ }
1578
+ if (rejectedAny) {
1579
+ setTransientError(t("attachment_unsupported_type"));
1580
+ }
1581
+ return queued;
1582
+ };
1518
1583
  const handleCapture = async () => {
1584
+ setMenuOpen(false);
1519
1585
  try {
1520
1586
  const blob = await captureScreenshot();
1521
1587
  await startUpload(blob);
@@ -1523,6 +1589,17 @@ function ChatInput() {
1523
1589
  if (e instanceof ScreenshotCancelled) return;
1524
1590
  }
1525
1591
  };
1592
+ const handlePickFile = () => {
1593
+ setMenuOpen(false);
1594
+ fileInputRef.current?.click();
1595
+ };
1596
+ const handleFileInputChange = (e) => {
1597
+ const files = e.target.files;
1598
+ if (files && files.length > 0) {
1599
+ ingestFiles(files);
1600
+ }
1601
+ e.target.value = "";
1602
+ };
1526
1603
  const handleRemove = (id) => {
1527
1604
  setAttachments((current) => {
1528
1605
  const target = current.find((a) => a.id === id);
@@ -1530,6 +1607,45 @@ function ChatInput() {
1530
1607
  return current.filter((a) => a.id !== id);
1531
1608
  });
1532
1609
  };
1610
+ const handlePaste = (e) => {
1611
+ const items = e.clipboardData?.items;
1612
+ if (!items || items.length === 0) return;
1613
+ const blobs = [];
1614
+ for (const item of Array.from(items)) {
1615
+ if (item.kind !== "file") continue;
1616
+ const blob = item.getAsFile();
1617
+ if (blob) blobs.push(blob);
1618
+ }
1619
+ if (blobs.length === 0) return;
1620
+ e.preventDefault();
1621
+ ingestFiles(blobs);
1622
+ };
1623
+ const handleDragEnter = (e) => {
1624
+ if (!hasFiles(e)) return;
1625
+ e.preventDefault();
1626
+ dragCounterRef.current += 1;
1627
+ setDragActive(true);
1628
+ };
1629
+ const handleDragOver = (e) => {
1630
+ if (!hasFiles(e)) return;
1631
+ e.preventDefault();
1632
+ };
1633
+ const handleDragLeave = (e) => {
1634
+ if (!hasFiles(e)) return;
1635
+ e.preventDefault();
1636
+ dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);
1637
+ if (dragCounterRef.current === 0) setDragActive(false);
1638
+ };
1639
+ const handleDrop = (e) => {
1640
+ if (!hasFiles(e)) return;
1641
+ e.preventDefault();
1642
+ dragCounterRef.current = 0;
1643
+ setDragActive(false);
1644
+ const files = e.dataTransfer?.files;
1645
+ if (files && files.length > 0) {
1646
+ ingestFiles(files);
1647
+ }
1648
+ };
1533
1649
  const readyTokens = attachments.filter(
1534
1650
  (a) => a.status === "ready"
1535
1651
  ).map((a) => a.token);
@@ -1550,6 +1666,7 @@ function ChatInput() {
1550
1666
  }
1551
1667
  };
1552
1668
  const containerStyle = {
1669
+ position: "relative",
1553
1670
  padding: "12px 16px",
1554
1671
  borderTop: "1px solid #eee",
1555
1672
  display: "flex",
@@ -1602,99 +1719,236 @@ function ChatInput() {
1602
1719
  flexShrink: 0,
1603
1720
  padding: 0
1604
1721
  });
1605
- const captureDisabled = attachments.length >= MAX_ATTACHMENTS || isLoading;
1606
- return /* @__PURE__ */ jsxs5("div", { style: containerStyle, children: [
1607
- attachments.length > 0 && /* @__PURE__ */ jsx8(
1608
- "div",
1609
- {
1610
- style: { display: "flex", gap: 8, flexWrap: "wrap" },
1611
- "aria-label": "Attachments",
1612
- children: attachments.map((a) => /* @__PURE__ */ jsx8(
1613
- Thumbnail,
1722
+ const menuStyle = {
1723
+ position: "absolute",
1724
+ bottom: "calc(100% + 4px)",
1725
+ left: 0,
1726
+ background: "white",
1727
+ border: "1px solid #e0e0e0",
1728
+ borderRadius: 8,
1729
+ boxShadow: "0 4px 16px rgba(0,0,0,0.12)",
1730
+ padding: 4,
1731
+ minWidth: 180,
1732
+ zIndex: 10,
1733
+ display: "flex",
1734
+ flexDirection: "column"
1735
+ };
1736
+ const menuItemStyle = {
1737
+ display: "flex",
1738
+ alignItems: "center",
1739
+ gap: 10,
1740
+ padding: "8px 12px",
1741
+ background: "transparent",
1742
+ border: "none",
1743
+ borderRadius: 4,
1744
+ cursor: "pointer",
1745
+ fontSize: 14,
1746
+ color: "#333",
1747
+ textAlign: "left",
1748
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
1749
+ };
1750
+ const dropOverlayStyle = {
1751
+ position: "absolute",
1752
+ inset: 0,
1753
+ background: "rgba(255,255,255,0.92)",
1754
+ border: `2px dashed ${config.primaryColor}`,
1755
+ borderRadius: 4,
1756
+ display: "flex",
1757
+ alignItems: "center",
1758
+ justifyContent: "center",
1759
+ color: config.primaryColor,
1760
+ fontSize: 14,
1761
+ fontWeight: 500,
1762
+ pointerEvents: "none",
1763
+ zIndex: 11
1764
+ };
1765
+ const errorPillStyle = {
1766
+ alignSelf: "flex-start",
1767
+ background: "#fef2f2",
1768
+ color: "#b91c1c",
1769
+ border: "1px solid #fecaca",
1770
+ borderRadius: 12,
1771
+ padding: "4px 10px",
1772
+ fontSize: 12
1773
+ };
1774
+ const attachDisabled = attachments.length >= MAX_ATTACHMENTS || isLoading;
1775
+ return /* @__PURE__ */ jsxs5(
1776
+ "div",
1777
+ {
1778
+ style: containerStyle,
1779
+ onDragEnter: handleDragEnter,
1780
+ onDragOver: handleDragOver,
1781
+ onDragLeave: handleDragLeave,
1782
+ onDrop: handleDrop,
1783
+ children: [
1784
+ dragActive && /* @__PURE__ */ jsx8("div", { style: dropOverlayStyle, "aria-hidden": "true", children: t("drop_files_here") }),
1785
+ transientError && /* @__PURE__ */ jsx8("div", { role: "alert", style: errorPillStyle, children: transientError }),
1786
+ attachments.length > 0 && /* @__PURE__ */ jsx8(
1787
+ "div",
1614
1788
  {
1615
- attachment: a,
1616
- onRemove: () => handleRemove(a.id),
1617
- t
1618
- },
1619
- a.id
1620
- ))
1621
- }
1622
- ),
1623
- /* @__PURE__ */ jsxs5("div", { style: rowStyle, children: [
1624
- captureSupported && /* @__PURE__ */ jsx8(
1625
- "button",
1626
- {
1627
- type: "button",
1628
- onClick: handleCapture,
1629
- disabled: captureDisabled,
1630
- style: iconButtonStyle(captureDisabled),
1631
- "aria-label": t("screenshot_capture"),
1632
- title: t("screenshot_capture"),
1633
- children: /* @__PURE__ */ jsx8(CameraIcon, {})
1634
- }
1635
- ),
1636
- /* @__PURE__ */ jsx8(
1637
- "input",
1638
- {
1639
- type: "text",
1640
- value,
1641
- onChange: (e) => setValue(e.target.value),
1642
- onKeyDown: handleKeyDown,
1643
- placeholder: config.placeholderText,
1644
- style: inputStyle,
1645
- disabled: isLoading
1646
- }
1647
- ),
1648
- /* @__PURE__ */ jsx8(
1649
- "button",
1650
- {
1651
- onClick: handleSend,
1652
- disabled: isLoading || !value.trim(),
1653
- style: sendButtonStyle,
1654
- "aria-label": t("send_message"),
1655
- onMouseEnter: (e) => {
1656
- if (!reduced && !isLoading)
1657
- e.currentTarget.style.transform = "scale(1.1)";
1658
- },
1659
- onMouseLeave: (e) => {
1660
- if (!reduced) e.currentTarget.style.transform = "scale(1)";
1661
- },
1662
- children: /* @__PURE__ */ jsxs5(
1663
- "svg",
1789
+ style: { display: "flex", gap: 8, flexWrap: "wrap" },
1790
+ "aria-label": "Attachments",
1791
+ children: attachments.map((a) => /* @__PURE__ */ jsx8(
1792
+ Thumbnail,
1793
+ {
1794
+ attachment: a,
1795
+ onRemove: () => handleRemove(a.id),
1796
+ t
1797
+ },
1798
+ a.id
1799
+ ))
1800
+ }
1801
+ ),
1802
+ /* @__PURE__ */ jsxs5("div", { style: rowStyle, children: [
1803
+ /* @__PURE__ */ jsxs5("div", { style: { position: "relative" }, children: [
1804
+ /* @__PURE__ */ jsx8(
1805
+ "button",
1806
+ {
1807
+ ref: menuButtonRef,
1808
+ type: "button",
1809
+ onClick: () => setMenuOpen((o) => !o),
1810
+ disabled: attachDisabled,
1811
+ style: iconButtonStyle(attachDisabled),
1812
+ "aria-label": t("attach_menu_open"),
1813
+ "aria-haspopup": "menu",
1814
+ "aria-expanded": menuOpen,
1815
+ title: t("attach_menu_open"),
1816
+ children: /* @__PURE__ */ jsx8(PaperclipIcon, {})
1817
+ }
1818
+ ),
1819
+ menuOpen && /* @__PURE__ */ jsxs5("div", { ref: menuRef, role: "menu", style: menuStyle, children: [
1820
+ /* @__PURE__ */ jsxs5(
1821
+ "button",
1822
+ {
1823
+ type: "button",
1824
+ role: "menuitem",
1825
+ onClick: handlePickFile,
1826
+ style: menuItemStyle,
1827
+ onMouseEnter: (e) => {
1828
+ e.currentTarget.style.background = "#f5f5f5";
1829
+ },
1830
+ onMouseLeave: (e) => {
1831
+ e.currentTarget.style.background = "transparent";
1832
+ },
1833
+ children: [
1834
+ /* @__PURE__ */ jsx8(ImageIcon, {}),
1835
+ t("attach_photo")
1836
+ ]
1837
+ }
1838
+ ),
1839
+ captureSupported && /* @__PURE__ */ jsxs5(
1840
+ "button",
1841
+ {
1842
+ type: "button",
1843
+ role: "menuitem",
1844
+ onClick: handleCapture,
1845
+ style: menuItemStyle,
1846
+ onMouseEnter: (e) => {
1847
+ e.currentTarget.style.background = "#f5f5f5";
1848
+ },
1849
+ onMouseLeave: (e) => {
1850
+ e.currentTarget.style.background = "transparent";
1851
+ },
1852
+ children: [
1853
+ /* @__PURE__ */ jsx8(CameraIcon, {}),
1854
+ t("screenshot_capture")
1855
+ ]
1856
+ }
1857
+ )
1858
+ ] })
1859
+ ] }),
1860
+ /* @__PURE__ */ jsx8(
1861
+ "input",
1664
1862
  {
1665
- width: "16",
1666
- height: "16",
1667
- viewBox: "0 0 24 24",
1668
- fill: "none",
1669
- stroke: "currentColor",
1670
- strokeWidth: "2",
1671
- strokeLinecap: "round",
1672
- strokeLinejoin: "round",
1673
- children: [
1674
- /* @__PURE__ */ jsx8("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
1675
- /* @__PURE__ */ jsx8("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
1676
- ]
1863
+ ref: fileInputRef,
1864
+ type: "file",
1865
+ accept: ACCEPT_ATTR,
1866
+ multiple: true,
1867
+ onChange: handleFileInputChange,
1868
+ style: { display: "none" },
1869
+ "aria-hidden": "true",
1870
+ tabIndex: -1
1871
+ }
1872
+ ),
1873
+ /* @__PURE__ */ jsx8(
1874
+ "input",
1875
+ {
1876
+ type: "text",
1877
+ value,
1878
+ onChange: (e) => setValue(e.target.value),
1879
+ onKeyDown: handleKeyDown,
1880
+ onPaste: handlePaste,
1881
+ placeholder: config.placeholderText,
1882
+ style: inputStyle,
1883
+ disabled: isLoading
1884
+ }
1885
+ ),
1886
+ /* @__PURE__ */ jsx8(
1887
+ "button",
1888
+ {
1889
+ onClick: handleSend,
1890
+ disabled: isLoading || !value.trim(),
1891
+ style: sendButtonStyle,
1892
+ "aria-label": t("send_message"),
1893
+ onMouseEnter: (e) => {
1894
+ if (!reduced && !isLoading)
1895
+ e.currentTarget.style.transform = "scale(1.1)";
1896
+ },
1897
+ onMouseLeave: (e) => {
1898
+ if (!reduced) e.currentTarget.style.transform = "scale(1)";
1899
+ },
1900
+ children: /* @__PURE__ */ jsxs5(
1901
+ "svg",
1902
+ {
1903
+ width: "16",
1904
+ height: "16",
1905
+ viewBox: "0 0 24 24",
1906
+ fill: "none",
1907
+ stroke: "currentColor",
1908
+ strokeWidth: "2",
1909
+ strokeLinecap: "round",
1910
+ strokeLinejoin: "round",
1911
+ children: [
1912
+ /* @__PURE__ */ jsx8("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
1913
+ /* @__PURE__ */ jsx8("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
1914
+ ]
1915
+ }
1916
+ )
1677
1917
  }
1678
1918
  )
1679
- }
1680
- )
1681
- ] })
1682
- ] });
1919
+ ] })
1920
+ ]
1921
+ }
1922
+ );
1923
+ }
1924
+ function hasFiles(e) {
1925
+ const types = e.dataTransfer?.types;
1926
+ if (!types) return false;
1927
+ for (let i = 0; i < types.length; i++) {
1928
+ if (types[i] === "Files") return true;
1929
+ }
1930
+ return false;
1683
1931
  }
1684
1932
  function Thumbnail({
1685
1933
  attachment,
1686
1934
  onRemove,
1687
1935
  t
1688
1936
  }) {
1689
- const { status, previewUrl } = attachment;
1937
+ const { status, previewUrl, blob, filename } = attachment;
1938
+ const isImage = isImageMime(blob.type);
1690
1939
  const wrap = {
1691
1940
  position: "relative",
1692
- width: 56,
1941
+ width: isImage ? 56 : 160,
1693
1942
  height: 56,
1694
1943
  borderRadius: 8,
1695
1944
  overflow: "hidden",
1696
1945
  border: status === "error" ? "2px solid #b91c1c" : "1px solid #e0e0e0",
1697
- background: "#f5f5f5"
1946
+ background: "#f5f5f5",
1947
+ display: "flex",
1948
+ alignItems: "center",
1949
+ justifyContent: isImage ? "stretch" : "flex-start",
1950
+ gap: isImage ? 0 : 8,
1951
+ padding: isImage ? 0 : "0 8px"
1698
1952
  };
1699
1953
  const img = {
1700
1954
  width: "100%",
@@ -1725,8 +1979,42 @@ function Thumbnail({
1725
1979
  alignItems: "center",
1726
1980
  justifyContent: "center"
1727
1981
  };
1982
+ const docName = {
1983
+ fontSize: 12,
1984
+ fontWeight: 500,
1985
+ color: "#333",
1986
+ overflow: "hidden",
1987
+ textOverflow: "ellipsis",
1988
+ whiteSpace: "nowrap",
1989
+ maxWidth: 110,
1990
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
1991
+ };
1992
+ const docMeta = {
1993
+ fontSize: 11,
1994
+ color: "#888",
1995
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
1996
+ };
1997
+ const displayName = filename ?? (isImage ? "" : "Document");
1998
+ const sizeLabel = formatBytes(blob.size);
1728
1999
  return /* @__PURE__ */ jsxs5("div", { style: wrap, children: [
1729
- /* @__PURE__ */ jsx8("img", { src: previewUrl, alt: "", style: img }),
2000
+ isImage ? /* @__PURE__ */ jsx8("img", { src: previewUrl, alt: "", style: img }) : /* @__PURE__ */ jsxs5(Fragment4, { children: [
2001
+ /* @__PURE__ */ jsx8(DocIcon, {}),
2002
+ /* @__PURE__ */ jsxs5(
2003
+ "div",
2004
+ {
2005
+ style: {
2006
+ display: "flex",
2007
+ flexDirection: "column",
2008
+ minWidth: 0,
2009
+ flex: 1
2010
+ },
2011
+ children: [
2012
+ /* @__PURE__ */ jsx8("span", { style: docName, title: displayName, children: displayName }),
2013
+ sizeLabel && /* @__PURE__ */ jsx8("span", { style: docMeta, children: sizeLabel })
2014
+ ]
2015
+ }
2016
+ )
2017
+ ] }),
1730
2018
  status === "uploading" && /* @__PURE__ */ jsx8("div", { style: overlay, "aria-label": "Uploading", children: /* @__PURE__ */ jsx8(Spinner2, {}) }),
1731
2019
  status === "error" && /* @__PURE__ */ jsx8(
1732
2020
  "div",
@@ -1749,7 +2037,45 @@ function Thumbnail({
1749
2037
  )
1750
2038
  ] });
1751
2039
  }
1752
- function CameraIcon() {
2040
+ function PaperclipIcon() {
2041
+ return /* @__PURE__ */ jsx8(
2042
+ "svg",
2043
+ {
2044
+ width: "20",
2045
+ height: "20",
2046
+ viewBox: "0 0 24 24",
2047
+ fill: "none",
2048
+ stroke: "currentColor",
2049
+ strokeWidth: "2",
2050
+ strokeLinecap: "round",
2051
+ strokeLinejoin: "round",
2052
+ "aria-hidden": "true",
2053
+ children: /* @__PURE__ */ jsx8("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" })
2054
+ }
2055
+ );
2056
+ }
2057
+ function ImageIcon() {
2058
+ return /* @__PURE__ */ jsxs5(
2059
+ "svg",
2060
+ {
2061
+ width: "18",
2062
+ height: "18",
2063
+ viewBox: "0 0 24 24",
2064
+ fill: "none",
2065
+ stroke: "currentColor",
2066
+ strokeWidth: "2",
2067
+ strokeLinecap: "round",
2068
+ strokeLinejoin: "round",
2069
+ "aria-hidden": "true",
2070
+ children: [
2071
+ /* @__PURE__ */ jsx8("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
2072
+ /* @__PURE__ */ jsx8("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
2073
+ /* @__PURE__ */ jsx8("polyline", { points: "21 15 16 10 5 21" })
2074
+ ]
2075
+ }
2076
+ );
2077
+ }
2078
+ function DocIcon() {
1753
2079
  return /* @__PURE__ */ jsxs5(
1754
2080
  "svg",
1755
2081
  {
@@ -1762,6 +2088,36 @@ function CameraIcon() {
1762
2088
  strokeLinecap: "round",
1763
2089
  strokeLinejoin: "round",
1764
2090
  "aria-hidden": "true",
2091
+ style: { color: "#666", flexShrink: 0 },
2092
+ children: [
2093
+ /* @__PURE__ */ jsx8("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
2094
+ /* @__PURE__ */ jsx8("polyline", { points: "14 2 14 8 20 8" }),
2095
+ /* @__PURE__ */ jsx8("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
2096
+ /* @__PURE__ */ jsx8("line", { x1: "16", y1: "17", x2: "8", y2: "17" }),
2097
+ /* @__PURE__ */ jsx8("polyline", { points: "10 9 9 9 8 9" })
2098
+ ]
2099
+ }
2100
+ );
2101
+ }
2102
+ function formatBytes(bytes) {
2103
+ if (!Number.isFinite(bytes) || bytes <= 0) return "";
2104
+ if (bytes < 1024) return `${bytes} B`;
2105
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
2106
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2107
+ }
2108
+ function CameraIcon() {
2109
+ return /* @__PURE__ */ jsxs5(
2110
+ "svg",
2111
+ {
2112
+ width: "18",
2113
+ height: "18",
2114
+ viewBox: "0 0 24 24",
2115
+ fill: "none",
2116
+ stroke: "currentColor",
2117
+ strokeWidth: "2",
2118
+ strokeLinecap: "round",
2119
+ strokeLinejoin: "round",
2120
+ "aria-hidden": "true",
1765
2121
  children: [
1766
2122
  /* @__PURE__ */ jsx8("path", { d: "M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" }),
1767
2123
  /* @__PURE__ */ jsx8("circle", { cx: "12", cy: "13", r: "4" })
@@ -2163,7 +2519,8 @@ function ChatWindow() {
2163
2519
  import { Fragment as Fragment6, jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
2164
2520
  function ChatWidgetInner({ identity }) {
2165
2521
  const client = useCustomerHeroClient();
2166
- const prevIdentityRef = useRef4(void 0);
2522
+ const { configLoaded, configError } = useChat();
2523
+ const prevIdentityRef = useRef5(void 0);
2167
2524
  useEffect8(() => {
2168
2525
  const key = identity ? JSON.stringify(identity) : void 0;
2169
2526
  if (key !== prevIdentityRef.current) {
@@ -2173,6 +2530,7 @@ function ChatWidgetInner({ identity }) {
2173
2530
  }
2174
2531
  }
2175
2532
  }, [identity, client]);
2533
+ if (!configLoaded || configError) return null;
2176
2534
  return /* @__PURE__ */ jsxs7(Fragment6, { children: [
2177
2535
  /* @__PURE__ */ jsx10(ChatBubble, {}),
2178
2536
  /* @__PURE__ */ jsx10(ChatWindow, {})
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@customerhero/react",
3
- "version": "1.1.0",
3
+ "version": "2.1.0",
4
4
  "private": false,
5
5
  "description": "React components for embedding the CustomerHero chat widget.",
6
6
  "keywords": [
@@ -58,7 +58,7 @@
58
58
  "react": ">=18"
59
59
  },
60
60
  "devDependencies": {
61
- "@customerhero/js": "^2.0.0",
61
+ "@customerhero/js": "^2.1.0",
62
62
  "@testing-library/react": "^16.1.0",
63
63
  "@types/react": "^19.0.0",
64
64
  "@types/react-dom": "^19.0.0",