feat(timeline): start at today + infinite scroll into the past #65

Merged
Hartmut merged 7 commits from feature/timeline-past-scroll into main 2026-05-22 11:43:08 +02:00
2 changed files with 40 additions and 9 deletions
Showing only changes of commit 944d36bdb2 - Show all commits
@@ -284,7 +284,7 @@ export function TimelineProvider({
const d = new Date(sp);
if (!isNaN(d.getTime())) return d;
}
return today;
return addDays(today, -90);
});
const [viewDays, setViewDays] = useState(() => {
const sp = searchParams.get("days");
@@ -310,7 +310,7 @@ export function TimelineProvider({
const d = new Date(spStart);
if (!isNaN(d.getTime())) return d;
}
return today;
return addDays(today, -90);
});
const spDays = searchParams.get("days");
@@ -687,15 +687,33 @@ function TimelineViewContent({
// Pixels to add to scrollLeft after a left-extension re-render (prevents jump).
const pendingLeftCompensationPx = useRef(0);
// Flag: scroll viewport to today after the next viewStart-driven re-layout.
const pendingScrollToTodayRef = useRef(false);
// Guard: only auto-scroll to today once on initial mount.
const scrolledToTodayOnMount = useRef(false);
// Apply scroll compensation synchronously after the canvas grows leftward.
// Scroll to today on first mount so the viewport opens with today at the left edge.
useLayoutEffect(() => {
const px = pendingLeftCompensationPx.current;
if (px === 0) return;
if (scrolledToTodayOnMount.current) return;
const el = scrollContainerRef.current;
if (el) el.scrollLeft += px;
pendingLeftCompensationPx.current = 0;
}, [viewStart]);
if (!el) return;
el.scrollLeft = toLeft(today);
scrolledToTodayOnMount.current = true;
}, [toLeft, today]);
// Apply scroll compensation synchronously after the canvas grows (left-extend or Today button).
useLayoutEffect(() => {
const el = scrollContainerRef.current;
if (!el) return;
const px = pendingLeftCompensationPx.current;
if (px !== 0) {
el.scrollLeft += px;
pendingLeftCompensationPx.current = 0;
} else if (pendingScrollToTodayRef.current) {
el.scrollLeft = toLeft(today);
pendingScrollToTodayRef.current = false;
}
}, [viewStart, toLeft, today]);
// 5-year floor — no practical data exists further back; prevents runaway growth.
const minDate = useMemo(() => addDays(today, -(365 * 5)), [today]);
@@ -709,7 +727,20 @@ function TimelineViewContent({
}),
[setViewStart, minDate],
);
const handleNavigateToday = useCallback(() => setViewStart(today), [setViewStart, today]);
const handleNavigateToday = useCallback(() => {
const el = scrollContainerRef.current;
const todayMs = new Date(today).setHours(0, 0, 0, 0);
const vsMs = new Date(viewStart).setHours(0, 0, 0, 0);
const veMs = new Date(addDays(viewStart, viewDays)).setHours(0, 0, 0, 0);
if (todayMs >= vsMs && todayMs < veMs && el) {
// Today is in range — just scroll without touching state.
el.scrollLeft = toLeft(today);
} else {
// Today is out of range — reset the window and schedule a scroll.
pendingScrollToTodayRef.current = true;
setViewStart(addDays(today, -90));
}
}, [today, viewStart, viewDays, toLeft, setViewStart]);
const handleNavigateForward = useCallback(
() => setViewStart((v) => addDays(v, 28)),
[setViewStart],