@foxeltech/angular-ui 0.7.44 → 0.7.83

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 (53) hide show
  1. package/components.css +345 -19
  2. package/familjen-grotesk/familjen-grotesk-400-latin-ext.woff2 +0 -0
  3. package/familjen-grotesk/familjen-grotesk-400-latin.woff2 +0 -0
  4. package/familjen-grotesk/familjen-grotesk-400-vietnamese.woff2 +0 -0
  5. package/familjen-grotesk/familjen-grotesk-500-latin-ext.woff2 +0 -0
  6. package/familjen-grotesk/familjen-grotesk-500-latin.woff2 +0 -0
  7. package/familjen-grotesk/familjen-grotesk-500-vietnamese.woff2 +0 -0
  8. package/familjen-grotesk/familjen-grotesk-600-latin-ext.woff2 +0 -0
  9. package/familjen-grotesk/familjen-grotesk-600-latin.woff2 +0 -0
  10. package/familjen-grotesk/familjen-grotesk-600-vietnamese.woff2 +0 -0
  11. package/familjen-grotesk/familjen-grotesk-700-latin-ext.woff2 +0 -0
  12. package/familjen-grotesk/familjen-grotesk-700-latin.woff2 +0 -0
  13. package/familjen-grotesk/familjen-grotesk-700-vietnamese.woff2 +0 -0
  14. package/fesm2022/foxeltech-angular-ui-chart.mjs +197 -24
  15. package/fesm2022/foxeltech-angular-ui-chart.mjs.map +1 -1
  16. package/fesm2022/foxeltech-angular-ui.mjs +585 -125
  17. package/fesm2022/foxeltech-angular-ui.mjs.map +1 -1
  18. package/fonts.css +104 -3
  19. package/package.json +1 -1
  20. package/src/lib/styles/components.css +345 -19
  21. package/src/lib/styles/fonts.css +104 -3
  22. package/src/lib/styles/tailwind.css +12 -0
  23. package/src/lib/styles/theme.css +160 -114
  24. package/src/lib/styles/tokens.css +25 -0
  25. package/src/lib/styles/utilities.css +18 -1
  26. package/src/lib/ui/components/button/button.component.html +2 -0
  27. package/src/lib/ui/components/drawer/drawer.component.scss +3 -1
  28. package/src/lib/ui/components/facets-rail/facets-rail.component.html +1 -6
  29. package/src/lib/ui/components/master-detail/master-detail.component.css +65 -13
  30. package/src/lib/ui/components/master-detail/master-detail.component.html +26 -8
  31. package/src/lib/ui/components/pending-badge/pending-badge.component.html +2 -2
  32. package/src/lib/ui/components/project-card/project-card.component.html +7 -3
  33. package/src/lib/ui/components/route-skeleton/route-skeleton.component.css +38 -0
  34. package/src/lib/ui/components/route-skeleton/route-skeleton.component.html +184 -0
  35. package/src/lib/ui/components/sample-frame/sample-frame.component.html +6 -7
  36. package/src/lib/ui/components/section-card/section-card.component.html +4 -6
  37. package/src/lib/ui/components/segmented/segmented.component.html +3 -1
  38. package/src/lib/ui/components/skeleton-dashboard/skeleton-dashboard.component.html +24 -0
  39. package/src/lib/ui/components/skeleton-detail/skeleton-detail.component.html +8 -8
  40. package/src/lib/ui/components/skeleton-facets/skeleton-facets.component.html +15 -0
  41. package/src/lib/ui/components/skeleton-frame/skeleton-frame.component.html +59 -0
  42. package/src/lib/ui/components/skeleton-list/skeleton-list.component.html +6 -6
  43. package/src/lib/ui/components/switch/switch.component.html +1 -1
  44. package/src/lib/ui/components/toast/toast.component.html +6 -6
  45. package/tailwind.css +12 -0
  46. package/theme.css +160 -114
  47. package/tokens.css +25 -0
  48. package/types/foxeltech-angular-ui-chart.d.ts +76 -1
  49. package/types/foxeltech-angular-ui.d.ts +318 -39
  50. package/utilities.css +18 -1
  51. package/src/lib/ui/components/skeleton-table-loading/skeleton-table-loading.component.html +0 -21
  52. package/src/lib/ui/components/skeleton-table-loading/skeleton-table-loading.component.scss +0 -0
  53. package/src/lib/ui/components/tab-component/tab.component.html +0 -1
@@ -1,5 +1,5 @@
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, NgModule, HostListener, output, ChangeDetectorRef, TemplateRef, contentChildren, Output, ViewChildren, computed, ElementRef, contentChild, model } from '@angular/core';
2
+ import { inject, DestroyRef, Injector, Directive, InjectionToken, input, isDevMode, ChangeDetectionStrategy, Component, Input, createComponent, Injectable, viewChild, ViewChild, Inject, EventEmitter, signal, computed, NgModule, HostListener, output, ChangeDetectorRef, contentChild, TemplateRef, contentChildren, Output, ViewChildren, ElementRef, model, viewChildren, afterRenderEffect, untracked } from '@angular/core';
3
3
  import { MatPaginator } from '@angular/material/paginator';
4
4
  import { MatTableDataSource, MatTable } from '@angular/material/table';
5
5
  import { NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
@@ -9,13 +9,13 @@ import * as i2 from '@angular/common/http';
9
9
  import { HttpRequest, HttpClient } from '@angular/common/http';
10
10
  import * as i1 from '@angular/material/dialog';
11
11
  import { MAT_DIALOG_DATA, MatDialogTitle, MatDialogContent, MatDialogActions, MatDialog } from '@angular/material/dialog';
12
+ import * as i2$1 from '@angular/router';
13
+ import { Router, RoutesRecognized, NavigationEnd, NavigationCancel, NavigationError, RouterLink, RouterModule } from '@angular/router';
12
14
  import * as i1$1 from '@ngx-translate/core';
13
15
  import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
14
16
  import { TranslateHttpLoader } from '@ngx-translate/http-loader';
15
17
  import * as i1$3 from '@angular/forms';
16
18
  import { NgControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
17
- import * as i2$1 from '@angular/router';
18
- import { NavigationEnd, RouterLink, RouterModule } from '@angular/router';
19
19
  import { catchError } from 'rxjs/operators';
20
20
  import * as signalR from '@microsoft/signalr';
21
21
  import { CdkScrollable } from '@angular/cdk/scrolling';
@@ -108,11 +108,11 @@ class ToastComponent {
108
108
  message = input('', ...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
109
109
  type = input(...(ngDevMode ? [undefined, { debugName: "type" }] : /* istanbul ignore next */ []));
110
110
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ToastComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
111
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ToastComponent, isStandalone: true, selector: "fx-ui-toast", inputs: { toastId: { classPropertyName: "toastId", publicName: "toastId", isSignal: true, isRequired: true, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: false, isRequired: false, transformFunction: null }, message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div\n animate.enter=\"horizontal-fade-in-animation\"\n animate.leave=\"horizontal-fade-out-animation\"\n [attr.role]=\"type() === 'error' ? 'alert' : 'status'\"\n class=\"flex border-l-[3px] items-center bg-bg-primary gap-semi min-w-[250px] max-w-[400px] px-large py-semi rounded shadow-lg transition-all duration-300 ease-in-out transform opacity-100 translate-y-0\"\n [ngClass]=\"{\n 'border-success': type() === 'success',\n 'border-critical': type() === 'error',\n 'border-warning': type() === 'warning',\n 'border-information': type() === 'info'\n }\"\n >\n <div class=\"\" aria-hidden=\"true\">\n @switch (type()) { @case('success') {\n <fx-ui-hero-icon\n icon=\"check-circle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"text-success\"\n ></fx-ui-hero-icon>\n } @case('error') {\n <fx-ui-hero-icon\n icon=\"exclamation-circle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"text-critical\"\n ></fx-ui-hero-icon>\n } @case('warning') {\n <fx-ui-hero-icon\n icon=\"exclamation-triangle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"text-warning\"\n ></fx-ui-hero-icon>\n } @default {\n <fx-ui-hero-icon\n icon=\"information-circle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"text-information\"\n ></fx-ui-hero-icon>\n } }\n </div>\n\n <div class=\"flex flex-col\">\n @if (title) {\n <strong class=\"txt-field-label\">{{ title }}</strong>\n }\n <p class=\"txt-default\">{{ message() }}</p>\n </div>\n </div>\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 });
111
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ToastComponent, isStandalone: true, selector: "fx-ui-toast", inputs: { toastId: { classPropertyName: "toastId", publicName: "toastId", isSignal: true, isRequired: true, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: false, isRequired: false, transformFunction: null }, message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div\n animate.enter=\"horizontal-fade-in-animation\"\n animate.leave=\"horizontal-fade-out-animation\"\n [attr.role]=\"type() === 'error' ? 'alert' : 'status'\"\n class=\"flex border-l-[3px] items-center bg-bg-primary gap-semi min-w-[250px] max-w-[400px] px-large py-semi rounded shadow-lg transition-all duration-300 ease-in-out transform opacity-100 translate-y-0\"\n [ngClass]=\"{\n 'tone-border-success': type() === 'success',\n 'tone-border-danger': type() === 'error',\n 'border-warning': type() === 'warning',\n 'border-information': type() === 'info'\n }\"\n >\n <div class=\"\" aria-hidden=\"true\">\n @switch (type()) { @case('success') {\n <fx-ui-hero-icon\n icon=\"check-circle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"tone-fg-success\"\n ></fx-ui-hero-icon>\n } @case('error') {\n <fx-ui-hero-icon\n icon=\"exclamation-circle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"tone-fg-danger\"\n ></fx-ui-hero-icon>\n } @case('warning') {\n <fx-ui-hero-icon\n icon=\"exclamation-triangle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"tone-fg-warning\"\n ></fx-ui-hero-icon>\n } @default {\n <fx-ui-hero-icon\n icon=\"information-circle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"tone-fg-information\"\n ></fx-ui-hero-icon>\n } }\n </div>\n\n <div class=\"flex flex-col\">\n @if (title) {\n <strong class=\"txt-field-label\">{{ title }}</strong>\n }\n <p class=\"txt-default\">{{ message() }}</p>\n </div>\n </div>\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 });
112
112
  }
113
113
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ToastComponent, decorators: [{
114
114
  type: Component,
115
- args: [{ selector: 'fx-ui-toast', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, HeroIconComponent], template: "<div\n animate.enter=\"horizontal-fade-in-animation\"\n animate.leave=\"horizontal-fade-out-animation\"\n [attr.role]=\"type() === 'error' ? 'alert' : 'status'\"\n class=\"flex border-l-[3px] items-center bg-bg-primary gap-semi min-w-[250px] max-w-[400px] px-large py-semi rounded shadow-lg transition-all duration-300 ease-in-out transform opacity-100 translate-y-0\"\n [ngClass]=\"{\n 'border-success': type() === 'success',\n 'border-critical': type() === 'error',\n 'border-warning': type() === 'warning',\n 'border-information': type() === 'info'\n }\"\n >\n <div class=\"\" aria-hidden=\"true\">\n @switch (type()) { @case('success') {\n <fx-ui-hero-icon\n icon=\"check-circle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"text-success\"\n ></fx-ui-hero-icon>\n } @case('error') {\n <fx-ui-hero-icon\n icon=\"exclamation-circle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"text-critical\"\n ></fx-ui-hero-icon>\n } @case('warning') {\n <fx-ui-hero-icon\n icon=\"exclamation-triangle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"text-warning\"\n ></fx-ui-hero-icon>\n } @default {\n <fx-ui-hero-icon\n icon=\"information-circle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"text-information\"\n ></fx-ui-hero-icon>\n } }\n </div>\n\n <div class=\"flex flex-col\">\n @if (title) {\n <strong class=\"txt-field-label\">{{ title }}</strong>\n }\n <p class=\"txt-default\">{{ message() }}</p>\n </div>\n </div>\n" }]
115
+ args: [{ selector: 'fx-ui-toast', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, HeroIconComponent], template: "<div\n animate.enter=\"horizontal-fade-in-animation\"\n animate.leave=\"horizontal-fade-out-animation\"\n [attr.role]=\"type() === 'error' ? 'alert' : 'status'\"\n class=\"flex border-l-[3px] items-center bg-bg-primary gap-semi min-w-[250px] max-w-[400px] px-large py-semi rounded shadow-lg transition-all duration-300 ease-in-out transform opacity-100 translate-y-0\"\n [ngClass]=\"{\n 'tone-border-success': type() === 'success',\n 'tone-border-danger': type() === 'error',\n 'border-warning': type() === 'warning',\n 'border-information': type() === 'info'\n }\"\n >\n <div class=\"\" aria-hidden=\"true\">\n @switch (type()) { @case('success') {\n <fx-ui-hero-icon\n icon=\"check-circle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"tone-fg-success\"\n ></fx-ui-hero-icon>\n } @case('error') {\n <fx-ui-hero-icon\n icon=\"exclamation-circle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"tone-fg-danger\"\n ></fx-ui-hero-icon>\n } @case('warning') {\n <fx-ui-hero-icon\n icon=\"exclamation-triangle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"tone-fg-warning\"\n ></fx-ui-hero-icon>\n } @default {\n <fx-ui-hero-icon\n icon=\"information-circle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"tone-fg-information\"\n ></fx-ui-hero-icon>\n } }\n </div>\n\n <div class=\"flex flex-col\">\n @if (title) {\n <strong class=\"txt-field-label\">{{ title }}</strong>\n }\n <p class=\"txt-default\">{{ message() }}</p>\n </div>\n </div>\n" }]
116
116
  }], propDecorators: { toastId: [{ type: i0.Input, args: [{ isSignal: true, alias: "toastId", required: true }] }], title: [{
117
117
  type: Input
118
118
  }], message: [{ type: i0.Input, args: [{ isSignal: true, alias: "message", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }] } });
@@ -1183,15 +1183,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
1183
1183
  args: [MAT_DIALOG_DATA]
1184
1184
  }] }, { type: FxToastrService }, { type: HttpWrapper }, { type: i1.MatDialog }], propDecorators: { ngForm: [{ type: i0.ViewChild, args: ['f', { isSignal: true }] }] } });
1185
1185
 
1186
+ /**
1187
+ * Overlay chặn màn cho HÀNH ĐỘNG của người dùng (bấm nút, đổi trạng thái,
1188
+ * xuất báo cáo…). `app.html` của host bind `fx-ui-loading-panel` vào
1189
+ * `listenerAction`.
1190
+ *
1191
+ * ⚠️ **KHÔNG dùng cho điều hướng.** Channel `listener` + `start()`/`stop()` đã
1192
+ * bị XOÁ ở 0.7.70 (Pha 3). Điều hướng dùng skeleton: `FxRouteSkeletonService`
1193
+ * đọc `data.skeleton` của route đích và shell dựng `fx-ui-route-skeleton`.
1194
+ *
1195
+ * Vì sao xoá thay vì để lại: trước đó cả 46 call site đổ chung MỘT channel, lẫn
1196
+ * hai ngữ nghĩa (điều hướng vs hành động). Sau khi skeleton thay phần điều
1197
+ * hướng, `listener` không còn ai nghe — để lại là một cái bẫy: gọi `start()`
1198
+ * tưởng có overlay mà thực tế không có gì hiện.
1199
+ */
1186
1200
  class FxLoadingService {
1187
- listener = new EventEmitter();
1188
1201
  listenerAction = new EventEmitter();
1189
- start() {
1190
- this.listener.emit(true);
1191
- }
1192
- stop() {
1193
- this.listener.emit(false);
1194
- }
1195
1202
  startAction() {
1196
1203
  this.listenerAction.emit(true);
1197
1204
  }
@@ -1208,16 +1215,97 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
1208
1215
  }]
1209
1216
  }] });
1210
1217
 
1211
- class BaseResolver {
1212
- loadingService;
1213
- injector;
1214
- constructor(loadingService, injector) {
1215
- this.loadingService = loadingService;
1216
- this.injector = injector;
1218
+ /**
1219
+ * Khai trên route: `data: { skeleton: 'table' }` (tên preset) hoặc
1220
+ * `data: { skeleton: ['heading', 'toolbar', 'table'] }` (mảng block, mô tả
1221
+ * ĐÚNG layout của màn — bảng ra bảng, chart ra chart).
1222
+ */
1223
+ const FX_SKELETON_DATA_KEY = 'skeleton';
1224
+ /**
1225
+ * Đọc `data.skeleton` của route đích rồi bật/tắt skeleton cho vùng nội dung.
1226
+ *
1227
+ * ⚠️ Vì sao phải nghe router event chứ không để từng màn tự lo: Angular
1228
+ * **không tạo component cho tới khi resolver xong**, nên trong lúc resolver
1229
+ * của một màn chạy thì component đó chưa tồn tại — không có gì để vẽ
1230
+ * skeleton. Chỉ shell (tồn tại trước) làm được.
1231
+ *
1232
+ * Mốc dùng là `RoutesRecognized` — event ĐẦU TIÊN mang `RouterStateSnapshot`,
1233
+ * và nó xảy ra TRƯỚC `ResolveStart`. `NavigationStart` thì chưa biết route
1234
+ * đích nên không đọc được hint.
1235
+ */
1236
+ class FxRouteSkeletonService {
1237
+ router = inject(Router);
1238
+ _hint = signal(null, ...(ngDevMode ? [{ debugName: "_hint" }] : /* istanbul ignore next */ []));
1239
+ /** Hint đang hiển thị, `null` = không vẽ skeleton. */
1240
+ hint = this._hint.asReadonly();
1241
+ active = computed(() => this._hint() !== null, ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
1242
+ /**
1243
+ * Điều hướng nhanh hơn mốc này thì KHÔNG vẽ skeleton — tránh nháy một
1244
+ * nhịp khi resolver trả về gần như tức thì (dữ liệu đã cache).
1245
+ */
1246
+ graceMs = 150;
1247
+ timer;
1248
+ started = false;
1249
+ constructor() {
1250
+ inject(DestroyRef).onDestroy(() => this.clearTimer());
1217
1251
  }
1218
- baseResolverInit() {
1219
- this.loadingService.start();
1252
+ /** Gọi một lần từ shell (host và mỗi remote standalone). */
1253
+ start() {
1254
+ if (this.started)
1255
+ return;
1256
+ this.started = true;
1257
+ this.router.events.subscribe((event) => {
1258
+ if (event instanceof RoutesRecognized) {
1259
+ const hint = readHint(event.state.root);
1260
+ this.clearTimer();
1261
+ if (!hint) {
1262
+ this._hint.set(null);
1263
+ return;
1264
+ }
1265
+ this.timer = setTimeout(() => {
1266
+ this.timer = undefined;
1267
+ this._hint.set(hint);
1268
+ }, this.graceMs);
1269
+ return;
1270
+ }
1271
+ if (event instanceof NavigationEnd ||
1272
+ event instanceof NavigationCancel ||
1273
+ event instanceof NavigationError) {
1274
+ this.clearTimer();
1275
+ this._hint.set(null);
1276
+ }
1277
+ });
1278
+ }
1279
+ clearTimer() {
1280
+ if (this.timer !== undefined) {
1281
+ clearTimeout(this.timer);
1282
+ this.timer = undefined;
1283
+ }
1220
1284
  }
1285
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FxRouteSkeletonService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1286
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FxRouteSkeletonService, providedIn: 'root' });
1287
+ }
1288
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FxRouteSkeletonService, decorators: [{
1289
+ type: Injectable,
1290
+ args: [{ providedIn: 'root' }]
1291
+ }], ctorParameters: () => [] });
1292
+ /**
1293
+ * Lấy hint ở nhánh SÂU NHẤT có khai — để route cha đặt mặc định và route con
1294
+ * ghi đè được.
1295
+ */
1296
+ function readHint(root) {
1297
+ let node = root;
1298
+ let found = null;
1299
+ while (node) {
1300
+ const value = node.data?.[FX_SKELETON_DATA_KEY];
1301
+ // Chấp nhận cả tên preset và mảng block; mảng rỗng coi như không khai.
1302
+ if (typeof value === 'string' && value)
1303
+ found = value;
1304
+ else if (Array.isArray(value) && value.length)
1305
+ found = value;
1306
+ node = node.firstChild;
1307
+ }
1308
+ return found;
1221
1309
  }
1222
1310
 
1223
1311
  class FxStorageService {
@@ -1242,11 +1330,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
1242
1330
  }] });
1243
1331
 
1244
1332
  /**
1245
- * Functional replacement for the copy-pasted list-resolver class:
1246
- * starts the loading overlay, runs one API request, and on failure stops the
1247
- * overlay and shows a parsed error toast the exact convention every
1248
- * BaseResolver subclass hand-rolls today. BaseResolver stays for existing
1249
- * classes; new routes should use this factory.
1333
+ * Chạy MỘT request cho list resolver, lỗi thì hiện toast đã parse.
1334
+ *
1335
+ * ⚠️ KHÔNG còn bật overlay chặn màn: điều hướng dùng skeleton
1336
+ * (`FxRouteSkeletonService` + `data.skeleton` trên route). `BaseResolver` đã bị
1337
+ * xoá 0.7.70 mọi resolver giờ là `@Injectable()` thuần hoặc dùng factory
1338
+ * này.
1250
1339
  *
1251
1340
  * Usage:
1252
1341
  * resolve: { histories: fxListResolver(ScanManagementApi, (route, state, ctx) =>
@@ -1255,11 +1344,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
1255
1344
  function fxListResolver(apiType, request) {
1256
1345
  return async (route, state) => {
1257
1346
  const api = inject(apiType);
1258
- const loading = inject(FxLoadingService);
1259
1347
  const toastr = inject(FxToastrService);
1260
1348
  const storage = inject(FxStorageService);
1261
1349
  try {
1262
- loading.start();
1263
1350
  const ctx = { pageSize: storage.resolve(state.url) || 10 };
1264
1351
  const req = request(route, state, ctx);
1265
1352
  if ((req.method ?? 'get') === 'post') {
@@ -1268,7 +1355,6 @@ function fxListResolver(apiType, request) {
1268
1355
  return await api.get(req.url, req.params);
1269
1356
  }
1270
1357
  catch (err) {
1271
- loading.stop();
1272
1358
  toastr.error(FxUtils.parseError(err), 'Error');
1273
1359
  return null;
1274
1360
  }
@@ -1441,26 +1527,54 @@ class RealtimeTableSync {
1441
1527
 
1442
1528
  const NEUTRAL_TEXT = 'text-text-secondary';
1443
1529
  const NEUTRAL_BG = 'bg-bg-hover';
1530
+ /**
1531
+ * ⚠️ Trỏ vào `.tone-fg-*` của hệ MỚI, KHÔNG còn `text-critical` (bộ LEGACY
1532
+ * vivid). Bỏ sót chỗ này chính là lý do badge/chữ trong list ở mọi tab vẫn
1533
+ * ra màu cũ sau khi đã đổi `TONE_BADGE` (anh Tú báo 03/09).
1534
+ */
1444
1535
  const TONE_TEXT = {
1445
- critical: 'text-critical',
1446
- danger: 'text-danger',
1447
- warning: 'text-warning',
1448
- success: 'text-success',
1449
- information: 'text-information',
1536
+ critical: 'tone-fg-critical',
1537
+ danger: 'tone-fg-danger',
1538
+ warning: 'tone-fg-warning',
1539
+ success: 'tone-fg-success',
1540
+ information: 'tone-fg-information',
1541
+ discovery: 'tone-fg-discovery',
1450
1542
  };
1543
+ /** Nền ĐẶC — dùng cho trạng thái đang chọn. Xem `.tone-solid-*`. */
1451
1544
  const TONE_BG = {
1452
- critical: 'bg-critical',
1453
- danger: 'bg-danger',
1454
- warning: 'bg-warning',
1455
- success: 'bg-success',
1456
- information: 'bg-information',
1545
+ critical: 'tone-solid-critical',
1546
+ danger: 'tone-solid-danger',
1547
+ warning: 'tone-solid-warning',
1548
+ success: 'tone-solid-success',
1549
+ information: 'tone-solid-information',
1550
+ discovery: 'tone-solid-discovery',
1457
1551
  };
