@ganttloom/gantt-core 0.4.0 → 0.4.3

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.cjs CHANGED
@@ -74,7 +74,8 @@ var DEFAULT_THEME = {
74
74
  backgroundColor: "#ffffff",
75
75
  fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
76
76
  fontSize: 12,
77
- borderRadius: 4
77
+ borderRadius: 4,
78
+ borderColor: "#e2e8f0"
78
79
  };
79
80
  var DARK_THEME = {
80
81
  ...DEFAULT_THEME,
@@ -91,7 +92,8 @@ var DARK_THEME = {
91
92
  markerColor: "#a78bfa",
92
93
  selectionColor: "#38bdf8",
93
94
  textColor: "#e5e7eb",
94
- backgroundColor: "#181c24"
95
+ backgroundColor: "#181c24",
96
+ borderColor: "#334155"
95
97
  };
96
98
  var DENSITY_PRESETS = {
97
99
  compact: { rowHeight: 26, barHeight: 16, headerHeight: 38, fontSize: 11 },
@@ -130,7 +132,8 @@ var CSS_VAR_NAMES = {
130
132
  backgroundColor: "--gantt-background-color",
131
133
  fontFamily: "--gantt-font-family",
132
134
  fontSize: "--gantt-font-size",
133
- borderRadius: "--gantt-border-radius"
135
+ borderRadius: "--gantt-border-radius",
136
+ borderColor: "--gantt-border-color"
134
137
  };
135
138
  var PX_FIELDS = /* @__PURE__ */ new Set([
136
139
  "rowHeight",
@@ -556,20 +559,38 @@ function labelFor(date, viewMode) {
556
559
  const h = date.getHours();
557
560
  const period = h >= 12 ? "PM" : "AM";
558
561
  const h12 = h % 12 === 0 ? 12 : h % 12;
559
- return `${h12} ${period}`;
562
+ return {
563
+ label: `${h12} ${period}`,
564
+ parentLabel: `${WEEKDAY_LABELS[date.getDay()]} ${date.getDate()} ${MONTH_LABELS[date.getMonth()]} ${date.getFullYear()}`
565
+ };
560
566
  }
561
567
  case "day":
562
- return `${WEEKDAY_LABELS[date.getDay()]} ${date.getDate()}`;
568
+ return {
569
+ label: `${WEEKDAY_LABELS[date.getDay()]} ${date.getDate()}`,
570
+ parentLabel: `${MONTH_LABELS[date.getMonth()]} ${date.getFullYear()}`
571
+ };
563
572
  case "week":
564
- return `${MONTH_LABELS[date.getMonth()]} ${date.getDate()}`;
573
+ return {
574
+ label: `${MONTH_LABELS[date.getMonth()]} ${date.getDate()}`,
575
+ parentLabel: `${MONTH_LABELS[date.getMonth()]} ${date.getFullYear()}`
576
+ };
565
577
  case "month":
566
- return `${MONTH_LABELS[date.getMonth()]} ${date.getFullYear()}`;
578
+ return {
579
+ label: MONTH_LABELS[date.getMonth()] ?? "",
580
+ parentLabel: `${date.getFullYear()}`
581
+ };
567
582
  case "quarter": {
568
583
  const q = Math.floor(date.getMonth() / 3) + 1;
569
- return `Q${q} ${date.getFullYear()}`;
584
+ return {
585
+ label: `Q${q}`,
586
+ parentLabel: `${date.getFullYear()}`
587
+ };
570
588
  }
571
589
  case "year":
572
- return `${date.getFullYear()}`;
590
+ return {
591
+ label: `${date.getFullYear()}`,
592
+ parentLabel: ""
593
+ };
573
594
  }
574
595
  }
575
596
  function generateTicks(rangeStart, rangeEnd, viewMode, columnWidth, today = /* @__PURE__ */ new Date(), calendar) {
@@ -583,10 +604,12 @@ function generateTicks(rangeStart, rangeEnd, viewMode, columnWidth, today = /* @
583
604
  while (cursor.getTime() < rangeEnd.getTime() && guard < MAX_TICKS) {
584
605
  guard++;
585
606
  const x = (cursor.getTime() - rangeStart.getTime()) * scale;
607
+ const labels = labelFor(cursor, viewMode);
586
608
  ticks.push({
587
609
  date: new Date(cursor.getTime()),
588
610
  x,
589
- label: labelFor(cursor, viewMode),
611
+ label: labels.label,
612
+ parentLabel: labels.parentLabel,
590
613
  isWeekend: dayLevel ? isWeekend(cursor) : false,
591
614
  isNonWorking: viewMode === "hour" && calendar?.workingHours ? !isWorkingTime(cursor, calendar) : dayLevel ? isNonWorkingDay(cursor, calendar) : false,
592
615
  isToday: isSameDay(cursor, today) && dayLevel,
@@ -827,11 +850,14 @@ function computeLayout(input) {
827
850
  let paddedStart = addUnit(rawRangeStart, viewMode, -1);
828
851
  let paddedEnd = addUnit(rawRangeEnd, viewMode, 1);
829
852
  const minUnits = MIN_VISIBLE_UNITS[viewMode];
853
+ let targetMinUnits = minUnits;
854
+ if (input.timelineViewportWidth && input.timelineViewportWidth > 0) {
855
+ targetMinUnits = Math.max(targetMinUnits, Math.ceil(input.timelineViewportWidth / columnWidth));
856
+ }
830
857
  const currentUnits = (paddedEnd.getTime() - paddedStart.getTime()) / approxUnitMs(viewMode);
831
- if (currentUnits < minUnits) {
832
- const deficit = minUnits - currentUnits;
833
- paddedStart = addUnit(paddedStart, viewMode, -Math.ceil(deficit / 2));
834
- paddedEnd = addUnit(paddedEnd, viewMode, Math.floor(deficit / 2));
858
+ if (currentUnits < targetMinUnits) {
859
+ const deficit = targetMinUnits - currentUnits;
860
+ paddedEnd = addUnit(paddedEnd, viewMode, Math.ceil(deficit));
835
861
  }
836
862
  const rows = [];
837
863
  const bars = [];
@@ -949,6 +975,7 @@ function computeLayout(input) {
949
975
  (t) => ({
950
976
  x: t.x,
951
977
  label: t.label,
978
+ parentLabel: t.parentLabel,
952
979
  isWeekend: t.isWeekend,
953
980
  isNonWorking: t.isNonWorking,
954
981
  isToday: t.isToday,
@@ -1226,6 +1253,9 @@ var GanttRenderer = class {
1226
1253
  this.svg = svgEl("svg");
1227
1254
  this.svg.classList.add("gantt-svg");
1228
1255
  this.timelineScroll.appendChild(this.headerContainer);
1256
+ if (this.options.readonly) {
1257
+ this.root.classList.add("gantt-readonly");
1258
+ }
1229
1259
  this.timelineScroll.appendChild(this.svg);
1230
1260
  this.root.appendChild(this.gridPanel);
1231
1261
  this.root.appendChild(this.timelineScroll);
@@ -1293,7 +1323,10 @@ var GanttRenderer = class {
1293
1323
  const cell = el("div", "gantt-grid-header-cell");
1294
1324
  cell.style.position = "relative";
1295
1325
  if (col.width) cell.style.width = `${col.width}px`;
1296
- cell.textContent = col.title;
1326
+ const titleSpan = document.createElement("span");
1327
+ titleSpan.className = "gantt-cell-text";
1328
+ titleSpan.textContent = col.title;
1329
+ cell.appendChild(titleSpan);
1297
1330
  if (col.sortable) {
1298
1331
  cell.classList.add("gantt-grid-header-cell-sortable");
1299
1332
  const active = sort?.columnId === col.id;
@@ -1335,7 +1368,7 @@ var GanttRenderer = class {
1335
1368
  evt.preventDefault();
1336
1369
  evt.stopPropagation();
1337
1370
  const startX = evt.clientX;
1338
- const startWidth = col.width ?? cell.getBoundingClientRect().width;
1371
+ const startWidth = cell.getBoundingClientRect().width;
1339
1372
  const onMove = (moveEvt) => {
1340
1373
  const newWidth = Math.max(30, startWidth + (moveEvt.clientX - startX));
1341
1374
  cell.style.width = `${newWidth}px`;
@@ -1388,9 +1421,11 @@ var GanttRenderer = class {
1388
1421
  const cell = el("div", "gantt-grid-cell");
1389
1422
  if (col.width) cell.style.width = `${col.width}px`;
1390
1423
  if (col.align) cell.style.textAlign = col.align;
1391
- const rawValue = col.accessor ? col.accessor(task) : colIndex === 0 ? task.name : "";
1424
+ const isTreeColumn = col.id === "name" || !columns.some((c) => c.id === "name") && colIndex === 0;
1425
+ console.log(`Column ${col.id}: isTreeColumn=${isTreeColumn}, colIndex=${colIndex}, hasNameCol=${columns.some((c) => c.id === "name")}`);
1426
+ const rawValue = col.accessor ? col.accessor(task) : isTreeColumn ? task.name : task[col.id] ?? "";
1392
1427
  const textValue = rawValue === null || rawValue === void 0 ? "" : String(rawValue);
1393
- if (colIndex === 0) {
1428
+ if (isTreeColumn) {
1394
1429
  cell.dataset.ganttNameCell = task.id;
1395
1430
  cell.dataset.ganttNameValue = textValue;
1396
1431
  const indent = el("span", "gantt-indent");
@@ -1399,17 +1434,27 @@ var GanttRenderer = class {
1399
1434
  if (row.hasChildren) {
1400
1435
  const toggle = el("button", "gantt-toggle");
1401
1436
  toggle.type = "button";
1437
+ toggle.className = `gantt-toggle ${row.collapsed ? "collapsed" : "expanded"}`;
1402
1438
  toggle.dataset.ganttToggle = task.id;
1403
1439
  toggle.setAttribute("aria-label", row.collapsed ? "Expand" : "Collapse");
1404
1440
  toggle.textContent = row.collapsed ? "\u25B8" : "\u25BE";
1405
1441
  cell.appendChild(toggle);
1406
1442
  }
1443
+ const addChildBtn = el("button", "gantt-add-child");
1444
+ addChildBtn.type = "button";
1445
+ addChildBtn.dataset.ganttAddChild = task.id;
1446
+ addChildBtn.setAttribute("aria-label", "Add child task");
1447
+ addChildBtn.textContent = "+";
1448
+ cell.appendChild(addChildBtn);
1407
1449
  }
1408
1450
  const rendered = col.render ? col.render(task) : void 0;
1409
1451
  if (rendered instanceof HTMLElement) {
1410
1452
  cell.appendChild(rendered);
1411
1453
  } else if (typeof rendered === "string") {
1412
- cell.appendChild(document.createTextNode(rendered));
1454
+ const span = document.createElement("span");
1455
+ span.className = "gantt-cell-text";
1456
+ span.textContent = rendered;
1457
+ cell.appendChild(span);
1413
1458
  } else {
1414
1459
  const href = col.getHref ? col.getHref(task) : null;
1415
1460
  if (href && isSafeHref(href)) {
@@ -1418,10 +1463,13 @@ var GanttRenderer = class {
1418
1463
  anchor.target = col.linkTarget ?? "_blank";
1419
1464
  anchor.rel = "noopener noreferrer";
1420
1465
  anchor.textContent = textValue;
1466
+ anchor.className = "gantt-cell-text";
1421
1467
  cell.appendChild(anchor);
1422
1468
  } else {
1423
- const textNode = document.createTextNode(textValue);
1424
- cell.appendChild(textNode);
1469
+ const span = document.createElement("span");
1470
+ span.className = "gantt-cell-text";
1471
+ span.textContent = textValue;
1472
+ cell.appendChild(span);
1425
1473
  }
1426
1474
  }
1427
1475
  rowEl.appendChild(cell);
@@ -1452,8 +1500,8 @@ var GanttRenderer = class {
1452
1500
  const bodyGroup = svgEl("g");
1453
1501
  this.renderGridLines(bodyGroup, model);
1454
1502
  this.renderRowBackgrounds(bodyGroup, model, startY, endY);
1455
- this.renderBars(bodyGroup, model, tasks, startY, endY);
1456
1503
  this.renderLinks(bodyGroup, model, startY, endY);
1504
+ this.renderBars(bodyGroup, model, tasks, startY, endY);
1457
1505
  this.renderMarkers(bodyGroup, model);
1458
1506
  this.svg.appendChild(bodyGroup);
1459
1507
  this.renderHeader(model);
@@ -1520,15 +1568,21 @@ var GanttRenderer = class {
1520
1568
  clipRect.setAttribute("height", String(model.height));
1521
1569
  clipPath.appendChild(clipRect);
1522
1570
  defs.appendChild(clipPath);
1523
- const label = svgEl("text");
1524
- label.setAttribute("x", String(marker.x + 4));
1525
- label.setAttribute("y", "12");
1526
- label.setAttribute("clip-path", `url(#${clipId})`);
1527
- label.setAttribute("fill", marker.color);
1528
- label.setAttribute("font-size", String(model.theme.fontSize));
1529
- label.setAttribute("font-family", model.theme.fontFamily);
1530
- label.textContent = marker.label;
1531
- group.appendChild(label);
1571
+ const fo = svgEl("foreignObject");
1572
+ fo.setAttribute("x", String(marker.x + 4));
1573
+ fo.setAttribute("y", "0");
1574
+ fo.setAttribute("width", String(cellWidth > 8 ? cellWidth - 8 : cellWidth));
1575
+ fo.setAttribute("height", String(model.height));
1576
+ const div = document.createElement("div");
1577
+ div.style.color = marker.color;
1578
+ div.style.fontSize = `${model.theme.fontSize}px`;
1579
+ div.style.fontFamily = model.theme.fontFamily;
1580
+ div.style.whiteSpace = "normal";
1581
+ div.style.overflowWrap = "break-word";
1582
+ div.style.paddingTop = "4px";
1583
+ div.textContent = marker.label;
1584
+ fo.appendChild(div);
1585
+ group.appendChild(fo);
1532
1586
  }
1533
1587
  });
1534
1588
  }
@@ -1549,6 +1603,60 @@ var GanttRenderer = class {
1549
1603
  headerGroup.appendChild(bg);
1550
1604
  const defs = svgEl("defs");
1551
1605
  const ticks = model.ticks;
1606
+ const parentGroups = [];
1607
+ let currentGroup = null;
1608
+ for (const tick of ticks) {
1609
+ if (!currentGroup || currentGroup.label !== tick.parentLabel) {
1610
+ currentGroup = { x: tick.x, label: tick.parentLabel };
1611
+ parentGroups.push(currentGroup);
1612
+ }
1613
+ }
1614
+ const hasParentTier = parentGroups.some((g) => g.label !== "");
1615
+ const bottomTierY = hasParentTier ? model.headerHeight / 2 : 0;
1616
+ const tierHeight = hasParentTier ? model.headerHeight / 2 : model.headerHeight;
1617
+ if (hasParentTier) {
1618
+ for (let i = 0; i < parentGroups.length; i++) {
1619
+ const group = parentGroups[i];
1620
+ const nextX = parentGroups[i + 1]?.x ?? model.width;
1621
+ const cellWidth = nextX - group.x;
1622
+ const fo = svgEl("foreignObject");
1623
+ fo.setAttribute("x", String(group.x + 4));
1624
+ fo.setAttribute("y", "0");
1625
+ fo.setAttribute("width", String(Math.max(0, cellWidth - 4)));
1626
+ fo.setAttribute("height", String(tierHeight));
1627
+ const div = document.createElement("div");
1628
+ div.style.color = themedFill("textColor", model.theme.textColor);
1629
+ div.style.fontSize = `${model.theme.fontSize * 0.95}px`;
1630
+ div.style.fontFamily = model.theme.fontFamily;
1631
+ div.style.fontWeight = "600";
1632
+ div.style.whiteSpace = "nowrap";
1633
+ div.style.overflow = "hidden";
1634
+ div.style.textOverflow = "ellipsis";
1635
+ div.style.display = "flex";
1636
+ div.style.alignItems = "center";
1637
+ div.style.justifyContent = "center";
1638
+ div.style.height = "100%";
1639
+ div.textContent = group.label;
1640
+ fo.appendChild(div);
1641
+ headerGroup.appendChild(fo);
1642
+ const border = svgEl("line");
1643
+ border.setAttribute("x1", String(group.x));
1644
+ border.setAttribute("x2", String(nextX));
1645
+ border.setAttribute("y1", String(tierHeight));
1646
+ border.setAttribute("y2", String(tierHeight));
1647
+ border.style.stroke = themedFill("borderColor", model.theme.borderColor ?? "#e2e8f0");
1648
+ headerGroup.appendChild(border);
1649
+ if (i > 0) {
1650
+ const sep = svgEl("line");
1651
+ sep.setAttribute("x1", String(group.x));
1652
+ sep.setAttribute("x2", String(group.x));
1653
+ sep.setAttribute("y1", "0");
1654
+ sep.setAttribute("y2", String(tierHeight));
1655
+ sep.style.stroke = themedFill("borderColor", model.theme.borderColor ?? "#e2e8f0");
1656
+ headerGroup.appendChild(sep);
1657
+ }
1658
+ }
1659
+ }
1552
1660
  for (let i = 0; i < ticks.length; i++) {
1553
1661
  const tick = ticks[i];
1554
1662
  const cellWidth = (ticks[i + 1]?.x ?? model.width) - tick.x;
@@ -1557,20 +1665,30 @@ var GanttRenderer = class {
1557
1665
  clipPath.setAttribute("id", clipId);
1558
1666
  const clipRect = svgEl("rect");
1559
1667
  clipRect.setAttribute("x", String(tick.x));
1560
- clipRect.setAttribute("y", "0");
1668
+ clipRect.setAttribute("y", String(bottomTierY));
1561
1669
  clipRect.setAttribute("width", String(Math.max(0, cellWidth)));
1562
- clipRect.setAttribute("height", String(model.headerHeight));
1670
+ clipRect.setAttribute("height", String(tierHeight));
1563
1671
  clipPath.appendChild(clipRect);
1564
1672
  defs.appendChild(clipPath);
1565
- const text = svgEl("text");
1566
- text.setAttribute("x", String(tick.x + 4));
1567
- text.setAttribute("y", String(model.headerHeight - 8));
1568
- text.setAttribute("clip-path", `url(#${clipId})`);
1569
- text.style.fill = themedFill("textColor", model.theme.textColor);
1570
- text.setAttribute("font-size", String(model.theme.fontSize));
1571
- text.setAttribute("font-family", model.theme.fontFamily);
1572
- text.textContent = tick.label;
1573
- headerGroup.appendChild(text);
1673
+ const fo = svgEl("foreignObject");
1674
+ fo.setAttribute("x", String(tick.x + 4));
1675
+ fo.setAttribute("y", String(bottomTierY));
1676
+ fo.setAttribute("width", String(cellWidth > 8 ? cellWidth - 8 : cellWidth));
1677
+ fo.setAttribute("height", String(tierHeight));
1678
+ const div = document.createElement("div");
1679
+ div.style.color = themedFill("textColor", model.theme.textColor);
1680
+ div.style.fontSize = `${model.theme.fontSize}px`;
1681
+ div.style.fontFamily = model.theme.fontFamily;
1682
+ div.style.whiteSpace = "normal";
1683
+ div.style.overflowWrap = "break-word";
1684
+ div.style.display = "flex";
1685
+ div.style.alignItems = hasParentTier ? "center" : "flex-end";
1686
+ div.style.justifyContent = "center";
1687
+ div.style.paddingBottom = hasParentTier ? "0" : "4px";
1688
+ div.style.height = "100%";
1689
+ div.textContent = tick.label;
1690
+ fo.appendChild(div);
1691
+ headerGroup.appendChild(fo);
1574
1692
  }
1575
1693
  headerGroup.insertBefore(defs, headerGroup.firstChild);
1576
1694
  this.headerSvg.appendChild(headerGroup);
@@ -1613,8 +1731,8 @@ var GanttRenderer = class {
1613
1731
  g.setAttribute("role", "button");
1614
1732
  if (task) {
1615
1733
  const progress = Math.round(task.progress ?? 0);
1616
- const label2 = `${task.name}, ${task.start.toDateString()} to ${task.end.toDateString()}, ${progress}% complete`;
1617
- g.setAttribute("aria-label", label2);
1734
+ const label = `${task.name}, ${task.start.toDateString()} to ${task.end.toDateString()}, ${progress}% complete`;
1735
+ g.setAttribute("aria-label", label);
1618
1736
  }
1619
1737
  }
1620
1738
  if (bar.baseline) {
@@ -1662,17 +1780,15 @@ var GanttRenderer = class {
1662
1780
  `rotate(45, 0, ${bar.height / 2})`
1663
1781
  );
1664
1782
  g.appendChild(diamond);
1665
- const label2 = svgEl("text");
1666
- label2.setAttribute("x", String(size / 2 + 8));
1667
- label2.setAttribute("y", String(bar.height / 2 + 4));
1668
- label2.style.fill = themedFill("textColor", model.theme.textColor);
1669
- label2.setAttribute("font-size", String(model.theme.fontSize));
1670
- label2.setAttribute("font-family", model.theme.fontFamily);
1671
- label2.textContent = bar.label;
1672
- g.appendChild(label2);
1673
- return g;
1674
- }
1675
- if (bar.segments && bar.segments.length > 0) {
1783
+ const label = svgEl("text");
1784
+ label.setAttribute("x", String(size / 2 + 8));
1785
+ label.setAttribute("y", String(bar.height / 2 + 4));
1786
+ label.style.fill = themedFill("textColor", model.theme.textColor);
1787
+ label.setAttribute("font-size", String(model.theme.fontSize));
1788
+ label.setAttribute("font-family", model.theme.fontFamily);
1789
+ label.textContent = bar.label;
1790
+ g.appendChild(label);
1791
+ } else if (bar.segments && bar.segments.length > 0) {
1676
1792
  bar.segments.forEach((seg, i) => {
1677
1793
  const segRect = svgEl("rect");
1678
1794
  segRect.setAttribute("x", String(seg.x));
@@ -1745,29 +1861,31 @@ var GanttRenderer = class {
1745
1861
  selectionOutline.setAttribute("stroke-width", "2");
1746
1862
  g.appendChild(selectionOutline);
1747
1863
  }
1748
- const handleWidth = 6;
1749
- const leftHandle = svgEl("rect");
1750
- leftHandle.setAttribute("x", "0");
1751
- leftHandle.setAttribute("y", "0");
1752
- leftHandle.setAttribute("width", String(handleWidth));
1753
- leftHandle.setAttribute("height", String(bar.height));
1754
- leftHandle.setAttribute("fill", "transparent");
1755
- leftHandle.dataset.ganttHandle = "left";
1756
- leftHandle.dataset.ganttHandleFor = bar.taskId;
1757
- leftHandle.style.cursor = "ew-resize";
1758
- g.appendChild(leftHandle);
1759
- const rightHandle = svgEl("rect");
1760
- rightHandle.setAttribute("x", String(Math.max(0, bar.width - handleWidth)));
1761
- rightHandle.setAttribute("y", "0");
1762
- rightHandle.setAttribute("width", String(handleWidth));
1763
- rightHandle.setAttribute("height", String(bar.height));
1764
- rightHandle.setAttribute("fill", "transparent");
1765
- rightHandle.dataset.ganttHandle = "right";
1766
- rightHandle.dataset.ganttHandleFor = bar.taskId;
1767
- rightHandle.style.cursor = "ew-resize";
1768
- g.appendChild(rightHandle);
1864
+ if (!bar.isMilestone) {
1865
+ const handleWidth = 6;
1866
+ const leftHandle = svgEl("rect");
1867
+ leftHandle.setAttribute("x", "0");
1868
+ leftHandle.setAttribute("y", "0");
1869
+ leftHandle.setAttribute("width", String(handleWidth));
1870
+ leftHandle.setAttribute("height", String(bar.height));
1871
+ leftHandle.setAttribute("fill", "transparent");
1872
+ leftHandle.dataset.ganttHandle = "left";
1873
+ leftHandle.dataset.ganttHandleFor = bar.taskId;
1874
+ leftHandle.style.cursor = "ew-resize";
1875
+ g.appendChild(leftHandle);
1876
+ const rightHandle = svgEl("rect");
1877
+ rightHandle.setAttribute("x", String(Math.max(0, bar.width - handleWidth)));
1878
+ rightHandle.setAttribute("y", "0");
1879
+ rightHandle.setAttribute("width", String(handleWidth));
1880
+ rightHandle.setAttribute("height", String(bar.height));
1881
+ rightHandle.setAttribute("fill", "transparent");
1882
+ rightHandle.dataset.ganttHandle = "right";
1883
+ rightHandle.dataset.ganttHandleFor = bar.taskId;
1884
+ rightHandle.style.cursor = "ew-resize";
1885
+ g.appendChild(rightHandle);
1886
+ }
1769
1887
  const connector = svgEl("circle");
1770
- connector.setAttribute("cx", String(bar.width));
1888
+ connector.setAttribute("cx", String(bar.width + (bar.isMilestone ? 10 : 0)));
1771
1889
  connector.setAttribute("cy", String(bar.height / 2));
1772
1890
  connector.setAttribute("r", "4");
1773
1891
  connector.style.fill = themedFill("linkColor", model.theme.linkColor);
@@ -1775,14 +1893,16 @@ var GanttRenderer = class {
1775
1893
  connector.dataset.ganttConnectorSide = "right";
1776
1894
  connector.style.cursor = "crosshair";
1777
1895
  g.appendChild(connector);
1778
- const label = svgEl("text");
1779
- label.setAttribute("x", String(bar.width + 6));
1780
- label.setAttribute("y", String(bar.height / 2 + 4));
1781
- label.style.fill = themedFill("textColor", model.theme.textColor);
1782
- label.setAttribute("font-size", String(model.theme.fontSize));
1783
- label.setAttribute("font-family", model.theme.fontFamily);
1784
- label.textContent = bar.label;
1785
- g.appendChild(label);
1896
+ if (!bar.isMilestone) {
1897
+ const label = svgEl("text");
1898
+ label.setAttribute("x", String(bar.width + 6));
1899
+ label.setAttribute("y", String(bar.height / 2 + 4));
1900
+ label.style.fill = themedFill("textColor", model.theme.textColor);
1901
+ label.setAttribute("font-size", String(model.theme.fontSize));
1902
+ label.setAttribute("font-family", model.theme.fontFamily);
1903
+ label.textContent = bar.label;
1904
+ g.appendChild(label);
1905
+ }
1786
1906
  if (bar.deadlineX !== void 0) {
1787
1907
  const markerX = bar.deadlineX - bar.x;
1788
1908
  const flag = svgEl("g");
@@ -1840,10 +1960,20 @@ var GanttRenderer = class {
1840
1960
  for (const link of model.links) {
1841
1961
  const fromBar = barByTaskId.get(link.fromId);
1842
1962
  const toBar = barByTaskId.get(link.toId);
1843
- const relevantY = toBar?.y ?? fromBar?.y ?? 0;
1963
+ if (!fromBar || !toBar) continue;
1964
+ const relevantY = toBar.y ?? fromBar.y ?? 0;
1844
1965
  if (relevantY < startY || relevantY > endY) continue;
1966
+ const type = link.type ?? "FS";
1967
+ const x1 = type.startsWith("F") ? fromBar.x + fromBar.width : fromBar.x;
1968
+ const y1 = fromBar.y + fromBar.height / 2;
1969
+ const x2 = type.endsWith("F") ? toBar.x + toBar.width : toBar.x;
1970
+ const y2 = toBar.y + toBar.height / 2;
1971
+ const offset = 10;
1972
+ const routingX1 = type.startsWith("F") ? x1 + offset : x1 - offset;
1973
+ const routingX2 = type.endsWith("F") ? x2 + offset : x2 - offset;
1974
+ const d = `M ${x1} ${y1} L ${routingX1} ${y1} C ${routingX1 + (routingX2 - routingX1) / 2} ${y1}, ${routingX1 + (routingX2 - routingX1) / 2} ${y2}, ${routingX2} ${y2} L ${x2} ${y2}`;
1845
1975
  const path = svgEl("path");
1846
- path.setAttribute("d", link.path);
1976
+ path.setAttribute("d", d);
1847
1977
  path.setAttribute("fill", "none");
1848
1978
  path.style.stroke = link.isCritical ? themedFill("criticalColor", model.theme.criticalColor) : themedFill("linkColor", model.theme.linkColor);
1849
1979
  path.setAttribute("stroke-width", link.isCritical ? "2" : "1.5");
@@ -2241,6 +2371,12 @@ var InteractionController = class {
2241
2371
  const toggleId = target.closest("[data-gantt-toggle]")?.dataset.ganttToggle;
2242
2372
  if (toggleId) {
2243
2373
  this.callbacks.onToggleCollapse(toggleId);
2374
+ return;
2375
+ }
2376
+ const addChildId = target.closest("[data-gantt-add-child]")?.dataset.ganttAddChild;
2377
+ if (addChildId && this.callbacks.onAddChild) {
2378
+ this.callbacks.onAddChild(addChildId);
2379
+ return;
2244
2380
  }
2245
2381
  };
2246
2382
  this.onContextMenu = (evt) => {
@@ -2700,6 +2836,7 @@ var _GanttChart = class _GanttChart {
2700
2836
  markers: options.markers,
2701
2837
  pagination: options.pagination,
2702
2838
  snapToUnit: options.snapToUnit,
2839
+ headerPosition: options.headerPosition ?? "top",
2703
2840
  onDateChange: options.onDateChange,
2704
2841
  onProgressChange: options.onProgressChange,
2705
2842
  onDependencyCreate: options.onDependencyCreate,
@@ -2732,6 +2869,7 @@ var _GanttChart = class _GanttChart {
2732
2869
  this.darkMediaQuery.addEventListener?.("change", this.handleSchemeChange);
2733
2870
  }
2734
2871
  this.renderer = new GanttRenderer(this.container, {
2872
+ readonly: this.options.readonly,
2735
2873
  keyboardAccessible: this.options.keyboardAccessible,
2736
2874
  showAssigneeAvatars: this.options.showAssigneeAvatars,
2737
2875
  virtualScroll: this.options.virtualScroll,
@@ -2741,6 +2879,13 @@ var _GanttChart = class _GanttChart {
2741
2879
  onRowReorder: this.options.onTaskReorder ? (draggedId, targetId, position) => this.handleRowReorder(draggedId, targetId, position) : void 0,
2742
2880
  onSortClick: (columnId) => this.handleSortClick(columnId)
2743
2881
  });
2882
+ if (this.options.headerPosition && this.renderer?.root) {
2883
+ this.renderer.root.dataset.ganttHeaderPosition = this.options.headerPosition;
2884
+ }
2885
+ if (typeof ResizeObserver !== "undefined") {
2886
+ this.resizeObserver = new ResizeObserver(() => this.scheduleRender());
2887
+ this.resizeObserver.observe(this.renderer.timelineScroll);
2888
+ }
2744
2889
  this.interactions = new InteractionController(
2745
2890
  this.renderer.svg,
2746
2891
  this.renderer.gridPanel,
@@ -2779,7 +2924,8 @@ var _GanttChart = class _GanttChart {
2779
2924
  } : void 0,
2780
2925
  onSelectAll: this.options.selectable ? () => this.selectAllVisible() : void 0,
2781
2926
  onJumpToStart: () => this.scrollToRangeStart(),
2782
- onJumpToEnd: () => this.scrollToRangeEnd()
2927
+ onJumpToEnd: () => this.scrollToRangeEnd(),
2928
+ onAddChild: (parentId) => this.handleAddChild(parentId)
2783
2929
  },
2784
2930
  {
2785
2931
  readonly: this.options.readonly,
@@ -3069,6 +3215,35 @@ var _GanttChart = class _GanttChart {
3069
3215
  };
3070
3216
  this.runCommand(cmd);
3071
3217
  }
3218
+ handleAddChild(parentId) {
3219
+ const parent = this.tasks.find((t) => t.id === parentId);
3220
+ if (!parent) return;
3221
+ const newTask = {
3222
+ id: `task-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
3223
+ name: "New task",
3224
+ start: new Date(parent.start.getTime()),
3225
+ end: new Date(parent.start.getTime() + MS_PER_DAY),
3226
+ parentId: parent.id
3227
+ };
3228
+ const cmd = {
3229
+ label: "task-create",
3230
+ do: () => {
3231
+ const index = this.tasks.findIndex((t) => t.id === parentId);
3232
+ if (index >= 0) this.tasks.splice(index + 1, 0, newTask);
3233
+ else this.tasks.push(newTask);
3234
+ this.emitter.emit("task-create", { task: newTask });
3235
+ this.options.onTaskCreate?.(newTask);
3236
+ this.emitter.emit("tasks-change", { tasks: this.getTasks() });
3237
+ this.scheduleRender();
3238
+ },
3239
+ undo: () => {
3240
+ this.tasks = this.tasks.filter((t) => t.id !== newTask.id);
3241
+ this.emitter.emit("tasks-change", { tasks: this.getTasks() });
3242
+ this.scheduleRender();
3243
+ }
3244
+ };
3245
+ this.runCommand(cmd);
3246
+ }
3072
3247
  handleBarClick(taskId, modifiers) {
3073
3248
  const task = this.tasks.find((t) => t.id === taskId);
3074
3249
  if (!task) return;
@@ -3300,6 +3475,12 @@ var _GanttChart = class _GanttChart {
3300
3475
  if (partial.gridPanelWidth !== void 0) {
3301
3476
  this.container.style.setProperty("--gantt-grid-panel-width", `${partial.gridPanelWidth}px`);
3302
3477
  }
3478
+ if ("headerPosition" in partial) {
3479
+ this.options.headerPosition = partial.headerPosition ?? "top";
3480
+ if (this.renderer?.root) {
3481
+ this.renderer.root.dataset.ganttHeaderPosition = this.options.headerPosition;
3482
+ }
3483
+ }
3303
3484
  Object.assign(this.options, partial);
3304
3485
  this.scheduleRender();
3305
3486
  }
@@ -3387,9 +3568,11 @@ var _GanttChart = class _GanttChart {
3387
3568
  }
3388
3569
  computeModel() {
3389
3570
  const pagination = this.options.pagination ? { pageSize: this.options.pagination.pageSize, page: this.currentPage } : void 0;
3571
+ const timelineViewportWidth = HAS_DOM && this.renderer ? this.renderer.timelineScroll.clientWidth : void 0;
3390
3572
  return computeLayout({
3391
3573
  tasks: this.tasks,
3392
3574
  dependencies: this.dependencies,
3575
+ timelineViewportWidth,
3393
3576
  viewMode: this.options.viewMode,
3394
3577
  columnWidth: this.columnWidth,
3395
3578
  theme: this.theme,
@@ -3472,7 +3655,35 @@ var _GanttChart = class _GanttChart {
3472
3655
  return this.renderer?.toSVGString() ?? "";
3473
3656
  }
3474
3657
  async rasterizeSVG(svgString, width, height) {
3475
- const svgBlob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
3658
+ let cleanSvgString = svgString;
3659
+ if (typeof DOMParser !== "undefined") {
3660
+ const parser = new DOMParser();
3661
+ const doc = parser.parseFromString(svgString, "image/svg+xml");
3662
+ const foreignObjects = Array.from(doc.querySelectorAll("foreignObject"));
3663
+ for (const fo of foreignObjects) {
3664
+ const x = fo.getAttribute("x") || "0";
3665
+ const y = fo.getAttribute("y") || "0";
3666
+ const w = parseFloat(fo.getAttribute("width") || "0");
3667
+ const h = parseFloat(fo.getAttribute("height") || "0");
3668
+ const div = fo.querySelector("div");
3669
+ if (div) {
3670
+ const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
3671
+ text.setAttribute("x", String(parseFloat(x) + w / 2));
3672
+ text.setAttribute("y", String(parseFloat(y) + h / 2 + 4));
3673
+ text.setAttribute("text-anchor", "middle");
3674
+ text.setAttribute("fill", div.style.color || "#000");
3675
+ text.setAttribute("font-size", div.style.fontSize || "12px");
3676
+ text.setAttribute("font-family", div.style.fontFamily || "sans-serif");
3677
+ text.setAttribute("font-weight", div.style.fontWeight || "normal");
3678
+ text.textContent = div.textContent;
3679
+ fo.parentNode?.replaceChild(text, fo);
3680
+ } else {
3681
+ fo.parentNode?.removeChild(fo);
3682
+ }
3683
+ }
3684
+ cleanSvgString = new XMLSerializer().serializeToString(doc);
3685
+ }
3686
+ const svgBlob = new Blob([cleanSvgString], { type: "image/svg+xml;charset=utf-8" });
3476
3687
  const url = URL.createObjectURL(svgBlob);
3477
3688
  try {
3478
3689
  return await new Promise((resolve, reject) => {