@sensiblestats/widget-react 0.16.0 → 0.18.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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @sensiblestats/widget-react
2
2
 
3
+ ## 0.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Fact-grounded club dashboard taps: per-fact renderers are now individually tappable and send their grounding back with the chat message.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @sensiblestats/widget-sdk@0.16.0
13
+
14
+ ## 0.17.0
15
+
16
+ ### Minor Changes
17
+
18
+ - 5fd5471: Redesign the betting insight card: crest-led three-row layout with competition logo, kickoff time and a bar-chart reasoning strip.
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies [5fd5471]
23
+ - @sensiblestats/widget-sdk@0.15.0
24
+
3
25
  ## 0.16.0
4
26
 
5
27
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -39,6 +39,18 @@ module.exports = __toCommonJS(index_exports);
39
39
  var import_react12 = require("react");
40
40
  var import_widget_sdk5 = require("@sensiblestats/widget-sdk");
41
41
 
42
+ // src/kickoff.ts
43
+ function formatBettingKickoff(kickoffUtc, ui, locale, now = /* @__PURE__ */ new Date()) {
44
+ const at = new Date(kickoffUtc);
45
+ const intlLocale = locale === "tr" ? "tr-TR" : "en-GB";
46
+ const time = new Intl.DateTimeFormat(intlLocale, { hour: "2-digit", minute: "2-digit" }).format(at);
47
+ const atLocalDate = new Date(at.getFullYear(), at.getMonth(), at.getDate());
48
+ const nowLocalDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
49
+ const dayDiff = Math.round((atLocalDate.getTime() - nowLocalDate.getTime()) / 864e5);
50
+ const dayLabel = dayDiff === 0 ? ui.today : dayDiff === 1 ? ui.tomorrow : new Intl.DateTimeFormat(intlLocale, { weekday: "short", day: "numeric", month: "short" }).format(at);
51
+ return { dayLabel, time };
52
+ }
53
+
42
54
  // src/i18n.ts