1458
- const TONE_BADGE = {
1459
- critical: 'bg-critical/10 text-critical',
1460
- danger: 'bg-danger/10 text-danger',
1461
- warning: 'bg-warning/10 text-warning',
1462
- success: 'bg-success/10 text-success',
1463
- information: 'bg-information/10 text-information',
1552
+ /**
1553
+ * ⚠️ ĐỪNG dùng `toneBadge()` cho badge CÓ CHẤM bên trong: Critical ra nền
1554
+ * đen đặc nên chấm (đỏ sẫm) nằm trên đó gần như vô hình. Demo chỉ dùng biến
1555
+ * thể `solid` cho badge severity ĐỨNG MỘT MÌNH; badge kèm `StatusDot` luôn
1556
+ * dùng nền MỀM. Chỗ đó hãy dùng `toneScale()`.
1557
+ *
1558
+ * Tông severity — theo ĐÚNG mô hình đo trên demo 03/09: token semantic
1559
+ * (`--og-negative`…) chứ không phải bộ vivid `--og-tone-*`, và Critical được
1560
+ * nhấn bằng nền ĐEN ĐẶC thay vì màu rực. Định nghĩa ở `.tone-tint-*` trong
1561
+ * components.css — xem comment ở đó để biết lý do không dùng utility Tailwind.
1562
+ *
1563
+ * `tone-tint-*` chỉ mang MÀU. Hình dạng pill nằm ở `.tone-badge` riêng, nên
1564
+ * ô icon vuông / chip filter dùng được màu mà không bị bo tròn 999px.
1565
+ */
1566
+ /**
1567
+ * Cố tình KHÔNG khoá vào `Record<Tone, …>`: `FxUtils.getTagClass()` còn trả
1568
+ * `discovery` (và `default`), mà `discovery` có đúng một biến thể tương ứng
1569
+ * trong demo là `.og-badge--accent`. `default` không có key → rơi về neutral.
1570
+ */
1571
+ const TONE_TINT = {
1572
+ critical: 'tone-tint-critical',
1573
+ danger: 'tone-tint-danger',
1574
+ warning: 'tone-tint-warning',
1575
+ success: 'tone-tint-success',
1576
+ information: 'tone-tint-information',
1577
+ discovery: 'tone-tint-discovery',
1464
1578
  };
1465
1579
  function hasVal(v) {
1466
1580
  if (v == null)
@@ -1515,8 +1629,87 @@ function toneBg(tone) {
1515
1629
  return tone ? TONE_BG[tone] : NEUTRAL_BG;
1516
1630
  }
1517
1631
  /** Tone → soft pill/badge classes (tinted bg + matching text), neutral fallback. */
1632
+ /**
1633
+ * Badge severity MẶC ĐỊNH — nền MỀM theo thang đo, Critical ra **đỏ sẫm**.
1634
+ *
1635
+ * ⚠️ Trước đây hàm này trả nền ĐẶC (`tone-tint-*`, Critical = đen `--og-text`)
1636
+ * vì demo có biến thể `.og-badge--solid`. Nhưng demo chỉ dùng biến thể đó ở
1637
+ * ĐÚNG MỘT chỗ — badge severity trong danh sách findings — còn app mình gọi
1638
+ * `toneBadge()` ở khắp nơi (`project-card`, `score-badge`, `list-row`,
1639
+ * `severity-badges`, `tab-group`, badge trong hàng…). Kết quả: pill đen tràn
1640
+ * ra mọi màn, anh Tú báo 3 lần (03/09). Nên MẶC ĐỊNH đảo về nền mềm; muốn
1641
+ * pill đen thì gọi `toneBadgeSolid()` một cách tường minh.
1642
+ */
1518
1643
  function toneBadge(tone) {
1519
- return tone ? TONE_BADGE[tone] : 'bg-bg-hover text-text-secondary';
1644
+ return `tone-badge ${toneScale(tone)}`;
1645
+ }
1646
+ /**
1647
+ * Badge severity nền ĐẶC (Critical = `--og-text`, chữ `--og-surface`) — đúng
1648
+ * `.og-badge--solid` của demo. CHỈ dùng cho badge severity đứng một mình ở
1649
+ * nơi cần nhấn mạnh nhất; đừng dùng cho badge có chấm bên trong (chấm sẽ chìm).
1650
+ */
1651
+ function toneBadgeSolid(tone) {
1652
+ return `tone-badge ${toneTint(tone)}`;
1653
+ }
1654
+ /**
1655
+ * Nhận `string` rộng chứ không chỉ `Tone`: rất nhiều call site truyền kết quả
1656
+ * của `FxUtils.getTagClass()` / tone tự tính, vốn là `string`. Tone lạ →
1657
+ * neutral, thay vì sinh ra class rác như thời còn ghép chuỗi `'bg-' + tone`.
1658
+ */
1659
+ const isTone = (t) => typeof t === 'string' && Object.prototype.hasOwnProperty.call(TONE_TINT, t);
1660
+ /**
1661
+ * Chỉ MÀU của tông, không kèm hình dạng pill — cho ô icon, chip, ô nền.
1662
+ *
1663
+ * ⚠️ Đây là họ SOLID: `tone-tint-critical` đặt `background: --og-text`
1664
+ * (gần đen) VÀ `color: --og-surface` (trắng) trên CÙNG element.
1665
+ *
1666
+ * Hệ quả bắt buộc nhớ:
1667
+ * 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
1668
+ * chữ riêng, và nếu con ghi đè bằng `toneFg()` (đỏ sẫm) thì thành đỏ sẫm
1669
+ * trên gần đen — không đọc được. Đã bị đúng vậy ở hộp RISK của
1670
+ * `info-binary-detail` (anh Tú 03/09).
1671
+ * 2. **KHÔNG** đặt `toneFg()` lên phần tử con của một phần tử đã có
1672
+ * `toneTint()` — nó phá màu tương phản mà `toneTint` vừa đặt.
1673
+ *
1674
+ * Hộp/khối nhiều dòng thì dùng `toneScale()`: nền nhạt (color-mix 12%) + chữ
1675
+ * đúng tông, con chỉ cần THỪA HƯỞNG.
1676
+ */
1677
+ function toneTint(tone) {
1678
+ return isTone(tone) ? TONE_TINT[tone] : 'tone-tint-neutral';
1679
+ }
1680
+ /**
1681
+ * CHỮ theo tông — dùng thành viên dễ đọc nhất của mỗi họ severity, KHÔNG phải
1682
+ * ramp chart (ramp có `warning #f59e0b` chỉ 2.15:1 trên nền trắng). Xem
1683
+ * `.tone-fg-*` trong components.css.
1684
+ */
1685
+ function toneFg(tone) {
1686
+ return isTone(tone) ? `tone-fg-${tone}` : 'tone-fg-neutral';
1687
+ }
1688
+ /**
1689
+ * Tông THANG ĐO — cho filter chip severity. Critical ra ĐỎ SẪM (nền mềm pha
1690
+ * từ ramp `--og-sev-*`) thay vì pill đen đặc của `toneTint()`: chip filter là
1691
+ * một thang đo, không phải badge severity của một finding. Xem `.tone-scale-*`.
1692
+ */
1693
+ function toneScale(tone) {
1694
+ return isTone(tone) ? `tone-scale-${tone}` : 'tone-scale-neutral';
1695
+ }
1696
+ /** Viền theo tông — cho panel trạng thái (dropzone upload…). */
1697
+ function toneBorder(tone) {
1698
+ return isTone(tone) ? `tone-border-${tone}` : 'tone-border-neutral';
1699
+ }
1700
+ /**
1701
+ * CHẤM trạng thái trong hàng (`● Confirmed`) — demo dùng màu SEMANTIC
1702
+ * (`.og-statusdot--*`), không phải ramp chart.
1703
+ */
1704
+ function toneDot(tone) {
1705
+ return isTone(tone) ? `tone-dot-${tone}` : 'tone-dot-neutral';
1706
+ }
1707
+ /**
1708
+ * CHẤM LEGEND của chart — phải TRÙNG màu cung donut nên dùng ramp
1709
+ * `--og-sev-*`. Đừng dùng cho chấm trạng thái trong hàng.
1710
+ */
1711
+ function toneDotChart(tone) {
1712
+ return isTone(tone) ? `tone-dot-chart-${tone}` : 'tone-dot-chart-neutral';
1520
1713
  }
1521
1714
  /**
1522
1715
  * Tone → resolved CSS colour string for ECharts / canvas where a Tailwind
@@ -1524,7 +1717,11 @@ function toneBadge(tone) {
1524
1717
  * legacy comma syntax — see FxUtils.convertColorFromVariable.
1525
1718
  */
1526
1719
  function toneColor(tone, alpha = 1) {
1527
- const name = tone ? `--${tone}` : '--text-secondary';
1720
+ // Chart dùng ramp RIÊNG `--og-sev-*` (màu literal đo trên demo), không dùng
1721
+ // `--og-tone-*`. `information` không có trong ramp của demo (demo chỉ có 4
1722
+ // bậc Critical/High/Medium/Low) nên vẫn rơi về tone legacy.
1723
+ const SEV = new Set(['critical', 'danger', 'warning', 'success']);
1724
+ const name = tone ? (SEV.has(tone) ? `--og-sev-${tone}` : `--${tone}`) : '--text-secondary';
1528
1725
  return FxUtils.convertColorFromVariable(name, alpha, { legacyCommaSyntax: true });
1529
1726
  }
1530
1727
 
@@ -1937,8 +2134,8 @@ const OG_THEMES = [
1937
2134
  ];
1938
2135
  const OG_LIGHT_THEMES = ['porcelain', 'ivory', 'terminal'];
1939
2136
  const OG_THEME_META = [
1940
- { name: 'porcelain', label: 'Porcelain', accent: '#7c3aed', pageBg: '#f2f2f1', dark: false },
1941
- { name: 'charcoal', label: 'Charcoal', accent: '#ff6b4a', pageBg: '#0e0e10', dark: true },
2137
+ { name: 'porcelain', label: 'Porcelain', accent: '#4a7dbd', pageBg: '#f2f2f1', dark: false },
2138
+ { name: 'charcoal', label: 'Charcoal', accent: '#8fc866', pageBg: '#1a1811', dark: true },
1942
2139
  { name: 'ivory', label: 'Ivory', accent: '#d97757', pageBg: '#e8e6e1', dark: false },
1943
2140
  { name: 'studio', label: 'Studio', accent: '#5a8fd6', pageBg: '#0e0e10', dark: true },
1944
2141
  { name: 'blueprint', label: 'Blueprint', accent: '#e07a3a', pageBg: '#0e0e10', dark: true },
@@ -2273,11 +2470,11 @@ class ButtonComponent {
2273
2470
  this.clicked.emit();
2274
2471
  }
2275
2472
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2276
- 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-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 }\"\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 });
2473
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ButtonComponent, isStandalone: true, selector: "fx-ui-button", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, buttonVariant: { classPropertyName: "buttonVariant", publicName: "buttonVariant", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked" }, ngImport: i0, template: "<button\n [attr.type]=\"type()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-busy]=\"loading() || null\"\n [class]=\"class()\"\n [ngClass]=\"{\n 'btn-default': buttonVariant() === 'default',\n 'btn-primary': buttonVariant() === 'primary',\n 'btn-success': buttonVariant() === 'success',\n 'btn-cta': buttonVariant() === 'cta',\n 'btn-alternative': buttonVariant() === 'alternative',\n }\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"onClick()\"\n>\n @if (loading()) {\n <div class=\"flex w-full items-center justify-center\" aria-hidden=\"true\">\n <svg\n [attr.fill]=\"\n buttonVariant() !== 'alternative' ? 'rgb(var(--text-primary))' : 'rgb(var(--primary))'\n \"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <circle cx=\"4\" cy=\"12\" r=\"0\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_lo66\"\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_z0Or\"\n begin=\"spinner_lo66.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"4\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_JsnR\"\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_Aguh\"\n begin=\"spinner_JsnR.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"12\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_hSjk\"\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_UHR2\"\n begin=\"spinner_hSjk.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"20\" cy=\"12\" r=\"3\">\n <animate\n id=\"spinner_4v5M\"\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_OLMs\"\n begin=\"spinner_4v5M.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n </circle>\n </svg>\n </div>\n } @else {\n <ng-container>\n <div class=\"flex items-center justify-center gap-small\">\n @if (icon) {\n <fx-ui-hero-icon\n [icon]=\"icon\"\n [size]=\"16\"\n class=\"flex\"\n [ngClass]=\"{\n 'text-text-inverse': buttonVariant() === 'default',\n 'text-text-primary': buttonVariant() === 'alternative',\n 'text-accent-contrast': buttonVariant() === 'primary',\n 'text-surface': buttonVariant() === 'success',\n 'text-white': buttonVariant() === 'cta',\n }\"\n ></fx-ui-hero-icon>\n }\n @if (label) {\n <!-- text-box: optical centering \u2014 Archivo's leading floats ink up otherwise -->\n <div class=\"[text-box:trim-both_cap_alphabetic]\">{{ label }}</div>\n }\n </div>\n </ng-container>\n }\n</button>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2277
2474
  }
2278
2475
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ButtonComponent, decorators: [{
2279
2476
  type: Component,
2280
- 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-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 }\"\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" }]
2477
+ args: [{ selector: 'fx-ui-button', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, HeroIconComponent], template: "<button\n [attr.type]=\"type()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-busy]=\"loading() || null\"\n [class]=\"class()\"\n [ngClass]=\"{\n 'btn-default': buttonVariant() === 'default',\n 'btn-primary': buttonVariant() === 'primary',\n 'btn-success': buttonVariant() === 'success',\n 'btn-cta': buttonVariant() === 'cta',\n 'btn-alternative': buttonVariant() === 'alternative',\n }\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"onClick()\"\n>\n @if (loading()) {\n <div class=\"flex w-full items-center justify-center\" aria-hidden=\"true\">\n <svg\n [attr.fill]=\"\n buttonVariant() !== 'alternative' ? 'rgb(var(--text-primary))' : 'rgb(var(--primary))'\n \"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <circle cx=\"4\" cy=\"12\" r=\"0\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_lo66\"\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_z0Or\"\n begin=\"spinner_lo66.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"4\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_JsnR\"\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_Aguh\"\n begin=\"spinner_JsnR.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"12\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_hSjk\"\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_UHR2\"\n begin=\"spinner_hSjk.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"20\" cy=\"12\" r=\"3\">\n <animate\n id=\"spinner_4v5M\"\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_OLMs\"\n begin=\"spinner_4v5M.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n </circle>\n </svg>\n </div>\n } @else {\n <ng-container>\n <div class=\"flex items-center justify-center gap-small\">\n @if (icon) {\n <fx-ui-hero-icon\n [icon]=\"icon\"\n [size]=\"16\"\n class=\"flex\"\n [ngClass]=\"{\n 'text-text-inverse': buttonVariant() === 'default',\n 'text-text-primary': buttonVariant() === 'alternative',\n 'text-accent-contrast': buttonVariant() === 'primary',\n 'text-surface': buttonVariant() === 'success',\n 'text-white': buttonVariant() === 'cta',\n }\"\n ></fx-ui-hero-icon>\n }\n @if (label) {\n <!-- text-box: optical centering \u2014 Archivo's leading floats ink up otherwise -->\n <div class=\"[text-box:trim-both_cap_alphabetic]\">{{ label }}</div>\n }\n </div>\n </ng-container>\n }\n</button>\n" }]
2281
2478
  }], propDecorators: { label: [{
2282
2479
  type: Input
2283
2480
  }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], buttonVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonVariant", required: false }] }], icon: [{
@@ -3089,17 +3286,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3089
3286
  type: Input
3090
3287
  }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], enableSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableSearch", required: false }] }], searchFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchFn", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
3091
3288
 
3092
- class SkeletonTableLoadingComponent {
3093
- columns = input(5, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
3094
- rows = input(5, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
3095
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonTableLoadingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3096
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SkeletonTableLoadingComponent, isStandalone: true, selector: "fx-ui-skeleton-table-loading", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"w-full animate-pulse space-y-normal\">\n <div\n class=\"grid gap-small\"\n [ngStyle]=\"{ 'grid-template-columns': 'repeat(' + columns() + ', minmax(0, 1fr))' }\"\n >\n @for(col of [].constructor(columns()); track col) {\n <div class=\"h-6 bg-bg-hover rounded\"></div>\n }\n </div>\n\n @for(row of [].constructor(rows()); track row) {\n <div\n class=\"grid gap-small\"\n [ngStyle]=\"{ 'grid-template-columns': 'repeat(' + columns() + ', minmax(0, 1fr))' }\"\n >\n @for(col of [].constructor(columns()); track col) {\n <div class=\"h-7 bg-bg-hover rounded\"></div>\n }\n </div>\n }\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3097
- }
3098
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonTableLoadingComponent, decorators: [{
3099
- type: Component,
3100
- args: [{ selector: 'fx-ui-skeleton-table-loading', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgStyle], template: "<div class=\"w-full animate-pulse space-y-normal\">\n <div\n class=\"grid gap-small\"\n [ngStyle]=\"{ 'grid-template-columns': 'repeat(' + columns() + ', minmax(0, 1fr))' }\"\n >\n @for(col of [].constructor(columns()); track col) {\n <div class=\"h-6 bg-bg-hover rounded\"></div>\n }\n </div>\n\n @for(row of [].constructor(rows()); track row) {\n <div\n class=\"grid gap-small\"\n [ngStyle]=\"{ 'grid-template-columns': 'repeat(' + columns() + ', minmax(0, 1fr))' }\"\n >\n @for(col of [].constructor(columns()); track col) {\n <div class=\"h-7 bg-bg-hover rounded\"></div>\n }\n </div>\n }\n</div>\n" }]
3101
- }], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }] } });
3102
-
3103
3289
  class SliderComponent extends FxComponent {
3104
3290
  ref;
3105
3291
  trackRef = viewChild.required('track');
@@ -3263,13 +3449,39 @@ class SwitchComponent extends FxComponent {
3263
3449
  this.onTouched();
3264
3450
  }
3265
3451
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SwitchComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3266
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SwitchComponent, isStandalone: true, selector: "fx-ui-switch", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSwitch: "onSwitch" }, usesInheritance: true, ngImport: i0, template: "<button\n type=\"button\"\n role=\"switch\"\n [attr.aria-checked]=\"!!value\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [disabled]=\"disabled\"\n class=\"relative flex items-center w-10 h-5 rounded-full transition-colors duration-300 cursor-pointer disabled:cursor-not-allowed focus-visible:ring-2 focus-visible:ring-primary/50\"\n [ngClass]=\"{\n 'bg-success': value && !disabled,\n 'bg-button-default': !value && !disabled,\n 'bg-bg-hover': disabled\n }\"\n (click)=\"toggle()\"\n>\n @if(value) {\n <i style=\"font-size: 20px;\" class=\"material-symbols-outlined absolute left-0.5 top-0 text-text-inverse\" aria-hidden=\"true\">check</i>\n\n }\n @if(!value) {\n <i style=\"font-size: 20px;\" class=\"material-symbols-outlined absolute right-0.5 top-0 text-text-inverse\" aria-hidden=\"true\">close</i>\n }\n <div\n class=\"absolute top-0.5 left-0.5 w-4 h-4 bg-text-inverse rounded-full shadow-md transform transition-transform duration-300\"\n [ngClass]=\"{ 'translate-x-5': value }\"\n ></div>\n</button>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3452
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SwitchComponent, isStandalone: true, selector: "fx-ui-switch", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSwitch: "onSwitch" }, usesInheritance: true, ngImport: i0, template: "<button\n type=\"button\"\n role=\"switch\"\n [attr.aria-checked]=\"!!value\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [disabled]=\"disabled\"\n class=\"relative flex items-center w-10 h-5 rounded-full transition-colors duration-300 cursor-pointer disabled:cursor-not-allowed focus-visible:ring-2 focus-visible:ring-primary/50\"\n [ngClass]=\"{\n 'tone-solid-success': value && !disabled,\n 'bg-button-default': !value && !disabled,\n 'bg-bg-hover': disabled\n }\"\n (click)=\"toggle()\"\n>\n @if(value) {\n <i style=\"font-size: 20px;\" class=\"material-symbols-outlined absolute left-0.5 top-0 text-text-inverse\" aria-hidden=\"true\">check</i>\n\n }\n @if(!value) {\n <i style=\"font-size: 20px;\" class=\"material-symbols-outlined absolute right-0.5 top-0 text-text-inverse\" aria-hidden=\"true\">close</i>\n }\n <div\n class=\"absolute top-0.5 left-0.5 w-4 h-4 bg-text-inverse rounded-full shadow-md transform transition-transform duration-300\"\n [ngClass]=\"{ 'translate-x-5': value }\"\n ></div>\n</button>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3267
3453
  }
3268
3454
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SwitchComponent, decorators: [{
3269
3455
  type: Component,
3270
- args: [{ selector: 'fx-ui-switch', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<button\n type=\"button\"\n role=\"switch\"\n [attr.aria-checked]=\"!!value\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [disabled]=\"disabled\"\n class=\"relative flex items-center w-10 h-5 rounded-full transition-colors duration-300 cursor-pointer disabled:cursor-not-allowed focus-visible:ring-2 focus-visible:ring-primary/50\"\n [ngClass]=\"{\n 'bg-success': value && !disabled,\n 'bg-button-default': !value && !disabled,\n 'bg-bg-hover': disabled\n }\"\n (click)=\"toggle()\"\n>\n @if(value) {\n <i style=\"font-size: 20px;\" class=\"material-symbols-outlined absolute left-0.5 top-0 text-text-inverse\" aria-hidden=\"true\">check</i>\n\n }\n @if(!value) {\n <i style=\"font-size: 20px;\" class=\"material-symbols-outlined absolute right-0.5 top-0 text-text-inverse\" aria-hidden=\"true\">close</i>\n }\n <div\n class=\"absolute top-0.5 left-0.5 w-4 h-4 bg-text-inverse rounded-full shadow-md transform transition-transform duration-300\"\n [ngClass]=\"{ 'translate-x-5': value }\"\n ></div>\n</button>\n" }]
3456
+ args: [{ selector: 'fx-ui-switch', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<button\n type=\"button\"\n role=\"switch\"\n [attr.aria-checked]=\"!!value\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [disabled]=\"disabled\"\n class=\"relative flex items-center w-10 h-5 rounded-full transition-colors duration-300 cursor-pointer disabled:cursor-not-allowed focus-visible:ring-2 focus-visible:ring-primary/50\"\n [ngClass]=\"{\n 'tone-solid-success': value && !disabled,\n 'bg-button-default': !value && !disabled,\n 'bg-bg-hover': disabled\n }\"\n (click)=\"toggle()\"\n>\n @if(value) {\n <i style=\"font-size: 20px;\" class=\"material-symbols-outlined absolute left-0.5 top-0 text-text-inverse\" aria-hidden=\"true\">check</i>\n\n }\n @if(!value) {\n <i style=\"font-size: 20px;\" class=\"material-symbols-outlined absolute right-0.5 top-0 text-text-inverse\" aria-hidden=\"true\">close</i>\n }\n <div\n class=\"absolute top-0.5 left-0.5 w-4 h-4 bg-text-inverse rounded-full shadow-md transform transition-transform duration-300\"\n [ngClass]=\"{ 'translate-x-5': value }\"\n ></div>\n</button>\n" }]
3271
3457
  }], propDecorators: { onSwitch: [{ type: i0.Output, args: ["onSwitch"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
3272
3458
 
3459
+ /**
3460
+ * Một tab của `fx-ui-tab-group`.
3461
+ *
3462
+ * ⚠️ Nội dung tab PHẢI bọc trong `<ng-template>`:
3463
+ *
3464
+ * ```html
3465
+ * <fx-ui-tab [label]="'Components'">
3466
+ * <ng-template>
3467
+ * <app-info-components … />
3468
+ * </ng-template>
3469
+ * </fx-ui-tab>
3470
+ * ```
3471
+ *
3472
+ * Vì sao bắt buộc: trước đây tab dùng `<ng-content>` bọc trong `<ng-template>`
3473
+ * của CHÍNH nó. Cách đó **không hoãn được gì** — `<ng-content>` chỉ CHIẾU node,
3474
+ * còn node do view khai báo (component cha) tạo **eager**. Kết quả: mọi tab đều
3475
+ * khởi tạo và gọi API ngay khi vào màn, dù người dùng chưa mở (đo được trên
3476
+ * scan-info: cả 8 cụm cùng fetch). Nhận `TemplateRef` từ cha thì nội dung chỉ
3477
+ * được dựng khi tab-group stamp template đó.
3478
+ *
3479
+ * Hệ quả cần biết: đổi tab sẽ **huỷ** nội dung tab cũ và dựng lại khi quay lại
3480
+ * — đúng bản chất lazy, giống `matTabContent` của Material.
3481
+ *
3482
+ * `descendants: false` để chỉ bắt `<ng-template>` là con TRỰC TIẾP, tránh nuốt
3483
+ * nhầm template lồng sâu bên trong nội dung tab.
3484
+ */
3273
3485
  class TabComponent {
3274
3486
  label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
3275
3487
  icon;
@@ -3277,16 +3489,20 @@ class TabComponent {
3277
3489
  count = input(null, ...(ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []));
3278
3490
  /** Tints the count badge (e.g. vulnerabilities tab shows a danger count). */
3279
3491
  tone = input(null, ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
3280
- content = viewChild.required(TemplateRef);
3492
+ content = contentChild.required(TemplateRef, { descendants: false });
3281
3493
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3282
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.20", type: TabComponent, isStandalone: true, selector: "fx-ui-tab", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "content", first: true, predicate: TemplateRef, descendants: true, isSignal: true }], ngImport: i0, template: "<ng-template><ng-content></ng-content></ng-template>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
3494
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.20", type: TabComponent, isStandalone: true, selector: "fx-ui-tab", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "content", first: true, predicate: TemplateRef, isSignal: true }], ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
3283
3495
  }
3284
3496
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TabComponent, decorators: [{
3285
3497
  type: Component,
3286
- args: [{ selector: 'fx-ui-tab', changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-template><ng-content></ng-content></ng-template>\n" }]
3498
+ args: [{
3499
+ selector: 'fx-ui-tab',
3500
+ template: '',
3501
+ changeDetection: ChangeDetectionStrategy.OnPush,
3502
+ }]
3287
3503
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], icon: [{
3288
3504
  type: Input
3289
- }], count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], content: [{ type: i0.ViewChild, args: [i0.forwardRef(() => TemplateRef), { isSignal: true }] }] } });
3505
+ }], count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], content: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TemplateRef), { ...{ descendants: false }, isSignal: true }] }] } });
3290
3506
 
