@customerhero/react 2.1.0 → 2.1.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
@@ -732,9 +732,11 @@ function renderMarkdown(source, opts = {}) {
732
732
  style: {
733
733
  margin: "0 0 8px",
734
734
  paddingLeft: 20,
735
- lineHeight: 1.5
735
+ lineHeight: 1.5,
736
+ listStyleType: "disc",
737
+ listStylePosition: "outside"
736
738
  },
737
- children: block.lines.map((l, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { children: renderInline(l) }, i))
739
+ children: block.lines.map((l, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { style: { display: "list-item" }, children: renderInline(l) }, i))
738
740
  },
739
741
  key
740
742
  );
@@ -745,9 +747,11 @@ function renderMarkdown(source, opts = {}) {
745
747
  style: {
746
748
  margin: "0 0 8px",
747
749
  paddingLeft: 22,
748
- lineHeight: 1.5
750
+ lineHeight: 1.5,
751
+ listStyleType: "decimal",
752
+ listStylePosition: "outside"
749
753
  },
750
- children: block.lines.map((l, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { children: renderInline(l) }, i))
754
+ children: block.lines.map((l, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { style: { display: "list-item" }, children: renderInline(l) }, i))
751
755
  },
752
756
  key
753
757
  );
@@ -950,11 +954,11 @@ function MessageStatusPill({
950
954
  const containerStyle = {
951
955
  display: "flex",
952
956
  alignItems: "center",
957
+ justifyContent: "flex-end",
953
958
  gap: 4,
954
959
  marginTop: 2,
955
960
  fontSize: 11,
956
- color: failed ? "#b91c1c" : "#888",
957
- alignSelf: "flex-end"
961
+ color: failed ? "#b91c1c" : "#888"
958
962
  };
959
963
  const labelKey = status === "sending" ? "status_sending" : status === "sent" ? "status_sent" : "status_failed";
960
964
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: containerStyle, "aria-live": "polite", children: [
@@ -1263,13 +1267,20 @@ function Message({
1263
1267
  };
1264
1268
  const linkColor = isUser ? "#ffffff" : config.primaryColor;
1265
1269
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(AnimatedMessage, { isUser, animate, reduced, children: [
1266
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: bubbleStyle, children: isUser ? message.content : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
1267
- renderMarkdown(message.content, {
1268
- sources: message.sources,
1269
- linkColor
1270
- }),
1271
- message.streaming && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(StreamingCursor, { reduced })
1272
- ] }) }),
1270
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1271
+ "div",
1272
+ {
1273
+ style: bubbleStyle,
1274
+ "data-streaming-bubble": !isUser && message.streaming ? "true" : void 0,
1275
+ children: isUser ? message.content : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
1276
+ renderMarkdown(message.content, {
1277
+ sources: message.sources,
1278
+ linkColor
1279
+ }),
1280
+ message.streaming && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(StreamingCursor, { reduced })
1281
+ ] })
1282
+ }
1283
+ ),
1273
1284
  isUser && message.status && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MessageStatusPill, { status: message.status, t }),
