@flyos/design-system 2.0.0 → 2.1.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.
@@ -48,7 +48,7 @@ import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
48
48
  // tools/publish-library.ps1 at bump time, and asserted by the spec beside this file.
49
49
  // Used only for the diagnostic message; the duplicate-instance detection itself is
50
50
  // version-agnostic, so a stale literal misnames a fork rather than hiding one.
51
- const FLY_DS_VERSION = '2.0.0';
51
+ const FLY_DS_VERSION = '2.1.0';
52
52
  const FLY_DS_REGISTRY_KEY = '__FLY_DS_INSTANCES__';
53
53
  /**
54
54
  * Records this design-system instance on the shared `scope` and returns the
@@ -1594,6 +1594,7 @@ const DS_BASELINE_LOCALES = {
1594
1594
  'currency_selector.clear': 'Clear selection',
1595
1595
  'currency_selector.pinned': 'Frequently used',
1596
1596
  'currency_selector.all': 'All currencies',
1597
+ 'currency_selector.locked_default_reason': 'This currency is locked and can’t be changed.',
1597
1598
  // chat composer (fly-chat-composer)
1598
1599
  'chat_composer.label.message': 'Message',
1599
1600
  'chat_composer.label.mention_suggestions': 'Mention suggestions',
@@ -1957,6 +1958,7 @@ const DS_BASELINE_LOCALES = {
1957
1958
  'currency_selector.clear': 'مسح التحديد',
1958
1959
  'currency_selector.pinned': 'الأكثر استخداماً',
1959
1960
  'currency_selector.all': 'كل العملات',
1961
+ 'currency_selector.locked_default_reason': 'هذه العملة مقفلة ولا يمكن تغييرها.',
1960
1962
  // chat composer (fly-chat-composer)
1961
1963
  'chat_composer.label.message': 'رسالة',
1962
1964
  'chat_composer.label.mention_suggestions': 'اقتراحات الإشارة',
@@ -2320,6 +2322,7 @@ const DS_BASELINE_LOCALES = {
2320
2322
  'currency_selector.clear': 'Effacer la sélection',
2321
2323
  'currency_selector.pinned': 'Fréquemment utilisées',
2322
2324
  'currency_selector.all': 'Toutes les devises',
2325
+ 'currency_selector.locked_default_reason': 'Cette devise est verrouillée et ne peut pas être modifiée.',
2323
2326
  // chat composer (fly-chat-composer)
2324
2327
  'chat_composer.label.message': 'Message',
2325
2328
  'chat_composer.label.mention_suggestions': 'Suggestions de mention',
@@ -2682,6 +2685,7 @@ const DS_BASELINE_LOCALES = {
2682
2685
  'currency_selector.clear': 'انتخاب صاف کریں',
2683
2686
  'currency_selector.pinned': 'کثرت سے استعمال شدہ',
2684
2687
  'currency_selector.all': 'تمام کرنسیاں',
2688
+ 'currency_selector.locked_default_reason': 'یہ کرنسی مقفل ہے اور اسے تبدیل نہیں کیا جا سکتا۔',
2685
2689
  // chat composer (fly-chat-composer)
2686
2690
  'chat_composer.label.message': 'پیغام',
2687
2691
  'chat_composer.label.mention_suggestions': 'تذکرے کی تجاویز',
@@ -4582,6 +4586,22 @@ const _linkLayerSupported = typeof HTMLLinkElement !== 'undefined' && 'layer' in
4582
4586
  const _cspNonce = FLY_CSP_NONCE;
4583
4587
  /** In-flight fetch promises keyed by appId — prevents duplicate fetches. */
4584
4588
  const _inFlight = new Map();
4589
+ /** Upper bound on the optional bytes-applied wait — never holds a caller hostage. */
4590
+ const APPLY_CAP_MS = 4000;
4591
+ /** Resolves when `el` fires load/error, or after `capMs` — whichever is first. */
4592
+ function _awaitLoadCapped(el, capMs) {
4593
+ return new Promise((resolve) => {
4594
+ const timer = setTimeout(done, capMs);
4595
+ function done() {
4596
+ clearTimeout(timer);
4597
+ el.removeEventListener('load', done);
4598
+ el.removeEventListener('error', done);
4599
+ resolve();
4600
+ }
4601
+ el.addEventListener('load', done);
4602
+ el.addEventListener('error', done);
4603
+ });
4604
+ }
4585
4605
  // ---------------------------------------------------------------------------
4586
4606
  // Layer order — injected immediately at module init (synchronous, runs once).
4587
4607
  // ---------------------------------------------------------------------------