3291
3507
  class TabGroupComponent {
3292
3508
  renderer;
@@ -3770,7 +3986,7 @@ class DrawerComponent {
3770
3986
  return this.isExpanded ? 'expanded' : 'collapsed';
3771
3987
  }
3772
3988
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DrawerComponent, deps: [{ token: i2$1.Router }], target: i0.ɵɵFactoryTarget.Component });
3773
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: DrawerComponent, isStandalone: true, selector: "fx-ui-drawer", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, footerItems: { classPropertyName: "footerItems", publicName: "footerItems", isSignal: true, isRequired: false, transformFunction: null }, isExpanded: { classPropertyName: "isExpanded", publicName: "isExpanded", isSignal: false, isRequired: false, transformFunction: null }, logo: { classPropertyName: "logo", publicName: "logo", isSignal: false, isRequired: false, transformFunction: null }, logoText: { classPropertyName: "logoText", publicName: "logoText", isSignal: false, isRequired: false, transformFunction: null }, logoSubtext: { classPropertyName: "logoSubtext", publicName: "logoSubtext", isSignal: false, isRequired: false, transformFunction: null }, version: { classPropertyName: "version", publicName: "version", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { toggleExpanded: "toggleExpanded", itemClicked: "itemClicked" }, ngImport: i0, template: "<div class=\"drawer-host-wrapper\">\n <a href=\"/\" class=\"drawer-brand\" [class.is-collapsed]=\"!isExpanded\">\n @if (logo) {\n <img [src]=\"logo\" alt=\"Home\" class=\"drawer-brand-mark\" />\n } @else {\n <div class=\"drawer-brand-mark\"></div>\n }\n @if (isExpanded && (logoText || logoSubtext)) {\n <span class=\"drawer-brand-copy\">\n @if (logoText) {\n <span class=\"drawer-brand-text\">{{ logoText }}</span>\n }\n @if (logoSubtext) {\n <span class=\"drawer-brand-subtext\">{{ logoSubtext }}</span>\n }\n </span>\n }\n </a>\n <div class=\"drawer-container\" [@drawerExpand]=\"getDrawerState()\">\n <!-- Navigation Items -->\n <nav class=\"drawer-nav\">\n @for (item of items(); track item) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: item, level: 0 }\"\n ></ng-container>\n }\n </nav>\n\n <!-- V2: bottom section (Administration group, user block, \u2026) -->\n <div class=\"mt-auto\">\n @if (footerItems().length) {\n <nav class=\"drawer-footer-nav\">\n @for (item of footerItems(); track item) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: item, level: 0 }\"\n ></ng-container>\n }\n </nav>\n }\n <ng-content select=\"[drawerFooter]\"></ng-content>\n </div>\n\n <!-- Version -->\n @if (version) {\n <div class=\"drawer-version\" [matTooltip]=\"!isExpanded ? version : ''\" matTooltipPosition=\"right\">\n @if (isExpanded) {\n <span>{{ version }}</span>\n }\n @if (!isExpanded) {\n <span class=\"drawer-version-dot\"></span>\n }\n </div>\n }\n </div>\n\n <!-- Floating Toggle Button on right border -->\n <button\n type=\"button\"\n class=\"toggle-btn\"\n [attr.aria-label]=\"isExpanded ? 'Collapse navigation' : 'Expand navigation'\"\n [attr.aria-expanded]=\"isExpanded\"\n (click)=\"toggleDrawer()\"\n [matTooltip]=\"isExpanded ? 'Collapse' : 'Expand'\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n matTooltipPosition=\"right\"\n >\n <fx-ui-hero-icon\n [icon]=\"isExpanded ? 'chevron-left' : 'chevron-right'\"\n [size]=\"16\"\n class=\"text-text-primary\"\n ></fx-ui-hero-icon>\n </button>\n</div>\n\n<!-- Recursive Template for Drawer Items -->\n<ng-template #drawerItem let-item let-level=\"level\">\n @if (item.heading) {\n @if (isExpanded) {\n <div class=\"drawer-section-label\">{{ item.label }}</div>\n } @else {\n <div class=\"drawer-section-rule\"></div>\n }\n } @else {\n <div class=\"drawer-item-wrapper\">\n <!-- Item Button/Link -->\n @if (item.route && !item.children) {\n <a\n [routerLink]=\"item.route\"\n class=\"drawer-item\"\n [class.active]=\"isActiveRoute(item.route)\"\n [class.has-children]=\"item.children && item.children.length > 0\"\n [style.paddingLeft.px]=\"isExpanded ? 16 + level * 16 : 16\"\n [matTooltip]=\"!isExpanded ? item.label : ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n (click)=\"onItemClick(item, $event)\"\n >\n <div class=\"item-content\">\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"20\" class=\"item-icon\"></fx-ui-hero-icon>\n @if (isExpanded) {\n <span class=\"item-label\">{{ item.label }}</span>\n }\n </div>\n </a>\n }\n\n @if (!item.route || item.children) {\n <button\n type=\"button\"\n class=\"drawer-item\"\n [class.active]=\"isActiveRoute(item.route)\"\n [class.has-children]=\"item.children && item.children.length > 0\"\n [style.paddingLeft.px]=\"isExpanded ? 16 + level * 16 : 16\"\n [matTooltip]=\"!isExpanded ? item.label : ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-sm font-semibold\"\n (click)=\"onItemClick(item, $event)\"\n >\n <div class=\"item-content\">\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"20\" class=\"item-icon\"></fx-ui-hero-icon>\n @if (isExpanded) {\n <span class=\"item-label\">{{ item.label }}</span>\n }\n @if (isExpanded && item.children && item.children.length > 0) {\n <fx-ui-hero-icon\n [icon]=\"'chevron-down'\"\n [size]=\"16\"\n class=\"chevron-icon\"\n [class.rotated]=\"isAccordionExpanded(item.id)\"\n ></fx-ui-hero-icon>\n }\n </div>\n </button>\n }\n\n <!-- Accordion Children -->\n @if (item.children && item.children.length > 0) {\n <div\n class=\"accordion-children\"\n [@accordionExpand]=\"getAccordionState(item.id)\"\n >\n @for (child of item.children; track child) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: child, level: level + 1 }\"\n ></ng-container>\n }\n </div>\n }\n </div>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";@layer properties;:host{display:block;height:100%}.drawer-host-wrapper{position:relative;display:flex;height:100%;flex-direction:column;border-right-style:var(--tw-border-style);border-right-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--og-page-bg))}.drawer-brand{display:flex;height:60px;flex-shrink:0;align-items:center;gap:calc(var(--spacing, .25rem) * 3);overflow:hidden;text-decoration-line:none;padding-left:24px;padding-right:8px;transition:padding .3s cubic-bezier(.4,0,.2,1);max-width:260px}.drawer-brand.is-collapsed{justify-content:center;padding-left:0;padding-right:0;max-width:70px}.drawer-brand-mark{height:20px;width:auto;flex-shrink:0}.drawer-brand-copy{display:flex;min-width:0px;flex-direction:column;--tw-leading: var(--leading-tight, 1.25);line-height:var(--leading-tight, 1.25)}.drawer-brand-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:15px;--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:rgb(var(--text-primary));-webkit-user-select:none;user-select:none;font-family:var(--og-font-display);letter-spacing:-.01em}.drawer-brand-subtext{margin-top:1px;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;font-size:11px;color:rgb(var(--text-placeholder));-webkit-user-select:none;user-select:none;letter-spacing:var(--og-tracking-label)}.drawer-container{display:flex;min-height:0px;flex:1;flex-direction:column;overflow:hidden;padding-inline:calc(var(--spacing, .25rem) * 2);transition:width .3s cubic-bezier(.4,0,.2,1)}.toggle-btn{position:absolute;top:50%;z-index:50;display:flex;height:calc(var(--spacing, .25rem) * 6);width:calc(var(--spacing, .25rem) * 6);--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y);cursor:pointer;align-items:center;justify-content:center;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--bg-primary));--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(var(--shadow-color) / .05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);right:-12px;transition:all .2s ease}.toggle-btn:hover{background-color:rgb(var(--bg-primary-hover));--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.toggle-btn:active{transform:translateY(-50%) scale(.9)}.toggle-btn:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-offset:2px;outline-color:rgb(var(--primary))}.drawer-footer-nav{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default));padding-inline:0px;padding-top:calc(var(--spacing, .25rem) * 2);padding-bottom:var(--spacing, .25rem)}.drawer-nav{flex:1;overflow-x:hidden;overflow-y:auto;padding-block:calc(var(--spacing, .25rem) * 2)}.drawer-nav::-webkit-scrollbar{width:calc(var(--spacing, .25rem) * 1.5)}.drawer-nav::-webkit-scrollbar-track{background-color:transparent}.drawer-nav::-webkit-scrollbar-thumb{border-radius:var(--og-radius-control);background-color:rgb(var(--border-default))}.drawer-nav::-webkit-scrollbar-thumb:hover{background-color:rgb(var(--text-placeholder))}.drawer-section-label{padding-inline:1rem;padding-top:calc(var(--spacing, .25rem) * 4);padding-bottom:var(--spacing, .25rem);font-size:11px;--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:rgb(var(--og-text-faint));text-transform:uppercase;-webkit-user-select:none;user-select:none;letter-spacing:var(--og-tracking-label)}.drawer-section-rule{margin-inline:calc(var(--spacing, .25rem) * 3);margin-block:calc(var(--spacing, .25rem) * 3);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default))}.drawer-item-wrapper{position:relative}.drawer-item{position:relative;margin-top:var(--spacing, .25rem);display:flex;min-height:calc(var(--spacing, .25rem) * 8);width:100%;cursor:pointer;align-items:center;border-radius:var(--og-radius-control);--tw-border-style: none;border-style:none;background-color:transparent;padding-inline:1rem;padding-block:calc(var(--spacing, .25rem) * 2);color:rgb(var(--text-primary));text-decoration-line:none;transition:all .2s ease}.drawer-item:hover{--tw-scale-x: 95%;--tw-scale-y: 95%;--tw-scale-z: 95%;scale:var(--tw-scale-x) var(--tw-scale-y);color:rgb(var(--text-primary));background:rgb(var(--og-surface-raised)/.5)}.drawer-item.active{--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:rgb(var(--text-primary));background:rgb(var(--og-surface-raised)/.8);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:inset 0 0 0 1px rgb(var(--og-border)),0 1px 2px rgb(var(--shadow-color)/.05)}.drawer-item.active:hover{background:rgb(var(--og-surface-raised)/.95)}.drawer-item:active{transform:scale(.98)}.drawer-item:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-offset:2px;outline-color:rgb(var(--primary))}.drawer-item{-webkit-tap-highlight-color:transparent;--tw-outline-style: none;outline-style:none}.item-content{display:flex;width:100%;align-items:center;gap:calc(var(--spacing, .25rem) * 3);overflow:hidden}.item-icon{flex-shrink:0;color:inherit}.item-label{flex:1;overflow:hidden;text-align:left;font-size:13px;--tw-leading: calc(var(--spacing, .25rem) * 5);line-height:calc(var(--spacing, .25rem) * 5);text-overflow:ellipsis;white-space:nowrap}.chevron-icon{margin-left:auto;flex-shrink:0;color:rgb(var(--text-primary));transition:transform .3s cubic-bezier(.4,0,.2,1)}.chevron-icon.rotated{transform:rotate(180deg)}.accordion-children{overflow:hidden}.drawer-version{display:flex;align-items:center;justify-content:center;border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default));padding-inline:calc(var(--spacing, .25rem) * 4);padding-block:calc(var(--spacing, .25rem) * 3);font-size:11px;color:rgb(var(--text-placeholder));-webkit-user-select:none;user-select:none;font-family:var(--og-font-mono)}.drawer-version-dot{display:block;height:calc(var(--spacing, .25rem) * 1.5);width:calc(var(--spacing, .25rem) * 1.5);border-radius:calc(infinity * 1px);background-color:rgb(var(--text-placeholder))}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-leading{syntax: \"*\"; inherits: false;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-translate-x{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-y{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-z{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-outline-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-scale-x{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-scale-y{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-scale-z{syntax: \"*\"; inherits: false; initial-value: 1;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-leading: initial;--tw-font-weight: initial;--tw-translate-x: 0;--tw-translate-y: 0;--tw-translate-z: 0;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-outline-style: solid;--tw-scale-x: 1;--tw-scale-y: 1;--tw-scale-z: 1}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], animations: [
3989
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: DrawerComponent, isStandalone: true, selector: "fx-ui-drawer", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, footerItems: { classPropertyName: "footerItems", publicName: "footerItems", isSignal: true, isRequired: false, transformFunction: null }, isExpanded: { classPropertyName: "isExpanded", publicName: "isExpanded", isSignal: false, isRequired: false, transformFunction: null }, logo: { classPropertyName: "logo", publicName: "logo", isSignal: false, isRequired: false, transformFunction: null }, logoText: { classPropertyName: "logoText", publicName: "logoText", isSignal: false, isRequired: false, transformFunction: null }, logoSubtext: { classPropertyName: "logoSubtext", publicName: "logoSubtext", isSignal: false, isRequired: false, transformFunction: null }, version: { classPropertyName: "version", publicName: "version", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { toggleExpanded: "toggleExpanded", itemClicked: "itemClicked" }, ngImport: i0, template: "<div class=\"drawer-host-wrapper\">\n <a href=\"/\" class=\"drawer-brand\" [class.is-collapsed]=\"!isExpanded\">\n @if (logo) {\n <img [src]=\"logo\" alt=\"Home\" class=\"drawer-brand-mark\" />\n } @else {\n <div class=\"drawer-brand-mark\"></div>\n }\n @if (isExpanded && (logoText || logoSubtext)) {\n <span class=\"drawer-brand-copy\">\n @if (logoText) {\n <span class=\"drawer-brand-text\">{{ logoText }}</span>\n }\n @if (logoSubtext) {\n <span class=\"drawer-brand-subtext\">{{ logoSubtext }}</span>\n }\n </span>\n }\n </a>\n <div class=\"drawer-container\" [@drawerExpand]=\"getDrawerState()\">\n <!-- Navigation Items -->\n <nav class=\"drawer-nav\">\n @for (item of items(); track item) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: item, level: 0 }\"\n ></ng-container>\n }\n </nav>\n\n <!-- V2: bottom section (Administration group, user block, \u2026) -->\n <div class=\"mt-auto\">\n @if (footerItems().length) {\n <nav class=\"drawer-footer-nav\">\n @for (item of footerItems(); track item) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: item, level: 0 }\"\n ></ng-container>\n }\n </nav>\n }\n <ng-content select=\"[drawerFooter]\"></ng-content>\n </div>\n\n <!-- Version -->\n @if (version) {\n <div class=\"drawer-version\" [matTooltip]=\"!isExpanded ? version : ''\" matTooltipPosition=\"right\">\n @if (isExpanded) {\n <span>{{ version }}</span>\n }\n @if (!isExpanded) {\n <span class=\"drawer-version-dot\"></span>\n }\n </div>\n }\n </div>\n\n <!-- Floating Toggle Button on right border -->\n <button\n type=\"button\"\n class=\"toggle-btn\"\n [attr.aria-label]=\"isExpanded ? 'Collapse navigation' : 'Expand navigation'\"\n [attr.aria-expanded]=\"isExpanded\"\n (click)=\"toggleDrawer()\"\n [matTooltip]=\"isExpanded ? 'Collapse' : 'Expand'\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n matTooltipPosition=\"right\"\n >\n <fx-ui-hero-icon\n [icon]=\"isExpanded ? 'chevron-left' : 'chevron-right'\"\n [size]=\"16\"\n class=\"text-text-primary\"\n ></fx-ui-hero-icon>\n </button>\n</div>\n\n<!-- Recursive Template for Drawer Items -->\n<ng-template #drawerItem let-item let-level=\"level\">\n @if (item.heading) {\n @if (isExpanded) {\n <div class=\"drawer-section-label\">{{ item.label }}</div>\n } @else {\n <div class=\"drawer-section-rule\"></div>\n }\n } @else {\n <div class=\"drawer-item-wrapper\">\n <!-- Item Button/Link -->\n @if (item.route && !item.children) {\n <a\n [routerLink]=\"item.route\"\n class=\"drawer-item\"\n [class.active]=\"isActiveRoute(item.route)\"\n [class.has-children]=\"item.children && item.children.length > 0\"\n [style.paddingLeft.px]=\"isExpanded ? 16 + level * 16 : 16\"\n [matTooltip]=\"!isExpanded ? item.label : ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n (click)=\"onItemClick(item, $event)\"\n >\n <div class=\"item-content\">\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"20\" class=\"item-icon\"></fx-ui-hero-icon>\n @if (isExpanded) {\n <span class=\"item-label\">{{ item.label }}</span>\n }\n </div>\n </a>\n }\n\n @if (!item.route || item.children) {\n <button\n type=\"button\"\n class=\"drawer-item\"\n [class.active]=\"isActiveRoute(item.route)\"\n [class.has-children]=\"item.children && item.children.length > 0\"\n [style.paddingLeft.px]=\"isExpanded ? 16 + level * 16 : 16\"\n [matTooltip]=\"!isExpanded ? item.label : ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-sm font-semibold\"\n (click)=\"onItemClick(item, $event)\"\n >\n <div class=\"item-content\">\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"20\" class=\"item-icon\"></fx-ui-hero-icon>\n @if (isExpanded) {\n <span class=\"item-label\">{{ item.label }}</span>\n }\n @if (isExpanded && item.children && item.children.length > 0) {\n <fx-ui-hero-icon\n [icon]=\"'chevron-down'\"\n [size]=\"16\"\n class=\"chevron-icon\"\n [class.rotated]=\"isAccordionExpanded(item.id)\"\n ></fx-ui-hero-icon>\n }\n </div>\n </button>\n }\n\n <!-- Accordion Children -->\n @if (item.children && item.children.length > 0) {\n <div\n class=\"accordion-children\"\n [@accordionExpand]=\"getAccordionState(item.id)\"\n >\n @for (child of item.children; track child) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: child, level: level + 1 }\"\n ></ng-container>\n }\n </div>\n }\n </div>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";@layer properties;:host{display:block;height:100%}.drawer-host-wrapper{position:relative;display:flex;height:100%;flex-direction:column;border-right-style:var(--tw-border-style);border-right-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--og-page-bg))}.drawer-brand{display:flex;height:60px;flex-shrink:0;align-items:center;gap:calc(var(--spacing, .25rem) * 3);overflow:hidden;text-decoration-line:none;padding-left:24px;padding-right:8px;transition:padding .3s cubic-bezier(.4,0,.2,1);max-width:260px}.drawer-brand.is-collapsed{justify-content:center;padding-left:0;padding-right:0;max-width:70px}.drawer-brand-mark{height:20px;width:auto;flex-shrink:0}.drawer-brand-copy{display:flex;min-width:0px;flex-direction:column;--tw-leading: var(--leading-tight, 1.25);line-height:var(--leading-tight, 1.25)}.drawer-brand-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:15px;--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:rgb(var(--text-primary));-webkit-user-select:none;user-select:none;font-family:var(--og-font-display);letter-spacing:-.01em}.drawer-brand-subtext{margin-top:1px;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;font-size:11px;color:rgb(var(--text-placeholder));-webkit-user-select:none;user-select:none;letter-spacing:var(--og-tracking-label)}.drawer-container{display:flex;min-height:0px;flex:1;flex-direction:column;overflow:hidden;padding-inline:calc(var(--spacing, .25rem) * 2);transition:width .3s cubic-bezier(.4,0,.2,1)}.toggle-btn{position:absolute;top:50%;z-index:50;display:flex;height:calc(var(--spacing, .25rem) * 6);width:calc(var(--spacing, .25rem) * 6);--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y);cursor:pointer;align-items:center;justify-content:center;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--bg-primary));--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(var(--shadow-color) / .05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);right:-12px;transition:all .2s ease}.toggle-btn:hover{background-color:rgb(var(--bg-primary-hover));--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.toggle-btn:active{transform:translateY(-50%) scale(.9)}.toggle-btn:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-offset:2px;outline-color:rgb(var(--primary))}.drawer-footer-nav{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default));padding-inline:0px;padding-top:calc(var(--spacing, .25rem) * 2);padding-bottom:var(--spacing, .25rem)}.drawer-nav{flex:1;overflow-x:hidden;overflow-y:auto;padding-block:calc(var(--spacing, .25rem) * 2)}.drawer-nav::-webkit-scrollbar{width:calc(var(--spacing, .25rem) * 1.5)}.drawer-nav::-webkit-scrollbar-track{background-color:transparent}.drawer-nav::-webkit-scrollbar-thumb{border-radius:var(--og-radius-control);background-color:rgb(var(--border-default))}.drawer-nav::-webkit-scrollbar-thumb:hover{background-color:rgb(var(--text-placeholder))}.drawer-section-label{padding-inline:1rem;padding-top:calc(var(--spacing, .25rem) * 4);padding-bottom:var(--spacing, .25rem);font-size:10.5px;line-height:1.4;--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:rgb(var(--og-text-faint));-webkit-user-select:none;user-select:none;letter-spacing:var(--og-tracking-label)}.drawer-section-rule{margin-inline:calc(var(--spacing, .25rem) * 3);margin-block:calc(var(--spacing, .25rem) * 3);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default))}.drawer-item-wrapper{position:relative}.drawer-item{position:relative;margin-top:var(--spacing, .25rem);display:flex;min-height:calc(var(--spacing, .25rem) * 8);width:100%;cursor:pointer;align-items:center;border-radius:var(--og-radius-control);--tw-border-style: none;border-style:none;background-color:transparent;padding-inline:1rem;padding-block:calc(var(--spacing, .25rem) * 2);color:rgb(var(--text-primary));text-decoration-line:none;transition:all .2s ease}.drawer-item:hover{--tw-scale-x: 95%;--tw-scale-y: 95%;--tw-scale-z: 95%;scale:var(--tw-scale-x) var(--tw-scale-y);color:rgb(var(--text-primary));background:rgb(var(--og-surface-raised)/.5)}.drawer-item.active{--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:rgb(var(--text-primary));background:rgb(var(--og-surface-raised)/.8);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:inset 0 0 0 1px rgb(var(--og-border)),0 1px 2px rgb(var(--shadow-color)/.05)}.drawer-item.active:hover{background:rgb(var(--og-surface-raised)/.95)}.drawer-item:active{transform:scale(.98)}.drawer-item:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-offset:2px;outline-color:rgb(var(--primary))}.drawer-item{-webkit-tap-highlight-color:transparent;--tw-outline-style: none;outline-style:none}.item-content{display:flex;width:100%;align-items:center;gap:calc(var(--spacing, .25rem) * 3);overflow:hidden}.item-icon{flex-shrink:0;color:inherit}.item-label{flex:1;overflow:hidden;text-align:left;font-size:13px;--tw-leading: calc(var(--spacing, .25rem) * 5);line-height:calc(var(--spacing, .25rem) * 5);text-overflow:ellipsis;white-space:nowrap}.chevron-icon{margin-left:auto;flex-shrink:0;color:rgb(var(--text-primary));transition:transform .3s cubic-bezier(.4,0,.2,1)}.chevron-icon.rotated{transform:rotate(180deg)}.accordion-children{overflow:hidden}.drawer-version{display:flex;align-items:center;justify-content:center;border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default));padding-inline:calc(var(--spacing, .25rem) * 4);padding-block:calc(var(--spacing, .25rem) * 3);font-size:11px;color:rgb(var(--text-placeholder));-webkit-user-select:none;user-select:none;font-family:var(--og-font-mono)}.drawer-version-dot{display:block;height:calc(var(--spacing, .25rem) * 1.5);width:calc(var(--spacing, .25rem) * 1.5);border-radius:calc(infinity * 1px);background-color:rgb(var(--text-placeholder))}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-leading{syntax: \"*\"; inherits: false;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-translate-x{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-y{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-z{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-outline-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-scale-x{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-scale-y{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-scale-z{syntax: \"*\"; inherits: false; initial-value: 1;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-leading: initial;--tw-font-weight: initial;--tw-translate-x: 0;--tw-translate-y: 0;--tw-translate-z: 0;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-outline-style: solid;--tw-scale-x: 1;--tw-scale-y: 1;--tw-scale-z: 1}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], animations: [
3774
3990
  trigger('drawerExpand', [
3775
3991
  state('collapsed', style({ width: '70px' })),
3776
3992
  state('expanded', style({ width: '260px' })),
@@ -3801,7 +4017,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3801
4017
  MatTooltip,
3802
4018
  HeroIconComponent,
3803
4019
  RouterLink,
3804
- ], template: "<div class=\"drawer-host-wrapper\">\n <a href=\"/\" class=\"drawer-brand\" [class.is-collapsed]=\"!isExpanded\">\n @if (logo) {\n <img [src]=\"logo\" alt=\"Home\" class=\"drawer-brand-mark\" />\n } @else {\n <div class=\"drawer-brand-mark\"></div>\n }\n @if (isExpanded && (logoText || logoSubtext)) {\n <span class=\"drawer-brand-copy\">\n @if (logoText) {\n <span class=\"drawer-brand-text\">{{ logoText }}</span>\n }\n @if (logoSubtext) {\n <span class=\"drawer-brand-subtext\">{{ logoSubtext }}</span>\n }\n </span>\n }\n </a>\n <div class=\"drawer-container\" [@drawerExpand]=\"getDrawerState()\">\n <!-- Navigation Items -->\n <nav class=\"drawer-nav\">\n @for (item of items(); track item) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: item, level: 0 }\"\n ></ng-container>\n }\n </nav>\n\n <!-- V2: bottom section (Administration group, user block, \u2026) -->\n <div class=\"mt-auto\">\n @if (footerItems().length) {\n <nav class=\"drawer-footer-nav\">\n @for (item of footerItems(); track item) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: item, level: 0 }\"\n ></ng-container>\n }\n </nav>\n }\n <ng-content select=\"[drawerFooter]\"></ng-content>\n </div>\n\n <!-- Version -->\n @if (version) {\n <div class=\"drawer-version\" [matTooltip]=\"!isExpanded ? version : ''\" matTooltipPosition=\"right\">\n @if (isExpanded) {\n <span>{{ version }}</span>\n }\n @if (!isExpanded) {\n <span class=\"drawer-version-dot\"></span>\n }\n </div>\n }\n </div>\n\n <!-- Floating Toggle Button on right border -->\n <button\n type=\"button\"\n class=\"toggle-btn\"\n [attr.aria-label]=\"isExpanded ? 'Collapse navigation' : 'Expand navigation'\"\n [attr.aria-expanded]=\"isExpanded\"\n (click)=\"toggleDrawer()\"\n [matTooltip]=\"isExpanded ? 'Collapse' : 'Expand'\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n matTooltipPosition=\"right\"\n >\n <fx-ui-hero-icon\n [icon]=\"isExpanded ? 'chevron-left' : 'chevron-right'\"\n [size]=\"16\"\n class=\"text-text-primary\"\n ></fx-ui-hero-icon>\n </button>\n</div>\n\n<!-- Recursive Template for Drawer Items -->\n<ng-template #drawerItem let-item let-level=\"level\">\n @if (item.heading) {\n @if (isExpanded) {\n <div class=\"drawer-section-label\">{{ item.label }}</div>\n } @else {\n <div class=\"drawer-section-rule\"></div>\n }\n } @else {\n <div class=\"drawer-item-wrapper\">\n <!-- Item Button/Link -->\n @if (item.route && !item.children) {\n <a\n [routerLink]=\"item.route\"\n class=\"drawer-item\"\n [class.active]=\"isActiveRoute(item.route)\"\n [class.has-children]=\"item.children && item.children.length > 0\"\n [style.paddingLeft.px]=\"isExpanded ? 16 + level * 16 : 16\"\n [matTooltip]=\"!isExpanded ? item.label : ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n (click)=\"onItemClick(item, $event)\"\n >\n <div class=\"item-content\">\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"20\" class=\"item-icon\"></fx-ui-hero-icon>\n @if (isExpanded) {\n <span class=\"item-label\">{{ item.label }}</span>\n }\n </div>\n </a>\n }\n\n @if (!item.route || item.children) {\n <button\n type=\"button\"\n class=\"drawer-item\"\n [class.active]=\"isActiveRoute(item.route)\"\n [class.has-children]=\"item.children && item.children.length > 0\"\n [style.paddingLeft.px]=\"isExpanded ? 16 + level * 16 : 16\"\n [matTooltip]=\"!isExpanded ? item.label : ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-sm font-semibold\"\n (click)=\"onItemClick(item, $event)\"\n >\n <div class=\"item-content\">\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"20\" class=\"item-icon\"></fx-ui-hero-icon>\n @if (isExpanded) {\n <span class=\"item-label\">{{ item.label }}</span>\n }\n @if (isExpanded && item.children && item.children.length > 0) {\n <fx-ui-hero-icon\n [icon]=\"'chevron-down'\"\n [size]=\"16\"\n class=\"chevron-icon\"\n [class.rotated]=\"isAccordionExpanded(item.id)\"\n ></fx-ui-hero-icon>\n }\n </div>\n </button>\n }\n\n <!-- Accordion Children -->\n @if (item.children && item.children.length > 0) {\n <div\n class=\"accordion-children\"\n [@accordionExpand]=\"getAccordionState(item.id)\"\n >\n @for (child of item.children; track child) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: child, level: level + 1 }\"\n ></ng-container>\n }\n </div>\n }\n </div>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";@layer properties;:host{display:block;height:100%}.drawer-host-wrapper{position:relative;display:flex;height:100%;flex-direction:column;border-right-style:var(--tw-border-style);border-right-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--og-page-bg))}.drawer-brand{display:flex;height:60px;flex-shrink:0;align-items:center;gap:calc(var(--spacing, .25rem) * 3);overflow:hidden;text-decoration-line:none;padding-left:24px;padding-right:8px;transition:padding .3s cubic-bezier(.4,0,.2,1);max-width:260px}.drawer-brand.is-collapsed{justify-content:center;padding-left:0;padding-right:0;max-width:70px}.drawer-brand-mark{height:20px;width:auto;flex-shrink:0}.drawer-brand-copy{display:flex;min-width:0px;flex-direction:column;--tw-leading: var(--leading-tight, 1.25);line-height:var(--leading-tight, 1.25)}.drawer-brand-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:15px;--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:rgb(var(--text-primary));-webkit-user-select:none;user-select:none;font-family:var(--og-font-display);letter-spacing:-.01em}.drawer-brand-subtext{margin-top:1px;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;font-size:11px;color:rgb(var(--text-placeholder));-webkit-user-select:none;user-select:none;letter-spacing:var(--og-tracking-label)}.drawer-container{display:flex;min-height:0px;flex:1;flex-direction:column;overflow:hidden;padding-inline:calc(var(--spacing, .25rem) * 2);transition:width .3s cubic-bezier(.4,0,.2,1)}.toggle-btn{position:absolute;top:50%;z-index:50;display:flex;height:calc(var(--spacing, .25rem) * 6);width:calc(var(--spacing, .25rem) * 6);--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y);cursor:pointer;align-items:center;justify-content:center;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--bg-primary));--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(var(--shadow-color) / .05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);right:-12px;transition:all .2s ease}.toggle-btn:hover{background-color:rgb(var(--bg-primary-hover));--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.toggle-btn:active{transform:translateY(-50%) scale(.9)}.toggle-btn:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-offset:2px;outline-color:rgb(var(--primary))}.drawer-footer-nav{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default));padding-inline:0px;padding-top:calc(var(--spacing, .25rem) * 2);padding-bottom:var(--spacing, .25rem)}.drawer-nav{flex:1;overflow-x:hidden;overflow-y:auto;padding-block:calc(var(--spacing, .25rem) * 2)}.drawer-nav::-webkit-scrollbar{width:calc(var(--spacing, .25rem) * 1.5)}.drawer-nav::-webkit-scrollbar-track{background-color:transparent}.drawer-nav::-webkit-scrollbar-thumb{border-radius:var(--og-radius-control);background-color:rgb(var(--border-default))}.drawer-nav::-webkit-scrollbar-thumb:hover{background-color:rgb(var(--text-placeholder))}.drawer-section-label{padding-inline:1rem;padding-top:calc(var(--spacing, .25rem) * 4);padding-bottom:var(--spacing, .25rem);font-size:11px;--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:rgb(var(--og-text-faint));text-transform:uppercase;-webkit-user-select:none;user-select:none;letter-spacing:var(--og-tracking-label)}.drawer-section-rule{margin-inline:calc(var(--spacing, .25rem) * 3);margin-block:calc(var(--spacing, .25rem) * 3);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default))}.drawer-item-wrapper{position:relative}.drawer-item{position:relative;margin-top:var(--spacing, .25rem);display:flex;min-height:calc(var(--spacing, .25rem) * 8);width:100%;cursor:pointer;align-items:center;border-radius:var(--og-radius-control);--tw-border-style: none;border-style:none;background-color:transparent;padding-inline:1rem;padding-block:calc(var(--spacing, .25rem) * 2);color:rgb(var(--text-primary));text-decoration-line:none;transition:all .2s ease}.drawer-item:hover{--tw-scale-x: 95%;--tw-scale-y: 95%;--tw-scale-z: 95%;scale:var(--tw-scale-x) var(--tw-scale-y);color:rgb(var(--text-primary));background:rgb(var(--og-surface-raised)/.5)}.drawer-item.active{--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:rgb(var(--text-primary));background:rgb(var(--og-surface-raised)/.8);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:inset 0 0 0 1px rgb(var(--og-border)),0 1px 2px rgb(var(--shadow-color)/.05)}.drawer-item.active:hover{background:rgb(var(--og-surface-raised)/.95)}.drawer-item:active{transform:scale(.98)}.drawer-item:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-offset:2px;outline-color:rgb(var(--primary))}.drawer-item{-webkit-tap-highlight-color:transparent;--tw-outline-style: none;outline-style:none}.item-content{display:flex;width:100%;align-items:center;gap:calc(var(--spacing, .25rem) * 3);overflow:hidden}.item-icon{flex-shrink:0;color:inherit}.item-label{flex:1;overflow:hidden;text-align:left;font-size:13px;--tw-leading: calc(var(--spacing, .25rem) * 5);line-height:calc(var(--spacing, .25rem) * 5);text-overflow:ellipsis;white-space:nowrap}.chevron-icon{margin-left:auto;flex-shrink:0;color:rgb(var(--text-primary));transition:transform .3s cubic-bezier(.4,0,.2,1)}.chevron-icon.rotated{transform:rotate(180deg)}.accordion-children{overflow:hidden}.drawer-version{display:flex;align-items:center;justify-content:center;border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default));padding-inline:calc(var(--spacing, .25rem) * 4);padding-block:calc(var(--spacing, .25rem) * 3);font-size:11px;color:rgb(var(--text-placeholder));-webkit-user-select:none;user-select:none;font-family:var(--og-font-mono)}.drawer-version-dot{display:block;height:calc(var(--spacing, .25rem) * 1.5);width:calc(var(--spacing, .25rem) * 1.5);border-radius:calc(infinity * 1px);background-color:rgb(var(--text-placeholder))}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-leading{syntax: \"*\"; inherits: false;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-translate-x{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-y{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-z{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-outline-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-scale-x{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-scale-y{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-scale-z{syntax: \"*\"; inherits: false; initial-value: 1;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-leading: initial;--tw-font-weight: initial;--tw-translate-x: 0;--tw-translate-y: 0;--tw-translate-z: 0;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-outline-style: solid;--tw-scale-x: 1;--tw-scale-y: 1;--tw-scale-z: 1}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
4020
+ ], template: "<div class=\"drawer-host-wrapper\">\n <a href=\"/\" class=\"drawer-brand\" [class.is-collapsed]=\"!isExpanded\">\n @if (logo) {\n <img [src]=\"logo\" alt=\"Home\" class=\"drawer-brand-mark\" />\n } @else {\n <div class=\"drawer-brand-mark\"></div>\n }\n @if (isExpanded && (logoText || logoSubtext)) {\n <span class=\"drawer-brand-copy\">\n @if (logoText) {\n <span class=\"drawer-brand-text\">{{ logoText }}</span>\n }\n @if (logoSubtext) {\n <span class=\"drawer-brand-subtext\">{{ logoSubtext }}</span>\n }\n </span>\n }\n </a>\n <div class=\"drawer-container\" [@drawerExpand]=\"getDrawerState()\">\n <!-- Navigation Items -->\n <nav class=\"drawer-nav\">\n @for (item of items(); track item) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: item, level: 0 }\"\n ></ng-container>\n }\n </nav>\n\n <!-- V2: bottom section (Administration group, user block, \u2026) -->\n <div class=\"mt-auto\">\n @if (footerItems().length) {\n <nav class=\"drawer-footer-nav\">\n @for (item of footerItems(); track item) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: item, level: 0 }\"\n ></ng-container>\n }\n </nav>\n }\n <ng-content select=\"[drawerFooter]\"></ng-content>\n </div>\n\n <!-- Version -->\n @if (version) {\n <div class=\"drawer-version\" [matTooltip]=\"!isExpanded ? version : ''\" matTooltipPosition=\"right\">\n @if (isExpanded) {\n <span>{{ version }}</span>\n }\n @if (!isExpanded) {\n <span class=\"drawer-version-dot\"></span>\n }\n </div>\n }\n </div>\n\n <!-- Floating Toggle Button on right border -->\n <button\n type=\"button\"\n class=\"toggle-btn\"\n [attr.aria-label]=\"isExpanded ? 'Collapse navigation' : 'Expand navigation'\"\n [attr.aria-expanded]=\"isExpanded\"\n (click)=\"toggleDrawer()\"\n [matTooltip]=\"isExpanded ? 'Collapse' : 'Expand'\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n matTooltipPosition=\"right\"\n >\n <fx-ui-hero-icon\n [icon]=\"isExpanded ? 'chevron-left' : 'chevron-right'\"\n [size]=\"16\"\n class=\"text-text-primary\"\n ></fx-ui-hero-icon>\n </button>\n</div>\n\n<!-- Recursive Template for Drawer Items -->\n<ng-template #drawerItem let-item let-level=\"level\">\n @if (item.heading) {\n @if (isExpanded) {\n <div class=\"drawer-section-label\">{{ item.label }}</div>\n } @else {\n <div class=\"drawer-section-rule\"></div>\n }\n } @else {\n <div class=\"drawer-item-wrapper\">\n <!-- Item Button/Link -->\n @if (item.route && !item.children) {\n <a\n [routerLink]=\"item.route\"\n class=\"drawer-item\"\n [class.active]=\"isActiveRoute(item.route)\"\n [class.has-children]=\"item.children && item.children.length > 0\"\n [style.paddingLeft.px]=\"isExpanded ? 16 + level * 16 : 16\"\n [matTooltip]=\"!isExpanded ? item.label : ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n (click)=\"onItemClick(item, $event)\"\n >\n <div class=\"item-content\">\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"20\" class=\"item-icon\"></fx-ui-hero-icon>\n @if (isExpanded) {\n <span class=\"item-label\">{{ item.label }}</span>\n }\n </div>\n </a>\n }\n\n @if (!item.route || item.children) {\n <button\n type=\"button\"\n class=\"drawer-item\"\n [class.active]=\"isActiveRoute(item.route)\"\n [class.has-children]=\"item.children && item.children.length > 0\"\n [style.paddingLeft.px]=\"isExpanded ? 16 + level * 16 : 16\"\n [matTooltip]=\"!isExpanded ? item.label : ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-sm font-semibold\"\n (click)=\"onItemClick(item, $event)\"\n >\n <div class=\"item-content\">\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"20\" class=\"item-icon\"></fx-ui-hero-icon>\n @if (isExpanded) {\n <span class=\"item-label\">{{ item.label }}</span>\n }\n @if (isExpanded && item.children && item.children.length > 0) {\n <fx-ui-hero-icon\n [icon]=\"'chevron-down'\"\n [size]=\"16\"\n class=\"chevron-icon\"\n [class.rotated]=\"isAccordionExpanded(item.id)\"\n ></fx-ui-hero-icon>\n }\n </div>\n </button>\n }\n\n <!-- Accordion Children -->\n @if (item.children && item.children.length > 0) {\n <div\n class=\"accordion-children\"\n [@accordionExpand]=\"getAccordionState(item.id)\"\n >\n @for (child of item.children; track child) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: child, level: level + 1 }\"\n ></ng-container>\n }\n </div>\n }\n </div>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";@layer properties;:host{display:block;height:100%}.drawer-host-wrapper{position:relative;display:flex;height:100%;flex-direction:column;border-right-style:var(--tw-border-style);border-right-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--og-page-bg))}.drawer-brand{display:flex;height:60px;flex-shrink:0;align-items:center;gap:calc(var(--spacing, .25rem) * 3);overflow:hidden;text-decoration-line:none;padding-left:24px;padding-right:8px;transition:padding .3s cubic-bezier(.4,0,.2,1);max-width:260px}.drawer-brand.is-collapsed{justify-content:center;padding-left:0;padding-right:0;max-width:70px}.drawer-brand-mark{height:20px;width:auto;flex-shrink:0}.drawer-brand-copy{display:flex;min-width:0px;flex-direction:column;--tw-leading: var(--leading-tight, 1.25);line-height:var(--leading-tight, 1.25)}.drawer-brand-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:15px;--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:rgb(var(--text-primary));-webkit-user-select:none;user-select:none;font-family:var(--og-font-display);letter-spacing:-.01em}.drawer-brand-subtext{margin-top:1px;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;font-size:11px;color:rgb(var(--text-placeholder));-webkit-user-select:none;user-select:none;letter-spacing:var(--og-tracking-label)}.drawer-container{display:flex;min-height:0px;flex:1;flex-direction:column;overflow:hidden;padding-inline:calc(var(--spacing, .25rem) * 2);transition:width .3s cubic-bezier(.4,0,.2,1)}.toggle-btn{position:absolute;top:50%;z-index:50;display:flex;height:calc(var(--spacing, .25rem) * 6);width:calc(var(--spacing, .25rem) * 6);--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y);cursor:pointer;align-items:center;justify-content:center;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--bg-primary));--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(var(--shadow-color) / .05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);right:-12px;transition:all .2s ease}.toggle-btn:hover{background-color:rgb(var(--bg-primary-hover));--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.toggle-btn:active{transform:translateY(-50%) scale(.9)}.toggle-btn:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-offset:2px;outline-color:rgb(var(--primary))}.drawer-footer-nav{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default));padding-inline:0px;padding-top:calc(var(--spacing, .25rem) * 2);padding-bottom:var(--spacing, .25rem)}.drawer-nav{flex:1;overflow-x:hidden;overflow-y:auto;padding-block:calc(var(--spacing, .25rem) * 2)}.drawer-nav::-webkit-scrollbar{width:calc(var(--spacing, .25rem) * 1.5)}.drawer-nav::-webkit-scrollbar-track{background-color:transparent}.drawer-nav::-webkit-scrollbar-thumb{border-radius:var(--og-radius-control);background-color:rgb(var(--border-default))}.drawer-nav::-webkit-scrollbar-thumb:hover{background-color:rgb(var(--text-placeholder))}.drawer-section-label{padding-inline:1rem;padding-top:calc(var(--spacing, .25rem) * 4);padding-bottom:var(--spacing, .25rem);font-size:10.5px;line-height:1.4;--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:rgb(var(--og-text-faint));-webkit-user-select:none;user-select:none;letter-spacing:var(--og-tracking-label)}.drawer-section-rule{margin-inline:calc(var(--spacing, .25rem) * 3);margin-block:calc(var(--spacing, .25rem) * 3);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default))}.drawer-item-wrapper{position:relative}.drawer-item{position:relative;margin-top:var(--spacing, .25rem);display:flex;min-height:calc(var(--spacing, .25rem) * 8);width:100%;cursor:pointer;align-items:center;border-radius:var(--og-radius-control);--tw-border-style: none;border-style:none;background-color:transparent;padding-inline:1rem;padding-block:calc(var(--spacing, .25rem) * 2);color:rgb(var(--text-primary));text-decoration-line:none;transition:all .2s ease}.drawer-item:hover{--tw-scale-x: 95%;--tw-scale-y: 95%;--tw-scale-z: 95%;scale:var(--tw-scale-x) var(--tw-scale-y);color:rgb(var(--text-primary));background:rgb(var(--og-surface-raised)/.5)}.drawer-item.active{--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:rgb(var(--text-primary));background:rgb(var(--og-surface-raised)/.8);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:inset 0 0 0 1px rgb(var(--og-border)),0 1px 2px rgb(var(--shadow-color)/.05)}.drawer-item.active:hover{background:rgb(var(--og-surface-raised)/.95)}.drawer-item:active{transform:scale(.98)}.drawer-item:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-offset:2px;outline-color:rgb(var(--primary))}.drawer-item{-webkit-tap-highlight-color:transparent;--tw-outline-style: none;outline-style:none}.item-content{display:flex;width:100%;align-items:center;gap:calc(var(--spacing, .25rem) * 3);overflow:hidden}.item-icon{flex-shrink:0;color:inherit}.item-label{flex:1;overflow:hidden;text-align:left;font-size:13px;--tw-leading: calc(var(--spacing, .25rem) * 5);line-height:calc(var(--spacing, .25rem) * 5);text-overflow:ellipsis;white-space:nowrap}.chevron-icon{margin-left:auto;flex-shrink:0;color:rgb(var(--text-primary));transition:transform .3s cubic-bezier(.4,0,.2,1)}.chevron-icon.rotated{transform:rotate(180deg)}.accordion-children{overflow:hidden}.drawer-version{display:flex;align-items:center;justify-content:center;border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default));padding-inline:calc(var(--spacing, .25rem) * 4);padding-block:calc(var(--spacing, .25rem) * 3);font-size:11px;color:rgb(var(--text-placeholder));-webkit-user-select:none;user-select:none;font-family:var(--og-font-mono)}.drawer-version-dot{display:block;height:calc(var(--spacing, .25rem) * 1.5);width:calc(var(--spacing, .25rem) * 1.5);border-radius:calc(infinity * 1px);background-color:rgb(var(--text-placeholder))}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-leading{syntax: \"*\"; inherits: false;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-translate-x{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-y{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-z{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-outline-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-scale-x{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-scale-y{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-scale-z{syntax: \"*\"; inherits: false; initial-value: 1;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-leading: initial;--tw-font-weight: initial;--tw-translate-x: 0;--tw-translate-y: 0;--tw-translate-z: 0;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-outline-style: solid;--tw-scale-x: 1;--tw-scale-y: 1;--tw-scale-z: 1}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
3805
4021
  }], ctorParameters: () => [{ type: i2$1.Router }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], footerItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "footerItems", required: false }] }], isExpanded: [{
3806
4022
  type: Input
3807
4023
  }], logo: [{
@@ -3817,36 +4033,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
3817
4033
  /** Row-card skeleton placeholder for master-detail list panes. */
3818
4034
  class SkeletonListComponent {
3819
4035
  rows = input(5, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
3820
- range(n) {
3821
- return Array.from({ length: Number(n) || 0 }, (_, i) => i);
3822
- }
4036
+ /** computed, không phải method: method trả mảng MỚI mỗi lần render. */
4037
+ rowList = computed(() => range$5(this.rows()), ...(ngDevMode ? [{ debugName: "rowList" }] : /* istanbul ignore next */ []));
3823
4038
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3824
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SkeletonListComponent, isStandalone: true, selector: "fx-ui-skeleton-list", inputs: { rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"flex flex-col gap-normal animate-pulse\">\n @for (r of range(rows()); track r) {\n <div class=\"rounded-lg border border-border-default p-semi\">\n <div class=\"flex items-center gap-normal mb-small\">\n <div class=\"w-8 h-8 rounded-lg bg-bg-hover shrink-0\"></div>\n <div class=\"h-4 flex-1 rounded bg-bg-hover\"></div>\n <div class=\"h-5 w-12 rounded-full bg-bg-hover shrink-0\"></div>\n </div>\n <div class=\"h-3 w-3/4 rounded bg-bg-hover\"></div>\n </div>\n }\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
4039
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SkeletonListComponent, isStandalone: true, selector: "fx-ui-skeleton-list", inputs: { rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"flex flex-col gap-normal\">\n @for (r of rowList(); track r) {\n <div class=\"rounded-lg border border-border-default p-semi\">\n <div class=\"flex items-center gap-normal mb-small\">\n <div class=\"w-8 h-8 rounded-lg skeleton-load shrink-0\"></div>\n <div class=\"h-4 flex-1 rounded skeleton-load\"></div>\n <div class=\"h-5 w-12 rounded-full skeleton-load shrink-0\"></div>\n </div>\n <div class=\"h-3 w-3/4 rounded skeleton-load\"></div>\n </div>\n }\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
3825
4040
  }
3826
4041
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonListComponent, decorators: [{
3827
4042
  type: Component,
3828
- args: [{ selector: 'fx-ui-skeleton-list', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex flex-col gap-normal animate-pulse\">\n @for (r of range(rows()); track r) {\n <div class=\"rounded-lg border border-border-default p-semi\">\n <div class=\"flex items-center gap-normal mb-small\">\n <div class=\"w-8 h-8 rounded-lg bg-bg-hover shrink-0\"></div>\n <div class=\"h-4 flex-1 rounded bg-bg-hover\"></div>\n <div class=\"h-5 w-12 rounded-full bg-bg-hover shrink-0\"></div>\n </div>\n <div class=\"h-3 w-3/4 rounded bg-bg-hover\"></div>\n </div>\n }\n</div>\n" }]
4043
+ args: [{ selector: 'fx-ui-skeleton-list', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex flex-col gap-normal\">\n @for (r of rowList(); track r) {\n <div class=\"rounded-lg border border-border-default p-semi\">\n <div class=\"flex items-center gap-normal mb-small\">\n <div class=\"w-8 h-8 rounded-lg skeleton-load shrink-0\"></div>\n <div class=\"h-4 flex-1 rounded skeleton-load\"></div>\n <div class=\"h-5 w-12 rounded-full skeleton-load shrink-0\"></div>\n </div>\n <div class=\"h-3 w-3/4 rounded skeleton-load\"></div>\n </div>\n }\n</div>\n" }]
3829
4044
  }], propDecorators: { rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }] } });
4045
+ function range$5(n) {
4046
+ return Array.from({ length: Math.max(0, Number(n) || 0) }, (_, i) => i);
4047
+ }
3830
4048
 
4049
+ const ROW_WIDTHS = [90, 70, 80, 60];
3831
4050
  /** Card-shaped skeleton placeholder for detail panels. */
3832
4051
  class SkeletonDetailComponent {
3833
4052
  /** Number of content cards rendered below the header card. */
3834
4053
  cards = input(3, ...(ngDevMode ? [{ debugName: "cards" }] : /* istanbul ignore next */ []));
3835
4054
  /** Rows per content card. */
3836
4055
  rows = input(4, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
3837
- range(n) {
3838
- return Array.from({ length: Number(n) || 0 }, (_, i) => i);
3839
- }
4056
+ /** computed, không phải method: method trả mảng MỚI mỗi lần render. */
4057
+ cardList = computed(() => range$4(this.cards()), ...(ngDevMode ? [{ debugName: "cardList" }] : /* istanbul ignore next */ []));
4058
+ rowList = computed(() => range$4(this.rows()), ...(ngDevMode ? [{ debugName: "rowList" }] : /* istanbul ignore next */ []));
3840
4059
  rowWidth(i) {
3841
- return [90, 70, 80, 60][i % 4];
4060
+ return ROW_WIDTHS[i % ROW_WIDTHS.length];
3842
4061
  }
3843
4062
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3844
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SkeletonDetailComponent, isStandalone: true, selector: "fx-ui-skeleton-detail", inputs: { cards: { classPropertyName: "cards", publicName: "cards", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"flex flex-col gap-xl animate-pulse\">\n <div class=\"card-common\">\n <div class=\"flex items-start gap-normal\">\n <div class=\"w-10 h-10 rounded-lg bg-bg-hover shrink-0\"></div>\n <div class=\"flex-1 min-w-0 flex flex-col gap-normal\">\n <div class=\"h-5 w-1/2 rounded bg-bg-hover\"></div>\n <div class=\"h-3 w-3/4 rounded bg-bg-hover\"></div>\n </div>\n </div>\n </div>\n\n @for (c of range(cards()); track c) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"h-4 w-1/3 rounded bg-bg-hover\"></div>\n <div class=\"flex flex-col gap-normal\">\n @for (r of range(rows()); track r) {\n <div class=\"h-3 rounded bg-bg-hover\" [style.width.%]=\"rowWidth(r)\"></div>\n }\n </div>\n </div>\n }\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
4063
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SkeletonDetailComponent, isStandalone: true, selector: "fx-ui-skeleton-detail", inputs: { cards: { classPropertyName: "cards", publicName: "cards", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"flex flex-col gap-xl\">\n <div class=\"card-common\">\n <div class=\"flex items-start gap-normal\">\n <div class=\"w-10 h-10 rounded-lg skeleton-load shrink-0\"></div>\n <div class=\"flex-1 min-w-0 flex flex-col gap-normal\">\n <div class=\"h-5 w-1/2 rounded skeleton-load\"></div>\n <div class=\"h-3 w-3/4 rounded skeleton-load\"></div>\n </div>\n </div>\n </div>\n\n @for (c of cardList(); track c) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"h-4 w-1/3 rounded skeleton-load\"></div>\n <div class=\"flex flex-col gap-normal\">\n @for (r of rowList(); track r) {\n <div class=\"h-3 rounded skeleton-load\" [style.width.%]=\"rowWidth(r)\"></div>\n }\n </div>\n </div>\n }\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
3845
4064
  }
3846
4065
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonDetailComponent, decorators: [{
3847
4066
  type: Component,
3848
- args: [{ selector: 'fx-ui-skeleton-detail', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex flex-col gap-xl animate-pulse\">\n <div class=\"card-common\">\n <div class=\"flex items-start gap-normal\">\n <div class=\"w-10 h-10 rounded-lg bg-bg-hover shrink-0\"></div>\n <div class=\"flex-1 min-w-0 flex flex-col gap-normal\">\n <div class=\"h-5 w-1/2 rounded bg-bg-hover\"></div>\n <div class=\"h-3 w-3/4 rounded bg-bg-hover\"></div>\n </div>\n </div>\n </div>\n\n @for (c of range(cards()); track c) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"h-4 w-1/3 rounded bg-bg-hover\"></div>\n <div class=\"flex flex-col gap-normal\">\n @for (r of range(rows()); track r) {\n <div class=\"h-3 rounded bg-bg-hover\" [style.width.%]=\"rowWidth(r)\"></div>\n }\n </div>\n </div>\n }\n</div>\n" }]
4067
+ args: [{ selector: 'fx-ui-skeleton-detail', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex flex-col gap-xl\">\n <div class=\"card-common\">\n <div class=\"flex items-start gap-normal\">\n <div class=\"w-10 h-10 rounded-lg skeleton-load shrink-0\"></div>\n <div class=\"flex-1 min-w-0 flex flex-col gap-normal\">\n <div class=\"h-5 w-1/2 rounded skeleton-load\"></div>\n <div class=\"h-3 w-3/4 rounded skeleton-load\"></div>\n </div>\n </div>\n </div>\n\n @for (c of cardList(); track c) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"h-4 w-1/3 rounded skeleton-load\"></div>\n <div class=\"flex flex-col gap-normal\">\n @for (r of rowList(); track r) {\n <div class=\"h-3 rounded skeleton-load\" [style.width.%]=\"rowWidth(r)\"></div>\n }\n </div>\n </div>\n }\n</div>\n" }]
3849
4068
  }], propDecorators: { cards: [{ type: i0.Input, args: [{ isSignal: true, alias: "cards", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }] } });
4069
+ function range$4(n) {
4070
+ return Array.from({ length: Math.max(0, Number(n) || 0) }, (_, i) => i);
4071
+ }
3850
4072
 
3851
4073
  /**
3852
4074
  * V2 sparkbar — inline mini bar-trend (risk trend on project cards, deltas in
@@ -3898,7 +4120,8 @@ class StatCardsComponent {
3898
4120
  return card.tone ? toneText(card.tone) : 'text-text-primary';
3899
4121
  }
3900
4122
  boxClass(card) {
3901
- return card.tone ? toneBadge(card.tone) : 'bg-bg-hover text-text-secondary';
4123
+ // Ô icon vuông: chỉ lấy MÀU, không lấy hình pill của badge.
4124
+ return toneTint(card.tone ?? null);
3902
4125
  }
3903
4126
  deltaClass(card) {
3904
4127
  return card.delta?.tone ? toneText(card.delta.tone) : 'text-muted';
@@ -3957,18 +4180,24 @@ class FilterChipsComponent {
3957
4180
  }
3958
4181
  chipClass(option) {
3959
4182
  const active = (this.value ?? '') === option.value;
4183
+ // Chip "All" (value rỗng) khi ĐANG CHỌN dùng **accent** (xanh), không phải
4184
+ // nền đen — khớp demo (ref 11: pill "All 40" là accent). Nền đen chỉ dành
4185
+ // cho badge severity Critical (anh Tú chốt 03/09).
3960
4186
  if (!option.value) {
3961
4187
  return active
3962
- ? 'bg-text-primary text-text-focus'
3963
- : 'bg-bg-hover text-text-secondary hover:bg-gradient-primary-start/10 hover:text-gradient-primary-start';
4188
+ ? 'bg-accent text-accent-contrast'
4189
+ : 'bg-bg-hover text-text-secondary hover:bg-accent-soft hover:text-accent-fg';
3964
4190
  }
3965
4191
  const tone = option.value in this.toneOverrides
3966
4192
  ? this.toneOverrides[option.value]
3967
4193
  : severityTone(option.value);
3968
4194
  if (!tone) {
3969
- return active ? 'bg-text-primary text-text-focus' : `${toneBadge(null)} hover:opacity-50`;
4195
+ return active ? 'bg-accent text-accent-contrast' : `${toneScale(null)} hover:opacity-50`;
3970
4196
  }
3971
- return active ? `${toneBg(tone)} text-text-focus` : `${toneBadge(tone)} hover:opacity-50`;
4197
+ // Chip tự hình dạng riêng → chỉ lấy MÀU. Dùng thang đo (`tone-scale-*`)
4198
+ // chứ KHÔNG dùng `toneTint`: Critical ở filter phải là đỏ sẫm, không phải
4199
+ // pill đen đặc (anh Tú báo 03/09).
4200
+ return active ? `${toneBg(tone)} text-text-focus` : `${toneScale(tone)} hover:opacity-50`;
3972
4201
  }
3973
4202
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FilterChipsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3974
4203
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: FilterChipsComponent, isStandalone: true, selector: "fx-ui-filter-chips", inputs: { options: "options", value: "value", toneOverrides: "toneOverrides" }, 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 class=\"px-3 py-1 rounded-full txt-utility-caption font-medium transition-colors cursor-pointer\"\n [ngClass]=\"chipClass(option)\"\n (click)=\"select(option)\"\n >\n {{ option.label }}\n </button>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
@@ -4003,9 +4232,15 @@ class FilterPillsComponent {
4003
4232
  isActive(option) {
4004
4233
  return (this.value() ?? '') === option.value;
4005
4234
  }
4235
+ /**
4236
+ * Pill ĐANG CHỌN dùng **accent** (xanh), không phải nền đen — khớp demo:
4237
+ * design ref 11 vẽ pill "All 40" là pill accent. Nền đen `--og-text` chỉ
4238
+ * dành cho badge severity Critical (`.og-badge--solid`), không dùng cho
4239
+ * trạng thái chọn của filter (anh Tú chốt 03/09).
4240
+ */
4006
4241
  pillClass(option) {
4007
4242
  return this.isActive(option)
4008
- ? 'bg-text-primary text-text-inverse border-text-primary'
4243
+ ? 'bg-accent text-accent-contrast border-accent'
4009
4244
  : 'border-border-default text-text-secondary hover:border-border-strong';
4010
4245
  }
4011
4246
  countClass(option) {
@@ -4138,6 +4373,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4138
4373
  * - titleStyle 'heading' (default): icon + heading, the pre-V2 look
4139
4374
  * - titleStyle 'eyebrow': the V2 design's panel header — small tracked
4140
4375
  * caption on the left, muted [aside] text on the right
4376
+ *
4377
+ * The host is a flex column (not the custom element's inline default) so that
4378
+ * in a CSS grid row every card stretches to the row height and panels line up
4379
+ * (anh Tú 04/09); a footer projected with `mt-auto` sticks to the bottom.
4141
4380
  */
4142
4381
  class SectionCardComponent {
4143
4382
  /** Caller-translated heading. */
@@ -4153,11 +4392,11 @@ class SectionCardComponent {
4153
4392
  return iconTone ? toneText(iconTone) : 'text-text-secondary';
4154
4393
  }
4155
4394
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SectionCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4156
- 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\n class=\"txt-table-header uppercase\"\n style=\"letter-spacing: var(--og-tracking-label)\"\n >{{ title() }}</span\n >\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 });
4395
+ 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 });
4157
4396
  }
4158
4397
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SectionCardComponent, decorators: [{
4159
4398
  type: Component,
4160
- 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\n class=\"txt-table-header uppercase\"\n style=\"letter-spacing: var(--og-tracking-label)\"\n >{{ title() }}</span\n >\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" }]
4399
+ 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" }]
4161
4400
  }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], icon: [{
4162
4401
  type: Input
4163
4402
  }], 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 }] }] } });
