@sensiblestats/widget-react 0.11.0 → 0.13.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.cjs CHANGED
@@ -65,6 +65,19 @@ var STRINGS = {
65
65
  oddsShortened: "shortened",
66
66
  oddsDrifted: "drifted",
67
67
  updatedLabel: "updated",
68
+ priceMovement: "Price movement",
69
+ openingLabel: "Opening",
70
+ currentLabel: "Current",
71
+ movementWindow: (hours) => hours % 24 === 0 ? `Last ${hours / 24}d` : `Last ${hours}h`,
72
+ notEnoughHistory: "No price history recorded yet for this selection.",
73
+ singlePricePoint: "Only one price recorded so far for this selection.",
74
+ selectionSuspended: "This selection is not currently available to bet.",
75
+ excludedSuspended: (n) => `${n} suspended price${n === 1 ? "" : "s"} excluded from this chart.`,
76
+ chartTimeLabel: (ms) => new Intl.DateTimeFormat("en-GB", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }).format(new Date(ms)),
77
+ chartAria: (opening, current, direction, hours) => {
78
+ const span = hours % 24 === 0 ? `${hours / 24} day${hours / 24 === 1 ? "" : "s"}` : `${hours} hour${hours === 1 ? "" : "s"}`;
79
+ return `Price movement over the last ${span}: opened at ${opening}, currently ${current}, ${direction}.`;
80
+ },
68
81
  inSlip: "In slip",
69
82
  slipAdded: (n) => n === void 0 ? "Added to slip" : `Added to slip (${n} selection${n === 1 ? "" : "s"})`,
70
83
  slipDuplicate: "Already in your slip",
@@ -98,6 +111,19 @@ var STRINGS = {
98
111
  oddsShortened: "d\xFC\u015Ft\xFC",
99
112
  oddsDrifted: "y\xFCkseldi",
100
113
  updatedLabel: "g\xFCncellendi",
114
+ priceMovement: "Oran hareketi",
115
+ openingLabel: "A\xE7\u0131l\u0131\u015F",
116
+ currentLabel: "G\xFCncel",
117
+ movementWindow: (hours) => hours % 24 === 0 ? `Son ${hours / 24} g\xFCn` : `Son ${hours} saat`,
118
+ notEnoughHistory: "Bu se\xE7im i\xE7in hen\xFCz oran ge\xE7mi\u015Fi kaydedilmedi.",
119
+ singlePricePoint: "Bu se\xE7im i\xE7in \u015Fimdiye kadar yaln\u0131zca bir fiyat kaydedildi.",
120
+ selectionSuspended: "Bu se\xE7im \u015Fu anda bahse kapal\u0131.",
121
+ excludedSuspended: (n) => `${n} ask\u0131ya al\u0131nm\u0131\u015F fiyat bu grafikten hari\xE7 tutuldu.`,
122
+ chartTimeLabel: (ms) => new Intl.DateTimeFormat("tr-TR", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }).format(new Date(ms)),
123
+ chartAria: (opening, current, direction, hours) => {
124
+ const span = hours % 24 === 0 ? `${hours / 24} g\xFCn` : `${hours} saat`;
125
+ return `Son ${span} i\xE7indeki oran hareketi: ${opening} ile a\xE7\u0131ld\u0131, \u015Fu anda ${current}, ${direction}.`;
126
+ },
101
127
  inSlip: "Kuponda",
102
128
  slipAdded: (n) => n === void 0 ? "Kupona eklendi" : `Kupona eklendi (${n} se\xE7im)`,
103
129
  slipDuplicate: "Bu se\xE7im kuponunuzda zaten var",
