@marianmeres/stuic 3.187.0 → 3.189.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/API.md +47 -0
- package/README.md +2 -2
- package/dist/README.md +1 -0
- package/dist/components/ColorPicker/ColorPicker.svelte +26 -1
- package/dist/components/ColorPicker/ColorPicker.svelte.d.ts +9 -0
- package/dist/components/ColorPicker/README.md +19 -7
- package/dist/components/Gantt/Gantt.svelte +451 -0
- package/dist/components/Gantt/Gantt.svelte.d.ts +134 -0
- package/dist/components/Gantt/README.md +298 -0
- package/dist/components/Gantt/gantt-geometry.d.ts +131 -0
- package/dist/components/Gantt/gantt-geometry.js +214 -0
- package/dist/components/Gantt/index.css +496 -0
- package/dist/components/Gantt/index.d.ts +2 -0
- package/dist/components/Gantt/index.js +2 -0
- package/dist/components/Input/FieldColorPicker.svelte +201 -0
- package/dist/components/Input/FieldColorPicker.svelte.d.ts +59 -0
- package/dist/components/Input/README.md +45 -0
- package/dist/components/Input/index.css +21 -9
- package/dist/components/Input/index.d.ts +1 -0
- package/dist/components/Input/index.js +1 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/utils/validate-fields.d.ts +1 -1
- package/docs/domains/components.md +64 -4
- package/package.json +1 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { Weekday } from "@marianmeres/calendar-utils";
|
|
4
|
+
import type { THC } from "../Thc/Thc.svelte";
|
|
5
|
+
import type { IntentColorKey } from "../../utils/design-tokens.js";
|
|
6
|
+
import type { GanttAxis, GanttColumn, GanttDateInput, GanttGroup, GanttPlacement, GanttUnit } from "./gantt-geometry.js";
|
|
7
|
+
/** Where a bar's own label is drawn. */
|
|
8
|
+
export type GanttBarLabels = "inside" | "after" | "none";
|
|
9
|
+
/** One bar (or milestone) on a row's track. */
|
|
10
|
+
export interface GanttBar {
|
|
11
|
+
/** Stable key — falls back to the index, which is fine for a static list. */
|
|
12
|
+
id?: string;
|
|
13
|
+
/** First day, inclusive. `YYYY-MM-DD`, or a `Date` (its **local** calendar date). */
|
|
14
|
+
from: GanttDateInput;
|
|
15
|
+
/** Last day, **inclusive**. Omitted = a single day. */
|
|
16
|
+
to?: GanttDateInput;
|
|
17
|
+
/** Text on the bar (see `barLabels`) */
|
|
18
|
+
label?: THC;
|
|
19
|
+
/** Semantic color */
|
|
20
|
+
intent?: IntentColorKey;
|
|
21
|
+
/** `0`–`1` — a darker fill from the bar's start ("% complete") */
|
|
22
|
+
progress?: number;
|
|
23
|
+
/** Draw a diamond at `from` instead of a bar (`to` is ignored) */
|
|
24
|
+
milestone?: boolean;
|
|
25
|
+
/** Draw thinner and on top — an emphasis range inside a wider bar of the same row */
|
|
26
|
+
inset?: boolean;
|
|
27
|
+
/** Renders the bar as a link */
|
|
28
|
+
href?: string;
|
|
29
|
+
/** Native tooltip (`title`) — the cheap way to show the detail a bar has no room for */
|
|
30
|
+
title?: string;
|
|
31
|
+
/** Not clickable, dimmed */
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
/** Additional classes for this bar */
|
|
34
|
+
class?: string;
|
|
35
|
+
/** Anything of yours — handed back by `onSelect` */
|
|
36
|
+
data?: unknown;
|
|
37
|
+
}
|
|
38
|
+
/** One lane of the chart: a label on the left, any number of bars on its track. */
|
|
39
|
+
export interface GanttRow {
|
|
40
|
+
/** Stable key — falls back to the index */
|
|
41
|
+
id?: string;
|
|
42
|
+
/** The left column's primary line */
|
|
43
|
+
label?: THC;
|
|
44
|
+
/** Secondary line under the label */
|
|
45
|
+
description?: THC;
|
|
46
|
+
/** The row's bars — one for a classic task, several for a resource lane */
|
|
47
|
+
bars: GanttBar[];
|
|
48
|
+
/** Renders the row label as a link */
|
|
49
|
+
href?: string;
|
|
50
|
+
/** Anything of yours — handed back by `onSelect` */
|
|
51
|
+
data?: unknown;
|
|
52
|
+
}
|
|
53
|
+
/** What `onSelect` (and the `renderBar` snippet) receives. */
|
|
54
|
+
export interface GanttSelectDetail {
|
|
55
|
+
bar: GanttBar;
|
|
56
|
+
row: GanttRow;
|
|
57
|
+
rowIndex: number;
|
|
58
|
+
barIndex: number;
|
|
59
|
+
/** The bar's track geometry, or `null` for a milestone (see `point`) */
|
|
60
|
+
placement: GanttPlacement | null;
|
|
61
|
+
/** A milestone's 0..1 position on the track, or `null` for a bar */
|
|
62
|
+
point: number | null;
|
|
63
|
+
}
|
|
64
|
+
export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
65
|
+
/** The lanes, in display order (sort them yourself) */
|
|
66
|
+
rows: GanttRow[];
|
|
67
|
+
/** Window start, inclusive. Default: the earliest date in `rows` */
|
|
68
|
+
from?: GanttDateInput;
|
|
69
|
+
/** Window end, inclusive. Default: the latest date in `rows` */
|
|
70
|
+
to?: GanttDateInput;
|
|
71
|
+
/** Column granularity. `week`/`month` snap the window outwards to whole units */
|
|
72
|
+
unit?: GanttUnit;
|
|
73
|
+
/** First day of a week for `unit="week"` (1 = Monday … 7 = Sunday) */
|
|
74
|
+
weekStartsOn?: Weekday;
|
|
75
|
+
/** BCP 47 tag for month/weekday names and the default `aria-label`s */
|
|
76
|
+
locale?: string;
|
|
77
|
+
/** The day the marker line sits on. `null` removes it. Default: today */
|
|
78
|
+
today?: GanttDateInput | null;
|
|
79
|
+
/**
|
|
80
|
+
* MINIMUM column width in px (the horizontal zoom). The chart always fills its
|
|
81
|
+
* parent; when the axis needs less than that, the columns share out the rest.
|
|
82
|
+
* When it needs more, the frame scrolls horizontally.
|
|
83
|
+
*
|
|
84
|
+
* `"fit"` drops the minimum: the columns always divide the available width, so
|
|
85
|
+
* the chart never scrolls horizontally however many of them there are.
|
|
86
|
+
*
|
|
87
|
+
* Default: the `--stuic-gantt-unit-width` token.
|
|
88
|
+
*/
|
|
89
|
+
unitWidth?: number | "fit";
|
|
90
|
+
/** Show the left label column. Default: whenever any row has a label */
|
|
91
|
+
labels?: boolean;
|
|
92
|
+
/** Where a bar's own `label` is drawn */
|
|
93
|
+
barLabels?: GanttBarLabels;
|
|
94
|
+
/** Fires on a bar click (and Enter/Space) — also what makes bars focusable */
|
|
95
|
+
onSelect?: (detail: GanttSelectDetail) => void;
|
|
96
|
+
/** Override a unit cell's header text */
|
|
97
|
+
formatColumn?: (column: GanttColumn, axis: GanttAxis) => string;
|
|
98
|
+
/** Override a group cell's header text (the month over its days, the year over its months) */
|
|
99
|
+
formatGroup?: (group: GanttGroup, axis: GanttAxis) => string;
|
|
100
|
+
/** Override a bar's accessible name */
|
|
101
|
+
formatBarAria?: (detail: GanttSelectDetail) => string;
|
|
102
|
+
/** Override the whole bar body (progress, label) */
|
|
103
|
+
renderBar?: Snippet<[GanttSelectDetail]>;
|
|
104
|
+
/** Override the left column's cell */
|
|
105
|
+
renderRowLabel?: Snippet<[{
|
|
106
|
+
row: GanttRow;
|
|
107
|
+
index: number;
|
|
108
|
+
}]>;
|
|
109
|
+
/** The header's top-left cell (above the labels) */
|
|
110
|
+
renderCorner?: Snippet<[{
|
|
111
|
+
axis: GanttAxis;
|
|
112
|
+
}]>;
|
|
113
|
+
/** Shown in place of the rows when `rows` is empty (the axis still renders) */
|
|
114
|
+
empty?: Snippet;
|
|
115
|
+
/** Skip all default styling */
|
|
116
|
+
unstyled?: boolean;
|
|
117
|
+
/** Additional CSS classes */
|
|
118
|
+
class?: string;
|
|
119
|
+
/** Class for the sticky header */
|
|
120
|
+
classHeader?: string;
|
|
121
|
+
/** Class for every row */
|
|
122
|
+
classRow?: string;
|
|
123
|
+
/** Class for every left-column cell */
|
|
124
|
+
classRowLabel?: string;
|
|
125
|
+
/** Class for every row's track */
|
|
126
|
+
classTrack?: string;
|
|
127
|
+
/** Class for every bar */
|
|
128
|
+
classBar?: string;
|
|
129
|
+
/** Bindable element reference */
|
|
130
|
+
el?: HTMLElement;
|
|
131
|
+
}
|
|
132
|
+
declare const Gantt: import("svelte").Component<Props, {}, "el">;
|
|
133
|
+
type Gantt = ReturnType<typeof Gantt>;
|
|
134
|
+
export default Gantt;
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# Gantt
|
|
2
|
+
|
|
3
|
+
Horizontal schedule chart: a shared time axis across the top, one lane per row,
|
|
4
|
+
and bars for the ranges each lane occupies. The classic project plan (task per
|
|
5
|
+
row, progress, milestones), but the same component also draws resource lanes —
|
|
6
|
+
a row can hold any number of bars.
|
|
7
|
+
|
|
8
|
+
Everything is **whole-day and inclusive at both ends**: `from: "2026-09-01",
|
|
9
|
+
to: "2026-09-03"` is three days, and no timezone can shift a bar by one. Columns
|
|
10
|
+
can be days, weeks or months; bars are positioned through the columns, so a
|
|
11
|
+
February column is the same width as March while still holding fewer days.
|
|
12
|
+
|
|
13
|
+
Read-only by design — there is no drag-to-reschedule and no dependency arrows.
|
|
14
|
+
For a vertical event list (activity feeds, audit logs) use `Timeline` instead.
|
|
15
|
+
|
|
16
|
+
## Props
|
|
17
|
+
|
|
18
|
+
| Prop | Type | Default | Description |
|
|
19
|
+
| --------------- | ------------------------------------- | ---------- | --------------------------------------------------------------------- |
|
|
20
|
+
| `rows` | `GanttRow[]` | required | The lanes, in display order (sort them yourself) |
|
|
21
|
+
| `from` | `string \| Date` | - | Window start, inclusive. Default: the earliest date in `rows` |
|
|
22
|
+
| `to` | `string \| Date` | - | Window end, inclusive. Default: the latest date in `rows` |
|
|
23
|
+
| `unit` | `"day" \| "week" \| "month"` | `"day"` | Column granularity; `week`/`month` snap the window out to whole units |
|
|
24
|
+
| `weekStartsOn` | `1`–`7` | `1` | First day of a week for `unit="week"` (1 = Monday … 7 = Sunday) |
|
|
25
|
+
| `locale` | `string` | - | BCP 47 tag for month/weekday names and the default `aria-label`s |
|
|
26
|
+
| `today` | `string \| Date \| null` | today | The day the marker line sits on; `null` removes it |
|
|
27
|
+
| `unitWidth` | `number \| "fit"` | token | **Minimum** column width in px (the zoom); `"fit"` drops the minimum |
|
|
28
|
+
| `labels` | `boolean` | auto | Show the left label column; defaults to on when any row has a `label` |
|
|
29
|
+
| `barLabels` | `"inside" \| "after" \| "none"` | `"inside"` | Where a bar's own `label` is drawn |
|
|
30
|
+
| `onSelect` | `(detail: GanttSelectDetail) => void` | - | Bar click / Enter / Space — also what makes bars focusable buttons |
|
|
31
|
+
| `formatColumn` | `(column, axis) => string` | - | Override a unit cell's header text |
|
|
32
|
+
| `formatGroup` | `(group, axis) => string` | - | Override a group cell's header text |
|
|
33
|
+
| `formatBarAria` | `(detail) => string` | - | Override a bar's accessible name |
|
|
34
|
+
| `unstyled` | `boolean` | `false` | Skip all default styling (the `data-*` contract stays) |
|
|
35
|
+
| `class` | `string` | - | Additional CSS classes (merged via twMerge) |
|
|
36
|
+
| `classHeader` | `string` | - | Class for the sticky header |
|
|
37
|
+
| `classRow` | `string` | - | Class for every row |
|
|
38
|
+
| `classRowLabel` | `string` | - | Class for every left-column cell |
|
|
39
|
+
| `classTrack` | `string` | - | Class for every row's track |
|
|
40
|
+
| `classBar` | `string` | - | Class for every bar |
|
|
41
|
+
| `el` | `HTMLElement` | - | Element reference (bindable) |
|
|
42
|
+
|
|
43
|
+
Any other attribute (`aria-label`, `style`, `data-*`, …) is passed to the root `<div>`.
|
|
44
|
+
|
|
45
|
+
### `GanttRow`
|
|
46
|
+
|
|
47
|
+
| Field | Type | Description |
|
|
48
|
+
| ------------- | ------------ | ------------------------------------------------------------- |
|
|
49
|
+
| `bars` | `GanttBar[]` | The row's ranges — one for a classic task, several for a lane |
|
|
50
|
+
| `id` | `string` | Stable key (falls back to the index) |
|
|
51
|
+
| `label` | `THC` | The left column's primary line |
|
|
52
|
+
| `description` | `THC` | Secondary line under the label |
|
|
53
|
+
| `href` | `string` | Renders the row label as a link |
|
|
54
|
+
| `data` | `unknown` | Anything of yours — handed back by `onSelect` |
|
|
55
|
+
|
|
56
|
+
### `GanttBar`
|
|
57
|
+
|
|
58
|
+
| Field | Type | Description |
|
|
59
|
+
| ----------- | ---------------- | ------------------------------------------------------------------------- |
|
|
60
|
+
| `from` | `string \| Date` | First day, inclusive. `YYYY-MM-DD`, or a `Date` (its **local** date) |
|
|
61
|
+
| `to` | `string \| Date` | Last day, **inclusive**. Omitted = a single day |
|
|
62
|
+
| `label` | `THC` | Text on the bar (see `barLabels`) |
|
|
63
|
+
| `intent` | `IntentColorKey` | `"primary" \| "accent" \| "success" \| "warning" \| "destructive"` |
|
|
64
|
+
| `progress` | `number` | `0`–`1` — a darker fill from the bar's start ("% complete"); clamped |
|
|
65
|
+
| `milestone` | `boolean` | Draw a diamond at `from` instead of a bar (`to` is ignored) |
|
|
66
|
+
| `inset` | `boolean` | Draw thinner and on top — an emphasis range inside a wider bar of the row |
|
|
67
|
+
| `href` | `string` | Renders the bar as a link |
|
|
68
|
+
| `title` | `string` | Native tooltip — the cheap way to show what a bar has no room for |
|
|
69
|
+
| `disabled` | `boolean` | Not clickable, dimmed |
|
|
70
|
+
| `id` | `string` | Stable key (falls back to the index) |
|
|
71
|
+
| `class` | `string` | Additional classes for this bar |
|
|
72
|
+
| `data` | `unknown` | Anything of yours — handed back by `onSelect` |
|
|
73
|
+
|
|
74
|
+
A bar that falls entirely outside the window is **dropped**, not pinned to the
|
|
75
|
+
edge — a hairline at the window's edge reads as "starts today", which is exactly
|
|
76
|
+
the wrong thing to tell a planner. A bar that only overhangs is clipped and
|
|
77
|
+
flagged with `data-clipped-start` / `data-clipped-end` (squared-off corners).
|
|
78
|
+
|
|
79
|
+
## Snippet Props
|
|
80
|
+
|
|
81
|
+
| Snippet | Argument | Description |
|
|
82
|
+
| ---------------- | ------------------- | ----------------------------------------------------------------------- |
|
|
83
|
+
| `renderBar` | `GanttSelectDetail` | Override the whole bar body (progress, label) |
|
|
84
|
+
| `renderRowLabel` | `{ row, index }` | Override the left column's cell |
|
|
85
|
+
| `renderCorner` | `{ axis }` | The header's top-left cell, above the labels |
|
|
86
|
+
| `empty` | – | Shown instead of the rows when `rows` is empty (the axis still renders) |
|
|
87
|
+
|
|
88
|
+
`GanttSelectDetail` is `{ bar, row, rowIndex, barIndex, placement, point }` —
|
|
89
|
+
`placement` is `{ start, end, clippedStart, clippedEnd, days }` in 0..1 track
|
|
90
|
+
fractions for a bar, and `point` is the 0..1 position of a milestone. Exactly one
|
|
91
|
+
of the two is set.
|
|
92
|
+
|
|
93
|
+
## Usage
|
|
94
|
+
|
|
95
|
+
### A project plan
|
|
96
|
+
|
|
97
|
+
```svelte
|
|
98
|
+
<script lang="ts">
|
|
99
|
+
import { Gantt, type GanttRow } from "@marianmeres/stuic";
|
|
100
|
+
|
|
101
|
+
const rows: GanttRow[] = [
|
|
102
|
+
{
|
|
103
|
+
label: "Design",
|
|
104
|
+
description: "2 people",
|
|
105
|
+
bars: [
|
|
106
|
+
{
|
|
107
|
+
from: "2026-09-07",
|
|
108
|
+
to: "2026-09-18",
|
|
109
|
+
label: "Wireframes",
|
|
110
|
+
intent: "primary",
|
|
111
|
+
progress: 0.65,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
label: "Build",
|
|
117
|
+
bars: [
|
|
118
|
+
{
|
|
119
|
+
from: "2026-09-15",
|
|
120
|
+
to: "2026-10-09",
|
|
121
|
+
label: "Implementation",
|
|
122
|
+
intent: "primary",
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
label: "Launch",
|
|
128
|
+
bars: [
|
|
129
|
+
{ from: "2026-10-15", milestone: true, label: "Go live", intent: "success" },
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
</script>
|
|
134
|
+
|
|
135
|
+
<Gantt {rows} onSelect={(d) => console.log(d.row.label, d.bar.from)} />
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Resource lanes (several bars per row)
|
|
139
|
+
|
|
140
|
+
`inset` draws a thinner bar on top of a wider one — the wide bar is the whole
|
|
141
|
+
occupancy, the thin one the part that matters.
|
|
142
|
+
|
|
143
|
+
```svelte
|
|
144
|
+
<Gantt
|
|
145
|
+
rows={[
|
|
146
|
+
{
|
|
147
|
+
label: "Truck #1",
|
|
148
|
+
bars: [
|
|
149
|
+
{
|
|
150
|
+
from: "2026-09-02",
|
|
151
|
+
to: "2026-09-06",
|
|
152
|
+
intent: "primary",
|
|
153
|
+
title: "Job A — out to return",
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
from: "2026-09-03",
|
|
157
|
+
to: "2026-09-05",
|
|
158
|
+
intent: "success",
|
|
159
|
+
inset: true,
|
|
160
|
+
title: "Job A — event days",
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
]}
|
|
165
|
+
from="2026-09-01"
|
|
166
|
+
to="2026-09-14"
|
|
167
|
+
/>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Zoom, scales and fitting
|
|
171
|
+
|
|
172
|
+
```svelte
|
|
173
|
+
<!-- a bindable zoom -->
|
|
174
|
+
<input type="range" min="12" max="80" bind:value={unitWidth} />
|
|
175
|
+
<Gantt {rows} {unitWidth} />
|
|
176
|
+
|
|
177
|
+
<!-- coarser scales; the window snaps out to whole weeks / months -->
|
|
178
|
+
<Gantt {rows} unit="week" weekStartsOn={7} />
|
|
179
|
+
<Gantt {rows} unit="month" unitWidth={90} barLabels="none" />
|
|
180
|
+
|
|
181
|
+
<!-- no horizontal scroll: the columns divide the available width -->
|
|
182
|
+
<Gantt {rows} unitWidth="fit" barLabels="after" />
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The chart always fills its parent. `unitWidth` is a **minimum**: when the axis
|
|
186
|
+
needs less width than the frame has, the columns share out what is left — so there
|
|
187
|
+
is never a dead gutter to the right of the last column — and when it needs more,
|
|
188
|
+
the frame scrolls horizontally. `unitWidth="fit"` drops the minimum entirely: the
|
|
189
|
+
columns always divide the available width, so the chart never scrolls sideways.
|
|
190
|
+
|
|
191
|
+
Bar labels that sit past their bar (`barLabels="after"`, and every milestone
|
|
192
|
+
label) are clipped at the chart's right edge rather than extending the scroll
|
|
193
|
+
area — a label is never worth a scrollbar on a chart that otherwise fits.
|
|
194
|
+
|
|
195
|
+
### Sticky header
|
|
196
|
+
|
|
197
|
+
The header is already `position: sticky`; give the chart a height and it sticks
|
|
198
|
+
while the rows scroll under it. The left label column is sticky horizontally at
|
|
199
|
+
all times.
|
|
200
|
+
|
|
201
|
+
```svelte
|
|
202
|
+
<Gantt {rows} style="--stuic-gantt-max-height: 20rem;" />
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Empty
|
|
206
|
+
|
|
207
|
+
```svelte
|
|
208
|
+
<Gantt rows={[]} from="2026-09-01" to="2026-09-21">
|
|
209
|
+
{#snippet empty()}
|
|
210
|
+
<EmptyState title="Nothing planned" description="Widen the window." />
|
|
211
|
+
{/snippet}
|
|
212
|
+
</Gantt>
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Geometry helpers
|
|
216
|
+
|
|
217
|
+
The date → track math is a separate, browser-free module, exported for building
|
|
218
|
+
your own axis-aligned overlays (a "capacity" strip, a legend, a print layout):
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import { buildGanttAxis, placeRange, placePoint } from "@marianmeres/stuic";
|
|
222
|
+
|
|
223
|
+
const axis = buildGanttAxis("2026-09-01", "2026-09-30", { unit: "day" });
|
|
224
|
+
placeRange("2026-09-03", "2026-09-04", axis);
|
|
225
|
+
// → { start: 0.0666…, end: 0.1333…, clippedStart: false, clippedEnd: false, days: 2 }
|
|
226
|
+
placePoint("2026-09-15", axis); // → 0.4833… (the middle of that day)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`buildGanttAxis` **throws** on an unparseable or backwards window, and on one
|
|
230
|
+
that would need more than `GANTT_MAX_COLUMNS` (2000) columns — at that zoom
|
|
231
|
+
nothing is readable, and quietly building 100k nodes is worse than saying so.
|
|
232
|
+
|
|
233
|
+
## Accessibility
|
|
234
|
+
|
|
235
|
+
- A bar with `onSelect` renders as a `<button>` (focusable, Enter/Space), with
|
|
236
|
+
`href` as an `<a>`, and otherwise as a `<span role="img">`.
|
|
237
|
+
- Every bar gets an `aria-label` built from the row and bar labels plus the
|
|
238
|
+
localized date range — only plain-string labels can contribute, so give
|
|
239
|
+
html/component labels a `title` or use `formatBarAria`.
|
|
240
|
+
- The gridlines, weekend shading and the today line are `aria-hidden`.
|
|
241
|
+
|
|
242
|
+
## CSS Variables
|
|
243
|
+
|
|
244
|
+
| Variable | Default | Description |
|
|
245
|
+
| ------------------------------------- | -------------------------------- | --------------------------------- |
|
|
246
|
+
| `--stuic-gantt-unit-width` | `2.5rem` | One column (the horizontal zoom) |
|
|
247
|
+
| `--stuic-gantt-label-width` | `12rem` | The sticky left column |
|
|
248
|
+
| `--stuic-gantt-row-height` | `2.5rem` | Minimum row height |
|
|
249
|
+
| `--stuic-gantt-max-height` | `none` | Set it to get a sticky header |
|
|
250
|
+
| `--stuic-gantt-bg` | `--stuic-color-background` | Frame background |
|
|
251
|
+
| `--stuic-gantt-border-color` | `--stuic-color-border` | Frame, row and column borders |
|
|
252
|
+
| `--stuic-gantt-font-size` | `--text-base` | Base size |
|
|
253
|
+
| `--stuic-gantt-header-bg` | `--stuic-color-surface` | Both header tiers |
|
|
254
|
+
| `--stuic-gantt-header-text` | `--stuic-color-foreground` | Header text |
|
|
255
|
+
| `--stuic-gantt-header-font-size` | `--text-xs` | Header text size |
|
|
256
|
+
| `--stuic-gantt-header-padding` | `0.25rem 0.375rem` | Header cell padding |
|
|
257
|
+
| `--stuic-gantt-header-sub-text` | `--stuic-color-muted-foreground` | Weekday / week-number line |
|
|
258
|
+
| `--stuic-gantt-grid-line-color` | `--stuic-color-border` | Column separators |
|
|
259
|
+
| `--stuic-gantt-weekend-bg` | `--stuic-color-muted` | Saturday/Sunday shading |
|
|
260
|
+
| `--stuic-gantt-today-color` | `--stuic-color-destructive` | Today line and header flag |
|
|
261
|
+
| `--stuic-gantt-today-width` | `2px` | Today line thickness |
|
|
262
|
+
| `--stuic-gantt-row-bg-hover` | translucent muted | Row hover (keeps shading visible) |
|
|
263
|
+
| `--stuic-gantt-label-bg` | `--stuic-color-background` | Left column (must stay opaque) |
|
|
264
|
+
| `--stuic-gantt-label-padding` | `0.375rem 0.75rem` | Left column padding |
|
|
265
|
+
| `--stuic-gantt-label-font-size` | `--text-base` | Row title size |
|
|
266
|
+
| `--stuic-gantt-label-text` | `--stuic-color-foreground` | Row title color |
|
|
267
|
+
| `--stuic-gantt-description-font-size` | `--text-xs` | Row description size |
|
|
268
|
+
| `--stuic-gantt-description-text` | `--stuic-color-muted-foreground` | Row description color |
|
|
269
|
+
| `--stuic-gantt-bar-height` | `1.25rem` | Bar height |
|
|
270
|
+
| `--stuic-gantt-bar-height-inset` | `0.625rem` | `inset` bar height |
|
|
271
|
+
| `--stuic-gantt-bar-bg` | `--stuic-color-muted-foreground` | Bar fill (per-intent overrides) |
|
|
272
|
+
| `--stuic-gantt-bar-text` | `--stuic-color-background` | Bar label color |
|
|
273
|
+
| `--stuic-gantt-bar-font-size` | `--text-xs` | Bar label size |
|
|
274
|
+
| `--stuic-gantt-bar-padding-inline` | `0.375rem` | Bar label inset |
|
|
275
|
+
| `--stuic-gantt-bar-progress-bg` | bar color, darkened | The `progress` fill |
|
|
276
|
+
| `--stuic-gantt-bar-opacity-disabled` | `0.45` | Disabled bar |
|
|
277
|
+
| `--stuic-gantt-milestone-size` | `0.875rem` | Diamond size |
|
|
278
|
+
| `--stuic-gantt-empty-padding` | `2rem 1rem` | Empty area padding |
|
|
279
|
+
|
|
280
|
+
Structural tokens are resolved as fallbacks at the usage sites, so
|
|
281
|
+
`--stuic-gantt-radius` → `--stuic-radius-container` (the frame),
|
|
282
|
+
`--stuic-gantt-bar-radius` → `--stuic-radius` (bars),
|
|
283
|
+
`--stuic-gantt-border-width` → `--stuic-border-width`, and
|
|
284
|
+
`--stuic-gantt-transition` → `--stuic-transition`.
|
|
285
|
+
|
|
286
|
+
## Data attributes
|
|
287
|
+
|
|
288
|
+
Kept in `unstyled` mode too — they describe the data, not the styling.
|
|
289
|
+
|
|
290
|
+
| Element | Attributes |
|
|
291
|
+
| ----------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
292
|
+
| root | `data-unit`, `data-fit`, `data-labels`, `data-bar-labels` |
|
|
293
|
+
| header group cell | `data-group` (`"2026-09"` / `"2026"`) |
|
|
294
|
+
| header unit cell | `data-date`, `data-weekend`, `data-today` |
|
|
295
|
+
| gridline | `data-weekend`, `data-today` |
|
|
296
|
+
| today line | `data-today-marker` |
|
|
297
|
+
| row | `data-row-id` |
|
|
298
|
+
| bar | `data-kind` (`bar`/`milestone`), `data-intent`, `data-inset`, `data-disabled`, `data-clipped-start`, `data-clipped-end` |
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Gantt's geometry: a date window in, columns and 0..1 track fractions out.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of the component (and out of the browser) on purpose — everything
|
|
5
|
+
* that could be wrong by a day is decided here, in plain functions a node test
|
|
6
|
+
* can pin. The component only multiplies fractions by a width.
|
|
7
|
+
*
|
|
8
|
+
* Whole-day semantics throughout (`YYYY-MM-DD`, both ends inclusive), so a bar
|
|
9
|
+
* never shifts by a day because the visitor is in Auckland. The day arithmetic
|
|
10
|
+
* reuses `Calendar/iso-date.js`, which is DST-safe via `@marianmeres/calendar-utils`.
|
|
11
|
+
*/
|
|
12
|
+
import type { Weekday } from "@marianmeres/calendar-utils";
|
|
13
|
+
import { type IsoDate } from "../Calendar/iso-date.js";
|
|
14
|
+
/** Column granularity of the time axis. */
|
|
15
|
+
export type GanttUnit = "day" | "week" | "month";
|
|
16
|
+
/** What a consumer may hand in as a date — normalized via `normalizeIsoDate`. */
|
|
17
|
+
export type GanttDateInput = IsoDate | Date;
|
|
18
|
+
/** One cell of the axis' bottom (unit) header row, and one background gridline. */
|
|
19
|
+
export interface GanttColumn {
|
|
20
|
+
/** The column's first calendar day, `YYYY-MM-DD`. */
|
|
21
|
+
date: IsoDate;
|
|
22
|
+
/** Calendar days the column covers (1 / 7 / 28–31). */
|
|
23
|
+
days: number;
|
|
24
|
+
/** Days from the axis' first day to this column's first day. */
|
|
25
|
+
offset: number;
|
|
26
|
+
/** Position in `axis.columns`. */
|
|
27
|
+
index: number;
|
|
28
|
+
/** Primary header label (day of month, or the month name for `unit: "month"`). */
|
|
29
|
+
label: string;
|
|
30
|
+
/** Secondary header label (weekday for days, ISO-ish week number for weeks). */
|
|
31
|
+
subLabel?: string;
|
|
32
|
+
/** Index into `axis.groups` — the top header row this column sits under. */
|
|
33
|
+
groupIndex: number;
|
|
34
|
+
/** Saturday or Sunday (`unit: "day"` only — a week/month column is never one). */
|
|
35
|
+
isWeekend: boolean;
|
|
36
|
+
/** The column contains `today`. */
|
|
37
|
+
isToday: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** One cell of the axis' top header row — the month over its days, the year over its months. */
|
|
40
|
+
export interface GanttGroup {
|
|
41
|
+
/** Stable key (`"2026-09"` / `"2026"`). */
|
|
42
|
+
key: string;
|
|
43
|
+
label: string;
|
|
44
|
+
/** Columns the cell spans. */
|
|
45
|
+
span: number;
|
|
46
|
+
/** Index of the first column it spans. */
|
|
47
|
+
from: number;
|
|
48
|
+
}
|
|
49
|
+
export interface GanttAxis {
|
|
50
|
+
unit: GanttUnit;
|
|
51
|
+
/** The window as asked for. */
|
|
52
|
+
from: IsoDate;
|
|
53
|
+
to: IsoDate;
|
|
54
|
+
/**
|
|
55
|
+
* The window as drawn. For `week`/`month` it is snapped outwards to whole
|
|
56
|
+
* units — a half-drawn week column would put its label on a lie.
|
|
57
|
+
*/
|
|
58
|
+
start: IsoDate;
|
|
59
|
+
end: IsoDate;
|
|
60
|
+
columns: GanttColumn[];
|
|
61
|
+
groups: GanttGroup[];
|
|
62
|
+
/** Calendar days from `start` to `end`, inclusive. */
|
|
63
|
+
totalDays: number;
|
|
64
|
+
}
|
|
65
|
+
/** A range's position on the track, as 0..1 fractions of the axis' full width. */
|
|
66
|
+
export interface GanttPlacement {
|
|
67
|
+
/** Left edge, 0..1. */
|
|
68
|
+
start: number;
|
|
69
|
+
/** Right edge, 0..1 (always `> start`). */
|
|
70
|
+
end: number;
|
|
71
|
+
/** The range begins before the axis — the component squares off that end. */
|
|
72
|
+
clippedStart: boolean;
|
|
73
|
+
clippedEnd: boolean;
|
|
74
|
+
/** Inclusive day count of the *visible* part. */
|
|
75
|
+
days: number;
|
|
76
|
+
}
|
|
77
|
+
export interface GanttAxisOptions {
|
|
78
|
+
unit?: GanttUnit;
|
|
79
|
+
/** First day of a week for `unit: "week"` (1 = Monday … 7 = Sunday). */
|
|
80
|
+
weekStartsOn?: Weekday;
|
|
81
|
+
/** BCP 47 tag for the month and weekday names; `undefined` = the runtime's. */
|
|
82
|
+
locale?: string;
|
|
83
|
+
/** The day to flag as today, or `null` for none. */
|
|
84
|
+
today?: IsoDate | null;
|
|
85
|
+
}
|
|
86
|
+
/** Cap on generated columns — a mistyped year would otherwise build 700k of them. */
|
|
87
|
+
export declare const GANTT_MAX_COLUMNS = 2000;
|
|
88
|
+
/** ISO-8601 week number of `iso` (weeks start Monday; week 1 holds the first Thursday). */
|
|
89
|
+
export declare function isoWeekNumber(iso: IsoDate): number;
|
|
90
|
+
/**
|
|
91
|
+
* The columns and the two header rows for a `[from, to]` window (both inclusive).
|
|
92
|
+
*
|
|
93
|
+
* Throws on an unparseable window, and on one so wide it would exceed
|
|
94
|
+
* {@link GANTT_MAX_COLUMNS} columns — at that zoom nothing is readable anyway,
|
|
95
|
+
* and silently drawing 100k nodes is worse than saying so.
|
|
96
|
+
*/
|
|
97
|
+
export declare function buildGanttAxis(from: GanttDateInput, to: GanttDateInput, options?: GanttAxisOptions): GanttAxis;
|
|
98
|
+
/**
|
|
99
|
+
* Where a day offset sits on the track, 0..1.
|
|
100
|
+
*
|
|
101
|
+
* Columns all render at the same width but do not all cover the same number of
|
|
102
|
+
* days (February against March), so this maps through the column the offset
|
|
103
|
+
* falls in rather than dividing by `totalDays`. Fractional offsets are allowed
|
|
104
|
+
* — `offset + 0.5` is the middle of that day.
|
|
105
|
+
*/
|
|
106
|
+
export declare function dayToFraction(offset: number, axis: GanttAxis): number;
|
|
107
|
+
/**
|
|
108
|
+
* A `[from, to]` inclusive range as a track placement, or `null` when it misses
|
|
109
|
+
* the axis entirely.
|
|
110
|
+
*
|
|
111
|
+
* A miss is `null` rather than a zero-width bar on purpose: a hairline pinned to
|
|
112
|
+
* the window's edge reads as "starts today", which is exactly the wrong thing to
|
|
113
|
+
* tell a planner.
|
|
114
|
+
*/
|
|
115
|
+
export declare function placeRange(from: GanttDateInput, to: GanttDateInput, axis: GanttAxis): GanttPlacement | null;
|
|
116
|
+
/**
|
|
117
|
+
* The middle of a single day as a 0..1 fraction (milestones, the today line), or
|
|
118
|
+
* `null` when the day is outside the axis.
|
|
119
|
+
*/
|
|
120
|
+
export declare function placePoint(date: GanttDateInput, axis: GanttAxis): number | null;
|
|
121
|
+
/**
|
|
122
|
+
* The `[min, max]` of every date in `ranges` — what the axis defaults to when
|
|
123
|
+
* the consumer gives no explicit window. `null` when nothing is datable.
|
|
124
|
+
*/
|
|
125
|
+
export declare function boundsOf(ranges: Iterable<{
|
|
126
|
+
from?: GanttDateInput;
|
|
127
|
+
to?: GanttDateInput;
|
|
128
|
+
}>): {
|
|
129
|
+
from: IsoDate;
|
|
130
|
+
to: IsoDate;
|
|
131
|
+
} | null;
|