@sensiblestats/widget-react 0.2.1 → 0.4.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,22 @@
1
1
  # @sensiblestats/widget-react
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Render `betting_insight` events as insight cards (market, selection + line, odds with an optional live badge, supporting stats, and the responsible-gambling disclaimer). **Bug fix:** the reducer previously had no case for `betting_insight` and no fallthrough, so any unmodelled event returned `undefined` and corrupted state — the next event then crashed the widget (`Cannot read properties of undefined (reading 'length')`). Unknown event types are now ignored safely and can never break the UI.
8
+
9
+ ## 0.3.0
10
+
11
+ ### Minor Changes
12
+
13
+ - Entity cards are no longer clickable — they render as static info cards
14
+ (planned to return later with proper question composition). Fix iOS Safari
15
+ zooming on input focus (textarea font-size ≥ 16px on touch). Fix the message
16
+ list yanking the reader to the bottom when late content (suggested actions)
17
+ arrives after they scrolled up — auto-scroll now follows only while pinned to
18
+ the bottom. Also ships the i18n/localized labels and logo refactor from master.
19
+
3
20
  ## 0.2.0
4
21
 
5
22
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -132,16 +132,19 @@ function str(v) {
132
132
  function num(v) {
133
133
  return typeof v === "number" && Number.isFinite(v) ? String(v) : void 0;
134
134
  }
135
- function readStats(card) {
136
- if (!Array.isArray(card.stats)) return [];
135
+ function readStatCells(raw) {
136
+ if (!Array.isArray(raw)) return [];
137
137
  const cells = [];
138
- for (const entry of card.stats) {
138
+ for (const entry of raw) {
139
139
  const label = str(entry.label);
140
140
  const value = num(entry.value) ?? str(entry.value);
141
141
  if (label !== void 0 && value !== void 0) cells.push({ label, value });
142
142
  }
143
143
  return cells;
144
144
  }
145
+ function readStats(card) {
146
+ return readStatCells(card.stats);
147
+ }
145
148
  function toCardVM(card) {
146
149
  const entityType = str(card.entityType)?.toLowerCase();
147
150
  const kind = entityType === "player" ? "player" : entityType === "team" ? "team" : "generic";
@@ -151,8 +154,7 @@ function toCardVM(card) {
151
154
  name: card.canonicalName,
152
155
  image: str(card.imagePath) ?? str(card.imageUrl) ?? str(card.image_url) ?? str(card.logoUrl) ?? str(card.logo),
153
156
  sub: str(card.subtitle) ?? str(card.teamName) ?? str(card.leagueName) ?? str(card.position),
154
- stats: readStats(card),
155
- query: card.canonicalName
157
+ stats: readStats(card)
156
158
  };
157
159
  }
158
160
  function toActionVM(action) {
@@ -168,6 +170,24 @@ function toChipVM(intent) {
168
170
  function chipFromText(text) {
169
171
  return { text };
170
172
  }
173
+ function toInsightVM(insight) {
174
+ const market = str(insight.marketLabel) ?? str(insight.marketDeveloperName) ?? "";
175
+ const selection = [str(insight.selectionLabel), str(insight.line)].filter(Boolean).join(" ") || void 0;
176
+ const value = insight.odds?.value;
177
+ const odds = typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : void 0;
178
+ return {
179
+ id: str(insight.insightId) ?? `${str(insight.ruleId) ?? "insight"}:${num(insight.matchId) ?? ""}`,
180
+ market,
181
+ selection,
182
+ odds,
183
+ // The wire payload carries bookmakerId (not a name); render a name only if one is present.
184
+ bookmaker: str(insight.odds?.bookmakerName),
185
+ isLive: insight.odds?.isLive === true,
186
+ text: str(insight.text) ?? "",
187
+ stats: readStatCells(insight.stats),
188
+ disclaimer: str(insight.disclaimer)
189
+ };
190
+ }
171
191
 
172
192
  // src/reducer.ts
173
193
  var POPUP_MAX = 160;
@@ -194,6 +214,8 @@ function applyEvent(state, event) {
194
214
  return { ...state, messages: mapActive(state, (t) => ({ ...t, cards: [...t.cards, toCardVM(event.data.card)] })) };
195
215
  case "action_button":
196
216
  return { ...state, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, actions: [...t.actions, toActionVM(event.data.action)] })) };
217
+ case "betting_insight":
218
+ return { ...state, messages: mapActive(state, (t) => ({ ...t, insights: [...t.insights, toInsightVM(event.data.insight)] })) };
197
219
  case "intent_chip": {
198
220
  const chip = toChipVM(event.data.intent);
199
221
  return chip ? { ...state, starters: [...state.starters, chip] } : state;
@@ -211,6 +233,7 @@ function applyEvent(state, event) {
211
233
  return { ...state, error: suppress ? state.error : event.data, isStreaming: false, status: null, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, done: true })) };
212
234
  }
213
235
  }
