gantt-lib 0.1.1 → 0.2.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
@@ -55,6 +55,7 @@ __export(index_exports, {
55
55
  calculateTaskBar: () => calculateTaskBar,
56
56
  calculateWeekendBlocks: () => calculateWeekendBlocks,
57
57
  cascadeByLinks: () => cascadeByLinks,
58
+ computeLagFromDates: () => computeLagFromDates,
58
59
  detectCycles: () => detectCycles,
59
60
  detectEdgeZone: () => detectEdgeZone,
60
61
  formatDateLabel: () => formatDateLabel,
@@ -271,11 +272,35 @@ function detectCycles(tasks) {
271
272
  }
272
273
  return { hasCycle: false };
273
274
  }
275
+ function computeLagFromDates(linkType, predStart, predEnd, succStart, succEnd) {
276
+ const DAY_MS = 24 * 60 * 60 * 1e3;
277
+ const pS = Date.UTC(predStart.getUTCFullYear(), predStart.getUTCMonth(), predStart.getUTCDate());
278
+ const pE = Date.UTC(predEnd.getUTCFullYear(), predEnd.getUTCMonth(), predEnd.getUTCDate());
279
+ const sS = Date.UTC(succStart.getUTCFullYear(), succStart.getUTCMonth(), succStart.getUTCDate());
280
+ const sE = Date.UTC(succEnd.getUTCFullYear(), succEnd.getUTCMonth(), succEnd.getUTCDate());
281
+ switch (linkType) {
282
+ case "FS":
283
+ return Math.round((sS - pE) / DAY_MS) - 1;
284
+ case "SS":
285
+ return Math.round((sS - pS) / DAY_MS);
286
+ case "FF":
287
+ return Math.round((sE - pE) / DAY_MS);
288
+ case "SF":
289
+ return Math.round((sE - pS) / DAY_MS) + 1;
290
+ }
291
+ }
274
292
  function calculateSuccessorDate(predecessorStart, predecessorEnd, linkType, lag = 0) {
275
- const baseDate = linkType.startsWith("F") ? predecessorEnd : predecessorStart;
276
- const lagMs = lag * 24 * 60 * 60 * 1e3;
277
- const resultDate = new Date(baseDate.getTime() + lagMs);
278
- return resultDate;
293
+ const DAY_MS = 24 * 60 * 60 * 1e3;
294
+ switch (linkType) {
295
+ case "FS":
296
+ return new Date(predecessorEnd.getTime() + (lag + 1) * DAY_MS);
297
+ case "SS":
298
+ return new Date(predecessorStart.getTime() + lag * DAY_MS);
299
+ case "FF":
300
+ return new Date(predecessorEnd.getTime() + lag * DAY_MS);
301
+ case "SF":
302
+ return new Date(predecessorStart.getTime() + (lag - 1) * DAY_MS);
303
+ }
279
304
  }
280
305
  function validateDependencies(tasks) {
281
306
  const errors = [];
@@ -422,35 +447,10 @@ function recalculateIncomingLags(task, newStartDate, newEndDate, allTasks) {
422
447
  return task.dependencies.map((dep) => {
423
448
  const predecessor = taskById.get(dep.taskId);
424
449
  if (!predecessor) return dep;
425
- if (dep.type === "FS") {
426
- const predEnd = new Date(predecessor.endDate);
427
- const lagDays = Math.round(
428
- (Date.UTC(newStartDate.getUTCFullYear(), newStartDate.getUTCMonth(), newStartDate.getUTCDate()) - Date.UTC(predEnd.getUTCFullYear(), predEnd.getUTCMonth(), predEnd.getUTCDate())) / (24 * 60 * 60 * 1e3)
429
- );
430
- return { ...dep, lag: lagDays };
431
- }
432
- if (dep.type === "SS") {
433
- const predStart = new Date(predecessor.startDate);
434
- const lagDays = Math.max(0, Math.round(
435
- (Date.UTC(newStartDate.getUTCFullYear(), newStartDate.getUTCMonth(), newStartDate.getUTCDate()) - Date.UTC(predStart.getUTCFullYear(), predStart.getUTCMonth(), predStart.getUTCDate())) / (24 * 60 * 60 * 1e3)
436
- ));
437
- return { ...dep, lag: lagDays };
438
- }
439
- if (dep.type === "FF") {
440
- const predEnd = new Date(predecessor.endDate);
441
- const lagDays = Math.round(
442
- (Date.UTC(newEndDate.getUTCFullYear(), newEndDate.getUTCMonth(), newEndDate.getUTCDate()) - Date.UTC(predEnd.getUTCFullYear(), predEnd.getUTCMonth(), predEnd.getUTCDate())) / (24 * 60 * 60 * 1e3)
443
- );
444
- return { ...dep, lag: lagDays };
445
- }
446
- if (dep.type === "SF") {
447
- const predStart = new Date(predecessor.startDate);
448
- const lagDays = Math.min(0, Math.round(
449
- (Date.UTC(newEndDate.getUTCFullYear(), newEndDate.getUTCMonth(), newEndDate.getUTCDate()) - Date.UTC(predStart.getUTCFullYear(), predStart.getUTCMonth(), predStart.getUTCDate()) + 24 * 60 * 60 * 1e3) / (24 * 60 * 60 * 1e3)
450
- ));
451
- return { ...dep, lag: lagDays };
452
- }
453
- return dep;
450
+ const predStart = new Date(predecessor.startDate);
451
+ const predEnd = new Date(predecessor.endDate);
452
+ const lagDays = computeLagFromDates(dep.type, predStart, predEnd, newStartDate, newEndDate);
453
+ return { ...dep, lag: lagDays };
454
454
  });
455
455
  }
456
456
  function getAllDependencyEdges(tasks) {
@@ -1353,15 +1353,11 @@ var TodayIndicator = ({ monthStart, dayWidth }) => {
1353
1353
  today.getMonth(),
1354
1354
  today.getDate()
1355
1355
  ));
1356
- const isInMonth = (0, import_react4.useMemo)(() => {
1357
- return todayLocal.getUTCFullYear() === monthStart.getUTCFullYear() && todayLocal.getUTCMonth() === monthStart.getUTCMonth();
1358
- }, [monthStart, todayLocal]);
1359
1356
  const position = (0, import_react4.useMemo)(() => {
1360
- if (!isInMonth) return null;
1361
1357
  const offset = getDayOffset(todayLocal, monthStart);
1362
1358
  return Math.round(offset * dayWidth);
1363
- }, [isInMonth, monthStart, dayWidth, todayLocal]);
1364
- if (!isInMonth || position === null) {
1359
+ }, [monthStart, dayWidth, todayLocal]);
1360
+ if (isNaN(position)) {
1365
1361
  return null;
1366
1362
  }
1367
1363
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -1481,60 +1477,11 @@ var DragGuideLines_default = DragGuideLines;
1481
1477
  var import_react6 = __toESM(require("react"));
1482
1478
  var import_jsx_runtime6 = require("react/jsx-runtime");
1483
1479
  function calculateEffectiveLag(edge, predPosition, succPosition, monthStart, dayWidth) {
1484
- const predStartDate = pixelsToDate(predPosition.left, monthStart, dayWidth);
1485
- const predEndDate = pixelsToDate(predPosition.right - dayWidth, monthStart, dayWidth);
1486
- const succStartDate = pixelsToDate(succPosition.left, monthStart, dayWidth);
1487
- const succEndDate = pixelsToDate(succPosition.right - dayWidth, monthStart, dayWidth);
1488
- let lagMs = 0;
1489
- switch (edge.type) {
1490
- case "FS":
1491
- lagMs = Date.UTC(
1492
- succStartDate.getUTCFullYear(),
1493
- succStartDate.getUTCMonth(),
1494
- succStartDate.getUTCDate()
1495
- ) - Date.UTC(
1496
- predEndDate.getUTCFullYear(),
1497
- predEndDate.getUTCMonth(),
1498
- predEndDate.getUTCDate()
1499
- ) - 24 * 60 * 60 * 1e3;
1500
- break;
1501
- case "SS":
1502
- lagMs = Date.UTC(
1503
- succStartDate.getUTCFullYear(),
1504
- succStartDate.getUTCMonth(),
1505
- succStartDate.getUTCDate()
1506
- ) - Date.UTC(
1507
- predStartDate.getUTCFullYear(),
1508
- predStartDate.getUTCMonth(),
1509
- predStartDate.getUTCDate()
1510
- );
1511
- break;
1512
- case "FF":
1513
- lagMs = Date.UTC(
1514
- succEndDate.getUTCFullYear(),
1515
- succEndDate.getUTCMonth(),
1516
- succEndDate.getUTCDate()
1517
- ) - Date.UTC(
1518
- predEndDate.getUTCFullYear(),
1519
- predEndDate.getUTCMonth(),
1520
- predEndDate.getUTCDate()
1521
- );
1522
- break;
1523
- case "SF":
1524
- lagMs = Date.UTC(
1525
- succEndDate.getUTCFullYear(),
1526
- succEndDate.getUTCMonth(),
1527
- succEndDate.getUTCDate()
1528
- ) - Date.UTC(
1529
- predStartDate.getUTCFullYear(),
1530
- predStartDate.getUTCMonth(),
1531
- predStartDate.getUTCDate()
1532
- ) + 24 * 60 * 60 * 1e3;
1533
- break;
1534
- default:
1535
- return 0;
1536
- }
1537
- return Math.round(lagMs / (24 * 60 * 60 * 1e3));
1480
+ const predStart = pixelsToDate(predPosition.left, monthStart, dayWidth);
1481
+ const predEnd = pixelsToDate(predPosition.right - dayWidth, monthStart, dayWidth);
1482
+ const succStart = pixelsToDate(succPosition.left, monthStart, dayWidth);
1483
+ const succEnd = pixelsToDate(succPosition.right - dayWidth, monthStart, dayWidth);
1484
+ return computeLagFromDates(edge.type, predStart, predEnd, succStart, succEnd);
1538
1485
  }
1539
1486
  var DependencyLines = import_react6.default.memo(({
1540
1487
  tasks,
@@ -1542,7 +1489,8 @@ var DependencyLines = import_react6.default.memo(({
1542
1489
  dayWidth,
1543
1490
  rowHeight,
1544
1491
  gridWidth,
1545
- dragOverrides
1492
+ dragOverrides,
1493
+ selectedDep
1546
1494
  }) => {
1547
1495
  const { taskPositions, taskIndices } = (0, import_react6.useMemo)(() => {
1548
1496
  const positions = /* @__PURE__ */ new Map();
@@ -1656,30 +1604,60 @@ var DependencyLines = import_react6.default.memo(({
1656
1604
  }
1657
1605
  )
1658
1606
  }
1659
- )
1660
- ] }),
1661
- lines.map(({ id, path, hasCycle, lag, fromX, toX, fromY, reverseOrder }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react6.default.Fragment, { children: [
1662
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1663
- "path",
1664
- {
1665
- d: path,
1666
- className: hasCycle ? "gantt-dependency-path gantt-dependency-cycle" : "gantt-dependency-path",
1667
- markerEnd: hasCycle ? "url(#arrowhead-cycle)" : "url(#arrowhead)"
1668
- }
1669
1607
  ),
1670
- lag !== 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1671
- "text",
1608
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1609
+ "marker",
1672
1610
  {
1673
- className: "gantt-dependency-lag-label",
1674
- x: lag < 0 ? toX + 14 : toX - 14,
1675
- y: reverseOrder ? fromY - 4 : fromY + 12,
1676
- textAnchor: "middle",
1677
- fontSize: "10",
1678
- fill: hasCycle ? "var(--gantt-dependency-cycle-color, #ef4444)" : "var(--gantt-dependency-line-color, #666666)",
1679
- children: lag > 0 ? `+${lag}` : `${lag}`
1611
+ id: "arrowhead-selected",
1612
+ markerWidth: "8",
1613
+ markerHeight: "6",
1614
+ markerUnits: "userSpaceOnUse",
1615
+ refX: "7",
1616
+ refY: "3",
1617
+ orient: "auto",
1618
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1619
+ "polygon",
1620
+ {
1621
+ points: "0 0, 8 3, 0 6",
1622
+ fill: "#ef4444"
1623
+ }
1624
+ )
1680
1625
  }
1681
1626
  )
1682
- ] }, id))
1627
+ ] }),
1628
+ lines.map(({ id, path, hasCycle, lag, fromX, toX, fromY, reverseOrder }) => {
1629
+ const isSelected = selectedDep != null && id === `${selectedDep.predecessorId}-${selectedDep.successorId}-${selectedDep.linkType}`;
1630
+ let pathClassName = "gantt-dependency-path";
1631
+ if (isSelected) pathClassName += " gantt-dependency-selected";
1632
+ else if (hasCycle) pathClassName += " gantt-dependency-cycle";
1633
+ let markerEnd;
1634
+ if (isSelected) markerEnd = "url(#arrowhead-selected)";
1635
+ else if (hasCycle) markerEnd = "url(#arrowhead-cycle)";
1636
+ else markerEnd = "url(#arrowhead)";
1637
+ const lagColor = isSelected ? "#ef4444" : hasCycle ? "var(--gantt-dependency-cycle-color, #ef4444)" : "var(--gantt-dependency-line-color, #666666)";
1638
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react6.default.Fragment, { children: [
1639
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1640
+ "path",
1641
+ {
1642
+ d: path,
1643
+ className: pathClassName,
1644
+ markerEnd
1645
+ }
1646
+ ),
1647
+ lag !== 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1648
+ "text",
1649
+ {
1650
+ className: "gantt-dependency-lag-label",
1651
+ x: lag < 0 ? toX + 14 : toX - 14,
1652
+ y: reverseOrder ? fromY - 4 : fromY + 12,
1653
+ textAnchor: "middle",
1654
+ fontSize: "10",
1655
+ fill: lagColor,
1656
+ children: lag > 0 ? `+${lag}` : `${lag}`
1657
+ }
1658
+ )
1659
+ ] }, id);
1660
+ })
1683
1661
  ]
