Files
CapaKraken/apps/web/src/hooks/useTimelineLayout.tsx
T
Hartmut ddec3a927a feat: timeline multi-select, demand popover, resource hover card, merged tooltips, dark mode fixes
Major timeline enhancements:
- Right-click drag multi-selection with floating action bar (batch delete/assign)
- DemandPopover for demand strip details (replaces broken "Loading" modal)
- ResourceHoverCard on name hover showing skills, rates, role, chapter
- Merged heatmap+vacation tooltips into unified TimelineTooltip component
- Fixed overbooking blink animation (date normalization, z-index ordering)
- Fixed dark mode sticky column bleed-through in project view
- System roles admin page, notification task management, performance review docs

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-18 23:43:51 +01:00

78 lines
2.7 KiB
TypeScript

"use client";
import { clsx } from "clsx";
import { useMemo } from "react";
import { addDays, createDatePositionCache } from "~/components/timeline/utils.js";
import { formatMonthYear } from "~/lib/format.js";
export function useTimelineLayout(
viewStart: Date,
viewDays: number,
zoom: "day" | "week" | "month",
showWeekends: boolean,
today: Date,
) {
const CELL_WIDTH = zoom === "day" ? 40 : zoom === "week" ? 14 : 4;
// Visible dates, filtered by showWeekends
const dates = useMemo(() => {
const result: Date[] = [];
for (let i = 0; i < viewDays; i++) {
const d = addDays(viewStart, i);
if (!showWeekends && (d.getDay() === 0 || d.getDay() === 6)) continue;
result.push(d);
}
return result;
}, [viewStart, viewDays, showWeekends]);
const visibleDays = dates.length;
const totalCanvasWidth = visibleDays * CELL_WIDTH;
// O(1) position helpers via pre-computed cache
const { toLeft, toWidth } = useMemo(
() => createDatePositionCache(viewStart, viewDays, CELL_WIDTH, showWeekends),
[viewStart, viewDays, CELL_WIDTH, showWeekends],
);
// Grid lines — memoized; identical for every row
const gridLines = useMemo(() => dates.map((date, i) => {
const isToday = date.toDateString() === today.toDateString();
const isSaturday = date.getDay() === 6;
const isSunday = date.getDay() === 0;
return (
<div
key={i}
className={clsx(
"absolute top-0 bottom-0 border-r",
isToday ? "border-brand-300 dark:border-brand-700 border-r-2" :
isSaturday ? "border-amber-200 dark:border-amber-800 bg-amber-50/40 dark:bg-amber-950/20" :
isSunday ? "border-gray-200 dark:border-gray-700 bg-gray-100/60 dark:bg-gray-800/40" :
"border-gray-100 dark:border-gray-800",
)}
style={{ left: i * CELL_WIDTH, width: CELL_WIDTH }}
/>
);
}), [dates, CELL_WIDTH, today]); // eslint-disable-line react-hooks/exhaustive-deps
// Month groups for the month header
const monthGroups = useMemo(() => {
const groups: { label: string; colCount: number }[] = [];
for (const d of dates) {
const label = formatMonthYear(d);
const last = groups[groups.length - 1];
if (last && last.label === label) last.colCount++;
else groups.push({ label, colCount: 1 });
}
return groups;
}, [dates]);
// Convert clientX to a Date on the visible timeline
function xToDate(clientX: number, rowCanvasRect: DOMRect): Date {
const x = clientX - rowCanvasRect.left;
const colIndex = Math.max(0, Math.min(dates.length - 1, Math.floor(x / CELL_WIDTH)));
return dates[colIndex] ?? today;
}
return { CELL_WIDTH, dates, visibleDays, totalCanvasWidth, toLeft, toWidth, gridLines, monthGroups, xToDate };
}