@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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -101
  3. package/dist/components/GanttBar.d.ts +23 -0
  4. package/dist/components/GanttChartHeader.d.ts +17 -0
  5. package/dist/components/GanttDependencyArrows.d.ts +15 -0
  6. package/dist/components/GanttDragGuides.d.ts +9 -0
  7. package/dist/components/GanttGridSplitter.d.ts +14 -0
  8. package/dist/components/GanttMarkers.d.ts +18 -0
  9. package/dist/components/GanttTaskGrid.d.ts +42 -0
  10. package/dist/components/ScaleSelector.d.ts +11 -0
  11. package/dist/constants/gantt.d.ts +47 -0
  12. package/dist/core/calendar.d.ts +45 -0
  13. package/dist/core/criticalPath.d.ts +67 -0
  14. package/dist/core/dates.d.ts +32 -0
  15. package/dist/core/index.d.ts +16 -0
  16. package/dist/core/scheduling.d.ts +104 -0
  17. package/dist/core/tree.d.ts +42 -0
  18. package/dist/core/types.d.ts +76 -0
  19. package/dist/gantt-chart.css +1 -1
  20. package/dist/hooks/useGanttBarDrag.d.ts +35 -0
  21. package/dist/hooks/useGanttDrawCreate.d.ts +31 -0
  22. package/dist/hooks/useGanttExportApi.d.ts +30 -0
  23. package/dist/hooks/useGanttHistoryApi.d.ts +28 -0
  24. package/dist/hooks/useGanttLinkDrag.d.ts +31 -0
  25. package/dist/hooks/useGanttProgressDrag.d.ts +16 -0
  26. package/dist/hooks/useGanttRowDrag.d.ts +33 -0
  27. package/dist/hooks/useGanttScrollApi.d.ts +63 -0
  28. package/dist/hooks/useGanttSelectors.d.ts +23 -0
  29. package/dist/hooks/useGanttVirtualization.d.ts +32 -0
  30. package/dist/hooks/useResolvedTheme.d.ts +15 -0
  31. package/dist/index.cjs +4 -0
  32. package/dist/index.d.cts +13 -0
  33. package/dist/index.d.ts +13 -51
  34. package/dist/index.js +5819 -0
  35. package/dist/pages/Gantt.d.ts +306 -0
  36. package/dist/stores/context.d.ts +10 -0
  37. package/dist/stores/store.d.ts +118 -0
  38. package/dist/types/gantt.d.ts +283 -0
  39. package/dist/types/task.d.ts +111 -0
  40. package/dist/utils/a11y.d.ts +141 -0
  41. package/dist/utils/arrowPath.d.ts +66 -0
  42. package/dist/utils/dependency.d.ts +42 -0
  43. package/dist/utils/grouping.d.ts +81 -0
  44. package/dist/utils/headerUtils.d.ts +6 -0
  45. package/dist/utils/history.d.ts +56 -0
  46. package/dist/utils/i18n.d.ts +16 -0
  47. package/dist/utils/mutation.d.ts +47 -0
  48. package/dist/utils/pngExport.d.ts +67 -0
  49. package/dist/utils/pointerGesture.d.ts +31 -0
  50. package/dist/utils/rowDrag.d.ts +69 -0
  51. package/dist/utils/timeline.d.ts +161 -0
  52. package/dist/utils/transformData.d.ts +15 -0
  53. package/dist/utils/viewport.d.ts +95 -0
  54. package/package.json +47 -32
  55. package/dist/index.cjs.js +0 -4
  56. package/dist/index.es.js +0 -2394
  57. package/dist/readmeImg.png +0 -0
