construction-gantt 0.1.0 → 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/CHANGELOG.md +71 -0
- package/LICENSE.md +27 -0
- package/dist/index.cjs +761 -77
- package/dist/index.d.cts +31 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +31 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +763 -79
- package/dist/{pdf-CBaoJRTI.js → pdf-BsFqo2UW.js} +1 -1
- package/dist/{pdf-CAQDrX0w.cjs → pdf-N2LwD-_F.cjs} +1 -1
- package/dist/{png-C8t74695.cjs → png-BloW1DDl.cjs} +1 -0
- package/dist/{png-DKZeKnRh.js → png-C2poOLQo.js} +1 -0
- package/package.json +2 -2
- package/src/Gantt.css +77 -0
- package/src/Gantt.tsx +564 -49
- package/src/editing/dragLink.ts +47 -0
- package/src/editing/useEditState.ts +98 -0
- package/src/editing/usePreviewEngine.ts +47 -0
- package/src/export/offscreen.tsx +2 -0
- package/src/export/pdf.ts +1 -0
- package/src/export/png.ts +1 -0
- package/src/index.ts +1 -0
- package/src/mspdi/parse.ts +37 -3
- package/src/mspdi/serialize.ts +50 -2
- package/src/visibility.ts +3 -5
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Link, TaskId } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export type DragLinkState =
|
|
4
|
+
| { status: 'idle' }
|
|
5
|
+
| {
|
|
6
|
+
status: 'dragging';
|
|
7
|
+
sourceId: TaskId;
|
|
8
|
+
startX: number;
|
|
9
|
+
startY: number;
|
|
10
|
+
cursorX: number;
|
|
11
|
+
cursorY: number;
|
|
12
|
+
}
|
|
13
|
+
| { status: 'dropped'; sourceId: TaskId; targetId: TaskId };
|
|
14
|
+
|
|
15
|
+
export const DRAG_INITIAL: DragLinkState = { status: 'idle' };
|
|
16
|
+
|
|
17
|
+
export function startDrag(sourceId: TaskId, startX: number, startY: number): DragLinkState {
|
|
18
|
+
return { status: 'dragging', sourceId, startX, startY, cursorX: startX, cursorY: startY };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function moveDrag(state: DragLinkState, cursorX: number, cursorY: number): DragLinkState {
|
|
22
|
+
if (state.status !== 'dragging') return state;
|
|
23
|
+
return { ...state, cursorX, cursorY };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function completeDrop(state: DragLinkState, targetId: TaskId | null): DragLinkState {
|
|
27
|
+
if (state.status !== 'dragging') return DRAG_INITIAL;
|
|
28
|
+
if (targetId === null) return DRAG_INITIAL;
|
|
29
|
+
return { status: 'dropped', sourceId: state.sourceId, targetId };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function cancelDrag(_state: DragLinkState): DragLinkState {
|
|
33
|
+
return DRAG_INITIAL;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isDragInvalid(
|
|
37
|
+
sourceId: TaskId,
|
|
38
|
+
targetId: TaskId,
|
|
39
|
+
existingLinks: Link[],
|
|
40
|
+
summaryIds: Set<TaskId>,
|
|
41
|
+
): boolean {
|
|
42
|
+
if (String(sourceId) === String(targetId)) return true;
|
|
43
|
+
if ([...summaryIds].some((id) => String(id) === String(targetId))) return true;
|
|
44
|
+
return existingLinks.some(
|
|
45
|
+
(l) => String(l.source) === String(sourceId) && String(l.target) === String(targetId),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from 'react';
|
|
2
|
+
// biome-ignore lint/correctness/noUnusedImports: ScheduleMode is implicitly used in TaskEditPatch return type
|
|
3
|
+
import type { ScheduleMode, Task, TaskId } from '../types.js';
|
|
4
|
+
|
|
5
|
+
export type EditableField = 'text' | 'start' | 'end' | 'duration' | 'progress' | 'scheduleMode';
|
|
6
|
+
|
|
7
|
+
export interface ActiveCell {
|
|
8
|
+
taskId: TaskId;
|
|
9
|
+
field: EditableField;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type TaskEditPatch = Partial<
|
|
13
|
+
Pick<Task, 'text' | 'start' | 'end' | 'duration' | 'progress' | 'scheduleMode'>
|
|
14
|
+
>;
|
|
15
|
+
|
|
16
|
+
export interface EditState {
|
|
17
|
+
activeCell: ActiveCell | null;
|
|
18
|
+
dirtyValue: string;
|
|
19
|
+
activateCell(taskId: TaskId, field: EditableField, initialValue: string): void;
|
|
20
|
+
setValue(value: string): void;
|
|
21
|
+
commitCell(onTaskEdit?: (id: TaskId, patch: TaskEditPatch) => void): void;
|
|
22
|
+
cancelCell(): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function parseFieldValue(field: EditableField, value: string): TaskEditPatch {
|
|
26
|
+
switch (field) {
|
|
27
|
+
case 'text':
|
|
28
|
+
return { text: value };
|
|
29
|
+
case 'start': {
|
|
30
|
+
const d = new Date(`${value}T08:00:00`);
|
|
31
|
+
if (Number.isNaN(d.getTime())) return {};
|
|
32
|
+
return { start: d };
|
|
33
|
+
}
|
|
34
|
+
case 'end': {
|
|
35
|
+
const d = new Date(`${value}T08:00:00`);
|
|
36
|
+
if (Number.isNaN(d.getTime())) return {};
|
|
37
|
+
return { end: d };
|
|
38
|
+
}
|
|
39
|
+
case 'duration': {
|
|
40
|
+
const n = Number(value);
|
|
41
|
+
if (!Number.isFinite(n) || n < 0) return {};
|
|
42
|
+
return { duration: Math.round(n * 8 * 60) };
|
|
43
|
+
}
|
|
44
|
+
case 'progress': {
|
|
45
|
+
const n = Number(value);
|
|
46
|
+
if (!Number.isFinite(n)) return {};
|
|
47
|
+
return { progress: Math.min(100, Math.max(0, n)) };
|
|
48
|
+
}
|
|
49
|
+
case 'scheduleMode': {
|
|
50
|
+
if (value !== 'auto' && value !== 'manual') return {};
|
|
51
|
+
return { scheduleMode: value };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface EditStateInternal {
|
|
57
|
+
activeCell: ActiveCell | null;
|
|
58
|
+
dirtyValue: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function useEditState(): EditState {
|
|
62
|
+
const [state, setState] = useState<EditStateInternal>({
|
|
63
|
+
activeCell: null,
|
|
64
|
+
dirtyValue: '',
|
|
65
|
+
});
|
|
66
|
+
// stateRef lets commitCell read the latest state without stale closures.
|
|
67
|
+
const stateRef = useRef(state);
|
|
68
|
+
stateRef.current = state;
|
|
69
|
+
|
|
70
|
+
const activateCell = useCallback((taskId: TaskId, field: EditableField, initialValue: string) => {
|
|
71
|
+
setState({ activeCell: { taskId, field }, dirtyValue: initialValue });
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
const setValue = useCallback((value: string) => {
|
|
75
|
+
setState((prev) => ({ ...prev, dirtyValue: value }));
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const commitCell = useCallback((onTaskEdit?: (id: TaskId, patch: TaskEditPatch) => void) => {
|
|
79
|
+
const { activeCell, dirtyValue } = stateRef.current;
|
|
80
|
+
if (activeCell === null) return;
|
|
81
|
+
const patch = parseFieldValue(activeCell.field, dirtyValue);
|
|
82
|
+
onTaskEdit?.(activeCell.taskId, patch);
|
|
83
|
+
setState({ activeCell: null, dirtyValue: '' });
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
const cancelCell = useCallback(() => {
|
|
87
|
+
setState({ activeCell: null, dirtyValue: '' });
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
activeCell: state.activeCell,
|
|
92
|
+
dirtyValue: state.dirtyValue,
|
|
93
|
+
activateCell,
|
|
94
|
+
setValue,
|
|
95
|
+
commitCell,
|
|
96
|
+
cancelCell,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { schedule } from '../schedule.js';
|
|
3
|
+
import type { Project } from '../types.js';
|
|
4
|
+
import { type ActiveCell, parseFieldValue } from './useEditState.js';
|
|
5
|
+
|
|
6
|
+
export function usePreviewEngine(
|
|
7
|
+
committed: Project,
|
|
8
|
+
activeCell: ActiveCell | null,
|
|
9
|
+
dirtyValue: string,
|
|
10
|
+
debounceMs = 80,
|
|
11
|
+
): Project | null {
|
|
12
|
+
const [ghostProject, setGhostProject] = useState<Project | null>(null);
|
|
13
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const CPM_IRRELEVANT_FIELDS = new Set(['text', 'progress', 'scheduleMode']);
|
|
17
|
+
if (activeCell === null || CPM_IRRELEVANT_FIELDS.has(activeCell.field)) {
|
|
18
|
+
setGhostProject(null);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (timerRef.current !== null) clearTimeout(timerRef.current);
|
|
23
|
+
|
|
24
|
+
timerRef.current = setTimeout(() => {
|
|
25
|
+
const patch = parseFieldValue(activeCell.field, dirtyValue);
|
|
26
|
+
if (Object.keys(patch).length === 0) {
|
|
27
|
+
setGhostProject(null); // invalid input — suppress ghost
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const patchedTasks = committed.tasks.map((t) =>
|
|
31
|
+
t.id === activeCell.taskId ? { ...t, ...patch } : t,
|
|
32
|
+
);
|
|
33
|
+
setGhostProject(schedule({ ...committed, tasks: patchedTasks }));
|
|
34
|
+
}, debounceMs);
|
|
35
|
+
|
|
36
|
+
return () => {
|
|
37
|
+
if (timerRef.current !== null) clearTimeout(timerRef.current);
|
|
38
|
+
};
|
|
39
|
+
}, [committed, activeCell, dirtyValue, debounceMs]);
|
|
40
|
+
|
|
41
|
+
// Clear immediately when cell deactivates (don't wait for debounce timeout).
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (activeCell === null) setGhostProject(null);
|
|
44
|
+
}, [activeCell]);
|
|
45
|
+
|
|
46
|
+
return ghostProject;
|
|
47
|
+
}
|
package/src/export/offscreen.tsx
CHANGED
|
@@ -14,6 +14,7 @@ export type OffscreenGanttProps = Pick<
|
|
|
14
14
|
| 'cellHeight'
|
|
15
15
|
| 'markers'
|
|
16
16
|
| 'baselineIndex'
|
|
17
|
+
| 'baselineIndices'
|
|
17
18
|
| 'showBaselineBars'
|
|
18
19
|
| 'columns'
|
|
19
20
|
| 'height'
|
|
@@ -50,6 +51,7 @@ export async function renderOffscreen(args: {
|
|
|
50
51
|
cellHeight={ganttProps.cellHeight}
|
|
51
52
|
markers={ganttProps.markers}
|
|
52
53
|
baselineIndex={ganttProps.baselineIndex}
|
|
54
|
+
baselineIndices={ganttProps.baselineIndices}
|
|
53
55
|
showBaselineBars={ganttProps.showBaselineBars}
|
|
54
56
|
columns={ganttProps.columns}
|
|
55
57
|
visibleTaskIds={ganttProps.visibleTaskIds}
|
package/src/export/pdf.ts
CHANGED
package/src/export/png.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ export {
|
|
|
33
33
|
} from './editing/factories.js';
|
|
34
34
|
export type { EditableProject } from './editing/use-editable-project.js';
|
|
35
35
|
export { useEditableProject } from './editing/use-editable-project.js';
|
|
36
|
+
export type { EditableField, TaskEditPatch } from './editing/useEditState.js';
|
|
36
37
|
export type {
|
|
37
38
|
GanttHandle,
|
|
38
39
|
PdfExportOptions,
|
package/src/mspdi/parse.ts
CHANGED
|
@@ -40,6 +40,7 @@ const KNOWN_TASK_FIELDS = new Set([
|
|
|
40
40
|
'Finish',
|
|
41
41
|
'Duration',
|
|
42
42
|
'ConstraintType',
|
|
43
|
+
'ConstraintDate',
|
|
43
44
|
'Milestone',
|
|
44
45
|
'Summary',
|
|
45
46
|
'OutlineLevel',
|
|
@@ -238,6 +239,7 @@ export function parseMspdi(xml: string): MspdiParseResult {
|
|
|
238
239
|
|
|
239
240
|
const tasks: Task[] = [];
|
|
240
241
|
const links: Link[] = [];
|
|
242
|
+
const outlineStack: TaskId[] = [];
|
|
241
243
|
// Per-task baseline snapshots, keyed by baseline Number (BaselineIndex).
|
|
242
244
|
// Flattened into project.baselines below.
|
|
243
245
|
const baselineAccum = new Map<BaselineIndex, Map<TaskId, BaselineTaskSnapshot>>();
|
|
@@ -263,8 +265,11 @@ export function parseMspdi(xml: string): MspdiParseResult {
|
|
|
263
265
|
const isMilestone = String(raw.Milestone ?? '0') === '1';
|
|
264
266
|
const isSummary = String(raw.Summary ?? '0') === '1';
|
|
265
267
|
const taskType: TaskType = isSummary ? 'summary' : isMilestone ? 'milestone' : 'task';
|
|
268
|
+
const outlineLevel = Math.max(1, Number(raw.OutlineLevel ?? '1'));
|
|
269
|
+
const parentId = outlineLevel > 1 ? outlineStack[outlineLevel - 2] : undefined;
|
|
270
|
+
const constraint = parseConstraint(raw);
|
|
266
271
|
|
|
267
|
-
|
|
272
|
+
const parsedTask: Task = {
|
|
268
273
|
id: uid,
|
|
269
274
|
text: name,
|
|
270
275
|
type: taskType,
|
|
@@ -272,8 +277,14 @@ export function parseMspdi(xml: string): MspdiParseResult {
|
|
|
272
277
|
duration,
|
|
273
278
|
start,
|
|
274
279
|
end,
|
|
275
|
-
progress: 0,
|
|
276
|
-
}
|
|
280
|
+
progress: Number(raw.PercentComplete ?? '0'),
|
|
281
|
+
};
|
|
282
|
+
if (parentId !== undefined) parsedTask.parent = parentId;
|
|
283
|
+
if (constraint !== undefined) parsedTask.constraint = constraint;
|
|
284
|
+
tasks.push(parsedTask);
|
|
285
|
+
|
|
286
|
+
outlineStack[outlineLevel - 1] = uid;
|
|
287
|
+
outlineStack.length = outlineLevel;
|
|
277
288
|
|
|
278
289
|
// Walk predecessor links nested inside the task.
|
|
279
290
|
const preds: unknown[] = Array.isArray(raw.PredecessorLink)
|
|
@@ -705,10 +716,33 @@ const MSPDI_TYPE_TO_DEPENDENCY: Record<number, DependencyType> = {
|
|
|
705
716
|
3: 'SS',
|
|
706
717
|
};
|
|
707
718
|
|
|
719
|
+
const MSPDI_CONSTRAINT_TO_INTERNAL = {
|
|
720
|
+
0: 'ASAP',
|
|
721
|
+
1: 'ALAP',
|
|
722
|
+
2: 'MSO',
|
|
723
|
+
3: 'MFO',
|
|
724
|
+
4: 'SNET',
|
|
725
|
+
5: 'SNLT',
|
|
726
|
+
6: 'FNET',
|
|
727
|
+
7: 'FNLT',
|
|
728
|
+
} as const;
|
|
729
|
+
|
|
708
730
|
function mspdiTypeToDependencyType(t: number): DependencyType {
|
|
709
731
|
return MSPDI_TYPE_TO_DEPENDENCY[t] ?? 'FS';
|
|
710
732
|
}
|
|
711
733
|
|
|
734
|
+
function parseConstraint(raw: Record<string, unknown>): Task['constraint'] {
|
|
735
|
+
const value = Number(raw.ConstraintType ?? '0');
|
|
736
|
+
const type = MSPDI_CONSTRAINT_TO_INTERNAL[value as keyof typeof MSPDI_CONSTRAINT_TO_INTERNAL];
|
|
737
|
+
if (!type || type === 'ASAP') return undefined;
|
|
738
|
+
|
|
739
|
+
const constraint: NonNullable<Task['constraint']> = { type };
|
|
740
|
+
if (raw.ConstraintDate !== undefined && raw.ConstraintDate !== '') {
|
|
741
|
+
constraint.date = parseMspdiDate(String(raw.ConstraintDate));
|
|
742
|
+
}
|
|
743
|
+
return constraint;
|
|
744
|
+
}
|
|
745
|
+
|
|
712
746
|
function parseMspdiDate(s: string): Date {
|
|
713
747
|
// MSPDI emits ISO 8601 like `2026-01-05T08:00:00` (no timezone in
|
|
714
748
|
// practice; MS Project writes local time). We treat it as local.
|
package/src/mspdi/serialize.ts
CHANGED
|
@@ -8,10 +8,12 @@ import type {
|
|
|
8
8
|
Baseline,
|
|
9
9
|
Calendar,
|
|
10
10
|
CalendarException,
|
|
11
|
+
ConstraintType,
|
|
11
12
|
DependencyType,
|
|
12
13
|
Link,
|
|
13
14
|
Project,
|
|
14
15
|
Resource,
|
|
16
|
+
Task,
|
|
15
17
|
WorkInterval,
|
|
16
18
|
} from '../types.js';
|
|
17
19
|
import type { MspdiSerializeOptions } from './types.js';
|
|
@@ -37,9 +39,11 @@ interface MspdiTaskOut {
|
|
|
37
39
|
Finish: string;
|
|
38
40
|
Duration: string;
|
|
39
41
|
ConstraintType: number;
|
|
42
|
+
ConstraintDate?: string;
|
|
40
43
|
Milestone: number;
|
|
41
44
|
Summary: number;
|
|
42
45
|
OutlineLevel: number;
|
|
46
|
+
PercentComplete: number;
|
|
43
47
|
PredecessorLink?: MspdiPredecessorLinkOut[];
|
|
44
48
|
Baseline?: MspdiTaskBaselineOut[];
|
|
45
49
|
}
|
|
@@ -112,6 +116,7 @@ export function serializeMspdi(project: Project, options: MspdiSerializeOptions
|
|
|
112
116
|
// Group baselines by taskId. Each task may have one snapshot per baseline
|
|
113
117
|
// (0-10), emitted as <Baseline> children nested inside the task.
|
|
114
118
|
const baselinesByTask = buildBaselinesByTask(project.baselines);
|
|
119
|
+
const outlineLevelByTask = buildOutlineLevels(project.tasks);
|
|
115
120
|
|
|
116
121
|
const tasksOut: MspdiTaskOut[] = project.tasks.map((t, idx) => {
|
|
117
122
|
const taskOut: MspdiTaskOut = {
|
|
@@ -121,11 +126,15 @@ export function serializeMspdi(project: Project, options: MspdiSerializeOptions
|
|
|
121
126
|
Start: formatMspdiDate(t.start),
|
|
122
127
|
Finish: formatMspdiDate(t.end),
|
|
123
128
|
Duration: formatMspdiDuration(t.duration),
|
|
124
|
-
ConstraintType:
|
|
129
|
+
ConstraintType: constraintTypeToMspdi(t.constraint?.type),
|
|
125
130
|
Milestone: t.type === 'milestone' ? 1 : 0,
|
|
126
131
|
Summary: t.type === 'summary' ? 1 : 0,
|
|
127
|
-
OutlineLevel: 1,
|
|
132
|
+
OutlineLevel: outlineLevelByTask.get(t.id) ?? 1,
|
|
133
|
+
PercentComplete: t.progress,
|
|
128
134
|
};
|
|
135
|
+
if (t.constraint?.date) {
|
|
136
|
+
taskOut.ConstraintDate = formatMspdiDate(t.constraint.date);
|
|
137
|
+
}
|
|
129
138
|
|
|
130
139
|
const incoming = linksByTarget.get(String(t.id));
|
|
131
140
|
if (incoming?.length) {
|
|
@@ -199,6 +208,45 @@ function dependencyTypeToMspdi(t: DependencyType): number {
|
|
|
199
208
|
}
|
|
200
209
|
}
|
|
201
210
|
|
|
211
|
+
function constraintTypeToMspdi(t: ConstraintType | undefined): number {
|
|
212
|
+
switch (t) {
|
|
213
|
+
case undefined:
|
|
214
|
+
case 'ASAP':
|
|
215
|
+
return 0;
|
|
216
|
+
case 'ALAP':
|
|
217
|
+
return 1;
|
|
218
|
+
case 'MSO':
|
|
219
|
+
return 2;
|
|
220
|
+
case 'MFO':
|
|
221
|
+
return 3;
|
|
222
|
+
case 'SNET':
|
|
223
|
+
return 4;
|
|
224
|
+
case 'SNLT':
|
|
225
|
+
return 5;
|
|
226
|
+
case 'FNET':
|
|
227
|
+
return 6;
|
|
228
|
+
case 'FNLT':
|
|
229
|
+
return 7;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function buildOutlineLevels(tasks: Task[]): Map<Task['id'], number> {
|
|
234
|
+
const parentById = new Map(tasks.map((t) => [t.id, t.parent]));
|
|
235
|
+
const cache = new Map<Task['id'], number>();
|
|
236
|
+
|
|
237
|
+
function depth(taskId: Task['id']): number {
|
|
238
|
+
const cached = cache.get(taskId);
|
|
239
|
+
if (cached !== undefined) return cached;
|
|
240
|
+
|
|
241
|
+
const parentId = parentById.get(taskId);
|
|
242
|
+
const value = parentId === undefined ? 1 : depth(parentId) + 1;
|
|
243
|
+
cache.set(taskId, value);
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return new Map(tasks.map((t) => [t.id, depth(t.id)]));
|
|
248
|
+
}
|
|
249
|
+
|
|
202
250
|
function formatMspdiDate(d: Date): string {
|
|
203
251
|
// MSPDI emits local time without timezone (e.g. `2026-01-05T08:00:00`).
|
|
204
252
|
const pad = (n: number): string => String(n).padStart(2, '0');
|
package/src/visibility.ts
CHANGED
|
@@ -5,11 +5,9 @@
|
|
|
5
5
|
// This is the canonical example: the engine always runs on the full task
|
|
6
6
|
// set; the visibility filter applies only to the rendered output.
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// task's early-start collapse to 0
|
|
11
|
-
// by structure — the filter sits AFTER schedule() in the pipeline, so
|
|
12
|
-
// computed fields on the visible tasks reflect the full schedule.
|
|
8
|
+
// The filter sits AFTER schedule() in the pipeline so computed fields on
|
|
9
|
+
// the visible tasks reflect the full schedule — hiding a predecessor must
|
|
10
|
+
// not cause a dependent task's early-start to collapse to 0.
|
|
13
11
|
|
|
14
12
|
import type { Task, TaskId } from './types.js';
|
|
15
13
|
|