@sensiblestats/widget-react 0.4.0 → 0.5.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,20 @@
1
1
  # @sensiblestats/widget-react
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Redesigned betting-insight card with scoped supporting stats.
8
+
9
+ - **widget-sdk:** extend `BettingInsight` with `homeTeamName`, `awayTeamName`, and `scopes` (`{ key, label, stats }[]`), so consumers can type the fixture line and the per-scope stat sets (this league / all competitions / head-to-head) the server now streams.
10
+ - **widget-react:** rebuild the betting card around the new payload — a phase-aware live/pre-match status line with the fixture, a promoted market with clearly labeled odds (and an optional bookmaker name), and scope chips that switch the supporting stats in place (shown only when ≥2 scopes are present, otherwise flat stat rows with a "show more" expander). The responsible-gambling disclaimer always renders. Type scale and spacing match the surrounding chat cards.
11
+ - **widget-embed:** rebuild against `@sensiblestats/widget-react` so the hosted embed renders the redesigned card.
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies
16
+ - @sensiblestats/widget-sdk@0.4.0
17
+
3
18
  ## 0.4.0
4
19
 
5
20
  ### Minor Changes
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_react6 = require("react");
39
+ var import_react7 = require("react");
40
40
  var import_widget_sdk3 = require("@sensiblestats/widget-sdk");
41
41
 
42
42
  // src/i18n.ts
@@ -52,7 +52,9 @@ var STRINGS = {
52
52
  somethingWentWrong: "Something went wrong.",
53
53
  retry: "Retry",
54
54
  showLess: "Show less",
55
- showMore: (n) => `Show ${n} more`
55
+ showMore: (n) => `Show ${n} more`,
56
+ oddsLabel: "Odds",
57
+ liveLabel: "Live"
56
58
  },
57
59
  tr: {
58
60
  launcherLabel: "SensibleStats ile sohbet et",
@@ -65,7 +67,9 @@ var STRINGS = {
65
67
  somethingWentWrong: "Bir \u015Feyler ters gitti.",
66
68
  retry: "Tekrar dene",
67
69
  showLess: "Daha az g\xF6ster",
68
- showMore: (n) => `${n} tane daha g\xF6ster`
70
+ showMore: (n) => `${n} tane daha g\xF6ster`,
71
+ oddsLabel: "Oran",
72
+ liveLabel: "Canl\u0131"
69
73
  }
70
74
  };
71
75
  function resolveLocaleStrings(locale) {
@@ -95,7 +99,9 @@ function normalizeUi(cfg) {
95
99
  somethingWentWrong: strings.somethingWentWrong,
96
100
  retry: strings.retry,
97
101
  showLess: strings.showLess,
98
- showMore: strings.showMore
102
+ showMore: strings.showMore,
103
+ oddsLabel: strings.oddsLabel,
104
+ liveLabel: strings.liveLabel
99
105
  };
100
106
  }