@@ -147,6 +173,16 @@ function normalizeUi(cfg) {
147
173
  oddsShortened: strings.oddsShortened,
148
174
  oddsDrifted: strings.oddsDrifted,
149
175
  updatedLabel: strings.updatedLabel,
176
+ priceMovement: strings.priceMovement,
177
+ openingLabel: strings.openingLabel,
178
+ currentLabel: strings.currentLabel,
179
+ movementWindow: strings.movementWindow,
180
+ notEnoughHistory: strings.notEnoughHistory,
181
+ singlePricePoint: strings.singlePricePoint,
182
+ selectionSuspended: strings.selectionSuspended,
183
+ excludedSuspended: strings.excludedSuspended,
184
+ chartTimeLabel: strings.chartTimeLabel,
185
+ chartAria: strings.chartAria,
150
186
  addToSlip: strings.addToSlip,
151
187
  inSlip: strings.inSlip,
152
188
  slipAdded: strings.slipAdded,
@@ -185,6 +221,54 @@ var import_widget_sdk2 = require("@sensiblestats/widget-sdk");
185
221
  // src/reducer.ts
186
222
  var import_widget_sdk = require("@sensiblestats/widget-sdk");
187
223
 
224
+ // src/chart.ts
225
+ var CHART_VIEW_W = 320;
226
+ var CHART_VIEW_H = 140;
227
+ var PLOT_X0 = 40;
228
+ var PLOT_X1 = 312;
229
+ var PLOT_Y0 = 10;
230
+ var PLOT_Y1 = 110;
231
+ var MARKER_MAX_POINTS = 8;
232
+ var FLAT_DOMAIN_PAD = 0.05;
233
+ var round2 = (n) => Math.round(n * 100) / 100;
234
+ function projectSeries(samples) {
235
+ if (samples.length < 2) return null;
236
+ const tMin = samples[0].atMs;
237
+ const tMax = samples[samples.length - 1].atMs;
238
+ if (tMax <= tMin) return null;
239
+ const prices = samples.map((s) => s.price);
240
+ const pMin = Math.min(...prices);
241
+ const pMax = Math.max(...prices);
242
+ let yMin;
243
+ let yMax;
244
+ if (pMax === pMin) {
245
+ yMin = pMin - FLAT_DOMAIN_PAD;
246
+ yMax = pMax + FLAT_DOMAIN_PAD;
247
+ } else {
248
+ const pad = (pMax - pMin) * 0.1;
249
+ yMin = pMin - pad;
250
+ yMax = pMax + pad;
251
+ }
252
+ const x = (atMs) => round2(PLOT_X0 + (atMs - tMin) / (tMax - tMin) * (PLOT_X1 - PLOT_X0));
253
+ const y = (price) => round2(PLOT_Y1 - (price - yMin) / (yMax - yMin) * (PLOT_Y1 - PLOT_Y0));
254
+ const projected = samples.map((s) => ({ x: x(s.atMs), y: y(s.price) }));
255
+ const path = projected.map((p, i) => `${i === 0 ? "M" : "L"}${p.x} ${p.y}`).join(" ");
256
+ return {
257
+ path,
258
+ markers: samples.length <= MARKER_MAX_POINTS ? projected : [],
259
+ // The real extremes, never the padded bounds: a gridline label is a price, and the padded
260
+ // bounds are prices nobody was ever offered. A flat series has one real extreme, not two —
261
+ // two identical gridlines would collide on both React key and rendered text.
262
+ gridlines: pMax === pMin ? [{ y: y(pMax), price: pMax }] : [
263
+ { y: y(pMax), price: pMax },
264
+ { y: y(pMin), price: pMin }
265
+ ],
266
+ viewBox: `0 0 ${CHART_VIEW_W} ${CHART_VIEW_H}`,
267
+ startMs: tMin,
268
+ endMs: tMax
269
+ };
270
+ }
271
+
188
272
  // src/model.ts
189
273
  function str(v) {
190
274
  return typeof v === "string" && v.length > 0 ? v : void 0;
@@ -250,6 +334,9 @@ function toInsightVM(insight) {
250
334
  const fixture = home && away ? `${home} vs ${away}` : void 0;
251
335
  const competition = str(insight.competitionName);
252
336
  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;
337
+ const previousValue = insight.odds?.previousValue;
338
+ const previous = typeof previousValue === "number" && Number.isFinite(previousValue) ? previousValue : void 0;
339
+ const direction = insight.odds?.movementDirection === "shortened" || insight.odds?.movementDirection === "drifted" ? insight.odds.movementDirection : void 0;
253
340
  return {
254
341
  id: str(insight.insightId) ?? `${str(insight.ruleId) ?? "insight"}:${num(insight.matchId) ?? ""}`,
255
342
  market,
@@ -269,7 +356,9 @@ function toInsightVM(insight) {
269
356
  matchOddsId,
270
357
  addToSlipEnabled: insight.addToSlipEnabled === true,
271
358
  oddsDecimal: typeof oddsValue === "number" && Number.isFinite(oddsValue) ? oddsValue : void 0,
272
- quotedAtUtc: str(insight.odds?.quotedAtUtc)
359
+ quotedAtUtc: str(insight.odds?.quotedAtUtc),
360
+ previousOdds: previous !== void 0 && direction !== void 0 ? previous.toFixed(2) : void 0,
361
+ movementDirection: previous !== void 0 && direction !== void 0 ? direction : void 0
273
362
  };
274
363
  }
275
364
  function toSlipSelection(vm, conversationId) {
@@ -322,6 +411,41 @@ function toMarketOddsVM(card) {
322
411
  addToSlipEnabled: card.addToSlipEnabled === true
323
412
  };
324
413
  }
414
+ function toOddsMovementVM(card) {
415
+ const raw = Array.isArray(card.points) ? card.points : [];
416
+ const samples = [];
417
+ for (const p of raw) {
418
+ const price = typeof p.price === "number" && Number.isFinite(p.price) ? p.price : void 0;
419
+ const atMs = typeof p.atUtc === "string" ? Date.parse(p.atUtc) : NaN;
420
+ if (price === void 0 || Number.isNaN(atMs)) continue;
421
+ samples.push({ atMs, price });
422
+ }
423
+ samples.sort((a, b) => a.atMs - b.atMs);
424
+ const asNumber = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
425
+ const opening = asNumber(card.openingPrice);
426
+ const current = asNumber(card.currentPrice);
427
+ const home = str(card.homeTeamName);
428
+ const away = str(card.awayTeamName);
429
+ const selection = [str(card.selectionLabel), str(card.line)].filter(Boolean).join(" ");
430
+ const bookmakerId = typeof card.bookmakerId === "number" ? card.bookmakerId : "";
431
+ return {
432
+ id: `${typeof card.matchId === "number" ? card.matchId : 0}:${str(card.marketDeveloperName) ?? ""}:${selection}:${bookmakerId}`,
433
+ market: str(card.marketLabel) ?? str(card.marketDeveloperName) ?? "",
434
+ selection,
435
+ fixture: home && away ? `${home} vs ${away}` : void 0,
436
+ competition: str(card.competitionName),
437
+ bookmaker: str(card.bookmakerName),
438
+ opening: opening !== void 0 ? opening.toFixed(2) : "",
439
+ current: current !== void 0 ? current.toFixed(2) : "",
440
+ direction: card.movementDirection === "shortened" || card.movementDirection === "drifted" ? card.movementDirection : void 0,
441
+ windowHours: asNumber(card.windowHours) ?? 0,
442
+ excludedCount: asNumber(card.excludedSuspendedCount) ?? 0,
443
+ isSuspended: card.isSuspended === true,
444
+ disclaimer: str(card.disclaimer),
445
+ geometry: projectSeries(samples),
446
+ observationCount: samples.length
447
+ };
448
+ }
325
449
  function marketOddsRowToSlipSelection(vm, row, conversationId) {
326
450
  if (row.matchOddsId === void 0 || row.oddsDecimal === void 0) return null;
327
451
  const selection = { matchOddsId: row.matchOddsId, matchId: vm.matchId, oddsDecimal: row.oddsDecimal };
@@ -362,6 +486,8 @@ function applyEvent(state, event) {
362
486
  return { ...state, messages: mapActive(state, (t) => ({ ...t, insights: [...t.insights, toInsightVM(event.data.insight)] })) };
363
487
  case "market_odds":
364
488
  return { ...state, messages: mapActive(state, (t) => ({ ...t, marketOdds: [...t.marketOdds, toMarketOddsVM(event.data.card)] })) };
489
+ case "odds_movement":
490
+ return { ...state, messages: mapActive(state, (t) => ({ ...t, oddsMovement: [...t.oddsMovement, toOddsMovementVM(event.data.card)] })) };
365
491
  case "intent_chip": {
366
492
  const chip = toChipVM(event.data.intent);
367
493
  return chip ? { ...state, starters: [...state.starters, chip] } : state;
@@ -397,11 +523,11 @@ function reducer(state, action) {
397
523
  return { ...state, isOpen: false, isStreaming: false, status: null, actionsLoading: false };
398
524
  case "USER_SENT": {
399
525
  const user = { role: "user", id: action.userId, text: action.displayText ?? action.input.message };
400
- const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], marketOdds: [], done: false };
526
+ const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], marketOdds: [], oddsMovement: [], done: false };
401
527
  return { ...state, messages: [...state.messages, user, assistant], isStreaming: true, status: null, actionsLoading: false, starters: [], error: null, lastUserInput: action.input, lastDisplayText: action.displayText ?? null };
402
528
  }
403
529
  case "GREETING_SENT": {
404
- const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], marketOdds: [], done: false, isGreeting: true };
530
+ const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], marketOdds: [], oddsMovement: [], done: false, isGreeting: true };
405
531
  return { ...state, messages: [...state.messages, assistant], isStreaming: true, status: null, error: null };
