Gantt
gantt ui Interactive Gantt chart primitive with multi-scale timeline (day/week/month/year), collapsible task tree, progress fill, milestone markers, dependency lines, and right-click GanttContextMenu.
Also available for React ->Installation
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/gantt.json $ npx shadcn-vue@latest add https://uipkge.dev/r/vue/gantt.json $ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/gantt.json $ bunx shadcn-vue@latest add https://uipkge.dev/r/vue/gantt.json Named registry:
npx shadcn-vue@latest add @uipkge/gantt Installs to: app/components/ui/gantt/ Examples
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
tasks | GanttTask[] | — | required |
scale | GanttScale | 'day' | optional |
startDate | string | — | optional |
endDate | string | — | optional |
rowHeight | number | 40 | optional |
headerHeight | number | 48 | optional |
treeWidth | number | 280 | optional |
class | HTMLAttributes['class'] | — | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
GanttDependency interface GanttDependency {
fromId: string
toId: string
type?: 'finish-to-start' | 'start-to-start' | 'finish-to-finish'
} GanttAssignee interface GanttAssignee {
name: string
avatar?: string
initials?: string
role?: string
} GanttTask interface GanttTask {
id: string
name: string
startDate: string // YYYY-MM-DD
endDate: string // YYYY-MM-DD
progress?: number // 0 to 100
color?: string
status?: GanttTaskStatus
priority?: GanttTaskPriority
assignee?: GanttAssignee
isMilestone?: boolean
isGroup?: boolean
parentId?: string | null
isExpanded?: boolean
dependencies?: string[] // task IDs
children?: GanttTask[]
} GanttColumn interface GanttColumn {
key: string
label: string
width?: string
} npm dependencies
Includes
Used by
Files installed (9)
-
app/components/ui/gantt/Gantt.vue 2.6 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { computed, provide, ref, toRef } from 'vue' import { cn } from '@/lib/utils' import type { GanttScale, GanttTask } from './types' interface Props { tasks: GanttTask[] scale?: GanttScale startDate?: string endDate?: string rowHeight?: number headerHeight?: number treeWidth?: number class?: HTMLAttributes['class'] } const props = withDefaults(defineProps<Props>(), { scale: 'day', rowHeight: 40, headerHeight: 48, treeWidth: 280, }) const emits = defineEmits<{ (e: 'update:scale', scale: GanttScale): void (e: 'task-click', task: GanttTask): void (e: 'task-change', task: GanttTask): void }>() const currentScale = ref<GanttScale>(props.scale) // Compute start and end dates from tasks if not explicitly provided const resolvedStartDate = computed(() => { if (props.startDate) return new Date(props.startDate) if (props.tasks.length === 0) return new Date() const dates = props.tasks.map((t) => new Date(t.startDate).getTime()) const min = Math.min(...dates) const d = new Date(min) d.setDate(d.getDate() - 3) // padding return d }) const resolvedEndDate = computed(() => { if (props.endDate) return new Date(props.endDate) if (props.tasks.length === 0) { const d = new Date() d.setDate(d.getDate() + 30) return d } const dates = props.tasks.map((t) => new Date(t.endDate).getTime()) const max = Math.max(...dates) const d = new Date(max) d.setDate(d.getDate() + 7) // padding return d }) const totalDays = computed(() => { const diff = resolvedEndDate.value.getTime() - resolvedStartDate.value.getTime() return Math.max(1, Math.ceil(diff / (1000 * 60 * 60 * 24))) }) const columnWidth = computed(() => { switch (currentScale.value) { case 'day': return 44 case 'week': return 120 case 'month': return 180 case 'year': return 240 default: return 44 } }) provide('ganttContext', { scale: currentScale, startDate: resolvedStartDate, endDate: resolvedEndDate, totalDays, columnWidth, rowHeight: toRef(props, 'rowHeight'), headerHeight: toRef(props, 'headerHeight'), treeWidth: toRef(props, 'treeWidth'), tasks: toRef(props, 'tasks'), onTaskClick: (task: GanttTask) => emits('task-click', task), onTaskChange: (task: GanttTask) => emits('task-change', task), }) </script> <template> <div data-uipkge data-slot="gantt" :class=" cn( 'border-border bg-card text-card-foreground relative flex w-full flex-col overflow-hidden rounded-xl border shadow-xs', props.class, ) " > <slot /> </div> </template> -
app/components/ui/gantt/GanttHeader.vue 1.8 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { inject } from 'vue' import { Calendar, ChevronLeft, ChevronRight } from 'lucide-vue-next' import { cn } from '@/lib/utils' import { Button, ButtonGroup } from '@/components/ui/button' import type { GanttScale } from './types' interface Props { title?: string showScaleSwitcher?: boolean showNavigation?: boolean class?: HTMLAttributes['class'] } const props = withDefaults(defineProps<Props>(), { title: 'Project Timeline', showScaleSwitcher: true, showNavigation: false, }) const context = inject<any>('ganttContext') function setScale(scale: GanttScale) { if (context?.scale) { context.scale.value = scale } } </script> <template> <div data-uipkge data-slot="gantt-header" :class="cn('border-border bg-muted/30 flex items-center justify-between border-b px-4 py-2.5', props.class)" > <div class="flex items-center gap-2"> <Calendar class="text-primary size-4" /> <span class="text-foreground text-sm font-semibold">{{ title }}</span> </div> <div class="flex items-center gap-3"> <slot name="actions" /> <ButtonGroup v-if="showScaleSwitcher && context"> <Button size="xs" :variant="context.scale.value === 'day' ? 'default' : 'outline'" @click="setScale('day')"> Day </Button> <Button size="xs" :variant="context.scale.value === 'week' ? 'default' : 'outline'" @click="setScale('week')"> Week </Button> <Button size="xs" :variant="context.scale.value === 'month' ? 'default' : 'outline'" @click="setScale('month')"> Month </Button> <Button size="xs" :variant="context.scale.value === 'year' ? 'default' : 'outline'" @click="setScale('year')"> Year </Button> </ButtonGroup> </div> </div> </template> -
app/components/ui/gantt/GanttTree.vue 4 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { inject, ref } from 'vue' import { ChevronRight, ChevronDown, Flag, User } from 'lucide-vue-next' import { cn } from '@/lib/utils' import type { GanttTask } from './types' interface Props { showAssignee?: boolean showPriority?: boolean class?: HTMLAttributes['class'] } const props = withDefaults(defineProps<Props>(), { showAssignee: true, showPriority: true, }) const context = inject<any>('ganttContext') const priorityColors: Record<string, string> = { urgent: 'text-destructive', high: 'text-amber-500', medium: 'text-primary', low: 'text-muted-foreground/60', } function calculateDays(startDate: string, endDate: string) { const diff = new Date(endDate).getTime() - new Date(startDate).getTime() const days = Math.max(1, Math.ceil(diff / (1000 * 60 * 60 * 24))) return `${days}d` } </script> <template> <div data-uipkge data-slot="gantt-tree" :style="{ width: `${context?.treeWidth.value ?? 300}px` }" :class="cn('border-border bg-card flex shrink-0 flex-col border-r transition-[width] select-none', props.class)" > <!-- Column Headers --> <div :style="{ height: `${context?.headerHeight.value ?? 48}px` }" class="border-border bg-muted/20 text-muted-foreground flex items-center justify-between border-b px-3 text-[11px] font-semibold tracking-wider uppercase" > <span class="flex-1 truncate">Deliverable</span> <span v-if="showPriority" class="w-12 shrink-0 text-center">Pri</span> <span class="w-16 shrink-0 text-right">Duration</span> </div> <!-- Rows List --> <div class="divide-border/40 flex-1 divide-y overflow-y-auto"> <div v-for="task in context?.tasks.value ?? []" :key="task.id" :style="{ height: `${context?.rowHeight.value ?? 40}px` }" :class=" cn( 'group/row text-foreground hover:bg-muted/40 flex cursor-pointer items-center justify-between px-3 text-xs transition-colors', task.isGroup && 'bg-muted/10 font-semibold', ) " @click="context?.onTaskClick(task)" > <!-- Task Name & Chevron / Indent --> <div class="flex min-w-0 flex-1 items-center gap-1.5 pr-2"> <button v-if="task.isGroup || (task.children && task.children.length > 0)" type="button" class="text-muted-foreground hover:text-foreground flex size-4 shrink-0 items-center justify-center rounded-xs p-0.5" @click.stop="context?.toggleExpand?.(task.id)" > <ChevronDown v-if="task.isExpanded !== false" class="size-3" /> <ChevronRight v-else class="size-3" /> </button> <span v-else-if="task.parentId" class="w-4 shrink-0" /> <!-- Status Indicator Dot --> <span v-if="task.status" :class="[ 'size-2 shrink-0 rounded-full', task.status === 'done' && 'bg-emerald-500 ring-2 ring-emerald-500/20', task.status === 'in-progress' && 'bg-primary ring-primary/20 ring-2', task.status === 'at-risk' && 'bg-amber-500 ring-2 ring-amber-500/20', task.status === 'todo' && 'bg-muted-foreground/40', task.status === 'blocked' && 'bg-destructive ring-destructive/20 ring-2', ]" /> <span class="truncate font-medium">{{ task.name }}</span> </div> <!-- Priority Flag --> <div v-if="showPriority" class="flex w-12 shrink-0 items-center justify-center"> <Flag v-if="task.priority" :class="cn('size-3', priorityColors[task.priority])" /> </div> <!-- Duration / Due Date Tag --> <div class="text-muted-foreground w-16 shrink-0 text-right font-mono text-[11px]"> <span v-if="task.isMilestone" class="text-[10px] font-semibold text-amber-500"> Milestone </span> <span v-else> {{ calculateDays(task.startDate, task.endDate) }} </span> </div> </div> </div> </div> </template> -
app/components/ui/gantt/GanttTimeline.vue 7.4 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { computed, inject, ref } from 'vue' import { cn } from '@/lib/utils' import GanttBar from './GanttBar.vue' import GanttMilestone from './GanttMilestone.vue' import type { GanttTask } from './types' interface Props { showTodayLine?: boolean showDependencies?: boolean class?: HTMLAttributes['class'] } const props = withDefaults(defineProps<Props>(), { showTodayLine: true, showDependencies: true, }) const context = inject<any>('ganttContext') // Generate header date columns const columns = computed(() => { if (!context) return [] const list: { date: Date; label: string; subLabel: string; isWeekend: boolean }[] = [] const start = new Date(context.startDate.value) const total = context.totalDays.value for (let i = 0; i < total; i++) { const d = new Date(start) d.setDate(d.getDate() + i) const dayOfWeek = d.getDay() const isWeekend = dayOfWeek === 0 || dayOfWeek === 6 list.push({ date: d, label: d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }), subLabel: d.toLocaleDateString(undefined, { weekday: 'narrow' }), isWeekend, }) } return list }) const timelineWidth = computed(() => { if (!context) return 800 return columns.value.length * context.columnWidth.value }) function getTaskCoordinates(task: GanttTask, index: number) { if (!context) return { left: 0, width: 100, top: 0, height: 28 } const start = new Date(context.startDate.value).getTime() const taskStart = new Date(task.startDate).getTime() const taskEnd = new Date(task.endDate).getTime() const oneDay = 1000 * 60 * 60 * 24 const startDiffDays = Math.max(0, (taskStart - start) / oneDay) const durationDays = Math.max(1, (taskEnd - taskStart) / oneDay) const left = startDiffDays * context.columnWidth.value const width = durationDays * context.columnWidth.value const rowHeight = context.rowHeight.value const top = index * rowHeight + (rowHeight - 28) / 2 return { left, width, top, height: 28 } } const todayPosition = computed(() => { if (!context) return null const start = new Date(context.startDate.value).getTime() const today = new Date().setHours(0, 0, 0, 0) const oneDay = 1000 * 60 * 60 * 24 const diffDays = (today - start) / oneDay if (diffDays < 0 || diffDays > context.totalDays.value) return null return diffDays * context.columnWidth.value + context.columnWidth.value / 2 }) // Compute SVG Dependency curves between tasks const dependencyPaths = computed(() => { if (!context || !props.showDependencies) return [] const tasksList: GanttTask[] = context.tasks.value ?? [] const taskMap = new Map<string, { task: GanttTask; index: number }>() tasksList.forEach((t, i) => taskMap.set(t.id, { task: t, index: i })) const paths: { d: string; fromId: string; toId: string }[] = [] tasksList.forEach((toTask, toIdx) => { if (!toTask.dependencies || toTask.dependencies.length === 0) return toTask.dependencies.forEach((fromId) => { const fromEntry = taskMap.get(fromId) if (!fromEntry) return const fromCoords = getTaskCoordinates(fromEntry.task, fromEntry.index) const toCoords = getTaskCoordinates(toTask, toIdx) const startX = fromEntry.task.isMilestone ? fromCoords.left : fromCoords.left + fromCoords.width const startY = fromCoords.top + 14 const endX = toCoords.left const endY = toCoords.top + 14 const deltaX = Math.max(16, (endX - startX) / 2) const d = `M ${startX} ${startY} C ${startX + deltaX} ${startY}, ${endX - deltaX} ${endY}, ${endX} ${endY}` paths.push({ d, fromId, toId: toTask.id }) }) }) return paths }) </script> <template> <div data-uipkge data-slot="gantt-timeline" :class="cn('bg-background relative flex-1 overflow-x-auto overflow-y-hidden select-none', props.class)" > <div :style="{ width: `${timelineWidth}px` }" class="relative"> <!-- Header Dates --> <div :style="{ height: `${context?.headerHeight.value ?? 48}px` }" class="border-border bg-muted/10 sticky top-0 z-20 flex border-b" > <div v-for="(col, i) in columns" :key="i" :style="{ width: `${context?.columnWidth.value ?? 44}px` }" :class=" cn( 'border-border/50 text-muted-foreground flex flex-col items-center justify-center border-r text-[10px]', col.isWeekend && 'bg-muted/20 text-muted-foreground/60', ) " > <span class="text-foreground font-medium">{{ col.label }}</span> <span class="text-[9px]">{{ col.subLabel }}</span> </div> </div> <!-- Grid Background & Task Bars --> <div class="relative"> <!-- Column grid lines --> <div class="pointer-events-none absolute inset-0 flex"> <div v-for="(col, i) in columns" :key="i" :style="{ width: `${context?.columnWidth.value ?? 44}px` }" :class="cn('border-border/30 h-full border-r', col.isWeekend && 'bg-muted/15')" /> </div> <!-- Today Marker Line --> <div v-if="showTodayLine && todayPosition != null" :style="{ left: `${todayPosition}px` }" class="pointer-events-none absolute inset-y-0 z-30 flex flex-col items-center" > <div class="bg-destructive text-destructive-foreground rounded-full px-1.5 py-0.5 text-[9px] font-bold shadow-xs" > Today </div> <div class="bg-destructive/60 h-full w-[1.5px] border-r border-dashed" /> </div> <!-- SVG Dependencies Overlay --> <svg v-if="dependencyPaths.length > 0" :width="timelineWidth" :height="(context?.tasks.value.length ?? 0) * (context?.rowHeight.value ?? 40)" class="pointer-events-none absolute inset-0 z-10" > <defs> <marker id="gantt-arrow" viewBox="0 0 6 6" refX="5" refY="3" markerWidth="6" markerHeight="6" orient="auto"> <path d="M 0 0 L 6 3 L 0 6 z" class="fill-primary/60" /> </marker> </defs> <path v-for="(p, i) in dependencyPaths" :key="i" :d="p.d" fill="none" class="stroke-primary/50" stroke-width="1.5" stroke-dasharray="3,3" marker-end="url(#gantt-arrow)" /> </svg> <!-- Task Rows & Bars --> <div v-for="(task, idx) in context?.tasks.value ?? []" :key="task.id" :style="{ height: `${context?.rowHeight.value ?? 40}px` }" class="border-border/40 hover:bg-muted/10 relative border-b transition-colors" > <template v-if="task.isMilestone"> <GanttMilestone :task="task" :left="getTaskCoordinates(task, idx).left" :top="(context?.rowHeight.value ?? 40) / 2" @click="context?.onTaskClick(task)" /> </template> <template v-else> <GanttBar :task="task" :left="getTaskCoordinates(task, idx).left" :width="getTaskCoordinates(task, idx).width" :top="(getTaskCoordinates(task, idx).height - 28) / 2 + 6" :height="28" @click="context?.onTaskClick(task)" /> </template> </div> </div> </div> </div> </template> -
app/components/ui/gantt/GanttBar.vue 3.3 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { computed } from 'vue' import { cn } from '@/lib/utils' import type { GanttTask } from './types' interface Props { task: GanttTask left: number width: number top: number height: number class?: HTMLAttributes['class'] } const props = defineProps<Props>() const emits = defineEmits<{ (e: 'click', task: GanttTask): void }>() const statusColors: Record<string, string> = { done: 'bg-emerald-500/20 text-emerald-700 dark:text-emerald-300 border-emerald-500/40', 'in-progress': 'bg-primary/20 text-primary border-primary/40', 'at-risk': 'bg-amber-500/20 text-amber-700 dark:text-amber-300 border-amber-500/40', todo: 'bg-muted/80 text-muted-foreground border-border', blocked: 'bg-destructive/20 text-destructive border-destructive/40', } const progressColors: Record<string, string> = { done: 'bg-emerald-500/40', 'in-progress': 'bg-primary/40', 'at-risk': 'bg-amber-500/40', todo: 'bg-muted-foreground/20', blocked: 'bg-destructive/40', } </script> <template> <!-- Group Parent Task Bracket Bar --> <div v-if="task.isGroup" data-uipkge data-slot="gantt-group-bar" :style="{ left: `${left}px`, width: `${Math.max(24, width)}px`, top: `${top + 4}px`, height: `${height - 8}px`, }" :class=" cn( 'group/bar bg-foreground/80 text-background hover:bg-foreground absolute z-10 flex cursor-pointer items-center justify-between rounded-xs px-2 text-xs font-semibold shadow-xs select-none', props.class, ) " @click="$emit('click', task)" > <span class="truncate">{{ task.name }}</span> <span v-if="task.progress != null" class="font-mono text-[10px] opacity-80"> {{ task.progress }}% </span> </div> <!-- Standard Deliverable Bar --> <div v-else data-uipkge data-slot="gantt-bar" :style="{ left: `${left}px`, width: `${Math.max(24, width)}px`, top: `${top}px`, height: `${height}px`, }" :class=" cn( 'group/bar absolute z-10 flex cursor-pointer items-center overflow-hidden rounded-md border text-xs font-medium shadow-xs transition-[box-shadow,transform] select-none hover:scale-[1.01] hover:shadow-md', task.color ? task.color : statusColors[task.status ?? 'in-progress'], props.class, ) " @click="$emit('click', task)" > <!-- Progress fill --> <div v-if="task.progress != null && task.progress > 0" :style="{ width: `${task.progress}%` }" :class="cn('absolute inset-y-0 left-0 transition-all', progressColors[task.status ?? 'in-progress'])" /> <!-- Content --> <div class="relative z-10 flex w-full min-w-0 items-center justify-between px-2"> <span class="truncate font-medium">{{ task.name }}</span> <span v-if="task.progress != null" class="ml-1 shrink-0 font-mono text-[10px] opacity-80"> {{ task.progress }}% </span> </div> <!-- Left / Right Resize Handles --> <div aria-hidden="true" class="bg-foreground/20 absolute inset-y-0 left-0 w-1.5 cursor-ew-resize opacity-0 transition-opacity group-hover/bar:opacity-100" /> <div aria-hidden="true" class="bg-foreground/20 absolute inset-y-0 right-0 w-1.5 cursor-ew-resize opacity-0 transition-opacity group-hover/bar:opacity-100" /> </div> </template> -
app/components/ui/gantt/GanttMilestone.vue 0.9 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' import type { GanttTask } from './types' interface Props { task: GanttTask left: number top: number size?: number class?: HTMLAttributes['class'] } const props = withDefaults(defineProps<Props>(), { size: 16, }) const emits = defineEmits<{ (e: 'click', task: GanttTask): void }>() </script> <template> <div data-uipkge data-slot="gantt-milestone" :style="{ left: `${left - size / 2}px`, top: `${top - size / 2}px`, width: `${size}px`, height: `${size}px`, }" :class=" cn( 'border-primary bg-primary absolute z-20 rotate-45 cursor-pointer rounded-xs border-2 shadow-sm transition-transform hover:scale-125', props.class, ) " :title="`${task.name} (${task.startDate})`" @click="$emit('click', task)" /> </template> -
app/components/ui/gantt/GanttContextMenu.vue 5.4 kB
<script setup lang="ts"> import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, } from '@/components/ui/context-menu' import { Edit2, Copy, Trash2, Clock, CheckCircle2, AlertCircle, Flag, Calendar, Layers } from 'lucide-vue-next' import type { GanttTask, GanttTaskStatus, GanttTaskPriority } from './types' const props = defineProps<{ task: GanttTask }>() const emits = defineEmits<{ (e: 'edit', task: GanttTask): void (e: 'status-change', task: GanttTask, status: GanttTaskStatus): void (e: 'priority-change', task: GanttTask, priority: GanttTaskPriority): void (e: 'duplicate', task: GanttTask): void (e: 'delete', task: GanttTask): void }>() function copyTaskId() { if (typeof navigator !== 'undefined' && navigator.clipboard) { navigator.clipboard.writeText(props.task.id) } } </script> <template> <ContextMenu> <ContextMenuTrigger as-child> <slot /> </ContextMenuTrigger> <ContextMenuContent class="w-56"> <ContextMenuLabel class="flex items-center justify-between text-xs"> <span class="truncate font-semibold">{{ task.name }}</span> <span class="text-muted-foreground font-mono text-[10px]">{{ task.id }}</span> </ContextMenuLabel> <ContextMenuSeparator /> <!-- Edit / Inspect --> <ContextMenuItem @select="$emit('edit', task)"> <Edit2 class="mr-2 size-3.5" /> <span>View Details</span> <ContextMenuShortcut>↵</ContextMenuShortcut> </ContextMenuItem> <!-- Status Submenu --> <ContextMenuSub> <ContextMenuSubTrigger> <Clock class="text-primary mr-2 size-3.5" /> <span>Change Status</span> </ContextMenuSubTrigger> <ContextMenuSubContent class="w-44"> <ContextMenuRadioGroup :model-value="task.status ?? 'todo'"> <ContextMenuRadioItem value="done" @select="$emit('status-change', task, 'done')"> <span class="mr-2 size-2 rounded-full bg-emerald-500" /> <span>Completed</span> </ContextMenuRadioItem> <ContextMenuRadioItem value="in-progress" @select="$emit('status-change', task, 'in-progress')"> <span class="bg-primary mr-2 size-2 rounded-full" /> <span>In Progress</span> </ContextMenuRadioItem> <ContextMenuRadioItem value="at-risk" @select="$emit('status-change', task, 'at-risk')"> <span class="mr-2 size-2 rounded-full bg-amber-500" /> <span>At Risk</span> </ContextMenuRadioItem> <ContextMenuRadioItem value="blocked" @select="$emit('status-change', task, 'blocked')"> <span class="bg-destructive mr-2 size-2 rounded-full" /> <span>Blocked</span> </ContextMenuRadioItem> <ContextMenuRadioItem value="todo" @select="$emit('status-change', task, 'todo')"> <span class="bg-muted-foreground/40 mr-2 size-2 rounded-full" /> <span>To Do</span> </ContextMenuRadioItem> </ContextMenuRadioGroup> </ContextMenuSubContent> </ContextMenuSub> <!-- Priority Submenu --> <ContextMenuSub> <ContextMenuSubTrigger> <Flag class="mr-2 size-3.5 text-amber-500" /> <span>Set Priority</span> </ContextMenuSubTrigger> <ContextMenuSubContent class="w-40"> <ContextMenuRadioGroup :model-value="task.priority ?? 'medium'"> <ContextMenuRadioItem value="urgent" @select="$emit('priority-change', task, 'urgent')"> <Flag class="text-destructive mr-2 size-3" /> <span>Urgent</span> </ContextMenuRadioItem> <ContextMenuRadioItem value="high" @select="$emit('priority-change', task, 'high')"> <Flag class="mr-2 size-3 text-amber-500" /> <span>High</span> </ContextMenuRadioItem> <ContextMenuRadioItem value="medium" @select="$emit('priority-change', task, 'medium')"> <Flag class="text-primary mr-2 size-3" /> <span>Medium</span> </ContextMenuRadioItem> <ContextMenuRadioItem value="low" @select="$emit('priority-change', task, 'low')"> <Flag class="text-muted-foreground mr-2 size-3" /> <span>Low</span> </ContextMenuRadioItem> </ContextMenuRadioGroup> </ContextMenuSubContent> </ContextMenuSub> <ContextMenuSeparator /> <!-- Copy ID --> <ContextMenuItem @select="copyTaskId"> <Copy class="mr-2 size-3.5" /> <span>Copy Task ID</span> <ContextMenuShortcut>⌘C</ContextMenuShortcut> </ContextMenuItem> <!-- Duplicate --> <ContextMenuItem @select="$emit('duplicate', task)"> <Layers class="mr-2 size-3.5" /> <span>Duplicate</span> <ContextMenuShortcut>⌘D</ContextMenuShortcut> </ContextMenuItem> <ContextMenuSeparator /> <!-- Delete --> <ContextMenuItem class="text-destructive focus:text-destructive" @select="$emit('delete', task)"> <Trash2 class="mr-2 size-3.5" /> <span>Delete Deliverable</span> <ContextMenuShortcut>⌫</ContextMenuShortcut> </ContextMenuItem> </ContextMenuContent> </ContextMenu> </template> -
app/components/ui/gantt/types.ts 0.9 kB
export type GanttScale = 'day' | 'week' | 'month' | 'quarter' | 'year' export type GanttTaskStatus = 'todo' | 'in-progress' | 'done' | 'blocked' | 'at-risk' export type GanttTaskPriority = 'low' | 'medium' | 'high' | 'urgent' export interface GanttDependency { fromId: string toId: string type?: 'finish-to-start' | 'start-to-start' | 'finish-to-finish' } export interface GanttAssignee { name: string avatar?: string initials?: string role?: string } export interface GanttTask { id: string name: string startDate: string // YYYY-MM-DD endDate: string // YYYY-MM-DD progress?: number // 0 to 100 color?: string status?: GanttTaskStatus priority?: GanttTaskPriority assignee?: GanttAssignee isMilestone?: boolean isGroup?: boolean parentId?: string | null isExpanded?: boolean dependencies?: string[] // task IDs children?: GanttTask[] } export interface GanttColumn { key: string label: string width?: string } -
app/components/ui/gantt/index.ts 0.4 kB
export { default as Gantt } from './Gantt.vue' export { default as GanttHeader } from './GanttHeader.vue' export { default as GanttTree } from './GanttTree.vue' export { default as GanttTimeline } from './GanttTimeline.vue' export { default as GanttBar } from './GanttBar.vue' export { default as GanttMilestone } from './GanttMilestone.vue' export { default as GanttContextMenu } from './GanttContextMenu.vue' export * from './types'
Raw manifest: https://uipkge.dev/r/vue/gantt.json