@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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/StatsWidget.tsx
2
- import { useEffect as useEffect9, useMemo as useMemo2, useRef as useRef6 } from "react";
2
+ import { useCallback as useCallback5, useEffect as useEffect10, useMemo as useMemo2, useRef as useRef7 } from "react";
3
3
  import { StatsWidgetClient } from "@sensiblestats/widget-sdk";
4
4
 
5
5
  // src/kickoff.ts
@@ -170,7 +170,7 @@ function normalizeUi(cfg) {
170
170
  cta: cfg.cta,
171
171
  surface: cfg.surface,
172
172
  ink: cfg.ink,
173
- attribution: cfg.attribution ?? true,
173
+ pinIcon: cfg.pinIcon ?? "pin",
174
174
  position: cfg.position ?? "right",
175
175
  offset: { x: cfg.offset?.x ?? 20, y: cfg.offset?.y ?? 20 },
176
176
  radius: cfg.radius ?? 16,
@@ -1158,6 +1158,9 @@ function useLiveMonitor(opts) {
1158
1158
  };
1159
1159
  }
1160
1160
 
1161
+ // src/components/ChatFab.tsx
1162
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState3 } from "react";
1163
+
1161
1164
  // src/components/BrandMark.tsx
1162
1165
  import { useState as useState2 } from "react";
1163
1166
 
@@ -1210,23 +1213,39 @@ function BrandMark({ logoUrl, className }) {
1210
1213
  import { jsx as jsx3, jsxs } from "react/jsx-runtime";
1211
1214
  function GreetingBubble({ text, onOpen, onDismiss, dismissLabel = "Dismiss" }) {
1212
1215
  return /* @__PURE__ */ jsxs("div", { className: "ss-w-greeting", role: "note", children: [
1213
- /* @__PURE__ */ jsx3("button", { type: "button", className: "ss-w-greeting-body", onClick: onOpen, children: text }),
1216
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "ss-w-greeting-body", onClick: onOpen, children: [
1217
+ /* @__PURE__ */ jsxs("span", { className: "ss-w-greeting-dots", "aria-hidden": "true", children: [
1218
+ /* @__PURE__ */ jsx3("i", {}),
1219
+ /* @__PURE__ */ jsx3("i", {}),
1220
+ /* @__PURE__ */ jsx3("i", {})
1221
+ ] }),
1222
+ /* @__PURE__ */ jsx3("span", { className: "ss-w-greeting-text", children: text })
1223
+ ] }),
1214
1224
  onDismiss ? /* @__PURE__ */ jsx3("button", { type: "button", className: "ss-w-greeting-x", "aria-label": dismissLabel, onClick: onDismiss, children: "\xD7" }) : null
1215
1225
  ] });
1216
1226
  }
1217
1227
 
1218
1228
  // src/components/ChatFab.tsx
1219
1229
  import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