@@ -4748,7 +4768,7 @@ function _validateHref(href, remoteBaseUrl) {
4748
4768
  * The `data-fly-href` attribute stores the discovered href separately from the
4749
4769
  * element content so idempotency checks can compare the URL without parsing CSS.
4750
4770
  */
4751
- async function loadRemoteStyles(appId, remoteBaseUrl) {
4771
+ async function loadRemoteStyles(appId, remoteBaseUrl, opts = {}) {
4752
4772
  if (typeof document === 'undefined')
4753
4773
  return; // SSR guard
4754
4774
  // Resolve relative remoteBaseUrl (e.g. '/circles-dev') against the current
@@ -4767,7 +4787,9 @@ async function loadRemoteStyles(appId, remoteBaseUrl) {
4767
4787
  const href = await fetchPromise;
4768
4788
  _inFlight.delete(appId);
4769
4789
  if (!href) {
4770
- console.warn(`[FlyOS] loadRemoteStyles: no stylesheet found in ${absoluteBase}/index.html for appId="${appId}"`);
4790
+ if (!opts.silent) {
4791
+ console.warn(`[FlyOS] loadRemoteStyles: no stylesheet found in ${absoluteBase}/index.html for appId="${appId}"`);
4792
+ }
4771
4793
  return;
4772
4794
  }
4773
4795
  // Check both <link> and <style> selectors — handle upgrades from the old fallback path.
@@ -4792,19 +4814,43 @@ async function loadRemoteStyles(appId, remoteBaseUrl) {
4792
4814
  if (_cspNonce)
4793
4815
  link.nonce = _cspNonce;
4794
4816
  document.head.appendChild(link);
4817
+ // A stylesheet <link> fires load once its bytes are parsed — the direct signal.
4818
+ if (opts.awaitApplied)
4819
+ await _awaitLoadCapped(link, APPLY_CAP_MS);
4795
4820
  }
4796
4821
  else {
4797
4822
  // Fallback path: <style> with @import layer(remote) for older browsers.
4798
4823
  // CSS Cascade Level 5: `@import url("…") layer(remote)` is the only valid
4799
4824
  // way to place an @import inside a named cascade layer. Block-form
4800
4825
  // `@layer remote { @import … }` is invalid and silently dropped by browsers.
4826
+ //
4801
4827
  const style = document.createElement('style');
4802
4828
  style.setAttribute('data-fly-app', appId);
4803
4829
  style.setAttribute('data-fly-href', href);
4804
4830
  style.textContent = `@import url("${href}") layer(remote);`;
4805
4831
  if (_cspNonce)
4806
4832
  style.nonce = _cspNonce;
4833
+ // Appended synchronously after the `existing` check above — an `await`
4834
+ // between check and append would let a concurrent caller double-inject.
4807
4835
  document.head.appendChild(style);
4836
+ // A <style> element fires no load event for its @import, so for the
4837
+ // bytes-applied guarantee we shadow the same href with a `<link
4838
+ // rel="preload" as="style">`: it rides the @import's own in-flight fetch
4839
+ // (same no-cors request key) and its load event is the byte signal.
4840
+ // Deliberately NO crossorigin attribute — the @import request is no-cors,
4841
+ // and a CORS-mode preload would create a non-matching cache entry
4842
+ // (double fetch).
4843
+ if (opts.awaitApplied) {
4844
+ const preload = document.createElement('link');
4845
+ preload.setAttribute('rel', 'preload');
4846
+ preload.setAttribute('as', 'style');
4847
+ preload.setAttribute('href', href);
4848
+ if (_cspNonce)
4849
+ preload.nonce = _cspNonce;
4850
+ document.head.appendChild(preload);
4851
+ await _awaitLoadCapped(preload, APPLY_CAP_MS);
4852
+ preload.remove();
4853
+ }
4808
4854
  }
4809
4855
  }
4810
4856
  /**
@@ -4957,10 +5003,10 @@ class FlyRemoteRouterOutletComponent {
4957
5003
  });
4958
5004
  }
4959
5005
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyRemoteRouterOutletComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4960
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyRemoteRouterOutletComponent, isStandalone: true, selector: "fly-remote-router-outlet", ngImport: i0, template: `
4961
- @if (rendered(); as cmp) {
4962
- <ng-container *ngComponentOutlet="cmp" />
4963
- }
5006
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyRemoteRouterOutletComponent, isStandalone: true, selector: "fly-remote-router-outlet", ngImport: i0, template: `
5007
+ @if (rendered(); as cmp) {
5008
+ <ng-container *ngComponentOutlet="cmp" />
5009
+ }
4964
5010
  `, isInline: true, dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4965
5011
  }
4966
5012
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyRemoteRouterOutletComponent, decorators: [{
@@ -4970,10 +5016,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
4970
5016
  standalone: true,
4971
5017
  imports: [NgComponentOutlet],
4972
5018
  changeDetection: ChangeDetectionStrategy.OnPush,
4973
- template: `
4974
- @if (rendered(); as cmp) {
4975
- <ng-container *ngComponentOutlet="cmp" />
4976
- }
5019
+ template: `
5020
+ @if (rendered(); as cmp) {
5021
+ <ng-container *ngComponentOutlet="cmp" />
5022
+ }
4977
5023
  `,
4978
5024
  }]
4979
5025
  }], ctorParameters: () => [] });
@@ -5040,11 +5086,7 @@ class MockAuthService {
5040
5086
  // All signal/computed calls are inside the constructor body so Angular's
5041
5087
  // injection context and ngDevMode are fully set up before they execute.
5042
5088
  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 */ []));
5089
+ this._session = signal(this.createSession(), ...(ngDevMode ? [{ debugName: "_session" }] : /* istanbul ignore next */ []));
5048
5090
  this.isAuthenticated = computed(() => {
5049
5091
  const s = this._session();
5050
5092
  return s !== null && s.expiresAt > Date.now();
@@ -5068,6 +5110,14 @@ class MockAuthService {
5068
5110
  return;
5069
5111
  this._session.set({ ...session, user: { ...session.user, ...patch } });
5070
5112
  }
5113
+ /** A fresh 24h mock session from the app-supplied config — used at construction and by {@link startLogin}. */
5114
+ createSession() {
5115
+ return {
5116
+ accessToken: this._config.token ?? 'mock-token',
5117
+ user: this._config.user,
5118
+ expiresAt: Date.now() + 24 * 60 * 60 * 1000,
5119
+ };
5120
+ }
5071
5121
  /** Override in subclass to supply app-specific mock data. */
5072
5122
  getConfig() {
5073
5123
  return {
@@ -5088,6 +5138,15 @@ class MockAuthService {
5088
5138
  // Mock mode: already authenticated, nothing to initialize.
5089
5139
  }
5090
5140
  startLogin() {
5141
+ // Parity with a real STS round-trip: startLogin() must land the user back
5142
+ // AUTHENTICATED. After logout() nulls the session, a guard-triggered
5143
+ // startLogin() that only navigated would bounce off authGuard forever
5144
+ // (navigate → guard sees no session → startLogin → navigate → …) and the
5145
+ // login CTA would appear dead. Re-arming the session first makes every
5146
+ // mock login an instant, self-healing "STS visit".
5147
+ if (this._session() === null) {
5148
+ this._session.set(this.createSession());
5149
+ }
5091
5150
  this.router.navigate(this._config.loginRedirect ?? ['/']);
5092
5151
  }
5093
5152
  handleCallback(_code, _state) {
@@ -9388,11 +9447,11 @@ class FlyBlockUiComponent {
9388
9447
  return k && k.length > 0 ? k : 'common.loading';
9389
9448
  }, ...(ngDevMode ? [{ debugName: "resolvedMessageKey" }] : /* istanbul ignore next */ []));
9390
9449
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyBlockUiComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9391
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyBlockUiComponent, isStandalone: true, selector: "fly-block-ui", inputs: { active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: true, transformFunction: null }, messageKey: { classPropertyName: "messageKey", publicName: "messageKey", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (active()) {\r\n <div\r\n class=\"fly-block-ui\"\r\n role=\"status\"\r\n aria-live=\"polite\"\r\n aria-busy=\"true\"\r\n [attr.aria-label]=\"resolvedMessageKey() | translate\">\r\n <div class=\"fly-block-ui__card\">\r\n <i class=\"pi pi-spin pi-spinner fly-block-ui__spinner\" aria-hidden=\"true\"></i>\r\n <span class=\"fly-block-ui__text\">{{ resolvedMessageKey() | translate }}</span>\r\n </div>\r\n </div>\r\n}\r\n", styles: [":host{display:contents}.fly-block-ui{position:absolute;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--surface-ground, #0c0c12) 58%,transparent);-webkit-backdrop-filter:blur(6px) saturate(120%);backdrop-filter:blur(6px) saturate(120%)}.fly-block-ui__card{display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 36px;border-radius:12px;background:var(--glass-bg, rgba(255, 255, 255, .14));border:1px solid var(--glass-border, rgba(255, 255, 255, .22));box-shadow:0 8px 32px #0000002e}.fly-block-ui__spinner{font-size:2rem;color:var(--primary-color)}.fly-block-ui__text{font-size:.875rem;color:var(--text-color-secondary)}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9450
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyBlockUiComponent, isStandalone: true, selector: "fly-block-ui", inputs: { active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: true, transformFunction: null }, messageKey: { classPropertyName: "messageKey", publicName: "messageKey", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (active()) {\n <div\n class=\"fly-block-ui\"\n role=\"status\"\n aria-live=\"polite\"\n aria-busy=\"true\"\n [attr.aria-label]=\"resolvedMessageKey() | translate\">\n <div class=\"fly-block-ui__card\">\n <i class=\"pi pi-spin pi-spinner fly-block-ui__spinner\" aria-hidden=\"true\"></i>\n <span class=\"fly-block-ui__text\">{{ resolvedMessageKey() | translate }}</span>\n </div>\n </div>\n}\n", styles: [":host{display:contents}.fly-block-ui{position:absolute;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--surface-ground, #0c0c12) 58%,transparent);-webkit-backdrop-filter:blur(6px) saturate(120%);backdrop-filter:blur(6px) saturate(120%)}.fly-block-ui__card{display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 36px;border-radius:12px;background:var(--glass-bg, rgba(255, 255, 255, .14));border:1px solid var(--glass-border, rgba(255, 255, 255, .22));box-shadow:0 8px 32px #0000002e}.fly-block-ui__spinner{font-size:2rem;color:var(--primary-color)}.fly-block-ui__text{font-size:.875rem;color:var(--text-color-secondary)}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9392
9451
  }
9393
9452
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyBlockUiComponent, decorators: [{
9394
9453
  type: Component,
9395
- args: [{ selector: 'fly-block-ui', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (active()) {\r\n <div\r\n class=\"fly-block-ui\"\r\n role=\"status\"\r\n aria-live=\"polite\"\r\n aria-busy=\"true\"\r\n [attr.aria-label]=\"resolvedMessageKey() | translate\">\r\n <div class=\"fly-block-ui__card\">\r\n <i class=\"pi pi-spin pi-spinner fly-block-ui__spinner\" aria-hidden=\"true\"></i>\r\n <span class=\"fly-block-ui__text\">{{ resolvedMessageKey() | translate }}</span>\r\n </div>\r\n </div>\r\n}\r\n", styles: [":host{display:contents}.fly-block-ui{position:absolute;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--surface-ground, #0c0c12) 58%,transparent);-webkit-backdrop-filter:blur(6px) saturate(120%);backdrop-filter:blur(6px) saturate(120%)}.fly-block-ui__card{display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 36px;border-radius:12px;background:var(--glass-bg, rgba(255, 255, 255, .14));border:1px solid var(--glass-border, rgba(255, 255, 255, .22));box-shadow:0 8px 32px #0000002e}.fly-block-ui__spinner{font-size:2rem;color:var(--primary-color)}.fly-block-ui__text{font-size:.875rem;color:var(--text-color-secondary)}\n"] }]
9454
+ args: [{ selector: 'fly-block-ui', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (active()) {\n <div\n class=\"fly-block-ui\"\n role=\"status\"\n aria-live=\"polite\"\n aria-busy=\"true\"\n [attr.aria-label]=\"resolvedMessageKey() | translate\">\n <div class=\"fly-block-ui__card\">\n <i class=\"pi pi-spin pi-spinner fly-block-ui__spinner\" aria-hidden=\"true\"></i>\n <span class=\"fly-block-ui__text\">{{ resolvedMessageKey() | translate }}</span>\n </div>\n </div>\n}\n", styles: [":host{display:contents}.fly-block-ui{position:absolute;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--surface-ground, #0c0c12) 58%,transparent);-webkit-backdrop-filter:blur(6px) saturate(120%);backdrop-filter:blur(6px) saturate(120%)}.fly-block-ui__card{display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 36px;border-radius:12px;background:var(--glass-bg, rgba(255, 255, 255, .14));border:1px solid var(--glass-border, rgba(255, 255, 255, .22));box-shadow:0 8px 32px #0000002e}.fly-block-ui__spinner{font-size:2rem;color:var(--primary-color)}.fly-block-ui__text{font-size:.875rem;color:var(--text-color-secondary)}\n"] }]
9396
9455
  }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: true }] }], messageKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "messageKey", required: false }] }] } });
9397
9456
 
9398
9457
  /**
@@ -18522,11 +18581,11 @@ class FlyPeoplePickerComponent {
18522
18581
  this.selectionChange.emit(this._selected().map((o) => o.id));
18523
18582
  }
18524
18583
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyPeoplePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
18525
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyPeoplePickerComponent, isStandalone: true, selector: "fly-people-picker", inputs: { searchFn: { classPropertyName: "searchFn", publicName: "searchFn", isSignal: true, isRequired: true, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, excludeIds: { classPropertyName: "excludeIds", publicName: "excludeIds", isSignal: true, isRequired: false, transformFunction: null }, initialSelection: { classPropertyName: "initialSelection", publicName: "initialSelection", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange" }, host: { classAttribute: "fly-people-picker" }, viewQueries: [{ propertyName: "typeahead", first: true, predicate: FlyTypeaheadComponent, descendants: true }], ngImport: i0, template: "@if (selected().length > 0) {\r\n <ul class=\"fly-people-picker__chips\" role=\"list\">\r\n @for (o of selected(); track o.id) {\r\n <li class=\"fly-people-picker__chip\" role=\"listitem\">\r\n <span class=\"fly-people-picker__chip-avatar\" aria-hidden=\"true\">\r\n @if (o.avatarUrl) {\r\n <img [src]=\"o.avatarUrl\" alt=\"\" />\r\n } @else {\r\n <span class=\"fly-people-picker__chip-initials\">{{ initialsFor(o.displayName) }}</span>\r\n }\r\n </span>\r\n <span class=\"fly-people-picker__chip-name\" [title]=\"o.email || o.displayName\">{{ o.displayName }}</span>\r\n <button\r\n type=\"button\"\r\n class=\"fly-people-picker__chip-remove\"\r\n [attr.aria-label]=\"'people_picker.remove' | translate: { name: o.displayName }\"\r\n (click)=\"remove(o.id)\">\r\n \u2715\r\n </button>\r\n </li>\r\n }\r\n </ul>\r\n}\r\n\r\n@if (showSearch()) {\r\n <fly-typeahead\r\n [searchFn]=\"typeaheadSearchFn\"\r\n [placeholder]=\"placeholder() || ('people_picker.search_placeholder' | translate)\"\r\n [allowFreeText]=\"false\"\r\n (selected)=\"onPicked($event)\" />\r\n} @else {\r\n <button type=\"button\" class=\"fly-people-picker__change-btn\" (click)=\"change()\">\r\n {{ 'people_picker.change' | translate }}\r\n </button>\r\n}\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-hover: var(--surface-hover, #f1f5f9);--_border: var(--separator, var(--surface-border, #e2e8f0));--_text: var(--label-primary, var(--text-color, #0f172a));--_text-subtle: var(--label-secondary, var(--text-color-secondary, #64748b));--_fill: var(--fill-tertiary, #f3f4f6);--_accent: var(--accent);--_danger: var(--system-red, #ef4444);--_radius: 999px;display:flex;flex-direction:column;gap:8px;inline-size:100%;color:var(--_text);font-size:13px}.fly-people-picker__chips{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px}.fly-people-picker__chip{display:inline-flex;align-items:center;gap:6px;padding-block:3px;padding-inline:3px 6px;border-radius:var(--_radius);background:var(--_fill);max-inline-size:220px}.fly-people-picker__chip-avatar{inline-size:22px;block-size:22px;border-radius:50%;overflow:hidden;flex:none;background:var(--_surface);display:flex;align-items:center;justify-content:center}.fly-people-picker__chip-avatar img{inline-size:100%;block-size:100%;object-fit:cover}.fly-people-picker__chip-initials{font-size:9px;font-weight:700;color:var(--_text-subtle);text-transform:uppercase}.fly-people-picker__chip-name{font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-people-picker__chip-remove{border:none;background:transparent;padding:2px;margin-inline-start:2px;border-radius:50%;color:var(--_text-subtle);font-size:10px;line-height:1;cursor:pointer;flex:none}.fly-people-picker__chip-remove:hover{background:color-mix(in srgb,var(--_danger) 18%,transparent);color:var(--_danger)}.fly-people-picker__change-btn{align-self:flex-start;border:1px solid var(--_border);border-radius:8px;background:var(--_surface);padding:5px 12px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-people-picker__change-btn:hover{background:var(--_surface-hover)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: FlyTypeaheadComponent, selector: "fly-typeahead", inputs: ["searchFn", "debounceMs", "placeholder", "ariaLabel", "value", "allowFreeText", "openOnFocus"], outputs: ["valueChange", "selected", "cleared"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
18584
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyPeoplePickerComponent, isStandalone: true, selector: "fly-people-picker", inputs: { searchFn: { classPropertyName: "searchFn", publicName: "searchFn", isSignal: true, isRequired: true, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, excludeIds: { classPropertyName: "excludeIds", publicName: "excludeIds", isSignal: true, isRequired: false, transformFunction: null }, initialSelection: { classPropertyName: "initialSelection", publicName: "initialSelection", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange" }, host: { classAttribute: "fly-people-picker" }, viewQueries: [{ propertyName: "typeahead", first: true, predicate: FlyTypeaheadComponent, descendants: true }], ngImport: i0, template: "@if (selected().length > 0) {\n <ul class=\"fly-people-picker__chips\" role=\"list\">\n @for (o of selected(); track o.id) {\n <li class=\"fly-people-picker__chip\" role=\"listitem\">\n <span class=\"fly-people-picker__chip-avatar\" aria-hidden=\"true\">\n @if (o.avatarUrl) {\n <img [src]=\"o.avatarUrl\" alt=\"\" />\n } @else {\n <span class=\"fly-people-picker__chip-initials\">{{ initialsFor(o.displayName) }}</span>\n }\n </span>\n <span class=\"fly-people-picker__chip-name\" [title]=\"o.email || o.displayName\">{{ o.displayName }}</span>\n <button\n type=\"button\"\n class=\"fly-people-picker__chip-remove\"\n [attr.aria-label]=\"'people_picker.remove' | translate: { name: o.displayName }\"\n (click)=\"remove(o.id)\">\n \u2715\n </button>\n </li>\n }\n </ul>\n}\n\n@if (showSearch()) {\n <fly-typeahead\n [searchFn]=\"typeaheadSearchFn\"\n [placeholder]=\"placeholder() || ('people_picker.search_placeholder' | translate)\"\n [allowFreeText]=\"false\"\n (selected)=\"onPicked($event)\" />\n} @else {\n <button type=\"button\" class=\"fly-people-picker__change-btn\" (click)=\"change()\">\n {{ 'people_picker.change' | translate }}\n </button>\n}\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-hover: var(--surface-hover, #f1f5f9);--_border: var(--separator, var(--surface-border, #e2e8f0));--_text: var(--label-primary, var(--text-color, #0f172a));--_text-subtle: var(--label-secondary, var(--text-color-secondary, #64748b));--_fill: var(--fill-tertiary, #f3f4f6);--_accent: var(--accent);--_danger: var(--system-red, #ef4444);--_radius: 999px;display:flex;flex-direction:column;gap:8px;inline-size:100%;color:var(--_text);font-size:13px}.fly-people-picker__chips{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px}.fly-people-picker__chip{display:inline-flex;align-items:center;gap:6px;padding-block:3px;padding-inline:3px 6px;border-radius:var(--_radius);background:var(--_fill);max-inline-size:220px}.fly-people-picker__chip-avatar{inline-size:22px;block-size:22px;border-radius:50%;overflow:hidden;flex:none;background:var(--_surface);display:flex;align-items:center;justify-content:center}.fly-people-picker__chip-avatar img{inline-size:100%;block-size:100%;object-fit:cover}.fly-people-picker__chip-initials{font-size:9px;font-weight:700;color:var(--_text-subtle);text-transform:uppercase}.fly-people-picker__chip-name{font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-people-picker__chip-remove{border:none;background:transparent;padding:2px;margin-inline-start:2px;border-radius:50%;color:var(--_text-subtle);font-size:10px;line-height:1;cursor:pointer;flex:none}.fly-people-picker__chip-remove:hover{background:color-mix(in srgb,var(--_danger) 18%,transparent);color:var(--_danger)}.fly-people-picker__change-btn{align-self:flex-start;border:1px solid var(--_border);border-radius:8px;background:var(--_surface);padding:5px 12px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-people-picker__change-btn:hover{background:var(--_surface-hover)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: FlyTypeaheadComponent, selector: "fly-typeahead", inputs: ["searchFn", "debounceMs", "placeholder", "ariaLabel", "value", "allowFreeText", "openOnFocus"], outputs: ["valueChange", "selected", "cleared"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
18526
18585
  }
18527
18586
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyPeoplePickerComponent, decorators: [{
18528
18587
  type: Component,
18529
- args: [{ selector: 'fly-people-picker', standalone: true, imports: [CommonModule, TranslatePipe, FlyTypeaheadComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'fly-people-picker' }, template: "@if (selected().length > 0) {\r\n <ul class=\"fly-people-picker__chips\" role=\"list\">\r\n @for (o of selected(); track o.id) {\r\n <li class=\"fly-people-picker__chip\" role=\"listitem\">\r\n <span class=\"fly-people-picker__chip-avatar\" aria-hidden=\"true\">\r\n @if (o.avatarUrl) {\r\n <img [src]=\"o.avatarUrl\" alt=\"\" />\r\n } @else {\r\n <span class=\"fly-people-picker__chip-initials\">{{ initialsFor(o.displayName) }}</span>\r\n }\r\n </span>\r\n <span class=\"fly-people-picker__chip-name\" [title]=\"o.email || o.displayName\">{{ o.displayName }}</span>\r\n <button\r\n type=\"button\"\r\n class=\"fly-people-picker__chip-remove\"\r\n [attr.aria-label]=\"'people_picker.remove' | translate: { name: o.displayName }\"\r\n (click)=\"remove(o.id)\">\r\n \u2715\r\n </button>\r\n </li>\r\n }\r\n </ul>\r\n}\r\n\r\n@if (showSearch()) {\r\n <fly-typeahead\r\n [searchFn]=\"typeaheadSearchFn\"\r\n [placeholder]=\"placeholder() || ('people_picker.search_placeholder' | translate)\"\r\n [allowFreeText]=\"false\"\r\n (selected)=\"onPicked($event)\" />\r\n} @else {\r\n <button type=\"button\" class=\"fly-people-picker__change-btn\" (click)=\"change()\">\r\n {{ 'people_picker.change' | translate }}\r\n </button>\r\n}\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-hover: var(--surface-hover, #f1f5f9);--_border: var(--separator, var(--surface-border, #e2e8f0));--_text: var(--label-primary, var(--text-color, #0f172a));--_text-subtle: var(--label-secondary, var(--text-color-secondary, #64748b));--_fill: var(--fill-tertiary, #f3f4f6);--_accent: var(--accent);--_danger: var(--system-red, #ef4444);--_radius: 999px;display:flex;flex-direction:column;gap:8px;inline-size:100%;color:var(--_text);font-size:13px}.fly-people-picker__chips{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px}.fly-people-picker__chip{display:inline-flex;align-items:center;gap:6px;padding-block:3px;padding-inline:3px 6px;border-radius:var(--_radius);background:var(--_fill);max-inline-size:220px}.fly-people-picker__chip-avatar{inline-size:22px;block-size:22px;border-radius:50%;overflow:hidden;flex:none;background:var(--_surface);display:flex;align-items:center;justify-content:center}.fly-people-picker__chip-avatar img{inline-size:100%;block-size:100%;object-fit:cover}.fly-people-picker__chip-initials{font-size:9px;font-weight:700;color:var(--_text-subtle);text-transform:uppercase}.fly-people-picker__chip-name{font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-people-picker__chip-remove{border:none;background:transparent;padding:2px;margin-inline-start:2px;border-radius:50%;color:var(--_text-subtle);font-size:10px;line-height:1;cursor:pointer;flex:none}.fly-people-picker__chip-remove:hover{background:color-mix(in srgb,var(--_danger) 18%,transparent);color:var(--_danger)}.fly-people-picker__change-btn{align-self:flex-start;border:1px solid var(--_border);border-radius:8px;background:var(--_surface);padding:5px 12px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-people-picker__change-btn:hover{background:var(--_surface-hover)}\n"] }]
18588
+ args: [{ selector: 'fly-people-picker', standalone: true, imports: [CommonModule, TranslatePipe, FlyTypeaheadComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'fly-people-picker' }, template: "@if (selected().length > 0) {\n <ul class=\"fly-people-picker__chips\" role=\"list\">\n @for (o of selected(); track o.id) {\n <li class=\"fly-people-picker__chip\" role=\"listitem\">\n <span class=\"fly-people-picker__chip-avatar\" aria-hidden=\"true\">\n @if (o.avatarUrl) {\n <img [src]=\"o.avatarUrl\" alt=\"\" />\n } @else {\n <span class=\"fly-people-picker__chip-initials\">{{ initialsFor(o.displayName) }}</span>\n }\n </span>\n <span class=\"fly-people-picker__chip-name\" [title]=\"o.email || o.displayName\">{{ o.displayName }}</span>\n <button\n type=\"button\"\n class=\"fly-people-picker__chip-remove\"\n [attr.aria-label]=\"'people_picker.remove' | translate: { name: o.displayName }\"\n (click)=\"remove(o.id)\">\n \u2715\n </button>\n </li>\n }\n </ul>\n}\n\n@if (showSearch()) {\n <fly-typeahead\n [searchFn]=\"typeaheadSearchFn\"\n [placeholder]=\"placeholder() || ('people_picker.search_placeholder' | translate)\"\n [allowFreeText]=\"false\"\n (selected)=\"onPicked($event)\" />\n} @else {\n <button type=\"button\" class=\"fly-people-picker__change-btn\" (click)=\"change()\">\n {{ 'people_picker.change' | translate }}\n </button>\n}\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-hover: var(--surface-hover, #f1f5f9);--_border: var(--separator, var(--surface-border, #e2e8f0));--_text: var(--label-primary, var(--text-color, #0f172a));--_text-subtle: var(--label-secondary, var(--text-color-secondary, #64748b));--_fill: var(--fill-tertiary, #f3f4f6);--_accent: var(--accent);--_danger: var(--system-red, #ef4444);--_radius: 999px;display:flex;flex-direction:column;gap:8px;inline-size:100%;color:var(--_text);font-size:13px}.fly-people-picker__chips{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px}.fly-people-picker__chip{display:inline-flex;align-items:center;gap:6px;padding-block:3px;padding-inline:3px 6px;border-radius:var(--_radius);background:var(--_fill);max-inline-size:220px}.fly-people-picker__chip-avatar{inline-size:22px;block-size:22px;border-radius:50%;overflow:hidden;flex:none;background:var(--_surface);display:flex;align-items:center;justify-content:center}.fly-people-picker__chip-avatar img{inline-size:100%;block-size:100%;object-fit:cover}.fly-people-picker__chip-initials{font-size:9px;font-weight:700;color:var(--_text-subtle);text-transform:uppercase}.fly-people-picker__chip-name{font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-people-picker__chip-remove{border:none;background:transparent;padding:2px;margin-inline-start:2px;border-radius:50%;color:var(--_text-subtle);font-size:10px;line-height:1;cursor:pointer;flex:none}.fly-people-picker__chip-remove:hover{background:color-mix(in srgb,var(--_danger) 18%,transparent);color:var(--_danger)}.fly-people-picker__change-btn{align-self:flex-start;border:1px solid var(--_border);border-radius:8px;background:var(--_surface);padding:5px 12px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-people-picker__change-btn:hover{background:var(--_surface-hover)}\n"] }]
18530
18589
  }], ctorParameters: () => [], propDecorators: { searchFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchFn", required: true }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], excludeIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "excludeIds", required: false }] }], initialSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialSelection", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], typeahead: [{
18531
18590
  type: ViewChild,
18532
18591
  args: [FlyTypeaheadComponent]
@@ -18618,6 +18677,21 @@ function unwrapCurrencies(res) {
18618
18677
  * i18n is self-sufficient through the `currency_selector.*` keys in `DS_BASELINE_LOCALES`
18619
18678
  * (en/ar/fr/ur); RTL works via logical CSS.
18620
18679
  *
18680
+ * ## `locked`, vs `disabled`
18681
+ * `disabled` is UI convention for "not applicable right now" — greyed out, no explanation,
18682
+ * because none is owed (a form section that only exists once a prior step completes, say).
18683
+ * `locked` is a different claim entirely: **the value is fixed on purpose**, because
18684
+ * something downstream now depends on it (PPM freezes a project's currency the moment any
18685
+ * financial row exists — changing it would silently re-denominate every stored amount).
18686
+ * A dimmed control with no explanation is exactly the failure `skills/magic-bar-actions.md`
18687
+ * §3.6 names for actions ("a vanishing action teaches the user nothing"); the same argument
18688
+ * applies to a frozen field. So `locked` renders differently on purpose: full-opacity (never
18689
+ * dimmed — the value is not "unavailable", it is authoritative), no dropdown affordance at
18690
+ * all, and an always-visible reason caption instead of a hover-only tooltip, so the "why" is
18691
+ * legible to a screen reader without requiring focus and to a sighted user without hovering.
18692
+ * `locked` takes precedence when both are set — it is the more specific state and the
18693
+ * `disabled` trigger markup (with its dropdown affordances) never renders underneath it.
18694
+ *
18621
18695
  * @example
18622
18696
  * ```html
18623
18697
  * <!-- Loads /api/currencies/brief itself: -->
@@ -18628,6 +18702,12 @@ function unwrapCurrencies(res) {
18628
18702
  * mode="multi"
18629
18703
  * [allowedCodes]="tenantCurrencies()"
18630
18704
  * (selectionDetailChange)="onCurrenciesPicked($event)" />
18705
+ *
18706
+ * <!-- Frozen once the project has financial rows — reason is an i18n KEY, never text: -->
18707
+ * <fly-currency-selector
18708
+ * [(ngModel)]="project.currency"
18709
+ * [locked]="project.hasFinancialRows"
18710
+ * lockedReasonKey="projects.currency_locked_reason" />
18631
18711
  * ```
18632
18712
  */
18633
18713
  class FlyCurrencySelectorComponent {
@@ -18650,6 +18730,24 @@ class FlyCurrencySelectorComponent {
18650
18730
  pinnedCodes = input([], ...(ngDevMode ? [{ debugName: "pinnedCodes" }] : /* istanbul ignore next */ []));
18651
18731
  /** Disable the whole control (also driven by reactive-forms `setDisabledState`). */
18652
18732
  disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
18733
+ /**
18734
+ * Freeze the current selection because something downstream now depends on it — a
18735
+ * DIFFERENT claim than `disabled`. See the class doc's "`locked`, vs `disabled`"
18736
+ * section. Renders the current pick as a plain, non-interactive readout (no dropdown
18737
+ * affordance at all) plus an always-visible reason caption — never a dimmed clickable-
18738
+ * looking control. Takes precedence over `disabled` when both are set.
18739
+ */
18740
+ locked = input(false, ...(ngDevMode ? [{ debugName: "locked" }] : /* istanbul ignore next */ []));
18741
+ /**
18742
+ * i18n KEY (never resolved text — see `skills/magic-bar-actions.md` §3.4) explaining
18743
+ * WHY the control is locked. Omit to use the localized `currency_selector.locked_default_reason`
18744
+ * baseline key; supply your own only when that default reason is wrong for your case
18745
+ * (mirrors `MagicBarActionSpec.disabledTipKey`'s "supply only when the default is wrong"
18746
+ * contract). Ignored while `locked` is `false`.
18747
+ */
18748
+ lockedReasonKey = input(null, ...(ngDevMode ? [{ debugName: "lockedReasonKey" }] : /* istanbul ignore next */ []));
18749
+ /** `I18nService.t()` params for `lockedReasonKey`, e.g. `{ date: frozenOn }`. */
18750
+ lockedReasonParams = input(undefined, ...(ngDevMode ? [{ debugName: "lockedReasonParams" }] : /* istanbul ignore next */ []));
18653
18751
  /** Show the trigger clear (✕) affordance when there is a selection. */
18654
18752
  clearable = input(true, ...(ngDevMode ? [{ debugName: "clearable" }] : /* istanbul ignore next */ []));
18655
18753
  /** Trigger text when nothing is picked. Omit for the localized default. */
@@ -18669,6 +18767,8 @@ class FlyCurrencySelectorComponent {
18669
18767
  _uid = ++_flyCurrencySelectorUid;
18670
18768
  listboxId = `fly-currency-selector-${this._uid}-listbox`;
18671
18769
  triggerId = `fly-currency-selector-${this._uid}-trigger`;
18770
+ /** id of the always-rendered locked-reason caption; `aria-describedby` target of the locked readout. */
18771
+ lockedReasonId = `fly-currency-selector-${this._uid}-locked-reason`;
18672
18772
  isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
18673
18773
  searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : /* istanbul ignore next */ []));
18674
18774
  activeIndex = signal(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
@@ -18709,6 +18809,8 @@ class FlyCurrencySelectorComponent {
18709
18809
  clearText = computed(() => this._i18n.t('currency_selector.clear'), ...(ngDevMode ? [{ debugName: "clearText" }] : /* istanbul ignore next */ []));
18710
18810
  pinnedGroupText = computed(() => this._i18n.t('currency_selector.pinned'), ...(ngDevMode ? [{ debugName: "pinnedGroupText" }] : /* istanbul ignore next */ []));
18711
18811
  allGroupText = computed(() => this._i18n.t('currency_selector.all'), ...(ngDevMode ? [{ debugName: "allGroupText" }] : /* istanbul ignore next */ []));
18812
+ /** Resolved locked-reason text — the default baseline key, or the host's `lockedReasonKey`. */
18813
+ lockedReasonText = computed(() => this._i18n.t(this.lockedReasonKey() ?? 'currency_selector.locked_default_reason', this.lockedReasonParams()), ...(ngDevMode ? [{ debugName: "lockedReasonText" }] : /* istanbul ignore next */ []));
18712
18814
  /** Every offered row, after the `allowedCodes` restriction. */
18713
18815
  available = computed(() => {
18714
18816
  const rows = this.currencies() ?? this._loaded();
@@ -18788,7 +18890,7 @@ class FlyCurrencySelectorComponent {
18788
18890
  }
18789
18891
  // ── Open / close ───────────────────────────────────────────────────────────
18790
18892
  toggleOpen() {
18791
- if (this.effectiveDisabled())
18893
+ if (this.effectiveDisabled() || this.locked())
18792
18894
  return;
18793
18895
  if (this.isOpen())
18794
18896
  this.close();
@@ -18796,7 +18898,7 @@ class FlyCurrencySelectorComponent {
18796
18898
  this.open();
18797
18899
  }
18798
18900
  open() {
18799
- if (this.effectiveDisabled() || this.isOpen())
18901
+ if (this.effectiveDisabled() || this.locked() || this.isOpen())
18800
18902
  return;
18801
18903
  this.isOpen.set(true);
18802
18904
  this.activeIndex.set(0);
@@ -18815,7 +18917,7 @@ class FlyCurrencySelectorComponent {
18815
18917
  }
18816
18918
  // ── Selection ──────────────────────────────────────────────────────────────
18817
18919
  pick(currency) {
18818
- if (this.effectiveDisabled())
18920
+ if (this.effectiveDisabled() || this.locked())
18819
18921
  return;
18820
18922
  if (!this.isMulti()) {
18821
18923
  this._commit([currency.code]);
@@ -18830,13 +18932,13 @@ class FlyCurrencySelectorComponent {
18830
18932
  }
18831
18933
  remove(code, event) {
18832
18934
  event?.stopPropagation();
18833
- if (this.effectiveDisabled())
18935
+ if (this.effectiveDisabled() || this.locked())
18834
18936
  return;
18835
18937
  this._commit(this._selectedCodes().filter((c) => c.toUpperCase() !== code.toUpperCase()));
18836
18938
  }
18837
18939
  clear(event) {
18838
18940
  event?.stopPropagation();
18839
- if (this.effectiveDisabled())
18941
+ if (this.effectiveDisabled() || this.locked())
18840
18942
  return;
18841
18943
  this._commit([]);
18842
18944
  }
@@ -18949,13 +19051,13 @@ class FlyCurrencySelectorComponent {
18949
19051
  this._onTouched();
18950
19052
  }
18951
19053
  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: [
19054
+ 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
19055
  {
18954
19056
  provide: NG_VALUE_ACCESSOR,
18955
19057
  useExisting: forwardRef(() => FlyCurrencySelectorComponent),
18956
19058
  multi: true,
18957
19059
  },
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 });
19060
+ ], 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
19061
  }
18960
19062
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyCurrencySelectorComponent, decorators: [{
18961
19063
  type: Component,
@@ -18969,8 +19071,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
18969
19071
  class: 'fly-currency-selector',
18970
19072
  '[class.fly-currency-selector--open]': 'isOpen()',
18971
19073
  '[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: [{
19074
+ '[class.fly-currency-selector--locked]': 'locked()',
19075
+ }, 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"] }]
19076
+ }], 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
19077
  type: ViewChild,
18975
19078
  args: ['searchRef']
18976
19079
  }], triggerEl: [{
@@ -18979,159 +19082,587 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
18979
19082
  }] } });
18980
19083
 
18981
19084
  /**
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.
19085
+ * Locale-aware formatting primitivesnumbers, byte sizes, relative time, and dates.
19001
19086
  *
19002
- * @example
19003
- * ```ts
19004
- * export class MyDrawer {
19005
- * private handle: OverlayHandle | null = null;
19087
+ * All of these are `Intl`-backed and take an explicit `locale`, which is the whole
19088
+ * point: the estate is full of hand-rolled formatters that hardcode ASCII digits,
19089
+ * English unit strings ("1.4 MB", "5m ago"), or the *browser's* default locale rather
19090
+ * than the app's selected language. Under `ar` / `ur` those render wrong — and i18n is
19091
+ * mandatory on this platform, so this is a correctness surface, not a convenience one.
19006
19092
  *
19007
- * open() { this.handle = overlayStack.push(); }
19008
- * close() { this.handle = overlayStack.remove(this.handle); }
19093
+ * Pure functions here; the `| flyCompact`-style pipes in `format.pipes.ts` wrap them
19094
+ * and default the locale to the active {@link I18nService} language.
19009
19095
  *
19010
- * onEscape() { if (overlayStack.isTop(this.handle)) this.close(); }
19011
- * }
19012
- * ```
19096
+ * ## The em-dash convention
19097
+ * Every formatter returns `'—'` (U+2014) for null / undefined / non-finite input rather
19098
+ * than throwing, `'NaN'`, or an empty string. A visible placeholder keeps table columns
19099
+ * aligned and makes "no value" legible; an empty string reads as a rendering bug.
19013
19100
  */
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);
19101
+ /** Rendered for null / undefined / non-finite input across every formatter here. */
19102
+ const FLY_EMPTY_VALUE = '—';
19103
+ // ─── Numbers ─────────────────────────────────────────────────────────────────
19104
+ /** At or above this magnitude, switch from grouped-exact to compact (K/M/B) notation. */
19105
+ const FLY_COMPACT_THRESHOLD = 10_000;
19106
+ // Intl formatters are expensive to construct relative to how often these are called in
19107
+ // a table cell or chart label; cache one per distinct configuration.
19108
+ const compactCache = new Map();
19109
+ const integerCache = new Map();
19110
+ const decimalCache = new Map();
19111
+ function compactFormatter(locale) {
19112
+ let f = compactCache.get(locale);
19113
+ if (!f) {
19114
+ f = new Intl.NumberFormat(locale, { notation: 'compact', maximumFractionDigits: 1 });
19115
+ compactCache.set(locale, f);
19042
19116
  }
19043
- /** Number of currently-open overlays. Useful for scroll-lock refcounting. */
19044
- get depth() {
19045
- return this.stack.length;
19117
+ return f;
19118
+ }
19119
+ function integerFormatter(locale) {
19120
+ let f = integerCache.get(locale);
19121
+ if (!f) {
19122
+ f = new Intl.NumberFormat(locale, { maximumFractionDigits: 0 });
19123
+ integerCache.set(locale, f);
19046
19124
  }
19125
+ return f;
19047
19126
  }
19048
19127
  /**
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;
19128
+ * Compact, scannable form grouped-exact below {@link FLY_COMPACT_THRESHOLD},
19129
+ * unit-compacted above it.
19067
19130
  *
19068
- * open() { this.restore = captureFocus(); }
19069
- * close() { this.restore = restoreFocus(this.restore); }
19070
19131
  * ```
19132
+ * 6 042 → "6,042" (exact; small buckets read best as real numbers)
19133
+ * 12 400 → "12.4K"
19134
+ * 1 900 787 → "1.9M"
19135
+ * 2 300 000 000 → "2.3B"
19136
+ * ```
19137
+ *
19138
+ * Pair with {@link flyFullNumber} in a tooltip or `aria-label` so the exact figure is
19139
+ * always one hover away — compaction is a display affordance, not data loss.
19071
19140
  */
19141
+ function flyCompactNumber(value, locale = 'en') {
19142
+ if (value == null || !Number.isFinite(value))
19143
+ return FLY_EMPTY_VALUE;
19144
+ return Math.abs(value) >= FLY_COMPACT_THRESHOLD
19145
+ ? compactFormatter(locale).format(value)
19146
+ : integerFormatter(locale).format(value);
19147
+ }
19148
+ /** Exact, fully-grouped integer form for tooltips / a11y (e.g. `"1,900,787"`). */
19149
+ function flyFullNumber(value, locale = 'en') {
19150
+ if (value == null || !Number.isFinite(value))
19151
+ return FLY_EMPTY_VALUE;
19152
+ return integerFormatter(locale).format(value);
19153
+ }
19072
19154
  /**
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.
19155
+ * Fractional form for ratios where the decimal *is* the signal (avg depth 1.5,
19156
+ * sparsity 0.25) — distinct from {@link flyFullNumber}, which floors to integers.
19157
+ * Trailing zeros drop.
19077
19158
  */
19078
- function captureFocus(doc = document) {
19079
- const active = doc.activeElement;
19080
- const element = active instanceof HTMLElement && active !== doc.body ? active : null;
19081
- return { element };
19159
+ function flyDecimalNumber(value, locale = 'en', maxFractionDigits = 2) {
19160
+ if (value == null || !Number.isFinite(value))
19161
+ return FLY_EMPTY_VALUE;
19162
+ const key = `${locale}|${maxFractionDigits}`;
19163
+ let f = decimalCache.get(key);
19164
+ if (!f) {
19165
+ f = new Intl.NumberFormat(locale, { maximumFractionDigits: maxFractionDigits });
19166
+ decimalCache.set(key, f);
19167
+ }
19168
+ return f.format(value);
19082
19169
  }
19083
19170
  /**
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.
19171
+ * Signed compact delta for "change since last poll" chips (`"+12.4K"`, `"−340"`).
19172
+ * Returns an empty string for zero / non-finite so a no-change chip renders nothing
19173
+ * rather than a meaningless "0".
19096
19174
  *
19097
- * Returns `null` so callers can clear their field in one statement.
19175
+ * Uses U+2212 MINUS SIGN, not a hyphen it matches the plus glyph's width and weight,
19176
+ * so a column of deltas stays visually aligned.
19098
19177
  */
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;
19178
+ function flySignedCompact(delta, locale = 'en') {
19179
+ if (!Number.isFinite(delta) || delta === 0)
19180
+ return '';
19181
+ const sign = delta > 0 ? '+' : '−';
19182
+ return sign + flyCompactNumber(Math.abs(delta), locale);
19107
19183
  }
19108
-
19184
+ // ─── Byte sizes ──────────────────────────────────────────────────────────────
19185
+ /** Binary unit ladder. Byte sizes are conventionally base-1024 in file UIs. */
19186
+ const BYTE_UNITS = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte'];
19187
+ const byteCache = new Map();
19109
19188
  /**
19110
- * Debounce primitives the shared replacement for the `setTimeout` / `clearTimeout`
19111
- * pairs hand-rolled in every list screen and typeahead across the estate.
19189
+ * Human-readable byte size (`"482 bytes"`, `"1.4 MB"`, `"2.3 GB"`), localized.
19112
19190
  *
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.
19191
+ * Base-1024 with a log-derived unit pick, clamped at TB so a bogus huge value degrades
19192
+ * to a large TB figure instead of overflowing the ladder. Trailing zeros drop
19193
+ * (`1.0 MB` `1 MB`).
19118
19194
  *
19119
- * ## Why not `debounceTime` from RxJS
19120
- * Nothing wrong with it when the input is already a stream. But the common case here
19121
- * is a signal-based component with an `(input)` handler and no Subject in sight, and
19122
- * standing up a `Subject` + `takeUntilDestroyed` + `subscribe` to debounce one field is
19123
- * more machinery than the problem deserves. These helpers are the imperative
19124
- * equivalent, with the teardown correctness that hand-rolled timers usually miss.
19195
+ * Localization matters here and is the reason this supersedes the eight hand-rolled
19196
+ * copies in the estate: those concatenate hardcoded English unit strings, so an Arabic
19197
+ * user saw Latin "MB" beside Arabic-Indic digits. `Intl` unit formatting renders both
19198
+ * the number and the unit in the active locale (French even gets "octets").
19125
19199
  *
19126
- * ## The leak these fix
19127
- * A bare `setTimeout(() => this.reload(), 150)` with no `clearTimeout` in `ngOnDestroy`
19128
- * still fires after the component is gonereading destroyed signals, calling a
19129
- * service on a torn-down injector, or logging a spurious HTTP request. {@link FlyDebouncer}
19130
- * makes cancellation available and {@link flyDebounced} makes it automatic.
19131
- */
19132
- /** Default settle delay for search-style inputs, in milliseconds. */
19133
- const FLY_SEARCH_DEBOUNCE_MS = 300;
19134
- /** Default settle delay for re-querying a list after a filter/sort change. */
19200
+ * Note the deliberate convention mismatch: the maths is base-1024 while the rendered
19201
+ * symbols are the SI ones ("kB", "MB"), so 1024 bytes shows as "1 kB" rather than the
19202
+ * pedantically-correct "1 KiB". Every mainstream file UIWindows Explorer, Finder
19203
+ * does exactly this, and `Intl` has no binary-prefix units, so matching user
19204
+ * expectation beats matching the standard here.
19205
+ */
19206
+ function flyFormatBytes(bytes, locale = 'en') {
19207
+ if (bytes == null || !Number.isFinite(bytes))
19208
+ return FLY_EMPTY_VALUE;
19209
+ if (bytes <= 0)
19210
+ return formatByteValue(0, 'byte', locale);
19211
+ const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), BYTE_UNITS.length - 1);
19212
+ const value = bytes / Math.pow(1024, index);
19213
+ // Bytes are whole things — never render "482.3 B".
19214
+ const rounded = index === 0 ? Math.round(value) : parseFloat(value.toFixed(1));
19215
+ return formatByteValue(rounded, BYTE_UNITS[index], locale);
19216
+ }
19217
+ function formatByteValue(value, unit, locale) {
19218
+ const key = `${locale}|${unit}`;
19219
+ let f = byteCache.get(key);
19220
+ if (!f) {
19221
+ f = new Intl.NumberFormat(locale, {
19222
+ style: 'unit',
19223
+ unit,
19224
+ // Raw bytes read best spelled out and pluralized ("482 bytes", "1 byte" —
19225
+ // and correctly "482 octets" in French). The larger units are universally
19226
+ // recognised as symbols, where the spelled-out form ("1.4 megabytes") would
19227
+ // be noise in a file list.
19228
+ unitDisplay: unit === 'byte' ? 'long' : 'short',
19229
+ maximumFractionDigits: 1,
19230
+ });
19231
+ byteCache.set(key, f);
19232
+ }
19233
+ return f.format(value);
19234
+ }
19235
+ // ─── Relative time ───────────────────────────────────────────────────────────
19236
+ /**
19237
+ * Formats an instant as a localized relative age — `"2 minutes ago"` /
19238
+ * `"منذ دقيقتين"` / `"il y a 2 minutes"`. Works for future instants too
19239
+ * (`"in 3 days"`), which is what makes it usable for due dates and SLA countdowns.
19240
+ *
19241
+ * `now` is injectable so tests can pin the clock instead of sleeping or accepting
19242
+ * flake around bucket boundaries.
19243
+ */
19244
+ function flyRelativeTime(value, locale = 'en', now = new Date()) {
19245
+ if (value == null || value === '')
19246
+ return FLY_EMPTY_VALUE;
19247
+ const then = value instanceof Date ? value : new Date(value);
19248
+ const ms = then.getTime();
19249
+ if (Number.isNaN(ms))
19250
+ return FLY_EMPTY_VALUE;
19251
+ const diffMs = ms - now.getTime();
19252
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
19253
+ const absSec = Math.abs(diffMs) / 1000;
19254
+ // `numeric: 'auto'` is what yields "yesterday" / "now" instead of a stiff
19255
+ // "1 day ago" / "0 seconds ago" at the bucket edges.
19256
+ if (absSec < 60)
19257
+ return rtf.format(Math.round(diffMs / 1000), 'second');
19258
+ if (absSec < 3600)
19259
+ return rtf.format(Math.round(diffMs / 60_000), 'minute');
19260
+ if (absSec < 86_400)
19261
+ return rtf.format(Math.round(diffMs / 3_600_000), 'hour');
19262
+ if (absSec < 2_592_000)
19263
+ return rtf.format(Math.round(diffMs / 86_400_000), 'day');
19264
+ if (absSec < 31_536_000)
19265
+ return rtf.format(Math.round(diffMs / 2_592_000_000), 'month');
19266
+ return rtf.format(Math.round(diffMs / 31_536_000_000), 'year');
19267
+ }
19268
+ /**
19269
+ * Formats a duration in seconds as a localized short unit string — `420` → `"7 min"`
19270
+ * (en) / `"7 د"` (ar). Picks seconds / minutes / hours by magnitude.
19271
+ */
19272
+ function flyDuration(seconds, locale = 'en') {
19273
+ if (seconds == null || !Number.isFinite(seconds) || seconds < 0) {
19274
+ return FLY_EMPTY_VALUE;
19275
+ }
19276
+ const fmt = (unit, v) => new Intl.NumberFormat(locale, {
19277
+ style: 'unit',
19278
+ unit,
19279
+ unitDisplay: 'narrow',
19280
+ maximumFractionDigits: 0,
19281
+ }).format(v);
19282
+ if (seconds < 60)
19283
+ return fmt('second', seconds);
19284
+ if (seconds < 3600)
19285
+ return fmt('minute', seconds / 60);
19286
+ return fmt('hour', seconds / 3600);
19287
+ }
19288
+ // ─── Dates ───────────────────────────────────────────────────────────────────
19289
+ /**
19290
+ * Localized calendar date (no time). Pass `options` to override the default
19291
+ * short-date presentation.
19292
+ *
19293
+ * Unlike the bare `toLocaleDateString()` calls it replaces, this takes an explicit
19294
+ * locale — those used the *browser's* locale and so ignored the app's language setting
19295
+ * entirely. Invalid input degrades to the em dash rather than `"Invalid Date"`.
19296
+ */
19297
+ function flyFormatDate(value, locale = 'en', options = { dateStyle: 'medium' }) {
19298
+ const date = toValidDate(value);
19299
+ if (!date)
19300
+ return FLY_EMPTY_VALUE;
19301
+ try {
19302
+ return new Intl.DateTimeFormat(locale, options).format(date);
19303
+ }
19304
+ catch {
19305
+ // A malformed `options` object (or an unsupported locale extension) throws;
19306
+ // degrade to the ISO date rather than taking the view down.
19307
+ return date.toISOString().slice(0, 10);
19308
+ }
19309
+ }
19310
+ /** Localized time of day (no date). */
19311
+ function flyFormatTime(value, locale = 'en', options = { timeStyle: 'short' }) {
19312
+ return flyFormatDate(value, locale, options);
19313
+ }
19314
+ /** Localized date + time — the tooltip companion to a relative or short-date cell. */
19315
+ function flyFormatDateTime(value, locale = 'en', options = { dateStyle: 'medium', timeStyle: 'short' }) {
19316
+ return flyFormatDate(value, locale, options);
19317
+ }
19318
+ /**
19319
+ * Calendar date as `yyyy-MM-dd` — the wire/sort form, deliberately NOT localized.
19320
+ *
19321
+ * Use for `<input type="date">` values, query params, and sort keys. For anything a
19322
+ * user reads, use {@link flyFormatDate}.
19323
+ *
19324
+ * Derived from the instant's **UTC** date so the value is stable across timezones —
19325
+ * a local-date derivation shifts the day for users east/west of the source data.
19326
+ */
19327
+ function flyToDateOnly(value) {
19328
+ const date = toValidDate(value);
19329
+ return date ? date.toISOString().slice(0, 10) : '';
19330
+ }
19331
+ function toValidDate(value) {
19332
+ if (value == null || value === '')
19333
+ return null;
19334
+ const date = value instanceof Date ? value : new Date(value);
19335
+ return Number.isNaN(date.getTime()) ? null : date;
19336
+ }
19337
+
19338
+ /**
19339
+ * ISO 4217 minor-unit EXCEPTIONS, used ONLY when `currency` is a bare code string with
19340
+ * no matching `FlyCurrency` row to read `decimalDigits` from. This is deliberately a
19341
+ * PARTIAL table — it mirrors exactly the two exception groups
19342
+ * {@link FlyCurrency.decimalDigits}'s own doc comment names (0-decimal: JPY, KRW, the
19343
+ * CFA francs; 3-decimal: the seven Gulf/MENA dinars/rials), not full ISO 4217 coverage.
19344
+ * Every code outside these two sets defaults to 2, which is correct for the ISO 4217
19345
+ * majority.
19346
+ *
19347
+ * This is the "explicit, documented fallback" D-B7-2 requires in place of a silent
19348
+ * "assume 2" — but it is still a fallback, not a substitute for the real data. A caller
19349
+ * that needs certainty for every currency (there are 0-decimal codes beyond the three
19350
+ * listed here) should pass the `FlyCurrency` row instead of a bare code:
19351
+ * `fly-currency-selector`'s `(selectionDetailChange)` emits the full row precisely so a
19352
+ * host never has to guess.
19353
+ */
19354
+ const ZERO_DECIMAL_FALLBACK_CODES = new Set(['JPY', 'KRW', 'XAF', 'XOF', 'XPF']);
19355
+ const THREE_DECIMAL_FALLBACK_CODES = new Set([
19356
+ 'BHD',
19357
+ 'IQD',
19358
+ 'JOD',
19359
+ 'KWD',
19360
+ 'LYD',
19361
+ 'OMR',
19362
+ 'TND',
19363
+ ]);
19364
+ function fallbackDecimalDigits(code) {
19365
+ if (ZERO_DECIMAL_FALLBACK_CODES.has(code))
19366
+ return 0;
19367
+ if (THREE_DECIMAL_FALLBACK_CODES.has(code))
19368
+ return 3;
19369
+ return 2;
19370
+ }
19371
+ /** Accepts a `FlyCurrency` row or a bare ISO 4217 code string; `null`/`undefined`/blank resolves to `null`. */
19372
+ function resolveCurrencyMeta(currency) {
19373
+ if (currency && typeof currency === 'object') {
19374
+ const digits = Number.isInteger(currency.decimalDigits) && currency.decimalDigits >= 0
19375
+ ? currency.decimalDigits
19376
+ : 2; // Defensive only — a malformed row should still render something rather than throw.
19377
+ return { code: currency.code.trim().toUpperCase(), symbol: currency.symbol ?? null, decimalDigits: digits };
19378
+ }
19379
+ if (typeof currency === 'string' && currency.trim()) {
19380
+ const code = currency.trim().toUpperCase();
19381
+ return { code, symbol: null, decimalDigits: fallbackDecimalDigits(code) };
19382
+ }
19383
+ return null;
19384
+ }
19385
+ /** The text substituted for Intl's `currency` part, per `display`. `null` removes it entirely. */
19386
+ function displayLabel(meta, display) {
19387
+ if (display === 'none')
19388
+ return null;
19389
+ if (display === 'code')
19390
+ return meta.code;
19391
+ // 'symbol' — degrade to the code when no symbol is known (the bare-code path), never
19392
+ // an Intl-guessed symbol that might not match the platform's own catalogue.
19393
+ return meta.symbol && meta.symbol.trim() ? meta.symbol : meta.code;
19394
+ }
19395
+ // Intl.NumberFormat construction is non-trivial; cache one per distinct (locale, code, digits).
19396
+ const moneyFormatterCache = new Map();
19397
+ /**
19398
+ * Returns `null` when `code` is not well-formed enough for `Intl` to accept as a
19399
+ * `currency` option (e.g. a malformed catalogue row) — the caller degrades to
19400
+ * {@link manualFormat} rather than letting a `RangeError` take the view down.
19401
+ */
19402
+ function currencyFormatter(locale, code, digits) {
19403
+ const key = `${locale}|${code}|${digits}`;
19404
+ const cached = moneyFormatterCache.get(key);
19405
+ if (cached)
19406
+ return cached;
19407
+ try {
19408
+ const f = new Intl.NumberFormat(locale, {
19409
+ style: 'currency',
19410
+ currency: code,
19411
+ // Always 'code', regardless of the caller's `display` option — see the module
19412
+ // doc comment on why this is the structural template rather than 'symbol'.
19413
+ currencyDisplay: 'code',
19414
+ minimumFractionDigits: digits,
19415
+ maximumFractionDigits: digits,
19416
+ });
19417
+ moneyFormatterCache.set(key, f);
19418
+ return f;
19419
+ }
19420
+ catch {
19421
+ return null;
19422
+ }
19423
+ }
19424
+ const plainDecimalCache = new Map();
19425
+ function plainDecimalFormatter(locale, digits) {
19426
+ const key = `${locale}|${digits}`;
19427
+ let f = plainDecimalCache.get(key);
19428
+ if (!f) {
19429
+ f = new Intl.NumberFormat(locale, { minimumFractionDigits: digits, maximumFractionDigits: digits });
19430
+ plainDecimalCache.set(key, f);
19431
+ }
19432
+ return f;
19433
+ }
19434
+ /** Manual `"<label> <number>"` construction for the rare case Intl rejects the code outright. */
19435
+ function manualFormat(amount, meta, display, locale) {
19436
+ const number = plainDecimalFormatter(locale, meta.decimalDigits).format(amount);
19437
+ const label = displayLabel(meta, display);
19438
+ return label ? `${label} ${number}` : number;
19439
+ }
19440
+ /**
19441
+ * Formats `amount` in `currency`, localized to `options.locale`.
19442
+ *
19443
+ * ```ts
19444
+ * flyFormatMoney(1234.5, kwdRow) // "د.ك 1,234.500" (KWD is 3-decimal)
19445
+ * flyFormatMoney(1234, 'JPY') // "JPY 1,234" (bare code, no symbol to read — degrades to the code)
19446
+ * flyFormatMoney(-42, usdRow, { display: 'code' }) // "-USD 42.00"
19447
+ * flyFormatMoney(99.9, eurRow, { display: 'none' }) // "99.90" (currency named elsewhere on screen)
19448
+ * flyFormatMoney(null, usdRow) // "—" (never "NaN")
19449
+ * ```
19450
+ *
19451
+ * `amount == null` or non-finite (`NaN`, `Infinity`), or an unresolvable `currency`,
19452
+ * renders {@link FLY_EMPTY_VALUE} — the same placeholder every other `format.ts`
19453
+ * primitive uses, so a money cell in a mixed table degrades exactly like its neighbours
19454
+ * instead of introducing a second "no value" convention.
19455
+ */
19456
+ function flyFormatMoney(amount, currency, options = {}) {
19457
+ if (amount == null || !Number.isFinite(amount))
19458
+ return FLY_EMPTY_VALUE;
19459
+ const meta = resolveCurrencyMeta(currency);
19460
+ if (!meta)
19461
+ return FLY_EMPTY_VALUE;
19462
+ const { display = 'symbol', locale = 'en' } = options;
19463
+ const formatter = currencyFormatter(locale, meta.code, meta.decimalDigits);
19464
+ if (!formatter)
19465
+ return manualFormat(amount, meta, display, locale);
19466
+ const label = displayLabel(meta, display);
19467
+ return formatter
19468
+ .formatToParts(amount)
19469
+ .map((part) => (part.type === 'currency' ? (label ?? '') : part.value))
19470
+ // Removing the currency part (display: 'none') leaves its adjacent literal
19471
+ // separator behind (Intl emits currency+space as two parts) — collapse and trim
19472
+ // rather than special-casing every locale's separator placement.
19473
+ .join('')
19474
+ .replace(/\s+/g, ' ')
19475
+ .trim();
19476
+ }
19477
+
19478
+ /**
19479
+ * `{{ amount | flyMoney: currencyRow }}` → `"$1,234.50"` / `{{ amount | flyMoney: 'KWD' }}`
19480
+ * → `"KD 1,234.500"`.
19481
+ *
19482
+ * Template wrapper over {@link flyFormatMoney}, defaulting the locale to the active
19483
+ * {@link I18nService} language the same way every other `Fly*Pipe` in `format.pipes.ts`
19484
+ * does — see that file's doc comment for the "pass the locale signal explicitly in a
19485
+ * view that must react live to the language switcher" caveat, which applies here too.
19486
+ *
19487
+ * A separate file from `format.pipes.ts` on purpose: this pipe's `currency` argument
19488
+ * depends on `FlyCurrency` (the currency-selector's data contract), a domain type the
19489
+ * pure number/byte/date formatters in `format.pipes.ts` have no reason to import.
19490
+ *
19491
+ * ```html
19492
+ * {{ invoice.total | flyMoney: invoice.currency }}
19493
+ * {{ invoice.total | flyMoney: invoice.currency : { display: 'code' } }}
19494
+ * ```
19495
+ */
19496
+ class FlyMoneyPipe {
19497
+ i18n = inject(I18nService);
19498
+ transform(amount, currency, options) {
19499
+ return flyFormatMoney(amount, currency, {
19500
+ display: options?.display,
19501
+ locale: options?.locale ?? this.i18n.locale(),
19502
+ });
19503
+ }
19504
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyMoneyPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
19505
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.19", ngImport: i0, type: FlyMoneyPipe, isStandalone: true, name: "flyMoney" });
19506
+ }
19507
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyMoneyPipe, decorators: [{
19508
+ type: Pipe,
19509
+ args: [{ name: 'flyMoney', standalone: true }]
19510
+ }] });
19511
+
19512
+ /**
19513
+ * Document-wide open-overlay stack — Escape arbitration for stacked overlays.
19514
+ *
19515
+ * Every layered overlay (drawer, modal, confirm dialog, menu, popover) listens for
19516
+ * Escape at the document level, so without arbitration one keypress closes the whole
19517
+ * pile — a confirm dialog opened over a drawer takes the drawer down with it on the
19518
+ * first Escape. Each overlay pushes a handle when it opens and removes it when it
19519
+ * closes or is destroyed; its Escape handler acts only while its handle is
19520
+ * top-of-stack. First Escape then closes the confirm, second the drawer.
19521
+ *
19522
+ * The stack is pure (no DOM, no DI) so the ordering rules are unit-testable, and the
19523
+ * shared {@link overlayStack} singleton spans the whole document — which is the point:
19524
+ * independently-owned overlays, including ones in different federated remotes, must
19525
+ * arbitrate globally or they cannot know about each other.
19526
+ *
19527
+ * ## Federation
19528
+ * Native Federation shares this package as a singleton, so every app in the shell
19529
+ * binds to one stack instance. A remote that forks its own DS copy would get its own
19530
+ * stack and lose arbitration against shell overlays — that is what the package's
19531
+ * federation singleton guard exists to catch.
19532
+ *
19533
+ * @example
19534
+ * ```ts
19535
+ * export class MyDrawer {
19536
+ * private handle: OverlayHandle | null = null;
19537
+ *
19538
+ * open() { this.handle = overlayStack.push(); }
19539
+ * close() { this.handle = overlayStack.remove(this.handle); }
19540
+ *
19541
+ * onEscape() { if (overlayStack.isTop(this.handle)) this.close(); }
19542
+ * }
19543
+ * ```
19544
+ */
19545
+ class OverlayStack {
19546
+ stack = [];
19547
+ /** Registers an opening overlay; the returned handle identifies it for later calls. */
19548
+ push() {
19549
+ const handle = { overlay: true };
19550
+ this.stack.push(handle);
19551
+ return handle;
19552
+ }
19553
+ /**
19554
+ * Unregisters a closing or destroyed overlay. Tolerates out-of-order removal (an
19555
+ * inner overlay torn down after its parent), unknown handles, and `null`.
19556
+ *
19557
+ * Returns `null` so callers can clear their field in one statement:
19558
+ * `this.handle = overlayStack.remove(this.handle)`.
19559
+ */
19560
+ remove(handle) {
19561
+ if (!handle)
19562
+ return null;
19563
+ const index = this.stack.indexOf(handle);
19564
+ if (index !== -1)
19565
+ this.stack.splice(index, 1);
19566
+ return null;
19567
+ }
19568
+ /** True when this handle is the topmost open overlay — its Escape handler may act. */
19569
+ isTop(handle) {
19570
+ return (handle !== null &&
19571
+ this.stack.length > 0 &&
19572
+ this.stack[this.stack.length - 1] === handle);
19573
+ }
19574
+ /** Number of currently-open overlays. Useful for scroll-lock refcounting. */
19575
+ get depth() {
19576
+ return this.stack.length;
19577
+ }
19578
+ }
19579
+ /**
19580
+ * The shared, document-wide stack. Import this rather than constructing an
19581
+ * `OverlayStack` — a private instance cannot arbitrate against anyone else's
19582
+ * overlays, which defeats the purpose.
19583
+ */
19584
+ const overlayStack = new OverlayStack();
19585
+
19586
+ /**
19587
+ * Focus capture/restore for overlays — the other half of a correct dismiss story.
19588
+ *
19589
+ * When an overlay opens it moves focus inside itself; when it closes, focus must go
19590
+ * back to whatever opened it, or the keyboard user is dumped at the top of the
19591
+ * document and has to re-traverse the page. Every hand-rolled modal/drawer in the
19592
+ * estate re-implements this with a `restoreFocusTo` field and a `.focus()` call, and
19593
+ * most of them miss at least one of the edge cases below.
19594
+ *
19595
+ * @example
19596
+ * ```ts
19597
+ * private restore: FocusRestore | null = null;
19598
+ *
19599
+ * open() { this.restore = captureFocus(); }
19600
+ * close() { this.restore = restoreFocus(this.restore); }
19601
+ * ```
19602
+ */
19603
+ /**
19604
+ * Snapshots the currently-focused element so it can be refocused later.
19605
+ *
19606
+ * Returns a token even when nothing is focused (`document.body` is treated as "no
19607
+ * meaningful focus"), so callers never branch — {@link restoreFocus} no-ops on it.
19608
+ */
19609
+ function captureFocus(doc = document) {
19610
+ const active = doc.activeElement;
19611
+ const element = active instanceof HTMLElement && active !== doc.body ? active : null;
19612
+ return { element };
19613
+ }
19614
+ /**
19615
+ * Returns focus to the captured element, if it is still focusable.
19616
+ *
19617
+ * Guards the three cases that make naive `restoreFocusTo.focus()` misbehave:
19618
+ * - the element was removed from the DOM while the overlay was open (a row deleted by
19619
+ * the very dialog that is closing) — `isConnected` is false, so we skip rather than
19620
+ * throw focus to `<body>` via a detached node;
19621
+ * - it became disabled or `inert` while the overlay was open;
19622
+ * - nothing was focused when the overlay opened.
19623
+ *
19624
+ * `preventScroll` keeps the page from jumping when the trigger has scrolled out of
19625
+ * view behind the overlay — the caller decides whether the trigger should be scrolled
19626
+ * back into view, which is a product decision, not a focus one.
19627
+ *
19628
+ * Returns `null` so callers can clear their field in one statement.
19629
+ */
19630
+ function restoreFocus(restore, options = {}) {
19631
+ const element = restore?.element;
19632
+ if (!element || !element.isConnected)
19633
+ return null;
19634
+ if (element.hasAttribute('disabled') || element.closest('[inert]'))
19635
+ return null;
19636
+ element.focus({ preventScroll: options.preventScroll ?? true });
19637
+ return null;
19638
+ }
19639
+
19640
+ /**
19641
+ * Debounce primitives — the shared replacement for the `setTimeout` / `clearTimeout`
19642
+ * pairs hand-rolled in every list screen and typeahead across the estate.
19643
+ *
19644
+ * Two entry points, for the two situations:
19645
+ * - {@link FlyDebouncer} — an object you hold and call, with `cancel()` and `flush()`.
19646
+ * Use it in a component that debounces on a field (search boxes, reload-on-filter).
19647
+ * - {@link flyDebounced} — an injection-context factory that wires `cancel()` to the
19648
+ * host's `DestroyRef` for you, so a pending call can never fire after teardown.
19649
+ *
19650
+ * ## Why not `debounceTime` from RxJS
19651
+ * Nothing wrong with it when the input is already a stream. But the common case here
19652
+ * is a signal-based component with an `(input)` handler and no Subject in sight, and
19653
+ * standing up a `Subject` + `takeUntilDestroyed` + `subscribe` to debounce one field is
19654
+ * more machinery than the problem deserves. These helpers are the imperative
19655
+ * equivalent, with the teardown correctness that hand-rolled timers usually miss.
19656
+ *
19657
+ * ## The leak these fix
19658
+ * A bare `setTimeout(() => this.reload(), 150)` with no `clearTimeout` in `ngOnDestroy`
19659
+ * still fires after the component is gone — reading destroyed signals, calling a
19660
+ * service on a torn-down injector, or logging a spurious HTTP request. {@link FlyDebouncer}
19661
+ * makes cancellation available and {@link flyDebounced} makes it automatic.
19662
+ */
19663
+ /** Default settle delay for search-style inputs, in milliseconds. */
19664
+ const FLY_SEARCH_DEBOUNCE_MS = 300;
19665
+ /** Default settle delay for re-querying a list after a filter/sort change. */
19135
19666
  const FLY_RELOAD_DEBOUNCE_MS = 150;
