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 Vue ->Installation
$ pnpm dlx shadcn@latest add https://uipkge.dev/r/react/gantt.json $ npx shadcn@latest add https://uipkge.dev/r/react/gantt.json $ yarn dlx shadcn@latest add https://uipkge.dev/r/react/gantt.json $ bunx shadcn@latest add https://uipkge.dev/r/react/gantt.json Named registry:
npx shadcn@latest add @uipkge-react/gantt Installs to: components/ui/gantt/ Examples
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
tasks | GanttTask[] | — | required |
scale | GanttScale | — | optional |
onScaleChange | (scale: GanttScale) => void | — | optional |
startDate | string | — | optional |
endDate | string | — | optional |
rowHeight | number | — | optional |
headerHeight | number | — | optional |
treeWidth | number | — | optional |
onTaskClick | (task: GanttTask) => void | — | optional |
onTaskChange | (task: GanttTask) => void | — | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
GanttContextValue interface GanttContextValue {
scale: GanttScale
setScale: (scale: GanttScale) => void
startDate: Date
endDate: Date
totalDays: number
columnWidth: number
rowHeight: number
headerHeight: number
treeWidth: number
tasks: GanttTask[]
onTaskClick?: (task: GanttTask) => void
onTaskChange?: (task: GanttTask) => void
} 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)
-
components/ui/gantt/Gantt.tsx 4.2 kB
'use client' import * as React from 'react' import { cn } from '@/lib/utils' import type { GanttScale, GanttTask } from './types' export interface GanttContextValue { scale: GanttScale setScale: (scale: GanttScale) => void startDate: Date endDate: Date totalDays: number columnWidth: number rowHeight: number headerHeight: number treeWidth: number tasks: GanttTask[] onTaskClick?: (task: GanttTask) => void onTaskChange?: (task: GanttTask) => void } export const GanttContext = React.createContext<GanttContextValue | null>(null) export function useGantt() { const context = React.useContext(GanttContext) if (!context) { throw new Error('useGantt must be used within a <Gantt /> component') } return context } export interface GanttProps extends React.HTMLAttributes<HTMLDivElement> { tasks: GanttTask[] scale?: GanttScale onScaleChange?: (scale: GanttScale) => void startDate?: string endDate?: string rowHeight?: number headerHeight?: number treeWidth?: number onTaskClick?: (task: GanttTask) => void onTaskChange?: (task: GanttTask) => void } export const Gantt = React.forwardRef<HTMLDivElement, GanttProps>( ( { className, tasks, scale: controlledScale, onScaleChange, startDate, endDate, rowHeight = 40, headerHeight = 48, treeWidth = 280, onTaskClick, onTaskChange, children, ...props }, ref, ) => { const [internalScale, setInternalScale] = React.useState<GanttScale>(controlledScale ?? 'day') const currentScale = controlledScale ?? internalScale const handleSetScale = React.useCallback( (newScale: GanttScale) => { setInternalScale(newScale) onScaleChange?.(newScale) }, [onScaleChange], ) const resolvedStartDate = React.useMemo(() => { if (startDate) return new Date(startDate) if (tasks.length === 0) return new Date() const dates = tasks.map((t) => new Date(t.startDate).getTime()) const min = Math.min(...dates) const d = new Date(min) d.setDate(d.getDate() - 3) return d }, [startDate, tasks]) const resolvedEndDate = React.useMemo(() => { if (endDate) return new Date(endDate) if (tasks.length === 0) { const d = new Date() d.setDate(d.getDate() + 30) return d } const dates = tasks.map((t) => new Date(t.endDate).getTime()) const max = Math.max(...dates) const d = new Date(max) d.setDate(d.getDate() + 7) return d }, [endDate, tasks]) const totalDays = React.useMemo(() => { const diff = resolvedEndDate.getTime() - resolvedStartDate.getTime() return Math.max(1, Math.ceil(diff / (1000 * 60 * 60 * 24))) }, [resolvedStartDate, resolvedEndDate]) const columnWidth = React.useMemo(() => { switch (currentScale) { case 'day': return 44 case 'week': return 120 case 'month': return 180 case 'year': return 240 default: return 44 } }, [currentScale]) const contextValue = React.useMemo<GanttContextValue>( () => ({ scale: currentScale, setScale: handleSetScale, startDate: resolvedStartDate, endDate: resolvedEndDate, totalDays, columnWidth, rowHeight, headerHeight, treeWidth, tasks, onTaskClick, onTaskChange, }), [ currentScale, handleSetScale, resolvedStartDate, resolvedEndDate, totalDays, columnWidth, rowHeight, headerHeight, treeWidth, tasks, onTaskClick, onTaskChange, ], ) return ( <GanttContext.Provider value={contextValue}> <div ref={ref} data-uipkge="" data-slot="gantt" className={cn( 'border-border bg-card text-card-foreground relative flex w-full flex-col overflow-hidden rounded-xl border shadow-xs', className, )} {...props} > {children} </div> </GanttContext.Provider> ) }, ) Gantt.displayName = 'Gantt' -
components/ui/gantt/GanttHeader.tsx 1.9 kB
'use client' import * as React from 'react' import { Calendar } from 'lucide-react' import { cn } from '@/lib/utils' import { Button, ButtonGroup } from '@/components/ui/button' import { useGantt } from './Gantt' export interface GanttHeaderProps extends React.HTMLAttributes<HTMLDivElement> { title?: string showScaleSwitcher?: boolean actions?: React.ReactNode } export const GanttHeader = React.forwardRef<HTMLDivElement, GanttHeaderProps>( ({ className, title = 'Project Timeline', showScaleSwitcher = true, actions, children, ...props }, ref) => { const { scale, setScale } = useGantt() return ( <div ref={ref} data-uipkge="" data-slot="gantt-header" className={cn('border-border bg-muted/30 flex items-center justify-between border-b px-4 py-2.5', className)} {...props} > <div className="flex items-center gap-2"> <Calendar className="text-primary size-4" /> <span className="text-foreground text-sm font-semibold">{title}</span> </div> <div className="flex items-center gap-3"> {actions} {children} {showScaleSwitcher ? ( <ButtonGroup> <Button size="xs" variant={scale === 'day' ? 'default' : 'outline'} onClick={() => setScale('day')}> Day </Button> <Button size="xs" variant={scale === 'week' ? 'default' : 'outline'} onClick={() => setScale('week')}> Week </Button> <Button size="xs" variant={scale === 'month' ? 'default' : 'outline'} onClick={() => setScale('month')}> Month </Button> <Button size="xs" variant={scale === 'year' ? 'default' : 'outline'} onClick={() => setScale('year')}> Year </Button> </ButtonGroup> ) : null} </div> </div> ) }, ) GanttHeader.displayName = 'GanttHeader' -
components/ui/gantt/GanttTree.tsx 4 kB
'use client' import * as React from 'react' import { ChevronRight, ChevronDown, Flag } from 'lucide-react' import { cn } from '@/lib/utils' import { useGantt } from './Gantt' import type { GanttTask } from './types' export interface GanttTreeProps extends React.HTMLAttributes<HTMLDivElement> { showAssignee?: boolean showPriority?: boolean onTaskClick?: (task: GanttTask) => void } 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` } export const GanttTree = React.forwardRef<HTMLDivElement, GanttTreeProps>( ({ className, showAssignee = true, showPriority = true, onTaskClick: propOnTaskClick, ...props }, ref) => { const { treeWidth, headerHeight, rowHeight, tasks, onTaskClick: ctxOnTaskClick } = useGantt() const handleTaskClick = propOnTaskClick ?? ctxOnTaskClick return ( <div ref={ref} data-uipkge="" data-slot="gantt-tree" style={{ width: `${treeWidth}px` }} className={cn( 'border-border bg-card flex shrink-0 flex-col border-r transition-[width] select-none', className, )} {...props} > <div style={{ height: `${headerHeight}px` }} className="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 className="flex-1 truncate">Deliverable</span> {showPriority ? <span className="w-12 shrink-0 text-center">Pri</span> : null} <span className="w-16 shrink-0 text-right">Duration</span> </div> <div className="divide-border/40 flex-1 divide-y overflow-y-auto"> {tasks.map((task) => ( <div key={task.id} style={{ height: `${rowHeight}px` }} className={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', )} onClick={() => handleTaskClick?.(task)} > <div className="flex min-w-0 flex-1 items-center gap-1.5 pr-2"> {task.parentId ? <span className="w-4 shrink-0" /> : null} {task.status ? ( <span className={cn( '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', )} /> ) : null} <span className="truncate font-medium">{task.name}</span> </div> {showPriority ? ( <div className="flex w-12 shrink-0 items-center justify-center"> {task.priority ? <Flag className={cn('size-3', priorityColors[task.priority])} /> : null} </div> ) : null} <div className="text-muted-foreground w-16 shrink-0 text-right font-mono text-[11px]"> {task.isMilestone ? ( <span className="text-[10px] font-semibold text-amber-500">Milestone</span> ) : ( <span>{calculateDays(task.startDate, task.endDate)}</span> )} </div> </div> ))} </div> </div> ) }, ) GanttTree.displayName = 'GanttTree' -
components/ui/gantt/GanttTimeline.tsx 8 kB
'use client' import * as React from 'react' import { cn } from '@/lib/utils' import { useGantt } from './Gantt' import { GanttBar } from './GanttBar' import { GanttMilestone } from './GanttMilestone' import type { GanttTask } from './types' export interface GanttTimelineProps extends React.HTMLAttributes<HTMLDivElement> { showTodayLine?: boolean showDependencies?: boolean onTaskClick?: (task: GanttTask) => void } export const GanttTimeline = React.forwardRef<HTMLDivElement, GanttTimelineProps>( ({ className, showTodayLine = true, showDependencies = true, onTaskClick: propOnTaskClick, ...props }, ref) => { const { startDate, totalDays, columnWidth, headerHeight, rowHeight, tasks, onTaskClick: ctxOnTaskClick, } = useGantt() const handleTaskClick = propOnTaskClick ?? ctxOnTaskClick const columns = React.useMemo(() => { const list: { date: Date; label: string; subLabel: string; isWeekend: boolean }[] = [] const start = new Date(startDate) for (let i = 0; i < totalDays; 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 }, [startDate, totalDays]) const timelineWidth = columns.length * columnWidth const getTaskCoordinates = React.useCallback( (task: GanttTask, index: number) => { const start = new Date(startDate).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 * columnWidth const width = durationDays * columnWidth const top = index * rowHeight + (rowHeight - 28) / 2 return { left, width, top, height: 28 } }, [startDate, columnWidth, rowHeight], ) const todayPosition = React.useMemo(() => { const start = new Date(startDate).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 > totalDays) return null return diffDays * columnWidth + columnWidth / 2 }, [startDate, totalDays, columnWidth]) const dependencyPaths = React.useMemo(() => { if (!showDependencies) return [] const taskMap = new Map<string, { task: GanttTask; index: number }>() tasks.forEach((t, i) => taskMap.set(t.id, { task: t, index: i })) const paths: { d: string; fromId: string; toId: string }[] = [] tasks.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 }, [showDependencies, tasks, getTaskCoordinates]) return ( <div ref={ref} data-uipkge="" data-slot="gantt-timeline" className={cn('bg-background relative flex-1 overflow-x-auto overflow-y-hidden select-none', className)} {...props} > <div style={{ width: `${timelineWidth}px` }} className="relative"> <div style={{ height: `${headerHeight}px` }} className="border-border bg-muted/10 sticky top-0 z-20 flex border-b" > {columns.map((col, i) => ( <div key={i} style={{ width: `${columnWidth}px` }} className={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 className="text-foreground font-medium">{col.label}</span> <span className="text-[9px]">{col.subLabel}</span> </div> ))} </div> <div className="relative"> <div className="pointer-events-none absolute inset-0 flex"> {columns.map((col, i) => ( <div key={i} style={{ width: `${columnWidth}px` }} className={cn('border-border/30 h-full border-r', col.isWeekend && 'bg-muted/15')} /> ))} </div> {showTodayLine && todayPosition != null ? ( <div style={{ left: `${todayPosition}px` }} className="pointer-events-none absolute inset-y-0 z-30 flex flex-col items-center" > <div className="bg-destructive text-destructive-foreground rounded-full px-1.5 py-0.5 text-[9px] font-bold shadow-xs"> Today </div> <div className="bg-destructive/60 h-full w-[1.5px] border-r border-dashed" /> </div> ) : null} {dependencyPaths.length > 0 ? ( <svg width={timelineWidth} height={tasks.length * rowHeight} className="pointer-events-none absolute inset-0 z-10" > <defs> <marker id="gantt-arrow-react" 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" className="fill-primary/60" /> </marker> </defs> {dependencyPaths.map((p, i) => ( <path key={i} d={p.d} fill="none" className="stroke-primary/50" strokeWidth="1.5" strokeDasharray="3,3" markerEnd="url(#gantt-arrow-react)" /> ))} </svg> ) : null} {tasks.map((task, idx) => { const coords = getTaskCoordinates(task, idx) return ( <div key={task.id} style={{ height: `${rowHeight}px` }} className="border-border/40 hover:bg-muted/10 relative border-b transition-colors" > {task.isMilestone ? ( <GanttMilestone task={task} left={coords.left} top={rowHeight / 2} onTaskClick={handleTaskClick} /> ) : ( <GanttBar task={task} left={coords.left} width={coords.width} top={(coords.height - 28) / 2 + 6} height={28} onTaskClick={handleTaskClick} /> )} </div> ) })} </div> </div> </div> ) }, ) GanttTimeline.displayName = 'GanttTimeline' -
components/ui/gantt/GanttBar.tsx 3.6 kB
'use client' import * as React from 'react' import { cn } from '@/lib/utils' import type { GanttTask } from './types' export interface GanttBarProps extends React.HTMLAttributes<HTMLDivElement> { task: GanttTask left: number width: number top: number height: number onTaskClick?: (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', } export const GanttBar = React.forwardRef<HTMLDivElement, GanttBarProps>( ({ className, task, left, width, top, height, onTaskClick, ...props }, ref) => { if (task.isGroup) { return ( <div ref={ref} 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`, }} className={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', className, )} onClick={() => onTaskClick?.(task)} {...props} > <span className="truncate">{task.name}</span> {task.progress != null ? <span className="font-mono text-[10px] opacity-80">{task.progress}%</span> : null} </div> ) } return ( <div ref={ref} data-uipkge="" data-slot="gantt-bar" style={{ left: `${left}px`, width: `${Math.max(24, width)}px`, top: `${top}px`, height: `${height}px`, }} className={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'], className, )} onClick={() => onTaskClick?.(task)} {...props} > {task.progress != null && task.progress > 0 ? ( <div style={{ width: `${task.progress}%` }} className={cn('absolute inset-y-0 left-0 transition-all', progressColors[task.status ?? 'in-progress'])} /> ) : null} <div className="relative z-10 flex w-full min-w-0 items-center justify-between px-2"> <span className="truncate font-medium">{task.name}</span> {task.progress != null ? ( <span className="ml-1 shrink-0 font-mono text-[10px] opacity-80">{task.progress}%</span> ) : null} </div> <div aria-hidden="true" className="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" className="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> ) }, ) GanttBar.displayName = 'GanttBar' -
components/ui/gantt/GanttMilestone.tsx 1.1 kB
'use client' import * as React from 'react' import { cn } from '@/lib/utils' import type { GanttTask } from './types' export interface GanttMilestoneProps extends React.HTMLAttributes<HTMLDivElement> { task: GanttTask left: number top: number size?: number onTaskClick?: (task: GanttTask) => void } export const GanttMilestone = React.forwardRef<HTMLDivElement, GanttMilestoneProps>( ({ className, task, left, top, size = 16, onTaskClick, ...props }, ref) => { return ( <div ref={ref} data-uipkge="" data-slot="gantt-milestone" style={{ left: `${left - size / 2}px`, top: `${top - size / 2}px`, width: `${size}px`, height: `${size}px`, }} title={`${task.name} (${task.startDate})`} className={cn( 'border-primary bg-primary absolute z-20 rotate-45 cursor-pointer rounded-xs border-2 shadow-sm transition-transform hover:scale-125', className, )} onClick={() => onTaskClick?.(task)} {...props} /> ) }, ) GanttMilestone.displayName = 'GanttMilestone' -
components/ui/gantt/GanttContextMenu.tsx 5.6 kB
'use client' import * as React from 'react' import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, } from '@/components/ui/context-menu' import { Edit2, Copy, Trash2, Clock, Flag, Layers } from 'lucide-react' import type { GanttTask, GanttTaskStatus, GanttTaskPriority } from './types' export interface GanttContextMenuProps { task: GanttTask onEdit?: (task: GanttTask) => void onStatusChange?: (task: GanttTask, status: GanttTaskStatus) => void onPriorityChange?: (task: GanttTask, priority: GanttTaskPriority) => void onDuplicate?: (task: GanttTask) => void onDelete?: (task: GanttTask) => void children: React.ReactNode } export function GanttContextMenu({ task, onEdit, onStatusChange, onPriorityChange, onDuplicate, onDelete, children, }: GanttContextMenuProps) { const copyTaskId = () => { if (typeof navigator !== 'undefined' && navigator.clipboard) { navigator.clipboard.writeText(task.id) } } return ( <ContextMenu> <ContextMenuTrigger asChild>{children}</ContextMenuTrigger> <ContextMenuContent className="w-56"> <ContextMenuLabel className="flex items-center justify-between text-xs"> <span className="truncate font-semibold">{task.name}</span> <span className="text-muted-foreground font-mono text-[10px]">{task.id}</span> </ContextMenuLabel> <ContextMenuSeparator /> <ContextMenuItem onClick={() => onEdit?.(task)}> <Edit2 className="mr-2 size-3.5" /> <span>View Details</span> <ContextMenuShortcut>↵</ContextMenuShortcut> </ContextMenuItem> <ContextMenuSub> <ContextMenuSubTrigger> <Clock className="text-primary mr-2 size-3.5" /> <span>Change Status</span> </ContextMenuSubTrigger> <ContextMenuSubContent className="w-44"> <ContextMenuRadioGroup value={task.status ?? 'todo'}> <ContextMenuRadioItem value="done" onClick={() => onStatusChange?.(task, 'done')}> <span className="mr-2 size-2 rounded-full bg-emerald-500" /> <span>Completed</span> </ContextMenuRadioItem> <ContextMenuRadioItem value="in-progress" onClick={() => onStatusChange?.(task, 'in-progress')}> <span className="bg-primary mr-2 size-2 rounded-full" /> <span>In Progress</span> </ContextMenuRadioItem> <ContextMenuRadioItem value="at-risk" onClick={() => onStatusChange?.(task, 'at-risk')}> <span className="mr-2 size-2 rounded-full bg-amber-500" /> <span>At Risk</span> </ContextMenuRadioItem> <ContextMenuRadioItem value="blocked" onClick={() => onStatusChange?.(task, 'blocked')}> <span className="bg-destructive mr-2 size-2 rounded-full" /> <span>Blocked</span> </ContextMenuRadioItem> <ContextMenuRadioItem value="todo" onClick={() => onStatusChange?.(task, 'todo')}> <span className="bg-muted-foreground/40 mr-2 size-2 rounded-full" /> <span>To Do</span> </ContextMenuRadioItem> </ContextMenuRadioGroup> </ContextMenuSubContent> </ContextMenuSub> <ContextMenuSub> <ContextMenuSubTrigger> <Flag className="mr-2 size-3.5 text-amber-500" /> <span>Set Priority</span> </ContextMenuSubTrigger> <ContextMenuSubContent className="w-40"> <ContextMenuRadioGroup value={task.priority ?? 'medium'}> <ContextMenuRadioItem value="urgent" onClick={() => onPriorityChange?.(task, 'urgent')}> <Flag className="text-destructive mr-2 size-3" /> <span>Urgent</span> </ContextMenuRadioItem> <ContextMenuRadioItem value="high" onClick={() => onPriorityChange?.(task, 'high')}> <Flag className="mr-2 size-3 text-amber-500" /> <span>High</span> </ContextMenuRadioItem> <ContextMenuRadioItem value="medium" onClick={() => onPriorityChange?.(task, 'medium')}> <Flag className="text-primary mr-2 size-3" /> <span>Medium</span> </ContextMenuRadioItem> <ContextMenuRadioItem value="low" onClick={() => onPriorityChange?.(task, 'low')}> <Flag className="text-muted-foreground mr-2 size-3" /> <span>Low</span> </ContextMenuRadioItem> </ContextMenuRadioGroup> </ContextMenuSubContent> </ContextMenuSub> <ContextMenuSeparator /> <ContextMenuItem onClick={copyTaskId}> <Copy className="mr-2 size-3.5" /> <span>Copy Task ID</span> <ContextMenuShortcut>⌘C</ContextMenuShortcut> </ContextMenuItem> <ContextMenuItem onClick={() => onDuplicate?.(task)}> <Layers className="mr-2 size-3.5" /> <span>Duplicate</span> <ContextMenuShortcut>⌘D</ContextMenuShortcut> </ContextMenuItem> <ContextMenuSeparator /> <ContextMenuItem className="text-destructive focus:text-destructive" onClick={() => onDelete?.(task)}> <Trash2 className="mr-2 size-3.5" /> <span>Delete Deliverable</span> <ContextMenuShortcut>⌫</ContextMenuShortcut> </ContextMenuItem> </ContextMenuContent> </ContextMenu> ) } -
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 } -
components/ui/gantt/index.ts 0.5 kB
export { Gantt, useGantt, type GanttProps, type GanttContextValue } from './Gantt' export { GanttHeader, type GanttHeaderProps } from './GanttHeader' export { GanttTree, type GanttTreeProps } from './GanttTree' export { GanttTimeline, type GanttTimelineProps } from './GanttTimeline' export { GanttBar, type GanttBarProps } from './GanttBar' export { GanttMilestone, type GanttMilestoneProps } from './GanttMilestone' export { GanttContextMenu, type GanttContextMenuProps } from './GanttContextMenu' export * from './types'
Raw manifest: https://uipkge.dev/r/react/gantt.json