@marianmeres/stuic 3.188.0 → 3.190.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/AGENTS.md CHANGED
@@ -23,7 +23,7 @@
23
23
 
24
24
  ```
25
25
  src/lib/
26
- ├── components/ # 80 component directories
26
+ ├── components/ # 82 component directories
27
27
  ├── actions/ # 16 Svelte actions (use: directives)
28
28
  ├── attachments/ # Svelte attachments ({@attach} — preferred for new DOM helpers)
29
29
  ├── utils/ # 55 utility modules (48 on the barrel)
@@ -146,7 +146,7 @@ so it is the only confusable pair — do not "fix" one into the other.
146
146
 
147
147
  ### Domain Docs
148
148
 
149
- - [Components](./docs/domains/components.md) — 80 component directories, Props pattern, snippets
149
+ - [Components](./docs/domains/components.md) — 82 component directories, Props pattern, snippets
150
150
  - [Theming](./docs/domains/theming.md) — CSS tokens, dark mode, themes
151
151
  - [CSS presets](./docs/domains/css-presets.md) — ratio-locked frame (letterbox), safe-area, scrollbar
152
152
  - [Actions](./docs/domains/actions.md) — 16 Svelte directives
package/API.md CHANGED
@@ -1230,6 +1230,86 @@ Responsive data table with paging, row selection, batch actions, and mobile card
1230
1230
  />
1231
1231
  ```
1232
1232
 
1233
+ #### `Gantt`
1234
+
1235
+ Horizontal schedule chart — project plans (task per row, progress, milestones) and resource lanes (many bars per row). Whole-day ranges, inclusive at both ends; day, week or month columns.
1236
+
1237
+ | Prop | Type | Default | Description |
1238
+ | ------------- | ------------------------------- | ---------- | ---------------------------------------------------------- |
1239
+ | `rows` | `GanttRow[]` | required | The lanes, in display order |
1240
+ | `from` / `to` | `IsoDate \| Date` | bar extent | The window, inclusive at both ends |
1241
+ | `unit` | `"day" \| "week" \| "month"` | `"day"` | Column granularity |
1242
+ | `unitWidth` | `number \| "fit"` | `2.5rem` | **Minimum** column width in px; `"fit"` drops the minimum |
1243
+ | `today` | `IsoDate \| Date \| null` | today | The marker line's day; `null` removes it |
1244
+ | `labels` | `boolean` | auto | Left label column — on when any row has a `label` |
1245
+ | `barLabels` | `"inside" \| "after" \| "none"` | `"inside"` | Where a bar's own label is drawn |
1246
+ | `onSelect` | `(detail) => void` | — | Bar click; also what turns bars into focusable `<button>`s |
1247
+
1248
+ ```svelte
1249
+ <Gantt
1250
+ rows={[
1251
+ {
1252
+ label: "Design",
1253
+ bars: [
1254
+ {
1255
+ from: "2026-09-07",
1256
+ to: "2026-09-18",
1257
+ label: "Wireframes",
1258
+ intent: "primary",
1259
+ progress: 0.65,
1260
+ },
1261
+ ],
1262
+ },
1263
+ {
1264
+ label: "Launch",
1265
+ bars: [
1266
+ { from: "2026-10-15", milestone: true, label: "Go live", intent: "success" },
1267
+ ],
1268
+ },
1269
+ ]}
1270
+ onSelect={(d) => console.log(d.row.label, d.bar.from)}
1271
+ />
1272
+ ```
1273
+
1274
+ The geometry is exported separately for axis-aligned overlays: `buildGanttAxis`, `placeRange`, `placePoint`, `dayToFraction`, `boundsOf`.
1275
+
1276
+ #### `ListGroup`
1277
+
1278
+ Bordered, rounded box of rows split by hairlines, with an optional header (title + aside) and footer. Each row is one wrapping flex line of parts; `data-grow` on the part that should absorb the slack. Generic over the item type.
1279
+
1280
+ | Prop | Type | Default | Description |
1281
+ | ------------ | -------------------------------------- | ------- | ---------------------------------------------------------------------- |
1282
+ | `items` | `T[]` | — | The rows (without `renderItem`, each renders as `THC`) |
1283
+ | `renderItem` | `Snippet<[{ item, index }]>` | — | Row content |
1284
+ | `children` | `Snippet` | — | Hand-written `<li>`s instead of `items` (styled identically) |
1285
+ | `getItemId` | `(item, index) => string \| number` | index | Keyed identity |
1286
+ | `itemProps` | `(item, index) => ListGroupItemProps` | — | Attributes (`data-*`, `class`) on each `<li>` |
1287
+ | `itemHref` | `(item, index) => string \| undefined` | — | Makes the whole row a link |
1288
+ | `title` | `THC` | — | Header start side; labels the list |
1289
+ | `titleLevel` | `1…6` | — | Render the title as `<hN>` (semantics only) |
1290
+ | `aside` | `THC` | — | Header end side |
1291
+ | `footer` | `THC` | — | A line under the rows |
1292
+ | `empty` | `THC` | — | Replaces the list when there are no rows; without it, nothing renders |
1293
+ | `listProps` | `ListGroupListProps` | — | Attributes for the `<ul>` (e.g. `aria-label` when there is no `title`) |
1294
+
1295
+ ```svelte
1296
+ <ListGroup
1297
+ class="text-sm"
1298
+ items={lines}
1299
+ getItemId={(l) => l.id}
1300
+ itemProps={(l) => ({ "data-line": l.id })}
1301
+ title="Loose items"
1302
+ aside="70 pc"
1303
+ empty="Nothing booked yet."
1304
+ >
1305
+ {#snippet renderItem({ item })}
1306
+ <span class="font-mono">{item.code}</span>
1307
+ <span data-grow>{item.name}</span>
1308
+ <span>×{item.qty}</span>
1309
+ {/snippet}
1310
+ </ListGroup>
1311
+ ```
1312
+
1233
1313
  #### `ImageCycler`