19136
19667
  /**
19137
19668
  * A cancellable trailing-edge debouncer.
@@ -19692,366 +20223,112 @@ class FlyFileDownloadService {
19692
20223
  .pipe(flyScanRetry(), tap((blob) => flyDownloadBlob(blob, fileName)));
19693
20224
  }
19694
20225
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyFileDownloadService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
19695
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyFileDownloadService, providedIn: 'root' });
19696
- }
19697
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyFileDownloadService, decorators: [{
19698
- type: Injectable,
19699
- args: [{ providedIn: 'root' }]
19700
- }] });
19701
-
19702
- /**
19703
- * Unwraps `FlyApiResponse<T>` to its payload, or `null` when the call did not succeed.
19704
- *
19705
- * This is the STRICT reading: `success: false` yields `null` even if a partial `data` came with
19706
- * it. Prefer it. Use {@link flyUnwrapLenient} only against an endpoint that is known to answer
19707
- * unwrapped.
19708
- */
19709
- function flyUnwrap(res) {
19710
- return res.success ? res.data ?? null : null;
19711
- }
19712
- /**
19713
- * Unwraps `FlyApiResponse<T>`, tolerating an endpoint that answered UNWRAPPED — i.e. returned the
19714
- * payload as the whole body with no envelope around it.
19715
- *
19716
- * **This is a workaround for a backend that is inconsistent, not a preference.** Every endpoint is
19717
- * supposed to wrap its payload; a handful do not, and call sites carried a defensive
19718
- * `res.data ?? res` for them. That expression forced the surrounding chain to `any`, which
19719
- * silenced the mismatch rather than naming it. This keeps the identical runtime behaviour while
19720
- * staying typed — but a call site reaching for it is evidence of an endpoint worth fixing, and it
19721
- * cannot distinguish "unwrapped payload" from "envelope with `success: false`".
19722
- */
19723
- function flyUnwrapLenient(res) {
19724
- return res.data ?? res;
19725
- }
19726
- /**
19727
- * Normalizes the `FlyApiResponse<FlyPaged<T>>` envelope to the flat `{ items, total }` a list UI
19728
- * reads. An absent `total` falls back to the row count, so a server that omits it still renders a
19729
- * correct count for the page in hand rather than `0`.
19730
- */
19731
- function flyToPage(res) {
19732
- const items = res.data?.items ?? [];
19733
- return { items, total: res.data?.total ?? items.length };
19734
- }
19735
- /**
19736
- * Best-effort extraction of the message (usually an i18n key) out of an
19737
- * `HttpErrorResponse.error` body returned by a FlyOS endpoint.
19738
- *
19739
- * The wire format is inconsistent by design: business-rule failures arrive as `{ errors: [key] }`,
19740
- * the structured envelope as `{ error: { code, message } }`, framework failures as `{ message }`,
19741
- * and some proxies hand back a bare string. Call sites used to inline this probe behind
19742
- * `err?.error as any`, typing the whole chain as `any` and duplicating the logic verbatim.
19743
- *
19744
- * @returns the first `errors[]` entry, else `error.message`, else `message`, else the raw string
19745
- * body — or `undefined` when none of those is present.
19746
- */
19747
- function flyApiErrorMessage(err) {
19748
- const raw = err?.error;
19749
- if (typeof raw === 'string')
19750
- return raw;
19751
- if (raw && typeof raw === 'object') {
19752
- const body = raw;
19753
- if (Array.isArray(body.errors) && body.errors.length > 0)
19754
- return String(body.errors[0]);
19755
- const nested = body.error;
19756
- if (nested && typeof nested === 'object' && typeof nested.message === 'string') {
19757
- return nested.message;
19758
- }
19759
- if (typeof body.message === 'string')
19760
- return body.message;
19761
- }
19762
- return undefined;
19763
- }
19764
-
19765
- /**
19766
- * Shared presence-colour utility — one deterministic seed → colour mapping so
19767
- * every co-authoring surface (mind-maps, canvas-boards, and future Documents/
19768
- * Thoughts Labs consumers) paints the SAME user the SAME colour, without any
19769
- * cross-app coordination beyond importing this module.
19770
- *
19771
- * The palette and hash algorithm are copied byte-for-byte from the mind-maps
19772
- * feature app's inline `identity` computed (`mind-maps.component.ts`), which
19773
- * predates this shared module and is the origin of the vocabulary — see
19774
- * `PRESENCE_COLORS` / the `hash = (hash*31 + charCode)|0` loop there. This
19775
- * file does not replace that call site (existing consumers are migrated in
19776
- * future work per the canvas-board gap-closure plan §7); it exists so NEW
19777
- * consumers (the canvas-board presence adapter) don't hand-roll a second copy
19778
- * that could silently drift from the original.
19779
- *
19780
- * Rides no transport of its own — purely a pure function over a string seed
19781
- * (typically a user id). Callers broadcast the resolved colour over Yjs
19782
- * awareness (`user.color`) themselves.
19783
- */
19784
- /** Stable presence palette — identical order/values to the mind-maps palette. */
19785
- const PRESENCE_COLORS = Object.freeze([
19786
- '#ef4444', '#06b6d4', '#10b981', '#3b82f6',
19787
- '#8b5cf6', '#ec4899', '#14b8a6', '#f43f5e',
19788
- ]);
19789
- /**
19790
- * Deterministically maps a seed (typically a user id, falling back to a
19791
- * display name where no id is available) to one of {@link PRESENCE_COLORS}.
19792
- *
19793
- * Same seed ⇒ same colour, always — across apps, sessions, and page reloads,
19794
- * with no server round-trip or shared state required.
19795
- */
19796
- function presenceColorFor(seed) {
19797
- let hash = 0;
19798
- for (let i = 0; i < seed.length; i++)
19799
- hash = (hash * 31 + seed.charCodeAt(i)) | 0;
19800
- return PRESENCE_COLORS[Math.abs(hash) % PRESENCE_COLORS.length];
19801
- }
19802
-
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();
20226
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyFileDownloadService, providedIn: 'root' });
20227
+ }
20228
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyFileDownloadService, decorators: [{
20229
+ type: Injectable,
20230
+ args: [{ providedIn: 'root' }]
20231
+ }] });
20232
+
19907
20233
  /**
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").
20234
+ * Unwraps `FlyApiResponse<T>` to its payload, or `null` when the call did not succeed.
19918
20235
  *
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.
20236
+ * This is the STRICT reading: `success: false` yields `null` even if a partial `data` came with
20237
+ * it. Prefer it. Use {@link flyUnwrapLenient} only against an endpoint that is known to answer
20238
+ * unwrapped.
19924
20239
  */
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);
20240
+ function flyUnwrap(res) {
20241
+ return res.success ? res.data ?? null : null;
19953
20242
  }
