gantt-lib 0.135.8 → 0.136.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.js CHANGED
@@ -42,6 +42,8 @@ __export(index_exports, {
42
42
  GenerationSkeletonRows: () => GenerationSkeleton_default,
43
43
  GridBackground: () => GridBackground_default,
44
44
  Input: () => Input,
45
+ MIN_EDGE_ZONE_PX: () => MIN_EDGE_ZONE_PX,
46
+ MIN_MOVE_ZONE_PX: () => MIN_MOVE_ZONE_PX,
45
47
  PlanFactMatrix: () => PlanFactMatrix,
46
48
  Popover: () => Popover,
47
49
  PopoverContent: () => PopoverContent,
@@ -3304,6 +3306,7 @@ var TimeScaleHeader_default = TimeScaleHeader;
3304
3306
 
3305
3307
  // src/components/TaskRow/TaskRow.tsx
3306
3308
  var import_react3 = __toESM(require("react"));
3309
+ var import_react_dom = require("react-dom");
3307
3310
 
3308
3311
  // src/utils/geometry.ts
3309
3312
  var getUTCDayDifference = (date1, date2) => {
@@ -3389,19 +3392,20 @@ var pixelsToDate = (pixels, monthStart, dayWidth) => {
3389
3392
  var calculateGridWidth = (daysInMonth, dayWidth) => {
3390
3393
  return Math.round(daysInMonth * dayWidth);
3391
3394
  };
3395
+ var MIN_EDGE_ZONE_PX = 8;
3396
+ var MIN_MOVE_ZONE_PX = 16;
3392
3397
  var detectEdgeZone = (clientX, taskBarElement, edgeZoneWidth = 12) => {
3393
3398
  const rect = taskBarElement.getBoundingClientRect();
3394
3399
  const relativeX = Math.round(clientX - rect.left);
3395
3400
  const width = Math.round(rect.width);
3396
- if (width <= 2 * edgeZoneWidth) {
3397
- const distanceToLeft = relativeX;
3398
- const distanceToRight = width - relativeX;
3399
- return distanceToLeft <= distanceToRight ? "left" : "right";
3400
- }
3401
- if (relativeX >= 0 && relativeX <= edgeZoneWidth) {
3401
+ const effectiveZone = Math.max(
3402
+ MIN_EDGE_ZONE_PX,
3403
+ Math.min(edgeZoneWidth, (width - MIN_MOVE_ZONE_PX) / 2)
3404
+ );
3405
+ if (relativeX >= 0 && relativeX <= effectiveZone) {
3402
3406
  return "left";
3403
3407
  }
3404
- if (relativeX >= width - edgeZoneWidth && relativeX <= width) {
3408
+ if (relativeX >= width - effectiveZone && relativeX <= width) {
3405
3409
  return "right";
3406
3410
  }
3407
3411
  return "move";
@@ -4021,8 +4025,7 @@ var useTaskDrag = (options) => {
4021
4025
  locked = false,
4022
4026
  disableTaskDrag = false,
4023
4027
  businessDays = true,
4024
- weekendPredicate,
4025
- viewMode = "day"
4028
+ weekendPredicate
4026
4029
  } = options;
4027
4030
  const rawHookTask = allTasks.find((t) => t.id === taskId);
4028
4031
  const hookTask = rawHookTask ? normalizeTaskDatesForType(rawHookTask) : void 0;
@@ -4294,23 +4297,18 @@ var useTaskDrag = (options) => {
4294
4297
  if (effectiveLocked) return;
4295
4298
  const target = e.currentTarget;
4296
4299
  const currentTask = allTasks.find((t) => t.id === taskId);
4297
- const isSingleDayTask = !!currentTask && !isMilestoneTask(currentTask) && !isTaskParent(taskId, allTasks) && currentWidth <= dayWidth && viewMode === "day";
4298
4300
  let mode = null;
4299
- if (isSingleDayTask) {
4300
- mode = "move";
4301
- } else {
4302
- const edgeZone = detectEdgeZone(e.clientX, target, edgeZoneWidth);
4303
- switch (edgeZone) {
4304
- case "left":
4305
- mode = "resize-left";
4306
- break;
4307
- case "right":
4308
- mode = "resize-right";
4309
- break;
4310
- case "move":
4311
- mode = "move";
4312
- break;
4313
- }
4301
+ const edgeZone = detectEdgeZone(e.clientX, target, edgeZoneWidth);
4302
+ switch (edgeZone) {
4303
+ case "left":
4304
+ mode = "resize-left";
4305
+ break;
4306
+ case "right":
4307
+ mode = "resize-right";
4308
+ break;
4309
+ case "move":
4310
+ mode = "move";
4311
+ break;
4314
4312
  }
4315
4313
  if (mode === "resize-left" || mode === "resize-right") {
4316
4314
  if (currentTask && isMilestoneTask(currentTask)) {
@@ -4360,7 +4358,7 @@ var useTaskDrag = (options) => {
4360
4358
  cascadeContext: shouldBuildCascadeContext ? createCascadeContext(allTasks) : void 0,
4361
4359
  scheduleIntentMode: Boolean(onScheduleIntent)
4362
4360
  };
4363
- }, [edgeZoneWidth, currentLeft, currentWidth, dayWidth, monthStart, taskId, onDragStateChange, handleProgress, handleComplete, handleCancel, allTasks, disableConstraints, deferCascadePreview, onCascadeProgress, onCascade, effectiveLocked, viewMode]);
4361
+ }, [edgeZoneWidth, currentLeft, currentWidth, dayWidth, monthStart, taskId, onDragStateChange, handleProgress, handleComplete, handleCancel, allTasks, disableConstraints, deferCascadePreview, onCascadeProgress, onCascade, effectiveLocked]);
4364
4362
  const handleMouseDown = (0, import_react2.useCallback)((e) => {
4365
4363
  startDrag(e);
4366
4364
  }, [startDrag]);
@@ -4379,14 +4377,13 @@ var useTaskDrag = (options) => {
4379
4377
  }
4380
4378
  const target = e.currentTarget;
4381
4379
  const currentTask = allTasks.find((t) => t.id === taskId);
4382
- const isSingleDayTask = !!currentTask && !isMilestoneTask(currentTask) && !isTaskParent(taskId, allTasks) && currentWidth <= dayWidth && viewMode === "day";
4383
- if (isSingleDayTask || currentTask && isMilestoneTask(currentTask)) {
4380
+ if (currentTask && isMilestoneTask(currentTask)) {
4384
4381
  setHoverCursor("grab");
4385
4382
  return;
4386
4383
  }
4387
4384
  const edgeZone = detectEdgeZone(e.clientX, target, edgeZoneWidth);
4388
4385
  setHoverCursor(edgeZone === "move" ? "grab" : "ew-resize");
4389
- }, [disableTaskDrag, locked, isDragging, allTasks, taskId, currentWidth, dayWidth, viewMode, edgeZoneWidth]);
4386
+ }, [disableTaskDrag, locked, isDragging, allTasks, taskId, edgeZoneWidth]);
4390
4387
  const handleMouseLeave = (0, import_react2.useCallback)(() => {
4391
4388
  setHoverCursor(disableTaskDrag ? "grab" : locked ? "not-allowed" : isDragging ? "grabbing" : "grab");
4392
4389
  }, [disableTaskDrag, locked, isDragging]);
@@ -4417,12 +4414,35 @@ var useTaskDrag = (options) => {
4417
4414
 
4418
4415
  // src/components/TaskRow/TaskRow.tsx
4419
4416
  var import_jsx_runtime2 = require("react/jsx-runtime");
4417
+ var formatCompositePreviewDate = (date) => {
4418
+ const month = new Intl.DateTimeFormat("ru-RU", { month: "short", timeZone: "UTC" }).format(date).replace(/\.$/, "");
4419
+ const yearSuffix = date.getUTCFullYear() === (/* @__PURE__ */ new Date()).getFullYear() ? "" : ` ${String(date.getUTCFullYear()).slice(-2)}`;
4420
+ return `${date.getUTCDate()} ${month}${yearSuffix}`;
4421
+ };
4420
4422
  var arePropsEqual = (prevProps, nextProps) => {
4421
- 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.baselineStartDate === nextProps.task.baselineStartDate && prevProps.task.baselineEndDate === nextProps.task.baselineEndDate && 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.previewPositionStore === nextProps.previewPositionStore && prevProps.allTasks === nextProps.allTasks && prevProps.disableConstraints === nextProps.disableConstraints && prevProps.deferCascadePreview === nextProps.deferCascadePreview && prevProps.task.locked === nextProps.task.locked && prevProps.task.synced === nextProps.task.synced && prevProps.task.divider === nextProps.task.divider && prevProps.highlightExpiredTasks === nextProps.highlightExpiredTasks && prevProps.isCritical === nextProps.isCritical && prevProps.showBaseline === nextProps.showBaseline && prevProps.isFilterMatch === nextProps.isFilterMatch && prevProps.businessDays === nextProps.businessDays && prevProps.customDays === nextProps.customDays && prevProps.isWeekend === nextProps.isWeekend && prevProps.disableTaskDrag === nextProps.disableTaskDrag && prevProps.disableDependencyEditing === nextProps.disableDependencyEditing && prevProps.isDependencyDragActive === nextProps.isDependencyDragActive && prevProps.viewMode === nextProps.viewMode && prevProps.showTaskDateLabels === nextProps.showTaskDateLabels && prevProps.showTaskNames === nextProps.showTaskNames;
4423
+ 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.baselineStartDate === nextProps.task.baselineStartDate && prevProps.task.baselineEndDate === nextProps.task.baselineEndDate && 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.previewPositionStore === nextProps.previewPositionStore && prevProps.allTasks === nextProps.allTasks && prevProps.disableConstraints === nextProps.disableConstraints && prevProps.deferCascadePreview === nextProps.deferCascadePreview && prevProps.task.locked === nextProps.task.locked && prevProps.task.synced === nextProps.task.synced && prevProps.task.divider === nextProps.task.divider && prevProps.highlightExpiredTasks === nextProps.highlightExpiredTasks && prevProps.isCritical === nextProps.isCritical && prevProps.showBaseline === nextProps.showBaseline && prevProps.onCompositeToggle === nextProps.onCompositeToggle && prevProps.compactDetail === nextProps.compactDetail && prevProps.compositeParentColor === nextProps.compositeParentColor && prevProps.compositeExpanded === nextProps.compositeExpanded && prevProps.isFilterMatch === nextProps.isFilterMatch && prevProps.businessDays === nextProps.businessDays && prevProps.customDays === nextProps.customDays && prevProps.isWeekend === nextProps.isWeekend && prevProps.disableTaskDrag === nextProps.disableTaskDrag && prevProps.disableDependencyEditing === nextProps.disableDependencyEditing && prevProps.isDependencyDragActive === nextProps.isDependencyDragActive && prevProps.viewMode === nextProps.viewMode && prevProps.showTaskDateLabels === nextProps.showTaskDateLabels && prevProps.showTaskNames === nextProps.showTaskNames;
4422
4424
  };
4423
4425
  var TaskRow = import_react3.default.memo(
4424
- ({ task, monthStart, dayWidth, rowHeight, onTasksChange, onScheduleIntent, onDragStateChange, rowIndex, allTasks, enableAutoSchedule, disableConstraints, deferCascadePreview = false, overridePosition, previewPositionStore, onCascadeProgress, onCascade, divider, highlightExpiredTasks, isCritical = false, showBaseline = false, isFilterMatch = false, businessDays, customDays, isWeekend: isWeekend3, disableTaskDrag = false, disableDependencyEditing = false, onDependencyPortPointerDown, isDependencyDragActive = false, viewMode = "day", showTaskDateLabels = true, showTaskNames = true }) => {
4426
+ ({ task, monthStart, dayWidth, rowHeight, onTasksChange, onScheduleIntent, onDragStateChange, rowIndex, allTasks, enableAutoSchedule, disableConstraints, deferCascadePreview = false, overridePosition, previewPositionStore, onCascadeProgress, onCascade, divider, highlightExpiredTasks, isCritical = false, showBaseline = false, isFilterMatch = false, businessDays, customDays, isWeekend: isWeekend3, disableTaskDrag = false, disableDependencyEditing = false, onDependencyPortPointerDown, isDependencyDragActive = false, viewMode = "day", showTaskDateLabels = true, showTaskNames = true, onCompositeToggle, compactDetail = false, compositeParentColor, compositeExpanded = false }) => {
4425
4427
  const defaultParentBarColor = "#782FC4";
4428
+ const [showCompositePreview, setShowCompositePreview] = (0, import_react3.useState)(false);
4429
+ const [compositePreviewPosition, setCompositePreviewPosition] = (0, import_react3.useState)(null);
4430
+ const pointerStart = (0, import_react3.useRef)(null);
4431
+ const suppressCompositeClick = (0, import_react3.useRef)(false);
4432
+ const compositeChildren = (0, import_react3.useMemo)(
4433
+ () => task.composite ? (allTasks ?? []).filter((child) => child.parentId === task.id) : [],
4434
+ [allTasks, task.composite, task.id]
4435
+ );
4436
+ (0, import_react3.useEffect)(() => {
4437
+ if (!showCompositePreview) return;
4438
+ const hide = () => setShowCompositePreview(false);
4439
+ window.addEventListener("scroll", hide, true);
4440
+ window.addEventListener("resize", hide);
4441
+ return () => {
4442
+ window.removeEventListener("scroll", hide, true);
4443
+ window.removeEventListener("resize", hide);
4444
+ };
4445
+ }, [showCompositePreview]);
4426
4446
  const { divider: taskDivider } = task;
4427
4447
  const normalizedTask = (0, import_react3.useMemo)(() => normalizeTaskDatesForType(task), [task]);
4428
4448
  const milestone = (0, import_react3.useMemo)(() => isMilestoneTask(normalizedTask), [normalizedTask]);
@@ -4439,6 +4459,7 @@ var TaskRow = import_react3.default.memo(
4439
4459
  const isParent = (0, import_react3.useMemo)(() => {
4440
4460
  return allTasks ? isTaskParent(task.id, allTasks) : false;
4441
4461
  }, [allTasks, task.id]);
4462
+ const isVisualParent = isParent && !task.composite;
4442
4463
  const childCount = (0, import_react3.useMemo)(() => {
4443
4464
  return allTasks ? getChildren(task.id, allTasks).length : 0;
4444
4465
  }, [allTasks, task.id]);
@@ -4484,19 +4505,19 @@ var TaskRow = import_react3.default.memo(
4484
4505
  if (isExpired) {
4485
4506
  return "color-mix(in srgb, var(--gantt-expired-color) 40%, black)";
4486
4507
  }
4487
- const baseColor = isParent ? task.color || defaultParentBarColor : task.color || "var(--gantt-task-bar-default-color)";
4508
+ const baseColor = isVisualParent ? task.color || defaultParentBarColor : task.color || "var(--gantt-task-bar-default-color)";
4488
4509
  return `color-mix(in srgb, ${baseColor} 40%, black)`;
4489
- }, [defaultParentBarColor, isCritical, isExpired, isParent, task.color]);
4510
+ }, [defaultParentBarColor, isCritical, isExpired, isVisualParent, task.color]);
4490
4511
  const barStyle = (0, import_react3.useMemo)(() => {
4491
4512
  const parentBarColor = task.color || defaultParentBarColor;
4492
4513
  if (isCritical && !isExpired) {
4493
4514
  const c = criticalBarColor;
4494
- if (isParent) {
4515
+ if (isVisualParent) {
4495
4516
  return { backgroundColor: c, "--gantt-parent-bar-color": c };
4496
4517
  }
4497
4518
  return { backgroundColor: c };
4498
4519
  }
4499
- if (isParent) {
4520
+ if (isVisualParent) {
4500
4521
  if (progressWidth >= 100) {
4501
4522
  const c = isExpired ? "color-mix(in srgb, var(--gantt-expired-color) 40%, black)" : `color-mix(in srgb, ${parentBarColor} 40%, black)`;
4502
4523
  return { backgroundColor: c, "--gantt-parent-bar-color": c };
@@ -4507,7 +4528,7 @@ var TaskRow = import_react3.default.memo(
4507
4528
  return { backgroundColor: progressColor };
4508
4529
  }
4509
4530
  return { backgroundColor: barColor };
4510
- }, [isCritical, defaultParentBarColor, isExpired, isParent, progressWidth, barColor, progressColor, task.color]);
4531
+ }, [isCritical, defaultParentBarColor, isExpired, isVisualParent, progressWidth, barColor, progressColor, task.color]);
4511
4532
  const handleDragEnd = (result) => {
4512
4533
  const updatedTask = {
4513
4534
  ...normalizedTask,
@@ -4581,6 +4602,25 @@ var TaskRow = import_react3.default.memo(
4581
4602
  }, [displayLeft, dayWidth, milestoneGeometry.size]);
4582
4603
  const visualLeft = milestone ? displayMilestoneGeometry.left : displayLeft;
4583
4604
  const visualWidth = milestone ? displayMilestoneGeometry.size : displayWidth;
4605
+ const compositeSegments = (0, import_react3.useMemo)(() => {
4606
+ if (!task.composite || width <= 0) return [];
4607
+ const laneEnds = [];
4608
+ return compositeChildren.map((child) => {
4609
+ const segment = calculateTaskBar(parseUTCDate(child.startDate), parseUTCDate(child.endDate), monthStart, dayWidth);
4610
+ let lane = laneEnds.findIndex((end) => end <= segment.left);
4611
+ if (lane < 0) lane = laneEnds.length;
4612
+ laneEnds[lane] = segment.left + segment.width;
4613
+ return {
4614
+ id: child.id,
4615
+ name: child.name,
4616
+ left: (segment.left - left) / width * 100,
4617
+ width: segment.width / width * 100,
4618
+ lane
4619
+ };
4620
+ });
4621
+ }, [task.composite, compositeChildren, monthStart, dayWidth, left, width, displayLeft]);
4622
+ const compositeLaneCount = Math.max(1, ...compositeSegments.map((segment) => segment.lane + 1));
4623
+ const showCompositeSegments = task.composite && compositeSegments.length > 0 && visualWidth / compositeSegments.length >= 22;
4584
4624
  const shouldRenderBaseline = showBaseline && baselineGeometry !== null;
4585
4625
  const hasPreviewPosition = isDragging || effectiveOverridePosition !== void 0;
4586
4626
  const currentStartDate = hasPreviewPosition ? pixelsToDate(displayLeft, monthStart, dayWidth) : taskStartDate;
@@ -4598,17 +4638,20 @@ var TaskRow = import_react3.default.memo(
4598
4638
  if (lastDigit >= 2 && lastDigit <= 4) return `${count} \u0440\u0430\u0431\u043E\u0442\u044B`;
4599
4639
  return `${count} \u0440\u0430\u0431\u043E\u0442`;
4600
4640
  };
4601
- const estimatedTextWidth = isParent ? 120 : durationDays >= 10 ? 76 : 62;
4602
- const showProgressInside = !milestone && progressWidth > 0 && displayWidth > estimatedTextWidth;
4603
- const MIN_DURATION_WIDTH = isParent ? 80 : 50;
4604
- const showDurationInside = !milestone && durationDays >= 2 && displayWidth > MIN_DURATION_WIDTH;
4641
+ const estimatedTextWidth = isVisualParent ? 120 : durationDays >= 10 ? 76 : 62;
4642
+ const showProgressInside = !milestone && progressWidth > 0 && displayWidth > (compactDetail ? 28 : estimatedTextWidth);
4643
+ const MIN_DURATION_WIDTH = isVisualParent ? 80 : task.composite ? 92 : 50;
4644
+ const showDurationInside = !compactDetail && !milestone && durationDays >= 2 && displayWidth > MIN_DURATION_WIDTH;
4605
4645
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
4606
4646
  "div",
4607
4647
  {
4608
4648
  "data-filter-match": isFilterMatch ? "true" : "false",
4609
4649
  "data-gantt-task-row-id": task.id,
4610
- className: `gantt-tr-row ${isFilterMatch ? "gantt-tr-row-filter-match" : ""}`,
4611
- style: { height: `${rowHeight}px` },
4650
+ className: `gantt-tr-row ${compactDetail ? "gantt-tr-row-compositeDetail" : ""} ${isFilterMatch ? "gantt-tr-row-filter-match" : ""}`,
4651
+ style: {
4652
+ height: `${rowHeight}px`,
4653
+ "--gantt-tr-composite-detail-color": compositeParentColor ?? "var(--gantt-task-bar-default-color, #8b5cf6)"
4654
+ },
4612
4655
  children: [
4613
4656
  taskDivider === "top" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "gantt-tr-divider gantt-tr-divider-top" }),
4614
4657
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "gantt-tr-taskContainer", children: [
@@ -4626,7 +4669,11 @@ var TaskRow = import_react3.default.memo(
4626
4669
  "div",
4627
4670
  {
4628
4671
  "data-taskbar": true,
4629
- className: `gantt-tr-taskBar ${isDragging ? "gantt-tr-dragging" : ""} ${task.locked ? "gantt-tr-locked" : ""} ${task.synced === false ? "gantt-tr-unsynced" : ""} ${isParent ? "gantt-tr-parentBar" : ""} ${milestone ? "gantt-tr-milestone" : ""} ${isCritical ? "gantt-tr-critical" : ""}`,
4672
+ role: task.composite ? "button" : void 0,
4673
+ tabIndex: task.composite ? 0 : void 0,
4674
+ "aria-label": task.composite ? `${task.name}: ${compositeExpanded ? "\u0441\u0432\u0435\u0440\u043D\u0443\u0442\u044C" : "\u0440\u0430\u0441\u043A\u0440\u044B\u0442\u044C"} \u0434\u0435\u0442\u0430\u043B\u0438` : void 0,
4675
+ "aria-expanded": task.composite ? compositeExpanded : void 0,
4676
+ className: `gantt-tr-taskBar ${isDragging ? "gantt-tr-dragging" : ""} ${task.locked ? "gantt-tr-locked" : ""} ${task.synced === false ? "gantt-tr-unsynced" : ""} ${isVisualParent ? "gantt-tr-parentBar" : ""} ${task.composite ? "gantt-tr-compositeBar" : ""} ${milestone ? "gantt-tr-milestone" : ""} ${isCritical ? "gantt-tr-critical" : ""}`,
4630
4677
  style: {
4631
4678
  left: `${visualLeft}px`,
4632
4679
  ...barStyle,
@@ -4636,14 +4683,57 @@ var TaskRow = import_react3.default.memo(
4636
4683
  padding: 0
4637
4684
  } : {
4638
4685
  width: `${visualWidth}px`,
4639
- height: isParent ? "var(--gantt-parent-bar-height, 14px)" : "var(--gantt-task-bar-height)"
4686
+ height: compactDetail ? "18px" : isVisualParent ? "var(--gantt-parent-bar-height, 14px)" : "var(--gantt-task-bar-height)"
4640
4687
  },
4641
4688
  cursor: dragHandleProps.style.cursor,
4642
4689
  userSelect: dragHandleProps.style.userSelect
4643
4690
  },
4644
4691
  onMouseDown: dragHandleProps.onMouseDown,
4645
4692
  onMouseMove: dragHandleProps.onMouseMove,
4646
- onMouseLeave: dragHandleProps.onMouseLeave,
4693
+ onMouseLeave: () => {
4694
+ dragHandleProps.onMouseLeave();
4695
+ setShowCompositePreview(false);
4696
+ },
4697
+ onMouseEnter: task.composite ? (event) => {
4698
+ const rect = event.currentTarget.getBoundingClientRect();
4699
+ const previewWidth = 220;
4700
+ const previewHeight = Math.min(340, 68 + Math.min(compositeChildren.length, 12) * 20);
4701
+ setCompositePreviewPosition({
4702
+ left: Math.max(8, Math.min(rect.left, window.innerWidth - previewWidth - 8)),
4703
+ top: rect.bottom + previewHeight + 8 < window.innerHeight ? rect.bottom + 6 : Math.max(8, rect.top - previewHeight - 6)
4704
+ });
4705
+ setShowCompositePreview(true);
4706
+ } : void 0,
4707
+ onMouseDownCapture: task.composite ? (event) => {
4708
+ suppressCompositeClick.current = Boolean(event.target.closest(".gantt-tr-resizeHandle"));
4709
+ pointerStart.current = { x: event.clientX, y: event.clientY };
4710
+ } : void 0,
4711
+ onMouseMoveCapture: task.composite ? (event) => {
4712
+ const start = pointerStart.current;
4713
+ if (start && Math.hypot(event.clientX - start.x, event.clientY - start.y) >= 4) {
4714
+ suppressCompositeClick.current = true;
4715
+ }
4716
+ } : void 0,
4717
+ onMouseUpCapture: task.composite ? (event) => {
4718
+ const start = pointerStart.current;
4719
+ if (start && Math.hypot(event.clientX - start.x, event.clientY - start.y) >= 4) {
4720
+ suppressCompositeClick.current = true;
4721
+ }
4722
+ pointerStart.current = null;
4723
+ } : void 0,
4724
+ onClick: task.composite ? () => {
4725
+ if (!isDragging && !suppressCompositeClick.current) {
4726
+ setShowCompositePreview(false);
4727
+ onCompositeToggle?.("click");
4728
+ }
4729
+ suppressCompositeClick.current = false;
4730
+ } : void 0,
4731
+ onKeyDown: task.composite ? (event) => {
4732
+ if (event.key === "Enter" || event.key === " ") {
4733
+ event.preventDefault();
4734
+ onCompositeToggle?.("keyboard");
4735
+ }
4736
+ } : void 0,
4647
4737
  children: [
4648
4738
  !milestone && progressWidth > 0 && progressWidth < 100 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
4649
4739
  "div",
@@ -4652,14 +4742,28 @@ var TaskRow = import_react3.default.memo(
4652
4742
  style: {
4653
4743
  width: `${progressWidth}%`,
4654
4744
  backgroundColor: progressColor,
4655
- ...isParent && {
4745
+ ...isVisualParent && {
4656
4746
  borderRadius: "var(--gantt-parent-bar-radius, 8px) 0 0 0"
4657
4747
  }
4658
4748
  }
4659
4749
  }
4660
4750
  ),
4661
4751
  !milestone && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "gantt-tr-resizeHandle gantt-tr-resizeHandleLeft" }),
4662
- showDurationInside && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gantt-tr-taskDuration", children: isParent ? getChildCountLabel(childCount) : `${durationDays} \u0434` }),
4752
+ showCompositeSegments && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gantt-tr-compositeSegments", "aria-hidden": "true", children: compositeSegments.map((segment) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
4753
+ "span",
4754
+ {
4755
+ className: "gantt-tr-compositeSegment",
4756
+ style: {
4757
+ left: `${segment.left}%`,
4758
+ width: `${segment.width}%`,
4759
+ top: `${segment.lane / compositeLaneCount * 100}%`,
4760
+ height: `${100 / compositeLaneCount}%`
4761
+ }
4762
+ },
4763
+ segment.id
4764
+ )) }),
4765
+ task.composite && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gantt-tr-compositeHoverChevron", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: `gantt-tr-compositeChevron${compositeExpanded ? " gantt-tr-compositeChevron-open" : ""}`, viewBox: "0 0 20 20", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m7 4 6 6-6 6", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }),
4766
+ showDurationInside && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gantt-tr-taskDuration", children: isVisualParent ? getChildCountLabel(childCount) : `${durationDays} \u0434` }),
4663
4767
  progressWidth > 0 && showProgressInside && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "gantt-tr-progressText", children: [
4664
4768
  progressWidth,
4665
4769
  "%"
@@ -4668,6 +4772,41 @@ var TaskRow = import_react3.default.memo(
4668
4772
  ]
4669
4773
  }
4670
4774
  ),
4775
+ task.composite && showCompositePreview && compositePreviewPosition && compositeChildren.length > 0 && !isDragging && typeof document !== "undefined" && (0, import_react_dom.createPortal)(
4776
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
4777
+ "div",
4778
+ {
4779
+ className: "gantt-tr-compositePreview",
4780
+ style: { left: `${compositePreviewPosition.left}px`, top: `${compositePreviewPosition.top}px` },
4781
+ role: "tooltip",
4782
+ children: [
4783
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "gantt-tr-compositePreviewAxis", children: [
4784
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: formatCompositePreviewDate(currentStartDate) }),
4785
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: formatCompositePreviewDate(currentEndDate) })
4786
+ ] }),
4787
+ compositeChildren.slice(0, 12).map((child) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "gantt-tr-compositePreviewRow", children: [
4788
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: child.name }),
4789
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gantt-tr-compositePreviewTrack", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
4790
+ "span",
4791
+ {
4792
+ className: "gantt-tr-compositePreviewBar",
4793
+ style: {
4794
+ left: `${Math.max(0, (parseUTCDate(child.startDate).getTime() - taskStartDate.getTime()) / (taskEndDate.getTime() - taskStartDate.getTime() + 864e5) * 100)}%`,
4795
+ width: `${Math.max(2, (parseUTCDate(child.endDate).getTime() - parseUTCDate(child.startDate).getTime() + 864e5) / (taskEndDate.getTime() - taskStartDate.getTime() + 864e5) * 100)}%`,
4796
+ backgroundColor: barColor
4797
+ }
4798
+ }
4799
+ ) })
4800
+ ] }, child.id)),
4801
+ compositeChildren.length > 12 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
4802
+ "+",
4803
+ compositeChildren.length - 12
4804
+ ] })
4805
+ ]
4806
+ }
4807
+ ),
4808
+ document.body
4809
+ ),
4671
4810
  !disableDependencyEditing && onDependencyPortPointerDown && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