406
532
  }
407
533
  case "DISMISS_POPUP":
@@ -758,20 +884,41 @@ function AddToSlipButton({ it, slip, ui }) {
758
884
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SlipButton, { selection, shownOdds: it.odds ?? "", marketName: it.market, slip, ui });
759
885
  }
760
886
 
887
+ // src/components/OddsMovementBadge.tsx
888
+ var import_jsx_runtime9 = (
889
+ // Strikethrough is invisible to screen readers, so the whole was/now/direction sentence rides
890
+ // on one aria-label and the visual glyphs are hidden from the accessibility tree.
891
+ require("react/jsx-runtime")
892
+ );
893
+ function OddsMovementBadge({
894
+ previous,
895
+ current,
896
+ direction,
897
+ ui
898
+ }) {
899
+ if (previous === void 0 || direction === void 0) return null;
900
+ const directionWord2 = direction === "shortened" ? ui?.oddsShortened ?? "shortened" : ui?.oddsDrifted ?? "drifted";
901
+ const label = ui?.oddsMoved?.(previous, current, directionWord2) ?? `was ${previous}, now ${current}, ${directionWord2}`;
902
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "ss-w-move", role: "img", "aria-label": label, children: [
903
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-move-prev", "aria-hidden": "true", children: previous }),
904
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-move-arrow", "aria-hidden": "true", children: direction === "shortened" ? "\u25BC" : "\u25B2" })
905
+ ] });
906
+ }
907
+
761
908
  // src/components/BettingInsightList.tsx
762
- var import_jsx_runtime9 = require("react/jsx-runtime");
909
+ var import_jsx_runtime10 = require("react/jsx-runtime");
763
910
  function StatRows2({ stats }) {
764
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "ss-w-bi-rows", children: stats.map((s) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "ss-w-bi-row", children: [
765
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-bi-row-lbl", children: s.label }),
766
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-bi-row-num", children: s.value })
911
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "ss-w-bi-rows", children: stats.map((s) => /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-bi-row", children: [
912
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-row-lbl", children: s.label }),
913
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-row-num", children: s.value })
767
914
  ] }, `${s.label}:${s.value}`)) });
768
915
  }
769
916
  function ScopedStats({ scopes }) {
770
917
  const [active, setActive] = (0, import_react5.useState)(0);
771
918
  const current = scopes[active] ?? scopes[0] ?? { key: "", label: "", stats: [] };
772
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "ss-w-bi-scoped", children: [
773
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "ss-w-bi-scopes", role: "group", children: scopes.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: "ss-w-bi-chip", "aria-pressed": i === active, onClick: () => setActive(i), children: s.label }, s.key)) }),
774
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(StatRows2, { stats: current.stats }, current.key)
919
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-bi-scoped", children: [
920
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "ss-w-bi-scopes", role: "group", children: scopes.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", className: "ss-w-bi-chip", "aria-pressed": i === active, onClick: () => setActive(i), children: s.label }, s.key)) }),
921
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(StatRows2, { stats: current.stats }, current.key)
775
922
  ] });
776
923
  }
