@flyos/design-system 2.0.0 → 2.2.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.
@@ -3,7 +3,6 @@ import { InjectionToken, signal, computed, Injectable, inject, TemplateRef, View
3
3
  import { catchError, throwError, Subject, from, firstValueFrom, of, Observable, map, ReplaySubject, retry, timer, tap } from 'rxjs';
4
4
  import { HttpClient, HttpParams, HttpHeaders, HttpErrorResponse, HttpEventType } from '@angular/common/http';
5
5
  import { switchMap, debounceTime, distinctUntilChanged, filter, catchError as catchError$1 } from 'rxjs/operators';
6
- import { HubConnectionBuilder, HttpTransportType, LogLevel, HubConnectionState } from '@microsoft/signalr';
7
6
  import { Router, NavigationEnd } from '@angular/router';
8
7
  import * as i1$1 from '@angular/common';
9
8
  import { isPlatformBrowser, NgComponentOutlet, CommonModule, DOCUMENT, NgTemplateOutlet, NgClass, DatePipe } from '@angular/common';
@@ -48,7 +47,7 @@ import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
48
47
  // tools/publish-library.ps1 at bump time, and asserted by the spec beside this file.
49
48
  // Used only for the diagnostic message; the duplicate-instance detection itself is
50
49
  // version-agnostic, so a stale literal misnames a fork rather than hiding one.
51
- const FLY_DS_VERSION = '2.0.0';
50
+ const FLY_DS_VERSION = '2.2.0';
52
51
  const FLY_DS_REGISTRY_KEY = '__FLY_DS_INSTANCES__';
53
52
  /**
54
53
  * Records this design-system instance on the shared `scope` and returns the
@@ -708,23 +707,42 @@ const FLY_STANDALONE_AUTH_CONFIG = new InjectionToken('FLY_STANDALONE_AUTH_CONFI
708
707
  * across replicas). The bearer rides as `?access_token=` via the {@link AuthService} token. Idempotent
709
708
  * `connect()`; safe to call once auth is established.</p>
710
709
  *
711
- * <p>Requires `@microsoft/signalr` (declared a peer dependency of the design system).</p>
710
+ * <p>Requires `@microsoft/signalr` (declared a peer dependency of the design system). The package is
711
+ * imported DYNAMICALLY on first `connect()`: this client sits in the core design system, which every
712
+ * boot loads, and a static import made `@microsoft/signalr` (~14 KB compressed) part of every boot's
713
+ * shared-module wave even though no bytes are needed until the hub actually connects. Under Native
714
+ * Federation the dynamic import resolves through the same import map / shared singleton as a static
715
+ * one — only the WHEN changes.</p>
712
716
  */
713
717
  class FlyHubClient {
714
718
  auth = inject(AuthService);
715
719
  connection = null;
720
+ /** In-flight first connect (module import + build + start). Guards double-connect. */
721
+ connecting = null;
722
+ /** Set when disconnect() arrives while the first connect is still importing/starting. */
723
+ abandonConnect = false;
716
724
  connected = signal(false, ...(ngDevMode ? [{ debugName: "connected" }] : /* istanbul ignore next */ []));
717
725
  _authRefresh$ = new Subject();
718
726
  /** Emits when the server signals this user's authorization shape changed. */
719
727
  authRefresh$ = this._authRefresh$.asObservable();
720
728
  /** Open the hub connection (idempotent). No-op without an access token. */
721
729
  connect() {
722
- if (this.connection !== null)
730
+ if (this.connection !== null || this.connecting !== null)
723
731
  return;
724
732
  const token = this.auth.accessToken();
725
733
  if (!token)
726
734
  return;
727
- this.connection = new HubConnectionBuilder()
735
+ this.abandonConnect = false;
736
+ this.connecting = this.doConnect()
737
+ .catch(() => this.connected.set(false))
738
+ .finally(() => { this.connecting = null; });
739
+ }
740
+ async doConnect() {
741
+ const { HubConnectionBuilder, HttpTransportType, LogLevel } = await import('@microsoft/signalr');
742
+ // A disconnect() may have raced the import — stay down.
743
+ if (this.abandonConnect)
744
+ return;
745
+ const connection = new HubConnectionBuilder()
728
746
  .withUrl('/hubs/fly', {
729
747
  accessTokenFactory: () => this.auth.accessToken() ?? '',
730
748
  skipNegotiation: true,
@@ -733,15 +751,25 @@ class FlyHubClient {
733
751
  .withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
734
752
  .configureLogging(LogLevel.Warning)
735
753
  .build();
736
- this.connection.on('AuthRefresh', (payload) => this._authRefresh$.next(payload ?? {}));
737
- this.connection.onreconnected(() => this.connected.set(true));
738
- this.connection.onreconnecting(() => this.connected.set(false));
739
- this.connection.onclose(() => { this.connected.set(false); this.connection = null; });
740
- this.connection.start()
754
+ this.connection = connection;
755
+ connection.on('AuthRefresh', (payload) => this._authRefresh$.next(payload ?? {}));
756
+ connection.onreconnected(() => this.connected.set(true));
757
+ connection.onreconnecting(() => this.connected.set(false));
758
+ connection.onclose(() => { this.connected.set(false); this.connection = null; });
759
+ await connection.start()
741
760
  .then(() => this.connected.set(true))
742
761
  .catch(() => this.connected.set(false));
743
762
  }
744
763
  async disconnect() {
764
+ // A first connect still importing/starting: mark it abandoned and wait it out,
765
+ // so a connection can't surface after the caller believed it closed.
766
+ if (this.connecting) {
767
+ this.abandonConnect = true;
768
+ try {
769
+ await this.connecting;
770
+ }
771
+ catch { /* connect failure — already down */ }
772
+ }
745
773
  if (this.connection) {
746
774
  const c = this.connection;
747
775
  this.connection = null;
@@ -753,7 +781,11 @@ class FlyHubClient {
753
781
  }
754
782
  }
755
783
  get isConnected() {
756
- return this.connection?.state === HubConnectionState.Connected;
784
+ // String-compare instead of `=== HubConnectionState.Connected`: the enum is a
785
+ // VALUE import, and pulling it statically would re-create the eager edge this
786
+ // client exists to avoid. `HubConnectionState` is a string enum — its
787
+ // `Connected` member is the literal 'Connected'.
788
+ return this.connection?.state === 'Connected';
757
789
  }
758
790
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyHubClient, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
759
791
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyHubClient, providedIn: 'root' });
@@ -1594,6 +1626,7 @@ const DS_BASELINE_LOCALES = {
1594
1626
  'currency_selector.clear': 'Clear selection',
1595
1627
  'currency_selector.pinned': 'Frequently used',
1596
1628
  'currency_selector.all': 'All currencies',
1629
+ 'currency_selector.locked_default_reason': 'This currency is locked and can’t be changed.',
1597
1630
  // chat composer (fly-chat-composer)
1598
1631
  'chat_composer.label.message': 'Message',
1599
1632
  'chat_composer.label.mention_suggestions': 'Mention suggestions',
@@ -1957,6 +1990,7 @@ const DS_BASELINE_LOCALES = {
1957
1990
  'currency_selector.clear': 'مسح التحديد',
1958
1991
  'currency_selector.pinned': 'الأكثر استخداماً',
1959
1992
  'currency_selector.all': 'كل العملات',
1993
+ 'currency_selector.locked_default_reason': 'هذه العملة مقفلة ولا يمكن تغييرها.',
1960
1994
  // chat composer (fly-chat-composer)
1961
1995
  'chat_composer.label.message': 'رسالة',
1962
1996
  'chat_composer.label.mention_suggestions': 'اقتراحات الإشارة',
@@ -2320,6 +2354,7 @@ const DS_BASELINE_LOCALES = {
2320
2354
  'currency_selector.clear': 'Effacer la sélection',
2321
2355
  'currency_selector.pinned': 'Fréquemment utilisées',
2322
2356
  'currency_selector.all': 'Toutes les devises',
2357
+ 'currency_selector.locked_default_reason': 'Cette devise est verrouillée et ne peut pas être modifiée.',
2323
2358
  // chat composer (fly-chat-composer)
2324
2359
  'chat_composer.label.message': 'Message',
2325
2360
  'chat_composer.label.mention_suggestions': 'Suggestions de mention',
@@ -2682,6 +2717,7 @@ const DS_BASELINE_LOCALES = {
2682
2717
  'currency_selector.clear': 'انتخاب صاف کریں',
2683
2718
  'currency_selector.pinned': 'کثرت سے استعمال شدہ',
2684
2719
  'currency_selector.all': 'تمام کرنسیاں',
2720
+ 'currency_selector.locked_default_reason': 'یہ کرنسی مقفل ہے اور اسے تبدیل نہیں کیا جا سکتا۔',
2685
2721
  // chat composer (fly-chat-composer)
2686
2722
  'chat_composer.label.message': 'پیغام',
2687
2723
  'chat_composer.label.mention_suggestions': 'تذکرے کی تجاویز',
@@ -4034,6 +4070,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
4034
4070
  args: [{ providedIn: 'root' }]
4035
4071
  }] });
4036
4072
 
4073
+ const ENTITY_LINK_LAUNCHER = new InjectionToken('ENTITY_LINK_LAUNCHER');
4074
+
4037
4075
  /**
4038
4076
  * Publishes a {@link WindowHelpHint} for a window so the shell's titlebar Help
4039
4077
  * button deeplinks the help-center reader to the article most relevant to where
@@ -4582,6 +4620,22 @@ const _linkLayerSupported = typeof HTMLLinkElement !== 'undefined' && 'layer' in
4582
4620
  const _cspNonce = FLY_CSP_NONCE;
4583
4621
  /** In-flight fetch promises keyed by appId — prevents duplicate fetches. */
4584
4622
  const _inFlight = new Map();
4623
+ /** Upper bound on the optional bytes-applied wait — never holds a caller hostage. */
4624
+ const APPLY_CAP_MS = 4000;
4625
+ /** Resolves when `el` fires load/error, or after `capMs` — whichever is first. */
4626
+ function _awaitLoadCapped(el, capMs) {
4627
+ return new Promise((resolve) => {
4628
+ const timer = setTimeout(done, capMs);
4629
+ function done() {
4630
+ clearTimeout(timer);
4631
+ el.removeEventListener('load', done);
4632
+ el.removeEventListener('error', done);
4633
+ resolve();
4634
+ }
4635
+ el.addEventListener('load', done);
4636
+ el.addEventListener('error', done);
4637
+ });
4638
+ }
4585
4639
  // ---------------------------------------------------------------------------
4586
4640
  // Layer order — injected immediately at module init (synchronous, runs once).
4587
4641
  // ---------------------------------------------------------------------------
@@ -4748,9 +4802,21 @@ function _validateHref(href, remoteBaseUrl) {
4748
4802
  * The `data-fly-href` attribute stores the discovered href separately from the
4749
4803
  * element content so idempotency checks can compare the URL without parsing CSS.
4750
4804
  */
4751
- async function loadRemoteStyles(appId, remoteBaseUrl) {
4805
+ async function loadRemoteStyles(appId, remoteBaseUrl, opts = {}) {
4752
4806
  if (typeof document === 'undefined')
4753
4807
  return; // SSR guard
4808
+ // Already-applied fast path — see the `preferApplied` option docs.
4809
+ if (opts.preferApplied) {
4810
+ const applied = document.head.querySelector(`link[data-fly-app="${CSS.escape(appId)}"], style[data-fly-app="${CSS.escape(appId)}"]`);
4811
+ if (applied) {
4812
+ // `.sheet` is non-null once the stylesheet is parsed — the bytes signal
4813
+ // for an element whose load event may already have fired.
4814
+ if (!opts.awaitApplied || applied.sheet)
4815
+ return;
4816
+ await _awaitLoadCapped(applied, APPLY_CAP_MS);
4817
+ return;
4818
+ }
4819
+ }
4754
4820
  // Resolve relative remoteBaseUrl (e.g. '/circles-dev') against the current
4755
4821
  // page origin so all downstream URL operations (fetch, origin check) receive
4756
4822
  // an absolute URL. Absolute inputs are returned unchanged by new URL().
@@ -4767,7 +4833,9 @@ async function loadRemoteStyles(appId, remoteBaseUrl) {
4767
4833
  const href = await fetchPromise;
4768
4834
  _inFlight.delete(appId);
4769
4835
  if (!href) {
4770
- console.warn(`[FlyOS] loadRemoteStyles: no stylesheet found in ${absoluteBase}/index.html for appId="${appId}"`);
4836
+ if (!opts.silent) {
4837
+ console.warn(`[FlyOS] loadRemoteStyles: no stylesheet found in ${absoluteBase}/index.html for appId="${appId}"`);
4838
+ }
4771
4839
  return;
4772
4840
  }
4773
4841
  // Check both <link> and <style> selectors — handle upgrades from the old fallback path.
@@ -4792,19 +4860,95 @@ async function loadRemoteStyles(appId, remoteBaseUrl) {
4792
4860
  if (_cspNonce)
4793
4861
  link.nonce = _cspNonce;
4794
4862
  document.head.appendChild(link);
4863
+ // A stylesheet <link> fires load once its bytes are parsed — the direct signal.
4864
+ if (opts.awaitApplied)
4865
+ await _awaitLoadCapped(link, APPLY_CAP_MS);
4795
4866
  }
4796
4867
  else {
4797
4868
  // Fallback path: <style> with @import layer(remote) for older browsers.
4798
4869
  // CSS Cascade Level 5: `@import url("…") layer(remote)` is the only valid
4799
4870
  // way to place an @import inside a named cascade layer. Block-form
4800
4871
  // `@layer remote { @import … }` is invalid and silently dropped by browsers.
4872
+ //
4801
4873
  const style = document.createElement('style');
4802
4874
  style.setAttribute('data-fly-app', appId);
4803
4875
  style.setAttribute('data-fly-href', href);
4804
4876
  style.textContent = `@import url("${href}") layer(remote);`;
4805
4877
  if (_cspNonce)
4806
4878
  style.nonce = _cspNonce;
4879
+ // Appended synchronously after the `existing` check above — an `await`
4880
+ // between check and append would let a concurrent caller double-inject.
4807
4881
  document.head.appendChild(style);
4882
+ // A <style> element fires no load event for its @import, so for the
4883
+ // bytes-applied guarantee we shadow the same href with a `<link
4884
+ // rel="preload" as="style">`: it rides the @import's own in-flight fetch
4885
+ // (same no-cors request key) and its load event is the byte signal.
4886
+ // Deliberately NO crossorigin attribute — the @import request is no-cors,
4887
+ // and a CORS-mode preload would create a non-matching cache entry
4888
+ // (double fetch).
4889
+ if (opts.awaitApplied) {
4890
+ const preload = document.createElement('link');
4891
+ preload.setAttribute('rel', 'preload');
4892
+ preload.setAttribute('as', 'style');
4893
+ preload.setAttribute('href', href);
4894
+ if (_cspNonce)
4895
+ preload.nonce = _cspNonce;
4896
+ document.head.appendChild(preload);
4897
+ await _awaitLoadCapped(preload, APPLY_CAP_MS);
4898
+ preload.remove();
4899
+ }
4900
+ }
4901
+ }
4902
+ /**
4903
+ * Warms a remote's stylesheet into the HTTP cache WITHOUT applying it.
4904
+ *
4905
+ * Discovery is identical to {@link loadRemoteStyles} (fetch the remote's
4906
+ * `index.html`, scan for the hashed stylesheet, validate the href), but the
4907
+ * CSS is then fetched with a plain `fetch()` and the body discarded — nothing
4908
+ * is injected into `document.head`.
4909
+ *
4910
+ * Why this exists: applying a stylesheet registers its `@font-face` rules
4911
+ * document-wide, and the browser then downloads every face some text in the
4912
+ * document matches. The shell's deferred warm-up used to APPLY every
4913
+ * registered-but-unopened remote's stylesheet, which on a Circles deep link
4914
+ * pulled Thoughts' self-hosted Inter + Noto Kufi woff2 (~71 KB) into the LCP
4915
+ * window — for an app with no open window (stg trace, 2026-08-15). A cache
4916
+ * prefetch keeps the later real apply (window open, `loadRemoteStyles` with
4917
+ * `awaitApplied`) near-instant while deferring the font cost to the first
4918
+ * open, where the faces are actually about to be used.
4919
+ *
4920
+ * The fetch uses `mode: 'cors'` + `credentials: 'omit'` so its cache entry
4921
+ * matches the apply path's `<link crossorigin="anonymous">` request. (The
4922
+ * legacy `<style>@import` fallback path is no-cors and would not hit this
4923
+ * entry — modern browsers all take the `link[layer]` path, so the fallback
4924
+ * merely re-fetches, same as before.)
4925
+ *
4926
+ * Fail-soft and idempotent: errors are swallowed (the open path retries
4927
+ * loudly), and an already-applied appId is a no-op.
4928
+ */
4929
+ async function prefetchRemoteStyles(appId, remoteBaseUrl) {
4930
+ if (typeof document === 'undefined')
4931
+ return; // SSR guard
4932
+ // Already applied — the bytes are necessarily present; nothing to warm.
4933
+ const escapedId = CSS.escape(appId);
4934
+ if (document.head.querySelector(`link[data-fly-app="${escapedId}"], style[data-fly-app="${escapedId}"]`)) {
4935
+ return;
4936
+ }
4937
+ const absoluteBase = new URL(remoteBaseUrl, location.href).href.replace(/\/$/, '');
4938
+ let fetchPromise = _inFlight.get(appId);
4939
+ if (!fetchPromise) {
4940
+ fetchPromise = _discoverStylesheetHref(appId, absoluteBase);
4941
+ _inFlight.set(appId, fetchPromise);
4942
+ }
4943
+ const href = await fetchPromise;
4944
+ _inFlight.delete(appId);
4945
+ if (!href)
4946
+ return;
4947
+ try {
4948
+ await fetch(href, { mode: 'cors', credentials: 'omit', cache: 'default' });
4949
+ }
4950
+ catch {
4951
+ /* unreachable remote — the open path retries and reports */
4808
4952
  }
4809
4953
  }
4810
4954
  /**
@@ -5040,11 +5184,7 @@ class MockAuthService {
5040
5184
  // All signal/computed calls are inside the constructor body so Angular's
5041
5185
  // injection context and ngDevMode are fully set up before they execute.
5042
5186
  this._config = this.getConfig();
5043
- this._session = signal({
5044
- accessToken: this._config.token ?? 'mock-token',
5045
- user: this._config.user,
5046
- expiresAt: Date.now() + 24 * 60 * 60 * 1000,
5047
- }, ...(ngDevMode ? [{ debugName: "_session" }] : /* istanbul ignore next */ []));
5187
+ this._session = signal(this.createSession(), ...(ngDevMode ? [{ debugName: "_session" }] : /* istanbul ignore next */ []));
5048
5188
  this.isAuthenticated = computed(() => {
5049
5189
  const s = this._session();
5050
5190
  return s !== null && s.expiresAt > Date.now();
@@ -5068,6 +5208,14 @@ class MockAuthService {
5068
5208
  return;
5069
5209
  this._session.set({ ...session, user: { ...session.user, ...patch } });
5070
5210
  }
5211
+ /** A fresh 24h mock session from the app-supplied config — used at construction and by {@link startLogin}. */
5212
+ createSession() {
5213
+ return {
5214
+ accessToken: this._config.token ?? 'mock-token',
5215
+ user: this._config.user,
5216
+ expiresAt: Date.now() + 24 * 60 * 60 * 1000,
5217
+ };
5218
+ }
5071
5219
  /** Override in subclass to supply app-specific mock data. */
5072
5220
  getConfig() {
5073
5221
  return {
@@ -5088,6 +5236,15 @@ class MockAuthService {
5088
5236
  // Mock mode: already authenticated, nothing to initialize.
5089
5237
  }
5090
5238
  startLogin() {
5239
+ // Parity with a real STS round-trip: startLogin() must land the user back
5240
+ // AUTHENTICATED. After logout() nulls the session, a guard-triggered
5241
+ // startLogin() that only navigated would bounce off authGuard forever
5242
+ // (navigate → guard sees no session → startLogin → navigate → …) and the
5243
+ // login CTA would appear dead. Re-arming the session first makes every
5244
+ // mock login an instant, self-healing "STS visit".
5245
+ if (this._session() === null) {
5246
+ this._session.set(this.createSession());
5247
+ }
5091
5248
  this.router.navigate(this._config.loginRedirect ?? ['/']);
5092
5249
  }
5093
5250
  handleCallback(_code, _state) {
@@ -18618,6 +18775,21 @@ function unwrapCurrencies(res) {
18618
18775
  * i18n is self-sufficient through the `currency_selector.*` keys in `DS_BASELINE_LOCALES`
18619
18776
  * (en/ar/fr/ur); RTL works via logical CSS.
18620
18777
  *
18778
+ * ## `locked`, vs `disabled`
18779
+ * `disabled` is UI convention for "not applicable right now" — greyed out, no explanation,
18780
+ * because none is owed (a form section that only exists once a prior step completes, say).
18781
+ * `locked` is a different claim entirely: **the value is fixed on purpose**, because
18782
+ * something downstream now depends on it (PPM freezes a project's currency the moment any
18783
+ * financial row exists — changing it would silently re-denominate every stored amount).
18784
+ * A dimmed control with no explanation is exactly the failure `skills/magic-bar-actions.md`
18785
+ * §3.6 names for actions ("a vanishing action teaches the user nothing"); the same argument
18786
+ * applies to a frozen field. So `locked` renders differently on purpose: full-opacity (never
18787
+ * dimmed — the value is not "unavailable", it is authoritative), no dropdown affordance at
18788
+ * all, and an always-visible reason caption instead of a hover-only tooltip, so the "why" is
18789
+ * legible to a screen reader without requiring focus and to a sighted user without hovering.
18790
+ * `locked` takes precedence when both are set — it is the more specific state and the
18791
+ * `disabled` trigger markup (with its dropdown affordances) never renders underneath it.
18792
+ *
18621
18793
  * @example
18622
18794
  * ```html
18623
18795
  * <!-- Loads /api/currencies/brief itself: -->
@@ -18628,6 +18800,12 @@ function unwrapCurrencies(res) {
18628
18800
  * mode="multi"
18629
18801
  * [allowedCodes]="tenantCurrencies()"
18630
18802
  * (selectionDetailChange)="onCurrenciesPicked($event)" />
18803
+ *
18804
+ * <!-- Frozen once the project has financial rows — reason is an i18n KEY, never text: -->
18805
+ * <fly-currency-selector
18806
+ * [(ngModel)]="project.currency"
18807
+ * [locked]="project.hasFinancialRows"
18808
+ * lockedReasonKey="projects.currency_locked_reason" />
18631
18809
  * ```
18632
18810
  */
18633
18811
  class FlyCurrencySelectorComponent {
@@ -18650,6 +18828,24 @@ class FlyCurrencySelectorComponent {
18650
18828
  pinnedCodes = input([], ...(ngDevMode ? [{ debugName: "pinnedCodes" }] : /* istanbul ignore next */ []));
18651
18829
  /** Disable the whole control (also driven by reactive-forms `setDisabledState`). */
18652
18830
  disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
18831
+ /**
18832
+ * Freeze the current selection because something downstream now depends on it — a
18833
+ * DIFFERENT claim than `disabled`. See the class doc's "`locked`, vs `disabled`"
18834
+ * section. Renders the current pick as a plain, non-interactive readout (no dropdown
18835
+ * affordance at all) plus an always-visible reason caption — never a dimmed clickable-
18836
+ * looking control. Takes precedence over `disabled` when both are set.
18837
+ */
18838
+ locked = input(false, ...(ngDevMode ? [{ debugName: "locked" }] : /* istanbul ignore next */ []));
18839
+ /**
18840
+ * i18n KEY (never resolved text — see `skills/magic-bar-actions.md` §3.4) explaining
18841
+ * WHY the control is locked. Omit to use the localized `currency_selector.locked_default_reason`
18842
+ * baseline key; supply your own only when that default reason is wrong for your case
18843
+ * (mirrors `MagicBarActionSpec.disabledTipKey`'s "supply only when the default is wrong"
18844
+ * contract). Ignored while `locked` is `false`.
18845
+ */
18846
+ lockedReasonKey = input(null, ...(ngDevMode ? [{ debugName: "lockedReasonKey" }] : /* istanbul ignore next */ []));
18847
+ /** `I18nService.t()` params for `lockedReasonKey`, e.g. `{ date: frozenOn }`. */
18848
+ lockedReasonParams = input(undefined, ...(ngDevMode ? [{ debugName: "lockedReasonParams" }] : /* istanbul ignore next */ []));
18653
18849
  /** Show the trigger clear (✕) affordance when there is a selection. */
18654
18850
  clearable = input(true, ...(ngDevMode ? [{ debugName: "clearable" }] : /* istanbul ignore next */ []));
18655
18851
  /** Trigger text when nothing is picked. Omit for the localized default. */
@@ -18669,6 +18865,8 @@ class FlyCurrencySelectorComponent {
18669
18865
  _uid = ++_flyCurrencySelectorUid;
18670
18866
  listboxId = `fly-currency-selector-${this._uid}-listbox`;
18671
18867
  triggerId = `fly-currency-selector-${this._uid}-trigger`;
18868
+ /** id of the always-rendered locked-reason caption; `aria-describedby` target of the locked readout. */
18869
+ lockedReasonId = `fly-currency-selector-${this._uid}-locked-reason`;
18672
18870
  isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
18673
18871
  searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : /* istanbul ignore next */ []));
18674
18872
  activeIndex = signal(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
@@ -18709,6 +18907,8 @@ class FlyCurrencySelectorComponent {
18709
18907
  clearText = computed(() => this._i18n.t('currency_selector.clear'), ...(ngDevMode ? [{ debugName: "clearText" }] : /* istanbul ignore next */ []));
18710
18908
  pinnedGroupText = computed(() => this._i18n.t('currency_selector.pinned'), ...(ngDevMode ? [{ debugName: "pinnedGroupText" }] : /* istanbul ignore next */ []));
18711
18909
  allGroupText = computed(() => this._i18n.t('currency_selector.all'), ...(ngDevMode ? [{ debugName: "allGroupText" }] : /* istanbul ignore next */ []));
18910
+ /** Resolved locked-reason text — the default baseline key, or the host's `lockedReasonKey`. */
18911
+ lockedReasonText = computed(() => this._i18n.t(this.lockedReasonKey() ?? 'currency_selector.locked_default_reason', this.lockedReasonParams()), ...(ngDevMode ? [{ debugName: "lockedReasonText" }] : /* istanbul ignore next */ []));
18712
18912
  /** Every offered row, after the `allowedCodes` restriction. */
18713
18913
  available = computed(() => {
18714
18914
  const rows = this.currencies() ?? this._loaded();
@@ -18788,7 +18988,7 @@ class FlyCurrencySelectorComponent {
18788
18988
  }
18789
18989
  // ── Open / close ───────────────────────────────────────────────────────────
18790
18990
  toggleOpen() {
18791
- if (this.effectiveDisabled())
18991
+ if (this.effectiveDisabled() || this.locked())
18792
18992
  return;
18793
18993
  if (this.isOpen())
18794
18994
  this.close();
@@ -18796,7 +18996,7 @@ class FlyCurrencySelectorComponent {
18796
18996
  this.open();
18797
18997
  }
18798
18998
  open() {
18799
- if (this.effectiveDisabled() || this.isOpen())
18999
+ if (this.effectiveDisabled() || this.locked() || this.isOpen())
18800
19000
  return;
18801
19001
  this.isOpen.set(true);
18802
19002
  this.activeIndex.set(0);
@@ -18815,7 +19015,7 @@ class FlyCurrencySelectorComponent {
18815
19015
  }
18816
19016
  // ── Selection ──────────────────────────────────────────────────────────────
18817
19017
  pick(currency) {
18818
- if (this.effectiveDisabled())
19018
+ if (this.effectiveDisabled() || this.locked())
18819
19019
  return;
18820
19020
  if (!this.isMulti()) {
18821
19021
  this._commit([currency.code]);
@@ -18830,13 +19030,13 @@ class FlyCurrencySelectorComponent {
18830
19030
  }
18831
19031
  remove(code, event) {
18832
19032
  event?.stopPropagation();
18833
- if (this.effectiveDisabled())
19033
+ if (this.effectiveDisabled() || this.locked())
18834
19034
  return;
18835
19035
  this._commit(this._selectedCodes().filter((c) => c.toUpperCase() !== code.toUpperCase()));
18836
19036
  }
18837
19037
  clear(event) {
18838
19038
  event?.stopPropagation();
18839
- if (this.effectiveDisabled())
19039
+ if (this.effectiveDisabled() || this.locked())
18840
19040
  return;
18841
19041
  this._commit([]);
18842
19042
  }
@@ -18949,13 +19149,13 @@ class FlyCurrencySelectorComponent {
18949
19149
  this._onTouched();
18950
19150
  }
18951
19151
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyCurrencySelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
18952
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyCurrencySelectorComponent, isStandalone: true, selector: "fly-currency-selector", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, currencies: { classPropertyName: "currencies", publicName: "currencies", isSignal: true, isRequired: false, transformFunction: null }, fetchFn: { classPropertyName: "fetchFn", publicName: "fetchFn", isSignal: true, isRequired: false, transformFunction: null }, allowedCodes: { classPropertyName: "allowedCodes", publicName: "allowedCodes", isSignal: true, isRequired: false, transformFunction: null }, pinnedCodes: { classPropertyName: "pinnedCodes", publicName: "pinnedCodes", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange", selectionDetailChange: "selectionDetailChange", openedChange: "openedChange" }, host: { properties: { "class.fly-currency-selector--open": "isOpen()", "class.fly-currency-selector--disabled": "effectiveDisabled()" }, classAttribute: "fly-currency-selector" }, providers: [
19152
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyCurrencySelectorComponent, isStandalone: true, selector: "fly-currency-selector", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, currencies: { classPropertyName: "currencies", publicName: "currencies", isSignal: true, isRequired: false, transformFunction: null }, fetchFn: { classPropertyName: "fetchFn", publicName: "fetchFn", isSignal: true, isRequired: false, transformFunction: null }, allowedCodes: { classPropertyName: "allowedCodes", publicName: "allowedCodes", isSignal: true, isRequired: false, transformFunction: null }, pinnedCodes: { classPropertyName: "pinnedCodes", publicName: "pinnedCodes", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, locked: { classPropertyName: "locked", publicName: "locked", isSignal: true, isRequired: false, transformFunction: null }, lockedReasonKey: { classPropertyName: "lockedReasonKey", publicName: "lockedReasonKey", isSignal: true, isRequired: false, transformFunction: null }, lockedReasonParams: { classPropertyName: "lockedReasonParams", publicName: "lockedReasonParams", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange", selectionDetailChange: "selectionDetailChange", openedChange: "openedChange" }, host: { properties: { "class.fly-currency-selector--open": "isOpen()", "class.fly-currency-selector--disabled": "effectiveDisabled()", "class.fly-currency-selector--locked": "locked()" }, classAttribute: "fly-currency-selector" }, providers: [
18953
19153
  {
18954
19154
  provide: NG_VALUE_ACCESSOR,
18955
19155
  useExisting: forwardRef(() => FlyCurrencySelectorComponent),
18956
19156
  multi: true,
18957
19157
  },
18958
- ], viewQueries: [{ propertyName: "searchEl", first: true, predicate: ["searchRef"], descendants: true }, { propertyName: "triggerEl", first: true, predicate: ["triggerRef"], descendants: true }], ngImport: i0, template: "<div\r\n class=\"fly-currency-selector__wrap\"\r\n [flyClickOutsideEnabled]=\"isOpen()\"\r\n (flyClickOutside)=\"close()\"\r\n>\r\n <!-- \u2500\u2500 Trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 `aria-label` is never null: a `combobox` does NOT take its accessible name from its\r\n contents the way a plain button does, so the chips/placeholder painted inside it name\r\n nothing. Absent a consumer label the localized placeholder supplies the control's\r\n purpose; its VALUE is still announced from the contents. -->\r\n <button\r\n #triggerRef\r\n type=\"button\"\r\n [id]=\"triggerId\"\r\n class=\"fly-currency-selector__trigger\"\r\n role=\"combobox\"\r\n aria-haspopup=\"listbox\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-controls]=\"listboxId\"\r\n [attr.aria-label]=\"ariaLabel() || placeholderText()\"\r\n [class.fly-currency-selector__trigger--empty]=\"!hasSelection()\"\r\n [disabled]=\"effectiveDisabled()\"\r\n (click)=\"toggleOpen()\"\r\n (keydown)=\"onTriggerKeydown($event)\"\r\n >\r\n @if (selected().length === 0) {\r\n <span class=\"fly-currency-selector__placeholder\">{{ placeholderText() }}</span>\r\n } @else {\r\n <span class=\"fly-currency-selector__chips\">\r\n @for (c of selected(); track c.code) {\r\n <span class=\"fly-currency-selector__chip\">\r\n <span class=\"fly-currency-selector__tile\" aria-hidden=\"true\">\r\n @if (flagsRenderable() && c.flag) {\r\n <span class=\"fly-currency-selector__tile-flag\">{{ c.flag }}</span>\r\n } @else {\r\n <span class=\"fly-currency-selector__tile-code\">{{ tileCode(c) }}</span>\r\n }\r\n </span>\r\n <span class=\"fly-currency-selector__chip-code\">{{ c.code }}</span>\r\n <span class=\"fly-currency-selector__chip-symbol\" aria-hidden=\"true\">{{ c.symbol }}</span>\r\n @if (isMulti() && !effectiveDisabled()) {\r\n <span\r\n class=\"fly-currency-selector__chip-remove\"\r\n role=\"button\"\r\n tabindex=\"-1\"\r\n [attr.aria-label]=\"'\u2715 ' + c.code\"\r\n (click)=\"remove(c.code, $event)\"\r\n (mousedown)=\"$event.preventDefault()\"\r\n >\u2715</span>\r\n }\r\n </span>\r\n }\r\n </span>\r\n }\r\n\r\n @if (clearable() && hasSelection() && !effectiveDisabled()) {\r\n <span\r\n class=\"fly-currency-selector__clear\"\r\n role=\"button\"\r\n tabindex=\"-1\"\r\n [attr.aria-label]=\"clearText()\"\r\n [title]=\"clearText()\"\r\n (click)=\"clear($event)\"\r\n (mousedown)=\"$event.preventDefault()\"\r\n >\u2715</span>\r\n }\r\n\r\n <span class=\"fly-currency-selector__chevron\" aria-hidden=\"true\">\u25BE</span>\r\n </button>\r\n\r\n <!-- \u2500\u2500 Panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 @if (isOpen()) {\r\n <div class=\"fly-currency-selector__panel\" tabindex=\"-1\" (keydown)=\"onPanelKeydown($event)\">\r\n <div class=\"fly-currency-selector__search\">\r\n <input\r\n #searchRef\r\n type=\"text\"\r\n class=\"fly-currency-selector__search-input\"\r\n role=\"combobox\"\r\n aria-autocomplete=\"list\"\r\n [attr.aria-expanded]=\"true\"\r\n [attr.aria-controls]=\"listboxId\"\r\n [attr.aria-activedescendant]=\"activeDescendant() || null\"\r\n [attr.aria-label]=\"searchPlaceholderText()\"\r\n [placeholder]=\"searchPlaceholderText()\"\r\n [value]=\"searchTerm()\"\r\n (input)=\"onSearchInput($any($event.target).value)\"\r\n />\r\n </div>\r\n\r\n @if (loading()) {\r\n <div class=\"fly-currency-selector__status\" role=\"status\">{{ loadingText() }}</div>\r\n } @else if (loadFailed()) {\r\n <div class=\"fly-currency-selector__status fly-currency-selector__status--error\" role=\"alert\">\r\n <span>{{ errorText() }}</span>\r\n <button type=\"button\" class=\"fly-currency-selector__retry\" (click)=\"retry()\">\r\n {{ retryText() }}\r\n </button>\r\n </div>\r\n }\r\n\r\n <div\r\n class=\"fly-currency-selector__listbox\"\r\n role=\"listbox\"\r\n [id]=\"listboxId\"\r\n [attr.aria-multiselectable]=\"isMulti() ? true : null\"\r\n [attr.aria-activedescendant]=\"activeDescendant() || null\"\r\n [attr.aria-label]=\"ariaLabel() || null\"\r\n >\r\n @if (pinned().length > 0) {\r\n <div class=\"fly-currency-selector__group\" role=\"presentation\">{{ pinnedGroupText() }}</div>\r\n @for (c of pinned(); track c.code) {\r\n <ng-container *ngTemplateOutlet=\"row; context: { $implicit: c }\" />\r\n }\r\n @if (rest().length > 0) {\r\n <div class=\"fly-currency-selector__group\" role=\"presentation\">{{ allGroupText() }}</div>\r\n }\r\n }\r\n @for (c of rest(); track c.code) {\r\n <ng-container *ngTemplateOutlet=\"row; context: { $implicit: c }\" />\r\n }\r\n\r\n @if (!loading() && !loadFailed() && filtered().length === 0) {\r\n <div class=\"fly-currency-selector__status\" role=\"presentation\">{{ noResultsText() }}</div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n\r\n<!-- \u2500\u2500 One option row, shared by the pinned and full groups \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n<ng-template #row let-c>\r\n <div\r\n class=\"fly-currency-selector__option\"\r\n role=\"option\"\r\n tabindex=\"-1\"\r\n [id]=\"optionId(navIndexOf(c.code))\"\r\n [attr.aria-selected]=\"isSelected(c.code)\"\r\n [class.fly-currency-selector__option--selected]=\"isSelected(c.code)\"\r\n [class.fly-currency-selector__option--active]=\"navIndexOf(c.code) === activeIndex()\"\r\n (click)=\"pick(c)\"\r\n (keydown.enter)=\"pick(c)\"\r\n (mouseenter)=\"onOptionHover(navIndexOf(c.code))\"\r\n >\r\n <span class=\"fly-currency-selector__tile\" aria-hidden=\"true\">\r\n @if (flagsRenderable() && c.flag) {\r\n <span class=\"fly-currency-selector__tile-flag\">{{ c.flag }}</span>\r\n } @else {\r\n <span class=\"fly-currency-selector__tile-code\">{{ tileCode(c) }}</span>\r\n }\r\n </span>\r\n\r\n <span class=\"fly-currency-selector__option-text\">\r\n <span class=\"fly-currency-selector__option-code\">{{ c.code }}</span>\r\n <span class=\"fly-currency-selector__option-name\">{{ c.name }}</span>\r\n </span>\r\n\r\n <span class=\"fly-currency-selector__option-symbol\" aria-hidden=\"true\">{{ c.symbol }}</span>\r\n\r\n <span class=\"fly-currency-selector__check\" aria-hidden=\"true\">\r\n @if (isSelected(c.code)) { \u2713 }\r\n </span>\r\n </div>\r\n</ng-template>\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-panel: var(--glass-bg-elevated, var(--surface-card, #fff));--_panel-blur: var(--glass-blur, 40px);--_surface-hover: var(--surface-hover, #f1f5f9);--_surface-active: var(--surface-active, #eef2ff);--_border: var(--surface-border, #e2e8f0);--_text: var(--text-color, #0f172a);--_text-subtle: var(--text-color-secondary, #64748b);--_fill: var(--fill-tertiary, #f3f4f6);--_primary: var(--primary-color);--_danger: var(--system-red, #ef4444);--_radius: 8px;display:inline-block;inline-size:100%;min-inline-size:11rem;font-size:13px;color:var(--_text)}.fly-currency-selector__wrap{position:relative;inline-size:100%}.fly-currency-selector__trigger{display:flex;align-items:center;gap:8px;inline-size:100%;min-block-size:2.25rem;padding-block:4px;padding-inline:8px;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);color:var(--_text);font:inherit;text-align:start;cursor:pointer}.fly-currency-selector__trigger:focus-visible{outline:2px solid var(--_primary);outline-offset:2px}.fly-currency-selector__trigger:disabled{opacity:.55;cursor:not-allowed}.fly-currency-selector__placeholder{flex:1 1 auto;padding-inline-start:4px;color:var(--_text-subtle);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.fly-currency-selector__chips{display:flex;flex:1 1 auto;flex-wrap:wrap;gap:4px;min-inline-size:0}.fly-currency-selector__chip{display:inline-flex;align-items:center;gap:5px;padding-block:2px;padding-inline:3px 7px;border-radius:999px;background:var(--_fill);font-size:12px}.fly-currency-selector__chip-code{font-weight:700;letter-spacing:.02em}.fly-currency-selector__chip-symbol{color:var(--_text-subtle)}.fly-currency-selector__chip-remove{padding:1px 3px;border-radius:50%;color:var(--_text-subtle);font-size:10px;line-height:1;cursor:pointer}.fly-currency-selector__chip-remove:hover{background:color-mix(in srgb,var(--_danger) 18%,transparent);color:var(--_danger)}.fly-currency-selector__clear,.fly-currency-selector__chevron{flex:none;color:var(--_text-subtle);font-size:11px;line-height:1}.fly-currency-selector__clear{padding:3px;border-radius:50%;cursor:pointer}.fly-currency-selector__clear:hover{background:var(--_surface-hover);color:var(--_text)}.fly-currency-selector__tile{display:inline-flex;flex:none;align-items:center;justify-content:center;inline-size:26px;block-size:19px;border-radius:4px;background:var(--_fill);overflow:hidden}.fly-currency-selector__tile-flag{font-size:15px;line-height:1}.fly-currency-selector__tile-code{font-size:9px;font-weight:800;letter-spacing:.04em;color:var(--_text-subtle);direction:ltr;unicode-bidi:isolate}.fly-currency-selector__panel{position:absolute;z-index:60;inset-inline:0;inset-block-start:calc(100% + 4px);display:flex;flex-direction:column;max-block-size:19rem;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface-panel);-webkit-backdrop-filter:blur(var(--_panel-blur));backdrop-filter:blur(var(--_panel-blur));box-shadow:0 12px 32px #0000002e;overflow:hidden}.fly-currency-selector__search{padding:6px;border-block-end:1px solid var(--_border)}.fly-currency-selector__search-input{inline-size:100%;padding-block:5px;padding-inline:8px;border:1px solid var(--_border);border-radius:6px;background:var(--_surface);color:var(--_text);font:inherit}.fly-currency-selector__search-input:focus-visible{outline:2px solid var(--_primary);outline-offset:-1px}.fly-currency-selector__listbox{flex:1 1 auto;overflow-y:auto;padding-block:4px}.fly-currency-selector__group{padding-block:6px 3px;padding-inline:10px;color:var(--_text-subtle);font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}.fly-currency-selector__option{display:flex;align-items:center;gap:9px;padding-block:5px;padding-inline:10px;cursor:pointer}.fly-currency-selector__option--active{background:var(--_surface-hover)}.fly-currency-selector__option--selected{background:var(--_surface-active)}.fly-currency-selector__option-text{display:flex;flex:1 1 auto;align-items:baseline;gap:8px;min-inline-size:0}.fly-currency-selector__option-code{flex:none;font-weight:700;letter-spacing:.02em;direction:ltr;unicode-bidi:isolate}.fly-currency-selector__option-name{flex:1 1 auto;min-inline-size:0;color:var(--_text-subtle);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.fly-currency-selector__option-symbol{flex:none;min-inline-size:2.2em;color:var(--_text);text-align:end}.fly-currency-selector__check{flex:none;inline-size:1em;color:var(--_primary)}.fly-currency-selector__status{display:flex;align-items:center;gap:8px;padding-block:10px;padding-inline:10px;color:var(--_text-subtle)}.fly-currency-selector__status--error{color:var(--_danger)}.fly-currency-selector__retry{border:1px solid var(--_border);border-radius:6px;background:var(--_surface);padding-block:2px;padding-inline:8px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: FlyClickOutsideDirective, selector: "[flyClickOutside]", inputs: ["flyClickOutsideEnabled"], outputs: ["flyClickOutside"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
19158
+ ], viewQueries: [{ propertyName: "searchEl", first: true, predicate: ["searchRef"], descendants: true }, { propertyName: "triggerEl", first: true, predicate: ["triggerRef"], descendants: true }], ngImport: i0, template: "<div\r\n class=\"fly-currency-selector__wrap\"\r\n [flyClickOutsideEnabled]=\"isOpen()\"\r\n (flyClickOutside)=\"close()\"\r\n>\r\n @if (locked()) {\r\n <!-- \u2500\u2500 Locked readout \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 NOT a dimmed `disabled` trigger: there is no dropdown affordance here at all \u2014 the\r\n value is frozen on purpose, not merely unavailable right now. `role=\"group\"` +\r\n `aria-label` name the field the way the combobox trigger's `aria-label` does; the\r\n reason is REAL, always-rendered text (not a `title` attribute, which many screen\r\n readers never announce and which no sighted user sees without hovering) and is\r\n additionally wired via `aria-describedby` for AT that reads by relationship rather\r\n than document order. -->\r\n <div\r\n class=\"fly-currency-selector__locked\"\r\n role=\"group\"\r\n [attr.aria-label]=\"ariaLabel() || placeholderText()\"\r\n [attr.aria-describedby]=\"lockedReasonId\"\r\n >\r\n @if (selected().length === 0) {\r\n <span class=\"fly-currency-selector__placeholder\">{{ placeholderText() }}</span>\r\n } @else {\r\n <span class=\"fly-currency-selector__chips\">\r\n @for (c of selected(); track c.code) {\r\n <span class=\"fly-currency-selector__chip\">\r\n <span class=\"fly-currency-selector__tile\" aria-hidden=\"true\">\r\n @if (flagsRenderable() && c.flag) {\r\n <span class=\"fly-currency-selector__tile-flag\">{{ c.flag }}</span>\r\n } @else {\r\n <span class=\"fly-currency-selector__tile-code\">{{ tileCode(c) }}</span>\r\n }\r\n </span>\r\n <span class=\"fly-currency-selector__chip-code\">{{ c.code }}</span>\r\n <span class=\"fly-currency-selector__chip-symbol\" aria-hidden=\"true\">{{ c.symbol }}</span>\r\n </span>\r\n }\r\n </span>\r\n }\r\n <span class=\"fly-currency-selector__lock-icon\" aria-hidden=\"true\">&#128274;</span>\r\n </div>\r\n <p class=\"fly-currency-selector__locked-reason\" [id]=\"lockedReasonId\">{{ lockedReasonText() }}</p>\r\n } @else {\r\n\r\n <!-- \u2500\u2500 Trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 `aria-label` is never null: a `combobox` does NOT take its accessible name from its\r\n contents the way a plain button does, so the chips/placeholder painted inside it name\r\n nothing. Absent a consumer label the localized placeholder supplies the control's\r\n purpose; its VALUE is still announced from the contents. -->\r\n <button\r\n #triggerRef\r\n type=\"button\"\r\n [id]=\"triggerId\"\r\n class=\"fly-currency-selector__trigger\"\r\n role=\"combobox\"\r\n aria-haspopup=\"listbox\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-controls]=\"listboxId\"\r\n [attr.aria-label]=\"ariaLabel() || placeholderText()\"\r\n [class.fly-currency-selector__trigger--empty]=\"!hasSelection()\"\r\n [disabled]=\"effectiveDisabled()\"\r\n (click)=\"toggleOpen()\"\r\n (keydown)=\"onTriggerKeydown($event)\"\r\n >\r\n @if (selected().length === 0) {\r\n <span class=\"fly-currency-selector__placeholder\">{{ placeholderText() }}</span>\r\n } @else {\r\n <span class=\"fly-currency-selector__chips\">\r\n @for (c of selected(); track c.code) {\r\n <span class=\"fly-currency-selector__chip\">\r\n <span class=\"fly-currency-selector__tile\" aria-hidden=\"true\">\r\n @if (flagsRenderable() && c.flag) {\r\n <span class=\"fly-currency-selector__tile-flag\">{{ c.flag }}</span>\r\n } @else {\r\n <span class=\"fly-currency-selector__tile-code\">{{ tileCode(c) }}</span>\r\n }\r\n </span>\r\n <span class=\"fly-currency-selector__chip-code\">{{ c.code }}</span>\r\n <span class=\"fly-currency-selector__chip-symbol\" aria-hidden=\"true\">{{ c.symbol }}</span>\r\n @if (isMulti() && !effectiveDisabled()) {\r\n <span\r\n class=\"fly-currency-selector__chip-remove\"\r\n role=\"button\"\r\n tabindex=\"-1\"\r\n [attr.aria-label]=\"'\u2715 ' + c.code\"\r\n (click)=\"remove(c.code, $event)\"\r\n (mousedown)=\"$event.preventDefault()\"\r\n >\u2715</span>\r\n }\r\n </span>\r\n }\r\n </span>\r\n }\r\n\r\n @if (clearable() && hasSelection() && !effectiveDisabled()) {\r\n <span\r\n class=\"fly-currency-selector__clear\"\r\n role=\"button\"\r\n tabindex=\"-1\"\r\n [attr.aria-label]=\"clearText()\"\r\n [title]=\"clearText()\"\r\n (click)=\"clear($event)\"\r\n (mousedown)=\"$event.preventDefault()\"\r\n >\u2715</span>\r\n }\r\n\r\n <span class=\"fly-currency-selector__chevron\" aria-hidden=\"true\">\u25BE</span>\r\n </button>\r\n\r\n <!-- \u2500\u2500 Panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 @if (isOpen()) {\r\n <div class=\"fly-currency-selector__panel\" tabindex=\"-1\" (keydown)=\"onPanelKeydown($event)\">\r\n <div class=\"fly-currency-selector__search\">\r\n <input\r\n #searchRef\r\n type=\"text\"\r\n class=\"fly-currency-selector__search-input\"\r\n role=\"combobox\"\r\n aria-autocomplete=\"list\"\r\n [attr.aria-expanded]=\"true\"\r\n [attr.aria-controls]=\"listboxId\"\r\n [attr.aria-activedescendant]=\"activeDescendant() || null\"\r\n [attr.aria-label]=\"searchPlaceholderText()\"\r\n [placeholder]=\"searchPlaceholderText()\"\r\n [value]=\"searchTerm()\"\r\n (input)=\"onSearchInput($any($event.target).value)\"\r\n />\r\n </div>\r\n\r\n @if (loading()) {\r\n <div class=\"fly-currency-selector__status\" role=\"status\">{{ loadingText() }}</div>\r\n } @else if (loadFailed()) {\r\n <div class=\"fly-currency-selector__status fly-currency-selector__status--error\" role=\"alert\">\r\n <span>{{ errorText() }}</span>\r\n <button type=\"button\" class=\"fly-currency-selector__retry\" (click)=\"retry()\">\r\n {{ retryText() }}\r\n </button>\r\n </div>\r\n }\r\n\r\n <div\r\n class=\"fly-currency-selector__listbox\"\r\n role=\"listbox\"\r\n [id]=\"listboxId\"\r\n [attr.aria-multiselectable]=\"isMulti() ? true : null\"\r\n [attr.aria-activedescendant]=\"activeDescendant() || null\"\r\n [attr.aria-label]=\"ariaLabel() || null\"\r\n >\r\n @if (pinned().length > 0) {\r\n <div class=\"fly-currency-selector__group\" role=\"presentation\">{{ pinnedGroupText() }}</div>\r\n @for (c of pinned(); track c.code) {\r\n <ng-container *ngTemplateOutlet=\"row; context: { $implicit: c }\" />\r\n }\r\n @if (rest().length > 0) {\r\n <div class=\"fly-currency-selector__group\" role=\"presentation\">{{ allGroupText() }}</div>\r\n }\r\n }\r\n @for (c of rest(); track c.code) {\r\n <ng-container *ngTemplateOutlet=\"row; context: { $implicit: c }\" />\r\n }\r\n\r\n @if (!loading() && !loadFailed() && filtered().length === 0) {\r\n <div class=\"fly-currency-selector__status\" role=\"presentation\">{{ noResultsText() }}</div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n }\r\n</div>\r\n\r\n<!-- \u2500\u2500 One option row, shared by the pinned and full groups \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n<ng-template #row let-c>\r\n <div\r\n class=\"fly-currency-selector__option\"\r\n role=\"option\"\r\n tabindex=\"-1\"\r\n [id]=\"optionId(navIndexOf(c.code))\"\r\n [attr.aria-selected]=\"isSelected(c.code)\"\r\n [class.fly-currency-selector__option--selected]=\"isSelected(c.code)\"\r\n [class.fly-currency-selector__option--active]=\"navIndexOf(c.code) === activeIndex()\"\r\n (click)=\"pick(c)\"\r\n (keydown.enter)=\"pick(c)\"\r\n (mouseenter)=\"onOptionHover(navIndexOf(c.code))\"\r\n >\r\n <span class=\"fly-currency-selector__tile\" aria-hidden=\"true\">\r\n @if (flagsRenderable() && c.flag) {\r\n <span class=\"fly-currency-selector__tile-flag\">{{ c.flag }}</span>\r\n } @else {\r\n <span class=\"fly-currency-selector__tile-code\">{{ tileCode(c) }}</span>\r\n }\r\n </span>\r\n\r\n <span class=\"fly-currency-selector__option-text\">\r\n <span class=\"fly-currency-selector__option-code\">{{ c.code }}</span>\r\n <span class=\"fly-currency-selector__option-name\">{{ c.name }}</span>\r\n </span>\r\n\r\n <span class=\"fly-currency-selector__option-symbol\" aria-hidden=\"true\">{{ c.symbol }}</span>\r\n\r\n <span class=\"fly-currency-selector__check\" aria-hidden=\"true\">\r\n @if (isSelected(c.code)) { \u2713 }\r\n </span>\r\n </div>\r\n</ng-template>\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-panel: var(--glass-bg-elevated, var(--surface-card, #fff));--_panel-blur: var(--glass-blur, 40px);--_surface-hover: var(--surface-hover, #f1f5f9);--_surface-active: var(--surface-active, #eef2ff);--_border: var(--surface-border, #e2e8f0);--_text: var(--text-color, #0f172a);--_text-subtle: var(--text-color-secondary, #64748b);--_fill: var(--fill-tertiary, #f3f4f6);--_primary: var(--primary-color);--_danger: var(--system-red, #ef4444);--_radius: 8px;display:inline-block;inline-size:100%;min-inline-size:11rem;font-size:13px;color:var(--_text)}.fly-currency-selector__wrap{position:relative;inline-size:100%}.fly-currency-selector__trigger{display:flex;align-items:center;gap:8px;inline-size:100%;min-block-size:2.25rem;padding-block:4px;padding-inline:8px;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);color:var(--_text);font:inherit;text-align:start;cursor:pointer}.fly-currency-selector__trigger:focus-visible{outline:2px solid var(--_primary);outline-offset:2px}.fly-currency-selector__trigger:disabled{opacity:.55;cursor:not-allowed}.fly-currency-selector__placeholder{flex:1 1 auto;padding-inline-start:4px;color:var(--_text-subtle);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.fly-currency-selector__chips{display:flex;flex:1 1 auto;flex-wrap:wrap;gap:4px;min-inline-size:0}.fly-currency-selector__chip{display:inline-flex;align-items:center;gap:5px;padding-block:2px;padding-inline:3px 7px;border-radius:999px;background:var(--_fill);font-size:12px}.fly-currency-selector__chip-code{font-weight:700;letter-spacing:.02em}.fly-currency-selector__chip-symbol{color:var(--_text-subtle)}.fly-currency-selector__chip-remove{padding:1px 3px;border-radius:50%;color:var(--_text-subtle);font-size:10px;line-height:1;cursor:pointer}.fly-currency-selector__chip-remove:hover{background:color-mix(in srgb,var(--_danger) 18%,transparent);color:var(--_danger)}.fly-currency-selector__clear,.fly-currency-selector__chevron{flex:none;color:var(--_text-subtle);font-size:11px;line-height:1}.fly-currency-selector__clear{padding:3px;border-radius:50%;cursor:pointer}.fly-currency-selector__clear:hover{background:var(--_surface-hover);color:var(--_text)}.fly-currency-selector__tile{display:inline-flex;flex:none;align-items:center;justify-content:center;inline-size:26px;block-size:19px;border-radius:4px;background:var(--_fill);overflow:hidden}.fly-currency-selector__tile-flag{font-size:15px;line-height:1}.fly-currency-selector__tile-code{font-size:9px;font-weight:800;letter-spacing:.04em;color:var(--_text-subtle);direction:ltr;unicode-bidi:isolate}.fly-currency-selector__locked{display:flex;align-items:center;gap:8px;inline-size:100%;min-block-size:2.25rem;padding-block:4px;padding-inline:8px;border:1px dashed var(--_border);border-radius:var(--_radius);background:var(--_fill);color:var(--_text);cursor:default}.fly-currency-selector__lock-icon{flex:none;margin-inline-start:auto;font-size:12px;opacity:.7}.fly-currency-selector__locked-reason{margin:4px 0 0;color:var(--_text-subtle);font-size:11px;line-height:1.4}.fly-currency-selector__panel{position:absolute;z-index:60;inset-inline:0;inset-block-start:calc(100% + 4px);display:flex;flex-direction:column;max-block-size:19rem;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface-panel);-webkit-backdrop-filter:blur(var(--_panel-blur));backdrop-filter:blur(var(--_panel-blur));box-shadow:0 12px 32px #0000002e;overflow:hidden}.fly-currency-selector__search{padding:6px;border-block-end:1px solid var(--_border)}.fly-currency-selector__search-input{inline-size:100%;padding-block:5px;padding-inline:8px;border:1px solid var(--_border);border-radius:6px;background:var(--_surface);color:var(--_text);font:inherit}.fly-currency-selector__search-input:focus-visible{outline:2px solid var(--_primary);outline-offset:-1px}.fly-currency-selector__listbox{flex:1 1 auto;overflow-y:auto;padding-block:4px}.fly-currency-selector__group{padding-block:6px 3px;padding-inline:10px;color:var(--_text-subtle);font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}.fly-currency-selector__option{display:flex;align-items:center;gap:9px;padding-block:5px;padding-inline:10px;cursor:pointer}.fly-currency-selector__option--active{background:var(--_surface-hover)}.fly-currency-selector__option--selected{background:var(--_surface-active)}.fly-currency-selector__option-text{display:flex;flex:1 1 auto;align-items:baseline;gap:8px;min-inline-size:0}.fly-currency-selector__option-code{flex:none;font-weight:700;letter-spacing:.02em;direction:ltr;unicode-bidi:isolate}.fly-currency-selector__option-name{flex:1 1 auto;min-inline-size:0;color:var(--_text-subtle);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.fly-currency-selector__option-symbol{flex:none;min-inline-size:2.2em;color:var(--_text);text-align:end}.fly-currency-selector__check{flex:none;inline-size:1em;color:var(--_primary)}.fly-currency-selector__status{display:flex;align-items:center;gap:8px;padding-block:10px;padding-inline:10px;color:var(--_text-subtle)}.fly-currency-selector__status--error{color:var(--_danger)}.fly-currency-selector__retry{border:1px solid var(--_border);border-radius:6px;background:var(--_surface);padding-block:2px;padding-inline:8px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: FlyClickOutsideDirective, selector: "[flyClickOutside]", inputs: ["flyClickOutsideEnabled"], outputs: ["flyClickOutside"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
18959
19159
  }
18960
19160
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyCurrencySelectorComponent, decorators: [{
18961
19161
  type: Component,
@@ -18969,8 +19169,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
18969
19169
  class: 'fly-currency-selector',
18970
19170
  '[class.fly-currency-selector--open]': 'isOpen()',
18971
19171
  '[class.fly-currency-selector--disabled]': 'effectiveDisabled()',
18972
- }, template: "<div\r\n class=\"fly-currency-selector__wrap\"\r\n [flyClickOutsideEnabled]=\"isOpen()\"\r\n (flyClickOutside)=\"close()\"\r\n>\r\n <!-- \u2500\u2500 Trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 `aria-label` is never null: a `combobox` does NOT take its accessible name from its\r\n contents the way a plain button does, so the chips/placeholder painted inside it name\r\n nothing. Absent a consumer label the localized placeholder supplies the control's\r\n purpose; its VALUE is still announced from the contents. -->\r\n <button\r\n #triggerRef\r\n type=\"button\"\r\n [id]=\"triggerId\"\r\n class=\"fly-currency-selector__trigger\"\r\n role=\"combobox\"\r\n aria-haspopup=\"listbox\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-controls]=\"listboxId\"\r\n [attr.aria-label]=\"ariaLabel() || placeholderText()\"\r\n [class.fly-currency-selector__trigger--empty]=\"!hasSelection()\"\r\n [disabled]=\"effectiveDisabled()\"\r\n (click)=\"toggleOpen()\"\r\n (keydown)=\"onTriggerKeydown($event)\"\r\n >\r\n @if (selected().length === 0) {\r\n <span class=\"fly-currency-selector__placeholder\">{{ placeholderText() }}</span>\r\n } @else {\r\n <span class=\"fly-currency-selector__chips\">\r\n @for (c of selected(); track c.code) {\r\n <span class=\"fly-currency-selector__chip\">\r\n <span class=\"fly-currency-selector__tile\" aria-hidden=\"true\">\r\n @if (flagsRenderable() && c.flag) {\r\n <span class=\"fly-currency-selector__tile-flag\">{{ c.flag }}</span>\r\n } @else {\r\n <span class=\"fly-currency-selector__tile-code\">{{ tileCode(c) }}</span>\r\n }\r\n </span>\r\n <span class=\"fly-currency-selector__chip-code\">{{ c.code }}</span>\r\n <span class=\"fly-currency-selector__chip-symbol\" aria-hidden=\"true\">{{ c.symbol }}</span>\r\n @if (isMulti() && !effectiveDisabled()) {\r\n <span\r\n class=\"fly-currency-selector__chip-remove\"\r\n role=\"button\"\r\n tabindex=\"-1\"\r\n [attr.aria-label]=\"'\u2715 ' + c.code\"\r\n (click)=\"remove(c.code, $event)\"\r\n (mousedown)=\"$event.preventDefault()\"\r\n >\u2715</span>\r\n }\r\n </span>\r\n }\r\n </span>\r\n }\r\n\r\n @if (clearable() && hasSelection() && !effectiveDisabled()) {\r\n <span\r\n class=\"fly-currency-selector__clear\"\r\n role=\"button\"\r\n tabindex=\"-1\"\r\n [attr.aria-label]=\"clearText()\"\r\n [title]=\"clearText()\"\r\n (click)=\"clear($event)\"\r\n (mousedown)=\"$event.preventDefault()\"\r\n >\u2715</span>\r\n }\r\n\r\n <span class=\"fly-currency-selector__chevron\" aria-hidden=\"true\">\u25BE</span>\r\n </button>\r\n\r\n <!-- \u2500\u2500 Panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 @if (isOpen()) {\r\n <div class=\"fly-currency-selector__panel\" tabindex=\"-1\" (keydown)=\"onPanelKeydown($event)\">\r\n <div class=\"fly-currency-selector__search\">\r\n <input\r\n #searchRef\r\n type=\"text\"\r\n class=\"fly-currency-selector__search-input\"\r\n role=\"combobox\"\r\n aria-autocomplete=\"list\"\r\n [attr.aria-expanded]=\"true\"\r\n [attr.aria-controls]=\"listboxId\"\r\n [attr.aria-activedescendant]=\"activeDescendant() || null\"\r\n [attr.aria-label]=\"searchPlaceholderText()\"\r\n [placeholder]=\"searchPlaceholderText()\"\r\n [value]=\"searchTerm()\"\r\n (input)=\"onSearchInput($any($event.target).value)\"\r\n />\r\n </div>\r\n\r\n @if (loading()) {\r\n <div class=\"fly-currency-selector__status\" role=\"status\">{{ loadingText() }}</div>\r\n } @else if (loadFailed()) {\r\n <div class=\"fly-currency-selector__status fly-currency-selector__status--error\" role=\"alert\">\r\n <span>{{ errorText() }}</span>\r\n <button type=\"button\" class=\"fly-currency-selector__retry\" (click)=\"retry()\">\r\n {{ retryText() }}\r\n </button>\r\n </div>\r\n }\r\n\r\n <div\r\n class=\"fly-currency-selector__listbox\"\r\n role=\"listbox\"\r\n [id]=\"listboxId\"\r\n [attr.aria-multiselectable]=\"isMulti() ? true : null\"\r\n [attr.aria-activedescendant]=\"activeDescendant() || null\"\r\n [attr.aria-label]=\"ariaLabel() || null\"\r\n >\r\n @if (pinned().length > 0) {\r\n <div class=\"fly-currency-selector__group\" role=\"presentation\">{{ pinnedGroupText() }}</div>\r\n @for (c of pinned(); track c.code) {\r\n <ng-container *ngTemplateOutlet=\"row; context: { $implicit: c }\" />\r\n }\r\n @if (rest().length > 0) {\r\n <div class=\"fly-currency-selector__group\" role=\"presentation\">{{ allGroupText() }}</div>\r\n }\r\n }\r\n @for (c of rest(); track c.code) {\r\n <ng-container *ngTemplateOutlet=\"row; context: { $implicit: c }\" />\r\n }\r\n\r\n @if (!loading() && !loadFailed() && filtered().length === 0) {\r\n <div class=\"fly-currency-selector__status\" role=\"presentation\">{{ noResultsText() }}</div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n\r\n<!-- \u2500\u2500 One option row, shared by the pinned and full groups \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n<ng-template #row let-c>\r\n <div\r\n class=\"fly-currency-selector__option\"\r\n role=\"option\"\r\n tabindex=\"-1\"\r\n [id]=\"optionId(navIndexOf(c.code))\"\r\n [attr.aria-selected]=\"isSelected(c.code)\"\r\n [class.fly-currency-selector__option--selected]=\"isSelected(c.code)\"\r\n [class.fly-currency-selector__option--active]=\"navIndexOf(c.code) === activeIndex()\"\r\n (click)=\"pick(c)\"\r\n (keydown.enter)=\"pick(c)\"\r\n (mouseenter)=\"onOptionHover(navIndexOf(c.code))\"\r\n >\r\n <span class=\"fly-currency-selector__tile\" aria-hidden=\"true\">\r\n @if (flagsRenderable() && c.flag) {\r\n <span class=\"fly-currency-selector__tile-flag\">{{ c.flag }}</span>\r\n } @else {\r\n <span class=\"fly-currency-selector__tile-code\">{{ tileCode(c) }}</span>\r\n }\r\n </span>\r\n\r\n <span class=\"fly-currency-selector__option-text\">\r\n <span class=\"fly-currency-selector__option-code\">{{ c.code }}</span>\r\n <span class=\"fly-currency-selector__option-name\">{{ c.name }}</span>\r\n </span>\r\n\r\n <span class=\"fly-currency-selector__option-symbol\" aria-hidden=\"true\">{{ c.symbol }}</span>\r\n\r\n <span class=\"fly-currency-selector__check\" aria-hidden=\"true\">\r\n @if (isSelected(c.code)) { \u2713 }\r\n </span>\r\n </div>\r\n</ng-template>\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-panel: var(--glass-bg-elevated, var(--surface-card, #fff));--_panel-blur: var(--glass-blur, 40px);--_surface-hover: var(--surface-hover, #f1f5f9);--_surface-active: var(--surface-active, #eef2ff);--_border: var(--surface-border, #e2e8f0);--_text: var(--text-color, #0f172a);--_text-subtle: var(--text-color-secondary, #64748b);--_fill: var(--fill-tertiary, #f3f4f6);--_primary: var(--primary-color);--_danger: var(--system-red, #ef4444);--_radius: 8px;display:inline-block;inline-size:100%;min-inline-size:11rem;font-size:13px;color:var(--_text)}.fly-currency-selector__wrap{position:relative;inline-size:100%}.fly-currency-selector__trigger{display:flex;align-items:center;gap:8px;inline-size:100%;min-block-size:2.25rem;padding-block:4px;padding-inline:8px;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);color:var(--_text);font:inherit;text-align:start;cursor:pointer}.fly-currency-selector__trigger:focus-visible{outline:2px solid var(--_primary);outline-offset:2px}.fly-currency-selector__trigger:disabled{opacity:.55;cursor:not-allowed}.fly-currency-selector__placeholder{flex:1 1 auto;padding-inline-start:4px;color:var(--_text-subtle);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.fly-currency-selector__chips{display:flex;flex:1 1 auto;flex-wrap:wrap;gap:4px;min-inline-size:0}.fly-currency-selector__chip{display:inline-flex;align-items:center;gap:5px;padding-block:2px;padding-inline:3px 7px;border-radius:999px;background:var(--_fill);font-size:12px}.fly-currency-selector__chip-code{font-weight:700;letter-spacing:.02em}.fly-currency-selector__chip-symbol{color:var(--_text-subtle)}.fly-currency-selector__chip-remove{padding:1px 3px;border-radius:50%;color:var(--_text-subtle);font-size:10px;line-height:1;cursor:pointer}.fly-currency-selector__chip-remove:hover{background:color-mix(in srgb,var(--_danger) 18%,transparent);color:var(--_danger)}.fly-currency-selector__clear,.fly-currency-selector__chevron{flex:none;color:var(--_text-subtle);font-size:11px;line-height:1}.fly-currency-selector__clear{padding:3px;border-radius:50%;cursor:pointer}.fly-currency-selector__clear:hover{background:var(--_surface-hover);color:var(--_text)}.fly-currency-selector__tile{display:inline-flex;flex:none;align-items:center;justify-content:center;inline-size:26px;block-size:19px;border-radius:4px;background:var(--_fill);overflow:hidden}.fly-currency-selector__tile-flag{font-size:15px;line-height:1}.fly-currency-selector__tile-code{font-size:9px;font-weight:800;letter-spacing:.04em;color:var(--_text-subtle);direction:ltr;unicode-bidi:isolate}.fly-currency-selector__panel{position:absolute;z-index:60;inset-inline:0;inset-block-start:calc(100% + 4px);display:flex;flex-direction:column;max-block-size:19rem;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface-panel);-webkit-backdrop-filter:blur(var(--_panel-blur));backdrop-filter:blur(var(--_panel-blur));box-shadow:0 12px 32px #0000002e;overflow:hidden}.fly-currency-selector__search{padding:6px;border-block-end:1px solid var(--_border)}.fly-currency-selector__search-input{inline-size:100%;padding-block:5px;padding-inline:8px;border:1px solid var(--_border);border-radius:6px;background:var(--_surface);color:var(--_text);font:inherit}.fly-currency-selector__search-input:focus-visible{outline:2px solid var(--_primary);outline-offset:-1px}.fly-currency-selector__listbox{flex:1 1 auto;overflow-y:auto;padding-block:4px}.fly-currency-selector__group{padding-block:6px 3px;padding-inline:10px;color:var(--_text-subtle);font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}.fly-currency-selector__option{display:flex;align-items:center;gap:9px;padding-block:5px;padding-inline:10px;cursor:pointer}.fly-currency-selector__option--active{background:var(--_surface-hover)}.fly-currency-selector__option--selected{background:var(--_surface-active)}.fly-currency-selector__option-text{display:flex;flex:1 1 auto;align-items:baseline;gap:8px;min-inline-size:0}.fly-currency-selector__option-code{flex:none;font-weight:700;letter-spacing:.02em;direction:ltr;unicode-bidi:isolate}.fly-currency-selector__option-name{flex:1 1 auto;min-inline-size:0;color:var(--_text-subtle);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.fly-currency-selector__option-symbol{flex:none;min-inline-size:2.2em;color:var(--_text);text-align:end}.fly-currency-selector__check{flex:none;inline-size:1em;color:var(--_primary)}.fly-currency-selector__status{display:flex;align-items:center;gap:8px;padding-block:10px;padding-inline:10px;color:var(--_text-subtle)}.fly-currency-selector__status--error{color:var(--_danger)}.fly-currency-selector__retry{border:1px solid var(--_border);border-radius:6px;background:var(--_surface);padding-block:2px;padding-inline:8px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}\n"] }]
18973
- }], ctorParameters: () => [], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], currencies: [{ type: i0.Input, args: [{ isSignal: true, alias: "currencies", required: false }] }], fetchFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "fetchFn", required: false }] }], allowedCodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowedCodes", required: false }] }], pinnedCodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "pinnedCodes", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], selectionDetailChange: [{ type: i0.Output, args: ["selectionDetailChange"] }], openedChange: [{ type: i0.Output, args: ["openedChange"] }], searchEl: [{
19172
+ '[class.fly-currency-selector--locked]': 'locked()',
19173
+ }, template: "<div\r\n class=\"fly-currency-selector__wrap\"\r\n [flyClickOutsideEnabled]=\"isOpen()\"\r\n (flyClickOutside)=\"close()\"\r\n>\r\n @if (locked()) {\r\n <!-- \u2500\u2500 Locked readout \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 NOT a dimmed `disabled` trigger: there is no dropdown affordance here at all \u2014 the\r\n value is frozen on purpose, not merely unavailable right now. `role=\"group\"` +\r\n `aria-label` name the field the way the combobox trigger's `aria-label` does; the\r\n reason is REAL, always-rendered text (not a `title` attribute, which many screen\r\n readers never announce and which no sighted user sees without hovering) and is\r\n additionally wired via `aria-describedby` for AT that reads by relationship rather\r\n than document order. -->\r\n <div\r\n class=\"fly-currency-selector__locked\"\r\n role=\"group\"\r\n [attr.aria-label]=\"ariaLabel() || placeholderText()\"\r\n [attr.aria-describedby]=\"lockedReasonId\"\r\n >\r\n @if (selected().length === 0) {\r\n <span class=\"fly-currency-selector__placeholder\">{{ placeholderText() }}</span>\r\n } @else {\r\n <span class=\"fly-currency-selector__chips\">\r\n @for (c of selected(); track c.code) {\r\n <span class=\"fly-currency-selector__chip\">\r\n <span class=\"fly-currency-selector__tile\" aria-hidden=\"true\">\r\n @if (flagsRenderable() && c.flag) {\r\n <span class=\"fly-currency-selector__tile-flag\">{{ c.flag }}</span>\r\n } @else {\r\n <span class=\"fly-currency-selector__tile-code\">{{ tileCode(c) }}</span>\r\n }\r\n </span>\r\n <span class=\"fly-currency-selector__chip-code\">{{ c.code }}</span>\r\n <span class=\"fly-currency-selector__chip-symbol\" aria-hidden=\"true\">{{ c.symbol }}</span>\r\n </span>\r\n }\r\n </span>\r\n }\r\n <span class=\"fly-currency-selector__lock-icon\" aria-hidden=\"true\">&#128274;</span>\r\n </div>\r\n <p class=\"fly-currency-selector__locked-reason\" [id]=\"lockedReasonId\">{{ lockedReasonText() }}</p>\r\n } @else {\r\n\r\n <!-- \u2500\u2500 Trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 `aria-label` is never null: a `combobox` does NOT take its accessible name from its\r\n contents the way a plain button does, so the chips/placeholder painted inside it name\r\n nothing. Absent a consumer label the localized placeholder supplies the control's\r\n purpose; its VALUE is still announced from the contents. -->\r\n <button\r\n #triggerRef\r\n type=\"button\"\r\n [id]=\"triggerId\"\r\n class=\"fly-currency-selector__trigger\"\r\n role=\"combobox\"\r\n aria-haspopup=\"listbox\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-controls]=\"listboxId\"\r\n [attr.aria-label]=\"ariaLabel() || placeholderText()\"\r\n [class.fly-currency-selector__trigger--empty]=\"!hasSelection()\"\r\n [disabled]=\"effectiveDisabled()\"\r\n (click)=\"toggleOpen()\"\r\n (keydown)=\"onTriggerKeydown($event)\"\r\n >\r\n @if (selected().length === 0) {\r\n <span class=\"fly-currency-selector__placeholder\">{{ placeholderText() }}</span>\r\n } @else {\r\n <span class=\"fly-currency-selector__chips\">\r\n @for (c of selected(); track c.code) {\r\n <span class=\"fly-currency-selector__chip\">\r\n <span class=\"fly-currency-selector__tile\" aria-hidden=\"true\">\r\n @if (flagsRenderable() && c.flag) {\r\n <span class=\"fly-currency-selector__tile-flag\">{{ c.flag }}</span>\r\n } @else {\r\n <span class=\"fly-currency-selector__tile-code\">{{ tileCode(c) }}</span>\r\n }\r\n </span>\r\n <span class=\"fly-currency-selector__chip-code\">{{ c.code }}</span>\r\n <span class=\"fly-currency-selector__chip-symbol\" aria-hidden=\"true\">{{ c.symbol }}</span>\r\n @if (isMulti() && !effectiveDisabled()) {\r\n <span\r\n class=\"fly-currency-selector__chip-remove\"\r\n role=\"button\"\r\n tabindex=\"-1\"\r\n [attr.aria-label]=\"'\u2715 ' + c.code\"\r\n (click)=\"remove(c.code, $event)\"\r\n (mousedown)=\"$event.preventDefault()\"\r\n >\u2715</span>\r\n }\r\n </span>\r\n }\r\n </span>\r\n }\r\n\r\n @if (clearable() && hasSelection() && !effectiveDisabled()) {\r\n <span\r\n class=\"fly-currency-selector__clear\"\r\n role=\"button\"\r\n tabindex=\"-1\"\r\n [attr.aria-label]=\"clearText()\"\r\n [title]=\"clearText()\"\r\n (click)=\"clear($event)\"\r\n (mousedown)=\"$event.preventDefault()\"\r\n >\u2715</span>\r\n }\r\n\r\n <span class=\"fly-currency-selector__chevron\" aria-hidden=\"true\">\u25BE</span>\r\n </button>\r\n\r\n <!-- \u2500\u2500 Panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 @if (isOpen()) {\r\n <div class=\"fly-currency-selector__panel\" tabindex=\"-1\" (keydown)=\"onPanelKeydown($event)\">\r\n <div class=\"fly-currency-selector__search\">\r\n <input\r\n #searchRef\r\n type=\"text\"\r\n class=\"fly-currency-selector__search-input\"\r\n role=\"combobox\"\r\n aria-autocomplete=\"list\"\r\n [attr.aria-expanded]=\"true\"\r\n [attr.aria-controls]=\"listboxId\"\r\n [attr.aria-activedescendant]=\"activeDescendant() || null\"\r\n [attr.aria-label]=\"searchPlaceholderText()\"\r\n [placeholder]=\"searchPlaceholderText()\"\r\n [value]=\"searchTerm()\"\r\n (input)=\"onSearchInput($any($event.target).value)\"\r\n />\r\n </div>\r\n\r\n @if (loading()) {\r\n <div class=\"fly-currency-selector__status\" role=\"status\">{{ loadingText() }}</div>\r\n } @else if (loadFailed()) {\r\n <div class=\"fly-currency-selector__status fly-currency-selector__status--error\" role=\"alert\">\r\n <span>{{ errorText() }}</span>\r\n <button type=\"button\" class=\"fly-currency-selector__retry\" (click)=\"retry()\">\r\n {{ retryText() }}\r\n </button>\r\n </div>\r\n }\r\n\r\n <div\r\n class=\"fly-currency-selector__listbox\"\r\n role=\"listbox\"\r\n [id]=\"listboxId\"\r\n [attr.aria-multiselectable]=\"isMulti() ? true : null\"\r\n [attr.aria-activedescendant]=\"activeDescendant() || null\"\r\n [attr.aria-label]=\"ariaLabel() || null\"\r\n >\r\n @if (pinned().length > 0) {\r\n <div class=\"fly-currency-selector__group\" role=\"presentation\">{{ pinnedGroupText() }}</div>\r\n @for (c of pinned(); track c.code) {\r\n <ng-container *ngTemplateOutlet=\"row; context: { $implicit: c }\" />\r\n }\r\n @if (rest().length > 0) {\r\n <div class=\"fly-currency-selector__group\" role=\"presentation\">{{ allGroupText() }}</div>\r\n }\r\n }\r\n @for (c of rest(); track c.code) {\r\n <ng-container *ngTemplateOutlet=\"row; context: { $implicit: c }\" />\r\n }\r\n\r\n @if (!loading() && !loadFailed() && filtered().length === 0) {\r\n <div class=\"fly-currency-selector__status\" role=\"presentation\">{{ noResultsText() }}</div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n }\r\n</div>\r\n\r\n<!-- \u2500\u2500 One option row, shared by the pinned and full groups \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n<ng-template #row let-c>\r\n <div\r\n class=\"fly-currency-selector__option\"\r\n role=\"option\"\r\n tabindex=\"-1\"\r\n [id]=\"optionId(navIndexOf(c.code))\"\r\n [attr.aria-selected]=\"isSelected(c.code)\"\r\n [class.fly-currency-selector__option--selected]=\"isSelected(c.code)\"\r\n [class.fly-currency-selector__option--active]=\"navIndexOf(c.code) === activeIndex()\"\r\n (click)=\"pick(c)\"\r\n (keydown.enter)=\"pick(c)\"\r\n (mouseenter)=\"onOptionHover(navIndexOf(c.code))\"\r\n >\r\n <span class=\"fly-currency-selector__tile\" aria-hidden=\"true\">\r\n @if (flagsRenderable() && c.flag) {\r\n <span class=\"fly-currency-selector__tile-flag\">{{ c.flag }}</span>\r\n } @else {\r\n <span class=\"fly-currency-selector__tile-code\">{{ tileCode(c) }}</span>\r\n }\r\n </span>\r\n\r\n <span class=\"fly-currency-selector__option-text\">\r\n <span class=\"fly-currency-selector__option-code\">{{ c.code }}</span>\r\n <span class=\"fly-currency-selector__option-name\">{{ c.name }}</span>\r\n </span>\r\n\r\n <span class=\"fly-currency-selector__option-symbol\" aria-hidden=\"true\">{{ c.symbol }}</span>\r\n\r\n <span class=\"fly-currency-selector__check\" aria-hidden=\"true\">\r\n @if (isSelected(c.code)) { \u2713 }\r\n </span>\r\n </div>\r\n</ng-template>\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-panel: var(--glass-bg-elevated, var(--surface-card, #fff));--_panel-blur: var(--glass-blur, 40px);--_surface-hover: var(--surface-hover, #f1f5f9);--_surface-active: var(--surface-active, #eef2ff);--_border: var(--surface-border, #e2e8f0);--_text: var(--text-color, #0f172a);--_text-subtle: var(--text-color-secondary, #64748b);--_fill: var(--fill-tertiary, #f3f4f6);--_primary: var(--primary-color);--_danger: var(--system-red, #ef4444);--_radius: 8px;display:inline-block;inline-size:100%;min-inline-size:11rem;font-size:13px;color:var(--_text)}.fly-currency-selector__wrap{position:relative;inline-size:100%}.fly-currency-selector__trigger{display:flex;align-items:center;gap:8px;inline-size:100%;min-block-size:2.25rem;padding-block:4px;padding-inline:8px;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);color:var(--_text);font:inherit;text-align:start;cursor:pointer}.fly-currency-selector__trigger:focus-visible{outline:2px solid var(--_primary);outline-offset:2px}.fly-currency-selector__trigger:disabled{opacity:.55;cursor:not-allowed}.fly-currency-selector__placeholder{flex:1 1 auto;padding-inline-start:4px;color:var(--_text-subtle);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.fly-currency-selector__chips{display:flex;flex:1 1 auto;flex-wrap:wrap;gap:4px;min-inline-size:0}.fly-currency-selector__chip{display:inline-flex;align-items:center;gap:5px;padding-block:2px;padding-inline:3px 7px;border-radius:999px;background:var(--_fill);font-size:12px}.fly-currency-selector__chip-code{font-weight:700;letter-spacing:.02em}.fly-currency-selector__chip-symbol{color:var(--_text-subtle)}.fly-currency-selector__chip-remove{padding:1px 3px;border-radius:50%;color:var(--_text-subtle);font-size:10px;line-height:1;cursor:pointer}.fly-currency-selector__chip-remove:hover{background:color-mix(in srgb,var(--_danger) 18%,transparent);color:var(--_danger)}.fly-currency-selector__clear,.fly-currency-selector__chevron{flex:none;color:var(--_text-subtle);font-size:11px;line-height:1}.fly-currency-selector__clear{padding:3px;border-radius:50%;cursor:pointer}.fly-currency-selector__clear:hover{background:var(--_surface-hover);color:var(--_text)}.fly-currency-selector__tile{display:inline-flex;flex:none;align-items:center;justify-content:center;inline-size:26px;block-size:19px;border-radius:4px;background:var(--_fill);overflow:hidden}.fly-currency-selector__tile-flag{font-size:15px;line-height:1}.fly-currency-selector__tile-code{font-size:9px;font-weight:800;letter-spacing:.04em;color:var(--_text-subtle);direction:ltr;unicode-bidi:isolate}.fly-currency-selector__locked{display:flex;align-items:center;gap:8px;inline-size:100%;min-block-size:2.25rem;padding-block:4px;padding-inline:8px;border:1px dashed var(--_border);border-radius:var(--_radius);background:var(--_fill);color:var(--_text);cursor:default}.fly-currency-selector__lock-icon{flex:none;margin-inline-start:auto;font-size:12px;opacity:.7}.fly-currency-selector__locked-reason{margin:4px 0 0;color:var(--_text-subtle);font-size:11px;line-height:1.4}.fly-currency-selector__panel{position:absolute;z-index:60;inset-inline:0;inset-block-start:calc(100% + 4px);display:flex;flex-direction:column;max-block-size:19rem;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface-panel);-webkit-backdrop-filter:blur(var(--_panel-blur));backdrop-filter:blur(var(--_panel-blur));box-shadow:0 12px 32px #0000002e;overflow:hidden}.fly-currency-selector__search{padding:6px;border-block-end:1px solid var(--_border)}.fly-currency-selector__search-input{inline-size:100%;padding-block:5px;padding-inline:8px;border:1px solid var(--_border);border-radius:6px;background:var(--_surface);color:var(--_text);font:inherit}.fly-currency-selector__search-input:focus-visible{outline:2px solid var(--_primary);outline-offset:-1px}.fly-currency-selector__listbox{flex:1 1 auto;overflow-y:auto;padding-block:4px}.fly-currency-selector__group{padding-block:6px 3px;padding-inline:10px;color:var(--_text-subtle);font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}.fly-currency-selector__option{display:flex;align-items:center;gap:9px;padding-block:5px;padding-inline:10px;cursor:pointer}.fly-currency-selector__option--active{background:var(--_surface-hover)}.fly-currency-selector__option--selected{background:var(--_surface-active)}.fly-currency-selector__option-text{display:flex;flex:1 1 auto;align-items:baseline;gap:8px;min-inline-size:0}.fly-currency-selector__option-code{flex:none;font-weight:700;letter-spacing:.02em;direction:ltr;unicode-bidi:isolate}.fly-currency-selector__option-name{flex:1 1 auto;min-inline-size:0;color:var(--_text-subtle);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.fly-currency-selector__option-symbol{flex:none;min-inline-size:2.2em;color:var(--_text);text-align:end}.fly-currency-selector__check{flex:none;inline-size:1em;color:var(--_primary)}.fly-currency-selector__status{display:flex;align-items:center;gap:8px;padding-block:10px;padding-inline:10px;color:var(--_text-subtle)}.fly-currency-selector__status--error{color:var(--_danger)}.fly-currency-selector__retry{border:1px solid var(--_border);border-radius:6px;background:var(--_surface);padding-block:2px;padding-inline:8px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}\n"] }]
19174
+ }], ctorParameters: () => [], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], currencies: [{ type: i0.Input, args: [{ isSignal: true, alias: "currencies", required: false }] }], fetchFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "fetchFn", required: false }] }], allowedCodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowedCodes", required: false }] }], pinnedCodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "pinnedCodes", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], locked: [{ type: i0.Input, args: [{ isSignal: true, alias: "locked", required: false }] }], lockedReasonKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "lockedReasonKey", required: false }] }], lockedReasonParams: [{ type: i0.Input, args: [{ isSignal: true, alias: "lockedReasonParams", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], selectionDetailChange: [{ type: i0.Output, args: ["selectionDetailChange"] }], openedChange: [{ type: i0.Output, args: ["openedChange"] }], searchEl: [{
18974
19175
  type: ViewChild,