4672
4811
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
4673
4812
  "button",
@@ -4676,8 +4815,8 @@ var TaskRow = import_react3.default.memo(
4676
4815
  "data-gantt-dependency-port": true,
4677
4816
  "data-task-id": task.id,
4678
4817
  "data-port-side": "left",
4679
- className: `gantt-tr-dependencyPort gantt-tr-dependencyPortLeft ${milestone ? "gantt-tr-dependencyPortMilestone" : ""} ${isParent ? "gantt-tr-dependencyPortParent" : ""} ${isDependencyDragActive ? "gantt-tr-dependencyPortDragActive" : ""}`,
4680
- style: { left: `${visualLeft - 21}px` },
4818
+ className: `gantt-tr-dependencyPort gantt-tr-dependencyPortLeft ${milestone ? "gantt-tr-dependencyPortMilestone" : ""} ${isVisualParent ? "gantt-tr-dependencyPortParent" : ""} ${isDependencyDragActive ? "gantt-tr-dependencyPortDragActive" : ""}`,
4819
+ style: { left: `${visualLeft - 24}px` },
4681
4820
  "aria-label": `\u041D\u0430\u0447\u0430\u0442\u044C \u0441\u0432\u044F\u0437\u044C \u043E\u0442 \u043B\u0435\u0432\u043E\u0433\u043E \u043A\u0440\u0430\u044F: ${task.name}`,
4682
4821
  title: "\u041F\u043E\u0442\u044F\u043D\u0438\u0442\u0435 \u043A \u043A\u0440\u0430\u044E \u0434\u0440\u0443\u0433\u043E\u0439 \u043F\u043E\u043B\u043E\u0441\u044B, \u0447\u0442\u043E\u0431\u044B \u0441\u043E\u0437\u0434\u0430\u0442\u044C \u0441\u0432\u044F\u0437\u044C",
4683
4822
  onPointerDown: (event) => onDependencyPortPointerDown(task.id, "left", event)
@@ -4690,7 +4829,7 @@ var TaskRow = import_react3.default.memo(
4690
4829
  "data-gantt-dependency-port": true,
4691
4830
  "data-task-id": task.id,
4692
4831
  "data-port-side": "right",
4693
- className: `gantt-tr-dependencyPort gantt-tr-dependencyPortRight ${milestone ? "gantt-tr-dependencyPortMilestone" : ""} ${isParent ? "gantt-tr-dependencyPortParent" : ""} ${isDependencyDragActive ? "gantt-tr-dependencyPortDragActive" : ""}`,
4832
+ className: `gantt-tr-dependencyPort gantt-tr-dependencyPortRight ${milestone ? "gantt-tr-dependencyPortMilestone" : ""} ${isVisualParent ? "gantt-tr-dependencyPortParent" : ""} ${isDependencyDragActive ? "gantt-tr-dependencyPortDragActive" : ""}`,
4694
4833
  style: { left: `${visualLeft + visualWidth}px` },
4695
4834
  "aria-label": `\u041D\u0430\u0447\u0430\u0442\u044C \u0441\u0432\u044F\u0437\u044C \u043E\u0442 \u043F\u0440\u0430\u0432\u043E\u0433\u043E \u043A\u0440\u0430\u044F: ${task.name}`,
4696
4835
  title: "\u041F\u043E\u0442\u044F\u043D\u0438\u0442\u0435 \u043A \u043A\u0440\u0430\u044E \u0434\u0440\u0443\u0433\u043E\u0439 \u043F\u043E\u043B\u043E\u0441\u044B, \u0447\u0442\u043E\u0431\u044B \u0441\u043E\u0437\u0434\u0430\u0442\u044C \u0441\u0432\u044F\u0437\u044C",
@@ -4729,6 +4868,14 @@ var TaskRow = import_react3.default.memo(
4729
4868
  children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM12 17c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z" })
4730
4869
  }
4731
4870
  ),
4871
+ !showDurationInside && !milestone && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
4872
+ "span",
4873
+ {
4874
+ className: `gantt-tr-externalDuration gantt-tr-durationBeforeBar${compactDetail ? " gantt-tr-compositeDuration" : ""}`,
4875
+ style: { left: `${visualLeft - 46}px`, color: isParent ? task.color || defaultParentBarColor : barColor },
4876
+ children: isVisualParent ? getChildCountLabel(childCount) : `${durationDays} \u0434`
4877
+ }
4878
+ ),
4732
4879
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
4733
4880
  "div",
