totalCanvasWidth is computed from viewStart/viewDays before data loads,
so the previous trigger fired during the loading spinner. scrollLeft
was clipped to 0 (no canvas in DOM yet) and the guard was set, blocking
the real scroll after data arrived. Using isInitialLoading as the dep
fires the effect exactly when the canvas enters the DOM.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
useLayoutEffect([]) fired before isInitialLoading resolved, so the
scroll container had no canvas yet — scrollLeft was clipped to 0.
Now the scroll-to-today fires on the first render where totalCanvasWidth
becomes non-zero. The cleanup effect resets the guard on unmount so
React Strict Mode's fake-unmount+remount also scrolls correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The guard-ref approach broke in React Strict Mode (dev): the ref
persisted as `true` across the simulated remount, so the second
invocation skipped the scroll — leaving scrollLeft=0 (today-90
at the left edge, not today). An empty-deps useLayoutEffect runs
twice in Strict Mode but both executions fire against the same
initial `toLeft` and produce the correct result.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
The test takes >5s on the QNAP act runner because dynamic import of
next/server has to transpile the module cold on first call. Raise the
per-test timeout to 15s to give it headroom without changing the test logic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously viewStart defaulted to today-30 and the scroll container had
no left-edge expansion logic, so users hit a hard wall when scrolling
left. This change:
- Sets viewStart default to today so the viewport opens with today at
the left edge (URL ?startDate= override still respected).
- Adds left-edge auto-expansion in handleContainerScroll: when the user
scrolls within 40 cells of the left boundary, 120 days are prepended
and a useLayoutEffect applies the matching scrollLeft compensation in
the same paint frame to prevent a visual jump.
- Floors backward navigation at 5 years (minDate) to prevent unbounded
viewDays growth.
- Updates handleNavigateToday to match: resets to today rather than
today-30.
Both resource view and project view use the same TimelineContext /
TimelineView, so both are fixed by this change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>