@sensiblestats/widget-react 0.38.0 → 0.39.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @sensiblestats/widget-react
2
2
 
3
+ ## 0.39.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 428171d: Fix dead scroll space below the chat: the top-anchor spacer was reserved even when a turn was first shown already-complete (panel opened after the greeting finished, or a restored/cross-tab-synced conversation), leaving a full viewport of blank scrollable space. It is now reserved only while a turn is actively streaming. Also adds `overscroll-behavior: contain` to the message list and drops the `+` icon from the "New conversation" menu item.
8
+
9
+ ## 0.39.0
10
+
11
+ ### Minor Changes
12
+
13
+ - a5e97e2: Move the "New conversation" header action into a kebab (⋮) overflow menu, keeping Close as a one-tap control. The menu opens on click, closes on Escape, outside-click, or selection, and manages focus for keyboard users.
14
+
3
15
  ## 0.38.0
4
16
 
5
17
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -36,7 +36,7 @@ __export(index_exports, {
36
36
  module.exports = __toCommonJS(index_exports);
37
37
 
38
38
  // src/StatsWidget.tsx
39
- var import_react23 = require("react");
39
+ var import_react24 = require("react");
40
40
  var import_widget_sdk5 = require("@sensiblestats/widget-sdk");
41
41
 
42
42
  // src/kickoff.ts
@@ -58,6 +58,7 @@ var STRINGS = {
58
58
  placeholder: "Ask about a team, player or match\u2026",
59
59
  closeChat: "Close chat",
60
60
  newConversation: "New conversation",
61
+ moreOptions: "More options",
61
62
  chatDialogLabel: "SensibleStats chat",
62
63
  stopResponse: "Stop response",
63
64
  sendMessage: "Send message",
@@ -131,6 +132,7 @@ var STRINGS = {
131
132
  placeholder: "Bir tak\u0131m, oyuncu veya ma\xE7 hakk\u0131nda sor\u2026",
132
133
  closeChat: "Sohbeti kapat",
133
134
  newConversation: "Yeni sohbet",
135
+ moreOptions: "Di\u011Fer se\xE7enekler",
134
136
  chatDialogLabel: "SensibleStats sohbet",
135
137
  stopResponse: "Yan\u0131t\u0131 durdur",
136
138
  sendMessage: "Mesaj g\xF6nder",
@@ -227,6 +229,7 @@ function normalizeUi(cfg) {
227
229
  injectStyles: cfg.injectStyles ?? true,
228
230
  closeChat: strings.closeChat,
229
231
  newConversation: strings.newConversation,
232
+ moreOptions: strings.moreOptions,
230
233
  chatDialogLabel: strings.chatDialogLabel,
231
234
  stopResponse: strings.stopResponse,
232
235
  sendMessage: strings.sendMessage,
@@ -1428,8 +1431,12 @@ function CloseIcon() {
1428
1431
  function ChevronIcon() {
1429
1432
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", width: "1em", height: "1em", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", d: "M9 5l7 7-7 7" }) });
1430
1433
  }
1431
- function NewChatIcon() {
1432
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", width: "1em", height: "1em", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { fill: "none", d: "M12 5v14M5 12h14", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) });
1434
+ function KebabIcon() {
1435
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", width: "1em", height: "1em", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { fill: "currentColor", children: [
1436
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "5", r: "1.6" }),
1437
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "12", r: "1.6" }),
1438
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "19", r: "1.6" })
1439
+ ] }) });
1433
1440
  }