777
924
  function StatsDisclosure({ it, ui }) {
@@ -779,8 +926,8 @@ function StatsDisclosure({ it, ui }) {
779
926
  const hasScopes = !!it.scopes && it.scopes.length >= 2;
780
927
  const hasStats = hasScopes || it.stats.length > 0;
781
928
  if (!hasStats) return null;
782
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "ss-w-bi-stats", children: [
783
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
929
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-bi-stats", children: [
930
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
784
931
  "button",
785
932
  {
786
933
  type: "button",
@@ -788,131 +935,189 @@ function StatsDisclosure({ it, ui }) {
788
935
  "aria-expanded": open,
789
936
  onClick: () => setOpen((v) => !v),
790
937
  children: [
791
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: open ? ui?.hideStats ?? "Hide stats" : ui?.showStats ?? "Show stats" }),
792
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: `ss-w-bi-chev${open ? " ss-w-bi-chev-up" : ""}`, "aria-hidden": "true" })
938
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: open ? ui?.hideStats ?? "Hide stats" : ui?.showStats ?? "Show stats" }),
939
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: `ss-w-bi-chev${open ? " ss-w-bi-chev-up" : ""}`, "aria-hidden": "true" })
793
940
  ]
794
941
  }
795
942
  ),
796
- open ? hasScopes ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ScopedStats, { scopes: it.scopes }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(StatRows2, { stats: it.stats }) : null
943
+ open ? hasScopes ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ScopedStats, { scopes: it.scopes }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(StatRows2, { stats: it.stats }) : null
797
944
  ] });
798
945
  }
799
946
  function InsightCard({ it, ui, slip }) {
800
947
  const live = it.isLive || it.phase === "live";
801
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "ss-w-binsight", children: [
802
- live ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "ss-w-bi-status", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "ss-w-bi-live", children: [
803
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-bi-live-dot", "aria-hidden": "true" }),
948
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-binsight", children: [
949
+ live ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "ss-w-bi-status", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "ss-w-bi-live", children: [
950
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-live-dot", "aria-hidden": "true" }),
804
951
  ui?.liveLabel ?? "Live"
805
952
  ] }) }) : null,
806
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "ss-w-bi-head", children: [
807
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "ss-w-bi-mkt", children: [
808
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-bi-market", children: it.market }),
809
- it.selection ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-bi-sel", children: it.selection }) : null,
810
- it.fixture || it.competition ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "ss-w-bi-match", children: [
811
- it.fixture ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-bi-fixture", children: it.fixture }) : null,
812
- it.competition ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-bi-comp", children: it.competition }) : null
953
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-bi-head", children: [
954
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "ss-w-bi-mkt", children: [
955
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-market", children: it.market }),
956
+ it.selection ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-sel", children: it.selection }) : null,
957
+ it.fixture || it.competition ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "ss-w-bi-match", children: [
958
+ it.fixture ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-fixture", children: it.fixture }) : null,
959
+ it.competition ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-comp", children: it.competition }) : null
813
960
  ] }) : null
814
961
  ] }),
815
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "ss-w-bi-action", children: [
816
- it.odds ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "ss-w-bi-odds", children: [
817
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-bi-odds-cap", children: ui?.oddsLabel ?? "Odds" }),
818
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("b", { children: it.odds }),
819
- it.bookmaker ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-bi-book", children: it.bookmaker }) : null
962
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-bi-action", children: [
963
+ it.odds ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "ss-w-bi-odds", children: [
964
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-odds-cap", children: ui?.oddsLabel ?? "Odds" }),
965
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("b", { children: it.odds }),
966
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(OddsMovementBadge, { previous: it.previousOdds, current: it.odds, direction: it.movementDirection, ui }),
967
+ it.bookmaker ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-bi-book", children: it.bookmaker }) : null
820
968
  ] }) : null,
821
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(AddToSlipButton, { it, slip, ui })
969
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(AddToSlipButton, { it, slip, ui })
822
970
  ] })
823
971
  ] }),
824
- it.text ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "ss-w-bi-text", children: it.text }) : null,
825
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(StatsDisclosure, { it, ui }),
826
- it.disclaimer ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "ss-w-bi-disc", children: it.disclaimer }) : null
972
+ it.text ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "ss-w-bi-text", children: it.text }) : null,
973
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(StatsDisclosure, { it, ui }),
974
+ it.disclaimer ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "ss-w-bi-disc", children: it.disclaimer }) : null
827
975
  ] });
828
976
  }
829
977
  function BettingInsightList({ insights, ui, slip }) {
830
978
  if (!insights || insights.length === 0) return null;
831
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "ss-w-binsights", children: insights.map((it) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(InsightCard, { it, ui, slip }, it.id)) });
979
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "ss-w-binsights", children: insights.map((it) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(InsightCard, { it, ui, slip }, it.id)) });
832
980
  }
833
981
 
834
982
  // src/components/MarketOddsCardList.tsx
835
983
  var import_react6 = require("react");
836
- var import_jsx_runtime10 = require("react/jsx-runtime");
984
+ var import_jsx_runtime11 = require("react/jsx-runtime");
837
985
  function OddsRow({ vm, row, ui, slip }) {
838
986
  const selection = vm.addToSlipEnabled ? marketOddsRowToSlipSelection(vm, row, slip.conversationId) : null;
839
- const directionWord = row.movementDirection === "shortened" ? ui?.oddsShortened ?? "shortened" : ui?.oddsDrifted ?? "drifted";
840
- const movementLabel = row.previousOdds !== void 0 ? ui?.oddsMoved?.(row.previousOdds, row.odds, directionWord) ?? `was ${row.previousOdds}, now ${row.odds}, ${directionWord}` : void 0;
841
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-mo-row", children: [
842
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-mo-sel", children: row.selection }),
843
- row.previousOdds !== void 0 ? (
844
- // Strikethrough is invisible to screen readers, so the whole was/now/direction sentence
845
- // rides on one aria-label and the visual glyphs are hidden from the accessibility tree.
846
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "ss-w-mo-move", role: "img", "aria-label": movementLabel, children: [
847
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-mo-prev", "aria-hidden": "true", children: row.previousOdds }),
848
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-mo-arrow", "aria-hidden": "true", children: row.movementDirection === "shortened" ? "\u25BC" : "\u25B2" })
849
- ] })
850
- ) : null,
851
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-mo-price", children: row.odds }),
852
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(SlipButton, { selection, shownOdds: row.odds, marketName: vm.market, slip, ui })
987
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "ss-w-mo-row", children: [
988
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "ss-w-mo-sel", children: row.selection }),
989
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(OddsMovementBadge, { previous: row.previousOdds, current: row.odds, direction: row.movementDirection, ui }),
990
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "ss-w-mo-price", children: row.odds }),
991
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SlipButton, { selection, shownOdds: row.odds, marketName: vm.market, slip, ui })
853
992
  ] });
