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 React ->Installation
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/project-roadmap.json $ npx shadcn-vue@latest add https://uipkge.dev/r/vue/project-roadmap.json $ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/project-roadmap.json $ bunx shadcn-vue@latest add https://uipkge.dev/r/vue/project-roadmap.json Named registry:
npx shadcn-vue@latest add @uipkge/project-roadmap Installs to: app/components/blocks/project-roadmap/ Examples
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
initialTasks | GanttTask[] | () => [ { id: 'task-1', name: 'System Architecture & Data… | optional |
title | string | 'Engineering Q3 Deliverables' | optional |
description | string | 'Track roadmap work breakdown | 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)
-
app/components/blocks/project-roadmap/ProjectRoadmap.vue 7.6 kB
<script setup lang="ts"> import { computed, ref } from 'vue' import { Plus, Search, Download, Filter } from 'lucide-vue-next' 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, GanttContextMenu, type GanttTask, type GanttScale, type GanttTaskStatus, type GanttTaskPriority, } from '@/components/ui/gantt' import RoadmapMetrics from './RoadmapMetrics.vue' import RoadmapTaskDetail from './RoadmapTaskDetail.vue' import RoadmapAddTaskDialog from './RoadmapAddTaskDialog.vue' interface Props { initialTasks?: GanttTask[] title?: string description?: string } const props = withDefaults(defineProps<Props>(), { title: 'Engineering Q3 Deliverables', description: 'Track roadmap work breakdown, sprint milestones, and cross-team dependencies.', initialTasks: () => [ { 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', }, ], }) const tasks = ref<GanttTask[]>([...props.initialTasks]) const searchQuery = ref('') const statusFilter = ref('all') const assigneeFilter = ref('all') const scale = ref<GanttScale>('day') const selectedTask = ref<GanttTask | null>(null) const sheetOpen = ref(false) const addDialogOpen = ref(false) const filteredTasks = computed(() => { return tasks.value.filter((t) => { const matchesSearch = !searchQuery.value || t.name.toLowerCase().includes(searchQuery.value.toLowerCase()) const matchesStatus = statusFilter.value === 'all' || t.status === statusFilter.value const matchesAssignee = assigneeFilter.value === 'all' || t.assignee?.name === assigneeFilter.value return matchesSearch && matchesStatus && matchesAssignee }) }) function handleTaskClick(task: GanttTask) { selectedTask.value = task sheetOpen.value = true } function handleAddTask(newTask: GanttTask) { tasks.value.push(newTask) } function handleStatusChange(task: GanttTask, status: GanttTaskStatus) { const target = tasks.value.find((t) => t.id === task.id) if (target) { target.status = status if (status === 'done') target.progress = 100 } } function handlePriorityChange(task: GanttTask, priority: GanttTaskPriority) { const target = tasks.value.find((t) => t.id === task.id) if (target) target.priority = priority } function handleDuplicate(task: GanttTask) { const cloned: GanttTask = { ...task, id: `task-${Date.now()}`, name: `${task.name} (Copy)`, } tasks.value.push(cloned) } function handleDelete(task: GanttTask) { tasks.value = tasks.value.filter((t) => t.id !== task.id) } </script> <template> <div data-uipkge data-slot="project-roadmap" class="space-y-6"> <!-- Section Top Header --> <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div> <h2 class="text-foreground text-xl font-bold tracking-tight sm:text-2xl">{{ title }}</h2> <p class="text-muted-foreground mt-0.5 text-sm">{{ description }}</p> </div> <div class="flex items-center gap-2"> <Button variant="outline" size="sm"> <Download class="mr-1 size-3.5" /> Export </Button> <Button size="sm" @click="addDialogOpen = true"> <Plus class="mr-1 size-3.5" /> Add Task </Button> </div> </div> <!-- Summary Metrics --> <RoadmapMetrics :tasks="tasks" /> <!-- Filters & Action Bar --> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div class="flex max-w-lg flex-1 flex-wrap items-center gap-2"> <InputGroup size="small" class="w-56"> <InputGroupAddon> <Search class="size-3.5" /> </InputGroupAddon> <Input v-model="searchQuery" placeholder="Filter tasks by name..." /> </InputGroup> <Select v-model="statusFilter"> <SelectTrigger class="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 v-model="assigneeFilter"> <SelectTrigger class="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 class="text-muted-foreground font-mono text-xs"> Showing <span class="text-foreground font-bold">{{ filteredTasks.length }}</span> of {{ tasks.length }} deliverables <span class="text-muted-foreground/60 ml-2">(Right-click task for quick actions)</span> </p> </div> <!-- Main Interactive Gantt Workspace with Right-Click Context Actions --> <Gantt :tasks="filteredTasks" :scale="scale" class="h-[480px]"> <GanttHeader :title="title" /> <div class="flex min-h-0 flex-1 overflow-hidden"> <GanttTree @click="handleTaskClick" /> <GanttTimeline @task-click="handleTaskClick" /> </div> </Gantt> <!-- Task Detail Drawer --> <RoadmapTaskDetail v-model:open="sheetOpen" :task="selectedTask" /> <!-- Add Task Modal Dialog --> <RoadmapAddTaskDialog v-model:open="addDialogOpen" @add-task="handleAddTask" /> </div> </template> -
app/components/blocks/project-roadmap/RoadmapMetrics.vue 2.9 kB
<script setup lang="ts"> import { computed } from 'vue' import { CheckCircle2, Clock, AlertCircle, Milestone } from 'lucide-vue-next' import { Card, CardContent } from '@/components/ui/card' import type { GanttTask } from '@/components/ui/gantt' const props = defineProps<{ tasks: GanttTask[] }>() const total = computed(() => props.tasks.length) const completed = computed(() => props.tasks.filter((t) => t.status === 'done').length) const inProgress = computed(() => props.tasks.filter((t) => t.status === 'in-progress').length) const milestones = computed(() => props.tasks.filter((t) => t.isMilestone).length) const progress = computed(() => { if (total.value === 0) return 0 const sum = props.tasks.reduce((acc, t) => acc + (t.progress ?? 0), 0) return Math.round(sum / total.value) }) </script> <template> <div data-slot="roadmap-metrics" class="grid grid-cols-2 gap-3 sm:grid-cols-4"> <Card class="bg-card/50 border-border/80 shadow-none"> <CardContent class="flex items-center justify-between p-3.5"> <div> <p class="text-muted-foreground text-xs font-medium">Overall Progress</p> <p class="text-foreground mt-0.5 font-mono text-xl font-bold">{{ progress }}%</p> </div> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-full"> <Clock class="size-4" /> </div> </CardContent> </Card> <Card class="bg-card/50 border-border/80 shadow-none"> <CardContent class="flex items-center justify-between p-3.5"> <div> <p class="text-muted-foreground text-xs font-medium">In Progress</p> <p class="text-foreground mt-0.5 font-mono text-xl font-bold">{{ inProgress }}</p> </div> <div class="flex size-8 items-center justify-center rounded-full bg-blue-500/10 text-blue-500"> <AlertCircle class="size-4" /> </div> </CardContent> </Card> <Card class="bg-card/50 border-border/80 shadow-none"> <CardContent class="flex items-center justify-between p-3.5"> <div> <p class="text-muted-foreground text-xs font-medium">Completed</p> <p class="text-foreground mt-0.5 font-mono text-xl font-bold">{{ completed }}</p> </div> <div class="flex size-8 items-center justify-center rounded-full bg-emerald-500/10 text-emerald-500"> <CheckCircle2 class="size-4" /> </div> </CardContent> </Card> <Card class="bg-card/50 border-border/80 shadow-none"> <CardContent class="flex items-center justify-between p-3.5"> <div> <p class="text-muted-foreground text-xs font-medium">Milestones</p> <p class="text-foreground mt-0.5 font-mono text-xl font-bold">{{ milestones }}</p> </div> <div class="flex size-8 items-center justify-center rounded-full bg-amber-500/10 text-amber-500"> <Milestone class="size-4" /> </div> </CardContent> </Card> </div> </template> -
app/components/blocks/project-roadmap/RoadmapTaskDetail.vue 3.6 kB
<script setup lang="ts"> import { computed } from 'vue' import { Calendar, User, CheckCircle2, Clock, X, Tag } from 'lucide-vue-next' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '@/components/ui/sheet' import type { GanttTask } from '@/components/ui/gantt' const props = defineProps<{ task: GanttTask | null open: boolean }>() const emits = defineEmits<{ (e: 'update:open', val: boolean): void }>() const statusBadge = computed(() => { if (!props.task?.status) return { label: 'Active', variant: 'secondary' as const } switch (props.task.status) { case 'done': return { label: 'Completed', variant: 'outline' as const, class: 'text-emerald-600 border-emerald-500/30' } case 'in-progress': return { label: 'In Progress', variant: 'default' as const } case 'blocked': return { label: 'Blocked', variant: 'destructive' as const } default: return { label: 'To Do', variant: 'secondary' as const } } }) </script> <template> <Sheet :open="open" @update:open="$emit('update:open', $event)"> <SheetContent v-if="task" class="sm:max-w-md"> <SheetHeader class="border-border space-y-2 border-b pb-4"> <div class="flex items-center gap-2"> <Badge :variant="statusBadge.variant" :class="statusBadge.class"> {{ statusBadge.label }} </Badge> <Badge v-if="task.isMilestone" variant="outline" class="border-amber-500/30 text-amber-500"> Milestone </Badge> </div> <SheetTitle class="text-foreground text-lg font-semibold"> {{ task.name }} </SheetTitle> <SheetDescription class="text-muted-foreground text-xs"> Task ID: <span class="font-mono">{{ task.id }}</span> </SheetDescription> </SheetHeader> <div class="space-y-5 py-5 text-sm"> <!-- Date Schedule --> <div class="space-y-1.5"> <span class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Schedule</span> <div class="text-foreground flex items-center gap-2 font-mono text-xs"> <Calendar class="text-muted-foreground size-4" /> <span>{{ task.startDate }}</span> <span>→</span> <span>{{ task.endDate }}</span> </div> </div> <!-- Progress --> <div v-if="task.progress != null" class="space-y-1.5"> <div class="flex items-center justify-between text-xs"> <span class="text-muted-foreground font-medium tracking-wider uppercase">Completion</span> <span class="font-mono font-bold">{{ task.progress }}%</span> </div> <div class="bg-muted h-2 w-full overflow-hidden rounded-full"> <div :style="{ width: `${task.progress}%` }" class="bg-primary h-full rounded-full transition-all duration-300" /> </div> </div> <!-- Assignee --> <div v-if="task.assignee" class="space-y-1.5"> <span class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Owner</span> <div class="flex items-center gap-2.5"> <div class="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 class="text-foreground text-sm font-medium">{{ task.assignee.name }}</span> </div> </div> </div> </SheetContent> </Sheet> </template> -
app/components/blocks/project-roadmap/RoadmapAddTaskDialog.vue 5.1 kB
<script setup lang="ts"> import { ref } from 'vue' import { Plus, Calendar as CalendarIcon } from 'lucide-vue-next' 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' const props = defineProps<{ open: boolean }>() const emits = defineEmits<{ (e: 'update:open', val: boolean): void (e: 'add-task', task: GanttTask): void }>() const name = ref('') const startDate = ref('2026-08-20') const endDate = ref('2026-08-28') const status = ref<'todo' | 'in-progress' | 'done'>('todo') const progress = ref(0) const isMilestone = ref(false) const assigneeName = ref('Marcus Rivera') function handleSubmit() { if (!name.value.trim()) return const newTask: GanttTask = { id: `task-${Date.now()}`, name: name.value.trim(), startDate: startDate.value, endDate: isMilestone.value ? startDate.value : endDate.value, status: status.value, progress: isMilestone.value ? 100 : Number(progress.value) || 0, isMilestone: isMilestone.value, assignee: { name: assigneeName.value, initials: assigneeName.value .split(' ') .map((p) => p[0]) .join(''), }, } emits('add-task', newTask) emits('update:open', false) // Reset form name.value = '' progress.value = 0 isMilestone.value = false } </script> <template> <Dialog :open="open" @update:open="$emit('update:open', $event)"> <DialogContent class="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 @submit.prevent="handleSubmit" class="space-y-4 py-2"> <!-- Task Name --> <div class="space-y-1.5"> <Label for="task-name">Task Name</Label> <Input id="task-name" v-model="name" placeholder="e.g. Design Token Hierarchy & Specs" required /> </div> <!-- Date Range --> <div class="grid grid-cols-2 gap-3"> <div class="space-y-1.5"> <Label for="start-date">Start Date</Label> <Input id="start-date" v-model="startDate" type="date" required /> </div> <div class="space-y-1.5"> <Label for="end-date">End Date</Label> <Input id="end-date" v-model="endDate" type="date" :disabled="isMilestone" required /> </div> </div> <!-- Milestone toggle --> <div class="border-border/80 bg-muted/20 flex items-center space-x-2 rounded-md border p-3"> <Checkbox id="milestone-toggle" v-model:checked="isMilestone" /> <div class="grid gap-0.5 leading-none"> <label for="milestone-toggle" class="text-foreground cursor-pointer text-xs font-semibold select-none"> Key Project Milestone </label> <p class="text-muted-foreground text-[11px]"> Marks a zero-duration target deliverable (e.g. Beta Release, Audit sign-off) </p> </div> </div> <!-- Status & Assignee --> <div class="grid grid-cols-2 gap-3"> <div class="space-y-1.5"> <Label>Initial Status</Label> <Select v-model="status"> <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 class="space-y-1.5"> <Label>Assignee</Label> <Select v-model="assigneeName"> <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 class="pt-4"> <Button variant="outline" type="button" @click="$emit('update:open', false)"> Cancel </Button> <Button type="submit"> <Plus class="mr-1.5 size-3.5" /> Create Task </Button> </DialogFooter> </form> </DialogContent> </Dialog> </template> -
app/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[] } -
app/components/blocks/project-roadmap/index.ts 0.3 kB
export { default as ProjectRoadmap } from './ProjectRoadmap.vue' export { default as RoadmapMetrics } from './RoadmapMetrics.vue' export { default as RoadmapTaskDetail } from './RoadmapTaskDetail.vue' export { default as RoadmapAddTaskDialog } from './RoadmapAddTaskDialog.vue' export * from './types'
Raw manifest: https://uipkge.dev/r/vue/project-roadmap.json