@@ -4272,7 +4511,13 @@ class MasterDetailComponent {
4272
4511
  table;
4273
4512
  /** Fixed list-pane width on lg+ viewports (stacked full-width below). */
4274
4513
  listWidth = input('420px', ...(ngDevMode ? [{ debugName: "listWidth" }] : /* istanbul ignore next */ []));
4275
- maxListHeight = input('640px', ...(ngDevMode ? [{ debugName: "maxListHeight" }] : /* istanbul ignore next */ []));
4514
+ /**
4515
+ * Chiều cao CHUNG của cả ba cụm (facets / list / detail). Mặc định trừ phần
4516
+ * chrome phía trên (topbar + tab strip + context line) khỏi viewport để
4517
+ * bảng ba cột vừa đúng một màn, không kéo trang cuộn theo.
4518
+ * Chỉ có tác dụng từ 1080px — xem `.fx-md-row` trong CSS.
4519
+ */
4520
+ paneHeight = input('calc(100vh - 300px)', ...(ngDevMode ? [{ debugName: "paneHeight" }] : /* istanbul ignore next */ []));
4276
4521
  pageSizeOptions = input([5, 10, 20, 50], ...(ngDevMode ? [{ debugName: "pageSizeOptions" }] : /* istanbul ignore next */ []));
4277
4522
  /** Labels come in via inputs — the lib carries no i18n. */
4278
4523
  emptyMessage = input('No data', ...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
@@ -4349,7 +4594,7 @@ class MasterDetailComponent {
4349
4594
  el?.scrollIntoView({ block: 'nearest' });
4350
4595
  }
4351
4596
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: MasterDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4352
- 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 }, maxListHeight: { classPropertyName: "maxListHeight", publicName: "maxListHeight", 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: "<div class=\"flex flex-col lg:flex-row items-start gap-xl\" [style.--fx-md-list-width]=\"listWidth()\">\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 class=\"fx-md-facets card-common bg-surface\" [class.hidden]=\"!hasFacets\">\n <ng-content select=\"[mdFacets]\"></ng-content>\n </div>\n\n <!-- Master: list pane -->\n <div class=\"fx-md-list card-common\">\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\"\n [style.maxHeight]=\"maxListHeight()\"\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=\"w-full lg:flex-1 min-w-0\">\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-[300px]\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n </div>\n</div>\n", styles: [".fx-md-list{width:100%}@media(min-width:1080px){.fx-md-list{width:var(--fx-md-list-width, 420px);flex-shrink:0}}.fx-md-facets{width:100%}@media(min-width:1080px){.fx-md-facets{width:230px;flex-shrink:0}}.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"] }] });
4597
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: MasterDetailComponent, isStandalone: true, selector: "fx-ui-master-detail", inputs: { table: { classPropertyName: "table", publicName: "table", isSignal: false, isRequired: false, transformFunction: null }, listWidth: { classPropertyName: "listWidth", publicName: "listWidth", isSignal: true, isRequired: false, transformFunction: null }, paneHeight: { classPropertyName: "paneHeight", publicName: "paneHeight", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, listLabel: { classPropertyName: "listLabel", publicName: "listLabel", isSignal: true, isRequired: false, transformFunction: null }, skeletonCards: { classPropertyName: "skeletonCards", publicName: "skeletonCards", isSignal: true, isRequired: false, transformFunction: null }, paginationStyle: { classPropertyName: "paginationStyle", publicName: "paginationStyle", isSignal: true, isRequired: false, transformFunction: null }, prevLabel: { classPropertyName: "prevLabel", publicName: "prevLabel", isSignal: true, isRequired: false, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "rowTemplate", first: true, predicate: ["row"], descendants: true, isSignal: true }, { propertyName: "detailTemplate", first: true, predicate: ["detail"], descendants: true, isSignal: true }], viewQueries: [{ propertyName: "paginator", first: true, predicate: MatPaginator, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- 3 c\u1EE5m CHUNG chi\u1EC1u cao, m\u1ED7i c\u1EE5m SCROLL RI\u00CANG (anh T\u00FA ch\u1ED1t 03/09).\n `items-stretch` + chi\u1EC1u cao \u0111\u1EB7t tr\u00EAn h\u00E0ng (xem .fx-md-row trong CSS) \u0111\u1EC3\n c\u1EA3 ba pane b\u1EB1ng nhau; m\u1ED7i pane l\u00E0 flex-col + min-h-0 v\u00E0 ch\u1EC9 ph\u1EA7n th\u00E2n\n `overflow-auto flex-1` cu\u1ED9n \u2014 header/filter/paginator \u0111\u1EE9ng y\u00EAn.\n \u26A0\uFE0F H\u01B0\u1EDBng h\u00E0ng + chi\u1EC1u cao do CONTAINER QUERY trong CSS quy\u1EBFt \u0111\u1ECBnh (theo\n b\u1EC1 r\u1ED9ng c\u1EE7a ch\u00EDnh h\u00E0ng), KH\u00D4NG ph\u1EA3i `lg:flex-row`. \u0110\u1EEBng th\u00EAm l\u1EA1i class\n `lg:*` \u1EDF \u0111\u00E2y \u2014 `lg` trong lib l\u00E0 1080px v\u00E0 viewport l\u00E0 proxy sai; xem\n ghi ch\u00FA \u0111\u1EA7u master-detail.component.css. -->\n<div\n class=\"fx-md-row flex flex-col items-stretch gap-xl\"\n [style.--fx-md-list-width]=\"listWidth()\"\n [style.--fx-md-pane-h]=\"paneHeight()\"\n>\n <!-- Facets rail (V2 3-column lookup) \u2014 wrapper always rendered so ng-content\n stays unconditional; hidden when the host projects nothing. -->\n <div\n class=\"fx-md-facets card-common bg-surface flex flex-col min-h-0\"\n [class.hidden]=\"!hasFacets\"\n >\n <div class=\"flex-1 min-h-0 overflow-auto pr-xs\">\n <ng-content select=\"[mdFacets]\"></ng-content>\n </div>\n </div>\n\n <!-- Master: list pane -->\n <div class=\"fx-md-list card-common flex flex-col min-h-0\">\n <ng-content select=\"[mdHeader]\"></ng-content>\n <ng-content select=\"[mdFilters]\"></ng-content>\n <div\n class=\"flex flex-col gap-normal overflow-auto pr-xs flex-1 min-h-0\"\n role=\"listbox\"\n [attr.aria-label]=\"listLabel() || null\"\n >\n @if (table.loading) {\n <fx-ui-skeleton-list [rows]=\"table.pageSize\"></fx-ui-skeleton-list>\n } @else {\n @for (row of table.dataSource.data; track table.idOf(row); let i = $index) {\n <button\n type=\"button\"\n role=\"option\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"isSelected(row)\"\n [tabindex]=\"isSelected(row) ? 0 : -1\"\n (click)=\"table.selectItem(row)\"\n (keydown)=\"onKeydown($event, i)\"\n class=\"text-left rounded-lg border p-semi transition-colors\"\n [ngClass]=\"\n isSelected(row)\n ? 'border-accent bg-accent-soft/60'\n : 'border-border-default hover:border-border-interactive bg-bg-primary'\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"rowTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: row, selected: isSelected(row) }\"\n ></ng-container>\n </button>\n }\n @if (!table.dataSource.data.length) {\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n }\n }\n </div>\n <!-- No (page) binding: ngAfterViewInit hands this paginator to the table,\n whose own BaseTable subscription (running after this child's hook)\n wires page events \u2014 a binding here would double-fetch every change. -->\n <mat-paginator\n class=\"mt-normal\"\n [class.fx-md-paginator-hidden]=\"paginationStyle() === 'prevnext'\"\n [length]=\"table.total\"\n [pageSize]=\"table.pageSize\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n ></mat-paginator>\n @if (paginationStyle() === 'prevnext') {\n <div class=\"flex items-center justify-between mt-normal\">\n <span class=\"txt-utility-description\" style=\"font-family: var(--og-font-figure)\">\n {{ rangeText() }}\n </span>\n <div class=\"flex gap-small\">\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasPreviousPage()\"\n (click)=\"paginator().previousPage()\"\n >\n {{ prevLabel() }}\n </button>\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasNextPage()\"\n (click)=\"paginator().nextPage()\"\n >\n {{ nextLabel() }}\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Detail pane -->\n <div class=\"fx-md-detail w-full min-w-0 flex flex-col min-h-0\">\n <div class=\"flex-1 min-h-0 overflow-auto\">\n @if (table.selected) {\n @if (table.detailBusy) {\n <fx-ui-skeleton-detail [cards]=\"skeletonCards()\"></fx-ui-skeleton-detail>\n } @else {\n <ng-container\n [ngTemplateOutlet]=\"detailTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: table.detail, selected: table.selected }\"\n ></ng-container>\n }\n } @else {\n <div class=\"card-common flex items-center justify-center h-full min-h-[300px]\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n </div>\n </div>\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-md}.fx-md-list,.fx-md-facets{width:100%}@container fx-md (min-width: 968px){.fx-md-row{flex-direction:row;height:var(--fx-md-pane-h, calc(100vh - 300px) );min-height:420px}.fx-md-facets{flex:0 1 230px;min-width:200px;max-width:260px}.fx-md-list{width:auto;flex:0 1 var(--fx-md-list-width, 420px);min-width:320px;max-width:var(--fx-md-list-width, 420px)}.fx-md-detail{flex:1 1 400px;min-width:400px}}.fx-md-paginator-hidden{position:absolute!important;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}\n"], dependencies: [{ kind: "component", type: SkeletonListComponent, selector: "fx-ui-skeleton-list", inputs: ["rows"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: EmptyStateComponent, selector: "fx-ui-empty-state", inputs: ["message", "icon", "height"] }, { kind: "component", type: MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "component", type: SkeletonDetailComponent, selector: "fx-ui-skeleton-detail", inputs: ["cards", "rows"] }] });
4353
4598
  }
