@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.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,6 +170,7 @@ function normalizeUi(cfg) {
170
170
  cta: cfg.cta,
171
171
  surface: cfg.surface,
172
172
  ink: cfg.ink,
173
+ pinIcon: cfg.pinIcon ?? "pin",
173
174
  position: cfg.position ?? "right",
174
175
  offset: { x: cfg.offset?.x ?? 20, y: cfg.offset?.y ?? 20 },
175
176
  radius: cfg.radius ?? 16,
@@ -1157,6 +1158,9 @@ function useLiveMonitor(opts) {
1157
1158
  };
1158
1159
  }
1159
1160
 
1161
+ // src/components/ChatFab.tsx
1162
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState3 } from "react";
1163
+
1160
1164
  // src/components/BrandMark.tsx
1161
1165
  import { useState as useState2 } from "react";
1162
1166
 
@@ -1209,23 +1213,39 @@ function BrandMark({ logoUrl, className }) {
1209
1213
  import { jsx as jsx3, jsxs } from "react/jsx-runtime";
1210
1214
  function GreetingBubble({ text, onOpen, onDismiss, dismissLabel = "Dismiss" }) {
1211
1215
  return /* @__PURE__ */ jsxs("div", { className: "ss-w-greeting", role: "note", children: [
1212
- /* @__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
+ ] }),
1213
1224
  onDismiss ? /* @__PURE__ */ jsx3("button", { type: "button", className: "ss-w-greeting-x", "aria-label": dismissLabel, onClick: onDismiss, children: "\xD7" }) : null
1214
1225
  ] });
1215
1226
  }
1216
1227
 
1217
1228
  // src/components/ChatFab.tsx
1218
1229
  import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
1219
- function ChatFab({ label, greeting, popupText, logoUrl, onOpen, onDismissPopup, dismissLabel = "Dismiss" }) {
1230
+ function ChatFab({ label, greeting, popupText, logoUrl, onOpen, onDismissPopup, dismissLabel = "Dismiss", introAllowed = false, onIntroStart }) {
1220
1231
  const bubble = popupText ?? greeting;
1221
- 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: [
1222
1242
  bubble ? /* @__PURE__ */ jsx4(GreetingBubble, { text: bubble, onOpen, onDismiss: popupText ? onDismissPopup : void 0, dismissLabel }) : null,
1223
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" }) })
1224
1244
  ] });
1225
1245
  }
1226
1246
 
1227
1247
  // src/components/ChatPanel.tsx
1228
- import { useEffect as useEffect8, useState as useState16 } from "react";
1248
+ import { useEffect as useEffect9, useState as useState17 } from "react";
1229
1249
 
1230
1250
  // src/components/ChatHeader.tsx
1231
1251
  import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -1238,7 +1258,7 @@ function ChatHeader({ onClose, closeLabel = "Close chat", logoUrl, headerTitle }
1238
1258
  }
1239
1259
 
1240
1260
  // src/components/MessageList.tsx
1241
- import { useEffect as useEffect7, useRef as useRef5 } from "react";
1261
+ import { useEffect as useEffect8, useRef as useRef6 } from "react";
1242
1262
 
1243
1263
  // src/components/Markdown.tsx
1244
1264
  import ReactMarkdown from "react-markdown";
@@ -1256,7 +1276,7 @@ function Markdown({ text }) {
1256
1276
  }
1257
1277
 
1258
1278
  // src/components/EntityCardList.tsx
1259
- import { useState as useState3 } from "react";
1279
+ import { useState as useState4 } from "react";
1260
1280
  import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
1261
1281
  var MAX_VISIBLE = 3;
1262
1282
  var RECORD_RE = /^(\d+)-(\d+)-(\d+)$/;
@@ -1340,7 +1360,7 @@ function ScopeTabs({ scopes, active, onSelect }) {
1340
1360
  )) });
1341
1361
  }
1342
1362
  function EntityCardItem({ c }) {
1343
- const [active, setActive] = useState3(0);
1363
+ const [active, setActive] = useState4(0);
1344
1364
  const hasBody = c.scopes.length > 0;
1345
1365
  if (!hasBody) {
1346
1366
  return /* @__PURE__ */ jsx7("div", { className: "ss-w-ecard ss-w-ecard-bare", "data-kind": c.kind, children: /* @__PURE__ */ jsx7(CardHeader, { c, showScopeCrest: false }) });
@@ -1354,7 +1374,7 @@ function EntityCardItem({ c }) {
1354
1374
  ] });
