@flyos/design-system 3.14.0 → 3.16.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.
@@ -26,7 +26,7 @@ import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
26
26
  * `providedIn: 'root'` singletons then mean:
27
27
  * - cross-app deep links break: `FlyRemoteRouter` writes the canonical
28
28
  * `?app=&route=` URL on the remote's own instance, so a reload never reaches
29
- * the shell's launch pipeline (see skills/cross-app-deep-linking.md);
29
+ * the shell's launch pipeline (see skills/platform-deep-linking.md);
30
30
  * - the `/lookup` + slash-command registries diverge, so entities registered
31
31
  * in the remote never reach the shell's agent input.
32
32
  *
@@ -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.14.0';
50
+ const FLY_DS_VERSION = '3.16.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
@@ -91,7 +91,7 @@ const WINDOW_DATA = new InjectionToken('WINDOW_DATA');
91
91
  * provisioning. Remotes subscribe to deep links via the federation-safe surfaces
92
92
  * instead: `FLYOS_LAUNCH_EVENT` (window CustomEvent) plus the
93
93
  * `__FLYOS_PENDING_LAUNCHES__` registry on `globalThis`. See
94
- * <c>skills/cross-app-deep-linking.md</c>.
94
+ * <c>skills/platform-deep-linking.md</c>.
95
95
  */
96
96
  const LAUNCH_CONTEXT = new InjectionToken('LAUNCH_CONTEXT');
97
97
  /**
@@ -1517,6 +1517,7 @@ const DS_BASELINE_LOCALES = {
1517
1517
  'gantt.aria.label_resize': 'Resize the label column',
1518
1518
  'gantt.aria.link': '{{type}} dependency: {{from}} to {{to}}',
1519
1519
  'gantt.aria.delete_link': 'Delete dependency',
1520
+ 'gantt.aria.retarget_link': 'Drag to re-point this dependency',
1520
1521
  'gantt.link_type.fs': 'Finish-to-start',
1521
1522
  'gantt.link_type.ss': 'Start-to-start',
1522
1523
  'gantt.link_type.ff': 'Finish-to-finish',
@@ -1531,8 +1532,6 @@ const DS_BASELINE_LOCALES = {
1531
1532
  'gantt.period.quarter': 'Quarter',
1532
1533
  'gantt.outline.indent': 'Indent',
1533
1534
  'gantt.outline.outdent': 'Outdent',
1534
- 'gantt.outline.indentBlocked': 'No row above this one to nest it under',
1535
- 'gantt.outline.outdentBlocked': 'This row is already at the top level',
1536
1535
  // dynamic-form (fly-dynamic-form / fly-survey-form)
1537
1536
  'form.required_error': 'This field is required.',
1538
1537
  'form.yes': 'Yes',
@@ -1932,6 +1931,7 @@ const DS_BASELINE_LOCALES = {
1932
1931
  'gantt.aria.label_resize': 'تغيير عرض عمود التسميات',
1933
1932
  'gantt.aria.link': 'ارتباط {{type}}: من {{from}} إلى {{to}}',
1934
1933
  'gantt.aria.delete_link': 'حذف الارتباط',
1934
+ 'gantt.aria.retarget_link': 'اسحب لإعادة توجيه هذا الارتباط',
1935
1935
  'gantt.link_type.fs': 'من النهاية إلى البداية',
1936
1936
  'gantt.link_type.ss': 'من البداية إلى البداية',
1937
1937
  'gantt.link_type.ff': 'من النهاية إلى النهاية',
@@ -1946,8 +1946,6 @@ const DS_BASELINE_LOCALES = {
1946
1946
  'gantt.period.quarter': 'ربع سنة',
1947
1947
  'gantt.outline.indent': 'إزاحة للداخل',
1948
1948
  'gantt.outline.outdent': 'إزاحة للخارج',
1949
- 'gantt.outline.indentBlocked': 'لا يوجد صف أعلاه لتدرجه تحته',
1950
- 'gantt.outline.outdentBlocked': 'هذا الصف في المستوى الأعلى بالفعل',
1951
1949
  // dynamic-form (fly-dynamic-form / fly-survey-form)
1952
1950
  'form.required_error': 'هذا الحقل مطلوب.',
1953
1951
  'form.yes': 'نعم',
@@ -2347,6 +2345,7 @@ const DS_BASELINE_LOCALES = {
2347
2345
  'gantt.aria.label_resize': 'Redimensionner la colonne des libellés',
2348
2346
  'gantt.aria.link': 'Liaison {{type}} : de {{from}} à {{to}}',
2349
2347
  'gantt.aria.delete_link': 'Supprimer la liaison',
2348
+ 'gantt.aria.retarget_link': 'Faites glisser pour réorienter cette liaison',
2350
2349
  'gantt.link_type.fs': 'Fin à début',
2351
2350
  'gantt.link_type.ss': 'Début à début',
2352
2351
  'gantt.link_type.ff': 'Fin à fin',
@@ -2361,8 +2360,6 @@ const DS_BASELINE_LOCALES = {
2361
2360
  'gantt.period.quarter': 'Trimestre',
2362
2361
  'gantt.outline.indent': 'Abaisser',
2363
2362
  'gantt.outline.outdent': 'Hausser',
2364
- 'gantt.outline.indentBlocked': 'Aucune ligne au-dessus pour l’imbriquer',
2365
- 'gantt.outline.outdentBlocked': 'Cette ligne est déjà au niveau supérieur',
2366
2363
  // dynamic-form (fly-dynamic-form / fly-survey-form)
2367
2364
  'form.required_error': 'Ce champ est obligatoire.',
2368
2365
  'form.yes': 'Oui',
@@ -2761,6 +2758,7 @@ const DS_BASELINE_LOCALES = {
2761
2758
  'gantt.aria.label_resize': 'لیبل کالم کی چوڑائی تبدیل کریں',
2762
2759
  'gantt.aria.link': '{{type}} انحصار: {{from}} سے {{to}} تک',
2763
2760
  'gantt.aria.delete_link': 'انحصار حذف کریں',
2761
+ 'gantt.aria.retarget_link': 'اس انحصار کا رخ بدلنے کو گھسیٹیں',
2764
2762
  'gantt.link_type.fs': 'اختتام سے آغاز',
2765
2763
  'gantt.link_type.ss': 'آغاز سے آغاز',
2766
2764
  'gantt.link_type.ff': 'اختتام سے اختتام',
@@ -2775,8 +2773,6 @@ const DS_BASELINE_LOCALES = {
2775
2773
  'gantt.period.quarter': 'سہ ماہی',
2776
2774
  'gantt.outline.indent': 'اندر کریں',
2777
2775
  'gantt.outline.outdent': 'باہر کریں',
2778
- 'gantt.outline.indentBlocked': 'اس کے اوپر کوئی قطار نہیں جس کے نیچے رکھا جا سکے',
2779
- 'gantt.outline.outdentBlocked': 'یہ قطار پہلے ہی بالاترین سطح پر ہے',
2780
2776
  // dynamic-form (fly-dynamic-form / fly-survey-form)
2781
2777
  'form.required_error': 'یہ خانہ لازمی ہے۔',
2782
2778
  'form.yes': 'ہاں',
@@ -4019,7 +4015,7 @@ class FlyRemoteRouter {
4019
4015
  * `/trends/abc`) instead would leave a host-unroutable path in the bar — on
4020
4016
  * reload the shell router falls through to its `**` wildcard, redirects to
4021
4017
  * `/desktop`, and the deep link is silently lost. See
4022
- * skills/cross-app-deep-linking.md.
4018
+ * skills/platform-deep-linking.md.
4023
4019
  *
4024
4020
  * `flyRemoteUrl` is still stashed in the history state so the popstate
4025
4021
  * listener restores the exact remote URL for back/forward without re-parsing
@@ -4140,11 +4136,163 @@ function requestAppLaunch(opts) {
4140
4136
  return true;
4141
4137
  }
4142
4138
 
4139
+ /**
4140
+ * Shared cross-bundle store key + sync event for the inbound launch channel.
4141
+ *
4142
+ * **Public contract**, for the same reason {@link FLY_REMOTE_CONTEXT_STORE_KEY} is: a
4143
+ * consumer pinned to a DS older than 3.15.0 can read this slot directly (same shape,
4144
+ * keyed by appId) rather than re-deriving the peek-the-pending-registry workaround.
4145
+ * Keep these literals stable; they are an integration boundary.
4146
+ */
4147
+ const FLY_LAUNCH_CONTEXT_STORE_KEY = '__flyLaunchContext__';
4148
+ const FLY_LAUNCH_CONTEXT_EVENT = 'fly:launch-context';
4149
+ /** The single cross-bundle store, lazily created on `globalThis`. */
4150
+ function store$3() {
4151
+ const g = globalThis;
4152
+ return (g[FLY_LAUNCH_CONTEXT_STORE_KEY] ??= {});
4153
+ }
4154
+ /** Notify every federated copy of the service that the store changed. */
4155
+ function emitSync$3() {
4156
+ const g = globalThis;
4157
+ try {
4158
+ g.dispatchEvent?.(new Event(FLY_LAUNCH_CONTEXT_EVENT));
4159
+ }
4160
+ catch {
4161
+ /* non-DOM realm (SSR / unit env without Event) — `context()`/`params()` read the store directly */
4162
+ }
4163
+ }
4164
+ function toMap$1(s) {
4165
+ return new Map(Object.entries(s));
4166
+ }
4167
+ /**
4168
+ * Shell → remote launch channel: the **whole** {@link LaunchContext} of the most
4169
+ * recent launch into each app, `params` included.
4170
+ *
4171
+ * Why this exists
4172
+ * ---------------
4173
+ * {@link connectRemoteLaunch} consumes the two DI-free launch surfaces
4174
+ * (`FLYOS_LAUNCH_EVENT` + the `__FLYOS_PENDING_LAUNCHES__` registry) and applies the
4175
+ * `route` half by navigating. Until DS 3.15.0 it read `ctx.route` and **nothing else**,
4176
+ * so `ctx.params` — the payload dashboard drill-down carries (`skills/dashboard-reports.md`
4177
+ * § "Drill-down": `{ [filterParam]: clickedValue }`) — was silently dropped, and every
4178
+ * drill-down into a federated remote landed on an UNFILTERED register.
4179
+ *
4180
+ * The shell-side answer to "read the launch params" is `inject(LAUNCH_CONTEXT)`, which a
4181
+ * federated remote must not use: Native Federation can split that `InjectionToken`
4182
+ * instance across host and remote bundles, so `inject()` returns `null` (see
4183
+ * `LAUNCH_CONTEXT`'s own doc). This service is the federation-safe counterpart —
4184
+ * `connectRemoteLaunch` publishes here, and any component in the remote (however deep, in
4185
+ * whatever lazy chunk) injects this and reads the same context.
4186
+ *
4187
+ * Why `globalThis`, not just the DI singleton
4188
+ * -------------------------------------------
4189
+ * Identical rationale to {@link FlyRemoteContextService}: the shell builds the design
4190
+ * system from workspace SOURCE while remotes consume the published npm package, and NF's
4191
+ * version negotiation has silently split `providedIn: 'root'` before. State therefore
4192
+ * lives on `globalThis` (see {@link FLY_LAUNCH_CONTEXT_STORE_KEY}); {@link context} /
4193
+ * {@link params} read it directly (split-proof, synchronous) and a per-instance signal
4194
+ * mirrors it for reactive consumers, re-synced from a `globalThis` event whenever another
4195
+ * copy of the service mutates the store.
4196
+ *
4197
+ * Reading it
4198
+ * ----------
4199
+ * ```ts
4200
+ * private readonly launch = inject(FlyLaunchContextService);
4201
+ * private readonly drillDown = this.launch.contextFor('ppm');
4202
+ * private lastVersion = -1; // -1 never collides: the shell counts from 1
4203
+ *
4204
+ * constructor() {
4205
+ * effect(() => {
4206
+ * const ctx = this.drillDown();
4207
+ * if (!ctx || ctx.version === this.lastVersion) return; // version-dedupe is the consumer's job
4208
+ * this.lastVersion = ctx.version;
4209
+ * const status = ctx.params?.['status'];
4210
+ * if (typeof status === 'string') this.applyStatusFilter(status);
4211
+ * });
4212
+ * }
4213
+ * ```
4214
+ *
4215
+ * Staleness
4216
+ * ---------
4217
+ * An entry is **not** dropped once read — a lazily-mounted consumer must still find the
4218
+ * launch that opened the window, which may have arrived long before its chunk loaded (the
4219
+ * exact ordering hazard the hand-rolled "peek the pending registry before
4220
+ * `connectRemoteLaunch` drains it" workaround existed to dodge). It cannot go stale across
4221
+ * a remount for the same reason `connectRemoteLaunch`'s `lastVersion` reset is safe: the
4222
+ * shell is the single writer and `reloadWindowContent` ALWAYS re-publishes the window's
4223
+ * CURRENT context (`params: null`, bumped version) before dropping the injector, so the
4224
+ * remount's drain overwrites this slot before any consumer mounts. Consumers dedupe on
4225
+ * `version` regardless, exactly as they must for the live-event path.
4226
+ *
4227
+ * Keyed by `appId` (last launch wins) — a single live window per app, matching
4228
+ * `__FLYOS_PENDING_LAUNCHES__`'s own keying.
4229
+ */
4230
+ class FlyLaunchContextService {
4231
+ _byApp = signal(toMap$1(store$3()), ...(ngDevMode ? [{ debugName: "_byApp" }] : /* istanbul ignore next */ []));
4232
+ /** Every app's latest inbound launch context, keyed by app id. Reactive. */
4233
+ contexts = this._byApp.asReadonly();
4234
+ /**
4235
+ * `contextFor` memo — one computed per app id, so repeated calls (a component field
4236
+ * initializer re-running per instance) hand back the SAME signal instead of leaking a
4237
+ * new computed each time.
4238
+ */
4239
+ perApp = new Map();
4240
+ constructor() {
4241
+ const g = globalThis;
4242
+ g.addEventListener?.(FLY_LAUNCH_CONTEXT_EVENT, () => this._byApp.set(toMap$1(store$3())));
4243
+ }
4244
+ /**
4245
+ * Record an inbound launch for an app. Called by {@link connectRemoteLaunch} once per
4246
+ * accepted (version-deduped) launch — a remote should not normally call this itself.
4247
+ */
4248
+ publish(appId, ctx) {
4249
+ store$3()[appId] = ctx;
4250
+ this._byApp.set(toMap$1(store$3()));
4251
+ emitSync$3();
4252
+ }
4253
+ /** Latest launch context for an app as a signal — the reactive read path. */
4254
+ contextFor(appId) {
4255
+ let s = this.perApp.get(appId);
4256
+ if (!s) {
4257
+ s = computed(() => this._byApp().get(appId) ?? null);
4258
+ this.perApp.set(appId, s);
4259
+ }
4260
+ return s;
4261
+ }
4262
+ /** Latest launch context for an app, read straight off the store. `null` when none. */
4263
+ context(appId) {
4264
+ return store$3()[appId] ?? null;
4265
+ }
4266
+ /**
4267
+ * The `params` half of an app's latest launch — the drill-down payload. `null` when the
4268
+ * app has no launch context, or when the launch carried no params. The **consumer**
4269
+ * interprets the keys against its own filter contract; the platform never does.
4270
+ */
4271
+ params(appId) {
4272
+ return store$3()[appId]?.params ?? null;
4273
+ }
4274
+ /** Drop an app's launch context. No-op if absent. Mainly for teardown and tests. */
4275
+ clear(appId) {
4276
+ const s = store$3();
4277
+ if (!(appId in s))
4278
+ return;
4279
+ delete s[appId];
4280
+ this._byApp.set(toMap$1(s));
4281
+ emitSync$3();
4282
+ }
4283
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyLaunchContextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4284
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyLaunchContextService, providedIn: 'root' });
4285
+ }
4286
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyLaunchContextService, decorators: [{
4287
+ type: Injectable,
4288
+ args: [{ providedIn: 'root' }]
4289
+ }], ctorParameters: () => [] });
4290
+
4143
4291
  /**
4144
4292
  * Federation-safe deep-link RECEIVER for an Angular Native-Federation remote — the
4145
4293
  * remote-side counterpart of the shell's `connectWindowRoute`. Collapses the
4146
4294
  * drain + dedupe + listen + cleanup boilerplate every remote root used to
4147
- * hand-roll (see `skills/cross-app-deep-linking.md` § "Receiver side") into one
4295
+ * hand-roll (see `skills/platform-deep-linking.md` § "Receiver side") into one
4148
4296
  * call, so the correct contract can't be hand-rolled wrong.
4149
4297
  *
4150
4298
  * **Must be called from an injection context** (component constructor or a field
@@ -4167,12 +4315,32 @@ function requestAppLaunch(opts) {
4167
4315
  * `appId`: the {@link FLYOS_LAUNCH_EVENT} window `CustomEvent` and the
4168
4316
  * `globalThis[FlyosPendingLaunchesGlobalKey]` registry. This helper consumes both.
4169
4317
  *
4318
+ * ## The launch PARAMS
4319
+ * `navigate` receives a route string, so it structurally cannot carry `ctx.params` —
4320
+ * the payload dashboard drill-down rides on (`skills/dashboard-reports.md` §
4321
+ * "Drill-down" sends `{ [filterParam]: clickedValue }`). Before DS 3.15.0 this helper
4322
+ * read `ctx.route` and dropped the rest, so every drill-down into a federated remote
4323
+ * opened an UNFILTERED register, and consumers hand-rolled a second, non-destructive
4324
+ * peek at the same pending-launch registry — with a load-bearing construction-ordering
4325
+ * dependency on running before this helper's drain deleted the entry.
4326
+ *
4327
+ * Every accepted launch is now published to {@link FlyLaunchContextService}, keyed by
4328
+ * `appId`, so any component in the remote reads the full context — params included —
4329
+ * with no ordering constraint:
4330
+ *
4331
+ * ```ts
4332
+ * private readonly ctx = inject(FlyLaunchContextService).contextFor('ppm');
4333
+ * ```
4334
+ *
4335
+ * {@link ConnectRemoteLaunchOptions.onLaunch} is the callback form, for a root-local
4336
+ * reaction that needs no injectable.
4337
+ *
4170
4338
  * ## Two inbound paths, one dedupe
4171
4339
  * 1. **First-mount drain.** The launch event fires BEFORE this listener attaches
4172
4340
  * for the very first launch, so the shell stashes the latest context per
4173
4341
  * `appId` on the `globalThis` registry. The helper reads it then **deletes**
4174
4342
  * it — never leaving a stale entry behind (a leftover entry would re-apply on
4175
- * every re-mount; this is the failure mode `skills/cross-app-deep-linking.md`
4343
+ * every re-mount; this is the failure mode `skills/platform-deep-linking.md`
4176
4344
  * § "What NOT to do" warns about).
4177
4345
  * 2. **Live event.** Re-launches into an already-mounted remote arrive on
4178
4346
  * {@link FLYOS_LAUNCH_EVENT}; the `appId` filter is mandatory.
@@ -4193,15 +4361,24 @@ function requestAppLaunch(opts) {
4193
4361
  */
4194
4362
  function connectRemoteLaunch(opts) {
4195
4363
  const destroyRef = inject(DestroyRef);
4196
- const { appId, navigate, enabled = true } = opts;
4364
+ const launchContext = inject(FlyLaunchContextService);
4365
+ const { appId, navigate, onLaunch, enabled = true } = opts;
4197
4366
  if (!enabled || typeof window === 'undefined')
4198
4367
  return;
4199
4368
  let lastVersion = 0;
4200
4369
  const apply = (ctx) => {
4201
- if (!ctx || !ctx.route || ctx.version === lastVersion)
4370
+ if (!ctx || ctx.version === lastVersion)
4202
4371
  return;
4203
4372
  lastVersion = ctx.version;
4204
- navigate(ctx.route);
4373
+ // Publish BEFORE navigating: the target route's component may read the context in
4374
+ // its own field initializer, and `navigate` can render it synchronously.
4375
+ // Deliberately unconditional on `ctx.route` — a params-only launch still carries a
4376
+ // filter to apply, and claiming the version here is what keeps the drain and the
4377
+ // live event deduped against each other for BOTH halves.
4378
+ launchContext.publish(appId, ctx);
4379
+ onLaunch?.(ctx);
4380
+ if (ctx.route)
4381
+ navigate(ctx.route);
4205
4382
  };
4206
4383
  // 1. First-mount drain — read then DELETE (no stale entry left behind).
4207
4384
  const g = globalThis;
@@ -4211,14 +4388,14 @@ function connectRemoteLaunch(opts) {
4211
4388
  delete g[FlyosPendingLaunchesGlobalKey][appId];
4212
4389
  }
4213
4390
  // 2. Live listener — re-launches into the already-mounted remote.
4214
- const onLaunch = (e) => {
4391
+ const onLaunchEvent = (e) => {
4215
4392
  const detail = e.detail;
4216
4393
  if (detail?.appId !== appId || !detail.ctx)
4217
4394
  return;
4218
4395
  apply(detail.ctx);
4219
4396
  };
4220
- window.addEventListener(FLYOS_LAUNCH_EVENT, onLaunch);
4221
- destroyRef.onDestroy(() => window.removeEventListener(FLYOS_LAUNCH_EVENT, onLaunch));
4397
+ window.addEventListener(FLYOS_LAUNCH_EVENT, onLaunchEvent);
4398
+ destroyRef.onDestroy(() => window.removeEventListener(FLYOS_LAUNCH_EVENT, onLaunchEvent));
4222
4399
  }
4223
4400
 
4224
4401
  /**
@@ -5561,10 +5738,10 @@ class FlyRemoteRouterOutletComponent {
5561
5738
  provide: FLY_REMOTE_OUTLET_DEPTH,
5562
5739
  useFactory: () => (inject(FLY_REMOTE_OUTLET_DEPTH, { optional: true, skipSelf: true }) ?? -1) + 1,
5563
5740
  },
5564
- ], ngImport: i0, template: `
5565
- @if (rendered(); as cmp) {
5566
- <ng-container *ngComponentOutlet="cmp" />
5567
- }
5741
+ ], ngImport: i0, template: `
5742
+ @if (rendered(); as cmp) {
5743
+ <ng-container *ngComponentOutlet="cmp" />
5744
+ }
5568
5745
  `, isInline: true, dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5569
5746
  }
5570
5747
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyRemoteRouterOutletComponent, decorators: [{
@@ -5584,10 +5761,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
5584
5761
  useFactory: () => (inject(FLY_REMOTE_OUTLET_DEPTH, { optional: true, skipSelf: true }) ?? -1) + 1,
5585
5762
  },
5586
5763
  ],
5587
- template: `
5588
- @if (rendered(); as cmp) {
5589
- <ng-container *ngComponentOutlet="cmp" />
5590
- }
5764
+ template: `
5765
+ @if (rendered(); as cmp) {
5766
+ <ng-container *ngComponentOutlet="cmp" />
5767
+ }
5591
5768
  `,
5592
5769
  }]
5593
5770
  }], ctorParameters: () => [] });
@@ -10037,11 +10214,11 @@ class FlyBlockUiComponent {
10037
10214
  return k && k.length > 0 ? k : 'common.loading';
10038
10215
  }, ...(ngDevMode ? [{ debugName: "resolvedMessageKey" }] : /* istanbul ignore next */ []));
