@foxeltech/angular-ui 0.7.83 → 0.7.101

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.
Files changed (27) hide show
  1. package/components.css +40 -1
  2. package/fesm2022/foxeltech-angular-ui-chart.mjs +33 -3
  3. package/fesm2022/foxeltech-angular-ui-chart.mjs.map +1 -1
  4. package/fesm2022/foxeltech-angular-ui-flow.mjs +42 -5
  5. package/fesm2022/foxeltech-angular-ui-flow.mjs.map +1 -1
  6. package/fesm2022/foxeltech-angular-ui.mjs +559 -42
  7. package/fesm2022/foxeltech-angular-ui.mjs.map +1 -1
  8. package/package.json +3 -2
  9. package/src/lib/styles/components.css +40 -1
  10. package/src/lib/styles/utilities.css +2 -1
  11. package/src/lib/ui/components/breadcrumb/breadcrumb.component.css +262 -0
  12. package/src/lib/ui/components/breadcrumb/breadcrumb.component.html +91 -0
  13. package/src/lib/ui/components/button/button.component.html +1 -0
  14. package/src/lib/ui/components/filter-pills/filter-pills.component.html +1 -1
  15. package/src/lib/ui/components/load-status/load-status.component.css +96 -0
  16. package/src/lib/ui/components/load-status/load-status.component.html +20 -0
  17. package/src/lib/ui/components/master-detail/master-detail.component.css +63 -11
  18. package/src/lib/ui/components/master-detail/master-detail.component.html +71 -42
  19. package/src/lib/ui/components/route-skeleton/route-skeleton.component.css +1 -1
  20. package/src/lib/ui/components/segmented/segmented.component.html +3 -1
  21. package/src/lib/ui/components/severity-badges/severity-badges.component.html +1 -1
  22. package/src/lib/ui/components/stat-cards/stat-cards.component.css +18 -0
  23. package/src/lib/ui/components/stat-cards/stat-cards.component.html +3 -3
  24. package/types/foxeltech-angular-ui-chart.d.ts +18 -1
  25. package/types/foxeltech-angular-ui-flow.d.ts +7 -0
  26. package/types/foxeltech-angular-ui.d.ts +309 -8
  27. package/utilities.css +2 -1
@@ -1,9 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, DestroyRef, Injector, Directive, InjectionToken, input, isDevMode, ChangeDetectionStrategy, Component, Input, createComponent, Injectable, viewChild, ViewChild, Inject, EventEmitter, signal, computed, NgModule, HostListener, output, ChangeDetectorRef, contentChild, TemplateRef, contentChildren, Output, ViewChildren, ElementRef, model, viewChildren, afterRenderEffect, untracked } from '@angular/core';
2
+ import { inject, DestroyRef, Injector, Directive, signal, computed, Injectable, InjectionToken, input, isDevMode, ChangeDetectionStrategy, Component, Input, createComponent, viewChild, ViewChild, Inject, EventEmitter, NgModule, HostListener, output, ChangeDetectorRef, contentChild, TemplateRef, contentChildren, Output, ViewChildren, ElementRef, effect, linkedSignal, model, viewChildren, afterRenderEffect, untracked, afterNextRender } from '@angular/core';
3
3
  import { MatPaginator } from '@angular/material/paginator';
4
4
  import { MatTableDataSource, MatTable } from '@angular/material/table';
5
+ import { Subject, Observable, firstValueFrom, debounceTime, distinctUntilChanged, BehaviorSubject, throwError, filter, takeUntil } from 'rxjs';
5
6
  import { NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
6
- import { Observable, firstValueFrom, Subject, debounceTime, distinctUntilChanged, BehaviorSubject, throwError, filter, takeUntil } from 'rxjs';
7
7
  import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
8
8
  import * as i2 from '@angular/common/http';
9
9
  import { HttpRequest, HttpClient } from '@angular/common/http';
@@ -23,11 +23,12 @@ import * as i1$2 from '@angular/material/datepicker';
23
23
  import { MatDatepickerModule } from '@angular/material/datepicker';
24
24
  import * as i2$2 from '@angular/material/timepicker';
25
25
  import { MatTimepickerModule } from '@angular/material/timepicker';
26
- import { MAT_DATE_LOCALE, provideNativeDateAdapter } from '@angular/material/core';
26
+ import { provideNativeDateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
27
27
  import { MatTooltip } from '@angular/material/tooltip';
28
28
  import { MatFormField, MatSelect, MatOption } from '@angular/material/select';
29
29
  import { moveItemInArray, transferArrayItem, CdkDropList, CdkDrag, CdkDragPlaceholder } from '@angular/cdk/drag-drop';
30
- import { trigger, state, style, transition, animate } from '@angular/animations';
30
+ import { trigger, state, transition, style, animate } from '@angular/animations';
31
+ import gsap from 'gsap';
31
32
  import * as i1$4 from '@angular/material/menu';
32
33
  import { MatMenuModule } from '@angular/material/menu';
33
34
 
@@ -50,6 +51,81 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
50
51
  type: Directive
51
52
  }], ctorParameters: () => [{ type: i0.Injector }] });
52
53
 
54
+ /**
55
+ * Backend-request bookkeeping behind the topbar load status (design
56
+ * "Loading States", 04/09): while any tracked request is in flight the shell
57
+ * shows a spinner + "Loading <label> · k of n" and a thin indeterminate bar;
58
+ * once the batch settles it shows "● Up to date · just now".
59
+ *
60
+ * A batch = every job started while at least one job is still pending. When
61
+ * the last one settles the batch closes (`lastUpdated` moves) and the next
62
+ * job opens a fresh count — so "k of n" always reads against the requests of
63
+ * the current page load, not the session total.
64
+ *
65
+ * Wire-ups: `fxListResolver` and `BaseTableComponent.fetch()` track
66
+ * automatically; screens with their own fetches call `track()`.
67
+ */
68
+ class FxLoadStatusService {
69
+ seq = 0;
70
+ _pending = signal([], ...(ngDevMode ? [{ debugName: "_pending" }] : /* istanbul ignore next */ []));
71
+ _batchTotal = signal(0, ...(ngDevMode ? [{ debugName: "_batchTotal" }] : /* istanbul ignore next */ []));
72
+ _lastUpdated = signal(null, ...(ngDevMode ? [{ debugName: "_lastUpdated" }] : /* istanbul ignore next */ []));
73
+ _reload = new Subject();
74
+ /** Jobs still in flight, oldest first. */
75
+ pending = this._pending.asReadonly();
76
+ /** Requests started in the current batch. */
77
+ total = this._batchTotal.asReadonly();
78
+ done = computed(() => Math.max(0, this._batchTotal() - this._pending().length), ...(ngDevMode ? [{ debugName: "done" }] : /* istanbul ignore next */ []));
79
+ loading = computed(() => this._pending().length > 0, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
80
+ /** Label of the oldest pending request — what the status line names. */
81
+ current = computed(() => this._pending()[0]?.label ?? '', ...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
82
+ /** When the last batch settled; null until the first one does. */
83
+ lastUpdated = this._lastUpdated.asReadonly();
84
+ /** Fired by the topbar ↻ — the shell decides how to re-fetch the route. */
85
+ reload$ = this._reload.asObservable();
86
+ /** Register a request; call the returned function when it settles. */
87
+ begin(label) {
88
+ const job = { id: ++this.seq, label };
89
+ if (this._pending().length === 0)
90
+ this._batchTotal.set(0);
91
+ this._batchTotal.update((n) => n + 1);
92
+ this._pending.update((list) => [...list, job]);
93
+ let ended = false;
94
+ return () => {
95
+ if (ended)
96
+ return;
97
+ ended = true;
98
+ this._pending.update((list) => list.filter((j) => j.id !== job.id));
99
+ if (this._pending().length === 0)
100
+ this._lastUpdated.set(new Date());
101
+ };
102
+ }
103
+ /** Track a promise (or a factory producing one) under `label`. */
104
+ async track(label, work) {
105
+ const end = this.begin(label);
106
+ try {
107
+ return await (typeof work === 'function' ? work() : work);
108
+ }
109
+ finally {
110
+ end();
111
+ }
112
+ }
113
+ requestReload() {
114
+ this._reload.next();
115
+ }
116
+ /** Short label from a request url: `/TEST1/history` → "history", `/search` → "search". */
117
+ static labelFromUrl(url) {
118
+ const parts = String(url).split('?')[0].split('/').filter(Boolean);
119
+ return parts[parts.length - 1] ?? 'data';
120
+ }
121
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FxLoadStatusService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
122
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FxLoadStatusService, providedIn: 'root' });
123
+ }
124
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FxLoadStatusService, decorators: [{
125
+ type: Injectable,
126
+ args: [{ providedIn: 'root' }]
127
+ }] });
128
+
53
129
  /**
54
130
  * Where the generated Heroicons sprite is served from. Defaults to
55
131
  * 'fx-icons.svg' at the app base — consumers copy it from the package via an
@@ -686,11 +762,38 @@ class BaseTableComponent extends BaseComponent {
686
762
  loading = false;
687
763
  expandedElement;
688
764
  toastr;
765
+ /** Topbar load status; optional so hand-rolled test injectors keep working. */
766
+ loadStatus = null;
689
767
  _lock = false;
690
768
  keywordChange$ = new Subject();
691
769
  dataSourcePlaceholder;
692
770
  config;
693
- paginator;
771
+ /**
772
+ * The paging engine. Either the page's own <mat-paginator> (found by the
773
+ * ViewChild query) or one handed in by a child such as fx-ui-master-detail,
774
+ * whose paginator lives in ITS view where the query cannot see it.
775
+ *
776
+ * ⚠️ A plain `@ViewChild` property was clobbered: the query re-runs before
777
+ * every ngAfterViewInit and, finding nothing, wrote `undefined` over the
778
+ * instance the child had assigned a moment earlier — so page events were
779
+ * never subscribed and Prev/Next changed the range text but not the rows
780
+ * (anh Tú 04/09). The setter attaches on any real assignment and ignores
781
+ * `undefined`, so whichever side provides the paginator wins.
782
+ */
783
+ set paginator(p) {
784
+ if (!p || p === this._paginator)
785
+ return;
786
+ this._paginator = p;
787
+ this._pageSub?.unsubscribe();
788
+ this._pageSub = p.page.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => {
789
+ void this.onPageChange(event);
790
+ });
791
+ }
792
+ get paginator() {
793
+ return this._paginator;
794
+ }
795
+ _paginator;
796
+ _pageSub;
694
797
  table = viewChild.required(MatTable);