236
+ return state;
214
237
  }
215
238
  function reducer(state, action) {
216
239
  switch (action.kind) {
@@ -220,11 +243,11 @@ function reducer(state, action) {
220
243
  return { ...state, isOpen: false, isStreaming: false, status: null, actionsLoading: false };
221
244
  case "USER_SENT": {
222
245
  const user = { role: "user", id: action.userId, text: action.displayText ?? action.input.message };
223
- const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], done: false };
246
+ const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], done: false };
224
247
  return { ...state, messages: [...state.messages, user, assistant], isStreaming: true, status: null, actionsLoading: false, starters: [], error: null, lastUserInput: action.input, lastDisplayText: action.displayText ?? null };
225
248
  }
226
249
  case "GREETING_SENT": {
227
- const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], done: false, isGreeting: true };
250
+ const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], done: false, isGreeting: true };
228
251
  return { ...state, messages: [...state.messages, assistant], isStreaming: true, status: null, error: null };
229
252
  }
230
253
  case "DISMISS_POPUP":
@@ -325,9 +348,6 @@ function SendIcon() {
325
348
  function CloseIcon() {
326
349
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", width: "1em", height: "1em", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { fill: "none", d: "M18 6L6 18M6 6l12 12", stroke: "currentColor", strokeWidth: "2" }) });
327
350
  }
328
- function ChevronIcon() {
329
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", width: "1em", height: "1em", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", d: "M9 6l6 6-6 6" }) });
330
- }
331
351
 
332
352
  // src/components/GreetingBubble.tsx
333
353
  var import_jsx_runtime2 = require("react/jsx-runtime");
@@ -386,13 +406,13 @@ var MAX_VISIBLE = 3;
386
406
  function initials(name) {
387
407
  return name.split(/\s+/).slice(0, 2).map((w) => w[0] ?? "").join("").toUpperCase();
388
408
  }
389
- function EntityCardList({ cards, disabled, onSelect, showLessLabel = "Show less", showMoreLabel = (n) => `Show ${n} more` }) {
409
+ function EntityCardList({ cards, disabled, showLessLabel = "Show less", showMoreLabel = (n) => `Show ${n} more` }) {
390
410
  const [expanded, setExpanded] = (0, import_react2.useState)(false);
391
411
  if (cards.length === 0) return null;
392
412
  const visible = expanded ? cards : cards.slice(0, MAX_VISIBLE);
393
413
  const overflow = cards.length - MAX_VISIBLE;
394
414
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "ss-w-cards", children: [
395
- visible.map((c) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("button", { type: "button", className: "ss-w-ecard", "data-kind": c.kind, disabled, onClick: () => onSelect(c.query), children: [
415
+ visible.map((c) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "ss-w-ecard", "data-kind": c.kind, children: [
396
416
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
397
417
  "span",
398
418
  {
@@ -409,82 +429,125 @@ function EntityCardList({ cards, disabled, onSelect, showLessLabel = "Show less"
409
429
  c.stats.map((s) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "ss-w-stat", children: [
410
430
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("b", { children: s.value }),
411
431
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: s.label })
412
- ] }, s.label)),
413
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "ss-w-chev", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ChevronIcon, {}) })
432
+ ] }, s.label))
414
433
  ] }, c.id)),
415
434
  overflow > 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "ss-w-cards-more", disabled, onClick: () => setExpanded((v) => !v), children: expanded ? showLessLabel : showMoreLabel(overflow) }) : null
416
435
  ] });
417
436
  }
418
437
 
