@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.js CHANGED
@@ -28,6 +28,19 @@ var STRINGS = {
28
28
  oddsShortened: "shortened",
29
29
  oddsDrifted: "drifted",
30
30
  updatedLabel: "updated",
31
+ priceMovement: "Price movement",
32
+ openingLabel: "Opening",
33
+ currentLabel: "Current",
34
+ movementWindow: (hours) => hours % 24 === 0 ? `Last ${hours / 24}d` : `Last ${hours}h`,
35
+ notEnoughHistory: "No price history recorded yet for this selection.",
36
+ singlePricePoint: "Only one price recorded so far for this selection.",
37
+ selectionSuspended: "This selection is not currently available to bet.",
38
+ excludedSuspended: (n) => `${n} suspended price${n === 1 ? "" : "s"} excluded from this chart.`,
39
+ chartTimeLabel: (ms) => new Intl.DateTimeFormat("en-GB", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }).format(new Date(ms)),
40
+ chartAria: (opening, current, direction, hours) => {
41
+ const span = hours % 24 === 0 ? `${hours / 24} day${hours / 24 === 1 ? "" : "s"}` : `${hours} hour${hours === 1 ? "" : "s"}`;
42
+ return `Price movement over the last ${span}: opened at ${opening}, currently ${current}, ${direction}.`;
43
+ },
31
44
  inSlip: "In slip",
32
45
  slipAdded: (n) => n === void 0 ? "Added to slip" : `Added to slip (${n} selection${n === 1 ? "" : "s"})`,
33
46
  slipDuplicate: "Already in your slip",
@@ -61,6 +74,19 @@ var STRINGS = {
61
74
  oddsShortened: "d\xFC\u015Ft\xFC",
62
75
  oddsDrifted: "y\xFCkseldi",
63
76
  updatedLabel: "g\xFCncellendi",
77
+ priceMovement: "Oran hareketi",
78
+ openingLabel: "A\xE7\u0131l\u0131\u015F",
79
+ currentLabel: "G\xFCncel",
80
+ movementWindow: (hours) => hours % 24 === 0 ? `Son ${hours / 24} g\xFCn` : `Son ${hours} saat`,
81
+ notEnoughHistory: "Bu se\xE7im i\xE7in hen\xFCz oran ge\xE7mi\u015Fi kaydedilmedi.",
82
+ singlePricePoint: "Bu se\xE7im i\xE7in \u015Fimdiye kadar yaln\u0131zca bir fiyat kaydedildi.",
83
+ selectionSuspended: "Bu se\xE7im \u015Fu anda bahse kapal\u0131.",
84
+ excludedSuspended: (n) => `${n} ask\u0131ya al\u0131nm\u0131\u015F fiyat bu grafikten hari\xE7 tutuldu.`,
85
+ chartTimeLabel: (ms) => new Intl.DateTimeFormat("tr-TR", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }).format(new Date(ms)),
86
+ chartAria: (opening, current, direction, hours) => {
87
+ const span = hours % 24 === 0 ? `${hours / 24} g\xFCn` : `${hours} saat`;
88
+ return `Son ${span} i\xE7indeki oran hareketi: ${opening} ile a\xE7\u0131ld\u0131, \u015Fu anda ${current}, ${direction}.`;
89
+ },
64
90
  inSlip: "Kuponda",
65
91
  slipAdded: (n) => n === void 0 ? "Kupona eklendi" : `Kupona eklendi (${n} se\xE7im)`,
66
92
  slipDuplicate: "Bu se\xE7im kuponunuzda zaten var",