43
55
  var STRINGS = {
44
56
  en: {
@@ -93,7 +105,10 @@ var STRINGS = {
93
105
  slipPriceNotAccepted: (from, to) => `Price moved to ${to} (was ${from}). Tap to add.`,
94
106
  slipLoginRequired: "Sign in to add to your slip",
95
107
  slipSuspended: "Market suspended",
96
- slipFailed: "Couldn't add to slip. Try again."
108
+ slipFailed: "Couldn't add to slip. Try again.",
109
+ today: "Today",
110
+ tomorrow: "Tomorrow",
111
+ bettingKickoff: (kickoffUtc, now) => formatBettingKickoff(kickoffUtc, STRINGS.en, "en", now)
97
112
  },
98
113
  tr: {
99
114
  launcherLabel: "SensibleStats ile sohbet et",
@@ -147,7 +162,10 @@ var STRINGS = {
147
162
  slipPriceNotAccepted: (from, to) => `Oran ${to} oldu (\xF6nceki ${from}). Eklemek i\xE7in dokunun.`,
148
163
  slipLoginRequired: "Kupona eklemek i\xE7in giri\u015F yap\u0131n",
149
164
  slipSuspended: "Bu market ask\u0131ya al\u0131nd\u0131",
150
- slipFailed: "Kupona eklenemedi. Tekrar deneyin."
165
+ slipFailed: "Kupona eklenemedi. Tekrar deneyin.",
166
+ today: "Bug\xFCn",
167
+ tomorrow: "Yar\u0131n",
168
+ bettingKickoff: (kickoffUtc, now) => formatBettingKickoff(kickoffUtc, STRINGS.tr, "tr", now)
151
169
  }
152
170
  };
153
171
  function resolveLocaleStrings(locale) {
@@ -214,7 +232,10 @@ function normalizeUi(cfg) {
214
232
  slipPriceNotAccepted: strings.slipPriceNotAccepted,
215
233
  slipLoginRequired: strings.slipLoginRequired,
216
234
  slipSuspended: strings.slipSuspended,
217
- slipFailed: strings.slipFailed
235
+ slipFailed: strings.slipFailed,
236
+ today: strings.today,
237
+ tomorrow: strings.tomorrow,
238
+ bettingKickoff: strings.bettingKickoff
218
239
  };
219
240
  }
220
241
  function toClientOptions(cfg) {
@@ -403,7 +424,13 @@ function toInsightVM(insight) {
403
424
  oddsDecimal: typeof oddsValue === "number" && Number.isFinite(oddsValue) ? oddsValue : void 0,
404
425
  quotedAtUtc: str(insight.odds?.quotedAtUtc),
405
426
  previousOdds: previous !== void 0 && direction !== void 0 ? previous.toFixed(2) : void 0,
406
- movementDirection: previous !== void 0 && direction !== void 0 ? direction : void 0
427
+ movementDirection: previous !== void 0 && direction !== void 0 ? direction : void 0,
428
+ homeTeamName: home,
429
+ awayTeamName: away,
430
+ homeCrest: str(insight.homeTeamImagePath),
431
+ awayCrest: str(insight.awayTeamImagePath),
432
+ competitionCrest: str(insight.competitionImagePath),
433
+ kickoffUtc: str(insight.kickoffUtc)
407
434
  };
408
435
  }
409
436
  function toSlipSelection(vm, conversationId) {
@@ -625,7 +652,7 @@ function useChatStream(conversation, starterTexts, greetingMessage) {
625
652
  const send = (0, import_react.useCallback)((text, opts) => {
626
653
  const trimmed = text.trim();
627
654
  if (!trimmed) return;
628
- run({ message: trimmed, actionId: opts?.actionId }, opts?.displayText);
655
+ run({ message: trimmed, actionId: opts?.actionId, grounding: opts?.grounding }, opts?.displayText);
629
656
  }, [run]);
630
657
  const retry = (0, import_react.useCallback)(() => {
631
658
  const last = stateRef.current.lastUserInput;
@@ -993,10 +1020,21 @@ function ScopedStats({ scopes }) {
993
1020
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(StatRows2, { stats: current.stats }, current.key)
994
1021
  ] });
995
1022
  }
1023
+ function hasStatsContent(it) {
1024
+ return !!it.scopes && it.scopes.length >= 2 || it.stats.length > 0;
1025
+ }
1026
+ function initials2(name) {
1027
+ return (name ?? "").trim().slice(0, 1).toUpperCase() || "?";
1028
+ }
1029
+ function Crest2({ url, name, kind }) {
1030
+ const cls = kind === "comp" ? "ss-w-bi-comp-logo" : "ss-w-bi-crest";
1031
+ if (url) return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: cls, style: { backgroundImage: `url(${url})` }, "aria-hidden": "true" });
1032
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: `${cls} ss-w-bi-crest-mono`, "aria-hidden": "true", children: initials2(name) });
1033
+ }
996
1034
  function StatsDisclosure({ it, ui }) {
997
1035
  const [open, setOpen] = (0, import_react5.useState)(false);
998
1036
  const hasScopes = !!it.scopes && it.scopes.length >= 2;
999
- const hasStats = hasScopes || it.stats.length > 0;
1037
+ const hasStats = hasStatsContent(it);
1000
1038
  if (!hasStats) return null;
1001
1039
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-bi-stats", children: [
1002
1040
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
@@ -1017,24 +1055,42 @@ function StatsDisclosure({ it, ui }) {
1017
1055
  }
1018
1056
  function InsightCard({ it, ui, slip }) {
1019
1057
  const live = it.isLive || it.phase === "live";
1058
+ const kickoff = it.kickoffUtc && ui?.bettingKickoff ? ui.bettingKickoff(it.kickoffUtc) : void 0;
1059
+ const hasNamedTeams = !!it.homeTeamName && !!it.awayTeamName;
1060
+ const showHeader = !!it.competition || live || !!kickoff;
1061
+ const showReason = !!it.text || hasStatsContent(it);
1020
1062
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-binsight", children: [
1021
- live || it.isMarketFloor ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-bi-status", children: [
1022
- live ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "ss-w-bi-live", children: [
1023
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-live-dot", "aria-hidden": "true" }),
1024
- ui?.liveLabel ?? "Live"
1025
- ] }) : null,
1026
- it.isMarketFloor ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-floor", children: ui?.marketFloorLabel ?? "Market price \xB7 not a value pick" }) : null
1027
- ] }) : null,
1028
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-bi-head", children: [
1029
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "ss-w-bi-mkt", children: [
1030
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-market", children: it.market }),
1031
- it.selection ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-sel", children: it.selection }) : null,
1032
- it.fixture || it.competition ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "ss-w-bi-match", children: [
1033
- it.fixture ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-fixture", children: it.fixture }) : null,
1034
- it.competition ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-comp", children: it.competition }) : null
1063
+ showHeader ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-bi-header", children: [
1064
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-header-left", children: it.competition ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
1065
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Crest2, { kind: "comp", url: it.competitionCrest, name: it.competition }),
1066
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-comp", children: it.competition })
1067
+ ] }) : null }),
1068
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "ss-w-bi-header-right", children: [
1069
+ live ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "ss-w-bi-live", children: [
1070
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-live-dot", "aria-hidden": "true" }),
1071
+ ui?.liveLabel ?? "Live"
1072
+ ] }) : null,
1073
+ kickoff ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "ss-w-bi-kick", children: [
1074
+ kickoff.dayLabel,
1075
+ " ",
1076
+ kickoff.time
1035
1077
  ] }) : null