695
798
  constructor(injector, api, modal, ref) {
696
799
  super(injector);
@@ -698,6 +801,7 @@ class BaseTableComponent extends BaseComponent {
698
801
  this.modal = modal;
699
802
  this.ref = ref;
700
803
  this.toastr = injector.get(FxToastrService);
804
+ this.loadStatus = injector.get(FxLoadStatusService, null);
701
805
  }
702
806
  ngOnInit() {
703
807
  this.keywordChange$
@@ -710,11 +814,7 @@ class BaseTableComponent extends BaseComponent {
710
814
  }
711
815
  init() { }
712
816
  ngAfterViewInit() {
713
- if (this.paginator && this.paginator.page) {
714
- this.paginator.page.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => {
715
- this.onPageChange(event);
716
- });
717
- }
817
+ // Page events are wired by the `paginator` setter (see above).
718
818
  this.ref.detectChanges();
719
819
  }
720
820
  async refresh() {
@@ -746,6 +846,7 @@ class BaseTableComponent extends BaseComponent {
746
846
  // a newer one and overwrite the table with stale data.
747
847
  const seq = ++this._fetchSeq;
748
848
  this.loading = true;
849
+ const endStatus = this.loadStatus?.begin(this.listKey) ?? null;
749
850
  try {
750
851
  const params = { page: (this.page ?? 0) + 1, pageSize: this.pageSize, ...this.filters };
751
852
  const req = this.listRequest();
@@ -764,6 +865,7 @@ class BaseTableComponent extends BaseComponent {
764
865
  this.toastr.error(FxUtils.parseError(err));
765
866
  }
766
867
  finally {
868
+ endStatus?.();
767
869
  if (seq === this._fetchSeq) {
768
870
  this.loading = false;
769
871
  this.ref.detectChanges();
@@ -1346,9 +1448,13 @@ function fxListResolver(apiType, request) {
1346
1448
  const api = inject(apiType);
1347
1449
  const toastr = inject(FxToastrService);
1348
1450
  const storage = inject(FxStorageService);
1451
+ const status = inject(FxLoadStatusService);
1452
+ let end = null;
1349
1453
  try {
1350
1454
  const ctx = { pageSize: storage.resolve(state.url) || 10 };
1351
1455
  const req = request(route, state, ctx);
1456
+ // Topbar "Loading <x> · k of n" — one job per resolver request.
1457
+ end = status.begin(FxLoadStatusService.labelFromUrl(req.url));
1352
1458
  if ((req.method ?? 'get') === 'post') {
1353
1459
  return await api.post(req.url, req.body ?? req.params ?? {});
1354
1460
  }
@@ -1358,6 +1464,9 @@ function fxListResolver(apiType, request) {
1358
1464
  toastr.error(FxUtils.parseError(err), 'Error');
1359
1465
  return null;
1360
1466
  }
1467
+ finally {
1468
+ end?.();
1469
+ }
1361
1470
  };
1362
1471
  }
1363
1472
 
@@ -2470,11 +2579,11 @@ class ButtonComponent {
2470
2579
  this.clicked.emit();
2471
2580
  }
2472
2581
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2473
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ButtonComponent, isStandalone: true, selector: "fx-ui-button", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, buttonVariant: { classPropertyName: "buttonVariant", publicName: "buttonVariant", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked" }, ngImport: i0, template: "<button\n [attr.type]=\"type()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-busy]=\"loading() || null\"\n [class]=\"class()\"\n [ngClass]=\"{\n 'btn-default': buttonVariant() === 'default',\n 'btn-primary': buttonVariant() === 'primary',\n 'btn-success': buttonVariant() === 'success',\n 'btn-cta': buttonVariant() === 'cta',\n 'btn-alternative': buttonVariant() === 'alternative',\n }\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"onClick()\"\n>\n @if (loading()) {\n <div class=\"flex w-full items-center justify-center\" aria-hidden=\"true\">\n <svg\n [attr.fill]=\"\n buttonVariant() !== 'alternative' ? 'rgb(var(--text-primary))' : 'rgb(var(--primary))'\n \"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <circle cx=\"4\" cy=\"12\" r=\"0\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_lo66\"\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_z0Or\"\n begin=\"spinner_lo66.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"4\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_JsnR\"\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_Aguh\"\n begin=\"spinner_JsnR.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"12\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_hSjk\"\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_UHR2\"\n begin=\"spinner_hSjk.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"20\" cy=\"12\" r=\"3\">\n <animate\n id=\"spinner_4v5M\"\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_OLMs\"\n begin=\"spinner_4v5M.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n </circle>\n </svg>\n </div>\n } @else {\n <ng-container>\n <div class=\"flex items-center justify-center gap-small\">\n @if (icon) {\n <fx-ui-hero-icon\n [icon]=\"icon\"\n [size]=\"16\"\n class=\"flex\"\n [ngClass]=\"{\n 'text-text-inverse': buttonVariant() === 'default',\n 'text-text-primary': buttonVariant() === 'alternative',\n 'text-accent-contrast': buttonVariant() === 'primary',\n 'text-surface': buttonVariant() === 'success',\n 'text-white': buttonVariant() === 'cta',\n }\"\n ></fx-ui-hero-icon>\n }\n @if (label) {\n <!-- text-box: optical centering \u2014 Archivo's leading floats ink up otherwise -->\n <div class=\"[text-box:trim-both_cap_alphabetic]\">{{ label }}</div>\n }\n </div>\n </ng-container>\n }\n</button>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2582
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ButtonComponent, isStandalone: true, selector: "fx-ui-button", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, buttonVariant: { classPropertyName: "buttonVariant", publicName: "buttonVariant", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked" }, ngImport: i0, template: "<button\n [attr.type]=\"type()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-busy]=\"loading() || null\"\n [class]=\"class()\"\n [ngClass]=\"{\n 'fx-btn--icon': !!icon && !label,\n 'btn-default': buttonVariant() === 'default',\n 'btn-primary': buttonVariant() === 'primary',\n 'btn-success': buttonVariant() === 'success',\n 'btn-cta': buttonVariant() === 'cta',\n 'btn-alternative': buttonVariant() === 'alternative',\n }\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"onClick()\"\n>\n @if (loading()) {\n <div class=\"flex w-full items-center justify-center\" aria-hidden=\"true\">\n <svg\n [attr.fill]=\"\n buttonVariant() !== 'alternative' ? 'rgb(var(--text-primary))' : 'rgb(var(--primary))'\n \"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <circle cx=\"4\" cy=\"12\" r=\"0\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_lo66\"\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_z0Or\"\n begin=\"spinner_lo66.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"4\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_JsnR\"\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_Aguh\"\n begin=\"spinner_JsnR.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"12\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_hSjk\"\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_UHR2\"\n begin=\"spinner_hSjk.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"20\" cy=\"12\" r=\"3\">\n <animate\n id=\"spinner_4v5M\"\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_OLMs\"\n begin=\"spinner_4v5M.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n </circle>\n </svg>\n </div>\n } @else {\n <ng-container>\n <div class=\"flex items-center justify-center gap-small\">\n @if (icon) {\n <fx-ui-hero-icon\n [icon]=\"icon\"\n [size]=\"16\"\n class=\"flex\"\n [ngClass]=\"{\n 'text-text-inverse': buttonVariant() === 'default',\n 'text-text-primary': buttonVariant() === 'alternative',\n 'text-accent-contrast': buttonVariant() === 'primary',\n 'text-surface': buttonVariant() === 'success',\n 'text-white': buttonVariant() === 'cta',\n }\"\n ></fx-ui-hero-icon>\n }\n @if (label) {\n <!-- text-box: optical centering \u2014 Archivo's leading floats ink up otherwise -->\n <div class=\"[text-box:trim-both_cap_alphabetic]\">{{ label }}</div>\n }\n </div>\n </ng-container>\n }\n</button>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2474
2583
  }
2475
2584
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ButtonComponent, decorators: [{
2476
2585
  type: Component,
2477
- args: [{ selector: 'fx-ui-button', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, HeroIconComponent], template: "<button\n [attr.type]=\"type()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-busy]=\"loading() || null\"\n [class]=\"class()\"\n [ngClass]=\"{\n 'btn-default': buttonVariant() === 'default',\n 'btn-primary': buttonVariant() === 'primary',\n 'btn-success': buttonVariant() === 'success',\n 'btn-cta': buttonVariant() === 'cta',\n 'btn-alternative': buttonVariant() === 'alternative',\n }\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"onClick()\"\n>\n @if (loading()) {\n <div class=\"flex w-full items-center justify-center\" aria-hidden=\"true\">\n <svg\n [attr.fill]=\"\n buttonVariant() !== 'alternative' ? 'rgb(var(--text-primary))' : 'rgb(var(--primary))'\n \"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <circle cx=\"4\" cy=\"12\" r=\"0\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_lo66\"\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_z0Or\"\n begin=\"spinner_lo66.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"4\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_JsnR\"\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_Aguh\"\n begin=\"spinner_JsnR.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"12\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_hSjk\"\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_UHR2\"\n begin=\"spinner_hSjk.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"20\" cy=\"12\" r=\"3\">\n <animate\n id=\"spinner_4v5M\"\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_OLMs\"\n begin=\"spinner_4v5M.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n </circle>\n </svg>\n </div>\n } @else {\n <ng-container>\n <div class=\"flex items-center justify-center gap-small\">\n @if (icon) {\n <fx-ui-hero-icon\n [icon]=\"icon\"\n [size]=\"16\"\n class=\"flex\"\n [ngClass]=\"{\n 'text-text-inverse': buttonVariant() === 'default',\n 'text-text-primary': buttonVariant() === 'alternative',\n 'text-accent-contrast': buttonVariant() === 'primary',\n 'text-surface': buttonVariant() === 'success',\n 'text-white': buttonVariant() === 'cta',\n }\"\n ></fx-ui-hero-icon>\n }\n @if (label) {\n <!-- text-box: optical centering \u2014 Archivo's leading floats ink up otherwise -->\n <div class=\"[text-box:trim-both_cap_alphabetic]\">{{ label }}</div>\n }\n </div>\n </ng-container>\n }\n</button>\n" }]
2586
+ args: [{ selector: 'fx-ui-button', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, HeroIconComponent], template: "<button\n [attr.type]=\"type()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-busy]=\"loading() || null\"\n [class]=\"class()\"\n [ngClass]=\"{\n 'fx-btn--icon': !!icon && !label,\n 'btn-default': buttonVariant() === 'default',\n 'btn-primary': buttonVariant() === 'primary',\n 'btn-success': buttonVariant() === 'success',\n 'btn-cta': buttonVariant() === 'cta',\n 'btn-alternative': buttonVariant() === 'alternative',\n }\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"onClick()\"\n>\n @if (loading()) {\n <div class=\"flex w-full items-center justify-center\" aria-hidden=\"true\">\n <svg\n [attr.fill]=\"\n buttonVariant() !== 'alternative' ? 'rgb(var(--text-primary))' : 'rgb(var(--primary))'\n \"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <circle cx=\"4\" cy=\"12\" r=\"0\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_lo66\"\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_z0Or\"\n begin=\"spinner_lo66.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"4\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_JsnR\"\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_Aguh\"\n begin=\"spinner_JsnR.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"12\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_hSjk\"\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_UHR2\"\n begin=\"spinner_hSjk.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"20\" cy=\"12\" r=\"3\">\n <animate\n id=\"spinner_4v5M\"\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_OLMs\"\n begin=\"spinner_4v5M.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n </circle>\n </svg>\n </div>\n } @else {\n <ng-container>\n <div class=\"flex items-center justify-center gap-small\">\n @if (icon) {\n <fx-ui-hero-icon\n [icon]=\"icon\"\n [size]=\"16\"\n class=\"flex\"\n [ngClass]=\"{\n 'text-text-inverse': buttonVariant() === 'default',\n 'text-text-primary': buttonVariant() === 'alternative',\n 'text-accent-contrast': buttonVariant() === 'primary',\n 'text-surface': buttonVariant() === 'success',\n 'text-white': buttonVariant() === 'cta',\n }\"\n ></fx-ui-hero-icon>\n }\n @if (label) {\n <!-- text-box: optical centering \u2014 Archivo's leading floats ink up otherwise -->\n <div class=\"[text-box:trim-both_cap_alphabetic]\">{{ label }}</div>\n }\n </div>\n </ng-container>\n }\n</button>\n" }]
2478
2587
  }], propDecorators: { label: [{
2479
2588
  type: Input
2480
2589
  }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], buttonVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonVariant", required: false }] }], icon: [{
@@ -4098,6 +4207,77 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4098
4207
  args: [{ selector: 'fx-ui-sparkbar', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<div\n class=\"flex items-end gap-[2px]\"\n [style.height.px]=\"height()\"\n role=\"img\"\n [attr.aria-label]=\"'Trend: ' + values().join(', ')\"\n>\n @for (bar of bars(); track $index) {\n <div\n class=\"flex-1 min-w-[3px] rounded-[1px] opacity-80\"\n [ngClass]=\"barClass()\"\n [style.height.%]=\"bar\"\n ></div>\n }\n</div>\n" }]
4099
4208
  }], propDecorators: { values: [{ type: i0.Input, args: [{ isSignal: true, alias: "values", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
4100
4209
 
4210
+ /** Users who asked the OS for reduced motion get instant state changes. */
4211
+ function fxMotionAllowed() {
4212
+ return (typeof window !== 'undefined' &&
4213
+ typeof window.matchMedia === 'function' &&
4214
+ !window.matchMedia('(prefers-reduced-motion: reduce)').matches);
4215
+ }
4216
+ /** Shared timing so every screen moves the same way. */
4217
+ const FX_MOTION = {
4218
+ duration: 0.5,
4219
+ /** Delay between consecutive slots. */
4220
+ stagger: 0.06,
4221
+ /** Never let a long list push the last item beyond this delay. */
4222
+ maxSlot: 12,
4223
+ travelPx: 12,
4224
+ ease: 'power2.out',
4225
+ };
4226
+
4227
+ /**
4228
+ * Counts a figure up from 0 (GSAP) whenever its value changes.
4229
+ *
4230
+ * <span [fxCountUp]="card.value"></span>
4231
+ *
4232
+ * Numbers tween over 0.8 s and render with the user's locale grouping; any
4233
+ * non-numeric value ('--', '98 / 100', '1,224') is written verbatim so pending
4234
+ * or composite figures never animate to nonsense. Reduced motion → final value.
4235
+ */
4236
+ class FxCountUpDirective {
4237
+ fxCountUp = input(undefined, ...(ngDevMode ? [{ debugName: "fxCountUp" }] : /* istanbul ignore next */ []));
4238
+ el = inject(ElementRef).nativeElement;
4239
+ tween;
4240
+ state = { v: 0 };
4241
+ constructor() {
4242
+ effect(() => {
4243
+ const raw = this.fxCountUp();
4244
+ const n = typeof raw === 'number' ? raw : Number(raw);
4245
+ const numeric = raw !== '' && raw !== null && raw !== undefined && Number.isFinite(n);
4246
+ this.tween?.kill();
4247
+ if (!numeric) {
4248
+ this.el.textContent = raw === null || raw === undefined ? '' : String(raw);
4249
+ return;
4250
+ }
4251
+ if (!fxMotionAllowed()) {
4252
+ this.state.v = n;
4253
+ this.el.textContent = this.format(n);
4254
+ return;
4255
+ }
4256
+ this.tween = gsap.to(this.state, {
4257
+ v: n,
4258
+ duration: 0.8,
4259
+ ease: 'power2.out',
4260
+ onUpdate: () => (this.el.textContent = this.format(this.state.v)),
4261
+ onComplete: () => (this.el.textContent = this.format(n)),
4262
+ });
4263
+ });
4264
+ }
4265
+ format(v) {
4266
+ return Math.round(v).toLocaleString();
4267
+ }
4268
+ ngOnDestroy() {
4269
+ this.tween?.kill();
4270
+ }
4271
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FxCountUpDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4272
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.20", type: FxCountUpDirective, isStandalone: true, selector: "[fxCountUp]", inputs: { fxCountUp: { classPropertyName: "fxCountUp", publicName: "fxCountUp", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
4273
+ }
4274
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FxCountUpDirective, decorators: [{
4275
+ type: Directive,
4276
+ args: [{
4277
+ selector: '[fxCountUp]',
4278
+ }]
4279
+ }], ctorParameters: () => [], propDecorators: { fxCountUp: [{ type: i0.Input, args: [{ isSignal: true, alias: "fxCountUp", required: false }] }] } });
4280
+
4101
4281
  /** KPI tile row: big number + tinted icon box + caption, in a responsive grid. */
4102
4282
  class StatCardsComponent {
4103
4283
  cards = input([], ...(ngDevMode ? [{ debugName: "cards" }] : /* istanbul ignore next */ []));
@@ -4109,10 +4289,14 @@ class StatCardsComponent {
4109
4289
  variant = input('tile', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
4110
4290
  /** Grid columns; defaults to one column per card. */
4111
4291
  cols;
4292
+ /**
4293
+ * Column count for the ≥ md (720px) container. Below that the CSS takes
4294
+ * over (2-up, then 1-up under 360px) — see stat-cards.component.css — so an
4295
+ * explicit `cols` never squeezes four figures into a phone-width row.
4296
+ */
4112
4297
  get gridColumns() {
4113
- // No explicit cols → responsive auto-fit so tiles stack on narrow viewports.
4114
4298
  if (this.cols === undefined) {
4115
- return 'repeat(auto-fit, minmax(180px, 1fr))';
4299
+ return 'repeat(auto-fit, minmax(min(180px, 100%), 1fr))';
4116
4300
  }
4117
4301
  return `repeat(${this.cols}, minmax(0, 1fr))`;
4118
4302
  }
@@ -4130,7 +4314,7 @@ class StatCardsComponent {
4130
4314
  return card.subTone ? toneText(card.subTone) : 'text-text-secondary';
4131
4315
  }
4132
4316
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StatCardsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4133
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: StatCardsComponent, isStandalone: true, selector: "fx-ui-stat-cards", inputs: { cards: { classPropertyName: "cards", publicName: "cards", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, cols: { classPropertyName: "cols", publicName: "cols", isSignal: false, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"grid gap-normal\" [ngStyle]=\"{ 'grid-template-columns': gridColumns }\">\n @for (card of cards(); track card.label) {\n @if (variant() === 'figure') {\n <div class=\"card-common flex flex-col gap-small min-w-0\">\n <span class=\"txt-utility-caption text-text-secondary truncate\">{{ card.label }}</span>\n <span\n class=\"txt-heading-03\"\n [ngClass]=\"numClass(card)\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card.value }}</span\n >\n @if (card.sub) {\n <span class=\"txt-utility-caption\" [ngClass]=\"subClass(card)\">{{ card.sub }}</span>\n }\n @if (card.spark?.length) {\n <fx-ui-sparkbar class=\"mt-xs\" [values]=\"card.spark!\" [height]=\"16\" [tone]=\"card.tone ?? null\" />\n }\n </div>\n } @else {\n <div class=\"card-common flex items-center gap-normal\">\n @if (card.icon) {\n <div\n class=\"w-10 h-10 rounded-lg flex items-center justify-center shrink-0\"\n [ngClass]=\"boxClass(card)\"\n >\n <fx-ui-hero-icon [icon]=\"card.icon\" [size]=\"20\"></fx-ui-hero-icon>\n </div>\n }\n <div class=\"flex flex-col min-w-0 flex-1\">\n <div class=\"flex items-baseline gap-small\">\n <span\n class=\"txt-heading-04\"\n [ngClass]=\"numClass(card)\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card.value }}</span\n >\n @if (card.delta) {\n <span class=\"text-xs font-semibold\" [ngClass]=\"deltaClass(card)\">\n {{ card.delta.direction === 'up' ? '\u25B2' : '\u25BC' }} {{ card.delta.value }}\n </span>\n }\n </div>\n <span class=\"txt-utility-caption text-text-secondary truncate\">{{ card.label }}</span>\n @if (card.spark?.length) {\n <fx-ui-sparkbar class=\"mt-xs\" [values]=\"card.spark!\" [height]=\"16\" [tone]=\"card.tone ?? null\" />\n }\n </div>\n </div>\n }\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "component", type: SparkbarComponent, selector: "fx-ui-sparkbar", inputs: ["values", "tone", "height"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4317
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: StatCardsComponent, isStandalone: true, selector: "fx-ui-stat-cards", inputs: { cards: { classPropertyName: "cards", publicName: "cards", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, cols: { classPropertyName: "cols", publicName: "cols", isSignal: false, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"fx-stat-grid grid gap-normal\" [ngStyle]=\"{ 'grid-template-columns': gridColumns }\">\n @for (card of cards(); track card.label) {\n @if (variant() === 'figure') {\n <div class=\"card-common flex flex-col gap-small min-w-0\">\n <span class=\"txt-utility-caption text-text-secondary truncate\">{{ card.label }}</span>\n <span\n class=\"txt-heading-03\"\n [ngClass]=\"numClass(card)\"\n style=\"font-family: var(--og-font-figure)\"\n ><span [fxCountUp]=\"card.value\"></span></span\n >\n @if (card.sub) {\n <span class=\"txt-utility-caption\" [ngClass]=\"subClass(card)\">{{ card.sub }}</span>\n }\n @if (card.spark?.length) {\n <fx-ui-sparkbar class=\"mt-xs\" [values]=\"card.spark!\" [height]=\"16\" [tone]=\"card.tone ?? null\" />\n }\n </div>\n } @else {\n <div class=\"card-common flex items-center gap-normal\">\n @if (card.icon) {\n <div\n class=\"w-10 h-10 rounded-lg flex items-center justify-center shrink-0\"\n [ngClass]=\"boxClass(card)\"\n >\n <fx-ui-hero-icon [icon]=\"card.icon\" [size]=\"20\"></fx-ui-hero-icon>\n </div>\n }\n <div class=\"flex flex-col min-w-0 flex-1\">\n <div class=\"flex items-baseline gap-small\">\n <span\n class=\"txt-heading-04\"\n [ngClass]=\"numClass(card)\"\n style=\"font-family: var(--og-font-figure)\"\n ><span [fxCountUp]=\"card.value\"></span></span\n >\n @if (card.delta) {\n <span class=\"text-xs font-semibold\" [ngClass]=\"deltaClass(card)\">\n {{ card.delta.direction === 'up' ? '\u25B2' : '\u25BC' }} {{ card.delta.value }}\n </span>\n }\n </div>\n <span class=\"txt-utility-caption text-text-secondary truncate\">{{ card.label }}</span>\n @if (card.spark?.length) {\n <fx-ui-sparkbar class=\"mt-xs\" [values]=\"card.spark!\" [height]=\"16\" [tone]=\"card.tone ?? null\" />\n }\n </div>\n </div>\n }\n }\n</div>\n", styles: [":host{display:block;container-type:inline-size}@container (width < 720px){.fx-stat-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important}}@container (width < 360px){.fx-stat-grid{grid-template-columns:minmax(0,1fr)!important}}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "component", type: SparkbarComponent, selector: "fx-ui-sparkbar", inputs: ["values", "tone", "height"] }, { kind: "directive", type: FxCountUpDirective, selector: "[fxCountUp]", inputs: ["fxCountUp"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4134
4318
  }
4135
4319
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StatCardsComponent, decorators: [{
4136
4320
  type: Component,
@@ -4139,7 +4323,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4139
4323
  NgClass,
4140
4324
  HeroIconComponent,
4141
4325
  SparkbarComponent,
4142
- ], template: "<div class=\"grid gap-normal\" [ngStyle]=\"{ 'grid-template-columns': gridColumns }\">\n @for (card of cards(); track card.label) {\n @if (variant() === 'figure') {\n <div class=\"card-common flex flex-col gap-small min-w-0\">\n <span class=\"txt-utility-caption text-text-secondary truncate\">{{ card.label }}</span>\n <span\n class=\"txt-heading-03\"\n [ngClass]=\"numClass(card)\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card.value }}</span\n >\n @if (card.sub) {\n <span class=\"txt-utility-caption\" [ngClass]=\"subClass(card)\">{{ card.sub }}</span>\n }\n @if (card.spark?.length) {\n <fx-ui-sparkbar class=\"mt-xs\" [values]=\"card.spark!\" [height]=\"16\" [tone]=\"card.tone ?? null\" />\n }\n </div>\n } @else {\n <div class=\"card-common flex items-center gap-normal\">\n @if (card.icon) {\n <div\n class=\"w-10 h-10 rounded-lg flex items-center justify-center shrink-0\"\n [ngClass]=\"boxClass(card)\"\n >\n <fx-ui-hero-icon [icon]=\"card.icon\" [size]=\"20\"></fx-ui-hero-icon>\n </div>\n }\n <div class=\"flex flex-col min-w-0 flex-1\">\n <div class=\"flex items-baseline gap-small\">\n <span\n class=\"txt-heading-04\"\n [ngClass]=\"numClass(card)\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card.value }}</span\n >\n @if (card.delta) {\n <span class=\"text-xs font-semibold\" [ngClass]=\"deltaClass(card)\">\n {{ card.delta.direction === 'up' ? '\u25B2' : '\u25BC' }} {{ card.delta.value }}\n </span>\n }\n </div>\n <span class=\"txt-utility-caption text-text-secondary truncate\">{{ card.label }}</span>\n @if (card.spark?.length) {\n <fx-ui-sparkbar class=\"mt-xs\" [values]=\"card.spark!\" [height]=\"16\" [tone]=\"card.tone ?? null\" />\n }\n </div>\n </div>\n }\n }\n</div>\n" }]
4326
+ FxCountUpDirective,
4327
+ ], template: "<div class=\"fx-stat-grid grid gap-normal\" [ngStyle]=\"{ 'grid-template-columns': gridColumns }\">\n @for (card of cards(); track card.label) {\n @if (variant() === 'figure') {\n <div class=\"card-common flex flex-col gap-small min-w-0\">\n <span class=\"txt-utility-caption text-text-secondary truncate\">{{ card.label }}</span>\n <span\n class=\"txt-heading-03\"\n [ngClass]=\"numClass(card)\"\n style=\"font-family: var(--og-font-figure)\"\n ><span [fxCountUp]=\"card.value\"></span></span\n >\n @if (card.sub) {\n <span class=\"txt-utility-caption\" [ngClass]=\"subClass(card)\">{{ card.sub }}</span>\n }\n @if (card.spark?.length) {\n <fx-ui-sparkbar class=\"mt-xs\" [values]=\"card.spark!\" [height]=\"16\" [tone]=\"card.tone ?? null\" />\n }\n </div>\n } @else {\n <div class=\"card-common flex items-center gap-normal\">\n @if (card.icon) {\n <div\n class=\"w-10 h-10 rounded-lg flex items-center justify-center shrink-0\"\n [ngClass]=\"boxClass(card)\"\n >\n <fx-ui-hero-icon [icon]=\"card.icon\" [size]=\"20\"></fx-ui-hero-icon>\n </div>\n }\n <div class=\"flex flex-col min-w-0 flex-1\">\n <div class=\"flex items-baseline gap-small\">\n <span\n class=\"txt-heading-04\"\n [ngClass]=\"numClass(card)\"\n style=\"font-family: var(--og-font-figure)\"\n ><span [fxCountUp]=\"card.value\"></span></span\n >\n @if (card.delta) {\n <span class=\"text-xs font-semibold\" [ngClass]=\"deltaClass(card)\">\n {{ card.delta.direction === 'up' ? '\u25B2' : '\u25BC' }} {{ card.delta.value }}\n </span>\n }\n </div>\n <span class=\"txt-utility-caption text-text-secondary truncate\">{{ card.label }}</span>\n @if (card.spark?.length) {\n <fx-ui-sparkbar class=\"mt-xs\" [values]=\"card.spark!\" [height]=\"16\" [tone]=\"card.tone ?? null\" />\n }\n </div>\n </div>\n }\n }\n</div>\n", styles: [":host{display:block;container-type:inline-size}@container (width < 720px){.fx-stat-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important}}@container (width < 360px){.fx-stat-grid{grid-template-columns:minmax(0,1fr)!important}}\n"] }]
4143
4328
  }], propDecorators: { cards: [{ type: i0.Input, args: [{ isSignal: true, alias: "cards", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], cols: [{
4144
4329
  type: Input
4145
4330
  }] } });
@@ -4229,6 +4414,12 @@ class FilterPillsComponent {
4229
4414
  /** Accepts undefined so screens can bind an optional filter-bag key. */
4230
4415
  value = input('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
4231
4416
  valueChange = output();
4417
+ /**
4418
+ * Fill of the ACTIVE pill. 'accent' (default, design 11 vulnerabilities);
4419
+ * 'solid' = `--og-text` on inverse text — the demo's Attack-paths panel on
4420
+ * the Overview tab (design 02 "All paths 164") and Recent analyses (design 13).
4421
+ */
4422
+ tone = input('accent', ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
4232
4423
  isActive(option) {
4233
4424
  return (this.value() ?? '') === option.value;
4234
4425
  }
@@ -4239,20 +4430,23 @@ class FilterPillsComponent {
4239
4430
  * trạng thái chọn của filter (anh Tú chốt 03/09).
4240
4431
  */
4241
4432
  pillClass(option) {
4242
- return this.isActive(option)
4243
- ? 'bg-accent text-accent-contrast border-accent'
4244
- : 'border-border-default text-text-secondary hover:border-border-strong';
4433
+ if (!this.isActive(option)) {
4434
+ return 'border-border-default text-text-secondary hover:border-border-strong';
4435
+ }
4436
+ return this.tone() === 'solid'
4437
+ ? 'bg-text-primary text-text-inverse border-text-primary'
4438
+ : 'bg-accent text-accent-contrast border-accent';
4245
4439
  }
4246
4440
  countClass(option) {
4247
4441
  return this.isActive(option) ? 'opacity-70' : 'text-text-placeholder';
4248
4442
  }
4249
4443
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FilterPillsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4250
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: FilterPillsComponent, isStandalone: true, selector: "fx-ui-filter-pills", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<div class=\"flex flex-wrap items-center gap-small\">\n @for (option of options(); track option.value) {\n <button\n type=\"button\"\n [attr.aria-pressed]=\"isActive(option)\"\n class=\"flex items-center gap-small px-semi py-xs rounded-full border txt-utility-caption font-bold cursor-pointer transition-colors\"\n [ngClass]=\"pillClass(option)\"\n (click)=\"valueChange.emit(option.value)\"\n >\n {{ option.label }}\n @if (option.count !== null && option.count !== undefined) {\n <span [ngClass]=\"countClass(option)\" style=\"font-family: var(--og-font-figure)\">{{ option.count }}</span>\n }\n </button>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4444
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: FilterPillsComponent, isStandalone: true, selector: "fx-ui-filter-pills", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<div class=\"flex flex-wrap items-center gap-small\">\n @for (option of options(); track option.value) {\n <button\n type=\"button\"\n [attr.aria-pressed]=\"isActive(option)\"\n class=\"flex items-center gap-small px-[11px] py-[4px] rounded-full border txt-utility-caption font-bold cursor-pointer transition-colors\"\n [ngClass]=\"pillClass(option)\"\n (click)=\"valueChange.emit(option.value)\"\n >\n {{ option.label }}\n @if (option.count !== null && option.count !== undefined) {\n <span [ngClass]=\"countClass(option)\" style=\"font-family: var(--og-font-figure)\">{{ option.count }}</span>\n }\n </button>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4251
4445
  }
4252
4446
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FilterPillsComponent, decorators: [{
4253
4447
  type: Component,
4254
- args: [{ selector: 'fx-ui-filter-pills', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<div class=\"flex flex-wrap items-center gap-small\">\n @for (option of options(); track option.value) {\n <button\n type=\"button\"\n [attr.aria-pressed]=\"isActive(option)\"\n class=\"flex items-center gap-small px-semi py-xs rounded-full border txt-utility-caption font-bold cursor-pointer transition-colors\"\n [ngClass]=\"pillClass(option)\"\n (click)=\"valueChange.emit(option.value)\"\n >\n {{ option.label }}\n @if (option.count !== null && option.count !== undefined) {\n <span [ngClass]=\"countClass(option)\" style=\"font-family: var(--og-font-figure)\">{{ option.count }}</span>\n }\n </button>\n }\n</div>\n" }]
4255
- }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
4448
+ args: [{ selector: 'fx-ui-filter-pills', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<div class=\"flex flex-wrap items-center gap-small\">\n @for (option of options(); track option.value) {\n <button\n type=\"button\"\n [attr.aria-pressed]=\"isActive(option)\"\n class=\"flex items-center gap-small px-[11px] py-[4px] rounded-full border txt-utility-caption font-bold cursor-pointer transition-colors\"\n [ngClass]=\"pillClass(option)\"\n (click)=\"valueChange.emit(option.value)\"\n >\n {{ option.label }}\n @if (option.count !== null && option.count !== undefined) {\n <span [ngClass]=\"countClass(option)\" style=\"font-family: var(--og-font-figure)\">{{ option.count }}</span>\n }\n </button>\n }\n</div>\n" }]
4449
+ }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }] } });
4256
4450
 
4257
4451
  /**
4258
4452
  * V2 stage progress dots — the walkthrough position indicator on the attack
@@ -4473,11 +4667,11 @@ class SeverityBadgesComponent {
4473
4667
  return badge.tone ? toneBadge(badge.tone) : 'bg-bg-hover text-text-primary';
4474
4668
  }
4475
4669
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SeverityBadgesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4476
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SeverityBadgesComponent, isStandalone: true, selector: "fx-ui-severity-badges", inputs: { stats: "stats", showZero: "showZero", suffixed: "suffixed" }, outputs: { badgeClick: "badgeClick" }, ngImport: i0, template: "<div class=\"flex items-center gap-small\">\n @for (badge of badges; track badge.key) {\n <button\n type=\"button\"\n class=\"px-2 py-0.5 rounded-full txt-utility-caption font-medium cursor-pointer focus-visible:ring-2 focus-visible:ring-primary/50\"\n [ngClass]=\"badgeClass(badge)\"\n [attr.aria-label]=\"badge.key + ': ' + badge.label\"\n (click)=\"badgeClick.emit(badge.key)\"\n >{{ badge.label }}</button>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4670
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SeverityBadgesComponent, isStandalone: true, selector: "fx-ui-severity-badges", inputs: { stats: "stats", showZero: "showZero", suffixed: "suffixed" }, outputs: { badgeClick: "badgeClick" }, ngImport: i0, template: "<div class=\"flex items-center gap-small\">\n @for (badge of badges; track badge.key) {\n <button\n type=\"button\"\n class=\"px-2 py-[3px] rounded-full txt-utility-caption font-medium cursor-pointer focus-visible:ring-2 focus-visible:ring-primary/50\"\n [ngClass]=\"badgeClass(badge)\"\n [attr.aria-label]=\"badge.key + ': ' + badge.label\"\n (click)=\"badgeClick.emit(badge.key)\"\n >{{ badge.label }}</button>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4477
4671
  }
4478
4672
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SeverityBadgesComponent, decorators: [{
4479
4673
  type: Component,
4480
- args: [{ selector: 'fx-ui-severity-badges', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<div class=\"flex items-center gap-small\">\n @for (badge of badges; track badge.key) {\n <button\n type=\"button\"\n class=\"px-2 py-0.5 rounded-full txt-utility-caption font-medium cursor-pointer focus-visible:ring-2 focus-visible:ring-primary/50\"\n [ngClass]=\"badgeClass(badge)\"\n [attr.aria-label]=\"badge.key + ': ' + badge.label\"\n (click)=\"badgeClick.emit(badge.key)\"\n >{{ badge.label }}</button>\n }\n</div>\n" }]
4674
+ args: [{ selector: 'fx-ui-severity-badges', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<div class=\"flex items-center gap-small\">\n @for (badge of badges; track badge.key) {\n <button\n type=\"button\"\n class=\"px-2 py-[3px] rounded-full txt-utility-caption font-medium cursor-pointer focus-visible:ring-2 focus-visible:ring-primary/50\"\n [ngClass]=\"badgeClass(badge)\"\n [attr.aria-label]=\"badge.key + ': ' + badge.label\"\n (click)=\"badgeClick.emit(badge.key)\"\n >{{ badge.label }}</button>\n }\n</div>\n" }]
4481
4675
  }], propDecorators: { stats: [{
4482
4676
  type: Input
4483
4677
  }], showZero: [{
@@ -4530,12 +4724,33 @@ class MasterDetailComponent {
4530
4724
  /** Caller-translated Prev/Next labels ("X of Y" is numeric). */
4531
4725
  prevLabel = input('Prev', ...(ngDevMode ? [{ debugName: "prevLabel" }] : /* istanbul ignore next */ []));
4532
4726
  nextLabel = input('Next', ...(ngDevMode ? [{ debugName: "nextLabel" }] : /* istanbul ignore next */ []));
4727
+ /** "Page 1 of 4" (design 11/14/17) — the two words, caller-translated. */
4728
+ pageLabel = input('Page', ...(ngDevMode ? [{ debugName: "pageLabel" }] : /* istanbul ignore next */ []));
4729
+ ofLabel = input('of', ...(ngDevMode ? [{ debugName: "ofLabel" }] : /* istanbul ignore next */ []));
4730
+ /**
4731
+ * Facets rail open (design 11/14) or collapsed to a 40px strip (design 17).
4732
+ * The user can toggle either way; this only sets the initial state.
4733
+ */
4734
+ facetsOpen = input(true, ...(ngDevMode ? [{ debugName: "facetsOpen" }] : /* istanbul ignore next */ []));
4735
+ /** Caller-translated rail heading ("Scope & facets"). */
4736
+ facetsLabel = input('Scope & facets', ...(ngDevMode ? [{ debugName: "facetsLabel" }] : /* istanbul ignore next */ []));
4737
+ expandFacetsLabel = input('Show scope & facets', ...(ngDevMode ? [{ debugName: "expandFacetsLabel" }] : /* istanbul ignore next */ []));
4738
+ collapseFacetsLabel = input('Hide scope & facets', ...(ngDevMode ? [{ debugName: "collapseFacetsLabel" }] : /* istanbul ignore next */ []));
4739
+ /** Current rail state — follows `facetsOpen` until the user toggles. */
4740
+ railOpen = linkedSignal(() => this.facetsOpen(), ...(ngDevMode ? [{ debugName: "railOpen" }] : /* istanbul ignore next */ []));
4533
4741
  rowTemplate = contentChild.required('row');
4534
4742
  detailTemplate = contentChild.required('detail');
4535
4743
  paginator = viewChild.required(MatPaginator);
4536
4744
  uid = `fx-md-${nextUid++}`;
4537
4745
  ngAfterContentInit() {
4538
4746
  this.hasFacets = !!this.hostEl.nativeElement.querySelector('[mdFacets]');
4747
+ // Stacked layout (container narrower than md = 720px): start with the rail
4748
+ // collapsed so the list is the first thing on screen — an open rail with
4749
+ // six facet groups pushed the list a full screen down on phones
4750
+ // (anh Tú 04/09 mobile audit). The user can still expand it.
4751
+ if (this.hasFacets && this.hostEl.nativeElement.clientWidth > 0 && this.hostEl.nativeElement.clientWidth < 720) {
4752
+ this.railOpen.set(false);
4753
+ }
4539
4754
  }
4540
4755
  ngAfterViewInit() {
4541
4756
  // The paginator lives in this view, not the host page's, so the host's
@@ -4545,16 +4760,13 @@ class MasterDetailComponent {
4545
4760
  this.table.paginator = this.paginator();
4546
4761
  }
4547
4762
  }
4548
- /** "X of Y" range text for the prevnext pagination style. */
4763
+ /** "Page X of Y" for the prevnext pagination style (design 11/14/17). */
4549
4764
  rangeText() {
4550
4765
  const total = this.table.total ?? 0;
4551
- if (!total)
4552
- return '0 of 0';
4553
- const page = this.paginator().pageIndex;
4554
- const size = this.table.pageSize;
4555
- const from = page * size + 1;
4556
- const to = Math.min((page + 1) * size, total);
4557
- return `${from}–${to} of ${total}`;
4766
+ const size = this.table.pageSize || 1;
4767
+ const pages = Math.max(1, Math.ceil(total / size));
4768
+ const page = Math.min(pages, (this.paginator().pageIndex ?? 0) + 1);
4769
+ return `${this.pageLabel()} ${page} ${this.ofLabel()} ${pages}`;
4558
4770
  }
4559
4771
  isSelected(row) {
4560
4772
  return this.table.idOf(row) === this.table.selectedId;
@@ -4594,7 +4806,7 @@ class MasterDetailComponent {
4594
4806
  el?.scrollIntoView({ block: 'nearest' });
4595
4807
  }
4596
4808
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: MasterDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4597
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: MasterDetailComponent, isStandalone: true, selector: "fx-ui-master-detail", inputs: { table: { classPropertyName: "table", publicName: "table", isSignal: false, isRequired: false, transformFunction: null }, listWidth: { classPropertyName: "listWidth", publicName: "listWidth", isSignal: true, isRequired: false, transformFunction: null }, paneHeight: { classPropertyName: "paneHeight", publicName: "paneHeight", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, listLabel: { classPropertyName: "listLabel", publicName: "listLabel", isSignal: true, isRequired: false, transformFunction: null }, skeletonCards: { classPropertyName: "skeletonCards", publicName: "skeletonCards", isSignal: true, isRequired: false, transformFunction: null }, paginationStyle: { classPropertyName: "paginationStyle", publicName: "paginationStyle", isSignal: true, isRequired: false, transformFunction: null }, prevLabel: { classPropertyName: "prevLabel", publicName: "prevLabel", isSignal: true, isRequired: false, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "rowTemplate", first: true, predicate: ["row"], descendants: true, isSignal: true }, { propertyName: "detailTemplate", first: true, predicate: ["detail"], descendants: true, isSignal: true }], viewQueries: [{ propertyName: "paginator", first: true, predicate: MatPaginator, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- 3 c\u1EE5m CHUNG chi\u1EC1u cao, m\u1ED7i c\u1EE5m SCROLL RI\u00CANG (anh T\u00FA ch\u1ED1t 03/09).\n `items-stretch` + chi\u1EC1u cao \u0111\u1EB7t tr\u00EAn h\u00E0ng (xem .fx-md-row trong CSS) \u0111\u1EC3\n c\u1EA3 ba pane b\u1EB1ng nhau; m\u1ED7i pane l\u00E0 flex-col + min-h-0 v\u00E0 ch\u1EC9 ph\u1EA7n th\u00E2n\n `overflow-auto flex-1` cu\u1ED9n \u2014 header/filter/paginator \u0111\u1EE9ng y\u00EAn.\n \u26A0\uFE0F H\u01B0\u1EDBng h\u00E0ng + chi\u1EC1u cao do CONTAINER QUERY trong CSS quy\u1EBFt \u0111\u1ECBnh (theo\n b\u1EC1 r\u1ED9ng c\u1EE7a ch\u00EDnh h\u00E0ng), KH\u00D4NG ph\u1EA3i `lg:flex-row`. \u0110\u1EEBng th\u00EAm l\u1EA1i class\n `lg:*` \u1EDF \u0111\u00E2y \u2014 `lg` trong lib l\u00E0 1080px v\u00E0 viewport l\u00E0 proxy sai; xem\n ghi ch\u00FA \u0111\u1EA7u master-detail.component.css. -->\n<div\n class=\"fx-md-row flex flex-col items-stretch gap-xl\"\n [style.--fx-md-list-width]=\"listWidth()\"\n [style.--fx-md-pane-h]=\"paneHeight()\"\n>\n <!-- Facets rail (V2 3-column lookup) \u2014 wrapper always rendered so ng-content\n stays unconditional; hidden when the host projects nothing. -->\n <div\n class=\"fx-md-facets card-common bg-surface flex flex-col min-h-0\"\n [class.hidden]=\"!hasFacets\"\n >\n <div class=\"flex-1 min-h-0 overflow-auto pr-xs\">\n <ng-content select=\"[mdFacets]\"></ng-content>\n </div>\n </div>\n\n <!-- Master: list pane -->\n <div class=\"fx-md-list card-common flex flex-col min-h-0\">\n <ng-content select=\"[mdHeader]\"></ng-content>\n <ng-content select=\"[mdFilters]\"></ng-content>\n <div\n class=\"flex flex-col gap-normal overflow-auto pr-xs flex-1 min-h-0\"\n role=\"listbox\"\n [attr.aria-label]=\"listLabel() || null\"\n >\n @if (table.loading) {\n <fx-ui-skeleton-list [rows]=\"table.pageSize\"></fx-ui-skeleton-list>\n } @else {\n @for (row of table.dataSource.data; track table.idOf(row); let i = $index) {\n <button\n type=\"button\"\n role=\"option\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"isSelected(row)\"\n [tabindex]=\"isSelected(row) ? 0 : -1\"\n (click)=\"table.selectItem(row)\"\n (keydown)=\"onKeydown($event, i)\"\n class=\"text-left rounded-lg border p-semi transition-colors\"\n [ngClass]=\"\n isSelected(row)\n ? 'border-accent bg-accent-soft/60'\n : 'border-border-default hover:border-border-interactive bg-bg-primary'\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"rowTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: row, selected: isSelected(row) }\"\n ></ng-container>\n </button>\n }\n @if (!table.dataSource.data.length) {\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n }\n }\n </div>\n <!-- No (page) binding: ngAfterViewInit hands this paginator to the table,\n whose own BaseTable subscription (running after this child's hook)\n wires page events \u2014 a binding here would double-fetch every change. -->\n <mat-paginator\n class=\"mt-normal\"\n [class.fx-md-paginator-hidden]=\"paginationStyle() === 'prevnext'\"\n [length]=\"table.total\"\n [pageSize]=\"table.pageSize\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n ></mat-paginator>\n @if (paginationStyle() === 'prevnext') {\n <div class=\"flex items-center justify-between mt-normal\">\n <span class=\"txt-utility-description\" style=\"font-family: var(--og-font-figure)\">\n {{ rangeText() }}\n </span>\n <div class=\"flex gap-small\">\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasPreviousPage()\"\n (click)=\"paginator().previousPage()\"\n >\n {{ prevLabel() }}\n </button>\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasNextPage()\"\n (click)=\"paginator().nextPage()\"\n >\n {{ nextLabel() }}\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Detail pane -->\n <div class=\"fx-md-detail w-full min-w-0 flex flex-col min-h-0\">\n <div class=\"flex-1 min-h-0 overflow-auto\">\n @if (table.selected) {\n @if (table.detailBusy) {\n <fx-ui-skeleton-detail [cards]=\"skeletonCards()\"></fx-ui-skeleton-detail>\n } @else {\n <ng-container\n [ngTemplateOutlet]=\"detailTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: table.detail, selected: table.selected }\"\n ></ng-container>\n }\n } @else {\n <div class=\"card-common flex items-center justify-center h-full min-h-[300px]\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n </div>\n </div>\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-md}.fx-md-list,.fx-md-facets{width:100%}@container fx-md (min-width: 968px){.fx-md-row{flex-direction:row;height:var(--fx-md-pane-h, calc(100vh - 300px) );min-height:420px}.fx-md-facets{flex:0 1 230px;min-width:200px;max-width:260px}.fx-md-list{width:auto;flex:0 1 var(--fx-md-list-width, 420px);min-width:320px;max-width:var(--fx-md-list-width, 420px)}.fx-md-detail{flex:1 1 400px;min-width:400px}}.fx-md-paginator-hidden{position:absolute!important;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}\n"], dependencies: [{ kind: "component", type: SkeletonListComponent, selector: "fx-ui-skeleton-list", inputs: ["rows"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: EmptyStateComponent, selector: "fx-ui-empty-state", inputs: ["message", "icon", "height"] }, { kind: "component", type: MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "component", type: SkeletonDetailComponent, selector: "fx-ui-skeleton-detail", inputs: ["cards", "rows"] }] });
4809
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: MasterDetailComponent, isStandalone: true, selector: "fx-ui-master-detail", inputs: { table: { classPropertyName: "table", publicName: "table", isSignal: false, isRequired: false, transformFunction: null }, listWidth: { classPropertyName: "listWidth", publicName: "listWidth", isSignal: true, isRequired: false, transformFunction: null }, paneHeight: { classPropertyName: "paneHeight", publicName: "paneHeight", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, listLabel: { classPropertyName: "listLabel", publicName: "listLabel", isSignal: true, isRequired: false, transformFunction: null }, skeletonCards: { classPropertyName: "skeletonCards", publicName: "skeletonCards", isSignal: true, isRequired: false, transformFunction: null }, paginationStyle: { classPropertyName: "paginationStyle", publicName: "paginationStyle", isSignal: true, isRequired: false, transformFunction: null }, prevLabel: { classPropertyName: "prevLabel", publicName: "prevLabel", isSignal: true, isRequired: false, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null }, pageLabel: { classPropertyName: "pageLabel", publicName: "pageLabel", isSignal: true, isRequired: false, transformFunction: null }, ofLabel: { classPropertyName: "ofLabel", publicName: "ofLabel", isSignal: true, isRequired: false, transformFunction: null }, facetsOpen: { classPropertyName: "facetsOpen", publicName: "facetsOpen", isSignal: true, isRequired: false, transformFunction: null }, facetsLabel: { classPropertyName: "facetsLabel", publicName: "facetsLabel", isSignal: true, isRequired: false, transformFunction: null }, expandFacetsLabel: { classPropertyName: "expandFacetsLabel", publicName: "expandFacetsLabel", isSignal: true, isRequired: false, transformFunction: null }, collapseFacetsLabel: { classPropertyName: "collapseFacetsLabel", publicName: "collapseFacetsLabel", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "rowTemplate", first: true, predicate: ["row"], descendants: true, isSignal: true }, { propertyName: "detailTemplate", first: true, predicate: ["detail"], descendants: true, isSignal: true }], viewQueries: [{ propertyName: "paginator", first: true, predicate: MatPaginator, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- ONE frame, three columns (designs 11/14/17): facets rail \u00B7 list \u00B7 detail,\n separated by hairlines, sharing one height; only each pane's body scrolls.\n Row direction / height come from the CONTAINER QUERY in the CSS (the row's\n own width), never from `lg:*` \u2014 see the note at the top of the .css. -->\n<div\n class=\"fx-md-row flex flex-col items-stretch\"\n [class.fx-md-row--rail-open]=\"railOpen()\"\n [style.--fx-md-list-width]=\"listWidth()\"\n [style.--fx-md-pane-h]=\"paneHeight()\"\n>\n <!-- Facets rail \u2014 collapsible (\u00BB / \u00AB), collapsed = 40px strip with the label\n set vertically. The [mdFacets] projection is unconditional (hasFacets is\n detected by querying it) and only hidden while collapsed. -->\n <aside\n class=\"fx-md-facets flex flex-col min-h-0 min-w-0\"\n [class.hidden]=\"!hasFacets\"\n [class.fx-md-facets--open]=\"railOpen()\"\n >\n <div class=\"fx-md-rail-head flex items-center gap-small px-normal py-semi border-b border-border-subtle shrink-0\" [class.hidden]=\"!railOpen()\">\n <button\n type=\"button\"\n class=\"flex items-center justify-center w-6 h-6 rounded-control text-muted hover:bg-bg-hover shrink-0\"\n [attr.aria-label]=\"collapseFacetsLabel()\"\n (click)=\"railOpen.set(false)\"\n >\n <fx-ui-hero-icon icon=\"chevron-double-left\" [size]=\"14\" class=\"flex\" />\n </button>\n <span class=\"txt-eyebrow truncate\">{{ facetsLabel() }}</span>\n </div>\n <div class=\"flex-1 min-h-0 overflow-auto px-normal py-normal\" [class.hidden]=\"!railOpen()\">\n <ng-content select=\"[mdFacets]\"></ng-content>\n </div>\n <button\n type=\"button\"\n class=\"fx-md-rail-toggle flex items-center gap-normal text-muted hover:bg-bg-hover transition-colors\"\n [class.hidden]=\"railOpen()\"\n [attr.aria-label]=\"expandFacetsLabel()\"\n [attr.aria-expanded]=\"false\"\n (click)=\"railOpen.set(true)\"\n >\n <fx-ui-hero-icon icon=\"chevron-double-right\" [size]=\"14\" class=\"flex shrink-0\" />\n <span class=\"fx-md-rail-label txt-eyebrow whitespace-nowrap\">{{ facetsLabel() }}</span>\n </button>\n </aside>\n\n <!-- Master: list pane -->\n <section class=\"fx-md-list flex flex-col min-h-0 min-w-0\">\n <div class=\"fx-md-list-head px-large pt-large pb-small border-b border-border-subtle shrink-0\">\n <ng-content select=\"[mdHeader]\"></ng-content>\n <ng-content select=\"[mdFilters]\"></ng-content>\n </div>\n <div\n class=\"flex flex-col overflow-auto flex-1 min-h-0\"\n role=\"listbox\"\n [attr.aria-label]=\"listLabel() || null\"\n >\n @if (table.loading) {\n <div class=\"px-large py-normal\">\n <fx-ui-skeleton-list [rows]=\"table.pageSize\"></fx-ui-skeleton-list>\n </div>\n } @else {\n @for (row of table.dataSource.data; track table.idOf(row); let i = $index) {\n <!-- Hairline rows; the selected one is tinted with an accent bar on\n the left (design 11/14 selected finding / component). -->\n <button\n type=\"button\"\n role=\"option\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"isSelected(row)\"\n [tabindex]=\"isSelected(row) ? 0 : -1\"\n (click)=\"table.selectItem(row)\"\n (keydown)=\"onKeydown($event, i)\"\n class=\"text-left px-large py-semi border-l-2 border-b border-b-border-subtle transition-colors\"\n [ngClass]=\"\n isSelected(row)\n ? 'border-l-accent bg-accent-soft/60'\n : 'border-l-transparent hover:bg-surface-sunken'\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"rowTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: row, selected: isSelected(row) }\"\n ></ng-container>\n </button>\n }\n @if (!table.dataSource.data.length) {\n <div class=\"px-large py-large\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n }\n </div>\n <!-- No (page) binding: ngAfterViewInit hands this paginator to the table,\n whose own BaseTable subscription (running after this child's hook)\n wires page events \u2014 a binding here would double-fetch every change. -->\n <mat-paginator\n class=\"fx-md-paginator\"\n [class.fx-md-paginator-hidden]=\"paginationStyle() === 'prevnext'\"\n [length]=\"table.total\"\n [pageSize]=\"table.pageSize\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n ></mat-paginator>\n @if (paginationStyle() === 'prevnext') {\n <div class=\"flex items-center justify-between gap-normal px-large py-normal border-t border-border-subtle shrink-0\">\n <span class=\"txt-utility-description\" style=\"font-family: var(--og-font-figure)\">\n {{ rangeText() }}\n </span>\n <div class=\"flex gap-small\">\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasPreviousPage()\"\n (click)=\"paginator().previousPage()\"\n >\n {{ prevLabel() }}\n </button>\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasNextPage()\"\n (click)=\"paginator().nextPage()\"\n >\n {{ nextLabel() }}\n </button>\n </div>\n </div>\n }\n </section>\n\n <!-- Detail pane -->\n <section class=\"fx-md-detail w-full min-w-0 flex flex-col min-h-0\">\n <div class=\"flex-1 min-h-0 overflow-auto px-large py-large\">\n @if (table.selected) {\n @if (table.detailBusy) {\n <fx-ui-skeleton-detail [cards]=\"skeletonCards()\"></fx-ui-skeleton-detail>\n } @else {\n <ng-container\n [ngTemplateOutlet]=\"detailTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: table.detail, selected: table.selected }\"\n ></ng-container>\n }\n } @else {\n <div class=\"flex items-center justify-center h-full min-h-[300px]\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n </div>\n </section>\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-md}.fx-md-row{background:rgb(var(--og-surface));border:1px solid rgb(var(--border-default));border-radius:.5rem;overflow:hidden;box-shadow:0 1px 2px #0000000a,0 3px 8px #0000000d}.fx-md-facets,.fx-md-list{border-bottom:1px solid rgb(var(--og-border-subtle))}.fx-md-rail-toggle{width:100%;padding:10px 12px;flex-direction:row;justify-content:flex-start}.fx-md-list,.fx-md-facets{width:100%}@container fx-md (min-width: 720px){.fx-md-row{flex-direction:row;height:var(--fx-md-pane-h, calc(100vh - 300px) );min-height:420px}.fx-md-facets,.fx-md-list{border-bottom:0;border-right:1px solid rgb(var(--og-border-subtle))}.fx-md-facets{flex:0 0 40px;width:40px;min-width:40px;max-width:40px}.fx-md-facets--open{flex:0 1 230px;width:auto;min-width:200px;max-width:260px}.fx-md-rail-toggle{height:100%;padding:12px 0;flex-direction:column;justify-content:flex-start}.fx-md-rail-label{writing-mode:vertical-rl;transform:rotate(180deg)}.fx-md-list{width:auto;flex:0 1 var(--fx-md-list-width, 420px);min-width:260px;max-width:var(--fx-md-list-width, 420px)}.fx-md-detail{flex:1 1 360px;min-width:360px}}.fx-md-paginator-hidden{position:absolute!important;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}.fx-md-paginator{padding:0 12px;border-top:1px solid rgb(var(--og-border-subtle))}\n"], dependencies: [{ kind: "component", type: SkeletonListComponent, selector: "fx-ui-skeleton-list", inputs: ["rows"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: EmptyStateComponent, selector: "fx-ui-empty-state", inputs: ["message", "icon", "height"] }, { kind: "component", type: MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "component", type: SkeletonDetailComponent, selector: "fx-ui-skeleton-detail", inputs: ["cards", "rows"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }] });
4598
4810
  }
4599
4811
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: MasterDetailComponent, decorators: [{
4600
4812
  type: Component,
@@ -4605,10 +4817,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4605
4817
  EmptyStateComponent,
4606
4818
  MatPaginator,
4607
4819
  SkeletonDetailComponent,
4608
- ], template: "<!-- 3 c\u1EE5m CHUNG chi\u1EC1u cao, m\u1ED7i c\u1EE5m SCROLL RI\u00CANG (anh T\u00FA ch\u1ED1t 03/09).\n `items-stretch` + chi\u1EC1u cao \u0111\u1EB7t tr\u00EAn h\u00E0ng (xem .fx-md-row trong CSS) \u0111\u1EC3\n c\u1EA3 ba pane b\u1EB1ng nhau; m\u1ED7i pane l\u00E0 flex-col + min-h-0 v\u00E0 ch\u1EC9 ph\u1EA7n th\u00E2n\n `overflow-auto flex-1` cu\u1ED9n \u2014 header/filter/paginator \u0111\u1EE9ng y\u00EAn.\n \u26A0\uFE0F H\u01B0\u1EDBng h\u00E0ng + chi\u1EC1u cao do CONTAINER QUERY trong CSS quy\u1EBFt \u0111\u1ECBnh (theo\n b\u1EC1 r\u1ED9ng c\u1EE7a ch\u00EDnh h\u00E0ng), KH\u00D4NG ph\u1EA3i `lg:flex-row`. \u0110\u1EEBng th\u00EAm l\u1EA1i class\n `lg:*` \u1EDF \u0111\u00E2y \u2014 `lg` trong lib l\u00E0 1080px v\u00E0 viewport l\u00E0 proxy sai; xem\n ghi ch\u00FA \u0111\u1EA7u master-detail.component.css. -->\n<div\n class=\"fx-md-row flex flex-col items-stretch gap-xl\"\n [style.--fx-md-list-width]=\"listWidth()\"\n [style.--fx-md-pane-h]=\"paneHeight()\"\n>\n <!-- Facets rail (V2 3-column lookup) \u2014 wrapper always rendered so ng-content\n stays unconditional; hidden when the host projects nothing. -->\n <div\n class=\"fx-md-facets card-common bg-surface flex flex-col min-h-0\"\n [class.hidden]=\"!hasFacets\"\n >\n <div class=\"flex-1 min-h-0 overflow-auto pr-xs\">\n <ng-content select=\"[mdFacets]\"></ng-content>\n </div>\n </div>\n\n <!-- Master: list pane -->\n <div class=\"fx-md-list card-common flex flex-col min-h-0\">\n <ng-content select=\"[mdHeader]\"></ng-content>\n <ng-content select=\"[mdFilters]\"></ng-content>\n <div\n class=\"flex flex-col gap-normal overflow-auto pr-xs flex-1 min-h-0\"\n role=\"listbox\"\n [attr.aria-label]=\"listLabel() || null\"\n >\n @if (table.loading) {\n <fx-ui-skeleton-list [rows]=\"table.pageSize\"></fx-ui-skeleton-list>\n } @else {\n @for (row of table.dataSource.data; track table.idOf(row); let i = $index) {\n <button\n type=\"button\"\n role=\"option\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"isSelected(row)\"\n [tabindex]=\"isSelected(row) ? 0 : -1\"\n (click)=\"table.selectItem(row)\"\n (keydown)=\"onKeydown($event, i)\"\n class=\"text-left rounded-lg border p-semi transition-colors\"\n [ngClass]=\"\n isSelected(row)\n ? 'border-accent bg-accent-soft/60'\n : 'border-border-default hover:border-border-interactive bg-bg-primary'\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"rowTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: row, selected: isSelected(row) }\"\n ></ng-container>\n </button>\n }\n @if (!table.dataSource.data.length) {\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n }\n }\n </div>\n <!-- No (page) binding: ngAfterViewInit hands this paginator to the table,\n whose own BaseTable subscription (running after this child's hook)\n wires page events \u2014 a binding here would double-fetch every change. -->\n <mat-paginator\n class=\"mt-normal\"\n [class.fx-md-paginator-hidden]=\"paginationStyle() === 'prevnext'\"\n [length]=\"table.total\"\n [pageSize]=\"table.pageSize\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n ></mat-paginator>\n @if (paginationStyle() === 'prevnext') {\n <div class=\"flex items-center justify-between mt-normal\">\n <span class=\"txt-utility-description\" style=\"font-family: var(--og-font-figure)\">\n {{ rangeText() }}\n </span>\n <div class=\"flex gap-small\">\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasPreviousPage()\"\n (click)=\"paginator().previousPage()\"\n >\n {{ prevLabel() }}\n </button>\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasNextPage()\"\n (click)=\"paginator().nextPage()\"\n >\n {{ nextLabel() }}\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Detail pane -->\n <div class=\"fx-md-detail w-full min-w-0 flex flex-col min-h-0\">\n <div class=\"flex-1 min-h-0 overflow-auto\">\n @if (table.selected) {\n @if (table.detailBusy) {\n <fx-ui-skeleton-detail [cards]=\"skeletonCards()\"></fx-ui-skeleton-detail>\n } @else {\n <ng-container\n [ngTemplateOutlet]=\"detailTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: table.detail, selected: table.selected }\"\n ></ng-container>\n }\n } @else {\n <div class=\"card-common flex items-center justify-center h-full min-h-[300px]\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n </div>\n </div>\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-md}.fx-md-list,.fx-md-facets{width:100%}@container fx-md (min-width: 968px){.fx-md-row{flex-direction:row;height:var(--fx-md-pane-h, calc(100vh - 300px) );min-height:420px}.fx-md-facets{flex:0 1 230px;min-width:200px;max-width:260px}.fx-md-list{width:auto;flex:0 1 var(--fx-md-list-width, 420px);min-width:320px;max-width:var(--fx-md-list-width, 420px)}.fx-md-detail{flex:1 1 400px;min-width:400px}}.fx-md-paginator-hidden{position:absolute!important;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}\n"] }]
4820
+ HeroIconComponent,
4821
+ ], template: "<!-- ONE frame, three columns (designs 11/14/17): facets rail \u00B7 list \u00B7 detail,\n separated by hairlines, sharing one height; only each pane's body scrolls.\n Row direction / height come from the CONTAINER QUERY in the CSS (the row's\n own width), never from `lg:*` \u2014 see the note at the top of the .css. -->\n<div\n class=\"fx-md-row flex flex-col items-stretch\"\n [class.fx-md-row--rail-open]=\"railOpen()\"\n [style.--fx-md-list-width]=\"listWidth()\"\n [style.--fx-md-pane-h]=\"paneHeight()\"\n>\n <!-- Facets rail \u2014 collapsible (\u00BB / \u00AB), collapsed = 40px strip with the label\n set vertically. The [mdFacets] projection is unconditional (hasFacets is\n detected by querying it) and only hidden while collapsed. -->\n <aside\n class=\"fx-md-facets flex flex-col min-h-0 min-w-0\"\n [class.hidden]=\"!hasFacets\"\n [class.fx-md-facets--open]=\"railOpen()\"\n >\n <div class=\"fx-md-rail-head flex items-center gap-small px-normal py-semi border-b border-border-subtle shrink-0\" [class.hidden]=\"!railOpen()\">\n <button\n type=\"button\"\n class=\"flex items-center justify-center w-6 h-6 rounded-control text-muted hover:bg-bg-hover shrink-0\"\n [attr.aria-label]=\"collapseFacetsLabel()\"\n (click)=\"railOpen.set(false)\"\n >\n <fx-ui-hero-icon icon=\"chevron-double-left\" [size]=\"14\" class=\"flex\" />\n </button>\n <span class=\"txt-eyebrow truncate\">{{ facetsLabel() }}</span>\n </div>\n <div class=\"flex-1 min-h-0 overflow-auto px-normal py-normal\" [class.hidden]=\"!railOpen()\">\n <ng-content select=\"[mdFacets]\"></ng-content>\n </div>\n <button\n type=\"button\"\n class=\"fx-md-rail-toggle flex items-center gap-normal text-muted hover:bg-bg-hover transition-colors\"\n [class.hidden]=\"railOpen()\"\n [attr.aria-label]=\"expandFacetsLabel()\"\n [attr.aria-expanded]=\"false\"\n (click)=\"railOpen.set(true)\"\n >\n <fx-ui-hero-icon icon=\"chevron-double-right\" [size]=\"14\" class=\"flex shrink-0\" />\n <span class=\"fx-md-rail-label txt-eyebrow whitespace-nowrap\">{{ facetsLabel() }}</span>\n </button>\n </aside>\n\n <!-- Master: list pane -->\n <section class=\"fx-md-list flex flex-col min-h-0 min-w-0\">\n <div class=\"fx-md-list-head px-large pt-large pb-small border-b border-border-subtle shrink-0\">\n <ng-content select=\"[mdHeader]\"></ng-content>\n <ng-content select=\"[mdFilters]\"></ng-content>\n </div>\n <div\n class=\"flex flex-col overflow-auto flex-1 min-h-0\"\n role=\"listbox\"\n [attr.aria-label]=\"listLabel() || null\"\n >\n @if (table.loading) {\n <div class=\"px-large py-normal\">\n <fx-ui-skeleton-list [rows]=\"table.pageSize\"></fx-ui-skeleton-list>\n </div>\n } @else {\n @for (row of table.dataSource.data; track table.idOf(row); let i = $index) {\n <!-- Hairline rows; the selected one is tinted with an accent bar on\n the left (design 11/14 selected finding / component). -->\n <button\n type=\"button\"\n role=\"option\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"isSelected(row)\"\n [tabindex]=\"isSelected(row) ? 0 : -1\"\n (click)=\"table.selectItem(row)\"\n (keydown)=\"onKeydown($event, i)\"\n class=\"text-left px-large py-semi border-l-2 border-b border-b-border-subtle transition-colors\"\n [ngClass]=\"\n isSelected(row)\n ? 'border-l-accent bg-accent-soft/60'\n : 'border-l-transparent hover:bg-surface-sunken'\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"rowTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: row, selected: isSelected(row) }\"\n ></ng-container>\n </button>\n }\n @if (!table.dataSource.data.length) {\n <div class=\"px-large py-large\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n }\n </div>\n <!-- No (page) binding: ngAfterViewInit hands this paginator to the table,\n whose own BaseTable subscription (running after this child's hook)\n wires page events \u2014 a binding here would double-fetch every change. -->\n <mat-paginator\n class=\"fx-md-paginator\"\n [class.fx-md-paginator-hidden]=\"paginationStyle() === 'prevnext'\"\n [length]=\"table.total\"\n [pageSize]=\"table.pageSize\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n ></mat-paginator>\n @if (paginationStyle() === 'prevnext') {\n <div class=\"flex items-center justify-between gap-normal px-large py-normal border-t border-border-subtle shrink-0\">\n <span class=\"txt-utility-description\" style=\"font-family: var(--og-font-figure)\">\n {{ rangeText() }}\n </span>\n <div class=\"flex gap-small\">\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasPreviousPage()\"\n (click)=\"paginator().previousPage()\"\n >\n {{ prevLabel() }}\n </button>\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasNextPage()\"\n (click)=\"paginator().nextPage()\"\n >\n {{ nextLabel() }}\n </button>\n </div>\n </div>\n }\n </section>\n\n <!-- Detail pane -->\n <section class=\"fx-md-detail w-full min-w-0 flex flex-col min-h-0\">\n <div class=\"flex-1 min-h-0 overflow-auto px-large py-large\">\n @if (table.selected) {\n @if (table.detailBusy) {\n <fx-ui-skeleton-detail [cards]=\"skeletonCards()\"></fx-ui-skeleton-detail>\n } @else {\n <ng-container\n [ngTemplateOutlet]=\"detailTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: table.detail, selected: table.selected }\"\n ></ng-container>\n }\n } @else {\n <div class=\"flex items-center justify-center h-full min-h-[300px]\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n </div>\n </section>\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-md}.fx-md-row{background:rgb(var(--og-surface));border:1px solid rgb(var(--border-default));border-radius:.5rem;overflow:hidden;box-shadow:0 1px 2px #0000000a,0 3px 8px #0000000d}.fx-md-facets,.fx-md-list{border-bottom:1px solid rgb(var(--og-border-subtle))}.fx-md-rail-toggle{width:100%;padding:10px 12px;flex-direction:row;justify-content:flex-start}.fx-md-list,.fx-md-facets{width:100%}@container fx-md (min-width: 720px){.fx-md-row{flex-direction:row;height:var(--fx-md-pane-h, calc(100vh - 300px) );min-height:420px}.fx-md-facets,.fx-md-list{border-bottom:0;border-right:1px solid rgb(var(--og-border-subtle))}.fx-md-facets{flex:0 0 40px;width:40px;min-width:40px;max-width:40px}.fx-md-facets--open{flex:0 1 230px;width:auto;min-width:200px;max-width:260px}.fx-md-rail-toggle{height:100%;padding:12px 0;flex-direction:column;justify-content:flex-start}.fx-md-rail-label{writing-mode:vertical-rl;transform:rotate(180deg)}.fx-md-list{width:auto;flex:0 1 var(--fx-md-list-width, 420px);min-width:260px;max-width:var(--fx-md-list-width, 420px)}.fx-md-detail{flex:1 1 360px;min-width:360px}}.fx-md-paginator-hidden{position:absolute!important;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}.fx-md-paginator{padding:0 12px;border-top:1px solid rgb(var(--og-border-subtle))}\n"] }]
4609
4822
  }], propDecorators: { table: [{
4610
4823
  type: Input
4611
- }], listWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "listWidth", required: false }] }], paneHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "paneHeight", required: false }] }], pageSizeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizeOptions", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], listLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "listLabel", required: false }] }], skeletonCards: [{ type: i0.Input, args: [{ isSignal: true, alias: "skeletonCards", required: false }] }], paginationStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginationStyle", required: false }] }], prevLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "prevLabel", required: false }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], rowTemplate: [{ type: i0.ContentChild, args: ['row', { isSignal: true }] }], detailTemplate: [{ type: i0.ContentChild, args: ['detail', { isSignal: true }] }], paginator: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MatPaginator), { isSignal: true }] }] } });
4824
+ }], listWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "listWidth", required: false }] }], paneHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "paneHeight", required: false }] }], pageSizeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizeOptions", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], listLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "listLabel", required: false }] }], skeletonCards: [{ type: i0.Input, args: [{ isSignal: true, alias: "skeletonCards", required: false }] }], paginationStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginationStyle", required: false }] }], prevLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "prevLabel", required: false }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], pageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageLabel", required: false }] }], ofLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ofLabel", required: false }] }], facetsOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "facetsOpen", required: false }] }], facetsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "facetsLabel", required: false }] }], expandFacetsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandFacetsLabel", required: false }] }], collapseFacetsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapseFacetsLabel", required: false }] }], rowTemplate: [{ type: i0.ContentChild, args: ['row', { isSignal: true }] }], detailTemplate: [{ type: i0.ContentChild, args: ['detail', { isSignal: true }] }], paginator: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MatPaginator), { isSignal: true }] }] } });
4612
4825
 