4734
4881
  {
@@ -4738,7 +4885,6 @@ var TaskRow = import_react3.default.memo(
4738
4885
  color: isParent ? task.color || defaultParentBarColor : barColor
4739
4886
  },
4740
4887
  children: [
4741
- !showDurationInside && !milestone && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gantt-tr-externalDuration", children: isParent ? getChildCountLabel(childCount) : `${durationDays} \u0434` }),
4742
4888
  progressWidth > 0 && !showProgressInside && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "gantt-tr-externalProgress", children: [
4743
4889
  progressWidth,
4744
4890
  "%"
@@ -5042,6 +5188,8 @@ var DependencyLines = import_react7.default.memo(({
5042
5188
  gridWidth,
5043
5189
  totalHeight,
5044
5190
  rowIndexByTaskId,
5191
+ rowTops,
5192
+ rowHeights,
5045
5193
  dragOverrides,
5046
5194
  selectedDep,
5047
5195
  criticalTaskIds,
@@ -5065,7 +5213,8 @@ var DependencyLines = import_react7.default.memo(({
5065
5213
  left: computed.left,
5066
5214
  right: computed.right,
5067
5215
  centerX: computed.centerX,
5068
- rowTop: rowIndex * rowHeight,
5216
+ rowTop: rowTops?.[rowIndex] ?? rowIndex * rowHeight,
5217
+ rowHeight: rowHeights?.[rowIndex] ?? rowHeight,
5069
5218
  isVirtual: false
5070
5219
  });
5071
5220
  });
@@ -5085,6 +5234,7 @@ var DependencyLines = import_react7.default.memo(({
5085
5234
  right: computed.right,
5086
5235
  centerX: computed.centerX,
5087
5236
  rowTop: ancestorPosition.rowTop,
5237
+ rowHeight: ancestorPosition.rowHeight,
5088
5238
  isVirtual: true
5089
5239
  });
5090
5240
  }
@@ -5098,18 +5248,19 @@ var DependencyLines = import_react7.default.memo(({
5098
5248
  const override = dragOverrides?.get(task.id);
5099
5249
  const computed = resolveTaskHorizontalGeometry(task, monthStart, dayWidth, override);
5100
5250
  const rowIndex = rowIndexByTaskId?.get(task.id);
5101
- const rowTop = rowIndex !== void 0 ? rowIndex * rowHeight : 0;
5251
+ const rowTop = rowIndex !== void 0 ? rowTops?.[rowIndex] ?? rowIndex * rowHeight : 0;
5102
5252
  positions.set(task.id, {
5103
5253
  left: computed.left,
5104
5254
  right: computed.right,
5105
5255
  centerX: computed.centerX,
5106
5256
  rowTop,
5257
+ rowHeight: rowIndex !== void 0 ? rowHeights?.[rowIndex] ?? rowHeight : rowHeight,
5107
5258
  isVirtual: false
5108
5259
  });
5109
5260
  }
5110
5261
  }
5111
5262
  return { taskPositions: positions, taskIndices: indices, hiddenTaskIds: hidden };
5112
- }, [tasks, tasksForPositions, allTasks, collapsedParentIds, monthStart, dayWidth, rowHeight, dragOverrides, rowIndexByTaskId]);
5263
+ }, [tasks, tasksForPositions, allTasks, collapsedParentIds, monthStart, dayWidth, rowHeight, rowTops, rowHeights, dragOverrides, rowIndexByTaskId]);
5113
5264
  const tasksForCycleDetection = allTasks ?? tasks;
5114
5265
  const cycleInfo = (0, import_react7.useMemo)(() => {
5115
5266
  const result = detectCycles(tasksForCycleDetection);
@@ -5151,7 +5302,9 @@ var DependencyLines = import_react7.default.memo(({
5151
5302
  continue;
5152
5303
  }
5153
5304
  }
5154
- if (!renderedTaskIdSet.has(edge.predecessorId) && !renderedTaskIdSet.has(edge.successorId)) {
5305
+ const predecessorVisibleId = predecessor.isVirtual ? findVisibleAncestor(taskMap.get(edge.predecessorId), collapsedParentIds, taskMap)?.id : edge.predecessorId;
5306
+ const successorVisibleId = successor.isVirtual ? findVisibleAncestor(taskMap.get(edge.successorId), collapsedParentIds, taskMap)?.id : edge.successorId;
5307
+ if (!renderedTaskIdSet.has(predecessorVisibleId ?? "") && !renderedTaskIdSet.has(successorVisibleId ?? "")) {
5155
5308
  continue;
5156
5309
  }
5157
5310
  const isVirtual = predecessor.isVirtual || successor.isVirtual;
@@ -5165,9 +5318,9 @@ var DependencyLines = import_react7.default.memo(({
5165
5318
  let toY;
5166
5319
  if (reverseOrder) {
5167
5320
  fromY = predecessor.rowTop + 10;
5168
- toY = successor.rowTop + rowHeight - 6;
5321
+ toY = successor.rowTop + successor.rowHeight - 6;
5169
5322
  } else {
5170
- fromY = predecessor.rowTop + rowHeight - 10;
5323
+ fromY = predecessor.rowTop + predecessor.rowHeight - 10;
5171
5324
  toY = successor.rowTop + 6;
5172
5325
  }
5173
5326
  let fromX = predecessorTask && isMilestoneTask(predecessorTask) ? predecessor.centerX : edge.type === "SS" || edge.type === "SF" ? predecessor.left : predecessor.right;
@@ -5394,18 +5547,18 @@ var LINK_TYPE_LABELS_RU = {
5394
5547
  FF: "\u041E\u041E",
5395
5548
  SF: "\u041D\u041E"
5396
5549
  };
5550
+ var fmt = (n) => Math.round(n * 100) / 100;
5397
5551
  function getPath(drag) {
5398
5552
  const from = drag.source;
5399
5553
  const to = drag.current;
5400
5554
  const direction = to.y >= from.y ? 1 : -1;
5401
5555
  const bend = Math.max(18, Math.abs(to.y - from.y) * 0.45);
5402
- const middleX = Math.round((from.x + to.x) / 2);
5403
5556
  if (Math.abs(to.y - from.y) < 2) {
5404
- return `M ${Math.round(from.x)} ${Math.round(from.y)} H ${Math.round(to.x)}`;
5557
+ return `M ${fmt(from.x)} ${fmt(from.y)} H ${fmt(to.x)}`;
5405
5558
  }
5406
5559
  return [
5407
- `M ${Math.round(from.x)} ${Math.round(from.y)}`,
5408
- `C ${Math.round(from.x)} ${Math.round(from.y + bend * direction)}, ${Math.round(to.x)} ${Math.round(to.y - bend * direction)}, ${Math.round(to.x)} ${Math.round(to.y)}`
5560
+ `M ${fmt(from.x)} ${fmt(from.y)}`,
5561
+ `C ${fmt(from.x)} ${fmt(from.y + bend * direction)}, ${fmt(to.x)} ${fmt(to.y - bend * direction)}, ${fmt(to.x)} ${fmt(to.y)}`
5409
5562
  ].join(" ");
5410
5563
  }
5411
5564
  var DependencyCreationOverlay = ({ drag, width, height }) => {
@@ -6845,6 +6998,7 @@ var TaskListRow = import_react11.default.memo(
6845
6998
  taskListMenuCommands = [],
6846
6999
  hideTaskListRowActions = false,
6847
7000
  rowClassName,
7001
+ compositeParentColor,
6848
7002
  getTaskListNamePrefixIcon,
6849
7003
  taskDateChangeMode = "preserve-duration",
6850
7004
  onTaskDateChangeModeChange,
@@ -6929,6 +7083,35 @@ var TaskListRow = import_react11.default.memo(
6929
7083
  [businessDays, weekendPredicate]
6930
7084
  );
6931
7085
  const isCollapsed = collapsedParentIds.has(task.id);
7086
+ const [compositeIconMode, setCompositeIconMode] = (0, import_react11.useState)(
7087
+ () => isCollapsed ? "composite" : "down"
7088
+ );
7089
+ const compositeIconModeRef = (0, import_react11.useRef)(compositeIconMode);
7090
+ const [compositeIconAnimation, setCompositeIconAnimation] = (0, import_react11.useState)(null);
7091
+ const [compositeIconAnimationPhase, setCompositeIconAnimationPhase] = (0, import_react11.useState)(null);
7092
+ (0, import_react11.useEffect)(() => {
7093
+ const nextMode = isCollapsed ? "composite" : "down";
7094
+ if (nextMode === compositeIconModeRef.current) {
7095
+ setCompositeIconAnimation(null);
7096
+ setCompositeIconAnimationPhase(null);
7097
+ return;
7098
+ }
7099
+ setCompositeIconAnimation(isCollapsed ? "closing" : "opening");
7100
+ setCompositeIconAnimationPhase("outgoing");
7101
+ const swapTimeoutId = window.setTimeout(() => {
7102
+ compositeIconModeRef.current = nextMode;
7103
+ setCompositeIconMode(nextMode);
7104
+ setCompositeIconAnimationPhase("incoming");
7105
+ }, 120);
7106
+ const finishTimeoutId = window.setTimeout(() => {
7107
+ setCompositeIconAnimation(null);
7108
+ setCompositeIconAnimationPhase(null);
7109
+ }, 240);
7110
+ return () => {
7111
+ window.clearTimeout(swapTimeoutId);
7112
+ window.clearTimeout(finishTimeoutId);
7113
+ };
7114
+ }, [isCollapsed]);
6932
7115
  const getHierarchyLineColor = (0, import_react11.useCallback)((columnDepth) => {
6933
7116
  return columnDepth % 2 === 0 ? "#93c5fd" : "var(--gantt-hierarchy-line-color)";
6934
7117
  }, []);
@@ -7037,6 +7220,7 @@ var TaskListRow = import_react11.default.memo(
7037
7220
  );
7038
7221
  const handleNameDoubleClick = (0, import_react11.useCallback)(
7039
7222
  (e) => {
7223
+ if (task.composite) return;
7040
7224
  if (disableTaskNameEditing) return;
7041
7225
  e.stopPropagation();
7042
7226
  nameConfirmedRef.current = false;
@@ -7044,7 +7228,7 @@ var TaskListRow = import_react11.default.memo(
7044
7228
  setNameValue(task.name);
7045
7229
  setEditingColumnId("name");
7046
7230
  },
7047
- [task.name, disableTaskNameEditing]
7231
+ [task.name, task.composite, disableTaskNameEditing]
7048
7232
  );
7049
7233
  const handleRowKeyDown = (0, import_react11.useCallback)(
7050
7234
  (e) => {
@@ -7693,7 +7877,7 @@ var TaskListRow = import_react11.default.memo(
7693
7877
  ]
7694
7878
  }
7695
7879
  );
7696
- const nameTriggerPaddingLeft = isParent ? `${nestingDepth * 20 + 28}px` : nestingDepth > 0 ? `${nestingDepth * 20 + 8}px` : void 0;
7880
+ const nameTriggerPaddingLeft = isParent && !task.composite ? `${nestingDepth * 20 + 28}px` : nestingDepth > 0 ? `${nestingDepth * 20 + 8}px` : void 0;
7697
7881
  const nameInputPaddingLeft = nestingDepth > 0 ? `${nestingDepth * 20 + 8}px` : void 0;
7698
7882
  const nestedDropIndicatorLeft = (() => {
7699
7883
  const numberColumnWidth = resolvedColumns?.find((col) => col.id === "number")?.width ?? 40;
@@ -7791,36 +7975,35 @@ var TaskListRow = import_react11.default.memo(
7791
7975
  }
7792
7976
  )
7793
7977
  ] }),
7794
- isParent && !editingName && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
7795
- !isFilterHideMode && !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
7796
- "span",
7797
- {
7798
- style: {
7799
- position: "absolute",
7800
- left: `${nestingDepth * 20 + 9}px`,
7801
- top: `${rowHeight / 2 + 7}px`,
7802
- height: `${rowHeight / 2 - 7}px`,
7803
- width: "1.5px",
7804
- background: getHierarchyLineColor(nestingDepth),
7805
- borderRadius: "1px",
7806
- pointerEvents: "none"
7807
- }
7808
- }
7809
- ),
7810
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
7811
- "button",
7812
- {
7813
- type: "button",
7814
- className: `gantt-tl-collapse-btn ${isCollapsed ? "gantt-tl-collapse-btn-collapsed" : ""}`,
7815
- onClick: handleToggleCollapse,
7816
- style: { left: `${nestingDepth * 20 + 1}px` },
7817
- "aria-label": isCollapsed ? "Expand children" : "Collapse children",
7818
- children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ChevronRightIcon, {})
7978
+ task.composite && !isFilterHideMode && !isCollapsed && !editingName && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
7979
+ "span",
7980
+ {
7981
+ "data-testid": "gantt-tl-parent-connector-tail",
7982
+ style: {
7983
+ position: "absolute",
7984
+ left: `${nestingDepth * 20 + 9}px`,
7985
+ top: `${rowHeight / 2 + 7}px`,
7986
+ height: `${rowHeight / 2 - 7}px`,
7987
+ width: "1.5px",
7988
+ background: getHierarchyLineColor(nestingDepth),
7989
+ borderRadius: "1px",
7990
+ pointerEvents: "none"
7819
7991
  }
7820
- )
7821
- ] })
7992
+ }
7993
+ ),
7994
+ isParent && !task.composite && !editingName && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_jsx_runtime14.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
7995
+ "button",
7996
+ {
7997
+ type: "button",
7998
+ className: `gantt-tl-collapse-btn ${isCollapsed ? "gantt-tl-collapse-btn-collapsed" : ""}`,
7999
+ onClick: handleToggleCollapse,
8000
+ style: { left: `${nestingDepth * 20 - 2}px` },
8001
+ "aria-label": isCollapsed ? "Expand children" : "Collapse children",
8002
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ChevronRightIcon, {})
8003
+ }
8004
+ ) })
7822
8005
  ] }),
