UIPackage
Menu

Framework

Change language

Boilerplate repo

Project Roadmap

block dashboard

Full 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

$ npx 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…

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>&rarr;</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