+
+ {/* Always on, including in the compact layout where the rest of the toolbar folds away:
+ without it, a window that crosses midnight has nothing naming the day. */}
+ {windowDayLabel}
+
+
+ {formatRange(timelineWindow.startMs, timelineWindow.endMs)}
+
+
+
+
+
+
+ {manualAnchorMs !== null && (
+
+ )}
+
+
+ {/* The scale is drawn by the ticks; the labels come from the hour ticks and the programme
+ boundaries, and are positioned from their measured width. */}
+
+ {ticks.map((tick) => (
+
+ ))}
+ {axisLabels.map((label) => (
+
+ {formatAxisLabel(label)}
+
+ ))}
+
+
+
+
+
+ {blocks.map((block) => {
+ const title = block.program.title || t("excellentProgram");
+ return (
+
+ );
+ })}
+
+
+
+
+
+ {/* The scrub preview and the prompts sit outside the track: the track clips its blocks,
+ and both must be able to float above the band. */}
+
+
+
+
+
+
+
+
+
+ {action?.kind === "confirm-catchup" && (
+
+
+
+
{t("epgCatchupConfirmTitle")}
+
+ {action.program.title || t("excellentProgram")} ·{" "}
+ {formatRange(action.program.start.getTime(), action.program.end.getTime())}
+
+
{t("epgCatchupConfirmBody")}
+
+
+
+
+
+
+ )}
+
+ {/* role="status" is already a polite live region, and the notice retires on its own. */}
+ {action?.kind === "notice" && (
+
+ )}
+
+
+ {hoveredProgram?.title || t("excellentProgram")}
+ {hoveredProgram && (
+
+ {formatRange(hoveredProgram.start.getTime(), hoveredProgram.end.getTime())}
+
+ )}
+
+
+ );
+}
+
+export const PlayerEpgTimeline = memo(PlayerEpgTimelineComponent);
diff --git a/web-ui/src/components/player/video-player.tsx b/web-ui/src/components/player/video-player.tsx
index 84adcf35..00139bf9 100644
--- a/web-ui/src/components/player/video-player.tsx
+++ b/web-ui/src/components/player/video-player.tsx
@@ -64,6 +64,8 @@ interface VideoPlayerProps {
onError?: (error: string) => void;
locale: Locale;
currentProgram?: EPGProgram | null;
+ /** Every programme known for the current channel, for the EPG timeline band. */
+ epgPrograms?: readonly EPGProgram[];
onSeek?: (seekTime: Date, goingLive: boolean) => void;
/** Channel EPG programmes used to split catchup playseek windows and to rebuild URLs on retry. */
catchupPrograms?: readonly Pick