@sensiblestats/widget-react 0.45.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # @sensiblestats/widget-react
2
2
 
3
+ ## 0.47.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Shows a half-time label instead of a frozen 45', ages the live minute against when the reading was
8
+ actually taken so it no longer sits up to two minutes behind, and stops asserting a clock for a
9
+ single match whose sync window has stalled.
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies
14
+ - @sensiblestats/widget-sdk@0.35.0
15
+
16
+ ## 0.46.0
17
+
18
+ ### Minor Changes
19
+
20
+ - Fixes the Today tab collapsing to blank bars: children of the monitor scroller no longer shrink,
21
+ so the list scrolls instead of compressing 27 competition groups to fit. Also disambiguates
22
+ same-named competitions with their country, which the schedule response now carries.
23
+
24
+ ### Patch Changes
25
+
26
+ - Updated dependencies
27
+ - @sensiblestats/widget-sdk@0.34.0
28
+
3
29
  ## 0.45.0
4
30
 
5
31
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -128,6 +128,7 @@ var STRINGS = {
128
128
  accordionSuspended: "Suspended",
129
129
  tabToday: "Today",
130
130
  sectionLive: "Live",
131
+ halfTime: "HT",
131
132
  staleStates: "Live scores can't be updated \u2014 showing the last known state."
132
133
  },
133
134
  tr: {
@@ -205,6 +206,7 @@ var STRINGS = {
205
206
  accordionSuspended: "Ask\u0131da",
206
207
  tabToday: "Bug\xFCn",
207
208
  sectionLive: "Canl\u0131",
209
+ halfTime: "\u0130Y",
208
210
  staleStates: "Canl\u0131 skorlar g\xFCncellenemiyor \u2014 son bilinen durum g\xF6steriliyor."
209
211
  }
210
212
  };
@@ -283,6 +285,7 @@ function normalizeUi(cfg) {
283
285
  tabOdds: strings.tabOdds,
284
286
  tabToday: strings.tabToday,
285
287
  sectionLive: strings.sectionLive,
288
+ halfTime: strings.halfTime,
286
289
  staleStates: strings.staleStates,
287
290
  pinLabel: strings.pinLabel,
288
291
  unpinLabel: strings.unpinLabel,
@@ -1542,8 +1545,40 @@ function dueMatchIds(groups, states, nowMs) {
1542
1545
  return [...due];
1543
1546
  }
1544
1547
 
1548
+ // src/liveClock.ts
1549
+ var STALE_READING_MS = 5 * 6e4;
1550
+ function parseMinute(label) {
1551
+ if (!label) return null;
1552
+ const plus = /^(\d+)\+(\d+)'$/.exec(label);
1553
+ if (plus) return Number(plus[1]) + Number(plus[2]);
1554
+ const plain = /^(\d+)'$/.exec(label);
1555
+ return plain ? Number(plain[1]) : null;
1556
+ }
1557
+ function readingAgeMs(state, nowMs) {
1558
+ if (!state?.stateAsOfUtc) return null;
1559
+ const t = Date.parse(state.stateAsOfUtc);
1560
+ return Number.isNaN(t) ? null : Math.max(0, nowMs - t);
1561
+ }
1562
+ function isReadingStale(state, nowMs) {
1563
+ if (state?.isHalfTime) return false;
1564
+ const age = readingAgeMs(state, nowMs);
1565
+ return age !== null && age > STALE_READING_MS;
1566
+ }
1567
+ function displayMinute(state, nowMs) {
1568
+ if (!state || state.phase !== "live") return null;
1569
+ if (state.isHalfTime) return null;
1570
+ if (isReadingStale(state, nowMs)) return null;
1571
+ const base = parseMinute(state.minuteLabel);
1572
+ if (base === null) return null;
1573
+ const age = readingAgeMs(state, nowMs);
1574
+ if (age === null) return state.minuteLabel ?? null;
1575
+ const advanced = base + Math.floor(age / 6e4);
1576
+ return `${advanced}'`;
1577
+ }
1578
+
1545
1579
  // src/useMatchDiscovery.ts
1546
1580
  var POLL_MS = 6e4;
1581
+ var CLOCK_MS = 1e4;
1547
1582
  var ROLLOVER_CHECK_MS = 6e4;
1548
1583
  var STALE_AFTER_FAILURES = 2;
1549
1584
  function localDateOf(now) {
@@ -1556,6 +1591,7 @@ function useMatchDiscovery(opts) {
1556
1591
  const [groups, setGroups] = (0, import_react4.useState)([]);
1557
1592
  const [states, setStates] = (0, import_react4.useState)({});
1558
1593
  const [failures, setFailures] = (0, import_react4.useState)(0);
1594
+ const [nowMs, setNowMs] = (0, import_react4.useState)(() => Date.now());
1559
1595
  const [expanded, setExpanded] = (0, import_react4.useState)({});
1560
1596
  const [touched, setTouched] = (0, import_react4.useState)({});
1561
1597
  const groupsRef = (0, import_react4.useRef)(groups);
@@ -1633,16 +1669,21 @@ function useMatchDiscovery(opts) {
1633
1669
  }, POLL_MS);
1634
1670
  return () => clearInterval(id);
1635
1671
  }, [opts.isOpen, tick, groups]);