1684
1662
  }
1685
1663
  );
@@ -1688,17 +1666,49 @@ DependencyLines.displayName = "DependencyLines";
1688
1666
  var DependencyLines_default = DependencyLines;
1689
1667
 
1690
1668
  // src/components/TaskList/TaskList.tsx
1691
- var import_react11 = require("react");
1669
+ var import_react11 = __toESM(require("react"));
1670
+
1671
+ // src/components/ui/Popover.tsx
1672
+ var RadixPopover = __toESM(require("@radix-ui/react-popover"));
1673
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1674
+ var Popover = ({ open, onOpenChange, children }) => {
1675
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(RadixPopover.Root, { open, onOpenChange, children });
1676
+ };
1677
+ var PopoverTrigger = RadixPopover.Trigger;
1678
+ var PopoverContent = ({
1679
+ children,
1680
+ className,
1681
+ align = "start",
1682
+ side = "bottom",
1683
+ portal = true,
1684
+ collisionPadding = 8
1685
+ }) => {
1686
+ const content = /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1687
+ RadixPopover.Content,
1688
+ {
1689
+ className: `gantt-popover${className ? ` ${className}` : ""}`,
1690
+ align,
1691
+ side,
1692
+ collisionPadding,
1693
+ sideOffset: 4,
1694
+ children
1695
+ }
1696
+ );
1697
+ if (portal) {
1698
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(RadixPopover.Portal, { children: content });
1699
+ }
1700
+ return content;
1701
+ };
1692
1702
 
