gantt-renderer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # 0.1.0 (2026-05-06)
2
+
3
+ ### Features
4
+
5
+ - Initial release of gantt-renderer
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2014-2018 Dieter Oberkofler <dieter.oberkofler@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,546 @@
1
+ # gantt-renderer
2
+
3
+ [![NPM Version](https://img.shields.io/npm/v/gantt-renderer.svg)](https://www.npmjs.com/package/gantt-renderer)
4
+ [![NPM Downloads](https://img.shields.io/npm/dm/gantt-renderer.svg)](https://www.npmjs.com/package/gantt-renderer)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Node.js CI](https://github.com/doberkofler/gantt-renderer/actions/workflows/ci.yml/badge.svg)](https://github.com/doberkofler/gantt-renderer/actions/workflows/ci.yml)
7
+ [![Coverage Status](https://coveralls.io/repos/github/doberkofler/gantt-renderer/badge.svg?branch=main)](https://coveralls.io/github/doberkofler/gantt-renderer?branch=main)
8
+
9
+ ![Gantt chart demo](docs/images/gantt-demo.png)
10
+
11
+ **[Live Demo →](https://doberkofler.github.io/gantt-renderer/)** &nbsp;|&nbsp; **[API Documentation →](https://doberkofler.github.io/gantt-renderer/docs/api/)**
12
+
13
+ A TypeScript Gantt chart renderer for **precalculated project plans**.
14
+
15
+ This package is focused on visualization and interaction: it renders tasks, timelines, and
16
+ dependencies, and provides UX hooks for user actions. It does **not** compute schedules,
17
+ optimize timelines, resolve resource constraints, or perform planning logic itself.
18
+
19
+ It is designed as a **core chart component** (`src/gantt-chart/**/*`) that your product can embed.
20
+ Outer-page/demo concerns (export toolbars, fullscreen shell controls, demo-only control rows)
21
+ are intentionally outside the core scope.
22
+
23
+ ## Purpose
24
+
25
+ - Render already-planned project data in a clear, interactive timeline UI.
26
+ - Capture user interactions (select, move, resize, double-click) and hand them back to your app.
27
+ - Let your backend or planning engine stay the source of truth for all project calculations.
28
+
29
+ ## Core Scope
30
+
31
+ - Includes: chart/grid rendering, task bars, timeline header, dependency links, selection, drag/resize UX, and responsive split-pane behavior.
32
+ - Excludes: built-in outer toolbars, export implementations, fullscreen shell behavior, and demo-only control strips.
33
+
34
+ ## What This Library Does
35
+
36
+ - Input validation with `zod` schemas.
37
+ - Tree-based task hierarchy with expand/collapse support.
38
+ - Timeline scales: `hour`, `day`, `week`, `month`, `quarter`, `year`.
39
+ - Special-day rendering API for weekends, holidays, and custom day semantics in `day` scale.
40
+ - Drag and resize interactions with callback hooks.
41
+ - Dependency validation and link path routing.
42
+ - Unit tests with Vitest and integration tests with Playwright.
43
+
44
+ ## What This Library Does Not Do
45
+
46
+ - Automatic project scheduling.
47
+ - Critical path or resource leveling calculations.
48
+ - Date recalculation across dependent tasks.
49
+ - Business-rule planning decisions.
50
+
51
+ Those concerns should be handled upstream, then passed into this renderer as `GanttInput`.
52
+
53
+ ## Integration Pattern
54
+
55
+ 1. Compute/plan project data in your domain layer or backend.
56
+ 2. Validate/shape the result as `GanttInput`.
57
+ 3. Render with `new GanttChart(...)` and react to interaction callbacks.
58
+ 4. Persist user edits through your own business logic, then update the chart state.
59
+
60
+ ## Getting Started
61
+
62
+ ```bash
63
+ pnpm install
64
+ pnpm run dev
65
+ ```
66
+
67
+ Open the local Vite URL shown in the terminal to view the demo.
68
+
69
+ ### Using the library in your project
70
+
71
+ ```bash
72
+ npm install gantt-renderer
73
+ ```
74
+
75
+ ```ts
76
+ import {GanttChart, parseGanttInput} from 'gantt-renderer';
77
+ import 'gantt-renderer/styles/gantt.css';
78
+
79
+ const input = parseGanttInput(yourData);
80
+ const instance = new GanttChart(document.getElementById('chart')!, input, {
81
+ scale: 'day',
82
+ theme: 'system',
83
+ });
84
+ ```
85
+
86
+ The package exports:
87
+ - **`gantt-renderer`** — ESM bundle with all types and utilities
88
+ - **`gantt-renderer/styles/gantt.css`** — Core chart stylesheet
89
+
90
+ ## GitHub Demo Verification Example
91
+
92
+ Use this quick scenario when validating the demo behavior on a hosted page (for example GitHub Pages):
93
+
94
+ 1. Open the page and confirm `Event Log` starts with `demo initialized`.
95
+ 2. In `Scale`, switch `Days -> Months -> Years`; verify each change is reflected in the selector and logged as `scale-select | integrated`.
96
+ 3. Click `Zoom Out` until `Years`, then `Zoom In` until `Hours`; verify the selected scale updates each step.
97
+ 4. Click `Collapse All` and confirm child rows hide, then click `Expand All` and confirm rows return.
98
+ 5. Click `Fullscreen`, then press `Escape`; verify the button label toggles and fullscreen state changes are logged.
99
+
100
+ Expected result: all controls are interactive, placeholder actions are clearly signaled, and every control action appends a timestamped line in `Event Log`.
101
+
102
+ ## Scripts
103
+
104
+ - `pnpm run dev` - Start the Vite development server.
105
+ - `pnpm run build` - Build demo SPA assets into `dist-demo/`.
106
+ - `pnpm run build:lib` - Build the library (ESM + `.d.ts` + CSS) into `dist/`.
107
+ - `pnpm run preview` - Preview the demo build locally.
108
+ - `pnpm run typecheck` - Run TypeScript type checks.
109
+ - `pnpm run lint` - Run oxlint.
110
+ - `pnpm run lint:css` - Run stylelint on CSS files.
111
+ - `pnpm run lint:css:fix` - Autofix CSS lint violations where possible.
112
+ - `pnpm run format` - Format code with oxfmt and autofix CSS with stylelint.
113
+ - `pnpm run format:check` - Verify formatting.
114
+ - `pnpm run test` - Run unit tests with Vitest (browser mode).
115
+ - `pnpm run test:build` - Run build regression tests verifying library output.
116
+ - `pnpm run integration-test` - Run Playwright integration tests.
117
+ - `pnpm run docs:api` - Generate TypeDoc API documentation into `docs/api/`.
118
+ - `pnpm run ci` - Run typecheck, lint, format check, library build, demo build, and tests.
119
+
120
+ ## Package API
121
+
122
+ The primary entrypoint is `src/gantt-chart/index.ts`, which exports:
123
+
124
+ - Core input and domain types (`Task`, `Link`, `GanttInput`, `TaskNode`, and related types).
125
+ - Locale types and utilities (`ChartLocale`, `LocaleLabelKey`, `resolveChartLocale`, `deriveWeekendDays`, etc.).
126
+ - Validation helpers (`parseGanttInput`, `safeParseGanttInput`, and schemas).
127
+ - Timeline/domain utilities (`computeLayout`, `createPixelMapper`, `routeLinks`, and others).
128
+ - Vanilla chart class (`GanttChart`) and instance/callback types.
129
+
130
+ ### Time scale support
131
+
132
+ `TimeScale` supports all chart zoom levels:
133
+
134
+ - `hour`
135
+ - `day`
136
+ - `week`
137
+ - `month`
138
+ - `quarter`
139
+ - `year`
140
+
141
+ Timeline cadence details:
142
+
143
+ - `hour`, `day`, and `week` use fixed-duration buckets.
144
+ - `month`, `quarter`, and `year` headers/grid boundaries are aligned to true UTC calendar boundaries.
145
+ - Upper and lower timeline header rows are derived from the same boundary cadence to keep grouping and bucket labels consistent while zooming and scrolling.
146
+
147
+ ### `GanttChart` constructor API
148
+
149
+ The primary entrypoint is via the `GanttChart` class exported from `src/gantt-chart/index.ts`, which also exports:
150
+
151
+ - Core input and domain types (`Task`, `Link`, `GanttInput`, `TaskNode`, and related types).
152
+ - Validation helpers (`parseGanttInput`, `safeParseGanttInput`, and schemas).
153
+ - Timeline/domain utilities (`computeLayout`, `createPixelMapper`, `routeLinks`, and others).
154
+ - Vanilla mounting API (`GanttChart`) and instance/callback types.
155
+
156
+ ### Locale / Internationalization
157
+
158
+ `GanttOptions.locale` accepts a BCP 47 language tag string or a `ChartLocale` object:
159
+
160
+ ```ts
161
+ export type ChartLocale = {
162
+ code: string; // BCP 47 language tag, e.g. 'en-US', 'de-DE'
163
+ labels?: Partial<Record<LocaleLabelKey, string>>; // optional UI string overrides
164
+ weekStartsOn?: 0 | 1 | 6; // 0=Sun, 1=Mon, 6=Sat (default: derived from locale)
165
+ weekNumbering?: 'iso' | 'us' | 'simple'; // week number scheme (default: derived from locale)
166
+ weekendDays?: number[]; // weekend day indices 0–6 (default: derived from locale)
167
+ };
168
+ ```
169
+
170
+ When a plain string is passed, `weekStartsOn`, `weekNumbering`, and `weekendDays` are derived from
171
+ CLDR conventions using `Intl.Locale.getWeekInfo()` (with a built-in fallback mapping table for
172
+ Firefox and older runtimes).
173
+
174
+ **Example — use a German locale:**
175
+
176
+ ```ts
177
+ import {GanttChart} from 'gantt-renderer';
178
+
179
+ const instance = new GanttChart(container, input, {
180
+ locale: 'de-DE',
181
+ });
182
+ ```
183
+
184
+ **Example — full ChartLocale object with label overrides:**
185
+
186
+ ```ts
187
+ import {GanttChart, type ChartLocale} from 'gantt-renderer';
188
+
189
+ const deLocale: ChartLocale = {
190
+ code: 'de-DE',
191
+ weekStartsOn: 1,
192
+ weekNumbering: 'iso',
193
+ labels: {
194
+ column_task_name: 'Aufgabe',
195
+ column_start_time: 'Start',
196
+ column_duration: 'Dauer',
197
+ add_subtask_title: 'Teilaufgabe hinzufügen',
198
+ aria_task: 'Aufgabe {0}',
199
+ aria_milestone: 'Meilenstein {0}',
200
+ },
201
+ };
202
+
203
+ const instance = new GanttChart(container, input, {locale: deLocale});
204
+ ```
205
+
206
+ When omitted, the default is `CHART_LOCALE_EN_US` (English labels, US week conventions).
207
+
208
+ **Label keys** (`LocaleLabelKey`):
209
+
210
+ | Key | Default (en-US) | Used for |
211
+ |---|---|---|
212
+ | `aria_task` | `Task {0}` | Task bar `aria-label` |
213
+ | `aria_milestone` | `Milestone {0}` | Milestone `aria-label` |
214
+ | `add_subtask_title` | `Add subtask` | Add-button `title` attribute |
215
+ | `column_task_name` | `Task name` | Grid column header |
216
+ | `column_start_time` | `Start time` | Grid column header |
217
+ | `column_duration` | `Duration` | Grid column header |
218
+ | `column_quarter` | `Q` | Quarter prefix in time header |
219
+
220
+ Aria-label templates use `{0}` as the task name placeholder. Only the keys you override in
221
+ `labels` are replaced; missing keys fall back to `EN_US_LABELS`.
222
+
223
+ **Exported locale utilities:**
224
+
225
+ ```ts
226
+ import {
227
+ CHART_LOCALE_EN_US, // default ChartLocale
228
+ EN_US_LABELS, // fallback label record
229
+ resolveChartLocale, // ChartLocale | string → ChartLocale
230
+ deriveWeekStartsOn, // BCP 47 → 0|1|6
231
+ deriveWeekNumbering, // BCP 47 → 'iso'|'us'|'simple'
232
+ deriveWeekendDays, // BCP 47 → number[]
233
+ formatWeekNumber, // Date × scheme → number
234
+ formatLabel, // template × arg → string
235
+ } from 'gantt-renderer';
236
+ ```
237
+
238
+ **Week numbering schemes:**
239
+
240
+ - `'iso'` — ISO 8601 (week 1 contains the first Thursday; Monday start).
241
+ - `'us'` — Week 1 contains January 1; Sunday start.
242
+ - `'simple'` — `Math.ceil(dayOfYear / 7)`.
243
+
244
+ **Weekend days** default to the locale-derived convention (e.g., Fri/Sat for `ar-SA`, Sat/Sun for `de-DE`).
245
+ The `weekendDays` option overrides the locale default for non-standard corporate calendars.
246
+
247
+ `GanttOptions` includes responsive controls for narrow viewports:
248
+
249
+ - `leftPaneWidth?: number` - Explicit left grid pane width in px. When omitted, the width is derived from the grid column schema using `gridNaturalWidth()` and viewport-proportional clamping (desktop: 25%&#x2013;40% of viewport; mobile: see below).
250
+ - `responsiveSplitPane?: boolean` - Enables automatic mobile adaptation (default `true`).
251
+ - `mobileBreakpoint?: number` - Viewport/container width threshold for mobile behavior (default `768`).
252
+ - `mobileLeftPaneMinWidth?: number` - Minimum left-pane width in mobile mode (default `140`).
253
+ - `mobileLeftPaneMaxRatio?: number` - Max left-pane share of viewport in mobile mode (default `0.45`).
254
+ - `timelineMinWidth?: number` - Minimum timeline pane width; if needed, horizontal scrolling is used (default `220`).
255
+
256
+ This keeps the timeline visible at narrow widths (for example `375px`) while preserving desktop behavior.
257
+ On desktop, the pane width is clamped between `viewportWidth * 0.25` and `viewportWidth * 0.4`, with the natural width computed from the active column schema as the initial value.
258
+
259
+ #### `gridNaturalWidth(columns)` and `GRID_COLUMN_FR_MIN_WIDTH`
260
+
261
+ Exported from the public API alongside `DEFAULT_GRID_COLUMNS`:
262
+
263
+ - `gridNaturalWidth(columns: GridColumn[]): number` — computes the minimum natural px width from a grid column schema. Fixed `px` columns sum directly; each `fr` unit contributes `GRID_COLUMN_FR_MIN_WIDTH` px (default `120`). Hidden columns are skipped.
264
+ - `GRID_COLUMN_FR_MIN_WIDTH` — px contribution per `fr` unit used by `gridNaturalWidth` (default `120`).
265
+
266
+ ### `GanttChart` constructor special-day options
267
+
268
+ `GanttOptions` includes timeline day-background controls:
269
+
270
+ - `showWeekends?: boolean` - Enables Saturday/Sunday highlighting in `day` scale (default `true`).
271
+ - `weekendDays?: number[]` - Configures weekend day indices (`0=Sun ... 6=Sat`) for locale/project calendars (default `[0, 6]`).
272
+ - `specialDays?: SpecialDay[]` - Explicit date-only special days that render in the timeline background.
273
+
274
+ `SpecialDay` shape:
275
+
276
+ - `date: string` - `YYYY-MM-DD` (UTC date-only semantics).
277
+ - `kind: 'holiday' | 'custom'` - Semantic class applied to the day cell.
278
+ - `label?: string` - Optional UI label (added as `data-label` and `title`).
279
+ - `className?: string` - Optional extra CSS class for consumer theming.
280
+
281
+ Notes:
282
+
283
+ - Special-day rendering applies to `day` scale columns only.
284
+ - Date matching is deterministic across locale/timezone because date keys are normalized in UTC.
285
+
286
+ ### `GanttChart` constructor dependency-link highlight options
287
+
288
+ `GanttOptions` includes dependency-link selection highlight controls:
289
+
290
+ - `highlightLinkedDependenciesOnSelect?: boolean` - When `true`, links related to the selected task use highlight color/arrow styling. Default is `false`.
291
+
292
+ By default, task selection does not change dependency link color, arrow marker, or stroke width.
293
+
294
+ ### `GanttChart` constructor link-creation options
295
+
296
+ `GanttOptions` includes interactive link-creation controls for dependency links:
297
+
298
+ - `linkCreationEnabled?: boolean` - Enables per-task endpoint handles and drag-to-create-link interactions. Default is `false`.
299
+
300
+ When enabled:
301
+
302
+ - Task bars show two endpoint handles (left = start, right = finish) on hover; milestones show one center handle.
303
+ - Dragging from a handle draws a ghost line in the SVG dependency layer (dashed when no target, solid with arrow when over a valid bar).
304
+ - Releasing over a different task bar fires `GanttCallbacks.onLinkCreate` with `{sourceTaskId, targetTaskId, type: 'FS'}`.
305
+ - Releasing over the source task or empty space cancels the drag with no callback.
306
+ - Handles are keyboard-accessible with `tabindex="0"`, `role="button"`, and descriptive `aria-label`.
307
+
308
+ `GanttCallbacks` callback:
309
+
310
+ ```ts
311
+ export type OnLinkCreate = (payload: {
312
+ sourceTaskId: number;
313
+ targetTaskId: number;
314
+ type: 'FS';
315
+ }) => void;
316
+ ```
317
+
318
+ Example — enable link creation and handle new links:
319
+
320
+ ```ts
321
+ import {GanttChart, type OnLinkCreate} from 'gantt-renderer';
322
+
323
+ const onLinkCreate: OnLinkCreate = (payload) => {
324
+ console.log('New link:', payload);
325
+ // Add the link to your data model and call instance.update(...)
326
+ };
327
+
328
+ const instance = new GanttChart(container, input, {
329
+ linkCreationEnabled: true,
330
+ onLinkCreate,
331
+ });
332
+ ```
333
+
334
+ ### `GanttChart` constructor theme option
335
+
336
+ `GanttOptions` includes theme/dark-mode controls:
337
+
338
+ - `theme?: 'light' | 'dark' | 'system'` — Controls the chart color scheme. The constructor sets a `data-theme` attribute on the container element. Default is `'system'`.
339
+
340
+ Mode behavior:
341
+
342
+ | `theme` value | `data-theme` attribute | Visual result |
343
+ | --- | --- | --- |
344
+ | `'system'` (default) | `system` | Respects the OS `prefers-color-scheme` media query. Light on light OS, dark on dark OS. |
345
+ | `'light'` | `light` | Always light (default design tokens, no dark overrides). |
346
+ | `'dark'` | `dark` | Always dark via `[data-theme="dark"]` CSS overrides. |
347
+
348
+ Dark-mode CSS tokens override 16 custom properties covering background, header, stripe, border, grid line, text, selection, today marker, row selection, special-day backgrounds, link color, and bar label color. The `[data-theme="dark"]` selector scopes dark overrides to the container or any ancestor with the attribute.
349
+
350
+ Example — force dark mode:
351
+
352
+ ```ts
353
+ import {GanttChart} from 'gantt-renderer';
354
+
355
+ const instance = new GanttChart(container, input, {
356
+ theme: 'dark',
357
+ });
358
+ ```
359
+
360
+ Consumers can also set `data-theme="dark"` on a parent element (e.g. `<html>`) to apply dark mode without passing the option.
361
+
362
+ ### Selection visual style
363
+
364
+ Selected bars and milestones use a subtle emphasis style rather than a dominant outline.
365
+
366
+ - Selected timeline shapes receive `.gantt-shape--selected`.
367
+ - The default theme uses `--gantt-selection-ring` and `--gantt-selection-glow` tokens.
368
+ - Consumers can override these CSS variables/classes to tune selection emphasis without changing interaction behavior.
369
+
370
+ ### Selection model
371
+
372
+ The chart uses a strict single-selection model:
373
+
374
+ - **Click a task** (grid row, bar, or milestone) to select it.
375
+ - **Click the same task again** — no-op; the selection stays on that task.
376
+ - **Click a different task** — switches selection to the new task.
377
+ - **Click empty space** in the timeline pane — deselects (clears selection).
378
+ - **Press Escape** — deselects when a task is selected.
379
+ - **`instance.select(null)`** — programmatic deselection.
380
+
381
+ The internal `onSelect` callback fires only on actual selection changes; repeated clicks on the same task do not emit duplicate events. Consumers receive `onSelect(taskId)` on select and `onSelect(null)` on deselect.
382
+
383
+ ### Container framing tokens
384
+
385
+ The chart container border, radius, and shadow are controlled via CSS custom properties:
386
+
387
+ ```css
388
+ :root {
389
+ --gantt-container-border-radius: 6px;
390
+ --gantt-container-border: 1px solid var(--gantt-border);
391
+ --gantt-container-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
392
+ }
393
+ ```
394
+
395
+ These tokens are applied by the `.gantt-root` CSS class. Consumers can override them
396
+ to customise the chart framing without touching inline styles. Container framing is
397
+ separate from internal pane dividers (which continue to use `--gantt-border`).
398
+
399
+ ### Typography scale
400
+
401
+ Core chart typography is tokenised via CSS custom properties so consumers can override
402
+ sizing without patching the renderer:
403
+
404
+ ```css
405
+ :root {
406
+ --gantt-font-size-xs: 11px; /* toolbar, time headers, milestone labels */
407
+ --gantt-font-size-sm: 12px; /* bar labels, grid cell values */
408
+ --gantt-font-size-md: 13px; /* row names */
409
+ --gantt-font-size-lg: 16px; /* add-button glyph */
410
+
411
+ --gantt-font-weight-normal: 400;
412
+ --gantt-font-weight-semibold: 600;
413
+ --gantt-font-weight-bold: 700;
414
+
415
+ --gantt-letter-spacing-tight: 0.04em;
416
+ --gantt-letter-spacing-wide: 0.05em;
417
+
418
+ /* Bar label colour — defaults to white for contrast on coloured bars.
419
+ Override for dark themes or custom bar colour palettes. */
420
+ --gantt-bar-label-color: #ffffff;
421
+ }
422
+ ```
423
+
424
+ All inline font-size, font-weight, and letter-spacing values in chart surfaces
425
+ reference these tokens, keeping the typography scale consistent and
426
+ customisable.
427
+
428
+ ### Density constants
429
+
430
+ `DENSITY` is a read-only constant object exported from the public API that defines the core chart geometry:
431
+
432
+ ```ts
433
+ export const DENSITY = {
434
+ rowHeight: 44, // px height of each grid/timeline row
435
+ barHeight: 28, // px height of task bars
436
+ milestoneSize: 20, // px width/height of milestone diamond
437
+ } as const;
438
+ ```
439
+
440
+ Derived values (`ROW_HEIGHT`, `BAR_HEIGHT`, `BAR_Y_OFFSET`, `MILESTONE_SIZE`, `MILESTONE_HALF`) are also exported.
441
+
442
+ CSS counterparts are available as custom properties for theme overrides:
443
+
444
+ ```css
445
+ :root {
446
+ --gantt-row-height: 44px;
447
+ --gantt-bar-height: 28px;
448
+ --gantt-milestone-size: 20px;
449
+ }
450
+ ```
451
+
452
+ These tokens ensure row alignment, bar centering, and hit areas stay consistent across all rendering paths.
453
+
454
+ ### `GanttChart` constructor grid column schema
455
+
456
+ `GanttOptions` includes a `gridColumns` option for customising the left-pane grid:
457
+
458
+ - `gridColumns?: GridColumn[]` - Column schema array. When omitted, `gridColumnDefaults(locale)` generates a localized 4-column layout (name, start time, duration, actions).
459
+
460
+ `GridColumn` shape:
461
+
462
+ - `id: string` - Unique column identifier. Built-in IDs: `name` (tree name cell), `actions` (add button). All other IDs render as data columns.
463
+ - `header: string` - Column header label.
464
+ - `width: string` - CSS width value (e.g. `'90px'`, `'1fr'`, `'68px'`).
465
+ - `align?: 'left' | 'center' | 'right'` - Horizontal alignment for header and body cells.
466
+ - `visible?: boolean` - Whether the column is shown (default `true`).
467
+ - `field?: keyof Task` - Task field to read for data columns.
468
+ - `format?: (value, task, row, locale: ChartLocale) => string` - Optional formatter. Receives the raw field value plus full task/row context and locale.
469
+
470
+ `gridColumnDefaults(locale)` returns a localized schema using the locale's label overrides
471
+ with `EN_US_LABELS` fallback. The static `DEFAULT_GRID_COLUMNS` constant is also exported
472
+ for consumers that manage localization externally.
473
+
474
+ Example — custom column schema:
475
+
476
+ ```ts
477
+ import {GanttChart, type GridColumn} from 'gantt-renderer';
478
+
479
+ const columns: GridColumn[] = [
480
+ {id: 'name', header: 'Task', width: '2fr'},
481
+ {id: 'progress', header: 'Progress', width: '70px', align: 'right', field: 'progress', format: (v) => `${Math.round((v as number) * 100)}%`},
482
+ {id: 'start_date', header: 'Start', width: '90px', field: 'start_date'},
483
+ {id: 'duration', header: 'Days', width: '60px', field: 'duration'},
484
+ ];
485
+
486
+ const instance = new GanttChart(container, input, {gridColumns: columns});
487
+ ```
488
+
489
+ Notes:
490
+ - The `name` column renders the tree structure (indentation + toggle button).
491
+ - The `actions` column renders the add-subtask button.
492
+ - Columns with `visible: false` are excluded from both header and body.
493
+ - Header and body always share the same `gridTemplateColumns` derived from visible columns.
494
+
495
+ ### `GanttChart` instance API
496
+
497
+ `new GanttChart(container, input, options)` returns a `GanttInstance` with these methods:
498
+
499
+ - `update(input)` - Replaces the full dataset and rerenders after validation.
500
+ - `setScale(scale)` - Switches between `hour|day|week|month|quarter|year` without remounting.
501
+ - `select(id | null)` - Programmatically selects a task id or clears selection with `null`.
502
+ - `collapseAll()` - Collapses all expandable groups in the task tree.
503
+ - `expandAll()` - Expands all expandable groups in the task tree.
504
+ - `destroy()` - Removes chart DOM and internal listeners.
505
+
506
+ `collapseAll()` and `expandAll()` are deterministic and idempotent; invoking the same method repeatedly does not change behavior beyond the first call.
507
+
508
+ ### Pane and column resize hooks
509
+
510
+ `GanttCallbacks` includes two drag-end hooks for observing user-initiated resize actions:
511
+
512
+ - `onLeftPaneWidthChange?: (width: number) => void` — Fires when the user finishes dragging the vertical splitter between the grid and timeline panes. The callback receives the final left-pane width in pixels. Fires on `pointerup`, not on every pixel move.
513
+ - `onGridColumnsChange?: (columns: GridColumn[]) => void` — Fires when the user finishes dragging a column resize handle in the grid header. The callback receives the full column array with updated `.width` values (all converted to `px` strings). Fires on `pointerup`, not on every pixel move.
514
+
515
+ These hooks let consumers persist user preferences (e.g., save to `localStorage` or a backend) and restore them on the next mount via `leftPaneWidth` and `gridColumns` options.
516
+
517
+ ## Development Notes
518
+
519
+ - The project uses ESM and strict TypeScript.
520
+ - The library is built with [tsdown](https://tsdown.dev) (powered by Rolldown) into `dist/`.
521
+ - The demo app is built with Vite into `dist-demo/`.
522
+ - TypeScript formatting and linting are handled with the Oxc toolchain (`oxfmt` and `oxlint`).
523
+ - CSS linting is handled with `stylelint` and `stylelint-config-standard`.
524
+ - Commit messages follow Conventional Commits.
525
+ - API documentation is generated with [TypeDoc](https://typedoc.org) (`pnpm run docs:api`).
526
+
527
+ ## CSS Linting
528
+
529
+ CSS files are linted with [stylelint](https://stylelint.io/) using `stylelint-config-standard`
530
+ as the base ruleset. The project-specific configuration is in `stylelint.config.js`.
531
+
532
+ ### Conventions
533
+
534
+ | Rule | Value | Notes |
535
+ |------|-------|-------|
536
+ | Indentation | Tabs | Enforced by `oxfmt`; stylelint 17 removed built-in indentation |
537
+ | Selector class naming | `^(gantt\|demo)-[a-zA-Z0-9_-]+$` | All CSS classes must use `gantt-` (core chart) or `demo-` (demo shell) prefix |
538
+ | Custom property naming | `^(gantt\|demo)-[a-z][a-z0-9-]*$` | Design tokens use prefixed kebab-case |
539
+ | `!important` | Allowed | Used deliberately for hover/state/affordance rules |
540
+ | Colour functions | `legacy` notation (`rgba()`) | Project consistently uses `rgba()` for alpha colours |
541
+
542
+ ### Commands
543
+
544
+ - `pnpm run lint:css` — Lint all CSS files.
545
+ - `pnpm run lint:css:fix` — Autofix where possible.
546
+ - `pnpm run format` — Runs `oxfmt --write` followed by `stylelint --fix`.