10039
10216
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyBlockUiComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10040
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyBlockUiComponent, isStandalone: true, selector: "fly-block-ui", inputs: { active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: true, transformFunction: null }, messageKey: { classPropertyName: "messageKey", publicName: "messageKey", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (active()) {\r\n <div\r\n class=\"fly-block-ui\"\r\n role=\"status\"\r\n aria-live=\"polite\"\r\n aria-busy=\"true\"\r\n [attr.aria-label]=\"resolvedMessageKey() | translate\">\r\n <div class=\"fly-block-ui__card\">\r\n <i class=\"pi pi-spin pi-spinner fly-block-ui__spinner\" aria-hidden=\"true\"></i>\r\n <span class=\"fly-block-ui__text\">{{ resolvedMessageKey() | translate }}</span>\r\n </div>\r\n </div>\r\n}\r\n", styles: [":host{display:contents}.fly-block-ui{position:absolute;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--surface-ground, #0c0c12) 58%,transparent);-webkit-backdrop-filter:blur(6px) saturate(120%);backdrop-filter:blur(6px) saturate(120%)}.fly-block-ui__card{display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 36px;border-radius:12px;background:var(--glass-bg, rgba(255, 255, 255, .14));border:1px solid var(--glass-border, rgba(255, 255, 255, .22));box-shadow:0 8px 32px #0000002e}.fly-block-ui__spinner{font-size:2rem;color:var(--primary-color)}.fly-block-ui__text{font-size:.875rem;color:var(--text-color-secondary)}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10217
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyBlockUiComponent, isStandalone: true, selector: "fly-block-ui", inputs: { active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: true, transformFunction: null }, messageKey: { classPropertyName: "messageKey", publicName: "messageKey", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (active()) {\n <div\n class=\"fly-block-ui\"\n role=\"status\"\n aria-live=\"polite\"\n aria-busy=\"true\"\n [attr.aria-label]=\"resolvedMessageKey() | translate\">\n <div class=\"fly-block-ui__card\">\n <i class=\"pi pi-spin pi-spinner fly-block-ui__spinner\" aria-hidden=\"true\"></i>\n <span class=\"fly-block-ui__text\">{{ resolvedMessageKey() | translate }}</span>\n </div>\n </div>\n}\n", styles: [":host{display:contents}.fly-block-ui{position:absolute;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--surface-ground, #0c0c12) 58%,transparent);-webkit-backdrop-filter:blur(6px) saturate(120%);backdrop-filter:blur(6px) saturate(120%)}.fly-block-ui__card{display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 36px;border-radius:12px;background:var(--glass-bg, rgba(255, 255, 255, .14));border:1px solid var(--glass-border, rgba(255, 255, 255, .22));box-shadow:0 8px 32px #0000002e}.fly-block-ui__spinner{font-size:2rem;color:var(--primary-color)}.fly-block-ui__text{font-size:.875rem;color:var(--text-color-secondary)}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10041
10218
  }
10042
10219
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyBlockUiComponent, decorators: [{
10043
10220
  type: Component,
10044
- args: [{ selector: 'fly-block-ui', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (active()) {\r\n <div\r\n class=\"fly-block-ui\"\r\n role=\"status\"\r\n aria-live=\"polite\"\r\n aria-busy=\"true\"\r\n [attr.aria-label]=\"resolvedMessageKey() | translate\">\r\n <div class=\"fly-block-ui__card\">\r\n <i class=\"pi pi-spin pi-spinner fly-block-ui__spinner\" aria-hidden=\"true\"></i>\r\n <span class=\"fly-block-ui__text\">{{ resolvedMessageKey() | translate }}</span>\r\n </div>\r\n </div>\r\n}\r\n", styles: [":host{display:contents}.fly-block-ui{position:absolute;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--surface-ground, #0c0c12) 58%,transparent);-webkit-backdrop-filter:blur(6px) saturate(120%);backdrop-filter:blur(6px) saturate(120%)}.fly-block-ui__card{display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 36px;border-radius:12px;background:var(--glass-bg, rgba(255, 255, 255, .14));border:1px solid var(--glass-border, rgba(255, 255, 255, .22));box-shadow:0 8px 32px #0000002e}.fly-block-ui__spinner{font-size:2rem;color:var(--primary-color)}.fly-block-ui__text{font-size:.875rem;color:var(--text-color-secondary)}\n"] }]
10221
+ args: [{ selector: 'fly-block-ui', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (active()) {\n <div\n class=\"fly-block-ui\"\n role=\"status\"\n aria-live=\"polite\"\n aria-busy=\"true\"\n [attr.aria-label]=\"resolvedMessageKey() | translate\">\n <div class=\"fly-block-ui__card\">\n <i class=\"pi pi-spin pi-spinner fly-block-ui__spinner\" aria-hidden=\"true\"></i>\n <span class=\"fly-block-ui__text\">{{ resolvedMessageKey() | translate }}</span>\n </div>\n </div>\n}\n", styles: [":host{display:contents}.fly-block-ui{position:absolute;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--surface-ground, #0c0c12) 58%,transparent);-webkit-backdrop-filter:blur(6px) saturate(120%);backdrop-filter:blur(6px) saturate(120%)}.fly-block-ui__card{display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 36px;border-radius:12px;background:var(--glass-bg, rgba(255, 255, 255, .14));border:1px solid var(--glass-border, rgba(255, 255, 255, .22));box-shadow:0 8px 32px #0000002e}.fly-block-ui__spinner{font-size:2rem;color:var(--primary-color)}.fly-block-ui__text{font-size:.875rem;color:var(--text-color-secondary)}\n"] }]
10045
10222
  }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: true }] }], messageKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "messageKey", required: false }] }] } });
10046
10223
 
10047
10224
  const STATE_KEY = {
@@ -11476,7 +11653,7 @@ const PANEL_MAX_HEIGHT = 242;
11476
11653
  * input still wins. It used to default to a hardcoded English literal that every consumer had to
11477
11654
  * remember to override — none did, so a searchable select read "Search…" under ar/fr/ur.
11478
11655
  * Styling composes the Nova token language (`--w*` ramp, `--accent`, `nova-glass.popover`) — see
11479
- * `skills/desktop-design-language.md` §3.2 and `.workflow/plans/ux-refresh/notes/S1.7-form-family.md`.
11656
+ * `skills/shell-design-language.md` §3.2 and `.workflow/plans/ux-refresh/notes/S1.7-form-family.md`.
11480
11657
  *
11481
11658
  * **Positioning (S1.7 — Nova):** the panel is `position: fixed`, measured from the trigger, and
11482
11659
  * **portaled to `<body>`** the same way {@link ContextMenuComponent} escapes a clipping/glass
@@ -13997,69 +14174,6 @@ class GanttRowGeometry {
13997
14174
  }
13998
14175
  }
13999
14176
 
14000
- /**
14001
- * Outline level — the pure half of the Indent / Outdent verbs.
14002
- *
14003
- * The chart owns the flattened tree, so it is the only party that can answer "which row would adopt
14004
- * this one" without every consumer re-deriving it from its own model. Nothing here writes anything:
14005
- * a resolved move is handed back as a {@link GanttRowIndentChange} for the component to emit and the
14006
- * consumer to persist, exactly like every other gesture in this component.
14007
- *
14008
- * Kept apart from {@link resolveGanttReorderDrop} on purpose even though both are outline logic:
14009
- * a DRAG is positional and may change level as a side effect, while these two verbs change level
14010
- * and nothing else. Separating them is what makes a drag predictable — an implicit level change
14011
- * that depends on pixel position is the single most-complained-about behaviour in every desktop
14012
- * planner.
14013
- */
14014
- /** Nearest row ABOVE `index` whose depth is at most `maxDepth`, or `null`. */
14015
- function nearestAbove(rows, index, maxDepth) {
14016
- for (let i = index - 1; i >= 0; i--) {
14017
- if (rows[i].depth <= maxDepth)
14018
- return rows[i];
14019
- }
14020
- return null;
14021
- }
14022
- /**
14023
- * Whether `id` can become a child of the row above it.
14024
- *
14025
- * A row can indent only when the nearest row above it sits at its OWN level — that row is the
14026
- * would-be parent. If the nearest row above is shallower, this row is already the first child of
14027
- * its parent and there is nothing to file it under: indenting would need a sibling it does not
14028
- * have. Same rule every outline editor applies, and the reason the action is dimmed rather than
14029
- * hidden — moving one row up makes it available again.
14030
- */
14031
- function canIndentGanttRow(rows, id) {
14032
- const index = rows.findIndex((r) => r.id === id);
14033
- if (index < 0)
14034
- return false;
14035
- return nearestAbove(rows, index, rows[index].depth)?.depth === rows[index].depth;
14036
- }
14037
- /** Whether `id` can be lifted out of its parent — i.e. whether it has one. */
14038
- function canOutdentGanttRow(rows, id) {
14039
- const row = rows.find((r) => r.id === id);
14040
- return !!row && row.depth > 0;
14041
- }
14042
- /** The indent move for `id`, or `null` when it has no eligible sibling above to nest under. */
14043
- function resolveGanttIndent(rows, id) {
14044
- const index = rows.findIndex((r) => r.id === id);
14045
- if (index < 0)
14046
- return null;
14047
- const parent = nearestAbove(rows, index, rows[index].depth);
14048
- if (!parent || parent.depth !== rows[index].depth)
14049
- return null;
14050
- return { id, delta: 1, parentId: parent.id };
14051
- }
14052
- /** The outdent move for `id` — up to its grandparent's level, or the root — or `null` at depth 0. */
14053
- function resolveGanttOutdent(rows, id) {
14054
- const index = rows.findIndex((r) => r.id === id);
14055
- if (index < 0 || rows[index].depth === 0)
14056
- return null;
14057
- const depth = rows[index].depth;
14058
- // Depth 1 lands at the root; anything deeper lands under whatever holds its current parent.
14059
- const grandparent = depth === 1 ? null : nearestAbove(rows, index, depth - 2);
14060
- return { id, delta: -1, parentId: grandparent?.id ?? null };
14061
- }
14062
-
14063
14177
  /**
14064
14178
  * The vertical window: which rows to actually mount, given a scroll position. Pure arithmetic —
14065
14179
  * `rowHeight` is uniform, so no measurement or `ResizeObserver` autosize strategy is needed.
@@ -14216,6 +14330,84 @@ class GanttViewport {
14216
14330
  }
14217
14331
  }
14218
14332
 
14333
+ /**
14334
+ * Which end of each row a relationship touches.
14335
+ *
14336
+ * The two letters ARE the anchors — `FS` is "from my **F**inish to your **S**tart" — so this is
14337
+ * the exact inverse of `linkTypeFor`, and stating it as a function rather than re-reading the
14338
+ * letters at each call site is what keeps the arrowhead, the re-target handle and the type
14339
+ * derivation from ever disagreeing about which end an arrow lands on.
14340
+ */
14341
+ function linkAnchorsFor(type) {
14342
+ return {
14343
+ from: type[0] === 'F' ? 'finish' : 'start',
14344
+ to: type[1] === 'S' ? 'start' : 'finish',
14345
+ };
14346
+ }
14347
+ /**
14348
+ * The dependency arrows' **presentation** half — the drawing, paired with `gantt-link-gestures.ts`
14349
+ * which owns the dragging.
14350
+ *
14351
+ * Split out of the component for the same reason as `gantt-row-vm.ts`: given the rendered rows and
14352
+ * the current scale it answers "where does each arrow go", and knows nothing about selection,
14353
+ * events or persistence. Keeping it beside `linkAnchorsFor` matters more than the line count — the
14354
+ * arrowhead position and the relationship type are two readings of the same two letters, and they
14355
+ * were previously derived in different places.
14356
+ */
14357
+ class GanttLinkVms {
14358
+ deps;
14359
+ constructor(deps) {
14360
+ this.deps = deps;
14361
+ }
14362
+ vms = computed(() => {
14363
+ const all = this.deps.rows();
14364
+ const indexById = this.deps.indexById();
14365
+ const links = resolveLinks(this.deps.dependencies(), new Set(indexById.keys()));
14366
+ const rtl = this.deps.rtl();
14367
+ const win = this.deps.window();
14368
+ const rowHeight = this.deps.rowHeight();
14369
+ const out = [];
14370
+ for (const link of links) {
14371
+ const fromIdx = indexById.get(link.fromId);
14372
+ const toIdx = indexById.get(link.toId);
14373
+ if (fromIdx === undefined || toIdx === undefined)
14374
+ continue;
14375
+ const from = this.deps.geometry.forFlat(all[fromIdx], fromIdx);
14376
+ const to = this.deps.geometry.forFlat(all[toIdx], toIdx);
14377
+ if (from.shape === 'empty' || to.shape === 'empty')
14378
+ continue;
14379
+ if (!linkCrossesWindow(from.midY, to.midY, win))
14380
+ continue;
14381
+ const routed = routeLink(from, to, link.type, rtl, rowHeight);
14382
+ const anchors = linkAnchorsFor(link.type);
14383
+ out.push({
14384
+ key: `${link.fromId}~${link.toId}~${link.type}`,
14385
+ fromId: link.fromId,
14386
+ toId: link.toId,
14387
+ type: link.type,
14388
+ path: routed.path,
14389
+ badgeX: routed.badgeX,
14390
+ badgeY: routed.badgeY,
14391
+ headX: anchorX(to, anchors.to),
14392
+ headY: to.midY,
14393
+ fromAnchor: anchors.from,
14394
+ ariaLabel: this.deps.t('gantt.aria.link', {
14395
+ type: this.deps.t(`gantt.link_type.${link.type.toLowerCase()}`),
14396
+ from: from.flat.row.label,
14397
+ to: to.flat.row.label,
14398
+ }),
14399
+ });
14400
+ }
14401
+ return out;
14402
+ }, ...(ngDevMode ? [{ debugName: "vms" }] : /* istanbul ignore next */ []));
14403
+ }
14404
+ /** A milestone has no width, so both of its ends are the same point. */
14405
+ function anchorX(row, anchor) {
14406
+ if (row.shape === 'milestone')
14407
+ return row.pointX;
14408
+ return anchor === 'finish' ? row.endX : row.startX;
14409
+ }
14410
+
14219
14411
  /**
14220
14412
  * Pure `treegrid` keymap resolution for {@link FlyGanttComponent} — the A9 companion that turns a
14221
14413
  * key into an intent (move the selection, toggle a collapse) without touching the DOM or Angular.
@@ -14304,6 +14496,189 @@ function resolveTreegridKey(key, currentIndex, flat, pageSize) {
14304
14496
  }
14305
14497
  }
14306
14498
 
14499
+ /**
14500
+ * Everything the keyboard does on the chart, in one place.
14501
+ *
14502
+ * Composed out of the component for the same reason as the three pointer gestures beside it: it
14503
+ * needs the rendered rows, a selection to move and somewhere to report a committed change, and
14504
+ * nothing else about the chart. The genuinely subtle parts — that Delete belongs to a selected
14505
+ * ARROW before it belongs to a selected row, and that a page is the visible window minus its
14506
+ * overscan rather than the window itself — are easier to see stated together than interleaved
14507
+ * with rendering.
14508
+ *
14509
+ * The treegrid half (Home/End/PageUp/PageDown/←/→) resolves to a pure intent in `gantt-a11y.ts`
14510
+ * and is only APPLIED here, so the ARIA behaviour stays testable without a component.
14511
+ */
14512
+ class GanttKeyboard {
14513
+ deps;
14514
+ constructor(deps) {
14515
+ this.deps = deps;
14516
+ }
14517
+ onKeydown(ev) {
14518
+ // Checked before the row branches: while an arrow is selected, Delete belongs to it — and
14519
+ // selecting an arrow already cleared the row selection, so the two can never both claim it.
14520
+ if (ev.key === 'Delete' || ev.key === 'Backspace') {
14521
+ const selected = this.deps.links().find((l) => l.key === this.deps.selectedLinkKey());
14522
+ if (selected) {
14523
+ ev.preventDefault();
14524
+ this.deps.deleteLink(ev, selected);
14525
+ }
14526
+ return;
14527
+ }
14528
+ const list = this.deps.rows();
14529
+ if (list.length === 0)
14530
+ return;
14531
+ // -1 when nothing is selected yet, so the first ArrowDown lands on row 0 (not row 1).
14532
+ const curIdx = list.findIndex((r) => r.row.id === this.deps.selectedId());
14533
+ if (isTreegridNavKey(ev.key)) {
14534
+ this.applyTreegrid(ev, list, curIdx);
14535
+ return;
14536
+ }
14537
+ switch (ev.key) {
14538
+ case 'ArrowDown':
14539
+ ev.preventDefault();
14540
+ this.select(list, curIdx < 0 ? 0 : Math.min(list.length - 1, curIdx + 1));
14541
+ return;
14542
+ case 'ArrowUp':
14543
+ ev.preventDefault();
14544
+ this.select(list, curIdx < 0 ? 0 : Math.max(0, curIdx - 1));
14545
+ return;
14546
+ case '+':
14547
+ case '=': // unshifted "+" key
14548
+ ev.preventDefault();
14549
+ this.nudge(1);
14550
+ return;
14551
+ case '-':
14552
+ case '_':
14553
+ ev.preventDefault();
14554
+ this.nudge(-1);
14555
+ return;
14556
+ case 'Enter': {
14557
+ ev.preventDefault();
14558
+ const id = this.deps.selectedId();
14559
+ if (id)
14560
+ this.deps.emitRowClick(id);
14561
+ return;
14562
+ }
14563
+ default:
14564
+ return;
14565
+ }
14566
+ }
14567
+ applyTreegrid(ev, list, curIdx) {
14568
+ const win = this.deps.window();
14569
+ // A page is what the user can SEE, so the overscan rows rendered off both edges come off it —
14570
+ // otherwise PageDown skips past content that was never on screen.
14571
+ const pageSize = Math.max(1, win.lastIndex - win.firstIndex + 1 - 2 * this.deps.overscanRows());
14572
+ const a11yRows = list.map((f) => ({
14573
+ id: f.row.id,
14574
+ depth: f.depth,
14575
+ hasChildren: f.hasChildren,
14576
+ collapsed: f.collapsed,
14577
+ }));
14578
+ const intent = resolveTreegridKey(ev.key, curIdx, a11yRows, pageSize);
14579
+ if (intent.kind === 'select') {
14580
+ ev.preventDefault();
14581
+ this.select(list, intent.index);
14582
+ }
14583
+ else if (intent.kind === 'toggle') {
14584
+ ev.preventDefault();
14585
+ this.deps.toggleCollapse(intent.id);
14586
+ }
14587
+ }
14588
+ select(list, index) {
14589
+ this.deps.selectedId.set(list[index].row.id);
14590
+ this.deps.scrollIndexIntoView(index);
14591
+ }
14592
+ /** Shift both dates of the selected bar by `deltaDays` and emit (respecting read-only). */
14593
+ nudge(deltaDays) {
14594
+ if (this.deps.readonly())
14595
+ return;
14596
+ const id = this.deps.selectedId();
14597
+ if (!id)
14598
+ return;
14599
+ const vm = this.deps.rowVms().find((v) => v.flat.row.id === id);
14600
+ if (!vm || !vm.editable)
14601
+ return;
14602
+ const dates = this.deps.resolvedDates().get(id);
14603
+ if (!dates?.start || !dates?.end)
14604
+ return;
14605
+ // A milestone is one instant, so both ends travel to the same day rather than keeping a span.
14606
+ if (vm.shape === 'milestone') {
14607
+ const at = addDays(dates.start, deltaDays);
14608
+ this.deps.emitRowDates({ id, start: toIsoDate(at), end: toIsoDate(at) });
14609
+ return;
14610
+ }
14611
+ this.deps.emitRowDates({
14612
+ id,
14613
+ start: toIsoDate(addDays(dates.start, deltaDays)),
14614
+ end: toIsoDate(addDays(dates.end, deltaDays)),
14615
+ });
14616
+ }
14617
+ }
14618
+
14619
+ /**
14620
+ * Outline level — the pure half of the Indent / Outdent verbs.
14621
+ *
14622
+ * The chart owns the flattened tree, so it is the only party that can answer "which row would adopt
14623
+ * this one" without every consumer re-deriving it from its own model. Nothing here writes anything:
14624
+ * a resolved move is handed back as a {@link GanttRowIndentChange} for the component to emit and the
14625
+ * consumer to persist, exactly like every other gesture in this component.
14626
+ *
14627
+ * Kept apart from {@link resolveGanttReorderDrop} on purpose even though both are outline logic:
14628
+ * a DRAG is positional and may change level as a side effect, while these two verbs change level
14629
+ * and nothing else. Separating them is what makes a drag predictable — an implicit level change
14630
+ * that depends on pixel position is the single most-complained-about behaviour in every desktop
14631
+ * planner.
14632
+ */
14633
+ /** Nearest row ABOVE `index` whose depth is at most `maxDepth`, or `null`. */
14634
+ function nearestAbove(rows, index, maxDepth) {
14635
+ for (let i = index - 1; i >= 0; i--) {
14636
+ if (rows[i].depth <= maxDepth)
14637
+ return rows[i];
14638
+ }
14639
+ return null;
14640
+ }
14641
+ /**
14642
+ * Whether `id` can become a child of the row above it.
14643
+ *
14644
+ * A row can indent only when the nearest row above it sits at its OWN level — that row is the
14645
+ * would-be parent. If the nearest row above is shallower, this row is already the first child of
14646
+ * its parent and there is nothing to file it under: indenting would need a sibling it does not
14647
+ * have. Same rule every outline editor applies, and the reason the action is dimmed rather than
14648
+ * hidden — moving one row up makes it available again.
14649
+ */
14650
+ function canIndentGanttRow(rows, id) {
14651
+ const index = rows.findIndex((r) => r.id === id);
14652
+ if (index < 0)
14653
+ return false;
14654
+ return nearestAbove(rows, index, rows[index].depth)?.depth === rows[index].depth;
14655
+ }
14656
+ /** Whether `id` can be lifted out of its parent — i.e. whether it has one. */
14657
+ function canOutdentGanttRow(rows, id) {
14658
+ const row = rows.find((r) => r.id === id);
14659
+ return !!row && row.depth > 0;
14660
+ }
14661
+ /** The indent move for `id`, or `null` when it has no eligible sibling above to nest under. */
14662
+ function resolveGanttIndent(rows, id) {
14663
+ const index = rows.findIndex((r) => r.id === id);
14664
+ if (index < 0)
14665
+ return null;
14666
+ const parent = nearestAbove(rows, index, rows[index].depth);
14667
+ if (!parent || parent.depth !== rows[index].depth)
14668
+ return null;
14669
+ return { id, delta: 1, parentId: parent.id };
14670
+ }
14671
+ /** The outdent move for `id` — up to its grandparent's level, or the root — or `null` at depth 0. */
14672
+ function resolveGanttOutdent(rows, id) {
14673
+ const index = rows.findIndex((r) => r.id === id);
14674
+ if (index < 0 || rows[index].depth === 0)
14675
+ return null;
14676
+ const depth = rows[index].depth;
14677
+ // Depth 1 lands at the root; anything deeper lands under whatever holds its current parent.
14678
+ const grandparent = depth === 1 ? null : nearestAbove(rows, index, depth - 2);
14679
+ return { id, delta: -1, parentId: grandparent?.id ?? null };
14680
+ }
14681
+
14307
14682
  /** Label-pane width bounds (px) the splitter clamps to. */
14308
14683
  const GANTT_LABEL_W_MIN = 140;
14309
14684
  const GANTT_LABEL_W_MAX = 720;