19954
- // ─── Relative time ───────────────────────────────────────────────────────────
19955
20243
  /**
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.
20244
+ * Unwraps `FlyApiResponse<T>`, tolerating an endpoint that answered UNWRAPPEDi.e. returned the
20245
+ * payload as the whole body with no envelope around it.
19959
20246
  *
19960
- * `now` is injectable so tests can pin the clock instead of sleeping or accepting
19961
- * flake around bucket boundaries.
20247
+ * **This is a workaround for a backend that is inconsistent, not a preference.** Every endpoint is
20248
+ * supposed to wrap its payload; a handful do not, and call sites carried a defensive
20249
+ * `res.data ?? res` for them. That expression forced the surrounding chain to `any`, which
20250
+ * silenced the mismatch rather than naming it. This keeps the identical runtime behaviour while
20251
+ * staying typed — but a call site reaching for it is evidence of an endpoint worth fixing, and it
20252
+ * cannot distinguish "unwrapped payload" from "envelope with `success: false`".
19962
20253
  */
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');
20254
+ function flyUnwrapLenient(res) {
20255
+ return res.data ?? res;
19986
20256
  }
19987
20257
  /**
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.
20258
+ * Normalizes the `FlyApiResponse<FlyPaged<T>>` envelope to the flat `{ items, total }` a list UI
20259
+ * reads. An absent `total` falls back to the row count, so a server that omits it still renders a
20260
+ * correct count for the page in hand rather than `0`.
19990
20261
  */
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);
20262
+ function flyToPage(res) {
20263
+ const items = res.data?.items ?? [];
20264
+ return { items, total: res.data?.total ?? items.length };
20006
20265
  }