18975
19176
  args: ['searchRef']
18976
19177
  }], triggerEl: [{
@@ -18979,144 +19180,572 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
18979
19180
  }] } });
18980
19181
 
18981
19182
  /**
18982
- * Document-wide open-overlay stackEscape arbitration for stacked overlays.
18983
- *
18984
- * Every layered overlay (drawer, modal, confirm dialog, menu, popover) listens for
18985
- * Escape at the document level, so without arbitration one keypress closes the whole
18986
- * pile — a confirm dialog opened over a drawer takes the drawer down with it on the
18987
- * first Escape. Each overlay pushes a handle when it opens and removes it when it
18988
- * closes or is destroyed; its Escape handler acts only while its handle is
18989
- * top-of-stack. First Escape then closes the confirm, second the drawer.
18990
- *
18991
- * The stack is pure (no DOM, no DI) so the ordering rules are unit-testable, and the
18992
- * shared {@link overlayStack} singleton spans the whole document — which is the point:
18993
- * independently-owned overlays, including ones in different federated remotes, must
18994
- * arbitrate globally or they cannot know about each other.
18995
- *
18996
- * ## Federation
18997
- * Native Federation shares this package as a singleton, so every app in the shell
18998
- * binds to one stack instance. A remote that forks its own DS copy would get its own
18999
- * stack and lose arbitration against shell overlays — that is what the package's
19000
- * federation singleton guard exists to catch.
19183
+ * Locale-aware formatting primitivesnumbers, byte sizes, relative time, and dates.
19001
19184
  *
19002
- * @example
19003
- * ```ts
19004
- * export class MyDrawer {
19005
- * private handle: OverlayHandle | null = null;
19185
+ * All of these are `Intl`-backed and take an explicit `locale`, which is the whole
19186
+ * point: the estate is full of hand-rolled formatters that hardcode ASCII digits,
19187
+ * English unit strings ("1.4 MB", "5m ago"), or the *browser's* default locale rather
19188
+ * than the app's selected language. Under `ar` / `ur` those render wrong — and i18n is
19189
+ * mandatory on this platform, so this is a correctness surface, not a convenience one.
19006
19190
  *
19007
- * open() { this.handle = overlayStack.push(); }
19008
- * close() { this.handle = overlayStack.remove(this.handle); }
19191
+ * Pure functions here; the `| flyCompact`-style pipes in `format.pipes.ts` wrap them
19192
+ * and default the locale to the active {@link I18nService} language.
19009
19193
  *
19010
- * onEscape() { if (overlayStack.isTop(this.handle)) this.close(); }
19011
- * }
19012
- * ```
19194
+ * ## The em-dash convention
19195
+ * Every formatter returns `'—'` (U+2014) for null / undefined / non-finite input rather
19196
+ * than throwing, `'NaN'`, or an empty string. A visible placeholder keeps table columns
19197
+ * aligned and makes "no value" legible; an empty string reads as a rendering bug.
19013
19198
  */
