@ganttloom/gantt-core 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -614,6 +614,29 @@ function computeProgressRollup(node) {
614
614
  }
615
615
  node.computedProgress = totalWeight > 0 ? weightedSum / totalWeight : 0;
616
616
  }
617
+ function sortValue(node, col) {
618
+ if (!col) return null;
619
+ if (col.accessor) return col.accessor(node.task) ?? null;
620
+ return col.id === "name" ? node.task.name : null;
621
+ }
622
+ function compareValues(a, b) {
623
+ if (a === null && b === null) return 0;
624
+ if (a === null) return 1;
625
+ if (b === null) return -1;
626
+ if (typeof a === "number" && typeof b === "number") return a - b;
627
+ return String(a).localeCompare(String(b), void 0, { numeric: true, sensitivity: "base" });
628
+ }
629
+ function sortTree(roots, columns, sort) {
630
+ if (!sort) return;
631
+ const col = columns.find((c) => c.id === sort.columnId);
632
+ const dir = sort.direction === "desc" ? -1 : 1;
633
+ const compare = (a, b) => dir * compareValues(sortValue(a, col), sortValue(b, col));
634
+ const sortLevel = (nodes) => {
635
+ nodes.sort(compare);
636
+ for (const node of nodes) sortLevel(node.children);
637
+ };
638
+ sortLevel(roots);
639
+ }
617
640
  function flatten(roots) {
618
641
  const out = [];
619
642
  const stack = [];
@@ -700,6 +723,14 @@ function linkPath(from, to) {
700
723
  const c2y = to.y;
701
724
  return `M ${from.x} ${from.y} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${to.x} ${to.y}`;
702
725
  }
726
+ var MIN_VISIBLE_UNITS = {
727
+ hour: 8,
728
+ day: 7,
729
+ week: 6,
730
+ month: 6,
731
+ quarter: 4,
732
+ year: 5
733
+ };
703
734
  function computeLayout(input) {
704
735
  const {
705
736
  tasks,
@@ -715,7 +746,8 @@ function computeLayout(input) {
715
746
  markers = [],
716
747
  autoRollupProgress = false,
717
748
  selectedTaskIds,
718
- pagination
749
+ pagination,
750
+ sort = null
719
751
  } = input;
720
752
  validateInputs(tasks, dependencies);
721
753
  const { roots, nodeById } = buildTree(tasks);
@@ -723,6 +755,7 @@ function computeLayout(input) {
723
755
  if (autoRollupProgress) {
724
756
  for (const root of roots) computeProgressRollup(root);
725
757
  }
758
+ sortTree(roots, columns, sort);
726
759
  let visibleRoots = roots;
727
760
  if (pagination && pagination.pageSize > 0) {
728
761
  const start = Math.max(0, (pagination.page - 1) * pagination.pageSize);
@@ -742,8 +775,15 @@ function computeLayout(input) {
742
775
  const now = Date.now();
743
776
  const rawRangeStart = rangeMin === null ? new Date(now) : new Date(rangeMin);
744
777
  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);
778
+ let paddedStart = addUnit(rawRangeStart, viewMode, -1);
779
+ let paddedEnd = addUnit(rawRangeEnd, viewMode, 1);
780
+ const minUnits = MIN_VISIBLE_UNITS[viewMode];
781
+ 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));
786
+ }
747
787
  const rows = [];
748
788
  const bars = [];
749
789
  const barByTaskId = /* @__PURE__ */ new Map();
@@ -894,7 +934,8 @@ function computeLayout(input) {
894
934
  columns,
895
935
  theme,
896
936
  markers: renderMarkers,
897
- rangeStart: paddedStart
937
+ rangeStart: paddedStart,
938
+ sort
898
939
  };
899
940
  }
900
941
 
@@ -1091,6 +1132,7 @@ var GanttRenderer = class {
1091
1132
  this.lastModel = null;
1092
1133
  this.lastTasks = [];
1093
1134
  this.lastColumns = [];
1135
+ this.lastExplicitThemeKeys = /* @__PURE__ */ new Set();
1094
1136
  this.onGridDoubleClick = (evt) => {
1095
1137
  if (!this.options.onRenameCommit) return;
1096
1138
  const target = evt.target;
@@ -1128,8 +1170,13 @@ var GanttRenderer = class {
1128
1170
  this.root = el("div", "gantt-root");
1129
1171
  this.gridPanel = el("div", "gantt-grid-panel");
1130
1172
  this.timelineScroll = el("div", "gantt-timeline-scroll");
1173
+ this.headerContainer = el("div", "gantt-timeline-header");
1174
+ this.headerSvg = svgEl("svg");
1175
+ this.headerSvg.classList.add("gantt-header-svg");
1176
+ this.headerContainer.appendChild(this.headerSvg);
1131
1177
  this.svg = svgEl("svg");
1132
1178
  this.svg.classList.add("gantt-svg");
1179
+ this.timelineScroll.appendChild(this.headerContainer);
1133
1180
  this.timelineScroll.appendChild(this.svg);
1134
1181
  this.root.appendChild(this.gridPanel);
1135
1182
  this.root.appendChild(this.timelineScroll);
@@ -1145,9 +1192,14 @@ var GanttRenderer = class {
1145
1192
  this.lastTasks = tasks;
1146
1193
  this.lastColumns = columns;
1147
1194
  const cssVars = explicitThemeToCssVars(explicitTheme);
1148
- for (const key of Object.keys(cssVars)) {
1195
+ const newKeys = new Set(Object.keys(cssVars));
1196
+ for (const key of this.lastExplicitThemeKeys) {
1197
+ if (!newKeys.has(key)) this.root.style.removeProperty(key);
1198
+ }
1199
+ for (const key of newKeys) {
1149
1200
  this.root.style.setProperty(key, cssVars[key]);
1150
1201
  }
1202
+ this.lastExplicitThemeKeys = newKeys;
1151
1203
  this.renderGridPanel(model, tasks, columns);
1152
1204
  this.renderTimeline(model, tasks);
1153
1205
  }
@@ -1175,7 +1227,7 @@ var GanttRenderer = class {
1175
1227
  const header = el("div", "gantt-grid-header-row");
1176
1228
  header.style.height = `${model.headerHeight}px`;
1177
1229
  for (const col of columns) {
1178
- header.appendChild(this.renderHeaderCell(col, columns));
1230
+ header.appendChild(this.renderHeaderCell(col, columns, model.sort));
1179
1231
  }
1180
1232
  this.gridPanel.appendChild(header);
1181
1233
  const body = el("div", "gantt-grid-body");
@@ -1188,11 +1240,23 @@ var GanttRenderer = class {
1188
1240
  });
1189
1241
  this.gridPanel.appendChild(body);
1190
1242
  }
1191
- renderHeaderCell(col, columns) {
1243
+ renderHeaderCell(col, columns, sort) {
1192
1244
  const cell = el("div", "gantt-grid-header-cell");
1193
1245
  cell.style.position = "relative";
1194
1246
  if (col.width) cell.style.width = `${col.width}px`;
1195
1247
  cell.textContent = col.title;
1248
+ if (col.sortable) {
1249
+ cell.classList.add("gantt-grid-header-cell-sortable");
1250
+ const active = sort?.columnId === col.id;
1251
+ const indicator = el("span", "gantt-sort-indicator");
1252
+ indicator.textContent = active ? sort.direction === "asc" ? " \u25B2" : " \u25BC" : "";
1253
+ cell.appendChild(indicator);
1254
+ cell.addEventListener("click", (evt) => {
1255
+ const target = evt.target;
1256
+ if (target.closest(".gantt-col-resize-handle")) return;
1257
+ this.options.onSortClick?.(col.id);
1258
+ });
1259
+ }
1196
1260
  if (this.options.onColumnReorder) {
1197
1261
  cell.draggable = true;
1198
1262
  cell.style.cursor = "grab";
@@ -1319,8 +1383,8 @@ var GanttRenderer = class {
1319
1383
  renderTimeline(model, tasks) {
1320
1384
  this.svg.replaceChildren();
1321
1385
  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}`);
1386
+ this.svg.setAttribute("height", String(model.height));
1387
+ this.svg.setAttribute("viewBox", `0 0 ${model.width} ${model.height}`);
1324
1388
  const defs = svgEl("defs");
1325
1389
  const marker = svgEl("marker");
1326
1390
  marker.setAttribute("id", this.markerDefsId);
@@ -1337,7 +1401,6 @@ var GanttRenderer = class {
1337
1401
  this.svg.appendChild(defs);
1338
1402
  const { startY, endY } = this.getViewportRowRange(model);
1339
1403
  const bodyGroup = svgEl("g");
1340
- bodyGroup.setAttribute("transform", `translate(0, ${model.headerHeight})`);
1341
1404
  this.renderGridLines(bodyGroup, model);
1342
1405
  this.renderRowBackgrounds(bodyGroup, model, startY, endY);
1343
1406
  this.renderBars(bodyGroup, model, tasks, startY, endY);
@@ -1421,9 +1484,12 @@ var GanttRenderer = class {
1421
1484
  });
1422
1485
  }
1423
1486
  renderHeader(model) {
1424
- let headerGroup = this.svg.querySelector(".gantt-header-group");
1425
- if (headerGroup) headerGroup.remove();
1426
- headerGroup = svgEl("g");
1487
+ this.headerSvg.replaceChildren();
1488
+ this.headerSvg.setAttribute("width", String(model.width));
1489
+ this.headerSvg.setAttribute("height", String(model.headerHeight));
1490
+ this.headerSvg.setAttribute("viewBox", `0 0 ${model.width} ${model.headerHeight}`);
1491
+ this.headerContainer.style.height = `${model.headerHeight}px`;
1492
+ const headerGroup = svgEl("g");
1427
1493
  headerGroup.setAttribute("class", "gantt-header-group");
1428
1494
  const bg = svgEl("rect");
1429
1495
  bg.setAttribute("x", "0");
@@ -1458,7 +1524,7 @@ var GanttRenderer = class {
1458
1524
  headerGroup.appendChild(text);
1459
1525
  }
1460
1526
  headerGroup.insertBefore(defs, headerGroup.firstChild);
1461
- this.svg.appendChild(headerGroup);
1527
+ this.headerSvg.appendChild(headerGroup);
1462
1528
  }
1463
1529
  /**
1464
1530
  * Invisible full-width hit-test rects, one per row, behind the bars. Lets
@@ -1493,11 +1559,6 @@ var GanttRenderer = class {
1493
1559
  g.setAttribute("class", "gantt-bar");
1494
1560
  g.dataset.ganttBar = bar.taskId;
1495
1561
  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
1562
  if (this.options.keyboardAccessible) {
1502
1563
  g.setAttribute("tabindex", "0");
1503
1564
  g.setAttribute("role", "button");
@@ -1742,8 +1803,30 @@ var GanttRenderer = class {
1742
1803
  group.appendChild(path);
1743
1804
  }
1744
1805
  }
1806
+ /**
1807
+ * The header and body are separate DOM elements at runtime (so plain CSS `position: sticky`
1808
+ * can pin the header - see the constructor's doc comment), but exports need one self-contained
1809
+ * SVG with both, laid out exactly as the original single-SVG version was.
1810
+ */
1745
1811
  toSVGString() {
1746
- return new XMLSerializer().serializeToString(this.svg);
1812
+ const headerHeight = this.lastModel?.headerHeight ?? 0;
1813
+ const width = this.lastModel?.width ?? 0;
1814
+ const bodyHeight = this.lastModel?.height ?? 0;
1815
+ const combined = svgEl("svg");
1816
+ combined.setAttribute("xmlns", SVG_NS);
1817
+ combined.setAttribute("width", String(width));
1818
+ combined.setAttribute("height", String(headerHeight + bodyHeight));
1819
+ combined.setAttribute("viewBox", `0 0 ${width} ${headerHeight + bodyHeight}`);
1820
+ for (const child of Array.from(this.headerSvg.children)) {
1821
+ combined.appendChild(child.cloneNode(true));
1822
+ }
1823
+ const bodyGroup = svgEl("g");
1824
+ bodyGroup.setAttribute("transform", `translate(0, ${headerHeight})`);
1825
+ for (const child of Array.from(this.svg.children)) {
1826
+ bodyGroup.appendChild(child.cloneNode(true));
1827
+ }
1828
+ combined.appendChild(bodyGroup);
1829
+ return new XMLSerializer().serializeToString(combined);
1747
1830
  }
1748
1831
  destroy() {
1749
1832
  if (this.scrollListener) {
@@ -2122,13 +2205,17 @@ var InteractionController = class {
2122
2205
  }
2123
2206
  };
2124
2207
  this.onSvgDoubleClick = (evt) => {
2125
- if (!this.callbacks.onLinkDblClick) return;
2126
2208
  const target = evt.target;
2127
2209
  const linkPath2 = target.closest("[data-gantt-link]");
2128
2210
  const key = linkPath2?.dataset.ganttLink;
2129
- if (!key) return;
2130
- const [fromId, toId] = key.split("->");
2131
- if (fromId && toId) this.callbacks.onLinkDblClick(fromId, toId);
2211
+ if (key && this.callbacks.onLinkDblClick) {
2212
+ const [fromId, toId] = key.split("->");
2213
+ if (fromId && toId) this.callbacks.onLinkDblClick(fromId, toId);
2214
+ return;
2215
+ }
2216
+ const barGroup = target.closest("[data-gantt-bar]");
2217
+ const taskId = barGroup?.dataset.ganttBar;
2218
+ if (taskId && this.callbacks.onBarDblClick) this.callbacks.onBarDblClick(taskId);
2132
2219
  };
2133
2220
  this.onPointerDown = (evt) => {
2134
2221
  const target = evt.target;
@@ -2391,6 +2478,119 @@ var InteractionController = class {
2391
2478
  }
2392
2479
  };
2393
2480
 
2481
+ // src/tooltip.ts
2482
+ function formatDate(d) {
2483
+ return d.toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" });
2484
+ }
2485
+ function defaultTooltipContent(task) {
2486
+ const el2 = document.createElement("div");
2487
+ const title = document.createElement("div");
2488
+ title.className = "gantt-tooltip-title";
2489
+ title.textContent = task.name;
2490
+ el2.appendChild(title);
2491
+ const dates = document.createElement("div");
2492
+ dates.className = "gantt-tooltip-row";
2493
+ dates.textContent = task.isMilestone ? formatDate(task.start) : `${formatDate(task.start)} \u2192 ${formatDate(task.end)}`;
2494
+ el2.appendChild(dates);
2495
+ if (task.progress !== void 0) {
2496
+ const progress = document.createElement("div");
2497
+ progress.className = "gantt-tooltip-row";
2498
+ progress.textContent = `${Math.round(task.progress)}% complete`;
2499
+ el2.appendChild(progress);
2500
+ }
2501
+ if (task.assignees && task.assignees.length > 0) {
2502
+ const assignees = document.createElement("div");
2503
+ assignees.className = "gantt-tooltip-row";
2504
+ assignees.textContent = task.assignees.map((a) => a.name).join(", ");
2505
+ el2.appendChild(assignees);
2506
+ }
2507
+ if (task.notes) {
2508
+ const notes = document.createElement("div");
2509
+ notes.className = "gantt-tooltip-notes";
2510
+ notes.textContent = task.notes;
2511
+ el2.appendChild(notes);
2512
+ }
2513
+ return el2;
2514
+ }
2515
+ var Tooltip = class {
2516
+ constructor(getOptions, getTask) {
2517
+ this.svg = null;
2518
+ this.hoveredTaskId = null;
2519
+ this.onPointerOver = (evt) => {
2520
+ if (!this.getOptions().enabled) return;
2521
+ const target = evt.target;
2522
+ const barGroup = target.closest("[data-gantt-bar]");
2523
+ const taskId = barGroup?.dataset.ganttBar;
2524
+ if (!taskId || taskId === this.hoveredTaskId) return;
2525
+ const task = this.getTask(taskId);
2526
+ if (!task) return;
2527
+ const { render } = this.getOptions();
2528
+ const content = render ? render(task) : defaultTooltipContent(task);
2529
+ if (content === null || content === void 0) {
2530
+ this.hide();
2531
+ return;
2532
+ }
2533
+ this.el.replaceChildren();
2534
+ if (content instanceof HTMLElement) {
2535
+ this.el.appendChild(content);
2536
+ } else {
2537
+ this.el.textContent = content;
2538
+ }
2539
+ this.hoveredTaskId = taskId;
2540
+ this.el.style.display = "block";
2541
+ this.position(evt.clientX, evt.clientY);
2542
+ };
2543
+ this.onPointerMove = (evt) => {
2544
+ if (this.hoveredTaskId === null) return;
2545
+ this.position(evt.clientX, evt.clientY);
2546
+ };
2547
+ this.onPointerOut = (evt) => {
2548
+ const related = evt.relatedTarget;
2549
+ const target = evt.target;
2550
+ const leftBarGroup = target.closest("[data-gantt-bar]");
2551
+ if (related && leftBarGroup?.contains(related)) return;
2552
+ this.hide();
2553
+ };
2554
+ this.getOptions = getOptions;
2555
+ this.getTask = getTask;
2556
+ this.el = document.createElement("div");
2557
+ this.el.className = "gantt-tooltip";
2558
+ this.el.setAttribute("role", "tooltip");
2559
+ this.el.style.display = "none";
2560
+ document.body.appendChild(this.el);
2561
+ }
2562
+ attach(svg) {
2563
+ this.svg = svg;
2564
+ svg.addEventListener("pointerover", this.onPointerOver);
2565
+ svg.addEventListener("pointermove", this.onPointerMove);
2566
+ svg.addEventListener("pointerout", this.onPointerOut);
2567
+ }
2568
+ position(clientX, clientY) {
2569
+ const OFFSET = 12;
2570
+ const rect = this.el.getBoundingClientRect();
2571
+ const viewportW = window.innerWidth;
2572
+ const viewportH = window.innerHeight;
2573
+ let left = clientX + OFFSET;
2574
+ let top = clientY + OFFSET;
2575
+ if (left + rect.width > viewportW) left = clientX - OFFSET - rect.width;
2576
+ if (top + rect.height > viewportH) top = clientY - OFFSET - rect.height;
2577
+ this.el.style.left = `${Math.max(0, left)}px`;
2578
+ this.el.style.top = `${Math.max(0, top)}px`;
2579
+ }
2580
+ hide() {
2581
+ this.hoveredTaskId = null;
2582
+ this.el.style.display = "none";
2583
+ }
2584
+ destroy() {
2585
+ if (this.svg) {
2586
+ this.svg.removeEventListener("pointerover", this.onPointerOver);
2587
+ this.svg.removeEventListener("pointermove", this.onPointerMove);
2588
+ this.svg.removeEventListener("pointerout", this.onPointerOut);
2589
+ }
2590
+ this.el.remove();
2591
+ }
2592
+ };
2593
+
2394
2594
  // src/index.ts
2395
2595
  var HAS_DOM = typeof document !== "undefined" && typeof window !== "undefined";
2396
2596
  var DEFAULT_COLUMNS = [{ id: "name", title: "Name" }];
@@ -2410,6 +2610,9 @@ var _GanttChart = class _GanttChart {
2410
2610
  this.history = null;
2411
2611
  this.renderer = null;
2412
2612
  this.interactions = null;
2613
+ this.tooltip = null;
2614
+ this.resizeObserver = null;
2615
+ this.sort = null;
2413
2616
  this.renderModel = null;
2414
2617
  this.rafHandle = null;
2415
2618
  this.currentPage = 1;
@@ -2432,6 +2635,8 @@ var _GanttChart = class _GanttChart {
2432
2635
  showCriticalPath: options.showCriticalPath ?? false,
2433
2636
  showBaseline: options.showBaseline ?? false,
2434
2637
  showDeadlines: options.showDeadlines ?? true,
2638
+ showTooltip: options.showTooltip ?? true,
2639
+ autoFitToViewport: options.autoFitToViewport ?? false,
2435
2640
  showAssigneeAvatars: options.showAssigneeAvatars ?? false,
2436
2641
  enableHistory: options.enableHistory ?? false,
2437
2642
  keyboardAccessible: options.keyboardAccessible ?? false,
@@ -2452,13 +2657,16 @@ var _GanttChart = class _GanttChart {
2452
2657
  onDependencyRemove: options.onDependencyRemove,
2453
2658
  onDependencyDblClick: options.onDependencyDblClick,
2454
2659
  onTaskClick: options.onTaskClick,
2660
+ onTaskDblClick: options.onTaskDblClick,
2455
2661
  onGroupToggle: options.onGroupToggle,
2456
2662
  onContextMenu: options.onContextMenu,
2457
2663
  onTaskCreate: options.onTaskCreate,
2458
2664
  onColumnResize: options.onColumnResize,
2459
2665
  onColumnReorder: options.onColumnReorder,
2460
2666
  onTaskReorder: options.onTaskReorder,
2461
- onSelectionChange: options.onSelectionChange
2667
+ onSelectionChange: options.onSelectionChange,
2668
+ onSortChange: options.onSortChange,
2669
+ renderTooltip: options.renderTooltip
2462
2670
  };
2463
2671
  if (this.options.enableHistory) {
2464
2672
  this.history = new HistoryManager((state) => this.emitter.emit("history-change", state));
@@ -2481,7 +2689,8 @@ var _GanttChart = class _GanttChart {
2481
2689
  onRenameCommit: (taskId, name) => this.updateTask(taskId, { name }),
2482
2690
  onColumnResize: this.options.onColumnResize ? (columnId, width) => this.handleColumnResize(columnId, width) : void 0,
2483
2691
  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
2692
+ onRowReorder: this.options.onTaskReorder ? (draggedId, targetId, position) => this.handleRowReorder(draggedId, targetId, position) : void 0,
2693
+ onSortClick: (columnId) => this.handleSortClick(columnId)
2485
2694
  });
2486
2695
  this.interactions = new InteractionController(
2487
2696
  this.renderer.svg,
@@ -2502,7 +2711,17 @@ var _GanttChart = class _GanttChart {
2502
2711
  onCreateTaskDrag: this.options.onTaskCreate ? (rowTaskId, startXPx, endXPx) => this.handleCreateTaskDrag(rowTaskId, startXPx, endXPx) : void 0,
2503
2712
  onLinkDblClick: this.options.onDependencyDblClick ? (fromId, toId) => {
2504
2713
  const dep = this.dependencies.find((d) => d.fromId === fromId && d.toId === toId);
2505
- if (dep) this.options.onDependencyDblClick?.(dep);
2714
+ if (dep) {
2715
+ this.emitter.emit("dependency-dblclick", dep);
2716
+ this.options.onDependencyDblClick?.(dep);
2717
+ }
2718
+ } : void 0,
2719
+ onBarDblClick: this.options.onTaskDblClick ? (taskId) => {
2720
+ const task = this.tasks.find((t) => t.id === taskId);
2721
+ if (task) {
2722
+ this.emitter.emit("task-dblclick", { task });
2723
+ this.options.onTaskDblClick?.(task);
2724
+ }
2506
2725
  } : void 0,
2507
2726
  onContextMenu: this.options.onContextMenu ? (taskId, evt) => {
2508
2727
  const task = this.tasks.find((t) => t.id === taskId);
@@ -2521,6 +2740,17 @@ var _GanttChart = class _GanttChart {
2521
2740
  pxPerMs: () => pxPerMs(this.options.viewMode, this.columnWidth)
2522
2741
  }
2523
2742
  );
2743
+ this.tooltip = new Tooltip(
2744
+ () => ({ enabled: this.options.showTooltip, render: this.options.renderTooltip }),
2745
+ (taskId) => this.tasks.find((t) => t.id === taskId)
2746
+ );
2747
+ this.tooltip.attach(this.renderer.svg);
2748
+ if (typeof ResizeObserver !== "undefined") {
2749
+ this.resizeObserver = new ResizeObserver(() => {
2750
+ if (this.options.autoFitToViewport) this.scheduleRender();
2751
+ });
2752
+ this.resizeObserver.observe(this.container);
2753
+ }
2524
2754
  }
2525
2755
  handleMove(taskId, dxMs) {
2526
2756
  if (dxMs === 0) return;
@@ -2925,6 +3155,24 @@ var _GanttChart = class _GanttChart {
2925
3155
  this.options.onColumnReorder?.(order);
2926
3156
  this.scheduleRender();
2927
3157
  }
3158
+ /** Cycles a sortable column's header through asc -> desc -> none; clicking a different column starts it fresh at asc. */
3159
+ handleSortClick(columnId) {
3160
+ if (this.sort?.columnId === columnId) {
3161
+ this.setSort(columnId, this.sort.direction === "asc" ? "desc" : null);
3162
+ } else {
3163
+ this.setSort(columnId, "asc");
3164
+ }
3165
+ }
3166
+ /** Sorts siblings at every tree level by a column's value (see GanttColumn.sortable); `direction: null`/omitted `columnId` clears it. */
3167
+ setSort(columnId, direction = "asc") {
3168
+ this.sort = columnId && direction ? { columnId, direction } : null;
3169
+ this.emitter.emit("sort-change", { sort: this.sort });
3170
+ this.options.onSortChange?.(this.sort);
3171
+ this.scheduleRender();
3172
+ }
3173
+ getSort() {
3174
+ return this.sort;
3175
+ }
2928
3176
  handleRowReorder(draggedId, targetId, position) {
2929
3177
  const draggedIndex = this.tasks.findIndex((t) => t.id === draggedId);
2930
3178
  const targetIndex = this.tasks.findIndex((t) => t.id === targetId);
@@ -2991,13 +3239,18 @@ var _GanttChart = class _GanttChart {
2991
3239
  this.scheduleRender();
2992
3240
  }
2993
3241
  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) {
3242
+ const hasTheme = "theme" in partial;
3243
+ const hasColorScheme = "colorScheme" in partial;
3244
+ if (hasColorScheme) this.colorScheme = partial.colorScheme ?? "auto";
3245
+ if (hasTheme) this.explicitTheme = partial.theme;
3246
+ if (hasTheme || hasColorScheme) {
2997
3247
  this.theme = mergeTheme(this.explicitTheme, this.colorScheme);
2998
3248
  }
2999
- if (partial.columns) this.columns = partial.columns;
3249
+ if ("columns" in partial) this.columns = partial.columns ?? this.columns;
3000
3250
  if (partial.columnWidth !== void 0) this.columnWidth = partial.columnWidth;
3251
+ if (partial.gridPanelWidth !== void 0) {
3252
+ this.container.style.setProperty("--gantt-grid-panel-width", `${partial.gridPanelWidth}px`);
3253
+ }
3001
3254
  Object.assign(this.options, partial);
3002
3255
  this.scheduleRender();
3003
3256
  }
@@ -3100,7 +3353,8 @@ var _GanttChart = class _GanttChart {
3100
3353
  markers: this.options.markers,
3101
3354
  autoRollupProgress: this.options.autoRollupProgress,
3102
3355
  selectedTaskIds: this.options.selectable ? this.selectedTaskIds : void 0,
3103
- pagination
3356
+ pagination,
3357
+ sort: this.sort
3104
3358
  });
3105
3359
  }
3106
3360
  scheduleRender(immediate = false) {
@@ -3115,6 +3369,7 @@ var _GanttChart = class _GanttChart {
3115
3369
  });
3116
3370
  }
3117
3371
  doRender() {
3372
+ if (this.options.autoFitToViewport) this.applyAutoFit();
3118
3373
  this.renderModel = this.computeModel();
3119
3374
  this.renderer?.render(this.renderModel, this.tasks, this.columns, this.explicitTheme);
3120
3375
  if (this.renderer && this.colorScheme !== "auto") {
@@ -3123,6 +3378,29 @@ var _GanttChart = class _GanttChart {
3123
3378
  this.renderer?.root.removeAttribute("data-gantt-theme");
3124
3379
  }
3125
3380
  }
3381
+ /**
3382
+ * Stretch-only fit: bump columnWidth up so the timeline fills the container's available
3383
+ * width, but never shrink it below what the current zoom level already implies - a project
3384
+ * wider than the container should still scroll normally, not get crammed to fit.
3385
+ */
3386
+ applyAutoFit() {
3387
+ const availableWidth = this.container.clientWidth - this.options.gridPanelWidth;
3388
+ if (!Number.isFinite(availableWidth) || availableWidth <= 0) return;
3389
+ let min = null;
3390
+ let max = null;
3391
+ for (const t of this.tasks) {
3392
+ if (!isValidDate(t.start) || !isValidDate(t.end)) continue;
3393
+ min = min === null ? t.start.getTime() : Math.min(min, t.start.getTime());
3394
+ max = max === null ? t.end.getTime() : Math.max(max, t.end.getTime());
3395
+ }
3396
+ if (min === null || max === null || max <= min) return;
3397
+ const rangeMs = max - min;
3398
+ const unitMs = approxUnitMs(this.options.viewMode);
3399
+ const neededColumnWidth = availableWidth * unitMs / rangeMs;
3400
+ if (neededColumnWidth > this.columnWidth) {
3401
+ this.columnWidth = Math.min(neededColumnWidth, _GanttChart.MAX_COLUMN_WIDTH);
3402
+ }
3403
+ }
3126
3404
  undo() {
3127
3405
  this.history?.undo();
3128
3406
  }
@@ -3217,6 +3495,8 @@ var _GanttChart = class _GanttChart {
3217
3495
  this.darkMediaQuery?.removeEventListener?.("change", this.handleSchemeChange);
3218
3496
  this.emitter.removeAllListeners();
3219
3497
  this.interactions?.destroy();
3498
+ this.tooltip?.destroy();
3499
+ this.resizeObserver?.disconnect();
3220
3500
  this.renderer?.destroy();
3221
3501
  }
3222
3502
  };