@ganttloom/gantt-core 0.2.0 → 0.2.1

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/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # @ganttloom/gantt-core
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/%40ganttloom%2Fgantt-core)](https://www.npmjs.com/package/@ganttloom/gantt-core)
4
+ [![license](https://img.shields.io/npm/l/%40ganttloom%2Fgantt-core)](../../LICENSE)
5
+
3
6
  Framework-agnostic Gantt chart engine: layout, SVG rendering, drag/resize/link
4
7
  interactions, grouping, critical path, undo/redo, and more — zero runtime
5
8
  dependencies. If you're using React, prefer
@@ -381,6 +384,54 @@ in addition to the JS `theme` option (`Partial<GanttTheme>`) — style via
381
384
  either, or both. `colorScheme: "auto"` (the default) follows
382
385
  `prefers-color-scheme` live.
383
386
 
387
+ To override a token via CSS, target `.gantt-root` itself (the element the
388
+ chart renders into) rather than an ancestor — the defaults are declared on
389
+ `.gantt-root`, and a same-element declaration always wins over one inherited
390
+ from a parent, regardless of specificity:
391
+
392
+ ```css
393
+ .gantt-root {
394
+ --gantt-bar-color: #16a34a;
395
+ --gantt-critical-color: #ea580c;
396
+ }
397
+ ```
398
+
399
+ ### Styling structural elements (buttons, cells, rows)
400
+
401
+ Beyond color tokens, every structural/interactive element the chart renders
402
+ has a stable, undocumented-nowhere-else class name and **no inline styles
403
+ and no `!important`** — so plain CSS targeting these classes has full
404
+ control, same as any element you'd render yourself:
405
+
406
+ | Class | What it is |
407
+ |---|---|
408
+ | `.gantt-toggle` | The expand/collapse button on a group row |
409
+ | `.gantt-grid-header-cell` | A column header cell |
410
+ | `.gantt-grid-cell` | A grid body cell |
411
+ | `.gantt-grid-row` | A grid body row (has a default `:hover` background) |
412
+ | `.gantt-rename-input` | The inline text input shown while renaming a task |
413
+
414
+ For example, to make the expand/collapse toggle look like a small round
415
+ button instead of bare text:
416
+
417
+ ```css
418
+ .gantt-toggle {
419
+ border-radius: 999px;
420
+ padding: 2px 6px;
421
+ background: var(--gantt-grid-color, #e5e7eb);
422
+ }
423
+ .gantt-toggle:hover {
424
+ background: var(--gantt-bar-color, #3b82f6);
425
+ color: #fff;
426
+ }
427
+ ```
428
+
429
+ None of this is specific to the library's own chrome, either — anything
430
+ *you* render, like the playground demo's toolbar (`apps/playground`,
431
+ `apps/vue-playground`), is plain HTML/CSS in your own app code with no
432
+ library involvement at all, so it's exactly as customizable as any other
433
+ element on your page.
434
+
384
435
  ## API surface
385
436
 
386
437
  The full, current type/function list is re-exported from
package/dist/index.cjs CHANGED
@@ -189,7 +189,6 @@ function computeCriticalPath(tasks, dependencies) {
189
189
  }
190
190
  for (const e of edges) {
191
191
  outgoing.get(e.fromId)?.push(e);
192
- incoming.get(e.fromId) === void 0 ? void 0 : void 0;
193
192
  incoming.get(e.toId)?.push(e);
194
193
  }
195
194
  const inDegree = /* @__PURE__ */ new Map();
@@ -274,11 +273,9 @@ function computeCriticalPath(tasks, dependencies) {
274
273
  criticalLinks.add(linkKey(e.fromId, e.toId));
275
274
  }
276
275
  }
277
- const taskMap = new Map(tasks.map((t) => [t.id, t]));
278
276
  for (const task of tasks) {
279
277
  if (!task.isGroup) continue;
280
278
  let hasCriticalDescendant = false;
281
- let cur = task;
282
279
  const descendantStack = [task.id];
283
280
  const visited = /* @__PURE__ */ new Set();
284
281
  while (descendantStack.length > 0) {
@@ -293,8 +290,6 @@ function computeCriticalPath(tasks, dependencies) {
293
290
  }
294
291
  }
295
292
  if (hasCriticalDescendant) criticalTasks.add(task.id);
296
- void cur;
297
- void taskMap;
298
293
  }
299
294
  return { criticalTasks, criticalLinks };
300
295
  }