@@ -14466,6 +14841,13 @@ class GanttLinkGestures {
14466
14841
  deps;
14467
14842
  /** The in-flight drag, or `null`. */
14468
14843
  gesture = signal(null, ...(ngDevMode ? [{ debugName: "gesture" }] : /* istanbul ignore next */ []));
14844
+ /**
14845
+ * The edge an in-flight drag is REPLACING, or `null` for an ordinary draw.
14846
+ *
14847
+ * A plain field, not a signal: nothing renders from it — the rubber band looks the same either
14848
+ * way — and it is written and read within one gesture's lifetime.
14849
+ */
14850
+ replacing = null;
14469
14851
  /** The clicked link's key, or `null`. Drives the delete affordance and the Delete key. */
14470
14852
  selectedKey = signal(null, ...(ngDevMode ? [{ debugName: "selectedKey" }] : /* istanbul ignore next */ []));
14471
14853
  /** Rubber-band path for the in-flight gesture (render space), or `null` when idle. */
@@ -14492,6 +14874,27 @@ class GanttLinkGestures {
14492
14874
  * Listeners go on `document`, not the connector: the connector is an 8px dot and any real drag
14493
14875
  * leaves it immediately.
14494
14876
  */
14877
+ /**
14878
+ * Re-point an existing arrow: drag its HEAD onto a different row.
14879
+ *
14880
+ * The same gesture as {@link start} and from the same origin — the source row's own anchor,
14881
+ * which is why `GanttLinkVm.fromAnchor` exists — so the drop still derives the relationship from
14882
+ * which end it lands nearest. Re-pointing therefore also RE-DERIVES the type: dragging an `FS`
14883
+ * arrow onto the finish of another bar makes it `FF`, exactly as drawing it fresh would. That is
14884
+ * the same rule, not a second one, which is why this is a thin wrapper rather than its own
14885
+ * gesture.
14886
+ *
14887
+ * Only the destination end moves. A source-end handle would need the mirrored rule and a second
14888
+ * grab target on an already busy arrow; delete-and-redraw covers that case.
14889
+ */
14890
+ startRetarget(ev, row, anchor, replacing) {
14891
+ this.replacing = replacing;
14892
+ this.start(ev, row, anchor);
14893
+ // `start` refuses a read-only chart and a non-primary button. Leaving `replacing` set after a
14894
+ // refusal would make the NEXT ordinary draw silently replace this edge instead of adding one.
14895
+ if (!this.gesture())
14896
+ this.replacing = null;
14897
+ }
14495
14898
  start(ev, row, anchor) {
14496
14899
  if (this.deps.readonly() || row.flat.row.readonly || ev.button !== 0)
14497
14900
  return;
@@ -14517,18 +14920,30 @@ class GanttLinkGestures {
14517
14920
  document.removeEventListener('pointermove', onMove);
14518
14921
  document.removeEventListener('pointerup', onUp);
14519
14922
  const g = this.gesture();
14923
+ const replacing = this.replacing;
14520
14924
  this.gesture.set(null);
14925
+ this.replacing = null;
14521
14926
  if (!g)
14522
14927
  return;
14523
14928
  const target = this.dropTargetAt(e.clientX, e.clientY);
14524
14929
  // A row cannot depend on itself; a drop on empty space is simply abandoned.
14525
- if (target && target.id !== g.fromId) {
14526
- this.deps.createLink({
14527
- fromId: g.fromId,
14528
- toId: target.id,
14529
- type: linkTypeFor(g.anchor, target.anchor),
14530
- });
14930
+ if (!target || target.id === g.fromId)
14931
+ return;
14932
+ const next = {
14933
+ fromId: g.fromId,
14934
+ toId: target.id,
14935
+ type: linkTypeFor(g.anchor, target.anchor),
14936
+ };
14937
+ if (!replacing) {
14938
+ this.deps.createLink(next);
14939
+ return;
14531
14940
  }
14941
+ // Dropped back where it started. Reporting a replace here would have the consumer delete and
14942
+ // re-create the same edge for nothing — and on an API that is not transactional, that is a
14943
+ // window in which the arrow does not exist.
14944
+ if (replacing.toId === next.toId && replacing.type === next.type)
14945
+ return;
14946
+ this.deps.retargetLink(replacing, next);
14532
14947
  };
14533
14948
  document.addEventListener('pointermove', onMove);
14534
14949
  document.addEventListener('pointerup', onUp);
@@ -14986,7 +15401,9 @@ function resolveGanttReorderDrop(rows, movedIndex, gapIndex) {
14986
15401
  parentId = null;
14987
15402
  afterId = null;
14988
15403
  }
14989
- else if (above.hasChildren && !above.collapsed) {
15404
+ else if (above.hasChildren ? !above.collapsed : above.canAdoptChildren) {
15405
+ // Nest as the first child — either an expanded container gaining another child, or a
15406
+ // currently-childless row the consumer marked as a valid parent gaining its first one.
14990
15407
  parentId = above.id;
14991
15408
  afterId = null;
14992
15409
  }
@@ -15165,6 +15582,7 @@ const HEADER_H = HEADER_UPPER_H + HEADER_LOWER_H;
15165
15582
  /** Edge-handle hit width (px) for a bar resize. The connector that used to fight it for the same
15166
15583
  * pixels now sits outside the bar entirely — see `CONNECTOR_GAP` in `gantt-row-vm.ts`. */
15167
15584
  const HANDLE_W = 8;
15585
+ /** One dependency arrow, routed and ready for the template. */
15168
15586
  /**
15169
15587
  * **`fly-gantt`** — the design-system SVG Gantt chart.
15170
15588
  *
@@ -15475,46 +15893,6 @@ class FlyGanttComponent {
15475
15893
  }, ...(ngDevMode ? [{ debugName: "rowVms" }] : /* istanbul ignore next */ []));
15476
15894
  /** Only the rows carrying a tint, so the band pass does not emit an empty rect per row. */
15477
15895
  tintedRows = computed(() => this.rowVms().filter((vm) => vm.tint !== null), ...(ngDevMode ? [{ debugName: "tintedRows" }] : /* istanbul ignore next */ []));
15478
- /** Dependency arrows whose y-span intersects the row window — O(E), not O(N). Endpoints are
15479
- * resolved via {@link _vmForFlat} directly (not off `rowVms()`) since an edge can cross the
15480
- * window with one endpoint outside it — the memo §4.3 rule-2 fix for the pre-A9 "both
15481
- * endpoints rendered" predicate, which would erase exactly those crossing arrows. */
15482
- linkVms = computed(() => {
15483
- const all = this.visibleRows();
15484
- const indexById = this._indexById();
15485
- const links = resolveLinks(this.dependencies(), new Set(indexById.keys()));
15486
- const rtl = this.rtl();
15487
- const win = this.rowWindow();
15488
- const out = [];
15489
- for (const link of links) {
15490
- const fromIdx = indexById.get(link.fromId);
15491
- const toIdx = indexById.get(link.toId);
15492
- if (fromIdx === undefined || toIdx === undefined)
15493
- continue;
15494
- const from = this.geo.forFlat(all[fromIdx], fromIdx);
15495
- const to = this.geo.forFlat(all[toIdx], toIdx);
15496
- if (from.shape === 'empty' || to.shape === 'empty')
15497
- continue;
15498
- if (!linkCrossesWindow(from.midY, to.midY, win))
15499
- continue;
15500
- const routed = routeLink(from, to, link.type, rtl, this.effectiveRowHeight());
15501
- out.push({
15502
- key: `${link.fromId}~${link.toId}~${link.type}`,
15503
- fromId: link.fromId,
15504
- toId: link.toId,
15505
- type: link.type,
15506
- path: routed.path,
15507
- badgeX: routed.badgeX,
15508
- badgeY: routed.badgeY,
15509
- ariaLabel: this.i18n.t('gantt.aria.link', {
15510
- type: this.i18n.t(`gantt.link_type.${link.type.toLowerCase()}`),
15511
- from: from.flat.row.label,
15512
- to: to.flat.row.label,
15513
- }),
15514
- });
15515
- }
15516
- return out;
15517
- }, ...(ngDevMode ? [{ debugName: "linkVms" }] : /* istanbul ignore next */ []));
15518
15896
  /**
15519
15897
  * Render-space geometry, in {@link GanttRowGeometry} — the row view-model plus every shape
15520
15898
  * derived from it. Composed for the same reason as the gestures below it: given a row and the
@@ -15536,6 +15914,28 @@ class FlyGanttComponent {
15536
15914
  });
15537
15915
  /** Bar height for the current row height — bound directly by the template. */
15538
15916
  barHeight = this.geo.barHeight;
15917
+ /**
15918
+ * Dependency arrows, in {@link GanttLinkVms} — the drawing half of the link surface, paired with
15919
+ * the dragging half in `gantt-link-gestures.ts`. Declared after {@link geo} because it is handed
15920
+ * that instance: both halves of a link resolve through the SAME row geometry the rows draw from,
15921
+ * so an arrow can never land somewhere its own endpoint is not.
15922
+ *
15923
+ * Only arrows whose y-span intersects the row window are built — O(E), not O(N) — and the
15924
+ * predicate is CROSSES, not "both endpoints rendered": an edge with one end scrolled out of the
15925
+ * window still has to be drawn through it (memo §4.3 rule 2).
15926
+ */
15927
+ _linkVms = new GanttLinkVms({
15928
+ rows: this.visibleRows,
15929
+ indexById: this._indexById,
15930
+ dependencies: this.dependencies,
15931
+ rtl: this.rtl,
15932
+ rowHeight: this.effectiveRowHeight,
15933
+ window: this.rowWindow,
15934
+ geometry: this.geo,
15935
+ t: (key, params) => this.i18n.t(key, params),
15936
+ });
15937
+ /** The arrows the template draws. */
15938
+ linkVms = this._linkVms.vms;
15539
15939
  // ── Label-pane divider ─────────────────────────────────────────────────────────
15540
15940
  /**
15541
15941
  * The divider's own state machine, in {@link GanttLabelPane}. It is composed rather than
@@ -15651,6 +16051,38 @@ class FlyGanttComponent {
15651
16051
  ev.stopPropagation();
15652
16052
  this._links.toggleSelection(vm.key);
15653
16053
  }
16054
+ /**
16055
+ * Claim the press on a dependency arrow before the canvas can read it as the start of a pan.
16056
+ *
16057
+ * An arrow is a CONTROL sitting on the pan surface, and it selects on `click` — but the pan
16058
+ * gesture swallows the trailing click of any press that travelled more than
16059
+ * `PAN_THRESHOLD_PX`, so on a 1.5px line a perfectly ordinary click (a few pixels of hand
16060
+ * movement between press and release) selected nothing and nudged the chart sideways instead.
16061
+ * The bar drag and the connector drag were never affected because both already stop the press
16062
+ * here; the arrow was the one interactive thing on the canvas that did not.
16063
+ */
16064
+ onLinkPathPointerDown(ev) {
16065
+ if (ev.button !== 0)
16066
+ return;
16067
+ ev.stopPropagation();
16068
+ }
16069
+ /**
16070
+ * Grab the head of the selected arrow and re-point it. Resolves the arrow's SOURCE row, because
16071
+ * that is where the gesture has to originate for the drop to derive a relationship type.
16072
+ */
16073
+ onLinkHeadPointerDown(ev, vm) {
16074
+ ev.stopPropagation();
16075
+ if (this.readonly())
16076
+ return;
16077
+ const from = this.rowVms().find((r) => r.flat.row.id === vm.fromId);
16078
+ if (!from)
16079
+ return;
16080
+ this._links.startRetarget(ev, from, vm.fromAnchor, {
16081
+ fromId: vm.fromId,
16082
+ toId: vm.toId,
16083
+ type: vm.type,
16084
+ });
16085
+ }
15654
16086
  deleteLink(ev, vm) {
15655
16087
  ev.stopPropagation();
15656
16088
  ev.preventDefault();
@@ -15659,98 +16091,6 @@ class FlyGanttComponent {
15659
16091
  this._links.clearSelection();
15660
16092
  this.dependencyDelete.emit({ fromId: vm.fromId, toId: vm.toId, type: vm.type });
15661
16093
  }
15662
- // ── Keyboard ───────────────────────────────────────────────────────────────────
15663
- onGridKeydown(ev) {
15664
- // Link removal is checked before the row branches: while an arrow is selected the grid's
15665
- // Delete belongs to it, and selecting an arrow already cleared the row selection.
15666
- if (ev.key === 'Delete' || ev.key === 'Backspace') {
15667
- const selected = this.linkVms().find((l) => l.key === this.selectedLinkKey());
15668
- if (selected) {
15669
- ev.preventDefault();
15670
- this.deleteLink(ev, selected);
15671
- }
15672
- return;
15673
- }
15674
- const list = this.visibleRows();
15675
- if (list.length === 0)
15676
- return;
15677
- // -1 when nothing is selected yet, so the first ArrowDown lands on row 0 (not row 1).
15678
- const curIdx = list.findIndex((r) => r.row.id === this.selectedId());
15679
- // Treegrid nav (Home/End/PageUp/PageDown/←/→), resolved as pure intent — see gantt-a11y.ts.
15680
- if (isTreegridNavKey(ev.key)) {
15681
- const win = this.rowWindow();
15682
- const pageSize = Math.max(1, win.lastIndex - win.firstIndex + 1 - 2 * this.overscanRows());
15683
- const a11yRows = list.map((f) => ({
15684
- id: f.row.id, depth: f.depth, hasChildren: f.hasChildren, collapsed: f.collapsed,
15685
- }));
15686
- const intent = resolveTreegridKey(ev.key, curIdx, a11yRows, pageSize);
15687
- if (intent.kind === 'select') {
15688
- ev.preventDefault();
15689
- this.selectedId.set(list[intent.index].row.id);
15690
- this._viewport.scrollIndexIntoView(intent.index);
15691
- }
15692
- else if (intent.kind === 'toggle') {
15693
- ev.preventDefault();
15694
- this.toggleCollapse(intent.id);
15695
- }
15696
- return;
15697
- }
15698
- switch (ev.key) {
15699
- case 'ArrowDown': {
15700
- ev.preventDefault();
15701
- const i = curIdx < 0 ? 0 : Math.min(list.length - 1, curIdx + 1);
15702
- this.selectedId.set(list[i].row.id);
15703
- this._viewport.scrollIndexIntoView(i);
15704
- return;
15705
- }
15706
- case 'ArrowUp': {
15707
- ev.preventDefault();
15708
- const i = curIdx < 0 ? 0 : Math.max(0, curIdx - 1);
15709
- this.selectedId.set(list[i].row.id);
15710
- this._viewport.scrollIndexIntoView(i);
15711
- return;
15712
- }
15713
- case '+':
15714
- case '=': // unshifted "+" key
15715
- ev.preventDefault();
15716
- this._nudgeSelected(1);
15717
- return;
15718
- case '-':
15719
- case '_':
15720
- ev.preventDefault();
15721
- this._nudgeSelected(-1);
15722
- return;
15723
- case 'Enter':
15724
- ev.preventDefault();
15725
- if (this.selectedId())
15726
- this.rowClick.emit(this.selectedId());
15727
- return;
15728
- default:
15729
- return;
15730
- }
15731
- }
15732
- /** Shift both dates of the selected bar by `deltaDays` and emit (respecting read-only). */
15733
- _nudgeSelected(deltaDays) {
15734
- const id = this.selectedId();
15735
- if (!id)
15736
- return;
15737
- const vm = this.rowVms().find((v) => v.flat.row.id === id);
15738
- if (!vm || !vm.editable)
15739
- return;
15740
- const dates = this._resolvedDates().get(id);
15741
- if (!dates?.start || !dates?.end)
15742
- return;
15743
- if (vm.shape === 'milestone') {
15744
- const at = addDays(dates.start, deltaDays);
15745
- this.rowDatesChange.emit({ id, start: toIsoDate(at), end: toIsoDate(at) });
15746
- return;
15747
- }
15748
- this.rowDatesChange.emit({
15749
- id,
15750
- start: toIsoDate(addDays(dates.start, deltaDays)),
15751
- end: toIsoDate(addDays(dates.end, deltaDays)),
15752
- });
15753
- }
15754
16094
  // ── Pointer drag: move / resize ──────────────────────────────────────────────────
15755
16095
  /**
15756
16096
  * Moving and resizing a bar, in {@link GanttBarDrag} — the last of the three pointer gestures
@@ -15781,6 +16121,15 @@ class FlyGanttComponent {
15781
16121
  bodySvg: () => this.bodyRef()?.nativeElement ?? null,
15782
16122
  windowTopPx: () => this.svgTop(),
15783
16123
  createLink: (dependency) => this.dependencyCreate.emit(dependency),
16124
+ // A re-point is expressed to the consumer as CREATE-then-DELETE, in that order and never the
16125
+ // reverse: these are two independent writes, and if the second one fails the user is left
16126
+ // holding a duplicated edge (visible, and fixable by deleting one) rather than none at all
16127
+ // (invisible, and only noticed when the plan is wrong). No new output either — a consumer
16128
+ // that already handles both arrows-changed events handles this without knowing it exists.
16129
+ retargetLink: (previous, next) => {
16130
+ this.dependencyCreate.emit(next);
16131
+ this.dependencyDelete.emit(previous);
16132
+ },
15784
16133
  destroyRef: this.destroyRef,
15785
16134
  });
15786
16135
  /** Key (`from~to~type`) of the dependency arrow the user has selected, if any. */
@@ -15789,6 +16138,31 @@ class FlyGanttComponent {
15789
16138
  linkGesture = this._links.gesture;
15790
16139
  /** Rubber-band path for an in-flight link gesture (render space). */
15791
16140
  linkGesturePath = this._links.path;
16141
+ // ── Keyboard ─────────────────────────────────────────────────────────────────
16142
+ /**
16143
+ * The chart's keyboard map, in {@link GanttKeyboard} — composed for the same reason as the three
16144
+ * pointer gestures below it. Declared after the link surface because it is handed both halves:
16145
+ * Delete belongs to a selected ARROW before it belongs to a selected row.
16146
+ */
16147
+ _keyboard = new GanttKeyboard({
16148
+ readonly: this.readonly,
16149
+ rows: this.visibleRows,
16150
+ rowVms: this.rowVms,
16151
+ links: this.linkVms,
16152
+ selectedLinkKey: this.selectedLinkKey,
16153
+ selectedId: this.selectedId,
16154
+ resolvedDates: this._resolvedDates,
16155
+ window: this.rowWindow,
16156
+ overscanRows: this.overscanRows,
16157
+ scrollIndexIntoView: (index) => this._viewport.scrollIndexIntoView(index),
16158
+ toggleCollapse: (id) => this.toggleCollapse(id),
16159
+ deleteLink: (ev, link) => this.deleteLink(ev, link),
16160
+ emitRowDates: (change) => this.rowDatesChange.emit(change),
16161
+ emitRowClick: (id) => this.rowClick.emit(id),
16162
+ });
16163
+ onGridKeydown(ev) {
16164
+ this._keyboard.onKeydown(ev);
16165
+ }
15792
16166
  onLinkPointerDown(ev, vm, anchor) {
15793
16167
  this._links.start(ev, vm, anchor);
15794
16168
  }
@@ -15834,6 +16208,7 @@ class FlyGanttComponent {
15834
16208
  parentId: f.depth === 0 ? null : (ancestry[f.depth - 1] ?? null),
15835
16209
  hasChildren: f.hasChildren,
15836
16210
  collapsed: f.collapsed,
16211
+ canAdoptChildren: f.row.canAdoptChildren === true,
15837
16212
  });
15838
16213
  ancestry[f.depth] = f.row.id;
15839
16214
  }
@@ -15902,7 +16277,7 @@ class FlyGanttComponent {
15902
16277
  return ganttRowAriaLabel((k, p) => this.i18n.t(k, p), row, start && toIsoDate(start), end && toIsoDate(end), shape);
15903
16278
  }
15904
16279
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyGanttComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
15905
- 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 }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, reorderable: { classPropertyName: "reorderable", publicName: "reorderable", isSignal: true, isRequired: false, transformFunction: null }, searchQuery: { classPropertyName: "searchQuery", publicName: "searchQuery", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { searchQuery: "searchQueryChange", rowDatesChange: "rowDatesChange", dependencyCreate: "dependencyCreate", dependencyDelete: "dependencyDelete", rowClick: "rowClick", rowDblClick: "rowDblClick", labelWidthChange: "labelWidthChange", collapsedIdsChange: "collapsedIdsChange", rowExpand: "rowExpand", effectiveZoomChange: "effectiveZoomChange", rowReorder: "rowReorder", rowIndentChange: "rowIndentChange" }, host: { properties: { "class.fly-gantt--rtl": "rtl()", "class.fly-gantt--readonly": "readonly()", "class.fly-gantt--resizing": "resizingLabels()", "class.fly-gantt--panning": "panning()", "class.fly-gantt--reordering": "reorderDraggingId() !== null", "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 (searchEnabled()) {\r\n <!-- The label-tree filter. It lives in the CORNER because that is the one cell pinned on\r\n both axes: a field anywhere else scrolls away from the list it filters, and the corner\r\n is otherwise dead space sized exactly to the label pane. -->\r\n <fly-search-input\r\n class=\"fly-gantt__search\"\r\n size=\"sm\"\r\n [value]=\"searchQuery()\"\r\n [placeholderKey]=\"'gantt.search.placeholder'\"\r\n [ariaLabel]=\"'gantt.aria.search' | translate\"\r\n (queryChange)=\"onSearchQueryChange($event)\"\r\n />\r\n }\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 <!-- `padding-block-start` = the body `<svg>`'s own `top`, and it is what keeps the two panes\r\n on the same rows. The label tree renders only the WINDOWED slice, in normal flow, so\r\n without this the first rendered row sits at the top of a full-height cell while its bar\r\n sits `svgTop` further down \u2014 measured at 3,842px apart on a 5,000-row chart scrolled to\r\n 4,000px, i.e. the label pane and the chart describing completely different rows. Padding\r\n rather than a `top`/transform because the pane is `position: sticky` (a `top` would move\r\n the STICKY inset) and because an absolutely-positioned child \u2014 the drop indicator \u2014 is\r\n laid out against the padding box's border edge, so it keeps its absolute row-space\r\n coordinates for free. -->\r\n <div\r\n #labelsEl\r\n class=\"fly-gantt__labels\"\r\n role=\"rowgroup\"\r\n [style.padding-block-start.px]=\"svgTop()\"\r\n >\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 [class.fly-gantt__label-row--dragging]=\"reorderDraggingId() === vm.flat.row.id\"\r\n [style.height.px]=\"effectiveRowHeight()\"\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 (pointerdown)=\"onLabelRowPointerDown($event, vm.flat.row.id)\"\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]=\"geo.indentFor(vm.flat.depth)\">\r\n @if (reorderEnabled()) {\r\n <!-- Visual affordance only: the drag is armed on the WHOLE row (above) behind a 4px\r\n threshold, so a plain click still selects. aria-hidden because the row is already\r\n the accessible handle and a second interactive child inside `role=\"row\"` would be\r\n structurally invalid \u2014 the same reason the splitter's full-height twin is. -->\r\n <span class=\"fly-gantt__grip\" aria-hidden=\"true\"></span>\r\n }\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 <!-- `flyTooltip`, never the native `[title]`: the browser tip is un-themable, has no\r\n keyboard story, and is clipped by nothing but also styled by nothing. The directive\r\n body-parents its surface, so it escapes the label pane's own overflow clip. -->\r\n <span\r\n class=\"fly-gantt__label-text\"\r\n [class.fly-gantt__label-text--group]=\"vm.shape === 'group'\"\r\n [flyTooltip]=\"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 <!-- `!== null`, not `@if (\u2026; as y)`: a drop at the very TOP of the list is y = 0, which the\r\n `as` form treats as falsy and silently refuses to draw. -->\r\n @if (reorderIndicatorTop() !== null) {\r\n <!-- Where the dragged row would land. Drawn here rather than by moving the row itself: the\r\n rows are windowed, so a live re-order of the rendered list would fight the virtualizer\r\n for every frame of the drag. -->\r\n <div class=\"fly-gantt__drop-line\" aria-hidden=\"true\" [style.top.px]=\"reorderIndicatorTop()\"></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 (pointerdown)=\"onPanPointerDown($event)\"\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]=\"effectiveRowHeight()\"\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 <!-- The row's full-width lane. It carries NO click handler: pressing the empty part of a\r\n row is not \"clicking an item\", it is grabbing the canvas, so the press falls through\r\n to the body's pan gesture. The rect still exists because it is the link-drag DROP\r\n target \u2014 `dropTargetAt` hit-tests `[data-row-id]` on the group above, and a lane with\r\n no painted fill would not be found by `elementFromPoint` at all. -->\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]=\"effectiveRowHeight()\"\r\n />\r\n\r\n @switch (vm.shape) {\r\n @case ('group') {\r\n <!-- A summary row is not editable (no drag, no resize, no connectors) but it IS\r\n selectable \u2014 `skills/design-system-gantt.md` \u00A71. Now that the lane behind it\r\n pans instead of selecting, the bracket itself has to carry the press. -->\r\n <path\r\n class=\"fly-gantt__group\"\r\n [attr.d]=\"geo.groupPath(vm)\"\r\n [style.fill]=\"vm.color\"\r\n [style.stroke]=\"vm.color\"\r\n (pointerdown)=\"onShapePointerDown($event, vm.flat.row.id)\"\r\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\r\n />\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]=\"geo.diamondPoints(vm)\"\r\n [style.fill]=\"vm.color\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'move')\"\r\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\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]=\"geo.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]=\"geo.milestoneConnectorX(vm)\"\r\n [attr.cy]=\"vm.midY\"\r\n [attr.r]=\"CONNECTOR_R\"\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]=\"geo.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 (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\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]=\"geo.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]=\"geo.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]=\"geo.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]=\"geo.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]=\"geo.connectorX(vm, 'start')\"\r\n [attr.cy]=\"vm.midY\"\r\n [attr.r]=\"CONNECTOR_R\"\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]=\"geo.connectorX(vm, 'finish')\"\r\n [attr.cy]=\"vm.midY\"\r\n [attr.r]=\"CONNECTOR_R\"\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\">\r\n {{ (activeQuery() ? 'gantt.no_matches' : 'gantt.no_data') | translate: { query: activeQuery() } }}\r\n </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}:host(.fly-gantt--panning){cursor:grabbing;-webkit-user-select:none;user-select:none}:host(.fly-gantt--reordering){cursor:grabbing;-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__search{display:block;min-inline-size:0;padding:8px;padding-inline-end:12px}.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__grip{flex:0 0 auto;inline-size:8px;block-size:14px;margin-inline-start:-4px;cursor:grab;opacity:0;background-image:radial-gradient(circle at 1.5px 1.5px,var(--_text-faint) 1.2px,transparent 1.3px);background-size:4px 4px;transition:opacity var(--_transition)}.fly-gantt__label-row:hover .fly-gantt__grip,.fly-gantt__label-row--selected .fly-gantt__grip{opacity:1}.fly-gantt__label-row--dragging{opacity:.45}.fly-gantt__drop-line{position:absolute;inset-inline:0;z-index:3;block-size:2px;margin-block-start:-1px;background:var(--_accent);pointer-events:none}.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;cursor:grab}.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;pointer-events:none;transition:opacity var(--_transition)}.fly-gantt__row:hover .fly-gantt__connector{opacity:1;pointer-events:auto}.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: "directive", type: FlyTooltipDirective, selector: "[flyTooltip], [data-tooltip]", inputs: ["flyTooltip", "flyTooltipPlacement", "flyTooltipDisabled", "flyTooltipDelay"] }, { kind: "component", type: FlySearchInputComponent, selector: "fly-search-input", inputs: ["value", "placeholder", "placeholderKey", "ariaLabel", "ariaLabelKey", "debounceMs", "disabled", "collapsible", "expandTrigger", "stayExpandedWithValue", "size", "showFilterToggle", "filterPanelOpen", "open"], outputs: ["valueChange", "queryChange", "searched", "submitted", "cleared", "expandedChange", "filterToggled"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
16280
+ 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 }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, reorderable: { classPropertyName: "reorderable", publicName: "reorderable", isSignal: true, isRequired: false, transformFunction: null }, searchQuery: { classPropertyName: "searchQuery", publicName: "searchQuery", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { searchQuery: "searchQueryChange", rowDatesChange: "rowDatesChange", dependencyCreate: "dependencyCreate", dependencyDelete: "dependencyDelete", rowClick: "rowClick", rowDblClick: "rowDblClick", labelWidthChange: "labelWidthChange", collapsedIdsChange: "collapsedIdsChange", rowExpand: "rowExpand", effectiveZoomChange: "effectiveZoomChange", rowReorder: "rowReorder", rowIndentChange: "rowIndentChange" }, host: { properties: { "class.fly-gantt--rtl": "rtl()", "class.fly-gantt--readonly": "readonly()", "class.fly-gantt--resizing": "resizingLabels()", "class.fly-gantt--panning": "panning()", "class.fly-gantt--reordering": "reorderDraggingId() !== null", "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 (searchEnabled()) {\r\n <!-- The label-tree filter. It lives in the CORNER because that is the one cell pinned on\r\n both axes: a field anywhere else scrolls away from the list it filters, and the corner\r\n is otherwise dead space sized exactly to the label pane. -->\r\n <fly-search-input\r\n class=\"fly-gantt__search\"\r\n size=\"sm\"\r\n [value]=\"searchQuery()\"\r\n [placeholderKey]=\"'gantt.search.placeholder'\"\r\n [ariaLabel]=\"'gantt.aria.search' | translate\"\r\n (queryChange)=\"onSearchQueryChange($event)\"\r\n />\r\n }\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 <!-- `padding-block-start` = the body `<svg>`'s own `top`, and it is what keeps the two panes\r\n on the same rows. The label tree renders only the WINDOWED slice, in normal flow, so\r\n without this the first rendered row sits at the top of a full-height cell while its bar\r\n sits `svgTop` further down \u2014 measured at 3,842px apart on a 5,000-row chart scrolled to\r\n 4,000px, i.e. the label pane and the chart describing completely different rows. Padding\r\n rather than a `top`/transform because the pane is `position: sticky` (a `top` would move\r\n the STICKY inset) and because an absolutely-positioned child \u2014 the drop indicator \u2014 is\r\n laid out against the padding box's border edge, so it keeps its absolute row-space\r\n coordinates for free. -->\r\n <div\r\n #labelsEl\r\n class=\"fly-gantt__labels\"\r\n role=\"rowgroup\"\r\n [style.padding-block-start.px]=\"svgTop()\"\r\n >\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 [class.fly-gantt__label-row--dragging]=\"reorderDraggingId() === vm.flat.row.id\"\r\n [style.height.px]=\"effectiveRowHeight()\"\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 (pointerdown)=\"onLabelRowPointerDown($event, vm.flat.row.id)\"\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]=\"geo.indentFor(vm.flat.depth)\">\r\n @if (reorderEnabled()) {\r\n <!-- Visual affordance only: the drag is armed on the WHOLE row (above) behind a 4px\r\n threshold, so a plain click still selects. aria-hidden because the row is already\r\n the accessible handle and a second interactive child inside `role=\"row\"` would be\r\n structurally invalid \u2014 the same reason the splitter's full-height twin is. -->\r\n <span class=\"fly-gantt__grip\" aria-hidden=\"true\"></span>\r\n }\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 <!-- `flyTooltip`, never the native `[title]`: the browser tip is un-themable, has no\r\n keyboard story, and is clipped by nothing but also styled by nothing. The directive\r\n body-parents its surface, so it escapes the label pane's own overflow clip. -->\r\n <span\r\n class=\"fly-gantt__label-text\"\r\n [class.fly-gantt__label-text--group]=\"vm.shape === 'group'\"\r\n [flyTooltip]=\"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 <!-- `!== null`, not `@if (\u2026; as y)`: a drop at the very TOP of the list is y = 0, which the\r\n `as` form treats as falsy and silently refuses to draw. -->\r\n @if (reorderIndicatorTop() !== null) {\r\n <!-- Where the dragged row would land. Drawn here rather than by moving the row itself: the\r\n rows are windowed, so a live re-order of the rendered list would fight the virtualizer\r\n for every frame of the drag. -->\r\n <div class=\"fly-gantt__drop-line\" aria-hidden=\"true\" [style.top.px]=\"reorderIndicatorTop()\"></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 (pointerdown)=\"onPanPointerDown($event)\"\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]=\"effectiveRowHeight()\"\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 <!-- The row's full-width lane. It carries NO click handler: pressing the empty part of a\r\n row is not \"clicking an item\", it is grabbing the canvas, so the press falls through\r\n to the body's pan gesture. The rect still exists because it is the link-drag DROP\r\n target \u2014 `dropTargetAt` hit-tests `[data-row-id]` on the group above, and a lane with\r\n no painted fill would not be found by `elementFromPoint` at all. -->\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]=\"effectiveRowHeight()\"\r\n />\r\n\r\n @switch (vm.shape) {\r\n @case ('group') {\r\n <!-- A summary row is not editable (no drag, no resize, no connectors) but it IS\r\n selectable \u2014 `skills/ds-gantt.md` \u00A71. Now that the lane behind it\r\n pans instead of selecting, the bracket itself has to carry the press. -->\r\n <path\r\n class=\"fly-gantt__group\"\r\n [attr.d]=\"geo.groupPath(vm)\"\r\n [style.fill]=\"vm.color\"\r\n [style.stroke]=\"vm.color\"\r\n (pointerdown)=\"onShapePointerDown($event, vm.flat.row.id)\"\r\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\r\n />\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]=\"geo.diamondPoints(vm)\"\r\n [style.fill]=\"vm.color\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'move')\"\r\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\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]=\"geo.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]=\"geo.milestoneConnectorX(vm)\"\r\n [attr.cy]=\"vm.midY\"\r\n [attr.r]=\"CONNECTOR_R\"\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]=\"geo.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 (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\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]=\"geo.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]=\"geo.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]=\"geo.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]=\"geo.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]=\"geo.connectorX(vm, 'start')\"\r\n [attr.cy]=\"vm.midY\"\r\n [attr.r]=\"CONNECTOR_R\"\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]=\"geo.connectorX(vm, 'finish')\"\r\n [attr.cy]=\"vm.midY\"\r\n [attr.r]=\"CONNECTOR_R\"\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 (pointerdown)=\"onLinkPathPointerDown($event)\"\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 <!-- Re-point handle: drag the arrowhead onto a different row. Drawn before the delete\r\n badge so the badge wins any overlap on a very short arrow. -->\r\n <circle\r\n class=\"fly-gantt__link-head\"\r\n [attr.cx]=\"link.headX\"\r\n [attr.cy]=\"link.headY\"\r\n r=\"5\"\r\n [attr.aria-label]=\"'gantt.aria.retarget_link' | translate\"\r\n (pointerdown)=\"onLinkHeadPointerDown($event, link)\"\r\n />\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\">\r\n {{ (activeQuery() ? 'gantt.no_matches' : 'gantt.no_data') | translate: { query: activeQuery() } }}\r\n </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);--_selected: color-mix(in srgb, var(--_accent) 22%, transparent);--_selected-hover: color-mix(in srgb, var(--_accent) 32%, 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}:host(.fly-gantt--panning){cursor:grabbing;-webkit-user-select:none;user-select:none}:host(.fly-gantt--reordering){cursor:grabbing;-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__search{display:block;min-inline-size:0;padding:8px;padding-inline-end:12px}.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(--_selected);box-shadow:inset 3px 0 0 0 var(--_accent)}:host(.fly-gantt--rtl) .fly-gantt__label-row--selected{box-shadow:inset -3px 0 0 0 var(--_accent)}.fly-gantt__label-row--selected:hover{background:var(--_selected-hover)}.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__grip{flex:0 0 auto;inline-size:8px;block-size:14px;margin-inline-start:-4px;cursor:grab;opacity:0;background-image:radial-gradient(circle at 1.5px 1.5px,var(--_text-faint) 1.2px,transparent 1.3px);background-size:4px 4px;transition:opacity var(--_transition)}.fly-gantt__label-row:hover .fly-gantt__grip,.fly-gantt__label-row--selected .fly-gantt__grip{opacity:1}.fly-gantt__label-row--dragging{opacity:.45}.fly-gantt__drop-line{position:absolute;inset-inline:0;z-index:3;block-size:2px;margin-block-start:-1px;background:var(--_accent);pointer-events:none}.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;cursor:grab}.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(--_selected)}.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;pointer-events:none;transition:opacity var(--_transition)}.fly-gantt__row:hover .fly-gantt__connector{opacity:1;pointer-events:auto}.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-head{fill:var(--_surface);stroke:var(--_accent);stroke-width:2;cursor:grab}.fly-gantt__link-head:hover,.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: "directive", type: FlyTooltipDirective, selector: "[flyTooltip], [data-tooltip]", inputs: ["flyTooltip", "flyTooltipPlacement", "flyTooltipDisabled", "flyTooltipDelay"] }, { kind: "component", type: FlySearchInputComponent, selector: "fly-search-input", inputs: ["value", "placeholder", "placeholderKey", "ariaLabel", "ariaLabelKey", "debounceMs", "disabled", "collapsible", "expandTrigger", "stayExpandedWithValue", "size", "showFilterToggle", "filterPanelOpen", "open"], outputs: ["valueChange", "queryChange", "searched", "submitted", "cleared", "expandedChange", "filterToggled"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
15906
16281
  }
15907
16282
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyGanttComponent, decorators: [{
15908
16283
  type: Component,
@@ -15914,7 +16289,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
15914
16289
  '[class.fly-gantt--panning]': 'panning()',
15915
16290
  '[class.fly-gantt--reordering]': 'reorderDraggingId() !== null',
15916
16291
  '[attr.dir]': 'i18n.direction()',
15917
- }, 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 (searchEnabled()) {\r\n <!-- The label-tree filter. It lives in the CORNER because that is the one cell pinned on\r\n both axes: a field anywhere else scrolls away from the list it filters, and the corner\r\n is otherwise dead space sized exactly to the label pane. -->\r\n <fly-search-input\r\n class=\"fly-gantt__search\"\r\n size=\"sm\"\r\n [value]=\"searchQuery()\"\r\n [placeholderKey]=\"'gantt.search.placeholder'\"\r\n [ariaLabel]=\"'gantt.aria.search' | translate\"\r\n (queryChange)=\"onSearchQueryChange($event)\"\r\n />\r\n }\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 <!-- `padding-block-start` = the body `<svg>`'s own `top`, and it is what keeps the two panes\r\n on the same rows. The label tree renders only the WINDOWED slice, in normal flow, so\r\n without this the first rendered row sits at the top of a full-height cell while its bar\r\n sits `svgTop` further down \u2014 measured at 3,842px apart on a 5,000-row chart scrolled to\r\n 4,000px, i.e. the label pane and the chart describing completely different rows. Padding\r\n rather than a `top`/transform because the pane is `position: sticky` (a `top` would move\r\n the STICKY inset) and because an absolutely-positioned child \u2014 the drop indicator \u2014 is\r\n laid out against the padding box's border edge, so it keeps its absolute row-space\r\n coordinates for free. -->\r\n <div\r\n #labelsEl\r\n class=\"fly-gantt__labels\"\r\n role=\"rowgroup\"\r\n [style.padding-block-start.px]=\"svgTop()\"\r\n >\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 [class.fly-gantt__label-row--dragging]=\"reorderDraggingId() === vm.flat.row.id\"\r\n [style.height.px]=\"effectiveRowHeight()\"\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 (pointerdown)=\"onLabelRowPointerDown($event, vm.flat.row.id)\"\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]=\"geo.indentFor(vm.flat.depth)\">\r\n @if (reorderEnabled()) {\r\n <!-- Visual affordance only: the drag is armed on the WHOLE row (above) behind a 4px\r\n threshold, so a plain click still selects. aria-hidden because the row is already\r\n the accessible handle and a second interactive child inside `role=\"row\"` would be\r\n structurally invalid \u2014 the same reason the splitter's full-height twin is. -->\r\n <span class=\"fly-gantt__grip\" aria-hidden=\"true\"></span>\r\n }\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 <!-- `flyTooltip`, never the native `[title]`: the browser tip is un-themable, has no\r\n keyboard story, and is clipped by nothing but also styled by nothing. The directive\r\n body-parents its surface, so it escapes the label pane's own overflow clip. -->\r\n <span\r\n class=\"fly-gantt__label-text\"\r\n [class.fly-gantt__label-text--group]=\"vm.shape === 'group'\"\r\n [flyTooltip]=\"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 <!-- `!== null`, not `@if (\u2026; as y)`: a drop at the very TOP of the list is y = 0, which the\r\n `as` form treats as falsy and silently refuses to draw. -->\r\n @if (reorderIndicatorTop() !== null) {\r\n <!-- Where the dragged row would land. Drawn here rather than by moving the row itself: the\r\n rows are windowed, so a live re-order of the rendered list would fight the virtualizer\r\n for every frame of the drag. -->\r\n <div class=\"fly-gantt__drop-line\" aria-hidden=\"true\" [style.top.px]=\"reorderIndicatorTop()\"></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 (pointerdown)=\"onPanPointerDown($event)\"\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]=\"effectiveRowHeight()\"\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 <!-- The row's full-width lane. It carries NO click handler: pressing the empty part of a\r\n row is not \"clicking an item\", it is grabbing the canvas, so the press falls through\r\n to the body's pan gesture. The rect still exists because it is the link-drag DROP\r\n target \u2014 `dropTargetAt` hit-tests `[data-row-id]` on the group above, and a lane with\r\n no painted fill would not be found by `elementFromPoint` at all. -->\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]=\"effectiveRowHeight()\"\r\n />\r\n\r\n @switch (vm.shape) {\r\n @case ('group') {\r\n <!-- A summary row is not editable (no drag, no resize, no connectors) but it IS\r\n selectable \u2014 `skills/design-system-gantt.md` \u00A71. Now that the lane behind it\r\n pans instead of selecting, the bracket itself has to carry the press. -->\r\n <path\r\n class=\"fly-gantt__group\"\r\n [attr.d]=\"geo.groupPath(vm)\"\r\n [style.fill]=\"vm.color\"\r\n [style.stroke]=\"vm.color\"\r\n (pointerdown)=\"onShapePointerDown($event, vm.flat.row.id)\"\r\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\r\n />\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]=\"geo.diamondPoints(vm)\"\r\n [style.fill]=\"vm.color\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'move')\"\r\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\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]=\"geo.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]=\"geo.milestoneConnectorX(vm)\"\r\n [attr.cy]=\"vm.midY\"\r\n [attr.r]=\"CONNECTOR_R\"\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]=\"geo.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 (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\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]=\"geo.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]=\"geo.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]=\"geo.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]=\"geo.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]=\"geo.connectorX(vm, 'start')\"\r\n [attr.cy]=\"vm.midY\"\r\n [attr.r]=\"CONNECTOR_R\"\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]=\"geo.connectorX(vm, 'finish')\"\r\n [attr.cy]=\"vm.midY\"\r\n [attr.r]=\"CONNECTOR_R\"\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\">\r\n {{ (activeQuery() ? 'gantt.no_matches' : 'gantt.no_data') | translate: { query: activeQuery() } }}\r\n </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}:host(.fly-gantt--panning){cursor:grabbing;-webkit-user-select:none;user-select:none}:host(.fly-gantt--reordering){cursor:grabbing;-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__search{display:block;min-inline-size:0;padding:8px;padding-inline-end:12px}.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__grip{flex:0 0 auto;inline-size:8px;block-size:14px;margin-inline-start:-4px;cursor:grab;opacity:0;background-image:radial-gradient(circle at 1.5px 1.5px,var(--_text-faint) 1.2px,transparent 1.3px);background-size:4px 4px;transition:opacity var(--_transition)}.fly-gantt__label-row:hover .fly-gantt__grip,.fly-gantt__label-row--selected .fly-gantt__grip{opacity:1}.fly-gantt__label-row--dragging{opacity:.45}.fly-gantt__drop-line{position:absolute;inset-inline:0;z-index:3;block-size:2px;margin-block-start:-1px;background:var(--_accent);pointer-events:none}.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;cursor:grab}.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;pointer-events:none;transition:opacity var(--_transition)}.fly-gantt__row:hover .fly-gantt__connector{opacity:1;pointer-events:auto}.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"] }]
16292
+ }, 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 (searchEnabled()) {\r\n <!-- The label-tree filter. It lives in the CORNER because that is the one cell pinned on\r\n both axes: a field anywhere else scrolls away from the list it filters, and the corner\r\n is otherwise dead space sized exactly to the label pane. -->\r\n <fly-search-input\r\n class=\"fly-gantt__search\"\r\n size=\"sm\"\r\n [value]=\"searchQuery()\"\r\n [placeholderKey]=\"'gantt.search.placeholder'\"\r\n [ariaLabel]=\"'gantt.aria.search' | translate\"\r\n (queryChange)=\"onSearchQueryChange($event)\"\r\n />\r\n }\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 <!-- `padding-block-start` = the body `<svg>`'s own `top`, and it is what keeps the two panes\r\n on the same rows. The label tree renders only the WINDOWED slice, in normal flow, so\r\n without this the first rendered row sits at the top of a full-height cell while its bar\r\n sits `svgTop` further down \u2014 measured at 3,842px apart on a 5,000-row chart scrolled to\r\n 4,000px, i.e. the label pane and the chart describing completely different rows. Padding\r\n rather than a `top`/transform because the pane is `position: sticky` (a `top` would move\r\n the STICKY inset) and because an absolutely-positioned child \u2014 the drop indicator \u2014 is\r\n laid out against the padding box's border edge, so it keeps its absolute row-space\r\n coordinates for free. -->\r\n <div\r\n #labelsEl\r\n class=\"fly-gantt__labels\"\r\n role=\"rowgroup\"\r\n [style.padding-block-start.px]=\"svgTop()\"\r\n >\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 [class.fly-gantt__label-row--dragging]=\"reorderDraggingId() === vm.flat.row.id\"\r\n [style.height.px]=\"effectiveRowHeight()\"\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 (pointerdown)=\"onLabelRowPointerDown($event, vm.flat.row.id)\"\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]=\"geo.indentFor(vm.flat.depth)\">\r\n @if (reorderEnabled()) {\r\n <!-- Visual affordance only: the drag is armed on the WHOLE row (above) behind a 4px\r\n threshold, so a plain click still selects. aria-hidden because the row is already\r\n the accessible handle and a second interactive child inside `role=\"row\"` would be\r\n structurally invalid \u2014 the same reason the splitter's full-height twin is. -->\r\n <span class=\"fly-gantt__grip\" aria-hidden=\"true\"></span>\r\n }\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 <!-- `flyTooltip`, never the native `[title]`: the browser tip is un-themable, has no\r\n keyboard story, and is clipped by nothing but also styled by nothing. The directive\r\n body-parents its surface, so it escapes the label pane's own overflow clip. -->\r\n <span\r\n class=\"fly-gantt__label-text\"\r\n [class.fly-gantt__label-text--group]=\"vm.shape === 'group'\"\r\n [flyTooltip]=\"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 <!-- `!== null`, not `@if (\u2026; as y)`: a drop at the very TOP of the list is y = 0, which the\r\n `as` form treats as falsy and silently refuses to draw. -->\r\n @if (reorderIndicatorTop() !== null) {\r\n <!-- Where the dragged row would land. Drawn here rather than by moving the row itself: the\r\n rows are windowed, so a live re-order of the rendered list would fight the virtualizer\r\n for every frame of the drag. -->\r\n <div class=\"fly-gantt__drop-line\" aria-hidden=\"true\" [style.top.px]=\"reorderIndicatorTop()\"></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 (pointerdown)=\"onPanPointerDown($event)\"\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]=\"effectiveRowHeight()\"\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 <!-- The row's full-width lane. It carries NO click handler: pressing the empty part of a\r\n row is not \"clicking an item\", it is grabbing the canvas, so the press falls through\r\n to the body's pan gesture. The rect still exists because it is the link-drag DROP\r\n target \u2014 `dropTargetAt` hit-tests `[data-row-id]` on the group above, and a lane with\r\n no painted fill would not be found by `elementFromPoint` at all. -->\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]=\"effectiveRowHeight()\"\r\n />\r\n\r\n @switch (vm.shape) {\r\n @case ('group') {\r\n <!-- A summary row is not editable (no drag, no resize, no connectors) but it IS\r\n selectable \u2014 `skills/ds-gantt.md` \u00A71. Now that the lane behind it\r\n pans instead of selecting, the bracket itself has to carry the press. -->\r\n <path\r\n class=\"fly-gantt__group\"\r\n [attr.d]=\"geo.groupPath(vm)\"\r\n [style.fill]=\"vm.color\"\r\n [style.stroke]=\"vm.color\"\r\n (pointerdown)=\"onShapePointerDown($event, vm.flat.row.id)\"\r\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\r\n />\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]=\"geo.diamondPoints(vm)\"\r\n [style.fill]=\"vm.color\"\r\n (pointerdown)=\"onBarPointerDown($event, vm, 'move')\"\r\n (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\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]=\"geo.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]=\"geo.milestoneConnectorX(vm)\"\r\n [attr.cy]=\"vm.midY\"\r\n [attr.r]=\"CONNECTOR_R\"\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]=\"geo.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 (dblclick)=\"onRowDblClick(vm.flat.row.id)\"\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]=\"geo.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]=\"geo.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]=\"geo.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]=\"geo.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]=\"geo.connectorX(vm, 'start')\"\r\n [attr.cy]=\"vm.midY\"\r\n [attr.r]=\"CONNECTOR_R\"\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]=\"geo.connectorX(vm, 'finish')\"\r\n [attr.cy]=\"vm.midY\"\r\n [attr.r]=\"CONNECTOR_R\"\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 (pointerdown)=\"onLinkPathPointerDown($event)\"\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 <!-- Re-point handle: drag the arrowhead onto a different row. Drawn before the delete\r\n badge so the badge wins any overlap on a very short arrow. -->\r\n <circle\r\n class=\"fly-gantt__link-head\"\r\n [attr.cx]=\"link.headX\"\r\n [attr.cy]=\"link.headY\"\r\n r=\"5\"\r\n [attr.aria-label]=\"'gantt.aria.retarget_link' | translate\"\r\n (pointerdown)=\"onLinkHeadPointerDown($event, link)\"\r\n />\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\">\r\n {{ (activeQuery() ? 'gantt.no_matches' : 'gantt.no_data') | translate: { query: activeQuery() } }}\r\n </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);--_selected: color-mix(in srgb, var(--_accent) 22%, transparent);--_selected-hover: color-mix(in srgb, var(--_accent) 32%, 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}:host(.fly-gantt--panning){cursor:grabbing;-webkit-user-select:none;user-select:none}:host(.fly-gantt--reordering){cursor:grabbing;-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__search{display:block;min-inline-size:0;padding:8px;padding-inline-end:12px}.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(--_selected);box-shadow:inset 3px 0 0 0 var(--_accent)}:host(.fly-gantt--rtl) .fly-gantt__label-row--selected{box-shadow:inset -3px 0 0 0 var(--_accent)}.fly-gantt__label-row--selected:hover{background:var(--_selected-hover)}.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__grip{flex:0 0 auto;inline-size:8px;block-size:14px;margin-inline-start:-4px;cursor:grab;opacity:0;background-image:radial-gradient(circle at 1.5px 1.5px,var(--_text-faint) 1.2px,transparent 1.3px);background-size:4px 4px;transition:opacity var(--_transition)}.fly-gantt__label-row:hover .fly-gantt__grip,.fly-gantt__label-row--selected .fly-gantt__grip{opacity:1}.fly-gantt__label-row--dragging{opacity:.45}.fly-gantt__drop-line{position:absolute;inset-inline:0;z-index:3;block-size:2px;margin-block-start:-1px;background:var(--_accent);pointer-events:none}.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;cursor:grab}.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(--_selected)}.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;pointer-events:none;transition:opacity var(--_transition)}.fly-gantt__row:hover .fly-gantt__connector{opacity:1;pointer-events:auto}.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-head{fill:var(--_surface);stroke:var(--_accent);stroke-width:2;cursor:grab}.fly-gantt__link-head:hover,.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"] }]
15918
16293
  }], 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 }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], reorderable: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderable", required: false }] }], searchQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchQuery", required: false }] }, { type: i0.Output, args: ["searchQueryChange"] }], 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"] }], rowReorder: [{ type: i0.Output, args: ["rowReorder"] }], rowIndentChange: [{ type: i0.Output, args: ["rowIndentChange"] }], 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 }] }] } });
