Project Roadmap
blockproductivityFull 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.
Installation
$pnpm dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/project-roadmap.json$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/project-roadmap.json$yarn dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/project-roadmap.json$bunx shadcn-svelte@latest add https://uipkge.dev/r/svelte/project-roadmap.jsonManifest:
https://uipkge.dev/r/svelte/project-roadmap.jsonInstalls to:src/lib/components/blocks/project-roadmap/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
RoadmapProjectinterface RoadmapProject {
id: string
name: string
description?: string
tasks: GanttTask[]
}Files installed (5)
src/lib/components/blocks/project-roadmap/ProjectRoadmap.svelte6.9 kB
<script lang="ts" module> import type { GanttScale, GanttTask } from '$lib/components/ui/gantt' export interface ProjectRoadmapProps { initialTasks?: GanttTask[] title?: string description?: string } </script> <script lang="ts"> import { Plus, Search, Download } from '@lucide/svelte' import { Button } from '$lib/components/ui/button' import { Input, InputGroup, InputGroupAddon } from '$lib/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '$lib/components/ui/select' import { Gantt, GanttHeader, GanttTree, GanttTimeline } from '$lib/components/ui/gantt' import RoadmapMetrics from './RoadmapMetrics.svelte' import RoadmapTaskDetail from './RoadmapTaskDetail.svelte' import RoadmapAddTaskDialog from './RoadmapAddTaskDialog.svelte' const DEFAULT_TASKS: 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', }, ] let { initialTasks = DEFAULT_TASKS, title = 'Engineering Q3 Deliverables', description = 'Track roadmap work breakdown, sprint milestones, and cross-team dependencies.', }: ProjectRoadmapProps = $props() // svelte-ignore state_referenced_locally -- intentionally initial-only; tasks are locally owned after mount. let tasks = $state<GanttTask[]>([...initialTasks]) let searchQuery = $state('') let statusFilter = $state('all') let assigneeFilter = $state('all') let scale = $state<GanttScale>('day') let selectedTask = $state<GanttTask | null>(null) let sheetOpen = $state(false) let addDialogOpen = $state(false) const filteredTasks = $derived( 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 }), ) function handleTaskClick(task: GanttTask) { selectedTask = task sheetOpen = true } function handleAddTask(newTask: GanttTask) { tasks.push(newTask) } </script> <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 aria-label="Download attachment" variant="outline" size="sm"> <Download class="mr-1 size-3.5" /> Export </Button> <Button size="sm" onclick={() => (addDialogOpen = true)}> <Plus class="mr-1 size-3.5" /> Add Task </Button> </div> </div> <!-- Summary Metrics --> <RoadmapMetrics {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 bind:value={searchQuery} placeholder="Filter tasks by name..." /> </InputGroup> <Select bind:value={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 bind:value={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} onscalechange={(s) => (scale = s)} ontaskclick={handleTaskClick} class="h-[480px]"> <GanttHeader {title} /> <div class="flex min-h-0 flex-1 overflow-hidden"> <GanttTree /> <GanttTimeline /> </div> </Gantt> <!-- Task Detail Drawer --> <RoadmapTaskDetail bind:open={sheetOpen} task={selectedTask} /> <!-- Add Task Modal Dialog --> <RoadmapAddTaskDialog bind:open={addDialogOpen} onAddTask={handleAddTask} /> </div>src/lib/components/blocks/project-roadmap/RoadmapMetrics.svelte2.8 kB
<script lang="ts" module> import type { GanttTask } from '$lib/components/ui/gantt' export interface RoadmapMetricsProps { tasks: GanttTask[] } </script> <script lang="ts"> import { CheckCircle2, Clock, AlertCircle, Milestone } from '@lucide/svelte' import { Card, CardContent } from '$lib/components/ui/card' let { tasks }: RoadmapMetricsProps = $props() const total = $derived(tasks.length) const completed = $derived(tasks.filter((t) => t.status === 'done').length) const inProgress = $derived(tasks.filter((t) => t.status === 'in-progress').length) const milestones = $derived(tasks.filter((t) => t.isMilestone).length) const progress = $derived.by(() => { if (total === 0) return 0 const sum = tasks.reduce((acc, t) => acc + (t.progress ?? 0), 0) return Math.round(sum / total) }) </script> <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="bg-info/10 text-info flex size-8 items-center justify-center rounded-full"> <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="bg-success/10 text-success flex size-8 items-center justify-center rounded-full"> <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="bg-warning/10 text-warning flex size-8 items-center justify-center rounded-full"> <Milestone class="size-4" /> </div> </CardContent> </Card> </div>src/lib/components/blocks/project-roadmap/RoadmapTaskDetail.svelte3.8 kB
<script lang="ts" module> import type { GanttTask } from '$lib/components/ui/gantt' export interface RoadmapTaskDetailProps { task: GanttTask | null open?: boolean } </script> <script lang="ts"> import { Calendar } from '@lucide/svelte' import { Badge } from '$lib/components/ui/badge' import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '$lib/components/ui/sheet' let { task, open = $bindable(false) }: RoadmapTaskDetailProps = $props() const statusBadge = $derived.by(() => { if (!task?.status) return { label: 'Active', variant: 'secondary' as const, class: undefined as string | undefined } switch (task.status) { case 'done': return { label: 'Completed', variant: 'outline' as const, class: 'text-success border-success/30' } case 'in-progress': return { label: 'In Progress', variant: 'default' as const, class: undefined as string | undefined } case 'blocked': return { label: 'Blocked', variant: 'destructive' as const, class: undefined as string | undefined } default: return { label: 'To Do', variant: 'secondary' as const, class: undefined as string | undefined } } }) </script> <!-- Sheet renders no DOM of its own, so data-slot lives on the content. --> <Sheet bind:open> {#if task} <SheetContent data-slot="project-roadmap" 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> {#if task.isMilestone} <Badge variant="outline" class="border-warning/30 text-warning">Milestone</Badge> {/if} </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 --> {#if task.progress != null} <div 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-[width] duration-300" ></div> </div> </div> {/if} <!-- Assignee --> {#if task.assignee} <div 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> {/if} </div> </SheetContent> {/if} </Sheet>src/lib/components/blocks/project-roadmap/RoadmapAddTaskDialog.svelte5 kB
<script lang="ts" module> import type { GanttTask } from '$lib/components/ui/gantt' export interface RoadmapAddTaskDialogProps { open?: boolean onAddTask?: (task: GanttTask) => void } </script> <script lang="ts"> import { Plus } from '@lucide/svelte' import { Button } from '$lib/components/ui/button' import { Input } from '$lib/components/ui/input' import { Label } from '$lib/components/ui/label' import { Checkbox } from '$lib/components/ui/checkbox' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '$lib/components/ui/select' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '$lib/components/ui/dialog' let { open = $bindable(false), onAddTask }: RoadmapAddTaskDialogProps = $props() let name = $state('') let startDate = $state('2026-08-20') let endDate = $state('2026-08-28') let status = $state<'todo' | 'in-progress' | 'done'>('todo') let progress = $state(0) let isMilestone = $state(false) let assigneeName = $state('Marcus Rivera') function handleSubmit() { if (!name.trim()) return const newTask: GanttTask = { id: `task-${Date.now()}`, name: name.trim(), startDate, endDate: isMilestone ? startDate : endDate, status, progress: isMilestone ? 100 : Number(progress) || 0, isMilestone, assignee: { name: assigneeName, initials: assigneeName .split(' ') .map((p) => p[0]) .join(''), }, } onAddTask?.(newTask) open = false // Reset form name = '' progress = 0 isMilestone = false } </script> <!-- Dialog renders no DOM of its own, so data-slot lives on the content. --> <Dialog bind:open> <DialogContent data-slot="project-roadmap" class="max-w-[480px] sm:max-w-full"> <DialogHeader> <DialogTitle>Add Roadmap Task</DialogTitle> <DialogDescription> Create a new scheduled deliverable or critical milestone in the project Gantt timeline. </DialogDescription> </DialogHeader> <form onsubmit={(e) => { e.preventDefault() 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" bind:value={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" bind:value={startDate} type="date" required /> </div> <div class="space-y-1.5"> <Label for="end-date">End Date</Label> <Input id="end-date" bind:value={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" bind: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-xs"> 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 value={status} onValueChange={(v) => (status = v as typeof 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 bind:value={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" onclick={() => (open = false)}>Cancel</Button> <Button type="submit"> <Plus class="mr-1.5 size-3.5" /> Create Task </Button> </DialogFooter> </form> </DialogContent> </Dialog>src/lib/components/blocks/project-roadmap/types.ts0.2 kB
import type { GanttTask } from '$lib/components/ui/gantt' export interface RoadmapProject { id: string name: string description?: string tasks: GanttTask[] }
Raw manifest:https://uipkge.dev/r/svelte/project-roadmap.json