From 65f1660718a49dd5cab8b850e821a7ecdc6723a5 Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Sat, 15 Aug 2026 01:00:56 +0200 Subject: [PATCH] fix: open the calendar at the current time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/client/components/Timeline.tsx | 64 ++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/src/client/components/Timeline.tsx b/src/client/components/Timeline.tsx index a5a8abb..14d2e1a 100644 --- a/src/client/components/Timeline.tsx +++ b/src/client/components/Timeline.tsx @@ -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 +/** + * 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. * @@ -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 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 totalDays = Math.ceil((max - min) / DAY); const width = totalDays * DAY_WIDTH; @@ -81,8 +90,11 @@ export function Timeline({

{events.map(({ event, clock }) => { - const left = x(clock.startsMs); 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 done = completions[event.id] !== undefined; return ( @@ -98,12 +110,14 @@ export function Timeline({ marginLeft: left, width: Math.max(right - left, 22), background: `color-mix(in srgb, ${game.hue} 22%, var(--color-surface))`, - borderLeft: `2px solid ${game.hue}`, - // A frayed right edge says the end is unannounced — - // visually distinct from an event ending far away. - maskImage: unknownEnd - ? "linear-gradient(90deg, #000 60%, transparent 100%)" - : undefined, + // No start edge to draw when the bar begins before + // the view does. + borderLeft: clippedStart + ? undefined + : `2px solid ${game.hue}`, + // 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)", }} > @@ -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) { - 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); d.setUTCDate(1); d.setUTCHours(0, 0, 0, 0); + d.setUTCMonth(d.getUTCMonth() + 1); while (d.getTime() <= max) { - if (d.getTime() >= min) { - out.push({ - ms: d.getTime(), - label: d.toLocaleDateString(undefined, { month: "short" }), - }); - } + out.push({ ms: d.getTime(), label: short(d.getTime()) }); d.setUTCMonth(d.getUTCMonth() + 1); } return out;