1220
- function ChatFab({ label, greeting, popupText, logoUrl, onOpen, onDismissPopup, dismissLabel = "Dismiss" }) {
1230
+ function ChatFab({ label, greeting, popupText, logoUrl, onOpen, onDismissPopup, dismissLabel = "Dismiss", introAllowed = false, onIntroStart }) {
1221
1231
  const bubble = popupText ?? greeting;
1222
- return /* @__PURE__ */ jsxs2("div", { className: "ss-w-fab-wrap", children: [
1232
+ const [intro, setIntro] = useState3(false);
1233
+ const armedRef = useRef4(introAllowed);
1234
+ useEffect4(() => {
1235
+ if (armedRef.current && bubble) {
1236
+ armedRef.current = false;
1237
+ setIntro(true);
1238
+ onIntroStart?.();
1239
+ }
1240
+ }, [bubble, onIntroStart]);
1241
+ return /* @__PURE__ */ jsxs2("div", { className: intro ? "ss-w-fab-wrap ss-w-intro" : "ss-w-fab-wrap", children: [
1223
1242
  bubble ? /* @__PURE__ */ jsx4(GreetingBubble, { text: bubble, onOpen, onDismiss: popupText ? onDismissPopup : void 0, dismissLabel }) : null,
1224
1243
  /* @__PURE__ */ jsx4("button", { type: "button", className: "ss-w-fab", "aria-label": label, onClick: onOpen, children: /* @__PURE__ */ jsx4(BrandMark, { logoUrl, className: "ss-w-fab-mark" }) })
1225
1244
  ] });
1226
1245
  }
1227
1246
 
1228
1247
  // src/components/ChatPanel.tsx
1229
- import { useEffect as useEffect8, useState as useState16 } from "react";
1248
+ import { useEffect as useEffect9, useState as useState17 } from "react";
1230
1249
 
1231
1250
  // src/components/ChatHeader.tsx
1232
1251
  import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -1239,7 +1258,7 @@ function ChatHeader({ onClose, closeLabel = "Close chat", logoUrl, headerTitle }
1239
1258
  }
1240
1259
 
1241
1260
  // src/components/MessageList.tsx
1242
- import { useEffect as useEffect7, useRef as useRef5 } from "react";
1261
+ import { useEffect as useEffect8, useRef as useRef6 } from "react";
1243
1262
 
1244
1263
  // src/components/Markdown.tsx
1245
1264
  import ReactMarkdown from "react-markdown";
@@ -1257,7 +1276,7 @@ function Markdown({ text }) {
1257
1276
  }
1258
1277
 
1259
1278
  // src/components/EntityCardList.tsx
1260
- import { useState as useState3 } from "react";
1279
+ import { useState as useState4 } from "react";
1261
1280
  import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
1262
1281
  var MAX_VISIBLE = 3;
1263
1282
  var RECORD_RE = /^(\d+)-(\d+)-(\d+)$/;
@@ -1341,7 +1360,7 @@ function ScopeTabs({ scopes, active, onSelect }) {
1341
1360
  )) });
1342
1361
  }
1343
1362
  function EntityCardItem({ c }) {
1344
- const [active, setActive] = useState3(0);
1363
+ const [active, setActive] = useState4(0);
1345
1364
  const hasBody = c.scopes.length > 0;
1346
1365
  if (!hasBody) {
1347
1366
  return /* @__PURE__ */ jsx7("div", { className: "ss-w-ecard ss-w-ecard-bare", "data-kind": c.kind, children: /* @__PURE__ */ jsx7(CardHeader, { c, showScopeCrest: false }) });
@@ -1355,7 +1374,7 @@ function EntityCardItem({ c }) {
1355
1374
  ] });
1356
1375
  }
