@konce-pt/angular 0.7.12 → 0.8.1

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.
@@ -1,1271 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { inject, input, booleanAttribute, model, linkedSignal, computed, output, viewChild, signal, afterRenderEffect, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
3
3
  import { KptI18n, KptTooltip } from '@konce-pt/angular';
4
-
5
- /*
6
- * Arytmetyka kalendarzowa w czasie LOKALNYM, odporna na zmianę czasu (DST).
7
- *
8
- * Roadmapa jest narzędziem planistycznym — użytkownik myśli zegarem ściennym,
9
- * więc wszystkie obliczenia idą po lokalnych polach daty.
10
- *
11
- * ZASADA: nigdy nie dodajemy milisekund. `t + 86_400_000` na 2026-03-29 00:00 CET
12
- * daje 2026-03-30 01:00 (dryf kumulatywny), a na 2026-10-25 daje tę samą datę
13
- * dwa razy. Zamiast tego iterujemy po polach `Date` (`setDate`, `setHours`),
14
- * bo one normalizują wynik do lokalnego zegara ściennego.
15
- *
16
- * Wszystkie różnice dni/tygodni liczymy przez `Math.round`, nigdy `floor`/`ceil`:
17
- * doba DST to 0.958 albo 1.042 doby, a `round` te odchyłki pochłania.
18
- */
19
- const MS_DAY = 86_400_000;
20
- const MS_WEEK = 604_800_000;
21
- const MS_HOUR = 3_600_000;
22
- /** Normalizuje dowolne wejście daty do epoch ms. Zwraca NaN dla wartości nieparsowalnych. */
23
- function toTimestamp(value) {
24
- if (typeof value === 'number')
25
- return value;
26
- if (value instanceof Date)
27
- return value.getTime();
28
- return new Date(value).getTime();
29
- }
30
- /** Kopia obiektu Date — nigdy nie mutujemy argumentów. */
31
- function clone(date) {
32
- return new Date(date);
33
- }
34
- /**
35
- * Początek lokalnej doby.
36
- *
37
- * Guard: w strefach, gdzie przejście DST wypada o północy (np. America/Santiago),
38
- * `setHours(0, …)` potrafi wylądować w dobie poprzedniej — wtedy korygujemy
39
- * o jedną dobę w przód.
40
- */
41
- function startOfDay(date) {
42
- const result = clone(date);
43
- result.setHours(0, 0, 0, 0);
44
- if (result.getDate() !== date.getDate() || result.getMonth() !== date.getMonth()) {
45
- result.setTime(date.getTime());
46
- result.setHours(0, 0, 0, 0);
47
- if (result.getTime() < date.getTime() - MS_DAY)
48
- result.setDate(result.getDate() + 1);
49
- }
50
- return result;
51
- }
52
- /** Dodaje `n` dób kalendarzowych, zachowując godzinę ścienną (DST-safe). */
53
- function addDays(date, n) {
54
- const result = clone(date);
55
- result.setDate(result.getDate() + n);
56
- return result;
57
- }
58
- /** Dodaje `n` godzin ściennych. Godzina nieistniejąca (wiosna) normalizuje się w przód. */
59
- function addHours(date, n) {
60
- const result = clone(date);
61
- result.setHours(result.getHours() + n);
62
- return result;
63
- }
64
- /** Liczba dni w miesiącu (`month` 0-indeksowany). */
65
- function daysInMonth(year, month) {
66
- return new Date(year, month + 1, 0).getDate();
67
- }
68
- /** Dodaje `n` miesięcy z przycięciem dnia (31 stycznia + 1 miesiąc = 28/29 lutego). */
69
- function addMonths(date, n) {
70
- const result = clone(date);
71
- const day = result.getDate();
72
- result.setDate(1);
73
- result.setMonth(result.getMonth() + n);
74
- result.setDate(Math.min(day, daysInMonth(result.getFullYear(), result.getMonth())));
75
- return result;
76
- }
77
- /**
78
- * Różnica w pełnych dobach kalendarzowych.
79
- * `Math.round` jest konieczny: doba DST ma 23 h (0.958) albo 25 h (1.042),
80
- * więc `Math.floor` gubiłby jeden dzień dwa razy w roku.
81
- */
82
- function diffDays(a, b) {
83
- return Math.round((startOfDay(b).getTime() - startOfDay(a).getTime()) / MS_DAY);
84
- }
85
- /** Czy obie daty wypadają w tej samej lokalnej dobie? */
86
- function isSameDay(a, b) {
87
- return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
88
- }
89
- // ──────────────────────────────────────────────────────────────────── ISO 8601
90
- /**
91
- * Dzień tygodnia wg ISO 8601: 1 = poniedziałek … 7 = niedziela.
92
- * `Date#getDay()` zwraca 0 dla niedzieli, stąd przesunięcie.
93
- */
94
- function isoWeekday(date) {
95
- return ((date.getDay() + 6) % 7) + 1;
96
- }
97
- /** Poniedziałek tygodnia ISO zawierającego `date` (początek doby). */
98
- function startOfIsoWeek(date) {
99
- const day = startOfDay(date);
100
- return startOfDay(addDays(day, -(isoWeekday(day) - 1)));
101
- }
102
- /**
103
- * Czwartek tygodnia ISO zawierającego `date`.
104
- *
105
- * Czwartek jest kotwicą ISO: rok czwartka to rok tygodnia, a odległość między
106
- * czwartkami dwóch tygodni to zawsze dokładnie N tygodni.
107
- */
108
- function isoThursday(date) {
109
- const day = startOfDay(date);
110
- return startOfDay(addDays(day, 4 - isoWeekday(day)));
111
- }
112
- /**
113
- * Rok tygodnia ISO 8601 — bywa różny od `getFullYear()` na przełomie roku.
114
- * Przykłady: 2025-12-29 → 2026 (tydzień 1), 2027-01-03 → 2026 (tydzień 53).
115
- */
116
- function isoWeekYear(date) {
117
- return isoThursday(date).getFullYear();
118
- }
119
- /**
120
- * Numer tygodnia ISO 8601 (1..53).
121
- * Tydzień zaczyna się w poniedziałek, a tydzień 1 to ten, który zawiera pierwszy
122
- * czwartek roku (równoważnie: zawiera 4 stycznia).
123
- *
124
- * `Math.round` jest tu krytyczny: przy DST różnica dwóch północy to 7 dni ±1 h
125
- * (6.994 / 7.006), więc `Math.floor` zaniżałby numer tygodnia przez pół roku.
126
- */
127
- function isoWeekNumber(date) {
128
- const thursday = isoThursday(date);
129
- const jan4 = new Date(thursday.getFullYear(), 0, 4);
130
- const firstThursday = isoThursday(jan4);
131
- return 1 + Math.round((thursday.getTime() - firstThursday.getTime()) / MS_WEEK);
132
- }
133
- /** Liczba tygodni ISO w danym roku tygodniowym (52 albo 53). */
134
- function isoWeeksInYear(weekYear) {
135
- // 28 grudnia zawsze leży w ostatnim tygodniu ISO danego roku.
136
- return isoWeekNumber(new Date(weekYear, 11, 28));
137
- }
138
- // ─────────────────────────────────────────────────── zaokrąglanie do jednostki
139
- /** Początek jednostki zoomu zawierającej `date` (w dół). */
140
- function floorToUnit(date, zoom) {
141
- const result = clone(date);
142
- switch (zoom) {
143
- case 'hour':
144
- result.setMinutes(0, 0, 0);
145
- return result;
146
- case 'day':
147
- return startOfDay(result);
148
- case 'week':
149
- return startOfIsoWeek(result);
150
- case 'month':
151
- result.setDate(1);
152
- return startOfDay(result);
153
- case 'quarter':
154
- result.setMonth(Math.floor(result.getMonth() / 3) * 3, 1);
155
- return startOfDay(result);
156
- case 'year':
157
- result.setMonth(0, 1);
158
- return startOfDay(result);
159
- }
160
- }
161
- /** Dodaje `n` jednostek zoomu (DST-safe — po polach daty, nie po ms). */
162
- function addUnits(date, zoom, n) {
163
- switch (zoom) {
164
- case 'hour':
165
- return addHours(date, n);
166
- case 'day':
167
- return addDays(date, n);
168
- case 'week':
169
- return addDays(date, n * 7);
170
- case 'month':
171
- return addMonths(date, n);
172
- case 'quarter':
173
- return addMonths(date, n * 3);
174
- case 'year':
175
- return addMonths(date, n * 12);
176
- }
177
- }
178
- /** Początek następnej jednostki zoomu; bez zmian, gdy `date` już leży na granicy. */
179
- function ceilToUnit(date, zoom) {
180
- const floored = floorToUnit(date, zoom);
181
- if (floored.getTime() === date.getTime())
182
- return floored;
183
- return addUnits(floored, zoom, 1);
184
- }
185
- /**
186
- * Liczba pełnych jednostek zoomu między dwiema granicami (DST-safe).
187
- * Dla 'hour' liczymy wprost po ms — doba DST po prostu ma 23 albo 25 godzin
188
- * i tyle właśnie powinno powstać kolumn.
189
- */
190
- function countUnits(start, end, zoom) {
191
- switch (zoom) {
192
- case 'hour':
193
- return Math.round((end.getTime() - start.getTime()) / MS_HOUR);
194
- case 'day':
195
- return diffDays(start, end);
196
- case 'week':
197
- return Math.round(diffDays(start, end) / 7);
198
- case 'month':
199
- return (end.getFullYear() - start.getFullYear()) * 12 + (end.getMonth() - start.getMonth());
200
- case 'quarter':
201
- return Math.round(((end.getFullYear() - start.getFullYear()) * 12 + (end.getMonth() - start.getMonth())) / 3);
202
- case 'year':
203
- return end.getFullYear() - start.getFullYear();
204
- }
205
- }
206
- /** Numer kwartału (1..4) dla danej daty. */
207
- function quarterOf(date) {
208
- return Math.floor(date.getMonth() / 3) + 1;
209
- }
210
-
211
- /*
212
- * Skalowanie liniowe i pomocniki liczbowe.
213
- *
214
- * `linearScale` i `extent` są celowo zduplikowane z `charts/src/core/scale.ts`
215
- * (ten sam kontrakt): import między subpathami tworzyłby twardą zależność
216
- * `roadmap → charts` dla kilkunastu linii kodu.
217
- */
218
- /** Zakres [min, max] tablicy liczb (0..0 dla pustej). */
219
- function extent(values) {
220
- if (values.length === 0)
221
- return [0, 0];
222
- let min = values[0];
223
- let max = values[0];
224
- for (const v of values) {
225
- if (v < min)
226
- min = v;
227
- if (v > max)
228
- max = v;
229
- }
230
- return [min, max];
231
- }
232
- /** Interpolacja liniowa domeny → zakresu (np. czas → udział 0..1). */
233
- function linearScale(domainMin, domainMax, rangeMin, rangeMax) {
234
- const dSpan = domainMax - domainMin || 1;
235
- const rSpan = rangeMax - rangeMin;
236
- return (value) => rangeMin + ((value - domainMin) / dSpan) * rSpan;
237
- }
238
- /** Ogranicza wartość do przedziału [min, max]. */
239
- function clamp(value, min, max) {
240
- if (value < min)
241
- return min;
242
- if (value > max)
243
- return max;
244
- return value;
245
- }
246
- /**
247
- * Skraca liczbę na potrzeby atrybutu `d` ścieżki SVG.
248
- * Odpowiednik prywatnego `n()` z `charts/src/core/bar.ts` — trzyma ścieżki krótkie.
249
- */
250
- function svgNum(value) {
251
- return Number.isInteger(value) ? String(value) : value.toFixed(3);
252
- }
253
-
254
- /*
255
- * Wyznaczanie zakresu osi czasu z danych.
256
- *
257
- * Krańce zawsze wyrównujemy na zewnątrz do granicy jednostki zoomu, żeby
258
- * pierwsza i ostatnia kolumna nie były ucięte w połowie.
259
- */
260
- /** Margines po obu stronach danych, w jednostkach zoomu. */
261
- const DEFAULT_PAD = {
262
- hour: 2,
263
- day: 2,
264
- week: 1,
265
- month: 1,
266
- quarter: 1,
267
- year: 0,
268
- };
269
- /** Minimalna szerokość osi, w jednostkach zoomu — żeby jeden item nie dał jednej kolumny. */
270
- const DEFAULT_MIN = {
271
- hour: 12,
272
- day: 14,
273
- week: 8,
274
- month: 6,
275
- quarter: 4,
276
- year: 3,
277
- };
278
- /** Zbiera skrajne wartości czasu z itemów, pomijając wpisy z niepoprawną datą. */
279
- function collectBounds(items) {
280
- const bounds = [];
281
- for (const item of items) {
282
- const start = toTimestamp(item.start);
283
- if (!Number.isFinite(start))
284
- continue;
285
- const end = item.end !== undefined ? toTimestamp(item.end) : start;
286
- bounds.push(start, Number.isFinite(end) ? end : start);
287
- }
288
- return bounds;
289
- }
290
- /**
291
- * Zakres osi wyliczony z danych: min(start)…max(end) powiększony o margines
292
- * i wyrównany do granic jednostki zoomu.
293
- * Dla pustej listy zwraca okno wokół „dziś" o szerokości `minUnits`.
294
- */
295
- function autoRange(items, opts) {
296
- const { zoom } = opts;
297
- const pad = opts.padUnits ?? DEFAULT_PAD[zoom];
298
- const minUnits = opts.minUnits ?? DEFAULT_MIN[zoom];
299
- const todayTs = opts.today ?? Date.now();
300
- const bounds = collectBounds(items);
301
- if ((opts.includeToday ?? true) && bounds.length > 0)
302
- bounds.push(todayTs);
303
- const [min, max] = bounds.length > 0 ? extent(bounds) : [todayTs, todayTs];
304
- // Margines dodajemy po polach daty (DST-safe), nie przez arytmetykę na ms.
305
- let start = floorToUnit(addUnits(new Date(min), zoom, -pad), zoom);
306
- let end = ceilToUnit(addUnits(new Date(max), zoom, pad), zoom);
307
- // Dociągnięcie do minimalnej szerokości — symetrycznie, zaczynając od prawej.
308
- let units = countUnits(start, end, zoom);
309
- while (units < minUnits) {
310
- end = addUnits(end, zoom, 1);
311
- units++;
312
- if (units < minUnits) {
313
- start = addUnits(start, zoom, -1);
314
- units++;
315
- }
316
- }
317
- return { start: start.getTime(), end: end.getTime() };
318
- }
319
- /**
320
- * Ostateczny zakres osi. `config.range` ma priorytet, a brakujący kraniec
321
- * uzupełniamy z `autoRange`. Wynik zawsze leży na granicach jednostki zoomu.
322
- */
323
- function resolveRange(config, items) {
324
- const zoom = config.zoom ?? 'week';
325
- const todaySource = config.today ?? config.timeline?.today;
326
- const today = todaySource !== undefined ? toTimestamp(todaySource) : Date.now();
327
- // `KptRoadmapNormalizedItem` niesie oryginał w `source` — sprowadzamy oba kształty
328
- // do postaci akceptowanej przez `autoRange`.
329
- const raw = items.map((item) => ('source' in item ? item.source : item));
330
- const auto = autoRange(raw, { zoom, today });
331
- const startTs = config.range?.start ?? auto.start;
332
- const endTs = config.range?.end ?? auto.end;
333
- const start = floorToUnit(new Date(startTs), zoom);
334
- let end = ceilToUnit(new Date(endTs), zoom);
335
- // Zakres musi obejmować co najmniej jedną kolumnę.
336
- if (end.getTime() <= start.getTime())
337
- end = addUnits(start, zoom, 1);
338
- return { start: start.getTime(), end: end.getTime() };
339
- }
340
-
341
- /*
342
- * Układ wierszy: normalizacja itemów, spłaszczenie grup do listy wierszy
343
- * renderowalnych, przydział pasów przy nakładaniu się zadań i wirtualizacja.
344
- *
345
- * Offset pionowy (`top`) liczymy tutaj, dzięki czemu warstwa widoku pozycjonuje
346
- * wiersze absolutnie, a wirtualizacja sprowadza się do wycięcia podlisty —
347
- * bez elementów dystansowych i z obsługą wierszy o różnej wysokości.
348
- */
349
- const DEFAULT_ITEM_HEIGHT = 44;
350
- const DEFAULT_GROUP_HEIGHT = 38;
351
- const DEFAULT_LANE_HEIGHT = 28;
352
- /**
353
- * Normalizuje itemy: daty na ms, brak `end` oznacza kamień milowy.
354
- * Odrzuca wpisy z nieparsowalną datą oraz duplikaty `id`.
355
- */
356
- function normalizeItems(items) {
357
- const valid = [];
358
- const invalid = [];
359
- const seen = new Set();
360
- for (const item of items) {
361
- if (!item.id) {
362
- invalid.push({ id: String(item.label ?? '?'), reason: 'brak id' });
363
- continue;
364
- }
365
- if (seen.has(item.id)) {
366
- invalid.push({ id: item.id, reason: 'zduplikowane id' });
367
- continue;
368
- }
369
- const start = toTimestamp(item.start);
370
- if (!Number.isFinite(start)) {
371
- invalid.push({ id: item.id, reason: 'niepoprawna data początkowa' });
372
- continue;
373
- }
374
- const rawEnd = item.end !== undefined ? toTimestamp(item.end) : start;
375
- if (!Number.isFinite(rawEnd)) {
376
- invalid.push({ id: item.id, reason: 'niepoprawna data końcowa' });
377
- continue;
378
- }
379
- // Odwrócony zakres prostujemy zamiast odrzucać — dane bywają brudne.
380
- const from = Math.min(start, rawEnd);
381
- const to = Math.max(start, rawEnd);
382
- const isMilestone = item.milestone === true || item.end === undefined || from === to;
383
- seen.add(item.id);
384
- valid.push({
385
- source: item,
386
- id: item.id,
387
- start: from,
388
- end: isMilestone ? from : to,
389
- isMilestone,
390
- durationMs: isMilestone ? 0 : to - from,
391
- });
392
- }
393
- return { valid, invalid };
394
- }
395
- /** Zakres zbiorczy listy itemów: min(start)…max(end). */
396
- function spanOf(items) {
397
- if (items.length === 0)
398
- return null;
399
- let start = items[0].start;
400
- let end = items[0].end;
401
- for (const item of items) {
402
- if (item.start < start)
403
- start = item.start;
404
- if (item.end > end)
405
- end = item.end;
406
- }
407
- return { start, end };
408
- }
409
- /**
410
- * Postęp grupy ważony czasem trwania.
411
- * Kamienie milowe mają zerową długość, więc liczymy je z wagą 1.
412
- */
413
- function rollupProgress(items) {
414
- let weightSum = 0;
415
- let progressSum = 0;
416
- let any = false;
417
- for (const item of items) {
418
- const progress = item.source.progress;
419
- if (progress === undefined)
420
- continue;
421
- any = true;
422
- const weight = item.isMilestone ? 1 : Math.max(1, item.durationMs);
423
- weightSum += weight;
424
- progressSum += weight * Math.min(1, Math.max(0, progress));
425
- }
426
- return any && weightSum > 0 ? progressSum / weightSum : null;
427
- }
428
- /**
429
- * Zachłanne przydzielanie pasów (interval partitioning).
430
- * Itemy sortujemy po czasie startu i wkładamy do pierwszego pasa, w którym
431
- * poprzednie zadanie zdążyło się skończyć. Zwraca liczbę użytych pasów.
432
- */
433
- function packLanes(items, minGapMs = 0) {
434
- const sorted = [...items].sort((a, b) => a.start - b.start || a.end - b.end);
435
- /** Ostatni zajęty koniec dla każdego pasa. */
436
- const laneEnd = [];
437
- for (const item of sorted) {
438
- const to = Math.max(item.end, item.start);
439
- let lane = laneEnd.findIndex((end) => end + minGapMs <= item.start);
440
- if (lane === -1) {
441
- lane = laneEnd.length;
442
- laneEnd.push(to);
443
- }
444
- else {
445
- laneEnd[lane] = to;
446
- }
447
- item.lane = lane;
448
- }
449
- return Math.max(1, laneEnd.length);
450
- }
451
- /** Głębokość grupy w drzewie `parentId` (z zabezpieczeniem przed cyklem). */
452
- function groupDepth(group, byId) {
453
- let depth = 0;
454
- let current = group.parentId;
455
- const guard = new Set([group.id]);
456
- while (current && !guard.has(current)) {
457
- guard.add(current);
458
- depth++;
459
- current = byId.get(current)?.parentId;
460
- }
461
- return depth;
462
- }
463
- /** Czy któryś z przodków grupy jest zwinięty? */
464
- function hasCollapsedAncestor(group, byId, collapsed) {
465
- let current = group.parentId;
466
- const guard = new Set([group.id]);
467
- while (current && !guard.has(current)) {
468
- if (collapsed.has(current))
469
- return true;
470
- guard.add(current);
471
- current = byId.get(current)?.parentId;
472
- }
473
- return false;
474
- }
475
- /**
476
- * Spłaszcza grupy i itemy do listy wierszy renderowalnych.
477
- *
478
- * Kolejność: grupy w kolejności podanej przez użytkownika, wewnątrz itemy
479
- * posortowane po czasie startu, na końcu itemy bez `groupId`. Zwinięta grupa
480
- * nie emituje wierszy potomnych, ale jej zakres zbiorczy nadal obejmuje dzieci.
481
- *
482
- * @param collapsed zbiór `id` grup zwiniętych (stan trzymany w sygnale komponentu)
483
- */
484
- function buildRows(items, groups, collapsed, opts) {
485
- const mode = opts?.mode ?? 'row-per-item';
486
- const itemHeight = opts?.itemHeight ?? DEFAULT_ITEM_HEIGHT;
487
- const groupHeight = opts?.groupHeight ?? DEFAULT_GROUP_HEIGHT;
488
- const laneHeight = opts?.laneHeight ?? DEFAULT_LANE_HEIGHT;
489
- const minGap = opts?.packMinGapMs ?? 0;
490
- const { valid, invalid } = normalizeItems(items);
491
- const byId = new Map(valid.map((item) => [item.id, item]));
492
- const rowOfItem = new Map();
493
- const rows = [];
494
- const groupList = groups ?? [];
495
- const groupById = new Map(groupList.map((group) => [group.id, group]));
496
- // Itemy w kubełkach wg grupy; nieznane `groupId` traktujemy jak brak grupy.
497
- const buckets = new Map();
498
- const ungrouped = [];
499
- for (const item of valid) {
500
- const groupId = item.source.groupId;
501
- if (groupId && groupById.has(groupId)) {
502
- const bucket = buckets.get(groupId);
503
- if (bucket)
504
- bucket.push(item);
505
- else
506
- buckets.set(groupId, [item]);
507
- }
508
- else {
509
- ungrouped.push(item);
510
- }
511
- }
512
- for (const bucket of buckets.values())
513
- bucket.sort((a, b) => a.start - b.start || a.end - b.end);
514
- ungrouped.sort((a, b) => a.start - b.start || a.end - b.end);
515
- let top = 0;
516
- /** Dokłada wiersze itemów — jeden na item albo jeden spakowany. */
517
- const pushItemRows = (bucket, depth, keyPrefix) => {
518
- if (bucket.length === 0)
519
- return;
520
- if (mode === 'pack') {
521
- const rowKey = `${keyPrefix}:pack`;
522
- const placed = bucket.map((item) => ({ ...item, lane: 0, rowKey }));
523
- const laneCount = packLanes(placed, minGap);
524
- const height = Math.max(itemHeight, laneCount * laneHeight + 8);
525
- for (const item of placed)
526
- rowOfItem.set(item.id, rowKey);
527
- rows.push({
528
- key: rowKey,
529
- kind: 'item',
530
- index: rows.length,
531
- depth,
532
- top,
533
- height,
534
- laneCount,
535
- items: placed,
536
- });
537
- top += height;
538
- return;
539
- }
540
- for (const item of bucket) {
541
- const rowKey = `item:${item.id}`;
542
- rowOfItem.set(item.id, rowKey);
543
- rows.push({
544
- key: rowKey,
545
- kind: 'item',
546
- index: rows.length,
547
- depth,
548
- top,
549
- height: itemHeight,
550
- laneCount: 1,
551
- items: [{ ...item, lane: 0, rowKey }],
552
- });
553
- top += itemHeight;
554
- }
555
- };
556
- for (const group of groupList) {
557
- // Grupa pod zwiniętym przodkiem w ogóle się nie renderuje.
558
- if (hasCollapsedAncestor(group, groupById, collapsed))
559
- continue;
560
- const bucket = buckets.get(group.id) ?? [];
561
- const depth = groupDepth(group, groupById);
562
- const isCollapsed = collapsed.has(group.id);
563
- const rowKey = `group:${group.id}`;
564
- rows.push({
565
- key: rowKey,
566
- kind: 'group',
567
- index: rows.length,
568
- depth,
569
- top,
570
- height: groupHeight,
571
- laneCount: 1,
572
- group,
573
- groupState: {
574
- collapsed: isCollapsed,
575
- itemCount: bucket.length,
576
- // Pasek zbiorczy obejmuje dzieci również wtedy, gdy grupa jest zwinięta.
577
- span: group.summary === 'none' ? null : spanOf(bucket),
578
- progress: rollupProgress(bucket),
579
- },
580
- items: [],
581
- });
582
- top += groupHeight;
583
- if (!isCollapsed)
584
- pushItemRows(bucket, depth + 1, `group:${group.id}`);
585
- }
586
- if (ungrouped.length > 0) {
587
- if (opts?.showUngroupedHeader) {
588
- const rowKey = 'group:__ungrouped';
589
- const isCollapsed = collapsed.has('__ungrouped');
590
- rows.push({
591
- key: rowKey,
592
- kind: 'group',
593
- index: rows.length,
594
- depth: 0,
595
- top,
596
- height: groupHeight,
597
- laneCount: 1,
598
- group: { id: '__ungrouped', label: opts.ungroupedLabel ?? '' },
599
- groupState: {
600
- collapsed: isCollapsed,
601
- itemCount: ungrouped.length,
602
- span: spanOf(ungrouped),
603
- progress: rollupProgress(ungrouped),
604
- },
605
- items: [],
606
- });
607
- top += groupHeight;
608
- if (!isCollapsed)
609
- pushItemRows(ungrouped, 1, 'ungrouped');
610
- }
611
- else {
612
- pushItemRows(ungrouped, 0, 'ungrouped');
613
- }
614
- }
615
- return { rows, totalHeight: top, byId, rowOfItem, invalid };
616
- }
617
- /** Indeks pierwszego wiersza sięgającego pozycji `y` (binary search po `top`). */
618
- function firstRowAt(rows, y) {
619
- let lo = 0;
620
- let hi = rows.length - 1;
621
- while (lo < hi) {
622
- const mid = (lo + hi) >> 1;
623
- if (rows[mid].top + rows[mid].height <= y)
624
- lo = mid + 1;
625
- else
626
- hi = mid;
627
- }
628
- return lo;
629
- }
630
- /**
631
- * Wycinek wierszy widocznych w oknie przewijania.
632
- * Szuka binarnie po skumulowanym `top`, więc działa także dla wierszy
633
- * o różnej wysokości (tryb 'pack').
634
- */
635
- function sliceRows(layout, scrollTop, viewportHeight, buffer = 4) {
636
- const all = layout.rows;
637
- if (all.length === 0 || viewportHeight <= 0) {
638
- return { rows: all, startIndex: 0, endIndex: all.length };
639
- }
640
- const first = firstRowAt(all, Math.max(0, scrollTop));
641
- const last = firstRowAt(all, Math.max(0, scrollTop + viewportHeight));
642
- const startIndex = Math.max(0, first - buffer);
643
- const endIndex = Math.min(all.length, last + buffer + 1);
644
- return { rows: all.slice(startIndex, endIndex), startIndex, endIndex };
645
- }
646
-
647
- /*
648
- * Geometria zależności między paskami — warstwa SVG nad torami.
649
- *
650
- * Overlay pracuje w pikselach, nie w procentach: zaokrąglenie narożnika wymaga
651
- * tych samych jednostek na obu osiach, inaczej łuki byłyby zniekształcone.
652
- */
653
- const DEFAULT_STUB = 12;
654
- const DEFAULT_CORNER = 6;
655
- const DEFAULT_ARROW = 6;
656
- /** Stabilny klucz krawędzi — pozwala pominąć duplikaty i służy jako trackBy. */
657
- function linkKey(link) {
658
- return link.id ?? `${link.from}→${link.to}:${link.type ?? 'FS'}`;
659
- }
660
- /**
661
- * Scala zależności z `item.dependsOn` (skrót oznacza typ 'FS' bez lagu)
662
- * z listą `config.links`, odrzuca krawędzie wiszące i wykrywa cykle.
663
- */
664
- function resolveLinks(items, links) {
665
- const known = new Set(items.map((item) => item.id));
666
- const merged = new Map();
667
- const add = (link) => {
668
- if (!known.has(link.from) || !known.has(link.to) || link.from === link.to)
669
- return;
670
- const key = linkKey(link);
671
- if (!merged.has(key))
672
- merged.set(key, link);
673
- };
674
- for (const item of items) {
675
- for (const dependency of item.source.dependsOn ?? []) {
676
- if (typeof dependency === 'string')
677
- add({ from: dependency, to: item.id, type: 'FS' });
678
- else
679
- add(dependency);
680
- }
681
- }
682
- for (const link of links ?? [])
683
- add(link);
684
- return { links: [...merged.values()], cycles: findCycles([...merged.values()]) };
685
- }
686
- /** Wykrywa cykle w grafie zależności (DFS z kolorowaniem wierzchołków). */
687
- function findCycles(links) {
688
- const adjacency = new Map();
689
- for (const link of links) {
690
- const list = adjacency.get(link.from);
691
- if (list)
692
- list.push(link.to);
693
- else
694
- adjacency.set(link.from, [link.to]);
695
- }
696
- const cycles = [];
697
- const state = new Map();
698
- const stack = [];
699
- const visit = (node) => {
700
- const current = state.get(node);
701
- if (current === 'done')
702
- return;
703
- if (current === 'visiting') {
704
- const from = stack.lastIndexOf(node);
705
- if (from !== -1)
706
- cycles.push(stack.slice(from));
707
- return;
708
- }
709
- state.set(node, 'visiting');
710
- stack.push(node);
711
- for (const next of adjacency.get(node) ?? [])
712
- visit(next);
713
- stack.pop();
714
- state.set(node, 'done');
715
- };
716
- for (const node of adjacency.keys())
717
- visit(node);
718
- return cycles;
719
- }
720
- /** Czy zależność jest naruszona? Liczone w ms, nie w pikselach. */
721
- function violates(link, source, target) {
722
- const lag = link.lag ?? 0;
723
- switch (link.type ?? 'FS') {
724
- case 'FS':
725
- return target.startTs < source.endTs + lag;
726
- case 'SS':
727
- return target.startTs < source.startTs + lag;
728
- case 'FF':
729
- return target.endTs < source.endTs + lag;
730
- case 'SF':
731
- return target.endTs < source.startTs + lag;
732
- }
733
- }
734
- /** Usuwa punkty pokrywające się i leżące na jednej prostej z sąsiadami. */
735
- function dedupeCollinear(points) {
736
- const result = [];
737
- for (const point of points) {
738
- const last = result.at(-1);
739
- if (last && Math.abs(last.x - point.x) < 1e-6 && Math.abs(last.y - point.y) < 1e-6)
740
- continue;
741
- result.push(point);
742
- }
743
- for (let i = result.length - 2; i >= 1; i--) {
744
- const previous = result[i - 1];
745
- const current = result[i];
746
- const next = result[i + 1];
747
- const collinearX = Math.abs(previous.x - current.x) < 1e-6 && Math.abs(current.x - next.x) < 1e-6;
748
- const collinearY = Math.abs(previous.y - current.y) < 1e-6 && Math.abs(current.y - next.y) < 1e-6;
749
- if (collinearX || collinearY)
750
- result.splice(i, 1);
751
- }
752
- return result;
753
- }
754
- /**
755
- * Łamana ortogonalna z zaokrąglonymi narożnikami.
756
- * Narożnik rysujemy krzywą kwadratową — dla kąta prostego wygląda jak łuk,
757
- * a ścieżka jest o połowę krótsza. Promień przycinamy do połowy krótszego
758
- * z sąsiednich odcinków, żeby ciasne zakręty nie tworzyły pętli.
759
- */
760
- function roundedPolyline(points, radius) {
761
- const path = dedupeCollinear(points);
762
- if (path.length < 2)
763
- return '';
764
- const parts = [`M ${svgNum(path[0].x)} ${svgNum(path[0].y)}`];
765
- for (let i = 1; i < path.length - 1; i++) {
766
- const previous = path[i - 1];
767
- const current = path[i];
768
- const next = path[i + 1];
769
- const lengthIn = Math.hypot(current.x - previous.x, current.y - previous.y);
770
- const lengthOut = Math.hypot(next.x - current.x, next.y - current.y);
771
- const r = Math.max(0, Math.min(radius, lengthIn / 2, lengthOut / 2));
772
- if (r === 0 || lengthIn === 0 || lengthOut === 0) {
773
- parts.push(`L ${svgNum(current.x)} ${svgNum(current.y)}`);
774
- continue;
775
- }
776
- const enter = {
777
- x: current.x + ((previous.x - current.x) / lengthIn) * r,
778
- y: current.y + ((previous.y - current.y) / lengthIn) * r,
779
- };
780
- const leave = {
781
- x: current.x + ((next.x - current.x) / lengthOut) * r,
782
- y: current.y + ((next.y - current.y) / lengthOut) * r,
783
- };
784
- parts.push(`L ${svgNum(enter.x)} ${svgNum(enter.y)}`, `Q ${svgNum(current.x)} ${svgNum(current.y)} ${svgNum(leave.x)} ${svgNum(leave.y)}`);
785
- }
786
- const last = path.at(-1);
787
- parts.push(`L ${svgNum(last.x)} ${svgNum(last.y)}`);
788
- return parts.join(' ');
789
- }
790
- /** Grot strzałki w punkcie `point`, skierowany zgodnie z `direction`. */
791
- function arrowHead(point, direction, size) {
792
- const sign = direction === 'right' ? 1 : -1;
793
- const baseX = point.x - sign * size;
794
- return [
795
- `M ${svgNum(point.x)} ${svgNum(point.y)}`,
796
- `L ${svgNum(baseX)} ${svgNum(point.y - size / 2)}`,
797
- `L ${svgNum(baseX)} ${svgNum(point.y + size / 2)}`,
798
- 'Z',
799
- ].join(' ');
800
- }
801
- /** Kotwica wyjścia/wejścia zależnie od typu: -1 = lewa krawędź, 1 = prawa. */
802
- function anchors(type) {
803
- switch (type) {
804
- case 'FS':
805
- return { out: 1, in: -1 };
806
- case 'SS':
807
- return { out: -1, in: -1 };
808
- case 'FF':
809
- return { out: 1, in: 1 };
810
- case 'SF':
811
- return { out: -1, in: 1 };
812
- }
813
- }
814
- function edgePoint(rect, side) {
815
- return { x: side === 1 ? rect.x + rect.width : rect.x, y: rect.y + rect.height / 2 };
816
- }
817
- /**
818
- * Trasa łamanej między dwoma kotwicami.
819
- *
820
- * Konwencja kierunków: `outDir` i `inDir` wskazują stronę paska (-1 = lewa
821
- * krawędź, +1 = prawa), więc odsunięcie „na zewnątrz" to zawsze `x + dir * stub`.
822
- *
823
- * Gdy cel leży za końcem źródła, wystarczy prosty schodek: pion wypada między
824
- * paskami. W przeciwnym razie trzeba je obejść poziomym objazdem prowadzonym
825
- * w połowie odstępu między wierszami.
826
- */
827
- function route(out, outDir, anchor, endPoint, inDir, stub, detourGap) {
828
- const afterOut = out.x + outDir * stub;
829
- const beforeIn = endPoint.x + inDir * stub;
830
- if (outDir === inDir) {
831
- // SS / FF — obie kotwice po tej samej stronie pasków. Wspólny pion musi ominąć
832
- // oba paski, więc bierzemy punkt najdalej wysunięty w kierunku wyjścia;
833
- // inaczej trasa schowałaby się pod paskiem, z którego wychodzi.
834
- const middle = outDir === -1 ? Math.min(afterOut, beforeIn) : Math.max(afterOut, beforeIn);
835
- return [out, { x: middle, y: out.y }, { x: middle, y: endPoint.y }, endPoint];
836
- }
837
- // FS / SF — kotwice po przeciwnych stronach. Prosty schodek ma sens tylko wtedy,
838
- // gdy między paskami zostaje miejsce na czytelny pion. Zapas liczymy względem
839
- // rzeczywistej krawędzi celu (`anchor`), a nie skróconego o grot końca trzonu.
840
- // Warunek `room >= 0` jest równoważny „odstęp >= 2 * stub", więc próg zawijasa
841
- // wynika wprost z geometrii i nie wymaga osobnej stałej.
842
- const room = (anchor.x + inDir * stub - afterOut) * (inDir === -1 ? 1 : -1);
843
- if (room >= 0) {
844
- const lo = Math.min(afterOut, beforeIn);
845
- const hi = Math.max(afterOut, beforeIn);
846
- const middle = Math.min(hi, Math.max(lo, (out.x + endPoint.x) / 2));
847
- return [out, { x: middle, y: out.y }, { x: middle, y: endPoint.y }, endPoint];
848
- }
849
- // Paski stykają się albo cel cofa się względem źródła — prowadzimy trasę wokół,
850
- // przez odstęp między wierszami.
851
- const detourY = Math.abs(endPoint.y - out.y) < 1e-6 ? out.y + detourGap : (out.y + endPoint.y) / 2;
852
- return [
853
- out,
854
- { x: afterOut, y: out.y },
855
- { x: afterOut, y: detourY },
856
- { x: beforeIn, y: detourY },
857
- { x: beforeIn, y: endPoint.y },
858
- endPoint,
859
- ];
860
- }
861
- /**
862
- * Geometria pojedynczej zależności.
863
- * Zwraca `null`, gdy któryś koniec nie jest widoczny (zwinięta grupa albo
864
- * wiersz poza oknem wirtualizacji).
865
- */
866
- function linkGeometry(link, source, target, opts) {
867
- if (!source || !target)
868
- return null;
869
- const type = link.type ?? 'FS';
870
- const stub = opts?.stub ?? DEFAULT_STUB;
871
- const corner = opts?.cornerRadius ?? DEFAULT_CORNER;
872
- const arrowSize = opts?.arrowSize ?? DEFAULT_ARROW;
873
- const side = anchors(type);
874
- const from = edgePoint(source.rect, side.out);
875
- const to = edgePoint(target.rect, side.in);
876
- // Grot dosuwamy do krawędzi paska, więc trzon kończy się tuż przed nim —
877
- // czyli o długość grotu na zewnątrz celu.
878
- const tip = to;
879
- const shaftEnd = { x: to.x + side.in * arrowSize, y: to.y };
880
- const detourGap = Math.max(source.rect.height, target.rect.height);
881
- const points = route(from, side.out, to, shaftEnd, side.in, stub, detourGap);
882
- const highlight = opts?.highlightViolations ?? true;
883
- return {
884
- key: linkKey(link),
885
- from: link.from,
886
- to: link.to,
887
- type,
888
- path: roundedPolyline(points, corner),
889
- arrow: arrowHead(tip, side.in === -1 ? 'right' : 'left', arrowSize),
890
- variant: link.variant,
891
- color: link.color,
892
- isViolated: highlight ? violates(link, source, target) : false,
893
- };
894
- }
895
-
896
- /*
897
- * Model osi czasu: kolumny dolnego poziomu nagłówka, pasma górnego poziomu
898
- * oraz skala czas ↔ pozycja.
899
- *
900
- * Skala domyślna ('uniform') odwzorowuje czas odcinkami: każda kolumna ma równą
901
- * szerokość, a wewnątrz niej interpolujemy liniowo po ms. Dzięki temu doba po
902
- * zmianie czasu (23/25 h) i luty nie są węższe od sąsiadów, a skala pozostaje
903
- * ciągła i monotoniczna — zadanie może zaczynać się w środku dnia.
904
- */
905
- /** Domyślna jednostka górnego poziomu nagłówka dla danego zoomu. */
906
- const DEFAULT_BAND = {
907
- hour: 'day',
908
- day: 'week',
909
- week: 'month',
910
- month: 'quarter',
911
- quarter: 'year',
912
- year: 'none',
913
- };
914
- const DEFAULT_MAX_CELLS = 4000;
915
- const DEFAULT_WEEKEND = [0, 6];
916
- function resolveOptions(zoom, opts) {
917
- const workHours = opts?.workHours;
918
- return {
919
- locale: opts?.locale,
920
- todayTs: opts?.today !== undefined ? toTimestamp(opts.today) : Date.now(),
921
- weekendDays: opts?.weekendDays ?? DEFAULT_WEEKEND,
922
- // Okno godzin roboczych ma sens tylko przy kolumnach godzinowych.
923
- workHours: zoom === 'hour' && workHours
924
- ? {
925
- from: workHours.from,
926
- to: workHours.to,
927
- mode: workHours.mode ?? 'skip',
928
- skipWeekends: workHours.skipWeekends ?? true,
929
- }
930
- : null,
931
- bandUnit: opts?.bandUnit ?? DEFAULT_BAND[zoom],
932
- positionMode: opts?.positionMode ?? 'uniform',
933
- weekPrefix: opts?.weekPrefix ?? 'W',
934
- maxCells: opts?.maxCells ?? DEFAULT_MAX_CELLS,
935
- };
936
- }
937
- function makeFormatters(locale) {
938
- return {
939
- dayNum: new Intl.DateTimeFormat(locale, { day: 'numeric' }),
940
- weekdayShort: new Intl.DateTimeFormat(locale, { weekday: 'short' }),
941
- dayMonth: new Intl.DateTimeFormat(locale, { day: '2-digit', month: '2-digit' }),
942
- monthLong: new Intl.DateTimeFormat(locale, { month: 'long' }),
943
- monthShort: new Intl.DateTimeFormat(locale, { month: 'short' }),
944
- dayLong: new Intl.DateTimeFormat(locale, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }),
945
- hour: new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit', hour12: false }),
946
- };
947
- }
948
- function isWeekendDay(date, o) {
949
- return o.weekendDays.includes(date.getDay());
950
- }
951
- /** Czy dana godzina mieści się w oknie roboczym (i poza weekendem, gdy tak ustawiono)? */
952
- function isWorkingSlot(date, o) {
953
- const wh = o.workHours;
954
- if (!wh)
955
- return true;
956
- if (wh.skipWeekends && isWeekendDay(date, o))
957
- return false;
958
- const hour = date.getHours();
959
- return hour >= wh.from && hour < wh.to;
960
- }
961
- /**
962
- * Kotwica przypisania kolumny do pasma.
963
- * Dla tygodnia bierzemy czwartek — tak samo jak ISO wyznacza rok tygodnia,
964
- * więc tydzień na przełomie miesiąca trafia do miesiąca swojego czwartku.
965
- */
966
- function bandAnchor(cellStart, zoom) {
967
- if (zoom !== 'week')
968
- return cellStart;
969
- const anchor = new Date(cellStart);
970
- anchor.setDate(anchor.getDate() + 3);
971
- return anchor;
972
- }
973
- /**
974
- * Wielka pierwsza litera. `Intl` zwraca nazwy zgodne z ortografią języka — po polsku,
975
- * francusku czy czesku miesiące i dni tygodnia pisze się małą literą, po angielsku
976
- * i niemiecku wielką. Na osi czasu i w podpowiedziach chcemy jednolitego wyglądu
977
- * niezależnie od języka, więc podnosimy wyłącznie pierwszy znak; reszta zostaje bez
978
- * zmian, żeby „wtorek, 15 września 2026" nie stało się „Wtorek, 15 Września 2026".
979
- */
980
- function capitalizeFirst(text) {
981
- if (!text)
982
- return text;
983
- const first = text.charAt(0);
984
- const upper = first.toLocaleUpperCase();
985
- // `toLocaleUpperCase` bywa dłuższe niż źródło (np. niemieckie ß → SS) — wtedy
986
- // zostawiamy oryginał, bo zamiana zmieniłaby długość i wygląd etykiety.
987
- return upper.length === first.length ? upper + text.slice(1) : text;
988
- }
989
- function cellLabels(start, zoom, o, f) {
990
- switch (zoom) {
991
- case 'hour':
992
- return {
993
- label: f.hour.format(start),
994
- title: capitalizeFirst(`${f.dayLong.format(start)}, ${f.hour.format(start)}`),
995
- };
996
- case 'day':
997
- return {
998
- label: f.dayNum.format(start),
999
- sublabel: capitalizeFirst(f.weekdayShort.format(start)),
1000
- title: capitalizeFirst(f.dayLong.format(start)),
1001
- };
1002
- case 'week': {
1003
- // Podpis to prawdziwy zakres dat tygodnia: poniedziałek – niedziela.
1004
- const sunday = new Date(start);
1005
- sunday.setDate(sunday.getDate() + 6);
1006
- const range = `${f.dayMonth.format(start)}–${f.dayMonth.format(sunday)}`;
1007
- return {
1008
- label: `${o.weekPrefix}${isoWeekNumber(start)}`,
1009
- sublabel: range,
1010
- title: `${o.weekPrefix}${isoWeekNumber(start)} ${isoWeekYear(start)} · ${range}`,
1011
- };
1012
- }
1013
- case 'month':
1014
- return {
1015
- label: capitalizeFirst(f.monthShort.format(start)),
1016
- title: capitalizeFirst(`${f.monthLong.format(start)} ${start.getFullYear()}`),
1017
- };
1018
- case 'quarter':
1019
- return { label: `Q${quarterOf(start)}`, title: `Q${quarterOf(start)} ${start.getFullYear()}` };
1020
- case 'year':
1021
- return { label: String(start.getFullYear()), title: String(start.getFullYear()) };
1022
- }
1023
- }
1024
- function bandLabels(start, unit, f) {
1025
- switch (unit) {
1026
- case 'hour':
1027
- return { label: f.hour.format(start) };
1028
- case 'day':
1029
- return { label: capitalizeFirst(f.dayLong.format(start)) };
1030
- case 'week':
1031
- return { label: `W${isoWeekNumber(start)}`, sublabel: capitalizeFirst(f.monthLong.format(start)) };
1032
- case 'month':
1033
- return { label: capitalizeFirst(f.monthLong.format(start)), sublabel: String(start.getFullYear()) };
1034
- case 'quarter':
1035
- return { label: `Q${quarterOf(start)}`, sublabel: String(start.getFullYear()) };
1036
- case 'year':
1037
- return { label: String(start.getFullYear()) };
1038
- }
1039
- }
1040
- /**
1041
- * Rok kolumny użyty do wykrycia granicy roku.
1042
- * Przy zoomie tygodniowym musi to być rok tygodnia ISO — inaczej W53/2026
1043
- * (28.12.2026) dostałaby błędnie znacznik „2027".
1044
- */
1045
- function yearOfCell(date, zoom) {
1046
- return zoom === 'week' ? isoWeekYear(date) : date.getFullYear();
1047
- }
1048
- /**
1049
- * Początek następnej kolumny albo `null`, gdy kursor nie potrafi się już posunąć.
1050
- * DST wiosną przy zoomie godzinowym: 02:00 nie istnieje, więc `setHours`
1051
- * normalizuje na 03:00 i zwykły krok mógłby nie wyprzedzić `cursor` — wtedy
1052
- * przeskakujemy o dwie godziny.
1053
- */
1054
- function nextBoundary(cursor, zoom) {
1055
- const next = addUnits(cursor, zoom, 1);
1056
- if (next.getTime() > cursor.getTime())
1057
- return next;
1058
- const jumped = addHours(cursor, 2);
1059
- return jumped.getTime() > cursor.getTime() ? jumped : null;
1060
- }
1061
- /**
1062
- * Czy kolumna obejmuje „dziś"? Dla jednostek grubszych niż tydzień akceptujemy
1063
- * dodatkowo zgodność samej daty — `todayTs` bywa znormalizowane do północy.
1064
- */
1065
- function isTodayCell(cursor, next, zoom, o, today) {
1066
- const withinSpan = o.todayTs >= cursor.getTime() && o.todayTs < next.getTime();
1067
- if (zoom === 'hour' || zoom === 'day' || zoom === 'week')
1068
- return withinSpan;
1069
- return withinSpan || isSameDay(cursor, today);
1070
- }
1071
- /** Flagi wyglądu kolumny: tło weekendu/godzin niepracujących i granice okresów. */
1072
- function cellDecorations(cursor, zoom, o) {
1073
- const isDayGrid = zoom === 'day' || zoom === 'hour';
1074
- return {
1075
- isWeekend: isDayGrid && isWeekendDay(cursor, o),
1076
- isNonWorking: o.workHours?.mode === 'dim' && !isWorkingSlot(cursor, o),
1077
- isMonthBoundary: isDayGrid && cursor.getDate() === 1,
1078
- isQuarterBoundary: (zoom === 'month' || zoom === 'quarter') && cursor.getMonth() % 3 === 0,
1079
- };
1080
- }
1081
- function buildCells(range, zoom, o, f) {
1082
- const cells = [];
1083
- const today = new Date(o.todayTs);
1084
- // Startujemy od granicy jednostki — inaczej pierwsza kolumna byłaby ucięta.
1085
- let cursor = floorToUnit(new Date(range.start), zoom);
1086
- let pendingGap = false;
1087
- let previousYear = null;
1088
- let guard = 0;
1089
- while (cursor.getTime() < range.end && guard++ < o.maxCells) {
1090
- const next = nextBoundary(cursor, zoom);
1091
- if (next === null)
1092
- break;
1093
- if (o.workHours?.mode === 'skip' && !isWorkingSlot(cursor, o)) {
1094
- pendingGap = true;
1095
- cursor = next;
1096
- continue;
1097
- }
1098
- const year = yearOfCell(cursor, zoom);
1099
- const labels = cellLabels(cursor, zoom, o, f);
1100
- cells.push({
1101
- index: cells.length,
1102
- key: String(cursor.getTime()),
1103
- start: cursor.getTime(),
1104
- end: next.getTime(),
1105
- label: labels.label,
1106
- sublabel: labels.sublabel,
1107
- title: labels.title,
1108
- ratioStart: 0,
1109
- ratioEnd: 0,
1110
- isToday: isTodayCell(cursor, next, zoom, o, today),
1111
- ...cellDecorations(cursor, zoom, o),
1112
- isYearBoundary: previousYear !== null && year !== previousYear,
1113
- hasGapBefore: pendingGap,
1114
- });
1115
- pendingGap = false;
1116
- previousYear = year;
1117
- cursor = next;
1118
- }
1119
- return cells;
1120
- }
1121
- function applyRatios(cells, mode, range) {
1122
- const count = cells.length;
1123
- if (count === 0)
1124
- return;
1125
- if (mode === 'uniform') {
1126
- const step = 1 / count;
1127
- for (const cell of cells) {
1128
- cell.ratioStart = cell.index * step;
1129
- cell.ratioEnd = (cell.index + 1) * step;
1130
- }
1131
- return;
1132
- }
1133
- const scale = linearScale(range.start, range.end, 0, 1);
1134
- for (const cell of cells) {
1135
- cell.ratioStart = clamp(scale(cell.start), 0, 1);
1136
- cell.ratioEnd = clamp(scale(cell.end), 0, 1);
1137
- }
1138
- }
1139
- /** Indeks kolumny zawierającej `ts`, albo pierwszej kolumny po `ts`. */
1140
- function findCell(cells, ts) {
1141
- let lo = 0;
1142
- let hi = cells.length - 1;
1143
- while (lo < hi) {
1144
- const mid = (lo + hi) >> 1;
1145
- if (ts >= cells[mid].end)
1146
- lo = mid + 1;
1147
- else
1148
- hi = mid;
1149
- }
1150
- return lo;
1151
- }
1152
- function buildBands(cells, zoom, o, f) {
1153
- const unit = o.bandUnit;
1154
- if (unit === 'none' || cells.length === 0)
1155
- return [];
1156
- const bands = [];
1157
- let seenYear = null;
1158
- for (const cell of cells) {
1159
- const anchor = bandAnchor(new Date(cell.start), zoom);
1160
- const bandStart = floorToUnit(anchor, unit);
1161
- const key = String(bandStart.getTime());
1162
- const last = bands.at(-1);
1163
- if (last?.key === key) {
1164
- last.toIndex = cell.index;
1165
- last.span += 1;
1166
- last.ratioEnd = cell.ratioEnd;
1167
- last.end = cell.end;
1168
- continue;
1169
- }
1170
- const labels = bandLabels(bandStart, unit, f);
1171
- const year = unit === 'week' ? isoWeekYear(bandStart) : bandStart.getFullYear();
1172
- const isYearStart = seenYear === null ? bands.length === 0 : year !== seenYear;
1173
- seenYear = year;
1174
- bands.push({
1175
- key,
1176
- start: cell.start,
1177
- end: cell.end,
1178
- fromIndex: cell.index,
1179
- toIndex: cell.index,
1180
- span: 1,
1181
- ratioStart: cell.ratioStart,
1182
- ratioEnd: cell.ratioEnd,
1183
- label: labels.label,
1184
- sublabel: labels.sublabel,
1185
- isYearStart,
1186
- // Znacznik roku niesie tylko pierwsze pasmo danego roku.
1187
- yearBadge: isYearStart ? String(year) : undefined,
1188
- });
1189
- }
1190
- return bands;
1191
- }
1192
- /**
1193
- * Buduje model osi czasu: kolumny, pasma nagłówka i skalę.
1194
- * Wszystkie obliczenia w czasie LOKALNYM przeglądarki.
1195
- *
1196
- * @param range zakres osi w ms — zwykle wynik `resolveRange`
1197
- * @param zoom poziom przybliżenia
1198
- * @param opts locale, „dziś", weekendy, godziny robocze, tryb pozycjonowania
1199
- */
1200
- function buildTimeline(range, zoom, opts) {
1201
- const o = resolveOptions(zoom, opts);
1202
- const f = makeFormatters(o.locale);
1203
- const cells = buildCells(range, zoom, o, f);
1204
- applyRatios(cells, o.positionMode, range);
1205
- const bands = buildBands(cells, zoom, o, f);
1206
- const count = cells.length;
1207
- const first = cells[0];
1208
- const last = cells[count - 1];
1209
- /** Czas → 0..1. Skala odcinkowa: indeks kolumny + interpolacja w jej wnętrzu. */
1210
- const toRatio = (ts) => {
1211
- if (count === 0)
1212
- return 0;
1213
- if (o.positionMode === 'linear') {
1214
- return clamp(linearScale(range.start, range.end, 0, 1)(ts), 0, 1);
1215
- }
1216
- if (ts <= first.start)
1217
- return 0;
1218
- if (ts >= last.end)
1219
- return 1;
1220
- const index = findCell(cells, ts);
1221
- const cell = cells[index];
1222
- // `ts` może wpaść w lukę wyciętą przez `workHours: 'skip'` — wtedy
1223
- // interpolacja wychodzi poza [0, 1] i clamp dociąga ją do krawędzi kolumny.
1224
- const inner = clamp(linearScale(cell.start, cell.end, 0, 1)(ts), 0, 1);
1225
- return (index + inner) / count;
1226
- };
1227
- const fromRatio = (ratio) => {
1228
- if (count === 0)
1229
- return range.start;
1230
- if (o.positionMode === 'linear') {
1231
- return range.start + clamp(ratio, 0, 1) * (range.end - range.start);
1232
- }
1233
- const scaled = clamp(ratio, 0, 1) * count;
1234
- const index = Math.min(count - 1, Math.floor(scaled));
1235
- const cell = cells[index];
1236
- // Poprawnie odwzorowuje także dobę 25-godzinną — długość bierzemy z kolumny.
1237
- return cell.start + (scaled - index) * (cell.end - cell.start);
1238
- };
1239
- const span = (startTs, endTs) => {
1240
- const lo = Math.min(startTs, endTs);
1241
- const hi = Math.max(startTs, endTs);
1242
- const axisStart = count > 0 ? first.start : range.start;
1243
- const axisEnd = count > 0 ? last.end : range.end;
1244
- const left = toRatio(lo);
1245
- const right = toRatio(hi);
1246
- return {
1247
- left,
1248
- width: Math.max(0, right - left),
1249
- clippedStart: lo < axisStart,
1250
- clippedEnd: hi > axisEnd,
1251
- // Kamień milowy ma zerową szerokość, więc sam dotyk krawędzi wystarczy.
1252
- visible: hi >= axisStart && lo <= axisEnd,
1253
- };
1254
- };
1255
- const todayInRange = count > 0 && o.todayTs >= first.start && o.todayTs <= last.end;
1256
- return {
1257
- zoom,
1258
- range,
1259
- positionMode: o.positionMode,
1260
- cells,
1261
- bands,
1262
- hasBands: bands.length > 0,
1263
- todayRatio: todayInRange ? toRatio(o.todayTs) : null,
1264
- toRatio,
1265
- fromRatio,
1266
- span,
1267
- };
1268
- }
4
+ import { resolveRange, buildTimeline, buildRows, sliceRows, resolveLinks, linkGeometry } from '@konce-pt/roadmap';
5
+ export { addDays, addHours, addMonths, addUnits, arrowHead, autoRange, buildRows, buildTimeline, ceilToUnit, countUnits, daysInMonth, diffDays, floorToUnit, isSameDay, isoWeekNumber, isoWeekYear, isoWeekday, isoWeeksInYear, linkGeometry, normalizeItems, packLanes, resolveLinks, resolveRange, rollupProgress, roundedPolyline, sliceRows, spanOf, startOfDay, startOfIsoWeek, toTimestamp } from '@konce-pt/roadmap';
1269
6
 
