@art-tools/react-gantt 0.1.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/LICENSE +21 -0
- package/README.md +626 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1042 -0
- package/dist/index.d.ts +1042 -0
- package/dist/index.mjs +3288 -0
- package/dist/index.mjs.map +1 -0
- package/dist/style.css +2 -0
- package/dist/style.css.d.ts +15 -0
- package/package.json +96 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Artem Makatera
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
# @art-tools/react-gantt
|
|
2
|
+
|
|
3
|
+
A high-performance, composable Gantt chart component library for React. It renders
|
|
4
|
+
thousands of tasks smoothly (rows **and** columns are virtualized on both axes),
|
|
5
|
+
ships a full undo/redo transaction model, cascading dependency scheduling, and a
|
|
6
|
+
MUI-style slot system for deep customization.
|
|
7
|
+
|
|
8
|
+
- **Composable** — drop in the all-in-one `<Gantt />`, or assemble
|
|
9
|
+
`<GanttProvider>` + `<TaskList>` + `<GanttGrid>` yourself.
|
|
10
|
+
- **Interactive** — drag to move/resize bars, draw and delete dependency links,
|
|
11
|
+
edit progress, expand/collapse hierarchy.
|
|
12
|
+
- **Fast** — windowed rendering with overscan, an incremental resolve cache, and
|
|
13
|
+
purpose-scoped React contexts so hot updates don't re-render stable subtrees.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Table of contents
|
|
18
|
+
|
|
19
|
+
- [Install](#install)
|
|
20
|
+
- [Quick start](#quick-start)
|
|
21
|
+
- [Composable API](#composable-api)
|
|
22
|
+
- [`GanttProps` reference](#ganttprops-reference)
|
|
23
|
+
- [Data model](#data-model)
|
|
24
|
+
- [Working time (calendars)](#working-time-calendars)
|
|
25
|
+
- [Task bars](#task-bars)
|
|
26
|
+
- [Dependencies & scheduling](#dependencies--scheduling)
|
|
27
|
+
- [Columns](#columns)
|
|
28
|
+
- [Read-only](#read-only)
|
|
29
|
+
- [Imperative API](#imperative-api)
|
|
30
|
+
- [Accessibility](#accessibility)
|
|
31
|
+
- [Slots & theming](#slots--theming)
|
|
32
|
+
- [Architecture (for contributors)](#architecture-for-contributors)
|
|
33
|
+
- [Roadmap](#roadmap)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pnpm add @art-tools/react-gantt
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Peer dependencies: `react` and `react-dom` (`^18 || ^19`). The only runtime
|
|
44
|
+
dependency is [`clsx`](https://github.com/lukeed/clsx).
|
|
45
|
+
|
|
46
|
+
Import the stylesheet once, near your app root:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import "@art-tools/react-gantt/style.css";
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { Gantt, type GanttTask } from "@art-tools/react-gantt";
|
|
58
|
+
import "@art-tools/react-gantt/style.css";
|
|
59
|
+
|
|
60
|
+
// Build dates with the (year, monthIndex, day) constructor, never an ISO string —
|
|
61
|
+
// `new Date("2023-01-10")` parses as UTC midnight while the geometry reads local
|
|
62
|
+
// civil instants. `endDate` is EXCLUSIVE: "Install Apache" occupies Jan 10 alone.
|
|
63
|
+
const tasks: GanttTask[] = [
|
|
64
|
+
{
|
|
65
|
+
id: 1000,
|
|
66
|
+
name: "Launch Cloud Platform",
|
|
67
|
+
startDate: new Date(2023, 0, 10),
|
|
68
|
+
endDate: new Date(2023, 0, 22),
|
|
69
|
+
type: "summary",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 1,
|
|
73
|
+
name: "Setup web server",
|
|
74
|
+
startDate: new Date(2023, 0, 10),
|
|
75
|
+
endDate: new Date(2023, 0, 14),
|
|
76
|
+
progress: 33,
|
|
77
|
+
parentId: 1000,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 11,
|
|
81
|
+
name: "Install Apache",
|
|
82
|
+
startDate: new Date(2023, 0, 10),
|
|
83
|
+
endDate: new Date(2023, 0, 11),
|
|
84
|
+
progress: 50,
|
|
85
|
+
parentId: 1,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 12,
|
|
89
|
+
name: "Configure firewall",
|
|
90
|
+
startDate: new Date(2023, 0, 10),
|
|
91
|
+
endDate: new Date(2023, 0, 12),
|
|
92
|
+
progress: 50,
|
|
93
|
+
parentId: 1,
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
export function App() {
|
|
98
|
+
return <Gantt tasks={tasks} height={500} colWidth={60} rowHeight={40} />;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`height` is **required** — when set, rows scroll vertically inside it while the
|
|
103
|
+
calendar header stays pinned.
|
|
104
|
+
|
|
105
|
+
> The `tasks` prop is a **stable seed**: pass it once and never feed a resolved list
|
|
106
|
+
> back into it. All create/update/delete/edit go through the internal change log via
|
|
107
|
+
> the [imperative API](#imperative-api) and action columns, keeping undo/redo intact.
|
|
108
|
+
> See [`docs/data-structures.md`](./docs/data-structures.md) for the full model.
|
|
109
|
+
|
|
110
|
+
A complete, interactive example (10,000 tasks, custom slots, edit modal, undo/redo)
|
|
111
|
+
lives in [`apps/playground/src/App.tsx`](../../apps/playground/src/App.tsx).
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Composable API
|
|
116
|
+
|
|
117
|
+
`<Gantt>` is a thin wrapper that wires the provider and lays out a resizable split
|
|
118
|
+
view (task list pane + calendar/grid). For full layout control, compose the pieces
|
|
119
|
+
directly:
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
import { GanttProvider, TaskList, GanttGrid } from "@art-tools/react-gantt";
|
|
123
|
+
|
|
124
|
+
<GanttProvider tasks={tasks} height={500}>
|
|
125
|
+
<TaskList />
|
|
126
|
+
<GanttGrid />
|
|
127
|
+
</GanttProvider>;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Set `hideTaskList` on `<Gantt>` to render only the calendar/grid (no task-list pane,
|
|
131
|
+
no splitter).
|
|
132
|
+
|
|
133
|
+
Exported components: `Gantt`, `GanttProvider`, `GanttGrid`, `TaskList`, `TaskBar`,
|
|
134
|
+
`ProjectBar`, `MilestoneBar`, `Calendar`.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## `GanttProps` reference
|
|
139
|
+
|
|
140
|
+
Defined in [`src/types.ts`](./src/types.ts).
|
|
141
|
+
|
|
142
|
+
### Data
|
|
143
|
+
|
|
144
|
+
| Prop | Type | Description |
|
|
145
|
+
| --------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
146
|
+
| `tasks` | `GanttTask[]` | **Required.** Stable seed list (see the note in Quick start). |
|
|
147
|
+
| `dependencies` | `TaskDependency[]` | Links between tasks (FS/FF/SS/SF, optional lag). |
|
|
148
|
+
| `columns` | `ColumnDef[]` | Task-list columns. Falls back to built-in default columns. |
|
|
149
|
+
| `readOnly` | `boolean` | Remove every editing affordance — see [Read-only](#read-only). |
|
|
150
|
+
| `calendar` | `GanttCalendar` | Working-time definition. Supplying it opts into working-time scheduling — see [Working time](#working-time-calendars). |
|
|
151
|
+
| `snapToWorking` | `boolean` | Default `true`. `false` keeps non-working shading but leaves dates untouched. |
|
|
152
|
+
| `durationUnit` | `"day" \| "hour" \| "minute"` | How an input `duration` is interpreted and displayed. Default `"day"`. |
|
|
153
|
+
|
|
154
|
+
### Layout
|
|
155
|
+
|
|
156
|
+
| Prop | Type | Description |
|
|
157
|
+
| ---------------------- | --------- | ---------------------------------------------------------------------------------------------- |
|
|
158
|
+
| `height` | `number` | **Required.** Total component height in px; enables the pinned header + vertical scroll. |
|
|
159
|
+
| `rowHeight` | `number` | Row height in px. |
|
|
160
|
+
| `colWidth` | `number` | Width of one day column in px. |
|
|
161
|
+
| `scales` | `Scale[]` | Calendar header rows (defaults to month + day — see [`DEFAULT_SCALES`](./src/core/scales.ts)). |
|
|
162
|
+
| `padDays` | `number` | Extra day columns padded before/after the task date range. |
|
|
163
|
+
| `defaultTaskListWidth` | `number` | Initial width of the task-list pane. |
|
|
164
|
+
| `hideTaskList` | `boolean` | Render only the calendar/grid. |
|
|
165
|
+
|
|
166
|
+
### Callbacks
|
|
167
|
+
|
|
168
|
+
| Prop | Signature | Fired when |
|
|
169
|
+
| -------------------- | ------------------------------- | --------------------------------------------------------------- |
|
|
170
|
+
| `onTaskClick` | `(task) => void` | A row/bar is selected. |
|
|
171
|
+
| `onDependencyCreate` | `(dep: TaskDependency) => void` | A link is drawn between two tasks. |
|
|
172
|
+
| `onDependencyDelete` | `(dep: TaskDependency) => void` | A link is deleted. |
|
|
173
|
+
| `onTaskCreate` | `(task, afterId?) => void` | A task is created. |
|
|
174
|
+
| `onTaskDelete` | `(id: Id) => void` | A task is deleted. |
|
|
175
|
+
| `onTaskEdit` | `(task) => void` | A column's edit action fires (e.g. the actions-column pencil). |
|
|
176
|
+
| `onTasksChange` | `(tasks: GanttTask[]) => void` | The resolved list changes (after create/delete/edit/undo/redo). |
|
|
177
|
+
|
|
178
|
+
### Other
|
|
179
|
+
|
|
180
|
+
| Prop | Type | Description |
|
|
181
|
+
| ----------------- | ------------------------ | ----------------------------------------------------------------------------------- |
|
|
182
|
+
| `apiRef` | `React.Ref<GanttHandle>` | The [imperative API](#imperative-api) handle. |
|
|
183
|
+
| `taskList` | `GanttTaskListSlots` | Slot overrides for the task-list pane (`treeCell`, `header`). |
|
|
184
|
+
| `bars` | `GanttBarsSlots` | Slot overrides for timeline bars and their handles. |
|
|
185
|
+
| `dependencySlots` | `GanttDependenciesSlots` | Slot overrides for dependency links (named to avoid colliding with `dependencies`). |
|
|
186
|
+
| `timeline` | `GanttTimelineSlots` | Slot overrides for the calendar/grid chrome. |
|
|
187
|
+
| `labels` | `GanttLabels` | Overrides for the [accessible strings](#accessibility). Pass a stable object. |
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Data model
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
interface GanttTask {
|
|
195
|
+
id: Id; // string | number
|
|
196
|
+
name: string;
|
|
197
|
+
startDate: Date;
|
|
198
|
+
endDate?: Date; // EXCLUSIVE — the instant work stops
|
|
199
|
+
duration?: number;
|
|
200
|
+
progress?: number; // 0–100
|
|
201
|
+
type?: "task" | "milestone" | "summary"; // default: "task"
|
|
202
|
+
parentId?: Id | null; // null/undefined = root
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
- **`Id`** — `string | number`.
|
|
207
|
+
- **`endDate` is exclusive** — it is the instant work _stops_, not the last day
|
|
208
|
+
worked. A task running Monday through Friday is
|
|
209
|
+
`{ startDate: Mon, endDate: Sat }`, and a 9-to-5 Friday task is
|
|
210
|
+
`Fri 09:00 → Fri 17:00`. This is what makes interval arithmetic work without
|
|
211
|
+
scattered ±1 day corrections. To show a user the inclusive last day, use
|
|
212
|
+
`api.format.endDate(task)` inside a column, or the exported `displayEndDate`
|
|
213
|
+
/ `endInstantFromDisplayDate` helpers when bridging a date input.
|
|
214
|
+
- **`duration`** — interpreted in the chart's `durationUnit` and, when a
|
|
215
|
+
`calendar` is set, counted in _working_ time. The library never writes this
|
|
216
|
+
field back; it derives dates from it and leaves your data alone.
|
|
217
|
+
- **`type`** — `"task"` (default), `"milestone"` (a diamond at `startDate`), or
|
|
218
|
+
`"summary"` (a parent whose dates and progress roll up from its children). See
|
|
219
|
+
[Task bars](#task-bars).
|
|
220
|
+
- **Hierarchy** — established via `parentId`. Roots have no `parentId`.
|
|
221
|
+
|
|
222
|
+
Dependencies are a separate array:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
type TaskDependencyType = "FS" | "FF" | "SS" | "SF";
|
|
226
|
+
|
|
227
|
+
type TaskDependency = {
|
|
228
|
+
from: Id;
|
|
229
|
+
to: Id;
|
|
230
|
+
type: TaskDependencyType;
|
|
231
|
+
lag?: number; // in `durationUnit`s; WORKING time when a calendar is set
|
|
232
|
+
};
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
For the full seed → change-log → resolved-list pipeline (transactions, cursor,
|
|
236
|
+
roll-up), see [`docs/data-structures.md`](./docs/data-structures.md).
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Working time (calendars)
|
|
241
|
+
|
|
242
|
+
By default the chart schedules in plain linear time: weekends are shaded but a
|
|
243
|
+
five-day task dragged onto a Thursday simply ends on Monday. Pass a `calendar` to
|
|
244
|
+
make non-working time real — for the scheduler, for drag, and for the dependency
|
|
245
|
+
cascade.
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
<Gantt
|
|
249
|
+
tasks={tasks}
|
|
250
|
+
height={480}
|
|
251
|
+
calendar={{
|
|
252
|
+
hours: ["8:00-12:00", "13:00-17:00"], // lunch is the gap between ranges
|
|
253
|
+
days: {
|
|
254
|
+
0: false,
|
|
255
|
+
6: false, // weekends off (0 = Sunday)
|
|
256
|
+
5: ["8:00-12:00"], // short Friday
|
|
257
|
+
},
|
|
258
|
+
dates: {
|
|
259
|
+
"2026-01-01": false, // holiday
|
|
260
|
+
"2026-01-10": ["9:00-13:00"], // half day
|
|
261
|
+
},
|
|
262
|
+
}}
|
|
263
|
+
/>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Three scopes resolve in the order **`dates` → `days` → `hours`**, so a specific
|
|
267
|
+
date beats a weekday rule, which beats the global default.
|
|
268
|
+
|
|
269
|
+
| Prop | Meaning |
|
|
270
|
+
| --------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
271
|
+
| `calendar` | The working-time definition. Supplying it _is_ the opt-in. Safe to write inline — it is keyed by content, not identity. |
|
|
272
|
+
| `snapToWorking` | Default `true`. Set `false` to keep the shading but leave dates untouched. |
|
|
273
|
+
| `durationUnit` | `"day"` (default), `"hour"`, or `"minute"` — how an input `duration` is read and displayed. |
|
|
274
|
+
|
|
275
|
+
Things worth knowing before you rely on it:
|
|
276
|
+
|
|
277
|
+
- **Omitting `hours` means whole days, not business hours.** A calendar that only
|
|
278
|
+
marks weekends off stays day-granular, so `duration: 3` is still three whole
|
|
279
|
+
days rather than three 8-hour shifts.
|
|
280
|
+
- **A day off is just a day with no hours** (`false`), so working _days_ are the
|
|
281
|
+
degenerate case of working _time_ — there is no separate concept.
|
|
282
|
+
- **One `durationUnit: "day"` is the week's longest working day.** With
|
|
283
|
+
`{ hours: ["8:00-17:00"] }` that is 9 hours. Adding a single longer weekday
|
|
284
|
+
therefore redefines "a day" for the whole chart.
|
|
285
|
+
- **Your data is never rewritten.** Snapping applies only to dates the library
|
|
286
|
+
authors — drag commits and cascade results. A task you author ending on a
|
|
287
|
+
Sunday renders where you put it until it is first edited.
|
|
288
|
+
- **Moves preserve working time, resizes set it.** Drag a three-working-day task
|
|
289
|
+
onto a Thursday and it stays three working days, growing visually across the
|
|
290
|
+
weekend. Drag its edge onto a Sunday and it settles back onto Friday.
|
|
291
|
+
- **Non-working time is shaded, not compressed.** The time axis stays linear.
|
|
292
|
+
Slot consumers get `isNonWorking` and `nonWorkingReason`
|
|
293
|
+
(`"weekend" | "holiday" | "offHours"`) on the grid-column and calendar-cell
|
|
294
|
+
ownerStates.
|
|
295
|
+
- **Known gap:** the actions column's "add after" button creates a task without
|
|
296
|
+
snapping it, because a column's `render` has no access to the calendar.
|
|
297
|
+
|
|
298
|
+
The reasoning behind each of these — including what was rejected — is recorded in
|
|
299
|
+
[`docs/adr/`](../../docs/adr/README.md).
|
|
300
|
+
|
|
301
|
+
## Task bars
|
|
302
|
+
|
|
303
|
+
`Bar` ([`src/components/bars/common/Bar.tsx`](./src/components/bars/common/Bar.tsx))
|
|
304
|
+
computes each row's pixel geometry and dispatches on `type`:
|
|
305
|
+
|
|
306
|
+
- **`task`** → `TaskBar` — draggable, resizable, with a progress fill and label.
|
|
307
|
+
- **`milestone`** → `MilestoneBar` — a diamond marker, movable.
|
|
308
|
+
- **`summary`** → `ProjectBar` — a rolled-up parent bar, movable.
|
|
309
|
+
|
|
310
|
+
**Summary roll-up:** only `summary`-typed parents roll up
|
|
311
|
+
(`getParentTaskData` in [`src/core/prepareData.ts`](./src/core/prepareData.ts)):
|
|
312
|
+
`startDate` = min child start, `endDate` = max child end, `progress` = weighted mean
|
|
313
|
+
of non-milestone children. A parent of any other `type` is left exactly as authored.
|
|
314
|
+
|
|
315
|
+
> Note: the `summary` bar is still implemented by the component/file named
|
|
316
|
+
> `ProjectBar`, and its theme variables are `--am-gantt-project-*` (the `deaf0b7`
|
|
317
|
+
> rename covered the public task `type` value only).
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Dependencies & scheduling
|
|
322
|
+
|
|
323
|
+
Hover a bar to reveal start/end **connector handles**
|
|
324
|
+
([`ConnectorHandles`](./src/components/bars/common/ConnectorHandles.tsx)); drag from
|
|
325
|
+
one bar's handle to another to create a link. The start/end handle combination maps
|
|
326
|
+
to the four dependency types (`HANDLE_TO_TYPE` in
|
|
327
|
+
[`src/hooks/useDependencyDrag.ts`](./src/hooks/useDependencyDrag.ts)):
|
|
328
|
+
Finish-to-Start, Finish-to-Finish, Start-to-Start, Start-to-Finish. Links render as
|
|
329
|
+
SVG polylines with arrowheads, a hit-area for selection, a delete button, and a lag
|
|
330
|
+
label ([`DependencyLinks`](./src/components/dependency-links/DependencyLinks.tsx));
|
|
331
|
+
they're culled to the visible rect.
|
|
332
|
+
|
|
333
|
+
**Cascading reschedule (ASAP):** when a task moves,
|
|
334
|
+
[`scheduleDependents`](./src/core/scheduling.ts) walks the dependency graph and
|
|
335
|
+
realigns successors according to each relationship type and its `lag`. The move and
|
|
336
|
+
all cascaded updates are committed as **one transaction**, so a drag-plus-cascade
|
|
337
|
+
undoes in a single step.
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Columns
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
interface ColumnDef<T extends GanttTask = GanttTask> {
|
|
345
|
+
key: string;
|
|
346
|
+
header: string;
|
|
347
|
+
width?: number;
|
|
348
|
+
render: (task: T, api: ColumnApi) => React.ReactNode;
|
|
349
|
+
isTreeColumn?: boolean; // renders the indent + expand/collapse toggle
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Every `render` receives a `ColumnApi` as its second argument — the full
|
|
354
|
+
[imperative handle](#imperative-api) plus `editTask(task)` (which fires the
|
|
355
|
+
consumer's `onTaskEdit`). This is how columns build inline actions:
|
|
356
|
+
|
|
357
|
+
```tsx
|
|
358
|
+
const columns: ColumnDef[] = [
|
|
359
|
+
{ key: "name", header: "Name", isTreeColumn: true, render: (t) => t.name },
|
|
360
|
+
{
|
|
361
|
+
key: "actions",
|
|
362
|
+
header: "",
|
|
363
|
+
render: (task, api) => (
|
|
364
|
+
<>
|
|
365
|
+
<button onClick={() => api.editTask(task)}>✎</button>
|
|
366
|
+
<button onClick={() => api.deleteTask(task.id)}>✖</button>
|
|
367
|
+
</>
|
|
368
|
+
),
|
|
369
|
+
},
|
|
370
|
+
];
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
When `columns` is omitted, `TaskList` renders `DEFAULT_COLUMNS`
|
|
374
|
+
([`src/components/taskList/TaskListHeader.tsx`](./src/components/taskList/TaskListHeader.tsx)):
|
|
375
|
+
an `__action` column (edit / add-after / delete), a `__name` tree column, `__start`,
|
|
376
|
+
`__end`, and `__progress`. Columns are resizable via a header divider (widths are
|
|
377
|
+
tracked as session-local overrides).
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Read-only
|
|
382
|
+
|
|
383
|
+
```tsx
|
|
384
|
+
<Gantt tasks={tasks} height={500} readOnly />
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
`readOnly` removes every editing affordance rather than disabling one:
|
|
388
|
+
|
|
389
|
+
| Gone | Kept |
|
|
390
|
+
| ---------------------------------------------- | --------------------------------------------------------- |
|
|
391
|
+
| Bar move, resize, progress drag | Row/bar selection, `onTaskClick` |
|
|
392
|
+
| Dependency connector handles | Dependency links themselves (drawn as usual) |
|
|
393
|
+
| Link selection + its `×` / Delete-key deletion | Expand/collapse, scroll, zoom, column resize |
|
|
394
|
+
| The built-in `__action` column | The `__name` / `__start` / `__end` / `__progress` columns |
|
|
395
|
+
|
|
396
|
+
Nothing is rendered-but-inert: each affordance only exists when its handlers are
|
|
397
|
+
wired, so there is no grabbable dead element and no disabled styling.
|
|
398
|
+
|
|
399
|
+
`apiRef` keeps working — `readOnly` is about the pointer, not the data ([ADR-021](../../docs/adr/021-readonly-removes-affordances-not-the-api.md)).
|
|
400
|
+
Drive an otherwise-frozen chart from your own toolbar:
|
|
401
|
+
|
|
402
|
+
```tsx
|
|
403
|
+
<Gantt tasks={tasks} height={500} readOnly apiRef={ref} />;
|
|
404
|
+
ref.current?.updateTask(id, { progress: 80 }); // still applies
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Custom `columns` are yours to gate — `render` receives `api.readOnly`:
|
|
408
|
+
|
|
409
|
+
```tsx
|
|
410
|
+
render: (task, api) =>
|
|
411
|
+
api.readOnly ? null : <button onClick={() => api.editTask(task)}>✎</button>,
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
`GanttProvider` takes the same prop, so the [composable API](#composable-api) behaves
|
|
415
|
+
identically.
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## Imperative API
|
|
420
|
+
|
|
421
|
+
Pass an `apiRef` to reach the `GanttHandle`:
|
|
422
|
+
|
|
423
|
+
```tsx
|
|
424
|
+
const ref = useRef<GanttHandle>(null);
|
|
425
|
+
<Gantt apiRef={ref} tasks={tasks} height={500} />;
|
|
426
|
+
|
|
427
|
+
ref.current?.createTask(task, afterId); // afterId omitted → append at end
|
|
428
|
+
ref.current?.updateTask(id, { progress: 80 });
|
|
429
|
+
ref.current?.deleteTask(id);
|
|
430
|
+
ref.current?.undo();
|
|
431
|
+
ref.current?.redo();
|
|
432
|
+
ref.current?.revealTask(id); // vertical; add { horizontal: true } for the bar
|
|
433
|
+
ref.current?.revealTask(id, { horizontal: true }); // expands collapsed ancestors first
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
`updateTask` takes a `TaskPatch` (`name`, `startDate`, `endDate`, `progress`); only
|
|
437
|
+
the provided fields change. `createTask` runs synchronously (`flushSync`) so the new
|
|
438
|
+
task is visible to consumers immediately.
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## Slots & theming
|
|
443
|
+
|
|
444
|
+
### CSS custom properties
|
|
445
|
+
|
|
446
|
+
The default look is driven by `--am-gantt-*` variables in
|
|
447
|
+
[`src/index.css`](./src/index.css). Override them in your own CSS to retheme:
|
|
448
|
+
|
|
449
|
+
```css
|
|
450
|
+
:root {
|
|
451
|
+
--am-gantt-task-bg: #0ba5ff; /* task bar fill */
|
|
452
|
+
--am-gantt-project-bg: #16a34a; /* summary bar fill */
|
|
453
|
+
--am-gantt-milestone-bg: #f59e0b; /* milestone diamond */
|
|
454
|
+
--am-gantt-calendar-header-bg: #f8fafc;
|
|
455
|
+
--am-gantt-calendar-weekend-bg: #f1f5f9;
|
|
456
|
+
/* …plus task colors, border radii, resizer sizing, z-indices */
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### Slots
|
|
461
|
+
|
|
462
|
+
Every customizable component follows the MUI `{ slots, slotProps }` pattern with an
|
|
463
|
+
`ownerState` function form. `slots` swaps the underlying element/component; `slotProps`
|
|
464
|
+
merges props onto the library's defaults — `className` is `clsx`-merged, `style` is
|
|
465
|
+
shallow-merged, and any other prop the consumer sets wins
|
|
466
|
+
([`mergeSlotProps`](./src/core/slots.ts)).
|
|
467
|
+
|
|
468
|
+
The `<Gantt>` props group slots into four buckets: **`taskList`** (`treeCell`,
|
|
469
|
+
`header`), **`bars`** (`taskBar`, `projectBar`, `milestoneBar`, progress, resizer,
|
|
470
|
+
connector handles), **`dependencySlots`** (links, preview), and **`timeline`**
|
|
471
|
+
(calendar rows, grid columns, grid, resize handle).
|
|
472
|
+
|
|
473
|
+
```tsx
|
|
474
|
+
// Swap the tree-cell expand button for a custom component, and set its glyph
|
|
475
|
+
// from ownerState. Keep the config object referentially stable (module-level or
|
|
476
|
+
// memoized) — rows are memoized, so a fresh object each render re-renders them all.
|
|
477
|
+
const taskListSlots: GanttTaskListSlots = {
|
|
478
|
+
treeCell: {
|
|
479
|
+
slots: { expandButton: RoundToggle },
|
|
480
|
+
slotProps: {
|
|
481
|
+
expandButton: ({ isExpanded }) => ({ children: isExpanded ? "−" : "+" }),
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// Restyle task bars via the `root` slot — merged, not replaced.
|
|
487
|
+
const barSlots: GanttBarsSlots = {
|
|
488
|
+
taskBar: { slotProps: { root: { style: { borderRadius: 8 } } } },
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
<Gantt tasks={tasks} height={500} taskList={taskListSlots} bars={barSlots} />;
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Per-component slot types (`*Slots`, `*SlotProps`, `*SlotConfig`, `*OwnerState`) are
|
|
495
|
+
all exported from the package entry.
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## Accessibility
|
|
500
|
+
|
|
501
|
+
The chart is fully readable by a screen reader in browse / table-navigation mode. It is
|
|
502
|
+
**not yet keyboard-operable** — see [Roadmap](#1-keyboard-navigation).
|
|
503
|
+
|
|
504
|
+
### Structure
|
|
505
|
+
|
|
506
|
+
The two panes are exposed as two widgets under one labelled `group`:
|
|
507
|
+
|
|
508
|
+
| Element | Role & state |
|
|
509
|
+
| -------------- | -------------------------------------------------------------------------------------------------------- |
|
|
510
|
+
| Widget root | `group` + `aria-label` (`labels.gantt`) |
|
|
511
|
+
| Task-list pane | `treegrid` + `aria-label`, `aria-rowcount`, `aria-colcount` |
|
|
512
|
+
| Header row | `row` `aria-rowindex="1"`, cells `columnheader` + `aria-colindex` |
|
|
513
|
+
| Task row | `row` + `aria-rowindex`, `aria-level`, `aria-expanded`, `aria-posinset`, `aria-setsize`, `aria-selected` |
|
|
514
|
+
| Task cells | `gridcell`, or `rowheader` for the tree column; each with `aria-colindex` |
|
|
515
|
+
| Timeline pane | `grid` + `aria-label`, `aria-rowcount`, `aria-colcount` (one column per date) |
|
|
516
|
+
| Calendar row | `row` + `aria-rowindex`, cells `columnheader` + `aria-colindex`/`aria-colspan` |
|
|
517
|
+
| Bar | `gridcell` + `aria-label`, `aria-colindex`/`aria-colspan` for its span on the date axis |
|
|
518
|
+
|
|
519
|
+
Both panes are virtualized, so `aria-rowcount` reports the **full** list while only a
|
|
520
|
+
window is in the DOM, and every `aria-rowindex` is absolute. Counts and indices are
|
|
521
|
+
1-based, and the timeline's task rows are offset by its calendar header rows.
|
|
522
|
+
|
|
523
|
+
Two deliberate choices are worth knowing about:
|
|
524
|
+
|
|
525
|
+
- **A bar is one announcement.** Its `aria-label` carries name, type, dates and progress
|
|
526
|
+
(`"Design phase, summary, 3 Mar 2026 to 12 Mar 2026, 40% complete"`), and its inner
|
|
527
|
+
subtree is `aria-hidden`. So a bar reads as one coherent unit instead of a pile of
|
|
528
|
+
nested `div`s — and the timeline stays usable on its own when `hideTaskList` is set.
|
|
529
|
+
- **Pointer-only affordances are hidden.** Bar move, resize, progress and dependency
|
|
530
|
+
creation are mouse-only drags, so the connector handles, resize grips, progress grip
|
|
531
|
+
and dependency-link layer are `aria-hidden` with `tabIndex={-1}`. They are not
|
|
532
|
+
advertised as controls that no key can activate. Tab visits only the tree expand
|
|
533
|
+
toggles and whatever buttons your columns render. Restore any of them through
|
|
534
|
+
`slotProps` if you wire up your own keyboard handling.
|
|
535
|
+
|
|
536
|
+
Calendar headers announce the full period rather than the abbreviated visible text
|
|
537
|
+
(`"31 December 2021"`, not `"31"`). Override per scale with `Scale.ariaFormat`.
|
|
538
|
+
|
|
539
|
+
### Labels
|
|
540
|
+
|
|
541
|
+
Every accessible string is overridable — pass a **stable** (memoized) object, since rows
|
|
542
|
+
and bars are memoized:
|
|
543
|
+
|
|
544
|
+
```tsx
|
|
545
|
+
const labels = useMemo(
|
|
546
|
+
() => ({
|
|
547
|
+
gantt: "Projektplan",
|
|
548
|
+
taskList: "Aufgabenliste",
|
|
549
|
+
timeline: "Zeitachse",
|
|
550
|
+
expand: "Aufklappen",
|
|
551
|
+
collapse: "Zuklappen",
|
|
552
|
+
editTask: (task) => `${task.name} bearbeiten`,
|
|
553
|
+
bar: (task, { progress }) => `${task.name}, ${progress}% erledigt`,
|
|
554
|
+
}),
|
|
555
|
+
[],
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
<Gantt tasks={tasks} height={400} labels={labels} />;
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
Omitted keys keep their English defaults. `ColumnDef.render` receives the resolved set as
|
|
562
|
+
`api.labels`, for naming controls a custom column renders.
|
|
563
|
+
|
|
564
|
+
### Focus ring
|
|
565
|
+
|
|
566
|
+
The one keyboard-focusable control the library owns (the tree expand toggle) draws a
|
|
567
|
+
focus ring on `:focus-visible`, themeable via `--am-gantt-focus-ring-color`, `-width`
|
|
568
|
+
and `-offset`.
|
|
569
|
+
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
## Architecture (for contributors)
|
|
573
|
+
|
|
574
|
+
- **Frequency-split contexts** —
|
|
575
|
+
[`src/context/GanttContext.tsx`](./src/context/GanttContext.tsx) deliberately splits
|
|
576
|
+
state into ~9 purpose-scoped contexts/hooks by update frequency (`useGanttConfig`,
|
|
577
|
+
`useGanttTaskState`, `useGanttTaskActions`, `useGanttSelectedId`, `useGanttScroll`,
|
|
578
|
+
`useGanttViewport`, `useGanttDependency`, `useGanttDragActive`,
|
|
579
|
+
`useGanttDependencyDrag`), so a high-frequency update (e.g. drag coordinates)
|
|
580
|
+
doesn't re-render stable consumers. Grid-side slot groups flow through a separate
|
|
581
|
+
[`GanttSlotsProvider`](./src/context/GanttSlotsContext.tsx); the `taskList` group is
|
|
582
|
+
prop-drilled.
|
|
583
|
+
- **Mutation model** — [`src/hooks/useTaskList.ts`](./src/hooks/useTaskList.ts) owns an
|
|
584
|
+
insertion-ordered `ChangeLog` of transactions with a cursor for undo/redo. Each user
|
|
585
|
+
action is one transaction. An incremental resolve cache
|
|
586
|
+
(`resolveCommittedTasksCached` in [`src/core/prepareData.ts`](./src/core/prepareData.ts))
|
|
587
|
+
backed by an [LRU cache](./src/core/lruCache.ts) keeps resolution fast regardless of
|
|
588
|
+
history length.
|
|
589
|
+
- **Virtualization** — [`src/core/virtualize.ts`](./src/core/virtualize.ts)
|
|
590
|
+
(`rangeFromOffset`) windows both rows and date columns with overscan (see
|
|
591
|
+
[`src/core/constants.ts`](./src/core/constants.ts)); applied in `Grid` and `TaskList`,
|
|
592
|
+
and dependency links are culled to the visible rect.
|
|
593
|
+
- **Scroll sync** — [`useScrollSync`](./src/hooks/useScrollSync.ts) keeps the list and
|
|
594
|
+
grid aligned vertically; [`useScrollToTask`](./src/hooks/useScrollToTask.ts) +
|
|
595
|
+
[`core/scroll.ts`](./src/core/scroll.ts) handle reveal-into-view.
|
|
596
|
+
|
|
597
|
+
### Styling & build
|
|
598
|
+
|
|
599
|
+
- **CSS Modules** (`*.module.css`) co-located with each component; theming via the
|
|
600
|
+
`--am-gantt-*` custom properties in `index.css`.
|
|
601
|
+
- **Build** — Vite library mode emits ESM (`index.mjs`) + CJS (`index.cjs`) with
|
|
602
|
+
`.d.ts` (via `vite-plugin-dts`) and a single `style.css`; `react`/`react-dom` are
|
|
603
|
+
externalized.
|
|
604
|
+
- **Tests** — Vitest + `@testing-library/react` (jsdom) under `src/tests/` and
|
|
605
|
+
`test/`; benchmarks (`vitest bench`) live in the playground.
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
## Roadmap
|
|
610
|
+
|
|
611
|
+
Proposed future features. These are **not yet implemented** — they capture gaps in the
|
|
612
|
+
current design and a sketch of how each would hook in.
|
|
613
|
+
|
|
614
|
+
### 1. Critical path
|
|
615
|
+
|
|
616
|
+
The scheduling engine already builds the dependency graph
|
|
617
|
+
(`buildDependencyGraph` in [`src/core/scheduling.ts`](./src/core/scheduling.ts)).
|
|
618
|
+
Propose layering CPM (critical path method) analysis on top — compute the longest
|
|
619
|
+
zero-slack chain, expose a `highlightCriticalPath` option, and add slot hooks / an
|
|
620
|
+
`ownerState` flag so critical bars and links can be styled distinctly.
|
|
621
|
+
|
|
622
|
+
### 2. Export / print
|
|
623
|
+
|
|
624
|
+
Propose export of the chart to PNG/SVG/PDF, plus a print-friendly render mode that
|
|
625
|
+
temporarily disables virtualization and renders the full extent so browser print
|
|
626
|
+
captures every row.
|