@@ -689,6 +684,9 @@ function flatten(roots) {
689
684
  }
690
685
  return out;
691
686
  }
687
+ function estimateTextWidth(text, fontSize) {
688
+ return text.length * fontSize * 0.62;
689
+ }
692
690
  function anchorPoint(bar, side) {
693
691
  const y = bar.y + bar.height / 2;
694
692
  return side === "start" ? { x: bar.x, y } : { x: bar.x + bar.width, y };
@@ -814,8 +812,9 @@ function computeLayout(input) {
814
812
  const end = row.node.computedEnd;
815
813
  if (!start || !end) return;
816
814
  const x = dateToX(start, paddedStart, viewMode, columnWidth);
817
- const width2 = Math.max(0, dateToX(end, paddedStart, viewMode, columnWidth) - x);
818
- if (!Number.isFinite(x) || !Number.isFinite(width2)) return;
815
+ const rawWidth = Math.max(0, dateToX(end, paddedStart, viewMode, columnWidth) - x);
816
+ if (!Number.isFinite(x) || !Number.isFinite(rawWidth)) return;
817
+ const width2 = rawWidth > 0 ? Math.max(rawWidth, 3) : rawWidth;
819
818
  const progress = task.progress !== void 0 ? Math.min(100, Math.max(0, task.progress)) : autoRollupProgress ? row.node.computedProgress : 0;
820
819
  const barHeight = theme.barHeight;
821
820
  const barY = y + (theme.rowHeight - barHeight) / 2;
@@ -923,7 +922,13 @@ function computeLayout(input) {
923
922
  })).filter((m) => Number.isFinite(m.x));
924
923
  let width = 0;
925
924
  for (const tick of ticks) width = Math.max(width, tick.x);
926
- for (const bar of bars) width = Math.max(width, bar.x + bar.width);
925
+ for (const bar of bars) {
926
+ width = Math.max(width, bar.x + bar.width);
927
+ if (bar.label) {
928
+ const labelOffset = bar.isMilestone ? theme.barHeight * 0.4 + 8 : bar.width + 6;
929
+ width = Math.max(width, bar.x + labelOffset + estimateTextWidth(bar.label, theme.fontSize));
930
+ }
931
+ }
927
932
  width += columnWidth;
928
933
  const height = rows.length * theme.rowHeight;
929
934
  return {
@@ -1422,7 +1427,9 @@ var GanttRenderer = class {
1422
1427
  }
1423
1428
  }