854
993
  }
855
994
  function MarketOddsCard({ vm, ui, slip }) {
856
995
  const [open, setOpen] = (0, import_react6.useState)(false);
857
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-mocard", children: [
858
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-mo-head", children: [
859
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-mo-market", children: vm.market }),
860
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-mo-tag", children: ui?.marketPrices ?? "Market prices" })
996
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "ss-w-mocard", children: [
997
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "ss-w-mo-head", children: [
998
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "ss-w-mo-market", children: vm.market }),
999
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "ss-w-mo-tag", children: ui?.marketPrices ?? "Market prices" })
861
1000
  ] }),
862
- vm.fixture || vm.competition ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-mo-fixture", children: [
863
- vm.fixture ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: vm.fixture }) : null,
864
- vm.competition ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-mo-comp", children: vm.competition }) : null
1001
+ vm.fixture || vm.competition ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "ss-w-mo-fixture", children: [
1002
+ vm.fixture ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: vm.fixture }) : null,
1003
+ vm.competition ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "ss-w-mo-comp", children: vm.competition }) : null
865
1004
  ] }) : null,
866
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-mo-rows", children: [
867
- vm.mainRows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(OddsRow, { vm, row, ui, slip }, row.key)),
868
- open ? vm.moreRows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(OddsRow, { vm, row, ui, slip }, row.key)) : null
1005
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "ss-w-mo-rows", children: [
1006
+ vm.mainRows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(OddsRow, { vm, row, ui, slip }, row.key)),
1007
+ open ? vm.moreRows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(OddsRow, { vm, row, ui, slip }, row.key)) : null
869
1008
  ] }),
870
- vm.moreRows.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("button", { type: "button", className: "ss-w-bi-toggle", "aria-expanded": open, onClick: () => setOpen((v) => !v), children: [
871
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: open ? ui?.hideLines ?? "Hide lines" : `${ui?.moreLines ?? "More lines"} (${vm.moreRows.length})` }),
872
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: `ss-w-bi-chev${open ? " ss-w-bi-chev-up" : ""}`, "aria-hidden": "true" })
1009
+ vm.moreRows.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("button", { type: "button", className: "ss-w-bi-toggle", "aria-expanded": open, onClick: () => setOpen((v) => !v), children: [
1010
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: open ? ui?.hideLines ?? "Hide lines" : `${ui?.moreLines ?? "More lines"} (${vm.moreRows.length})` }),
1011
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: `ss-w-bi-chev${open ? " ss-w-bi-chev-up" : ""}`, "aria-hidden": "true" })
873
1012
  ] }) : null,
874
- vm.bookmaker ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "ss-w-mo-foot", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: vm.bookmaker }) }) : null,
875
- vm.disclaimer ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "ss-w-bi-disc", children: vm.disclaimer }) : null
1013
+ vm.bookmaker ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "ss-w-mo-foot", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: vm.bookmaker }) }) : null,
1014
+ vm.disclaimer ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "ss-w-bi-disc", children: vm.disclaimer }) : null
876
1015
  ] });
877
1016
  }
