@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 +26 -0
- package/dist/index.cjs +79 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +79 -16
- package/dist/index.js.map +1 -1
- package/dist/styles.css +6 -0
- package/package.json +2 -2
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 [
|
|
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
|
|
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
|
|
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: `${
|
|
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
|
|
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
|
|
3742
|
-
|
|
3743
|
-
|
|
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.
|
|
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
|
|