gantt-lib 0.64.0 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -499,6 +499,12 @@ init_dateMath();
499
499
 
500
500
  // src/core/scheduling/dependencies.ts
501
501
  init_dateMath();
502
+ function normalizePredecessorDates(predecessor, parseDateFn) {
503
+ const predStart = parseDateFn(predecessor.startDate);
504
+ const isMilestone = predecessor.type === "milestone";
505
+ const predEnd = isMilestone ? new Date(predStart.getTime() - DAY_MS) : parseDateFn(predecessor.endDate);
506
+ return { predStart, predEnd };
507
+ }
502
508
  function getDependencyLag(dep) {
503
509
  return Number.isFinite(dep.lag) ? dep.lag : 0;
504
510
  }
@@ -774,8 +780,7 @@ function clampTaskRangeForIncomingFS(task, proposedStart, proposedEnd, allTasks,
774
780
  if (!predecessor) {
775
781
  continue;
776
782
  }
777
- const predecessorStart = parseDateOnly(predecessor.startDate);
778
- const predecessorEnd = parseDateOnly(predecessor.endDate);
783
+ const { predStart: predecessorStart, predEnd: predecessorEnd } = normalizePredecessorDates(predecessor, parseDateOnly);
779
784
  const predecessorDuration = getTaskDuration(
780
785
  predecessorStart,
781
786
  predecessorEnd,
@@ -811,8 +816,10 @@ function recalculateIncomingLags(task, newStartDate, newEndDate, allTasks, busin
811
816
  if (!predecessor) {
812
817
  return { ...dep, lag: getDependencyLag(dep) };
813
818
  }
814
- const predecessorStart = new Date(predecessor.startDate);
815
- const predecessorEnd = new Date(predecessor.endDate);
819
+ const { predStart: predecessorStart, predEnd: predecessorEnd } = normalizePredecessorDates(
820
+ predecessor,
821
+ (d) => new Date(d instanceof Date ? d.getTime() : `${String(d).split("T")[0]}T00:00:00.000Z`)
822
+ );
816
823
  const nextLag = computeLagFromDates(
817
824
  dep.type,
818
825
  predecessorStart,
@@ -827,6 +834,12 @@ function recalculateIncomingLags(task, newStartDate, newEndDate, allTasks, busin
827
834
  }
828
835
 
829
836
  // src/core/scheduling/cascade.ts
837
+ function parseCascadeDateInput(date) {
838
+ if (date instanceof Date) {
839
+ return normalizeUTCDate(date);
840
+ }
841
+ return normalizeUTCDate(/* @__PURE__ */ new Date(`${date.split("T")[0]}T00:00:00.000Z`));
842
+ }
830
843
  function getSuccessorChain(draggedTaskId, allTasks, linkTypes = ["FS"]) {
831
844
  const successorMap = /* @__PURE__ */ new Map();
832
845
  for (const task of allTasks) {
@@ -905,7 +918,21 @@ function cascadeByLinks(movedTaskId, newStart, newEnd, allTasks, skipChildCascad
905
918
  const origStart = new Date(orig.startDate);
906
919
  const origEnd = new Date(orig.endDate);
907
920
  const duration = getTaskDuration(origStart, origEnd);
908
- const constraintDate = calculateSuccessorDate(predStart, predEnd, dep.type, getDependencyLag(dep));
921
+ const currentTask = taskById.get(currentId);
922
+ const { predStart: normalizedPredStart, predEnd: normalizedPredEnd } = normalizePredecessorDates(
923
+ {
924
+ startDate: predStart,
925
+ endDate: predEnd,
926
+ type: currentTask.type
927
+ },
928
+ parseCascadeDateInput
929
+ );
930
+ const constraintDate = calculateSuccessorDate(
931
+ normalizedPredStart,
932
+ normalizedPredEnd,
933
+ dep.type,
934
+ getDependencyLag(dep)
935
+ );
909
936
  let newSuccStart;
910
937
  let newSuccEnd;
911
938
  if (dep.type === "FS" || dep.type === "SS") {
@@ -1060,9 +1087,17 @@ function universalCascade(movedTask, newStart, newEnd, allTasks, businessDays =
1060
1087
  if (!dep) continue;
1061
1088
  const origStart = new Date(task.startDate);
1062
1089
  const origEnd = new Date(task.endDate);
1090
+ const { predStart: normalizedPredStart, predEnd: normalizedPredEnd } = normalizePredecessorDates(
1091
+ {
1092
+ startDate: currStart,
1093
+ endDate: currEnd,
1094
+ type: currentOriginal.type
1095
+ },
1096
+ parseCascadeDateInput
1097
+ );
1063
1098
  const constraintDate = calculateSuccessorDate(
1064
- currStart,
1065
- currEnd,
1099
+ normalizedPredStart,
1100
+ normalizedPredEnd,
1066
1101
  dep.type,
1067
1102
  getDependencyLag(dep),
1068
1103
  businessDays,
@@ -1279,8 +1314,7 @@ function recalculateTaskFromDependencies(taskId, snapshot, options) {
1279
1314
  for (const dep of task.dependencies) {
1280
1315
  const predecessor = snapshot.find((t) => t.id === dep.taskId);
1281
1316
  if (!predecessor) continue;
1282
- const predStart = parseDateOnly(predecessor.startDate);
1283
- const predEnd = parseDateOnly(predecessor.endDate);
1317
+ const { predStart, predEnd } = normalizePredecessorDates(predecessor, parseDateOnly);
1284
1318
  const constraintDate = calculateSuccessorDate(
1285
1319
  predStart,
1286
1320
  predEnd,
@@ -1396,8 +1430,7 @@ function recalculateProjectSchedule(snapshot, options) {
1396
1430
  if (!predecessor) {
1397
1431
  continue;
1398
1432
  }
1399
- const predecessorStart = parseDateOnly(predecessor.startDate);
1400
- const predecessorEnd = parseDateOnly(predecessor.endDate);
1433
+ const { predStart: predecessorStart, predEnd: predecessorEnd } = normalizePredecessorDates(predecessor, parseDateOnly);
1401
1434
  const constraintDate = calculateSuccessorDate(
1402
1435
  predecessorStart,
1403
1436
  predecessorEnd,
@@ -1816,6 +1849,45 @@ var calculateTaskBar = (taskStartDate, taskEndDate, monthStart, dayWidth) => {
1816
1849
  const width = Math.round((duration + 1) * dayWidth);
1817
1850
  return { left, width };
1818
1851
  };
1852
+ var calculateMilestoneGeometry = (taskDate, monthStart, dayWidth, size = 14) => {
1853
+ const { left, width } = calculateTaskBar(taskDate, taskDate, monthStart, dayWidth);
1854
+ const centerX = Math.round(left + width / 2);
1855
+ const halfSize = Math.round(size / 2);
1856
+ return {
1857
+ centerX,
1858
+ left: centerX - halfSize,
1859
+ right: centerX + halfSize,
1860
+ size
1861
+ };
1862
+ };
1863
+ var calculateMilestoneConnectionBounds = (dayLeft, dayWidth, size = 14) => {
1864
+ const halfDiagonal = Math.round(size / Math.SQRT2);
1865
+ const visualNudge = 2;
1866
+ return {
1867
+ left: dayLeft + halfDiagonal + visualNudge,
1868
+ right: dayLeft + dayWidth - halfDiagonal - visualNudge
1869
+ };
1870
+ };
1871
+ var resolveTaskHorizontalGeometry = (task, monthStart, dayWidth, override) => {
1872
+ const startDate = new Date(task.startDate);
1873
+ const endDate = new Date(task.endDate);
1874
+ if (task.type === "milestone") {
1875
+ const size = 14;
1876
+ if (override) {
1877
+ return calculateMilestoneConnectionBounds(override.left, dayWidth, size);
1878
+ }
1879
+ const bar2 = calculateTaskBar(startDate, startDate, monthStart, dayWidth);
1880
+ return calculateMilestoneConnectionBounds(bar2.left, dayWidth, size);
1881
+ }
1882
+ if (override) {
1883
+ return {
1884
+ left: override.left,
1885
+ right: override.left + override.width
1886
+ };
1887
+ }
1888
+ const bar = calculateTaskBar(startDate, endDate, monthStart, dayWidth);
1889
+ return { left: bar.left, right: bar.left + bar.width };
1890
+ };
1819
1891
  var pixelsToDate = (pixels, monthStart, dayWidth) => {
1820
1892
  const days = Math.round(pixels / dayWidth);
1821
1893
  return new Date(Date.UTC(
@@ -1919,6 +1991,9 @@ var calculateDependencyPath = (from, to, arrivesFromRight) => {
1919
1991
  if (fy === ty) {
1920
1992
  return `M ${fx} ${fy} H ${tx}`;
1921
1993
  }
1994
+ if (fx === tx) {
1995
+ return `M ${fx} ${fy} V ${ty}`;
1996
+ }
1922
1997
  const C = 2;
1923
1998
  const goingDown = ty > fy;
1924
1999
  const dirY = goingDown ? 1 : -1;
@@ -2026,6 +2101,37 @@ var isTaskExpired = (task, referenceDate = /* @__PURE__ */ new Date()) => {
2026
2101
  return actualProgress < expectedProgress;
2027
2102
  };
2028
2103
 
2104
+ // src/utils/taskType.ts
2105
+ init_dateUtils();
2106
+ var TASK_TYPE_DEFAULT = "task";
2107
+ function getTaskType(task) {
2108
+ return task.type ?? TASK_TYPE_DEFAULT;
2109
+ }
2110
+ function isMilestoneTask(task) {
2111
+ return getTaskType(task) === "milestone";
2112
+ }
2113
+ function normalizeMilestoneStartDate(startDateInput) {
2114
+ const parsedStartDate = parseUTCDate(startDateInput);
2115
+ if (startDateInput instanceof Date) {
2116
+ return new Date(Date.UTC(
2117
+ parsedStartDate.getUTCFullYear(),
2118
+ parsedStartDate.getUTCMonth(),
2119
+ parsedStartDate.getUTCDate()
2120
+ ));
2121
+ }
2122
+ return parsedStartDate.toISOString().split("T")[0];
2123
+ }
2124
+ function normalizeTaskDatesForType(task) {
2125
+ if (!isMilestoneTask(task)) {
2126
+ return task;
2127
+ }
2128
+ const startDate = normalizeMilestoneStartDate(task.startDate);
2129
+ return {
2130
+ ...task,
2131
+ endDate: startDate
2132
+ };
2133
+ }
2134
+
2029
2135
  // src/hooks/useTaskDrag.ts
2030
2136
  import { useEffect, useRef, useState, useCallback } from "react";
2031
2137
 
@@ -2044,6 +2150,17 @@ function resolveDateRangeFromPixels(mode, left, width, monthStart, dayWidth, tas
2044
2150
  monthStart.getUTCMonth(),
2045
2151
  monthStart.getUTCDate() + rawEndOffset
2046
2152
  ));
2153
+ const isMilestone = task.type === "milestone";
2154
+ if (isMilestone) {
2155
+ const anchorDate = mode === "resize-right" ? rawEndDate : rawStartDate;
2156
+ if (businessDays && weekendPredicate) {
2157
+ const originalAnchor = mode === "resize-right" ? new Date(task.endDate) : new Date(task.startDate);
2158
+ const snapDirection2 = anchorDate.getTime() >= originalAnchor.getTime() ? 1 : -1;
2159
+ const alignedDate = alignToWorkingDay(anchorDate, snapDirection2, weekendPredicate);
2160
+ return { start: alignedDate, end: alignedDate };
2161
+ }
2162
+ return { start: anchorDate, end: anchorDate };
2163
+ }
2047
2164
  if (!(businessDays && weekendPredicate)) {
2048
2165
  return { start: rawStartDate, end: rawEndDate };
2049
2166
  }
@@ -2134,8 +2251,10 @@ function handleGlobalMouseMove(e) {
2134
2251
  const activeDrag = globalActiveDrag;
2135
2252
  const { startX, initialLeft, initialWidth, mode, dayWidth, onProgress, allTasks } = activeDrag;
2136
2253
  const deltaX = e.clientX - startX;
2254
+ const draggedTask = allTasks.find((t) => t.id === activeDrag.taskId);
2255
+ const effectiveWidth = draggedTask && isMilestoneTask(draggedTask) ? dayWidth : initialWidth;
2137
2256
  let newLeft = initialLeft;
2138
- let newWidth = initialWidth;
2257
+ let newWidth = effectiveWidth;
2139
2258
  switch (mode) {
2140
2259
  case "move":
2141
2260
  newLeft = snapToGrid(initialLeft + deltaX, dayWidth);
@@ -2151,7 +2270,6 @@ function handleGlobalMouseMove(e) {
2151
2270
  newWidth = Math.max(dayWidth, snappedWidth);
2152
2271
  break;
2153
2272
  }
2154
- const draggedTask = allTasks.find((t) => t.id === activeDrag.taskId);
2155
2273
  if (activeDrag.businessDays && activeDrag.weekendPredicate && draggedTask) {
2156
2274
  const previewRange = clampDateRangeForIncomingFS(
2157
2275
  draggedTask,
@@ -2193,6 +2311,9 @@ function handleGlobalMouseMove(e) {
2193
2311
  newLeft = Math.round(alignedStartDay * dayWidth);
2194
2312
  newWidth = Math.round((alignedEndDay - alignedStartDay + 1) * dayWidth);
2195
2313
  }
2314
+ if (draggedTask && isMilestoneTask(draggedTask)) {
2315
+ newWidth = dayWidth;
2316
+ }
2196
2317
  if (!activeDrag.disableConstraints && activeDrag.onCascadeProgress) {
2197
2318
  const { dayWidth: dayWidth2, monthStart: mStart, taskId: dragId } = activeDrag;
2198
2319
  const originalDraggedTask = draggedTask ?? allTasks.find((t) => t.id === dragId);
@@ -2229,7 +2350,8 @@ function handleGlobalMouseMove(e) {
2229
2350
  };
2230
2351
  })();
2231
2352
  const previewStartDate = previewRange.start;
2232
- const previewEndDate = previewRange.end;
2353
+ const isMilestone = originalDraggedTask ? isMilestoneTask(originalDraggedTask) : false;
2354
+ const previewEndDate = isMilestone ? previewRange.start : previewRange.end;
2233
2355
  const movedTaskData = originalDraggedTask ?? { id: dragId, name: "", startDate: "", endDate: "" };
2234
2356
  const cascadeResult = universalCascade(
2235
2357
  { ...movedTaskData, startDate: previewStartDate.toISOString(), endDate: previewEndDate.toISOString() },
@@ -2319,6 +2441,9 @@ var useTaskDrag = (options) => {
2319
2441
  businessDays = true,
2320
2442
  weekendPredicate
2321
2443
  } = options;
2444
+ const rawHookTask = allTasks.find((t) => t.id === taskId);
2445
+ const hookTask = rawHookTask ? normalizeTaskDatesForType(rawHookTask) : void 0;
2446
+ const hookTaskIsMilestone = hookTask ? isMilestoneTask(hookTask) : false;
2322
2447
  const isOwnerRef = useRef(false);
2323
2448
  const effectiveLocked = locked || disableTaskDrag;
2324
2449
  const [isDragging, setIsDragging] = useState(false);
@@ -2340,11 +2465,11 @@ var useTaskDrag = (options) => {
2340
2465
  return Math.round((ms1 - ms2) / (1e3 * 60 * 60 * 24));
2341
2466
  };
2342
2467
  const startOffset = getUTCDayDifference2(initialStartDate, monthStart);
2343
- const duration = getUTCDayDifference2(initialEndDate, initialStartDate);
2468
+ const duration = hookTaskIsMilestone ? 0 : getUTCDayDifference2(initialEndDate, initialStartDate);
2344
2469
  const left = Math.round(startOffset * dayWidth);
2345
2470
  const width = Math.round((duration + 1) * dayWidth);
2346
2471
  return { left, width };
2347
- }, [initialStartDate, initialEndDate, monthStart, dayWidth]);
2472
+ }, [initialStartDate, initialEndDate, monthStart, dayWidth, hookTaskIsMilestone]);
2348
2473
  useEffect(() => {
2349
2474
  if (isOwnerRef.current && globalActiveDrag) return;
2350
2475
  const { left, width } = getInitialPosition();
@@ -2379,7 +2504,8 @@ var useTaskDrag = (options) => {
2379
2504
  const handleComplete = useCallback((finalLeft, finalWidth, finalMode) => {
2380
2505
  const wasOwner = isOwnerRef.current;
2381
2506
  isOwnerRef.current = false;
2382
- const currentTask = allTasks.find((t) => t.id === taskId);
2507
+ const currentTaskRaw = allTasks.find((t) => t.id === taskId);
2508
+ const currentTask = currentTaskRaw ? normalizeTaskDatesForType(currentTaskRaw) : void 0;
2383
2509
  const finalRange = currentTask ? clampDateRangeForIncomingFS(
2384
2510
  currentTask,
2385
2511
  resolveDateRangeFromPixels(
@@ -2413,7 +2539,7 @@ var useTaskDrag = (options) => {
2413
2539
  };
2414
2540
  })();
2415
2541
  const newStartDate = finalRange.start;
2416
- const newEndDate = finalRange.end;
2542
+ const newEndDate = currentTask && isMilestoneTask(currentTask) ? finalRange.start : finalRange.end;
2417
2543
  setIsDragging(false);
2418
2544
  setDragMode(null);
2419
2545
  if (onDragStateChange) {
@@ -2425,6 +2551,23 @@ var useTaskDrag = (options) => {
2425
2551
  });
2426
2552
  }
2427
2553
  if (wasOwner) {
2554
+ const startUnchanged = newStartDate.getTime() === Date.UTC(
2555
+ initialStartDate.getUTCFullYear(),
2556
+ initialStartDate.getUTCMonth(),
2557
+ initialStartDate.getUTCDate()
2558
+ );
2559
+ const baselineEndDate = hookTaskIsMilestone ? initialStartDate : initialEndDate;
2560
+ const endUnchanged = newEndDate.getTime() === Date.UTC(
2561
+ baselineEndDate.getUTCFullYear(),
2562
+ baselineEndDate.getUTCMonth(),
2563
+ baselineEndDate.getUTCDate()
2564
+ );
2565
+ if (startUnchanged && endUnchanged) {
2566
+ const { left, width } = getInitialPosition();
2567
+ setCurrentLeft(left);
2568
+ setCurrentWidth(width);
2569
+ return;
2570
+ }
2428
2571
  if (!disableConstraints && onCascade && allTasks.length > 0) {
2429
2572
  const draggedTaskData = currentTask;
2430
2573
  const movedTask = {
@@ -2462,7 +2605,8 @@ var useTaskDrag = (options) => {
2462
2605
  businessDays,
2463
2606
  weekendPredicate,
2464
2607
  initialStartDate,
2465
- initialEndDate
2608
+ initialEndDate,
2609
+ hookTaskIsMilestone
2466
2610
  ]);
2467
2611
  const handleCancel = useCallback(() => {
2468
2612
  isOwnerRef.current = false;
@@ -2505,6 +2649,9 @@ var useTaskDrag = (options) => {
2505
2649
  if (currentTask2 && isTaskParent(taskId, allTasks)) {
2506
2650
  mode = "move";
2507
2651
  }
2652
+ if (currentTask2 && isMilestoneTask(currentTask2)) {
2653
+ mode = "move";
2654
+ }
2508
2655
  }
2509
2656
  if (!mode) {
2510
2657
  return;
@@ -2590,14 +2737,16 @@ var useTaskDrag = (options) => {
2590
2737
  // src/components/TaskRow/TaskRow.tsx
2591
2738
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
2592
2739
  var arePropsEqual = (prevProps, nextProps) => {
2593
- return prevProps.task.id === nextProps.task.id && prevProps.task.name === nextProps.task.name && prevProps.task.startDate === nextProps.task.startDate && prevProps.task.endDate === nextProps.task.endDate && prevProps.task.color === nextProps.task.color && prevProps.task.progress === nextProps.task.progress && prevProps.task.accepted === nextProps.task.accepted && prevProps.monthStart.getTime() === nextProps.monthStart.getTime() && prevProps.dayWidth === nextProps.dayWidth && prevProps.rowHeight === nextProps.rowHeight && prevProps.overridePosition?.left === nextProps.overridePosition?.left && prevProps.overridePosition?.width === nextProps.overridePosition?.width && prevProps.allTasks === nextProps.allTasks && prevProps.disableConstraints === nextProps.disableConstraints && prevProps.task.locked === nextProps.task.locked && prevProps.task.divider === nextProps.task.divider && prevProps.highlightExpiredTasks === nextProps.highlightExpiredTasks && prevProps.isFilterMatch === nextProps.isFilterMatch && prevProps.businessDays === nextProps.businessDays && prevProps.customDays === nextProps.customDays && prevProps.isWeekend === nextProps.isWeekend && prevProps.disableTaskDrag === nextProps.disableTaskDrag;
2740
+ return prevProps.task.id === nextProps.task.id && prevProps.task.name === nextProps.task.name && prevProps.task.startDate === nextProps.task.startDate && prevProps.task.endDate === nextProps.task.endDate && prevProps.task.type === nextProps.task.type && prevProps.task.color === nextProps.task.color && prevProps.task.progress === nextProps.task.progress && prevProps.task.accepted === nextProps.task.accepted && prevProps.monthStart.getTime() === nextProps.monthStart.getTime() && prevProps.dayWidth === nextProps.dayWidth && prevProps.rowHeight === nextProps.rowHeight && prevProps.overridePosition?.left === nextProps.overridePosition?.left && prevProps.overridePosition?.width === nextProps.overridePosition?.width && prevProps.allTasks === nextProps.allTasks && prevProps.disableConstraints === nextProps.disableConstraints && prevProps.task.locked === nextProps.task.locked && prevProps.task.divider === nextProps.task.divider && prevProps.highlightExpiredTasks === nextProps.highlightExpiredTasks && prevProps.isFilterMatch === nextProps.isFilterMatch && prevProps.businessDays === nextProps.businessDays && prevProps.customDays === nextProps.customDays && prevProps.isWeekend === nextProps.isWeekend && prevProps.disableTaskDrag === nextProps.disableTaskDrag;
2594
2741
  };
2595
2742
  var TaskRow = React2.memo(
2596
2743
  ({ task, monthStart, dayWidth, rowHeight, onTasksChange, onDragStateChange, rowIndex, allTasks, enableAutoSchedule, disableConstraints, overridePosition, onCascadeProgress, onCascade, divider, highlightExpiredTasks, isFilterMatch = false, businessDays, customDays, isWeekend: isWeekend3, disableTaskDrag = false }) => {
2597
2744
  const defaultParentBarColor = "#782FC4";
2598
2745
  const { divider: taskDivider } = task;
2599
- const taskStartDate = useMemo2(() => parseUTCDate(task.startDate), [task.startDate]);
2600
- const taskEndDate = useMemo2(() => parseUTCDate(task.endDate), [task.endDate]);
2746
+ const normalizedTask = useMemo2(() => normalizeTaskDatesForType(task), [task]);
2747
+ const milestone = useMemo2(() => isMilestoneTask(normalizedTask), [normalizedTask]);
2748
+ const taskStartDate = useMemo2(() => parseUTCDate(normalizedTask.startDate), [normalizedTask.startDate]);
2749
+ const taskEndDate = useMemo2(() => parseUTCDate(normalizedTask.endDate), [normalizedTask.endDate]);
2601
2750
  const isParent = useMemo2(() => {
2602
2751
  return allTasks ? isTaskParent(task.id, allTasks) : false;
2603
2752
  }, [allTasks, task.id]);
@@ -2606,12 +2755,16 @@ var TaskRow = React2.memo(
2606
2755
  }, [allTasks, task.id]);
2607
2756
  const isExpired = useMemo2(() => {
2608
2757
  if (!highlightExpiredTasks) return false;
2609
- return isTaskExpired(task);
2610
- }, [task.startDate, task.endDate, task.progress, highlightExpiredTasks]);
2758
+ return isTaskExpired(normalizedTask);
2759
+ }, [normalizedTask.startDate, normalizedTask.endDate, normalizedTask.progress, highlightExpiredTasks]);
2611
2760
  const { left, width } = useMemo2(
2612
2761
  () => calculateTaskBar(taskStartDate, taskEndDate, monthStart, dayWidth),
2613
2762
  [taskStartDate, taskEndDate, monthStart, dayWidth]
2614
2763
  );
2764
+ const milestoneGeometry = useMemo2(
2765
+ () => calculateMilestoneGeometry(taskStartDate, monthStart, dayWidth),
2766
+ [taskStartDate, monthStart, dayWidth]
2767
+ );
2615
2768
  const barColor = isExpired ? "var(--gantt-expired-color)" : task.color || "var(--gantt-task-bar-default-color)";
2616
2769
  const progressWidth = useMemo2(() => {
2617
2770
  if (task.progress === void 0 || task.progress <= 0) return 0;
@@ -2650,7 +2803,7 @@ var TaskRow = React2.memo(
2650
2803
  }, [defaultParentBarColor, isExpired, isParent, progressWidth, barColor, progressColor, task.color]);
2651
2804
  const handleDragEnd = (result) => {
2652
2805
  const updatedTask = {
2653
- ...task,
2806
+ ...normalizedTask,
2654
2807
  startDate: result.startDate.toISOString(),
2655
2808
  endDate: result.endDate.toISOString(),
2656
2809
  ...result.updatedDependencies !== void 0 && { dependencies: result.updatedDependencies }
@@ -2689,8 +2842,20 @@ var TaskRow = React2.memo(
2689
2842
  });
2690
2843
  const displayLeft = overridePosition?.left ?? (isDragging ? currentLeft : left);
2691
2844
  const displayWidth = overridePosition?.width ?? (isDragging ? currentWidth : width);
2845
+ const displayMilestoneGeometry = useMemo2(() => {
2846
+ const centerX = Math.round(displayLeft + dayWidth / 2);
2847
+ const halfSize = Math.round(milestoneGeometry.size / 2);
2848
+ return {
2849
+ centerX,
2850
+ left: centerX - halfSize,
2851
+ right: centerX + halfSize,
2852
+ size: milestoneGeometry.size
2853
+ };
2854
+ }, [displayLeft, dayWidth, milestoneGeometry.size]);
2855
+ const visualLeft = milestone ? displayMilestoneGeometry.left : displayLeft;
2856
+ const visualWidth = milestone ? displayMilestoneGeometry.size : displayWidth;
2692
2857
  const currentStartDate = isDragging ? pixelsToDate(displayLeft, monthStart, dayWidth) : taskStartDate;
2693
- const currentEndDate = isDragging ? pixelsToDate(displayLeft + displayWidth - dayWidth, monthStart, dayWidth) : taskEndDate;
2858
+ const currentEndDate = isDragging ? milestone ? pixelsToDate(displayLeft, monthStart, dayWidth) : pixelsToDate(displayLeft + displayWidth - dayWidth, monthStart, dayWidth) : taskEndDate;
2694
2859
  const dateRangeLabel = formatDateRangeLabel(currentStartDate, currentEndDate);
2695
2860
  const durationDays = businessDays ? getBusinessDaysCount(currentStartDate, currentEndDate, weekendPredicate) : Math.round(
2696
2861
  (currentEndDate.getTime() - currentStartDate.getTime()) / (1e3 * 60 * 60 * 24)
@@ -2705,9 +2870,9 @@ var TaskRow = React2.memo(
2705
2870
  return `${count} \u0437\u0430\u0434\u0430\u0447`;
2706
2871
  };
2707
2872
  const estimatedTextWidth = isParent ? 120 : durationDays >= 10 ? 76 : 62;
2708
- const showProgressInside = progressWidth > 0 && displayWidth > estimatedTextWidth;
2873
+ const showProgressInside = !milestone && progressWidth > 0 && displayWidth > estimatedTextWidth;
2709
2874
  const MIN_DURATION_WIDTH = isParent ? 80 : 50;
2710
- const showDurationInside = durationDays >= 2 && displayWidth > MIN_DURATION_WIDTH;
2875
+ const showDurationInside = !milestone && durationDays >= 2 && displayWidth > MIN_DURATION_WIDTH;
2711
2876
  return /* @__PURE__ */ jsxs2(
2712
2877
  "div",
2713
2878
  {
@@ -2721,18 +2886,24 @@ var TaskRow = React2.memo(
2721
2886
  "div",
2722
2887
  {
2723
2888
  "data-taskbar": true,
2724
- className: `gantt-tr-taskBar ${isDragging ? "gantt-tr-dragging" : ""} ${task.locked ? "gantt-tr-locked" : ""} ${isParent ? "gantt-tr-parentBar" : ""}`,
2889
+ className: `gantt-tr-taskBar ${isDragging ? "gantt-tr-dragging" : ""} ${task.locked ? "gantt-tr-locked" : ""} ${isParent ? "gantt-tr-parentBar" : ""} ${milestone ? "gantt-tr-milestone" : ""}`,
2725
2890
  style: {
2726
- left: `${displayLeft}px`,
2727
- width: `${displayWidth}px`,
2891
+ left: `${visualLeft}px`,
2728
2892
  ...barStyle,
2729
- height: isParent ? "var(--gantt-parent-bar-height, 14px)" : "var(--gantt-task-bar-height)",
2893
+ ...milestone ? {
2894
+ height: `${displayMilestoneGeometry.size}px`,
2895
+ width: `${displayMilestoneGeometry.size}px`,
2896
+ padding: 0
2897
+ } : {
2898
+ width: `${visualWidth}px`,
2899
+ height: isParent ? "var(--gantt-parent-bar-height, 14px)" : "var(--gantt-task-bar-height)"
2900
+ },
2730
2901
  cursor: dragHandleProps.style.cursor,
2731
2902
  userSelect: dragHandleProps.style.userSelect
2732
2903
  },
2733
2904
  onMouseDown: dragHandleProps.onMouseDown,
2734
2905
  children: [
2735
- progressWidth > 0 && progressWidth < 100 && /* @__PURE__ */ jsx2(
2906
+ !milestone && progressWidth > 0 && progressWidth < 100 && /* @__PURE__ */ jsx2(
2736
2907
  "div",
2737
2908
  {
2738
2909
  className: "gantt-tr-progressBar",
@@ -2745,13 +2916,13 @@ var TaskRow = React2.memo(
2745
2916
  }
2746
2917
  }
2747
2918
  ),
2748
- !isParent && /* @__PURE__ */ jsx2("div", { className: "gantt-tr-resizeHandle gantt-tr-resizeHandleLeft" }),
2919
+ !isParent && !milestone && /* @__PURE__ */ jsx2("div", { className: "gantt-tr-resizeHandle gantt-tr-resizeHandleLeft" }),
2749
2920
  showDurationInside && /* @__PURE__ */ jsx2("span", { className: "gantt-tr-taskDuration", children: isParent ? getChildCountLabel(childCount) : `${durationDays} \u0434` }),
2750
2921
  progressWidth > 0 && showProgressInside && /* @__PURE__ */ jsxs2("span", { className: "gantt-tr-progressText", children: [
2751
2922
  progressWidth,
2752
2923
  "%"
2753
2924
  ] }),
2754
- !isParent && /* @__PURE__ */ jsx2("div", { className: "gantt-tr-resizeHandle gantt-tr-resizeHandleRight" })
2925
+ !isParent && !milestone && /* @__PURE__ */ jsx2("div", { className: "gantt-tr-resizeHandle gantt-tr-resizeHandleRight" })
2755
2926
  ]
2756
2927
  }
2757
2928
  ),
@@ -2760,7 +2931,7 @@ var TaskRow = React2.memo(
2760
2931
  {
2761
2932
  className: `gantt-tr-leftLabels ${task.locked ? "gantt-tr-leftLabels-locked" : ""}`,
2762
2933
  style: {
2763
- left: `${displayLeft}px`
2934
+ left: `${visualLeft}px`
2764
2935
  },
2765
2936
  children: /* @__PURE__ */ jsx2("span", { className: "gantt-tr-dateLabel gantt-tr-dateLabelLeft", children: dateRangeLabel })
2766
2937
  }
@@ -2771,7 +2942,7 @@ var TaskRow = React2.memo(
2771
2942
  className: "gantt-tr-lockIcon",
2772
2943
  style: {
2773
2944
  position: "absolute",
2774
- left: `${displayLeft - 16}px`,
2945
+ left: `${visualLeft - 16}px`,
2775
2946
  top: "50%",
2776
2947
  transform: "translateY(-50%)",
2777
2948
  width: "12px",
@@ -2791,11 +2962,11 @@ var TaskRow = React2.memo(
2791
2962
  {
2792
2963
  className: "gantt-tr-rightLabels",
2793
2964
  style: {
2794
- left: `${displayLeft + Math.max(displayWidth, 20) - Math.min(6, Math.max(displayWidth, 20) / 2) + 8}px`,
2965
+ left: `${visualLeft + Math.max(visualWidth, 20) - Math.min(6, Math.max(visualWidth, 20) / 2) + 8}px`,
2795
2966
  color: isParent ? task.color || defaultParentBarColor : barColor
2796
2967
  },
2797
2968
  children: [
2798
- !showDurationInside && /* @__PURE__ */ jsx2("span", { className: "gantt-tr-externalDuration", children: isParent ? getChildCountLabel(childCount) : `${durationDays} \u0434` }),
2969
+ !showDurationInside && !milestone && /* @__PURE__ */ jsx2("span", { className: "gantt-tr-externalDuration", children: isParent ? getChildCountLabel(childCount) : `${durationDays} \u0434` }),
2799
2970
  progressWidth > 0 && !showProgressInside && /* @__PURE__ */ jsxs2("span", { className: "gantt-tr-externalProgress", children: [
2800
2971
  progressWidth,
2801
2972
  "%"
@@ -3044,16 +3215,12 @@ var DependencyLines = React5.memo(({
3044
3215
  const taskMap = new Map(tasksForPositions.map((t) => [t.id, t]));
3045
3216
  const visibleTaskMap = new Map(tasks.map((t) => [t.id, t]));
3046
3217
  tasks.forEach((task, index) => {
3047
- const startDate = new Date(task.startDate);
3048
- const endDate = new Date(task.endDate);
3049
- const computed = calculateTaskBar(startDate, endDate, monthStart, dayWidth);
3050
3218
  const override = dragOverrides?.get(task.id);
3051
- const resolvedLeft = override?.left ?? computed.left;
3052
- const resolvedWidth = override?.width ?? computed.width;
3219
+ const computed = resolveTaskHorizontalGeometry(task, monthStart, dayWidth, override);
3053
3220
  indices.set(task.id, index);
3054
3221
  positions.set(task.id, {
3055
- left: resolvedLeft,
3056
- right: resolvedLeft + resolvedWidth,
3222
+ left: computed.left,
3223
+ right: computed.right,
3057
3224
  rowTop: index * rowHeight,
3058
3225
  isVirtual: false
3059
3226
  });
@@ -3067,15 +3234,11 @@ var DependencyLines = React5.memo(({
3067
3234
  if (!visibleAncestor) continue;
3068
3235
  const ancestorPosition = positions.get(visibleAncestor.id);
3069
3236
  if (!ancestorPosition) continue;
3070
- const startDate = new Date(task.startDate);
3071
- const endDate = new Date(task.endDate);
3072
- const computed = calculateTaskBar(startDate, endDate, monthStart, dayWidth);
3073
3237
  const override = dragOverrides?.get(task.id);
3074
- const resolvedLeft = override?.left ?? computed.left;
3075
- const resolvedWidth = override?.width ?? computed.width;
3238
+ const computed = resolveTaskHorizontalGeometry(task, monthStart, dayWidth, override);
3076
3239
  positions.set(task.id, {
3077
- left: resolvedLeft,
3078
- right: resolvedLeft + resolvedWidth,
3240
+ left: computed.left,
3241
+ right: computed.right,
3079
3242
  rowTop: ancestorPosition.rowTop,
3080
3243
  isVirtual: true
3081
3244
  });
@@ -3091,6 +3254,7 @@ var DependencyLines = React5.memo(({
3091
3254
  }, [tasks, allTasks]);
3092
3255
  const lines = useMemo5(() => {
3093
3256
  const tasksForEdges = allTasks ?? tasks;
3257
+ const taskMap = new Map(tasksForEdges.map((task) => [task.id, task]));
3094
3258
  const edges = getAllDependencyEdges(tasksForEdges);
3095
3259
  const lines2 = [];
3096
3260
  for (const edge of edges) {
@@ -3101,9 +3265,11 @@ var DependencyLines = React5.memo(({
3101
3265
  if (!predecessor || !successor) {
3102
3266
  continue;
3103
3267
  }
3268
+ const predecessorTask = taskMap.get(edge.predecessorId);
3269
+ const successorTask = taskMap.get(edge.successorId);
3104
3270
  if (allTasks && collapsedParentIds.size > 0) {
3105
- const taskMap = new Map(allTasks.map((t) => [t.id, t]));
3106
- if (areBothHiddenInSameParent(edge.predecessorId, edge.successorId, collapsedParentIds, taskMap)) {
3271
+ const taskMap2 = new Map(allTasks.map((t) => [t.id, t]));
3272
+ if (areBothHiddenInSameParent(edge.predecessorId, edge.successorId, collapsedParentIds, taskMap2)) {
3107
3273
  continue;
3108
3274
  }
3109
3275
  }
@@ -3123,11 +3289,18 @@ var DependencyLines = React5.memo(({
3123
3289
  fromY = predecessor.rowTop + rowHeight - 10;
3124
3290
  toY = successor.rowTop + 6;
3125
3291
  }
3126
- const fromX = edge.type === "SS" || edge.type === "SF" ? predecessor.left : predecessor.right;
3292
+ let fromX = edge.type === "SS" || edge.type === "SF" ? predecessor.left : predecessor.right;
3127
3293
  const toX = edge.type === "FF" || edge.type === "SF" ? successor.right : successor.left;
3294
+ const stackedMilestonesSameDay = Boolean(
3295
+ predecessorTask && successorTask && isMilestoneTask(predecessorTask) && isMilestoneTask(successorTask) && edge.lag === 0 && new Date(predecessorTask.startDate).toISOString().split("T")[0] === new Date(successorTask.startDate).toISOString().split("T")[0] && predecessor.rowTop !== successor.rowTop && edge.type === "FS"
3296
+ );
3297
+ const finalToX = stackedMilestonesSameDay ? Math.round(((predecessor.left + predecessor.right) / 2 + (successor.left + successor.right) / 2) / 2) : toX;
3298
+ if (stackedMilestonesSameDay) {
3299
+ fromX = finalToX;
3300
+ }
3128
3301
  const arrivesFromRight = edge.type === "FF" || edge.type === "SF";
3129
3302
  const from = { x: fromX, y: fromY };
3130
- const to = { x: toX, y: toY };
3303
+ const to = { x: finalToX, y: toY };
3131
3304
  const path = calculateDependencyPath(from, to, arrivesFromRight);
3132
3305
  const hasCycle = cycleInfo.has(edge.predecessorId) || cycleInfo.has(edge.successorId);
3133
3306
  lines2.push({
@@ -3136,7 +3309,7 @@ var DependencyLines = React5.memo(({
3136
3309
  hasCycle,
3137
3310
  lag: edge.lag,
3138
3311
  fromX,
3139
- toX,
3312
+ toX: finalToX,
3140
3313
  fromY,
3141
3314
  reverseOrder,
3142
3315
  isVirtual
@@ -3149,6 +3322,7 @@ var DependencyLines = React5.memo(({
3149
3322
  "svg",
3150
3323
  {
3151
3324
  className: "gantt-dependencies-svg",
3325
+ "data-testid": "dependency-lines-svg",
3152
3326
  width: gridWidth,
3153
3327
  height: svgHeight,
3154
3328
  xmlns: "http://www.w3.org/2000/svg",
@@ -4147,7 +4321,7 @@ var DepChip = ({
4147
4321
  const predecessor = taskById.get(dep.taskId);
4148
4322
  if (!predecessor) return;
4149
4323
  const predStart = parseUTCDate(predecessor.startDate);
4150
- const predEnd = parseUTCDate(predecessor.endDate);
4324
+ const predEnd = predecessor.type === "milestone" ? predStart : parseUTCDate(predecessor.endDate);
4151
4325
  const origStart = parseUTCDate(task.startDate);
4152
4326
  const origEnd = parseUTCDate(task.endDate);
4153
4327
  const durationMs = origEnd.getTime() - origStart.getTime();
@@ -4410,10 +4584,12 @@ var TaskListRow = React9.memo(
4410
4584
  const editingName = editingColumnId === "name";
4411
4585
  const editingDuration = editingColumnId === "duration";
4412
4586
  const editingProgress = editingColumnId === "progress";
4587
+ const normalizedTask = useMemo7(() => normalizeTaskDatesForType(task), [task]);
4588
+ const isMilestone = useMemo7(() => isMilestoneTask(normalizedTask), [normalizedTask]);
4413
4589
  const [nameValue, setNameValue] = useState4("");
4414
4590
  const nameInputRef = useRef4(null);
4415
4591
  const [durationValue, setDurationValue] = useState4(
4416
- () => getInclusiveDurationDays(task.startDate, task.endDate)
4592
+ () => getInclusiveDurationDays(normalizedTask.startDate, normalizedTask.endDate)
4417
4593
  );
4418
4594
  const durationInputRef = useRef4(null);
4419
4595
  const dependencySearchInputRef = useRef4(null);
@@ -4614,14 +4790,14 @@ var TaskListRow = React9.memo(
4614
4790
  e.stopPropagation();
4615
4791
  durationConfirmedRef.current = false;
4616
4792
  setDurationValue(
4617
- getDuration(task.startDate, task.endDate)
4793
+ isMilestone ? 0 : getDuration(normalizedTask.startDate, normalizedTask.endDate)
4618
4794
  );
4619
4795
  setEditingColumnId("duration");
4620
4796
  },
4621
- [task.locked, task.startDate, task.endDate, getDuration]
4797
+ [task.locked, normalizedTask.startDate, normalizedTask.endDate, getDuration, isMilestone]
4622
4798
  );
4623
4799
  const applyDurationChange = useCallback4((nextDuration) => {
4624
- const normalizedDuration = Math.max(1, Math.round(nextDuration) || 1);
4800
+ const normalizedDuration = Math.max(0, Math.round(nextDuration) || 0);
4625
4801
  setDurationValue(normalizedDuration);
4626
4802
  }, []);
4627
4803
  const handleDurationSave = useCallback4(() => {
@@ -4629,19 +4805,26 @@ var TaskListRow = React9.memo(
4629
4805
  durationConfirmedRef.current = false;
4630
4806
  return;
4631
4807
  }
4632
- const normalizedDuration = Math.max(1, Math.round(durationValue) || 1);
4633
- onTasksChange?.([
4634
- {
4635
- ...task,
4636
- endDate: getEndDate(task.startDate, normalizedDuration)
4637
- }
4638
- ]);
4808
+ const rounded = Math.round(durationValue) || 0;
4809
+ if (isMilestone && rounded > 0) {
4810
+ onTasksChange?.([
4811
+ { ...task, type: "task", endDate: getEndDate(task.startDate, rounded) }
4812
+ ]);
4813
+ } else if (!isMilestone && rounded === 0) {
4814
+ onTasksChange?.([
4815
+ { ...task, type: "milestone", endDate: task.startDate }
4816
+ ]);
4817
+ } else if (!isMilestone && rounded > 0) {
4818
+ onTasksChange?.([
4819
+ { ...task, endDate: getEndDate(task.startDate, rounded) }
4820
+ ]);
4821
+ }
4639
4822
  setEditingColumnId(null);
4640
- }, [durationValue, task, onTasksChange, getEndDate]);
4823
+ }, [durationValue, task, onTasksChange, getEndDate, isMilestone]);
4641
4824
  const handleDurationCancel = useCallback4(() => {
4642
- setDurationValue(getDuration(task.startDate, task.endDate));
4825
+ setDurationValue(isMilestone ? 0 : getDuration(normalizedTask.startDate, normalizedTask.endDate));
4643
4826
  setEditingColumnId(null);
4644
- }, [task.startDate, task.endDate, getDuration]);
4827
+ }, [normalizedTask.startDate, normalizedTask.endDate, getDuration, isMilestone]);
4645
4828
  const handleDurationAdjust = useCallback4(
4646
4829
  (delta) => {
4647
4830
  applyDurationChange(durationValue + delta);
@@ -4653,25 +4836,26 @@ var TaskListRow = React9.memo(
4653
4836
  e.stopPropagation();
4654
4837
  if (e.key === "Enter") {
4655
4838
  durationConfirmedRef.current = true;
4656
- const normalizedDuration = Math.max(
4657
- 1,
4658
- Math.round(durationValue) || 1
4659
- );
4660
- onTasksChange?.([
4661
- {
4662
- ...task,
4663
- endDate: getEndDate(
4664
- task.startDate,
4665
- normalizedDuration
4666
- )
4667
- }
4668
- ]);
4839
+ const rounded = Math.round(durationValue) || 0;
4840
+ if (isMilestone && rounded > 0) {
4841
+ onTasksChange?.([
4842
+ { ...task, type: "task", endDate: getEndDate(task.startDate, rounded) }
4843
+ ]);
4844
+ } else if (!isMilestone && rounded === 0) {
4845
+ onTasksChange?.([
4846
+ { ...task, type: "milestone", endDate: task.startDate }
4847
+ ]);
4848
+ } else if (!isMilestone && rounded > 0) {
4849
+ onTasksChange?.([
4850
+ { ...task, endDate: getEndDate(task.startDate, rounded) }
4851
+ ]);
4852
+ }
4669
4853
  setEditingColumnId(null);
4670
4854
  } else if (e.key === "Escape") {
4671
4855
  handleDurationCancel();
4672
4856
  }
4673
4857
  },
4674
- [durationValue, task, onTasksChange, handleDurationCancel, getEndDate]
4858
+ [durationValue, task, onTasksChange, handleDurationCancel, getEndDate, isMilestone]
4675
4859
  );
4676
4860
  const handleProgressClick = useCallback4(
4677
4861
  (e) => {
@@ -4742,17 +4926,54 @@ var TaskListRow = React9.memo(
4742
4926
  }
4743
4927
  }, [editingProgress]);
4744
4928
  useEffect4(() => {
4745
- setDurationValue(getDuration(task.startDate, task.endDate));
4746
- }, [task.startDate, task.endDate, getDuration]);
4929
+ setDurationValue(getDuration(normalizedTask.startDate, normalizedTask.endDate));
4930
+ }, [normalizedTask.startDate, normalizedTask.endDate, getDuration]);
4747
4931
  useEffect4(() => {
4748
4932
  if (editingDuration && durationInputRef.current) {
4749
4933
  durationInputRef.current.focus();
4750
4934
  durationInputRef.current.select();
4751
4935
  }
4752
4936
  }, [editingDuration]);
4937
+ const emitMilestoneDateChange = useCallback4((nextDateISO) => {
4938
+ const alignedDate = businessDays ? alignToWorkingDay(/* @__PURE__ */ new Date(`${nextDateISO}T00:00:00.000Z`), 1, weekendPredicate) : /* @__PURE__ */ new Date(`${nextDateISO}T00:00:00.000Z`);
4939
+ const clampedRange = clampTaskRangeForIncomingFS(
4940
+ task,
4941
+ alignedDate,
4942
+ alignedDate,
4943
+ allTasks,
4944
+ businessDays,
4945
+ weekendPredicate
4946
+ );
4947
+ const normalized = normalizeTaskDatesForType({
4948
+ ...task,
4949
+ startDate: clampedRange.start.toISOString().split("T")[0],
4950
+ endDate: clampedRange.end.toISOString().split("T")[0]
4951
+ });
4952
+ const startDate = parseUTCDate(normalized.startDate);
4953
+ const endDate = parseUTCDate(normalized.endDate);
4954
+ onTasksChange?.([
4955
+ {
4956
+ ...normalized,
4957
+ ...task.dependencies && {
4958
+ dependencies: recalculateIncomingLags(
4959
+ task,
4960
+ startDate,
4961
+ endDate,
4962
+ allTasks,
4963
+ businessDays,
4964
+ weekendPredicate
4965
+ )
4966
+ }
4967
+ }
4968
+ ]);
4969
+ }, [task, onTasksChange, allTasks, businessDays, weekendPredicate]);
4753
4970
  const handleStartDateChange = useCallback4(
4754
4971
  (newDateISO) => {
4755
4972
  if (!newDateISO) return;
4973
+ if (isMilestone) {
4974
+ emitMilestoneDateChange(newDateISO);
4975
+ return;
4976
+ }
4756
4977
  let nextEndISO;
4757
4978
  const normalizedInputStart = businessDays ? alignToWorkingDay(/* @__PURE__ */ new Date(`${newDateISO}T00:00:00.000Z`), 1, weekendPredicate) : /* @__PURE__ */ new Date(`${newDateISO}T00:00:00.000Z`);
4758
4979
  if (businessDays) {
@@ -4799,11 +5020,15 @@ var TaskListRow = React9.memo(
4799
5020
  }
4800
5021
  ]);
4801
5022
  },
4802
- [task, onTasksChange, businessDays, getDuration, getEndDate, allTasks, weekendPredicate]
5023
+ [task, onTasksChange, businessDays, getDuration, getEndDate, allTasks, weekendPredicate, isMilestone, emitMilestoneDateChange]
4803
5024
  );
4804
5025
  const handleEndDateChange = useCallback4(
4805
5026
  (newDateISO) => {
4806
5027
  if (!newDateISO) return;
5028
+ if (isMilestone) {
5029
+ emitMilestoneDateChange(newDateISO);
5030
+ return;
5031
+ }
4807
5032
  let nextStartISO;
4808
5033
  const normalizedInputEnd = businessDays ? alignToWorkingDay(/* @__PURE__ */ new Date(`${newDateISO}T00:00:00.000Z`), -1, weekendPredicate) : /* @__PURE__ */ new Date(`${newDateISO}T00:00:00.000Z`);
4809
5034
  if (businessDays) {
@@ -4850,7 +5075,7 @@ var TaskListRow = React9.memo(
4850
5075
  }
4851
5076
  ]);
4852
5077
  },
4853
- [task, onTasksChange, businessDays, getDuration, weekendPredicate, allTasks]
5078
+ [task, onTasksChange, businessDays, getDuration, weekendPredicate, allTasks, isMilestone, emitMilestoneDateChange]
4854
5079
  );
4855
5080
  const handleRowClickInternal = useCallback4(() => {
4856
5081
  onRowClick?.(task.id);
@@ -5121,8 +5346,8 @@ var TaskListRow = React9.memo(
5121
5346
  },
5122
5347
  [selectedChip?.successorId, selectedChip?.predecessorId, selectedChip?.linkType, onRemoveDependency, onChipSelect]
5123
5348
  );
5124
- const startDateISO = toISODate(task.startDate);
5125
- const endDateISO = editingDuration ? getEndDate(task.startDate, durationValue) : toISODate(task.endDate);
5349
+ const startDateISO = toISODate(normalizedTask.startDate);
5350
+ const endDateISO = editingDuration ? getEndDate(normalizedTask.startDate, durationValue) : toISODate(normalizedTask.endDate);
5126
5351
  const numberCell = /* @__PURE__ */ jsxs9(
5127
5352
  "div",
5128
5353
  {
@@ -5528,10 +5753,10 @@ var TaskListRow = React9.memo(
5528
5753
  {
5529
5754
  ref: durationInputRef,
5530
5755
  type: "number",
5531
- min: 1,
5756
+ min: 0,
5532
5757
  step: 1,
5533
5758
  value: durationValue,
5534
- onChange: (e) => applyDurationChange(parseInt(e.target.value, 10) || 1),
5759
+ onChange: (e) => applyDurationChange(parseInt(e.target.value, 10) || 0),
5535
5760
  onBlur: handleDurationSave,
5536
5761
  onKeyDown: handleDurationKeyDown,
5537
5762
  className: "gantt-tl-number-input"
@@ -5592,14 +5817,11 @@ var TaskListRow = React9.memo(
5592
5817
  ]
5593
5818
  }
5594
5819
  ),
5595
- /* @__PURE__ */ jsxs9(
5820
+ /* @__PURE__ */ jsx12(
5596
5821
  "span",
5597
5822
  {
5598
5823
  style: editingDuration ? { visibility: "hidden", pointerEvents: "none" } : void 0,
5599
- children: [
5600
- getDuration(task.startDate, task.endDate),
5601
- "\u0434"
5602
- ]
5824
+ children: isMilestone ? "0" : `${getDuration(normalizedTask.startDate, normalizedTask.endDate)}\u0434`
5603
5825
  }
5604
5826
  )
5605
5827
  ]
@@ -7089,7 +7311,8 @@ function GanttChartInner(props, ref) {
7089
7311
  }
7090
7312
  return;
7091
7313
  }
7092
- const cascadedTasks = disableConstraints ? [updatedTask] : universalCascade(updatedTask, newStart, newEnd, tasks, businessDays, isCustomWeekend);
7314
+ const sourceTasks = tasks.map((task) => task.id === updatedTask.id ? updatedTask : task);
7315
+ const cascadedTasks = disableConstraints ? [updatedTask] : universalCascade(updatedTask, newStart, newEnd, sourceTasks, businessDays, isCustomWeekend);
7093
7316
  onTasksChange?.(cascadedTasks);
7094
7317
  }, [tasks, onTasksChange, disableConstraints, editingTaskId, businessDays, isCustomWeekend]);
7095
7318
  const handleDelete = useCallback6((taskId) => {
@@ -7542,6 +7765,8 @@ export {
7542
7765
  calculateDependencyPath,
7543
7766
  calculateGridLines,
7544
7767
  calculateGridWidth,
7768
+ calculateMilestoneConnectionBounds,
7769
+ calculateMilestoneGeometry,
7545
7770
  calculateMonthGridLines,
7546
7771
  calculateOrthogonalPath,
7547
7772
  calculateSuccessorDate,
@@ -7593,6 +7818,7 @@ export {
7593
7818
  nameContains,
7594
7819
  normalizeDependencyLag,
7595
7820
  normalizeHierarchyTasks,
7821
+ normalizePredecessorDates,
7596
7822
  normalizeTaskDates,
7597
7823
  normalizeUTCDate,
7598
7824
  not,
@@ -7608,6 +7834,7 @@ export {
7608
7834
  removeDependenciesBetweenTasks,
7609
7835
  resizeTaskWithCascade,
7610
7836
  resolveDateRangeFromPixels,
7837
+ resolveTaskHorizontalGeometry,
7611
7838
  shiftBusinessDayOffset,
7612
7839
  subtractBusinessDays2 as subtractBusinessDays,
7613
7840
  universalCascade,