419
- // src/components/ActionButtons.tsx
438
+ // src/components/BettingInsightList.tsx
420
439
  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: [
443
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-head", children: [
444
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "ss-w-bi-mkt", children: [
445
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-market", children: it.market }),
446
+ it.selection ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-sel", children: it.selection }) : null
447
+ ] }),
448
+ 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,
450
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("b", { children: it.odds }),
451
+ it.bookmaker ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-book", children: it.bookmaker }) : null
452
+ ] }) : null
453
+ ] }),
454
+ 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,
459
+ it.disclaimer ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "ss-w-bi-disc", children: it.disclaimer }) : null
460
+ ] }, it.id)) });
461
+ }
462
+
463
+ // src/components/ActionButtons.tsx
464
+ var import_jsx_runtime8 = require("react/jsx-runtime");
421
465
  function ActionButtons({ actions, loading, disabled, onAct }) {
422
466
  if (actions.length === 0 && !loading) return null;
423
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-actions", children: [
424
- actions.map((a, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "ss-w-abtn", disabled, onClick: () => onAct(a), children: a.label }, `${a.label}-${i}`)),
425
- actions.length === 0 && loading ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
426
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-abtn-shimmer" }),
427
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-abtn-shimmer" })
467
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "ss-w-actions", children: [
468
+ actions.map((a, i) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: "ss-w-abtn", disabled, onClick: () => onAct(a), children: a.label }, `${a.label}-${i}`)),
469
+ actions.length === 0 && loading ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
470
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "ss-w-abtn-shimmer" }),
471
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "ss-w-abtn-shimmer" })
428
472
  ] }) : null
429
473
  ] });
430
474
  }
431
475
 
432
476
  // src/components/ChatMessage.tsx
433
- var import_jsx_runtime8 = require("react/jsx-runtime");
434
- function ChatMessage({ message, disabled, onAct, onSelect, ui }) {
477
+ var import_jsx_runtime9 = require("react/jsx-runtime");
478
+ function ChatMessage({ message, disabled, onAct, ui }) {
435
479
  if (message.role === "user") {
436
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "ss-w-msg ss-w-user", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "ss-w-bubble", children: message.text }) });
480
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "ss-w-msg ss-w-user", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "ss-w-bubble", children: message.text }) });
437
481
  }
