@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.js CHANGED
@@ -25,7 +25,8 @@ var DEFAULT_THEME = {
25
25
  backgroundColor: "#ffffff",
26
26
  fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
27
27
  fontSize: 12,
28
- borderRadius: 4
28
+ borderRadius: 4,
29
+ borderColor: "#e2e8f0"
29
30
  };
30
31
  var DARK_THEME = {
31
32
  ...DEFAULT_THEME,
@@ -42,7 +43,8 @@ var DARK_THEME = {
42
43
  markerColor: "#a78bfa",
43
44
  selectionColor: "#38bdf8",
44
45
  textColor: "#e5e7eb",
45
- backgroundColor: "#181c24"
46
+ backgroundColor: "#181c24",
47
+ borderColor: "#334155"
46
48
  };
47
49
  var DENSITY_PRESETS = {
48
50
  compact: { rowHeight: 26, barHeight: 16, headerHeight: 38, fontSize: 11 },
@@ -81,7 +83,8 @@ var CSS_VAR_NAMES = {
81
83
  backgroundColor: "--gantt-background-color",
82
84
  fontFamily: "--gantt-font-family",
83
85
  fontSize: "--gantt-font-size",
84
- borderRadius: "--gantt-border-radius"
86
+ borderRadius: "--gantt-border-radius",
87
+ borderColor: "--gantt-border-color"
85
88
  };
86
89
  var PX_FIELDS = /* @__PURE__ */ new Set([
87
90
  "rowHeight",
@@ -507,20 +510,38 @@ function labelFor(date, viewMode) {
507
510
  const h = date.getHours();
508
511
  const period = h >= 12 ? "PM" : "AM";
509
512
  const h12 = h % 12 === 0 ? 12 : h % 12;
510
- return `${h12} ${period}`;
513
+ return {
514
+ label: `${h12} ${period}`,
515
+ parentLabel: `${WEEKDAY_LABELS[date.getDay()]} ${date.getDate()} ${MONTH_LABELS[date.getMonth()]} ${date.getFullYear()}`
516
+ };
511
517
  }
512
518
  case "day":
513
- return `${WEEKDAY_LABELS[date.getDay()]} ${date.getDate()}`;
519
+ return {
520
+ label: `${WEEKDAY_LABELS[date.getDay()]} ${date.getDate()}`,
521
+ parentLabel: `${MONTH_LABELS[date.getMonth()]} ${date.getFullYear()}`
522
+ };
514
523
  case "week":
515
- return `${MONTH_LABELS[date.getMonth()]} ${date.getDate()}`;
524
+ return {
525
+ label: `${MONTH_LABELS[date.getMonth()]} ${date.getDate()}`,
526
+ parentLabel: `${MONTH_LABELS[date.getMonth()]} ${date.getFullYear()}`
527
+ };
516
528
  case "month":
517
- return `${MONTH_LABELS[date.getMonth()]} ${date.getFullYear()}`;
529
+ return {
530
+ label: MONTH_LABELS[date.getMonth()] ?? "",
531
+ parentLabel: `${date.getFullYear()}`
532
+ };
518
533
  case "quarter": {
519
534
  const q = Math.floor(date.getMonth() / 3) + 1;
520
- return `Q${q} ${date.getFullYear()}`;
535
+ return {
536
+ label: `Q${q}`,
537
+ parentLabel: `${date.getFullYear()}`
538
+ };
521
539
  }
522
540
  case "year":
523
- return `${date.getFullYear()}`;
541
+ return {
542
+ label: `${date.getFullYear()}`,
543
+ parentLabel: ""
544
+ };
524
545
  }
525
546
  }
526
547
  function generateTicks(rangeStart, rangeEnd, viewMode, columnWidth, today = /* @__PURE__ */ new Date(), calendar) {
@@ -534,10 +555,12 @@ function generateTicks(rangeStart, rangeEnd, viewMode, columnWidth, today = /* @
534
555
  while (cursor.getTime() < rangeEnd.getTime() && guard < MAX_TICKS) {
535
556
  guard++;
536
557
  const x = (cursor.getTime() - rangeStart.getTime()) * scale;
558
+ const labels = labelFor(cursor, viewMode);
537
559
  ticks.push({
538
560
  date: new Date(cursor.getTime()),
539
561
  x,
540
- label: labelFor(cursor, viewMode),
562
+ label: labels.label,
563
+ parentLabel: labels.parentLabel,
541
564
  isWeekend: dayLevel ? isWeekend(cursor) : false,
542
565
  isNonWorking: viewMode === "hour" && calendar?.workingHours ? !isWorkingTime(cursor, calendar) : dayLevel ? isNonWorkingDay(cursor, calendar) : false,
543
566
  isToday: isSameDay(cursor, today) && dayLevel,
@@ -778,11 +801,14 @@ function computeLayout(input) {
778
801
  let paddedStart = addUnit(rawRangeStart, viewMode, -1);
779
802
  let paddedEnd = addUnit(rawRangeEnd, viewMode, 1);
780
803
  const minUnits = MIN_VISIBLE_UNITS[viewMode];
804
+ let targetMinUnits = minUnits;
805
+ if (input.timelineViewportWidth && input.timelineViewportWidth > 0) {
806
+ targetMinUnits = Math.max(targetMinUnits, Math.ceil(input.timelineViewportWidth / columnWidth));
807
+ }
781
808
  const currentUnits = (paddedEnd.getTime() - paddedStart.getTime()) / approxUnitMs(viewMode);
782
- if (currentUnits < minUnits) {
783
- const deficit = minUnits - currentUnits;
784
- paddedStart = addUnit(paddedStart, viewMode, -Math.ceil(deficit / 2));
785
- paddedEnd = addUnit(paddedEnd, viewMode, Math.floor(deficit / 2));
809
+ if (currentUnits < targetMinUnits) {
810
+ const deficit = targetMinUnits - currentUnits;
811
+ paddedEnd = addUnit(paddedEnd, viewMode, Math.ceil(deficit));
786
812
  }
787
813
  const rows = [];
788
814
  const bars = [];
@@ -900,6 +926,7 @@ function computeLayout(input) {
900
926
  (t) => ({
901
927
  x: t.x,
902
928
  label: t.label,
929
+ parentLabel: t.parentLabel,
903
930
  isWeekend: t.isWeekend,
904
931
  isNonWorking: t.isNonWorking,
905
932
  isToday: t.isToday,
@@ -1177,6 +1204,9 @@ var GanttRenderer = class {
1177
1204
  this.svg = svgEl("svg");
1178
1205
  this.svg.classList.add("gantt-svg");
1179
1206
  this.timelineScroll.appendChild(this.headerContainer);
1207
+ if (this.options.readonly) {
1208
+ this.root.classList.add("gantt-readonly");
1209
+ }
1180
1210
  this.timelineScroll.appendChild(this.svg);
1181
1211
  this.root.appendChild(this.gridPanel);
1182
1212
  this.root.appendChild(this.timelineScroll);
@@ -1244,7 +1274,10 @@ var GanttRenderer = class {
1244
1274
  const cell = el("div", "gantt-grid-header-cell");
1245
1275
  cell.style.position = "relative";
1246
1276
  if (col.width) cell.style.width = `${col.width}px`;
1247
- cell.textContent = col.title;
1277
+ const titleSpan = document.createElement("span");
1278
+ titleSpan.className = "gantt-cell-text";
1279
+ titleSpan.textContent = col.title;
1280
+ cell.appendChild(titleSpan);
1248
1281
  if (col.sortable) {
1249
1282
  cell.classList.add("gantt-grid-header-cell-sortable");
1250
1283
  const active = sort?.columnId === col.id;
@@ -1286,7 +1319,7 @@ var GanttRenderer = class {
1286
1319
  evt.preventDefault();
1287
1320
  evt.stopPropagation();
1288
1321
  const startX = evt.clientX;
1289
- const startWidth = col.width ?? cell.getBoundingClientRect().width;
1322
+ const startWidth = cell.getBoundingClientRect().width;
1290
1323
  const onMove = (moveEvt) => {
1291
1324
  const newWidth = Math.max(30, startWidth + (moveEvt.clientX - startX));
1292
1325
  cell.style.width = `${newWidth}px`;
@@ -1339,9 +1372,11 @@ var GanttRenderer = class {
1339
1372
  const cell = el("div", "gantt-grid-cell");
1340
1373
  if (col.width) cell.style.width = `${col.width}px`;
1341
1374
  if (col.align) cell.style.textAlign = col.align;
1342
- const rawValue = col.accessor ? col.accessor(task) : colIndex === 0 ? task.name : "";
1375
+ const isTreeColumn = col.id === "name" || !columns.some((c) => c.id === "name") && colIndex === 0;
1376
+ console.log(`Column ${col.id}: isTreeColumn=${isTreeColumn}, colIndex=${colIndex}, hasNameCol=${columns.some((c) => c.id === "name")}`);
1377
+ const rawValue = col.accessor ? col.accessor(task) : isTreeColumn ? task.name : task[col.id] ?? "";
1343
1378
  const textValue = rawValue === null || rawValue === void 0 ? "" : String(rawValue);
1344
- if (colIndex === 0) {
1379
+ if (isTreeColumn) {
1345
1380
  cell.dataset.ganttNameCell = task.id;
1346
1381
  cell.dataset.ganttNameValue = textValue;
1347
1382
  const indent = el("span", "gantt-indent");
@@ -1350,17 +1385,27 @@ var GanttRenderer = class {
1350
1385
  if (row.hasChildren) {
1351
1386
  const toggle = el("button", "gantt-toggle");
1352
1387
  toggle.type = "button";
1388
+ toggle.className = `gantt-toggle ${row.collapsed ? "collapsed" : "expanded"}`;
1353
1389
  toggle.dataset.ganttToggle = task.id;
1354
1390
  toggle.setAttribute("aria-label", row.collapsed ? "Expand" : "Collapse");
1355
1391
  toggle.textContent = row.collapsed ? "\u25B8" : "\u25BE";
1356
1392
  cell.appendChild(toggle);
1357
1393
  }
1394
+ const addChildBtn = el("button", "gantt-add-child");
1395
+ addChildBtn.type = "button";
1396
+ addChildBtn.dataset.ganttAddChild = task.id;
1397
+ addChildBtn.setAttribute("aria-label", "Add child task");
1398
+ addChildBtn.textContent = "+";
1399
+ cell.appendChild(addChildBtn);
1358
1400
  }
1359
1401
  const rendered = col.render ? col.render(task) : void 0;
1360
1402
  if (rendered instanceof HTMLElement) {
1361
1403
  cell.appendChild(rendered);
1362
1404
  } else if (typeof rendered === "string") {
1363
- cell.appendChild(document.createTextNode(rendered));
1405
+ const span = document.createElement("span");
1406
+ span.className = "gantt-cell-text";
1407
+ span.textContent = rendered;
1408
+ cell.appendChild(span);
1364
1409
  } else {
1365
1410
  const href = col.getHref ? col.getHref(task) : null;
1366
1411
  if (href && isSafeHref(href)) {
@@ -1369,10 +1414,13 @@ var GanttRenderer = class {
1369
1414
  anchor.target = col.linkTarget ?? "_blank";
1370
1415
  anchor.rel = "noopener noreferrer";
1371
1416
  anchor.textContent = textValue;
1417
+ anchor.className = "gantt-cell-text";
1372
1418
  cell.appendChild(anchor);
1373
1419
  } else {
1374
- const textNode = document.createTextNode(textValue);
1375
- cell.appendChild(textNode);
1420
+ const span = document.createElement("span");
1421
+ span.className = "gantt-cell-text";
1422
+ span.textContent = textValue;
1423
+ cell.appendChild(span);
1376
1424
  }
1377
1425
  }
1378
1426
  rowEl.appendChild(cell);
@@ -1403,8 +1451,8 @@ var GanttRenderer = class {
1403
1451
  const bodyGroup = svgEl("g");
1404
1452
  this.renderGridLines(bodyGroup, model);
1405
1453
  this.renderRowBackgrounds(bodyGroup, model, startY, endY);
1406
- this.renderBars(bodyGroup, model, tasks, startY, endY);
1407
1454
  this.renderLinks(bodyGroup, model, startY, endY);
1455
+ this.renderBars(bodyGroup, model, tasks, startY, endY);
1408
1456
  this.renderMarkers(bodyGroup, model);
1409
1457
  this.svg.appendChild(bodyGroup);
1410
1458
  this.renderHeader(model);
@@ -1471,15 +1519,21 @@ var GanttRenderer = class {
1471
1519
  clipRect.setAttribute("height", String(model.height));
1472
1520
  clipPath.appendChild(clipRect);
1473
1521
  defs.appendChild(clipPath);
1474
- const label = svgEl("text");
1475
- label.setAttribute("x", String(marker.x + 4));
1476
- label.setAttribute("y", "12");
1477
- label.setAttribute("clip-path", `url(#${clipId})`);
1478
- label.setAttribute("fill", marker.color);
1479
- label.setAttribute("font-size", String(model.theme.fontSize));
1480
- label.setAttribute("font-family", model.theme.fontFamily);
1481
- label.textContent = marker.label;
1482
- group.appendChild(label);
1522
+ const fo = svgEl("foreignObject");
1523
+ fo.setAttribute("x", String(marker.x + 4));
1524
+ fo.setAttribute("y", "0");
1525
+ fo.setAttribute("width", String(cellWidth > 8 ? cellWidth - 8 : cellWidth));
1526
+ fo.setAttribute("height", String(model.height));
1527
+ const div = document.createElement("div");
1528
+ div.style.color = marker.color;
1529
+ div.style.fontSize = `${model.theme.fontSize}px`;
1530
+ div.style.fontFamily = model.theme.fontFamily;
1531
+ div.style.whiteSpace = "normal";
1532
+ div.style.overflowWrap = "break-word";
1533
+ div.style.paddingTop = "4px";
1534
+ div.textContent = marker.label;
1535
+ fo.appendChild(div);
1536
+ group.appendChild(fo);
1483
1537
  }
1484
1538
  });
1485
1539
  }
@@ -1500,6 +1554,60 @@ var GanttRenderer = class {
1500
1554
  headerGroup.appendChild(bg);
1501
1555
  const defs = svgEl("defs");
1502
1556
  const ticks = model.ticks;
1557
+ const parentGroups = [];
1558
+ let currentGroup = null;
1559
+ for (const tick of ticks) {
1560
+ if (!currentGroup || currentGroup.label !== tick.parentLabel) {
1561
+ currentGroup = { x: tick.x, label: tick.parentLabel };
1562
+ parentGroups.push(currentGroup);
1563
+ }
1564
+ }
1565
+ const hasParentTier = parentGroups.some((g) => g.label !== "");
1566
+ const bottomTierY = hasParentTier ? model.headerHeight / 2 : 0;
1567
+ const tierHeight = hasParentTier ? model.headerHeight / 2 : model.headerHeight;
1568
+ if (hasParentTier) {
1569
+ for (let i = 0; i < parentGroups.length; i++) {
1570
+ const group = parentGroups[i];
1571
+ const nextX = parentGroups[i + 1]?.x ?? model.width;
1572
+ const cellWidth = nextX - group.x;
1573
+ const fo = svgEl("foreignObject");
1574
+ fo.setAttribute("x", String(group.x + 4));
1575
+ fo.setAttribute("y", "0");
1576
+ fo.setAttribute("width", String(Math.max(0, cellWidth - 4)));
1577
+ fo.setAttribute("height", String(tierHeight));
1578
+ const div = document.createElement("div");
1579
+ div.style.color = themedFill("textColor", model.theme.textColor);
1580
+ div.style.fontSize = `${model.theme.fontSize * 0.95}px`;
1581
+ div.style.fontFamily = model.theme.fontFamily;
1582
+ div.style.fontWeight = "600";
1583
+ div.style.whiteSpace = "nowrap";
1584
+ div.style.overflow = "hidden";
1585
+ div.style.textOverflow = "ellipsis";
1586
+ div.style.display = "flex";
1587
+ div.style.alignItems = "center";
1588
+ div.style.justifyContent = "center";
1589
+ div.style.height = "100%";
1590
+ div.textContent = group.label;
1591
+ fo.appendChild(div);
1592
+ headerGroup.appendChild(fo);
1593
+ const border = svgEl("line");
1594
+ border.setAttribute("x1", String(group.x));
1595
+ border.setAttribute("x2", String(nextX));
1596
+ border.setAttribute("y1", String(tierHeight));
1597
+ border.setAttribute("y2", String(tierHeight));
1598
+ border.style.stroke = themedFill("borderColor", model.theme.borderColor ?? "#e2e8f0");
1599
+ headerGroup.appendChild(border);
1600
+ if (i > 0) {
1601
+ const sep = svgEl("line");
1602
+ sep.setAttribute("x1", String(group.x));
1603
+ sep.setAttribute("x2", String(group.x));
1604
+ sep.setAttribute("y1", "0");
1605
+ sep.setAttribute("y2", String(tierHeight));
1606
+ sep.style.stroke = themedFill("borderColor", model.theme.borderColor ?? "#e2e8f0");
1607
+ headerGroup.appendChild(sep);
1608
+ }
1609
+ }
1610
+ }
1503
1611
  for (let i = 0; i < ticks.length; i++) {
1504
1612
  const tick = ticks[i];
1505
1613
  const cellWidth = (ticks[i + 1]?.x ?? model.width) - tick.x;
@@ -1508,20 +1616,30 @@ var GanttRenderer = class {
1508
1616
  clipPath.setAttribute("id", clipId);
1509
1617
  const clipRect = svgEl("rect");
1510
1618
  clipRect.setAttribute("x", String(tick.x));
1511
- clipRect.setAttribute("y", "0");
1619
+ clipRect.setAttribute("y", String(bottomTierY));
1512
1620
  clipRect.setAttribute("width", String(Math.max(0, cellWidth)));
1513
- clipRect.setAttribute("height", String(model.headerHeight));
1621
+ clipRect.setAttribute("height", String(tierHeight));
1514
1622
  clipPath.appendChild(clipRect);
1515
1623
  defs.appendChild(clipPath);
1516
- const text = svgEl("text");
1517
- text.setAttribute("x", String(tick.x + 4));
1518
- text.setAttribute("y", String(model.headerHeight - 8));
1519
- text.setAttribute("clip-path", `url(#${clipId})`);
1520
- text.style.fill = themedFill("textColor", model.theme.textColor);
1521
- text.setAttribute("font-size", String(model.theme.fontSize));
1522
- text.setAttribute("font-family", model.theme.fontFamily);
1523
- text.textContent = tick.label;
1524
- headerGroup.appendChild(text);
1624
+ const fo = svgEl("foreignObject");
1625
+ fo.setAttribute("x", String(tick.x + 4));
1626
+ fo.setAttribute("y", String(bottomTierY));
1627
+ fo.setAttribute("width", String(cellWidth > 8 ? cellWidth - 8 : cellWidth));
1628
+ fo.setAttribute("height", String(tierHeight));
1629
+ const div = document.createElement("div");
1630
+ div.style.color = themedFill("textColor", model.theme.textColor);
1631
+ div.style.fontSize = `${model.theme.fontSize}px`;
1632
+ div.style.fontFamily = model.theme.fontFamily;
1633
+ div.style.whiteSpace = "normal";
1634
+ div.style.overflowWrap = "break-word";
1635
+ div.style.display = "flex";
1636
+ div.style.alignItems = hasParentTier ? "center" : "flex-end";
1637
+ div.style.justifyContent = "center";
1638
+ div.style.paddingBottom = hasParentTier ? "0" : "4px";
1639
+ div.style.height = "100%";
1640
+ div.textContent = tick.label;
1641
+ fo.appendChild(div);
1642
+ headerGroup.appendChild(fo);
1525
1643
  }
1526
1644
  headerGroup.insertBefore(defs, headerGroup.firstChild);
1527
1645
  this.headerSvg.appendChild(headerGroup);
@@ -1564,8 +1682,8 @@ var GanttRenderer = class {
1564
1682
  g.setAttribute("role", "button");
1565
1683
  if (task) {
1566
1684
  const progress = Math.round(task.progress ?? 0);
1567
- const label2 = `${task.name}, ${task.start.toDateString()} to ${task.end.toDateString()}, ${progress}% complete`;
1568
- g.setAttribute("aria-label", label2);
1685
+ const label = `${task.name}, ${task.start.toDateString()} to ${task.end.toDateString()}, ${progress}% complete`;
1686
+ g.setAttribute("aria-label", label);
1569
1687
  }
1570
1688
  }
1571
1689
  if (bar.baseline) {
@@ -1613,17 +1731,15 @@ var GanttRenderer = class {
1613
1731
  `rotate(45, 0, ${bar.height / 2})`
1614
1732
  );
1615
1733
  g.appendChild(diamond);
1616
- const label2 = svgEl("text");
1617
- label2.setAttribute("x", String(size / 2 + 8));
1618
- label2.setAttribute("y", String(bar.height / 2 + 4));
1619
- label2.style.fill = themedFill("textColor", model.theme.textColor);
1620
- label2.setAttribute("font-size", String(model.theme.fontSize));
1621
- label2.setAttribute("font-family", model.theme.fontFamily);
1622
- label2.textContent = bar.label;
1623
- g.appendChild(label2);
1624
- return g;
1625
- }
1626
- if (bar.segments && bar.segments.length > 0) {
1734
+ const label = svgEl("text");
1735
+ label.setAttribute("x", String(size / 2 + 8));
1736
+ label.setAttribute("y", String(bar.height / 2 + 4));
1737
+ label.style.fill = themedFill("textColor", model.theme.textColor);
1738
+ label.setAttribute("font-size", String(model.theme.fontSize));
1739
+ label.setAttribute("font-family", model.theme.fontFamily);
1740
+ label.textContent = bar.label;
1741
+ g.appendChild(label);
1742
+ } else if (bar.segments && bar.segments.length > 0) {
1627
1743
  bar.segments.forEach((seg, i) => {
1628
1744
  const segRect = svgEl("rect");
1629
1745
  segRect.setAttribute("x", String(seg.x));
@@ -1696,29 +1812,31 @@ var GanttRenderer = class {
1696
1812
  selectionOutline.setAttribute("stroke-width", "2");
1697
1813
  g.appendChild(selectionOutline);
1698
1814
  }
1699
- const handleWidth = 6;
1700
- const leftHandle = svgEl("rect");
1701
- leftHandle.setAttribute("x", "0");
1702
- leftHandle.setAttribute("y", "0");
1703
- leftHandle.setAttribute("width", String(handleWidth));
1704
- leftHandle.setAttribute("height", String(bar.height));
1705
- leftHandle.setAttribute("fill", "transparent");
1706
- leftHandle.dataset.ganttHandle = "left";
1707
- leftHandle.dataset.ganttHandleFor = bar.taskId;
1708
- leftHandle.style.cursor = "ew-resize";
1709
- g.appendChild(leftHandle);
1710
- const rightHandle = svgEl("rect");
1711
- rightHandle.setAttribute("x", String(Math.max(0, bar.width - handleWidth)));
1712
- rightHandle.setAttribute("y", "0");
1713
- rightHandle.setAttribute("width", String(handleWidth));
1714
- rightHandle.setAttribute("height", String(bar.height));
1715
- rightHandle.setAttribute("fill", "transparent");
1716
- rightHandle.dataset.ganttHandle = "right";
1717
- rightHandle.dataset.ganttHandleFor = bar.taskId;
1718
- rightHandle.style.cursor = "ew-resize";
1719
- g.appendChild(rightHandle);
1815
+ if (!bar.isMilestone) {
1816
+ const handleWidth = 6;
1817
+ const leftHandle = svgEl("rect");
1818
+ leftHandle.setAttribute("x", "0");
1819
+ leftHandle.setAttribute("y", "0");
1820
+ leftHandle.setAttribute("width", String(handleWidth));
1821
+ leftHandle.setAttribute("height", String(bar.height));
1822
+ leftHandle.setAttribute("fill", "transparent");
1823
+ leftHandle.dataset.ganttHandle = "left";
1824
+ leftHandle.dataset.ganttHandleFor = bar.taskId;
1825
+ leftHandle.style.cursor = "ew-resize";
1826
+ g.appendChild(leftHandle);
1827
+ const rightHandle = svgEl("rect");
1828
+ rightHandle.setAttribute("x", String(Math.max(0, bar.width - handleWidth)));
1829
+ rightHandle.setAttribute("y", "0");
1830
+ rightHandle.setAttribute("width", String(handleWidth));
1831
+ rightHandle.setAttribute("height", String(bar.height));
1832
+ rightHandle.setAttribute("fill", "transparent");
1833
+ rightHandle.dataset.ganttHandle = "right";
1834
+ rightHandle.dataset.ganttHandleFor = bar.taskId;
1835
+ rightHandle.style.cursor = "ew-resize";
1836
+ g.appendChild(rightHandle);
1837
+ }
1720
1838
  const connector = svgEl("circle");
1721
- connector.setAttribute("cx", String(bar.width));
1839
+ connector.setAttribute("cx", String(bar.width + (bar.isMilestone ? 10 : 0)));
1722
1840
  connector.setAttribute("cy", String(bar.height / 2));
1723
1841
  connector.setAttribute("r", "4");
1724
1842
  connector.style.fill = themedFill("linkColor", model.theme.linkColor);
@@ -1726,14 +1844,16 @@ var GanttRenderer = class {
1726
1844
  connector.dataset.ganttConnectorSide = "right";
1727
1845
  connector.style.cursor = "crosshair";
1728
1846
  g.appendChild(connector);
1729
- const label = svgEl("text");
1730
- label.setAttribute("x", String(bar.width + 6));
1731
- label.setAttribute("y", String(bar.height / 2 + 4));
1732
- label.style.fill = themedFill("textColor", model.theme.textColor);
1733
- label.setAttribute("font-size", String(model.theme.fontSize));
1734
- label.setAttribute("font-family", model.theme.fontFamily);
1735
- label.textContent = bar.label;
1736
- g.appendChild(label);
1847
+ if (!bar.isMilestone) {
1848
+ const label = svgEl("text");
1849
+ label.setAttribute("x", String(bar.width + 6));
1850
+ label.setAttribute("y", String(bar.height / 2 + 4));
1851
+ label.style.fill = themedFill("textColor", model.theme.textColor);
1852
+ label.setAttribute("font-size", String(model.theme.fontSize));
1853
+ label.setAttribute("font-family", model.theme.fontFamily);
1854
+ label.textContent = bar.label;
1855
+ g.appendChild(label);
1856
+ }
1737
1857
  if (bar.deadlineX !== void 0) {
1738
1858
  const markerX = bar.deadlineX - bar.x;
1739
1859
  const flag = svgEl("g");
@@ -1791,10 +1911,20 @@ var GanttRenderer = class {
1791
1911
  for (const link of model.links) {
1792
1912
  const fromBar = barByTaskId.get(link.fromId);
1793
1913
  const toBar = barByTaskId.get(link.toId);
1794
- const relevantY = toBar?.y ?? fromBar?.y ?? 0;
1914
+ if (!fromBar || !toBar) continue;
1915
+ const relevantY = toBar.y ?? fromBar.y ?? 0;
1795
1916
  if (relevantY < startY || relevantY > endY) continue;
1917
+ const type = link.type ?? "FS";
1918
+ const x1 = type.startsWith("F") ? fromBar.x + fromBar.width : fromBar.x;
1919
+ const y1 = fromBar.y + fromBar.height / 2;
1920
+ const x2 = type.endsWith("F") ? toBar.x + toBar.width : toBar.x;
1921
+ const y2 = toBar.y + toBar.height / 2;
1922
+ const offset = 10;
1923
+ const routingX1 = type.startsWith("F") ? x1 + offset : x1 - offset;
1924
+ const routingX2 = type.endsWith("F") ? x2 + offset : x2 - offset;
1925
+ const d = `M ${x1} ${y1} L ${routingX1} ${y1} C ${routingX1 + (routingX2 - routingX1) / 2} ${y1}, ${routingX1 + (routingX2 - routingX1) / 2} ${y2}, ${routingX2} ${y2} L ${x2} ${y2}`;
1796
1926
  const path = svgEl("path");
1797
- path.setAttribute("d", link.path);
1927
+ path.setAttribute("d", d);
1798
1928
  path.setAttribute("fill", "none");
1799
1929
  path.style.stroke = link.isCritical ? themedFill("criticalColor", model.theme.criticalColor) : themedFill("linkColor", model.theme.linkColor);
1800
1930
  path.setAttribute("stroke-width", link.isCritical ? "2" : "1.5");
@@ -2192,6 +2322,12 @@ var InteractionController = class {
2192
2322
  const toggleId = target.closest("[data-gantt-toggle]")?.dataset.ganttToggle;
2193
2323
  if (toggleId) {
2194
2324
  this.callbacks.onToggleCollapse(toggleId);
2325
+ return;
2326
+ }
2327
+ const addChildId = target.closest("[data-gantt-add-child]")?.dataset.ganttAddChild;
2328
+ if (addChildId && this.callbacks.onAddChild) {
2329
+ this.callbacks.onAddChild(addChildId);
2330
+ return;
2195
2331
  }
2196
2332
  };
2197
2333
  this.onContextMenu = (evt) => {
@@ -2651,6 +2787,7 @@ var _GanttChart = class _GanttChart {
2651
2787
  markers: options.markers,
2652
2788
  pagination: options.pagination,
2653
2789
  snapToUnit: options.snapToUnit,
2790
+ headerPosition: options.headerPosition ?? "top",
2654
2791
  onDateChange: options.onDateChange,
2655
2792
  onProgressChange: options.onProgressChange,
2656
2793
  onDependencyCreate: options.onDependencyCreate,
@@ -2683,6 +2820,7 @@ var _GanttChart = class _GanttChart {
2683
2820
  this.darkMediaQuery.addEventListener?.("change", this.handleSchemeChange);
2684
2821
  }
2685
2822
  this.renderer = new GanttRenderer(this.container, {
2823
+ readonly: this.options.readonly,
2686
2824
  keyboardAccessible: this.options.keyboardAccessible,
2687
2825
  showAssigneeAvatars: this.options.showAssigneeAvatars,
2688
2826
  virtualScroll: this.options.virtualScroll,
@@ -2692,6 +2830,13 @@ var _GanttChart = class _GanttChart {
2692
2830
  onRowReorder: this.options.onTaskReorder ? (draggedId, targetId, position) => this.handleRowReorder(draggedId, targetId, position) : void 0,
2693
2831
  onSortClick: (columnId) => this.handleSortClick(columnId)
2694
2832
  });
2833
+ if (this.options.headerPosition && this.renderer?.root) {
2834
+ this.renderer.root.dataset.ganttHeaderPosition = this.options.headerPosition;
2835
+ }
2836
+ if (typeof ResizeObserver !== "undefined") {
2837
+ this.resizeObserver = new ResizeObserver(() => this.scheduleRender());
2838
+ this.resizeObserver.observe(this.renderer.timelineScroll);
2839
+ }
2695
2840
  this.interactions = new InteractionController(
2696
2841
  this.renderer.svg,
2697
2842
  this.renderer.gridPanel,
@@ -2730,7 +2875,8 @@ var _GanttChart = class _GanttChart {
2730
2875
  } : void 0,
2731
2876
  onSelectAll: this.options.selectable ? () => this.selectAllVisible() : void 0,
2732
2877
  onJumpToStart: () => this.scrollToRangeStart(),
2733
- onJumpToEnd: () => this.scrollToRangeEnd()
2878
+ onJumpToEnd: () => this.scrollToRangeEnd(),
2879
+ onAddChild: (parentId) => this.handleAddChild(parentId)
2734
2880
  },
2735
2881
  {
2736
2882
  readonly: this.options.readonly,
@@ -3020,6 +3166,35 @@ var _GanttChart = class _GanttChart {
3020
3166
  };
3021
3167
  this.runCommand(cmd);
3022
3168
  }
3169
+ handleAddChild(parentId) {
3170
+ const parent = this.tasks.find((t) => t.id === parentId);
3171
+ if (!parent) return;
3172
+ const newTask = {
3173
+ id: `task-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
3174
+ name: "New task",
3175
+ start: new Date(parent.start.getTime()),
3176
+ end: new Date(parent.start.getTime() + MS_PER_DAY),
3177
+ parentId: parent.id
3178
+ };
3179
+ const cmd = {
3180
+ label: "task-create",
3181
+ do: () => {
3182
+ const index = this.tasks.findIndex((t) => t.id === parentId);
3183
+ if (index >= 0) this.tasks.splice(index + 1, 0, newTask);
3184
+ else this.tasks.push(newTask);
3185
+ this.emitter.emit("task-create", { task: newTask });
3186
+ this.options.onTaskCreate?.(newTask);
3187
+ this.emitter.emit("tasks-change", { tasks: this.getTasks() });
3188
+ this.scheduleRender();
3189
+ },
3190
+ undo: () => {
3191
+ this.tasks = this.tasks.filter((t) => t.id !== newTask.id);
3192
+ this.emitter.emit("tasks-change", { tasks: this.getTasks() });
3193
+ this.scheduleRender();
3194
+ }
3195
+ };
3196
+ this.runCommand(cmd);
3197
+ }
3023
3198
  handleBarClick(taskId, modifiers) {
3024
3199
  const task = this.tasks.find((t) => t.id === taskId);
3025
3200
  if (!task) return;
@@ -3251,6 +3426,12 @@ var _GanttChart = class _GanttChart {
3251
3426
  if (partial.gridPanelWidth !== void 0) {
3252
3427
  this.container.style.setProperty("--gantt-grid-panel-width", `${partial.gridPanelWidth}px`);
3253
3428
  }
3429
+ if ("headerPosition" in partial) {
3430
+ this.options.headerPosition = partial.headerPosition ?? "top";
3431
+ if (this.renderer?.root) {
3432
+ this.renderer.root.dataset.ganttHeaderPosition = this.options.headerPosition;
3433
+ }
3434
+ }
3254
3435
  Object.assign(this.options, partial);
3255
3436
  this.scheduleRender();
3256
3437
  }
@@ -3338,9 +3519,11 @@ var _GanttChart = class _GanttChart {
3338
3519
  }
3339
3520
  computeModel() {
3340
3521
  const pagination = this.options.pagination ? { pageSize: this.options.pagination.pageSize, page: this.currentPage } : void 0;
3522
+ const timelineViewportWidth = HAS_DOM && this.renderer ? this.renderer.timelineScroll.clientWidth : void 0;
3341
3523
  return computeLayout({
3342
3524
  tasks: this.tasks,
3343
3525
  dependencies: this.dependencies,
3526
+ timelineViewportWidth,
3344
3527
  viewMode: this.options.viewMode,
3345
3528
  columnWidth: this.columnWidth,
3346
3529
  theme: this.theme,
@@ -3423,7 +3606,35 @@ var _GanttChart = class _GanttChart {
3423
3606
  return this.renderer?.toSVGString() ?? "";
3424
3607
  }
3425
3608
  async rasterizeSVG(svgString, width, height) {
3426
- const svgBlob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
3609
+ let cleanSvgString = svgString;
3610
+ if (typeof DOMParser !== "undefined") {
3611
+ const parser = new DOMParser();
3612
+ const doc = parser.parseFromString(svgString, "image/svg+xml");
3613
+ const foreignObjects = Array.from(doc.querySelectorAll("foreignObject"));
3614
+ for (const fo of foreignObjects) {
3615
+ const x = fo.getAttribute("x") || "0";
3616
+ const y = fo.getAttribute("y") || "0";
3617
+ const w = parseFloat(fo.getAttribute("width") || "0");
3618
+ const h = parseFloat(fo.getAttribute("height") || "0");
3619
+ const div = fo.querySelector("div");
3620
+ if (div) {
3621
+ const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
3622
+ text.setAttribute("x", String(parseFloat(x) + w / 2));
3623
+ text.setAttribute("y", String(parseFloat(y) + h / 2 + 4));
3624
+ text.setAttribute("text-anchor", "middle");
3625
+ text.setAttribute("fill", div.style.color || "#000");
3626
+ text.setAttribute("font-size", div.style.fontSize || "12px");
3627
+ text.setAttribute("font-family", div.style.fontFamily || "sans-serif");
3628
+ text.setAttribute("font-weight", div.style.fontWeight || "normal");
3629
+ text.textContent = div.textContent;
3630
+ fo.parentNode?.replaceChild(text, fo);
3631
+ } else {
3632
+ fo.parentNode?.removeChild(fo);
3633
+ }
3634
+ }
3635
+ cleanSvgString = new XMLSerializer().serializeToString(doc);
3636
+ }
3637
+ const svgBlob = new Blob([cleanSvgString], { type: "image/svg+xml;charset=utf-8" });
3427
3638
  const url = URL.createObjectURL(svgBlob);
3428
3639
  try {
3429
3640
  return await new Promise((resolve, reject) => {