gantt-lib 0.112.0 → 0.113.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -557,10 +557,10 @@ function buildTaskRangeFromStart(startDate, duration, businessDays = false, week
557
557
  end: parseDateOnly(addBusinessDays(normalizedStart, duration, weekendPredicate))
558
558
  };
559
559
  }
560
- const DAY_MS5 = 24 * 60 * 60 * 1e3;
560
+ const DAY_MS6 = 24 * 60 * 60 * 1e3;
561
561
  return {
562
562
  start: normalizedStart,
563
- end: new Date(normalizedStart.getTime() + (Math.max(1, duration) - 1) * DAY_MS5)
563
+ end: new Date(normalizedStart.getTime() + (Math.max(1, duration) - 1) * DAY_MS6)
564
564
  };
565
565
  }
566
566
  function buildTaskRangeFromEnd(endDate, duration, businessDays = false, weekendPredicate, snapDirection = -1) {
@@ -571,9 +571,9 @@ function buildTaskRangeFromEnd(endDate, duration, businessDays = false, weekendP
571
571
  end: normalizedEnd
572
572
  };
573
573
  }
574
- const DAY_MS5 = 24 * 60 * 60 * 1e3;
574
+ const DAY_MS6 = 24 * 60 * 60 * 1e3;
575
575
  return {
576
- start: new Date(normalizedEnd.getTime() - (Math.max(1, duration) - 1) * DAY_MS5),
576
+ start: new Date(normalizedEnd.getTime() - (Math.max(1, duration) - 1) * DAY_MS6),
577
577
  end: normalizedEnd
578
578
  };
579
579
  }
