gantt-renderer 0.1.2 → 0.3.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.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  import { z } from "zod";
2
- //#region src/gantt-chart/validation/schemas.ts
2
+ //#region src/lib/validation/schemas.ts
3
3
  const LinkTypeSchema = z.enum([
4
4
  "FS",
5
5
  "SS",
6
6
  "FF",
7
7
  "SF"
8
8
  ]);
9
- const TaskTypeSchema = z.enum([
9
+ const TaskKindSchema = z.enum([
10
10
  "task",
11
11
  "project",
12
12
  "milestone"
@@ -14,49 +14,157 @@ const TaskTypeSchema = z.enum([
14
14
  const SpecialDayKindSchema = z.enum(["holiday", "custom"]);
15
15
  const SpecialDaySchema = z.object({
16
16
  /** ISO date: YYYY-MM-DD */
17
- date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected YYYY-MM-DD"),
17
+ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/u, "Expected YYYY-MM-DD"),
18
18
  kind: SpecialDayKindSchema,
19
19
  label: z.string().min(1).optional(),
20
20
  className: z.string().min(1).optional()
21
21
  });
22
- const TaskSchema = z.object({
22
+ const taskBase = {
23
+ /** Unique positive integer identifier for the task. */
23
24
  id: z.number().int().positive(),
25
+ /** Display name / label of the task. */
24
26
  text: z.string().min(1),
25
27
  /** ISO date: YYYY-MM-DD */
26
- start_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected YYYY-MM-DD"),
27
- /** Duration in days; 0 = milestone */
28
- duration: z.number().int().min(0),
28
+ startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/u, "Expected YYYY-MM-DD"),
29
+ /** Optional id of the parent task. When set, this task is a child in the hierarchy. */
29
30
  parent: z.number().int().positive().optional(),
30
- /** 0–1 completion ratio */
31
- progress: z.number().min(0).max(1).default(0),
32
- type: TaskTypeSchema.default("task"),
33
- /** Initial expanded state (only relevant for parent nodes) */
34
- open: z.boolean().default(true),
35
- color: z.string().optional()
31
+ /** Optional CSS color value for the task bar. Overrides the default color assignment. */
32
+ color: z.string().optional(),
33
+ /** Optional arbitrary metadata for consumer use. Preserved in the parsed output. */
34
+ data: z.record(z.string(), z.unknown()).optional()
35
+ };
36
+ /** @internal */
37
+ const TaskLeafSchema = z.object({
38
+ ...taskBase,
39
+ kind: z.literal("task"),
40
+ /** Duration in hours. Must be positive; use `kind: 'milestone'` for zero-duration points. */
41
+ durationHours: z.number().int().positive(),
42
+ /** 0–100 completion percentage (integer). */
43
+ percentComplete: z.number().int().min(0).max(100).default(0)
44
+ });
45
+ /** @internal */
46
+ const TaskProjectSchema = z.object({
47
+ ...taskBase,
48
+ kind: z.literal("project"),
49
+ /** Duration in hours. Must be positive; use `kind: 'milestone'` for zero-duration points. */
50
+ durationHours: z.number().int().positive(),
51
+ /** 0–100 completion percentage (integer). */
52
+ percentComplete: z.number().int().min(0).max(100).default(0),
53
+ /**
54
+ * Initial expanded state for tree hierarchy.
55
+ * When `false`, children of this task are hidden on initial render.
56
+ *
57
+ * @default true
58
+ */
59
+ open: z.boolean().default(true)
36
60
  });
61
+ /** @internal */
62
+ const TaskMilestoneSchema = z.object({
63
+ ...taskBase,
64
+ kind: z.literal("milestone")
65
+ });
66
+ const TaskSchema = z.discriminatedUnion("kind", [
67
+ TaskLeafSchema,
68
+ TaskProjectSchema,
69
+ TaskMilestoneSchema
70
+ ]);
37
71
  const LinkSchema = z.object({
72
+ /** Unique positive integer identifier for the dependency link. */
38
73
  id: z.number().int().positive(),
74
+ /** The `id` of the predecessor task (the task that drives the dependency). */
39
75
  source: z.number().int().positive(),
76
+ /** The `id` of the successor task (the task that depends on the predecessor). */
40
77
  target: z.number().int().positive(),
41
- type: LinkTypeSchema.default("FS")
78
+ /**
79
+ * Dependency type.
80
+ *
81
+ * - `'FS'` — Finish-to-start: successor starts after predecessor finishes.
82
+ * - `'SS'` — Start-to-start: successor starts at the same time as predecessor.
83
+ * - `'FF'` — Finish-to-finish: successor finishes at the same time as predecessor.
84
+ * - `'SF'` — Start-to-finish: successor finishes after predecessor starts.
85
+ *
86
+ * @default 'FS'
87
+ */
88
+ type: LinkTypeSchema.default("FS"),
89
+ /** Optional arbitrary metadata for consumer use. Preserved in the parsed output. */
90
+ data: z.record(z.string(), z.unknown()).optional()
91
+ }).refine((l) => l.source !== l.target, {
92
+ message: "A link cannot connect a task to itself",
93
+ path: ["target"]
42
94
  });
43
95
  const GanttInputSchema = z.object({
96
+ /** Array of task objects. At least one task is required. */
44
97
  tasks: z.array(TaskSchema).min(1),
98
+ /** Optional array of dependency link objects. Defaults to empty array. */
45
99
  links: z.array(LinkSchema).default([])
100
+ }).superRefine((data, ctx) => {
101
+ const taskIds = /* @__PURE__ */ new Set();
102
+ for (let i = 0; i < data.tasks.length; i++) {
103
+ const task = data.tasks[i];
104
+ if (task !== void 0) {
105
+ if (taskIds.has(task.id)) ctx.addIssue({
106
+ code: "custom",
107
+ message: `Duplicate task id: ${task.id}`,
108
+ path: [
109
+ "tasks",
110
+ i,
111
+ "id"
112
+ ]
113
+ });
114
+ taskIds.add(task.id);
115
+ }
116
+ }
117
+ const linkIds = /* @__PURE__ */ new Set();
118
+ for (let i = 0; i < data.links.length; i++) {
119
+ const link = data.links[i];
120
+ if (link !== void 0) {
121
+ if (linkIds.has(link.id)) ctx.addIssue({
122
+ code: "custom",
123
+ message: `Duplicate link id: ${link.id}`,
124
+ path: [
125
+ "links",
126
+ i,
127
+ "id"
128
+ ]
129
+ });
130
+ linkIds.add(link.id);
131
+ }
132
+ }
133
+ const pairKeys = /* @__PURE__ */ new Set();
134
+ for (let i = 0; i < data.links.length; i++) {
135
+ const link = data.links[i];
136
+ if (link !== void 0) {
137
+ const key = `${link.source}:${link.target}`;
138
+ if (pairKeys.has(key)) ctx.addIssue({
139
+ code: "custom",
140
+ message: `Duplicate link pair: source=${link.source} target=${link.target}`,
141
+ path: ["links", i]
142
+ });
143
+ pairKeys.add(key);
144
+ }
145
+ }
46
146
  });
47
- /** Parses raw external data. Throws ZodError on failure. */
147
+ /**
148
+ * Parses raw external data.
149
+ *
150
+ * @param raw - The unvalidated input from the consumer.
151
+ * @returns The parsed and validated {@link GanttInput}.
152
+ * @throws {import('zod').ZodError} On schema validation failure.
153
+ */
48
154
  function parseGanttInput(raw) {
49
155
  return GanttInputSchema.parse(raw);
50
156
  }
51
- /** Returns null instead of throwing. */
52
- function safeParseGanttInput(raw) {
53
- const result = GanttInputSchema.safeParse(raw);
54
- return result.success ? result.data : null;
55
- }
56
157
  //#endregion
57
- //#region src/gantt-chart/errors.ts
158
+ //#region src/lib/errors.ts
159
+ /**
160
+ * Domain-specific error with a machine-readable {@link GanttErrorCode}.
161
+ */
58
162
  var GanttError = class extends Error {
59
163
  code;
164
+ /**
165
+ * @param code - A machine-readable {@link GanttErrorCode} categorising the error.
166
+ * @param message - A human-readable description.
167
+ */
60
168
  constructor(code, message) {
61
169
  super(message);
62
170
  this.name = "GanttError";
@@ -64,20 +172,67 @@ var GanttError = class extends Error {
64
172
  }
65
173
  };
66
174
  //#endregion
67
- //#region src/gantt-chart/domain/tree.ts
175
+ //#region src/lib/domain/tree.ts
176
+ function detectParentCycles(tasks) {
177
+ const adj = /* @__PURE__ */ new Map();
178
+ for (const task of tasks) {
179
+ adj.set(task.id, []);
180
+ if (task.parent !== void 0) {
181
+ const parents = adj.get(task.parent);
182
+ if (parents !== void 0) parents.push(task.id);
183
+ }
184
+ }
185
+ const WHITE = 0, GRAY = 1, BLACK = 2;
186
+ const color = /* @__PURE__ */ new Map();
187
+ const parent = /* @__PURE__ */ new Map();
188
+ for (const id of adj.keys()) color.set(id, WHITE);
189
+ const dfs = (u) => {
190
+ color.set(u, GRAY);
191
+ for (const v of adj.get(u) ?? []) {
192
+ const vc = color.get(v) ?? WHITE;
193
+ if (vc === GRAY) {
194
+ const path = [v, u];
195
+ let cur = u;
196
+ while (cur !== v) {
197
+ const p = parent.get(cur);
198
+ if (p === void 0) break;
199
+ path.push(p);
200
+ cur = p;
201
+ }
202
+ throw new GanttError("PARENT_CYCLE", `Parent cycle detected: ${[...path].reverse().join(" -> ")}`);
203
+ }
204
+ if (vc === WHITE) {
205
+ parent.set(v, u);
206
+ dfs(v);
207
+ }
208
+ }
209
+ color.set(u, BLACK);
210
+ };
211
+ for (const id of adj.keys()) if ((color.get(id) ?? WHITE) === WHITE) dfs(id);
212
+ }
68
213
  /**
69
214
  * Builds a typed tree from a flat task array.
70
215
  * Order of tasks[] is irrelevant — parents need not precede children.
71
- * Throws if any parent reference is unresolvable.
216
+ *
217
+ * @param tasks - The flat array of tasks to convert into a tree.
218
+ * @returns Root-level {@link TaskNode} instances with populated `children`.
219
+ * @throws {GanttError} When a task references a `parent` id that does not exist,
220
+ * when a parent cycle is detected, or when a `parent` points to a
221
+ * milestone or leaf task.
72
222
  */
73
223
  function buildTaskTree(tasks) {
74
224
  const map = /* @__PURE__ */ new Map();
75
225
  const roots = [];
226
+ for (const task of tasks) if (task.parent !== void 0) {
227
+ const parentTask = tasks.find((t) => t.id === task.parent);
228
+ if (parentTask !== void 0 && (parentTask.kind === "milestone" || parentTask.kind === "task")) throw new GanttError("PARENT_REFERENCE", `Task id=${task.id} cannot have parent id=${task.parent} of kind '${parentTask.kind}'`);
229
+ }
76
230
  for (const task of tasks) map.set(task.id, {
77
231
  ...task,
78
232
  children: [],
79
233
  depth: 0
80
234
  });
235
+ detectParentCycles(tasks);
81
236
  for (const task of tasks) {
82
237
  const node = map.get(task.id);
83
238
  if (node === void 0) continue;
@@ -97,7 +252,11 @@ function buildTaskTree(tasks) {
97
252
  }
98
253
  /**
99
254
  * Flattens a tree into a visible row list.
100
- * A node's children are included only when its id is in expandedIds.
255
+ * A node's children are included only when its id is in `expandedIds`.
256
+ *
257
+ * @param roots - The root-level {@link TaskNode} instances of the tree.
258
+ * @param expandedIds - Set of task IDs whose children should be rendered.
259
+ * @returns A depth-first flattened array of visible {@link TaskNode} items.
101
260
  */
102
261
  function flattenTree(roots, expandedIds) {
103
262
  const rows = [];
@@ -108,16 +267,23 @@ function flattenTree(roots, expandedIds) {
108
267
  for (const root of roots) walk(root);
109
268
  return rows;
110
269
  }
111
- /** Returns true when a node has children in the tree. */
270
+ /**
271
+ * Returns `true` when a node has children in the tree.
272
+ *
273
+ * @param node - The {@link TaskNode} to inspect.
274
+ * @returns `true` if `node.children.length > 0`.
275
+ */
112
276
  function isParent(node) {
113
277
  return node.children.length > 0;
114
278
  }
115
279
  //#endregion
116
- //#region src/gantt-chart/domain/dependencies.ts
280
+ //#region src/lib/domain/dependencies.ts
117
281
  /**
118
282
  * Detects circular dependencies in the link graph using DFS tri-colour marking.
119
- * Throws with a human-readable cycle path on detection.
120
- * Silent return = no cycles.
283
+ *
284
+ * @param tasks - The task list (used to build the vertex set).
285
+ * @param links - The dependency links defining the directed edges.
286
+ * @throws {GanttError} When a cycle is detected, with a human-readable cycle path.
121
287
  */
122
288
  function detectCycles(tasks, links) {
123
289
  const adj = /* @__PURE__ */ new Map();
@@ -155,88 +321,34 @@ function detectCycles(tasks, links) {
155
321
  for (const id of adj.keys()) if ((color.get(id) ?? WHITE) === WHITE) dfs(id);
156
322
  }
157
323
  /**
158
- * Returns true when all link source/target ids reference existing tasks.
159
- * Throws with details on failure.
324
+ * Validates that every link references existing task IDs and that no
325
+ * duplicate (source, target) pairs exist.
326
+ *
327
+ * @param tasks - The task list (used as the reference set of valid IDs).
328
+ * @param links - The dependency links to validate.
329
+ * @throws {GanttError} When any link references a non-existent source or target task,
330
+ * when a non-FS link connects to/from a milestone, or when duplicate
331
+ * (source, target) pairs exist.
160
332
  */
161
333
  function validateLinkRefs(tasks, links) {
162
334
  const ids = new Set(tasks.map((t) => t.id));
335
+ const taskById = new Map(tasks.map((t) => [t.id, t]));
336
+ const pairKeys = /* @__PURE__ */ new Set();
163
337
  for (const link of links) {
164
338
  if (!ids.has(link.source)) throw new GanttError("LINK_REFERENCE", `Link id=${link.id}: source=${link.source} not found`);
165
339
  if (!ids.has(link.target)) throw new GanttError("LINK_REFERENCE", `Link id=${link.id}: target=${link.target} not found`);
166
- }
167
- }
168
- //#endregion
169
- //#region src/gantt-chart/locale.ts
170
- const EN_US_LABELS = {
171
- aria_task: "Task {0}",
172
- aria_milestone: "Milestone {0}",
173
- add_subtask_title: "Add subtask",
174
- column_task_name: "Task name",
175
- column_start_time: "Start time",
176
- column_duration: "Duration",
177
- column_quarter: "Q"
178
- };
179
- const CHART_LOCALE_EN_US = {
180
- code: "en-US",
181
- labels: EN_US_LABELS,
182
- weekStartsOn: 0,
183
- weekNumbering: "iso",
184
- weekendDays: [0, 6]
185
- };
186
- /**
187
- * Resolves a ChartLocale from either a full ChartLocale object or a BCP 47 string.
188
- * When given a string, derives weekStartsOn, weekNumbering, and weekendDays from CLDR conventions.
189
- */
190
- function resolveChartLocale(raw) {
191
- if (raw === void 0) return CHART_LOCALE_EN_US;
192
- if (typeof raw !== "string") {
193
- const locale = {
194
- code: raw.code,
195
- weekStartsOn: raw.weekStartsOn ?? deriveWeekStartsOn(raw.code),
196
- weekNumbering: raw.weekNumbering ?? deriveWeekNumbering(raw.code),
197
- weekendDays: raw.weekendDays ?? deriveWeekendDays(raw.code)
198
- };
199
- if (raw.labels !== void 0) locale.labels = raw.labels;
200
- return locale;
201
- }
202
- const code = raw;
203
- return {
204
- code,
205
- weekStartsOn: deriveWeekStartsOn(code),
206
- weekNumbering: deriveWeekNumbering(code),
207
- weekendDays: deriveWeekendDays(code)
208
- };
209
- }
210
- function tryGetWeekInfo(code) {
211
- try {
212
- if (typeof Intl !== "undefined" && typeof Intl.Locale === "function") {
213
- const locale = new Intl.Locale(code);
214
- const fn = locale.getWeekInfo;
215
- if (typeof fn === "function") return fn.call(locale);
340
+ const pairKey = `${link.source}:${link.target}`;
341
+ if (pairKeys.has(pairKey)) throw new GanttError("DUPLICATE_LINK_PAIR", `Link id=${link.id}: duplicate pair source=${link.source} target=${link.target}`);
342
+ pairKeys.add(pairKey);
343
+ if (link.type !== "FS") {
344
+ const sourceTask = taskById.get(link.source);
345
+ const targetTask = taskById.get(link.target);
346
+ if (sourceTask?.kind === "milestone" || targetTask?.kind === "milestone") throw new GanttError("MILESTONE_LINK_TYPE", `Link id=${link.id}: non-FS type '${link.type}' not allowed when connected to a milestone`);
216
347
  }
217
- } catch {}
218
- }
219
- /**
220
- * Derives the first day of week (0=Sun, 1=Mon, 6=Sat) from a BCP 47 code.
221
- * Uses Intl.Locale.getWeekInfo() where available (Chromium, Safari 15.4+),
222
- * with a CLDR-based fallback table for Firefox and older runtimes.
223
- */
224
- function deriveWeekStartsOn(code) {
225
- const primary = code.split("-")[0]?.toLowerCase() ?? "en";
226
- const region = code.split("-")[1]?.toUpperCase();
227
- if (region !== void 0) {
228
- const fromRegion = WEEK_START_REGION[region];
229
- if (fromRegion !== void 0) return fromRegion;
230
- }
231
- const fromLang = WEEK_START_LANG[primary];
232
- if (fromLang !== void 0) return fromLang;
233
- const info = tryGetWeekInfo(code);
234
- if (info !== void 0) {
235
- const day = info.firstDay;
236
- return day === 7 ? 0 : day;
237
348
  }
238
- return 1;
239
349
  }
350
+ //#endregion
351
+ //#region src/lib/locale.ts
240
352
  const WEEK_START_REGION = {
241
353
  US: 0,
242
354
  CA: 0,
@@ -289,24 +401,6 @@ const WEEK_START_LANG = {
289
401
  ar: 6,
290
402
  fa: 6
291
403
  };
292
- /**
293
- * Derives the week numbering scheme from a BCP 47 code.
294
- * Europe and ISO-aligned regions default to 'iso'; Americas and others to 'us'.
295
- */
296
- function deriveWeekNumbering(code) {
297
- const region = code.split("-")[1]?.toUpperCase();
298
- if (region !== void 0) {
299
- const fromRegion = WEEK_NUMBERING_REGION[region];
300
- if (fromRegion !== void 0) return fromRegion;
301
- if (region in WEEK_START_REGION) return "us";
302
- }
303
- const info = tryGetWeekInfo(code);
304
- if (info !== void 0) {
305
- if (info.minimalDays >= 4 && info.firstDay === 1) return "iso";
306
- return "us";
307
- }
308
- return "iso";
309
- }
310
404
  const WEEK_NUMBERING_REGION = {
311
405
  US: "us",
312
406
  CA: "us",
@@ -330,28 +424,6 @@ const WEEK_NUMBERING_REGION = {
330
424
  AU: "us",
331
425
  NZ: "us"
332
426
  };
333
- /**
334
- * Derives weekend days (0=Sun … 6=Sat) from a BCP 47 code.
335
- * Uses Intl.Locale.getWeekInfo() where available, with a CLDR-based fallback table.
336
- */
337
- function deriveWeekendDays(code) {
338
- const region = code.split("-")[1]?.toUpperCase();
339
- if (region !== void 0) {
340
- const fromRegion = WEEKEND_REGION[region];
341
- if (fromRegion !== void 0) {
342
- const days = [...fromRegion];
343
- days.sort((a, b) => a - b);
344
- return days;
345
- }
346
- }
347
- const info = tryGetWeekInfo(code);
348
- if (info !== void 0) {
349
- const days = info.weekend.map((d) => d === 7 ? 0 : d);
350
- days.sort((a, b) => a - b);
351
- return days;
352
- }
353
- return [0, 6];
354
- }
355
427
  const WEEKEND_REGION = {
356
428
  AE: [5, 6],
357
429
  AF: [4, 5],
@@ -385,19 +457,127 @@ const WEEKEND_REGION = {
385
457
  SO: [5],
386
458
  MY: [5, 0]
387
459
  };
460
+ const EN_US_LABELS = {
461
+ ariaTask: "Task {0}",
462
+ ariaMilestone: "Milestone {0}",
463
+ addSubtaskTitle: "Add subtask",
464
+ columnTaskName: "Task name",
465
+ columnStartDate: "Start time",
466
+ columnDuration: "Duration",
467
+ columnQuarter: "Q"
468
+ };
469
+ const CHART_LOCALE_EN_US = {
470
+ code: "en-US",
471
+ labels: EN_US_LABELS,
472
+ weekStartsOn: 0,
473
+ weekNumbering: "iso",
474
+ weekendDays: [0, 6]
475
+ };
476
+ function tryGetWeekInfo(code) {
477
+ try {
478
+ if (typeof Intl !== "undefined" && typeof Intl.Locale === "function") {
479
+ const locale = new Intl.Locale(code);
480
+ const fn = locale.getWeekInfo;
481
+ if (typeof fn === "function") return fn.call(locale);
482
+ }
483
+ } catch {}
484
+ }
388
485
  /**
389
- * Formats a week number according to the specified scheme.
486
+ * Derives the first day of week (0=Sun, 1=Mon, 6=Sat) from a BCP 47 code.
487
+ * Uses `Intl.Locale.getWeekInfo()` where available (Chromium, Safari 15.4+),
488
+ * with a CLDR-based fallback table for Firefox and older runtimes.
390
489
  *
391
- * - `'iso'`: ISO 8601 (week 1 contains the first Thursday; Monday start).
392
- * - `'us'`: Week 1 contains January 1; Sunday start.
393
- * - `'simple'`: `Math.ceil(dayOfYear / 7)`.
490
+ * @param code - A BCP 47 language tag (e.g. `'en-US'`, `'de-DE'`).
491
+ * @returns The first day of the week: `0` (Sunday), `1` (Monday), or `6` (Saturday).
394
492
  */
395
- function formatWeekNumber(date, scheme) {
396
- switch (scheme) {
397
- case "iso": return isoWeek(date);
398
- case "us": return usWeek(date);
399
- case "simple": return simpleWeek(date);
493
+ function deriveWeekStartsOn(code) {
494
+ const primary = code.split("-")[0]?.toLowerCase() ?? "en";
495
+ const region = code.split("-")[1]?.toUpperCase();
496
+ if (region !== void 0) {
497
+ const fromRegion = WEEK_START_REGION[region];
498
+ if (fromRegion !== void 0) return fromRegion;
499
+ }
500
+ const fromLang = WEEK_START_LANG[primary];
501
+ if (fromLang !== void 0) return fromLang;
502
+ const info = tryGetWeekInfo(code);
503
+ if (info !== void 0) {
504
+ const day = info.firstDay;
505
+ return day === 7 ? 0 : day;
506
+ }
507
+ return 1;
508
+ }
509
+ /**
510
+ * Derives the week numbering scheme from a BCP 47 code.
511
+ * Europe and ISO-aligned regions default to `'iso'`; Americas and others to `'us'`.
512
+ *
513
+ * @param code - A BCP 47 language tag (e.g. `'en-US'`, `'de-DE'`).
514
+ * @returns The week numbering scheme: `'iso'`, `'us'`, or `'simple'`.
515
+ */
516
+ function deriveWeekNumbering(code) {
517
+ const region = code.split("-")[1]?.toUpperCase();
518
+ if (region !== void 0) {
519
+ const fromRegion = WEEK_NUMBERING_REGION[region];
520
+ if (fromRegion !== void 0) return fromRegion;
521
+ if (region in WEEK_START_REGION) return "us";
522
+ }
523
+ const info = tryGetWeekInfo(code);
524
+ if (info !== void 0) {
525
+ if (info.minimalDays >= 4 && info.firstDay === 1) return "iso";
526
+ return "us";
527
+ }
528
+ return "iso";
529
+ }
530
+ /**
531
+ * Derives weekend days (0=Sun … 6=Sat) from a BCP 47 code.
532
+ * Uses `Intl.Locale.getWeekInfo()` where available, with a CLDR-based fallback table.
533
+ *
534
+ * @param code - A BCP 47 language tag (e.g. `'en-US'`, `'de-DE'`).
535
+ * @returns An array of weekend day indices (sorted ascending).
536
+ */
537
+ function deriveWeekendDays(code) {
538
+ const region = code.split("-")[1]?.toUpperCase();
539
+ if (region !== void 0) {
540
+ const fromRegion = WEEKEND_REGION[region];
541
+ if (fromRegion !== void 0) {
542
+ const days = [...fromRegion];
543
+ days.sort((a, b) => a - b);
544
+ return days;
545
+ }
546
+ }
547
+ const info = tryGetWeekInfo(code);
548
+ if (info !== void 0) {
549
+ const days = info.weekend.map((d) => d === 7 ? 0 : d);
550
+ days.sort((a, b) => a - b);
551
+ return days;
552
+ }
553
+ return [0, 6];
554
+ }
555
+ /**
556
+ * Resolves a {@link ChartLocale} from either a full `ChartLocale` object or a BCP 47 string.
557
+ * When given a string, derives `weekStartsOn`, `weekNumbering`, and `weekendDays` from CLDR conventions.
558
+ *
559
+ * @param raw - A {@link ChartLocale} object, a BCP 47 language tag string, or `undefined`.
560
+ * @returns A fully resolved {@link ChartLocale} with defaults applied.
561
+ */
562
+ function resolveChartLocale(raw) {
563
+ if (raw === void 0) return CHART_LOCALE_EN_US;
564
+ if (typeof raw !== "string") {
565
+ const locale = {
566
+ code: raw.code,
567
+ weekStartsOn: raw.weekStartsOn ?? deriveWeekStartsOn(raw.code),
568
+ weekNumbering: raw.weekNumbering ?? deriveWeekNumbering(raw.code),
569
+ weekendDays: raw.weekendDays ?? deriveWeekendDays(raw.code)
570
+ };
571
+ if (raw.labels !== void 0) locale.labels = raw.labels;
572
+ return locale;
400
573
  }
574
+ const code = raw;
575
+ return {
576
+ code,
577
+ weekStartsOn: deriveWeekStartsOn(code),
578
+ weekNumbering: deriveWeekNumbering(code),
579
+ weekendDays: deriveWeekendDays(code)
580
+ };
401
581
  }
402
582
  function isoWeek(date) {
403
583
  const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
@@ -422,33 +602,107 @@ function simpleWeek(date) {
422
602
  return Math.ceil((dayOfYear + 1) / 7);
423
603
  }
424
604
  /**
605
+ * Formats a week number according to the specified scheme.
606
+ *
607
+ * - `'iso'`: ISO 8601 (week 1 contains the first Thursday; Monday start).
608
+ * - `'us'`: Week 1 contains January 1; Sunday start.
609
+ * - `'simple'`: `Math.ceil(dayOfYear / 7)`.
610
+ *
611
+ * @param date - The date to compute the week number for.
612
+ * @param scheme - The week numbering scheme: `'iso'`, `'us'`, or `'simple'`.
613
+ * @returns The week number as a positive integer.
614
+ */
615
+ function formatWeekNumber(date, scheme) {
616
+ switch (scheme) {
617
+ case "iso": return isoWeek(date);
618
+ case "us": return usWeek(date);
619
+ case "simple": return simpleWeek(date);
620
+ }
621
+ }
622
+ /**
425
623
  * Formats a label template by replacing `{0}` with the given argument.
624
+ *
625
+ * @param template - The template string containing `{0}` as placeholder.
626
+ * @param arg - The value to substitute for `{0}`.
627
+ * @returns The formatted string with the placeholder replaced.
426
628
  */
427
629
  function formatLabel(template, arg) {
428
630
  return template.replaceAll("{0}", arg);
429
631
  }
430
632
  //#endregion
431
- //#region src/gantt-chart/domain/dateMath.ts
432
- /** Parses YYYY-MM-DD → UTC midnight Date. Throws on invalid input. */
633
+ //#region src/lib/domain/dateMath.ts
634
+ /**
635
+ * Parses `YYYY-MM-DD` → UTC midnight `Date`.
636
+ *
637
+ * @param dateStr - An ISO-8601 date string in `YYYY-MM-DD` format.
638
+ * @returns A `Date` representing UTC midnight of the given date.
639
+ * @throws {Error} When `dateStr` does not represent a valid date.
640
+ */
433
641
  function parseDate(dateStr) {
434
642
  const d = /* @__PURE__ */ new Date(`${dateStr}T00:00:00.000Z`);
435
643
  if (isNaN(d.getTime())) throw new Error(`Invalid date: "${dateStr}"`);
436
644
  return d;
437
645
  }
438
- /** Returns date + n days (exact ms arithmetic). */
646
+ /**
647
+ * Returns `date + n` days using exact millisecond arithmetic.
648
+ *
649
+ * @param date - The base date.
650
+ * @param days - Number of days to add (may be negative).
651
+ * @returns A new `Date` offset by the given number of days.
652
+ */
439
653
  function addDays(date, days) {
440
654
  return new Date(date.getTime() + days * 864e5);
441
655
  }
442
- /** Difference in days (float). Positive when b > a. */
656
+ /**
657
+ * Returns `date + n` hours using exact millisecond arithmetic.
658
+ *
659
+ * @param date - The base date.
660
+ * @param hours - Number of hours to add (may be negative).
661
+ * @returns A new `Date` offset by the given number of hours.
662
+ */
663
+ function addHours(date, hours) {
664
+ return new Date(date.getTime() + hours * 36e5);
665
+ }
666
+ /**
667
+ * Difference in days (float). Positive when `b > a`.
668
+ *
669
+ * @param a - The earlier date.
670
+ * @param b - The later date.
671
+ * @returns The fractional number of days between the two dates.
672
+ */
443
673
  function diffDays(a, b) {
444
674
  return (b.getTime() - a.getTime()) / 864e5;
445
675
  }
446
- /** UTC start-of-day. */
676
+ /**
677
+ * Difference in hours (float). Positive when `b > a`.
678
+ *
679
+ * @param a - The earlier date.
680
+ * @param b - The later date.
681
+ * @returns The fractional number of hours between the two dates.
682
+ */
683
+ function diffHours(a, b) {
684
+ return (b.getTime() - a.getTime()) / 36e5;
685
+ }
686
+ /**
687
+ * Returns the UTC start-of-day for the given date.
688
+ *
689
+ * @param date - Any `Date`.
690
+ * @returns A new `Date` set to UTC midnight of the same calendar date.
691
+ */
447
692
  function startOfDay(date) {
448
693
  return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
449
694
  }
695
+ function resolveQuarterLabel(locale) {
696
+ if (locale.labels?.columnQuarter !== void 0) return locale.labels.columnQuarter;
697
+ return EN_US_LABELS.columnQuarter;
698
+ }
450
699
  /**
451
- * Formats a Date for the time-header label given the active scale.
700
+ * Formats a `Date` for the time-header label given the active scale.
701
+ *
702
+ * @param date - The date to format.
703
+ * @param scale - The active {@link TimeScale} determining the label granularity.
704
+ * @param locale - The {@link ChartLocale} used for formatting.
705
+ * @returns A human-readable header label string.
452
706
  */
453
707
  function formatHeaderLabel(date, scale, locale) {
454
708
  const { code, weekNumbering: weekNumScheme = "iso" } = locale;
@@ -473,7 +727,12 @@ function formatHeaderLabel(date, scale, locale) {
473
727
  }
474
728
  /**
475
729
  * Returns the upper-level (month/year) label for a given scale column.
476
- * Used in the top header row.
730
+ * Used in the top header row of the timeline.
731
+ *
732
+ * @param date - The date to format.
733
+ * @param scale - The active {@link TimeScale}. Determines how the upper label is computed.
734
+ * @param locale - The {@link ChartLocale} used for formatting.
735
+ * @returns A human-readable upper-level header label string.
477
736
  */
478
737
  function formatUpperLabel(date, scale, locale) {
479
738
  const { code } = locale;
@@ -495,11 +754,13 @@ function formatUpperLabel(date, scale, locale) {
495
754
  case "year": return `${date.getUTCFullYear()}`;
496
755
  }
497
756
  }
498
- function resolveQuarterLabel(locale) {
499
- if (locale.labels?.column_quarter !== void 0) return locale.labels.column_quarter;
500
- return EN_US_LABELS.column_quarter;
501
- }
502
- /** Formats YYYY-MM-DD for display in the grid. */
757
+ /**
758
+ * Formats a `YYYY-MM-DD` string for display in the grid.
759
+ *
760
+ * @param dateStr - An ISO-8601 date string in `YYYY-MM-DD` format.
761
+ * @param locale - The {@link ChartLocale} used for locale-aware formatting.
762
+ * @returns A locale-formatted date string.
763
+ */
503
764
  function formatDisplayDate(dateStr, locale) {
504
765
  return parseDate(dateStr).toLocaleDateString(locale.code, {
505
766
  year: "numeric",
@@ -509,7 +770,7 @@ function formatDisplayDate(dateStr, locale) {
509
770
  });
510
771
  }
511
772
  //#endregion
512
- //#region src/gantt-chart/timeline/scale.ts
773
+ //#region src/lib/timeline/scale.ts
513
774
  const H = 36e5;
514
775
  const D = 864e5;
515
776
  const SCALE_CONFIGS = {
@@ -547,8 +808,12 @@ const SCALE_CONFIGS = {
547
808
  /**
548
809
  * Snaps a date to the column boundary for the provided scale.
549
810
  * All operations use UTC semantics.
550
- * The week boundary respects the optional weekStartsOn override (0=Sun, 1=Mon, 6=Sat).
551
- * Defaults to Monday (1) when omitted.
811
+ * The week boundary respects the optional `weekStartsOn` override (0=Sun, 1=Mon, 6=Sat).
812
+ *
813
+ * @param date - The date to snap.
814
+ * @param scale - The target {@link TimeScale}.
815
+ * @param weekStartsOn - First day of the week (`0`-Sun, `1`-Mon, `6`-Sat). Defaults to `1` (Monday).
816
+ * @returns A new `Date` snapped to the column boundary.
552
817
  */
553
818
  function snapToScaleBoundary(date, scale, weekStartsOn = 1) {
554
819
  switch (scale) {
@@ -572,6 +837,10 @@ function snapToScaleBoundary(date, scale, weekStartsOn = 1) {
572
837
  /**
573
838
  * Returns the next column boundary from a boundary-aligned date.
574
839
  * Month/quarter/year use true calendar stepping (not fixed-day approximations).
840
+ *
841
+ * @param date - A boundary-aligned date.
842
+ * @param scale - The target {@link TimeScale}.
843
+ * @returns The next boundary date.
575
844
  */
576
845
  function nextScaleBoundary(date, scale) {
577
846
  switch (scale) {
@@ -584,17 +853,21 @@ function nextScaleBoundary(date, scale) {
584
853
  }
585
854
  }
586
855
  //#endregion
587
- //#region src/gantt-chart/timeline/pixelMapper.ts
856
+ //#region src/lib/timeline/pixelMapper.ts
588
857
  /**
589
858
  * Creates a stateless pixel mapper for the given scale and viewport start.
590
859
  * All conversions are O(1) arithmetic — safe to call in tight loops.
860
+ *
861
+ * @param scale - The active {@link TimeScale}.
862
+ * @param viewportStart - The leftmost date visible in the viewport.
863
+ * @returns A {@link PixelMapper} configured for the given viewport.
591
864
  */
592
865
  function createPixelMapper(scale, viewportStart) {
593
866
  const { columnWidth, msPerColumn } = SCALE_CONFIGS[scale];
594
867
  const originMs = viewportStart.getTime();
595
868
  const pxPerMs = columnWidth / msPerColumn;
596
869
  const msPerPx = msPerColumn / columnWidth;
597
- const msPerDay = 864e5;
870
+ const msPerHour = 36e5;
598
871
  return {
599
872
  originMs,
600
873
  columnWidth,
@@ -604,16 +877,16 @@ function createPixelMapper(scale, viewportStart) {
604
877
  toDate(x) {
605
878
  return new Date(originMs + x * msPerPx);
606
879
  },
607
- durationToWidth(days) {
608
- return days * msPerDay * pxPerMs;
880
+ durationToWidth(hours) {
881
+ return hours * msPerHour * pxPerMs;
609
882
  },
610
883
  widthToDuration(px) {
611
- return px * msPerPx / msPerDay;
884
+ return px * msPerPx / msPerHour;
612
885
  }
613
886
  };
614
887
  }
615
888
  //#endregion
616
- //#region src/gantt-chart/timeline/layoutEngine.ts
889
+ //#region src/lib/timeline/layoutEngine.ts
617
890
  const DENSITY = {
618
891
  rowHeight: 44,
619
892
  barHeight: 28,
@@ -628,18 +901,21 @@ const MILESTONE_HALF = MILESTONE_SIZE / 2;
628
901
  /**
629
902
  * Computes pixel-space layout for all visible task rows.
630
903
  * Returns a map keyed by task id for O(1) lookup during link routing.
904
+ *
905
+ * @param rows - The flattened, visible {@link TaskNode} rows.
906
+ * @param mapper - The {@link PixelMapper} for coordinate conversion.
907
+ * @returns A `Map` from task ID to its computed {@link BarLayout}.
631
908
  */
632
909
  function computeLayout(rows, mapper) {
633
910
  const result = /* @__PURE__ */ new Map();
634
911
  for (let i = 0; i < rows.length; i++) {
635
912
  const task = rows[i];
636
913
  if (task === void 0) continue;
637
- const start = parseDate(task.start_date);
914
+ const start = parseDate(task.startDate);
638
915
  const x = mapper.toX(start);
639
916
  const y = i * ROW_HEIGHT + BAR_Y_OFFSET;
640
917
  const centerY = i * ROW_HEIGHT + ROW_HEIGHT / 2;
641
- const type = task.type ?? "task";
642
- if (type === "milestone") {
918
+ if (task.kind === "milestone") {
643
919
  result.set(task.id, {
644
920
  taskId: task.id,
645
921
  x,
@@ -647,15 +923,15 @@ function computeLayout(rows, mapper) {
647
923
  width: 0,
648
924
  height: BAR_HEIGHT,
649
925
  progressWidth: 0,
650
- type: "milestone",
926
+ kind: "milestone",
651
927
  rowIndex: i,
652
928
  centerX: x,
653
929
  centerY
654
930
  });
655
931
  continue;
656
932
  }
657
- const width = Math.max(mapper.durationToWidth(task.duration), 4);
658
- const progressWidth = width * Math.min(1, Math.max(0, task.progress ?? 0));
933
+ const width = Math.max(mapper.durationToWidth(task.durationHours), 4);
934
+ const progressWidth = width * Math.min(1, Math.max(0, (task.percentComplete ?? 0) / 100));
659
935
  result.set(task.id, {
660
936
  taskId: task.id,
661
937
  x,
@@ -663,7 +939,7 @@ function computeLayout(rows, mapper) {
663
939
  width,
664
940
  height: BAR_HEIGHT,
665
941
  progressWidth,
666
- type,
942
+ kind: task.kind,
667
943
  rowIndex: i,
668
944
  centerX: x + width / 2,
669
945
  centerY
@@ -673,36 +949,334 @@ function computeLayout(rows, mapper) {
673
949
  }
674
950
  /**
675
951
  * Computes the total pixel height of all rows.
952
+ *
953
+ * @param rowCount - The number of visible rows.
954
+ * @returns The total pixel height (`rowCount * ROW_HEIGHT`).
676
955
  */
677
956
  function totalContentHeight(rowCount) {
678
957
  return rowCount * ROW_HEIGHT;
679
958
  }
680
959
  /**
681
960
  * Derives viewport bounds from task data with padding.
682
- * Returns [start, end] as UTC midnight dates.
961
+ *
962
+ * @param tasks - The task nodes to derive bounds from.
963
+ * @param paddingHours - Extra hours added before the earliest start and after the latest end. Defaults to `48`.
964
+ * @returns A tuple `[start, end]` of UTC midnight `Date` instances.
683
965
  */
684
- function deriveViewport(tasks, paddingDays = 2) {
966
+ function deriveViewport(tasks, paddingHours = 48) {
685
967
  if (tasks.length === 0) {
686
968
  const now = /* @__PURE__ */ new Date();
687
- return [now, addDays(now, 30)];
969
+ return [now, addHours(now, 720)];
688
970
  }
689
971
  let minMs = Infinity;
690
972
  let maxMs = -Infinity;
691
973
  for (const task of tasks) {
692
- const start = parseDate(task.start_date);
693
- const end = addDays(start, task.duration);
974
+ const start = parseDate(task.startDate);
694
975
  if (start.getTime() < minMs) minMs = start.getTime();
695
- if (end.getTime() > maxMs) maxMs = end.getTime();
976
+ if (task.kind !== "milestone") {
977
+ const end = addHours(start, task.durationHours);
978
+ if (end.getTime() > maxMs) maxMs = end.getTime();
979
+ } else if (start.getTime() > maxMs) maxMs = start.getTime();
980
+ }
981
+ return [addHours(new Date(minMs), -paddingHours), addHours(new Date(maxMs), paddingHours)];
982
+ }
983
+ //#endregion
984
+ //#region src/lib/rendering/linkRouter.ts
985
+ /** px gap before/after bar for routing clearance and arrow approach */
986
+ const TURN_MARGIN = 24;
987
+ /** px vertical offset below the bar row for same-row loop detours */
988
+ const SAME_ROW_DETOUR = 24;
989
+ /** Segments shorter than this (px) are collapsed before stroking */
990
+ const STUB_THRESHOLD = 2;
991
+ /**
992
+ * Removes consecutive points whose Euclidean distance is below {@link STUB_THRESHOLD}.
993
+ * The first point is always kept. This prevents near‑zero‑length segments from
994
+ * appearing as visible stubs near the arrowhead.
995
+ *
996
+ * @param points - The ordered vertex list.
997
+ * @returns A filtered copy, guaranteed to contain at least the first point.
998
+ */
999
+ function collapseStubs(points) {
1000
+ const out = [];
1001
+ for (const pt of points) {
1002
+ const last = out.at(-1);
1003
+ if (last === void 0 || Math.hypot(pt.x - last.x, pt.y - last.y) >= STUB_THRESHOLD) out.push(pt);
1004
+ }
1005
+ return out;
1006
+ }
1007
+ /**
1008
+ * True when the dependency arrow enters the target on its **left** edge (FS, SS).
1009
+ * False when the arrow enters on the target's **right** edge (FF, SF).
1010
+ *
1011
+ * The arrowhead uses SVG `orient="auto"` so it rotates to match the direction
1012
+ * of the **last** path segment. Therefore:
1013
+ *
1014
+ * - Left-entry → last segment must travel **RIGHT** (penultimate.x < tx).
1015
+ * - Right-entry → last segment must travel **LEFT** (penultimate.x > tx).
1016
+ *
1017
+ * @param type - The link type.
1018
+ * @returns `true` for FS / SS, `false` for FF / SF.
1019
+ */
1020
+ function isLeftEntry(type) {
1021
+ return type === "FS" || type === "SS";
1022
+ }
1023
+ /**
1024
+ * True when the link exits the source bar on its **right** edge (FS, FF).
1025
+ * False when it exits on the **left** edge (SS, SF).
1026
+ *
1027
+ * The first step after the source anchor should move **away** from the bar,
1028
+ * **not** into it. This means:
1029
+ *
1030
+ * - Exit‑right → first horizontal segment goes RIGHT (+TURN_MARGIN).
1031
+ * - Exit‑left → first horizontal segment goes LEFT (-TURN_MARGIN).
1032
+ *
1033
+ * @param type - The link type.
1034
+ * @returns `true` for FS / FF, `false` for SS / SF.
1035
+ */
1036
+ function isExitRight(type) {
1037
+ return type === "FS" || type === "FF";
1038
+ }
1039
+ /**
1040
+ * Computes anchor points for the given link type.
1041
+ *
1042
+ * | Type | Source anchor (`sx`) | Target anchor (`tx`) |
1043
+ * |------|-----------------------------|-------------------------------|
1044
+ * | FS | right edge of source | left edge of target |
1045
+ * | SS | left edge of source | left edge of target |
1046
+ * | FF | right edge of source | right edge of target |
1047
+ * | SF | left edge of source | right edge of target |
1048
+ *
1049
+ * Milestone offsets are applied automatically: ± {@link MILESTONE_HALF} replaces
1050
+ * ± width for zero‑width milestones.
1051
+ *
1052
+ * @param type - The link type determining start/end anchor points.
1053
+ * @param src - The source bar layout.
1054
+ * @param tgt - The target bar layout.
1055
+ * @returns Anchor x coordinates `{sx, tx}`.
1056
+ * @throws {Error} if the link type is not handled (exhaustiveness guard).
1057
+ */
1058
+ function getAnchors(type, src, tgt) {
1059
+ const srcRight = src.kind === "milestone" ? src.x + MILESTONE_HALF : src.x + src.width;
1060
+ const srcLeft = src.kind === "milestone" ? src.x - MILESTONE_HALF : src.x;
1061
+ const tgtRight = tgt.kind === "milestone" ? tgt.x + MILESTONE_HALF : tgt.x + tgt.width;
1062
+ const tgtLeft = tgt.kind === "milestone" ? tgt.x - MILESTONE_HALF : tgt.x;
1063
+ switch (type) {
1064
+ case "FS": return {
1065
+ sx: srcRight,
1066
+ tx: tgtLeft
1067
+ };
1068
+ case "SS": return {
1069
+ sx: srcLeft,
1070
+ tx: tgtLeft
1071
+ };
1072
+ case "FF": return {
1073
+ sx: srcRight,
1074
+ tx: tgtRight
1075
+ };
1076
+ case "SF": return {
1077
+ sx: srcLeft,
1078
+ tx: tgtRight
1079
+ };
1080
+ default: throw new Error(`Unhandled link type: ${String(type)}`);
1081
+ }
1082
+ }
1083
+ /**
1084
+ * Routes a link whose source and target rows are within 1 px of each other.
1085
+ *
1086
+ * **Direct‑line optimisation**
1087
+ * A plain horizontal segment is only used when it is non‑degenerate (`sx ≠ tx`)
1088
+ * AND the arrowhead direction is visually correct:
1089
+ *
1090
+ * | Entry side | Condition | Arrow direction |
1091
+ * |-----------|-----------|----------------|
1092
+ * | left | `sx < tx` | → RIGHT ✓ |
1093
+ * | right | `sx > tx` | ← LEFT ✓ |
1094
+ *
1095
+ * Otherwise a 6‑vertex detour is drawn so that the last segment approaches
1096
+ * the target from the correct side. By default the detour goes **below** the
1097
+ * bars; pass `above = true` when headroom is insufficient below.
1098
+ *
1099
+ * @param sx - Source anchor x.
1100
+ * @param sy - Source row center y.
1101
+ * @param tx - Target anchor x.
1102
+ * @param ty - Target row center y.
1103
+ * @param leftEntry - Whether the link enters the target on its left edge.
1104
+ * @param exitRight - Whether the link exits the source on its right edge.
1105
+ * @param above - Route the detour above the bar row instead of below (default `false`).
1106
+ * @returns An ordered array of {@link Point} vertices.
1107
+ */
1108
+ function routeSameRow(sx, sy, tx, ty, leftEntry, exitRight, above = false) {
1109
+ if (Math.abs(sx - tx) >= STUB_THRESHOLD) {
1110
+ if (leftEntry && sx < tx || !leftEntry && sx > tx) return [{
1111
+ x: sx,
1112
+ y: sy
1113
+ }, {
1114
+ x: tx,
1115
+ y: ty
1116
+ }];
696
1117
  }
697
- return [addDays(new Date(minMs), -paddingDays), addDays(new Date(maxMs), paddingDays)];
1118
+ const exitDir = exitRight ? TURN_MARGIN : -TURN_MARGIN;
1119
+ const detourY = sy + (above ? -SAME_ROW_DETOUR : SAME_ROW_DETOUR);
1120
+ const approachX = leftEntry ? tx - TURN_MARGIN : tx + TURN_MARGIN;
1121
+ return [
1122
+ {
1123
+ x: sx,
1124
+ y: sy
1125
+ },
1126
+ {
1127
+ x: sx + exitDir,
1128
+ y: sy
1129
+ },
1130
+ {
1131
+ x: sx + exitDir,
1132
+ y: detourY
1133
+ },
1134
+ {
1135
+ x: approachX,
1136
+ y: detourY
1137
+ },
1138
+ {
1139
+ x: approachX,
1140
+ y: ty
1141
+ },
1142
+ {
1143
+ x: tx,
1144
+ y: ty
1145
+ }
1146
+ ];
1147
+ }
1148
+ /**
1149
+ * Routes a link between **different** rows using an orthogonal path.
1150
+ *
1151
+ * 1. Step **away** from the source bar to the crossover x (`crossX`).
1152
+ * 2. Travel **vertically** to the midpoint between rows (`midY`).
1153
+ * 3. Travel **horizontally** to the approach point on the correct side
1154
+ * of the target.
1155
+ * 4. Travel **vertically** to the target row (`ty`).
1156
+ * 5. Final segment to the target entry point (`tx`).
1157
+ *
1158
+ * The crossover x is clamped so the path never doubles back past both bars.
1159
+ * When exit and entry are on the **same** side (SS / FF) the exit-side step
1160
+ * is limited to the approach x, avoiding a wide U‑shape.
1161
+ *
1162
+ * The approach point is chosen so the last segment travels in the arrow
1163
+ * direction demanded by the entry side:
1164
+ *
1165
+ * - Left‑entry (FS, SS): approach from the **left** → `tx - TURN_MARGIN`
1166
+ * Last segment goes RIGHT.
1167
+ * - Right‑entry (FF, SF): approach from the **right** → `tx + TURN_MARGIN`
1168
+ * Last segment goes LEFT.
1169
+ *
1170
+ * left‑entry (FS / SS) right‑entry (FF / SF)
1171
+ * ───────────────────── ─────────────────────
1172
+ * sx ●────────────────► sx ●────────────────►
1173
+ * exitDir exitDir
1174
+ * │ │
1175
+ * │ midY │ midY
1176
+ * ▼ ════════════════════► ▼ ════════════════════►
1177
+ * │ │
1178
+ * │ │
1179
+ * ▼ approachFromLeft ▼ approachFromRight
1180
+ * ●────────────────► ◄────────────────●
1181
+ * tx tx
1182
+ *
1183
+ * @param sx - Source anchor x.
1184
+ * @param sy - Source row center y.
1185
+ * @param tx - Target anchor x.
1186
+ * @param ty - Target row center y.
1187
+ * @param leftEntry - Whether the link enters the target on its left edge.
1188
+ * @param exitRight - Whether the link exits the source on its right edge.
1189
+ * @returns An ordered array of {@link Point} vertices.
1190
+ */
1191
+ function routeMultiRow(sx, sy, tx, ty, leftEntry, exitRight) {
1192
+ const midY = Math.round(Math.abs(sy - ty) / ROW_HEIGHT) % 2 === 0 ? (sy + ty) / 2 + ROW_HEIGHT / 2 : (sy + ty) / 2;
1193
+ const approachX = leftEntry ? tx - TURN_MARGIN : tx + TURN_MARGIN;
1194
+ const crossX = exitRight ? Math.max(sx + TURN_MARGIN, approachX) : Math.min(sx - TURN_MARGIN, approachX);
1195
+ return [
1196
+ {
1197
+ x: sx,
1198
+ y: sy
1199
+ },
1200
+ {
1201
+ x: crossX,
1202
+ y: sy
1203
+ },
1204
+ {
1205
+ x: crossX,
1206
+ y: midY
1207
+ },
1208
+ {
1209
+ x: approachX,
1210
+ y: midY
1211
+ },
1212
+ {
1213
+ x: approachX,
1214
+ y: ty
1215
+ },
1216
+ {
1217
+ x: tx,
1218
+ y: ty
1219
+ }
1220
+ ];
1221
+ }
1222
+ /**
1223
+ * Produces the vertex list for an orthogonal connector between source and target.
1224
+ *
1225
+ * ## Anchor points (sx / tx)
1226
+ *
1227
+ * | Type | Source anchor (`sx`) | Target anchor (`tx`) |
1228
+ * |------|-----------------------------|-------------------------------|
1229
+ * | FS | right edge of source | left edge of target |
1230
+ * | SS | left edge of source | left edge of target |
1231
+ * | FF | right edge of source | right edge of target |
1232
+ * | SF | left edge of source | right edge of target |
1233
+ *
1234
+ * Milestone offsets are applied automatically: ± {@link MILESTONE_HALF} replaces
1235
+ * ± width for zero‑width milestones.
1236
+ *
1237
+ * ## Routing strategy
1238
+ *
1239
+ * **Same row** (|sy − ty| < 1 px):
1240
+ * - Direct horizontal line when non‑degenerate **and** the arrow direction
1241
+ * naturally points **into** the target (see {@link routeSameRow}).
1242
+ * - Otherwise a 6‑vertex detour below the bars is drawn.
1243
+ *
1244
+ * **Different rows**: always a 6‑vertex orthogonal path that steps away from
1245
+ * the source, passes through the midpoint between rows, and approaches the
1246
+ * target from the correct side (see {@link routeMultiRow}).
1247
+ *
1248
+ * ## Arrowhead direction guarantee
1249
+ *
1250
+ * The SVG `marker-end` uses `orient="auto"`, so the arrow rotates to match
1251
+ * the last segment. This function ensures the last segment always travels
1252
+ * **into** the target on the semantically correct edge:
1253
+ *
1254
+ * | Entry side | Target edge | Last segment direction |
1255
+ * |-----------|-------------|-----------------------|
1256
+ * | left | left edge | → RIGHT |
1257
+ * | right | right edge | ← LEFT |
1258
+ *
1259
+ * @param type - The link type determining start/end anchor points.
1260
+ * @param src - The source bar layout.
1261
+ * @param tgt - The target bar layout.
1262
+ * @returns An ordered array of {@link Point} vertices.
1263
+ */
1264
+ function route(type, src, tgt) {
1265
+ const { sx, tx } = getAnchors(type, src, tgt);
1266
+ const sy = src.centerY;
1267
+ const ty = tgt.centerY;
1268
+ const leftEntry = isLeftEntry(type);
1269
+ const exitRight = isExitRight(type);
1270
+ return collapseStubs(Math.abs(sy - ty) < 1 ? routeSameRow(sx, sy, tx, ty, leftEntry, exitRight) : routeMultiRow(sx, sy, tx, ty, leftEntry, exitRight));
698
1271
  }
699
- //#endregion
700
- //#region src/gantt-chart/rendering/linkRouter.ts
701
- const TURN_MARGIN = 12;
702
1272
  /**
703
1273
  * Computes orthogonal routing for all dependency links.
704
1274
  * Links whose source or target is not in the layout map are skipped silently
705
1275
  * (e.g. when the row is collapsed).
1276
+ *
1277
+ * @param links - The dependency links to route.
1278
+ * @param layouts - A map from task ID to its computed {@link BarLayout}.
1279
+ * @returns An array of {@link RoutedLink} objects with computed vertex paths.
706
1280
  */
707
1281
  function routeLinks(links, layouts) {
708
1282
  return links.map((link) => {
@@ -713,99 +1287,22 @@ function routeLinks(links, layouts) {
713
1287
  linkId: link.id,
714
1288
  sourceTaskId: link.source,
715
1289
  targetTaskId: link.target,
1290
+ type: link.type,
716
1291
  points: route(link.type, src, tgt)
717
1292
  };
718
1293
  }).filter((r) => r !== null);
719
1294
  }
1295
+ //#endregion
1296
+ //#region src/lib/vanilla/dom/helpers.ts
720
1297
  /**
721
- * Produces the vertex list for an orthogonal connector between src and tgt.
1298
+ * Batches style assignments; avoids repeated style recalculations.
722
1299
  *
723
- * Link semantics:
724
- * FS = source.finish target.start (most common)
725
- * SS = source.start → target.start
726
- * FF = source.finish → target.finish
727
- * SF = source.start → target.finish
1300
+ * @param elem - The target element.
1301
+ * @param styles - A partial CSS style declaration to apply.
728
1302
  */
729
- function route(type, src, tgt) {
730
- let sx, tx;
731
- const sy = src.centerY;
732
- const ty = tgt.centerY;
733
- switch (type) {
734
- case "FS":
735
- sx = src.type === "milestone" ? src.x + MILESTONE_HALF : src.x + src.width;
736
- tx = tgt.type === "milestone" ? tgt.x - MILESTONE_HALF : tgt.x;
737
- break;
738
- case "SS":
739
- sx = src.type === "milestone" ? src.x - MILESTONE_HALF : src.x;
740
- tx = tgt.type === "milestone" ? tgt.x - MILESTONE_HALF : tgt.x;
741
- break;
742
- case "FF":
743
- sx = src.type === "milestone" ? src.x + MILESTONE_HALF : src.x + src.width;
744
- tx = tgt.type === "milestone" ? tgt.x + MILESTONE_HALF : tgt.x + tgt.width;
745
- break;
746
- case "SF":
747
- sx = src.type === "milestone" ? src.x - MILESTONE_HALF : src.x;
748
- tx = tgt.type === "milestone" ? tgt.x + MILESTONE_HALF : tgt.x + tgt.width;
749
- break;
750
- }
751
- if (Math.abs(sy - ty) < 1) return [{
752
- x: sx,
753
- y: sy
754
- }, {
755
- x: tx,
756
- y: ty
757
- }];
758
- if (sx <= tx) {
759
- const midX = sx + Math.max(TURN_MARGIN, (tx - sx) / 2);
760
- return [
761
- {
762
- x: sx,
763
- y: sy
764
- },
765
- {
766
- x: midX,
767
- y: sy
768
- },
769
- {
770
- x: midX,
771
- y: ty
772
- },
773
- {
774
- x: tx,
775
- y: ty
776
- }
777
- ];
778
- }
779
- const loopX = tx - TURN_MARGIN;
780
- return [
781
- {
782
- x: sx,
783
- y: sy
784
- },
785
- {
786
- x: sx + TURN_MARGIN,
787
- y: sy
788
- },
789
- {
790
- x: sx + TURN_MARGIN,
791
- y: (sy + ty) / 2
792
- },
793
- {
794
- x: loopX,
795
- y: (sy + ty) / 2
796
- },
797
- {
798
- x: loopX,
799
- y: ty
800
- },
801
- {
802
- x: tx,
803
- y: ty
804
- }
805
- ];
1303
+ function css(elem, styles) {
1304
+ for (const [k, v] of Object.entries(styles)) elem.style[k] = v ?? "";
806
1305
  }
807
- //#endregion
808
- //#region src/gantt-chart/vanilla/dom/helpers.ts
809
1306
  function el(tag, props, ns) {
810
1307
  const elem = ns ? document.createElementNS(ns, tag) : document.createElement(tag);
811
1308
  if (props !== void 0) for (const [k, v] of Object.entries(props)) if (k === "style" && typeof v === "object" && v !== null) css(elem, v);
@@ -813,32 +1310,61 @@ function el(tag, props, ns) {
813
1310
  else elem.setAttribute(k, String(v));
814
1311
  return elem;
815
1312
  }
816
- /** Batches style assignments; avoids repeated style recalculations. */
817
- function css(elem, styles) {
818
- for (const [k, v] of Object.entries(styles)) elem.style[k] = v ?? "";
819
- }
820
- /** Removes all child nodes from elem. Faster than innerHTML = '' for large subtrees. */
1313
+ /**
1314
+ * Removes all child nodes from elem. Faster than `innerHTML = ''` for large subtrees.
1315
+ *
1316
+ * @param elem - The element to clear.
1317
+ */
821
1318
  function clearChildren(elem) {
822
1319
  while (elem.firstChild !== null) elem.removeChild(elem.firstChild);
823
1320
  }
824
- /** Appends all nodes from an array/fragment into parent in one pass. */
1321
+ /**
1322
+ * Appends all nodes from an array/fragment into parent in one pass.
1323
+ *
1324
+ * @param parent - The parent element.
1325
+ * @param children - The child elements or text nodes to append.
1326
+ */
825
1327
  function appendAll(parent, children) {
826
1328
  const frag = document.createDocumentFragment();
827
1329
  for (const c of children) frag.appendChild(c);
828
1330
  parent.append(frag);
829
1331
  }
830
- /** Sets multiple SVG attributes in one call. */
1332
+ /**
1333
+ * Sets multiple SVG attributes in one call.
1334
+ *
1335
+ * @param elem - The target SVG element.
1336
+ * @param attrs - Attributes to set (values are stringified).
1337
+ */
831
1338
  function setAttrs(elem, attrs) {
832
1339
  for (const [k, v] of Object.entries(attrs)) elem.setAttribute(k, String(v));
833
1340
  }
834
1341
  //#endregion
835
- //#region src/gantt-chart/vanilla/dom/timeHeader.ts
1342
+ //#region src/lib/vanilla/dom/timeHeader.ts
1343
+ function specialDayKind(date, specialDaysByDate, showWeekends, weekendDays) {
1344
+ const dateKey = startOfDay(date).toISOString().slice(0, 10);
1345
+ const specialDay = specialDaysByDate.get(dateKey);
1346
+ if (specialDay !== void 0) return specialDay.kind;
1347
+ if (showWeekends && weekendDays.has(date.getUTCDay())) return "weekend";
1348
+ return null;
1349
+ }
1350
+ /**
1351
+ * Inline style helper local to this module.
1352
+ *
1353
+ * @param elem - The target element.
1354
+ * @param styles - A partial CSS style declaration to apply.
1355
+ */
1356
+ function css_(elem, styles) {
1357
+ for (const [k, v] of Object.entries(styles)) elem.style[k] = v ?? "";
1358
+ }
836
1359
  /**
837
1360
  * Fully replaces the content of `container` with two header rows.
838
1361
  * Called on scale change or viewport change only — not on scroll.
1362
+ *
1363
+ * @param container - The header container element to render into.
1364
+ * @param state - The current chart state.
839
1365
  */
840
1366
  function renderTimeHeader(container, state) {
841
- const { scale, viewportStart, viewportEnd, mapper, totalWidth, locale } = state;
1367
+ const { scale, viewportStart, viewportEnd, mapper, totalWidth, locale, showWeekends, weekendDays, specialDaysByDate } = state;
842
1368
  const weekStartsOn = locale.weekStartsOn ?? 1;
843
1369
  const upperCells = [];
844
1370
  const lowerCells = [];
@@ -853,7 +1379,8 @@ function renderTimeHeader(container, state) {
853
1379
  lowerCells.push({
854
1380
  label: formatHeaderLabel(cur, scale, locale),
855
1381
  x,
856
- width: w
1382
+ width: w,
1383
+ date: new Date(cur)
857
1384
  });
858
1385
  const uLabel = formatUpperLabel(cur, scale, locale);
859
1386
  if (uLabel !== prevUpperLabel) {
@@ -928,6 +1455,19 @@ function renderTimeHeader(container, state) {
928
1455
  whiteSpace: "nowrap"
929
1456
  });
930
1457
  d.textContent = cell.label;
1458
+ if (scale === "day") {
1459
+ const kind = specialDayKind(cell.date, specialDaysByDate, showWeekends, weekendDays);
1460
+ if (kind !== null) {
1461
+ d.classList.add(`gantt-header-cell--${kind}`);
1462
+ const dateKey = startOfDay(cell.date).toISOString().slice(0, 10);
1463
+ d.dataset["date"] = dateKey;
1464
+ const specialDay = specialDaysByDate.get(dateKey);
1465
+ if (specialDay?.label !== void 0) {
1466
+ d.dataset["label"] = specialDay.label;
1467
+ d.title = specialDay.label;
1468
+ }
1469
+ }
1470
+ }
931
1471
  return d;
932
1472
  });
933
1473
  appendAll(upperRow, upperNodes);
@@ -936,12 +1476,8 @@ function renderTimeHeader(container, state) {
936
1476
  container.append(upperRow);
937
1477
  container.append(lowerRow);
938
1478
  }
939
- /** Inline style helper local to this module. */
940
- function css_(elem, styles) {
941
- for (const [k, v] of Object.entries(styles)) elem.style[k] = v ?? "";
942
- }
943
1479
  //#endregion
944
- //#region src/gantt-chart/vanilla/dom/gridColumns.ts
1480
+ //#region src/lib/vanilla/dom/gridColumns.ts
945
1481
  const DEFAULT_GRID_COLUMNS = [
946
1482
  {
947
1483
  id: "name",
@@ -949,17 +1485,17 @@ const DEFAULT_GRID_COLUMNS = [
949
1485
  width: "1fr"
950
1486
  },
951
1487
  {
952
- id: "start_date",
1488
+ id: "startDate",
953
1489
  header: "Start time",
954
1490
  width: "90px",
955
- field: "start_date",
1491
+ field: "startDate",
956
1492
  format: (value, _task, _row, locale) => formatDisplayDate(String(value), locale)
957
1493
  },
958
1494
  {
959
- id: "duration",
1495
+ id: "durationHours",
960
1496
  header: "Duration",
961
1497
  width: "68px",
962
- field: "duration",
1498
+ field: "durationHours",
963
1499
  format: (value) => value > 0 ? String(value) : "—"
964
1500
  },
965
1501
  {
@@ -970,27 +1506,30 @@ const DEFAULT_GRID_COLUMNS = [
970
1506
  ];
971
1507
  /**
972
1508
  * Returns a localized default grid column schema.
973
- * Column headers use locale label overrides with EN_US_LABELS fallback.
1509
+ * Column headers use locale label overrides with `EN_US_LABELS` fallback.
1510
+ *
1511
+ * @param locale - The {@link ChartLocale} to derive column header labels from.
1512
+ * @returns An array of {@link GridColumn} objects.
974
1513
  */
975
1514
  function gridColumnDefaults(locale) {
976
1515
  return [
977
1516
  {
978
1517
  id: "name",
979
- header: locale.labels?.column_task_name ?? EN_US_LABELS.column_task_name,
1518
+ header: locale.labels?.columnTaskName ?? EN_US_LABELS.columnTaskName,
980
1519
  width: "1fr"
981
1520
  },
982
1521
  {
983
- id: "start_date",
984
- header: locale.labels?.column_start_time ?? EN_US_LABELS.column_start_time,
1522
+ id: "startDate",
1523
+ header: locale.labels?.columnStartDate ?? EN_US_LABELS.columnStartDate,
985
1524
  width: "90px",
986
- field: "start_date",
1525
+ field: "startDate",
987
1526
  format: (value, _task, _row, loc) => formatDisplayDate(String(value), loc)
988
1527
  },
989
1528
  {
990
- id: "duration",
991
- header: locale.labels?.column_duration ?? EN_US_LABELS.column_duration,
1529
+ id: "durationHours",
1530
+ header: locale.labels?.columnDuration ?? EN_US_LABELS.columnDuration,
992
1531
  width: "68px",
993
- field: "duration",
1532
+ field: "durationHours",
994
1533
  format: (value) => value > 0 ? String(value) : "—"
995
1534
  },
996
1535
  {
@@ -1000,15 +1539,27 @@ function gridColumnDefaults(locale) {
1000
1539
  }
1001
1540
  ];
1002
1541
  }
1542
+ /**
1543
+ * Builds a CSS `grid-template-columns` value from a column schema.
1544
+ *
1545
+ * @param columns - The full column schema array (only visible columns are included).
1546
+ * @returns A space-separated CSS track list.
1547
+ */
1003
1548
  function gridTemplateColumns(columns) {
1004
1549
  return columns.filter((c) => c.visible !== false).map((c) => c.width).join(" ");
1005
1550
  }
1551
+ /**
1552
+ * Filters a column schema to only visible columns.
1553
+ *
1554
+ * @param columns - The full column schema array.
1555
+ * @returns A new array containing only columns where `visible` is not `false`.
1556
+ */
1006
1557
  function visibleColumns(columns) {
1007
1558
  return columns.filter((c) => c.visible !== false);
1008
1559
  }
1009
1560
  const GRID_COLUMN_FR_MIN_WIDTH = 120;
1010
- const PX_RE = /^(\d+(?:\.\d+)?)px$/;
1011
- const FR_RE = /^(\d+(?:\.\d+)?)fr$/;
1561
+ const PX_RE = /^(\d+(?:\.\d+)?)px$/u;
1562
+ const FR_RE = /^(\d+(?:\.\d+)?)fr$/u;
1012
1563
  function parseColumnMinWidth(width) {
1013
1564
  const trimmed = width.trim();
1014
1565
  const pxMatch = PX_RE.exec(trimmed);
@@ -1017,73 +1568,57 @@ function parseColumnMinWidth(width) {
1017
1568
  if (frMatch) return parseFloat(frMatch[1] ?? "0") * 120;
1018
1569
  return 0;
1019
1570
  }
1571
+ /**
1572
+ * Computes the minimum natural pixel width of a grid column schema.
1573
+ *
1574
+ * @param columns - The full column schema array.
1575
+ * @returns The sum of minimum widths: `px` columns sum directly, `fr` units contribute
1576
+ * `GRID_COLUMN_FR_MIN_WIDTH` px each.
1577
+ */
1020
1578
  function gridNaturalWidth(columns) {
1021
1579
  let total = 0;
1022
1580
  for (const col of visibleColumns(columns)) total += parseColumnMinWidth(col.width);
1023
1581
  return total;
1024
1582
  }
1025
1583
  //#endregion
1026
- //#region src/gantt-chart/vanilla/dom/leftPane.ts
1584
+ //#region src/lib/vanilla/dom/leftPane.ts
1027
1585
  const INDENT = 16;
1028
1586
  const COLUMN_MIN_WIDTH = 30;
1029
- /** Renders the left grid pane. */
1030
- function renderLeftPane(container, state, cbs, columns) {
1031
- const { allRows, selectedId, expandedIds, startIndex, endIndex, paddingTop, paddingBottom, locale } = state;
1032
- const frag = document.createDocumentFragment();
1033
- if (paddingTop > 0) {
1034
- const spacer = el("div");
1035
- spacer.style.height = `${paddingTop}px`;
1036
- frag.append(spacer);
1037
- }
1038
- for (const row of allRows.slice(startIndex, endIndex + 1)) frag.append(buildRow(row, selectedId, expandedIds, cbs, columns, locale));
1039
- if (paddingBottom > 0) {
1040
- const spacer = el("div");
1041
- spacer.style.height = `${paddingBottom}px`;
1042
- frag.append(spacer);
1587
+ function toTask$1(node) {
1588
+ const base = {
1589
+ id: node.id,
1590
+ text: node.text,
1591
+ startDate: node.startDate,
1592
+ ...node.parent === void 0 ? {} : { parent: node.parent },
1593
+ ...node.color === void 0 ? {} : { color: node.color },
1594
+ ...node.data === void 0 ? {} : { data: node.data }
1595
+ };
1596
+ switch (node.kind) {
1597
+ case "task": return {
1598
+ ...base,
1599
+ kind: "task",
1600
+ durationHours: node.durationHours,
1601
+ percentComplete: node.percentComplete
1602
+ };
1603
+ case "project": return {
1604
+ ...base,
1605
+ kind: "project",
1606
+ durationHours: node.durationHours,
1607
+ percentComplete: node.percentComplete,
1608
+ open: node.open
1609
+ };
1610
+ case "milestone": return {
1611
+ ...base,
1612
+ kind: "milestone"
1613
+ };
1043
1614
  }
1044
- clearChildren(container);
1045
- container.append(frag);
1046
- }
1047
- function buildRow(row, selectedId, expandedIds, cbs, columns, locale) {
1048
- const selected = row.id === selectedId;
1049
- const wrapper = el("div");
1050
- wrapper.className = "gantt-row";
1051
- css(wrapper, {
1052
- display: "grid",
1053
- gridTemplateColumns: gridTemplateColumns(columns),
1054
- height: `${ROW_HEIGHT}px`,
1055
- alignItems: "center",
1056
- paddingLeft: "8px",
1057
- background: selected ? "var(--gantt-row-selected)" : "var(--gantt-bg)",
1058
- borderBottom: "1px solid var(--gantt-border)",
1059
- cursor: "default",
1060
- boxSizing: "border-box"
1061
- });
1062
- wrapper.tabIndex = 0;
1063
- wrapper.setAttribute("role", "row");
1064
- wrapper.setAttribute("aria-selected", String(selected));
1065
- wrapper.dataset["taskId"] = String(row.id);
1066
- wrapper.addEventListener("click", () => {
1067
- const task = toTask$1(row);
1068
- cbs.onRowClick({
1069
- id: row.id,
1070
- task
1071
- });
1072
- });
1073
- wrapper.addEventListener("keydown", (event) => {
1074
- if (event.key === "Enter" || event.key === " ") {
1075
- event.preventDefault();
1076
- cbs.onSelect(row.id);
1077
- }
1078
- });
1079
- for (const column of visibleColumns(columns)) wrapper.append(buildCell(column, row, expandedIds, cbs, locale));
1080
- return wrapper;
1081
1615
  }
1082
- function buildCell(column, row, expandedIds, cbs, locale) {
1083
- switch (column.id) {
1084
- case "name": return buildTreeNameCell(row, expandedIds, cbs);
1085
- case "actions": return buildAddButton(row, cbs, locale);
1086
- default: return buildDataCell(row, column, locale);
1616
+ function getTaskField(task, field) {
1617
+ switch (field) {
1618
+ case "durationHours": return task.kind !== "milestone" ? task.durationHours : void 0;
1619
+ case "percentComplete": return task.kind !== "milestone" ? task.percentComplete : void 0;
1620
+ case "open": return task.kind === "project" ? task.open : void 0;
1621
+ default: return task[field];
1087
1622
  }
1088
1623
  }
1089
1624
  function buildTreeNameCell(row, expandedIds, cbs) {
@@ -1128,7 +1663,7 @@ function buildTreeNameCell(row, expandedIds, cbs) {
1128
1663
  const label = el("span");
1129
1664
  css(label, {
1130
1665
  fontSize: "var(--gantt-font-size-md)",
1131
- fontWeight: row.type === "project" ? "var(--gantt-font-weight-bold)" : "var(--gantt-font-weight-normal)",
1666
+ fontWeight: row.kind === "project" ? "var(--gantt-font-weight-bold)" : "var(--gantt-font-weight-normal)",
1132
1667
  color: "var(--gantt-text)",
1133
1668
  overflow: "hidden",
1134
1669
  textOverflow: "ellipsis",
@@ -1152,9 +1687,9 @@ function buildDataCell(row, column, locale) {
1152
1687
  css(cell, styles);
1153
1688
  const task = toTask$1(row);
1154
1689
  if (column.field !== void 0) {
1155
- const rawValue = task[column.field];
1690
+ const rawValue = getTaskField(task, column.field);
1156
1691
  if (column.format !== void 0) cell.textContent = column.format(rawValue, task, row, locale);
1157
- else cell.textContent = rawValue !== null && rawValue !== void 0 ? String(rawValue) : "";
1692
+ else cell.textContent = rawValue !== null && rawValue !== void 0 ? typeof rawValue === "object" ? JSON.stringify(rawValue) : String(rawValue) : "";
1158
1693
  }
1159
1694
  return cell;
1160
1695
  }
@@ -1162,7 +1697,7 @@ function buildAddButton(row, cbs, locale) {
1162
1697
  const btn = el("button");
1163
1698
  btn.className = "gantt-add-btn";
1164
1699
  btn.textContent = "+";
1165
- btn.title = locale.labels?.add_subtask_title ?? EN_US_LABELS.add_subtask_title;
1700
+ btn.title = locale.labels?.addSubtaskTitle ?? EN_US_LABELS.addSubtaskTitle;
1166
1701
  css(btn, {
1167
1702
  background: "none",
1168
1703
  border: "none",
@@ -1173,24 +1708,83 @@ function buildAddButton(row, cbs, locale) {
1173
1708
  });
1174
1709
  btn.addEventListener("click", (event) => {
1175
1710
  event.stopPropagation();
1176
- cbs.onAdd(row.id);
1711
+ cbs.onTaskAdd(row.id);
1177
1712
  });
1178
1713
  return btn;
1179
1714
  }
1180
- function toTask$1(row) {
1181
- return {
1182
- id: row.id,
1183
- text: row.text,
1184
- start_date: row.start_date,
1185
- duration: row.duration,
1186
- progress: row.progress,
1187
- type: row.type,
1188
- open: row.open,
1189
- ...row.parent === void 0 ? {} : { parent: row.parent },
1190
- ...row.color === void 0 ? {} : { color: row.color }
1191
- };
1715
+ function buildCell(column, row, expandedIds, cbs, locale) {
1716
+ switch (column.id) {
1717
+ case "name": return buildTreeNameCell(row, expandedIds, cbs);
1718
+ case "actions": return buildAddButton(row, cbs, locale);
1719
+ default: return buildDataCell(row, column, locale);
1720
+ }
1721
+ }
1722
+ function buildRow(row, selectedId, expandedIds, cbs, columns, locale) {
1723
+ const selected = row.id === selectedId;
1724
+ const wrapper = el("div");
1725
+ wrapper.className = "gantt-row";
1726
+ css(wrapper, {
1727
+ display: "grid",
1728
+ gridTemplateColumns: gridTemplateColumns(columns),
1729
+ height: `${ROW_HEIGHT}px`,
1730
+ alignItems: "center",
1731
+ paddingLeft: "8px",
1732
+ background: selected ? "var(--gantt-row-selected)" : "var(--gantt-bg)",
1733
+ borderBottom: "1px solid var(--gantt-border)",
1734
+ cursor: "default",
1735
+ boxSizing: "border-box"
1736
+ });
1737
+ wrapper.tabIndex = 0;
1738
+ wrapper.setAttribute("role", "row");
1739
+ wrapper.setAttribute("aria-selected", String(selected));
1740
+ wrapper.dataset["taskId"] = String(row.id);
1741
+ wrapper.addEventListener("click", () => {
1742
+ const task = toTask$1(row);
1743
+ cbs.onRowClick({
1744
+ id: row.id,
1745
+ task
1746
+ });
1747
+ });
1748
+ wrapper.addEventListener("keydown", (event) => {
1749
+ if (event.key === "Enter" || event.key === " ") {
1750
+ event.preventDefault();
1751
+ cbs.onTaskSelect(row.id);
1752
+ }
1753
+ });
1754
+ for (const column of visibleColumns(columns)) wrapper.append(buildCell(column, row, expandedIds, cbs, locale));
1755
+ return wrapper;
1756
+ }
1757
+ /**
1758
+ * Renders the left grid pane.
1759
+ *
1760
+ * @param container - The left pane body element to render into.
1761
+ * @param state - The current chart state.
1762
+ * @param cbs - The left pane callbacks.
1763
+ * @param columns - The grid column schema.
1764
+ */
1765
+ function renderLeftPane(container, state, cbs, columns) {
1766
+ const { allRows, selectedId, expandedIds, startIndex, endIndex, paddingTop, paddingBottom, locale } = state;
1767
+ const frag = document.createDocumentFragment();
1768
+ if (paddingTop > 0) {
1769
+ const spacer = el("div");
1770
+ spacer.style.height = `${paddingTop}px`;
1771
+ frag.append(spacer);
1772
+ }
1773
+ for (const row of allRows.slice(startIndex, endIndex + 1)) frag.append(buildRow(row, selectedId, expandedIds, cbs, columns, locale));
1774
+ if (paddingBottom > 0) {
1775
+ const spacer = el("div");
1776
+ spacer.style.height = `${paddingBottom}px`;
1777
+ frag.append(spacer);
1778
+ }
1779
+ clearChildren(container);
1780
+ container.append(frag);
1192
1781
  }
1193
- /** Builds the header row for the left pane. */
1782
+ /**
1783
+ * Builds the header row for the left pane.
1784
+ *
1785
+ * @param columns - The grid column schema.
1786
+ * @returns The header DOM element.
1787
+ */
1194
1788
  function buildLeftPaneHeader(columns) {
1195
1789
  const header = el("div");
1196
1790
  css(header, {
@@ -1246,8 +1840,13 @@ function buildLeftPaneHeader(columns) {
1246
1840
  }
1247
1841
  /**
1248
1842
  * Wires up column resize interactions on header handles.
1249
- * Must be called after the header is in the DOM (so getBoundingClientRect works).
1250
- * Returns a cleanup function.
1843
+ * Must be called after the header is in the DOM (so `getBoundingClientRect` works).
1844
+ *
1845
+ * @param headerEl - The header element containing resize handles.
1846
+ * @param bodyEl - The body element whose rows share the column widths.
1847
+ * @param columns - The grid column schema (mutated in place on resize end).
1848
+ * @param onChange - Optional callback fired on drag end with updated columns.
1849
+ * @returns A cleanup function that removes all resize listeners.
1251
1850
  */
1252
1851
  function setupColumnResize(headerEl, bodyEl, columns, onChange) {
1253
1852
  const handles = headerEl.querySelectorAll(".gantt-col-resize-handle");
@@ -1300,19 +1899,22 @@ function setupColumnResize(headerEl, bodyEl, columns, onChange) {
1300
1899
  };
1301
1900
  }
1302
1901
  //#endregion
1303
- //#region src/gantt-chart/vanilla/dom/dependencyLayer.ts
1902
+ //#region src/lib/vanilla/dom/dependencyLayer.ts
1304
1903
  const NS = "http://www.w3.org/2000/svg";
1904
+ const ARROW_PATH = "M 0 1 L 10 5 L 0 9 Z";
1305
1905
  const ARROW_SIZE = 6;
1306
1906
  /**
1307
1907
  * Creates the SVG overlay element. Call once; pass to updateDependencyLayer on each render.
1308
1908
  * Also creates a hidden ghost-line path used during link-creation drags.
1909
+ *
1910
+ * The SVG is initially zero-sized; `updateDependencyLayer` sets width/height each frame.
1911
+ *
1912
+ * @param _totalWidth - The total pixel width of the SVG viewport.
1913
+ * @param _totalHeight - The total pixel height of the SVG viewport.
1914
+ * @returns An `SVGSVGElement` ready to be inserted into the DOM.
1309
1915
  */
1310
- function createDependencyLayer(totalWidth, totalHeight) {
1916
+ function createDependencyLayer(_totalWidth, _totalHeight) {
1311
1917
  const svg = document.createElementNS(NS, "svg");
1312
- setAttrs(svg, {
1313
- width: totalWidth,
1314
- height: totalHeight
1315
- });
1316
1918
  Object.assign(svg.style, {
1317
1919
  position: "absolute",
1318
1920
  top: "0",
@@ -1327,7 +1929,7 @@ function createDependencyLayer(totalWidth, totalHeight) {
1327
1929
  setAttrs(marker, {
1328
1930
  id,
1329
1931
  viewBox: "0 0 10 10",
1330
- refX: "9",
1932
+ refX: "10",
1331
1933
  refY: "5",
1332
1934
  markerWidth: ARROW_SIZE,
1333
1935
  markerHeight: ARROW_SIZE,
@@ -1335,7 +1937,7 @@ function createDependencyLayer(totalWidth, totalHeight) {
1335
1937
  });
1336
1938
  const path = document.createElementNS(NS, "path");
1337
1939
  setAttrs(path, {
1338
- d: "M 0 1 L 10 5 L 0 9 Z",
1940
+ d: ARROW_PATH,
1339
1941
  fill: color
1340
1942
  });
1341
1943
  marker.append(path);
@@ -1357,21 +1959,28 @@ function createDependencyLayer(totalWidth, totalHeight) {
1357
1959
  }
1358
1960
  /**
1359
1961
  * Shows or updates the ghost line drawn during a link-creation drag.
1360
- * Pass valid=true when the pointer is over a valid target bar.
1962
+ *
1963
+ * @param svg - The SVG dependency layer element.
1964
+ * @param x1 - Start X coordinate.
1965
+ * @param y1 - Start Y coordinate.
1966
+ * @param x2 - End X coordinate.
1967
+ * @param y2 - End Y coordinate.
1968
+ * @param valid - When `true`, the line is drawn solid with an arrow marker.
1361
1969
  */
1362
1970
  function showGhostLine(svg, x1, y1, x2, y2, valid) {
1363
1971
  const ghost = svg.querySelector("path.gantt-ghost-line");
1364
1972
  if (ghost === null) return;
1365
- setAttrs(ghost, {
1366
- d: `M ${x1},${y1} L ${x2},${y2}`,
1367
- "stroke-dasharray": valid ? "none" : "5 3"
1368
- });
1973
+ setAttrs(ghost, { d: `M ${x1},${y1} L ${x2},${y2}` });
1974
+ if (valid) ghost.removeAttribute("stroke-dasharray");
1975
+ else ghost.setAttribute("stroke-dasharray", "5 3");
1369
1976
  if (valid) ghost.setAttribute("marker-end", "url(#gantt-arrow)");
1370
1977
  else ghost.removeAttribute("marker-end");
1371
1978
  ghost.style.display = "";
1372
1979
  }
1373
1980
  /**
1374
1981
  * Hides the ghost line after a link-creation drag completes or is cancelled.
1982
+ *
1983
+ * @param svg - The SVG dependency layer element.
1375
1984
  */
1376
1985
  function hideGhostLine(svg) {
1377
1986
  const ghost = svg.querySelector("path.gantt-ghost-line");
@@ -1383,26 +1992,28 @@ function hideGhostLine(svg) {
1383
1992
  /**
1384
1993
  * Replaces all path elements in the SVG to reflect the current link set.
1385
1994
  * The `<defs>` node (first child) is preserved.
1995
+ *
1996
+ * @param svg - The SVG dependency layer element.
1997
+ * @param links - The array of routed links to render.
1998
+ * @param totalWidth - The total pixel width of the SVG viewport.
1999
+ * @param totalHeight - The total pixel height of the SVG viewport.
2000
+ * @param selectedTaskId - The currently selected task ID, or `null`.
2001
+ * @param highlightLinkedDependenciesOnSelect - When `true`, links connected to the selected task use highlight styling.
2002
+ * @param cbs - Optional callbacks for link click and double-click events.
1386
2003
  */
1387
- function updateDependencyLayer(svg, links, totalWidth, totalHeight, selectedTaskId, highlightLinkedDependenciesOnSelect) {
2004
+ function updateDependencyLayer(svg, links, totalWidth, totalHeight, selectedTaskId, highlightLinkedDependenciesOnSelect, cbs) {
1388
2005
  setAttrs(svg, {
1389
2006
  width: totalWidth,
1390
2007
  height: totalHeight
1391
2008
  });
1392
- const toRemove = [];
1393
- for (let i = 1; i < svg.children.length; i++) {
1394
- const child = svg.children[i];
1395
- if (child !== void 0 && !child.classList.contains("gantt-ghost-line")) toRemove.push(child);
1396
- }
2009
+ const toRemove = [...svg.children].slice(1).filter((c) => !c.classList.contains("gantt-ghost-line"));
1397
2010
  for (const node of toRemove) svg.removeChild(node);
2011
+ const ghost = svg.querySelector("path.gantt-ghost-line");
1398
2012
  for (const link of links) {
1399
2013
  const { points } = link;
1400
2014
  if (points.length === 0) continue;
1401
- let d = `M ${points[0]?.x ?? 0},${points[0]?.y ?? 0}`;
1402
- for (let i = 1; i < points.length; i++) {
1403
- const p = points[i];
1404
- if (p !== void 0) d += ` L ${p.x},${p.y}`;
1405
- }
2015
+ const [first, ...rest] = points;
2016
+ const d = `M ${first.x},${first.y}${rest.map((p) => ` L ${p.x},${p.y}`).join("")}`;
1406
2017
  const isRelated = highlightLinkedDependenciesOnSelect && selectedTaskId !== null && (link.sourceTaskId === selectedTaskId || link.targetTaskId === selectedTaskId);
1407
2018
  const path = document.createElementNS(NS, "path");
1408
2019
  setAttrs(path, {
@@ -1411,19 +2022,74 @@ function updateDependencyLayer(svg, links, totalWidth, totalHeight, selectedTask
1411
2022
  stroke: isRelated ? "var(--gantt-link-hi)" : "var(--gantt-link)",
1412
2023
  "stroke-width": isRelated ? "1.8" : "1.5",
1413
2024
  "stroke-linejoin": "round",
1414
- "marker-end": isRelated ? "url(#gantt-arrow-hi)" : "url(#gantt-arrow)"
2025
+ "marker-end": isRelated ? "url(#gantt-arrow-hi)" : "url(#gantt-arrow)",
2026
+ "data-link-id": String(link.linkId)
2027
+ });
2028
+ path.style.pointerEvents = "visibleStroke";
2029
+ path.style.cursor = "pointer";
2030
+ path.addEventListener("click", (event) => {
2031
+ if (event.detail === 1) cbs?.onLinkClick?.({
2032
+ id: link.linkId,
2033
+ source: link.sourceTaskId,
2034
+ target: link.targetTaskId,
2035
+ type: link.type
2036
+ });
2037
+ });
2038
+ path.addEventListener("dblclick", (_event) => {
2039
+ cbs?.onLinkDblClick?.({
2040
+ id: link.linkId,
2041
+ source: link.sourceTaskId,
2042
+ target: link.targetTaskId,
2043
+ type: link.type
2044
+ });
1415
2045
  });
1416
- svg.append(path);
2046
+ if (ghost !== null) svg.insertBefore(path, ghost);
2047
+ else svg.append(path);
1417
2048
  }
1418
2049
  }
1419
2050
  //#endregion
1420
- //#region src/gantt-chart/vanilla/interaction/drag.ts
2051
+ //#region src/lib/vanilla/interaction/drag.ts
2052
+ function toTask(node) {
2053
+ const base = {
2054
+ id: node.id,
2055
+ text: node.text,
2056
+ startDate: node.startDate,
2057
+ ...node.parent === void 0 ? {} : { parent: node.parent },
2058
+ ...node.color === void 0 ? {} : { color: node.color },
2059
+ ...node.data === void 0 ? {} : { data: node.data }
2060
+ };
2061
+ switch (node.kind) {
2062
+ case "task": return {
2063
+ ...base,
2064
+ kind: "task",
2065
+ durationHours: node.durationHours,
2066
+ percentComplete: node.percentComplete
2067
+ };
2068
+ case "project": return {
2069
+ ...base,
2070
+ kind: "project",
2071
+ durationHours: node.durationHours,
2072
+ percentComplete: node.percentComplete,
2073
+ open: node.open
2074
+ };
2075
+ case "milestone": return {
2076
+ ...base,
2077
+ kind: "milestone"
2078
+ };
2079
+ }
2080
+ }
1421
2081
  /**
1422
2082
  * Attaches drag-to-move and resize listeners to a bar element.
1423
- * Returns a cleanup function that removes all listeners.
1424
2083
  *
1425
2084
  * Design: all mutable state lives in closure variables captured at mousedown.
1426
2085
  * No global state; multiple bars can be dragged independently (one at a time).
2086
+ *
2087
+ * @param barEl - The bar DOM element.
2088
+ * @param resizeHandleEl - The resize handle DOM element.
2089
+ * @param task - The {@link TaskNode} for this bar.
2090
+ * @param getMapper - A function returning the current {@link PixelMapper} (snapshotted at mousedown).
2091
+ * @param cbs - The chart callbacks.
2092
+ * @returns A cleanup function that removes all listeners.
1427
2093
  */
1428
2094
  function attachDrag(barEl, resizeHandleEl, task, getMapper, cbs) {
1429
2095
  function onBarDown(e) {
@@ -1432,22 +2098,27 @@ function attachDrag(barEl, resizeHandleEl, task, getMapper, cbs) {
1432
2098
  try {
1433
2099
  barEl.setPointerCapture(e.pointerId);
1434
2100
  } catch {}
1435
- cbs.onSelect?.(task.id);
2101
+ cbs.onTaskSelect?.(task.id);
1436
2102
  const startX = e.clientX;
1437
- const originDate = parseDate(task.start_date);
2103
+ const originDate = parseDate(task.startDate);
1438
2104
  const mapper = getMapper();
2105
+ let lastHours = 0;
1439
2106
  function onMove(me) {
1440
2107
  const dx = me.clientX - startX;
1441
- const days = Math.round(mapper.widthToDuration(dx));
1442
- cbs.onMove?.({
2108
+ lastHours = Math.round(mapper.widthToDuration(dx));
2109
+ cbs.onTaskMove?.({
1443
2110
  id: task.id,
1444
- startDate: addDays(originDate, days)
2111
+ startDate: addHours(originDate, lastHours)
1445
2112
  });
1446
2113
  }
1447
2114
  function onUp() {
1448
2115
  window.removeEventListener("pointermove", onMove);
1449
2116
  window.removeEventListener("pointerup", onUp);
1450
2117
  barEl.style.cursor = "grab";
2118
+ cbs._onTaskMoveFinal?.({
2119
+ id: task.id,
2120
+ startDate: addHours(originDate, lastHours)
2121
+ });
1451
2122
  }
1452
2123
  barEl.style.cursor = "grabbing";
1453
2124
  window.addEventListener("pointermove", onMove);
@@ -1461,29 +2132,33 @@ function attachDrag(barEl, resizeHandleEl, task, getMapper, cbs) {
1461
2132
  resizeHandleEl.setPointerCapture(e.pointerId);
1462
2133
  } catch {}
1463
2134
  const startX = e.clientX;
1464
- const origDur = task.duration;
2135
+ const origDur = task.kind !== "milestone" ? task.durationHours : 0;
1465
2136
  const mapper = getMapper();
2137
+ let lastDuration = origDur;
1466
2138
  function onMove(me) {
1467
2139
  const dx = me.clientX - startX;
1468
- const daysDelta = Math.round(mapper.widthToDuration(dx));
1469
- cbs.onResize?.({
2140
+ const hoursDelta = Math.round(mapper.widthToDuration(dx));
2141
+ lastDuration = Math.max(1, origDur + hoursDelta);
2142
+ cbs.onTaskResize?.({
1470
2143
  id: task.id,
1471
- duration: Math.max(1, origDur + daysDelta)
2144
+ durationHours: lastDuration
1472
2145
  });
1473
2146
  }
1474
2147
  function onUp() {
1475
2148
  window.removeEventListener("pointermove", onMove);
1476
2149
  window.removeEventListener("pointerup", onUp);
2150
+ cbs._onTaskResizeFinal?.({
2151
+ id: task.id,
2152
+ durationHours: lastDuration
2153
+ });
1477
2154
  }
1478
2155
  window.addEventListener("pointermove", onMove);
1479
2156
  window.addEventListener("pointerup", onUp);
1480
2157
  }
1481
2158
  function onBarClick(event) {
1482
2159
  if (event.detail !== 2) return;
1483
- cbs.onTaskEditIntent?.({
2160
+ cbs.onTaskDoubleClick?.({
1484
2161
  id: task.id,
1485
- source: "bar",
1486
- trigger: "double_click",
1487
2162
  task: toTask(task)
1488
2163
  });
1489
2164
  }
@@ -1497,21 +2172,73 @@ function attachDrag(barEl, resizeHandleEl, task, getMapper, cbs) {
1497
2172
  };
1498
2173
  }
1499
2174
  /**
2175
+ * Attaches drag-to-change-progress listeners to a progress overlay element.
2176
+ *
2177
+ * @param progressEl - The progress overlay DOM element.
2178
+ * @param barEl - The bar DOM element (for width measurement).
2179
+ * @param task - The {@link TaskNode} for this bar.
2180
+ * @param _getMapper - A function returning the current {@link PixelMapper} (unused, kept for API symmetry).
2181
+ * @param cbs - The chart callbacks.
2182
+ * @returns A cleanup function that removes all listeners.
2183
+ */
2184
+ function attachProgressDrag(progressEl, barEl, task, _getMapper, cbs) {
2185
+ function onProgressDown(e) {
2186
+ if (e.button !== 0) return;
2187
+ e.preventDefault();
2188
+ e.stopPropagation();
2189
+ cbs.onTaskSelect?.(task.id);
2190
+ try {
2191
+ progressEl.setPointerCapture(e.pointerId);
2192
+ } catch {}
2193
+ const startX = e.clientX;
2194
+ const barWidth = barEl.getBoundingClientRect().width;
2195
+ const origPercent = task.kind !== "milestone" ? task.percentComplete ?? 0 : 0;
2196
+ let lastPercent = origPercent;
2197
+ function onMove(me) {
2198
+ const dx = me.clientX - startX;
2199
+ const percentDelta = barWidth > 0 ? dx / barWidth * 100 : 0;
2200
+ lastPercent = Math.max(0, Math.min(100, Math.round(origPercent + percentDelta)));
2201
+ cbs.onTaskProgressDrag?.({
2202
+ id: task.id,
2203
+ percentComplete: lastPercent
2204
+ });
2205
+ }
2206
+ function onUp() {
2207
+ window.removeEventListener("pointermove", onMove);
2208
+ window.removeEventListener("pointerup", onUp);
2209
+ progressEl.style.cursor = "ew-resize";
2210
+ cbs._onTaskProgressDragFinal?.({
2211
+ id: task.id,
2212
+ percentComplete: lastPercent
2213
+ });
2214
+ }
2215
+ progressEl.style.cursor = "ew-resize";
2216
+ window.addEventListener("pointermove", onMove);
2217
+ window.addEventListener("pointerup", onUp);
2218
+ }
2219
+ progressEl.addEventListener("pointerdown", onProgressDown);
2220
+ return () => {
2221
+ progressEl.removeEventListener("pointerdown", onProgressDown);
2222
+ };
2223
+ }
2224
+ /**
1500
2225
  * Attaches click-to-select on a milestone diamond.
1501
- * Returns cleanup.
2226
+ *
2227
+ * @param diamondEl - The milestone diamond DOM element.
2228
+ * @param taskId - The task ID to select.
2229
+ * @param cbs - The chart callbacks.
2230
+ * @returns A cleanup function that removes all listeners.
1502
2231
  */
1503
2232
  function attachMilestoneClick(diamondEl, taskId, cbs) {
1504
2233
  function onClick() {
1505
- cbs.onSelect?.(taskId);
2234
+ cbs.onTaskSelect?.(taskId);
1506
2235
  }
1507
2236
  function onDoubleClick(event) {
1508
2237
  if (event.detail === 2) {
1509
2238
  const task = diamondEl.__task;
1510
2239
  if (task === void 0) return;
1511
- cbs.onTaskEditIntent?.({
2240
+ cbs.onTaskDoubleClick?.({
1512
2241
  id: taskId,
1513
- source: "milestone",
1514
- trigger: "double_click",
1515
2242
  task
1516
2243
  });
1517
2244
  }
@@ -1526,24 +2253,19 @@ function attachMilestoneClick(diamondEl, taskId, cbs) {
1526
2253
  function bindMilestoneTask(diamondEl, task) {
1527
2254
  diamondEl.__task = task;
1528
2255
  }
1529
- function toTask(row) {
1530
- return {
1531
- id: row.id,
1532
- text: row.text,
1533
- start_date: row.start_date,
1534
- duration: row.duration,
1535
- progress: row.progress,
1536
- type: row.type,
1537
- open: row.open,
1538
- ...row.parent === void 0 ? {} : { parent: row.parent },
1539
- ...row.color === void 0 ? {} : { color: row.color }
1540
- };
1541
- }
1542
2256
  //#endregion
1543
- //#region src/gantt-chart/vanilla/interaction/linkCreation.ts
2257
+ //#region src/lib/vanilla/interaction/linkCreation.ts
1544
2258
  /**
1545
2259
  * Attaches a link-creation drag listener to an endpoint handle.
1546
- * Returns a cleanup function that removes all listeners.
2260
+ *
2261
+ * @param handle - The endpoint DOM element.
2262
+ * @param sourceTaskId - The task ID from which the link originates.
2263
+ * @param anchorX - The X anchor coordinate (task center).
2264
+ * @param anchorY - The Y anchor coordinate (task center).
2265
+ * @param svgLayer - The SVG dependency layer element for ghost line rendering.
2266
+ * @param absoluteLayer - The absolute-positioned layer for coordinate calculations.
2267
+ * @param cbs - The chart callbacks.
2268
+ * @returns A cleanup function that removes all listeners.
1547
2269
  */
1548
2270
  function attachLinkEndpointHandle(handle, sourceTaskId, anchorX, anchorY, svgLayer, absoluteLayer, cbs) {
1549
2271
  function onPointerDown(e) {
@@ -1592,6 +2314,8 @@ function attachLinkEndpointHandle(handle, sourceTaskId, anchorX, anchorY, svgLay
1592
2314
  /**
1593
2315
  * Creates an endpoint handle DOM element.
1594
2316
  * The caller must position it with inline styles and append it to the layer.
2317
+ *
2318
+ * @returns A new `HTMLElement` with the `gantt-link-endpoint` class.
1595
2319
  */
1596
2320
  function createEndpointHandle() {
1597
2321
  const handle = document.createElement("div");
@@ -1612,13 +2336,17 @@ function createEndpointHandle() {
1612
2336
  return handle;
1613
2337
  }
1614
2338
  //#endregion
1615
- //#region src/gantt-chart/vanilla/dom/rightPane.ts
2339
+ //#region src/lib/vanilla/dom/rightPane.ts
1616
2340
  const BAR_COLOR = {
1617
2341
  task: "var(--gantt-task)",
1618
2342
  project: "var(--gantt-project)",
1619
2343
  milestone: "var(--gantt-milestone)"
1620
2344
  };
1621
- /** Creates the skeleton DOM structure for the right pane. Call once. */
2345
+ /**
2346
+ * Creates the skeleton DOM structure for the right pane. Call once.
2347
+ *
2348
+ * @returns A new {@link RightPaneRefs} with empty containers and bar registry.
2349
+ */
1622
2350
  function createRightPaneRefs() {
1623
2351
  const scrollContainer = el("div");
1624
2352
  const stripeContainer = el("div");
@@ -1627,7 +2355,7 @@ function createRightPaneRefs() {
1627
2355
  css(stripeContainer, { position: "relative" });
1628
2356
  css(absoluteLayer, {
1629
2357
  position: "absolute",
1630
- top: "0",
2358
+ top: "52px",
1631
2359
  left: "0"
1632
2360
  });
1633
2361
  scrollContainer.append(stripeContainer);
@@ -1641,94 +2369,8 @@ function createRightPaneRefs() {
1641
2369
  barRegistry: /* @__PURE__ */ new Map()
1642
2370
  };
1643
2371
  }
1644
- /**
1645
- * Full render of the right pane.
1646
- * Grid lines and stripes are rebuilt each call (cheap — no event listeners).
1647
- * Bars are rebuilt each call with fresh drag listeners (old ones cleaned up first).
1648
- */
1649
- function renderRightPane(refs, state, cbs) {
1650
- const { allRows, layouts, links, mapper, scale, viewportStart, viewportEnd, totalWidth, selectedId, highlightLinkedDependenciesOnSelect, paddingTop, paddingBottom, startIndex } = state;
1651
- const { stripeContainer, absoluteLayer, svgLayer, barRegistry } = refs;
1652
- const rowCount = allRows.length;
1653
- const contentHeight = totalContentHeight(rowCount);
1654
- const visibleRows = allRows.slice(state.startIndex, state.endIndex + 1);
1655
- clearChildren(stripeContainer);
1656
- css(stripeContainer, { width: `${totalWidth}px` });
1657
- if (paddingTop > 0) {
1658
- const s = el("div");
1659
- s.style.height = `${paddingTop}px`;
1660
- stripeContainer.append(s);
1661
- }
1662
- for (let i = 0; i < visibleRows.length; i++) {
1663
- const rowIdx = startIndex + i;
1664
- const stripe = el("div");
1665
- css(stripe, {
1666
- height: `${ROW_HEIGHT}px`,
1667
- background: rowIdx % 2 === 0 ? "var(--gantt-bg)" : "var(--gantt-stripe)",
1668
- borderBottom: "1px solid var(--gantt-border)"
1669
- });
1670
- stripeContainer.append(stripe);
1671
- }
1672
- if (paddingBottom > 0) {
1673
- const s = el("div");
1674
- s.style.height = `${paddingBottom}px`;
1675
- stripeContainer.append(s);
1676
- }
1677
- css(absoluteLayer, {
1678
- width: `${totalWidth}px`,
1679
- height: `${contentHeight}px`
1680
- });
1681
- const toRemove = [];
1682
- for (const child of [...absoluteLayer.children]) if (child !== svgLayer) toRemove.push(child);
1683
- for (const node of toRemove) absoluteLayer.removeChild(node);
1684
- hideGhostLine(svgLayer);
1685
- for (const { cleanupDrag, cleanupLinkHandles } of barRegistry.values()) {
1686
- cleanupDrag();
1687
- cleanupLinkHandles?.();
1688
- }
1689
- barRegistry.clear();
1690
- if (scale === "day") renderSpecialDayBackgrounds(absoluteLayer, svgLayer, state, contentHeight);
1691
- let gridCur = snapToScaleBoundary(viewportStart, scale);
1692
- while (gridCur <= viewportEnd) {
1693
- const x = mapper.toX(gridCur);
1694
- const line = el("div");
1695
- css(line, {
1696
- position: "absolute",
1697
- left: `${x}px`,
1698
- top: "0",
1699
- width: "1px",
1700
- height: `${contentHeight}px`,
1701
- background: "var(--gantt-grid-line)",
1702
- pointerEvents: "none"
1703
- });
1704
- absoluteLayer.insertBefore(line, svgLayer);
1705
- gridCur = nextScaleBoundary(gridCur, scale);
1706
- }
1707
- const todayX = mapper.toX(/* @__PURE__ */ new Date());
1708
- const todayLineWidth = 2;
1709
- if (todayX >= 0 && todayX <= totalWidth - todayLineWidth) {
1710
- const todayLine = el("div");
1711
- todayLine.className = "gantt-today-marker";
1712
- css(todayLine, {
1713
- position: "absolute",
1714
- left: `${todayX}px`,
1715
- top: "0",
1716
- width: `${todayLineWidth}px`,
1717
- height: `${contentHeight}px`,
1718
- background: "var(--gantt-today)",
1719
- pointerEvents: "none",
1720
- zIndex: "5"
1721
- });
1722
- absoluteLayer.insertBefore(todayLine, svgLayer);
1723
- }
1724
- const visibleTaskIds = new Set(visibleRows.map((task) => task.id));
1725
- for (const task of visibleRows) {
1726
- const layout = layouts.get(task.id);
1727
- if (layout === void 0) continue;
1728
- if (layout.type === "milestone") renderMilestone(absoluteLayer, svgLayer, task, layout, selectedId, barRegistry, cbs, state);
1729
- else renderBar(absoluteLayer, svgLayer, task, layout, selectedId, barRegistry, state, cbs);
1730
- }
1731
- updateDependencyLayer(svgLayer, links.filter((link) => visibleTaskIds.has(link.sourceTaskId) && visibleTaskIds.has(link.targetTaskId)), totalWidth, contentHeight, selectedId, highlightLinkedDependenciesOnSelect);
2372
+ function ariaLabel(locale, key, arg) {
2373
+ return formatLabel(locale.labels?.[key] ?? EN_US_LABELS[key], arg);
1732
2374
  }
1733
2375
  function renderSpecialDayBackgrounds(layer, beforeNode, state, contentHeight) {
1734
2376
  const { mapper, viewportStart, viewportEnd, showWeekends, weekendDays, specialDaysByDate } = state;
@@ -1770,7 +2412,7 @@ function renderSpecialDayBackgrounds(layer, beforeNode, state, contentHeight) {
1770
2412
  }
1771
2413
  function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, cbs) {
1772
2414
  const selected = task.id === selectedId;
1773
- const color = BAR_COLOR[layout.type] ?? BAR_COLOR["task"];
2415
+ const color = BAR_COLOR[layout.kind] ?? BAR_COLOR["task"];
1774
2416
  const bar = el("div");
1775
2417
  bar.className = `gantt-bar${selected ? " gantt-bar--selected gantt-shape--selected" : ""}`;
1776
2418
  css(bar, {
@@ -1780,15 +2422,17 @@ function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, c
1780
2422
  width: `${layout.width}px`,
1781
2423
  height: `${layout.height}px`,
1782
2424
  ...color === void 0 ? {} : { background: color },
1783
- borderRadius: layout.type === "project" ? "3px" : "4px",
2425
+ borderRadius: layout.kind === "project" ? "3px" : "4px",
1784
2426
  cursor: "grab",
1785
2427
  userSelect: "none",
1786
2428
  overflow: "hidden",
1787
2429
  zIndex: selected ? "3" : "2",
1788
2430
  touchAction: "none"
1789
2431
  });
2432
+ let cleanupProgressDrag;
1790
2433
  if (layout.progressWidth > 0) {
1791
2434
  const prog = el("div");
2435
+ const progressEnabled = state.progressDragEnabled;
1792
2436
  css(prog, {
1793
2437
  position: "absolute",
1794
2438
  left: "0",
@@ -1796,8 +2440,15 @@ function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, c
1796
2440
  width: `${layout.progressWidth}px`,
1797
2441
  height: "100%",
1798
2442
  background: "rgba(0,0,0,0.18)",
1799
- pointerEvents: "none"
2443
+ ...progressEnabled ? {
2444
+ cursor: "ew-resize",
2445
+ touchAction: "none"
2446
+ } : { pointerEvents: "none" }
1800
2447
  });
2448
+ if (progressEnabled) {
2449
+ prog.className = "gantt-progress-overlay";
2450
+ cleanupProgressDrag = attachProgressDrag(prog, bar, task, () => state.mapper, cbs);
2451
+ }
1801
2452
  bar.append(prog);
1802
2453
  }
1803
2454
  const label = el("span");
@@ -1820,16 +2471,16 @@ function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, c
1820
2471
  bar.append(label);
1821
2472
  bar.tabIndex = 0;
1822
2473
  bar.setAttribute("role", "button");
1823
- bar.setAttribute("aria-label", ariaLabel(state.locale, "aria_task", task.text));
2474
+ bar.setAttribute("aria-label", ariaLabel(state.locale, "ariaTask", task.text));
1824
2475
  bar.setAttribute("aria-pressed", String(selected));
1825
2476
  bar.dataset["taskId"] = String(task.id);
1826
2477
  bar.addEventListener("click", () => {
1827
- cbs.onSelect?.(task.id);
2478
+ cbs.onTaskSelect?.(task.id);
1828
2479
  });
1829
2480
  bar.addEventListener("keydown", (event) => {
1830
2481
  if (event.key === "Enter" || event.key === " ") {
1831
2482
  event.preventDefault();
1832
- cbs.onSelect?.(task.id);
2483
+ cbs.onTaskSelect?.(task.id);
1833
2484
  }
1834
2485
  });
1835
2486
  const handle = el("div");
@@ -1887,6 +2538,7 @@ function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, c
1887
2538
  cleanupDrag
1888
2539
  };
1889
2540
  if (cleanupLinkHandles !== void 0) entry.cleanupLinkHandles = cleanupLinkHandles;
2541
+ if (cleanupProgressDrag !== void 0) entry.cleanupProgressDrag = cleanupProgressDrag;
1890
2542
  registry.set(task.id, entry);
1891
2543
  }
1892
2544
  function renderMilestone(layer, svgLayer, task, layout, selectedId, registry, cbs, state) {
@@ -1907,13 +2559,13 @@ function renderMilestone(layer, svgLayer, task, layout, selectedId, registry, cb
1907
2559
  });
1908
2560
  diamond.tabIndex = 0;
1909
2561
  diamond.setAttribute("role", "button");
1910
- diamond.setAttribute("aria-label", ariaLabel(state.locale, "aria_milestone", task.text));
2562
+ diamond.setAttribute("aria-label", ariaLabel(state.locale, "ariaMilestone", task.text));
1911
2563
  diamond.setAttribute("aria-pressed", String(selected));
1912
2564
  diamond.dataset["taskId"] = String(task.id);
1913
2565
  diamond.addEventListener("keydown", (event) => {
1914
2566
  if (event.key === "Enter" || event.key === " ") {
1915
2567
  event.preventDefault();
1916
- cbs.onSelect?.(task.id);
2568
+ cbs.onTaskSelect?.(task.id);
1917
2569
  }
1918
2570
  });
1919
2571
  const labelEl = el("span");
@@ -1967,11 +2619,102 @@ function renderMilestone(layer, svgLayer, task, layout, selectedId, registry, cb
1967
2619
  if (cleanupLinkHandles !== void 0) entry.cleanupLinkHandles = cleanupLinkHandles;
1968
2620
  registry.set(task.id, entry);
1969
2621
  }
1970
- function ariaLabel(locale, key, arg) {
1971
- return formatLabel(locale.labels?.[key] ?? EN_US_LABELS[key], arg);
2622
+ /**
2623
+ * Full render of the right pane.
2624
+ * Grid lines and stripes are rebuilt each call (cheap — no event listeners).
2625
+ * Bars are rebuilt each call with fresh drag listeners (old ones cleaned up first).
2626
+ *
2627
+ * @param refs - The right pane DOM references.
2628
+ * @param state - The current chart state.
2629
+ * @param cbs - The chart callbacks.
2630
+ */
2631
+ function renderRightPane(refs, state, cbs) {
2632
+ const { allRows, layouts, links, mapper, scale, viewportStart, viewportEnd, totalWidth, selectedId, highlightLinkedDependenciesOnSelect, paddingTop, paddingBottom, startIndex } = state;
2633
+ const { stripeContainer, absoluteLayer, svgLayer, barRegistry } = refs;
2634
+ const rowCount = allRows.length;
2635
+ const contentHeight = totalContentHeight(rowCount);
2636
+ const visibleRows = allRows.slice(state.startIndex, state.endIndex + 1);
2637
+ clearChildren(stripeContainer);
2638
+ css(stripeContainer, { width: `${totalWidth}px` });
2639
+ if (paddingTop > 0) {
2640
+ const s = el("div");
2641
+ s.style.height = `${paddingTop}px`;
2642
+ stripeContainer.append(s);
2643
+ }
2644
+ for (let i = 0; i < visibleRows.length; i++) {
2645
+ const rowIdx = startIndex + i;
2646
+ const stripe = el("div");
2647
+ css(stripe, {
2648
+ height: `${ROW_HEIGHT}px`,
2649
+ background: rowIdx % 2 === 0 ? "var(--gantt-bg)" : "var(--gantt-stripe)",
2650
+ borderBottom: "1px solid var(--gantt-border)"
2651
+ });
2652
+ stripeContainer.append(stripe);
2653
+ }
2654
+ if (paddingBottom > 0) {
2655
+ const s = el("div");
2656
+ s.style.height = `${paddingBottom}px`;
2657
+ stripeContainer.append(s);
2658
+ }
2659
+ css(absoluteLayer, {
2660
+ width: `${totalWidth}px`,
2661
+ height: `${contentHeight}px`
2662
+ });
2663
+ const toRemove = [];
2664
+ for (const child of [...absoluteLayer.children]) if (child !== svgLayer) toRemove.push(child);
2665
+ for (const node of toRemove) absoluteLayer.removeChild(node);
2666
+ hideGhostLine(svgLayer);
2667
+ for (const { cleanupDrag, cleanupLinkHandles, cleanupProgressDrag } of barRegistry.values()) {
2668
+ cleanupDrag();
2669
+ cleanupLinkHandles?.();
2670
+ cleanupProgressDrag?.();
2671
+ }
2672
+ barRegistry.clear();
2673
+ if (scale === "day") renderSpecialDayBackgrounds(absoluteLayer, svgLayer, state, contentHeight);
2674
+ let gridCur = snapToScaleBoundary(viewportStart, scale);
2675
+ while (gridCur <= viewportEnd) {
2676
+ const x = mapper.toX(gridCur);
2677
+ const line = el("div");
2678
+ css(line, {
2679
+ position: "absolute",
2680
+ left: `${x}px`,
2681
+ top: "0",
2682
+ width: "1px",
2683
+ height: `${contentHeight}px`,
2684
+ background: "var(--gantt-grid-line)",
2685
+ pointerEvents: "none"
2686
+ });
2687
+ absoluteLayer.insertBefore(line, svgLayer);
2688
+ gridCur = nextScaleBoundary(gridCur, scale);
2689
+ }
2690
+ const todayX = mapper.toX(/* @__PURE__ */ new Date());
2691
+ const todayLineWidth = 2;
2692
+ if (todayX >= 0 && todayX <= totalWidth - todayLineWidth) {
2693
+ const todayLine = el("div");
2694
+ todayLine.className = "gantt-today-marker";
2695
+ css(todayLine, {
2696
+ position: "absolute",
2697
+ left: `${todayX}px`,
2698
+ top: "0",
2699
+ width: `${todayLineWidth}px`,
2700
+ height: `${contentHeight}px`,
2701
+ background: "var(--gantt-today)",
2702
+ pointerEvents: "none",
2703
+ zIndex: "5"
2704
+ });
2705
+ absoluteLayer.insertBefore(todayLine, svgLayer);
2706
+ }
2707
+ const visibleTaskIds = new Set(visibleRows.map((task) => task.id));
2708
+ for (const task of visibleRows) {
2709
+ const layout = layouts.get(task.id);
2710
+ if (layout === void 0) continue;
2711
+ if (layout.kind === "milestone") renderMilestone(absoluteLayer, svgLayer, task, layout, selectedId, barRegistry, cbs, state);
2712
+ else renderBar(absoluteLayer, svgLayer, task, layout, selectedId, barRegistry, state, cbs);
2713
+ }
2714
+ updateDependencyLayer(svgLayer, links.filter((link) => visibleTaskIds.has(link.sourceTaskId) && visibleTaskIds.has(link.targetTaskId)), totalWidth, contentHeight, selectedId, highlightLinkedDependenciesOnSelect, cbs);
1972
2715
  }
1973
2716
  //#endregion
1974
- //#region src/gantt-chart/vanilla/utils.ts
2717
+ //#region src/lib/vanilla/utils.ts
1975
2718
  function buildTaskIndex(tasks) {
1976
2719
  const index = /* @__PURE__ */ new Map();
1977
2720
  for (let i = 0; i < tasks.length; i++) {
@@ -1980,6 +2723,9 @@ function buildTaskIndex(tasks) {
1980
2723
  }
1981
2724
  return index;
1982
2725
  }
2726
+ function toIsoDate(date) {
2727
+ return date.toISOString().slice(0, 10);
2728
+ }
1983
2729
  function buildSpecialDayIndex(specialDays) {
1984
2730
  const map = /* @__PURE__ */ new Map();
1985
2731
  for (const specialDay of specialDays) {
@@ -1993,9 +2739,6 @@ function buildSpecialDayIndex(specialDays) {
1993
2739
  }
1994
2740
  return map;
1995
2741
  }
1996
- function toIsoDate(date) {
1997
- return date.toISOString().slice(0, 10);
1998
- }
1999
2742
  function normalizeWeekendDays(days) {
2000
2743
  if (days === void 0) return new Set([0, 6]);
2001
2744
  const normalized = /* @__PURE__ */ new Set();
@@ -2020,11 +2763,11 @@ function getExpandableTaskIds(tasks) {
2020
2763
  function getInitialExpandedIds(tasks) {
2021
2764
  const expandableIds = getExpandableTaskIds(tasks);
2022
2765
  const expandedIds = /* @__PURE__ */ new Set();
2023
- for (const task of tasks) if (task.open && expandableIds.has(task.id)) expandedIds.add(task.id);
2766
+ for (const task of tasks) if (task.kind === "project" && task.open && expandableIds.has(task.id)) expandedIds.add(task.id);
2024
2767
  return expandedIds;
2025
2768
  }
2026
2769
  //#endregion
2027
- //#region src/gantt-chart/vanilla/splitter.ts
2770
+ //#region src/lib/vanilla/splitter.ts
2028
2771
  const MIN_PANE_WIDTH$1 = 96;
2029
2772
  function attachSplitter(splitterHandle, leftPane, container, timelineMinWidth, onDragEnd) {
2030
2773
  splitterHandle.addEventListener("pointerdown", (e) => {
@@ -2073,19 +2816,34 @@ function computeLeftPaneWidth(options) {
2073
2816
  return Math.max(MIN_PANE_WIDTH, Math.floor(width));
2074
2817
  }
2075
2818
  //#endregion
2076
- //#region src/gantt-chart/vanilla/gantt-chart.ts
2819
+ //#region src/lib/vanilla/gantt-chart.ts
2077
2820
  const HEADER_H = 52;
2078
2821
  const OVERSCAN = 4;
2822
+ /**
2823
+ * Progressive-enhancement Gantt chart component.
2824
+ * Validates input, builds a DOM tree, and renders a full interactive chart
2825
+ * inside the given container element.
2826
+ *
2827
+ * @example
2828
+ * ```ts
2829
+ * const chart = new GanttChart(document.getElementById('chart')!, input, {
2830
+ * locale: 'de-DE',
2831
+ * theme: 'dark',
2832
+ * });
2833
+ * ```
2834
+ */
2079
2835
  var GanttChart = class {
2080
2836
  #container;
2081
2837
  #opts;
2082
- #input;
2838
+ #callbacks;
2839
+ #input = null;
2083
2840
  #scale;
2084
2841
  #selectedId = null;
2085
2842
  #scrollTop = 0;
2086
2843
  #rafPending = false;
2087
2844
  #rafId = null;
2088
2845
  #destroyed = false;
2846
+ #dragOriginals = /* @__PURE__ */ new Map();
2089
2847
  #taskIndex;
2090
2848
  #lastGridClick = null;
2091
2849
  #userSplitWidth = null;
@@ -2100,6 +2858,7 @@ var GanttChart = class {
2100
2858
  #root;
2101
2859
  #scrollEl;
2102
2860
  #leftPane;
2861
+ #leftHeader;
2103
2862
  #leftBody;
2104
2863
  #rightPane;
2105
2864
  #rightHeader;
@@ -2107,14 +2866,19 @@ var GanttChart = class {
2107
2866
  #cbs;
2108
2867
  #resizeObserver = null;
2109
2868
  #columnResizeCleanup;
2110
- constructor(container, input, opts = {}) {
2869
+ /**
2870
+ * Constructs a new chart, builds the DOM, and wires internal event handling.
2871
+ * Data must be loaded via {@link update} before the chart renders.
2872
+ *
2873
+ * @param container - The host `HTMLElement` the chart will be appended to.
2874
+ * @param opts - Configuration and callback options.
2875
+ */
2876
+ constructor(container, opts = {}, cbs = {}) {
2111
2877
  this.#container = container;
2112
- validateLinkRefs(input.tasks, input.links);
2113
- detectCycles(input.tasks, input.links);
2114
- this.#input = input;
2115
2878
  this.#scale = opts.scale ?? "day";
2116
2879
  this.#opts = opts;
2117
- this.#taskIndex = buildTaskIndex(input.tasks);
2880
+ this.#callbacks = cbs;
2881
+ this.#taskIndex = /* @__PURE__ */ new Map();
2118
2882
  this.#locale = resolveChartLocale(opts.locale);
2119
2883
  this.#columns = opts.gridColumns ?? gridColumnDefaults(this.#locale);
2120
2884
  this.#leftPaneDefaultWidth = opts.leftPaneWidth ?? gridNaturalWidth(this.#columns);
@@ -2122,43 +2886,117 @@ var GanttChart = class {
2122
2886
  this.#timelineMinWidth = opts.timelineMinWidth ?? 220;
2123
2887
  this.#weekendDays = normalizeWeekendDays(opts.weekendDays ?? this.#locale.weekendDays);
2124
2888
  this.#specialDaysByDate = buildSpecialDayIndex(opts.specialDays ?? []);
2125
- this.#expandedIds = getInitialExpandedIds(input.tasks);
2889
+ this.#expandedIds = /* @__PURE__ */ new Set();
2126
2890
  this.#cbs = {
2127
- onSelect: (id) => {
2891
+ onTaskSelect: (id) => {
2128
2892
  if (this.#selectedId === id) return;
2129
2893
  this.#selectedId = id;
2130
- opts.onSelect?.(this.#selectedId);
2894
+ if (this.#selectedId !== null) {
2895
+ const task = this.#findTask(this.#selectedId);
2896
+ if (task !== void 0) this.#callbacks.onTaskSelect?.({ task });
2897
+ }
2131
2898
  this.#scheduleRender();
2132
2899
  },
2133
2900
  onTaskDoubleClick: (payload) => {
2134
- opts.onTaskDoubleClick?.(payload);
2901
+ this.#callbacks.onTaskDoubleClick?.({ task: payload.task });
2135
2902
  },
2136
2903
  onTaskEditIntent: (payload) => {
2137
- opts.onTaskEditIntent?.(payload);
2138
- opts.onTaskDoubleClick?.({
2139
- id: payload.id,
2140
- source: payload.source
2141
- });
2904
+ this.#callbacks.onTaskDoubleClick?.({ task: payload.task });
2142
2905
  },
2143
- onMove: (payload) => {
2906
+ onTaskMove: (payload) => {
2907
+ if (!this.#dragOriginals.has(payload.id)) {
2908
+ const task = this.#input?.tasks.find((t) => t.id === payload.id);
2909
+ if (task !== void 0) this.#dragOriginals.set(payload.id, task);
2910
+ }
2144
2911
  const iso = payload.startDate.toISOString().slice(0, 10);
2145
- this.#patchTask(payload.id, { start_date: iso });
2146
- opts.onMove?.(payload);
2912
+ this.#patchTask(payload.id, { startDate: iso });
2913
+ this.#scheduleRender();
2914
+ },
2915
+ _onTaskMoveFinal: (payload) => {
2916
+ const task = this.#findTask(payload.id);
2917
+ if (task !== void 0) {
2918
+ if (this.#callbacks.onTaskMove?.({
2919
+ task,
2920
+ newStartDate: payload.startDate
2921
+ }) === false) {
2922
+ const original = this.#dragOriginals.get(payload.id);
2923
+ if (original !== void 0) this.#patchTask(payload.id, { startDate: original.startDate });
2924
+ }
2925
+ }
2926
+ this.#dragOriginals.clear();
2927
+ this.#scheduleRender();
2928
+ return true;
2929
+ },
2930
+ onTaskResize: (payload) => {
2931
+ if (!this.#dragOriginals.has(payload.id)) {
2932
+ const task = this.#input?.tasks.find((t) => t.id === payload.id);
2933
+ if (task !== void 0) this.#dragOriginals.set(payload.id, task);
2934
+ }
2935
+ this.#patchTask(payload.id, { durationHours: payload.durationHours });
2147
2936
  this.#scheduleRender();
2148
2937
  },
2149
- onResize: (payload) => {
2150
- this.#patchTask(payload.id, { duration: payload.duration });
2151
- opts.onResize?.(payload);
2938
+ _onTaskResizeFinal: (payload) => {
2939
+ const task = this.#findTask(payload.id);
2940
+ if (task !== void 0) {
2941
+ if (this.#callbacks.onTaskResize?.({
2942
+ task,
2943
+ newDurationHours: payload.durationHours
2944
+ }) === false) {
2945
+ const original = this.#dragOriginals.get(payload.id);
2946
+ if (original !== void 0 && original.kind !== "milestone") this.#patchTask(payload.id, { durationHours: original.durationHours });
2947
+ }
2948
+ }
2949
+ this.#dragOriginals.clear();
2950
+ this.#scheduleRender();
2951
+ return true;
2952
+ },
2953
+ onTaskProgressDrag: (payload) => {
2954
+ if (!this.#dragOriginals.has(payload.id)) {
2955
+ const task = this.#input?.tasks.find((t) => t.id === payload.id);
2956
+ if (task !== void 0) this.#dragOriginals.set(payload.id, task);
2957
+ }
2958
+ this.#patchTask(payload.id, { percentComplete: payload.percentComplete });
2959
+ this.#scheduleRender();
2960
+ },
2961
+ _onTaskProgressDragFinal: (payload) => {
2962
+ const task = this.#findTask(payload.id);
2963
+ if (task !== void 0) {
2964
+ if (this.#callbacks.onProgressChange?.({
2965
+ task,
2966
+ newPercentComplete: payload.percentComplete
2967
+ }) === false) {
2968
+ const original = this.#dragOriginals.get(payload.id);
2969
+ if (original !== void 0 && original.kind !== "milestone") this.#patchTask(payload.id, { percentComplete: original.percentComplete });
2970
+ }
2971
+ }
2972
+ this.#dragOriginals.clear();
2152
2973
  this.#scheduleRender();
2974
+ return true;
2975
+ },
2976
+ onTaskAdd: (parentId) => {
2977
+ const parentTask = this.#findTask(parentId);
2978
+ if (parentTask !== void 0) this.#callbacks.onTaskAdd?.({ parentTask });
2153
2979
  },
2154
2980
  onLeftPaneWidthChange: (width) => {
2155
- opts.onLeftPaneWidthChange?.(width);
2981
+ this.#callbacks.onLeftPaneWidthChange?.(width);
2156
2982
  },
2157
2983
  onGridColumnsChange: (updatedColumns) => {
2158
- opts.onGridColumnsChange?.(updatedColumns);
2984
+ this.#callbacks.onGridColumnsChange?.(updatedColumns);
2159
2985
  },
2160
2986
  onLinkCreate: (payload) => {
2161
- opts.onLinkCreate?.(payload);
2987
+ const sourceTask = this.#findTask(payload.sourceTaskId);
2988
+ const targetTask = this.#findTask(payload.targetTaskId);
2989
+ if (sourceTask !== void 0 && targetTask !== void 0) this.#callbacks.onLinkCreate?.({
2990
+ type: "FS",
2991
+ sourceTask,
2992
+ targetTask
2993
+ });
2994
+ },
2995
+ onLinkClick: (payload) => {
2996
+ this.#callbacks.onLinkClick?.({ link: payload });
2997
+ },
2998
+ onLinkDblClick: (payload) => {
2999
+ this.#callbacks.onLinkDblClick?.({ link: payload });
2162
3000
  }
2163
3001
  };
2164
3002
  this.#buildDom();
@@ -2167,25 +3005,96 @@ var GanttChart = class {
2167
3005
  this.#applyTheme();
2168
3006
  this.#applyResponsivePaneStyles();
2169
3007
  this.#setupResizeObserver();
2170
- this.#render();
2171
3008
  }
3009
+ /**
3010
+ * Replaces the full dataset and re-renders.
3011
+ *
3012
+ * @param newInput - The new {@link GanttInput} to apply.
3013
+ * @throws {GanttError} When the instance has been destroyed.
3014
+ */
2172
3015
  update(newInput) {
2173
3016
  this.#assertAlive();
2174
3017
  validateLinkRefs(newInput.tasks, newInput.links);
2175
3018
  detectCycles(newInput.tasks, newInput.links);
2176
- this.#input = newInput;
2177
- this.#taskIndex = buildTaskIndex(newInput.tasks);
2178
- this.#scheduleRender();
3019
+ this.#input = structuredClone(newInput);
3020
+ this.#taskIndex = buildTaskIndex(this.#input.tasks);
3021
+ this.#expandedIds = getInitialExpandedIds(this.#input.tasks);
3022
+ if (this.#rafPending && this.#rafId !== null) {
3023
+ cancelAnimationFrame(this.#rafId);
3024
+ this.#rafId = null;
3025
+ this.#rafPending = false;
3026
+ }
3027
+ this.#render();
2179
3028
  }
2180
- setScale(scale) {
3029
+ /**
3030
+ * Merges the supplied options into the current configuration and re-renders
3031
+ * only the panes affected by the changed options.
3032
+ *
3033
+ * @param opts - A partial {@link GanttOptions} object. Only the keys present
3034
+ * in this parameter are updated; missing keys keep their
3035
+ * previous values.
3036
+ * @throws {GanttError} When the instance has been destroyed.
3037
+ */
3038
+ setOptions(opts) {
2181
3039
  this.#assertAlive();
2182
- this.#scale = scale;
2183
- this.#scheduleRender();
2184
- }
3040
+ Object.assign(this.#opts, opts);
3041
+ this.#scale = this.#opts.scale ?? "day";
3042
+ let columnsChanged = false;
3043
+ if (opts.locale !== void 0) {
3044
+ this.#locale = resolveChartLocale(opts.locale);
3045
+ if (this.#opts.gridColumns === void 0) {
3046
+ this.#columns = gridColumnDefaults(this.#locale);
3047
+ this.#leftPaneDefaultWidth = gridNaturalWidth(this.#columns);
3048
+ columnsChanged = true;
3049
+ }
3050
+ if (this.#opts.weekendDays === void 0) this.#weekendDays = normalizeWeekendDays(this.#locale.weekendDays);
3051
+ }
3052
+ if (opts.gridColumns !== void 0) {
3053
+ this.#columns = opts.gridColumns;
3054
+ this.#leftPaneDefaultWidth = this.#opts.leftPaneWidth ?? gridNaturalWidth(this.#columns);
3055
+ columnsChanged = true;
3056
+ }
3057
+ if (columnsChanged && this.#input !== null) this.#rebuildLeftPaneHeader();
3058
+ if (opts.leftPaneWidth !== void 0) this.#leftPaneDefaultWidth = opts.leftPaneWidth;
3059
+ if (opts.height !== void 0) {
3060
+ this.#height = opts.height;
3061
+ this.#root.style.height = `${this.#height}px`;
3062
+ }
3063
+ if (opts.timelineMinWidth !== void 0) {
3064
+ this.#timelineMinWidth = opts.timelineMinWidth;
3065
+ this.#rightPane.style.minWidth = `${this.#timelineMinWidth}px`;
3066
+ }
3067
+ if (opts.weekendDays !== void 0) this.#weekendDays = normalizeWeekendDays(opts.weekendDays);
3068
+ if (opts.specialDays !== void 0) this.#specialDaysByDate = buildSpecialDayIndex(opts.specialDays);
3069
+ if (opts.theme !== void 0) this.#applyTheme();
3070
+ const hasLayoutChange = opts.leftPaneWidth !== void 0 || opts.responsiveSplitPane !== void 0 || opts.mobileBreakpoint !== void 0 || opts.mobileLeftPaneMinWidth !== void 0 || opts.mobileLeftPaneMaxRatio !== void 0 || opts.timelineMinWidth !== void 0;
3071
+ if (hasLayoutChange) this.#applyResponsivePaneStyles();
3072
+ const hasLeftPaneChange = columnsChanged || opts.locale !== void 0;
3073
+ const hasRightPaneChange = opts.scale !== void 0 || opts.showWeekends !== void 0 || opts.weekendDays !== void 0 || opts.specialDays !== void 0 || opts.highlightLinkedDependenciesOnSelect !== void 0 || opts.linkCreationEnabled !== void 0 || opts.progressDragEnabled !== void 0 || opts.viewportStart !== void 0 || opts.viewportEnd !== void 0 || opts.locale !== void 0 || opts.timelineMinWidth !== void 0;
3074
+ if (!(hasLeftPaneChange || hasRightPaneChange || hasLayoutChange)) return;
3075
+ if (this.#rafPending && this.#rafId !== null) {
3076
+ cancelAnimationFrame(this.#rafId);
3077
+ this.#rafId = null;
3078
+ this.#rafPending = false;
3079
+ }
3080
+ if (hasLeftPaneChange && !hasRightPaneChange) this.#renderGrid();
3081
+ else if (!hasLeftPaneChange && hasRightPaneChange) this.#renderTimeline();
3082
+ else this.#render();
3083
+ }
3084
+ /**
3085
+ * Programmatically selects or deselects a task.
3086
+ *
3087
+ * @param id - The task ID to select, or `null` to clear the selection.
3088
+ * @throws {GanttError} When the instance has been destroyed.
3089
+ */
2185
3090
  select(id) {
2186
3091
  this.#assertAlive();
2187
- this.#selectedId = id;
2188
- this.#opts.onSelect?.(id);
3092
+ if (id === null) this.#selectedId = null;
3093
+ else {
3094
+ const task = this.#input?.tasks.find((t) => t.id === id);
3095
+ if (task !== void 0) this.#callbacks.onTaskSelect?.({ task });
3096
+ this.#selectedId = id;
3097
+ }
2189
3098
  if (this.#rafPending && this.#rafId !== null) {
2190
3099
  cancelAnimationFrame(this.#rafId);
2191
3100
  this.#rafId = null;
@@ -2193,6 +3102,11 @@ var GanttChart = class {
2193
3102
  }
2194
3103
  this.#render();
2195
3104
  }
3105
+ /**
3106
+ * Collapses all expandable groups in the task tree.
3107
+ *
3108
+ * @throws {GanttError} When the instance has been destroyed.
3109
+ */
2196
3110
  collapseAll() {
2197
3111
  this.#assertAlive();
2198
3112
  this.#expandedIds.clear();
@@ -2203,10 +3117,15 @@ var GanttChart = class {
2203
3117
  }
2204
3118
  this.#render();
2205
3119
  }
3120
+ /**
3121
+ * Expands all expandable groups in the task tree.
3122
+ *
3123
+ * @throws {GanttError} When the instance has been destroyed.
3124
+ */
2206
3125
  expandAll() {
2207
3126
  this.#assertAlive();
2208
3127
  this.#expandedIds.clear();
2209
- for (const id of getExpandableTaskIds(this.#input.tasks)) this.#expandedIds.add(id);
3128
+ if (this.#input !== null) for (const id of getExpandableTaskIds(this.#input.tasks)) this.#expandedIds.add(id);
2210
3129
  if (this.#rafPending && this.#rafId !== null) {
2211
3130
  cancelAnimationFrame(this.#rafId);
2212
3131
  this.#rafId = null;
@@ -2214,6 +3133,10 @@ var GanttChart = class {
2214
3133
  }
2215
3134
  this.#render();
2216
3135
  }
3136
+ /**
3137
+ * Removes the chart DOM and internal listeners, rendering the instance
3138
+ * unusable. Subsequent calls to any public method will throw.
3139
+ */
2217
3140
  destroy() {
2218
3141
  if (this.#destroyed) return;
2219
3142
  this.#destroyed = true;
@@ -2222,13 +3145,15 @@ var GanttChart = class {
2222
3145
  else window.removeEventListener("resize", this.#applyResponsivePaneStyles);
2223
3146
  if (this.#rafId !== null) cancelAnimationFrame(this.#rafId);
2224
3147
  this.#columnResizeCleanup();
2225
- for (const { cleanupDrag, cleanupLinkHandles } of this.#rightPaneRefs.barRegistry.values()) {
3148
+ for (const { cleanupDrag, cleanupLinkHandles, cleanupProgressDrag } of this.#rightPaneRefs.barRegistry.values()) {
2226
3149
  cleanupDrag();
2227
3150
  cleanupLinkHandles?.();
3151
+ cleanupProgressDrag?.();
2228
3152
  }
2229
3153
  clearChildren(this.#container);
2230
3154
  }
2231
3155
  #patchTask(id, patch) {
3156
+ if (this.#input === null) return;
2232
3157
  const index = this.#taskIndex.get(id);
2233
3158
  if (index === void 0) return;
2234
3159
  const target = this.#input.tasks[index];
@@ -2238,15 +3163,16 @@ var GanttChart = class {
2238
3163
  ...patch
2239
3164
  };
2240
3165
  }
3166
+ #findTask(id) {
3167
+ return this.#input?.tasks.find((t) => t.id === id);
3168
+ }
2241
3169
  #handleGridClick = (payload) => {
2242
3170
  const now = Date.now();
2243
3171
  const prev = this.#lastGridClick;
2244
3172
  if (prev !== null && prev.id === payload.id && now - prev.atMs <= 350) {
2245
3173
  this.#lastGridClick = null;
2246
- this.#cbs.onTaskEditIntent?.({
3174
+ this.#cbs.onTaskDoubleClick?.({
2247
3175
  id: payload.id,
2248
- source: "grid",
2249
- trigger: "double_click",
2250
3176
  task: payload.task
2251
3177
  });
2252
3178
  return;
@@ -2255,7 +3181,7 @@ var GanttChart = class {
2255
3181
  id: payload.id,
2256
3182
  atMs: now
2257
3183
  };
2258
- this.#cbs.onSelect?.(payload.id);
3184
+ this.#cbs.onTaskSelect?.(payload.id);
2259
3185
  };
2260
3186
  #onScroll = () => {
2261
3187
  ({scrollTop: this.#scrollTop} = this.#scrollEl);
@@ -2278,13 +3204,13 @@ var GanttChart = class {
2278
3204
  this.#leftPane.style.maxWidth = `${computedWidth}px`;
2279
3205
  this.#rightPane.style.minWidth = `${this.#timelineMinWidth}px`;
2280
3206
  };
2281
- #computeState() {
2282
- const allRows = flattenTree(buildTaskTree(this.#input.tasks), this.#expandedIds);
2283
- const [vpStart, vpEnd] = this.#opts.viewportStart !== void 0 && this.#opts.viewportEnd !== void 0 ? [this.#opts.viewportStart, this.#opts.viewportEnd] : deriveViewport(allRows, 2);
3207
+ #computeState(input) {
3208
+ const allRows = flattenTree(buildTaskTree(input.tasks), this.#expandedIds);
3209
+ const [vpStart, vpEnd] = this.#opts.viewportStart !== void 0 && this.#opts.viewportEnd !== void 0 ? [this.#opts.viewportStart, this.#opts.viewportEnd] : deriveViewport(allRows, 48);
2284
3210
  const mapper = createPixelMapper(this.#scale, vpStart);
2285
3211
  const totalWidth = Math.ceil(mapper.toX(vpEnd)) + 1;
2286
3212
  const layouts = computeLayout(allRows, mapper);
2287
- const links = routeLinks(this.#input.links, layouts);
3213
+ const links = routeLinks(input.links, layouts);
2288
3214
  const containerH = this.#height - HEADER_H;
2289
3215
  const rowCount = allRows.length;
2290
3216
  const startIndex = Math.max(0, Math.floor(this.#scrollTop / ROW_HEIGHT) - OVERSCAN);
@@ -2292,10 +3218,11 @@ var GanttChart = class {
2292
3218
  const paddingTop = startIndex * ROW_HEIGHT;
2293
3219
  const paddingBottom = Math.max(0, (rowCount - 1 - endIndex) * ROW_HEIGHT);
2294
3220
  return {
2295
- input: this.#input,
3221
+ input,
2296
3222
  scale: this.#scale,
2297
3223
  highlightLinkedDependenciesOnSelect: this.#opts.highlightLinkedDependenciesOnSelect ?? false,
2298
3224
  linkCreationEnabled: this.#opts.linkCreationEnabled ?? false,
3225
+ progressDragEnabled: this.#opts.progressDragEnabled ?? false,
2299
3226
  expandedIds: this.#expandedIds,
2300
3227
  selectedId: this.#selectedId,
2301
3228
  scrollTop: this.#scrollTop,
@@ -2318,23 +3245,51 @@ var GanttChart = class {
2318
3245
  }
2319
3246
  #render = () => {
2320
3247
  this.#rafPending = false;
2321
- const state = this.#computeState();
3248
+ const input = this.#input;
3249
+ if (input === null) return;
3250
+ const state = this.#computeState(input);
2322
3251
  renderTimeHeader(this.#rightHeader, state);
3252
+ this.#renderGridInternal(state);
3253
+ renderRightPane(this.#rightPaneRefs, state, this.#cbs);
3254
+ };
3255
+ #renderGrid = () => {
3256
+ this.#rafPending = false;
3257
+ const input = this.#input;
3258
+ if (input === null) return;
3259
+ this.#renderGridInternal(this.#computeState(input));
3260
+ };
3261
+ #renderGridInternal(state) {
2323
3262
  renderLeftPane(this.#leftBody, state, {
2324
3263
  onToggle: (id) => {
2325
3264
  if (this.#expandedIds.has(id)) this.#expandedIds.delete(id);
2326
3265
  else this.#expandedIds.add(id);
2327
3266
  this.#scheduleRender();
2328
3267
  },
2329
- onSelect: (id) => this.#cbs.onSelect?.(id),
3268
+ onTaskSelect: (id) => this.#cbs.onTaskSelect?.(id),
2330
3269
  onRowClick: (payload) => {
2331
3270
  this.#handleGridClick(payload);
2332
3271
  },
2333
- onTaskEditIntent: (payload) => this.#cbs.onTaskEditIntent?.(payload),
2334
- onAdd: (id) => this.#cbs.onAdd?.({ parentId: id })
3272
+ onTaskDoubleClick: (payload) => this.#cbs.onTaskDoubleClick?.(payload),
3273
+ onTaskAdd: (id) => this.#cbs.onTaskAdd?.(id)
2335
3274
  }, this.#columns);
3275
+ }
3276
+ #renderTimeline = () => {
3277
+ this.#rafPending = false;
3278
+ const input = this.#input;
3279
+ if (input === null) return;
3280
+ const state = this.#computeState(input);
3281
+ renderTimeHeader(this.#rightHeader, state);
2336
3282
  renderRightPane(this.#rightPaneRefs, state, this.#cbs);
2337
3283
  };
3284
+ #rebuildLeftPaneHeader() {
3285
+ this.#columnResizeCleanup();
3286
+ clearChildren(this.#leftHeader);
3287
+ const headerEl = buildLeftPaneHeader(this.#columns);
3288
+ this.#leftHeader.append(headerEl);
3289
+ this.#columnResizeCleanup = setupColumnResize(headerEl, this.#leftBody, this.#columns, (updated) => {
3290
+ this.#cbs.onGridColumnsChange?.(updated);
3291
+ });
3292
+ }
2338
3293
  #scheduleRender() {
2339
3294
  if (this.#rafPending || this.#destroyed) return;
2340
3295
  this.#rafPending = true;
@@ -2390,6 +3345,7 @@ var GanttChart = class {
2390
3345
  const headerEl = buildLeftPaneHeader(this.#columns);
2391
3346
  leftHeader.append(headerEl);
2392
3347
  leftPane.append(leftHeader);
3348
+ this.#leftHeader = leftHeader;
2393
3349
  const leftBody = el("div");
2394
3350
  leftPane.append(leftBody);
2395
3351
  this.#leftBody = leftBody;
@@ -2437,12 +3393,14 @@ var GanttChart = class {
2437
3393
  #wireEvents() {
2438
3394
  this.#rightPaneRefs.absoluteLayer.addEventListener("click", (event) => {
2439
3395
  if (event.target.closest(".gantt-bar, .gantt-milestone, .gantt-resize-handle")) return;
2440
- this.#cbs.onSelect?.(null);
3396
+ this.#selectedId = null;
3397
+ this.#scheduleRender();
2441
3398
  });
2442
3399
  this.#root.addEventListener("keydown", (event) => {
2443
3400
  if (event.key === "Escape" && this.#selectedId !== null) {
2444
3401
  event.preventDefault();
2445
- this.#cbs.onSelect?.(null);
3402
+ this.#selectedId = null;
3403
+ this.#scheduleRender();
2446
3404
  }
2447
3405
  });
2448
3406
  this.#scrollEl.addEventListener("scroll", this.#onScroll);
@@ -2457,6 +3415,6 @@ var GanttChart = class {
2457
3415
  }
2458
3416
  };
2459
3417
  //#endregion
2460
- export { BAR_HEIGHT, BAR_Y_OFFSET, CHART_LOCALE_EN_US, DEFAULT_GRID_COLUMNS, DENSITY, EN_US_LABELS, GRID_COLUMN_FR_MIN_WIDTH, GanttChart, GanttError, GanttInputSchema, LinkSchema, LinkTypeSchema, MILESTONE_HALF, MILESTONE_SIZE, ROW_HEIGHT, SCALE_CONFIGS, SpecialDayKindSchema, SpecialDaySchema, TaskSchema, TaskTypeSchema, addDays, buildTaskTree, computeLayout, createPixelMapper, deriveViewport, deriveWeekNumbering, deriveWeekStartsOn, deriveWeekendDays, detectCycles, diffDays, flattenTree, formatLabel, formatWeekNumber, gridColumnDefaults, gridNaturalWidth, gridTemplateColumns, isParent, parseDate, parseGanttInput, resolveChartLocale, routeLinks, safeParseGanttInput, validateLinkRefs, visibleColumns };
3418
+ export { BAR_HEIGHT, BAR_Y_OFFSET, CHART_LOCALE_EN_US, DEFAULT_GRID_COLUMNS, DENSITY, EN_US_LABELS, GRID_COLUMN_FR_MIN_WIDTH, GanttChart, GanttError, GanttInputSchema, LinkSchema, LinkTypeSchema, MILESTONE_HALF, MILESTONE_SIZE, ROW_HEIGHT, SCALE_CONFIGS, SpecialDayKindSchema, SpecialDaySchema, TaskKindSchema, TaskSchema, addDays, addHours, buildTaskTree, computeLayout, createPixelMapper, deriveViewport, deriveWeekNumbering, deriveWeekStartsOn, deriveWeekendDays, detectCycles, diffDays, diffHours, flattenTree, formatLabel, formatWeekNumber, gridColumnDefaults, gridNaturalWidth, gridTemplateColumns, isParent, parseDate, parseGanttInput, resolveChartLocale, routeLinks, validateLinkRefs, visibleColumns };
2461
3419
 
2462
3420
  //# sourceMappingURL=index.mjs.map