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