Gantt Timeline
alphav0.6.0Editable Gantt timeline — per-task bars, collapsible summary rows, milestone diamonds, continuous zoom from hours to quarters, and a today line.
Context
Gantt Timeline is the time-axis sibling of task-card (cards) and kanban-board (columns): it consumes the SAME canonical TaskItem type and lays it on a horizontal time axis, so a 'Timeline' tab is literally the same task data the List + Board tabs show, on a third surface. v0.2 makes it editable: opt in with `editable` and edits fire task-card-shaped events + onChange(TaskItem[]) for the controlled consumer to echo (no internal data state). The permission matrix (TaskPermissions) + per-action predicates are reused from task-card — the gantt is the third consumer after the card and task-tree. Use it for sprint/cycle planning, delivery roadmaps (collapse to epics), content embargo→sunset schedules, and agent/pipeline run windows. Compound structure: GanttTimelineRoot (headless provider) + flat parts (Toolbar/Axis/Gutter/Body) + Tier-C primitives (GanttBar/SummaryBar/MilestoneDiamond/TodayLine/GutterRow/AxisHeader/BarTooltip/GanttContextMenu) + the GanttTimeline assembly. The full-card tooltip and the edit editor lazy-load task-card, so the default read-only path keeps it out of the bundle.
Installation
pnpm dlx shadcn@latest init"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}pnpm dlx shadcn@latest add @ilinxa/gantt-timelineAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/gantt-timeline-fixturesPreview
Demo source
Usage
When to use
Reach for GanttTimeline when you already render the canonical TaskItem[] as cards (task-card) or columns (kanban-board) and want a third surface that answers “what runs when, and what overlaps?”— a time axis the other surfaces can't provide. Editing (drag-to-reschedule, resize, create, delete, reparent, detail-edit) is opt-in via editable — off by default, so it stays a clean read-only timeline until you want more.
Basic example
import { GanttTimeline } from "@/components/gantt-timeline"
export function TasksTimeline({ tasks }) {
return (
<GanttTimeline
data={tasks} // the same TaskItem[] as List + Board
statusOptions={STATUS_OPTIONS}
defaultZoom="week"
onTaskClick={(t) => openTaskDetail(t.id)}
/>
)
}Composed (lighter)
Drop the assembly and hand-place the parts. GanttTimelineRoot holds all state + gestures; any subset of GanttTimelineGutter / GanttTimelineAxis / GanttTimelineBody wires itself through context — no prop-drilling.
<GanttTimelineRoot data={tasks} statusOptions={STATUS_OPTIONS} defaultZoom="day">
<GanttTimelineAxis />
<div className="flex h-105">
<GanttTimelineGutter />
<GanttTimelineBody />
</div>
</GanttTimelineRoot>Editing (v0.2–v0.4, opt-in)
Set editable and wire onChange — data is controlled, so you echo the mutated TaskItem[] back (and get undo/redo for free from a history stack). Drag bars to move, drag edges to resize, double-click or right-click to edit, drag the gutter grip to reparent, and drag a summary bracket to group-move the whole subtree (v0.3). To draw a new task, flip the toolbar Draw toggle (v0.4) and drag on an empty row — with Draw off, an empty-row drag pans instead. Gated by the same permissions matrix as task-card / task-tree.
<GanttTimeline
data={tasks}
editable
onChange={setTasks} // controlled echo (source of truth)
permissions={{ byItem: { "epic-1": { remove: false } } }}
onItemMoved={(e) => persist(e)}
/>Notes
- Navigation: drag to pan (flick for momentum), pinch or ⌘/ctrl-wheel to zoom toward the cursor, plain wheel scrolls rows, shift-wheel pans. Toolbar
+ / − / Fit / Todayand the imperative handle cover the same ground. - Bars: effective window is
startAt ?? setAt→expireAt ?? (start + duration); no end ⇒ a milestone diamond. Color is status-tone fill (done=gray, blocked=red, active=urgency ramp imported from task-card);borderColoroverrides per item; overdue adds a red end-cap. - Tooltip: lightweight by default; pass
renderTooltip={(item) => <GanttFullCardTooltip item={item} />}to lazy-embed the full card (task-card only enters the bundle then). - Keyboard: the gutter is a WAI-ARIA tree — ↑/↓ move rows, ←/→ collapse/expand, Enter activates, Space toggles. Pan/zoom is gesture + toolbar.
- SSR: pass
nowfor deterministic first paint; otherwise the today line + urgency resolve after mount.
Features
- One bar per TaskItem: effectiveStart = startAt ?? setAt; effectiveEnd = expireAt ?? (start + duration); no end ⇒ milestone diamond
- Collapsible WBS summary rows from children; summary bar spans min(child start) → max(child end)
- Continuous zoom (pixels-per-time) with five named header buckets (hour · day · week · month · quarter) auto-selected with hysteresis; default week
- Pan/swipe/zoom canvas: drag-pan with dominant-direction lock + flick momentum + boundary resistance; pinch + ⌘/ctrl-wheel focal-point zoom; +/−/fit toolbar; disableGestures opt-out
- Filled bars colored by status tone: done=gray, blocked=red, active=time-urgency ramp (green→red) imported from task-card; per-item borderColor override; overdue red end-cap
- Today 'now' line; SSR-safe first paint (now prop seeds; otherwise deferred to post-mount); colorRefreshIntervalMs tick
- Frozen gutter tree: caret + assignee avatar + name + label dots + status badge; row virtualization via @tanstack/react-virtual (gutter mirrors the body scroll)
- Read-only interactions: lightweight hover tooltip (renderTooltip override; GanttFullCardTooltip lazy-embeds the full card), click select + onTaskClick, collapse/expand, onViewportChange
- WAI-ARIA tree on the gutter: role=tree/treeitem + aria-level/expanded/selected; arrow nav + Home/End + Enter + Space (conflict-free with viewport pan)
- Imperative handle: scrollToDate / scrollToItem / scrollToToday / expandAll / collapseAll / setZoom / zoomBy / zoomToFit / addTask / deleteTask / editTask / beginRename
- States: empty / loading (GanttTimelineSkeleton) / single / deep-nest / all-milestones / dense; weekend shading (opt-in, day/week zoom)
- v0.2 editing (opt-in via `editable`, default off = byte-identical v1): drag-to-reschedule + edge-resize (snap to active unit, Alt = free) + milestone drag, with a live preview ghost
- v0.2 CRUD: draw-on-canvas + gutter + create, delete (affordance / Delete key / context-menu), inline rename (double-click / F2), gutter reparent+reorder via @dnd-kit three-zone drops
- v0.2 detail editing: right-click context-menu (Edit / Add / Status / Delete) + embedded <TaskCard editable> in a lazy overlay; edits fire onItemAdded/Removed/Moved/onFieldEdited/onStatusChanged + onChange(TaskItem[])
- v0.2 permissions: reuses task-card's TaskPermissions matrix (default/byLevel/byItem/inherit) + canMove/Resize/Delete/CreateChild/EditItem predicates + onPermissionDenied; item.locked blocks all; summary bars non-manipulable for single-edit (group-move added in v0.3)
- v0.3 group-move: drag a WBS summary bracket → rigidly shift the whole subtree by one snapped delta (Alt = free); atomic permission (summary + every descendant leaf movable, else the drag pans); fires per-leaf onFieldEdited/onTaskReschedule + one onChange; imperative shiftTaskGroup(id, deltaMs)
- v0.4 Draw mode: a toolbar Draw toggle (editable only) gates draw-to-create — empty-row drag pans by default (off) and draws a new task only when on, ending the draw-vs-pan conflict; the whole gesture pipeline defers intent to the first move (a press classifies what's under it, then click→select, vertical→scroll, horizontal→resize/move/group-move/draw/pan); right-click menu fix — its portaled pointer/key events no longer bubble back into the canvas and swallow menu-item clicks
- v0.5 double-click-to-create: double-click an empty row area → a lightweight quick-composer floats at the pointer (name autofocus + status pick, Enter creates a snapped sibling, More options → full editor); independent of Draw mode; `quickCompose` (default true) / `renderQuickComposer` override
- v0.5 cross-surface clipboard: copy/cut/paste TaskItems through the shared `ilinxa/task` envelope (task-card/lib/clipboard) — ⌘/Ctrl+C·X·V (document-level, gated on focus + skipped over inputs) + right-click Copy/Cut; paste re-ids each subtree and lands as a sibling of the selection (dates preserved); interops with calendar / card-tree / tree
- v0.5 Priority submenu: the right-click menu gains a Priority picker (when priorityOptions provided); changePriority mutates + onChange only (parity with calendar — 'priority' is not a typed field event)