@sensiblestats/widget-react 0.35.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,6 +207,7 @@ function normalizeUi(cfg) {
207
207
  cta: cfg.cta,
208
208
  surface: cfg.surface,
209
209
  ink: cfg.ink,
210
+ pinIcon: cfg.pinIcon ?? "pin",
210
211
  position: cfg.position ?? "right",
211
212
  offset: { x: cfg.offset?.x ?? 20, y: cfg.offset?.y ?? 20 },
212
213
  radius: cfg.radius ?? 16,
@@ -1194,6 +1195,9 @@ function useLiveMonitor(opts) {
1194
1195
  };
1195
1196
  }
1196
1197
 
1198
+ // src/components/ChatFab.tsx
1199
+ var import_react5 = require("react");
1200
+
1197
1201
  // src/components/BrandMark.tsx
1198
1202
  var import_react4 = require("react");
1199
1203
 
@@ -1246,23 +1250,39 @@ function BrandMark({ logoUrl, className }) {
1246
1250
  var import_jsx_runtime3 = require("react/jsx-runtime");
1247
1251
  function GreetingBubble({ text, onOpen, onDismiss, dismissLabel = "Dismiss" }) {
1248
1252
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "ss-w-greeting", role: "note", children: [
1249
- /* @__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
+ ] }),
1250
1261
  onDismiss ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "ss-w-greeting-x", "aria-label": dismissLabel, onClick: onDismiss, children: "\xD7" }) : null
1251
1262
  ] });
1252
1263
  }
1253
1264
 
1254
1265
  // src/components/ChatFab.tsx
1255
1266
  var import_jsx_runtime4 = require("react/jsx-runtime");
1256
- function ChatFab({ label, greeting, popupText, logoUrl, onOpen, onDismissPopup, dismissLabel = "Dismiss" }) {
1267
+ function ChatFab({ label, greeting, popupText, logoUrl, onOpen, onDismissPopup, dismissLabel = "Dismiss", introAllowed = false, onIntroStart }) {
1257
1268
  const bubble = popupText ?? greeting;
1258
- 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: [
1259
1279
  bubble ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(GreetingBubble, { text: bubble, onOpen, onDismiss: popupText ? onDismissPopup : void 0, dismissLabel }) : null,
1260
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" }) })
1261
1281
  ] });
1262
1282
  }
1263
1283
 
1264
1284
  // src/components/ChatPanel.tsx
1265
- var import_react21 = require("react");
1285
+ var import_react22 = require("react");
1266
1286
 
1267
1287
  // src/components/ChatHeader.tsx
1268
1288
  var import_jsx_runtime5 = require("react/jsx-runtime");
@@ -1275,7 +1295,7 @@ function ChatHeader({ onClose, closeLabel = "Close chat", logoUrl, headerTitle }
1275
1295
  }
1276
1296
 
1277
1297
  // src/components/MessageList.tsx
1278
- var import_react19 = require("react");
1298
+ var import_react20 = require("react");
1279
1299
 
1280
1300
  // src/components/Markdown.tsx
1281
1301
  var import_react_markdown = __toESM(require("react-markdown"), 1);
@@ -1293,7 +1313,7 @@ function Markdown({ text }) {
1293
1313
  }
1294
1314
 
1295
1315
  // src/components/EntityCardList.tsx
1296
- var import_react5 = require("react");
1316
+ var import_react6 = require("react");
1297
1317
  var import_jsx_runtime7 = require("react/jsx-runtime");
1298
1318
  var MAX_VISIBLE = 3;
1299
1319
  var RECORD_RE = /^(\d+)-(\d+)-(\d+)$/;
@@ -1377,7 +1397,7 @@ function ScopeTabs({ scopes, active, onSelect }) {
1377
1397
  )) });
1378
1398
  }
1379
1399
  function EntityCardItem({ c }) {
1380
- const [active, setActive] = (0, import_react5.useState)(0);
1400
+ const [active, setActive] = (0, import_react6.useState)(0);
1381
1401
  const hasBody = c.scopes.length > 0;
1382
1402
  if (!hasBody) {
1383
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 }) });
@@ -1391,7 +1411,7 @@ function EntityCardItem({ c }) {
1391
1411
  ] });
1392
1412
  }
1393
1413
  function EntityCardList({ cards, disabled, showLessLabel = "Show less", showMoreLabel = (n) => `Show ${n} more` }) {
1394
- const [expanded, setExpanded] = (0, import_react5.useState)(false);
1414
+ const [expanded, setExpanded] = (0, import_react6.useState)(false);
1395
1415
  if (cards.length === 0) return null;
1396
1416
  const visible = expanded ? cards : cards.slice(0, MAX_VISIBLE);
1397
1417
  const overflow = cards.length - MAX_VISIBLE;
@@ -1402,18 +1422,18 @@ function EntityCardList({ cards, disabled, showLessLabel = "Show less", showMore
1402
1422
  }
1403
1423
 
1404
1424
  // src/components/BettingInsightList.tsx
1405
- var import_react10 = require("react");
1425
+ var import_react11 = require("react");
1406
1426
 
1407
1427
  // src/components/AddToSlipButton.tsx
1408
- var import_react7 = require("react");
1428
+ var import_react8 = require("react");
1409
1429
  var import_widget_sdk4 = require("@sensiblestats/widget-sdk");
1410
1430
 
1411
1431
  // src/components/Toast.tsx
1412
- var import_react6 = require("react");
1432
+ var import_react7 = require("react");
1413
1433
  var import_jsx_runtime8 = require("react/jsx-runtime");
1414
1434
  var DISMISS_MS = 4e3;
1415
1435
  function Toast({ toast, onDismiss }) {
1416
- (0, import_react6.useEffect)(() => {
1436
+ (0, import_react7.useEffect)(() => {
1417
1437
  if (!toast) return;
1418
1438
  const timer = setTimeout(onDismiss, DISMISS_MS);
1419
1439
  return () => clearTimeout(timer);
@@ -1431,10 +1451,10 @@ function withTimeout(value) {
1431
1451
  ]);
1432
1452
  }
1433
1453
  function SlipButton({ selection, shownOdds, marketName, slip, ui }) {
1434
- const [pending, setPending] = (0, import_react7.useState)(false);
1435
- const [settled, setSettled] = (0, import_react7.useState)(false);
1436
- const [toast, setToast] = (0, import_react7.useState)(null);
1437
- 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), []);
1438
1458
  const handler = slip.onAddToSlip;
1439
1459
  if (!ui || !handler || !selection) return null;
1440
1460
  const onClick = async () => {
@@ -1498,14 +1518,14 @@ function OddsMovementBadge({
1498
1518
  }
1499
1519
 
1500
1520
  // src/components/Crest.tsx
1501
- var import_react8 = require("react");
1521
+ var import_react9 = require("react");
1502
1522
  var import_jsx_runtime11 = require("react/jsx-runtime");
1503
1523
  function initials2(name) {
1504
1524
  return (name ?? "").trim().slice(0, 1).toUpperCase() || "?";
1505
1525
  }
1506
1526
  function Crest2({ url, name, kind }) {
1507
1527
  const cls = kind === "comp" ? "ss-w-bi-comp-logo" : "ss-w-bi-crest";
1508
- const [failed, setFailed] = (0, import_react8.useState)(false);
1528
+ const [failed, setFailed] = (0, import_react9.useState)(false);
1509
1529
  if (url && !failed) {
1510
1530
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("img", { className: cls, src: url, alt: "", "aria-hidden": "true", onError: () => setFailed(true) });
1511
1531
  }
@@ -1513,11 +1533,16 @@ function Crest2({ url, name, kind }) {
1513
1533
  }
1514
1534
 
1515
1535
  // src/components/PinToggle.tsx
1516
- var import_react9 = require("react");
1536
+ var import_react10 = require("react");
1517
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
+ };
1518
1543
  function PinToggle({ pin, monitor, ui }) {
1519
- const [toast, setToast] = (0, import_react9.useState)(null);
1520
- 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), []);
1521
1546
  if (!monitor || !monitor.enabled) return null;
1522
1547
  const key = pinKey(pin);
1523
1548
  const pinned = monitor.isPinned(key);
@@ -1530,6 +1555,7 @@ function PinToggle({ pin, monitor, ui }) {
1530
1555
  setToast({ kind: "error", text: ui.marketLimitToast });
1531
1556
  }
1532
1557
  };
1558
+ const glyph = PIN_GLYPHS[ui.pinIcon];
1533
1559
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "ss-w-pin-wrap", children: [
1534
1560
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1535
1561
  "button",
@@ -1537,10 +1563,22 @@ function PinToggle({ pin, monitor, ui }) {
1537
1563
  type: "button",
1538
1564
  className: "ss-w-pin",
1539
1565
  "data-on": pinned || void 0,
1566
+ "data-icon": ui.pinIcon,
1540
1567
  "aria-pressed": pinned,
1541
1568
  "aria-label": pinned ? labels.unpin : labels.pin,
1542
1569
  onClick,
1543
- 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
+ ) })
1544
1582
  }
1545
1583
  ),
1546
1584
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Toast, { toast, onDismiss: dismiss })
@@ -1556,7 +1594,7 @@ function StatRows({ stats }) {
1556
1594
  ] }, `${s.label}:${s.value}`)) });
1557
1595
  }
1558
1596
  function ScopedStats({ scopes }) {
1559
- const [active, setActive] = (0, import_react10.useState)(0);
1597
+ const [active, setActive] = (0, import_react11.useState)(0);
1560
1598
  const current = scopes[active] ?? scopes[0] ?? { key: "", label: "", stats: [] };
1561
1599
  return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "ss-w-bi-scoped", children: [
1562
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)) }),
@@ -1567,7 +1605,7 @@ function hasStatsContent(it) {
1567
1605
  return !!it.scopes && it.scopes.length >= 2 || it.stats.length > 0;
1568
1606
  }
1569
1607
  function ReasonStrip({ it, ui }) {
1570
- const [open, setOpen] = (0, import_react10.useState)(false);
1608
+ const [open, setOpen] = (0, import_react11.useState)(false);
1571
1609
  const hasScopes = !!it.scopes && it.scopes.length >= 2;
1572
1610
  const hasStats = hasStatsContent(it);
1573
1611
  const hasText = !!it.text;
@@ -1677,7 +1715,7 @@ function BettingInsightList({ insights, ui, slip, monitor }) {
1677
1715
  }
1678
1716
 
1679
1717
  // src/components/MarketOddsCardList.tsx
1680
- var import_react11 = require("react");
1718
+ var import_react12 = require("react");
1681
1719
  var import_jsx_runtime14 = require("react/jsx-runtime");
1682
1720
  function OddsRow({ vm, row, ui, slip, inSlipIds }) {
1683
1721
  const selection = vm.addToSlipEnabled ? marketOddsRowToSlipSelection(vm, row, slip.conversationId) : null;
@@ -1694,7 +1732,7 @@ function OddsRow({ vm, row, ui, slip, inSlipIds }) {
1694
1732
  ] });
1695
1733
  }
1696
1734
  function MarketOddsCard({ vm, ui, slip, monitor, inSlipIds }) {
1697
- const [open, setOpen] = (0, import_react11.useState)(false);
1735
+ const [open, setOpen] = (0, import_react12.useState)(false);
1698
1736
  const kickoff = vm.kickoffUtc && ui?.bettingKickoff ? ui.bettingKickoff(vm.kickoffUtc) : void 0;
1699
1737
  const hasNamedTeams = !!vm.homeTeamName && !!vm.awayTeamName;
1700
1738
  const showPin = !!monitor && vm.pinEnabled;
@@ -1987,7 +2025,7 @@ function FixtureCard({ card }) {
1987
2025
  }
1988
2026
 
1989
2027
  // src/components/dashboard/CountdownCard.tsx
1990
- var import_react12 = require("react");
2028
+ var import_react13 = require("react");
1991
2029
  var import_jsx_runtime20 = require("react/jsx-runtime");
1992
2030
  var DAY_MS = 864e5;
1993
2031
  function remainingSegments(kickoffUnix, nowMs, withSeconds) {
@@ -2006,12 +2044,12 @@ function remainingSegments(kickoffUnix, nowMs, withSeconds) {
2006
2044
  function CountdownCard({ card }) {
2007
2045
  const { data } = card;
2008
2046
  const unitLabel = { d: data.daysUnit, h: data.hoursUnit, m: data.minutesUnit, s: data.secondsUnit };
2009
- const [nowMs, setNowMs] = (0, import_react12.useState)(() => Date.now());
2010
- const [reduced] = (0, import_react12.useState)(
2047
+ const [nowMs, setNowMs] = (0, import_react13.useState)(() => Date.now());
2048
+ const [reduced] = (0, import_react13.useState)(
2011
2049
  () => typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches
2012
2050
  );
2013
2051
  const withinDay = data.kickoffUnix * 1e3 - nowMs < DAY_MS;
2014
- (0, import_react12.useEffect)(() => {
2052
+ (0, import_react13.useEffect)(() => {
2015
2053
  if (reduced) return;
2016
2054
  const id = window.setInterval(() => setNowMs(Date.now()), withinDay ? 1e3 : 6e4);
2017
2055
  return () => window.clearInterval(id);
@@ -2019,7 +2057,7 @@ function CountdownCard({ card }) {
2019
2057
  const segments = remainingSegments(data.kickoffUnix, nowMs, withinDay && !reduced);
2020
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: [
2021
2059
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "ss-w-fandash-eyebrow", children: card.title ?? "Kick-off in" }),
2022
- 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: [
2023
2061
  i > 0 ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "ss-w-fandash-countdown-colon", children: ":" }) : null,
2024
2062
  /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("span", { className: "ss-w-fandash-countdown-seg", children: [
2025
2063
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "ss-w-fandash-countdown-value", children: s.value }),
@@ -2030,7 +2068,7 @@ function CountdownCard({ card }) {
2030
2068
  }
2031
2069
 
2032
2070
  // src/components/dashboard/InjuriesCard.tsx
2033
- var import_react13 = require("react");
2071
+ var import_react14 = require("react");
2034
2072
  var import_jsx_runtime21 = require("react/jsx-runtime");
2035
2073
  var SHOWN = 3;
2036
2074
  function ColHead({ name, crestUrl }) {
@@ -2053,7 +2091,7 @@ function InjuryList({ rows, emptyLabel, limit }) {
2053
2091
  }
2054
2092
  function InjuriesCard({ card, ui }) {
2055
2093
  const { data } = card;
2056
- const [expanded, setExpanded] = (0, import_react13.useState)(false);
2094
+ const [expanded, setExpanded] = (0, import_react14.useState)(false);
2057
2095
  const hidden = Math.max(0, data.ours.length - SHOWN) + Math.max(0, data.theirs.length - SHOWN);
2058
2096
  const limit = expanded ? void 0 : SHOWN;
2059
2097
  const showMore = ui?.showMore ?? ((n) => `Show ${n} more`);
@@ -2157,7 +2195,7 @@ function LinksCard({ card }) {
2157
2195
  }
2158
2196
 
2159
2197
  // src/components/dashboard/PostMatchResultCard.tsx
2160
- var import_react14 = require("react");
2198
+ var import_react15 = require("react");
2161
2199
  var import_jsx_runtime27 = require("react/jsx-runtime");
2162
2200
  function initials3(name) {
2163
2201
  return name.trim().slice(0, 3).toUpperCase();
@@ -2167,7 +2205,7 @@ function PostMatchCrest({
2167
2205
  name,
2168
2206
  side
2169
2207
  }) {
2170
- const [failed, setFailed] = (0, import_react14.useState)(false);
2208
+ const [failed, setFailed] = (0, import_react15.useState)(false);
2171
2209
  const cls = `ss-w-fandash-postmatch-crest ss-w-fandash-postmatch-crest--${side}`;
2172
2210
  if (url && !failed) {
2173
2211
  return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("img", { className: cls, src: url, alt: "", "aria-hidden": "true", onError: () => setFailed(true) });
@@ -2366,10 +2404,10 @@ function RestingFormCard({ card }) {
2366
2404
  }
2367
2405
 
2368
2406
  // src/components/dashboard/LiveScoreCard.tsx
2369
- var import_react16 = require("react");
2407
+ var import_react17 = require("react");
2370
2408
 
2371
2409
  // src/useLiveClock.ts
2372
- var import_react15 = require("react");
2410
+ var import_react16 = require("react");
2373
2411
  var TICK_MS = 6e4;
2374
2412
  var MAX_LIVE_TICK_MINUTES = 15;
2375
2413
  function computeDisplayMinute(minute, clockRunning, receivedAtMs) {
@@ -2378,10 +2416,10 @@ function computeDisplayMinute(minute, clockRunning, receivedAtMs) {
2378
2416
  return minute + Math.min(Math.max(0, elapsedMinutes), MAX_LIVE_TICK_MINUTES);
2379
2417
  }
2380
2418
  function useLiveClock(minute, clockRunning, receivedAtMs) {
2381
- const [displayMinute, setDisplayMinute] = (0, import_react15.useState)(
2419
+ const [displayMinute, setDisplayMinute] = (0, import_react16.useState)(
2382
2420
  () => computeDisplayMinute(minute, clockRunning, receivedAtMs)
2383
2421
  );
2384
- (0, import_react15.useEffect)(() => {
2422
+ (0, import_react16.useEffect)(() => {
2385
2423
  setDisplayMinute(computeDisplayMinute(minute, clockRunning, receivedAtMs));
2386
2424
  if (!clockRunning) return;
2387
2425
  const id = window.setInterval(() => {
@@ -2398,7 +2436,7 @@ function initials5(name) {
2398
2436
  return name.trim().slice(0, 3).toUpperCase();
2399
2437
  }
2400
2438
  function LiveCrest({ url, name, side }) {
2401
- const [failed, setFailed] = (0, import_react16.useState)(false);
2439
+ const [failed, setFailed] = (0, import_react17.useState)(false);
2402
2440
  const cls = `ss-w-fandash-live-crest ss-w-fandash-live-crest--${side}`;
2403
2441
  if (url && !failed) {
2404
2442
  return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("img", { className: cls, src: url, alt: "", "aria-hidden": "true", onError: () => setFailed(true) });
@@ -2417,7 +2455,7 @@ function LiveScoreCard({
2417
2455
  }) {
2418
2456
  const { data } = card;
2419
2457
  const seedKey = `${data.minute}:${data.homeScore}:${data.awayScore}`;
2420
- const seedRef = (0, import_react16.useRef)(null);
2458
+ const seedRef = (0, import_react17.useRef)(null);
2421
2459
  if (!seedRef.current || seedRef.current.key !== seedKey) {
2422
2460
  seedRef.current = { key: seedKey, receivedAtMs: Date.now() };
2423
2461
  }
@@ -2457,7 +2495,7 @@ function LiveScoreCard({
2457
2495
  }
2458
2496
 
2459
2497
  // src/components/dashboard/LiveTimelineCard.tsx
2460
- var import_react17 = require("react");
2498
+ var import_react18 = require("react");
2461
2499
  var import_jsx_runtime37 = require("react/jsx-runtime");
2462
2500
  var SHOWN2 = 3;
2463
2501
  function EventIcon({ type }) {
@@ -2499,7 +2537,7 @@ function EventRow({ e }) {
2499
2537
  ] });
2500
2538
  }
2501
2539
  function LiveTimelineCard({ card, ui }) {
2502
- const [expanded, setExpanded] = (0, import_react17.useState)(false);
2540
+ const [expanded, setExpanded] = (0, import_react18.useState)(false);
2503
2541
  const events = card.data.events;
2504
2542
  if (!events || events.length === 0) return null;
2505
2543
  const sorted = [...events].sort((a, b) => b.minute - a.minute);
@@ -2524,7 +2562,7 @@ function LiveTimelineCard({ card, ui }) {
2524
2562
  }
2525
2563
 
2526
2564
  // src/components/dashboard/MomentumWave.tsx
2527
- var import_react18 = require("react");
2565
+ var import_react19 = require("react");
2528
2566
  var import_jsx_runtime38 = require("react/jsx-runtime");
2529
2567
  var WIDTH = 270;
2530
2568
  var HEIGHT = 56;
@@ -2548,7 +2586,7 @@ function MomentumWave({
2548
2586
  }) {
2549
2587
  if (points.length === 0) return null;
2550
2588
  const { area, line, lastLeftPct, lastTopPx } = buildPaths(points);
2551
- const clipId = (0, import_react18.useId)();
2589
+ const clipId = (0, import_react19.useId)();
2552
2590
  const topClipId = `${clipId}-top`;
2553
2591
  const bottomClipId = `${clipId}-bottom`;
2554
2592
  const topClass = clubIsHome ? "ss-w-fandash-live-mom-home" : "ss-w-fandash-live-mom-away";
@@ -2739,11 +2777,11 @@ function turnAnchor(messages) {
2739
2777
  }
2740
2778
  function MessageList({ controller, ui, slip, onDashboardTap, monitor }) {
2741
2779
  const { state, send } = controller;
2742
- const listRef = (0, import_react19.useRef)(null);
2743
- const endRef = (0, import_react19.useRef)(null);
2744
- const pinnedRef = (0, import_react19.useRef)(true);
2745
- const anchoredTurnRef = (0, import_react19.useRef)(null);
2746
- (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)(() => {
2747
2785
  const anchor = turnAnchor(state.messages);
2748
2786
  const list = listRef.current;
2749
2787
  if (anchor && anchor.turnId !== anchoredTurnRef.current) {
@@ -2799,10 +2837,10 @@ function MessageList({ controller, ui, slip, onDashboardTap, monitor }) {
2799
2837
  }
2800
2838
 
2801
2839
  // src/components/ChatInput.tsx
2802
- var import_react20 = require("react");
2840
+ var import_react21 = require("react");
2803
2841
  var import_jsx_runtime44 = require("react/jsx-runtime");
2804
2842
  function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop response", sendLabel = "Send message" }) {
2805
- const [value, setValue] = (0, import_react20.useState)("");
2843
+ const [value, setValue] = (0, import_react21.useState)("");
2806
2844
  const submit = () => {
2807
2845
  const t = value.trim();
2808
2846
  if (!t) return;
@@ -3049,8 +3087,8 @@ function OddsPanel({ monitor, ui, slip }) {
3049
3087
  var import_jsx_runtime48 = require("react/jsx-runtime");
3050
3088
  var FULLSCREEN_QUERY = "(max-width: 639px)";
3051
3089
  function useIsModal() {
3052
- const [modal, setModal] = (0, import_react21.useState)(false);
3053
- (0, import_react21.useEffect)(() => {
3090
+ const [modal, setModal] = (0, import_react22.useState)(false);
3091
+ (0, import_react22.useEffect)(() => {
3054
3092
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
3055
3093
  const mql = window.matchMedia(FULLSCREEN_QUERY);
3056
3094
  const sync = () => setModal(mql.matches);
@@ -3063,7 +3101,7 @@ function useIsModal() {
3063
3101
  function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
3064
3102
  const { state, close, stop, send } = controller;
3065
3103
  const isModal = useIsModal();
3066
- (0, import_react21.useEffect)(() => {
3104
+ (0, import_react22.useEffect)(() => {
3067
3105
  const onKey = (e) => {
3068
3106
  if (e.key === "Escape") close();
3069
3107
  };
@@ -3074,7 +3112,7 @@ function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
3074
3112
  monitor?.setActiveTab("chat");
3075
3113
  send(text);
3076
3114
  };
3077
- (0, import_react21.useEffect)(() => {
3115
+ (0, import_react22.useEffect)(() => {
3078
3116
  if (monitor && monitor.activeTab !== "chat" && state.isStreaming) monitor.markChatActivity();
3079
3117
  }, [monitor, state.isStreaming, state.messages.length]);
3080
3118
  return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
@@ -3145,8 +3183,40 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
3145
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); }
3146
3184
  .ss-w-fab-mark { width: 30px; height: 30px; display: block; object-fit: contain; }
3147
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); }
3148
- .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; }
3149
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); } }
3150
3220
 
3151
3221
  /* panel */
3152
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); }
@@ -3878,16 +3948,20 @@ function injectStyles(root) {
3878
3948
  function StatsWidget(props) {
3879
3949
  const injectedClient = "client" in props ? props.client : void 0;
3880
3950
  const cfg = props.config;
3881
- const built = (0, import_react22.useMemo)(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
3882
- const ui = (0, import_react22.useMemo)(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
3883
- 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
+ }, []);
3884
3958
  const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters, built ? ui.greetingMessage : void 0);
3885
3959
  const monitor = useLiveMonitor({
3886
3960
  getLiveMonitor: (pins) => built ? built.conversation.getLiveMonitor(pins) : Promise.resolve(null),
3887
3961
  storageKey: `ss-w-pins:${cfg?.operatorId ?? ""}:${cfg?.userContext?.operatorUserId ?? "anon"}`,
3888
3962
  isOpen: controller.state.isOpen
3889
3963
  });
3890
- const slip = (0, import_react22.useMemo)(
3964
+ const slip = (0, import_react23.useMemo)(
3891
3965
  () => {
3892
3966
  const handler = props.config?.onAddToSlip;
3893
3967
  return {
@@ -3910,11 +3984,11 @@ function StatsWidget(props) {
3910
3984
  },
3911
3985
  [props.config?.onAddToSlip, controller.state.conversationId, built, monitor.markInSlip]
3912
3986
  );
3913
- const onDashboardTap = (0, import_react22.useMemo)(
3987
+ const onDashboardTap = (0, import_react23.useMemo)(
3914
3988
  () => built ? createDashboardTapSignal((input) => built.conversation.sendSignal(input)) : void 0,
3915
3989
  [built]
3916
3990
  );
3917
- (0, import_react22.useEffect)(() => {
3991
+ (0, import_react23.useEffect)(() => {
3918
3992
  if (!built || !ui.injectStyles) return;
3919
3993
  const node = rootRef.current?.getRootNode();
3920
3994
  if (node) injectStyles(node);
@@ -3930,7 +4004,9 @@ function StatsWidget(props) {
3930
4004
  logoUrl: ui.logoUrl,
3931
4005
  onOpen: controller.open,
3932
4006
  onDismissPopup: controller.dismissPopup,
3933
- dismissLabel: ui.dismissGreeting
4007
+ dismissLabel: ui.dismissGreeting,
4008
+ introAllowed: !introDoneRef.current,
4009
+ onIntroStart: markIntroDone
3934
4010
  }
3935
4011
  ) });
3936
4012
  }