1672
+ (0, import_react4.useEffect)(() => {
1673
+ if (!opts.isOpen) return;
1674
+ const id = setInterval(() => setNowMs(Date.now()), CLOCK_MS);
1675
+ return () => clearInterval(id);
1676
+ }, [opts.isOpen]);
1636
1677
  const stale = failures >= STALE_AFTER_FAILURES;
1637
1678
  const liveCount = (0, import_react4.useMemo)(
1638
- () => stale ? 0 : Object.values(states).filter((s) => s.phase === "live").length,
1639
- [states, stale]
1679
+ () => stale ? 0 : Object.values(states).filter((s) => s.phase === "live" && !isReadingStale(s, nowMs)).length,
1680
+ [states, stale, nowMs]
1640
1681
  );
1641
1682
  const toggleGroup = (0, import_react4.useCallback)((competitionId) => {
1642
1683
  setTouched((prev) => ({ ...prev, [competitionId]: true }));
1643
1684
  setExpanded((prev) => ({ ...prev, [competitionId]: !prev[competitionId] }));
1644
1685
  }, []);
1645
- return { groups, states, liveCount, stale, expanded, toggleGroup, _tick: tick };
1686
+ return { groups, states, nowMs, liveCount, stale, expanded, toggleGroup, _tick: tick };
1646
1687
  }
1647
1688
 
1648
1689
  // src/eventLogContext.ts
@@ -3000,7 +3041,7 @@ function computeDisplayMinute(minute, clockRunning, receivedAtMs) {
3000
3041
  return minute + Math.min(Math.max(0, elapsedMinutes), MAX_LIVE_TICK_MINUTES);
3001
3042
  }
3002
3043
  function useLiveClock(minute, clockRunning, receivedAtMs) {
3003
- const [displayMinute, setDisplayMinute] = (0, import_react19.useState)(
3044
+ const [displayMinute2, setDisplayMinute] = (0, import_react19.useState)(
3004
3045
  () => computeDisplayMinute(minute, clockRunning, receivedAtMs)
3005
3046
  );
3006
3047
  (0, import_react19.useEffect)(() => {
@@ -3011,7 +3052,7 @@ function useLiveClock(minute, clockRunning, receivedAtMs) {
3011
3052
  }, TICK_MS);
3012
3053
  return () => window.clearInterval(id);
3013
3054
  }, [minute, clockRunning, receivedAtMs]);
3014
- return displayMinute;
3055
+ return displayMinute2;
3015
3056
  }
3016
3057
 
3017
3058
  // src/components/dashboard/LiveScoreCard.tsx
@@ -3043,7 +3084,7 @@ function LiveScoreCard({
3043
3084
  if (!seedRef.current || seedRef.current.key !== seedKey) {
3044
3085
  seedRef.current = { key: seedKey, receivedAtMs: Date.now() };
3045
3086
  }
3046
- const displayMinute = useLiveClock(data.minute, data.clockRunning, seedRef.current.receivedAtMs);
3087
+ const displayMinute2 = useLiveClock(data.minute, data.clockRunning, seedRef.current.receivedAtMs);
3047
3088
  const minuteSuffix = data.minuteLabel.replace(/^\d+/, "");
3048
3089
  const scorers = scorersLine(goalEvents);
3049
3090
  return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "ss-w-fandash-card ss-w-fandash-live-score", children: [
@@ -3053,7 +3094,7 @@ function LiveScoreCard({
3053
3094
  /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "ss-w-fandash-live-dot", "aria-hidden": "true" }),
3054
3095
  data.statusLabel
3055
3096
  ] }),