438
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "ss-w-msg ss-w-bot", children: [
439
- message.text ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Markdown, { text: message.text }) : null,
440
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(EntityCardList, { cards: message.cards, disabled, onSelect, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
441
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
482
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "ss-w-msg ss-w-bot", children: [
483
+ message.text ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Markdown, { text: message.text }) : null,
484
+ /* @__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 }),
486
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
442
487
  ] });
443
488
  }
444
489
 
445
490
  // src/components/IntentChips.tsx
446
- var import_jsx_runtime9 = require("react/jsx-runtime");
491
+ var import_jsx_runtime10 = require("react/jsx-runtime");
447
492
  function IntentChips({ chips, onPick }) {
448
493
  if (chips.length === 0) return null;
449
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "ss-w-chips", children: chips.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: "ss-w-chip", onClick: () => onPick(c.text), children: c.text }, `${c.text}-${i}`)) });
494
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "ss-w-chips", children: chips.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", className: "ss-w-chip", onClick: () => onPick(c.text), children: c.text }, `${c.text}-${i}`)) });
495
+ }
496
+
497
+ // src/scroll.ts
498
+ var PIN_THRESHOLD = 48;
499
+ function isPinnedToBottom(el, threshold = PIN_THRESHOLD) {
500
+ return el.scrollHeight - el.scrollTop - el.clientHeight <= threshold;
450
501
  }
451
502
 
452
503
  // src/components/MessageList.tsx
453
- var import_jsx_runtime10 = require("react/jsx-runtime");
504
+ var import_jsx_runtime11 = require("react/jsx-runtime");
454
505
  function MessageList({ controller, ui }) {
455
506
  const { state, send } = controller;
507
+ const listRef = (0, import_react3.useRef)(null);
456
508
  const endRef = (0, import_react3.useRef)(null);
509
+ const pinnedRef = (0, import_react3.useRef)(true);
457
510
  (0, import_react3.useEffect)(() => {
458
- endRef.current?.scrollIntoView({ block: "end" });
511
+ if (pinnedRef.current) endRef.current?.scrollIntoView({ block: "end" });
459
512
  }, [state.messages, state.status]);
460
513
  const empty = state.messages.length === 0;
461
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-list", "aria-live": "polite", children: [
462
- state.messages.map((m) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
463
- ChatMessage,
464
- {
465
- message: m,
466
- disabled: state.isStreaming,
467
- onAct: (a) => send(a.message, { actionId: a.actionId, displayText: a.label }),
468
- onSelect: (q) => send(q),
469
- ui
514
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
515
+ "div",
516
+ {
517
+ className: "ss-w-list",
518
+ "aria-live": "polite",
519
+ ref: listRef,
520
+ onScroll: () => {
521
+ if (listRef.current) pinnedRef.current = isPinnedToBottom(listRef.current);
470
522
  },
471
- m.id
472
- )),
473
- state.isStreaming && state.actionsLoading ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ActionButtons, { actions: [], loading: true, onAct: () => {
474
- } }) : null,
475
- state.status ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "ss-w-status", children: state.status }) : null,
476
- state.error ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-error", role: "alert", children: [
477
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
478
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
479
- ] }) : null,
480
- empty ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
481
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { ref: endRef })
482
- ] });
523
+ children: [
524
+ state.messages.map((m) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
525
+ ChatMessage,
526
+ {
527
+ message: m,
528
+ disabled: state.isStreaming,
529
+ onAct: (a) => send(a.message, { actionId: a.actionId, displayText: a.label }),
530
+ ui
531
+ },
532
+ m.id
533
+ )),
534
+ state.isStreaming && state.actionsLoading ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ActionButtons, { actions: [], loading: true, onAct: () => {
535
+ } }) : null,
536
+ state.status ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "ss-w-status", children: state.status }) : null,
537
+ state.error ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "ss-w-error", role: "alert", children: [
538
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
539
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
540
+ ] }) : null,
541
+ empty ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
542
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { ref: endRef })
543
+ ]
544
+ }
545
+ );
483
546
  }
484
547
 
485
548
  // src/components/ChatInput.tsx
486
549
  var import_react4 = require("react");
487
- var import_jsx_runtime11 = require("react/jsx-runtime");
550
+ var import_jsx_runtime12 = require("react/jsx-runtime");
488
551
  function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop response", sendLabel = "Send message" }) {
489
552
  const [value, setValue] = (0, import_react4.useState)("");
490
553
  const submit = () => {
@@ -499,14 +562,14 @@ function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop r
499
562
  submit();
500
563
  }
501
564
  };
502
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "ss-w-input", children: [
503
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
504
- streaming ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": stopLabel, onClick: onStop, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", className: "ss-w-send", "aria-label": sendLabel, onClick: submit, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SendIcon, {}) })
565
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "ss-w-input", children: [
566
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
567
+ streaming ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": stopLabel, onClick: onStop, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", className: "ss-w-send", "aria-label": sendLabel, onClick: submit, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SendIcon, {}) })
505
568
  ] });
506
569
  }
507
570
 
508
571
  // src/components/ChatPanel.tsx
509
- var import_jsx_runtime12 = require("react/jsx-runtime");
572
+ var import_jsx_runtime13 = require("react/jsx-runtime");
510
573
  var FULLSCREEN_QUERY = "(max-width: 639px)";
511
574
  function useIsModal() {
512
575
  const [modal, setModal] = (0, import_react5.useState)(false);
@@ -530,10 +593,10 @@ function ChatPanel({ controller, ui }) {
530
593
  window.addEventListener("keydown", onKey);
531
594
  return () => window.removeEventListener("keydown", onKey);
532
595
  }, [close]);
533
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
534
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
535
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(MessageList, { controller, ui }),
536
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
596
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
597
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
598
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(MessageList, { controller, ui }),
599
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
537
600
  ] });
538
601
  }
539
602
 
@@ -617,7 +680,7 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
617
680
 
618
681
  /* entity cards */
619
682
  .ss-w-cards { display: flex; flex-direction: column; gap: 7px; }
620
- .ss-w-ecard { display: flex; align-items: center; gap: 11px; width: 100%; background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); border-radius: 12px; padding: 8px 11px; text-align: left; cursor: pointer; }
683
+ .ss-w-ecard { display: flex; align-items: center; gap: 11px; width: 100%; background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); border-radius: 12px; padding: 8px 11px; text-align: left; }
621
684
  .ss-w-thumb { width: 40px; height: 40px; flex: none; border-radius: 50%; display: grid; place-items: center; font-size: 13px; font-weight: 700; color: #fff; background-color: var(--ss-w-nav); background-size: cover; background-position: center; }
622
685
  .ss-w-thumb.ss-w-team { border-radius: 10px; }
623
686
  .ss-w-ecard[data-kind="player"] .ss-w-thumb { background-color: var(--ss-w-accent); }
@@ -628,10 +691,26 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
628
691
  .ss-w-stat { margin-left: auto; text-align: center; }
629
692
  .ss-w-stat b { display: block; font-variant-numeric: tabular-nums; }
630
693
  .ss-w-stat span { font-size: 9px; text-transform: uppercase; color: var(--ss-w-faint); }
631
- .ss-w-chev { color: var(--ss-w-faint); font-size: 16px; display: grid; place-items: center; }
632
694
  .ss-w-cards-more { align-self: flex-start; font-size: 11.5px; font-weight: 560; color: var(--ss-w-accent); background: none; border: none; cursor: pointer; padding: 3px 2px; }
633
695
  .ss-w-cards-more:hover:not(:disabled) { text-decoration: underline; }
634
- .ss-w-ecard:disabled, .ss-w-cards-more:disabled { opacity: .5; cursor: default; }
696
+ .ss-w-cards-more:disabled { opacity: .5; cursor: default; }
697
+
698
+ /* betting insights */
699
+ .ss-w-binsights { display: flex; flex-direction: column; gap: 7px; }
700
+ .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; }
701
+ .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; }
709
+ .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); }
713
+ .ss-w-bi-disc { font-size: 10px; font-style: italic; color: var(--ss-w-faint); }
635
714
 