@@ -110,6 +136,16 @@ function normalizeUi(cfg) {
110
136
  oddsShortened: strings.oddsShortened,
111
137
  oddsDrifted: strings.oddsDrifted,
112
138
  updatedLabel: strings.updatedLabel,
139
+ priceMovement: strings.priceMovement,
140
+ openingLabel: strings.openingLabel,
141
+ currentLabel: strings.currentLabel,
142
+ movementWindow: strings.movementWindow,
143
+ notEnoughHistory: strings.notEnoughHistory,
144
+ singlePricePoint: strings.singlePricePoint,
145
+ selectionSuspended: strings.selectionSuspended,
146
+ excludedSuspended: strings.excludedSuspended,
147
+ chartTimeLabel: strings.chartTimeLabel,
148
+ chartAria: strings.chartAria,
113
149
  addToSlip: strings.addToSlip,
114
150
  inSlip: strings.inSlip,
115
151
  slipAdded: strings.slipAdded,
@@ -148,6 +184,54 @@ import { WidgetSdkError as WidgetSdkError2 } from "@sensiblestats/widget-sdk";
148
184
  // src/reducer.ts
149
185
  import "@sensiblestats/widget-sdk";
150
186
 
187
+ // src/chart.ts
188
+ var CHART_VIEW_W = 320;
189
+ var CHART_VIEW_H = 140;
190
+ var PLOT_X0 = 40;
191
+ var PLOT_X1 = 312;
192
+ var PLOT_Y0 = 10;
193
+ var PLOT_Y1 = 110;
194
+ var MARKER_MAX_POINTS = 8;
195
+ var FLAT_DOMAIN_PAD = 0.05;
196
+ var round2 = (n) => Math.round(n * 100) / 100;
197
+ function projectSeries(samples) {
198
+ if (samples.length < 2) return null;
199
+ const tMin = samples[0].atMs;
200
+ const tMax = samples[samples.length - 1].atMs;
201
+ if (tMax <= tMin) return null;
202
+ const prices = samples.map((s) => s.price);
203
+ const pMin = Math.min(...prices);
204
+ const pMax = Math.max(...prices);
205
+ let yMin;
206
+ let yMax;
207
+ if (pMax === pMin) {
208
+ yMin = pMin - FLAT_DOMAIN_PAD;
209
+ yMax = pMax + FLAT_DOMAIN_PAD;
210
+ } else {
211
+ const pad = (pMax - pMin) * 0.1;
212
+ yMin = pMin - pad;
213
+ yMax = pMax + pad;
214
+ }
215
+ const x = (atMs) => round2(PLOT_X0 + (atMs - tMin) / (tMax - tMin) * (PLOT_X1 - PLOT_X0));
216
+ const y = (price) => round2(PLOT_Y1 - (price - yMin) / (yMax - yMin) * (PLOT_Y1 - PLOT_Y0));
217
+ const projected = samples.map((s) => ({ x: x(s.atMs), y: y(s.price) }));
218
+ const path = projected.map((p, i) => `${i === 0 ? "M" : "L"}${p.x} ${p.y}`).join(" ");
219
+ return {
220
+ path,
221
+ markers: samples.length <= MARKER_MAX_POINTS ? projected : [],
222
+ // The real extremes, never the padded bounds: a gridline label is a price, and the padded
223
+ // bounds are prices nobody was ever offered. A flat series has one real extreme, not two —
224
+ // two identical gridlines would collide on both React key and rendered text.
225
+ gridlines: pMax === pMin ? [{ y: y(pMax), price: pMax }] : [
226
+ { y: y(pMax), price: pMax },
227
+ { y: y(pMin), price: pMin }
228
+ ],
229
+ viewBox: `0 0 ${CHART_VIEW_W} ${CHART_VIEW_H}`,
230
+ startMs: tMin,
231
+ endMs: tMax
232
+ };
233
+ }
234
+
151
235
  // src/model.ts
152
236
  function str(v) {
153
237
  return typeof v === "string" && v.length > 0 ? v : void 0;
@@ -213,6 +297,9 @@ function toInsightVM(insight) {
213
297
  const fixture = home && away ? `${home} vs ${away}` : void 0;
214
298
  const competition = str(insight.competitionName);
215
299
  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;
300
+ const previousValue = insight.odds?.previousValue;
301
+ const previous = typeof previousValue === "number" && Number.isFinite(previousValue) ? previousValue : void 0;
302
+ const direction = insight.odds?.movementDirection === "shortened" || insight.odds?.movementDirection === "drifted" ? insight.odds.movementDirection : void 0;
216
303
  return {
217
304
  id: str(insight.insightId) ?? `${str(insight.ruleId) ?? "insight"}:${num(insight.matchId) ?? ""}`,
218
305
  market,
@@ -232,7 +319,9 @@ function toInsightVM(insight) {
232
319
  matchOddsId,
233
320
  addToSlipEnabled: insight.addToSlipEnabled === true,
234
321
  oddsDecimal: typeof oddsValue === "number" && Number.isFinite(oddsValue) ? oddsValue : void 0,
235
- quotedAtUtc: str(insight.odds?.quotedAtUtc)
322
+ quotedAtUtc: str(insight.odds?.quotedAtUtc),
323
+ previousOdds: previous !== void 0 && direction !== void 0 ? previous.toFixed(2) : void 0,
324
+ movementDirection: previous !== void 0 && direction !== void 0 ? direction : void 0
236
325
  };
237
326
  }
238
327
  function toSlipSelection(vm, conversationId) {
@@ -285,6 +374,41 @@ function toMarketOddsVM(card) {
285
374
  addToSlipEnabled: card.addToSlipEnabled === true
286
375
  };
287
376
  }
377
+ function toOddsMovementVM(card) {
378
+ const raw = Array.isArray(card.points) ? card.points : [];
379
+ const samples = [];
380
+ for (const p of raw) {
381
+ const price = typeof p.price === "number" && Number.isFinite(p.price) ? p.price : void 0;
382
+ const atMs = typeof p.atUtc === "string" ? Date.parse(p.atUtc) : NaN;
383
+ if (price === void 0 || Number.isNaN(atMs)) continue;
384
+ samples.push({ atMs, price });
385
+ }
386
+ samples.sort((a, b) => a.atMs - b.atMs);
387
+ const asNumber = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
388
+ const opening = asNumber(card.openingPrice);
389
+ const current = asNumber(card.currentPrice);
390
+ const home = str(card.homeTeamName);
391
+ const away = str(card.awayTeamName);
392
+ const selection = [str(card.selectionLabel), str(card.line)].filter(Boolean).join(" ");
393
+ const bookmakerId = typeof card.bookmakerId === "number" ? card.bookmakerId : "";
394
+ return {
395
+ id: `${typeof card.matchId === "number" ? card.matchId : 0}:${str(card.marketDeveloperName) ?? ""}:${selection}:${bookmakerId}`,
396
+ market: str(card.marketLabel) ?? str(card.marketDeveloperName) ?? "",
397
+ selection,
398
+ fixture: home && away ? `${home} vs ${away}` : void 0,
399
+ competition: str(card.competitionName),
400
+ bookmaker: str(card.bookmakerName),
401
+ opening: opening !== void 0 ? opening.toFixed(2) : "",
402
+ current: current !== void 0 ? current.toFixed(2) : "",
403
+ direction: card.movementDirection === "shortened" || card.movementDirection === "drifted" ? card.movementDirection : void 0,
404
+ windowHours: asNumber(card.windowHours) ?? 0,
405
+ excludedCount: asNumber(card.excludedSuspendedCount) ?? 0,
406
+ isSuspended: card.isSuspended === true,
407
+ disclaimer: str(card.disclaimer),
408
+ geometry: projectSeries(samples),
409
+ observationCount: samples.length
410
+ };
411
+ }
288
412
  function marketOddsRowToSlipSelection(vm, row, conversationId) {
289
413
  if (row.matchOddsId === void 0 || row.oddsDecimal === void 0) return null;
290
414
  const selection = { matchOddsId: row.matchOddsId, matchId: vm.matchId, oddsDecimal: row.oddsDecimal };
@@ -325,6 +449,8 @@ function applyEvent(state, event) {
325
449
  return { ...state, messages: mapActive(state, (t) => ({ ...t, insights: [...t.insights, toInsightVM(event.data.insight)] })) };
326
450
  case "market_odds":
327
451
  return { ...state, messages: mapActive(state, (t) => ({ ...t, marketOdds: [...t.marketOdds, toMarketOddsVM(event.data.card)] })) };
452
+ case "odds_movement":
453
+ return { ...state, messages: mapActive(state, (t) => ({ ...t, oddsMovement: [...t.oddsMovement, toOddsMovementVM(event.data.card)] })) };
328
454
  case "intent_chip": {
329
455
  const chip = toChipVM(event.data.intent);
330
456
  return chip ? { ...state, starters: [...state.starters, chip] } : state;
@@ -360,11 +486,11 @@ function reducer(state, action) {
360
486
  return { ...state, isOpen: false, isStreaming: false, status: null, actionsLoading: false };
361
487
  case "USER_SENT": {
362
488
  const user = { role: "user", id: action.userId, text: action.displayText ?? action.input.message };
363
- const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], marketOdds: [], done: false };
489
+ const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], marketOdds: [], oddsMovement: [], done: false };
364
490
  return { ...state, messages: [...state.messages, user, assistant], isStreaming: true, status: null, actionsLoading: false, starters: [], error: null, lastUserInput: action.input, lastDisplayText: action.displayText ?? null };
365
491
  }
366
492
  case "GREETING_SENT": {
367
- const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], marketOdds: [], done: false, isGreeting: true };
493
+ const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], marketOdds: [], oddsMovement: [], done: false, isGreeting: true };
368
494
  return { ...state, messages: [...state.messages, assistant], isStreaming: true, status: null, error: null };
369
495
  }
370
496
  case "DISMISS_POPUP":