1036
- ] }),
1078
+ ] })
1079
+ ] }) : null,
1080
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-bi-body", children: [
1081
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "ss-w-bi-fixture", children: hasNamedTeams ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
1082
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Crest2, { kind: "team", url: it.homeCrest, name: it.homeTeamName }),
1083
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-team", children: it.homeTeamName }),
1084
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-vs", "aria-hidden": "true", children: "vs" }),
1085
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-team", children: it.awayTeamName }),
1086
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Crest2, { kind: "team", url: it.awayCrest, name: it.awayTeamName })
1087
+ ] }) : it.fixture ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-fixture-txt", children: it.fixture }) : null }),
1037
1088
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-bi-action", children: [
1089
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "ss-w-bi-mkt", children: [
1090
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-market", children: it.market }),
1091
+ it.selection ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-sel", children: it.selection }) : null
1092
+ ] }),
1093
+ it.isMarketFloor ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-floor", children: ui?.marketFloorLabel ?? "Market price \xB7 not a value pick" }) : null,
1038
1094
  it.odds ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "ss-w-bi-odds", children: [
1039
1095
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-odds-cap", children: ui?.oddsLabel ?? "Odds" }),
1040
1096
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("b", { children: it.odds }),
@@ -1044,8 +1100,13 @@ function InsightCard({ it, ui, slip }) {
1044
1100
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(AddToSlipButton, { it, slip, ui })
1045
1101
  ] })
1046
1102
  ] }),
1047
- it.text ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "ss-w-bi-text", children: it.text }) : null,
1048
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(StatsDisclosure, { it, ui }),
1103
+ showReason ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-bi-reason", children: [
1104
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-chart", "aria-hidden": "true" }),
1105
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-bi-reason-body", children: [
1106
+ it.text ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "ss-w-bi-text", children: it.text }) : null,
1107
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(StatsDisclosure, { it, ui })
1108
+ ] })
1109
+ ] }) : null,
1049
1110
  it.disclaimer ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "ss-w-bi-disc", children: it.disclaimer }) : null
1050
1111
  ] });
1051
1112
  }