1434
1441
  var DASHBOARD_EMOJI = {
1435
1442
  football: "\u26BD",
@@ -1495,21 +1502,86 @@ function ChatFab({ label, greeting, popupText, logoUrl, onOpen, onDismissPopup,
1495
1502
  }
1496
1503
 
1497
1504
  // src/components/ChatPanel.tsx
1498
- var import_react22 = require("react");
1505
+ var import_react23 = require("react");
1499
1506
 
1500
1507
  // src/components/ChatHeader.tsx
1508
+ var import_react6 = require("react");
1501
1509
  var import_jsx_runtime5 = require("react/jsx-runtime");
1502
- function ChatHeader({ onClose, closeLabel = "Close chat", logoUrl, headerTitle, onNewConversation, canReset, newConversationLabel = "New conversation" }) {
1510
+ function ChatHeader({
1511
+ onClose,
1512
+ closeLabel = "Close chat",
1513
+ logoUrl,
1514
+ headerTitle,
1515
+ onNewConversation,
1516
+ canReset,
1517
+ newConversationLabel = "New conversation",
1518
+ moreOptionsLabel = "More options"
1519
+ }) {
1520
+ const [menuOpen, setMenuOpen] = (0, import_react6.useState)(false);
1521
+ const wrapRef = (0, import_react6.useRef)(null);
1522
+ const kebabRef = (0, import_react6.useRef)(null);
1523
+ const firstItemRef = (0, import_react6.useRef)(null);
1524
+ const hasMenu = !!(canReset && onNewConversation);
1525
+ (0, import_react6.useEffect)(() => {
1526
+ if (menuOpen) firstItemRef.current?.focus();
1527
+ }, [menuOpen]);
1528
+ (0, import_react6.useEffect)(() => {
1529
+ if (!menuOpen) return;
1530
+ const onPointer = (e) => {
1531
+ if (!wrapRef.current?.contains(e.target)) setMenuOpen(false);
1532
+ };
1533
+ const onKey = (e) => {
1534
+ if (e.key === "Escape") {
1535
+ setMenuOpen(false);
1536
+ kebabRef.current?.focus();
1537
+ }
1538
+ };
1539
+ document.addEventListener("mousedown", onPointer);
1540
+ document.addEventListener("keydown", onKey);
1541
+ return () => {
1542
+ document.removeEventListener("mousedown", onPointer);
1543
+ document.removeEventListener("keydown", onKey);
1544
+ };
1545
+ }, [menuOpen]);
1546
+ const runNewConversation = () => {
1547
+ setMenuOpen(false);
1548
+ onNewConversation?.();
1549
+ };
1503
1550
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("header", { className: "ss-w-header", children: [
1504
1551
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(BrandMark, { logoUrl, className: "ss-w-header-mark" }),
1505
1552
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "ss-w-header-title", children: headerTitle ?? "SensibleStats" }),
1506
- canReset && onNewConversation ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "ss-w-newconv", "aria-label": newConversationLabel, onClick: onNewConversation, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(NewChatIcon, {}) }) : null,
1553
+ hasMenu ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "ss-w-menu-wrap", ref: wrapRef, children: [
1554
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1555
+ "button",
1556
+ {
1557
+ type: "button",
1558
+ className: "ss-w-kebab",
1559
+ ref: kebabRef,
1560
+ "aria-haspopup": "menu",
1561
+ "aria-expanded": menuOpen,
1562
+ "aria-label": moreOptionsLabel,
1563
+ onClick: () => setMenuOpen((open) => !open),
1564
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(KebabIcon, {})
1565
+ }
1566
+ ),
1567
+ menuOpen ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "ss-w-menu", role: "menu", "aria-label": moreOptionsLabel, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1568
+ "button",
1569
+ {
1570
+ type: "button",
1571
+ className: "ss-w-menu-item",
1572
+ role: "menuitem",
1573
+ ref: firstItemRef,
1574
+ onClick: runNewConversation,
1575
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: newConversationLabel })
1576
+ }
1577
+ ) }) : null
1578
+ ] }) : null,
1507
1579
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "ss-w-close", "aria-label": closeLabel, onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CloseIcon, {}) })
1508
1580
  ] });
1509
1581
  }
1510
1582
 
1511
1583
  // src/components/MessageList.tsx
1512
- var import_react20 = require("react");
1584
+ var import_react21 = require("react");
1513
1585
 
1514
1586
  // src/components/Markdown.tsx
1515
1587
  var import_react_markdown = __toESM(require("react-markdown"), 1);
@@ -1527,7 +1599,7 @@ function Markdown({ text }) {
1527
1599
  }
1528
1600
 
1529
1601
  // src/components/EntityCardList.tsx
1530
- var import_react6 = require("react");
1602
+ var import_react7 = require("react");
1531
1603
  var import_jsx_runtime7 = require("react/jsx-runtime");
1532
1604
  var MAX_VISIBLE = 3;
1533
1605
  var RECORD_RE = /^(\d+)-(\d+)-(\d+)$/;
@@ -1611,7 +1683,7 @@ function ScopeTabs({ scopes, active, onSelect }) {
1611
1683
  )) });
1612
1684
  }
1613
1685
  function EntityCardItem({ c }) {
1614
- const [active, setActive] = (0, import_react6.useState)(0);
1686
+ const [active, setActive] = (0, import_react7.useState)(0);
1615
1687
  const hasBody = c.scopes.length > 0;
1616
1688
  if (!hasBody) {
1617
1689
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "ss-w-ecard ss-w-ecard-bare", "data-kind": c.kind, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CardHeader, { c, showScopeCrest: false }) });
@@ -1625,7 +1697,7 @@ function EntityCardItem({ c }) {
1625
1697
  ] });
1626
1698
  }
