fix: open the calendar at the current time

The window ran from the earliest event start, which is months back, so today
sat far off-screen and every visit began with a scroll to find yourself.

It now opens at now. Events already running are clipped at the left edge and
faded there, the same way an unannounced end frays on the right — both say
the truth extends past what is drawn. The left edge also carries a month
label, since the window opens mid-month and the first real boundary can be
weeks away.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-15 01:00:56 +02:00
co-authored by Claude Opus 5
parent 395d8b8ca8
commit 65f1660718
+48 -16
View File
@@ -6,6 +6,12 @@ import { URGENCY_COLOR } from "./Meter.tsx";
const DAY_WIDTH = 13; // px per day — dense enough to see a patch cycle at once const DAY_WIDTH = 13; // px per day — dense enough to see a patch cycle at once
/**
* A sliver of time before now, so the "now" rule reads as a line in the view
* rather than merging with the border.
*/
const HALF_DAY_LEAD = 12 * 60 * 60 * 1000;
/** /**
* 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.
* *
@@ -32,9 +38,12 @@ export function Timeline({
); );
} }
const starts = rows.map((r) => r.clock.startsMs);
const ends = rows.map((r) => r.clock.endsMs ?? r.clock.startsMs + 14 * DAY); const ends = rows.map((r) => r.clock.endsMs ?? r.clock.startsMs + 14 * DAY);
const min = Math.min(...starts, now) - 2 * 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 max = Math.max(...ends, now) + 2 * DAY;
const totalDays = Math.ceil((max - min) / DAY); const totalDays = Math.ceil((max - min) / DAY);
const width = totalDays * DAY_WIDTH; const width = totalDays * DAY_WIDTH;
@@ -81,8 +90,11 @@ export function Timeline({
</p> </p>
<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 left = x(clock.startsMs);
const unknownEnd = clock.endsMs === null; const unknownEnd = clock.endsMs === null;
// Already running when the view opens: clip to the edge and
// say so, the same way an unknown end frays on the right.
const clippedStart = clock.startsMs < min;
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);
const done = completions[event.id] !== undefined; const done = completions[event.id] !== undefined;
return ( return (
@@ -98,12 +110,14 @@ export function Timeline({
marginLeft: left, marginLeft: left,
width: Math.max(right - left, 22), width: Math.max(right - left, 22),
background: `color-mix(in srgb, ${game.hue} 22%, var(--color-surface))`, background: `color-mix(in srgb, ${game.hue} 22%, var(--color-surface))`,
borderLeft: `2px solid ${game.hue}`, // No start edge to draw when the bar begins before
// A frayed right edge says the end is unannounced — // the view does.
// visually distinct from an event ending far away. borderLeft: clippedStart
maskImage: unknownEnd ? undefined
? "linear-gradient(90deg, #000 60%, transparent 100%)" : `2px solid ${game.hue}`,
: undefined, // Frayed right = end unannounced; faded left = started
// before now. Both are honest about what is not shown.
maskImage: edgeMask(clippedStart, unknownEnd),
color: "var(--color-ink)", color: "var(--color-ink)",
}} }}
> >
@@ -126,18 +140,36 @@ export function Timeline({
); );
} }
/**
* Fade a bar's edge where the truth extends past what is drawn: the left when
* the event began before the view opens, the right when its end is unannounced.
*/
function edgeMask(clippedStart: boolean, unknownEnd: boolean): string | undefined {
if (clippedStart && unknownEnd) {
return "linear-gradient(90deg, transparent 0%, #000 14%, #000 60%, transparent 100%)";
}
if (clippedStart) return "linear-gradient(90deg, transparent 0%, #000 14%)";
if (unknownEnd) return "linear-gradient(90deg, #000 60%, transparent 100%)";
return undefined;
}
function monthBoundaries(min: number, max: number) { function monthBoundaries(min: number, max: number) {
const out: Array<{ ms: number; label: string }> = []; const short = (ms: number) =>
new Date(ms).toLocaleDateString(undefined, { month: "short" });
// The window opens mid-month, so the first real boundary can be weeks away.
// Label the left edge with the current month or the opening stretch has no
// date context at all.
const out: Array<{ ms: number; label: string }> = [
{ ms: min, label: short(min) },
];
const d = new Date(min); const d = new Date(min);
d.setUTCDate(1); d.setUTCDate(1);
d.setUTCHours(0, 0, 0, 0); d.setUTCHours(0, 0, 0, 0);
d.setUTCMonth(d.getUTCMonth() + 1);
while (d.getTime() <= max) { while (d.getTime() <= max) {
if (d.getTime() >= min) { out.push({ ms: d.getTime(), label: short(d.getTime()) });
out.push({
ms: d.getTime(),
label: d.toLocaleDateString(undefined, { month: "short" }),
});
}
d.setUTCMonth(d.getUTCMonth() + 1); d.setUTCMonth(d.getUTCMonth() + 1);
} }
return out; return out;