@@ -1233,13 +1294,13 @@ function GreetingHeaderBlock({ block }) {
1233
1294
  // src/components/club-dashboard/FixtureHeroBlock.tsx
1234
1295
  var import_react7 = require("react");
1235
1296
  var import_jsx_runtime17 = require("react/jsx-runtime");
1236
- function initials2(name) {
1297
+ function initials3(name) {
1237
1298
  return name.split(/\s+/).filter(Boolean).slice(0, 2).map((w) => w[0].toUpperCase()).join("");
1238
1299
  }
1239
- function Crest2({ url, name }) {
1300
+ function Crest3({ url, name }) {
1240
1301
  const [broken, setBroken] = (0, import_react7.useState)(false);
1241
1302
  if (!url || broken) {
1242
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "ss-w-cdash-crest ss-w-cdash-crest--fallback", "aria-hidden": "true", children: initials2(name) });
1303
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "ss-w-cdash-crest ss-w-cdash-crest--fallback", "aria-hidden": "true", children: initials3(name) });
1243
1304
  }
1244
1305
  return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("img", { className: "ss-w-cdash-crest", src: url, alt: "", "aria-hidden": "true", onError: () => setBroken(true) });
1245
1306
  }
@@ -1250,18 +1311,18 @@ function FixtureHeroBlock({
1250
1311
  }) {
1251
1312
  return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "ss-w-cdash-fixture", children: [
1252
1313
  /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "ss-w-cdash-tie", children: [
1253
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Crest2, { url: block.homeCrestUrl, name: block.homeName }),
1314
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Crest3, { url: block.homeCrestUrl, name: block.homeName }),
1254
1315
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "ss-w-cdash-team", children: block.homeName }),
1255
1316
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "ss-w-cdash-vs", children: "v" }),
1256
1317
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "ss-w-cdash-team", children: block.awayName }),
1257
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Crest2, { url: block.awayCrestUrl, name: block.awayName })
1318
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Crest3, { url: block.awayCrestUrl, name: block.awayName })
1258
1319
  ] }),
1259
1320
  /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "ss-w-cdash-fixture-meta", children: [
1260
1321
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "ss-w-cdash-kickoff", children: ui.clubKickoff(block.kickoffUnix * 1e3) }),
1261
1322
  block.venue ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "ss-w-cdash-venue", children: block.venue }) : null,
1262
1323
  block.competition ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "ss-w-cdash-comp", children: block.competition }) : null
1263
1324
  ] }),
1264
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", className: "ss-w-cdash-cta", onClick: () => onAction(block.ctaActionValue, block.ctaLabel), children: block.ctaLabel })
1325
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", className: "ss-w-cdash-cta", onClick: () => onAction(block.ctaActionValue, void 0, block.ctaLabel), children: block.ctaLabel })
1265
1326
  ] });
1266
1327
  }
1267
1328
 
@@ -1285,11 +1346,28 @@ function MatchPreviewBlock({
1285
1346
  ui,
1286
1347
  onAction
1287
1348
  }) {
1288
- if (block.lines.length === 0) return null;
1349
+ if (block.facts.length === 0) return null;
1289
1350
  const heading = ui.clubMatchPreviewHeading;
1290
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("button", { type: "button", className: "ss-w-cdash-lines", onClick: () => onAction(block.actionValue, heading), children: [
1351
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "ss-w-cdash-lines", children: [
1291
1352
  /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "ss-w-cdash-lines-heading", children: heading }),
1292
- block.lines.map((line, i) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { children: line }, i))
1353
+ block.facts.map(
1354
+ (f, i) => f.entityRef ? /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
1355
+ "button",
1356
+ {
1357
+ type: "button",
1358
+ className: "ss-w-cdash-fact",
1359
+ onClick: () => onAction(f.question ?? f.sentence, { ...f.entityRef, subject: f.sentence, personaHint: "club" }),
1360
+ children: [
1361
+ f.sentence,
1362
+ f.label ? /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("span", { className: "ss-w-cdash-fact-label", children: [
1363
+ " ",
1364
+ f.label
1365
+ ] }) : null
1366
+ ]
1367
+ },
1368
+ i
1369
+ ) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { children: f.sentence }, i)
1370
+ )
1293
1371
  ] });
1294
1372
  }
1295
1373
 