1627
1699
  function EntityCardList({ cards, disabled, showLessLabel = "Show less", showMoreLabel = (n) => `Show ${n} more` }) {
1628
- const [expanded, setExpanded] = (0, import_react6.useState)(false);
1700
+ const [expanded, setExpanded] = (0, import_react7.useState)(false);
1629
1701
  if (cards.length === 0) return null;
1630
1702
  const visible = expanded ? cards : cards.slice(0, MAX_VISIBLE);
1631
1703
  const overflow = cards.length - MAX_VISIBLE;
@@ -1636,18 +1708,18 @@ function EntityCardList({ cards, disabled, showLessLabel = "Show less", showMore
1636
1708
  }
1637
1709
 
1638
1710
  // src/components/BettingInsightList.tsx
1639
- var import_react11 = require("react");
1711
+ var import_react12 = require("react");
1640
1712
 
1641
1713
  // src/components/AddToSlipButton.tsx
1642
- var import_react8 = require("react");
1714
+ var import_react9 = require("react");
1643
1715
  var import_widget_sdk4 = require("@sensiblestats/widget-sdk");
1644
1716
 
1645
1717
  // src/components/Toast.tsx
1646
- var import_react7 = require("react");
1718
+ var import_react8 = require("react");
1647
1719
  var import_jsx_runtime8 = require("react/jsx-runtime");
1648
1720
  var DISMISS_MS = 4e3;
1649
1721
  function Toast({ toast, onDismiss }) {
1650
- (0, import_react7.useEffect)(() => {
1722
+ (0, import_react8.useEffect)(() => {
1651
1723
  if (!toast) return;
1652
1724
  const timer = setTimeout(onDismiss, DISMISS_MS);
1653
1725
  return () => clearTimeout(timer);
@@ -1665,10 +1737,10 @@ function withTimeout(value) {
1665
1737
  ]);
1666
1738
  }
1667
1739
  function SlipButton({ selection, shownOdds, marketName, slip, ui }) {
1668
- const [pending, setPending] = (0, import_react8.useState)(false);
1669
- const [settled, setSettled] = (0, import_react8.useState)(false);
1670
- const [toast, setToast] = (0, import_react8.useState)(null);
1671
- const dismiss = (0, import_react8.useCallback)(() => setToast(null), []);
1740
+ const [pending, setPending] = (0, import_react9.useState)(false);
1741
+ const [settled, setSettled] = (0, import_react9.useState)(false);
1742
+ const [toast, setToast] = (0, import_react9.useState)(null);
1743
+ const dismiss = (0, import_react9.useCallback)(() => setToast(null), []);
1672
1744
  const handler = slip.onAddToSlip;
1673
1745
  if (!ui || !handler || !selection) return null;
1674
1746
  const onClick = async () => {
@@ -1732,14 +1804,14 @@ function OddsMovementBadge({
1732
1804
  }
1733
1805
 
1734
1806
  // src/components/Crest.tsx
1735
- var import_react9 = require("react");
1807
+ var import_react10 = require("react");
1736
1808
  var import_jsx_runtime11 = require("react/jsx-runtime");
1737
1809
  function initials2(name) {
1738
1810
  return (name ?? "").trim().slice(0, 1).toUpperCase() || "?";
1739
1811
  }
1740
1812
  function Crest2({ url, name, kind }) {
1741
1813
  const cls = kind === "comp" ? "ss-w-bi-comp-logo" : "ss-w-bi-crest";
1742
- const [failed, setFailed] = (0, import_react9.useState)(false);
1814
+ const [failed, setFailed] = (0, import_react10.useState)(false);
1743
1815
  if (url && !failed) {
1744
1816
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("img", { className: cls, src: url, alt: "", "aria-hidden": "true", onError: () => setFailed(true) });
1745
1817
  }
@@ -1747,7 +1819,7 @@ function Crest2({ url, name, kind }) {
1747
1819
  }
1748
1820
 
1749
1821
  // src/components/PinToggle.tsx
1750
- var import_react10 = require("react");
1822
+ var import_react11 = require("react");
1751
1823
  var import_jsx_runtime12 = require("react/jsx-runtime");
1752
1824
  var PIN_GLYPHS = {
1753
1825
  pin: { viewBox: "0 0 24 24", strokeWidth: 1.4, transform: "scale(1.5)", d: "M9.5 1.5 14.5 6.5 13 8l-.5-.5-3 3 .3 2.2-1.4 1.4L5.6 11.3l-3.3 3.3-1-1 3.3-3.3L1.9 7.5l1.4-1.4 2.2.3 3-3L8 2.9z" },
@@ -1755,8 +1827,8 @@ var PIN_GLYPHS = {
1755
1827
  bookmark: { viewBox: "0 0 24 24", strokeWidth: 2, d: "M7 3h10a1.5 1.5 0 0 1 1.5 1.5V21l-6.5-3.6L5.5 21V4.5A1.5 1.5 0 0 1 7 3z" }
1756
1828
  };
1757
1829
  function PinToggle({ pin, monitor, ui }) {
1758
- const [toast, setToast] = (0, import_react10.useState)(null);
1759
- const dismiss = (0, import_react10.useCallback)(() => setToast(null), []);
1830
+ const [toast, setToast] = (0, import_react11.useState)(null);
1831
+ const dismiss = (0, import_react11.useCallback)(() => setToast(null), []);
1760
1832
  if (!monitor || !monitor.enabled) return null;
1761
1833
  const key = pinKey(pin);
1762
1834
  const pinned = monitor.isPinned(key);
@@ -1808,7 +1880,7 @@ function StatRows({ stats }) {
1808
1880
  ] }, `${s.label}:${s.value}`)) });
1809
1881
  }
1810
1882
  function ScopedStats({ scopes }) {
1811
- const [active, setActive] = (0, import_react11.useState)(0);
1883
+ const [active, setActive] = (0, import_react12.useState)(0);
1812
1884
  const current = scopes[active] ?? scopes[0] ?? { key: "", label: "", stats: [] };
1813
1885
  return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "ss-w-bi-scoped", children: [
1814
1886
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "ss-w-bi-scopes", role: "group", children: scopes.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("button", { type: "button", className: "ss-w-bi-chip", "aria-pressed": i === active, onClick: () => setActive(i), children: s.label }, s.key)) }),
@@ -1819,7 +1891,7 @@ function hasStatsContent(it) {
1819
1891
  return !!it.scopes && it.scopes.length >= 2 || it.stats.length > 0;
1820
1892
  }
1821
1893
  function ReasonStrip({ it, ui }) {
1822
- const [open, setOpen] = (0, import_react11.useState)(false);
1894
+ const [open, setOpen] = (0, import_react12.useState)(false);
1823
1895
  const hasScopes = !!it.scopes && it.scopes.length >= 2;
1824
1896
  const hasStats = hasStatsContent(it);
1825
1897
  const hasText = !!it.text;
@@ -1929,7 +2001,7 @@ function BettingInsightList({ insights, ui, slip, monitor }) {
1929
2001
  }
1930
2002
 
1931
2003
  // src/components/MarketOddsCardList.tsx
1932
- var import_react12 = require("react");
2004
+ var import_react13 = require("react");
1933
2005
  var import_jsx_runtime14 = require("react/jsx-runtime");
1934
2006
  function OddsRow({ vm, row, ui, slip, inSlipIds }) {
1935
2007
  const selection = vm.addToSlipEnabled ? marketOddsRowToSlipSelection(vm, row, slip.conversationId) : null;
@@ -1946,7 +2018,7 @@ function OddsRow({ vm, row, ui, slip, inSlipIds }) {
1946
2018
  ] });
1947
2019
  }
1948
2020
  function MarketOddsCard({ vm, ui, slip, monitor, inSlipIds, hideHeader, pinRef }) {
1949
- const [open, setOpen] = (0, import_react12.useState)(false);
2021
+ const [open, setOpen] = (0, import_react13.useState)(false);
1950
2022
  const kickoff = vm.kickoffUtc && ui?.bettingKickoff ? ui.bettingKickoff(vm.kickoffUtc) : void 0;
1951
2023
  const hasNamedTeams = !!vm.homeTeamName && !!vm.awayTeamName;
1952
2024
  const showPin = !!monitor && vm.pinEnabled;
@@ -2241,7 +2313,7 @@ function FixtureCard({ card }) {
2241
2313
  }
2242
2314
 
2243
2315
  // src/components/dashboard/CountdownCard.tsx
2244
- var import_react13 = require("react");
2316
+ var import_react14 = require("react");
2245
2317
  var import_jsx_runtime20 = require("react/jsx-runtime");
2246
2318
  var DAY_MS = 864e5;
2247
2319
  function remainingSegments(kickoffUnix, nowMs, withSeconds) {
@@ -2260,12 +2332,12 @@ function remainingSegments(kickoffUnix, nowMs, withSeconds) {
2260
2332
  function CountdownCard({ card }) {
2261
2333
  const { data } = card;
2262
2334
  const unitLabel = { d: data.daysUnit, h: data.hoursUnit, m: data.minutesUnit, s: data.secondsUnit };
2263
- const [nowMs, setNowMs] = (0, import_react13.useState)(() => Date.now());
2264
- const [reduced] = (0, import_react13.useState)(
2335
+ const [nowMs, setNowMs] = (0, import_react14.useState)(() => Date.now());
2336
+ const [reduced] = (0, import_react14.useState)(
2265
2337
  () => typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches
2266
2338
  );
2267
2339
  const withinDay = data.kickoffUnix * 1e3 - nowMs < DAY_MS;
2268
- (0, import_react13.useEffect)(() => {
2340
+ (0, import_react14.useEffect)(() => {
2269
2341
  if (reduced) return;
2270
2342
  const id = window.setInterval(() => setNowMs(Date.now()), withinDay ? 1e3 : 6e4);
2271
2343
  return () => window.clearInterval(id);
@@ -2273,7 +2345,7 @@ function CountdownCard({ card }) {
2273
2345
  const segments = remainingSegments(data.kickoffUnix, nowMs, withinDay && !reduced);
2274
2346
  return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "ss-w-fandash-card ss-w-fandash-countdown", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "ss-w-fandash-countdown-top", children: [
2275
2347
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "ss-w-fandash-eyebrow", children: card.title ?? "Kick-off in" }),
2276
- segments ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "ss-w-fandash-countdown-clock", children: segments.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_react13.Fragment, { children: [
2348
+ segments ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "ss-w-fandash-countdown-clock", children: segments.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_react14.Fragment, { children: [
2277
2349
  i > 0 ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "ss-w-fandash-countdown-colon", children: ":" }) : null,
2278
2350
  /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("span", { className: "ss-w-fandash-countdown-seg", children: [
2279
2351
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "ss-w-fandash-countdown-value", children: s.value }),
@@ -2284,7 +2356,7 @@ function CountdownCard({ card }) {
2284
2356
  }
2285
2357
 
2286
2358
  // src/components/dashboard/InjuriesCard.tsx
2287
- var import_react14 = require("react");
2359
+ var import_react15 = require("react");
2288
2360
  var import_jsx_runtime21 = require("react/jsx-runtime");
2289
2361
  var SHOWN = 3;
2290
2362
  function ColHead({ name, crestUrl }) {
@@ -2307,7 +2379,7 @@ function InjuryList({ rows, emptyLabel, limit }) {
2307
2379
  }
2308
2380
  function InjuriesCard({ card, ui }) {
2309
2381
  const { data } = card;
2310
- const [expanded, setExpanded] = (0, import_react14.useState)(false);
2382
+ const [expanded, setExpanded] = (0, import_react15.useState)(false);
2311
2383
  const hidden = Math.max(0, data.ours.length - SHOWN) + Math.max(0, data.theirs.length - SHOWN);
2312
2384
  const limit = expanded ? void 0 : SHOWN;
2313
2385
  const showMore = ui?.showMore ?? ((n) => `Show ${n} more`);
@@ -2411,7 +2483,7 @@ function LinksCard({ card }) {
2411
2483
  }
2412
2484
 
2413
2485
  // src/components/dashboard/PostMatchResultCard.tsx
2414
- var import_react15 = require("react");
2486
+ var import_react16 = require("react");
2415
2487
  var import_jsx_runtime27 = require("react/jsx-runtime");
2416
2488
  function initials3(name) {
2417
2489
  return name.trim().slice(0, 3).toUpperCase();
@@ -2421,7 +2493,7 @@ function PostMatchCrest({
2421
2493
  name,
2422
2494
  side
2423
2495
  }) {
2424
- const [failed, setFailed] = (0, import_react15.useState)(false);
2496
+ const [failed, setFailed] = (0, import_react16.useState)(false);
2425
2497
  const cls = `ss-w-fandash-postmatch-crest ss-w-fandash-postmatch-crest--${side}`;
2426
2498
  if (url && !failed) {
2427
2499
  return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("img", { className: cls, src: url, alt: "", "aria-hidden": "true", onError: () => setFailed(true) });
@@ -2620,10 +2692,10 @@ function RestingFormCard({ card }) {
2620
2692
  }
2621
2693
 
2622
2694
  // src/components/dashboard/LiveScoreCard.tsx
2623
- var import_react17 = require("react");
2695
+ var import_react18 = require("react");
2624
2696
 
2625
2697
  // src/useLiveClock.ts
2626
- var import_react16 = require("react");
2698
+ var import_react17 = require("react");
2627
2699
  var TICK_MS = 6e4;
2628
2700
  var MAX_LIVE_TICK_MINUTES = 15;
2629
2701
  function computeDisplayMinute(minute, clockRunning, receivedAtMs) {
@@ -2632,10 +2704,10 @@ function computeDisplayMinute(minute, clockRunning, receivedAtMs) {
2632
2704
  return minute + Math.min(Math.max(0, elapsedMinutes), MAX_LIVE_TICK_MINUTES);
2633
2705
  }
2634
2706
  function useLiveClock(minute, clockRunning, receivedAtMs) {
2635
- const [displayMinute, setDisplayMinute] = (0, import_react16.useState)(
2707
+ const [displayMinute, setDisplayMinute] = (0, import_react17.useState)(
2636
2708
  () => computeDisplayMinute(minute, clockRunning, receivedAtMs)
2637
2709
  );
2638
- (0, import_react16.useEffect)(() => {
2710
+ (0, import_react17.useEffect)(() => {
2639
2711
  setDisplayMinute(computeDisplayMinute(minute, clockRunning, receivedAtMs));
2640
2712
  if (!clockRunning) return;
2641
2713
  const id = window.setInterval(() => {
@@ -2652,7 +2724,7 @@ function initials5(name) {
2652
2724
  return name.trim().slice(0, 3).toUpperCase();
2653
2725
  }
2654
2726
  function LiveCrest({ url, name, side }) {
2655
- const [failed, setFailed] = (0, import_react17.useState)(false);
2727
+ const [failed, setFailed] = (0, import_react18.useState)(false);
2656
2728
  const cls = `ss-w-fandash-live-crest ss-w-fandash-live-crest--${side}`;
2657
2729
  if (url && !failed) {
2658
2730
  return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("img", { className: cls, src: url, alt: "", "aria-hidden": "true", onError: () => setFailed(true) });
@@ -2671,7 +2743,7 @@ function LiveScoreCard({
2671
2743
  }) {
2672
2744
  const { data } = card;
2673
2745
  const seedKey = `${data.minute}:${data.homeScore}:${data.awayScore}`;
2674
- const seedRef = (0, import_react17.useRef)(null);
2746
+ const seedRef = (0, import_react18.useRef)(null);
2675
2747
  if (!seedRef.current || seedRef.current.key !== seedKey) {
2676
2748
  seedRef.current = { key: seedKey, receivedAtMs: Date.now() };
2677
2749
  }
@@ -2711,7 +2783,7 @@ function LiveScoreCard({
2711
2783
  }
2712
2784
 
2713
2785
  // src/components/dashboard/LiveTimelineCard.tsx
2714
- var import_react18 = require("react");
2786
+ var import_react19 = require("react");
2715
2787
  var import_jsx_runtime37 = require("react/jsx-runtime");
2716
2788
  var SHOWN2 = 3;
2717
2789
  function EventIcon({ type }) {
@@ -2753,7 +2825,7 @@ function EventRow({ e }) {
2753
2825
  ] });
2754
2826
  }
2755
2827
  function LiveTimelineCard({ card, ui }) {
2756
- const [expanded, setExpanded] = (0, import_react18.useState)(false);
2828
+ const [expanded, setExpanded] = (0, import_react19.useState)(false);
2757
2829
  const events = card.data.events;
2758
2830
  if (!events || events.length === 0) return null;
2759
2831
  const sorted = [...events].sort((a, b) => b.minute - a.minute);
@@ -2778,7 +2850,7 @@ function LiveTimelineCard({ card, ui }) {
2778
2850
  }
2779
2851
 
2780
2852
  // src/components/dashboard/MomentumWave.tsx
2781
- var import_react19 = require("react");
2853
+ var import_react20 = require("react");
2782
2854
  var import_jsx_runtime38 = require("react/jsx-runtime");
2783
2855
  var WIDTH = 270;
2784
2856
  var HEIGHT = 56;
@@ -2802,7 +2874,7 @@ function MomentumWave({
2802
2874
  }) {
2803
2875
  if (points.length === 0) return null;
2804
2876
  const { area, line, lastLeftPct, lastTopPx } = buildPaths(points);
2805
- const clipId = (0, import_react19.useId)();
2877
+ const clipId = (0, import_react20.useId)();
2806
2878
  const topClipId = `${clipId}-top`;
2807
2879
  const bottomClipId = `${clipId}-bottom`;
2808
2880
  const topClass = clubIsHome ? "ss-w-fandash-live-mom-home" : "ss-w-fandash-live-mom-away";
@@ -2993,16 +3065,16 @@ function turnAnchor(messages) {
2993
3065
  }
2994
3066
  function MessageList({ controller, ui, slip, onDashboardTap, monitor }) {
2995
3067
  const { state, send } = controller;
2996
- const listRef = (0, import_react20.useRef)(null);
2997
- const endRef = (0, import_react20.useRef)(null);
2998
- const pinnedRef = (0, import_react20.useRef)(true);
2999
- const anchoredTurnRef = (0, import_react20.useRef)(null);
3000
- (0, import_react20.useEffect)(() => {
3068
+ const listRef = (0, import_react21.useRef)(null);
3069
+ const endRef = (0, import_react21.useRef)(null);
3070
+ const pinnedRef = (0, import_react21.useRef)(true);
3071
+ const anchoredTurnRef = (0, import_react21.useRef)(null);
3072
+ (0, import_react21.useEffect)(() => {
3001
3073
  const anchor = turnAnchor(state.messages);
3002
3074
  const list = listRef.current;
3003
3075
  if (anchor && anchor.turnId !== anchoredTurnRef.current) {
3004
3076
  anchoredTurnRef.current = anchor.turnId;
3005
- if (endRef.current && list) endRef.current.style.height = `${list.clientHeight}px`;
3077
+ if (endRef.current && list) endRef.current.style.height = state.isStreaming ? `${list.clientHeight}px` : "";
3006
3078
  pinnedRef.current = false;
3007
3079
  list?.querySelector(`[data-msg-id="${anchor.anchorId}"]`)?.scrollIntoView({ block: "start" });
3008
3080
  return;
@@ -3053,10 +3125,10 @@ function MessageList({ controller, ui, slip, onDashboardTap, monitor }) {
3053
3125
  }
3054
3126
 
3055
3127
  // src/components/ChatInput.tsx
3056
- var import_react21 = require("react");
3128
+ var import_react22 = require("react");
3057
3129
  var import_jsx_runtime44 = require("react/jsx-runtime");
3058
3130
  function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop response", sendLabel = "Send message" }) {
3059
- const [value, setValue] = (0, import_react21.useState)("");
3131
+ const [value, setValue] = (0, import_react22.useState)("");
3060
3132
  const submit = () => {
3061
3133
  const t = value.trim();
3062
3134
  if (!t) return;
@@ -3363,8 +3435,8 @@ function OddsPanel({ monitor, ui, slip }) {
3363
3435
  var import_jsx_runtime49 = require("react/jsx-runtime");
3364
3436
  var FULLSCREEN_QUERY = "(max-width: 639px)";
3365
3437
  function useIsModal() {
3366
- const [modal, setModal] = (0, import_react22.useState)(false);
3367
- (0, import_react22.useEffect)(() => {
3438
+ const [modal, setModal] = (0, import_react23.useState)(false);
3439
+ (0, import_react23.useEffect)(() => {
3368
3440
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
3369
3441
  const mql = window.matchMedia(FULLSCREEN_QUERY);
3370
3442
  const sync = () => setModal(mql.matches);
@@ -3377,7 +3449,7 @@ function useIsModal() {
3377
3449
  function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
3378
3450
  const { state, close, stop, send, reset } = controller;
3379
3451
  const isModal = useIsModal();
3380
- (0, import_react22.useEffect)(() => {
3452
+ (0, import_react23.useEffect)(() => {
3381
3453
  const onKey = (e) => {
3382
3454
  if (e.key === "Escape") close();
3383
3455
  };
@@ -3388,7 +3460,7 @@ function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
3388
3460
  monitor?.setActiveTab("chat");
3389
3461
  send(text);
3390
3462
  };
3391
- (0, import_react22.useEffect)(() => {
3463
+ (0, import_react23.useEffect)(() => {
3392
3464
  if (monitor && monitor.activeTab !== "chat" && state.isStreaming) monitor.markChatActivity();
3393
3465
  }, [monitor, state.isStreaming, state.messages.length]);
3394
3466
  return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
@@ -3401,7 +3473,8 @@ function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
3401
3473
  headerTitle: ui.headerTitle,
3402
3474
  onNewConversation: reset,
3403
3475
  canReset: state.messages.length > 0,
3404
- newConversationLabel: ui.newConversation
3476
+ newConversationLabel: ui.newConversation,
3477
+ moreOptionsLabel: ui.moreOptions
3405
3478
  }
3406
3479
  ),
3407
3480
  monitor && /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
@@ -3519,14 +3592,19 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
3519
3592
  .ss-w-header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--ss-w-line); }
3520
3593
  .ss-w-header-mark { width: 18px; height: 18px; display: block; object-fit: contain; }
3521
3594
  .ss-w-header-title { font-weight: 660; flex: 1; }
3522
- .ss-w-newconv { background: none; border: none; color: var(--ss-w-faint); cursor: pointer; font-size: 16px; display: grid; place-items: center; }
3523
3595
  .ss-w-close { background: none; border: none; color: var(--ss-w-faint); cursor: pointer; font-size: 16px; display: grid; place-items: center; }
3596
+ .ss-w-menu-wrap { position: relative; display: grid; place-items: center; }
3597
+ .ss-w-kebab { background: none; border: none; color: var(--ss-w-faint); cursor: pointer; font-size: 16px; display: grid; place-items: center; }
3598
+ .ss-w-menu { position: absolute; top: calc(100% + 6px); right: 0; z-index: 5; min-width: 184px; padding: 6px; background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,.18); }
3599
+ .ss-w-menu-item { display: flex; align-items: center; gap: 10px; width: 100%; padding: 9px 10px; background: none; border: none; border-radius: 8px; color: var(--ss-w-ink); font: inherit; font-size: 13px; text-align: left; cursor: pointer; }
3600
+ .ss-w-menu-item:hover, .ss-w-menu-item:focus-visible { background: var(--ss-w-bubble); }
3601
+ .ss-w-menu-item svg { color: var(--ss-w-faint); font-size: 15px; flex: none; }
3524
3602
 
3525
3603
  /* messages */
3526
3604
  /* .ss-w-mon-list (Matches/Odds tab scrollers) shares .ss-w-list's box/scroll rules by design --
3527
3605
  same visual rhythm as the chat message list -- but gets its own class so it isn't the same DOM
3528
3606
  landmark as the chat list when both exist in the panel at once (Task 16 fix round, M4). */
3529
- .ss-w-list, .ss-w-mon-list { flex: 1; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 12px; scroll-padding-top: 14px; }
3607
+ .ss-w-list, .ss-w-mon-list { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 14px; display: flex; flex-direction: column; gap: 12px; scroll-padding-top: 14px; }
3530
3608
  /* Transient bottom spacer: reserves a viewport of room below a new turn so its
3531
3609
  top can be scrolled into view while the reply is still streaming in. Height is
3532
3610
  set imperatively and flex must not shrink it away. */
@@ -4260,11 +4338,11 @@ function injectStyles(root) {
4260
4338
  function StatsWidget(props) {
4261
4339
  const injectedClient = "client" in props ? props.client : void 0;
4262
4340
  const cfg = props.config;
4263
- const built = (0, import_react23.useMemo)(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
4264
- const ui = (0, import_react23.useMemo)(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
4265
- const rootRef = (0, import_react23.useRef)(null);
4266
- const introDoneRef = (0, import_react23.useRef)(false);
4267
- const markIntroDone = (0, import_react23.useCallback)(() => {
4341
+ const built = (0, import_react24.useMemo)(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
4342
+ const ui = (0, import_react24.useMemo)(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
4343
+ const rootRef = (0, import_react24.useRef)(null);
4344
+ const introDoneRef = (0, import_react24.useRef)(false);
4345
+ const markIntroDone = (0, import_react24.useCallback)(() => {
4268
4346
  introDoneRef.current = true;
4269
4347
  }, []);
4270
4348
  const convStorageKey = `ss-w-conv:${cfg?.operatorId ?? ""}:${cfg?.userContext?.operatorUserId ?? "anon"}`;
@@ -4275,7 +4353,7 @@ function StatsWidget(props) {
4275
4353
  storageKey: `ss-w-pins:${cfg?.operatorId ?? ""}:${cfg?.userContext?.operatorUserId ?? "anon"}`,
4276
4354
  isOpen: controller.state.isOpen
4277
4355
  });
4278
- const slip = (0, import_react23.useMemo)(
4356
+ const slip = (0, import_react24.useMemo)(
4279
4357
  () => {
4280
4358
  const handler = props.config?.onAddToSlip;
4281
4359
  return {
@@ -4298,11 +4376,11 @@ function StatsWidget(props) {
4298
4376
  },
4299
4377
  [props.config?.onAddToSlip, controller.state.conversationId, built, monitor.markInSlip]
4300
4378
  );
4301
- const onDashboardTap = (0, import_react23.useMemo)(
4379
+ const onDashboardTap = (0, import_react24.useMemo)(
4302
4380
  () => built ? createDashboardTapSignal((input) => built.conversation.sendSignal(input)) : void 0,
4303
4381
  [built]
4304
4382
  );
4305
- (0, import_react23.useEffect)(() => {
4383
+ (0, import_react24.useEffect)(() => {
4306
4384
  if (!built || !ui.injectStyles) return;
4307
4385
  const node = rootRef.current?.getRootNode();
4308
4386
  if (node) injectStyles(node);