@sensiblestats/widget-react 0.47.2 → 0.48.1

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,17 @@
1
1
  # @sensiblestats/widget-react
2
2
 
3
+ ## 0.48.1
4
+
5
+ ### Patch Changes
6
+
7
+ - A live match no longer loses its minute and drop out of the live section just because its score has not changed recently. The per-match staleness check read a last-changed timestamp as if it were a last-polled one.
8
+
9
+ ## 0.48.0
10
+
11
+ ### Minor Changes
12
+
13
+ - Today-tab fixture rows now read like a scoreboard: the score sits between the two teams instead of after them, a finished match shows FT (MS in Turkish) in place of its kickoff time, and a finished match no longer offers a pin unless it is already pinned.
14
+
3
15
  ## 0.47.2
4
16
 
5
17
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -129,6 +129,7 @@ var STRINGS = {
129
129
  tabToday: "Today",
130
130
  sectionLive: "Live",
131
131
  halfTime: "HT",
132
+ fullTime: "FT",
132
133
  staleStates: "Live scores can't be updated \u2014 showing the last known state."
133
134
  },
134
135
  tr: {
@@ -207,6 +208,7 @@ var STRINGS = {
207
208
  tabToday: "Bug\xFCn",
208
209
  sectionLive: "Canl\u0131",
209
210
  halfTime: "\u0130Y",
211
+ fullTime: "MS",
210
212
  staleStates: "Canl\u0131 skorlar g\xFCncellenemiyor \u2014 son bilinen durum g\xF6steriliyor."
211
213
  }
212
214
  };
@@ -286,6 +288,7 @@ function normalizeUi(cfg) {
286
288
  tabToday: strings.tabToday,
287
289
  sectionLive: strings.sectionLive,
288
290
  halfTime: strings.halfTime,
291
+ fullTime: strings.fullTime,
289
292
  staleStates: strings.staleStates,
290
293
  pinLabel: strings.pinLabel,
291
294
  unpinLabel: strings.unpinLabel,
@@ -1545,43 +1548,8 @@ function dueMatchIds(groups, states, nowMs) {
1545
1548
  return [...due];
1546
1549
  }
1547
1550
 
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
- function isTreatedLive(state, pollStale, nowMs) {
1579
- return state?.phase === "live" && !pollStale && !isReadingStale(state, nowMs);
1580
- }
1581
-
1582
1551
  // src/useMatchDiscovery.ts
1583
1552
  var POLL_MS = 6e4;
1584
- var CLOCK_MS = 1e4;
1585
1553
  var ROLLOVER_CHECK_MS = 6e4;
1586
1554
  var STALE_AFTER_FAILURES = 2;
1587
1555
  function localDateOf(now) {
@@ -1594,7 +1562,6 @@ function useMatchDiscovery(opts) {
1594
1562
  const [groups, setGroups] = (0, import_react4.useState)([]);
1595
1563
  const [states, setStates] = (0, import_react4.useState)({});
1596
1564
  const [failures, setFailures] = (0, import_react4.useState)(0);
1597
- const [nowMs, setNowMs] = (0, import_react4.useState)(() => Date.now());
1598
1565
  const [expanded, setExpanded] = (0, import_react4.useState)({});
1599
1566
  const [touched, setTouched] = (0, import_react4.useState)({});
1600
1567
  const groupsRef = (0, import_react4.useRef)(groups);
@@ -1672,21 +1639,16 @@ function useMatchDiscovery(opts) {
1672
1639
  }, POLL_MS);
1673
1640
  return () => clearInterval(id);
1674
1641
  }, [opts.isOpen, tick, groups]);
1675
- (0, import_react4.useEffect)(() => {
1676
- if (!opts.isOpen) return;
1677
- const id = setInterval(() => setNowMs(Date.now()), CLOCK_MS);
1678
- return () => clearInterval(id);
1679
- }, [opts.isOpen]);
1680
1642
  const stale = failures >= STALE_AFTER_FAILURES;