20007
- // ─── Dates ───────────────────────────────────────────────────────────────────
20008
20266
  /**
20009
- * Localized calendar date (no time). Pass `options` to override the default
20010
- * short-date presentation.
20267
+ * Best-effort extraction of the message (usually an i18n key) out of an
20268
+ * `HttpErrorResponse.error` body returned by a FlyOS endpoint.
20011
20269
  *
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"`.
20270
+ * The wire format is inconsistent by design: business-rule failures arrive as `{ errors: [key] }`,
20271
+ * the structured envelope as `{ error: { code, message } }`, framework failures as `{ message }`,
20272
+ * and some proxies hand back a bare string. Call sites used to inline this probe behind
20273
+ * `err?.error as any`, typing the whole chain as `any` and duplicating the logic verbatim.
20274
+ *
20275
+ * @returns the first `errors[]` entry, else `error.message`, else `message`, else the raw string
20276
+ * body — or `undefined` when none of those is present.
20015
20277
  */
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);
20278
+ function flyApiErrorMessage(err) {
20279
+ const raw = err?.error;
20280
+ if (typeof raw === 'string')
20281
+ return raw;
20282
+ if (raw && typeof raw === 'object') {
20283
+ const body = raw;
20284
+ if (Array.isArray(body.errors) && body.errors.length > 0)
20285
+ return String(body.errors[0]);
20286
+ const nested = body.error;
20287
+ if (nested && typeof nested === 'object' && typeof nested.message === 'string') {
20288
+ return nested.message;
20289
+ }
20290
+ if (typeof body.message === 'string')
20291
+ return body.message;
20027
20292
  }
20293
+ return undefined;
20028
20294
  }
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
- }
20295
+
20037
20296
  /**
20038
- * Calendar date as `yyyy-MM-dd` the wire/sort form, deliberately NOT localized.
20297
+ * Shared presence-colour utilityone deterministic seed colour mapping so
20298
+ * every co-authoring surface (mind-maps, canvas-boards, and future Documents/
20299
+ * Thoughts Labs consumers) paints the SAME user the SAME colour, without any
20300
+ * cross-app coordination beyond importing this module.
20039
20301
  *
20040
- * Use for `<input type="date">` values, query params, and sort keys. For anything a
20041
- * user reads, use {@link flyFormatDate}.
20302
+ * The palette and hash algorithm are copied byte-for-byte from the mind-maps
20303
+ * feature app's inline `identity` computed (`mind-maps.component.ts`), which
20304
+ * predates this shared module and is the origin of the vocabulary — see
20305
+ * `PRESENCE_COLORS` / the `hash = (hash*31 + charCode)|0` loop there. This
20306
+ * file does not replace that call site (existing consumers are migrated in
20307
+ * future work per the canvas-board gap-closure plan §7); it exists so NEW
20308
+ * consumers (the canvas-board presence adapter) don't hand-roll a second copy
20309
+ * that could silently drift from the original.
20042
20310
  *
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.
20311
+ * Rides no transport of its own purely a pure function over a string seed
20312
+ * (typically a user id). Callers broadcast the resolved colour over Yjs
20313
+ * awareness (`user.color`) themselves.
20045
20314
  */
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;
20315
+ /** Stable presence palette — identical order/values to the mind-maps palette. */
20316
+ const PRESENCE_COLORS = Object.freeze([
20317
+ '#ef4444', '#06b6d4', '#10b981', '#3b82f6',
20318
+ '#8b5cf6', '#ec4899', '#14b8a6', '#f43f5e',
20319
+ ]);
20320
+ /**
20321
+ * Deterministically maps a seed (typically a user id, falling back to a
20322
+ * display name where no id is available) to one of {@link PRESENCE_COLORS}.
20323
+ *
20324
+ * Same seed ⇒ same colour, always — across apps, sessions, and page reloads,
20325
+ * with no server round-trip or shared state required.
20326
+ */
20327
+ function presenceColorFor(seed) {
20328
+ let hash = 0;
20329
+ for (let i = 0; i < seed.length; i++)
20330
+ hash = (hash * 31 + seed.charCodeAt(i)) | 0;
20331
+ return PRESENCE_COLORS[Math.abs(hash) % PRESENCE_COLORS.length];
20055
20332
  }
