@foxeltech/angular-ui 0.7.75 → 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 (31) hide show
  1. package/components.css +66 -3
  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 +641 -60
  7. package/fesm2022/foxeltech-angular-ui.mjs.map +1 -1
  8. package/package.json +3 -2
  9. package/src/lib/styles/components.css +66 -3
  10. package/src/lib/styles/utilities.css +8 -2
  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/pending-badge/pending-badge.component.html +2 -2
  20. package/src/lib/ui/components/project-card/project-card.component.html +7 -3
  21. package/src/lib/ui/components/route-skeleton/route-skeleton.component.css +1 -1
  22. package/src/lib/ui/components/sample-frame/sample-frame.component.html +6 -7
  23. package/src/lib/ui/components/section-card/section-card.component.html +3 -1
  24. package/src/lib/ui/components/segmented/segmented.component.html +6 -2
  25. package/src/lib/ui/components/severity-badges/severity-badges.component.html +1 -1
  26. package/src/lib/ui/components/stat-cards/stat-cards.component.css +18 -0
  27. package/src/lib/ui/components/stat-cards/stat-cards.component.html +3 -3
  28. package/types/foxeltech-angular-ui-chart.d.ts +18 -1
  29. package/types/foxeltech-angular-ui-flow.d.ts +7 -0
  30. package/types/foxeltech-angular-ui.d.ts +349 -14
  31. package/utilities.css +8 -2
@@ -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 } 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
 
@@ -1657,7 +1766,23 @@ function toneBadgeSolid(tone) {
1657
1766
  * neutral, thay vì sinh ra class rác như thời còn ghép chuỗi `'bg-' + tone`.
1658
1767
  */
1659
1768
  const isTone = (t) => typeof t === 'string' && Object.prototype.hasOwnProperty.call(TONE_TINT, t);
1660
- /** Chỉ MÀU của tông, không kèm hình dạng pill — cho ô icon, chip, ô nền. */
1769
+ /**
1770
+ * Chỉ MÀU của tông, không kèm hình dạng pill — cho ô icon, chip, ô nền.
1771
+ *
1772
+ * ⚠️ Đây là họ SOLID: `tone-tint-critical` đặt `background: --og-text`
1773
+ * (gần đen) VÀ `color: --og-surface` (trắng) trên CÙNG element.
1774
+ *
1775
+ * Hệ quả bắt buộc nhớ:
1776
+ * 1. Chỉ dùng cho **badge một phần tử**. Với hộp nhiều dòng thì con sẽ cần màu
1777
+ * chữ riêng, và nếu con ghi đè bằng `toneFg()` (đỏ sẫm) thì thành đỏ sẫm
1778
+ * trên gần đen — không đọc được. Đã bị đúng vậy ở hộp RISK của
1779
+ * `info-binary-detail` (anh Tú 03/09).
1780
+ * 2. **KHÔNG** đặt `toneFg()` lên phần tử con của một phần tử đã có
1781
+ * `toneTint()` — nó phá màu tương phản mà `toneTint` vừa đặt.
1782
+ *
1783
+ * Hộp/khối nhiều dòng thì dùng `toneScale()`: nền nhạt (color-mix 12%) + chữ
1784
+ * đúng tông, con chỉ cần THỪA HƯỞNG.
1785
+ */
1661
1786
  function toneTint(tone) {
1662
1787
  return isTone(tone) ? TONE_TINT[tone] : 'tone-tint-neutral';
1663
1788
  }
@@ -2454,11 +2579,11 @@ class ButtonComponent {
2454
2579
  this.clicked.emit();
2455
2580
  }
2456
2581
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2457
- 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 });
2458
2583
  }
2459
2584
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ButtonComponent, decorators: [{
2460
2585
  type: Component,
2461
- 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" }]
2462
2587
  }], propDecorators: { label: [{
2463
2588
  type: Input
2464
2589
  }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], buttonVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonVariant", required: false }] }], icon: [{
@@ -4082,6 +4207,77 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4082
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" }]
4083
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 }] }] } });
4084
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
+
4085
4281
  /** KPI tile row: big number + tinted icon box + caption, in a responsive grid. */
4086
4282
  class StatCardsComponent {
4087
4283
  cards = input([], ...(ngDevMode ? [{ debugName: "cards" }] : /* istanbul ignore next */ []));
@@ -4093,10 +4289,14 @@ class StatCardsComponent {
4093
4289
  variant = input('tile', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
4094
4290
  /** Grid columns; defaults to one column per card. */
4095
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
+ */
4096
4297
  get gridColumns() {
4097
- // No explicit cols → responsive auto-fit so tiles stack on narrow viewports.
4098
4298
  if (this.cols === undefined) {
4099
- return 'repeat(auto-fit, minmax(180px, 1fr))';
4299
+ return 'repeat(auto-fit, minmax(min(180px, 100%), 1fr))';
4100
4300
  }
4101
4301
  return `repeat(${this.cols}, minmax(0, 1fr))`;
4102
4302
  }
@@ -4114,7 +4314,7 @@ class StatCardsComponent {
4114
4314
  return card.subTone ? toneText(card.subTone) : 'text-text-secondary';
4115
4315
  }
4116
4316
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StatCardsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4117
- 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 });
4118
4318
  }
4119
4319
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StatCardsComponent, decorators: [{
4120
4320
  type: Component,
@@ -4123,7 +4323,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4123
4323
  NgClass,
4124
4324
  HeroIconComponent,
4125
4325
  SparkbarComponent,
4126
- ], 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"] }]
4127
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: [{
4128
4329
  type: Input
4129
4330
  }] } });
@@ -4213,6 +4414,12 @@ class FilterPillsComponent {
4213
4414
  /** Accepts undefined so screens can bind an optional filter-bag key. */
4214
4415
  value = input('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
4215
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 */ []));
4216
4423
  isActive(option) {
4217
4424
  return (this.value() ?? '') === option.value;
4218
4425
  }
