import type { KeyboardEvent } from "react"; import type { StepSummary } from "../types"; import "./Timeline.css"; export type LaneStatus = "violated" | "pending" | "holds"; export interface PropertyLane { name: string; statuses: LaneStatus[]; } export interface TimelineProps { steps: StepSummary[]; lanes: PropertyLane[]; selectedIndex: number; onSelect: (index: number) => void; } const LANE_HEIGHT = 22; const LABEL_WIDTH = 140; const CELL_GAP = 2; const CHART_WIDTH = 1000; const STATUS_FILL: Record = { violated: "var(--accent-violation)", pending: "color-mix(in srgb, var(--text-primary) 30%, transparent)", holds: "color-mix(in srgb, var(--text-primary) 10%, transparent)", }; export default function Timeline({ steps, lanes, selectedIndex, onSelect }: TimelineProps) { if (steps.length === 0 || lanes.length === 0) { return
no timeline data
; } const cellAreaWidth = CHART_WIDTH - LABEL_WIDTH; const cellWidth = cellAreaWidth / steps.length; const totalRows = lanes.length + 1; const height = totalRows * LANE_HEIGHT; const handleKeyDown = (event: KeyboardEvent, stepIndex: number) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); onSelect(stepIndex); } }; const selectedColumn = steps.findIndex((step) => step.index === selectedIndex); const highlightX = selectedColumn >= 0 ? LABEL_WIDTH + selectedColumn * cellWidth + cellWidth / 2 : null; return ( {lanes.map((lane, laneIndex) => { const rowY = laneIndex * LANE_HEIGHT; return ( {lane.name} {steps.map((step, stepCol) => { const status = lane.statuses[stepCol] ?? "pending"; const x = LABEL_WIDTH + stepCol * cellWidth + CELL_GAP / 2; const w = Math.max(cellWidth - CELL_GAP, 1); const y = rowY + 3; const h = LANE_HEIGHT - 6; return ( onSelect(step.index)} onKeyDown={(event) => handleKeyDown(event, step.index)} > {`step ${step.index}: ${status}`} ); })} ); })} {steps.map((step, stepCol) => { const cx = LABEL_WIDTH + stepCol * cellWidth + cellWidth / 2; const cy = lanes.length * LANE_HEIGHT + LANE_HEIGHT / 2; return ( onSelect(step.index)} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); onSelect(step.index); } }} > {`step ${step.index}`} ); })} {highlightX !== null ? ( ) : null} ); }