15919
16294
 
15920
16295
  /**
@@ -16105,38 +16480,43 @@ function ganttPeriodBarItem(spec) {
16105
16480
  * would have to pick a label for a state the user cannot see.
16106
16481
  *
16107
16482
  * ## Icon-only, and why they are still discoverable
16108
- * Both are icon-only despite `skills/magic-bar-view-contributions.md` §5's default, because the
16483
+ * Both are icon-only despite `skills/shell-magic-bar-views.md` §5's default, because the
16109
16484
  * indent/outdent glyph pair is one of the few marks a user genuinely reads without help — it is the
16110
16485
  * same pair every word processor, IDE and desktop planner has shipped for thirty years, and the two
16111
16486
  * are identified by being MIRRORS of each other, which a label cannot improve on. They also sort
16112
16487
  * inside Add/Delete/Edit under {@link orderMagicBarItems}, so they never take the outer edge from a
16113
16488
  * verb that needs it.
16114
16489
  *
16115
- * ## Disabled, never hidden
16116
- * `canIndent` / `canOutdent` are state gates, not permission gates: the same row becomes indentable
16117
- * the moment a suitable row appears above it. So both stay rendered and dimmed, with a tip saying
16118
- * why §6's default. Pass `disabledTipKey`s when your app's reason is more specific than the
16119
- * baseline "this row cannot move any further".
16490
+ * ## Omitted, never dimmed
16491
+ * An unavailable verb is left OUT of the returned array — the magic bar renders no disabled
16492
+ * buttons at all (`skills/shell-magic-bar-views.md` §6). The bar is one small shared strip
16493
+ * and its cost is measured in how many things it shows at once, so a dimmed button spends that
16494
+ * budget on an action the user cannot take. This pair is the clearest case: at most ONE of the two
16495
+ * is ever available for a given row, so dimming doubled the group's width to say nothing. What
16496
+ * availability depends on is also on screen while the bar is being read — a row already nested
16497
+ * cannot nest further — so the dimmed state was not teaching anything the chart did not show.
16498
+ *
16499
+ * Returns 0, 1 or 2 actions. Spread it; never index it.
16120
16500
  */