@@ -4223,20 +4430,23 @@ class FilterPillsComponent {
4223
4430
  * trạng thái chọn của filter (anh Tú chốt 03/09).
4224
4431
  */
4225
4432
  pillClass(option) {
4226
- return this.isActive(option)
4227
- ? 'bg-accent text-accent-contrast border-accent'
4228
- : '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';
4229
4439
  }
4230
4440
  countClass(option) {
4231
4441
  return this.isActive(option) ? 'opacity-70' : 'text-text-placeholder';
4232
4442
  }
4233
4443
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FilterPillsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4234
- 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 });
4235
4445
  }
4236
4446
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FilterPillsComponent, decorators: [{
4237
4447
  type: Component,
4238
- 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" }]
4239
- }], 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 }] }] } });
4240
4450
 
4241
4451
  /**
4242
4452
  * V2 stage progress dots — the walkthrough position indicator on the attack
@@ -4357,6 +4567,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4357
4567
  * - titleStyle 'heading' (default): icon + heading, the pre-V2 look
4358
4568
  * - titleStyle 'eyebrow': the V2 design's panel header — small tracked
4359
4569
  * caption on the left, muted [aside] text on the right
4570
+ *
4571
+ * The host is a flex column (not the custom element's inline default) so that
4572
+ * in a CSS grid row every card stretches to the row height and panels line up
4573
+ * (anh Tú 04/09); a footer projected with `mt-auto` sticks to the bottom.
4360
4574
  */
4361
4575
  class SectionCardComponent {
4362
4576
  /** Caller-translated heading. */
@@ -4372,11 +4586,11 @@ class SectionCardComponent {
4372
4586
  return iconTone ? toneText(iconTone) : 'text-text-secondary';
4373
4587
  }
4374
4588
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SectionCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4375
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SectionCardComponent, isStandalone: true, selector: "fx-ui-section-card", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, iconTone: { classPropertyName: "iconTone", publicName: "iconTone", isSignal: true, isRequired: false, transformFunction: null }, titleStyle: { classPropertyName: "titleStyle", publicName: "titleStyle", isSignal: true, isRequired: false, transformFunction: null }, aside: { classPropertyName: "aside", publicName: "aside", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"card-common\">\n @if (titleStyle() === 'eyebrow') {\n <div class=\"flex items-baseline justify-between gap-normal mb-large\">\n <span class=\"txt-eyebrow\">{{ title() }}</span>\n @if (aside()) {\n <span class=\"txt-utility-description text-right\">{{ aside() }}</span>\n }\n </div>\n } @else {\n <div class=\"flex items-center gap-normal mb-large\">\n @if (icon) {\n <fx-ui-hero-icon [ngClass]=\"iconClass\" [icon]=\"icon\" [size]=\"18\"></fx-ui-hero-icon>\n }\n <span class=\"txt-heading-06\">{{ title() }}</span>\n </div>\n }\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4589
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SectionCardComponent, isStandalone: true, selector: "fx-ui-section-card", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, iconTone: { classPropertyName: "iconTone", publicName: "iconTone", isSignal: true, isRequired: false, transformFunction: null }, titleStyle: { classPropertyName: "titleStyle", publicName: "titleStyle", isSignal: true, isRequired: false, transformFunction: null }, aside: { classPropertyName: "aside", publicName: "aside", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "flex flex-col min-w-0" }, ngImport: i0, template: "<!-- flex column filling the host: cards in one grid row share a height and a\n projected footer can sit at the bottom with `mt-auto` (design 01). -->\n<div class=\"card-common bg-surface flex flex-col flex-1 min-h-0\">\n @if (titleStyle() === 'eyebrow') {\n <div class=\"flex items-baseline justify-between gap-normal mb-large\">\n <span class=\"txt-eyebrow\">{{ title() }}</span>\n @if (aside()) {\n <span class=\"txt-utility-description text-right\">{{ aside() }}</span>\n }\n </div>\n } @else {\n <div class=\"flex items-center gap-normal mb-large\">\n @if (icon) {\n <fx-ui-hero-icon [ngClass]=\"iconClass\" [icon]=\"icon\" [size]=\"18\"></fx-ui-hero-icon>\n }\n <span class=\"txt-heading-06\">{{ title() }}</span>\n </div>\n }\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4376
4590
  }
4377
4591
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SectionCardComponent, decorators: [{
4378
4592
  type: Component,
4379
- args: [{ selector: 'fx-ui-section-card', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent, NgClass], template: "<div class=\"card-common\">\n @if (titleStyle() === 'eyebrow') {\n <div class=\"flex items-baseline justify-between gap-normal mb-large\">\n <span class=\"txt-eyebrow\">{{ title() }}</span>\n @if (aside()) {\n <span class=\"txt-utility-description text-right\">{{ aside() }}</span>\n }\n </div>\n } @else {\n <div class=\"flex items-center gap-normal mb-large\">\n @if (icon) {\n <fx-ui-hero-icon [ngClass]=\"iconClass\" [icon]=\"icon\" [size]=\"18\"></fx-ui-hero-icon>\n }\n <span class=\"txt-heading-06\">{{ title() }}</span>\n </div>\n }\n <ng-content></ng-content>\n</div>\n" }]
4593
+ args: [{ selector: 'fx-ui-section-card', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent, NgClass], host: { class: 'flex flex-col min-w-0' }, template: "<!-- flex column filling the host: cards in one grid row share a height and a\n projected footer can sit at the bottom with `mt-auto` (design 01). -->\n<div class=\"card-common bg-surface flex flex-col flex-1 min-h-0\">\n @if (titleStyle() === 'eyebrow') {\n <div class=\"flex items-baseline justify-between gap-normal mb-large\">\n <span class=\"txt-eyebrow\">{{ title() }}</span>\n @if (aside()) {\n <span class=\"txt-utility-description text-right\">{{ aside() }}</span>\n }\n </div>\n } @else {\n <div class=\"flex items-center gap-normal mb-large\">\n @if (icon) {\n <fx-ui-hero-icon [ngClass]=\"iconClass\" [icon]=\"icon\" [size]=\"18\"></fx-ui-hero-icon>\n }\n <span class=\"txt-heading-06\">{{ title() }}</span>\n </div>\n }\n <ng-content></ng-content>\n</div>\n" }]
4380
4594
  }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], icon: [{
4381
4595
  type: Input
4382
4596
  }], iconTone: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconTone", required: false }] }], titleStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleStyle", required: false }] }], aside: [{ type: i0.Input, args: [{ isSignal: true, alias: "aside", required: false }] }] } });