4354
4599
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: MasterDetailComponent, decorators: [{
4355
4600
  type: Component,
@@ -4360,10 +4605,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4360
4605
  EmptyStateComponent,
4361
4606
  MatPaginator,
4362
4607
  SkeletonDetailComponent,
4363
- ], template: "<div class=\"flex flex-col lg:flex-row items-start gap-xl\" [style.--fx-md-list-width]=\"listWidth()\">\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 class=\"fx-md-facets card-common bg-surface\" [class.hidden]=\"!hasFacets\">\n <ng-content select=\"[mdFacets]\"></ng-content>\n </div>\n\n <!-- Master: list pane -->\n <div class=\"fx-md-list card-common\">\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\"\n [style.maxHeight]=\"maxListHeight()\"\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=\"w-full lg:flex-1 min-w-0\">\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-[300px]\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n </div>\n</div>\n", styles: [".fx-md-list{width:100%}@media(min-width:1080px){.fx-md-list{width:var(--fx-md-list-width, 420px);flex-shrink:0}}.fx-md-facets{width:100%}@media(min-width:1080px){.fx-md-facets{width:230px;flex-shrink:0}}.fx-md-paginator-hidden{position:absolute!important;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}\n"] }]
4608
+ ], template: "<!-- 3 c\u1EE5m CHUNG chi\u1EC1u cao, m\u1ED7i c\u1EE5m SCROLL RI\u00CANG (anh T\u00FA ch\u1ED1t 03/09).\n `items-stretch` + chi\u1EC1u cao \u0111\u1EB7t tr\u00EAn h\u00E0ng (xem .fx-md-row trong CSS) \u0111\u1EC3\n c\u1EA3 ba pane b\u1EB1ng nhau; m\u1ED7i pane l\u00E0 flex-col + min-h-0 v\u00E0 ch\u1EC9 ph\u1EA7n th\u00E2n\n `overflow-auto flex-1` cu\u1ED9n \u2014 header/filter/paginator \u0111\u1EE9ng y\u00EAn.\n \u26A0\uFE0F H\u01B0\u1EDBng h\u00E0ng + chi\u1EC1u cao do CONTAINER QUERY trong CSS quy\u1EBFt \u0111\u1ECBnh (theo\n b\u1EC1 r\u1ED9ng c\u1EE7a ch\u00EDnh h\u00E0ng), KH\u00D4NG ph\u1EA3i `lg:flex-row`. \u0110\u1EEBng th\u00EAm l\u1EA1i class\n `lg:*` \u1EDF \u0111\u00E2y \u2014 `lg` trong lib l\u00E0 1080px v\u00E0 viewport l\u00E0 proxy sai; xem\n ghi ch\u00FA \u0111\u1EA7u master-detail.component.css. -->\n<div\n class=\"fx-md-row flex flex-col items-stretch gap-xl\"\n [style.--fx-md-list-width]=\"listWidth()\"\n [style.--fx-md-pane-h]=\"paneHeight()\"\n>\n <!-- Facets rail (V2 3-column lookup) \u2014 wrapper always rendered so ng-content\n stays unconditional; hidden when the host projects nothing. -->\n <div\n class=\"fx-md-facets card-common bg-surface flex flex-col min-h-0\"\n [class.hidden]=\"!hasFacets\"\n >\n <div class=\"flex-1 min-h-0 overflow-auto pr-xs\">\n <ng-content select=\"[mdFacets]\"></ng-content>\n </div>\n </div>\n\n <!-- Master: list pane -->\n <div class=\"fx-md-list card-common flex flex-col min-h-0\">\n <ng-content select=\"[mdHeader]\"></ng-content>\n <ng-content select=\"[mdFilters]\"></ng-content>\n <div\n class=\"flex flex-col gap-normal overflow-auto pr-xs flex-1 min-h-0\"\n role=\"listbox\"\n [attr.aria-label]=\"listLabel() || null\"\n >\n @if (table.loading) {\n <fx-ui-skeleton-list [rows]=\"table.pageSize\"></fx-ui-skeleton-list>\n } @else {\n @for (row of table.dataSource.data; track table.idOf(row); let i = $index) {\n <button\n type=\"button\"\n role=\"option\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"isSelected(row)\"\n [tabindex]=\"isSelected(row) ? 0 : -1\"\n (click)=\"table.selectItem(row)\"\n (keydown)=\"onKeydown($event, i)\"\n class=\"text-left rounded-lg border p-semi transition-colors\"\n [ngClass]=\"\n isSelected(row)\n ? 'border-accent bg-accent-soft/60'\n : 'border-border-default hover:border-border-interactive bg-bg-primary'\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"rowTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: row, selected: isSelected(row) }\"\n ></ng-container>\n </button>\n }\n @if (!table.dataSource.data.length) {\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n }\n }\n </div>\n <!-- No (page) binding: ngAfterViewInit hands this paginator to the table,\n whose own BaseTable subscription (running after this child's hook)\n wires page events \u2014 a binding here would double-fetch every change. -->\n <mat-paginator\n class=\"mt-normal\"\n [class.fx-md-paginator-hidden]=\"paginationStyle() === 'prevnext'\"\n [length]=\"table.total\"\n [pageSize]=\"table.pageSize\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n ></mat-paginator>\n @if (paginationStyle() === 'prevnext') {\n <div class=\"flex items-center justify-between mt-normal\">\n <span class=\"txt-utility-description\" style=\"font-family: var(--og-font-figure)\">\n {{ rangeText() }}\n </span>\n <div class=\"flex gap-small\">\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasPreviousPage()\"\n (click)=\"paginator().previousPage()\"\n >\n {{ prevLabel() }}\n </button>\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasNextPage()\"\n (click)=\"paginator().nextPage()\"\n >\n {{ nextLabel() }}\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Detail pane -->\n <div class=\"fx-md-detail w-full min-w-0 flex flex-col min-h-0\">\n <div class=\"flex-1 min-h-0 overflow-auto\">\n @if (table.selected) {\n @if (table.detailBusy) {\n <fx-ui-skeleton-detail [cards]=\"skeletonCards()\"></fx-ui-skeleton-detail>\n } @else {\n <ng-container\n [ngTemplateOutlet]=\"detailTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: table.detail, selected: table.selected }\"\n ></ng-container>\n }\n } @else {\n <div class=\"card-common flex items-center justify-center h-full min-h-[300px]\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n </div>\n </div>\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-md}.fx-md-list,.fx-md-facets{width:100%}@container fx-md (min-width: 968px){.fx-md-row{flex-direction:row;height:var(--fx-md-pane-h, calc(100vh - 300px) );min-height:420px}.fx-md-facets{flex:0 1 230px;min-width:200px;max-width:260px}.fx-md-list{width:auto;flex:0 1 var(--fx-md-list-width, 420px);min-width:320px;max-width:var(--fx-md-list-width, 420px)}.fx-md-detail{flex:1 1 400px;min-width:400px}}.fx-md-paginator-hidden{position:absolute!important;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}\n"] }]
4364
4609
  }], propDecorators: { table: [{
4365
4610
  type: Input
4366
- }], listWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "listWidth", required: false }] }], maxListHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxListHeight", 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 }] }] } });
4611
+ }], listWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "listWidth", required: false }] }], paneHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "paneHeight", required: false }] }], pageSizeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizeOptions", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], listLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "listLabel", required: false }] }], skeletonCards: [{ type: i0.Input, args: [{ isSignal: true, alias: "skeletonCards", required: false }] }], paginationStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginationStyle", required: false }] }], prevLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "prevLabel", required: false }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], rowTemplate: [{ type: i0.ContentChild, args: ['row', { isSignal: true }] }], detailTemplate: [{ type: i0.ContentChild, args: ['detail', { isSignal: true }] }], paginator: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MatPaginator), { isSignal: true }] }] } });
4367
4612
 
