aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
515 lines (512 loc) • 20.7 kB
JavaScript
'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