fix: let the calendar scroll back
Clamping the window to now made the past unreachable — you could not see when a running event began. Rendering from the earliest start instead buried today off-screen, which is what the clamp was fixing. The window now covers both and the container opens scrolled to today, so the default view is unchanged and history is a scroll away. It reaches a week past the oldest running event, so the range stays bounded by what is actually live. Nothing is clipped at the left edge any more. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0efcfc52f0
commit
5ea5d8292f
@@ -1,3 +1,4 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
import { gameMeta } from "../../shared/games.ts";
|
import { gameMeta } from "../../shared/games.ts";
|
||||||
import type { GameId } from "../../shared/schema.ts";
|
import type { GameId } from "../../shared/schema.ts";
|
||||||
import { DAY } from "../../shared/time.ts";
|
import { DAY } from "../../shared/time.ts";
|
||||||
@@ -12,6 +13,12 @@ const DAY_WIDTH = 13; // px per day — dense enough to see a patch cycle at onc
|
|||||||
*/
|
*/
|
||||||
const HALF_DAY_LEAD = 12 * 60 * 60 * 1000;
|
const HALF_DAY_LEAD = 12 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How far back the view can be scrolled beyond the oldest running event, so
|
||||||
|
* "when did this start?" is answerable without the window being unbounded.
|
||||||
|
*/
|
||||||
|
const PAST_LEAD = 7 * DAY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One lane per game, bars spanning start→end, today pinned as a rule.
|
* One lane per game, bars spanning start→end, today pinned as a rule.
|
||||||
*
|
*
|
||||||
@@ -30,6 +37,31 @@ export function Timeline({
|
|||||||
onOpen: (id: string) => void;
|
onOpen: (id: string) => void;
|
||||||
completions: Record<string, unknown>;
|
completions: Record<string, unknown>;
|
||||||
}) {
|
}) {
|
||||||
|
const scroller = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const ends = rows.map((r) => r.clock.endsMs ?? r.clock.startsMs + 14 * DAY);
|
||||||
|
const starts = rows.map((r) => r.clock.startsMs);
|
||||||
|
|
||||||
|
// The window covers the past too, so a reader can scroll back to see when a
|
||||||
|
// running event began — but it *opens* scrolled to now, because that is what
|
||||||
|
// they came for. Rendering from the earliest start alone buried today
|
||||||
|
// off-screen; clamping to now made the past unreachable. This does both.
|
||||||
|
const min = Math.min(...starts, now) - PAST_LEAD;
|
||||||
|
const max = Math.max(...ends, now) + 2 * DAY;
|
||||||
|
// Where "now" sits, so the container can be scrolled there on open.
|
||||||
|
const nowOffset = ((now - HALF_DAY_LEAD - min) / DAY) * DAY_WIDTH;
|
||||||
|
const totalDays = Math.ceil((max - min) / DAY);
|
||||||
|
const width = totalDays * DAY_WIDTH;
|
||||||
|
const x = (ms: number) => ((ms - min) / DAY) * DAY_WIDTH;
|
||||||
|
|
||||||
|
// Open at today rather than at the far past. Keyed on the rounded offset so
|
||||||
|
// it runs when the range changes, not every second — re-scrolling on each
|
||||||
|
// tick would fight the reader's own scrolling.
|
||||||
|
const openAt = Math.round(nowOffset);
|
||||||
|
useEffect(() => {
|
||||||
|
scroller.current?.scrollTo({ left: openAt, behavior: "instant" });
|
||||||
|
}, [openAt]);
|
||||||
|
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return (
|
return (
|
||||||
<p className="px-4 py-10 text-sm text-muted">
|
<p className="px-4 py-10 text-sm text-muted">
|
||||||
@@ -38,17 +70,6 @@ export function Timeline({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ends = rows.map((r) => r.clock.endsMs ?? r.clock.startsMs + 14 * DAY);
|
|
||||||
// The window opens at now. Events that began earlier are clipped at the left
|
|
||||||
// edge rather than pushing the view back weeks — what already happened is not
|
|
||||||
// what the reader is here for, and starting at the earliest start buries
|
|
||||||
// today off-screen.
|
|
||||||
const min = now - HALF_DAY_LEAD;
|
|
||||||
const max = Math.max(...ends, now) + 2 * DAY;
|
|
||||||
const totalDays = Math.ceil((max - min) / DAY);
|
|
||||||
const width = totalDays * DAY_WIDTH;
|
|
||||||
const x = (ms: number) => ((ms - min) / DAY) * DAY_WIDTH;
|
|
||||||
|
|
||||||
const byGame = new Map<GameId, RowEvent[]>();
|
const byGame = new Map<GameId, RowEvent[]>();
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
byGame.set(row.event.game, [...(byGame.get(row.event.game) ?? []), row]);
|
byGame.set(row.event.game, [...(byGame.get(row.event.game) ?? []), row]);
|
||||||
@@ -57,7 +78,7 @@ export function Timeline({
|
|||||||
const monthTicks = monthBoundaries(min, max);
|
const monthTicks = monthBoundaries(min, max);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="scroll-x">
|
<div ref={scroller} className="scroll-x">
|
||||||
<div style={{ width, minWidth: "100%" }} className="relative px-4 pb-8 pt-3">
|
<div style={{ width, minWidth: "100%" }} className="relative px-4 pb-8 pt-3">
|
||||||
{/* Month rule, so a bar's absolute position means something. */}
|
{/* Month rule, so a bar's absolute position means something. */}
|
||||||
<div className="relative mb-3 h-4 border-b border-hairline">
|
<div className="relative mb-3 h-4 border-b border-hairline">
|
||||||
@@ -91,8 +112,10 @@ export function Timeline({
|
|||||||
<div className="relative h-auto space-y-1">
|
<div className="relative h-auto space-y-1">
|
||||||
{events.map(({ event, clock }) => {
|
{events.map(({ event, clock }) => {
|
||||||
const unknownEnd = clock.endsMs === null;
|
const unknownEnd = clock.endsMs === null;
|
||||||
// Already running when the view opens: clip to the edge and
|
// Only clipped if it began before the rendered window,
|
||||||
// say so, the same way an unknown end frays on the right.
|
// which now reaches a week past the oldest running event —
|
||||||
|
// so in practice bars show their real start and the fade is
|
||||||
|
// reserved for genuinely truncated ones.
|
||||||
const clippedStart = clock.startsMs < min;
|
const clippedStart = clock.startsMs < min;
|
||||||
const left = Math.max(x(clock.startsMs), 0);
|
const left = Math.max(x(clock.startsMs), 0);
|
||||||
const right = x(clock.endsMs ?? clock.startsMs + 14 * DAY);
|
const right = x(clock.endsMs ?? clock.startsMs + 14 * DAY);
|
||||||
|
|||||||
Reference in New Issue
Block a user