101
107
  function toClientOptions(cfg) {
@@ -175,6 +181,10 @@ function toInsightVM(insight) {
175
181
  const selection = [str(insight.selectionLabel), str(insight.line)].filter(Boolean).join(" ") || void 0;
176
182
  const value = insight.odds?.value;
177
183
  const odds = typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : void 0;
184
+ const home = str(insight.homeTeamName);
185
+ const away = str(insight.awayTeamName);
186
+ const fixture = home && away ? `${home} vs ${away}` : void 0;
187
+ const scopes = Array.isArray(insight.scopes) ? insight.scopes.map((s) => ({ key: str(s.key), label: str(s.label), stats: readStatCells(s.stats) })).filter((s) => !!s.key && !!s.label) : void 0;
178
188
  return {
179
189
  id: str(insight.insightId) ?? `${str(insight.ruleId) ?? "insight"}:${num(insight.matchId) ?? ""}`,
180
190
  market,
@@ -183,6 +193,9 @@ function toInsightVM(insight) {
183
193
  // The wire payload carries bookmakerId (not a name); render a name only if one is present.
184
194
  bookmaker: str(insight.odds?.bookmakerName),
185
195
  isLive: insight.odds?.isLive === true,
196
+ phase: str(insight.phase),
197
+ fixture,
198
+ scopes,
186
199
  text: str(insight.text) ?? "",
187
200
  stats: readStatCells(insight.stats),
188
201
  disclaimer: str(insight.disclaimer)
@@ -369,7 +382,7 @@ function ChatFab({ label, greeting, popupText, onOpen, onDismissPopup, dismissLa
369
382
  }
370
383
 
371
384
  // src/components/ChatPanel.tsx
372
- var import_react5 = require("react");
385
+ var import_react6 = require("react");
373
386
 
374
387
  // src/components/ChatHeader.tsx
375
388
  var import_jsx_runtime4 = require("react/jsx-runtime");
@@ -382,7 +395,7 @@ function ChatHeader({ onClose, closeLabel = "Close chat" }) {
382
395
  }
383
396
 
384
397
  // src/components/MessageList.tsx
385
- var import_react3 = require("react");
398
+ var import_react4 = require("react");
386
399
 
387
400
  // src/components/Markdown.tsx
388
401
  var import_react_markdown = __toESM(require("react-markdown"), 1);
@@ -436,28 +449,59 @@ function EntityCardList({ cards, disabled, showLessLabel = "Show less", showMore
436
449
  }
437
450
 
438
451
  // src/components/BettingInsightList.tsx
452
+ var import_react3 = require("react");
439
453
  var import_jsx_runtime7 = require("react/jsx-runtime");
440
- function BettingInsightList({ insights }) {
441
- if (!insights || insights.length === 0) return null;
442
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "ss-w-binsights", children: insights.map((it) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-binsight", children: [
454
+ var MAX_ROWS = 3;
455
+ function StatRows({ stats, ui }) {
456
+ const [expanded, setExpanded] = (0, import_react3.useState)(false);
457
+ const shown = expanded ? stats : stats.slice(0, MAX_ROWS);
458
+ const overflow = stats.length - MAX_ROWS;
459
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-rows", children: [
460
+ shown.map((s) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-row", children: [
461
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-row-lbl", children: s.label }),
462
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-row-dots", "aria-hidden": "true" }),
463
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-row-num", children: s.value })
464
+ ] }, `${s.label}:${s.value}`)),
465
+ overflow > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "ss-w-bi-more", onClick: () => setExpanded((v) => !v), children: expanded ? ui?.showLess ?? "Show less" : (ui?.showMore ?? ((n) => `Show ${n} more`))(overflow) }) : null
466
+ ] });
467
+ }
468
+ function ScopedStats({ scopes, ui }) {
469
+ const [active, setActive] = (0, import_react3.useState)(0);
470
+ const current = scopes[active] ?? scopes[0] ?? { key: "", label: "", stats: [] };
471
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-scoped", children: [
472
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "ss-w-bi-scopes", role: "group", children: scopes.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "ss-w-bi-chip", "aria-pressed": i === active, onClick: () => setActive(i), children: s.label }, s.key)) }),
473
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(StatRows, { stats: current.stats, ui }, current.key)
474
+ ] });
475
+ }
476
+ function InsightCard({ it, ui }) {
477
+ const live = it.isLive || it.phase === "live";
478
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-binsight", children: [
479
+ live || it.fixture ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-status", children: [
480
+ live ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "ss-w-bi-live", children: [
481
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-live-dot", "aria-hidden": "true" }),
482
+ ui?.liveLabel ?? "Live"
483
+ ] }) : null,
484
+ it.fixture ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-fixture", children: it.fixture }) : null
485
+ ] }) : null,
443
486
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-head", children: [
444
487
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "ss-w-bi-mkt", children: [
445
488
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-market", children: it.market }),
446
489
  it.selection ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-sel", children: it.selection }) : null
447
490
  ] }),
448
491
  it.odds ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "ss-w-bi-odds", children: [
449
- it.isLive ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-live", children: "Live" }) : null,
492
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-odds-cap", children: ui?.oddsLabel ?? "Odds" }),
450
493
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("b", { children: it.odds }),
451
494
  it.bookmaker ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-book", children: it.bookmaker }) : null
452
495
  ] }) : null
453
496
  ] }),
454
497
  it.text ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "ss-w-bi-text", children: it.text }) : null,
455
- it.stats.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "ss-w-bi-stats", children: it.stats.map((s) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "ss-w-bi-stat", children: [
456
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("b", { children: s.value }),
457
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: s.label })
458
- ] }, `${s.label}:${s.value}`)) }) : null,
498
+ it.scopes && it.scopes.length >= 2 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ScopedStats, { scopes: it.scopes, ui }) : it.stats.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(StatRows, { stats: it.stats, ui }) : null,
459
499
  it.disclaimer ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "ss-w-bi-disc", children: it.disclaimer }) : null
