@ganttloom/gantt-core 0.2.1 → 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,
@@ -614,6 +637,29 @@ function computeProgressRollup(node) {
614
637
  }
615
638
  node.computedProgress = totalWeight > 0 ? weightedSum / totalWeight : 0;
616
639
  }
640
+ function sortValue(node, col) {
641
+ if (!col) return null;
642
+ if (col.accessor) return col.accessor(node.task) ?? null;
643
+ return col.id === "name" ? node.task.name : null;
644
+ }
645
+ function compareValues(a, b) {
646
+ if (a === null && b === null) return 0;
647
+ if (a === null) return 1;
648
+ if (b === null) return -1;
649
+ if (typeof a === "number" && typeof b === "number") return a - b;
650
+ return String(a).localeCompare(String(b), void 0, { numeric: true, sensitivity: "base" });
651
+ }
652
+ function sortTree(roots, columns, sort) {
653
+ if (!sort) return;
654
+ const col = columns.find((c) => c.id === sort.columnId);
655
+ const dir = sort.direction === "desc" ? -1 : 1;
656
+ const compare = (a, b) => dir * compareValues(sortValue(a, col), sortValue(b, col));
657
+ const sortLevel = (nodes) => {
658
+ nodes.sort(compare);
659
+ for (const node of nodes) sortLevel(node.children);
660
+ };
661
+ sortLevel(roots);
662
+ }
617
663
  function flatten(roots) {
618
664
  const out = [];
619
665
  const stack = [];
@@ -700,6 +746,14 @@ function linkPath(from, to) {
700
746
  const c2y = to.y;
701
747
  return `M ${from.x} ${from.y} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${to.x} ${to.y}`;
702
748
  }
749
+ var MIN_VISIBLE_UNITS = {
750
+ hour: 8,
751
+ day: 7,
752
+ week: 6,
753
+ month: 6,
754
+ quarter: 4,
755
+ year: 5
756
+ };
703
757
  function computeLayout(input) {
704
758
  const {
705
759
  tasks,
@@ -715,7 +769,8 @@ function computeLayout(input) {
715
769
  markers = [],
716
770
  autoRollupProgress = false,
717
771
  selectedTaskIds,
718
- pagination
772
+ pagination,
773
+ sort = null
719
774
  } = input;
720
775
  validateInputs(tasks, dependencies);
721
776
  const { roots, nodeById } = buildTree(tasks);
@@ -723,6 +778,7 @@ function computeLayout(input) {
723
778
  if (autoRollupProgress) {
724
779
  for (const root of roots) computeProgressRollup(root);
725
780
  }
781
+ sortTree(roots, columns, sort);
726
782
  let visibleRoots = roots;
727
783
  if (pagination && pagination.pageSize > 0) {
728
784
  const start = Math.max(0, (pagination.page - 1) * pagination.pageSize);
@@ -742,8 +798,18 @@ function computeLayout(input) {
742
798
  const now = Date.now();
743
799
  const rawRangeStart = rangeMin === null ? new Date(now) : new Date(rangeMin);
744
800
  const rawRangeEnd = rangeMax === null ? new Date(now + 1) : new Date(rangeMax);
745
- const paddedStart = addUnit(rawRangeStart, viewMode, -1);
746
- const paddedEnd = addUnit(rawRangeEnd, viewMode, 1);
801
+ let paddedStart = addUnit(rawRangeStart, viewMode, -1);
802
+ let paddedEnd = addUnit(rawRangeEnd, viewMode, 1);
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
+ }
808
+ const currentUnits = (paddedEnd.getTime() - paddedStart.getTime()) / approxUnitMs(viewMode);
809
+ if (currentUnits < targetMinUnits) {
810
+ const deficit = targetMinUnits - currentUnits;
811
+ paddedEnd = addUnit(paddedEnd, viewMode, Math.ceil(deficit));
812
+ }
747
813
  const rows = [];
748
814
  const bars = [];
749
815
  const barByTaskId = /* @__PURE__ */ new Map();
@@ -860,6 +926,7 @@ function computeLayout(input) {
860
926
  (t) => ({
861
927
  x: t.x,
862
928
  label: t.label,
929
+ parentLabel: t.parentLabel,
863
930
  isWeekend: t.isWeekend,
864
931
  isNonWorking: t.isNonWorking,
865
932
  isToday: t.isToday,
@@ -894,7 +961,8 @@ function computeLayout(input) {
894
961
  columns,
895
962
  theme,
896
963
  markers: renderMarkers,
897
- rangeStart: paddedStart
964
+ rangeStart: paddedStart,
965
+ sort
898
966
  };
899
967
  }
900
968
 
@@ -1091,6 +1159,7 @@ var GanttRenderer = class {
1091
1159
  this.lastModel = null;
1092
1160
  this.lastTasks = [];
1093
1161
  this.lastColumns = [];
1162
+ this.lastExplicitThemeKeys = /* @__PURE__ */ new Set();
1094
1163
  this.onGridDoubleClick = (evt) => {
1095
1164
  if (!this.options.onRenameCommit) return;
1096
1165
  const target = evt.target;
@@ -1128,8 +1197,16 @@ var GanttRenderer = class {
1128
1197
  this.root = el("div", "gantt-root");
1129
1198
  this.gridPanel = el("div", "gantt-grid-panel");
1130
1199
  this.timelineScroll = el("div", "gantt-timeline-scroll");
1200
+ this.headerContainer = el("div", "gantt-timeline-header");
1201
+ this.headerSvg = svgEl("svg");
1202
+ this.headerSvg.classList.add("gantt-header-svg");
1203
+ this.headerContainer.appendChild(this.headerSvg);
1131
1204
  this.svg = svgEl("svg");
1132
1205
  this.svg.classList.add("gantt-svg");
1206
+ this.timelineScroll.appendChild(this.headerContainer);
1207
+ if (this.options.readonly) {
1208
+ this.root.classList.add("gantt-readonly");
1209
+ }
1133
1210
  this.timelineScroll.appendChild(this.svg);
1134
1211
  this.root.appendChild(this.gridPanel);
1135
1212
  this.root.appendChild(this.timelineScroll);
@@ -1145,9 +1222,14 @@ var GanttRenderer = class {
1145
1222
  this.lastTasks = tasks;
1146
1223
  this.lastColumns = columns;
1147
1224
  const cssVars = explicitThemeToCssVars(explicitTheme);
1148
- for (const key of Object.keys(cssVars)) {
1225
+ const newKeys = new Set(Object.keys(cssVars));
1226
+ for (const key of this.lastExplicitThemeKeys) {
1227
+ if (!newKeys.has(key)) this.root.style.removeProperty(key);
1228
+ }
1229
+ for (const key of newKeys) {
1149
1230
  this.root.style.setProperty(key, cssVars[key]);
1150
1231
  }
1232
+ this.lastExplicitThemeKeys = newKeys;
1151
1233
  this.renderGridPanel(model, tasks, columns);
1152
1234
  this.renderTimeline(model, tasks);
1153
1235
  }
@@ -1175,7 +1257,7 @@ var GanttRenderer = class {
1175
1257
  const header = el("div", "gantt-grid-header-row");
1176
1258
  header.style.height = `${model.headerHeight}px`;
1177
1259
  for (const col of columns) {
1178
- header.appendChild(this.renderHeaderCell(col, columns));
1260
+ header.appendChild(this.renderHeaderCell(col, columns, model.sort));
1179
1261
  }
1180
1262
  this.gridPanel.appendChild(header);
1181
1263
  const body = el("div", "gantt-grid-body");
@@ -1188,11 +1270,26 @@ var GanttRenderer = class {
1188
1270
  });
1189
1271
  this.gridPanel.appendChild(body);
1190
1272
  }
1191
- renderHeaderCell(col, columns) {
1273
+ renderHeaderCell(col, columns, sort) {
1192
1274
  const cell = el("div", "gantt-grid-header-cell");
1193
1275
  cell.style.position = "relative";
1194
1276
  if (col.width) cell.style.width = `${col.width}px`;
1195
- 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);
1281
+ if (col.sortable) {
1282
+ cell.classList.add("gantt-grid-header-cell-sortable");
1283
+ const active = sort?.columnId === col.id;
1284
+ const indicator = el("span", "gantt-sort-indicator");
1285
+ indicator.textContent = active ? sort.direction === "asc" ? " \u25B2" : " \u25BC" : "";
1286
+ cell.appendChild(indicator);
1287
+ cell.addEventListener("click", (evt) => {
1288
+ const target = evt.target;
1289
+ if (target.closest(".gantt-col-resize-handle")) return;
1290
+ this.options.onSortClick?.(col.id);
1291
+ });
1292
+ }
1196
1293
  if (this.options.onColumnReorder) {
1197
1294
  cell.draggable = true;
1198
1295
  cell.style.cursor = "grab";
@@ -1222,7 +1319,7 @@ var GanttRenderer = class {
1222
1319
  evt.preventDefault();
1223
1320
  evt.stopPropagation();
1224
1321
  const startX = evt.clientX;
1225
- const startWidth = col.width ?? cell.getBoundingClientRect().width;
1322
+ const startWidth = cell.getBoundingClientRect().width;
1226
1323
  const onMove = (moveEvt) => {
1227
1324
  const newWidth = Math.max(30, startWidth + (moveEvt.clientX - startX));
1228
1325
  cell.style.width = `${newWidth}px`;
@@ -1275,9 +1372,11 @@ var GanttRenderer = class {
1275
1372
  const cell = el("div", "gantt-grid-cell");
1276
1373
  if (col.width) cell.style.width = `${col.width}px`;
1277
1374
  if (col.align) cell.style.textAlign = col.align;
1278
- 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] ?? "";
1279
1378
  const textValue = rawValue === null || rawValue === void 0 ? "" : String(rawValue);
1280
- if (colIndex === 0) {
1379
+ if (isTreeColumn) {
1281
1380
  cell.dataset.ganttNameCell = task.id;
1282
1381
  cell.dataset.ganttNameValue = textValue;
1283
1382
  const indent = el("span", "gantt-indent");
@@ -1286,17 +1385,27 @@ var GanttRenderer = class {
1286
1385
  if (row.hasChildren) {
1287
1386
  const toggle = el("button", "gantt-toggle");
1288
1387
  toggle.type = "button";
1388
+ toggle.className = `gantt-toggle ${row.collapsed ? "collapsed" : "expanded"}`;
1289
1389
  toggle.dataset.ganttToggle = task.id;
1290
1390
  toggle.setAttribute("aria-label", row.collapsed ? "Expand" : "Collapse");
1291
1391
  toggle.textContent = row.collapsed ? "\u25B8" : "\u25BE";
1292
1392
  cell.appendChild(toggle);
1293
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);
1294
1400
  }
1295
1401
  const rendered = col.render ? col.render(task) : void 0;
1296
1402
  if (rendered instanceof HTMLElement) {
1297
1403
  cell.appendChild(rendered);
1298
1404
  } else if (typeof rendered === "string") {
1299
- 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);
1300
1409
  } else {
1301
1410
  const href = col.getHref ? col.getHref(task) : null;
1302
1411
  if (href && isSafeHref(href)) {
@@ -1305,10 +1414,13 @@ var GanttRenderer = class {
1305
1414
  anchor.target = col.linkTarget ?? "_blank";
1306
1415
  anchor.rel = "noopener noreferrer";
1307
1416
  anchor.textContent = textValue;
1417
+ anchor.className = "gantt-cell-text";
1308
1418
  cell.appendChild(anchor);
1309
1419
  } else {
1310
- const textNode = document.createTextNode(textValue);
1311
- cell.appendChild(textNode);
1420
+ const span = document.createElement("span");
1421
+ span.className = "gantt-cell-text";
1422
+ span.textContent = textValue;
1423
+ cell.appendChild(span);
1312
1424
  }
1313
1425
  }
1314
1426
  rowEl.appendChild(cell);
@@ -1319,8 +1431,8 @@ var GanttRenderer = class {
1319
1431
  renderTimeline(model, tasks) {
1320
1432
  this.svg.replaceChildren();
1321
1433
  this.svg.setAttribute("width", String(model.width));
1322
- this.svg.setAttribute("height", String(model.headerHeight + model.height));
1323
- this.svg.setAttribute("viewBox", `0 0 ${model.width} ${model.headerHeight + model.height}`);
1434
+ this.svg.setAttribute("height", String(model.height));
1435
+ this.svg.setAttribute("viewBox", `0 0 ${model.width} ${model.height}`);
1324
1436
  const defs = svgEl("defs");
1325
1437
  const marker = svgEl("marker");
1326
1438
  marker.setAttribute("id", this.markerDefsId);
@@ -1337,11 +1449,10 @@ var GanttRenderer = class {
1337
1449
  this.svg.appendChild(defs);
1338
1450
  const { startY, endY } = this.getViewportRowRange(model);
1339
1451
  const bodyGroup = svgEl("g");
1340
- bodyGroup.setAttribute("transform", `translate(0, ${model.headerHeight})`);
1341
1452
  this.renderGridLines(bodyGroup, model);
1342
1453
  this.renderRowBackgrounds(bodyGroup, model, startY, endY);
1343
- this.renderBars(bodyGroup, model, tasks, startY, endY);
1344
1454
  this.renderLinks(bodyGroup, model, startY, endY);
1455
+ this.renderBars(bodyGroup, model, tasks, startY, endY);
1345
1456
  this.renderMarkers(bodyGroup, model);
1346
1457
  this.svg.appendChild(bodyGroup);
1347
1458
  this.renderHeader(model);
@@ -1408,22 +1519,31 @@ var GanttRenderer = class {
1408
1519
  clipRect.setAttribute("height", String(model.height));
1409
1520
  clipPath.appendChild(clipRect);
1410
1521
  defs.appendChild(clipPath);
1411
- const label = svgEl("text");
1412
- label.setAttribute("x", String(marker.x + 4));
1413
- label.setAttribute("y", "12");
1414
- label.setAttribute("clip-path", `url(#${clipId})`);
1415
- label.setAttribute("fill", marker.color);
1416
- label.setAttribute("font-size", String(model.theme.fontSize));
1417
- label.setAttribute("font-family", model.theme.fontFamily);
1418
- label.textContent = marker.label;
1419
- 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);
1420
1537
  }
1421
1538
  });
1422
1539
  }
1423
1540
  renderHeader(model) {
1424
- let headerGroup = this.svg.querySelector(".gantt-header-group");
1425
- if (headerGroup) headerGroup.remove();
1426
- headerGroup = svgEl("g");
1541
+ this.headerSvg.replaceChildren();
1542
+ this.headerSvg.setAttribute("width", String(model.width));
1543
+ this.headerSvg.setAttribute("height", String(model.headerHeight));
1544
+ this.headerSvg.setAttribute("viewBox", `0 0 ${model.width} ${model.headerHeight}`);
1545
+ this.headerContainer.style.height = `${model.headerHeight}px`;
1546
+ const headerGroup = svgEl("g");
1427
1547
  headerGroup.setAttribute("class", "gantt-header-group");
1428
1548
  const bg = svgEl("rect");
1429
1549
  bg.setAttribute("x", "0");
@@ -1434,6 +1554,60 @@ var GanttRenderer = class {
1434
1554
  headerGroup.appendChild(bg);
1435
1555
  const defs = svgEl("defs");
1436
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
+ }
1437
1611
  for (let i = 0; i < ticks.length; i++) {
1438
1612
  const tick = ticks[i];
1439
1613
  const cellWidth = (ticks[i + 1]?.x ?? model.width) - tick.x;
@@ -1442,23 +1616,33 @@ var GanttRenderer = class {
1442
1616
  clipPath.setAttribute("id", clipId);
1443
1617
  const clipRect = svgEl("rect");
1444
1618
  clipRect.setAttribute("x", String(tick.x));
1445
- clipRect.setAttribute("y", "0");
1619
+ clipRect.setAttribute("y", String(bottomTierY));
1446
1620
  clipRect.setAttribute("width", String(Math.max(0, cellWidth)));
1447
- clipRect.setAttribute("height", String(model.headerHeight));
1621
+ clipRect.setAttribute("height", String(tierHeight));
1448
1622
  clipPath.appendChild(clipRect);
1449
1623
  defs.appendChild(clipPath);
1450
- const text = svgEl("text");
1451
- text.setAttribute("x", String(tick.x + 4));
1452
- text.setAttribute("y", String(model.headerHeight - 8));
1453
- text.setAttribute("clip-path", `url(#${clipId})`);
1454
- text.style.fill = themedFill("textColor", model.theme.textColor);
1455
- text.setAttribute("font-size", String(model.theme.fontSize));
1456
- text.setAttribute("font-family", model.theme.fontFamily);
1457
- text.textContent = tick.label;
1458
- 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);
1459
1643
  }
1460
1644
  headerGroup.insertBefore(defs, headerGroup.firstChild);
1461
- this.svg.appendChild(headerGroup);
1645
+ this.headerSvg.appendChild(headerGroup);
1462
1646
  }
1463
1647
  /**
1464
1648
  * Invisible full-width hit-test rects, one per row, behind the bars. Lets
@@ -1493,18 +1677,13 @@ var GanttRenderer = class {
1493
1677
  g.setAttribute("class", "gantt-bar");
1494
1678
  g.dataset.ganttBar = bar.taskId;
1495
1679
  g.setAttribute("transform", `translate(${bar.x}, ${bar.y})`);
1496
- if (task?.notes) {
1497
- const title = svgEl("title");
1498
- title.textContent = task.notes;
1499
- g.appendChild(title);
1500
- }
1501
1680
  if (this.options.keyboardAccessible) {
1502
1681
  g.setAttribute("tabindex", "0");
1503
1682
  g.setAttribute("role", "button");
1504
1683
  if (task) {
1505
1684
  const progress = Math.round(task.progress ?? 0);
1506
- const label2 = `${task.name}, ${task.start.toDateString()} to ${task.end.toDateString()}, ${progress}% complete`;
1507
- 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);
1508
1687
  }
1509
1688
  }
1510
1689
  if (bar.baseline) {
@@ -1552,17 +1731,15 @@ var GanttRenderer = class {
1552
1731
  `rotate(45, 0, ${bar.height / 2})`
1553
1732
  );
1554
1733
  g.appendChild(diamond);
1555
- const label2 = svgEl("text");
1556
- label2.setAttribute("x", String(size / 2 + 8));
1557
- label2.setAttribute("y", String(bar.height / 2 + 4));
1558
- label2.style.fill = themedFill("textColor", model.theme.textColor);
1559
- label2.setAttribute("font-size", String(model.theme.fontSize));
1560
- label2.setAttribute("font-family", model.theme.fontFamily);
1561
- label2.textContent = bar.label;
1562
- g.appendChild(label2);
1563
- return g;
1564
- }
1565
- 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) {
1566
1743
  bar.segments.forEach((seg, i) => {
1567
1744
  const segRect = svgEl("rect");
1568
1745
  segRect.setAttribute("x", String(seg.x));
@@ -1635,29 +1812,31 @@ var GanttRenderer = class {
1635
1812
  selectionOutline.setAttribute("stroke-width", "2");
1636
1813
  g.appendChild(selectionOutline);
1637
1814
  }
1638
- const handleWidth = 6;
1639
- const leftHandle = svgEl("rect");
1640
- leftHandle.setAttribute("x", "0");
1641
- leftHandle.setAttribute("y", "0");
1642
- leftHandle.setAttribute("width", String(handleWidth));
1643
- leftHandle.setAttribute("height", String(bar.height));
1644
- leftHandle.setAttribute("fill", "transparent");
1645
- leftHandle.dataset.ganttHandle = "left";
1646
- leftHandle.dataset.ganttHandleFor = bar.taskId;
1647
- leftHandle.style.cursor = "ew-resize";
1648
- g.appendChild(leftHandle);
1649
- const rightHandle = svgEl("rect");
1650
- rightHandle.setAttribute("x", String(Math.max(0, bar.width - handleWidth)));
1651
- rightHandle.setAttribute("y", "0");
1652
- rightHandle.setAttribute("width", String(handleWidth));
1653
- rightHandle.setAttribute("height", String(bar.height));
1654
- rightHandle.setAttribute("fill", "transparent");
1655
- rightHandle.dataset.ganttHandle = "right";
1656
- rightHandle.dataset.ganttHandleFor = bar.taskId;
1657
- rightHandle.style.cursor = "ew-resize";
1658
- 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
+ }
1659
1838
  const connector = svgEl("circle");
1660
- connector.setAttribute("cx", String(bar.width));
1839
+ connector.setAttribute("cx", String(bar.width + (bar.isMilestone ? 10 : 0)));
1661
1840
  connector.setAttribute("cy", String(bar.height / 2));
1662
1841
  connector.setAttribute("r", "4");
1663
1842
  connector.style.fill = themedFill("linkColor", model.theme.linkColor);
@@ -1665,14 +1844,16 @@ var GanttRenderer = class {
1665
1844
  connector.dataset.ganttConnectorSide = "right";
1666
1845
  connector.style.cursor = "crosshair";
1667
1846
  g.appendChild(connector);
1668
- const label = svgEl("text");
1669
- label.setAttribute("x", String(bar.width + 6));
1670
- label.setAttribute("y", String(bar.height / 2 + 4));
1671
- label.style.fill = themedFill("textColor", model.theme.textColor);
1672
- label.setAttribute("font-size", String(model.theme.fontSize));
1673
- label.setAttribute("font-family", model.theme.fontFamily);
1674
- label.textContent = bar.label;
1675
- 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
+ }
1676
1857
  if (bar.deadlineX !== void 0) {
1677
1858
  const markerX = bar.deadlineX - bar.x;
1678
1859
  const flag = svgEl("g");
@@ -1730,10 +1911,20 @@ var GanttRenderer = class {
1730
1911
  for (const link of model.links) {
1731
1912
  const fromBar = barByTaskId.get(link.fromId);
1732
1913
  const toBar = barByTaskId.get(link.toId);
1733
- const relevantY = toBar?.y ?? fromBar?.y ?? 0;
1914
+ if (!fromBar || !toBar) continue;
1915
+ const relevantY = toBar.y ?? fromBar.y ?? 0;
1734
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}`;
1735
1926
  const path = svgEl("path");
1736
- path.setAttribute("d", link.path);
1927
+ path.setAttribute("d", d);
1737
1928
  path.setAttribute("fill", "none");
1738
1929
  path.style.stroke = link.isCritical ? themedFill("criticalColor", model.theme.criticalColor) : themedFill("linkColor", model.theme.linkColor);
1739
1930
  path.setAttribute("stroke-width", link.isCritical ? "2" : "1.5");
@@ -1742,8 +1933,30 @@ var GanttRenderer = class {
1742
1933
  group.appendChild(path);
1743
1934
  }
1744
1935
  }
1936
+ /**
1937
+ * The header and body are separate DOM elements at runtime (so plain CSS `position: sticky`
1938
+ * can pin the header - see the constructor's doc comment), but exports need one self-contained
1939
+ * SVG with both, laid out exactly as the original single-SVG version was.
1940
+ */
1745
1941
  toSVGString() {
1746
- return new XMLSerializer().serializeToString(this.svg);
1942
+ const headerHeight = this.lastModel?.headerHeight ?? 0;
1943
+ const width = this.lastModel?.width ?? 0;
1944
+ const bodyHeight = this.lastModel?.height ?? 0;
1945
+ const combined = svgEl("svg");
1946
+ combined.setAttribute("xmlns", SVG_NS);
1947
+ combined.setAttribute("width", String(width));
1948
+ combined.setAttribute("height", String(headerHeight + bodyHeight));
1949
+ combined.setAttribute("viewBox", `0 0 ${width} ${headerHeight + bodyHeight}`);
1950
+ for (const child of Array.from(this.headerSvg.children)) {
1951
+ combined.appendChild(child.cloneNode(true));
1952
+ }
1953
+ const bodyGroup = svgEl("g");
1954
+ bodyGroup.setAttribute("transform", `translate(0, ${headerHeight})`);
1955
+ for (const child of Array.from(this.svg.children)) {
1956
+ bodyGroup.appendChild(child.cloneNode(true));
1957
+ }
1958
+ combined.appendChild(bodyGroup);
1959
+ return new XMLSerializer().serializeToString(combined);
1747
1960
  }
1748
1961
  destroy() {
1749
1962
  if (this.scrollListener) {
@@ -2109,6 +2322,12 @@ var InteractionController = class {
2109
2322
  const toggleId = target.closest("[data-gantt-toggle]")?.dataset.ganttToggle;
2110
2323
  if (toggleId) {
2111
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;
2112
2331
  }
2113
2332
  };
2114
2333
  this.onContextMenu = (evt) => {
@@ -2122,13 +2341,17 @@ var InteractionController = class {
2122
2341
  }
2123
2342
  };
2124
2343
  this.onSvgDoubleClick = (evt) => {
2125
- if (!this.callbacks.onLinkDblClick) return;
2126
2344
  const target = evt.target;
2127
2345
  const linkPath2 = target.closest("[data-gantt-link]");
2128
2346
  const key = linkPath2?.dataset.ganttLink;
2129
- if (!key) return;
2130
- const [fromId, toId] = key.split("->");
2131
- if (fromId && toId) this.callbacks.onLinkDblClick(fromId, toId);
2347
+ if (key && this.callbacks.onLinkDblClick) {
2348
+ const [fromId, toId] = key.split("->");
2349
+ if (fromId && toId) this.callbacks.onLinkDblClick(fromId, toId);
2350
+ return;
2351
+ }
2352
+ const barGroup = target.closest("[data-gantt-bar]");
2353
+ const taskId = barGroup?.dataset.ganttBar;
2354
+ if (taskId && this.callbacks.onBarDblClick) this.callbacks.onBarDblClick(taskId);
2132
2355
  };
2133
2356
  this.onPointerDown = (evt) => {
2134
2357
  const target = evt.target;
@@ -2391,6 +2614,119 @@ var InteractionController = class {
2391
2614
  }
2392
2615
  };
2393
2616
 
2617
+ // src/tooltip.ts
2618
+ function formatDate(d) {
2619
+ return d.toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" });
2620
+ }
2621
+ function defaultTooltipContent(task) {
2622
+ const el2 = document.createElement("div");
2623
+ const title = document.createElement("div");
2624
+ title.className = "gantt-tooltip-title";
2625
+ title.textContent = task.name;
2626
+ el2.appendChild(title);
2627
+ const dates = document.createElement("div");
2628
+ dates.className = "gantt-tooltip-row";
2629
+ dates.textContent = task.isMilestone ? formatDate(task.start) : `${formatDate(task.start)} \u2192 ${formatDate(task.end)}`;
2630
+ el2.appendChild(dates);
2631
+ if (task.progress !== void 0) {
2632
+ const progress = document.createElement("div");
2633
+ progress.className = "gantt-tooltip-row";
2634
+ progress.textContent = `${Math.round(task.progress)}% complete`;
2635
+ el2.appendChild(progress);
2636
+ }
2637
+ if (task.assignees && task.assignees.length > 0) {
2638
+ const assignees = document.createElement("div");
2639
+ assignees.className = "gantt-tooltip-row";
2640
+ assignees.textContent = task.assignees.map((a) => a.name).join(", ");
2641
+ el2.appendChild(assignees);
2642
+ }
2643
+ if (task.notes) {
2644
+ const notes = document.createElement("div");
2645
+ notes.className = "gantt-tooltip-notes";
2646
+ notes.textContent = task.notes;
2647
+ el2.appendChild(notes);
2648
+ }
2649
+ return el2;
2650
+ }
2651
+ var Tooltip = class {
2652
+ constructor(getOptions, getTask) {
2653
+ this.svg = null;
2654
+ this.hoveredTaskId = null;
2655
+ this.onPointerOver = (evt) => {
2656
+ if (!this.getOptions().enabled) return;
2657
+ const target = evt.target;
2658
+ const barGroup = target.closest("[data-gantt-bar]");
2659
+ const taskId = barGroup?.dataset.ganttBar;
2660
+ if (!taskId || taskId === this.hoveredTaskId) return;
2661
+ const task = this.getTask(taskId);
2662
+ if (!task) return;
2663
+ const { render } = this.getOptions();
2664
+ const content = render ? render(task) : defaultTooltipContent(task);
2665
+ if (content === null || content === void 0) {
2666
+ this.hide();
2667
+ return;
2668
+ }
2669
+ this.el.replaceChildren();
2670
+ if (content instanceof HTMLElement) {
2671
+ this.el.appendChild(content);
2672
+ } else {
2673
+ this.el.textContent = content;
2674
+ }
2675
+ this.hoveredTaskId = taskId;
2676
+ this.el.style.display = "block";
2677
+ this.position(evt.clientX, evt.clientY);
2678
+ };
2679
+ this.onPointerMove = (evt) => {
2680
+ if (this.hoveredTaskId === null) return;
2681
+ this.position(evt.clientX, evt.clientY);
2682
+ };
2683
+ this.onPointerOut = (evt) => {
2684
+ const related = evt.relatedTarget;
2685
+ const target = evt.target;
2686
+ const leftBarGroup = target.closest("[data-gantt-bar]");
2687
+ if (related && leftBarGroup?.contains(related)) return;
2688
+ this.hide();
2689
+ };
2690
+ this.getOptions = getOptions;
2691
+ this.getTask = getTask;
2692
+ this.el = document.createElement("div");
2693
+ this.el.className = "gantt-tooltip";
2694
+ this.el.setAttribute("role", "tooltip");
2695
+ this.el.style.display = "none";
2696
+ document.body.appendChild(this.el);
2697
+ }
2698
+ attach(svg) {
2699
+ this.svg = svg;
2700
+ svg.addEventListener("pointerover", this.onPointerOver);
2701
+ svg.addEventListener("pointermove", this.onPointerMove);
2702
+ svg.addEventListener("pointerout", this.onPointerOut);
2703
+ }
2704
+ position(clientX, clientY) {
2705
+ const OFFSET = 12;
2706
+ const rect = this.el.getBoundingClientRect();
2707
+ const viewportW = window.innerWidth;
2708
+ const viewportH = window.innerHeight;
2709
+ let left = clientX + OFFSET;
2710
+ let top = clientY + OFFSET;
2711
+ if (left + rect.width > viewportW) left = clientX - OFFSET - rect.width;
2712
+ if (top + rect.height > viewportH) top = clientY - OFFSET - rect.height;
2713
+ this.el.style.left = `${Math.max(0, left)}px`;
2714
+ this.el.style.top = `${Math.max(0, top)}px`;
2715
+ }
2716
+ hide() {
2717
+ this.hoveredTaskId = null;
2718
+ this.el.style.display = "none";
2719
+ }
2720
+ destroy() {
2721
+ if (this.svg) {
2722
+ this.svg.removeEventListener("pointerover", this.onPointerOver);
2723
+ this.svg.removeEventListener("pointermove", this.onPointerMove);
2724
+ this.svg.removeEventListener("pointerout", this.onPointerOut);
2725
+ }
2726
+ this.el.remove();
2727
+ }
2728
+ };
2729
+
2394
2730
  // src/index.ts
2395
2731
  var HAS_DOM = typeof document !== "undefined" && typeof window !== "undefined";
2396
2732
  var DEFAULT_COLUMNS = [{ id: "name", title: "Name" }];
@@ -2410,6 +2746,9 @@ var _GanttChart = class _GanttChart {
2410
2746
  this.history = null;
2411
2747
  this.renderer = null;
2412
2748
  this.interactions = null;
2749
+ this.tooltip = null;
2750
+ this.resizeObserver = null;
2751
+ this.sort = null;
2413
2752
  this.renderModel = null;
2414
2753
  this.rafHandle = null;
2415
2754
  this.currentPage = 1;
@@ -2432,6 +2771,8 @@ var _GanttChart = class _GanttChart {
2432
2771
  showCriticalPath: options.showCriticalPath ?? false,
2433
2772
  showBaseline: options.showBaseline ?? false,
2434
2773
  showDeadlines: options.showDeadlines ?? true,
2774
+ showTooltip: options.showTooltip ?? true,
2775
+ autoFitToViewport: options.autoFitToViewport ?? false,
2435
2776
  showAssigneeAvatars: options.showAssigneeAvatars ?? false,
2436
2777
  enableHistory: options.enableHistory ?? false,
2437
2778
  keyboardAccessible: options.keyboardAccessible ?? false,
@@ -2446,19 +2787,23 @@ var _GanttChart = class _GanttChart {
2446
2787
  markers: options.markers,
2447
2788
  pagination: options.pagination,
2448
2789
  snapToUnit: options.snapToUnit,
2790
+ headerPosition: options.headerPosition ?? "top",
2449
2791
  onDateChange: options.onDateChange,
2450
2792
  onProgressChange: options.onProgressChange,
2451
2793
  onDependencyCreate: options.onDependencyCreate,
2452
2794
  onDependencyRemove: options.onDependencyRemove,
2453
2795
  onDependencyDblClick: options.onDependencyDblClick,
2454
2796
  onTaskClick: options.onTaskClick,
2797
+ onTaskDblClick: options.onTaskDblClick,
2455
2798
  onGroupToggle: options.onGroupToggle,
2456
2799
  onContextMenu: options.onContextMenu,
2457
2800
  onTaskCreate: options.onTaskCreate,
2458
2801
  onColumnResize: options.onColumnResize,
2459
2802
  onColumnReorder: options.onColumnReorder,
2460
2803
  onTaskReorder: options.onTaskReorder,
2461
- onSelectionChange: options.onSelectionChange
2804
+ onSelectionChange: options.onSelectionChange,
2805
+ onSortChange: options.onSortChange,
2806
+ renderTooltip: options.renderTooltip
2462
2807
  };
2463
2808
  if (this.options.enableHistory) {
2464
2809
  this.history = new HistoryManager((state) => this.emitter.emit("history-change", state));
@@ -2475,14 +2820,23 @@ var _GanttChart = class _GanttChart {
2475
2820
  this.darkMediaQuery.addEventListener?.("change", this.handleSchemeChange);
2476
2821
  }
2477
2822
  this.renderer = new GanttRenderer(this.container, {
2823
+ readonly: this.options.readonly,
2478
2824
  keyboardAccessible: this.options.keyboardAccessible,
2479
2825
  showAssigneeAvatars: this.options.showAssigneeAvatars,
2480
2826
  virtualScroll: this.options.virtualScroll,
2481
2827
  onRenameCommit: (taskId, name) => this.updateTask(taskId, { name }),
2482
2828
  onColumnResize: this.options.onColumnResize ? (columnId, width) => this.handleColumnResize(columnId, width) : void 0,
2483
2829
  onColumnReorder: this.options.onColumnReorder ? (order) => this.handleColumnReorder(order) : void 0,
2484
- onRowReorder: this.options.onTaskReorder ? (draggedId, targetId, position) => this.handleRowReorder(draggedId, targetId, position) : void 0
2830
+ onRowReorder: this.options.onTaskReorder ? (draggedId, targetId, position) => this.handleRowReorder(draggedId, targetId, position) : void 0,
2831
+ onSortClick: (columnId) => this.handleSortClick(columnId)
2485
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
+ }
2486
2840
  this.interactions = new InteractionController(
2487
2841
  this.renderer.svg,
2488
2842
  this.renderer.gridPanel,
@@ -2502,7 +2856,17 @@ var _GanttChart = class _GanttChart {
2502
2856
  onCreateTaskDrag: this.options.onTaskCreate ? (rowTaskId, startXPx, endXPx) => this.handleCreateTaskDrag(rowTaskId, startXPx, endXPx) : void 0,
2503
2857
  onLinkDblClick: this.options.onDependencyDblClick ? (fromId, toId) => {
2504
2858
  const dep = this.dependencies.find((d) => d.fromId === fromId && d.toId === toId);
2505
- if (dep) this.options.onDependencyDblClick?.(dep);
2859
+ if (dep) {
2860
+ this.emitter.emit("dependency-dblclick", dep);
2861
+ this.options.onDependencyDblClick?.(dep);
2862
+ }
2863
+ } : void 0,
2864
+ onBarDblClick: this.options.onTaskDblClick ? (taskId) => {
2865
+ const task = this.tasks.find((t) => t.id === taskId);
2866
+ if (task) {
2867
+ this.emitter.emit("task-dblclick", { task });
2868
+ this.options.onTaskDblClick?.(task);
2869
+ }
2506
2870
  } : void 0,
2507
2871
  onContextMenu: this.options.onContextMenu ? (taskId, evt) => {
2508
2872
  const task = this.tasks.find((t) => t.id === taskId);
@@ -2511,7 +2875,8 @@ var _GanttChart = class _GanttChart {
2511
2875
  } : void 0,
2512
2876
  onSelectAll: this.options.selectable ? () => this.selectAllVisible() : void 0,
2513
2877
  onJumpToStart: () => this.scrollToRangeStart(),
2514
- onJumpToEnd: () => this.scrollToRangeEnd()
2878
+ onJumpToEnd: () => this.scrollToRangeEnd(),
2879
+ onAddChild: (parentId) => this.handleAddChild(parentId)
2515
2880
  },
2516
2881
  {
2517
2882
  readonly: this.options.readonly,
@@ -2521,6 +2886,17 @@ var _GanttChart = class _GanttChart {
2521
2886
  pxPerMs: () => pxPerMs(this.options.viewMode, this.columnWidth)
2522
2887
  }
2523
2888
  );
2889
+ this.tooltip = new Tooltip(
2890
+ () => ({ enabled: this.options.showTooltip, render: this.options.renderTooltip }),
2891
+ (taskId) => this.tasks.find((t) => t.id === taskId)
2892
+ );
2893
+ this.tooltip.attach(this.renderer.svg);
2894
+ if (typeof ResizeObserver !== "undefined") {
2895
+ this.resizeObserver = new ResizeObserver(() => {
2896
+ if (this.options.autoFitToViewport) this.scheduleRender();
2897
+ });
2898
+ this.resizeObserver.observe(this.container);
2899
+ }
2524
2900
  }
2525
2901
  handleMove(taskId, dxMs) {
2526
2902
  if (dxMs === 0) return;
@@ -2790,6 +3166,35 @@ var _GanttChart = class _GanttChart {
2790
3166
  };
2791
3167
  this.runCommand(cmd);
2792
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
+ }
2793
3198
  handleBarClick(taskId, modifiers) {
2794
3199
  const task = this.tasks.find((t) => t.id === taskId);
2795
3200
  if (!task) return;
@@ -2925,6 +3330,24 @@ var _GanttChart = class _GanttChart {
2925
3330
  this.options.onColumnReorder?.(order);
2926
3331
  this.scheduleRender();
2927
3332
  }
3333
+ /** Cycles a sortable column's header through asc -> desc -> none; clicking a different column starts it fresh at asc. */
3334
+ handleSortClick(columnId) {
3335
+ if (this.sort?.columnId === columnId) {
3336
+ this.setSort(columnId, this.sort.direction === "asc" ? "desc" : null);
3337
+ } else {
3338
+ this.setSort(columnId, "asc");
3339
+ }
3340
+ }
3341
+ /** Sorts siblings at every tree level by a column's value (see GanttColumn.sortable); `direction: null`/omitted `columnId` clears it. */
3342
+ setSort(columnId, direction = "asc") {
3343
+ this.sort = columnId && direction ? { columnId, direction } : null;
3344
+ this.emitter.emit("sort-change", { sort: this.sort });
3345
+ this.options.onSortChange?.(this.sort);
3346
+ this.scheduleRender();
3347
+ }
3348
+ getSort() {
3349
+ return this.sort;
3350
+ }
2928
3351
  handleRowReorder(draggedId, targetId, position) {
2929
3352
  const draggedIndex = this.tasks.findIndex((t) => t.id === draggedId);
2930
3353
  const targetIndex = this.tasks.findIndex((t) => t.id === targetId);
@@ -2991,13 +3414,24 @@ var _GanttChart = class _GanttChart {
2991
3414
  this.scheduleRender();
2992
3415
  }
2993
3416
  setOptions(partial) {
2994
- if (partial.colorScheme !== void 0) this.colorScheme = partial.colorScheme;
2995
- if (partial.theme !== void 0) this.explicitTheme = partial.theme;
2996
- if (partial.theme !== void 0 || partial.colorScheme !== void 0) {
3417
+ const hasTheme = "theme" in partial;
3418
+ const hasColorScheme = "colorScheme" in partial;
3419
+ if (hasColorScheme) this.colorScheme = partial.colorScheme ?? "auto";
3420
+ if (hasTheme) this.explicitTheme = partial.theme;
3421
+ if (hasTheme || hasColorScheme) {
2997
3422
  this.theme = mergeTheme(this.explicitTheme, this.colorScheme);
2998
3423
  }
2999
- if (partial.columns) this.columns = partial.columns;
3424
+ if ("columns" in partial) this.columns = partial.columns ?? this.columns;
3000
3425
  if (partial.columnWidth !== void 0) this.columnWidth = partial.columnWidth;
3426
+ if (partial.gridPanelWidth !== void 0) {
3427
+ this.container.style.setProperty("--gantt-grid-panel-width", `${partial.gridPanelWidth}px`);
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
+ }
3001
3435
  Object.assign(this.options, partial);
3002
3436
  this.scheduleRender();
3003
3437
  }
@@ -3085,9 +3519,11 @@ var _GanttChart = class _GanttChart {
3085
3519
  }
3086
3520
  computeModel() {
3087
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;
3088
3523
  return computeLayout({
3089
3524
  tasks: this.tasks,
3090
3525
  dependencies: this.dependencies,
3526
+ timelineViewportWidth,
3091
3527
  viewMode: this.options.viewMode,
3092
3528
  columnWidth: this.columnWidth,
3093
3529
  theme: this.theme,
@@ -3100,7 +3536,8 @@ var _GanttChart = class _GanttChart {
3100
3536
  markers: this.options.markers,
3101
3537
  autoRollupProgress: this.options.autoRollupProgress,
3102
3538
  selectedTaskIds: this.options.selectable ? this.selectedTaskIds : void 0,
3103
- pagination
3539
+ pagination,
3540
+ sort: this.sort
3104
3541
  });
3105
3542
  }
3106
3543
  scheduleRender(immediate = false) {
@@ -3115,6 +3552,7 @@ var _GanttChart = class _GanttChart {
3115
3552
  });
3116
3553
  }
3117
3554
  doRender() {
3555
+ if (this.options.autoFitToViewport) this.applyAutoFit();
3118
3556
  this.renderModel = this.computeModel();
3119
3557
  this.renderer?.render(this.renderModel, this.tasks, this.columns, this.explicitTheme);
3120
3558
  if (this.renderer && this.colorScheme !== "auto") {
@@ -3123,6 +3561,29 @@ var _GanttChart = class _GanttChart {
3123
3561
  this.renderer?.root.removeAttribute("data-gantt-theme");
3124
3562
  }
3125
3563
  }
3564
+ /**
3565
+ * Stretch-only fit: bump columnWidth up so the timeline fills the container's available
3566
+ * width, but never shrink it below what the current zoom level already implies - a project
3567
+ * wider than the container should still scroll normally, not get crammed to fit.
3568
+ */
3569
+ applyAutoFit() {
3570
+ const availableWidth = this.container.clientWidth - this.options.gridPanelWidth;
3571
+ if (!Number.isFinite(availableWidth) || availableWidth <= 0) return;
3572
+ let min = null;
3573
+ let max = null;
3574
+ for (const t of this.tasks) {
3575
+ if (!isValidDate(t.start) || !isValidDate(t.end)) continue;
3576
+ min = min === null ? t.start.getTime() : Math.min(min, t.start.getTime());
3577
+ max = max === null ? t.end.getTime() : Math.max(max, t.end.getTime());
3578
+ }
3579
+ if (min === null || max === null || max <= min) return;
3580
+ const rangeMs = max - min;
3581
+ const unitMs = approxUnitMs(this.options.viewMode);
3582
+ const neededColumnWidth = availableWidth * unitMs / rangeMs;
3583
+ if (neededColumnWidth > this.columnWidth) {
3584
+ this.columnWidth = Math.min(neededColumnWidth, _GanttChart.MAX_COLUMN_WIDTH);
3585
+ }
3586
+ }
3126
3587
  undo() {
3127
3588
  this.history?.undo();
3128
3589
  }
@@ -3145,7 +3606,35 @@ var _GanttChart = class _GanttChart {
3145
3606
  return this.renderer?.toSVGString() ?? "";
3146
3607
  }
3147
3608
  async rasterizeSVG(svgString, width, height) {
3148
- 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" });
3149
3638
  const url = URL.createObjectURL(svgBlob);
3150
3639
  try {
3151
3640
  return await new Promise((resolve, reject) => {
@@ -3217,6 +3706,8 @@ var _GanttChart = class _GanttChart {
3217
3706
  this.darkMediaQuery?.removeEventListener?.("change", this.handleSchemeChange);
3218
3707
  this.emitter.removeAllListeners();
3219
3708
  this.interactions?.destroy();
3709
+ this.tooltip?.destroy();
3710
+ this.resizeObserver?.disconnect();
3220
3711
  this.renderer?.destroy();
3221
3712
  }
3222
3713
  };