19014
- class OverlayStack {
19015
- stack = [];
19016
- /** Registers an opening overlay; the returned handle identifies it for later calls. */
19017
- push() {
19018
- const handle = { overlay: true };
19019
- this.stack.push(handle);
19020
- return handle;
19021
- }
19022
- /**
19023
- * Unregisters a closing or destroyed overlay. Tolerates out-of-order removal (an
19024
- * inner overlay torn down after its parent), unknown handles, and `null`.
19025
- *
19026
- * Returns `null` so callers can clear their field in one statement:
19027
- * `this.handle = overlayStack.remove(this.handle)`.
19028
- */
19029
- remove(handle) {
19030
- if (!handle)
19031
- return null;
19032
- const index = this.stack.indexOf(handle);
19033
- if (index !== -1)
19034
- this.stack.splice(index, 1);
19035
- return null;
19036
- }
19037
- /** True when this handle is the topmost open overlay — its Escape handler may act. */
19038
- isTop(handle) {
19039
- return (handle !== null &&
19040
- this.stack.length > 0 &&
19041
- this.stack[this.stack.length - 1] === handle);
19199
+ /** Rendered for null / undefined / non-finite input across every formatter here. */
19200
+ const FLY_EMPTY_VALUE = '—';
19201
+ // ─── Numbers ─────────────────────────────────────────────────────────────────
19202
+ /** At or above this magnitude, switch from grouped-exact to compact (K/M/B) notation. */
19203
+ const FLY_COMPACT_THRESHOLD = 10_000;
19204
+ // Intl formatters are expensive to construct relative to how often these are called in
19205
+ // a table cell or chart label; cache one per distinct configuration.
19206
+ const compactCache = new Map();
19207
+ const integerCache = new Map();
19208
+ const decimalCache = new Map();
19209
+ function compactFormatter(locale) {
19210
+ let f = compactCache.get(locale);
19211
+ if (!f) {
19212
+ f = new Intl.NumberFormat(locale, { notation: 'compact', maximumFractionDigits: 1 });
19213
+ compactCache.set(locale, f);
19042
19214
  }
19043
- /** Number of currently-open overlays. Useful for scroll-lock refcounting. */
19044
- get depth() {
19045
- return this.stack.length;
19215
+ return f;
19216
+ }
19217
+ function integerFormatter(locale) {
19218
+ let f = integerCache.get(locale);
19219
+ if (!f) {
19220
+ f = new Intl.NumberFormat(locale, { maximumFractionDigits: 0 });
19221
+ integerCache.set(locale, f);
19046
19222
  }
19223
+ return f;
19047
19224
  }
19048
19225
  /**
19049
- * The shared, document-wide stack. Import this rather than constructing an
19050
- * `OverlayStack` a private instance cannot arbitrate against anyone else's
19051
- * overlays, which defeats the purpose.
19052
- */
19053
- const overlayStack = new OverlayStack();
19054
-
19055
- /**
19056
- * Focus capture/restore for overlays — the other half of a correct dismiss story.
19057
- *
19058
- * When an overlay opens it moves focus inside itself; when it closes, focus must go
19059
- * back to whatever opened it, or the keyboard user is dumped at the top of the
19060
- * document and has to re-traverse the page. Every hand-rolled modal/drawer in the
19061
- * estate re-implements this with a `restoreFocusTo` field and a `.focus()` call, and
19062
- * most of them miss at least one of the edge cases below.
19063
- *
19064
- * @example
19065
- * ```ts
19066
- * private restore: FocusRestore | null = null;
19226
+ * Compact, scannable form grouped-exact below {@link FLY_COMPACT_THRESHOLD},
19227
+ * unit-compacted above it.
19067
19228
  *
19068
- * open() { this.restore = captureFocus(); }
19069
- * close() { this.restore = restoreFocus(this.restore); }
19070
19229
  * ```
19230
+ * 6 042 → "6,042" (exact; small buckets read best as real numbers)
19231
+ * 12 400 → "12.4K"
19232
+ * 1 900 787 → "1.9M"
19233
+ * 2 300 000 000 → "2.3B"
19234
+ * ```
19235
+ *
19236
+ * Pair with {@link flyFullNumber} in a tooltip or `aria-label` so the exact figure is
19237
+ * always one hover away — compaction is a display affordance, not data loss.
19071
19238
  */
19239
+ function flyCompactNumber(value, locale = 'en') {
19240
+ if (value == null || !Number.isFinite(value))
19241
+ return FLY_EMPTY_VALUE;
19242
+ return Math.abs(value) >= FLY_COMPACT_THRESHOLD
19243
+ ? compactFormatter(locale).format(value)
19244
+ : integerFormatter(locale).format(value);
19245
+ }
19246
+ /** Exact, fully-grouped integer form for tooltips / a11y (e.g. `"1,900,787"`). */
19247
+ function flyFullNumber(value, locale = 'en') {
19248
+ if (value == null || !Number.isFinite(value))
19249
+ return FLY_EMPTY_VALUE;
19250
+ return integerFormatter(locale).format(value);
19251
+ }
19072
19252
  /**
19073
- * Snapshots the currently-focused element so it can be refocused later.
19074
- *
19075
- * Returns a token even when nothing is focused (`document.body` is treated as "no
19076
- * meaningful focus"), so callers never branch — {@link restoreFocus} no-ops on it.
19253
+ * Fractional form for ratios where the decimal *is* the signal (avg depth 1.5,
19254
+ * sparsity 0.25) — distinct from {@link flyFullNumber}, which floors to integers.
19255
+ * Trailing zeros drop.
19077
19256
  */
19078
- function captureFocus(doc = document) {
19079
- const active = doc.activeElement;
19080
- const element = active instanceof HTMLElement && active !== doc.body ? active : null;
19081
- return { element };
19257
+ function flyDecimalNumber(value, locale = 'en', maxFractionDigits = 2) {
19258
+ if (value == null || !Number.isFinite(value))
19259
+ return FLY_EMPTY_VALUE;
19260
+ const key = `${locale}|${maxFractionDigits}`;
19261
+ let f = decimalCache.get(key);
19262
+ if (!f) {
19263
+ f = new Intl.NumberFormat(locale, { maximumFractionDigits: maxFractionDigits });
19264
+ decimalCache.set(key, f);
19265
+ }
19266
+ return f.format(value);
19082
19267
  }
19083
19268
  /**
19084
- * Returns focus to the captured element, if it is still focusable.
19085
- *
19086
- * Guards the three cases that make naive `restoreFocusTo.focus()` misbehave:
19087
- * - the element was removed from the DOM while the overlay was open (a row deleted by
19088
- * the very dialog that is closing) — `isConnected` is false, so we skip rather than
19089
- * throw focus to `<body>` via a detached node;
19090
- * - it became disabled or `inert` while the overlay was open;
19091
- * - nothing was focused when the overlay opened.
19092
- *
19093
- * `preventScroll` keeps the page from jumping when the trigger has scrolled out of
19094
- * view behind the overlay — the caller decides whether the trigger should be scrolled
19095
- * back into view, which is a product decision, not a focus one.
19269
+ * Signed compact delta for "change since last poll" chips (`"+12.4K"`, `"−340"`).
19270
+ * Returns an empty string for zero / non-finite so a no-change chip renders nothing
19271
+ * rather than a meaningless "0".
19096
19272
  *
19097
- * Returns `null` so callers can clear their field in one statement.
19273
+ * Uses U+2212 MINUS SIGN, not a hyphen it matches the plus glyph's width and weight,
19274
+ * so a column of deltas stays visually aligned.
19098
19275
  */
19099
- function restoreFocus(restore, options = {}) {
19100
- const element = restore?.element;
19101
- if (!element || !element.isConnected)
19102
- return null;
19103
- if (element.hasAttribute('disabled') || element.closest('[inert]'))
19104
- return null;
19105
- element.focus({ preventScroll: options.preventScroll ?? true });
19106
- return null;
19276
+ function flySignedCompact(delta, locale = 'en') {
19277
+ if (!Number.isFinite(delta) || delta === 0)
19278
+ return '';
19279
+ const sign = delta > 0 ? '+' : '−';
19280
+ return sign + flyCompactNumber(Math.abs(delta), locale);
19107
19281
  }
19108
-
19282
+ // ─── Byte sizes ──────────────────────────────────────────────────────────────
19283
+ /** Binary unit ladder. Byte sizes are conventionally base-1024 in file UIs. */
19284
+ const BYTE_UNITS = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte'];
19285
+ const byteCache = new Map();
19109
19286
  /**
19110
- * Debounce primitives the shared replacement for the `setTimeout` / `clearTimeout`
19111
- * pairs hand-rolled in every list screen and typeahead across the estate.
19112
- *
19113
- * Two entry points, for the two situations:
19114
- * - {@link FlyDebouncer} — an object you hold and call, with `cancel()` and `flush()`.
19115
- * Use it in a component that debounces on a field (search boxes, reload-on-filter).
19116
- * - {@link flyDebounced} — an injection-context factory that wires `cancel()` to the
19117
- * host's `DestroyRef` for you, so a pending call can never fire after teardown.
19287
+ * Human-readable byte size (`"482 bytes"`, `"1.4 MB"`, `"2.3 GB"`), localized.
19118
19288
  *
19119
- * ## Why not `debounceTime` from RxJS
19289
+ * Base-1024 with a log-derived unit pick, clamped at TB so a bogus huge value degrades
19290
+ * to a large TB figure instead of overflowing the ladder. Trailing zeros drop
19291
+ * (`1.0 MB` → `1 MB`).
19292
+ *
19293
+ * Localization matters here and is the reason this supersedes the eight hand-rolled
19294
+ * copies in the estate: those concatenate hardcoded English unit strings, so an Arabic
19295
+ * user saw Latin "MB" beside Arabic-Indic digits. `Intl` unit formatting renders both
19296
+ * the number and the unit in the active locale (French even gets "octets").
19297
+ *
19298
+ * Note the deliberate convention mismatch: the maths is base-1024 while the rendered
19299
+ * symbols are the SI ones ("kB", "MB"), so 1024 bytes shows as "1 kB" rather than the
19300
+ * pedantically-correct "1 KiB". Every mainstream file UI — Windows Explorer, Finder —
19301
+ * does exactly this, and `Intl` has no binary-prefix units, so matching user
19302
+ * expectation beats matching the standard here.
19303
+ */
19304
+ function flyFormatBytes(bytes, locale = 'en') {
19305
+ if (bytes == null || !Number.isFinite(bytes))
19306
+ return FLY_EMPTY_VALUE;
19307
+ if (bytes <= 0)
19308
+ return formatByteValue(0, 'byte', locale);
19309
+ const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), BYTE_UNITS.length - 1);
19310
+ const value = bytes / Math.pow(1024, index);
19311
+ // Bytes are whole things — never render "482.3 B".
19312
+ const rounded = index === 0 ? Math.round(value) : parseFloat(value.toFixed(1));
19313
+ return formatByteValue(rounded, BYTE_UNITS[index], locale);
19314
+ }
19315
+ function formatByteValue(value, unit, locale) {
19316
+ const key = `${locale}|${unit}`;
19317
+ let f = byteCache.get(key);
19318
+ if (!f) {
19319
+ f = new Intl.NumberFormat(locale, {
19320
+ style: 'unit',
19321
+ unit,
19322
+ // Raw bytes read best spelled out and pluralized ("482 bytes", "1 byte" —
19323
+ // and correctly "482 octets" in French). The larger units are universally
19324
+ // recognised as symbols, where the spelled-out form ("1.4 megabytes") would
19325
+ // be noise in a file list.
19326
+ unitDisplay: unit === 'byte' ? 'long' : 'short',
19327
+ maximumFractionDigits: 1,
19328
+ });
19329
+ byteCache.set(key, f);
19330
+ }
19331
+ return f.format(value);
19332
+ }
19333
+ // ─── Relative time ───────────────────────────────────────────────────────────
19334
+ /**
19335
+ * Formats an instant as a localized relative age — `"2 minutes ago"` /
19336
+ * `"منذ دقيقتين"` / `"il y a 2 minutes"`. Works for future instants too
19337
+ * (`"in 3 days"`), which is what makes it usable for due dates and SLA countdowns.
19338
+ *
19339
+ * `now` is injectable so tests can pin the clock instead of sleeping or accepting
19340
+ * flake around bucket boundaries.
19341
+ */
19342
+ function flyRelativeTime(value, locale = 'en', now = new Date()) {
19343
+ if (value == null || value === '')
19344
+ return FLY_EMPTY_VALUE;
19345
+ const then = value instanceof Date ? value : new Date(value);
19346
+ const ms = then.getTime();
19347
+ if (Number.isNaN(ms))
19348
+ return FLY_EMPTY_VALUE;
19349
+ const diffMs = ms - now.getTime();
19350
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
19351
+ const absSec = Math.abs(diffMs) / 1000;
19352
+ // `numeric: 'auto'` is what yields "yesterday" / "now" instead of a stiff
19353
+ // "1 day ago" / "0 seconds ago" at the bucket edges.
19354
+ if (absSec < 60)
19355
+ return rtf.format(Math.round(diffMs / 1000), 'second');
19356
+ if (absSec < 3600)
19357
+ return rtf.format(Math.round(diffMs / 60_000), 'minute');
19358
+ if (absSec < 86_400)
19359
+ return rtf.format(Math.round(diffMs / 3_600_000), 'hour');
19360
+ if (absSec < 2_592_000)
19361
+ return rtf.format(Math.round(diffMs / 86_400_000), 'day');
19362
+ if (absSec < 31_536_000)
19363
+ return rtf.format(Math.round(diffMs / 2_592_000_000), 'month');
19364
+ return rtf.format(Math.round(diffMs / 31_536_000_000), 'year');
19365
+ }
19366
+ /**
19367
+ * Formats a duration in seconds as a localized short unit string — `420` → `"7 min"`
19368
+ * (en) / `"7 د"` (ar). Picks seconds / minutes / hours by magnitude.
19369
+ */
19370
+ function flyDuration(seconds, locale = 'en') {
19371
+ if (seconds == null || !Number.isFinite(seconds) || seconds < 0) {
19372
+ return FLY_EMPTY_VALUE;
19373
+ }
19374
+ const fmt = (unit, v) => new Intl.NumberFormat(locale, {
19375
+ style: 'unit',
19376
+ unit,
19377
+ unitDisplay: 'narrow',
19378
+ maximumFractionDigits: 0,
19379
+ }).format(v);
19380
+ if (seconds < 60)
19381
+ return fmt('second', seconds);
19382
+ if (seconds < 3600)
19383
+ return fmt('minute', seconds / 60);
19384
+ return fmt('hour', seconds / 3600);
19385
+ }
19386
+ // ─── Dates ───────────────────────────────────────────────────────────────────
19387
+ /**
19388
+ * Localized calendar date (no time). Pass `options` to override the default
19389
+ * short-date presentation.
19390
+ *
19391
+ * Unlike the bare `toLocaleDateString()` calls it replaces, this takes an explicit
19392
+ * locale — those used the *browser's* locale and so ignored the app's language setting
19393
+ * entirely. Invalid input degrades to the em dash rather than `"Invalid Date"`.
19394
+ */
19395
+ function flyFormatDate(value, locale = 'en', options = { dateStyle: 'medium' }) {
19396
+ const date = toValidDate(value);
19397
+ if (!date)
19398
+ return FLY_EMPTY_VALUE;
19399
+ try {
19400
+ return new Intl.DateTimeFormat(locale, options).format(date);
19401
+ }
19402
+ catch {
19403
+ // A malformed `options` object (or an unsupported locale extension) throws;
19404
+ // degrade to the ISO date rather than taking the view down.
19405
+ return date.toISOString().slice(0, 10);
19406
+ }
19407
+ }
19408
+ /** Localized time of day (no date). */
19409
+ function flyFormatTime(value, locale = 'en', options = { timeStyle: 'short' }) {
19410
+ return flyFormatDate(value, locale, options);
19411
+ }
19412
+ /** Localized date + time — the tooltip companion to a relative or short-date cell. */
19413
+ function flyFormatDateTime(value, locale = 'en', options = { dateStyle: 'medium', timeStyle: 'short' }) {
19414
+ return flyFormatDate(value, locale, options);
19415
+ }
19416
+ /**
19417
+ * Calendar date as `yyyy-MM-dd` — the wire/sort form, deliberately NOT localized.
19418
+ *
19419
+ * Use for `<input type="date">` values, query params, and sort keys. For anything a
19420
+ * user reads, use {@link flyFormatDate}.
19421
+ *
19422
+ * Derived from the instant's **UTC** date so the value is stable across timezones —
19423
+ * a local-date derivation shifts the day for users east/west of the source data.
19424
+ */
19425
+ function flyToDateOnly(value) {
19426
+ const date = toValidDate(value);
19427
+ return date ? date.toISOString().slice(0, 10) : '';
19428
+ }
19429
+ function toValidDate(value) {
19430
+ if (value == null || value === '')
19431
+ return null;
19432
+ const date = value instanceof Date ? value : new Date(value);
19433
+ return Number.isNaN(date.getTime()) ? null : date;
19434
+ }
19435
+
19436
+ /**
19437
+ * ISO 4217 minor-unit EXCEPTIONS, used ONLY when `currency` is a bare code string with
19438
+ * no matching `FlyCurrency` row to read `decimalDigits` from. This is deliberately a
19439
+ * PARTIAL table — it mirrors exactly the two exception groups
19440
+ * {@link FlyCurrency.decimalDigits}'s own doc comment names (0-decimal: JPY, KRW, the
19441
+ * CFA francs; 3-decimal: the seven Gulf/MENA dinars/rials), not full ISO 4217 coverage.
19442
+ * Every code outside these two sets defaults to 2, which is correct for the ISO 4217
19443
+ * majority.
19444
+ *
19445
+ * This is the "explicit, documented fallback" D-B7-2 requires in place of a silent
19446
+ * "assume 2" — but it is still a fallback, not a substitute for the real data. A caller
19447
+ * that needs certainty for every currency (there are 0-decimal codes beyond the three
19448
+ * listed here) should pass the `FlyCurrency` row instead of a bare code:
19449
+ * `fly-currency-selector`'s `(selectionDetailChange)` emits the full row precisely so a
19450
+ * host never has to guess.
19451
+ */
19452
+ const ZERO_DECIMAL_FALLBACK_CODES = new Set(['JPY', 'KRW', 'XAF', 'XOF', 'XPF']);
19453
+ const THREE_DECIMAL_FALLBACK_CODES = new Set([
19454
+ 'BHD',
19455
+ 'IQD',
19456
+ 'JOD',
19457
+ 'KWD',
19458
+ 'LYD',
19459
+ 'OMR',
19460
+ 'TND',
19461
+ ]);
19462
+ function fallbackDecimalDigits(code) {
19463
+ if (ZERO_DECIMAL_FALLBACK_CODES.has(code))
19464
+ return 0;
19465
+ if (THREE_DECIMAL_FALLBACK_CODES.has(code))
19466
+ return 3;
19467
+ return 2;
19468
+ }
19469
+ /** Accepts a `FlyCurrency` row or a bare ISO 4217 code string; `null`/`undefined`/blank resolves to `null`. */
19470
+ function resolveCurrencyMeta(currency) {
19471
+ if (currency && typeof currency === 'object') {
19472
+ const digits = Number.isInteger(currency.decimalDigits) && currency.decimalDigits >= 0
19473
+ ? currency.decimalDigits
19474
+ : 2; // Defensive only — a malformed row should still render something rather than throw.
19475
+ return { code: currency.code.trim().toUpperCase(), symbol: currency.symbol ?? null, decimalDigits: digits };
19476
+ }
19477
+ if (typeof currency === 'string' && currency.trim()) {
19478
+ const code = currency.trim().toUpperCase();
19479
+ return { code, symbol: null, decimalDigits: fallbackDecimalDigits(code) };
19480
+ }
19481
+ return null;
19482
+ }
19483
+ /** The text substituted for Intl's `currency` part, per `display`. `null` removes it entirely. */
19484
+ function displayLabel(meta, display) {
19485
+ if (display === 'none')
19486
+ return null;
19487
+ if (display === 'code')
19488
+ return meta.code;
19489
+ // 'symbol' — degrade to the code when no symbol is known (the bare-code path), never
19490
+ // an Intl-guessed symbol that might not match the platform's own catalogue.
19491
+ return meta.symbol && meta.symbol.trim() ? meta.symbol : meta.code;
19492
+ }
19493
+ // Intl.NumberFormat construction is non-trivial; cache one per distinct (locale, code, digits).
19494
+ const moneyFormatterCache = new Map();
19495
+ /**
19496
+ * Returns `null` when `code` is not well-formed enough for `Intl` to accept as a
19497
+ * `currency` option (e.g. a malformed catalogue row) — the caller degrades to
19498
+ * {@link manualFormat} rather than letting a `RangeError` take the view down.
19499
+ */
19500
+ function currencyFormatter(locale, code, digits) {
19501
+ const key = `${locale}|${code}|${digits}`;
19502
+ const cached = moneyFormatterCache.get(key);
19503
+ if (cached)
19504
+ return cached;
19505
+ try {
19506
+ const f = new Intl.NumberFormat(locale, {
19507
+ style: 'currency',
19508
+ currency: code,
19509
+ // Always 'code', regardless of the caller's `display` option — see the module
19510
+ // doc comment on why this is the structural template rather than 'symbol'.
19511
+ currencyDisplay: 'code',
19512
+ minimumFractionDigits: digits,
19513
+ maximumFractionDigits: digits,
19514
+ });
19515
+ moneyFormatterCache.set(key, f);
19516
+ return f;
19517
+ }
19518
+ catch {
19519
+ return null;
19520
+ }
19521
+ }
19522
+ const plainDecimalCache = new Map();
19523
+ function plainDecimalFormatter(locale, digits) {
19524
+ const key = `${locale}|${digits}`;
19525
+ let f = plainDecimalCache.get(key);
19526
+ if (!f) {
19527
+ f = new Intl.NumberFormat(locale, { minimumFractionDigits: digits, maximumFractionDigits: digits });
19528
+ plainDecimalCache.set(key, f);
19529
+ }
19530
+ return f;
19531
+ }
19532
+ /** Manual `"<label> <number>"` construction for the rare case Intl rejects the code outright. */
19533
+ function manualFormat(amount, meta, display, locale) {
19534
+ const number = plainDecimalFormatter(locale, meta.decimalDigits).format(amount);
19535
+ const label = displayLabel(meta, display);
19536
+ return label ? `${label} ${number}` : number;
19537
+ }
19538
+ /**
19539
+ * Formats `amount` in `currency`, localized to `options.locale`.
19540
+ *
19541
+ * ```ts
19542
+ * flyFormatMoney(1234.5, kwdRow) // "د.ك 1,234.500" (KWD is 3-decimal)
19543
+ * flyFormatMoney(1234, 'JPY') // "JPY 1,234" (bare code, no symbol to read — degrades to the code)
19544
+ * flyFormatMoney(-42, usdRow, { display: 'code' }) // "-USD 42.00"
19545
+ * flyFormatMoney(99.9, eurRow, { display: 'none' }) // "99.90" (currency named elsewhere on screen)
19546
+ * flyFormatMoney(null, usdRow) // "—" (never "NaN")
19547
+ * ```
19548
+ *
19549
+ * `amount == null` or non-finite (`NaN`, `Infinity`), or an unresolvable `currency`,
19550
+ * renders {@link FLY_EMPTY_VALUE} — the same placeholder every other `format.ts`
19551
+ * primitive uses, so a money cell in a mixed table degrades exactly like its neighbours
19552
+ * instead of introducing a second "no value" convention.
19553
+ */
19554
+ function flyFormatMoney(amount, currency, options = {}) {
19555
+ if (amount == null || !Number.isFinite(amount))
19556
+ return FLY_EMPTY_VALUE;
19557
+ const meta = resolveCurrencyMeta(currency);
19558
+ if (!meta)
19559
+ return FLY_EMPTY_VALUE;
19560
+ const { display = 'symbol', locale = 'en' } = options;
19561
+ const formatter = currencyFormatter(locale, meta.code, meta.decimalDigits);
19562
+ if (!formatter)
19563
+ return manualFormat(amount, meta, display, locale);
19564
+ const label = displayLabel(meta, display);
19565
+ return formatter
19566
+ .formatToParts(amount)
19567
+ .map((part) => (part.type === 'currency' ? (label ?? '') : part.value))
19568
+ // Removing the currency part (display: 'none') leaves its adjacent literal
19569
+ // separator behind (Intl emits currency+space as two parts) — collapse and trim
19570
+ // rather than special-casing every locale's separator placement.
19571
+ .join('')
19572
+ .replace(/\s+/g, ' ')
19573
+ .trim();
19574
+ }
19575
+
19576
+ /**
19577
+ * `{{ amount | flyMoney: currencyRow }}` → `"$1,234.50"` / `{{ amount | flyMoney: 'KWD' }}`
19578
+ * → `"KD 1,234.500"`.
19579
+ *
19580
+ * Template wrapper over {@link flyFormatMoney}, defaulting the locale to the active
19581
+ * {@link I18nService} language the same way every other `Fly*Pipe` in `format.pipes.ts`
19582
+ * does — see that file's doc comment for the "pass the locale signal explicitly in a
19583
+ * view that must react live to the language switcher" caveat, which applies here too.
19584
+ *
19585
+ * A separate file from `format.pipes.ts` on purpose: this pipe's `currency` argument
19586
+ * depends on `FlyCurrency` (the currency-selector's data contract), a domain type the
19587
+ * pure number/byte/date formatters in `format.pipes.ts` have no reason to import.
19588
+ *
19589
+ * ```html
19590
+ * {{ invoice.total | flyMoney: invoice.currency }}
19591
+ * {{ invoice.total | flyMoney: invoice.currency : { display: 'code' } }}
19592
+ * ```
19593
+ */
19594
+ class FlyMoneyPipe {
19595
+ i18n = inject(I18nService);
19596
+ transform(amount, currency, options) {
19597
+ return flyFormatMoney(amount, currency, {
19598
+ display: options?.display,
19599
+ locale: options?.locale ?? this.i18n.locale(),
19600
+ });
19601
+ }
19602
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyMoneyPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
19603
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.19", ngImport: i0, type: FlyMoneyPipe, isStandalone: true, name: "flyMoney" });
19604
+ }
19605
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyMoneyPipe, decorators: [{
19606
+ type: Pipe,
19607
+ args: [{ name: 'flyMoney', standalone: true }]
19608
+ }] });
19609
+
19610
+ /**
19611
+ * Document-wide open-overlay stack — Escape arbitration for stacked overlays.
19612
+ *
19613
+ * Every layered overlay (drawer, modal, confirm dialog, menu, popover) listens for
19614
+ * Escape at the document level, so without arbitration one keypress closes the whole
19615
+ * pile — a confirm dialog opened over a drawer takes the drawer down with it on the
19616
+ * first Escape. Each overlay pushes a handle when it opens and removes it when it
19617
+ * closes or is destroyed; its Escape handler acts only while its handle is
19618
+ * top-of-stack. First Escape then closes the confirm, second the drawer.
19619
+ *
19620
+ * The stack is pure (no DOM, no DI) so the ordering rules are unit-testable, and the
19621
+ * shared {@link overlayStack} singleton spans the whole document — which is the point:
19622
+ * independently-owned overlays, including ones in different federated remotes, must
19623
+ * arbitrate globally or they cannot know about each other.
19624
+ *
19625
+ * ## Federation
19626
+ * Native Federation shares this package as a singleton, so every app in the shell
19627
+ * binds to one stack instance. A remote that forks its own DS copy would get its own
19628
+ * stack and lose arbitration against shell overlays — that is what the package's
19629
+ * federation singleton guard exists to catch.
19630
+ *
19631
+ * @example
19632
+ * ```ts
19633
+ * export class MyDrawer {
19634
+ * private handle: OverlayHandle | null = null;
19635
+ *
19636
+ * open() { this.handle = overlayStack.push(); }
19637
+ * close() { this.handle = overlayStack.remove(this.handle); }
19638
+ *
19639
+ * onEscape() { if (overlayStack.isTop(this.handle)) this.close(); }
19640
+ * }
19641
+ * ```
19642
+ */
19643
+ class OverlayStack {
19644
+ stack = [];
19645
+ /** Registers an opening overlay; the returned handle identifies it for later calls. */
19646
+ push() {
19647
+ const handle = { overlay: true };
19648
+ this.stack.push(handle);
19649
+ return handle;
19650
+ }
19651
+ /**
19652
+ * Unregisters a closing or destroyed overlay. Tolerates out-of-order removal (an
19653
+ * inner overlay torn down after its parent), unknown handles, and `null`.
19654
+ *
19655
+ * Returns `null` so callers can clear their field in one statement:
19656
+ * `this.handle = overlayStack.remove(this.handle)`.
19657
+ */
19658
+ remove(handle) {
19659
+ if (!handle)
19660
+ return null;
19661
+ const index = this.stack.indexOf(handle);
19662
+ if (index !== -1)
19663
+ this.stack.splice(index, 1);
19664
+ return null;
19665
+ }
19666
+ /** True when this handle is the topmost open overlay — its Escape handler may act. */
19667
+ isTop(handle) {
19668
+ return (handle !== null &&
19669
+ this.stack.length > 0 &&
19670
+ this.stack[this.stack.length - 1] === handle);
19671
+ }
19672
+ /** Number of currently-open overlays. Useful for scroll-lock refcounting. */
19673
+ get depth() {
19674
+ return this.stack.length;
19675
+ }
19676
+ }
19677
+ /**
19678
+ * The shared, document-wide stack. Import this rather than constructing an
19679
+ * `OverlayStack` — a private instance cannot arbitrate against anyone else's
19680
+ * overlays, which defeats the purpose.
19681
+ */
19682
+ const overlayStack = new OverlayStack();
19683
+
19684
+ /**
19685
+ * Focus capture/restore for overlays — the other half of a correct dismiss story.
19686
+ *
19687
+ * When an overlay opens it moves focus inside itself; when it closes, focus must go
19688
+ * back to whatever opened it, or the keyboard user is dumped at the top of the
19689
+ * document and has to re-traverse the page. Every hand-rolled modal/drawer in the
19690
+ * estate re-implements this with a `restoreFocusTo` field and a `.focus()` call, and
19691
+ * most of them miss at least one of the edge cases below.
19692
+ *
19693
+ * @example
19694
+ * ```ts
19695
+ * private restore: FocusRestore | null = null;
19696
+ *
19697
+ * open() { this.restore = captureFocus(); }
19698
+ * close() { this.restore = restoreFocus(this.restore); }
19699
+ * ```
19700
+ */
19701
+ /**
19702
+ * Snapshots the currently-focused element so it can be refocused later.
19703
+ *
19704
+ * Returns a token even when nothing is focused (`document.body` is treated as "no
19705
+ * meaningful focus"), so callers never branch — {@link restoreFocus} no-ops on it.
19706
+ */
19707
+ function captureFocus(doc = document) {
19708
+ const active = doc.activeElement;
19709
+ const element = active instanceof HTMLElement && active !== doc.body ? active : null;
19710
+ return { element };
19711
+ }
19712
+ /**
19713
+ * Returns focus to the captured element, if it is still focusable.
19714
+ *
19715
+ * Guards the three cases that make naive `restoreFocusTo.focus()` misbehave:
19716
+ * - the element was removed from the DOM while the overlay was open (a row deleted by
19717
+ * the very dialog that is closing) — `isConnected` is false, so we skip rather than
19718
+ * throw focus to `<body>` via a detached node;
19719
+ * - it became disabled or `inert` while the overlay was open;
19720
+ * - nothing was focused when the overlay opened.
19721
+ *
19722
+ * `preventScroll` keeps the page from jumping when the trigger has scrolled out of
19723
+ * view behind the overlay — the caller decides whether the trigger should be scrolled
19724
+ * back into view, which is a product decision, not a focus one.
19725
+ *
19726
+ * Returns `null` so callers can clear their field in one statement.
19727
+ */
19728
+ function restoreFocus(restore, options = {}) {
19729
+ const element = restore?.element;
19730
+ if (!element || !element.isConnected)
19731
+ return null;
19732
+ if (element.hasAttribute('disabled') || element.closest('[inert]'))
19733
+ return null;
19734
+ element.focus({ preventScroll: options.preventScroll ?? true });
19735
+ return null;
19736
+ }
19737
+
19738
+ /**
19739
+ * Debounce primitives — the shared replacement for the `setTimeout` / `clearTimeout`
19740
+ * pairs hand-rolled in every list screen and typeahead across the estate.
19741
+ *
19742
+ * Two entry points, for the two situations:
19743
+ * - {@link FlyDebouncer} — an object you hold and call, with `cancel()` and `flush()`.
19744
+ * Use it in a component that debounces on a field (search boxes, reload-on-filter).
19745
+ * - {@link flyDebounced} — an injection-context factory that wires `cancel()` to the
19746
+ * host's `DestroyRef` for you, so a pending call can never fire after teardown.
19747
+ *
19748
+ * ## Why not `debounceTime` from RxJS
19120
19749
  * Nothing wrong with it when the input is already a stream. But the common case here
19121
19750
  * is a signal-based component with an `(input)` handler and no Subject in sight, and
19122
19751
  * standing up a `Subject` + `takeUntilDestroyed` + `subscribe` to debounce one field is
@@ -19800,260 +20429,6 @@ function presenceColorFor(seed) {
19800
20429
  return PRESENCE_COLORS[Math.abs(hash) % PRESENCE_COLORS.length];
19801
20430
  }
19802
20431
 
19803
- /**
19804
- * Locale-aware formatting primitives — numbers, byte sizes, relative time, and dates.
19805
- *
19806
- * All of these are `Intl`-backed and take an explicit `locale`, which is the whole
19807
- * point: the estate is full of hand-rolled formatters that hardcode ASCII digits,
19808
- * English unit strings ("1.4 MB", "5m ago"), or the *browser's* default locale rather
19809
- * than the app's selected language. Under `ar` / `ur` those render wrong — and i18n is
19810
- * mandatory on this platform, so this is a correctness surface, not a convenience one.
19811
- *
19812
- * Pure functions here; the `| flyCompact`-style pipes in `format.pipes.ts` wrap them
19813
- * and default the locale to the active {@link I18nService} language.
19814
- *
19815
- * ## The em-dash convention
19816
- * Every formatter returns `'—'` (U+2014) for null / undefined / non-finite input rather
19817
- * than throwing, `'NaN'`, or an empty string. A visible placeholder keeps table columns
19818
- * aligned and makes "no value" legible; an empty string reads as a rendering bug.
19819
- */
19820
- /** Rendered for null / undefined / non-finite input across every formatter here. */
19821
- const FLY_EMPTY_VALUE = '—';
19822
- // ─── Numbers ─────────────────────────────────────────────────────────────────
19823
- /** At or above this magnitude, switch from grouped-exact to compact (K/M/B) notation. */
19824
- const FLY_COMPACT_THRESHOLD = 10_000;
19825
- // Intl formatters are expensive to construct relative to how often these are called in
19826
- // a table cell or chart label; cache one per distinct configuration.
19827
- const compactCache = new Map();
19828
- const integerCache = new Map();
19829
- const decimalCache = new Map();
19830
- function compactFormatter(locale) {
19831
- let f = compactCache.get(locale);
19832
- if (!f) {
19833
- f = new Intl.NumberFormat(locale, { notation: 'compact', maximumFractionDigits: 1 });
19834
- compactCache.set(locale, f);
19835
- }
19836
- return f;
19837
- }
19838
- function integerFormatter(locale) {
19839
- let f = integerCache.get(locale);
19840
- if (!f) {
19841
- f = new Intl.NumberFormat(locale, { maximumFractionDigits: 0 });
19842
- integerCache.set(locale, f);
19843
- }
19844
- return f;
19845
- }
19846
- /**
19847
- * Compact, scannable form — grouped-exact below {@link FLY_COMPACT_THRESHOLD},
19848
- * unit-compacted above it.
19849
- *
19850
- * ```
19851
- * 6 042 → "6,042" (exact; small buckets read best as real numbers)
19852
- * 12 400 → "12.4K"
19853
- * 1 900 787 → "1.9M"
19854
- * 2 300 000 000 → "2.3B"
19855
- * ```
19856
- *
19857
- * Pair with {@link flyFullNumber} in a tooltip or `aria-label` so the exact figure is
19858
- * always one hover away — compaction is a display affordance, not data loss.
19859
- */
19860
- function flyCompactNumber(value, locale = 'en') {
19861
- if (value == null || !Number.isFinite(value))
19862
- return FLY_EMPTY_VALUE;
19863
- return Math.abs(value) >= FLY_COMPACT_THRESHOLD
19864
- ? compactFormatter(locale).format(value)
19865
- : integerFormatter(locale).format(value);
19866
- }
19867
- /** Exact, fully-grouped integer form for tooltips / a11y (e.g. `"1,900,787"`). */
19868
- function flyFullNumber(value, locale = 'en') {
19869
- if (value == null || !Number.isFinite(value))
19870
- return FLY_EMPTY_VALUE;
19871
- return integerFormatter(locale).format(value);
19872
- }
19873
- /**
19874
- * Fractional form for ratios where the decimal *is* the signal (avg depth 1.5,
19875
- * sparsity 0.25) — distinct from {@link flyFullNumber}, which floors to integers.
19876
- * Trailing zeros drop.
19877
- */
19878
- function flyDecimalNumber(value, locale = 'en', maxFractionDigits = 2) {
19879
- if (value == null || !Number.isFinite(value))
19880
- return FLY_EMPTY_VALUE;
19881
- const key = `${locale}|${maxFractionDigits}`;
19882
- let f = decimalCache.get(key);
19883
- if (!f) {
19884
- f = new Intl.NumberFormat(locale, { maximumFractionDigits: maxFractionDigits });
19885
- decimalCache.set(key, f);
19886
- }
19887
- return f.format(value);
19888
- }
19889
- /**
19890
- * Signed compact delta for "change since last poll" chips (`"+12.4K"`, `"−340"`).
19891
- * Returns an empty string for zero / non-finite so a no-change chip renders nothing
19892
- * rather than a meaningless "0".
19893
- *
19894
- * Uses U+2212 MINUS SIGN, not a hyphen — it matches the plus glyph's width and weight,
19895
- * so a column of deltas stays visually aligned.
19896
- */
19897
- function flySignedCompact(delta, locale = 'en') {
19898
- if (!Number.isFinite(delta) || delta === 0)
19899
- return '';
19900
- const sign = delta > 0 ? '+' : '−';
19901
- return sign + flyCompactNumber(Math.abs(delta), locale);
19902
- }
19903
- // ─── Byte sizes ──────────────────────────────────────────────────────────────
19904
- /** Binary unit ladder. Byte sizes are conventionally base-1024 in file UIs. */
19905
- const BYTE_UNITS = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte'];
19906
- const byteCache = new Map();
19907
- /**
19908
- * Human-readable byte size (`"482 bytes"`, `"1.4 MB"`, `"2.3 GB"`), localized.
19909
- *
19910
- * Base-1024 with a log-derived unit pick, clamped at TB so a bogus huge value degrades
19911
- * to a large TB figure instead of overflowing the ladder. Trailing zeros drop
19912
- * (`1.0 MB` → `1 MB`).
19913
- *
19914
- * Localization matters here and is the reason this supersedes the eight hand-rolled
19915
- * copies in the estate: those concatenate hardcoded English unit strings, so an Arabic
19916
- * user saw Latin "MB" beside Arabic-Indic digits. `Intl` unit formatting renders both
19917
- * the number and the unit in the active locale (French even gets "octets").
19918
- *
19919
- * Note the deliberate convention mismatch: the maths is base-1024 while the rendered
19920
- * symbols are the SI ones ("kB", "MB"), so 1024 bytes shows as "1 kB" rather than the
19921
- * pedantically-correct "1 KiB". Every mainstream file UI — Windows Explorer, Finder —
19922
- * does exactly this, and `Intl` has no binary-prefix units, so matching user
19923
- * expectation beats matching the standard here.
19924
- */
19925
- function flyFormatBytes(bytes, locale = 'en') {
19926
- if (bytes == null || !Number.isFinite(bytes))
19927
- return FLY_EMPTY_VALUE;
19928
- if (bytes <= 0)
19929
- return formatByteValue(0, 'byte', locale);
19930
- const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), BYTE_UNITS.length - 1);
19931
- const value = bytes / Math.pow(1024, index);
19932
- // Bytes are whole things — never render "482.3 B".
19933
- const rounded = index === 0 ? Math.round(value) : parseFloat(value.toFixed(1));
19934
- return formatByteValue(rounded, BYTE_UNITS[index], locale);
19935
- }
19936
- function formatByteValue(value, unit, locale) {
19937
- const key = `${locale}|${unit}`;
19938
- let f = byteCache.get(key);
19939
- if (!f) {
19940
- f = new Intl.NumberFormat(locale, {
19941
- style: 'unit',
19942
- unit,
19943
- // Raw bytes read best spelled out and pluralized ("482 bytes", "1 byte" —
19944
- // and correctly "482 octets" in French). The larger units are universally
19945
- // recognised as symbols, where the spelled-out form ("1.4 megabytes") would
19946
- // be noise in a file list.
19947
- unitDisplay: unit === 'byte' ? 'long' : 'short',
19948
- maximumFractionDigits: 1,
19949
- });
19950
- byteCache.set(key, f);
19951
- }
19952
- return f.format(value);
19953
- }
19954
- // ─── Relative time ───────────────────────────────────────────────────────────
19955
- /**
19956
- * Formats an instant as a localized relative age — `"2 minutes ago"` /
19957
- * `"منذ دقيقتين"` / `"il y a 2 minutes"`. Works for future instants too
19958
- * (`"in 3 days"`), which is what makes it usable for due dates and SLA countdowns.
19959
- *
19960
- * `now` is injectable so tests can pin the clock instead of sleeping or accepting
19961
- * flake around bucket boundaries.
19962
- */
19963
- function flyRelativeTime(value, locale = 'en', now = new Date()) {
19964
- if (value == null || value === '')
19965
- return FLY_EMPTY_VALUE;
19966
- const then = value instanceof Date ? value : new Date(value);
19967
- const ms = then.getTime();
19968
- if (Number.isNaN(ms))
19969
- return FLY_EMPTY_VALUE;
19970
- const diffMs = ms - now.getTime();
19971
- const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
19972
- const absSec = Math.abs(diffMs) / 1000;
19973
- // `numeric: 'auto'` is what yields "yesterday" / "now" instead of a stiff
19974
- // "1 day ago" / "0 seconds ago" at the bucket edges.
19975
- if (absSec < 60)
19976
- return rtf.format(Math.round(diffMs / 1000), 'second');
19977
- if (absSec < 3600)
19978
- return rtf.format(Math.round(diffMs / 60_000), 'minute');
19979
- if (absSec < 86_400)
19980
- return rtf.format(Math.round(diffMs / 3_600_000), 'hour');
19981
- if (absSec < 2_592_000)
19982
- return rtf.format(Math.round(diffMs / 86_400_000), 'day');
19983
- if (absSec < 31_536_000)
19984
- return rtf.format(Math.round(diffMs / 2_592_000_000), 'month');
19985
- return rtf.format(Math.round(diffMs / 31_536_000_000), 'year');
19986
- }
19987
- /**
19988
- * Formats a duration in seconds as a localized short unit string — `420` → `"7 min"`
19989
- * (en) / `"7 د"` (ar). Picks seconds / minutes / hours by magnitude.
19990
- */
19991
- function flyDuration(seconds, locale = 'en') {
19992
- if (seconds == null || !Number.isFinite(seconds) || seconds < 0) {
19993
- return FLY_EMPTY_VALUE;
19994
- }
19995
- const fmt = (unit, v) => new Intl.NumberFormat(locale, {
19996
- style: 'unit',
19997
- unit,
19998
- unitDisplay: 'narrow',
19999
- maximumFractionDigits: 0,
20000
- }).format(v);
20001
- if (seconds < 60)
20002
- return fmt('second', seconds);
20003
- if (seconds < 3600)
20004
- return fmt('minute', seconds / 60);
20005
- return fmt('hour', seconds / 3600);
20006
- }
20007
- // ─── Dates ───────────────────────────────────────────────────────────────────
20008
- /**
20009
- * Localized calendar date (no time). Pass `options` to override the default
20010
- * short-date presentation.
20011
- *
20012
- * Unlike the bare `toLocaleDateString()` calls it replaces, this takes an explicit
20013
- * locale — those used the *browser's* locale and so ignored the app's language setting
20014
- * entirely. Invalid input degrades to the em dash rather than `"Invalid Date"`.
20015
- */
20016
- function flyFormatDate(value, locale = 'en', options = { dateStyle: 'medium' }) {
20017
- const date = toValidDate(value);
20018
- if (!date)
20019
- return FLY_EMPTY_VALUE;
20020
- try {
20021
- return new Intl.DateTimeFormat(locale, options).format(date);
20022
- }
20023
- catch {
20024
- // A malformed `options` object (or an unsupported locale extension) throws;
20025
- // degrade to the ISO date rather than taking the view down.
20026
- return date.toISOString().slice(0, 10);
20027
- }
20028
- }
20029
- /** Localized time of day (no date). */
20030
- function flyFormatTime(value, locale = 'en', options = { timeStyle: 'short' }) {
20031
- return flyFormatDate(value, locale, options);
20032
- }
20033
- /** Localized date + time — the tooltip companion to a relative or short-date cell. */
20034
- function flyFormatDateTime(value, locale = 'en', options = { dateStyle: 'medium', timeStyle: 'short' }) {
20035
- return flyFormatDate(value, locale, options);
20036
- }
20037
- /**
20038
- * Calendar date as `yyyy-MM-dd` — the wire/sort form, deliberately NOT localized.
20039
- *
20040
- * Use for `<input type="date">` values, query params, and sort keys. For anything a
20041
- * user reads, use {@link flyFormatDate}.
20042
- *
20043
- * Derived from the instant's **UTC** date so the value is stable across timezones —
20044
- * a local-date derivation shifts the day for users east/west of the source data.
20045
- */
20046
- function flyToDateOnly(value) {
20047
- const date = toValidDate(value);
20048
- return date ? date.toISOString().slice(0, 10) : '';
20049
- }
20050
- function toValidDate(value) {
20051
- if (value == null || value === '')
20052
- return null;
20053
- const date = value instanceof Date ? value : new Date(value);
20054
- return Number.isNaN(date.getTime()) ? null : date;
20055
- }
20056
-
20057
20432
  /**
20058
20433
  * Template wrappers over the `format.ts` primitives. Each defaults its locale to the
20059
20434
  * active {@link I18nService} language, so the common case is `{{ value | flyBytes }}`
@@ -21858,8 +22233,22 @@ class FlyAppTopbarComponent {
21858
22233
  pendingFocus = signal(false, ...(ngDevMode ? [{ debugName: "pendingFocus" }] : /* istanbul ignore next */ []));