878
1017
  function MarketOddsCardList({ cards, ui, slip }) {
879
1018
  if (!cards || cards.length === 0) return null;
880
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "ss-w-mocards", children: cards.map((vm) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MarketOddsCard, { vm, ui, slip }, vm.id)) });
1019
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "ss-w-mocards", children: cards.map((vm) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(MarketOddsCard, { vm, ui, slip }, vm.id)) });
1020
+ }
1021
+
1022
+ // src/components/OddsMovementCard.tsx
1023
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1024
+ function directionWord(vm, ui) {
1025
+ if (vm.direction === "shortened") return ui?.oddsShortened ?? "shortened";
1026
+ if (vm.direction === "drifted") return ui?.oddsDrifted ?? "drifted";
1027
+ return "";
1028
+ }
1029
+ function OddsMovementCard({ vm, ui }) {
1030
+ const aria = ui?.chartAria ? ui.chartAria(vm.opening, vm.current, directionWord(vm, ui), vm.windowHours) : `Price movement over the last ${vm.windowHours} hours: opened at ${vm.opening}, currently ${vm.current}, ${directionWord(vm, ui)}.`;
1031
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "ss-w-omcard", children: [
1032
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "ss-w-om-head", children: [
1033
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "ss-w-om-market", children: vm.market }),
1034
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "ss-w-om-tag", children: ui?.priceMovement ?? "Price movement" })
1035
+ ] }),
1036
+ vm.fixture ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "ss-w-om-fixture", children: [
1037
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { children: vm.fixture }),
1038
+ vm.competition ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "ss-w-om-comp", children: vm.competition }) : null
1039
+ ] }) : null,
1040
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "ss-w-om-sel", children: vm.selection }),
1041
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(OddsMovementBadge, { previous: vm.opening, current: vm.current, direction: vm.direction, ui }),
1042
+ vm.geometry === null ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "ss-w-om-empty", children: vm.observationCount === 1 ? ui?.singlePricePoint ?? "Only one price recorded so far for this selection." : ui?.notEnoughHistory ?? "No price history recorded yet for this selection." }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1043
+ "svg",
1044
+ {
1045
+ className: "ss-w-om-chart",
1046
+ viewBox: vm.geometry.viewBox,
1047
+ width: "100%",
1048
+ role: "img",
1049
+ "aria-label": aria,
1050
+ children: [
1051
+ vm.geometry.gridlines.map((g) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("g", { children: [
1052
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("line", { className: "ss-w-om-grid", x1: 40, y1: g.y, x2: CHART_VIEW_W - 8, y2: g.y }),
1053
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("text", { className: "ss-w-om-ylab", x: 34, y: g.y + 3, textAnchor: "end", children: g.price.toFixed(2) })
1054
+ ] }, g.price)),
1055
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("path", { className: "ss-w-om-line", d: vm.geometry.path, fill: "none" }),
1056
+ vm.geometry.markers.map((m, i) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("circle", { className: "ss-w-om-dot", cx: m.x, cy: m.y, r: 2.5 }, i)),
1057
+ ui?.chartTimeLabel ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
1058
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("text", { className: "ss-w-om-xlab", x: 40, y: CHART_VIEW_H - 8, textAnchor: "start", children: ui.chartTimeLabel(vm.geometry.startMs) }),
1059
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("text", { className: "ss-w-om-xlab", x: CHART_VIEW_W - 8, y: CHART_VIEW_H - 8, textAnchor: "end", children: ui.chartTimeLabel(vm.geometry.endMs) })
1060
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("text", { className: "ss-w-om-xlab", x: 40, y: CHART_VIEW_H - 8, children: ui?.movementWindow ? ui.movementWindow(vm.windowHours) : `Last ${vm.windowHours}h` })
1061
+ ]
1062
+ }
1063
+ ),
1064
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "ss-w-om-foot", children: [
1065
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { children: [
1066
+ ui?.openingLabel ?? "Opening",
1067
+ " ",
1068
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("b", { children: vm.opening })
1069
+ ] }),
1070
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { children: [
1071
+ ui?.currentLabel ?? "Current",
1072
+ " ",
1073
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("b", { children: vm.current })
1074
+ ] })
1075
+ ] }),
1076
+ vm.bookmaker ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "ss-w-om-foot", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { children: vm.bookmaker }) }) : null,
1077
+ vm.excludedCount > 0 ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "ss-w-om-excl", children: ui?.excludedSuspended ? ui.excludedSuspended(vm.excludedCount) : `${vm.excludedCount} suspended price(s) excluded from this chart.` }) : null,
1078
+ vm.isSuspended ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "ss-w-om-susp", children: ui?.selectionSuspended ?? "This selection is not currently available to bet." }) : null,
1079
+ vm.disclaimer ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "ss-w-om-disc", children: vm.disclaimer }) : null
1080
+ ] });
1081
+ }
1082
+ function OddsMovementCardList({ cards, ui }) {
1083
+ if (!cards || cards.length === 0) return null;
1084
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "ss-w-omcards", children: cards.map((vm) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(OddsMovementCard, { vm, ui }, vm.id)) });
881
1085
  }
882
1086
 
883
1087
  // src/components/ActionButtons.tsx
884
- var import_jsx_runtime11 = require("react/jsx-runtime");
1088
+ var import_jsx_runtime13 = require("react/jsx-runtime");
885
1089
  function ActionButtons({ actions, loading, disabled, onAct }) {
886
1090
  if (actions.length === 0 && !loading) return null;
887
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "ss-w-actions", children: [
888
- actions.map((a, i) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", className: "ss-w-abtn", disabled, onClick: () => onAct(a), children: a.label }, `${a.label}-${i}`)),
889
- actions.length === 0 && loading ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
890
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "ss-w-abtn-shimmer" }),
891
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "ss-w-abtn-shimmer" })
1091
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "ss-w-actions", children: [
1092
+ actions.map((a, i) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("button", { type: "button", className: "ss-w-abtn", disabled, onClick: () => onAct(a), children: a.label }, `${a.label}-${i}`)),
1093
+ actions.length === 0 && loading ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
1094
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "ss-w-abtn-shimmer" }),
1095
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "ss-w-abtn-shimmer" })
892
1096
  ] }) : null
893
1097
  ] });
894
1098
  }
895
1099
 
896
1100
  // src/components/ChatMessage.tsx
897
- var import_jsx_runtime12 = require("react/jsx-runtime");
1101
+ var import_jsx_runtime14 = require("react/jsx-runtime");
898
1102
  function ChatMessage({ message, disabled, onAct, ui, slip }) {
899
1103
  if (message.role === "user") {
900
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "ss-w-msg ss-w-user", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "ss-w-bubble", children: message.text }) });
1104
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "ss-w-msg ss-w-user", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "ss-w-bubble", children: message.text }) });
901
1105
  }
902
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "ss-w-msg ss-w-bot", children: [
903
- message.text ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Markdown, { text: message.text }) : null,
904
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
905
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(BettingInsightList, { insights: message.insights, ui, slip }),
906
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(MarketOddsCardList, { cards: message.marketOdds, ui, slip }),
907
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
1106
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "ss-w-msg ss-w-bot", children: [
1107
+ message.text ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Markdown, { text: message.text }) : null,
1108
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
1109
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(BettingInsightList, { insights: message.insights, ui, slip }),
1110
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(MarketOddsCardList, { cards: message.marketOdds, ui, slip }),
1111
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(OddsMovementCardList, { cards: message.oddsMovement, ui }),
1112
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
908
1113
  ] });