1270
7
  const _c0 = ["scrollEl"];
1271
8
  const _c1 = ["laneMeasure"];
@@ -1600,7 +337,7 @@ const LANE_GAP = 4;
1600
337
  *
1601
338
  * Układ opiera się na DOM i `position: sticky` (kolumna etykiet oraz nagłówek),
1602
339
  * paski są pozycjonowane procentowo w torze, a zależności rysuje warstwa SVG
1603
- * rozpięta nad torami. Cała geometria pochodzi z rdzenia w `../core`.
340
+ * rozpięta nad torami. Cała geometria pochodzi z rdzenia w `@konce-pt/roadmap`.
1604
341
  */
1605
342
  class KptRoadmap {
1606
343
  constructor() {
@@ -1904,16 +641,18 @@ class KptRoadmap {
1904
641
  '[class.kpt-roadmap--fixed-height]': '!!scrollHeight()',
1905
642
  }, template: "<div\n #scrollEl\n class=\"kpt-roadmap__scroll\"\n [style.max-block-size]=\"scrollHeight()\"\n (scroll)=\"onScroll($event)\"\n>\n <!-- Nag\u0142\u00F3wek: przykleja si\u0119 do g\u00F3ry, gdy podano sta\u0142\u0105 wysoko\u015B\u0107. -->\n <div class=\"kpt-roadmap__head\">\n <div class=\"kpt-roadmap__head-label\"></div>\n\n <div class=\"kpt-roadmap__head-track\">\n @if (timeline().hasBands) {\n <div class=\"kpt-roadmap__bands\">\n @for (band of timeline().bands; track band.key) {\n <div\n class=\"kpt-roadmap__band\"\n [class.kpt-roadmap__band--year-start]=\"band.isYearStart\"\n [style.inline-size.%]=\"(band.ratioEnd - band.ratioStart) * 100\"\n [kptTooltip]=\"band.sublabel ? band.label + ' ' + band.sublabel : band.label\"\n kptTooltipOnlyWhenTruncated\n >\n @if (band.yearBadge) {\n <span class=\"kpt-roadmap__year-badge\">{{ band.yearBadge }}</span>\n }\n <span class=\"kpt-roadmap__band-label\">{{ band.label }}</span>\n @if (band.sublabel && !band.yearBadge) {\n <span class=\"kpt-roadmap__band-sublabel\">{{ band.sublabel }}</span>\n }\n </div>\n }\n </div>\n }\n\n <!-- Nag\u0142\u00F3wek definiuje siatk\u0119, wi\u0119c to on jest \u017Ar\u00F3d\u0142em pomiaru szeroko\u015Bci toru. -->\n <div #laneMeasure class=\"kpt-roadmap__cells\">\n @for (cell of timeline().cells; track cell.key) {\n <div\n class=\"kpt-roadmap__cell\"\n [class.kpt-roadmap__cell--weekend]=\"cell.isWeekend\"\n [class.kpt-roadmap__cell--today]=\"cell.isToday && showToday()\"\n [class.kpt-roadmap__cell--year-boundary]=\"cell.isYearBoundary\"\n [class.kpt-roadmap__cell--gap-before]=\"cell.hasGapBefore\"\n [style.inline-size.%]=\"(cell.ratioEnd - cell.ratioStart) * 100\"\n [kptTooltip]=\"cell.title\"\n >\n <span class=\"kpt-roadmap__cell-label\">{{ cell.label }}</span>\n @if (cell.sublabel) {\n <span class=\"kpt-roadmap__cell-sublabel\">{{ cell.sublabel }}</span>\n }\n </div>\n }\n </div>\n\n <!-- Znacznik \u201Edzi\u015B\" mieszka w nag\u0142\u00F3wku: warstwa siatki ma w\u0142asny kontekst\n uk\u0142adania, wi\u0119c nic w niej nie mo\u017Ce znale\u017A\u0107 si\u0119 nad nag\u0142\u00F3wkiem. -->\n @if (showToday() && timeline().todayRatio !== null) {\n <span\n class=\"kpt-roadmap__today-dot\"\n [style.inset-inline-start.%]=\"timeline().todayRatio! * 100\"\n [attr.title]=\"i18n.t('roadmap.today')\"\n ></span>\n }\n </div>\n </div>\n\n @if (isEmpty()) {\n <div class=\"kpt-roadmap__empty\">{{ i18n.t('roadmap.empty') }}</div>\n } @else {\n <div class=\"kpt-roadmap__body\" [style.block-size.px]=\"layout().totalHeight\">\n <!-- Siatka t\u0142a: kolumny weekendowe, granice roku i znacznik dzi\u015B. -->\n <div class=\"kpt-roadmap__grid\" aria-hidden=\"true\">\n <div class=\"kpt-roadmap__grid-cells\">\n @for (cell of timeline().cells; track cell.key) {\n <div\n class=\"kpt-roadmap__grid-cell\"\n [class.kpt-roadmap__grid-cell--weekend]=\"cell.isWeekend\"\n [class.kpt-roadmap__grid-cell--non-working]=\"cell.isNonWorking\"\n [class.kpt-roadmap__grid-cell--year-boundary]=\"cell.isYearBoundary\"\n [class.kpt-roadmap__grid-cell--gap-before]=\"cell.hasGapBefore\"\n [style.inline-size.%]=\"(cell.ratioEnd - cell.ratioStart) * 100\"\n ></div>\n }\n </div>\n\n @if (showToday() && timeline().todayRatio !== null) {\n <!-- Sama prowadnica; znacznik siedzi w nag\u0142\u00F3wku, \u017Ceby by\u0142 nad nim widoczny. -->\n <div class=\"kpt-roadmap__today\" [style.inset-inline-start.%]=\"timeline().todayRatio! * 100\"></div>\n }\n </div>\n\n <!-- Zale\u017Cno\u015Bci rysowane nad paskami, pod kolumn\u0105 etykiet. -->\n @if (links().length > 0) {\n <svg\n class=\"kpt-roadmap__links\"\n [attr.viewBox]=\"'0 0 ' + laneWidth() + ' ' + layout().totalHeight\"\n [attr.width]=\"laneWidth()\"\n [attr.height]=\"layout().totalHeight\"\n aria-hidden=\"true\"\n >\n @for (link of links(); track link.key) {\n <g\n class=\"kpt-roadmap__link\"\n [class.kpt-roadmap__link--violated]=\"link.isViolated\"\n [attr.data-variant]=\"link.variant\"\n >\n <path class=\"kpt-roadmap__link-path\" [attr.d]=\"link.path\" [style.stroke]=\"link.color\" />\n <path class=\"kpt-roadmap__link-arrow\" [attr.d]=\"link.arrow\" [style.fill]=\"link.color\" />\n </g>\n }\n </svg>\n }\n\n @for (row of visibleRows(); track row.key) {\n <div\n class=\"kpt-roadmap__row\"\n [class.kpt-roadmap__row--group]=\"row.kind === 'group'\"\n [style.inset-block-start.px]=\"row.top\"\n [style.block-size.px]=\"row.height\"\n >\n <div\n class=\"kpt-roadmap__label\"\n [style.padding-inline-start.rem]=\"0.75 + row.depth * 1\"\n [class.kpt-roadmap__label--group]=\"row.kind === 'group'\"\n [kptTooltip]=\"labelTooltip(row)\"\n kptTooltipOnlyWhenTruncated\n >\n @if (row.group; as group) {\n <button\n type=\"button\"\n class=\"kpt-roadmap__toggle\"\n [attr.aria-expanded]=\"!isCollapsed(group)\"\n [attr.aria-label]=\"isCollapsed(group) ? i18n.t('roadmap.expandGroup') : i18n.t('roadmap.collapseGroup')\"\n (click)=\"toggleGroup(group)\"\n >\n <span class=\"kpt-roadmap__toggle-icon\" [class.kpt-roadmap__toggle-icon--open]=\"!isCollapsed(group)\">\n </span>\n </button>\n <!-- Dymek siedzi na stacku, nie na ca\u0142ej etykiecie: w grupie tekst jest\n o poziom g\u0142\u0119biej, a pomiar obejmuje tylko bezpo\u015Brednie dzieci hosta. -->\n <span\n class=\"kpt-roadmap__label-stack\"\n [kptTooltip]=\"group.sublabel ? group.label + ' \u2014 ' + group.sublabel : group.label\"\n kptTooltipOnlyWhenTruncated\n >\n <span class=\"kpt-roadmap__label-text kpt-roadmap__label-text--group\">{{ group.label }}</span>\n @if (group.sublabel) {\n <span class=\"kpt-roadmap__label-sub\">{{ group.sublabel }}</span>\n }\n </span>\n @if (row.groupState; as state) {\n @if (state.itemCount) {\n <span class=\"kpt-roadmap__label-count\">{{ state.itemCount }}</span>\n }\n }\n } @else if (row.items[0]; as first) {\n <span class=\"kpt-roadmap__label-text\">{{ first.source.label }}</span>\n @if (row.items.length === 1 && first.source.sublabel) {\n <span class=\"kpt-roadmap__label-sub\">{{ first.source.sublabel }}</span>\n }\n }\n </div>\n\n <div class=\"kpt-roadmap__lane\">\n @if (row.kind === 'group') {\n @if (summary(row); as sum) {\n <div\n class=\"kpt-roadmap__summary\"\n [attr.data-variant]=\"sum.variant\"\n [style.inset-inline-start.%]=\"sum.left\"\n [style.inline-size.%]=\"sum.width\"\n [style.background-color]=\"sum.color\"\n ></div>\n }\n } @else {\n @for (bar of bars(row); track bar.item.id) {\n @if (bar.isMilestone) {\n <div\n class=\"kpt-roadmap__milestone\"\n [attr.data-variant]=\"bar.variant\"\n [kptTooltip]=\"bar.title\"\n [attr.aria-label]=\"i18n.t('roadmap.milestone') + ': ' + bar.label\"\n [style.inset-inline-start.%]=\"bar.left\"\n [style.inset-block-start.px]=\"bar.top\"\n [style.inline-size.px]=\"bar.height\"\n [style.block-size.px]=\"bar.height\"\n [style.background-color]=\"bar.color\"\n tabindex=\"0\"\n (click)=\"onItemClick(bar.item)\"\n (keydown)=\"onItemKeydown($event, bar.item)\"\n ></div>\n } @else {\n <div\n class=\"kpt-roadmap__bar\"\n [attr.data-variant]=\"bar.variant\"\n [kptTooltip]=\"bar.title\"\n kptTooltipOnlyWhenTruncated\n [class.kpt-roadmap__bar--clipped-start]=\"bar.clippedStart\"\n [class.kpt-roadmap__bar--clipped-end]=\"bar.clippedEnd\"\n [class.kpt-roadmap__bar--disabled]=\"bar.item.source.disabled\"\n [style.inset-inline-start.%]=\"bar.left\"\n [style.inline-size.%]=\"bar.width\"\n [style.inset-block-start.px]=\"bar.top\"\n [style.block-size.px]=\"bar.height\"\n [style.background-color]=\"bar.color\"\n tabindex=\"0\"\n (click)=\"onItemClick(bar.item)\"\n (keydown)=\"onItemKeydown($event, bar.item)\"\n >\n @if (bar.progress !== null) {\n <span class=\"kpt-roadmap__bar-progress\" [style.inline-size.%]=\"bar.progress * 100\"></span>\n }\n <span class=\"kpt-roadmap__bar-label\">{{ bar.label }}</span>\n </div>\n }\n }\n }\n </div>\n </div>\n }\n </div>\n }\n</div>\n", styles: ["@layer kpt.components{.kpt-roadmap{display:block;--kpt-roadmap-cells: 1;--kpt-roadmap-cell-w: 76px;--kpt-roadmap-label-w: 14rem;--kpt-roadmap-track-w: calc(var(--kpt-roadmap-cell-w) * var(--kpt-roadmap-cells));color:var(--kpt-color-on-surface);font-family:var(--kpt-font-family-sans);background:var(--kpt-color-surface);border:var(--kpt-border-width-thin, 1px) solid var(--kpt-color-border);border-radius:var(--kpt-radius-lg, .75rem);overflow:hidden}.kpt-roadmap__scroll{overflow:auto;position:relative}.kpt-roadmap__head,.kpt-roadmap__row{display:grid;grid-template-columns:var(--kpt-roadmap-label-w) minmax(var(--kpt-roadmap-track-w),1fr);inline-size:max-content;min-inline-size:100%}.kpt-roadmap__head{position:sticky;inset-block-start:0;z-index:4;background:var(--kpt-color-surface-variant, var(--kpt-color-surface));border-block-end:var(--kpt-border-width-thin, 1px) solid var(--kpt-color-border)}.kpt-roadmap__head-label{position:sticky;inset-inline-start:0;z-index:5;background:var(--kpt-color-surface-variant, var(--kpt-color-surface));border-inline-end:var(--kpt-border-width-thin, 1px) solid var(--kpt-color-border)}.kpt-roadmap__head-track{position:relative;display:flex;flex-direction:column;min-inline-size:0}.kpt-roadmap__bands,.kpt-roadmap__cells,.kpt-roadmap__grid-cells{display:flex;inline-size:100%}.kpt-roadmap__band{position:relative;display:flex;align-items:center;justify-content:center;gap:var(--kpt-space-2, .5rem);min-inline-size:0;padding:var(--kpt-space-2, .5rem) var(--kpt-space-3, .75rem);font-size:var(--kpt-font-size-sm, .875rem);font-weight:var(--kpt-font-weight-medium, 500);color:var(--kpt-color-on-surface-muted);white-space:nowrap;overflow:hidden;border-inline-start:var(--kpt-border-width-thin, 1px) solid var(--kpt-color-border)}.kpt-roadmap__band:first-child{border-inline-start:0}.kpt-roadmap__band--year-start{border-inline-start:var(--kpt-border-width-medium, 2px) solid var(--kpt-color-border-strong)}.kpt-roadmap__band-label{overflow:hidden;text-overflow:ellipsis}.kpt-roadmap__band-sublabel{color:var(--kpt-color-text-subtle, var(--kpt-color-on-surface-muted))}.kpt-roadmap__year-badge{display:inline-flex;align-items:center;padding:.125rem var(--kpt-space-2, .5rem);font-size:var(--kpt-font-size-xs, .75rem);font-weight:var(--kpt-font-weight-semibold, 600);color:var(--kpt-color-surface);background:var(--kpt-color-on-surface);border-radius:var(--kpt-radius-full, 9999px)}.kpt-roadmap__cell{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.125rem;min-inline-size:0;padding:var(--kpt-space-2, .5rem) .25rem;text-align:center;white-space:nowrap;border-inline-start:var(--kpt-border-width-thin, 1px) solid var(--kpt-color-border)}.kpt-roadmap__cell:first-child{border-inline-start:0}.kpt-roadmap__cell-label{font-size:var(--kpt-font-size-xs, .75rem);font-weight:var(--kpt-font-weight-semibold, 600);letter-spacing:.02em;text-transform:uppercase;color:var(--kpt-color-on-surface-muted)}.kpt-roadmap__cell-sublabel{font-size:var(--kpt-font-size-xs, .75rem);color:var(--kpt-color-text-subtle, var(--kpt-color-on-surface-muted));overflow:hidden;text-overflow:ellipsis;inline-size:100%}.kpt-roadmap__cell--weekend{background:var(--kpt-color-surface-sunken, var(--kpt-color-muted))}.kpt-roadmap__cell--today .kpt-roadmap__cell-label{color:var(--kpt-color-primary)}.kpt-roadmap__cell--year-boundary,.kpt-roadmap__grid-cell--year-boundary{border-inline-start:var(--kpt-border-width-medium, 2px) solid var(--kpt-color-border-strong)}.kpt-roadmap__cell--gap-before,.kpt-roadmap__grid-cell--gap-before{border-inline-start:var(--kpt-border-width-thin, 1px) dashed var(--kpt-color-border-strong)}.kpt-roadmap__body{position:relative;inline-size:calc(var(--kpt-roadmap-label-w) + var(--kpt-roadmap-track-w));min-inline-size:100%}.kpt-roadmap__grid{position:absolute;inset-block:0;inset-inline-start:var(--kpt-roadmap-label-w);inset-inline-end:0;z-index:0;pointer-events:none}.kpt-roadmap__grid-cells{block-size:100%}.kpt-roadmap__grid-cell{min-inline-size:0;border-inline-start:var(--kpt-border-width-thin, 1px) solid var(--kpt-color-border)}.kpt-roadmap__grid-cell:first-child{border-inline-start:0}.kpt-roadmap__grid-cell--weekend,.kpt-roadmap__grid-cell--non-working{background:var(--kpt-color-surface-sunken, var(--kpt-color-muted))}.kpt-roadmap__today{position:absolute;inset-block:0;inline-size:0;border-inline-start:var(--kpt-border-width-medium, 2px) dashed color-mix(in oklch,var(--kpt-color-primary) 40%,transparent)}.kpt-roadmap__today-dot{position:absolute;inset-block-end:0rem;inline-size:.5rem;block-size:.5rem;transform:translate(-50%);background:color-mix(in oklch,var(--kpt-color-primary) 40%,transparent);border-radius:var(--kpt-radius-full, 9999px);pointer-events:none}.kpt-roadmap__links{position:absolute;inset-block-start:0;inset-inline-start:var(--kpt-roadmap-label-w);z-index:2;pointer-events:none;overflow:visible}.kpt-roadmap__link-path{fill:none;stroke:var(--kpt-color-on-surface-muted);stroke-width:1.75}.kpt-roadmap__link-arrow{fill:var(--kpt-color-on-surface-muted)}.kpt-roadmap__link--violated .kpt-roadmap__link-path{stroke:var(--kpt-color-danger);stroke-dasharray:4 3}.kpt-roadmap__link--violated .kpt-roadmap__link-arrow{fill:var(--kpt-color-danger)}.kpt-roadmap__row{position:absolute;inset-inline:0;border-block-end:var(--kpt-border-width-thin, 1px) solid var(--kpt-color-border)}.kpt-roadmap__row:hover{background:color-mix(in oklch,var(--kpt-color-primary) 10%,transparent)}.kpt-roadmap__row:hover .kpt-roadmap__label{background:color-mix(in oklch,var(--kpt-color-primary) 10%,var(--kpt-color-surface))}.kpt-roadmap__row--group,.kpt-roadmap__row--group .kpt-roadmap__label{background:var(--kpt-color-surface-variant, var(--kpt-color-surface))}.kpt-roadmap__row--group:hover,.kpt-roadmap__row--group:hover .kpt-roadmap__label{background:color-mix(in oklch,var(--kpt-color-primary) 10%,var(--kpt-color-surface-variant, var(--kpt-color-surface)))}.kpt-roadmap__label{position:sticky;inset-inline-start:0;z-index:3;display:flex;flex-direction:column;justify-content:center;gap:.125rem;min-inline-size:0;padding-inline-end:var(--kpt-space-3, .75rem);background:var(--kpt-color-surface);border-inline-end:var(--kpt-border-width-thin, 1px) solid var(--kpt-color-border)}.kpt-roadmap__label--group{justify-content:start;flex-direction:row;align-items:center;gap:var(--kpt-space-2, .5rem)}.kpt-roadmap__label-text{overflow:hidden;font-size:var(--kpt-font-size-sm, .875rem);font-weight:var(--kpt-font-weight-medium, 500);text-overflow:ellipsis;white-space:nowrap}.kpt-roadmap__label-text--group{font-weight:var(--kpt-font-weight-semibold, 600)}.kpt-roadmap__label-sub{overflow:hidden;font-size:var(--kpt-font-size-xs, .75rem);color:var(--kpt-color-on-surface-muted);text-overflow:ellipsis;white-space:nowrap}.kpt-roadmap__label-stack{display:flex;flex-direction:column;justify-content:center;gap:.125rem;min-inline-size:0}.kpt-roadmap__label-count{align-self:center;flex:0 0 auto;padding:0 var(--kpt-space-2, .5rem);font-size:var(--kpt-font-size-xs, .75rem);color:var(--kpt-color-on-surface-muted);background:var(--kpt-color-muted);border-radius:var(--kpt-radius-full, 9999px)}.kpt-roadmap__toggle{display:inline-flex;flex:0 0 auto;align-items:center;justify-content:center;inline-size:1.25rem;block-size:1.25rem;padding:0;color:inherit;cursor:pointer;background:transparent;border:0;border-radius:var(--kpt-radius-sm, .25rem)}.kpt-roadmap__toggle:focus-visible{outline:var(--kpt-border-width-medium, 2px) solid var(--kpt-color-focus-ring);outline-offset:1px}.kpt-roadmap__toggle-icon{inline-size:0;block-size:0;border-block-start:.3rem solid transparent;border-block-end:.3rem solid transparent;border-inline-start:.4rem solid currentcolor;transition:transform var(--kpt-motion-duration-fast, .12s) var(--kpt-motion-easing-standard)}.kpt-roadmap__toggle-icon--open{transform:rotate(90deg)}.kpt-roadmap__lane{position:relative;min-inline-size:0}.kpt-roadmap__bar,.kpt-roadmap__milestone,.kpt-roadmap__summary{--_color: var(--kpt-color-primary)}.kpt-roadmap__bar[data-variant=neutral],.kpt-roadmap__milestone[data-variant=neutral],.kpt-roadmap__summary[data-variant=neutral]{--_color: var(--kpt-color-muted)}.kpt-roadmap__bar[data-variant=success],.kpt-roadmap__milestone[data-variant=success],.kpt-roadmap__summary[data-variant=success]{--_color: var(--kpt-color-success)}.kpt-roadmap__bar[data-variant=danger],.kpt-roadmap__milestone[data-variant=danger],.kpt-roadmap__summary[data-variant=danger]{--_color: var(--kpt-color-danger)}.kpt-roadmap__bar[data-variant=warning],.kpt-roadmap__milestone[data-variant=warning],.kpt-roadmap__summary[data-variant=warning]{--_color: var(--kpt-color-warning)}.kpt-roadmap__bar[data-variant=info],.kpt-roadmap__milestone[data-variant=info],.kpt-roadmap__summary[data-variant=info]{--_color: var(--kpt-color-info)}.kpt-roadmap__bar{position:absolute;z-index:1;display:flex;align-items:center;min-inline-size:.25rem;padding-inline:var(--kpt-space-2, .5rem);overflow:hidden;cursor:pointer;background:var(--_color);border-radius:var(--kpt-radius-full, 9999px);transition:filter var(--kpt-motion-duration-fast, .12s) var(--kpt-motion-easing-standard)}.kpt-roadmap__bar:hover{filter:brightness(1.08)}.kpt-roadmap__bar:focus-visible{outline:var(--kpt-border-width-medium, 2px) solid var(--kpt-color-focus-ring);outline-offset:2px}.kpt-roadmap__bar--disabled{cursor:default;opacity:.55}.kpt-roadmap__bar--clipped-start{border-start-start-radius:0;border-end-start-radius:0;clip-path:polygon(.5rem 0,100% 0,100% 100%,.5rem 100%,0 50%)}.kpt-roadmap__bar--clipped-end{border-start-end-radius:0;border-end-end-radius:0;clip-path:polygon(0 0,calc(100% - .5rem) 0,100% 50%,calc(100% - .5rem) 100%,0 100%)}.kpt-roadmap__bar--clipped-start.kpt-roadmap__bar--clipped-end{clip-path:polygon(.5rem 0,calc(100% - .5rem) 0,100% 50%,calc(100% - .5rem) 100%,.5rem 100%,0 50%)}.kpt-roadmap__bar-progress{position:absolute;inset-block:0;inset-inline-start:0;background:#00000038;pointer-events:none}.kpt-roadmap__bar-label{position:relative;overflow:hidden;font-size:var(--kpt-font-size-xs, .75rem);font-weight:var(--kpt-font-weight-medium, 500);color:var(--kpt-color-text-inverse, #fff);text-overflow:ellipsis;white-space:nowrap}.kpt-roadmap__bar[data-variant=neutral] .kpt-roadmap__bar-label{color:var(--kpt-color-on-muted)}.kpt-roadmap__milestone{position:absolute;z-index:1;cursor:pointer;background:var(--_color);transform:translate(-50%) rotate(45deg);border-radius:var(--kpt-radius-sm, .25rem)}.kpt-roadmap__milestone:focus-visible{outline:var(--kpt-border-width-medium, 2px) solid var(--kpt-color-focus-ring);outline-offset:2px}.kpt-roadmap__summary{position:absolute;inset-block-start:50%;z-index:1;block-size:.5rem;background:var(--_color);border-radius:var(--kpt-radius-full, 9999px);transform:translateY(-50%)}.kpt-roadmap__summary[data-variant=neutral]{--_color: var(--kpt-color-on-surface-muted)}.kpt-roadmap__empty{padding:var(--kpt-space-8, 2rem);color:var(--kpt-color-on-surface-muted);text-align:center}}\n"] }]
