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 uipkge-ng@latest add project-roadmap$npx uipkge-ng@latest add project-roadmap$yarn dlx uipkge-ng@latest add project-roadmap$bunx uipkge-ng@latest add project-roadmapManifest:
https://uipkge.dev/r/angular/project-roadmap.jsonInstalls to:src/app/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 (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
// Summary metric cards for the project roadmap: overall progress, in-progress count, // completed count, and milestone count. Port of the Vue RoadmapMetrics block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { AlertCircle, CheckCircle2, Clock, LucideAngularModule, Milestone } from 'lucide-angular' import { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component' import type { GanttTask } from '@/ui/gantt/types' @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-roadmap-metrics, [ui-roadmap-metrics]', standalone: true, host: { '[attr.class]': '"contents"' }, imports: [LucideAngularModule, UiCardComponent, UiCardContentComponent], template: ` <div data-slot="roadmap-metrics" class="grid grid-cols-2 gap-3 sm:grid-cols-4"> <div ui-card class="bg-card/50 border-border/80 shadow-none"> <div ui-card-content 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"> <lucide-icon [img]="Clock" class="size-4" /> </div> </div> </div> <div ui-card class="bg-card/50 border-border/80 shadow-none"> <div ui-card-content 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"> <lucide-icon [img]="AlertCircle" class="size-4" /> </div> </div> </div> <div ui-card class="bg-card/50 border-border/80 shadow-none"> <div ui-card-content 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"> <lucide-icon [img]="CheckCircle2" class="size-4" /> </div> </div> </div> <div ui-card class="bg-card/50 border-border/80 shadow-none"> <div ui-card-content 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"> <lucide-icon [img]="Milestone" class="size-4" /> </div> </div> </div> </div> `, }) export class UiRoadmapMetricsComponent { protected readonly AlertCircle = AlertCircle protected readonly CheckCircle2 = CheckCircle2 protected readonly Clock = Clock protected readonly Milestone = Milestone @Input() set tasks(v: GanttTask[] | undefined) { this._tasks.set(v ?? []) } private readonly _tasks = signal<GanttTask[]>([]) readonly total = computed(() => this._tasks().length) readonly completed = computed(() => this._tasks().filter((t) => t.status === 'done').length) readonly inProgress = computed(() => this._tasks().filter((t) => t.status === 'in-progress').length) readonly milestones = computed(() => this._tasks().filter((t) => t.isMilestone).length) readonly progress = computed(() => { const total = this.total() if (total === 0) return 0 const sum = this._tasks().reduce((acc, t) => acc + (t.progress ?? 0), 0) return Math.round(sum / total) }) }src/app/components/blocks/project-roadmap/roadmap-task-detail.component.ts4.9 kB
// Slide-over detail drawer for the selected roadmap task: status + milestone badges, // schedule dates, completion bar, and owner. Port of the Vue RoadmapTaskDetail block 1:1. import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, computed } from '@angular/core' import { Calendar, LucideAngularModule } from 'lucide-angular' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiSheetComponent, UiSheetContentComponent, UiSheetDescriptionComponent, UiSheetHeaderComponent, UiSheetTitleComponent, } from '@/ui/sheet/sheet.component' import type { GanttTask } from '@/ui/gantt/types' @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-roadmap-task-detail, [ui-roadmap-task-detail]', standalone: true, host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiSheetComponent, UiSheetContentComponent, UiSheetDescriptionComponent, UiSheetHeaderComponent, UiSheetTitleComponent, ], template: ` <div ui-sheet data-slot="project-roadmap" [open]="open" (openChange)="openChange.emit($event)"> @if (task) { <div ui-sheet-content class="sm:max-w-md"> <div ui-sheet-header class="border-border space-y-2 border-b pb-4"> <div class="flex items-center gap-2"> <span ui-badge [variant]="statusBadge().variant" [class]="statusBadge().class"> {{ statusBadge().label }} </span> @if (task.isMilestone) { <span ui-badge variant="outline" class="border-warning/30 text-warning"> Milestone </span> } </div> <h2 ui-sheet-title class="text-foreground text-lg font-semibold"> {{ task.name }} </h2> <p ui-sheet-description class="text-muted-foreground text-xs"> Task ID: <span class="font-mono">{{ task.id }}</span> </p> </div> <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"> <lucide-icon [img]="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> } <!-- 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> } </div> </div> } </div> `, }) export class UiRoadmapTaskDetailComponent { protected readonly Calendar = Calendar @Input() task: GanttTask | null = null @Input() open = false @Output() openChange = new EventEmitter<boolean>() readonly statusBadge = computed(() => { const status = this.task?.status if (!status) return { label: 'Active', variant: 'secondary' as const, class: undefined as string | undefined } switch (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 } } }) }src/app/components/blocks/project-roadmap/roadmap-add-task-dialog.component.ts7.3 kB
// Add-task modal dialog for the project roadmap: name, date range, milestone toggle, // initial status, and assignee. Emits `addTask`. Port of the Vue RoadmapAddTaskDialog 1:1. import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, signal } from '@angular/core' import { LucideAngularModule, Plus } from 'lucide-angular' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCheckboxComponent } from '@/ui/checkbox/checkbox.component' import { UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, } from '@/ui/dialog/dialog.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiLabelComponent } from '@/ui/label/label.component' import { UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, } from '@/ui/select/select.component' import type { GanttTask } from '@/ui/gantt/types' @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-roadmap-add-task-dialog, [ui-roadmap-add-task-dialog]', standalone: true, host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiButtonComponent, UiCheckboxComponent, UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, UiInputComponent, UiLabelComponent, UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, ], template: ` <ui-dialog data-slot="project-roadmap" [open]="open" (openChange)="openChange.emit($event)"> <ui-dialog-content class="max-w-[480px] sm:max-w-full"> <ui-dialog-header> <h2 ui-dialog-title>Add Roadmap Task</h2> <p ui-dialog-description> Create a new scheduled deliverable or critical milestone in the project Gantt timeline. </p> </ui-dialog-header> <form (submit)="handleSubmit($event)" class="space-y-4 py-2"> <!-- Task Name --> <div class="space-y-1.5"> <label ui-label for="roadmap-task-name">Task Name</label> <ui-input id="roadmap-task-name" [value]="name()" (valueChange)="name.set($event)" placeholder="e.g. Design Token Hierarchy & Specs" /> </div> <!-- Date Range --> <div class="grid grid-cols-2 gap-3"> <div class="space-y-1.5"> <label ui-label for="roadmap-start-date">Start Date</label> <ui-input id="roadmap-start-date" [value]="startDate()" (valueChange)="startDate.set($event)" type="date" /> </div> <div class="space-y-1.5"> <label ui-label for="roadmap-end-date">End Date</label> <ui-input id="roadmap-end-date" [value]="endDate()" (valueChange)="endDate.set($event)" type="date" [disabled]="isMilestone()" /> </div> </div> <!-- Milestone toggle --> <div class="border-border/80 bg-muted/20 flex items-center space-x-2 rounded-md border p-3"> <ui-checkbox id="roadmap-milestone-toggle" [checked]="isMilestone()" (checkedChange)="isMilestone.set(!!$event)" /> <div class="grid gap-0.5 leading-none"> <label for="roadmap-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 ui-label>Initial Status</label> <ui-select [value]="status()" (valueChange)="status.set($any($event))"> <button ui-select-trigger> <ui-select-value placeholder="Status" /> </button> <ui-select-content> <ui-select-item value="todo">To Do</ui-select-item> <ui-select-item value="in-progress">In Progress</ui-select-item> <ui-select-item value="done">Completed</ui-select-item> </ui-select-content> </ui-select> </div> <div class="space-y-1.5"> <label ui-label>Assignee</label> <ui-select [value]="assigneeName()" (valueChange)="assigneeName.set($event)"> <button ui-select-trigger> <ui-select-value placeholder="Assignee" /> </button> <ui-select-content> <ui-select-item value="Marcus Rivera">Marcus Rivera</ui-select-item> <ui-select-item value="Sarah Connor">Sarah Connor</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> </div> <div ui-dialog-footer class="pt-4"> <button ui-button variant="outline" type="button" (click)="openChange.emit(false)">Cancel</button> <button ui-button type="submit"> <lucide-icon [img]="Plus" class="mr-1.5 size-3.5" /> Create Task </button> </div> </form> </ui-dialog-content> </ui-dialog> `, }) export class UiRoadmapAddTaskDialogComponent { protected readonly Plus = Plus @Input() open = false @Output() openChange = new EventEmitter<boolean>() @Output() addTask = new EventEmitter<GanttTask>() readonly name = signal('') readonly startDate = signal('2026-08-20') readonly endDate = signal('2026-08-28') readonly status = signal<'todo' | 'in-progress' | 'done'>('todo') readonly progress = signal(0) readonly isMilestone = signal(false) readonly assigneeName = signal('Marcus Rivera') handleSubmit(event: Event): void { event.preventDefault() const trimmed = this.name().trim() if (!trimmed) return const assignee = this.assigneeName() const newTask: GanttTask = { id: `task-${Date.now()}`, name: trimmed, startDate: this.startDate(), endDate: this.isMilestone() ? this.startDate() : this.endDate(), status: this.status(), progress: this.isMilestone() ? 100 : Number(this.progress()) || 0, isMilestone: this.isMilestone(), assignee: { name: assignee, initials: assignee .split(' ') .map((p) => p[0]) .join(''), }, } this.addTask.emit(newTask) this.openChange.emit(false) // Reset form this.name.set('') this.progress.set(0) this.isMilestone.set(false) } }src/app/components/blocks/project-roadmap/types.ts0.2 kB
import type { GanttTask } from '@/ui/gantt/types' export interface RoadmapProject { id: string name: string description?: string tasks: GanttTask[] }src/app/components/blocks/project-roadmap/index.ts0.3 kB
export { UiProjectRoadmapComponent } from './project-roadmap.component' export { UiRoadmapAddTaskDialogComponent } from './roadmap-add-task-dialog.component' export { UiRoadmapMetricsComponent } from './roadmap-metrics.component' export { UiRoadmapTaskDetailComponent } from './roadmap-task-detail.component' export type { RoadmapProject } from './types'
Raw manifest:https://uipkge.dev/r/angular/project-roadmap.json