@jaeungkim/gantt-chart 0.3.1 → 0.4.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/LICENSE +21 -0
- package/README.md +60 -101
- package/dist/components/GanttBar.d.ts +23 -0
- package/dist/components/GanttChartHeader.d.ts +17 -0
- package/dist/components/GanttDependencyArrows.d.ts +15 -0
- package/dist/components/GanttDragGuides.d.ts +9 -0
- package/dist/components/GanttGridSplitter.d.ts +14 -0
- package/dist/components/GanttMarkers.d.ts +18 -0
- package/dist/components/GanttTaskGrid.d.ts +42 -0
- package/dist/components/ScaleSelector.d.ts +11 -0
- package/dist/constants/gantt.d.ts +47 -0
- package/dist/core/calendar.d.ts +45 -0
- package/dist/core/criticalPath.d.ts +67 -0
- package/dist/core/dates.d.ts +32 -0
- package/dist/core/index.d.ts +16 -0
- package/dist/core/scheduling.d.ts +104 -0
- package/dist/core/tree.d.ts +42 -0
- package/dist/core/types.d.ts +76 -0
- package/dist/gantt-chart.css +1 -1
- package/dist/hooks/useGanttBarDrag.d.ts +35 -0
- package/dist/hooks/useGanttDrawCreate.d.ts +31 -0
- package/dist/hooks/useGanttExportApi.d.ts +30 -0
- package/dist/hooks/useGanttHistoryApi.d.ts +28 -0
- package/dist/hooks/useGanttLinkDrag.d.ts +31 -0
- package/dist/hooks/useGanttProgressDrag.d.ts +16 -0
- package/dist/hooks/useGanttRowDrag.d.ts +33 -0
- package/dist/hooks/useGanttScrollApi.d.ts +63 -0
- package/dist/hooks/useGanttSelectors.d.ts +23 -0
- package/dist/hooks/useGanttVirtualization.d.ts +32 -0
- package/dist/hooks/useResolvedTheme.d.ts +15 -0
- package/dist/index.cjs +4 -0
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -51
- package/dist/index.js +5819 -0
- package/dist/pages/Gantt.d.ts +306 -0
- package/dist/stores/context.d.ts +10 -0
- package/dist/stores/store.d.ts +118 -0
- package/dist/types/gantt.d.ts +283 -0
- package/dist/types/task.d.ts +111 -0
- package/dist/utils/a11y.d.ts +141 -0
- package/dist/utils/arrowPath.d.ts +66 -0
- package/dist/utils/dependency.d.ts +42 -0
- package/dist/utils/grouping.d.ts +81 -0
- package/dist/utils/headerUtils.d.ts +6 -0
- package/dist/utils/history.d.ts +56 -0
- package/dist/utils/i18n.d.ts +16 -0
- package/dist/utils/mutation.d.ts +47 -0
- package/dist/utils/pngExport.d.ts +67 -0
- package/dist/utils/pointerGesture.d.ts +31 -0
- package/dist/utils/rowDrag.d.ts +69 -0
- package/dist/utils/timeline.d.ts +161 -0
- package/dist/utils/transformData.d.ts +15 -0
- package/dist/utils/viewport.d.ts +95 -0
- package/package.json +47 -32
- package/dist/index.cjs.js +0 -4
- package/dist/index.es.js +0 -2394
- package/dist/readmeImg.png +0 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Dayjs } from 'dayjs';
|
|
2
|
+
import { WorkingCalendar } from './calendar';
|
|
3
|
+
import { DependencyType, Task } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* How far a predecessor's move carries into its successors.
|
|
6
|
+
*
|
|
7
|
+
* - `off` - nothing propagates (the default; a chart behaves exactly as it did before)
|
|
8
|
+
* - `shift-on-overlap` - a successor is pushed later only when a link would otherwise be
|
|
9
|
+
* broken, and is never pulled earlier
|
|
10
|
+
* - `maintain-gap` - a successor sits exactly at its earliest legal date, so it follows the
|
|
11
|
+
* predecessor in both directions and the gap stays equal to the link's lag
|
|
12
|
+
*/
|
|
13
|
+
export type SchedulingPolicy = 'off' | 'shift-on-overlap' | 'maintain-gap';
|
|
14
|
+
/** One dependency, with both ends resolved */
|
|
15
|
+
export interface SchedulingLink {
|
|
16
|
+
predecessorId: string;
|
|
17
|
+
successorId: string;
|
|
18
|
+
type: DependencyType;
|
|
19
|
+
/** Signed, in the calendar's day unit */
|
|
20
|
+
lag: number;
|
|
21
|
+
}
|
|
22
|
+
/** Stable identity for a link - used to tag the rendered arrow */
|
|
23
|
+
export declare function linkKey(link: SchedulingLink): string;
|
|
24
|
+
export interface TaskGraph {
|
|
25
|
+
byId: Map<string, Task>;
|
|
26
|
+
links: SchedulingLink[];
|
|
27
|
+
/** successor id -> the links that constrain it */
|
|
28
|
+
incoming: Map<string, SchedulingLink[]>;
|
|
29
|
+
/** predecessor id -> the links it constrains */
|
|
30
|
+
outgoing: Map<string, SchedulingLink[]>;
|
|
31
|
+
/** Topological order, predecessors first. Excludes anything caught in a cycle. */
|
|
32
|
+
order: string[];
|
|
33
|
+
/** Ids that could not be ordered because they sit on a cycle (null when there is none) */
|
|
34
|
+
cycle: string[] | null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Builds the dependency graph and topologically sorts it (Kahn).
|
|
38
|
+
*
|
|
39
|
+
* Links pointing at a task that is not in the data are dropped, and anything caught in a
|
|
40
|
+
* cycle is left out of `order` and reported in `cycle` - so every caller walks a finite,
|
|
41
|
+
* acyclic list no matter what the data says.
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildTaskGraph(tasks: Task[]): TaskGraph;
|
|
44
|
+
/**
|
|
45
|
+
* A dependency path from `fromId` to `toId`, or null when there is none.
|
|
46
|
+
* Walks successors, so the result reads predecessor-first.
|
|
47
|
+
*/
|
|
48
|
+
export declare function findPath(tasks: Task[], fromId: string, toId: string): string[] | null;
|
|
49
|
+
/**
|
|
50
|
+
* Whether a new predecessor -> successor link can be added without closing a loop.
|
|
51
|
+
*
|
|
52
|
+
* Call this before writing a link into the data: a cycle that never gets created is a
|
|
53
|
+
* cycle the engine never has to work around. `cycle` is the offending chain, ready to
|
|
54
|
+
* put in an error message.
|
|
55
|
+
*/
|
|
56
|
+
export declare function canLink(tasks: Task[], predecessorId: string, successorId: string): {
|
|
57
|
+
ok: boolean;
|
|
58
|
+
cycle: string[] | null;
|
|
59
|
+
};
|
|
60
|
+
/** The instant a task starts (milestones are a single point at startDate) */
|
|
61
|
+
export declare function taskStart(task: Task): Dayjs;
|
|
62
|
+
/** The instant a task finishes (milestones are a single point at startDate) */
|
|
63
|
+
export declare function taskEnd(task: Task): Dayjs;
|
|
64
|
+
/** The predecessor end of a link: FS and FF hang off the finish, SS and SF off the start */
|
|
65
|
+
export declare function linkSourceDate(link: SchedulingLink, predecessor: Task): Dayjs;
|
|
66
|
+
/** The successor end of a link: FS and SS constrain the start, FF and SF the finish */
|
|
67
|
+
export declare function linkTargetDate(link: SchedulingLink, successor: Task): Dayjs;
|
|
68
|
+
/**
|
|
69
|
+
* How many days the successor must move for this link to hold.
|
|
70
|
+
*
|
|
71
|
+
* Positive means it has to move later, negative means it is sitting later than it needs
|
|
72
|
+
* to. Whole days in the calendar's unit, so applying it keeps every time of day intact.
|
|
73
|
+
*/
|
|
74
|
+
export declare function linkDelta(link: SchedulingLink, predecessor: Task, successor: Task, calendar: WorkingCalendar): number;
|
|
75
|
+
/** Moves a task by whole days - both ends together, so its duration is untouched */
|
|
76
|
+
export declare function shiftTask(task: Task, days: number, calendar: WorkingCalendar): Task;
|
|
77
|
+
export interface ScheduleOptions {
|
|
78
|
+
policy?: SchedulingPolicy;
|
|
79
|
+
calendar?: WorkingCalendar;
|
|
80
|
+
/**
|
|
81
|
+
* The tasks that just moved. Only their successors are rescheduled, and the seeds
|
|
82
|
+
* themselves are left exactly where the caller put them.
|
|
83
|
+
* Omitted, the whole project is levelled.
|
|
84
|
+
*/
|
|
85
|
+
seeds?: Iterable<string>;
|
|
86
|
+
/** Pins summary rows - with hierarchy on their dates come from their children */
|
|
87
|
+
hierarchy?: boolean;
|
|
88
|
+
/** Called with the ids caught in a dependency cycle; those tasks are left alone */
|
|
89
|
+
onCycle?: (taskIds: string[]) => void;
|
|
90
|
+
}
|
|
91
|
+
export interface ScheduleResult {
|
|
92
|
+
/** The same array instance when nothing moved, so callers can skip the update */
|
|
93
|
+
tasks: Task[];
|
|
94
|
+
movedIds: string[];
|
|
95
|
+
cycle: string[] | null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Propagates a move through the dependency graph.
|
|
99
|
+
*
|
|
100
|
+
* One forward pass in topological order: each task is shifted by the largest delta its
|
|
101
|
+
* predecessors demand, then becomes the input for its own successors. Cycles are reported
|
|
102
|
+
* and skipped rather than followed, so this always terminates.
|
|
103
|
+
*/
|
|
104
|
+
export declare function scheduleTasks(tasks: Task[], options?: ScheduleOptions): ScheduleResult;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Task } from './types';
|
|
2
|
+
/** The minimum a task needs for the tree math - TaskTransformed fits as-is */
|
|
3
|
+
type TaskNode = Pick<Task, "id" | "parentId">;
|
|
4
|
+
/**
|
|
5
|
+
* A normalized tree built from parentId
|
|
6
|
+
*
|
|
7
|
+
* Orphans (a parent id that is not in the data), self-references and cyclic chains all get
|
|
8
|
+
* their parent link cut and become roots. The parentOf/childIds that come out are therefore
|
|
9
|
+
* always acyclic, so the functions below - and the render - can walk up or down without
|
|
10
|
+
* risking an infinite loop.
|
|
11
|
+
*/
|
|
12
|
+
export interface TaskTree {
|
|
13
|
+
/** parent id -> child ids (in input order) */
|
|
14
|
+
childIds: Map<string, string[]>;
|
|
15
|
+
/** task id -> normalized parent id (null for a root, an orphan or a cycle) */
|
|
16
|
+
parentOf: Map<string, string | null>;
|
|
17
|
+
/** task id -> depth from the root */
|
|
18
|
+
depthOf: Map<string, number>;
|
|
19
|
+
/** Root ids, in input order - the sibling list childIds has no key for */
|
|
20
|
+
rootIds: string[];
|
|
21
|
+
}
|
|
22
|
+
export declare function buildTaskTree(tasks: TaskNode[]): TaskTree;
|
|
23
|
+
/**
|
|
24
|
+
* The subtree's ids including the root itself (breadth first)
|
|
25
|
+
* An id that is not in the tree yields an empty array
|
|
26
|
+
*/
|
|
27
|
+
export declare function collectSubtreeIds(tasks: TaskNode[], rootId: string, tree?: TaskTree): string[];
|
|
28
|
+
/**
|
|
29
|
+
* The tasks left after dropping any task with a collapsed ancestor
|
|
30
|
+
* Input order is preserved (row order is decided by the sequence sort)
|
|
31
|
+
*/
|
|
32
|
+
export declare function getVisibleTasks<T extends TaskNode>(tasks: T[], collapsedIds: Iterable<string>, tree?: TaskTree): T[];
|
|
33
|
+
/**
|
|
34
|
+
* The tasks with every parent recomputed as a summary row
|
|
35
|
+
*
|
|
36
|
+
* Start and end always come from the children, never from what the data says
|
|
37
|
+
* (min(child start)..max(child end); a milestone child counts at its startDate alone).
|
|
38
|
+
* Deepest first, so a grandchild's move travels up through the parent and the grandparent.
|
|
39
|
+
* An explicit progress is left alone; only a missing one is rolled up from the children.
|
|
40
|
+
*/
|
|
41
|
+
export declare function rollUpTasks(tasks: Task[], tree?: TaskTree): Task[];
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The task data model - the part of it the headless core owns.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is plain data and pure functions: no React, no DOM, no pixels.
|
|
5
|
+
* `src/types/task.ts` re-exports these and adds the render-side types (bar geometry,
|
|
6
|
+
* arrow coordinates) on top.
|
|
7
|
+
*/
|
|
8
|
+
export type TaskType = 'task' | 'milestone';
|
|
9
|
+
export type DependencyType = 'FS' | 'SS' | 'FF' | 'SF';
|
|
10
|
+
export interface TaskDependency {
|
|
11
|
+
/** The predecessor's id - a task's `dependencies` list the tasks it waits on */
|
|
12
|
+
targetId: string;
|
|
13
|
+
type: DependencyType;
|
|
14
|
+
/**
|
|
15
|
+
* Signed delay between the two ends of the link, in days
|
|
16
|
+
*
|
|
17
|
+
* Positive is lag (wait this long after the predecessor), negative is lead (overlap).
|
|
18
|
+
* Counted in working days when the working-day calendar is on, calendar days otherwise.
|
|
19
|
+
*/
|
|
20
|
+
lag?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface Task {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
startDate: string;
|
|
26
|
+
endDate: string;
|
|
27
|
+
parentId: string | null;
|
|
28
|
+
sequence: string;
|
|
29
|
+
/** Task kind - 'milestone' renders as a diamond at startDate (default 'task') */
|
|
30
|
+
type?: TaskType;
|
|
31
|
+
/** Progress 0-100 (%) - omitted means no progress display */
|
|
32
|
+
progress?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Bar color - any CSS color value
|
|
35
|
+
*
|
|
36
|
+
* The progress fill and the hover shade are derived from it, so one value colors the
|
|
37
|
+
* whole bar. Omitted, the `--gantt-*` theme tokens decide as before.
|
|
38
|
+
*/
|
|
39
|
+
color?: string;
|
|
40
|
+
/** Extra class name put on this task's bar and its task-list row */
|
|
41
|
+
className?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Swimlane this task shares a row with
|
|
44
|
+
*
|
|
45
|
+
* Tasks with the same lane (inside the same group) are drawn side by side on one
|
|
46
|
+
* row; overlapping ones stack onto extra rows automatically. Omitted, the task
|
|
47
|
+
* gets a row of its own as before.
|
|
48
|
+
*/
|
|
49
|
+
lane?: string;
|
|
50
|
+
dependencies?: TaskDependency[];
|
|
51
|
+
/** Blocks every gesture on this task - overrides the chart's `readOnly` prop */
|
|
52
|
+
readOnly?: boolean;
|
|
53
|
+
/** Allows/blocks moving this task - overrides both `readOnly` settings */
|
|
54
|
+
allowMove?: boolean;
|
|
55
|
+
/** Allows/blocks resizing this task - overrides both `readOnly` settings */
|
|
56
|
+
allowResize?: boolean;
|
|
57
|
+
/** Allows/blocks dragging this task's progress handle - overrides both `readOnly` settings */
|
|
58
|
+
allowProgressChange?: boolean;
|
|
59
|
+
/** Allows/blocks starting a dependency drag from this task - overrides both `readOnly` settings */
|
|
60
|
+
allowLinkCreate?: boolean;
|
|
61
|
+
/** Allows/blocks deleting a dependency this task owns - overrides both `readOnly` settings */
|
|
62
|
+
allowLinkDelete?: boolean;
|
|
63
|
+
/** Earliest date this task may be dragged to (ISO string) - overrides the chart's `minDate` */
|
|
64
|
+
minDate?: string;
|
|
65
|
+
/** Latest date this task may be dragged to (ISO string) - overrides the chart's `maxDate` */
|
|
66
|
+
maxDate?: string;
|
|
67
|
+
/** The scheduling engine never moves this task; it still constrains its successors */
|
|
68
|
+
manuallyScheduled?: boolean;
|
|
69
|
+
/** Planned start snapshot - drawn as a thin bar under the live one (UTC ISO string) */
|
|
70
|
+
baselineStart?: string;
|
|
71
|
+
/** Planned end snapshot (UTC ISO string) */
|
|
72
|
+
baselineEnd?: string;
|
|
73
|
+
}
|
|
74
|
+
export declare function isMilestoneTask(task: Pick<Task, 'type'>): boolean;
|
|
75
|
+
/** Normalizes progress into the 0-100 range; null when the value is missing or invalid */
|
|
76
|
+
export declare function normalizeProgress(progress: number | undefined): number | null;
|
package/dist/gantt-chart.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
@import"https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&display=swap";:root{--font-sans: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--background: #fafafa;--foreground: #18181b;--muted: #f4f4f5;--muted-foreground: #71717a;--border: #e4e4e7;--border-subtle: rgba(0, 0, 0, .04);--bar-bg: #e4e4e7;--bar-bg-hover: #d4d4d8;--bar-text: #18181b;--bar-shadow: 0 1px 2px rgba(0, 0, 0, .05);--bar-shadow-hover: 0 4px 12px rgba(0, 0, 0, .1);--bar-shadow-drag: 0 8px 24px rgba(0, 0, 0, .15);--arrow: #a1a1aa;--accent: #3b82f6;--transition-fast: .15s ease;--transition-normal: .2s ease}.dark,[data-theme=dark]{--background: #09090b;--foreground: #fafafa;--muted: #18181b;--muted-foreground: #a1a1aa;--border: #27272a;--border-subtle: rgba(255, 255, 255, .04);--bar-bg: #27272a;--bar-bg-hover: #3f3f46;--bar-text: #fafafa;--bar-shadow: 0 1px 2px rgba(0, 0, 0, .2);--bar-shadow-hover: 0 4px 12px rgba(0, 0, 0, .3);--bar-shadow-drag: 0 8px 24px rgba(0, 0, 0, .4);--arrow: #71717a}.gantt-container{display:flex;flex-direction:column;overflow:hidden;background:var(--background);font-family:var(--font-sans);font-size:13px;line-height:1.5;color:var(--foreground);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.gantt-toolbar{flex-shrink:0;display:flex;align-items:center;justify-content:flex-end;padding:12px 16px;background:var(--background);border-bottom:1px solid var(--border)}.gantt-scale-selector{display:inline-flex}.gantt-scale-control{display:flex;gap:2px;padding:3px;background:var(--muted);border-radius:8px;border:1px solid var(--border)}.gantt-scale-button{position:relative;padding:6px 14px;font-family:inherit;font-size:12px;font-weight:500;color:var(--muted-foreground);background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all var(--transition-fast);text-transform:capitalize}.gantt-scale-button:hover{color:var(--foreground)}.gantt-scale-button:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.gantt-scale-button[data-active=true]{color:var(--foreground);background:var(--background);box-shadow:0 1px 3px #00000014}.gantt-main{flex:1;min-height:0;overflow:hidden}.gantt-scroll-container{width:100%;height:100%;overflow:auto}.gantt-scroll-container::-webkit-scrollbar{width:10px;height:10px}.gantt-scroll-container::-webkit-scrollbar-track{background:transparent}.gantt-scroll-container::-webkit-scrollbar-thumb{background:var(--border);border-radius:5px;border:2px solid var(--background)}.gantt-scroll-container::-webkit-scrollbar-thumb:hover{background:var(--muted-foreground)}.gantt-scroll-container::-webkit-scrollbar-corner{background:transparent}.gantt-header-wrapper{position:sticky;top:0;z-index:30}.gantt-header{position:relative;background:var(--background);border-bottom:1px solid var(--border)}.gantt-header-content{display:flex;flex-direction:column}.gantt-top-header{position:relative;display:flex;height:44px;border-bottom:1px solid var(--border-subtle)}.gantt-top-groups{display:flex}.gantt-top-group{display:flex;align-items:center;padding:0 16px;font-size:13px;font-weight:600;letter-spacing:-.01em;background:var(--background);color:var(--foreground)}.gantt-top-group.sticky{position:sticky;left:0;z-index:1}.gantt-top-group-label{margin:0;padding:0;white-space:nowrap}.gantt-bottom-row{display:flex;height:28px;background:var(--muted)}.gantt-bottom-cell{display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:500;color:var(--muted-foreground);letter-spacing:.01em}.gantt-content{position:relative;background:var(--background)}.gantt-rows{position:absolute;top:0;left:0;width:100%;height:100%;z-index:1;pointer-events:none}.gantt-task-row{position:absolute;top:0;left:0;display:flex;width:100%;align-items:center;border-bottom:1px solid var(--border-subtle);pointer-events:none}.gantt-dependency-arrows{position:absolute;top:0;left:0;width:100%;pointer-events:none;z-index:5}.gantt-dependency-arrow{stroke:var(--arrow);stroke-width:1.5;fill:none}.gantt-dependency-arrow-head{fill:var(--arrow);stroke:none}.gantt-task-bar{position:relative;z-index:10;display:flex;align-items:center;background:var(--bar-bg);border-radius:6px;-webkit-user-select:none;user-select:none;cursor:grab;box-shadow:var(--bar-shadow);transition:background var(--transition-fast),box-shadow var(--transition-normal)}.gantt-task-bar:before,.gantt-task-bar:after{content:"";position:absolute;top:50%;width:3px;height:12px;background:var(--muted-foreground);border-radius:2px;opacity:0;transform:translateY(-50%);transition:opacity var(--transition-fast)}.gantt-task-bar:before{left:4px}.gantt-task-bar:after{right:4px}.gantt-task-bar:hover{background:var(--bar-bg-hover);box-shadow:var(--bar-shadow-hover)}.gantt-task-bar:hover:before,.gantt-task-bar:hover:after{opacity:.5}.gantt-task-bar:active,.gantt-task-bar.dragging{cursor:grabbing;box-shadow:var(--bar-shadow-drag);z-index:100}.gantt-task-bar.dragging:before,.gantt-task-bar.dragging:after{opacity:.7}.gantt-task-name{flex:1;padding:0 14px;font-family:inherit;font-size:12px;font-weight:500;color:var(--bar-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:none}.gantt-bar-tooltip{position:absolute;bottom:calc(100% + 8px);left:50%;transform:translate(-50%);z-index:200;padding:8px 12px;background:var(--foreground);color:var(--background);border-radius:6px;font-family:inherit;font-size:11px;font-weight:500;white-space:nowrap;box-shadow:0 4px 16px #00000026;pointer-events:none;animation:tooltipFadeIn var(--transition-fast) ease-out}@keyframes tooltipFadeIn{0%{opacity:0;transform:translate(-50%) translateY(4px)}to{opacity:1;transform:translate(-50%) translateY(0)}}.gantt-bar-tooltip:after{content:"";position:absolute;top:100%;left:50%;transform:translate(-50%);border:5px solid transparent;border-top-color:var(--foreground)}@media (max-width: 768px){.gantt-toolbar{padding:8px 12px}.gantt-scale-button{padding:5px 10px;font-size:11px}.gantt-top-group{font-size:12px;padding:0 12px}.gantt-bottom-cell{font-size:10px}.gantt-task-name{font-size:11px;padding:0 10px}}
|
|
1
|
+
.gantt-container{--gantt-font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Geist", system-ui, sans-serif;--gantt-background: #fafafa;--gantt-foreground: #18181b;--gantt-muted: #f4f4f5;--gantt-muted-foreground: #71717a;--gantt-border: #e4e4e7;--gantt-border-subtle: rgba(0, 0, 0, .04);--gantt-bar-bg: #e4e4e7;--gantt-bar-bg-hover: #d4d4d8;--gantt-bar-text: #18181b;--gantt-bar-shadow: 0 1px 2px rgba(0, 0, 0, .05);--gantt-bar-shadow-hover: 0 4px 12px rgba(0, 0, 0, .1);--gantt-bar-shadow-drag: 0 8px 24px rgba(0, 0, 0, .15);--gantt-arrow: #a1a1aa;--gantt-accent: #3b82f6;--gantt-today-marker: #f43f5e;--gantt-marker: #6366f1;--gantt-marker-warning: #f59e0b;--gantt-marker-label: #ffffff;--gantt-band-bg: rgba(99, 102, 241, .1);--gantt-milestone-bg: #52525b;--gantt-milestone-bg-hover: #3f3f46;--gantt-critical: #dc2626;--gantt-critical-bg: #fecaca;--gantt-critical-bg-hover: #fca5a5;--gantt-critical-text: #7f1d1d;--gantt-baseline-bg: #a1a1aa;--gantt-progress-bg: #a1a1aa;--gantt-progress-handle: #52525b;--gantt-non-working-bg: rgba(0, 0, 0, .035);--gantt-duration-fast: .15s;--gantt-transition-fast: .15s ease;--gantt-transition-normal: .2s ease}.gantt-container.dark,.gantt-container[data-theme=dark]{--gantt-background: #09090b;--gantt-foreground: #fafafa;--gantt-muted: #18181b;--gantt-muted-foreground: #a1a1aa;--gantt-border: #27272a;--gantt-border-subtle: rgba(255, 255, 255, .04);--gantt-bar-bg: #27272a;--gantt-bar-bg-hover: #3f3f46;--gantt-bar-text: #fafafa;--gantt-bar-shadow: 0 1px 2px rgba(0, 0, 0, .2);--gantt-bar-shadow-hover: 0 4px 12px rgba(0, 0, 0, .3);--gantt-bar-shadow-drag: 0 8px 24px rgba(0, 0, 0, .4);--gantt-arrow: #71717a;--gantt-today-marker: #fb7185;--gantt-marker: #818cf8;--gantt-marker-warning: #fbbf24;--gantt-marker-label: #18181b;--gantt-band-bg: rgba(129, 140, 248, .16);--gantt-milestone-bg: #d4d4d8;--gantt-milestone-bg-hover: #e4e4e7;--gantt-critical: #f87171;--gantt-critical-bg: #7f1d1d;--gantt-critical-bg-hover: #991b1b;--gantt-critical-text: #fee2e2;--gantt-baseline-bg: #71717a;--gantt-progress-bg: #52525b;--gantt-progress-handle: #a1a1aa;--gantt-non-working-bg: rgba(255, 255, 255, .03)}@media(prefers-color-scheme:dark){.gantt-container:not(.light):not([data-theme=light]){--gantt-background: #09090b;--gantt-foreground: #fafafa;--gantt-muted: #18181b;--gantt-muted-foreground: #a1a1aa;--gantt-border: #27272a;--gantt-border-subtle: rgba(255, 255, 255, .04);--gantt-bar-bg: #27272a;--gantt-bar-bg-hover: #3f3f46;--gantt-bar-text: #fafafa;--gantt-bar-shadow: 0 1px 2px rgba(0, 0, 0, .2);--gantt-bar-shadow-hover: 0 4px 12px rgba(0, 0, 0, .3);--gantt-bar-shadow-drag: 0 8px 24px rgba(0, 0, 0, .4);--gantt-arrow: #71717a;--gantt-today-marker: #fb7185;--gantt-marker: #818cf8;--gantt-marker-warning: #fbbf24;--gantt-marker-label: #18181b;--gantt-band-bg: rgba(129, 140, 248, .16);--gantt-milestone-bg: #d4d4d8;--gantt-milestone-bg-hover: #e4e4e7;--gantt-critical: #f87171;--gantt-critical-bg: #7f1d1d;--gantt-critical-bg-hover: #991b1b;--gantt-critical-text: #fee2e2;--gantt-baseline-bg: #71717a;--gantt-progress-bg: #52525b;--gantt-progress-handle: #a1a1aa;--gantt-non-working-bg: rgba(255, 255, 255, .03)}}.gantt-container,.gantt-container *,.gantt-container *:before,.gantt-container *:after{box-sizing:border-box}.gantt-container{display:flex;flex-direction:column;overflow:hidden;background:var(--gantt-background);font-family:var(--gantt-font-sans);font-size:13px;line-height:1.5;color:var(--gantt-foreground);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.gantt-container:focus{outline:none}.gantt-toolbar{flex-shrink:0;display:flex;align-items:center;justify-content:flex-end;padding:12px 16px;background:var(--gantt-background);border-bottom:1px solid var(--gantt-border)}.gantt-grid-toggle{margin-right:auto;display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;padding:0;color:var(--gantt-muted-foreground);background:transparent;border:1px solid var(--gantt-border);border-radius:6px;cursor:pointer;transition:color var(--gantt-transition-fast),background var(--gantt-transition-fast)}.gantt-grid-toggle:hover{color:var(--gantt-foreground);background:var(--gantt-muted)}.gantt-grid-toggle:focus-visible{outline:2px solid var(--gantt-accent);outline-offset:-2px}.gantt-grid-toggle svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.3}.gantt-grid-toggle[aria-expanded=true] svg rect{fill:var(--gantt-muted)}.gantt-scale-selector{display:inline-flex}.gantt-scale-control{display:flex;gap:2px;padding:3px;background:var(--gantt-muted);border-radius:8px;border:1px solid var(--gantt-border)}.gantt-scale-button{position:relative;padding:6px 14px;font-family:inherit;font-size:12px;font-weight:500;color:var(--gantt-muted-foreground);background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all var(--gantt-transition-fast);text-transform:capitalize}.gantt-scale-button:hover{color:var(--gantt-foreground)}.gantt-scale-button:focus-visible{outline:2px solid var(--gantt-accent);outline-offset:-2px}.gantt-scale-button[data-active=true]{color:var(--gantt-foreground);background:var(--gantt-background);box-shadow:0 1px 3px #00000014}.gantt-main{position:relative;flex:1;min-height:0;overflow:hidden}.gantt-scroll-container{position:relative;width:100%;height:100%;overflow:auto}.gantt-body{display:flex;align-items:stretch;width:max-content;min-width:100%}.gantt-timeline{position:relative;flex:none}.gantt-grid{position:sticky;left:0;z-index:50;flex:none;background:var(--gantt-background);border-right:1px solid var(--gantt-border)}.gantt-grid-header{position:sticky;top:0;z-index:1;display:flex;align-items:flex-end;padding-bottom:6px;overflow:hidden;background:var(--gantt-background);border-bottom:1px solid var(--gantt-border)}.gantt-grid-header-cell{padding:0 10px;font-size:11px;font-weight:600;letter-spacing:.02em;text-transform:uppercase;color:var(--gantt-muted-foreground);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.gantt-grid-body{position:relative}.gantt-grid-row{position:absolute;top:0;left:0;display:flex;width:100%;align-items:center;overflow:hidden;border-bottom:1px solid var(--gantt-border-subtle)}.gantt-grid-row:hover{background:var(--gantt-muted)}.gantt-grid-row.summary{font-weight:600}.gantt-grid-row.selected{background:var(--gantt-muted);box-shadow:inset 2px 0 0 var(--gantt-accent)}.gantt-grid-row.draggable{cursor:grab}.gantt-grid.row-dragging,.gantt-grid.row-dragging .gantt-grid-row{cursor:grabbing}.gantt-grid-row.dragging{opacity:.45}.gantt-grid-row.drop-into{background:color-mix(in srgb,var(--gantt-accent) 14%,transparent);box-shadow:inset 0 0 0 1px var(--gantt-accent)}.gantt-grid-row.drop-into.invalid,.gantt-grid.row-dragging .gantt-grid-row.drop-into.invalid{background:color-mix(in srgb,var(--gantt-today-marker) 14%,transparent);box-shadow:inset 0 0 0 1px var(--gantt-today-marker);cursor:no-drop}.gantt-grid-drop-line{position:absolute;left:0;right:0;height:2px;margin-top:-1px;background:var(--gantt-accent);pointer-events:none}.gantt-grid-drop-line:before{content:"";position:absolute;top:-3px;left:0;width:8px;height:8px;border-radius:50%;background:inherit}.gantt-grid-drop-line.invalid{background:var(--gantt-today-marker)}.gantt-grid-cell{display:flex;align-items:center;min-width:0;padding:0 10px;font-size:12px;color:var(--gantt-foreground)}.gantt-grid-cell+.gantt-grid-cell{color:var(--gantt-muted-foreground)}.gantt-grid-cell-text{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.gantt-grid-indent{flex:none}.gantt-grid-expander{flex:none;display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;margin-right:4px;padding:0;color:var(--gantt-muted-foreground);background:transparent;border:none;border-radius:3px;cursor:pointer}.gantt-grid-expander:hover{color:var(--gantt-foreground);background:var(--gantt-border)}.gantt-grid-expander:focus-visible{outline:2px solid var(--gantt-accent);outline-offset:-2px}.gantt-grid-expander svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;transition:transform var(--gantt-transition-fast)}.gantt-grid-expander.open svg{transform:rotate(90deg)}.gantt-grid-expander-spacer{flex:none;width:20px}.gantt-grid-splitter{position:absolute;top:0;bottom:0;z-index:60;width:5px;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.gantt-grid-splitter:hover,.gantt-grid-splitter:focus-visible{background:var(--gantt-accent);opacity:.5;outline:none}.gantt-drag-guides{position:absolute;top:0;bottom:0;left:0;z-index:40;pointer-events:none}.gantt-drag-guide{position:absolute;top:0;bottom:0;width:1px;background:var(--gantt-accent);opacity:.9}.gantt-drag-guide-label{position:absolute;top:6px;left:0;transform:translate(-50%);padding:3px 7px;background:var(--gantt-accent);color:#fff;border-radius:4px;font-family:inherit;font-size:11px;font-weight:500;white-space:nowrap}.gantt-scroll-container{scrollbar-width:thin;scrollbar-color:var(--gantt-border) transparent}.gantt-scroll-container::-webkit-scrollbar{width:10px;height:10px}.gantt-scroll-container::-webkit-scrollbar-track{background:transparent}.gantt-scroll-container::-webkit-scrollbar-thumb{background:var(--gantt-border);border-radius:5px;border:2px solid var(--gantt-background)}.gantt-scroll-container::-webkit-scrollbar-thumb:hover{background:var(--gantt-muted-foreground)}.gantt-scroll-container::-webkit-scrollbar-corner{background:transparent}.gantt-header-wrapper{position:sticky;top:0;z-index:30}.gantt-header{position:relative;background:var(--gantt-background);border-bottom:1px solid var(--gantt-border)}.gantt-header-content{display:flex;flex-direction:column}.gantt-top-header{position:relative;display:flex;height:44px;border-bottom:1px solid var(--gantt-border-subtle)}.gantt-top-groups{display:flex}.gantt-top-group{--top-group-inset: 16px;display:flex;align-items:center;padding:0 var(--top-group-inset);font-size:13px;font-weight:600;letter-spacing:-.01em;background:var(--gantt-background);color:var(--gantt-foreground)}.gantt-top-group-label{position:sticky;left:var(--top-group-inset);margin:0;padding:0;white-space:nowrap}.gantt-bottom-row{display:flex;height:28px;background:var(--gantt-muted)}.gantt-bottom-cell{display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:500;color:var(--gantt-muted-foreground);letter-spacing:.01em}.gantt-content{position:relative;background:var(--gantt-background)}.gantt-non-working-layer{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0}.gantt-non-working-range{position:absolute;top:0;bottom:0;background:var(--gantt-non-working-bg)}.gantt-rows{position:absolute;top:0;left:0;width:100%;height:100%;z-index:0;pointer-events:none}.gantt-task-row{position:absolute;top:0;left:0;display:flex;width:100%;align-items:center;border-bottom:1px solid var(--gantt-border-subtle);pointer-events:none}.gantt-range-band-layer{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0}.gantt-range-band{position:absolute;top:0;bottom:0;background:var(--gantt-band-color, var(--gantt-band-bg))}.gantt-range-band-label{position:absolute;top:2px;left:4px;font-size:10px;font-weight:600;color:var(--gantt-muted-foreground);white-space:nowrap}.gantt-marker{position:absolute;top:0;bottom:0;width:2px;margin-left:-1px;background:var(--gantt-marker-color, var(--gantt-marker));pointer-events:none;z-index:2}.gantt-marker[data-warning=true]{--gantt-marker-color: var(--gantt-marker-warning)}.gantt-marker-label{position:absolute;top:2px;left:3px;padding:1px 5px;font-size:10px;font-weight:600;line-height:1.4;color:var(--gantt-marker-label);background:var(--gantt-marker-color, var(--gantt-marker));border-radius:3px;white-space:nowrap}.gantt-today-marker{position:absolute;top:0;bottom:0;width:2px;margin-left:-1px;background:var(--gantt-today-marker);opacity:.7;pointer-events:none;z-index:2}.gantt-dependency-arrows{position:absolute;top:0;left:0;width:100%;pointer-events:none;z-index:0}.gantt-dependency-arrows.linking{z-index:150}.gantt-dependency-arrow{stroke:var(--gantt-arrow);stroke-width:1.5;fill:none}.gantt-dependency-arrow.selected{stroke:var(--gantt-accent);stroke-width:2.5}.gantt-dependency-arrow-head{fill:var(--gantt-arrow);stroke:none}.gantt-dependency-hit{stroke:transparent;stroke-width:12;fill:none;cursor:pointer;pointer-events:stroke}.gantt-dependency-hit:hover+.gantt-dependency-arrow{stroke:var(--gantt-accent)}.gantt-dependency-delete{cursor:pointer;pointer-events:auto}.gantt-dependency-delete circle{fill:var(--gantt-accent)}.gantt-dependency-delete path{stroke:#fff;stroke-width:1.6;stroke-linecap:round}.gantt-link-handle{position:absolute;top:50%;width:10px;height:10px;background:var(--gantt-background);border:2px solid var(--gantt-accent);border-radius:50%;transform:translateY(-50%);opacity:0;cursor:crosshair;touch-action:none;pointer-events:none;transition:opacity var(--gantt-transition-fast)}.gantt-link-handle.start{left:-14px}.gantt-link-handle.end{right:-14px}.gantt-milestone .gantt-link-handle.start{left:-9px}.gantt-milestone .gantt-link-handle.end{left:15px;right:auto}.gantt-task-bar:hover .gantt-link-handle,.gantt-milestone:hover .gantt-link-handle,.gantt-task-bar.selected .gantt-link-handle,.gantt-milestone.selected .gantt-link-handle{opacity:1;pointer-events:auto}.gantt-task-bar.link-target,.gantt-milestone.link-target .gantt-milestone-diamond{outline:2px solid var(--gantt-accent);outline-offset:2px}.gantt-task-bar.link-target.invalid,.gantt-milestone.link-target.invalid .gantt-milestone-diamond{outline-color:var(--gantt-today-marker)}.gantt-link-preview-line{stroke:var(--gantt-accent);stroke-width:2;stroke-dasharray:4 3;fill:none}.gantt-link-preview-line.invalid{stroke:var(--gantt-today-marker)}.gantt-link-preview-origin{fill:var(--gantt-accent)}.gantt-link-preview-reason{fill:var(--gantt-today-marker);font-family:inherit;font-size:11px;font-weight:500;paint-order:stroke;stroke:var(--gantt-background);stroke-width:3px;stroke-linejoin:round}.gantt-content.drawable{cursor:crosshair}.gantt-draw-ghost{position:absolute;top:0;left:0;height:19px;margin-top:9px;background:var(--gantt-accent);opacity:.35;border:1px dashed var(--gantt-accent);border-radius:6px;pointer-events:none;z-index:5}.gantt-task-bar{position:relative;touch-action:pan-x pan-y;z-index:10;display:flex;align-items:center;background:var(--gantt-bar-color, var(--gantt-bar-bg));border-radius:6px;-webkit-user-select:none;user-select:none;cursor:grab;box-shadow:var(--gantt-bar-shadow);transition:background var(--gantt-transition-fast),box-shadow var(--gantt-transition-normal)}.gantt-task-bar:before,.gantt-task-bar:after{content:"";position:absolute;top:50%;width:3px;height:12px;background:var(--gantt-muted-foreground);border-radius:2px;opacity:0;transform:translateY(-50%);transition:opacity var(--gantt-transition-fast)}.gantt-task-bar:before{left:4px}.gantt-task-bar:after{right:4px}.gantt-task-bar:hover{background:var(--gantt-bar-color-hover, var(--gantt-bar-bg-hover));box-shadow:var(--gantt-bar-shadow-hover)}.gantt-task-bar:hover:before,.gantt-task-bar:hover:after{opacity:.5}.gantt-task-bar:active,.gantt-task-bar.dragging{cursor:grabbing;box-shadow:var(--gantt-bar-shadow-drag);z-index:100}.gantt-task-bar.dragging:before,.gantt-task-bar.dragging:after{opacity:.7}.gantt-task-bar.summary{background:var(--gantt-bar-color, var(--gantt-milestone-bg));border-radius:3px}.gantt-task-bar.summary:hover{background:var(--gantt-bar-color-hover, var(--gantt-milestone-bg-hover))}.gantt-task-bar.summary .gantt-task-name{color:var(--gantt-background)}.gantt-task-bar.summary .gantt-task-name.outside{color:var(--gantt-foreground)}.gantt-task-bar.summary .gantt-progress-fill{background:var(--gantt-progress-color, var(--gantt-foreground));opacity:.35;border-radius:3px 0 0 3px}.gantt-task-bar.summary:before,.gantt-task-bar.summary:after,.gantt-task-bar.summary:hover:before,.gantt-task-bar.summary:hover:after{opacity:0}.gantt-baseline{position:absolute;bottom:3px;height:4px;background:var(--gantt-baseline-bg);border-radius:2px;opacity:.65;pointer-events:none}.gantt-baseline.milestone{width:8px;height:8px;bottom:1px;margin-left:-4px;border-radius:1px;transform:rotate(45deg)}.gantt-task-bar.critical{background:var(--gantt-critical-bg)}.gantt-task-bar.critical:hover{background:var(--gantt-critical-bg-hover)}.gantt-task-bar.critical .gantt-task-name{color:var(--gantt-critical-text)}.gantt-task-bar.critical .gantt-task-name.outside{color:var(--gantt-foreground)}.gantt-task-bar.critical .gantt-progress-fill{background:var(--gantt-critical);opacity:.45}.gantt-milestone.critical .gantt-milestone-diamond{background:var(--gantt-critical)}.gantt-dependency-arrow.critical{stroke:var(--gantt-critical);stroke-width:2}.gantt-dependency-arrow-head.critical{fill:var(--gantt-critical)}.gantt-task-bar.selected,.gantt-milestone.selected .gantt-milestone-diamond{outline:2px solid var(--gantt-accent);outline-offset:2px}.gantt-task-bar.reverting,.gantt-milestone.reverting{transition:transform var(--gantt-transition-normal),width var(--gantt-transition-normal),background var(--gantt-transition-fast),box-shadow var(--gantt-transition-normal)}.gantt-task-bar.reverting .gantt-progress-fill,.gantt-task-bar.reverting .gantt-progress-handle{transition:width var(--gantt-transition-normal),left var(--gantt-transition-normal)}.gantt-task-bar.compact:before,.gantt-task-bar.compact:after,.gantt-task-bar.compact:hover:before,.gantt-task-bar.compact:hover:after,.gantt-task-bar.compact.dragging:before,.gantt-task-bar.compact.dragging:after,.gantt-task-bar.no-resize:before,.gantt-task-bar.no-resize:after,.gantt-task-bar.no-resize:hover:before,.gantt-task-bar.no-resize:hover:after,.gantt-task-bar.no-resize.dragging:before,.gantt-task-bar.no-resize.dragging:after{opacity:0}.gantt-progress-fill{position:absolute;top:0;left:0;bottom:0;background:var(--gantt-progress-color, var(--gantt-progress-bg));border-radius:6px 0 0 6px;pointer-events:none}.gantt-progress-handle{position:absolute;top:50%;width:10px;height:10px;margin-left:-5px;background:var(--gantt-background);border:2px solid var(--gantt-progress-handle);border-radius:50%;transform:translateY(-50%);opacity:0;cursor:ew-resize;transition:opacity var(--gantt-transition-fast)}.gantt-task-bar:hover .gantt-progress-handle,.gantt-progress-handle.dragging{opacity:1}@media(hover:none){.gantt-progress-handle{opacity:1}.gantt-progress-handle:before{content:"";position:absolute;top:-17px;right:-17px;bottom:-17px;left:-17px}}.gantt-task-name{position:relative;flex:1;padding:0 14px;font-family:inherit;font-size:12px;font-weight:500;color:var(--gantt-bar-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:none}.gantt-task-name.outside{position:absolute;left:100%;flex:none;padding:0 0 0 8px;color:var(--gantt-foreground);overflow:visible}.gantt-milestone{position:relative;touch-action:pan-x pan-y;z-index:10;display:flex;align-items:center;gap:10px;-webkit-user-select:none;user-select:none;cursor:grab}.gantt-milestone-diamond{flex-shrink:0;width:16px;height:16px;background:var(--gantt-bar-color, var(--gantt-milestone-bg));border-radius:3px;transform:rotate(45deg);box-shadow:var(--gantt-bar-shadow);transition:background var(--gantt-transition-fast),box-shadow var(--gantt-transition-normal)}.gantt-milestone:hover .gantt-milestone-diamond{background:var(--gantt-bar-color-hover, var(--gantt-milestone-bg-hover));box-shadow:var(--gantt-bar-shadow-hover)}.gantt-milestone:active,.gantt-milestone.dragging{cursor:grabbing;z-index:100}.gantt-milestone.dragging .gantt-milestone-diamond{box-shadow:var(--gantt-bar-shadow-drag)}.gantt-milestone-name{font-family:inherit;font-size:12px;font-weight:500;color:var(--gantt-foreground);white-space:nowrap;pointer-events:none}.gantt-bar-tooltip{position:absolute;top:calc(100% + 8px);left:50%;transform:translate(-50%);z-index:200;padding:8px 12px;background:var(--gantt-foreground);color:var(--gantt-background);border-radius:6px;font-family:inherit;font-size:11px;font-weight:500;white-space:nowrap;box-shadow:0 4px 16px #00000026;pointer-events:none;animation:tooltipFadeIn var(--gantt-duration-fast) ease-out}@keyframes tooltipFadeIn{0%{opacity:0;transform:translate(-50%) translateY(-4px)}to{opacity:1;transform:translate(-50%) translateY(0)}}.gantt-bar-tooltip-detail{display:flex;flex-direction:column;gap:2px;text-align:left}.gantt-tooltip-name{font-weight:600}.gantt-tooltip-meta{opacity:.75}.gantt-bar-tooltip:after{content:"";position:absolute;bottom:100%;left:50%;transform:translate(-50%);border:5px solid transparent;border-bottom-color:var(--gantt-foreground)}@media(max-width:768px){.gantt-toolbar{padding:8px 12px}.gantt-scale-button{padding:5px 10px;font-size:11px}.gantt-top-group{--top-group-inset: 12px;font-size:12px}.gantt-bottom-cell{font-size:10px}.gantt-task-name{font-size:11px;padding:0 10px}}.gantt-grid-row.group,.gantt-task-row.group{background:var(--gantt-muted);font-weight:600}.gantt-grid-row.group .gantt-grid-cell{gap:6px}.gantt-group-label{position:sticky;left:0;display:flex;align-items:center;gap:6px;height:100%;padding:0 10px;pointer-events:auto;font-weight:600;white-space:nowrap}.gantt-grid-group-count{flex:none;padding:0 6px;border-radius:999px;background:var(--gantt-border);color:var(--gantt-muted-foreground);font-size:10px;font-weight:600;line-height:16px}.gantt-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;border:0;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap}.gantt-container [data-gantt-cell]:focus-visible{outline:2px solid var(--gantt-accent);outline-offset:1px}@media(prefers-reduced-motion:reduce){.gantt-container *,.gantt-container *:before,.gantt-container *:after{transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important}}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Dayjs } from 'dayjs';
|
|
2
|
+
import { GanttBeforeChangeHandler, GanttDragMode, GanttScheduling } from '../types/gantt';
|
|
3
|
+
import { GanttInteractionConfig, Task, TaskTransformed } from '../types/task';
|
|
4
|
+
export type DragMode = GanttDragMode;
|
|
5
|
+
export interface GanttBarDragOptions {
|
|
6
|
+
onTasksChange?: (updatedTasks: Task[]) => void;
|
|
7
|
+
onBeforeTaskChange?: GanttBeforeChangeHandler;
|
|
8
|
+
/** Scroll the timeline when the drag reaches a viewport edge (default true) */
|
|
9
|
+
autoScroll?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** The dates the drag is proposing for the tasks it moves directly */
|
|
12
|
+
type DraggedDates = Map<string, {
|
|
13
|
+
start: Dayjs;
|
|
14
|
+
end: Dayjs;
|
|
15
|
+
}>;
|
|
16
|
+
/** The task array with the dragged tasks' proposed dates written in */
|
|
17
|
+
export declare function applyDraggedDates(rawTasks: Task[], dragged: DraggedDates): Task[];
|
|
18
|
+
/**
|
|
19
|
+
* Runs the scheduling engine for the tasks the drag is moving.
|
|
20
|
+
* The dragged tasks are the seeds, so only what they reach is rescheduled and the bar
|
|
21
|
+
* under the pointer stays exactly where the pointer put it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function reschedule(rawTasks: Task[], dragged: DraggedDates, scheduling: GanttScheduling): import('core').ScheduleResult;
|
|
24
|
+
/**
|
|
25
|
+
* Hook providing the Gantt bar drag behavior
|
|
26
|
+
*
|
|
27
|
+
* `autoScroll` (default on) scrolls the timeline when the drag reaches a viewport edge,
|
|
28
|
+
* faster the closer the pointer gets, and stops on drop or cancel.
|
|
29
|
+
*/
|
|
30
|
+
export declare function useGanttBarDrag(task: TaskTransformed, options?: GanttBarDragOptions, interaction?: GanttInteractionConfig, scheduling?: GanttScheduling): {
|
|
31
|
+
onPointerDown: import('react').PointerEventHandler<HTMLDivElement>;
|
|
32
|
+
dragMode: GanttDragMode | null;
|
|
33
|
+
consumeDragClick: () => boolean;
|
|
34
|
+
};
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** The task the user drew, handed to `onTaskCreate` - nothing is committed by the chart */
|
|
2
|
+
export interface GanttTaskDraft {
|
|
3
|
+
/** UTC ISO string, snapped to the current scale */
|
|
4
|
+
startDate: string;
|
|
5
|
+
endDate: string;
|
|
6
|
+
/** Id of the task whose row the range was drawn on, null when the row has none */
|
|
7
|
+
rowTaskId: string | null;
|
|
8
|
+
}
|
|
9
|
+
/** The ghost bar drawn while the pointer is down (px, timeline content coordinates) */
|
|
10
|
+
interface DrawGhost {
|
|
11
|
+
leftPx: number;
|
|
12
|
+
widthPx: number;
|
|
13
|
+
topPx: number;
|
|
14
|
+
}
|
|
15
|
+
interface UseGanttDrawCreateParams {
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
/** Rows on screen, in order - the row under the pointer names the task it belongs to */
|
|
18
|
+
rowIds: (string | null)[];
|
|
19
|
+
onTaskCreate?: (draft: GanttTaskDraft) => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Drawing a new task on the empty part of a row
|
|
23
|
+
*
|
|
24
|
+
* The chart never adds the task itself: the drawn range is snapped to the current scale
|
|
25
|
+
* and handed to `onTaskCreate`, and the host decides what (if anything) to do with it.
|
|
26
|
+
*/
|
|
27
|
+
export declare function useGanttDrawCreate({ enabled, rowIds, onTaskCreate, }: UseGanttDrawCreateParams): {
|
|
28
|
+
onDrawPointerDown: (e: React.PointerEvent<HTMLDivElement>) => void;
|
|
29
|
+
ghost: DrawGhost | null;
|
|
30
|
+
};
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
import { GanttBottomRowCell, GanttScaleKey } from '../types/gantt';
|
|
3
|
+
import { GanttExportOptions } from '../utils/pngExport';
|
|
4
|
+
/** Imperative export API */
|
|
5
|
+
export interface GanttExportApi {
|
|
6
|
+
/**
|
|
7
|
+
* Renders the whole chart to a PNG and resolves with the blob
|
|
8
|
+
*
|
|
9
|
+
* No download is triggered - what to do with the blob is the caller's choice
|
|
10
|
+
* (save it, upload it, drop it into a PDF).
|
|
11
|
+
*/
|
|
12
|
+
exportToPng: (options?: GanttExportOptions) => Promise<Blob>;
|
|
13
|
+
}
|
|
14
|
+
interface UseGanttExportApiParams {
|
|
15
|
+
scrollRef: RefObject<HTMLDivElement | null>;
|
|
16
|
+
bottomRowCells: GanttBottomRowCell[];
|
|
17
|
+
selectedScale: GanttScaleKey;
|
|
18
|
+
taskCount: number;
|
|
19
|
+
totalWidth: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Imperative PNG export
|
|
23
|
+
*
|
|
24
|
+
* Rows, arrows and header cells are all virtualized, so the live DOM only ever
|
|
25
|
+
* holds the visible slice. The export turns virtualization off, waits for the
|
|
26
|
+
* full chart to render, captures it, and puts the chart back exactly as it was
|
|
27
|
+
* - scroll position included - whether the capture succeeded or threw.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useGanttExportApi({ scrollRef, bottomRowCells, selectedScale, taskCount, totalWidth, }: UseGanttExportApiParams): GanttExportApi;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { Task } from '../types/task';
|
|
3
|
+
/** Imperative undo/redo API */
|
|
4
|
+
export interface GanttHistoryApi {
|
|
5
|
+
/** Reverts the newest gesture and fires `onTasksChange`. No-op with an empty stack. */
|
|
6
|
+
undo: () => void;
|
|
7
|
+
/** Replays the newest undone gesture and fires `onTasksChange`. No-op with an empty stack. */
|
|
8
|
+
redo: () => void;
|
|
9
|
+
/** Whether there is a gesture to undo */
|
|
10
|
+
canUndo: boolean;
|
|
11
|
+
/** Whether there is an undone gesture to redo */
|
|
12
|
+
canRedo: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Whether the key event landed in something the user is typing into, e.g. an input
|
|
16
|
+
* rendered by a custom task list cell
|
|
17
|
+
*/
|
|
18
|
+
export declare function isTextEntryTarget(target: unknown): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Undo/redo for the chart: the imperative methods plus the keyboard shortcuts.
|
|
21
|
+
*
|
|
22
|
+
* The key handler is meant for the chart container, so it only ever sees events
|
|
23
|
+
* from inside the chart - the shortcut does nothing while the chart is not focused.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useGanttHistoryApi(onTasksChange?: (updatedTasks: Task[]) => void): {
|
|
26
|
+
historyApi: GanttHistoryApi;
|
|
27
|
+
onKeyDown: React.KeyboardEventHandler<HTMLElement>;
|
|
28
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { DependencyType, GanttInteractionConfig, Task, TaskTransformed } from '../types/task';
|
|
2
|
+
import { LinkAnchor } from '../utils/dependency';
|
|
3
|
+
/** The link the user drew, handed to `onDependencyCreate` before anything is committed */
|
|
4
|
+
export interface GanttDependencyChange {
|
|
5
|
+
/** Task the drag started on */
|
|
6
|
+
predecessorId: string;
|
|
7
|
+
/** Task the drag was dropped on - the one whose `dependencies` gains the entry */
|
|
8
|
+
successorId: string;
|
|
9
|
+
type: DependencyType;
|
|
10
|
+
}
|
|
11
|
+
interface UseGanttLinkDragParams {
|
|
12
|
+
task: TaskTransformed;
|
|
13
|
+
interaction?: GanttInteractionConfig;
|
|
14
|
+
onTasksChange?: (updatedTasks: Task[]) => void;
|
|
15
|
+
/** Returning false rejects the link */
|
|
16
|
+
onDependencyCreate?: (change: GanttDependencyChange) => boolean | void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Dragging a dependency from one of a bar's connector dots to another bar
|
|
20
|
+
*
|
|
21
|
+
* The gesture's direction is the dependency's direction: the bar the drag starts on is
|
|
22
|
+
* the predecessor, the bar it lands on the successor. Which ends the two dots sit on
|
|
23
|
+
* decides the type (see `linkTypeFromAnchors`).
|
|
24
|
+
*
|
|
25
|
+
* The drop target is found with `elementFromPoint` rather than from the bar geometry, so
|
|
26
|
+
* virtualization, scrolling and the sticky task list pane all take care of themselves.
|
|
27
|
+
*/
|
|
28
|
+
export declare function useGanttLinkDrag({ task, interaction, onTasksChange, onDependencyCreate, }: UseGanttLinkDragParams): {
|
|
29
|
+
startLink: (anchor: LinkAnchor) => (e: React.PointerEvent<HTMLElement>) => void;
|
|
30
|
+
};
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { GanttBeforeChangeHandler } from '../types/gantt';
|
|
3
|
+
import { Task, TaskTransformed } from '../types/task';
|
|
4
|
+
export interface GanttProgressDragOptions {
|
|
5
|
+
onTasksChange?: (updatedTasks: Task[]) => void;
|
|
6
|
+
onBeforeTaskChange?: GanttBeforeChangeHandler;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Progress handle drag hook
|
|
10
|
+
* Previews with a local value while dragging, commits to rawTasks on pointerup
|
|
11
|
+
*/
|
|
12
|
+
export declare function useGanttProgressDrag(task: TaskTransformed, barRef: React.RefObject<HTMLDivElement | null>, options?: GanttProgressDragOptions): {
|
|
13
|
+
onProgressPointerDown: React.PointerEventHandler<HTMLDivElement>;
|
|
14
|
+
progress: number | null;
|
|
15
|
+
isDraggingProgress: boolean;
|
|
16
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
import { GanttReorderChange } from '../types/gantt';
|
|
3
|
+
import { Task } from '../types/task';
|
|
4
|
+
import { RowDropRow, RowDropTarget } from '../utils/rowDrag';
|
|
5
|
+
export interface RowDragState {
|
|
6
|
+
draggedId: string;
|
|
7
|
+
/** Where the drop would land - null while the pointer is nowhere useful */
|
|
8
|
+
target: RowDropTarget | null;
|
|
9
|
+
}
|
|
10
|
+
interface UseGanttRowDragParams {
|
|
11
|
+
/** The rows on screen, in row order */
|
|
12
|
+
rows: RowDropRow[];
|
|
13
|
+
/** The element the rows are positioned in - pointer Y is measured from its top */
|
|
14
|
+
bodyRef: RefObject<HTMLDivElement | null>;
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
onReorder?: (change: GanttReorderChange) => void | boolean;
|
|
17
|
+
onTasksChange?: (updatedTasks: Task[]) => void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Vertical drag of a grid row - reorders siblings and re-parents
|
|
21
|
+
*
|
|
22
|
+
* Deliberately separate from useGanttBarDrag: that one is horizontal and lives on the bars in
|
|
23
|
+
* the timeline pane, this one is vertical and lives on the rows in the grid pane, so neither
|
|
24
|
+
* gesture can start the other.
|
|
25
|
+
*
|
|
26
|
+
* Nothing is committed while the drop is illegal - the indicator says so during the drag and
|
|
27
|
+
* the pointerup does nothing.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useGanttRowDrag({ rows, bodyRef, enabled, onReorder, onTasksChange, }: UseGanttRowDragParams): {
|
|
30
|
+
onRowPointerDown: import('react').PointerEventHandler<HTMLDivElement>;
|
|
31
|
+
dragState: RowDragState | null;
|
|
32
|
+
};
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Dayjs } from 'dayjs';
|
|
2
|
+
import { RefObject } from 'react';
|
|
3
|
+
import { GanttExportApi } from './useGanttExportApi';
|
|
4
|
+
import { GanttHistoryApi } from './useGanttHistoryApi';
|
|
5
|
+
import { GanttBottomRowCell, GanttScaleKey } from '../types/gantt';
|
|
6
|
+
import { TaskTransformed } from '../types/task';
|
|
7
|
+
/** Options for the scrollTo* methods */
|
|
8
|
+
export interface GanttScrollOptions {
|
|
9
|
+
/** Whether to animate the scroll (default true) */
|
|
10
|
+
smooth?: boolean;
|
|
11
|
+
/** Where the target lands inside the viewport (default 'center') */
|
|
12
|
+
align?: "start" | "center";
|
|
13
|
+
}
|
|
14
|
+
/** A date pinned at a fixed distance from the timeline's visible left edge */
|
|
15
|
+
export interface GanttZoomAnchor {
|
|
16
|
+
date: Dayjs;
|
|
17
|
+
/** px from the left edge of the timeline area (the task list pane excluded) */
|
|
18
|
+
viewportX: number;
|
|
19
|
+
}
|
|
20
|
+
/** Imperative scroll and zoom API */
|
|
21
|
+
export interface GanttScrollApi {
|
|
22
|
+
/** Scroll horizontally to a given date */
|
|
23
|
+
scrollToDate: (date: string | Date | Dayjs, options?: GanttScrollOptions) => void;
|
|
24
|
+
/** Scroll horizontally to today */
|
|
25
|
+
scrollToToday: (options?: GanttScrollOptions) => void;
|
|
26
|
+
/** Scroll horizontally and vertically to a given task */
|
|
27
|
+
scrollToTask: (taskId: string, options?: GanttScrollOptions) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Switch to the finest scale at which the whole project fits the viewport width
|
|
30
|
+
*
|
|
31
|
+
* Also scrolls the project into view. Does nothing while there are no tasks.
|
|
32
|
+
*/
|
|
33
|
+
zoomToFit: () => void;
|
|
34
|
+
/** The scroll container DOM node (null when unavailable) */
|
|
35
|
+
getScrollElement: () => HTMLDivElement | null;
|
|
36
|
+
}
|
|
37
|
+
/** Imperative API exposed through the ref */
|
|
38
|
+
export interface GanttHandle extends GanttScrollApi, GanttExportApi, GanttHistoryApi {
|
|
39
|
+
}
|
|
40
|
+
interface UseGanttScrollApiParams {
|
|
41
|
+
scrollRef: RefObject<HTMLDivElement | null>;
|
|
42
|
+
bottomRowCells: GanttBottomRowCell[];
|
|
43
|
+
transformedTasks: TaskTransformed[];
|
|
44
|
+
selectedScale: GanttScaleKey;
|
|
45
|
+
rowHeight: number;
|
|
46
|
+
/**
|
|
47
|
+
* How much width the pinned task list on the left covers (default 0)
|
|
48
|
+
*
|
|
49
|
+
* The timeline starts that far to the right, so centering has to measure against the
|
|
50
|
+
* narrowed viewport or the target lands behind the pane.
|
|
51
|
+
*/
|
|
52
|
+
viewportInsetPx?: number;
|
|
53
|
+
/** Switches scale while keeping the anchor date where it is on screen */
|
|
54
|
+
zoomTo: (scale: GanttScaleKey, anchor: GanttZoomAnchor) => void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Imperative scroll API
|
|
58
|
+
*
|
|
59
|
+
* Dates outside the timeline and unknown task ids are ignored silently - so that
|
|
60
|
+
* the common case of calling while data is still loading does not throw.
|
|
61
|
+
*/
|
|
62
|
+
export declare function useGanttScrollApi({ scrollRef, bottomRowCells, transformedTasks, selectedScale, rowHeight, viewportInsetPx, zoomTo, }: UseGanttScrollApiParams): GanttScrollApi;
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook selecting the state and actions needed from the Gantt store
|
|
3
|
+
* Uses shallow comparison to avoid unnecessary re-renders
|
|
4
|
+
*
|
|
5
|
+
* dragOffsets/currentTask change on every drag frame and are deliberately not
|
|
6
|
+
* subscribed to here. (Components that need them subscribe individually - this keeps
|
|
7
|
+
* the whole chart from re-rendering every frame)
|
|
8
|
+
*/
|
|
9
|
+
export declare function useGanttSelectors(): {
|
|
10
|
+
rawTasks: import('..').Task[];
|
|
11
|
+
transformedTasks: import('..').TaskTransformed[];
|
|
12
|
+
bottomRowCells: import('../types/gantt').GanttBottomRowCell[];
|
|
13
|
+
selectedScale: import('..').GanttScaleKey;
|
|
14
|
+
selectedTaskId: string | null;
|
|
15
|
+
syncTasksFromProps: (rawTasks: import('..').Task[]) => void;
|
|
16
|
+
setHistoryLimit: (limit: number) => void;
|
|
17
|
+
setTransformedTasks: (tasks: import('..').TaskTransformed[]) => void;
|
|
18
|
+
setBottomRowCells: (cells: import('../types/gantt').GanttBottomRowCell[]) => void;
|
|
19
|
+
setSelectedScale: (scale: import('..').GanttScaleKey) => void;
|
|
20
|
+
setLocaleOptions: (options: import('../types/gantt').GanttLocaleOptions | undefined) => void;
|
|
21
|
+
clearAllDragOffsets: () => void;
|
|
22
|
+
getTotalWidth: () => number;
|
|
23
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Virtualizer } from '@tanstack/react-virtual';
|
|
2
|
+
import { RefObject } from 'react';
|
|
3
|
+
import { GanttBottomRowCell } from '../types/gantt';
|
|
4
|
+
interface UseGanttColumnVirtualizationParams {
|
|
5
|
+
bottomRowCells: GanttBottomRowCell[];
|
|
6
|
+
scrollRef: RefObject<HTMLDivElement | null>;
|
|
7
|
+
}
|
|
8
|
+
interface UseGanttColumnVirtualizationResult {
|
|
9
|
+
columnVirtualizer: Virtualizer<HTMLDivElement, Element>;
|
|
10
|
+
isBarVisible: (barLeft: number, barWidth: number) => boolean;
|
|
11
|
+
}
|
|
12
|
+
interface UseGanttVirtualizationParams extends UseGanttColumnVirtualizationParams {
|
|
13
|
+
/** Number of rows on screen - not the task count, since a lane can share one row */
|
|
14
|
+
rowCount: number;
|
|
15
|
+
}
|
|
16
|
+
interface UseGanttVirtualizationResult extends UseGanttColumnVirtualizationResult {
|
|
17
|
+
rowVirtualizer: Virtualizer<HTMLDivElement, Element>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Column virtualization
|
|
21
|
+
*
|
|
22
|
+
* Split out so that rendering the header's bottom time cells and judging horizontal
|
|
23
|
+
* visibility use the same window - the header and the bars must not be culled by
|
|
24
|
+
* different criteria.
|
|
25
|
+
*/
|
|
26
|
+
export declare function useGanttColumnVirtualization({ bottomRowCells, scrollRef, }: UseGanttColumnVirtualizationParams): UseGanttColumnVirtualizationResult;
|
|
27
|
+
/**
|
|
28
|
+
* Hook managing the Gantt chart's virtualization
|
|
29
|
+
* Sets up row and column virtualization and provides the visibility check
|
|
30
|
+
*/
|
|
31
|
+
export declare function useGanttVirtualization({ rowCount, bottomRowCells, scrollRef, }: UseGanttVirtualizationParams): UseGanttVirtualizationResult;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { GanttTheme } from '../types/gantt';
|
|
2
|
+
interface UseResolvedThemeResult {
|
|
3
|
+
containerClassName: string;
|
|
4
|
+
dataTheme: 'light' | 'dark' | undefined;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Hook that resolves the theme and builds the matching class name and data attribute
|
|
8
|
+
*
|
|
9
|
+
* The 'system' theme is subscribed to with useSyncExternalStore - server rendering and
|
|
10
|
+
* the first hydration render use null (no theme class) and switch to the real system
|
|
11
|
+
* setting after hydration, so there is no hydration mismatch and no getting stuck on
|
|
12
|
+
* the wrong theme.
|
|
13
|
+
*/
|
|
14
|
+
export declare function useResolvedTheme(theme?: GanttTheme, baseClassName?: string): UseResolvedThemeResult;
|
|
15
|
+
export {};
|