1355
1375
  }
1356
1376
  function EntityCardList({ cards, disabled, showLessLabel = "Show less", showMoreLabel = (n) => `Show ${n} more` }) {
1357
- const [expanded, setExpanded] = useState3(false);
1377
+ const [expanded, setExpanded] = useState4(false);
1358
1378
  if (cards.length === 0) return null;
1359
1379
  const visible = expanded ? cards : cards.slice(0, MAX_VISIBLE);
1360
1380
  const overflow = cards.length - MAX_VISIBLE;
@@ -1365,18 +1385,18 @@ function EntityCardList({ cards, disabled, showLessLabel = "Show less", showMore
1365
1385
  }
1366
1386
 
1367
1387
  // src/components/BettingInsightList.tsx
1368
- import { useState as useState7 } from "react";
1388
+ import { useState as useState8 } from "react";
1369
1389
 
1370
1390
  // src/components/AddToSlipButton.tsx
1371
- import { useCallback as useCallback3, useState as useState4 } from "react";
1391
+ import { useCallback as useCallback3, useState as useState5 } from "react";
1372
1392
  import { isAddToSlipResult } from "@sensiblestats/widget-sdk";
1373
1393
 
1374
1394
  // src/components/Toast.tsx
1375
- import { useEffect as useEffect4 } from "react";
1395
+ import { useEffect as useEffect5 } from "react";
1376
1396
  import { jsx as jsx8 } from "react/jsx-runtime";
1377
1397
  var DISMISS_MS = 4e3;
1378
1398
  function Toast({ toast, onDismiss }) {
1379
- useEffect4(() => {
1399
+ useEffect5(() => {
1380
1400
  if (!toast) return;
1381
1401
  const timer = setTimeout(onDismiss, DISMISS_MS);
1382
1402
  return () => clearTimeout(timer);
@@ -1394,9 +1414,9 @@ function withTimeout(value) {
1394
1414
  ]);
1395
1415
  }
1396
1416
  function SlipButton({ selection, shownOdds, marketName, slip, ui }) {
1397
- const [pending, setPending] = useState4(false);
1398
- const [settled, setSettled] = useState4(false);
1399
- const [toast, setToast] = useState4(null);
1417
+ const [pending, setPending] = useState5(false);
1418
+ const [settled, setSettled] = useState5(false);
1419
+ const [toast, setToast] = useState5(null);
1400
1420
  const dismiss = useCallback3(() => setToast(null), []);
1401
1421
  const handler = slip.onAddToSlip;
1402
1422
  if (!ui || !handler || !selection) return null;
@@ -1461,14 +1481,14 @@ function OddsMovementBadge({
1461
1481
  }
1462
1482
 
1463
1483
  // src/components/Crest.tsx
1464
- import { useState as useState5 } from "react";
1484
+ import { useState as useState6 } from "react";
1465
1485
  import { jsx as jsx11 } from "react/jsx-runtime";
1466
1486
  function initials2(name) {
1467
1487
  return (name ?? "").trim().slice(0, 1).toUpperCase() || "?";
1468
1488
  }
1469
1489
  function Crest2({ url, name, kind }) {
1470
1490
  const cls = kind === "comp" ? "ss-w-bi-comp-logo" : "ss-w-bi-crest";
1471
- const [failed, setFailed] = useState5(false);
1491
+ const [failed, setFailed] = useState6(false);
1472
1492
  if (url && !failed) {
1473
1493
  return /* @__PURE__ */ jsx11("img", { className: cls, src: url, alt: "", "aria-hidden": "true", onError: () => setFailed(true) });
1474
1494
  }
@@ -1476,10 +1496,15 @@ function Crest2({ url, name, kind }) {
1476
1496
  }
1477
1497
 
1478
1498
  // src/components/PinToggle.tsx
1479
- import { useCallback as useCallback4, useState as useState6 } from "react";
1499
+ import { useCallback as useCallback4, useState as useState7 } from "react";
1480
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
+ };
1481
1506
  function PinToggle({ pin, monitor, ui }) {
1482
- const [toast, setToast] = useState6(null);
1507
+ const [toast, setToast] = useState7(null);
1483
1508
  const dismiss = useCallback4(() => setToast(null), []);
1484
1509
  if (!monitor || !monitor.enabled) return null;
1485
1510
  const key = pinKey(pin);
@@ -1493,6 +1518,7 @@ function PinToggle({ pin, monitor, ui }) {
1493
1518
  setToast({ kind: "error", text: ui.marketLimitToast });
1494
1519
  }
1495
1520
  };
1521
+ const glyph = PIN_GLYPHS[ui.pinIcon];
1496
1522
  return /* @__PURE__ */ jsxs7("div", { className: "ss-w-pin-wrap", children: [
1497
1523
  /* @__PURE__ */ jsx12(
1498
1524
  "button",
@@ -1500,10 +1526,22 @@ function PinToggle({ pin, monitor, ui }) {
1500
1526
  type: "button",
1501
1527
  className: "ss-w-pin",
1502
1528
  "data-on": pinned || void 0,
1529
+ "data-icon": ui.pinIcon,
1503
1530
  "aria-pressed": pinned,
1504
1531
  "aria-label": pinned ? labels.unpin : labels.pin,
1505
1532
  onClick,
1506
- 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
+ ) })
1507
1545
  }
1508
1546
  ),
1509
1547
  /* @__PURE__ */ jsx12(Toast, { toast, onDismiss: dismiss })
@@ -1519,7 +1557,7 @@ function StatRows({ stats }) {
1519
1557
  ] }, `${s.label}:${s.value}`)) });
1520
1558
  }
1521
1559
  function ScopedStats({ scopes }) {
1522
- const [active, setActive] = useState7(0);
1560
+ const [active, setActive] = useState8(0);
1523
1561
  const current = scopes[active] ?? scopes[0] ?? { key: "", label: "", stats: [] };
1524
1562
  return /* @__PURE__ */ jsxs8("div", { className: "ss-w-bi-scoped", children: [
1525
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)) }),
@@ -1530,7 +1568,7 @@ function hasStatsContent(it) {
1530
1568
  return !!it.scopes && it.scopes.length >= 2 || it.stats.length > 0;
1531
1569
  }
1532
1570
  function ReasonStrip({ it, ui }) {
1533
- const [open, setOpen] = useState7(false);
1571
+ const [open, setOpen] = useState8(false);
1534
1572
  const hasScopes = !!it.scopes && it.scopes.length >= 2;
1535
1573
  const hasStats = hasStatsContent(it);
1536
1574
  const hasText = !!it.text;
@@ -1640,7 +1678,7 @@ function BettingInsightList({ insights, ui, slip, monitor }) {
1640
1678
  }
1641
1679
 
1642
1680
  // src/components/MarketOddsCardList.tsx
1643
- import { useState as useState8 } from "react";
1681
+ import { useState as useState9 } from "react";
1644
1682
  import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
1645
1683
  function OddsRow({ vm, row, ui, slip, inSlipIds }) {
1646
1684
  const selection = vm.addToSlipEnabled ? marketOddsRowToSlipSelection(vm, row, slip.conversationId) : null;
@@ -1657,7 +1695,7 @@ function OddsRow({ vm, row, ui, slip, inSlipIds }) {
1657
1695
  ] });
1658
1696
  }