7823
- !isChild && isParent && !editingName && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
8006
+ !isChild && isParent && !task.composite && !editingName && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
7824
8007
  !isFilterHideMode && !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
7825
8008
  "span",
7826
8009
  {
@@ -7843,7 +8026,7 @@ var TaskListRow = import_react11.default.memo(
7843
8026
  type: "button",
7844
8027
  className: `gantt-tl-collapse-btn ${isCollapsed ? "gantt-tl-collapse-btn-collapsed" : ""}`,
7845
8028
  onClick: handleToggleCollapse,
7846
- style: { left: `${nestingDepth * 20 + 1}px` },
8029
+ style: { left: `${nestingDepth * 20 - 2}px` },
7847
8030
  "aria-label": isCollapsed ? "Expand children" : "Collapse children",
7848
8031
  children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ChevronRightIcon, {})
7849
8032
  }
@@ -7862,27 +8045,57 @@ var TaskListRow = import_react11.default.memo(
7862
8045
  style: { paddingLeft: nameInputPaddingLeft },
7863
8046
  onClick: (e) => e.stopPropagation()
7864
8047
  }
7865
- ) : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
7866
- "button",
7867
- {
7868
- type: "button",
7869
- className: [
7870
- "gantt-tl-name-trigger",
7871
- disableTaskNameEditing ? "gantt-tl-name-locked" : ""
7872
- ].filter(Boolean).join(" "),
7873
- title: task.name,
7874
- onClick: handleNameClick,
7875
- onDoubleClick: handleNameDoubleClick,
7876
- style: {
7877
- paddingLeft: nameTriggerPaddingLeft,
7878
- paddingRight: task.color ? "20px" : void 0
7879
- },
7880
- children: [
7881
- taskListNamePrefixIcon ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "gantt-tl-name-trigger-icon", "aria-hidden": "true", children: taskListNamePrefixIcon }) : null,
7882
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "gantt-tl-name-trigger-text", children: task.name })
7883
- ]
7884
- }
7885
- ),
8048
+ ) : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
8049
+ task.composite && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
8050
+ "button",
8051
+ {
8052
+ type: "button",
8053
+ className: "gantt-tl-compositeToggle",
8054
+ onClick: handleToggleCollapse,
8055
+ style: { left: `${nestingDepth * 20 - 3}px` },
8056
+ "aria-label": isCollapsed ? `\u0420\u0430\u0441\u043A\u0440\u044B\u0442\u044C ${task.name}` : `\u0421\u0432\u0435\u0440\u043D\u0443\u0442\u044C ${task.name}`,
8057
+ "aria-expanded": !isCollapsed,
8058
+ title: isCollapsed ? "\u0420\u0430\u0441\u043A\u0440\u044B\u0442\u044C \u044D\u0442\u0430\u0436\u0438" : "\u0421\u0432\u0435\u0440\u043D\u0443\u0442\u044C \u044D\u0442\u0430\u0436\u0438",
8059
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
8060
+ "svg",
8061
+ {
8062
+ className: compositeIconAnimation && compositeIconAnimationPhase ? `gantt-tl-compositeGlyph-${compositeIconMode}-${compositeIconAnimation}-${compositeIconAnimationPhase}` : void 0,
8063
+ viewBox: "0 0 24 24",
8064
+ fill: "none",
8065
+ stroke: "currentColor",
8066
+ strokeWidth: "2",
8067
+ strokeLinecap: "round",
8068
+ strokeLinejoin: "round",
8069
+ "aria-hidden": "true",
8070
+ children: compositeIconMode === "composite" ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
8071
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("path", { d: "M21 5H11" }),
8072
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("path", { d: "M21 12H11" }),
8073
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("path", { d: "M21 19H11" }),
8074
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("path", { d: "m3 8 4 4-4 4" })
8075
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("path", { d: "m6 9 6 6 6-6" })
8076
+ }
8077
+ )
8078
+ }
8079
+ ),
8080
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
8081
+ "button",
8082
+ {
8083
+ type: "button",
8084
+ className: `gantt-tl-name-trigger${disableTaskNameEditing ? " gantt-tl-name-locked" : ""}`,
8085
+ title: task.name,
8086
+ onClick: handleNameClick,
8087
+ onDoubleClick: handleNameDoubleClick,
8088
+ style: {
8089
+ paddingLeft: task.composite ? `${nestingDepth * 20 + 30}px` : nameTriggerPaddingLeft,
8090
+ paddingRight: task.color ? "20px" : void 0
8091
+ },
8092
+ children: [
8093
+ taskListNamePrefixIcon ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "gantt-tl-name-trigger-icon", "aria-hidden": "true", children: taskListNamePrefixIcon }) : null,
8094
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "gantt-tl-name-trigger-text", children: task.name })
8095
+ ]
8096
+ }
8097
+ )
8098
+ ] }),
7886
8099
  !editingName && task.color && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
