fix(timeline): pre-load 90-day past buffer + scroll to today on mount
CI / Architecture Guardrails (pull_request) Successful in 5m6s
CI / Typecheck (pull_request) Successful in 7m31s
CI / Assistant Split Regression (pull_request) Successful in 6m45s
CI / Lint (pull_request) Successful in 6m19s
CI / Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Fresh-Linux Docker Deploy (pull_request) Has been cancelled
CI / Release Images (pull_request) Has been cancelled
CI / Build (pull_request) Has been cancelled
CI / Architecture Guardrails (pull_request) Successful in 5m6s
CI / Typecheck (pull_request) Successful in 7m31s
CI / Assistant Split Regression (pull_request) Successful in 6m45s
CI / Lint (pull_request) Successful in 6m19s
CI / Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Fresh-Linux Docker Deploy (pull_request) Has been cancelled
CI / Release Images (pull_request) Has been cancelled
CI / Build (pull_request) Has been cancelled
viewStart=today left no canvas to the left of scrollLeft=0, making left-scroll physically impossible. Now viewStart defaults to today-90 so the canvas always has 90 days to scroll into, and a mount-time useLayoutEffect positions the viewport with today at the left edge. The Today button restores this view: scrolls in-range, or resets viewStart and schedules a post-layout scroll if today has scrolled out of the visible window. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -284,7 +284,7 @@ export function TimelineProvider({
|
|||||||
const d = new Date(sp);
|
const d = new Date(sp);
|
||||||
if (!isNaN(d.getTime())) return d;
|
if (!isNaN(d.getTime())) return d;
|
||||||
}
|
}
|
||||||
return today;
|
return addDays(today, -90);
|
||||||
});
|
});
|
||||||
const [viewDays, setViewDays] = useState(() => {
|
const [viewDays, setViewDays] = useState(() => {
|
||||||
const sp = searchParams.get("days");
|
const sp = searchParams.get("days");
|
||||||
@@ -310,7 +310,7 @@ export function TimelineProvider({
|
|||||||
const d = new Date(spStart);
|
const d = new Date(spStart);
|
||||||
if (!isNaN(d.getTime())) return d;
|
if (!isNaN(d.getTime())) return d;
|
||||||
}
|
}
|
||||||
return today;
|
return addDays(today, -90);
|
||||||
});
|
});
|
||||||
|
|
||||||
const spDays = searchParams.get("days");
|
const spDays = searchParams.get("days");
|
||||||
|
|||||||
@@ -687,15 +687,33 @@ function TimelineViewContent({
|
|||||||
|
|
||||||
// Pixels to add to scrollLeft after a left-extension re-render (prevents jump).
|
// Pixels to add to scrollLeft after a left-extension re-render (prevents jump).
|
||||||
const pendingLeftCompensationPx = useRef(0);
|
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(() => {
|
useLayoutEffect(() => {
|
||||||
const px = pendingLeftCompensationPx.current;
|
if (scrolledToTodayOnMount.current) return;
|
||||||
if (px === 0) return;
|
|
||||||
const el = scrollContainerRef.current;
|
const el = scrollContainerRef.current;
|
||||||
if (el) el.scrollLeft += px;
|
if (!el) return;
|
||||||
pendingLeftCompensationPx.current = 0;
|
el.scrollLeft = toLeft(today);
|
||||||
}, [viewStart]);
|
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.
|
// 5-year floor — no practical data exists further back; prevents runaway growth.
|
||||||
const minDate = useMemo(() => addDays(today, -(365 * 5)), [today]);
|
const minDate = useMemo(() => addDays(today, -(365 * 5)), [today]);
|
||||||
@@ -709,7 +727,20 @@ function TimelineViewContent({
|
|||||||
}),
|
}),
|
||||||
[setViewStart, minDate],
|
[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(
|
const handleNavigateForward = useCallback(
|
||||||
() => setViewStart((v) => addDays(v, 28)),
|
() => setViewStart((v) => addDays(v, 28)),
|
||||||
[setViewStart],
|
[setViewStart],
|
||||||
|
|||||||
Reference in New Issue
Block a user