20056
20333
 
20057
20334
  /**
@@ -21483,30 +21760,30 @@ class FlyDetailCardComponent {
21483
21760
  /** Drop the body padding — for a child that brings its own list/table chrome. */
21484
21761
  flush = input(false, ...(ngDevMode ? [{ debugName: "flush" }] : /* istanbul ignore next */ []));
21485
21762
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyDetailCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
21486
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyDetailCardComponent, isStandalone: true, selector: "fly-detail-card", inputs: { titleKey: { classPropertyName: "titleKey", publicName: "titleKey", isSignal: true, isRequired: false, transformFunction: null }, hasProjectedTitle: { classPropertyName: "hasProjectedTitle", publicName: "hasProjectedTitle", isSignal: true, isRequired: false, transformFunction: null }, flush: { classPropertyName: "flush", publicName: "flush", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
21487
- @if (titleKey() || hasProjectedTitle()) {
21488
- <fly-section-header [titleKey]="titleKey()">
21489
- <ng-content select="[card-title]" ngProjectAs="[section-title]" />
21490
- <ng-content select="[card-actions]" ngProjectAs="[section-actions]" />
21491
- </fly-section-header>
21492
- }
21493
- <div class="dc__body" [class.dc__body--flush]="flush()">
21494
- <ng-content />
21495
- </div>
21763
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyDetailCardComponent, isStandalone: true, selector: "fly-detail-card", inputs: { titleKey: { classPropertyName: "titleKey", publicName: "titleKey", isSignal: true, isRequired: false, transformFunction: null }, hasProjectedTitle: { classPropertyName: "hasProjectedTitle", publicName: "hasProjectedTitle", isSignal: true, isRequired: false, transformFunction: null }, flush: { classPropertyName: "flush", publicName: "flush", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
21764
+ @if (titleKey() || hasProjectedTitle()) {
21765
+ <fly-section-header [titleKey]="titleKey()">
21766
+ <ng-content select="[card-title]" ngProjectAs="[section-title]" />
21767
+ <ng-content select="[card-actions]" ngProjectAs="[section-actions]" />
21768
+ </fly-section-header>
21769
+ }
21770
+ <div class="dc__body" [class.dc__body--flush]="flush()">
21771
+ <ng-content />
21772
+ </div>
21496
21773
  `, isInline: true, styles: [":host{display:block;background:var(--bg-2);border:1px solid var(--w08);border-radius:var(--r-lg);box-shadow:var(--shadow-card);overflow:hidden;animation:itemIn .3s var(--nova-ease-micro) both}.dc__body{padding:var(--sp-3) var(--sp-4) var(--sp-4)}.dc__body--flush{padding:0}\n"], dependencies: [{ kind: "component", type: FlySectionHeaderComponent, selector: "fly-section-header", inputs: ["titleKey"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
21497
21774
  }
21498
21775
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyDetailCardComponent, decorators: [{
21499
21776
  type: Component,
21500
- args: [{ selector: 'fly-detail-card', standalone: true, imports: [FlySectionHeaderComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
21501
- @if (titleKey() || hasProjectedTitle()) {
21502
- <fly-section-header [titleKey]="titleKey()">
21503
- <ng-content select="[card-title]" ngProjectAs="[section-title]" />
21504
- <ng-content select="[card-actions]" ngProjectAs="[section-actions]" />
21505
- </fly-section-header>
21506
- }
21507
- <div class="dc__body" [class.dc__body--flush]="flush()">
21508
- <ng-content />
21509
- </div>
21777
+ args: [{ selector: 'fly-detail-card', standalone: true, imports: [FlySectionHeaderComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
21778
+ @if (titleKey() || hasProjectedTitle()) {
21779
+ <fly-section-header [titleKey]="titleKey()">
21780
+ <ng-content select="[card-title]" ngProjectAs="[section-title]" />
21781
+ <ng-content select="[card-actions]" ngProjectAs="[section-actions]" />
21782
+ </fly-section-header>
21783
+ }
21784
+ <div class="dc__body" [class.dc__body--flush]="flush()">
21785
+ <ng-content />
21786
+ </div>
21510
21787
  `, styles: [":host{display:block;background:var(--bg-2);border:1px solid var(--w08);border-radius:var(--r-lg);box-shadow:var(--shadow-card);overflow:hidden;animation:itemIn .3s var(--nova-ease-micro) both}.dc__body{padding:var(--sp-3) var(--sp-4) var(--sp-4)}.dc__body--flush{padding:0}\n"] }]
21511
21788
  }], propDecorators: { titleKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleKey", required: false }] }], hasProjectedTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasProjectedTitle", required: false }] }], flush: [{ type: i0.Input, args: [{ isSignal: true, alias: "flush", required: false }] }] } });