1234
1314
 
1235
1315
  Auto-cycling image carousel with fade transitions. Preloads next image before displaying. Supports custom title/description snippets.
@@ -0,0 +1,451 @@
1
+ <script lang="ts" module>
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ import type { Snippet } from "svelte";
4
+ import type { Weekday } from "@marianmeres/calendar-utils";
5
+ import type { THC } from "../Thc/Thc.svelte";
6
+ import type { IntentColorKey } from "../../utils/design-tokens.js";
7
+ import type {
8
+ GanttAxis,
9
+ GanttColumn,
10
+ GanttDateInput,
11
+ GanttGroup,
12
+ GanttPlacement,
13
+ GanttUnit,
14
+ } from "./gantt-geometry.js";
15
+
16
+ /** Where a bar's own label is drawn. */
17
+ export type GanttBarLabels = "inside" | "after" | "none";
18
+
19
+ /** One bar (or milestone) on a row's track. */
20
+ export interface GanttBar {
21
+ /** Stable key — falls back to the index, which is fine for a static list. */
22
+ id?: string;
23
+ /** First day, inclusive. `YYYY-MM-DD`, or a `Date` (its **local** calendar date). */
24
+ from: GanttDateInput;
25
+ /** Last day, **inclusive**. Omitted = a single day. */
26
+ to?: GanttDateInput;
27
+ /** Text on the bar (see `barLabels`) */
28
+ label?: THC;
29
+ /** Semantic color */
30
+ intent?: IntentColorKey;
31
+ /** `0`–`1` — a darker fill from the bar's start ("% complete") */
32
+ progress?: number;
33
+ /** Draw a diamond at `from` instead of a bar (`to` is ignored) */
34
+ milestone?: boolean;
35
+ /** Draw thinner and on top — an emphasis range inside a wider bar of the same row */
36
+ inset?: boolean;
37
+ /** Renders the bar as a link */
38
+ href?: string;
39
+ /** Native tooltip (`title`) — the cheap way to show the detail a bar has no room for */
40
+ title?: string;
41
+ /** Not clickable, dimmed */
42
+ disabled?: boolean;
43
+ /** Additional classes for this bar */
44
+ class?: string;
45
+ /** Anything of yours — handed back by `onSelect` */
46
+ data?: unknown;
47
+ }
48
+
49
+ /** One lane of the chart: a label on the left, any number of bars on its track. */
50
+ export interface GanttRow {
51
+ /** Stable key — falls back to the index */
52
+ id?: string;
53
+ /** The left column's primary line */
54
+ label?: THC;
55
+ /** Secondary line under the label */
56
+ description?: THC;
57
+ /** The row's bars — one for a classic task, several for a resource lane */
58
+ bars: GanttBar[];
59
+ /** Renders the row label as a link */
60
+ href?: string;
61
+ /** Anything of yours — handed back by `onSelect` */
62
+ data?: unknown;
63
+ }
64
+
65
+ /** What `onSelect` (and the `renderBar` snippet) receives. */
66
+ export interface GanttSelectDetail {
67
+ bar: GanttBar;
68
+ row: GanttRow;
69
+ rowIndex: number;
70
+ barIndex: number;
71
+ /** The bar's track geometry, or `null` for a milestone (see `point`) */
72
+ placement: GanttPlacement | null;
73
+ /** A milestone's 0..1 position on the track, or `null` for a bar */
74
+ point: number | null;
75
+ }
76
+
77
+ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
78
+ /** The lanes, in display order (sort them yourself) */
79
+ rows: GanttRow[];
80
+ /** Window start, inclusive. Default: the earliest date in `rows` */
81
+ from?: GanttDateInput;
82
+ /** Window end, inclusive. Default: the latest date in `rows` */
83
+ to?: GanttDateInput;
84
+ /** Column granularity. `week`/`month` snap the window outwards to whole units */
85
+ unit?: GanttUnit;
86
+ /** First day of a week for `unit="week"` (1 = Monday … 7 = Sunday) */
87
+ weekStartsOn?: Weekday;
88
+ /** BCP 47 tag for month/weekday names and the default `aria-label`s */
89
+ locale?: string;
90
+ /** The day the marker line sits on. `null` removes it. Default: today */
91
+ today?: GanttDateInput | null;
92
+ /**
93
+ * MINIMUM column width in px (the horizontal zoom). The chart always fills its
94
+ * parent; when the axis needs less than that, the columns share out the rest.
95
+ * When it needs more, the frame scrolls horizontally.
96
+ *
97
+ * `"fit"` drops the minimum: the columns always divide the available width, so
98
+ * the chart never scrolls horizontally however many of them there are.
99
+ *
100
+ * Default: the `--stuic-gantt-unit-width` token.
101
+ */
102
+ unitWidth?: number | "fit";
103
+ /** Show the left label column. Default: whenever any row has a label */
104
+ labels?: boolean;
105
+ /** Where a bar's own `label` is drawn */
106
+ barLabels?: GanttBarLabels;
107
+ /** Fires on a bar click (and Enter/Space) — also what makes bars focusable */
108
+ onSelect?: (detail: GanttSelectDetail) => void;
109
+ /** Override a unit cell's header text */
110
+ formatColumn?: (column: GanttColumn, axis: GanttAxis) => string;
111
+ /** Override a group cell's header text (the month over its days, the year over its months) */
112
+ formatGroup?: (group: GanttGroup, axis: GanttAxis) => string;
113
+ /** Override a bar's accessible name */
114
+ formatBarAria?: (detail: GanttSelectDetail) => string;
115
+ /** Override the whole bar body (progress, label) */
116
+ renderBar?: Snippet<[GanttSelectDetail]>;
117
+ /** Override the left column's cell */
118
+ renderRowLabel?: Snippet<[{ row: GanttRow; index: number }]>;
119
+ /** The header's top-left cell (above the labels) */
120
+ renderCorner?: Snippet<[{ axis: GanttAxis }]>;
121
+ /** Shown in place of the rows when `rows` is empty (the axis still renders) */
122
+ empty?: Snippet;
123
+ /** Skip all default styling */
124
+ unstyled?: boolean;
125
+ /** Additional CSS classes */
126
+ class?: string;
127
+ /** Class for the sticky header */
128
+ classHeader?: string;
129
+ /** Class for every row */
130
+ classRow?: string;
131
+ /** Class for every left-column cell */
132
+ classRowLabel?: string;
133
+ /** Class for every row's track */
134
+ classTrack?: string;
135
+ /** Class for every bar */
136
+ classBar?: string;
137
+ /** Bindable element reference */
138
+ el?: HTMLElement;
139
+ }
140
+ </script>
141
+
142
+ <script lang="ts">
143
+ import { twMerge } from "../../utils/tw-merge.js";
144
+ import Thc from "../Thc/Thc.svelte";
145
+ import {
146
+ formatIsoDate,
147
+ formatIsoDateRange,
148
+ normalizeIsoDate,
149
+ todayIso,
150
+ addDaysIso,
151
+ } from "../Calendar/iso-date.js";
152
+ import { boundsOf, buildGanttAxis, placePoint, placeRange } from "./gantt-geometry.js";
153
+
154
+ let {
155
+ rows,
156
+ from,
157
+ to,
158
+ unit = "day",
159
+ weekStartsOn = 1,
160
+ locale,
161
+ today,
162
+ unitWidth,
163
+ labels,
164
+ barLabels = "inside",
165
+ onSelect,
166
+ formatColumn,
167
+ formatGroup,
168
+ formatBarAria,
169
+ renderBar,
170
+ renderRowLabel,
171
+ renderCorner,
172
+ empty,
173
+ unstyled = false,
174
+ class: classProp,
175
+ classHeader: classHeaderProp,
176
+ classRow: classRowProp,
177
+ classRowLabel: classRowLabelProp,
178
+ classTrack: classTrackProp,
179
+ classBar: classBarProp,
180
+ el = $bindable(),
181
+ ...rest
182
+ }: Props = $props();
183
+
184
+ const cx = (base: string, extra?: string) => (unstyled ? extra : twMerge(base, extra));
185
+
186
+ let _class = $derived(cx("stuic-gantt", classProp));
187
+ let _classHeader = $derived(cx("stuic-gantt-head", classHeaderProp));
188
+ let _classRow = $derived(cx("stuic-gantt-row", classRowProp));
189
+ let _classRowLabel = $derived(cx("stuic-gantt-row-label", classRowLabelProp));
190
+ let _classTrack = $derived(cx("stuic-gantt-track", classTrackProp));
191
+
192
+ let _today = $derived(
193
+ today === null ? null : (normalizeIsoDate(today ?? todayIso()) ?? null)
194
+ );
195
+
196
+ // The window: explicit ends win, the rows' own extent fills the rest, and with
197
+ // neither (no rows at all) a month from today — an axis is still a useful answer.
198
+ let _window = $derived.by(() => {
199
+ const explicitFrom = from === undefined ? null : normalizeIsoDate(from);
200
+ const explicitTo = to === undefined ? null : normalizeIsoDate(to);
201
+ if (explicitFrom && explicitTo) return { from: explicitFrom, to: explicitTo };
202
+ const bounds = boundsOf(rows.flatMap((r) => r.bars ?? []));
203
+ const fallback = _today ?? todayIso();
204
+ return {
205
+ from: explicitFrom ?? bounds?.from ?? fallback,
206
+ to: explicitTo ?? bounds?.to ?? addDaysIso(fallback, 29),
207
+ };
208
+ });
209
+
210
+ let axis = $derived(
211
+ buildGanttAxis(_window.from, _window.to, {
212
+ unit,
213
+ weekStartsOn,
214
+ locale,
215
+ today: _today,
216
+ })
217
+ );
218
+
219
+ let todayAt = $derived(_today ? placePoint(_today, axis) : null);
220
+
221
+ let showLabels = $derived(labels ?? rows.some((r) => r.label !== undefined));
222
+
223
+ interface PlacedBar extends GanttSelectDetail {
224
+ key: string | number;
225
+ }
226
+
227
+ let placed = $derived.by(() =>
228
+ rows.map((row, rowIndex) => ({
229
+ row,
230
+ rowIndex,
231
+ key: row.id ?? rowIndex,
232
+ bars: (row.bars ?? []).reduce<PlacedBar[]>((acc, bar, barIndex) => {
233
+ const point = bar.milestone ? placePoint(bar.from, axis) : null;
234
+ const placement = bar.milestone
235
+ ? null
236
+ : placeRange(bar.from, bar.to ?? bar.from, axis);
237
+ // Off-window (or undatable) bars are dropped rather than pinned to an
238
+ // edge, where a hairline reads as "starts today".
239
+ if (point === null && placement === null) return acc;
240
+ acc.push({
241
+ bar,
242
+ row,
243
+ rowIndex,
244
+ barIndex,
245
+ placement,
246
+ point,
247
+ key: bar.id ?? barIndex,
248
+ });
249
+ return acc;
250
+ }, []),
251
+ }))
252
+ );
253
+
254
+ // 4 decimals is sub-pixel at any realistic track width; the unary + drops the
255
+ // trailing zeros so the style attribute stays readable ("20%", not "20.0000%").
256
+ const pct = (n: number) => `${+(n * 100).toFixed(4)}%`;
257
+
258
+ function columnLabel(c: GanttColumn) {
259
+ return formatColumn ? formatColumn(c, axis) : c.label;
260
+ }
261
+
262
+ function groupLabel(g: GanttGroup) {
263
+ return formatGroup ? formatGroup(g, axis) : g.label;
264
+ }
265
+
266
+ function barAria(d: PlacedBar): string {
267
+ if (formatBarAria) return formatBarAria(d);
268
+ // Only plain-string labels can become an accessible name; an html/component
269
+ // THC is markup, and `title` is the documented way out of that.
270
+ const name = [d.row.label, d.bar.label]
271
+ .filter((v): v is string => typeof v === "string" && v !== "")
272
+ .join(" – ");
273
+ const a = normalizeIsoDate(d.bar.from);
274
+ const b = normalizeIsoDate(d.bar.to ?? d.bar.from);
275
+ const when = !a
276
+ ? ""
277
+ : !b || a === b || d.bar.milestone
278
+ ? formatIsoDate(a, locale)
279
+ : formatIsoDateRange(a, b, locale);
280
+ return [name, when].filter(Boolean).join(": ");
281
+ }
282
+
283
+ function barTag(bar: GanttBar) {
284
+ if (bar.disabled) return "span";
285
+ if (bar.href) return "a";
286
+ return onSelect ? "button" : "span";
287
+ }
288
+ </script>
289
+
290
+ {#snippet barBody(d: PlacedBar)}
291
+ {#if renderBar}
292
+ {@render renderBar(d)}
293
+ {:else}
294
+ {#if !d.bar.milestone && d.bar.progress !== undefined}
295
+ <span
296
+ class={unstyled ? undefined : "stuic-gantt-bar-progress"}
297
+ style:width={pct(Math.min(1, Math.max(0, d.bar.progress)))}
298
+ ></span>
299
+ {/if}
300
+ {#if d.bar.label !== undefined && barLabels !== "none"}
301
+ <span class={unstyled ? undefined : "stuic-gantt-bar-label"}>
302
+ <Thc thc={d.bar.label} />
303
+ </span>
304
+ {/if}
305
+ {/if}
306
+ {/snippet}
307
+
308
+ <div
309
+ bind:this={el}
310
+ class={_class}
311
+ data-unit={unit}
312
+ data-fit={unitWidth === "fit" ? "" : undefined}
313
+ data-labels={showLabels ? "" : undefined}
314
+ data-bar-labels={barLabels}
315
+ style:--stuic-gantt-unit-width={typeof unitWidth === "number"
316
+ ? `${unitWidth}px`
317
+ : undefined}
318
+ style:--_gantt-columns={axis.columns.length}
319
+ {...rest}
320
+ >
321
+ <div class={unstyled ? undefined : "stuic-gantt-viewport"}>
322
+ <div class={unstyled ? undefined : "stuic-gantt-inner"}>
323
+ <div class={_classHeader}>
324
+ {#if showLabels}
325
+ <div class={unstyled ? undefined : "stuic-gantt-corner"}>
326
+ {@render renderCorner?.({ axis })}
327
+ </div>
328
+ {/if}
329
+ <div class={unstyled ? undefined : "stuic-gantt-axis"}>
330
+ <div class={unstyled ? undefined : "stuic-gantt-groups"}>
331
+ {#each axis.groups as group (group.key)}
332
+ <div
333
+ class={unstyled ? undefined : "stuic-gantt-group"}
334
+ style:--_gantt-span={group.span}
335
+ data-group={group.key}
336
+ >
337
+ <span>{groupLabel(group)}</span>
338
+ </div>
339
+ {/each}
340
+ </div>
341
+ <div class={unstyled ? undefined : "stuic-gantt-units"}>
342
+ {#each axis.columns as column (column.date)}
343
+ <div
344
+ class={unstyled ? undefined : "stuic-gantt-unit"}
345
+ data-date={column.date}
346
+ data-weekend={column.isWeekend ? "" : undefined}
347
+ data-today={column.isToday ? "" : undefined}
348
+ >
349
+ <span class={unstyled ? undefined : "stuic-gantt-unit-label"}>
350
+ {columnLabel(column)}
351
+ </span>
352
+ {#if column.subLabel && !formatColumn}
353
+ <span class={unstyled ? undefined : "stuic-gantt-unit-sub"}>
354
+ {column.subLabel}
355
+ </span>
356
+ {/if}
357
+ </div>
358
+ {/each}
359
+ </div>
360
+ </div>
361
+ </div>
362
+
363
+ <div class={unstyled ? undefined : "stuic-gantt-body"}>
364
+ <!-- One grid layer for the whole body: the alternative is a cell per
365
+ row per column, which is rows×columns nodes for the same pixels. -->
366
+ <div class={unstyled ? undefined : "stuic-gantt-grid"} aria-hidden="true">
367
+ {#each axis.columns as column (column.date)}
368
+ <div
369
+ class={unstyled ? undefined : "stuic-gantt-gridline"}
370
+ data-weekend={column.isWeekend ? "" : undefined}
371
+ data-today={column.isToday ? "" : undefined}
372
+ ></div>
373
+ {/each}
374
+ {#if todayAt !== null}
375
+ <div
376
+ class={unstyled ? undefined : "stuic-gantt-today"}
377
+ style:left={pct(todayAt)}
378
+ data-today-marker
379
+ ></div>
380
+ {/if}
381
+ </div>
382
+
383
+ {#each placed as line (line.key)}
384
+ <div class={_classRow} data-row-id={line.row.id}>
385
+ {#if showLabels}
386
+ <div class={_classRowLabel}>
387
+ {#if renderRowLabel}
388
+ {@render renderRowLabel({ row: line.row, index: line.rowIndex })}
389
+ {:else}
390
+ {#if line.row.label !== undefined}
391
+ <span class={unstyled ? undefined : "stuic-gantt-row-title"}>
392
+ {#if line.row.href}
393
+ <a
394
+ href={line.row.href}
395
+ class={unstyled ? undefined : "stuic-gantt-row-link"}
396
+ >
397
+ <Thc thc={line.row.label} />
398
+ </a>
399
+ {:else}
400
+ <Thc thc={line.row.label} />
401
+ {/if}
402
+ </span>
403
+ {/if}
404
+ {#if line.row.description !== undefined}
405
+ <span class={unstyled ? undefined : "stuic-gantt-row-description"}>
406
+ <Thc thc={line.row.description} />
407
+ </span>
408
+ {/if}
409
+ {/if}
410
+ </div>
411
+ {/if}
412
+ <div class={_classTrack}>
413
+ {#each line.bars as d (d.key)}
414
+ {@const tag = barTag(d.bar)}
415
+ <svelte:element
416
+ this={tag}
417
+ class={cx("stuic-gantt-bar", twMerge(classBarProp, d.bar.class))}
418
+ href={tag === "a" ? d.bar.href : undefined}
419
+ type={tag === "button" ? "button" : undefined}
420
+ role={tag === "span" ? "img" : undefined}
421
+ aria-label={barAria(d)}
422
+ aria-disabled={d.bar.disabled ? "true" : undefined}
423
+ title={d.bar.title}
424
+ data-kind={d.bar.milestone ? "milestone" : "bar"}
425
+ data-intent={d.bar.intent}
426
+ data-inset={d.bar.inset ? "" : undefined}
427
+ data-disabled={d.bar.disabled ? "" : undefined}
428
+ data-clipped-start={d.placement?.clippedStart ? "" : undefined}
429
+ data-clipped-end={d.placement?.clippedEnd ? "" : undefined}
430
+ style:left={pct(d.point ?? d.placement!.start)}
431
+ style:width={d.placement
432
+ ? pct(d.placement.end - d.placement.start)
433
+ : undefined}
434
+ onclick={d.bar.disabled || !onSelect ? undefined : () => onSelect(d)}
435
+ >
436
+ {@render barBody(d)}
437
+ </svelte:element>
438
+ {/each}
439
+ </div>
440
+ </div>
441
+ {:else}
442
+ {#if empty}
443
+ <div class={unstyled ? undefined : "stuic-gantt-empty"}>
444
+ {@render empty()}
445
+ </div>
446
+ {/if}
447
+ {/each}
448
+ </div>
449
+ </div>
450
+ </div>
451
+ </div>
@@ -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;