@sensiblestats/widget-react 0.34.0 → 0.36.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.
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_react22 = require("react");
39
+ var import_react23 = require("react");
40
40
  var import_widget_sdk5 = require("@sensiblestats/widget-sdk");
41
41
 
42
42
  // src/kickoff.ts
@@ -207,7 +207,7 @@ function normalizeUi(cfg) {
207
207
  cta: cfg.cta,
208
208
  surface: cfg.surface,
209
209
  ink: cfg.ink,
210
- attribution: cfg.attribution ?? true,
210
+ pinIcon: cfg.pinIcon ?? "pin",
211
211
  position: cfg.position ?? "right",
212
212
  offset: { x: cfg.offset?.x ?? 20, y: cfg.offset?.y ?? 20 },
213
213
  radius: cfg.radius ?? 16,
@@ -1195,6 +1195,9 @@ function useLiveMonitor(opts) {
1195
1195
  };
1196
1196
  }
1197
1197
 
1198
+ // src/components/ChatFab.tsx
1199
+ var import_react5 = require("react");
1200
+
1198
1201
  // src/components/BrandMark.tsx
1199
1202
  var import_react4 = require("react");
1200
1203
 
@@ -1247,23 +1250,39 @@ function BrandMark({ logoUrl, className }) {
1247
1250
  var import_jsx_runtime3 = require("react/jsx-runtime");
1248
1251
  function GreetingBubble({ text, onOpen, onDismiss, dismissLabel = "Dismiss" }) {
1249
1252
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "ss-w-greeting", role: "note", children: [
1250
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "ss-w-greeting-body", onClick: onOpen, children: text }),
1253
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { type: "button", className: "ss-w-greeting-body", onClick: onOpen, children: [
1254
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "ss-w-greeting-dots", "aria-hidden": "true", children: [
1255
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("i", {}),
1256
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("i", {}),
1257
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("i", {})
1258
+ ] }),
1259
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "ss-w-greeting-text", children: text })
1260
+ ] }),
1251
1261
  onDismiss ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "ss-w-greeting-x", "aria-label": dismissLabel, onClick: onDismiss, children: "\xD7" }) : null
1252
1262
  ] });
1253
1263
  }
1254
1264
 
1255
1265
  // src/components/ChatFab.tsx
1256
1266
  var import_jsx_runtime4 = require("react/jsx-runtime");
1257
- function ChatFab({ label, greeting, popupText, logoUrl, onOpen, onDismissPopup, dismissLabel = "Dismiss" }) {
1267
+ function ChatFab({ label, greeting, popupText, logoUrl, onOpen, onDismissPopup, dismissLabel = "Dismiss", introAllowed = false, onIntroStart }) {
1258
1268
  const bubble = popupText ?? greeting;
1259
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "ss-w-fab-wrap", children: [
1269
+ const [intro, setIntro] = (0, import_react5.useState)(false);
1270
+ const armedRef = (0, import_react5.useRef)(introAllowed);
1271
+ (0, import_react5.useEffect)(() => {
1272
+ if (armedRef.current && bubble) {
1273
+ armedRef.current = false;
1274
+ setIntro(true);
1275
+ onIntroStart?.();
1276
+ }
1277
+ }, [bubble, onIntroStart]);
1278
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: intro ? "ss-w-fab-wrap ss-w-intro" : "ss-w-fab-wrap", children: [
1260
1279
  bubble ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(GreetingBubble, { text: bubble, onOpen, onDismiss: popupText ? onDismissPopup : void 0, dismissLabel }) : null,
1261
1280
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: "ss-w-fab", "aria-label": label, onClick: onOpen, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BrandMark, { logoUrl, className: "ss-w-fab-mark" }) })
1262
1281
  ] });
1263
1282
  }
1264
1283
 
1265
1284
  // src/components/ChatPanel.tsx
1266
- var import_react21 = require("react");
1285
+ var import_react22 = require("react");
1267
1286
 
1268
1287
  // src/components/ChatHeader.tsx
1269
1288
  var import_jsx_runtime5 = require("react/jsx-runtime");
@@ -1276,7 +1295,7 @@ function ChatHeader({ onClose, closeLabel = "Close chat", logoUrl, headerTitle }
1276
1295
  }
1277
1296
 
1278
1297
  // src/components/MessageList.tsx
1279
- var import_react19 = require("react");
1298
+ var import_react20 = require("react");
1280
1299
 
1281
1300
  // src/components/Markdown.tsx
1282
1301
  var import_react_markdown = __toESM(require("react-markdown"), 1);
@@ -1294,7 +1313,7 @@ function Markdown({ text }) {
1294
1313
  }