1693
1703
  // src/components/TaskList/TaskListRow.tsx
1694
1704
  var import_react10 = __toESM(require("react"));
1695
1705
 
1696
1706
  // src/components/ui/Input.tsx
1697
1707
  var import_react7 = __toESM(require("react"));
1698
- var import_jsx_runtime7 = require("react/jsx-runtime");
1708
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1699
1709
  var Input = import_react7.default.forwardRef(
1700
1710
  ({ className, ...props }, ref) => {
1701
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1711
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1702
1712
  "input",
1703
1713
  {
1704
1714
  ref,
@@ -1718,7 +1728,7 @@ var import_date_fns3 = require("date-fns");
1718
1728
  var import_react8 = require("react");
1719
1729
  var import_date_fns2 = require("date-fns");
1720
1730
  var import_locale2 = require("date-fns/locale");
1721
- var import_jsx_runtime8 = require("react/jsx-runtime");
1731
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1722
1732
  function getDayClassName(day, selected) {
1723
1733
  const classes = ["gantt-day-btn"];
1724
1734
  if (selected && (0, import_date_fns2.isSameDay)(day, selected)) classes.push("selected");
@@ -1788,12 +1798,12 @@ var Calendar = ({
1788
1798
  const emptyDays = ((0, import_date_fns2.getDay)(firstDay) + 6) % 7;
1789
1799
  const monthKey = (0, import_date_fns2.format)(month, "yyyy-MM");
1790
1800
  const monthLabel = (0, import_date_fns2.format)(month, "LLLL yyyy", { locale: import_locale2.ru });
1791
- const emptyCells = Array.from({ length: emptyDays }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "gantt-cal-empty-day" }, `e-${i}`));
1801
+ const emptyCells = Array.from({ length: emptyDays }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "gantt-cal-empty-day" }, `e-${i}`));
1792
1802
  const dayCells = Array.from({ length: totalDays }, (_, i) => {
1793
1803
  const dayNum = i + 1;
1794
1804
  const day = new Date(month.getFullYear(), month.getMonth(), dayNum);
1795
1805
  const className = getDayClassName(day, selected);
1796
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1806
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1797
1807
  "button",
1798
1808
  {
1799
1809
  type: "button",
@@ -1809,9 +1819,9 @@ var Calendar = ({
1809
1819
  dayNum
1810
1820
  );
1811
1821
  });
1812
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "gantt-cal-month", "data-month": monthKey, children: [
1813
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "gantt-cal-month-header", children: monthLabel }),
1814
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "gantt-cal-month-days", children: [
1822
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "gantt-cal-month", "data-month": monthKey, children: [
1823
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "gantt-cal-month-header", children: monthLabel }),
1824
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "gantt-cal-month-days", children: [
1815
1825
  emptyCells,
1816
1826
  dayCells
1817
1827
  ] })
@@ -1823,42 +1833,10 @@ var Calendar = ({
1823
1833
  () => months.map(renderMonth),
1824
1834
  [months, renderMonth]
1825
1835
  );
1826
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { ref: scrollRef, className: "gantt-cal-container", children: renderedMonths });
1836
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { ref: scrollRef, className: "gantt-cal-container", children: renderedMonths });
1827
1837
  };
1828
1838
  Calendar.displayName = "Calendar";
1829
1839
 
1830
- // src/components/ui/Popover.tsx
1831
- var RadixPopover = __toESM(require("@radix-ui/react-popover"));
1832
- var import_jsx_runtime9 = require("react/jsx-runtime");
1833
- var Popover = ({ open, onOpenChange, children }) => {
1834
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(RadixPopover.Root, { open, onOpenChange, children });
1835
- };
1836
- var PopoverTrigger = RadixPopover.Trigger;
1837
- var PopoverContent = ({
1838
- children,
1839
- className,
1840
- align = "start",
1841
- side = "bottom",
1842
- portal = true,
1843
- collisionPadding = 8
1844
- }) => {
1845
- const content = /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1846
- RadixPopover.Content,
1847
- {
1848
- className: `gantt-popover${className ? ` ${className}` : ""}`,
1849
- align,
1850
- side,
1851
- collisionPadding,
1852
- sideOffset: 4,
1853
- children
1854
- }
1855
- );
1856
- if (portal) {
1857
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(RadixPopover.Portal, { children: content });
1858
- }
1859
- return content;
1860
- };
1861
-
1862
1840
  // src/components/ui/DatePicker.tsx
1863
1841
  var import_jsx_runtime10 = require("react/jsx-runtime");