@@ -1300,11 +1378,28 @@ function TeamDnaBlock({
1300
1378
  ui,
1301
1379
  onAction
1302
1380
  }) {
1303
- if (block.lines.length === 0) return null;
1381
+ if (block.facts.length === 0) return null;
1304
1382
  const heading = ui.clubTeamDnaHeading;
1305
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("button", { type: "button", className: "ss-w-cdash-lines", onClick: () => onAction(block.actionValue, heading), children: [
1383
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "ss-w-cdash-lines", children: [
1306
1384
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "ss-w-cdash-lines-heading", children: heading }),
1307
- block.lines.map((line, i) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { children: line }, i))
1385
+ block.facts.map(
1386
+ (f, i) => f.entityRef ? /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
1387
+ "button",
1388
+ {
1389
+ type: "button",
1390
+ className: "ss-w-cdash-fact",
1391
+ onClick: () => onAction(f.question ?? f.sentence, { ...f.entityRef, subject: f.sentence, personaHint: "club" }),
1392
+ children: [
1393
+ f.sentence,
1394
+ f.label ? /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("span", { className: "ss-w-cdash-fact-label", children: [
1395
+ " ",
1396
+ f.label
1397
+ ] }) : null
1398
+ ]
1399
+ },
1400
+ i
1401
+ ) : /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { children: f.sentence }, i)
1402
+ )
1308
1403
  ] });
1309
1404
  }
1310
1405
 
@@ -1439,7 +1534,7 @@ function MessageList({ controller, ui, slip, onDashboardTap }) {
1439
1534
  send(tile.actionValue, { displayText: tile.title });
1440
1535
  if (m.role === "assistant" && m.greetingDashboard) onDashboardTap?.(tile, index, m.greetingDashboard.layout);
1441
1536
  },
1442
- onClubAction: (actionValue, displayText) => send(actionValue, { displayText }),
1537
+ onClubAction: (message, grounding, displayText) => send(message, { grounding, displayText: displayText ?? message }),
1443
1538
  ui,
1444
1539
  slip
1445
1540
  },
@@ -1648,26 +1743,51 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
1648
1743
 
1649
1744
  /* betting insights */
1650
1745
  .ss-w-binsights { display: flex; flex-direction: column; gap: 7px; }
