@ganttloom/gantt-core 0.2.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 ADDED
@@ -0,0 +1,3268 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DEFAULT_PALETTE: () => DEFAULT_PALETTE,
24
+ DEFAULT_THEME: () => DEFAULT_THEME,
25
+ DENSITY_PRESETS: () => DENSITY_PRESETS,
26
+ EventEmitter: () => EventEmitter,
27
+ GanttChart: () => GanttChart,
28
+ HistoryManager: () => HistoryManager,
29
+ applyColorByField: () => applyColorByField,
30
+ applyConstraint: () => applyConstraint,
31
+ colorByField: () => colorByField,
32
+ computeCriticalPath: () => computeCriticalPath,
33
+ computeLayout: () => computeLayout,
34
+ computeResourceHistogram: () => computeResourceHistogram,
35
+ computeWBSCodes: () => computeWBSCodes,
36
+ dateUtils: () => date_utils_exports,
37
+ filterTasks: () => filterTasks,
38
+ isHoliday: () => isHoliday,
39
+ isSafeHref: () => isSafeHref,
40
+ isWithinWorkingHours: () => isWithinWorkingHours,
41
+ isWorkingDay: () => isWorkingDay,
42
+ isWorkingTime: () => isWorkingTime,
43
+ levelResources: () => levelResources,
44
+ mergeTheme: () => mergeTheme,
45
+ nextWorkingDay: () => nextWorkingDay,
46
+ previousWorkingDay: () => previousWorkingDay,
47
+ renderResourceHistogramSVG: () => renderResourceHistogramSVG,
48
+ shiftToWorkingDay: () => shiftToWorkingDay,
49
+ shiftToWorkingTime: () => shiftToWorkingTime,
50
+ tasksFromCSV: () => tasksFromCSV,
51
+ tasksToCSV: () => tasksToCSV,
52
+ themeToCssVars: () => themeToCssVars
53
+ });
54
+ module.exports = __toCommonJS(index_exports);
55
+
56
+ // src/theme.ts
57
+ var DEFAULT_THEME = {
58
+ rowHeight: 36,
59
+ barHeight: 22,
60
+ headerHeight: 50,
61
+ gridColor: "#e5e7eb",
62
+ weekendColor: "#f3f4f6",
63
+ todayColor: "#ef4444",
64
+ barColor: "#3b82f6",
65
+ barProgressColor: "#2563eb",
66
+ groupBarColor: "#64748b",
67
+ linkColor: "#94a3b8",
68
+ criticalColor: "#dc2626",
69
+ baselineColor: "#9ca3af",
70
+ deadlineColor: "#f59e0b",
71
+ markerColor: "#8b5cf6",
72
+ selectionColor: "#0ea5e9",
73
+ textColor: "#1f2937",
74
+ backgroundColor: "#ffffff",
75
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
76
+ fontSize: 12,
77
+ borderRadius: 4
78
+ };
79
+ var DARK_THEME = {
80
+ ...DEFAULT_THEME,
81
+ gridColor: "#2d3341",
82
+ weekendColor: "#252a35",
83
+ todayColor: "#f87171",
84
+ barColor: "#60a5fa",
85
+ barProgressColor: "#3b82f6",
86
+ groupBarColor: "#94a3b8",
87
+ linkColor: "#64748b",
88
+ criticalColor: "#f87171",
89
+ baselineColor: "#4b5563",
90
+ deadlineColor: "#fbbf24",
91
+ markerColor: "#a78bfa",
92
+ selectionColor: "#38bdf8",
93
+ textColor: "#e5e7eb",
94
+ backgroundColor: "#181c24"
95
+ };
96
+ var DENSITY_PRESETS = {
97
+ compact: { rowHeight: 26, barHeight: 16, headerHeight: 38, fontSize: 11 },
98
+ comfortable: { rowHeight: 36, barHeight: 22, headerHeight: 50, fontSize: 12 },
99
+ spacious: { rowHeight: 48, barHeight: 30, headerHeight: 60, fontSize: 13 }
100
+ };
101
+ function prefersDark() {
102
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches;
103
+ }
104
+ function baseThemeForScheme(scheme = "auto") {
105
+ if (scheme === "dark") return DARK_THEME;
106
+ if (scheme === "light") return DEFAULT_THEME;
107
+ return prefersDark() ? DARK_THEME : DEFAULT_THEME;
108
+ }
109
+ function mergeTheme(partial, scheme = "light") {
110
+ const base = scheme === "light" ? DEFAULT_THEME : baseThemeForScheme(scheme);
111
+ return { ...base, ...partial };
112
+ }
113
+ var CSS_VAR_NAMES = {
114
+ rowHeight: "--gantt-row-height",
115
+ barHeight: "--gantt-bar-height",
116
+ headerHeight: "--gantt-header-height",
117
+ gridColor: "--gantt-grid-color",
118
+ weekendColor: "--gantt-weekend-color",
119
+ todayColor: "--gantt-today-color",
120
+ barColor: "--gantt-bar-color",
121
+ barProgressColor: "--gantt-bar-progress-color",
122
+ groupBarColor: "--gantt-group-bar-color",
123
+ linkColor: "--gantt-link-color",
124
+ criticalColor: "--gantt-critical-color",
125
+ baselineColor: "--gantt-baseline-color",
126
+ deadlineColor: "--gantt-deadline-color",
127
+ markerColor: "--gantt-marker-color",
128
+ selectionColor: "--gantt-selection-color",
129
+ textColor: "--gantt-text-color",
130
+ backgroundColor: "--gantt-background-color",
131
+ fontFamily: "--gantt-font-family",
132
+ fontSize: "--gantt-font-size",
133
+ borderRadius: "--gantt-border-radius"
134
+ };
135
+ var PX_FIELDS = /* @__PURE__ */ new Set([
136
+ "rowHeight",
137
+ "barHeight",
138
+ "headerHeight",
139
+ "fontSize",
140
+ "borderRadius"
141
+ ]);
142
+ function themeToCssVars(theme) {
143
+ const vars = {};
144
+ for (const key of Object.keys(CSS_VAR_NAMES)) {
145
+ const varName = CSS_VAR_NAMES[key];
146
+ const value = theme[key];
147
+ vars[varName] = PX_FIELDS.has(key) ? `${value}px` : String(value);
148
+ }
149
+ return vars;
150
+ }
151
+ function explicitThemeToCssVars(partial) {
152
+ if (!partial) return {};
153
+ const vars = {};
154
+ for (const key of Object.keys(partial)) {
155
+ const value = partial[key];
156
+ if (value === void 0) continue;
157
+ vars[CSS_VAR_NAMES[key]] = PX_FIELDS.has(key) ? `${value}px` : String(value);
158
+ }
159
+ return vars;
160
+ }
161
+ function themedFill(key, resolvedValue) {
162
+ return `var(${CSS_VAR_NAMES[key]}, ${resolvedValue})`;
163
+ }
164
+
165
+ // src/critical-path.ts
166
+ function linkKey(fromId, toId) {
167
+ return `${fromId}->${toId}`;
168
+ }
169
+ function isValidTask(task) {
170
+ return task.start instanceof Date && task.end instanceof Date && !Number.isNaN(task.start.getTime()) && !Number.isNaN(task.end.getTime());
171
+ }
172
+ function durationOf(task) {
173
+ const d = task.end.getTime() - task.start.getTime();
174
+ return d > 0 ? d : 0;
175
+ }
176
+ function computeCriticalPath(tasks, dependencies) {
177
+ const empty = { criticalTasks: /* @__PURE__ */ new Set(), criticalLinks: /* @__PURE__ */ new Set() };
178
+ const leafTasks = tasks.filter((t) => !t.isGroup && isValidTask(t));
179
+ const leafIds = new Set(leafTasks.map((t) => t.id));
180
+ if (leafTasks.length === 0) return empty;
181
+ const edges = dependencies.filter(
182
+ (d) => leafIds.has(d.fromId) && leafIds.has(d.toId) && d.fromId !== d.toId
183
+ );
184
+ const outgoing = /* @__PURE__ */ new Map();
185
+ const incoming = /* @__PURE__ */ new Map();
186
+ for (const id of leafIds) {
187
+ outgoing.set(id, []);
188
+ incoming.set(id, []);
189
+ }
190
+ for (const e of edges) {
191
+ outgoing.get(e.fromId)?.push(e);
192
+ incoming.get(e.fromId) === void 0 ? void 0 : void 0;
193
+ incoming.get(e.toId)?.push(e);
194
+ }
195
+ const inDegree = /* @__PURE__ */ new Map();
196
+ for (const id of leafIds) inDegree.set(id, incoming.get(id)?.length ?? 0);
197
+ const queue = [];
198
+ for (const id of leafIds) {
199
+ if ((inDegree.get(id) ?? 0) === 0) queue.push(id);
200
+ }
201
+ const topoOrder = [];
202
+ const maxOps = leafIds.size + edges.length + 1;
203
+ let ops = 0;
204
+ let qi = 0;
205
+ while (qi < queue.length) {
206
+ if (ops++ > maxOps) return empty;
207
+ const nodeId = queue[qi++];
208
+ if (nodeId === void 0) break;
209
+ topoOrder.push(nodeId);
210
+ for (const e of outgoing.get(nodeId) ?? []) {
211
+ const nextDeg = (inDegree.get(e.toId) ?? 0) - 1;
212
+ inDegree.set(e.toId, nextDeg);
213
+ if (nextDeg === 0) queue.push(e.toId);
214
+ }
215
+ }
216
+ if (topoOrder.length !== leafIds.size) {
217
+ return empty;
218
+ }
219
+ const taskById = new Map(leafTasks.map((t) => [t.id, t]));
220
+ const es = /* @__PURE__ */ new Map();
221
+ const ef = /* @__PURE__ */ new Map();
222
+ for (const id of topoOrder) {
223
+ const task = taskById.get(id);
224
+ if (!task) continue;
225
+ let start = 0;
226
+ for (const e of incoming.get(id) ?? []) {
227
+ const predEf = ef.get(e.fromId);
228
+ if (predEf === void 0) continue;
229
+ const lag = e.lagMs ?? 0;
230
+ start = Math.max(start, predEf + lag);
231
+ }
232
+ es.set(id, start);
233
+ ef.set(id, start + durationOf(task));
234
+ }
235
+ let projectEnd = 0;
236
+ for (const id of topoOrder) {
237
+ projectEnd = Math.max(projectEnd, ef.get(id) ?? 0);
238
+ }
239
+ const lf = /* @__PURE__ */ new Map();
240
+ const ls = /* @__PURE__ */ new Map();
241
+ for (let i = topoOrder.length - 1; i >= 0; i--) {
242
+ const id = topoOrder[i];
243
+ if (id === void 0) continue;
244
+ const task = taskById.get(id);
245
+ if (!task) continue;
246
+ const succs = outgoing.get(id) ?? [];
247
+ let finish = projectEnd;
248
+ if (succs.length > 0) {
249
+ finish = Infinity;
250
+ for (const e of succs) {
251
+ const succLs = ls.get(e.toId);
252
+ if (succLs === void 0) continue;
253
+ const lag = e.lagMs ?? 0;
254
+ finish = Math.min(finish, succLs - lag);
255
+ }
256
+ if (!Number.isFinite(finish)) finish = projectEnd;
257
+ }
258
+ lf.set(id, finish);
259
+ ls.set(id, finish - durationOf(task));
260
+ }
261
+ const EPSILON = 1;
262
+ const criticalTasks = /* @__PURE__ */ new Set();
263
+ for (const id of topoOrder) {
264
+ const slack = (ls.get(id) ?? 0) - (es.get(id) ?? 0);
265
+ if (Math.abs(slack) < EPSILON) criticalTasks.add(id);
266
+ }
267
+ const criticalLinks = /* @__PURE__ */ new Set();
268
+ for (const e of edges) {
269
+ if (!criticalTasks.has(e.fromId) || !criticalTasks.has(e.toId)) continue;
270
+ const predEf = ef.get(e.fromId) ?? 0;
271
+ const succEs = es.get(e.toId) ?? 0;
272
+ const lag = e.lagMs ?? 0;
273
+ if (Math.abs(succEs - (predEf + lag)) < EPSILON) {
274
+ criticalLinks.add(linkKey(e.fromId, e.toId));
275
+ }
276
+ }
277
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
278
+ for (const task of tasks) {
279
+ if (!task.isGroup) continue;
280
+ let hasCriticalDescendant = false;
281
+ let cur = task;
282
+ const descendantStack = [task.id];
283
+ const visited = /* @__PURE__ */ new Set();
284
+ while (descendantStack.length > 0) {
285
+ const id = descendantStack.pop();
286
+ if (id === void 0 || visited.has(id)) continue;
287
+ visited.add(id);
288
+ for (const t of tasks) {
289
+ if (t.parentId === id) {
290
+ if (criticalTasks.has(t.id)) hasCriticalDescendant = true;
291
+ descendantStack.push(t.id);
292
+ }
293
+ }
294
+ }
295
+ if (hasCriticalDescendant) criticalTasks.add(task.id);
296
+ void cur;
297
+ void taskMap;
298
+ }
299
+ return { criticalTasks, criticalLinks };
300
+ }
301
+
302
+ // src/date-utils.ts
303
+ var date_utils_exports = {};
304
+ __export(date_utils_exports, {
305
+ MS_PER_DAY: () => MS_PER_DAY,
306
+ MS_PER_HOUR: () => MS_PER_HOUR,
307
+ MS_PER_WEEK: () => MS_PER_WEEK,
308
+ addUnit: () => addUnit,
309
+ approxUnitMs: () => approxUnitMs,
310
+ dateToX: () => dateToX,
311
+ generateTicks: () => generateTicks,
312
+ isSameDay: () => isSameDay,
313
+ isValidDate: () => isValidDate,
314
+ isWeekend: () => isWeekend,
315
+ pxPerMs: () => pxPerMs,
316
+ snapToGrid: () => snapToGrid,
317
+ startOfUnit: () => startOfUnit
318
+ });
319
+
320
+ // src/calendar.ts
321
+ var DEFAULT_WORKING_DAYS = [false, true, true, true, true, true, false];
322
+ function workingDaysOf(calendar) {
323
+ return calendar?.workingDays ?? DEFAULT_WORKING_DAYS;
324
+ }
325
+ function toISODate(date) {
326
+ const y = date.getFullYear();
327
+ const m = String(date.getMonth() + 1).padStart(2, "0");
328
+ const d = String(date.getDate()).padStart(2, "0");
329
+ return `${y}-${m}-${d}`;
330
+ }
331
+ function isHoliday(date, calendar) {
332
+ if (!calendar?.holidays || calendar.holidays.length === 0) return false;
333
+ const iso = toISODate(date);
334
+ return calendar.holidays.includes(iso);
335
+ }
336
+ function isWorkingDay(date, calendar) {
337
+ const days = workingDaysOf(calendar);
338
+ const dow = date.getDay();
339
+ if (!days[dow]) return false;
340
+ return !isHoliday(date, calendar);
341
+ }
342
+ function isNonWorkingDay(date, calendar) {
343
+ return !isWorkingDay(date, calendar);
344
+ }
345
+ function nextWorkingDay(date, calendar) {
346
+ let cursor = new Date(date.getTime());
347
+ let guard = 0;
348
+ while (!isWorkingDay(cursor, calendar) && guard++ < 366) {
349
+ cursor = new Date(cursor.getTime() + MS_PER_DAY);
350
+ }
351
+ return cursor;
352
+ }
353
+ function previousWorkingDay(date, calendar) {
354
+ let cursor = new Date(date.getTime());
355
+ let guard = 0;
356
+ while (!isWorkingDay(cursor, calendar) && guard++ < 366) {
357
+ cursor = new Date(cursor.getTime() - MS_PER_DAY);
358
+ }
359
+ return cursor;
360
+ }
361
+ function shiftToWorkingDay(date, calendar, direction) {
362
+ if (!calendar) return new Date(date.getTime());
363
+ return direction === 1 ? nextWorkingDay(date, calendar) : previousWorkingDay(date, calendar);
364
+ }
365
+ function hourOfDay(date) {
366
+ return date.getHours() + date.getMinutes() / 60 + date.getSeconds() / 3600;
367
+ }
368
+ function isWithinWorkingHours(date, calendar) {
369
+ const hours = calendar?.workingHours;
370
+ if (!hours) return true;
371
+ const h = hourOfDay(date);
372
+ return h >= hours.start && h < hours.end;
373
+ }
374
+ function isWorkingTime(date, calendar) {
375
+ return isWorkingDay(date, calendar) && isWithinWorkingHours(date, calendar);
376
+ }
377
+ function applyHour(date, hour) {
378
+ const wholeHours = Math.floor(hour);
379
+ const minutes = Math.round((hour - wholeHours) * 60);
380
+ date.setHours(wholeHours, minutes, 0, 0);
381
+ }
382
+ function shiftToWorkingTime(date, calendar, direction) {
383
+ if (!calendar) return new Date(date.getTime());
384
+ const wh = calendar.workingHours;
385
+ let guard = 0;
386
+ let cursor = new Date(date.getTime());
387
+ while (guard++ < 400) {
388
+ if (!isWorkingDay(cursor, calendar)) {
389
+ cursor = direction === 1 ? nextWorkingDay(cursor, calendar) : previousWorkingDay(cursor, calendar);
390
+ if (wh) applyHour(cursor, direction === 1 ? wh.start : wh.end);
391
+ continue;
392
+ }
393
+ if (!wh) return cursor;
394
+ const h = hourOfDay(cursor);
395
+ if (h < wh.start) {
396
+ if (direction === 1) {
397
+ applyHour(cursor, wh.start);
398
+ return cursor;
399
+ }
400
+ cursor = previousWorkingDay(new Date(cursor.getTime() - MS_PER_DAY), calendar);
401
+ applyHour(cursor, wh.end);
402
+ return new Date(cursor.getTime() - 1);
403
+ }
404
+ if (h >= wh.end) {
405
+ if (direction === 1) {
406
+ cursor = new Date(cursor.getTime() + MS_PER_DAY);
407
+ applyHour(cursor, wh.start);
408
+ continue;
409
+ }
410
+ applyHour(cursor, wh.end);
411
+ return new Date(cursor.getTime() - 1);
412
+ }
413
+ return cursor;
414
+ }
415
+ return cursor;
416
+ }
417
+
418
+ // src/date-utils.ts
419
+ var MS_PER_HOUR = 60 * 60 * 1e3;
420
+ var MS_PER_DAY = 24 * MS_PER_HOUR;
421
+ var MS_PER_WEEK = 7 * MS_PER_DAY;
422
+ var WEEKDAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
423
+ var MONTH_LABELS = [
424
+ "Jan",
425
+ "Feb",
426
+ "Mar",
427
+ "Apr",
428
+ "May",
429
+ "Jun",
430
+ "Jul",
431
+ "Aug",
432
+ "Sep",
433
+ "Oct",
434
+ "Nov",
435
+ "Dec"
436
+ ];
437
+ function approxUnitMs(viewMode) {
438
+ switch (viewMode) {
439
+ case "hour":
440
+ return MS_PER_HOUR;
441
+ case "day":
442
+ return MS_PER_DAY;
443
+ case "week":
444
+ return MS_PER_WEEK;
445
+ case "month":
446
+ return 30 * MS_PER_DAY;
447
+ case "quarter":
448
+ return 91 * MS_PER_DAY;
449
+ case "year":
450
+ return 365 * MS_PER_DAY;
451
+ }
452
+ }
453
+ function pxPerMs(viewMode, columnWidth) {
454
+ return columnWidth / approxUnitMs(viewMode);
455
+ }
456
+ function dateToX(date, rangeStart, viewMode, columnWidth) {
457
+ return (date.getTime() - rangeStart.getTime()) * pxPerMs(viewMode, columnWidth);
458
+ }
459
+ function isWeekend(date) {
460
+ const day = date.getDay();
461
+ return day === 0 || day === 6;
462
+ }
463
+ function isValidDate(date) {
464
+ return date instanceof Date && !Number.isNaN(date.getTime());
465
+ }
466
+ function isSameDay(a, b) {
467
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
468
+ }
469
+ function startOfDay(date) {
470
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
471
+ }
472
+ function startOfWeek(date) {
473
+ const d = startOfDay(date);
474
+ const day = d.getDay();
475
+ const diff = (day + 6) % 7;
476
+ d.setDate(d.getDate() - diff);
477
+ return d;
478
+ }
479
+ function startOfMonth(date) {
480
+ return new Date(date.getFullYear(), date.getMonth(), 1);
481
+ }
482
+ function startOfQuarter(date) {
483
+ const q = Math.floor(date.getMonth() / 3);
484
+ return new Date(date.getFullYear(), q * 3, 1);
485
+ }
486
+ function startOfYear(date) {
487
+ return new Date(date.getFullYear(), 0, 1);
488
+ }
489
+ function startOfHour(date) {
490
+ return new Date(
491
+ date.getFullYear(),
492
+ date.getMonth(),
493
+ date.getDate(),
494
+ date.getHours()
495
+ );
496
+ }
497
+ function startOfUnit(date, viewMode) {
498
+ switch (viewMode) {
499
+ case "hour":
500
+ return startOfHour(date);
501
+ case "day":
502
+ return startOfDay(date);
503
+ case "week":
504
+ return startOfWeek(date);
505
+ case "month":
506
+ return startOfMonth(date);
507
+ case "quarter":
508
+ return startOfQuarter(date);
509
+ case "year":
510
+ return startOfYear(date);
511
+ }
512
+ }
513
+ function addUnit(date, viewMode, count) {
514
+ const d = new Date(date.getTime());
515
+ switch (viewMode) {
516
+ case "hour":
517
+ d.setHours(d.getHours() + count);
518
+ return d;
519
+ case "day":
520
+ d.setDate(d.getDate() + count);
521
+ return d;
522
+ case "week":
523
+ d.setDate(d.getDate() + count * 7);
524
+ return d;
525
+ case "month":
526
+ d.setMonth(d.getMonth() + count);
527
+ return d;
528
+ case "quarter":
529
+ d.setMonth(d.getMonth() + count * 3);
530
+ return d;
531
+ case "year":
532
+ d.setFullYear(d.getFullYear() + count);
533
+ return d;
534
+ }
535
+ }
536
+ function snapToGrid(date, viewMode) {
537
+ const floor = startOfUnit(date, viewMode);
538
+ const ceil = addUnit(floor, viewMode, 1);
539
+ const mid = (floor.getTime() + ceil.getTime()) / 2;
540
+ return date.getTime() < mid ? floor : ceil;
541
+ }
542
+ function isMajorBoundary(date, viewMode) {
543
+ switch (viewMode) {
544
+ case "hour":
545
+ return date.getHours() === 0;
546
+ case "day":
547
+ return date.getDate() === 1;
548
+ case "week":
549
+ return date.getDate() <= 7 && date.getDay() === 1;
550
+ case "month":
551
+ return date.getMonth() === 0;
552
+ case "quarter":
553
+ return date.getMonth() === 0;
554
+ case "year":
555
+ return false;
556
+ }
557
+ }
558
+ function labelFor(date, viewMode) {
559
+ switch (viewMode) {
560
+ case "hour": {
561
+ const h = date.getHours();
562
+ const period = h >= 12 ? "PM" : "AM";
563
+ const h12 = h % 12 === 0 ? 12 : h % 12;
564
+ return `${h12} ${period}`;
565
+ }
566
+ case "day":
567
+ return `${WEEKDAY_LABELS[date.getDay()]} ${date.getDate()}`;
568
+ case "week":
569
+ return `${MONTH_LABELS[date.getMonth()]} ${date.getDate()}`;
570
+ case "month":
571
+ return `${MONTH_LABELS[date.getMonth()]} ${date.getFullYear()}`;
572
+ case "quarter": {
573
+ const q = Math.floor(date.getMonth() / 3) + 1;
574
+ return `Q${q} ${date.getFullYear()}`;
575
+ }
576
+ case "year":
577
+ return `${date.getFullYear()}`;
578
+ }
579
+ }
580
+ function generateTicks(rangeStart, rangeEnd, viewMode, columnWidth, today = /* @__PURE__ */ new Date(), calendar) {
581
+ if (!isValidDate(rangeStart) || !isValidDate(rangeEnd)) return [];
582
+ const ticks = [];
583
+ let cursor = startOfUnit(rangeStart, viewMode);
584
+ const scale = pxPerMs(viewMode, columnWidth);
585
+ let guard = 0;
586
+ const MAX_TICKS = 2e3;
587
+ const dayLevel = viewMode === "day" || viewMode === "hour";
588
+ while (cursor.getTime() < rangeEnd.getTime() && guard < MAX_TICKS) {
589
+ guard++;
590
+ const x = (cursor.getTime() - rangeStart.getTime()) * scale;
591
+ ticks.push({
592
+ date: new Date(cursor.getTime()),
593
+ x,
594
+ label: labelFor(cursor, viewMode),
595
+ isWeekend: dayLevel ? isWeekend(cursor) : false,
596
+ isNonWorking: viewMode === "hour" && calendar?.workingHours ? !isWorkingTime(cursor, calendar) : dayLevel ? isNonWorkingDay(cursor, calendar) : false,
597
+ isToday: isSameDay(cursor, today) && dayLevel,
598
+ isMajorBoundary: isMajorBoundary(cursor, viewMode)
599
+ });
600
+ cursor = addUnit(cursor, viewMode, 1);
601
+ }
602
+ return ticks;
603
+ }
604
+
605
+ // src/layout.ts
606
+ function buildTree(tasks) {
607
+ const nodeById = /* @__PURE__ */ new Map();
608
+ for (const task of tasks) {
609
+ nodeById.set(task.id, { task, children: [], computedStart: null, computedEnd: null, computedProgress: 0 });
610
+ }
611
+ const roots = [];
612
+ for (const task of tasks) {
613
+ const node = nodeById.get(task.id);
614
+ if (!node) continue;
615
+ const parent = task.parentId ? nodeById.get(task.parentId) : void 0;
616
+ if (parent) {
617
+ parent.children.push(node);
618
+ } else {
619
+ roots.push(node);
620
+ }
621
+ }
622
+ return { roots, nodeById };
623
+ }
624
+ function computeSpans(node) {
625
+ for (const child of node.children) computeSpans(child);
626
+ const hasExplicit = isValidDate(node.task.start) && isValidDate(node.task.end);
627
+ if (hasExplicit) {
628
+ node.computedStart = node.task.start;
629
+ node.computedEnd = node.task.end;
630
+ return;
631
+ }
632
+ if (node.children.length === 0) {
633
+ node.computedStart = null;
634
+ node.computedEnd = null;
635
+ return;
636
+ }
637
+ let min = null;
638
+ let max = null;
639
+ for (const child of node.children) {
640
+ if (child.computedStart) {
641
+ const t = child.computedStart.getTime();
642
+ min = min === null ? t : Math.min(min, t);
643
+ }
644
+ if (child.computedEnd) {
645
+ const t = child.computedEnd.getTime();
646
+ max = max === null ? t : Math.max(max, t);
647
+ }
648
+ }
649
+ node.computedStart = min === null ? null : new Date(min);
650
+ node.computedEnd = max === null ? null : new Date(max);
651
+ }
652
+ function computeProgressRollup(node) {
653
+ for (const child of node.children) computeProgressRollup(child);
654
+ if (node.task.progress !== void 0) {
655
+ node.computedProgress = Math.min(100, Math.max(0, node.task.progress));
656
+ return;
657
+ }
658
+ if (node.children.length === 0) {
659
+ node.computedProgress = 0;
660
+ return;
661
+ }
662
+ let weightedSum = 0;
663
+ let totalWeight = 0;
664
+ for (const child of node.children) {
665
+ const duration = child.computedStart && child.computedEnd ? Math.max(1, child.computedEnd.getTime() - child.computedStart.getTime()) : 1;
666
+ weightedSum += child.computedProgress * duration;
667
+ totalWeight += duration;
668
+ }
669
+ node.computedProgress = totalWeight > 0 ? weightedSum / totalWeight : 0;
670
+ }
671
+ function flatten(roots) {
672
+ const out = [];
673
+ const stack = [];
674
+ for (let i = roots.length - 1; i >= 0; i--) {
675
+ const r = roots[i];
676
+ if (r) stack.push({ node: r, depth: 0, parentCollapsed: false });
677
+ }
678
+ while (stack.length > 0) {
679
+ const item = stack.pop();
680
+ if (!item) continue;
681
+ const { node, depth, parentCollapsed } = item;
682
+ if (parentCollapsed) continue;
683
+ out.push({ node, depth, visible: true });
684
+ const collapsedHere = !!node.task.collapsed;
685
+ for (let i = node.children.length - 1; i >= 0; i--) {
686
+ const child = node.children[i];
687
+ if (child) stack.push({ node: child, depth: depth + 1, parentCollapsed: collapsedHere });
688
+ }
689
+ }
690
+ return out;
691
+ }
692
+ function anchorPoint(bar, side) {
693
+ const y = bar.y + bar.height / 2;
694
+ return side === "start" ? { x: bar.x, y } : { x: bar.x + bar.width, y };
695
+ }
696
+ function devWarn(message) {
697
+ const isProd = typeof process !== "undefined" && process.env && process.env.NODE_ENV === "production";
698
+ if (!isProd) console.warn(`[gantt-core] ${message}`);
699
+ }
700
+ function validateInputs(tasks, dependencies) {
701
+ const ids = new Set(tasks.map((t) => t.id));
702
+ for (const dep of dependencies) {
703
+ if (!ids.has(dep.fromId) || !ids.has(dep.toId)) {
704
+ devWarn(
705
+ `dependency references a non-existent task: ${dep.fromId} -> ${dep.toId}`
706
+ );
707
+ }
708
+ }
709
+ for (const task of tasks) {
710
+ if (isValidDate(task.start) && isValidDate(task.end) && task.end < task.start) {
711
+ devWarn(`task "${task.id}" has end date before start date`);
712
+ }
713
+ }
714
+ const outgoing = /* @__PURE__ */ new Map();
715
+ for (const dep of dependencies) {
716
+ if (!ids.has(dep.fromId) || !ids.has(dep.toId)) continue;
717
+ const list = outgoing.get(dep.fromId) ?? [];
718
+ list.push(dep.toId);
719
+ outgoing.set(dep.fromId, list);
720
+ }
721
+ const indegree = /* @__PURE__ */ new Map();
722
+ for (const id of ids) indegree.set(id, 0);
723
+ for (const [, targets] of outgoing) {
724
+ for (const t of targets) indegree.set(t, (indegree.get(t) ?? 0) + 1);
725
+ }
726
+ const queue = [];
727
+ for (const [id, deg] of indegree) if (deg === 0) queue.push(id);
728
+ let visited = 0;
729
+ let qi = 0;
730
+ const maxOps = ids.size + dependencies.length + 1;
731
+ let ops = 0;
732
+ while (qi < queue.length && ops++ <= maxOps) {
733
+ const id = queue[qi++];
734
+ if (id === void 0) break;
735
+ visited++;
736
+ for (const next of outgoing.get(id) ?? []) {
737
+ const d = (indegree.get(next) ?? 0) - 1;
738
+ indegree.set(next, d);
739
+ if (d === 0) queue.push(next);
740
+ }
741
+ }
742
+ if (visited !== ids.size) {
743
+ devWarn("circular dependency detected; critical path / scheduling for affected tasks is skipped");
744
+ }
745
+ }
746
+ function linkPath(from, to) {
747
+ const dx = Math.max(20, Math.abs(to.x - from.x) / 2);
748
+ const c1x = from.x + dx;
749
+ const c1y = from.y;
750
+ const c2x = to.x - dx;
751
+ const c2y = to.y;
752
+ return `M ${from.x} ${from.y} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${to.x} ${to.y}`;
753
+ }
754
+ function computeLayout(input) {
755
+ const {
756
+ tasks,
757
+ dependencies,
758
+ viewMode,
759
+ columnWidth,
760
+ theme,
761
+ columns,
762
+ showCriticalPath = false,
763
+ showBaseline = false,
764
+ showDeadlines = true,
765
+ calendar,
766
+ markers = [],
767
+ autoRollupProgress = false,
768
+ selectedTaskIds,
769
+ pagination
770
+ } = input;
771
+ validateInputs(tasks, dependencies);
772
+ const { roots, nodeById } = buildTree(tasks);
773
+ for (const root of roots) computeSpans(root);
774
+ if (autoRollupProgress) {
775
+ for (const root of roots) computeProgressRollup(root);
776
+ }
777
+ let visibleRoots = roots;
778
+ if (pagination && pagination.pageSize > 0) {
779
+ const start = Math.max(0, (pagination.page - 1) * pagination.pageSize);
780
+ visibleRoots = roots.slice(start, start + pagination.pageSize);
781
+ }
782
+ const flatRows = flatten(visibleRoots);
783
+ const visibleIds = new Set(flatRows.map((f) => f.node.task.id));
784
+ const { criticalTasks, criticalLinks } = showCriticalPath ? computeCriticalPath(tasks, dependencies) : { criticalTasks: /* @__PURE__ */ new Set(), criticalLinks: /* @__PURE__ */ new Set() };
785
+ let rangeMin = null;
786
+ let rangeMax = null;
787
+ for (const row of flatRows) {
788
+ const s = row.node.computedStart;
789
+ const e = row.node.computedEnd;
790
+ if (s) rangeMin = rangeMin === null ? s.getTime() : Math.min(rangeMin, s.getTime());
791
+ if (e) rangeMax = rangeMax === null ? e.getTime() : Math.max(rangeMax, e.getTime());
792
+ }
793
+ const now = Date.now();
794
+ const rawRangeStart = rangeMin === null ? new Date(now) : new Date(rangeMin);
795
+ const rawRangeEnd = rangeMax === null ? new Date(now + 1) : new Date(rangeMax);
796
+ const paddedStart = addUnit(rawRangeStart, viewMode, -1);
797
+ const paddedEnd = addUnit(rawRangeEnd, viewMode, 1);
798
+ const rows = [];
799
+ const bars = [];
800
+ const barByTaskId = /* @__PURE__ */ new Map();
801
+ flatRows.forEach((row, index) => {
802
+ const { task } = row.node;
803
+ const y = index * theme.rowHeight;
804
+ rows.push({
805
+ taskId: task.id,
806
+ y,
807
+ height: theme.rowHeight,
808
+ isGroup: !!task.isGroup,
809
+ collapsed: !!task.collapsed,
810
+ depth: row.depth,
811
+ hasChildren: row.node.children.length > 0
812
+ });
813
+ const start = row.node.computedStart;
814
+ const end = row.node.computedEnd;
815
+ if (!start || !end) return;
816
+ 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;
819
+ const progress = task.progress !== void 0 ? Math.min(100, Math.max(0, task.progress)) : autoRollupProgress ? row.node.computedProgress : 0;
820
+ const barHeight = theme.barHeight;
821
+ const barY = y + (theme.rowHeight - barHeight) / 2;
822
+ const color = task.color ?? (task.isGroup ? theme.groupBarColor : theme.barColor);
823
+ const progressColor = task.progressColor ?? theme.barProgressColor;
824
+ const bar = {
825
+ taskId: task.id,
826
+ x,
827
+ y: barY,
828
+ width: width2,
829
+ height: barHeight,
830
+ progressWidth: width2 * progress / 100,
831
+ color,
832
+ progressColor,
833
+ label: task.name,
834
+ isCritical: showCriticalPath && criticalTasks.has(task.id),
835
+ isMilestone: task.isMilestone ?? start.getTime() === end.getTime(),
836
+ isSelected: selectedTaskIds?.has(task.id) ?? false
837
+ };
838
+ if (task.segments && task.segments.length > 0) {
839
+ const validSegments = task.segments.filter(
840
+ (s) => isValidDate(s.start) && isValidDate(s.end) && s.end.getTime() > s.start.getTime()
841
+ );
842
+ if (validSegments.length > 0) {
843
+ bar.segments = validSegments.map((s) => {
844
+ const sx = dateToX(s.start, paddedStart, viewMode, columnWidth);
845
+ const sw = Math.max(0, dateToX(s.end, paddedStart, viewMode, columnWidth) - sx);
846
+ return { x: sx - x, width: sw, progressWidth: sw * progress / 100 };
847
+ });
848
+ }
849
+ }
850
+ if (showBaseline && isValidDate(task.baselineStart) && isValidDate(task.baselineEnd)) {
851
+ const bx = dateToX(task.baselineStart, paddedStart, viewMode, columnWidth);
852
+ const bw = Math.max(
853
+ 0,
854
+ dateToX(task.baselineEnd, paddedStart, viewMode, columnWidth) - bx
855
+ );
856
+ if (Number.isFinite(bx) && Number.isFinite(bw)) {
857
+ bar.baseline = { x: bx, width: bw };
858
+ }
859
+ }
860
+ if (showBaseline && task.baselines && task.baselines.length > 0) {
861
+ bar.baselines = task.baselines.filter((b) => isValidDate(b.start) && isValidDate(b.end)).map((b) => {
862
+ const bx = dateToX(b.start, paddedStart, viewMode, columnWidth);
863
+ const bw = Math.max(0, dateToX(b.end, paddedStart, viewMode, columnWidth) - bx);
864
+ return { label: b.label, x: bx, width: bw };
865
+ }).filter((b) => Number.isFinite(b.x) && Number.isFinite(b.width));
866
+ }
867
+ if (showDeadlines && isValidDate(task.deadline)) {
868
+ const dx = dateToX(task.deadline, paddedStart, viewMode, columnWidth);
869
+ if (Number.isFinite(dx)) {
870
+ bar.deadlineX = dx;
871
+ bar.isOverdue = end.getTime() > task.deadline.getTime();
872
+ }
873
+ }
874
+ bars.push(bar);
875
+ barByTaskId.set(task.id, bar);
876
+ });
877
+ function nearestVisibleAncestorId(taskId) {
878
+ let currentId = taskId;
879
+ const guard = /* @__PURE__ */ new Set();
880
+ while (currentId !== null && !guard.has(currentId)) {
881
+ guard.add(currentId);
882
+ if (visibleIds.has(currentId)) return currentId;
883
+ const node = nodeById.get(currentId);
884
+ currentId = node?.task.parentId ?? null;
885
+ }
886
+ return null;
887
+ }
888
+ const links = [];
889
+ for (const dep of dependencies) {
890
+ if (!nodeById.has(dep.fromId) || !nodeById.has(dep.toId)) continue;
891
+ const fromVisibleId = nearestVisibleAncestorId(dep.fromId);
892
+ const toVisibleId = nearestVisibleAncestorId(dep.toId);
893
+ if (!fromVisibleId || !toVisibleId || fromVisibleId === toVisibleId) continue;
894
+ const fromBar = barByTaskId.get(fromVisibleId);
895
+ const toBar = barByTaskId.get(toVisibleId);
896
+ if (!fromBar || !toBar) continue;
897
+ const fromSide = dep.type === "SS" || dep.type === "SF" ? "start" : "end";
898
+ const toSide = dep.type === "FF" || dep.type === "SF" ? "end" : "start";
899
+ const from = anchorPoint(fromBar, fromSide);
900
+ const to = anchorPoint(toBar, toSide);
901
+ links.push({
902
+ fromId: dep.fromId,
903
+ toId: dep.toId,
904
+ type: dep.type,
905
+ path: linkPath(from, to),
906
+ isCritical: showCriticalPath && criticalLinks.has(`${dep.fromId}->${dep.toId}`)
907
+ });
908
+ }
909
+ const ticks = generateTicks(paddedStart, paddedEnd, viewMode, columnWidth, /* @__PURE__ */ new Date(), calendar).map(
910
+ (t) => ({
911
+ x: t.x,
912
+ label: t.label,
913
+ isWeekend: t.isWeekend,
914
+ isNonWorking: t.isNonWorking,
915
+ isToday: t.isToday,
916
+ isMajorBoundary: t.isMajorBoundary
917
+ })
918
+ );
919
+ const renderMarkers = markers.filter((m) => isValidDate(m.date)).map((m) => ({
920
+ x: dateToX(m.date, paddedStart, viewMode, columnWidth),
921
+ label: m.label ?? "",
922
+ color: m.color ?? theme.markerColor
923
+ })).filter((m) => Number.isFinite(m.x));
924
+ let width = 0;
925
+ 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);
927
+ width += columnWidth;
928
+ const height = rows.length * theme.rowHeight;
929
+ return {
930
+ width,
931
+ height,
932
+ rowHeight: theme.rowHeight,
933
+ headerHeight: theme.headerHeight,
934
+ rows,
935
+ bars,
936
+ links,
937
+ ticks,
938
+ columns,
939
+ theme,
940
+ markers: renderMarkers,
941
+ rangeStart: paddedStart
942
+ };
943
+ }
944
+
945
+ // src/history.ts
946
+ var HistoryManager = class {
947
+ constructor(onChange) {
948
+ this.undoStack = [];
949
+ this.redoStack = [];
950
+ this.onChange = onChange;
951
+ }
952
+ push(cmd) {
953
+ cmd.do();
954
+ this.undoStack.push(cmd);
955
+ this.redoStack = [];
956
+ this.notify();
957
+ }
958
+ undo() {
959
+ const cmd = this.undoStack.pop();
960
+ if (!cmd) return false;
961
+ cmd.undo();
962
+ this.redoStack.push(cmd);
963
+ this.notify();
964
+ return true;
965
+ }
966
+ redo() {
967
+ const cmd = this.redoStack.pop();
968
+ if (!cmd) return false;
969
+ cmd.do();
970
+ this.undoStack.push(cmd);
971
+ this.notify();
972
+ return true;
973
+ }
974
+ get canUndo() {
975
+ return this.undoStack.length > 0;
976
+ }
977
+ get canRedo() {
978
+ return this.redoStack.length > 0;
979
+ }
980
+ clear() {
981
+ this.undoStack = [];
982
+ this.redoStack = [];
983
+ this.notify();
984
+ }
985
+ notify() {
986
+ this.onChange?.({ canUndo: this.canUndo, canRedo: this.canRedo });
987
+ }
988
+ };
989
+
990
+ // src/event-emitter.ts
991
+ var EventEmitter = class {
992
+ constructor() {
993
+ this.listeners = {};
994
+ }
995
+ on(event, listener) {
996
+ let set = this.listeners[event];
997
+ if (!set) {
998
+ set = /* @__PURE__ */ new Set();
999
+ this.listeners[event] = set;
1000
+ }
1001
+ set.add(listener);
1002
+ }
1003
+ off(event, listener) {
1004
+ this.listeners[event]?.delete(listener);
1005
+ }
1006
+ once(event, listener) {
1007
+ const wrapper = (payload) => {
1008
+ this.off(event, wrapper);
1009
+ listener(payload);
1010
+ };
1011
+ this.on(event, wrapper);
1012
+ }
1013
+ emit(event, payload) {
1014
+ const set = this.listeners[event];
1015
+ if (!set) return;
1016
+ for (const listener of Array.from(set)) {
1017
+ listener(payload);
1018
+ }
1019
+ }
1020
+ removeAllListeners() {
1021
+ this.listeners = {};
1022
+ }
1023
+ };
1024
+
1025
+ // src/csv.ts
1026
+ var CSV_COLUMNS = ["id", "name", "start", "end", "progress", "parentId"];
1027
+ function escapeCsvField(value) {
1028
+ if (/[",\n]/.test(value)) {
1029
+ return `"${value.replace(/"/g, '""')}"`;
1030
+ }
1031
+ return value;
1032
+ }
1033
+ function parseCsvLine(line) {
1034
+ const fields = [];
1035
+ let current = "";
1036
+ let inQuotes = false;
1037
+ for (let i = 0; i < line.length; i++) {
1038
+ const char = line[i] ?? "";
1039
+ if (inQuotes) {
1040
+ if (char === '"') {
1041
+ if (line[i + 1] === '"') {
1042
+ current += '"';
1043
+ i++;
1044
+ } else {
1045
+ inQuotes = false;
1046
+ }
1047
+ } else {
1048
+ current += char;
1049
+ }
1050
+ } else if (char === '"') {
1051
+ inQuotes = true;
1052
+ } else if (char === ",") {
1053
+ fields.push(current);
1054
+ current = "";
1055
+ } else {
1056
+ current += char;
1057
+ }
1058
+ }
1059
+ fields.push(current);
1060
+ return fields;
1061
+ }
1062
+ function tasksToCSV(tasks) {
1063
+ const lines = [CSV_COLUMNS.join(",")];
1064
+ for (const task of tasks) {
1065
+ const row = [
1066
+ task.id,
1067
+ task.name,
1068
+ task.start instanceof Date ? task.start.toISOString() : "",
1069
+ task.end instanceof Date ? task.end.toISOString() : "",
1070
+ task.progress !== void 0 ? String(task.progress) : "",
1071
+ task.parentId ?? ""
1072
+ ].map((v) => escapeCsvField(String(v)));
1073
+ lines.push(row.join(","));
1074
+ }
1075
+ return lines.join("\n");
1076
+ }
1077
+ function tasksFromCSV(csv) {
1078
+ const lines = csv.split(/\r?\n/).filter((l) => l.length > 0);
1079
+ if (lines.length === 0) return [];
1080
+ const header = parseCsvLine(lines[0] ?? "");
1081
+ const colIndex = new Map(header.map((h, i) => [h.trim(), i]));
1082
+ const tasks = [];
1083
+ for (let i = 1; i < lines.length; i++) {
1084
+ const fields = parseCsvLine(lines[i] ?? "");
1085
+ const get = (col) => {
1086
+ const idx = colIndex.get(col);
1087
+ return idx === void 0 ? "" : fields[idx] ?? "";
1088
+ };
1089
+ const id = get("id");
1090
+ if (!id) continue;
1091
+ const progressRaw = get("progress");
1092
+ const parentIdRaw = get("parentId");
1093
+ tasks.push({
1094
+ id,
1095
+ name: get("name"),
1096
+ start: new Date(get("start")),
1097
+ end: new Date(get("end")),
1098
+ progress: progressRaw ? Number(progressRaw) : void 0,
1099
+ parentId: parentIdRaw || null
1100
+ });
1101
+ }
1102
+ return tasks;
1103
+ }
1104
+
1105
+ // src/renderer.ts
1106
+ var SVG_NS = "http://www.w3.org/2000/svg";
1107
+ function isSafeHref(url) {
1108
+ try {
1109
+ const base = typeof location !== "undefined" && location.origin ? location.origin : "http://localhost";
1110
+ const parsed = new URL(url, base);
1111
+ return parsed.protocol === "http:" || parsed.protocol === "https:" || parsed.protocol === "mailto:";
1112
+ } catch {
1113
+ return false;
1114
+ }
1115
+ }
1116
+ function el(tag, className) {
1117
+ const e = document.createElement(tag);
1118
+ if (className) e.className = className;
1119
+ return e;
1120
+ }
1121
+ function svgEl(tag) {
1122
+ return document.createElementNS(SVG_NS, tag);
1123
+ }
1124
+ function initials(name) {
1125
+ const parts = name.trim().split(/\s+/).filter(Boolean);
1126
+ if (parts.length === 0) return "?";
1127
+ const first = parts[0]?.[0] ?? "";
1128
+ const last = parts.length > 1 ? parts[parts.length - 1]?.[0] ?? "" : "";
1129
+ return (first + last).toUpperCase();
1130
+ }
1131
+ var GanttRenderer = class {
1132
+ constructor(container, options = {}) {
1133
+ this.container = container;
1134
+ this.options = options;
1135
+ this.lastModel = null;
1136
+ this.lastTasks = [];
1137
+ this.lastColumns = [];
1138
+ this.onGridDoubleClick = (evt) => {
1139
+ if (!this.options.onRenameCommit) return;
1140
+ const target = evt.target;
1141
+ const cell = target.closest("[data-gantt-name-cell]");
1142
+ if (!cell || cell.querySelector("input")) return;
1143
+ const taskId = cell.dataset.ganttNameCell;
1144
+ if (!taskId) return;
1145
+ const currentText = cell.dataset.ganttNameValue ?? "";
1146
+ cell.replaceChildren();
1147
+ const input = document.createElement("input");
1148
+ input.type = "text";
1149
+ input.value = currentText;
1150
+ input.className = "gantt-rename-input";
1151
+ cell.appendChild(input);
1152
+ input.focus();
1153
+ input.select();
1154
+ const commit = () => {
1155
+ this.options.onRenameCommit?.(taskId, input.value);
1156
+ };
1157
+ const cancel = () => {
1158
+ this.rerenderVisibleOnly();
1159
+ };
1160
+ input.addEventListener("keydown", (kEvt) => {
1161
+ if (kEvt.key === "Enter") {
1162
+ kEvt.preventDefault();
1163
+ commit();
1164
+ } else if (kEvt.key === "Escape") {
1165
+ kEvt.preventDefault();
1166
+ cancel();
1167
+ }
1168
+ });
1169
+ input.addEventListener("blur", commit, { once: true });
1170
+ };
1171
+ this.markerDefsId = `gantt-arrow-${Math.random().toString(36).slice(2)}`;
1172
+ this.root = el("div", "gantt-root");
1173
+ this.gridPanel = el("div", "gantt-grid-panel");
1174
+ this.timelineScroll = el("div", "gantt-timeline-scroll");
1175
+ this.svg = svgEl("svg");
1176
+ this.svg.classList.add("gantt-svg");
1177
+ this.timelineScroll.appendChild(this.svg);
1178
+ this.root.appendChild(this.gridPanel);
1179
+ this.root.appendChild(this.timelineScroll);
1180
+ this.container.appendChild(this.root);
1181
+ if (options.virtualScroll !== false && typeof document !== "undefined") {
1182
+ this.scrollListener = () => this.rerenderVisibleOnly();
1183
+ this.timelineScroll.addEventListener("scroll", this.scrollListener);
1184
+ }
1185
+ this.gridPanel.addEventListener("dblclick", this.onGridDoubleClick);
1186
+ }
1187
+ render(model, tasks, columns, explicitTheme) {
1188
+ this.lastModel = model;
1189
+ this.lastTasks = tasks;
1190
+ this.lastColumns = columns;
1191
+ const cssVars = explicitThemeToCssVars(explicitTheme);
1192
+ for (const key of Object.keys(cssVars)) {
1193
+ this.root.style.setProperty(key, cssVars[key]);
1194
+ }
1195
+ this.renderGridPanel(model, tasks, columns);
1196
+ this.renderTimeline(model, tasks);
1197
+ }
1198
+ getViewportRowRange(model) {
1199
+ if (this.options.virtualScroll === false) {
1200
+ return { startY: -Infinity, endY: Infinity };
1201
+ }
1202
+ const overscan = (this.options.overscan ?? 5) * model.rowHeight;
1203
+ const scrollTop = this.timelineScroll.scrollTop || 0;
1204
+ const viewportHeight = this.timelineScroll.clientHeight || model.height;
1205
+ return {
1206
+ startY: scrollTop - overscan,
1207
+ endY: scrollTop + viewportHeight + overscan
1208
+ };
1209
+ }
1210
+ rerenderVisibleOnly() {
1211
+ if (!this.lastModel) return;
1212
+ this.renderGridPanel(this.lastModel, this.lastTasks, this.lastColumns);
1213
+ this.renderTimeline(this.lastModel, this.lastTasks);
1214
+ }
1215
+ renderGridPanel(model, tasks, columns) {
1216
+ this.gridPanel.replaceChildren();
1217
+ const taskById = new Map(tasks.map((t) => [t.id, t]));
1218
+ const { startY, endY } = this.getViewportRowRange(model);
1219
+ const header = el("div", "gantt-grid-header-row");
1220
+ header.style.height = `${model.headerHeight}px`;
1221
+ for (const col of columns) {
1222
+ header.appendChild(this.renderHeaderCell(col, columns));
1223
+ }
1224
+ this.gridPanel.appendChild(header);
1225
+ const body = el("div", "gantt-grid-body");
1226
+ body.style.height = `${model.height}px`;
1227
+ model.rows.forEach((row, index) => {
1228
+ if (row.y + row.height < startY || row.y > endY) return;
1229
+ const task = taskById.get(row.taskId);
1230
+ if (!task) return;
1231
+ body.appendChild(this.renderGridRow(row, task, columns, index));
1232
+ });
1233
+ this.gridPanel.appendChild(body);
1234
+ }
1235
+ renderHeaderCell(col, columns) {
1236
+ const cell = el("div", "gantt-grid-header-cell");
1237
+ cell.style.position = "relative";
1238
+ if (col.width) cell.style.width = `${col.width}px`;
1239
+ cell.textContent = col.title;
1240
+ if (this.options.onColumnReorder) {
1241
+ cell.draggable = true;
1242
+ cell.style.cursor = "grab";
1243
+ cell.addEventListener("dragstart", (evt) => {
1244
+ evt.dataTransfer?.setData("text/plain", col.id);
1245
+ });
1246
+ cell.addEventListener("dragover", (evt) => evt.preventDefault());
1247
+ cell.addEventListener("drop", (evt) => {
1248
+ evt.preventDefault();
1249
+ const draggedId = evt.dataTransfer?.getData("text/plain");
1250
+ if (!draggedId || draggedId === col.id) return;
1251
+ const order = columns.map((c) => c.id).filter((id) => id !== draggedId);
1252
+ const targetIndex = order.indexOf(col.id);
1253
+ order.splice(targetIndex, 0, draggedId);
1254
+ this.options.onColumnReorder?.(order);
1255
+ });
1256
+ }
1257
+ if (this.options.onColumnResize) {
1258
+ const handle = el("div", "gantt-col-resize-handle");
1259
+ handle.style.position = "absolute";
1260
+ handle.style.right = "0";
1261
+ handle.style.top = "0";
1262
+ handle.style.bottom = "0";
1263
+ handle.style.width = "6px";
1264
+ handle.style.cursor = "col-resize";
1265
+ handle.addEventListener("pointerdown", (evt) => {
1266
+ evt.preventDefault();
1267
+ evt.stopPropagation();
1268
+ const startX = evt.clientX;
1269
+ const startWidth = col.width ?? cell.getBoundingClientRect().width;
1270
+ const onMove = (moveEvt) => {
1271
+ const newWidth = Math.max(30, startWidth + (moveEvt.clientX - startX));
1272
+ cell.style.width = `${newWidth}px`;
1273
+ };
1274
+ const onUp = (upEvt) => {
1275
+ window.removeEventListener("pointermove", onMove);
1276
+ window.removeEventListener("pointerup", onUp);
1277
+ const newWidth = Math.max(30, startWidth + (upEvt.clientX - startX));
1278
+ this.options.onColumnResize?.(col.id, newWidth);
1279
+ };
1280
+ window.addEventListener("pointermove", onMove);
1281
+ window.addEventListener("pointerup", onUp);
1282
+ });
1283
+ cell.appendChild(handle);
1284
+ }
1285
+ return cell;
1286
+ }
1287
+ renderGridRow(row, task, columns, firstColumnIndex) {
1288
+ const rowEl = el("div", "gantt-grid-row");
1289
+ rowEl.style.position = "absolute";
1290
+ rowEl.style.top = `${row.y}px`;
1291
+ rowEl.style.height = `${row.height}px`;
1292
+ rowEl.dataset.ganttRow = task.id;
1293
+ if (this.options.keyboardAccessible) {
1294
+ rowEl.setAttribute("role", "row");
1295
+ }
1296
+ if (this.options.onRowReorder) {
1297
+ rowEl.draggable = true;
1298
+ rowEl.style.cursor = "grab";
1299
+ rowEl.addEventListener("dragstart", (evt) => {
1300
+ evt.dataTransfer?.setData("text/plain", task.id);
1301
+ evt.stopPropagation();
1302
+ });
1303
+ rowEl.addEventListener("dragover", (evt) => {
1304
+ evt.preventDefault();
1305
+ evt.stopPropagation();
1306
+ });
1307
+ rowEl.addEventListener("drop", (evt) => {
1308
+ evt.preventDefault();
1309
+ evt.stopPropagation();
1310
+ const draggedId = evt.dataTransfer?.getData("text/plain");
1311
+ if (!draggedId || draggedId === task.id) return;
1312
+ const bounds = rowEl.getBoundingClientRect();
1313
+ const fraction = (evt.clientY - bounds.top) / bounds.height;
1314
+ const position = fraction < 0.25 ? "before" : fraction > 0.75 ? "after" : "inside";
1315
+ this.options.onRowReorder?.(draggedId, task.id, position);
1316
+ });
1317
+ }
1318
+ columns.forEach((col, colIndex) => {
1319
+ const cell = el("div", "gantt-grid-cell");
1320
+ if (col.width) cell.style.width = `${col.width}px`;
1321
+ if (col.align) cell.style.textAlign = col.align;
1322
+ const rawValue = col.accessor ? col.accessor(task) : colIndex === 0 ? task.name : "";
1323
+ const textValue = rawValue === null || rawValue === void 0 ? "" : String(rawValue);
1324
+ if (colIndex === 0) {
1325
+ cell.dataset.ganttNameCell = task.id;
1326
+ cell.dataset.ganttNameValue = textValue;
1327
+ const indent = el("span", "gantt-indent");
1328
+ indent.style.width = `${row.depth * 16}px`;
1329
+ cell.appendChild(indent);
1330
+ if (row.hasChildren) {
1331
+ const toggle = el("button", "gantt-toggle");
1332
+ toggle.type = "button";
1333
+ toggle.dataset.ganttToggle = task.id;
1334
+ toggle.setAttribute("aria-label", row.collapsed ? "Expand" : "Collapse");
1335
+ toggle.textContent = row.collapsed ? "\u25B8" : "\u25BE";
1336
+ cell.appendChild(toggle);
1337
+ }
1338
+ }
1339
+ const rendered = col.render ? col.render(task) : void 0;
1340
+ if (rendered instanceof HTMLElement) {
1341
+ cell.appendChild(rendered);
1342
+ } else if (typeof rendered === "string") {
1343
+ cell.appendChild(document.createTextNode(rendered));
1344
+ } else {
1345
+ const href = col.getHref ? col.getHref(task) : null;
1346
+ if (href && isSafeHref(href)) {
1347
+ const anchor = document.createElement("a");
1348
+ anchor.href = href;
1349
+ anchor.target = col.linkTarget ?? "_blank";
1350
+ anchor.rel = "noopener noreferrer";
1351
+ anchor.textContent = textValue;
1352
+ cell.appendChild(anchor);
1353
+ } else {
1354
+ const textNode = document.createTextNode(textValue);
1355
+ cell.appendChild(textNode);
1356
+ }
1357
+ }
1358
+ rowEl.appendChild(cell);
1359
+ });
1360
+ void firstColumnIndex;
1361
+ return rowEl;
1362
+ }
1363
+ renderTimeline(model, tasks) {
1364
+ this.svg.replaceChildren();
1365
+ this.svg.setAttribute("width", String(model.width));
1366
+ this.svg.setAttribute("height", String(model.headerHeight + model.height));
1367
+ this.svg.setAttribute("viewBox", `0 0 ${model.width} ${model.headerHeight + model.height}`);
1368
+ const defs = svgEl("defs");
1369
+ const marker = svgEl("marker");
1370
+ marker.setAttribute("id", this.markerDefsId);
1371
+ marker.setAttribute("markerWidth", "8");
1372
+ marker.setAttribute("markerHeight", "8");
1373
+ marker.setAttribute("refX", "6");
1374
+ marker.setAttribute("refY", "3");
1375
+ marker.setAttribute("orient", "auto");
1376
+ const arrowPath = svgEl("path");
1377
+ arrowPath.setAttribute("d", "M0,0 L6,3 L0,6 Z");
1378
+ arrowPath.style.fill = themedFill("linkColor", model.theme.linkColor);
1379
+ marker.appendChild(arrowPath);
1380
+ defs.appendChild(marker);
1381
+ this.svg.appendChild(defs);
1382
+ const { startY, endY } = this.getViewportRowRange(model);
1383
+ const bodyGroup = svgEl("g");
1384
+ bodyGroup.setAttribute("transform", `translate(0, ${model.headerHeight})`);
1385
+ this.renderGridLines(bodyGroup, model);
1386
+ this.renderRowBackgrounds(bodyGroup, model, startY, endY);
1387
+ this.renderBars(bodyGroup, model, tasks, startY, endY);
1388
+ this.renderLinks(bodyGroup, model, startY, endY);
1389
+ this.renderMarkers(bodyGroup, model);
1390
+ this.svg.appendChild(bodyGroup);
1391
+ this.renderHeader(model);
1392
+ }
1393
+ renderGridLines(group, model) {
1394
+ for (const tick of model.ticks) {
1395
+ if (tick.isNonWorking) {
1396
+ const rect = svgEl("rect");
1397
+ rect.setAttribute("x", String(tick.x));
1398
+ rect.setAttribute("y", "0");
1399
+ rect.setAttribute("width", "1");
1400
+ rect.setAttribute("height", String(model.height));
1401
+ rect.style.fill = themedFill("weekendColor", model.theme.weekendColor);
1402
+ group.appendChild(rect);
1403
+ }
1404
+ const line = svgEl("line");
1405
+ line.setAttribute("x1", String(tick.x));
1406
+ line.setAttribute("x2", String(tick.x));
1407
+ line.setAttribute("y1", "0");
1408
+ line.setAttribute("y2", String(model.height));
1409
+ line.style.stroke = themedFill("gridColor", model.theme.gridColor);
1410
+ line.setAttribute("stroke-width", tick.isMajorBoundary ? "1.5" : "1");
1411
+ group.appendChild(line);
1412
+ if (tick.isToday) {
1413
+ const todayLine = svgEl("line");
1414
+ todayLine.setAttribute("x1", String(tick.x));
1415
+ todayLine.setAttribute("x2", String(tick.x));
1416
+ todayLine.setAttribute("y1", "0");
1417
+ todayLine.setAttribute("y2", String(model.height));
1418
+ todayLine.style.stroke = themedFill("todayColor", model.theme.todayColor);
1419
+ todayLine.setAttribute("stroke-width", "2");
1420
+ group.appendChild(todayLine);
1421
+ }
1422
+ }
1423
+ }
1424
+ renderMarkers(group, model) {
1425
+ for (const marker of model.markers) {
1426
+ const line = svgEl("line");
1427
+ line.setAttribute("x1", String(marker.x));
1428
+ line.setAttribute("x2", String(marker.x));
1429
+ line.setAttribute("y1", "0");
1430
+ line.setAttribute("y2", String(model.height));
1431
+ line.setAttribute("stroke", marker.color);
1432
+ line.setAttribute("stroke-width", "1.5");
1433
+ line.setAttribute("stroke-dasharray", "3 3");
1434
+ line.dataset.ganttMarker = marker.label;
1435
+ group.appendChild(line);
1436
+ if (marker.label) {
1437
+ const label = svgEl("text");
1438
+ label.setAttribute("x", String(marker.x + 4));
1439
+ label.setAttribute("y", "12");
1440
+ label.setAttribute("fill", marker.color);
1441
+ label.setAttribute("font-size", String(model.theme.fontSize));
1442
+ label.setAttribute("font-family", model.theme.fontFamily);
1443
+ label.textContent = marker.label;
1444
+ group.appendChild(label);
1445
+ }
1446
+ }
1447
+ }
1448
+ renderHeader(model) {
1449
+ let headerGroup = this.svg.querySelector(".gantt-header-group");
1450
+ if (headerGroup) headerGroup.remove();
1451
+ headerGroup = svgEl("g");
1452
+ headerGroup.setAttribute("class", "gantt-header-group");
1453
+ const bg = svgEl("rect");
1454
+ bg.setAttribute("x", "0");
1455
+ bg.setAttribute("y", "0");
1456
+ bg.setAttribute("width", String(model.width));
1457
+ bg.setAttribute("height", String(model.headerHeight));
1458
+ bg.style.fill = themedFill("backgroundColor", model.theme.backgroundColor);
1459
+ headerGroup.appendChild(bg);
1460
+ for (const tick of model.ticks) {
1461
+ const text = svgEl("text");
1462
+ text.setAttribute("x", String(tick.x + 4));
1463
+ text.setAttribute("y", String(model.headerHeight - 8));
1464
+ text.style.fill = themedFill("textColor", model.theme.textColor);
1465
+ text.setAttribute("font-size", String(model.theme.fontSize));
1466
+ text.setAttribute("font-family", model.theme.fontFamily);
1467
+ text.textContent = tick.label;
1468
+ headerGroup.appendChild(text);
1469
+ }
1470
+ this.svg.appendChild(headerGroup);
1471
+ }
1472
+ /**
1473
+ * Invisible full-width hit-test rects, one per row, behind the bars. Lets
1474
+ * drag-to-create (InteractionController) find "which row / empty timeline
1475
+ * space was this pointerdown on" without needing its own copy of the
1476
+ * layout math - a bar drawn on top naturally wins the hit test where one exists.
1477
+ */
1478
+ renderRowBackgrounds(group, model, startY, endY) {
1479
+ for (const row of model.rows) {
1480
+ if (row.y + row.height < startY || row.y > endY) continue;
1481
+ const rect = svgEl("rect");
1482
+ rect.setAttribute("x", "0");
1483
+ rect.setAttribute("y", String(row.y));
1484
+ rect.setAttribute("width", String(model.width));
1485
+ rect.setAttribute("height", String(row.height));
1486
+ rect.setAttribute("fill", "transparent");
1487
+ rect.style.pointerEvents = "all";
1488
+ rect.dataset.ganttRowBg = row.taskId;
1489
+ group.appendChild(rect);
1490
+ }
1491
+ }
1492
+ renderBars(group, model, tasks, startY, endY) {
1493
+ const taskById = new Map(tasks.map((t) => [t.id, t]));
1494
+ for (const bar of model.bars) {
1495
+ if (bar.y + bar.height < startY || bar.y > endY) continue;
1496
+ const task = taskById.get(bar.taskId);
1497
+ group.appendChild(this.renderBar(bar, task, model));
1498
+ }
1499
+ }
1500
+ renderBar(bar, task, model) {
1501
+ const g = svgEl("g");
1502
+ g.setAttribute("class", "gantt-bar");
1503
+ g.dataset.ganttBar = bar.taskId;
1504
+ g.setAttribute("transform", `translate(${bar.x}, ${bar.y})`);
1505
+ if (task?.notes) {
1506
+ const title = svgEl("title");
1507
+ title.textContent = task.notes;
1508
+ g.appendChild(title);
1509
+ }
1510
+ if (this.options.keyboardAccessible) {
1511
+ g.setAttribute("tabindex", "0");
1512
+ g.setAttribute("role", "button");
1513
+ if (task) {
1514
+ const progress = Math.round(task.progress ?? 0);
1515
+ const label2 = `${task.name}, ${task.start.toDateString()} to ${task.end.toDateString()}, ${progress}% complete`;
1516
+ g.setAttribute("aria-label", label2);
1517
+ }
1518
+ }
1519
+ if (bar.baseline) {
1520
+ const baseline = svgEl("rect");
1521
+ baseline.setAttribute("x", String(bar.baseline.x - bar.x));
1522
+ baseline.setAttribute("y", "-2");
1523
+ baseline.setAttribute("width", String(bar.baseline.width));
1524
+ baseline.setAttribute("height", String(bar.height + 4));
1525
+ baseline.style.fill = themedFill("baselineColor", model.theme.baselineColor);
1526
+ baseline.setAttribute("opacity", "0.35");
1527
+ baseline.setAttribute("rx", String(model.theme.borderRadius));
1528
+ g.appendChild(baseline);
1529
+ }
1530
+ if (bar.baselines && bar.baselines.length > 0) {
1531
+ bar.baselines.forEach((b, i) => {
1532
+ const offset = (bar.baselines.length - i) * 2;
1533
+ const rect = svgEl("rect");
1534
+ rect.setAttribute("x", String(b.x - bar.x));
1535
+ rect.setAttribute("y", String(-2 - offset));
1536
+ rect.setAttribute("width", String(b.width));
1537
+ rect.setAttribute("height", "3");
1538
+ rect.style.fill = themedFill("baselineColor", model.theme.baselineColor);
1539
+ rect.setAttribute("opacity", String(Math.max(0.15, 0.4 - i * 0.08)));
1540
+ if (b.label) {
1541
+ const title = svgEl("title");
1542
+ title.textContent = b.label;
1543
+ rect.appendChild(title);
1544
+ }
1545
+ g.appendChild(rect);
1546
+ });
1547
+ }
1548
+ const isDefaultBarColor = bar.color === (task?.isGroup ? model.theme.groupBarColor : model.theme.barColor);
1549
+ const isDefaultProgressColor = bar.progressColor === model.theme.barProgressColor;
1550
+ const fillColor = bar.isCritical ? themedFill("criticalColor", model.theme.criticalColor) : isDefaultBarColor ? themedFill(task?.isGroup ? "groupBarColor" : "barColor", bar.color) : bar.color;
1551
+ if (bar.isMilestone) {
1552
+ const size = bar.height * 0.8;
1553
+ const diamond = svgEl("rect");
1554
+ diamond.setAttribute("x", String(-size / 2));
1555
+ diamond.setAttribute("y", String((bar.height - size) / 2));
1556
+ diamond.setAttribute("width", String(size));
1557
+ diamond.setAttribute("height", String(size));
1558
+ diamond.style.fill = fillColor;
1559
+ diamond.setAttribute(
1560
+ "transform",
1561
+ `rotate(45, 0, ${bar.height / 2})`
1562
+ );
1563
+ g.appendChild(diamond);
1564
+ const label2 = svgEl("text");
1565
+ label2.setAttribute("x", String(size / 2 + 8));
1566
+ label2.setAttribute("y", String(bar.height / 2 + 4));
1567
+ label2.style.fill = themedFill("textColor", model.theme.textColor);
1568
+ label2.setAttribute("font-size", String(model.theme.fontSize));
1569
+ label2.setAttribute("font-family", model.theme.fontFamily);
1570
+ label2.textContent = bar.label;
1571
+ g.appendChild(label2);
1572
+ return g;
1573
+ }
1574
+ if (bar.segments && bar.segments.length > 0) {
1575
+ bar.segments.forEach((seg, i) => {
1576
+ const segRect = svgEl("rect");
1577
+ segRect.setAttribute("x", String(seg.x));
1578
+ segRect.setAttribute("y", "0");
1579
+ segRect.setAttribute("width", String(seg.width));
1580
+ segRect.setAttribute("height", String(bar.height));
1581
+ segRect.setAttribute("rx", String(model.theme.borderRadius));
1582
+ segRect.style.fill = fillColor;
1583
+ if (bar.isCritical) {
1584
+ segRect.style.stroke = themedFill("criticalColor", model.theme.criticalColor);
1585
+ segRect.setAttribute("stroke-width", "1.5");
1586
+ }
1587
+ g.appendChild(segRect);
1588
+ if (seg.progressWidth > 0) {
1589
+ const segProgress = svgEl("rect");
1590
+ segProgress.setAttribute("x", String(seg.x));
1591
+ segProgress.setAttribute("y", "0");
1592
+ segProgress.setAttribute("width", String(seg.progressWidth));
1593
+ segProgress.setAttribute("height", String(bar.height));
1594
+ segProgress.setAttribute("rx", String(model.theme.borderRadius));
1595
+ segProgress.style.fill = isDefaultProgressColor ? themedFill("barProgressColor", bar.progressColor) : bar.progressColor;
1596
+ g.appendChild(segProgress);
1597
+ }
1598
+ const next = bar.segments[i + 1];
1599
+ if (next) {
1600
+ const connector2 = svgEl("line");
1601
+ connector2.setAttribute("x1", String(seg.x + seg.width));
1602
+ connector2.setAttribute("x2", String(next.x));
1603
+ connector2.setAttribute("y1", String(bar.height / 2));
1604
+ connector2.setAttribute("y2", String(bar.height / 2));
1605
+ connector2.style.stroke = fillColor;
1606
+ connector2.setAttribute("stroke-width", "1.5");
1607
+ connector2.setAttribute("stroke-dasharray", "3 2");
1608
+ g.appendChild(connector2);
1609
+ }
1610
+ });
1611
+ } else {
1612
+ const rect = svgEl("rect");
1613
+ rect.setAttribute("x", "0");
1614
+ rect.setAttribute("y", "0");
1615
+ rect.setAttribute("width", String(bar.width));
1616
+ rect.setAttribute("height", String(bar.height));
1617
+ rect.setAttribute("rx", String(model.theme.borderRadius));
1618
+ rect.style.fill = fillColor;
1619
+ if (bar.isCritical) {
1620
+ rect.style.stroke = themedFill("criticalColor", model.theme.criticalColor);
1621
+ rect.setAttribute("stroke-width", "1.5");
1622
+ }
1623
+ g.appendChild(rect);
1624
+ if (bar.progressWidth > 0) {
1625
+ const progressRect = svgEl("rect");
1626
+ progressRect.setAttribute("x", "0");
1627
+ progressRect.setAttribute("y", "0");
1628
+ progressRect.setAttribute("width", String(bar.progressWidth));
1629
+ progressRect.setAttribute("height", String(bar.height));
1630
+ progressRect.setAttribute("rx", String(model.theme.borderRadius));
1631
+ progressRect.style.fill = isDefaultProgressColor ? themedFill("barProgressColor", bar.progressColor) : bar.progressColor;
1632
+ g.appendChild(progressRect);
1633
+ }
1634
+ }
1635
+ if (bar.isSelected) {
1636
+ const selectionOutline = svgEl("rect");
1637
+ selectionOutline.setAttribute("x", "-2");
1638
+ selectionOutline.setAttribute("y", "-2");
1639
+ selectionOutline.setAttribute("width", String(bar.width + 4));
1640
+ selectionOutline.setAttribute("height", String(bar.height + 4));
1641
+ selectionOutline.setAttribute("rx", String(model.theme.borderRadius + 2));
1642
+ selectionOutline.setAttribute("fill", "none");
1643
+ selectionOutline.style.stroke = themedFill("selectionColor", model.theme.selectionColor);
1644
+ selectionOutline.setAttribute("stroke-width", "2");
1645
+ g.appendChild(selectionOutline);
1646
+ }
1647
+ const handleWidth = 6;
1648
+ const leftHandle = svgEl("rect");
1649
+ leftHandle.setAttribute("x", "0");
1650
+ leftHandle.setAttribute("y", "0");
1651
+ leftHandle.setAttribute("width", String(handleWidth));
1652
+ leftHandle.setAttribute("height", String(bar.height));
1653
+ leftHandle.setAttribute("fill", "transparent");
1654
+ leftHandle.dataset.ganttHandle = "left";
1655
+ leftHandle.dataset.ganttHandleFor = bar.taskId;
1656
+ leftHandle.style.cursor = "ew-resize";
1657
+ g.appendChild(leftHandle);
1658
+ const rightHandle = svgEl("rect");
1659
+ rightHandle.setAttribute("x", String(Math.max(0, bar.width - handleWidth)));
1660
+ rightHandle.setAttribute("y", "0");
1661
+ rightHandle.setAttribute("width", String(handleWidth));
1662
+ rightHandle.setAttribute("height", String(bar.height));
1663
+ rightHandle.setAttribute("fill", "transparent");
1664
+ rightHandle.dataset.ganttHandle = "right";
1665
+ rightHandle.dataset.ganttHandleFor = bar.taskId;
1666
+ rightHandle.style.cursor = "ew-resize";
1667
+ g.appendChild(rightHandle);
1668
+ const connector = svgEl("circle");
1669
+ connector.setAttribute("cx", String(bar.width));
1670
+ connector.setAttribute("cy", String(bar.height / 2));
1671
+ connector.setAttribute("r", "4");
1672
+ connector.style.fill = themedFill("linkColor", model.theme.linkColor);
1673
+ connector.dataset.ganttConnector = bar.taskId;
1674
+ connector.dataset.ganttConnectorSide = "right";
1675
+ connector.style.cursor = "crosshair";
1676
+ g.appendChild(connector);
1677
+ const label = svgEl("text");
1678
+ label.setAttribute("x", String(bar.width + 6));
1679
+ label.setAttribute("y", String(bar.height / 2 + 4));
1680
+ label.style.fill = themedFill("textColor", model.theme.textColor);
1681
+ label.setAttribute("font-size", String(model.theme.fontSize));
1682
+ label.setAttribute("font-family", model.theme.fontFamily);
1683
+ label.textContent = bar.label;
1684
+ g.appendChild(label);
1685
+ if (bar.deadlineX !== void 0) {
1686
+ const markerX = bar.deadlineX - bar.x;
1687
+ const flag = svgEl("g");
1688
+ flag.setAttribute("transform", `translate(${markerX}, -6)`);
1689
+ const pole = svgEl("line");
1690
+ pole.setAttribute("x1", "0");
1691
+ pole.setAttribute("x2", "0");
1692
+ pole.setAttribute("y1", "0");
1693
+ pole.setAttribute("y2", String(bar.height + 6));
1694
+ const deadlineColor = bar.isOverdue ? themedFill("criticalColor", model.theme.criticalColor) : themedFill("deadlineColor", model.theme.deadlineColor);
1695
+ pole.style.stroke = deadlineColor;
1696
+ pole.setAttribute("stroke-width", "1.5");
1697
+ flag.appendChild(pole);
1698
+ const pennant = svgEl("path");
1699
+ pennant.setAttribute("d", "M0,0 L7,3 L0,6 Z");
1700
+ pennant.style.fill = deadlineColor;
1701
+ flag.appendChild(pennant);
1702
+ g.appendChild(flag);
1703
+ }
1704
+ if (this.options.showAssigneeAvatars && task?.assignees?.length) {
1705
+ let ax = -10;
1706
+ for (const assignee of task.assignees.slice(0, 3)) {
1707
+ const avatarGroup = svgEl("g");
1708
+ avatarGroup.setAttribute("transform", `translate(${ax}, ${bar.height / 2})`);
1709
+ if (assignee.avatarUrl) {
1710
+ const image = svgEl("image");
1711
+ image.setAttribute("href", assignee.avatarUrl);
1712
+ image.setAttribute("x", "-8");
1713
+ image.setAttribute("y", "-8");
1714
+ image.setAttribute("width", "16");
1715
+ image.setAttribute("height", "16");
1716
+ image.setAttribute("clip-path", "circle(8px at 8px 8px)");
1717
+ avatarGroup.appendChild(image);
1718
+ } else {
1719
+ const circle = svgEl("circle");
1720
+ circle.setAttribute("r", "8");
1721
+ circle.style.fill = assignee.color ?? themedFill("groupBarColor", model.theme.groupBarColor);
1722
+ avatarGroup.appendChild(circle);
1723
+ const text = svgEl("text");
1724
+ text.setAttribute("text-anchor", "middle");
1725
+ text.setAttribute("y", "3");
1726
+ text.setAttribute("font-size", "8");
1727
+ text.setAttribute("fill", "#fff");
1728
+ text.textContent = initials(assignee.name);
1729
+ avatarGroup.appendChild(text);
1730
+ }
1731
+ g.appendChild(avatarGroup);
1732
+ ax -= 18;
1733
+ }
1734
+ }
1735
+ return g;
1736
+ }
1737
+ renderLinks(group, model, startY, endY) {
1738
+ const barByTaskId = new Map(model.bars.map((b) => [b.taskId, b]));
1739
+ for (const link of model.links) {
1740
+ const fromBar = barByTaskId.get(link.fromId);
1741
+ const toBar = barByTaskId.get(link.toId);
1742
+ const relevantY = toBar?.y ?? fromBar?.y ?? 0;
1743
+ if (relevantY < startY || relevantY > endY) continue;
1744
+ const path = svgEl("path");
1745
+ path.setAttribute("d", link.path);
1746
+ path.setAttribute("fill", "none");
1747
+ path.style.stroke = link.isCritical ? themedFill("criticalColor", model.theme.criticalColor) : themedFill("linkColor", model.theme.linkColor);
1748
+ path.setAttribute("stroke-width", link.isCritical ? "2" : "1.5");
1749
+ path.setAttribute("marker-end", `url(#${this.markerDefsId})`);
1750
+ path.dataset.ganttLink = `${link.fromId}->${link.toId}`;
1751
+ group.appendChild(path);
1752
+ }
1753
+ }
1754
+ toSVGString() {
1755
+ return new XMLSerializer().serializeToString(this.svg);
1756
+ }
1757
+ destroy() {
1758
+ if (this.scrollListener) {
1759
+ this.timelineScroll.removeEventListener("scroll", this.scrollListener);
1760
+ }
1761
+ this.gridPanel.removeEventListener("dblclick", this.onGridDoubleClick);
1762
+ this.root.remove();
1763
+ }
1764
+ };
1765
+
1766
+ // src/auto-color.ts
1767
+ var DEFAULT_PALETTE = [
1768
+ "#3b82f6",
1769
+ // blue
1770
+ "#f59e0b",
1771
+ // amber
1772
+ "#10b981",
1773
+ // emerald
1774
+ "#ef4444",
1775
+ // red
1776
+ "#8b5cf6",
1777
+ // violet
1778
+ "#06b6d4",
1779
+ // cyan
1780
+ "#f97316",
1781
+ // orange
1782
+ "#84cc16",
1783
+ // lime
1784
+ "#ec4899",
1785
+ // pink
1786
+ "#64748b"
1787
+ // slate
1788
+ ];
1789
+ function colorByField(tasks, field, palette = DEFAULT_PALETTE) {
1790
+ const colors = /* @__PURE__ */ new Map();
1791
+ if (palette.length === 0) return colors;
1792
+ for (const task of tasks) {
1793
+ const value = field(task);
1794
+ if (value === null || value === void 0 || colors.has(value)) continue;
1795
+ colors.set(value, palette[colors.size % palette.length]);
1796
+ }
1797
+ return colors;
1798
+ }
1799
+ function applyColorByField(tasks, field, palette = DEFAULT_PALETTE) {
1800
+ const colors = colorByField(tasks, field, palette);
1801
+ return tasks.map((task) => {
1802
+ if (task.color) return task;
1803
+ const value = field(task);
1804
+ const color = value === null || value === void 0 ? void 0 : colors.get(value);
1805
+ return color ? { ...task, color } : task;
1806
+ });
1807
+ }
1808
+
1809
+ // src/constraints.ts
1810
+ function applyConstraint(range, constraintType, constraintDate) {
1811
+ if (!constraintType || !constraintDate) return range;
1812
+ const duration = range.end.getTime() - range.start.getTime();
1813
+ const cd = constraintDate.getTime();
1814
+ switch (constraintType) {
1815
+ case "mso":
1816
+ return { start: new Date(cd), end: new Date(cd + duration) };
1817
+ case "mfo":
1818
+ return { start: new Date(cd - duration), end: new Date(cd) };
1819
+ case "snet":
1820
+ if (range.start.getTime() >= cd) return range;
1821
+ return { start: new Date(cd), end: new Date(cd + duration) };
1822
+ case "snlt":
1823
+ if (range.start.getTime() <= cd) return range;
1824
+ return { start: new Date(cd), end: new Date(cd + duration) };
1825
+ case "fnet":
1826
+ if (range.end.getTime() >= cd) return range;
1827
+ return { start: new Date(cd - duration), end: new Date(cd) };
1828
+ case "fnlt":
1829
+ if (range.end.getTime() <= cd) return range;
1830
+ return { start: new Date(cd - duration), end: new Date(cd) };
1831
+ case "asap":
1832
+ case "alap":
1833
+ return range;
1834
+ }
1835
+ }
1836
+
1837
+ // src/wbs.ts
1838
+ function computeWBSCodes(tasks) {
1839
+ const ids = new Set(tasks.map((t) => t.id));
1840
+ const effectiveParent = (task) => task.parentId && ids.has(task.parentId) ? task.parentId : null;
1841
+ const childrenByParent = /* @__PURE__ */ new Map();
1842
+ for (const task of tasks) {
1843
+ const key = effectiveParent(task);
1844
+ const list = childrenByParent.get(key) ?? [];
1845
+ list.push(task);
1846
+ childrenByParent.set(key, list);
1847
+ }
1848
+ const codes = /* @__PURE__ */ new Map();
1849
+ function assign(parentId, prefix) {
1850
+ const children = childrenByParent.get(parentId) ?? [];
1851
+ children.forEach((task, index) => {
1852
+ const code = prefix ? `${prefix}.${index + 1}` : `${index + 1}`;
1853
+ codes.set(task.id, code);
1854
+ assign(task.id, code);
1855
+ });
1856
+ }
1857
+ assign(null, "");
1858
+ return codes;
1859
+ }
1860
+
1861
+ // src/resource-histogram.ts
1862
+ function startOfDay2(date) {
1863
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
1864
+ }
1865
+ function computeResourceHistogram(tasks, options = {}) {
1866
+ const capacityHours = options.capacityHoursPerDay ?? 8;
1867
+ const buckets = /* @__PURE__ */ new Map();
1868
+ for (const task of tasks) {
1869
+ if (!task.assignees || task.assignees.length === 0) continue;
1870
+ if (!(task.start instanceof Date) || !(task.end instanceof Date)) continue;
1871
+ if (task.end.getTime() <= task.start.getTime()) continue;
1872
+ let cursor = startOfDay2(task.start);
1873
+ const end = task.end;
1874
+ let guard = 0;
1875
+ while (cursor.getTime() < end.getTime() && guard++ < 3660) {
1876
+ if (isWorkingDay(cursor, options.calendar)) {
1877
+ for (const assignee of task.assignees) {
1878
+ const key = `${assignee.id}|${cursor.getTime()}`;
1879
+ const existing = buckets.get(key);
1880
+ if (existing) {
1881
+ existing.allocatedHours += capacityHours;
1882
+ } else {
1883
+ buckets.set(key, {
1884
+ assigneeId: assignee.id,
1885
+ assigneeName: assignee.name,
1886
+ date: new Date(cursor.getTime()),
1887
+ allocatedHours: capacityHours,
1888
+ capacityHours,
1889
+ overallocated: false
1890
+ });
1891
+ }
1892
+ }
1893
+ }
1894
+ cursor = new Date(cursor.getTime() + MS_PER_DAY);
1895
+ }
1896
+ }
1897
+ const result = Array.from(buckets.values());
1898
+ for (const bucket of result) {
1899
+ bucket.overallocated = bucket.allocatedHours > bucket.capacityHours;
1900
+ }
1901
+ result.sort((a, b) => a.date.getTime() - b.date.getTime() || a.assigneeId.localeCompare(b.assigneeId));
1902
+ return result;
1903
+ }
1904
+ function renderResourceHistogramSVG(buckets, options = {}) {
1905
+ const barWidth = options.barWidth ?? 14;
1906
+ const gap = 4;
1907
+ const maxBarHeight = options.maxBarHeight ?? 120;
1908
+ const barColor = options.barColor ?? "#3b82f6";
1909
+ const overColor = options.overallocatedColor ?? "#dc2626";
1910
+ const days = Array.from(new Set(buckets.map((b) => b.date.getTime()))).sort((a, b) => a - b);
1911
+ const assigneesByDay = /* @__PURE__ */ new Map();
1912
+ for (const b of buckets) {
1913
+ const list = assigneesByDay.get(b.date.getTime()) ?? [];
1914
+ list.push(b);
1915
+ assigneesByDay.set(b.date.getTime(), list);
1916
+ }
1917
+ const maxHours = buckets.reduce((m, b) => Math.max(m, b.allocatedHours), 1);
1918
+ const dayWidth = (assigneesByDay.get(days[0] ?? 0)?.length ?? 1) * (barWidth + gap) + gap;
1919
+ const width = options.width ?? Math.max(dayWidth * days.length, 100);
1920
+ const height = maxBarHeight + 24;
1921
+ const parts = [
1922
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`
1923
+ ];
1924
+ days.forEach((day, dayIndex) => {
1925
+ const dayBuckets = assigneesByDay.get(day) ?? [];
1926
+ dayBuckets.forEach((b, i) => {
1927
+ const x = dayIndex * dayWidth + gap + i * (barWidth + gap);
1928
+ const barHeight = Math.min(maxBarHeight, b.allocatedHours / maxHours * maxBarHeight);
1929
+ const y = maxBarHeight - barHeight;
1930
+ const fill = b.overallocated ? overColor : barColor;
1931
+ parts.push(
1932
+ `<rect x="${x}" y="${y}" width="${barWidth}" height="${barHeight}" fill="${fill}"><title>${escapeXml(
1933
+ b.assigneeName
1934
+ )}: ${b.allocatedHours}h</title></rect>`
1935
+ );
1936
+ });
1937
+ });
1938
+ parts.push(`</svg>`);
1939
+ return parts.join("");
1940
+ }
1941
+ function escapeXml(text) {
1942
+ return text.replace(/[&<>"']/g, (c) => {
1943
+ switch (c) {
1944
+ case "&":
1945
+ return "&amp;";
1946
+ case "<":
1947
+ return "&lt;";
1948
+ case ">":
1949
+ return "&gt;";
1950
+ case '"':
1951
+ return "&quot;";
1952
+ default:
1953
+ return "&#39;";
1954
+ }
1955
+ });
1956
+ }
1957
+
1958
+ // src/filter.ts
1959
+ function filterTasks(tasks, predicate, options = {}) {
1960
+ const includeAncestors = options.includeAncestors ?? true;
1961
+ const includeDescendants = options.includeDescendants ?? true;
1962
+ const byId = new Map(tasks.map((t) => [t.id, t]));
1963
+ const matchedIds = new Set(tasks.filter(predicate).map((t) => t.id));
1964
+ const keep = new Set(matchedIds);
1965
+ if (includeAncestors) {
1966
+ for (const id of matchedIds) {
1967
+ let current = byId.get(id);
1968
+ while (current?.parentId) {
1969
+ const parent = byId.get(current.parentId);
1970
+ if (!parent || keep.has(parent.id)) break;
1971
+ keep.add(parent.id);
1972
+ current = parent;
1973
+ }
1974
+ }
1975
+ }
1976
+ if (includeDescendants) {
1977
+ const childrenByParent = /* @__PURE__ */ new Map();
1978
+ for (const t of tasks) {
1979
+ if (!t.parentId) continue;
1980
+ const list = childrenByParent.get(t.parentId) ?? [];
1981
+ list.push(t);
1982
+ childrenByParent.set(t.parentId, list);
1983
+ }
1984
+ const stack = Array.from(matchedIds);
1985
+ while (stack.length > 0) {
1986
+ const id = stack.pop();
1987
+ if (id === void 0) continue;
1988
+ for (const child of childrenByParent.get(id) ?? []) {
1989
+ if (keep.has(child.id)) continue;
1990
+ keep.add(child.id);
1991
+ stack.push(child.id);
1992
+ }
1993
+ }
1994
+ }
1995
+ return { tasks: tasks.filter((t) => keep.has(t.id)), matchedIds };
1996
+ }
1997
+
1998
+ // src/resource-leveling.ts
1999
+ function startOfDay3(date) {
2000
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
2001
+ }
2002
+ function workingDaySpan(start, end, calendar) {
2003
+ const days = [];
2004
+ let cursor = startOfDay3(start);
2005
+ let guard = 0;
2006
+ while (cursor.getTime() < end.getTime() && guard++ < 3660) {
2007
+ if (isWorkingDay(cursor, calendar)) days.push(cursor.getTime());
2008
+ cursor = new Date(cursor.getTime() + MS_PER_DAY);
2009
+ }
2010
+ return days;
2011
+ }
2012
+ function levelResources(tasks, dependencies, options = {}) {
2013
+ const calendar = options.calendar;
2014
+ const byId = new Map(tasks.map((t) => [t.id, t]));
2015
+ const fsPredecessors = /* @__PURE__ */ new Map();
2016
+ const fsSuccessors = /* @__PURE__ */ new Map();
2017
+ for (const dep of dependencies) {
2018
+ if (dep.type !== "FS") continue;
2019
+ if (!byId.has(dep.fromId) || !byId.has(dep.toId)) continue;
2020
+ const preds = fsPredecessors.get(dep.toId) ?? [];
2021
+ preds.push(dep.fromId);
2022
+ fsPredecessors.set(dep.toId, preds);
2023
+ const succs = fsSuccessors.get(dep.fromId) ?? [];
2024
+ succs.push(dep.toId);
2025
+ fsSuccessors.set(dep.fromId, succs);
2026
+ }
2027
+ const indegree = /* @__PURE__ */ new Map();
2028
+ for (const t of tasks) indegree.set(t.id, (fsPredecessors.get(t.id) ?? []).length);
2029
+ const queue = tasks.filter((t) => indegree.get(t.id) === 0);
2030
+ const order = [];
2031
+ const seen = /* @__PURE__ */ new Set();
2032
+ let guard = 0;
2033
+ while (queue.length > 0 && guard++ < tasks.length * 2 + 10) {
2034
+ queue.sort((a, b) => a.start.getTime() - b.start.getTime());
2035
+ const t = queue.shift();
2036
+ if (!t || seen.has(t.id)) continue;
2037
+ seen.add(t.id);
2038
+ order.push(t);
2039
+ for (const nextId of fsSuccessors.get(t.id) ?? []) {
2040
+ const d = (indegree.get(nextId) ?? 0) - 1;
2041
+ indegree.set(nextId, d);
2042
+ if (d === 0) {
2043
+ const nt = byId.get(nextId);
2044
+ if (nt) queue.push(nt);
2045
+ }
2046
+ }
2047
+ }
2048
+ for (const t of tasks) if (!seen.has(t.id)) order.push(t);
2049
+ const placedById = /* @__PURE__ */ new Map();
2050
+ const occupiedDays = /* @__PURE__ */ new Map();
2051
+ const shifted = [];
2052
+ for (const task of order) {
2053
+ const duration = task.end.getTime() - task.start.getTime();
2054
+ let start = new Date(task.start.getTime());
2055
+ for (const predId of fsPredecessors.get(task.id) ?? []) {
2056
+ const placed = placedById.get(predId);
2057
+ if (placed && placed.end.getTime() > start.getTime()) start = new Date(placed.end.getTime());
2058
+ }
2059
+ if (task.assignees && task.assignees.length > 0 && duration > 0) {
2060
+ let levelGuard = 0;
2061
+ while (levelGuard++ < 400) {
2062
+ const end3 = new Date(start.getTime() + duration);
2063
+ const days2 = workingDaySpan(start, end3, calendar);
2064
+ const conflict = task.assignees.some((a) => {
2065
+ const set = occupiedDays.get(a.id);
2066
+ return set && days2.some((d) => set.has(d));
2067
+ });
2068
+ if (!conflict) break;
2069
+ start = new Date(start.getTime() + MS_PER_DAY);
2070
+ while (!isWorkingDay(start, calendar)) start = new Date(start.getTime() + MS_PER_DAY);
2071
+ }
2072
+ const end2 = new Date(start.getTime() + duration);
2073
+ const days = workingDaySpan(start, end2, calendar);
2074
+ for (const a of task.assignees) {
2075
+ const set = occupiedDays.get(a.id) ?? /* @__PURE__ */ new Set();
2076
+ for (const d of days) set.add(d);
2077
+ occupiedDays.set(a.id, set);
2078
+ }
2079
+ }
2080
+ const end = new Date(start.getTime() + duration);
2081
+ placedById.set(task.id, { start, end });
2082
+ const delayMs = start.getTime() - task.start.getTime();
2083
+ if (delayMs !== 0) shifted.push({ taskId: task.id, delayMs });
2084
+ }
2085
+ const resultById = /* @__PURE__ */ new Map();
2086
+ for (const task of order) {
2087
+ const placed = placedById.get(task.id);
2088
+ resultById.set(task.id, placed && placed.start.getTime() !== task.start.getTime() ? { ...task, ...placed } : task);
2089
+ }
2090
+ return { tasks: tasks.map((t) => resultById.get(t.id) ?? t), shifted };
2091
+ }
2092
+
2093
+ // src/interactions.ts
2094
+ function snapDeltaMs(dxMs, snapToUnit) {
2095
+ if (!snapToUnit) return dxMs;
2096
+ const base = /* @__PURE__ */ new Date(0);
2097
+ const unitMs = addUnit(base, snapToUnit, 1).getTime() - base.getTime();
2098
+ if (unitMs <= 0) return dxMs;
2099
+ return Math.round(dxMs / unitMs) * unitMs;
2100
+ }
2101
+ var InteractionController = class {
2102
+ constructor(svg, gridPanel, callbacks, options) {
2103
+ this.svg = svg;
2104
+ this.gridPanel = gridPanel;
2105
+ this.callbacks = callbacks;
2106
+ this.options = options;
2107
+ this.dragMode = null;
2108
+ this.dragTaskId = null;
2109
+ this.dragStartX = 0;
2110
+ this.linkFromId = null;
2111
+ this.linkFromAnchor = null;
2112
+ this.tempLinkEl = null;
2113
+ this.rafHandle = null;
2114
+ this.pendingDxMs = 0;
2115
+ this.pendingMode = null;
2116
+ this.onGridClick = (evt) => {
2117
+ const target = evt.target;
2118
+ const toggleId = target.closest("[data-gantt-toggle]")?.dataset.ganttToggle;
2119
+ if (toggleId) {
2120
+ this.callbacks.onToggleCollapse(toggleId);
2121
+ }
2122
+ };
2123
+ this.onContextMenu = (evt) => {
2124
+ const pointerEvt = evt;
2125
+ const target = pointerEvt.target;
2126
+ const barGroup = target.closest("[data-gantt-bar]");
2127
+ const taskId = barGroup?.dataset.ganttBar;
2128
+ if (taskId && this.callbacks.onContextMenu) {
2129
+ pointerEvt.preventDefault();
2130
+ this.callbacks.onContextMenu(taskId, pointerEvt);
2131
+ }
2132
+ };
2133
+ this.onSvgDoubleClick = (evt) => {
2134
+ if (!this.callbacks.onLinkDblClick) return;
2135
+ const target = evt.target;
2136
+ const linkPath2 = target.closest("[data-gantt-link]");
2137
+ const key = linkPath2?.dataset.ganttLink;
2138
+ if (!key) return;
2139
+ const [fromId, toId] = key.split("->");
2140
+ if (fromId && toId) this.callbacks.onLinkDblClick(fromId, toId);
2141
+ };
2142
+ this.onPointerDown = (evt) => {
2143
+ const target = evt.target;
2144
+ const connector = target.closest("[data-gantt-connector]");
2145
+ if (connector) {
2146
+ const taskId = connector.dataset.ganttConnector;
2147
+ const side = connector.dataset.ganttConnectorSide ?? "right";
2148
+ if (taskId) this.startLinkDrag(evt, taskId, side);
2149
+ return;
2150
+ }
2151
+ const handle = target.closest("[data-gantt-handle]");
2152
+ if (handle) {
2153
+ const taskId = handle.dataset.ganttHandleFor;
2154
+ const edge = handle.dataset.ganttHandle;
2155
+ if (taskId && !this.isReadonly(taskId)) this.startResize(evt, taskId, edge);
2156
+ return;
2157
+ }
2158
+ const barGroup = target.closest("[data-gantt-bar]");
2159
+ if (barGroup) {
2160
+ const taskId = barGroup.dataset.ganttBar;
2161
+ if (taskId) this.startMove(evt, taskId, barGroup);
2162
+ return;
2163
+ }
2164
+ const rowBg = target.closest("[data-gantt-row-bg]");
2165
+ if (rowBg && this.callbacks.onCreateTaskDrag && !this.options.readonly) {
2166
+ const taskId = rowBg.dataset.ganttRowBg;
2167
+ if (taskId) this.startCreateDrag(evt, taskId, rowBg);
2168
+ }
2169
+ };
2170
+ /**
2171
+ * Delegates to handleBarKeyDown when a focused bar dispatched the event
2172
+ * (bars get tabindex="0" when keyboardAccessible, so native Tab/Shift+Tab
2173
+ * traversal between them already works without any code here), and
2174
+ * otherwise checks the chart-wide shortcuts (Ctrl/Cmd+A, Home, End).
2175
+ */
2176
+ this.onKeyDown = (evt) => {
2177
+ if (!this.options.keyboardAccessible) return;
2178
+ const target = evt.target;
2179
+ const barGroup = target.closest?.("[data-gantt-bar]");
2180
+ const taskId = barGroup?.getAttribute("data-gantt-bar");
2181
+ if (taskId) {
2182
+ this.handleBarKeyDown(taskId, evt);
2183
+ return;
2184
+ }
2185
+ if ((evt.ctrlKey || evt.metaKey) && evt.key.toLowerCase() === "a") {
2186
+ evt.preventDefault();
2187
+ this.callbacks.onSelectAll?.();
2188
+ } else if (evt.key === "Home") {
2189
+ evt.preventDefault();
2190
+ this.callbacks.onJumpToStart?.();
2191
+ } else if (evt.key === "End") {
2192
+ evt.preventDefault();
2193
+ this.callbacks.onJumpToEnd?.();
2194
+ }
2195
+ };
2196
+ this.attach();
2197
+ }
2198
+ isReadonly(taskId) {
2199
+ if (this.options.readonly) return true;
2200
+ return this.options.isTaskReadonly?.(taskId) ?? false;
2201
+ }
2202
+ attach() {
2203
+ this.svg.addEventListener("pointerdown", this.onPointerDown);
2204
+ this.gridPanel.addEventListener("click", this.onGridClick);
2205
+ if (typeof document !== "undefined") {
2206
+ this.svg.addEventListener("contextmenu", this.onContextMenu);
2207
+ this.svg.addEventListener("dblclick", this.onSvgDoubleClick);
2208
+ this.svg.addEventListener("keydown", this.onKeyDown);
2209
+ }
2210
+ }
2211
+ destroy() {
2212
+ this.svg.removeEventListener("pointerdown", this.onPointerDown);
2213
+ this.gridPanel.removeEventListener("click", this.onGridClick);
2214
+ this.svg.removeEventListener("contextmenu", this.onContextMenu);
2215
+ this.svg.removeEventListener("dblclick", this.onSvgDoubleClick);
2216
+ this.svg.removeEventListener("keydown", this.onKeyDown);
2217
+ if (this.rafHandle !== null && typeof cancelAnimationFrame !== "undefined") {
2218
+ cancelAnimationFrame(this.rafHandle);
2219
+ }
2220
+ }
2221
+ startMove(evt, taskId, barGroup) {
2222
+ if (this.isReadonly(taskId)) return;
2223
+ this.dragMode = "move";
2224
+ this.dragTaskId = taskId;
2225
+ this.dragStartX = evt.clientX;
2226
+ let moved = false;
2227
+ barGroup.setPointerCapture(evt.pointerId);
2228
+ const onMove = (moveEvt) => {
2229
+ const dx = moveEvt.clientX - this.dragStartX;
2230
+ if (Math.abs(dx) > 2) moved = true;
2231
+ this.scheduleDeltaEmit(taskId, "move", dx);
2232
+ };
2233
+ const onUp = (upEvt) => {
2234
+ barGroup.removeEventListener("pointermove", onMove);
2235
+ barGroup.removeEventListener("pointerup", onUp);
2236
+ this.flushPending();
2237
+ this.dragMode = null;
2238
+ this.dragTaskId = null;
2239
+ if (!moved) {
2240
+ this.callbacks.onBarClick(taskId, {
2241
+ ctrlKey: upEvt.ctrlKey,
2242
+ metaKey: upEvt.metaKey,
2243
+ shiftKey: upEvt.shiftKey
2244
+ });
2245
+ }
2246
+ };
2247
+ barGroup.addEventListener("pointermove", onMove);
2248
+ barGroup.addEventListener("pointerup", onUp);
2249
+ }
2250
+ startResize(evt, taskId, edge) {
2251
+ this.dragMode = edge === "left" ? "resize-left" : "resize-right";
2252
+ this.dragTaskId = taskId;
2253
+ this.dragStartX = evt.clientX;
2254
+ const target = evt.currentTarget;
2255
+ const capture = target instanceof Element ? target : this.svg;
2256
+ capture.setPointerCapture?.(evt.pointerId);
2257
+ const onMove = (moveEvt) => {
2258
+ const dx = moveEvt.clientX - this.dragStartX;
2259
+ this.scheduleDeltaEmit(taskId, edge === "left" ? "resize-left" : "resize-right", dx);
2260
+ };
2261
+ const onUp = () => {
2262
+ this.svg.removeEventListener("pointermove", onMove);
2263
+ this.svg.removeEventListener("pointerup", onUp);
2264
+ this.flushPending();
2265
+ this.dragMode = null;
2266
+ this.dragTaskId = null;
2267
+ };
2268
+ this.svg.addEventListener("pointermove", onMove);
2269
+ this.svg.addEventListener("pointerup", onUp);
2270
+ }
2271
+ scheduleDeltaEmit(taskId, mode, dxPx) {
2272
+ const pxPerMs2 = this.options.pxPerMs();
2273
+ const dxMs = pxPerMs2 > 0 ? dxPx / pxPerMs2 : 0;
2274
+ this.pendingDxMs = snapDeltaMs(dxMs, this.options.snapToUnit);
2275
+ this.pendingMode = mode;
2276
+ if (this.rafHandle !== null) return;
2277
+ const raf = typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : (cb) => {
2278
+ cb();
2279
+ return 0;
2280
+ };
2281
+ this.rafHandle = raf(() => {
2282
+ this.rafHandle = null;
2283
+ if (!this.dragTaskId || !this.pendingMode) return;
2284
+ if (this.pendingMode === "move") {
2285
+ this.callbacks.onMove(taskId, this.pendingDxMs);
2286
+ } else {
2287
+ this.callbacks.onResize(
2288
+ taskId,
2289
+ this.pendingMode === "resize-left" ? "left" : "right",
2290
+ this.pendingDxMs
2291
+ );
2292
+ }
2293
+ });
2294
+ }
2295
+ flushPending() {
2296
+ if (this.rafHandle !== null && typeof cancelAnimationFrame !== "undefined") {
2297
+ cancelAnimationFrame(this.rafHandle);
2298
+ this.rafHandle = null;
2299
+ }
2300
+ }
2301
+ startLinkDrag(evt, taskId, anchor) {
2302
+ this.dragMode = "link";
2303
+ this.linkFromId = taskId;
2304
+ this.linkFromAnchor = anchor;
2305
+ this.callbacks.onLinkStart(taskId, anchor);
2306
+ const svgPoint = (clientX, clientY) => {
2307
+ const rect = this.svg.getBoundingClientRect();
2308
+ return { x: clientX - rect.left + this.svg.scrollLeft, y: clientY - rect.top };
2309
+ };
2310
+ const startPos = svgPoint(evt.clientX, evt.clientY);
2311
+ const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
2312
+ line.setAttribute("x1", String(startPos.x));
2313
+ line.setAttribute("y1", String(startPos.y));
2314
+ line.setAttribute("x2", String(startPos.x));
2315
+ line.setAttribute("y2", String(startPos.y));
2316
+ line.setAttribute("stroke", "#94a3b8");
2317
+ line.setAttribute("stroke-dasharray", "4 3");
2318
+ line.classList.add("gantt-temp-link");
2319
+ this.svg.appendChild(line);
2320
+ this.tempLinkEl = line;
2321
+ const onMove = (moveEvt) => {
2322
+ const pos = svgPoint(moveEvt.clientX, moveEvt.clientY);
2323
+ line.setAttribute("x2", String(pos.x));
2324
+ line.setAttribute("y2", String(pos.y));
2325
+ };
2326
+ const onUp = (upEvt) => {
2327
+ window.removeEventListener("pointermove", onMove);
2328
+ window.removeEventListener("pointerup", onUp);
2329
+ this.tempLinkEl?.remove();
2330
+ this.tempLinkEl = null;
2331
+ const target = document.elementFromPoint(upEvt.clientX, upEvt.clientY);
2332
+ const connector = target?.closest("[data-gantt-connector]");
2333
+ const toId = connector?.dataset.ganttConnector;
2334
+ if (toId && this.linkFromId && toId !== this.linkFromId) {
2335
+ this.callbacks.onLinkComplete(this.linkFromId, toId);
2336
+ } else {
2337
+ this.callbacks.onLinkCancel();
2338
+ }
2339
+ this.dragMode = null;
2340
+ this.linkFromId = null;
2341
+ this.linkFromAnchor = null;
2342
+ };
2343
+ window.addEventListener("pointermove", onMove);
2344
+ window.addEventListener("pointerup", onUp);
2345
+ }
2346
+ startCreateDrag(evt, rowTaskId, rowBg) {
2347
+ this.dragMode = "create";
2348
+ const parent = rowBg.parentNode;
2349
+ const rowY = Number(rowBg.getAttribute("y") ?? "0");
2350
+ const rowHeight = Number(rowBg.getAttribute("height") ?? "0");
2351
+ const svgX = (clientX) => {
2352
+ const rect = this.svg.getBoundingClientRect();
2353
+ return clientX - rect.left + this.svg.scrollLeft;
2354
+ };
2355
+ const startX = svgX(evt.clientX);
2356
+ let moved = false;
2357
+ const preview = document.createElementNS("http://www.w3.org/2000/svg", "rect");
2358
+ preview.setAttribute("y", String(rowY + 2));
2359
+ preview.setAttribute("height", String(Math.max(0, rowHeight - 4)));
2360
+ preview.setAttribute("x", String(startX));
2361
+ preview.setAttribute("width", "0");
2362
+ preview.setAttribute("fill", "currentColor");
2363
+ preview.setAttribute("opacity", "0.25");
2364
+ preview.classList.add("gantt-create-preview");
2365
+ parent?.appendChild(preview);
2366
+ const onMove = (moveEvt) => {
2367
+ const currentX = svgX(moveEvt.clientX);
2368
+ if (Math.abs(currentX - startX) > 2) moved = true;
2369
+ const x1 = Math.min(startX, currentX);
2370
+ const x2 = Math.max(startX, currentX);
2371
+ preview.setAttribute("x", String(x1));
2372
+ preview.setAttribute("width", String(x2 - x1));
2373
+ };
2374
+ const onUp = (upEvt) => {
2375
+ window.removeEventListener("pointermove", onMove);
2376
+ window.removeEventListener("pointerup", onUp);
2377
+ preview.remove();
2378
+ this.dragMode = null;
2379
+ if (moved) {
2380
+ const endX = svgX(upEvt.clientX);
2381
+ this.callbacks.onCreateTaskDrag?.(rowTaskId, Math.min(startX, endX), Math.max(startX, endX));
2382
+ }
2383
+ };
2384
+ window.addEventListener("pointermove", onMove);
2385
+ window.addEventListener("pointerup", onUp);
2386
+ }
2387
+ handleBarKeyDown(taskId, evt) {
2388
+ if (!this.options.keyboardAccessible) return;
2389
+ if (this.isReadonly(taskId)) return;
2390
+ if (evt.key === "ArrowLeft") {
2391
+ evt.preventDefault();
2392
+ this.callbacks.onMove(taskId, -MS_PER_DAY);
2393
+ } else if (evt.key === "ArrowRight") {
2394
+ evt.preventDefault();
2395
+ this.callbacks.onMove(taskId, MS_PER_DAY);
2396
+ } else if (evt.key === "Enter") {
2397
+ evt.preventDefault();
2398
+ this.callbacks.onBarClick(taskId);
2399
+ }
2400
+ }
2401
+ };
2402
+
2403
+ // src/index.ts
2404
+ var HAS_DOM = typeof document !== "undefined" && typeof window !== "undefined";
2405
+ var DEFAULT_COLUMNS = [{ id: "name", title: "Name" }];
2406
+ function cloneTask(task) {
2407
+ return { ...task };
2408
+ }
2409
+ var _GanttChart = class _GanttChart {
2410
+ constructor(container, tasks, dependencies = [], options = {}) {
2411
+ this.container = container;
2412
+ this.darkMediaQuery = null;
2413
+ this.handleSchemeChange = () => {
2414
+ if (this.colorScheme !== "auto") return;
2415
+ this.theme = mergeTheme(this.explicitTheme, "auto");
2416
+ this.scheduleRender();
2417
+ };
2418
+ this.emitter = new EventEmitter();
2419
+ this.history = null;
2420
+ this.renderer = null;
2421
+ this.interactions = null;
2422
+ this.renderModel = null;
2423
+ this.rafHandle = null;
2424
+ this.currentPage = 1;
2425
+ this.selectedTaskIds = /* @__PURE__ */ new Set();
2426
+ this.lastSelectedId = null;
2427
+ this.tasks = tasks.map(cloneTask);
2428
+ this.dependencies = dependencies.map((d) => ({ ...d }));
2429
+ this.colorScheme = options.colorScheme ?? "auto";
2430
+ this.explicitTheme = options.theme;
2431
+ this.theme = mergeTheme(options.theme, this.colorScheme);
2432
+ this.columns = options.columns ?? DEFAULT_COLUMNS;
2433
+ this.columnWidth = options.columnWidth ?? 32;
2434
+ this.currentPage = options.pagination?.page ?? 1;
2435
+ this.options = {
2436
+ viewMode: options.viewMode ?? "day",
2437
+ readonly: options.readonly ?? false,
2438
+ showProgress: options.showProgress ?? true,
2439
+ showDependencies: options.showDependencies ?? true,
2440
+ gridPanelWidth: options.gridPanelWidth ?? 260,
2441
+ showCriticalPath: options.showCriticalPath ?? false,
2442
+ showBaseline: options.showBaseline ?? false,
2443
+ showDeadlines: options.showDeadlines ?? true,
2444
+ showAssigneeAvatars: options.showAssigneeAvatars ?? false,
2445
+ enableHistory: options.enableHistory ?? false,
2446
+ keyboardAccessible: options.keyboardAccessible ?? false,
2447
+ virtualScroll: options.virtualScroll ?? true,
2448
+ autoSchedule: options.autoSchedule ?? false,
2449
+ autoRollupProgress: options.autoRollupProgress ?? false,
2450
+ selectable: options.selectable ?? false,
2451
+ columnWidth: options.columnWidth,
2452
+ theme: options.theme,
2453
+ columns: options.columns,
2454
+ calendar: options.calendar,
2455
+ markers: options.markers,
2456
+ pagination: options.pagination,
2457
+ snapToUnit: options.snapToUnit,
2458
+ onDateChange: options.onDateChange,
2459
+ onProgressChange: options.onProgressChange,
2460
+ onDependencyCreate: options.onDependencyCreate,
2461
+ onDependencyRemove: options.onDependencyRemove,
2462
+ onDependencyDblClick: options.onDependencyDblClick,
2463
+ onTaskClick: options.onTaskClick,
2464
+ onGroupToggle: options.onGroupToggle,
2465
+ onContextMenu: options.onContextMenu,
2466
+ onTaskCreate: options.onTaskCreate,
2467
+ onColumnResize: options.onColumnResize,
2468
+ onColumnReorder: options.onColumnReorder,
2469
+ onTaskReorder: options.onTaskReorder,
2470
+ onSelectionChange: options.onSelectionChange
2471
+ };
2472
+ if (this.options.enableHistory) {
2473
+ this.history = new HistoryManager((state) => this.emitter.emit("history-change", state));
2474
+ }
2475
+ if (HAS_DOM) {
2476
+ this.setupDom();
2477
+ }
2478
+ this.scheduleRender(true);
2479
+ }
2480
+ setupDom() {
2481
+ this.container.style.setProperty("--gantt-grid-panel-width", `${this.options.gridPanelWidth}px`);
2482
+ if (typeof window.matchMedia === "function") {
2483
+ this.darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
2484
+ this.darkMediaQuery.addEventListener?.("change", this.handleSchemeChange);
2485
+ }
2486
+ this.renderer = new GanttRenderer(this.container, {
2487
+ keyboardAccessible: this.options.keyboardAccessible,
2488
+ showAssigneeAvatars: this.options.showAssigneeAvatars,
2489
+ virtualScroll: this.options.virtualScroll,
2490
+ onRenameCommit: (taskId, name) => this.updateTask(taskId, { name }),
2491
+ onColumnResize: this.options.onColumnResize ? (columnId, width) => this.handleColumnResize(columnId, width) : void 0,
2492
+ onColumnReorder: this.options.onColumnReorder ? (order) => this.handleColumnReorder(order) : void 0,
2493
+ onRowReorder: this.options.onTaskReorder ? (draggedId, targetId, position) => this.handleRowReorder(draggedId, targetId, position) : void 0
2494
+ });
2495
+ this.interactions = new InteractionController(
2496
+ this.renderer.svg,
2497
+ this.renderer.gridPanel,
2498
+ {
2499
+ onMove: (taskId, dxMs) => this.handleMove(taskId, dxMs),
2500
+ onResize: (taskId, edge, dxMs) => this.handleResize(taskId, edge, dxMs),
2501
+ onLinkStart: () => {
2502
+ },
2503
+ onLinkComplete: (fromId, toId) => this.handleLinkComplete(fromId, toId),
2504
+ onLinkCancel: () => {
2505
+ },
2506
+ onToggleCollapse: (taskId) => this.toggleGroup(taskId),
2507
+ onBarClick: (taskId, modifiers) => this.handleBarClick(taskId, modifiers),
2508
+ onProgressDrag: (taskId, newProgress) => this.handleProgressDrag(taskId, newProgress),
2509
+ // Only wired when the consumer opted in via onTaskCreate - like onContextMenu below,
2510
+ // this keeps drag-to-create off by default instead of silently mutating task state.
2511
+ onCreateTaskDrag: this.options.onTaskCreate ? (rowTaskId, startXPx, endXPx) => this.handleCreateTaskDrag(rowTaskId, startXPx, endXPx) : void 0,
2512
+ onLinkDblClick: this.options.onDependencyDblClick ? (fromId, toId) => {
2513
+ const dep = this.dependencies.find((d) => d.fromId === fromId && d.toId === toId);
2514
+ if (dep) this.options.onDependencyDblClick?.(dep);
2515
+ } : void 0,
2516
+ onContextMenu: this.options.onContextMenu ? (taskId, evt) => {
2517
+ const task = this.tasks.find((t) => t.id === taskId);
2518
+ if (task) this.emitter.emit("context-menu", { task, evt });
2519
+ this.options.onContextMenu?.(task ?? { id: taskId }, evt);
2520
+ } : void 0,
2521
+ onSelectAll: this.options.selectable ? () => this.selectAllVisible() : void 0,
2522
+ onJumpToStart: () => this.scrollToRangeStart(),
2523
+ onJumpToEnd: () => this.scrollToRangeEnd()
2524
+ },
2525
+ {
2526
+ readonly: this.options.readonly,
2527
+ isTaskReadonly: (taskId) => !!this.tasks.find((t) => t.id === taskId)?.readonly,
2528
+ keyboardAccessible: this.options.keyboardAccessible,
2529
+ snapToUnit: this.options.snapToUnit,
2530
+ pxPerMs: () => pxPerMs(this.options.viewMode, this.columnWidth)
2531
+ }
2532
+ );
2533
+ }
2534
+ handleMove(taskId, dxMs) {
2535
+ if (dxMs === 0) return;
2536
+ const task = this.tasks.find((t) => t.id === taskId);
2537
+ if (!task || task.readonly || this.options.readonly) return;
2538
+ const newStart = new Date(task.start.getTime() + dxMs);
2539
+ const newEnd = new Date(task.end.getTime() + dxMs);
2540
+ const newSegments = task.segments?.map((s) => ({
2541
+ start: new Date(s.start.getTime() + dxMs),
2542
+ end: new Date(s.end.getTime() + dxMs)
2543
+ }));
2544
+ this.applyDateChange(taskId, newStart, newEnd, newSegments);
2545
+ }
2546
+ handleResize(taskId, edge, dxMs) {
2547
+ if (dxMs === 0) return;
2548
+ const task = this.tasks.find((t) => t.id === taskId);
2549
+ if (!task || task.readonly || this.options.readonly) return;
2550
+ const newStart = edge === "left" ? new Date(task.start.getTime() + dxMs) : task.start;
2551
+ const newEnd = edge === "right" ? new Date(task.end.getTime() + dxMs) : task.end;
2552
+ if (newEnd.getTime() <= newStart.getTime()) return;
2553
+ let newSegments = task.segments?.map((s) => ({ ...s }));
2554
+ if (newSegments && newSegments.length > 0) {
2555
+ if (edge === "left") newSegments[0] = { ...newSegments[0], start: newStart };
2556
+ else newSegments[newSegments.length - 1] = { ...newSegments[newSegments.length - 1], end: newEnd };
2557
+ }
2558
+ this.applyDateChange(taskId, newStart, newEnd, newSegments);
2559
+ }
2560
+ applyDateChange(taskId, newStart, newEnd, newSegments) {
2561
+ const task = this.tasks.find((t) => t.id === taskId);
2562
+ if (!task) return;
2563
+ const before = [
2564
+ { id: taskId, start: task.start, end: task.end, segments: task.segments }
2565
+ ];
2566
+ const after = [
2567
+ { id: taskId, start: newStart, end: newEnd, segments: newSegments }
2568
+ ];
2569
+ if (this.options.autoSchedule) {
2570
+ const cascade = this.computeCascade(taskId, newStart, newEnd);
2571
+ for (const c of cascade) {
2572
+ before.push({ id: c.id, start: c.oldStart, end: c.oldEnd });
2573
+ after.push({ id: c.id, start: c.newStart, end: c.newEnd });
2574
+ }
2575
+ }
2576
+ const applyState = (state) => {
2577
+ for (const s of state) {
2578
+ const t = this.tasks.find((tt) => tt.id === s.id);
2579
+ if (t) {
2580
+ t.start = s.start;
2581
+ t.end = s.end;
2582
+ if (s.segments !== void 0) t.segments = s.segments;
2583
+ }
2584
+ }
2585
+ };
2586
+ const cmd = {
2587
+ label: "move-or-resize",
2588
+ do: () => {
2589
+ applyState(after);
2590
+ this.emitDateChangeEvents(after.map((a) => a.id));
2591
+ },
2592
+ undo: () => {
2593
+ applyState(before);
2594
+ this.emitDateChangeEvents(before.map((b) => b.id));
2595
+ }
2596
+ };
2597
+ this.runCommand(cmd);
2598
+ }
2599
+ emitDateChangeEvents(ids) {
2600
+ for (const id of ids) {
2601
+ const t = this.tasks.find((tt) => tt.id === id);
2602
+ if (!t) continue;
2603
+ this.emitter.emit("date-change", { task: t, start: t.start, end: t.end });
2604
+ this.options.onDateChange?.(t, t.start, t.end);
2605
+ }
2606
+ this.emitter.emit("tasks-change", { tasks: this.getTasks() });
2607
+ this.scheduleRender();
2608
+ }
2609
+ /** Shift successors that would violate their dependency constraint after `taskId` moves to [newStart, newEnd]. */
2610
+ computeCascade(taskId, newStart, newEnd) {
2611
+ const results = [];
2612
+ const simulated = /* @__PURE__ */ new Map();
2613
+ simulated.set(taskId, { start: newStart, end: newEnd });
2614
+ const outgoing = /* @__PURE__ */ new Map();
2615
+ for (const dep of this.dependencies) {
2616
+ const list = outgoing.get(dep.fromId) ?? [];
2617
+ list.push(dep);
2618
+ outgoing.set(dep.fromId, list);
2619
+ }
2620
+ const queue = [taskId];
2621
+ const visited = /* @__PURE__ */ new Set();
2622
+ let ops = 0;
2623
+ const maxOps = this.tasks.length * 2 + 5;
2624
+ while (queue.length > 0 && ops++ < maxOps) {
2625
+ const currentId = queue.shift();
2626
+ if (currentId === void 0) break;
2627
+ const currentState = simulated.get(currentId);
2628
+ if (!currentState) continue;
2629
+ for (const dep of outgoing.get(currentId) ?? []) {
2630
+ if (visited.has(dep.toId) && !simulated.has(dep.toId)) continue;
2631
+ const succTask = this.tasks.find((t) => t.id === dep.toId);
2632
+ if (!succTask) continue;
2633
+ const succState = simulated.get(dep.toId) ?? { start: succTask.start, end: succTask.end };
2634
+ const lag = dep.lagMs ?? 0;
2635
+ const duration = succState.end.getTime() - succState.start.getTime();
2636
+ let requiredStart = null;
2637
+ let requiredEnd = null;
2638
+ if (dep.type === "FS") requiredStart = currentState.end.getTime() + lag;
2639
+ else if (dep.type === "SS") requiredStart = currentState.start.getTime() + lag;
2640
+ else if (dep.type === "FF") requiredEnd = currentState.end.getTime() + lag;
2641
+ else if (dep.type === "SF") requiredEnd = currentState.start.getTime() + lag;
2642
+ let newSuccStart = succState.start.getTime();
2643
+ let newSuccEnd = succState.end.getTime();
2644
+ let violated = false;
2645
+ if (requiredStart !== null && succState.start.getTime() < requiredStart) {
2646
+ newSuccStart = requiredStart;
2647
+ newSuccEnd = requiredStart + duration;
2648
+ violated = true;
2649
+ } else if (requiredEnd !== null && succState.end.getTime() < requiredEnd) {
2650
+ newSuccEnd = requiredEnd;
2651
+ newSuccStart = requiredEnd - duration;
2652
+ violated = true;
2653
+ }
2654
+ if (violated && !visited.has(dep.toId)) {
2655
+ visited.add(dep.toId);
2656
+ let newState = { start: new Date(newSuccStart), end: new Date(newSuccEnd) };
2657
+ newState = applyConstraint(newState, succTask.constraintType, succTask.constraintDate);
2658
+ if (this.options.calendar) {
2659
+ const shiftedStart = this.options.calendar.workingHours ? shiftToWorkingTime(newState.start, this.options.calendar, 1) : shiftToWorkingDay(newState.start, this.options.calendar, 1);
2660
+ if (shiftedStart.getTime() !== newState.start.getTime()) {
2661
+ const dur = newState.end.getTime() - newState.start.getTime();
2662
+ newState = { start: shiftedStart, end: new Date(shiftedStart.getTime() + dur) };
2663
+ }
2664
+ }
2665
+ simulated.set(dep.toId, newState);
2666
+ results.push({
2667
+ id: dep.toId,
2668
+ oldStart: succTask.start,
2669
+ oldEnd: succTask.end,
2670
+ newStart: newState.start,
2671
+ newEnd: newState.end
2672
+ });
2673
+ queue.push(dep.toId);
2674
+ }
2675
+ }
2676
+ }
2677
+ return results;
2678
+ }
2679
+ handleLinkComplete(fromId, toId) {
2680
+ const dep = { fromId, toId, type: "FS" };
2681
+ const cmd = {
2682
+ label: "link-create",
2683
+ do: () => {
2684
+ this.dependencies.push(dep);
2685
+ this.emitter.emit("dependency-create", dep);
2686
+ this.options.onDependencyCreate?.(dep);
2687
+ this.scheduleRender();
2688
+ },
2689
+ undo: () => {
2690
+ this.dependencies = this.dependencies.filter((d) => d !== dep);
2691
+ this.emitter.emit("dependency-remove", dep);
2692
+ this.options.onDependencyRemove?.(dep);
2693
+ this.scheduleRender();
2694
+ }
2695
+ };
2696
+ this.runCommand(cmd);
2697
+ }
2698
+ removeDependency(fromId, toId) {
2699
+ const dep = this.dependencies.find((d) => d.fromId === fromId && d.toId === toId);
2700
+ if (!dep) return;
2701
+ const cmd = {
2702
+ label: "link-remove",
2703
+ do: () => {
2704
+ this.dependencies = this.dependencies.filter((d) => d !== dep);
2705
+ this.emitter.emit("dependency-remove", dep);
2706
+ this.options.onDependencyRemove?.(dep);
2707
+ this.scheduleRender();
2708
+ },
2709
+ undo: () => {
2710
+ this.dependencies.push(dep);
2711
+ this.emitter.emit("dependency-create", dep);
2712
+ this.options.onDependencyCreate?.(dep);
2713
+ this.scheduleRender();
2714
+ }
2715
+ };
2716
+ this.runCommand(cmd);
2717
+ }
2718
+ /** Update a dependency's type/lag in place (e.g. from your own "edit this link" UI opened via onDependencyDblClick). Undoable. */
2719
+ updateDependency(fromId, toId, partial) {
2720
+ const dep = this.dependencies.find((d) => d.fromId === fromId && d.toId === toId);
2721
+ if (!dep) return;
2722
+ const before = { ...dep };
2723
+ const after = { ...dep, ...partial, fromId: dep.fromId, toId: dep.toId };
2724
+ const cmd = {
2725
+ label: "link-update",
2726
+ do: () => {
2727
+ Object.assign(dep, after);
2728
+ this.emitter.emit("dependency-create", dep);
2729
+ this.options.onDependencyCreate?.(dep);
2730
+ this.scheduleRender();
2731
+ },
2732
+ undo: () => {
2733
+ Object.assign(dep, before);
2734
+ this.emitter.emit("dependency-create", dep);
2735
+ this.options.onDependencyCreate?.(dep);
2736
+ this.scheduleRender();
2737
+ }
2738
+ };
2739
+ this.runCommand(cmd);
2740
+ }
2741
+ handleProgressDrag(taskId, newProgress) {
2742
+ const task = this.tasks.find((t) => t.id === taskId);
2743
+ if (!task || task.readonly || this.options.readonly) return;
2744
+ const oldProgress = task.progress ?? 0;
2745
+ const clamped = Math.min(100, Math.max(0, newProgress));
2746
+ const cmd = {
2747
+ label: "progress-change",
2748
+ do: () => {
2749
+ task.progress = clamped;
2750
+ this.emitter.emit("progress-change", { task, progress: clamped });
2751
+ this.options.onProgressChange?.(task, clamped);
2752
+ this.scheduleRender();
2753
+ },
2754
+ undo: () => {
2755
+ task.progress = oldProgress;
2756
+ this.emitter.emit("progress-change", { task, progress: oldProgress });
2757
+ this.options.onProgressChange?.(task, oldProgress);
2758
+ this.scheduleRender();
2759
+ }
2760
+ };
2761
+ this.runCommand(cmd);
2762
+ }
2763
+ handleCreateTaskDrag(rowTaskId, startXPx, endXPx) {
2764
+ if (!this.renderModel) return;
2765
+ const scale = pxPerMs(this.options.viewMode, this.columnWidth);
2766
+ if (scale <= 0) return;
2767
+ const rangeStartMs = this.renderModel.rangeStart.getTime();
2768
+ let start = new Date(rangeStartMs + startXPx / scale);
2769
+ let end = new Date(rangeStartMs + endXPx / scale);
2770
+ if (this.options.snapToUnit) {
2771
+ start = snapToGrid(start, this.options.snapToUnit);
2772
+ end = snapToGrid(end, this.options.snapToUnit);
2773
+ }
2774
+ if (end.getTime() <= start.getTime()) end = new Date(start.getTime() + MS_PER_DAY);
2775
+ const rowTask = this.tasks.find((t) => t.id === rowTaskId);
2776
+ const newTask = {
2777
+ id: `task-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
2778
+ name: "New task",
2779
+ start,
2780
+ end,
2781
+ parentId: rowTask?.parentId ?? null
2782
+ };
2783
+ const cmd = {
2784
+ label: "task-create",
2785
+ do: () => {
2786
+ const index = this.tasks.findIndex((t) => t.id === rowTaskId);
2787
+ if (index >= 0) this.tasks.splice(index + 1, 0, newTask);
2788
+ else this.tasks.push(newTask);
2789
+ this.emitter.emit("task-create", { task: newTask });
2790
+ this.options.onTaskCreate?.(newTask);
2791
+ this.emitter.emit("tasks-change", { tasks: this.getTasks() });
2792
+ this.scheduleRender();
2793
+ },
2794
+ undo: () => {
2795
+ this.tasks = this.tasks.filter((t) => t.id !== newTask.id);
2796
+ this.emitter.emit("tasks-change", { tasks: this.getTasks() });
2797
+ this.scheduleRender();
2798
+ }
2799
+ };
2800
+ this.runCommand(cmd);
2801
+ }
2802
+ handleBarClick(taskId, modifiers) {
2803
+ const task = this.tasks.find((t) => t.id === taskId);
2804
+ if (!task) return;
2805
+ if (this.options.selectable) {
2806
+ if (modifiers?.shiftKey && this.lastSelectedId && this.renderModel) {
2807
+ const ids = this.renderModel.rows.map((r) => r.taskId);
2808
+ const i1 = ids.indexOf(this.lastSelectedId);
2809
+ const i2 = ids.indexOf(taskId);
2810
+ if (i1 >= 0 && i2 >= 0) {
2811
+ const [lo, hi] = i1 < i2 ? [i1, i2] : [i2, i1];
2812
+ this.selectedTaskIds = new Set(ids.slice(lo, hi + 1));
2813
+ }
2814
+ } else if (modifiers?.ctrlKey || modifiers?.metaKey) {
2815
+ if (this.selectedTaskIds.has(taskId)) this.selectedTaskIds.delete(taskId);
2816
+ else this.selectedTaskIds.add(taskId);
2817
+ this.lastSelectedId = taskId;
2818
+ } else {
2819
+ this.selectedTaskIds = /* @__PURE__ */ new Set([taskId]);
2820
+ this.lastSelectedId = taskId;
2821
+ }
2822
+ this.emitter.emit("selection-change", { taskIds: this.getSelectedTaskIds() });
2823
+ this.options.onSelectionChange?.(this.getSelectedTaskIds());
2824
+ this.scheduleRender();
2825
+ }
2826
+ this.emitter.emit("task-click", { task });
2827
+ this.options.onTaskClick?.(task);
2828
+ }
2829
+ /** Replace (default) or add to (`additive: true`) the current selection. Requires `selectable`. */
2830
+ selectTask(id, options = {}) {
2831
+ if (!options.additive) this.selectedTaskIds.clear();
2832
+ this.selectedTaskIds.add(id);
2833
+ this.lastSelectedId = id;
2834
+ this.emitter.emit("selection-change", { taskIds: this.getSelectedTaskIds() });
2835
+ this.options.onSelectionChange?.(this.getSelectedTaskIds());
2836
+ this.scheduleRender();
2837
+ }
2838
+ clearSelection() {
2839
+ this.selectedTaskIds.clear();
2840
+ this.lastSelectedId = null;
2841
+ this.emitter.emit("selection-change", { taskIds: [] });
2842
+ this.options.onSelectionChange?.([]);
2843
+ this.scheduleRender();
2844
+ }
2845
+ getSelectedTaskIds() {
2846
+ return Array.from(this.selectedTaskIds);
2847
+ }
2848
+ /** Shift every selected task's start/end by `deltaMs`. Single undoable step. */
2849
+ bulkShiftDates(deltaMs) {
2850
+ if (deltaMs === 0 || this.selectedTaskIds.size === 0) return;
2851
+ const ids = Array.from(this.selectedTaskIds);
2852
+ const before = ids.map((id) => this.tasks.find((t) => t.id === id)).filter((t) => !!t).map((t) => ({ id: t.id, start: t.start, end: t.end }));
2853
+ const after = before.map((b) => ({
2854
+ id: b.id,
2855
+ start: new Date(b.start.getTime() + deltaMs),
2856
+ end: new Date(b.end.getTime() + deltaMs)
2857
+ }));
2858
+ const applyState = (state) => {
2859
+ for (const s of state) {
2860
+ const t = this.tasks.find((tt) => tt.id === s.id);
2861
+ if (t) {
2862
+ t.start = s.start;
2863
+ t.end = s.end;
2864
+ }
2865
+ }
2866
+ };
2867
+ const cmd = {
2868
+ label: "bulk-shift",
2869
+ do: () => {
2870
+ applyState(after);
2871
+ this.emitDateChangeEvents(after.map((a) => a.id));
2872
+ },
2873
+ undo: () => {
2874
+ applyState(before);
2875
+ this.emitDateChangeEvents(before.map((b) => b.id));
2876
+ }
2877
+ };
2878
+ this.runCommand(cmd);
2879
+ }
2880
+ /** Delete every selected task (and any dependency touching one). Single undoable step. */
2881
+ bulkDelete() {
2882
+ if (this.selectedTaskIds.size === 0) return;
2883
+ const ids = Array.from(this.selectedTaskIds);
2884
+ const idSet = new Set(ids);
2885
+ const removedTasks = this.tasks.filter((t) => idSet.has(t.id));
2886
+ const removedIndices = removedTasks.map((t) => this.tasks.indexOf(t));
2887
+ const removedDeps = this.dependencies.filter((d) => idSet.has(d.fromId) || idSet.has(d.toId));
2888
+ const cmd = {
2889
+ label: "bulk-delete",
2890
+ do: () => {
2891
+ this.tasks = this.tasks.filter((t) => !idSet.has(t.id));
2892
+ this.dependencies = this.dependencies.filter((d) => !idSet.has(d.fromId) && !idSet.has(d.toId));
2893
+ this.selectedTaskIds.clear();
2894
+ this.emitter.emit("selection-change", { taskIds: [] });
2895
+ this.options.onSelectionChange?.([]);
2896
+ this.emitter.emit("tasks-change", { tasks: this.getTasks() });
2897
+ this.scheduleRender();
2898
+ },
2899
+ undo: () => {
2900
+ const combined = [...this.tasks];
2901
+ removedTasks.forEach((t, i) => {
2902
+ const at = Math.min(removedIndices[i] ?? combined.length, combined.length);
2903
+ combined.splice(at, 0, t);
2904
+ });
2905
+ this.tasks = combined;
2906
+ this.dependencies.push(...removedDeps);
2907
+ this.emitter.emit("tasks-change", { tasks: this.getTasks() });
2908
+ this.scheduleRender();
2909
+ }
2910
+ };
2911
+ this.runCommand(cmd);
2912
+ }
2913
+ runCommand(cmd) {
2914
+ if (this.history) {
2915
+ this.history.push(cmd);
2916
+ } else {
2917
+ cmd.do();
2918
+ }
2919
+ }
2920
+ handleColumnResize(columnId, width) {
2921
+ const col = this.columns.find((c) => c.id === columnId);
2922
+ if (!col) return;
2923
+ col.width = width;
2924
+ this.emitter.emit("column-resize", { columnId, width });
2925
+ this.options.onColumnResize?.(columnId, width);
2926
+ this.scheduleRender();
2927
+ }
2928
+ handleColumnReorder(order) {
2929
+ const byId = new Map(this.columns.map((c) => [c.id, c]));
2930
+ const reordered = order.map((id) => byId.get(id)).filter((c) => !!c);
2931
+ if (reordered.length !== this.columns.length) return;
2932
+ this.columns = reordered;
2933
+ this.emitter.emit("column-reorder", { order });
2934
+ this.options.onColumnReorder?.(order);
2935
+ this.scheduleRender();
2936
+ }
2937
+ handleRowReorder(draggedId, targetId, position) {
2938
+ const draggedIndex = this.tasks.findIndex((t) => t.id === draggedId);
2939
+ const targetIndex = this.tasks.findIndex((t) => t.id === targetId);
2940
+ if (draggedIndex < 0 || targetIndex < 0) return;
2941
+ const dragged = this.tasks[draggedIndex];
2942
+ const target = this.tasks[targetIndex];
2943
+ if (!dragged || !target) return;
2944
+ let cursor = target;
2945
+ while (cursor) {
2946
+ if (cursor.id === draggedId) return;
2947
+ cursor = cursor.parentId ? this.tasks.find((t) => t.id === cursor.parentId) : void 0;
2948
+ }
2949
+ this.tasks.splice(draggedIndex, 1);
2950
+ const newTargetIndex = this.tasks.findIndex((t) => t.id === targetId);
2951
+ if (position === "inside") {
2952
+ dragged.parentId = target.id;
2953
+ this.tasks.splice(newTargetIndex + 1, 0, dragged);
2954
+ } else {
2955
+ dragged.parentId = target.parentId ?? null;
2956
+ this.tasks.splice(position === "before" ? newTargetIndex : newTargetIndex + 1, 0, dragged);
2957
+ }
2958
+ this.emitter.emit("task-reorder", { draggedTaskId: draggedId, targetTaskId: targetId, position });
2959
+ this.options.onTaskReorder?.(draggedId, targetId, position);
2960
+ this.emitter.emit("tasks-change", { tasks: this.getTasks() });
2961
+ this.scheduleRender();
2962
+ }
2963
+ toggleGroup(taskId) {
2964
+ const task = this.tasks.find((t) => t.id === taskId);
2965
+ if (!task) return;
2966
+ task.collapsed = !task.collapsed;
2967
+ this.emitter.emit("group-toggle", { task, collapsed: !!task.collapsed });
2968
+ this.options.onGroupToggle?.(task, !!task.collapsed);
2969
+ this.scheduleRender();
2970
+ }
2971
+ expandAll() {
2972
+ for (const t of this.tasks) t.collapsed = false;
2973
+ this.scheduleRender();
2974
+ }
2975
+ collapseAll() {
2976
+ for (const t of this.tasks) if (t.isGroup) t.collapsed = true;
2977
+ this.scheduleRender();
2978
+ }
2979
+ setTasks(tasks) {
2980
+ this.tasks = tasks.map(cloneTask);
2981
+ this.emitter.emit("tasks-change", { tasks: this.getTasks() });
2982
+ this.scheduleRender();
2983
+ }
2984
+ getTasks() {
2985
+ return this.tasks.map(cloneTask);
2986
+ }
2987
+ setDependencies(deps) {
2988
+ this.dependencies = deps.map((d) => ({ ...d }));
2989
+ this.scheduleRender();
2990
+ }
2991
+ updateTask(id, partial) {
2992
+ const task = this.tasks.find((t) => t.id === id);
2993
+ if (!task) return;
2994
+ Object.assign(task, partial);
2995
+ this.emitter.emit("tasks-change", { tasks: this.getTasks() });
2996
+ this.scheduleRender();
2997
+ }
2998
+ setViewMode(mode) {
2999
+ this.options.viewMode = mode;
3000
+ this.scheduleRender();
3001
+ }
3002
+ setOptions(partial) {
3003
+ if (partial.colorScheme !== void 0) this.colorScheme = partial.colorScheme;
3004
+ if (partial.theme !== void 0) this.explicitTheme = partial.theme;
3005
+ if (partial.theme !== void 0 || partial.colorScheme !== void 0) {
3006
+ this.theme = mergeTheme(this.explicitTheme, this.colorScheme);
3007
+ }
3008
+ if (partial.columns) this.columns = partial.columns;
3009
+ if (partial.columnWidth !== void 0) this.columnWidth = partial.columnWidth;
3010
+ Object.assign(this.options, partial);
3011
+ this.scheduleRender();
3012
+ }
3013
+ /** Scroll the timeline so "today" is centered in the viewport. No-op without a DOM / today column visible. */
3014
+ scrollToToday() {
3015
+ if (!HAS_DOM || !this.renderer || !this.renderModel) return;
3016
+ const todayTick = this.renderModel.ticks.find((t) => t.isToday);
3017
+ if (!todayTick) return;
3018
+ const scrollEl = this.renderer.timelineScroll;
3019
+ const viewportWidth = scrollEl.clientWidth || 0;
3020
+ scrollEl.scrollLeft = Math.max(0, todayTick.x - viewportWidth / 2);
3021
+ }
3022
+ /** Scroll the timeline to the very start of the project's date range. Also the Home-key shortcut when keyboardAccessible. */
3023
+ scrollToRangeStart() {
3024
+ if (!HAS_DOM || !this.renderer) return;
3025
+ this.renderer.timelineScroll.scrollLeft = 0;
3026
+ }
3027
+ /** Scroll the timeline to the very end of the project's date range. Also the End-key shortcut when keyboardAccessible. */
3028
+ scrollToRangeEnd() {
3029
+ if (!HAS_DOM || !this.renderer || !this.renderModel) return;
3030
+ this.renderer.timelineScroll.scrollLeft = this.renderModel.width;
3031
+ }
3032
+ /** Select every currently visible task. Also the Ctrl/Cmd+A shortcut when both `selectable` and `keyboardAccessible` are on. */
3033
+ selectAllVisible() {
3034
+ if (!this.options.selectable || !this.renderModel) return;
3035
+ this.selectedTaskIds = new Set(this.renderModel.rows.map((r) => r.taskId));
3036
+ this.emitter.emit("selection-change", { taskIds: this.getSelectedTaskIds() });
3037
+ this.options.onSelectionChange?.(this.getSelectedTaskIds());
3038
+ this.scheduleRender();
3039
+ }
3040
+ fitToViewport(containerWidthPx) {
3041
+ let min = null;
3042
+ let max = null;
3043
+ for (const t of this.tasks) {
3044
+ if (!isValidDate(t.start) || !isValidDate(t.end)) continue;
3045
+ min = min === null ? t.start.getTime() : Math.min(min, t.start.getTime());
3046
+ max = max === null ? t.end.getTime() : Math.max(max, t.end.getTime());
3047
+ }
3048
+ if (min === null || max === null || max <= min) return;
3049
+ const rangeMs = max - min;
3050
+ const unitMs = approxUnitMs(this.options.viewMode);
3051
+ const newColumnWidth = Math.max(4, containerWidthPx * unitMs / rangeMs);
3052
+ this.columnWidth = newColumnWidth;
3053
+ this.scheduleRender();
3054
+ }
3055
+ /** Zoom in by `factor` (default 1.25x), clamped to a sane minimum column width. */
3056
+ zoomIn(factor = 1.25) {
3057
+ this.columnWidth = Math.min(_GanttChart.MAX_COLUMN_WIDTH, this.columnWidth * factor);
3058
+ this.scheduleRender();
3059
+ }
3060
+ /** Zoom out by `factor` (default 1.25x), clamped to a sane maximum column width. */
3061
+ zoomOut(factor = 1.25) {
3062
+ this.columnWidth = Math.max(_GanttChart.MIN_COLUMN_WIDTH, this.columnWidth / factor);
3063
+ this.scheduleRender();
3064
+ }
3065
+ /** Current zoom level, expressed as px per grid-unit column (same unit as the `columnWidth` option). */
3066
+ getZoomLevel() {
3067
+ return this.columnWidth;
3068
+ }
3069
+ /** Per-assignee daily workload, computed from the current tasks. See computeResourceHistogram. */
3070
+ getResourceHistogram(options = {}) {
3071
+ return computeResourceHistogram(this.tasks, { calendar: this.options.calendar, ...options });
3072
+ }
3073
+ /** Resolve resource overallocation by delaying tasks. Doesn't mutate state - call setTasks(result.tasks) to apply it. See levelResources. */
3074
+ getLeveledTasks(options = {}) {
3075
+ return levelResources(this.tasks, this.dependencies, { calendar: this.options.calendar, ...options });
3076
+ }
3077
+ getPageCount() {
3078
+ const pageSize = this.options.pagination?.pageSize;
3079
+ if (!pageSize || pageSize <= 0) return 1;
3080
+ const rootCount = this.tasks.filter((t) => !t.parentId).length;
3081
+ return Math.max(1, Math.ceil(rootCount / pageSize));
3082
+ }
3083
+ setPage(page) {
3084
+ const pageCount = this.getPageCount();
3085
+ const clamped = Math.min(Math.max(1, page), pageCount);
3086
+ this.currentPage = clamped;
3087
+ if (this.options.pagination) this.options.pagination.page = clamped;
3088
+ this.emitter.emit("page-change", { page: clamped, pageCount });
3089
+ this.scheduleRender();
3090
+ }
3091
+ getRenderModel() {
3092
+ if (!this.renderModel) this.renderModel = this.computeModel();
3093
+ return this.renderModel;
3094
+ }
3095
+ computeModel() {
3096
+ const pagination = this.options.pagination ? { pageSize: this.options.pagination.pageSize, page: this.currentPage } : void 0;
3097
+ return computeLayout({
3098
+ tasks: this.tasks,
3099
+ dependencies: this.dependencies,
3100
+ viewMode: this.options.viewMode,
3101
+ columnWidth: this.columnWidth,
3102
+ theme: this.theme,
3103
+ columns: this.columns,
3104
+ showCriticalPath: this.options.showCriticalPath,
3105
+ showBaseline: this.options.showBaseline,
3106
+ showDeadlines: this.options.showDeadlines,
3107
+ showAssigneeAvatars: this.options.showAssigneeAvatars,
3108
+ calendar: this.options.calendar,
3109
+ markers: this.options.markers,
3110
+ autoRollupProgress: this.options.autoRollupProgress,
3111
+ selectedTaskIds: this.options.selectable ? this.selectedTaskIds : void 0,
3112
+ pagination
3113
+ });
3114
+ }
3115
+ scheduleRender(immediate = false) {
3116
+ if (immediate || !HAS_DOM) {
3117
+ this.doRender();
3118
+ return;
3119
+ }
3120
+ if (this.rafHandle !== null) return;
3121
+ this.rafHandle = requestAnimationFrame(() => {
3122
+ this.rafHandle = null;
3123
+ this.doRender();
3124
+ });
3125
+ }
3126
+ doRender() {
3127
+ this.renderModel = this.computeModel();
3128
+ this.renderer?.render(this.renderModel, this.tasks, this.columns, this.explicitTheme);
3129
+ if (this.renderer && this.colorScheme !== "auto") {
3130
+ this.renderer.root.setAttribute("data-gantt-theme", this.colorScheme);
3131
+ } else {
3132
+ this.renderer?.root.removeAttribute("data-gantt-theme");
3133
+ }
3134
+ }
3135
+ undo() {
3136
+ this.history?.undo();
3137
+ }
3138
+ redo() {
3139
+ this.history?.redo();
3140
+ }
3141
+ get canUndo() {
3142
+ return this.history?.canUndo ?? false;
3143
+ }
3144
+ get canRedo() {
3145
+ return this.history?.canRedo ?? false;
3146
+ }
3147
+ on(event, listener) {
3148
+ this.emitter.on(event, listener);
3149
+ }
3150
+ off(event, listener) {
3151
+ this.emitter.off(event, listener);
3152
+ }
3153
+ toSVGString() {
3154
+ return this.renderer?.toSVGString() ?? "";
3155
+ }
3156
+ async rasterizeSVG(svgString, width, height) {
3157
+ const svgBlob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
3158
+ const url = URL.createObjectURL(svgBlob);
3159
+ try {
3160
+ return await new Promise((resolve, reject) => {
3161
+ const image = new Image();
3162
+ image.onload = () => {
3163
+ const canvas = document.createElement("canvas");
3164
+ canvas.width = image.width || width || 800;
3165
+ canvas.height = image.height || height || 600;
3166
+ const ctx = canvas.getContext("2d");
3167
+ if (!ctx) {
3168
+ reject(new Error("2D canvas context unavailable"));
3169
+ return;
3170
+ }
3171
+ ctx.drawImage(image, 0, 0);
3172
+ resolve(canvas.toDataURL("image/png"));
3173
+ };
3174
+ image.onerror = () => reject(new Error("Failed to rasterize SVG"));
3175
+ image.src = url;
3176
+ });
3177
+ } finally {
3178
+ URL.revokeObjectURL(url);
3179
+ }
3180
+ }
3181
+ async toPNGDataURL() {
3182
+ if (!HAS_DOM || !this.renderer) {
3183
+ throw new Error("toPNGDataURL requires a browser DOM environment");
3184
+ }
3185
+ const svgString = this.renderer.toSVGString();
3186
+ return this.rasterizeSVG(svgString, this.renderer.svg.clientWidth, this.renderer.svg.clientHeight);
3187
+ }
3188
+ /**
3189
+ * Like toPNGDataURL, but tiles a chart larger than `pageWidthPx`/`pageHeightPx`
3190
+ * into multiple images (row-major order) instead of one oversized raster -
3191
+ * useful for charts too big to comfortably rasterize/view as a single PNG.
3192
+ * Reuses the full rendered SVG (so every page renders at full fidelity);
3193
+ * defaults to the chart's actual size, i.e. a single image, when omitted.
3194
+ */
3195
+ async toPNGDataURLs(options = {}) {
3196
+ if (!HAS_DOM || !this.renderer || !this.renderModel) {
3197
+ throw new Error("toPNGDataURLs requires a browser DOM environment");
3198
+ }
3199
+ const svgString = this.renderer.toSVGString();
3200
+ const totalWidth = this.renderModel.width;
3201
+ const totalHeight = this.renderModel.headerHeight + this.renderModel.height;
3202
+ const pageWidth = Math.max(1, options.pageWidthPx ?? totalWidth);
3203
+ const pageHeight = Math.max(1, options.pageHeightPx ?? totalHeight);
3204
+ const cols = Math.max(1, Math.ceil(totalWidth / pageWidth));
3205
+ const rows = Math.max(1, Math.ceil(totalHeight / pageHeight));
3206
+ const results = [];
3207
+ for (let r = 0; r < rows; r++) {
3208
+ for (let c = 0; c < cols; c++) {
3209
+ const tx = c * pageWidth;
3210
+ const ty = r * pageHeight;
3211
+ const tw = Math.min(pageWidth, totalWidth - tx);
3212
+ const th = Math.min(pageHeight, totalHeight - ty);
3213
+ const tileSvg = svgString.replace(/<svg([^>]*)>/, (_match, attrs) => {
3214
+ const cleaned = attrs.replace(/\swidth="[^"]*"/, "").replace(/\sheight="[^"]*"/, "").replace(/\sviewBox="[^"]*"/, "");
3215
+ return `<svg${cleaned} width="${tw}" height="${th}" viewBox="${tx} ${ty} ${tw} ${th}">`;
3216
+ });
3217
+ results.push(await this.rasterizeSVG(tileSvg, tw, th));
3218
+ }
3219
+ }
3220
+ return results;
3221
+ }
3222
+ destroy() {
3223
+ if (this.rafHandle !== null && typeof cancelAnimationFrame !== "undefined") {
3224
+ cancelAnimationFrame(this.rafHandle);
3225
+ }
3226
+ this.darkMediaQuery?.removeEventListener?.("change", this.handleSchemeChange);
3227
+ this.emitter.removeAllListeners();
3228
+ this.interactions?.destroy();
3229
+ this.renderer?.destroy();
3230
+ }
3231
+ };
3232
+ _GanttChart.MIN_COLUMN_WIDTH = 4;
3233
+ _GanttChart.MAX_COLUMN_WIDTH = 400;
3234
+ var GanttChart = _GanttChart;
3235
+ // Annotate the CommonJS export names for ESM import in node:
3236
+ 0 && (module.exports = {
3237
+ DEFAULT_PALETTE,
3238
+ DEFAULT_THEME,
3239
+ DENSITY_PRESETS,
3240
+ EventEmitter,
3241
+ GanttChart,
3242
+ HistoryManager,
3243
+ applyColorByField,
3244
+ applyConstraint,
3245
+ colorByField,
3246
+ computeCriticalPath,
3247
+ computeLayout,
3248
+ computeResourceHistogram,
3249
+ computeWBSCodes,
3250
+ dateUtils,
3251
+ filterTasks,
3252
+ isHoliday,
3253
+ isSafeHref,
3254
+ isWithinWorkingHours,
3255
+ isWorkingDay,
3256
+ isWorkingTime,
3257
+ levelResources,
3258
+ mergeTheme,
3259
+ nextWorkingDay,
3260
+ previousWorkingDay,
3261
+ renderResourceHistogramSVG,
3262
+ shiftToWorkingDay,
3263
+ shiftToWorkingTime,
3264
+ tasksFromCSV,
3265
+ tasksToCSV,
3266
+ themeToCssVars
3267
+ });
3268
+ //# sourceMappingURL=index.cjs.map