16121
16501
  function ganttOutlineBarItems(spec) {
16122
- return [
16123
- {
16502
+ const items = [];
16503
+ if (spec.canOutdent) {
16504
+ items.push({
16124
16505
  id: 'outdent-row',
16125
16506
  labelKey: 'gantt.outline.outdent',
16126
16507
  iconPath: FLY_MAGIC_BAR_ICONS.outdent,
16127
- disabled: !spec.canOutdent,
16128
- disabledTipKey: spec.outdentDisabledTipKey ?? 'gantt.outline.outdentBlocked',
16129
16508
  onSelect: spec.onOutdent,
16130
- },
16131
- {
16509
+ });
16510
+ }
16511
+ if (spec.canIndent) {
16512
+ items.push({
16132
16513
  id: 'indent-row',
16133
16514
  labelKey: 'gantt.outline.indent',
16134
16515
  iconPath: FLY_MAGIC_BAR_ICONS.indent,
16135
- disabled: !spec.canIndent,
16136
- disabledTipKey: spec.indentDisabledTipKey ?? 'gantt.outline.indentBlocked',
16137
16516
  onSelect: spec.onIndent,
16138
- },
16139
- ];
16517
+ });
16518
+ }
16519
+ return items;
16140
16520
  }
16141
16521
 
16142
16522
  // ── Bundled ISO 3166-1 country list ──────────────────────────────────────────