@@ -4453,11 +4667,11 @@ class SeverityBadgesComponent {
4453
4667
  return badge.tone ? toneBadge(badge.tone) : 'bg-bg-hover text-text-primary';
4454
4668
  }
4455
4669
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SeverityBadgesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4456
- 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 });
4457
4671
  }
4458
4672
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SeverityBadgesComponent, decorators: [{
4459
4673
  type: Component,
4460
- 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" }]
4461
4675
  }], propDecorators: { stats: [{
4462
4676
  type: Input
4463
4677
  }], showZero: [{
@@ -4510,12 +4724,33 @@ class MasterDetailComponent {
4510
4724
  /** Caller-translated Prev/Next labels ("X of Y" is numeric). */
4511
4725
  prevLabel = input('Prev', ...(ngDevMode ? [{ debugName: "prevLabel" }] : /* istanbul ignore next */ []));
4512
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 */ []));
4513
4741
  rowTemplate = contentChild.required('row');
4514
4742
  detailTemplate = contentChild.required('detail');
4515
4743
  paginator = viewChild.required(MatPaginator);
4516
4744
  uid = `fx-md-${nextUid++}`;
4517
4745
  ngAfterContentInit() {
4518
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
+ }
4519
4754
  }
4520
4755
  ngAfterViewInit() {
4521
4756
  // The paginator lives in this view, not the host page's, so the host's
@@ -4525,16 +4760,13 @@ class MasterDetailComponent {
4525
4760
  this.table.paginator = this.paginator();
4526
4761
  }
4527
4762
  }
4528
- /** "X of Y" range text for the prevnext pagination style. */
4763
+ /** "Page X of Y" for the prevnext pagination style (design 11/14/17). */
4529
4764
  rangeText() {
4530
4765
  const total = this.table.total ?? 0;
4531
- if (!total)
4532
- return '0 of 0';
4533
- const page = this.paginator().pageIndex;
4534
- const size = this.table.pageSize;
4535
- const from = page * size + 1;
4536
- const to = Math.min((page + 1) * size, total);
4537
- 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}`;
4538
4770
  }
4539
4771
  isSelected(row) {
4540
4772
  return this.table.idOf(row) === this.table.selectedId;
@@ -4574,7 +4806,7 @@ class MasterDetailComponent {
4574
4806
  el?.scrollIntoView({ block: 'nearest' });
4575
4807
  }
4576
4808
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: MasterDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4577
- 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"] }] });
4578
4810
  }
4579
4811
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: MasterDetailComponent, decorators: [{
4580
4812
  type: Component,
@@ -4585,10 +4817,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4585
4817
  EmptyStateComponent,
4586
4818
  MatPaginator,
4587
4819
  SkeletonDetailComponent,
4588
- ], 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"] }]
4589
4822
  }], propDecorators: { table: [{
4590
4823
  type: Input
4591
- }], 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 }] }] } });
4592
4825
 
4593
4826
  /**
4594
4827
  * Row-action dropdown wrapper over mat-menu (behavior-layer rule, Playbook
@@ -4669,6 +4902,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4669
4902
  * <fx-ui-pending-badge [field]="'confirmed-chains weekly series'" />
4670
4903
  *
4671
4904
  * Keep the copy factual: it states that data is missing, never fakes a value.
4905
+ * Styling is neutral on purpose (anh Tú 04/09): a hint, not a warning.
4672
4906
  */
4673
4907
  class PendingBadgeComponent {
4674
4908
  /** Caller-translated chip text. */
@@ -4684,22 +4918,203 @@ class PendingBadgeComponent {
4684
4918
  return this.field() ? `${this.label()} — ${this.field()}` : this.label();
4685
4919
  }
4686
4920
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: PendingBadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4687
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: PendingBadgeComponent, isStandalone: true, selector: "fx-ui-pending-badge", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (variant() === 'dot') {\n <span\n class=\"inline-block w-[7px] h-[7px] rounded-full bg-warning/70 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-dashed border-warning/60 bg-warning/10 text-warning 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", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4921
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: PendingBadgeComponent, isStandalone: true, selector: "fx-ui-pending-badge", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, 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", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4688
4922
  }
4689
4923
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: PendingBadgeComponent, decorators: [{
4690
4924
  type: Component,
4691
- 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-warning/70 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-dashed border-warning/60 bg-warning/10 text-warning 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" }]
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" }]
4692
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 }] }] } });
4693
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
+
4694
5108
  /**
4695
5109
  * Wraps UI that is rendered from PLACEHOLDER data so the layout can be
4696
5110
  * reviewed before its endpoint exists (anh Tú 29/08: "muốn có full giao diện
4697
5111
  * trước để review").
4698
5112
  *
4699
- * The frame makes the status unmistakable dashed outline, a corner label and
4700
- * the pending marker naming the missing field so sample shapes can never be
4701
- * mistaken for measured values. Drop the wrapper (and the sample input) the
4702
- * moment the API lands.
5113
+ * The marker is deliberately quiet (anh 04/09: no yellow frame, just a
5114
+ * subtle "Pending API" chip): a neutral chip above the block, with the label
5115
+ * and missing field in its tooltip. The content itself is non-interactive so
5116
+ * sample shapes are never mistaken for live state. Drop the wrapper (and the
5117
+ * sample input) the moment the API lands.
4703
5118
  *
4704
5119
  * The pending chip is inlined rather than composed from fx-ui-pending-badge:
4705
5120
  * one lib component importing another inside the same barrel tripped NG0919
@@ -4710,21 +5125,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4710
5125
  * </fx-ui-sample-frame>
4711
5126
  */
4712
5127
  class SampleFrameComponent {
4713
- /** Caller-translated corner label. */
5128
+ /** Caller-translated block label — surfaces in the chip tooltip only. */
4714
5129
  label = input('Sample layout', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
4715
5130
  /** Field the backend still owes — shown on the pending badge tooltip. */
4716
5131
  field = input('', ...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
4717
5132
  /** Caller-translated pending-chip text. */
4718
5133
  pendingLabel = input('Pending API', ...(ngDevMode ? [{ debugName: "pendingLabel" }] : /* istanbul ignore next */ []));
4719
5134
  get tooltip() {
4720
- return this.field() ? `${this.pendingLabel()} ${this.field()}` : this.pendingLabel();
5135
+ const head = `${this.label()} · ${this.pendingLabel()}`;
5136
+ return this.field() ? `${head} — ${this.field()}` : head;
4721
5137
  }
4722
5138
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SampleFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4723
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.20", type: SampleFrameComponent, isStandalone: true, selector: "fx-ui-sample-frame", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: false, transformFunction: null }, pendingLabel: { classPropertyName: "pendingLabel", publicName: "pendingLabel", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"relative rounded-lg border border-dashed border-warning/50 bg-warning/[0.03] p-normal\">\n <!-- flex-wrap + nowrap label: in narrow rails the chip drops to the next line\n instead of the label breaking mid-word (\"SAMPLE / LAYOUT\"). -->\n <div class=\"flex flex-wrap items-center gap-small mb-normal\">\n <span class=\"txt-tag-label text-warning uppercase tracking-wide whitespace-nowrap\">{{ label() }}</span>\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-dashed border-warning/60 bg-warning/10 text-warning txt-tag-label whitespace-nowrap\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ pendingLabel() }}\n </span>\n </div>\n <!-- Sample content is non-interactive: nothing here reflects real state. -->\n <div class=\"pointer-events-none select-none opacity-90\">\n <ng-content></ng-content>\n </div>\n</div>\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5139
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.20", type: SampleFrameComponent, isStandalone: true, selector: "fx-ui-sample-frame", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: false, transformFunction: null }, pendingLabel: { classPropertyName: "pendingLabel", publicName: "pendingLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "flex flex-col min-h-0" }, ngImport: i0, template: "<div class=\"relative flex flex-col flex-1 min-h-0\">\n <!-- One quiet chip marks the block as sample data; the tooltip names the\n missing field. flex-wrap so the chip drops a line in narrow rails. -->\n <div class=\"flex flex-wrap items-center gap-small mb-small\">\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\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ pendingLabel() }}\n </span>\n </div>\n <!-- Sample content is non-interactive: nothing here reflects real state. -->\n <div class=\"pointer-events-none select-none opacity-90 flex-1 min-h-0 flex flex-col\">\n <ng-content></ng-content>\n </div>\n</div>\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4724
5140
  }
4725
5141
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SampleFrameComponent, decorators: [{
4726
5142
  type: Component,
4727
- args: [{ selector: 'fx-ui-sample-frame', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "<div class=\"relative rounded-lg border border-dashed border-warning/50 bg-warning/[0.03] p-normal\">\n <!-- flex-wrap + nowrap label: in narrow rails the chip drops to the next line\n instead of the label breaking mid-word (\"SAMPLE / LAYOUT\"). -->\n <div class=\"flex flex-wrap items-center gap-small mb-normal\">\n <span class=\"txt-tag-label text-warning uppercase tracking-wide whitespace-nowrap\">{{ label() }}</span>\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-dashed border-warning/60 bg-warning/10 text-warning txt-tag-label whitespace-nowrap\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ pendingLabel() }}\n </span>\n </div>\n <!-- Sample content is non-interactive: nothing here reflects real state. -->\n <div class=\"pointer-events-none select-none opacity-90\">\n <ng-content></ng-content>\n </div>\n</div>\n" }]
5143
+ args: [{ selector: 'fx-ui-sample-frame', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], host: { class: 'flex flex-col min-h-0' }, template: "<div class=\"relative flex flex-col flex-1 min-h-0\">\n <!-- One quiet chip marks the block as sample data; the tooltip names the\n missing field. flex-wrap so the chip drops a line in narrow rails. -->\n <div class=\"flex flex-wrap items-center gap-small mb-small\">\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\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ pendingLabel() }}\n </span>\n </div>\n <!-- Sample content is non-interactive: nothing here reflects real state. -->\n <div class=\"pointer-events-none select-none opacity-90 flex-1 min-h-0 flex flex-col\">\n <ng-content></ng-content>\n </div>\n</div>\n" }]
4728
5144
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: false }] }], pendingLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pendingLabel", required: false }] }] } });
4729
5145
 
4730
5146
  /**
@@ -4761,7 +5177,7 @@ class ScoreBadgeComponent {
4761
5177
  sizeClass() {
4762
5178
  switch (this.size()) {
4763
5179
  case 'sm':
4764
- return 'text-xs px-normal py-[1px]';
5180
+ return 'text-xs px-normal py-[3px]';
4765
5181
  case 'lg':
4766
5182
  return 'text-2xl px-semi py-small font-semibold';
4767
5183
  default:
@@ -4822,12 +5238,53 @@ class SegmentedComponent {
4822
5238
  * (design refs 04/05).
4823
5239
  */
4824
5240
  variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
5241
+ /**
5242
+ * 'equal' (default): every segment takes the same width (view toggles such
5243
+ * as Catalog | Table). 'content': each segment is as wide as its label —
5244
+ * the demo's detail-pane tabs ("Affected software · Summary · Evidence"),
5245
+ * where equal columns would truncate the long label in a narrow pane.
5246
+ */
5247
+ fit = input('equal', ...(ngDevMode ? [{ debugName: "fit" }] : /* istanbul ignore next */ []));
5248
+ optionEls = viewChildren('opt', ...(ngDevMode ? [{ debugName: "optionEls" }] : /* istanbul ignore next */ []));
5249
+ /** Measured box of the active segment — only used when fit === 'content'. */
5250
+ thumbBox = signal(null, ...(ngDevMode ? [{ debugName: "thumbBox" }] : /* istanbul ignore next */ []));
5251
+ constructor() {
5252
+ // Content-fit segments have no shared width to derive the thumb from, so
5253
+ // measure the active button after each render that could move it.
5254
+ afterRenderEffect(() => {
5255
+ const index = this.activeIndex();
5256
+ this.options();
5257
+ const els = this.optionEls();
5258
+ if (this.fit() !== 'content' || index < 0 || !els[index]) {
5259
+ untracked(() => this.thumbBox.set(null));
5260
+ return;
5261
+ }
5262
+ const el = els[index].nativeElement;
5263
+ const box = { left: el.offsetLeft, width: el.offsetWidth };
5264
+ untracked(() => {
5265
+ const prev = this.thumbBox();
5266
+ if (!prev || prev.left !== box.left || prev.width !== box.width)
5267
+ this.thumbBox.set(box);
5268
+ });
5269
+ });
5270
+ }
4825
5271
  /** Index the sliding thumb sits on; -1 hides it (no/unknown selection). */
4826
5272
  activeIndex = computed(() => this.options().findIndex((o) => o.value === this.value()), ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
4827
5273
  /** Equal-width tracks → the thumb is 1/n of the inner width and slides by
4828
5274
  * multiples of its own width. Pure CSS transform, nothing to measure. */
4829
- thumbWidth = computed(() => `calc((100% - 6px) / ${this.options().length || 1})`, ...(ngDevMode ? [{ debugName: "thumbWidth" }] : /* istanbul ignore next */ []));
4830
- thumbTransform = computed(() => `translateX(${this.activeIndex() * 100}%)`, ...(ngDevMode ? [{ debugName: "thumbTransform" }] : /* istanbul ignore next */ []));
5275
+ thumbWidth = computed(() => {
5276
+ const box = this.thumbBox();
5277
+ if (this.fit() === 'content')
5278
+ return box ? `${box.width}px` : '0px';
5279
+ return `calc((100% - 6px) / ${this.options().length || 1})`;
5280
+ }, ...(ngDevMode ? [{ debugName: "thumbWidth" }] : /* istanbul ignore next */ []));
5281
+ thumbTransform = computed(() => {
5282
+ const box = this.thumbBox();
5283
+ if (this.fit() === 'content')
5284
+ return box ? `translateX(${box.left - 3}px)` : 'none';
5285
+ return `translateX(${this.activeIndex() * 100}%)`;
5286
+ }, ...(ngDevMode ? [{ debugName: "thumbTransform" }] : /* istanbul ignore next */ []));
5287
+ gridClass = computed(() => (this.fit() === 'content' ? 'auto-cols-auto' : 'auto-cols-fr'), ...(ngDevMode ? [{ debugName: "gridClass" }] : /* istanbul ignore next */ []));
4831
5288
  select(option) {
4832
5289
  if (option.disabled || option.value === this.value())
4833
5290
  return;
@@ -4845,12 +5302,12 @@ class SegmentedComponent {
4845
5302
  return this.variant() === 'pill' ? 'text-accent-contrast' : 'text-strong';
4846
5303
  }
4847
5304
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SegmentedComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4848
- 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 } }, outputs: { value: "valueChange" }, ngImport: i0, template: "<div\n class=\"relative inline-grid max-w-full grid-flow-col auto-cols-fr items-stretch p-[3px] bg-surface-sunken border border-border rounded-full\"\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 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 });
4849
5306
  }
4850
5307
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SegmentedComponent, decorators: [{
4851
5308
  type: Component,
4852
- args: [{ selector: 'fx-ui-segmented', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "<div\n class=\"relative inline-grid max-w-full grid-flow-col auto-cols-fr items-stretch p-[3px] bg-surface-sunken border border-border rounded-full\"\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 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"] }]
4853
- }], 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 }] }] } });
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"] }]
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 }] }] } });
4854
5311
 
4855
5312
  /**
4856
5313
  * V2 ListRow — presentational body for master-detail #row templates and other
@@ -4989,11 +5446,11 @@ class ProjectCardComponent {
4989
5446
  return tone ? TREND_BLOCK[tone] : 'bg-bg-hover';
4990
5447
  }
4991
5448
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ProjectCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4992
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ProjectCardComponent, isStandalone: true, selector: "fx-ui-project-card", inputs: { card: { classPropertyName: "card", publicName: "card", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { open: "open" }, ngImport: i0, template: "<button\n type=\"button\"\n class=\"card-common bg-surface w-full flex flex-col gap-normal hover:border-accent hover:shadow-md transition-all\"\n (click)=\"open.emit(card())\"\n>\n <!-- Header: avatar + name/sub + score chip (design 12) -->\n <div class=\"flex items-start justify-between gap-normal w-full\">\n <div class=\"flex items-start gap-normal min-w-0\">\n @if (card().avatar) {\n <span\n class=\"w-9 h-9 rounded-control bg-accent-soft text-accent flex items-center justify-center shrink-0 txt-utility-caption font-bold uppercase\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card().avatar }}</span\n >\n }\n <div class=\"min-w-0\">\n <div class=\"txt-heading-06 truncate text-left\">{{ card().name }}</div>\n @if (card().sub; as sub) {\n <div class=\"flex items-center gap-xs txt-utility-description mt-xs\">\n @if (sub.icon) {\n <fx-ui-hero-icon [icon]=\"sub.icon\" [size]=\"13\" class=\"flex shrink-0\" />\n }\n <span class=\"truncate\">{{ sub.label }}</span>\n </div>\n } @else if (card().description) {\n <div class=\"txt-card-content truncate mt-xs text-left\">{{ card().description }}</div>\n }\n </div>\n </div>\n @if (card().score !== undefined) {\n <div class=\"text-right shrink-0\">\n <span\n class=\"txt-tag-label px-normal py-[2px] rounded-pill whitespace-nowrap\"\n [ngClass]=\"scoreChipClass()\"\n >{{ card().score }}</span\n >\n @if (card().scoreLabel) {\n <div class=\"txt-utility-description mt-xs\">{{ card().scoreLabel }}</div>\n }\n </div>\n }\n </div>\n\n @if (card().chips?.length) {\n <div class=\"flex flex-wrap gap-xs w-full\">\n @for (chip of card().chips; track chip.label) {\n <span class=\"txt-tag-label px-normal py-[1px] rounded-pill\" [ngClass]=\"chipClass(chip)\">\n {{ chip.label }}\n </span>\n }\n </div>\n }\n\n <!-- Design-12 risk trend: toned blocks + caption line -->\n @if (card().trend; as trend) {\n <div class=\"w-full\">\n <div class=\"flex gap-xs w-full\">\n @for (tone of trend.tones; track $index) {\n <span class=\"flex-1 h-4 rounded-[2px]\" [ngClass]=\"blockClass(tone)\"></span>\n }\n </div>\n @if (trend.caption || trend.aside) {\n <div class=\"flex items-center justify-between mt-xs txt-utility-description font-mono\">\n <span>{{ trend.caption }}</span>\n <span>{{ trend.aside }}</span>\n </div>\n }\n </div>\n }\n\n @if (card().spark?.length) {\n <fx-ui-sparkbar class=\"w-full\" [values]=\"card().spark!\" [height]=\"24\" />\n }\n\n <!-- Metric columns: figure + caption (design 12) -->\n @if (card().metrics?.length) {\n <div\n class=\"grid gap-normal w-full\"\n [style.grid-template-columns]=\"'repeat(' + card().metrics!.length + ', minmax(0, 1fr))'\"\n >\n @for (m of card().metrics; track m.label) {\n <div class=\"min-w-0 text-left\">\n <div class=\"txt-heading-06\" style=\"font-family: var(--og-font-figure)\">{{ m.value }}</div>\n <div class=\"txt-utility-description truncate\">{{ m.label }}</div>\n </div>\n }\n </div>\n }\n\n @if (card().meta || card().openLabel) {\n <div\n class=\"flex items-center justify-between w-full pt-normal border-t border-border-default\"\n >\n <span class=\"txt-utility-description text-left\">{{ card().meta }}</span>\n @if (card().openLabel) {\n <span class=\"txt-utility-caption font-bold text-accent whitespace-nowrap\"\n >{{ card().openLabel }} \u203A</span\n >\n }\n </div>\n }\n</button>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: SparkbarComponent, selector: "fx-ui-sparkbar", inputs: ["values", "tone", "height"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5449
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ProjectCardComponent, isStandalone: true, selector: "fx-ui-project-card", inputs: { card: { classPropertyName: "card", publicName: "card", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { open: "open" }, host: { classAttribute: "flex flex-col min-w-0" }, ngImport: i0, template: "<button\n type=\"button\"\n class=\"card-common bg-surface w-full flex-1 flex flex-col gap-normal hover:border-accent hover:shadow-md transition-all\"\n (click)=\"open.emit(card())\"\n>\n <!-- Header: avatar + name/sub + score chip (design 12) -->\n <div class=\"flex items-start justify-between gap-normal w-full\">\n <div class=\"flex items-start gap-normal min-w-0\">\n @if (card().avatar) {\n <span\n class=\"w-9 h-9 rounded-control bg-accent-soft text-accent flex items-center justify-center shrink-0 txt-utility-caption font-bold uppercase\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card().avatar }}</span\n >\n }\n <!-- min-h 46px = name (24) + mt-xs (2) + sub (20): reserves the two-line\n header even when a card has no sub line, so trend/metric rows line\n up across the grid row (measured 04/09). -->\n <div class=\"min-w-0 min-h-[46px]\">\n <div class=\"txt-heading-06 truncate text-left\">{{ card().name }}</div>\n @if (card().sub; as sub) {\n <div class=\"flex items-center gap-xs txt-utility-description mt-xs\">\n @if (sub.icon) {\n <fx-ui-hero-icon [icon]=\"sub.icon\" [size]=\"13\" class=\"flex shrink-0\" />\n }\n <span class=\"truncate\">{{ sub.label }}</span>\n </div>\n } @else if (card().description) {\n <div class=\"txt-card-content truncate mt-xs text-left\">{{ card().description }}</div>\n }\n </div>\n </div>\n @if (card().score !== undefined) {\n <div class=\"text-right shrink-0\">\n <span\n class=\"txt-tag-label px-normal py-[2px] rounded-pill whitespace-nowrap\"\n [ngClass]=\"scoreChipClass()\"\n >{{ card().score }}</span\n >\n @if (card().scoreLabel) {\n <div class=\"txt-utility-description mt-xs\">{{ card().scoreLabel }}</div>\n }\n </div>\n }\n </div>\n\n @if (card().chips?.length) {\n <div class=\"flex flex-wrap gap-xs w-full\">\n @for (chip of card().chips; track chip.label) {\n <span class=\"txt-tag-label px-normal py-[1px] rounded-pill\" [ngClass]=\"chipClass(chip)\">\n {{ chip.label }}\n </span>\n }\n </div>\n }\n\n <!-- Design-12 risk trend: toned blocks + caption line -->\n @if (card().trend; as trend) {\n <div class=\"w-full\">\n <div class=\"flex gap-xs w-full\">\n @for (tone of trend.tones; track $index) {\n <span class=\"flex-1 h-4 rounded-[2px]\" [ngClass]=\"blockClass(tone)\"></span>\n }\n </div>\n @if (trend.caption || trend.aside) {\n <div class=\"flex items-center justify-between mt-xs txt-utility-description font-mono\">\n <span>{{ trend.caption }}</span>\n <span>{{ trend.aside }}</span>\n </div>\n }\n </div>\n }\n\n @if (card().spark?.length) {\n <fx-ui-sparkbar class=\"w-full\" [values]=\"card().spark!\" [height]=\"24\" />\n }\n\n <!-- Metric columns: figure + caption (design 12) -->\n @if (card().metrics?.length) {\n <div\n class=\"grid gap-normal w-full\"\n [style.grid-template-columns]=\"'repeat(' + card().metrics!.length + ', minmax(0, 1fr))'\"\n >\n @for (m of card().metrics; track m.label) {\n <div class=\"min-w-0 text-left\">\n <div class=\"txt-heading-06\" style=\"font-family: var(--og-font-figure)\">{{ m.value }}</div>\n <div class=\"txt-utility-description truncate\">{{ m.label }}</div>\n </div>\n }\n </div>\n }\n\n @if (card().meta || card().openLabel) {\n <!-- mt-auto: footers line up across a grid row whatever each card holds -->\n <div\n class=\"flex items-center justify-between w-full pt-normal mt-auto border-t border-border-default\"\n >\n <span class=\"txt-utility-description text-left\">{{ card().meta }}</span>\n @if (card().openLabel) {\n <span class=\"txt-utility-caption font-bold text-accent whitespace-nowrap\"\n >{{ card().openLabel }} \u203A</span\n >\n }\n </div>\n }\n</button>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: SparkbarComponent, selector: "fx-ui-sparkbar", inputs: ["values", "tone", "height"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4993
5450
  }
4994
5451
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ProjectCardComponent, decorators: [{
4995
5452
  type: Component,
4996
- args: [{ selector: 'fx-ui-project-card', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, SparkbarComponent, HeroIconComponent], template: "<button\n type=\"button\"\n class=\"card-common bg-surface w-full flex flex-col gap-normal hover:border-accent hover:shadow-md transition-all\"\n (click)=\"open.emit(card())\"\n>\n <!-- Header: avatar + name/sub + score chip (design 12) -->\n <div class=\"flex items-start justify-between gap-normal w-full\">\n <div class=\"flex items-start gap-normal min-w-0\">\n @if (card().avatar) {\n <span\n class=\"w-9 h-9 rounded-control bg-accent-soft text-accent flex items-center justify-center shrink-0 txt-utility-caption font-bold uppercase\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card().avatar }}</span\n >\n }\n <div class=\"min-w-0\">\n <div class=\"txt-heading-06 truncate text-left\">{{ card().name }}</div>\n @if (card().sub; as sub) {\n <div class=\"flex items-center gap-xs txt-utility-description mt-xs\">\n @if (sub.icon) {\n <fx-ui-hero-icon [icon]=\"sub.icon\" [size]=\"13\" class=\"flex shrink-0\" />\n }\n <span class=\"truncate\">{{ sub.label }}</span>\n </div>\n } @else if (card().description) {\n <div class=\"txt-card-content truncate mt-xs text-left\">{{ card().description }}</div>\n }\n </div>\n </div>\n @if (card().score !== undefined) {\n <div class=\"text-right shrink-0\">\n <span\n class=\"txt-tag-label px-normal py-[2px] rounded-pill whitespace-nowrap\"\n [ngClass]=\"scoreChipClass()\"\n >{{ card().score }}</span\n >\n @if (card().scoreLabel) {\n <div class=\"txt-utility-description mt-xs\">{{ card().scoreLabel }}</div>\n }\n </div>\n }\n </div>\n\n @if (card().chips?.length) {\n <div class=\"flex flex-wrap gap-xs w-full\">\n @for (chip of card().chips; track chip.label) {\n <span class=\"txt-tag-label px-normal py-[1px] rounded-pill\" [ngClass]=\"chipClass(chip)\">\n {{ chip.label }}\n </span>\n }\n </div>\n }\n\n <!-- Design-12 risk trend: toned blocks + caption line -->\n @if (card().trend; as trend) {\n <div class=\"w-full\">\n <div class=\"flex gap-xs w-full\">\n @for (tone of trend.tones; track $index) {\n <span class=\"flex-1 h-4 rounded-[2px]\" [ngClass]=\"blockClass(tone)\"></span>\n }\n </div>\n @if (trend.caption || trend.aside) {\n <div class=\"flex items-center justify-between mt-xs txt-utility-description font-mono\">\n <span>{{ trend.caption }}</span>\n <span>{{ trend.aside }}</span>\n </div>\n }\n </div>\n }\n\n @if (card().spark?.length) {\n <fx-ui-sparkbar class=\"w-full\" [values]=\"card().spark!\" [height]=\"24\" />\n }\n\n <!-- Metric columns: figure + caption (design 12) -->\n @if (card().metrics?.length) {\n <div\n class=\"grid gap-normal w-full\"\n [style.grid-template-columns]=\"'repeat(' + card().metrics!.length + ', minmax(0, 1fr))'\"\n >\n @for (m of card().metrics; track m.label) {\n <div class=\"min-w-0 text-left\">\n <div class=\"txt-heading-06\" style=\"font-family: var(--og-font-figure)\">{{ m.value }}</div>\n <div class=\"txt-utility-description truncate\">{{ m.label }}</div>\n </div>\n }\n </div>\n }\n\n @if (card().meta || card().openLabel) {\n <div\n class=\"flex items-center justify-between w-full pt-normal border-t border-border-default\"\n >\n <span class=\"txt-utility-description text-left\">{{ card().meta }}</span>\n @if (card().openLabel) {\n <span class=\"txt-utility-caption font-bold text-accent whitespace-nowrap\"\n >{{ card().openLabel }} \u203A</span\n >\n }\n </div>\n }\n</button>\n" }]
5453
+ args: [{ selector: 'fx-ui-project-card', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, SparkbarComponent, HeroIconComponent], host: { class: 'flex flex-col min-w-0' }, template: "<button\n type=\"button\"\n class=\"card-common bg-surface w-full flex-1 flex flex-col gap-normal hover:border-accent hover:shadow-md transition-all\"\n (click)=\"open.emit(card())\"\n>\n <!-- Header: avatar + name/sub + score chip (design 12) -->\n <div class=\"flex items-start justify-between gap-normal w-full\">\n <div class=\"flex items-start gap-normal min-w-0\">\n @if (card().avatar) {\n <span\n class=\"w-9 h-9 rounded-control bg-accent-soft text-accent flex items-center justify-center shrink-0 txt-utility-caption font-bold uppercase\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card().avatar }}</span\n >\n }\n <!-- min-h 46px = name (24) + mt-xs (2) + sub (20): reserves the two-line\n header even when a card has no sub line, so trend/metric rows line\n up across the grid row (measured 04/09). -->\n <div class=\"min-w-0 min-h-[46px]\">\n <div class=\"txt-heading-06 truncate text-left\">{{ card().name }}</div>\n @if (card().sub; as sub) {\n <div class=\"flex items-center gap-xs txt-utility-description mt-xs\">\n @if (sub.icon) {\n <fx-ui-hero-icon [icon]=\"sub.icon\" [size]=\"13\" class=\"flex shrink-0\" />\n }\n <span class=\"truncate\">{{ sub.label }}</span>\n </div>\n } @else if (card().description) {\n <div class=\"txt-card-content truncate mt-xs text-left\">{{ card().description }}</div>\n }\n </div>\n </div>\n @if (card().score !== undefined) {\n <div class=\"text-right shrink-0\">\n <span\n class=\"txt-tag-label px-normal py-[2px] rounded-pill whitespace-nowrap\"\n [ngClass]=\"scoreChipClass()\"\n >{{ card().score }}</span\n >\n @if (card().scoreLabel) {\n <div class=\"txt-utility-description mt-xs\">{{ card().scoreLabel }}</div>\n }\n </div>\n }\n </div>\n\n @if (card().chips?.length) {\n <div class=\"flex flex-wrap gap-xs w-full\">\n @for (chip of card().chips; track chip.label) {\n <span class=\"txt-tag-label px-normal py-[1px] rounded-pill\" [ngClass]=\"chipClass(chip)\">\n {{ chip.label }}\n </span>\n }\n </div>\n }\n\n <!-- Design-12 risk trend: toned blocks + caption line -->\n @if (card().trend; as trend) {\n <div class=\"w-full\">\n <div class=\"flex gap-xs w-full\">\n @for (tone of trend.tones; track $index) {\n <span class=\"flex-1 h-4 rounded-[2px]\" [ngClass]=\"blockClass(tone)\"></span>\n }\n </div>\n @if (trend.caption || trend.aside) {\n <div class=\"flex items-center justify-between mt-xs txt-utility-description font-mono\">\n <span>{{ trend.caption }}</span>\n <span>{{ trend.aside }}</span>\n </div>\n }\n </div>\n }\n\n @if (card().spark?.length) {\n <fx-ui-sparkbar class=\"w-full\" [values]=\"card().spark!\" [height]=\"24\" />\n }\n\n <!-- Metric columns: figure + caption (design 12) -->\n @if (card().metrics?.length) {\n <div\n class=\"grid gap-normal w-full\"\n [style.grid-template-columns]=\"'repeat(' + card().metrics!.length + ', minmax(0, 1fr))'\"\n >\n @for (m of card().metrics; track m.label) {\n <div class=\"min-w-0 text-left\">\n <div class=\"txt-heading-06\" style=\"font-family: var(--og-font-figure)\">{{ m.value }}</div>\n <div class=\"txt-utility-description truncate\">{{ m.label }}</div>\n </div>\n }\n </div>\n }\n\n @if (card().meta || card().openLabel) {\n <!-- mt-auto: footers line up across a grid row whatever each card holds -->\n <div\n class=\"flex items-center justify-between w-full pt-normal mt-auto border-t border-border-default\"\n >\n <span class=\"txt-utility-description text-left\">{{ card().meta }}</span>\n @if (card().openLabel) {\n <span class=\"txt-utility-caption font-bold text-accent whitespace-nowrap\"\n >{{ card().openLabel }} \u203A</span\n >\n }\n </div>\n }\n</button>\n" }]
4997
5454
  }], propDecorators: { card: [{ type: i0.Input, args: [{ isSignal: true, alias: "card", required: true }] }], open: [{ type: i0.Output, args: ["open"] }] } });
4998
5455
 
4999
5456
  /**
@@ -5181,11 +5638,11 @@ class RouteSkeletonComponent {
5181
5638
  }
5182
5639
  bars = range$1(7);
5183
5640
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: RouteSkeletonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5184
- 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 });
5185
5642
  }
5186
5643
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: RouteSkeletonComponent, decorators: [{
5187
5644
  type: Component,
5188
- 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"] }]
5189
5646
  }], propDecorators: { hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }] } });
5190
5647
  function range$1(n) {
5191
5648
  return Array.from({ length: Math.max(0, Number(n) || 0) }, (_, i) => i);
@@ -5264,6 +5721,8 @@ const FX_COMPONENTS = [
5264
5721
  MenuComponent,
5265
5722
  ThemePickerComponent,
5266
5723
  PendingBadgeComponent,
5724
+ BreadcrumbComponent,
5725
+ LoadStatusComponent,
5267
5726
  SampleFrameComponent,
5268
5727
  ScoreBadgeComponent,
5269
5728
  StatusDotComponent,
@@ -5284,6 +5743,126 @@ const DIALOGS_COMPONENT = [
5284
5743
  ConfirmationDialogComponent
5285
5744
  ];
5286
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
+
5287
5866
  /**
5288
5867
  * Compatibility barrel over the (now standalone) core components.
5289
5868
  *
@@ -5301,7 +5880,7 @@ class UiModule {
5301
5880
  ReactiveFormsModule,
5302
5881
  RouterModule,
5303
5882
  HasPermissionDirective,
5304
- 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,
5305
5884
  ReactiveFormsModule,
5306
5885
  RouterModule,
5307
5886
  HasPermissionDirective,
@@ -5323,10 +5902,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
5323
5902
  TrimOnBlurDirective,
5324
5903
  ...FX_COMPONENTS,
5325
5904
  ...DIALOGS_COMPONENT,
5905
+ ...FX_MOTION_DIRECTIVES,
5326
5906
  ],
5327
5907
  exports: [
5328
5908
  ...FX_COMPONENTS,
5329
5909
  ...DIALOGS_COMPONENT,
5910
+ ...FX_MOTION_DIRECTIVES,
5330
5911
  FormsModule,
5331
5912
  ReactiveFormsModule,
5332
5913
  RouterModule,
@@ -5344,5 +5925,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
5344
5925
  * Generated bundle index. Do not edit.
5345
5926
  */
5346
5927
 
5347
- 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 };
5348
5929
  //# sourceMappingURL=foxeltech-angular-ui.mjs.map