4368
4613
  /**
4369
4614
  * Row-action dropdown wrapper over mat-menu (behavior-layer rule, Playbook
@@ -4384,13 +4629,13 @@ class MenuComponent {
4384
4629
  switch (item.tone) {
4385
4630
  case 'critical':
4386
4631
  case 'danger':
4387
- return 'text-danger';
4632
+ return 'tone-fg-danger';
4388
4633
  case 'warning':
4389
- return 'text-warning';
4634
+ return 'tone-fg-warning';
4390
4635
  case 'success':
4391
- return 'text-success';
4636
+ return 'tone-fg-success';
4392
4637
  case 'information':
4393
- return 'text-information';
4638
+ return 'tone-fg-information';
4394
4639
  default:
4395
4640
  return 'text-text-primary';
4396
4641
  }
@@ -4444,6 +4689,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4444
4689
  * <fx-ui-pending-badge [field]="'confirmed-chains weekly series'" />
4445
4690
  *
4446
4691
  * Keep the copy factual: it states that data is missing, never fakes a value.
4692
+ * Styling is neutral on purpose (anh Tú 04/09): a hint, not a warning.
4447
4693
  */
4448
4694
  class PendingBadgeComponent {
4449
4695
  /** Caller-translated chip text. */
@@ -4459,11 +4705,11 @@ class PendingBadgeComponent {
4459
4705
  return this.field() ? `${this.label()} — ${this.field()}` : this.label();
4460
4706
  }
4461
4707
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: PendingBadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4462
- 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 });
4708
+ 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 });
4463
4709
  }
4464
4710
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: PendingBadgeComponent, decorators: [{
4465
4711
  type: Component,
4466
- 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" }]
4712
+ 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" }]
4467
4713
  }], 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 }] }] } });
4468
4714
 
4469
4715
  /**
@@ -4471,10 +4717,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4471
4717
  * reviewed before its endpoint exists (anh Tú 29/08: "muốn có full giao diện
4472
4718
  * trước để review").
4473
4719
  *
4474
- * The frame makes the status unmistakable dashed outline, a corner label and
4475
- * the pending marker naming the missing field so sample shapes can never be
4476
- * mistaken for measured values. Drop the wrapper (and the sample input) the
4477
- * moment the API lands.
4720
+ * The marker is deliberately quiet (anh 04/09: no yellow frame, just a
4721
+ * subtle "Pending API" chip): a neutral chip above the block, with the label
4722
+ * and missing field in its tooltip. The content itself is non-interactive so
4723
+ * sample shapes are never mistaken for live state. Drop the wrapper (and the
4724
+ * sample input) the moment the API lands.
4478
4725
  *
4479
4726
  * The pending chip is inlined rather than composed from fx-ui-pending-badge:
4480
4727
  * one lib component importing another inside the same barrel tripped NG0919
@@ -4485,21 +4732,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4485
4732
  * </fx-ui-sample-frame>
4486
4733
  */
4487
4734
  class SampleFrameComponent {
4488
- /** Caller-translated corner label. */
4735
+ /** Caller-translated block label — surfaces in the chip tooltip only. */
4489
4736
  label = input('Sample layout', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
4490
4737
  /** Field the backend still owes — shown on the pending badge tooltip. */
4491
4738
  field = input('', ...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
4492
4739
  /** Caller-translated pending-chip text. */
4493
4740
  pendingLabel = input('Pending API', ...(ngDevMode ? [{ debugName: "pendingLabel" }] : /* istanbul ignore next */ []));
4494
4741
  get tooltip() {
4495
- return this.field() ? `${this.pendingLabel()} ${this.field()}` : this.pendingLabel();
4742
+ const head = `${this.label()} · ${this.pendingLabel()}`;
4743
+ return this.field() ? `${head} — ${this.field()}` : head;
4496
4744
  }
4497
4745
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SampleFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4498
- 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 });
4746
+ 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 });
4499
4747
  }
4500
4748
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SampleFrameComponent, decorators: [{
4501
4749
  type: Component,
4502
- 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" }]
4750
+ 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" }]
4503
4751
  }], 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 }] }] } });
4504
4752
 
4505
4753
  /**
@@ -4597,12 +4845,53 @@ class SegmentedComponent {
4597
4845
  * (design refs 04/05).
4598
4846
  */
4599
4847
  variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
4848
+ /**
4849
+ * 'equal' (default): every segment takes the same width (view toggles such
4850
+ * as Catalog | Table). 'content': each segment is as wide as its label —
4851
+ * the demo's detail-pane tabs ("Affected software · Summary · Evidence"),
4852
+ * where equal columns would truncate the long label in a narrow pane.
4853
+ */
4854
+ fit = input('equal', ...(ngDevMode ? [{ debugName: "fit" }] : /* istanbul ignore next */ []));
4855
+ optionEls = viewChildren('opt', ...(ngDevMode ? [{ debugName: "optionEls" }] : /* istanbul ignore next */ []));
4856
+ /** Measured box of the active segment — only used when fit === 'content'. */
4857
+ thumbBox = signal(null, ...(ngDevMode ? [{ debugName: "thumbBox" }] : /* istanbul ignore next */ []));
4858
+ constructor() {
4859
+ // Content-fit segments have no shared width to derive the thumb from, so
4860
+ // measure the active button after each render that could move it.
4861
+ afterRenderEffect(() => {
4862
+ const index = this.activeIndex();
4863
+ this.options();
4864
+ const els = this.optionEls();
4865
+ if (this.fit() !== 'content' || index < 0 || !els[index]) {
4866
+ untracked(() => this.thumbBox.set(null));
4867
+ return;
4868
+ }
4869
+ const el = els[index].nativeElement;
4870
+ const box = { left: el.offsetLeft, width: el.offsetWidth };
4871
+ untracked(() => {
4872
+ const prev = this.thumbBox();
4873
+ if (!prev || prev.left !== box.left || prev.width !== box.width)
4874
+ this.thumbBox.set(box);
4875
+ });
4876
+ });
4877
+ }
4600
4878
  /** Index the sliding thumb sits on; -1 hides it (no/unknown selection). */
4601
4879
  activeIndex = computed(() => this.options().findIndex((o) => o.value === this.value()), ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
4602
4880
  /** Equal-width tracks → the thumb is 1/n of the inner width and slides by
4603
4881
  * multiples of its own width. Pure CSS transform, nothing to measure. */
4604
- thumbWidth = computed(() => `calc((100% - 6px) / ${this.options().length || 1})`, ...(ngDevMode ? [{ debugName: "thumbWidth" }] : /* istanbul ignore next */ []));
4605
- thumbTransform = computed(() => `translateX(${this.activeIndex() * 100}%)`, ...(ngDevMode ? [{ debugName: "thumbTransform" }] : /* istanbul ignore next */ []));
4882
+ thumbWidth = computed(() => {
4883
+ const box = this.thumbBox();
4884
+ if (this.fit() === 'content')
4885
+ return box ? `${box.width}px` : '0px';
4886
+ return `calc((100% - 6px) / ${this.options().length || 1})`;
4887
+ }, ...(ngDevMode ? [{ debugName: "thumbWidth" }] : /* istanbul ignore next */ []));
4888
+ thumbTransform = computed(() => {
4889
+ const box = this.thumbBox();
4890
+ if (this.fit() === 'content')
4891
+ return box ? `translateX(${box.left - 3}px)` : 'none';
4892
+ return `translateX(${this.activeIndex() * 100}%)`;
4893
+ }, ...(ngDevMode ? [{ debugName: "thumbTransform" }] : /* istanbul ignore next */ []));
4894
+ gridClass = computed(() => (this.fit() === 'content' ? 'auto-cols-auto' : 'auto-cols-fr'), ...(ngDevMode ? [{ debugName: "gridClass" }] : /* istanbul ignore next */ []));
4606
4895
  select(option) {
4607
4896
  if (option.disabled || option.value === this.value())
4608
4897
  return;
@@ -4620,12 +4909,12 @@ class SegmentedComponent {
4620
4909
  return this.variant() === 'pill' ? 'text-accent-contrast' : 'text-strong';
4621
4910
  }
4622
4911
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SegmentedComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4623
- 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 });
4912
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SegmentedComponent, isStandalone: true, selector: "fx-ui-segmented", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, fit: { classPropertyName: "fit", publicName: "fit", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, viewQueries: [{ propertyName: "optionEls", predicate: ["opt"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"relative inline-grid max-w-full grid-flow-col items-stretch p-[3px] bg-surface-sunken border border-border rounded-full\"\n [class]=\"gridClass()\"\n role=\"group\"\n [attr.aria-label]=\"ariaLabel()\"\n>\n <!-- Sliding thumb: one element that translates to the active option -->\n @if (activeIndex() >= 0) {\n <span\n aria-hidden=\"true\"\n class=\"fx-segmented__thumb absolute top-[3px] bottom-[3px] left-[3px] rounded-full pointer-events-none\"\n [class]=\"thumbClass()\"\n [style.width]=\"thumbWidth()\"\n [style.transform]=\"thumbTransform()\"\n ></span>\n }\n @for (option of options(); track option.value) {\n <button\n #opt\n type=\"button\"\n (click)=\"select(option)\"\n [disabled]=\"option.disabled\"\n [attr.aria-pressed]=\"option.value === value()\"\n class=\"relative z-[1] flex min-w-0 items-center justify-center gap-xs px-semi py-[5px] rounded-full text-[13px] leading-none font-medium whitespace-nowrap transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed\"\n [class]=\"optionClass(option)\"\n >\n @if (option.icon) {\n <fx-ui-hero-icon [icon]=\"option.icon\" [size]=\"14\" class=\"flex\" />\n }\n <!-- text-box trims Archivo's uneven leading \u2192 true optical centering\n (Chrome 128+/Safari 18.2+; older browsers ignore it harmlessly).\n truncate adds overflow:hidden, and the trimmed box ends at cap\n height / baseline \u2014 so the dot on \"i\" and the tail on \"y\" fall\n outside it and get clipped. py/-my pair gives both back without\n changing the control's height. -->\n <span class=\"min-w-0 truncate py-[3px] -my-[3px] [text-box:trim-both_cap_alphabetic]\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label px-small rounded-pill bg-neutral-soft text-neutral-soft-fg\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n</div>\n", styles: ["@charset \"UTF-8\";.fx-segmented__thumb{transition:transform .2s cubic-bezier(.2,.9,.2,1),width .2s cubic-bezier(.2,.9,.2,1);will-change:transform}@media(prefers-reduced-motion:reduce){.fx-segmented__thumb{transition:none}}\n"], dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4624
4913
  }
4625
4914
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SegmentedComponent, decorators: [{
4626
4915
  type: Component,
4627
- 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"] }]
4628
- }], 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 }] }] } });
4916
+ args: [{ selector: 'fx-ui-segmented', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "<div\n class=\"relative inline-grid max-w-full grid-flow-col items-stretch p-[3px] bg-surface-sunken border border-border rounded-full\"\n [class]=\"gridClass()\"\n role=\"group\"\n [attr.aria-label]=\"ariaLabel()\"\n>\n <!-- Sliding thumb: one element that translates to the active option -->\n @if (activeIndex() >= 0) {\n <span\n aria-hidden=\"true\"\n class=\"fx-segmented__thumb absolute top-[3px] bottom-[3px] left-[3px] rounded-full pointer-events-none\"\n [class]=\"thumbClass()\"\n [style.width]=\"thumbWidth()\"\n [style.transform]=\"thumbTransform()\"\n ></span>\n }\n @for (option of options(); track option.value) {\n <button\n #opt\n type=\"button\"\n (click)=\"select(option)\"\n [disabled]=\"option.disabled\"\n [attr.aria-pressed]=\"option.value === value()\"\n class=\"relative z-[1] flex min-w-0 items-center justify-center gap-xs px-semi py-[5px] rounded-full text-[13px] leading-none font-medium whitespace-nowrap transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed\"\n [class]=\"optionClass(option)\"\n >\n @if (option.icon) {\n <fx-ui-hero-icon [icon]=\"option.icon\" [size]=\"14\" class=\"flex\" />\n }\n <!-- text-box trims Archivo's uneven leading \u2192 true optical centering\n (Chrome 128+/Safari 18.2+; older browsers ignore it harmlessly).\n truncate adds overflow:hidden, and the trimmed box ends at cap\n height / baseline \u2014 so the dot on \"i\" and the tail on \"y\" fall\n outside it and get clipped. py/-my pair gives both back without\n changing the control's height. -->\n <span class=\"min-w-0 truncate py-[3px] -my-[3px] [text-box:trim-both_cap_alphabetic]\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label px-small rounded-pill bg-neutral-soft text-neutral-soft-fg\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n</div>\n", styles: ["@charset \"UTF-8\";.fx-segmented__thumb{transition:transform .2s cubic-bezier(.2,.9,.2,1),width .2s cubic-bezier(.2,.9,.2,1);will-change:transform}@media(prefers-reduced-motion:reduce){.fx-segmented__thumb{transition:none}}\n"] }]
4917
+ }], 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 }] }] } });
4629
4918
 
4630
4919
  /**
4631
4920
  * V2 ListRow — presentational body for master-detail #row templates and other
@@ -4729,20 +5018,20 @@ class FacetsRailComponent {
4729
5018
  return option.tone ? toneText(option.tone) : '';
4730
5019
  }
4731
5020
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FacetsRailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4732
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: FacetsRailComponent, isStandalone: true, selector: "fx-ui-facets-rail", inputs: { groups: { classPropertyName: "groups", publicName: "groups", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, resetLabel: { classPropertyName: "resetLabel", publicName: "resetLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange" }, ngImport: i0, template: "<div class=\"flex flex-col gap-large\">\n @if (hasSelection()) {\n <button type=\"button\" class=\"txt-link self-end\" (click)=\"reset()\">\n {{ resetLabel() }}\n </button>\n }\n @for (group of groups(); track group.id) {\n <div role=\"group\" [attr.aria-label]=\"group.label\">\n <div\n class=\"txt-table-header uppercase mb-small\"\n [style.letter-spacing]=\"'var(--og-tracking-label)'\"\n >\n {{ group.label }}\n </div>\n <div class=\"flex flex-col gap-xs\">\n @for (option of group.options; track option.value) {\n <button\n type=\"button\"\n (click)=\"toggle(group, option)\"\n [attr.aria-pressed]=\"isSelected(group, option)\"\n class=\"flex items-center gap-normal px-normal py-small rounded-control text-left transition-colors\"\n [ngClass]=\"\n isSelected(group, option)\n ? 'bg-accent-soft text-accent-soft-fg font-medium'\n : 'text-body hover:bg-surface-sunken'\n \"\n >\n @if (option.tone) {\n <fx-ui-status-dot [tone]=\"option.tone\" [size]=\"7\" />\n }\n <span class=\"text-sm truncate flex-1\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label text-faint\" style=\"font-family: var(--og-font-figure)\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n </div>\n </div>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: StatusDotComponent, selector: "fx-ui-status-dot", inputs: ["tone", "size", "pulse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5021
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: FacetsRailComponent, isStandalone: true, selector: "fx-ui-facets-rail", inputs: { groups: { classPropertyName: "groups", publicName: "groups", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, resetLabel: { classPropertyName: "resetLabel", publicName: "resetLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange" }, ngImport: i0, template: "<div class=\"flex flex-col gap-large\">\n @if (hasSelection()) {\n <button type=\"button\" class=\"txt-link self-end\" (click)=\"reset()\">\n {{ resetLabel() }}\n </button>\n }\n @for (group of groups(); track group.id) {\n <div role=\"group\" [attr.aria-label]=\"group.label\">\n <div class=\"txt-eyebrow mb-small\">{{ group.label }}</div>\n <div class=\"flex flex-col gap-xs\">\n @for (option of group.options; track option.value) {\n <button\n type=\"button\"\n (click)=\"toggle(group, option)\"\n [attr.aria-pressed]=\"isSelected(group, option)\"\n class=\"flex items-center gap-normal px-normal py-small rounded-control text-left transition-colors\"\n [ngClass]=\"\n isSelected(group, option)\n ? 'bg-accent-soft text-accent-soft-fg font-medium'\n : 'text-body hover:bg-surface-sunken'\n \"\n >\n @if (option.tone) {\n <fx-ui-status-dot [tone]=\"option.tone\" [size]=\"7\" />\n }\n <span class=\"text-sm truncate flex-1\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label text-faint\" style=\"font-family: var(--og-font-figure)\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n </div>\n </div>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: StatusDotComponent, selector: "fx-ui-status-dot", inputs: ["tone", "size", "pulse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4733
5022
  }
4734
5023
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FacetsRailComponent, decorators: [{
4735
5024
  type: Component,
4736
- args: [{ selector: 'fx-ui-facets-rail', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, StatusDotComponent], template: "<div class=\"flex flex-col gap-large\">\n @if (hasSelection()) {\n <button type=\"button\" class=\"txt-link self-end\" (click)=\"reset()\">\n {{ resetLabel() }}\n </button>\n }\n @for (group of groups(); track group.id) {\n <div role=\"group\" [attr.aria-label]=\"group.label\">\n <div\n class=\"txt-table-header uppercase mb-small\"\n [style.letter-spacing]=\"'var(--og-tracking-label)'\"\n >\n {{ group.label }}\n </div>\n <div class=\"flex flex-col gap-xs\">\n @for (option of group.options; track option.value) {\n <button\n type=\"button\"\n (click)=\"toggle(group, option)\"\n [attr.aria-pressed]=\"isSelected(group, option)\"\n class=\"flex items-center gap-normal px-normal py-small rounded-control text-left transition-colors\"\n [ngClass]=\"\n isSelected(group, option)\n ? 'bg-accent-soft text-accent-soft-fg font-medium'\n : 'text-body hover:bg-surface-sunken'\n \"\n >\n @if (option.tone) {\n <fx-ui-status-dot [tone]=\"option.tone\" [size]=\"7\" />\n }\n <span class=\"text-sm truncate flex-1\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label text-faint\" style=\"font-family: var(--og-font-figure)\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n </div>\n </div>\n }\n</div>\n" }]
5025
+ args: [{ selector: 'fx-ui-facets-rail', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, StatusDotComponent], template: "<div class=\"flex flex-col gap-large\">\n @if (hasSelection()) {\n <button type=\"button\" class=\"txt-link self-end\" (click)=\"reset()\">\n {{ resetLabel() }}\n </button>\n }\n @for (group of groups(); track group.id) {\n <div role=\"group\" [attr.aria-label]=\"group.label\">\n <div class=\"txt-eyebrow mb-small\">{{ group.label }}</div>\n <div class=\"flex flex-col gap-xs\">\n @for (option of group.options; track option.value) {\n <button\n type=\"button\"\n (click)=\"toggle(group, option)\"\n [attr.aria-pressed]=\"isSelected(group, option)\"\n class=\"flex items-center gap-normal px-normal py-small rounded-control text-left transition-colors\"\n [ngClass]=\"\n isSelected(group, option)\n ? 'bg-accent-soft text-accent-soft-fg font-medium'\n : 'text-body hover:bg-surface-sunken'\n \"\n >\n @if (option.tone) {\n <fx-ui-status-dot [tone]=\"option.tone\" [size]=\"7\" />\n }\n <span class=\"text-sm truncate flex-1\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label text-faint\" style=\"font-family: var(--og-font-figure)\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n </div>\n </div>\n }\n</div>\n" }]
4737
5026
  }], propDecorators: { groups: [{ type: i0.Input, args: [{ isSignal: true, alias: "groups", required: false }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }], resetLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "resetLabel", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }] } });
4738
5027
 
4739
5028
  /** Trend blocks are softened solids — whole literals (Tailwind v4 has no safelist). */
