Menu

Change language

GitHub repo

Project Roadmap

blockproductivity

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.

Installation

$npx uipkge-ng@latest add project-roadmap
Manifest:https://uipkge.dev/r/angular/project-roadmap.jsonInstalls to:src/app/components/blocks/project-roadmap/

Variants

Loading interactive previews…

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)

  • src/app/components/blocks/project-roadmap/project-roadmap.component.ts9.9 kB
    // Full interactive project roadmap and deliverables surface: Gantt timeline, work
    // breakdown task tree, overall progress metrics, search + status/assignee filters,
    // task detail slide-over sheet, and an add-task modal dialog. Port of the Vue block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import { Download, LucideAngularModule, Plus, Search } from 'lucide-angular'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiGanttComponent,
      UiGanttHeaderComponent,
      UiGanttTimelineComponent,
      UiGanttTreeComponent,
    } from '@/ui/gantt/gantt.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    import {
      UiSelectComponent,
      UiSelectContentComponent,
      UiSelectItemComponent,
      UiSelectTriggerComponent,
      UiSelectValueComponent,
    } from '@/ui/select/select.component'
    import type { GanttScale, GanttTask, GanttTaskPriority, GanttTaskStatus } from '@/ui/gantt/types'
    import { UiRoadmapAddTaskDialogComponent } from './roadmap-add-task-dialog.component'
    import { UiRoadmapMetricsComponent } from './roadmap-metrics.component'
    import { UiRoadmapTaskDetailComponent } from './roadmap-task-detail.component'
    
    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',
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-project-roadmap, [ui-project-roadmap]',
      standalone: true,
      // Vue renders the root <div> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiButtonComponent,
        UiGanttComponent,
        UiGanttHeaderComponent,
        UiGanttTimelineComponent,
        UiGanttTreeComponent,
        UiInputComponent,
        UiRoadmapAddTaskDialogComponent,
        UiRoadmapMetricsComponent,
        UiRoadmapTaskDetailComponent,
        UiSelectComponent,
        UiSelectContentComponent,
        UiSelectItemComponent,
        UiSelectTriggerComponent,
        UiSelectValueComponent,
      ],
      template: `
        <div data-uipkge data-slot="project-roadmap" [class]="rootClass">
          <!-- 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 ui-button aria-label="Download attachment" variant="outline" size="sm">
                <lucide-icon [img]="Download" class="mr-1 size-3.5" />
                Export
              </button>
              <button ui-button size="sm" (click)="addDialogOpen.set(true)">
                <lucide-icon [img]="Plus" class="mr-1 size-3.5" />
                Add Task
              </button>
            </div>
          </div>
    
          <!-- Summary Metrics -->
          <ui-roadmap-metrics [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">
              <div class="relative w-56">
                <lucide-icon
                  [img]="Search"
                  class="text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2"
                />
                <ui-input
                  [value]="searchQuery()"
                  (valueChange)="searchQuery.set($event)"
                  placeholder="Filter tasks by name..."
                  class="h-8 pl-8 text-xs"
                />
              </div>
    
              <ui-select [value]="statusFilter()" (valueChange)="statusFilter.set($event)">
                <button ui-select-trigger class="h-8 w-32 text-xs">
                  <ui-select-value placeholder="All Status" />
                </button>
                <ui-select-content>
                  <ui-select-item value="all">All Status</ui-select-item>
                  <ui-select-item value="done">Completed</ui-select-item>
                  <ui-select-item value="in-progress">In Progress</ui-select-item>
                  <ui-select-item value="todo">To Do</ui-select-item>
                </ui-select-content>
              </ui-select>
    
              <ui-select [value]="assigneeFilter()" (valueChange)="assigneeFilter.set($event)">
                <button ui-select-trigger class="h-8 w-36 text-xs">
                  <ui-select-value placeholder="All Assignees" />
                </button>
                <ui-select-content>
                  <ui-select-item value="all">All Assignees</ui-select-item>
                  <ui-select-item value="Sarah Connor">Sarah Connor</ui-select-item>
                  <ui-select-item value="Marcus Rivera">Marcus Rivera</ui-select-item>
                  <ui-select-item value="Priya Nair">Priya Nair</ui-select-item>
                  <ui-select-item value="Sundar Krishnan">Sundar Krishnan</ui-select-item>
                  <ui-select-item value="Diane Cho">Diane Cho</ui-select-item>
                </ui-select-content>
              </ui-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 -->
          <ui-gantt [tasks]="filteredTasks()" [scale]="scale()" (scaleChange)="scale.set($event)" class="h-[480px]">
            <ui-gantt-header [title]="title" />
            <div class="flex min-h-0 flex-1 overflow-hidden">
              <ui-gantt-tree (taskClick)="handleTaskClick($event)" />
              <ui-gantt-timeline (taskClick)="handleTaskClick($event)" />
            </div>
          </ui-gantt>
    
          <!-- Task Detail Drawer -->
          <ui-roadmap-task-detail [task]="selectedTask()" [open]="sheetOpen()" (openChange)="sheetOpen.set($event)" />
    
          <!-- Add Task Modal Dialog -->
          <ui-roadmap-add-task-dialog
            [open]="addDialogOpen()"
            (openChange)="addDialogOpen.set($event)"
            (addTask)="handleAddTask($event)"
          />
        </div>
      `,
    })
    export class UiProjectRoadmapComponent {
      protected readonly Download = Download
      protected readonly Plus = Plus
      protected readonly Search = Search
    
      @Input() title = 'Engineering Q3 Deliverables'
      @Input() description = 'Track roadmap work breakdown, sprint milestones, and cross-team dependencies.'
      @Input() set initialTasks(v: GanttTask[] | undefined) {
        this.tasks.set((v ?? defaultTasks).map((t) => ({ ...t })))
      }
      @Input('class') className?: string
    
      readonly tasks = signal<GanttTask[]>(defaultTasks.map((t) => ({ ...t })))
      readonly searchQuery = signal('')
      readonly statusFilter = signal('all')
      readonly assigneeFilter = signal('all')
      readonly scale = signal<GanttScale>('day')
    
      readonly selectedTask = signal<GanttTask | null>(null)
      readonly sheetOpen = signal(false)
      readonly addDialogOpen = signal(false)
    
      readonly filteredTasks = computed(() => {
        const q = this.searchQuery().toLowerCase()
        const status = this.statusFilter()
        const assignee = this.assigneeFilter()
        return this.tasks().filter((t) => {
          const matchesSearch = !q || t.name.toLowerCase().includes(q)
          const matchesStatus = status === 'all' || t.status === status
          const matchesAssignee = assignee === 'all' || t.assignee?.name === assignee
          return matchesSearch && matchesStatus && matchesAssignee
        })
      })
    
      get rootClass(): string {
        return ['space-y-6', this.className].filter(Boolean).join(' ')
      }
    
      handleTaskClick(task: GanttTask): void {
        this.selectedTask.set(task)
        this.sheetOpen.set(true)
      }
    
      handleAddTask(newTask: GanttTask): void {
        this.tasks.update((prev) => [...prev, newTask])
      }
    
      handleStatusChange(task: GanttTask, status: GanttTaskStatus): void {
        this.tasks.update((prev) =>
          prev.map((t) => (t.id === task.id ? { ...t, status, progress: status === 'done' ? 100 : t.progress } : t)),
        )
      }
    
      handlePriorityChange(task: GanttTask, priority: GanttTaskPriority): void {
        this.tasks.update((prev) => prev.map((t) => (t.id === task.id ? { ...t, priority } : t)))
      }
    
      handleDuplicate(task: GanttTask): void {
        const cloned: GanttTask = { ...task, id: `task-${Date.now()}`, name: `${task.name} (Copy)` }
        this.tasks.update((prev) => [...prev, cloned])
      }
    
      handleDelete(task: GanttTask): void {
        this.tasks.update((prev) => prev.filter((t) => t.id !== task.id))
      }
    }
    
  • src/app/components/blocks/project-roadmap/roadmap-metrics.component.ts4 kB
  • src/app/components/blocks/project-roadmap/roadmap-task-detail.component.ts4.9 kB
  • src/app/components/blocks/project-roadmap/roadmap-add-task-dialog.component.ts7.3 kB
  • src/app/components/blocks/project-roadmap/types.ts0.2 kB
  • src/app/components/blocks/project-roadmap/index.ts0.3 kB

Raw manifest:https://uipkge.dev/r/angular/project-roadmap.json