3056
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "ss-w-fandash-live-minute", children: `${displayMinute}${minuteSuffix}` }),
3097
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "ss-w-fandash-live-minute", children: `${displayMinute2}${minuteSuffix}` }),
3057
3098
  data.competition ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "ss-w-fandash-live-competition", children: data.competition }) : null
3058
3099
  ] }),
3059
3100
  /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "ss-w-fandash-live-main", children: [
@@ -3735,12 +3776,14 @@ function Side({ name, crest }) {
3735
3776
  /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "ss-w-drow-nm", children: name })
3736
3777
  ] });
3737
3778
  }
3738
- function FixtureRow({ match, state, stale, ui, monitor }) {
3739
- const isLive = state?.phase === "live" && !stale;
3779
+ function FixtureRow({ match, state, stale, nowMs, ui, monitor }) {
3780
+ const readingStale = isReadingStale(state, nowMs);
3781
+ const isLive = state?.phase === "live" && !stale && !readingStale;
3740
3782
  const hasScore = (state?.phase === "live" || state?.phase === "fullTime") && typeof state.homeScore === "number" && typeof state.awayScore === "number";
3741
- const when = stale && state?.phase === "live" ? "\u2014" : isLive ? state?.minuteLabel ?? "\u2014" : kickoffLabel(match.kickoffUnix);
3742
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: isLive ? "ss-w-drow ss-w-drow--live" : "ss-w-drow", children: [
3743
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: isLive ? "ss-w-drow-when ss-w-drow-when--live" : "ss-w-drow-when", children: when }),
3783
+ const ticked = displayMinute(state, nowMs);
3784
+ const when = state?.isHalfTime && !stale ? ui.halfTime : isLive ? ticked ?? "\u2014" : state?.phase === "live" ? "\u2014" : kickoffLabel(match.kickoffUnix);
3785
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: isLive || state?.isHalfTime ? "ss-w-drow ss-w-drow--live" : "ss-w-drow", children: [
3786
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: isLive || state?.isHalfTime ? "ss-w-drow-when ss-w-drow-when--live" : "ss-w-drow-when", children: when }),
3744
3787
  /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("span", { className: "ss-w-drow-fx", children: [
3745
3788
  /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Side, { name: match.homeTeamName, crest: match.homeTeamImagePath }),
3746
3789
  /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "ss-w-drow-dash", children: "\u2013" }),
@@ -3753,25 +3796,36 @@ function FixtureRow({ match, state, stale, ui, monitor }) {
3753
3796
 
3754
3797
  // src/components/CompetitionGroup.tsx
3755
3798
  var import_jsx_runtime50 = require("react/jsx-runtime");
3756
- function CompetitionGroup({ group, open, onToggle, states, stale, ui, monitor }) {
3799
+ function CompetitionGroup({ group, open, onToggle, states, stale, nowMs, ui, monitor, disambiguate = false }) {
3757
3800
  const own = group.matches.filter((m) => states[m.matchId]?.phase !== "live" || stale);
3758
3801
  const hasLive = own.length !== group.matches.length;
3759
3802
  return /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: "ss-w-grp", children: [
3760
3803
  /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("button", { type: "button", className: "ss-w-grp-head", "aria-expanded": open, onClick: onToggle, children: [
3761
3804
  /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { className: "ss-w-grp-chev", children: open ? "\u25BE" : "\u25B8" }),
3762
3805
  hasLive && /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { className: "ss-w-sec-dot", "aria-hidden": "true" }),
3763
- /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { className: "ss-w-grp-name", children: group.competitionName }),
3806
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("span", { className: "ss-w-grp-name", children: [
3807
+ group.competitionName,
3808
+ disambiguate && group.competitionCountry ? /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("span", { className: "ss-w-grp-country", children: [
3809
+ " (",
3810
+ group.competitionCountry,
3811
+ ")"
3812
+ ] }) : null
3813
+ ] }),
3764
3814
  /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { className: "ss-w-grp-count", children: own.length })
3765
3815
  ] }),