909
1114
  }
910
1115
 
911
1116
  // src/components/IntentChips.tsx
912
- var import_jsx_runtime13 = require("react/jsx-runtime");
1117
+ var import_jsx_runtime15 = require("react/jsx-runtime");
913
1118
  function IntentChips({ chips, onPick }) {
914
1119
  if (chips.length === 0) return null;
915
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "ss-w-chips", children: chips.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("button", { type: "button", className: "ss-w-chip", onClick: () => onPick(c.text), children: c.text }, `${c.text}-${i}`)) });
1120
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "ss-w-chips", children: chips.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { type: "button", className: "ss-w-chip", onClick: () => onPick(c.text), children: c.text }, `${c.text}-${i}`)) });
916
1121
  }
917
1122
 
918
1123
  // src/scroll.ts
@@ -922,7 +1127,7 @@ function isPinnedToBottom(el, threshold = PIN_THRESHOLD) {
922
1127
  }
923
1128
 
924
1129
  // src/components/MessageList.tsx
925
- var import_jsx_runtime14 = require("react/jsx-runtime");
1130
+ var import_jsx_runtime16 = require("react/jsx-runtime");
926
1131
  function MessageList({ controller, ui, slip }) {
927
1132
  const { state, send } = controller;
928
1133
  const listRef = (0, import_react7.useRef)(null);
@@ -932,7 +1137,7 @@ function MessageList({ controller, ui, slip }) {
932
1137
  if (pinnedRef.current) endRef.current?.scrollIntoView({ block: "end" });
933
1138
  }, [state.messages, state.status]);
934
1139
  const empty = state.messages.length === 0;
935
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
1140
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
936
1141
  "div",
937
1142
  {
938
1143
  className: "ss-w-list",
@@ -942,7 +1147,7 @@ function MessageList({ controller, ui, slip }) {
942
1147
  if (listRef.current) pinnedRef.current = isPinnedToBottom(listRef.current);
943
1148
  },
944
1149
  children: [
945
- state.messages.map((m) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1150
+ state.messages.map((m) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
946
1151
  ChatMessage,
947
1152
  {
948
1153
  message: m,
@@ -953,15 +1158,15 @@ function MessageList({ controller, ui, slip }) {
953
1158
  },
954
1159
  m.id
955
1160
  )),
956
- state.isStreaming && state.actionsLoading ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ActionButtons, { actions: [], loading: true, onAct: () => {
1161
+ state.isStreaming && state.actionsLoading ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ActionButtons, { actions: [], loading: true, onAct: () => {
957
1162
  } }) : null,
958
- state.status ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "ss-w-status", children: state.status }) : null,
959
- state.error ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "ss-w-error", role: "alert", children: [
960
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
961
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
1163
+ state.status ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "ss-w-status", children: state.status }) : null,
1164
+ state.error ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "ss-w-error", role: "alert", children: [
1165
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
1166
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
962
1167
  ] }) : null,
963
- empty ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
964
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { ref: endRef })
1168
+ empty ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
1169
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { ref: endRef })
965
1170
  ]
966
1171
  }
967
1172
  );
@@ -969,7 +1174,7 @@ function MessageList({ controller, ui, slip }) {
969
1174
 
970
1175
  // src/components/ChatInput.tsx
971
1176
  var import_react8 = require("react");
972
- var import_jsx_runtime15 = require("react/jsx-runtime");
1177
+ var import_jsx_runtime17 = require("react/jsx-runtime");
973
1178
  function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop response", sendLabel = "Send message" }) {
974
1179
  const [value, setValue] = (0, import_react8.useState)("");
975
1180
  const submit = () => {
@@ -984,14 +1189,14 @@ function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop r
984
1189
  submit();
985
1190
  }
986
1191
  };
987
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "ss-w-input", children: [
988
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
989
- streaming ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": stopLabel, onClick: onStop, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { type: "button", className: "ss-w-send", "aria-label": sendLabel, onClick: submit, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SendIcon, {}) })
1192
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "ss-w-input", children: [
1193
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
1194
+ streaming ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": stopLabel, onClick: onStop, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", className: "ss-w-send", "aria-label": sendLabel, onClick: submit, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SendIcon, {}) })
990
1195
  ] });
991
1196
  }
992
1197
 
993
1198
  // src/components/ChatPanel.tsx
994
- var import_jsx_runtime16 = require("react/jsx-runtime");
1199
+ var import_jsx_runtime18 = require("react/jsx-runtime");
995
1200
  var FULLSCREEN_QUERY = "(max-width: 639px)";
996
1201
  function useIsModal() {
997
1202
  const [modal, setModal] = (0, import_react9.useState)(false);
@@ -1015,10 +1220,10 @@ function ChatPanel({ controller, ui, slip }) {
1015
1220
  window.addEventListener("keydown", onKey);
1016
1221
  return () => window.removeEventListener("keydown", onKey);
1017
1222
  }, [close]);
1018
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
1019
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
1020
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(MessageList, { controller, ui, slip }),
1021
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
1223
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
1224
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
1225
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(MessageList, { controller, ui, slip }),
1226
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
1022
1227
  ] });
1023
1228
  }
1024
1229
 
@@ -1207,13 +1412,40 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
1207
1412
  .ss-w-mo-price { margin-left: auto; font-family: ui-monospace, monospace; font-variant-numeric: tabular-nums; font-size: 14px; font-weight: 700; color: var(--ss-w-accent); }