1424
1429
  renderMarkers(group, model) {
1425
- for (const marker of model.markers) {
1430
+ const sorted = [...model.markers].sort((a, b) => a.x - b.x);
1431
+ let defs = null;
1432
+ sorted.forEach((marker, i) => {
1426
1433
  const line = svgEl("line");
1427
1434
  line.setAttribute("x1", String(marker.x));
1428
1435
  line.setAttribute("x2", String(marker.x));
@@ -1434,16 +1441,33 @@ var GanttRenderer = class {
1434
1441
  line.dataset.ganttMarker = marker.label;
1435
1442
  group.appendChild(line);
1436
1443
  if (marker.label) {
1444
+ const nextX = sorted[i + 1]?.x ?? model.width;
1445
+ const cellWidth = Math.max(0, nextX - marker.x);
1446
+ if (!defs) {
1447
+ defs = svgEl("defs");
1448
+ group.insertBefore(defs, group.firstChild);
1449
+ }
1450
+ const clipId = `${this.markerDefsId}-marker-${i}`;
1451
+ const clipPath = svgEl("clipPath");
1452
+ clipPath.setAttribute("id", clipId);
1453
+ const clipRect = svgEl("rect");
1454
+ clipRect.setAttribute("x", String(marker.x));
1455
+ clipRect.setAttribute("y", "0");
1456
+ clipRect.setAttribute("width", String(cellWidth));
1457
+ clipRect.setAttribute("height", String(model.height));
1458
+ clipPath.appendChild(clipRect);
1459
+ defs.appendChild(clipPath);
1437
1460
  const label = svgEl("text");
1438
1461
  label.setAttribute("x", String(marker.x + 4));
1439
1462
  label.setAttribute("y", "12");
1463
+ label.setAttribute("clip-path", `url(#${clipId})`);
1440
1464
  label.setAttribute("fill", marker.color);
1441
1465
  label.setAttribute("font-size", String(model.theme.fontSize));
1442
1466
  label.setAttribute("font-family", model.theme.fontFamily);
1443
1467
  label.textContent = marker.label;
1444
1468
  group.appendChild(label);
1445
1469
  }
1446
- }
1470
+ });
1447
1471
  }
1448
1472
  renderHeader(model) {
1449
1473
  let headerGroup = this.svg.querySelector(".gantt-header-group");
@@ -1457,16 +1481,32 @@ var GanttRenderer = class {
1457
1481
  bg.setAttribute("height", String(model.headerHeight));
1458
1482
  bg.style.fill = themedFill("backgroundColor", model.theme.backgroundColor);
1459
1483
  headerGroup.appendChild(bg);
1460
- for (const tick of model.ticks) {
1484
+ const defs = svgEl("defs");
1485
+ const ticks = model.ticks;
1486
+ for (let i = 0; i < ticks.length; i++) {
1487
+ const tick = ticks[i];
1488
+ const cellWidth = (ticks[i + 1]?.x ?? model.width) - tick.x;
1489
+ const clipId = `${this.markerDefsId}-tick-${i}`;
1490
+ const clipPath = svgEl("clipPath");
1491
+ clipPath.setAttribute("id", clipId);
1492
+ const clipRect = svgEl("rect");
1493
+ clipRect.setAttribute("x", String(tick.x));
1494
+ clipRect.setAttribute("y", "0");
1495
+ clipRect.setAttribute("width", String(Math.max(0, cellWidth)));
1496
+ clipRect.setAttribute("height", String(model.headerHeight));
1497
+ clipPath.appendChild(clipRect);
1498
+ defs.appendChild(clipPath);
1461
1499
  const text = svgEl("text");
1462
1500
  text.setAttribute("x", String(tick.x + 4));
1463
1501
  text.setAttribute("y", String(model.headerHeight - 8));
1502
+ text.setAttribute("clip-path", `url(#${clipId})`);
1464
1503
  text.style.fill = themedFill("textColor", model.theme.textColor);
1465
1504
  text.setAttribute("font-size", String(model.theme.fontSize));
1466
1505
  text.setAttribute("font-family", model.theme.fontFamily);
1467
1506
  text.textContent = tick.label;
1468
1507
  headerGroup.appendChild(text);
1469
1508
  }
1509
+ headerGroup.insertBefore(defs, headerGroup.firstChild);
1470
1510
  this.svg.appendChild(headerGroup);
1471
1511
  }
1472
1512
  /**
@@ -2550,7 +2590,7 @@ var _GanttChart = class _GanttChart {
2550
2590
  const newStart = edge === "left" ? new Date(task.start.getTime() + dxMs) : task.start;
2551
2591
  const newEnd = edge === "right" ? new Date(task.end.getTime() + dxMs) : task.end;
2552
2592
  if (newEnd.getTime() <= newStart.getTime()) return;
2553
- let newSegments = task.segments?.map((s) => ({ ...s }));
2593
+ const newSegments = task.segments?.map((s) => ({ ...s }));
2554
2594
  if (newSegments && newSegments.length > 0) {
2555
2595
  if (edge === "left") newSegments[0] = { ...newSegments[0], start: newStart };
2556
2596
  else newSegments[newSegments.length - 1] = { ...newSegments[newSegments.length - 1], end: newEnd };