1864
1842
  var DatePicker = ({
@@ -1923,19 +1901,182 @@ var DatePicker = ({
1923
1901
  };
1924
1902
  DatePicker.displayName = "DatePicker";
1925
1903
 
1926
- // src/components/TaskList/TaskListRow.tsx
1904
+ // src/components/TaskList/DepIcons.tsx
1927
1905
  var import_jsx_runtime11 = require("react/jsx-runtime");
1906
+ var DepIconFS = () => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1907
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "m10 15 5 5 5-5" }),
1908
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M4 4h7a4 4 0 0 1 4 4v12" })
1909
+ ] });
1910
+ var DepIconSS = () => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1911
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M3 5v14" }),
1912
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M21 12H7" }),
1913
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "m15 18 6-6-6-6" })
1914
+ ] });
1915
+ var DepIconFF = () => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1916
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M17 12H3" }),
1917
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "m11 18 6-6-6-6" }),
1918
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M21 5v14" })
1919
+ ] });
1920
+ var DepIconSF = () => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1921
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "m14 15-5 5-5-5" }),
1922
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M20 4h-7a4 4 0 0 0-4 4v12" })
1923
+ ] });
1924
+ var LINK_TYPE_ICONS = {
1925
+ FS: DepIconFS,
1926
+ SS: DepIconSS,
1927
+ FF: DepIconFF,
1928
+ SF: DepIconSF
1929
+ };
1930
+ var LINK_TYPE_LABELS = {
1931
+ FS: "\u041E\u043A\u043E\u043D\u0447\u0430\u043D\u0438\u0435-\u043D\u0430\u0447\u0430\u043B\u043E",
1932
+ SS: "\u041D\u0430\u0447\u0430\u043B\u043E-\u043D\u0430\u0447\u0430\u043B\u043E",
1933
+ FF: "\u041E\u043A\u043E\u043D\u0447\u0430\u043D\u0438\u0435-\u043E\u043A\u043E\u043D\u0447\u0430\u043D\u0438\u0435",
1934
+ SF: "\u041D\u0430\u0447\u0430\u043B\u043E-\u043E\u043A\u043E\u043D\u0447\u0430\u043D\u0438\u0435"
1935
+ };
1936
+
1937
+ // src/components/TaskList/TaskListRow.tsx
1938
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1939
+ var TrashIcon = () => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("svg", { xmlns: "http://www.w3.org/2000/svg", width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1940
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" }),
1941
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("path", { d: "M3 6h18" }),
1942
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("path", { d: "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" })
1943
+ ] });
1944
+ function formatDepDescription(type, lag) {
1945
+ const effectiveLag = lag ?? 0;
1946
+ if (type === "FS") {
1947
+ if (effectiveLag > 0) return `\u041D\u0430\u0447\u0430\u0442\u044C \u0447\u0435\u0440\u0435\u0437 ${effectiveLag} \u0434\u043D. \u043F\u043E\u0441\u043B\u0435 \u043E\u043A\u043E\u043D\u0447\u0430\u043D\u0438\u044F`;
1948
+ if (effectiveLag < 0) return `\u041D\u0430\u0447\u0430\u0442\u044C \u0437\u0430 ${Math.abs(effectiveLag)} \u0434\u043D. \u0434\u043E \u043E\u043A\u043E\u043D\u0447\u0430\u043D\u0438\u044F`;
1949
+ return `\u041D\u0430\u0447\u0430\u0442\u044C \u0441\u0440\u0430\u0437\u0443 \u043F\u043E\u0441\u043B\u0435 \u043E\u043A\u043E\u043D\u0447\u0430\u043D\u0438\u044F`;
1950
+ }
1951
+ if (type === "FF") {
1952
+ if (effectiveLag > 0) return `\u0417\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C \u0447\u0435\u0440\u0435\u0437 ${effectiveLag} \u0434\u043D. \u043F\u043E\u0441\u043B\u0435 \u043E\u043A\u043E\u043D\u0447\u0430\u043D\u0438\u044F`;
1953
+ if (effectiveLag < 0) return `\u0417\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C \u0437\u0430 ${Math.abs(effectiveLag)} \u0434\u043D. \u0434\u043E \u043E\u043A\u043E\u043D\u0447\u0430\u043D\u0438\u044F`;
1954
+ return `\u0417\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C \u043F\u043E\u0441\u043B\u0435 \u043E\u043A\u043E\u043D\u0447\u0430\u043D\u0438\u044F`;
1955
+ }
1956
+ if (type === "SS") {
1957
+ if (effectiveLag > 0) return `\u041D\u0430\u0447\u0430\u0442\u044C \u0447\u0435\u0440\u0435\u0437 ${effectiveLag} \u0434\u043D. \u043F\u043E\u0441\u043B\u0435 \u043D\u0430\u0447\u0430\u043B\u0430`;
1958
+ if (effectiveLag < 0) return `\u041D\u0430\u0447\u0430\u0442\u044C \u0437\u0430 ${Math.abs(effectiveLag)} \u0434\u043D. \u0434\u043E \u043D\u0430\u0447\u0430\u043B\u0430`;
1959
+ return `\u041D\u0430\u0447\u0430\u0442\u044C \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u043D\u0430\u0447\u0430\u043B\u043E\u043C`;
1960
+ }
1961
+ if (type === "SF") {
1962
+ if (effectiveLag > 0) return `\u0417\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C \u0447\u0435\u0440\u0435\u0437 ${effectiveLag} \u0434\u043D. \u043F\u043E\u0441\u043B\u0435 \u043D\u0430\u0447\u0430\u043B\u0430`;
1963
+ if (effectiveLag < 0) return `\u0417\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C \u0437\u0430 ${Math.abs(effectiveLag)} \u0434\u043D. \u0434\u043E \u043D\u0430\u0447\u0430\u043B\u0430`;
1964
+ return `\u0417\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C \u0434\u043E \u043D\u0430\u0447\u0430\u043B\u0430`;
1965
+ }
1966
+ return "";
1967
+ }
1968
+ var DepChip = ({
1969
+ lag,
1970
+ dep,
1971
+ taskId,
1972
+ predecessorName,
1973
+ selectedChip,
1974
+ disableDependencyEditing,
1975
+ onChipSelect,
1976
+ onRowClick,
1977
+ onScrollToTask,
1978
+ onRemoveDependency,
1979
+ onChipSelectClear
1980
+ }) => {
1981
+ const isSelected = selectedChip?.successorId === taskId && selectedChip?.predecessorId === dep.taskId && selectedChip?.linkType === dep.type;
1982
+ const handleClick = (e) => {
1983
+ e.stopPropagation();
1984
+ if (disableDependencyEditing) return;
1985
+ onChipSelect?.(isSelected ? null : { successorId: taskId, predecessorId: dep.taskId, linkType: dep.type });
1986
+ if (!isSelected) {
1987
+ onRowClick?.(taskId);
1988
+ onScrollToTask?.(taskId);
1989
+ }
1990
+ };
1991
+ const handleTrashClick = (e) => {
1992
+ e.stopPropagation();
1993
+ onRemoveDependency?.(taskId, dep.taskId, dep.type);
1994
+ onChipSelectClear();
1995
+ };
1996
+ const Icon = LINK_TYPE_ICONS[dep.type];
1997
+ const depPrefix = formatDepDescription(dep.type, lag);
1998
+ const depName = predecessorName ?? dep.taskId;
1999
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(Popover, { open: isSelected, onOpenChange: (open) => {
2000
+ if (!open) onChipSelectClear();
2001
+ }, children: [
2002
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "gantt-tl-dep-chip-wrapper", children: [
2003
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2004
+ "span",
2005
+ {
2006
+ className: `gantt-tl-dep-chip${isSelected ? " gantt-tl-dep-chip-selected" : ""}`,
2007
+ onClick: handleClick,
2008
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
2009
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Icon, {}),
2010
+ lag != null && lag !== 0 ? lag > 0 ? `+${lag}` : `${lag}` : ""
2011
+ ] })
2012
+ }
2013
+ ) }),
2014
+ !disableDependencyEditing && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2015
+ "button",
2016
+ {
2017
+ type: "button",
2018
+ className: "gantt-tl-dep-chip-trash",
2019
+ "aria-label": "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0432\u044F\u0437\u044C",
2020
+ onClick: handleTrashClick,
2021
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(TrashIcon, {})
2022
+ }
2023
+ )
2024
+ ] }),
2025
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(PopoverContent, { portal: true, side: "bottom", align: "start", className: "gantt-tl-dep-info-popover", children: [
2026
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "gantt-tl-dep-info-prefix", children: depPrefix }),
2027
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "gantt-tl-dep-info-name", children: depName })
2028
+ ] })
2029
+ ] });
2030
+ };
1928
2031
  var toISODate = (value) => {
1929
2032
  if (value instanceof Date) return value.toISOString().split("T")[0];
1930
2033
  if (typeof value === "string" && value.includes("T")) return value.split("T")[0];
1931
2034
  return value;
1932
2035
  };