636
715
  /* action buttons */
637
716
  .ss-w-actions { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
@@ -648,6 +727,11 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
648
727
  .ss-w-input { display: flex; align-items: flex-end; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--ss-w-line); }
649
728
  .ss-w-textarea { flex: 1; resize: none; border: 1px solid var(--ss-w-line); border-radius: 12px; padding: 8px 10px; font: inherit; color: inherit; background: var(--ss-w-panel); max-height: 96px; }
650
729
  .ss-w-textarea:disabled { opacity: .55; cursor: not-allowed; }
730
+ /* iOS Safari zooms the page when a focused field's font-size is < 16px. Bump it
731
+ on touch devices only, so the desktop 13px design is unchanged. */
732
+ @media (pointer: coarse) {
733
+ .ss-w-textarea { font-size: 16px; }
734
+ }
651
735
  .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; }
652
736
  .ss-w-stop-glyph { width: 11px; height: 11px; background: currentColor; border-radius: 2px; }
653
737
 
@@ -657,7 +741,7 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
657
741
  `;
658
742
 
659
743
  // src/StatsWidget.tsx
660
- var import_jsx_runtime13 = require("react/jsx-runtime");
744
+ var import_jsx_runtime14 = require("react/jsx-runtime");
661
745
  function buildClient(props) {
662
746
  try {
663
747
  if ("client" in props && props.client) {
@@ -693,7 +777,7 @@ function StatsWidget(props) {
693
777
  }, [built, ui.injectStyles]);
694
778
  if (!built) return null;
695
779
  const attrs = themeAttrs(ui);
696
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { ref: rootRef, className: "ss-w-root", "data-ss-w-theme": attrs["data-ss-w-theme"], "data-ss-w-pos": attrs["data-ss-w-pos"], style: attrs.style, children: controller.state.isOpen ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ChatPanel, { controller, ui }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
780
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { ref: rootRef, className: "ss-w-root", "data-ss-w-theme": attrs["data-ss-w-theme"], "data-ss-w-pos": attrs["data-ss-w-pos"], style: attrs.style, children: controller.state.isOpen ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ChatPanel, { controller, ui }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
697
781
  ChatFab,
698
782
  {
699
783
  label: ui.launcherLabel,