1357
1376
  function EntityCardList({ cards, disabled, showLessLabel = "Show less", showMoreLabel = (n) => `Show ${n} more` }) {
1358
- const [expanded, setExpanded] = useState3(false);
1377
+ const [expanded, setExpanded] = useState4(false);
1359
1378
  if (cards.length === 0) return null;
1360
1379
  const visible = expanded ? cards : cards.slice(0, MAX_VISIBLE);
1361
1380
  const overflow = cards.length - MAX_VISIBLE;
@@ -1366,18 +1385,18 @@ function EntityCardList({ cards, disabled, showLessLabel = "Show less", showMore
1366
1385
  }
1367
1386
 
1368
1387
  // src/components/BettingInsightList.tsx
1369
- import { useState as useState7 } from "react";
1388
+ import { useState as useState8 } from "react";
1370
1389
 
1371
1390
  // src/components/AddToSlipButton.tsx
1372
- import { useCallback as useCallback3, useState as useState4 } from "react";
1391
+ import { useCallback as useCallback3, useState as useState5 } from "react";
1373
1392
  import { isAddToSlipResult } from "@sensiblestats/widget-sdk";
1374
1393
 
1375
1394
  // src/components/Toast.tsx
1376
- import { useEffect as useEffect4 } from "react";
1395
+ import { useEffect as useEffect5 } from "react";
1377
1396
  import { jsx as jsx8 } from "react/jsx-runtime";
1378
1397
  var DISMISS_MS = 4e3;
1379
1398
  function Toast({ toast, onDismiss }) {
1380
- useEffect4(() => {
1399
+ useEffect5(() => {
1381
1400
  if (!toast) return;
1382
1401
  const timer = setTimeout(onDismiss, DISMISS_MS);
1383
1402
  return () => clearTimeout(timer);
@@ -1395,9 +1414,9 @@ function withTimeout(value) {
1395
1414
  ]);
1396
1415
  }
1397
1416
  function SlipButton({ selection, shownOdds, marketName, slip, ui }) {
1398
- const [pending, setPending] = useState4(false);
1399
- const [settled, setSettled] = useState4(false);
1400
- const [toast, setToast] = useState4(null);
1417
+ const [pending, setPending] = useState5(false);
1418
+ const [settled, setSettled] = useState5(false);
1419
+ const [toast, setToast] = useState5(null);
1401
1420
  const dismiss = useCallback3(() => setToast(null), []);
1402
1421
  const handler = slip.onAddToSlip;
1403
1422
  if (!ui || !handler || !selection) return null;
@@ -1462,14 +1481,14 @@ function OddsMovementBadge({
1462
1481
  }
1463
1482
 
1464
1483
  // src/components/Crest.tsx
1465
- import { useState as useState5 } from "react";
1484
+ import { useState as useState6 } from "react";
1466
1485
  import { jsx as jsx11 } from "react/jsx-runtime";
1467
1486
  function initials2(name) {
1468
1487
  return (name ?? "").trim().slice(0, 1).toUpperCase() || "?";
1469
1488
  }
1470
1489
  function Crest2({ url, name, kind }) {
1471
1490
  const cls = kind === "comp" ? "ss-w-bi-comp-logo" : "ss-w-bi-crest";
1472
- const [failed, setFailed] = useState5(false);
1491
+ const [failed, setFailed] = useState6(false);
1473
1492
  if (url && !failed) {
1474
1493
  return /* @__PURE__ */ jsx11("img", { className: cls, src: url, alt: "", "aria-hidden": "true", onError: () => setFailed(true) });
1475
1494
  }
@@ -1477,10 +1496,15 @@ function Crest2({ url, name, kind }) {
1477
1496
  }
1478
1497
 
1479
1498
  // src/components/PinToggle.tsx
1480
- import { useCallback as useCallback4, useState as useState6 } from "react";
1499
+ import { useCallback as useCallback4, useState as useState7 } from "react";
1481
1500
  import { jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
1501
+ var PIN_GLYPHS = {
1502
+ 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" },
1503
+ 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" },
1504
+ 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" }
1505
+ };
1482
1506
  function PinToggle({ pin, monitor, ui }) {
1483
- const [toast, setToast] = useState6(null);
1507
+ const [toast, setToast] = useState7(null);
1484
1508
  const dismiss = useCallback4(() => setToast(null), []);
1485
1509
  if (!monitor || !monitor.enabled) return null;
1486
1510
  const key = pinKey(pin);
@@ -1494,6 +1518,7 @@ function PinToggle({ pin, monitor, ui }) {
1494
1518
  setToast({ kind: "error", text: ui.marketLimitToast });
1495
1519
  }
1496
1520
  };
1521
+ const glyph = PIN_GLYPHS[ui.pinIcon];
1497
1522
  return /* @__PURE__ */ jsxs7("div", { className: "ss-w-pin-wrap", children: [
1498
1523
  /* @__PURE__ */ jsx12(
1499
1524
  "button",
@@ -1501,10 +1526,22 @@ function PinToggle({ pin, monitor, ui }) {
1501
1526
  type: "button",
1502
1527
  className: "ss-w-pin",
1503
1528
  "data-on": pinned || void 0,
1529
+ "data-icon": ui.pinIcon,
1504
1530
  "aria-pressed": pinned,
1505
1531
  "aria-label": pinned ? labels.unpin : labels.pin,
1506
1532
  onClick,
1507
- children: /* @__PURE__ */ jsx12("svg", { viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx12("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" }) })
1533
+ children: /* @__PURE__ */ jsx12("svg", { viewBox: glyph.viewBox, "aria-hidden": "true", children: /* @__PURE__ */ jsx12(
1534
+ "path",
1535
+ {
1536
+ transform: glyph.transform,
1537
+ d: glyph.d,
1538
+ fill: pinned ? "currentColor" : "none",
1539
+ stroke: pinned ? "none" : "currentColor",
1540
+ strokeWidth: glyph.strokeWidth,
1541
+ strokeLinejoin: "round",
1542
+ strokeLinecap: "round"
1543
+ }
1544
+ ) })
1508
1545
  }
1509
1546
  ),
1510
1547
  /* @__PURE__ */ jsx12(Toast, { toast, onDismiss: dismiss })
@@ -1520,7 +1557,7 @@ function StatRows({ stats }) {
1520
1557
  ] }, `${s.label}:${s.value}`)) });
1521
1558
  }
1522
1559
  function ScopedStats({ scopes }) {
1523
- const [active, setActive] = useState7(0);
1560
+ const [active, setActive] = useState8(0);
1524
1561
  const current = scopes[active] ?? scopes[0] ?? { key: "", label: "", stats: [] };
1525
1562
  return /* @__PURE__ */ jsxs8("div", { className: "ss-w-bi-scoped", children: [
1526
1563
  /* @__PURE__ */ jsx13("div", { className: "ss-w-bi-scopes", role: "group", children: scopes.map((s, i) => /* @__PURE__ */ jsx13("button", { type: "button", className: "ss-w-bi-chip", "aria-pressed": i === active, onClick: () => setActive(i), children: s.label }, s.key)) }),
@@ -1531,7 +1568,7 @@ function hasStatsContent(it) {
1531
1568
  return !!it.scopes && it.scopes.length >= 2 || it.stats.length > 0;
1532
1569
  }
1533
1570
  function ReasonStrip({ it, ui }) {
1534
- const [open, setOpen] = useState7(false);
1571
+ const [open, setOpen] = useState8(false);
1535
1572
  const hasScopes = !!it.scopes && it.scopes.length >= 2;
1536
1573
  const hasStats = hasStatsContent(it);
1537
1574
  const hasText = !!it.text;
@@ -1641,7 +1678,7 @@ function BettingInsightList({ insights, ui, slip, monitor }) {
1641
1678
  }
1642
1679
 
1643
1680
  // src/components/MarketOddsCardList.tsx
1644
- import { useState as useState8 } from "react";
1681
+ import { useState as useState9 } from "react";
1645
1682
  import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
1646
1683
  function OddsRow({ vm, row, ui, slip, inSlipIds }) {
1647
1684
  const selection = vm.addToSlipEnabled ? marketOddsRowToSlipSelection(vm, row, slip.conversationId) : null;
@@ -1658,7 +1695,7 @@ function OddsRow({ vm, row, ui, slip, inSlipIds }) {
1658
1695
  ] });
1659
1696
  }
1660
1697
  function MarketOddsCard({ vm, ui, slip, monitor, inSlipIds }) {
1661
- const [open, setOpen] = useState8(false);
1698
+ const [open, setOpen] = useState9(false);
1662
1699
  const kickoff = vm.kickoffUtc && ui?.bettingKickoff ? ui.bettingKickoff(vm.kickoffUtc) : void 0;
1663
1700
  const hasNamedTeams = !!vm.homeTeamName && !!vm.awayTeamName;
1664
1701
  const showPin = !!monitor && vm.pinEnabled;
@@ -1951,7 +1988,7 @@ function FixtureCard({ card }) {
1951
1988
  }
1952
1989
 
1953
1990
  // src/components/dashboard/CountdownCard.tsx
1954
- import { Fragment as Fragment6, useEffect as useEffect5, useState as useState9 } from "react";
1991
+ import { Fragment as Fragment6, useEffect as useEffect6, useState as useState10 } from "react";
1955
1992
  import { jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
1956
1993
  var DAY_MS = 864e5;
1957
1994
  function remainingSegments(kickoffUnix, nowMs, withSeconds) {
@@ -1970,12 +2007,12 @@ function remainingSegments(kickoffUnix, nowMs, withSeconds) {
1970
2007
  function CountdownCard({ card }) {
1971
2008
  const { data } = card;
1972
2009
  const unitLabel = { d: data.daysUnit, h: data.hoursUnit, m: data.minutesUnit, s: data.secondsUnit };
1973
- const [nowMs, setNowMs] = useState9(() => Date.now());
1974
- const [reduced] = useState9(
2010
+ const [nowMs, setNowMs] = useState10(() => Date.now());
2011
+ const [reduced] = useState10(
1975
2012
  () => typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches
1976
2013
  );
1977
2014
  const withinDay = data.kickoffUnix * 1e3 - nowMs < DAY_MS;
1978
- useEffect5(() => {
2015
+ useEffect6(() => {
1979
2016
  if (reduced) return;
1980
2017
  const id = window.setInterval(() => setNowMs(Date.now()), withinDay ? 1e3 : 6e4);
1981
2018
  return () => window.clearInterval(id);
@@ -1994,7 +2031,7 @@ function CountdownCard({ card }) {
1994
2031
  }
1995
2032
 
1996
2033
  // src/components/dashboard/InjuriesCard.tsx
1997
- import { useState as useState10 } from "react";
2034
+ import { useState as useState11 } from "react";
1998
2035
  import { jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
1999
2036
  var SHOWN = 3;
2000
2037
  function ColHead({ name, crestUrl }) {
@@ -2017,7 +2054,7 @@ function InjuryList({ rows, emptyLabel, limit }) {
2017
2054
  }
2018
2055
  function InjuriesCard({ card, ui }) {
2019
2056
  const { data } = card;
2020
- const [expanded, setExpanded] = useState10(false);
2057
+ const [expanded, setExpanded] = useState11(false);
2021
2058
  const hidden = Math.max(0, data.ours.length - SHOWN) + Math.max(0, data.theirs.length - SHOWN);
2022
2059
  const limit = expanded ? void 0 : SHOWN;
2023
2060
  const showMore = ui?.showMore ?? ((n) => `Show ${n} more`);
@@ -2121,7 +2158,7 @@ function LinksCard({ card }) {
2121
2158
  }
2122
2159
 
2123
2160
  // src/components/dashboard/PostMatchResultCard.tsx
2124
- import { useState as useState11 } from "react";
2161
+ import { useState as useState12 } from "react";
2125
2162
  import { jsx as jsx27, jsxs as jsxs22 } from "react/jsx-runtime";
2126
2163
  function initials3(name) {
2127
2164
  return name.trim().slice(0, 3).toUpperCase();
@@ -2131,7 +2168,7 @@ function PostMatchCrest({
2131
2168
  name,
2132
2169
  side
2133
2170
  }) {
2134
- const [failed, setFailed] = useState11(false);
2171
+ const [failed, setFailed] = useState12(false);
2135
2172
  const cls = `ss-w-fandash-postmatch-crest ss-w-fandash-postmatch-crest--${side}`;
2136
2173
  if (url && !failed) {
2137
2174
  return /* @__PURE__ */ jsx27("img", { className: cls, src: url, alt: "", "aria-hidden": "true", onError: () => setFailed(true) });
@@ -2330,10 +2367,10 @@ function RestingFormCard({ card }) {
2330
2367
  }
2331
2368
 
2332
2369
  // src/components/dashboard/LiveScoreCard.tsx
2333
- import { useRef as useRef4, useState as useState13 } from "react";
2370
+ import { useRef as useRef5, useState as useState14 } from "react";
2334
2371
 
2335
2372
  // src/useLiveClock.ts
2336
- import { useEffect as useEffect6, useState as useState12 } from "react";
2373
+ import { useEffect as useEffect7, useState as useState13 } from "react";
2337
2374
  var TICK_MS = 6e4;
2338
2375
  var MAX_LIVE_TICK_MINUTES = 15;
2339
2376
  function computeDisplayMinute(minute, clockRunning, receivedAtMs) {
@@ -2342,10 +2379,10 @@ function computeDisplayMinute(minute, clockRunning, receivedAtMs) {
2342
2379
  return minute + Math.min(Math.max(0, elapsedMinutes), MAX_LIVE_TICK_MINUTES);
2343
2380
  }
2344
2381
  function useLiveClock(minute, clockRunning, receivedAtMs) {
2345
- const [displayMinute, setDisplayMinute] = useState12(
2382
+ const [displayMinute, setDisplayMinute] = useState13(
2346
2383
  () => computeDisplayMinute(minute, clockRunning, receivedAtMs)
2347
2384
  );
2348
- useEffect6(() => {
2385
+ useEffect7(() => {
2349
2386
  setDisplayMinute(computeDisplayMinute(minute, clockRunning, receivedAtMs));
2350
2387
  if (!clockRunning) return;
2351
2388
  const id = window.setInterval(() => {
@@ -2362,7 +2399,7 @@ function initials5(name) {
2362
2399
  return name.trim().slice(0, 3).toUpperCase();
2363
2400
  }
2364
2401
  function LiveCrest({ url, name, side }) {
2365
- const [failed, setFailed] = useState13(false);
2402
+ const [failed, setFailed] = useState14(false);
2366
2403
  const cls = `ss-w-fandash-live-crest ss-w-fandash-live-crest--${side}`;
2367
2404
  if (url && !failed) {
2368
2405
  return /* @__PURE__ */ jsx36("img", { className: cls, src: url, alt: "", "aria-hidden": "true", onError: () => setFailed(true) });
@@ -2381,7 +2418,7 @@ function LiveScoreCard({
2381
2418
  }) {
2382
2419
  const { data } = card;
2383
2420
  const seedKey = `${data.minute}:${data.homeScore}:${data.awayScore}`;
2384
- const seedRef = useRef4(null);
2421
+ const seedRef = useRef5(null);
2385
2422
  if (!seedRef.current || seedRef.current.key !== seedKey) {
2386
2423
  seedRef.current = { key: seedKey, receivedAtMs: Date.now() };
2387
2424
  }
@@ -2421,7 +2458,7 @@ function LiveScoreCard({
2421
2458
  }
2422
2459
 
2423
2460
  // src/components/dashboard/LiveTimelineCard.tsx
2424
- import { useState as useState14 } from "react";
2461
+ import { useState as useState15 } from "react";
2425
2462
  import { jsx as jsx37, jsxs as jsxs32 } from "react/jsx-runtime";
2426
2463
  var SHOWN2 = 3;
2427
2464
  function EventIcon({ type }) {
@@ -2463,7 +2500,7 @@ function EventRow({ e }) {
2463
2500
  ] });
2464
2501
  }
2465
2502
  function LiveTimelineCard({ card, ui }) {
2466
- const [expanded, setExpanded] = useState14(false);
2503
+ const [expanded, setExpanded] = useState15(false);
2467
2504
  const events = card.data.events;
2468
2505
  if (!events || events.length === 0) return null;
2469
2506
  const sorted = [...events].sort((a, b) => b.minute - a.minute);
@@ -2703,11 +2740,11 @@ function turnAnchor(messages) {
2703
2740
  }
2704
2741
  function MessageList({ controller, ui, slip, onDashboardTap, monitor }) {
2705
2742
  const { state, send } = controller;
2706
- const listRef = useRef5(null);
2707
- const endRef = useRef5(null);
2708
- const pinnedRef = useRef5(true);
2709
- const anchoredTurnRef = useRef5(null);
2710
- useEffect7(() => {
2743
+ const listRef = useRef6(null);
2744
+ const endRef = useRef6(null);
2745
+ const pinnedRef = useRef6(true);
2746
+ const anchoredTurnRef = useRef6(null);
2747
+ useEffect8(() => {
2711
2748
  const anchor = turnAnchor(state.messages);
2712
2749
  const list = listRef.current;
2713
2750
  if (anchor && anchor.turnId !== anchoredTurnRef.current) {
@@ -2763,10 +2800,10 @@ function MessageList({ controller, ui, slip, onDashboardTap, monitor }) {
2763
2800
  }
2764
2801
 
2765
2802
  // src/components/ChatInput.tsx
2766
- import { useState as useState15 } from "react";
2803
+ import { useState as useState16 } from "react";
2767
2804
  import { jsx as jsx44, jsxs as jsxs38 } from "react/jsx-runtime";
2768
2805
  function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop response", sendLabel = "Send message" }) {
2769
- const [value, setValue] = useState15("");
2806
+ const [value, setValue] = useState16("");
2770
2807
  const submit = () => {
2771
2808
  const t = value.trim();
2772
2809
  if (!t) return;
@@ -3013,8 +3050,8 @@ function OddsPanel({ monitor, ui, slip }) {
3013
3050
  import { jsx as jsx48, jsxs as jsxs42 } from "react/jsx-runtime";
3014
3051
  var FULLSCREEN_QUERY = "(max-width: 639px)";
3015
3052
  function useIsModal() {
3016
- const [modal, setModal] = useState16(false);
3017
- useEffect8(() => {
3053
+ const [modal, setModal] = useState17(false);
3054
+ useEffect9(() => {
3018
3055
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
3019
3056
  const mql = window.matchMedia(FULLSCREEN_QUERY);
3020
3057
  const sync = () => setModal(mql.matches);
@@ -3027,7 +3064,7 @@ function useIsModal() {
3027
3064
  function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
3028
3065
  const { state, close, stop, send } = controller;
3029
3066
  const isModal = useIsModal();
3030
- useEffect8(() => {
3067
+ useEffect9(() => {
3031
3068
  const onKey = (e) => {
3032
3069
  if (e.key === "Escape") close();
3033
3070
  };
@@ -3038,7 +3075,7 @@ function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
3038
3075
  monitor?.setActiveTab("chat");
3039
3076
  send(text);
3040
3077
  };
3041
- useEffect8(() => {
3078
+ useEffect9(() => {
3042
3079
  if (monitor && monitor.activeTab !== "chat" && state.isStreaming) monitor.markChatActivity();
3043
3080
  }, [monitor, state.isStreaming, state.messages.length]);
3044
3081
  return /* @__PURE__ */ jsxs42("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
@@ -3061,8 +3098,7 @@ function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
3061
3098
  ),
3062
3099
  monitor && monitor.activeTab === "matches" && /* @__PURE__ */ jsx48(MatchesPanel, { monitor, ui, slip }),
3063
3100
  monitor && monitor.activeTab === "odds" && /* @__PURE__ */ jsx48(OddsPanel, { monitor, ui, slip }),
3064
- /* @__PURE__ */ jsx48(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: handleSend, onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage }),
3065
- ui.attribution !== false ? /* @__PURE__ */ jsx48("div", { className: "ss-w-attribution", children: "Powered by SensibleStats" }) : null
3101
+ /* @__PURE__ */ jsx48(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: handleSend, onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
3066
3102
  ] });
3067
3103
  }
3068
3104
 
@@ -3110,8 +3146,40 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
3110
3146
  .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); }
3111
3147
  .ss-w-fab-mark { width: 30px; height: 30px; display: block; object-fit: contain; }
3112
3148
  .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); }
3113
- .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; }
3149
+ .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; }
3114
3150
  .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; }
3151
+ .ss-w-greeting-text { display: block; }
3152
+ /* Typing indicator: overlays the text area so bubble height stays put; hidden at
3153
+ rest, revealed only during the one-shot intro (see prefers-reduced-motion block). */
3154
+ .ss-w-greeting-dots { position: absolute; top: 0; bottom: 0; left: 12px; display: flex; align-items: center; gap: 4px; opacity: 0; pointer-events: none; }
3155
+ .ss-w-greeting-dots i { width: 6px; height: 6px; border-radius: 50%; background: var(--ss-w-faint); display: block; }
3156
+ /* One-shot launcher attention intro: FAB nudges, then the greeting types in.
3157
+ Gated behind no-preference so reduce-motion users get the greeting shown
3158
+ statically (text visible, no nudge, no typing). ChatFab adds .ss-w-intro once
3159
+ per page load; every animation runs a single time and settles at rest. */
3160
+ @media (prefers-reduced-motion: no-preference) {
3161
+ .ss-w-intro .ss-w-fab { animation: ss-w-nudge .8s ease both; }
3162
+ .ss-w-intro .ss-w-greeting { animation: ss-w-greet-in .45s cubic-bezier(.2,.8,.3,1.15) .8s both; }
3163
+ .ss-w-intro .ss-w-greeting-text { animation: ss-w-greet-textin .4s ease 1.65s both; }
3164
+ .ss-w-intro .ss-w-greeting-dots { animation: ss-w-greet-dots .95s ease .85s both; }
3165
+ .ss-w-intro .ss-w-greeting-dots i { animation: ss-w-greet-dotbounce .5s ease .85s 2; }
3166
+ .ss-w-intro .ss-w-greeting-dots i:nth-child(2) { animation-delay: 1s; }
3167
+ .ss-w-intro .ss-w-greeting-dots i:nth-child(3) { animation-delay: 1.15s; }
3168
+ }
3169
+ @keyframes ss-w-nudge {
3170
+ 0%, 100% { transform: none; }
3171
+ 22% { transform: translateY(-9px) rotate(-9deg); }
3172
+ 44% { transform: translateY(0) rotate(7deg); }
3173
+ 64% { transform: translateY(-4px) rotate(-4deg); }
3174
+ 82% { transform: rotate(2deg); }
3175
+ }
3176
+ @keyframes ss-w-greet-in {
3177
+ from { opacity: 0; transform: translateY(8px) scale(.92); }
3178
+ to { opacity: 1; transform: none; }
3179
+ }
3180
+ @keyframes ss-w-greet-textin { from { opacity: 0; } to { opacity: 1; } }
3181
+ @keyframes ss-w-greet-dots { 0% { opacity: 0; } 14% { opacity: 1; } 78% { opacity: 1; } 100% { opacity: 0; } }
3182
+ @keyframes ss-w-greet-dotbounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
3115
3183
 
3116
3184
  /* panel */
3117
3185
  .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); }
@@ -3350,7 +3418,6 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
3350
3418
  }
3351
3419
  .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; }
3352
3420
  .ss-w-stop-glyph { width: 11px; height: 11px; background: currentColor; border-radius: 2px; }
3353
- .ss-w-attribution { flex: 0 0 auto; padding: 6px 12px 8px; text-align: center; font-size: 11px; color: var(--ss-w-faint); }
3354
3421
 
3355
3422
  /* add to slip */
3356
3423
  .ss-w-slip { position: relative; display: flex; }
@@ -3846,7 +3913,11 @@ function StatsWidget(props) {
3846
3913
  const cfg = props.config;
3847
3914
  const built = useMemo2(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
3848
3915
  const ui = useMemo2(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
3849
- const rootRef = useRef6(null);
3916
+ const rootRef = useRef7(null);
3917
+ const introDoneRef = useRef7(false);
3918
+ const markIntroDone = useCallback5(() => {
3919
+ introDoneRef.current = true;
3920
+ }, []);
3850
3921
  const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters, built ? ui.greetingMessage : void 0);
3851
3922
  const monitor = useLiveMonitor({
3852
3923
  getLiveMonitor: (pins) => built ? built.conversation.getLiveMonitor(pins) : Promise.resolve(null),
@@ -3880,7 +3951,7 @@ function StatsWidget(props) {
3880
3951
  () => built ? createDashboardTapSignal((input) => built.conversation.sendSignal(input)) : void 0,
3881
3952
  [built]
3882
3953
  );
3883
- useEffect9(() => {
3954
+ useEffect10(() => {
3884
3955
  if (!built || !ui.injectStyles) return;
3885
3956
  const node = rootRef.current?.getRootNode();
3886
3957
  if (node) injectStyles(node);
@@ -3896,7 +3967,9 @@ function StatsWidget(props) {
3896
3967
  logoUrl: ui.logoUrl,
3897
3968
  onOpen: controller.open,
3898
3969
  onDismissPopup: controller.dismissPopup,
3899
- dismissLabel: ui.dismissGreeting
3970
+ dismissLabel: ui.dismissGreeting,
3971
+ introAllowed: !introDoneRef.current,
3972
+ onIntroStart: markIntroDone
3900
3973
  }
3901
3974
  ) });
3902
3975
  }