1933
2036
  var TaskListRow = import_react10.default.memo(
1934
- ({ task, rowIndex, rowHeight, onTaskChange, selectedTaskId, onRowClick, disableTaskNameEditing = false }) => {
2037
+ ({
2038
+ task,
2039
+ rowIndex,
2040
+ rowHeight,
2041
+ onTaskChange,
2042
+ selectedTaskId,
2043
+ onRowClick,
2044
+ disableTaskNameEditing = false,
2045
+ disableDependencyEditing = false,
2046
+ allTasks = [],
2047
+ activeLinkType,
2048
+ selectingPredecessorFor,
2049
+ onSetSelectingPredecessorFor,
2050
+ onAddDependency,
2051
+ onRemoveDependency,
2052
+ selectedChip,
2053
+ onChipSelect,
2054
+ onScrollToTask
2055
+ }) => {
1935
2056
  const [editingName, setEditingName] = (0, import_react10.useState)(false);
1936
2057
  const [nameValue, setNameValue] = (0, import_react10.useState)("");
1937
2058
  const nameInputRef = (0, import_react10.useRef)(null);
2059
+ const [overflowOpen, setOverflowOpen] = (0, import_react10.useState)(false);
1938
2060
  const isSelected = selectedTaskId === task.id;
2061
+ const isPicking = selectingPredecessorFor != null;
2062
+ const isSourceRow = isPicking && selectingPredecessorFor === task.id;
2063
+ const chips = (0, import_react10.useMemo)(() => {
2064
+ const succStart = new Date(task.startDate);
2065
+ const succEnd = new Date(task.endDate);
2066
+ const taskById = new Map((allTasks ?? []).map((t) => [t.id, t]));
2067
+ return (task.dependencies ?? []).map((dep) => {
2068
+ const pred = taskById.get(dep.taskId);
2069
+ const lag = pred ? computeLagFromDates(
2070
+ dep.type,
2071
+ new Date(pred.startDate),
2072
+ new Date(pred.endDate),
2073
+ succStart,
2074
+ succEnd
2075
+ ) : dep.lag ?? 0;
2076
+ return { dep, lag, predecessorName: pred?.name ?? dep.taskId };
2077
+ });
2078
+ }, [task.dependencies, task.startDate, task.endDate, allTasks]);
2079
+ const linkWord = chips.length <= 4 ? "\u0441\u0432\u044F\u0437\u0438" : "\u0441\u0432\u044F\u0437\u0435\u0439";
1939
2080
  (0, import_react10.useEffect)(() => {
1940
2081
  if (editingName && nameInputRef.current) {
1941
2082
  nameInputRef.current.focus();
@@ -1981,18 +2122,60 @@ var TaskListRow = import_react10.default.memo(
1981
2122
  const handleRowClickInternal = (0, import_react10.useCallback)(() => {
1982
2123
  onRowClick?.(task.id);
1983
2124
  }, [task.id, onRowClick]);
2125
+ const handleNumberClick = (0, import_react10.useCallback)((e) => {
2126
+ e.stopPropagation();
2127
+ onRowClick?.(task.id);
2128
+ onScrollToTask?.(task.id);
2129
+ }, [task.id, onRowClick, onScrollToTask]);
2130
+ const handleAddClick = (0, import_react10.useCallback)((e) => {
2131
+ e.stopPropagation();
2132
+ onSetSelectingPredecessorFor?.(task.id);
2133
+ }, [task.id, onSetSelectingPredecessorFor]);
2134
+ const handlePredecessorPick = (0, import_react10.useCallback)((e) => {
2135
+ e.stopPropagation();
2136
+ if (!isPicking || isSourceRow) return;
2137
+ if (!selectingPredecessorFor || !activeLinkType) return;
2138
+ onAddDependency?.(task.id, selectingPredecessorFor, activeLinkType);
2139
+ }, [isPicking, isSourceRow, selectingPredecessorFor, task.id, activeLinkType, onAddDependency]);
2140
+ const isSelectedPredecessor = selectedChip != null && selectedChip.predecessorId === task.id;
2141
+ const handleDeleteSelected = (0, import_react10.useCallback)((e) => {
2142
+ e.stopPropagation();
2143
+ if (!selectedChip) return;
2144
+ onRemoveDependency?.(selectedChip.successorId, selectedChip.predecessorId, selectedChip.linkType);
2145
+ onChipSelect?.(null);
2146
+ }, [selectedChip, onRemoveDependency, onChipSelect]);
1984
2147
  const startDateISO = toISODate(task.startDate);
1985
2148
  const endDateISO = toISODate(task.endDate);
1986
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
2149
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1987
2150
  "div",
1988
2151
  {
1989
- className: `gantt-tl-row ${isSelected ? "gantt-tl-row-selected" : ""}`,
2152
+ className: [
2153
+ "gantt-tl-row",
2154
+ isSelected ? "gantt-tl-row-selected" : "",
2155
+ isPicking && !isSourceRow ? "gantt-tl-row-picking" : "",
2156
+ isSourceRow ? "gantt-tl-row-picking-self" : ""
2157
+ ].filter(Boolean).join(" "),
1990
2158
  style: { minHeight: `${rowHeight}px` },
1991
2159
  onClick: handleRowClickInternal,
1992
2160
  children: [
1993
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "gantt-tl-cell gantt-tl-cell-number", children: rowIndex + 1 }),
1994
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "gantt-tl-cell gantt-tl-cell-name", children: [
1995
- editingName && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2161
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
2162
+ "div",
2163
+ {
2164
+ className: "gantt-tl-cell gantt-tl-cell-number",
2165
+ onClick: handleNumberClick,
2166
+ title: "\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u0440\u0430\u0431\u043E\u0442\u0435",
2167
+ children: [
2168
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "gantt-tl-num-label", children: rowIndex + 1 }),
2169
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("svg", { className: "gantt-tl-num-icon", xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2170
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("path", { d: "M17 12H3" }),
2171
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("path", { d: "m11 18 6-6-6-6" }),
2172
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("path", { d: "M21 5v14" })
2173
+ ] })
2174
+ ]
2175
+ }
2176
+ ),
2177
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "gantt-tl-cell gantt-tl-cell-name", children: [
2178
+ editingName && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1996
2179
  Input,
1997
2180
  {
1998
2181
  ref: nameInputRef,
@@ -2005,7 +2188,7 @@ var TaskListRow = import_react10.default.memo(
2005
2188
  onClick: (e) => e.stopPropagation()
2006
2189
  }
2007
2190
  ),
2008
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2191
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2009
2192
  "button",
2010
2193
  {
2011
2194
  type: "button",
@@ -2016,7 +2199,7 @@ var TaskListRow = import_react10.default.memo(
2016
2199
  }
2017
2200
  )
2018
2201
  ] }),
2019
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "gantt-tl-cell gantt-tl-cell-date", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2202
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "gantt-tl-cell gantt-tl-cell-date", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2020
2203
  DatePicker,
2021
2204
  {
2022
2205
  value: startDateISO,
@@ -2026,7 +2209,7 @@ var TaskListRow = import_react10.default.memo(
2026
2209
  disabled: task.locked
2027
2210
  }
2028
2211
  ) }),
2029
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "gantt-tl-cell gantt-tl-cell-date", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2212
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "gantt-tl-cell gantt-tl-cell-date", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2030
2213
  DatePicker,
