feat(timeline): start at today and allow infinite scroll into the past
CI / Architecture Guardrails (pull_request) Successful in 17m31s
CI / Assistant Split Regression (pull_request) Successful in 9m42s
CI / Typecheck (pull_request) Successful in 20m48s
CI / Lint (pull_request) Successful in 8m6s
CI / Unit Tests (pull_request) Failing after 7m32s
CI / Build (pull_request) Successful in 9m12s
CI / E2E Tests (pull_request) Successful in 6m12s
CI / Fresh-Linux Docker Deploy (pull_request) Successful in 6m58s
CI / Release Images (pull_request) Has been skipped
CI / Architecture Guardrails (pull_request) Successful in 17m31s
CI / Assistant Split Regression (pull_request) Successful in 9m42s
CI / Typecheck (pull_request) Successful in 20m48s
CI / Lint (pull_request) Successful in 8m6s
CI / Unit Tests (pull_request) Failing after 7m32s
CI / Build (pull_request) Successful in 9m12s
CI / E2E Tests (pull_request) Successful in 6m12s
CI / Fresh-Linux Docker Deploy (pull_request) Successful in 6m58s
CI / Release Images (pull_request) Has been skipped
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>
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 addDays(today, -30);
|
return today;
|
||||||
});
|
});
|
||||||
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 addDays(today, -30);
|
return today;
|
||||||
});
|
});
|
||||||
|
|
||||||
const spDays = searchParams.get("days");
|
const spDays = searchParams.get("days");
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useAllocationHistory } from "~/hooks/useAllocationHistory.js";
|
import { useAllocationHistory } from "~/hooks/useAllocationHistory.js";
|
||||||
import { useProjectDragContext } from "~/hooks/useProjectDragContext.js";
|
import { useProjectDragContext } from "~/hooks/useProjectDragContext.js";
|
||||||
import { useTimelineDrag } from "~/hooks/useTimelineDrag.js";
|
import { useTimelineDrag } from "~/hooks/useTimelineDrag.js";
|
||||||
@@ -685,15 +685,31 @@ function TimelineViewContent({
|
|||||||
const scrollRafRef = useRef<number | null>(null);
|
const scrollRafRef = useRef<number | null>(null);
|
||||||
const [scrollLeft, setScrollLeft] = useState(0);
|
const [scrollLeft, setScrollLeft] = useState(0);
|
||||||
|
|
||||||
|
// Pixels to add to scrollLeft after a left-extension re-render (prevents jump).
|
||||||
|
const pendingLeftCompensationPx = useRef(0);
|
||||||
|
|
||||||
|
// Apply scroll compensation synchronously after the canvas grows leftward.
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const px = pendingLeftCompensationPx.current;
|
||||||
|
if (px === 0) return;
|
||||||
|
const el = scrollContainerRef.current;
|
||||||
|
if (el) el.scrollLeft += px;
|
||||||
|
pendingLeftCompensationPx.current = 0;
|
||||||
|
}, [viewStart]);
|
||||||
|
|
||||||
|
// 5-year floor — no practical data exists further back; prevents runaway growth.
|
||||||
|
const minDate = useMemo(() => addDays(today, -(365 * 5)), [today]);
|
||||||
|
|
||||||
// ─── Navigation callbacks for TimelineToolbar ────────────────────────────
|
// ─── Navigation callbacks for TimelineToolbar ────────────────────────────
|
||||||
const handleNavigateBack = useCallback(
|
const handleNavigateBack = useCallback(
|
||||||
() => setViewStart((v) => addDays(v, -28)),
|
() =>
|
||||||
[setViewStart],
|
setViewStart((v) => {
|
||||||
);
|
const candidate = addDays(v, -28);
|
||||||
const handleNavigateToday = useCallback(
|
return candidate < minDate ? minDate : candidate;
|
||||||
() => setViewStart(addDays(today, -30)),
|
}),
|
||||||
[setViewStart, today],
|
[setViewStart, minDate],
|
||||||
);
|
);
|
||||||
|
const handleNavigateToday = useCallback(() => setViewStart(today), [setViewStart, today]);
|
||||||
const handleNavigateForward = useCallback(
|
const handleNavigateForward = useCallback(
|
||||||
() => setViewStart((v) => addDays(v, 28)),
|
() => setViewStart((v) => addDays(v, 28)),
|
||||||
[setViewStart],
|
[setViewStart],
|
||||||
@@ -709,10 +725,31 @@ function TimelineViewContent({
|
|||||||
const handleContainerScroll = useCallback(() => {
|
const handleContainerScroll = useCallback(() => {
|
||||||
const el = scrollContainerRef.current;
|
const el = scrollContainerRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
|
// Right-edge: extend future range
|
||||||
const distanceFromRight = el.scrollWidth - el.scrollLeft - el.clientWidth;
|
const distanceFromRight = el.scrollWidth - el.scrollLeft - el.clientWidth;
|
||||||
if (distanceFromRight < CELL_WIDTH * 40) {
|
if (distanceFromRight < CELL_WIDTH * 40) {
|
||||||
setViewDays((d) => d + 120);
|
setViewDays((d) => d + 120);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Left-edge: prepend past range and compensate scroll position so viewport doesn't jump
|
||||||
|
if (el.scrollLeft < CELL_WIDTH * 40 && viewStart > minDate) {
|
||||||
|
const daysToPrepend = 120;
|
||||||
|
// Count the exact visible days (respecting showWeekends) being prepended
|
||||||
|
let prependedVisible = 0;
|
||||||
|
for (let i = 1; i <= daysToPrepend; i++) {
|
||||||
|
const d = addDays(viewStart, -i);
|
||||||
|
const dow = d.getDay();
|
||||||
|
if (filters.showWeekends || (dow !== 0 && dow !== 6)) prependedVisible++;
|
||||||
|
}
|
||||||
|
pendingLeftCompensationPx.current = prependedVisible * CELL_WIDTH;
|
||||||
|
setViewStart((v) => {
|
||||||
|
const candidate = addDays(v, -daysToPrepend);
|
||||||
|
return candidate < minDate ? minDate : candidate;
|
||||||
|
});
|
||||||
|
setViewDays((d) => d + daysToPrepend);
|
||||||
|
}
|
||||||
|
|
||||||
scrollLeftRef.current = el.scrollLeft;
|
scrollLeftRef.current = el.scrollLeft;
|
||||||
if (scrollRafRef.current === null) {
|
if (scrollRafRef.current === null) {
|
||||||
scrollRafRef.current = requestAnimationFrame(() => {
|
scrollRafRef.current = requestAnimationFrame(() => {
|
||||||
@@ -720,7 +757,7 @@ function TimelineViewContent({
|
|||||||
setScrollLeft(scrollLeftRef.current);
|
setScrollLeft(scrollLeftRef.current);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [CELL_WIDTH, setViewDays]);
|
}, [CELL_WIDTH, setViewDays, viewStart, minDate, setViewStart, filters.showWeekends]);
|
||||||
|
|
||||||
// ─── Canvas mousemove — only forwards event when drag overlay is active ───
|
// ─── Canvas mousemove — only forwards event when drag overlay is active ───
|
||||||
const handleMouseMove = useCallback(
|
const handleMouseMove = useCallback(
|
||||||
|
|||||||
Reference in New Issue
Block a user