1681
1643
  const liveCount = (0, import_react4.useMemo)(
1682
- () => stale ? 0 : Object.values(states).filter((s) => s.phase === "live" && !isReadingStale(s, nowMs)).length,
1683
- [states, stale, nowMs]
1644
+ () => stale ? 0 : Object.values(states).filter((s) => s.phase === "live").length,
1645
+ [states, stale]
1684
1646
  );
1685
1647
  const toggleGroup = (0, import_react4.useCallback)((competitionId) => {
1686
1648
  setTouched((prev) => ({ ...prev, [competitionId]: true }));
1687
1649
  setExpanded((prev) => ({ ...prev, [competitionId]: !prev[competitionId] }));
1688
1650
  }, []);
1689
- return { groups, states, nowMs, liveCount, stale, expanded, toggleGroup, _tick: tick };
1651
+ return { groups, states, liveCount, stale, expanded, toggleGroup, _tick: tick };
1690
1652
  }
1691
1653
 
1692
1654
  // src/eventLogContext.ts
@@ -3044,7 +3006,7 @@ function computeDisplayMinute(minute, clockRunning, receivedAtMs) {
3044
3006
  return minute + Math.min(Math.max(0, elapsedMinutes), MAX_LIVE_TICK_MINUTES);
3045
3007
  }
3046
3008
  function useLiveClock(minute, clockRunning, receivedAtMs) {
3047
- const [displayMinute2, setDisplayMinute] = (0, import_react19.useState)(
3009
+ const [displayMinute, setDisplayMinute] = (0, import_react19.useState)(
3048
3010
  () => computeDisplayMinute(minute, clockRunning, receivedAtMs)
3049
3011
  );
3050
3012
  (0, import_react19.useEffect)(() => {
@@ -3055,7 +3017,7 @@ function useLiveClock(minute, clockRunning, receivedAtMs) {
3055
3017
  }, TICK_MS);
3056
3018
  return () => window.clearInterval(id);
3057
3019
  }, [minute, clockRunning, receivedAtMs]);
3058
- return displayMinute2;
3020
+ return displayMinute;
3059
3021
  }
3060
3022
 
3061
3023
  // src/components/dashboard/LiveScoreCard.tsx
@@ -3087,7 +3049,7 @@ function LiveScoreCard({
3087
3049
  if (!seedRef.current || seedRef.current.key !== seedKey) {
3088
3050
  seedRef.current = { key: seedKey, receivedAtMs: Date.now() };
3089
3051
  }
3090
- const displayMinute2 = useLiveClock(data.minute, data.clockRunning, seedRef.current.receivedAtMs);
3052
+ const displayMinute = useLiveClock(data.minute, data.clockRunning, seedRef.current.receivedAtMs);
3091
3053
  const minuteSuffix = data.minuteLabel.replace(/^\d+/, "");
3092
3054
  const scorers = scorersLine(goalEvents);
3093
3055
  return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "ss-w-fandash-card ss-w-fandash-live-score", children: [
@@ -3097,7 +3059,7 @@ function LiveScoreCard({
3097
3059
  /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "ss-w-fandash-live-dot", "aria-hidden": "true" }),
3098
3060
  data.statusLabel
3099
3061
  ] }),
3100
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "ss-w-fandash-live-minute", children: `${displayMinute2}${minuteSuffix}` }),
3062
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "ss-w-fandash-live-minute", children: `${displayMinute}${minuteSuffix}` }),
3101
3063
  data.competition ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "ss-w-fandash-live-competition", children: data.competition }) : null
3102
3064
  ] }),
3103
3065
  /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "ss-w-fandash-live-main", children: [
@@ -3779,28 +3741,34 @@ function Side({ name, crest }) {
3779
3741
  /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "ss-w-drow-nm", children: name })
3780
3742
  ] });
3781
3743
  }