4740
5029
  const TREND_BLOCK = {
4741
- critical: 'bg-critical/60',
4742
- danger: 'bg-danger/60',
4743
- warning: 'bg-warning/60',
4744
- success: 'bg-success/60',
4745
- information: 'bg-information/60',
5030
+ critical: 'tone-dot-chart-critical',
5031
+ danger: 'tone-dot-chart-danger',
5032
+ warning: 'tone-dot-chart-warning',
5033
+ success: 'tone-dot-chart-success',
5034
+ information: 'tone-dot-chart-information',
4746
5035
  };
4747
5036
  /**
4748
5037
  * V2 project card — the Project Management catalog tile (design 12): initials
@@ -4764,11 +5053,11 @@ class ProjectCardComponent {
4764
5053
  return tone ? TREND_BLOCK[tone] : 'bg-bg-hover';
4765
5054
  }
4766
5055
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ProjectCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4767
- 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 });
5056
+ 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 });
4768
5057
  }
4769
5058
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ProjectCardComponent, decorators: [{
4770
5059
  type: Component,
4771
- 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" }]
5060
+ 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" }]
4772
5061
  }], propDecorators: { card: [{ type: i0.Input, args: [{ isSignal: true, alias: "card", required: true }] }], open: [{ type: i0.Output, args: ["open"] }] } });
4773
5062
 
4774
5063
  /**
@@ -4795,7 +5084,7 @@ class PipelineStepperComponent {
4795
5084
  case 'active':
4796
5085
  return 'bg-accent-soft text-accent-soft-fg border-accent';
4797
5086
  case 'error':
4798
- return 'bg-critical/15 text-critical border-critical';
5087
+ return 'tone-scale-danger tone-border-danger';
4799
5088
  default:
4800
5089
  return 'bg-surface text-faint border-border';
4801
5090
  }
@@ -4807,7 +5096,7 @@ class PipelineStepperComponent {
4807
5096
  case 'done':
4808
5097
  return 'text-body';
4809
5098
  case 'error':
4810
- return 'text-critical';
5099
+ return 'tone-fg-danger';
4811
5100
  default:
4812
5101
  return 'text-faint';
4813
5102
  }
@@ -4830,6 +5119,174 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4830
5119
  args: [{ selector: 'fx-ui-pipeline-stepper', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, HeroIconComponent], template: "<ol class=\"flex items-start overflow-x-auto pb-small\" role=\"list\">\n @for (stage of stages(); track stage.id; let i = $index; let last = $last) {\n <li class=\"flex items-start\" [class.flex-1]=\"!last\" [attr.aria-current]=\"stateOf(stage) === 'active' ? 'step' : null\">\n <button\n type=\"button\"\n class=\"flex flex-col items-center gap-xs min-w-20 px-xs\"\n [class.cursor-pointer]=\"canClick(stage)\"\n [class.cursor-default]=\"!canClick(stage)\"\n [tabindex]=\"canClick(stage) ? 0 : -1\"\n (click)=\"onStageClick(stage, i)\"\n >\n <span\n class=\"w-8 h-8 rounded-full border-2 flex items-center justify-center text-sm font-semibold transition-colors\"\n [ngClass]=\"circleClass(stage)\"\n >\n @switch (stateOf(stage)) {\n @case ('done') {\n <fx-ui-hero-icon icon=\"check\" [size]=\"16\" class=\"flex\" />\n }\n @case ('error') {\n <fx-ui-hero-icon icon=\"exclamation-triangle\" [size]=\"16\" class=\"flex\" />\n }\n @default {\n {{ i + 1 }}\n }\n }\n </span>\n <span class=\"text-xs text-center whitespace-nowrap\" [ngClass]=\"labelClass(stage)\">\n {{ stage.label }}\n </span>\n @if (stage.description) {\n <span class=\"txt-utility-description text-center whitespace-nowrap\">\n {{ stage.description }}\n </span>\n }\n </button>\n @if (!last) {\n <div class=\"flex-1 h-[2px] mt-4 mx-xs rounded-pill min-w-6\" [ngClass]=\"connectorClass(stage)\"></div>\n }\n </li>\n }\n</ol>\n<div class=\"mt-large\">\n <ng-content></ng-content>\n</div>\n" }]
4831
5120
  }], propDecorators: { stages: [{ type: i0.Input, args: [{ isSignal: true, alias: "stages", required: false }] }], clickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clickable", required: false }] }], stageClick: [{ type: i0.Output, args: ["stageClick"] }] } });
4832
5121
 
5122
+ /** Facets-rail skeleton: N nhóm, mỗi nhóm một eyebrow + vài dòng lựa chọn. */
5123
+ class SkeletonFacetsComponent {
5124
+ groups = input(3, ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
5125
+ rows = input(4, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
5126
+ // computed, không phải method — xem [[skeleton-ng0956-array-holes]].
5127
+ groupList = computed(() => range$3(this.groups()), ...(ngDevMode ? [{ debugName: "groupList" }] : /* istanbul ignore next */ []));
5128
+ rowList = computed(() => range$3(this.rows()), ...(ngDevMode ? [{ debugName: "rowList" }] : /* istanbul ignore next */ []));
5129
+ rowWidth(i) {
5130
+ return [80, 65, 72, 58][i % 4];
5131
+ }
5132
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonFacetsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5133
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SkeletonFacetsComponent, isStandalone: true, selector: "fx-ui-skeleton-facets", inputs: { groups: { classPropertyName: "groups", publicName: "groups", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"flex flex-col gap-xl\">\n @for (g of groupList(); track g) {\n <div class=\"flex flex-col gap-normal\">\n <div class=\"h-3 w-20 rounded skeleton-load\"></div>\n <div class=\"flex flex-col gap-small\">\n @for (r of rowList(); track r) {\n <div class=\"flex items-center gap-normal\">\n <div class=\"w-3.5 h-3.5 rounded skeleton-load shrink-0\"></div>\n <div class=\"h-3 rounded skeleton-load\" [style.width.%]=\"rowWidth(r)\"></div>\n </div>\n }\n </div>\n </div>\n }\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
5134
+ }
5135
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonFacetsComponent, decorators: [{
5136
+ type: Component,
5137
+ args: [{ selector: 'fx-ui-skeleton-facets', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex flex-col gap-xl\">\n @for (g of groupList(); track g) {\n <div class=\"flex flex-col gap-normal\">\n <div class=\"h-3 w-20 rounded skeleton-load\"></div>\n <div class=\"flex flex-col gap-small\">\n @for (r of rowList(); track r) {\n <div class=\"flex items-center gap-normal\">\n <div class=\"w-3.5 h-3.5 rounded skeleton-load shrink-0\"></div>\n <div class=\"h-3 rounded skeleton-load\" [style.width.%]=\"rowWidth(r)\"></div>\n </div>\n }\n </div>\n </div>\n }\n</div>\n" }]
5138
+ }], propDecorators: { groups: [{ type: i0.Input, args: [{ isSignal: true, alias: "groups", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }] } });
5139
+ function range$3(n) {
5140
+ return Array.from({ length: Math.max(0, Number(n) || 0) }, (_, i) => i);
5141
+ }
5142
+
5143
+ /** Dashboard skeleton: một hàng KPI + N panel có vùng chart. */
5144
+ class SkeletonDashboardComponent {
5145
+ kpis = input(4, ...(ngDevMode ? [{ debugName: "kpis" }] : /* istanbul ignore next */ []));
5146
+ panels = input(2, ...(ngDevMode ? [{ debugName: "panels" }] : /* istanbul ignore next */ []));
5147
+ kpiList = computed(() => range$2(this.kpis()), ...(ngDevMode ? [{ debugName: "kpiList" }] : /* istanbul ignore next */ []));
5148
+ panelList = computed(() => range$2(this.panels()), ...(ngDevMode ? [{ debugName: "panelList" }] : /* istanbul ignore next */ []));
5149
+ barList = computed(() => range$2(7), ...(ngDevMode ? [{ debugName: "barList" }] : /* istanbul ignore next */ []));
5150
+ /** Chiều cao cột giả — dựng hình chart mà không cần dữ liệu. */
5151
+ barHeight(i) {
5152
+ return [45, 70, 35, 85, 55, 75, 40][i % 7];
5153
+ }
5154
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonDashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5155
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SkeletonDashboardComponent, isStandalone: true, selector: "fx-ui-skeleton-dashboard", inputs: { kpis: { classPropertyName: "kpis", publicName: "kpis", isSignal: true, isRequired: false, transformFunction: null }, panels: { classPropertyName: "panels", publicName: "panels", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"flex flex-col gap-xl\">\n <div class=\"grid gap-xl grid-cols-2 md:grid-cols-4\">\n @for (k of kpiList(); track k) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"h-3 w-16 rounded skeleton-load\"></div>\n <div class=\"h-7 w-24 rounded skeleton-load\"></div>\n <div class=\"h-3 w-20 rounded skeleton-load\"></div>\n </div>\n }\n </div>\n\n <div class=\"grid gap-xl grid-cols-1 md:grid-cols-2\">\n @for (p of panelList(); track p) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"h-4 w-1/3 rounded skeleton-load\"></div>\n <div class=\"flex items-end gap-normal h-40\">\n @for (b of barList(); track b) {\n <div class=\"flex-1 rounded-t skeleton-load\" [style.height.%]=\"barHeight(b)\"></div>\n }\n </div>\n </div>\n }\n </div>\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
5156
+ }
5157
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonDashboardComponent, decorators: [{
5158
+ type: Component,
5159
+ args: [{ selector: 'fx-ui-skeleton-dashboard', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex flex-col gap-xl\">\n <div class=\"grid gap-xl grid-cols-2 md:grid-cols-4\">\n @for (k of kpiList(); track k) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"h-3 w-16 rounded skeleton-load\"></div>\n <div class=\"h-7 w-24 rounded skeleton-load\"></div>\n <div class=\"h-3 w-20 rounded skeleton-load\"></div>\n </div>\n }\n </div>\n\n <div class=\"grid gap-xl grid-cols-1 md:grid-cols-2\">\n @for (p of panelList(); track p) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"h-4 w-1/3 rounded skeleton-load\"></div>\n <div class=\"flex items-end gap-normal h-40\">\n @for (b of barList(); track b) {\n <div class=\"flex-1 rounded-t skeleton-load\" [style.height.%]=\"barHeight(b)\"></div>\n }\n </div>\n </div>\n }\n </div>\n</div>\n" }]
5160
+ }], propDecorators: { kpis: [{ type: i0.Input, args: [{ isSignal: true, alias: "kpis", required: false }] }], panels: [{ type: i0.Input, args: [{ isSignal: true, alias: "panels", required: false }] }] } });
5161
+ function range$2(n) {
5162
+ return Array.from({ length: Math.max(0, Number(n) || 0) }, (_, i) => i);
5163
+ }
5164
+
5165
+ const PRESETS = {
5166
+ table: ['heading', 'toolbar', 'table'],
5167
+ panels: [
5168
+ 'heading',
5169
+ 'stat-cards',
5170
+ { kind: 'panels', count: 3, cols: 3 },
5171
+ 'toolbar',
5172
+ { kind: 'cards', count: 6, cols: 3 },
5173
+ ],
5174
+ dashboard: [
5175
+ 'heading',
5176
+ 'kpi-strip',
5177
+ { kind: 'panels', count: 2 },
5178
+ { kind: 'panels', count: 2 },
5179
+ 'card',
5180
+ ],
5181
+ 'tabs-facets': ['tabs', 'facets'],
5182
+ facets: ['facets'],
5183
+ detail: ['detail'],
5184
+ form: ['heading', 'form'],
5185
+ };
5186
+ /**
5187
+ * Skeleton cho vùng NỘI DUNG trong lúc resolver của route đang chạy.
5188
+ *
5189
+ * ⚠️ Vì sao skeleton phải nằm ở shell chứ không ở từng màn: Angular **không
5190
+ * tạo component cho tới khi resolver xong**, nên một màn không thể tự vẽ
5191
+ * skeleton của chính nó trong lúc resolver của nó chạy. Thứ vẽ được phải tồn
5192
+ * tại TRƯỚC component đó → shell + router event. Xem `FxRouteSkeletonService`.
5193
+ */
5194
+ class RouteSkeletonComponent {
5195
+ /** Tên preset, hoặc mảng block mô tả đúng layout của màn. */
5196
+ hint = input('table', ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
5197
+ blocks = computed(() => {
5198
+ const value = this.hint();
5199
+ // Mảng rỗng / tên preset lạ / null đều rơi về preset bảng: mục tiêu của cả
5200
+ // cơ chế này là KHÔNG BAO GIỜ để vùng nội dung trắng, nên vẽ hình generic
5201
+ // vẫn hơn không vẽ gì.
5202
+ const list = Array.isArray(value) && value.length
5203
+ ? value
5204
+ : (PRESETS[value] ?? PRESETS.table);
5205
+ return list.map((b) => (typeof b === 'string' ? { kind: b } : b));
5206
+ }, ...(ngDevMode ? [{ debugName: "blocks" }] : /* istanbul ignore next */ []));
5207
+ cols(b) {
5208
+ return range$1(b.columns ?? 6);
5209
+ }
5210
+ rows(b) {
5211
+ return range$1(b.rows ?? 8);
5212
+ }
5213
+ items(b, fallback) {
5214
+ return range$1(b.count ?? fallback);
5215
+ }
5216
+ /** Lớp lưới cho `panels`/`cards` — Tailwind v4 không có safelist nên phải
5217
+ * là chuỗi tĩnh, không ghép động. */
5218
+ gridClass(b) {
5219
+ switch (b.cols ?? 2) {
5220
+ case 1:
5221
+ return 'grid-cols-1';
5222
+ case 3:
5223
+ return 'grid-cols-1 md:grid-cols-3';
5224
+ case 4:
5225
+ return 'grid-cols-2 md:grid-cols-4';
5226
+ default:
5227
+ return 'grid-cols-1 md:grid-cols-2';
5228
+ }
5229
+ }
5230
+ /** Header ngắn hơn ô dữ liệu, cột đầu rộng nhất (thường là tên). */
5231
+ headWidth(c) {
5232
+ return c === 0 ? 55 : [40, 34, 46, 30, 38][c % 5];
5233
+ }
5234
+ /**
5235
+ * Bề rộng đổi theo cả hàng và cột — cùng bề rộng thì khối skeleton trông
5236
+ * như một hình chữ nhật đặc, mất cảm giác "từng field".
5237
+ */
5238
+ cellWidth(r, c) {
5239
+ if (c === 0)
5240
+ return [92, 78, 85, 70][r % 4];
5241
+ return [62, 48, 74, 55, 66, 42][(r + c) % 6];
5242
+ }
5243
+ barHeight(i) {
5244
+ return [45, 70, 35, 85, 55, 75, 40][i % 7];
5245
+ }
5246
+ bars = range$1(7);
5247
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: RouteSkeletonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5248
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: RouteSkeletonComponent, isStandalone: true, selector: "fx-ui-route-skeleton", inputs: { hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"w-full flex flex-col gap-xl\">\n @for (b of blocks(); track $index) {\n @switch (b.kind) {\n @case ('heading') {\n <div data-fx-skel=\"heading\" class=\"flex flex-col md:flex-row md:items-start justify-between gap-normal\">\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-7 w-64\"></div>\n <div class=\"skeleton-load h-3 w-32\"></div>\n </div>\n @if (items(b, 2).length) {\n <div class=\"flex items-center gap-normal\">\n @for (a of items(b, 2); track a) {\n <div class=\"skeleton-load h-9 rounded-lg\" [class]=\"a === 0 ? 'w-28' : 'w-32'\"></div>\n }\n </div>\n }\n </div>\n }\n\n @case ('toolbar') {\n <div data-fx-skel=\"toolbar\" class=\"flex flex-wrap items-center gap-normal\">\n <div class=\"skeleton-load h-9 w-72 rounded-lg\"></div>\n @for (f of items(b, 2); track f) {\n <div class=\"skeleton-load h-7 rounded-full\" [class]=\"f % 2 ? 'w-24' : 'w-20'\"></div>\n }\n </div>\n }\n\n @case ('table') {\n <!-- B\u1EA3ng: m\u1ED7i \u00D4 l\u00E0 m\u1ED9t `.skeleton-load` ri\u00EAng, \u0111\u00FAng c\u00E1ch\n `fx-ui-table-cell [loading]` l\u00E0m tr\u00EAn b\u1EA3ng th\u1EADt. -->\n <div data-fx-skel=\"table\" class=\"card-common overflow-x-auto\">\n <table class=\"w-full\">\n <thead>\n <tr class=\"border-b border-border-default\">\n @for (c of cols(b); track c) {\n <th class=\"px-normal py-semi text-left\">\n <div class=\"skeleton-load h-3\" [style.width.%]=\"headWidth(c)\"></div>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (r of rows(b); track r) {\n <tr class=\"border-b border-border-subtle\">\n @for (c of cols(b); track c) {\n <td class=\"px-normal py-semi align-middle\">\n <div class=\"skeleton-load h-4\" [style.width.%]=\"cellWidth(r, c)\"></div>\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n @case ('stat-cards') {\n <div data-fx-skel=\"stat-cards\" class=\"grid gap-normal grid-cols-2 md:grid-cols-4\">\n @for (i of items(b, 4); track i) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"skeleton-load h-3 w-16\"></div>\n <div class=\"skeleton-load h-8 w-24\"></div>\n <div class=\"skeleton-load h-3 w-20\"></div>\n </div>\n }\n </div>\n }\n\n @case ('kpi-strip') {\n <!-- M\u1ED8T card chia c\u1ED9t (ki\u1EC3u app-dash-kpi-row), kh\u00F4ng ph\u1EA3i c\u00E1c tile r\u1EDDi -->\n <div data-fx-skel=\"kpi-strip\" class=\"card-common !p-0\">\n <div class=\"grid grid-cols-2 md:grid-cols-4\">\n @for (i of items(b, 4); track i) {\n <div\n class=\"flex flex-col gap-normal px-xl py-large border-border-subtle\"\n [class.border-l]=\"!$first\"\n >\n <div class=\"skeleton-load h-3 w-20\"></div>\n <div class=\"skeleton-load h-9 w-28\"></div>\n <div class=\"skeleton-load h-3 w-24\"></div>\n </div>\n }\n </div>\n </div>\n }\n\n @case ('panels') {\n <div data-fx-skel=\"panels\" class=\"grid gap-xl\" [class]=\"gridClass(b)\">\n @for (i of items(b, 2); track i) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"skeleton-load h-4 w-1/3\"></div>\n <div class=\"flex items-end gap-normal h-40\">\n @for (x of bars; track x) {\n <div class=\"skeleton-load flex-1 rounded-t\" [style.height.%]=\"barHeight(x)\"></div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @case ('cards') {\n <div data-fx-skel=\"cards\" class=\"grid gap-xl\" [class]=\"gridClass(b)\">\n @for (i of items(b, 6); track i) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"flex items-center gap-normal\">\n <div class=\"skeleton-load w-9 h-9 rounded-lg shrink-0\"></div>\n <div class=\"flex flex-col gap-small flex-1 min-w-0\">\n <div class=\"skeleton-load h-4 w-3/4\"></div>\n <div class=\"skeleton-load h-3 w-1/2\"></div>\n </div>\n <div class=\"skeleton-load h-5 w-14 rounded-full shrink-0\"></div>\n </div>\n <div class=\"grid grid-cols-4 gap-normal pt-normal border-t border-border-subtle\">\n @for (m of [0, 1, 2, 3]; track m) {\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-2.5 w-full\"></div>\n <div class=\"skeleton-load h-4 w-2/3\"></div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @case ('card') {\n <div data-fx-skel=\"card\" class=\"card-common flex flex-col gap-large\">\n <div class=\"skeleton-load h-4 w-1/3\"></div>\n <div class=\"flex flex-col gap-normal\">\n @for (r of [0, 1, 2, 3]; track r) {\n <div class=\"skeleton-load h-3\" [style.width.%]=\"cellWidth(r, 1)\"></div>\n }\n </div>\n </div>\n }\n\n @case ('tabs') {\n <div data-fx-skel=\"tabs\" class=\"flex items-center gap-large border-b border-border-default pb-normal\">\n @for (t of items(b, 6); track t) {\n <div class=\"flex items-center gap-small\">\n <div class=\"skeleton-load w-4 h-4\"></div>\n <div class=\"skeleton-load h-3 w-16\"></div>\n </div>\n }\n </div>\n }\n\n @case ('facets') {\n <div data-fx-skel=\"facets\" class=\"fx-rs-row\">\n <div class=\"fx-rs-facets card-common overflow-hidden\">\n <fx-ui-skeleton-facets />\n </div>\n <div class=\"fx-rs-list card-common overflow-hidden\">\n <fx-ui-skeleton-list [rows]=\"6\" />\n </div>\n <div class=\"flex-1 min-w-0 overflow-hidden\">\n <fx-ui-skeleton-detail />\n </div>\n </div>\n }\n\n @case ('detail') {\n <fx-ui-skeleton-detail data-fx-skel=\"detail\" />\n }\n\n @case ('form') {\n <div data-fx-skel=\"form\" class=\"card-common flex flex-col gap-large\">\n @for (r of [0, 1, 2, 3]; track r) {\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-3 w-24\"></div>\n <div class=\"skeleton-load h-9 w-full rounded-lg\"></div>\n </div>\n }\n <div class=\"flex justify-end gap-normal\">\n <div class=\"skeleton-load h-9 w-24 rounded-lg\"></div>\n <div class=\"skeleton-load h-9 w-28 rounded-lg\"></div>\n </div>\n </div>\n }\n }\n }\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-rs}.fx-rs-row{display:flex;flex-direction:column;gap:var(--spacing-xl, 1.5rem)}@container fx-rs (min-width: 968px){.fx-rs-row{flex-direction:row;align-items:stretch;height:calc(100vh - 300px);min-height:420px}.fx-rs-facets{flex:0 1 230px;min-width:200px;max-width:260px}.fx-rs-list{flex:0 1 420px;min-width:320px;max-width:420px}}\n"], dependencies: [{ kind: "component", type: SkeletonListComponent, selector: "fx-ui-skeleton-list", inputs: ["rows"] }, { kind: "component", type: SkeletonDetailComponent, selector: "fx-ui-skeleton-detail", inputs: ["cards", "rows"] }, { kind: "component", type: SkeletonFacetsComponent, selector: "fx-ui-skeleton-facets", inputs: ["groups", "rows"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5249
+ }
5250
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: RouteSkeletonComponent, decorators: [{
5251
+ type: Component,
5252
+ args: [{ selector: 'fx-ui-route-skeleton', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SkeletonListComponent, SkeletonDetailComponent, SkeletonFacetsComponent], template: "<div class=\"w-full flex flex-col gap-xl\">\n @for (b of blocks(); track $index) {\n @switch (b.kind) {\n @case ('heading') {\n <div data-fx-skel=\"heading\" class=\"flex flex-col md:flex-row md:items-start justify-between gap-normal\">\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-7 w-64\"></div>\n <div class=\"skeleton-load h-3 w-32\"></div>\n </div>\n @if (items(b, 2).length) {\n <div class=\"flex items-center gap-normal\">\n @for (a of items(b, 2); track a) {\n <div class=\"skeleton-load h-9 rounded-lg\" [class]=\"a === 0 ? 'w-28' : 'w-32'\"></div>\n }\n </div>\n }\n </div>\n }\n\n @case ('toolbar') {\n <div data-fx-skel=\"toolbar\" class=\"flex flex-wrap items-center gap-normal\">\n <div class=\"skeleton-load h-9 w-72 rounded-lg\"></div>\n @for (f of items(b, 2); track f) {\n <div class=\"skeleton-load h-7 rounded-full\" [class]=\"f % 2 ? 'w-24' : 'w-20'\"></div>\n }\n </div>\n }\n\n @case ('table') {\n <!-- B\u1EA3ng: m\u1ED7i \u00D4 l\u00E0 m\u1ED9t `.skeleton-load` ri\u00EAng, \u0111\u00FAng c\u00E1ch\n `fx-ui-table-cell [loading]` l\u00E0m tr\u00EAn b\u1EA3ng th\u1EADt. -->\n <div data-fx-skel=\"table\" class=\"card-common overflow-x-auto\">\n <table class=\"w-full\">\n <thead>\n <tr class=\"border-b border-border-default\">\n @for (c of cols(b); track c) {\n <th class=\"px-normal py-semi text-left\">\n <div class=\"skeleton-load h-3\" [style.width.%]=\"headWidth(c)\"></div>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (r of rows(b); track r) {\n <tr class=\"border-b border-border-subtle\">\n @for (c of cols(b); track c) {\n <td class=\"px-normal py-semi align-middle\">\n <div class=\"skeleton-load h-4\" [style.width.%]=\"cellWidth(r, c)\"></div>\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n @case ('stat-cards') {\n <div data-fx-skel=\"stat-cards\" class=\"grid gap-normal grid-cols-2 md:grid-cols-4\">\n @for (i of items(b, 4); track i) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"skeleton-load h-3 w-16\"></div>\n <div class=\"skeleton-load h-8 w-24\"></div>\n <div class=\"skeleton-load h-3 w-20\"></div>\n </div>\n }\n </div>\n }\n\n @case ('kpi-strip') {\n <!-- M\u1ED8T card chia c\u1ED9t (ki\u1EC3u app-dash-kpi-row), kh\u00F4ng ph\u1EA3i c\u00E1c tile r\u1EDDi -->\n <div data-fx-skel=\"kpi-strip\" class=\"card-common !p-0\">\n <div class=\"grid grid-cols-2 md:grid-cols-4\">\n @for (i of items(b, 4); track i) {\n <div\n class=\"flex flex-col gap-normal px-xl py-large border-border-subtle\"\n [class.border-l]=\"!$first\"\n >\n <div class=\"skeleton-load h-3 w-20\"></div>\n <div class=\"skeleton-load h-9 w-28\"></div>\n <div class=\"skeleton-load h-3 w-24\"></div>\n </div>\n }\n </div>\n </div>\n }\n\n @case ('panels') {\n <div data-fx-skel=\"panels\" class=\"grid gap-xl\" [class]=\"gridClass(b)\">\n @for (i of items(b, 2); track i) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"skeleton-load h-4 w-1/3\"></div>\n <div class=\"flex items-end gap-normal h-40\">\n @for (x of bars; track x) {\n <div class=\"skeleton-load flex-1 rounded-t\" [style.height.%]=\"barHeight(x)\"></div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @case ('cards') {\n <div data-fx-skel=\"cards\" class=\"grid gap-xl\" [class]=\"gridClass(b)\">\n @for (i of items(b, 6); track i) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"flex items-center gap-normal\">\n <div class=\"skeleton-load w-9 h-9 rounded-lg shrink-0\"></div>\n <div class=\"flex flex-col gap-small flex-1 min-w-0\">\n <div class=\"skeleton-load h-4 w-3/4\"></div>\n <div class=\"skeleton-load h-3 w-1/2\"></div>\n </div>\n <div class=\"skeleton-load h-5 w-14 rounded-full shrink-0\"></div>\n </div>\n <div class=\"grid grid-cols-4 gap-normal pt-normal border-t border-border-subtle\">\n @for (m of [0, 1, 2, 3]; track m) {\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-2.5 w-full\"></div>\n <div class=\"skeleton-load h-4 w-2/3\"></div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @case ('card') {\n <div data-fx-skel=\"card\" class=\"card-common flex flex-col gap-large\">\n <div class=\"skeleton-load h-4 w-1/3\"></div>\n <div class=\"flex flex-col gap-normal\">\n @for (r of [0, 1, 2, 3]; track r) {\n <div class=\"skeleton-load h-3\" [style.width.%]=\"cellWidth(r, 1)\"></div>\n }\n </div>\n </div>\n }\n\n @case ('tabs') {\n <div data-fx-skel=\"tabs\" class=\"flex items-center gap-large border-b border-border-default pb-normal\">\n @for (t of items(b, 6); track t) {\n <div class=\"flex items-center gap-small\">\n <div class=\"skeleton-load w-4 h-4\"></div>\n <div class=\"skeleton-load h-3 w-16\"></div>\n </div>\n }\n </div>\n }\n\n @case ('facets') {\n <div data-fx-skel=\"facets\" class=\"fx-rs-row\">\n <div class=\"fx-rs-facets card-common overflow-hidden\">\n <fx-ui-skeleton-facets />\n </div>\n <div class=\"fx-rs-list card-common overflow-hidden\">\n <fx-ui-skeleton-list [rows]=\"6\" />\n </div>\n <div class=\"flex-1 min-w-0 overflow-hidden\">\n <fx-ui-skeleton-detail />\n </div>\n </div>\n }\n\n @case ('detail') {\n <fx-ui-skeleton-detail data-fx-skel=\"detail\" />\n }\n\n @case ('form') {\n <div data-fx-skel=\"form\" class=\"card-common flex flex-col gap-large\">\n @for (r of [0, 1, 2, 3]; track r) {\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-3 w-24\"></div>\n <div class=\"skeleton-load h-9 w-full rounded-lg\"></div>\n </div>\n }\n <div class=\"flex justify-end gap-normal\">\n <div class=\"skeleton-load h-9 w-24 rounded-lg\"></div>\n <div class=\"skeleton-load h-9 w-28 rounded-lg\"></div>\n </div>\n </div>\n }\n }\n }\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-rs}.fx-rs-row{display:flex;flex-direction:column;gap:var(--spacing-xl, 1.5rem)}@container fx-rs (min-width: 968px){.fx-rs-row{flex-direction:row;align-items:stretch;height:calc(100vh - 300px);min-height:420px}.fx-rs-facets{flex:0 1 230px;min-width:200px;max-width:260px}.fx-rs-list{flex:0 1 420px;min-width:320px;max-width:420px}}\n"] }]
5253
+ }], propDecorators: { hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }] } });
5254
+ function range$1(n) {
5255
+ return Array.from({ length: Math.max(0, Number(n) || 0) }, (_, i) => i);
5256
+ }
5257
+
5258
+ /**
5259
+ * Skeleton của cả AppFrame (drawer + topbar + vùng nội dung).
5260
+ *
5261
+ * Chỉ dùng cho giai đoạn **bootstrap**, khi shell của host còn CHƯA tồn tại
5262
+ * (App.ngOnInit đang kiểm tenant + session, router chưa initialNavigation).
5263
+ * Mọi điều hướng SAU đó đã có shell thật → dùng `fx-ui-route-skeleton` bên
5264
+ * trong vùng nội dung, đừng vẽ lại cả frame.
5265
+ *
5266
+ * ⚠️ `contentHint` là thứ làm F5 không còn vẽ hình generic. Lần điều hướng ĐẦU
5267
+ * tạo ra chính `HomeComponent` — nơi chứa `fx-ui-route-skeleton` — nên vùng nội
5268
+ * dung không thể tự vẽ skeleton của route ở lần đó. Truyền hint vào đây để
5269
+ * frame dựng đúng composition của màn đích ngay từ F5.
5270
+ */
5271
+ class SkeletonFrameComponent {
5272
+ navItems = input(7, ...(ngDevMode ? [{ debugName: "navItems" }] : /* istanbul ignore next */ []));
5273
+ /** Composition của route đích; null thì vẽ khối generic. */
5274
+ contentHint = input(null, ...(ngDevMode ? [{ debugName: "contentHint" }] : /* istanbul ignore next */ []));
5275
+ navList = computed(() => range(this.navItems()), ...(ngDevMode ? [{ debugName: "navList" }] : /* istanbul ignore next */ []));
5276
+ navWidth(i) {
5277
+ return [70, 55, 62, 48, 66, 52, 58][i % 7];
5278
+ }
5279
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5280
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SkeletonFrameComponent, isStandalone: true, selector: "fx-ui-skeleton-frame", inputs: { navItems: { classPropertyName: "navItems", publicName: "navItems", isSignal: true, isRequired: false, transformFunction: null }, contentHint: { classPropertyName: "contentHint", publicName: "contentHint", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"flex h-screen w-full overflow-hidden bg-bg-primary\">\n <!-- drawer -->\n <div\n class=\"hidden md:flex w-[260px] shrink-0 flex-col gap-xl border-r border-border-subtle p-semi\"\n >\n <div class=\"flex items-center gap-normal\">\n <div class=\"w-6 h-6 rounded skeleton-load shrink-0\"></div>\n <div class=\"flex flex-col gap-small flex-1 min-w-0\">\n <div class=\"h-3.5 w-20 rounded skeleton-load\"></div>\n <div class=\"h-2.5 w-full rounded skeleton-load\"></div>\n </div>\n </div>\n\n <div class=\"flex flex-col gap-small\">\n @for (n of navList(); track n) {\n <div class=\"flex items-center gap-normal py-1\">\n <div class=\"w-4 h-4 rounded skeleton-load shrink-0\"></div>\n <div class=\"h-3 rounded skeleton-load\" [style.width.%]=\"navWidth(n)\"></div>\n </div>\n }\n </div>\n\n <div class=\"mt-auto flex items-center gap-normal border-t border-border-subtle pt-semi\">\n <div class=\"w-8 h-8 rounded-full skeleton-load shrink-0\"></div>\n <div class=\"flex flex-col gap-small flex-1 min-w-0\">\n <div class=\"h-3 w-24 rounded skeleton-load\"></div>\n <div class=\"h-2.5 w-32 rounded skeleton-load\"></div>\n </div>\n </div>\n </div>\n\n <div class=\"flex flex-1 min-w-0 flex-col\">\n <!-- topbar -->\n <div\n class=\"flex h-14 shrink-0 items-center gap-normal border-b border-border-subtle px-semi\"\n >\n <div class=\"h-3.5 w-40 rounded skeleton-load\"></div>\n <div class=\"ml-auto h-8 w-32 rounded-lg skeleton-load\"></div>\n </div>\n\n <!-- content: d\u1EF1ng \u0111\u00FAng composition c\u1EE7a route \u0111\u00EDch n\u1EBFu \u0111\u00E3 bi\u1EBFt -->\n <div class=\"flex-1 min-h-0 overflow-hidden p-semi\">\n @if (contentHint(); as hint) {\n <fx-ui-route-skeleton [hint]=\"hint\" />\n } @else {\n <div class=\"flex flex-col gap-xl\">\n <div class=\"h-6 w-1/4 rounded skeleton-load\"></div>\n <div class=\"grid gap-xl grid-cols-2 md:grid-cols-4\">\n <div class=\"h-24 rounded-lg skeleton-load\"></div>\n <div class=\"h-24 rounded-lg skeleton-load\"></div>\n <div class=\"h-24 rounded-lg skeleton-load\"></div>\n <div class=\"h-24 rounded-lg skeleton-load\"></div>\n </div>\n <div class=\"h-64 rounded-lg skeleton-load\"></div>\n </div>\n }\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "component", type: RouteSkeletonComponent, selector: "fx-ui-route-skeleton", inputs: ["hint"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5281
+ }
5282
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonFrameComponent, decorators: [{
5283
+ type: Component,
5284
+ args: [{ selector: 'fx-ui-skeleton-frame', changeDetection: ChangeDetectionStrategy.OnPush, imports: [RouteSkeletonComponent], template: "<div class=\"flex h-screen w-full overflow-hidden bg-bg-primary\">\n <!-- drawer -->\n <div\n class=\"hidden md:flex w-[260px] shrink-0 flex-col gap-xl border-r border-border-subtle p-semi\"\n >\n <div class=\"flex items-center gap-normal\">\n <div class=\"w-6 h-6 rounded skeleton-load shrink-0\"></div>\n <div class=\"flex flex-col gap-small flex-1 min-w-0\">\n <div class=\"h-3.5 w-20 rounded skeleton-load\"></div>\n <div class=\"h-2.5 w-full rounded skeleton-load\"></div>\n </div>\n </div>\n\n <div class=\"flex flex-col gap-small\">\n @for (n of navList(); track n) {\n <div class=\"flex items-center gap-normal py-1\">\n <div class=\"w-4 h-4 rounded skeleton-load shrink-0\"></div>\n <div class=\"h-3 rounded skeleton-load\" [style.width.%]=\"navWidth(n)\"></div>\n </div>\n }\n </div>\n\n <div class=\"mt-auto flex items-center gap-normal border-t border-border-subtle pt-semi\">\n <div class=\"w-8 h-8 rounded-full skeleton-load shrink-0\"></div>\n <div class=\"flex flex-col gap-small flex-1 min-w-0\">\n <div class=\"h-3 w-24 rounded skeleton-load\"></div>\n <div class=\"h-2.5 w-32 rounded skeleton-load\"></div>\n </div>\n </div>\n </div>\n\n <div class=\"flex flex-1 min-w-0 flex-col\">\n <!-- topbar -->\n <div\n class=\"flex h-14 shrink-0 items-center gap-normal border-b border-border-subtle px-semi\"\n >\n <div class=\"h-3.5 w-40 rounded skeleton-load\"></div>\n <div class=\"ml-auto h-8 w-32 rounded-lg skeleton-load\"></div>\n </div>\n\n <!-- content: d\u1EF1ng \u0111\u00FAng composition c\u1EE7a route \u0111\u00EDch n\u1EBFu \u0111\u00E3 bi\u1EBFt -->\n <div class=\"flex-1 min-h-0 overflow-hidden p-semi\">\n @if (contentHint(); as hint) {\n <fx-ui-route-skeleton [hint]=\"hint\" />\n } @else {\n <div class=\"flex flex-col gap-xl\">\n <div class=\"h-6 w-1/4 rounded skeleton-load\"></div>\n <div class=\"grid gap-xl grid-cols-2 md:grid-cols-4\">\n <div class=\"h-24 rounded-lg skeleton-load\"></div>\n <div class=\"h-24 rounded-lg skeleton-load\"></div>\n <div class=\"h-24 rounded-lg skeleton-load\"></div>\n <div class=\"h-24 rounded-lg skeleton-load\"></div>\n </div>\n <div class=\"h-64 rounded-lg skeleton-load\"></div>\n </div>\n }\n </div>\n </div>\n</div>\n" }]
5285
+ }], propDecorators: { navItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "navItems", required: false }] }], contentHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "contentHint", required: false }] }] } });
5286
+ function range(n) {
5287
+ return Array.from({ length: Math.max(0, Number(n) || 0) }, (_, i) => i);
5288
+ }
5289
+
4833
5290
  const FX_COMPONENTS = [
4834
5291
  InputComponent,
4835
5292
  SelectComponent,
@@ -4849,7 +5306,6 @@ const FX_COMPONENTS = [
4849
5306
  TagComponent,
4850
5307
  SliderComponent,
4851
5308
  SwitchComponent,
4852
- SkeletonTableLoadingComponent,
4853
5309
  CircleProgressBar,
4854
5310
  TreeDiagram,
4855
5311
  TableCell,
@@ -4882,6 +5338,10 @@ const FX_COMPONENTS = [
4882
5338
  FacetsRailComponent,
4883
5339
  ProjectCardComponent,
4884
5340
  PipelineStepperComponent,
5341
+ SkeletonFacetsComponent,
5342
+ SkeletonDashboardComponent,
5343
+ SkeletonFrameComponent,
5344
+ RouteSkeletonComponent,
4885
5345
  ];
4886
5346
 
4887
5347
  const DIALOGS_COMPONENT = [
@@ -4905,7 +5365,7 @@ class UiModule {
4905
5365
  ReactiveFormsModule,
4906
5366
  RouterModule,
4907
5367
  HasPermissionDirective,
4908
- TrimOnBlurDirective, InputComponent, SelectComponent, RadioButtonComponent, CheckboxComponent, DndUploadComponent, ButtonComponent, RadioButtonToggleComponent, DatetimePicker, LoadingPanel, SearchBarComponent, TabGroupComponent, TabComponent, HeroIconComponent, ToastComponent, ToastContainerComponent, TagComponent, SliderComponent, SwitchComponent, SkeletonTableLoadingComponent, 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, ConfirmationDialogComponent], exports: [InputComponent, SelectComponent, RadioButtonComponent, CheckboxComponent, DndUploadComponent, ButtonComponent, RadioButtonToggleComponent, DatetimePicker, LoadingPanel, SearchBarComponent, TabGroupComponent, TabComponent, HeroIconComponent, ToastComponent, ToastContainerComponent, TagComponent, SliderComponent, SwitchComponent, SkeletonTableLoadingComponent, 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, ConfirmationDialogComponent, FormsModule,
5368
+ TrimOnBlurDirective, InputComponent, SelectComponent, RadioButtonComponent, CheckboxComponent, DndUploadComponent, ButtonComponent, RadioButtonToggleComponent, DatetimePicker, LoadingPanel, SearchBarComponent, TabGroupComponent, TabComponent, HeroIconComponent, ToastComponent, ToastContainerComponent, TagComponent, SliderComponent, SwitchComponent, CircleProgressBar, TreeDiagram, TableCell, KanbanBoardComponent, DrawerComponent, SkeletonListComponent, SkeletonDetailComponent, StatCardsComponent, EmptyStateComponent, FilterChipsComponent, FilterPillsComponent, StageDotsComponent, CodeBlockComponent, ScreenshotFrameComponent, MeterComponent, SectionCardComponent, InfoFieldComponent, SeverityBadgesComponent, MasterDetailComponent, MenuComponent, ThemePickerComponent, PendingBadgeComponent, SampleFrameComponent, ScoreBadgeComponent, StatusDotComponent, SparkbarComponent, SegmentedComponent, ListRowComponent, BarListComponent, FacetsRailComponent, ProjectCardComponent, PipelineStepperComponent, SkeletonFacetsComponent, SkeletonDashboardComponent, SkeletonFrameComponent, RouteSkeletonComponent, ConfirmationDialogComponent], exports: [InputComponent, SelectComponent, RadioButtonComponent, CheckboxComponent, DndUploadComponent, ButtonComponent, RadioButtonToggleComponent, DatetimePicker, LoadingPanel, SearchBarComponent, TabGroupComponent, TabComponent, HeroIconComponent, ToastComponent, ToastContainerComponent, TagComponent, SliderComponent, SwitchComponent, CircleProgressBar, TreeDiagram, TableCell, KanbanBoardComponent, DrawerComponent, SkeletonListComponent, SkeletonDetailComponent, StatCardsComponent, EmptyStateComponent, FilterChipsComponent, FilterPillsComponent, StageDotsComponent, CodeBlockComponent, ScreenshotFrameComponent, MeterComponent, SectionCardComponent, InfoFieldComponent, SeverityBadgesComponent, MasterDetailComponent, MenuComponent, ThemePickerComponent, PendingBadgeComponent, SampleFrameComponent, ScoreBadgeComponent, StatusDotComponent, SparkbarComponent, SegmentedComponent, ListRowComponent, BarListComponent, FacetsRailComponent, ProjectCardComponent, PipelineStepperComponent, SkeletonFacetsComponent, SkeletonDashboardComponent, SkeletonFrameComponent, RouteSkeletonComponent, ConfirmationDialogComponent, FormsModule,
4909
5369
  ReactiveFormsModule,
4910
5370
  RouterModule,
4911
5371
  HasPermissionDirective,
@@ -4948,5 +5408,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
4948
5408
  * Generated bundle index. Do not edit.
4949
5409
  */
4950
5410
 
4951
- export { AuthInterceptor, AuthStateService, BarListComponent, BaseComponent, BaseDialogComponent, BaseMasterDetailComponent, BaseResolver, BaseTableComponent, BreadcrumbService, ButtonComponent, CheckboxComponent, CircleProgressBar, CodeBlockComponent, ConfirmationDialogComponent, DatetimePicker, DndUploadComponent, DrawerComponent, EmptyStateComponent, FX_ICON_SPRITE_URL, FacetsRailComponent, FilterChipsComponent, FilterPillsComponent, FxComponent, FxDialogService, FxLoadingService, 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, SampleFrameComponent, ScoreBadgeComponent, ScreenshotFrameComponent, SearchBarComponent, SectionCardComponent, SegmentedComponent, SelectComponent, SeverityBadgesComponent, SkeletonDetailComponent, SkeletonListComponent, SkeletonTableLoadingComponent, 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, toneBg, toneColor, toneText };
5411
+ export { AuthInterceptor, AuthStateService, BarListComponent, BaseComponent, BaseDialogComponent, BaseMasterDetailComponent, BaseTableComponent, BreadcrumbService, ButtonComponent, CheckboxComponent, CircleProgressBar, CodeBlockComponent, ConfirmationDialogComponent, DatetimePicker, DndUploadComponent, DrawerComponent, EmptyStateComponent, FX_ICON_SPRITE_URL, FX_SKELETON_DATA_KEY, FacetsRailComponent, FilterChipsComponent, FilterPillsComponent, FxComponent, FxDialogService, FxLoadingService, FxRouteSkeletonService, FxStorageService, FxToastrService, FxUtils, FxValidators, HasPermissionDirective, HeroIconComponent, HttpLoaderFactory, HttpWrapper, InfoFieldComponent, InputComponent, KanbanBoardComponent, ListRowComponent, LoadingPanel, MasterDetailComponent, MenuComponent, MeterComponent, NotificationService, OG_LIGHT_THEMES, OG_PICKER_THEMES, OG_PICKER_THEME_META, OG_THEMES, OG_THEME_META, PendingBadgeComponent, PermissionGuard, PermissionService, PipelineStepperComponent, ProjectCardComponent, RadioButtonComponent, RadioButtonToggleComponent, RealtimeTableSync, RouteSkeletonComponent, SampleFrameComponent, ScoreBadgeComponent, ScreenshotFrameComponent, SearchBarComponent, SectionCardComponent, SegmentedComponent, SelectComponent, SeverityBadgesComponent, SkeletonDashboardComponent, SkeletonDetailComponent, SkeletonFacetsComponent, SkeletonFrameComponent, SkeletonListComponent, SliderComponent, SparkbarComponent, StageDotsComponent, StatCardsComponent, StatusDotComponent, SwitchComponent, TabComponent, TabGroupComponent, TableCell, TagComponent, ThemePickerComponent, ThemeService, ToastComponent, ToastContainerComponent, TranslationModule, TranslationService, TreeDiagram, TrimOnBlurDirective, UiModule, anyFeatureEnabled, countTone, createApi, featureEnabled, fxListResolver, scoreTone, severityTone, toneBadge, toneBadgeSolid, toneBg, toneBorder, toneColor, toneDot, toneDotChart, toneFg, toneScale, toneText, toneTint };
4952
5412
  //# sourceMappingURL=foxeltech-angular-ui.mjs.map