4613
4826
  /**
4614
4827
  * Row-action dropdown wrapper over mat-menu (behavior-layer rule, Playbook
@@ -4712,6 +4925,186 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4712
4925
  args: [{ selector: 'fx-ui-pending-badge', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "@if (variant() === 'dot') {\n <span\n class=\"inline-block w-[7px] h-[7px] rounded-full bg-muted/60 align-middle\"\n [attr.title]=\"tooltip\"\n [attr.aria-label]=\"tooltip\"\n ></span>\n} @else {\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-border-subtle bg-neutral-soft text-muted txt-tag-label whitespace-nowrap align-middle\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ label() }}\n </span>\n}\n" }]
4713
4926
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }] } });
4714
4927
 
4928
+ /**
4929
+ * V2 topbar breadcrumb (design 02/12/13, measured on the demo 04/09).
4930
+ *
4931
+ * Projects / [◳ RocketChat ▾] / [⬡ rocketchat.exe · 4.8.1 ▾]
4932
+ *
4933
+ * Plain crumbs are quiet links (12.5px / 500 / muted); the last one is strong.
4934
+ * A crumb with `icon` or `switcher` renders as a chip — hairline border,
4935
+ * surface fill, 1px shadow — and a `switcher` opens a listbox with a filter
4936
+ * box, the sibling options, an optional "View all" row and a "Create" CTA.
4937
+ * Data comes from the app through `BreadcrumbService`; this component only
4938
+ * draws and navigates.
4939
+ */
4940
+ class BreadcrumbComponent {
4941
+ service = inject(BreadcrumbService);
4942
+ router = inject(Router);
4943
+ host = inject(ElementRef);
4944
+ crumbs = toSignal(this.service.get(), { initialValue: [] });
4945
+ /** Index of the crumb whose switcher panel is open; null = closed. */
4946
+ openIndex = signal(null, ...(ngDevMode ? [{ debugName: "openIndex" }] : /* istanbul ignore next */ []));
4947
+ options = signal([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
4948
+ loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
4949
+ keyword = signal('', ...(ngDevMode ? [{ debugName: "keyword" }] : /* istanbul ignore next */ []));
4950
+ /** Drops results of a superseded load (typing fast, reopening). */
4951
+ loadSeq = 0;
4952
+ isChip(crumb) {
4953
+ return !!(crumb.icon || crumb.switcher);
4954
+ }
4955
+ onChipClick(crumb, index, event) {
4956
+ event.stopPropagation();
4957
+ if (crumb.switcher) {
4958
+ if (this.openIndex() === index) {
4959
+ this.close();
4960
+ }
4961
+ else {
4962
+ this.open(crumb, index);
4963
+ }
4964
+ return;
4965
+ }
4966
+ if (crumb.url)
4967
+ this.navigate(crumb.url);
4968
+ }
4969
+ open(crumb, index) {
4970
+ this.openIndex.set(index);
4971
+ this.keyword.set('');
4972
+ void this.load(crumb, '');
4973
+ // Focus the filter box once it exists so typing starts immediately.
4974
+ setTimeout(() => {
4975
+ this.host.nativeElement.querySelector('.fx-crumb-panel__filter')?.focus();
4976
+ });
4977
+ }
4978
+ close() {
4979
+ this.openIndex.set(null);
4980
+ this.options.set([]);
4981
+ this.loading.set(false);
4982
+ }
4983
+ onFilter(crumb, value) {
4984
+ this.keyword.set(value);
4985
+ void this.load(crumb, value);
4986
+ }
4987
+ async load(crumb, keyword) {
4988
+ const switcher = crumb.switcher;
4989
+ if (!switcher)
4990
+ return;
4991
+ const seq = ++this.loadSeq;
4992
+ this.loading.set(true);
4993
+ try {
4994
+ const rows = await switcher.load(keyword);
4995
+ if (seq !== this.loadSeq)
4996
+ return;
4997
+ this.options.set(rows ?? []);
4998
+ }
4999
+ catch {
5000
+ if (seq !== this.loadSeq)
5001
+ return;
5002
+ this.options.set([]);
5003
+ }
5004
+ finally {
5005
+ if (seq === this.loadSeq)
5006
+ this.loading.set(false);
5007
+ }
5008
+ }
5009
+ pick(option, event) {
5010
+ event.stopPropagation();
5011
+ this.close();
5012
+ this.navigate(option.url);
5013
+ }
5014
+ go(event, url) {
5015
+ event.preventDefault();
5016
+ event.stopPropagation();
5017
+ this.close();
5018
+ if (url)
5019
+ this.navigate(url);
5020
+ }
5021
+ navigate(url) {
5022
+ this.router.navigateByUrl(url).catch(() => {
5023
+ window.location.href = url;
5024
+ });
5025
+ }
5026
+ onDocumentClick(event) {
5027
+ if (this.openIndex() === null)
5028
+ return;
5029
+ if (!this.host.nativeElement.contains(event.target))
5030
+ this.close();
5031
+ }
5032
+ onEscape() {
5033
+ if (this.openIndex() !== null)
5034
+ this.close();
5035
+ }
5036
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: BreadcrumbComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5037
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: BreadcrumbComponent, isStandalone: true, selector: "fx-ui-breadcrumb", host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "<nav aria-label=\"Breadcrumb\" class=\"fx-crumbs\">\n @for (crumb of crumbs(); track $index; let last = $last; let i = $index) {\n @if (i > 0) {\n <span aria-hidden=\"true\" class=\"fx-crumb-sep\">/</span>\n }\n @if (isChip(crumb)) {\n <span class=\"relative inline-flex min-w-0\">\n <button\n type=\"button\"\n class=\"fx-crumb-chip\"\n [class.fx-crumb-chip--mono]=\"crumb.mono\"\n [attr.aria-haspopup]=\"crumb.switcher ? 'listbox' : null\"\n [attr.aria-expanded]=\"crumb.switcher ? openIndex() === i : null\"\n [attr.title]=\"crumb.switcher?.title || crumb.label\"\n (click)=\"onChipClick(crumb, i, $event)\"\n >\n @if (crumb.icon) {\n <fx-ui-hero-icon [icon]=\"crumb.icon\" [size]=\"13\" class=\"flex shrink-0 text-accent\" />\n }\n <span class=\"truncate\">{{ crumb.label }}</span>\n @if (crumb.switcher) {\n <fx-ui-hero-icon icon=\"chevron-down\" [size]=\"10\" class=\"flex shrink-0 text-faint\" />\n }\n </button>\n\n @if (crumb.switcher; as sw) {\n @if (openIndex() === i) {\n <div role=\"listbox\" class=\"fx-crumb-panel\" [class.fx-crumb-panel--mono]=\"sw.mono\">\n <div class=\"fx-crumb-panel__title\">{{ sw.title }}</div>\n @if (sw.filterPlaceholder) {\n <input\n type=\"search\"\n class=\"fx-crumb-panel__filter\"\n [placeholder]=\"sw.filterPlaceholder\"\n [attr.aria-label]=\"sw.filterPlaceholder\"\n [value]=\"keyword()\"\n (input)=\"onFilter(crumb, $any($event.target).value)\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n @if (loading() && options().length === 0) {\n <div class=\"fx-crumb-panel__skeleton\" aria-busy=\"true\">\n <span></span><span></span><span></span>\n </div>\n } @else if (options().length === 0) {\n <div class=\"fx-crumb-panel__empty\">{{ sw.emptyLabel || '\u2014' }}</div>\n }\n @for (opt of options(); track opt.value) {\n <button\n type=\"button\"\n role=\"option\"\n class=\"fx-crumb-panel__opt\"\n [class.fx-crumb-panel__opt--on]=\"opt.value === sw.selected\"\n [attr.aria-selected]=\"opt.value === sw.selected\"\n (click)=\"pick(opt, $event)\"\n >\n <span class=\"truncate\">{{ opt.label }}</span>\n @if (opt.meta) {\n <span class=\"fx-crumb-panel__meta\">{{ opt.meta }}</span>\n }\n @if (opt.value === sw.selected) {\n <fx-ui-hero-icon icon=\"check\" [size]=\"13\" class=\"flex shrink-0 text-strong\" />\n }\n </button>\n }\n @if (sw.viewAll || sw.create) {\n <div class=\"fx-crumb-panel__rule\"></div>\n }\n @if (sw.viewAll; as more) {\n <button type=\"button\" class=\"fx-crumb-panel__more\" (click)=\"go($event, more.url)\">\n <fx-ui-hero-icon icon=\"ellipsis-horizontal\" [size]=\"13\" class=\"flex shrink-0 text-faint\" />\n {{ more.label }}\n </button>\n }\n @if (sw.create; as create) {\n <button type=\"button\" class=\"fx-crumb-panel__create\" (click)=\"go($event, create.url)\">\n <fx-ui-hero-icon icon=\"plus\" [size]=\"14\" class=\"flex shrink-0\" />\n {{ create.label }}\n </button>\n }\n </div>\n }\n }\n </span>\n } @else if (!last && crumb.url) {\n <a class=\"fx-crumb-link\" [href]=\"crumb.url\" (click)=\"go($event, crumb.url)\">{{ crumb.label }}</a>\n } @else {\n <span class=\"fx-crumb-current\">{{ crumb.label }}</span>\n }\n }\n</nav>\n", styles: [":host{display:block;min-width:0}.fx-crumbs{display:flex;align-items:center;gap:7px;min-width:0;white-space:nowrap}.fx-crumb-sep{flex:0 0 auto;padding:0 2px;font-size:13px;color:rgb(var(--og-text-faint))}.fx-crumb-link{flex:0 0 auto;max-width:260px;overflow:hidden;text-overflow:ellipsis;font-size:var(--og-fs-sm);font-weight:500;letter-spacing:.005em;color:rgb(var(--og-text-muted));text-decoration:none;transition:color .15s}.fx-crumb-link:hover{color:rgb(var(--og-text))}.fx-crumb-current{min-width:0;overflow:hidden;text-overflow:ellipsis;font-size:var(--og-fs-sm);font-weight:600;color:rgb(var(--og-text))}.fx-crumb-chip{appearance:none;display:inline-flex;align-items:center;gap:8px;min-width:0;max-width:320px;padding:4px 12px;line-height:1.5;border:1px solid rgb(var(--og-border));border-radius:var(--og-radius-control);background:rgb(var(--og-surface));font-size:var(--og-fs-xs);font-weight:600;color:rgb(var(--og-text));box-shadow:0 1px 2px #0000000a;white-space:nowrap;cursor:pointer;transition:box-shadow .12s,border-color .12s}.fx-crumb-chip:hover,.fx-crumb-chip[aria-expanded=true]{box-shadow:0 2px 7px #0000001a}.fx-crumb-chip--mono{font-family:var(--og-font-mono)}.fx-crumb-panel{position:absolute;top:calc(100% + 8px);left:0;z-index:60;min-width:240px;max-width:min(380px,90vw);display:flex;flex-direction:column;padding:6px;background:rgb(var(--og-surface));border:1px solid rgb(var(--og-border));border-radius:var(--og-radius-card);box-shadow:0 10px 28px #00000024;text-align:left;white-space:nowrap;animation:fx-crumb-panel-in .16s ease-out}@keyframes fx-crumb-panel-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}@media(prefers-reduced-motion:reduce){.fx-crumb-panel{animation:none}}.fx-crumb-panel__title{padding:6px 12px 8px;margin-bottom:4px;font-size:var(--og-fs-2xs);letter-spacing:.02em;color:rgb(var(--og-text-faint));border-bottom:1px solid rgb(var(--og-border-subtle))}.fx-crumb-panel__filter{margin:2px 6px 6px;padding:6px 10px;border:1px solid rgb(var(--og-border));border-radius:var(--og-radius-control);background:rgb(var(--og-surface-sunken));color:rgb(var(--og-text));font-family:var(--og-font-body);font-size:var(--og-fs-xs);outline:none}.fx-crumb-panel__filter:focus{border-color:rgb(var(--og-accent))}.fx-crumb-panel__opt{appearance:none;width:100%;display:flex;align-items:center;gap:8px;padding:8px 12px;border:0;border-radius:var(--og-radius-control);background:transparent;font-family:var(--og-font-body);font-size:var(--og-fs-sm);font-weight:400;color:rgb(var(--og-text-muted));text-align:left;cursor:pointer;transition:background .12s}.fx-crumb-panel--mono .fx-crumb-panel__opt{font-family:var(--og-font-mono);font-size:var(--og-fs-xs)}.fx-crumb-panel__opt:hover{background:rgb(var(--og-surface-sunken))}.fx-crumb-panel__opt--on{font-weight:600;color:rgb(var(--og-text))}.fx-crumb-panel__meta{margin-left:auto;font-family:var(--og-font-mono);font-size:var(--og-fs-2xs);color:rgb(var(--og-text-faint))}.fx-crumb-panel__opt>fx-ui-hero-icon:last-child{margin-left:auto}.fx-crumb-panel__opt>.fx-crumb-panel__meta+fx-ui-hero-icon{margin-left:0}.fx-crumb-panel__empty{padding:8px 12px;font-size:var(--og-fs-xs);color:rgb(var(--og-text-faint))}.fx-crumb-panel__skeleton{display:flex;flex-direction:column;gap:8px;padding:8px 12px}.fx-crumb-panel__skeleton span{display:block;height:10px;border-radius:4px;background:rgb(var(--og-border-subtle));animation:fx-crumb-pulse 1.2s ease-in-out infinite}.fx-crumb-panel__skeleton span:nth-child(1){width:62%}.fx-crumb-panel__skeleton span:nth-child(2){width:48%;animation-delay:.15s}.fx-crumb-panel__skeleton span:nth-child(3){width:55%;animation-delay:.3s}@keyframes fx-crumb-pulse{0%,to{opacity:1}50%{opacity:.45}}.fx-crumb-panel__rule{margin:4px 0;border-top:1px solid rgb(var(--og-border-subtle))}.fx-crumb-panel__more{appearance:none;width:100%;display:flex;align-items:center;gap:8px;padding:7px 12px;border:0;border-radius:var(--og-radius-control);background:transparent;font-family:var(--og-font-body);font-size:var(--og-fs-xs);color:rgb(var(--og-text-muted));text-align:left;cursor:pointer}.fx-crumb-panel__more:hover{background:rgb(var(--og-surface-sunken));color:rgb(var(--og-text))}.fx-crumb-panel__create{appearance:none;width:calc(100% - 12px);margin:4px 6px 2px;display:flex;align-items:center;gap:8px;padding:9px 12px;border:0;border-radius:var(--og-radius-control);background:rgb(var(--og-accent-soft-bg));color:rgb(var(--og-accent-soft-fg));font-family:var(--og-font-body);font-size:var(--og-fs-sm);font-weight:600;text-align:left;cursor:pointer;transition:background .15s,color .15s}.fx-crumb-panel__create:hover{background:rgb(var(--og-accent));color:rgb(var(--og-accent-contrast))}.text-accent{color:rgb(var(--og-accent))}.text-faint{color:rgb(var(--og-text-faint))}.text-strong{color:rgb(var(--og-text))}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}\n"], dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5038
+ }
5039
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: BreadcrumbComponent, decorators: [{
5040
+ type: Component,
5041
+ args: [{ selector: 'fx-ui-breadcrumb', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "<nav aria-label=\"Breadcrumb\" class=\"fx-crumbs\">\n @for (crumb of crumbs(); track $index; let last = $last; let i = $index) {\n @if (i > 0) {\n <span aria-hidden=\"true\" class=\"fx-crumb-sep\">/</span>\n }\n @if (isChip(crumb)) {\n <span class=\"relative inline-flex min-w-0\">\n <button\n type=\"button\"\n class=\"fx-crumb-chip\"\n [class.fx-crumb-chip--mono]=\"crumb.mono\"\n [attr.aria-haspopup]=\"crumb.switcher ? 'listbox' : null\"\n [attr.aria-expanded]=\"crumb.switcher ? openIndex() === i : null\"\n [attr.title]=\"crumb.switcher?.title || crumb.label\"\n (click)=\"onChipClick(crumb, i, $event)\"\n >\n @if (crumb.icon) {\n <fx-ui-hero-icon [icon]=\"crumb.icon\" [size]=\"13\" class=\"flex shrink-0 text-accent\" />\n }\n <span class=\"truncate\">{{ crumb.label }}</span>\n @if (crumb.switcher) {\n <fx-ui-hero-icon icon=\"chevron-down\" [size]=\"10\" class=\"flex shrink-0 text-faint\" />\n }\n </button>\n\n @if (crumb.switcher; as sw) {\n @if (openIndex() === i) {\n <div role=\"listbox\" class=\"fx-crumb-panel\" [class.fx-crumb-panel--mono]=\"sw.mono\">\n <div class=\"fx-crumb-panel__title\">{{ sw.title }}</div>\n @if (sw.filterPlaceholder) {\n <input\n type=\"search\"\n class=\"fx-crumb-panel__filter\"\n [placeholder]=\"sw.filterPlaceholder\"\n [attr.aria-label]=\"sw.filterPlaceholder\"\n [value]=\"keyword()\"\n (input)=\"onFilter(crumb, $any($event.target).value)\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n @if (loading() && options().length === 0) {\n <div class=\"fx-crumb-panel__skeleton\" aria-busy=\"true\">\n <span></span><span></span><span></span>\n </div>\n } @else if (options().length === 0) {\n <div class=\"fx-crumb-panel__empty\">{{ sw.emptyLabel || '\u2014' }}</div>\n }\n @for (opt of options(); track opt.value) {\n <button\n type=\"button\"\n role=\"option\"\n class=\"fx-crumb-panel__opt\"\n [class.fx-crumb-panel__opt--on]=\"opt.value === sw.selected\"\n [attr.aria-selected]=\"opt.value === sw.selected\"\n (click)=\"pick(opt, $event)\"\n >\n <span class=\"truncate\">{{ opt.label }}</span>\n @if (opt.meta) {\n <span class=\"fx-crumb-panel__meta\">{{ opt.meta }}</span>\n }\n @if (opt.value === sw.selected) {\n <fx-ui-hero-icon icon=\"check\" [size]=\"13\" class=\"flex shrink-0 text-strong\" />\n }\n </button>\n }\n @if (sw.viewAll || sw.create) {\n <div class=\"fx-crumb-panel__rule\"></div>\n }\n @if (sw.viewAll; as more) {\n <button type=\"button\" class=\"fx-crumb-panel__more\" (click)=\"go($event, more.url)\">\n <fx-ui-hero-icon icon=\"ellipsis-horizontal\" [size]=\"13\" class=\"flex shrink-0 text-faint\" />\n {{ more.label }}\n </button>\n }\n @if (sw.create; as create) {\n <button type=\"button\" class=\"fx-crumb-panel__create\" (click)=\"go($event, create.url)\">\n <fx-ui-hero-icon icon=\"plus\" [size]=\"14\" class=\"flex shrink-0\" />\n {{ create.label }}\n </button>\n }\n </div>\n }\n }\n </span>\n } @else if (!last && crumb.url) {\n <a class=\"fx-crumb-link\" [href]=\"crumb.url\" (click)=\"go($event, crumb.url)\">{{ crumb.label }}</a>\n } @else {\n <span class=\"fx-crumb-current\">{{ crumb.label }}</span>\n }\n }\n</nav>\n", styles: [":host{display:block;min-width:0}.fx-crumbs{display:flex;align-items:center;gap:7px;min-width:0;white-space:nowrap}.fx-crumb-sep{flex:0 0 auto;padding:0 2px;font-size:13px;color:rgb(var(--og-text-faint))}.fx-crumb-link{flex:0 0 auto;max-width:260px;overflow:hidden;text-overflow:ellipsis;font-size:var(--og-fs-sm);font-weight:500;letter-spacing:.005em;color:rgb(var(--og-text-muted));text-decoration:none;transition:color .15s}.fx-crumb-link:hover{color:rgb(var(--og-text))}.fx-crumb-current{min-width:0;overflow:hidden;text-overflow:ellipsis;font-size:var(--og-fs-sm);font-weight:600;color:rgb(var(--og-text))}.fx-crumb-chip{appearance:none;display:inline-flex;align-items:center;gap:8px;min-width:0;max-width:320px;padding:4px 12px;line-height:1.5;border:1px solid rgb(var(--og-border));border-radius:var(--og-radius-control);background:rgb(var(--og-surface));font-size:var(--og-fs-xs);font-weight:600;color:rgb(var(--og-text));box-shadow:0 1px 2px #0000000a;white-space:nowrap;cursor:pointer;transition:box-shadow .12s,border-color .12s}.fx-crumb-chip:hover,.fx-crumb-chip[aria-expanded=true]{box-shadow:0 2px 7px #0000001a}.fx-crumb-chip--mono{font-family:var(--og-font-mono)}.fx-crumb-panel{position:absolute;top:calc(100% + 8px);left:0;z-index:60;min-width:240px;max-width:min(380px,90vw);display:flex;flex-direction:column;padding:6px;background:rgb(var(--og-surface));border:1px solid rgb(var(--og-border));border-radius:var(--og-radius-card);box-shadow:0 10px 28px #00000024;text-align:left;white-space:nowrap;animation:fx-crumb-panel-in .16s ease-out}@keyframes fx-crumb-panel-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}@media(prefers-reduced-motion:reduce){.fx-crumb-panel{animation:none}}.fx-crumb-panel__title{padding:6px 12px 8px;margin-bottom:4px;font-size:var(--og-fs-2xs);letter-spacing:.02em;color:rgb(var(--og-text-faint));border-bottom:1px solid rgb(var(--og-border-subtle))}.fx-crumb-panel__filter{margin:2px 6px 6px;padding:6px 10px;border:1px solid rgb(var(--og-border));border-radius:var(--og-radius-control);background:rgb(var(--og-surface-sunken));color:rgb(var(--og-text));font-family:var(--og-font-body);font-size:var(--og-fs-xs);outline:none}.fx-crumb-panel__filter:focus{border-color:rgb(var(--og-accent))}.fx-crumb-panel__opt{appearance:none;width:100%;display:flex;align-items:center;gap:8px;padding:8px 12px;border:0;border-radius:var(--og-radius-control);background:transparent;font-family:var(--og-font-body);font-size:var(--og-fs-sm);font-weight:400;color:rgb(var(--og-text-muted));text-align:left;cursor:pointer;transition:background .12s}.fx-crumb-panel--mono .fx-crumb-panel__opt{font-family:var(--og-font-mono);font-size:var(--og-fs-xs)}.fx-crumb-panel__opt:hover{background:rgb(var(--og-surface-sunken))}.fx-crumb-panel__opt--on{font-weight:600;color:rgb(var(--og-text))}.fx-crumb-panel__meta{margin-left:auto;font-family:var(--og-font-mono);font-size:var(--og-fs-2xs);color:rgb(var(--og-text-faint))}.fx-crumb-panel__opt>fx-ui-hero-icon:last-child{margin-left:auto}.fx-crumb-panel__opt>.fx-crumb-panel__meta+fx-ui-hero-icon{margin-left:0}.fx-crumb-panel__empty{padding:8px 12px;font-size:var(--og-fs-xs);color:rgb(var(--og-text-faint))}.fx-crumb-panel__skeleton{display:flex;flex-direction:column;gap:8px;padding:8px 12px}.fx-crumb-panel__skeleton span{display:block;height:10px;border-radius:4px;background:rgb(var(--og-border-subtle));animation:fx-crumb-pulse 1.2s ease-in-out infinite}.fx-crumb-panel__skeleton span:nth-child(1){width:62%}.fx-crumb-panel__skeleton span:nth-child(2){width:48%;animation-delay:.15s}.fx-crumb-panel__skeleton span:nth-child(3){width:55%;animation-delay:.3s}@keyframes fx-crumb-pulse{0%,to{opacity:1}50%{opacity:.45}}.fx-crumb-panel__rule{margin:4px 0;border-top:1px solid rgb(var(--og-border-subtle))}.fx-crumb-panel__more{appearance:none;width:100%;display:flex;align-items:center;gap:8px;padding:7px 12px;border:0;border-radius:var(--og-radius-control);background:transparent;font-family:var(--og-font-body);font-size:var(--og-fs-xs);color:rgb(var(--og-text-muted));text-align:left;cursor:pointer}.fx-crumb-panel__more:hover{background:rgb(var(--og-surface-sunken));color:rgb(var(--og-text))}.fx-crumb-panel__create{appearance:none;width:calc(100% - 12px);margin:4px 6px 2px;display:flex;align-items:center;gap:8px;padding:9px 12px;border:0;border-radius:var(--og-radius-control);background:rgb(var(--og-accent-soft-bg));color:rgb(var(--og-accent-soft-fg));font-family:var(--og-font-body);font-size:var(--og-fs-sm);font-weight:600;text-align:left;cursor:pointer;transition:background .15s,color .15s}.fx-crumb-panel__create:hover{background:rgb(var(--og-accent));color:rgb(var(--og-accent-contrast))}.text-accent{color:rgb(var(--og-accent))}.text-faint{color:rgb(var(--og-text-faint))}.text-strong{color:rgb(var(--og-text))}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}\n"] }]
5042
+ }], propDecorators: { onDocumentClick: [{
5043
+ type: HostListener,
5044
+ args: ['document:click', ['$event']]
5045
+ }], onEscape: [{
5046
+ type: HostListener,
5047
+ args: ['document:keydown.escape']
5048
+ }] } });
5049
+
5050
+ /**
5051
+ * Topbar load status (design "Loading States", measured 04/09):
5052
+ *
5053
+ * ◌ Loading charts · 1 of 4 → while requests are pending
5054
+ * ● Up to date · just now ↻ → once the batch settled
5055
+ *
5056
+ * Mono 10.5px; muted while loading, faint when idle; 11px spinner (2px ring,
5057
+ * accent top). With `bar` a 2px indeterminate strip is drawn along the top
5058
+ * edge of the nearest positioned ancestor (the topbar) while loading.
5059
+ * Labels are caller-translated inputs; the ↻ button emits `reload`.
5060
+ */
5061
+ class LoadStatusComponent {
5062
+ status = inject(FxLoadStatusService);
5063
+ loadingLabel = input('Loading', ...(ngDevMode ? [{ debugName: "loadingLabel" }] : /* istanbul ignore next */ []));
5064
+ ofLabel = input('of', ...(ngDevMode ? [{ debugName: "ofLabel" }] : /* istanbul ignore next */ []));
5065
+ upToDateLabel = input('Up to date', ...(ngDevMode ? [{ debugName: "upToDateLabel" }] : /* istanbul ignore next */ []));
5066
+ justNowLabel = input('just now', ...(ngDevMode ? [{ debugName: "justNowLabel" }] : /* istanbul ignore next */ []));
5067
+ /** `{n}` is replaced by the minute count. */
5068
+ minutesAgoLabel = input('{n} min ago', ...(ngDevMode ? [{ debugName: "minutesAgoLabel" }] : /* istanbul ignore next */ []));
5069
+ reloadLabel = input('Reload data', ...(ngDevMode ? [{ debugName: "reloadLabel" }] : /* istanbul ignore next */ []));
5070
+ /** Draw the 2px indeterminate bar along the top of the positioned ancestor. */
5071
+ bar = input(false, ...(ngDevMode ? [{ debugName: "bar" }] : /* istanbul ignore next */ []));
5072
+ /** Show the ↻ button. */
5073
+ showReload = input(true, ...(ngDevMode ? [{ debugName: "showReload" }] : /* istanbul ignore next */ []));
5074
+ reload = output();
5075
+ /** Ticks every 30 s so "just now" ages into "N min ago" without traffic. */
5076
+ tick = signal(0, ...(ngDevMode ? [{ debugName: "tick" }] : /* istanbul ignore next */ []));
5077
+ constructor() {
5078
+ const timer = setInterval(() => this.tick.update((n) => n + 1), 30_000);
5079
+ inject(DestroyRef).onDestroy(() => clearInterval(timer));
5080
+ }
5081
+ loadingText = computed(() => {
5082
+ const total = Math.max(this.status.total(), 1);
5083
+ const done = Math.min(this.status.done(), total);
5084
+ const label = this.status.current();
5085
+ return `${this.loadingLabel()}${label ? ' ' + label : ''} · ${done} ${this.ofLabel()} ${total}`;
5086
+ }, ...(ngDevMode ? [{ debugName: "loadingText" }] : /* istanbul ignore next */ []));
5087
+ idleText = computed(() => {
5088
+ this.tick();
5089
+ const at = this.status.lastUpdated();
5090
+ if (!at)
5091
+ return this.upToDateLabel();
5092
+ const minutes = Math.floor((Date.now() - at.getTime()) / 60_000);
5093
+ const when = minutes < 1 ? this.justNowLabel() : this.minutesAgoLabel().replace('{n}', String(minutes));
5094
+ return `${this.upToDateLabel()} · ${when}`;
5095
+ }, ...(ngDevMode ? [{ debugName: "idleText" }] : /* istanbul ignore next */ []));
5096
+ onReload() {
5097
+ this.reload.emit();
5098
+ this.status.requestReload();
5099
+ }
5100
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: LoadStatusComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5101
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: LoadStatusComponent, isStandalone: true, selector: "fx-ui-load-status", inputs: { loadingLabel: { classPropertyName: "loadingLabel", publicName: "loadingLabel", isSignal: true, isRequired: false, transformFunction: null }, ofLabel: { classPropertyName: "ofLabel", publicName: "ofLabel", isSignal: true, isRequired: false, transformFunction: null }, upToDateLabel: { classPropertyName: "upToDateLabel", publicName: "upToDateLabel", isSignal: true, isRequired: false, transformFunction: null }, justNowLabel: { classPropertyName: "justNowLabel", publicName: "justNowLabel", isSignal: true, isRequired: false, transformFunction: null }, minutesAgoLabel: { classPropertyName: "minutesAgoLabel", publicName: "minutesAgoLabel", isSignal: true, isRequired: false, transformFunction: null }, reloadLabel: { classPropertyName: "reloadLabel", publicName: "reloadLabel", isSignal: true, isRequired: false, transformFunction: null }, bar: { classPropertyName: "bar", publicName: "bar", isSignal: true, isRequired: false, transformFunction: null }, showReload: { classPropertyName: "showReload", publicName: "showReload", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { reload: "reload" }, ngImport: i0, template: "@if (bar() && status.loading()) {\n <div class=\"fx-load-bar\" aria-hidden=\"true\"><div class=\"fx-load-bar__run\"></div></div>\n}\n<div class=\"fx-load-status\" role=\"status\" aria-live=\"polite\" [class.fx-load-status--idle]=\"!status.loading()\">\n @if (status.loading()) {\n <span class=\"fx-load-spinner\" aria-hidden=\"true\"></span>\n <span class=\"truncate\">{{ loadingText() }}</span>\n } @else {\n <span class=\"fx-load-dot\" aria-hidden=\"true\">\u25CF</span>\n <span class=\"truncate\">{{ idleText() }}</span>\n }\n</div>\n@if (showReload()) {\n <button type=\"button\" class=\"fx-load-reload\" [attr.aria-label]=\"reloadLabel()\" [title]=\"reloadLabel()\" (click)=\"onReload()\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M21 12a9 9 0 1 1-3.1-6.8\" />\n <path d=\"M21 3v6h-6\" />\n </svg>\n </button>\n}\n", styles: [":host{display:inline-flex;align-items:center;gap:8px;min-width:0}.fx-load-status{display:inline-flex;align-items:center;gap:7px;min-width:0;font-family:var(--og-font-mono);font-size:var(--og-fs-2xs);color:rgb(var(--og-text-muted));white-space:nowrap}.fx-load-status--idle{color:rgb(var(--og-text-faint))}.fx-load-dot{color:rgb(var(--og-positive))}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.fx-load-spinner{display:inline-block;flex:none;width:11px;height:11px;border-radius:50%;border:2px solid rgb(var(--og-text) / .14);border-top-color:rgb(var(--og-accent));animation:fx-load-spin .8s linear infinite}@keyframes fx-load-spin{to{transform:rotate(360deg)}}.fx-load-reload{appearance:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:var(--og-radius-control);background:transparent;color:rgb(var(--og-text-muted));cursor:pointer;transition:background .12s,color .12s}.fx-load-reload:hover{background:rgb(var(--og-surface-sunken));color:rgb(var(--og-text))}.fx-load-bar{position:absolute;left:0;right:0;top:0;height:2px;overflow:hidden;background:rgb(var(--og-accent) / .18);pointer-events:none}.fx-load-bar__run{width:38%;height:100%;border-radius:2px;background:rgb(var(--og-accent));animation:fx-load-indet 1.15s cubic-bezier(.4,0,.2,1) infinite}@keyframes fx-load-indet{0%{transform:translate(-100%)}60%{transform:translate(160%)}to{transform:translate(160%)}}@media(prefers-reduced-motion:reduce){.fx-load-spinner,.fx-load-bar__run{animation:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5102
+ }
5103
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: LoadStatusComponent, decorators: [{
5104
+ type: Component,
5105
+ args: [{ selector: 'fx-ui-load-status', changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (bar() && status.loading()) {\n <div class=\"fx-load-bar\" aria-hidden=\"true\"><div class=\"fx-load-bar__run\"></div></div>\n}\n<div class=\"fx-load-status\" role=\"status\" aria-live=\"polite\" [class.fx-load-status--idle]=\"!status.loading()\">\n @if (status.loading()) {\n <span class=\"fx-load-spinner\" aria-hidden=\"true\"></span>\n <span class=\"truncate\">{{ loadingText() }}</span>\n } @else {\n <span class=\"fx-load-dot\" aria-hidden=\"true\">\u25CF</span>\n <span class=\"truncate\">{{ idleText() }}</span>\n }\n</div>\n@if (showReload()) {\n <button type=\"button\" class=\"fx-load-reload\" [attr.aria-label]=\"reloadLabel()\" [title]=\"reloadLabel()\" (click)=\"onReload()\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M21 12a9 9 0 1 1-3.1-6.8\" />\n <path d=\"M21 3v6h-6\" />\n </svg>\n </button>\n}\n", styles: [":host{display:inline-flex;align-items:center;gap:8px;min-width:0}.fx-load-status{display:inline-flex;align-items:center;gap:7px;min-width:0;font-family:var(--og-font-mono);font-size:var(--og-fs-2xs);color:rgb(var(--og-text-muted));white-space:nowrap}.fx-load-status--idle{color:rgb(var(--og-text-faint))}.fx-load-dot{color:rgb(var(--og-positive))}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.fx-load-spinner{display:inline-block;flex:none;width:11px;height:11px;border-radius:50%;border:2px solid rgb(var(--og-text) / .14);border-top-color:rgb(var(--og-accent));animation:fx-load-spin .8s linear infinite}@keyframes fx-load-spin{to{transform:rotate(360deg)}}.fx-load-reload{appearance:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:var(--og-radius-control);background:transparent;color:rgb(var(--og-text-muted));cursor:pointer;transition:background .12s,color .12s}.fx-load-reload:hover{background:rgb(var(--og-surface-sunken));color:rgb(var(--og-text))}.fx-load-bar{position:absolute;left:0;right:0;top:0;height:2px;overflow:hidden;background:rgb(var(--og-accent) / .18);pointer-events:none}.fx-load-bar__run{width:38%;height:100%;border-radius:2px;background:rgb(var(--og-accent));animation:fx-load-indet 1.15s cubic-bezier(.4,0,.2,1) infinite}@keyframes fx-load-indet{0%{transform:translate(-100%)}60%{transform:translate(160%)}to{transform:translate(160%)}}@media(prefers-reduced-motion:reduce){.fx-load-spinner,.fx-load-bar__run{animation:none}}\n"] }]
5106
+ }], ctorParameters: () => [], propDecorators: { loadingLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingLabel", required: false }] }], ofLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ofLabel", required: false }] }], upToDateLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "upToDateLabel", required: false }] }], justNowLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "justNowLabel", required: false }] }], minutesAgoLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "minutesAgoLabel", required: false }] }], reloadLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "reloadLabel", required: false }] }], bar: [{ type: i0.Input, args: [{ isSignal: true, alias: "bar", required: false }] }], showReload: [{ type: i0.Input, args: [{ isSignal: true, alias: "showReload", required: false }] }], reload: [{ type: i0.Output, args: ["reload"] }] } });
5107
+
4715
5108
  /**
4716
5109
  * Wraps UI that is rendered from PLACEHOLDER data so the layout can be
4717
5110
  * reviewed before its endpoint exists (anh Tú 29/08: "muốn có full giao diện
@@ -4784,7 +5177,7 @@ class ScoreBadgeComponent {
4784
5177
  sizeClass() {
4785
5178
  switch (this.size()) {
4786
5179
  case 'sm':
4787
- return 'text-xs px-normal py-[1px]';
5180
+ return 'text-xs px-normal py-[3px]';
4788
5181
  case 'lg':
4789
5182
  return 'text-2xl px-semi py-small font-semibold';
4790
5183
  default:
@@ -4909,11 +5302,11 @@ class SegmentedComponent {
4909
5302
  return this.variant() === 'pill' ? 'text-accent-contrast' : 'text-strong';
4910
5303
  }
4911
5304
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SegmentedComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4912
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SegmentedComponent, isStandalone: true, selector: "fx-ui-segmented", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, fit: { classPropertyName: "fit", publicName: "fit", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, viewQueries: [{ propertyName: "optionEls", predicate: ["opt"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"relative inline-grid max-w-full grid-flow-col items-stretch p-[3px] bg-surface-sunken border border-border rounded-full\"\n [class]=\"gridClass()\"\n role=\"group\"\n [attr.aria-label]=\"ariaLabel()\"\n>\n <!-- Sliding thumb: one element that translates to the active option -->\n @if (activeIndex() >= 0) {\n <span\n aria-hidden=\"true\"\n class=\"fx-segmented__thumb absolute top-[3px] bottom-[3px] left-[3px] rounded-full pointer-events-none\"\n [class]=\"thumbClass()\"\n [style.width]=\"thumbWidth()\"\n [style.transform]=\"thumbTransform()\"\n ></span>\n }\n @for (option of options(); track option.value) {\n <button\n #opt\n type=\"button\"\n (click)=\"select(option)\"\n [disabled]=\"option.disabled\"\n [attr.aria-pressed]=\"option.value === value()\"\n class=\"relative z-[1] flex min-w-0 items-center justify-center gap-xs px-semi py-[5px] rounded-full text-[13px] leading-none font-medium whitespace-nowrap transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed\"\n [class]=\"optionClass(option)\"\n >\n @if (option.icon) {\n <fx-ui-hero-icon [icon]=\"option.icon\" [size]=\"14\" class=\"flex\" />\n }\n <!-- text-box trims Archivo's uneven leading \u2192 true optical centering\n (Chrome 128+/Safari 18.2+; older browsers ignore it harmlessly).\n truncate adds overflow:hidden, and the trimmed box ends at cap\n height / baseline \u2014 so the dot on \"i\" and the tail on \"y\" fall\n outside it and get clipped. py/-my pair gives both back without\n changing the control's height. -->\n <span class=\"min-w-0 truncate py-[3px] -my-[3px] [text-box:trim-both_cap_alphabetic]\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label px-small rounded-pill bg-neutral-soft text-neutral-soft-fg\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n</div>\n", styles: ["@charset \"UTF-8\";.fx-segmented__thumb{transition:transform .2s cubic-bezier(.2,.9,.2,1),width .2s cubic-bezier(.2,.9,.2,1);will-change:transform}@media(prefers-reduced-motion:reduce){.fx-segmented__thumb{transition:none}}\n"], dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5305
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SegmentedComponent, isStandalone: true, selector: "fx-ui-segmented", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, fit: { classPropertyName: "fit", publicName: "fit", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, viewQueries: [{ propertyName: "optionEls", predicate: ["opt"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"relative inline-grid max-w-full grid-flow-col items-stretch p-[3px] bg-surface-sunken border border-border rounded-full\"\n [class]=\"gridClass()\"\n role=\"group\"\n [attr.aria-label]=\"ariaLabel()\"\n>\n <!-- Sliding thumb: one element that translates to the active option -->\n @if (activeIndex() >= 0) {\n <span\n aria-hidden=\"true\"\n class=\"fx-segmented__thumb absolute top-[3px] bottom-[3px] left-[3px] rounded-full pointer-events-none\"\n [class]=\"thumbClass()\"\n [style.width]=\"thumbWidth()\"\n [style.transform]=\"thumbTransform()\"\n ></span>\n }\n <!-- option 30px tall (7px \u00D7 18px padding): the text-box trim below collapses the\n glyph box to ~8px, so padding alone left the pill at 22px (anh T\u00FA 04/09) -->\n @for (option of options(); track option.value) {\n <button\n #opt\n type=\"button\"\n (click)=\"select(option)\"\n [disabled]=\"option.disabled\"\n [attr.aria-pressed]=\"option.value === value()\"\n class=\"relative z-[1] flex min-w-0 min-h-[30px] items-center justify-center gap-xs px-[18px] py-[7px] rounded-full text-[13px] leading-none font-medium whitespace-nowrap transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed\"\n [class]=\"optionClass(option)\"\n >\n @if (option.icon) {\n <fx-ui-hero-icon [icon]=\"option.icon\" [size]=\"14\" class=\"flex\" />\n }\n <!-- text-box trims Archivo's uneven leading \u2192 true optical centering\n (Chrome 128+/Safari 18.2+; older browsers ignore it harmlessly).\n truncate adds overflow:hidden, and the trimmed box ends at cap\n height / baseline \u2014 so the dot on \"i\" and the tail on \"y\" fall\n outside it and get clipped. py/-my pair gives both back without\n changing the control's height. -->\n <span class=\"min-w-0 truncate py-[3px] -my-[3px] [text-box:trim-both_cap_alphabetic]\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label px-small rounded-pill bg-neutral-soft text-neutral-soft-fg\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n</div>\n", styles: ["@charset \"UTF-8\";.fx-segmented__thumb{transition:transform .2s cubic-bezier(.2,.9,.2,1),width .2s cubic-bezier(.2,.9,.2,1);will-change:transform}@media(prefers-reduced-motion:reduce){.fx-segmented__thumb{transition:none}}\n"], dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4913
5306
  }
4914
5307
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SegmentedComponent, decorators: [{
4915
5308
  type: Component,
4916
- args: [{ selector: 'fx-ui-segmented', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "<div\n class=\"relative inline-grid max-w-full grid-flow-col items-stretch p-[3px] bg-surface-sunken border border-border rounded-full\"\n [class]=\"gridClass()\"\n role=\"group\"\n [attr.aria-label]=\"ariaLabel()\"\n>\n <!-- Sliding thumb: one element that translates to the active option -->\n @if (activeIndex() >= 0) {\n <span\n aria-hidden=\"true\"\n class=\"fx-segmented__thumb absolute top-[3px] bottom-[3px] left-[3px] rounded-full pointer-events-none\"\n [class]=\"thumbClass()\"\n [style.width]=\"thumbWidth()\"\n [style.transform]=\"thumbTransform()\"\n ></span>\n }\n @for (option of options(); track option.value) {\n <button\n #opt\n type=\"button\"\n (click)=\"select(option)\"\n [disabled]=\"option.disabled\"\n [attr.aria-pressed]=\"option.value === value()\"\n class=\"relative z-[1] flex min-w-0 items-center justify-center gap-xs px-semi py-[5px] rounded-full text-[13px] leading-none font-medium whitespace-nowrap transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed\"\n [class]=\"optionClass(option)\"\n >\n @if (option.icon) {\n <fx-ui-hero-icon [icon]=\"option.icon\" [size]=\"14\" class=\"flex\" />\n }\n <!-- text-box trims Archivo's uneven leading \u2192 true optical centering\n (Chrome 128+/Safari 18.2+; older browsers ignore it harmlessly).\n truncate adds overflow:hidden, and the trimmed box ends at cap\n height / baseline \u2014 so the dot on \"i\" and the tail on \"y\" fall\n outside it and get clipped. py/-my pair gives both back without\n changing the control's height. -->\n <span class=\"min-w-0 truncate py-[3px] -my-[3px] [text-box:trim-both_cap_alphabetic]\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label px-small rounded-pill bg-neutral-soft text-neutral-soft-fg\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n</div>\n", styles: ["@charset \"UTF-8\";.fx-segmented__thumb{transition:transform .2s cubic-bezier(.2,.9,.2,1),width .2s cubic-bezier(.2,.9,.2,1);will-change:transform}@media(prefers-reduced-motion:reduce){.fx-segmented__thumb{transition:none}}\n"] }]
5309
+ args: [{ selector: 'fx-ui-segmented', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "<div\n class=\"relative inline-grid max-w-full grid-flow-col items-stretch p-[3px] bg-surface-sunken border border-border rounded-full\"\n [class]=\"gridClass()\"\n role=\"group\"\n [attr.aria-label]=\"ariaLabel()\"\n>\n <!-- Sliding thumb: one element that translates to the active option -->\n @if (activeIndex() >= 0) {\n <span\n aria-hidden=\"true\"\n class=\"fx-segmented__thumb absolute top-[3px] bottom-[3px] left-[3px] rounded-full pointer-events-none\"\n [class]=\"thumbClass()\"\n [style.width]=\"thumbWidth()\"\n [style.transform]=\"thumbTransform()\"\n ></span>\n }\n <!-- option 30px tall (7px \u00D7 18px padding): the text-box trim below collapses the\n glyph box to ~8px, so padding alone left the pill at 22px (anh T\u00FA 04/09) -->\n @for (option of options(); track option.value) {\n <button\n #opt\n type=\"button\"\n (click)=\"select(option)\"\n [disabled]=\"option.disabled\"\n [attr.aria-pressed]=\"option.value === value()\"\n class=\"relative z-[1] flex min-w-0 min-h-[30px] items-center justify-center gap-xs px-[18px] py-[7px] rounded-full text-[13px] leading-none font-medium whitespace-nowrap transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed\"\n [class]=\"optionClass(option)\"\n >\n @if (option.icon) {\n <fx-ui-hero-icon [icon]=\"option.icon\" [size]=\"14\" class=\"flex\" />\n }\n <!-- text-box trims Archivo's uneven leading \u2192 true optical centering\n (Chrome 128+/Safari 18.2+; older browsers ignore it harmlessly).\n truncate adds overflow:hidden, and the trimmed box ends at cap\n height / baseline \u2014 so the dot on \"i\" and the tail on \"y\" fall\n outside it and get clipped. py/-my pair gives both back without\n changing the control's height. -->\n <span class=\"min-w-0 truncate py-[3px] -my-[3px] [text-box:trim-both_cap_alphabetic]\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label px-small rounded-pill bg-neutral-soft text-neutral-soft-fg\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n</div>\n", styles: ["@charset \"UTF-8\";.fx-segmented__thumb{transition:transform .2s cubic-bezier(.2,.9,.2,1),width .2s cubic-bezier(.2,.9,.2,1);will-change:transform}@media(prefers-reduced-motion:reduce){.fx-segmented__thumb{transition:none}}\n"] }]
4917
5310
  }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], fit: [{ type: i0.Input, args: [{ isSignal: true, alias: "fit", required: false }] }], optionEls: [{ type: i0.ViewChildren, args: ['opt', { isSignal: true }] }] } });
4918
5311
 
4919
5312
  /**
@@ -5245,11 +5638,11 @@ class RouteSkeletonComponent {
5245
5638
  }
5246
5639
  bars = range$1(7);
5247
5640
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: RouteSkeletonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5248
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: RouteSkeletonComponent, isStandalone: true, selector: "fx-ui-route-skeleton", inputs: { hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"w-full flex flex-col gap-xl\">\n @for (b of blocks(); track $index) {\n @switch (b.kind) {\n @case ('heading') {\n <div data-fx-skel=\"heading\" class=\"flex flex-col md:flex-row md:items-start justify-between gap-normal\">\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-7 w-64\"></div>\n <div class=\"skeleton-load h-3 w-32\"></div>\n </div>\n @if (items(b, 2).length) {\n <div class=\"flex items-center gap-normal\">\n @for (a of items(b, 2); track a) {\n <div class=\"skeleton-load h-9 rounded-lg\" [class]=\"a === 0 ? 'w-28' : 'w-32'\"></div>\n }\n </div>\n }\n </div>\n }\n\n @case ('toolbar') {\n <div data-fx-skel=\"toolbar\" class=\"flex flex-wrap items-center gap-normal\">\n <div class=\"skeleton-load h-9 w-72 rounded-lg\"></div>\n @for (f of items(b, 2); track f) {\n <div class=\"skeleton-load h-7 rounded-full\" [class]=\"f % 2 ? 'w-24' : 'w-20'\"></div>\n }\n </div>\n }\n\n @case ('table') {\n <!-- B\u1EA3ng: m\u1ED7i \u00D4 l\u00E0 m\u1ED9t `.skeleton-load` ri\u00EAng, \u0111\u00FAng c\u00E1ch\n `fx-ui-table-cell [loading]` l\u00E0m tr\u00EAn b\u1EA3ng th\u1EADt. -->\n <div data-fx-skel=\"table\" class=\"card-common overflow-x-auto\">\n <table class=\"w-full\">\n <thead>\n <tr class=\"border-b border-border-default\">\n @for (c of cols(b); track c) {\n <th class=\"px-normal py-semi text-left\">\n <div class=\"skeleton-load h-3\" [style.width.%]=\"headWidth(c)\"></div>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (r of rows(b); track r) {\n <tr class=\"border-b border-border-subtle\">\n @for (c of cols(b); track c) {\n <td class=\"px-normal py-semi align-middle\">\n <div class=\"skeleton-load h-4\" [style.width.%]=\"cellWidth(r, c)\"></div>\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n @case ('stat-cards') {\n <div data-fx-skel=\"stat-cards\" class=\"grid gap-normal grid-cols-2 md:grid-cols-4\">\n @for (i of items(b, 4); track i) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"skeleton-load h-3 w-16\"></div>\n <div class=\"skeleton-load h-8 w-24\"></div>\n <div class=\"skeleton-load h-3 w-20\"></div>\n </div>\n }\n </div>\n }\n\n @case ('kpi-strip') {\n <!-- M\u1ED8T card chia c\u1ED9t (ki\u1EC3u app-dash-kpi-row), kh\u00F4ng ph\u1EA3i c\u00E1c tile r\u1EDDi -->\n <div data-fx-skel=\"kpi-strip\" class=\"card-common !p-0\">\n <div class=\"grid grid-cols-2 md:grid-cols-4\">\n @for (i of items(b, 4); track i) {\n <div\n class=\"flex flex-col gap-normal px-xl py-large border-border-subtle\"\n [class.border-l]=\"!$first\"\n >\n <div class=\"skeleton-load h-3 w-20\"></div>\n <div class=\"skeleton-load h-9 w-28\"></div>\n <div class=\"skeleton-load h-3 w-24\"></div>\n </div>\n }\n </div>\n </div>\n }\n\n @case ('panels') {\n <div data-fx-skel=\"panels\" class=\"grid gap-xl\" [class]=\"gridClass(b)\">\n @for (i of items(b, 2); track i) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"skeleton-load h-4 w-1/3\"></div>\n <div class=\"flex items-end gap-normal h-40\">\n @for (x of bars; track x) {\n <div class=\"skeleton-load flex-1 rounded-t\" [style.height.%]=\"barHeight(x)\"></div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @case ('cards') {\n <div data-fx-skel=\"cards\" class=\"grid gap-xl\" [class]=\"gridClass(b)\">\n @for (i of items(b, 6); track i) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"flex items-center gap-normal\">\n <div class=\"skeleton-load w-9 h-9 rounded-lg shrink-0\"></div>\n <div class=\"flex flex-col gap-small flex-1 min-w-0\">\n <div class=\"skeleton-load h-4 w-3/4\"></div>\n <div class=\"skeleton-load h-3 w-1/2\"></div>\n </div>\n <div class=\"skeleton-load h-5 w-14 rounded-full shrink-0\"></div>\n </div>\n <div class=\"grid grid-cols-4 gap-normal pt-normal border-t border-border-subtle\">\n @for (m of [0, 1, 2, 3]; track m) {\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-2.5 w-full\"></div>\n <div class=\"skeleton-load h-4 w-2/3\"></div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @case ('card') {\n <div data-fx-skel=\"card\" class=\"card-common flex flex-col gap-large\">\n <div class=\"skeleton-load h-4 w-1/3\"></div>\n <div class=\"flex flex-col gap-normal\">\n @for (r of [0, 1, 2, 3]; track r) {\n <div class=\"skeleton-load h-3\" [style.width.%]=\"cellWidth(r, 1)\"></div>\n }\n </div>\n </div>\n }\n\n @case ('tabs') {\n <div data-fx-skel=\"tabs\" class=\"flex items-center gap-large border-b border-border-default pb-normal\">\n @for (t of items(b, 6); track t) {\n <div class=\"flex items-center gap-small\">\n <div class=\"skeleton-load w-4 h-4\"></div>\n <div class=\"skeleton-load h-3 w-16\"></div>\n </div>\n }\n </div>\n }\n\n @case ('facets') {\n <div data-fx-skel=\"facets\" class=\"fx-rs-row\">\n <div class=\"fx-rs-facets card-common overflow-hidden\">\n <fx-ui-skeleton-facets />\n </div>\n <div class=\"fx-rs-list card-common overflow-hidden\">\n <fx-ui-skeleton-list [rows]=\"6\" />\n </div>\n <div class=\"flex-1 min-w-0 overflow-hidden\">\n <fx-ui-skeleton-detail />\n </div>\n </div>\n }\n\n @case ('detail') {\n <fx-ui-skeleton-detail data-fx-skel=\"detail\" />\n }\n\n @case ('form') {\n <div data-fx-skel=\"form\" class=\"card-common flex flex-col gap-large\">\n @for (r of [0, 1, 2, 3]; track r) {\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-3 w-24\"></div>\n <div class=\"skeleton-load h-9 w-full rounded-lg\"></div>\n </div>\n }\n <div class=\"flex justify-end gap-normal\">\n <div class=\"skeleton-load h-9 w-24 rounded-lg\"></div>\n <div class=\"skeleton-load h-9 w-28 rounded-lg\"></div>\n </div>\n </div>\n }\n }\n }\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-rs}.fx-rs-row{display:flex;flex-direction:column;gap:var(--spacing-xl, 1.5rem)}@container fx-rs (min-width: 968px){.fx-rs-row{flex-direction:row;align-items:stretch;height:calc(100vh - 300px);min-height:420px}.fx-rs-facets{flex:0 1 230px;min-width:200px;max-width:260px}.fx-rs-list{flex:0 1 420px;min-width:320px;max-width:420px}}\n"], dependencies: [{ kind: "component", type: SkeletonListComponent, selector: "fx-ui-skeleton-list", inputs: ["rows"] }, { kind: "component", type: SkeletonDetailComponent, selector: "fx-ui-skeleton-detail", inputs: ["cards", "rows"] }, { kind: "component", type: SkeletonFacetsComponent, selector: "fx-ui-skeleton-facets", inputs: ["groups", "rows"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5641
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: RouteSkeletonComponent, isStandalone: true, selector: "fx-ui-route-skeleton", inputs: { hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"w-full flex flex-col gap-xl\">\n @for (b of blocks(); track $index) {\n @switch (b.kind) {\n @case ('heading') {\n <div data-fx-skel=\"heading\" class=\"flex flex-col md:flex-row md:items-start justify-between gap-normal\">\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-7 w-64\"></div>\n <div class=\"skeleton-load h-3 w-32\"></div>\n </div>\n @if (items(b, 2).length) {\n <div class=\"flex items-center gap-normal\">\n @for (a of items(b, 2); track a) {\n <div class=\"skeleton-load h-9 rounded-lg\" [class]=\"a === 0 ? 'w-28' : 'w-32'\"></div>\n }\n </div>\n }\n </div>\n }\n\n @case ('toolbar') {\n <div data-fx-skel=\"toolbar\" class=\"flex flex-wrap items-center gap-normal\">\n <div class=\"skeleton-load h-9 w-72 rounded-lg\"></div>\n @for (f of items(b, 2); track f) {\n <div class=\"skeleton-load h-7 rounded-full\" [class]=\"f % 2 ? 'w-24' : 'w-20'\"></div>\n }\n </div>\n }\n\n @case ('table') {\n <!-- B\u1EA3ng: m\u1ED7i \u00D4 l\u00E0 m\u1ED9t `.skeleton-load` ri\u00EAng, \u0111\u00FAng c\u00E1ch\n `fx-ui-table-cell [loading]` l\u00E0m tr\u00EAn b\u1EA3ng th\u1EADt. -->\n <div data-fx-skel=\"table\" class=\"card-common overflow-x-auto\">\n <table class=\"w-full\">\n <thead>\n <tr class=\"border-b border-border-default\">\n @for (c of cols(b); track c) {\n <th class=\"px-normal py-semi text-left\">\n <div class=\"skeleton-load h-3\" [style.width.%]=\"headWidth(c)\"></div>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (r of rows(b); track r) {\n <tr class=\"border-b border-border-subtle\">\n @for (c of cols(b); track c) {\n <td class=\"px-normal py-semi align-middle\">\n <div class=\"skeleton-load h-4\" [style.width.%]=\"cellWidth(r, c)\"></div>\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n @case ('stat-cards') {\n <div data-fx-skel=\"stat-cards\" class=\"grid gap-normal grid-cols-2 md:grid-cols-4\">\n @for (i of items(b, 4); track i) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"skeleton-load h-3 w-16\"></div>\n <div class=\"skeleton-load h-8 w-24\"></div>\n <div class=\"skeleton-load h-3 w-20\"></div>\n </div>\n }\n </div>\n }\n\n @case ('kpi-strip') {\n <!-- M\u1ED8T card chia c\u1ED9t (ki\u1EC3u app-dash-kpi-row), kh\u00F4ng ph\u1EA3i c\u00E1c tile r\u1EDDi -->\n <div data-fx-skel=\"kpi-strip\" class=\"card-common !p-0\">\n <div class=\"grid grid-cols-2 md:grid-cols-4\">\n @for (i of items(b, 4); track i) {\n <div\n class=\"flex flex-col gap-normal px-xl py-large border-border-subtle\"\n [class.border-l]=\"!$first\"\n >\n <div class=\"skeleton-load h-3 w-20\"></div>\n <div class=\"skeleton-load h-9 w-28\"></div>\n <div class=\"skeleton-load h-3 w-24\"></div>\n </div>\n }\n </div>\n </div>\n }\n\n @case ('panels') {\n <div data-fx-skel=\"panels\" class=\"grid gap-xl\" [class]=\"gridClass(b)\">\n @for (i of items(b, 2); track i) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"skeleton-load h-4 w-1/3\"></div>\n <div class=\"flex items-end gap-normal h-40\">\n @for (x of bars; track x) {\n <div class=\"skeleton-load flex-1 rounded-t\" [style.height.%]=\"barHeight(x)\"></div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @case ('cards') {\n <div data-fx-skel=\"cards\" class=\"grid gap-xl\" [class]=\"gridClass(b)\">\n @for (i of items(b, 6); track i) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"flex items-center gap-normal\">\n <div class=\"skeleton-load w-9 h-9 rounded-lg shrink-0\"></div>\n <div class=\"flex flex-col gap-small flex-1 min-w-0\">\n <div class=\"skeleton-load h-4 w-3/4\"></div>\n <div class=\"skeleton-load h-3 w-1/2\"></div>\n </div>\n <div class=\"skeleton-load h-5 w-14 rounded-full shrink-0\"></div>\n </div>\n <div class=\"grid grid-cols-4 gap-normal pt-normal border-t border-border-subtle\">\n @for (m of [0, 1, 2, 3]; track m) {\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-2.5 w-full\"></div>\n <div class=\"skeleton-load h-4 w-2/3\"></div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @case ('card') {\n <div data-fx-skel=\"card\" class=\"card-common flex flex-col gap-large\">\n <div class=\"skeleton-load h-4 w-1/3\"></div>\n <div class=\"flex flex-col gap-normal\">\n @for (r of [0, 1, 2, 3]; track r) {\n <div class=\"skeleton-load h-3\" [style.width.%]=\"cellWidth(r, 1)\"></div>\n }\n </div>\n </div>\n }\n\n @case ('tabs') {\n <div data-fx-skel=\"tabs\" class=\"flex items-center gap-large border-b border-border-default pb-normal\">\n @for (t of items(b, 6); track t) {\n <div class=\"flex items-center gap-small\">\n <div class=\"skeleton-load w-4 h-4\"></div>\n <div class=\"skeleton-load h-3 w-16\"></div>\n </div>\n }\n </div>\n }\n\n @case ('facets') {\n <div data-fx-skel=\"facets\" class=\"fx-rs-row\">\n <div class=\"fx-rs-facets card-common overflow-hidden\">\n <fx-ui-skeleton-facets />\n </div>\n <div class=\"fx-rs-list card-common overflow-hidden\">\n <fx-ui-skeleton-list [rows]=\"6\" />\n </div>\n <div class=\"flex-1 min-w-0 overflow-hidden\">\n <fx-ui-skeleton-detail />\n </div>\n </div>\n }\n\n @case ('detail') {\n <fx-ui-skeleton-detail data-fx-skel=\"detail\" />\n }\n\n @case ('form') {\n <div data-fx-skel=\"form\" class=\"card-common flex flex-col gap-large\">\n @for (r of [0, 1, 2, 3]; track r) {\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-3 w-24\"></div>\n <div class=\"skeleton-load h-9 w-full rounded-lg\"></div>\n </div>\n }\n <div class=\"flex justify-end gap-normal\">\n <div class=\"skeleton-load h-9 w-24 rounded-lg\"></div>\n <div class=\"skeleton-load h-9 w-28 rounded-lg\"></div>\n </div>\n </div>\n }\n }\n }\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-rs}.fx-rs-row{display:flex;flex-direction:column;gap:var(--spacing-xl, 1.5rem)}@container fx-rs (min-width: 720px){.fx-rs-row{flex-direction:row;align-items:stretch;height:calc(100vh - 300px);min-height:420px}.fx-rs-facets{flex:0 1 230px;min-width:200px;max-width:260px}.fx-rs-list{flex:0 1 420px;min-width:320px;max-width:420px}}\n"], dependencies: [{ kind: "component", type: SkeletonListComponent, selector: "fx-ui-skeleton-list", inputs: ["rows"] }, { kind: "component", type: SkeletonDetailComponent, selector: "fx-ui-skeleton-detail", inputs: ["cards", "rows"] }, { kind: "component", type: SkeletonFacetsComponent, selector: "fx-ui-skeleton-facets", inputs: ["groups", "rows"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5249
5642
  }
5250
5643
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: RouteSkeletonComponent, decorators: [{
5251
5644
  type: Component,
5252
- args: [{ selector: 'fx-ui-route-skeleton', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SkeletonListComponent, SkeletonDetailComponent, SkeletonFacetsComponent], template: "<div class=\"w-full flex flex-col gap-xl\">\n @for (b of blocks(); track $index) {\n @switch (b.kind) {\n @case ('heading') {\n <div data-fx-skel=\"heading\" class=\"flex flex-col md:flex-row md:items-start justify-between gap-normal\">\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-7 w-64\"></div>\n <div class=\"skeleton-load h-3 w-32\"></div>\n </div>\n @if (items(b, 2).length) {\n <div class=\"flex items-center gap-normal\">\n @for (a of items(b, 2); track a) {\n <div class=\"skeleton-load h-9 rounded-lg\" [class]=\"a === 0 ? 'w-28' : 'w-32'\"></div>\n }\n </div>\n }\n </div>\n }\n\n @case ('toolbar') {\n <div data-fx-skel=\"toolbar\" class=\"flex flex-wrap items-center gap-normal\">\n <div class=\"skeleton-load h-9 w-72 rounded-lg\"></div>\n @for (f of items(b, 2); track f) {\n <div class=\"skeleton-load h-7 rounded-full\" [class]=\"f % 2 ? 'w-24' : 'w-20'\"></div>\n }\n </div>\n }\n\n @case ('table') {\n <!-- B\u1EA3ng: m\u1ED7i \u00D4 l\u00E0 m\u1ED9t `.skeleton-load` ri\u00EAng, \u0111\u00FAng c\u00E1ch\n `fx-ui-table-cell [loading]` l\u00E0m tr\u00EAn b\u1EA3ng th\u1EADt. -->\n <div data-fx-skel=\"table\" class=\"card-common overflow-x-auto\">\n <table class=\"w-full\">\n <thead>\n <tr class=\"border-b border-border-default\">\n @for (c of cols(b); track c) {\n <th class=\"px-normal py-semi text-left\">\n <div class=\"skeleton-load h-3\" [style.width.%]=\"headWidth(c)\"></div>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (r of rows(b); track r) {\n <tr class=\"border-b border-border-subtle\">\n @for (c of cols(b); track c) {\n <td class=\"px-normal py-semi align-middle\">\n <div class=\"skeleton-load h-4\" [style.width.%]=\"cellWidth(r, c)\"></div>\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n @case ('stat-cards') {\n <div data-fx-skel=\"stat-cards\" class=\"grid gap-normal grid-cols-2 md:grid-cols-4\">\n @for (i of items(b, 4); track i) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"skeleton-load h-3 w-16\"></div>\n <div class=\"skeleton-load h-8 w-24\"></div>\n <div class=\"skeleton-load h-3 w-20\"></div>\n </div>\n }\n </div>\n }\n\n @case ('kpi-strip') {\n <!-- M\u1ED8T card chia c\u1ED9t (ki\u1EC3u app-dash-kpi-row), kh\u00F4ng ph\u1EA3i c\u00E1c tile r\u1EDDi -->\n <div data-fx-skel=\"kpi-strip\" class=\"card-common !p-0\">\n <div class=\"grid grid-cols-2 md:grid-cols-4\">\n @for (i of items(b, 4); track i) {\n <div\n class=\"flex flex-col gap-normal px-xl py-large border-border-subtle\"\n [class.border-l]=\"!$first\"\n >\n <div class=\"skeleton-load h-3 w-20\"></div>\n <div class=\"skeleton-load h-9 w-28\"></div>\n <div class=\"skeleton-load h-3 w-24\"></div>\n </div>\n }\n </div>\n </div>\n }\n\n @case ('panels') {\n <div data-fx-skel=\"panels\" class=\"grid gap-xl\" [class]=\"gridClass(b)\">\n @for (i of items(b, 2); track i) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"skeleton-load h-4 w-1/3\"></div>\n <div class=\"flex items-end gap-normal h-40\">\n @for (x of bars; track x) {\n <div class=\"skeleton-load flex-1 rounded-t\" [style.height.%]=\"barHeight(x)\"></div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @case ('cards') {\n <div data-fx-skel=\"cards\" class=\"grid gap-xl\" [class]=\"gridClass(b)\">\n @for (i of items(b, 6); track i) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"flex items-center gap-normal\">\n <div class=\"skeleton-load w-9 h-9 rounded-lg shrink-0\"></div>\n <div class=\"flex flex-col gap-small flex-1 min-w-0\">\n <div class=\"skeleton-load h-4 w-3/4\"></div>\n <div class=\"skeleton-load h-3 w-1/2\"></div>\n </div>\n <div class=\"skeleton-load h-5 w-14 rounded-full shrink-0\"></div>\n </div>\n <div class=\"grid grid-cols-4 gap-normal pt-normal border-t border-border-subtle\">\n @for (m of [0, 1, 2, 3]; track m) {\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-2.5 w-full\"></div>\n <div class=\"skeleton-load h-4 w-2/3\"></div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @case ('card') {\n <div data-fx-skel=\"card\" class=\"card-common flex flex-col gap-large\">\n <div class=\"skeleton-load h-4 w-1/3\"></div>\n <div class=\"flex flex-col gap-normal\">\n @for (r of [0, 1, 2, 3]; track r) {\n <div class=\"skeleton-load h-3\" [style.width.%]=\"cellWidth(r, 1)\"></div>\n }\n </div>\n </div>\n }\n\n @case ('tabs') {\n <div data-fx-skel=\"tabs\" class=\"flex items-center gap-large border-b border-border-default pb-normal\">\n @for (t of items(b, 6); track t) {\n <div class=\"flex items-center gap-small\">\n <div class=\"skeleton-load w-4 h-4\"></div>\n <div class=\"skeleton-load h-3 w-16\"></div>\n </div>\n }\n </div>\n }\n\n @case ('facets') {\n <div data-fx-skel=\"facets\" class=\"fx-rs-row\">\n <div class=\"fx-rs-facets card-common overflow-hidden\">\n <fx-ui-skeleton-facets />\n </div>\n <div class=\"fx-rs-list card-common overflow-hidden\">\n <fx-ui-skeleton-list [rows]=\"6\" />\n </div>\n <div class=\"flex-1 min-w-0 overflow-hidden\">\n <fx-ui-skeleton-detail />\n </div>\n </div>\n }\n\n @case ('detail') {\n <fx-ui-skeleton-detail data-fx-skel=\"detail\" />\n }\n\n @case ('form') {\n <div data-fx-skel=\"form\" class=\"card-common flex flex-col gap-large\">\n @for (r of [0, 1, 2, 3]; track r) {\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-3 w-24\"></div>\n <div class=\"skeleton-load h-9 w-full rounded-lg\"></div>\n </div>\n }\n <div class=\"flex justify-end gap-normal\">\n <div class=\"skeleton-load h-9 w-24 rounded-lg\"></div>\n <div class=\"skeleton-load h-9 w-28 rounded-lg\"></div>\n </div>\n </div>\n }\n }\n }\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-rs}.fx-rs-row{display:flex;flex-direction:column;gap:var(--spacing-xl, 1.5rem)}@container fx-rs (min-width: 968px){.fx-rs-row{flex-direction:row;align-items:stretch;height:calc(100vh - 300px);min-height:420px}.fx-rs-facets{flex:0 1 230px;min-width:200px;max-width:260px}.fx-rs-list{flex:0 1 420px;min-width:320px;max-width:420px}}\n"] }]
5645
+ args: [{ selector: 'fx-ui-route-skeleton', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SkeletonListComponent, SkeletonDetailComponent, SkeletonFacetsComponent], template: "<div class=\"w-full flex flex-col gap-xl\">\n @for (b of blocks(); track $index) {\n @switch (b.kind) {\n @case ('heading') {\n <div data-fx-skel=\"heading\" class=\"flex flex-col md:flex-row md:items-start justify-between gap-normal\">\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-7 w-64\"></div>\n <div class=\"skeleton-load h-3 w-32\"></div>\n </div>\n @if (items(b, 2).length) {\n <div class=\"flex items-center gap-normal\">\n @for (a of items(b, 2); track a) {\n <div class=\"skeleton-load h-9 rounded-lg\" [class]=\"a === 0 ? 'w-28' : 'w-32'\"></div>\n }\n </div>\n }\n </div>\n }\n\n @case ('toolbar') {\n <div data-fx-skel=\"toolbar\" class=\"flex flex-wrap items-center gap-normal\">\n <div class=\"skeleton-load h-9 w-72 rounded-lg\"></div>\n @for (f of items(b, 2); track f) {\n <div class=\"skeleton-load h-7 rounded-full\" [class]=\"f % 2 ? 'w-24' : 'w-20'\"></div>\n }\n </div>\n }\n\n @case ('table') {\n <!-- B\u1EA3ng: m\u1ED7i \u00D4 l\u00E0 m\u1ED9t `.skeleton-load` ri\u00EAng, \u0111\u00FAng c\u00E1ch\n `fx-ui-table-cell [loading]` l\u00E0m tr\u00EAn b\u1EA3ng th\u1EADt. -->\n <div data-fx-skel=\"table\" class=\"card-common overflow-x-auto\">\n <table class=\"w-full\">\n <thead>\n <tr class=\"border-b border-border-default\">\n @for (c of cols(b); track c) {\n <th class=\"px-normal py-semi text-left\">\n <div class=\"skeleton-load h-3\" [style.width.%]=\"headWidth(c)\"></div>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (r of rows(b); track r) {\n <tr class=\"border-b border-border-subtle\">\n @for (c of cols(b); track c) {\n <td class=\"px-normal py-semi align-middle\">\n <div class=\"skeleton-load h-4\" [style.width.%]=\"cellWidth(r, c)\"></div>\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n @case ('stat-cards') {\n <div data-fx-skel=\"stat-cards\" class=\"grid gap-normal grid-cols-2 md:grid-cols-4\">\n @for (i of items(b, 4); track i) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"skeleton-load h-3 w-16\"></div>\n <div class=\"skeleton-load h-8 w-24\"></div>\n <div class=\"skeleton-load h-3 w-20\"></div>\n </div>\n }\n </div>\n }\n\n @case ('kpi-strip') {\n <!-- M\u1ED8T card chia c\u1ED9t (ki\u1EC3u app-dash-kpi-row), kh\u00F4ng ph\u1EA3i c\u00E1c tile r\u1EDDi -->\n <div data-fx-skel=\"kpi-strip\" class=\"card-common !p-0\">\n <div class=\"grid grid-cols-2 md:grid-cols-4\">\n @for (i of items(b, 4); track i) {\n <div\n class=\"flex flex-col gap-normal px-xl py-large border-border-subtle\"\n [class.border-l]=\"!$first\"\n >\n <div class=\"skeleton-load h-3 w-20\"></div>\n <div class=\"skeleton-load h-9 w-28\"></div>\n <div class=\"skeleton-load h-3 w-24\"></div>\n </div>\n }\n </div>\n </div>\n }\n\n @case ('panels') {\n <div data-fx-skel=\"panels\" class=\"grid gap-xl\" [class]=\"gridClass(b)\">\n @for (i of items(b, 2); track i) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"skeleton-load h-4 w-1/3\"></div>\n <div class=\"flex items-end gap-normal h-40\">\n @for (x of bars; track x) {\n <div class=\"skeleton-load flex-1 rounded-t\" [style.height.%]=\"barHeight(x)\"></div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @case ('cards') {\n <div data-fx-skel=\"cards\" class=\"grid gap-xl\" [class]=\"gridClass(b)\">\n @for (i of items(b, 6); track i) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"flex items-center gap-normal\">\n <div class=\"skeleton-load w-9 h-9 rounded-lg shrink-0\"></div>\n <div class=\"flex flex-col gap-small flex-1 min-w-0\">\n <div class=\"skeleton-load h-4 w-3/4\"></div>\n <div class=\"skeleton-load h-3 w-1/2\"></div>\n </div>\n <div class=\"skeleton-load h-5 w-14 rounded-full shrink-0\"></div>\n </div>\n <div class=\"grid grid-cols-4 gap-normal pt-normal border-t border-border-subtle\">\n @for (m of [0, 1, 2, 3]; track m) {\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-2.5 w-full\"></div>\n <div class=\"skeleton-load h-4 w-2/3\"></div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @case ('card') {\n <div data-fx-skel=\"card\" class=\"card-common flex flex-col gap-large\">\n <div class=\"skeleton-load h-4 w-1/3\"></div>\n <div class=\"flex flex-col gap-normal\">\n @for (r of [0, 1, 2, 3]; track r) {\n <div class=\"skeleton-load h-3\" [style.width.%]=\"cellWidth(r, 1)\"></div>\n }\n </div>\n </div>\n }\n\n @case ('tabs') {\n <div data-fx-skel=\"tabs\" class=\"flex items-center gap-large border-b border-border-default pb-normal\">\n @for (t of items(b, 6); track t) {\n <div class=\"flex items-center gap-small\">\n <div class=\"skeleton-load w-4 h-4\"></div>\n <div class=\"skeleton-load h-3 w-16\"></div>\n </div>\n }\n </div>\n }\n\n @case ('facets') {\n <div data-fx-skel=\"facets\" class=\"fx-rs-row\">\n <div class=\"fx-rs-facets card-common overflow-hidden\">\n <fx-ui-skeleton-facets />\n </div>\n <div class=\"fx-rs-list card-common overflow-hidden\">\n <fx-ui-skeleton-list [rows]=\"6\" />\n </div>\n <div class=\"flex-1 min-w-0 overflow-hidden\">\n <fx-ui-skeleton-detail />\n </div>\n </div>\n }\n\n @case ('detail') {\n <fx-ui-skeleton-detail data-fx-skel=\"detail\" />\n }\n\n @case ('form') {\n <div data-fx-skel=\"form\" class=\"card-common flex flex-col gap-large\">\n @for (r of [0, 1, 2, 3]; track r) {\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-3 w-24\"></div>\n <div class=\"skeleton-load h-9 w-full rounded-lg\"></div>\n </div>\n }\n <div class=\"flex justify-end gap-normal\">\n <div class=\"skeleton-load h-9 w-24 rounded-lg\"></div>\n <div class=\"skeleton-load h-9 w-28 rounded-lg\"></div>\n </div>\n </div>\n }\n }\n }\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-rs}.fx-rs-row{display:flex;flex-direction:column;gap:var(--spacing-xl, 1.5rem)}@container fx-rs (min-width: 720px){.fx-rs-row{flex-direction:row;align-items:stretch;height:calc(100vh - 300px);min-height:420px}.fx-rs-facets{flex:0 1 230px;min-width:200px;max-width:260px}.fx-rs-list{flex:0 1 420px;min-width:320px;max-width:420px}}\n"] }]
5253
5646
  }], propDecorators: { hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }] } });
5254
5647
  function range$1(n) {
5255
5648
  return Array.from({ length: Math.max(0, Number(n) || 0) }, (_, i) => i);
@@ -5328,6 +5721,8 @@ const FX_COMPONENTS = [
5328
5721
  MenuComponent,
5329
5722
  ThemePickerComponent,
5330
5723
  PendingBadgeComponent,
5724
+ BreadcrumbComponent,
5725
+ LoadStatusComponent,
5331
5726
  SampleFrameComponent,
5332
5727
  ScoreBadgeComponent,
5333
5728
  StatusDotComponent,
@@ -5348,6 +5743,126 @@ const DIALOGS_COMPONENT = [
5348
5743
  ConfirmationDialogComponent
5349
5744
  ];
5350
5745
 
5746
+ /**
5747
+ * Staggered entrance for one element (GSAP).
5748
+ *
5749
+ * <fx-ui-section-card [fxReveal]="2">…</fx-ui-section-card>
5750
+ *
5751
+ * The host starts invisible and rises into place; `fxReveal` is its slot in
5752
+ * the stagger (× 60 ms). Elements that mount later (async lists) animate on
5753
+ * their own mount, so late data arrives with motion instead of a jump.
5754
+ * Reduced-motion users see the final state immediately. For a whole page use
5755
+ * `fxRevealGroup` on the container instead of numbering children by hand.
5756
+ */
5757
+ class FxRevealDirective {
5758
+ /** Stagger slot; 0 = first. */
5759
+ fxReveal = input(0, ...(ngDevMode ? [{ debugName: "fxReveal" }] : /* istanbul ignore next */ []));
5760
+ el = inject(ElementRef).nativeElement;
5761
+ constructor() {
5762
+ if (!fxMotionAllowed())
5763
+ return;
5764
+ // Hide before first paint — otherwise the element flashes, then animates.
5765
+ this.el.style.opacity = '0';
5766
+ afterNextRender(() => fxRevealElement(this.el, Number(this.fxReveal()) || 0));
5767
+ }
5768
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FxRevealDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
5769
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.20", type: FxRevealDirective, isStandalone: true, selector: "[fxReveal]", inputs: { fxReveal: { classPropertyName: "fxReveal", publicName: "fxReveal", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
5770
+ }
5771
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FxRevealDirective, decorators: [{
5772
+ type: Directive,
5773
+ args: [{
5774
+ selector: '[fxReveal]',
5775
+ }]
5776
+ }], ctorParameters: () => [], propDecorators: { fxReveal: [{ type: i0.Input, args: [{ isSignal: true, alias: "fxReveal", required: false }] }] } });
5777
+ /** Run the standard reveal on an element at the given stagger slot. */
5778
+ function fxRevealElement(el, slot = 0) {
5779
+ gsap.fromTo(el, { opacity: 0, y: FX_MOTION.travelPx }, {
5780
+ opacity: 1,
5781
+ y: 0,
5782
+ duration: FX_MOTION.duration,
5783
+ delay: Math.min(Math.max(slot, 0), FX_MOTION.maxSlot) * FX_MOTION.stagger,
5784
+ ease: FX_MOTION.ease,
5785
+ clearProps: 'opacity,transform',
5786
+ overwrite: 'auto',
5787
+ });
5788
+ }
5789
+
5790
+ /**
5791
+ * One attribute on a page root reveals its sections top-to-bottom.
5792
+ *
5793
+ * <div fxRevealGroup> ← page root
5794
+ * <div>heading…</div> ← slot 0
5795
+ * <fx-ui-stat-cards …/> ← slot 1
5796
+ * <div class="grid …">…</div> ← slot 2 (the grid moves as one block)
5797
+ * </div>
5798
+ *
5799
+ * Direct element children present at first render get consecutive slots.
5800
+ * Children added later (a list that arrives after its request, an `@if` that
5801
+ * flips) are revealed as they mount, so async content never pops in flat.
5802
+ * Skeletons (`fx-ui-route-skeleton`, `[aria-busy]`) are left alone. Replaces
5803
+ * the page-level `animate.enter="fade-in-animation"` one-shot fade.
5804
+ */
5805
+ class FxRevealGroupDirective {
5806
+ /** Cap on how many children get their own slot; the rest share the last one. */
5807
+ fxRevealGroup = input(undefined, ...(ngDevMode ? [{ debugName: "fxRevealGroup" }] : /* istanbul ignore next */ []));
5808
+ el = inject(ElementRef).nativeElement;
5809
+ observer;
5810
+ seen = new WeakSet();
5811
+ constructor() {
5812
+ if (!fxMotionAllowed())
5813
+ return;
5814
+ // Children are hidden right away so nothing paints before its reveal.
5815
+ for (const child of this.children())
5816
+ this.hide(child);
5817
+ afterNextRender(() => {
5818
+ const kids = this.children();
5819
+ kids.forEach((child, i) => this.reveal(child, i));
5820
+ this.observer = new MutationObserver((records) => {
5821
+ for (const r of records) {
5822
+ r.addedNodes.forEach((n) => {
5823
+ if (n instanceof HTMLElement && n.parentElement === this.el && this.eligible(n) && !this.seen.has(n)) {
5824
+ this.hide(n);
5825
+ this.reveal(n, 0);
5826
+ }
5827
+ });
5828
+ }
5829
+ });
5830
+ this.observer.observe(this.el, { childList: true });
5831
+ });
5832
+ }
5833
+ children() {
5834
+ return Array.from(this.el.children).filter((c) => c instanceof HTMLElement && this.eligible(c));
5835
+ }
5836
+ eligible(c) {
5837
+ const tag = c.tagName.toLowerCase();
5838
+ return tag !== 'fx-ui-route-skeleton' && tag !== 'script' && tag !== 'style' && !c.hasAttribute('aria-busy');
5839
+ }
5840
+ hide(c) {
5841
+ if (this.seen.has(c))
5842
+ return;
5843
+ this.seen.add(c);
5844
+ c.style.opacity = '0';
5845
+ }
5846
+ reveal(c, slot) {
5847
+ const cap = Number(this.fxRevealGroup());
5848
+ fxRevealElement(c, Number.isFinite(cap) && cap > 0 ? Math.min(slot, cap) : slot);
5849
+ }
5850
+ ngOnDestroy() {
5851
+ this.observer?.disconnect();
5852
+ }
5853
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FxRevealGroupDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
5854
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.20", type: FxRevealGroupDirective, isStandalone: true, selector: "[fxRevealGroup]", inputs: { fxRevealGroup: { classPropertyName: "fxRevealGroup", publicName: "fxRevealGroup", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
5855
+ }
5856
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FxRevealGroupDirective, decorators: [{
5857
+ type: Directive,
5858
+ args: [{
5859
+ selector: '[fxRevealGroup]',
5860
+ }]
5861
+ }], ctorParameters: () => [], propDecorators: { fxRevealGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "fxRevealGroup", required: false }] }] } });
5862
+
5863
+ /** Motion directives exported by UiModule (GSAP-based, reduced-motion aware). */
5864
+ const FX_MOTION_DIRECTIVES = [FxRevealDirective, FxRevealGroupDirective, FxCountUpDirective];
5865
+
5351
5866
  /**
5352
5867
  * Compatibility barrel over the (now standalone) core components.
5353
5868
  *
@@ -5365,7 +5880,7 @@ class UiModule {
5365
5880
  ReactiveFormsModule,
5366
5881
  RouterModule,
5367
5882
  HasPermissionDirective,
5368
- TrimOnBlurDirective, InputComponent, SelectComponent, RadioButtonComponent, CheckboxComponent, DndUploadComponent, ButtonComponent, RadioButtonToggleComponent, DatetimePicker, LoadingPanel, SearchBarComponent, TabGroupComponent, TabComponent, HeroIconComponent, ToastComponent, ToastContainerComponent, TagComponent, SliderComponent, SwitchComponent, CircleProgressBar, TreeDiagram, TableCell, KanbanBoardComponent, DrawerComponent, SkeletonListComponent, SkeletonDetailComponent, StatCardsComponent, EmptyStateComponent, FilterChipsComponent, FilterPillsComponent, StageDotsComponent, CodeBlockComponent, ScreenshotFrameComponent, MeterComponent, SectionCardComponent, InfoFieldComponent, SeverityBadgesComponent, MasterDetailComponent, MenuComponent, ThemePickerComponent, PendingBadgeComponent, SampleFrameComponent, ScoreBadgeComponent, StatusDotComponent, SparkbarComponent, SegmentedComponent, ListRowComponent, BarListComponent, FacetsRailComponent, ProjectCardComponent, PipelineStepperComponent, SkeletonFacetsComponent, SkeletonDashboardComponent, SkeletonFrameComponent, RouteSkeletonComponent, ConfirmationDialogComponent], exports: [InputComponent, SelectComponent, RadioButtonComponent, CheckboxComponent, DndUploadComponent, ButtonComponent, RadioButtonToggleComponent, DatetimePicker, LoadingPanel, SearchBarComponent, TabGroupComponent, TabComponent, HeroIconComponent, ToastComponent, ToastContainerComponent, TagComponent, SliderComponent, SwitchComponent, CircleProgressBar, TreeDiagram, TableCell, KanbanBoardComponent, DrawerComponent, SkeletonListComponent, SkeletonDetailComponent, StatCardsComponent, EmptyStateComponent, FilterChipsComponent, FilterPillsComponent, StageDotsComponent, CodeBlockComponent, ScreenshotFrameComponent, MeterComponent, SectionCardComponent, InfoFieldComponent, SeverityBadgesComponent, MasterDetailComponent, MenuComponent, ThemePickerComponent, PendingBadgeComponent, SampleFrameComponent, ScoreBadgeComponent, StatusDotComponent, SparkbarComponent, SegmentedComponent, ListRowComponent, BarListComponent, FacetsRailComponent, ProjectCardComponent, PipelineStepperComponent, SkeletonFacetsComponent, SkeletonDashboardComponent, SkeletonFrameComponent, RouteSkeletonComponent, ConfirmationDialogComponent, FormsModule,
5883
+ TrimOnBlurDirective, InputComponent, SelectComponent, RadioButtonComponent, CheckboxComponent, DndUploadComponent, ButtonComponent, RadioButtonToggleComponent, DatetimePicker, LoadingPanel, SearchBarComponent, TabGroupComponent, TabComponent, HeroIconComponent, ToastComponent, ToastContainerComponent, TagComponent, SliderComponent, SwitchComponent, CircleProgressBar, TreeDiagram, TableCell, KanbanBoardComponent, DrawerComponent, SkeletonListComponent, SkeletonDetailComponent, StatCardsComponent, EmptyStateComponent, FilterChipsComponent, FilterPillsComponent, StageDotsComponent, CodeBlockComponent, ScreenshotFrameComponent, MeterComponent, SectionCardComponent, InfoFieldComponent, SeverityBadgesComponent, MasterDetailComponent, MenuComponent, ThemePickerComponent, PendingBadgeComponent, BreadcrumbComponent, LoadStatusComponent, SampleFrameComponent, ScoreBadgeComponent, StatusDotComponent, SparkbarComponent, SegmentedComponent, ListRowComponent, BarListComponent, FacetsRailComponent, ProjectCardComponent, PipelineStepperComponent, SkeletonFacetsComponent, SkeletonDashboardComponent, SkeletonFrameComponent, RouteSkeletonComponent, ConfirmationDialogComponent, FxRevealDirective, FxRevealGroupDirective, FxCountUpDirective], exports: [InputComponent, SelectComponent, RadioButtonComponent, CheckboxComponent, DndUploadComponent, ButtonComponent, RadioButtonToggleComponent, DatetimePicker, LoadingPanel, SearchBarComponent, TabGroupComponent, TabComponent, HeroIconComponent, ToastComponent, ToastContainerComponent, TagComponent, SliderComponent, SwitchComponent, CircleProgressBar, TreeDiagram, TableCell, KanbanBoardComponent, DrawerComponent, SkeletonListComponent, SkeletonDetailComponent, StatCardsComponent, EmptyStateComponent, FilterChipsComponent, FilterPillsComponent, StageDotsComponent, CodeBlockComponent, ScreenshotFrameComponent, MeterComponent, SectionCardComponent, InfoFieldComponent, SeverityBadgesComponent, MasterDetailComponent, MenuComponent, ThemePickerComponent, PendingBadgeComponent, BreadcrumbComponent, LoadStatusComponent, SampleFrameComponent, ScoreBadgeComponent, StatusDotComponent, SparkbarComponent, SegmentedComponent, ListRowComponent, BarListComponent, FacetsRailComponent, ProjectCardComponent, PipelineStepperComponent, SkeletonFacetsComponent, SkeletonDashboardComponent, SkeletonFrameComponent, RouteSkeletonComponent, ConfirmationDialogComponent, FxRevealDirective, FxRevealGroupDirective, FxCountUpDirective, FormsModule,
5369
5884
  ReactiveFormsModule,
5370
5885
  RouterModule,
5371
5886
  HasPermissionDirective,
@@ -5387,10 +5902,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
5387
5902
  TrimOnBlurDirective,
5388
5903
  ...FX_COMPONENTS,
5389
5904
  ...DIALOGS_COMPONENT,
5905
+ ...FX_MOTION_DIRECTIVES,
5390
5906
  ],
5391
5907
  exports: [
5392
5908
  ...FX_COMPONENTS,
5393
5909
  ...DIALOGS_COMPONENT,
5910
+ ...FX_MOTION_DIRECTIVES,
5394
5911
  FormsModule,
5395
5912
  ReactiveFormsModule,
5396
5913
  RouterModule,
@@ -5408,5 +5925,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
5408
5925
  * Generated bundle index. Do not edit.
5409
5926
  */