@@ -0,0 +1,81 @@
1
+ import { GanttGroupBy } from '../types/gantt';
2
+ import { TaskTransformed } from '../types/task';
3
+ import { TaskTree } from '../core/tree';
4
+ /** Prefix the collapsed-id set uses for a group header row */
5
+ export declare const GROUP_ROW_PREFIX = "group:";
6
+ /** Label a task with no group value falls into */
7
+ export declare const DEFAULT_UNGROUPED_LABEL = "Ungrouped";
8
+ export interface GanttRowGroup {
9
+ /** The raw value `groupBy` produced ("" for the ungrouped bucket) */
10
+ key: string;
11
+ /** What the header shows */
12
+ label: string;
13
+ /** How many tasks the group holds (rows can be fewer - lanes share a row) */
14
+ count: number;
15
+ }
16
+ /**
17
+ * One rendered row
18
+ *
19
+ * Normally one task, several when they share a `lane`, none when the row is a
20
+ * group header. The tree numbers (`level`/`posinset`/`setsize`) are the ARIA
21
+ * values for the row, so the render never has to work them out again.
22
+ */
23
+ export interface GanttRow {
24
+ /** Stable key - the group id for a header, otherwise the ids on the row */
25
+ id: string;
26
+ tasks: TaskTransformed[];
27
+ /** Indentation level, 0-based */
28
+ depth: number;
29
+ /** `aria-level`, 1-based */
30
+ level: number;
31
+ /** `aria-posinset` among the rows that share a parent */
32
+ posinset: number;
33
+ /** `aria-setsize` for that same set */
34
+ setsize: number;
35
+ /** Set only on a group header row */
36
+ group?: GanttRowGroup;
37
+ }
38
+ export interface BuildGanttRowsOptions {
39
+ /** Field name or accessor deciding which group a task belongs to */
40
+ groupBy?: GanttGroupBy;
41
+ /** Collapsed group ids (task ids in the same set are ignored here - the tree filter handles those) */
42
+ collapsedIds?: ReadonlySet<string>;
43
+ /**
44
+ * The parentId tree (only when hierarchy is on)
45
+ *
46
+ * Given, a task's group is read off its root ancestor, so a subtree is never
47
+ * split across two groups.
48
+ */
49
+ tree?: TaskTree;
50
+ /** Header label for tasks whose group value is missing */
51
+ ungroupedLabel?: string;
52
+ }
53
+ /** The row id for a group key */
54
+ export declare function groupRowId(key: string): string;
55
+ /**
56
+ * Packs tasks that share a lane onto as few rows as possible
57
+ *
58
+ * Greedy interval partitioning: walking the tasks in start order, each one joins
59
+ * the first row whose last task has already ended, and opens a new row when
60
+ * every row is still busy. Non-overlapping tasks therefore end up side by side
61
+ * on one row, and an overlap stacks onto the next row instead of drawing two
62
+ * bars on top of each other.
63
+ */
64
+ export declare function packLanes(tasks: TaskTransformed[]): TaskTransformed[][];
65
+ /**
66
+ * The row model the chart renders
67
+ *
68
+ * With no `groupBy` and no task carrying a `lane` this is one row per task, in
69
+ * the order given - the behavior of a chart that sets neither.
70
+ *
71
+ * `groupBy` puts a header row in front of each group and indents its tasks by
72
+ * one level; with `tree` (hierarchy on) the group comes from a task's root
73
+ * ancestor, so grouping decides the top level and the parentId nesting is kept
74
+ * inside the group. A group listed in `collapsedIds` keeps its header and drops
75
+ * its rows.
76
+ *
77
+ * Every task's `order` is rewritten to its row number, so anything positioning
78
+ * by row - the dependency arrows above all - follows grouping and lane packing
79
+ * without knowing about either.
80
+ */
81
+ export declare function buildGanttRows(tasks: TaskTransformed[], options?: BuildGanttRowsOptions): GanttRow[];
@@ -0,0 +1,6 @@
1
+ import { GanttTopHeaderGroup } from '../types/gantt';
2
+ /**
3
+ * Merges consecutive groups that carry the same label
4
+ * Consecutive cells in the same month/year and so on become a single group
5
+ */
6
+ export declare function mergeHeaderGroups(groups: GanttTopHeaderGroup[]): GanttTopHeaderGroup[];
@@ -0,0 +1,56 @@
1
+ import { Task } from '../types/task';
2
+ /**
3
+ * Field values one task changed by, recorded per gesture.
4
+ *
5
+ * Only the keys that actually differ are stored, so a step costs a handful of
6
+ * strings rather than a copy of the task array.
7
+ */
8
+ export interface TaskPatch {
9
+ id: string;
10
+ /** Values from before the gesture - what undo writes back */
11
+ before: Partial<Task>;
12
+ /** Values from after the gesture - what redo writes back */
13
+ after: Partial<Task>;
14
+ }
15
+ /** One user gesture. A subtree drag that moved 20 rows is still one entry. */
16
+ export type HistoryEntry = TaskPatch[];
17
+ export interface HistoryStack {
18
+ /** Oldest step first - the last one is what undo pops */
19
+ past: HistoryEntry[];
20
+ /** Steps undone and still redoable, newest first */
21
+ future: HistoryEntry[];
22
+ }
23
+ export declare const EMPTY_HISTORY: HistoryStack;
24
+ /** How many steps are kept when no `historyLimit` is given */
25
+ export declare const DEFAULT_HISTORY_LIMIT = 100;
26
+ /**
27
+ * Builds the undo step for one gesture, or null when the change cannot be
28
+ * inverted by a field patch.
29
+ *
30
+ * Every gesture today rewrites fields on existing rows (`tasks.map(...)`), which
31
+ * is exactly what a patch can undo. A change that adds, removes or replaces a row
32
+ * returns null instead of a half-correct entry, and the caller drops the history
33
+ * rather than storing a step that would corrupt the data on replay.
34
+ */
35
+ export declare function diffTasks(before: Task[], after: Task[]): HistoryEntry | null;
36
+ /** Writes one direction of a step back onto the tasks it touched */
37
+ export declare function applyPatches(tasks: Task[], entry: HistoryEntry, direction: 'before' | 'after'): Task[];
38
+ /**
39
+ * Records a step.
40
+ *
41
+ * A gesture that changed nothing is not a step. A new action branches the
42
+ * timeline, so anything that was undone stops being redoable.
43
+ */
44
+ export declare function pushHistory(stack: HistoryStack, entry: HistoryEntry, limit: number): HistoryStack;
45
+ /** Applies a new depth to an existing stack, dropping the steps that no longer fit */
46
+ export declare function limitHistory(stack: HistoryStack, limit: number): HistoryStack;
47
+ /** Moves the newest step from past to future. null when there is nothing to undo. */
48
+ export declare function popUndo(stack: HistoryStack): {
49
+ stack: HistoryStack;
50
+ entry: HistoryEntry;
51
+ } | null;
52
+ /** Moves the newest undone step back onto past. null when there is nothing to redo. */
53
+ export declare function popRedo(stack: HistoryStack): {
54
+ stack: HistoryStack;
55
+ entry: HistoryEntry;
56
+ } | null;
@@ -0,0 +1,16 @@
1
+ import { GanttFormatters, GanttLabelUnit, GanttLocaleOptions, GanttScaleKey } from '../types/gantt';
2
+ /**
3
+ * Unit the top header row groups by
4
+ *
5
+ * A first day of the week is only meaningful where weeks are the grouping, which is the
6
+ * week scale - and there it is opt-in, so a chart that passes no `firstDayOfWeek` keeps
7
+ * grouping the week scale by month exactly as before.
8
+ */
9
+ export declare function resolveLabelUnit(scale: GanttScaleKey, options?: GanttLocaleOptions): GanttLabelUnit;
10
+ /**
11
+ * The tick/header/tooltip formatters for one scale
12
+ *
13
+ * Cheap enough to call per render, but it builds `Intl.DateTimeFormat` instances, so
14
+ * callers memoize it on [scale, options].
15
+ */
16
+ export declare function resolveFormatters(scale: GanttScaleKey, options?: GanttLocaleOptions): GanttFormatters;
@@ -0,0 +1,47 @@
1
+ import { GanttBeforeChangeHandler, GanttChangeType, GanttTaskChange } from '../types/gantt';
2
+ import { Task } from '../types/task';
3
+ /** How long a rolled-back bar animates back - matches --gantt-transition-normal */
4
+ export declare const REVERT_DURATION_MS = 200;
5
+ /** What the caller should do once the before-handler has answered */
6
+ export type MutationOutcome = 'commit' | 'rollback' | 'stale';
7
+ /**
8
+ * The gate's key for a gesture
9
+ *
10
+ * Moves and resizes both rewrite the dates, so they share a lane and supersede each
11
+ * other; a progress edit runs in its own lane and leaves a pending date change alone.
12
+ */
13
+ export declare function mutationKey(type: GanttChangeType, taskId: string): string;
14
+ export interface BuildTaskChangeParams {
15
+ type: GanttChangeType;
16
+ /** The bar the user grabbed */
17
+ taskId: string;
18
+ /** Ids the gesture rewrites - more than one when a summary bar carries its subtree */
19
+ changedIds: string[];
20
+ /** The task array as it was before the gesture */
21
+ previous: Task[];
22
+ /** The task array the gesture wants to commit */
23
+ next: Task[];
24
+ edge?: 'start' | 'end';
25
+ }
26
+ /**
27
+ * Builds the payload handed to `onBeforeTaskChange`
28
+ *
29
+ * `changedTasks` and `previousTasks` line up index for index, so a host can diff the two
30
+ * without looking anything up.
31
+ */
32
+ export declare function buildTaskChange({ type, taskId, changedIds, previous, next, edge, }: BuildTaskChangeParams): GanttTaskChange;
33
+ /**
34
+ * Decides commit or rollback for gestures whose before-handler may still be in flight
35
+ *
36
+ * A gesture claims its lane on the first movement and settles once the handler answers.
37
+ * If another gesture claimed the same lane in between, this one lost the bar and its
38
+ * answer is dropped ('stale') - otherwise a slow veto would drag a bar the user has since
39
+ * moved somewhere else back to a position nobody asked for.
40
+ */
41
+ export declare function createMutationGate(): {
42
+ /** Claims a lane and returns the token identifying this gesture */
43
+ begin(key: string): number;
44
+ /** Runs the handler and reports what the caller should do with the result */
45
+ settle(key: string, token: number, handler: GanttBeforeChangeHandler, change: GanttTaskChange): Promise<MutationOutcome>;
46
+ };
47
+ export type MutationGate = ReturnType<typeof createMutationGate>;
@@ -0,0 +1,67 @@
1
+ import { Dayjs } from 'dayjs';
2
+ import { GanttBottomRowCell, GanttScaleKey } from '../types/gantt';
3
+ /** Date range the export is clipped to */
4
+ export interface GanttExportRange {
5
+ from: string | Date | Dayjs;
6
+ to: string | Date | Dayjs;
7
+ }
8
+ /** Options for `GanttHandle.exportToPng` */
9
+ export interface GanttExportOptions {
10
+ /**
11
+ * Pixel density of the output (default 2)
12
+ *
13
+ * Lowered automatically when the resulting canvas would exceed the browser's
14
+ * limits - a very wide timeline is downscaled rather than failing.
15
+ */
16
+ pixelRatio?: number;
17
+ /** Background colour (any CSS colour). Defaults to the resolved theme background. */
18
+ background?: string;
19
+ /**
20
+ * Clip the export to a date range
21
+ *
22
+ * Dates outside the timeline are clamped to its edges. Omit to export the
23
+ * whole timeline.
24
+ */
25
+ range?: GanttExportRange;
26
+ }
27
+ /**
28
+ * Largest scale factor that keeps the canvas inside the browser's limits
29
+ *
30
+ * Returns `pixelRatio` unchanged when it already fits. May return a value below
31
+ * 1 for a timeline that is wider than the maximum canvas on its own.
32
+ */
33
+ export declare function resolveCanvasScale(width: number, height: number, pixelRatio: number, maxSide?: number, maxArea?: number): number;
34
+ /**
35
+ * Resolves the export's horizontal window in px
36
+ *
37
+ * With no range, the whole timeline. Throws when the requested range does not
38
+ * overlap the timeline at all - an empty image is never what the caller wanted.
39
+ */
40
+ export declare function resolveExportRangePx(range: GanttExportRange | undefined, cells: GanttBottomRowCell[], scale: GanttScaleKey, totalWidth: number): {
41
+ left: number;
42
+ width: number;
43
+ };
44
+ /** Wraps serialized XHTML in an SVG `foreignObject` and returns it as a data URL */
45
+ export declare function toSvgDataUrl(markup: string, width: number, height: number): string;
46
+ /** Waits one animation frame */
47
+ export declare function nextFrame(): Promise<void>;
48
+ export interface CaptureParams {
49
+ /** Left edge of the exported window, in timeline px */
50
+ left: number;
51
+ /** Width of the exported window, in timeline px */
52
+ width: number;
53
+ background: string;
54
+ pixelRatio: number;
55
+ }
56
+ /**
57
+ * Rasterizes the scroll container's full content to a PNG blob
58
+ *
59
+ * The chart is DOM, not canvas, so it is captured by cloning the subtree,
60
+ * inlining the computed styles it actually uses, and handing the result to the
61
+ * browser's own renderer through `<svg><foreignObject>`. The clone is never
62
+ * attached to the document, so the live chart is untouched.
63
+ *
64
+ * The caller is responsible for having every row, arrow and header cell
65
+ * rendered before this runs - a virtualized chart only holds the visible slice.
66
+ */
67
+ export declare function captureScrollContainer(scrollEl: HTMLElement, { left, width, background, pixelRatio }: CaptureParams): Promise<Blob>;
@@ -0,0 +1,31 @@
1
+ /** How long a finger has to rest on a bar before the drag lifts (ms) */
2
+ export declare const TOUCH_LONG_PRESS_MS = 400;
3
+ /** How far the finger may drift during that wait before the press is read as a scroll (px) */
4
+ export declare const TOUCH_SLOP_PX = 10;
5
+ export interface PointerGestureStart {
6
+ pointerType: string;
7
+ pointerId: number;
8
+ clientX: number;
9
+ clientY: number;
10
+ }
11
+ /**
12
+ * Starts a drag gesture the way the pointer that began it expects.
13
+ *
14
+ * A mouse press is unambiguous, so `onStart` runs immediately and nothing changes
15
+ * for existing behavior. Touch and pen have to be disambiguated from a scroll:
16
+ * `onStart` runs only once the pointer has stayed within `TOUCH_SLOP_PX` for
17
+ * `TOUCH_LONG_PRESS_MS`, so a swipe over a bar still scrolls the timeline and only
18
+ * a deliberate press lifts it.
19
+ *
20
+ * Returns the function that aborts a pending long press, or null when the gesture
21
+ * already started (a mouse, or nothing left to abort).
22
+ */
23
+ export declare function armPointerGesture(start: PointerGestureStart, onStart: (clientX: number, clientY: number) => void, target?: EventTarget): (() => void) | null;
24
+ /**
25
+ * Stops the browser from scrolling while a touch drag is running.
26
+ *
27
+ * `touch-action` is fixed when the gesture starts, and bars have to allow panning
28
+ * so a swipe can scroll - so once the long press lifts a bar, the scroll has to be
29
+ * suppressed here instead. The listener must be non-passive for that to work.
30
+ */
31
+ export declare function suppressTouchScroll(target?: EventTarget): () => void;
@@ -0,0 +1,69 @@
1
+ import { Task } from '../types/task';
2
+ import { TaskTree } from '../core/tree';
3
+ /**
4
+ * Row reordering
5
+ *
6
+ * Row order comes from the dotted `sequence`, nesting from `parentId` - two independent
7
+ * sources. A drop only has to change `parentId`, but leaving `sequence` alone would put the
8
+ * row back where it was on the next sort. So a move renumbers `sequence` from the resulting
9
+ * tree: it becomes a derived value (position among siblings, prefixed by the parent's
10
+ * sequence) and the two sources can no longer disagree. That costs a new sequence on every
11
+ * row after the move, which is the price of the round-trip through `onTasksChange` landing
12
+ * where the user dropped it.
13
+ *
14
+ * Sibling order before a move still comes from `sequence`, so every tree here is built from
15
+ * the sequence-sorted array - `childIds` and `rootIds` are then in row order.
16
+ */
17
+ /** A row as the resolver sees it - TaskTransformed fits as-is */
18
+ export interface RowDropRow {
19
+ id: string;
20
+ }
21
+ export interface RowDropTarget {
22
+ /**
23
+ * `"line"` - an insertion line drawn at the top edge of row `rowIndex`
24
+ * (`rowIndex === rows.length` means below the last row).
25
+ * `"into"` - row `rowIndex` is highlighted and becomes the new parent.
26
+ */
27
+ mode: "line" | "into";
28
+ rowIndex: number;
29
+ /** Tree depth the dragged row lands at - how far the indicator is indented */
30
+ depth: number;
31
+ /** The new parent (null = root) */
32
+ parentId: string | null;
33
+ /** Position among the new parent's children, counted after the row is detached */
34
+ index: number;
35
+ /** false when the drop would put the row inside its own subtree - never committed */
36
+ valid: boolean;
37
+ }
38
+ export interface ResolveRowDropOptions {
39
+ draggedId: string;
40
+ /** Pointer Y measured from the top of the first row (px) */
41
+ offsetY: number;
42
+ /** Pointer X travel since the drag started (px) - positive indents, negative outdents */
43
+ deltaX: number;
44
+ /** Tree of the full task list, built from the sequence-sorted array */
45
+ tree: TaskTree;
46
+ /** The dragged subtree's ids - anything in here is an illegal parent */
47
+ blockedIds: Set<string>;
48
+ rowHeight?: number;
49
+ indentWidth?: number;
50
+ }
51
+ /**
52
+ * Where a pointer at (offsetY, deltaX) would drop the dragged row
53
+ *
54
+ * `rows` are the rows actually on screen (collapsed subtrees already filtered out), in row
55
+ * order. Returns null when the dragged row is not among them.
56
+ */
57
+ export declare function resolveRowDropTarget(rows: RowDropRow[], options: ResolveRowDropOptions): RowDropTarget | null;
58
+ /**
59
+ * The task array with `moveId` re-parented to `parentId` at `index` among its new siblings
60
+ *
61
+ * Every `sequence` is rewritten from the resulting tree, so the dotted string and the parent
62
+ * chain agree afterwards - the array survives a round-trip through `onTasksChange`.
63
+ * `parentId` is only ever written on the moved task; a task whose parent link is an orphan or
64
+ * a cycle keeps that link and is numbered as the root the tree already treats it as.
65
+ *
66
+ * Returns the input array unchanged when the move is unknown, illegal (the target is inside
67
+ * the moved subtree) or a no-op.
68
+ */
69
+ export declare function moveTaskInTree(tasks: Task[], moveId: string, parentId: string | null, index: number): Task[];
@@ -0,0 +1,161 @@
1
+ import { Dayjs } from 'dayjs';
2
+ import { GanttBottomRowCell, GanttDateRange, GanttDragBounds, GanttDragMode, GanttLocaleOptions, GanttMarker, GanttRangeBand, GanttRangeExtension, GanttScaleKey, GanttTopHeaderGroup, GanttVisibleRange } from '../types/gantt';
3
+ import { Task, TaskTransformed } from '../types/task';
4
+ export interface TimelineData {
5
+ bottomCells: GanttBottomRowCell[];
6
+ transformedTasks: TaskTransformed[];
7
+ }
8
+ export interface NonWorkingRange {
9
+ left: number;
10
+ width: number;
11
+ }
12
+ /**
13
+ * How far the timeline origin moved, in px - the scrollLeft compensation
14
+ *
15
+ * The timeline range comes from the tasks' min/max dates, so dragging the
16
+ * earliest task moves the origin as a whole and pushes every bar across the
17
+ * screen. Adding this value to scrollLeft keeps the date you were looking at
18
+ * in place.
19
+ *
20
+ * Positive when cells were added in front, negative when they were removed.
21
+ */
22
+ export declare function originShiftPx(prevTicks: GanttBottomRowCell[], nextTicks: GanttBottomRowCell[], scaleKey: GanttScaleKey): number;
23
+ /**
24
+ * Merges non-working (weekend/holiday) cells into px ranges
25
+ * Only applies to scales whose tick unit is a day or finer
26
+ */
27
+ export declare function computeNonWorkingRanges(timelineTicks: GanttBottomRowCell[], scaleKey: GanttScaleKey, isNonWorkingDay: (date: Dayjs) => boolean): NonWorkingRange[];
28
+ /**
29
+ * Moves a date by the given number of drag steps
30
+ *
31
+ * Adds the scale's drag unit (hour/day) as-is. Converting to minutes first
32
+ * (day = 1440 minutes) drifts by an hour at a local-calendar DST boundary,
33
+ * where a day is 23 or 25 hours, and tasks near midnight get committed to an
34
+ * entirely different date cell.
35
+ */
36
+ export declare function shiftByDragSteps(date: Dayjs, steps: number, scaleKey: GanttScaleKey): Dayjs;
37
+ /**
38
+ * Distance between two dates in px, at the scale's drag resolution
39
+ *
40
+ * The inverse of shiftByDragSteps: a date moved by N drag steps sits exactly
41
+ * N * basePxPerDragStep away. Fractional, so a date clamped part-way through a
42
+ * step still gets an exact px offset.
43
+ */
44
+ export declare function pxBetweenDates(from: Dayjs, to: Dayjs, scaleKey: GanttScaleKey): number;
45
+ /**
46
+ * Clamps a dragged bar into its allowed date window
47
+ *
48
+ * A drag that runs past a bound snaps to it instead of stopping short or
49
+ * jumping - the returned dates are what gets both previewed and committed.
50
+ *
51
+ * - `bar`: both ends move together, so the bar keeps its length. When the bar is
52
+ * longer than the window itself the two bounds cannot both hold; `min` wins.
53
+ * - `left`/`right`: only the dragged edge moves, and the bar is kept at least one
54
+ * drag step wide. That non-inversion guard is applied last, so a task whose
55
+ * window has already been passed stays a valid bar rather than folding over.
56
+ *
57
+ * Returns the inputs untouched when no bound is set.
58
+ */
59
+ export declare function clampDragDates(mode: GanttDragMode, startDate: Dayjs, endDate: Dayjs, bounds: GanttDragBounds, scaleKey: GanttScaleKey): {
60
+ startDate: Dayjs;
61
+ endDate: Dayjs;
62
+ };
63
+ /**
64
+ * Largest shared move that keeps every bar inside its own window
65
+ *
66
+ * Bars dragged as one group - a summary row and its subtree - have to move by a
67
+ * single delta or the group tears apart, so the group moves by the smallest
68
+ * magnitude any member allows. A descendant's own bounds therefore constrain the
69
+ * whole drag: no bar can be pushed out of its window by grabbing its parent.
70
+ *
71
+ * The result always lies between 0 and the requested delta, so a bar that is
72
+ * already outside its window simply refuses to move further, rather than yanking
73
+ * the group backwards against the drag.
74
+ */
75
+ export declare function clampMoveDelta(members: {
76
+ start: Dayjs;
77
+ end: Dayjs;
78
+ bounds: GanttDragBounds;
79
+ }[], requestedMs: number, scaleKey: GanttScaleKey): number;
80
+ export declare function calculateDateOffsets(startDate: Dayjs, endDate: Dayjs, timelineTicks: GanttBottomRowCell[], scaleKey: GanttScaleKey): {
81
+ barMarginLeftAmount: number;
82
+ barWidthSize: number;
83
+ };
84
+ /**
85
+ * Computes the px offset of a given date along the timeline
86
+ * Returns null when the date is outside the timeline range
87
+ */
88
+ export declare function calculateDateOffsetPx(date: Dayjs, timelineTicks: GanttBottomRowCell[], scaleKey: GanttScaleKey): number | null;
89
+ /**
90
+ * Date sitting at a px offset along the timeline - the inverse of calculateDateOffsetPx
91
+ * Returns null when the offset falls outside the rendered range
92
+ *
93
+ * Used to remember what the cursor was pointing at before a zoom, so the same date can be
94
+ * put back under it afterwards.
95
+ */
96
+ export declare function dateAtOffsetPx(offsetPx: number, timelineTicks: GanttBottomRowCell[], scaleKey: GanttScaleKey): Dayjs | null;
97
+ /** First and last moment the rendered ticks cover (null for an empty timeline) */
98
+ export declare function timelineRange(timelineTicks: GanttBottomRowCell[], scaleKey: GanttScaleKey): GanttDateRange | null;
99
+ /** A marker placed on the timeline, with the overrun check already resolved */
100
+ export interface PositionedMarker {
101
+ marker: GanttMarker;
102
+ leftPx: number;
103
+ /** A task covered by the marker ends past its date */
104
+ overrun: boolean;
105
+ }
106
+ /**
107
+ * Places markers on the timeline, dropping the ones outside the rendered range
108
+ *
109
+ * `warnOnOverrun` markers report whether a task ends past their date - every task, or
110
+ * only the ones named in `taskIds`.
111
+ */
112
+ export declare function computeMarkerOffsets(markers: GanttMarker[], timelineTicks: GanttBottomRowCell[], scaleKey: GanttScaleKey, tasks?: Pick<Task, "id" | "endDate">[]): PositionedMarker[];
113
+ /** A range band placed on the timeline */
114
+ export interface PositionedBand {
115
+ band: GanttRangeBand;
116
+ leftPx: number;
117
+ widthPx: number;
118
+ }
119
+ export interface DrawnRange {
120
+ startDate: Dayjs;
121
+ endDate: Dayjs;
122
+ /** Where the snapped range sits on the timeline - the ghost bar's box (px) */
123
+ leftPx: number;
124
+ widthPx: number;
125
+ }
126
+ /**
127
+ * Places range bands on the timeline, dropping the ones that miss the rendered range
128
+ * entirely (a band that only overlaps it is clipped to the part that is on screen)
129
+ */
130
+ export declare function computeBandRects(bands: GanttRangeBand[], timelineTicks: GanttBottomRowCell[], scaleKey: GanttScaleKey): PositionedBand[];
131
+ /**
132
+ * Turns a range drawn on the timeline into dates snapped to the current scale
133
+ *
134
+ * Both arguments are px from the timeline's left edge - the same coordinates the bars
135
+ * are positioned in. The range snaps outwards to the ticks the two ends landed on, so the
136
+ * proposed task lines up with the columns on screen, and a range that stays inside one
137
+ * tick still comes out one tick long. Null when the timeline has no cells.
138
+ */
139
+ export declare function snapDrawnRange(startPx: number, endPx: number, timelineTicks: GanttBottomRowCell[], scaleKey: GanttScaleKey): DrawnRange | null;
140
+ /**
141
+ * Builds the top header groups from the bottom cells
142
+ * Used by the header component
143
+ */
144
+ export declare function createTopHeaderGroups(bottomCells: GanttBottomRowCell[], selectedScale: GanttScaleKey, localeOptions?: GanttLocaleOptions): GanttTopHeaderGroup[];
145
+ /**
146
+ * Computes the timeline data
147
+ * Returns bottomCells and transformedTasks for the given rawTasks and scale
148
+ *
149
+ * `visibleRange` pins either end of the window. A pinned end is used verbatim
150
+ * (no task fitting, no buffer padding) so the chart renders exactly what was
151
+ * asked for; an open end still auto-fits to the tasks as before.
152
+ *
153
+ * With hierarchy on, a parentId tree is built and parents are recomputed as summary rows.
154
+ * (Rolled up before the range is computed so dates derived from children also widen an
155
+ * auto-fitted timeline)
156
+ *
157
+ * `extension` widens the auto-fitted range beyond the usual buffer - that is how scrolling
158
+ * past an edge grows the timeline instead of hitting a wall. An end pinned by
159
+ * `visibleRange` is left exactly where the host put it.
160
+ */
161
+ export declare function computeTimelineData(rawTasks: Task[], selectedScale: GanttScaleKey, visibleRange?: GanttVisibleRange, hierarchy?: boolean, extension?: GanttRangeExtension): TimelineData;
@@ -0,0 +1,15 @@
1
+ import { GanttBottomRowCell, GanttScaleKey } from '../types/gantt';
2
+ import { Task, TaskTransformed } from '../types/task';
3
+ import { TaskTree } from '../core/tree';
4
+ /**
5
+ * Sort tasks by their sequence hierarchy
6
+ *
7
+ * Row order comes from this and nothing else - '1.10' lands after '1.2' because the
8
+ * segments compare as numbers.
9
+ */
10
+ export declare function sortTasksBySequence(tasks: Task[]): Task[];
11
+ /**
12
+ * @param tree the parentId tree (only when hierarchy is on) - given, depth comes from the
13
+ * parent chain instead of sequence, and rows with children are marked as summaries
14
+ */
15
+ export declare function transformTasks(tasks: Task[], timelineTicks: GanttBottomRowCell[], selectedScale: GanttScaleKey, tree?: TaskTree): TaskTransformed[];
@@ -0,0 +1,95 @@
1
+ import { GanttRangeExtension, GanttScaleKey } from '../types/gantt';
2
+ /**
3
+ * The zoom ladder, finest first
4
+ *
5
+ * Declaration order in GANTT_SCALE_CONFIG is the ladder - the scale selector lists the
6
+ * scales in the same order, so a wheel step and an arrow key move the same way.
7
+ */
8
+ export declare const SCALE_LADDER: GanttScaleKey[];
9
+ /**
10
+ * Roughly how many px one millisecond takes at a scale
11
+ *
12
+ * Exact for every scale whose ticks are a fixed duration, and close enough for the
13
+ * month-tick scales (a month is 28-31 days), which is all `fitScale` needs.
14
+ */
15
+ export declare function pxPerMs(scale: GanttScaleKey): number;
16
+ /** Moves `direction` places along the ladder (negative = finer), clamped at both ends */
17
+ export declare function stepScale(scale: GanttScaleKey, direction: number): GanttScaleKey;
18
+ /**
19
+ * Finest scale at which `durationMs` still fits in `viewportPx`
20
+ *
21
+ * Falls back to the coarsest scale when even that is too narrow - a project that does not
22
+ * fit anywhere is shown as wide as the ladder goes rather than not zoomed at all.
23
+ */
24
+ export declare function fitScale(durationMs: number, viewportPx: number): GanttScaleKey;
25
+ export interface ZoomAccumulator {
26
+ /** Wheel delta collected during this gesture */
27
+ delta: number;
28
+ /** Timestamp of the last wheel event */
29
+ lastEventAt: number;
30
+ /** This gesture has already produced its step - the next pause unlocks it */
31
+ locked: boolean;
32
+ }
33
+ export declare const INITIAL_ZOOM_ACCUMULATOR: ZoomAccumulator;
34
+ /**
35
+ * Folds one wheel delta into the accumulator and reports whether it makes a scale step
36
+ *
37
+ * `step` is -1 (finer), 0 (nothing yet) or 1 (coarser). Small trackpad deltas add up until
38
+ * they reach the threshold, and once a gesture has stepped, everything else it fires is
39
+ * swallowed until the stream pauses - so a pinch that runs for half a second is one scale
40
+ * step, not five, while two deliberate flicks are still two.
41
+ */
42
+ export declare function accumulateZoom(state: ZoomAccumulator, deltaY: number, now: number): {
43
+ state: ZoomAccumulator;
44
+ step: number;
45
+ };
46
+ /** How close to the edge a drag has to get before the timeline starts scrolling (px) */
47
+ export declare const EDGE_SCROLL_THRESHOLD = 48;
48
+ /** Fastest the timeline scrolls itself, at the very edge (px per frame) */
49
+ export declare const EDGE_SCROLL_MAX_SPEED = 22;
50
+ /**
51
+ * Auto-scroll speed for a pointer at `clientX`, in px per frame
52
+ *
53
+ * 0 while the pointer is away from the edges, then ramps linearly to `maxSpeed` at the
54
+ * edge itself, so a drag creeps at the boundary and races once it is pinned against it.
55
+ * The zone never eats more than half the viewport, so a narrow timeline does not
56
+ * auto-scroll everywhere.
57
+ */
58
+ export declare function edgeScrollVelocity(clientX: number, left: number, right: number, threshold?: number, maxSpeed?: number): number;
59
+ export declare const NO_RANGE_EXTENSION: GanttRangeExtension;
60
+ /**
61
+ * Cap on the ticks added per side
62
+ *
63
+ * Every task is positioned by walking the tick array, so an unbounded range would make
64
+ * that walk unbounded too. 2000 ticks is years of headroom at every scale.
65
+ */
66
+ export declare const MAX_RANGE_EXTENSION_TICKS = 2000;
67
+ interface ExtendRangeParams {
68
+ current: GanttRangeExtension;
69
+ scrollLeft: number;
70
+ /** Visible timeline width - the task list pane's share already taken off */
71
+ viewportPx: number;
72
+ /** Width of the whole rendered timeline */
73
+ totalPx: number;
74
+ /** Average px one tick takes at the current scale */
75
+ pxPerTick: number;
76
+ /**
77
+ * Ends that are still free to move (default: both)
78
+ *
79
+ * An end pinned by `visibleRange` cannot grow, and extending it would just be recomputed
80
+ * into the same timeline on every scroll event.
81
+ */
82
+ canExtend?: {
83
+ before: boolean;
84
+ after: boolean;
85
+ };
86
+ }
87
+ /**
88
+ * The extension the range should grow to, or null when the current one still covers the view
89
+ *
90
+ * Extends by roughly a viewport at a time once the view comes within half a viewport of
91
+ * either end, so the user never reaches the wall. One extension is always enough to push
92
+ * the edge back out of the trigger zone, so this cannot loop.
93
+ */
94
+ export declare function extendRangeForScroll({ current, scrollLeft, viewportPx, totalPx, pxPerTick, canExtend, }: ExtendRangeParams): GanttRangeExtension | null;
95
+ export {};