1906
643
  }], () => [], { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }], scrollHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollHeight", required: false }] }], labelWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelWidth", required: false }] }], cellWidthPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellWidthPx", required: false }] }], showToday: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToday", required: false }] }], virtual: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtual", required: false }] }], collapsedGroups: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsedGroups", required: false }] }, { type: i0.Output, args: ["collapsedGroupsChange"] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], rangeChange: [{ type: i0.Output, args: ["rangeChange"] }], scrollEl: [{ type: i0.ViewChild, args: ['scrollEl', { isSignal: true }] }], laneEl: [{ type: i0.ViewChild, args: ['laneMeasure', { isSignal: true }] }] }); })();
1907
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(KptRoadmap, { className: "KptRoadmap", filePath: "lib/roadmap.component.ts", lineNumber: 94 }); })();
644
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(KptRoadmap, { className: "KptRoadmap", filePath: "lib/roadmap.component.ts", lineNumber: 101 }); })();
1908
645
 
1909
646
  /*
1910
647
  * Public API — @konce-pt/angular/roadmap
1911
- * Roadmapa / oś czasu typu Gantt (sygnały, zoneless). Rdzeń obliczeń: pure TS.
648
+ * Roadmapa / oś czasu typu Gantt (sygnały, zoneless). Rdzeń obliczeń mieszka w `@konce-pt/roadmap` —
649
+ * paczce bez frameworka, wspólnej z portem Reacta; tu tylko re-eksport, żeby publiczne API
650
+ * subpathu się nie zmieniło.
1912
651
  */
1913
652
 
1914
653
  /**
1915
654
  * Generated bundle index. Do not edit.
1916
655
  */
1917
656
 
1918
- export { KptRoadmap, addDays, addHours, addMonths, addUnits, arrowHead, autoRange, buildRows, buildTimeline, ceilToUnit, countUnits, daysInMonth, diffDays, floorToUnit, isSameDay, isoWeekNumber, isoWeekYear, isoWeekday, isoWeeksInYear, linkGeometry, normalizeItems, packLanes, resolveLinks, resolveRange, rollupProgress, roundedPolyline, sliceRows, spanOf, startOfDay, startOfIsoWeek, toTimestamp };
657
+ export { KptRoadmap };
1919
658
  //# sourceMappingURL=konce-pt-angular-roadmap.mjs.map