2031
2214
  {
2032
2215
  value: endDateISO,
@@ -2035,7 +2218,97 @@ var TaskListRow = import_react10.default.memo(
2035
2218
  portal: true,
2036
2219
  disabled: task.locked
2037
2220
  }
2038
- ) })
2221
+ ) }),
2222
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2223
+ "div",
2224
+ {
2225
+ className: "gantt-tl-cell gantt-tl-cell-deps",
2226
+ onClick: isPicking && !isSourceRow ? handlePredecessorPick : void 0,
2227
+ children: isSourceRow ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "gantt-tl-dep-source-hint", children: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0437\u0430\u0434\u0430\u0447\u0443" }) : isSelectedPredecessor && !disableDependencyEditing ? (
2228
+ /* Full-replacement: "Зависит от [name]" → hover → "Удалить" */
2229
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
2230
+ "button",
2231
+ {
2232
+ type: "button",
2233
+ className: "gantt-tl-dep-delete-label",
2234
+ onClick: handleDeleteSelected,
2235
+ "aria-label": "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0432\u044F\u0437\u044C",
2236
+ children: [
2237
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "gantt-tl-dep-delete-label-default", children: "\u0421\u0432\u044F\u0437\u0430\u043D\u043E \u0441" }),
2238
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "gantt-tl-dep-delete-label-hover", children: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0432\u044F\u0437\u044C" })
2239
+ ]
2240
+ }
2241
+ )
2242
+ ) : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
2243
+ chips.length >= 2 ? (
2244
+ /* 2+ deps — show only "N связей" summary chip that opens a popover */
2245
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(Popover, { open: overflowOpen, onOpenChange: setOverflowOpen, children: [
2246
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
2247
+ "button",
2248
+ {
2249
+ type: "button",
2250
+ className: "gantt-tl-dep-summary-chip",
2251
+ onClick: (e) => {
2252
+ e.stopPropagation();
2253
+ setOverflowOpen((v) => !v);
2254
+ },
2255
+ children: [
2256
+ chips.length,
2257
+ " ",
2258
+ linkWord
2259
+ ]
2260
+ }
2261
+ ) }),
2262
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PopoverContent, { portal: true, align: "start", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "gantt-tl-dep-overflow-list", onClick: (e) => e.stopPropagation(), children: chips.map(({ dep, lag, predecessorName }) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2263
+ DepChip,
2264
+ {
2265
+ lag,
2266
+ dep,
2267
+ taskId: task.id,
2268
+ predecessorName,
2269
+ selectedChip,
2270
+ disableDependencyEditing,
2271
+ onChipSelect,
2272
+ onRowClick,
2273
+ onScrollToTask,
2274
+ onRemoveDependency,
2275
+ onChipSelectClear: () => onChipSelect?.(null)
2276
+ },
2277
+ `${dep.taskId}-${dep.type}`
2278
+ )) }) })
2279
+ ] })
2280
+ ) : chips.length === 1 ? (
2281
+ /* Single chip — unified DepChip */
2282
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2283
+ DepChip,
2284
+ {
2285
+ lag: chips[0].lag,
2286
+ dep: chips[0].dep,
2287
+ taskId: task.id,
2288
+ predecessorName: chips[0].predecessorName,
2289
+ selectedChip,
2290
+ disableDependencyEditing,
2291
+ onChipSelect,
2292
+ onRowClick,
2293
+ onScrollToTask,
2294
+ onRemoveDependency,
2295
+ onChipSelectClear: () => onChipSelect?.(null)
2296
+ }
2297
+ )
2298
+ ) : null,
2299
+ !disableDependencyEditing && !isPicking && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2300
+ "button",
2301
+ {
2302
+ type: "button",
2303
+ className: "gantt-tl-dep-add",
2304
+ onClick: handleAddClick,
2305
+ "aria-label": "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0441\u0432\u044F\u0437\u044C",
2306
+ children: "+"
2307
+ }
2308
+ )
2309
+ ] })
2310
+ }
2311
+ )
2039
2312
  ]
2040
2313
  }
2041
2314
  );