@@ -19405,7 +19785,7 @@ function iconVerbRank(action) {
19405
19785
  *
19406
19786
  * ## Why this lives in the design system
19407
19787
  * It shipped first inside PPM (`ppm/shared/magic-bar-order.util.ts`) and
19408
- * `skills/magic-bar-view-contributions.md` §7 named that file as the reference implementation —
19788
+ * `skills/shell-magic-bar-views.md` §7 named that file as the reference implementation —
19409
19789
  * which is exactly the shape of a rule that is about to be copy-pasted into the next External App.
19410
19790
  * The ordering is a property of the BAR, not of any one app: the same renderer paints every
19411
19791
  * contribution, so two apps disagreeing about it is a platform-visible inconsistency, not a local
@@ -19993,7 +20373,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
19993
20373
  * App running standalone.
19994
20374
  *
19995
20375
  * ## The gap this closes
19996
- * A view publishes one `MagicBarContribution` (skill: `magic-bar-actions.md`)
20376
+ * A view publishes one `MagicBarContribution` (skill: `shell-magic-bar.md`)
19997
20377
  * and, embedded in the desktop shell, the top-bar pill renders it. Standalone,
19998
20378
  * nothing did — so every screen also kept a bespoke header-actions row
19999
20379
  * (`[page-header-actions]` buttons) that duplicated the published actions in
@@ -21882,11 +22262,11 @@ class FlyPeoplePickerComponent {
21882
22262
  this.selectionChange.emit(this._selected().map((o) => o.id));
21883
22263
  }
21884
22264
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyPeoplePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
21885
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyPeoplePickerComponent, isStandalone: true, selector: "fly-people-picker", inputs: { searchFn: { classPropertyName: "searchFn", publicName: "searchFn", isSignal: true, isRequired: true, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, excludeIds: { classPropertyName: "excludeIds", publicName: "excludeIds", isSignal: true, isRequired: false, transformFunction: null }, initialSelection: { classPropertyName: "initialSelection", publicName: "initialSelection", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange" }, host: { classAttribute: "fly-people-picker" }, viewQueries: [{ propertyName: "typeahead", first: true, predicate: FlyTypeaheadComponent, descendants: true }], ngImport: i0, template: "@if (selected().length > 0) {\r\n <ul class=\"fly-people-picker__chips\" role=\"list\">\r\n @for (o of selected(); track o.id) {\r\n <li class=\"fly-people-picker__chip\" role=\"listitem\">\r\n <span class=\"fly-people-picker__chip-avatar\" aria-hidden=\"true\">\r\n @if (o.avatarUrl) {\r\n <img [src]=\"o.avatarUrl\" alt=\"\" />\r\n } @else {\r\n <span class=\"fly-people-picker__chip-initials\">{{ initialsFor(o.displayName) }}</span>\r\n }\r\n </span>\r\n <span class=\"fly-people-picker__chip-name\" [title]=\"o.email || o.displayName\">{{ o.displayName }}</span>\r\n <button\r\n type=\"button\"\r\n class=\"fly-people-picker__chip-remove\"\r\n [attr.aria-label]=\"'people_picker.remove' | translate: { name: o.displayName }\"\r\n (click)=\"remove(o.id)\">\r\n \u2715\r\n </button>\r\n </li>\r\n }\r\n </ul>\r\n}\r\n\r\n@if (showSearch()) {\r\n <fly-typeahead\r\n [searchFn]=\"typeaheadSearchFn\"\r\n [placeholder]=\"placeholder() || ('people_picker.search_placeholder' | translate)\"\r\n [allowFreeText]=\"false\"\r\n (selected)=\"onPicked($event)\" />\r\n} @else {\r\n <button type=\"button\" class=\"fly-people-picker__change-btn\" (click)=\"change()\">\r\n {{ 'people_picker.change' | translate }}\r\n </button>\r\n}\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-hover: var(--surface-hover, #f1f5f9);--_border: var(--separator, var(--surface-border, #e2e8f0));--_text: var(--label-primary, var(--text-color, #0f172a));--_text-subtle: var(--label-secondary, var(--text-color-secondary, #64748b));--_fill: var(--fill-tertiary, #f3f4f6);--_accent: var(--accent);--_danger: var(--system-red, #ef4444);--_radius: 999px;display:flex;flex-direction:column;gap:8px;inline-size:100%;color:var(--_text);font-size:13px}.fly-people-picker__chips{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px}.fly-people-picker__chip{display:inline-flex;align-items:center;gap:6px;padding-block:3px;padding-inline:3px 6px;border-radius:var(--_radius);background:var(--_fill);max-inline-size:220px}.fly-people-picker__chip-avatar{inline-size:22px;block-size:22px;border-radius:50%;overflow:hidden;flex:none;background:var(--_surface);display:flex;align-items:center;justify-content:center}.fly-people-picker__chip-avatar img{inline-size:100%;block-size:100%;object-fit:cover}.fly-people-picker__chip-initials{font-size:9px;font-weight:700;color:var(--_text-subtle);text-transform:uppercase}.fly-people-picker__chip-name{font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-people-picker__chip-remove{border:none;background:transparent;padding:2px;margin-inline-start:2px;border-radius:50%;color:var(--_text-subtle);font-size:10px;line-height:1;cursor:pointer;flex:none}.fly-people-picker__chip-remove:hover{background:color-mix(in srgb,var(--_danger) 18%,transparent);color:var(--_danger)}.fly-people-picker__change-btn{align-self:flex-start;border:1px solid var(--_border);border-radius:8px;background:var(--_surface);padding:5px 12px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-people-picker__change-btn:hover{background:var(--_surface-hover)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: FlyTypeaheadComponent, selector: "fly-typeahead", inputs: ["searchFn", "debounceMs", "placeholder", "ariaLabel", "value", "allowFreeText", "openOnFocus"], outputs: ["valueChange", "selected", "cleared"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
22265
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyPeoplePickerComponent, isStandalone: true, selector: "fly-people-picker", inputs: { searchFn: { classPropertyName: "searchFn", publicName: "searchFn", isSignal: true, isRequired: true, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, excludeIds: { classPropertyName: "excludeIds", publicName: "excludeIds", isSignal: true, isRequired: false, transformFunction: null }, initialSelection: { classPropertyName: "initialSelection", publicName: "initialSelection", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange" }, host: { classAttribute: "fly-people-picker" }, viewQueries: [{ propertyName: "typeahead", first: true, predicate: FlyTypeaheadComponent, descendants: true }], ngImport: i0, template: "@if (selected().length > 0) {\n <ul class=\"fly-people-picker__chips\" role=\"list\">\n @for (o of selected(); track o.id) {\n <li class=\"fly-people-picker__chip\" role=\"listitem\">\n <span class=\"fly-people-picker__chip-avatar\" aria-hidden=\"true\">\n @if (o.avatarUrl) {\n <img [src]=\"o.avatarUrl\" alt=\"\" />\n } @else {\n <span class=\"fly-people-picker__chip-initials\">{{ initialsFor(o.displayName) }}</span>\n }\n </span>\n <span class=\"fly-people-picker__chip-name\" [title]=\"o.email || o.displayName\">{{ o.displayName }}</span>\n <button\n type=\"button\"\n class=\"fly-people-picker__chip-remove\"\n [attr.aria-label]=\"'people_picker.remove' | translate: { name: o.displayName }\"\n (click)=\"remove(o.id)\">\n \u2715\n </button>\n </li>\n }\n </ul>\n}\n\n@if (showSearch()) {\n <fly-typeahead\n [searchFn]=\"typeaheadSearchFn\"\n [placeholder]=\"placeholder() || ('people_picker.search_placeholder' | translate)\"\n [allowFreeText]=\"false\"\n (selected)=\"onPicked($event)\" />\n} @else {\n <button type=\"button\" class=\"fly-people-picker__change-btn\" (click)=\"change()\">\n {{ 'people_picker.change' | translate }}\n </button>\n}\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-hover: var(--surface-hover, #f1f5f9);--_border: var(--separator, var(--surface-border, #e2e8f0));--_text: var(--label-primary, var(--text-color, #0f172a));--_text-subtle: var(--label-secondary, var(--text-color-secondary, #64748b));--_fill: var(--fill-tertiary, #f3f4f6);--_accent: var(--accent);--_danger: var(--system-red, #ef4444);--_radius: 999px;display:flex;flex-direction:column;gap:8px;inline-size:100%;color:var(--_text);font-size:13px}.fly-people-picker__chips{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px}.fly-people-picker__chip{display:inline-flex;align-items:center;gap:6px;padding-block:3px;padding-inline:3px 6px;border-radius:var(--_radius);background:var(--_fill);max-inline-size:220px}.fly-people-picker__chip-avatar{inline-size:22px;block-size:22px;border-radius:50%;overflow:hidden;flex:none;background:var(--_surface);display:flex;align-items:center;justify-content:center}.fly-people-picker__chip-avatar img{inline-size:100%;block-size:100%;object-fit:cover}.fly-people-picker__chip-initials{font-size:9px;font-weight:700;color:var(--_text-subtle);text-transform:uppercase}.fly-people-picker__chip-name{font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-people-picker__chip-remove{border:none;background:transparent;padding:2px;margin-inline-start:2px;border-radius:50%;color:var(--_text-subtle);font-size:10px;line-height:1;cursor:pointer;flex:none}.fly-people-picker__chip-remove:hover{background:color-mix(in srgb,var(--_danger) 18%,transparent);color:var(--_danger)}.fly-people-picker__change-btn{align-self:flex-start;border:1px solid var(--_border);border-radius:8px;background:var(--_surface);padding:5px 12px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-people-picker__change-btn:hover{background:var(--_surface-hover)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: FlyTypeaheadComponent, selector: "fly-typeahead", inputs: ["searchFn", "debounceMs", "placeholder", "ariaLabel", "value", "allowFreeText", "openOnFocus"], outputs: ["valueChange", "selected", "cleared"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
21886
22266
  }
21887
22267
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyPeoplePickerComponent, decorators: [{
21888
22268
  type: Component,
21889
- args: [{ selector: 'fly-people-picker', standalone: true, imports: [CommonModule, TranslatePipe, FlyTypeaheadComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'fly-people-picker' }, template: "@if (selected().length > 0) {\r\n <ul class=\"fly-people-picker__chips\" role=\"list\">\r\n @for (o of selected(); track o.id) {\r\n <li class=\"fly-people-picker__chip\" role=\"listitem\">\r\n <span class=\"fly-people-picker__chip-avatar\" aria-hidden=\"true\">\r\n @if (o.avatarUrl) {\r\n <img [src]=\"o.avatarUrl\" alt=\"\" />\r\n } @else {\r\n <span class=\"fly-people-picker__chip-initials\">{{ initialsFor(o.displayName) }}</span>\r\n }\r\n </span>\r\n <span class=\"fly-people-picker__chip-name\" [title]=\"o.email || o.displayName\">{{ o.displayName }}</span>\r\n <button\r\n type=\"button\"\r\n class=\"fly-people-picker__chip-remove\"\r\n [attr.aria-label]=\"'people_picker.remove' | translate: { name: o.displayName }\"\r\n (click)=\"remove(o.id)\">\r\n \u2715\r\n </button>\r\n </li>\r\n }\r\n </ul>\r\n}\r\n\r\n@if (showSearch()) {\r\n <fly-typeahead\r\n [searchFn]=\"typeaheadSearchFn\"\r\n [placeholder]=\"placeholder() || ('people_picker.search_placeholder' | translate)\"\r\n [allowFreeText]=\"false\"\r\n (selected)=\"onPicked($event)\" />\r\n} @else {\r\n <button type=\"button\" class=\"fly-people-picker__change-btn\" (click)=\"change()\">\r\n {{ 'people_picker.change' | translate }}\r\n </button>\r\n}\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-hover: var(--surface-hover, #f1f5f9);--_border: var(--separator, var(--surface-border, #e2e8f0));--_text: var(--label-primary, var(--text-color, #0f172a));--_text-subtle: var(--label-secondary, var(--text-color-secondary, #64748b));--_fill: var(--fill-tertiary, #f3f4f6);--_accent: var(--accent);--_danger: var(--system-red, #ef4444);--_radius: 999px;display:flex;flex-direction:column;gap:8px;inline-size:100%;color:var(--_text);font-size:13px}.fly-people-picker__chips{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px}.fly-people-picker__chip{display:inline-flex;align-items:center;gap:6px;padding-block:3px;padding-inline:3px 6px;border-radius:var(--_radius);background:var(--_fill);max-inline-size:220px}.fly-people-picker__chip-avatar{inline-size:22px;block-size:22px;border-radius:50%;overflow:hidden;flex:none;background:var(--_surface);display:flex;align-items:center;justify-content:center}.fly-people-picker__chip-avatar img{inline-size:100%;block-size:100%;object-fit:cover}.fly-people-picker__chip-initials{font-size:9px;font-weight:700;color:var(--_text-subtle);text-transform:uppercase}.fly-people-picker__chip-name{font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-people-picker__chip-remove{border:none;background:transparent;padding:2px;margin-inline-start:2px;border-radius:50%;color:var(--_text-subtle);font-size:10px;line-height:1;cursor:pointer;flex:none}.fly-people-picker__chip-remove:hover{background:color-mix(in srgb,var(--_danger) 18%,transparent);color:var(--_danger)}.fly-people-picker__change-btn{align-self:flex-start;border:1px solid var(--_border);border-radius:8px;background:var(--_surface);padding:5px 12px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-people-picker__change-btn:hover{background:var(--_surface-hover)}\n"] }]
22269
+ args: [{ selector: 'fly-people-picker', standalone: true, imports: [CommonModule, TranslatePipe, FlyTypeaheadComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'fly-people-picker' }, template: "@if (selected().length > 0) {\n <ul class=\"fly-people-picker__chips\" role=\"list\">\n @for (o of selected(); track o.id) {\n <li class=\"fly-people-picker__chip\" role=\"listitem\">\n <span class=\"fly-people-picker__chip-avatar\" aria-hidden=\"true\">\n @if (o.avatarUrl) {\n <img [src]=\"o.avatarUrl\" alt=\"\" />\n } @else {\n <span class=\"fly-people-picker__chip-initials\">{{ initialsFor(o.displayName) }}</span>\n }\n </span>\n <span class=\"fly-people-picker__chip-name\" [title]=\"o.email || o.displayName\">{{ o.displayName }}</span>\n <button\n type=\"button\"\n class=\"fly-people-picker__chip-remove\"\n [attr.aria-label]=\"'people_picker.remove' | translate: { name: o.displayName }\"\n (click)=\"remove(o.id)\">\n \u2715\n </button>\n </li>\n }\n </ul>\n}\n\n@if (showSearch()) {\n <fly-typeahead\n [searchFn]=\"typeaheadSearchFn\"\n [placeholder]=\"placeholder() || ('people_picker.search_placeholder' | translate)\"\n [allowFreeText]=\"false\"\n (selected)=\"onPicked($event)\" />\n} @else {\n <button type=\"button\" class=\"fly-people-picker__change-btn\" (click)=\"change()\">\n {{ 'people_picker.change' | translate }}\n </button>\n}\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-hover: var(--surface-hover, #f1f5f9);--_border: var(--separator, var(--surface-border, #e2e8f0));--_text: var(--label-primary, var(--text-color, #0f172a));--_text-subtle: var(--label-secondary, var(--text-color-secondary, #64748b));--_fill: var(--fill-tertiary, #f3f4f6);--_accent: var(--accent);--_danger: var(--system-red, #ef4444);--_radius: 999px;display:flex;flex-direction:column;gap:8px;inline-size:100%;color:var(--_text);font-size:13px}.fly-people-picker__chips{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px}.fly-people-picker__chip{display:inline-flex;align-items:center;gap:6px;padding-block:3px;padding-inline:3px 6px;border-radius:var(--_radius);background:var(--_fill);max-inline-size:220px}.fly-people-picker__chip-avatar{inline-size:22px;block-size:22px;border-radius:50%;overflow:hidden;flex:none;background:var(--_surface);display:flex;align-items:center;justify-content:center}.fly-people-picker__chip-avatar img{inline-size:100%;block-size:100%;object-fit:cover}.fly-people-picker__chip-initials{font-size:9px;font-weight:700;color:var(--_text-subtle);text-transform:uppercase}.fly-people-picker__chip-name{font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-people-picker__chip-remove{border:none;background:transparent;padding:2px;margin-inline-start:2px;border-radius:50%;color:var(--_text-subtle);font-size:10px;line-height:1;cursor:pointer;flex:none}.fly-people-picker__chip-remove:hover{background:color-mix(in srgb,var(--_danger) 18%,transparent);color:var(--_danger)}.fly-people-picker__change-btn{align-self:flex-start;border:1px solid var(--_border);border-radius:8px;background:var(--_surface);padding:5px 12px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-people-picker__change-btn:hover{background:var(--_surface-hover)}\n"] }]
21890
22270
  }], ctorParameters: () => [], propDecorators: { searchFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchFn", required: true }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], excludeIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "excludeIds", required: false }] }], initialSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialSelection", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], typeahead: [{
21891
22271
  type: ViewChild,
21892
22272
  args: [FlyTypeaheadComponent]
@@ -22419,7 +22799,7 @@ function __setFlyFlagEmojiSupport(value) {
22419
22799
  * `locked` is a different claim entirely: **the value is fixed on purpose**, because
22420
22800
  * something downstream now depends on it (PPM freezes a project's currency the moment any
22421
22801
  * financial row exists — changing it would silently re-denominate every stored amount).
22422
- * A dimmed control with no explanation is exactly the failure `skills/magic-bar-actions.md`
22802
+ * A dimmed control with no explanation is exactly the failure `skills/shell-magic-bar.md`
22423
22803
  * §3.6 names for actions ("a vanishing action teaches the user nothing"); the same argument
22424
22804
  * applies to a frozen field. So `locked` renders differently on purpose: full-opacity (never
22425
22805
  * dimmed — the value is not "unavailable", it is authoritative), no dropdown affordance at
@@ -22503,7 +22883,7 @@ class FlyCurrencySelectorComponent {
22503
22883
  */
22504
22884
  locked = input(false, ...(ngDevMode ? [{ debugName: "locked" }] : /* istanbul ignore next */ []));
22505
22885
  /**
22506
- * i18n KEY (never resolved text — see `skills/magic-bar-actions.md` §3.4) explaining
22886
+ * i18n KEY (never resolved text — see `skills/shell-magic-bar.md` §3.4) explaining
22507
22887
  * WHY the control is locked. Omit to use the localized `currency_selector.locked_default_reason`
22508
22888
  * baseline key; supply your own only when that default reason is wrong for your case
22509
22889
  * (mirrors `MagicBarActionSpec.disabledTipKey`'s "supply only when the default is wrong"
@@ -25363,6 +25743,18 @@ function ariaSort(current, column) {
25363
25743
  return 'none';
25364
25744
  return current.dir === 'asc' ? 'ascending' : 'descending';
25365
25745
  }
25746
+ /**
25747
+ * A row's own `id` field, for matching against `FlyDataTableComponent`'s `selectedId` input.
25748
+ * Every row shape this table renders is a CRUD record with a real `id` — reading it off `unknown`
25749
+ * rather than requiring a typed row keeps the component generic. `undefined` (no `id`, or not an
25750
+ * object) never matches a real `selectedId`, so an ill-shaped row simply never paints selected.
25751
+ */
25752
+ function rowId(row) {
25753
+ if (row === null || typeof row !== 'object')
25754
+ return undefined;
25755
+ const id = row['id'];
25756
+ return typeof id === 'string' ? id : undefined;
25757
+ }
25366
25758
  /**
25367
25759
  * Resolves a column's raw cell value off a row via the column's dot-path
25368
25760
  * `field` (falling back to `id`). Missing segments resolve to `null` rather
@@ -25413,6 +25805,13 @@ class FlyDataTableComponent {
25413
25805
  /** `fly-state-message` key when `rows` is empty (omit to render nothing). */
25414
25806
  emptyKey = input(...(ngDevMode ? [undefined, { debugName: "emptyKey" }] : /* istanbul ignore next */ []));
25415
25807
  emptyIconClass = input(...(ngDevMode ? [undefined, { debugName: "emptyIconClass" }] : /* istanbul ignore next */ []));
25808
+ /**
25809
+ * The selected row's `id`, or `null`/omitted for no selection — the select-then-act model
25810
+ * (`skills/shell-ux-rulebook.md` §1) paints the matching `<tr>` with `--tint-sel` and
25811
+ * `aria-selected`. A page that still opens rows on activation (rather than selecting them) simply
25812
+ * never sets this; the table renders exactly as before.
25813
+ */
25814
+ selectedId = input(null, ...(ngDevMode ? [{ debugName: "selectedId" }] : /* istanbul ignore next */ []));
25416
25815
  skeletonRows = [0, 1, 2, 3, 4];
25417
25816
  cellTemplates = contentChildren(FlyCellDirective, ...(ngDevMode ? [{ debugName: "cellTemplates" }] : /* istanbul ignore next */ []));
25418
25817
  templateMap = computed(() => {
@@ -25439,6 +25838,10 @@ class FlyDataTableComponent {
25439
25838
  cellValue(row, col) {
25440
25839
  return resolveCellValue(row, col);
25441
25840
  }
25841
+ isSelected(row) {
25842
+ const selected = this.selectedId();
25843
+ return selected !== null && rowId(row) === selected;
25844
+ }
25442
25845
  /** Default text rendering — em-dash for absent values, verbatim otherwise. */
25443
25846
  displayValue(row, col) {
25444
25847
  const value = resolveCellValue(row, col);
@@ -25455,12 +25858,18 @@ class FlyDataTableComponent {
25455
25858
  this.sortChange.emit(nextSort(this.sort(), col.id));
25456
25859
  }
25457
25860
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyDataTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
25458
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyDataTableComponent, isStandalone: true, selector: "fly-data-table", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: true, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelKey: { classPropertyName: "ariaLabelKey", publicName: "ariaLabelKey", isSignal: true, isRequired: false, transformFunction: null }, sort: { classPropertyName: "sort", publicName: "sort", isSignal: true, isRequired: false, transformFunction: null }, emptyKey: { classPropertyName: "emptyKey", publicName: "emptyKey", isSignal: true, isRequired: false, transformFunction: null }, emptyIconClass: { classPropertyName: "emptyIconClass", publicName: "emptyIconClass", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sortChange: "sortChange", rowActivated: "rowActivated" }, queries: [{ propertyName: "cellTemplates", predicate: FlyCellDirective, isSignal: true }], ngImport: i0, template: "<table class=\"dt\" [attr.aria-label]=\"ariaLabelKey() ? (ariaLabelKey()! | translate) : null\">\r\n <thead>\r\n <tr>\r\n @for (col of columns(); track col.id) {\r\n <th\r\n scope=\"col\"\r\n [class.dt__th--end]=\"col.align === 'end'\"\r\n [attr.aria-sort]=\"headerAriaSort(col)\"\r\n >\r\n @if (col.sortable) {\r\n <button type=\"button\" class=\"dt__sort\" (click)=\"onHeaderClick(col)\">\r\n <span>{{ col.headerKey | translate }}</span>\r\n <span class=\"dt__sort-icon\" [attr.data-dir]=\"sortDirFor(col)\" aria-hidden=\"true\"></span>\r\n </button>\r\n } @else {\r\n {{ col.headerKey | translate }}\r\n }\r\n </th>\r\n }\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @if (loading()) {\r\n @for (i of skeletonRows; track i) {\r\n <tr class=\"dt__row dt__row--skel\">\r\n <td [attr.colspan]=\"columns().length\">\r\n <!-- shape=\"lines\" is the DS skeleton's no-chrome text shimmer\r\n (the kit's former shape=\"text\"); one line per collapsed row.\r\n No backticks in here: this is an INLINE template literal. -->\r\n <fly-skeleton shape=\"lines\" [lines]=\"1\" />\r\n </td>\r\n </tr>\r\n }\r\n } @else {\r\n @for (row of rows(); track $index) {\r\n <tr\r\n class=\"dt__row\"\r\n tabindex=\"0\"\r\n (click)=\"onRowActivate($event, row)\"\r\n (keydown.enter)=\"onRowActivate($event, row)\"\r\n >\r\n @for (col of columns(); track col.id) {\r\n <td\r\n [class.dt__td--end]=\"col.align === 'end'\"\r\n [class.mono]=\"col.kind === 'mono'\"\r\n [class.dt__td--date]=\"col.kind === 'date'\"\r\n [class.dt__td--num]=\"col.kind === 'number'\"\r\n [attr.data-label]=\"col.headerKey | translate\"\r\n >\r\n @if (templateFor(col.id); as tpl) {\r\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: row }\" />\r\n } @else {\r\n @switch (col.kind ?? 'text') {\r\n @case ('date') {\r\n @if (cellValue(row, col); as value) {\r\n {{ $any(value) | date: 'yyyy-MM-dd' : 'UTC' }}\r\n } @else { \u2014 }\r\n }\r\n @default {\r\n {{ displayValue(row, col) }}\r\n }\r\n }\r\n }\r\n </td>\r\n }\r\n </tr>\r\n }\r\n }\r\n </tbody>\r\n</table>\r\n@if (!loading() && rows().length === 0 && emptyKey()) {\r\n <fly-state-message kind=\"empty\" [messageKey]=\"emptyKey()\" [iconClass]=\"emptyIconClass()\" />\r\n}\r\n", styles: ["@charset \"UTF-8\";:host{display:block;background:var(--bg-2);border:1px solid var(--w08);border-radius:var(--r-lg);overflow:hidden;box-shadow:var(--shadow-card);container-type:inline-size}.dt{width:100%;border-collapse:collapse;font-size:var(--text-base)}.dt thead th{text-align:start;padding:var(--sp-2) var(--sp-3);font-size:12px;font-weight:var(--fw-bold);color:var(--ink-3);background:var(--bg-3);border-block-end:1px solid var(--w1)}.dt__th--end{text-align:end}.dt__sort{display:inline-flex;align-items:center;gap:var(--sp-1);padding:0;border:0;background:transparent;font:inherit;letter-spacing:inherit;text-transform:inherit;color:inherit;cursor:pointer}.dt__sort:hover{color:var(--ink)}.dt__sort:focus-visible{outline:2px solid;outline-color:var(--focus-ring);outline-offset:2px}.dt__sort-icon{inline-size:0;block-size:0;border-inline:4px solid transparent;border-block-start:5px solid var(--ink-4);opacity:.45;transition:transform var(--t-state),opacity var(--t-state)}.dt__sort-icon[data-dir=asc]{opacity:1;border-block-start-color:var(--accent);transform:rotate(180deg)}.dt__sort-icon[data-dir=desc]{opacity:1;border-block-start-color:var(--accent)}.dt tbody td{height:var(--row-h);padding:var(--sp-2) var(--sp-3);border-block-end:1px solid var(--line-3);color:var(--ink);transition:background var(--t-state)}.dt tbody tr:last-child td{border-block-end:0}.dt__row{cursor:pointer}.dt__row:hover td{background:var(--bg-hover)}.dt__row:focus-visible{outline:2px solid;outline-color:var(--focus-ring);outline-offset:2px;outline-offset:-2px}.dt__row--skel{cursor:default}.dt__row--skel:hover td{background:transparent}.dt__td--end{text-align:end}.dt__td--date,.dt__td--num{color:var(--ink-3);font-size:var(--text-sm);font-variant-numeric:tabular-nums}@container (width <= 720px){.dt thead{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.dt,.dt tbody,.dt tbody tr,.dt tbody td{display:block}.dt__row{padding:var(--sp-2) 0;border-block-end:1px solid var(--line-3)}.dt__row:last-child{border-block-end:0}.dt tbody td{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-3);height:auto;border-block-end:0;padding:var(--sp-1) var(--sp-3);text-align:end}.dt tbody td:before{content:attr(data-label);flex-shrink:0;font-size:var(--text-xs);font-weight:var(--fw-medium);color:var(--ink-4);text-align:start}}@media(width<=720px){.dt thead{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.dt,.dt tbody,.dt tbody tr,.dt tbody td{display:block}.dt__row{padding:var(--sp-2) 0;border-block-end:1px solid var(--line-3)}.dt__row:last-child{border-block-end:0}.dt tbody td{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-3);height:auto;border-block-end:0;padding:var(--sp-1) var(--sp-3);text-align:end}.dt tbody td:before{content:attr(data-label);flex-shrink:0;font-size:var(--text-xs);font-weight:var(--fw-medium);color:var(--ink-4);text-align:start}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: FlyStateMessageComponent, selector: "fly-state-message", inputs: ["kind", "messageKey", "iconClass", "noteKey", "retryLabelKey", "showRetry"], outputs: ["retried"] }, { kind: "component", type: FlySkeletonComponent, selector: "fly-skeleton", inputs: ["layout", "shape", "count", "lines", "animation", "fill", "messageKey", "announce"] }, { kind: "pipe", type: DatePipe, name: "date" }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
25861
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyDataTableComponent, isStandalone: true, selector: "fly-data-table", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: true, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelKey: { classPropertyName: "ariaLabelKey", publicName: "ariaLabelKey", isSignal: true, isRequired: false, transformFunction: null }, sort: { classPropertyName: "sort", publicName: "sort", isSignal: true, isRequired: false, transformFunction: null }, emptyKey: { classPropertyName: "emptyKey", publicName: "emptyKey", isSignal: true, isRequired: false, transformFunction: null }, emptyIconClass: { classPropertyName: "emptyIconClass", publicName: "emptyIconClass", isSignal: true, isRequired: false, transformFunction: null }, selectedId: { classPropertyName: "selectedId", publicName: "selectedId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sortChange: "sortChange", rowActivated: "rowActivated" }, queries: [{ propertyName: "cellTemplates", predicate: FlyCellDirective, isSignal: true }], ngImport: i0, template: "<table class=\"dt\" [attr.aria-label]=\"ariaLabelKey() ? (ariaLabelKey()! | translate) : null\">\n <thead>\n <tr>\n @for (col of columns(); track col.id) {\n <th\n scope=\"col\"\n [class.dt__th--end]=\"col.align === 'end'\"\n [attr.aria-sort]=\"headerAriaSort(col)\"\n >\n @if (col.sortable) {\n <button type=\"button\" class=\"dt__sort\" (click)=\"onHeaderClick(col)\">\n <span>{{ col.headerKey | translate }}</span>\n <span\n class=\"dt__sort-icon\"\n [attr.data-dir]=\"sortDirFor(col)\"\n aria-hidden=\"true\"\n ></span>\n </button>\n } @else {\n {{ col.headerKey | translate }}\n }\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @if (loading()) {\n @for (i of skeletonRows; track i) {\n <tr class=\"dt__row dt__row--skel\">\n <td [attr.colspan]=\"columns().length\">\n <!-- shape=\"lines\" is the DS skeleton's no-chrome text shimmer\n (the kit's former shape=\"text\"); one line per collapsed row.\n No backticks in here: this is an INLINE template literal. -->\n <fly-skeleton shape=\"lines\" [lines]=\"1\" />\n </td>\n </tr>\n }\n } @else {\n @for (row of rows(); track $index) {\n <tr\n class=\"dt__row\"\n [class.dt__row--selected]=\"isSelected(row)\"\n [attr.aria-selected]=\"isSelected(row) ? 'true' : null\"\n tabindex=\"0\"\n (click)=\"onRowActivate($event, row)\"\n (keydown.enter)=\"onRowActivate($event, row)\"\n >\n @for (col of columns(); track col.id) {\n <td\n [class.dt__td--end]=\"col.align === 'end'\"\n [class.mono]=\"col.kind === 'mono'\"\n [class.dt__td--date]=\"col.kind === 'date'\"\n [class.dt__td--num]=\"col.kind === 'number'\"\n [attr.data-label]=\"col.headerKey | translate\"\n >\n @if (templateFor(col.id); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: row }\" />\n } @else {\n @switch (col.kind ?? 'text') {\n @case ('date') {\n @if (cellValue(row, col); as value) {\n {{ $any(value) | date: 'yyyy-MM-dd' : 'UTC' }}\n } @else {\n \u2014\n }\n }\n @default {\n {{ displayValue(row, col) }}\n }\n }\n }\n </td>\n }\n </tr>\n }\n }\n </tbody>\n</table>\n@if (!loading() && rows().length === 0 && emptyKey()) {\n <fly-state-message kind=\"empty\" [messageKey]=\"emptyKey()\" [iconClass]=\"emptyIconClass()\" />\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;background:var(--bg-2);border:1px solid var(--w08);border-radius:var(--r-lg);overflow:hidden;box-shadow:var(--shadow-card);container-type:inline-size}.dt{width:100%;border-collapse:collapse;font-size:var(--text-base)}.dt thead th{text-align:start;padding:var(--sp-2) var(--sp-3);font-size:12px;font-weight:var(--fw-bold);color:var(--ink-3);background:var(--bg-3);border-block-end:1px solid var(--w1)}.dt__th--end{text-align:end}.dt__sort{display:inline-flex;align-items:center;gap:var(--sp-1);padding:0;border:0;background:transparent;font:inherit;letter-spacing:inherit;text-transform:inherit;color:inherit;cursor:pointer}.dt__sort:hover{color:var(--ink)}.dt__sort:focus-visible{outline:2px solid;outline-color:var(--focus-ring);outline-offset:2px}.dt__sort-icon{inline-size:0;block-size:0;border-inline:4px solid transparent;border-block-start:5px solid var(--ink-4);opacity:.45;transition:transform var(--t-state),opacity var(--t-state)}.dt__sort-icon[data-dir=asc]{opacity:1;border-block-start-color:var(--accent);transform:rotate(180deg)}.dt__sort-icon[data-dir=desc]{opacity:1;border-block-start-color:var(--accent)}.dt tbody td{height:var(--row-h);padding:var(--sp-2) var(--sp-3);border-block-end:1px solid var(--line-3);color:var(--ink);transition:background var(--t-state)}.dt tbody tr:last-child td{border-block-end:0}.dt__row{cursor:pointer}.dt__row:hover td{background:var(--bg-hover)}.dt__row:focus-visible{outline:2px solid;outline-color:var(--focus-ring);outline-offset:2px;outline-offset:-2px}.dt__row--selected td,.dt__row--selected:hover td{background:var(--tint-sel)}.dt__row--skel{cursor:default}.dt__row--skel:hover td{background:transparent}.dt__td--end{text-align:end}.dt__td--date,.dt__td--num{color:var(--ink-3);font-size:var(--text-sm);font-variant-numeric:tabular-nums}@container (width <= 720px){.dt thead{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.dt,.dt tbody,.dt tbody tr,.dt tbody td{display:block}.dt__row{padding:var(--sp-2) 0;border-block-end:1px solid var(--line-3)}.dt__row:last-child{border-block-end:0}.dt tbody td{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-3);height:auto;border-block-end:0;padding:var(--sp-1) var(--sp-3);text-align:end}.dt tbody td:before{content:attr(data-label);flex-shrink:0;font-size:var(--text-xs);font-weight:var(--fw-medium);color:var(--ink-4);text-align:start}}@media(width<=720px){.dt thead{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.dt,.dt tbody,.dt tbody tr,.dt tbody td{display:block}.dt__row{padding:var(--sp-2) 0;border-block-end:1px solid var(--line-3)}.dt__row:last-child{border-block-end:0}.dt tbody td{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-3);height:auto;border-block-end:0;padding:var(--sp-1) var(--sp-3);text-align:end}.dt tbody td:before{content:attr(data-label);flex-shrink:0;font-size:var(--text-xs);font-weight:var(--fw-medium);color:var(--ink-4);text-align:start}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: FlyStateMessageComponent, selector: "fly-state-message", inputs: ["kind", "messageKey", "iconClass", "noteKey", "retryLabelKey", "showRetry"], outputs: ["retried"] }, { kind: "component", type: FlySkeletonComponent, selector: "fly-skeleton", inputs: ["layout", "shape", "count", "lines", "animation", "fill", "messageKey", "announce"] }, { kind: "pipe", type: DatePipe, name: "date" }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
25459
25862
  }
25460
25863
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyDataTableComponent, decorators: [{
25461
25864
  type: Component,
25462
- args: [{ selector: 'fly-data-table', standalone: true, imports: [DatePipe, NgTemplateOutlet, TranslatePipe, FlyStateMessageComponent, FlySkeletonComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<table class=\"dt\" [attr.aria-label]=\"ariaLabelKey() ? (ariaLabelKey()! | translate) : null\">\r\n <thead>\r\n <tr>\r\n @for (col of columns(); track col.id) {\r\n <th\r\n scope=\"col\"\r\n [class.dt__th--end]=\"col.align === 'end'\"\r\n [attr.aria-sort]=\"headerAriaSort(col)\"\r\n >\r\n @if (col.sortable) {\r\n <button type=\"button\" class=\"dt__sort\" (click)=\"onHeaderClick(col)\">\r\n <span>{{ col.headerKey | translate }}</span>\r\n <span class=\"dt__sort-icon\" [attr.data-dir]=\"sortDirFor(col)\" aria-hidden=\"true\"></span>\r\n </button>\r\n } @else {\r\n {{ col.headerKey | translate }}\r\n }\r\n </th>\r\n }\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @if (loading()) {\r\n @for (i of skeletonRows; track i) {\r\n <tr class=\"dt__row dt__row--skel\">\r\n <td [attr.colspan]=\"columns().length\">\r\n <!-- shape=\"lines\" is the DS skeleton's no-chrome text shimmer\r\n (the kit's former shape=\"text\"); one line per collapsed row.\r\n No backticks in here: this is an INLINE template literal. -->\r\n <fly-skeleton shape=\"lines\" [lines]=\"1\" />\r\n </td>\r\n </tr>\r\n }\r\n } @else {\r\n @for (row of rows(); track $index) {\r\n <tr\r\n class=\"dt__row\"\r\n tabindex=\"0\"\r\n (click)=\"onRowActivate($event, row)\"\r\n (keydown.enter)=\"onRowActivate($event, row)\"\r\n >\r\n @for (col of columns(); track col.id) {\r\n <td\r\n [class.dt__td--end]=\"col.align === 'end'\"\r\n [class.mono]=\"col.kind === 'mono'\"\r\n [class.dt__td--date]=\"col.kind === 'date'\"\r\n [class.dt__td--num]=\"col.kind === 'number'\"\r\n [attr.data-label]=\"col.headerKey | translate\"\r\n >\r\n @if (templateFor(col.id); as tpl) {\r\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: row }\" />\r\n } @else {\r\n @switch (col.kind ?? 'text') {\r\n @case ('date') {\r\n @if (cellValue(row, col); as value) {\r\n {{ $any(value) | date: 'yyyy-MM-dd' : 'UTC' }}\r\n } @else { \u2014 }\r\n }\r\n @default {\r\n {{ displayValue(row, col) }}\r\n }\r\n }\r\n }\r\n </td>\r\n }\r\n </tr>\r\n }\r\n }\r\n </tbody>\r\n</table>\r\n@if (!loading() && rows().length === 0 && emptyKey()) {\r\n <fly-state-message kind=\"empty\" [messageKey]=\"emptyKey()\" [iconClass]=\"emptyIconClass()\" />\r\n}\r\n", styles: ["@charset \"UTF-8\";:host{display:block;background:var(--bg-2);border:1px solid var(--w08);border-radius:var(--r-lg);overflow:hidden;box-shadow:var(--shadow-card);container-type:inline-size}.dt{width:100%;border-collapse:collapse;font-size:var(--text-base)}.dt thead th{text-align:start;padding:var(--sp-2) var(--sp-3);font-size:12px;font-weight:var(--fw-bold);color:var(--ink-3);background:var(--bg-3);border-block-end:1px solid var(--w1)}.dt__th--end{text-align:end}.dt__sort{display:inline-flex;align-items:center;gap:var(--sp-1);padding:0;border:0;background:transparent;font:inherit;letter-spacing:inherit;text-transform:inherit;color:inherit;cursor:pointer}.dt__sort:hover{color:var(--ink)}.dt__sort:focus-visible{outline:2px solid;outline-color:var(--focus-ring);outline-offset:2px}.dt__sort-icon{inline-size:0;block-size:0;border-inline:4px solid transparent;border-block-start:5px solid var(--ink-4);opacity:.45;transition:transform var(--t-state),opacity var(--t-state)}.dt__sort-icon[data-dir=asc]{opacity:1;border-block-start-color:var(--accent);transform:rotate(180deg)}.dt__sort-icon[data-dir=desc]{opacity:1;border-block-start-color:var(--accent)}.dt tbody td{height:var(--row-h);padding:var(--sp-2) var(--sp-3);border-block-end:1px solid var(--line-3);color:var(--ink);transition:background var(--t-state)}.dt tbody tr:last-child td{border-block-end:0}.dt__row{cursor:pointer}.dt__row:hover td{background:var(--bg-hover)}.dt__row:focus-visible{outline:2px solid;outline-color:var(--focus-ring);outline-offset:2px;outline-offset:-2px}.dt__row--skel{cursor:default}.dt__row--skel:hover td{background:transparent}.dt__td--end{text-align:end}.dt__td--date,.dt__td--num{color:var(--ink-3);font-size:var(--text-sm);font-variant-numeric:tabular-nums}@container (width <= 720px){.dt thead{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.dt,.dt tbody,.dt tbody tr,.dt tbody td{display:block}.dt__row{padding:var(--sp-2) 0;border-block-end:1px solid var(--line-3)}.dt__row:last-child{border-block-end:0}.dt tbody td{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-3);height:auto;border-block-end:0;padding:var(--sp-1) var(--sp-3);text-align:end}.dt tbody td:before{content:attr(data-label);flex-shrink:0;font-size:var(--text-xs);font-weight:var(--fw-medium);color:var(--ink-4);text-align:start}}@media(width<=720px){.dt thead{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.dt,.dt tbody,.dt tbody tr,.dt tbody td{display:block}.dt__row{padding:var(--sp-2) 0;border-block-end:1px solid var(--line-3)}.dt__row:last-child{border-block-end:0}.dt tbody td{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-3);height:auto;border-block-end:0;padding:var(--sp-1) var(--sp-3);text-align:end}.dt tbody td:before{content:attr(data-label);flex-shrink:0;font-size:var(--text-xs);font-weight:var(--fw-medium);color:var(--ink-4);text-align:start}}\n"] }]
25463
- }], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: true }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], ariaLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelKey", required: false }] }], sort: [{ type: i0.Input, args: [{ isSignal: true, alias: "sort", required: false }] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], rowActivated: [{ type: i0.Output, args: ["rowActivated"] }], emptyKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyKey", required: false }] }], emptyIconClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyIconClass", required: false }] }], cellTemplates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => FlyCellDirective), { isSignal: true }] }] } });
25865
+ args: [{ selector: 'fly-data-table', standalone: true, imports: [
25866
+ DatePipe,
25867
+ NgTemplateOutlet,
25868
+ TranslatePipe,
25869
+ FlyStateMessageComponent,
25870
+ FlySkeletonComponent,
25871
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<table class=\"dt\" [attr.aria-label]=\"ariaLabelKey() ? (ariaLabelKey()! | translate) : null\">\n <thead>\n <tr>\n @for (col of columns(); track col.id) {\n <th\n scope=\"col\"\n [class.dt__th--end]=\"col.align === 'end'\"\n [attr.aria-sort]=\"headerAriaSort(col)\"\n >\n @if (col.sortable) {\n <button type=\"button\" class=\"dt__sort\" (click)=\"onHeaderClick(col)\">\n <span>{{ col.headerKey | translate }}</span>\n <span\n class=\"dt__sort-icon\"\n [attr.data-dir]=\"sortDirFor(col)\"\n aria-hidden=\"true\"\n ></span>\n </button>\n } @else {\n {{ col.headerKey | translate }}\n }\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @if (loading()) {\n @for (i of skeletonRows; track i) {\n <tr class=\"dt__row dt__row--skel\">\n <td [attr.colspan]=\"columns().length\">\n <!-- shape=\"lines\" is the DS skeleton's no-chrome text shimmer\n (the kit's former shape=\"text\"); one line per collapsed row.\n No backticks in here: this is an INLINE template literal. -->\n <fly-skeleton shape=\"lines\" [lines]=\"1\" />\n </td>\n </tr>\n }\n } @else {\n @for (row of rows(); track $index) {\n <tr\n class=\"dt__row\"\n [class.dt__row--selected]=\"isSelected(row)\"\n [attr.aria-selected]=\"isSelected(row) ? 'true' : null\"\n tabindex=\"0\"\n (click)=\"onRowActivate($event, row)\"\n (keydown.enter)=\"onRowActivate($event, row)\"\n >\n @for (col of columns(); track col.id) {\n <td\n [class.dt__td--end]=\"col.align === 'end'\"\n [class.mono]=\"col.kind === 'mono'\"\n [class.dt__td--date]=\"col.kind === 'date'\"\n [class.dt__td--num]=\"col.kind === 'number'\"\n [attr.data-label]=\"col.headerKey | translate\"\n >\n @if (templateFor(col.id); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: row }\" />\n } @else {\n @switch (col.kind ?? 'text') {\n @case ('date') {\n @if (cellValue(row, col); as value) {\n {{ $any(value) | date: 'yyyy-MM-dd' : 'UTC' }}\n } @else {\n \u2014\n }\n }\n @default {\n {{ displayValue(row, col) }}\n }\n }\n }\n </td>\n }\n </tr>\n }\n }\n </tbody>\n</table>\n@if (!loading() && rows().length === 0 && emptyKey()) {\n <fly-state-message kind=\"empty\" [messageKey]=\"emptyKey()\" [iconClass]=\"emptyIconClass()\" />\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;background:var(--bg-2);border:1px solid var(--w08);border-radius:var(--r-lg);overflow:hidden;box-shadow:var(--shadow-card);container-type:inline-size}.dt{width:100%;border-collapse:collapse;font-size:var(--text-base)}.dt thead th{text-align:start;padding:var(--sp-2) var(--sp-3);font-size:12px;font-weight:var(--fw-bold);color:var(--ink-3);background:var(--bg-3);border-block-end:1px solid var(--w1)}.dt__th--end{text-align:end}.dt__sort{display:inline-flex;align-items:center;gap:var(--sp-1);padding:0;border:0;background:transparent;font:inherit;letter-spacing:inherit;text-transform:inherit;color:inherit;cursor:pointer}.dt__sort:hover{color:var(--ink)}.dt__sort:focus-visible{outline:2px solid;outline-color:var(--focus-ring);outline-offset:2px}.dt__sort-icon{inline-size:0;block-size:0;border-inline:4px solid transparent;border-block-start:5px solid var(--ink-4);opacity:.45;transition:transform var(--t-state),opacity var(--t-state)}.dt__sort-icon[data-dir=asc]{opacity:1;border-block-start-color:var(--accent);transform:rotate(180deg)}.dt__sort-icon[data-dir=desc]{opacity:1;border-block-start-color:var(--accent)}.dt tbody td{height:var(--row-h);padding:var(--sp-2) var(--sp-3);border-block-end:1px solid var(--line-3);color:var(--ink);transition:background var(--t-state)}.dt tbody tr:last-child td{border-block-end:0}.dt__row{cursor:pointer}.dt__row:hover td{background:var(--bg-hover)}.dt__row:focus-visible{outline:2px solid;outline-color:var(--focus-ring);outline-offset:2px;outline-offset:-2px}.dt__row--selected td,.dt__row--selected:hover td{background:var(--tint-sel)}.dt__row--skel{cursor:default}.dt__row--skel:hover td{background:transparent}.dt__td--end{text-align:end}.dt__td--date,.dt__td--num{color:var(--ink-3);font-size:var(--text-sm);font-variant-numeric:tabular-nums}@container (width <= 720px){.dt thead{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.dt,.dt tbody,.dt tbody tr,.dt tbody td{display:block}.dt__row{padding:var(--sp-2) 0;border-block-end:1px solid var(--line-3)}.dt__row:last-child{border-block-end:0}.dt tbody td{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-3);height:auto;border-block-end:0;padding:var(--sp-1) var(--sp-3);text-align:end}.dt tbody td:before{content:attr(data-label);flex-shrink:0;font-size:var(--text-xs);font-weight:var(--fw-medium);color:var(--ink-4);text-align:start}}@media(width<=720px){.dt thead{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.dt,.dt tbody,.dt tbody tr,.dt tbody td{display:block}.dt__row{padding:var(--sp-2) 0;border-block-end:1px solid var(--line-3)}.dt__row:last-child{border-block-end:0}.dt tbody td{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-3);height:auto;border-block-end:0;padding:var(--sp-1) var(--sp-3);text-align:end}.dt tbody td:before{content:attr(data-label);flex-shrink:0;font-size:var(--text-xs);font-weight:var(--fw-medium);color:var(--ink-4);text-align:start}}\n"] }]
25872
+ }], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: true }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], ariaLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelKey", required: false }] }], sort: [{ type: i0.Input, args: [{ isSignal: true, alias: "sort", required: false }] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], rowActivated: [{ type: i0.Output, args: ["rowActivated"] }], emptyKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyKey", required: false }] }], emptyIconClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyIconClass", required: false }] }], selectedId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedId", required: false }] }], cellTemplates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => FlyCellDirective), { isSignal: true }] }] } });
25464
25873
 