1295
1314
 
1296
1315
  // src/components/EntityCardList.tsx
1297
- var import_react5 = require("react");
1316
+ var import_react6 = require("react");
1298
1317
  var import_jsx_runtime7 = require("react/jsx-runtime");
1299
1318
  var MAX_VISIBLE = 3;
1300
1319
  var RECORD_RE = /^(\d+)-(\d+)-(\d+)$/;
@@ -1378,7 +1397,7 @@ function ScopeTabs({ scopes, active, onSelect }) {
1378
1397
  )) });
1379
1398
  }
1380
1399
  function EntityCardItem({ c }) {
1381
- const [active, setActive] = (0, import_react5.useState)(0);
1400
+ const [active, setActive] = (0, import_react6.useState)(0);
1382
1401
  const hasBody = c.scopes.length > 0;
1383
1402
  if (!hasBody) {
1384
1403
  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 }) });
@@ -1392,7 +1411,7 @@ function EntityCardItem({ c }) {
1392
1411
  ] });
1393
1412
  }
1394
1413
  function EntityCardList({ cards, disabled, showLessLabel = "Show less", showMoreLabel = (n) => `Show ${n} more` }) {
1395
- const [expanded, setExpanded] = (0, import_react5.useState)(false);
1414
+ const [expanded, setExpanded] = (0, import_react6.useState)(false);
1396
1415
  if (cards.length === 0) return null;
1397
1416
  const visible = expanded ? cards : cards.slice(0, MAX_VISIBLE);
1398
1417
  const overflow = cards.length - MAX_VISIBLE;
@@ -1403,18 +1422,18 @@ function EntityCardList({ cards, disabled, showLessLabel = "Show less", showMore
1403
1422
  }
1404
1423
 
1405
1424
  // src/components/BettingInsightList.tsx
1406
- var import_react10 = require("react");
1425
+ var import_react11 = require("react");
1407
1426
 
1408
1427
  // src/components/AddToSlipButton.tsx
1409
- var import_react7 = require("react");
1428
+ var import_react8 = require("react");
1410
1429
  var import_widget_sdk4 = require("@sensiblestats/widget-sdk");
1411
1430
 
1412
1431
  // src/components/Toast.tsx
1413
- var import_react6 = require("react");
1432
+ var import_react7 = require("react");
1414
1433
  var import_jsx_runtime8 = require("react/jsx-runtime");
1415
1434
  var DISMISS_MS = 4e3;
1416
1435
  function Toast({ toast, onDismiss }) {
1417
- (0, import_react6.useEffect)(() => {
1436
+ (0, import_react7.useEffect)(() => {
1418
1437
  if (!toast) return;
1419
1438
  const timer = setTimeout(onDismiss, DISMISS_MS);
1420
1439
  return () => clearTimeout(timer);
@@ -1432,10 +1451,10 @@ function withTimeout(value) {
1432
1451
  ]);
1433
1452
  }
1434
1453
  function SlipButton({ selection, shownOdds, marketName, slip, ui }) {
1435
- const [pending, setPending] = (0, import_react7.useState)(false);
1436
- const [settled, setSettled] = (0, import_react7.useState)(false);
1437
- const [toast, setToast] = (0, import_react7.useState)(null);
1438
- const dismiss = (0, import_react7.useCallback)(() => setToast(null), []);
1454
+ const [pending, setPending] = (0, import_react8.useState)(false);
1455
+ const [settled, setSettled] = (0, import_react8.useState)(false);
1456
+ const [toast, setToast] = (0, import_react8.useState)(null);
1457
+ const dismiss = (0, import_react8.useCallback)(() => setToast(null), []);
1439
1458
  const handler = slip.onAddToSlip;
1440
1459
  if (!ui || !handler || !selection) return null;
1441
1460
  const onClick = async () => {
@@ -1499,14 +1518,14 @@ function OddsMovementBadge({
1499
1518
  }
1500
1519
 
1501
1520
  // src/components/Crest.tsx
1502
- var import_react8 = require("react");
1521
+ var import_react9 = require("react");
1503
1522
  var import_jsx_runtime11 = require("react/jsx-runtime");
1504
1523
  function initials2(name) {
1505
1524
  return (name ?? "").trim().slice(0, 1).toUpperCase() || "?";
1506
1525
  }
1507
1526
  function Crest2({ url, name, kind }) {
1508
1527
  const cls = kind === "comp" ? "ss-w-bi-comp-logo" : "ss-w-bi-crest";
1509
- const [failed, setFailed] = (0, import_react8.useState)(false);
1528
+ const [failed, setFailed] = (0, import_react9.useState)(false);
1510
1529
  if (url && !failed) {
1511
1530
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("img", { className: cls, src: url, alt: "", "aria-hidden": "true", onError: () => setFailed(true) });
1512
1531
  }
@@ -1514,11 +1533,16 @@ function Crest2({ url, name, kind }) {
1514
1533
  }
1515
1534
 
1516
1535
  // src/components/PinToggle.tsx
1517
- var import_react9 = require("react");
1536
+ var import_react10 = require("react");
1518
1537
  var import_jsx_runtime12 = require("react/jsx-runtime");
1538
+ var PIN_GLYPHS = {
1539
+ 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" },
1540
+ star: { viewBox: "0 0 24 24", strokeWidth: 2, d: "M12 2.5l2.9 5.9 6.5.95-4.7 4.58 1.1 6.47L12 17.35 6.1 20.4l1.1-6.47L2.5 9.35l6.5-.95z" },
1541
+ 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" }
1542
+ };
1519
1543
  function PinToggle({ pin, monitor, ui }) {
1520
- const [toast, setToast] = (0, import_react9.useState)(null);
1521
- const dismiss = (0, import_react9.useCallback)(() => setToast(null), []);
1544
+ const [toast, setToast] = (0, import_react10.useState)(null);
1545
+ const dismiss = (0, import_react10.useCallback)(() => setToast(null), []);
1522
1546
  if (!monitor || !monitor.enabled) return null;
1523
1547
  const key = pinKey(pin);
1524
1548
  const pinned = monitor.isPinned(key);
@@ -1531,6 +1555,7 @@ function PinToggle({ pin, monitor, ui }) {
1531
1555
  setToast({ kind: "error", text: ui.marketLimitToast });
1532
1556
  }
1533
1557
  };
1558
+ const glyph = PIN_GLYPHS[ui.pinIcon];
1534
1559
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "ss-w-pin-wrap", children: [
1535
1560
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1536
1561
  "button",
@@ -1538,10 +1563,22 @@ function PinToggle({ pin, monitor, ui }) {
1538
1563
  type: "button",
1539
1564
  className: "ss-w-pin",
1540
1565
  "data-on": pinned || void 0,
1566
+ "data-icon": ui.pinIcon,
1541
1567
  "aria-pressed": pinned,
1542
1568
  "aria-label": pinned ? labels.unpin : labels.pin,
1543
1569
  onClick,
1544
- children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("svg", { viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("path", { fill: "currentColor", 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" }) })
1570
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("svg", { viewBox: glyph.viewBox, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1571
+ "path",
1572
+ {
1573
+ transform: glyph.transform,
1574
+ d: glyph.d,
1575
+ fill: pinned ? "currentColor" : "none",
1576
+ stroke: pinned ? "none" : "currentColor",
1577
+ strokeWidth: glyph.strokeWidth,
1578
+ strokeLinejoin: "round",
1579
+ strokeLinecap: "round"
1580
+ }
1581
+ ) })
1545
1582
  }
1546
1583
  ),
1547
1584
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Toast, { toast, onDismiss: dismiss })
@@ -1557,7 +1594,7 @@ function StatRows({ stats }) {
1557
1594
  ] }, `${s.label}:${s.value}`)) });
1558
1595
  }
1559
1596
  function ScopedStats({ scopes }) {
1560
- const [active, setActive] = (0, import_react10.useState)(0);
1597
+ const [active, setActive] = (0, import_react11.useState)(0);
1561
1598
  const current = scopes[active] ?? scopes[0] ?? { key: "", label: "", stats: [] };
1562
1599
  return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "ss-w-bi-scoped", children: [
1563
1600
  /* @__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)) }),
@@ -1568,7 +1605,7 @@ function hasStatsContent(it) {
1568
1605
  return !!it.scopes && it.scopes.length >= 2 || it.stats.length > 0;
1569
1606
  }
1570
1607
  function ReasonStrip({ it, ui }) {
1571
- const [open, setOpen] = (0, import_react10.useState)(false);
1608
+ const [open, setOpen] = (0, import_react11.useState)(false);
1572
1609
  const hasScopes = !!it.scopes && it.scopes.length >= 2;
1573
1610
  const hasStats = hasStatsContent(it);
1574
1611
  const hasText = !!it.text;
@@ -1678,7 +1715,7 @@ function BettingInsightList({ insights, ui, slip, monitor }) {
1678
1715
  }
1679
1716
 
1680
1717
  // src/components/MarketOddsCardList.tsx
1681
- var import_react11 = require("react");
1718
+ var import_react12 = require("react");
1682
1719
  var import_jsx_runtime14 = require("react/jsx-runtime");
1683
1720
  function OddsRow({ vm, row, ui, slip, inSlipIds }) {
1684
1721
  const selection = vm.addToSlipEnabled ? marketOddsRowToSlipSelection(vm, row, slip.conversationId) : null;
@@ -1695,7 +1732,7 @@ function OddsRow({ vm, row, ui, slip, inSlipIds }) {
1695
1732
  ] });
1696
1733
  }
1697
1734
  function MarketOddsCard({ vm, ui, slip, monitor, inSlipIds }) {
1698
- const [open, setOpen] = (0, import_react11.useState)(false);
1735
+ const [open, setOpen] = (0, import_react12.useState)(false);
1699
1736
  const kickoff = vm.kickoffUtc && ui?.bettingKickoff ? ui.bettingKickoff(vm.kickoffUtc) : void 0;
1700
1737
  const hasNamedTeams = !!vm.homeTeamName && !!vm.awayTeamName;
1701
1738
  const showPin = !!monitor && vm.pinEnabled;
@@ -1988,7 +2025,7 @@ function FixtureCard({ card }) {
1988
2025
  }
1989
2026
 
1990
2027
  // src/components/dashboard/CountdownCard.tsx
1991
- var import_react12 = require("react");
2028
+ var import_react13 = require("react");
1992
2029
  var import_jsx_runtime20 = require("react/jsx-runtime");
1993
2030
  var DAY_MS = 864e5;
1994
2031
  function remainingSegments(kickoffUnix, nowMs, withSeconds) {
@@ -2007,12 +2044,12 @@ function remainingSegments(kickoffUnix, nowMs, withSeconds) {
2007
2044
  function CountdownCard({ card }) {
2008
2045
  const { data } = card;
2009
2046
  const unitLabel = { d: data.daysUnit, h: data.hoursUnit, m: data.minutesUnit, s: data.secondsUnit };
2010
- const [nowMs, setNowMs] = (0, import_react12.useState)(() => Date.now());
2011
- const [reduced] = (0, import_react12.useState)(
2047
+ const [nowMs, setNowMs] = (0, import_react13.useState)(() => Date.now());
2048
+ const [reduced] = (0, import_react13.useState)(
2012
2049
  () => typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches
2013
2050
  );
2014
2051
  const withinDay = data.kickoffUnix * 1e3 - nowMs < DAY_MS;
2015
- (0, import_react12.useEffect)(() => {
2052
+ (0, import_react13.useEffect)(() => {
2016
2053
  if (reduced) return;
2017
2054
  const id = window.setInterval(() => setNowMs(Date.now()), withinDay ? 1e3 : 6e4);
2018
2055
  return () => window.clearInterval(id);
@@ -2020,7 +2057,7 @@ function CountdownCard({ card }) {
2020
2057
  const segments = remainingSegments(data.kickoffUnix, nowMs, withinDay && !reduced);
2021
2058
  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: [
2022
2059
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "ss-w-fandash-eyebrow", children: card.title ?? "Kick-off in" }),
2023
- 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_react12.Fragment, { children: [
2060
+ 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: [
2024
2061
  i > 0 ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "ss-w-fandash-countdown-colon", children: ":" }) : null,
2025
2062
  /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("span", { className: "ss-w-fandash-countdown-seg", children: [
2026
2063
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "ss-w-fandash-countdown-value", children: s.value }),
@@ -2031,7 +2068,7 @@ function CountdownCard({ card }) {
2031
2068
  }
2032
2069
 
2033
2070
  // src/components/dashboard/InjuriesCard.tsx
2034
- var import_react13 = require("react");
2071
+ var import_react14 = require("react");
2035
2072
  var import_jsx_runtime21 = require("react/jsx-runtime");
2036
2073
  var SHOWN = 3;
2037
2074
  function ColHead({ name, crestUrl }) {
@@ -2054,7 +2091,7 @@ function InjuryList({ rows, emptyLabel, limit }) {
2054
2091
  }
2055
2092
  function InjuriesCard({ card, ui }) {
2056
2093
  const { data } = card;
2057
- const [expanded, setExpanded] = (0, import_react13.useState)(false);
2094
+ const [expanded, setExpanded] = (0, import_react14.useState)(false);
2058
2095
  const hidden = Math.max(0, data.ours.length - SHOWN) + Math.max(0, data.theirs.length - SHOWN);
2059
2096
  const limit = expanded ? void 0 : SHOWN;
2060
2097
  const showMore = ui?.showMore ?? ((n) => `Show ${n} more`);
@@ -2158,7 +2195,7 @@ function LinksCard({ card }) {
2158
2195
  }
2159
2196
 
2160
2197
  // src/components/dashboard/PostMatchResultCard.tsx
2161
- var import_react14 = require("react");
2198
+ var import_react15 = require("react");
2162
2199
  var import_jsx_runtime27 = require("react/jsx-runtime");
2163
2200
  function initials3(name) {
2164
2201
  return name.trim().slice(0, 3).toUpperCase();
@@ -2168,7 +2205,7 @@ function PostMatchCrest({
2168
2205
  name,
2169
2206
  side
2170
2207
  }) {
2171
- const [failed, setFailed] = (0, import_react14.useState)(false);
2208
+ const [failed, setFailed] = (0, import_react15.useState)(false);
2172
2209
  const cls = `ss-w-fandash-postmatch-crest ss-w-fandash-postmatch-crest--${side}`;
2173
2210
  if (url && !failed) {
2174
2211
  return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("img", { className: cls, src: url, alt: "", "aria-hidden": "true", onError: () => setFailed(true) });
@@ -2367,10 +2404,10 @@ function RestingFormCard({ card }) {
2367
2404
  }
2368
2405
 
2369
2406
  // src/components/dashboard/LiveScoreCard.tsx
2370
- var import_react16 = require("react");
2407
+ var import_react17 = require("react");
2371
2408
 
2372
2409
  // src/useLiveClock.ts
2373
- var import_react15 = require("react");
2410
+ var import_react16 = require("react");
2374
2411
  var TICK_MS = 6e4;
2375
2412
  var MAX_LIVE_TICK_MINUTES = 15;
2376
2413
  function computeDisplayMinute(minute, clockRunning, receivedAtMs) {
@@ -2379,10 +2416,10 @@ function computeDisplayMinute(minute, clockRunning, receivedAtMs) {
2379
2416
  return minute + Math.min(Math.max(0, elapsedMinutes), MAX_LIVE_TICK_MINUTES);
2380
2417
  }
2381
2418
  function useLiveClock(minute, clockRunning, receivedAtMs) {
2382
- const [displayMinute, setDisplayMinute] = (0, import_react15.useState)(
2419
+ const [displayMinute, setDisplayMinute] = (0, import_react16.useState)(
2383
2420
  () => computeDisplayMinute(minute, clockRunning, receivedAtMs)
2384
2421
  );
2385
- (0, import_react15.useEffect)(() => {
2422
+ (0, import_react16.useEffect)(() => {
2386
2423
  setDisplayMinute(computeDisplayMinute(minute, clockRunning, receivedAtMs));
2387
2424
  if (!clockRunning) return;
2388
2425
  const id = window.setInterval(() => {
@@ -2399,7 +2436,7 @@ function initials5(name) {
2399
2436
  return name.trim().slice(0, 3).toUpperCase();
2400
2437
  }
2401
2438
  function LiveCrest({ url, name, side }) {
2402
- const [failed, setFailed] = (0, import_react16.useState)(false);
2439
+ const [failed, setFailed] = (0, import_react17.useState)(false);
2403
2440
  const cls = `ss-w-fandash-live-crest ss-w-fandash-live-crest--${side}`;
2404
2441
  if (url && !failed) {
2405
2442
  return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("img", { className: cls, src: url, alt: "", "aria-hidden": "true", onError: () => setFailed(true) });
@@ -2418,7 +2455,7 @@ function LiveScoreCard({
2418
2455
  }) {
2419
2456
  const { data } = card;
2420
2457
  const seedKey = `${data.minute}:${data.homeScore}:${data.awayScore}`;
2421
- const seedRef = (0, import_react16.useRef)(null);
2458
+ const seedRef = (0, import_react17.useRef)(null);
2422
2459
  if (!seedRef.current || seedRef.current.key !== seedKey) {
2423
2460
  seedRef.current = { key: seedKey, receivedAtMs: Date.now() };
2424
2461
  }
@@ -2458,7 +2495,7 @@ function LiveScoreCard({
2458
2495
  }
2459
2496
 
2460
2497
  // src/components/dashboard/LiveTimelineCard.tsx
2461
- var import_react17 = require("react");
2498
+ var import_react18 = require("react");
2462
2499
  var import_jsx_runtime37 = require("react/jsx-runtime");
2463
2500
  var SHOWN2 = 3;
2464
2501
  function EventIcon({ type }) {
@@ -2500,7 +2537,7 @@ function EventRow({ e }) {
2500
2537
  ] });
2501
2538
  }
2502
2539
  function LiveTimelineCard({ card, ui }) {
2503
- const [expanded, setExpanded] = (0, import_react17.useState)(false);
2540
+ const [expanded, setExpanded] = (0, import_react18.useState)(false);
2504
2541
  const events = card.data.events;
2505
2542
  if (!events || events.length === 0) return null;
2506
2543
  const sorted = [...events].sort((a, b) => b.minute - a.minute);
@@ -2525,7 +2562,7 @@ function LiveTimelineCard({ card, ui }) {
2525
2562
  }
2526
2563
 
2527
2564
  // src/components/dashboard/MomentumWave.tsx
2528
- var import_react18 = require("react");
2565
+ var import_react19 = require("react");
2529
2566
  var import_jsx_runtime38 = require("react/jsx-runtime");
2530
2567
  var WIDTH = 270;
2531
2568
  var HEIGHT = 56;
@@ -2549,7 +2586,7 @@ function MomentumWave({
2549
2586
  }) {
2550
2587
  if (points.length === 0) return null;
2551
2588
  const { area, line, lastLeftPct, lastTopPx } = buildPaths(points);
2552
- const clipId = (0, import_react18.useId)();
2589
+ const clipId = (0, import_react19.useId)();
2553
2590
  const topClipId = `${clipId}-top`;
2554
2591
  const bottomClipId = `${clipId}-bottom`;
2555
2592
  const topClass = clubIsHome ? "ss-w-fandash-live-mom-home" : "ss-w-fandash-live-mom-away";
@@ -2740,11 +2777,11 @@ function turnAnchor(messages) {
2740
2777
  }
2741
2778
  function MessageList({ controller, ui, slip, onDashboardTap, monitor }) {
2742
2779
  const { state, send } = controller;
2743
- const listRef = (0, import_react19.useRef)(null);
2744
- const endRef = (0, import_react19.useRef)(null);
2745
- const pinnedRef = (0, import_react19.useRef)(true);
2746
- const anchoredTurnRef = (0, import_react19.useRef)(null);
2747
- (0, import_react19.useEffect)(() => {
2780
+ const listRef = (0, import_react20.useRef)(null);
2781
+ const endRef = (0, import_react20.useRef)(null);
2782
+ const pinnedRef = (0, import_react20.useRef)(true);
2783
+ const anchoredTurnRef = (0, import_react20.useRef)(null);
2784
+ (0, import_react20.useEffect)(() => {
2748
2785
  const anchor = turnAnchor(state.messages);
2749
2786
  const list = listRef.current;
2750
2787
  if (anchor && anchor.turnId !== anchoredTurnRef.current) {
@@ -2800,10 +2837,10 @@ function MessageList({ controller, ui, slip, onDashboardTap, monitor }) {
2800
2837
  }
2801
2838
 
2802
2839
  // src/components/ChatInput.tsx
2803
- var import_react20 = require("react");
2840
+ var import_react21 = require("react");
2804
2841
  var import_jsx_runtime44 = require("react/jsx-runtime");
2805
2842
  function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop response", sendLabel = "Send message" }) {
2806
- const [value, setValue] = (0, import_react20.useState)("");
2843
+ const [value, setValue] = (0, import_react21.useState)("");
2807
2844
  const submit = () => {
2808
2845
  const t = value.trim();
2809
2846
  if (!t) return;
@@ -3050,8 +3087,8 @@ function OddsPanel({ monitor, ui, slip }) {
3050
3087
  var import_jsx_runtime48 = require("react/jsx-runtime");
3051
3088
  var FULLSCREEN_QUERY = "(max-width: 639px)";
3052
3089
  function useIsModal() {
3053
- const [modal, setModal] = (0, import_react21.useState)(false);
3054
- (0, import_react21.useEffect)(() => {
3090
+ const [modal, setModal] = (0, import_react22.useState)(false);
3091
+ (0, import_react22.useEffect)(() => {
3055
3092
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
3056
3093
  const mql = window.matchMedia(FULLSCREEN_QUERY);
3057
3094
  const sync = () => setModal(mql.matches);
@@ -3064,7 +3101,7 @@ function useIsModal() {
3064
3101
  function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
3065
3102
  const { state, close, stop, send } = controller;
3066
3103
  const isModal = useIsModal();
3067
- (0, import_react21.useEffect)(() => {
3104
+ (0, import_react22.useEffect)(() => {
3068
3105
  const onKey = (e) => {
3069
3106
  if (e.key === "Escape") close();
3070
3107
  };
@@ -3075,7 +3112,7 @@ function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
3075
3112
  monitor?.setActiveTab("chat");
3076
3113
  send(text);
3077
3114
  };
3078
- (0, import_react21.useEffect)(() => {
3115
+ (0, import_react22.useEffect)(() => {
3079
3116
  if (monitor && monitor.activeTab !== "chat" && state.isStreaming) monitor.markChatActivity();
3080
3117
  }, [monitor, state.isStreaming, state.messages.length]);
3081
3118
  return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
@@ -3098,8 +3135,7 @@ function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
3098
3135
  ),
3099
3136
  monitor && monitor.activeTab === "matches" && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(MatchesPanel, { monitor, ui, slip }),
3100
3137
  monitor && monitor.activeTab === "odds" && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(OddsPanel, { monitor, ui, slip }),
3101
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: handleSend, onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage }),
3102
- ui.attribution !== false ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "ss-w-attribution", children: "Powered by SensibleStats" }) : null
3138
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: handleSend, onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
3103
3139
  ] });
3104
3140
  }
3105
3141
 
@@ -3147,8 +3183,40 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
3147
3183
  .ss-w-fab { width: 56px; height: 56px; border: none; border-radius: 50%; background: var(--ss-w-nav); color: #fff; cursor: pointer; display: grid; place-items: center; box-shadow: 0 6px 20px rgba(0,0,0,.28); }
3148
3184
  .ss-w-fab-mark { width: 30px; height: 30px; display: block; object-fit: contain; }
3149
3185
  .ss-w-greeting { max-width: 240px; display: flex; align-items: flex-start; background: var(--ss-w-panel); color: var(--ss-w-ink); border: 1px solid var(--ss-w-line); border-radius: 12px; box-shadow: 0 4px 14px rgba(0,0,0,.14); }
3150
- .ss-w-greeting-body { flex: 1; min-width: 0; text-align: left; background: none; border: none; color: inherit; font: inherit; cursor: pointer; padding: 9px 4px 9px 12px; }
3186
+ .ss-w-greeting-body { position: relative; flex: 1; min-width: 0; text-align: left; background: none; border: none; color: inherit; font: inherit; cursor: pointer; padding: 9px 4px 9px 12px; }
3151
3187
  .ss-w-greeting-x { flex: none; background: none; border: none; color: var(--ss-w-faint); cursor: pointer; font-size: 17px; line-height: 1; padding: 7px 9px 0 2px; }
3188
+ .ss-w-greeting-text { display: block; }
3189
+ /* Typing indicator: overlays the text area so bubble height stays put; hidden at
3190
+ rest, revealed only during the one-shot intro (see prefers-reduced-motion block). */
3191
+ .ss-w-greeting-dots { position: absolute; top: 0; bottom: 0; left: 12px; display: flex; align-items: center; gap: 4px; opacity: 0; pointer-events: none; }
3192
+ .ss-w-greeting-dots i { width: 6px; height: 6px; border-radius: 50%; background: var(--ss-w-faint); display: block; }
3193
+ /* One-shot launcher attention intro: FAB nudges, then the greeting types in.
3194
+ Gated behind no-preference so reduce-motion users get the greeting shown
3195
+ statically (text visible, no nudge, no typing). ChatFab adds .ss-w-intro once
3196
+ per page load; every animation runs a single time and settles at rest. */
3197
+ @media (prefers-reduced-motion: no-preference) {
3198
+ .ss-w-intro .ss-w-fab { animation: ss-w-nudge .8s ease both; }
3199
+ .ss-w-intro .ss-w-greeting { animation: ss-w-greet-in .45s cubic-bezier(.2,.8,.3,1.15) .8s both; }
3200
+ .ss-w-intro .ss-w-greeting-text { animation: ss-w-greet-textin .4s ease 1.65s both; }
3201
+ .ss-w-intro .ss-w-greeting-dots { animation: ss-w-greet-dots .95s ease .85s both; }
3202
+ .ss-w-intro .ss-w-greeting-dots i { animation: ss-w-greet-dotbounce .5s ease .85s 2; }
3203
+ .ss-w-intro .ss-w-greeting-dots i:nth-child(2) { animation-delay: 1s; }
3204
+ .ss-w-intro .ss-w-greeting-dots i:nth-child(3) { animation-delay: 1.15s; }
3205
+ }
3206
+ @keyframes ss-w-nudge {
3207
+ 0%, 100% { transform: none; }
3208
+ 22% { transform: translateY(-9px) rotate(-9deg); }
3209
+ 44% { transform: translateY(0) rotate(7deg); }
3210
+ 64% { transform: translateY(-4px) rotate(-4deg); }
3211
+ 82% { transform: rotate(2deg); }
3212
+ }
3213
+ @keyframes ss-w-greet-in {
3214
+ from { opacity: 0; transform: translateY(8px) scale(.92); }
3215
+ to { opacity: 1; transform: none; }
3216
+ }
3217
+ @keyframes ss-w-greet-textin { from { opacity: 0; } to { opacity: 1; } }
3218
+ @keyframes ss-w-greet-dots { 0% { opacity: 0; } 14% { opacity: 1; } 78% { opacity: 1; } 100% { opacity: 0; } }
3219
+ @keyframes ss-w-greet-dotbounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
3152
3220
 
3153
3221
  /* panel */
3154
3222
  .ss-w-panel { position: fixed; bottom: calc(var(--ss-w-offset-y) + 72px); z-index: 2147483000; display: flex; flex-direction: column; width: min(460px, calc(100vw - 40px)); height: min(640px, calc(100vh - 120px)); background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); border-radius: var(--ss-w-radius); overflow: hidden; box-shadow: 0 12px 40px rgba(0,0,0,.24); }
@@ -3387,7 +3455,6 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
3387
3455
  }
3388
3456
  .ss-w-send { width: 36px; height: 36px; flex: none; border: none; border-radius: 50%; background: var(--ss-w-accent); color: #fff; cursor: pointer; display: grid; place-items: center; font-size: 16px; }
3389
3457
  .ss-w-stop-glyph { width: 11px; height: 11px; background: currentColor; border-radius: 2px; }
3390
- .ss-w-attribution { flex: 0 0 auto; padding: 6px 12px 8px; text-align: center; font-size: 11px; color: var(--ss-w-faint); }
3391
3458
 
3392
3459
  /* add to slip */
3393
3460
  .ss-w-slip { position: relative; display: flex; }
@@ -3881,16 +3948,20 @@ function injectStyles(root) {
3881
3948
  function StatsWidget(props) {
3882
3949
  const injectedClient = "client" in props ? props.client : void 0;
3883
3950
  const cfg = props.config;
3884
- const built = (0, import_react22.useMemo)(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
3885
- const ui = (0, import_react22.useMemo)(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
3886
- const rootRef = (0, import_react22.useRef)(null);
3951
+ const built = (0, import_react23.useMemo)(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
3952
+ const ui = (0, import_react23.useMemo)(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
3953
+ const rootRef = (0, import_react23.useRef)(null);
3954
+ const introDoneRef = (0, import_react23.useRef)(false);
3955
+ const markIntroDone = (0, import_react23.useCallback)(() => {
3956
+ introDoneRef.current = true;
3957
+ }, []);
3887
3958
  const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters, built ? ui.greetingMessage : void 0);
3888
3959
  const monitor = useLiveMonitor({
3889
3960
  getLiveMonitor: (pins) => built ? built.conversation.getLiveMonitor(pins) : Promise.resolve(null),
3890
3961
  storageKey: `ss-w-pins:${cfg?.operatorId ?? ""}:${cfg?.userContext?.operatorUserId ?? "anon"}`,
3891
3962
  isOpen: controller.state.isOpen
3892
3963
  });
3893
- const slip = (0, import_react22.useMemo)(
3964
+ const slip = (0, import_react23.useMemo)(
3894
3965
  () => {
3895
3966
  const handler = props.config?.onAddToSlip;
3896
3967
  return {
@@ -3913,11 +3984,11 @@ function StatsWidget(props) {
3913
3984
  },
3914
3985
  [props.config?.onAddToSlip, controller.state.conversationId, built, monitor.markInSlip]
3915
3986
  );
3916
- const onDashboardTap = (0, import_react22.useMemo)(
3987
+ const onDashboardTap = (0, import_react23.useMemo)(
3917
3988
  () => built ? createDashboardTapSignal((input) => built.conversation.sendSignal(input)) : void 0,
3918
3989
  [built]
3919
3990
  );
3920
- (0, import_react22.useEffect)(() => {
3991
+ (0, import_react23.useEffect)(() => {
3921
3992
  if (!built || !ui.injectStyles) return;
3922
3993
  const node = rootRef.current?.getRootNode();
3923
3994
  if (node) injectStyles(node);
@@ -3933,7 +4004,9 @@ function StatsWidget(props) {
3933
4004
  logoUrl: ui.logoUrl,
3934
4005
  onOpen: controller.open,
3935
4006
  onDismissPopup: controller.dismissPopup,
3936
- dismissLabel: ui.dismissGreeting
4007
+ dismissLabel: ui.dismissGreeting,
4008
+ introAllowed: !introDoneRef.current,
4009
+ onIntroStart: markIntroDone
3937
4010
  }
3938
4011
  ) });
3939
4012
  }