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