25465
25874
  /**
25466
25875
  * Plain-signals factory (no DI) replacing the per-page boilerplate of
@@ -25854,30 +26263,30 @@ class FlyDetailCardComponent {
25854
26263
  /** Drop the body padding — for a child that brings its own list/table chrome. */
25855
26264
  flush = input(false, ...(ngDevMode ? [{ debugName: "flush" }] : /* istanbul ignore next */ []));
25856
26265
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyDetailCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
25857
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyDetailCardComponent, isStandalone: true, selector: "fly-detail-card", inputs: { titleKey: { classPropertyName: "titleKey", publicName: "titleKey", isSignal: true, isRequired: false, transformFunction: null }, hasProjectedTitle: { classPropertyName: "hasProjectedTitle", publicName: "hasProjectedTitle", isSignal: true, isRequired: false, transformFunction: null }, flush: { classPropertyName: "flush", publicName: "flush", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
25858
- @if (titleKey() || hasProjectedTitle()) {
25859
- <fly-section-header [titleKey]="titleKey()">
25860
- <ng-content select="[card-title]" ngProjectAs="[section-title]" />
25861
- <ng-content select="[card-actions]" ngProjectAs="[section-actions]" />
25862
- </fly-section-header>
25863
- }
25864
- <div class="dc__body" [class.dc__body--flush]="flush()">
25865
- <ng-content />
25866
- </div>
26266
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyDetailCardComponent, isStandalone: true, selector: "fly-detail-card", inputs: { titleKey: { classPropertyName: "titleKey", publicName: "titleKey", isSignal: true, isRequired: false, transformFunction: null }, hasProjectedTitle: { classPropertyName: "hasProjectedTitle", publicName: "hasProjectedTitle", isSignal: true, isRequired: false, transformFunction: null }, flush: { classPropertyName: "flush", publicName: "flush", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
26267
+ @if (titleKey() || hasProjectedTitle()) {
26268
+ <fly-section-header [titleKey]="titleKey()">
26269
+ <ng-content select="[card-title]" ngProjectAs="[section-title]" />
26270
+ <ng-content select="[card-actions]" ngProjectAs="[section-actions]" />
26271
+ </fly-section-header>
26272
+ }
26273
+ <div class="dc__body" [class.dc__body--flush]="flush()">
26274
+ <ng-content />
26275
+ </div>
25867
26276
  `, isInline: true, styles: [":host{display:block;background:var(--bg-2);border:1px solid var(--w08);border-radius:var(--r-lg);box-shadow:var(--shadow-card);overflow:hidden;animation:itemIn .3s var(--nova-ease-micro) both}.dc__body{padding:var(--sp-3) var(--sp-4) var(--sp-4)}.dc__body--flush{padding:0}\n"], dependencies: [{ kind: "component", type: FlySectionHeaderComponent, selector: "fly-section-header", inputs: ["titleKey"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
25868
26277
  }
25869
26278
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyDetailCardComponent, decorators: [{
25870
26279
  type: Component,
25871
- args: [{ selector: 'fly-detail-card', standalone: true, imports: [FlySectionHeaderComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
25872
- @if (titleKey() || hasProjectedTitle()) {
25873
- <fly-section-header [titleKey]="titleKey()">
25874
- <ng-content select="[card-title]" ngProjectAs="[section-title]" />
25875
- <ng-content select="[card-actions]" ngProjectAs="[section-actions]" />
25876
- </fly-section-header>
25877
- }
25878
- <div class="dc__body" [class.dc__body--flush]="flush()">
25879
- <ng-content />
25880
- </div>
26280
+ args: [{ selector: 'fly-detail-card', standalone: true, imports: [FlySectionHeaderComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
26281
+ @if (titleKey() || hasProjectedTitle()) {
26282
+ <fly-section-header [titleKey]="titleKey()">
26283
+ <ng-content select="[card-title]" ngProjectAs="[section-title]" />
26284
+ <ng-content select="[card-actions]" ngProjectAs="[section-actions]" />
26285
+ </fly-section-header>
26286
+ }
26287
+ <div class="dc__body" [class.dc__body--flush]="flush()">
26288
+ <ng-content />
26289
+ </div>
25881
26290
  `, styles: [":host{display:block;background:var(--bg-2);border:1px solid var(--w08);border-radius:var(--r-lg);box-shadow:var(--shadow-card);overflow:hidden;animation:itemIn .3s var(--nova-ease-micro) both}.dc__body{padding:var(--sp-3) var(--sp-4) var(--sp-4)}.dc__body--flush{padding:0}\n"] }]
25882
26291
  }], propDecorators: { titleKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleKey", required: false }] }], hasProjectedTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasProjectedTitle", required: false }] }], flush: [{ type: i0.Input, args: [{ isSignal: true, alias: "flush", required: false }] }] } });
25883
26292
 
@@ -26005,108 +26414,108 @@ class FlyDetailShellComponent {
26005
26414
  buttons?.[next]?.focus();
26006
26415
  }
26007
26416
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyDetailShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
26008
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyDetailShellComponent, isStandalone: true, selector: "fly-detail-shell", inputs: { sections: { classPropertyName: "sections", publicName: "sections", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, sectionsLabelKey: { classPropertyName: "sectionsLabelKey", publicName: "sectionsLabelKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selected: "selectedChange" }, viewQueries: [{ propertyName: "rail", first: true, predicate: ["rail"], descendants: true, isSignal: true }], ngImport: i0, template: `
26009
- <div class="ds__layout">
26010
- <aside class="ds__aside">
26011
- <div class="ds__pinned">
26012
- <ng-content select="[detail-aside]" />
26013
- </div>
26014
-
26015
- @if (sections().length > 0) {
26016
- <div
26017
- #rail
26018
- class="ds__rail"
26019
- role="tablist"
26020
- tabindex="-1"
26021
- [attr.aria-orientation]="'vertical'"
26022
- [attr.aria-label]="sectionsLabelKey() | translate"
26023
- (keydown)="onKey($event)"
26024
- >
26025
- @for (s of sections(); track s.id) {
26026
- <button
26027
- type="button"
26028
- class="ds__tab"
26029
- role="tab"
26030
- [id]="tabId(s.id)"
26031
- [class.ds__tab--active]="activeId() === s.id"
26032
- [attr.aria-selected]="activeId() === s.id"
26033
- [attr.aria-controls]="panelId()"
26034
- [attr.tabindex]="activeId() === s.id ? 0 : -1"
26035
- [attr.title]="s.labelKey | translate"
26036
- (click)="select(s.id)"
26037
- >
26038
- @if (s.icon) {
26039
- <span class="pi {{ s.icon }} ds__tab-ico" aria-hidden="true"></span>
26040
- }
26041
- <span class="ds__tab-label">{{ s.labelKey | translate }}</span>
26042
- </button>
26043
- }
26044
- </div>
26045
- }
26046
- </aside>
26047
-
26048
- <div
26049
- class="ds__panel"
26050
- [id]="panelId()"
26051
- [attr.role]="sections().length ? 'tabpanel' : null"
26052
- [attr.aria-labelledby]="activeId() ? tabId(activeId()!) : null"
26053
- >
26054
- <ng-content />
26055
- </div>
26056
- </div>
26417
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyDetailShellComponent, isStandalone: true, selector: "fly-detail-shell", inputs: { sections: { classPropertyName: "sections", publicName: "sections", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, sectionsLabelKey: { classPropertyName: "sectionsLabelKey", publicName: "sectionsLabelKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selected: "selectedChange" }, viewQueries: [{ propertyName: "rail", first: true, predicate: ["rail"], descendants: true, isSignal: true }], ngImport: i0, template: `
26418
+ <div class="ds__layout">
26419
+ <aside class="ds__aside">
26420
+ <div class="ds__pinned">
26421
+ <ng-content select="[detail-aside]" />
26422
+ </div>
26423
+
26424
+ @if (sections().length > 0) {
26425
+ <div
26426
+ #rail
26427
+ class="ds__rail"
26428
+ role="tablist"
26429
+ tabindex="-1"
26430
+ [attr.aria-orientation]="'vertical'"
26431
+ [attr.aria-label]="sectionsLabelKey() | translate"
26432
+ (keydown)="onKey($event)"
26433
+ >
26434
+ @for (s of sections(); track s.id) {
26435
+ <button
26436
+ type="button"
26437
+ class="ds__tab"
26438
+ role="tab"
26439
+ [id]="tabId(s.id)"
26440
+ [class.ds__tab--active]="activeId() === s.id"
26441
+ [attr.aria-selected]="activeId() === s.id"
26442
+ [attr.aria-controls]="panelId()"
26443
+ [attr.tabindex]="activeId() === s.id ? 0 : -1"
26444
+ [attr.title]="s.labelKey | translate"
26445
+ (click)="select(s.id)"
26446
+ >
26447
+ @if (s.icon) {
26448
+ <span class="pi {{ s.icon }} ds__tab-ico" aria-hidden="true"></span>
26449
+ }
26450
+ <span class="ds__tab-label">{{ s.labelKey | translate }}</span>
26451
+ </button>
26452
+ }
26453
+ </div>
26454
+ }
26455
+ </aside>
26456
+
26457
+ <div
26458
+ class="ds__panel"
26459
+ [id]="panelId()"
26460
+ [attr.role]="sections().length ? 'tabpanel' : null"
26461
+ [attr.aria-labelledby]="activeId() ? tabId(activeId()!) : null"
26462
+ >
26463
+ <ng-content />
26464
+ </div>
26465
+ </div>
26057
26466
  `, isInline: true, styles: [":host{display:block;container-type:inline-size}.ds__layout{display:grid;grid-template-columns:var(--fly-detail-aside, 300px) minmax(0,1fr);gap:var(--sp-4);align-items:start}.ds__aside{display:flex;flex-direction:column;gap:var(--sp-4);min-width:0}.ds__aside .ds__rail{order:-1}.ds__pinned{display:flex;flex-direction:column;gap:var(--sp-4);min-width:0}.ds__panel{display:flex;flex-direction:column;gap:var(--sp-4);min-width:0;align-self:stretch}.ds__panel>:only-child{flex:1}.ds__rail{display:flex;flex-direction:column;gap:var(--sp-1);padding:var(--sp-2);background:var(--bg-2);border:1px solid var(--w08);border-radius:var(--r-lg);box-shadow:var(--shadow-card)}.ds__tab{display:flex;align-items:center;gap:var(--sp-3);width:100%;padding:var(--sp-2);border:0;background:transparent;border-radius:var(--r-md);color:var(--ink-2);cursor:pointer;font-family:inherit;font-size:var(--text-sm);text-align:start;white-space:nowrap;overflow:hidden;transition:background var(--t-state),color var(--t-state)}.ds__tab:hover{background:var(--w06);color:var(--ink)}.ds__tab:focus-visible{outline:2px solid;outline-color:var(--focus-ring);outline-offset:2px}.ds__tab--active{background:var(--accent-fill);color:var(--on-accent-fill);font-weight:var(--fw-semibold)}.ds__tab-ico{font-size:var(--text-md);flex-shrink:0;width:20px;text-align:center}.ds__tab-label{min-width:0;overflow:hidden;text-overflow:ellipsis}@container (width <= 980px){.ds__layout{grid-template-columns:minmax(0,1fr)}.ds__rail{flex-flow:row wrap}.ds__tab{width:auto}}@media(width<=980px){.ds__layout{grid-template-columns:minmax(0,1fr)}.ds__rail{flex-flow:row wrap}.ds__tab{width:auto}}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
26058
26467
  }
26059
26468
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyDetailShellComponent, decorators: [{
26060
26469
  type: Component,
26061
- args: [{ selector: 'fly-detail-shell', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: `
26062
- <div class="ds__layout">
26063
- <aside class="ds__aside">
26064
- <div class="ds__pinned">
26065
- <ng-content select="[detail-aside]" />
26066
- </div>
26067
-
26068
- @if (sections().length > 0) {
26069
- <div
26070
- #rail
26071
- class="ds__rail"
26072
- role="tablist"
26073
- tabindex="-1"
26074
- [attr.aria-orientation]="'vertical'"
26075
- [attr.aria-label]="sectionsLabelKey() | translate"
26076
- (keydown)="onKey($event)"
26077
- >
26078
- @for (s of sections(); track s.id) {
26079
- <button
26080
- type="button"
26081
- class="ds__tab"
26082
- role="tab"
26083
- [id]="tabId(s.id)"
26084
- [class.ds__tab--active]="activeId() === s.id"
26085
- [attr.aria-selected]="activeId() === s.id"
26086
- [attr.aria-controls]="panelId()"
26087
- [attr.tabindex]="activeId() === s.id ? 0 : -1"
26088
- [attr.title]="s.labelKey | translate"
26089
- (click)="select(s.id)"
26090
- >
26091
- @if (s.icon) {
26092
- <span class="pi {{ s.icon }} ds__tab-ico" aria-hidden="true"></span>
26093
- }
26094
- <span class="ds__tab-label">{{ s.labelKey | translate }}</span>
26095
- </button>
26096
- }
26097
- </div>
26098
- }
26099
- </aside>
26100
-
26101
- <div
26102
- class="ds__panel"
26103
- [id]="panelId()"
26104
- [attr.role]="sections().length ? 'tabpanel' : null"
26105
- [attr.aria-labelledby]="activeId() ? tabId(activeId()!) : null"
26106
- >
26107
- <ng-content />
26108
- </div>
26109
- </div>
26470
+ args: [{ selector: 'fly-detail-shell', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: `
26471
+ <div class="ds__layout">
26472
+ <aside class="ds__aside">
26473
+ <div class="ds__pinned">
26474
+ <ng-content select="[detail-aside]" />
26475
+ </div>
26476
+
26477
+ @if (sections().length > 0) {
26478
+ <div
26479
+ #rail
26480
+ class="ds__rail"
26481
+ role="tablist"
26482
+ tabindex="-1"
26483
+ [attr.aria-orientation]="'vertical'"
26484
+ [attr.aria-label]="sectionsLabelKey() | translate"
26485
+ (keydown)="onKey($event)"
26486
+ >
26487
+ @for (s of sections(); track s.id) {
26488
+ <button
26489
+ type="button"
26490
+ class="ds__tab"
26491
+ role="tab"
26492
+ [id]="tabId(s.id)"
26493
+ [class.ds__tab--active]="activeId() === s.id"
26494
+ [attr.aria-selected]="activeId() === s.id"
26495
+ [attr.aria-controls]="panelId()"
26496
+ [attr.tabindex]="activeId() === s.id ? 0 : -1"
26497
+ [attr.title]="s.labelKey | translate"
26498
+ (click)="select(s.id)"
26499
+ >
26500
+ @if (s.icon) {
26501
+ <span class="pi {{ s.icon }} ds__tab-ico" aria-hidden="true"></span>
26502
+ }
26503
+ <span class="ds__tab-label">{{ s.labelKey | translate }}</span>
26504
+ </button>
26505
+ }
26506
+ </div>
26507
+ }
26508
+ </aside>
26509
+
26510
+ <div
26511
+ class="ds__panel"
26512
+ [id]="panelId()"
26513
+ [attr.role]="sections().length ? 'tabpanel' : null"
26514
+ [attr.aria-labelledby]="activeId() ? tabId(activeId()!) : null"
26515
+ >
26516
+ <ng-content />
26517
+ </div>
26518
+ </div>
26110
26519
  `, styles: [":host{display:block;container-type:inline-size}.ds__layout{display:grid;grid-template-columns:var(--fly-detail-aside, 300px) minmax(0,1fr);gap:var(--sp-4);align-items:start}.ds__aside{display:flex;flex-direction:column;gap:var(--sp-4);min-width:0}.ds__aside .ds__rail{order:-1}.ds__pinned{display:flex;flex-direction:column;gap:var(--sp-4);min-width:0}.ds__panel{display:flex;flex-direction:column;gap:var(--sp-4);min-width:0;align-self:stretch}.ds__panel>:only-child{flex:1}.ds__rail{display:flex;flex-direction:column;gap:var(--sp-1);padding:var(--sp-2);background:var(--bg-2);border:1px solid var(--w08);border-radius:var(--r-lg);box-shadow:var(--shadow-card)}.ds__tab{display:flex;align-items:center;gap:var(--sp-3);width:100%;padding:var(--sp-2);border:0;background:transparent;border-radius:var(--r-md);color:var(--ink-2);cursor:pointer;font-family:inherit;font-size:var(--text-sm);text-align:start;white-space:nowrap;overflow:hidden;transition:background var(--t-state),color var(--t-state)}.ds__tab:hover{background:var(--w06);color:var(--ink)}.ds__tab:focus-visible{outline:2px solid;outline-color:var(--focus-ring);outline-offset:2px}.ds__tab--active{background:var(--accent-fill);color:var(--on-accent-fill);font-weight:var(--fw-semibold)}.ds__tab-ico{font-size:var(--text-md);flex-shrink:0;width:20px;text-align:center}.ds__tab-label{min-width:0;overflow:hidden;text-overflow:ellipsis}@container (width <= 980px){.ds__layout{grid-template-columns:minmax(0,1fr)}.ds__rail{flex-flow:row wrap}.ds__tab{width:auto}}@media(width<=980px){.ds__layout{grid-template-columns:minmax(0,1fr)}.ds__rail{flex-flow:row wrap}.ds__tab{width:auto}}\n"] }]
26111
26520
  }], propDecorators: { rail: [{ type: i0.ViewChild, args: ['rail', { isSignal: true }] }], sections: [{ type: i0.Input, args: [{ isSignal: true, alias: "sections", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }, { type: i0.Output, args: ["selectedChange"] }], sectionsLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "sectionsLabelKey", required: false }] }] } });
26112
26521
 
@@ -28604,7 +29013,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
28604
29013
  * `FlyosPendingLaunches`. `LAUNCH_CONTEXT` is now documented as
28605
29014
  * shell-internal — Native Federation can split the InjectionToken across
28606
29015
  * host/remote bundles, so federated remotes must consume the
28607
- * event + registry surfaces. See `skills/cross-app-deep-linking.md`.
29016
+ * event + registry surfaces. See `skills/platform-deep-linking.md`.
28608
29017
  * Also: new `SourceAppResolver` service + `APP_LOOKUP` adapter token,
28609
29018
  * centralising the canonicalization rule that strips `-service` from
28610
29019
  * JWT client_ids (`circles-service` → `circles`) so chips, banners,
@@ -28635,7 +29044,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
28635
29044
  * `FLY_WINDOW_HELP_HINT_EVENT`, `FlyWindowHelpHintEventDetail`.
28636
29045
  * Backed by the help-center reader's new `params.topic` launch payload
28637
29046
  * and "no help for this app yet" empty-state. See
28638
- * `skills/help-center.md` and `skills/desktop-shell-angular.md`.
29047
+ * `skills/app-help-center.md` and `skills/shell-angular.md`.
28639
29048
  * v2.20.0: Centralized identity gating (Layers 1–3 of the auth-gate hardening).
28640
29049
  * `AuthService` is now the single identity source of truth and gains a
28641
29050
  * gate surface: `roles`, `isAdmin`, `hasAnyRole()`, plus `expiresAt` /
@@ -28656,7 +29065,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
28656
29065
  * re-applies on re-mount, mandatory `appId` filter, null-route no-op,
28657
29066
  * reload-safety via the shell's single-writer republish). Additive — old
28658
29067
  * hand-rolled receivers keep working. New exports: `connectRemoteLaunch`,
28659
- * `ConnectRemoteLaunchOptions`. See `skills/cross-app-deep-linking.md`.
29068
+ * `ConnectRemoteLaunchOptions`. See `skills/platform-deep-linking.md`.
28660
29069
  * v2.5.1: New `MessageBoxService.showAcknowledged()` entry point returning
28661
29070
  * `DialogResultWithAcknowledgement` for "Don't ask again"-style flows.
28662
29071
  * The `dontAskAgain` config (`MessageBoxDontAskAgainConfig`) lives ONLY on
@@ -28894,6 +29303,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
28894
29303
  * + `[(value)]`; `fly-empty-state` → `fly-state-message` `[kind]`/`[messageKey]`.
28895
29304
  * The `pagination.*` baseline-locale namespace went with them (the kit uses
28896
29305
  * `ui.pagination.*`); `breadcrumb.*` and `tree_nav.*` remain.