21859
22234
  flat = computed(() => flattenModules(this.sections()), ...(ngDevMode ? [{ debugName: "flat" }] : /* istanbul ignore next */ []));
21860
22235
  activeModule = computed(() => resolveActiveModule(this.sections(), this.activeKey()), ...(ngDevMode ? [{ debugName: "activeModule" }] : /* istanbul ignore next */ []));
21861
- brandAriaLabel = computed(() => this.i18n.t('ui.nav.home'), ...(ngDevMode ? [{ debugName: "brandAriaLabel" }] : /* istanbul ignore next */ []));
22236
+ // Both trigger names must START with the button's visible text ("Circles",
22237
+ // "Signals") — voice-control users activate controls by saying what they see,
22238
+ // so a name that omits it fails WCAG 2.5.3 Label in Name. The action context
22239
+ // is appended after the visible text; the popover menu keeps the plain
22240
+ // "Switch module" name since it renders no visible text of its own.
22241
+ brandAriaLabel = computed(() => {
22242
+ const home = this.i18n.t('ui.nav.home');
22243
+ const key = this.brandLabelKey();
22244
+ return key ? `${this.i18n.t(key)}, ${home}` : home;
22245
+ }, ...(ngDevMode ? [{ debugName: "brandAriaLabel" }] : /* istanbul ignore next */ []));
21862
22246
  switchAriaLabel = computed(() => this.i18n.t('ui.nav.switchModule'), ...(ngDevMode ? [{ debugName: "switchAriaLabel" }] : /* istanbul ignore next */ []));
