@flyos/design-system 3.6.0 → 3.8.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.
|
@@ -47,7 +47,7 @@ import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
|
|
|
47
47
|
// tools/publish-library.ps1 at bump time, and asserted by the spec beside this file.
|
|
48
48
|
// Used only for the diagnostic message; the duplicate-instance detection itself is
|
|
49
49
|
// version-agnostic, so a stale literal misnames a fork rather than hiding one.
|
|
50
|
-
const FLY_DS_VERSION = '3.
|
|
50
|
+
const FLY_DS_VERSION = '3.8.0';
|
|
51
51
|
const FLY_DS_REGISTRY_KEY = '__FLY_DS_INSTANCES__';
|
|
52
52
|
/**
|
|
53
53
|
* Records this design-system instance on the shared `scope` and returns the
|
|
@@ -12791,8 +12791,10 @@ function snapDomainStart(date, zoom) {
|
|
|
12791
12791
|
* snapped to a clean period boundary on the left. Falls back to a window around `today`
|
|
12792
12792
|
* (or the real today) when nothing is scheduled, so an empty chart still renders a grid.
|
|
12793
12793
|
*/
|
|
12794
|
-
function computeDomain(rows, zoom, today = new Date()
|
|
12795
|
-
|
|
12794
|
+
function computeDomain(rows, zoom, today = new Date(),
|
|
12795
|
+
// Accepting the caller's already-resolved map (when it has one) saves a second O(N)
|
|
12796
|
+
// `resolveRowDates` pass per recompute — the component now resolves once and passes it in.
|
|
12797
|
+
resolved = resolveRowDates(rows)) {
|
|
12796
12798
|
let min = null;
|
|
12797
12799
|
let max = null;
|
|
12798
12800
|
for (const { start, end } of resolved.values()) {
|
|
@@ -12862,7 +12864,9 @@ function flattenRows(rows, collapsedIds) {
|
|
|
12862
12864
|
if (!kids)
|
|
12863
12865
|
return;
|
|
12864
12866
|
for (const row of kids) {
|
|
12865
|
-
|
|
12867
|
+
// `row.hasChildren === true` asserts expandability before children are LOADED (lazy
|
|
12868
|
+
// trees): a row can be flagged expandable while `childrenByParent` has nothing for it yet.
|
|
12869
|
+
const hasChildren = row.hasChildren === true || (childrenByParent.get(row.id)?.length ?? 0) > 0;
|
|
12866
12870
|
const collapsed = hasChildren && collapsedIds.has(row.id);
|
|
12867
12871
|
out.push({ row, depth, hasChildren, collapsed });
|
|
12868
12872
|
if (hasChildren && !collapsed)
|
|
@@ -13032,16 +13036,18 @@ const MONTH_ABBR = [
|
|
|
13032
13036
|
function tickWidth(from, to, pxPerDay) {
|
|
13033
13037
|
return daysBetween(from, to) * pxPerDay;
|
|
13034
13038
|
}
|
|
13039
|
+
function outsideWindow(x, width, w) {
|
|
13040
|
+
return !!w && (x + width < w.x0 || x > w.x1);
|
|
13041
|
+
}
|
|
13035
13042
|
/** Lower (fine) header band: one cell per day/week/month/quarter depending on zoom. */
|
|
13036
|
-
function lowerTicks(domain, zoom, pxPerDay) {
|
|
13043
|
+
function lowerTicks(domain, zoom, pxPerDay, xWindow) {
|
|
13037
13044
|
const ticks = [];
|
|
13038
13045
|
const push = (from, to, label) => {
|
|
13039
|
-
|
|
13040
|
-
|
|
13041
|
-
|
|
13042
|
-
|
|
13043
|
-
|
|
13044
|
-
});
|
|
13046
|
+
const x = dateToX(from, domain.start, pxPerDay);
|
|
13047
|
+
const width = tickWidth(from, to, pxPerDay);
|
|
13048
|
+
if (outsideWindow(x, width, xWindow))
|
|
13049
|
+
return;
|
|
13050
|
+
ticks.push({ x, width, label, key: toIsoDate(from) });
|
|
13045
13051
|
};
|
|
13046
13052
|
if (zoom === 'day') {
|
|
13047
13053
|
for (let d = startOfUtcDay(domain.start); d < domain.end; d = addDays(d, 1)) {
|
|
@@ -13070,11 +13076,14 @@ function lowerTicks(domain, zoom, pxPerDay) {
|
|
|
13070
13076
|
return ticks;
|
|
13071
13077
|
}
|
|
13072
13078
|
/** Upper (coarse) header band: month for day/week, year for month/quarter. */
|
|
13073
|
-
function upperTicks(domain, zoom, pxPerDay) {
|
|
13079
|
+
function upperTicks(domain, zoom, pxPerDay, xWindow) {
|
|
13074
13080
|
const ticks = [];
|
|
13075
13081
|
const push = (from, to, label, key) => {
|
|
13076
13082
|
const x = dateToX(from, domain.start, pxPerDay);
|
|
13077
|
-
|
|
13083
|
+
const width = tickWidth(from, to, pxPerDay);
|
|
13084
|
+
if (outsideWindow(x, width, xWindow))
|
|
13085
|
+
return;
|
|
13086
|
+
ticks.push({ x, width, label, key });
|
|
13078
13087
|
};
|
|
13079
13088
|
if (zoom === 'day' || zoom === 'week') {
|
|
13080
13089
|
for (let m = startOfUtcMonth(domain.start); m < domain.end;) {
|
|
@@ -13093,24 +13102,270 @@ function upperTicks(domain, zoom, pxPerDay) {
|
|
|
13093
13102
|
return ticks;
|
|
13094
13103
|
}
|
|
13095
13104
|
/** Weekend (Sat/Sun) shading bands in logical space; empty when the zoom hides weekends. */
|
|
13096
|
-
function weekendBands(domain, zoom, pxPerDay) {
|
|
13105
|
+
function weekendBands(domain, zoom, pxPerDay, xWindow) {
|
|
13097
13106
|
if (!zoomShowsWeekends(zoom))
|
|
13098
13107
|
return [];
|
|
13099
13108
|
const bands = [];
|
|
13100
13109
|
for (let d = startOfUtcDay(domain.start); d < domain.end; d = addDays(d, 1)) {
|
|
13101
13110
|
const dow = d.getUTCDay();
|
|
13102
|
-
if (dow
|
|
13103
|
-
|
|
13104
|
-
|
|
13105
|
-
|
|
13106
|
-
|
|
13107
|
-
|
|
13108
|
-
});
|
|
13109
|
-
}
|
|
13111
|
+
if (dow !== 0 && dow !== 6)
|
|
13112
|
+
continue;
|
|
13113
|
+
const x = dateToX(d, domain.start, pxPerDay);
|
|
13114
|
+
if (outsideWindow(x, pxPerDay, xWindow))
|
|
13115
|
+
continue;
|
|
13116
|
+
bands.push({ x, width: pxPerDay, label: '', key: `we-${toIsoDate(d)}` });
|
|
13110
13117
|
}
|
|
13111
13118
|
return bands;
|
|
13112
13119
|
}
|
|
13113
13120
|
|
|
13121
|
+
/**
|
|
13122
|
+
* The vertical window: which rows to actually mount, given a scroll position. Pure arithmetic —
|
|
13123
|
+
* `rowHeight` is uniform, so no measurement or `ResizeObserver` autosize strategy is needed.
|
|
13124
|
+
*
|
|
13125
|
+
* `pinnedIndex` (nullable) is the a11y safety net (memo §6.2 mitigation 1): if the currently
|
|
13126
|
+
* focused row's index falls outside the natural scroll window, the window is EXTENDED (never
|
|
13127
|
+
* shifted) to include it, so a keyboard user's focus is never unmounted out from under them.
|
|
13128
|
+
*/
|
|
13129
|
+
function computeRowWindow(scrollTop, viewportH, rowHeight, totalRows, overscan, pinnedIndex = null) {
|
|
13130
|
+
if (totalRows <= 0 || rowHeight <= 0) {
|
|
13131
|
+
return { firstIndex: 0, lastIndex: -1, topPx: 0, heightPx: 0 };
|
|
13132
|
+
}
|
|
13133
|
+
const count = Math.min(totalRows, Math.ceil(Math.max(0, viewportH) / rowHeight) + 2 * overscan);
|
|
13134
|
+
let first = Math.max(0, Math.min(Math.floor(scrollTop / rowHeight) - overscan, totalRows - count));
|
|
13135
|
+
let last = first + count - 1;
|
|
13136
|
+
if (pinnedIndex !== null && pinnedIndex >= 0 && pinnedIndex < totalRows) {
|
|
13137
|
+
if (pinnedIndex < first)
|
|
13138
|
+
first = pinnedIndex;
|
|
13139
|
+
else if (pinnedIndex > last)
|
|
13140
|
+
last = pinnedIndex;
|
|
13141
|
+
}
|
|
13142
|
+
return { firstIndex: first, lastIndex: last, topPx: first * rowHeight, heightPx: (last - first + 1) * rowHeight };
|
|
13143
|
+
}
|
|
13144
|
+
/**
|
|
13145
|
+
* The horizontal window, derived from geometry rather than `scrollLeft` — whose sign and origin
|
|
13146
|
+
* under RTL vary by browser/engine (memo §4.3). `mapX` is an involution for a fixed `innerWidth`,
|
|
13147
|
+
* so it is its own inverse here; nothing outside this function may read `scrollLeft`.
|
|
13148
|
+
*
|
|
13149
|
+
* NOTE on the sign, verified by construction (the memo's §4.3 pseudocode has it backwards): the
|
|
13150
|
+
* canvas is the huge scrolled CONTENT, the scroller is the fixed-size clipping VIEWPORT — same
|
|
13151
|
+
* relationship `_toBodyPoint`/`GanttLinkGestures.toBodyPoint` already use (`clientX - rect.left`
|
|
13152
|
+
* to go from a screen coordinate to a coordinate local to an element's own origin). Scrolling
|
|
13153
|
+
* forward moves the content left, so `canvasRect.left` becomes MORE negative while the scroller's
|
|
13154
|
+
* own box does not move — the render-local x of the scroller's leading edge is therefore
|
|
13155
|
+
* `scrollerRect.left - canvasRect.left`, not the other way round.
|
|
13156
|
+
*/
|
|
13157
|
+
function logicalScrollWindow(canvasRect, scrollerRect, innerWidth, rtl) {
|
|
13158
|
+
const renderLeft = scrollerRect.left - canvasRect.left;
|
|
13159
|
+
const renderRight = renderLeft + scrollerRect.width;
|
|
13160
|
+
const a = mapX(renderLeft, innerWidth, rtl);
|
|
13161
|
+
const b = mapX(renderRight, innerWidth, rtl);
|
|
13162
|
+
return a <= b ? { x0: a, x1: b } : { x0: b, x1: a };
|
|
13163
|
+
}
|
|
13164
|
+
/**
|
|
13165
|
+
* Coarsen `zoom` (day → week → month → quarter) until `domain span × pxPerDay` fits under
|
|
13166
|
+
* `maxCanvasPx` — browsers mis-rasterize an SVG canvas past roughly 16k px in one layer. The
|
|
13167
|
+
* chart still renders correctly at the coarsened level; `effectiveZoomChange` tells the consumer
|
|
13168
|
+
* so a zoom picker can reflect it.
|
|
13169
|
+
*/
|
|
13170
|
+
function clampScale(domain, zoom, maxCanvasPx) {
|
|
13171
|
+
const days = Math.max(0, daysBetween(domain.start, domain.end));
|
|
13172
|
+
let idx = GANTT_ZOOMS.indexOf(zoom);
|
|
13173
|
+
let pxPerDay = ZOOM_PX_PER_DAY[zoom];
|
|
13174
|
+
let coarsened = false;
|
|
13175
|
+
while (days * pxPerDay > maxCanvasPx && idx < GANTT_ZOOMS.length - 1) {
|
|
13176
|
+
idx += 1;
|
|
13177
|
+
pxPerDay = ZOOM_PX_PER_DAY[GANTT_ZOOMS[idx]];
|
|
13178
|
+
coarsened = true;
|
|
13179
|
+
}
|
|
13180
|
+
return { zoom: GANTT_ZOOMS[idx], pxPerDay, coarsened };
|
|
13181
|
+
}
|
|
13182
|
+
// ── Dependency-link culling under a row window ────────────────────────────────────
|
|
13183
|
+
/**
|
|
13184
|
+
* Whether a link between two render-space y's should be drawn under the current row window.
|
|
13185
|
+
* Replaces the pre-A9 "both endpoints rendered" predicate, which would erase exactly the arrows
|
|
13186
|
+
* crossing the viewport edge — the `viewBox` pan already clips the rest for free.
|
|
13187
|
+
*/
|
|
13188
|
+
function linkCrossesWindow(yFrom, yTo, window) {
|
|
13189
|
+
const lo = Math.min(yFrom, yTo);
|
|
13190
|
+
const hi = Math.max(yFrom, yTo);
|
|
13191
|
+
return hi >= window.topPx && lo <= window.topPx + window.heightPx;
|
|
13192
|
+
}
|
|
13193
|
+
/**
|
|
13194
|
+
* Owns the scroll-derived signals (`scrollTop`, viewport size, the horizontal window) and turns
|
|
13195
|
+
* them into a {@link RowWindow} / {@link GanttXWindow} pair, rAF-coalesced so a fast scroll writes
|
|
13196
|
+
* at most one signal update per frame (memo §4.4: "never write a signal per scroll event").
|
|
13197
|
+
*
|
|
13198
|
+
* Composed rather than inlined for the same reason `GanttLabelPane`/`GanttLinkGestures` are: it is
|
|
13199
|
+
* a self-contained concern (DOM rects in, a window out) that is far more legible — and far more
|
|
13200
|
+
* testable in isolation via its `deps` seam — beside its own scroll listener than folded into the
|
|
13201
|
+
* component's already-large signal graph.
|
|
13202
|
+
*/
|
|
13203
|
+
class GanttViewport {
|
|
13204
|
+
deps;
|
|
13205
|
+
_scrollTop = signal(0, ...(ngDevMode ? [{ debugName: "_scrollTop" }] : /* istanbul ignore next */ []));
|
|
13206
|
+
_viewportH = signal(0, ...(ngDevMode ? [{ debugName: "_viewportH" }] : /* istanbul ignore next */ []));
|
|
13207
|
+
_xWindow = signal(null, ...(ngDevMode ? [{ debugName: "_xWindow" }] : /* istanbul ignore next */ []));
|
|
13208
|
+
_pinnedIndex = signal(null, ...(ngDevMode ? [{ debugName: "_pinnedIndex" }] : /* istanbul ignore next */ []));
|
|
13209
|
+
_rafPending = false;
|
|
13210
|
+
_unlisten = null;
|
|
13211
|
+
rowWindow = computed(() => {
|
|
13212
|
+
const total = this.deps.totalRows();
|
|
13213
|
+
const rh = this.deps.rowHeight();
|
|
13214
|
+
if (!this.deps.virtualized()) {
|
|
13215
|
+
return { firstIndex: 0, lastIndex: total - 1, topPx: 0, heightPx: Math.max(rh, total * rh) };
|
|
13216
|
+
}
|
|
13217
|
+
return computeRowWindow(this._scrollTop(), this._viewportH(), rh, total, this.deps.overscanRows(), this._pinnedIndex());
|
|
13218
|
+
}, ...(ngDevMode ? [{ debugName: "rowWindow" }] : /* istanbul ignore next */ []));
|
|
13219
|
+
/** Logical horizontal window, or `null` when non-virtualized (decorations render unwindowed). */
|
|
13220
|
+
xWindow = computed(() => this.deps.virtualized() ? this._xWindow() : null, ...(ngDevMode ? [{ debugName: "xWindow" }] : /* istanbul ignore next */ []));
|
|
13221
|
+
constructor(deps) {
|
|
13222
|
+
this.deps = deps;
|
|
13223
|
+
}
|
|
13224
|
+
/** Pin a row index into the render set regardless of scroll (the focused-row safety net). */
|
|
13225
|
+
setPinnedIndex(index) {
|
|
13226
|
+
this._pinnedIndex.set(index);
|
|
13227
|
+
}
|
|
13228
|
+
/** Wire the scroll listener once the scroller element exists (called from the host's effect). */
|
|
13229
|
+
attach() {
|
|
13230
|
+
const el = this.deps.scroller();
|
|
13231
|
+
if (!el || this._unlisten)
|
|
13232
|
+
return;
|
|
13233
|
+
this.sync();
|
|
13234
|
+
const onScroll = () => this.requestSync();
|
|
13235
|
+
el.addEventListener('scroll', onScroll, { passive: true });
|
|
13236
|
+
this._unlisten = () => el.removeEventListener('scroll', onScroll);
|
|
13237
|
+
this.deps.destroyRef.onDestroy(() => this._unlisten?.());
|
|
13238
|
+
}
|
|
13239
|
+
/** rAF-coalesced scroll handling: many events per frame collapse into one signal write. */
|
|
13240
|
+
requestSync() {
|
|
13241
|
+
if (this._rafPending)
|
|
13242
|
+
return;
|
|
13243
|
+
this._rafPending = true;
|
|
13244
|
+
requestAnimationFrame(() => {
|
|
13245
|
+
this._rafPending = false;
|
|
13246
|
+
this.sync();
|
|
13247
|
+
});
|
|
13248
|
+
}
|
|
13249
|
+
/** Read the DOM once (the only place `scrollTop`/rects are read) and push into signals. */
|
|
13250
|
+
sync() {
|
|
13251
|
+
const el = this.deps.scroller();
|
|
13252
|
+
if (!el)
|
|
13253
|
+
return;
|
|
13254
|
+
this._scrollTop.set(el.scrollTop);
|
|
13255
|
+
this._viewportH.set(el.clientHeight);
|
|
13256
|
+
const canvas = this.deps.canvas();
|
|
13257
|
+
if (canvas) {
|
|
13258
|
+
this._xWindow.set(logicalScrollWindow(canvas.getBoundingClientRect(), el.getBoundingClientRect(), this.deps.innerWidth(), this.deps.rtl()));
|
|
13259
|
+
}
|
|
13260
|
+
}
|
|
13261
|
+
/** Scroll a row index fully into view (used by `scrollToRow`), then re-sync the window. */
|
|
13262
|
+
scrollIndexIntoView(index) {
|
|
13263
|
+
const el = this.deps.scroller();
|
|
13264
|
+
if (!el)
|
|
13265
|
+
return;
|
|
13266
|
+
const rh = this.deps.rowHeight();
|
|
13267
|
+
const top = index * rh;
|
|
13268
|
+
const bottom = top + rh;
|
|
13269
|
+
if (top < el.scrollTop)
|
|
13270
|
+
el.scrollTop = top;
|
|
13271
|
+
else if (bottom > el.scrollTop + el.clientHeight)
|
|
13272
|
+
el.scrollTop = bottom - el.clientHeight;
|
|
13273
|
+
this.sync();
|
|
13274
|
+
}
|
|
13275
|
+
}
|
|
13276
|
+
|
|
13277
|
+
/**
|
|
13278
|
+
* Pure `treegrid` keymap resolution for {@link FlyGanttComponent} — the A9 companion that turns a
|
|
13279
|
+
* key into an intent (move the selection, toggle a collapse) without touching the DOM or Angular.
|
|
13280
|
+
*
|
|
13281
|
+
* Extending `onGridKeydown` in place would grow `gantt.component.ts` past its LoC-ratchet ceiling
|
|
13282
|
+
* (see the note atop `gantt-window.ts`), and — more importantly — the *rule* here (which key does
|
|
13283
|
+
* what, and when Left means "collapse" vs. "go to my parent") is exactly the kind of thing that
|
|
13284
|
+
* benefits from living beside a plain unit test rather than inside a 40-line `switch` on a
|
|
13285
|
+
* `KeyboardEvent`. The host applies the returned {@link GanttA11yIntent}; this module never mutates
|
|
13286
|
+
* anything.
|
|
13287
|
+
*
|
|
13288
|
+
* Home/End/PageUp/PageDown are not a nicety at 20,000 rows — arrow keys alone are unusable at that
|
|
13289
|
+
* scale (memo §6.2). Cell-level navigation is deliberately out of scope: a row's accessible name
|
|
13290
|
+
* already summarises its timeline cell.
|
|
13291
|
+
*/
|
|
13292
|
+
const NAV_KEYS = new Set(['Home', 'End', 'PageUp', 'PageDown', 'ArrowLeft', 'ArrowRight']);
|
|
13293
|
+
/** Whether {@link resolveTreegridKey} has an opinion about this key at all (lets the host early-out). */
|
|
13294
|
+
function isTreegridNavKey(key) {
|
|
13295
|
+
return NAV_KEYS.has(key);
|
|
13296
|
+
}
|
|
13297
|
+
/**
|
|
13298
|
+
* Resolve one `treegrid` navigation key into an intent. `currentIndex` may be `-1` (nothing
|
|
13299
|
+
* selected yet); every case clamps it into range first, matching the existing ArrowUp/Down
|
|
13300
|
+
* behaviour of landing on row 0 rather than throwing.
|
|
13301
|
+
*
|
|
13302
|
+
* `←`/`→` follow the standard treegrid pattern: on an expandable row, `→` expands / `←` collapses;
|
|
13303
|
+
* on an already-collapsed (or childless) row, `←` instead moves to the row's parent — the nearest
|
|
13304
|
+
* PRECEDING row at a shallower depth, which the flattened depth-first order guarantees is correct
|
|
13305
|
+
* without re-walking `parentId`.
|
|
13306
|
+
*/
|
|
13307
|
+
function resolveTreegridKey(key, currentIndex, flat, pageSize) {
|
|
13308
|
+
const total = flat.length;
|
|
13309
|
+
if (total === 0)
|
|
13310
|
+
return { kind: 'none' };
|
|
13311
|
+
const cur = currentIndex < 0 ? 0 : Math.min(currentIndex, total - 1);
|
|
13312
|
+
const row = flat[cur];
|
|
13313
|
+
switch (key) {
|
|
13314
|
+
case 'Home':
|
|
13315
|
+
return { kind: 'select', index: 0 };
|
|
13316
|
+
case 'End':
|
|
13317
|
+
return { kind: 'select', index: total - 1 };
|
|
13318
|
+
case 'PageDown':
|
|
13319
|
+
return { kind: 'select', index: Math.min(total - 1, cur + Math.max(1, pageSize)) };
|
|
13320
|
+
case 'PageUp':
|
|
13321
|
+
return { kind: 'select', index: Math.max(0, cur - Math.max(1, pageSize)) };
|
|
13322
|
+
case 'ArrowRight':
|
|
13323
|
+
return row.hasChildren && row.collapsed ? { kind: 'toggle', id: row.id } : { kind: 'none' };
|
|
13324
|
+
case 'ArrowLeft': {
|
|
13325
|
+
if (row.hasChildren && !row.collapsed)
|
|
13326
|
+
return { kind: 'toggle', id: row.id };
|
|
13327
|
+
for (let i = cur - 1; i >= 0; i--) {
|
|
13328
|
+
if (flat[i].depth < row.depth)
|
|
13329
|
+
return { kind: 'select', index: i };
|
|
13330
|
+
}
|
|
13331
|
+
return { kind: 'none' };
|
|
13332
|
+
}
|
|
13333
|
+
default:
|
|
13334
|
+
return { kind: 'none' };
|
|
13335
|
+
}
|
|
13336
|
+
}
|
|
13337
|
+
|
|
13338
|
+
/**
|
|
13339
|
+
* Pure geometry for the baseline (plan-of-record) overlay — a NEW, app-agnostic capability, not
|
|
13340
|
+
* part of the A9 memo. `GanttRow.baselineStart`/`baselineEnd` are a second, optional pair of ISO
|
|
13341
|
+
* dates; when both are present and `showBaselines` is on, the chart renders a thin muted underbar
|
|
13342
|
+
* beneath a `bar`/`group` row, or a small hollow diamond at a `milestone` row's baseline date —
|
|
13343
|
+
* the generic "baseline vs current" comparison a consumer's own vocabulary later gives meaning to
|
|
13344
|
+
* (e.g. "planned vs actual"), which is exactly why nothing here is named after that vocabulary.
|
|
13345
|
+
*
|
|
13346
|
+
* Kept in its own pure, dependency-free module — same reasoning as `gantt-scale.ts`'s docstring —
|
|
13347
|
+
* and, in particular, routed through `dateToX`/`mapX`/`projectBar` so it mirrors under RTL for
|
|
13348
|
+
* free rather than ever computing its own x from scratch.
|
|
13349
|
+
*/
|
|
13350
|
+
/**
|
|
13351
|
+
* Resolve a row's baseline geometry. Returns `null` when either date is missing/unparseable, so
|
|
13352
|
+
* the caller can treat "no baseline" and "baseline off" identically (skip rendering).
|
|
13353
|
+
*/
|
|
13354
|
+
function resolveBaselineVm(row, domain, pxPerDay, innerWidth, rtl) {
|
|
13355
|
+
const start = parseIsoDate(row.baselineStart);
|
|
13356
|
+
const end = parseIsoDate(row.baselineEnd);
|
|
13357
|
+
if (!start || !end)
|
|
13358
|
+
return null;
|
|
13359
|
+
if (row.kind === 'milestone') {
|
|
13360
|
+
const logical = dateToX(start, domain.start, pxPerDay);
|
|
13361
|
+
return { shape: 'milestone', x: mapX(logical, innerWidth, rtl), width: 0 };
|
|
13362
|
+
}
|
|
13363
|
+
const xs = dateToX(start, domain.start, pxPerDay);
|
|
13364
|
+
const xe = dateToX(addDays(end, 1), domain.start, pxPerDay); // inclusive of the end day, like the main bar
|
|
13365
|
+
const { x, width } = projectBar(xs, xe, innerWidth, rtl);
|
|
13366
|
+
return { shape: 'bar', x, width };
|
|
13367
|
+
}
|
|
13368
|
+
|
|
13114
13369
|
/** Label-pane width bounds (px) the splitter clamps to. */
|
|
13115
13370
|
const GANTT_LABEL_W_MIN = 140;
|
|
13116
13371
|
const GANTT_LABEL_W_MAX = 720;
|
|
@@ -13332,13 +13587,14 @@ class GanttLinkGestures {
|
|
|
13332
13587
|
document.removeEventListener('pointerup', onUp);
|
|
13333
13588
|
});
|
|
13334
13589
|
}
|
|
13335
|
-
/** Convert client coordinates to a point inside the body SVG (render space
|
|
13590
|
+
/** Convert client coordinates to a point inside the body SVG (render space, i.e. absolute
|
|
13591
|
+
* row-space y — the `windowTopPx` term undoes the `viewBox` pan the rect itself doesn't reflect). */
|
|
13336
13592
|
toBodyPoint(clientX, clientY) {
|
|
13337
13593
|
const svg = this.deps.bodySvg();
|
|
13338
13594
|
if (!svg)
|
|
13339
13595
|
return { x: 0, y: 0 };
|
|
13340
13596
|
const rect = svg.getBoundingClientRect();
|
|
13341
|
-
return { x: clientX - rect.left, y: clientY - rect.top };
|
|
13597
|
+
return { x: clientX - rect.left, y: clientY - rect.top + (this.deps.windowTopPx?.() ?? 0) };
|
|
13342
13598
|
}
|
|
13343
13599
|
/**
|
|
13344
13600
|
* The row a link gesture was dropped on **and which of its ends** the drop landed nearest —
|
|
@@ -13372,6 +13628,8 @@ const HEADER_H = HEADER_UPPER_H + HEADER_LOWER_H;
|
|
|
13372
13628
|
const BAR_V_PAD = 6;
|
|
13373
13629
|
/** Edge-handle hit width (px) for resize + the link connector radius. */
|
|
13374
13630
|
const HANDLE_W = 8;
|
|
13631
|
+
/** Thickness of the baseline underbar (px) — thin enough to read as a comparison, not a duplicate. */
|
|
13632
|
+
const BASELINE_BAR_H = Math.max(2, BAR_V_PAD - 2);
|
|
13375
13633
|
/**
|
|
13376
13634
|
* **`fly-gantt`** — the design-system SVG Gantt chart.
|
|
13377
13635
|
*
|
|
@@ -13395,9 +13653,12 @@ const HANDLE_W = 8;
|
|
|
13395
13653
|
* placed among those controls matches them; the shell-chrome family (`--surface-card`, …) and
|
|
13396
13654
|
* then a light-neutral literal are chained as fallbacks for consumers that map neither.
|
|
13397
13655
|
*
|
|
13398
|
-
* **Scale
|
|
13399
|
-
*
|
|
13400
|
-
*
|
|
13656
|
+
* **Scale (A9).** Row count is unbounded by default (`maxRows` now defaults to `0`); only the rows
|
|
13657
|
+
* in view (plus `overscanRows`) mount as DOM/SVG, panned via a `viewBox` y-offset on the body
|
|
13658
|
+
* `<svg>` so `./gantt-scale`'s geometry stays oblivious to windowing (`./gantt-window`'s doc).
|
|
13659
|
+
* Decorations window the same way; bars are never culled. `maxCanvasPx` coarsens the zoom
|
|
13660
|
+
* (`effectiveZoomChange`) past that width; `virtualized = false` restores the pre-A9 path; a
|
|
13661
|
+
* positive `maxRows` keeps the old hard cap (with its overflow footer) too.
|
|
13401
13662
|
*/
|
|
13402
13663
|
class FlyGanttComponent {
|
|
13403
13664
|
i18n = inject(I18nService);
|
|
@@ -13420,8 +13681,20 @@ class FlyGanttComponent {
|
|
|
13420
13681
|
resizableLabels = input(true, ...(ngDevMode ? [{ debugName: "resizableLabels" }] : /* istanbul ignore next */ []));
|
|
13421
13682
|
/** Row band height in px. */
|
|
13422
13683
|
rowHeight = input(34, ...(ngDevMode ? [{ debugName: "rowHeight" }] : /* istanbul ignore next */ []));
|
|
13423
|
-
/** Hard cap on
|
|
13424
|
-
|
|
13684
|
+
/** Hard cap on the flattened row list; beyond it the list is truncated (see class doc).
|
|
13685
|
+
* `0` (the default) is uncapped — windowing (below) is what keeps an uncapped chart cheap. */
|
|
13686
|
+
maxRows = input(0, ...(ngDevMode ? [{ debugName: "maxRows" }] : /* istanbul ignore next */ []));
|
|
13687
|
+
/** Controlled collapse state. `null` (the default) keeps the pre-A9 internal-state behaviour —
|
|
13688
|
+
* adopt controlled mode by echoing {@link collapsedIdsChange} back through this input. */
|
|
13689
|
+
collapsedIds = input(null, ...(ngDevMode ? [{ debugName: "collapsedIds" }] : /* istanbul ignore next */ []));
|
|
13690
|
+
/** Rows rendered above and below the viewport window (virtualization overscan). */
|
|
13691
|
+
overscanRows = input(4, ...(ngDevMode ? [{ debugName: "overscanRows" }] : /* istanbul ignore next */ []));
|
|
13692
|
+
/** Canvas-width ceiling in px; a zoom that would exceed it is coarsened one step at a time. */
|
|
13693
|
+
maxCanvasPx = input(32_000, ...(ngDevMode ? [{ debugName: "maxCanvasPx" }] : /* istanbul ignore next */ []));
|
|
13694
|
+
/** `false` restores the legacy render-everything path (no vertical/horizontal windowing). */
|
|
13695
|
+
virtualized = input(true, ...(ngDevMode ? [{ debugName: "virtualized" }] : /* istanbul ignore next */ []));
|
|
13696
|
+
/** Render the baseline underbar/diamond for rows carrying `baselineStart`/`baselineEnd`. */
|
|
13697
|
+
showBaselines = input(true, ...(ngDevMode ? [{ debugName: "showBaselines" }] : /* istanbul ignore next */ []));
|
|
13425
13698
|
// ── Outputs ────────────────────────────────────────────────────────────────
|
|
13426
13699
|
/** Fires on a committed bar move / resize with the new ISO `start`/`end`. */
|
|
13427
13700
|
rowDatesChange = output();
|
|
@@ -13436,6 +13709,12 @@ class FlyGanttComponent {
|
|
|
13436
13709
|
rowDblClick = output();
|
|
13437
13710
|
/** Fires once per committed divider drag / keyboard resize with the new pane width in px. */
|
|
13438
13711
|
labelWidthChange = output();
|
|
13712
|
+
/** Emitted whenever collapse state changes, in controlled or uncontrolled mode. */
|
|
13713
|
+
collapsedIdsChange = output();
|
|
13714
|
+
/** A row with `hasChildren: true` but no loaded children was expanded — go fetch them. */
|
|
13715
|
+
rowExpand = output();
|
|
13716
|
+
/** The component coarsened the requested zoom to stay under `maxCanvasPx`. */
|
|
13717
|
+
effectiveZoomChange = output();
|
|
13439
13718
|
// ── Identity + geometry constants (template-visible) ─────────────────────────
|
|
13440
13719
|
_uid = ++_flyGanttUid;
|
|
13441
13720
|
markerId = `fly-gantt-${this._uid}-arrow`;
|
|
@@ -13445,31 +13724,96 @@ class FlyGanttComponent {
|
|
|
13445
13724
|
HANDLE_W = HANDLE_W;
|
|
13446
13725
|
MIN_LABEL_W = GANTT_LABEL_W_MIN;
|
|
13447
13726
|
MAX_LABEL_W = GANTT_LABEL_W_MAX;
|
|
13727
|
+
BASELINE_BAR_H = BASELINE_BAR_H;
|
|
13448
13728
|
// ── Interaction state ─────────────────────────────────────────────────────────
|
|
13449
13729
|
selectedId = signal(null, ...(ngDevMode ? [{ debugName: "selectedId" }] : /* istanbul ignore next */ []));
|
|
13450
|
-
|
|
13730
|
+
/** Uncontrolled collapse state — the source of truth whenever the `collapsedIds` input is `null`. */
|
|
13731
|
+
_uncontrolledCollapsed = signal(new Set(), ...(ngDevMode ? [{ debugName: "_uncontrolledCollapsed" }] : /* istanbul ignore next */ []));
|
|
13451
13732
|
/** Live drag preview `{id,start,end}` folded into geometry while a gesture runs. */
|
|
13452
13733
|
_dragPreview = signal(null, ...(ngDevMode ? [{ debugName: "_dragPreview" }] : /* istanbul ignore next */ []));
|
|
13453
13734
|
dragTooltip = signal(null, ...(ngDevMode ? [{ debugName: "dragTooltip" }] : /* istanbul ignore next */ []));
|
|
13454
|
-
/** True while a divider drag is in flight (suppresses text selection host-wide). */
|
|
13455
|
-
/** User-chosen pane width; `null` = still following the {@link labelWidth} input. */
|
|
13456
13735
|
bodyRef = viewChild('bodySvg', ...(ngDevMode ? [{ debugName: "bodyRef" }] : /* istanbul ignore next */ []));
|
|
13736
|
+
scrollElRef = viewChild('scrollEl', ...(ngDevMode ? [{ debugName: "scrollElRef" }] : /* istanbul ignore next */ []));
|
|
13737
|
+
canvasElRef = viewChild('canvasEl', ...(ngDevMode ? [{ debugName: "canvasElRef" }] : /* istanbul ignore next */ []));
|
|
13738
|
+
labelsElRef = viewChild('labelsEl', ...(ngDevMode ? [{ debugName: "labelsElRef" }] : /* istanbul ignore next */ []));
|
|
13457
13739
|
// ── Derived model ──────────────────────────────────────────────────────────────
|
|
13458
13740
|
rtl = computed(() => this.i18n.isRtl(), ...(ngDevMode ? [{ debugName: "rtl" }] : /* istanbul ignore next */ []));
|
|
13459
|
-
|
|
13460
|
-
|
|
13461
|
-
|
|
13462
|
-
|
|
13463
|
-
|
|
13741
|
+
/** Effective collapse set: the controlled input when the consumer supplied one, else internal state. */
|
|
13742
|
+
_collapsed = computed(() => {
|
|
13743
|
+
const controlled = this.collapsedIds();
|
|
13744
|
+
if (controlled === null)
|
|
13745
|
+
return this._uncontrolledCollapsed();
|
|
13746
|
+
return controlled instanceof Set ? controlled : new Set(controlled);
|
|
13747
|
+
}, ...(ngDevMode ? [{ debugName: "_collapsed" }] : /* istanbul ignore next */ []));
|
|
13748
|
+
/** Full visible list after tree flatten/collapse (no `maxRows` cap applied yet). */
|
|
13464
13749
|
_flat = computed(() => flattenRows(this.rows(), this._collapsed()), ...(ngDevMode ? [{ debugName: "_flat" }] : /* istanbul ignore next */ []));
|
|
13465
|
-
/**
|
|
13466
|
-
visibleRows = computed(() =>
|
|
13467
|
-
|
|
13750
|
+
/** `_flat`, capped at `maxRows` when it is positive — unchanged from the pre-A9 contract. */
|
|
13751
|
+
visibleRows = computed(() => {
|
|
13752
|
+
const flat = this._flat();
|
|
13753
|
+
const cap = this.maxRows();
|
|
13754
|
+
return cap > 0 ? flat.slice(0, cap) : flat;
|
|
13755
|
+
}, ...(ngDevMode ? [{ debugName: "visibleRows" }] : /* istanbul ignore next */ []));
|
|
13756
|
+
overflowCount = computed(() => Math.max(0, this._flat().length - (this.maxRows() || Infinity)), ...(ngDevMode ? [{ debugName: "overflowCount" }] : /* istanbul ignore next */ []));
|
|
13757
|
+
/** TRUE total row count for `aria-rowcount` — independent of `maxRows` (pre-A9 defect §2.4/§6.2). */
|
|
13758
|
+
totalRowCount = computed(() => this._flat().length, ...(ngDevMode ? [{ debugName: "totalRowCount" }] : /* istanbul ignore next */ []));
|
|
13759
|
+
/** Full spacer height — keeps the scrollbar honest; the `<svg>` itself only spans `svgHeight`. */
|
|
13468
13760
|
bodyHeight = computed(() => Math.max(this.rowHeight(), this.visibleRows().length * this.rowHeight()), ...(ngDevMode ? [{ debugName: "bodyHeight" }] : /* istanbul ignore next */ []));
|
|
13469
13761
|
_resolvedDates = computed(() => resolveRowDates(this.rows()), ...(ngDevMode ? [{ debugName: "_resolvedDates" }] : /* istanbul ignore next */ []));
|
|
13470
|
-
|
|
13471
|
-
|
|
13472
|
-
|
|
13762
|
+
domain = computed(() => computeDomain(this.rows(), this.zoom(), new Date(), this._resolvedDates()), ...(ngDevMode ? [{ debugName: "domain" }] : /* istanbul ignore next */ []));
|
|
13763
|
+
/** Zoom coarsened (if needed) to keep `domain span × pxPerDay` under `maxCanvasPx`. */
|
|
13764
|
+
_clampedScale = computed(() => clampScale(this.domain(), this.zoom(), this.maxCanvasPx()), ...(ngDevMode ? [{ debugName: "_clampedScale" }] : /* istanbul ignore next */ []));
|
|
13765
|
+
effectiveZoom = computed(() => this._clampedScale().zoom, ...(ngDevMode ? [{ debugName: "effectiveZoom" }] : /* istanbul ignore next */ []));
|
|
13766
|
+
pxPerDay = computed(() => this._clampedScale().pxPerDay, ...(ngDevMode ? [{ debugName: "pxPerDay" }] : /* istanbul ignore next */ []));
|
|
13767
|
+
innerWidth = computed(() => Math.max(1, dateToX(this.domain().end, this.domain().start, this.pxPerDay())), ...(ngDevMode ? [{ debugName: "innerWidth" }] : /* istanbul ignore next */ []));
|
|
13768
|
+
// ── Virtualization: the row/column windows + the viewport wiring that derives them ───────────
|
|
13769
|
+
_viewport = new GanttViewport({
|
|
13770
|
+
scroller: () => this.scrollElRef()?.nativeElement ?? null,
|
|
13771
|
+
canvas: () => this.canvasElRef()?.nativeElement ?? null,
|
|
13772
|
+
rowHeight: this.rowHeight,
|
|
13773
|
+
totalRows: computed(() => this.visibleRows().length),
|
|
13774
|
+
overscanRows: this.overscanRows,
|
|
13775
|
+
innerWidth: this.innerWidth,
|
|
13776
|
+
rtl: this.rtl,
|
|
13777
|
+
virtualized: this.virtualized,
|
|
13778
|
+
destroyRef: this.destroyRef,
|
|
13779
|
+
});
|
|
13780
|
+
/** {@link visibleRows} indices `[firstIndex, lastIndex]` currently rendered. */
|
|
13781
|
+
rowWindow = this._viewport.rowWindow;
|
|
13782
|
+
/** Logical horizontal window for decorations, or `null` when non-virtualized. */
|
|
13783
|
+
xWindow = this._viewport.xWindow;
|
|
13784
|
+
/** Body `<svg>`'s CSS `top` / `viewBox` y-offset — the windowing trick (`./gantt-window`). */
|
|
13785
|
+
svgTop = computed(() => this.rowWindow().topPx, ...(ngDevMode ? [{ debugName: "svgTop" }] : /* istanbul ignore next */ []));
|
|
13786
|
+
/** Body `<svg>`'s own height (window only, not the full spacer). */
|
|
13787
|
+
svgHeight = computed(() => Math.max(this.rowHeight(), this.rowWindow().heightPx), ...(ngDevMode ? [{ debugName: "svgHeight" }] : /* istanbul ignore next */ []));
|
|
13788
|
+
_lastEmittedZoom = null;
|
|
13789
|
+
constructor() {
|
|
13790
|
+
afterNextRender(() => this._viewport.attach());
|
|
13791
|
+
// Selecting a row pins it into the render window (memo §6.2-1 focus safety net) and, once the
|
|
13792
|
+
// DOM catches up, moves roving-tabindex focus onto it (DOM order matches `rowVms()`'s order).
|
|
13793
|
+
effect(() => {
|
|
13794
|
+
const id = this.selectedId();
|
|
13795
|
+
const idx = id === null ? -1 : this.visibleRows().findIndex((f) => f.row.id === id);
|
|
13796
|
+
this._viewport.setPinnedIndex(idx >= 0 ? idx : null);
|
|
13797
|
+
const root = this.labelsElRef()?.nativeElement;
|
|
13798
|
+
if (id === null || !root)
|
|
13799
|
+
return;
|
|
13800
|
+
queueMicrotask(() => {
|
|
13801
|
+
const domIdx = this.rowVms().findIndex((v) => v.flat.row.id === id);
|
|
13802
|
+
if (domIdx >= 0)
|
|
13803
|
+
root.querySelectorAll('.fly-gantt__label-row')[domIdx]?.focus();
|
|
13804
|
+
});
|
|
13805
|
+
});
|
|
13806
|
+
effect(() => {
|
|
13807
|
+
const scale = this._clampedScale();
|
|
13808
|
+
if (scale.coarsened && scale.zoom !== this._lastEmittedZoom) {
|
|
13809
|
+
this._lastEmittedZoom = scale.zoom;
|
|
13810
|
+
this.effectiveZoomChange.emit(scale.zoom);
|
|
13811
|
+
}
|
|
13812
|
+
});
|
|
13813
|
+
}
|
|
13814
|
+
weekendBands = computed(() => this._mapTicks(weekendBands(this.domain(), this.effectiveZoom(), this.pxPerDay(), this.xWindow() ?? undefined)), ...(ngDevMode ? [{ debugName: "weekendBands" }] : /* istanbul ignore next */ []));
|
|
13815
|
+
lowerTicks = computed(() => this._mapTicks(lowerTicks(this.domain(), this.effectiveZoom(), this.pxPerDay(), this.xWindow() ?? undefined)), ...(ngDevMode ? [{ debugName: "lowerTicks" }] : /* istanbul ignore next */ []));
|
|
13816
|
+
upperTicks = computed(() => this._mapTicks(upperTicks(this.domain(), this.effectiveZoom(), this.pxPerDay(), this.xWindow() ?? undefined)), ...(ngDevMode ? [{ debugName: "upperTicks" }] : /* istanbul ignore next */ []));
|
|
13473
13817
|
/** Render-space today x, or `null` when today is outside the domain (or disabled). */
|
|
13474
13818
|
todayX = computed(() => {
|
|
13475
13819
|
if (!this.showToday())
|
|
@@ -13481,85 +13825,48 @@ class FlyGanttComponent {
|
|
|
13481
13825
|
const logical = dateToX(addDays(today, 0.5), dom.start, this.pxPerDay());
|
|
13482
13826
|
return mapX(logical, this.innerWidth(), this.rtl());
|
|
13483
13827
|
}, ...(ngDevMode ? [{ debugName: "todayX" }] : /* istanbul ignore next */ []));
|
|
13828
|
+
/** Id→absolute-index into {@link visibleRows}, O(N) on data/collapse only (memo §4.4). */
|
|
13829
|
+
_indexById = computed(() => {
|
|
13830
|
+
const m = new Map();
|
|
13831
|
+
this.visibleRows().forEach((f, i) => m.set(f.row.id, i));
|
|
13832
|
+
return m;
|
|
13833
|
+
}, ...(ngDevMode ? [{ debugName: "_indexById" }] : /* istanbul ignore next */ []));
|
|
13834
|
+
/** Only the windowed slice of {@link visibleRows} (memo §4.4). `index` stays the row's ABSOLUTE
|
|
13835
|
+
* position — `y = index × rowHeight` — the `viewBox` pan, not this math, hides the rest. */
|
|
13484
13836
|
rowVms = computed(() => {
|
|
13485
|
-
const
|
|
13486
|
-
const
|
|
13487
|
-
const
|
|
13488
|
-
|
|
13489
|
-
|
|
13490
|
-
|
|
13491
|
-
|
|
13492
|
-
|
|
13493
|
-
return
|
|
13494
|
-
const row = flat.row;
|
|
13495
|
-
const y = index * rh;
|
|
13496
|
-
const midY = y + rh / 2;
|
|
13497
|
-
const isGroup = flat.hasChildren || row.kind === 'group';
|
|
13498
|
-
const isMilestone = row.kind === 'milestone';
|
|
13499
|
-
// Resolve dates, letting an in-flight drag preview win for this row.
|
|
13500
|
-
let start = resolved.get(row.id)?.start ?? null;
|
|
13501
|
-
let end = resolved.get(row.id)?.end ?? null;
|
|
13502
|
-
if (preview && preview.id === row.id) {
|
|
13503
|
-
start = preview.start;
|
|
13504
|
-
end = preview.end;
|
|
13505
|
-
}
|
|
13506
|
-
const editable = !globalReadonly && !row.readonly && !isGroup;
|
|
13507
|
-
const color = row.color ?? null;
|
|
13508
|
-
const tint = this._tintFor(row);
|
|
13509
|
-
if (isMilestone) {
|
|
13510
|
-
const at = start ?? end;
|
|
13511
|
-
const logical = at ? dateToX(at, dom.start, ppd) : 0;
|
|
13512
|
-
const pointX = mapX(logical, w, rtl);
|
|
13513
|
-
return {
|
|
13514
|
-
flat, index, y, midY, shape: at ? 'milestone' : 'empty',
|
|
13515
|
-
x: pointX, width: 0, progressWidth: 0, pointX,
|
|
13516
|
-
startX: pointX, endX: pointX, color, tint,
|
|
13517
|
-
ariaLabel: this._ariaFor(row, start, end, 'milestone'),
|
|
13518
|
-
editable: editable && !!at,
|
|
13519
|
-
};
|
|
13520
|
-
}
|
|
13521
|
-
if (!start || !end) {
|
|
13522
|
-
return {
|
|
13523
|
-
flat, index, y, midY, shape: 'empty',
|
|
13524
|
-
x: 0, width: 0, progressWidth: 0, pointX: 0, startX: 0, endX: 0, color, tint,
|
|
13525
|
-
ariaLabel: this._ariaFor(row, start, end, isGroup ? 'group' : 'bar'),
|
|
13526
|
-
editable: false,
|
|
13527
|
-
};
|
|
13528
|
-
}
|
|
13529
|
-
const xs = dateToX(start, dom.start, ppd);
|
|
13530
|
-
// Bars are inclusive of the end day, so extend one day for a visible width.
|
|
13531
|
-
const xe = dateToX(addDays(end, 1), dom.start, ppd);
|
|
13532
|
-
const { x, width } = projectBar(xs, xe, w, rtl);
|
|
13533
|
-
const progress = Math.max(0, Math.min(100, row.progress ?? 0));
|
|
13534
|
-
return {
|
|
13535
|
-
flat, index, y, midY,
|
|
13536
|
-
shape: isGroup ? 'group' : 'bar',
|
|
13537
|
-
x, width,
|
|
13538
|
-
progressWidth: (width * progress) / 100,
|
|
13539
|
-
pointX: 0,
|
|
13540
|
-
startX: mapX(xs, w, rtl),
|
|
13541
|
-
endX: mapX(xe, w, rtl),
|
|
13542
|
-
color, tint,
|
|
13543
|
-
ariaLabel: this._ariaFor(row, start, end, isGroup ? 'group' : 'bar'),
|
|
13544
|
-
editable: isGroup ? false : editable,
|
|
13545
|
-
};
|
|
13546
|
-
});
|
|
13837
|
+
const all = this.visibleRows();
|
|
13838
|
+
const win = this.rowWindow();
|
|
13839
|
+
const out = [];
|
|
13840
|
+
for (let index = win.firstIndex; index <= win.lastIndex; index++) {
|
|
13841
|
+
const flat = all[index];
|
|
13842
|
+
if (flat)
|
|
13843
|
+
out.push(this._vmForFlat(flat, index));
|
|
13844
|
+
}
|
|
13845
|
+
return out;
|
|
13547
13846
|
}, ...(ngDevMode ? [{ debugName: "rowVms" }] : /* istanbul ignore next */ []));
|
|
13548
13847
|
/** Only the rows carrying a tint, so the band pass does not emit an empty rect per row. */
|
|
13549
13848
|
tintedRows = computed(() => this.rowVms().filter((vm) => vm.tint !== null), ...(ngDevMode ? [{ debugName: "tintedRows" }] : /* istanbul ignore next */ []));
|
|
13550
|
-
/** Dependency arrows
|
|
13849
|
+
/** Dependency arrows whose y-span intersects the row window — O(E), not O(N). Endpoints are
|
|
13850
|
+
* resolved via {@link _vmForFlat} directly (not off `rowVms()`) since an edge can cross the
|
|
13851
|
+
* window with one endpoint outside it — the memo §4.3 rule-2 fix for the pre-A9 "both
|
|
13852
|
+
* endpoints rendered" predicate, which would erase exactly those crossing arrows. */
|
|
13551
13853
|
linkVms = computed(() => {
|
|
13552
|
-
const
|
|
13553
|
-
|
|
13554
|
-
|
|
13555
|
-
vmById.set(vm.flat.row.id, vm);
|
|
13556
|
-
const links = resolveLinks(this.dependencies(), new Set(vmById.keys()));
|
|
13854
|
+
const all = this.visibleRows();
|
|
13855
|
+
const indexById = this._indexById();
|
|
13856
|
+
const links = resolveLinks(this.dependencies(), new Set(indexById.keys()));
|
|
13557
13857
|
const rtl = this.rtl();
|
|
13858
|
+
const win = this.rowWindow();
|
|
13558
13859
|
const out = [];
|
|
13559
13860
|
for (const link of links) {
|
|
13560
|
-
const
|
|
13561
|
-
const
|
|
13562
|
-
if (
|
|
13861
|
+
const fromIdx = indexById.get(link.fromId);
|
|
13862
|
+
const toIdx = indexById.get(link.toId);
|
|
13863
|
+
if (fromIdx === undefined || toIdx === undefined)
|
|
13864
|
+
continue;
|
|
13865
|
+
const from = this._vmForFlat(all[fromIdx], fromIdx);
|
|
13866
|
+
const to = this._vmForFlat(all[toIdx], toIdx);
|
|
13867
|
+
if (from.shape === 'empty' || to.shape === 'empty')
|
|
13868
|
+
continue;
|
|
13869
|
+
if (!linkCrossesWindow(from.midY, to.midY, win))
|
|
13563
13870
|
continue;
|
|
13564
13871
|
const routed = routeLink(from, to, link.type, rtl, this.rowHeight());
|
|
13565
13872
|
out.push({
|
|
@@ -13579,11 +13886,78 @@ class FlyGanttComponent {
|
|
|
13579
13886
|
}
|
|
13580
13887
|
return out;
|
|
13581
13888
|
}, ...(ngDevMode ? [{ debugName: "linkVms" }] : /* istanbul ignore next */ []));
|
|
13889
|
+
/** Full geometry for one row — shared by `rowVms` and `linkVms` (endpoints outside the window). */
|
|
13890
|
+
_vmForFlat(flat, index) {
|
|
13891
|
+
const dom = this.domain();
|
|
13892
|
+
const ppd = this.pxPerDay();
|
|
13893
|
+
const w = this.innerWidth();
|
|
13894
|
+
const rtl = this.rtl();
|
|
13895
|
+
const rh = this.rowHeight();
|
|
13896
|
+
const resolved = this._resolvedDates();
|
|
13897
|
+
const preview = this._dragPreview();
|
|
13898
|
+
const row = flat.row;
|
|
13899
|
+
const y = index * rh;
|
|
13900
|
+
const midY = y + rh / 2;
|
|
13901
|
+
const isGroup = flat.hasChildren || row.kind === 'group';
|
|
13902
|
+
const isMilestone = row.kind === 'milestone';
|
|
13903
|
+
const baseline = this.showBaselines() ? resolveBaselineVm(row, dom, ppd, w, rtl) : null;
|
|
13904
|
+
// Resolve dates, letting an in-flight drag preview win for this row.
|
|
13905
|
+
let start = resolved.get(row.id)?.start ?? null;
|
|
13906
|
+
let end = resolved.get(row.id)?.end ?? null;
|
|
13907
|
+
if (preview && preview.id === row.id) {
|
|
13908
|
+
start = preview.start;
|
|
13909
|
+
end = preview.end;
|
|
13910
|
+
}
|
|
13911
|
+
const editable = !this.readonly() && !row.readonly && !isGroup;
|
|
13912
|
+
const color = row.color ?? null;
|
|
13913
|
+
const tint = this._tintFor(row);
|
|
13914
|
+
if (isMilestone) {
|
|
13915
|
+
const at = start ?? end;
|
|
13916
|
+
const logical = at ? dateToX(at, dom.start, ppd) : 0;
|
|
13917
|
+
const pointX = mapX(logical, w, rtl);
|
|
13918
|
+
return {
|
|
13919
|
+
flat, index, y, midY, shape: at ? 'milestone' : 'empty',
|
|
13920
|
+
x: pointX, width: 0, progressWidth: 0, pointX,
|
|
13921
|
+
startX: pointX, endX: pointX, color, tint, baseline,
|
|
13922
|
+
ariaLabel: this._ariaFor(row, start, end, 'milestone'),
|
|
13923
|
+
editable: editable && !!at,
|
|
13924
|
+
};
|
|
13925
|
+
}
|
|
13926
|
+
if (!start || !end) {
|
|
13927
|
+
return {
|
|
13928
|
+
flat, index, y, midY, shape: 'empty',
|
|
13929
|
+
x: 0, width: 0, progressWidth: 0, pointX: 0, startX: 0, endX: 0, color, tint, baseline,
|
|
13930
|
+
ariaLabel: this._ariaFor(row, start, end, isGroup ? 'group' : 'bar'),
|
|
13931
|
+
editable: false,
|
|
13932
|
+
};
|
|
13933
|
+
}
|
|
13934
|
+
const xs = dateToX(start, dom.start, ppd);
|
|
13935
|
+
// Bars are inclusive of the end day, so extend one day for a visible width.
|
|
13936
|
+
const xe = dateToX(addDays(end, 1), dom.start, ppd);
|
|
13937
|
+
const { x, width } = projectBar(xs, xe, w, rtl);
|
|
13938
|
+
const progress = Math.max(0, Math.min(100, row.progress ?? 0));
|
|
13939
|
+
return {
|
|
13940
|
+
flat, index, y, midY,
|
|
13941
|
+
shape: isGroup ? 'group' : 'bar',
|
|
13942
|
+
x, width,
|
|
13943
|
+
progressWidth: (width * progress) / 100,
|
|
13944
|
+
pointX: 0,
|
|
13945
|
+
startX: mapX(xs, w, rtl),
|
|
13946
|
+
endX: mapX(xe, w, rtl),
|
|
13947
|
+
color, tint, baseline,
|
|
13948
|
+
ariaLabel: this._ariaFor(row, start, end, isGroup ? 'group' : 'bar'),
|
|
13949
|
+
editable: isGroup ? false : editable,
|
|
13950
|
+
};
|
|
13951
|
+
}
|
|
13582
13952
|
// ── Template geometry helpers ─────────────────────────────────────────────────
|
|
13583
13953
|
barHeight = computed(() => Math.max(8, this.rowHeight() - 2 * BAR_V_PAD), ...(ngDevMode ? [{ debugName: "barHeight" }] : /* istanbul ignore next */ []));
|
|
13584
13954
|
barTop(vm) {
|
|
13585
13955
|
return vm.y + BAR_V_PAD;
|
|
13586
13956
|
}
|
|
13957
|
+
/** Baseline underbar's top edge — a thin strip directly beneath the main bar. */
|
|
13958
|
+
baselineBarTop(vm) {
|
|
13959
|
+
return this.barTop(vm) + this.barHeight() + 1;
|
|
13960
|
+
}
|
|
13587
13961
|
milestoneR() {
|
|
13588
13962
|
return Math.min(9, this.barHeight() / 2);
|
|
13589
13963
|
}
|
|
@@ -13593,6 +13967,13 @@ class FlyGanttComponent {
|
|
|
13593
13967
|
const { pointX: cx, midY: cy } = vm;
|
|
13594
13968
|
return `${cx},${cy - r} ${cx + r},${cy} ${cx},${cy + r} ${cx - r},${cy}`;
|
|
13595
13969
|
}
|
|
13970
|
+
/** Hollow outline diamond for a milestone row's baseline date (its own x, same row's y). */
|
|
13971
|
+
baselineDiamondPoints(vm) {
|
|
13972
|
+
const r = Math.max(4, this.milestoneR() - 3);
|
|
13973
|
+
const cx = vm.baseline?.x ?? vm.pointX;
|
|
13974
|
+
const cy = vm.midY;
|
|
13975
|
+
return `${cx},${cy - r} ${cx + r},${cy} ${cx},${cy + r} ${cx - r},${cy}`;
|
|
13976
|
+
}
|
|
13596
13977
|
/**
|
|
13597
13978
|
* Link connector x for a milestone. A diamond has no width, so the connector is nudged clear
|
|
13598
13979
|
* of the marker in the forward-in-time direction (which flips under RTL).
|
|
@@ -13647,14 +14028,45 @@ class FlyGanttComponent {
|
|
|
13647
14028
|
isCollapsed(id) {
|
|
13648
14029
|
return this._collapsed().has(id);
|
|
13649
14030
|
}
|
|
14031
|
+
/** Any row in the current data naming `id` as its parent — i.e. children are actually loaded. */
|
|
14032
|
+
_hasLoadedChildren(id) {
|
|
14033
|
+
return this.rows().some((r) => r.parentId === id);
|
|
14034
|
+
}
|
|
13650
14035
|
toggleCollapse(id, ev) {
|
|
13651
14036
|
ev?.stopPropagation();
|
|
13652
|
-
const
|
|
13653
|
-
|
|
14037
|
+
const collapsed = this._collapsed();
|
|
14038
|
+
const expanding = collapsed.has(id); // currently collapsed → this toggle expands it
|
|
14039
|
+
if (expanding && !this._hasLoadedChildren(id)) {
|
|
14040
|
+
// `hasChildren` was asserted ahead of data (lazy tree) — ask the consumer to fetch rather
|
|
14041
|
+
// than silently expanding into nothing.
|
|
14042
|
+
this.rowExpand.emit(id);
|
|
14043
|
+
}
|
|
14044
|
+
const next = new Set(collapsed);
|
|
14045
|
+
if (expanding)
|
|
13654
14046
|
next.delete(id);
|
|
13655
14047
|
else
|
|
13656
14048
|
next.add(id);
|
|
13657
|
-
this.
|
|
14049
|
+
this._uncontrolledCollapsed.set(next);
|
|
14050
|
+
this.collapsedIdsChange.emit([...next]);
|
|
14051
|
+
}
|
|
14052
|
+
/** Expand every group (clears all collapse state). */
|
|
14053
|
+
expandAll() {
|
|
14054
|
+
this._uncontrolledCollapsed.set(new Set());
|
|
14055
|
+
this.collapsedIdsChange.emit([]);
|
|
14056
|
+
}
|
|
14057
|
+
/** Collapse every group that has (or asserts) children. */
|
|
14058
|
+
collapseAll() {
|
|
14059
|
+
const ids = new Set(this._flat().filter((f) => f.hasChildren).map((f) => f.row.id));
|
|
14060
|
+
this._uncontrolledCollapsed.set(ids);
|
|
14061
|
+
this.collapsedIdsChange.emit([...ids]);
|
|
14062
|
+
}
|
|
14063
|
+
/** Scroll a row into the rendered window and pin it there (deep-link / programmatic focus). */
|
|
14064
|
+
scrollToRow(id) {
|
|
14065
|
+
const idx = this.visibleRows().findIndex((f) => f.row.id === id);
|
|
14066
|
+
if (idx < 0)
|
|
14067
|
+
return;
|
|
14068
|
+
this._viewport.setPinnedIndex(idx);
|
|
14069
|
+
this._viewport.scrollIndexIntoView(idx);
|
|
13658
14070
|
}
|
|
13659
14071
|
/**
|
|
13660
14072
|
* Enter/Space on a focused label row selects it (mirrors the pointer click).
|
|
@@ -13708,17 +14120,38 @@ class FlyGanttComponent {
|
|
|
13708
14120
|
return;
|
|
13709
14121
|
// -1 when nothing is selected yet, so the first ArrowDown lands on row 0 (not row 1).
|
|
13710
14122
|
const curIdx = list.findIndex((r) => r.row.id === this.selectedId());
|
|
14123
|
+
// Treegrid nav (Home/End/PageUp/PageDown/←/→), resolved as pure intent — see gantt-a11y.ts.
|
|
14124
|
+
if (isTreegridNavKey(ev.key)) {
|
|
14125
|
+
const win = this.rowWindow();
|
|
14126
|
+
const pageSize = Math.max(1, win.lastIndex - win.firstIndex + 1 - 2 * this.overscanRows());
|
|
14127
|
+
const a11yRows = list.map((f) => ({
|
|
14128
|
+
id: f.row.id, depth: f.depth, hasChildren: f.hasChildren, collapsed: f.collapsed,
|
|
14129
|
+
}));
|
|
14130
|
+
const intent = resolveTreegridKey(ev.key, curIdx, a11yRows, pageSize);
|
|
14131
|
+
if (intent.kind === 'select') {
|
|
14132
|
+
ev.preventDefault();
|
|
14133
|
+
this.selectedId.set(list[intent.index].row.id);
|
|
14134
|
+
this._viewport.scrollIndexIntoView(intent.index);
|
|
14135
|
+
}
|
|
14136
|
+
else if (intent.kind === 'toggle') {
|
|
14137
|
+
ev.preventDefault();
|
|
14138
|
+
this.toggleCollapse(intent.id);
|
|
14139
|
+
}
|
|
14140
|
+
return;
|
|
14141
|
+
}
|
|
13711
14142
|
switch (ev.key) {
|
|
13712
14143
|
case 'ArrowDown': {
|
|
13713
14144
|
ev.preventDefault();
|
|
13714
14145
|
const i = curIdx < 0 ? 0 : Math.min(list.length - 1, curIdx + 1);
|
|
13715
14146
|
this.selectedId.set(list[i].row.id);
|
|
14147
|
+
this._viewport.scrollIndexIntoView(i);
|
|
13716
14148
|
return;
|
|
13717
14149
|
}
|
|
13718
14150
|
case 'ArrowUp': {
|
|
13719
14151
|
ev.preventDefault();
|
|
13720
14152
|
const i = curIdx < 0 ? 0 : Math.max(0, curIdx - 1);
|
|
13721
14153
|
this.selectedId.set(list[i].row.id);
|
|
14154
|
+
this._viewport.scrollIndexIntoView(i);
|
|
13722
14155
|
return;
|
|
13723
14156
|
}
|
|
13724
14157
|
case '+':
|
|
@@ -13834,6 +14267,7 @@ class FlyGanttComponent {
|
|
|
13834
14267
|
readonly: this.readonly,
|
|
13835
14268
|
rows: this.rowVms,
|
|
13836
14269
|
bodySvg: () => this.bodyRef()?.nativeElement ?? null,
|
|
14270
|
+
windowTopPx: () => this.svgTop(),
|
|
13837
14271
|
createLink: (dependency) => this.dependencyCreate.emit(dependency),
|
|
13838
14272
|
destroyRef: this.destroyRef,
|
|
13839
14273
|
});
|
|
@@ -13885,7 +14319,7 @@ class FlyGanttComponent {
|
|
|
13885
14319
|
: t('gantt.aria.bar', { label: row.label, start: toIsoDate(start), end: toIsoDate(end), progress });
|
|
13886
14320
|
}
|
|
13887
14321
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyGanttComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
13888
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyGanttComponent, isStandalone: true, selector: "fly-gantt", inputs: { rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, dependencies: { classPropertyName: "dependencies", publicName: "dependencies", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null }, showToday: { classPropertyName: "showToday", publicName: "showToday", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, labelWidth: { classPropertyName: "labelWidth", publicName: "labelWidth", isSignal: true, isRequired: false, transformFunction: null }, resizableLabels: { classPropertyName: "resizableLabels", publicName: "resizableLabels", isSignal: true, isRequired: false, transformFunction: null }, rowHeight: { classPropertyName: "rowHeight", publicName: "rowHeight", isSignal: true, isRequired: false, transformFunction: null }, maxRows: { classPropertyName: "maxRows", publicName: "maxRows", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rowDatesChange: "rowDatesChange", dependencyCreate: "dependencyCreate", dependencyDelete: "dependencyDelete", rowClick: "rowClick", rowDblClick: "rowDblClick", labelWidthChange: "labelWidthChange" }, host: { properties: { "class.fly-gantt--rtl": "rtl()", "class.fly-gantt--readonly": "readonly()", "class.fly-gantt--resizing": "resizingLabels()", "attr.dir": "i18n.direction()" }, classAttribute: "fly-gantt" }, viewQueries: [{ propertyName: "bodyRef", first: true, predicate: ["bodySvg"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Cross-scrolling canvas: a CSS grid whose header row and label column are sticky, so the\r\n time grid scrolls under a pinned header + label tree. All SVG geometry is pre-mirrored for\r\n RTL in the component (see gantt-scale mapX), so this template never branches on direction. -->\r\n<div\r\n class=\"fly-gantt__scroll\"\r\n role=\"grid\"\r\n tabindex=\"0\"\r\n [attr.aria-label]=\"'gantt.aria.grid' | translate\"\r\n [attr.aria-rowcount]=\"visibleRows().length\"\r\n (keydown)=\"onGridKeydown($event)\"\r\n>\r\n <div\r\n class=\"fly-gantt__canvas\"\r\n [style.grid-template-columns]=\"effectiveLabelWidth() + 'px ' + innerWidth() + 'px'\"\r\n [style.grid-template-rows]=\"HEADER_H + 'px ' + bodyHeight() + 'px'\"\r\n >\r\n <!-- \u2500\u2500 Corner (pinned both axes) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <div class=\"fly-gantt__corner\" [style.height.px]=\"HEADER_H\">\r\n @if (resizableLabels()) {\r\n <!-- The divider lives in the corner because the corner is the one cell pinned on BOTH\r\n axes: the grip stays reachable however far the user has scrolled. The full-height\r\n strip down the label pane below is the same gesture, presentational only. -->\r\n <div\r\n class=\"fly-gantt__splitter\"\r\n role=\"separator\"\r\n tabindex=\"0\"\r\n aria-orientation=\"vertical\"\r\n [attr.aria-label]=\"'gantt.aria.label_resize' | translate\"\r\n [attr.aria-valuenow]=\"effectiveLabelWidth()\"\r\n [attr.aria-valuemin]=\"MIN_LABEL_W\"\r\n [attr.aria-valuemax]=\"MAX_LABEL_W\"\r\n (pointerdown)=\"onLabelResizePointerDown($event)\"\r\n (keydown)=\"onLabelResizeKeydown($event)\"\r\n (dblclick)=\"resetLabelWidth()\"\r\n ></div>\r\n }\r\n </div>\r\n\r\n <!-- \u2500\u2500 Time header (pinned top) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <svg\r\n class=\"fly-gantt__time-header\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"HEADER_H\"\r\n [attr.viewBox]=\"'0 0 ' + innerWidth() + ' ' + HEADER_H\"\r\n aria-hidden=\"true\"\r\n >\r\n @for (b of weekendBands(); track b.key) {\r\n <rect class=\"fly-gantt__weekend\" [attr.x]=\"b.x\" [attr.y]=\"HEADER_UPPER_H\" [attr.width]=\"b.width\" [attr.height]=\"HEADER_LOWER_H\" />\r\n }\r\n @for (t of upperTicks(); track t.key) {\r\n <line class=\"fly-gantt__tick-line\" [attr.x1]=\"t.x\" [attr.y1]=\"0\" [attr.x2]=\"t.x\" [attr.y2]=\"HEADER_H\" />\r\n <text class=\"fly-gantt__tick-label fly-gantt__tick-label--upper\" [attr.x]=\"t.x + t.width / 2\" [attr.y]=\"HEADER_UPPER_H / 2 + 4\">{{ t.label }}</text>\r\n }\r\n @for (t of lowerTicks(); track t.key) {\r\n <line class=\"fly-gantt__tick-line\" [attr.x1]=\"t.x\" [attr.y1]=\"HEADER_UPPER_H\" [attr.x2]=\"t.x\" [attr.y2]=\"HEADER_H\" />\r\n <text class=\"fly-gantt__tick-label\" [attr.x]=\"t.x + t.width / 2\" [attr.y]=\"HEADER_UPPER_H + HEADER_LOWER_H / 2 + 4\">{{ t.label }}</text>\r\n }\r\n @if (todayX() !== null) {\r\n <line class=\"fly-gantt__today\" [attr.x1]=\"todayX()\" [attr.y1]=\"HEADER_UPPER_H\" [attr.x2]=\"todayX()\" [attr.y2]=\"HEADER_H\" />\r\n }\r\n </svg>\r\n\r\n <!-- \u2500\u2500 Label tree (pinned inline-start) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <div class=\"fly-gantt__labels\" role=\"rowgroup\">\r\n @for (vm of rowVms(); track vm.flat.row.id) {\r\n <div\r\n class=\"fly-gantt__label-row\"\r\n role=\"row\"\r\n tabindex=\"-1\"\r\n [class.fly-gantt__label-row--selected]=\"selectedId() === vm.flat.row.id\"\r\n [class.fly-gantt__label-row--tinted]=\"vm.tint !== null\"\r\n [style.height.px]=\"rowHeight()\"\r\n [style.--fly-gantt-row-tint]=\"vm.tint\"\r\n [attr.aria-selected]=\"selectedId() === vm.flat.row.id\"\r\n [attr.aria-label]=\"vm.ariaLabel\"\r\n (click)=\"onRowClick(vm.flat.row.id)\"\r\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\r\n (keydown.enter)=\"onLabelRowKeydown($event, vm.flat.row.id)\"\r\n >\r\n <span class=\"fly-gantt__label-inner\" [style.padding-inline-start.px]=\"indentFor(vm.flat.depth)\">\r\n @if (vm.flat.hasChildren) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-gantt__chevron\"\r\n [class.fly-gantt__chevron--collapsed]=\"isCollapsed(vm.flat.row.id)\"\r\n [attr.aria-label]=\"(isCollapsed(vm.flat.row.id) ? 'gantt.expand' : 'gantt.collapse') | translate\"\r\n [attr.aria-expanded]=\"!isCollapsed(vm.flat.row.id)\"\r\n (click)=\"toggleCollapse(vm.flat.row.id, $event)\"\r\n >\u25B8</button>\r\n } @else {\r\n <span class=\"fly-gantt__chevron-spacer\"></span>\r\n }\r\n @if (vm.color) {\r\n <!-- Colour chip: the row's own colour at full strength, so a milestone stays\r\n identifiable in the label pane where the band tint is deliberately faint. -->\r\n <span class=\"fly-gantt__label-swatch\" [style.background]=\"vm.color\" aria-hidden=\"true\"></span>\r\n }\r\n <span\r\n class=\"fly-gantt__label-text\"\r\n [class.fly-gantt__label-text--group]=\"vm.shape === 'group'\"\r\n [title]=\"vm.flat.row.label\"\r\n >{{ vm.flat.row.label }}</span>\r\n </span>\r\n </div>\r\n }\r\n @if (resizableLabels()) {\r\n <!-- aria-hidden so this presentational twin of the corner separator is not a second\r\n (and structurally invalid) child of the rowgroup in the accessibility tree. -->\r\n <div\r\n class=\"fly-gantt__splitter fly-gantt__splitter--rail\"\r\n aria-hidden=\"true\"\r\n (pointerdown)=\"onLabelResizePointerDown($event)\"\r\n (dblclick)=\"resetLabelWidth()\"\r\n ></div>\r\n }\r\n </div>\r\n\r\n <!-- \u2500\u2500 Time grid body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <svg\r\n #bodySvg\r\n class=\"fly-gantt__body\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"bodyHeight()\"\r\n [attr.viewBox]=\"'0 0 ' + innerWidth() + ' ' + bodyHeight()\"\r\n >\r\n <defs>\r\n <marker\r\n [attr.id]=\"markerId\"\r\n markerWidth=\"8\"\r\n markerHeight=\"8\"\r\n refX=\"6\"\r\n refY=\"4\"\r\n orient=\"auto\"\r\n markerUnits=\"userSpaceOnUse\"\r\n >\r\n <path class=\"fly-gantt__arrowhead\" d=\"M0,0 L7,4 L0,8 Z\" />\r\n </marker>\r\n </defs>\r\n\r\n <!-- Row colour bands sit at the very bottom of the stack, so the weekend shading and the\r\n gridlines below still read through them. -->\r\n @for (vm of tintedRows(); track vm.flat.row.id) {\r\n <rect\r\n class=\"fly-gantt__row-band\"\r\n [attr.x]=\"0\"\r\n [attr.y]=\"vm.y\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"rowHeight()\"\r\n [style.fill]=\"vm.tint\"\r\n />\r\n }\r\n\r\n <!-- Weekend shading + vertical gridlines (behind the bars). -->\r\n @for (b of weekendBands(); track b.key) {\r\n <rect class=\"fly-gantt__weekend\" [attr.x]=\"b.x\" [attr.y]=\"0\" [attr.width]=\"b.width\" [attr.height]=\"bodyHeight()\" />\r\n }\r\n @for (t of lowerTicks(); track t.key) {\r\n <line class=\"fly-gantt__grid-line\" [attr.x1]=\"t.x\" [attr.y1]=\"0\" [attr.x2]=\"t.x\" [attr.y2]=\"bodyHeight()\" />\r\n }\r\n\r\n <!-- Rows: a full-width hit rect (click + link drop target) then the shape. -->\r\n @for (vm of rowVms(); track vm.flat.row.id) {\r\n <g class=\"fly-gantt__row\" [attr.data-row-id]=\"vm.flat.row.id\">\r\n <rect\r\n class=\"fly-gantt__row-hit\"\r\n [class.fly-gantt__row-hit--selected]=\"selectedId() === vm.flat.row.id\"\r\n [attr.x]=\"0\"\r\n [attr.y]=\"vm.y\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"rowHeight()\"\r\n (pointerdown)=\"onRowClick(vm.flat.row.id)\"\r\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\r\n />\r\n\r\n @switch (vm.shape) {\r\n @case ('group') {\r\n <path class=\"fly-gantt__group\" [attr.d]=\"groupPath(vm)\" [style.fill]=\"vm.color\" [style.stroke]=\"vm.color\" />\r\n }\r\n @case ('milestone') {\r\n <polygon\r\n class=\"fly-gantt__milestone\"\r\n [class.fly-gantt__milestone--editable]=\"vm.editable\"\r\n [attr.points]=\"diamondPoints(vm)\"\r\n [style.fill]=\"vm.color\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'move')\"\r\n />\r\n @if (!readonly() && !vm.flat.row.readonly) {\r\n <!-- A diamond has no width, so one connector serves it; it reads as the finish\r\n end, which is what makes a milestone\u2192task drag the FS everyone expects. -->\r\n <circle\r\n class=\"fly-gantt__connector\"\r\n [attr.cx]=\"milestoneConnectorX(vm)\"\r\n [attr.cy]=\"vm.midY\"\r\n r=\"4\"\r\n [attr.aria-label]=\"'gantt.aria.link_handle' | translate: { label: vm.flat.row.label }\"\r\n (pointerdown)=\"onLinkPointerDown($event, vm, 'finish')\"\r\n />\r\n }\r\n }\r\n @case ('bar') {\r\n <g class=\"fly-gantt__bar-group\">\r\n <rect\r\n class=\"fly-gantt__bar\"\r\n [class.fly-gantt__bar--editable]=\"vm.editable\"\r\n [attr.x]=\"vm.x\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"vm.width\"\r\n [attr.height]=\"barHeight()\"\r\n rx=\"4\"\r\n [style.fill]=\"vm.color\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'move')\"\r\n />\r\n @if (vm.progressWidth > 0) {\r\n <rect\r\n class=\"fly-gantt__progress\"\r\n [attr.x]=\"vm.x\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"vm.progressWidth\"\r\n [attr.height]=\"barHeight()\"\r\n rx=\"4\"\r\n />\r\n }\r\n @if (vm.editable) {\r\n <!-- Resize handles at both edges. -->\r\n <rect\r\n class=\"fly-gantt__handle\"\r\n [attr.x]=\"vm.x\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"HANDLE_W\"\r\n [attr.height]=\"barHeight()\"\r\n [attr.aria-label]=\"'gantt.aria.resize_start' | translate\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'resize-start')\"\r\n />\r\n <rect\r\n class=\"fly-gantt__handle\"\r\n [attr.x]=\"vm.x + vm.width - HANDLE_W\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"HANDLE_W\"\r\n [attr.height]=\"barHeight()\"\r\n [attr.aria-label]=\"'gantt.aria.resize_end' | translate\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'resize-end')\"\r\n />\r\n }\r\n @if (!readonly() && !vm.flat.row.readonly) {\r\n <!-- One connector per END. Dragging from the start edge yields an SS/SF link\r\n and from the finish edge an FS/FF one \u2014 the drop edge picks which. -->\r\n <circle\r\n class=\"fly-gantt__connector\"\r\n [attr.cx]=\"vm.startX\"\r\n [attr.cy]=\"vm.midY\"\r\n r=\"4\"\r\n [attr.aria-label]=\"'gantt.aria.link_handle_start' | translate: { label: vm.flat.row.label }\"\r\n (pointerdown)=\"onLinkPointerDown($event, vm, 'start')\"\r\n />\r\n <circle\r\n class=\"fly-gantt__connector\"\r\n [attr.cx]=\"vm.endX\"\r\n [attr.cy]=\"vm.midY\"\r\n r=\"4\"\r\n [attr.aria-label]=\"'gantt.aria.link_handle' | translate: { label: vm.flat.row.label }\"\r\n (pointerdown)=\"onLinkPointerDown($event, vm, 'finish')\"\r\n />\r\n }\r\n </g>\r\n }\r\n }\r\n </g>\r\n }\r\n\r\n <!-- Dependency arrows (skipped for missing / collapsed endpoints). Each carries a fat\r\n transparent twin so a 1.5px line is still clickable at pointer precision. -->\r\n @for (link of linkVms(); track link.key) {\r\n <g class=\"fly-gantt__link-group\" [class.fly-gantt__link-group--selected]=\"selectedLinkKey() === link.key\">\r\n <path\r\n class=\"fly-gantt__link-hit\"\r\n [attr.d]=\"link.path\"\r\n [attr.aria-label]=\"link.ariaLabel\"\r\n (click)=\"onLinkClick($event, link)\"\r\n />\r\n <path class=\"fly-gantt__link\" [attr.d]=\"link.path\" [attr.marker-end]=\"'url(#' + markerId + ')'\" />\r\n @if (selectedLinkKey() === link.key && !readonly()) {\r\n <g\r\n class=\"fly-gantt__link-delete\"\r\n [attr.transform]=\"'translate(' + link.badgeX + ',' + link.badgeY + ')'\"\r\n [attr.aria-label]=\"'gantt.aria.delete_link' | translate\"\r\n (click)=\"deleteLink($event, link)\"\r\n >\r\n <circle class=\"fly-gantt__link-delete-bg\" r=\"8\" />\r\n <path class=\"fly-gantt__link-delete-x\" d=\"M-3.5,-3.5 L3.5,3.5 M3.5,-3.5 L-3.5,3.5\" />\r\n </g>\r\n }\r\n </g>\r\n }\r\n\r\n <!-- In-flight link rubber band. -->\r\n @if (linkGesturePath(); as gp) {\r\n <path class=\"fly-gantt__link fly-gantt__link--ghost\" [attr.d]=\"gp\" [attr.marker-end]=\"'url(#' + markerId + ')'\" />\r\n }\r\n\r\n <!-- Today line (over the bars). -->\r\n @if (todayX() !== null) {\r\n <line class=\"fly-gantt__today\" [attr.x1]=\"todayX()\" [attr.y1]=\"0\" [attr.x2]=\"todayX()\" [attr.y2]=\"bodyHeight()\" />\r\n }\r\n </svg>\r\n </div>\r\n\r\n @if (visibleRows().length === 0) {\r\n <p class=\"fly-gantt__empty\">{{ 'gantt.no_data' | translate }}</p>\r\n }\r\n @if (overflowCount() > 0) {\r\n <p class=\"fly-gantt__overflow\">\r\n {{ 'gantt.overflow' | translate: { shown: maxRows(), total: maxRows() + overflowCount() } }}\r\n </p>\r\n }\r\n</div>\r\n\r\n<!-- Drag candidate tooltip (HTML overlay, follows the cursor). -->\r\n@if (dragTooltip(); as tip) {\r\n <div class=\"fly-gantt__tooltip\" [style.left.px]=\"tip.x\" [style.top.px]=\"tip.y\">{{ tip.text }}</div>\r\n}\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--bg-2, var(--surface-card, #fff));--_surface-alt: var(--bg-3, var(--glass-bg-elevated, #f8fafc));--_surface-hover: var(--bg-hover, var(--surface-hover, #f1f5f9));--_border: var(--w08, var(--surface-border, #e2e8f0));--_border-strong: var(--w1, var(--surface-border, #cbd5e1));--_grid-line: var(--line-3, var(--surface-border, #eef2f7));--_text: var(--ink, var(--text-color, #0f172a));--_text-subtle: var(--ink-3, var(--text-color-secondary, #64748b));--_text-faint: var(--ink-4, var(--text-color-secondary, #94a3b8));--_accent: var(--accent);--_bar: var(--_accent);--_bar-progress: color-mix(in srgb, var(--_accent) 72%, #000);--_weekend: color-mix(in srgb, var(--_text-subtle) 9%, transparent);--_today: var(--danger, #ef4444);--_link: var(--_text-subtle);--_radius: var(--r-lg, 8px);--_transition: var(--t-state, .12s ease);display:block;inline-size:100%;block-size:100%;min-block-size:200px;color:var(--_text);font-size:13px}:host(.fly-gantt--resizing){cursor:col-resize;-webkit-user-select:none;user-select:none}.fly-gantt__scroll{position:relative;inline-size:100%;block-size:100%;overflow:auto;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);box-shadow:var(--shadow-card, none);outline:none}.fly-gantt__scroll:focus-visible{outline:2px solid;outline-color:var(--_accent);outline-offset:-2px}.fly-gantt__canvas{display:grid;position:relative}.fly-gantt__corner{position:sticky;inset-block-start:0;inset-inline-start:0;z-index:4;background:var(--_surface-alt);-webkit-backdrop-filter:blur(12px) saturate(140%);backdrop-filter:blur(12px) saturate(140%);border-inline-end:1px solid var(--_border);border-block-end:1px solid var(--_border-strong)}.fly-gantt__time-header{position:sticky;inset-block-start:0;z-index:3;display:block;background:var(--_surface-alt);-webkit-backdrop-filter:blur(12px) saturate(140%);backdrop-filter:blur(12px) saturate(140%);border-block-end:1px solid var(--_border-strong)}.fly-gantt__tick-line{stroke:var(--_grid-line);stroke-width:1}.fly-gantt__tick-label{fill:var(--_text-subtle);font-size:11px;text-anchor:middle;dominant-baseline:middle}.fly-gantt__tick-label--upper{fill:var(--_text);font-weight:600}.fly-gantt__splitter{position:absolute;inset-block:0;inset-inline-end:-3px;inline-size:7px;z-index:5;cursor:col-resize;touch-action:none}.fly-gantt__splitter:after{content:\"\";position:absolute;inset-block:0;inset-inline-start:3px;inline-size:1px;background:transparent;transition:background var(--_transition)}.fly-gantt__splitter:hover:after,.fly-gantt__splitter:focus-visible:after{background:var(--_accent)}.fly-gantt__splitter:focus-visible{outline:2px solid;outline-color:var(--_accent);outline-offset:-1px}:host(.fly-gantt--resizing) .fly-gantt__splitter:after{background:var(--_accent)}.fly-gantt__labels{position:sticky;inset-inline-start:0;z-index:2;background:var(--_surface);border-inline-end:1px solid var(--_border)}.fly-gantt__label-row{position:relative;display:flex;align-items:center;border-block-end:1px solid var(--_grid-line);cursor:pointer;transition:background var(--_transition)}.fly-gantt__label-row:hover{background:var(--_surface-hover)}.fly-gantt__label-row--selected{background:var(--_surface-hover);box-shadow:inset 3px 0 0 0 var(--_accent)}.fly-gantt__label-row--tinted:before{content:\"\";position:absolute;inset:0;background:var(--fly-gantt-row-tint);pointer-events:none}.fly-gantt__label-inner{position:relative;display:flex;align-items:center;gap:4px;inline-size:100%;min-inline-size:0;padding-inline-end:8px}.fly-gantt__label-swatch{flex:0 0 auto;inline-size:8px;block-size:8px;border-radius:2px}.fly-gantt__chevron{flex:0 0 auto;inline-size:18px;block-size:18px;padding:0;border:none;background:transparent;color:var(--_text-subtle);cursor:pointer;line-height:1;transform:rotate(90deg);transition:transform var(--_transition)}.fly-gantt__chevron--collapsed{transform:rotate(0)}:host(.fly-gantt--rtl) .fly-gantt__chevron--collapsed{transform:rotate(180deg)}.fly-gantt__chevron:focus-visible{outline:2px solid;outline-color:var(--_accent);outline-offset:1px;border-radius:4px}.fly-gantt__chevron-spacer{flex:0 0 auto;inline-size:18px}.fly-gantt__label-text{min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-gantt__label-text--group{font-weight:600}.fly-gantt__body{display:block;touch-action:pan-x pan-y}.fly-gantt__row-band{pointer-events:none}.fly-gantt__weekend{fill:var(--_weekend)}.fly-gantt__grid-line{stroke:var(--_grid-line);stroke-width:1}.fly-gantt__row-hit{fill:transparent}.fly-gantt__row-hit--selected{fill:var(--_surface-hover)}.fly-gantt__bar{fill:var(--_bar);stroke:none}.fly-gantt__bar--editable{cursor:grab}.fly-gantt__progress{fill:var(--_bar-progress);pointer-events:none}.fly-gantt__handle{fill:transparent;cursor:ew-resize}.fly-gantt__connector{fill:var(--_surface);stroke:var(--_accent);stroke-width:1.5;cursor:crosshair;opacity:0;transition:opacity var(--_transition)}.fly-gantt__row:hover .fly-gantt__connector{opacity:1}.fly-gantt__milestone{fill:var(--_accent);stroke:var(--_surface);stroke-width:1}.fly-gantt__milestone--editable{cursor:grab}.fly-gantt__group{fill:var(--_text-subtle);stroke:var(--_text-subtle)}.fly-gantt__link{fill:none;stroke:var(--_link);stroke-width:1.5;pointer-events:none}.fly-gantt__link-hit{fill:none;stroke:transparent;stroke-width:11;pointer-events:stroke;cursor:pointer}.fly-gantt__link-group:hover .fly-gantt__link{stroke:var(--_text)}.fly-gantt__link-group--selected .fly-gantt__link{stroke:var(--_accent);stroke-width:2.5}.fly-gantt__link-delete-bg{fill:var(--_accent)}.fly-gantt__link-delete{cursor:pointer}.fly-gantt__link-delete-x{stroke:var(--_surface);stroke-width:1.75;stroke-linecap:round;fill:none;pointer-events:none}.fly-gantt__link--ghost{stroke-dasharray:4 3;opacity:.8}.fly-gantt__arrowhead{fill:var(--_link)}.fly-gantt__today{stroke:var(--_today);stroke-width:1.5;stroke-dasharray:3 3;pointer-events:none}.fly-gantt__empty,.fly-gantt__overflow{margin:0;padding:12px;color:var(--_text-faint);font-size:12px;text-align:center}.fly-gantt__tooltip{position:fixed;z-index:20;transform:translate(12px,16px);padding:4px 8px;border-radius:6px;background:var(--_text);color:var(--_surface);font-size:12px;white-space:nowrap;pointer-events:none;direction:ltr;unicode-bidi:isolate}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
14322
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyGanttComponent, isStandalone: true, selector: "fly-gantt", inputs: { rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, dependencies: { classPropertyName: "dependencies", publicName: "dependencies", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null }, showToday: { classPropertyName: "showToday", publicName: "showToday", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, labelWidth: { classPropertyName: "labelWidth", publicName: "labelWidth", isSignal: true, isRequired: false, transformFunction: null }, resizableLabels: { classPropertyName: "resizableLabels", publicName: "resizableLabels", isSignal: true, isRequired: false, transformFunction: null }, rowHeight: { classPropertyName: "rowHeight", publicName: "rowHeight", isSignal: true, isRequired: false, transformFunction: null }, maxRows: { classPropertyName: "maxRows", publicName: "maxRows", isSignal: true, isRequired: false, transformFunction: null }, collapsedIds: { classPropertyName: "collapsedIds", publicName: "collapsedIds", isSignal: true, isRequired: false, transformFunction: null }, overscanRows: { classPropertyName: "overscanRows", publicName: "overscanRows", isSignal: true, isRequired: false, transformFunction: null }, maxCanvasPx: { classPropertyName: "maxCanvasPx", publicName: "maxCanvasPx", isSignal: true, isRequired: false, transformFunction: null }, virtualized: { classPropertyName: "virtualized", publicName: "virtualized", isSignal: true, isRequired: false, transformFunction: null }, showBaselines: { classPropertyName: "showBaselines", publicName: "showBaselines", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rowDatesChange: "rowDatesChange", dependencyCreate: "dependencyCreate", dependencyDelete: "dependencyDelete", rowClick: "rowClick", rowDblClick: "rowDblClick", labelWidthChange: "labelWidthChange", collapsedIdsChange: "collapsedIdsChange", rowExpand: "rowExpand", effectiveZoomChange: "effectiveZoomChange" }, host: { properties: { "class.fly-gantt--rtl": "rtl()", "class.fly-gantt--readonly": "readonly()", "class.fly-gantt--resizing": "resizingLabels()", "attr.dir": "i18n.direction()" }, classAttribute: "fly-gantt" }, viewQueries: [{ propertyName: "bodyRef", first: true, predicate: ["bodySvg"], descendants: true, isSignal: true }, { propertyName: "scrollElRef", first: true, predicate: ["scrollEl"], descendants: true, isSignal: true }, { propertyName: "canvasElRef", first: true, predicate: ["canvasEl"], descendants: true, isSignal: true }, { propertyName: "labelsElRef", first: true, predicate: ["labelsEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Cross-scrolling canvas: a CSS grid whose header row and label column are sticky, so the\r\n time grid scrolls under a pinned header + label tree. All SVG geometry is pre-mirrored for\r\n RTL in the component (see gantt-scale mapX), so this template never branches on direction. -->\r\n<div\r\n #scrollEl\r\n class=\"fly-gantt__scroll\"\r\n role=\"treegrid\"\r\n tabindex=\"0\"\r\n [attr.aria-label]=\"'gantt.aria.grid' | translate\"\r\n [attr.aria-rowcount]=\"totalRowCount()\"\r\n (keydown)=\"onGridKeydown($event)\"\r\n>\r\n <div\r\n #canvasEl\r\n class=\"fly-gantt__canvas\"\r\n [style.grid-template-columns]=\"effectiveLabelWidth() + 'px ' + innerWidth() + 'px'\"\r\n [style.grid-template-rows]=\"HEADER_H + 'px ' + bodyHeight() + 'px'\"\r\n >\r\n <!-- \u2500\u2500 Corner (pinned both axes) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <div class=\"fly-gantt__corner\" [style.height.px]=\"HEADER_H\">\r\n @if (resizableLabels()) {\r\n <!-- The divider lives in the corner because the corner is the one cell pinned on BOTH\r\n axes: the grip stays reachable however far the user has scrolled. The full-height\r\n strip down the label pane below is the same gesture, presentational only. -->\r\n <div\r\n class=\"fly-gantt__splitter\"\r\n role=\"separator\"\r\n tabindex=\"0\"\r\n aria-orientation=\"vertical\"\r\n [attr.aria-label]=\"'gantt.aria.label_resize' | translate\"\r\n [attr.aria-valuenow]=\"effectiveLabelWidth()\"\r\n [attr.aria-valuemin]=\"MIN_LABEL_W\"\r\n [attr.aria-valuemax]=\"MAX_LABEL_W\"\r\n (pointerdown)=\"onLabelResizePointerDown($event)\"\r\n (keydown)=\"onLabelResizeKeydown($event)\"\r\n (dblclick)=\"resetLabelWidth()\"\r\n ></div>\r\n }\r\n </div>\r\n\r\n <!-- \u2500\u2500 Time header (pinned top) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <svg\r\n class=\"fly-gantt__time-header\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"HEADER_H\"\r\n [attr.viewBox]=\"'0 0 ' + innerWidth() + ' ' + HEADER_H\"\r\n aria-hidden=\"true\"\r\n >\r\n @for (b of weekendBands(); track b.key) {\r\n <rect class=\"fly-gantt__weekend\" [attr.x]=\"b.x\" [attr.y]=\"HEADER_UPPER_H\" [attr.width]=\"b.width\" [attr.height]=\"HEADER_LOWER_H\" />\r\n }\r\n @for (t of upperTicks(); track t.key) {\r\n <line class=\"fly-gantt__tick-line\" [attr.x1]=\"t.x\" [attr.y1]=\"0\" [attr.x2]=\"t.x\" [attr.y2]=\"HEADER_H\" />\r\n <text class=\"fly-gantt__tick-label fly-gantt__tick-label--upper\" [attr.x]=\"t.x + t.width / 2\" [attr.y]=\"HEADER_UPPER_H / 2 + 4\">{{ t.label }}</text>\r\n }\r\n @for (t of lowerTicks(); track t.key) {\r\n <line class=\"fly-gantt__tick-line\" [attr.x1]=\"t.x\" [attr.y1]=\"HEADER_UPPER_H\" [attr.x2]=\"t.x\" [attr.y2]=\"HEADER_H\" />\r\n <text class=\"fly-gantt__tick-label\" [attr.x]=\"t.x + t.width / 2\" [attr.y]=\"HEADER_UPPER_H + HEADER_LOWER_H / 2 + 4\">{{ t.label }}</text>\r\n }\r\n @if (todayX() !== null) {\r\n <line class=\"fly-gantt__today\" [attr.x1]=\"todayX()\" [attr.y1]=\"HEADER_UPPER_H\" [attr.x2]=\"todayX()\" [attr.y2]=\"HEADER_H\">\r\n <title>{{ 'gantt.today' | translate }}</title>\r\n </line>\r\n }\r\n </svg>\r\n\r\n <!-- \u2500\u2500 Label tree (pinned inline-start) \u2014 the ACCESSIBLE projection: the body SVG below is\r\n aria-hidden, so every row/level/expanded/selected fact a screen reader needs lives here. -->\r\n <div #labelsEl class=\"fly-gantt__labels\" role=\"rowgroup\">\r\n @for (vm of rowVms(); track vm.flat.row.id) {\r\n <div\r\n class=\"fly-gantt__label-row\"\r\n role=\"row\"\r\n [attr.tabindex]=\"selectedId() === vm.flat.row.id ? 0 : -1\"\r\n [attr.data-row-id]=\"vm.flat.row.id\"\r\n [class.fly-gantt__label-row--selected]=\"selectedId() === vm.flat.row.id\"\r\n [class.fly-gantt__label-row--tinted]=\"vm.tint !== null\"\r\n [style.height.px]=\"rowHeight()\"\r\n [style.--fly-gantt-row-tint]=\"vm.tint\"\r\n [attr.aria-selected]=\"selectedId() === vm.flat.row.id\"\r\n [attr.aria-rowindex]=\"vm.index + 1\"\r\n [attr.aria-level]=\"vm.flat.depth + 1\"\r\n [attr.aria-expanded]=\"vm.flat.hasChildren ? !vm.flat.collapsed : null\"\r\n [attr.aria-label]=\"vm.ariaLabel\"\r\n (click)=\"onRowClick(vm.flat.row.id)\"\r\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\r\n (keydown.enter)=\"onLabelRowKeydown($event, vm.flat.row.id)\"\r\n >\r\n <span class=\"fly-gantt__label-inner\" role=\"gridcell\" [style.padding-inline-start.px]=\"indentFor(vm.flat.depth)\">\r\n @if (vm.flat.hasChildren) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-gantt__chevron\"\r\n [class.fly-gantt__chevron--collapsed]=\"isCollapsed(vm.flat.row.id)\"\r\n [attr.aria-label]=\"(isCollapsed(vm.flat.row.id) ? 'gantt.expand' : 'gantt.collapse') | translate\"\r\n [attr.aria-expanded]=\"!isCollapsed(vm.flat.row.id)\"\r\n (click)=\"toggleCollapse(vm.flat.row.id, $event)\"\r\n >\u25B8</button>\r\n } @else {\r\n <span class=\"fly-gantt__chevron-spacer\"></span>\r\n }\r\n @if (vm.color) {\r\n <!-- Colour chip: the row's own colour at full strength, so a milestone stays\r\n identifiable in the label pane where the band tint is deliberately faint. -->\r\n <span class=\"fly-gantt__label-swatch\" [style.background]=\"vm.color\" aria-hidden=\"true\"></span>\r\n }\r\n <span\r\n class=\"fly-gantt__label-text\"\r\n [class.fly-gantt__label-text--group]=\"vm.shape === 'group'\"\r\n [title]=\"vm.flat.row.label\"\r\n >{{ vm.flat.row.label }}</span>\r\n </span>\r\n <!-- Visually-hidden second gridcell: the timeline summary a sighted user reads off the\r\n bar/diamond geometry. The row's own aria-label already announces it; this cell keeps\r\n the treegrid's row\u2192cell structure valid for a cell-navigating AT. -->\r\n <span class=\"fly-gantt__label-cell-hidden\" role=\"gridcell\">{{ vm.ariaLabel }}</span>\r\n </div>\r\n }\r\n @if (resizableLabels()) {\r\n <!-- aria-hidden so this presentational twin of the corner separator is not a second\r\n (and structurally invalid) child of the rowgroup in the accessibility tree. -->\r\n <div\r\n class=\"fly-gantt__splitter fly-gantt__splitter--rail\"\r\n aria-hidden=\"true\"\r\n (pointerdown)=\"onLabelResizePointerDown($event)\"\r\n (dblclick)=\"resetLabelWidth()\"\r\n ></div>\r\n }\r\n </div>\r\n\r\n <!-- \u2500\u2500 Time grid body \u2014 a spacer cell (full height, keeps the native scrollbar honest) with\r\n the actual `<svg>` absolutely positioned + `aria-hidden` (the label pane above is the\r\n accessible projection). The `viewBox` y-offset panning IS the vertical virtualization \u2014\r\n see `gantt-window.ts`'s docstring; nothing below this point knows windowing exists. -->\r\n <div class=\"fly-gantt__body-cell\" [style.height.px]=\"bodyHeight()\">\r\n <svg\r\n #bodySvg\r\n class=\"fly-gantt__body\"\r\n aria-hidden=\"true\"\r\n [style.top.px]=\"svgTop()\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"svgHeight()\"\r\n [attr.viewBox]=\"'0 ' + svgTop() + ' ' + innerWidth() + ' ' + svgHeight()\"\r\n >\r\n <defs>\r\n <marker\r\n [attr.id]=\"markerId\"\r\n markerWidth=\"8\"\r\n markerHeight=\"8\"\r\n refX=\"6\"\r\n refY=\"4\"\r\n orient=\"auto\"\r\n markerUnits=\"userSpaceOnUse\"\r\n >\r\n <path class=\"fly-gantt__arrowhead\" d=\"M0,0 L7,4 L0,8 Z\" />\r\n </marker>\r\n </defs>\r\n\r\n <!-- Row colour bands sit at the very bottom of the stack, so the weekend shading and the\r\n gridlines below still read through them. -->\r\n @for (vm of tintedRows(); track vm.flat.row.id) {\r\n <rect\r\n class=\"fly-gantt__row-band\"\r\n [attr.x]=\"0\"\r\n [attr.y]=\"vm.y\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"rowHeight()\"\r\n [style.fill]=\"vm.tint\"\r\n />\r\n }\r\n\r\n <!-- Weekend shading + vertical gridlines (behind the bars) \u2014 span the WINDOW's y-extent,\r\n like the today line above; `weekendBands`/`lowerTicks` are already x-windowed too. -->\r\n @for (b of weekendBands(); track b.key) {\r\n <rect class=\"fly-gantt__weekend\" [attr.x]=\"b.x\" [attr.y]=\"svgTop()\" [attr.width]=\"b.width\" [attr.height]=\"svgHeight()\" />\r\n }\r\n @for (t of lowerTicks(); track t.key) {\r\n <line class=\"fly-gantt__grid-line\" [attr.x1]=\"t.x\" [attr.y1]=\"svgTop()\" [attr.x2]=\"t.x\" [attr.y2]=\"svgTop() + svgHeight()\" />\r\n }\r\n\r\n <!-- Rows: a full-width hit rect (click + link drop target) then the shape. -->\r\n @for (vm of rowVms(); track vm.flat.row.id) {\r\n <g class=\"fly-gantt__row\" [attr.data-row-id]=\"vm.flat.row.id\">\r\n <rect\r\n class=\"fly-gantt__row-hit\"\r\n [class.fly-gantt__row-hit--selected]=\"selectedId() === vm.flat.row.id\"\r\n [attr.x]=\"0\"\r\n [attr.y]=\"vm.y\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"rowHeight()\"\r\n (pointerdown)=\"onRowClick(vm.flat.row.id)\"\r\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\r\n />\r\n\r\n @switch (vm.shape) {\r\n @case ('group') {\r\n <path class=\"fly-gantt__group\" [attr.d]=\"groupPath(vm)\" [style.fill]=\"vm.color\" [style.stroke]=\"vm.color\" />\r\n }\r\n @case ('milestone') {\r\n <polygon\r\n class=\"fly-gantt__milestone\"\r\n [class.fly-gantt__milestone--editable]=\"vm.editable\"\r\n [attr.points]=\"diamondPoints(vm)\"\r\n [style.fill]=\"vm.color\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'move')\"\r\n />\r\n @if (vm.baseline) {\r\n <!-- Baseline (plan-of-record) date: a small HOLLOW diamond so it reads as a\r\n comparison against the solid current one, never a duplicate marker. -->\r\n <polygon class=\"fly-gantt__baseline-milestone\" [attr.points]=\"baselineDiamondPoints(vm)\" />\r\n }\r\n @if (!readonly() && !vm.flat.row.readonly) {\r\n <!-- A diamond has no width, so one connector serves it; it reads as the finish\r\n end, which is what makes a milestone\u2192task drag the FS everyone expects. -->\r\n <circle\r\n class=\"fly-gantt__connector\"\r\n [attr.cx]=\"milestoneConnectorX(vm)\"\r\n [attr.cy]=\"vm.midY\"\r\n r=\"4\"\r\n [attr.aria-label]=\"'gantt.aria.link_handle' | translate: { label: vm.flat.row.label }\"\r\n (pointerdown)=\"onLinkPointerDown($event, vm, 'finish')\"\r\n />\r\n }\r\n }\r\n @case ('bar') {\r\n <g class=\"fly-gantt__bar-group\">\r\n <rect\r\n class=\"fly-gantt__bar\"\r\n [class.fly-gantt__bar--editable]=\"vm.editable\"\r\n [attr.x]=\"vm.x\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"vm.width\"\r\n [attr.height]=\"barHeight()\"\r\n rx=\"4\"\r\n [style.fill]=\"vm.color\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'move')\"\r\n />\r\n @if (vm.progressWidth > 0) {\r\n <rect\r\n class=\"fly-gantt__progress\"\r\n [attr.x]=\"vm.x\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"vm.progressWidth\"\r\n [attr.height]=\"barHeight()\"\r\n rx=\"4\"\r\n />\r\n }\r\n @if (vm.baseline) {\r\n <!-- Baseline (plan-of-record) underbar \u2014 a thin muted strip beneath the current\r\n bar, the generic \"baseline vs current\" comparison. -->\r\n <rect\r\n class=\"fly-gantt__baseline-bar\"\r\n [attr.x]=\"vm.baseline.x\"\r\n [attr.y]=\"baselineBarTop(vm)\"\r\n [attr.width]=\"vm.baseline.width\"\r\n [attr.height]=\"BASELINE_BAR_H\"\r\n />\r\n }\r\n @if (vm.editable) {\r\n <!-- Resize handles at both edges. -->\r\n <rect\r\n class=\"fly-gantt__handle\"\r\n [attr.x]=\"vm.x\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"HANDLE_W\"\r\n [attr.height]=\"barHeight()\"\r\n [attr.aria-label]=\"'gantt.aria.resize_start' | translate\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'resize-start')\"\r\n />\r\n <rect\r\n class=\"fly-gantt__handle\"\r\n [attr.x]=\"vm.x + vm.width - HANDLE_W\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"HANDLE_W\"\r\n [attr.height]=\"barHeight()\"\r\n [attr.aria-label]=\"'gantt.aria.resize_end' | translate\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'resize-end')\"\r\n />\r\n }\r\n @if (!readonly() && !vm.flat.row.readonly) {\r\n <!-- One connector per END. Dragging from the start edge yields an SS/SF link\r\n and from the finish edge an FS/FF one \u2014 the drop edge picks which. -->\r\n <circle\r\n class=\"fly-gantt__connector\"\r\n [attr.cx]=\"vm.startX\"\r\n [attr.cy]=\"vm.midY\"\r\n r=\"4\"\r\n [attr.aria-label]=\"'gantt.aria.link_handle_start' | translate: { label: vm.flat.row.label }\"\r\n (pointerdown)=\"onLinkPointerDown($event, vm, 'start')\"\r\n />\r\n <circle\r\n class=\"fly-gantt__connector\"\r\n [attr.cx]=\"vm.endX\"\r\n [attr.cy]=\"vm.midY\"\r\n r=\"4\"\r\n [attr.aria-label]=\"'gantt.aria.link_handle' | translate: { label: vm.flat.row.label }\"\r\n (pointerdown)=\"onLinkPointerDown($event, vm, 'finish')\"\r\n />\r\n }\r\n </g>\r\n }\r\n }\r\n </g>\r\n }\r\n\r\n <!-- Dependency arrows (skipped for missing / collapsed endpoints). Each carries a fat\r\n transparent twin so a 1.5px line is still clickable at pointer precision. -->\r\n @for (link of linkVms(); track link.key) {\r\n <g class=\"fly-gantt__link-group\" [class.fly-gantt__link-group--selected]=\"selectedLinkKey() === link.key\">\r\n <path\r\n class=\"fly-gantt__link-hit\"\r\n [attr.d]=\"link.path\"\r\n [attr.aria-label]=\"link.ariaLabel\"\r\n (click)=\"onLinkClick($event, link)\"\r\n />\r\n <path class=\"fly-gantt__link\" [attr.d]=\"link.path\" [attr.marker-end]=\"'url(#' + markerId + ')'\" />\r\n @if (selectedLinkKey() === link.key && !readonly()) {\r\n <g\r\n class=\"fly-gantt__link-delete\"\r\n [attr.transform]=\"'translate(' + link.badgeX + ',' + link.badgeY + ')'\"\r\n [attr.aria-label]=\"'gantt.aria.delete_link' | translate\"\r\n (click)=\"deleteLink($event, link)\"\r\n >\r\n <circle class=\"fly-gantt__link-delete-bg\" r=\"8\" />\r\n <path class=\"fly-gantt__link-delete-x\" d=\"M-3.5,-3.5 L3.5,3.5 M3.5,-3.5 L-3.5,3.5\" />\r\n </g>\r\n }\r\n </g>\r\n }\r\n\r\n <!-- In-flight link rubber band. -->\r\n @if (linkGesturePath(); as gp) {\r\n <path class=\"fly-gantt__link fly-gantt__link--ghost\" [attr.d]=\"gp\" [attr.marker-end]=\"'url(#' + markerId + ')'\" />\r\n }\r\n\r\n <!-- Today line (over the bars) \u2014 spans the WINDOW's y-extent, not the full spacer, since\r\n the svg's own viewBox only covers `[svgTop, svgTop + svgHeight]` (memo \u00A74.3 point 3). -->\r\n @if (todayX() !== null) {\r\n <line class=\"fly-gantt__today\" [attr.x1]=\"todayX()\" [attr.y1]=\"svgTop()\" [attr.x2]=\"todayX()\" [attr.y2]=\"svgTop() + svgHeight()\" />\r\n }\r\n </svg>\r\n </div>\r\n </div>\r\n\r\n @if (visibleRows().length === 0) {\r\n <p class=\"fly-gantt__empty\">{{ 'gantt.no_data' | translate }}</p>\r\n }\r\n @if (overflowCount() > 0) {\r\n <p class=\"fly-gantt__overflow\">\r\n {{ 'gantt.overflow' | translate: { shown: maxRows(), total: maxRows() + overflowCount() } }}\r\n </p>\r\n }\r\n</div>\r\n\r\n<!-- Drag candidate tooltip (HTML overlay, follows the cursor). -->\r\n@if (dragTooltip(); as tip) {\r\n <div class=\"fly-gantt__tooltip\" [style.left.px]=\"tip.x\" [style.top.px]=\"tip.y\">{{ tip.text }}</div>\r\n}\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--bg-2, var(--surface-card, #fff));--_surface-alt: var(--bg-3, var(--glass-bg-elevated, #f8fafc));--_surface-hover: var(--bg-hover, var(--surface-hover, #f1f5f9));--_border: var(--w08, var(--surface-border, #e2e8f0));--_border-strong: var(--w1, var(--surface-border, #cbd5e1));--_grid-line: var(--line-3, var(--surface-border, #eef2f7));--_text: var(--ink, var(--text-color, #0f172a));--_text-subtle: var(--ink-3, var(--text-color-secondary, #64748b));--_text-faint: var(--ink-4, var(--text-color-secondary, #94a3b8));--_accent: var(--accent);--_bar: var(--_accent);--_baseline: var(--fly-gantt-baseline, var(--ink-4, var(--text-color-secondary, #94a3b8)));--_bar-progress: color-mix(in srgb, var(--_accent) 72%, #000);--_weekend: color-mix(in srgb, var(--_text-subtle) 9%, transparent);--_today: var(--danger, #ef4444);--_link: var(--_text-subtle);--_radius: var(--r-lg, 8px);--_transition: var(--t-state, .12s ease);display:block;inline-size:100%;block-size:100%;min-block-size:200px;color:var(--_text);font-size:13px}:host(.fly-gantt--resizing){cursor:col-resize;-webkit-user-select:none;user-select:none}.fly-gantt__scroll{position:relative;inline-size:100%;block-size:100%;overflow:auto;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);box-shadow:var(--shadow-card, none);outline:none}.fly-gantt__scroll:focus-visible{outline:2px solid;outline-color:var(--_accent);outline-offset:-2px}.fly-gantt__canvas{display:grid;position:relative}.fly-gantt__corner{position:sticky;inset-block-start:0;inset-inline-start:0;z-index:4;background:var(--_surface-alt);-webkit-backdrop-filter:blur(12px) saturate(140%);backdrop-filter:blur(12px) saturate(140%);border-inline-end:1px solid var(--_border);border-block-end:1px solid var(--_border-strong)}.fly-gantt__time-header{position:sticky;inset-block-start:0;z-index:3;display:block;background:var(--_surface-alt);-webkit-backdrop-filter:blur(12px) saturate(140%);backdrop-filter:blur(12px) saturate(140%);border-block-end:1px solid var(--_border-strong)}.fly-gantt__tick-line{stroke:var(--_grid-line);stroke-width:1}.fly-gantt__tick-label{fill:var(--_text-subtle);font-size:11px;text-anchor:middle;dominant-baseline:middle}.fly-gantt__tick-label--upper{fill:var(--_text);font-weight:600}.fly-gantt__splitter{position:absolute;inset-block:0;inset-inline-end:-3px;inline-size:7px;z-index:5;cursor:col-resize;touch-action:none}.fly-gantt__splitter:after{content:\"\";position:absolute;inset-block:0;inset-inline-start:3px;inline-size:1px;background:transparent;transition:background var(--_transition)}.fly-gantt__splitter:hover:after,.fly-gantt__splitter:focus-visible:after{background:var(--_accent)}.fly-gantt__splitter:focus-visible{outline:2px solid;outline-color:var(--_accent);outline-offset:-1px}:host(.fly-gantt--resizing) .fly-gantt__splitter:after{background:var(--_accent)}.fly-gantt__labels{position:sticky;inset-inline-start:0;z-index:2;background:var(--_surface);border-inline-end:1px solid var(--_border)}.fly-gantt__label-row{position:relative;display:flex;align-items:center;border-block-end:1px solid var(--_grid-line);cursor:pointer;transition:background var(--_transition)}.fly-gantt__label-row:hover{background:var(--_surface-hover)}.fly-gantt__label-row--selected{background:var(--_surface-hover);box-shadow:inset 3px 0 0 0 var(--_accent)}.fly-gantt__label-row--tinted:before{content:\"\";position:absolute;inset:0;background:var(--fly-gantt-row-tint);pointer-events:none}.fly-gantt__label-inner{position:relative;display:flex;align-items:center;gap:4px;inline-size:100%;min-inline-size:0;padding-inline-end:8px}.fly-gantt__label-swatch{flex:0 0 auto;inline-size:8px;block-size:8px;border-radius:2px}.fly-gantt__chevron{flex:0 0 auto;inline-size:18px;block-size:18px;padding:0;border:none;background:transparent;color:var(--_text-subtle);cursor:pointer;line-height:1;transform:rotate(90deg);transition:transform var(--_transition)}.fly-gantt__chevron--collapsed{transform:rotate(0)}:host(.fly-gantt--rtl) .fly-gantt__chevron--collapsed{transform:rotate(180deg)}.fly-gantt__chevron:focus-visible{outline:2px solid;outline-color:var(--_accent);outline-offset:1px;border-radius:4px}.fly-gantt__chevron-spacer{flex:0 0 auto;inline-size:18px}.fly-gantt__label-text{min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-gantt__label-text--group{font-weight:600}.fly-gantt__label-cell-hidden{position:absolute;inline-size:1px;block-size:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.fly-gantt__body-cell{position:relative}.fly-gantt__body{position:absolute;inset-inline-start:0;display:block;touch-action:pan-x pan-y}.fly-gantt__row-band{pointer-events:none}.fly-gantt__weekend{fill:var(--_weekend)}.fly-gantt__grid-line{stroke:var(--_grid-line);stroke-width:1}.fly-gantt__row-hit{fill:transparent}.fly-gantt__row-hit--selected{fill:var(--_surface-hover)}.fly-gantt__bar{fill:var(--_bar);stroke:none}.fly-gantt__bar--editable{cursor:grab}.fly-gantt__progress{fill:var(--_bar-progress);pointer-events:none}.fly-gantt__handle{fill:transparent;cursor:ew-resize}.fly-gantt__connector{fill:var(--_surface);stroke:var(--_accent);stroke-width:1.5;cursor:crosshair;opacity:0;transition:opacity var(--_transition)}.fly-gantt__row:hover .fly-gantt__connector{opacity:1}.fly-gantt__milestone{fill:var(--_accent);stroke:var(--_surface);stroke-width:1}.fly-gantt__milestone--editable{cursor:grab}.fly-gantt__group{fill:var(--_text-subtle);stroke:var(--_text-subtle)}.fly-gantt__baseline-bar{fill:var(--_baseline);opacity:.55;pointer-events:none}.fly-gantt__baseline-milestone{fill:none;stroke:var(--_baseline);stroke-width:1.5;pointer-events:none}.fly-gantt__link{fill:none;stroke:var(--_link);stroke-width:1.5;pointer-events:none}.fly-gantt__link-hit{fill:none;stroke:transparent;stroke-width:11;pointer-events:stroke;cursor:pointer}.fly-gantt__link-group:hover .fly-gantt__link{stroke:var(--_text)}.fly-gantt__link-group--selected .fly-gantt__link{stroke:var(--_accent);stroke-width:2.5}.fly-gantt__link-delete-bg{fill:var(--_accent)}.fly-gantt__link-delete{cursor:pointer}.fly-gantt__link-delete-x{stroke:var(--_surface);stroke-width:1.75;stroke-linecap:round;fill:none;pointer-events:none}.fly-gantt__link--ghost{stroke-dasharray:4 3;opacity:.8}.fly-gantt__arrowhead{fill:var(--_link)}.fly-gantt__today{stroke:var(--_today);stroke-width:1.5;stroke-dasharray:3 3;pointer-events:none}.fly-gantt__empty,.fly-gantt__overflow{margin:0;padding:12px;color:var(--_text-faint);font-size:12px;text-align:center}.fly-gantt__tooltip{position:fixed;z-index:20;transform:translate(12px,16px);padding:4px 8px;border-radius:6px;background:var(--_text);color:var(--_surface);font-size:12px;white-space:nowrap;pointer-events:none;direction:ltr;unicode-bidi:isolate}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
13889
14323
|
}
|
|
13890
14324
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyGanttComponent, decorators: [{
|
|
13891
14325
|
type: Component,
|
|
@@ -13895,8 +14329,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
13895
14329
|
'[class.fly-gantt--readonly]': 'readonly()',
|
|
13896
14330
|
'[class.fly-gantt--resizing]': 'resizingLabels()',
|
|
13897
14331
|
'[attr.dir]': 'i18n.direction()',
|
|
13898
|
-
}, template: "<!-- Cross-scrolling canvas: a CSS grid whose header row and label column are sticky, so the\r\n time grid scrolls under a pinned header + label tree. All SVG geometry is pre-mirrored for\r\n RTL in the component (see gantt-scale mapX), so this template never branches on direction. -->\r\n<div\r\n class=\"fly-gantt__scroll\"\r\n role=\"grid\"\r\n tabindex=\"0\"\r\n [attr.aria-label]=\"'gantt.aria.grid' | translate\"\r\n [attr.aria-rowcount]=\"visibleRows().length\"\r\n (keydown)=\"onGridKeydown($event)\"\r\n>\r\n <div\r\n class=\"fly-gantt__canvas\"\r\n [style.grid-template-columns]=\"effectiveLabelWidth() + 'px ' + innerWidth() + 'px'\"\r\n [style.grid-template-rows]=\"HEADER_H + 'px ' + bodyHeight() + 'px'\"\r\n >\r\n <!-- \u2500\u2500 Corner (pinned both axes) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <div class=\"fly-gantt__corner\" [style.height.px]=\"HEADER_H\">\r\n @if (resizableLabels()) {\r\n <!-- The divider lives in the corner because the corner is the one cell pinned on BOTH\r\n axes: the grip stays reachable however far the user has scrolled. The full-height\r\n strip down the label pane below is the same gesture, presentational only. -->\r\n <div\r\n class=\"fly-gantt__splitter\"\r\n role=\"separator\"\r\n tabindex=\"0\"\r\n aria-orientation=\"vertical\"\r\n [attr.aria-label]=\"'gantt.aria.label_resize' | translate\"\r\n [attr.aria-valuenow]=\"effectiveLabelWidth()\"\r\n [attr.aria-valuemin]=\"MIN_LABEL_W\"\r\n [attr.aria-valuemax]=\"MAX_LABEL_W\"\r\n (pointerdown)=\"onLabelResizePointerDown($event)\"\r\n (keydown)=\"onLabelResizeKeydown($event)\"\r\n (dblclick)=\"resetLabelWidth()\"\r\n ></div>\r\n }\r\n </div>\r\n\r\n <!-- \u2500\u2500 Time header (pinned top) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <svg\r\n class=\"fly-gantt__time-header\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"HEADER_H\"\r\n [attr.viewBox]=\"'0 0 ' + innerWidth() + ' ' + HEADER_H\"\r\n aria-hidden=\"true\"\r\n >\r\n @for (b of weekendBands(); track b.key) {\r\n <rect class=\"fly-gantt__weekend\" [attr.x]=\"b.x\" [attr.y]=\"HEADER_UPPER_H\" [attr.width]=\"b.width\" [attr.height]=\"HEADER_LOWER_H\" />\r\n }\r\n @for (t of upperTicks(); track t.key) {\r\n <line class=\"fly-gantt__tick-line\" [attr.x1]=\"t.x\" [attr.y1]=\"0\" [attr.x2]=\"t.x\" [attr.y2]=\"HEADER_H\" />\r\n <text class=\"fly-gantt__tick-label fly-gantt__tick-label--upper\" [attr.x]=\"t.x + t.width / 2\" [attr.y]=\"HEADER_UPPER_H / 2 + 4\">{{ t.label }}</text>\r\n }\r\n @for (t of lowerTicks(); track t.key) {\r\n <line class=\"fly-gantt__tick-line\" [attr.x1]=\"t.x\" [attr.y1]=\"HEADER_UPPER_H\" [attr.x2]=\"t.x\" [attr.y2]=\"HEADER_H\" />\r\n <text class=\"fly-gantt__tick-label\" [attr.x]=\"t.x + t.width / 2\" [attr.y]=\"HEADER_UPPER_H + HEADER_LOWER_H / 2 + 4\">{{ t.label }}</text>\r\n }\r\n @if (todayX() !== null) {\r\n <line class=\"fly-gantt__today\" [attr.x1]=\"todayX()\" [attr.y1]=\"HEADER_UPPER_H\" [attr.x2]=\"todayX()\" [attr.y2]=\"HEADER_H\" />\r\n }\r\n </svg>\r\n\r\n <!-- \u2500\u2500 Label tree (pinned inline-start) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <div class=\"fly-gantt__labels\" role=\"rowgroup\">\r\n @for (vm of rowVms(); track vm.flat.row.id) {\r\n <div\r\n class=\"fly-gantt__label-row\"\r\n role=\"row\"\r\n tabindex=\"-1\"\r\n [class.fly-gantt__label-row--selected]=\"selectedId() === vm.flat.row.id\"\r\n [class.fly-gantt__label-row--tinted]=\"vm.tint !== null\"\r\n [style.height.px]=\"rowHeight()\"\r\n [style.--fly-gantt-row-tint]=\"vm.tint\"\r\n [attr.aria-selected]=\"selectedId() === vm.flat.row.id\"\r\n [attr.aria-label]=\"vm.ariaLabel\"\r\n (click)=\"onRowClick(vm.flat.row.id)\"\r\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\r\n (keydown.enter)=\"onLabelRowKeydown($event, vm.flat.row.id)\"\r\n >\r\n <span class=\"fly-gantt__label-inner\" [style.padding-inline-start.px]=\"indentFor(vm.flat.depth)\">\r\n @if (vm.flat.hasChildren) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-gantt__chevron\"\r\n [class.fly-gantt__chevron--collapsed]=\"isCollapsed(vm.flat.row.id)\"\r\n [attr.aria-label]=\"(isCollapsed(vm.flat.row.id) ? 'gantt.expand' : 'gantt.collapse') | translate\"\r\n [attr.aria-expanded]=\"!isCollapsed(vm.flat.row.id)\"\r\n (click)=\"toggleCollapse(vm.flat.row.id, $event)\"\r\n >\u25B8</button>\r\n } @else {\r\n <span class=\"fly-gantt__chevron-spacer\"></span>\r\n }\r\n @if (vm.color) {\r\n <!-- Colour chip: the row's own colour at full strength, so a milestone stays\r\n identifiable in the label pane where the band tint is deliberately faint. -->\r\n <span class=\"fly-gantt__label-swatch\" [style.background]=\"vm.color\" aria-hidden=\"true\"></span>\r\n }\r\n <span\r\n class=\"fly-gantt__label-text\"\r\n [class.fly-gantt__label-text--group]=\"vm.shape === 'group'\"\r\n [title]=\"vm.flat.row.label\"\r\n >{{ vm.flat.row.label }}</span>\r\n </span>\r\n </div>\r\n }\r\n @if (resizableLabels()) {\r\n <!-- aria-hidden so this presentational twin of the corner separator is not a second\r\n (and structurally invalid) child of the rowgroup in the accessibility tree. -->\r\n <div\r\n class=\"fly-gantt__splitter fly-gantt__splitter--rail\"\r\n aria-hidden=\"true\"\r\n (pointerdown)=\"onLabelResizePointerDown($event)\"\r\n (dblclick)=\"resetLabelWidth()\"\r\n ></div>\r\n }\r\n </div>\r\n\r\n <!-- \u2500\u2500 Time grid body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <svg\r\n #bodySvg\r\n class=\"fly-gantt__body\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"bodyHeight()\"\r\n [attr.viewBox]=\"'0 0 ' + innerWidth() + ' ' + bodyHeight()\"\r\n >\r\n <defs>\r\n <marker\r\n [attr.id]=\"markerId\"\r\n markerWidth=\"8\"\r\n markerHeight=\"8\"\r\n refX=\"6\"\r\n refY=\"4\"\r\n orient=\"auto\"\r\n markerUnits=\"userSpaceOnUse\"\r\n >\r\n <path class=\"fly-gantt__arrowhead\" d=\"M0,0 L7,4 L0,8 Z\" />\r\n </marker>\r\n </defs>\r\n\r\n <!-- Row colour bands sit at the very bottom of the stack, so the weekend shading and the\r\n gridlines below still read through them. -->\r\n @for (vm of tintedRows(); track vm.flat.row.id) {\r\n <rect\r\n class=\"fly-gantt__row-band\"\r\n [attr.x]=\"0\"\r\n [attr.y]=\"vm.y\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"rowHeight()\"\r\n [style.fill]=\"vm.tint\"\r\n />\r\n }\r\n\r\n <!-- Weekend shading + vertical gridlines (behind the bars). -->\r\n @for (b of weekendBands(); track b.key) {\r\n <rect class=\"fly-gantt__weekend\" [attr.x]=\"b.x\" [attr.y]=\"0\" [attr.width]=\"b.width\" [attr.height]=\"bodyHeight()\" />\r\n }\r\n @for (t of lowerTicks(); track t.key) {\r\n <line class=\"fly-gantt__grid-line\" [attr.x1]=\"t.x\" [attr.y1]=\"0\" [attr.x2]=\"t.x\" [attr.y2]=\"bodyHeight()\" />\r\n }\r\n\r\n <!-- Rows: a full-width hit rect (click + link drop target) then the shape. -->\r\n @for (vm of rowVms(); track vm.flat.row.id) {\r\n <g class=\"fly-gantt__row\" [attr.data-row-id]=\"vm.flat.row.id\">\r\n <rect\r\n class=\"fly-gantt__row-hit\"\r\n [class.fly-gantt__row-hit--selected]=\"selectedId() === vm.flat.row.id\"\r\n [attr.x]=\"0\"\r\n [attr.y]=\"vm.y\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"rowHeight()\"\r\n (pointerdown)=\"onRowClick(vm.flat.row.id)\"\r\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\r\n />\r\n\r\n @switch (vm.shape) {\r\n @case ('group') {\r\n <path class=\"fly-gantt__group\" [attr.d]=\"groupPath(vm)\" [style.fill]=\"vm.color\" [style.stroke]=\"vm.color\" />\r\n }\r\n @case ('milestone') {\r\n <polygon\r\n class=\"fly-gantt__milestone\"\r\n [class.fly-gantt__milestone--editable]=\"vm.editable\"\r\n [attr.points]=\"diamondPoints(vm)\"\r\n [style.fill]=\"vm.color\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'move')\"\r\n />\r\n @if (!readonly() && !vm.flat.row.readonly) {\r\n <!-- A diamond has no width, so one connector serves it; it reads as the finish\r\n end, which is what makes a milestone\u2192task drag the FS everyone expects. -->\r\n <circle\r\n class=\"fly-gantt__connector\"\r\n [attr.cx]=\"milestoneConnectorX(vm)\"\r\n [attr.cy]=\"vm.midY\"\r\n r=\"4\"\r\n [attr.aria-label]=\"'gantt.aria.link_handle' | translate: { label: vm.flat.row.label }\"\r\n (pointerdown)=\"onLinkPointerDown($event, vm, 'finish')\"\r\n />\r\n }\r\n }\r\n @case ('bar') {\r\n <g class=\"fly-gantt__bar-group\">\r\n <rect\r\n class=\"fly-gantt__bar\"\r\n [class.fly-gantt__bar--editable]=\"vm.editable\"\r\n [attr.x]=\"vm.x\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"vm.width\"\r\n [attr.height]=\"barHeight()\"\r\n rx=\"4\"\r\n [style.fill]=\"vm.color\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'move')\"\r\n />\r\n @if (vm.progressWidth > 0) {\r\n <rect\r\n class=\"fly-gantt__progress\"\r\n [attr.x]=\"vm.x\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"vm.progressWidth\"\r\n [attr.height]=\"barHeight()\"\r\n rx=\"4\"\r\n />\r\n }\r\n @if (vm.editable) {\r\n <!-- Resize handles at both edges. -->\r\n <rect\r\n class=\"fly-gantt__handle\"\r\n [attr.x]=\"vm.x\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"HANDLE_W\"\r\n [attr.height]=\"barHeight()\"\r\n [attr.aria-label]=\"'gantt.aria.resize_start' | translate\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'resize-start')\"\r\n />\r\n <rect\r\n class=\"fly-gantt__handle\"\r\n [attr.x]=\"vm.x + vm.width - HANDLE_W\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"HANDLE_W\"\r\n [attr.height]=\"barHeight()\"\r\n [attr.aria-label]=\"'gantt.aria.resize_end' | translate\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'resize-end')\"\r\n />\r\n }\r\n @if (!readonly() && !vm.flat.row.readonly) {\r\n <!-- One connector per END. Dragging from the start edge yields an SS/SF link\r\n and from the finish edge an FS/FF one \u2014 the drop edge picks which. -->\r\n <circle\r\n class=\"fly-gantt__connector\"\r\n [attr.cx]=\"vm.startX\"\r\n [attr.cy]=\"vm.midY\"\r\n r=\"4\"\r\n [attr.aria-label]=\"'gantt.aria.link_handle_start' | translate: { label: vm.flat.row.label }\"\r\n (pointerdown)=\"onLinkPointerDown($event, vm, 'start')\"\r\n />\r\n <circle\r\n class=\"fly-gantt__connector\"\r\n [attr.cx]=\"vm.endX\"\r\n [attr.cy]=\"vm.midY\"\r\n r=\"4\"\r\n [attr.aria-label]=\"'gantt.aria.link_handle' | translate: { label: vm.flat.row.label }\"\r\n (pointerdown)=\"onLinkPointerDown($event, vm, 'finish')\"\r\n />\r\n }\r\n </g>\r\n }\r\n }\r\n </g>\r\n }\r\n\r\n <!-- Dependency arrows (skipped for missing / collapsed endpoints). Each carries a fat\r\n transparent twin so a 1.5px line is still clickable at pointer precision. -->\r\n @for (link of linkVms(); track link.key) {\r\n <g class=\"fly-gantt__link-group\" [class.fly-gantt__link-group--selected]=\"selectedLinkKey() === link.key\">\r\n <path\r\n class=\"fly-gantt__link-hit\"\r\n [attr.d]=\"link.path\"\r\n [attr.aria-label]=\"link.ariaLabel\"\r\n (click)=\"onLinkClick($event, link)\"\r\n />\r\n <path class=\"fly-gantt__link\" [attr.d]=\"link.path\" [attr.marker-end]=\"'url(#' + markerId + ')'\" />\r\n @if (selectedLinkKey() === link.key && !readonly()) {\r\n <g\r\n class=\"fly-gantt__link-delete\"\r\n [attr.transform]=\"'translate(' + link.badgeX + ',' + link.badgeY + ')'\"\r\n [attr.aria-label]=\"'gantt.aria.delete_link' | translate\"\r\n (click)=\"deleteLink($event, link)\"\r\n >\r\n <circle class=\"fly-gantt__link-delete-bg\" r=\"8\" />\r\n <path class=\"fly-gantt__link-delete-x\" d=\"M-3.5,-3.5 L3.5,3.5 M3.5,-3.5 L-3.5,3.5\" />\r\n </g>\r\n }\r\n </g>\r\n }\r\n\r\n <!-- In-flight link rubber band. -->\r\n @if (linkGesturePath(); as gp) {\r\n <path class=\"fly-gantt__link fly-gantt__link--ghost\" [attr.d]=\"gp\" [attr.marker-end]=\"'url(#' + markerId + ')'\" />\r\n }\r\n\r\n <!-- Today line (over the bars). -->\r\n @if (todayX() !== null) {\r\n <line class=\"fly-gantt__today\" [attr.x1]=\"todayX()\" [attr.y1]=\"0\" [attr.x2]=\"todayX()\" [attr.y2]=\"bodyHeight()\" />\r\n }\r\n </svg>\r\n </div>\r\n\r\n @if (visibleRows().length === 0) {\r\n <p class=\"fly-gantt__empty\">{{ 'gantt.no_data' | translate }}</p>\r\n }\r\n @if (overflowCount() > 0) {\r\n <p class=\"fly-gantt__overflow\">\r\n {{ 'gantt.overflow' | translate: { shown: maxRows(), total: maxRows() + overflowCount() } }}\r\n </p>\r\n }\r\n</div>\r\n\r\n<!-- Drag candidate tooltip (HTML overlay, follows the cursor). -->\r\n@if (dragTooltip(); as tip) {\r\n <div class=\"fly-gantt__tooltip\" [style.left.px]=\"tip.x\" [style.top.px]=\"tip.y\">{{ tip.text }}</div>\r\n}\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--bg-2, var(--surface-card, #fff));--_surface-alt: var(--bg-3, var(--glass-bg-elevated, #f8fafc));--_surface-hover: var(--bg-hover, var(--surface-hover, #f1f5f9));--_border: var(--w08, var(--surface-border, #e2e8f0));--_border-strong: var(--w1, var(--surface-border, #cbd5e1));--_grid-line: var(--line-3, var(--surface-border, #eef2f7));--_text: var(--ink, var(--text-color, #0f172a));--_text-subtle: var(--ink-3, var(--text-color-secondary, #64748b));--_text-faint: var(--ink-4, var(--text-color-secondary, #94a3b8));--_accent: var(--accent);--_bar: var(--_accent);--_bar-progress: color-mix(in srgb, var(--_accent) 72%, #000);--_weekend: color-mix(in srgb, var(--_text-subtle) 9%, transparent);--_today: var(--danger, #ef4444);--_link: var(--_text-subtle);--_radius: var(--r-lg, 8px);--_transition: var(--t-state, .12s ease);display:block;inline-size:100%;block-size:100%;min-block-size:200px;color:var(--_text);font-size:13px}:host(.fly-gantt--resizing){cursor:col-resize;-webkit-user-select:none;user-select:none}.fly-gantt__scroll{position:relative;inline-size:100%;block-size:100%;overflow:auto;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);box-shadow:var(--shadow-card, none);outline:none}.fly-gantt__scroll:focus-visible{outline:2px solid;outline-color:var(--_accent);outline-offset:-2px}.fly-gantt__canvas{display:grid;position:relative}.fly-gantt__corner{position:sticky;inset-block-start:0;inset-inline-start:0;z-index:4;background:var(--_surface-alt);-webkit-backdrop-filter:blur(12px) saturate(140%);backdrop-filter:blur(12px) saturate(140%);border-inline-end:1px solid var(--_border);border-block-end:1px solid var(--_border-strong)}.fly-gantt__time-header{position:sticky;inset-block-start:0;z-index:3;display:block;background:var(--_surface-alt);-webkit-backdrop-filter:blur(12px) saturate(140%);backdrop-filter:blur(12px) saturate(140%);border-block-end:1px solid var(--_border-strong)}.fly-gantt__tick-line{stroke:var(--_grid-line);stroke-width:1}.fly-gantt__tick-label{fill:var(--_text-subtle);font-size:11px;text-anchor:middle;dominant-baseline:middle}.fly-gantt__tick-label--upper{fill:var(--_text);font-weight:600}.fly-gantt__splitter{position:absolute;inset-block:0;inset-inline-end:-3px;inline-size:7px;z-index:5;cursor:col-resize;touch-action:none}.fly-gantt__splitter:after{content:\"\";position:absolute;inset-block:0;inset-inline-start:3px;inline-size:1px;background:transparent;transition:background var(--_transition)}.fly-gantt__splitter:hover:after,.fly-gantt__splitter:focus-visible:after{background:var(--_accent)}.fly-gantt__splitter:focus-visible{outline:2px solid;outline-color:var(--_accent);outline-offset:-1px}:host(.fly-gantt--resizing) .fly-gantt__splitter:after{background:var(--_accent)}.fly-gantt__labels{position:sticky;inset-inline-start:0;z-index:2;background:var(--_surface);border-inline-end:1px solid var(--_border)}.fly-gantt__label-row{position:relative;display:flex;align-items:center;border-block-end:1px solid var(--_grid-line);cursor:pointer;transition:background var(--_transition)}.fly-gantt__label-row:hover{background:var(--_surface-hover)}.fly-gantt__label-row--selected{background:var(--_surface-hover);box-shadow:inset 3px 0 0 0 var(--_accent)}.fly-gantt__label-row--tinted:before{content:\"\";position:absolute;inset:0;background:var(--fly-gantt-row-tint);pointer-events:none}.fly-gantt__label-inner{position:relative;display:flex;align-items:center;gap:4px;inline-size:100%;min-inline-size:0;padding-inline-end:8px}.fly-gantt__label-swatch{flex:0 0 auto;inline-size:8px;block-size:8px;border-radius:2px}.fly-gantt__chevron{flex:0 0 auto;inline-size:18px;block-size:18px;padding:0;border:none;background:transparent;color:var(--_text-subtle);cursor:pointer;line-height:1;transform:rotate(90deg);transition:transform var(--_transition)}.fly-gantt__chevron--collapsed{transform:rotate(0)}:host(.fly-gantt--rtl) .fly-gantt__chevron--collapsed{transform:rotate(180deg)}.fly-gantt__chevron:focus-visible{outline:2px solid;outline-color:var(--_accent);outline-offset:1px;border-radius:4px}.fly-gantt__chevron-spacer{flex:0 0 auto;inline-size:18px}.fly-gantt__label-text{min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-gantt__label-text--group{font-weight:600}.fly-gantt__body{display:block;touch-action:pan-x pan-y}.fly-gantt__row-band{pointer-events:none}.fly-gantt__weekend{fill:var(--_weekend)}.fly-gantt__grid-line{stroke:var(--_grid-line);stroke-width:1}.fly-gantt__row-hit{fill:transparent}.fly-gantt__row-hit--selected{fill:var(--_surface-hover)}.fly-gantt__bar{fill:var(--_bar);stroke:none}.fly-gantt__bar--editable{cursor:grab}.fly-gantt__progress{fill:var(--_bar-progress);pointer-events:none}.fly-gantt__handle{fill:transparent;cursor:ew-resize}.fly-gantt__connector{fill:var(--_surface);stroke:var(--_accent);stroke-width:1.5;cursor:crosshair;opacity:0;transition:opacity var(--_transition)}.fly-gantt__row:hover .fly-gantt__connector{opacity:1}.fly-gantt__milestone{fill:var(--_accent);stroke:var(--_surface);stroke-width:1}.fly-gantt__milestone--editable{cursor:grab}.fly-gantt__group{fill:var(--_text-subtle);stroke:var(--_text-subtle)}.fly-gantt__link{fill:none;stroke:var(--_link);stroke-width:1.5;pointer-events:none}.fly-gantt__link-hit{fill:none;stroke:transparent;stroke-width:11;pointer-events:stroke;cursor:pointer}.fly-gantt__link-group:hover .fly-gantt__link{stroke:var(--_text)}.fly-gantt__link-group--selected .fly-gantt__link{stroke:var(--_accent);stroke-width:2.5}.fly-gantt__link-delete-bg{fill:var(--_accent)}.fly-gantt__link-delete{cursor:pointer}.fly-gantt__link-delete-x{stroke:var(--_surface);stroke-width:1.75;stroke-linecap:round;fill:none;pointer-events:none}.fly-gantt__link--ghost{stroke-dasharray:4 3;opacity:.8}.fly-gantt__arrowhead{fill:var(--_link)}.fly-gantt__today{stroke:var(--_today);stroke-width:1.5;stroke-dasharray:3 3;pointer-events:none}.fly-gantt__empty,.fly-gantt__overflow{margin:0;padding:12px;color:var(--_text-faint);font-size:12px;text-align:center}.fly-gantt__tooltip{position:fixed;z-index:20;transform:translate(12px,16px);padding:4px 8px;border-radius:6px;background:var(--_text);color:var(--_surface);font-size:12px;white-space:nowrap;pointer-events:none;direction:ltr;unicode-bidi:isolate}\n"] }]
|
|
13899
|
-
}], propDecorators: { rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], dependencies: [{ type: i0.Input, args: [{ isSignal: true, alias: "dependencies", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }], showToday: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToday", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], labelWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelWidth", required: false }] }], resizableLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizableLabels", required: false }] }], rowHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowHeight", required: false }] }], maxRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxRows", required: false }] }], rowDatesChange: [{ type: i0.Output, args: ["rowDatesChange"] }], dependencyCreate: [{ type: i0.Output, args: ["dependencyCreate"] }], dependencyDelete: [{ type: i0.Output, args: ["dependencyDelete"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], rowDblClick: [{ type: i0.Output, args: ["rowDblClick"] }], labelWidthChange: [{ type: i0.Output, args: ["labelWidthChange"] }], bodyRef: [{ type: i0.ViewChild, args: ['bodySvg', { isSignal: true }] }] } });
|
|
14332
|
+
}, template: "<!-- Cross-scrolling canvas: a CSS grid whose header row and label column are sticky, so the\r\n time grid scrolls under a pinned header + label tree. All SVG geometry is pre-mirrored for\r\n RTL in the component (see gantt-scale mapX), so this template never branches on direction. -->\r\n<div\r\n #scrollEl\r\n class=\"fly-gantt__scroll\"\r\n role=\"treegrid\"\r\n tabindex=\"0\"\r\n [attr.aria-label]=\"'gantt.aria.grid' | translate\"\r\n [attr.aria-rowcount]=\"totalRowCount()\"\r\n (keydown)=\"onGridKeydown($event)\"\r\n>\r\n <div\r\n #canvasEl\r\n class=\"fly-gantt__canvas\"\r\n [style.grid-template-columns]=\"effectiveLabelWidth() + 'px ' + innerWidth() + 'px'\"\r\n [style.grid-template-rows]=\"HEADER_H + 'px ' + bodyHeight() + 'px'\"\r\n >\r\n <!-- \u2500\u2500 Corner (pinned both axes) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <div class=\"fly-gantt__corner\" [style.height.px]=\"HEADER_H\">\r\n @if (resizableLabels()) {\r\n <!-- The divider lives in the corner because the corner is the one cell pinned on BOTH\r\n axes: the grip stays reachable however far the user has scrolled. The full-height\r\n strip down the label pane below is the same gesture, presentational only. -->\r\n <div\r\n class=\"fly-gantt__splitter\"\r\n role=\"separator\"\r\n tabindex=\"0\"\r\n aria-orientation=\"vertical\"\r\n [attr.aria-label]=\"'gantt.aria.label_resize' | translate\"\r\n [attr.aria-valuenow]=\"effectiveLabelWidth()\"\r\n [attr.aria-valuemin]=\"MIN_LABEL_W\"\r\n [attr.aria-valuemax]=\"MAX_LABEL_W\"\r\n (pointerdown)=\"onLabelResizePointerDown($event)\"\r\n (keydown)=\"onLabelResizeKeydown($event)\"\r\n (dblclick)=\"resetLabelWidth()\"\r\n ></div>\r\n }\r\n </div>\r\n\r\n <!-- \u2500\u2500 Time header (pinned top) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <svg\r\n class=\"fly-gantt__time-header\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"HEADER_H\"\r\n [attr.viewBox]=\"'0 0 ' + innerWidth() + ' ' + HEADER_H\"\r\n aria-hidden=\"true\"\r\n >\r\n @for (b of weekendBands(); track b.key) {\r\n <rect class=\"fly-gantt__weekend\" [attr.x]=\"b.x\" [attr.y]=\"HEADER_UPPER_H\" [attr.width]=\"b.width\" [attr.height]=\"HEADER_LOWER_H\" />\r\n }\r\n @for (t of upperTicks(); track t.key) {\r\n <line class=\"fly-gantt__tick-line\" [attr.x1]=\"t.x\" [attr.y1]=\"0\" [attr.x2]=\"t.x\" [attr.y2]=\"HEADER_H\" />\r\n <text class=\"fly-gantt__tick-label fly-gantt__tick-label--upper\" [attr.x]=\"t.x + t.width / 2\" [attr.y]=\"HEADER_UPPER_H / 2 + 4\">{{ t.label }}</text>\r\n }\r\n @for (t of lowerTicks(); track t.key) {\r\n <line class=\"fly-gantt__tick-line\" [attr.x1]=\"t.x\" [attr.y1]=\"HEADER_UPPER_H\" [attr.x2]=\"t.x\" [attr.y2]=\"HEADER_H\" />\r\n <text class=\"fly-gantt__tick-label\" [attr.x]=\"t.x + t.width / 2\" [attr.y]=\"HEADER_UPPER_H + HEADER_LOWER_H / 2 + 4\">{{ t.label }}</text>\r\n }\r\n @if (todayX() !== null) {\r\n <line class=\"fly-gantt__today\" [attr.x1]=\"todayX()\" [attr.y1]=\"HEADER_UPPER_H\" [attr.x2]=\"todayX()\" [attr.y2]=\"HEADER_H\">\r\n <title>{{ 'gantt.today' | translate }}</title>\r\n </line>\r\n }\r\n </svg>\r\n\r\n <!-- \u2500\u2500 Label tree (pinned inline-start) \u2014 the ACCESSIBLE projection: the body SVG below is\r\n aria-hidden, so every row/level/expanded/selected fact a screen reader needs lives here. -->\r\n <div #labelsEl class=\"fly-gantt__labels\" role=\"rowgroup\">\r\n @for (vm of rowVms(); track vm.flat.row.id) {\r\n <div\r\n class=\"fly-gantt__label-row\"\r\n role=\"row\"\r\n [attr.tabindex]=\"selectedId() === vm.flat.row.id ? 0 : -1\"\r\n [attr.data-row-id]=\"vm.flat.row.id\"\r\n [class.fly-gantt__label-row--selected]=\"selectedId() === vm.flat.row.id\"\r\n [class.fly-gantt__label-row--tinted]=\"vm.tint !== null\"\r\n [style.height.px]=\"rowHeight()\"\r\n [style.--fly-gantt-row-tint]=\"vm.tint\"\r\n [attr.aria-selected]=\"selectedId() === vm.flat.row.id\"\r\n [attr.aria-rowindex]=\"vm.index + 1\"\r\n [attr.aria-level]=\"vm.flat.depth + 1\"\r\n [attr.aria-expanded]=\"vm.flat.hasChildren ? !vm.flat.collapsed : null\"\r\n [attr.aria-label]=\"vm.ariaLabel\"\r\n (click)=\"onRowClick(vm.flat.row.id)\"\r\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\r\n (keydown.enter)=\"onLabelRowKeydown($event, vm.flat.row.id)\"\r\n >\r\n <span class=\"fly-gantt__label-inner\" role=\"gridcell\" [style.padding-inline-start.px]=\"indentFor(vm.flat.depth)\">\r\n @if (vm.flat.hasChildren) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-gantt__chevron\"\r\n [class.fly-gantt__chevron--collapsed]=\"isCollapsed(vm.flat.row.id)\"\r\n [attr.aria-label]=\"(isCollapsed(vm.flat.row.id) ? 'gantt.expand' : 'gantt.collapse') | translate\"\r\n [attr.aria-expanded]=\"!isCollapsed(vm.flat.row.id)\"\r\n (click)=\"toggleCollapse(vm.flat.row.id, $event)\"\r\n >\u25B8</button>\r\n } @else {\r\n <span class=\"fly-gantt__chevron-spacer\"></span>\r\n }\r\n @if (vm.color) {\r\n <!-- Colour chip: the row's own colour at full strength, so a milestone stays\r\n identifiable in the label pane where the band tint is deliberately faint. -->\r\n <span class=\"fly-gantt__label-swatch\" [style.background]=\"vm.color\" aria-hidden=\"true\"></span>\r\n }\r\n <span\r\n class=\"fly-gantt__label-text\"\r\n [class.fly-gantt__label-text--group]=\"vm.shape === 'group'\"\r\n [title]=\"vm.flat.row.label\"\r\n >{{ vm.flat.row.label }}</span>\r\n </span>\r\n <!-- Visually-hidden second gridcell: the timeline summary a sighted user reads off the\r\n bar/diamond geometry. The row's own aria-label already announces it; this cell keeps\r\n the treegrid's row\u2192cell structure valid for a cell-navigating AT. -->\r\n <span class=\"fly-gantt__label-cell-hidden\" role=\"gridcell\">{{ vm.ariaLabel }}</span>\r\n </div>\r\n }\r\n @if (resizableLabels()) {\r\n <!-- aria-hidden so this presentational twin of the corner separator is not a second\r\n (and structurally invalid) child of the rowgroup in the accessibility tree. -->\r\n <div\r\n class=\"fly-gantt__splitter fly-gantt__splitter--rail\"\r\n aria-hidden=\"true\"\r\n (pointerdown)=\"onLabelResizePointerDown($event)\"\r\n (dblclick)=\"resetLabelWidth()\"\r\n ></div>\r\n }\r\n </div>\r\n\r\n <!-- \u2500\u2500 Time grid body \u2014 a spacer cell (full height, keeps the native scrollbar honest) with\r\n the actual `<svg>` absolutely positioned + `aria-hidden` (the label pane above is the\r\n accessible projection). The `viewBox` y-offset panning IS the vertical virtualization \u2014\r\n see `gantt-window.ts`'s docstring; nothing below this point knows windowing exists. -->\r\n <div class=\"fly-gantt__body-cell\" [style.height.px]=\"bodyHeight()\">\r\n <svg\r\n #bodySvg\r\n class=\"fly-gantt__body\"\r\n aria-hidden=\"true\"\r\n [style.top.px]=\"svgTop()\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"svgHeight()\"\r\n [attr.viewBox]=\"'0 ' + svgTop() + ' ' + innerWidth() + ' ' + svgHeight()\"\r\n >\r\n <defs>\r\n <marker\r\n [attr.id]=\"markerId\"\r\n markerWidth=\"8\"\r\n markerHeight=\"8\"\r\n refX=\"6\"\r\n refY=\"4\"\r\n orient=\"auto\"\r\n markerUnits=\"userSpaceOnUse\"\r\n >\r\n <path class=\"fly-gantt__arrowhead\" d=\"M0,0 L7,4 L0,8 Z\" />\r\n </marker>\r\n </defs>\r\n\r\n <!-- Row colour bands sit at the very bottom of the stack, so the weekend shading and the\r\n gridlines below still read through them. -->\r\n @for (vm of tintedRows(); track vm.flat.row.id) {\r\n <rect\r\n class=\"fly-gantt__row-band\"\r\n [attr.x]=\"0\"\r\n [attr.y]=\"vm.y\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"rowHeight()\"\r\n [style.fill]=\"vm.tint\"\r\n />\r\n }\r\n\r\n <!-- Weekend shading + vertical gridlines (behind the bars) \u2014 span the WINDOW's y-extent,\r\n like the today line above; `weekendBands`/`lowerTicks` are already x-windowed too. -->\r\n @for (b of weekendBands(); track b.key) {\r\n <rect class=\"fly-gantt__weekend\" [attr.x]=\"b.x\" [attr.y]=\"svgTop()\" [attr.width]=\"b.width\" [attr.height]=\"svgHeight()\" />\r\n }\r\n @for (t of lowerTicks(); track t.key) {\r\n <line class=\"fly-gantt__grid-line\" [attr.x1]=\"t.x\" [attr.y1]=\"svgTop()\" [attr.x2]=\"t.x\" [attr.y2]=\"svgTop() + svgHeight()\" />\r\n }\r\n\r\n <!-- Rows: a full-width hit rect (click + link drop target) then the shape. -->\r\n @for (vm of rowVms(); track vm.flat.row.id) {\r\n <g class=\"fly-gantt__row\" [attr.data-row-id]=\"vm.flat.row.id\">\r\n <rect\r\n class=\"fly-gantt__row-hit\"\r\n [class.fly-gantt__row-hit--selected]=\"selectedId() === vm.flat.row.id\"\r\n [attr.x]=\"0\"\r\n [attr.y]=\"vm.y\"\r\n [attr.width]=\"innerWidth()\"\r\n [attr.height]=\"rowHeight()\"\r\n (pointerdown)=\"onRowClick(vm.flat.row.id)\"\r\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\r\n />\r\n\r\n @switch (vm.shape) {\r\n @case ('group') {\r\n <path class=\"fly-gantt__group\" [attr.d]=\"groupPath(vm)\" [style.fill]=\"vm.color\" [style.stroke]=\"vm.color\" />\r\n }\r\n @case ('milestone') {\r\n <polygon\r\n class=\"fly-gantt__milestone\"\r\n [class.fly-gantt__milestone--editable]=\"vm.editable\"\r\n [attr.points]=\"diamondPoints(vm)\"\r\n [style.fill]=\"vm.color\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'move')\"\r\n />\r\n @if (vm.baseline) {\r\n <!-- Baseline (plan-of-record) date: a small HOLLOW diamond so it reads as a\r\n comparison against the solid current one, never a duplicate marker. -->\r\n <polygon class=\"fly-gantt__baseline-milestone\" [attr.points]=\"baselineDiamondPoints(vm)\" />\r\n }\r\n @if (!readonly() && !vm.flat.row.readonly) {\r\n <!-- A diamond has no width, so one connector serves it; it reads as the finish\r\n end, which is what makes a milestone\u2192task drag the FS everyone expects. -->\r\n <circle\r\n class=\"fly-gantt__connector\"\r\n [attr.cx]=\"milestoneConnectorX(vm)\"\r\n [attr.cy]=\"vm.midY\"\r\n r=\"4\"\r\n [attr.aria-label]=\"'gantt.aria.link_handle' | translate: { label: vm.flat.row.label }\"\r\n (pointerdown)=\"onLinkPointerDown($event, vm, 'finish')\"\r\n />\r\n }\r\n }\r\n @case ('bar') {\r\n <g class=\"fly-gantt__bar-group\">\r\n <rect\r\n class=\"fly-gantt__bar\"\r\n [class.fly-gantt__bar--editable]=\"vm.editable\"\r\n [attr.x]=\"vm.x\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"vm.width\"\r\n [attr.height]=\"barHeight()\"\r\n rx=\"4\"\r\n [style.fill]=\"vm.color\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'move')\"\r\n />\r\n @if (vm.progressWidth > 0) {\r\n <rect\r\n class=\"fly-gantt__progress\"\r\n [attr.x]=\"vm.x\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"vm.progressWidth\"\r\n [attr.height]=\"barHeight()\"\r\n rx=\"4\"\r\n />\r\n }\r\n @if (vm.baseline) {\r\n <!-- Baseline (plan-of-record) underbar \u2014 a thin muted strip beneath the current\r\n bar, the generic \"baseline vs current\" comparison. -->\r\n <rect\r\n class=\"fly-gantt__baseline-bar\"\r\n [attr.x]=\"vm.baseline.x\"\r\n [attr.y]=\"baselineBarTop(vm)\"\r\n [attr.width]=\"vm.baseline.width\"\r\n [attr.height]=\"BASELINE_BAR_H\"\r\n />\r\n }\r\n @if (vm.editable) {\r\n <!-- Resize handles at both edges. -->\r\n <rect\r\n class=\"fly-gantt__handle\"\r\n [attr.x]=\"vm.x\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"HANDLE_W\"\r\n [attr.height]=\"barHeight()\"\r\n [attr.aria-label]=\"'gantt.aria.resize_start' | translate\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'resize-start')\"\r\n />\r\n <rect\r\n class=\"fly-gantt__handle\"\r\n [attr.x]=\"vm.x + vm.width - HANDLE_W\"\r\n [attr.y]=\"barTop(vm)\"\r\n [attr.width]=\"HANDLE_W\"\r\n [attr.height]=\"barHeight()\"\r\n [attr.aria-label]=\"'gantt.aria.resize_end' | translate\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'resize-end')\"\r\n />\r\n }\r\n @if (!readonly() && !vm.flat.row.readonly) {\r\n <!-- One connector per END. Dragging from the start edge yields an SS/SF link\r\n and from the finish edge an FS/FF one \u2014 the drop edge picks which. -->\r\n <circle\r\n class=\"fly-gantt__connector\"\r\n [attr.cx]=\"vm.startX\"\r\n [attr.cy]=\"vm.midY\"\r\n r=\"4\"\r\n [attr.aria-label]=\"'gantt.aria.link_handle_start' | translate: { label: vm.flat.row.label }\"\r\n (pointerdown)=\"onLinkPointerDown($event, vm, 'start')\"\r\n />\r\n <circle\r\n class=\"fly-gantt__connector\"\r\n [attr.cx]=\"vm.endX\"\r\n [attr.cy]=\"vm.midY\"\r\n r=\"4\"\r\n [attr.aria-label]=\"'gantt.aria.link_handle' | translate: { label: vm.flat.row.label }\"\r\n (pointerdown)=\"onLinkPointerDown($event, vm, 'finish')\"\r\n />\r\n }\r\n </g>\r\n }\r\n }\r\n </g>\r\n }\r\n\r\n <!-- Dependency arrows (skipped for missing / collapsed endpoints). Each carries a fat\r\n transparent twin so a 1.5px line is still clickable at pointer precision. -->\r\n @for (link of linkVms(); track link.key) {\r\n <g class=\"fly-gantt__link-group\" [class.fly-gantt__link-group--selected]=\"selectedLinkKey() === link.key\">\r\n <path\r\n class=\"fly-gantt__link-hit\"\r\n [attr.d]=\"link.path\"\r\n [attr.aria-label]=\"link.ariaLabel\"\r\n (click)=\"onLinkClick($event, link)\"\r\n />\r\n <path class=\"fly-gantt__link\" [attr.d]=\"link.path\" [attr.marker-end]=\"'url(#' + markerId + ')'\" />\r\n @if (selectedLinkKey() === link.key && !readonly()) {\r\n <g\r\n class=\"fly-gantt__link-delete\"\r\n [attr.transform]=\"'translate(' + link.badgeX + ',' + link.badgeY + ')'\"\r\n [attr.aria-label]=\"'gantt.aria.delete_link' | translate\"\r\n (click)=\"deleteLink($event, link)\"\r\n >\r\n <circle class=\"fly-gantt__link-delete-bg\" r=\"8\" />\r\n <path class=\"fly-gantt__link-delete-x\" d=\"M-3.5,-3.5 L3.5,3.5 M3.5,-3.5 L-3.5,3.5\" />\r\n </g>\r\n }\r\n </g>\r\n }\r\n\r\n <!-- In-flight link rubber band. -->\r\n @if (linkGesturePath(); as gp) {\r\n <path class=\"fly-gantt__link fly-gantt__link--ghost\" [attr.d]=\"gp\" [attr.marker-end]=\"'url(#' + markerId + ')'\" />\r\n }\r\n\r\n <!-- Today line (over the bars) \u2014 spans the WINDOW's y-extent, not the full spacer, since\r\n the svg's own viewBox only covers `[svgTop, svgTop + svgHeight]` (memo \u00A74.3 point 3). -->\r\n @if (todayX() !== null) {\r\n <line class=\"fly-gantt__today\" [attr.x1]=\"todayX()\" [attr.y1]=\"svgTop()\" [attr.x2]=\"todayX()\" [attr.y2]=\"svgTop() + svgHeight()\" />\r\n }\r\n </svg>\r\n </div>\r\n </div>\r\n\r\n @if (visibleRows().length === 0) {\r\n <p class=\"fly-gantt__empty\">{{ 'gantt.no_data' | translate }}</p>\r\n }\r\n @if (overflowCount() > 0) {\r\n <p class=\"fly-gantt__overflow\">\r\n {{ 'gantt.overflow' | translate: { shown: maxRows(), total: maxRows() + overflowCount() } }}\r\n </p>\r\n }\r\n</div>\r\n\r\n<!-- Drag candidate tooltip (HTML overlay, follows the cursor). -->\r\n@if (dragTooltip(); as tip) {\r\n <div class=\"fly-gantt__tooltip\" [style.left.px]=\"tip.x\" [style.top.px]=\"tip.y\">{{ tip.text }}</div>\r\n}\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--bg-2, var(--surface-card, #fff));--_surface-alt: var(--bg-3, var(--glass-bg-elevated, #f8fafc));--_surface-hover: var(--bg-hover, var(--surface-hover, #f1f5f9));--_border: var(--w08, var(--surface-border, #e2e8f0));--_border-strong: var(--w1, var(--surface-border, #cbd5e1));--_grid-line: var(--line-3, var(--surface-border, #eef2f7));--_text: var(--ink, var(--text-color, #0f172a));--_text-subtle: var(--ink-3, var(--text-color-secondary, #64748b));--_text-faint: var(--ink-4, var(--text-color-secondary, #94a3b8));--_accent: var(--accent);--_bar: var(--_accent);--_baseline: var(--fly-gantt-baseline, var(--ink-4, var(--text-color-secondary, #94a3b8)));--_bar-progress: color-mix(in srgb, var(--_accent) 72%, #000);--_weekend: color-mix(in srgb, var(--_text-subtle) 9%, transparent);--_today: var(--danger, #ef4444);--_link: var(--_text-subtle);--_radius: var(--r-lg, 8px);--_transition: var(--t-state, .12s ease);display:block;inline-size:100%;block-size:100%;min-block-size:200px;color:var(--_text);font-size:13px}:host(.fly-gantt--resizing){cursor:col-resize;-webkit-user-select:none;user-select:none}.fly-gantt__scroll{position:relative;inline-size:100%;block-size:100%;overflow:auto;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);box-shadow:var(--shadow-card, none);outline:none}.fly-gantt__scroll:focus-visible{outline:2px solid;outline-color:var(--_accent);outline-offset:-2px}.fly-gantt__canvas{display:grid;position:relative}.fly-gantt__corner{position:sticky;inset-block-start:0;inset-inline-start:0;z-index:4;background:var(--_surface-alt);-webkit-backdrop-filter:blur(12px) saturate(140%);backdrop-filter:blur(12px) saturate(140%);border-inline-end:1px solid var(--_border);border-block-end:1px solid var(--_border-strong)}.fly-gantt__time-header{position:sticky;inset-block-start:0;z-index:3;display:block;background:var(--_surface-alt);-webkit-backdrop-filter:blur(12px) saturate(140%);backdrop-filter:blur(12px) saturate(140%);border-block-end:1px solid var(--_border-strong)}.fly-gantt__tick-line{stroke:var(--_grid-line);stroke-width:1}.fly-gantt__tick-label{fill:var(--_text-subtle);font-size:11px;text-anchor:middle;dominant-baseline:middle}.fly-gantt__tick-label--upper{fill:var(--_text);font-weight:600}.fly-gantt__splitter{position:absolute;inset-block:0;inset-inline-end:-3px;inline-size:7px;z-index:5;cursor:col-resize;touch-action:none}.fly-gantt__splitter:after{content:\"\";position:absolute;inset-block:0;inset-inline-start:3px;inline-size:1px;background:transparent;transition:background var(--_transition)}.fly-gantt__splitter:hover:after,.fly-gantt__splitter:focus-visible:after{background:var(--_accent)}.fly-gantt__splitter:focus-visible{outline:2px solid;outline-color:var(--_accent);outline-offset:-1px}:host(.fly-gantt--resizing) .fly-gantt__splitter:after{background:var(--_accent)}.fly-gantt__labels{position:sticky;inset-inline-start:0;z-index:2;background:var(--_surface);border-inline-end:1px solid var(--_border)}.fly-gantt__label-row{position:relative;display:flex;align-items:center;border-block-end:1px solid var(--_grid-line);cursor:pointer;transition:background var(--_transition)}.fly-gantt__label-row:hover{background:var(--_surface-hover)}.fly-gantt__label-row--selected{background:var(--_surface-hover);box-shadow:inset 3px 0 0 0 var(--_accent)}.fly-gantt__label-row--tinted:before{content:\"\";position:absolute;inset:0;background:var(--fly-gantt-row-tint);pointer-events:none}.fly-gantt__label-inner{position:relative;display:flex;align-items:center;gap:4px;inline-size:100%;min-inline-size:0;padding-inline-end:8px}.fly-gantt__label-swatch{flex:0 0 auto;inline-size:8px;block-size:8px;border-radius:2px}.fly-gantt__chevron{flex:0 0 auto;inline-size:18px;block-size:18px;padding:0;border:none;background:transparent;color:var(--_text-subtle);cursor:pointer;line-height:1;transform:rotate(90deg);transition:transform var(--_transition)}.fly-gantt__chevron--collapsed{transform:rotate(0)}:host(.fly-gantt--rtl) .fly-gantt__chevron--collapsed{transform:rotate(180deg)}.fly-gantt__chevron:focus-visible{outline:2px solid;outline-color:var(--_accent);outline-offset:1px;border-radius:4px}.fly-gantt__chevron-spacer{flex:0 0 auto;inline-size:18px}.fly-gantt__label-text{min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-gantt__label-text--group{font-weight:600}.fly-gantt__label-cell-hidden{position:absolute;inline-size:1px;block-size:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.fly-gantt__body-cell{position:relative}.fly-gantt__body{position:absolute;inset-inline-start:0;display:block;touch-action:pan-x pan-y}.fly-gantt__row-band{pointer-events:none}.fly-gantt__weekend{fill:var(--_weekend)}.fly-gantt__grid-line{stroke:var(--_grid-line);stroke-width:1}.fly-gantt__row-hit{fill:transparent}.fly-gantt__row-hit--selected{fill:var(--_surface-hover)}.fly-gantt__bar{fill:var(--_bar);stroke:none}.fly-gantt__bar--editable{cursor:grab}.fly-gantt__progress{fill:var(--_bar-progress);pointer-events:none}.fly-gantt__handle{fill:transparent;cursor:ew-resize}.fly-gantt__connector{fill:var(--_surface);stroke:var(--_accent);stroke-width:1.5;cursor:crosshair;opacity:0;transition:opacity var(--_transition)}.fly-gantt__row:hover .fly-gantt__connector{opacity:1}.fly-gantt__milestone{fill:var(--_accent);stroke:var(--_surface);stroke-width:1}.fly-gantt__milestone--editable{cursor:grab}.fly-gantt__group{fill:var(--_text-subtle);stroke:var(--_text-subtle)}.fly-gantt__baseline-bar{fill:var(--_baseline);opacity:.55;pointer-events:none}.fly-gantt__baseline-milestone{fill:none;stroke:var(--_baseline);stroke-width:1.5;pointer-events:none}.fly-gantt__link{fill:none;stroke:var(--_link);stroke-width:1.5;pointer-events:none}.fly-gantt__link-hit{fill:none;stroke:transparent;stroke-width:11;pointer-events:stroke;cursor:pointer}.fly-gantt__link-group:hover .fly-gantt__link{stroke:var(--_text)}.fly-gantt__link-group--selected .fly-gantt__link{stroke:var(--_accent);stroke-width:2.5}.fly-gantt__link-delete-bg{fill:var(--_accent)}.fly-gantt__link-delete{cursor:pointer}.fly-gantt__link-delete-x{stroke:var(--_surface);stroke-width:1.75;stroke-linecap:round;fill:none;pointer-events:none}.fly-gantt__link--ghost{stroke-dasharray:4 3;opacity:.8}.fly-gantt__arrowhead{fill:var(--_link)}.fly-gantt__today{stroke:var(--_today);stroke-width:1.5;stroke-dasharray:3 3;pointer-events:none}.fly-gantt__empty,.fly-gantt__overflow{margin:0;padding:12px;color:var(--_text-faint);font-size:12px;text-align:center}.fly-gantt__tooltip{position:fixed;z-index:20;transform:translate(12px,16px);padding:4px 8px;border-radius:6px;background:var(--_text);color:var(--_surface);font-size:12px;white-space:nowrap;pointer-events:none;direction:ltr;unicode-bidi:isolate}\n"] }]
|
|
14333
|
+
}], ctorParameters: () => [], propDecorators: { rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], dependencies: [{ type: i0.Input, args: [{ isSignal: true, alias: "dependencies", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }], showToday: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToday", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], labelWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelWidth", required: false }] }], resizableLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizableLabels", required: false }] }], rowHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowHeight", required: false }] }], maxRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxRows", required: false }] }], collapsedIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsedIds", required: false }] }], overscanRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "overscanRows", required: false }] }], maxCanvasPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxCanvasPx", required: false }] }], virtualized: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualized", required: false }] }], showBaselines: [{ type: i0.Input, args: [{ isSignal: true, alias: "showBaselines", required: false }] }], rowDatesChange: [{ type: i0.Output, args: ["rowDatesChange"] }], dependencyCreate: [{ type: i0.Output, args: ["dependencyCreate"] }], dependencyDelete: [{ type: i0.Output, args: ["dependencyDelete"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], rowDblClick: [{ type: i0.Output, args: ["rowDblClick"] }], labelWidthChange: [{ type: i0.Output, args: ["labelWidthChange"] }], collapsedIdsChange: [{ type: i0.Output, args: ["collapsedIdsChange"] }], rowExpand: [{ type: i0.Output, args: ["rowExpand"] }], effectiveZoomChange: [{ type: i0.Output, args: ["effectiveZoomChange"] }], bodyRef: [{ type: i0.ViewChild, args: ['bodySvg', { isSignal: true }] }], scrollElRef: [{ type: i0.ViewChild, args: ['scrollEl', { isSignal: true }] }], canvasElRef: [{ type: i0.ViewChild, args: ['canvasEl', { isSignal: true }] }], labelsElRef: [{ type: i0.ViewChild, args: ['labelsEl', { isSignal: true }] }] } });
|
|
13900
14334
|
|
|
13901
14335
|
// ── Bundled ISO 3166-1 country list ──────────────────────────────────────────
|
|
13902
14336
|
// Ships with <fly-survey-form> so a `Country` question renders a searchable
|
|
@@ -17762,6 +18196,21 @@ const ORIGINAL_SAME_CONVENTION = {
|
|
|
17762
18196
|
viewTrace: '<path d="M3 12h4l2-7 4 14 2-7h6"/>',
|
|
17763
18197
|
/** Signal-detail "Archive". */
|
|
17764
18198
|
archive: '<rect x="3" y="4" width="18" height="4" rx="1"/><path d="M5 8v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8"/><path d="M10 13h4"/>',
|
|
18199
|
+
/**
|
|
18200
|
+
* "Reopen …" / "Resubmit …" — reversing a terminal state, the counterpart to
|
|
18201
|
+
* {@link ORIGINAL_SAME_CONVENTION.archive}. Same lidded box, with an up arrow where
|
|
18202
|
+
* archive draws its flat line: the Material archive/unarchive pairing.
|
|
18203
|
+
*
|
|
18204
|
+
* Deliberately NOT a counter-clockwise arc, which is the other common restore mark:
|
|
18205
|
+
* it would be a near-mirror of {@link VERBATIM_FROM_UX_DROP.refresh} and the two are
|
|
18206
|
+
* indistinguishable at the 15px these render at. Pairing with `archive` also earns its
|
|
18207
|
+
* keep in context — Thoughts' idea lifecycle menu draws archive and resubmit as
|
|
18208
|
+
* adjacent rows, so the two glyphs read as one opposed pair rather than two arrows.
|
|
18209
|
+
*
|
|
18210
|
+
* The box is closed with a lid, which is what keeps it apart from
|
|
18211
|
+
* {@link VERBATIM_FROM_UX_DROP.export}: that arrow rises out of an open-topped TRAY.
|
|
18212
|
+
*/
|
|
18213
|
+
restore: '<rect x="3" y="4" width="18" height="4" rx="1"/><path d="M5 8v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8"/><path d="M12 17.5v-5.5M9.5 14.5l2.5-2.5 2.5 2.5"/>',
|
|
17765
18214
|
/** Task-list "Sort by" trigger. */
|
|
17766
18215
|
sort: '<path d="M7 4v16"/><path d="M3 8l4-4 4 4"/><path d="M17 20V4"/><path d="M13 16l4 4 4-4"/>',
|
|
17767
18216
|
/** Task-detail "Add audience". */
|