21512
21789
 
@@ -21634,108 +21911,108 @@ class FlyDetailShellComponent {
21634
21911
  buttons?.[next]?.focus();
21635
21912
  }
21636
21913
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyDetailShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
21637
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyDetailShellComponent, isStandalone: true, selector: "fly-detail-shell", inputs: { sections: { classPropertyName: "sections", publicName: "sections", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, sectionsLabelKey: { classPropertyName: "sectionsLabelKey", publicName: "sectionsLabelKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selected: "selectedChange" }, viewQueries: [{ propertyName: "rail", first: true, predicate: ["rail"], descendants: true, isSignal: true }], ngImport: i0, template: `
21638
- <div class="ds__layout">
21639
- <aside class="ds__aside">
21640
- <div class="ds__pinned">
21641
- <ng-content select="[detail-aside]" />
21642
- </div>
21643
-
21644
- @if (sections().length > 0) {
21645
- <div
21646
- #rail
21647
- class="ds__rail"
21648
- role="tablist"
21649
- tabindex="-1"
21650
- [attr.aria-orientation]="'vertical'"
21651
- [attr.aria-label]="sectionsLabelKey() | translate"
21652
- (keydown)="onKey($event)"
21653
- >
21654
- @for (s of sections(); track s.id) {
21655
- <button
21656
- type="button"
21657
- class="ds__tab"
21658
- role="tab"
21659
- [id]="tabId(s.id)"
21660
- [class.ds__tab--active]="activeId() === s.id"
21661
- [attr.aria-selected]="activeId() === s.id"
21662
- [attr.aria-controls]="panelId()"
21663
- [attr.tabindex]="activeId() === s.id ? 0 : -1"
21664
- [attr.title]="s.labelKey | translate"
21665
- (click)="select(s.id)"
21666
- >
21667
- @if (s.icon) {
21668
- <span class="pi {{ s.icon }} ds__tab-ico" aria-hidden="true"></span>
21669
- }
21670
- <span class="ds__tab-label">{{ s.labelKey | translate }}</span>
21671
- </button>
21672
- }
21673
- </div>
21674
- }
21675
- </aside>
21676
-
21677
- <div
21678
- class="ds__panel"
21679
- [id]="panelId()"
21680
- [attr.role]="sections().length ? 'tabpanel' : null"
21681
- [attr.aria-labelledby]="activeId() ? tabId(activeId()!) : null"
21682
- >
21683
- <ng-content />
21684
- </div>
21685
- </div>
21914
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyDetailShellComponent, isStandalone: true, selector: "fly-detail-shell", inputs: { sections: { classPropertyName: "sections", publicName: "sections", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, sectionsLabelKey: { classPropertyName: "sectionsLabelKey", publicName: "sectionsLabelKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selected: "selectedChange" }, viewQueries: [{ propertyName: "rail", first: true, predicate: ["rail"], descendants: true, isSignal: true }], ngImport: i0, template: `
21915
+ <div class="ds__layout">
21916
+ <aside class="ds__aside">
21917
+ <div class="ds__pinned">
21918
+ <ng-content select="[detail-aside]" />
21919
+ </div>
21920
+
21921
+ @if (sections().length > 0) {
21922
+ <div
21923
+ #rail
21924
+ class="ds__rail"
21925
+ role="tablist"
21926
+ tabindex="-1"
21927
+ [attr.aria-orientation]="'vertical'"
21928
+ [attr.aria-label]="sectionsLabelKey() | translate"
21929
+ (keydown)="onKey($event)"
21930
+ >
21931
+ @for (s of sections(); track s.id) {
21932
+ <button
21933
+ type="button"
21934
+ class="ds__tab"
21935
+ role="tab"
21936
+ [id]="tabId(s.id)"
21937
+ [class.ds__tab--active]="activeId() === s.id"
21938
+ [attr.aria-selected]="activeId() === s.id"
21939
+ [attr.aria-controls]="panelId()"
21940
+ [attr.tabindex]="activeId() === s.id ? 0 : -1"
21941
+ [attr.title]="s.labelKey | translate"
21942
+ (click)="select(s.id)"
21943
+ >
21944
+ @if (s.icon) {
21945
+ <span class="pi {{ s.icon }} ds__tab-ico" aria-hidden="true"></span>
21946
+ }
21947
+ <span class="ds__tab-label">{{ s.labelKey | translate }}</span>
21948
+ </button>
21949
+ }
21950
+ </div>
21951
+ }
21952
+ </aside>
21953
+
21954
+ <div
21955
+ class="ds__panel"
21956
+ [id]="panelId()"
21957
+ [attr.role]="sections().length ? 'tabpanel' : null"
21958
+ [attr.aria-labelledby]="activeId() ? tabId(activeId()!) : null"
21959
+ >
21960
+ <ng-content />
21961
+ </div>
21962
+ </div>
21686
21963
  `, isInline: true, styles: [":host{display:block;container-type:inline-size}.ds__layout{display:grid;grid-template-columns:var(--fly-detail-aside, 300px) minmax(0,1fr);gap:var(--sp-4);align-items:start}.ds__aside{display:flex;flex-direction:column;gap:var(--sp-4);min-width:0}.ds__aside .ds__rail{order:-1}.ds__pinned{display:flex;flex-direction:column;gap:var(--sp-4);min-width:0}.ds__panel{display:flex;flex-direction:column;gap:var(--sp-4);min-width:0;align-self:stretch}.ds__panel>:only-child{flex:1}.ds__rail{display:flex;flex-direction:column;gap:var(--sp-1);padding:var(--sp-2);background:var(--bg-2);border:1px solid var(--w08);border-radius:var(--r-lg);box-shadow:var(--shadow-card)}.ds__tab{display:flex;align-items:center;gap:var(--sp-3);width:100%;padding:var(--sp-2);border:0;background:transparent;border-radius:var(--r-md);color:var(--ink-2);cursor:pointer;font-family:inherit;font-size:var(--text-sm);text-align:start;white-space:nowrap;overflow:hidden;transition:background var(--t-state),color var(--t-state)}.ds__tab:hover{background:var(--w06);color:var(--ink)}.ds__tab:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}.ds__tab--active{background:var(--accent-fill);color:var(--on-accent-fill);font-weight:var(--fw-semibold)}.ds__tab-ico{font-size:var(--text-md);flex-shrink:0;width:20px;text-align:center}.ds__tab-label{min-width:0;overflow:hidden;text-overflow:ellipsis}@container (width <= 980px){.ds__layout{grid-template-columns:minmax(0,1fr)}.ds__rail{flex-flow:row wrap}.ds__tab{width:auto}}@media(width<=980px){.ds__layout{grid-template-columns:minmax(0,1fr)}.ds__rail{flex-flow:row wrap}.ds__tab{width:auto}}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
21687
21964
  }
21688
21965
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyDetailShellComponent, decorators: [{
21689
21966
  type: Component,
21690
- args: [{ selector: 'fly-detail-shell', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: `
21691
- <div class="ds__layout">
21692
- <aside class="ds__aside">
21693
- <div class="ds__pinned">
21694
- <ng-content select="[detail-aside]" />
21695
- </div>
21696
-
21697
- @if (sections().length > 0) {
21698
- <div
21699
- #rail
21700
- class="ds__rail"
21701
- role="tablist"
21702
- tabindex="-1"
21703
- [attr.aria-orientation]="'vertical'"
21704
- [attr.aria-label]="sectionsLabelKey() | translate"
21705
- (keydown)="onKey($event)"
21706
- >
21707
- @for (s of sections(); track s.id) {
21708
- <button
21709
- type="button"
21710
- class="ds__tab"
21711
- role="tab"
21712
- [id]="tabId(s.id)"
21713
- [class.ds__tab--active]="activeId() === s.id"
21714
- [attr.aria-selected]="activeId() === s.id"
21715
- [attr.aria-controls]="panelId()"
21716
- [attr.tabindex]="activeId() === s.id ? 0 : -1"
21717
- [attr.title]="s.labelKey | translate"
21718
- (click)="select(s.id)"
21719
- >
21720
- @if (s.icon) {
21721
- <span class="pi {{ s.icon }} ds__tab-ico" aria-hidden="true"></span>
21722
- }
21723
- <span class="ds__tab-label">{{ s.labelKey | translate }}</span>
21724
- </button>
21725
- }
21726
- </div>
21727
- }
21728
- </aside>
21729
-
21730
- <div
21731
- class="ds__panel"
21732
- [id]="panelId()"
21733
- [attr.role]="sections().length ? 'tabpanel' : null"
21734
- [attr.aria-labelledby]="activeId() ? tabId(activeId()!) : null"
21735
- >
21736
- <ng-content />
21737
- </div>
21738
- </div>
21967
+ args: [{ selector: 'fly-detail-shell', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: `
21968
+ <div class="ds__layout">
21969
+ <aside class="ds__aside">
21970
+ <div class="ds__pinned">
21971
+ <ng-content select="[detail-aside]" />
21972
+ </div>
21973
+
21974
+ @if (sections().length > 0) {
21975
+ <div
21976
+ #rail
21977
+ class="ds__rail"
21978
+ role="tablist"
21979
+ tabindex="-1"
21980
+ [attr.aria-orientation]="'vertical'"
21981
+ [attr.aria-label]="sectionsLabelKey() | translate"
21982
+ (keydown)="onKey($event)"
21983
+ >
21984
+ @for (s of sections(); track s.id) {
21985
+ <button
21986
+ type="button"
21987
+ class="ds__tab"
21988
+ role="tab"
21989
+ [id]="tabId(s.id)"
21990
+ [class.ds__tab--active]="activeId() === s.id"
21991
+ [attr.aria-selected]="activeId() === s.id"
21992
+ [attr.aria-controls]="panelId()"
21993
+ [attr.tabindex]="activeId() === s.id ? 0 : -1"
21994
+ [attr.title]="s.labelKey | translate"
21995
+ (click)="select(s.id)"
21996
+ >
21997
+ @if (s.icon) {
21998
+ <span class="pi {{ s.icon }} ds__tab-ico" aria-hidden="true"></span>
21999
+ }
22000
+ <span class="ds__tab-label">{{ s.labelKey | translate }}</span>
22001
+ </button>
22002
+ }
22003
+ </div>
22004
+ }
22005
+ </aside>
22006
+
22007
+ <div
22008
+ class="ds__panel"
22009
+ [id]="panelId()"
22010
+ [attr.role]="sections().length ? 'tabpanel' : null"
22011
+ [attr.aria-labelledby]="activeId() ? tabId(activeId()!) : null"
22012
+ >
22013
+ <ng-content />
22014
+ </div>
22015
+ </div>
21739
22016
  `, styles: [":host{display:block;container-type:inline-size}.ds__layout{display:grid;grid-template-columns:var(--fly-detail-aside, 300px) minmax(0,1fr);gap:var(--sp-4);align-items:start}.ds__aside{display:flex;flex-direction:column;gap:var(--sp-4);min-width:0}.ds__aside .ds__rail{order:-1}.ds__pinned{display:flex;flex-direction:column;gap:var(--sp-4);min-width:0}.ds__panel{display:flex;flex-direction:column;gap:var(--sp-4);min-width:0;align-self:stretch}.ds__panel>:only-child{flex:1}.ds__rail{display:flex;flex-direction:column;gap:var(--sp-1);padding:var(--sp-2);background:var(--bg-2);border:1px solid var(--w08);border-radius:var(--r-lg);box-shadow:var(--shadow-card)}.ds__tab{display:flex;align-items:center;gap:var(--sp-3);width:100%;padding:var(--sp-2);border:0;background:transparent;border-radius:var(--r-md);color:var(--ink-2);cursor:pointer;font-family:inherit;font-size:var(--text-sm);text-align:start;white-space:nowrap;overflow:hidden;transition:background var(--t-state),color var(--t-state)}.ds__tab:hover{background:var(--w06);color:var(--ink)}.ds__tab:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}.ds__tab--active{background:var(--accent-fill);color:var(--on-accent-fill);font-weight:var(--fw-semibold)}.ds__tab-ico{font-size:var(--text-md);flex-shrink:0;width:20px;text-align:center}.ds__tab-label{min-width:0;overflow:hidden;text-overflow:ellipsis}@container (width <= 980px){.ds__layout{grid-template-columns:minmax(0,1fr)}.ds__rail{flex-flow:row wrap}.ds__tab{width:auto}}@media(width<=980px){.ds__layout{grid-template-columns:minmax(0,1fr)}.ds__rail{flex-flow:row wrap}.ds__tab{width:auto}}\n"] }]
21740
22017
  }], propDecorators: { rail: [{ type: i0.ViewChild, args: ['rail', { isSignal: true }] }], sections: [{ type: i0.Input, args: [{ isSignal: true, alias: "sections", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }, { type: i0.Output, args: ["selectedChange"] }], sectionsLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "sectionsLabelKey", required: false }] }] } });
