@ninthforge/gantt 0.0.0-stage → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,439 @@
1
+ import * as React from 'react';
2
+
3
+ /**
4
+ * Text size: type roles and one scale for every component (the same roles in Power Apps and Power BI).
5
+ *
6
+ * Components size text by role, never by a pixel number, so one setting ("Text size", 75–200 %)
7
+ * makes every label, value and title bigger or smaller together. The same numbers reach CSS
8
+ * (`--nf-text-<role>`) and layout maths (`textSize(role, scale)`), so label fitting and row
9
+ * heights match what the browser draws.
10
+ */
11
+ /** What the text is for, not how big it is. */
12
+ type TextRole = 'caption' | 'body' | 'label' | 'value' | 'callout' | 'header' | 'title' | 'hero';
13
+ /** Per-role sizes at 100 %, in CSS pixels, that replace the defaults. The scale still applies. */
14
+ type TextSizes = Partial<Record<TextRole, number>>;
15
+
16
+ /** A component theme. Every key becomes a `--nf-<kebab-key>` CSS variable. */
17
+ interface ComponentTheme {
18
+ accent: string;
19
+ accentTint: string;
20
+ accentText: string;
21
+ surface: string;
22
+ surfaceAlt: string;
23
+ ink: string;
24
+ muted: string;
25
+ line: string;
26
+ lineSoft: string;
27
+ phase: string;
28
+ milestone: string;
29
+ baseline: string;
30
+ selected: string;
31
+ focus: string;
32
+ positive: string;
33
+ negative: string;
34
+ tooltipBg: string;
35
+ tooltipInk: string;
36
+ font: string;
37
+ monoFont: string;
38
+ }
39
+
40
+ /**
41
+ * One action offered in a component's empty state, e.g. "Load sample data" on a Ninth Forge
42
+ * sample table. The host decides when to offer it and runs it; the component only shows it.
43
+ */
44
+ interface EmptyAction {
45
+ label: string;
46
+ onClick: () => void;
47
+ /** Running: the button gives way to `status` ("Loading sample data… 120 of 380"). */
48
+ busy?: boolean;
49
+ status?: string;
50
+ /** Shown under the button, e.g. what the action does. */
51
+ hint?: string;
52
+ /** The last attempt failed (the button stays, so the user can retry). */
53
+ error?: string;
54
+ }
55
+
56
+ /** The host props every Plan component takes for its watermark. */
57
+ interface WatermarkHostProps {
58
+ /**
59
+ * `false` draws no watermark at all (no link, no space). The licence is still checked; the
60
+ * host decides when, e.g. Power BI Desktop for authoring (DECISIONS 2026-10-31). Default true.
61
+ */
62
+ watermark?: boolean;
63
+ /** `text`: words only, no coloured mark (Power BI certification). Default `mark`. */
64
+ watermarkStyle?: 'mark' | 'text';
65
+ /** The host opens the link itself (Power BI's `launchUrl`): the watermark becomes a button. */
66
+ onWatermarkClick?: () => void;
67
+ }
68
+
69
+ type Zoom = 'week' | 'month' | 'quarter';
70
+ interface Tick {
71
+ day: number;
72
+ x: number;
73
+ width: number;
74
+ label: string;
75
+ }
76
+ interface TimeScale {
77
+ zoom: Zoom;
78
+ startDay: number;
79
+ /** Exclusive. */
80
+ endDay: number;
81
+ dayWidth: number;
82
+ width: number;
83
+ x(day: number): number;
84
+ /** Day at a pixel offset (for drag, later). */
85
+ dayAt(x: number): number;
86
+ major: Tick[];
87
+ minor: Tick[];
88
+ /** Weekend bands, only at week zoom. */
89
+ weekends: {
90
+ x: number;
91
+ width: number;
92
+ }[];
93
+ }
94
+
95
+ /**
96
+ * Host hooks shared by the Plan components (Gantt, Timeline, capacity heatmap) when a host such
97
+ * as a Power BI visual wraps them. Same shapes as `@ninthforge/charts`' hooks. All optional:
98
+ * Power Apps passes none of them.
99
+ */
100
+ /** Passed with a selection: `multi` is true when Ctrl or Cmd was held (add to the selection). */
101
+ interface SelectInfo {
102
+ multi: boolean;
103
+ }
104
+ /** Where the pointer (or, from the keyboard, the item's anchor) is, in client coordinates. */
105
+ interface PointerInfo extends SelectInfo {
106
+ clientX: number;
107
+ clientY: number;
108
+ }
109
+ /** A host formatter, e.g. one built from a Power BI format string. `C` says what is formatted. */
110
+ type HostFormat<C> = (value: number, ctx: C) => string;
111
+
112
+ /**
113
+ * "Copy diagnostics": one click copies what support needs to reproduce a problem, and nothing
114
+ * else. Product and control version, licence state (never the key), host, browser, locale, time
115
+ * zone, the mapped property names and the row count. No data values and no personal data: the
116
+ * input types only take names and counts, and every string is trimmed to one short line.
117
+ *
118
+ * Framework-free. The drop-in menu item is `CopyDiagnosticsItem` in `@ninthforge/core/react`.
119
+ */
120
+ /** Where the control runs. PCF wrappers and Power BI visuals pass it when they know. */
121
+ type DiagnosticsHost = 'model-driven' | 'canvas' | 'canvas (Studio)' | 'custom page' | 'Power BI' | 'code app' | 'web page';
122
+ /**
123
+ * The licence as diagnostics reads it: a `LicenseResult` from `@ninthforge/license` fits. Only
124
+ * `status`, `payload.tier`, `payload.org` and `payload.exp` are read; the key itself never
125
+ * reaches this function, and the tenant ID and key id are left out.
126
+ */
127
+ interface DiagnosticsLicence {
128
+ status: string;
129
+ payload?: {
130
+ tier?: string;
131
+ org?: string;
132
+ exp?: string;
133
+ } | null;
134
+ }
135
+ interface DiagnosticsInput {
136
+ /** The product, e.g. `Plan`, `Charts`. */
137
+ product: string;
138
+ /** The control, e.g. `Gantt` or `nf_NinthForge.PlanGantt`. */
139
+ control: string;
140
+ /** The control or package version, e.g. `0.4.4`. */
141
+ version: string;
142
+ licence?: DiagnosticsLicence | null;
143
+ /** Overrides detection (Power BI visuals, code apps, or a PCF that knows its host). */
144
+ host?: DiagnosticsHost;
145
+ /**
146
+ * Mapped properties: `{ taskStart: 'nf_start' }` gives `taskStart → nf_start`, and a list
147
+ * gives the names alone. Names only, never values.
148
+ */
149
+ properties?: Readonly<Record<string, string | null | undefined>> | readonly string[];
150
+ /** Rows the control received. */
151
+ rows?: number;
152
+ /**
153
+ * A few product-specific settings, e.g. `{ 'Save mode': 'direct' }`. Settings only: never
154
+ * record values, user names or e-mail addresses.
155
+ */
156
+ extra?: Readonly<Record<string, string | number | boolean | null | undefined>>;
157
+ now?: Date;
158
+ }
159
+ /**
160
+ * What a host adds to a component's own diagnostics (the component fills product, version,
161
+ * licence and its row count): a PCF passes its control name, mapped properties and settings.
162
+ */
163
+ type DiagnosticsHostInfo = Partial<Pick<DiagnosticsInput, 'control' | 'version' | 'host' | 'properties' | 'rows' | 'extra'>>;
164
+
165
+ type GanttTaskKind = 'task' | 'phase' | 'milestone';
166
+ interface GanttTask {
167
+ id: string;
168
+ name: string;
169
+ /** ISO date. Phases without dates take the span of their children. */
170
+ start?: string;
171
+ /** ISO date, inclusive. */
172
+ end?: string;
173
+ /** Milestones: the date. Falls back to `start`. */
174
+ date?: string;
175
+ kind?: GanttTaskKind;
176
+ parentId?: string | null;
177
+ /** 0–100. */
178
+ progress?: number;
179
+ owner?: {
180
+ name: string;
181
+ initials?: string;
182
+ };
183
+ baselineStart?: string;
184
+ baselineEnd?: string;
185
+ /** Finish-to-start predecessors by id. */
186
+ dependsOn?: string[];
187
+ /** Locks this row's bar: no move, resize, progress or link. */
188
+ readOnly?: boolean;
189
+ }
190
+ /** Edits the user can make on the chart. */
191
+ type EditAction = 'move' | 'resize' | 'progress' | 'link' | 'add' | 'rename';
192
+ /** Optional grid columns beside the task name. */
193
+ type GanttColumn = 'owner' | 'progress' | 'start' | 'end' | 'duration';
194
+ /** Which grid columns show. Owner and Done show by default; dates and duration don't. */
195
+ type GanttColumns = Partial<Record<GanttColumn, boolean>>;
196
+ /** `false` disables all editing; an object switches individual actions off. */
197
+ type Editable = boolean | Partial<Record<EditAction, boolean>>;
198
+ /** One edit, as the host receives it. Only the changed fields are present. */
199
+ interface GanttChange {
200
+ id: string;
201
+ name?: string;
202
+ start?: string;
203
+ end?: string;
204
+ date?: string;
205
+ progress?: number;
206
+ parentId?: string | null;
207
+ dependsOn?: string[];
208
+ /** The same fields before the edit — for undo, and for hosts that diff. */
209
+ previous: Partial<GanttTask>;
210
+ source: 'drag' | 'keyboard' | 'undo' | 'redo';
211
+ }
212
+ /** Where the user asked for a new task. */
213
+ interface AddTaskRequest {
214
+ /** Parent for the new task (`null` = top level). */
215
+ parentId?: string | null;
216
+ /** Insert after this task (the host decides ordering). */
217
+ afterId?: string;
218
+ /** Suggested start date (ISO). */
219
+ start?: string;
220
+ }
221
+ interface GanttProps extends WatermarkHostProps {
222
+ tasks: GanttTask[];
223
+ /** Shown in the toolbar and used as the accessible name. */
224
+ title?: string;
225
+ zoom?: Zoom;
226
+ defaultZoom?: Zoom;
227
+ onZoomChange?: (zoom: Zoom) => void;
228
+ selectedId?: string | null;
229
+ /** `info.multi` is true when Ctrl or Cmd was held (hosts that keep a multi-selection). */
230
+ onSelect?: (id: string | null, info: SelectInfo) => void;
231
+ /** Enter key or double-click on a row — open the record. */
232
+ onOpen?: (task: GanttTask) => void;
233
+ /** Override "today" (ISO) — useful for demos and tests. */
234
+ today?: string;
235
+ showToolbar?: boolean;
236
+ /** Baseline bars; the toolbar toggle controls this when uncontrolled. */
237
+ showBaseline?: boolean;
238
+ /** Height of the whole control. Default fills its container. */
239
+ height?: number | string;
240
+ /** `light`, `dark`, or a partial theme layered over light. */
241
+ theme?: 'light' | 'dark' | Partial<ComponentTheme>;
242
+ /** Shortcut for the most common customisation. */
243
+ accent?: string;
244
+ /**
245
+ * Text size, 1 = 100 % (0.75–2). Every label, value and title scales together, and rows grow
246
+ * with it. The PCF's "Text size" input (percent) sets it.
247
+ */
248
+ textScale?: number;
249
+ /** Per-role sizes at 100 % in px, e.g. `{ body: 14 }`; the scale still applies. */
250
+ textSizes?: TextSizes;
251
+ /** Ninth Forge licence key. Missing keys show a small watermark; nothing breaks. */
252
+ /** An action offered when there's nothing to show, e.g. "Load sample data" (PCF on a Ninth Forge sample table). */
253
+ emptyAction?: EmptyAction;
254
+ /** One or more keys; or a promise of them (the PCF reading its licence-key environment variable). */
255
+ licenseKey?: string | PromiseLike<string | undefined>;
256
+ /** Host tenant (PCF: context.orgSettings.tenantId). */
257
+ tenantId?: string;
258
+ /**
259
+ * Show the small "renew within N days" notice while the licence is in its 14-day grace
260
+ * period. The licensing spec shows it to makers only: the PCF passes `true` only for makers
261
+ * (Studio in canvas, an admin or customiser in model-driven). Default `true`; turn it off in a
262
+ * code app your end users see.
263
+ */
264
+ renewalNotice?: boolean;
265
+ /**
266
+ * Grid columns beside the task name. Users can change them from the toolbar's
267
+ * Columns menu; `onColumnsChange` reports that.
268
+ */
269
+ columns?: GanttColumns;
270
+ onColumnsChange?: (columns: Required<GanttColumns>) => void;
271
+ /** Initial width of the task grid in px. Users can drag the divider; `onGridWidthChange` reports it. */
272
+ gridWidth?: number;
273
+ onGridWidthChange?: (width: number) => void;
274
+ /** Show the toolbar filter box (name or owner). Default true. */
275
+ showFilter?: boolean;
276
+ className?: string;
277
+ /**
278
+ * Editing on the chart. Needs `onTaskChange` (and `onAddTask` for adding).
279
+ * Default: everything on when the handlers are given.
280
+ */
281
+ editable?: Editable;
282
+ /**
283
+ * Called for every edit. Return a promise for an optimistic save: the chart keeps
284
+ * the edit while it's pending, and reverts with a toast if the promise rejects.
285
+ */
286
+ onTaskChange?: (change: GanttChange) => void | Promise<void>;
287
+ /** "+ Add task", Insert / Ctrl+Insert, and the row menu. */
288
+ onAddTask?: (request: AddTaskRequest) => void;
289
+ /** Each task has at most one predecessor (e.g. a single lookup column); linking replaces it after a confirm. */
290
+ singlePredecessor?: boolean;
291
+ /**
292
+ * Which days are working days. `mon-fri` (default): durations count working days, and drags or
293
+ * keyboard nudges that land on a weekend snap to the nearest working day in the direction of travel.
294
+ * `all`: every day counts and nothing snaps. Weekends are shaded either way.
295
+ */
296
+ workingWeek?: 'mon-fri' | 'all';
297
+ /**
298
+ * Text direction. Default: inherited from the page. With `rtl` the task list, toolbar and
299
+ * tooltips mirror; the timeline keeps time running left to right.
300
+ */
301
+ dir?: 'ltr' | 'rtl';
302
+ /** The host is still loading rows: the empty state says "Loading tasks…" instead. */
303
+ loading?: boolean;
304
+ /**
305
+ * "Copy diagnostics" in the Export menu: copies what support needs (version, licence state,
306
+ * host, browser, locale, time zone, mapped property names, row count; never data values or
307
+ * the key). A function adds what the host knows, e.g. the PCF's control name and mapped
308
+ * properties. `false` hides the item. Default: shown.
309
+ */
310
+ diagnostics?: false | (() => DiagnosticsHostInfo);
311
+ /**
312
+ * `false` hides the Export menu (print, PNG and Copy diagnostics), e.g. in Power BI, whose
313
+ * sandbox blocks downloads, new windows and usually the clipboard. Default true.
314
+ */
315
+ exports?: boolean;
316
+ /** `none`: no built-in tooltip (the host shows its own from `onHover`). Default `built-in`. */
317
+ tooltip?: 'built-in' | 'none';
318
+ /**
319
+ * The pointer moved over a task row, or the keyboard moved to one (at its bar). Called once
320
+ * with `null` on leaving the rows.
321
+ */
322
+ onHover?: (task: GanttTask | null, at: PointerInfo) => void;
323
+ /** Task ids kept at full strength; the rest dim. Empty or undefined: nothing dims. */
324
+ highlighted?: readonly string[];
325
+ /**
326
+ * Right-click, the Menu key or Shift+F10 on a task. When set, the browser's menu and the
327
+ * chart's own row menu don't open.
328
+ */
329
+ onContextMenu?: (task: GanttTask, at: PointerInfo) => void;
330
+ /** Formats numbers the chart shows: % done. */
331
+ formatValue?: HostFormat<GanttFormatContext>;
332
+ /** Formats dates the chart shows (grid columns and the tooltip). */
333
+ formatDate?: (iso: string) => string;
334
+ /** Colours from the host (a report theme, or high contrast). */
335
+ colors?: Partial<GanttColors>;
336
+ /** `false`: no entrance animation or transitions (export snapshots). Default true. */
337
+ animate?: boolean;
338
+ }
339
+ /** What `formatValue` is formatting. */
340
+ interface GanttFormatContext {
341
+ field: 'progress';
342
+ use: 'grid' | 'tooltip';
343
+ }
344
+ /**
345
+ * Host colours. `foreground` and `background` (high contrast) replace every grey: lines,
346
+ * muted text, shading and the tooltip.
347
+ */
348
+ interface GanttColors {
349
+ bar: string;
350
+ progress: string;
351
+ phase: string;
352
+ milestone: string;
353
+ foreground: string;
354
+ background: string;
355
+ }
356
+
357
+ declare function Gantt(props: GanttProps): React.JSX.Element;
358
+
359
+ declare const ROW_H = 40;
360
+ declare const HEADER_H = 52;
361
+ interface Row {
362
+ task: GanttTask;
363
+ kind: GanttTaskKind;
364
+ depth: number;
365
+ index: number;
366
+ hasChildren: boolean;
367
+ collapsed: boolean;
368
+ /** Day numbers; NaN when the task has no usable dates. */
369
+ start: number;
370
+ end: number;
371
+ baselineStart: number;
372
+ baselineEnd: number;
373
+ /** 0–100; phases roll up their children weighted by duration. */
374
+ progress: number;
375
+ }
376
+ /** Flatten the task tree into visible rows, deriving phase spans from their children. */
377
+ declare function buildRows(tasks: GanttTask[], collapsed?: ReadonlySet<string>): Row[];
378
+ /** First and last day that any row (or baseline) touches. */
379
+ declare function rowsRange(rows: Row[]): {
380
+ from: number;
381
+ to: number;
382
+ } | null;
383
+ interface BarGeometry {
384
+ left: number;
385
+ width: number;
386
+ }
387
+ /** Inclusive end: a task from day 4 to day 15 covers 12 day-widths. */
388
+ declare function barGeometry(start: number, end: number, scale: TimeScale): BarGeometry | null;
389
+ interface DependencyLink {
390
+ /** `from>to` */
391
+ key: string;
392
+ from: string;
393
+ to: string;
394
+ /** Row indexes of the two ends (for culling to the visible range). */
395
+ fromIndex: number;
396
+ toIndex: number;
397
+ /** Connector line. */
398
+ d: string;
399
+ /** Arrowhead. */
400
+ arrow: string;
401
+ /** Same route minus both ends, for a wide invisible hit stroke that never covers bar handles. */
402
+ hit: string;
403
+ }
404
+ /**
405
+ * Finish-to-start connectors, one per link. With `window` (row indexes), only links whose
406
+ * vertical span crosses it are built — big plans then pay for what's on screen, not 5,000 paths.
407
+ */
408
+ declare function dependencyLinks(rows: Row[], scale: TimeScale, rowH?: number, window?: {
409
+ first: number;
410
+ last: number;
411
+ }): DependencyLink[];
412
+ /** Finish-to-start connectors as one SVG path plus arrowheads. */
413
+ declare function dependencyPaths(rows: Row[], scale: TimeScale, rowH?: number): {
414
+ lines: string;
415
+ arrows: string;
416
+ };
417
+
418
+ /**
419
+ * Editing logic for the Gantt — pure functions, no React, so node:test covers them.
420
+ * Days are day numbers (see @ninthforge/core); tasks carry ISO `YYYY-MM-DD` strings.
421
+ */
422
+
423
+ /**
424
+ * Finish-to-start links whose successor starts too early, keyed `from>to`.
425
+ * After a task, the successor must start the next day or later. A milestone is a point
426
+ * in time, so its successor may start on the milestone's date.
427
+ */
428
+ declare function brokenLinks(rows: Row[]): Set<string>;
429
+ type LinkCheck = 'ok' | 'self' | 'cycle' | 'exists';
430
+ /** Can `toId` be made to depend on `fromId`? Refuses self-links and cycles. */
431
+ declare function validateLink(tasks: GanttTask[], fromId: string, toId: string): LinkCheck;
432
+
433
+ /** Realistic sample plan used by the demo, docs and tests. */
434
+ declare const sampleTasks: GanttTask[];
435
+
436
+ /** This package's version, for Copy diagnostics. `version.test.ts` keeps it equal to package.json. */
437
+ declare const VERSION = "1.0.0-rc.1";
438
+
439
+ export { type AddTaskRequest, type DependencyLink, type EditAction, type Editable, Gantt, type GanttChange, type GanttColors, type GanttColumn, type GanttColumns, type GanttFormatContext, type GanttProps, type GanttTask, type GanttTaskKind, HEADER_H, ROW_H, type Row, VERSION, type Zoom, barGeometry, brokenLinks, buildRows, dependencyLinks, dependencyPaths, rowsRange, sampleTasks, validateLink };