@@ -2044,7 +2317,8 @@ var TaskListRow = import_react10.default.memo(
2044
2317
  TaskListRow.displayName = "TaskListRow";
2045
2318
 
2046
2319
  // src/components/TaskList/TaskList.tsx
2047
- var import_jsx_runtime12 = require("react/jsx-runtime");
2320
+ var import_jsx_runtime13 = require("react/jsx-runtime");
2321
+ var LINK_TYPE_ORDER = ["FS", "SS", "FF", "SF"];
2048
2322
  var TaskList = ({
2049
2323
  tasks,
2050
2324
  rowHeight,
@@ -2054,7 +2328,10 @@ var TaskList = ({
2054
2328
  selectedTaskId,
2055
2329
  onTaskSelect,
2056
2330
  show = true,
2057
- disableTaskNameEditing = false
2331
+ disableTaskNameEditing = false,
2332
+ disableDependencyEditing = false,
2333
+ onScrollToTask,
2334
+ onSelectedChipChange
2058
2335
  }) => {
2059
2336
  const totalHeight = (0, import_react11.useMemo)(
2060
2337
  () => tasks.length * rowHeight,
@@ -2063,19 +2340,144 @@ var TaskList = ({
2063
2340
  const handleRowClick = (0, import_react11.useCallback)((taskId) => {
2064
2341
  onTaskSelect?.(taskId);
2065
2342
  }, [onTaskSelect]);
2066
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2343
+ const [activeLinkType, setActiveLinkType] = (0, import_react11.useState)("FS");
2344
+ const [selectingPredecessorFor, setSelectingPredecessorFor] = (0, import_react11.useState)(null);
2345
+ const [typeMenuOpen, setTypeMenuOpen] = (0, import_react11.useState)(false);
2346
+ const [cycleError, setCycleError] = (0, import_react11.useState)(false);
2347
+ const overlayRef = (0, import_react11.useRef)(null);
2348
+ const [selectedChip, setSelectedChip] = (0, import_react11.useState)(null);
2349
+ const handleChipSelect = (0, import_react11.useCallback)((chip) => {
2350
+ setSelectedChip(chip);
2351
+ onSelectedChipChange?.(chip);
2352
+ }, [onSelectedChipChange]);
2353
+ (0, import_react11.useEffect)(() => {
2354
+ if (!selectingPredecessorFor && !selectedChip) return;
2355
+ const handleKeyDown = (e) => {
2356
+ if (e.key === "Escape") {
2357
+ setSelectingPredecessorFor(null);
2358
+ setSelectedChip(null);
2359
+ onSelectedChipChange?.(null);
2360
+ }
2361
+ };
2362
+ const handleMouseDown = (e) => {
2363
+ const target = e.target;
2364
+ if (overlayRef.current?.contains(target)) return;
2365
+ if (target.closest?.(".gantt-popover")) return;
2366
+ setSelectingPredecessorFor(null);
2367
+ setSelectedChip(null);
2368
+ onSelectedChipChange?.(null);
2369
+ };
2370
+ document.addEventListener("keydown", handleKeyDown);
2371
+ document.addEventListener("mousedown", handleMouseDown, true);
2372
+ return () => {
2373
+ document.removeEventListener("keydown", handleKeyDown);
2374
+ document.removeEventListener("mousedown", handleMouseDown, true);
2375
+ };
2376
+ }, [selectingPredecessorFor, selectedChip, onSelectedChipChange]);
2377
+ const handleAddDependency = (0, import_react11.useCallback)((successorTaskId, predecessorTaskId, linkType) => {
2378
+ if (successorTaskId === predecessorTaskId) return;
2379
+ const successor = tasks.find((t) => t.id === successorTaskId);
2380
+ if (!successor) return;
2381
+ const alreadyExists = (successor.dependencies ?? []).some(
2382
+ (d) => d.taskId === predecessorTaskId && d.type === linkType
2383
+ );
2384
+ if (alreadyExists) {
2385
+ setSelectingPredecessorFor(null);
2386
+ return;
2387
+ }
2388
+ const newDep = { taskId: predecessorTaskId, type: linkType, lag: 0 };
2389
+ const hypothetical = tasks.map(
2390
+ (t) => t.id === successorTaskId ? { ...t, dependencies: [...t.dependencies ?? [], newDep] } : t
2391
+ );
2392
+ const validation = validateDependencies(hypothetical);
2393
+ if (!validation.isValid) {
2394
+ setCycleError(true);
2395
+ setTimeout(() => setCycleError(false), 3e3);
2396
+ return;
2397
+ }
2398
+ const updatedTask = hypothetical.find((t) => t.id === successorTaskId);
2399
+ const predecessor = tasks.find((t) => t.id === predecessorTaskId);
2400
+ if (predecessor) {
2401
+ const predStart = new Date(predecessor.startDate);
2402
+ const predEnd = new Date(predecessor.endDate);
2403
+ const constraintDate = calculateSuccessorDate(predStart, predEnd, linkType, 0);
2404
+ const origSuccessor = tasks.find((t) => t.id === successorTaskId);
2405
+ const durationMs = new Date(origSuccessor.endDate).getTime() - new Date(origSuccessor.startDate).getTime();
2406
+ let newStart;
2407
+ let newEnd;
2408
+ if (linkType === "FS" || linkType === "SS") {
2409
+ newStart = constraintDate;
2410
+ newEnd = new Date(constraintDate.getTime() + durationMs);
2411
+ } else {
2412
+ newEnd = constraintDate;
2413
+ newStart = new Date(constraintDate.getTime() - durationMs);
2414
+ }
2415
+ const snappedTask = {
2416
+ ...updatedTask,
2417
+ startDate: newStart.toISOString().split("T")[0],
2418
+ endDate: newEnd.toISOString().split("T")[0]
2419
+ };
2420
+ onTaskChange?.(snappedTask);
2421
+ } else {
2422
+ onTaskChange?.(updatedTask);
2423
+ }
2424
+ setSelectingPredecessorFor(null);
2425
+ }, [tasks, onTaskChange]);
2426
+ const handleRemoveDependency = (0, import_react11.useCallback)((taskId, predecessorTaskId, linkType) => {
2427
+ const task = tasks.find((t) => t.id === taskId);
2428
+ if (!task) return;
2429
+ const updatedDeps = (task.dependencies ?? []).filter(
2430
+ (d) => !(d.taskId === predecessorTaskId && d.type === linkType)
2431
+ );
2432
+ onTaskChange?.({ ...task, dependencies: updatedDeps });
2433
+ }, [tasks, onTaskChange]);
2434
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2067
2435
  "div",
2068
2436
  {
2437
+ ref: overlayRef,
2069
2438
  className: `gantt-tl-overlay${show ? "" : " gantt-tl-hidden"}`,
2070
2439
  style: { width: `${taskListWidth}px` },
2071
- children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "gantt-tl-table", children: [
2072
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "gantt-tl-header", style: { height: `${headerHeight + 0.5}px` }, children: [
2073
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "gantt-tl-headerCell gantt-tl-cell-number", children: "\u2116" }),
2074
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "gantt-tl-headerCell gantt-tl-cell-name", children: "\u0418\u043C\u044F" }),
2075
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "gantt-tl-headerCell gantt-tl-cell-date", children: "\u041D\u0430\u0447\u0430\u043B\u043E" }),
2076
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "gantt-tl-headerCell gantt-tl-cell-date", children: "\u041E\u043A\u043E\u043D\u0447\u0430\u043D\u0438\u0435" })
2440
+ children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "gantt-tl-table", children: [
2441
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "gantt-tl-header", style: { height: `${headerHeight + 0.5}px` }, children: [
2442
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "gantt-tl-headerCell gantt-tl-cell-number", children: "\u2116" }),
2443
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "gantt-tl-headerCell gantt-tl-cell-name", children: "\u0418\u043C\u044F" }),
2444
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "gantt-tl-headerCell gantt-tl-cell-date", children: "\u041D\u0430\u0447\u0430\u043B\u043E" }),
2445
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "gantt-tl-headerCell gantt-tl-cell-date", children: "\u041E\u043A\u043E\u043D\u0447\u0430\u043D\u0438\u0435" }),
2446
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "gantt-tl-headerCell gantt-tl-cell-deps", style: { position: "relative" }, children: [
2447
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(Popover, { open: typeMenuOpen, onOpenChange: setTypeMenuOpen, children: [
2448
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2449
+ "button",
2450
+ {
2451
+ className: "gantt-tl-dep-type-trigger",
2452
+ disabled: disableDependencyEditing,
2453
+ onClick: (e) => e.stopPropagation(),
2454
+ children: [
2455
+ "\u0421\u0432\u044F\u0437\u0438 ",
2456
+ import_react11.default.createElement(LINK_TYPE_ICONS[activeLinkType]),
2457
+ " \u25BE"
2458
+ ]
2459
+ }
2460
+ ) }),
2461
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PopoverContent, { portal: true, align: "start", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "gantt-tl-dep-type-menu", children: LINK_TYPE_ORDER.map((lt) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2462
+ "button",
2463
+ {
2464
+ className: `gantt-tl-dep-type-option${activeLinkType === lt ? " active" : ""}`,
2465
+ onClick: () => {
2466
+ setActiveLinkType(lt);
2467
+ setTypeMenuOpen(false);
2468
+ },
2469
+ children: [
2470
+ import_react11.default.createElement(LINK_TYPE_ICONS[lt]),
2471
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { children: LINK_TYPE_LABELS[lt] })
2472
+ ]
2473
+ },
2474
+ lt
2475
+ )) }) })
2476
+ ] }),
2477
+ cycleError && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "gantt-tl-dep-error", children: "\u0426\u0438\u043A\u043B \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0435\u0439!" })
2478
+ ] })
2077
2479
  ] }),
2078
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "gantt-tl-body", style: { height: `${totalHeight}px` }, children: tasks.map((task, index) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2480
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "gantt-tl-body", style: { height: `${totalHeight}px` }, children: tasks.map((task, index) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2079
2481
  TaskListRow,
2080
2482
  {
2081
2483
  task,
@@ -2084,7 +2486,17 @@ var TaskList = ({
2084
2486
  onTaskChange,
2085
2487
  selectedTaskId,
2086
2488
  onRowClick: handleRowClick,
2087
- disableTaskNameEditing
2489
+ disableTaskNameEditing,
2490
+ disableDependencyEditing,
2491
+ allTasks: tasks,
2492
+ activeLinkType,
2493
+ selectingPredecessorFor,
2494
+ onSetSelectingPredecessorFor: setSelectingPredecessorFor,
2495
+ onAddDependency: handleAddDependency,
2496
+ onRemoveDependency: handleRemoveDependency,
2497
+ selectedChip,
2498
+ onChipSelect: handleChipSelect,
2499
+ onScrollToTask
2088
2500
  },
2089
2501
  task.id
2090
2502
  )) })