29306
+ * v3.15.0: `FlyLaunchContextService` — the launch **params**, which `connectRemoteLaunch`
29307
+ * used to drop. That helper's `navigate` callback takes a route STRING, so it
29308
+ * structurally cannot carry `ctx.params` — the payload dashboard drill-down
29309
+ * rides on (`skills/dashboard-reports.md` § "Drill-down": a report card declares
29310
+ * `{ appId, route, filterParam, valueFrom }` and the shell launches with
29311
+ * `params: { [filterParam]: clickedValue }`). Every drill-down into a federated
29312
+ * remote therefore landed on an UNFILTERED register, and the only workaround was
29313
+ * a per-app service non-destructively peeking the same `__FLYOS_PENDING_LAUNCHES__`
29314
+ * registry — with a load-bearing construction-ordering dependency on running
29315
+ * BEFORE `connectRemoteLaunch` drained (deleted) the entry. `LAUNCH_CONTEXT`, the
29316
+ * shell-side answer, is unusable from a remote (Native Federation splits the
29317
+ * token identity).
29318
+ *
29319
+ * `connectRemoteLaunch` now publishes every accepted (version-deduped) launch —
29320
+ * `route` AND `params`, including params-only launches whose route is `null` —
29321
+ * into `FlyLaunchContextService`, an appId-keyed, `globalThis`-backed store
29322
+ * (`FLY_LAUNCH_CONTEXT_STORE_KEY` / `FLY_LAUNCH_CONTEXT_EVENT`) on the same
29323
+ * substrate as `FlyRemoteContextService` and `MagicBarRegistry`, so a federation
29324
+ * version-negotiation split cannot present as "params only missing for remotes".
29325
+ * Read it from anywhere in the remote, at any time, with no ordering constraint:
29326
+ * `inject(FlyLaunchContextService).contextFor(appId)` (signal),
29327
+ * `.context(appId)` / `.params(appId)` (direct, split-proof reads). Entries are
29328
+ * NOT consumed on read — a lazily-mounted leaf must still see the launch that
29329
+ * opened the window — so consumers dedupe on `LaunchContext.version` exactly as
29330
+ * they already do for the live-event path. New `ConnectRemoteLaunchOptions.onLaunch`
29331
+ * is the callback form for a root-local reaction that needs no injectable.
29332
+ * Additive: `navigate` still fires exactly when it did. New exports:
29333
+ * `FlyLaunchContextService`, `FLY_LAUNCH_CONTEXT_STORE_KEY`,
29334
+ * `FLY_LAUNCH_CONTEXT_EVENT`. See `skills/cross-app-deep-linking.md`.
28897
29335
  * v1.8.0: `MagicBarRegistry` — the UX-v2 magic bar's contribution store. The active view
28898
29336
  * publishes what the shell's top-bar toolbar pill should offer (search, ordered
28899
29337
  * action groups, an accent primary CTA, an overflow set, and the ids of the
@@ -29019,5 +29457,5 @@ const AUDIENCE_ERROR_CODES = {
29019
29457
  * Generated bundle index. Do not edit.
29020
29458
  */
29021
29459
 
29022
- export { AGENT_DRAG_MIME, AGENT_PAYLOAD_VERSION, APP_LOOKUP, AUDIENCE_ERROR_CODES, AUDIENCE_LIMITS, AUDIENCE_PRESETS, AUDIENCE_TERM_KINDS, AgentActionBus, AgentActionUnsupportedDispatchError, AgentCommandRegistry, AgentDropRegistry, AgentFlightAnimator, AgentLookupRegistry, AgentPayloadOversizeError, AudienceBuilderComponent, AuthService, BilingualFieldComponent, CRON_MODES, CRON_WEEKDAYS, ContextMenuComponent, DEFAULT_AGENT_PAYLOAD_LIMITS, DEFAULT_FLY_THEME_MODE, DS_BASELINE_LOCALES, DashboardKpiComponent, DialogResult, ENTITY_LINK_LAUNCHER, EntityLookupComponent, FLYOS_LAUNCH_EVENT, FLYOS_LAUNCH_REQUEST_EVENT, FLYOS_REMOTE_ROUTE_EVENT, FLY_ACCENT_PROPERTY, FLY_ADMIN_ROLES, FLY_CHUNK_RELOAD_FLAG, FLY_COMPACT_THRESHOLD, FLY_COUNTRIES, FLY_CURRENCIES_BRIEF_ENDPOINT, FLY_CURRENCY_DEFAULT_ENDPOINT, FLY_EMOJI_BY_ID, FLY_EMOJI_CATEGORIES, FLY_EMOJI_CATEGORY_BY_ID, FLY_EMOJI_DEFAULT_CATEGORIES, FLY_EMOJI_PACK, FLY_EMOJI_PACK_MISSING_MESSAGE, FLY_EMOJI_PREVIEW_COUNT, FLY_EMPTY_VALUE, FLY_GANTT_DEFAULTS, FLY_LOCALE_CATALOG, FLY_MAGIC_BAR_EVENT, FLY_MAGIC_BAR_ICONS, FLY_MAGIC_BAR_STORE_KEY, FLY_NF_CACHE_HEAL_FLAG, FLY_RELOAD_DEBOUNCE_MS, FLY_RELOAD_REARM_MS, FLY_REMOTE_BASE_PATH, FLY_REMOTE_CONTEXT_EVENT, FLY_REMOTE_CONTEXT_STORE_KEY, FLY_REMOTE_OUTLET_DEPTH, FLY_REMOTE_ROUTES, FLY_SCAN_PENDING_DEFAULT_DELAY_MS, FLY_SCAN_PENDING_MAX_RETRIES, FLY_SCAN_PENDING_STATUS, FLY_SEARCH_DEBOUNCE_MS, FLY_SKIN_TONES, FLY_STANDALONE_AUTH_CONFIG, FLY_STRATEGY_OBJECTIVES_LOOKUP_ENDPOINT, FLY_THEME_MODE_IDS, FLY_VIEWPORT_IS_MOBILE, FLY_WINDOW_BREADCRUMBS_EVENT, FLY_WINDOW_BREADCRUMBS_STORE_KEY, FLY_WINDOW_HELP_HINT_EVENT, FLY_WINDOW_PAGE_TITLE_EVENT, FlyAchievementProgressComponent, FlyActionMenuComponent, FlyActionStackComponent, FlyAgentDraggableDirective, FlyAnimatedEmojiComponent, FlyAppHomeComponent, FlyAppTopbarComponent, FlyAppUnavailableComponent, FlyAuthLiveRefresh, FlyBadgeWallComponent, FlyBlockUiComponent, FlyBreadcrumbComponent, FlyButtonComponent, FlyBytesPipe, FlyCaptchaComponent, FlyCardActionsComponent, FlyCardBodyComponent, FlyCardComponent, FlyCardFooterComponent, FlyCardGridComponent, FlyCardMediaComponent, FlyCardMetaComponent, FlyCardStatusComponent, FlyCardTitleComponent, FlyCellDirective, FlyCheckboxComponent, FlyChipComponent, FlyChunkReloadErrorHandler, FlyClickOutsideDirective, FlyCollapsibleComponent, FlyCommentThreadComponent, FlyCompactNumberPipe, FlyConfirmDialogComponent, FlyControlDirective, FlyCronBuilderComponent, FlyCurrencyCatalogService, FlyCurrencySelectorComponent, FlyDataTableComponent, FlyDatePipe, FlyDateTimePipe, FlyDebouncer, FlyDecimalNumberPipe, FlyDeepLinkPrefetchService, FlyDetailCardComponent, FlyDetailShellComponent, FlyDrawerComponent, FlyDurationPipe, FlyDynamicFormComponent, FlyEmojiPickerComponent, FlyFieldComponent, FlyFileDownloadService, FlyFileUploadComponent, FlyFilterPanelComponent, FlyFormBannerComponent, FlyFormFooterComponent, FlyFormGridComponent, FlyFormSectionComponent, FlyFullNumberPipe, FlyGanttComponent, FlyHubClient, FlyIconButtonComponent, FlyIfAdminDirective, FlyIfRoleDirective, FlyImageUploadComponent, FlyLeaderboardComponent, FlyLifecyclePipelineComponent, FlyMagicActionsComponent, FlyMetaListComponent, FlyModalComponent, FlyModerationQueueComponent, FlyModuleIconDirective, FlyMoneyPipe, FlyOverviewKpiRowComponent, FlyOverviewRowsComponent, FlyOverviewSectionComponent, FlyOverviewSurfaceComponent, FlyPagerComponent, FlyPaginationComponent, FlyPeoplePickerComponent, FlyPointsTierComponent, FlyProgressComponent, FlyRelativeTimePipe, FlyRemoteContextService, FlyRemoteRouter, FlyRemoteRouterOutletComponent, FlySearchInputComponent, FlySectionHeaderComponent, FlySecureSrcDirective, FlySegmentedComponent, FlySelectComponent, FlySkeletonComponent, FlySliderComponent, FlySparklineComponent, FlySpinnerComponent, FlyStandaloneAuthCallbackComponent, FlyStandaloneAuthService, FlyStandaloneMagicActionsComponent, FlyStateMessageComponent, FlyStrategySelectorComponent, FlyDynamicFormComponent as FlySurveyFormComponent, FlyTabComponent, FlyTabsComponent, FlyTagsInputComponent, FlyThemeService, FlyTimePipe, FlyToastService, FlyToggleComponent, FlyTooltipDirective, FlyTreeNavComponent, FlyTypeaheadComponent, FlyUserDirectoryService, FlyWindowHelpService, FlyWindowTitleService, FlyosPendingLaunchesGlobalKey, FlyosShellHandlesLaunchRequestsGlobalKey, FlyosShellOwnsHistoryGlobalKey, GANTT_DEPENDENCY_TYPES, GANTT_LABEL_W_DEFAULT, GANTT_LABEL_W_MAX, GANTT_LABEL_W_MIN, GANTT_ROW_TINT_ALPHA, GANTT_ZOOMS, I18nService, LAUNCH_CONTEXT, MAGIC_BAR_SEARCH_WIDTH_DEFAULT, MAGIC_BAR_SEARCH_WIDTH_MAX, MAGIC_BAR_SEARCH_WIDTH_MIN, MagicBarRegistry, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, NOVA_PRIORITY_TONE, NOVA_STATUS_TONE, NOVA_TONE_FALLBACK, OverlayStack, PRESENCE_COLORS, PageHeaderComponent, RTL_LOCALE_SET, SHARE_ORG_CHART_SYSTEM_KEY_APPS, SHARE_ORG_CHART_SYSTEM_KEY_DEFAULT, SHARE_PANEL_DEFAULT_FILE_LEVELS, STATE_DEFAULT_ICONS, STATE_DEFAULT_MESSAGE_KEYS, STEP_UP_CODE, STEP_UP_REAUTH_HANDLER, STEP_UP_RETURN_KEY, SUPPORTED_AGENT_PAYLOAD_VERSIONS, SharePanelComponent, SourceAppResolver, StandaloneWindowManagerService, StatusBadgeComponent, StepUpService, ToastHostComponent, TranslatePipe, WINDOW_DATA, WINDOW_HELP_HINT, WindowBreadcrumbsRegistry, WindowManagerService, applyGanttRowReorder, applySkinTone, applySuggestion, ariaSort, buildCron, canConfirm, canGoNext$1 as canGoNext, canGoPrev$1 as canGoPrev, captureFocus, clampMagicBarSearchWidth, clampPage$1 as clampPage, clampSliderValue, connectRemoteLaunch, deepestFlyMatch, enterActivatesNatively, filterGanttRows, filterGroup, filterSuggestions, findLocaleByDialect, findLocaleByPrefix, firstEnabledIndex, firstModuleIndex, flattenModules, flyAdminGuard, flyApiErrorMessage, flyCaptchaBase64Utf8, flyCaptchaBuildToken, flyCaptchaSha256Hex, flyCompactNumber, flyDebounced, flyDecimalNumber, flyDownloadBlob, flyDuration, flyEmojiFor, flyEmojiIsTonable, flyEmojiPackUrl, flyExportFileName, flyFirstEnabledIndex, flyFormatBytes, flyFormatDate, flyFormatDateTime, flyFormatMoney, flyFormatTime, flyFullNumber, flyNextEnabledIndex, flyNextSegmentIndex, flyRelativeTime, flyResolveActiveTab, flyRoleGuard, flyScanPendingDelayMs, flyScanRetry, flySignedCompact, flySkinToneLabelKey, flyStandaloneAuthGuard, flyStandaloneAuthInterceptor, flyToDateOnly, flyToPage, flyUnwrap, flyUnwrapCurrencies, flyUnwrapCurrency, flyUnwrapLenient, flyUnwrapObjectiveLookup, ganttOutlineBarItems, ganttPeriodBarItem, hasAnyRoleIn, healNativeFederationCacheOnce, initialExpanded, isAccentColor, isChunkLoadError, isFlyAuthStorePopulated, isNativeFederationCacheError, isRtlLocale, isRtlLocaleEntry, isValidCron, isValidSingleField, loadRemoteStyles, magicBarOwnerKey, magicBarSearchText, matchFlyRoutePattern, matchFlyRoutePrefix, matchFlyRouteTable, nextEnabledIndex, nextModuleIndex, nextSegmentIndex, nextSort, normalizeFlyTheme, normalizeGanttSearchText, normalizeRoles, novaPriorityToneVar, novaStatusToneVar, orderMagicBarItems, overlayStack, canGoNext as pagerCanGoNext, canGoPrev as pagerCanGoPrev, clampPage as pagerClampPage, parseCron, parseStepUpChallenge, parseTags, prefetchRemoteStyles, presenceColorFor, printConsoleSecurityWarning, provideFlyChunkReloadRecovery, provideFlyEmojiPack, provideFlyGanttDefaults, provideFlyOfflineAuth, provideFlyStandaloneAuth, reloadOnceForChunkError, requestAppLaunch, resolveActiveModule, resolveActiveSection, resolveActiveTab, resolveCellValue, resolveStateIcon, resolveStateMessageKey, restoreFocus, sameTags, sectionHintKey, sectionLayout, sliderTrackGradient, sparklinePath, sparklinePoints, stepUpInterceptor, stepUpReturnKey, trimAgentPayload, trimAgentString, unloadRemoteStyles, utf8ByteLength, validateAgentPayload, warnIfEmbeddedSessionMissing };
29460
+ export { AGENT_DRAG_MIME, AGENT_PAYLOAD_VERSION, APP_LOOKUP, AUDIENCE_ERROR_CODES, AUDIENCE_LIMITS, AUDIENCE_PRESETS, AUDIENCE_TERM_KINDS, AgentActionBus, AgentActionUnsupportedDispatchError, AgentCommandRegistry, AgentDropRegistry, AgentFlightAnimator, AgentLookupRegistry, AgentPayloadOversizeError, AudienceBuilderComponent, AuthService, BilingualFieldComponent, CRON_MODES, CRON_WEEKDAYS, ContextMenuComponent, DEFAULT_AGENT_PAYLOAD_LIMITS, DEFAULT_FLY_THEME_MODE, DS_BASELINE_LOCALES, DashboardKpiComponent, DialogResult, ENTITY_LINK_LAUNCHER, EntityLookupComponent, FLYOS_LAUNCH_EVENT, FLYOS_LAUNCH_REQUEST_EVENT, FLYOS_REMOTE_ROUTE_EVENT, FLY_ACCENT_PROPERTY, FLY_ADMIN_ROLES, FLY_CHUNK_RELOAD_FLAG, FLY_COMPACT_THRESHOLD, FLY_COUNTRIES, FLY_CURRENCIES_BRIEF_ENDPOINT, FLY_CURRENCY_DEFAULT_ENDPOINT, FLY_EMOJI_BY_ID, FLY_EMOJI_CATEGORIES, FLY_EMOJI_CATEGORY_BY_ID, FLY_EMOJI_DEFAULT_CATEGORIES, FLY_EMOJI_PACK, FLY_EMOJI_PACK_MISSING_MESSAGE, FLY_EMOJI_PREVIEW_COUNT, FLY_EMPTY_VALUE, FLY_GANTT_DEFAULTS, FLY_LAUNCH_CONTEXT_EVENT, FLY_LAUNCH_CONTEXT_STORE_KEY, FLY_LOCALE_CATALOG, FLY_MAGIC_BAR_EVENT, FLY_MAGIC_BAR_ICONS, FLY_MAGIC_BAR_STORE_KEY, FLY_NF_CACHE_HEAL_FLAG, FLY_RELOAD_DEBOUNCE_MS, FLY_RELOAD_REARM_MS, FLY_REMOTE_BASE_PATH, FLY_REMOTE_CONTEXT_EVENT, FLY_REMOTE_CONTEXT_STORE_KEY, FLY_REMOTE_OUTLET_DEPTH, FLY_REMOTE_ROUTES, FLY_SCAN_PENDING_DEFAULT_DELAY_MS, FLY_SCAN_PENDING_MAX_RETRIES, FLY_SCAN_PENDING_STATUS, FLY_SEARCH_DEBOUNCE_MS, FLY_SKIN_TONES, FLY_STANDALONE_AUTH_CONFIG, FLY_STRATEGY_OBJECTIVES_LOOKUP_ENDPOINT, FLY_THEME_MODE_IDS, FLY_VIEWPORT_IS_MOBILE, FLY_WINDOW_BREADCRUMBS_EVENT, FLY_WINDOW_BREADCRUMBS_STORE_KEY, FLY_WINDOW_HELP_HINT_EVENT, FLY_WINDOW_PAGE_TITLE_EVENT, FlyAchievementProgressComponent, FlyActionMenuComponent, FlyActionStackComponent, FlyAgentDraggableDirective, FlyAnimatedEmojiComponent, FlyAppHomeComponent, FlyAppTopbarComponent, FlyAppUnavailableComponent, FlyAuthLiveRefresh, FlyBadgeWallComponent, FlyBlockUiComponent, FlyBreadcrumbComponent, FlyButtonComponent, FlyBytesPipe, FlyCaptchaComponent, FlyCardActionsComponent, FlyCardBodyComponent, FlyCardComponent, FlyCardFooterComponent, FlyCardGridComponent, FlyCardMediaComponent, FlyCardMetaComponent, FlyCardStatusComponent, FlyCardTitleComponent, FlyCellDirective, FlyCheckboxComponent, FlyChipComponent, FlyChunkReloadErrorHandler, FlyClickOutsideDirective, FlyCollapsibleComponent, FlyCommentThreadComponent, FlyCompactNumberPipe, FlyConfirmDialogComponent, FlyControlDirective, FlyCronBuilderComponent, FlyCurrencyCatalogService, FlyCurrencySelectorComponent, FlyDataTableComponent, FlyDatePipe, FlyDateTimePipe, FlyDebouncer, FlyDecimalNumberPipe, FlyDeepLinkPrefetchService, FlyDetailCardComponent, FlyDetailShellComponent, FlyDrawerComponent, FlyDurationPipe, FlyDynamicFormComponent, FlyEmojiPickerComponent, FlyFieldComponent, FlyFileDownloadService, FlyFileUploadComponent, FlyFilterPanelComponent, FlyFormBannerComponent, FlyFormFooterComponent, FlyFormGridComponent, FlyFormSectionComponent, FlyFullNumberPipe, FlyGanttComponent, FlyHubClient, FlyIconButtonComponent, FlyIfAdminDirective, FlyIfRoleDirective, FlyImageUploadComponent, FlyLaunchContextService, FlyLeaderboardComponent, FlyLifecyclePipelineComponent, FlyMagicActionsComponent, FlyMetaListComponent, FlyModalComponent, FlyModerationQueueComponent, FlyModuleIconDirective, FlyMoneyPipe, FlyOverviewKpiRowComponent, FlyOverviewRowsComponent, FlyOverviewSectionComponent, FlyOverviewSurfaceComponent, FlyPagerComponent, FlyPaginationComponent, FlyPeoplePickerComponent, FlyPointsTierComponent, FlyProgressComponent, FlyRelativeTimePipe, FlyRemoteContextService, FlyRemoteRouter, FlyRemoteRouterOutletComponent, FlySearchInputComponent, FlySectionHeaderComponent, FlySecureSrcDirective, FlySegmentedComponent, FlySelectComponent, FlySkeletonComponent, FlySliderComponent, FlySparklineComponent, FlySpinnerComponent, FlyStandaloneAuthCallbackComponent, FlyStandaloneAuthService, FlyStandaloneMagicActionsComponent, FlyStateMessageComponent, FlyStrategySelectorComponent, FlyDynamicFormComponent as FlySurveyFormComponent, FlyTabComponent, FlyTabsComponent, FlyTagsInputComponent, FlyThemeService, FlyTimePipe, FlyToastService, FlyToggleComponent, FlyTooltipDirective, FlyTreeNavComponent, FlyTypeaheadComponent, FlyUserDirectoryService, FlyWindowHelpService, FlyWindowTitleService, FlyosPendingLaunchesGlobalKey, FlyosShellHandlesLaunchRequestsGlobalKey, FlyosShellOwnsHistoryGlobalKey, GANTT_DEPENDENCY_TYPES, GANTT_LABEL_W_DEFAULT, GANTT_LABEL_W_MAX, GANTT_LABEL_W_MIN, GANTT_ROW_TINT_ALPHA, GANTT_ZOOMS, I18nService, LAUNCH_CONTEXT, MAGIC_BAR_SEARCH_WIDTH_DEFAULT, MAGIC_BAR_SEARCH_WIDTH_MAX, MAGIC_BAR_SEARCH_WIDTH_MIN, MagicBarRegistry, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, NOVA_PRIORITY_TONE, NOVA_STATUS_TONE, NOVA_TONE_FALLBACK, OverlayStack, PRESENCE_COLORS, PageHeaderComponent, RTL_LOCALE_SET, SHARE_ORG_CHART_SYSTEM_KEY_APPS, SHARE_ORG_CHART_SYSTEM_KEY_DEFAULT, SHARE_PANEL_DEFAULT_FILE_LEVELS, STATE_DEFAULT_ICONS, STATE_DEFAULT_MESSAGE_KEYS, STEP_UP_CODE, STEP_UP_REAUTH_HANDLER, STEP_UP_RETURN_KEY, SUPPORTED_AGENT_PAYLOAD_VERSIONS, SharePanelComponent, SourceAppResolver, StandaloneWindowManagerService, StatusBadgeComponent, StepUpService, ToastHostComponent, TranslatePipe, WINDOW_DATA, WINDOW_HELP_HINT, WindowBreadcrumbsRegistry, WindowManagerService, applyGanttRowReorder, applySkinTone, applySuggestion, ariaSort, buildCron, canConfirm, canGoNext$1 as canGoNext, canGoPrev$1 as canGoPrev, captureFocus, clampMagicBarSearchWidth, clampPage$1 as clampPage, clampSliderValue, connectRemoteLaunch, deepestFlyMatch, enterActivatesNatively, filterGanttRows, filterGroup, filterSuggestions, findLocaleByDialect, findLocaleByPrefix, firstEnabledIndex, firstModuleIndex, flattenModules, flyAdminGuard, flyApiErrorMessage, flyCaptchaBase64Utf8, flyCaptchaBuildToken, flyCaptchaSha256Hex, flyCompactNumber, flyDebounced, flyDecimalNumber, flyDownloadBlob, flyDuration, flyEmojiFor, flyEmojiIsTonable, flyEmojiPackUrl, flyExportFileName, flyFirstEnabledIndex, flyFormatBytes, flyFormatDate, flyFormatDateTime, flyFormatMoney, flyFormatTime, flyFullNumber, flyNextEnabledIndex, flyNextSegmentIndex, flyRelativeTime, flyResolveActiveTab, flyRoleGuard, flyScanPendingDelayMs, flyScanRetry, flySignedCompact, flySkinToneLabelKey, flyStandaloneAuthGuard, flyStandaloneAuthInterceptor, flyToDateOnly, flyToPage, flyUnwrap, flyUnwrapCurrencies, flyUnwrapCurrency, flyUnwrapLenient, flyUnwrapObjectiveLookup, ganttOutlineBarItems, ganttPeriodBarItem, hasAnyRoleIn, healNativeFederationCacheOnce, initialExpanded, isAccentColor, isChunkLoadError, isFlyAuthStorePopulated, isNativeFederationCacheError, isRtlLocale, isRtlLocaleEntry, isValidCron, isValidSingleField, linkAnchorsFor, loadRemoteStyles, magicBarOwnerKey, magicBarSearchText, matchFlyRoutePattern, matchFlyRoutePrefix, matchFlyRouteTable, nextEnabledIndex, nextModuleIndex, nextSegmentIndex, nextSort, normalizeFlyTheme, normalizeGanttSearchText, normalizeRoles, novaPriorityToneVar, novaStatusToneVar, orderMagicBarItems, overlayStack, canGoNext as pagerCanGoNext, canGoPrev as pagerCanGoPrev, clampPage as pagerClampPage, parseCron, parseStepUpChallenge, parseTags, prefetchRemoteStyles, presenceColorFor, printConsoleSecurityWarning, provideFlyChunkReloadRecovery, provideFlyEmojiPack, provideFlyGanttDefaults, provideFlyOfflineAuth, provideFlyStandaloneAuth, reloadOnceForChunkError, requestAppLaunch, resolveActiveModule, resolveActiveSection, resolveActiveTab, resolveCellValue, resolveStateIcon, resolveStateMessageKey, restoreFocus, sameTags, sectionHintKey, sectionLayout, sliderTrackGradient, sparklinePath, sparklinePoints, stepUpInterceptor, stepUpReturnKey, trimAgentPayload, trimAgentString, unloadRemoteStyles, utf8ByteLength, validateAgentPayload, warnIfEmbeddedSessionMissing };
29023
29461
  //# sourceMappingURL=flyos-design-system.mjs.map