@flyos/design-system 3.7.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.7.0';
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
- const resolved = resolveRowDates(rows);
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
- const hasChildren = (childrenByParent.get(row.id)?.length ?? 0) > 0;
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
- ticks.push({
13040
- x: dateToX(from, domain.start, pxPerDay),
13041
- width: tickWidth(from, to, pxPerDay),
13042
- label,
13043
- key: toIsoDate(from),
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
- ticks.push({ x, width: tickWidth(from, to, pxPerDay), label, key });
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 === 0 || dow === 6) {
13103
- bands.push({
13104
- x: dateToX(d, domain.start, pxPerDay),
13105
- width: pxPerDay,
13106
- label: '',
13107
- key: `we-${toIsoDate(d)}`,
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 limit:** renders up to `maxRows` (default 500) visible rows; beyond that the list is
13399
- * capped and a footer notes the overflow. Heavy windowing/virtualization is intentionally
13400
- * deferred see the component skill.
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 rendered rows; beyond it the list is truncated (see class doc). */
13424
- maxRows = input(500, ...(ngDevMode ? [{ debugName: "maxRows" }] : /* istanbul ignore next */ []));
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
- _collapsed = signal(new Set(), ...(ngDevMode ? [{ debugName: "_collapsed" }] : /* istanbul ignore next */ []));
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
- pxPerDay = computed(() => ZOOM_PX_PER_DAY[this.zoom()], ...(ngDevMode ? [{ debugName: "pxPerDay" }] : /* istanbul ignore next */ []));
13460
- domain = computed(() => computeDomain(this.rows(), this.zoom()), ...(ngDevMode ? [{ debugName: "domain" }] : /* istanbul ignore next */ []));
13461
- innerWidth = computed(() => Math.max(1, dateToX(this.domain().end, this.domain().start, this.pxPerDay())), ...(ngDevMode ? [{ debugName: "innerWidth" }] : /* istanbul ignore next */ []));
13462
- /** Label-pane width actually rendered — the user's dragged width, else the input. */
13463
- /** Full visible list after tree flatten/collapse. */
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
- /** Capped list actually rendered. */
13466
- visibleRows = computed(() => this._flat().slice(0, this.maxRows()), ...(ngDevMode ? [{ debugName: "visibleRows" }] : /* istanbul ignore next */ []));
13467
- overflowCount = computed(() => Math.max(0, this._flat().length - this.maxRows()), ...(ngDevMode ? [{ debugName: "overflowCount" }] : /* istanbul ignore next */ []));
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
- weekendBands = computed(() => this._mapTicks(weekendBands(this.domain(), this.zoom(), this.pxPerDay())), ...(ngDevMode ? [{ debugName: "weekendBands" }] : /* istanbul ignore next */ []));
13471
- lowerTicks = computed(() => this._mapTicks(lowerTicks(this.domain(), this.zoom(), this.pxPerDay())), ...(ngDevMode ? [{ debugName: "lowerTicks" }] : /* istanbul ignore next */ []));
13472
- upperTicks = computed(() => this._mapTicks(upperTicks(this.domain(), this.zoom(), this.pxPerDay())), ...(ngDevMode ? [{ debugName: "upperTicks" }] : /* istanbul ignore next */ []));
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 dom = this.domain();
13486
- const ppd = this.pxPerDay();
13487
- const w = this.innerWidth();
13488
- const rtl = this.rtl();
13489
- const rh = this.rowHeight();
13490
- const resolved = this._resolvedDates();
13491
- const preview = this._dragPreview();
13492
- const globalReadonly = this.readonly();
13493
- return this.visibleRows().map((flat, index) => {
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 between currently-rendered rows (missing/collapsed endpoints skipped). */
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 vmById = new Map();
13553
- for (const vm of this.rowVms())
13554
- if (vm.shape !== 'empty')
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 from = vmById.get(link.fromId);
13561
- const to = vmById.get(link.toId);
13562
- if (!from || !to)
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 next = new Set(this._collapsed());
13653
- if (next.has(id))
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._collapsed.set(next);
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