@@ -988,13 +988,13 @@ function computeParentProgress(parentId, tasks) {
988
988
  if (children.length === 0) {
989
989
  return 0;
990
990
  }
991
- const DAY_MS5 = 24 * 60 * 60 * 1e3;
991
+ const DAY_MS6 = 24 * 60 * 60 * 1e3;
992
992
  let totalWeight = 0;
993
993
  let weightedSum = 0;
994
994
  for (const child of children) {
995
995
  const start = new Date(child.startDate).getTime();
996
996
  const end = new Date(child.endDate).getTime();
997
- const duration = (end - start + DAY_MS5) / DAY_MS5;
997
+ const duration = (end - start + DAY_MS6) / DAY_MS6;
998
998
  const progress = child.progress ?? 0;
999
999
  totalWeight += duration;
1000
1000
  weightedSum += duration * progress;
@@ -10448,8 +10448,9 @@ function TableMatrix({
10448
10448
  }
10449
10449
 
10450
10450
  // src/components/PlanFactMatrix/PlanFactMatrix.tsx
10451
- import { useCallback as useCallback9, useEffect as useEffect9, useMemo as useMemo12, useRef as useRef10, useState as useState10 } from "react";
10451
+ import React15, { useCallback as useCallback9, useEffect as useEffect9, useMemo as useMemo12, useRef as useRef10, useState as useState10 } from "react";
10452
10452
  import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
10453
+ var DAY_MS5 = 24 * 60 * 60 * 1e3;
10453
10454
  function joinClasses2(...values) {
10454
10455
  return values.filter(Boolean).join(" ");
10455
10456
  }
@@ -10472,13 +10473,25 @@ function parseNumberInput(value) {
10472
10473
  }
10473
10474
  return parsed;
10474
10475
  }
10475
- function isDateWithinTask(task, date) {
10476
+ function getDateOnlyMs(date) {
10477
+ return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
10478
+ }
10479
+ function getPlannedIndexRange(task, rangeStartMs, rangeLength) {
10480
+ if (rangeLength <= 0) return null;
10476
10481
  const start = parseUTCDate(task.startDate);
10477
10482
  const end = parseUTCDate(task.endDate);
10478
- const dateMs = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
10479
- const startMs = Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate());
10480
- const endMs = Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate());
10481
- return startMs <= dateMs && dateMs <= endMs;
10483
+ const startIndex = Math.ceil((getDateOnlyMs(start) - rangeStartMs) / DAY_MS5);
10484
+ const endIndex = Math.floor((getDateOnlyMs(end) - rangeStartMs) / DAY_MS5);
10485
+ const clampedStartIndex = Math.max(0, startIndex);
10486
+ const clampedEndIndex = Math.min(rangeLength - 1, endIndex);
10487
+ if (clampedStartIndex > clampedEndIndex) return null;
10488
+ return {
10489
+ startIndex: clampedStartIndex,
10490
+ endIndex: clampedEndIndex
10491
+ };
10492
+ }
10493
+ function isDateIndexWithinPlannedRange(dateIndex, range) {
10494
+ return !!range && range.startIndex <= dateIndex && dateIndex <= range.endIndex;
10482
10495
  }
10483
10496
  function formatValue(value) {
10484
10497
  if (value === void 0) return "";
@@ -10577,6 +10590,261 @@ function PlanFactCellEditor({
10577
10590
  }
10578
10591
  );
10579
10592
  }
10593
+ function getCellSignatureForTask(cell, taskId) {
10594
+ return cell?.taskId === taskId ? `${cell.dateIndex}:${cell.kind}` : "";
10595
+ }
10596
+ function getEditingCellSignatureForTask(cell, taskId) {
10597
+ return cell?.taskId === taskId ? `${cell.dateIndex}:${cell.kind}:${cell.startValue ?? ""}` : "";
10598
+ }
10599
+ function getRangeAnchorSignatureForTask(range, taskId) {
10600
+ return range?.anchor.taskId === taskId ? `${range.anchor.dateIndex}:${range.anchor.kind}` : "";
10601
+ }
10602
+ function doesRangeTouchRow(bounds, rowIndex) {
10603
+ if (!bounds) return false;
10604
+ const firstSubrowIndex = rowIndex * 2;
10605
+ const lastSubrowIndex = firstSubrowIndex + 1;
10606
+ return bounds.fromSubrowIndex <= lastSubrowIndex && bounds.toSubrowIndex >= firstSubrowIndex;
10607
+ }
10608
+ function areRangeBoundsEqual(left, right) {
10609
+ if (left === right) return true;
10610
+ if (!left || !right) return false;
10611
+ return left.fromDateIndex === right.fromDateIndex && left.toDateIndex === right.toDateIndex && left.fromSubrowIndex === right.fromSubrowIndex && left.toSubrowIndex === right.toSubrowIndex;
10612
+ }
10613
+ function PlanFactRowInner({
10614
+ task,
10615
+ rowIndex,
10616
+ dateRange,
10617
+ dateKeys,
10618
+ renderedDateIndices,
10619
+ rowHeight,
10620
+ subrowHeight,
10621
+ dayWidth,
10622
+ plannedRange,
10623
+ todayDateIndex,
10624
+ isParent,
10625
+ isHighlighted,
10626
+ selectedTaskId,
10627
+ activeCell,
10628
+ editingCell,
10629
+ selectedRange,
10630
+ renderedRangeBounds,
10631
+ didDragSelectRef,
10632
+ isSelectingRef,
10633
+ isFillDraggingRef,
10634
+ onTaskSelect,
10635
+ selectSingleCell,
10636
+ queueHoverCellUpdate,
10637
+ setActiveCell,
10638
+ setEditingCell,
10639
+ setFillRange,
10640
+ clearSelectedCells,
10641
+ commitCell,
10642
+ commitSelectedCells,
10643
+ moveActiveCell,
10644
+ extendSelectedRange,
10645
+ focusCell,
10646
+ showOverflowTooltip,
10647
+ hideOverflowTooltip
10648
+ }) {
10649
+ return /* @__PURE__ */ jsx18(
10650
+ "div",
10651
+ {
10652
+ "data-gantt-task-row-id": task.id,
10653
+ className: joinClasses2(
10654
+ "gantt-pf-row",
10655
+ isParent && "gantt-pf-row-parent",
10656
+ selectedTaskId === task.id && "gantt-pf-row-selected",
10657
+ isHighlighted && "gantt-pf-row-highlighted"
10658
+ ),
10659
+ style: {
10660
+ top: `${rowIndex * rowHeight}px`,
10661
+ height: `${rowHeight}px`,
10662
+ gridTemplateColumns: `repeat(${dateRange.length}, ${dayWidth}px)`,
10663
+ ["--gantt-pf-today-left"]: todayDateIndex !== void 0 && todayDateIndex >= 0 ? `${todayDateIndex * dayWidth}px` : void 0
10664
+ },
10665
+ onClick: () => onTaskSelect?.(task.id),
10666
+ children: renderedDateIndices.map((dateIndex) => {
10667
+ const dateKey = dateKeys[dateIndex];
10668
+ if (dateKey === void 0) return null;
10669
+ const planned = isDateIndexWithinPlannedRange(dateIndex, plannedRange);
10670
+ return ["plan", "fact"].map((kind) => {
10671
+ const subrowIndex = getSubrowIndex(rowIndex, kind);
10672
+ const isInRenderedRange = !!renderedRangeBounds && dateIndex >= renderedRangeBounds.fromDateIndex && dateIndex <= renderedRangeBounds.toDateIndex && subrowIndex >= renderedRangeBounds.fromSubrowIndex && subrowIndex <= renderedRangeBounds.toSubrowIndex;
10673
+ const planValue = task.planByDate?.[dateKey];
10674
+ const factValue = task.factByDate?.[dateKey];
10675
+ const value = kind === "plan" ? planValue : factValue;
10676
+ const factStatus = factValue === void 0 || planValue === void 0 ? null : factValue >= planValue ? "success" : "warning";
10677
+ const isActive = activeCell?.taskId === task.id && activeCell.dateIndex === dateIndex && activeCell.kind === kind;
10678
+ const isEditing = editingCell?.taskId === task.id && editingCell.dateIndex === dateIndex && editingCell.kind === kind;
10679
+ const currentCell = { taskId: task.id, dateIndex, kind };
10680
+ const isSelected = !isParent && isInRenderedRange;
10681
+ const showFillHandle = !isParent && !isEditing && isInRenderedRange && renderedRangeBounds !== null && dateIndex === renderedRangeBounds.toDateIndex && subrowIndex === renderedRangeBounds.toSubrowIndex;
10682
+ const isRangeAnchor = !showFillHandle && !isParent && selectedRange?.anchor.taskId === task.id && selectedRange.anchor.dateIndex === dateIndex && selectedRange.anchor.kind === kind;
10683
+ return /* @__PURE__ */ jsxs14(
10684
+ "div",
10685
+ {
10686
+ "data-plan-fact-task-id": task.id,
10687
+ "data-plan-fact-date-index": dateIndex,
10688
+ "data-plan-fact-kind": kind,
10689
+ className: joinClasses2(
10690
+ "gantt-pf-cell",
10691
+ `gantt-pf-cell-${kind}`,
10692
+ planned && kind === "plan" && "gantt-pf-cell-planned",
10693
+ value !== void 0 && "gantt-pf-cell-hasValue",
10694
+ kind === "fact" && factStatus === "success" && "gantt-pf-cell-factSuccess",
10695
+ kind === "fact" && factStatus === "warning" && "gantt-pf-cell-factWarning",
10696
+ isSelected && "gantt-pf-cell-selected",
10697
+ isInRenderedRange && renderedRangeBounds !== null && dateIndex === renderedRangeBounds.fromDateIndex && "gantt-pf-cell-rangeLeft",
10698
+ isInRenderedRange && renderedRangeBounds !== null && dateIndex === renderedRangeBounds.toDateIndex && "gantt-pf-cell-rangeRight",
10699
+ isInRenderedRange && renderedRangeBounds !== null && subrowIndex === renderedRangeBounds.fromSubrowIndex && "gantt-pf-cell-rangeTop",
10700
+ isInRenderedRange && renderedRangeBounds !== null && subrowIndex === renderedRangeBounds.toSubrowIndex && "gantt-pf-cell-rangeBottom",
10701
+ isRangeAnchor && "gantt-pf-cell-rangeAnchor",
10702
+ isActive && "gantt-pf-cell-active",
10703
+ isEditing && "gantt-pf-cell-editing",
10704
+ isParent && "gantt-pf-cell-readonly"
10705
+ ),
10706
+ style: {
10707
+ gridColumn: dateIndex + 1,
10708
+ gridRow: kind === "plan" ? 1 : 2,
10709
+ height: `${subrowHeight}px`
10710
+ },
10711
+ tabIndex: isParent ? -1 : 0,
10712
+ onMouseDown: (event) => {
10713
+ if (isParent) return;
10714
+ event.preventDefault();
10715
+ event.stopPropagation();
10716
+ didDragSelectRef.current = false;
10717
+ isSelectingRef.current = true;
10718
+ selectSingleCell(currentCell);
10719
+ onTaskSelect?.(task.id);
10720
+ event.currentTarget.focus();
10721
+ },
10722
+ onMouseEnter: () => {
10723
+ if (isParent) return;
10724
+ if (!isFillDraggingRef.current && !isSelectingRef.current) return;
10725
+ queueHoverCellUpdate(currentCell);
10726
+ },
10727
+ onFocus: () => {
10728
+ if (isParent) return;
10729
+ setActiveCell({ taskId: task.id, dateIndex, kind });
10730
+ },
10731
+ onClick: (event) => {
10732
+ event.stopPropagation();
10733
+ if (didDragSelectRef.current) {
10734
+ didDragSelectRef.current = false;
10735
+ return;
10736
+ }
10737
+ onTaskSelect?.(task.id);
10738
+ if (isParent) return;
10739
+ selectSingleCell(currentCell);
10740
+ event.currentTarget.focus();
10741
+ },
10742
+ onDoubleClick: (event) => {
10743
+ event.stopPropagation();
10744
+ if (isParent) return;
10745
+ setEditingCell({ taskId: task.id, dateIndex, kind });
10746
+ },
10747
+ onKeyDown: (event) => {
10748
+ if (isParent || isEditing) return;
10749
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight" || event.key === "ArrowUp" || event.key === "ArrowDown") {
10750
+ event.preventDefault();
10751
+ event.stopPropagation();
10752
+ const direction = event.key.replace("Arrow", "").toLowerCase();
10753
+ if (event.shiftKey) {
10754
+ extendSelectedRange(selectedRange?.focus ?? currentCell, direction);
10755
+ } else {
10756
+ moveActiveCell(currentCell, direction);
10757
+ }
10758
+ return;
10759
+ }
10760
+ if (event.key === "Enter" || event.key === "F2") {
10761
+ event.preventDefault();
10762
+ event.stopPropagation();
10763
+ setEditingCell(selectedRange?.anchor ?? currentCell);
10764
+ return;
10765
+ }
10766
+ if (event.key === "Backspace" || event.key === "Delete") {
10767
+ event.preventDefault();
10768
+ event.stopPropagation();
10769
+ clearSelectedCells();
10770
+ return;
10771
+ }
10772
+ if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
10773
+ event.preventDefault();
10774
+ event.stopPropagation();
10775
+ setEditingCell({ ...currentCell, startValue: event.key });
10776
+ }
10777
+ },
10778
+ children: [
10779
+ isEditing ? /* @__PURE__ */ jsx18(
10780
+ PlanFactCellEditor,
10781
+ {
10782
+ value,
10783
+ startValue: editingCell.startValue,
10784
+ onCommit: (nextValue) => {
10785
+ commitCell(task, dateIndex, kind, nextValue);
10786
+ setEditingCell(null);
10787
+ const nextActiveCell = { taskId: task.id, dateIndex, kind };
10788
+ selectSingleCell(nextActiveCell);
10789
+ focusCell(nextActiveCell);
10790
+ },
10791
+ onCommitRange: (nextValue) => {
10792
+ commitSelectedCells(nextValue);
10793
+ setEditingCell(null);
10794
+ const nextActiveCell = { taskId: task.id, dateIndex, kind };
10795
+ selectSingleCell(nextActiveCell);
10796
+ focusCell(nextActiveCell);
10797
+ },
10798
+ onCancel: () => {
10799
+ setEditingCell(null);
10800
+ const nextActiveCell = { taskId: task.id, dateIndex, kind };
10801
+ selectSingleCell(nextActiveCell);
10802
+ focusCell(nextActiveCell);
10803
+ }
10804
+ }
10805
+ ) : /* @__PURE__ */ jsx18(
10806
+ "span",
10807
+ {
10808
+ className: "gantt-pf-cellValue",
10809
+ onMouseEnter: (event) => {
10810
+ if (isParent || value === void 0) return;
10811
+ const compactValue = formatValue(value);
10812
+ const fullValue = formatTooltipValue(value);
10813
+ showOverflowTooltip(event.currentTarget, fullValue, compactValue !== fullValue);
10814
+ },
10815
+ onMouseLeave: hideOverflowTooltip,
10816
+ children: isParent ? "" : formatValue(value)
10817
+ }
10818
+ ),
10819
+ showFillHandle && /* @__PURE__ */ jsx18(
10820
+ "span",
10821
+ {
10822
+ className: "gantt-pf-fillHandle",
10823
+ "aria-hidden": "true",
10824
+ onMouseDown: (event) => {
10825
+ event.preventDefault();
10826
+ event.stopPropagation();
10827
+ isSelectingRef.current = false;
10828
+ isFillDraggingRef.current = true;
10829
+ setFillRange(selectedRange);
10830
+ }
10831
+ }
10832
+ )
10833
+ ]
10834
+ },
10835
+ `${task.id}:${dateKey}:${kind}`
10836
+ );
10837
+ });
10838
+ })
10839
+ }
10840
+ );
10841
+ }
10842
+ function arePlanFactRowsEqual(previous, next) {
10843
+ const previousRangeTouchesRow = doesRangeTouchRow(previous.renderedRangeBounds, previous.rowIndex);
10844
+ const nextRangeTouchesRow = doesRangeTouchRow(next.renderedRangeBounds, next.rowIndex);
10845
+ return previous.task === next.task && previous.rowIndex === next.rowIndex && previous.dateRange === next.dateRange && previous.dateKeys === next.dateKeys && previous.renderedDateIndices === next.renderedDateIndices && previous.rowHeight === next.rowHeight && previous.subrowHeight === next.subrowHeight && previous.dayWidth === next.dayWidth && previous.plannedRange === next.plannedRange && previous.todayDateIndex === next.todayDateIndex && previous.isParent === next.isParent && previous.isHighlighted === next.isHighlighted && previous.selectedTaskId === previous.task.id === (next.selectedTaskId === next.task.id) && getCellSignatureForTask(previous.activeCell, previous.task.id) === getCellSignatureForTask(next.activeCell, next.task.id) && getEditingCellSignatureForTask(previous.editingCell, previous.task.id) === getEditingCellSignatureForTask(next.editingCell, next.task.id) && getRangeAnchorSignatureForTask(previous.selectedRange, previous.task.id) === getRangeAnchorSignatureForTask(next.selectedRange, next.task.id) && previousRangeTouchesRow === nextRangeTouchesRow && (!nextRangeTouchesRow || areRangeBoundsEqual(previous.renderedRangeBounds, next.renderedRangeBounds));
10846
+ }
10847
+ var PlanFactRow = React15.memo(PlanFactRowInner, arePlanFactRowsEqual);
10580
10848
  function PlanFactMatrix({
10581
10849
  tasks,
10582
10850
  allTasks = tasks,
@@ -10590,7 +10858,10 @@ function PlanFactMatrix({
10590
10858
  onTasksChange,
10591
10859
  onCellCommit,
10592
10860
  highlightedTaskIds,
10593
- filterMode = "highlight"
10861
+ filterMode = "highlight",
10862
+ visibleRowIndices,
10863
+ visibleDateIndices,
10864
+ todayDateIndex
10594
10865
  }) {
10595
10866
  const [activeCell, setActiveCell] = useState10(null);
10596
10867
  const [editingCell, setEditingCell] = useState10(null);
@@ -10600,10 +10871,20 @@ function PlanFactMatrix({
10600
10871
  const isSelectingRef = useRef10(false);
10601
10872
  const isFillDraggingRef = useRef10(false);
10602
10873
  const didDragSelectRef = useRef10(false);
10874
+ const pendingHoverCellRef = useRef10(null);
10875
+ const hoverFrameRef = useRef10(null);
10603
10876
  const rootRef = useRef10(null);
10604
10877
  const bodyRef = useRef10(null);
10605
10878
  const totalWidth = dateRange.length * dayWidth;
10606
10879
  const subrowHeight = rowHeight / 2;
10880
+ const renderedRowIndices = useMemo12(
10881
+ () => visibleRowIndices ?? tasks.map((_, index) => index),
10882
+ [tasks, visibleRowIndices]
10883
+ );
10884
+ const renderedDateIndices = useMemo12(
10885
+ () => visibleDateIndices ?? dateRange.map((_, index) => index),
10886
+ [dateRange, visibleDateIndices]
10887
+ );
10607
10888
  const parentTaskIds = useMemo12(() => {
10608
10889
  const ids = /* @__PURE__ */ new Set();
10609
10890
  for (const task of allTasks) {
@@ -10612,11 +10893,32 @@ function PlanFactMatrix({
10612
10893
  return ids;
10613
10894
  }, [allTasks]);
10614
10895
  const dateKeys = useMemo12(() => dateRange.map(formatDateKey), [dateRange]);
10896
+ const dateRangeStartMs = dateRange[0] ? getDateOnlyMs(dateRange[0]) : 0;
10615
10897
  const taskIndexById = useMemo12(() => {
10616
10898
  const indexById = /* @__PURE__ */ new Map();
10617
10899
  tasks.forEach((task, index) => indexById.set(task.id, index));
10618
10900
  return indexById;
10619
10901
  }, [tasks]);
10902
+ const taskById = useMemo12(
10903
+ () => new Map(tasks.map((task) => [task.id, task])),
10904
+ [tasks]
10905
+ );
10906
+ const monthSeparatorIndices = useMemo12(() => {
10907
+ const indices = [];
10908
+ for (let index = 1; index < dateRange.length; index += 1) {
10909
+ if (dateRange[index].getUTCDate() === 1) {
10910
+ indices.push(index);
10911
+ }
10912
+ }
10913
+ return indices;
10914
+ }, [dateRange]);
10915
+ const plannedRangeByTaskId = useMemo12(() => {
10916
+ const rangeByTaskId = /* @__PURE__ */ new Map();
10917
+ for (const task of tasks) {
10918
+ rangeByTaskId.set(task.id, getPlannedIndexRange(task, dateRangeStartMs, dateRange.length));
10919
+ }
10920
+ return rangeByTaskId;
10921
+ }, [dateRange.length, dateRangeStartMs, tasks]);
10620
10922
  const focusCell = useCallback9((cell) => {
10621
10923
  window.requestAnimationFrame(() => {
10622
10924
  const selector = [
@@ -10628,6 +10930,11 @@ function PlanFactMatrix({
10628
10930
  });
10629
10931
  }, []);
10630
10932
  const clearSelection = useCallback9(() => {
10933
+ if (hoverFrameRef.current !== null) {
10934
+ window.cancelAnimationFrame(hoverFrameRef.current);
10935
+ hoverFrameRef.current = null;
10936
+ }
10937
+ pendingHoverCellRef.current = null;
10631
10938
  isSelectingRef.current = false;
10632
10939
  isFillDraggingRef.current = false;
10633
10940
  didDragSelectRef.current = false;
@@ -10640,6 +10947,29 @@ function PlanFactMatrix({
10640
10947
  const hideOverflowTooltip = useCallback9(() => {
10641
10948
  setOverflowTooltip(null);
10642
10949
  }, []);
10950
+ const flushPendingHoverCell = useCallback9(() => {
10951
+ hoverFrameRef.current = null;
10952
+ const currentCell = pendingHoverCellRef.current;
10953
+ if (!currentCell) return;
10954
+ if (isFillDraggingRef.current && selectedRange) {
10955
+ setFillRange({ anchor: selectedRange.anchor, focus: currentCell });
10956
+ setActiveCell(currentCell);
10957
+ onTaskSelect?.(currentCell.taskId);
10958
+ return;
10959
+ }
10960
+ if (!isSelectingRef.current) return;
10961
+ didDragSelectRef.current = true;
10962
+ setSelectedRange((currentRange) => ({
10963
+ anchor: currentRange?.anchor ?? currentCell,
10964
+ focus: currentCell
10965
+ }));
10966
+ onTaskSelect?.(currentCell.taskId);
10967
+ }, [onTaskSelect, selectedRange]);
10968
+ const queueHoverCellUpdate = useCallback9((cell) => {
10969
+ pendingHoverCellRef.current = cell;
10970
+ if (hoverFrameRef.current !== null) return;
10971
+ hoverFrameRef.current = window.requestAnimationFrame(flushPendingHoverCell);
10972
+ }, [flushPendingHoverCell]);
10643
10973
  const showOverflowTooltip = useCallback9((target, label, force = false) => {
10644
10974
  if (!rootRef.current || !label) return;
10645
10975
  if (!force && target.scrollWidth <= target.clientWidth) {
@@ -10678,6 +11008,11 @@ function PlanFactMatrix({
10678
11008
  )
10679
11009
  };
10680
11010
  }, [taskIndexById]);
11011
+ const renderedRange = fillRange ?? selectedRange;
11012
+ const renderedRangeBounds = useMemo12(
11013
+ () => renderedRange ? getRangeBounds(renderedRange) : null,
11014
+ [getRangeBounds, renderedRange]
11015
+ );
10681
11016
  const getCellFromPosition = useCallback9((subrowIndex, dateIndex) => {
10682
11017
  const task = tasks[Math.floor(subrowIndex / 2)];
10683
11018
  if (!task) return null;
@@ -10697,33 +11032,6 @@ function PlanFactMatrix({
10697
11032
  const cellSubrowIndex = getSubrowIndex(cellTaskIndex, cell.kind);
10698
11033
  return cell.dateIndex >= bounds.fromDateIndex && cell.dateIndex <= bounds.toDateIndex && cellSubrowIndex >= bounds.fromSubrowIndex && cellSubrowIndex <= bounds.toSubrowIndex;
10699
11034
  }, [getRangeBounds, taskIndexById]);
10700
- const isCellInSelectedRange = useCallback9((cell) => {
10701
- if (!selectedRange) return false;
10702
- return isCellInRange(cell, fillRange ?? selectedRange);
10703
- }, [fillRange, isCellInRange, selectedRange]);
10704
- const getSelectedRangeEdgeClasses = useCallback9((cell) => {
10705
- if (!selectedRange) return [];
10706
- const range = fillRange ?? selectedRange;
10707
- const bounds = getRangeBounds(range);
10708
- const taskIndex = taskIndexById.get(cell.taskId);
10709
- if (!bounds || taskIndex === void 0 || !isCellInRange(cell, range)) return [];
10710
- const subrowIndex = getSubrowIndex(taskIndex, cell.kind);
10711
- return [
10712
- cell.dateIndex === bounds.fromDateIndex && "gantt-pf-cell-rangeLeft",
10713
- cell.dateIndex === bounds.toDateIndex && "gantt-pf-cell-rangeRight",
10714
- subrowIndex === bounds.fromSubrowIndex && "gantt-pf-cell-rangeTop",
10715
- subrowIndex === bounds.toSubrowIndex && "gantt-pf-cell-rangeBottom"
10716
- ];
10717
- }, [fillRange, getRangeBounds, isCellInRange, selectedRange, taskIndexById]);
10718
- const isFillHandleCell = useCallback9((cell) => {
10719
- if (!selectedRange) return false;
10720
- const range = fillRange ?? selectedRange;
10721
- const bounds = getRangeBounds(range);
10722
- const taskIndex = taskIndexById.get(cell.taskId);
10723
- if (!bounds || taskIndex === void 0 || !isCellInRange(cell, range)) return false;
10724
- const subrowIndex = getSubrowIndex(taskIndex, cell.kind);
10725
- return cell.dateIndex === bounds.toDateIndex && subrowIndex === bounds.toSubrowIndex;
10726
- }, [fillRange, getRangeBounds, isCellInRange, selectedRange, taskIndexById]);
10727
11035
  const commitCell = useCallback9((task, dateIndex, kind, value) => {
10728
11036
  const dateKey = dateKeys[dateIndex];
10729
11037
  const source = kind === "plan" ? task.planByDate : task.factByDate;
@@ -10746,40 +11054,45 @@ function PlanFactMatrix({
10746
11054
  value
10747
11055
  });
10748
11056
  }, [dateKeys, dateRange, onCellCommit, onTasksChange]);
10749
- const clearSelectedCells = useCallback9(() => {
10750
- if (!selectedRange) {
10751
- if (!activeCell) return;
10752
- const task = tasks.find((candidate) => candidate.id === activeCell.taskId);
10753
- if (!task || parentTaskIds.has(task.id)) return;
10754
- commitCell(task, activeCell.dateIndex, activeCell.kind, void 0);
10755
- return;
10756
- }
11057
+ const commitRangeCells = useCallback9((bounds, value, mode) => {
10757
11058
  const changedTasksById = /* @__PURE__ */ new Map();
10758
- for (const task of tasks) {
10759
- if (parentTaskIds.has(task.id)) continue;
10760
- let nextPlanByDate = task.planByDate;
10761
- let nextFactByDate = task.factByDate;
11059
+ for (let subrowIndex = bounds.fromSubrowIndex; subrowIndex <= bounds.toSubrowIndex; subrowIndex += 1) {
11060
+ const task = tasks[Math.floor(subrowIndex / 2)];
11061
+ if (!task || parentTaskIds.has(task.id)) continue;
11062
+ const kind = subrowIndex % 2 === 0 ? "plan" : "fact";
11063
+ const currentChangedTask = changedTasksById.get(task.id) ?? task;
11064
+ let nextPlanByDate = currentChangedTask.planByDate;
11065
+ let nextFactByDate = currentChangedTask.factByDate;
10762
11066
  let didChange = false;
10763
- for (let dateIndex = 0; dateIndex < dateKeys.length; dateIndex += 1) {
11067
+ for (let dateIndex = bounds.fromDateIndex; dateIndex <= bounds.toDateIndex; dateIndex += 1) {
10764
11068
  const dateKey = dateKeys[dateIndex];
10765
- const planCell = { taskId: task.id, dateIndex, kind: "plan" };
10766
- if (isCellInSelectedRange(planCell) && nextPlanByDate?.[dateKey] !== void 0) {
11069
+ if (dateKey === void 0) continue;
11070
+ const currentValues = kind === "plan" ? nextPlanByDate : nextFactByDate;
11071
+ const currentValue = currentValues?.[dateKey];
11072
+ const nextValue = mode === "clear" ? void 0 : value;
11073
+ if (currentValue === nextValue) continue;
11074
+ if (kind === "plan") {
10767
11075
  nextPlanByDate = { ...nextPlanByDate ?? {} };
10768
- delete nextPlanByDate[dateKey];
10769
- didChange = true;
10770
- }
10771
- const factCell = { taskId: task.id, dateIndex, kind: "fact" };
10772
- if (isCellInSelectedRange(factCell) && nextFactByDate?.[dateKey] !== void 0) {
11076
+ if (nextValue === void 0) {
11077
+ delete nextPlanByDate[dateKey];
11078
+ } else {
11079
+ nextPlanByDate[dateKey] = nextValue;
11080
+ }
11081
+ } else {
10773
11082
  nextFactByDate = { ...nextFactByDate ?? {} };
10774
- delete nextFactByDate[dateKey];
10775
- didChange = true;
11083
+ if (nextValue === void 0) {
11084
+ delete nextFactByDate[dateKey];
11085
+ } else {
11086
+ nextFactByDate[dateKey] = nextValue;
11087
+ }
10776
11088
  }
11089
+ didChange = true;
10777
11090
  }
10778
11091
  if (didChange) {
10779
11092
  changedTasksById.set(task.id, {
10780
- ...task,
10781
- ...nextPlanByDate !== task.planByDate ? { planByDate: nextPlanByDate ?? {} } : {},
10782
- ...nextFactByDate !== task.factByDate ? { factByDate: nextFactByDate ?? {} } : {}
11093
+ ...currentChangedTask,
11094
+ ...nextPlanByDate !== currentChangedTask.planByDate ? { planByDate: nextPlanByDate ?? {} } : {},
11095
+ ...nextFactByDate !== currentChangedTask.factByDate ? { factByDate: nextFactByDate ?? {} } : {}
10783
11096
  });
10784
11097
  }
10785
11098
  }
@@ -10787,67 +11100,46 @@ function PlanFactMatrix({
10787
11100
  if (changedTasks.length > 0) {
10788
11101
  onTasksChange?.(changedTasks);
10789
11102
  }
10790
- }, [activeCell, commitCell, dateKeys, isCellInSelectedRange, onTasksChange, parentTaskIds, selectedRange, tasks]);
11103
+ }, [dateKeys, onTasksChange, parentTaskIds, tasks]);
11104
+ const clearSelectedCells = useCallback9(() => {
11105
+ const activeRange = fillRange ?? selectedRange;
11106
+ if (!activeRange) {
11107
+ if (!activeCell) return;
11108
+ const task = taskById.get(activeCell.taskId);
11109
+ if (!task || parentTaskIds.has(task.id)) return;
11110
+ commitCell(task, activeCell.dateIndex, activeCell.kind, void 0);
11111
+ return;
11112
+ }
11113
+ const bounds = getRangeBounds(activeRange);
11114
+ if (bounds) {
11115
+ commitRangeCells(bounds, void 0, "clear");
11116
+ }
11117
+ }, [activeCell, commitCell, commitRangeCells, fillRange, getRangeBounds, parentTaskIds, selectedRange, taskById]);
10791
11118
  const commitSelectedCells = useCallback9((value) => {
10792
- if (!selectedRange) {
11119
+ const activeRange = fillRange ?? selectedRange;
11120
+ if (!activeRange) {
10793
11121
  if (!activeCell) return;
10794
- const task = tasks.find((candidate) => candidate.id === activeCell.taskId);
11122
+ const task = taskById.get(activeCell.taskId);
10795
11123
  if (!task || parentTaskIds.has(task.id)) return;
10796
11124
  commitCell(task, activeCell.dateIndex, activeCell.kind, value);
10797
11125
  return;
10798
11126
  }
10799
- const changedTasksById = /* @__PURE__ */ new Map();
10800
- for (const task of tasks) {
10801
- if (parentTaskIds.has(task.id)) continue;
10802
- let nextPlanByDate = task.planByDate;
10803
- let nextFactByDate = task.factByDate;
10804
- let didChange = false;
10805
- for (let dateIndex = 0; dateIndex < dateKeys.length; dateIndex += 1) {
10806
- const dateKey = dateKeys[dateIndex];
10807
- const planCell = { taskId: task.id, dateIndex, kind: "plan" };
10808
- if (isCellInSelectedRange(planCell) && nextPlanByDate?.[dateKey] !== value) {
10809
- nextPlanByDate = { ...nextPlanByDate ?? {} };
10810
- if (value === void 0) {
10811
- delete nextPlanByDate[dateKey];
10812
- } else {
10813
- nextPlanByDate[dateKey] = value;
10814
- }
10815
- didChange = true;
10816
- }
10817
- const factCell = { taskId: task.id, dateIndex, kind: "fact" };
10818
- if (isCellInSelectedRange(factCell) && nextFactByDate?.[dateKey] !== value) {
10819
- nextFactByDate = { ...nextFactByDate ?? {} };
10820
- if (value === void 0) {
10821
- delete nextFactByDate[dateKey];
10822
- } else {
10823
- nextFactByDate[dateKey] = value;
10824
- }
10825
- didChange = true;
10826
- }
10827
- }
10828
- if (didChange) {
10829
- changedTasksById.set(task.id, {
10830
- ...task,
10831
- ...nextPlanByDate !== task.planByDate ? { planByDate: nextPlanByDate ?? {} } : {},
10832
- ...nextFactByDate !== task.factByDate ? { factByDate: nextFactByDate ?? {} } : {}
10833
- });
10834
- }
10835
- }
10836
- const changedTasks = Array.from(changedTasksById.values());
10837
- if (changedTasks.length > 0) {
10838
- onTasksChange?.(changedTasks);
11127
+ const bounds = getRangeBounds(activeRange);
11128
+ if (bounds) {
11129
+ commitRangeCells(bounds, value, "set");
10839
11130
  }
10840
- }, [activeCell, commitCell, dateKeys, isCellInSelectedRange, onTasksChange, parentTaskIds, selectedRange, tasks]);
11131
+ }, [activeCell, commitCell, commitRangeCells, fillRange, getRangeBounds, parentTaskIds, selectedRange, taskById]);
10841
11132
  const getCellValue = useCallback9((cell) => {
10842
- const task = tasks.find((candidate) => candidate.id === cell.taskId);
11133
+ const task = taskById.get(cell.taskId);
10843
11134
  if (!task) return void 0;
10844
11135
  const dateKey = dateKeys[cell.dateIndex];
10845
11136
  return cell.kind === "plan" ? task.planByDate?.[dateKey] : task.factByDate?.[dateKey];
10846
- }, [dateKeys, tasks]);
10847
- const applyFillRange = useCallback9(() => {
10848
- if (!selectedRange || !fillRange) return;
11137
+ }, [dateKeys, taskById]);
11138
+ const applyFillRange = useCallback9((nextFillRange) => {
11139
+ const targetRange = nextFillRange ?? fillRange;
11140
+ if (!selectedRange || !targetRange) return;
10849
11141
  const sourceBounds = getRangeBounds(selectedRange);
10850
- const targetBounds = getRangeBounds(fillRange);
11142
+ const targetBounds = getRangeBounds(targetRange);
10851
11143
  if (!sourceBounds || !targetBounds) return;
10852
11144
  const sourceDateSpan = sourceBounds.toDateIndex - sourceBounds.fromDateIndex + 1;
10853
11145
  const sourceSubrowSpan = sourceBounds.toSubrowIndex - sourceBounds.fromSubrowIndex + 1;
@@ -10855,7 +11147,7 @@ function PlanFactMatrix({
10855
11147
  for (let subrowIndex = targetBounds.fromSubrowIndex; subrowIndex <= targetBounds.toSubrowIndex; subrowIndex += 1) {
10856
11148
  const targetCellForRow = getCellFromPosition(subrowIndex, targetBounds.fromDateIndex);
10857
11149
  if (!targetCellForRow || parentTaskIds.has(targetCellForRow.taskId)) continue;
10858
- const originalTask = tasks.find((task) => task.id === targetCellForRow.taskId);
11150
+ const originalTask = taskById.get(targetCellForRow.taskId);
10859
11151
  if (!originalTask) continue;
10860
11152
  let changedTask = changedTasksById.get(originalTask.id) ?? originalTask;
10861
11153
  let nextPlanByDate = changedTask.planByDate;
@@ -10901,8 +11193,8 @@ function PlanFactMatrix({
10901
11193
  if (changedTasks.length > 0) {
10902
11194
  onTasksChange?.(changedTasks);
10903
11195
  }
10904
- setSelectedRange(fillRange);
10905
- setActiveCell(fillRange.anchor);
11196
+ setSelectedRange(targetRange);
11197
+ setActiveCell(targetRange.anchor);
10906
11198
  setFillRange(null);
10907
11199
  }, [
10908
11200
  dateKeys,
@@ -10914,7 +11206,7 @@ function PlanFactMatrix({
10914
11206
  onTasksChange,
10915
11207
  parentTaskIds,
10916
11208
  selectedRange,
10917
- tasks
11209
+ taskById
10918
11210
  ]);
10919
11211
  const moveActiveCell = useCallback9((cell, direction) => {
10920
11212
  const taskIndex = tasks.findIndex((task) => task.id === cell.taskId);
@@ -10986,9 +11278,19 @@ function PlanFactMatrix({
10986
11278
  }, [dateRange.length, focusCell, onTaskSelect, parentTaskIds, selectedRange, tasks]);
10987
11279
  useEffect9(() => {
10988
11280
  const endSelection = () => {
11281
+ let pendingFillRange = null;
11282
+ if (hoverFrameRef.current !== null) {
11283
+ window.cancelAnimationFrame(hoverFrameRef.current);
11284
+ hoverFrameRef.current = null;
11285
+ const pendingCell = pendingHoverCellRef.current;
11286
+ if (pendingCell && isFillDraggingRef.current && selectedRange) {
11287
+ pendingFillRange = { anchor: selectedRange.anchor, focus: pendingCell };
11288
+ }
11289
+ flushPendingHoverCell();
11290
+ }
10989
11291
  if (isFillDraggingRef.current) {
10990
11292
  isFillDraggingRef.current = false;
10991
- applyFillRange();
11293
+ applyFillRange(pendingFillRange);
10992
11294
  }
10993
11295
  isSelectingRef.current = false;
10994
11296
  };
@@ -10996,7 +11298,12 @@ function PlanFactMatrix({
10996
11298
  return () => {
10997
11299
  window.removeEventListener("mouseup", endSelection);
10998
11300
  };
10999
- }, [applyFillRange]);
11301
+ }, [applyFillRange, flushPendingHoverCell]);
11302
+ useEffect9(() => () => {
11303
+ if (hoverFrameRef.current !== null) {
11304
+ window.cancelAnimationFrame(hoverFrameRef.current);
11305
+ }
11306
+ }, []);
11000
11307
  useEffect9(() => {
11001
11308
  const handleKeyDown = (event) => {
11002
11309
  if (event.key !== "Escape") return;
@@ -11026,15 +11333,39 @@ function PlanFactMatrix({
11026
11333
  ["--gantt-pf-day-width"]: `${dayWidth}px`
11027
11334
  },
11028
11335
  children: [
11029
- /* @__PURE__ */ jsx18("div", { className: "gantt-pf-header", style: { width: `${totalWidth}px`, height: `${headerHeight}px` }, children: /* @__PURE__ */ jsx18(
11030
- TimeScaleHeader_default,
11336
+ /* @__PURE__ */ jsxs14("div", { className: "gantt-pf-header", style: { width: `${totalWidth}px`, height: `${headerHeight}px` }, children: [
11337
+ /* @__PURE__ */ jsx18(
11338
+ TimeScaleHeader_default,
11339
+ {
11340
+ days: dateRange,
11341
+ dayWidth,
11342
+ headerHeight: headerHeight - 1,
11343
+ viewMode: "day"
11344
+ }
11345
+ ),
11346
+ todayDateIndex !== void 0 && todayDateIndex >= 0 && /* @__PURE__ */ jsx18(
11347
+ "span",
11348
+ {
11349
+ className: "gantt-pf-headerTodayLine",
11350
+ "aria-hidden": "true",
11351
+ style: {
11352
+ left: `${todayDateIndex * dayWidth}px`,
11353
+ top: `${Math.max(0, headerHeight / 2)}px`
11354
+ }
11355
+ }
11356
+ )
11357
+ ] }),
11358
+ /* @__PURE__ */ jsx18("div", { className: "gantt-pf-monthSeparatorLayer", "aria-hidden": "true", children: monthSeparatorIndices.map((dateIndex) => /* @__PURE__ */ jsx18(
11359
+ "span",
11031
11360
  {
11032
- days: dateRange,
11033
- dayWidth,
11034
- headerHeight: headerHeight - 1,
11035
- viewMode: "day"
11036
- }
11037
- ) }),
11361
+ className: "gantt-pf-monthSeparator",
11362
+ style: {
11363
+ left: `${Math.round(dateIndex * dayWidth)}px`,
11364
+ top: `${Math.max(0, headerHeight / 2)}px`
11365
+ }
11366
+ },
11367
+ `month-separator-${dateIndex}`
11368
+ )) }),
11038
11369
  /* @__PURE__ */ jsx18(
11039
11370
  "div",
11040
11371
  {
@@ -11045,202 +11376,48 @@ function PlanFactMatrix({
11045
11376
  minHeight: bodyMinHeight,
11046
11377
  width: `${totalWidth}px`
11047
11378
  },
11048
- children: tasks.map((task, rowIndex) => {
11379
+ children: renderedRowIndices.map((rowIndex) => {
11380
+ const task = tasks[rowIndex];
11381
+ if (!task) return null;
11049
11382
  const isParent = parentTaskIds.has(task.id);
11050
11383
  const isHighlighted = filterMode === "highlight" && !!highlightedTaskIds?.has(task.id);
11051
11384
  return /* @__PURE__ */ jsx18(
11052
- "div",
11385
+ PlanFactRow,
11053
11386
  {
11054
- "data-gantt-task-row-id": task.id,
11055
- className: joinClasses2(
11056
- "gantt-pf-row",
11057
- isParent && "gantt-pf-row-parent",
11058
- selectedTaskId === task.id && "gantt-pf-row-selected",
11059
- isHighlighted && "gantt-pf-row-highlighted"
11060
- ),
11061
- style: {
11062
- top: `${rowIndex * rowHeight}px`,
11063
- height: `${rowHeight}px`,
11064
- gridTemplateColumns: `repeat(${dateRange.length}, ${dayWidth}px)`
11065
- },
11066
- onClick: () => onTaskSelect?.(task.id),
11067
- children: dateRange.map((date, dateIndex) => {
11068
- const dateKey = dateKeys[dateIndex];
11069
- const planned = isDateWithinTask(task, date);
11070
- return ["plan", "fact"].map((kind) => {
11071
- const planValue = task.planByDate?.[dateKey];
11072
- const factValue = task.factByDate?.[dateKey];
11073
- const value = kind === "plan" ? planValue : factValue;
11074
- const factStatus = factValue === void 0 || planValue === void 0 ? null : factValue >= planValue ? "success" : "warning";
11075
- const isActive = activeCell?.taskId === task.id && activeCell.dateIndex === dateIndex && activeCell.kind === kind;
11076
- const isEditing = editingCell?.taskId === task.id && editingCell.dateIndex === dateIndex && editingCell.kind === kind;
11077
- const currentCell = { taskId: task.id, dateIndex, kind };
11078
- const isSelected = !isParent && isCellInSelectedRange(currentCell);
11079
- const showFillHandle = !isParent && !isEditing && isFillHandleCell(currentCell);
11080
- const isRangeAnchor = !showFillHandle && !isParent && selectedRange?.anchor.taskId === task.id && selectedRange.anchor.dateIndex === dateIndex && selectedRange.anchor.kind === kind;
11081
- return /* @__PURE__ */ jsxs14(
11082
- "div",
11083
- {
11084
- "data-plan-fact-task-id": task.id,
11085
- "data-plan-fact-date-index": dateIndex,
11086
- "data-plan-fact-kind": kind,
11087
- className: joinClasses2(
11088
- "gantt-pf-cell",
11089
- `gantt-pf-cell-${kind}`,
11090
- planned && kind === "plan" && !isParent && "gantt-pf-cell-planned",
11091
- value !== void 0 && "gantt-pf-cell-hasValue",
11092
- kind === "fact" && factStatus === "success" && "gantt-pf-cell-factSuccess",
11093
- kind === "fact" && factStatus === "warning" && "gantt-pf-cell-factWarning",
11094
- isSelected && "gantt-pf-cell-selected",
11095
- ...getSelectedRangeEdgeClasses(currentCell),
11096
- isRangeAnchor && "gantt-pf-cell-rangeAnchor",
11097
- isActive && "gantt-pf-cell-active",
11098
- isEditing && "gantt-pf-cell-editing",
11099
- isParent && "gantt-pf-cell-readonly"
11100
- ),
11101
- style: {
11102
- gridColumn: dateIndex + 1,
11103
- gridRow: kind === "plan" ? 1 : 2,
11104
- height: `${subrowHeight}px`
11105
- },
11106
- tabIndex: isParent ? -1 : 0,
11107
- onMouseDown: (event) => {
11108
- if (isParent) return;
11109
- event.preventDefault();
11110
- event.stopPropagation();
11111
- didDragSelectRef.current = false;
11112
- isSelectingRef.current = true;
11113
- selectSingleCell(currentCell);
11114
- onTaskSelect?.(task.id);
11115
- event.currentTarget.focus();
11116
- },
11117
- onMouseEnter: () => {
11118
- if (!isParent && isFillDraggingRef.current && selectedRange) {
11119
- setFillRange({ anchor: selectedRange.anchor, focus: currentCell });
11120
- setActiveCell(currentCell);
11121
- onTaskSelect?.(task.id);
11122
- return;
11123
- }
11124
- if (isParent || !isSelectingRef.current) return;
11125
- didDragSelectRef.current = true;
11126
- setSelectedRange((currentRange) => ({
11127
- anchor: currentRange?.anchor ?? currentCell,
11128
- focus: currentCell
11129
- }));
11130
- onTaskSelect?.(task.id);
11131
- },
11132
- onFocus: () => {
11133
- if (isParent) return;
11134
- setActiveCell({ taskId: task.id, dateIndex, kind });
11135
- },
11136
- onClick: (event) => {
11137
- event.stopPropagation();
11138
- if (didDragSelectRef.current) {
11139
- didDragSelectRef.current = false;
11140
- return;
11141
- }
11142
- onTaskSelect?.(task.id);
11143
- if (isParent) return;
11144
- selectSingleCell(currentCell);
11145
- event.currentTarget.focus();
11146
- },
11147
- onDoubleClick: (event) => {
11148
- event.stopPropagation();
11149
- if (isParent) return;
11150
- setEditingCell({ taskId: task.id, dateIndex, kind });
11151
- },
11152
- onKeyDown: (event) => {
11153
- if (isParent || isEditing) return;
11154
- if (event.key === "ArrowLeft" || event.key === "ArrowRight" || event.key === "ArrowUp" || event.key === "ArrowDown") {
11155
- event.preventDefault();
11156
- event.stopPropagation();
11157
- const direction = event.key.replace("Arrow", "").toLowerCase();
11158
- if (event.shiftKey) {
11159
- extendSelectedRange(selectedRange?.focus ?? currentCell, direction);
11160
- } else {
11161
- moveActiveCell(currentCell, direction);
11162
- }
11163
- return;
11164
- }
11165
- if (event.key === "Enter" || event.key === "F2") {
11166
- event.preventDefault();
11167
- event.stopPropagation();
11168
- setEditingCell(selectedRange?.anchor ?? currentCell);
11169
- return;
11170
- }
11171
- if (event.key === "Backspace" || event.key === "Delete") {
11172
- event.preventDefault();
11173
- event.stopPropagation();
11174
- clearSelectedCells();
11175
- return;
11176
- }
11177
- if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
11178
- event.preventDefault();
11179
- event.stopPropagation();
11180
- setEditingCell({ ...currentCell, startValue: event.key });
11181
- }
11182
- },
11183
- children: [
11184
- isEditing ? /* @__PURE__ */ jsx18(
11185
- PlanFactCellEditor,
11186
- {
11187
- value,
11188
- startValue: editingCell.startValue,
11189
- onCommit: (nextValue) => {
11190
- commitCell(task, dateIndex, kind, nextValue);
11191
- setEditingCell(null);
11192
- const nextActiveCell = { taskId: task.id, dateIndex, kind };
11193
- selectSingleCell(nextActiveCell);
11194
- focusCell(nextActiveCell);
11195
- },
11196
- onCommitRange: (nextValue) => {
11197
- commitSelectedCells(nextValue);
11198
- setEditingCell(null);
11199
- const nextActiveCell = { taskId: task.id, dateIndex, kind };
11200
- setActiveCell(nextActiveCell);
11201
- focusCell(nextActiveCell);
11202
- },
11203
- onCancel: () => {
11204
- setEditingCell(null);
11205
- const nextActiveCell = { taskId: task.id, dateIndex, kind };
11206
- setActiveCell(nextActiveCell);
11207
- focusCell(nextActiveCell);
11208
- }
11209
- }
11210
- ) : /* @__PURE__ */ jsx18(
11211
- "span",
11212
- {
11213
- className: "gantt-pf-cellValue",
11214
- onMouseEnter: (event) => {
11215
- if (isParent || value === void 0) return;
11216
- const compactValue = formatValue(value);
11217
- const fullValue = formatTooltipValue(value);
11218
- showOverflowTooltip(event.currentTarget, fullValue, compactValue !== fullValue);
11219
- },
11220
- onMouseLeave: hideOverflowTooltip,
11221
- children: isParent ? "" : formatValue(value)
11222
- }
11223
- ),
11224
- showFillHandle && /* @__PURE__ */ jsx18(
11225
- "span",
11226
- {
11227
- className: "gantt-pf-fillHandle",
11228
- "aria-hidden": "true",
11229
- onMouseDown: (event) => {
11230
- event.preventDefault();
11231
- event.stopPropagation();
11232
- isSelectingRef.current = false;
11233
- isFillDraggingRef.current = true;
11234
- setFillRange(selectedRange);
11235
- }
11236
- }
11237
- )
11238
- ]
11239
- },
11240
- `${task.id}:${dateKey}:${kind}`
11241
- );
11242
- });
11243
- })
11387
+ task,
11388
+ rowIndex,
11389
+ dateRange,
11390
+ dateKeys,
11391
+ renderedDateIndices,
11392
+ rowHeight,
11393
+ subrowHeight,
11394
+ dayWidth,
11395
+ plannedRange: plannedRangeByTaskId.get(task.id) ?? null,
11396
+ todayDateIndex,
11397
+ isParent,
11398
+ isHighlighted,
11399
+ selectedTaskId,
11400
+ activeCell,
11401
+ editingCell,
11402
+ selectedRange,
11403
+ renderedRangeBounds,
11404
+ didDragSelectRef,
11405
+ isSelectingRef,
11406
+ isFillDraggingRef,
11407
+ onTaskSelect,
11408
+ selectSingleCell,
11409
+ queueHoverCellUpdate,
11410
+ setActiveCell,
11411
+ setEditingCell,
11412
+ setFillRange,
11413
+ clearSelectedCells,
11414
+ commitCell,
11415
+ commitSelectedCells,
11416
+ moveActiveCell,
11417
+ extendSelectedRange,
11418
+ focusCell,
11419
+ showOverflowTooltip,
11420
+ hideOverflowTooltip
11244
11421
  },
11245
11422
  task.id
11246
11423
  );
@@ -11676,6 +11853,57 @@ function createTaskPreviewPositionStore() {
11676
11853
  import { Fragment as Fragment4, jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
11677
11854
  var SCROLL_TO_ROW_CONTEXT_ROWS = 2;
11678
11855
  var TASK_ROW_OVERSCAN = 8;
11856
+ var PLAN_FACT_COLUMN_OVERSCAN = 24;
11857
+ var PLAN_FACT_COLUMN_WINDOW_STEP = 14;
11858
+ function getFullMonthDays(tasks) {
11859
+ if (!tasks || tasks.length === 0) {
11860
+ return getMultiMonthDays(tasks);
11861
+ }
11862
+ let minDate = null;
11863
+ let maxDate = null;
11864
+ for (const task of tasks) {
11865
+ const start = parseUTCDate(task.startDate);
11866
+ const end = parseUTCDate(task.endDate);
11867
+ if (!minDate || start.getTime() < minDate.getTime()) {
11868
+ minDate = start;
11869
+ }
11870
+ if (!maxDate || end.getTime() > maxDate.getTime()) {
11871
+ maxDate = end;
11872
+ }
11873
+ }
11874
+ if (!minDate || !maxDate) {
11875
+ return getMultiMonthDays(tasks);
11876
+ }
11877
+ const startOfMonth2 = new Date(Date.UTC(minDate.getUTCFullYear(), minDate.getUTCMonth(), 1));
11878
+ const endOfMonth = new Date(Date.UTC(maxDate.getUTCFullYear(), maxDate.getUTCMonth() + 1, 0));
11879
+ const days = [];
11880
+ const current = new Date(startOfMonth2);
11881
+ while (current.getTime() <= endOfMonth.getTime()) {
11882
+ days.push(new Date(Date.UTC(
11883
+ current.getUTCFullYear(),
11884
+ current.getUTCMonth(),
11885
+ current.getUTCDate()
11886
+ )));
11887
+ current.setUTCDate(current.getUTCDate() + 1);
11888
+ }
11889
+ return days;
11890
+ }
11891
+ function getPlanFactRangeTasks(tasks) {
11892
+ const rangeTasks = [];
11893
+ for (const task of tasks) {
11894
+ rangeTasks.push({ startDate: task.startDate, endDate: task.endDate });
11895
+ for (const dateKey of Object.keys(task.planByDate ?? {})) {
11896
+ rangeTasks.push({ startDate: dateKey, endDate: dateKey });
11897
+ }
11898
+ for (const dateKey of Object.keys(task.factByDate ?? {})) {
11899
+ rangeTasks.push({ startDate: dateKey, endDate: dateKey });
11900
+ }
11901
+ }
11902
+ return rangeTasks;
11903
+ }
11904
+ function clampScrollValue(value, max) {
11905
+ return Math.min(max, Math.max(0, value));
11906
+ }
11679
11907
  function arePositionMapsEqual(left, right) {
11680
11908
  if (left.size !== right.size) return false;
11681
11909
  for (const [taskId, leftPosition] of left) {
@@ -11798,6 +12026,7 @@ function TaskGanttChartInner(props, ref) {
11798
12026
  const [taskListHasRightShadow, setTaskListHasRightShadow] = useState11(false);
11799
12027
  const [internalTaskDateChangeMode, setInternalTaskDateChangeMode] = useState11("preserve-duration");
11800
12028
  const [scrollViewport, setScrollViewport] = useState11({ scrollTop: 0, viewportHeight: 0 });
12029
+ const [planFactDateWindow, setPlanFactDateWindow] = useState11(null);
11801
12030
  const [selectedChip, setSelectedChip] = useState11(null);
11802
12031
  const [activeTimelineTooltip, setActiveTimelineTooltip] = useState11(null);
11803
12032
  const [internalCollapsedParentIds, setInternalCollapsedParentIds] = useState11(/* @__PURE__ */ new Set());
@@ -11816,6 +12045,9 @@ function TaskGanttChartInner(props, ref) {
11816
12045
  [customDays, isWeekend3]
11817
12046
  );
11818
12047
  const dateRangeTasks = useMemo13(() => {
12048
+ if (isPlanFactMode) {
12049
+ return getPlanFactRangeTasks(normalizedTasks);
12050
+ }
11819
12051
  if (!showBaseline) {
11820
12052
  return normalizedTasks;
11821
12053
  }
@@ -11824,8 +12056,11 @@ function TaskGanttChartInner(props, ref) {
11824
12056
  startDate: task.baselineStartDate && parseUTCDate(task.baselineStartDate).getTime() < parseUTCDate(task.startDate).getTime() ? task.baselineStartDate : task.startDate,
11825
12057
  endDate: task.baselineEndDate && parseUTCDate(task.baselineEndDate).getTime() > parseUTCDate(task.endDate).getTime() ? task.baselineEndDate : task.endDate
11826
12058
  }));
11827
- }, [normalizedTasks, showBaseline]);
11828
- const dateRange = useMemo13(() => getMultiMonthDays(dateRangeTasks), [dateRangeTasks]);
12059
+ }, [isPlanFactMode, normalizedTasks, showBaseline]);
12060
+ const dateRange = useMemo13(
12061
+ () => isPlanFactMode ? getFullMonthDays(dateRangeTasks) : getMultiMonthDays(dateRangeTasks),
12062
+ [dateRangeTasks, isPlanFactMode]
12063
+ );
11829
12064
  const [validationResult, setValidationResult] = useState11(null);
11830
12065
  const [cascadeOverrides, setCascadeOverrides] = useState11(/* @__PURE__ */ new Map());
11831
12066
  const gridWidth = useMemo13(
@@ -11891,11 +12126,12 @@ function TaskGanttChartInner(props, ref) {
11891
12126
  const firstDay = dateRange[0];
11892
12127
  return new Date(Date.UTC(firstDay.getUTCFullYear(), firstDay.getUTCMonth(), 1));
11893
12128
  }, [dateRange]);
11894
- const todayInRange = useMemo13(() => {
12129
+ const todayIndex = useMemo13(() => {
11895
12130
  const now = /* @__PURE__ */ new Date();
11896
12131
  const today = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
11897
- return dateRange.some((day) => day.getTime() === today.getTime());
12132
+ return dateRange.findIndex((day) => day.getTime() === today.getTime());
11898
12133
  }, [dateRange]);
12134
+ const todayInRange = todayIndex !== -1;
11899
12135
  const visibleTimelineMarkers = useMemo13(() => {
11900
12136
  if (isTableMatrixMode || !timelineMarkers || timelineMarkers.length === 0 || dateRange.length === 0) {
11901
12137
  return [];
@@ -11914,9 +12150,9 @@ function TaskGanttChartInner(props, ref) {
11914
12150
  if (!container || dateRange.length === 0) return;
11915
12151
  const now = /* @__PURE__ */ new Date();
11916
12152
  const today = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
11917
- const todayIndex = dateRange.findIndex((day) => day.getTime() === today.getTime());
11918
- if (todayIndex === -1) return;
11919
- const todayOffset = todayIndex * dayWidth;
12153
+ const todayIndex2 = dateRange.findIndex((day) => day.getTime() === today.getTime());
12154
+ if (todayIndex2 === -1) return;
12155
+ const todayOffset = todayIndex2 * dayWidth;
11920
12156
  const containerWidth = container.clientWidth;
11921
12157
  const scrollLeft = Math.round(todayOffset + dayWidth / 2 - containerWidth * 0.3);
11922
12158
  container.scrollLeft = Math.max(0, scrollLeft);
@@ -11930,13 +12166,33 @@ function TaskGanttChartInner(props, ref) {
11930
12166
  frameId = null;
11931
12167
  const nextHasRightShadow = container.scrollLeft > 0;
11932
12168
  const nextViewportHeight = Math.max(0, container.clientHeight - timelineHeaderHeight);
12169
+ const nextViewportWidth = Math.max(0, container.clientWidth - (showTaskList ? taskListWidth : 0));
11933
12170
  const nextScrollTop = container.scrollTop;
12171
+ const nextScrollLeft = container.scrollLeft;
12172
+ const nextChartScrollLeft = Math.max(0, nextScrollLeft);
11934
12173
  setTaskListHasRightShadow(
11935
12174
  (previous) => previous === nextHasRightShadow ? previous : nextHasRightShadow
11936
12175
  );
11937
12176
  setScrollViewport(
11938
12177
  (previous) => previous.scrollTop === nextScrollTop && previous.viewportHeight === nextViewportHeight ? previous : { scrollTop: nextScrollTop, viewportHeight: nextViewportHeight }
11939
12178
  );
12179
+ setPlanFactDateWindow((previous) => {
12180
+ if (!isPlanFactMode || dateRange.length === 0 || nextViewportWidth <= 0) {
12181
+ return previous === null ? previous : null;
12182
+ }
12183
+ const firstVisibleColumn = Math.max(0, Math.floor(nextChartScrollLeft / dayWidth));
12184
+ const visibleColumnCount = Math.max(1, Math.ceil(nextViewportWidth / dayWidth));
12185
+ const lastVisibleColumn = Math.min(dateRange.length - 1, firstVisibleColumn + visibleColumnCount - 1);
12186
+ const rangeStart = Math.max(
12187
+ 0,
12188
+ Math.floor(Math.max(0, firstVisibleColumn - PLAN_FACT_COLUMN_OVERSCAN) / PLAN_FACT_COLUMN_WINDOW_STEP) * PLAN_FACT_COLUMN_WINDOW_STEP
12189
+ );
12190
+ const rangeEnd = Math.min(
12191
+ dateRange.length - 1,
12192
+ Math.ceil((lastVisibleColumn + PLAN_FACT_COLUMN_OVERSCAN + 1) / PLAN_FACT_COLUMN_WINDOW_STEP) * PLAN_FACT_COLUMN_WINDOW_STEP - 1
12193
+ );
12194
+ return previous?.start === rangeStart && previous.end === rangeEnd ? previous : { start: rangeStart, end: rangeEnd };
12195
+ });
11940
12196
  };
11941
12197
  const scheduleUpdate = () => {
11942
12198
  if (frameId !== null) return;
@@ -11957,16 +12213,16 @@ function TaskGanttChartInner(props, ref) {
11957
12213
  container.removeEventListener("scroll", scheduleUpdate);
11958
12214
  window.removeEventListener("resize", scheduleUpdate);
11959
12215
  };
11960
- }, [timelineHeaderHeight]);
12216
+ }, [dateRange.length, dayWidth, isPlanFactMode, showTaskList, taskListWidth, timelineHeaderHeight]);
11961
12217
  const scrollToToday = useCallback10(() => {
11962
12218
  if (isTableMatrixMode) return;
11963
12219
  const container = scrollContainerRef.current;
11964
12220
  if (!container || dateRange.length === 0) return;
11965
12221
  const now = /* @__PURE__ */ new Date();
11966
12222
  const today = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
11967
- const todayIndex = dateRange.findIndex((day) => day.getTime() === today.getTime());
11968
- if (todayIndex === -1) return;
11969
- const todayOffset = todayIndex * dayWidth;
12223
+ const todayIndex2 = dateRange.findIndex((day) => day.getTime() === today.getTime());
12224
+ if (todayIndex2 === -1) return;
12225
+ const todayOffset = todayIndex2 * dayWidth;
11970
12226
  const containerWidth = container.clientWidth;
11971
12227
  const scrollLeft = Math.round(todayOffset + dayWidth / 2 - containerWidth * 0.3);
11972
12228
  container.scrollTo({ left: Math.max(0, scrollLeft), behavior: "smooth" });
@@ -12202,6 +12458,15 @@ function TaskGanttChartInner(props, ref) {
12202
12458
  }
12203
12459
  return Array.from(indices).sort((left, right) => left - right);
12204
12460
  }, [effectiveRowHeight, forcedRenderedTaskIds, scrollViewport, visibleTaskIndexMap, visibleTasks.length]);
12461
+ const visiblePlanFactDateIndices = useMemo13(() => {
12462
+ if (!isPlanFactMode || dateRange.length === 0 || !planFactDateWindow) {
12463
+ return void 0;
12464
+ }
12465
+ return Array.from(
12466
+ { length: planFactDateWindow.end - planFactDateWindow.start + 1 },
12467
+ (_, index) => planFactDateWindow.start + index
12468
+ );
12469
+ }, [dateRange.length, isPlanFactMode, planFactDateWindow]);
12205
12470
  const renderedChartTasks = useMemo13(
12206
12471
  () => visibleTaskWindowIndices.map((index) => {
12207
12472
  const task = previewVisibleTasks[index];
@@ -12458,7 +12723,10 @@ function TaskGanttChartInner(props, ref) {
12458
12723
  startX: e.clientX,
12459
12724
  startY: e.clientY,
12460
12725
  scrollX: container.scrollLeft,
12461
- scrollY: container.scrollTop
12726
+ scrollY: container.scrollTop,
12727
+ currentX: e.clientX,
12728
+ currentY: e.clientY,
12729
+ frameId: null
12462
12730
  };
12463
12731
  if (document.activeElement instanceof HTMLElement) {
12464
12732
  document.activeElement.blur();
@@ -12467,16 +12735,37 @@ function TaskGanttChartInner(props, ref) {
12467
12735
  e.preventDefault();
12468
12736
  }, []);
12469
12737
  useEffect10(() => {
12470
- const handlePanMove = (e) => {
12738
+ const flushPanMove = () => {
12471
12739
  const pan = panStateRef.current;
12472
12740
  if (!pan?.active) return;
12741
+ pan.frameId = null;
12473
12742
  const container = scrollContainerRef.current;
12474
12743
  if (!container) return;
12475
- container.scrollLeft = pan.scrollX - (e.clientX - pan.startX);
12476
- container.scrollTop = pan.scrollY - (e.clientY - pan.startY);
12744
+ const maxScrollLeft = Math.max(0, container.scrollWidth - container.clientWidth);
12745
+ const maxScrollTop = Math.max(0, container.scrollHeight - container.clientHeight);
12746
+ const nextScrollLeft = clampScrollValue(pan.scrollX - (pan.currentX - pan.startX), maxScrollLeft);
12747
+ const nextScrollTop = clampScrollValue(pan.scrollY - (pan.currentY - pan.startY), maxScrollTop);
12748
+ if (Math.abs(container.scrollLeft - nextScrollLeft) > 0.5) {
12749
+ container.scrollLeft = nextScrollLeft;
12750
+ }
12751
+ if (Math.abs(container.scrollTop - nextScrollTop) > 0.5) {
12752
+ container.scrollTop = nextScrollTop;
12753
+ }
12754
+ };
12755
+ const handlePanMove = (e) => {
12756
+ const pan = panStateRef.current;
12757
+ if (!pan?.active) return;
12758
+ pan.currentX = e.clientX;
12759
+ pan.currentY = e.clientY;
12760
+ if (pan.frameId !== null) return;
12761
+ pan.frameId = window.requestAnimationFrame(flushPanMove);
12477
12762
  };
12478
12763
  const handlePanEnd = () => {
12479
- if (!panStateRef.current?.active) return;
12764
+ const pan = panStateRef.current;
12765
+ if (!pan?.active) return;
12766
+ if (pan.frameId !== null) {
12767
+ window.cancelAnimationFrame(pan.frameId);
12768
+ }
12480
12769
  panStateRef.current = null;
12481
12770
  const container = scrollContainerRef.current;
12482
12771
  if (container) container.style.cursor = "";
@@ -12603,7 +12892,10 @@ function TaskGanttChartInner(props, ref) {
12603
12892
  onTasksChange: handleTaskChange,
12604
12893
  onCellCommit: onPlanFactCellCommit,
12605
12894
  highlightedTaskIds: taskListHighlightedTaskIds,
12606
- filterMode
12895
+ filterMode,
12896
+ visibleRowIndices: visibleTaskWindowIndices,
12897
+ visibleDateIndices: visiblePlanFactDateIndices,
12898
+ todayDateIndex: todayInRange ? todayIndex : void 0
12607
12899
  }
12608
12900
  ) : /* @__PURE__ */ jsxs15(Fragment4, { children: [
12609
12901
  /* @__PURE__ */ jsxs15(