1659
1697
  function MarketOddsCard({ vm, ui, slip, monitor, inSlipIds }) {
1660
- const [open, setOpen] = useState8(false);
1698
+ const [open, setOpen] = useState9(false);
1661
1699
  const kickoff = vm.kickoffUtc && ui?.bettingKickoff ? ui.bettingKickoff(vm.kickoffUtc) : void 0;
1662
1700
  const hasNamedTeams = !!vm.homeTeamName && !!vm.awayTeamName;
1663
1701
  const showPin = !!monitor && vm.pinEnabled;
@@ -1950,7 +1988,7 @@ function FixtureCard({ card }) {
1950
1988
  }
1951
1989
 
1952
1990
  // src/components/dashboard/CountdownCard.tsx
1953
- 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";
1954
1992
  import { jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
1955
1993
  var DAY_MS = 864e5;
1956
1994
  function remainingSegments(kickoffUnix, nowMs, withSeconds) {
@@ -1969,12 +2007,12 @@ function remainingSegments(kickoffUnix, nowMs, withSeconds) {
1969
2007
  function CountdownCard({ card }) {
1970
2008
  const { data } = card;
1971
2009
  const unitLabel = { d: data.daysUnit, h: data.hoursUnit, m: data.minutesUnit, s: data.secondsUnit };
1972
- const [nowMs, setNowMs] = useState9(() => Date.now());
1973
- const [reduced] = useState9(
2010
+ const [nowMs, setNowMs] = useState10(() => Date.now());
2011
+ const [reduced] = useState10(
1974
2012
  () => typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches
1975
2013
  );
1976
2014
  const withinDay = data.kickoffUnix * 1e3 - nowMs < DAY_MS;
1977
- useEffect5(() => {
2015
+ useEffect6(() => {
1978
2016
  if (reduced) return;
1979
2017
  const id = window.setInterval(() => setNowMs(Date.now()), withinDay ? 1e3 : 6e4);
1980
2018
  return () => window.clearInterval(id);
@@ -1993,7 +2031,7 @@ function CountdownCard({ card }) {
1993
2031
  }
1994
2032
 
1995
2033
  // src/components/dashboard/InjuriesCard.tsx
1996
- import { useState as useState10 } from "react";
2034
+ import { useState as useState11 } from "react";
1997
2035
  import { jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
1998
2036
  var SHOWN = 3;
1999
2037
  function ColHead({ name, crestUrl }) {
@@ -2016,7 +2054,7 @@ function InjuryList({ rows, emptyLabel, limit }) {
2016
2054
  }
2017
2055
  function InjuriesCard({ card, ui }) {
2018
2056
  const { data } = card;
2019
- const [expanded, setExpanded] = useState10(false);
2057
+ const [expanded, setExpanded] = useState11(false);
2020
2058
  const hidden = Math.max(0, data.ours.length - SHOWN) + Math.max(0, data.theirs.length - SHOWN);
2021
2059
  const limit = expanded ? void 0 : SHOWN;
2022
2060
  const showMore = ui?.showMore ?? ((n) => `Show ${n} more`);
@@ -2120,7 +2158,7 @@ function LinksCard({ card }) {
2120
2158
  }
2121
2159
 
2122
2160
  // src/components/dashboard/PostMatchResultCard.tsx
2123
- import { useState as useState11 } from "react";
2161
+ import { useState as useState12 } from "react";
2124
2162
  import { jsx as jsx27, jsxs as jsxs22 } from "react/jsx-runtime";
2125
2163
  function initials3(name) {
2126
2164
  return name.trim().slice(0, 3).toUpperCase();
@@ -2130,7 +2168,7 @@ function PostMatchCrest({
2130
2168
  name,
2131
2169
  side
2132
2170
  }) {
2133
- const [failed, setFailed] = useState11(false);
2171
+ const [failed, setFailed] = useState12(false);
2134
2172
  const cls = `ss-w-fandash-postmatch-crest ss-w-fandash-postmatch-crest--${side}`;
2135
2173
  if (url && !failed) {
2136
2174
  return /* @__PURE__ */ jsx27("img", { className: cls, src: url, alt: "", "aria-hidden": "true", onError: () => setFailed(true) });
@@ -2329,10 +2367,10 @@ function RestingFormCard({ card }) {
2329
2367
  }
2330
2368
 
2331
2369
  // src/components/dashboard/LiveScoreCard.tsx
2332
- import { useRef as useRef4, useState as useState13 } from "react";
2370
+ import { useRef as useRef5, useState as useState14 } from "react";
2333
2371
 
2334
2372
  // src/useLiveClock.ts
2335
- import { useEffect as useEffect6, useState as useState12 } from "react";
2373
+ import { useEffect as useEffect7, useState as useState13 } from "react";
2336
2374
  var TICK_MS = 6e4;
2337
2375
  var MAX_LIVE_TICK_MINUTES = 15;
2338
2376
  function computeDisplayMinute(minute, clockRunning, receivedAtMs) {
@@ -2341,10 +2379,10 @@ function computeDisplayMinute(minute, clockRunning, receivedAtMs) {
2341
2379
  return minute + Math.min(Math.max(0, elapsedMinutes), MAX_LIVE_TICK_MINUTES);
2342
2380
  }
2343
2381
  function useLiveClock(minute, clockRunning, receivedAtMs) {
2344
- const [displayMinute, setDisplayMinute] = useState12(
2382
+ const [displayMinute, setDisplayMinute] = useState13(
2345
2383
  () => computeDisplayMinute(minute, clockRunning, receivedAtMs)
2346
2384
  );
2347
- useEffect6(() => {
2385
+ useEffect7(() => {
2348
2386
  setDisplayMinute(computeDisplayMinute(minute, clockRunning, receivedAtMs));
2349
2387
  if (!clockRunning) return;
2350
2388
  const id = window.setInterval(() => {
@@ -2361,7 +2399,7 @@ function initials5(name) {
2361
2399
  return name.trim().slice(0, 3).toUpperCase();
2362
2400
  }
2363
2401
  function LiveCrest({ url, name, side }) {
2364
- const [failed, setFailed] = useState13(false);
2402
+ const [failed, setFailed] = useState14(false);
2365
2403
  const cls = `ss-w-fandash-live-crest ss-w-fandash-live-crest--${side}`;
2366
2404
  if (url && !failed) {
2367
2405
  return /* @__PURE__ */ jsx36("img", { className: cls, src: url, alt: "", "aria-hidden": "true", onError: () => setFailed(true) });
@@ -2380,7 +2418,7 @@ function LiveScoreCard({
2380
2418
  }) {
2381
2419
  const { data } = card;
2382
2420
  const seedKey = `${data.minute}:${data.homeScore}:${data.awayScore}`;
2383
- const seedRef = useRef4(null);
2421
+ const seedRef = useRef5(null);
2384
2422
  if (!seedRef.current || seedRef.current.key !== seedKey) {
2385
2423
  seedRef.current = { key: seedKey, receivedAtMs: Date.now() };
2386
2424
  }
@@ -2420,7 +2458,7 @@ function LiveScoreCard({
2420
2458
  }
2421
2459
 
2422
2460
  // src/components/dashboard/LiveTimelineCard.tsx
2423
- import { useState as useState14 } from "react";
2461
+ import { useState as useState15 } from "react";
2424
2462
  import { jsx as jsx37, jsxs as jsxs32 } from "react/jsx-runtime";
2425
2463
  var SHOWN2 = 3;
2426
2464
  function EventIcon({ type }) {
@@ -2462,7 +2500,7 @@ function EventRow({ e }) {
2462
2500
  ] });
2463
2501
  }
2464
2502
  function LiveTimelineCard({ card, ui }) {
2465
- const [expanded, setExpanded] = useState14(false);
2503
+ const [expanded, setExpanded] = useState15(false);
2466
2504
  const events = card.data.events;
2467
2505
  if (!events || events.length === 0) return null;
2468
2506
  const sorted = [...events].sort((a, b) => b.minute - a.minute);
@@ -2702,11 +2740,11 @@ function turnAnchor(messages) {
2702
2740
  }
2703
2741
  function MessageList({ controller, ui, slip, onDashboardTap, monitor }) {
2704
2742
  const { state, send } = controller;
2705
- const listRef = useRef5(null);
2706
- const endRef = useRef5(null);
2707
- const pinnedRef = useRef5(true);
2708
- const anchoredTurnRef = useRef5(null);
2709
- useEffect7(() => {
2743
+ const listRef = useRef6(null);
2744
+ const endRef = useRef6(null);
2745
+ const pinnedRef = useRef6(true);
2746
+ const anchoredTurnRef = useRef6(null);
2747
+ useEffect8(() => {
2710
2748
  const anchor = turnAnchor(state.messages);
2711
2749
  const list = listRef.current;
2712
2750
  if (anchor && anchor.turnId !== anchoredTurnRef.current) {
@@ -2762,10 +2800,10 @@ function MessageList({ controller, ui, slip, onDashboardTap, monitor }) {
2762
2800
  }
2763
2801
 
2764
2802
  // src/components/ChatInput.tsx
2765
- import { useState as useState15 } from "react";
2803
+ import { useState as useState16 } from "react";
2766
2804
  import { jsx as jsx44, jsxs as jsxs38 } from "react/jsx-runtime";
2767
2805
  function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop response", sendLabel = "Send message" }) {
2768
- const [value, setValue] = useState15("");
2806
+ const [value, setValue] = useState16("");
2769
2807
  const submit = () => {
2770
2808
  const t = value.trim();
2771
2809
  if (!t) return;
@@ -3012,8 +3050,8 @@ function OddsPanel({ monitor, ui, slip }) {
3012
3050
  import { jsx as jsx48, jsxs as jsxs42 } from "react/jsx-runtime";
3013
3051
  var FULLSCREEN_QUERY = "(max-width: 639px)";
3014
3052
  function useIsModal() {
3015
- const [modal, setModal] = useState16(false);
3016
- useEffect8(() => {
3053
+ const [modal, setModal] = useState17(false);
3054
+ useEffect9(() => {
3017
3055
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
3018
3056
  const mql = window.matchMedia(FULLSCREEN_QUERY);
3019
3057
  const sync = () => setModal(mql.matches);
@@ -3026,7 +3064,7 @@ function useIsModal() {
3026
3064
  function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
3027
3065
  const { state, close, stop, send } = controller;
3028
3066
  const isModal = useIsModal();
3029
- useEffect8(() => {
3067
+ useEffect9(() => {
3030
3068
  const onKey = (e) => {
3031
3069
  if (e.key === "Escape") close();
3032
3070
  };
@@ -3037,7 +3075,7 @@ function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
3037
3075
  monitor?.setActiveTab("chat");
3038
3076
  send(text);
3039
3077
  };
3040
- useEffect8(() => {
3078
+ useEffect9(() => {
3041
3079
  if (monitor && monitor.activeTab !== "chat" && state.isStreaming) monitor.markChatActivity();
3042
3080
  }, [monitor, state.isStreaming, state.messages.length]);
3043
3081
  return /* @__PURE__ */ jsxs42("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
@@ -3108,8 +3146,40 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
3108
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); }
3109
3147
  .ss-w-fab-mark { width: 30px; height: 30px; display: block; object-fit: contain; }
3110
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); }
3111
- .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; }
3112
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); } }
3113
3183
 
3114
3184
  /* panel */
3115
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); }
@@ -3843,7 +3913,11 @@ function StatsWidget(props) {
3843
3913
  const cfg = props.config;
3844
3914
  const built = useMemo2(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
3845
3915
  const ui = useMemo2(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
3846
- const rootRef = useRef6(null);
3916
+ const rootRef = useRef7(null);
3917
+ const introDoneRef = useRef7(false);
3918
+ const markIntroDone = useCallback5(() => {
3919
+ introDoneRef.current = true;
3920
+ }, []);
3847
3921
  const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters, built ? ui.greetingMessage : void 0);
3848
3922
  const monitor = useLiveMonitor({
3849
3923
  getLiveMonitor: (pins) => built ? built.conversation.getLiveMonitor(pins) : Promise.resolve(null),
@@ -3877,7 +3951,7 @@ function StatsWidget(props) {
3877
3951
  () => built ? createDashboardTapSignal((input) => built.conversation.sendSignal(input)) : void 0,
3878
3952
  [built]
3879
3953
  );
3880
- useEffect9(() => {
3954
+ useEffect10(() => {
3881
3955
  if (!built || !ui.injectStyles) return;
3882
3956
  const node = rootRef.current?.getRootNode();
3883
3957
  if (node) injectStyles(node);
@@ -3893,7 +3967,9 @@ function StatsWidget(props) {
3893
3967
  logoUrl: ui.logoUrl,
3894
3968
  onOpen: controller.open,
3895
3969
  onDismissPopup: controller.dismissPopup,
3896
- dismissLabel: ui.dismissGreeting
3970
+ dismissLabel: ui.dismissGreeting,
3971
+ introAllowed: !introDoneRef.current,
3972
+ onIntroStart: markIntroDone
3897
3973
  }
3898
3974
  ) });
3899
3975
  }