1651
- .ss-w-binsight { background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); border-radius: 12px; border-left: 3px solid var(--ss-w-accent); padding: 9px 11px; display: flex; flex-direction: column; gap: 6px; }
1652
- .ss-w-bi-status { display: flex; align-items: center; gap: 6px; min-width: 0; }
1653
- .ss-w-bi-fixture { font-size: 11px; color: var(--ss-w-ink); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1654
- .ss-w-bi-comp { font-size: 10.5px; color: var(--ss-w-faint); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1746
+ .ss-w-binsight { background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); border-radius: 12px; border-left: 3px solid var(--ss-w-accent); padding: 9px 11px; display: flex; flex-direction: column; gap: 8px; }
1747
+
1748
+ /* row 1: competition logo + name on the left, live badge + kickoff on the right */
1749
+ .ss-w-bi-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; padding-bottom: 7px; border-bottom: 1px solid var(--ss-w-line); }
1750
+ .ss-w-bi-header-left { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
1751
+ .ss-w-bi-header-right { display: inline-flex; align-items: center; gap: 8px; flex: none; }
1752
+ .ss-w-bi-comp { font-size: 10.5px; color: var(--ss-w-faint); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1753
+ .ss-w-bi-kick { font-size: 10.5px; font-weight: 600; color: var(--ss-w-faint); white-space: nowrap; flex: none; }
1655
1754
  .ss-w-bi-live { font-size: 8.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: #fff; background: #c0392b; border-radius: 4px; padding: 1px 5px; display: inline-flex; align-items: center; gap: 4px; flex: none; }
1656
1755
  .ss-w-bi-live-dot { width: 5px; height: 5px; border-radius: 50%; background: #fff; }
1657
- .ss-w-bi-floor { font-size: 8.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--ss-w-faint); background: var(--ss-w-bubble); border: 1px solid var(--ss-w-line); border-radius: 4px; padding: 1px 5px; display: inline-flex; align-items: center; flex: none; }
1658
- .ss-w-bi-head { display: flex; align-items: flex-start; gap: 10px; }
1659
- .ss-w-bi-mkt { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
1660
- .ss-w-bi-market { font-size: 15px; font-weight: 700; line-height: 1.25; color: var(--ss-w-ink); }
1661
- .ss-w-bi-sel { font-size: 12px; line-height: 1.3; color: var(--ss-w-faint); }
1662
- .ss-w-bi-match { display: flex; flex-direction: column; gap: 1px; min-width: 0; margin-top: 5px; }
1663
- .ss-w-bi-action { margin-left: auto; flex: none; display: flex; flex-direction: column; align-items: stretch; gap: 6px; }
1756
+ .ss-w-bi-floor { font-size: 8.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--ss-w-faint); background: var(--ss-w-bubble); border: 1px solid var(--ss-w-line); border-radius: 4px; padding: 1px 5px; display: inline-flex; align-items: center; align-self: flex-start; flex: none; }
1757
+
1758
+ /* crests -- CSS background-image on a span, never <img> (matches .ss-w-ecard-crest precedent) */
1759
+ .ss-w-bi-comp-logo { width: 15px; height: 15px; flex: none; display: block; border-radius: 50%; background-size: contain; background-repeat: no-repeat; background-position: center; }
1760
+ .ss-w-bi-crest { width: 22px; height: 22px; flex: none; display: block; border-radius: 50%; background-size: contain; background-repeat: no-repeat; background-position: center; background-color: var(--ss-w-bubble); border: 1px solid var(--ss-w-line); }
1761
+ .ss-w-bi-crest-mono { display: grid; place-items: center; font-size: 9px; font-weight: 700; color: var(--ss-w-accent); background: color-mix(in srgb, var(--ss-w-accent) 16%, var(--ss-w-panel)); }
1762
+
1763
+ /* row 2: crest-flanked fixture on the left, boxed action panel on the right */
1764
+ .ss-w-bi-body { display: flex; align-items: center; gap: 10px; }
1765
+ .ss-w-bi-fixture { display: flex; align-items: center; gap: 6px; min-width: 0; flex: 1; font-size: 12px; font-weight: 650; color: var(--ss-w-ink); }
1766
+ .ss-w-bi-team { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1767
+ .ss-w-bi-vs { font-size: 10px; font-weight: 600; color: var(--ss-w-faint); flex: none; }
1768
+ .ss-w-bi-fixture-txt { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1769
+ .ss-w-bi-action { margin-left: auto; flex: none; display: flex; flex-direction: column; align-items: stretch; gap: 6px; background: var(--ss-w-bubble); border: 1px solid var(--ss-w-line); border-radius: 12px; padding: 8px 10px; }
1664
1770
  .ss-w-bi-action .ss-w-slip > button { flex: 1; text-align: center; }
1665
1771
  /* The action column is narrow; let its toast size to its own text and anchor to the card's right edge instead of being clipped to the button width. */
1666
1772
  .ss-w-bi-action .ss-w-toast { left: auto; right: 0; width: max-content; max-width: 210px; white-space: normal; }
1773
+ .ss-w-bi-mkt { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
1774
+ .ss-w-bi-market { font-size: 13.5px; font-weight: 700; line-height: 1.25; color: var(--ss-w-ink); }
1775
+ .ss-w-bi-sel { font-size: 11.5px; line-height: 1.3; color: var(--ss-w-faint); }
1667
1776
  .ss-w-bi-odds { flex: none; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 1px; background: color-mix(in srgb, var(--ss-w-accent) 12%, var(--ss-w-panel)); border: 1px solid color-mix(in srgb, var(--ss-w-accent) 30%, var(--ss-w-line)); border-radius: 10px; padding: 4px 11px; min-width: 62px; }
1668
1777
  .ss-w-bi-odds-cap { font-size: 9px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: var(--ss-w-faint); }
1669
1778
  .ss-w-bi-odds b { font-size: 19px; font-family: ui-monospace, monospace; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--ss-w-accent); line-height: 1.1; }
1670
1779
  .ss-w-bi-book { font-size: 9.5px; color: var(--ss-w-faint); }
1780
+
1781
+ /* row 3: bar-chart glyph + reasoning prose + stats-expander chevron, in a subtle strip */
1782
+ .ss-w-bi-reason { display: flex; align-items: flex-start; gap: 8px; background: var(--ss-w-bubble); border-radius: 10px; padding: 8px 10px; }
1783
+ .ss-w-bi-chart { flex: none; width: 13px; height: 12px; margin-top: 2px; opacity: .55;
1784
+ background:
1785
+ linear-gradient(var(--ss-w-accent), var(--ss-w-accent)) left / 3px 55% no-repeat,
1786
+ linear-gradient(var(--ss-w-accent), var(--ss-w-accent)) center / 3px 100% no-repeat,
1787
+ linear-gradient(var(--ss-w-accent), var(--ss-w-accent)) right / 3px 75% no-repeat;
1788
+ background-position: left bottom, center bottom, right bottom;
1789
+ }
1790
+ .ss-w-bi-reason-body { display: flex; flex-direction: column; gap: 8px; min-width: 0; flex: 1; }
1671
1791
  .ss-w-bi-text { font-size: 12px; line-height: 1.45; color: var(--ss-w-ink); }
1672
1792
  .ss-w-bi-stats { display: flex; flex-direction: column; gap: 8px; }
1673
1793
  .ss-w-bi-toggle { align-self: flex-start; display: inline-flex; align-items: center; gap: 5px; background: none; border: none; color: var(--ss-w-accent); font: inherit; font-size: 11.5px; font-weight: 600; padding: 2px 0; cursor: pointer; }
@@ -1864,9 +1984,12 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
1864
1984
  .ss-w-cdash-pill--d { background: var(--ss-w-faint); }
1865
1985
  .ss-w-cdash-pill--l { background: color-mix(in srgb, var(--ss-w-neg) 88%, black); }
1866
1986
 
1867
- .ss-w-cdash-lines { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; width: 100%; text-align: left; background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); border-left: 3px solid var(--ss-w-accent); border-radius: 12px; padding: 10px 12px; font: inherit; cursor: pointer; }
1987
+ .ss-w-cdash-lines { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; width: 100%; text-align: left; background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); border-left: 3px solid var(--ss-w-accent); border-radius: 12px; padding: 10px 12px; }
1868
1988
  .ss-w-cdash-lines-heading { font-size: 12px; font-weight: 700; color: var(--ss-w-accent); }
1869
- .ss-w-cdash-lines p { font-size: 12px; line-height: 1.4; color: var(--ss-w-ink); }
1989
+ .ss-w-cdash-lines p { font-size: 12px; line-height: 1.4; color: var(--ss-w-ink); margin: 0; }
1990
+ .ss-w-cdash-fact { display: block; width: 100%; text-align: left; font: inherit; font-size: 12px; line-height: 1.4; color: var(--ss-w-ink); background: none; border: none; padding: 0; cursor: pointer; }
1991
+ .ss-w-cdash-fact:hover, .ss-w-cdash-fact:focus-visible { color: var(--ss-w-accent); text-decoration: underline; }
1992
+ .ss-w-cdash-fact-label { font-size: 11px; font-weight: 700; color: var(--ss-w-accent); }
1870
1993
 
1871
1994
  .ss-w-cdash-cd { display: flex; flex-direction: column; align-items: center; gap: 4px; background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); border-radius: 12px; padding: 10px 12px; text-align: center; }
1872
1995
  .ss-w-cdash-cd-clock { display: flex; align-items: baseline; justify-content: center; gap: 2px; font-variant-numeric: tabular-nums; font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; font-weight: 700; font-size: 18px; color: var(--ss-w-ink); }