21741
22018
 
@@ -21858,8 +22135,22 @@ class FlyAppTopbarComponent {
21858
22135
  pendingFocus = signal(false, ...(ngDevMode ? [{ debugName: "pendingFocus" }] : /* istanbul ignore next */ []));
21859
22136
  flat = computed(() => flattenModules(this.sections()), ...(ngDevMode ? [{ debugName: "flat" }] : /* istanbul ignore next */ []));
21860
22137
  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 */ []));
22138
+ // Both trigger names must START with the button's visible text ("Circles",
22139
+ // "Signals") — voice-control users activate controls by saying what they see,
22140
+ // so a name that omits it fails WCAG 2.5.3 Label in Name. The action context
22141
+ // is appended after the visible text; the popover menu keeps the plain
22142
+ // "Switch module" name since it renders no visible text of its own.
22143
+ brandAriaLabel = computed(() => {
22144
+ const home = this.i18n.t('ui.nav.home');
22145
+ const key = this.brandLabelKey();
22146
+ return key ? `${this.i18n.t(key)}, ${home}` : home;
22147
+ }, ...(ngDevMode ? [{ debugName: "brandAriaLabel" }] : /* istanbul ignore next */ []));
21862
22148
  switchAriaLabel = computed(() => this.i18n.t('ui.nav.switchModule'), ...(ngDevMode ? [{ debugName: "switchAriaLabel" }] : /* istanbul ignore next */ []));
22149
+ triggerAriaLabel = computed(() => {
22150
+ const active = this.activeModule();
22151
+ const visible = active ? this.i18n.t(active.labelKey) : this.i18n.t('ui.nav.selectModule');
22152
+ return `${visible}, ${this.i18n.t('ui.nav.switchModule')}`;
22153
+ }, ...(ngDevMode ? [{ debugName: "triggerAriaLabel" }] : /* istanbul ignore next */ []));
21863
22154
  constructor() {
21864
22155
  inject(DestroyRef).onDestroy(() => overlayStack.remove(this.stackHandle));
21865
22156
  effect(() => {
@@ -21965,11 +22256,11 @@ class FlyAppTopbarComponent {
21965
22256
  this.rows()[index]?.nativeElement.focus();
21966
22257
  }
21967
22258
  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 });
22259
+ 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
22260
  }
21970
22261
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyAppTopbarComponent, decorators: [{
21971
22262
  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"] }]
22263
+ 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
22264
  }], 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
22265
 
21975
22266
  /**
@@ -24450,5 +24741,5 @@ const AUDIENCE_ERROR_CODES = {
24450
24741
  * Generated bundle index. Do not edit.
24451
24742
  */
24452
24743
 
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 };
24744
+ 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, 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, 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
24745
  //# sourceMappingURL=flyos-design-system.mjs.map