22247
+ triggerAriaLabel = computed(() => {
22248
+ const active = this.activeModule();
22249
+ const visible = active ? this.i18n.t(active.labelKey) : this.i18n.t('ui.nav.selectModule');
22250
+ return `${visible}, ${this.i18n.t('ui.nav.switchModule')}`;
22251
+ }, ...(ngDevMode ? [{ debugName: "triggerAriaLabel" }] : /* istanbul ignore next */ []));
21863
22252
  constructor() {
21864
22253
  inject(DestroyRef).onDestroy(() => overlayStack.remove(this.stackHandle));
21865
22254
  effect(() => {
@@ -21965,11 +22354,11 @@ class FlyAppTopbarComponent {
21965
22354
  this.rows()[index]?.nativeElement.focus();
21966
22355
  }
21967
22356
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyAppTopbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
21968
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyAppTopbarComponent, isStandalone: true, selector: "fly-app-topbar", inputs: { brandLabelKey: { classPropertyName: "brandLabelKey", publicName: "brandLabelKey", isSignal: true, isRequired: false, transformFunction: null }, sections: { classPropertyName: "sections", publicName: "sections", isSignal: true, isRequired: false, transformFunction: null }, activeKey: { classPropertyName: "activeKey", publicName: "activeKey", isSignal: true, isRequired: false, transformFunction: null }, footerLabel: { classPropertyName: "footerLabel", publicName: "footerLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { moduleSelected: "moduleSelected", brandSelected: "brandSelected" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, queries: [{ propertyName: "icons", predicate: FlyModuleIconDirective, isSignal: true }], viewQueries: [{ propertyName: "rows", predicate: ["row"], descendants: true, isSignal: true }, { propertyName: "trigger", predicate: ["trigger"], descendants: true, isSignal: true }], ngImport: i0, template: "<header class=\"fly-topbar\">\r\n <div class=\"fly-topbar__lead\">\r\n <button\r\n type=\"button\"\r\n class=\"fly-topbar__brand\"\r\n (click)=\"brandSelected.emit()\"\r\n [attr.aria-label]=\"brandAriaLabel()\"\r\n >\r\n <span class=\"fly-topbar__brand-mark\"><ng-content select=\"[app-topbar-brand]\" /></span>\r\n @if (brandLabelKey(); as key) {\r\n <span class=\"fly-topbar__brand-name\">{{ key | translate }}</span>\r\n }\r\n </button>\r\n\r\n @if (activeModule()) {\r\n <span class=\"fly-topbar__sep\" aria-hidden=\"true\">/</span>\r\n }\r\n\r\n <div class=\"fly-topbar__anchor\" (flyClickOutside)=\"close()\" [flyClickOutsideEnabled]=\"open()\">\r\n <button\r\n #trigger\r\n type=\"button\"\r\n class=\"fly-topbar__module\"\r\n [class.fly-topbar__module--on]=\"open()\"\r\n (click)=\"toggle()\"\r\n (keydown)=\"onTriggerKeydown($event)\"\r\n aria-haspopup=\"menu\"\r\n [attr.aria-expanded]=\"open()\"\r\n [attr.aria-label]=\"switchAriaLabel()\"\r\n >\r\n @if (activeModule(); as active) {\r\n <span class=\"fly-topbar__module-icon\">\r\n @if (iconFor(active.key); as tpl) {\r\n <ng-container *ngTemplateOutlet=\"tpl\" />\r\n } @else if (active.iconClass) {\r\n <i [class]=\"active.iconClass\" aria-hidden=\"true\"></i>\r\n }\r\n </span>\r\n <span class=\"fly-topbar__module-label\">{{ active.labelKey | translate }}</span>\r\n } @else {\r\n <span class=\"fly-topbar__module-label fly-topbar__module-label--empty\">\r\n {{ 'ui.nav.selectModule' | translate }}\r\n </span>\r\n }\r\n <svg class=\"fly-topbar__chevron\" width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\"\r\n stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\r\n aria-hidden=\"true\">\r\n <polyline points=\"6 9 12 15 18 9\" />\r\n </svg>\r\n </button>\r\n\r\n @if (open()) {\r\n <div class=\"fly-topbar__pop\" role=\"menu\" [attr.aria-label]=\"switchAriaLabel()\">\r\n <div class=\"fly-topbar__cols\">\r\n @for (section of sections(); track $index) {\r\n <div class=\"fly-topbar__col\">\r\n @if (section.titleKey; as titleKey) {\r\n <div class=\"fly-topbar__col-title\">{{ titleKey | translate }}</div>\r\n }\r\n @for (mod of section.modules; track mod.key) {\r\n <button\r\n #row\r\n type=\"button\"\r\n class=\"fly-topbar__item\"\r\n [class.fly-topbar__item--on]=\"mod.key === activeKey()\"\r\n role=\"menuitem\"\r\n [disabled]=\"mod.disabled ?? false\"\r\n [attr.tabindex]=\"indexOf(mod) === focusIndex() ? 0 : -1\"\r\n [attr.aria-current]=\"mod.key === activeKey() ? 'page' : null\"\r\n (click)=\"select(mod)\"\r\n (keydown)=\"onRowKeydown($event)\"\r\n >\r\n <span class=\"fly-topbar__item-icon\">\r\n @if (iconFor(mod.key); as tpl) {\r\n <ng-container *ngTemplateOutlet=\"tpl\" />\r\n } @else if (mod.iconClass) {\r\n <i [class]=\"mod.iconClass\" aria-hidden=\"true\"></i>\r\n }\r\n </span>\r\n <span class=\"fly-topbar__item-main\">\r\n <span class=\"fly-topbar__item-label\">{{ mod.labelKey | translate }}</span>\r\n @if (mod.descKey; as descKey) {\r\n <span class=\"fly-topbar__item-desc\">{{ descKey | translate }}</span>\r\n }\r\n </span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n @if (footerLabel()) {\r\n <div class=\"fly-topbar__foot\">{{ footerLabel() }}</div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <div class=\"fly-topbar__actions\"><ng-content select=\"[app-topbar-actions]\" /></div>\r\n</header>\r\n", styles: [":host{display:block;position:relative;z-index:var(--z-sticky)}.fly-topbar{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);padding:var(--sp-2) var(--sp-3);border-block-end:1px solid var(--line);background:var(--bg-3)}.fly-topbar__lead{display:flex;align-items:center;gap:var(--sp-2);min-inline-size:0}.fly-topbar__brand,.fly-topbar__module{display:inline-flex;align-items:center;gap:var(--sp-2);appearance:none;border:0;background:transparent;cursor:pointer;font:inherit;color:var(--ink);padding:var(--sp-1) var(--sp-2);border-radius:var(--r-md);transition:background var(--t-state)}.fly-topbar__brand:hover,.fly-topbar__module:hover{background:var(--bg-hover)}.fly-topbar__brand:focus-visible,.fly-topbar__module:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}.fly-topbar__brand-name{font-weight:var(--fw-semibold);white-space:nowrap}.fly-topbar__brand-mark{display:inline-flex;align-items:center;max-block-size:var(--icon-lg)}.fly-topbar__sep{color:var(--ink-4);-webkit-user-select:none;user-select:none}.fly-topbar__module{min-inline-size:0}.fly-topbar__module--on{background:var(--bg-hover)}.fly-topbar__module-label{font-weight:var(--fw-medium);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-topbar__module-label--empty{color:var(--ink-3)}.fly-topbar__module-icon,.fly-topbar__item-icon{display:inline-flex;align-items:center;justify-content:center;inline-size:var(--icon-md);block-size:var(--icon-md);flex:0 0 auto;color:var(--accent)}.fly-topbar__chevron{flex:0 0 auto;color:var(--ink-3);transition:transform var(--t-state)}.fly-topbar__module--on .fly-topbar__chevron{transform:rotate(180deg)}.fly-topbar__anchor{position:relative}.fly-topbar__pop{position:absolute;inset-block-start:calc(100% + var(--sp-1));inset-inline-start:0;z-index:var(--z-overlay);min-inline-size:260px;max-inline-size:min(680px,92vw);padding:var(--sp-3);border:1px solid var(--line);border-radius:var(--r-lg);background:var(--bg-2);box-shadow:var(--shadow-panel)}.fly-topbar__cols{display:flex;gap:var(--sp-4);align-items:flex-start}@media(width<=720px){.fly-topbar__cols{flex-direction:column;gap:var(--sp-3)}}.fly-topbar__col{display:flex;flex-direction:column;gap:2px;min-inline-size:220px;flex:1 1 0}.fly-topbar__col-title{font-size:var(--text-2xs);font-weight:var(--fw-semibold);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--ink-3);padding:var(--sp-2) var(--sp-2) var(--sp-1)}.fly-topbar__item{display:flex;align-items:flex-start;gap:var(--sp-2);inline-size:100%;text-align:start;appearance:none;border:0;background:transparent;cursor:pointer;font:inherit;color:var(--ink);padding:var(--sp-2);border-radius:var(--r-md);transition:background var(--t-state)}.fly-topbar__item:hover:not(:disabled){background:var(--bg-hover)}.fly-topbar__item:focus-visible{outline:2px solid var(--focus-ring);outline-offset:-2px}.fly-topbar__item:disabled{opacity:.5;cursor:default}.fly-topbar__item--on{background:var(--accent-soft);color:var(--on-accent-soft);box-shadow:inset 2px 0 0 0 var(--accent)}.fly-topbar__item-main{display:flex;flex-direction:column;gap:1px;min-inline-size:0}.fly-topbar__item-label{font-weight:var(--fw-medium);line-height:1.3}.fly-topbar__item-desc{font-size:var(--text-xs);color:var(--ink-3);line-height:1.35}.fly-topbar__foot{margin-block-start:var(--sp-3);padding-block-start:var(--sp-2);border-block-start:1px solid var(--line-3);font-size:var(--text-2xs);color:var(--ink-4)}.fly-topbar__actions{display:flex;align-items:center;gap:var(--sp-2);flex:0 0 auto}@media(forced-colors:active){.fly-topbar__item--on{border-inline-start:2px solid CanvasText}}@media(prefers-reduced-motion:reduce){.fly-topbar__brand,.fly-topbar__module,.fly-topbar__item,.fly-topbar__chevron{transition:none}}\n"], dependencies: [{ kind: "directive", type: FlyClickOutsideDirective, selector: "[flyClickOutside]", inputs: ["flyClickOutsideEnabled"], outputs: ["flyClickOutside"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
22357
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyAppTopbarComponent, isStandalone: true, selector: "fly-app-topbar", inputs: { brandLabelKey: { classPropertyName: "brandLabelKey", publicName: "brandLabelKey", isSignal: true, isRequired: false, transformFunction: null }, sections: { classPropertyName: "sections", publicName: "sections", isSignal: true, isRequired: false, transformFunction: null }, activeKey: { classPropertyName: "activeKey", publicName: "activeKey", isSignal: true, isRequired: false, transformFunction: null }, footerLabel: { classPropertyName: "footerLabel", publicName: "footerLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { moduleSelected: "moduleSelected", brandSelected: "brandSelected" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, queries: [{ propertyName: "icons", predicate: FlyModuleIconDirective, isSignal: true }], viewQueries: [{ propertyName: "rows", predicate: ["row"], descendants: true, isSignal: true }, { propertyName: "trigger", predicate: ["trigger"], descendants: true, isSignal: true }], ngImport: i0, template: "<header class=\"fly-topbar\">\r\n <div class=\"fly-topbar__lead\">\r\n <button\r\n type=\"button\"\r\n class=\"fly-topbar__brand\"\r\n (click)=\"brandSelected.emit()\"\r\n [attr.aria-label]=\"brandAriaLabel()\"\r\n >\r\n <span class=\"fly-topbar__brand-mark\"><ng-content select=\"[app-topbar-brand]\" /></span>\r\n @if (brandLabelKey(); as key) {\r\n <span class=\"fly-topbar__brand-name\">{{ key | translate }}</span>\r\n }\r\n </button>\r\n\r\n @if (activeModule()) {\r\n <span class=\"fly-topbar__sep\" aria-hidden=\"true\">/</span>\r\n }\r\n\r\n <div class=\"fly-topbar__anchor\" (flyClickOutside)=\"close()\" [flyClickOutsideEnabled]=\"open()\">\r\n <button\r\n #trigger\r\n type=\"button\"\r\n class=\"fly-topbar__module\"\r\n [class.fly-topbar__module--on]=\"open()\"\r\n (click)=\"toggle()\"\r\n (keydown)=\"onTriggerKeydown($event)\"\r\n aria-haspopup=\"menu\"\r\n [attr.aria-expanded]=\"open()\"\r\n [attr.aria-label]=\"triggerAriaLabel()\"\r\n >\r\n @if (activeModule(); as active) {\r\n <span class=\"fly-topbar__module-icon\">\r\n @if (iconFor(active.key); as tpl) {\r\n <ng-container *ngTemplateOutlet=\"tpl\" />\r\n } @else if (active.iconClass) {\r\n <i [class]=\"active.iconClass\" aria-hidden=\"true\"></i>\r\n }\r\n </span>\r\n <span class=\"fly-topbar__module-label\">{{ active.labelKey | translate }}</span>\r\n } @else {\r\n <span class=\"fly-topbar__module-label fly-topbar__module-label--empty\">\r\n {{ 'ui.nav.selectModule' | translate }}\r\n </span>\r\n }\r\n <svg class=\"fly-topbar__chevron\" width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\"\r\n stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\r\n aria-hidden=\"true\">\r\n <polyline points=\"6 9 12 15 18 9\" />\r\n </svg>\r\n </button>\r\n\r\n @if (open()) {\r\n <div class=\"fly-topbar__pop\" role=\"menu\" [attr.aria-label]=\"switchAriaLabel()\">\r\n <div class=\"fly-topbar__cols\">\r\n @for (section of sections(); track $index) {\r\n <div class=\"fly-topbar__col\">\r\n @if (section.titleKey; as titleKey) {\r\n <div class=\"fly-topbar__col-title\">{{ titleKey | translate }}</div>\r\n }\r\n @for (mod of section.modules; track mod.key) {\r\n <button\r\n #row\r\n type=\"button\"\r\n class=\"fly-topbar__item\"\r\n [class.fly-topbar__item--on]=\"mod.key === activeKey()\"\r\n role=\"menuitem\"\r\n [disabled]=\"mod.disabled ?? false\"\r\n [attr.tabindex]=\"indexOf(mod) === focusIndex() ? 0 : -1\"\r\n [attr.aria-current]=\"mod.key === activeKey() ? 'page' : null\"\r\n (click)=\"select(mod)\"\r\n (keydown)=\"onRowKeydown($event)\"\r\n >\r\n <span class=\"fly-topbar__item-icon\">\r\n @if (iconFor(mod.key); as tpl) {\r\n <ng-container *ngTemplateOutlet=\"tpl\" />\r\n } @else if (mod.iconClass) {\r\n <i [class]=\"mod.iconClass\" aria-hidden=\"true\"></i>\r\n }\r\n </span>\r\n <span class=\"fly-topbar__item-main\">\r\n <span class=\"fly-topbar__item-label\">{{ mod.labelKey | translate }}</span>\r\n @if (mod.descKey; as descKey) {\r\n <span class=\"fly-topbar__item-desc\">{{ descKey | translate }}</span>\r\n }\r\n </span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n @if (footerLabel()) {\r\n <div class=\"fly-topbar__foot\">{{ footerLabel() }}</div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <div class=\"fly-topbar__actions\"><ng-content select=\"[app-topbar-actions]\" /></div>\r\n</header>\r\n", styles: [":host{display:block;position:relative;z-index:var(--z-sticky)}.fly-topbar{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);padding:var(--sp-2) var(--sp-3);border-block-end:1px solid var(--line);background:var(--bg-3)}.fly-topbar__lead{display:flex;align-items:center;gap:var(--sp-2);min-inline-size:0}.fly-topbar__brand,.fly-topbar__module{display:inline-flex;align-items:center;gap:var(--sp-2);appearance:none;border:0;background:transparent;cursor:pointer;font:inherit;color:var(--ink);padding:var(--sp-1) var(--sp-2);border-radius:var(--r-md);transition:background var(--t-state)}.fly-topbar__brand:hover,.fly-topbar__module:hover{background:var(--bg-hover)}.fly-topbar__brand:focus-visible,.fly-topbar__module:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}.fly-topbar__brand-name{font-weight:var(--fw-semibold);white-space:nowrap}.fly-topbar__brand-mark{display:inline-flex;align-items:center;max-block-size:var(--icon-lg)}.fly-topbar__sep{color:var(--ink-4);-webkit-user-select:none;user-select:none}.fly-topbar__module{min-inline-size:0}.fly-topbar__module--on{background:var(--bg-hover)}.fly-topbar__module-label{font-weight:var(--fw-medium);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-topbar__module-label--empty{color:var(--ink-3)}.fly-topbar__module-icon,.fly-topbar__item-icon{display:inline-flex;align-items:center;justify-content:center;inline-size:var(--icon-md);block-size:var(--icon-md);flex:0 0 auto;color:var(--accent)}.fly-topbar__chevron{flex:0 0 auto;color:var(--ink-3);transition:transform var(--t-state)}.fly-topbar__module--on .fly-topbar__chevron{transform:rotate(180deg)}.fly-topbar__anchor{position:relative}.fly-topbar__pop{position:absolute;inset-block-start:calc(100% + var(--sp-1));inset-inline-start:0;z-index:var(--z-overlay);min-inline-size:260px;max-inline-size:min(680px,92vw);padding:var(--sp-3);border:1px solid var(--line);border-radius:var(--r-lg);background:var(--bg-2);box-shadow:var(--shadow-panel)}.fly-topbar__cols{display:flex;gap:var(--sp-4);align-items:flex-start}@media(width<=720px){.fly-topbar__cols{flex-direction:column;gap:var(--sp-3)}}.fly-topbar__col{display:flex;flex-direction:column;gap:2px;min-inline-size:220px;flex:1 1 0}.fly-topbar__col-title{font-size:var(--text-2xs);font-weight:var(--fw-semibold);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--ink-3);padding:var(--sp-2) var(--sp-2) var(--sp-1)}.fly-topbar__item{display:flex;align-items:flex-start;gap:var(--sp-2);inline-size:100%;text-align:start;appearance:none;border:0;background:transparent;cursor:pointer;font:inherit;color:var(--ink);padding:var(--sp-2);border-radius:var(--r-md);transition:background var(--t-state)}.fly-topbar__item:hover:not(:disabled){background:var(--bg-hover)}.fly-topbar__item:focus-visible{outline:2px solid var(--focus-ring);outline-offset:-2px}.fly-topbar__item:disabled{opacity:.5;cursor:default}.fly-topbar__item--on{background:var(--accent-soft);color:var(--on-accent-soft);box-shadow:inset 2px 0 0 0 var(--accent)}.fly-topbar__item-main{display:flex;flex-direction:column;gap:1px;min-inline-size:0}.fly-topbar__item-label{font-weight:var(--fw-medium);line-height:1.3}.fly-topbar__item-desc{font-size:var(--text-xs);color:var(--ink-3);line-height:1.35}.fly-topbar__foot{margin-block-start:var(--sp-3);padding-block-start:var(--sp-2);border-block-start:1px solid var(--line-3);font-size:var(--text-2xs);color:var(--ink-4)}.fly-topbar__actions{display:flex;align-items:center;gap:var(--sp-2);flex:0 0 auto}@media(forced-colors:active){.fly-topbar__item--on{border-inline-start:2px solid CanvasText}}@media(prefers-reduced-motion:reduce){.fly-topbar__brand,.fly-topbar__module,.fly-topbar__item,.fly-topbar__chevron{transition:none}}\n"], dependencies: [{ kind: "directive", type: FlyClickOutsideDirective, selector: "[flyClickOutside]", inputs: ["flyClickOutsideEnabled"], outputs: ["flyClickOutside"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
21969
22358
  }
21970
22359
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyAppTopbarComponent, decorators: [{
21971
22360
  type: Component,
21972
- args: [{ selector: 'fly-app-topbar', standalone: true, imports: [TranslatePipe, FlyClickOutsideDirective, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, host: { '(document:keydown.escape)': 'onEscape()' }, template: "<header class=\"fly-topbar\">\r\n <div class=\"fly-topbar__lead\">\r\n <button\r\n type=\"button\"\r\n class=\"fly-topbar__brand\"\r\n (click)=\"brandSelected.emit()\"\r\n [attr.aria-label]=\"brandAriaLabel()\"\r\n >\r\n <span class=\"fly-topbar__brand-mark\"><ng-content select=\"[app-topbar-brand]\" /></span>\r\n @if (brandLabelKey(); as key) {\r\n <span class=\"fly-topbar__brand-name\">{{ key | translate }}</span>\r\n }\r\n </button>\r\n\r\n @if (activeModule()) {\r\n <span class=\"fly-topbar__sep\" aria-hidden=\"true\">/</span>\r\n }\r\n\r\n <div class=\"fly-topbar__anchor\" (flyClickOutside)=\"close()\" [flyClickOutsideEnabled]=\"open()\">\r\n <button\r\n #trigger\r\n type=\"button\"\r\n class=\"fly-topbar__module\"\r\n [class.fly-topbar__module--on]=\"open()\"\r\n (click)=\"toggle()\"\r\n (keydown)=\"onTriggerKeydown($event)\"\r\n aria-haspopup=\"menu\"\r\n [attr.aria-expanded]=\"open()\"\r\n [attr.aria-label]=\"switchAriaLabel()\"\r\n >\r\n @if (activeModule(); as active) {\r\n <span class=\"fly-topbar__module-icon\">\r\n @if (iconFor(active.key); as tpl) {\r\n <ng-container *ngTemplateOutlet=\"tpl\" />\r\n } @else if (active.iconClass) {\r\n <i [class]=\"active.iconClass\" aria-hidden=\"true\"></i>\r\n }\r\n </span>\r\n <span class=\"fly-topbar__module-label\">{{ active.labelKey | translate }}</span>\r\n } @else {\r\n <span class=\"fly-topbar__module-label fly-topbar__module-label--empty\">\r\n {{ 'ui.nav.selectModule' | translate }}\r\n </span>\r\n }\r\n <svg class=\"fly-topbar__chevron\" width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\"\r\n stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\r\n aria-hidden=\"true\">\r\n <polyline points=\"6 9 12 15 18 9\" />\r\n </svg>\r\n </button>\r\n\r\n @if (open()) {\r\n <div class=\"fly-topbar__pop\" role=\"menu\" [attr.aria-label]=\"switchAriaLabel()\">\r\n <div class=\"fly-topbar__cols\">\r\n @for (section of sections(); track $index) {\r\n <div class=\"fly-topbar__col\">\r\n @if (section.titleKey; as titleKey) {\r\n <div class=\"fly-topbar__col-title\">{{ titleKey | translate }}</div>\r\n }\r\n @for (mod of section.modules; track mod.key) {\r\n <button\r\n #row\r\n type=\"button\"\r\n class=\"fly-topbar__item\"\r\n [class.fly-topbar__item--on]=\"mod.key === activeKey()\"\r\n role=\"menuitem\"\r\n [disabled]=\"mod.disabled ?? false\"\r\n [attr.tabindex]=\"indexOf(mod) === focusIndex() ? 0 : -1\"\r\n [attr.aria-current]=\"mod.key === activeKey() ? 'page' : null\"\r\n (click)=\"select(mod)\"\r\n (keydown)=\"onRowKeydown($event)\"\r\n >\r\n <span class=\"fly-topbar__item-icon\">\r\n @if (iconFor(mod.key); as tpl) {\r\n <ng-container *ngTemplateOutlet=\"tpl\" />\r\n } @else if (mod.iconClass) {\r\n <i [class]=\"mod.iconClass\" aria-hidden=\"true\"></i>\r\n }\r\n </span>\r\n <span class=\"fly-topbar__item-main\">\r\n <span class=\"fly-topbar__item-label\">{{ mod.labelKey | translate }}</span>\r\n @if (mod.descKey; as descKey) {\r\n <span class=\"fly-topbar__item-desc\">{{ descKey | translate }}</span>\r\n }\r\n </span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n @if (footerLabel()) {\r\n <div class=\"fly-topbar__foot\">{{ footerLabel() }}</div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <div class=\"fly-topbar__actions\"><ng-content select=\"[app-topbar-actions]\" /></div>\r\n</header>\r\n", styles: [":host{display:block;position:relative;z-index:var(--z-sticky)}.fly-topbar{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);padding:var(--sp-2) var(--sp-3);border-block-end:1px solid var(--line);background:var(--bg-3)}.fly-topbar__lead{display:flex;align-items:center;gap:var(--sp-2);min-inline-size:0}.fly-topbar__brand,.fly-topbar__module{display:inline-flex;align-items:center;gap:var(--sp-2);appearance:none;border:0;background:transparent;cursor:pointer;font:inherit;color:var(--ink);padding:var(--sp-1) var(--sp-2);border-radius:var(--r-md);transition:background var(--t-state)}.fly-topbar__brand:hover,.fly-topbar__module:hover{background:var(--bg-hover)}.fly-topbar__brand:focus-visible,.fly-topbar__module:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}.fly-topbar__brand-name{font-weight:var(--fw-semibold);white-space:nowrap}.fly-topbar__brand-mark{display:inline-flex;align-items:center;max-block-size:var(--icon-lg)}.fly-topbar__sep{color:var(--ink-4);-webkit-user-select:none;user-select:none}.fly-topbar__module{min-inline-size:0}.fly-topbar__module--on{background:var(--bg-hover)}.fly-topbar__module-label{font-weight:var(--fw-medium);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-topbar__module-label--empty{color:var(--ink-3)}.fly-topbar__module-icon,.fly-topbar__item-icon{display:inline-flex;align-items:center;justify-content:center;inline-size:var(--icon-md);block-size:var(--icon-md);flex:0 0 auto;color:var(--accent)}.fly-topbar__chevron{flex:0 0 auto;color:var(--ink-3);transition:transform var(--t-state)}.fly-topbar__module--on .fly-topbar__chevron{transform:rotate(180deg)}.fly-topbar__anchor{position:relative}.fly-topbar__pop{position:absolute;inset-block-start:calc(100% + var(--sp-1));inset-inline-start:0;z-index:var(--z-overlay);min-inline-size:260px;max-inline-size:min(680px,92vw);padding:var(--sp-3);border:1px solid var(--line);border-radius:var(--r-lg);background:var(--bg-2);box-shadow:var(--shadow-panel)}.fly-topbar__cols{display:flex;gap:var(--sp-4);align-items:flex-start}@media(width<=720px){.fly-topbar__cols{flex-direction:column;gap:var(--sp-3)}}.fly-topbar__col{display:flex;flex-direction:column;gap:2px;min-inline-size:220px;flex:1 1 0}.fly-topbar__col-title{font-size:var(--text-2xs);font-weight:var(--fw-semibold);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--ink-3);padding:var(--sp-2) var(--sp-2) var(--sp-1)}.fly-topbar__item{display:flex;align-items:flex-start;gap:var(--sp-2);inline-size:100%;text-align:start;appearance:none;border:0;background:transparent;cursor:pointer;font:inherit;color:var(--ink);padding:var(--sp-2);border-radius:var(--r-md);transition:background var(--t-state)}.fly-topbar__item:hover:not(:disabled){background:var(--bg-hover)}.fly-topbar__item:focus-visible{outline:2px solid var(--focus-ring);outline-offset:-2px}.fly-topbar__item:disabled{opacity:.5;cursor:default}.fly-topbar__item--on{background:var(--accent-soft);color:var(--on-accent-soft);box-shadow:inset 2px 0 0 0 var(--accent)}.fly-topbar__item-main{display:flex;flex-direction:column;gap:1px;min-inline-size:0}.fly-topbar__item-label{font-weight:var(--fw-medium);line-height:1.3}.fly-topbar__item-desc{font-size:var(--text-xs);color:var(--ink-3);line-height:1.35}.fly-topbar__foot{margin-block-start:var(--sp-3);padding-block-start:var(--sp-2);border-block-start:1px solid var(--line-3);font-size:var(--text-2xs);color:var(--ink-4)}.fly-topbar__actions{display:flex;align-items:center;gap:var(--sp-2);flex:0 0 auto}@media(forced-colors:active){.fly-topbar__item--on{border-inline-start:2px solid CanvasText}}@media(prefers-reduced-motion:reduce){.fly-topbar__brand,.fly-topbar__module,.fly-topbar__item,.fly-topbar__chevron{transition:none}}\n"] }]
22361
+ args: [{ selector: 'fly-app-topbar', standalone: true, imports: [TranslatePipe, FlyClickOutsideDirective, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, host: { '(document:keydown.escape)': 'onEscape()' }, template: "<header class=\"fly-topbar\">\r\n <div class=\"fly-topbar__lead\">\r\n <button\r\n type=\"button\"\r\n class=\"fly-topbar__brand\"\r\n (click)=\"brandSelected.emit()\"\r\n [attr.aria-label]=\"brandAriaLabel()\"\r\n >\r\n <span class=\"fly-topbar__brand-mark\"><ng-content select=\"[app-topbar-brand]\" /></span>\r\n @if (brandLabelKey(); as key) {\r\n <span class=\"fly-topbar__brand-name\">{{ key | translate }}</span>\r\n }\r\n </button>\r\n\r\n @if (activeModule()) {\r\n <span class=\"fly-topbar__sep\" aria-hidden=\"true\">/</span>\r\n }\r\n\r\n <div class=\"fly-topbar__anchor\" (flyClickOutside)=\"close()\" [flyClickOutsideEnabled]=\"open()\">\r\n <button\r\n #trigger\r\n type=\"button\"\r\n class=\"fly-topbar__module\"\r\n [class.fly-topbar__module--on]=\"open()\"\r\n (click)=\"toggle()\"\r\n (keydown)=\"onTriggerKeydown($event)\"\r\n aria-haspopup=\"menu\"\r\n [attr.aria-expanded]=\"open()\"\r\n [attr.aria-label]=\"triggerAriaLabel()\"\r\n >\r\n @if (activeModule(); as active) {\r\n <span class=\"fly-topbar__module-icon\">\r\n @if (iconFor(active.key); as tpl) {\r\n <ng-container *ngTemplateOutlet=\"tpl\" />\r\n } @else if (active.iconClass) {\r\n <i [class]=\"active.iconClass\" aria-hidden=\"true\"></i>\r\n }\r\n </span>\r\n <span class=\"fly-topbar__module-label\">{{ active.labelKey | translate }}</span>\r\n } @else {\r\n <span class=\"fly-topbar__module-label fly-topbar__module-label--empty\">\r\n {{ 'ui.nav.selectModule' | translate }}\r\n </span>\r\n }\r\n <svg class=\"fly-topbar__chevron\" width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\"\r\n stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\r\n aria-hidden=\"true\">\r\n <polyline points=\"6 9 12 15 18 9\" />\r\n </svg>\r\n </button>\r\n\r\n @if (open()) {\r\n <div class=\"fly-topbar__pop\" role=\"menu\" [attr.aria-label]=\"switchAriaLabel()\">\r\n <div class=\"fly-topbar__cols\">\r\n @for (section of sections(); track $index) {\r\n <div class=\"fly-topbar__col\">\r\n @if (section.titleKey; as titleKey) {\r\n <div class=\"fly-topbar__col-title\">{{ titleKey | translate }}</div>\r\n }\r\n @for (mod of section.modules; track mod.key) {\r\n <button\r\n #row\r\n type=\"button\"\r\n class=\"fly-topbar__item\"\r\n [class.fly-topbar__item--on]=\"mod.key === activeKey()\"\r\n role=\"menuitem\"\r\n [disabled]=\"mod.disabled ?? false\"\r\n [attr.tabindex]=\"indexOf(mod) === focusIndex() ? 0 : -1\"\r\n [attr.aria-current]=\"mod.key === activeKey() ? 'page' : null\"\r\n (click)=\"select(mod)\"\r\n (keydown)=\"onRowKeydown($event)\"\r\n >\r\n <span class=\"fly-topbar__item-icon\">\r\n @if (iconFor(mod.key); as tpl) {\r\n <ng-container *ngTemplateOutlet=\"tpl\" />\r\n } @else if (mod.iconClass) {\r\n <i [class]=\"mod.iconClass\" aria-hidden=\"true\"></i>\r\n }\r\n </span>\r\n <span class=\"fly-topbar__item-main\">\r\n <span class=\"fly-topbar__item-label\">{{ mod.labelKey | translate }}</span>\r\n @if (mod.descKey; as descKey) {\r\n <span class=\"fly-topbar__item-desc\">{{ descKey | translate }}</span>\r\n }\r\n </span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n @if (footerLabel()) {\r\n <div class=\"fly-topbar__foot\">{{ footerLabel() }}</div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <div class=\"fly-topbar__actions\"><ng-content select=\"[app-topbar-actions]\" /></div>\r\n</header>\r\n", styles: [":host{display:block;position:relative;z-index:var(--z-sticky)}.fly-topbar{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);padding:var(--sp-2) var(--sp-3);border-block-end:1px solid var(--line);background:var(--bg-3)}.fly-topbar__lead{display:flex;align-items:center;gap:var(--sp-2);min-inline-size:0}.fly-topbar__brand,.fly-topbar__module{display:inline-flex;align-items:center;gap:var(--sp-2);appearance:none;border:0;background:transparent;cursor:pointer;font:inherit;color:var(--ink);padding:var(--sp-1) var(--sp-2);border-radius:var(--r-md);transition:background var(--t-state)}.fly-topbar__brand:hover,.fly-topbar__module:hover{background:var(--bg-hover)}.fly-topbar__brand:focus-visible,.fly-topbar__module:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}.fly-topbar__brand-name{font-weight:var(--fw-semibold);white-space:nowrap}.fly-topbar__brand-mark{display:inline-flex;align-items:center;max-block-size:var(--icon-lg)}.fly-topbar__sep{color:var(--ink-4);-webkit-user-select:none;user-select:none}.fly-topbar__module{min-inline-size:0}.fly-topbar__module--on{background:var(--bg-hover)}.fly-topbar__module-label{font-weight:var(--fw-medium);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-topbar__module-label--empty{color:var(--ink-3)}.fly-topbar__module-icon,.fly-topbar__item-icon{display:inline-flex;align-items:center;justify-content:center;inline-size:var(--icon-md);block-size:var(--icon-md);flex:0 0 auto;color:var(--accent)}.fly-topbar__chevron{flex:0 0 auto;color:var(--ink-3);transition:transform var(--t-state)}.fly-topbar__module--on .fly-topbar__chevron{transform:rotate(180deg)}.fly-topbar__anchor{position:relative}.fly-topbar__pop{position:absolute;inset-block-start:calc(100% + var(--sp-1));inset-inline-start:0;z-index:var(--z-overlay);min-inline-size:260px;max-inline-size:min(680px,92vw);padding:var(--sp-3);border:1px solid var(--line);border-radius:var(--r-lg);background:var(--bg-2);box-shadow:var(--shadow-panel)}.fly-topbar__cols{display:flex;gap:var(--sp-4);align-items:flex-start}@media(width<=720px){.fly-topbar__cols{flex-direction:column;gap:var(--sp-3)}}.fly-topbar__col{display:flex;flex-direction:column;gap:2px;min-inline-size:220px;flex:1 1 0}.fly-topbar__col-title{font-size:var(--text-2xs);font-weight:var(--fw-semibold);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--ink-3);padding:var(--sp-2) var(--sp-2) var(--sp-1)}.fly-topbar__item{display:flex;align-items:flex-start;gap:var(--sp-2);inline-size:100%;text-align:start;appearance:none;border:0;background:transparent;cursor:pointer;font:inherit;color:var(--ink);padding:var(--sp-2);border-radius:var(--r-md);transition:background var(--t-state)}.fly-topbar__item:hover:not(:disabled){background:var(--bg-hover)}.fly-topbar__item:focus-visible{outline:2px solid var(--focus-ring);outline-offset:-2px}.fly-topbar__item:disabled{opacity:.5;cursor:default}.fly-topbar__item--on{background:var(--accent-soft);color:var(--on-accent-soft);box-shadow:inset 2px 0 0 0 var(--accent)}.fly-topbar__item-main{display:flex;flex-direction:column;gap:1px;min-inline-size:0}.fly-topbar__item-label{font-weight:var(--fw-medium);line-height:1.3}.fly-topbar__item-desc{font-size:var(--text-xs);color:var(--ink-3);line-height:1.35}.fly-topbar__foot{margin-block-start:var(--sp-3);padding-block-start:var(--sp-2);border-block-start:1px solid var(--line-3);font-size:var(--text-2xs);color:var(--ink-4)}.fly-topbar__actions{display:flex;align-items:center;gap:var(--sp-2);flex:0 0 auto}@media(forced-colors:active){.fly-topbar__item--on{border-inline-start:2px solid CanvasText}}@media(prefers-reduced-motion:reduce){.fly-topbar__brand,.fly-topbar__module,.fly-topbar__item,.fly-topbar__chevron{transition:none}}\n"] }]
21973
22362
  }], ctorParameters: () => [], propDecorators: { brandLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "brandLabelKey", required: false }] }], sections: [{ type: i0.Input, args: [{ isSignal: true, alias: "sections", required: false }] }], activeKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeKey", required: false }] }], footerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "footerLabel", required: false }] }], moduleSelected: [{ type: i0.Output, args: ["moduleSelected"] }], brandSelected: [{ type: i0.Output, args: ["brandSelected"] }], icons: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => FlyModuleIconDirective), { isSignal: true }] }], rows: [{ type: i0.ViewChildren, args: ['row', { isSignal: true }] }], trigger: [{ type: i0.ViewChildren, args: ['trigger', { isSignal: true }] }] } });
21974
22363
 
21975
22364
  /**
@@ -24450,5 +24839,5 @@ const AUDIENCE_ERROR_CODES = {
24450
24839
  * Generated bundle index. Do not edit.
24451
24840
  */
24452
24841
 
24453
- 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, 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_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_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_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_THEME_MODE_IDS, FLY_VIEWPORT_IS_MOBILE, FLY_WINDOW_HELP_HINT_EVENT, FLY_WINDOW_PAGE_TITLE_EVENT, FlyAchievementProgressComponent, FlyActionMenuComponent, FlyActionStackComponent, FlyAgentDraggableDirective, FlyAnimatedEmojiComponent, FlyAppHomeComponent, FlyAppTopbarComponent, 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, FlyCurrencySelectorComponent, FlyDataTableComponent, FlyDatePipe, FlyDateTimePipe, FlyDebouncer, FlyDecimalNumberPipe, FlyDetailCardComponent, FlyDetailShellComponent, FlyDrawerComponent, FlyDurationPipe, FlyDynamicFormComponent, FlyEmojiPickerComponent, FlyFieldComponent, FlyFileDownloadService, FlyFileUploadComponent, FlyFilterPanelComponent, FlyFormBannerComponent, FlyFormFooterComponent, FlyFormGridComponent, FlyFormSectionComponent, FlyFullNumberPipe, FlyGanttComponent, FlyHubClient, FlyIconButtonComponent, FlyIfAdminDirective, FlyIfRoleDirective, FlyImageUploadComponent, FlyLeaderboardComponent, FlyMagicActionsComponent, FlyMetaListComponent, FlyModalComponent, FlyModerationQueueComponent, FlyModuleIconDirective, FlyPaginationComponent, FlyPeoplePickerComponent, FlyPointsTierComponent, FlyProgressComponent, FlyRelativeTimePipe, FlyRemoteContextService, FlyRemoteRouter, FlyRemoteRouterOutletComponent, FlySearchInputComponent, FlySectionHeaderComponent, FlySecureSrcDirective, FlySegmentedComponent, FlySelectComponent, FlySkeletonComponent, FlySliderComponent, FlySparklineComponent, FlySpinnerComponent, FlyStandaloneAuthCallbackComponent, FlyStandaloneAuthService, FlyStateMessageComponent, FlyDynamicFormComponent as FlySurveyFormComponent, FlyTabComponent, FlyTabsComponent, FlyTagsInputComponent, FlyThemeService, FlyTimePipe, FlyToastService, FlyToggleComponent, FlyTooltipDirective, FlyTreeNavComponent, FlyTypeaheadComponent, FlyUserDirectoryService, FlyWindowHelpService, FlyWindowTitleService, FlyosPendingLaunchesGlobalKey, FlyosShellHandlesLaunchRequestsGlobalKey, FlyosShellOwnsHistoryGlobalKey, 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, WindowManagerService, applySkinTone, applySuggestion, ariaSort, buildCron, canConfirm, canGoNext, canGoPrev, captureFocus, clampMagicBarSearchWidth, clampPage, clampSliderValue, connectRemoteLaunch, enterActivatesNatively, 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, flyFormatTime, flyFullNumber, flyNextEnabledIndex, flyNextSegmentIndex, flyRelativeTime, flyResolveActiveTab, flyRoleGuard, flyScanPendingDelayMs, flyScanRetry, flySignedCompact, flySkinToneLabelKey, flyStandaloneAuthGuard, flyStandaloneAuthInterceptor, flyToDateOnly, flyToPage, flyUnwrap, flyUnwrapLenient, hasAnyRoleIn, healNativeFederationCacheOnce, initialExpanded, isAccentColor, isChunkLoadError, isFlyAuthStorePopulated, isNativeFederationCacheError, isRtlLocale, isRtlLocaleEntry, isValidCron, isValidSingleField, loadRemoteStyles, magicBarOwnerKey, magicBarSearchText, matchFlyRoutePattern, nextEnabledIndex, nextModuleIndex, nextSegmentIndex, nextSort, normalizeFlyTheme, normalizeRoles, novaPriorityToneVar, novaStatusToneVar, overlayStack, parseCron, parseStepUpChallenge, parseTags, presenceColorFor, printConsoleSecurityWarning, provideFlyChunkReloadRecovery, provideFlyEmojiPack, provideFlyStandaloneAuth, reloadOnceForChunkError, requestAppLaunch, resolveActiveModule, resolveActiveSection, resolveActiveTab, resolveCellValue, resolveStateIcon, resolveStateMessageKey, restoreFocus, sameTags, sectionHintKey, sectionLayout, sliderTrackGradient, sparklinePath, sparklinePoints, stepUpInterceptor, stepUpReturnKey, trimAgentPayload, trimAgentString, unloadRemoteStyles, utf8ByteLength, validateAgentPayload, warnIfEmbeddedSessionMissing };
24842
+ 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_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_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_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_THEME_MODE_IDS, FLY_VIEWPORT_IS_MOBILE, FLY_WINDOW_HELP_HINT_EVENT, FLY_WINDOW_PAGE_TITLE_EVENT, FlyAchievementProgressComponent, FlyActionMenuComponent, FlyActionStackComponent, FlyAgentDraggableDirective, FlyAnimatedEmojiComponent, FlyAppHomeComponent, FlyAppTopbarComponent, 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, FlyCurrencySelectorComponent, FlyDataTableComponent, FlyDatePipe, FlyDateTimePipe, FlyDebouncer, FlyDecimalNumberPipe, FlyDetailCardComponent, FlyDetailShellComponent, FlyDrawerComponent, FlyDurationPipe, FlyDynamicFormComponent, FlyEmojiPickerComponent, FlyFieldComponent, FlyFileDownloadService, FlyFileUploadComponent, FlyFilterPanelComponent, FlyFormBannerComponent, FlyFormFooterComponent, FlyFormGridComponent, FlyFormSectionComponent, FlyFullNumberPipe, FlyGanttComponent, FlyHubClient, FlyIconButtonComponent, FlyIfAdminDirective, FlyIfRoleDirective, FlyImageUploadComponent, FlyLeaderboardComponent, FlyMagicActionsComponent, FlyMetaListComponent, FlyModalComponent, FlyModerationQueueComponent, FlyModuleIconDirective, FlyMoneyPipe, FlyPaginationComponent, FlyPeoplePickerComponent, FlyPointsTierComponent, FlyProgressComponent, FlyRelativeTimePipe, FlyRemoteContextService, FlyRemoteRouter, FlyRemoteRouterOutletComponent, FlySearchInputComponent, FlySectionHeaderComponent, FlySecureSrcDirective, FlySegmentedComponent, FlySelectComponent, FlySkeletonComponent, FlySliderComponent, FlySparklineComponent, FlySpinnerComponent, FlyStandaloneAuthCallbackComponent, FlyStandaloneAuthService, FlyStateMessageComponent, FlyDynamicFormComponent as FlySurveyFormComponent, FlyTabComponent, FlyTabsComponent, FlyTagsInputComponent, FlyThemeService, FlyTimePipe, FlyToastService, FlyToggleComponent, FlyTooltipDirective, FlyTreeNavComponent, FlyTypeaheadComponent, FlyUserDirectoryService, FlyWindowHelpService, FlyWindowTitleService, FlyosPendingLaunchesGlobalKey, FlyosShellHandlesLaunchRequestsGlobalKey, FlyosShellOwnsHistoryGlobalKey, 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, WindowManagerService, applySkinTone, applySuggestion, ariaSort, buildCron, canConfirm, canGoNext, canGoPrev, captureFocus, clampMagicBarSearchWidth, clampPage, clampSliderValue, connectRemoteLaunch, enterActivatesNatively, 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, flyUnwrapLenient, hasAnyRoleIn, healNativeFederationCacheOnce, initialExpanded, isAccentColor, isChunkLoadError, isFlyAuthStorePopulated, isNativeFederationCacheError, isRtlLocale, isRtlLocaleEntry, isValidCron, isValidSingleField, loadRemoteStyles, magicBarOwnerKey, magicBarSearchText, matchFlyRoutePattern, nextEnabledIndex, nextModuleIndex, nextSegmentIndex, nextSort, normalizeFlyTheme, normalizeRoles, novaPriorityToneVar, novaStatusToneVar, overlayStack, parseCron, parseStepUpChallenge, parseTags, prefetchRemoteStyles, presenceColorFor, printConsoleSecurityWarning, provideFlyChunkReloadRecovery, provideFlyEmojiPack, provideFlyStandaloneAuth, reloadOnceForChunkError, requestAppLaunch, resolveActiveModule, resolveActiveSection, resolveActiveTab, resolveCellValue, resolveStateIcon, resolveStateMessageKey, restoreFocus, sameTags, sectionHintKey, sectionLayout, sliderTrackGradient, sparklinePath, sparklinePoints, stepUpInterceptor, stepUpReturnKey, trimAgentPayload, trimAgentString, unloadRemoteStyles, utf8ByteLength, validateAgentPayload, warnIfEmbeddedSessionMissing };
24454
24843
  //# sourceMappingURL=flyos-design-system.mjs.map