@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.cjs CHANGED
@@ -663,6 +663,29 @@ function computeProgressRollup(node) {
663
663
  }
664
664
  node.computedProgress = totalWeight > 0 ? weightedSum / totalWeight : 0;
665
665
  }
666
+ function sortValue(node, col) {
667
+ if (!col) return null;
668
+ if (col.accessor) return col.accessor(node.task) ?? null;
669
+ return col.id === "name" ? node.task.name : null;
670
+ }
671
+ function compareValues(a, b) {
672
+ if (a === null && b === null) return 0;
673
+ if (a === null) return 1;
674
+ if (b === null) return -1;
675
+ if (typeof a === "number" && typeof b === "number") return a - b;
676
+ return String(a).localeCompare(String(b), void 0, { numeric: true, sensitivity: "base" });
677
+ }
678
+ function sortTree(roots, columns, sort) {
679
+ if (!sort) return;
680
+ const col = columns.find((c) => c.id === sort.columnId);
681
+ const dir = sort.direction === "desc" ? -1 : 1;
682
+ const compare = (a, b) => dir * compareValues(sortValue(a, col), sortValue(b, col));
683
+ const sortLevel = (nodes) => {
684
+ nodes.sort(compare);
685
+ for (const node of nodes) sortLevel(node.children);
686
+ };
687
+ sortLevel(roots);
688
+ }
666
689
  function flatten(roots) {
667
690
  const out = [];
668
691
  const stack = [];
@@ -749,6 +772,14 @@ function linkPath(from, to) {
749
772
  const c2y = to.y;
750
773
  return `M ${from.x} ${from.y} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${to.x} ${to.y}`;
751
774
  }
775
+ var MIN_VISIBLE_UNITS = {
776
+ hour: 8,
777
+ day: 7,
778
+ week: 6,
779
+ month: 6,
780
+ quarter: 4,
781
+ year: 5
782
+ };
752
783
  function computeLayout(input) {
753
784
  const {
754
785
  tasks,
@@ -764,7 +795,8 @@ function computeLayout(input) {
764
795
  markers = [],
765
796
  autoRollupProgress = false,
766
797
  selectedTaskIds,
767
- pagination
798
+ pagination,
799
+ sort = null
768
800
  } = input;
769
801
  validateInputs(tasks, dependencies);
770
802
  const { roots, nodeById } = buildTree(tasks);
@@ -772,6 +804,7 @@ function computeLayout(input) {
772
804
  if (autoRollupProgress) {
773
805
  for (const root of roots) computeProgressRollup(root);
774
806
  }
807
+ sortTree(roots, columns, sort);
775
808
  let visibleRoots = roots;
776
809
  if (pagination && pagination.pageSize > 0) {
777
810
  const start = Math.max(0, (pagination.page - 1) * pagination.pageSize);
@@ -791,8 +824,15 @@ function computeLayout(input) {
791
824
  const now = Date.now();
792
825
  const rawRangeStart = rangeMin === null ? new Date(now) : new Date(rangeMin);
793
826
  const rawRangeEnd = rangeMax === null ? new Date(now + 1) : new Date(rangeMax);
794
- const paddedStart = addUnit(rawRangeStart, viewMode, -1);
795
- const paddedEnd = addUnit(rawRangeEnd, viewMode, 1);
827
+ let paddedStart = addUnit(rawRangeStart, viewMode, -1);
828
+ let paddedEnd = addUnit(rawRangeEnd, viewMode, 1);
829
+ const minUnits = MIN_VISIBLE_UNITS[viewMode];
830
+ const currentUnits = (paddedEnd.getTime() - paddedStart.getTime()) / approxUnitMs(viewMode);
831
+ if (currentUnits < minUnits) {
832
+ const deficit = minUnits - currentUnits;
833
+ paddedStart = addUnit(paddedStart, viewMode, -Math.ceil(deficit / 2));
834
+ paddedEnd = addUnit(paddedEnd, viewMode, Math.floor(deficit / 2));
835
+ }
796
836
  const rows = [];
797
837
  const bars = [];
798
838
  const barByTaskId = /* @__PURE__ */ new Map();
@@ -943,7 +983,8 @@ function computeLayout(input) {
943
983
  columns,
944
984
  theme,
945
985
  markers: renderMarkers,
946
- rangeStart: paddedStart
986
+ rangeStart: paddedStart,
987
+ sort
947
988
  };
948
989
  }
949
990
 
@@ -1140,6 +1181,7 @@ var GanttRenderer = class {
1140
1181
  this.lastModel = null;
1141
1182
  this.lastTasks = [];
1142
1183
  this.lastColumns = [];
1184
+ this.lastExplicitThemeKeys = /* @__PURE__ */ new Set();
1143
1185
  this.onGridDoubleClick = (evt) => {
1144
1186
  if (!this.options.onRenameCommit) return;
1145
1187
  const target = evt.target;
@@ -1177,8 +1219,13 @@ var GanttRenderer = class {
1177
1219
  this.root = el("div", "gantt-root");
1178
1220
  this.gridPanel = el("div", "gantt-grid-panel");
1179
1221
  this.timelineScroll = el("div", "gantt-timeline-scroll");
1222
+ this.headerContainer = el("div", "gantt-timeline-header");
1223
+ this.headerSvg = svgEl("svg");
1224
+ this.headerSvg.classList.add("gantt-header-svg");
1225
+ this.headerContainer.appendChild(this.headerSvg);
1180
1226
  this.svg = svgEl("svg");
1181
1227
  this.svg.classList.add("gantt-svg");
1228
+ this.timelineScroll.appendChild(this.headerContainer);
1182
1229
  this.timelineScroll.appendChild(this.svg);
1183
1230
  this.root.appendChild(this.gridPanel);
1184
1231
  this.root.appendChild(this.timelineScroll);
@@ -1194,9 +1241,14 @@ var GanttRenderer = class {
1194
1241
  this.lastTasks = tasks;
1195
1242
  this.lastColumns = columns;
1196
1243
  const cssVars = explicitThemeToCssVars(explicitTheme);
1197
- for (const key of Object.keys(cssVars)) {
1244
+ const newKeys = new Set(Object.keys(cssVars));
1245
+ for (const key of this.lastExplicitThemeKeys) {
1246
+ if (!newKeys.has(key)) this.root.style.removeProperty(key);
1247
+ }
1248
+ for (const key of newKeys) {
1198
1249
  this.root.style.setProperty(key, cssVars[key]);
1199
1250
  }
1251
+ this.lastExplicitThemeKeys = newKeys;
1200
1252
  this.renderGridPanel(model, tasks, columns);
1201
1253
  this.renderTimeline(model, tasks);
1202
1254
  }
@@ -1224,7 +1276,7 @@ var GanttRenderer = class {
1224
1276
  const header = el("div", "gantt-grid-header-row");
1225
1277
  header.style.height = `${model.headerHeight}px`;
1226
1278
  for (const col of columns) {
1227
- header.appendChild(this.renderHeaderCell(col, columns));
1279
+ header.appendChild(this.renderHeaderCell(col, columns, model.sort));
1228
1280
  }
1229
1281
  this.gridPanel.appendChild(header);
1230
1282
  const body = el("div", "gantt-grid-body");
@@ -1237,11 +1289,23 @@ var GanttRenderer = class {
1237
1289
  });
1238
1290
  this.gridPanel.appendChild(body);
1239
1291
  }
1240
- renderHeaderCell(col, columns) {
1292
+ renderHeaderCell(col, columns, sort) {
1241
1293
  const cell = el("div", "gantt-grid-header-cell");
1242
1294
  cell.style.position = "relative";
1243
1295
  if (col.width) cell.style.width = `${col.width}px`;
1244
1296
  cell.textContent = col.title;
1297
+ if (col.sortable) {
1298
+ cell.classList.add("gantt-grid-header-cell-sortable");
1299
+ const active = sort?.columnId === col.id;
1300
+ const indicator = el("span", "gantt-sort-indicator");
1301
+ indicator.textContent = active ? sort.direction === "asc" ? " \u25B2" : " \u25BC" : "";
1302
+ cell.appendChild(indicator);
1303
+ cell.addEventListener("click", (evt) => {
1304
+ const target = evt.target;
1305
+ if (target.closest(".gantt-col-resize-handle")) return;
1306
+ this.options.onSortClick?.(col.id);
1307
+ });
1308
+ }
1245
1309
  if (this.options.onColumnReorder) {
1246
1310
  cell.draggable = true;
1247
1311
  cell.style.cursor = "grab";
@@ -1368,8 +1432,8 @@ var GanttRenderer = class {
1368
1432
  renderTimeline(model, tasks) {
1369
1433
  this.svg.replaceChildren();
1370
1434
  this.svg.setAttribute("width", String(model.width));
1371
- this.svg.setAttribute("height", String(model.headerHeight + model.height));
1372
- this.svg.setAttribute("viewBox", `0 0 ${model.width} ${model.headerHeight + model.height}`);
1435
+ this.svg.setAttribute("height", String(model.height));
1436
+ this.svg.setAttribute("viewBox", `0 0 ${model.width} ${model.height}`);
1373
1437
  const defs = svgEl("defs");
1374
1438
  const marker = svgEl("marker");
1375
1439
  marker.setAttribute("id", this.markerDefsId);
@@ -1386,7 +1450,6 @@ var GanttRenderer = class {
1386
1450
  this.svg.appendChild(defs);
1387
1451
  const { startY, endY } = this.getViewportRowRange(model);
1388
1452
  const bodyGroup = svgEl("g");
1389
- bodyGroup.setAttribute("transform", `translate(0, ${model.headerHeight})`);
1390
1453
  this.renderGridLines(bodyGroup, model);
1391
1454
  this.renderRowBackgrounds(bodyGroup, model, startY, endY);
1392
1455
  this.renderBars(bodyGroup, model, tasks, startY, endY);
@@ -1470,9 +1533,12 @@ var GanttRenderer = class {
1470
1533
  });
1471
1534
  }
1472
1535
  renderHeader(model) {
1473
- let headerGroup = this.svg.querySelector(".gantt-header-group");
1474
- if (headerGroup) headerGroup.remove();
1475
- headerGroup = svgEl("g");
1536
+ this.headerSvg.replaceChildren();
1537
+ this.headerSvg.setAttribute("width", String(model.width));
1538
+ this.headerSvg.setAttribute("height", String(model.headerHeight));
1539
+ this.headerSvg.setAttribute("viewBox", `0 0 ${model.width} ${model.headerHeight}`);
1540
+ this.headerContainer.style.height = `${model.headerHeight}px`;
1541
+ const headerGroup = svgEl("g");
1476
1542
  headerGroup.setAttribute("class", "gantt-header-group");
1477
1543
  const bg = svgEl("rect");
1478
1544
  bg.setAttribute("x", "0");
@@ -1507,7 +1573,7 @@ var GanttRenderer = class {
1507
1573
  headerGroup.appendChild(text);
1508
1574
  }
1509
1575
  headerGroup.insertBefore(defs, headerGroup.firstChild);
1510
- this.svg.appendChild(headerGroup);
1576
+ this.headerSvg.appendChild(headerGroup);
1511
1577
  }
1512
1578
  /**
1513
1579
  * Invisible full-width hit-test rects, one per row, behind the bars. Lets
@@ -1542,11 +1608,6 @@ var GanttRenderer = class {
1542
1608
  g.setAttribute("class", "gantt-bar");
1543
1609
  g.dataset.ganttBar = bar.taskId;
1544
1610
  g.setAttribute("transform", `translate(${bar.x}, ${bar.y})`);
1545
- if (task?.notes) {
1546
- const title = svgEl("title");
1547
- title.textContent = task.notes;
1548
- g.appendChild(title);
1549
- }
1550
1611
  if (this.options.keyboardAccessible) {
1551
1612
  g.setAttribute("tabindex", "0");
1552
1613
  g.setAttribute("role", "button");
@@ -1791,8 +1852,30 @@ var GanttRenderer = class {
1791
1852
  group.appendChild(path);
1792
1853
  }
1793
1854
  }
1855
+ /**
1856
+ * The header and body are separate DOM elements at runtime (so plain CSS `position: sticky`
1857
+ * can pin the header - see the constructor's doc comment), but exports need one self-contained
1858
+ * SVG with both, laid out exactly as the original single-SVG version was.
1859
+ */
1794
1860
  toSVGString() {
1795
- return new XMLSerializer().serializeToString(this.svg);
1861
+ const headerHeight = this.lastModel?.headerHeight ?? 0;
1862
+ const width = this.lastModel?.width ?? 0;
1863
+ const bodyHeight = this.lastModel?.height ?? 0;
1864
+ const combined = svgEl("svg");
1865
+ combined.setAttribute("xmlns", SVG_NS);
1866
+ combined.setAttribute("width", String(width));
1867
+ combined.setAttribute("height", String(headerHeight + bodyHeight));
1868
+ combined.setAttribute("viewBox", `0 0 ${width} ${headerHeight + bodyHeight}`);
1869
+ for (const child of Array.from(this.headerSvg.children)) {
1870
+ combined.appendChild(child.cloneNode(true));
1871
+ }
1872
+ const bodyGroup = svgEl("g");
1873
+ bodyGroup.setAttribute("transform", `translate(0, ${headerHeight})`);
1874
+ for (const child of Array.from(this.svg.children)) {
1875
+ bodyGroup.appendChild(child.cloneNode(true));
1876
+ }
1877
+ combined.appendChild(bodyGroup);
1878
+ return new XMLSerializer().serializeToString(combined);
1796
1879
  }
1797
1880
  destroy() {
1798
1881
  if (this.scrollListener) {
@@ -2171,13 +2254,17 @@ var InteractionController = class {
2171
2254
  }
2172
2255
  };
2173
2256
  this.onSvgDoubleClick = (evt) => {
2174
- if (!this.callbacks.onLinkDblClick) return;
2175
2257
  const target = evt.target;
2176
2258
  const linkPath2 = target.closest("[data-gantt-link]");
2177
2259
  const key = linkPath2?.dataset.ganttLink;
2178
- if (!key) return;
2179
- const [fromId, toId] = key.split("->");
2180
- if (fromId && toId) this.callbacks.onLinkDblClick(fromId, toId);
2260
+ if (key && this.callbacks.onLinkDblClick) {
2261
+ const [fromId, toId] = key.split("->");
2262
+ if (fromId && toId) this.callbacks.onLinkDblClick(fromId, toId);
2263
+ return;
2264
+ }
2265
+ const barGroup = target.closest("[data-gantt-bar]");
2266
+ const taskId = barGroup?.dataset.ganttBar;
2267
+ if (taskId && this.callbacks.onBarDblClick) this.callbacks.onBarDblClick(taskId);
2181
2268
  };
2182
2269
  this.onPointerDown = (evt) => {
2183
2270
  const target = evt.target;
@@ -2440,6 +2527,119 @@ var InteractionController = class {
2440
2527
  }
2441
2528
  };
2442
2529
 
2530
+ // src/tooltip.ts
2531
+ function formatDate(d) {
2532
+ return d.toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" });
2533
+ }
2534
+ function defaultTooltipContent(task) {
2535
+ const el2 = document.createElement("div");
2536
+ const title = document.createElement("div");
2537
+ title.className = "gantt-tooltip-title";
2538
+ title.textContent = task.name;
2539
+ el2.appendChild(title);
2540
+ const dates = document.createElement("div");
2541
+ dates.className = "gantt-tooltip-row";
2542
+ dates.textContent = task.isMilestone ? formatDate(task.start) : `${formatDate(task.start)} \u2192 ${formatDate(task.end)}`;
2543
+ el2.appendChild(dates);
2544
+ if (task.progress !== void 0) {
2545
+ const progress = document.createElement("div");
2546
+ progress.className = "gantt-tooltip-row";
2547
+ progress.textContent = `${Math.round(task.progress)}% complete`;
2548
+ el2.appendChild(progress);
2549
+ }
2550
+ if (task.assignees && task.assignees.length > 0) {
2551
+ const assignees = document.createElement("div");
2552
+ assignees.className = "gantt-tooltip-row";
2553
+ assignees.textContent = task.assignees.map((a) => a.name).join(", ");
2554
+ el2.appendChild(assignees);
2555
+ }
2556
+ if (task.notes) {
2557
+ const notes = document.createElement("div");
2558
+ notes.className = "gantt-tooltip-notes";
2559
+ notes.textContent = task.notes;
2560
+ el2.appendChild(notes);
2561
+ }
2562
+ return el2;
2563
+ }
2564
+ var Tooltip = class {
2565
+ constructor(getOptions, getTask) {
2566
+ this.svg = null;
2567
+ this.hoveredTaskId = null;
2568
+ this.onPointerOver = (evt) => {
2569
+ if (!this.getOptions().enabled) return;
2570
+ const target = evt.target;
2571
+ const barGroup = target.closest("[data-gantt-bar]");
2572
+ const taskId = barGroup?.dataset.ganttBar;
2573
+ if (!taskId || taskId === this.hoveredTaskId) return;
2574
+ const task = this.getTask(taskId);
2575
+ if (!task) return;
2576
+ const { render } = this.getOptions();
2577
+ const content = render ? render(task) : defaultTooltipContent(task);
2578
+ if (content === null || content === void 0) {
2579
+ this.hide();
2580
+ return;
2581
+ }
2582
+ this.el.replaceChildren();
2583
+ if (content instanceof HTMLElement) {
2584
+ this.el.appendChild(content);
2585
+ } else {
2586
+ this.el.textContent = content;
2587
+ }
2588
+ this.hoveredTaskId = taskId;
2589
+ this.el.style.display = "block";
2590
+ this.position(evt.clientX, evt.clientY);
2591
+ };
2592
+ this.onPointerMove = (evt) => {
2593
+ if (this.hoveredTaskId === null) return;
2594
+ this.position(evt.clientX, evt.clientY);
2595
+ };
2596
+ this.onPointerOut = (evt) => {
2597
+ const related = evt.relatedTarget;
2598
+ const target = evt.target;
2599
+ const leftBarGroup = target.closest("[data-gantt-bar]");
2600
+ if (related && leftBarGroup?.contains(related)) return;
2601
+ this.hide();
2602
+ };
2603
+ this.getOptions = getOptions;
2604
+ this.getTask = getTask;
2605
+ this.el = document.createElement("div");
2606
+ this.el.className = "gantt-tooltip";
2607
+ this.el.setAttribute("role", "tooltip");
2608
+ this.el.style.display = "none";
2609
+ document.body.appendChild(this.el);
2610
+ }
2611
+ attach(svg) {
2612
+ this.svg = svg;
2613
+ svg.addEventListener("pointerover", this.onPointerOver);
2614
+ svg.addEventListener("pointermove", this.onPointerMove);
2615
+ svg.addEventListener("pointerout", this.onPointerOut);
2616
+ }
2617
+ position(clientX, clientY) {
2618
+ const OFFSET = 12;
2619
+ const rect = this.el.getBoundingClientRect();
2620
+ const viewportW = window.innerWidth;
2621
+ const viewportH = window.innerHeight;
2622
+ let left = clientX + OFFSET;
2623
+ let top = clientY + OFFSET;
2624
+ if (left + rect.width > viewportW) left = clientX - OFFSET - rect.width;
2625
+ if (top + rect.height > viewportH) top = clientY - OFFSET - rect.height;
2626
+ this.el.style.left = `${Math.max(0, left)}px`;
2627
+ this.el.style.top = `${Math.max(0, top)}px`;
2628
+ }
2629
+ hide() {
2630
+ this.hoveredTaskId = null;
2631
+ this.el.style.display = "none";
2632
+ }
2633
+ destroy() {
2634
+ if (this.svg) {
2635
+ this.svg.removeEventListener("pointerover", this.onPointerOver);
2636
+ this.svg.removeEventListener("pointermove", this.onPointerMove);
2637
+ this.svg.removeEventListener("pointerout", this.onPointerOut);
2638
+ }
2639
+ this.el.remove();
2640
+ }
2641
+ };
2642
+
2443
2643
  // src/index.ts
2444
2644
  var HAS_DOM = typeof document !== "undefined" && typeof window !== "undefined";
2445
2645
  var DEFAULT_COLUMNS = [{ id: "name", title: "Name" }];
@@ -2459,6 +2659,9 @@ var _GanttChart = class _GanttChart {
2459
2659
  this.history = null;
2460
2660
  this.renderer = null;
2461
2661
  this.interactions = null;
2662
+ this.tooltip = null;
2663
+ this.resizeObserver = null;
2664
+ this.sort = null;
2462
2665
  this.renderModel = null;
2463
2666
  this.rafHandle = null;
2464
2667
  this.currentPage = 1;
@@ -2481,6 +2684,8 @@ var _GanttChart = class _GanttChart {
2481
2684
  showCriticalPath: options.showCriticalPath ?? false,
2482
2685
  showBaseline: options.showBaseline ?? false,
2483
2686
  showDeadlines: options.showDeadlines ?? true,
2687
+ showTooltip: options.showTooltip ?? true,
2688
+ autoFitToViewport: options.autoFitToViewport ?? false,
2484
2689
  showAssigneeAvatars: options.showAssigneeAvatars ?? false,
2485
2690
  enableHistory: options.enableHistory ?? false,
2486
2691
  keyboardAccessible: options.keyboardAccessible ?? false,
@@ -2501,13 +2706,16 @@ var _GanttChart = class _GanttChart {
2501
2706
  onDependencyRemove: options.onDependencyRemove,
2502
2707
  onDependencyDblClick: options.onDependencyDblClick,
2503
2708
  onTaskClick: options.onTaskClick,
2709
+ onTaskDblClick: options.onTaskDblClick,
2504
2710
  onGroupToggle: options.onGroupToggle,
2505
2711
  onContextMenu: options.onContextMenu,
2506
2712
  onTaskCreate: options.onTaskCreate,
2507
2713
  onColumnResize: options.onColumnResize,
2508
2714
  onColumnReorder: options.onColumnReorder,
2509
2715
  onTaskReorder: options.onTaskReorder,
2510
- onSelectionChange: options.onSelectionChange
2716
+ onSelectionChange: options.onSelectionChange,
2717
+ onSortChange: options.onSortChange,
2718
+ renderTooltip: options.renderTooltip
2511
2719
  };
2512
2720
  if (this.options.enableHistory) {
2513
2721
  this.history = new HistoryManager((state) => this.emitter.emit("history-change", state));
@@ -2530,7 +2738,8 @@ var _GanttChart = class _GanttChart {
2530
2738
  onRenameCommit: (taskId, name) => this.updateTask(taskId, { name }),
2531
2739
  onColumnResize: this.options.onColumnResize ? (columnId, width) => this.handleColumnResize(columnId, width) : void 0,
2532
2740
  onColumnReorder: this.options.onColumnReorder ? (order) => this.handleColumnReorder(order) : void 0,
2533
- onRowReorder: this.options.onTaskReorder ? (draggedId, targetId, position) => this.handleRowReorder(draggedId, targetId, position) : void 0
2741
+ onRowReorder: this.options.onTaskReorder ? (draggedId, targetId, position) => this.handleRowReorder(draggedId, targetId, position) : void 0,
2742
+ onSortClick: (columnId) => this.handleSortClick(columnId)
2534
2743
  });
2535
2744
  this.interactions = new InteractionController(
2536
2745
  this.renderer.svg,
@@ -2551,7 +2760,17 @@ var _GanttChart = class _GanttChart {
2551
2760
  onCreateTaskDrag: this.options.onTaskCreate ? (rowTaskId, startXPx, endXPx) => this.handleCreateTaskDrag(rowTaskId, startXPx, endXPx) : void 0,
2552
2761
  onLinkDblClick: this.options.onDependencyDblClick ? (fromId, toId) => {
2553
2762
  const dep = this.dependencies.find((d) => d.fromId === fromId && d.toId === toId);
2554
- if (dep) this.options.onDependencyDblClick?.(dep);
2763
+ if (dep) {
2764
+ this.emitter.emit("dependency-dblclick", dep);
2765
+ this.options.onDependencyDblClick?.(dep);
2766
+ }
2767
+ } : void 0,
2768
+ onBarDblClick: this.options.onTaskDblClick ? (taskId) => {
2769
+ const task = this.tasks.find((t) => t.id === taskId);
2770
+ if (task) {
2771
+ this.emitter.emit("task-dblclick", { task });
2772
+ this.options.onTaskDblClick?.(task);
2773
+ }
2555
2774
  } : void 0,
2556
2775
  onContextMenu: this.options.onContextMenu ? (taskId, evt) => {
2557
2776
  const task = this.tasks.find((t) => t.id === taskId);
@@ -2570,6 +2789,17 @@ var _GanttChart = class _GanttChart {
2570
2789
  pxPerMs: () => pxPerMs(this.options.viewMode, this.columnWidth)
2571
2790
  }
2572
2791
  );
2792
+ this.tooltip = new Tooltip(
2793
+ () => ({ enabled: this.options.showTooltip, render: this.options.renderTooltip }),
2794
+ (taskId) => this.tasks.find((t) => t.id === taskId)
2795
+ );
2796
+ this.tooltip.attach(this.renderer.svg);
2797
+ if (typeof ResizeObserver !== "undefined") {
2798
+ this.resizeObserver = new ResizeObserver(() => {
2799
+ if (this.options.autoFitToViewport) this.scheduleRender();
2800
+ });
2801
+ this.resizeObserver.observe(this.container);
2802
+ }
2573
2803
  }
2574
2804
  handleMove(taskId, dxMs) {
2575
2805
  if (dxMs === 0) return;
@@ -2974,6 +3204,24 @@ var _GanttChart = class _GanttChart {
2974
3204
  this.options.onColumnReorder?.(order);
2975
3205
  this.scheduleRender();
2976
3206
  }
3207
+ /** Cycles a sortable column's header through asc -> desc -> none; clicking a different column starts it fresh at asc. */
3208
+ handleSortClick(columnId) {
3209
+ if (this.sort?.columnId === columnId) {
3210
+ this.setSort(columnId, this.sort.direction === "asc" ? "desc" : null);
3211
+ } else {
3212
+ this.setSort(columnId, "asc");
3213
+ }
3214
+ }
3215
+ /** Sorts siblings at every tree level by a column's value (see GanttColumn.sortable); `direction: null`/omitted `columnId` clears it. */
3216
+ setSort(columnId, direction = "asc") {
3217
+ this.sort = columnId && direction ? { columnId, direction } : null;
3218
+ this.emitter.emit("sort-change", { sort: this.sort });
3219
+ this.options.onSortChange?.(this.sort);
3220
+ this.scheduleRender();
3221
+ }
3222
+ getSort() {
3223
+ return this.sort;
3224
+ }
2977
3225
  handleRowReorder(draggedId, targetId, position) {
2978
3226
  const draggedIndex = this.tasks.findIndex((t) => t.id === draggedId);
2979
3227
  const targetIndex = this.tasks.findIndex((t) => t.id === targetId);
@@ -3040,13 +3288,18 @@ var _GanttChart = class _GanttChart {
3040
3288
  this.scheduleRender();
3041
3289
  }
3042
3290
  setOptions(partial) {
3043
- if (partial.colorScheme !== void 0) this.colorScheme = partial.colorScheme;
3044
- if (partial.theme !== void 0) this.explicitTheme = partial.theme;
3045
- if (partial.theme !== void 0 || partial.colorScheme !== void 0) {
3291
+ const hasTheme = "theme" in partial;
3292
+ const hasColorScheme = "colorScheme" in partial;
3293
+ if (hasColorScheme) this.colorScheme = partial.colorScheme ?? "auto";
3294
+ if (hasTheme) this.explicitTheme = partial.theme;
3295
+ if (hasTheme || hasColorScheme) {
3046
3296
  this.theme = mergeTheme(this.explicitTheme, this.colorScheme);
3047
3297
  }
3048
- if (partial.columns) this.columns = partial.columns;
3298
+ if ("columns" in partial) this.columns = partial.columns ?? this.columns;
3049
3299
  if (partial.columnWidth !== void 0) this.columnWidth = partial.columnWidth;
3300
+ if (partial.gridPanelWidth !== void 0) {
3301
+ this.container.style.setProperty("--gantt-grid-panel-width", `${partial.gridPanelWidth}px`);
3302
+ }
3050
3303
  Object.assign(this.options, partial);
3051
3304
  this.scheduleRender();
3052
3305
  }
@@ -3149,7 +3402,8 @@ var _GanttChart = class _GanttChart {
3149
3402
  markers: this.options.markers,
3150
3403
  autoRollupProgress: this.options.autoRollupProgress,
3151
3404
  selectedTaskIds: this.options.selectable ? this.selectedTaskIds : void 0,
3152
- pagination
3405
+ pagination,
3406
+ sort: this.sort
3153
3407
  });
3154
3408
  }
3155
3409
  scheduleRender(immediate = false) {
@@ -3164,6 +3418,7 @@ var _GanttChart = class _GanttChart {
3164
3418
  });
3165
3419
  }
3166
3420
  doRender() {
3421
+ if (this.options.autoFitToViewport) this.applyAutoFit();
3167
3422
  this.renderModel = this.computeModel();
3168
3423
  this.renderer?.render(this.renderModel, this.tasks, this.columns, this.explicitTheme);
3169
3424
  if (this.renderer && this.colorScheme !== "auto") {
@@ -3172,6 +3427,29 @@ var _GanttChart = class _GanttChart {
3172
3427
  this.renderer?.root.removeAttribute("data-gantt-theme");
3173
3428
  }
3174
3429
  }
3430
+ /**
3431
+ * Stretch-only fit: bump columnWidth up so the timeline fills the container's available
3432
+ * width, but never shrink it below what the current zoom level already implies - a project
3433
+ * wider than the container should still scroll normally, not get crammed to fit.
3434
+ */
3435
+ applyAutoFit() {
3436
+ const availableWidth = this.container.clientWidth - this.options.gridPanelWidth;
3437
+ if (!Number.isFinite(availableWidth) || availableWidth <= 0) return;
3438
+ let min = null;
3439
+ let max = null;
3440
+ for (const t of this.tasks) {
3441
+ if (!isValidDate(t.start) || !isValidDate(t.end)) continue;
3442
+ min = min === null ? t.start.getTime() : Math.min(min, t.start.getTime());
3443
+ max = max === null ? t.end.getTime() : Math.max(max, t.end.getTime());
3444
+ }
3445
+ if (min === null || max === null || max <= min) return;
3446
+ const rangeMs = max - min;
3447
+ const unitMs = approxUnitMs(this.options.viewMode);
3448
+ const neededColumnWidth = availableWidth * unitMs / rangeMs;
3449
+ if (neededColumnWidth > this.columnWidth) {
3450
+ this.columnWidth = Math.min(neededColumnWidth, _GanttChart.MAX_COLUMN_WIDTH);
3451
+ }
3452
+ }
3175
3453
  undo() {
3176
3454
  this.history?.undo();
3177
3455
  }
@@ -3266,6 +3544,8 @@ var _GanttChart = class _GanttChart {
3266
3544
  this.darkMediaQuery?.removeEventListener?.("change", this.handleSchemeChange);
3267
3545
  this.emitter.removeAllListeners();
3268
3546
  this.interactions?.destroy();
3547
+ this.tooltip?.destroy();
3548
+ this.resizeObserver?.disconnect();
3269
3549
  this.renderer?.destroy();
3270
3550
  }
3271
3551
  };