refactor(web): split TimelineContext into data, view, and display contexts

Reduces unnecessary re-renders by separating the monolithic 20+ property
context into TimelineDataContext, TimelineViewContext, and
TimelineDisplayContext. Panel components now subscribe only to the
slices they need.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 08:17:58 +02:00
parent 7eac5816d6
commit f18777c365
5 changed files with 182 additions and 134 deletions
@@ -2,12 +2,13 @@
import { clsx } from "clsx"; import { clsx } from "clsx";
import { memo, useMemo, useState } from "react"; import { memo, useMemo, useState } from "react";
import { useTimelineContext } from "./TimelineContext.js"; import { useTimelineData, useTimelineView } from "./TimelineContext.js";
import { getProjectColor } from "~/lib/project-colors.js"; import { getProjectColor } from "~/lib/project-colors.js";
import { FadeIn } from "~/components/ui/FadeIn.js"; import { FadeIn } from "~/components/ui/FadeIn.js";
function ProjectColorLegendInner() { function ProjectColorLegendInner() {
const { visibleAssignments, viewMode, projectGroups } = useTimelineContext(); const { visibleAssignments, projectGroups } = useTimelineData();
const { viewMode } = useTimelineView();
const [dismissed, setDismissed] = useState(false); const [dismissed, setDismissed] = useState(false);
// Collect unique visible projects with their colors // Collect unique visible projects with their colors
@@ -76,7 +77,13 @@ function ProjectColorLegendInner() {
)} )}
aria-label="Dismiss color legend" aria-label="Dismiss color legend"
> >
<svg className="w-3 h-3" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2"> <svg
className="w-3 h-3"
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M2 2l8 8M10 2l-8 8" /> <path d="M2 2l8 8M10 2l-8 8" />
</svg> </svg>
</button> </button>
@@ -176,10 +176,9 @@ export type HolidayOverlayEntry = {
metroCityName?: string | null; metroCityName?: string | null;
}; };
// ─── Context shape ───────────────────────────────────────────────────────── // ─── Context shapes ─────────────────────────────────────────────────────────
export interface TimelineContextValue { export interface TimelineDataContextValue {
// ─ Data
assignments: TimelineAssignmentEntry[]; assignments: TimelineAssignmentEntry[];
demands: TimelineDemandEntry[]; demands: TimelineDemandEntry[];
visibleAssignments: TimelineAssignmentEntry[]; visibleAssignments: TimelineAssignmentEntry[];
@@ -190,8 +189,13 @@ export interface TimelineContextValue {
allocsByResource: Map<string, TimelineAssignmentEntry[]>; allocsByResource: Map<string, TimelineAssignmentEntry[]>;
projectGroups: ProjectGroup[]; projectGroups: ProjectGroup[];
openDemandsByProject: Map<string, TimelineDemandEntry[]>; openDemandsByProject: Map<string, TimelineDemandEntry[]>;
isLoading: boolean;
isInitialLoading: boolean;
isEntriesError: boolean;
totalAllocCount: number;
}
// ─ View state export interface TimelineViewContextValue {
viewStart: Date; viewStart: Date;
viewEnd: Date; viewEnd: Date;
viewDays: number; viewDays: number;
@@ -204,32 +208,46 @@ export interface TimelineContextValue {
viewMode: ViewMode; viewMode: ViewMode;
setViewMode: React.Dispatch<React.SetStateAction<ViewMode>>; setViewMode: React.Dispatch<React.SetStateAction<ViewMode>>;
today: Date; today: Date;
activeFilterCount: number;
}
// ─ Display preferences export interface TimelineDisplayContextValue {
displayMode: TimelineDisplayMode; displayMode: TimelineDisplayMode;
heatmapScheme: HeatmapColorScheme; heatmapScheme: HeatmapColorScheme;
blinkOverbookedDays: boolean; blinkOverbookedDays: boolean;
// ─ Loading
isLoading: boolean;
isInitialLoading: boolean;
isEntriesError: boolean;
totalAllocCount: number;
activeFilterCount: number;
// ─ SSE is initialized by the provider (no value exposed)
} }
const TimelineContext = createContext<TimelineContextValue | null>(null); export type TimelineContextValue = TimelineDataContextValue &
TimelineViewContextValue &
TimelineDisplayContextValue;
export function useTimelineContext(): TimelineContextValue { const TimelineDataContext = createContext<TimelineDataContextValue | null>(null);
const ctx = useContext(TimelineContext); const TimelineViewContext = createContext<TimelineViewContextValue | null>(null);
if (!ctx) { const TimelineDisplayContext = createContext<TimelineDisplayContextValue | null>(null);
throw new Error("useTimelineContext must be used within a <TimelineProvider>");
} export function useTimelineData(): TimelineDataContextValue {
const ctx = useContext(TimelineDataContext);
if (!ctx) throw new Error("useTimelineData must be used within a <TimelineProvider>");
return ctx; return ctx;
} }
export function useTimelineView(): TimelineViewContextValue {
const ctx = useContext(TimelineViewContext);
if (!ctx) throw new Error("useTimelineView must be used within a <TimelineProvider>");
return ctx;
}
export function useTimelineDisplay(): TimelineDisplayContextValue {
const ctx = useContext(TimelineDisplayContext);
if (!ctx) throw new Error("useTimelineDisplay must be used within a <TimelineProvider>");
return ctx;
}
/** Combined hook — use the specific hooks above to avoid unnecessary re-renders. */
export function useTimelineContext(): TimelineContextValue {
return { ...useTimelineData(), ...useTimelineView(), ...useTimelineDisplay() };
}
// ─── Provider ─────────────────────────────────────────────────────────────── // ─── Provider ───────────────────────────────────────────────────────────────
interface TimelineProviderProps { interface TimelineProviderProps {
@@ -752,7 +770,7 @@ export function TimelineProvider({
filters.projectIds.length + filters.projectIds.length +
filters.countryCodes.length; filters.countryCodes.length;
const value = useMemo<TimelineContextValue>( const dataValue = useMemo<TimelineDataContextValue>(
() => ({ () => ({
assignments, assignments,
demands, demands,
@@ -764,26 +782,10 @@ export function TimelineProvider({
allocsByResource, allocsByResource,
projectGroups, projectGroups,
openDemandsByProject, openDemandsByProject,
viewStart,
viewEnd,
viewDays,
setViewStart,
setViewDays,
filters,
setFilters,
filterOpen,
setFilterOpen,
viewMode,
setViewMode,
today,
displayMode,
heatmapScheme,
blinkOverbookedDays,
isLoading, isLoading,
isInitialLoading, isInitialLoading,
isEntriesError, isEntriesError,
totalAllocCount, totalAllocCount,
activeFilterCount,
}), }),
[ [
assignments, assignments,
@@ -796,23 +798,44 @@ export function TimelineProvider({
allocsByResource, allocsByResource,
projectGroups, projectGroups,
openDemandsByProject, openDemandsByProject,
viewStart,
viewEnd,
viewDays,
filters,
filterOpen,
viewMode,
today,
displayMode,
heatmapScheme,
blinkOverbookedDays,
isLoading, isLoading,
isInitialLoading, isInitialLoading,
isEntriesError, isEntriesError,
totalAllocCount, totalAllocCount,
activeFilterCount,
], ],
); );
return <TimelineContext.Provider value={value}>{children}</TimelineContext.Provider>; const viewValue = useMemo<TimelineViewContextValue>(
() => ({
viewStart,
viewEnd,
viewDays,
setViewStart,
setViewDays,
filters,
setFilters,
filterOpen,
setFilterOpen,
viewMode,
setViewMode,
today,
activeFilterCount,
}),
[viewStart, viewEnd, viewDays, filters, filterOpen, viewMode, today, activeFilterCount],
);
const displayValue = useMemo<TimelineDisplayContextValue>(
() => ({ displayMode, heatmapScheme, blinkOverbookedDays }),
[displayMode, heatmapScheme, blinkOverbookedDays],
);
return (
<TimelineDataContext.Provider value={dataValue}>
<TimelineViewContext.Provider value={viewValue}>
<TimelineDisplayContext.Provider value={displayValue}>
{children}
</TimelineDisplayContext.Provider>
</TimelineViewContext.Provider>
</TimelineDataContext.Provider>
);
} }
@@ -5,7 +5,9 @@ import { memo, startTransition, useCallback, useEffect, useMemo, useRef, useStat
import { useVirtualizer } from "@tanstack/react-virtual"; import { useVirtualizer } from "@tanstack/react-virtual";
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
import { import {
useTimelineContext, useTimelineData,
useTimelineView,
useTimelineDisplay,
type TimelineAssignmentEntry, type TimelineAssignmentEntry,
type TimelineDemandEntry, type TimelineDemandEntry,
} from "./TimelineContext.js"; } from "./TimelineContext.js";
@@ -32,7 +34,12 @@ import {
ORDER_TYPE_COLORS, ORDER_TYPE_COLORS,
} from "./timelineConstants.js"; } from "./timelineConstants.js";
import { getProjectColor } from "~/lib/project-colors.js"; import { getProjectColor } from "~/lib/project-colors.js";
import type { DragState, AllocDragState, RangeState, MultiSelectState } from "~/hooks/useTimelineDrag.js"; import type {
DragState,
AllocDragState,
RangeState,
MultiSelectState,
} from "~/hooks/useTimelineDrag.js";
import type { AllocMouseDownInfo, RowMouseDownInfo } from "./TimelineResourcePanel.js"; import type { AllocMouseDownInfo, RowMouseDownInfo } from "./TimelineResourcePanel.js";
import { import {
buildVacationBlocksByResource, buildVacationBlocksByResource,
@@ -50,10 +57,7 @@ import {
} from "./timelineHover.js"; } from "./timelineHover.js";
import { buildResourceHeatmapSeries } from "./timelineHeatmap.js"; import { buildResourceHeatmapSeries } from "./timelineHeatmap.js";
import { buildResourceCapacitySeries } from "./timelineCapacity.js"; import { buildResourceCapacitySeries } from "./timelineCapacity.js";
import { import { buildProjectRowMetrics, type ProjectDayMetric } from "./timelineProjectMetrics.js";
buildProjectRowMetrics,
type ProjectDayMetric,
} from "./timelineProjectMetrics.js";
import { import {
buildProjectFlatRows, buildProjectFlatRows,
estimateProjectRowHeight, estimateProjectRowHeight,
@@ -147,18 +151,10 @@ function TimelineProjectPanelInner({
gridLines, gridLines,
xToDate, xToDate,
}: TimelineProjectPanelProps) { }: TimelineProjectPanelProps) {
const { const { projectGroups, openDemandsByProject, allocsByResource, vacationsByResource } =
projectGroups, useTimelineData();
openDemandsByProject, const { filters, activeFilterCount, today } = useTimelineView();
allocsByResource, const { displayMode, heatmapScheme, blinkOverbookedDays } = useTimelineDisplay();
vacationsByResource,
filters,
displayMode,
heatmapScheme,
blinkOverbookedDays,
activeFilterCount,
today,
} = useTimelineContext();
const visualAllocsByResource = useMemo(() => { const visualAllocsByResource = useMemo(() => {
if (optimisticAllocations.size === 0) return allocsByResource; if (optimisticAllocations.size === 0) return allocsByResource;
@@ -171,7 +167,8 @@ function TimelineProjectPanelInner({
}, [allocsByResource, optimisticAllocations]); }, [allocsByResource, optimisticAllocations]);
const visualProjectGroups = useMemo( const visualProjectGroups = useMemo(
() => projectGroups.map((project) => ({ () =>
projectGroups.map((project) => ({
...project, ...project,
resourceRows: project.resourceRows.map((row) => ({ resourceRows: project.resourceRows.map((row) => ({
...row, ...row,
@@ -224,7 +221,15 @@ function TimelineProjectPanelInner({
totalCanvasWidth, totalCanvasWidth,
filters.showWeekends, filters.showWeekends,
), ),
[CELL_WIDTH, filters.showVacations, filters.showWeekends, toLeft, toWidth, totalCanvasWidth, vacationsByResource], [
CELL_WIDTH,
filters.showVacations,
filters.showWeekends,
toLeft,
toWidth,
totalCanvasWidth,
vacationsByResource,
],
); );
const projectRowMetrics = useMemo(() => { const projectRowMetrics = useMemo(() => {
@@ -262,10 +267,7 @@ function TimelineProjectPanelInner({
const rect = e.currentTarget.getBoundingClientRect(); const rect = e.currentTarget.getBoundingClientRect();
const dayIndex = Math.floor((e.clientX - rect.left) / CELL_WIDTH); const dayIndex = Math.floor((e.clientX - rect.left) / CELL_WIDTH);
if ( if (dayIndex === lastHeatmapDayRef.current && resourceId === lastHeatmapResourceRef.current)
dayIndex === lastHeatmapDayRef.current &&
resourceId === lastHeatmapResourceRef.current
)
return; return;
pendingHeatmapRef.current = { clientX: e.clientX, rect, resourceId }; pendingHeatmapRef.current = { clientX: e.clientX, rect, resourceId };
@@ -310,7 +312,14 @@ function TimelineProjectPanelInner({
return; return;
} }
updateTooltipPosition(vacationTooltipPosRef, vacationTooltipRef, e.clientX, e.clientY, 14, -8); updateTooltipPosition(
vacationTooltipPosRef,
vacationTooltipRef,
e.clientX,
e.clientY,
14,
-8,
);
scheduleVacationHoverUpdate({ scheduleVacationHoverUpdate({
frameRef: vacationHoverRafRef, frameRef: vacationHoverRafRef,
hoveredKeyRef: hoveredVacationKeyRef, hoveredKeyRef: hoveredVacationKeyRef,
@@ -350,16 +359,13 @@ function TimelineProjectPanelInner({
} }
}, [demandHover]); }, [demandHover]);
const handleDemandHoverMove = useCallback( const handleDemandHoverMove = useCallback((e: React.MouseEvent, demand: TimelineDemandEntry) => {
(e: React.MouseEvent, demand: TimelineDemandEntry) => {
updateTooltipPosition(demandTooltipPosRef, demandTooltipRef, e.clientX, e.clientY, 16, -36); updateTooltipPosition(demandTooltipPosRef, demandTooltipRef, e.clientX, e.clientY, 16, -36);
startTransition(() => { startTransition(() => {
setDemandHover(buildDemandHoverData(demand)); setDemandHover(buildDemandHoverData(demand));
}); });
}, }, []);
[],
);
useEffect( useEffect(
() => () => { () => () => {
@@ -432,8 +438,14 @@ function TimelineProjectPanelInner({
return ( return (
<div <div
data-project-group="true" data-project-group="true"
className={clsx("flex border-b border-gray-200 dark:border-gray-700 group/proj", colors.light)} className={clsx(
style={{ height: PROJECT_HEADER_HEIGHT, borderLeft: `4px solid ${customColor ?? projectColor.hex}` }} "flex border-b border-gray-200 dark:border-gray-700 group/proj",
colors.light,
)}
style={{
height: PROJECT_HEADER_HEIGHT,
borderLeft: `4px solid ${customColor ?? projectColor.hex}`,
}}
> >
<div <div
className={clsx( className={clsx(
@@ -570,7 +582,9 @@ function TimelineProjectPanelInner({
<div className="text-xs font-medium text-gray-800 dark:text-gray-200 truncate cursor-pointer hover:text-brand-600 dark:hover:text-brand-400 transition-colors"> <div className="text-xs font-medium text-gray-800 dark:text-gray-200 truncate cursor-pointer hover:text-brand-600 dark:hover:text-brand-400 transition-colors">
{row.resource.displayName} {row.resource.displayName}
</div> </div>
<div className="text-[10px] text-gray-400 dark:text-gray-500 truncate">{row.resource.eid}</div> <div className="text-[10px] text-gray-400 dark:text-gray-500 truncate">
{row.resource.eid}
</div>
</div> </div>
</div> </div>
@@ -721,7 +735,9 @@ function renderOpenDemandRow(
? ?
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<div className="text-xs font-medium text-amber-700 dark:text-amber-400 truncate">Open demand</div> <div className="text-xs font-medium text-amber-700 dark:text-amber-400 truncate">
Open demand
</div>
<div className="text-[10px] text-amber-500 dark:text-amber-600 truncate"> <div className="text-[10px] text-amber-500 dark:text-amber-600 truncate">
{openDemandCount} open demand{openDemandCount > 1 ? "s" : ""} {openDemandCount} open demand{openDemandCount > 1 ? "s" : ""}
</div> </div>
@@ -741,19 +757,23 @@ function renderOpenDemandRow(
const allocStart = new Date(alloc.startDate); const allocStart = new Date(alloc.startDate);
const allocEnd = new Date(alloc.endDate); const allocEnd = new Date(alloc.endDate);
const isAllocDragged = allocDragState.isActive && allocDragState.allocationId === alloc.id; const isAllocDragged =
allocDragState.isActive && allocDragState.allocationId === alloc.id;
const dispStart = const dispStart =
isAllocDragged && allocDragState.currentStartDate isAllocDragged && allocDragState.currentStartDate
? allocDragState.currentStartDate ? allocDragState.currentStartDate
: allocStart; : allocStart;
const dispEnd = const dispEnd =
isAllocDragged && allocDragState.currentEndDate ? allocDragState.currentEndDate : allocEnd; isAllocDragged && allocDragState.currentEndDate
? allocDragState.currentEndDate
: allocEnd;
// Multi-drag visual offset // Multi-drag visual offset
const isMultiDragTarget = const isMultiDragTarget =
multiSelectState.isMultiDragging && multiSelectState.isMultiDragging && selectedAllocationSet.has(alloc.id);
selectedAllocationSet.has(alloc.id); const multiDragPx = isMultiDragTarget
const multiDragPx = isMultiDragTarget ? multiSelectState.multiDragDaysDelta * CELL_WIDTH : 0; ? multiSelectState.multiDragDaysDelta * CELL_WIDTH
: 0;
const multiDragMode = multiSelectState.multiDragMode; const multiDragMode = multiSelectState.multiDragMode;
let left = toLeft(dispStart); let left = toLeft(dispStart);
@@ -838,9 +858,12 @@ function renderOpenDemandRow(
border: `2px dashed ${roleColor}B3`, border: `2px dashed ${roleColor}B3`,
...((multiDragPx && multiDragMode === "move") || dragTransform ...((multiDragPx && multiDragMode === "move") || dragTransform
? { ? {
transform: [dragTransform, multiDragPx && multiDragMode === "move" transform: [
dragTransform,
multiDragPx && multiDragMode === "move"
? `translateX(${multiDragPx}px)` ? `translateX(${multiDragPx}px)`
: null] : null,
]
.filter(Boolean) .filter(Boolean)
.join(" "), .join(" "),
} }
@@ -964,18 +987,18 @@ function renderProjectUtilOverlay(
const projPct = (projH / capacityH) * 100; const projPct = (projH / capacityH) * 100;
const totalPct = (totalH / capacityH) * 100; const totalPct = (totalH / capacityH) * 100;
const projColor = useHeatmapColors const projColor = useHeatmapColors
? heatmapColor( ? (heatmapColor(
projPct, projPct,
heatmapScheme as import("~/hooks/useAppPreferences.js").HeatmapColorScheme, heatmapScheme as import("~/hooks/useAppPreferences.js").HeatmapColorScheme,
"bar", "bar",
) ?? "rgba(59,130,246,0.8)" ) ?? "rgba(59,130,246,0.8)")
: "rgba(96,165,250,0.8)"; : "rgba(96,165,250,0.8)";
const totalColor = useHeatmapColors const totalColor = useHeatmapColors
? heatmapColor( ? (heatmapColor(
totalPct, totalPct,
heatmapScheme as import("~/hooks/useAppPreferences.js").HeatmapColorScheme, heatmapScheme as import("~/hooks/useAppPreferences.js").HeatmapColorScheme,
"bar", "bar",
) ?? "rgba(156,163,175,0.5)" ) ?? "rgba(156,163,175,0.5)")
: isOver : isOver
? "rgba(252,211,77,0.8)" ? "rgba(252,211,77,0.8)"
: "rgba(209,213,219,0.8)"; : "rgba(209,213,219,0.8)";
@@ -1081,8 +1104,7 @@ function renderProjectDragHandles(
// Multi-drag visual offset // Multi-drag visual offset
const isMultiDragTarget = const isMultiDragTarget =
multiSelectState.isMultiDragging && multiSelectState.isMultiDragging && selectedAllocationSet.has(alloc.id);
selectedAllocationSet.has(alloc.id);
const multiDragPx = isMultiDragTarget ? multiSelectState.multiDragDaysDelta * CELL_WIDTH : 0; const multiDragPx = isMultiDragTarget ? multiSelectState.multiDragDaysDelta * CELL_WIDTH : 0;
const multiDragMode = multiSelectState.multiDragMode; const multiDragMode = multiSelectState.multiDragMode;
@@ -1132,9 +1154,7 @@ function renderProjectDragHandles(
? { ? {
transform: [ transform: [
dragTransform, dragTransform,
multiDragPx && multiDragMode === "move" multiDragPx && multiDragMode === "move" ? `translateX(${multiDragPx}px)` : null,
? `translateX(${multiDragPx}px)`
: null,
] ]
.filter(Boolean) .filter(Boolean)
.join(" "), .join(" "),
@@ -3,7 +3,12 @@
import { clsx } from "clsx"; import { clsx } from "clsx";
import { memo, startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { memo, startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useVirtualizer } from "@tanstack/react-virtual"; import { useVirtualizer } from "@tanstack/react-virtual";
import { useTimelineContext, type TimelineAssignmentEntry } from "./TimelineContext.js"; import {
useTimelineData,
useTimelineView,
useTimelineDisplay,
type TimelineAssignmentEntry,
} from "./TimelineContext.js";
import { applyVisualOverrides, type TimelineVisualOverrides } from "./allocationVisualState.js"; import { applyVisualOverrides, type TimelineVisualOverrides } from "./allocationVisualState.js";
import { ConflictOverlay } from "./ConflictOverlay.js"; import { ConflictOverlay } from "./ConflictOverlay.js";
import { computeSubLanes } from "./utils.js"; import { computeSubLanes } from "./utils.js";
@@ -117,18 +122,9 @@ function TimelineResourcePanelInner({
gridLines, gridLines,
xToDate, xToDate,
}: TimelineResourcePanelProps) { }: TimelineResourcePanelProps) {
const { const { resources, allocsByResource, vacationsByResource } = useTimelineData();
resources, const { filters, viewStart, viewEnd, activeFilterCount } = useTimelineView();
allocsByResource, const { displayMode, heatmapScheme, blinkOverbookedDays } = useTimelineDisplay();
vacationsByResource,
filters,
viewStart,
viewEnd,
displayMode,
heatmapScheme,
blinkOverbookedDays,
activeFilterCount,
} = useTimelineContext();
// ─── Heatmap hover state ──────────────────────────────────────────────────── // ─── Heatmap hover state ────────────────────────────────────────────────────
const heatmapRafRef = useRef<number | null>(null); const heatmapRafRef = useRef<number | null>(null);
@@ -28,7 +28,8 @@ import { HEADER_DAY_HEIGHT, HEADER_MONTH_HEIGHT, LABEL_WIDTH } from "./timelineC
import { formatDateShort } from "~/lib/format.js"; import { formatDateShort } from "~/lib/format.js";
import { import {
TimelineProvider, TimelineProvider,
useTimelineContext, useTimelineData,
useTimelineView,
type TimelineAssignmentEntry, type TimelineAssignmentEntry,
} from "./TimelineContext.js"; } from "./TimelineContext.js";
import { TimelineResourcePanel } from "./TimelineResourcePanel.js"; import { TimelineResourcePanel } from "./TimelineResourcePanel.js";
@@ -339,17 +340,22 @@ function TimelineViewContent({
undo: () => Promise<void>; undo: () => Promise<void>;
redo: () => Promise<void>; redo: () => Promise<void>;
}) { }) {
const ctx = useTimelineContext();
const { const {
resources, resources,
projectGroups, projectGroups,
allocsByResource, allocsByResource,
openDemandsByProject, openDemandsByProject,
visibleAssignments,
visibleDemands,
isLoading,
isInitialLoading,
isEntriesError,
totalAllocCount,
} = useTimelineData();
const {
viewStart, viewStart,
viewEnd, viewEnd,
viewDays, viewDays,
visibleAssignments,
visibleDemands,
setViewStart, setViewStart,
setViewDays, setViewDays,
filters, filters,
@@ -359,11 +365,7 @@ function TimelineViewContent({
viewMode, viewMode,
setViewMode, setViewMode,
today, today,
isLoading, } = useTimelineView();
isInitialLoading,
isEntriesError,
totalAllocCount,
} = ctx;
const scrollContainerRef = useRef<HTMLDivElement>(null); const scrollContainerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLDivElement>(null); const canvasRef = useRef<HTMLDivElement>(null);