3766
- open && /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("div", { className: "ss-w-grp-body", children: own.map((m) => /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(FixtureRow, { match: m, state: states[m.matchId], stale, ui, monitor }, m.matchId)) })
3816
+ open && /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("div", { className: "ss-w-grp-body", children: own.map((m) => /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(FixtureRow, { match: m, state: states[m.matchId], stale, nowMs, ui, monitor }, m.matchId)) })
3767
3817
  ] });
3768
3818
  }
3769
3819
 
3770
3820
  // src/components/TodayPanel.tsx
3771
3821
  var import_jsx_runtime51 = require("react/jsx-runtime");
3772
3822
  function TodayPanel({ discovery, ui, monitor }) {
3773
- const { groups, states, stale, expanded, toggleGroup } = discovery;
3823
+ const { groups, states, nowMs, stale, expanded, toggleGroup } = discovery;
3774
3824
  const liveRows = stale ? [] : groups.flatMap((group) => group.matches.filter((m) => states[m.matchId]?.phase === "live").map((match) => ({ match, rank: group.rank }))).sort((a, b) => a.rank - b.rank);
3825
+ const nameCounts = groups.reduce((acc, g) => {
3826
+ acc[g.competitionName] = (acc[g.competitionName] ?? 0) + 1;
3827
+ return acc;
3828
+ }, {});
3775
3829
  return /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)("div", { className: "ss-w-mon-list", children: [
3776
3830
  stale && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("div", { className: "ss-w-stale", children: ui.staleStates }),
3777
3831
  liveRows.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)(import_jsx_runtime51.Fragment, { children: [
@@ -3785,6 +3839,7 @@ function TodayPanel({ discovery, ui, monitor }) {
3785
3839
  match,
3786
3840
  state: states[match.matchId],
3787
3841
  stale,
3842
+ nowMs,
3788
3843
  ui,
3789
3844
  monitor
3790
3845
  },
@@ -3797,8 +3852,10 @@ function TodayPanel({ discovery, ui, monitor }) {
3797
3852
  group,
3798
3853
  open: expanded[group.competitionId] ?? group.expandedByDefault,
3799
3854
  onToggle: () => toggleGroup(group.competitionId),
3855
+ disambiguate: (nameCounts[group.competitionName] ?? 0) > 1,
3800
3856
  states,
3801
3857
  stale,
3858
+ nowMs,
3802
3859
  ui,
3803
3860
  monitor
3804
3861
  },
@@ -3983,6 +4040,11 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
3983
4040
  /* .ss-w-mon-list (Matches/Odds tab scrollers) shares .ss-w-list's box/scroll rules by design --
3984
4041
  same visual rhythm as the chat message list -- but gets its own class so it isn't the same DOM
3985
4042
  landmark as the chat list when both exist in the panel at once (Task 16 fix round, M4). */
4043
+ /* Children of this scrolling column flex container must NOT shrink. The default flex-shrink:1
4044
+ compresses every child to fit the panel instead of letting the container scroll, and
4045
+ .ss-w-grp's own overflow:hidden then clips the compressed content away entirely \u2014 27
4046
+ competition groups rendered as blank bars on a phone. Applies to the pinned cards too. */
4047
+ .ss-w-mon-list > * { flex: none; }
3986
4048
  .ss-w-list, .ss-w-mon-list { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 14px; display: flex; flex-direction: column; gap: 12px; scroll-padding-top: 14px; }
3987
4049
  /* Transient bottom spacer: reserves a viewport of room below a new turn so its
3988
4050
  top can be scrolled into view while the reply is still streaming in. Height is
@@ -4714,6 +4776,7 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
4714
4776
  .ss-w-grp-head:focus-visible { outline: 2px solid var(--ss-w-accent); outline-offset: -2px; }
4715
4777
  .ss-w-grp-chev { color: var(--ss-w-faint); font-size: 10px; flex: none; }
4716
4778
  .ss-w-grp-name { font-size: 12px; font-weight: 700; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
4779
+ .ss-w-grp-country { font-weight: 600; color: var(--ss-w-faint); }
4717
4780
  .ss-w-grp-count { margin-left: auto; flex: none; font-size: 10.5px; font-weight: 700; color: var(--ss-w-faint); background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); border-radius: 999px; padding: 1px 7px; font-variant-numeric: tabular-nums; }
4718
4781
  .ss-w-grp-body { display: flex; flex-direction: column; padding: 2px 11px 6px; }
4719
4782