460
- ] }, it.id)) });
500
+ ] });
501
+ }
502
+ function BettingInsightList({ insights, ui }) {
503
+ if (!insights || insights.length === 0) return null;
504
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "ss-w-binsights", children: insights.map((it) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(InsightCard, { it, ui }, it.id)) });
461
505
  }
462
506
 
463
507
  // src/components/ActionButtons.tsx
@@ -482,7 +526,7 @@ function ChatMessage({ message, disabled, onAct, ui }) {
482
526
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "ss-w-msg ss-w-bot", children: [
483
527
  message.text ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Markdown, { text: message.text }) : null,
484
528
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
485
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(BettingInsightList, { insights: message.insights }),
529
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(BettingInsightList, { insights: message.insights, ui }),
486
530
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
487
531
  ] });
488
532
  }
@@ -504,10 +548,10 @@ function isPinnedToBottom(el, threshold = PIN_THRESHOLD) {
504
548
  var import_jsx_runtime11 = require("react/jsx-runtime");
505
549
  function MessageList({ controller, ui }) {
506
550
  const { state, send } = controller;
507
- const listRef = (0, import_react3.useRef)(null);
508
- const endRef = (0, import_react3.useRef)(null);
509
- const pinnedRef = (0, import_react3.useRef)(true);
510
- (0, import_react3.useEffect)(() => {
551
+ const listRef = (0, import_react4.useRef)(null);
552
+ const endRef = (0, import_react4.useRef)(null);
553
+ const pinnedRef = (0, import_react4.useRef)(true);
554
+ (0, import_react4.useEffect)(() => {
511
555
  if (pinnedRef.current) endRef.current?.scrollIntoView({ block: "end" });
512
556
  }, [state.messages, state.status]);
513
557
  const empty = state.messages.length === 0;
@@ -546,10 +590,10 @@ function MessageList({ controller, ui }) {
546
590
  }
547
591
 
548
592
  // src/components/ChatInput.tsx
549
- var import_react4 = require("react");
593
+ var import_react5 = require("react");
550
594
  var import_jsx_runtime12 = require("react/jsx-runtime");
551
595
  function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop response", sendLabel = "Send message" }) {
552
- const [value, setValue] = (0, import_react4.useState)("");
596
+ const [value, setValue] = (0, import_react5.useState)("");
553
597
  const submit = () => {
554
598
  const t = value.trim();
555
599
  if (!t) return;
@@ -572,8 +616,8 @@ function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop r
572
616
  var import_jsx_runtime13 = require("react/jsx-runtime");
573
617
  var FULLSCREEN_QUERY = "(max-width: 639px)";
574
618
  function useIsModal() {
575
- const [modal, setModal] = (0, import_react5.useState)(false);
576
- (0, import_react5.useEffect)(() => {
619
+ const [modal, setModal] = (0, import_react6.useState)(false);
620
+ (0, import_react6.useEffect)(() => {
577
621
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
578
622
  const mql = window.matchMedia(FULLSCREEN_QUERY);
579
623
  const sync = () => setModal(mql.matches);
@@ -586,7 +630,7 @@ function useIsModal() {
586
630
  function ChatPanel({ controller, ui }) {
587
631
  const { state, close, stop, send } = controller;
588
632
  const isModal = useIsModal();
589
- (0, import_react5.useEffect)(() => {
633
+ (0, import_react6.useEffect)(() => {
590
634
  const onKey = (e) => {
591
635
  if (e.key === "Escape") close();
592
636
  };
@@ -698,19 +742,32 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
698
742
  /* betting insights */
699
743
  .ss-w-binsights { display: flex; flex-direction: column; gap: 7px; }
700
744
  .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; }
745
+ .ss-w-bi-status { display: flex; align-items: center; gap: 6px; min-width: 0; }
746
+ .ss-w-bi-fixture { font-size: 11px; color: var(--ss-w-ink); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
747
+ .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; }
748
+ .ss-w-bi-live-dot { width: 5px; height: 5px; border-radius: 50%; background: #fff; }
701
749
  .ss-w-bi-head { display: flex; align-items: flex-start; gap: 10px; }
702
- .ss-w-bi-mkt { display: flex; flex-direction: column; min-width: 0; }
703
- .ss-w-bi-market { font-size: 8.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--ss-w-faint); line-height: 1.3; }
704
- .ss-w-bi-sel { font-weight: 660; }
705
- .ss-w-bi-odds { margin-left: auto; display: flex; align-items: center; gap: 6px; flex: none; }
706
- .ss-w-bi-odds b { font-size: 15px; font-variant-numeric: tabular-nums; color: var(--ss-w-accent); }
707
- .ss-w-bi-book { font-size: 10px; color: var(--ss-w-faint); }
708
- .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; }
750
+ .ss-w-bi-mkt { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
751
+ .ss-w-bi-market { font-size: 13px; font-weight: 660; line-height: 1.3; color: var(--ss-w-ink); }
752
+ .ss-w-bi-sel { font-size: 11px; line-height: 1.3; color: var(--ss-w-faint); }
753
+ .ss-w-bi-odds { margin-left: auto; flex: none; display: flex; flex-direction: column; align-items: center; justify-content: center; 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) 24%, var(--ss-w-line)); border-radius: 8px; padding: 3px 9px; min-width: 56px; }
754
+ .ss-w-bi-odds-cap { font-size: 8px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: var(--ss-w-faint); }
755
+ .ss-w-bi-odds b { font-size: 15px; font-family: ui-monospace, monospace; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--ss-w-accent); line-height: 1.15; }
756
+ .ss-w-bi-book { font-size: 9.5px; color: var(--ss-w-faint); }
709
757
  .ss-w-bi-text { font-size: 12px; color: var(--ss-w-ink); }
710
- .ss-w-bi-stats { display: flex; flex-wrap: wrap; gap: 12px; }
711
- .ss-w-bi-stat b { display: block; font-variant-numeric: tabular-nums; font-weight: 660; }
712
- .ss-w-bi-stat span { font-size: 9px; text-transform: uppercase; color: var(--ss-w-faint); }
758
+ .ss-w-bi-rows { display: flex; flex-direction: column; }
759
+ .ss-w-bi-row { display: flex; align-items: baseline; gap: 8px; padding: 4px 0; border-top: 1px solid var(--ss-w-line); }
760
+ .ss-w-bi-row:first-child { border-top: none; }
761
+ .ss-w-bi-row-lbl { font-size: 11px; color: var(--ss-w-faint); min-width: 0; }
762
+ .ss-w-bi-row-dots { flex: 1; border-bottom: 1px dotted var(--ss-w-line); transform: translateY(-3px); min-width: 12px; }
763
+ .ss-w-bi-row-num { font-family: ui-monospace, monospace; font-weight: 700; font-variant-numeric: tabular-nums; font-size: 12.5px; flex: none; color: var(--ss-w-ink); }
764
+ .ss-w-bi-more { align-self: flex-start; background: none; border: none; color: var(--ss-w-accent); font: inherit; font-size: 11px; font-weight: 560; padding: 2px 0; cursor: pointer; }
765
+ .ss-w-bi-more:hover { text-decoration: underline; }
713
766
  .ss-w-bi-disc { font-size: 10px; font-style: italic; color: var(--ss-w-faint); }
767
+ .ss-w-bi-scoped { display: flex; flex-direction: column; gap: 6px; }
768
+ .ss-w-bi-scopes { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
769
+ .ss-w-bi-chip { font-size: 11px; font-weight: 550; border: 1px solid var(--ss-w-line); border-radius: 14px; padding: 3px 10px; color: var(--ss-w-ink); background: var(--ss-w-bubble); cursor: pointer; }
770
+ .ss-w-bi-chip[aria-pressed="true"] { color: #fff; background: var(--ss-w-accent); border-color: var(--ss-w-accent); }
714
771
 
715
772
  /* action buttons */
716
773
  .ss-w-actions { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
@@ -766,11 +823,11 @@ function injectStyles(root) {
766
823
  function StatsWidget(props) {
767
824
  const injectedClient = "client" in props ? props.client : void 0;
768
825
  const cfg = props.config;
769
- const built = (0, import_react6.useMemo)(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
770
- const ui = (0, import_react6.useMemo)(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
771
- const rootRef = (0, import_react6.useRef)(null);
826
+ const built = (0, import_react7.useMemo)(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
827
+ const ui = (0, import_react7.useMemo)(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
828
+ const rootRef = (0, import_react7.useRef)(null);
772
829
  const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters, built ? ui.greetingMessage : void 0);
773
- (0, import_react6.useEffect)(() => {
830
+ (0, import_react7.useEffect)(() => {
774
831
  if (!built || !ui.injectStyles) return;
775
832
  const node = rootRef.current?.getRootNode();
776
833
  if (node) injectStyles(node);