8f7c69056f
BenchResourceCard, MobileProjectCard, MobileCapacityCard, DynamicFieldRenderer, BudgetStatusBar, and TimelineHeader use no hooks, event handlers, or browser APIs — they can be server components, reducing client bundle size. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
125 lines
4.5 KiB
TypeScript
125 lines
4.5 KiB
TypeScript
import { clsx } from "clsx";
|
|
import { MONTHS_SHORT } from "./timelineConstants.js";
|
|
|
|
interface TimelineHeaderProps {
|
|
monthGroups: { label: string; colCount: number }[];
|
|
dates: Date[];
|
|
CELL_WIDTH: number;
|
|
LABEL_WIDTH: number;
|
|
HEADER_MONTH_HEIGHT: number;
|
|
HEADER_DAY_HEIGHT: number;
|
|
zoom: "day" | "week" | "month";
|
|
viewMode: "resource" | "project";
|
|
today: Date;
|
|
}
|
|
|
|
export function TimelineHeader({
|
|
monthGroups,
|
|
dates,
|
|
CELL_WIDTH,
|
|
LABEL_WIDTH,
|
|
HEADER_MONTH_HEIGHT,
|
|
HEADER_DAY_HEIGHT,
|
|
zoom,
|
|
viewMode,
|
|
today,
|
|
}: TimelineHeaderProps) {
|
|
return (
|
|
<>
|
|
{/* Month header */}
|
|
<div
|
|
className="sticky top-0 z-40 flex bg-white dark:bg-gray-900 border-b border-gray-100 dark:border-gray-800"
|
|
style={{ height: HEADER_MONTH_HEIGHT }}
|
|
>
|
|
<div
|
|
className="flex-shrink-0 border-r border-gray-200 dark:border-gray-700"
|
|
style={{ width: LABEL_WIDTH }}
|
|
/>
|
|
<div className="flex">
|
|
{monthGroups.map((m, i) => (
|
|
<div
|
|
key={i}
|
|
className="text-xs font-semibold text-gray-500 dark:text-gray-400 border-r border-gray-200 dark:border-gray-700 px-2 flex items-center bg-gray-50 dark:bg-gray-800"
|
|
style={{ width: m.colCount * CELL_WIDTH }}
|
|
>
|
|
{m.label}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Day header — hidden at month zoom (cells too narrow for labels) */}
|
|
{zoom !== "month" && (
|
|
<div
|
|
className="sticky z-40 flex bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 select-none"
|
|
style={{ top: HEADER_MONTH_HEIGHT, height: HEADER_DAY_HEIGHT }}
|
|
>
|
|
<div
|
|
className="flex-shrink-0 border-r border-gray-200 dark:border-gray-700 flex items-center px-4 text-xs font-medium text-gray-400 dark:text-gray-500 uppercase tracking-wider"
|
|
style={{ width: LABEL_WIDTH }}
|
|
>
|
|
{viewMode === "resource" ? "Resource" : "Project / Resource"}
|
|
</div>
|
|
<div className="flex">
|
|
{dates.map((date, i) => {
|
|
const isToday = date.toDateString() === today.toDateString();
|
|
const dow = date.getDay();
|
|
const isMonday = dow === 1;
|
|
const isWeekend = dow === 0 || dow === 6;
|
|
// Week zoom: show label only on Mondays to avoid overcrowding
|
|
const showLabel = zoom === "day" || isMonday;
|
|
return (
|
|
<div
|
|
key={i}
|
|
className={clsx(
|
|
"flex-shrink-0 border-r flex flex-col items-center justify-center text-xs overflow-hidden",
|
|
isToday
|
|
? "bg-brand-50 dark:bg-brand-950/40 border-brand-200 dark:border-brand-800"
|
|
: isWeekend
|
|
? "bg-brand-50/60 dark:bg-brand-950/30 border-brand-200 dark:border-brand-800"
|
|
: isMonday
|
|
? "border-gray-200 dark:border-gray-700"
|
|
: "border-gray-100 dark:border-gray-800",
|
|
)}
|
|
style={{ width: CELL_WIDTH, height: HEADER_DAY_HEIGHT }}
|
|
>
|
|
{showLabel && (
|
|
<>
|
|
<span
|
|
className={clsx(
|
|
"font-medium leading-none",
|
|
isToday
|
|
? "text-brand-600"
|
|
: isWeekend
|
|
? "text-brand-600 dark:text-brand-400"
|
|
: "text-gray-600 dark:text-gray-300",
|
|
)}
|
|
>
|
|
{zoom === "week"
|
|
? `${date.getDate()} ${MONTHS_SHORT[date.getMonth()]}`
|
|
: date.getDate()}
|
|
</span>
|
|
{zoom === "day" && (
|
|
<span
|
|
className={clsx(
|
|
"text-[9px] leading-none mt-0.5",
|
|
isWeekend
|
|
? "text-brand-400 dark:text-brand-500"
|
|
: "text-gray-300 dark:text-gray-600",
|
|
)}
|
|
>
|
|
{["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"][dow]}
|
|
</span>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|