3782
- function FixtureRow({ match, state, stale, nowMs, ui, monitor }) {
3783
- const readingStale = isReadingStale(state, nowMs);
3784
- const isLive = state?.phase === "live" && !stale && !readingStale;
3744
+ function FixtureRow({ match, state, stale, ui, monitor }) {
3745
+ const isLive = state?.phase === "live" && !stale;
3785
3746
  const hasScore = (state?.phase === "live" || state?.phase === "fullTime") && typeof state.homeScore === "number" && typeof state.awayScore === "number";
3786
- const ticked = displayMinute(state, nowMs);
3787
- const when = state?.isHalfTime && !stale ? ui.halfTime : isLive ? ticked ?? "\u2014" : state?.phase === "live" ? "\u2014" : kickoffLabel(match.kickoffUnix);
3747
+ const ticked = state?.phase === "live" ? state.minuteLabel ?? null : null;
3748
+ const isFinished = state?.phase === "fullTime";
3749
+ const when = state?.isHalfTime && !stale ? ui.halfTime : isLive ? ticked ?? "\u2014" : state?.phase === "live" ? "\u2014" : isFinished ? ui.fullTime : kickoffLabel(match.kickoffUnix);
3750
+ const pin = { kind: "match", matchId: match.matchId };
3751
+ const showPin = !isFinished || monitor.isPinned(pinKey(pin));
3788
3752
  return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: isLive || state?.isHalfTime ? "ss-w-drow ss-w-drow--live" : "ss-w-drow", children: [
3789
3753
  /* @__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 }),
3790
3754
  /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("span", { className: "ss-w-drow-fx", children: [
3791
3755
  /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Side, { name: match.homeTeamName, crest: match.homeTeamImagePath }),
3792
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "ss-w-drow-dash", children: "\u2013" }),
3756
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: hasScore ? "ss-w-drow-sc" : "ss-w-drow-sc ss-w-drow-sc--idle", children: hasScore ? `${state.homeScore}\u2013${state.awayScore}` : "\u2013" }),
3793
3757
  /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Side, { name: match.awayTeamName, crest: match.awayTeamImagePath })
3794
3758
  ] }),
3795
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: hasScore ? "ss-w-drow-sc" : "ss-w-drow-sc ss-w-drow-sc--idle", children: hasScore ? `${state.homeScore}\u2013${state.awayScore}` : "\u2013" }),
3796
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(PinToggle, { ui, monitor, pin: { kind: "match", matchId: match.matchId } })
3759
+ showPin ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(PinToggle, { ui, monitor, pin }) : null
3797
3760
  ] });
3798
3761
  }
3799
3762
 
3763
+ // src/liveClock.ts
3764
+ function isTreatedLive(state, pollStale) {
3765
+ return state?.phase === "live" && !pollStale;
3766
+ }
3767
+
3800
3768
  // src/components/CompetitionGroup.tsx
3801
3769
  var import_jsx_runtime50 = require("react/jsx-runtime");
3802
- function CompetitionGroup({ group, open, onToggle, states, stale, nowMs, ui, monitor, disambiguate = false }) {
3803
- const own = group.matches.filter((m) => !isTreatedLive(states[m.matchId], stale, nowMs));
3770
+ function CompetitionGroup({ group, open, onToggle, states, stale, ui, monitor, disambiguate = false }) {
3771
+ const own = group.matches.filter((m) => !isTreatedLive(states[m.matchId], stale));
3804
3772
  const hasLive = own.length !== group.matches.length;
3805
3773
  if (own.length === 0) {
3806
3774
  return null;
@@ -3819,15 +3787,15 @@ function CompetitionGroup({ group, open, onToggle, states, stale, nowMs, ui, mon
3819
3787
  ] }),
3820
3788
  /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { className: "ss-w-grp-count", children: own.length })
3821
3789
  ] }),
3822
- 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)) })
3790
+ 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)) })
3823
3791
  ] });
3824
3792
  }
3825
3793
 
3826
3794
  // src/components/TodayPanel.tsx
3827
3795
  var import_jsx_runtime51 = require("react/jsx-runtime");
3828
3796
  function TodayPanel({ discovery, ui, monitor }) {
3829
- const { groups, states, nowMs, stale, expanded, toggleGroup } = discovery;
3830
- const liveRows = stale ? [] : groups.flatMap((group) => group.matches.filter((m) => isTreatedLive(states[m.matchId], stale, nowMs)).map((match) => ({ match, rank: group.rank }))).sort((a, b) => a.rank - b.rank);
3797
+ const { groups, states, stale, expanded, toggleGroup } = discovery;
3798
+ const liveRows = stale ? [] : groups.flatMap((group) => group.matches.filter((m) => isTreatedLive(states[m.matchId], stale)).map((match) => ({ match, rank: group.rank }))).sort((a, b) => a.rank - b.rank);
3831
3799
  const nameCounts = groups.reduce((acc, g) => {
3832
3800
  acc[g.competitionName] = (acc[g.competitionName] ?? 0) + 1;
3833
3801
  return acc;
@@ -3845,7 +3813,6 @@ function TodayPanel({ discovery, ui, monitor }) {
3845
3813
  match,
3846
3814
  state: states[match.matchId],
3847
3815
  stale,
3848
- nowMs,
3849
3816
  ui,
3850
3817
  monitor
3851
3818
  },
@@ -3861,7 +3828,6 @@ function TodayPanel({ discovery, ui, monitor }) {
3861
3828
  disambiguate: (nameCounts[group.competitionName] ?? 0) > 1,
3862
3829
  states,
3863
3830
  stale,
3864
- nowMs,
3865
3831
  ui,
3866
3832
  monitor
3867
3833
  },
@@ -4786,7 +4752,7 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
4786
4752
  .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; }
4787
4753
  .ss-w-grp-body { display: flex; flex-direction: column; padding: 2px 11px 6px; }
4788
4754
 
4789
- .ss-w-drow { display: grid; grid-template-columns: 42px minmax(0,1fr) auto 22px; align-items: center; gap: 9px; padding: 7px 0; border-bottom: 1px solid var(--ss-w-line); }
4755
+ .ss-w-drow { display: grid; grid-template-columns: 42px minmax(0,1fr) 22px; align-items: center; gap: 9px; padding: 7px 0; border-bottom: 1px solid var(--ss-w-line); }
4790
4756
  .ss-w-drow:last-child { border-bottom: none; }
4791
4757
  .ss-w-drow-when { font-size: 11px; font-weight: 700; color: var(--ss-w-faint); font-variant-numeric: tabular-nums; }
4792
4758
  .ss-w-drow-when--live { color: var(--ss-w-neg); }
@@ -4795,8 +4761,11 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
4795
4761
  .ss-w-drow-fx { display: flex; align-items: center; gap: 6px; min-width: 0; }
4796
4762
  .ss-w-drow-side { display: flex; align-items: center; gap: 5px; min-width: 0; flex: 1 1 0; }
4797
4763
  .ss-w-drow-nm { font-size: 12.5px; font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
4798
- .ss-w-drow-dash { flex: none; color: var(--ss-w-faint); font-size: 11px; }
4799
- .ss-w-drow-sc { font-size: 12.5px; font-weight: 800; font-variant-numeric: tabular-nums; color: var(--ss-w-ink); }
4764
+ /* Centre slot of .ss-w-drow-fx, between the two sides: flex:none so a widening score never
4765
+ steals width the team names need to avoid ellipsis. min-width holds the slot at the width of a
4766
+ one-digit scoreline so an unscored row's bare dash does not pull the column out of line -- the
4767
+ scores read as a column down the list, not a jitter. */
4768
+ .ss-w-drow-sc { flex: none; min-width: 26px; text-align: center; font-size: 12.5px; font-weight: 800; font-variant-numeric: tabular-nums; color: var(--ss-w-ink); }
4800
4769
  .ss-w-drow-sc--idle { color: var(--ss-w-faint); font-weight: 600; }
4801
4770
  .ss-w-drow--live { background: color-mix(in srgb, var(--ss-w-neg) 5%, transparent); border-radius: 8px; padding: 7px 6px; margin: 0 -6px; border-bottom: none; }
4802
4771
  .ss-w-drow-crest { width: 16px; height: 16px; flex: none; border-radius: 50%; object-fit: contain; background: var(--ss-w-bubble); }