Project Roadmap
block dashboardFull interactive project roadmap and deliverables surface: Gantt timeline with multi-scale zooming, work breakdown task tree, overall progress metrics, search and status/assignee filters, task detail slide-over Sheet, and Add Task modal dialog.
Also available for Vue ->Installation
$ pnpm dlx shadcn@latest add https://uipkge.dev/r/react/project-roadmap.json $ npx shadcn@latest add https://uipkge.dev/r/react/project-roadmap.json $ yarn dlx shadcn@latest add https://uipkge.dev/r/react/project-roadmap.json $ bunx shadcn@latest add https://uipkge.dev/r/react/project-roadmap.json Named registry:
npx shadcn@latest add @uipkge-react/project-roadmap Installs to: components/blocks/project-roadmap/ Examples
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
initialTasks | GanttTask[] | — | optional |
title | string | — | optional |
description | string | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
RoadmapProject interface RoadmapProject {
id: string
name: string
description?: string
tasks: GanttTask[]
} Files installed (6)
-
components/blocks/project-roadmap/ProjectRoadmap.tsx 8 kB
'use client' import * as React from 'react' import { Plus, Search, Download } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input, InputGroup, InputGroupAddon } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Gantt, GanttHeader, GanttTree, GanttTimeline, type GanttTask, type GanttScale, type GanttTaskStatus, type GanttTaskPriority, } from '@/components/ui/gantt' import { RoadmapMetrics } from './RoadmapMetrics' import { RoadmapTaskDetail } from './RoadmapTaskDetail' import { RoadmapAddTaskDialog } from './RoadmapAddTaskDialog' export interface ProjectRoadmapProps { initialTasks?: GanttTask[] title?: string description?: string } const defaultTasks: GanttTask[] = [ { id: 'task-1', name: 'System Architecture & Database Schema', startDate: '2026-08-01', endDate: '2026-08-08', progress: 100, status: 'done', priority: 'high', assignee: { name: 'Sarah Connor', initials: 'SC' }, }, { id: 'task-2', name: 'Auth, RBAC & Multi-Tenant Session Engine', startDate: '2026-08-06', endDate: '2026-08-16', progress: 85, status: 'in-progress', priority: 'urgent', assignee: { name: 'Marcus Rivera', initials: 'MR' }, }, { id: 'task-3', name: 'Real-time WebSocket Notification Bus', startDate: '2026-08-12', endDate: '2026-08-22', progress: 50, status: 'in-progress', priority: 'high', assignee: { name: 'Priya Nair', initials: 'PN' }, }, { id: 'task-4', name: 'Alpha Core API Release Milestone', startDate: '2026-08-23', endDate: '2026-08-23', isMilestone: true, status: 'todo', priority: 'urgent', }, { id: 'task-5', name: 'Stripe Billing & Invoicing Integration', startDate: '2026-08-24', endDate: '2026-09-04', progress: 15, status: 'todo', priority: 'medium', assignee: { name: 'Sundar Krishnan', initials: 'SK' }, }, { id: 'task-6', name: 'End-to-End Playwright Automated Testing', startDate: '2026-08-28', endDate: '2026-09-08', progress: 0, status: 'todo', priority: 'low', assignee: { name: 'Diane Cho', initials: 'DC' }, }, { id: 'task-7', name: 'Production Cloudflare Pages Deployment', startDate: '2026-09-10', endDate: '2026-09-10', isMilestone: true, status: 'todo', }, ] export function ProjectRoadmap({ initialTasks = defaultTasks, title = 'Engineering Q3 Deliverables', description = 'Track roadmap work breakdown, sprint milestones, and cross-team dependencies.', }: ProjectRoadmapProps) { const [tasks, setTasks] = React.useState<GanttTask[]>([...initialTasks]) const [searchQuery, setSearchQuery] = React.useState('') const [statusFilter, setStatusFilter] = React.useState('all') const [assigneeFilter, setAssigneeFilter] = React.useState('all') const [scale, setScale] = React.useState<GanttScale>('day') const [selectedTask, setSelectedTask] = React.useState<GanttTask | null>(null) const [sheetOpen, setSheetOpen] = React.useState(false) const [addDialogOpen, setAddDialogOpen] = React.useState(false) const filteredTasks = React.useMemo(() => { return tasks.filter((t) => { const matchesSearch = !searchQuery || t.name.toLowerCase().includes(searchQuery.toLowerCase()) const matchesStatus = statusFilter === 'all' || t.status === statusFilter const matchesAssignee = assigneeFilter === 'all' || t.assignee?.name === assigneeFilter return matchesSearch && matchesStatus && matchesAssignee }) }, [tasks, searchQuery, statusFilter, assigneeFilter]) const handleTaskClick = (task: GanttTask) => { setSelectedTask(task) setSheetOpen(true) } const handleAddTask = (newTask: GanttTask) => { setTasks((prev) => [...prev, newTask]) } const handleStatusChange = (task: GanttTask, status: GanttTaskStatus) => { setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, status, progress: status === 'done' ? 100 : t.progress } : t)), ) } const handlePriorityChange = (task: GanttTask, priority: GanttTaskPriority) => { setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, priority } : t))) } const handleDuplicate = (task: GanttTask) => { const cloned: GanttTask = { ...task, id: `task-${Date.now()}`, name: `${task.name} (Copy)`, } setTasks((prev) => [...prev, cloned]) } const handleDelete = (task: GanttTask) => { setTasks((prev) => prev.filter((t) => t.id !== task.id)) } return ( <div data-uipkge="" data-slot="project-roadmap" className="space-y-6"> <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div> <h2 className="text-foreground text-xl font-bold tracking-tight sm:text-2xl">{title}</h2> <p className="text-muted-foreground mt-0.5 text-sm">{description}</p> </div> <div className="flex items-center gap-2"> <Button variant="outline" size="sm"> <Download className="mr-1 size-3.5" /> Export </Button> <Button size="sm" onClick={() => setAddDialogOpen(true)}> <Plus className="mr-1 size-3.5" /> Add Task </Button> </div> </div> <RoadmapMetrics tasks={tasks} /> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div className="flex max-w-lg flex-1 flex-wrap items-center gap-2"> <InputGroup size="small" className="w-56"> <InputGroupAddon> <Search className="size-3.5" /> </InputGroupAddon> <Input value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder="Filter tasks by name..." /> </InputGroup> <Select value={statusFilter} onValueChange={setStatusFilter}> <SelectTrigger className="h-8 w-32 text-xs"> <SelectValue placeholder="All Status" /> </SelectTrigger> <SelectContent> <SelectItem value="all">All Status</SelectItem> <SelectItem value="done">Completed</SelectItem> <SelectItem value="in-progress">In Progress</SelectItem> <SelectItem value="todo">To Do</SelectItem> </SelectContent> </Select> <Select value={assigneeFilter} onValueChange={setAssigneeFilter}> <SelectTrigger className="h-8 w-36 text-xs"> <SelectValue placeholder="All Assignees" /> </SelectTrigger> <SelectContent> <SelectItem value="all">All Assignees</SelectItem> <SelectItem value="Sarah Connor">Sarah Connor</SelectItem> <SelectItem value="Marcus Rivera">Marcus Rivera</SelectItem> <SelectItem value="Priya Nair">Priya Nair</SelectItem> <SelectItem value="Sundar Krishnan">Sundar Krishnan</SelectItem> <SelectItem value="Diane Cho">Diane Cho</SelectItem> </SelectContent> </Select> </div> <p className="text-muted-foreground font-mono text-xs"> Showing <span className="text-foreground font-bold">{filteredTasks.length}</span> of {tasks.length}{' '} deliverables <span className="text-muted-foreground/60 ml-2">(Right-click task for quick actions)</span> </p> </div> <Gantt tasks={filteredTasks} scale={scale} onScaleChange={setScale} className="h-[480px]"> <GanttHeader title={title} /> <div className="flex min-h-0 flex-1 overflow-hidden"> <GanttTree onTaskClick={handleTaskClick} /> <GanttTimeline onTaskClick={handleTaskClick} /> </div> </Gantt> <RoadmapTaskDetail open={sheetOpen} onOpenChange={setSheetOpen} task={selectedTask} /> <RoadmapAddTaskDialog open={addDialogOpen} onOpenChange={setAddDialogOpen} onAddTask={handleAddTask} /> </div> ) } -
components/blocks/project-roadmap/RoadmapMetrics.tsx 3 kB
'use client' import * as React from 'react' import { CheckCircle2, Clock, AlertCircle, Milestone } from 'lucide-react' import { Card, CardContent } from '@/components/ui/card' import type { GanttTask } from '@/components/ui/gantt' export interface RoadmapMetricsProps { tasks: GanttTask[] } export function RoadmapMetrics({ tasks }: RoadmapMetricsProps) { const total = tasks.length const completed = tasks.filter((t) => t.status === 'done').length const inProgress = tasks.filter((t) => t.status === 'in-progress').length const milestones = tasks.filter((t) => t.isMilestone).length const progress = total === 0 ? 0 : Math.round(tasks.reduce((acc, t) => acc + (t.progress ?? 0), 0) / total) return ( <div data-slot="roadmap-metrics" className="grid grid-cols-2 gap-3 sm:grid-cols-4"> <Card className="bg-card/50 border-border/80 shadow-none"> <CardContent className="flex items-center justify-between p-3.5"> <div> <p className="text-muted-foreground text-xs font-medium">Overall Progress</p> <p className="text-foreground mt-0.5 font-mono text-xl font-bold">{progress}%</p> </div> <div className="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-full"> <Clock className="size-4" /> </div> </CardContent> </Card> <Card className="bg-card/50 border-border/80 shadow-none"> <CardContent className="flex items-center justify-between p-3.5"> <div> <p className="text-muted-foreground text-xs font-medium">In Progress</p> <p className="text-foreground mt-0.5 font-mono text-xl font-bold">{inProgress}</p> </div> <div className="flex size-8 items-center justify-center rounded-full bg-blue-500/10 text-blue-500"> <AlertCircle className="size-4" /> </div> </CardContent> </Card> <Card className="bg-card/50 border-border/80 shadow-none"> <CardContent className="flex items-center justify-between p-3.5"> <div> <p className="text-muted-foreground text-xs font-medium">Completed</p> <p className="text-foreground mt-0.5 font-mono text-xl font-bold">{completed}</p> </div> <div className="flex size-8 items-center justify-center rounded-full bg-emerald-500/10 text-emerald-500"> <CheckCircle2 className="size-4" /> </div> </CardContent> </Card> <Card className="bg-card/50 border-border/80 shadow-none"> <CardContent className="flex items-center justify-between p-3.5"> <div> <p className="text-muted-foreground text-xs font-medium">Milestones</p> <p className="text-foreground mt-0.5 font-mono text-xl font-bold">{milestones}</p> </div> <div className="flex size-8 items-center justify-center rounded-full bg-amber-500/10 text-amber-500"> <Milestone className="size-4" /> </div> </CardContent> </Card> </div> ) } -
components/blocks/project-roadmap/RoadmapTaskDetail.tsx 3.7 kB
'use client' import * as React from 'react' import { Calendar } from 'lucide-react' import { Badge } from '@/components/ui/badge' import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '@/components/ui/sheet' import type { GanttTask } from '@/components/ui/gantt' export interface RoadmapTaskDetailProps { task: GanttTask | null open: boolean onOpenChange: (open: boolean) => void } export function RoadmapTaskDetail({ task, open, onOpenChange }: RoadmapTaskDetailProps) { if (!task) return null const getStatusBadge = () => { switch (task.status) { case 'done': return { label: 'Completed', variant: 'outline' as const, className: 'text-emerald-600 border-emerald-500/30' } case 'in-progress': return { label: 'In Progress', variant: 'default' as const, className: '' } case 'blocked': return { label: 'Blocked', variant: 'destructive' as const, className: '' } default: return { label: 'To Do', variant: 'secondary' as const, className: '' } } } const badge = getStatusBadge() return ( <Sheet open={open} onOpenChange={onOpenChange}> <SheetContent className="sm:max-w-md"> <SheetHeader className="border-border space-y-2 border-b pb-4"> <div className="flex items-center gap-2"> <Badge variant={badge.variant} className={badge.className}> {badge.label} </Badge> {task.isMilestone ? ( <Badge variant="outline" className="border-amber-500/30 text-amber-500"> Milestone </Badge> ) : null} </div> <SheetTitle className="text-foreground text-lg font-semibold">{task.name}</SheetTitle> <SheetDescription className="text-muted-foreground text-xs"> Task ID: <span className="font-mono">{task.id}</span> </SheetDescription> </SheetHeader> <div className="space-y-5 py-5 text-sm"> <div className="space-y-1.5"> <span className="text-muted-foreground text-xs font-medium tracking-wider uppercase">Schedule</span> <div className="text-foreground flex items-center gap-2 font-mono text-xs"> <Calendar className="text-muted-foreground size-4" /> <span>{task.startDate}</span> <span>→</span> <span>{task.endDate}</span> </div> </div> {task.progress != null ? ( <div className="space-y-1.5"> <div className="flex items-center justify-between text-xs"> <span className="text-muted-foreground font-medium tracking-wider uppercase">Completion</span> <span className="font-mono font-bold">{task.progress}%</span> </div> <div className="bg-muted h-2 w-full overflow-hidden rounded-full"> <div style={{ width: `${task.progress}%` }} className="bg-primary h-full rounded-full transition-all duration-300" /> </div> </div> ) : null} {task.assignee ? ( <div className="space-y-1.5"> <span className="text-muted-foreground text-xs font-medium tracking-wider uppercase">Owner</span> <div className="flex items-center gap-2.5"> <div className="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-full text-xs font-medium"> {task.assignee.initials || task.assignee.name.charAt(0)} </div> <span className="text-foreground text-sm font-medium">{task.assignee.name}</span> </div> </div> ) : null} </div> </SheetContent> </Sheet> ) } -
components/blocks/project-roadmap/RoadmapAddTaskDialog.tsx 5.9 kB
'use client' import * as React from 'react' import { Plus } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Checkbox } from '@/components/ui/checkbox' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import type { GanttTask } from '@/components/ui/gantt' export interface RoadmapAddTaskDialogProps { open: boolean onOpenChange: (open: boolean) => void onAddTask: (task: GanttTask) => void } export function RoadmapAddTaskDialog({ open, onOpenChange, onAddTask }: RoadmapAddTaskDialogProps) { const [name, setName] = React.useState('') const [startDate, setStartDate] = React.useState('2026-08-20') const [endDate, setEndDate] = React.useState('2026-08-28') const [status, setStatus] = React.useState<'todo' | 'in-progress' | 'done'>('todo') const [isMilestone, setIsMilestone] = React.useState(false) const [assigneeName, setAssigneeName] = React.useState('Marcus Rivera') const handleSubmit = (e: React.FormEvent) => { e.preventDefault() if (!name.trim()) return const newTask: GanttTask = { id: `task-${Date.now()}`, name: name.trim(), startDate, endDate: isMilestone ? startDate : endDate, status, progress: isMilestone ? 100 : 0, isMilestone, assignee: { name: assigneeName, initials: assigneeName .split(' ') .map((p) => p[0]) .join(''), }, } onAddTask(newTask) onOpenChange(false) setName('') setIsMilestone(false) } return ( <Dialog open={open} onOpenChange={onOpenChange}> <DialogContent className="sm:max-w-[480px]"> <DialogHeader> <DialogTitle>Add Roadmap Task</DialogTitle> <DialogDescription> Create a new scheduled deliverable or critical milestone in the project Gantt timeline. </DialogDescription> </DialogHeader> <form onSubmit={handleSubmit} className="space-y-4 py-2"> <div className="space-y-1.5"> <Label htmlFor="task-name">Task Name</Label> <Input id="task-name" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Design Token Hierarchy & Specs" required /> </div> <div className="grid grid-cols-2 gap-3"> <div className="space-y-1.5"> <Label htmlFor="start-date">Start Date</Label> <Input id="start-date" type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} required /> </div> <div className="space-y-1.5"> <Label htmlFor="end-date">End Date</Label> <Input id="end-date" type="date" value={endDate} disabled={isMilestone} onChange={(e) => setEndDate(e.target.value)} required /> </div> </div> <div className="border-border/80 bg-muted/20 flex items-center space-x-2 rounded-md border p-3"> <Checkbox id="milestone-toggle" checked={isMilestone} onCheckedChange={(checked) => setIsMilestone(!!checked)} /> <div className="grid gap-0.5 leading-none"> <label htmlFor="milestone-toggle" className="text-foreground cursor-pointer text-xs font-semibold select-none" > Key Project Milestone </label> <p className="text-muted-foreground text-[11px]"> Marks a zero-duration target deliverable (e.g. Beta Release, Audit sign-off) </p> </div> </div> <div className="grid grid-cols-2 gap-3"> <div className="space-y-1.5"> <Label>Initial Status</Label> <Select value={status} onValueChange={(val: any) => setStatus(val)}> <SelectTrigger> <SelectValue placeholder="Status" /> </SelectTrigger> <SelectContent> <SelectItem value="todo">To Do</SelectItem> <SelectItem value="in-progress">In Progress</SelectItem> <SelectItem value="done">Completed</SelectItem> </SelectContent> </Select> </div> <div className="space-y-1.5"> <Label>Assignee</Label> <Select value={assigneeName} onValueChange={setAssigneeName}> <SelectTrigger> <SelectValue placeholder="Assignee" /> </SelectTrigger> <SelectContent> <SelectItem value="Marcus Rivera">Marcus Rivera</SelectItem> <SelectItem value="Sarah Connor">Sarah Connor</SelectItem> <SelectItem value="Priya Nair">Priya Nair</SelectItem> <SelectItem value="Sundar Krishnan">Sundar Krishnan</SelectItem> <SelectItem value="Diane Cho">Diane Cho</SelectItem> </SelectContent> </Select> </div> </div> <DialogFooter className="pt-4"> <Button variant="outline" type="button" onClick={() => onOpenChange(false)}> Cancel </Button> <Button type="submit"> <Plus className="mr-1.5 size-3.5" /> Create Task </Button> </DialogFooter> </form> </DialogContent> </Dialog> ) } -
components/blocks/project-roadmap/types.ts 0.2 kB
import type { GanttTask } from '@/components/ui/gantt' export interface RoadmapProject { id: string name: string description?: string tasks: GanttTask[] } -
components/blocks/project-roadmap/index.ts 0.3 kB
export { ProjectRoadmap, type ProjectRoadmapProps } from './ProjectRoadmap' export { RoadmapMetrics, type RoadmapMetricsProps } from './RoadmapMetrics' export { RoadmapTaskDetail, type RoadmapTaskDetailProps } from './RoadmapTaskDetail' export { RoadmapAddTaskDialog, type RoadmapAddTaskDialogProps } from './RoadmapAddTaskDialog' export * from './types'
Raw manifest: https://uipkge.dev/r/react/project-roadmap.json