@@ -2094,13 +2506,13 @@ var TaskList = ({
2094
2506
  };
2095
2507
 
2096
2508
  // src/components/GanttChart/GanttChart.tsx
2097
- var import_jsx_runtime13 = require("react/jsx-runtime");
2509
+ var import_jsx_runtime14 = require("react/jsx-runtime");
2098
2510
  var GanttChart = (0, import_react12.forwardRef)(({
2099
2511
  tasks,
2100
2512
  dayWidth = 40,
2101
2513
  rowHeight = 40,
2102
2514
  headerHeight = 40,
2103
- containerHeight = 600,
2515
+ containerHeight,
2104
2516
  onChange,
2105
2517
  onValidateDependencies,
2106
2518
  enableAutoSchedule,
@@ -2108,10 +2520,12 @@ var GanttChart = (0, import_react12.forwardRef)(({
2108
2520
  onCascade,
2109
2521
  showTaskList = false,
2110
2522
  taskListWidth = 520,
2111
- disableTaskNameEditing = false
2523
+ disableTaskNameEditing = false,
2524
+ disableDependencyEditing = false
2112
2525
  }, ref) => {
2113
2526
  const scrollContainerRef = (0, import_react12.useRef)(null);
2114
2527
  const [selectedTaskId, setSelectedTaskId] = (0, import_react12.useState)(null);
2528
+ const [selectedChip, setSelectedChip] = (0, import_react12.useState)(null);
2115
2529
  const dateRange = (0, import_react12.useMemo)(() => getMultiMonthDays(tasks), [tasks]);
2116
2530
  const [validationResult, setValidationResult] = (0, import_react12.useState)(null);
2117
2531
  const [cascadeOverrides, setCascadeOverrides] = (0, import_react12.useState)(/* @__PURE__ */ new Map());
@@ -2159,12 +2573,30 @@ var GanttChart = (0, import_react12.forwardRef)(({
2159
2573
  const scrollLeft = Math.round(todayOffset - containerWidth / 2 + dayWidth / 2);
2160
2574
  container.scrollLeft = Math.max(0, scrollLeft);
2161
2575
  }, [dateRange, dayWidth]);
2576
+ const scrollToTask = (0, import_react12.useCallback)((taskId) => {
2577
+ const container = scrollContainerRef.current;
2578
+ if (!container || dateRange.length === 0) return;
2579
+ const task = tasks.find((t) => t.id === taskId);
2580
+ if (!task) return;
2581
+ const taskStart = new Date(task.startDate);
2582
+ const taskStartUTC = new Date(Date.UTC(
2583
+ taskStart.getUTCFullYear(),
2584
+ taskStart.getUTCMonth(),
2585
+ taskStart.getUTCDate()
2586
+ ));
2587
+ const taskIndex = dateRange.findIndex((day) => day.getTime() === taskStartUTC.getTime());
2588
+ if (taskIndex === -1) return;
2589
+ const taskOffset = taskIndex * dayWidth;
2590
+ const scrollLeft = Math.round(taskOffset - dayWidth * 2);
2591
+ container.scrollLeft = Math.max(0, scrollLeft);
2592
+ }, [tasks, dateRange, dayWidth]);
2162
2593
  (0, import_react12.useImperativeHandle)(
2163
2594
  ref,
2164
2595
  () => ({
2165
- scrollToToday
2596
+ scrollToToday,
2597
+ scrollToTask
2166
2598
  }),
2167
- [scrollToToday]
2599
+ [scrollToToday, scrollToTask]
2168
2600
  );
2169
2601
  const [dragGuideLines, setDragGuideLines] = (0, import_react12.useState)(null);
2170
2602
  const [draggedTaskOverride, setDraggedTaskOverride] = (0, import_react12.useState)(null);
@@ -2264,15 +2696,15 @@ var GanttChart = (0, import_react12.forwardRef)(({
2264
2696
  window.removeEventListener("mouseup", handlePanEnd);
2265
2697
  };
2266
2698
  }, []);
2267
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "gantt-container", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2699
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "gantt-container", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2268
2700
  "div",
2269
2701
  {
2270
2702
  ref: scrollContainerRef,
2271
2703
  className: "gantt-scrollContainer",
2272
- style: { height: `${containerHeight}px`, cursor: "grab" },
2704
+ style: { height: containerHeight ?? "auto", cursor: "grab" },
2273
2705
  onMouseDown: handlePanStart,
2274
- children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "gantt-scrollContent", children: [
2275
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2706
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "gantt-scrollContent", children: [
2707
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2276
2708
  TaskList,
2277
2709
  {
2278
2710
  tasks,
@@ -2283,11 +2715,14 @@ var GanttChart = (0, import_react12.forwardRef)(({
2283
2715
  selectedTaskId: selectedTaskId ?? void 0,
2284
2716
  onTaskSelect: handleTaskSelect,
2285
2717
  show: showTaskList,
2286
- disableTaskNameEditing
2718
+ disableTaskNameEditing,
2719
+ disableDependencyEditing,
2720
+ onScrollToTask: scrollToTask,
2721
+ onSelectedChipChange: setSelectedChip
2287
2722
  }
2288
2723
  ),
2289
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { style: { minWidth: `${gridWidth}px`, flex: 1 }, children: [
2290
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "gantt-stickyHeader", style: { width: `${gridWidth}px` }, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2724
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { style: { minWidth: `${gridWidth}px`, flex: 1 }, children: [
2725
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "gantt-stickyHeader", style: { width: `${gridWidth}px` }, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2291
2726
  TimeScaleHeader_default,
2292
2727
  {
2293
2728
  days: dateRange,
@@ -2295,7 +2730,7 @@ var GanttChart = (0, import_react12.forwardRef)(({
2295
2730
  headerHeight
2296
2731
  }
2297
2732
  ) }),
2298
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2733
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
2299
2734
  "div",
2300
2735
  {
2301
2736
  className: "gantt-taskArea",
@@ -2304,7 +2739,7 @@ var GanttChart = (0, import_react12.forwardRef)(({
2304
2739
  width: `${gridWidth}px`
2305
2740
  },
2306
2741
  children: [
2307
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2742
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2308
2743
  GridBackground_default,
2309
2744
  {
2310
2745
  dateRange,
@@ -2312,8 +2747,8 @@ var GanttChart = (0, import_react12.forwardRef)(({
2312
2747
  totalHeight: totalGridHeight
2313
2748
  }
2314
2749
  ),
2315
- todayInRange && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(TodayIndicator_default, { monthStart, dayWidth }),
2316
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2750
+ todayInRange && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(TodayIndicator_default, { monthStart, dayWidth }),
2751
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2317
2752
  DependencyLines_default,
2318
2753
  {
2319
2754
  tasks,
@@ -2321,10 +2756,11 @@ var GanttChart = (0, import_react12.forwardRef)(({
2321
2756
  dayWidth,
2322
2757
  rowHeight,
2323
2758
  gridWidth,
2324
- dragOverrides: dependencyOverrides
2759
+ dragOverrides: dependencyOverrides,
2760
+ selectedDep: selectedChip
2325
2761
  }
2326
2762
  ),
2327
- dragGuideLines && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2763
+ dragGuideLines && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2328
2764
  DragGuideLines_default,
2329
2765
  {
2330
2766
  isDragging: dragGuideLines.isDragging,
@@ -2334,7 +2770,7 @@ var GanttChart = (0, import_react12.forwardRef)(({
2334
2770
  totalHeight: totalGridHeight
2335
2771
  }
2336
2772
  ),
2337
- tasks.map((task, index) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2773
+ tasks.map((task, index) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2338
2774
  TaskRow_default,
2339
2775
  {
2340
2776
  task,
@@ -2373,7 +2809,7 @@ GanttChart.displayName = "GanttChart";
2373
2809
 
2374
2810
  // src/components/ui/Button.tsx
2375
2811
  var import_react13 = __toESM(require("react"));
2376
- var import_jsx_runtime14 = require("react/jsx-runtime");
2812
+ var import_jsx_runtime15 = require("react/jsx-runtime");
2377
2813
  var Button = import_react13.default.forwardRef(
2378
2814
  ({ className, variant = "default", size = "default", children, ...props }, ref) => {
2379
2815
  const classes = [
@@ -2382,7 +2818,7 @@ var Button = import_react13.default.forwardRef(
2382
2818
  size !== "default" ? `gantt-btn-${size}` : "",
2383
2819
  className || ""
2384
2820
  ].filter(Boolean).join(" ");
2385
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("button", { ref, className: classes, ...props, children });
2821
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { ref, className: classes, ...props, children });
2386
2822
  }
2387
2823
  );
2388
2824
  Button.displayName = "Button";
@@ -2412,6 +2848,7 @@ Button.displayName = "Button";
2412
2848
  calculateTaskBar,
2413
2849
  calculateWeekendBlocks,
2414
2850
  cascadeByLinks,
2851
+ computeLagFromDates,
2415
2852
  detectCycles,
2416
2853
  detectEdgeZone,
2417
2854
  formatDateLabel,