@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 +51 -0
- package/dist/index.cjs +52 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +52 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# @ganttloom/gantt-core
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@ganttloom/gantt-core)
|
|
4
|
+
[](../../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
|
|
818
|
-
if (!Number.isFinite(x) || !Number.isFinite(
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|