@@ -721,20 +847,41 @@ function AddToSlipButton({ it, slip, ui }) {
721
847
  return /* @__PURE__ */ jsx8(SlipButton, { selection, shownOdds: it.odds ?? "", marketName: it.market, slip, ui });
722
848
  }
723
849
 
724
- // src/components/BettingInsightList.tsx
850
+ // src/components/OddsMovementBadge.tsx
725
851
  import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
852
+ function OddsMovementBadge({
853
+ previous,
854
+ current,
855
+ direction,
856
+ ui
857
+ }) {
858
+ if (previous === void 0 || direction === void 0) return null;
859
+ const directionWord2 = direction === "shortened" ? ui?.oddsShortened ?? "shortened" : ui?.oddsDrifted ?? "drifted";
860
+ const label = ui?.oddsMoved?.(previous, current, directionWord2) ?? `was ${previous}, now ${current}, ${directionWord2}`;
861
+ return (
862
+ // Strikethrough is invisible to screen readers, so the whole was/now/direction sentence rides
863
+ // on one aria-label and the visual glyphs are hidden from the accessibility tree.
864
+ /* @__PURE__ */ jsxs6("span", { className: "ss-w-move", role: "img", "aria-label": label, children: [
865
+ /* @__PURE__ */ jsx9("span", { className: "ss-w-move-prev", "aria-hidden": "true", children: previous }),
866
+ /* @__PURE__ */ jsx9("span", { className: "ss-w-move-arrow", "aria-hidden": "true", children: direction === "shortened" ? "\u25BC" : "\u25B2" })
867
+ ] })
868
+ );
869
+ }
870
+
871
+ // src/components/BettingInsightList.tsx
872
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
726
873
  function StatRows2({ stats }) {
727
- return /* @__PURE__ */ jsx9("div", { className: "ss-w-bi-rows", children: stats.map((s) => /* @__PURE__ */ jsxs6("div", { className: "ss-w-bi-row", children: [
728
- /* @__PURE__ */ jsx9("span", { className: "ss-w-bi-row-lbl", children: s.label }),
729
- /* @__PURE__ */ jsx9("span", { className: "ss-w-bi-row-num", children: s.value })
874
+ return /* @__PURE__ */ jsx10("div", { className: "ss-w-bi-rows", children: stats.map((s) => /* @__PURE__ */ jsxs7("div", { className: "ss-w-bi-row", children: [
875
+ /* @__PURE__ */ jsx10("span", { className: "ss-w-bi-row-lbl", children: s.label }),
876
+ /* @__PURE__ */ jsx10("span", { className: "ss-w-bi-row-num", children: s.value })
730
877
  ] }, `${s.label}:${s.value}`)) });
731
878
  }
732
879
  function ScopedStats({ scopes }) {
733
880
  const [active, setActive] = useState3(0);
734
881
  const current = scopes[active] ?? scopes[0] ?? { key: "", label: "", stats: [] };
735
- return /* @__PURE__ */ jsxs6("div", { className: "ss-w-bi-scoped", children: [
736
- /* @__PURE__ */ jsx9("div", { className: "ss-w-bi-scopes", role: "group", children: scopes.map((s, i) => /* @__PURE__ */ jsx9("button", { type: "button", className: "ss-w-bi-chip", "aria-pressed": i === active, onClick: () => setActive(i), children: s.label }, s.key)) }),
737
- /* @__PURE__ */ jsx9(StatRows2, { stats: current.stats }, current.key)
882
+ return /* @__PURE__ */ jsxs7("div", { className: "ss-w-bi-scoped", children: [
883
+ /* @__PURE__ */ jsx10("div", { className: "ss-w-bi-scopes", role: "group", children: scopes.map((s, i) => /* @__PURE__ */ jsx10("button", { type: "button", className: "ss-w-bi-chip", "aria-pressed": i === active, onClick: () => setActive(i), children: s.label }, s.key)) }),
884
+ /* @__PURE__ */ jsx10(StatRows2, { stats: current.stats }, current.key)
738
885
  ] });
739
886
  }
740
887
  function StatsDisclosure({ it, ui }) {
@@ -742,8 +889,8 @@ function StatsDisclosure({ it, ui }) {
742
889
  const hasScopes = !!it.scopes && it.scopes.length >= 2;
743
890
  const hasStats = hasScopes || it.stats.length > 0;
744
891
  if (!hasStats) return null;
745
- return /* @__PURE__ */ jsxs6("div", { className: "ss-w-bi-stats", children: [
746
- /* @__PURE__ */ jsxs6(
892
+ return /* @__PURE__ */ jsxs7("div", { className: "ss-w-bi-stats", children: [
893
+ /* @__PURE__ */ jsxs7(
747
894
  "button",
748
895
  {
749
896
  type: "button",
@@ -751,131 +898,189 @@ function StatsDisclosure({ it, ui }) {
751
898
  "aria-expanded": open,
752
899
  onClick: () => setOpen((v) => !v),
753
900
  children: [
754
- /* @__PURE__ */ jsx9("span", { children: open ? ui?.hideStats ?? "Hide stats" : ui?.showStats ?? "Show stats" }),
755
- /* @__PURE__ */ jsx9("span", { className: `ss-w-bi-chev${open ? " ss-w-bi-chev-up" : ""}`, "aria-hidden": "true" })
901
+ /* @__PURE__ */ jsx10("span", { children: open ? ui?.hideStats ?? "Hide stats" : ui?.showStats ?? "Show stats" }),
902
+ /* @__PURE__ */ jsx10("span", { className: `ss-w-bi-chev${open ? " ss-w-bi-chev-up" : ""}`, "aria-hidden": "true" })
756
903
  ]
757
904
  }
758
905
  ),
759
- open ? hasScopes ? /* @__PURE__ */ jsx9(ScopedStats, { scopes: it.scopes }) : /* @__PURE__ */ jsx9(StatRows2, { stats: it.stats }) : null
906
+ open ? hasScopes ? /* @__PURE__ */ jsx10(ScopedStats, { scopes: it.scopes }) : /* @__PURE__ */ jsx10(StatRows2, { stats: it.stats }) : null
760
907
  ] });
761
908
  }
762
909
  function InsightCard({ it, ui, slip }) {
763
910
  const live = it.isLive || it.phase === "live";
764
- return /* @__PURE__ */ jsxs6("div", { className: "ss-w-binsight", children: [
765
- live ? /* @__PURE__ */ jsx9("div", { className: "ss-w-bi-status", children: /* @__PURE__ */ jsxs6("span", { className: "ss-w-bi-live", children: [
766
- /* @__PURE__ */ jsx9("span", { className: "ss-w-bi-live-dot", "aria-hidden": "true" }),
911
+ return /* @__PURE__ */ jsxs7("div", { className: "ss-w-binsight", children: [
912
+ live ? /* @__PURE__ */ jsx10("div", { className: "ss-w-bi-status", children: /* @__PURE__ */ jsxs7("span", { className: "ss-w-bi-live", children: [
913
+ /* @__PURE__ */ jsx10("span", { className: "ss-w-bi-live-dot", "aria-hidden": "true" }),
767
914
  ui?.liveLabel ?? "Live"
768
915
  ] }) }) : null,
769
- /* @__PURE__ */ jsxs6("div", { className: "ss-w-bi-head", children: [
770
- /* @__PURE__ */ jsxs6("span", { className: "ss-w-bi-mkt", children: [
771
- /* @__PURE__ */ jsx9("span", { className: "ss-w-bi-market", children: it.market }),
772
- it.selection ? /* @__PURE__ */ jsx9("span", { className: "ss-w-bi-sel", children: it.selection }) : null,
773
- it.fixture || it.competition ? /* @__PURE__ */ jsxs6("span", { className: "ss-w-bi-match", children: [
774
- it.fixture ? /* @__PURE__ */ jsx9("span", { className: "ss-w-bi-fixture", children: it.fixture }) : null,
775
- it.competition ? /* @__PURE__ */ jsx9("span", { className: "ss-w-bi-comp", children: it.competition }) : null
916
+ /* @__PURE__ */ jsxs7("div", { className: "ss-w-bi-head", children: [
917
+ /* @__PURE__ */ jsxs7("span", { className: "ss-w-bi-mkt", children: [
918
+ /* @__PURE__ */ jsx10("span", { className: "ss-w-bi-market", children: it.market }),
919
+ it.selection ? /* @__PURE__ */ jsx10("span", { className: "ss-w-bi-sel", children: it.selection }) : null,
920
+ it.fixture || it.competition ? /* @__PURE__ */ jsxs7("span", { className: "ss-w-bi-match", children: [
921
+ it.fixture ? /* @__PURE__ */ jsx10("span", { className: "ss-w-bi-fixture", children: it.fixture }) : null,
922
+ it.competition ? /* @__PURE__ */ jsx10("span", { className: "ss-w-bi-comp", children: it.competition }) : null
776
923
  ] }) : null
777
924
  ] }),
778
- /* @__PURE__ */ jsxs6("div", { className: "ss-w-bi-action", children: [
779
- it.odds ? /* @__PURE__ */ jsxs6("span", { className: "ss-w-bi-odds", children: [
780
- /* @__PURE__ */ jsx9("span", { className: "ss-w-bi-odds-cap", children: ui?.oddsLabel ?? "Odds" }),
781
- /* @__PURE__ */ jsx9("b", { children: it.odds }),
782
- it.bookmaker ? /* @__PURE__ */ jsx9("span", { className: "ss-w-bi-book", children: it.bookmaker }) : null
925
+ /* @__PURE__ */ jsxs7("div", { className: "ss-w-bi-action", children: [
926
+ it.odds ? /* @__PURE__ */ jsxs7("span", { className: "ss-w-bi-odds", children: [
927
+ /* @__PURE__ */ jsx10("span", { className: "ss-w-bi-odds-cap", children: ui?.oddsLabel ?? "Odds" }),
928
+ /* @__PURE__ */ jsx10("b", { children: it.odds }),
929
+ /* @__PURE__ */ jsx10(OddsMovementBadge, { previous: it.previousOdds, current: it.odds, direction: it.movementDirection, ui }),
930
+ it.bookmaker ? /* @__PURE__ */ jsx10("span", { className: "ss-w-bi-book", children: it.bookmaker }) : null
783
931
  ] }) : null,
784
- /* @__PURE__ */ jsx9(AddToSlipButton, { it, slip, ui })
932
+ /* @__PURE__ */ jsx10(AddToSlipButton, { it, slip, ui })
785
933
  ] })
786
934
  ] }),
787
- it.text ? /* @__PURE__ */ jsx9("p", { className: "ss-w-bi-text", children: it.text }) : null,
788
- /* @__PURE__ */ jsx9(StatsDisclosure, { it, ui }),
789
- it.disclaimer ? /* @__PURE__ */ jsx9("p", { className: "ss-w-bi-disc", children: it.disclaimer }) : null
935
+ it.text ? /* @__PURE__ */ jsx10("p", { className: "ss-w-bi-text", children: it.text }) : null,
936
+ /* @__PURE__ */ jsx10(StatsDisclosure, { it, ui }),
937
+ it.disclaimer ? /* @__PURE__ */ jsx10("p", { className: "ss-w-bi-disc", children: it.disclaimer }) : null
790
938
  ] });
791
939
  }
792
940
  function BettingInsightList({ insights, ui, slip }) {
793
941
  if (!insights || insights.length === 0) return null;
794
- return /* @__PURE__ */ jsx9("div", { className: "ss-w-binsights", children: insights.map((it) => /* @__PURE__ */ jsx9(InsightCard, { it, ui, slip }, it.id)) });
942
+ return /* @__PURE__ */ jsx10("div", { className: "ss-w-binsights", children: insights.map((it) => /* @__PURE__ */ jsx10(InsightCard, { it, ui, slip }, it.id)) });
795
943
  }
796
944
 
797
945
  // src/components/MarketOddsCardList.tsx
798
946
  import { useState as useState4 } from "react";
799
- import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
947
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
800
948
  function OddsRow({ vm, row, ui, slip }) {
801
949
  const selection = vm.addToSlipEnabled ? marketOddsRowToSlipSelection(vm, row, slip.conversationId) : null;
802
- const directionWord = row.movementDirection === "shortened" ? ui?.oddsShortened ?? "shortened" : ui?.oddsDrifted ?? "drifted";
803
- const movementLabel = row.previousOdds !== void 0 ? ui?.oddsMoved?.(row.previousOdds, row.odds, directionWord) ?? `was ${row.previousOdds}, now ${row.odds}, ${directionWord}` : void 0;
804
- return /* @__PURE__ */ jsxs7("div", { className: "ss-w-mo-row", children: [
805
- /* @__PURE__ */ jsx10("span", { className: "ss-w-mo-sel", children: row.selection }),
806
- row.previousOdds !== void 0 ? (
807
- // Strikethrough is invisible to screen readers, so the whole was/now/direction sentence
808
- // rides on one aria-label and the visual glyphs are hidden from the accessibility tree.
809
- /* @__PURE__ */ jsxs7("span", { className: "ss-w-mo-move", role: "img", "aria-label": movementLabel, children: [
810
- /* @__PURE__ */ jsx10("span", { className: "ss-w-mo-prev", "aria-hidden": "true", children: row.previousOdds }),
811
- /* @__PURE__ */ jsx10("span", { className: "ss-w-mo-arrow", "aria-hidden": "true", children: row.movementDirection === "shortened" ? "\u25BC" : "\u25B2" })
812
- ] })
813
- ) : null,
814
- /* @__PURE__ */ jsx10("span", { className: "ss-w-mo-price", children: row.odds }),
815
- /* @__PURE__ */ jsx10(SlipButton, { selection, shownOdds: row.odds, marketName: vm.market, slip, ui })
950
+ return /* @__PURE__ */ jsxs8("div", { className: "ss-w-mo-row", children: [
951
+ /* @__PURE__ */ jsx11("span", { className: "ss-w-mo-sel", children: row.selection }),
952
+ /* @__PURE__ */ jsx11(OddsMovementBadge, { previous: row.previousOdds, current: row.odds, direction: row.movementDirection, ui }),
953
+ /* @__PURE__ */ jsx11("span", { className: "ss-w-mo-price", children: row.odds }),
954
+ /* @__PURE__ */ jsx11(SlipButton, { selection, shownOdds: row.odds, marketName: vm.market, slip, ui })
816
955
  ] });
817
956
  }
818
957
  function MarketOddsCard({ vm, ui, slip }) {
819
958
  const [open, setOpen] = useState4(false);
820
- return /* @__PURE__ */ jsxs7("div", { className: "ss-w-mocard", children: [
821
- /* @__PURE__ */ jsxs7("div", { className: "ss-w-mo-head", children: [
822
- /* @__PURE__ */ jsx10("span", { className: "ss-w-mo-market", children: vm.market }),
823
- /* @__PURE__ */ jsx10("span", { className: "ss-w-mo-tag", children: ui?.marketPrices ?? "Market prices" })
959
+ return /* @__PURE__ */ jsxs8("div", { className: "ss-w-mocard", children: [
960
+ /* @__PURE__ */ jsxs8("div", { className: "ss-w-mo-head", children: [
961
+ /* @__PURE__ */ jsx11("span", { className: "ss-w-mo-market", children: vm.market }),
962
+ /* @__PURE__ */ jsx11("span", { className: "ss-w-mo-tag", children: ui?.marketPrices ?? "Market prices" })
824
963
  ] }),
825
- vm.fixture || vm.competition ? /* @__PURE__ */ jsxs7("div", { className: "ss-w-mo-fixture", children: [
826
- vm.fixture ? /* @__PURE__ */ jsx10("span", { children: vm.fixture }) : null,
827
- vm.competition ? /* @__PURE__ */ jsx10("span", { className: "ss-w-mo-comp", children: vm.competition }) : null
964
+ vm.fixture || vm.competition ? /* @__PURE__ */ jsxs8("div", { className: "ss-w-mo-fixture", children: [
965
+ vm.fixture ? /* @__PURE__ */ jsx11("span", { children: vm.fixture }) : null,
966
+ vm.competition ? /* @__PURE__ */ jsx11("span", { className: "ss-w-mo-comp", children: vm.competition }) : null
828
967
  ] }) : null,
829
- /* @__PURE__ */ jsxs7("div", { className: "ss-w-mo-rows", children: [
830
- vm.mainRows.map((row) => /* @__PURE__ */ jsx10(OddsRow, { vm, row, ui, slip }, row.key)),
831
- open ? vm.moreRows.map((row) => /* @__PURE__ */ jsx10(OddsRow, { vm, row, ui, slip }, row.key)) : null
968
+ /* @__PURE__ */ jsxs8("div", { className: "ss-w-mo-rows", children: [
969
+ vm.mainRows.map((row) => /* @__PURE__ */ jsx11(OddsRow, { vm, row, ui, slip }, row.key)),
970
+ open ? vm.moreRows.map((row) => /* @__PURE__ */ jsx11(OddsRow, { vm, row, ui, slip }, row.key)) : null
832
971
  ] }),
833
- vm.moreRows.length > 0 ? /* @__PURE__ */ jsxs7("button", { type: "button", className: "ss-w-bi-toggle", "aria-expanded": open, onClick: () => setOpen((v) => !v), children: [
834
- /* @__PURE__ */ jsx10("span", { children: open ? ui?.hideLines ?? "Hide lines" : `${ui?.moreLines ?? "More lines"} (${vm.moreRows.length})` }),
835
- /* @__PURE__ */ jsx10("span", { className: `ss-w-bi-chev${open ? " ss-w-bi-chev-up" : ""}`, "aria-hidden": "true" })
972
+ vm.moreRows.length > 0 ? /* @__PURE__ */ jsxs8("button", { type: "button", className: "ss-w-bi-toggle", "aria-expanded": open, onClick: () => setOpen((v) => !v), children: [
973
+ /* @__PURE__ */ jsx11("span", { children: open ? ui?.hideLines ?? "Hide lines" : `${ui?.moreLines ?? "More lines"} (${vm.moreRows.length})` }),
974
+ /* @__PURE__ */ jsx11("span", { className: `ss-w-bi-chev${open ? " ss-w-bi-chev-up" : ""}`, "aria-hidden": "true" })
836
975
  ] }) : null,
837
- vm.bookmaker ? /* @__PURE__ */ jsx10("div", { className: "ss-w-mo-foot", children: /* @__PURE__ */ jsx10("span", { children: vm.bookmaker }) }) : null,
838
- vm.disclaimer ? /* @__PURE__ */ jsx10("p", { className: "ss-w-bi-disc", children: vm.disclaimer }) : null
976
+ vm.bookmaker ? /* @__PURE__ */ jsx11("div", { className: "ss-w-mo-foot", children: /* @__PURE__ */ jsx11("span", { children: vm.bookmaker }) }) : null,
977
+ vm.disclaimer ? /* @__PURE__ */ jsx11("p", { className: "ss-w-bi-disc", children: vm.disclaimer }) : null
839
978
  ] });
840
979
  }
841
980
  function MarketOddsCardList({ cards, ui, slip }) {
842
981
  if (!cards || cards.length === 0) return null;
843
- return /* @__PURE__ */ jsx10("div", { className: "ss-w-mocards", children: cards.map((vm) => /* @__PURE__ */ jsx10(MarketOddsCard, { vm, ui, slip }, vm.id)) });
982
+ return /* @__PURE__ */ jsx11("div", { className: "ss-w-mocards", children: cards.map((vm) => /* @__PURE__ */ jsx11(MarketOddsCard, { vm, ui, slip }, vm.id)) });
983
+ }
984
+
985
+ // src/components/OddsMovementCard.tsx
986
+ import { Fragment, jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
987
+ function directionWord(vm, ui) {
988
+ if (vm.direction === "shortened") return ui?.oddsShortened ?? "shortened";
989
+ if (vm.direction === "drifted") return ui?.oddsDrifted ?? "drifted";
990
+ return "";
991
+ }
992
+ function OddsMovementCard({ vm, ui }) {
993
+ 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)}.`;
994
+ return /* @__PURE__ */ jsxs9("div", { className: "ss-w-omcard", children: [
995
+ /* @__PURE__ */ jsxs9("div", { className: "ss-w-om-head", children: [
996
+ /* @__PURE__ */ jsx12("span", { className: "ss-w-om-market", children: vm.market }),
997
+ /* @__PURE__ */ jsx12("span", { className: "ss-w-om-tag", children: ui?.priceMovement ?? "Price movement" })
998
+ ] }),
999
+ vm.fixture ? /* @__PURE__ */ jsxs9("div", { className: "ss-w-om-fixture", children: [
1000
+ /* @__PURE__ */ jsx12("span", { children: vm.fixture }),
1001
+ vm.competition ? /* @__PURE__ */ jsx12("span", { className: "ss-w-om-comp", children: vm.competition }) : null
1002
+ ] }) : null,
1003
+ /* @__PURE__ */ jsx12("div", { className: "ss-w-om-sel", children: vm.selection }),
1004
+ /* @__PURE__ */ jsx12(OddsMovementBadge, { previous: vm.opening, current: vm.current, direction: vm.direction, ui }),
1005
+ vm.geometry === null ? /* @__PURE__ */ jsx12("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__ */ jsxs9(
1006
+ "svg",
1007
+ {
1008
+ className: "ss-w-om-chart",
1009
+ viewBox: vm.geometry.viewBox,
1010
+ width: "100%",
1011
+ role: "img",
1012
+ "aria-label": aria,
1013
+ children: [
1014
+ vm.geometry.gridlines.map((g) => /* @__PURE__ */ jsxs9("g", { children: [
1015
+ /* @__PURE__ */ jsx12("line", { className: "ss-w-om-grid", x1: 40, y1: g.y, x2: CHART_VIEW_W - 8, y2: g.y }),
1016
+ /* @__PURE__ */ jsx12("text", { className: "ss-w-om-ylab", x: 34, y: g.y + 3, textAnchor: "end", children: g.price.toFixed(2) })
1017
+ ] }, g.price)),
1018
+ /* @__PURE__ */ jsx12("path", { className: "ss-w-om-line", d: vm.geometry.path, fill: "none" }),
1019
+ vm.geometry.markers.map((m, i) => /* @__PURE__ */ jsx12("circle", { className: "ss-w-om-dot", cx: m.x, cy: m.y, r: 2.5 }, i)),
1020
+ ui?.chartTimeLabel ? /* @__PURE__ */ jsxs9(Fragment, { children: [
1021
+ /* @__PURE__ */ jsx12("text", { className: "ss-w-om-xlab", x: 40, y: CHART_VIEW_H - 8, textAnchor: "start", children: ui.chartTimeLabel(vm.geometry.startMs) }),
1022
+ /* @__PURE__ */ jsx12("text", { className: "ss-w-om-xlab", x: CHART_VIEW_W - 8, y: CHART_VIEW_H - 8, textAnchor: "end", children: ui.chartTimeLabel(vm.geometry.endMs) })
1023
+ ] }) : /* @__PURE__ */ jsx12("text", { className: "ss-w-om-xlab", x: 40, y: CHART_VIEW_H - 8, children: ui?.movementWindow ? ui.movementWindow(vm.windowHours) : `Last ${vm.windowHours}h` })
1024
+ ]
1025
+ }
1026
+ ),
1027
+ /* @__PURE__ */ jsxs9("div", { className: "ss-w-om-foot", children: [
1028
+ /* @__PURE__ */ jsxs9("span", { children: [
1029
+ ui?.openingLabel ?? "Opening",
1030
+ " ",
1031
+ /* @__PURE__ */ jsx12("b", { children: vm.opening })
1032
+ ] }),
1033
+ /* @__PURE__ */ jsxs9("span", { children: [
1034
+ ui?.currentLabel ?? "Current",
1035
+ " ",
1036
+ /* @__PURE__ */ jsx12("b", { children: vm.current })
1037
+ ] })
1038
+ ] }),
1039
+ vm.bookmaker ? /* @__PURE__ */ jsx12("div", { className: "ss-w-om-foot", children: /* @__PURE__ */ jsx12("span", { children: vm.bookmaker }) }) : null,
1040
+ vm.excludedCount > 0 ? /* @__PURE__ */ jsx12("div", { className: "ss-w-om-excl", children: ui?.excludedSuspended ? ui.excludedSuspended(vm.excludedCount) : `${vm.excludedCount} suspended price(s) excluded from this chart.` }) : null,
1041
+ vm.isSuspended ? /* @__PURE__ */ jsx12("div", { className: "ss-w-om-susp", children: ui?.selectionSuspended ?? "This selection is not currently available to bet." }) : null,
1042
+ vm.disclaimer ? /* @__PURE__ */ jsx12("div", { className: "ss-w-om-disc", children: vm.disclaimer }) : null
1043
+ ] });
1044
+ }
1045
+ function OddsMovementCardList({ cards, ui }) {
1046
+ if (!cards || cards.length === 0) return null;
1047
+ return /* @__PURE__ */ jsx12("div", { className: "ss-w-omcards", children: cards.map((vm) => /* @__PURE__ */ jsx12(OddsMovementCard, { vm, ui }, vm.id)) });
844
1048
  }
845
1049
 
846
1050
  // src/components/ActionButtons.tsx
847
- import { Fragment, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
1051
+ import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
848
1052
  function ActionButtons({ actions, loading, disabled, onAct }) {
849
1053
  if (actions.length === 0 && !loading) return null;
850
- return /* @__PURE__ */ jsxs8("div", { className: "ss-w-actions", children: [
851
- actions.map((a, i) => /* @__PURE__ */ jsx11("button", { type: "button", className: "ss-w-abtn", disabled, onClick: () => onAct(a), children: a.label }, `${a.label}-${i}`)),
852
- actions.length === 0 && loading ? /* @__PURE__ */ jsxs8(Fragment, { children: [
853
- /* @__PURE__ */ jsx11("span", { className: "ss-w-abtn-shimmer" }),
854
- /* @__PURE__ */ jsx11("span", { className: "ss-w-abtn-shimmer" })
1054
+ return /* @__PURE__ */ jsxs10("div", { className: "ss-w-actions", children: [
1055
+ actions.map((a, i) => /* @__PURE__ */ jsx13("button", { type: "button", className: "ss-w-abtn", disabled, onClick: () => onAct(a), children: a.label }, `${a.label}-${i}`)),
1056
+ actions.length === 0 && loading ? /* @__PURE__ */ jsxs10(Fragment2, { children: [
1057
+ /* @__PURE__ */ jsx13("span", { className: "ss-w-abtn-shimmer" }),
1058
+ /* @__PURE__ */ jsx13("span", { className: "ss-w-abtn-shimmer" })
855
1059
  ] }) : null
856
1060
  ] });
857
1061
  }
858
1062
 
859
1063
  // src/components/ChatMessage.tsx
860
- import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
1064
+ import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
861
1065
  function ChatMessage({ message, disabled, onAct, ui, slip }) {
862
1066
  if (message.role === "user") {
863
- return /* @__PURE__ */ jsx12("div", { className: "ss-w-msg ss-w-user", children: /* @__PURE__ */ jsx12("div", { className: "ss-w-bubble", children: message.text }) });
1067
+ return /* @__PURE__ */ jsx14("div", { className: "ss-w-msg ss-w-user", children: /* @__PURE__ */ jsx14("div", { className: "ss-w-bubble", children: message.text }) });
864
1068
  }
865
- return /* @__PURE__ */ jsxs9("div", { className: "ss-w-msg ss-w-bot", children: [
866
- message.text ? /* @__PURE__ */ jsx12(Markdown, { text: message.text }) : null,
867
- /* @__PURE__ */ jsx12(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
868
- /* @__PURE__ */ jsx12(BettingInsightList, { insights: message.insights, ui, slip }),
869
- /* @__PURE__ */ jsx12(MarketOddsCardList, { cards: message.marketOdds, ui, slip }),
870
- /* @__PURE__ */ jsx12(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
1069
+ return /* @__PURE__ */ jsxs11("div", { className: "ss-w-msg ss-w-bot", children: [
1070
+ message.text ? /* @__PURE__ */ jsx14(Markdown, { text: message.text }) : null,
1071
+ /* @__PURE__ */ jsx14(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
1072
+ /* @__PURE__ */ jsx14(BettingInsightList, { insights: message.insights, ui, slip }),
1073
+ /* @__PURE__ */ jsx14(MarketOddsCardList, { cards: message.marketOdds, ui, slip }),
1074
+ /* @__PURE__ */ jsx14(OddsMovementCardList, { cards: message.oddsMovement, ui }),
1075
+ /* @__PURE__ */ jsx14(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
871
1076
  ] });
872
1077
  }
873
1078
 
874
1079
  // src/components/IntentChips.tsx
875
- import { jsx as jsx13 } from "react/jsx-runtime";
1080
+ import { jsx as jsx15 } from "react/jsx-runtime";
876
1081
  function IntentChips({ chips, onPick }) {
877
1082
  if (chips.length === 0) return null;
878
- return /* @__PURE__ */ jsx13("div", { className: "ss-w-chips", children: chips.map((c, i) => /* @__PURE__ */ jsx13("button", { type: "button", className: "ss-w-chip", onClick: () => onPick(c.text), children: c.text }, `${c.text}-${i}`)) });
1083
+ return /* @__PURE__ */ jsx15("div", { className: "ss-w-chips", children: chips.map((c, i) => /* @__PURE__ */ jsx15("button", { type: "button", className: "ss-w-chip", onClick: () => onPick(c.text), children: c.text }, `${c.text}-${i}`)) });
879
1084
  }
880
1085
 
881
1086
  // src/scroll.ts
@@ -885,7 +1090,7 @@ function isPinnedToBottom(el, threshold = PIN_THRESHOLD) {
885
1090
  }
886
1091
 
887
1092
  // src/components/MessageList.tsx
888
- import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
1093
+ import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
889
1094
  function MessageList({ controller, ui, slip }) {
890
1095
  const { state, send } = controller;
891
1096
  const listRef = useRef2(null);
@@ -895,7 +1100,7 @@ function MessageList({ controller, ui, slip }) {
895
1100
  if (pinnedRef.current) endRef.current?.scrollIntoView({ block: "end" });
896
1101
  }, [state.messages, state.status]);
897
1102
  const empty = state.messages.length === 0;
898
- return /* @__PURE__ */ jsxs10(
1103
+ return /* @__PURE__ */ jsxs12(
899
1104
  "div",
900
1105
  {
901
1106
  className: "ss-w-list",
@@ -905,7 +1110,7 @@ function MessageList({ controller, ui, slip }) {
905
1110
  if (listRef.current) pinnedRef.current = isPinnedToBottom(listRef.current);
906
1111
  },
907
1112
  children: [
908
- state.messages.map((m) => /* @__PURE__ */ jsx14(
1113
+ state.messages.map((m) => /* @__PURE__ */ jsx16(
909
1114
  ChatMessage,
910
1115
  {
911
1116
  message: m,
@@ -916,15 +1121,15 @@ function MessageList({ controller, ui, slip }) {
916
1121
  },
917
1122
  m.id
918
1123
  )),
919
- state.isStreaming && state.actionsLoading ? /* @__PURE__ */ jsx14(ActionButtons, { actions: [], loading: true, onAct: () => {
1124
+ state.isStreaming && state.actionsLoading ? /* @__PURE__ */ jsx16(ActionButtons, { actions: [], loading: true, onAct: () => {
920
1125
  } }) : null,
921
- state.status ? /* @__PURE__ */ jsx14("div", { className: "ss-w-status", children: state.status }) : null,
922
- state.error ? /* @__PURE__ */ jsxs10("div", { className: "ss-w-error", role: "alert", children: [
923
- /* @__PURE__ */ jsx14("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
924
- /* @__PURE__ */ jsx14("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
1126
+ state.status ? /* @__PURE__ */ jsx16("div", { className: "ss-w-status", children: state.status }) : null,
1127
+ state.error ? /* @__PURE__ */ jsxs12("div", { className: "ss-w-error", role: "alert", children: [
1128
+ /* @__PURE__ */ jsx16("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
1129
+ /* @__PURE__ */ jsx16("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
925
1130
  ] }) : null,
926
- empty ? /* @__PURE__ */ jsx14(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
927
- /* @__PURE__ */ jsx14("div", { ref: endRef })
1131
+ empty ? /* @__PURE__ */ jsx16(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
1132
+ /* @__PURE__ */ jsx16("div", { ref: endRef })
928
1133
  ]
929
1134
  }
930
1135
  );
@@ -932,7 +1137,7 @@ function MessageList({ controller, ui, slip }) {
932
1137
 
933
1138
  // src/components/ChatInput.tsx
934
1139
  import { useState as useState5 } from "react";
935
- import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
1140
+ import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
936
1141
  function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop response", sendLabel = "Send message" }) {
937
1142
  const [value, setValue] = useState5("");
938
1143
  const submit = () => {
@@ -947,14 +1152,14 @@ function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop r
947
1152
  submit();
948
1153
  }
949
1154
  };
950
- return /* @__PURE__ */ jsxs11("div", { className: "ss-w-input", children: [
951
- /* @__PURE__ */ jsx15("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
952
- streaming ? /* @__PURE__ */ jsx15("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": stopLabel, onClick: onStop, children: /* @__PURE__ */ jsx15("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ jsx15("button", { type: "button", className: "ss-w-send", "aria-label": sendLabel, onClick: submit, children: /* @__PURE__ */ jsx15(SendIcon, {}) })
1155
+ return /* @__PURE__ */ jsxs13("div", { className: "ss-w-input", children: [
1156
+ /* @__PURE__ */ jsx17("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
1157
+ streaming ? /* @__PURE__ */ jsx17("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": stopLabel, onClick: onStop, children: /* @__PURE__ */ jsx17("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ jsx17("button", { type: "button", className: "ss-w-send", "aria-label": sendLabel, onClick: submit, children: /* @__PURE__ */ jsx17(SendIcon, {}) })
953
1158
  ] });
954
1159
  }
955
1160
 
956
1161
  // src/components/ChatPanel.tsx
957
- import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
1162
+ import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
958
1163
  var FULLSCREEN_QUERY = "(max-width: 639px)";
959
1164
  function useIsModal() {
960
1165
  const [modal, setModal] = useState6(false);
@@ -978,10 +1183,10 @@ function ChatPanel({ controller, ui, slip }) {
978
1183
  window.addEventListener("keydown", onKey);
979
1184
  return () => window.removeEventListener("keydown", onKey);
980
1185
  }, [close]);
981
- return /* @__PURE__ */ jsxs12("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
982
- /* @__PURE__ */ jsx16(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
983
- /* @__PURE__ */ jsx16(MessageList, { controller, ui, slip }),
984
- /* @__PURE__ */ jsx16(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
1186
+ return /* @__PURE__ */ jsxs14("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
1187
+ /* @__PURE__ */ jsx18(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
1188
+ /* @__PURE__ */ jsx18(MessageList, { controller, ui, slip }),
1189
+ /* @__PURE__ */ jsx18(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
985
1190
  ] });
986
1191
  }
987
1192
 
@@ -1170,13 +1375,40 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
1170
1375
  .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); }
1171
1376
  /* Movement: BOTH directions share --ss-w-faint on purpose. Green-good/red-bad would frame a
1172
1377
  shortening price as a discount, which inverts what it actually means for the bettor. */
1173
- .ss-w-mo-move { margin-left: auto; display: inline-flex; align-items: baseline; gap: 4px; color: var(--ss-w-faint); }
1174
- .ss-w-mo-prev { font-family: ui-monospace, monospace; font-variant-numeric: tabular-nums; font-size: 12px; text-decoration: line-through; }
1175
- .ss-w-mo-arrow { font-size: 9px; line-height: 1; }
1176
- .ss-w-mo-move + .ss-w-mo-price { margin-left: 0; }
1378
+ .ss-w-move { display: inline-flex; align-items: baseline; gap: 4px; color: var(--ss-w-faint); }
1379
+ .ss-w-move-prev { font-family: ui-monospace, monospace; font-variant-numeric: tabular-nums; font-size: 12px; text-decoration: line-through; }
1380
+ .ss-w-move-arrow { font-size: 9px; line-height: 1; }
1381
+ /* margin-left:auto is row layout, not badge styling -- it belongs to the market-odds row, which is
1382
+ a flex row, and must NOT follow the badge into the insight card's odds column. */
1383
+ .ss-w-mo-row .ss-w-move { margin-left: auto; }
1384
+ .ss-w-mo-row .ss-w-move + .ss-w-mo-price { margin-left: 0; }
1177
1385
  .ss-w-mo-row .ss-w-slip { margin: 0; }
1178
1386
  .ss-w-mo-foot { display: flex; align-items: center; gap: 8px; font-size: 10.5px; color: var(--ss-w-faint); }
1179
1387
 
1388
+ /* odds movement card */
1389
+ .ss-w-omcards { display: flex; flex-direction: column; gap: 7px; }
1390
+ .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; }
1391
+ .ss-w-om-head { display: flex; align-items: center; gap: 8px; }
1392
+ .ss-w-om-market { font-size: 13px; font-weight: 700; color: var(--ss-w-ink); }
1393
+ .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; }
1394
+ .ss-w-om-fixture { display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--ss-w-faint); min-width: 0; }
1395
+ .ss-w-om-comp { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1396
+ .ss-w-om-sel { font-size: 12.5px; font-weight: 600; color: var(--ss-w-ink); }
1397
+ .ss-w-om-chart { display: block; width: 100%; height: auto; }
1398
+ /* Single neutral accent in BOTH directions, deliberately. Colouring a shortening price green
1399
+ frames it as a discount, which inverts what it means for the bettor -- the same reason
1400
+ .ss-w-move keeps both directions on --ss-w-faint. */
1401
+ .ss-w-om-line { stroke: var(--ss-w-accent); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
1402
+ .ss-w-om-dot { fill: var(--ss-w-accent); }
1403
+ .ss-w-om-grid { stroke: var(--ss-w-line); stroke-width: 1; }
1404
+ .ss-w-om-ylab, .ss-w-om-xlab { fill: var(--ss-w-faint); font-size: 9px; font-family: ui-monospace, monospace; }
1405
+ .ss-w-om-empty { font-size: 11.5px; color: var(--ss-w-faint); padding: 6px 0; }
1406
+ .ss-w-om-foot { display: flex; align-items: center; gap: 14px; font-size: 11.5px; color: var(--ss-w-faint); }
1407
+ .ss-w-om-foot b { font-family: ui-monospace, monospace; font-variant-numeric: tabular-nums; font-size: 13px; color: var(--ss-w-ink); }
1408
+ .ss-w-om-excl { font-size: 10.5px; color: var(--ss-w-faint); }
1409
+ .ss-w-om-susp { font-size: 11px; font-weight: 600; color: #c0392b; }
1410
+ .ss-w-om-disc { font-size: 10px; font-style: italic; color: var(--ss-w-faint); }
1411
+
1180
1412
  /* action buttons */
1181
1413
  .ss-w-actions { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
1182
1414
  .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; }
@@ -1214,7 +1446,7 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
1214
1446
  `;
1215
1447
 
1216
1448
  // src/StatsWidget.tsx
1217
- import { jsx as jsx17 } from "react/jsx-runtime";
1449
+ import { jsx as jsx19 } from "react/jsx-runtime";
1218
1450
  function buildClient(props) {
1219
1451
  try {
1220
1452
  if ("client" in props && props.client) {
@@ -1263,7 +1495,7 @@ function StatsWidget(props) {
1263
1495
  }, [built, ui.injectStyles]);
1264
1496
  if (!built) return null;
1265
1497
  const attrs = themeAttrs(ui);
1266
- return /* @__PURE__ */ jsx17("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__ */ jsx17(ChatPanel, { controller, ui, slip }) : /* @__PURE__ */ jsx17(
1498
+ return /* @__PURE__ */ jsx19("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__ */ jsx19(ChatPanel, { controller, ui, slip }) : /* @__PURE__ */ jsx19(
1267
1499
  ChatFab,
1268
1500
  {
1269
1501
  label: ui.launcherLabel,