5410
5927
 
5411
- export { AuthInterceptor, AuthStateService, BarListComponent, BaseComponent, BaseDialogComponent, BaseMasterDetailComponent, BaseTableComponent, BreadcrumbService, ButtonComponent, CheckboxComponent, CircleProgressBar, CodeBlockComponent, ConfirmationDialogComponent, DatetimePicker, DndUploadComponent, DrawerComponent, EmptyStateComponent, FX_ICON_SPRITE_URL, FX_SKELETON_DATA_KEY, FacetsRailComponent, FilterChipsComponent, FilterPillsComponent, FxComponent, FxDialogService, FxLoadingService, FxRouteSkeletonService, FxStorageService, FxToastrService, FxUtils, FxValidators, HasPermissionDirective, HeroIconComponent, HttpLoaderFactory, HttpWrapper, InfoFieldComponent, InputComponent, KanbanBoardComponent, ListRowComponent, LoadingPanel, MasterDetailComponent, MenuComponent, MeterComponent, NotificationService, OG_LIGHT_THEMES, OG_PICKER_THEMES, OG_PICKER_THEME_META, OG_THEMES, OG_THEME_META, PendingBadgeComponent, PermissionGuard, PermissionService, PipelineStepperComponent, ProjectCardComponent, RadioButtonComponent, RadioButtonToggleComponent, RealtimeTableSync, RouteSkeletonComponent, SampleFrameComponent, ScoreBadgeComponent, ScreenshotFrameComponent, SearchBarComponent, SectionCardComponent, SegmentedComponent, SelectComponent, SeverityBadgesComponent, SkeletonDashboardComponent, SkeletonDetailComponent, SkeletonFacetsComponent, SkeletonFrameComponent, SkeletonListComponent, SliderComponent, SparkbarComponent, StageDotsComponent, StatCardsComponent, StatusDotComponent, SwitchComponent, TabComponent, TabGroupComponent, TableCell, TagComponent, ThemePickerComponent, ThemeService, ToastComponent, ToastContainerComponent, TranslationModule, TranslationService, TreeDiagram, TrimOnBlurDirective, UiModule, anyFeatureEnabled, countTone, createApi, featureEnabled, fxListResolver, scoreTone, severityTone, toneBadge, toneBadgeSolid, toneBg, toneBorder, toneColor, toneDot, toneDotChart, toneFg, toneScale, toneText, toneTint };
5928
+ export { AuthInterceptor, AuthStateService, BarListComponent, BaseComponent, BaseDialogComponent, BaseMasterDetailComponent, BaseTableComponent, BreadcrumbComponent, BreadcrumbService, ButtonComponent, CheckboxComponent, CircleProgressBar, CodeBlockComponent, ConfirmationDialogComponent, DatetimePicker, DndUploadComponent, DrawerComponent, EmptyStateComponent, FX_ICON_SPRITE_URL, FX_MOTION, FX_MOTION_DIRECTIVES, FX_SKELETON_DATA_KEY, FacetsRailComponent, FilterChipsComponent, FilterPillsComponent, FxComponent, FxCountUpDirective, FxDialogService, FxLoadStatusService, FxLoadingService, FxRevealDirective, FxRevealGroupDirective, FxRouteSkeletonService, FxStorageService, FxToastrService, FxUtils, FxValidators, HasPermissionDirective, HeroIconComponent, HttpLoaderFactory, HttpWrapper, InfoFieldComponent, InputComponent, KanbanBoardComponent, ListRowComponent, LoadStatusComponent, LoadingPanel, MasterDetailComponent, MenuComponent, MeterComponent, NotificationService, OG_LIGHT_THEMES, OG_PICKER_THEMES, OG_PICKER_THEME_META, OG_THEMES, OG_THEME_META, PendingBadgeComponent, PermissionGuard, PermissionService, PipelineStepperComponent, ProjectCardComponent, RadioButtonComponent, RadioButtonToggleComponent, RealtimeTableSync, RouteSkeletonComponent, SampleFrameComponent, ScoreBadgeComponent, ScreenshotFrameComponent, SearchBarComponent, SectionCardComponent, SegmentedComponent, SelectComponent, SeverityBadgesComponent, SkeletonDashboardComponent, SkeletonDetailComponent, SkeletonFacetsComponent, SkeletonFrameComponent, SkeletonListComponent, SliderComponent, SparkbarComponent, StageDotsComponent, StatCardsComponent, StatusDotComponent, SwitchComponent, TabComponent, TabGroupComponent, TableCell, TagComponent, ThemePickerComponent, ThemeService, ToastComponent, ToastContainerComponent, TranslationModule, TranslationService, TreeDiagram, TrimOnBlurDirective, UiModule, anyFeatureEnabled, countTone, createApi, featureEnabled, fxListResolver, fxMotionAllowed, fxRevealElement, scoreTone, severityTone, toneBadge, toneBadgeSolid, toneBg, toneBorder, toneColor, toneDot, toneDotChart, toneFg, toneScale, toneText, toneTint };
5412
5929
  //# sourceMappingURL=foxeltech-angular-ui.mjs.map