1274
1285
  !isUser && message.blocks?.map((block, i) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1275
1286
  BlockRenderer,
@@ -1354,16 +1365,51 @@ function ChatMessages() {
1354
1365
  t
1355
1366
  } = useChat();
1356
1367
  const reduced = useReducedMotion();
1368
+ const containerRef = (0, import_react7.useRef)(null);
1357
1369
  const messagesEndRef = (0, import_react7.useRef)(null);
1358
1370
  const isFirstRender = (0, import_react7.useRef)(true);
1359
1371
  const prevMessageCount = (0, import_react7.useRef)(0);
1372
+ const stickRef = (0, import_react7.useRef)(true);
1373
+ const suppressScrollRef = (0, import_react7.useRef)(false);
1374
+ const autoScrollTo = (top) => {
1375
+ const el = containerRef.current;
1376
+ if (!el) return;
1377
+ suppressScrollRef.current = true;
1378
+ el.scrollTop = top;
1379
+ requestAnimationFrame(() => {
1380
+ suppressScrollRef.current = false;
1381
+ });
1382
+ };
1383
+ const handleScroll = () => {
1384
+ if (suppressScrollRef.current) return;
1385
+ const el = containerRef.current;
1386
+ if (!el) return;
1387
+ const distFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
1388
+ stickRef.current = distFromBottom < 60;
1389
+ };
1360
1390
  (0, import_react7.useEffect)(() => {
1361
- if (messagesEndRef.current) {
1362
- messagesEndRef.current.scrollIntoView({
1363
- behavior: isFirstRender.current || reduced ? "auto" : "smooth"
1364
- });
1365
- isFirstRender.current = false;
1391
+ const el = containerRef.current;
1392
+ if (!el) return;
1393
+ const lastMsg2 = messages[messages.length - 1];
1394
+ if (messages.length > prevMessageCount.current && lastMsg2?.role === "user") {
1395
+ stickRef.current = true;
1396
+ }
1397
+ if (!stickRef.current && !isFirstRender.current) {
1398
+ return;
1366
1399
  }
1400
+ const streamingBubble = el.querySelector(
1401
+ "[data-streaming-bubble='true']"
1402
+ );
1403
+ let target = el.scrollHeight - el.clientHeight;
1404
+ if (streamingBubble && streamingBubble.offsetHeight > el.clientHeight - 24) {
1405
+ const containerTop = el.getBoundingClientRect().top;
1406
+ const bubbleTop = streamingBubble.getBoundingClientRect().top;
1407
+ const TOP_GAP = 16;
1408
+ target = el.scrollTop + (bubbleTop - containerTop) - TOP_GAP;
1409
+ stickRef.current = false;
1410
+ }
1411
+ autoScrollTo(target);
1412
+ isFirstRender.current = false;
1367
1413
  }, [messages, isLoading, reduced]);
1368
1414
  const newStartIndex = isFirstRender.current ? messages.length : prevMessageCount.current;
1369
1415
  (0, import_react7.useEffect)(() => {
@@ -1386,7 +1432,7 @@ function ChatMessages() {
1386
1432
  };
1387
1433
  const lastMsg = messages[messages.length - 1];
1388
1434
  const waitingForFirstToken = isLoading && (lastMsg?.role !== "bot" || lastMsg.streaming !== true);
1389
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: containerStyle, children: [
1435
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { ref: containerRef, style: containerStyle, onScroll: handleScroll, children: [
1390
1436
  messages.map((msg, i) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1391
1437
  Message,
1392
1438
  {
@@ -1505,12 +1551,23 @@ function ChatInput() {
1505
1551
  const [dragActive, setDragActive] = (0, import_react8.useState)(false);
1506
1552
  const [transientError, setTransientError] = (0, import_react8.useState)(null);
1507
1553
  const fileInputRef = (0, import_react8.useRef)(null);
1554
+ const textInputRef = (0, import_react8.useRef)(null);
1508
1555
  const menuRef = (0, import_react8.useRef)(null);
1509
1556
  const menuButtonRef = (0, import_react8.useRef)(null);
1510
1557
  const dragCounterRef = (0, import_react8.useRef)(0);
1511
1558
  (0, import_react8.useEffect)(() => {
1512
1559
  setCaptureSupported((0, import_js2.canCaptureScreenshot)());
1513
1560
  }, []);
1561
+ (0, import_react8.useEffect)(() => {
1562
+ const id = requestAnimationFrame(() => textInputRef.current?.focus());
1563
+ return () => cancelAnimationFrame(id);
1564
+ }, []);
1565
+ (0, import_react8.useLayoutEffect)(() => {
1566
+ const el = textInputRef.current;
1567
+ if (!el) return;
1568
+ el.style.height = "auto";
1569
+ el.style.height = `${el.scrollHeight}px`;
1570
+ }, [value]);
1514
1571
  (0, import_react8.useEffect)(() => {
1515
1572
  if (pendingPrefill === null) return;
1516
1573
  const text = consumePendingPrefill();
@@ -1688,19 +1745,27 @@ function ChatInput() {
1688
1745
  };
1689
1746
  const rowStyle = {
1690
1747
  display: "flex",
1691
- alignItems: "center",
1748
+ // Anchor the icon buttons to the bottom of the row so a multi-line
1749
+ // textarea grows upward without dragging them along.
1750
+ alignItems: "flex-end",
1692
1751
  gap: 8
1693
1752
  };
1753
+ const TEXTAREA_MAX_HEIGHT = 140;
1694
1754
  const inputStyle = {
1695
1755
  flex: 1,
1696
1756
  border: "1px solid #e0e0e0",
1697
- borderRadius: 24,
1757
+ borderRadius: 18,
1698
1758
  padding: "10px 16px",
1699
1759
  fontSize: 14,
1760
+ lineHeight: 1.4,
1700
1761
  outline: "none",
1701
1762
  background: "#fafafa",
1702
1763
  color: config.textColor,
1703
- fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
1764
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
1765
+ resize: "none",
1766
+ overflowY: "auto",
1767
+ maxHeight: TEXTAREA_MAX_HEIGHT,
1768
+ boxSizing: "border-box"
1704
1769
  };
1705
1770
  const sendButtonStyle = {
1706
1771
  width: 36,
@@ -1758,6 +1823,7 @@ function ChatInput() {
1758
1823
  fontSize: 14,
1759
1824
  color: "#333",
1760
1825
  textAlign: "left",
1826
+ whiteSpace: "nowrap",
1761
1827
  fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
1762
1828
  };
1763
1829
  const dropOverlayStyle = {
@@ -1884,9 +1950,10 @@ function ChatInput() {
1884
1950
  }
1885
1951
  ),
1886
1952
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1887
- "input",
1953
+ "textarea",
1888
1954
  {
1889
- type: "text",
1955
+ ref: textInputRef,
1956
+ rows: 1,
1890
1957
  value,
1891
1958
  onChange: (e) => setValue(e.target.value),
1892
1959
  onKeyDown: handleKeyDown,
package/dist/index.js CHANGED
@@ -711,9 +711,11 @@ function renderMarkdown(source, opts = {}) {
711
711
  style: {
712
712
  margin: "0 0 8px",
713
713
  paddingLeft: 20,
714
- lineHeight: 1.5
714
+ lineHeight: 1.5,
715
+ listStyleType: "disc",
716
+ listStylePosition: "outside"
715
717
  },
716
- children: block.lines.map((l, i) => /* @__PURE__ */ jsx4("li", { children: renderInline(l) }, i))
718
+ children: block.lines.map((l, i) => /* @__PURE__ */ jsx4("li", { style: { display: "list-item" }, children: renderInline(l) }, i))
717
719
  },
718
720
  key
719
721
  );
@@ -724,9 +726,11 @@ function renderMarkdown(source, opts = {}) {
724
726
  style: {
725
727
  margin: "0 0 8px",
726
728
  paddingLeft: 22,
727
- lineHeight: 1.5
729
+ lineHeight: 1.5,
730
+ listStyleType: "decimal",
731
+ listStylePosition: "outside"
728
732
  },
729
- children: block.lines.map((l, i) => /* @__PURE__ */ jsx4("li", { children: renderInline(l) }, i))
733
+ children: block.lines.map((l, i) => /* @__PURE__ */ jsx4("li", { style: { display: "list-item" }, children: renderInline(l) }, i))
730
734
  },
731
735
  key
732
736
  );
@@ -929,11 +933,11 @@ function MessageStatusPill({
929
933
  const containerStyle = {
930
934
  display: "flex",
931
935
  alignItems: "center",
936
+ justifyContent: "flex-end",
932
937
  gap: 4,
933
938
  marginTop: 2,
934
939
  fontSize: 11,
935
- color: failed ? "#b91c1c" : "#888",
936
- alignSelf: "flex-end"
940
+ color: failed ? "#b91c1c" : "#888"
937
941
  };
938
942
  const labelKey = status === "sending" ? "status_sending" : status === "sent" ? "status_sent" : "status_failed";
939
943
  return /* @__PURE__ */ jsxs4("div", { style: containerStyle, "aria-live": "polite", children: [
@@ -1242,13 +1246,20 @@ function Message({
1242
1246
  };
1243
1247
  const linkColor = isUser ? "#ffffff" : config.primaryColor;
1244
1248
  return /* @__PURE__ */ jsxs4(AnimatedMessage, { isUser, animate, reduced, children: [
1245
- /* @__PURE__ */ jsx6("div", { style: bubbleStyle, children: isUser ? message.content : /* @__PURE__ */ jsxs4(Fragment3, { children: [
1246
- renderMarkdown(message.content, {
1247
- sources: message.sources,
1248
- linkColor
1249
- }),
1250
- message.streaming && /* @__PURE__ */ jsx6(StreamingCursor, { reduced })
1251
- ] }) }),
1249
+ /* @__PURE__ */ jsx6(
1250
+ "div",
1251
+ {
1252
+ style: bubbleStyle,
1253
+ "data-streaming-bubble": !isUser && message.streaming ? "true" : void 0,
1254
+ children: isUser ? message.content : /* @__PURE__ */ jsxs4(Fragment3, { children: [
1255
+ renderMarkdown(message.content, {
1256
+ sources: message.sources,
1257
+ linkColor
1258
+ }),
1259
+ message.streaming && /* @__PURE__ */ jsx6(StreamingCursor, { reduced })
1260
+ ] })
1261
+ }
1262
+ ),
1252
1263
  isUser && message.status && /* @__PURE__ */ jsx6(MessageStatusPill, { status: message.status, t }),
1253
1264
  !isUser && message.blocks?.map((block, i) => /* @__PURE__ */ jsx6(
1254
1265
  BlockRenderer,
@@ -1333,16 +1344,51 @@ function ChatMessages() {
1333
1344
  t
1334
1345
  } = useChat();
1335
1346
  const reduced = useReducedMotion();
1347
+ const containerRef = useRef3(null);
1336
1348
  const messagesEndRef = useRef3(null);
1337
1349
  const isFirstRender = useRef3(true);
1338
1350
  const prevMessageCount = useRef3(0);
1351
+ const stickRef = useRef3(true);
1352
+ const suppressScrollRef = useRef3(false);
1353
+ const autoScrollTo = (top) => {
1354
+ const el = containerRef.current;
1355
+ if (!el) return;
1356
+ suppressScrollRef.current = true;
1357
+ el.scrollTop = top;
1358
+ requestAnimationFrame(() => {
1359
+ suppressScrollRef.current = false;
1360
+ });
1361
+ };
1362
+ const handleScroll = () => {
1363
+ if (suppressScrollRef.current) return;
1364
+ const el = containerRef.current;
1365
+ if (!el) return;
1366
+ const distFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
1367
+ stickRef.current = distFromBottom < 60;
1368
+ };
1339
1369
  useEffect5(() => {
1340
- if (messagesEndRef.current) {
1341
- messagesEndRef.current.scrollIntoView({
1342
- behavior: isFirstRender.current || reduced ? "auto" : "smooth"
1343
- });
1344
- isFirstRender.current = false;
1370
+ const el = containerRef.current;
1371
+ if (!el) return;
1372
+ const lastMsg2 = messages[messages.length - 1];
1373
+ if (messages.length > prevMessageCount.current && lastMsg2?.role === "user") {
1374
+ stickRef.current = true;
1375
+ }
1376
+ if (!stickRef.current && !isFirstRender.current) {
1377
+ return;
1345
1378
  }
1379
+ const streamingBubble = el.querySelector(
1380
+ "[data-streaming-bubble='true']"
1381
+ );
1382
+ let target = el.scrollHeight - el.clientHeight;
1383
+ if (streamingBubble && streamingBubble.offsetHeight > el.clientHeight - 24) {
1384
+ const containerTop = el.getBoundingClientRect().top;
1385
+ const bubbleTop = streamingBubble.getBoundingClientRect().top;
1386
+ const TOP_GAP = 16;
1387
+ target = el.scrollTop + (bubbleTop - containerTop) - TOP_GAP;
1388
+ stickRef.current = false;
1389
+ }
1390
+ autoScrollTo(target);
1391
+ isFirstRender.current = false;
1346
1392
  }, [messages, isLoading, reduced]);
1347
1393
  const newStartIndex = isFirstRender.current ? messages.length : prevMessageCount.current;
1348
1394
  useEffect5(() => {
@@ -1365,7 +1411,7 @@ function ChatMessages() {
1365
1411
  };
1366
1412
  const lastMsg = messages[messages.length - 1];
1367
1413
  const waitingForFirstToken = isLoading && (lastMsg?.role !== "bot" || lastMsg.streaming !== true);
1368
- return /* @__PURE__ */ jsxs4("div", { style: containerStyle, children: [
1414
+ return /* @__PURE__ */ jsxs4("div", { ref: containerRef, style: containerStyle, onScroll: handleScroll, children: [
1369
1415
  messages.map((msg, i) => /* @__PURE__ */ jsx6(
1370
1416
  Message,
1371
1417
  {
@@ -1454,6 +1500,7 @@ function ChatSuggestions() {
1454
1500
  // src/components/chat-input.tsx
1455
1501
  import {
1456
1502
  useEffect as useEffect6,
1503
+ useLayoutEffect,
1457
1504
  useRef as useRef4,
1458
1505
  useState as useState6
1459
1506
  } from "react";
@@ -1492,12 +1539,23 @@ function ChatInput() {
1492
1539
  const [dragActive, setDragActive] = useState6(false);
1493
1540
  const [transientError, setTransientError] = useState6(null);
1494
1541
  const fileInputRef = useRef4(null);
1542
+ const textInputRef = useRef4(null);
1495
1543
  const menuRef = useRef4(null);
1496
1544
  const menuButtonRef = useRef4(null);
1497
1545
  const dragCounterRef = useRef4(0);
1498
1546
  useEffect6(() => {
1499
1547
  setCaptureSupported(canCaptureScreenshot());
1500
1548
  }, []);
1549
+ useEffect6(() => {
1550
+ const id = requestAnimationFrame(() => textInputRef.current?.focus());
1551
+ return () => cancelAnimationFrame(id);
1552
+ }, []);
1553
+ useLayoutEffect(() => {
1554
+ const el = textInputRef.current;
1555
+ if (!el) return;
1556
+ el.style.height = "auto";
1557
+ el.style.height = `${el.scrollHeight}px`;
1558
+ }, [value]);
1501
1559
  useEffect6(() => {
1502
1560
  if (pendingPrefill === null) return;
1503
1561
  const text = consumePendingPrefill();
@@ -1675,19 +1733,27 @@ function ChatInput() {
1675
1733
  };
1676
1734
  const rowStyle = {
1677
1735
  display: "flex",
1678
- alignItems: "center",
1736
+ // Anchor the icon buttons to the bottom of the row so a multi-line
1737
+ // textarea grows upward without dragging them along.
1738
+ alignItems: "flex-end",
1679
1739
  gap: 8
1680
1740
  };
1741
+ const TEXTAREA_MAX_HEIGHT = 140;
1681
1742
  const inputStyle = {
1682
1743
  flex: 1,
1683
1744
  border: "1px solid #e0e0e0",
1684
- borderRadius: 24,
1745
+ borderRadius: 18,
1685
1746
  padding: "10px 16px",
1686
1747
  fontSize: 14,
1748
+ lineHeight: 1.4,
1687
1749
  outline: "none",
1688
1750
  background: "#fafafa",
1689
1751
  color: config.textColor,
1690
- fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
1752
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
1753
+ resize: "none",
1754
+ overflowY: "auto",
1755
+ maxHeight: TEXTAREA_MAX_HEIGHT,
1756
+ boxSizing: "border-box"
1691
1757
  };
1692
1758
  const sendButtonStyle = {
1693
1759
  width: 36,
@@ -1745,6 +1811,7 @@ function ChatInput() {
1745
1811
  fontSize: 14,
1746
1812
  color: "#333",
1747
1813
  textAlign: "left",
1814
+ whiteSpace: "nowrap",
1748
1815
  fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
1749
1816
  };
1750
1817
  const dropOverlayStyle = {
@@ -1871,9 +1938,10 @@ function ChatInput() {
1871
1938
  }
1872
1939
  ),
1873
1940
  /* @__PURE__ */ jsx8(
1874
- "input",
1941
+ "textarea",
1875
1942
  {
1876
- type: "text",
1943
+ ref: textInputRef,
1944
+ rows: 1,
1877
1945
  value,
1878
1946
  onChange: (e) => setValue(e.target.value),
1879
1947
  onKeyDown: handleKeyDown,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@customerhero/react",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
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.1.0",
61
+ "@customerhero/js": "^2.1.1",
62
62
  "@testing-library/react": "^16.1.0",
63
63
  "@types/react": "^19.0.0",
64
64
  "@types/react-dom": "^19.0.0",