From 644843ce0cc82f5ad40c43031da8ce8030239240 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 21 Sep 2026 16:14:09 +0200 Subject: [PATCH] feat(plan): GanttBar component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single horizontal bar — same component renders both card bars (full colour) and subtask bars (same colour, optionally strikethrough when done). Uses the same TimelineRange as the header so the bar and the month ticks above it can never disagree. Keyboard-focusable with a brand-coloured focus ring. --- src/components/roadmap/gantt-bar.tsx | 75 ++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/components/roadmap/gantt-bar.tsx diff --git a/src/components/roadmap/gantt-bar.tsx b/src/components/roadmap/gantt-bar.tsx new file mode 100644 index 0000000..68f18b9 --- /dev/null +++ b/src/components/roadmap/gantt-bar.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { useMemo } from "react"; + +import { datePosition } from "@/lib/roadmap/timeline"; +import type { TimelineRange } from "@/lib/roadmap/timeline"; + +interface GanttBarProps { + href: string; + startDate: string; + targetDate: string; + range: TimelineRange; + /** Hex/rgb colour from the client. */ + color: string; + /** Indent from the left of the track — subtasks use a larger pad. */ + insetLeft: number; + /** Bar height in px. Cards taller, subtasks shorter. */ + height: number; + /** Tooltip text shown on hover (native title attribute). */ + title: string; + /** Optional strikethrough for subtasks whose `done` is true. */ + strike?: boolean; +} + +/** + * A single horizontal bar in the Gantt track. Width is derived from + * the date range against the same `TimelineRange` the header uses, so + * a card and the month ticks above it can never disagree. Falls back to + * a small minimum width so a card with `start_date == target_date` + * still gets a clickable target. + */ +export function GanttBar({ + href, + startDate, + targetDate, + range, + color, + insetLeft, + height, + title, + strike = false, +}: GanttBarProps) { + const MIN_WIDTH = 0.012; + + const { left, width } = useMemo(() => { + const anchor = startDate || targetDate; + const startFraction = datePosition(anchor, range); + const endFraction = Math.max( + startFraction + MIN_WIDTH, + datePosition(targetDate || anchor, range) + ); + return { left: startFraction, width: endFraction - startFraction }; + }, [startDate, targetDate, range]); + + return ( + + {title} + + ); +}