1208
1413
  /* Movement: BOTH directions share --ss-w-faint on purpose. Green-good/red-bad would frame a
1209
1414
  shortening price as a discount, which inverts what it actually means for the bettor. */
1210
- .ss-w-mo-move { margin-left: auto; display: inline-flex; align-items: baseline; gap: 4px; color: var(--ss-w-faint); }
1211
- .ss-w-mo-prev { font-family: ui-monospace, monospace; font-variant-numeric: tabular-nums; font-size: 12px; text-decoration: line-through; }
1212
- .ss-w-mo-arrow { font-size: 9px; line-height: 1; }
1213
- .ss-w-mo-move + .ss-w-mo-price { margin-left: 0; }
1415
+ .ss-w-move { display: inline-flex; align-items: baseline; gap: 4px; color: var(--ss-w-faint); }
1416
+ .ss-w-move-prev { font-family: ui-monospace, monospace; font-variant-numeric: tabular-nums; font-size: 12px; text-decoration: line-through; }
1417
+ .ss-w-move-arrow { font-size: 9px; line-height: 1; }
1418
+ /* margin-left:auto is row layout, not badge styling -- it belongs to the market-odds row, which is
1419
+ a flex row, and must NOT follow the badge into the insight card's odds column. */
1420
+ .ss-w-mo-row .ss-w-move { margin-left: auto; }
1421
+ .ss-w-mo-row .ss-w-move + .ss-w-mo-price { margin-left: 0; }
1214
1422
  .ss-w-mo-row .ss-w-slip { margin: 0; }
1215
1423
  .ss-w-mo-foot { display: flex; align-items: center; gap: 8px; font-size: 10.5px; color: var(--ss-w-faint); }
1216
1424
 
1425
+ /* odds movement card */
1426
+ .ss-w-omcards { display: flex; flex-direction: column; gap: 7px; }
1427
+ .ss-w-omcard { 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; }
1428
+ .ss-w-om-head { display: flex; align-items: center; gap: 8px; }
1429
+ .ss-w-om-market { font-size: 13px; font-weight: 700; color: var(--ss-w-ink); }
1430
+ .ss-w-om-tag { margin-left: auto; flex: none; font-size: 9px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: var(--ss-w-faint); border: 1px solid var(--ss-w-line); border-radius: 8px; padding: 2px 7px; }
1431
+ .ss-w-om-fixture { display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--ss-w-faint); min-width: 0; }
1432
+ .ss-w-om-comp { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1433
+ .ss-w-om-sel { font-size: 12.5px; font-weight: 600; color: var(--ss-w-ink); }
1434
+ .ss-w-om-chart { display: block; width: 100%; height: auto; }
1435
+ /* Single neutral accent in BOTH directions, deliberately. Colouring a shortening price green
1436
+ frames it as a discount, which inverts what it means for the bettor -- the same reason
1437
+ .ss-w-move keeps both directions on --ss-w-faint. */
1438
+ .ss-w-om-line { stroke: var(--ss-w-accent); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
1439
+ .ss-w-om-dot { fill: var(--ss-w-accent); }
1440
+ .ss-w-om-grid { stroke: var(--ss-w-line); stroke-width: 1; }
1441
+ .ss-w-om-ylab, .ss-w-om-xlab { fill: var(--ss-w-faint); font-size: 9px; font-family: ui-monospace, monospace; }
1442
+ .ss-w-om-empty { font-size: 11.5px; color: var(--ss-w-faint); padding: 6px 0; }
1443
+ .ss-w-om-foot { display: flex; align-items: center; gap: 14px; font-size: 11.5px; color: var(--ss-w-faint); }
1444
+ .ss-w-om-foot b { font-family: ui-monospace, monospace; font-variant-numeric: tabular-nums; font-size: 13px; color: var(--ss-w-ink); }
1445
+ .ss-w-om-excl { font-size: 10.5px; color: var(--ss-w-faint); }
1446
+ .ss-w-om-susp { font-size: 11px; font-weight: 600; color: #c0392b; }
1447
+ .ss-w-om-disc { font-size: 10px; font-style: italic; color: var(--ss-w-faint); }
1448
+
1217
1449
  /* action buttons */
1218
1450
  .ss-w-actions { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
1219
1451
  .ss-w-abtn { font-size: 12px; font-weight: 560; color: var(--ss-w-accent); background: var(--ss-w-panel); border: 1px solid var(--ss-w-accent); border-radius: 16px; padding: 6px 12px; cursor: pointer; }
@@ -1251,7 +1483,7 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
1251
1483
  `;
1252
1484
 
1253
1485
  // src/StatsWidget.tsx
1254
- var import_jsx_runtime17 = require("react/jsx-runtime");
1486
+ var import_jsx_runtime19 = require("react/jsx-runtime");
1255
1487
  function buildClient(props) {
1256
1488
  try {
1257
1489
  if ("client" in props && props.client) {
@@ -1300,7 +1532,7 @@ function StatsWidget(props) {
1300
1532
  }, [built, ui.injectStyles]);
1301
1533
  if (!built) return null;
1302
1534
  const attrs = themeAttrs(ui);
1303
- return /* @__PURE__ */ (0, import_jsx_runtime17.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_runtime17.jsx)(ChatPanel, { controller, ui, slip }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1535
+ return /* @__PURE__ */ (0, import_jsx_runtime19.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_runtime19.jsx)(ChatPanel, { controller, ui, slip }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1304
1536
  ChatFab,
1305
1537
  {
1306
1538
  label: ui.launcherLabel,