7887
8100
  "span",
7888
8101
  {
@@ -8499,8 +8712,9 @@ var TaskListRow = import_react11.default.memo(
8499
8712
  isDragOver && isNestedDropTarget ? "gantt-tl-row-drag-over-nested" : "",
8500
8713
  isDragOver && isDirectChildDropTarget ? "gantt-tl-row-drag-over-direct-child" : "",
8501
8714
  isChild ? "gantt-tl-row-child" : "",
8502
- isParent ? "gantt-tl-row-parent" : "",
8503
- isParent && fillParentRow ? "gantt-tl-row-parent-filled" : "",
8715
+ isParent && !task.composite ? "gantt-tl-row-parent" : "",
8716
+ task.composite ? "gantt-tl-row-composite" : "",
8717
+ isParent && !task.composite && fillParentRow ? "gantt-tl-row-parent-filled" : "",
8504
8718
  `gantt-tl-row-level-${rowFillLevel}`,
8505
8719
  isTotalRow ? "gantt-tl-row-total" : "",
8506
8720
  rowClassName ?? ""
@@ -8508,6 +8722,7 @@ var TaskListRow = import_react11.default.memo(
8508
8722
  style: {
8509
8723
  height: `${rowHeight}px`,
8510
8724
  position: "relative",
8725
+ "--gantt-tl-composite-detail-color": compositeParentColor ?? "var(--gantt-task-bar-default-color, #8b5cf6)",
8511
8726
  "--gantt-tl-nested-drop-left": nestedDropIndicatorLeft,
8512
8727
  "--gantt-tl-nested-inside-drop-left": nestedInsideDropIndicatorLeft
8513
8728
  },
@@ -8733,6 +8948,7 @@ var GenerationSkeletonRows = ({
8733
8948
  rowHeight,
8734
8949
  variant,
8735
8950
  startIndex = 0,
8951
+ startTop,
8736
8952
  taskListLayout,
8737
8953
  chartDayWidth = 40,
8738
8954
  chartStartDayOffset = 0,
@@ -8773,7 +8989,7 @@ var GenerationSkeletonRows = ({
8773
8989
  "div",
8774
8990
  {
8775
8991
  className: "gantt-generation-skeleton-row",
8776
- style: { top: `${rowIndex * rowHeight}px`, height: `${rowHeight}px` },
8992
+ style: { top: `${(startTop ?? startIndex * rowHeight) + index * rowHeight}px`, height: `${rowHeight}px` },
8777
8993
  children: variant === "task-list" ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_jsx_runtime16.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: `gantt-generation-skeleton-label ${shortShape ? "gantt-generation-skeleton-label-short" : ""} ${mediumShape ? "gantt-generation-skeleton-label-medium" : ""}` }) }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: `gantt-generation-skeleton-bar ${parentShape ? "gantt-generation-skeleton-bar-parent" : ""} ${shortShape ? "gantt-generation-skeleton-bar-short" : ""}`, style: chartBarStyle })
8778
8994
  },
8779
8995
  rowIndex
@@ -9033,6 +9249,8 @@ var SelectAllCheckbox = ({
9033
9249
  var TaskList = ({
9034
9250
  tasks,
9035
9251
  rowHeight,
9252
+ rowTops,
9253
+ rowHeights,
9036
9254
  headerHeight,
9037
9255
  taskListWidth,
9038
9256
  onTasksChange,
@@ -9160,14 +9378,22 @@ var TaskList = ({
9160
9378
  return tasks2;
9161
9379
  }, [orderedTasks, collapsedParentIds, filterMode, filteredTaskIds, isFilterActive, criticalPathMode, criticalTaskIds]);
9162
9380
  const totalHeight = (0, import_react14.useMemo)(
9163
- () => (visibleTasks.length + Math.max(0, Math.floor(skeletonRowCount))) * rowHeight,
9164
- [rowHeight, skeletonRowCount, visibleTasks.length]
9381
+ () => (rowTops && rowHeights && visibleTasks.length > 0 ? rowTops[visibleTasks.length - 1] + rowHeights[visibleTasks.length - 1] : visibleTasks.length * rowHeight) + Math.max(0, Math.floor(skeletonRowCount)) * rowHeight,
9382
+ [rowHeight, rowTops, rowHeights, skeletonRowCount, visibleTasks.length]
9165
9383
  );
9166
9384
  const visibleTaskNumberMap = (0, import_react14.useMemo)(
9167
9385
  () => Object.fromEntries(buildTaskNumberMap(visibleTasks)),
9168
9386
  [visibleTasks]
9169
9387
  );
9170
9388
  const visibleTaskIds = (0, import_react14.useMemo)(() => visibleTasks.map((task) => task.id), [visibleTasks]);
9389
+ const compositeParentIds = (0, import_react14.useMemo)(
9390
+ () => new Set(tasks.filter((task) => task.composite).map((task) => task.id)),
9391
+ [tasks]
9392
+ );
9393
+ const compositeParentsById = (0, import_react14.useMemo)(
9394
+ () => new Map(tasks.filter((task) => task.composite).map((task) => [task.id, task])),
9395
+ [tasks]
9396
+ );
9171
9397
  const renderedVisibleRowIndices = (0, import_react14.useMemo)(
9172
9398
  () => visibleRowIndices ?? visibleTasks.map((_, index) => index),
9173
9399
  [visibleRowIndices, visibleTasks]
@@ -10159,7 +10385,7 @@ var TaskList = ({
10159
10385
  renderedVisibleTasks.map(({ task, index }) => {
10160
10386
  const previousVisibleTask = index > 0 ? visibleTasks[index - 1] : void 0;
10161
10387
  const canDemoteTask = index === 0 || !task.parentId || previousVisibleTask?.id !== task.parentId;
10162
- const topOffset = index * rowHeight + (hasVisiblePendingInsert && index > pendingInsertRowIndex ? rowHeight : 0);
10388
+ const topOffset = (rowTops?.[index] ?? index * rowHeight) + (hasVisiblePendingInsert && index > pendingInsertRowIndex ? rowHeight : 0);
10163
10389
  return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
10164
10390
  "div",
10165
10391
  {
@@ -10176,7 +10402,7 @@ var TaskList = ({
10176
10402
  rowIndex: index,
10177
10403
  taskNumber: originalTaskNumberMap[task.id] || "",
10178
10404
  taskNumberMap: originalTaskNumberMap,
10179
- rowHeight,
10405
+ rowHeight: rowHeights?.[index] ?? rowHeight,
10180
10406
  onTasksChange,
10181
10407
  onDurationChange,
10182
10408
  selectedTaskId,
@@ -10233,7 +10459,8 @@ var TaskList = ({
10233
10459
  onActiveCustomCellChange: setActiveCustomCell,
10234
10460
  taskListMenuCommands,
10235
10461
  hideTaskListRowActions,
10236
- rowClassName: getTaskListRowClassName?.(task),
10462
+ rowClassName: [task.parentId && compositeParentIds.has(task.parentId) ? "gantt-tl-row-composite-detail" : "", getTaskListRowClassName?.(task)].filter(Boolean).join(" "),
10463
+ compositeParentColor: task.parentId ? compositeParentsById.get(task.parentId)?.color : void 0,
10237
10464
  getTaskListNamePrefixIcon,
10238
10465
  taskDateChangeMode,
10239
10466
  onTaskDateChangeModeChange,
@@ -10249,7 +10476,7 @@ var TaskList = ({
10249
10476
  {
10250
10477
  style: {
10251
10478
  position: "absolute",
10252
- top: `${(pendingInsertRowIndex + 1) * rowHeight}px`,
10479
+ top: `${rowTops?.[pendingInsertRowIndex + 1] ?? (pendingInsertRowIndex + 1) * rowHeight}px`,
10253
10480
  left: 0,
10254
10481
  right: 0
10255
10482
  },
@@ -10271,6 +10498,7 @@ var TaskList = ({
10271
10498
  rowHeight,
10272
10499
  variant: "task-list",
10273
10500
  startIndex: visibleTasks.length,
10501
+ startTop: totalHeight - Math.max(0, Math.floor(skeletonRowCount)) * rowHeight,
10274
10502
  taskListLayout: generationSkeletonTaskListLayout
10275
10503
  }
10276
10504
  )
@@ -14297,10 +14525,14 @@ function TaskGanttChartInner(props, ref) {
14297
14525
  const rect = element.getBoundingClientRect();
14298
14526
  const side = element.getAttribute("data-port-side");
14299
14527
  return {
14300
- // The port hit-area is deliberately outside the bar. Anchor the preview
14301
- // to the actual bar boundary instead of the center of that outside zone.
14302
- x: Math.round((side === "left" ? rect.right - 6 : rect.left + 6) - chartRect.left),
14303
- y: Math.round(rect.top + rect.height / 2 - chartRect.top)
14528
+ // Anchor the preview at the port dot's center so the dragged polyline
14529
+ // ends exactly on the link handle. The dot core is 8px and sits 4px
14530
+ // inside the 24px zone from the bar side (see TaskRow.css), which puts
14531
+ // its center 8px off the button edge adjacent to the bar. No rounding:
14532
+ // the CSS ball centers at a fractional position, and a rounded SVG
14533
+ // coordinate landed ~1px off it.
14534
+ x: (side === "left" ? rect.right - 8 : rect.left + 8) - chartRect.left,
14535
+ y: rect.top + rect.height / 2 - chartRect.top
14304
14536
  };
14305
14537
  }, []);
14306
14538
  const getVisualDependencyType = (0, import_react19.useCallback)((sourceSide, targetSide) => {
@@ -14451,7 +14683,7 @@ function TaskGanttChartInner(props, ref) {
14451
14683
  }, [dependencyLineMenu, onTasksChange, tasks]);
14452
14684
  const [activeTimelineTooltip, setActiveTimelineTooltip] = (0, import_react19.useState)(null);
14453
14685
  const [internalCollapsedParentIds, setInternalCollapsedParentIds] = (0, import_react19.useState)(/* @__PURE__ */ new Set());
14454
- const collapsedParentIds = externalCollapsedParentIds ?? internalCollapsedParentIds;
14686
+ const [expandedCompositeIds, setExpandedCompositeIds] = (0, import_react19.useState)(/* @__PURE__ */ new Set());
14455
14687
  const [editingTaskId, setEditingTaskId] = (0, import_react19.useState)(null);
14456
14688
  const taskDateChangeMode = externalTaskDateChangeMode ?? internalTaskDateChangeMode;
14457
14689
  const handleTaskDateChangeMode = externalOnTaskDateChangeModeChange ?? setInternalTaskDateChangeMode;
@@ -14461,7 +14693,36 @@ function TaskGanttChartInner(props, ref) {
14461
14693
  () => Math.max(rowHeight, 10 + resolvedRowContentLines * 18),
14462
14694
  [resolvedRowContentLines, rowHeight]
14463
14695
  );
14464
- const normalizedTasks = (0, import_react19.useMemo)(() => normalizeHierarchyTasks(tasks), [tasks]);
14696
+ const normalizedTasks = (0, import_react19.useMemo)(() => {
14697
+ const ordered = normalizeHierarchyTasks(tasks);
14698
+ const ranges = /* @__PURE__ */ new Map();
14699
+ for (const task of ordered) {
14700
+ if (!task.parentId) continue;
14701
+ const start = parseUTCDate(task.startDate).getTime();
14702
+ const end = parseUTCDate(task.endDate).getTime();
14703
+ const range = ranges.get(task.parentId);
14704
+ ranges.set(task.parentId, {
14705
+ start: Math.min(range?.start ?? start, start),
14706
+ end: Math.max(range?.end ?? end, end)
14707
+ });
14708
+ }
14709
+ return ordered.map((task) => {
14710
+ const range = task.composite ? ranges.get(task.id) : void 0;
14711
+ return range ? {
14712
+ ...task,
14713
+ startDate: new Date(range.start).toISOString().slice(0, 10),
14714
+ endDate: new Date(range.end).toISOString().slice(0, 10)
14715
+ } : task;
14716
+ });
14717
+ }, [tasks]);
14718
+ const collapsedParentIds = (0, import_react19.useMemo)(() => {
14719
+ if (externalCollapsedParentIds) return externalCollapsedParentIds;
14720
+ const ids = new Set(internalCollapsedParentIds);
14721
+ for (const task of normalizedTasks) {
14722
+ if (task.composite && !expandedCompositeIds.has(task.id)) ids.add(task.id);
14723
+ }
14724
+ return ids;
14725
+ }, [externalCollapsedParentIds, internalCollapsedParentIds, normalizedTasks, expandedCompositeIds]);
14465
14726
  const controlledScheduleIdentity = (0, import_react19.useMemo)(
14466
14727
  () => buildControlledScheduleIdentity(normalizedTasks),
14467
14728
  [normalizedTasks]
@@ -14541,6 +14802,23 @@ function TaskGanttChartInner(props, ref) {
14541
14802
  }
14542
14803
  return tasks2;
14543
14804
  }, [normalizedTasks, collapsedParentIds, filterMode, taskFilter, criticalPathMode, criticalTaskIds]);
14805
+ const compositeRowLayout = (0, import_react19.useMemo)(() => {
14806
+ const compositeIds = new Set(normalizedTasks.filter((task) => task.composite).map((task) => task.id));
14807
+ const tops = [];
14808
+ const heights = [];
14809
+ let top = 0;
14810
+ for (const task of visibleTasks) {
14811
+ tops.push(top);
14812
+ const height = task.parentId && compositeIds.has(task.parentId) ? Math.min(effectiveRowHeight, 22) : effectiveRowHeight;
14813
+ heights.push(height);
14814
+ top += height;
14815
+ }
14816
+ return { tops, heights, totalHeight: top };
14817
+ }, [normalizedTasks, visibleTasks, effectiveRowHeight]);
14818
+ const compositeParentsById = (0, import_react19.useMemo)(
14819
+ () => new Map(normalizedTasks.filter((task) => task.composite).map((task) => [task.id, task])),
14820
+ [normalizedTasks]
14821
+ );
14544
14822
  const matchedTaskIds = (0, import_react19.useMemo)(() => {
14545
14823
  if (!taskFilter) return /* @__PURE__ */ new Set();
14546
14824
  return new Set(visibleTasks.filter(taskFilter).map((task) => task.id));
@@ -14562,8 +14840,8 @@ function TaskGanttChartInner(props, ref) {
14562
14840
  return mergedHighlightedTaskIds;
14563
14841
  }, [filterMode, highlightedTaskIds, matchedTaskIds, criticalPathMode, criticalTaskIds]);
14564
14842
  const totalGridHeight = (0, import_react19.useMemo)(
14565
- () => (visibleTasks.length + Math.max(0, Math.floor(skeletonRowCount))) * effectiveRowHeight,
14566
- [effectiveRowHeight, skeletonRowCount, visibleTasks.length]
14843
+ () => compositeRowLayout.totalHeight + Math.max(0, Math.floor(skeletonRowCount)) * effectiveRowHeight,
14844
+ [compositeRowLayout.totalHeight, effectiveRowHeight, skeletonRowCount]
14567
14845
  );
14568
14846
  const timelineHeaderHeight = headerHeight + 1;
14569
14847
  const tableBodyMinHeight = (0, import_react19.useMemo)(() => {
@@ -14690,7 +14968,7 @@ function TaskGanttChartInner(props, ref) {
14690
14968
  const rowIndex = visibleTasks.findIndex((visibleTask) => visibleTask.id === taskId);
14691
14969
  if (rowIndex === -1) return;
14692
14970
  const paddedRowIndex = Math.max(0, rowIndex - SCROLL_TO_ROW_CONTEXT_ROWS);
14693
- container2.scrollTo({ top: Math.max(0, effectiveRowHeight * paddedRowIndex), behavior: "smooth" });
14971
+ container2.scrollTo({ top: Math.max(0, compositeRowLayout.tops[paddedRowIndex] ?? 0), behavior: "smooth" });
14694
14972
  setSelectedTaskId(taskId);
14695
14973
  return;
14696
14974
  }
@@ -14709,7 +14987,7 @@ function TaskGanttChartInner(props, ref) {
14709
14987
  const taskOffset = taskIndex * dayWidth;
14710
14988
  const scrollLeft = Math.round(taskOffset - dayWidth * 2);
14711
14989
  container.scrollTo({ left: Math.max(0, scrollLeft), behavior: "smooth" });
14712
- }, [dateRange, dayWidth, effectiveRowHeight, isTableMatrixMode, tasks, visibleTasks]);
14990
+ }, [dateRange, dayWidth, effectiveRowHeight, compositeRowLayout, isTableMatrixMode, tasks, visibleTasks]);
14713
14991
  const scrollToRow = (0, import_react19.useCallback)((taskId, options = {}) => {
14714
14992
  const container = scrollContainerRef.current;
14715
14993
  if (!container) return;
@@ -14718,7 +14996,7 @@ function TaskGanttChartInner(props, ref) {
14718
14996
  const rowIndex = visibleTasks.findIndex((visibleTask) => visibleTask.id === task.id);
14719
14997
  if (rowIndex === -1) return;
14720
14998
  const paddedRowIndex = Math.max(0, rowIndex - SCROLL_TO_ROW_CONTEXT_ROWS);
14721
- const scrollTop = Math.max(0, effectiveRowHeight * paddedRowIndex);
14999
+ const scrollTop = Math.max(0, compositeRowLayout.tops[paddedRowIndex] ?? 0);
14722
15000
  const {
14723
15001
  select = true,
14724
15002
  behavior = "smooth",
@@ -14738,7 +15016,7 @@ function TaskGanttChartInner(props, ref) {
14738
15016
  }
14739
15017
  }
14740
15018
  container.scrollTo({ top: scrollTop, behavior });
14741
- }, [effectiveRowHeight, tasks, visibleTasks]);
15019
+ }, [effectiveRowHeight, compositeRowLayout, tasks, visibleTasks]);
14742
15020
  const [dragGuideLines, setDragGuideLines] = (0, import_react19.useState)(null);
14743
15021
  const [draggedTaskOverride, setDraggedTaskOverride] = (0, import_react19.useState)(null);
14744
15022
  (0, import_react19.useEffect)(() => {
@@ -14765,9 +15043,9 @@ function TaskGanttChartInner(props, ref) {
14765
15043
  previewPositionStore.clear();
14766
15044
  }, [previewPositionStore]);
14767
15045
  const handleTaskChange = (0, import_react19.useCallback)((updatedTasks, durationEdit) => {
14768
- const updatedTask = updatedTasks[0];
15046
+ let updatedTask = updatedTasks[0];
14769
15047
  if (!updatedTask) return;
14770
- const originalTask = tasks.find((t) => t.id === updatedTask.id);
15048
+ const originalTask = normalizedTasks.find((t) => t.id === updatedTask.id);
14771
15049
  if (!originalTask) {
14772
15050
  onTasksChange?.(updatedTasks);
14773
15051
  if (editingTaskId === updatedTask.id) {
@@ -14780,6 +15058,8 @@ function TaskGanttChartInner(props, ref) {
14780
15058
  const newStart = new Date(updatedTask.startDate);
14781
15059
  const newEnd = new Date(updatedTask.endDate);
14782
15060
  const datesChanged = origStart.getTime() !== newStart.getTime() || origEnd.getTime() !== newEnd.getTime();
15061
+ const isCompositeDetail = datesChanged && tasks.some((task) => task.id === updatedTask.parentId && task.composite);
15062
+ if (isCompositeDetail) updatedTask = { ...updatedTask, synced: false };
14783
15063
  if (!datesChanged) {
14784
15064
  if (updatedTasks.length > 1) {
14785
15065
  onTasksChange?.(updatedTasks);
@@ -14841,7 +15121,7 @@ function TaskGanttChartInner(props, ref) {
14841
15121
  }
14842
15122
  return;
14843
15123
  }
14844
- const movedParentCascade = disableConstraints ? [updatedTask] : universalCascade(updatedTask, newStart, newEnd, tasks, businessDays, isCustomWeekend);
15124
+ const movedParentCascade = disableConstraints && !originalTask.composite ? [updatedTask] : universalCascade(updatedTask, newStart, newEnd, normalizedTasks, businessDays, isCustomWeekend);
14845
15125
  if (onScheduleIntent) {
14846
15126
  onScheduleIntent({
14847
15127
  type: "move_task",
@@ -14865,7 +15145,8 @@ function TaskGanttChartInner(props, ref) {
14865
15145
  taskId: updatedTask.id,
14866
15146
  duration: durationEdit.duration,
14867
15147
  anchor: durationEdit.anchor,
14868
- taskType: updatedTask.type ?? "task"
15148
+ taskType: updatedTask.type ?? "task",
15149
+ ...isCompositeDetail ? { localOverride: true } : {}
14869
15150
  });
14870
15151
  } else {
14871
15152
  onTasksChange?.(cascadedTasks);
@@ -14877,7 +15158,8 @@ function TaskGanttChartInner(props, ref) {
14877
15158
  onScheduleIntent({
14878
15159
  type: "move_task",
14879
15160
  taskId: updatedTask.id,
14880
- startDate: newStart.toISOString().split("T")[0]
15161
+ startDate: newStart.toISOString().split("T")[0],
15162
+ ...isCompositeDetail ? { localOverride: true } : {}
14881
15163
  });
14882
15164
  } else {
14883
15165
  const anchor = startChanged ? "end" : "start";
@@ -14885,13 +15167,14 @@ function TaskGanttChartInner(props, ref) {
14885
15167
  type: "resize_task",
14886
15168
  taskId: updatedTask.id,
14887
15169
  anchor,
14888
- date: (startChanged ? newStart : newEnd).toISOString().split("T")[0]
15170
+ date: (startChanged ? newStart : newEnd).toISOString().split("T")[0],
15171
+ ...isCompositeDetail ? { localOverride: true } : {}
14889
15172
  });
14890
15173
  }
14891
15174
  } else {
14892
15175
  onTasksChange?.(cascadedTasks);
14893
15176
  }
14894
- }, [tasks, onTasksChange, onScheduleIntent, disableConstraints, editingTaskId, businessDays, isCustomWeekend, onSubtreeScaleResult]);
15177
+ }, [tasks, normalizedTasks, onTasksChange, onScheduleIntent, disableConstraints, editingTaskId, businessDays, isCustomWeekend, onSubtreeScaleResult]);
14895
15178
  const handleTaskDurationChange = (0, import_react19.useCallback)((task, duration) => {
14896
15179
  const updatedTask = {
14897
15180
  ...task,
@@ -15005,12 +15288,25 @@ function TaskGanttChartInner(props, ref) {
15005
15288
  if (scrollViewport.viewportHeight <= 0) {
15006
15289
  return Array.from({ length: totalTasks }, (_, index) => index);
15007
15290
  }
15008
- const viewportRows = Math.max(1, Math.ceil(scrollViewport.viewportHeight / effectiveRowHeight));
15009
- const rangeStart = Math.max(0, Math.floor(scrollViewport.scrollTop / effectiveRowHeight) - TASK_ROW_OVERSCAN);
15010
- const rangeEnd = Math.min(
15011
- totalTasks - 1,
15012
- rangeStart + viewportRows + TASK_ROW_OVERSCAN * 2 - 1
15013
- );
15291
+ let low = 0;
15292
+ let high = totalTasks;
15293
+ while (low < high) {
15294
+ const middle = low + high >>> 1;
15295
+ if (compositeRowLayout.tops[middle] + compositeRowLayout.heights[middle] < scrollViewport.scrollTop) low = middle + 1;
15296
+ else high = middle;
15297
+ }
15298
+ const first = Math.min(low, totalTasks - 1);
15299
+ const viewportBottom = scrollViewport.scrollTop + scrollViewport.viewportHeight;
15300
+ low = first;
15301
+ high = totalTasks;
15302
+ while (low < high) {
15303
+ const middle = low + high >>> 1;
15304
+ if (compositeRowLayout.tops[middle] < viewportBottom) low = middle + 1;
15305
+ else high = middle;
15306
+ }
15307
+ const last = Math.min(low, totalTasks - 1);
15308
+ const rangeStart = Math.max(0, first - TASK_ROW_OVERSCAN);
15309
+ const rangeEnd = Math.min(totalTasks - 1, last + TASK_ROW_OVERSCAN);
15014
15310
  const indices = /* @__PURE__ */ new Set();
15015
15311
  for (let index = rangeStart; index <= rangeEnd; index += 1) {
15016
15312
  indices.add(index);
@@ -15022,7 +15318,7 @@ function TaskGanttChartInner(props, ref) {
15022
15318
  }
15023
15319
  }
15024
15320
  return Array.from(indices).sort((left, right) => left - right);
15025
- }, [effectiveRowHeight, forceFullRenderForPrint, forcedRenderedTaskIds, scrollViewport, visibleTaskIndexMap, visibleTasks.length]);
15321
+ }, [compositeRowLayout, forceFullRenderForPrint, forcedRenderedTaskIds, scrollViewport, visibleTaskIndexMap, visibleTasks.length]);
15026
15322
  const visiblePlanFactDateIndices = (0, import_react19.useMemo)(() => {
15027
15323
  if (forceFullRenderForPrint) {
15028
15324
  return void 0;
@@ -15099,7 +15395,16 @@ function TaskGanttChartInner(props, ref) {
15099
15395
  }
15100
15396
  applyHoveredRows(taskId);
15101
15397
  }, [applyHoveredRows]);
15102
- const handleToggleCollapse = externalOnToggleCollapse ?? (0, import_react19.useCallback)((parentId) => {
15398
+ const handleToggleCollapse = externalOnToggleCollapse ?? ((parentId) => {
15399
+ if (normalizedTasks.some((task) => task.id === parentId && task.composite)) {
15400
+ setExpandedCompositeIds((prev) => {
15401
+ const next = new Set(prev);
15402
+ if (next.has(parentId)) next.delete(parentId);
15403
+ else next.add(parentId);
15404
+ return next;
15405
+ });
15406
+ return;
15407
+ }
15103
15408
  setInternalCollapsedParentIds((prev) => {
15104
15409
  const next = new Set(prev);
15105
15410
  if (next.has(parentId)) {
@@ -15109,7 +15414,14 @@ function TaskGanttChartInner(props, ref) {
15109
15414
  }
15110
15415
  return next;
15111
15416
  });
15112
- }, []);
15417
+ });
15418
+ const lastCompositeBarToggleRef = (0, import_react19.useRef)(null);
15419
+ const handleCompositeBarToggle = (taskId, source) => {
15420
+ const now = Date.now();
15421
+ if (source === "click" && lastCompositeBarToggleRef.current?.source === "release" && lastCompositeBarToggleRef.current.id === taskId && now - lastCompositeBarToggleRef.current.time < 150) return;
15422
+ lastCompositeBarToggleRef.current = { id: taskId, time: now, source };
15423
+ handleToggleCollapse(taskId);
15424
+ };
15113
15425
  const allParentIds = (0, import_react19.useMemo)(() => {
15114
15426
  const taskIds = new Set(normalizedTasks.map((task) => task.id));
15115
15427
  return new Set(normalizedTasks.map((task) => task.parentId).filter((parentId) => Boolean(parentId && taskIds.has(parentId))));
@@ -15117,11 +15429,13 @@ function TaskGanttChartInner(props, ref) {
15117
15429
  const handleCollapseAll = (0, import_react19.useCallback)(() => {
15118
15430
  if (externalCollapsedParentIds) return;
15119
15431
  setInternalCollapsedParentIds(allParentIds);
15432
+ setExpandedCompositeIds(/* @__PURE__ */ new Set());
15120
15433
  }, [allParentIds, externalCollapsedParentIds]);
15121
15434
  const handleExpandAll = (0, import_react19.useCallback)(() => {
15122
15435
  if (externalCollapsedParentIds) return;
15123
15436
  setInternalCollapsedParentIds(/* @__PURE__ */ new Set());
15124
- }, [externalCollapsedParentIds]);
15437
+ setExpandedCompositeIds(new Set(normalizedTasks.filter((task) => task.composite).map((task) => task.id)));
15438
+ }, [externalCollapsedParentIds, normalizedTasks]);
15125
15439
  const exportToPdf = (0, import_react19.useCallback)(async (options) => {
15126
15440
  const sourceContainer = containerRef.current;
15127
15441
  if (!sourceContainer || typeof window === "undefined" || typeof document === "undefined") {
@@ -15386,6 +15700,8 @@ function TaskGanttChartInner(props, ref) {
15386
15700
  {
15387
15701
  tasks: normalizedTasks,
15388
15702
  rowHeight: effectiveRowHeight,
15703
+ rowTops: compositeRowLayout.tops,
15704
+ rowHeights: compositeRowLayout.heights,
15389
15705
  headerHeight,
15390
15706
  taskListWidth,
15391
15707
  onTasksChange: handleTaskChange,
@@ -15580,6 +15896,8 @@ function TaskGanttChartInner(props, ref) {
15580
15896
  gridWidth: renderGridWidth,
15581
15897
  totalHeight: totalGridHeight,
15582
15898
  rowIndexByTaskId: visibleTaskIndexMap,
15899
+ rowTops: compositeRowLayout.tops,
15900
+ rowHeights: compositeRowLayout.heights,
15583
15901
  dragOverrides: dependencyOverrides,
15584
15902
  selectedDep: selectedChip,
15585
15903
  businessDays,
@@ -15666,10 +15984,10 @@ function TaskGanttChartInner(props, ref) {
15666
15984
  {
15667
15985
  style: {
15668
15986
  position: "absolute",
15669
- top: `${index * effectiveRowHeight}px`,
15987
+ top: `${compositeRowLayout.tops[index]}px`,
15670
15988
  left: 0,
15671
15989
  right: 0,
15672
- height: `${effectiveRowHeight}px`
15990
+ height: `${compositeRowLayout.heights[index]}px`
15673
15991
  },
15674
15992
  children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
15675
15993
  TaskRow_default,
@@ -15677,7 +15995,9 @@ function TaskGanttChartInner(props, ref) {
15677
15995
  task,
15678
15996
  monthStart,
15679
15997
  dayWidth,
15680
- rowHeight: effectiveRowHeight,
15998
+ rowHeight: compositeRowLayout.heights[index],
15999
+ compactDetail: compositeRowLayout.heights[index] < effectiveRowHeight,
16000
+ compositeParentColor: task.parentId ? compositeParentsById.get(task.parentId)?.color : void 0,
15681
16001
  onTasksChange: handleTaskChange,
15682
16002
  onDragStateChange: (state) => {
15683
16003
  if (state.isDragging) {
@@ -15714,7 +16034,9 @@ function TaskGanttChartInner(props, ref) {
15714
16034
  isDependencyDragActive: dependencyCreationDrag !== null,
15715
16035
  viewMode,
15716
16036
  showTaskDateLabels,
15717
- showTaskNames
16037
+ showTaskNames,
16038
+ onCompositeToggle: task.composite ? (source) => handleCompositeBarToggle(task.id, source) : void 0,
16039
+ compositeExpanded: task.composite ? !collapsedParentIds.has(task.id) : void 0
15718
16040
  }
15719
16041
  )
15720
16042
  },
@@ -15727,6 +16049,7 @@ function TaskGanttChartInner(props, ref) {
15727
16049
  rowHeight: effectiveRowHeight,
15728
16050
  variant: "chart",
15729
16051
  startIndex: visibleTasks.length,
16052
+ startTop: compositeRowLayout.totalHeight,
15730
16053
  chartDayWidth: dayWidth,
15731
16054
  chartStartDayOffset: todayInRange ? Math.max(0, todayIndex + Math.floor(Number.isFinite(skeletonStartOffsetDays) ? skeletonStartOffsetDays : 0)) : 0,
15732
16055
  chartStartJitterDays: skeletonStartJitterDays
@@ -15810,6 +16133,8 @@ var nameContains = (substring, caseSensitive = false) => (task) => {
15810
16133
  GenerationSkeletonRows,
15811
16134
  GridBackground,
15812
16135
  Input,
16136
+ MIN_EDGE_ZONE_PX,
16137
+ MIN_MOVE_ZONE_PX,
15813
16138
  PlanFactMatrix,
15814
16139
  Popover,
15815
16140
  PopoverContent,