UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

515 lines (512 loc) 20.7 kB
'use client'; import { jsx, jsxs, Fragment } from 'react/jsx-runtime'; import { cn } from '../../lib/utilsComprehensive.js'; import { forwardRef, useState, useRef, useMemo, useCallback } from 'react'; import { useMotionPreference } from '../../hooks/useMotionPreference.js'; import '../../primitives/GlassCore.js'; import '../../primitives/glass/GlassAdvanced.js'; import { OptimizedGlassCore } from '../../primitives/OptimizedGlassCore.js'; import '../../primitives/glass/OptimizedGlassAdvanced.js'; import '../../primitives/MotionNative.js'; import { MotionFramer } from '../../primitives/motion/MotionFramer.js'; import { useA11yId } from '../../utils/a11y.js'; import { useGlassSound } from '../../utils/soundDesign.js'; const GlassGanttChart = /*#__PURE__*/forwardRef(({ tasks, startDate, endDate, timeScale = { unit: "day", step: 1 }, viewOptions = { showWeekends: true, showToday: true, showProgress: true, showDependencies: true, showMilestones: true, showCriticalPath: false, showBaseline: false, showResources: true }, height = 600, rowHeight = 40, columnWidth = 40, editable = false, showHierarchy = true, onTaskClick, onTaskUpdate, onTaskResize, onTaskMove, onProgressUpdate, onDependencyCreate, onDependencyDelete, renderTask, renderTimelineHeader, respectMotionPreference = true, className, ...props }, ref) => { const { shouldAnimate } = useMotionPreference(); const { play } = useGlassSound(); const ganttId = useA11yId("glass-gantt-chart"); const [selectedTask, setSelectedTask] = useState(null); const [dragState, setDragState] = useState({ isDragging: false }); const [scrollPosition, setScrollPosition] = useState({ x: 0, y: 0 }); const [hoveredTask, setHoveredTask] = useState(null); const chartRef = useRef(null); const timelineRef = useRef(null); const taskListRef = useRef(null); // Calculate date range const dateRange = useMemo(() => { const taskDates = tasks.flatMap(task => [task.startDate, task.endDate]); const minDate = startDate || new Date(Math.min(...taskDates.map(d => d.getTime()))); const maxDate = endDate || new Date(Math.max(...taskDates.map(d => d.getTime()))); // Add some padding const paddedStart = new Date(minDate); paddedStart.setDate(paddedStart.getDate() - 7); const paddedEnd = new Date(maxDate); paddedEnd.setDate(paddedEnd.getDate() + 7); return { start: paddedStart, end: paddedEnd }; }, [tasks, startDate, endDate]); // Generate time columns const timeColumns = useMemo(() => { const columns = []; const current = new Date(dateRange.start); while (current <= dateRange.end) { columns.push(new Date(current)); switch (timeScale.unit) { case "day": current.setDate(current.getDate() + timeScale.step); break; case "week": current.setDate(current.getDate() + 7 * timeScale.step); break; case "month": current.setMonth(current.getMonth() + timeScale.step); break; case "quarter": current.setMonth(current.getMonth() + 3 * timeScale.step); break; case "year": current.setFullYear(current.getFullYear() + timeScale.step); break; } } return columns; }, [dateRange, timeScale]); // Calculate task positions const getTaskPosition = useCallback(task => { const totalDuration = dateRange.end.getTime() - dateRange.start.getTime(); const taskStart = task.startDate.getTime() - dateRange.start.getTime(); const taskDuration = task.endDate.getTime() - task.startDate.getTime(); const x = taskStart / totalDuration * (timeColumns.length * columnWidth); const width = taskDuration / totalDuration * (timeColumns.length * columnWidth); return { x, width: Math.max(width, 20) }; // Minimum width of 20px }, [dateRange, timeColumns.length, columnWidth]); // Status colors const statusColors = { "not-started": "bg-gray-400", "in-progress": "bg-blue-500", completed: "bg-green-500", blocked: "bg-red-500", cancelled: "bg-gray-600" }; // Priority colors const priorityColors = { low: "border-green-500/30", medium: "border-yellow-500/30", high: "border-orange-500/30", critical: "border-red-500/30" }; // Handle task drag start const handleTaskDragStart = useCallback((e, taskId, mode) => { if (!editable) return; const task = tasks.find(t => t.id === taskId); if (!task) return; setDragState({ isDragging: true, taskId, startX: e.clientX, startDate: task.startDate, mode }); play("drag_start"); }, [editable, tasks, play]); // Handle task drag const handleTaskDrag = useCallback(e => { if (!dragState.isDragging || !dragState.taskId || !dragState.startX || !dragState.startDate) return; const deltaX = e.clientX - dragState.startX; const totalDuration = dateRange.end.getTime() - dateRange.start.getTime(); const timePerPixel = totalDuration / (timeColumns.length * columnWidth); const deltaTime = deltaX * timePerPixel; const task = tasks.find(t => t.id === dragState.taskId); if (!task) return; if (dragState.mode === "move") { const newStartDate = new Date(dragState.startDate.getTime() + deltaTime); const taskDuration = task.endDate.getTime() - task.startDate.getTime(); const newEndDate = new Date(newStartDate.getTime() + taskDuration); onTaskMove?.(dragState.taskId, newStartDate, newEndDate); } else if (dragState.mode === "resize-end") { const newEndDate = new Date(task.endDate.getTime() + deltaTime); if (newEndDate > task.startDate) { onTaskResize?.(dragState.taskId, task.startDate, newEndDate); } } else if (dragState.mode === "resize-start") { const newStartDate = new Date(task.startDate.getTime() + deltaTime); if (newStartDate < task.endDate) { onTaskResize?.(dragState.taskId, newStartDate, task.endDate); } } }, [dragState, dateRange, timeColumns.length, columnWidth, tasks, onTaskMove, onTaskResize]); // Handle task drag end const handleTaskDragEnd = useCallback(() => { if (dragState.isDragging) { setDragState({ isDragging: false }); play("drag_end"); } }, [dragState.isDragging, play]); // Handle progress update const handleProgressUpdate = useCallback((taskId, e, progressBarWidth) => { if (!editable) return; const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; const progress = Math.max(0, Math.min(100, x / progressBarWidth * 100)); onProgressUpdate?.(taskId, Math.round(progress)); play("progress_update"); }, [editable, onProgressUpdate, play]); // Format date for header const formatHeaderDate = useCallback(date => { switch (timeScale.unit) { case "day": return date.getDate().toString(); case "week": return `W${Math.ceil(date.getDate() / 7)}`; case "month": return date.toLocaleDateString("default", { month: "short" }); case "quarter": return `Q${Math.ceil((date.getMonth() + 1) / 3)}`; case "year": return date.getFullYear().toString(); default: return date.toLocaleDateString(); } }, [timeScale.unit]); // Organize tasks by hierarchy const organizedTasks = useMemo(() => { if (!showHierarchy) return tasks; new Map(tasks.map(task => [task.id, task])); const roots = []; const children = new Map(); // Build hierarchy tasks.forEach(task => { if (task.parent) { const parentChildren = children.get(task.parent) || []; parentChildren.push(task); children.set(task.parent, parentChildren); } else { roots.push(task); } }); // Flatten with hierarchy const flattened = []; const addTasksRecursively = (taskList, level = 0) => { taskList.forEach(task => { flattened.push({ ...task, customData: { ...task.customData, level } }); const taskChildren = children.get(task.id); if (taskChildren) { addTasksRecursively(taskChildren, level + 1); } }); }; addTasksRecursively(roots); return flattened; }, [tasks, showHierarchy]); // Default task renderer const defaultRenderTask = useCallback((task, bounds) => { const isSelected = selectedTask === task.id; const isHovered = hoveredTask === task.id; const level = task.customData?.level || 0; if (task.milestone) { return jsx("div", { "data-glass-component": true, className: cn("absolute flex items-center justify-center cursor-pointer", "transform rotate-45 border-2", task.color ? `border-[${task.color}] bg-[${task.color}]/20` : "border-primary bg-primary/20", isSelected && "ring-2 ring-primary/50", isHovered && "scale-110"), style: { left: bounds.x, top: bounds.y + bounds.height / 4, width: bounds.height / 2, height: bounds.height / 2 }, onClick: () => onTaskClick?.(task), onMouseEnter: () => setHoveredTask(task.id), onMouseLeave: () => setHoveredTask(null), title: `${task.name} (Milestone)` }); } return jsx(OptimizedGlassCore, { elevation: "level2", intensity: "medium", depth: 1, tint: "neutral", border: "subtle", className: cn("absolute cursor-pointer transition-all duration-200", "glass-backdrop-blur-sm border border-border/20 glass-radius-md", task.priority && priorityColors[task.priority], isSelected && "ring-2 ring-primary/50", isHovered && "shadow-lg scale-[1.02]"), style: { left: bounds.x, top: bounds.y + 4, width: bounds.width, height: bounds.height - 8, marginLeft: showHierarchy ? level * 20 : 0 }, onClick: () => { setSelectedTask(task.id); onTaskClick?.(task); }, onMouseEnter: () => setHoveredTask(task.id), onMouseLeave: () => setHoveredTask(null), onMouseDown: e => handleTaskDragStart(e, task.id, "move"), children: jsxs("div", { className: 'relative glass-h-full glass-flex glass-items-center', children: [jsx("div", { className: cn("absolute left-0 top-0 h-full w-1 rounded-l", task.status ? statusColors[task.status] : statusColors["not-started"]) }), viewOptions.showProgress && task.progress > 0 && jsx("div", { className: 'absolute left-1 glass-top-1 bottom-1 glass-surface-primary/30 glass-radius-sm transition-all duration-300', style: { width: `${task.progress / 100 * (bounds.width - 8)}px` }, onClick: e => handleProgressUpdate(task.id, e, bounds.width - 8) }), jsxs("div", { className: "glass-flex-1 glass-px-3 glass-min-w-0", children: [jsx("div", { className: 'glass-text-sm font-medium text-primary truncate', children: task.name }), viewOptions.showResources && task.assignee && jsx("div", { className: 'glass-text-xs glass-text-secondary truncate', children: task.assignee.name })] }), viewOptions.showProgress && jsxs("div", { className: "glass-text-xs glass-text-secondary glass-px-2", children: [task.progress, "%"] }), editable && isSelected && jsxs(Fragment, { children: [jsx("div", { className: 'absolute left-0 top-0 bottom-0 w-1 cursor-ew-resize glass-surface-primary/20 hover:glass-surface-primary/40', onMouseDown: e => handleTaskDragStart(e, task.id, "resize-start") }), jsx("div", { className: 'absolute right-0 top-0 bottom-0 w-1 cursor-ew-resize glass-surface-primary/20 hover:glass-surface-primary/40', onMouseDown: e => handleTaskDragStart(e, task.id, "resize-end") })] })] }) }); }, [selectedTask, hoveredTask, priorityColors, statusColors, showHierarchy, viewOptions.showProgress, viewOptions.showResources, editable, onTaskClick, handleTaskDragStart, handleProgressUpdate]); // Handle scroll synchronization const handleScroll = useCallback(e => { const scrollLeft = e.currentTarget.scrollLeft; const scrollTop = e.currentTarget.scrollTop; setScrollPosition({ x: scrollLeft, y: scrollTop }); if (timelineRef.current) { timelineRef.current.scrollLeft = scrollLeft; } if (taskListRef.current) { taskListRef.current.scrollTop = scrollTop; } }, []); return jsx(OptimizedGlassCore, { ref: ref, id: ganttId, elevation: "level1", intensity: "subtle", depth: 1, tint: "neutral", border: "subtle", className: cn("glass-gantt-chart glass-radius-lg glass-backdrop-blur-md border border-border/20 overflow-hidden", className), style: { height }, onMouseMove: handleTaskDrag, onMouseUp: handleTaskDragEnd, onMouseLeave: handleTaskDragEnd, ...props, children: jsxs(MotionFramer, { preset: shouldAnimate && respectMotionPreference ? "fadeIn" : "none", className: "glass-h-full glass-flex glass-flex-col", children: [jsxs("div", { className: "glass-flex glass-border-b glass-border-glass-border/20", children: [jsx("div", { className: 'w-64 glass-p-4 glass-border-r glass-border-glass-border/20 glass-surface-overlay', children: jsx("h3", { className: 'glass-text-sm font-semibold text-primary', children: "Tasks" }) }), jsx("div", { ref: timelineRef, className: 'glass-flex-1 overflow-x-hidden glass-surface-overlay', style: { scrollbarWidth: "none", msOverflowStyle: "none" }, children: jsx("div", { className: 'glass-flex h-16', style: { width: timeColumns.length * columnWidth }, children: timeColumns.map((date, index) => { const isToday = viewOptions.showToday && date.toDateString() === new Date().toDateString(); const isWeekend = !viewOptions.showWeekends && (date.getDay() === 0 || date.getDay() === 6); return jsx("div", { className: cn("flex-shrink-0 border-r border-border/10 flex items-center justify-center glass-text-xs", isToday && "bg-primary/10 text-primary font-semibold", isWeekend && "bg-muted/20 glass-text-secondary"), style: { width: columnWidth }, children: renderTimelineHeader ? renderTimelineHeader(date, timeScale.unit) : formatHeaderDate(date) }, index); }) }) })] }), jsxs("div", { className: 'glass-flex glass-flex-1 overflow-hidden', children: [jsx("div", { ref: taskListRef, className: 'w-64 glass-border-r glass-border-glass-border/20 overflow-y-hidden glass-surface-overlay', children: jsx("div", { children: organizedTasks.map((task, index) => { const level = task.customData?.level || 0; return jsx(MotionFramer, { preset: shouldAnimate && respectMotionPreference ? "slideUp" : "none", delay: index * 50, children: jsxs("div", { className: cn("flex items-center glass-p-2 border-b border-border/10 cursor-pointer transition-colors", "hover:bg-background/40", selectedTask === task.id && "bg-primary/10 text-primary"), style: { height: rowHeight, paddingLeft: showHierarchy ? 8 + level * 16 : 8 }, onClick: () => { setSelectedTask(task.id); onTaskClick?.(task); }, children: [showHierarchy && task.children && task.children.length > 0 && jsx("button", { className: 'glass-mr-2 glass-text-secondary hover:text-primary', children: "\u25BC" }), jsxs("div", { className: "glass-flex-1 glass-min-w-0", children: [jsx("div", { className: 'glass-text-sm font-medium text-primary truncate', children: task.name }), task.assignee && jsx("div", { className: 'glass-text-xs glass-text-secondary truncate', children: task.assignee.name })] }), task.milestone && jsx("div", { className: 'w-3 h-3 glass-surface-primary glass-radius-full transform rotate-45 glass-ml-2' })] }) }, task.id); }) }) }), jsx("div", { ref: chartRef, className: 'glass-flex-1 overflow-auto relative', onScroll: handleScroll, children: jsxs("div", { className: 'relative', style: { width: timeColumns.length * columnWidth, height: organizedTasks.length * rowHeight }, children: [jsxs("div", { className: 'absolute inset-0', children: [timeColumns.map((date, index) => { const isToday = viewOptions.showToday && date.toDateString() === new Date().toDateString(); const isWeekend = date.getDay() === 0 || date.getDay() === 6; return jsx("div", { className: cn("absolute top-0 bottom-0 border-r", isToday ? "border-primary/50 bg-primary/5" : "border-border/10", !viewOptions.showWeekends && isWeekend && "bg-muted/10"), style: { left: index * columnWidth } }, index); }), organizedTasks.map((_, index) => jsx("div", { className: 'absolute left-0 right-0 glass-border-b glass-border-glass-border/10', style: { top: (index + 1) * rowHeight } }, index))] }), organizedTasks.map((task, index) => { const position = getTaskPosition(task); const bounds = { x: position.x, width: position.width, y: index * rowHeight, height: rowHeight }; return jsx(MotionFramer, { preset: shouldAnimate && respectMotionPreference ? "slideRight" : "none", delay: index * 100, children: renderTask ? renderTask(task, bounds) : defaultRenderTask(task, bounds) }, task.id); }), viewOptions.showDependencies && organizedTasks.map(task => task.dependencies?.map(depId => { const depTask = organizedTasks.find(t => t.id === depId); if (!depTask) return null; const taskIndex = organizedTasks.indexOf(task); const depIndex = organizedTasks.indexOf(depTask); const taskPos = getTaskPosition(task); const depPos = getTaskPosition(depTask); return jsxs("svg", { className: 'absolute pointer-events-none', style: { left: 0, top: 0, width: "100%", height: "100%" }, children: [jsx("line", { x1: depPos.x + depPos.width, y1: depIndex * rowHeight + rowHeight / 2, x2: taskPos.x, y2: taskIndex * rowHeight + rowHeight / 2, stroke: "currentColor", strokeWidth: "2", strokeDasharray: "4,4", className: 'text-primary/40' }), jsx("polygon", { points: `${taskPos.x - 6},${taskIndex * rowHeight + rowHeight / 2 - 3} ${taskPos.x},${taskIndex * rowHeight + rowHeight / 2} ${taskPos.x - 6},${taskIndex * rowHeight + rowHeight / 2 + 3}`, fill: "currentColor", className: 'text-primary/40' })] }, `${task.id}-${depId}`); }))] }) })] })] }) }); }); GlassGanttChart.displayName = "GlassGanttChart"; export { GlassGanttChart, GlassGanttChart as default }; //# sourceMappingURL=GlassGanttChart.js.map