gantt-lib 0.135.9 → 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
@@ -3306,6 +3306,7 @@ var TimeScaleHeader_default = TimeScaleHeader;
3306
3306
 
3307
3307
  // src/components/TaskRow/TaskRow.tsx
3308
3308
  var import_react3 = __toESM(require("react"));
3309
+ var import_react_dom = require("react-dom");
3309
3310
 
3310
3311
  // src/utils/geometry.ts
3311
3312
  var getUTCDayDifference = (date1, date2) => {
@@ -4413,12 +4414,35 @@ var useTaskDrag = (options) => {
4413
4414
 
4414
4415
  // src/components/TaskRow/TaskRow.tsx
4415
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
+ };
4416
4422
  var arePropsEqual = (prevProps, nextProps) => {
4417
- 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;
4418
4424
  };
4419
4425
  var TaskRow = import_react3.default.memo(
4420
- ({ 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 }) => {
4421
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]);
4422
4446
  const { divider: taskDivider } = task;
4423
4447
  const normalizedTask = (0, import_react3.useMemo)(() => normalizeTaskDatesForType(task), [task]);
4424
4448
  const milestone = (0, import_react3.useMemo)(() => isMilestoneTask(normalizedTask), [normalizedTask]);
@@ -4435,6 +4459,7 @@ var TaskRow = import_react3.default.memo(
4435
4459
  const isParent = (0, import_react3.useMemo)(() => {
4436
4460
  return allTasks ? isTaskParent(task.id, allTasks) : false;
4437
4461
  }, [allTasks, task.id]);
4462
+ const isVisualParent = isParent && !task.composite;
4438
4463
  const childCount = (0, import_react3.useMemo)(() => {
4439
4464
  return allTasks ? getChildren(task.id, allTasks).length : 0;
4440
4465
  }, [allTasks, task.id]);
@@ -4480,19 +4505,19 @@ var TaskRow = import_react3.default.memo(
4480
4505
  if (isExpired) {
4481
4506
  return "color-mix(in srgb, var(--gantt-expired-color) 40%, black)";
4482
4507
  }
4483
- 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)";
4484
4509
  return `color-mix(in srgb, ${baseColor} 40%, black)`;
4485
- }, [defaultParentBarColor, isCritical, isExpired, isParent, task.color]);
4510
+ }, [defaultParentBarColor, isCritical, isExpired, isVisualParent, task.color]);
4486
4511
  const barStyle = (0, import_react3.useMemo)(() => {
4487
4512
  const parentBarColor = task.color || defaultParentBarColor;
4488
4513
  if (isCritical && !isExpired) {
4489
4514
  const c = criticalBarColor;
4490
- if (isParent) {
4515
+ if (isVisualParent) {
4491
4516
  return { backgroundColor: c, "--gantt-parent-bar-color": c };
4492
4517
  }
4493
4518
  return { backgroundColor: c };
4494
4519
  }
4495
- if (isParent) {
4520
+ if (isVisualParent) {
4496
4521
  if (progressWidth >= 100) {
4497
4522
  const c = isExpired ? "color-mix(in srgb, var(--gantt-expired-color) 40%, black)" : `color-mix(in srgb, ${parentBarColor} 40%, black)`;
4498
4523
  return { backgroundColor: c, "--gantt-parent-bar-color": c };
@@ -4503,7 +4528,7 @@ var TaskRow = import_react3.default.memo(
4503
4528
  return { backgroundColor: progressColor };
4504
4529
  }
4505
4530
  return { backgroundColor: barColor };
4506
- }, [isCritical, defaultParentBarColor, isExpired, isParent, progressWidth, barColor, progressColor, task.color]);
4531
+ }, [isCritical, defaultParentBarColor, isExpired, isVisualParent, progressWidth, barColor, progressColor, task.color]);
4507
4532
  const handleDragEnd = (result) => {
4508
4533
  const updatedTask = {
4509
4534
  ...normalizedTask,
@@ -4577,6 +4602,25 @@ var TaskRow = import_react3.default.memo(
4577
4602
  }, [displayLeft, dayWidth, milestoneGeometry.size]);
4578
4603
  const visualLeft = milestone ? displayMilestoneGeometry.left : displayLeft;
4579
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;
4580
4624
  const shouldRenderBaseline = showBaseline && baselineGeometry !== null;
4581
4625
  const hasPreviewPosition = isDragging || effectiveOverridePosition !== void 0;
4582
4626
  const currentStartDate = hasPreviewPosition ? pixelsToDate(displayLeft, monthStart, dayWidth) : taskStartDate;
@@ -4594,17 +4638,20 @@ var TaskRow = import_react3.default.memo(
4594
4638
  if (lastDigit >= 2 && lastDigit <= 4) return `${count} \u0440\u0430\u0431\u043E\u0442\u044B`;
4595
4639
  return `${count} \u0440\u0430\u0431\u043E\u0442`;
4596
4640
  };
4597
- const estimatedTextWidth = isParent ? 120 : durationDays >= 10 ? 76 : 62;
4598
- const showProgressInside = !milestone && progressWidth > 0 && displayWidth > estimatedTextWidth;
4599
- const MIN_DURATION_WIDTH = isParent ? 80 : 50;
4600
- 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;
4601
4645
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
4602
4646
  "div",
4603
4647
  {
4604
4648
  "data-filter-match": isFilterMatch ? "true" : "false",
4605
4649
  "data-gantt-task-row-id": task.id,
4606
- className: `gantt-tr-row ${isFilterMatch ? "gantt-tr-row-filter-match" : ""}`,
4607
- 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
+ },
4608
4655
  children: [
4609
4656
  taskDivider === "top" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "gantt-tr-divider gantt-tr-divider-top" }),
4610
4657
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "gantt-tr-taskContainer", children: [
@@ -4622,7 +4669,11 @@ var TaskRow = import_react3.default.memo(
4622
4669
  "div",
4623
4670
  {
4624
4671
  "data-taskbar": true,
4625
- 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" : ""}`,
4626
4677
  style: {
4627
4678
  left: `${visualLeft}px`,
4628
4679
  ...barStyle,
@@ -4632,14 +4683,57 @@ var TaskRow = import_react3.default.memo(
4632
4683
  padding: 0
4633
4684
  } : {
4634
4685
  width: `${visualWidth}px`,
4635
- 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)"
4636
4687
  },
4637
4688
  cursor: dragHandleProps.style.cursor,
4638
4689
  userSelect: dragHandleProps.style.userSelect
4639
4690
  },
4640
4691
  onMouseDown: dragHandleProps.onMouseDown,
4641
4692
  onMouseMove: dragHandleProps.onMouseMove,
4642
- 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,
4643
4737
  children: [
4644
4738
  !milestone && progressWidth > 0 && progressWidth < 100 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
4645
4739
  "div",
@@ -4648,14 +4742,28 @@ var TaskRow = import_react3.default.memo(
4648
4742
  style: {
4649
4743
  width: `${progressWidth}%`,
4650
4744
  backgroundColor: progressColor,
4651
- ...isParent && {
4745
+ ...isVisualParent && {
4652
4746
  borderRadius: "var(--gantt-parent-bar-radius, 8px) 0 0 0"
4653
4747
  }
4654
4748
  }
4655
4749
  }
4656
4750
  ),
4657
4751
  !milestone && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "gantt-tr-resizeHandle gantt-tr-resizeHandleLeft" }),
4658
- 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` }),
4659
4767
  progressWidth > 0 && showProgressInside && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "gantt-tr-progressText", children: [
4660
4768
  progressWidth,
4661
4769
  "%"
@@ -4664,6 +4772,41 @@ var TaskRow = import_react3.default.memo(
4664
4772
  ]
4665
4773
  }
4666
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
+ ),
4667
4810
  !disableDependencyEditing && onDependencyPortPointerDown && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
4668
4811
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
4669
4812
  "button",
@@ -4672,7 +4815,7 @@ var TaskRow = import_react3.default.memo(
4672
4815
  "data-gantt-dependency-port": true,
4673
4816
  "data-task-id": task.id,
4674
4817
  "data-port-side": "left",
4675
- className: `gantt-tr-dependencyPort gantt-tr-dependencyPortLeft ${milestone ? "gantt-tr-dependencyPortMilestone" : ""} ${isParent ? "gantt-tr-dependencyPortParent" : ""} ${isDependencyDragActive ? "gantt-tr-dependencyPortDragActive" : ""}`,
4818
+ className: `gantt-tr-dependencyPort gantt-tr-dependencyPortLeft ${milestone ? "gantt-tr-dependencyPortMilestone" : ""} ${isVisualParent ? "gantt-tr-dependencyPortParent" : ""} ${isDependencyDragActive ? "gantt-tr-dependencyPortDragActive" : ""}`,
4676
4819
  style: { left: `${visualLeft - 24}px` },
4677
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}`,
4678
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",
@@ -4686,7 +4829,7 @@ var TaskRow = import_react3.default.memo(
4686
4829
  "data-gantt-dependency-port": true,
4687
4830
  "data-task-id": task.id,
4688
4831
  "data-port-side": "right",
4689
- 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" : ""}`,
4690
4833
  style: { left: `${visualLeft + visualWidth}px` },
4691
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}`,
4692
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",
@@ -4725,6 +4868,14 @@ var TaskRow = import_react3.default.memo(
4725
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" })
4726
4869
  }
4727
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
+ ),
4728
4879
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
4729
4880
  "div",
4730
4881
  {
@@ -4734,7 +4885,6 @@ var TaskRow = import_react3.default.memo(
4734
4885
  color: isParent ? task.color || defaultParentBarColor : barColor
4735
4886
  },
4736
4887
  children: [
4737
- !showDurationInside && !milestone && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gantt-tr-externalDuration", children: isParent ? getChildCountLabel(childCount) : `${durationDays} \u0434` }),
4738
4888
  progressWidth > 0 && !showProgressInside && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "gantt-tr-externalProgress", children: [
4739
4889
  progressWidth,
4740
4890
  "%"
@@ -5038,6 +5188,8 @@ var DependencyLines = import_react7.default.memo(({
5038
5188
  gridWidth,
5039
5189
  totalHeight,
5040
5190
  rowIndexByTaskId,
5191
+ rowTops,
5192
+ rowHeights,
5041
5193
  dragOverrides,
5042
5194
  selectedDep,
5043
5195
  criticalTaskIds,
@@ -5061,7 +5213,8 @@ var DependencyLines = import_react7.default.memo(({
5061
5213
  left: computed.left,
5062
5214
  right: computed.right,
5063
5215
  centerX: computed.centerX,
5064
- rowTop: rowIndex * rowHeight,
5216
+ rowTop: rowTops?.[rowIndex] ?? rowIndex * rowHeight,
5217
+ rowHeight: rowHeights?.[rowIndex] ?? rowHeight,
5065
5218
  isVirtual: false
5066
5219
  });
5067
5220
  });
@@ -5081,6 +5234,7 @@ var DependencyLines = import_react7.default.memo(({
5081
5234
  right: computed.right,
5082
5235
  centerX: computed.centerX,
5083
5236
  rowTop: ancestorPosition.rowTop,
5237
+ rowHeight: ancestorPosition.rowHeight,
5084
5238
  isVirtual: true
5085
5239
  });
5086
5240
  }
@@ -5094,18 +5248,19 @@ var DependencyLines = import_react7.default.memo(({
5094
5248
  const override = dragOverrides?.get(task.id);
5095
5249
  const computed = resolveTaskHorizontalGeometry(task, monthStart, dayWidth, override);
5096
5250
  const rowIndex = rowIndexByTaskId?.get(task.id);
5097
- const rowTop = rowIndex !== void 0 ? rowIndex * rowHeight : 0;
5251
+ const rowTop = rowIndex !== void 0 ? rowTops?.[rowIndex] ?? rowIndex * rowHeight : 0;
5098
5252
  positions.set(task.id, {
5099
5253
  left: computed.left,
5100
5254
  right: computed.right,
5101
5255
  centerX: computed.centerX,
5102
5256
  rowTop,
5257
+ rowHeight: rowIndex !== void 0 ? rowHeights?.[rowIndex] ?? rowHeight : rowHeight,
5103
5258
  isVirtual: false
5104
5259
  });
5105
5260
  }
5106
5261
  }
5107
5262
  return { taskPositions: positions, taskIndices: indices, hiddenTaskIds: hidden };
5108
- }, [tasks, tasksForPositions, allTasks, collapsedParentIds, monthStart, dayWidth, rowHeight, dragOverrides, rowIndexByTaskId]);
5263
+ }, [tasks, tasksForPositions, allTasks, collapsedParentIds, monthStart, dayWidth, rowHeight, rowTops, rowHeights, dragOverrides, rowIndexByTaskId]);
5109
5264
  const tasksForCycleDetection = allTasks ?? tasks;
5110
5265
  const cycleInfo = (0, import_react7.useMemo)(() => {
5111
5266
  const result = detectCycles(tasksForCycleDetection);
@@ -5147,7 +5302,9 @@ var DependencyLines = import_react7.default.memo(({
5147
5302
  continue;
5148
5303
  }
5149
5304
  }
5150
- 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 ?? "")) {
5151
5308
  continue;
5152
5309
  }
5153
5310
  const isVirtual = predecessor.isVirtual || successor.isVirtual;
@@ -5161,9 +5318,9 @@ var DependencyLines = import_react7.default.memo(({
5161
5318
  let toY;
5162
5319
  if (reverseOrder) {
5163
5320
  fromY = predecessor.rowTop + 10;
5164
- toY = successor.rowTop + rowHeight - 6;
5321
+ toY = successor.rowTop + successor.rowHeight - 6;
5165
5322
  } else {
5166
- fromY = predecessor.rowTop + rowHeight - 10;
5323
+ fromY = predecessor.rowTop + predecessor.rowHeight - 10;
5167
5324
  toY = successor.rowTop + 6;
5168
5325
  }
5169
5326
  let fromX = predecessorTask && isMilestoneTask(predecessorTask) ? predecessor.centerX : edge.type === "SS" || edge.type === "SF" ? predecessor.left : predecessor.right;
@@ -6841,6 +6998,7 @@ var TaskListRow = import_react11.default.memo(
6841
6998
  taskListMenuCommands = [],
6842
6999
  hideTaskListRowActions = false,
6843
7000
  rowClassName,
7001
+ compositeParentColor,
6844
7002
  getTaskListNamePrefixIcon,
6845
7003
  taskDateChangeMode = "preserve-duration",
6846
7004
  onTaskDateChangeModeChange,
@@ -6925,6 +7083,35 @@ var TaskListRow = import_react11.default.memo(
6925
7083
  [businessDays, weekendPredicate]
6926
7084
  );
6927
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]);
6928
7115
  const getHierarchyLineColor = (0, import_react11.useCallback)((columnDepth) => {
6929
7116
  return columnDepth % 2 === 0 ? "#93c5fd" : "var(--gantt-hierarchy-line-color)";
6930
7117
  }, []);
@@ -7033,6 +7220,7 @@ var TaskListRow = import_react11.default.memo(
7033
7220
  );
7034
7221
  const handleNameDoubleClick = (0, import_react11.useCallback)(
7035
7222
  (e) => {
7223
+ if (task.composite) return;
7036
7224
  if (disableTaskNameEditing) return;
7037
7225
  e.stopPropagation();
7038
7226
  nameConfirmedRef.current = false;
@@ -7040,7 +7228,7 @@ var TaskListRow = import_react11.default.memo(
7040
7228
  setNameValue(task.name);
7041
7229
  setEditingColumnId("name");
7042
7230
  },
7043
- [task.name, disableTaskNameEditing]
7231
+ [task.name, task.composite, disableTaskNameEditing]
7044
7232
  );
7045
7233
  const handleRowKeyDown = (0, import_react11.useCallback)(
7046
7234
  (e) => {
@@ -7689,7 +7877,7 @@ var TaskListRow = import_react11.default.memo(
7689
7877
  ]
7690
7878
  }
7691
7879
  );
7692
- 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;
7693
7881
  const nameInputPaddingLeft = nestingDepth > 0 ? `${nestingDepth * 20 + 8}px` : void 0;
7694
7882
  const nestedDropIndicatorLeft = (() => {
7695
7883
  const numberColumnWidth = resolvedColumns?.find((col) => col.id === "number")?.width ?? 40;
@@ -7787,36 +7975,35 @@ var TaskListRow = import_react11.default.memo(
7787
7975
  }
7788
7976
  )
7789
7977
  ] }),
7790
- isParent && !editingName && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
7791
- !isFilterHideMode && !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
7792
- "span",
7793
- {
7794
- style: {
7795
- position: "absolute",
7796
- left: `${nestingDepth * 20 + 9}px`,
7797
- top: `${rowHeight / 2 + 7}px`,
7798
- height: `${rowHeight / 2 - 7}px`,
7799
- width: "1.5px",
7800
- background: getHierarchyLineColor(nestingDepth),
7801
- borderRadius: "1px",
7802
- pointerEvents: "none"
7803
- }
7804
- }
7805
- ),
7806
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
7807
- "button",
7808
- {
7809
- type: "button",
7810
- className: `gantt-tl-collapse-btn ${isCollapsed ? "gantt-tl-collapse-btn-collapsed" : ""}`,
7811
- onClick: handleToggleCollapse,
7812
- style: { left: `${nestingDepth * 20 + 1}px` },
7813
- "aria-label": isCollapsed ? "Expand children" : "Collapse children",
7814
- 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"
7815
7991
  }
7816
- )
7817
- ] })
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
+ ) })
7818
8005
  ] }),
7819
- !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: [
7820
8007
  !isFilterHideMode && !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
7821
8008
  "span",
7822
8009
  {
@@ -7839,7 +8026,7 @@ var TaskListRow = import_react11.default.memo(
7839
8026
  type: "button",
7840
8027
  className: `gantt-tl-collapse-btn ${isCollapsed ? "gantt-tl-collapse-btn-collapsed" : ""}`,
7841
8028
  onClick: handleToggleCollapse,
7842
- style: { left: `${nestingDepth * 20 + 1}px` },
8029
+ style: { left: `${nestingDepth * 20 - 2}px` },
7843
8030
  "aria-label": isCollapsed ? "Expand children" : "Collapse children",
7844
8031
  children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ChevronRightIcon, {})
7845
8032
  }
@@ -7858,27 +8045,57 @@ var TaskListRow = import_react11.default.memo(
7858
8045
  style: { paddingLeft: nameInputPaddingLeft },
7859
8046
  onClick: (e) => e.stopPropagation()
7860
8047
  }
7861
- ) : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
7862
- "button",
7863
- {
7864
- type: "button",
7865
- className: [
7866
- "gantt-tl-name-trigger",
7867
- disableTaskNameEditing ? "gantt-tl-name-locked" : ""
7868
- ].filter(Boolean).join(" "),
7869
- title: task.name,
7870
- onClick: handleNameClick,
7871
- onDoubleClick: handleNameDoubleClick,
7872
- style: {
7873
- paddingLeft: nameTriggerPaddingLeft,
7874
- paddingRight: task.color ? "20px" : void 0
7875
- },
7876
- children: [
7877
- taskListNamePrefixIcon ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "gantt-tl-name-trigger-icon", "aria-hidden": "true", children: taskListNamePrefixIcon }) : null,
7878
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "gantt-tl-name-trigger-text", children: task.name })
7879
- ]
7880
- }
7881
- ),
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
+ ] }),
7882
8099
  !editingName && task.color && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
7883
8100
  "span",
7884
8101
  {
@@ -8495,8 +8712,9 @@ var TaskListRow = import_react11.default.memo(
8495
8712
  isDragOver && isNestedDropTarget ? "gantt-tl-row-drag-over-nested" : "",
8496
8713
  isDragOver && isDirectChildDropTarget ? "gantt-tl-row-drag-over-direct-child" : "",
8497
8714
  isChild ? "gantt-tl-row-child" : "",
8498
- isParent ? "gantt-tl-row-parent" : "",
8499
- 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" : "",
8500
8718
  `gantt-tl-row-level-${rowFillLevel}`,
8501
8719
  isTotalRow ? "gantt-tl-row-total" : "",
8502
8720
  rowClassName ?? ""
@@ -8504,6 +8722,7 @@ var TaskListRow = import_react11.default.memo(
8504
8722
  style: {
8505
8723
  height: `${rowHeight}px`,
8506
8724
  position: "relative",
8725
+ "--gantt-tl-composite-detail-color": compositeParentColor ?? "var(--gantt-task-bar-default-color, #8b5cf6)",
8507
8726
  "--gantt-tl-nested-drop-left": nestedDropIndicatorLeft,
8508
8727
  "--gantt-tl-nested-inside-drop-left": nestedInsideDropIndicatorLeft
8509
8728
  },
@@ -8729,6 +8948,7 @@ var GenerationSkeletonRows = ({
8729
8948
  rowHeight,
8730
8949
  variant,
8731
8950
  startIndex = 0,
8951
+ startTop,
8732
8952
  taskListLayout,
8733
8953
  chartDayWidth = 40,
8734
8954
  chartStartDayOffset = 0,
@@ -8769,7 +8989,7 @@ var GenerationSkeletonRows = ({
8769
8989
  "div",
8770
8990
  {
8771
8991
  className: "gantt-generation-skeleton-row",
8772
- style: { top: `${rowIndex * rowHeight}px`, height: `${rowHeight}px` },
8992
+ style: { top: `${(startTop ?? startIndex * rowHeight) + index * rowHeight}px`, height: `${rowHeight}px` },
8773
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 })
8774
8994
  },
8775
8995
  rowIndex
@@ -9029,6 +9249,8 @@ var SelectAllCheckbox = ({
9029
9249
  var TaskList = ({
9030
9250
  tasks,
9031
9251
  rowHeight,
9252
+ rowTops,
9253
+ rowHeights,
9032
9254
  headerHeight,
9033
9255
  taskListWidth,
9034
9256
  onTasksChange,
@@ -9156,14 +9378,22 @@ var TaskList = ({
9156
9378
  return tasks2;
9157
9379
  }, [orderedTasks, collapsedParentIds, filterMode, filteredTaskIds, isFilterActive, criticalPathMode, criticalTaskIds]);
9158
9380
  const totalHeight = (0, import_react14.useMemo)(
9159
- () => (visibleTasks.length + Math.max(0, Math.floor(skeletonRowCount))) * rowHeight,
9160
- [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]
9161
9383
  );
9162
9384
  const visibleTaskNumberMap = (0, import_react14.useMemo)(
9163
9385
  () => Object.fromEntries(buildTaskNumberMap(visibleTasks)),
9164
9386
  [visibleTasks]
9165
9387
  );
9166
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
+ );
9167
9397
  const renderedVisibleRowIndices = (0, import_react14.useMemo)(
9168
9398
  () => visibleRowIndices ?? visibleTasks.map((_, index) => index),
9169
9399
  [visibleRowIndices, visibleTasks]
@@ -10155,7 +10385,7 @@ var TaskList = ({
10155
10385
  renderedVisibleTasks.map(({ task, index }) => {
10156
10386
  const previousVisibleTask = index > 0 ? visibleTasks[index - 1] : void 0;
10157
10387
  const canDemoteTask = index === 0 || !task.parentId || previousVisibleTask?.id !== task.parentId;
10158
- const topOffset = index * rowHeight + (hasVisiblePendingInsert && index > pendingInsertRowIndex ? rowHeight : 0);
10388
+ const topOffset = (rowTops?.[index] ?? index * rowHeight) + (hasVisiblePendingInsert && index > pendingInsertRowIndex ? rowHeight : 0);
10159
10389
  return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
10160
10390
  "div",
10161
10391
  {
@@ -10172,7 +10402,7 @@ var TaskList = ({
10172
10402
  rowIndex: index,
10173
10403
  taskNumber: originalTaskNumberMap[task.id] || "",
10174
10404
  taskNumberMap: originalTaskNumberMap,
10175
- rowHeight,
10405
+ rowHeight: rowHeights?.[index] ?? rowHeight,
10176
10406
  onTasksChange,
10177
10407
  onDurationChange,
10178
10408
  selectedTaskId,
@@ -10229,7 +10459,8 @@ var TaskList = ({
10229
10459
  onActiveCustomCellChange: setActiveCustomCell,
10230
10460
  taskListMenuCommands,
10231
10461
  hideTaskListRowActions,
10232
- 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,
10233
10464
  getTaskListNamePrefixIcon,
10234
10465
  taskDateChangeMode,
10235
10466
  onTaskDateChangeModeChange,
@@ -10245,7 +10476,7 @@ var TaskList = ({
10245
10476
  {
10246
10477
  style: {
10247
10478
  position: "absolute",
10248
- top: `${(pendingInsertRowIndex + 1) * rowHeight}px`,
10479
+ top: `${rowTops?.[pendingInsertRowIndex + 1] ?? (pendingInsertRowIndex + 1) * rowHeight}px`,
10249
10480
  left: 0,
10250
10481
  right: 0
10251
10482
  },
@@ -10267,6 +10498,7 @@ var TaskList = ({
10267
10498
  rowHeight,
10268
10499
  variant: "task-list",
10269
10500
  startIndex: visibleTasks.length,
10501
+ startTop: totalHeight - Math.max(0, Math.floor(skeletonRowCount)) * rowHeight,
10270
10502
  taskListLayout: generationSkeletonTaskListLayout
10271
10503
  }
10272
10504
  )
@@ -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